From 4e3763f17f1efd75e139ce229cdd1730e4406d51 Mon Sep 17 00:00:00 2001 From: Juhwan Kim <13selfesteem91@naver.com> Date: Thu, 10 Oct 2024 17:17:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20v2.1.0=20=EC=98=81=EC=88=98=EC=A6=9D=20?= =?UTF-8?q?=EC=B2=A8=EB=B6=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: CI/CD 스크립트 정상화 (#706) * feat: workflow 복구 * test: dev 워크플로우 테스트 * test: 잘못된 yaml 형식 수정 * test: 설정 서브모듈 추가 * fix: eof 추가 * feat: 이미지 업로드 경로 업데이트 * feat: 은행 이름 수정 (#712) * feat: BaseEntity 적용 (#700) * feat: 영수증 이미지 업로드 기능 추가 (#714) * feat: gitignore 추가 * feat: 이미지 업로드/다운로드 기능 추가 * feat: 이미지 업로드/다운로드 기능 추가 * feat: 운영, 개발 image 설정 분리 * feat: 서브모듈 삭제 * test: imageUploadService 추가 * test: 행사 이미지 저장 및 조회 기능 테스트 * feat: 이미지 삭제 기능 구현 * test: 이미지 삭제 테스트 추가 --------- Co-authored-by: juha * docs: adoc 수정 (#722) * chore: 테스트용 트리거 브랜치 삭제 (#726) --------- Co-authored-by: juha <84626225+khabh@users.noreply.github.com> Co-authored-by: Arachne <66822642+Arachneee@users.noreply.github.com> Co-authored-by: juha --- .github/workflows/backend-dev.yml | 72 +++++++++++++++++ .github/workflows/backend-prod.yml | 77 +++++++++++++++++++ .github/workflows/backend-pull-request.yml | 44 +++++++++++ server/.gitignore => .gitignore | 2 +- .gitmodules | 3 + server/build.gradle | 2 + server/src/docs/asciidoc/event.adoc | 63 +++++++++++++++ .../haengdong/HaengdongApplication.java | 1 + .../haengdong/application/EventService.java | 47 ++++++++++- .../haengdong/application/ImageService.java | 72 +++++++++++++++++ .../application/response/BillAppResponse.java | 1 + .../response/EventImageAppResponse.java | 7 ++ .../response/ImageNameAppResponse.java | 11 +++ .../response/MemberAppResponse.java | 1 + .../response/MemberDepositAppResponse.java | 1 + .../response/MemberSaveAppResponse.java | 1 + .../response/MembersSaveAppResponse.java | 1 + .../application/response/StepAppResponse.java | 1 + .../server/haengdong/config/JpaConfig.java | 9 +++ .../server/haengdong/config/S3Config.java | 17 ++++ .../server/haengdong/domain/BaseEntity.java | 23 ++++++ .../server/haengdong/domain/bill/Bill.java | 3 +- .../haengdong/domain/bill/BillDetail.java | 3 +- .../server/haengdong/domain/event/Bank.java | 10 +-- .../server/haengdong/domain/event/Event.java | 4 +- .../haengdong/domain/event/EventImage.java | 10 ++- .../domain/event/EventImageRepository.java | 11 +++ .../haengdong/domain/member/Member.java | 3 +- .../exception/HaengdongErrorCode.java | 3 + .../presentation/EventController.java | 9 +++ .../admin/AdminEventController.java | 29 +++++++ .../request/BillDetailUpdateRequest.java | 1 + .../request/BillDetailsUpdateRequest.java | 6 +- .../request/BillUpdateRequest.java | 1 + .../request/EventLoginRequest.java | 1 + .../request/MemberNameUpdateRequest.java | 19 ----- .../request/MemberNamesUpdateRequest.java | 20 ----- .../request/MembersSaveRequest.java | 5 +- .../presentation/response/BillResponse.java | 1 + .../response/EventImageResponse.java | 13 ++++ .../response/EventImagesResponse.java | 15 ++++ .../response/MemberSaveResponse.java | 1 + .../response/MembersSaveResponse.java | 1 + .../presentation/response/StepResponse.java | 2 +- .../presentation/response/StepsResponse.java | 1 + server/src/main/resources/application.yml | 9 +++ server/src/main/resources/config | 1 + .../application/EventServiceTest.java | 66 ++++++++++++++++ .../docs/AdminEventControllerDocsTest.java | 64 ++++++++++++++- .../docs/EventControllerDocsTest.java | 35 +++++++++ .../presentation/ControllerTestSupport.java | 4 + .../presentation/EventControllerTest.java | 18 +++++ .../admin/AdminEventControllerTest.java | 19 +++++ 53 files changed, 788 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/backend-dev.yml create mode 100644 .github/workflows/backend-prod.yml create mode 100644 .github/workflows/backend-pull-request.yml rename server/.gitignore => .gitignore (99%) create mode 100644 .gitmodules create mode 100644 server/src/main/java/server/haengdong/application/ImageService.java create mode 100644 server/src/main/java/server/haengdong/application/response/EventImageAppResponse.java create mode 100644 server/src/main/java/server/haengdong/application/response/ImageNameAppResponse.java create mode 100644 server/src/main/java/server/haengdong/config/JpaConfig.java create mode 100644 server/src/main/java/server/haengdong/config/S3Config.java create mode 100644 server/src/main/java/server/haengdong/domain/BaseEntity.java create mode 100644 server/src/main/java/server/haengdong/domain/event/EventImageRepository.java delete mode 100644 server/src/main/java/server/haengdong/presentation/request/MemberNameUpdateRequest.java delete mode 100644 server/src/main/java/server/haengdong/presentation/request/MemberNamesUpdateRequest.java create mode 100644 server/src/main/java/server/haengdong/presentation/response/EventImageResponse.java create mode 100644 server/src/main/java/server/haengdong/presentation/response/EventImagesResponse.java create mode 160000 server/src/main/resources/config diff --git a/.github/workflows/backend-dev.yml b/.github/workflows/backend-dev.yml new file mode 100644 index 000000000..bec561e00 --- /dev/null +++ b/.github/workflows/backend-dev.yml @@ -0,0 +1,72 @@ +name: backend-push + +on: + push: + branches: [ "be-dev" ] + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./server + + permissions: + contents: read + + steps: + - name: CheckOut + uses: actions/checkout@v4 + with: + token: ${{secrets.CONFIG_SUBMODULE_TOKEN}} + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test with Gradle Wrapper + run: ./gradlew clean build + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker BuildX + uses: docker/setup-buildx-action@v3 + + - name: Build and push + run: | + docker buildx build --platform linux/arm64 -t \ + ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_BE_DEV }} --push . + + deploy: + needs: build + runs-on: [ self-hosted, backend-dev ] + steps: + - name: Docker remove + run: | + CONTAINER_IDS=$(sudo docker ps -qa) + if [ -n "$CONTAINER_IDS" ]; then + sudo docker rm -f $CONTAINER_IDS + else + echo "No running containers found." + fi + + - name: Docker Image pull + run: sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_BE_DEV }} + + - name: Docker run + run: sudo docker run -d -p 8080:8080 -e SPRING_PROFILES_ACTIVE=dev -v log-volume:/app/logs --name haengdong-backend ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_BE_DEV }} diff --git a/.github/workflows/backend-prod.yml b/.github/workflows/backend-prod.yml new file mode 100644 index 000000000..dd54042ce --- /dev/null +++ b/.github/workflows/backend-prod.yml @@ -0,0 +1,77 @@ +name: backend-push + +on: + push: + branches: [ "main" ] + paths: + - 'server/**' + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./server + + permissions: + contents: read + + steps: + - name: CheckOut + uses: actions/checkout@v4 + with: + token: ${{secrets.CONFIG_SUBMODULE_TOKEN}} + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test with Gradle Wrapper + run: ./gradlew clean build + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker BuildX + uses: docker/setup-buildx-action@v3 + + - name: Build and push + run: | + docker buildx build --platform linux/arm64 -t \ + ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_BE_PROD }} --push . + + deploy: + needs: build + strategy: + matrix: + runner: [ prod-1, prod-2 ] + runs-on: [ self-hosted, '${{ matrix.runner }}' ] + steps: + - name: Docker remove + run: | + CONTAINER_IDS=$(sudo docker ps -qa) + if [ -n "$CONTAINER_IDS" ]; then + sudo docker rm -f $CONTAINER_IDS + else + echo "No running containers found." + fi + + - name: Docker Image pull + run: sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_BE_PROD }} + + - name: Docker run + run: sudo docker run -d -p 8080:8080 -e SPRING_PROFILES_ACTIVE=prod -v log-volume:/app/logs --name haengdong-backend ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_BE_PROD }} diff --git a/.github/workflows/backend-pull-request.yml b/.github/workflows/backend-pull-request.yml new file mode 100644 index 000000000..df008fd97 --- /dev/null +++ b/.github/workflows/backend-pull-request.yml @@ -0,0 +1,44 @@ +name: backend-pull-request + +on: + pull_request: + branches: [ "main", "be-dev" ] + +jobs: + build: + runs-on: [ ubuntu-latest ] + + defaults: + run: + working-directory: ./server + + steps: + - name: CheckOut + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test with Gradle Wrapper + run: ./gradlew clean build + + - name: publish unit test results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: server/build/test-results/test/TEST-*.xml + + - name: add comments to a pull request + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: server/build/test-results/test/TEST-*.xml diff --git a/server/.gitignore b/.gitignore similarity index 99% rename from server/.gitignore rename to .gitignore index 616fb815a..358a2b7b2 100644 --- a/server/.gitignore +++ b/.gitignore @@ -216,7 +216,7 @@ $RECYCLE.BIN/ *.lnk ### Gradle ### -.gradle +server/.gradle **/build/ !src/**/build/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..be80d0dde --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "server/src/main/resources/config"] + path = server/src/main/resources/config + url = https://github.com/woowacourse-teams/2024-haeng-dong-config diff --git a/server/build.gradle b/server/build.gradle index c2936590a..e28f5d7de 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -41,6 +41,8 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'software.amazon.awssdk:s3:2.25.27' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/server/src/docs/asciidoc/event.adoc b/server/src/docs/asciidoc/event.adoc index 10b165282..282d08cfe 100644 --- a/server/src/docs/asciidoc/event.adoc +++ b/server/src/docs/asciidoc/event.adoc @@ -203,3 +203,66 @@ operation::authenticateEvent[snippets="http-request,http-response,request-cookie } ] ---- + +=== 행사 이미지 업로드 +operation::uploadImages[snippets="http-request,http-response"] + +==== [.red]#Exceptions# + +[source,json,options="nowrap"] +---- +[ + { + "code": "EVENT_NOT_FOUND", + "message": "존재하지 않는 행사입니다." + }, + { + "code": "IMAGE_UPLOAD_FAIL", + "message": "이미지 업로드에 실패했습니다." + }, + { + "code":"TOKEN_NOT_FOUND", + "message":"토큰이 존재하지 않습니다." + }, + { + "code":"TOKEN_EXPIRED", + "message":"만료된 토큰입니다." + }, + { + "code":"TOKEN_INVALID", + "message":"유효하지 않은 토큰입니다." + } +] +---- + +=== 행사 이미지 목록 조회 + +operation::findImages[snippets="http-request,response-body,response-fields,http-response"] + +==== [.red]#Exceptions# + +[source,json,options="nowrap"] +---- +[ + { + "code": "EVENT_NOT_FOUND", + "message": "존재하지 않는 행사입니다." + } +] +---- + +=== 행사 이미지 삭제 + +operation::deleteImage[snippets="http-request,http-response"] + +==== [.red]#Exceptions# + +[source,json,options="nowrap"] +---- +[ + { + "code": "EVENT_NOT_FOUND", + "message": "존재하지 않는 행사입니다." + } +] +---- diff --git a/server/src/main/java/server/haengdong/HaengdongApplication.java b/server/src/main/java/server/haengdong/HaengdongApplication.java index 4a84c6120..f25499e01 100644 --- a/server/src/main/java/server/haengdong/HaengdongApplication.java +++ b/server/src/main/java/server/haengdong/HaengdongApplication.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Slf4j @SpringBootApplication diff --git a/server/src/main/java/server/haengdong/application/EventService.java b/server/src/main/java/server/haengdong/application/EventService.java index af49ebb5a..2eb80f265 100644 --- a/server/src/main/java/server/haengdong/application/EventService.java +++ b/server/src/main/java/server/haengdong/application/EventService.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map.Entry; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import server.haengdong.application.request.EventAppRequest; @@ -10,14 +11,17 @@ import server.haengdong.application.request.EventUpdateAppRequest; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; +import server.haengdong.application.response.EventImageAppResponse; import server.haengdong.application.response.MemberBillReportAppResponse; import server.haengdong.domain.bill.Bill; import server.haengdong.domain.bill.BillRepository; -import server.haengdong.domain.member.Member; import server.haengdong.domain.bill.MemberBillReport; import server.haengdong.domain.event.Event; +import server.haengdong.domain.event.EventImage; +import server.haengdong.domain.event.EventImageRepository; import server.haengdong.domain.event.EventRepository; import server.haengdong.domain.event.EventTokenProvider; +import server.haengdong.domain.member.Member; import server.haengdong.exception.AuthenticationException; import server.haengdong.exception.HaengdongErrorCode; import server.haengdong.exception.HaengdongException; @@ -30,6 +34,10 @@ public class EventService { private final EventRepository eventRepository; private final EventTokenProvider eventTokenProvider; private final BillRepository billRepository; + private final EventImageRepository eventImageRepository; + + @Value("${image.base-url}") + private String baseUrl; @Transactional public EventAppResponse saveEvent(EventAppRequest request) { @@ -92,4 +100,41 @@ public void updateEvent(String token, EventUpdateAppRequest request) { event.changeAccount(request.bankName(), request.accountNumber()); } } + + @Transactional + public void saveImages(String token, List imageNames) { + Event event = getEvent(token); + + List images = imageNames.stream() + .map(imageName -> new EventImage(event, imageName)) + .toList(); + + eventImageRepository.saveAll(images); + } + + public List findImages(String token) { + Event event = getEvent(token); + + return eventImageRepository.findAllByEvent(event) + .stream() + .map(image -> new EventImageAppResponse(image.getId(), createUrl(image))) + .toList(); + } + + private String createUrl(EventImage image) { + return baseUrl + image.getName(); + } + + @Transactional + public String deleteImage(String token, Long imageId) { + EventImage eventImage = eventImageRepository.findById(imageId) + .orElseThrow(() -> new HaengdongException(HaengdongErrorCode.IMAGE_NOT_FOUND)); + + Event event = eventImage.getEvent(); + if (event.isTokenMismatch(token)) { + throw new AuthenticationException(HaengdongErrorCode.PASSWORD_INVALID); + } + eventImageRepository.delete(eventImage); + return eventImage.getName(); + } } diff --git a/server/src/main/java/server/haengdong/application/ImageService.java b/server/src/main/java/server/haengdong/application/ImageService.java new file mode 100644 index 000000000..6e0ffdb76 --- /dev/null +++ b/server/src/main/java/server/haengdong/application/ImageService.java @@ -0,0 +1,72 @@ +package server.haengdong.application; + +import static software.amazon.awssdk.core.sync.RequestBody.fromInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import server.haengdong.application.response.ImageNameAppResponse; +import server.haengdong.exception.HaengdongErrorCode; +import server.haengdong.exception.HaengdongException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ImageService { + + @Value("${image.bucket}") + private String bucketName; + + @Value("${image.directory}") + private String directoryPath; + + private final S3Client s3Client; + + public List uploadImages(List images) { + return images.stream() + .map(this::uploadImage) + .toList(); + } + + private String uploadImage(MultipartFile image) { + try (InputStream inputStream = image.getInputStream()) { + return uploadImageToStorage(inputStream, image); + } catch (IOException e) { + throw new HaengdongException(HaengdongErrorCode.IMAGE_UPLOAD_FAIL); + } + } + + private String uploadImageToStorage(InputStream inputStream, MultipartFile image) { + String imageName = UUID.randomUUID() + image.getOriginalFilename(); + String key = directoryPath + imageName; + long contentLength = image.getSize(); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentLength(contentLength) + .contentType(image.getContentType()) + .build(); + + s3Client.putObject(putObjectRequest, fromInputStream(inputStream, contentLength)); + return imageName; + } + + public void deleteImage(String imageName) { + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(directoryPath + imageName) + .build(); + + s3Client.deleteObject(deleteObjectRequest); + } +} diff --git a/server/src/main/java/server/haengdong/application/response/BillAppResponse.java b/server/src/main/java/server/haengdong/application/response/BillAppResponse.java index 8a5a5b2bc..6ee68935c 100644 --- a/server/src/main/java/server/haengdong/application/response/BillAppResponse.java +++ b/server/src/main/java/server/haengdong/application/response/BillAppResponse.java @@ -8,6 +8,7 @@ public record BillAppResponse( Long price, boolean isFixed ) { + public static BillAppResponse of(Bill bill) { return new BillAppResponse(bill.getId(), bill.getTitle(), bill.getPrice(), bill.isFixed()); } diff --git a/server/src/main/java/server/haengdong/application/response/EventImageAppResponse.java b/server/src/main/java/server/haengdong/application/response/EventImageAppResponse.java new file mode 100644 index 000000000..26e442403 --- /dev/null +++ b/server/src/main/java/server/haengdong/application/response/EventImageAppResponse.java @@ -0,0 +1,7 @@ +package server.haengdong.application.response; + +public record EventImageAppResponse( + Long id, + String url +) { +} diff --git a/server/src/main/java/server/haengdong/application/response/ImageNameAppResponse.java b/server/src/main/java/server/haengdong/application/response/ImageNameAppResponse.java new file mode 100644 index 000000000..2e6efaceb --- /dev/null +++ b/server/src/main/java/server/haengdong/application/response/ImageNameAppResponse.java @@ -0,0 +1,11 @@ +package server.haengdong.application.response; + +import server.haengdong.domain.event.Event; +import server.haengdong.domain.event.EventImage; + +public record ImageNameAppResponse(String name) { + + public EventImage toEventImage(Event event) { + return new EventImage(event, name); + } +} diff --git a/server/src/main/java/server/haengdong/application/response/MemberAppResponse.java b/server/src/main/java/server/haengdong/application/response/MemberAppResponse.java index b253ea697..98579b2d1 100644 --- a/server/src/main/java/server/haengdong/application/response/MemberAppResponse.java +++ b/server/src/main/java/server/haengdong/application/response/MemberAppResponse.java @@ -6,6 +6,7 @@ public record MemberAppResponse( Long id, String name ) { + public static MemberAppResponse of(Member member) { return new MemberAppResponse(member.getId(), member.getName()); } diff --git a/server/src/main/java/server/haengdong/application/response/MemberDepositAppResponse.java b/server/src/main/java/server/haengdong/application/response/MemberDepositAppResponse.java index 94dd77117..bb09e02df 100644 --- a/server/src/main/java/server/haengdong/application/response/MemberDepositAppResponse.java +++ b/server/src/main/java/server/haengdong/application/response/MemberDepositAppResponse.java @@ -7,6 +7,7 @@ public record MemberDepositAppResponse( String name, boolean isDeposited ) { + public static MemberDepositAppResponse of(Member member) { return new MemberDepositAppResponse(member.getId(), member.getName(), member.isDeposited()); } diff --git a/server/src/main/java/server/haengdong/application/response/MemberSaveAppResponse.java b/server/src/main/java/server/haengdong/application/response/MemberSaveAppResponse.java index 1d08536ba..bd1d117f2 100644 --- a/server/src/main/java/server/haengdong/application/response/MemberSaveAppResponse.java +++ b/server/src/main/java/server/haengdong/application/response/MemberSaveAppResponse.java @@ -6,6 +6,7 @@ public record MemberSaveAppResponse( Long id, String name ) { + public static MemberSaveAppResponse of(Member member) { return new MemberSaveAppResponse(member.getId(), member.getName()); } diff --git a/server/src/main/java/server/haengdong/application/response/MembersSaveAppResponse.java b/server/src/main/java/server/haengdong/application/response/MembersSaveAppResponse.java index e171f9950..c81fe0996 100644 --- a/server/src/main/java/server/haengdong/application/response/MembersSaveAppResponse.java +++ b/server/src/main/java/server/haengdong/application/response/MembersSaveAppResponse.java @@ -6,6 +6,7 @@ public record MembersSaveAppResponse( List members ) { + public static MembersSaveAppResponse of(List members) { return new MembersSaveAppResponse( members.stream() diff --git a/server/src/main/java/server/haengdong/application/response/StepAppResponse.java b/server/src/main/java/server/haengdong/application/response/StepAppResponse.java index 2537d1db1..57ca1860c 100644 --- a/server/src/main/java/server/haengdong/application/response/StepAppResponse.java +++ b/server/src/main/java/server/haengdong/application/response/StepAppResponse.java @@ -7,6 +7,7 @@ public record StepAppResponse( List bills, List members ) { + public static StepAppResponse of(Step step) { List billAppResponses = step.getBills().stream() .map(BillAppResponse::of) diff --git a/server/src/main/java/server/haengdong/config/JpaConfig.java b/server/src/main/java/server/haengdong/config/JpaConfig.java new file mode 100644 index 000000000..f0210e4b0 --- /dev/null +++ b/server/src/main/java/server/haengdong/config/JpaConfig.java @@ -0,0 +1,9 @@ +package server.haengdong.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/server/src/main/java/server/haengdong/config/S3Config.java b/server/src/main/java/server/haengdong/config/S3Config.java new file mode 100644 index 000000000..13aa51546 --- /dev/null +++ b/server/src/main/java/server/haengdong/config/S3Config.java @@ -0,0 +1,17 @@ +package server.haengdong.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class S3Config { + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.AP_NORTHEAST_2) + .build(); + } +} diff --git a/server/src/main/java/server/haengdong/domain/BaseEntity.java b/server/src/main/java/server/haengdong/domain/BaseEntity.java new file mode 100644 index 000000000..2eb4aa01c --- /dev/null +++ b/server/src/main/java/server/haengdong/domain/BaseEntity.java @@ -0,0 +1,23 @@ +package server.haengdong.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.Instant; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public abstract class BaseEntity { + + @Column(updatable = false) + @CreatedDate + private Instant createdAt; + + @LastModifiedDate + private Instant updatedAt; +} diff --git a/server/src/main/java/server/haengdong/domain/bill/Bill.java b/server/src/main/java/server/haengdong/domain/bill/Bill.java index 6face8274..747a526e0 100644 --- a/server/src/main/java/server/haengdong/domain/bill/Bill.java +++ b/server/src/main/java/server/haengdong/domain/bill/Bill.java @@ -19,6 +19,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import server.haengdong.domain.BaseEntity; import server.haengdong.domain.member.Member; import server.haengdong.domain.event.Event; import server.haengdong.exception.HaengdongErrorCode; @@ -27,7 +28,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class Bill { +public class Bill extends BaseEntity { private static final int MIN_TITLE_LENGTH = 1; private static final int MAX_TITLE_LENGTH = 30; diff --git a/server/src/main/java/server/haengdong/domain/bill/BillDetail.java b/server/src/main/java/server/haengdong/domain/bill/BillDetail.java index 6f3bcf973..8a754b2b9 100644 --- a/server/src/main/java/server/haengdong/domain/bill/BillDetail.java +++ b/server/src/main/java/server/haengdong/domain/bill/BillDetail.java @@ -11,12 +11,13 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import server.haengdong.domain.BaseEntity; import server.haengdong.domain.member.Member; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class BillDetail { +public class BillDetail extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/server/src/main/java/server/haengdong/domain/event/Bank.java b/server/src/main/java/server/haengdong/domain/event/Bank.java index 6e449ae7e..53fbbd92d 100644 --- a/server/src/main/java/server/haengdong/domain/event/Bank.java +++ b/server/src/main/java/server/haengdong/domain/event/Bank.java @@ -6,21 +6,21 @@ public enum Bank { WOORI_BANK("우리은행"), - JEIL_BANK("제일은행"), + JEIL_BANK("SC제일은행"), SHINHAN_BANK("신한은행"), KB_BANK("KB국민은행"), HANA_BANK("하나은행"), - CITI_BANK("시티은행"), + CITI_BANK("씨티은행"), IM_BANK("IM뱅크"), BUSAN_BANK("부산은행"), GYEONGNAM_BANK("경남은행"), GWANGJU_BANK("광주은행"), JEONBUK_BANK("전북은행"), JEJU_BANK("제주은행"), - IBK_BANK("기업은행"), - KDB_BANK("산업은행"), + IBK_BANK("IBK기업은행"), + KDB_BANK("KDB산업은행"), SUHYUP_BANK("수협은행"), - NH_BANK("농협은행"), + NH_BANK("NH농협"), SAEMAUL_BANK("새마을금고"), POST_BANK("우체국은행"), SHINHYEOP_BANK("신협은행"), diff --git a/server/src/main/java/server/haengdong/domain/event/Event.java b/server/src/main/java/server/haengdong/domain/event/Event.java index 58d080a43..a7cc00fb3 100644 --- a/server/src/main/java/server/haengdong/domain/event/Event.java +++ b/server/src/main/java/server/haengdong/domain/event/Event.java @@ -7,17 +7,19 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.time.Instant; import java.util.Arrays; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import server.haengdong.domain.BaseEntity; import server.haengdong.exception.HaengdongErrorCode; import server.haengdong.exception.HaengdongException; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class Event { +public class Event extends BaseEntity { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 20; diff --git a/server/src/main/java/server/haengdong/domain/event/EventImage.java b/server/src/main/java/server/haengdong/domain/event/EventImage.java index cc5aad0dd..16b2f5964 100644 --- a/server/src/main/java/server/haengdong/domain/event/EventImage.java +++ b/server/src/main/java/server/haengdong/domain/event/EventImage.java @@ -11,11 +11,12 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import server.haengdong.domain.BaseEntity; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class EventImage { +public class EventImage extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -26,5 +27,10 @@ public class EventImage { private Event event; @Column(nullable = false) - private String url; + private String name; + + public EventImage(Event event, String name) { + this.event = event; + this.name = name; + } } diff --git a/server/src/main/java/server/haengdong/domain/event/EventImageRepository.java b/server/src/main/java/server/haengdong/domain/event/EventImageRepository.java new file mode 100644 index 000000000..00f84b8af --- /dev/null +++ b/server/src/main/java/server/haengdong/domain/event/EventImageRepository.java @@ -0,0 +1,11 @@ +package server.haengdong.domain.event; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EventImageRepository extends JpaRepository { + + List findAllByEvent(Event event); +} diff --git a/server/src/main/java/server/haengdong/domain/member/Member.java b/server/src/main/java/server/haengdong/domain/member/Member.java index 25a19ccb0..2c9cb41d6 100644 --- a/server/src/main/java/server/haengdong/domain/member/Member.java +++ b/server/src/main/java/server/haengdong/domain/member/Member.java @@ -14,6 +14,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import server.haengdong.domain.BaseEntity; import server.haengdong.domain.event.Event; import server.haengdong.exception.HaengdongErrorCode; import server.haengdong.exception.HaengdongException; @@ -22,7 +23,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"event_id", "name"})}) @Entity -public class Member { +public class Member extends BaseEntity { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 8; diff --git a/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java b/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java index a59288f4b..fc79123b9 100644 --- a/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java +++ b/server/src/main/java/server/haengdong/exception/HaengdongErrorCode.java @@ -13,6 +13,7 @@ public enum HaengdongErrorCode { EVENT_PASSWORD_FORMAT_INVALID("비밀번호는 %d자리 숫자만 가능합니다."), BANK_NAME_INVALID("지원하지 않는 은행입니다. 지원하는 은행 목록: %s"), ACCOUNT_LENGTH_INVALID("계좌번호는 %d자 이상 %d자 이하만 입력 가능합니다."), + IMAGE_UPLOAD_FAIL("이미지 업로드에 실패했습니다."), MEMBER_NAME_LENGTH_INVALID("참여자 이름은 %d자 이상 %d자 이하만 입력 가능합니다."), MEMBER_NAME_DUPLICATE("행사에 중복된 참여자 이름이 존재합니다."), @@ -29,6 +30,8 @@ public enum HaengdongErrorCode { DIFFERENT_STEP_MEMBERS("참여자 목록이 일치하지 않습니다."), + IMAGE_NOT_FOUND("존재하지 않는 이미지 입니다."), + /* Authentication */ PASSWORD_INVALID("비밀번호가 일치하지 않습니다."), diff --git a/server/src/main/java/server/haengdong/presentation/EventController.java b/server/src/main/java/server/haengdong/presentation/EventController.java index 77caa744a..47bdd4fce 100644 --- a/server/src/main/java/server/haengdong/presentation/EventController.java +++ b/server/src/main/java/server/haengdong/presentation/EventController.java @@ -14,12 +14,14 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import server.haengdong.application.AuthService; +import server.haengdong.application.response.EventImageAppResponse; import server.haengdong.application.EventService; import server.haengdong.application.response.MemberBillReportAppResponse; import server.haengdong.infrastructure.auth.CookieProperties; import server.haengdong.presentation.request.EventLoginRequest; import server.haengdong.presentation.request.EventSaveRequest; import server.haengdong.presentation.response.EventDetailResponse; +import server.haengdong.presentation.response.EventImagesResponse; import server.haengdong.presentation.response.EventResponse; import server.haengdong.presentation.response.MemberBillReportsResponse; @@ -84,4 +86,11 @@ private ResponseCookie createResponseCookie(String token) { .maxAge(cookieProperties.maxAge()) .build(); } + + @GetMapping("/api/events/{eventId}/images") + public ResponseEntity findAllImages(@PathVariable("eventId") String token) { + List images = eventService.findImages(token); + + return ResponseEntity.ok(EventImagesResponse.of(images)); + } } diff --git a/server/src/main/java/server/haengdong/presentation/admin/AdminEventController.java b/server/src/main/java/server/haengdong/presentation/admin/AdminEventController.java index 46d11cdd5..8fd913dae 100644 --- a/server/src/main/java/server/haengdong/presentation/admin/AdminEventController.java +++ b/server/src/main/java/server/haengdong/presentation/admin/AdminEventController.java @@ -1,15 +1,21 @@ package server.haengdong.presentation.admin; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import server.haengdong.application.EventService; +import server.haengdong.application.ImageService; +import server.haengdong.application.response.ImageNameAppResponse; import server.haengdong.presentation.request.EventUpdateRequest; @Slf4j @@ -18,6 +24,7 @@ public class AdminEventController { private final EventService eventService; + private final ImageService imageUploadService; @PostMapping("/api/admin/events/{eventId}/auth") public ResponseEntity authenticate() { @@ -33,4 +40,26 @@ public ResponseEntity updateEvent( return ResponseEntity.ok().build(); } + + @PostMapping("/api/admin/events/{eventId}/images") + public ResponseEntity uploadImages( + @PathVariable("eventId") String token, + @RequestPart("images") List images + ) { + List imageNames = imageUploadService.uploadImages(images); + eventService.saveImages(token, imageNames); + + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/api/admin/events/{eventId}/images/{imageId}") + public ResponseEntity deleteImage( + @PathVariable("eventId") String token, + @PathVariable("imageId") Long imageId + ) { + String imageName = eventService.deleteImage(token, imageId); + imageUploadService.deleteImage(imageName); + + return ResponseEntity.ok().build(); + } } diff --git a/server/src/main/java/server/haengdong/presentation/request/BillDetailUpdateRequest.java b/server/src/main/java/server/haengdong/presentation/request/BillDetailUpdateRequest.java index de670f28f..d0f69b148 100644 --- a/server/src/main/java/server/haengdong/presentation/request/BillDetailUpdateRequest.java +++ b/server/src/main/java/server/haengdong/presentation/request/BillDetailUpdateRequest.java @@ -13,6 +13,7 @@ public record BillDetailUpdateRequest( boolean isFixed ) { + public BillDetailUpdateAppRequest toAppRequest() { return new BillDetailUpdateAppRequest(this.id, this.price, this.isFixed); } diff --git a/server/src/main/java/server/haengdong/presentation/request/BillDetailsUpdateRequest.java b/server/src/main/java/server/haengdong/presentation/request/BillDetailsUpdateRequest.java index f09d101bd..091db5967 100644 --- a/server/src/main/java/server/haengdong/presentation/request/BillDetailsUpdateRequest.java +++ b/server/src/main/java/server/haengdong/presentation/request/BillDetailsUpdateRequest.java @@ -6,8 +6,12 @@ import server.haengdong.application.request.BillDetailsUpdateAppRequest; public record BillDetailsUpdateRequest( - @Valid @NotEmpty List billDetails + + @Valid + @NotEmpty + List billDetails ) { + public BillDetailsUpdateAppRequest toAppRequest() { return new BillDetailsUpdateAppRequest(billDetails.stream() .map(BillDetailUpdateRequest::toAppRequest) diff --git a/server/src/main/java/server/haengdong/presentation/request/BillUpdateRequest.java b/server/src/main/java/server/haengdong/presentation/request/BillUpdateRequest.java index a37c75013..d5ea67474 100644 --- a/server/src/main/java/server/haengdong/presentation/request/BillUpdateRequest.java +++ b/server/src/main/java/server/haengdong/presentation/request/BillUpdateRequest.java @@ -12,6 +12,7 @@ public record BillUpdateRequest( @NotNull(message = "지출 금액은 공백일 수 없습니다.") Long price ) { + public BillUpdateAppRequest toAppResponse() { return new BillUpdateAppRequest(title, price); } diff --git a/server/src/main/java/server/haengdong/presentation/request/EventLoginRequest.java b/server/src/main/java/server/haengdong/presentation/request/EventLoginRequest.java index a1286e903..d49a0e510 100644 --- a/server/src/main/java/server/haengdong/presentation/request/EventLoginRequest.java +++ b/server/src/main/java/server/haengdong/presentation/request/EventLoginRequest.java @@ -8,6 +8,7 @@ public record EventLoginRequest( @NotBlank(message = "비밀번호는 공백일 수 없습니다.") String password ) { + public EventLoginAppRequest toAppRequest(String token) { return new EventLoginAppRequest(token, password); } diff --git a/server/src/main/java/server/haengdong/presentation/request/MemberNameUpdateRequest.java b/server/src/main/java/server/haengdong/presentation/request/MemberNameUpdateRequest.java deleted file mode 100644 index fb1ff655f..000000000 --- a/server/src/main/java/server/haengdong/presentation/request/MemberNameUpdateRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package server.haengdong.presentation.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import server.haengdong.application.request.MemberNameUpdateAppRequest; - -public record MemberNameUpdateRequest( - - @NotNull(message = "멤버 id는 공백일 수 없습니다.") - Long id, - - @NotBlank(message = "멤버 이름은 공백일 수 없습니다.") - String name -) { - - public MemberNameUpdateAppRequest toAppRequest() { - return new MemberNameUpdateAppRequest(id, name); - } -} diff --git a/server/src/main/java/server/haengdong/presentation/request/MemberNamesUpdateRequest.java b/server/src/main/java/server/haengdong/presentation/request/MemberNamesUpdateRequest.java deleted file mode 100644 index 79c46590d..000000000 --- a/server/src/main/java/server/haengdong/presentation/request/MemberNamesUpdateRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package server.haengdong.presentation.request; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotEmpty; -import java.util.List; -import server.haengdong.application.request.MemberNamesUpdateAppRequest; - -public record MemberNamesUpdateRequest( - @Valid - @NotEmpty - List members -) { - - public MemberNamesUpdateAppRequest toAppRequest() { - return new MemberNamesUpdateAppRequest(members.stream() - .map(MemberNameUpdateRequest::toAppRequest) - .toList() - ); - } -} diff --git a/server/src/main/java/server/haengdong/presentation/request/MembersSaveRequest.java b/server/src/main/java/server/haengdong/presentation/request/MembersSaveRequest.java index c5e2100fd..cc611742a 100644 --- a/server/src/main/java/server/haengdong/presentation/request/MembersSaveRequest.java +++ b/server/src/main/java/server/haengdong/presentation/request/MembersSaveRequest.java @@ -4,7 +4,10 @@ import server.haengdong.application.request.MemberSaveAppRequest; import server.haengdong.application.request.MembersSaveAppRequest; -public record MembersSaveRequest(List members) { +public record MembersSaveRequest( + List members +) { + public MembersSaveAppRequest toAppRequest() { return new MembersSaveAppRequest(members.stream() .map(member -> new MemberSaveAppRequest(member.name())) diff --git a/server/src/main/java/server/haengdong/presentation/response/BillResponse.java b/server/src/main/java/server/haengdong/presentation/response/BillResponse.java index 8b79896bb..0f3aaeaa0 100644 --- a/server/src/main/java/server/haengdong/presentation/response/BillResponse.java +++ b/server/src/main/java/server/haengdong/presentation/response/BillResponse.java @@ -8,6 +8,7 @@ public record BillResponse( Long price, boolean isFixed ) { + public static BillResponse of(BillAppResponse response) { return new BillResponse(response.id(), response.title(), response.price(), response.isFixed()); } diff --git a/server/src/main/java/server/haengdong/presentation/response/EventImageResponse.java b/server/src/main/java/server/haengdong/presentation/response/EventImageResponse.java new file mode 100644 index 000000000..da807655f --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/response/EventImageResponse.java @@ -0,0 +1,13 @@ +package server.haengdong.presentation.response; + +import server.haengdong.application.response.EventImageAppResponse; + +public record EventImageResponse( + Long id, + String url +) { + + public static EventImageResponse of(EventImageAppResponse response) { + return new EventImageResponse(response.id(), response.url()); + } +} diff --git a/server/src/main/java/server/haengdong/presentation/response/EventImagesResponse.java b/server/src/main/java/server/haengdong/presentation/response/EventImagesResponse.java new file mode 100644 index 000000000..d5c2b0ab8 --- /dev/null +++ b/server/src/main/java/server/haengdong/presentation/response/EventImagesResponse.java @@ -0,0 +1,15 @@ +package server.haengdong.presentation.response; + +import java.util.List; +import server.haengdong.application.response.EventImageAppResponse; + +public record EventImagesResponse(List images) { + + public static EventImagesResponse of(List responses) { + List images = responses.stream() + .map(EventImageResponse::of) + .toList(); + + return new EventImagesResponse(images); + } +} diff --git a/server/src/main/java/server/haengdong/presentation/response/MemberSaveResponse.java b/server/src/main/java/server/haengdong/presentation/response/MemberSaveResponse.java index b0740c6ca..21a7e7969 100644 --- a/server/src/main/java/server/haengdong/presentation/response/MemberSaveResponse.java +++ b/server/src/main/java/server/haengdong/presentation/response/MemberSaveResponse.java @@ -6,6 +6,7 @@ public record MemberSaveResponse( Long id, String name ) { + public static MemberSaveResponse of(MemberSaveAppResponse response) { return new MemberSaveResponse(response.id(), response.name()); } diff --git a/server/src/main/java/server/haengdong/presentation/response/MembersSaveResponse.java b/server/src/main/java/server/haengdong/presentation/response/MembersSaveResponse.java index 04e9b133e..d4b7ad456 100644 --- a/server/src/main/java/server/haengdong/presentation/response/MembersSaveResponse.java +++ b/server/src/main/java/server/haengdong/presentation/response/MembersSaveResponse.java @@ -6,6 +6,7 @@ public record MembersSaveResponse( List members ) { + public static MembersSaveResponse of(MembersSaveAppResponse response) { return new MembersSaveResponse( response.members().stream() diff --git a/server/src/main/java/server/haengdong/presentation/response/StepResponse.java b/server/src/main/java/server/haengdong/presentation/response/StepResponse.java index 924651c73..a164e71f5 100644 --- a/server/src/main/java/server/haengdong/presentation/response/StepResponse.java +++ b/server/src/main/java/server/haengdong/presentation/response/StepResponse.java @@ -7,6 +7,7 @@ public record StepResponse( List bills, List members ) { + public static StepResponse of(StepAppResponse response) { List bills = response.bills().stream() .map(BillResponse::of) @@ -16,7 +17,6 @@ public static StepResponse of(StepAppResponse response) { .map(MemberResponse::of) .toList(); return new StepResponse(bills, members); - } } diff --git a/server/src/main/java/server/haengdong/presentation/response/StepsResponse.java b/server/src/main/java/server/haengdong/presentation/response/StepsResponse.java index baceeb549..8a66df1c6 100644 --- a/server/src/main/java/server/haengdong/presentation/response/StepsResponse.java +++ b/server/src/main/java/server/haengdong/presentation/response/StepsResponse.java @@ -6,6 +6,7 @@ public record StepsResponse( List steps ) { + public static StepsResponse of(List steps) { return new StepsResponse(steps.stream() .map(StepResponse::of) diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index 22c337545..69472a880 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -4,6 +4,10 @@ spring: url: jdbc:h2:mem:database username: sa password: + servlet: + multipart: + max-file-size: 50MB + max-request-size: 300MB h2: console: @@ -37,6 +41,11 @@ cookie: same-site: none max-age: 7D +image: + bucket: techcourse-project-2024 + directory: haeng-dong/s3-upload-test/ + base-url: https://d2unln22cedgp9.cloudfront.net/ + management: endpoints: web: diff --git a/server/src/main/resources/config b/server/src/main/resources/config new file mode 160000 index 000000000..a57fb2fff --- /dev/null +++ b/server/src/main/resources/config @@ -0,0 +1 @@ +Subproject commit a57fb2fffd85ad12af1ee98e6b1998dadb3b9d0a diff --git a/server/src/test/java/server/haengdong/application/EventServiceTest.java b/server/src/test/java/server/haengdong/application/EventServiceTest.java index e40a682dc..bcfc0a3e4 100644 --- a/server/src/test/java/server/haengdong/application/EventServiceTest.java +++ b/server/src/test/java/server/haengdong/application/EventServiceTest.java @@ -10,14 +10,19 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.mock.mockito.MockBean; import server.haengdong.application.request.EventAppRequest; import server.haengdong.application.request.EventUpdateAppRequest; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; +import server.haengdong.application.response.EventImageAppResponse; +import server.haengdong.application.response.ImageNameAppResponse; import server.haengdong.application.response.MemberBillReportAppResponse; import server.haengdong.domain.bill.Bill; import server.haengdong.domain.bill.BillRepository; +import server.haengdong.domain.event.EventImage; +import server.haengdong.domain.event.EventImageRepository; import server.haengdong.domain.member.Member; import server.haengdong.domain.member.MemberRepository; import server.haengdong.domain.event.Event; @@ -39,9 +44,15 @@ class EventServiceTest extends ServiceTestSupport { @Autowired private MemberRepository memberRepository; + @Autowired + private EventImageRepository eventImageRepository; + @MockBean private EventTokenProvider eventTokenProvider; + @Value("${image.base-url}") + private String baseUrl; + @DisplayName("행사를 생성한다") @Test void saveEventTest() { @@ -162,4 +173,59 @@ void getMemberBillReports() { tuple("고구마", 20_000L) ); } + + @DisplayName("행사 이미지를 조회한다.") + @Test + void findAllImages() { + Event event = Fixture.EVENT1; + List eventImages = List.of( + new EventImage(event, "image1.jpg"), + new EventImage(event, "image2.jpg") + ); + eventRepository.save(event); + eventImageRepository.saveAll(eventImages); + + List responses = eventService.findImages(event.getToken()); + + assertThat(responses) + .hasSize(2) + .extracting(EventImageAppResponse::url) + .containsExactlyInAnyOrder( + baseUrl + "image1.jpg", + baseUrl + "image2.jpg" + ); + } + + @DisplayName("행사 이미지를 저장한다.") + @Test + void saveImages() { + Event event = Fixture.EVENT1; + eventRepository.save(event); + List imageNames = List.of("image1.jpg", "image2.jpg"); + + eventService.saveImages(event.getToken(), imageNames); + + List savedEventImages = eventImageRepository.findAllByEvent(event); + assertThat(savedEventImages) + .hasSize(2) + .extracting(EventImage::getName) + .containsExactlyInAnyOrder( + "image1.jpg", + "image2.jpg" + ); + } + + @DisplayName("행사 이미지를 삭제한다.") + @Test + void deleteImage() { + Event event = Fixture.EVENT1; + eventRepository.save(event); + EventImage eventImage = new EventImage(event, "image1.jpg"); + eventImageRepository.save(eventImage); + + eventService.deleteImage(event.getToken(), eventImage.getId()); + + assertThat(eventImageRepository.findById(eventImage.getId())) + .isEmpty(); + } } diff --git a/server/src/test/java/server/haengdong/docs/AdminEventControllerDocsTest.java b/server/src/test/java/server/haengdong/docs/AdminEventControllerDocsTest.java index 22c028169..658c57512 100644 --- a/server/src/test/java/server/haengdong/docs/AdminEventControllerDocsTest.java +++ b/server/src/test/java/server/haengdong/docs/AdminEventControllerDocsTest.java @@ -4,15 +4,19 @@ import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static server.haengdong.support.fixture.Fixture.EVENT_COOKIE; @@ -20,18 +24,21 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.payload.JsonFieldType; import server.haengdong.application.EventService; +import server.haengdong.application.ImageService; import server.haengdong.presentation.admin.AdminEventController; import server.haengdong.presentation.request.EventUpdateRequest; class AdminEventControllerDocsTest extends RestDocsSupport { private final EventService eventService = mock(EventService.class); + private final ImageService imageUploadService = mock(ImageService.class); @Override protected Object initController() { - return new AdminEventController(eventService); + return new AdminEventController(eventService, imageUploadService); } @DisplayName("행사 어드민 권한을 확인한다.") @@ -92,4 +99,59 @@ void updateEventTest() throws Exception { ) ); } + + @DisplayName("행사에 이미지를 업로드한다.") + @Test + void uploadImages() throws Exception { + String token = "TOKEN"; + MockMultipartFile image1 = new MockMultipartFile("images", "image1.jpg", "image/jpeg", "이미지1".getBytes()); + MockMultipartFile image2 = new MockMultipartFile("images", "image2.jpg", "image/jpeg", "이미지2".getBytes()); + + mockMvc.perform(multipart("/api/admin/events/{eventId}/images", token) + .file(image1) + .file(image2) + .cookie(EVENT_COOKIE) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andDo(print()) + .andExpect(status().isOk()) + .andDo( + document("uploadImages", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("eventId").description("행사 ID") + ), + requestCookies( + cookieWithName("eventToken").description("행사 관리자 토큰") + ), + requestParts( + partWithName("images").description("행사 이미지") + ) + ) + ); + } + + @DisplayName("행사 이미지를 삭제한다.") + @Test + void deleteImage() throws Exception { + String token = "TOKEN"; + + mockMvc.perform(delete("/api/admin/events/{eventId}/images/{imageId}", token, 1L) + .cookie(EVENT_COOKIE)) + .andDo(print()) + .andExpect(status().isOk()) + .andDo( + document("deleteImage", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("eventId").description("행사 ID"), + parameterWithName("imageId").description("이미지 ID") + ), + requestCookies( + cookieWithName("eventToken").description("행사 관리자 토큰") + ) + ) + ); + } } diff --git a/server/src/test/java/server/haengdong/docs/EventControllerDocsTest.java b/server/src/test/java/server/haengdong/docs/EventControllerDocsTest.java index 83144f135..0918721fd 100644 --- a/server/src/test/java/server/haengdong/docs/EventControllerDocsTest.java +++ b/server/src/test/java/server/haengdong/docs/EventControllerDocsTest.java @@ -34,6 +34,7 @@ import server.haengdong.application.request.EventAppRequest; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; +import server.haengdong.application.response.EventImageAppResponse; import server.haengdong.application.response.MemberBillReportAppResponse; import server.haengdong.infrastructure.auth.CookieProperties; import server.haengdong.presentation.EventController; @@ -192,4 +193,38 @@ void loginEvent() throws Exception { ) ); } + + + @DisplayName("행사 이미지를 조회한다.") + @Test + void findAllImages() throws Exception { + String token = "TOKEN"; + List imageNameAppResponses = List.of( + new EventImageAppResponse(1L, "https://host.com/image1.jpg"), + new EventImageAppResponse(2L, "https://host.com/image2.jpg"), + new EventImageAppResponse(3L, "https://host.com/zeze.jpg") + ); + given(eventService.findImages(token)).willReturn(imageNameAppResponses); + + mockMvc.perform(get("/api/events/{eventId}/images", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andDo( + document("findImages", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("eventId").description("행사 ID") + ), + responseFields( + fieldWithPath("images").type(JsonFieldType.ARRAY) + .description("행사 이미지 목록"), + fieldWithPath("images[].id").type(JsonFieldType.NUMBER) + .description("이미지 id"), + fieldWithPath("images[].url").type(JsonFieldType.STRING) + .description("이미지 url") + ) + ) + ); + } } diff --git a/server/src/test/java/server/haengdong/presentation/ControllerTestSupport.java b/server/src/test/java/server/haengdong/presentation/ControllerTestSupport.java index 6ba5540d5..738823c40 100644 --- a/server/src/test/java/server/haengdong/presentation/ControllerTestSupport.java +++ b/server/src/test/java/server/haengdong/presentation/ControllerTestSupport.java @@ -11,6 +11,7 @@ import server.haengdong.application.AuthService; import server.haengdong.application.BillService; import server.haengdong.application.EventService; +import server.haengdong.application.ImageService; import server.haengdong.application.MemberService; import server.haengdong.presentation.admin.AdminBillController; import server.haengdong.presentation.admin.AdminEventController; @@ -46,4 +47,7 @@ public abstract class ControllerTestSupport { @MockBean protected BillService billService; + + @MockBean + protected ImageService imageUploadService; } diff --git a/server/src/test/java/server/haengdong/presentation/EventControllerTest.java b/server/src/test/java/server/haengdong/presentation/EventControllerTest.java index 5facbb740..90e7fd618 100644 --- a/server/src/test/java/server/haengdong/presentation/EventControllerTest.java +++ b/server/src/test/java/server/haengdong/presentation/EventControllerTest.java @@ -14,10 +14,12 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import server.haengdong.application.request.EventAppRequest; import server.haengdong.application.response.EventAppResponse; import server.haengdong.application.response.EventDetailAppResponse; +import server.haengdong.application.response.EventImageAppResponse; import server.haengdong.application.response.MemberBillReportAppResponse; import server.haengdong.presentation.request.EventLoginRequest; import server.haengdong.presentation.request.EventSaveRequest; @@ -98,4 +100,20 @@ void loginEvent() throws Exception { .andExpect(cookie().value("eventToken", "jwtToken")) .andExpect(status().isOk()); } + + @DisplayName("행사 이미지를 조회한다.") + @Test + void findAllImages() throws Exception { + String token = "TOKEN"; + List imageNameAppResponses = List.of( + new EventImageAppResponse(1L, "https://host.com/image1.jpg"), + new EventImageAppResponse(2L, "https://host.com/image2.jpg"), + new EventImageAppResponse(3L, "https://host.com/zeze.jpg") + ); + given(eventService.findImages(token)).willReturn(imageNameAppResponses); + + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/events/{eventId}/images", token)) + .andDo(print()) + .andExpect(status().isOk()); + } } diff --git a/server/src/test/java/server/haengdong/presentation/admin/AdminEventControllerTest.java b/server/src/test/java/server/haengdong/presentation/admin/AdminEventControllerTest.java index df7b70e92..e071d70d3 100644 --- a/server/src/test/java/server/haengdong/presentation/admin/AdminEventControllerTest.java +++ b/server/src/test/java/server/haengdong/presentation/admin/AdminEventControllerTest.java @@ -1,12 +1,15 @@ package server.haengdong.presentation.admin; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static server.haengdong.support.fixture.Fixture.EVENT_COOKIE; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import server.haengdong.presentation.ControllerTestSupport; import server.haengdong.presentation.request.EventUpdateRequest; @@ -27,4 +30,20 @@ void updateEventTest() throws Exception { .andDo(print()) .andExpect(status().isOk()); } + + @DisplayName("행사에 이미지를 업로드한다.") + @Test + void uploadImages() throws Exception { + String token = "TOKEN"; + MockMultipartFile image1 = new MockMultipartFile("images", "image1.jpg", "image/jpeg", "이미지1".getBytes()); + MockMultipartFile image2 = new MockMultipartFile("images", "image2.jpg", "image/jpeg", "이미지2".getBytes()); + + mockMvc.perform(multipart("/api/admin/events/{eventId}/images", token) + .file(image1) + .file(image2) + .cookie(EVENT_COOKIE) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andDo(print()) + .andExpect(status().isOk()); + } }