From b9e0fa34ffbb8dfd8472b51535390bb8169e143f Mon Sep 17 00:00:00 2001 From: "Choi, Minwoo" Date: Sun, 1 Feb 2026 20:36:42 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20TripReportStudyLog,=20TripReport,?= =?UTF-8?q?=20Dummy=20=EA=B8=B0=EB=8A=A5=20=EC=A0=84=EB=B0=98=20Kotlin=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: TripReportStudyLogCommandService.kt 구현 * feat: TripReportStudyLogRepository.kt, TripReportStudyLogJpaRepository.kt, TripReportStudyLogRepositoryAdapter.kt 구현 * feat: TripReportStudyLogFactory.kt 구현 * feat: TripReportController.kt 구현 * feat: TripReportFacade.kt 구현 * feat: TripReportQueryService.kt, TripReportCommandService.kt 구현 * feat: TripReportRepository.kt, TripReportJpaRepository.kt, TripReportRepositoryAdapter.kt 구현 * feat: TripReportErrorCode.kt, TripReportPolicy.kt 구현 * feat: TripReportFactory.kt 구현 * feat: DummyStampController.kt, DummyMissionController.kt 구현 * feat: DummyStampFacade.kt, DummyMissionFacade.kt 구현 * feat: DummyTripCommandService.kt, DummyStampCommandService.kt, DummyMissionCommandService.kt 구현 * feat: CreateTripReportRequest.kt, ConfirmTripReportImageRequest.kt, PresignTripReportImageRequest.kt 추가 * feat: CreateTripReportResponse.kt, LoadTripReportsResponse.kt, LoadTripReportDetailResponse.kt 추가 * feat: LoadTripRetrospectDetailResponse.kt, PresignedTripReportImageResponse.kt 추가 * feat: TripReportInfo.kt, TripReportsInfo.kt, TripReportDetail.kt, PresignedTripReportImageInfo.kt 추가 * feat: TripRetrospectSummary.kt, TripRetrospectDetail.kt 추가 * feat: LoadDummyStampInfoResponse.kt, LoadDummyMissionInfoResponse.kt 추가 * feat: CreateDummyTripCommand.kt, CreateDummyStampCommand.kt, CreateDummyMissionCommand.kt 추가 * feat: DummyStampInfo.kt, DummyStampsInfo.kt, DummyMissionInfo.kt, DummyMissionsInfo.kt 추가 * refactor: TripReportQueryRepository.java에 findAllActiveByMemberId 메서드 추가 * test: TripReportStudyLogCommandServiceTest.kt 단위 테스트 추가 * test: TripReportFixture.kt, CreateTripReportRequestFixture.kt 추가 * test: ConfirmTripReportImageRequestFixture.kt, PresignTripReportImageRequestFixture.kt 추가 * test: TripReportTestHelper.kt 추가 * test: TripReportQueryServiceTest.kt, TripReportCommandServiceTest.kt 단위 테스트 추가 * test: TripReportControllerIntegrationTest.kt 통합 테스트 추가 * test: DummyTripCommandServiceTest.kt, DummyStampCommandServiceTest.kt, DummyMissionCommandServiceTest.kt 단위 테스트 추가 * test: DummyStampControllerIntegrationTest.kt, DummyMissionControllerIntegrationTest.kt 통합 테스트 추가 * test: StudyLogTestHelper.kt에 saveDeletedStudyLog 메서드 추가 * test: test java trip, dummy 패키지 제거 (Kotlin 마이그레이션 완료) --- .../dto/CreateDummyMissionCommand.java | 7 - .../dto/CreateDummyStampCommand.java | 9 - .../dto/CreateDummyTripCommand.java | 12 - .../application/dto/DummyMissionInfo.java | 22 - .../application/dto/DummyMissionsInfo.java | 9 - .../dummy/application/dto/DummyStampInfo.java | 30 - .../application/dto/DummyStampsInfo.java | 9 - .../facade/DummyMissionFacade.java | 39 - .../application/facade/DummyStampFacade.java | 35 - .../CreateDummyMissionCommandGenerator.java | 11 - .../CreateDummyStampCommandGenerator.java | 24 - .../CreateDummyTripCommandGenerator.java | 25 - .../service/DummyMissionCommandService.java | 18 - .../service/DummyStampCommandService.java | 19 - .../service/DummyTripCommandService.java | 26 - .../controller/DummyMissionController.java | 52 - .../controller/DummyStampController.java | 52 - .../LoadDummyMissionInfoResponse.java | 14 - .../response/LoadDummyStampInfoResponse.java | 24 - .../application/facade/MemberFacade.java | 4 +- .../dto/PresignedTripReportImageInfo.java | 8 - .../application/dto/TripReportDetail.java | 10 - .../trip/application/dto/TripReportInfo.java | 36 - .../trip/application/dto/TripReportsInfo.java | 9 - .../application/dto/TripRetrospectDetail.java | 13 - .../dto/TripRetrospectSummary.java | 15 - .../application/facade/TripReportFacade.java | 190 ---- .../service/TripReportCommandService.java | 53 - .../service/TripReportQueryService.java | 48 - .../TripReportStudyLogCommandService.java | 35 - .../domain/error/TripReportErrorCode.java | 35 - .../domain/factory/TripReportFactory.java | 31 - .../factory/TripReportStudyLogFactory.java | 14 - .../trip/domain/policy/TripReportPolicy.java | 21 - .../repository/TripReportQueryRepository.java | 3 + .../repository/TripReportRepository.java | 13 - .../TripReportStudyLogRepository.java | 8 - .../infra/jpa/TripReportJpaRepository.java | 9 - .../jpa/TripReportRepositoryAdapter.java | 30 - .../jpa/TripReportStudyLogJpaRepository.java | 6 - .../TripReportStudyLogRepositoryAdapter.java | 18 - .../TripReportQueryRepositoryAdapter.java | 10 + .../controller/TripReportController.java | 183 ---- .../ConfirmTripReportImageRequest.java | 8 - .../dto/request/CreateTripReportRequest.java | 22 - .../PresignTripReportImageRequest.java | 8 - .../response/CreateTripReportResponse.java | 10 - .../LoadTripReportDetailResponse.java | 36 - .../dto/response/LoadTripReportsResponse.java | 47 - .../LoadTripRetrospectDetailResponse.java | 35 - .../PresignedTripReportImageResponse.java | 13 - .../dto/CreateDummyMissionCommand.kt | 10 + .../dto/CreateDummyStampCommand.kt | 18 + .../application/dto/CreateDummyTripCommand.kt | 21 + .../dummy/application/dto/DummyMissionInfo.kt | 17 + .../application/dto/DummyMissionsInfo.kt | 10 + .../dummy/application/dto/DummyStampInfo.kt | 26 + .../dummy/application/dto/DummyStampsInfo.kt | 10 + .../application/facade/DummyMissionFacade.kt | 32 + .../application/facade/DummyStampFacade.kt | 29 + .../service/DummyMissionCommandService.kt | 16 + .../service/DummyStampCommandService.kt | 25 + .../service/DummyTripCommandService.kt | 28 + .../controller/DummyMissionController.kt | 48 + .../controller/DummyStampController.kt | 48 + .../dto/LoadDummyMissionInfoResponse.kt | 20 + .../dto/LoadDummyStampInfoResponse.kt | 32 + .../dto/PresignedTripReportImageInfo.kt | 16 + .../trip/application/dto/TripReportDetail.kt | 16 + .../trip/application/dto/TripReportInfo.kt | 40 + .../trip/application/dto/TripReportsInfo.kt | 10 + .../application/dto/TripRetrospectDetail.kt | 18 + .../application/dto/TripRetrospectSummary.kt | 24 + .../application/facade/TripReportFacade.kt | 170 +++ .../service/TripReportCommandService.kt | 48 + .../service/TripReportQueryService.kt | 39 + .../TripReportStudyLogCommandService.kt | 27 + .../trip/domain/error/TripReportErrorCode.kt | 25 + .../trip/domain/factory/TripReportFactory.kt | 19 + .../factory/TripReportStudyLogFactory.kt | 13 + .../trip/domain/policy/TripReportPolicy.kt | 22 + .../domain/repository/TripReportRepository.kt | 10 + .../TripReportStudyLogRepository.kt | 7 + .../trip/infra/jpa/TripReportJpaRepository.kt | 6 + .../infra/jpa/TripReportRepositoryAdapter.kt | 15 + .../jpa/TripReportStudyLogJpaRepository.kt | 6 + .../TripReportStudyLogRepositoryAdapter.kt | 14 + .../presentation/controller/TripController.kt | 2 +- .../controller/TripReportController.kt | 199 ++++ .../request/ConfirmTripReportImageRequest.kt | 10 + .../dto/request/CreateTripReportRequest.kt | 33 + .../request/PresignTripReportImageRequest.kt | 10 + .../dto/response/CreateTripReportResponse.kt | 14 + .../response/LoadTripReportDetailResponse.kt | 52 + .../dto/response/LoadTripReportsResponse.kt | 63 ++ .../LoadTripRetrospectDetailResponse.kt | 45 + .../PresignedTripReportImageResponse.kt | 21 + .../DummyMissionCommandServiceTest.java | 52 - .../service/DummyStampCommandServiceTest.java | 67 -- .../service/DummyTripCommandServiceTest.java | 68 -- ...DummyMissionControllerIntegrationTest.java | 171 ---- .../DummyStampControllerIntegrationTest.java | 173 ---- .../service/TripReportCommandServiceTest.java | 187 ---- .../service/TripReportQueryServiceTest.java | 235 ----- .../TripReportStudyLogCommandServiceTest.java | 156 --- .../ConfirmTripReportImageRequestFixture.java | 16 - .../CreateTripReportRequestFixture.java | 35 - .../PresignTripReportImageRequestFixture.java | 16 - .../trip/fixture/TripReportFixture.java | 47 - .../fixture/TripReportStudyLogFixture.java | 24 - .../trip/helper/TripReportTestHelper.java | 19 - .../TripReportControllerIntegrationTest.java | 966 ------------------ .../service/DummyMissionCommandServiceTest.kt | 62 ++ .../service/DummyStampCommandServiceTest.kt | 56 + .../service/DummyTripCommandServiceTest.kt | 62 ++ .../DummyMissionControllerIntegrationTest.kt | 137 +++ .../DummyStampControllerIntegrationTest.kt | 139 +++ .../service/StudyLogQueryServiceTest.kt | 2 +- .../studylog/helper/StudyLogTestHelper.kt | 5 + .../service/TripReportCommandServiceTest.kt | 196 ++++ .../service/TripReportQueryServiceTest.kt | 207 ++++ .../TripReportStudyLogCommandServiceTest.kt | 127 +++ .../ConfirmTripReportImageRequestFixture.kt | 11 + .../fixture/CreateTripReportRequestFixture.kt | 30 + .../PresignTripReportImageRequestFixture.kt | 11 + .../trip/fixture/TripReportFixture.kt | 24 + .../trip/helper/TripReportTestHelper.kt | 21 + .../TripControllerIntegrationTest.kt | 3 +- .../TripReportControllerIntegrationTest.kt | 820 +++++++++++++++ 129 files changed, 3309 insertions(+), 3638 deletions(-) delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampInfo.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/facade/DummyStampFacade.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyMissionCommandGenerator.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyStampCommandGenerator.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyTripCommandGenerator.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/service/DummyStampCommandService.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/application/service/DummyTripCommandService.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyStampController.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyMissionInfoResponse.java delete mode 100644 src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyStampInfoResponse.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/dto/TripReportDetail.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/dto/TripReportInfo.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/dto/TripReportsInfo.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/facade/TripReportFacade.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/service/TripReportQueryService.java delete mode 100644 src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java delete mode 100644 src/main/java/com/ject/studytrip/trip/domain/error/TripReportErrorCode.java delete mode 100644 src/main/java/com/ject/studytrip/trip/domain/factory/TripReportFactory.java delete mode 100644 src/main/java/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.java delete mode 100644 src/main/java/com/ject/studytrip/trip/domain/policy/TripReportPolicy.java delete mode 100644 src/main/java/com/ject/studytrip/trip/domain/repository/TripReportRepository.java delete mode 100644 src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.java delete mode 100644 src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.java delete mode 100644 src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.java delete mode 100644 src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.java delete mode 100644 src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/controller/TripReportController.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java delete mode 100644 src/main/java/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.java create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampInfo.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyStampFacade.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandService.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandService.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampController.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyMissionInfoResponse.kt create mode 100644 src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyStampInfoResponse.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportDetail.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportInfo.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportsInfo.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/facade/TripReportFacade.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandService.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryService.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/domain/error/TripReportErrorCode.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportFactory.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/domain/policy/TripReportPolicy.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportRepository.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportController.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.kt create mode 100644 src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.kt delete mode 100644 src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java delete mode 100644 src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java delete mode 100644 src/test/java/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.java delete mode 100644 src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.java delete mode 100644 src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.java delete mode 100644 src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java delete mode 100644 src/test/java/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.java delete mode 100644 src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java delete mode 100644 src/test/java/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.java delete mode 100644 src/test/java/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.java delete mode 100644 src/test/java/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.java delete mode 100644 src/test/java/com/ject/studytrip/trip/fixture/TripReportFixture.java delete mode 100644 src/test/java/com/ject/studytrip/trip/fixture/TripReportStudyLogFixture.java delete mode 100644 src/test/java/com/ject/studytrip/trip/helper/TripReportTestHelper.java delete mode 100644 src/test/java/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.java create mode 100644 src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/fixture/TripReportFixture.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/helper/TripReportTestHelper.kt create mode 100644 src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.kt diff --git a/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.java b/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.java deleted file mode 100644 index f539f01..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.ject.studytrip.dummy.application.dto; - -public record CreateDummyMissionCommand(String name) { - public static CreateDummyMissionCommand of(String name) { - return new CreateDummyMissionCommand(name); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.java b/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.java deleted file mode 100644 index 9a998d9..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.dummy.application.dto; - -import java.time.LocalDate; - -public record CreateDummyStampCommand(String name, int stampOrder, LocalDate endDate) { - public static CreateDummyStampCommand of(String name, int stampOrder, LocalDate endDate) { - return new CreateDummyStampCommand(name, stampOrder, endDate); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.java b/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.java deleted file mode 100644 index 93e269b..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.ject.studytrip.dummy.application.dto; - -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.time.LocalDate; - -public record CreateDummyTripCommand( - String name, String memo, TripCategory tripCategory, LocalDate endDate) { - public static CreateDummyTripCommand of( - String name, String memo, TripCategory tripCategory, LocalDate endDate) { - return new CreateDummyTripCommand(name, memo, tripCategory, endDate); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.java b/src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.java deleted file mode 100644 index 13463dd..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ject.studytrip.dummy.application.dto; - -import com.ject.studytrip.global.util.DateUtil; -import com.ject.studytrip.mission.domain.model.Mission; - -public record DummyMissionInfo( - Long missionId, - String missionName, - boolean completed, - String createdAt, - String updatedAt, - String deletedAt) { - public static DummyMissionInfo from(Mission mission) { - return new DummyMissionInfo( - mission.getId(), - mission.getName(), - mission.isCompleted(), - DateUtil.formatDateTime(mission.getCreatedAt()), - DateUtil.formatDateTime(mission.getUpdatedAt()), - DateUtil.formatDateTime(mission.getDeletedAt())); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.java b/src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.java deleted file mode 100644 index 86fee60..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.dummy.application.dto; - -import java.util.List; - -public record DummyMissionsInfo(List missionInfos) { - public static DummyMissionsInfo of(List missionInfos) { - return new DummyMissionsInfo(missionInfos); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampInfo.java b/src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampInfo.java deleted file mode 100644 index e0219dc..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ject.studytrip.dummy.application.dto; - -import com.ject.studytrip.global.util.DateUtil; -import com.ject.studytrip.stamp.domain.model.Stamp; - -public record DummyStampInfo( - Long stampId, - String stampName, - int stampOrder, - String endDate, - int totalMissions, - int completedMissions, - boolean completed, - String createdAt, - String updatedAt, - String deletedAt) { - public static DummyStampInfo from(Stamp stamp) { - return new DummyStampInfo( - stamp.getId(), - stamp.getName(), - stamp.getStampOrder(), - DateUtil.formatDate(stamp.getEndDate()), - stamp.getTotalMissions(), - stamp.getCompletedMissions(), - stamp.isCompleted(), - DateUtil.formatDateTime(stamp.getCreatedAt()), - DateUtil.formatDateTime(stamp.getUpdatedAt()), - DateUtil.formatDateTime(stamp.getDeletedAt())); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.java b/src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.java deleted file mode 100644 index 0da4af9..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.dummy.application.dto; - -import java.util.List; - -public record DummyStampsInfo(List stampsInfos) { - public static DummyStampsInfo of(List stampsInfos) { - return new DummyStampsInfo(stampsInfos); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.java b/src/main/java/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.java deleted file mode 100644 index 4f423ad..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.ject.studytrip.dummy.application.facade; - -import com.ject.studytrip.dummy.application.dto.DummyMissionInfo; -import com.ject.studytrip.dummy.application.dto.DummyMissionsInfo; -import com.ject.studytrip.dummy.application.service.DummyMissionCommandService; -import com.ject.studytrip.dummy.application.service.DummyStampCommandService; -import com.ject.studytrip.dummy.application.service.DummyTripCommandService; -import com.ject.studytrip.member.application.service.MemberQueryService; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.domain.model.Trip; -import java.util.List; -import java.util.stream.Stream; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class DummyMissionFacade { - private final MemberQueryService memberQueryService; - - private final DummyTripCommandService dummyTripCommandService; - private final DummyStampCommandService dummyStampCommandService; - private final DummyMissionCommandService dummyMissionCommandService; - - public DummyMissionsInfo generateDummyMissions(Long memberId, String category, int count) { - Member member = memberQueryService.getValidMember(memberId); - - Trip trip = dummyTripCommandService.createDummyTrip(member, category, count); - Stamp stamp = dummyStampCommandService.createDummyStamp(trip, count); - List missions = - Stream.generate(() -> dummyMissionCommandService.createDummyMission(stamp)) - .limit(count) - .toList(); - - return DummyMissionsInfo.of(missions.stream().map(DummyMissionInfo::from).toList()); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/facade/DummyStampFacade.java b/src/main/java/com/ject/studytrip/dummy/application/facade/DummyStampFacade.java deleted file mode 100644 index bc123ba..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/facade/DummyStampFacade.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ject.studytrip.dummy.application.facade; - -import com.ject.studytrip.dummy.application.dto.DummyStampInfo; -import com.ject.studytrip.dummy.application.dto.DummyStampsInfo; -import com.ject.studytrip.dummy.application.service.DummyStampCommandService; -import com.ject.studytrip.dummy.application.service.DummyTripCommandService; -import com.ject.studytrip.member.application.service.MemberQueryService; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.domain.model.Trip; -import java.util.List; -import java.util.stream.IntStream; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class DummyStampFacade { - private final MemberQueryService memberQueryService; - - private final DummyTripCommandService dummyTripCommandService; - private final DummyStampCommandService dummyStampCommandService; - - public DummyStampsInfo generateDummyStamps(Long memberId, String category, int count) { - Member member = memberQueryService.getValidMember(memberId); - - Trip trip = dummyTripCommandService.createDummyTrip(member, category, count); - List stamps = - IntStream.rangeClosed(1, count) - .mapToObj(order -> dummyStampCommandService.createDummyStamp(trip, order)) - .toList(); - - return DummyStampsInfo.of(stamps.stream().map(DummyStampInfo::from).toList()); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyMissionCommandGenerator.java b/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyMissionCommandGenerator.java deleted file mode 100644 index 7ae33fd..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyMissionCommandGenerator.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ject.studytrip.dummy.application.generator; - -import com.ject.studytrip.dummy.application.dto.CreateDummyMissionCommand; - -public final class CreateDummyMissionCommandGenerator { - private CreateDummyMissionCommandGenerator() {} - - public static CreateDummyMissionCommand of() { - return CreateDummyMissionCommand.of("testMission"); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyStampCommandGenerator.java b/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyStampCommandGenerator.java deleted file mode 100644 index edc4af3..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyStampCommandGenerator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ject.studytrip.dummy.application.generator; - -import com.ject.studytrip.dummy.application.dto.CreateDummyStampCommand; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.time.LocalDate; - -public final class CreateDummyStampCommandGenerator { - private CreateDummyStampCommandGenerator() {} - - public static CreateDummyStampCommand of(TripCategory tripCategory, int stampOrder) { - return switch (tripCategory) { - case COURSE -> ofCourse(stampOrder); - case EXPLORE -> ofExplore(); - }; - } - - private static CreateDummyStampCommand ofCourse(int stampOrder) { - return CreateDummyStampCommand.of("testStamp", stampOrder, LocalDate.now().plusDays(10)); - } - - private static CreateDummyStampCommand ofExplore() { - return CreateDummyStampCommand.of("testStamp", 0, null); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyTripCommandGenerator.java b/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyTripCommandGenerator.java deleted file mode 100644 index 1a2e495..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/generator/CreateDummyTripCommandGenerator.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ject.studytrip.dummy.application.generator; - -import com.ject.studytrip.dummy.application.dto.CreateDummyTripCommand; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.time.LocalDate; - -public final class CreateDummyTripCommandGenerator { - private CreateDummyTripCommandGenerator() {} - - public static CreateDummyTripCommand of(TripCategory tripCategory) { - return switch (tripCategory) { - case COURSE -> ofCourse(); - case EXPLORE -> ofExplore(); - }; - } - - private static CreateDummyTripCommand ofCourse() { - return CreateDummyTripCommand.of( - "testTrip", "testMemo", TripCategory.COURSE, LocalDate.now().plusDays(10)); - } - - private static CreateDummyTripCommand ofExplore() { - return CreateDummyTripCommand.of("testTrip", "testMemo", TripCategory.EXPLORE, null); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.java b/src/main/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.java deleted file mode 100644 index 3d5108b..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.ject.studytrip.dummy.application.service; - -import com.ject.studytrip.dummy.application.dto.CreateDummyMissionCommand; -import com.ject.studytrip.dummy.application.generator.CreateDummyMissionCommandGenerator; -import com.ject.studytrip.mission.domain.factory.MissionFactory; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.stamp.domain.model.Stamp; -import org.springframework.stereotype.Service; - -@Service -public class DummyMissionCommandService { - - public Mission createDummyMission(Stamp stamp) { - CreateDummyMissionCommand command = CreateDummyMissionCommandGenerator.of(); - - return MissionFactory.create(stamp, command.name()); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/service/DummyStampCommandService.java b/src/main/java/com/ject/studytrip/dummy/application/service/DummyStampCommandService.java deleted file mode 100644 index a6ed863..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/service/DummyStampCommandService.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.ject.studytrip.dummy.application.service; - -import com.ject.studytrip.dummy.application.dto.CreateDummyStampCommand; -import com.ject.studytrip.dummy.application.generator.CreateDummyStampCommandGenerator; -import com.ject.studytrip.stamp.domain.factory.StampFactory; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.domain.model.Trip; -import org.springframework.stereotype.Service; - -@Service -public class DummyStampCommandService { - - public Stamp createDummyStamp(Trip trip, int stampOrder) { - CreateDummyStampCommand command = - CreateDummyStampCommandGenerator.of(trip.getCategory(), stampOrder); - - return StampFactory.create(trip, command.name(), command.stampOrder(), command.endDate()); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/application/service/DummyTripCommandService.java b/src/main/java/com/ject/studytrip/dummy/application/service/DummyTripCommandService.java deleted file mode 100644 index 98a7aad..0000000 --- a/src/main/java/com/ject/studytrip/dummy/application/service/DummyTripCommandService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.ject.studytrip.dummy.application.service; - -import com.ject.studytrip.dummy.application.dto.CreateDummyTripCommand; -import com.ject.studytrip.dummy.application.generator.CreateDummyTripCommandGenerator; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.factory.TripFactory; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import org.springframework.stereotype.Service; - -@Service -public class DummyTripCommandService { - - public Trip createDummyTrip(Member member, String category, int count) { - CreateDummyTripCommand command = - CreateDummyTripCommandGenerator.of(TripCategory.from(category)); - - return TripFactory.create( - member, - command.name(), - command.memo(), - command.tripCategory(), - command.endDate(), - count); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.java b/src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.java deleted file mode 100644 index bd7c376..0000000 --- a/src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ject.studytrip.dummy.presentation.controller; - -import com.ject.studytrip.dummy.application.dto.DummyMissionsInfo; -import com.ject.studytrip.dummy.application.facade.DummyMissionFacade; -import com.ject.studytrip.dummy.presentation.dto.response.LoadDummyMissionInfoResponse; -import com.ject.studytrip.global.common.response.StandardResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Dummy Mission", description = "더미 미션 API") -@RestController -@RequestMapping("/api/dummies/missions") -@RequiredArgsConstructor -@Validated -public class DummyMissionController { - private final DummyMissionFacade dummyMissionFacade; - - @Operation( - summary = "더미 미션 목록 조회", - description = "여행 카테고리와 생성할 더미 데이터 개수를 이용해 더미 미션 목록을 조회합니다.(DB 저장 X)") - @GetMapping - public ResponseEntity loadDummyMissions( - @AuthenticationPrincipal String memberId, - @RequestParam - @NotNull(message = "여행 카테고리는 필수 요청 값입니다.") - @Pattern( - regexp = "^(COURSE|EXPLORE)$", - message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.") - String category, - @RequestParam @Min(value = 1, message = "count는 1 이상이어야 합니다.") int count) { - DummyMissionsInfo result = - dummyMissionFacade.generateDummyMissions(Long.valueOf(memberId), category, count); - List responses = - result.missionInfos().stream().map(LoadDummyMissionInfoResponse::of).toList(); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), responses)); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyStampController.java b/src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyStampController.java deleted file mode 100644 index b8bd220..0000000 --- a/src/main/java/com/ject/studytrip/dummy/presentation/controller/DummyStampController.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ject.studytrip.dummy.presentation.controller; - -import com.ject.studytrip.dummy.application.dto.DummyStampsInfo; -import com.ject.studytrip.dummy.application.facade.DummyStampFacade; -import com.ject.studytrip.dummy.presentation.dto.response.LoadDummyStampInfoResponse; -import com.ject.studytrip.global.common.response.StandardResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "Dummy Stamp", description = "더미 스탬프 API") -@RestController -@RequestMapping("/api/dummies/stamps") -@RequiredArgsConstructor -@Validated -public class DummyStampController { - private final DummyStampFacade dummyStampFacade; - - @Operation( - summary = "더미 스탬프 목록 조회", - description = "여행 카테고리와 생성할 더미 데이터 개수를 이용해 더미 스탬프 목록을 조회합니다.") - @GetMapping - public ResponseEntity loadDummyStamps( - @AuthenticationPrincipal String memberId, - @RequestParam - @NotNull(message = "여행 카테고리는 필수 요청 값입니다.") - @Pattern( - regexp = "^(COURSE|EXPLORE)$", - message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.") - String category, - @RequestParam @Min(value = 1, message = "count는 1 이상이어야 합니다.") int count) { - DummyStampsInfo result = - dummyStampFacade.generateDummyStamps(Long.valueOf(memberId), category, count); - List responses = - result.stampsInfos().stream().map(LoadDummyStampInfoResponse::of).toList(); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), responses)); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyMissionInfoResponse.java b/src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyMissionInfoResponse.java deleted file mode 100644 index 3dbcac8..0000000 --- a/src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyMissionInfoResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ject.studytrip.dummy.presentation.dto.response; - -import com.ject.studytrip.dummy.application.dto.DummyMissionInfo; -import io.swagger.v3.oas.annotations.media.Schema; - -public record LoadDummyMissionInfoResponse( - @Schema(description = "미션 ID") Long missionId, - @Schema(description = "미션 이름") String missionName, - @Schema(description = "미션 완료여부") boolean completed) { - public static LoadDummyMissionInfoResponse of(DummyMissionInfo info) { - return new LoadDummyMissionInfoResponse( - info.missionId(), info.missionName(), info.completed()); - } -} diff --git a/src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyStampInfoResponse.java b/src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyStampInfoResponse.java deleted file mode 100644 index f86ffb1..0000000 --- a/src/main/java/com/ject/studytrip/dummy/presentation/dto/response/LoadDummyStampInfoResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ject.studytrip.dummy.presentation.dto.response; - -import com.ject.studytrip.dummy.application.dto.DummyStampInfo; -import io.swagger.v3.oas.annotations.media.Schema; - -public record LoadDummyStampInfoResponse( - @Schema(description = "스탬프 ID") Long stampId, - @Schema(description = "스탬프 이름") String stampName, - @Schema(description = "스탬프 순서") int stampOrder, - @Schema(description = "스탬프 종료일") String endDate, - @Schema(description = "스탬프에 속한 총 미션 개수") int totalMissions, - @Schema(description = "스탬프에 속한 완료된 미션 개수") int completedMissions, - @Schema(description = "스탬프 완료 여부") boolean completed) { - public static LoadDummyStampInfoResponse of(DummyStampInfo info) { - return new LoadDummyStampInfoResponse( - info.stampId(), - info.stampName(), - info.stampOrder(), - info.endDate(), - info.totalMissions(), - info.completedMissions(), - info.completed()); - } -} diff --git a/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java b/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java index 2c4a868..fedd1cc 100644 --- a/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java +++ b/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java @@ -156,8 +156,8 @@ private List collectImageUrlsForMember(Member member) { private void cascadeHardDeleteByMemberId(Long memberId) { // 자식 -> 부모 순으로 삭제 진행 - tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsByMember(memberId); - tripReportCommandService.hardDeleteTripReportsByMember(memberId); + tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsOwnedByMember(memberId); + tripReportCommandService.hardDeleteTripReportsOwnedByMember(memberId); studyLogDailyMissionCommandService.hardDeleteStudyLogDailyMissionsOwnedByMember(memberId); pomodoroCommandService.hardDeletePomodorosOwnedByMember(memberId); diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.java b/src/main/java/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.java deleted file mode 100644 index 3b7a2df..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -public record PresignedTripReportImageInfo(Long tripReportId, String tmpKey, String presignedUrl) { - public static PresignedTripReportImageInfo of( - Long tripReportId, String tmpKey, String presignedUrl) { - return new PresignedTripReportImageInfo(tripReportId, tmpKey, presignedUrl); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripReportDetail.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripReportDetail.java deleted file mode 100644 index d322b0c..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripReportDetail.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo; - -public record TripReportDetail(TripReportInfo tripReportInfo, StudyLogSliceInfo studyLogSliceInfo) { - public static TripReportDetail from( - TripReportInfo tripReportInfo, StudyLogSliceInfo studyLogSliceInfo) { - return new TripReportDetail(tripReportInfo, studyLogSliceInfo); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripReportInfo.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripReportInfo.java deleted file mode 100644 index cfdba43..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripReportInfo.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.global.util.DateUtil; -import com.ject.studytrip.trip.domain.model.TripReport; - -public record TripReportInfo( - Long tripReportId, - String title, - String content, - String startDate, - String endDate, - long studyLogCount, - long totalFocusHours, - long studyDays, - String imageTitle, - String imageUrl, - String createdAt, - String updatedAt, - String deletedAt) { - public static TripReportInfo from(TripReport tripReport) { - return new TripReportInfo( - tripReport.getId(), - tripReport.getTitle(), - tripReport.getContent(), - tripReport.getStartDate(), - tripReport.getEndDate(), - tripReport.getStudyLogCount(), - tripReport.getTotalFocusHours(), - tripReport.getStudyDays(), - tripReport.getImageTitle(), - tripReport.getImageUrl(), - DateUtil.formatDateTime(tripReport.getCreatedAt()), - DateUtil.formatDateTime(tripReport.getUpdatedAt()), - DateUtil.formatDateTime(tripReport.getDeletedAt())); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripReportsInfo.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripReportsInfo.java deleted file mode 100644 index 64582a0..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripReportsInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import java.util.List; - -public record TripReportsInfo(List tripReportInfos) { - public static TripReportsInfo of(List tripReportInfos) { - return new TripReportsInfo(tripReportInfos); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.java deleted file mode 100644 index 3c7ed36..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo; - -public record TripRetrospectDetail( - TripRetrospectSummary summary, TripInfo tripInfo, StudyLogSliceInfo studyLogDetailSlice) { - public static TripRetrospectDetail from( - TripRetrospectSummary summary, - TripInfo tripInfo, - StudyLogSliceInfo studyLogDetailSlice) { - return new TripRetrospectDetail(summary, tripInfo, studyLogDetailSlice); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.java deleted file mode 100644 index a442fc9..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import java.util.List; - -public record TripRetrospectSummary( - long studyLogCount, // 학습 로그 개수 - long totalFocusHours, // 총 집중 시간(시간 단위) - long studyDays, // 학습한 일수(중복 날짜 제거) - List studyLogIds // 학습 로그 ID 목록 - ) { - public static TripRetrospectSummary of( - long studyLogCount, long totalFocusHours, long studyDays, List studyLogIds) { - return new TripRetrospectSummary(studyLogCount, totalFocusHours, studyDays, studyLogIds); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/facade/TripReportFacade.java b/src/main/java/com/ject/studytrip/trip/application/facade/TripReportFacade.java deleted file mode 100644 index 6ccffa5..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/facade/TripReportFacade.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.ject.studytrip.trip.application.facade; - -import static com.ject.studytrip.global.common.constants.CacheNameConstants.TRIP_REPORT; -import static com.ject.studytrip.global.common.constants.CacheNameConstants.TRIP_REPORTS; - -import com.ject.studytrip.image.application.dto.PresignedImageInfo; -import com.ject.studytrip.image.application.service.ImageService; -import com.ject.studytrip.member.application.service.MemberQueryService; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.pomodoro.application.service.PomodoroQueryService; -import com.ject.studytrip.studylog.application.dto.StudyLogDetail; -import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo; -import com.ject.studytrip.studylog.application.service.StudyLogDailyMissionQueryService; -import com.ject.studytrip.studylog.application.service.StudyLogQueryService; -import com.ject.studytrip.studylog.domain.model.StudyLog; -import com.ject.studytrip.studylog.domain.model.StudyLogDailyMission; -import com.ject.studytrip.trip.application.dto.*; -import com.ject.studytrip.trip.application.service.TripQueryService; -import com.ject.studytrip.trip.application.service.TripReportCommandService; -import com.ject.studytrip.trip.application.service.TripReportQueryService; -import com.ject.studytrip.trip.application.service.TripReportStudyLogCommandService; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest; -import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cache.annotation.Caching; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class TripReportFacade { - private static final String TRIP_REPORT_IMAGE_KEY_PREFIX = "trip-reports"; - - private final MemberQueryService memberQueryService; - private final TripQueryService tripQueryService; - private final StudyLogQueryService studyLogQueryService; - private final StudyLogDailyMissionQueryService studyLogDailyMissionQueryService; - private final PomodoroQueryService pomodoroQueryService; - private final TripReportQueryService tripReportQueryService; - - private final TripReportCommandService tripReportCommandService; - private final TripReportStudyLogCommandService tripReportStudyLogCommandService; - - private final ImageService imageService; - - @Transactional(readOnly = true) - public TripRetrospectDetail getTripRetrospect(Long memberId, Long tripId, int page, int size) { - Member member = memberQueryService.getValidMember(memberId); - Trip trip = tripQueryService.getValidCompletedTrip(member.getId(), tripId); // 완료된 여행 - Slice studyLogSlice = - studyLogQueryService.getStudyLogsSliceByTripId(trip.getId(), page, size, "LATEST"); - - long studyLogCount = studyLogQueryService.getStudyLogCountByTripId(trip.getId()); - long totalFocusHours = pomodoroQueryService.getTotalFocusHoursByTripId(trip.getId()); - long studyDays = - trip.getEndDate() != null - ? Math.max( - 0, - ChronoUnit.DAYS.between(trip.getStartDate(), trip.getEndDate()) + 1) - : 0L; - List studyLogIds = studyLogQueryService.getStudyLogIdsByTripId(trip.getId()); - - TripRetrospectSummary summary = - TripRetrospectSummary.of(studyLogCount, totalFocusHours, studyDays, studyLogIds); - TripInfo tripInfo = TripInfo.from(trip, 0, 100); - StudyLogSliceInfo studyLogDetailSlice = buildStudyLogDetailsSlice(studyLogSlice); - - return TripRetrospectDetail.from(summary, tripInfo, studyLogDetailSlice); - } - - @Cacheable( - cacheNames = TRIP_REPORTS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReports(#memberId)") - @Transactional(readOnly = true) - public TripReportsInfo getTripReportsByMember(Long memberId) { - Member member = memberQueryService.getValidMember(memberId); - List tripReports = - tripReportQueryService.getTripReportsByMemberId(member.getId()); - - return TripReportsInfo.of(tripReports.stream().map(TripReportInfo::from).toList()); - } - - @Cacheable( - cacheNames = TRIP_REPORT, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReport(#memberId, #tripReportId, #page, #size)") - @Transactional(readOnly = true) - public TripReportDetail getTripReport(Long memberId, Long tripReportId, int page, int size) { - Member member = memberQueryService.getValidMember(memberId); - TripReport tripReport = - tripReportQueryService.getValidTripReport(member.getId(), tripReportId); - Slice studyLogSlice = - studyLogQueryService.getStudyLogsSliceByTripReportId( - tripReport.getId(), page, size); - - TripReportInfo tripReportInfo = TripReportInfo.from(tripReport); - StudyLogSliceInfo studyLogDetailSlice = buildStudyLogDetailsSlice(studyLogSlice); - - return TripReportDetail.from(tripReportInfo, studyLogDetailSlice); - } - - @CacheEvict( - cacheNames = TRIP_REPORTS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReports(#memberId)") - @Transactional - public TripReportInfo createTripReport(Long memberId, CreateTripReportRequest request) { - Member member = memberQueryService.getValidMember(memberId); - TripReport tripReport = tripReportCommandService.createTripReport(member, request); - List studyLogs = studyLogQueryService.getValidStudyLogs(request.studyLogIds()); - tripReportStudyLogCommandService.createTripReportStudyLogs(tripReport, studyLogs); - - return TripReportInfo.from(tripReport); - } - - @Caching( - evict = { - @CacheEvict( - cacheNames = TRIP_REPORTS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReports(#memberId)"), - @CacheEvict(cacheNames = TRIP_REPORT, allEntries = true) - }) - @Transactional - public void deleteTripReport(Long memberId, Long tripReportId) { - Member member = memberQueryService.getValidMember(memberId); - TripReport tripReport = - tripReportQueryService.getValidTripReport(member.getId(), tripReportId); - - tripReportCommandService.deleteTripReport(tripReport); - } - - @Transactional(readOnly = true) - public PresignedTripReportImageInfo issuePresignedUrl( - Long tripReportId, PresignTripReportImageRequest request) { - TripReport tripReport = tripReportQueryService.getTripReport(tripReportId); - - PresignedImageInfo info = - imageService.presign( - TRIP_REPORT_IMAGE_KEY_PREFIX, - tripReport.getId().toString(), - request.originFilename()); - - return PresignedTripReportImageInfo.of( - tripReport.getId(), info.tmpKey(), info.presignedUrl()); - } - - @Caching( - evict = { - @CacheEvict(cacheNames = TRIP_REPORTS, allEntries = true), - @CacheEvict(cacheNames = TRIP_REPORT, allEntries = true) - }) - @Transactional - public void confirmImage(Long tripReportId, ConfirmTripReportImageRequest request) { - TripReport tripReport = tripReportQueryService.getTripReport(tripReportId); - String imageUrl = imageService.confirm(request.tmpKey()); - - tripReportCommandService.updateImageUrl(tripReport, imageUrl); - } - - private StudyLogSliceInfo buildStudyLogDetailsSlice(Slice studyLogSlice) { - List studyLogIds = studyLogSlice.getContent().stream().map(StudyLog::getId).toList(); - - // 학습 로그별 학습 로그 데일리 미션 목록 그룹화 - Map> groupedStudyLogDailyMissions = - studyLogDailyMissionQueryService.getGroupedStudyLogDailyMissionsByStudyLogIds( - studyLogIds); - - List studyLogDetails = - studyLogSlice.getContent().stream() - .map( - studyLog -> - StudyLogDetail.from( - studyLog, - groupedStudyLogDailyMissions.get(studyLog.getId()))) - .toList(); - - return StudyLogSliceInfo.of(studyLogDetails, studyLogSlice.hasNext()); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java deleted file mode 100644 index d2f8ba8..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.factory.TripReportFactory; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.repository.TripReportCommandRepository; -import com.ject.studytrip.trip.domain.repository.TripReportRepository; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class TripReportCommandService { - private final TripReportRepository tripReportRepository; - private final TripReportCommandRepository tripReportCommandRepository; - - public TripReport createTripReport(Member member, CreateTripReportRequest request) { - TripReport tripReport = - TripReportFactory.create( - member, - request.title(), - request.content(), - request.startDate(), - request.endDate(), - request.studyLogCount(), - request.totalFocusHours(), - request.studyDays(), - request.imageTitle()); - - return tripReportRepository.save(tripReport); - } - - public void updateImageUrl(TripReport tripReport, String imageUrl) { - tripReport.updateImageUrl(imageUrl); - } - - public void deleteTripReport(TripReport tripReport) { - tripReport.updateDeletedAt(); - } - - public long hardDeleteTripReports() { - return tripReportCommandRepository.deleteAllByDeletedAtIsNotNull(); - } - - public long hardDeleteTripReportsOwnedByDeletedMember() { - return tripReportCommandRepository.deleteAllByDeletedMemberOwner(); - } - - public long hardDeleteTripReportsByMember(Long memberId) { - return tripReportCommandRepository.deleteAllByMemberId(memberId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripReportQueryService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripReportQueryService.java deleted file mode 100644 index a9e1141..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripReportQueryService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.trip.domain.error.TripReportErrorCode; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.policy.TripReportPolicy; -import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository; -import com.ject.studytrip.trip.domain.repository.TripReportRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class TripReportQueryService { - private final TripReportRepository tripReportRepository; - private final TripReportQueryRepository tripReportQueryRepository; - - public TripReport getTripReport(Long tripReportId) { - return tripReportRepository - .findById(tripReportId) - .orElseThrow(() -> new CustomException(TripReportErrorCode.TRIP_REPORT_NOT_FOUND)); - } - - public TripReport getValidTripReport(Long memberId, Long tripReportId) { - TripReport tripReport = - tripReportRepository - .findById(tripReportId) - .orElseThrow( - () -> - new CustomException( - TripReportErrorCode.TRIP_REPORT_NOT_FOUND)); - - TripReportPolicy.validateOwner(memberId, tripReport); - TripReportPolicy.validateNotDeleted(tripReport); - - return tripReport; - } - - public List getTripReportsByMemberId(Long memberId) { - return tripReportRepository.findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc( - memberId); - } - - public List getTripReportImageUrlsByMemberId(Long memberId) { - return tripReportQueryRepository.findImageUrlsByMemberId(memberId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java deleted file mode 100644 index ba6a395..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import com.ject.studytrip.studylog.domain.model.StudyLog; -import com.ject.studytrip.trip.domain.factory.TripReportStudyLogFactory; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.model.TripReportStudyLog; -import com.ject.studytrip.trip.domain.repository.TripReportStudyLogCommandRepository; -import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class TripReportStudyLogCommandService { - private final TripReportStudyLogRepository tripReportStudyLogRepository; - private final TripReportStudyLogCommandRepository tripReportStudyLogCommandRepository; - - public void createTripReportStudyLogs(TripReport tripReport, List studyLogs) { - List tripReportStudyLogs = - studyLogs.stream() - .map(studyLog -> TripReportStudyLogFactory.create(tripReport, studyLog)) - .toList(); - - tripReportStudyLogRepository.saveAll(tripReportStudyLogs); - } - - public long hardDeleteTripReportStudyLogsOwnedByDeletedMember() { - return tripReportStudyLogCommandRepository.deleteAllByDeletedMemberOwner(); - } - - public long hardDeleteTripReportStudyLogsByMember(Long memberId) { - return tripReportStudyLogCommandRepository.deleteAllByMemberId(memberId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/error/TripReportErrorCode.java b/src/main/java/com/ject/studytrip/trip/domain/error/TripReportErrorCode.java deleted file mode 100644 index 1da3920..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/error/TripReportErrorCode.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ject.studytrip.trip.domain.error; - -import com.ject.studytrip.global.exception.error.ErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@RequiredArgsConstructor -public enum TripReportErrorCode implements ErrorCode { - // 400 - TRIP_REPORT_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 여행 리포트입니다."), - - // 403 - NOT_TRIP_REPORT_OWNER(HttpStatus.FORBIDDEN, "요청한 여행 리포트 정보를 조회할 권한이 없습니다."), - - // 404 - TRIP_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 여행 리포트가 존재하지 않습니다."); - - private final HttpStatus status; - private final String message; - - @Override - public String getName() { - return this.name(); - } - - @Override - public HttpStatus getStatus() { - return this.status; - } - - @Override - public String getMessage() { - return this.message; - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/factory/TripReportFactory.java b/src/main/java/com/ject/studytrip/trip/domain/factory/TripReportFactory.java deleted file mode 100644 index b7e5c8b..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/factory/TripReportFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.ject.studytrip.trip.domain.factory; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.model.TripReport; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TripReportFactory { - public static TripReport create( - Member member, - String title, - String content, - String startDate, - String endDate, - long studyLogCount, - long totalFocusHours, - long studyDays, - String imageTitle) { - return TripReport.of( - member, - title, - content, - startDate, - endDate, - studyLogCount, - totalFocusHours, - studyDays, - imageTitle); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.java b/src/main/java/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.java deleted file mode 100644 index a6fd2e4..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ject.studytrip.trip.domain.factory; - -import com.ject.studytrip.studylog.domain.model.StudyLog; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.model.TripReportStudyLog; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TripReportStudyLogFactory { - public static TripReportStudyLog create(TripReport tripReport, StudyLog studyLog) { - return TripReportStudyLog.of(tripReport, studyLog); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/policy/TripReportPolicy.java b/src/main/java/com/ject/studytrip/trip/domain/policy/TripReportPolicy.java deleted file mode 100644 index ac29c8a..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/policy/TripReportPolicy.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ject.studytrip.trip.domain.policy; - -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.trip.domain.error.TripReportErrorCode; -import com.ject.studytrip.trip.domain.model.TripReport; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TripReportPolicy { - public static void validateOwner(Long memberId, TripReport tripReport) { - if (!tripReport.getMember().getId().equals(memberId)) { - throw new CustomException(TripReportErrorCode.NOT_TRIP_REPORT_OWNER); - } - } - - public static void validateNotDeleted(TripReport tripReport) { - if (tripReport.getDeletedAt() != null) - throw new CustomException(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java index 9a7d82d..075c91f 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java +++ b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportQueryRepository.java @@ -1,7 +1,10 @@ package com.ject.studytrip.trip.domain.repository; +import com.ject.studytrip.trip.domain.model.TripReport; import java.util.List; public interface TripReportQueryRepository { + List findAllActiveByMemberId(Long memberId); + List findImageUrlsByMemberId(Long memberId); } diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportRepository.java deleted file mode 100644 index 4b9f05f..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ject.studytrip.trip.domain.repository; - -import com.ject.studytrip.trip.domain.model.TripReport; -import java.util.List; -import java.util.Optional; - -public interface TripReportRepository { - Optional findById(Long tripReportId); - - List findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc(Long memberId); - - TripReport save(TripReport tripReport); -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.java deleted file mode 100644 index 2ad2660..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ject.studytrip.trip.domain.repository; - -import com.ject.studytrip.trip.domain.model.TripReportStudyLog; -import java.util.List; - -public interface TripReportStudyLogRepository { - void saveAll(List tripReportStudyLogs); -} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.java deleted file mode 100644 index fcdf417..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.TripReport; -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface TripReportJpaRepository extends JpaRepository { - List findAllByMember_IdAndDeletedAtIsNullOrderByCreatedAtDesc(Long memberId); -} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.java deleted file mode 100644 index d3b08a6..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.repository.TripReportRepository; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class TripReportRepositoryAdapter implements TripReportRepository { - private final TripReportJpaRepository tripReportJpaRepository; - - @Override - public Optional findById(Long tripReportId) { - return tripReportJpaRepository.findById(tripReportId); - } - - @Override - public List findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc(Long memberId) { - return tripReportJpaRepository.findAllByMember_IdAndDeletedAtIsNullOrderByCreatedAtDesc( - memberId); - } - - @Override - public TripReport save(TripReport tripReport) { - return tripReportJpaRepository.save(tripReport); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.java deleted file mode 100644 index 7f9acd2..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.TripReportStudyLog; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface TripReportStudyLogJpaRepository extends JpaRepository {} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.java deleted file mode 100644 index db52358..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.TripReportStudyLog; -import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class TripReportStudyLogRepositoryAdapter implements TripReportStudyLogRepository { - private final TripReportStudyLogJpaRepository tripReportStudyLogJpaRepository; - - @Override - public void saveAll(List tripReportStudyLogs) { - tripReportStudyLogJpaRepository.saveAll(tripReportStudyLogs); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java index 2789122..a55a889 100644 --- a/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/trip/infra/querydsl/TripReportQueryRepositoryAdapter.java @@ -2,6 +2,7 @@ import static com.ject.studytrip.trip.domain.model.QTripReport.tripReport; +import com.ject.studytrip.trip.domain.model.TripReport; import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; @@ -13,6 +14,15 @@ public class TripReportQueryRepositoryAdapter implements TripReportQueryRepository { private final JPAQueryFactory queryFactory; + @Override + public List findAllActiveByMemberId(Long memberId) { + return queryFactory + .selectFrom(tripReport) + .where(tripReport.member.id.eq(memberId), tripReport.deletedAt.isNull()) + .orderBy(tripReport.createdAt.desc()) + .fetch(); + } + @Override public List findImageUrlsByMemberId(Long memberId) { return queryFactory diff --git a/src/main/java/com/ject/studytrip/trip/presentation/controller/TripReportController.java b/src/main/java/com/ject/studytrip/trip/presentation/controller/TripReportController.java deleted file mode 100644 index 853ff12..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/controller/TripReportController.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.ject.studytrip.trip.presentation.controller; - -import com.ject.studytrip.global.common.response.StandardResponse; -import com.ject.studytrip.trip.application.dto.*; -import com.ject.studytrip.trip.application.facade.TripReportFacade; -import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest; -import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest; -import com.ject.studytrip.trip.presentation.dto.response.*; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "TripReport", description = "여행 리포트 API") -@RestController -@RequestMapping -@RequiredArgsConstructor -@Validated -public class TripReportController { - private final TripReportFacade tripReportFacade; - - @Operation(summary = "여행 회고", description = "사용자가 여행을 완료한 후, 사용자가 진행했던 여행을 회고합니다.") - @GetMapping("/api/trips/{tripId}/retrospect") - public ResponseEntity loadTripRetrospect( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @RequestParam(name = "page", defaultValue = "0") @Min(0) int page, - @RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) int size) { - TripRetrospectDetail result = - tripReportFacade.getTripRetrospect(Long.valueOf(memberId), tripId, page, size); - - return ResponseEntity.status(HttpStatus.OK) - .body( - StandardResponse.success( - HttpStatus.OK.value(), - LoadTripRetrospectDetailResponse.of( - result.summary(), - result.tripInfo(), - result.studyLogDetailSlice()))); - } - - @Operation(summary = "여행 리포트 목록 조회", description = "사용자가 작성한 여행 리포트 목록을 조회합니다.") - @GetMapping("/api/trip-reports") - public ResponseEntity loadTripReports( - @AuthenticationPrincipal String memberId) { - TripReportsInfo result = tripReportFacade.getTripReportsByMember(Long.valueOf(memberId)); - - return ResponseEntity.status(HttpStatus.OK) - .body( - StandardResponse.success( - HttpStatus.OK.value(), - LoadTripReportsResponse.of(result.tripReportInfos()))); - } - - @Operation(summary = "여행 리포트 상세 조회", description = "사용자가 작성한 여행 리포트를 상세 조회합니다.") - @GetMapping("/api/trip-reports/{tripReportId}") - public ResponseEntity loadTripReport( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") Long tripReportId, - @RequestParam(name = "page", defaultValue = "0") @Min(0) int page, - @RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) int size) { - TripReportDetail result = - tripReportFacade.getTripReport(Long.valueOf(memberId), tripReportId, page, size); - - return ResponseEntity.status(HttpStatus.OK) - .body( - StandardResponse.success( - HttpStatus.OK.value(), - LoadTripReportDetailResponse.of( - result.tripReportInfo(), result.studyLogSliceInfo()))); - } - - @Operation(summary = "여행 리포트 생성", description = "사용자가 여행 회고에서 얻은 정보와 회고록을 기반으로 여행 리포트를 생성합니다.") - @PostMapping("/api/trip-reports") - public ResponseEntity createTripReport( - @AuthenticationPrincipal String memberId, - @RequestBody @Valid CreateTripReportRequest request) { - TripReportInfo result = tripReportFacade.createTripReport(Long.valueOf(memberId), request); - - return ResponseEntity.status(HttpStatus.CREATED) - .body( - StandardResponse.success( - HttpStatus.CREATED.value(), CreateTripReportResponse.of(result))); - } - - @Operation(summary = "여행 리포트 삭제", description = "사용자가 작성한 여행 리포트를 삭제합니다.") - @DeleteMapping("/api/trip-reports/{tripReportId}") - public ResponseEntity deleteTripReport( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") Long tripReportId) { - tripReportFacade.deleteTripReport(Long.valueOf(memberId), tripReportId); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } - - @Operation( - summary = "여행 리포트 이미지 업로드용 Presigned URL 발급", - description = - """ - 여행 리포트 이미지를 S3에 업로드하기 위한 Presigned URL을 발급합니다. - - [흐름] - 1) 먼저 여행 리포트 생성 API를 호출해 TripReportId를 응답받습니다. - 2) 사용자가 이미지를 첨부했을 경우, 생성 시 받은 TripReportId를 PathVariable로 전달하여 - 업로드용 파일명 정보를 함께 Presigned URL 발급 API를 요청합니다. - 서버는 업로드에 사용할 Presigned PUT URL과 임시키(tmpKey)를 반환합니다. - 3) 반환받은 Presigned URL로 PUT 요청을 통해 이미지를 S3에 업로드합니다. - 4) 업로드가 정상적으로 완료되면 바로 학습 로그 이미지 Confirm API를 호출합니다. - 이때 Presigned URL 발급 API에서 반환받은 임시키(tmpKey)를 함께 요청합니다. - 서버는 업로드된 이미지를 검증(크키/MIME)하고 확정합니다. - - [주의] - - 여행 리포트 이미지 Presigned URL 발급 요청 API는 TripReportId가 필요하기 때문에 필수로 본 API를 호출하기 전 여행 리포트를 먼저 생성해야 합니다. - - 요청 값의 originFilename은 꼭 파일 확장자를 포함한 파일명으로 요청해야합니다. - - Presigned URL 유효시간은 짧습니다(예: 10분). 만료되면 재발급해야 합니다. - """) - @PostMapping("/api/trip-reports/{tripReportId}/images/presigned") - public ResponseEntity presigned( - @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") Long tripReportId, - @RequestBody @Valid PresignTripReportImageRequest request) { - PresignedTripReportImageInfo info = - tripReportFacade.issuePresignedUrl(tripReportId, request); - - return ResponseEntity.ok() - .body( - StandardResponse.success( - HttpStatus.OK.value(), - PresignedTripReportImageResponse.of( - info.tripReportId(), info.tmpKey(), info.presignedUrl()))); - } - - @Operation( - summary = "업로드된 여행 리포트 이미지 검증/확정", - description = - """ - Presigned URL을 통해 S3에 업로드된 여행 리포트 이미지를 서버에서 검증하고 확정(Confirm)합니다. - - [흐름] - 1) 클라이언트는 발급받은 URL로 이미지를 업로드합니다. - 2) 업로드 완료 후, Presigned URL 발급 API에서 응답받은 임시키(tmpKey)를 포함해 해당 API를 호출합니다. - 임시키(tmpKey)는 Presigned URL의 전체 경로 중, 버킷 호스트명을 제외한 S3 객체 경로(ObjectKey) 입니다. - (예: https://bucket.s3.ap-northeast-2.amazonaws.com/tmp/report-logs/1/abc.jpg -> tmp/report-logs/1/abc.jpg) - - 3) 서버는 이미지 존재 여부, 크기, MIME 타입 등을 검증한 뒤 최종 경로로 이동시키고 여행 리포트 이미지 정보를 갱신합니다. - 만약 S3 Storage 기술 자체 에러가 발생하면 임시 경로에 저장된 이미지를 즉시 삭제하지 않아 컨펌 재시도가 가능하지만, - 유효하지 않은 이미지 크기/확장자 등 도메인 정책을 위반해 실패할 경우 임시 경로에 저장된 이미지가 즉시 삭제되며 다시 업로드부터 수행해야합니다. - - S3 Storage 기술 자체 예외 예시 - { - "status": 502 (BAD_GATEWAY), - "message": "Storage 서버 에러가 발생했습니다." - } - - 이미지 도메인 정책 위반 예외 예시 - { - "status": 400 (BAD_REQUEST), - "message": "유효하지 않은 이미지 확장자 입니다." , "유효하지 않은 이미지 MIME 입니다." 등 - } - - [주의] - - 이미지 타입(MIME/Content-Type)은 JPG, JPEG, PNG, WEBP만 허용합니다. 그 외 타입은 도메인 정책 위반으로 예외가 발생합니다. - - 이미지 최대 크기는 5MB로 설정되어있으며, 크기가 0 이하이거나 최대 크기를 벗어날 경우 도메인 정책 위반으로 예외가 발생합니다. - - 업로드는 되었지만 그 이후 문제가 발생하더라고 tmp/ 경로의 객체는 라이프사이클 정책에 따라 자동 정리되기 때문에 따로 삭제 요청 API는 호출하지 않아도 됩니다. - """) - @PostMapping("/api/trip-reports/{tripReportId}/images/confirm") - public ResponseEntity confirm( - @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") Long tripReportId, - @RequestBody @Valid ConfirmTripReportImageRequest request) { - tripReportFacade.confirmImage(tripReportId, request); - - return ResponseEntity.ok().body(StandardResponse.success(HttpStatus.OK.value(), null)); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.java deleted file mode 100644 index 9b56779..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; - -public record ConfirmTripReportImageRequest( - @Schema(description = "업로드된 이미지 임시키") @NotEmpty(message = "업로드된 이미지 임시키는 필수 요청 값입니다.") - String tmpKey) {} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.java deleted file mode 100644 index 412b078..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import java.util.List; - -public record CreateTripReportRequest( - @Schema(description = "여행 리포트 제목") @NotEmpty(message = "여행 리포트 제목은 필수 요청 값입니다.") - String title, - @Schema(description = "여행 리포트 내용") @NotEmpty(message = "여행 리포트 내용은 필수 요청 값입니다.") - String content, - @Schema(description = "여행 시작일") @NotEmpty(message = "여행 시작일은 필수 요청 값입니다.") String startDate, - @Schema(description = "여행 종료일") String endDate, - @Schema(description = "학습 로그 개수 (세션 성공)") @NotNull(message = "학습 로그 개수는 필수 요청 값입니다.") - long studyLogCount, - @Schema(description = "총 학습 시간") @NotNull(message = "총 학습 시간은 필수 요청 값입니다.") - long totalFocusHours, - @Schema(description = "연속 학습일") @NotNull(message = "연속 학습일은 필수 요청 값입니다.") long studyDays, - @Schema(description = "이미지 제목") String imageTitle, - @Schema(description = "학스 로그 ID 목록") @NotEmpty(message = "학습 로그 ID 목록은 최소 1개 이상이어야 합니다.") - List<@NotNull(message = "학습 로그 ID는 필수 요청 값입니다.") Long> studyLogIds) {} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.java deleted file mode 100644 index 0bf029a..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; - -public record PresignTripReportImageRequest( - @Schema(description = "원본 이미지 파일명") @NotEmpty(message = "원본 이미지 파일명은 필수 요청 값입니다.") - String originFilename) {} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.java deleted file mode 100644 index 2a210de..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.trip.application.dto.TripReportInfo; -import io.swagger.v3.oas.annotations.media.Schema; - -public record CreateTripReportResponse(@Schema(name = "여행 리포트 ID") Long tripReportId) { - public static CreateTripReportResponse of(TripReportInfo tripReportInfo) { - return new CreateTripReportResponse(tripReportInfo.tripReportId()); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.java deleted file mode 100644 index 1928822..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo; -import com.ject.studytrip.studylog.presentation.dto.response.LoadStudyLogsSliceResponse; -import com.ject.studytrip.trip.application.dto.TripReportInfo; -import io.swagger.v3.oas.annotations.media.Schema; - -public record LoadTripReportDetailResponse( - @Schema(description = "여행 리포트 ID") Long tripReportId, - @Schema(description = "여행 리포트 제목") String title, - @Schema(description = "여행 리포트 내용") String content, - @Schema(description = "여행 시작일 (여행 회고)") String startDate, - @Schema(description = "여행 종료일 (여행 회고)") String endDate, - @Schema(description = "총 학습 시간") long totalFocusHours, - @Schema(description = "학습 로그 개수 (세션 성공)") long studyLogCount, - @Schema(description = "연속 학습일") long studyDays, - @Schema(description = "여행 리포트 이미지 제목") String imageTitle, - @Schema(description = "여행 리포트 이미지 URL") String imageUrl, - @Schema(description = "학습 로그 히스토리") LoadStudyLogsSliceResponse history) { - public static LoadTripReportDetailResponse of( - TripReportInfo tripReportInfo, StudyLogSliceInfo studyLogSliceInfo) { - return new LoadTripReportDetailResponse( - tripReportInfo.tripReportId(), - tripReportInfo.title(), - tripReportInfo.content(), - tripReportInfo.startDate(), - tripReportInfo.endDate(), - tripReportInfo.totalFocusHours(), - tripReportInfo.studyLogCount(), - tripReportInfo.studyDays(), - tripReportInfo.imageTitle(), - tripReportInfo.imageUrl(), - LoadStudyLogsSliceResponse.of( - studyLogSliceInfo.getStudyLogDetails(), studyLogSliceInfo.getHasNext())); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.java deleted file mode 100644 index 338d6e9..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.trip.application.dto.TripReportInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -public record LoadTripReportsResponse( - TripReportSummary summary, List tripReports) { - public static LoadTripReportsResponse of(List tripReportInfos) { - return new LoadTripReportsResponse( - TripReportSummary.of(tripReportInfos), - tripReportInfos.stream().map(LoadTripReportInfoResponse::of).toList()); - } - - private record TripReportSummary( - @Schema(description = "여행 완료 수") long completedTripCount, - @Schema(description = "누적 학습 시간") long totalFocusHours, - @Schema(description = "가장 긴 학습 시간") long longestFocusHours) { - private static TripReportSummary of(List tripReportInfos) { - return new TripReportSummary( - tripReportInfos.size(), - tripReportInfos.stream().mapToLong(TripReportInfo::totalFocusHours).sum(), - tripReportInfos.stream() - .mapToLong(TripReportInfo::totalFocusHours) - .max() - .orElse(0)); - } - } - - private record LoadTripReportInfoResponse( - @Schema(description = "여행 리포트 ID") Long tripReportId, - @Schema(description = "여행 리포트 제목") String title, - @Schema(description = "여행 시작일 (여행 회고)") String startDate, - @Schema(description = "여행 종료일 (여행 회고)") String endDate, - @Schema(description = "총 학습 시간") long totalFocusHours, - @Schema(description = "여행 리포트 이미지 URL") String imageUrl) { - private static LoadTripReportInfoResponse of(TripReportInfo tripReportInfo) { - return new LoadTripReportInfoResponse( - tripReportInfo.tripReportId(), - tripReportInfo.title(), - tripReportInfo.startDate(), - tripReportInfo.endDate(), - tripReportInfo.totalFocusHours(), - tripReportInfo.imageUrl()); - } - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java deleted file mode 100644 index 41bda14..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo; -import com.ject.studytrip.studylog.presentation.dto.response.LoadStudyLogsSliceResponse; -import com.ject.studytrip.trip.application.dto.TripInfo; -import com.ject.studytrip.trip.application.dto.TripRetrospectSummary; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -public record LoadTripRetrospectDetailResponse( - @Schema(description = "여행 이름") String name, - @Schema(description = "여행 시작일") String startDate, - @Schema(description = "여행 종료일") String endDate, - @Schema(description = "총 학습 시간") long totalFocusHours, - @Schema(description = "학습 로그 개수 (세션 성공)") long studyLogCount, - @Schema(description = "연속 학습일") long studyDays, - @Schema(description = "학습 로그 ID 목록") List studyLogIds, - @Schema(description = "학습 로그 히스토리") LoadStudyLogsSliceResponse history) { - public static LoadTripRetrospectDetailResponse of( - TripRetrospectSummary tripRetrospectSummary, - TripInfo tripInfo, - StudyLogSliceInfo studyLogDetailSlice) { - return new LoadTripRetrospectDetailResponse( - tripInfo.getTripName(), - tripInfo.getStartDate(), - tripInfo.getEndDate(), - tripRetrospectSummary.totalFocusHours(), - tripRetrospectSummary.studyLogCount(), - tripRetrospectSummary.studyDays(), - tripRetrospectSummary.studyLogIds(), - LoadStudyLogsSliceResponse.of( - studyLogDetailSlice.getStudyLogDetails(), - studyLogDetailSlice.getHasNext())); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.java deleted file mode 100644 index ef20172..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record PresignedTripReportImageResponse( - @Schema(description = "여행 리포트 ID") Long tripReportId, - @Schema(description = "여행 리포트 이미지 임시키") String tmpKey, - @Schema(description = "여행 리포트 이미지 업로드용 Presigned URL") String presignedUrl) { - public static PresignedTripReportImageResponse of( - Long tripReportId, String tmpKey, String presignedUrl) { - return new PresignedTripReportImageResponse(tripReportId, tmpKey, presignedUrl); - } -} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.kt new file mode 100644 index 0000000..f77f52e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyMissionCommand.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.dummy.application.dto + +data class CreateDummyMissionCommand( + val name: String, +) { + companion object { + @JvmStatic + fun of(name: String): CreateDummyMissionCommand = CreateDummyMissionCommand(name) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.kt new file mode 100644 index 0000000..0e55c92 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyStampCommand.kt @@ -0,0 +1,18 @@ +package com.ject.studytrip.dummy.application.dto + +import java.time.LocalDate + +data class CreateDummyStampCommand( + val name: String, + val stampOrder: Int, + val endDate: LocalDate?, +) { + companion object { + @JvmStatic + fun of( + name: String, + stampOrder: Int, + endDate: LocalDate?, + ): CreateDummyStampCommand = CreateDummyStampCommand(name, stampOrder, endDate) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.kt new file mode 100644 index 0000000..4391c55 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/CreateDummyTripCommand.kt @@ -0,0 +1,21 @@ +package com.ject.studytrip.dummy.application.dto + +import com.ject.studytrip.trip.domain.model.TripCategory +import java.time.LocalDate + +data class CreateDummyTripCommand( + val name: String, + val memo: String, + val tripCategory: TripCategory, + val endDate: LocalDate?, +) { + companion object { + @JvmStatic + fun of( + name: String, + memo: String, + tripCategory: TripCategory, + endDate: LocalDate?, + ): CreateDummyTripCommand = CreateDummyTripCommand(name, memo, tripCategory, endDate) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.kt new file mode 100644 index 0000000..8ec1208 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionInfo.kt @@ -0,0 +1,17 @@ +package com.ject.studytrip.dummy.application.dto + +import com.ject.studytrip.mission.domain.model.Mission + +data class DummyMissionInfo( + val missionName: String, + val completed: Boolean, +) { + companion object { + @JvmStatic + fun from(mission: Mission): DummyMissionInfo = + DummyMissionInfo( + mission.name, + mission.isCompleted, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.kt new file mode 100644 index 0000000..050354f --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyMissionsInfo.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.dummy.application.dto + +data class DummyMissionsInfo( + val missionInfos: List, +) { + companion object { + @JvmStatic + fun of(missionInfos: List): DummyMissionsInfo = DummyMissionsInfo(missionInfos) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampInfo.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampInfo.kt new file mode 100644 index 0000000..3a0b619 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampInfo.kt @@ -0,0 +1,26 @@ +package com.ject.studytrip.dummy.application.dto + +import com.ject.studytrip.global.util.DateUtil +import com.ject.studytrip.stamp.domain.model.Stamp + +data class DummyStampInfo( + val stampName: String, + val stampOrder: Int, + val endDate: String?, + val totalMissions: Int, + val completedMissions: Int, + val completed: Boolean, +) { + companion object { + @JvmStatic + fun from(stamp: Stamp): DummyStampInfo = + DummyStampInfo( + stamp.name, + stamp.stampOrder, + DateUtil.formatDate(stamp.endDate), + stamp.totalMissions, + stamp.completedMissions, + stamp.isCompleted, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.kt new file mode 100644 index 0000000..23dfd1d --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/dto/DummyStampsInfo.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.dummy.application.dto + +data class DummyStampsInfo( + val stampsInfos: List, +) { + companion object { + @JvmStatic + fun of(stampsInfos: List): DummyStampsInfo = DummyStampsInfo(stampsInfos) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.kt new file mode 100644 index 0000000..7dda983 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyMissionFacade.kt @@ -0,0 +1,32 @@ +package com.ject.studytrip.dummy.application.facade + +import com.ject.studytrip.dummy.application.dto.DummyMissionInfo +import com.ject.studytrip.dummy.application.dto.DummyMissionsInfo +import com.ject.studytrip.dummy.application.service.DummyMissionCommandService +import com.ject.studytrip.dummy.application.service.DummyStampCommandService +import com.ject.studytrip.dummy.application.service.DummyTripCommandService +import com.ject.studytrip.member.application.service.MemberQueryService +import org.springframework.stereotype.Component + +@Component +class DummyMissionFacade( + // Query Service + private val memberQueryService: MemberQueryService, + // Command Service + private val dummyTripCommandService: DummyTripCommandService, + private val dummyStampCommandService: DummyStampCommandService, + private val dummyMissionCommandService: DummyMissionCommandService, +) { + fun generateDummyMissions( + memberId: Long, + category: String, + count: Int, + ): DummyMissionsInfo { + val member = memberQueryService.getValidMember(memberId) + val trip = dummyTripCommandService.createDummyTrip(member, category, count) + val stamp = dummyStampCommandService.createDummyStamp(trip, count) + val missions = List(count) { dummyMissionCommandService.createDummyMission(stamp) } + + return DummyMissionsInfo.of(missions.map { DummyMissionInfo.from(it) }) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyStampFacade.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyStampFacade.kt new file mode 100644 index 0000000..03059c1 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/facade/DummyStampFacade.kt @@ -0,0 +1,29 @@ +package com.ject.studytrip.dummy.application.facade + +import com.ject.studytrip.dummy.application.dto.DummyStampInfo +import com.ject.studytrip.dummy.application.dto.DummyStampsInfo +import com.ject.studytrip.dummy.application.service.DummyStampCommandService +import com.ject.studytrip.dummy.application.service.DummyTripCommandService +import com.ject.studytrip.member.application.service.MemberQueryService +import org.springframework.stereotype.Component + +@Component +class DummyStampFacade( + // Query Service + private val memberQueryService: MemberQueryService, + // Command Service + private val dummyTripCommandService: DummyTripCommandService, + private val dummyStampCommandService: DummyStampCommandService, +) { + fun generateDummyStamps( + memberId: Long, + category: String, + count: Int, + ): DummyStampsInfo { + val member = memberQueryService.getValidMember(memberId) + val trip = dummyTripCommandService.createDummyTrip(member, category, count) + val stamps = (1..count).map { order -> dummyStampCommandService.createDummyStamp(trip, order) } + + return DummyStampsInfo.of(stamps.map { DummyStampInfo.from(it) }) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.kt new file mode 100644 index 0000000..af05ef5 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandService.kt @@ -0,0 +1,16 @@ +package com.ject.studytrip.dummy.application.service + +import com.ject.studytrip.dummy.application.dto.CreateDummyMissionCommand +import com.ject.studytrip.mission.domain.factory.MissionFactory +import com.ject.studytrip.mission.domain.model.Mission +import com.ject.studytrip.stamp.domain.model.Stamp +import org.springframework.stereotype.Service + +@Service +class DummyMissionCommandService { + fun createDummyMission(stamp: Stamp): Mission { + val command = CreateDummyMissionCommand.of("testMission") + + return MissionFactory.create(stamp, command.name) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandService.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandService.kt new file mode 100644 index 0000000..03c8340 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandService.kt @@ -0,0 +1,25 @@ +package com.ject.studytrip.dummy.application.service + +import com.ject.studytrip.dummy.application.dto.CreateDummyStampCommand +import com.ject.studytrip.stamp.domain.factory.StampFactory +import com.ject.studytrip.stamp.domain.model.Stamp +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class DummyStampCommandService { + fun createDummyStamp( + trip: Trip, + stampOrder: Int, + ): Stamp { + val command = + when (trip.category) { + TripCategory.COURSE -> CreateDummyStampCommand.of("testStamp", stampOrder, LocalDate.now().plusDays(10)) + TripCategory.EXPLORE -> CreateDummyStampCommand.of("testStamp", 0, null) + } + + return StampFactory.create(trip, command.name, command.stampOrder, command.endDate) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandService.kt b/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandService.kt new file mode 100644 index 0000000..3ba855a --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandService.kt @@ -0,0 +1,28 @@ +package com.ject.studytrip.dummy.application.service + +import com.ject.studytrip.dummy.application.dto.CreateDummyTripCommand +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.factory.TripFactory +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class DummyTripCommandService { + fun createDummyTrip( + member: Member, + category: String, + count: Int, + ): Trip { + val tripCategory = TripCategory.from(category) + + val command = + when (tripCategory) { + TripCategory.COURSE -> CreateDummyTripCommand.of("testTrip", "testMemo", tripCategory, LocalDate.now().plusDays(10)) + TripCategory.EXPLORE -> CreateDummyTripCommand.of("testTrip", "testMemo", tripCategory, null) + } + + return TripFactory.create(member, command.name, command.memo, command.tripCategory, command.endDate, count) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.kt b/src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.kt new file mode 100644 index 0000000..b7758b7 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionController.kt @@ -0,0 +1,48 @@ +package com.ject.studytrip.dummy.presentation.controller + +import com.ject.studytrip.dummy.application.facade.DummyMissionFacade +import com.ject.studytrip.dummy.presentation.response.dto.LoadDummyMissionInfoResponse +import com.ject.studytrip.global.common.response.StandardResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Dummy Mission", description = "더미 미션 API") +@RestController +@RequestMapping("/api/dummies/missions") +@Validated +class DummyMissionController( + private val dummyMissionFacade: DummyMissionFacade, +) { + @Operation( + summary = "더미 미션 목록 조회", + description = "여행 카테고리와 생성할 더미 데이터 개수를 이용해 더미 미션 목록을 조회합니다. (DB 저장 X)", + ) + @GetMapping + fun loadDummyMissions( + @AuthenticationPrincipal memberId: String, + @RequestParam @NotNull(message = "여행 카테고리는 필수 요청 값입니다.") + @Pattern( + regexp = "^(COURSE|EXPLORE)$", + message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.", + ) + category: String, + @RequestParam @Min(value = 1, message = "count는 1 이상이어야 합니다.") count: Int, + ): ResponseEntity { + val result = dummyMissionFacade.generateDummyMissions(memberId.toLong(), category, count) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), result.missionInfos.map(LoadDummyMissionInfoResponse::of))) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampController.kt b/src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampController.kt new file mode 100644 index 0000000..02f0e6e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampController.kt @@ -0,0 +1,48 @@ +package com.ject.studytrip.dummy.presentation.controller + +import com.ject.studytrip.dummy.application.facade.DummyStampFacade +import com.ject.studytrip.dummy.presentation.response.dto.LoadDummyStampInfoResponse +import com.ject.studytrip.global.common.response.StandardResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Dummy Stamp", description = "더미 스탬프 API") +@RestController +@RequestMapping("/api/dummies/stamps") +@Validated +class DummyStampController( + private val dummyStampFacade: DummyStampFacade, +) { + @Operation( + summary = "더미 스탬프 목록 조회", + description = "여행 카테고리와 생성할 더미 데이터 개수를 이용해 더미 스탬프 목록을 조회합니다. (DB 저장 X)", + ) + @GetMapping + fun loadDummyStamps( + @AuthenticationPrincipal memberId: String, + @RequestParam @NotNull(message = "여행 카테고리는 필수 요청 값입니다.") + @Pattern( + regexp = "^(COURSE|EXPLORE)$", + message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.", + ) + category: String, + @RequestParam @Min(value = 1, message = "count는 1 이상이어야 합니다.") count: Int, + ): ResponseEntity { + val result = dummyStampFacade.generateDummyStamps(memberId.toLong(), category, count) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), result.stampsInfos.map(LoadDummyStampInfoResponse::of))) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyMissionInfoResponse.kt b/src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyMissionInfoResponse.kt new file mode 100644 index 0000000..4643c5d --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyMissionInfoResponse.kt @@ -0,0 +1,20 @@ +package com.ject.studytrip.dummy.presentation.response.dto + +import com.ject.studytrip.dummy.application.dto.DummyMissionInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadDummyMissionInfoResponse( + @field:Schema(description = "미션 이름") + val missionName: String, + @field:Schema(description = "미션 완료 여부") + val completed: Boolean, +) { + companion object { + @JvmStatic + fun of(dummyMissionInfo: DummyMissionInfo): LoadDummyMissionInfoResponse = + LoadDummyMissionInfoResponse( + dummyMissionInfo.missionName, + dummyMissionInfo.completed, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyStampInfoResponse.kt b/src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyStampInfoResponse.kt new file mode 100644 index 0000000..5dff219 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/dummy/presentation/response/dto/LoadDummyStampInfoResponse.kt @@ -0,0 +1,32 @@ +package com.ject.studytrip.dummy.presentation.response.dto + +import com.ject.studytrip.dummy.application.dto.DummyStampInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadDummyStampInfoResponse( + @field:Schema(description = "스탬프 이름") + val stampName: String, + @field:Schema(description = "스탬프 순서") + val stampOrder: Int, + @field:Schema(description = "스탬프 종료일") + val endDate: String?, + @field:Schema(description = "스탬프에 속한 총 미션 개수") + val totalMissions: Int, + @field:Schema(description = "스탬프에 속한 완료된 미션 개수") + val completedMissions: Int, + @field:Schema(description = "스탬프 완료 여부") + val completed: Boolean, +) { + companion object { + @JvmStatic + fun of(dummyStampInfo: DummyStampInfo): LoadDummyStampInfoResponse = + LoadDummyStampInfoResponse( + dummyStampInfo.stampName, + dummyStampInfo.stampOrder, + dummyStampInfo.endDate, + dummyStampInfo.totalMissions, + dummyStampInfo.completedMissions, + dummyStampInfo.completed, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.kt new file mode 100644 index 0000000..e5c840f --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/PresignedTripReportImageInfo.kt @@ -0,0 +1,16 @@ +package com.ject.studytrip.trip.application.dto + +data class PresignedTripReportImageInfo( + val tripReportId: Long, + val tmpKey: String, + val presignedUrl: String, +) { + companion object { + @JvmStatic + fun of( + tripReportId: Long, + tmpKey: String, + presignedUrl: String, + ): PresignedTripReportImageInfo = PresignedTripReportImageInfo(tripReportId, tmpKey, presignedUrl) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportDetail.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportDetail.kt new file mode 100644 index 0000000..37a2f6b --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportDetail.kt @@ -0,0 +1,16 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo + +class TripReportDetail( + val tripReportInfo: TripReportInfo, + val studyLogSliceInfo: StudyLogSliceInfo, +) { + companion object { + @JvmStatic + fun from( + tripReportInfo: TripReportInfo, + studyLogSliceInfo: StudyLogSliceInfo, + ) = TripReportDetail(tripReportInfo, studyLogSliceInfo) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportInfo.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportInfo.kt new file mode 100644 index 0000000..ea2e47d --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportInfo.kt @@ -0,0 +1,40 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.global.util.DateUtil +import com.ject.studytrip.trip.domain.model.TripReport + +data class TripReportInfo( + val tripReportId: Long, + val title: String, + val content: String, + val startDate: String, + val endDate: String, + val studyLogCount: Long, + val totalFocusHours: Long, + val studyDays: Long, + val imageTitle: String?, + val imageUrl: String?, + val createdAt: String, + val updatedAt: String, + val deletedAt: String?, +) { + companion object { + @JvmStatic + fun from(tripReport: TripReport): TripReportInfo = + TripReportInfo( + tripReport.id, + tripReport.title, + tripReport.content, + tripReport.startDate, + tripReport.endDate, + tripReport.studyLogCount, + tripReport.totalFocusHours, + tripReport.studyDays, + tripReport.imageTitle, + tripReport.imageUrl, + DateUtil.formatDateTime(tripReport.createdAt), + DateUtil.formatDateTime(tripReport.updatedAt), + tripReport.deletedAt?.let { DateUtil.formatDateTime(it) }, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportsInfo.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportsInfo.kt new file mode 100644 index 0000000..90a17c1 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripReportsInfo.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.trip.application.dto + +data class TripReportsInfo( + val tripReportInfos: List, +) { + companion object { + @JvmStatic + fun of(tripReportInfos: List): TripReportsInfo = TripReportsInfo(tripReportInfos) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.kt new file mode 100644 index 0000000..699053f --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectDetail.kt @@ -0,0 +1,18 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo + +data class TripRetrospectDetail( + val summary: TripRetrospectSummary, + val tripInfo: TripInfo, + val studyLogSliceInfo: StudyLogSliceInfo, +) { + companion object { + @JvmStatic + fun from( + summary: TripRetrospectSummary, + tripInfo: TripInfo, + studyLogSliceInfo: StudyLogSliceInfo, + ): TripRetrospectDetail = TripRetrospectDetail(summary, tripInfo, studyLogSliceInfo) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.kt new file mode 100644 index 0000000..53897d1 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripRetrospectSummary.kt @@ -0,0 +1,24 @@ +package com.ject.studytrip.trip.application.dto + +data class TripRetrospectSummary( + val studyLogCount: Long, // 학습 로그 개수 + val totalFocusHours: Long, // 총 집중 시간 (시간 단위) + val studyDays: Long, // 학습한 일수 (중복 날짜 제거) + val studyLogIds: List, // 학습 로그 ID 목록 +) { + companion object { + @JvmStatic + fun of( + studyLogCount: Long, + totalFocusHours: Long, + studyDays: Long, + studyLogIds: List, + ): TripRetrospectSummary = + TripRetrospectSummary( + studyLogCount, + totalFocusHours, + studyDays, + studyLogIds, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/facade/TripReportFacade.kt b/src/main/kotlin/com/ject/studytrip/trip/application/facade/TripReportFacade.kt new file mode 100644 index 0000000..4dba90c --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/facade/TripReportFacade.kt @@ -0,0 +1,170 @@ +package com.ject.studytrip.trip.application.facade + +import com.ject.studytrip.global.common.constants.CacheNameConstants.TRIP_REPORT +import com.ject.studytrip.global.common.constants.CacheNameConstants.TRIP_REPORTS +import com.ject.studytrip.image.application.service.ImageService +import com.ject.studytrip.member.application.service.MemberQueryService +import com.ject.studytrip.pomodoro.application.service.PomodoroQueryService +import com.ject.studytrip.studylog.application.dto.StudyLogDetail +import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo +import com.ject.studytrip.studylog.application.service.StudyLogDailyMissionQueryService +import com.ject.studytrip.studylog.application.service.StudyLogQueryService +import com.ject.studytrip.studylog.domain.model.StudyLog +import com.ject.studytrip.trip.application.dto.TripInfo +import com.ject.studytrip.trip.application.dto.TripReportDetail +import com.ject.studytrip.trip.application.dto.TripReportInfo +import com.ject.studytrip.trip.application.dto.TripReportsInfo +import com.ject.studytrip.trip.application.dto.TripRetrospectDetail +import com.ject.studytrip.trip.application.dto.TripRetrospectSummary +import com.ject.studytrip.trip.application.service.TripQueryService +import com.ject.studytrip.trip.application.service.TripReportCommandService +import com.ject.studytrip.trip.application.service.TripReportQueryService +import com.ject.studytrip.trip.application.service.TripReportStudyLogCommandService +import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest +import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest +import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest +import com.ject.studytrip.trip.presentation.dto.response.PresignedTripReportImageResponse +import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.Cacheable +import org.springframework.cache.annotation.Caching +import org.springframework.data.domain.Slice +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.temporal.ChronoUnit + +@Component +class TripReportFacade( + // Query Service + private val memberQueryService: MemberQueryService, + private val tripQueryService: TripQueryService, + private val studyLogQueryService: StudyLogQueryService, + private val studyLogDailyMissionQueryService: StudyLogDailyMissionQueryService, + private val pomodoroQueryService: PomodoroQueryService, + private val tripReportQueryService: TripReportQueryService, + // Command Service + private val tripReportCommandService: TripReportCommandService, + private val tripReportStudyLogCommandService: TripReportStudyLogCommandService, + // Image Service + private val imageService: ImageService, +) { + companion object { + private const val TRIP_REPORT_IMAGE_KEY_PREFIX = "trip-reports" + } + + @CacheEvict(cacheNames = [TRIP_REPORTS], key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReports(#memberId)") + @Transactional + fun createTripReport( + memberId: Long, + request: CreateTripReportRequest, + ): TripReportInfo { + val member = memberQueryService.getValidMember(memberId) + val tripReport = tripReportCommandService.createTripReport(member, request) + val studyLogs = studyLogQueryService.getValidStudyLogs(request.studyLogIds) + + tripReportStudyLogCommandService.createTripReportStudyLogs(tripReport, studyLogs) + + return TripReportInfo.from(tripReport) + } + + @Caching( + evict = [ + CacheEvict( + cacheNames = [TRIP_REPORTS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReports(#memberId)", + ), + CacheEvict( + cacheNames = [TRIP_REPORT], + allEntries = true, + ), + ], + ) + @Transactional + fun deleteTripReport( + memberId: Long, + tripReportId: Long, + ) { + val tripReport = tripReportQueryService.getValidTripReport(memberId, tripReportId) + + tripReportCommandService.deleteTripReport(tripReport) + } + + @Transactional(readOnly = true) + fun getTripRetrospect( + memberId: Long, + tripId: Long, + page: Int, + size: Int, + ): TripRetrospectDetail { + val trip = tripQueryService.getValidCompletedTrip(memberId, tripId) + val studyLogSlice = studyLogQueryService.getStudyLogsSliceByTripId(trip.id, page, size, "LATEST") + val studyLogCount = studyLogQueryService.getStudyLogCountByTripId(trip.id) + val totalFocusHours = pomodoroQueryService.getTotalFocusHoursByTripId(trip.id) + val studyDays = trip.endDate?.let { maxOf(0, ChronoUnit.DAYS.between(trip.startDate, it) + 1) } ?: 0L + val studyLogIds = studyLogQueryService.getStudyLogIdsByTripId(trip.id) + + val summary = TripRetrospectSummary(studyLogCount, totalFocusHours, studyDays, studyLogIds) + val tripInfo = TripInfo.from(trip, 0, 100) + val studyLogSliceInfo = buildStudyLogSliceInfo(studyLogSlice) + + return TripRetrospectDetail.from(summary, tripInfo, studyLogSliceInfo) + } + + @Cacheable(cacheNames = [TRIP_REPORTS], key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReports(#memberId)") + @Transactional(readOnly = true) + fun getTripReportsByMember(memberId: Long): TripReportsInfo { + val tripReports = tripReportQueryService.getTripReportsByMemberId(memberId) + + return TripReportsInfo.of(tripReports.map { TripReportInfo.from(it) }) + } + + @Cacheable( + cacheNames = [TRIP_REPORT], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).tripReport(#memberId, #tripReportId, #page, #size)", + ) + @Transactional(readOnly = true) + fun getTripReport( + memberId: Long, + tripReportId: Long, + page: Int, + size: Int, + ): TripReportDetail { + val tripReport = tripReportQueryService.getValidTripReport(memberId, tripReportId) + val studyLogSlice = studyLogQueryService.getStudyLogsSliceByTripReportId(tripReport.id, page, size) + + val tripReportInfo = TripReportInfo.from(tripReport) + val studyLogSliceInfo = buildStudyLogSliceInfo(studyLogSlice) + + return TripReportDetail.from(tripReportInfo, studyLogSliceInfo) + } + + @Transactional(readOnly = true) + fun issuePresignedUrl( + tripReportId: Long, + request: PresignTripReportImageRequest, + ): PresignedTripReportImageResponse { + val tripReport = tripReportQueryService.getTripReport(tripReportId) + val info = imageService.presign(TRIP_REPORT_IMAGE_KEY_PREFIX, tripReport.id.toString(), request.originFilename) + + return PresignedTripReportImageResponse.of(tripReport.id, info.tmpKey, info.presignedUrl) + } + + fun confirmImage( + tripReportId: Long, + request: ConfirmTripReportImageRequest, + ) { + val tripReport = tripReportQueryService.getTripReport(tripReportId) + val imageUrl = imageService.confirm(request.tmpKey) + + tripReportCommandService.updateImageUrl(tripReport, imageUrl) + } + + private fun buildStudyLogSliceInfo(studyLogSlice: Slice): StudyLogSliceInfo { + val studyLogIds = studyLogSlice.content.map { it.id } + + // 학습 로그별 학습 로그 데일리 미션 목록 그룹화 + val groupedStudyLogDailyMissions = studyLogDailyMissionQueryService.getGroupedStudyLogDailyMissionsByStudyLogIds(studyLogIds) + val studyLogDetails = studyLogSlice.content.map { StudyLogDetail.from(it, groupedStudyLogDailyMissions[it.id]) } + + return StudyLogSliceInfo.of(studyLogDetails, studyLogSlice.hasNext()) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandService.kt b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandService.kt new file mode 100644 index 0000000..3b1df88 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandService.kt @@ -0,0 +1,48 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.factory.TripReportFactory +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.repository.TripReportCommandRepository +import com.ject.studytrip.trip.domain.repository.TripReportRepository +import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest +import org.springframework.stereotype.Service + +@Service +class TripReportCommandService( + private val tripReportRepository: TripReportRepository, + private val tripReportCommandRepository: TripReportCommandRepository, +) { + fun createTripReport( + member: Member, + request: CreateTripReportRequest, + ): TripReport { + val tripReport = + TripReportFactory.create( + member, + request.title, + request.content, + request.startDate, + request.endDate, + request.studyLogCount, + request.totalFocusHours, + request.studyDays, + request.imageTitle, + ) + + return tripReportRepository.save(tripReport) + } + + fun updateImageUrl( + tripReport: TripReport, + imageUrl: String, + ) = tripReport.updateImageUrl(imageUrl) + + fun deleteTripReport(tripReport: TripReport) = tripReport.updateDeletedAt() + + fun hardDeleteTripReports(): Long = tripReportCommandRepository.deleteAllByDeletedAtIsNotNull() + + fun hardDeleteTripReportsOwnedByDeletedMember(): Long = tripReportCommandRepository.deleteAllByDeletedMemberOwner() + + fun hardDeleteTripReportsOwnedByMember(memberId: Long): Long = tripReportCommandRepository.deleteAllByMemberId(memberId) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryService.kt b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryService.kt new file mode 100644 index 0000000..484341c --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryService.kt @@ -0,0 +1,39 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.trip.domain.error.TripReportErrorCode +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.policy.TripReportPolicy +import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository +import com.ject.studytrip.trip.domain.repository.TripReportRepository +import org.springframework.stereotype.Service + +@Service +class TripReportQueryService( + private val tripReportRepository: TripReportRepository, + private val tripReportQueryRepository: TripReportQueryRepository, +) { + fun getTripReport(tripReportId: Long): TripReport = + tripReportRepository + .findById(tripReportId) + .orElseThrow { CustomException(TripReportErrorCode.TRIP_REPORT_NOT_FOUND) } + + fun getValidTripReport( + memberId: Long, + tripReportId: Long, + ): TripReport { + val tripReport = + tripReportRepository + .findById(tripReportId) + .orElseThrow { CustomException(TripReportErrorCode.TRIP_REPORT_NOT_FOUND) } + + TripReportPolicy.validateOwner(memberId, tripReport) + TripReportPolicy.validateNotDeleted(tripReport) + + return tripReport + } + + fun getTripReportsByMemberId(memberId: Long): List = tripReportQueryRepository.findAllActiveByMemberId(memberId) + + fun getTripReportImageUrlsByMemberId(memberId: Long): List = tripReportQueryRepository.findImageUrlsByMemberId(memberId) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.kt b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.kt new file mode 100644 index 0000000..5b8ce3a --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandService.kt @@ -0,0 +1,27 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.studylog.domain.model.StudyLog +import com.ject.studytrip.trip.domain.factory.TripReportStudyLogFactory +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogCommandRepository +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository +import org.springframework.stereotype.Service + +@Service +class TripReportStudyLogCommandService( + private val tripReportStudyLogRepository: TripReportStudyLogRepository, + private val tripReportStudyLogCommandRepository: TripReportStudyLogCommandRepository, +) { + fun createTripReportStudyLogs( + tripReport: TripReport, + studyLogs: List, + ) { + val tripReportStudyLogs = studyLogs.map { TripReportStudyLogFactory.create(tripReport, it) } + + tripReportStudyLogRepository.saveAll(tripReportStudyLogs) + } + + fun hardDeleteTripReportStudyLogsOwnedByDeletedMember(): Long = tripReportStudyLogCommandRepository.deleteAllByDeletedMemberOwner() + + fun hardDeleteTripReportStudyLogsOwnedByMember(memberId: Long): Long = tripReportStudyLogCommandRepository.deleteAllByMemberId(memberId) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/error/TripReportErrorCode.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/error/TripReportErrorCode.kt new file mode 100644 index 0000000..10e3431 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/error/TripReportErrorCode.kt @@ -0,0 +1,25 @@ +package com.ject.studytrip.trip.domain.error + +import com.ject.studytrip.global.exception.error.ErrorCode +import org.springframework.http.HttpStatus + +enum class TripReportErrorCode( + private val status: HttpStatus, + private val message: String, +) : ErrorCode { + // 400 + TRIP_REPORT_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 여행 리포트입니다."), + + // 403 + NOT_TRIP_REPORT_OWNER(HttpStatus.FORBIDDEN, "여행 리포트를 수정/삭제할 권한이 없습니다."), + + // 404 + TRIP_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "요창한 여행 리포트를 찾을 수 없습니다."), + ; + + override fun getName(): String = name + + override fun getStatus(): HttpStatus = status + + override fun getMessage(): String = message +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportFactory.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportFactory.kt new file mode 100644 index 0000000..f8f6458 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportFactory.kt @@ -0,0 +1,19 @@ +package com.ject.studytrip.trip.domain.factory + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.model.TripReport + +object TripReportFactory { + @JvmStatic + fun create( + member: Member, + title: String, + content: String, + startDate: String, + endDate: String?, + studyLogCount: Long, + totalFocusHours: Long, + studyDays: Long, + imageTitle: String?, + ): TripReport = TripReport.of(member, title, content, startDate, endDate, studyLogCount, totalFocusHours, studyDays, imageTitle) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.kt new file mode 100644 index 0000000..58042b9 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripReportStudyLogFactory.kt @@ -0,0 +1,13 @@ +package com.ject.studytrip.trip.domain.factory + +import com.ject.studytrip.studylog.domain.model.StudyLog +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.model.TripReportStudyLog + +object TripReportStudyLogFactory { + @JvmStatic + fun create( + tripReport: TripReport, + studyLog: StudyLog, + ): TripReportStudyLog = TripReportStudyLog.of(tripReport, studyLog) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/policy/TripReportPolicy.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/policy/TripReportPolicy.kt new file mode 100644 index 0000000..893317f --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/policy/TripReportPolicy.kt @@ -0,0 +1,22 @@ +package com.ject.studytrip.trip.domain.policy + +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.trip.domain.error.TripReportErrorCode +import com.ject.studytrip.trip.domain.model.TripReport + +object TripReportPolicy { + fun validateNotDeleted(tripReport: TripReport) { + if (tripReport.isDeleted) { + throw CustomException(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED) + } + } + + fun validateOwner( + memberId: Long, + tripReport: TripReport, + ) { + if (tripReport.member.id != memberId) { + throw CustomException(TripReportErrorCode.NOT_TRIP_REPORT_OWNER) + } + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportRepository.kt new file mode 100644 index 0000000..99ceaab --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportRepository.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.trip.domain.repository + +import com.ject.studytrip.trip.domain.model.TripReport +import java.util.Optional + +interface TripReportRepository { + fun findById(tripReportId: Long): Optional + + fun save(tripReport: TripReport): TripReport +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.kt new file mode 100644 index 0000000..fc138d6 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripReportStudyLogRepository.kt @@ -0,0 +1,7 @@ +package com.ject.studytrip.trip.domain.repository + +import com.ject.studytrip.trip.domain.model.TripReportStudyLog + +interface TripReportStudyLogRepository { + fun saveAll(tripReportStudyLogs: List) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.kt new file mode 100644 index 0000000..dbac581 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.kt @@ -0,0 +1,6 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.TripReport +import org.springframework.data.jpa.repository.JpaRepository + +interface TripReportJpaRepository : JpaRepository diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.kt new file mode 100644 index 0000000..d969967 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.repository.TripReportRepository +import org.springframework.stereotype.Repository +import java.util.Optional + +@Repository +class TripReportRepositoryAdapter( + private val tripReportJpaRepository: TripReportJpaRepository, +) : TripReportRepository { + override fun findById(tripReportId: Long): Optional = tripReportJpaRepository.findById(tripReportId) + + override fun save(tripReport: TripReport): TripReport = tripReportJpaRepository.save(tripReport) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.kt new file mode 100644 index 0000000..c709757 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogJpaRepository.kt @@ -0,0 +1,6 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.TripReportStudyLog +import org.springframework.data.jpa.repository.JpaRepository + +interface TripReportStudyLogJpaRepository : JpaRepository diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.kt new file mode 100644 index 0000000..2c02721 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripReportStudyLogRepositoryAdapter.kt @@ -0,0 +1,14 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.TripReportStudyLog +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository +import org.springframework.stereotype.Repository + +@Repository +class TripReportStudyLogRepositoryAdapter( + private val tripReportStudyLogJpaRepository: TripReportStudyLogJpaRepository, +) : TripReportStudyLogRepository { + override fun saveAll(tripReportStudyLogs: List) { + tripReportStudyLogJpaRepository.saveAll(tripReportStudyLogs) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripController.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripController.kt index 830fe2e..a26c09f 100644 --- a/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripController.kt +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripController.kt @@ -99,7 +99,7 @@ class TripController( .body(StandardResponse.success(HttpStatus.OK.value(), responses)) } - @Operation(summary = "특정 멤버의 여행 목록을 조회합니다. 슬라이스를 적용하고 D-DAY 정보가 이른 순으로 정렬합니다") + @Operation(summary = "여행 목록 조회", description = "특정 멤버의 여행 목록을 조회합니다. 슬라이스를 적용하고 D-DAY 정보가 이른 순으로 정렬합니다") @GetMapping fun loadTrips( @AuthenticationPrincipal memberId: String, diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportController.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportController.kt new file mode 100644 index 0000000..3c7638e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportController.kt @@ -0,0 +1,199 @@ +package com.ject.studytrip.trip.presentation.controller + +import com.ject.studytrip.global.common.response.StandardResponse +import com.ject.studytrip.trip.application.facade.TripReportFacade +import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest +import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest +import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest +import com.ject.studytrip.trip.presentation.dto.response.CreateTripReportResponse +import com.ject.studytrip.trip.presentation.dto.response.LoadTripReportDetailResponse +import com.ject.studytrip.trip.presentation.dto.response.LoadTripReportsResponse +import com.ject.studytrip.trip.presentation.dto.response.LoadTripRetrospectDetailResponse +import com.ject.studytrip.trip.presentation.dto.response.PresignedTripReportImageResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +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.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "TripReport", description = "여행 리포트 API") +@RestController +@RequestMapping +@Validated +class TripReportController( + private val tripReportFacade: TripReportFacade, +) { + @Operation(summary = "여행 리포트 생성", description = "멤버가 여행 회고에서 얻은 정보를 기반으로 새로운 여행 리포트를 생성합니다.") + @PostMapping("/api/trip-reports") + fun createTripReport( + @AuthenticationPrincipal memberId: String, + @RequestBody @Valid request: CreateTripReportRequest, + ): ResponseEntity { + val result = tripReportFacade.createTripReport(memberId.toLong(), request) + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(StandardResponse.success(HttpStatus.CREATED.value(), CreateTripReportResponse.of(result))) + } + + @Operation(summary = "여행 리포트 삭제", description = "특정 여행 리포트를 삭제합니다.") + @DeleteMapping("/api/trip-reports/{tripReportId}") + fun deleteTripReport( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") tripReportId: Long, + ): ResponseEntity { + tripReportFacade.deleteTripReport(memberId.toLong(), tripReportId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "여행 회고", description = "여행을 완료한 후, 멤버가 진행했던 여행을 회고합니다.") + @GetMapping("/api/trips/{tripId}/retrospect") + fun loadTripRetrospect( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @RequestParam(name = "page", defaultValue = "0") @Min(0) page: Int, + @RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) size: Int, + ): ResponseEntity { + val result = tripReportFacade.getTripRetrospect(memberId.toLong(), tripId, page, size) + + return ResponseEntity + .status(HttpStatus.OK) + .body( + StandardResponse.success( + HttpStatus.OK.value(), + LoadTripRetrospectDetailResponse.of(result.summary, result.tripInfo, result.studyLogSliceInfo), + ), + ) + } + + @Operation(summary = "여행 리포트 목록 조회", description = "특정 멤버의 여행 리포트 목록을 조회합니다.") + @GetMapping("/api/trip-reports") + fun loadTripReports( + @AuthenticationPrincipal memberId: String, + ): ResponseEntity { + val result = tripReportFacade.getTripReportsByMember(memberId.toLong()) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), LoadTripReportsResponse.of(result.tripReportInfos))) + } + + @Operation(summary = "여행 리포트 상세 조회", description = "특정 여행 리포트를 상세 조회합니다.") + @GetMapping("/api/trip-reports/{tripReportId}") + fun loadTripReport( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") tripReportId: Long, + @RequestParam(name = "page", defaultValue = "0") @Min(0) page: Int, + @RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) size: Int, + ): ResponseEntity { + val result = tripReportFacade.getTripReport(memberId.toLong(), tripReportId, page, size) + + return ResponseEntity + .status(HttpStatus.OK) + .body( + StandardResponse.success( + HttpStatus.OK.value(), + LoadTripReportDetailResponse.of(result.tripReportInfo, result.studyLogSliceInfo), + ), + ) + } + + @Operation( + summary = "여행 리포트 이미지 업로드용 Presigned URL 발급", + description = """ + 여행 리포트 이미지를 S3에 업로드하기 위한 Presigned URL을 발급합니다. + + [흐름] + 1) 먼저 여행 리포트 생성 API를 호출해 TripReportId를 응답받습니다. + 2) 사용자가 이미지를 첨부했을 경우, 생성 시 받은 TripReportId를 PathVariable로 전달하여 + 업로드용 파일명 정보를 함께 Presigned URL 발급 API를 요청합니다. + 서버는 업로드에 사용할 Presigned PUT URL과 임시키(tmpKey)를 반환합니다. + 3) 반환받은 Presigned URL로 PUT 요청을 통해 이미지를 S3에 업로드합니다. + 4) 업로드가 정상적으로 완료되면 바로 학습 로그 이미지 Confirm API를 호출합니다. + 이때 Presigned URL 발급 API에서 반환받은 임시키(tmpKey)를 함께 요청합니다. + 서버는 업로드된 이미지를 검증(크키/MIME)하고 확정합니다. + + [주의] + - 여행 리포트 이미지 Presigned URL 발급 요청 API는 TripReportId가 필요하기 때문에 필수로 본 API를 호출하기 전 여행 리포트를 먼저 생성해야 합니다. + - 요청 값의 originFilename은 꼭 파일 확장자를 포함한 파일명으로 요청해야합니다. + - Presigned URL 유효시간은 짧습니다(예: 10분). 만료되면 재발급해야 합니다. + """, + ) + @PostMapping("/api/trip-reports/{tripReportId}/images/presigned") + fun presigned( + @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") tripReportId: Long, + @RequestBody @Valid request: PresignTripReportImageRequest, + ): ResponseEntity { + val result = tripReportFacade.issuePresignedUrl(tripReportId, request) + + return ResponseEntity + .status(HttpStatus.OK) + .body( + StandardResponse.success( + HttpStatus.OK.value(), + PresignedTripReportImageResponse.of(result.tripReportId, result.tmpKey, result.presignedUrl), + ), + ) + } + + @Operation( + summary = "업로드된 여행 리포트 이미지 검증/확정", + description = """ + Presigned URL을 통해 S3에 업로드된 여행 리포트 이미지를 서버에서 검증하고 확정(Confirm)합니다. + + [흐름] + 1) 클라이언트는 발급받은 URL로 이미지를 업로드합니다. + 2) 업로드 완료 후, Presigned URL 발급 API에서 응답받은 임시키(tmpKey)를 포함해 해당 API를 호출합니다. + 임시키(tmpKey)는 Presigned URL의 전체 경로 중, 버킷 호스트명을 제외한 S3 객체 경로(ObjectKey) 입니다. + (예: https://bucket.s3.ap-northeast-2.amazonaws.com/tmp/report-logs/1/abc.jpg -> tmp/report-logs/1/abc.jpg) + + 3) 서버는 이미지 존재 여부, 크기, MIME 타입 등을 검증한 뒤 최종 경로로 이동시키고 여행 리포트 이미지 정보를 갱신합니다. + 만약 S3 Storage 기술 자체 에러가 발생하면 임시 경로에 저장된 이미지를 즉시 삭제하지 않아 컨펌 재시도가 가능하지만, + 유효하지 않은 이미지 크기/확장자 등 도메인 정책을 위반해 실패할 경우 임시 경로에 저장된 이미지가 즉시 삭제되며 다시 업로드부터 수행해야합니다. + + S3 Storage 기술 자체 예외 예시 + { + "status": 502 (BAD_GATEWAY), + "message": "Storage 서버 에러가 발생했습니다." + } + + 이미지 도메인 정책 위반 예외 예시 + { + "status": 400 (BAD_REQUEST), + "message": "유효하지 않은 이미지 확장자 입니다." , "유효하지 않은 이미지 MIME 입니다." 등 + } + + [주의] + - 이미지 타입(MIME/Content-Type)은 JPG, JPEG, PNG, WEBP만 허용합니다. 그 외 타입은 도메인 정책 위반으로 예외가 발생합니다. + - 이미지 최대 크기는 5MB로 설정되어있으며, 크기가 0 이하이거나 최대 크기를 벗어날 경우 도메인 정책 위반으로 예외가 발생합니다. + - 업로드는 되었지만 그 이후 문제가 발생하더라고 tmp/ 경로의 객체는 라이프사이클 정책에 따라 자동 정리되기 때문에 따로 삭제 요청 API는 호출하지 않아도 됩니다. + """, + ) + @PostMapping("/api/trip-reports/{tripReportId}/images/confirm") + fun confirm( + @PathVariable @NotNull(message = "여행 리포트 ID는 필수 요청 파라미터입니다.") tripReportId: Long, + @RequestBody @Valid request: ConfirmTripReportImageRequest, + ): ResponseEntity { + tripReportFacade.confirmImage(tripReportId, request) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.kt new file mode 100644 index 0000000..26ba9a3 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/ConfirmTripReportImageRequest.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.trip.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotEmpty + +data class ConfirmTripReportImageRequest( + @field:Schema(description = "업로드된 이미지 임시키") + @field:NotEmpty(message = "업로드된 이미지 임시키는 필수 요청 값입니다.") + val tmpKey: String, +) diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.kt new file mode 100644 index 0000000..3b829ca --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateTripReportRequest.kt @@ -0,0 +1,33 @@ +package com.ject.studytrip.trip.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull + +data class CreateTripReportRequest( + @field:Schema(description = "여행 리포트 제목") + @field:NotEmpty(message = "여행 리포트 제목은 필수 요청 값입니다.") + val title: String, + @field:Schema(description = "여행 리포트 내용") + @field:NotEmpty(message = "여행 리포트 내용은 필수 요청 값입니다.") + val content: String, + @field:Schema(description = "여행 시작일") + @field:NotEmpty(message = "여행 시작일은 필수 요청 값입니다.") + val startDate: String, + @field:Schema(description = "여행 종료일") + val endDate: String?, + @field:Schema(description = "학습 로그 개수 (세션 성공)") + val studyLogCount: Long, + @field:Schema(description = "총 학습 시간") + val totalFocusHours: Long, + @field:Schema(description = "연속 학습일") + val studyDays: Long, + @field:Schema(description = "이미지 제목") + val imageTitle: String?, + @field:Schema(description = "학습 로그 ID 목록") + @field:NotEmpty(message = "학습 로그 ID 목록은 최소 1개 이상이어야 합니다.") + val studyLogIds: List< + @NotNull(message = "학습 로그 ID는 필수 요청 값입니다.") + Long, + >, +) diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.kt new file mode 100644 index 0000000..f511110 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/PresignTripReportImageRequest.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.trip.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotEmpty + +data class PresignTripReportImageRequest( + @field:Schema(description = "원본 이미지 파일명") + @field:NotEmpty(message = "원본 이미지 파일명은 필수 요청 값입니다.") + val originFilename: String, +) diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.kt new file mode 100644 index 0000000..81d2978 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateTripReportResponse.kt @@ -0,0 +1,14 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.trip.application.dto.TripReportInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateTripReportResponse( + @field:Schema(description = "여행 리포트 ID") + val tripReportId: Long, +) { + companion object { + @JvmStatic + fun of(tripReportInfo: TripReportInfo): CreateTripReportResponse = CreateTripReportResponse(tripReportInfo.tripReportId) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.kt new file mode 100644 index 0000000..1aefaac --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportDetailResponse.kt @@ -0,0 +1,52 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo +import com.ject.studytrip.studylog.presentation.dto.response.LoadStudyLogsSliceResponse +import com.ject.studytrip.trip.application.dto.TripReportInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadTripReportDetailResponse( + @field:Schema(description = "여행 리포트 ID") + val tripReportId: Long, + @field:Schema(description = "여행 리포트 제목") + val title: String, + @field:Schema(description = "여행 리포트 내용") + val content: String, + @field:Schema(description = "여행 시작일 (여행 회고)") + val startDate: String, + @field:Schema(description = "여행 종료일 (여행 회고)") + val endDate: String, + @field:Schema(description = "총 학습 시간") + val totalFocusHours: Long, + @field:Schema(description = "학습 로그 개수 (세션 성공)") + val studyLogCount: Long, + @field:Schema(description = "연속 학습일") + val studyDays: Long, + @field:Schema(description = "여행 리포트 이미지 제목") + val imageTitle: String?, + @field:Schema(description = "여행 리포트 이미지 URL") + val imageUrl: String?, + @field:Schema(description = "학습 로그 히스토리") + val history: LoadStudyLogsSliceResponse, +) { + companion object { + @JvmStatic + fun of( + tripReportInfo: TripReportInfo, + studyLogSliceInfo: StudyLogSliceInfo, + ): LoadTripReportDetailResponse = + LoadTripReportDetailResponse( + tripReportInfo.tripReportId, + tripReportInfo.title, + tripReportInfo.content, + tripReportInfo.startDate, + tripReportInfo.endDate, + tripReportInfo.totalFocusHours, + tripReportInfo.studyLogCount, + tripReportInfo.studyDays, + tripReportInfo.imageTitle, + tripReportInfo.imageUrl, + LoadStudyLogsSliceResponse.of(studyLogSliceInfo.studyLogDetails, studyLogSliceInfo.hasNext), + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.kt new file mode 100644 index 0000000..a861d9c --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripReportsResponse.kt @@ -0,0 +1,63 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.trip.application.dto.TripReportInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadTripReportsResponse( + val summary: TripReportSummary, + val tripReports: List, +) { + companion object { + @JvmStatic + fun of(tripReportInfos: List): LoadTripReportsResponse = + LoadTripReportsResponse( + TripReportSummary.of(tripReportInfos), + tripReportInfos.map { LoadTripReportInfoResponse.of(it) }, + ) + } +} + +data class TripReportSummary( + @field:Schema(description = "여행 완료 수") + val completedTripCount: Long, + @field:Schema(description = "누적 학습 시간") + val totalFocusHours: Long, + @field:Schema(description = "가장 긴 학습 시간") + val longestFocusHours: Long, +) { + companion object { + fun of(tripReportInfos: List): TripReportSummary = + TripReportSummary( + tripReportInfos.size.toLong(), + tripReportInfos.sumOf { it.totalFocusHours }, + tripReportInfos.maxOfOrNull { it.totalFocusHours } ?: 0L, + ) + } +} + +data class LoadTripReportInfoResponse( + @field:Schema(description = "여행 리포트 ID") + val tripReportId: Long, + @field:Schema(description = "여행 리포트 제목") + val title: String, + @field:Schema(description = "여행 시작일 (여행 회고)") + val startDate: String, + @field:Schema(description = "여행 종료일 (여행 회고)") + val endDate: String?, + @field:Schema(description = "총 학습 시간") + val totalFocusHours: Long, + @field:Schema(description = "여행 리포트 이미지 URL") + val imageUrl: String?, +) { + companion object { + fun of(tripReportInfo: TripReportInfo): LoadTripReportInfoResponse = + LoadTripReportInfoResponse( + tripReportInfo.tripReportId, + tripReportInfo.title, + tripReportInfo.startDate, + tripReportInfo.endDate, + tripReportInfo.totalFocusHours, + tripReportInfo.imageUrl, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.kt new file mode 100644 index 0000000..fde013d --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.kt @@ -0,0 +1,45 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo +import com.ject.studytrip.studylog.presentation.dto.response.LoadStudyLogsSliceResponse +import com.ject.studytrip.trip.application.dto.TripInfo +import com.ject.studytrip.trip.application.dto.TripRetrospectSummary +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadTripRetrospectDetailResponse( + @field:Schema(description = "여행 이름") + val name: String, + @field:Schema(description = "여행 시작일") + val startDate: String, + @field:Schema(description = "여행 종료일") + val endDate: String?, + @field:Schema(description = "총 학습 시간") + val totalFocusHours: Long, + @field:Schema(description = "학습 로그 개수 (세션 성공)") + val studyLogCount: Long, + @field:Schema(description = "연속 학습일") + val studyDays: Long, + @field:Schema(description = "학습 로그 ID 목록") + val studyLogIds: List, + @field:Schema(description = "학습 로그 히스토리") + val history: LoadStudyLogsSliceResponse, +) { + companion object { + @JvmStatic + fun of( + tripRetrospectSummary: TripRetrospectSummary, + tripInfo: TripInfo, + studyLogSliceInfo: StudyLogSliceInfo, + ): LoadTripRetrospectDetailResponse = + LoadTripRetrospectDetailResponse( + tripInfo.tripName, + tripInfo.startDate, + tripInfo.endDate, + tripRetrospectSummary.totalFocusHours, + tripRetrospectSummary.studyLogCount, + tripRetrospectSummary.studyDays, + tripRetrospectSummary.studyLogIds, + LoadStudyLogsSliceResponse.of(studyLogSliceInfo.studyLogDetails, studyLogSliceInfo.hasNext), + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.kt new file mode 100644 index 0000000..5623400 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/PresignedTripReportImageResponse.kt @@ -0,0 +1,21 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import io.swagger.v3.oas.annotations.media.Schema + +data class PresignedTripReportImageResponse( + @field:Schema(description = "여행 리포트 ID") + val tripReportId: Long, + @field:Schema(description = "여행 리포트 이미지 임시키") + val tmpKey: String, + @field:Schema(description = "여행 리포트 이미지 업로드용 Presigned URL") + val presignedUrl: String, +) { + companion object { + @JvmStatic + fun of( + tripReportId: Long, + tmpKey: String, + presignedUrl: String, + ): PresignedTripReportImageResponse = PresignedTripReportImageResponse(tripReportId, tmpKey, presignedUrl) + } +} diff --git a/src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java b/src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java deleted file mode 100644 index 825ceef..0000000 --- a/src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ject.studytrip.dummy.application.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.stamp.fixture.StampFixture; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.fixture.TripFixture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; - -@DisplayName("DummyMissionCommandService 단위 테스트") -class DummyMissionCommandServiceTest extends BaseUnitTest { - private static final int COUNT = 10; - - @InjectMocks private DummyMissionCommandService dummyMissionCommandService; - - private Trip courseTrip; - - @BeforeEach - void setUp() { - Member member = MemberFixture.createMemberFromKakao(); - courseTrip = new TripFixture(member, TripCategory.COURSE).create(); - } - - @Nested - @DisplayName("createDummyMission 메서드는") - class CreateDummyMission { - - @Test - @DisplayName("특정 스탬프가 들어오면 더미 미션을 생성하고 리턴한다.") - void shouldReturnDummyMissionForStamp() { - // given - Stamp stamp = new StampFixture(courseTrip, COUNT).create(); - - // when - Mission result = dummyMissionCommandService.createDummyMission(stamp); - - // then - assertThat(result).isNotNull(); - assertThat(result.getName()).isNotNull(); - } - } -} diff --git a/src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java b/src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java deleted file mode 100644 index f0cfc64..0000000 --- a/src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.ject.studytrip.dummy.application.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.fixture.TripFixture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; - -@DisplayName("DummyStampCommandService 단위 테스트") -class DummyStampCommandServiceTest extends BaseUnitTest { - private static final int COUNT = 10; - - @InjectMocks private DummyStampCommandService dummyStampCommandService; - - private Member member; - - @BeforeEach - void setUp() { - member = MemberFixture.createMemberFromKakao(); - } - - @Nested - @DisplayName("createDummyStamp 메서드는") - class CreateDummyStamp { - - @Test - @DisplayName("코스형 여행이 들어오면 코스형 더미 스탬프를 생성하고 반환한다.") - void shouldReturnDummyCourseStampForCourseTrip() { - // given - Trip courseTrip = new TripFixture(member, TripCategory.COURSE).create(); - - // when - Stamp result = dummyStampCommandService.createDummyStamp(courseTrip, COUNT); - - // then - assertThat(result).isNotNull(); - assertThat(result.getName()).isNotNull(); - assertThat(result.getStampOrder()).isPositive(); // 양수 - assertThat(result.getEndDate()).isNotNull(); - } - - @Test - @DisplayName("탐험형 여행이 들어오면 탐험형 더미 스탬프를 생성하고 반환한다.") - void shouldReturnDummyExploreStampForExploreTrip() { - // given - Trip exploreTrip = new TripFixture(member, TripCategory.EXPLORE).create(); - - // when - Stamp result = dummyStampCommandService.createDummyStamp(exploreTrip, COUNT); - - // then - assertThat(result).isNotNull(); - assertThat(result.getName()).isNotNull(); - assertThat(result.getStampOrder()).isZero(); - assertThat(result.getEndDate()).isNull(); - } - } -} diff --git a/src/test/java/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.java b/src/test/java/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.java deleted file mode 100644 index 44ed6f0..0000000 --- a/src/test/java/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.ject.studytrip.dummy.application.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.trip.domain.model.Trip; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; - -@DisplayName("DummyTripCommandService 단위 테스트") -class DummyTripCommandServiceTest extends BaseUnitTest { - private static final int COUNT = 10; - - @InjectMocks private DummyTripCommandService dummyTripCommandService; - - private Member member; - - @BeforeEach - void setUp() { - member = MemberFixture.createMemberFromKakao(); - } - - @Nested - @DisplayName("createDummyTrip 메서드는") - class CreateDummyTrip { - - @Test - @DisplayName("COURSE 카테고리가 들어오면 코스형 더미 여행을 생성하고 반환한다.") - void shouldReturnDummyCourseTripWhenCategoryIsCourse() { - // given - String category = "COURSE"; - - // when - Trip result = dummyTripCommandService.createDummyTrip(member, category, COUNT); - - // then - assertThat(result).isNotNull(); - assertThat(result.getName()).isNotNull(); - assertThat(result.getMemo()).isNotNull(); - assertThat(result.getCategory()).isNotNull(); - assertThat(result.getStartDate()).isNotNull(); - assertThat(result.getEndDate()).isNotNull(); - } - - @Test - @DisplayName("EXPLORE 카테고리가 들어오면 탐험형 더미 여행을 생성하고 반환한다.") - void shouldReturnDummyExploreTripWhenCategoryIsExplore() { - // given - String category = "EXPLORE"; - - // when - Trip result = dummyTripCommandService.createDummyTrip(member, category, COUNT); - - // then - assertThat(result).isNotNull(); - assertThat(result.getName()).isNotNull(); - assertThat(result.getMemo()).isNotNull(); - assertThat(result.getCategory()).isNotNull(); - assertThat(result.getStartDate()).isNotNull(); - assertThat(result.getEndDate()).isNull(); - } - } -} diff --git a/src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.java b/src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.java deleted file mode 100644 index 22da293..0000000 --- a/src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.ject.studytrip.dummy.presentation.controller; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.ject.studytrip.BaseIntegrationTest; -import com.ject.studytrip.auth.domain.error.AuthErrorCode; -import com.ject.studytrip.auth.fixture.TokenFixture; -import com.ject.studytrip.auth.helper.TokenTestHelper; -import com.ject.studytrip.global.exception.error.CommonErrorCode; -import com.ject.studytrip.member.domain.error.MemberErrorCode; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.helper.MemberTestHelper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.ResultActions; - -@DisplayName("DummyMissionController 통합 테스트") -class DummyMissionControllerIntegrationTest extends BaseIntegrationTest { - private static final String BASE_DUMMY_MISSION_URL = "/api/dummies/missions"; - private static final String COURSE_CATEGORY = "COURSE"; - private static final String EXPLORE_CATEGORY = "EXPLORE"; - private static final int COUNT = 10; - - @Autowired private MemberTestHelper memberTestHelper; - @Autowired private TokenTestHelper tokenTestHelper; - - private Member member; - private String accessToken; - - @BeforeEach - void setUp() { - member = memberTestHelper.saveMember(); - accessToken = - tokenTestHelper.createAccessToken( - member.getId().toString(), member.getRole().name()); - } - - @Nested - @DisplayName("더미 미션 목록 조회 API") - class LoadDummyMissions { - private ResultActions getResultActions(String accessToken, String category, int count) - throws Exception { - return mockMvc.perform( - get(BASE_DUMMY_MISSION_URL) - .header( - HttpHeaders.AUTHORIZATION, - TokenFixture.TOKEN_PREFIX + accessToken) - .param("category", category) - .param("count", String.valueOf(count)) - .contentType(MediaType.APPLICATION_JSON)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // when - ResultActions resultActions = getResultActions("", COURSE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(AuthErrorCode.UNAUTHENTICATED.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 카테고리가 들어오면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenCategoryIsInvalid() throws Exception { - // given - String invalidCategory = "INVALID"; - - // when - ResultActions resultActions = getResultActions(accessToken, invalidCategory, COUNT); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("유효하지 않은 더미 데이터 개수가 들어오면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenCountIsInvalid() throws Exception { - // given - int invalidCount = 0; - - // when - ResultActions resultActions = - getResultActions(accessToken, COURSE_CATEGORY, invalidCount); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 사용자일 경우 404 Not Found를 반환한다.") - void shouldReturnBadRequestWhenMemberAlreadyDeleted() throws Exception { - // given - member.updateDeletedAt(); - - // when - ResultActions resultActions = getResultActions(accessToken, COURSE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(MemberErrorCode.MEMBER_NOT_FOUND.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(MemberErrorCode.MEMBER_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("COURSE 카테고리가 들어오면 더미 미션 목록를 반환한다.(DB 저장 X)") - void shouldReturnDummyMissionsWhenCategoryIsCourse() throws Exception { - // when - ResultActions resultActions = getResultActions(accessToken, COURSE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) - .andExpect(jsonPath("$.data").isArray()); - } - - @Test - @DisplayName("EXPLORE 카테고리가 들어오면 더미 미션 목록를 반환한다.(DB 저장 X)") - void shouldReturnDummyMissionsWhenCategoryIsExplore() throws Exception { - // when - ResultActions resultActions = getResultActions(accessToken, EXPLORE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) - .andExpect(jsonPath("$.data").isArray()); - } - } -} diff --git a/src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.java b/src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.java deleted file mode 100644 index 2268e48..0000000 --- a/src/test/java/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.ject.studytrip.dummy.presentation.controller; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.ject.studytrip.BaseIntegrationTest; -import com.ject.studytrip.auth.domain.error.AuthErrorCode; -import com.ject.studytrip.auth.fixture.TokenFixture; -import com.ject.studytrip.auth.helper.TokenTestHelper; -import com.ject.studytrip.global.exception.error.CommonErrorCode; -import com.ject.studytrip.member.domain.error.MemberErrorCode; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.helper.MemberTestHelper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.ResultActions; - -@DisplayName("DummyStampController 통합 테스트") -class DummyStampControllerIntegrationTest extends BaseIntegrationTest { - private static final String BASE_DUMMY_STAMP_URL = "/api/dummies/stamps"; - private static final String COURSE_CATEGORY = "COURSE"; - private static final String EXPLORE_CATEGORY = "EXPLORE"; - private static final int COUNT = 10; - - @Autowired private MemberTestHelper memberTestHelper; - @Autowired private TokenTestHelper tokenTestHelper; - - private Member member; - private String accessToken; - - @BeforeEach - void setUp() { - member = memberTestHelper.saveMember(); - accessToken = - tokenTestHelper.createAccessToken( - member.getId().toString(), member.getRole().name()); - } - - @Nested - @DisplayName("더미 스탬프 목록 조회 API") - class LoadDummyMissions { - private ResultActions getResultActions(String accessToken, String category, int count) - throws Exception { - return mockMvc.perform( - get(BASE_DUMMY_STAMP_URL) - .header( - HttpHeaders.AUTHORIZATION, - TokenFixture.TOKEN_PREFIX + accessToken) - .param("category", category) - .param("count", String.valueOf(count)) - .contentType(MediaType.APPLICATION_JSON)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // when - ResultActions resultActions = getResultActions("", COURSE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(AuthErrorCode.UNAUTHENTICATED.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 카테고리가 들어오면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenCategoryIsInvalid() throws Exception { - // given - String invalidCategory = "INVALID"; - - // when - ResultActions resultActions = getResultActions(accessToken, invalidCategory, COUNT); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("유효하지 않은 더미 데이터 개수가 들어오면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenCountIsInvalid() throws Exception { - // given - int invalidCount = 0; - - // when - ResultActions resultActions = - getResultActions(accessToken, COURSE_CATEGORY, invalidCount); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 사용자일 경우 404 Not Found를 반환한다.") - void shouldReturnBadRequestWhenMemberAlreadyDeleted() throws Exception { - // given - member.updateDeletedAt(); - - // when - ResultActions resultActions = getResultActions(accessToken, COURSE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(MemberErrorCode.MEMBER_NOT_FOUND.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(MemberErrorCode.MEMBER_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("COURSE 카테고리가 들어오면 코스형 더미 스탬프 목록를 반환한다.(DB 저장 X)") - void shouldReturnDummyCourseStampsWhenCategoryIsCourse() throws Exception { - // when - ResultActions resultActions = getResultActions(accessToken, COURSE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) - .andExpect(jsonPath("$.data").isArray()) - .andExpect(jsonPath("$.data[0].stampOrder").value(1)); - } - - @Test - @DisplayName("EXPLORE 카테고리가 들어오면 탐험형 더미 스탬프 목록를 반환한다.(DB 저장 X)") - void shouldReturnDummyExploreStampsWhenCategoryIsExplore() throws Exception { - // when - ResultActions resultActions = getResultActions(accessToken, EXPLORE_CATEGORY, COUNT); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) - .andExpect(jsonPath("$.data").isArray()) - .andExpect(jsonPath("$.data[0].stampOrder").value(0)); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java deleted file mode 100644 index c2c2708..0000000 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.repository.TripReportCommandRepository; -import com.ject.studytrip.trip.domain.repository.TripReportRepository; -import com.ject.studytrip.trip.fixture.CreateTripReportRequestFixture; -import com.ject.studytrip.trip.fixture.TripReportFixture; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -@DisplayName("TripReportCommandService 단위 테스트") -class TripReportCommandServiceTest extends BaseUnitTest { - @InjectMocks private TripReportCommandService tripReportCommandService; - @Mock private TripReportRepository tripReportRepository; - @Mock private TripReportCommandRepository tripReportCommandRepository; - - private Member member; - private TripReport tripReport; - - @BeforeEach - void setup() { - member = MemberFixture.createMemberFromKakaoWithId(1L); - tripReport = TripReportFixture.createTripReport(member); - } - - @Nested - @DisplayName("createTripReport 메서드는") - class CreateTripReport { - - @Test - @DisplayName("유효한 요청이 들어오면 여행 리포트를 생성하고 반환한다.") - void shouldReturnTripReportWhenRequestIsValid() { - // given - CreateTripReportRequest request = new CreateTripReportRequestFixture().build(); - given(tripReportRepository.save(any(TripReport.class))).willReturn(tripReport); - - // when - TripReport result = tripReportCommandService.createTripReport(member, request); - - // then - assertThat(result).isEqualTo(tripReport); - } - } - - @Nested - @DisplayName("updateImageUrl 메서드는") - class UpdateImageUrl { - private static final String NEW_IMAGE_URL = - "https://cdn.example.com/trip-reports/1/image.jpg"; - - @Test - @DisplayName("유효한 여행 리포트의 이미지 URL을 수정한다.") - void shouldUpdateImageUrlWhenTripReportIsValid() { - // given - String oldImageUrl = tripReport.getImageUrl(); - - // when - tripReportCommandService.updateImageUrl(tripReport, NEW_IMAGE_URL); - - // then - assertThat(tripReport.getImageUrl()).isEqualTo(NEW_IMAGE_URL); - assertThat(tripReport.getImageUrl()).isNotEqualTo(oldImageUrl); - } - } - - @Nested - @DisplayName("deleteTripReport 메서드는") - class DeleteTripReport { - - @Test - @DisplayName("특정 여행 리포트의 deletedAt 필드를 현재 시간으로 업데이트한다") - void shouldDeleteTripForUpdateDeletedAt() { - // when - tripReportCommandService.deleteTripReport(tripReport); - - // then - assertThat(tripReport.getDeletedAt()).isNotNull(); - } - } - - @Nested - @DisplayName("hardDeleteTripReports 메서드는") - class HardDeleteTripReports { - - @Test - @DisplayName("삭제된 여행 리포트가 하나라도 없으면 0을 반환한다.") - void shouldReturnZeroWhenDeletedTripReportDoesNotExist() { - // given - given(tripReportCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); - - // when - long result = tripReportCommandService.hardDeleteTripReports(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 여행 리포트가 하나라도 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenDeletedTripReportExist() { - // given - given(tripReportCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); - - // when - long result = tripReportCommandService.hardDeleteTripReports(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteTripReportsOwnedByDeletedMember 메서드는") - class HardDeleteTripReportsOwnedByDeletedMember { - - @Test - @DisplayName("삭제된 멤버가 소유한 여행 리포트가 없으면 0을 반환한다.") - void shouldReturnZeroWhenTripReportsOwnedByDeletedMemberDoNotExist() { - // given - given(tripReportCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(0L); - - // when - long result = tripReportCommandService.hardDeleteTripReportsOwnedByDeletedMember(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 멤버가 소유한 여행 리포트가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenTripReportsOwnedByDeletedMemberExist() { - // given - given(tripReportCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(5L); - - // when - long result = tripReportCommandService.hardDeleteTripReportsOwnedByDeletedMember(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteTripReportsByMember 메서드는") - class HardDeleteTripReportsByMember { - - @Test - @DisplayName("특정 멤버가 소유한 여행 리포트가 없으면 0을 반환한다.") - void shouldReturnZeroWhenTripReportsOwnedByMemberDoNotExist() { - // given - Long memberId = 1L; - given(tripReportCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L); - - // when - long result = tripReportCommandService.hardDeleteTripReportsByMember(memberId); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("특정 멤버가 소유한 여행 리포트가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenTripReportsOwnedByMemberExist() { - // given - Long memberId = 1L; - given(tripReportCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L); - - // when - long result = tripReportCommandService.hardDeleteTripReportsByMember(memberId); - - // then - assertThat(result).isEqualTo(5L); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.java deleted file mode 100644 index da45f8f..0000000 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.java +++ /dev/null @@ -1,235 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.trip.domain.error.TripReportErrorCode; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository; -import com.ject.studytrip.trip.domain.repository.TripReportRepository; -import com.ject.studytrip.trip.fixture.TripReportFixture; -import java.util.List; -import java.util.Optional; -import org.assertj.core.api.AssertionsForClassTypes; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -@DisplayName("TripReportQueryService 단위 테스트") -class TripReportQueryServiceTest extends BaseUnitTest { - @InjectMocks private TripReportQueryService tripReportQueryService; - @Mock private TripReportRepository tripReportRepository; - @Mock private TripReportQueryRepository tripReportQueryRepository; - - private Member member; - private TripReport tripReport1; - private TripReport tripReport2; - - @BeforeEach - void setUp() { - member = MemberFixture.createMemberFromKakaoWithId(1L); - tripReport1 = TripReportFixture.createTripReportWithId(1L, member); - tripReport2 = TripReportFixture.createTripReportWithId(2L, member); - } - - @Nested - @DisplayName("getTripReport 메서드는") - class GetTripReport { - - @Test - @DisplayName("존재하지 않는 여행 리포트로 조회하면 예외가 발생한다.") - void shouldThrowExceptionWhenTripReportDoNotExist() { - // given - Long invalidId = -1L; - given(tripReportRepository.findById(invalidId)).willReturn(Optional.empty()); - - // when & then - assertThatThrownBy( - () -> - tripReportQueryService.getValidTripReport( - member.getId(), invalidId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("여행 리포트가 이미 삭제되었다면 예외가 발생한다.") - void shouldThrowExceptionWhenTripReportAlreadyDeleted() { - // given - Long tripReportId = tripReport1.getId(); - tripReport1.updateDeletedAt(); - given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)); - - // when & then - assertThatThrownBy( - () -> - tripReportQueryService.getValidTripReport( - member.getId(), tripReportId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED.getMessage()); - } - - @Test - @DisplayName("여행 리포트가 존재하면 여행 리포트를 반환한다.") - void shouldReturnValidTripReportWhenTripReportExist() { - // given - Long tripReportId = tripReport1.getId(); - given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)); - - // when - TripReport result = - tripReportQueryService.getValidTripReport(member.getId(), tripReportId); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(tripReportId); - assertThat(result.getMember().getId()).isEqualTo(member.getId()); - } - } - - @Nested - @DisplayName("getValidTripReport 메서드는") - class GetValidTripReport { - - @Test - @DisplayName("존재하지 않는 여행 리포트로 조회하면 예외가 발생한다.") - void shouldThrowExceptionWhenTripReportDoNotExist() { - // given - Long invalidId = -1L; - given(tripReportRepository.findById(invalidId)).willReturn(Optional.empty()); - - // when & then - assertThatThrownBy( - () -> - tripReportQueryService.getValidTripReport( - member.getId(), invalidId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("여행 리포트의 소유자가 아니라면 예외가 발생한다.") - void shouldThrowExceptionWhenNotTripReportOwner() { - // given - Member newMember = MemberFixture.createMemberFromKakaoWithId(2L); - Long tripReportId = tripReport1.getId(); - given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)); - - // When & Then - AssertionsForClassTypes.assertThatThrownBy( - () -> - tripReportQueryService.getValidTripReport( - newMember.getId(), tripReportId)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.getMessage()); - } - - @Test - @DisplayName("여행 리포트가 존재하면 여행 리포트를 반환한다.") - void shouldReturnValidTripReportWhenTripReportExist() { - // given - Long tripReportId = tripReport1.getId(); - given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)); - - // when - TripReport result = - tripReportQueryService.getValidTripReport(member.getId(), tripReportId); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(tripReportId); - assertThat(result.getMember().getId()).isEqualTo(member.getId()); - } - } - - @Nested - @DisplayName("getTripReportsByMemberId 메서드는") - class GetTripReportsByMemberId { - - @Test - @DisplayName("여행 리포트가 존재하지 않으면 빈 리스트를 반환한다.") - void shouldReturnEmptyListWhenTripReportDoNotExist() { - // given - Long memberId = member.getId(); - given( - tripReportRepository - .findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc( - memberId)) - .willReturn(List.of()); - - // when - List result = tripReportQueryService.getTripReportsByMemberId(memberId); - - // then - assertThat(result.size()).isEqualTo(0); - } - - @Test - @DisplayName("여행 리포트가 하나라도 존재하면 특정 멤버가 생성한 여행 리포트 리스트를 반환한다.") - void shouldReturnTripReportsWhenTripReportExists() { - // given - Long memberId = member.getId(); - given( - tripReportRepository - .findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc( - memberId)) - .willReturn(List.of(tripReport1, tripReport2)); - - // when - List result = tripReportQueryService.getTripReportsByMemberId(memberId); - - // then - assertThat(result.size()).isEqualTo(2); - assertThat(result.get(0).getId()).isEqualTo(tripReport1.getId()); - assertThat(result.get(1).getId()).isEqualTo(tripReport2.getId()); - } - } - - @Nested - @DisplayName("getTripReportImageUrlsByMemberId 메서드는") - class GetTripReportImageUrlsByMemberId { - - @Test - @DisplayName("이미지가 없으면 빈 리스트를 반환한다") - void shouldReturnEmptyListWhenNoImages() { - // given - Long memberId = member.getId(); - given(tripReportQueryRepository.findImageUrlsByMemberId(memberId)) - .willReturn(List.of()); - - // when - List result = tripReportQueryService.getTripReportImageUrlsByMemberId(memberId); - - // then - assertThat(result).isEmpty(); - } - - @Test - @DisplayName("이미지가 존재하면 URL 리스트를 반환한다") - void shouldReturnImageUrlsWhenExist() { - // given - Long memberId = member.getId(); - List imageUrls = - List.of( - "https://cdn.example.com/reports/1.jpg", - "https://cdn.example.com/reports/2.jpg"); - given(tripReportQueryRepository.findImageUrlsByMemberId(memberId)) - .willReturn(imageUrls); - - // when - List result = tripReportQueryService.getTripReportImageUrlsByMemberId(memberId); - - // then - assertThat(result).hasSize(2); - assertThat(result).isEqualTo(imageUrls); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java deleted file mode 100644 index 06059d0..0000000 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.studylog.domain.model.StudyLog; -import com.ject.studytrip.studylog.fixture.StudyLogFixture; -import com.ject.studytrip.trip.domain.model.*; -import com.ject.studytrip.trip.domain.repository.TripReportStudyLogCommandRepository; -import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository; -import com.ject.studytrip.trip.fixture.DailyGoalFixture; -import com.ject.studytrip.trip.fixture.TripFixture; -import com.ject.studytrip.trip.fixture.TripReportFixture; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -@DisplayName("TripReportStudyLogCommandService 단위 테스트") -class TripReportStudyLogCommandServiceTest extends BaseUnitTest { - @InjectMocks private TripReportStudyLogCommandService tripReportStudyLogCommandService; - @Mock private TripReportStudyLogRepository tripReportStudyLogRepository; - @Mock private TripReportStudyLogCommandRepository tripReportStudyLogCommandRepository; - - private TripReport tripReport; - private List studyLogs; - - @BeforeEach - void setUp() { - Member member = MemberFixture.createMemberFromKakaoWithId(1L); - Trip trip = new TripFixture(member, TripCategory.COURSE).create(); - DailyGoal dailyGoal = new DailyGoalFixture(trip).create(); - StudyLog studyLog1 = new StudyLogFixture(member, dailyGoal).createWithId(1L); - StudyLog studyLog2 = new StudyLogFixture(member, dailyGoal).createWithId(2L); - tripReport = TripReportFixture.createTripReportWithId(1L, member); - studyLogs = List.of(studyLog1, studyLog2); - } - - @Nested - @DisplayName("createTripReportStudyLogs 메서드는") - class CreateTripReportStudyLogs { - - @Test - @DisplayName("여행 리포트와 학습 로그 목록으로 여행 리포트 학습 로그를 생성한다.") - void shouldCreateTripReportStudyLogs() { - // given - willDoNothing().given(tripReportStudyLogRepository).saveAll(anyList()); - - // when - tripReportStudyLogCommandService.createTripReportStudyLogs(tripReport, studyLogs); - - // then - verify(tripReportStudyLogRepository, times(1)).saveAll(anyList()); - } - } - - @Nested - @DisplayName("hardDeleteTripReportStudyLogsOwnedByDeletedMember 메서드는") - class HardDeleteTripReportStudyLogsOwnedByDeletedMember { - - @Test - @DisplayName("삭제된 멤버가 소유한 여행 리포트가 없으면 0을 반환한다.") - void shouldReturnZeroWhenTripReportsOwnedByDeletedMemberDoNotExist() { - // given - given(tripReportStudyLogCommandRepository.deleteAllByDeletedMemberOwner()) - .willReturn(0L); - - // when - long result = - tripReportStudyLogCommandService - .hardDeleteTripReportStudyLogsOwnedByDeletedMember(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 멤버가 소유한 학습 로그가 없으면 0을 반환한다.") - void shouldReturnZeroWhenStudyLogsOwnedByDeletedMemberDoNotExist() { - // given - given(tripReportStudyLogCommandRepository.deleteAllByDeletedMemberOwner()) - .willReturn(0L); - - // when - long result = - tripReportStudyLogCommandService - .hardDeleteTripReportStudyLogsOwnedByDeletedMember(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 멤버가 소유한 여행 리포트 또는 학습 로그가 있으면 삭제된 TripReportStudyLog 개수를 반환한다.") - void shouldReturnCountWhenTripReportsOrStudyLogsOwnedByDeletedMemberExist() { - // given - given(tripReportStudyLogCommandRepository.deleteAllByDeletedMemberOwner()) - .willReturn(5L); - - // when - long result = - tripReportStudyLogCommandService - .hardDeleteTripReportStudyLogsOwnedByDeletedMember(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteTripReportStudyLogsByMember 메서드는") - class HardDeleteTripReportStudyLogsByMember { - - @Test - @DisplayName("특정 멤버가 소유한 여행 리포트 학습 로그가 없으면 0을 반환한다.") - void shouldReturnZeroWhenTripReportStudyLogsOwnedByMemberDoNotExist() { - // given - Long memberId = 1L; - given(tripReportStudyLogCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L); - - // when - long result = - tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsByMember( - memberId); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("특정 멤버가 소유한 여행 리포트 학습 로그가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenTripReportStudyLogsOwnedByMemberExist() { - // given - Long memberId = 1L; - given(tripReportStudyLogCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L); - - // when - long result = - tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsByMember( - memberId); - - // then - assertThat(result).isEqualTo(5L); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.java deleted file mode 100644 index d6f2aa3..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest; - -public class ConfirmTripReportImageRequestFixture { - private String tmpKey = "tmp/trip-reports/1/test.jpg"; - - public ConfirmTripReportImageRequest build() { - return new ConfirmTripReportImageRequest(tmpKey); - } - - public ConfirmTripReportImageRequestFixture withTmpKey(String tmpKey) { - this.tmpKey = tmpKey; - return this; - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.java deleted file mode 100644 index 35d01d1..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest; -import java.util.List; - -public class CreateTripReportRequestFixture { - private static final String TRIP_REPORT_TITLE = "TEST TITLE"; - private static final String TRIP_REPORT_CONTENT = "TEST CONTENT"; - private static final String TRIP_START_DATE = "2018.01.01"; - private static final String TRIP_END_DATE = "2018.01.31"; - private static final long STUDY_LOG_COUNT = 10L; - private static final long TRIP_REPORT_TOTAL_FOCUS_HOURS = 100L; - private static final long TRIP_REPORT_STUDY_DAYS = 10L; - private static final String TRIP_REPORT_IMAGE_TITLE = "TEST IMAGE TITLE"; - - private List studyLogIds = List.of(1L, 2L); - - public CreateTripReportRequestFixture withStudyLogIds(List studyLogIds) { - this.studyLogIds = studyLogIds; - return this; - } - - public CreateTripReportRequest build() { - return new CreateTripReportRequest( - TRIP_REPORT_TITLE, - TRIP_REPORT_CONTENT, - TRIP_START_DATE, - TRIP_END_DATE, - STUDY_LOG_COUNT, - TRIP_REPORT_TOTAL_FOCUS_HOURS, - TRIP_REPORT_STUDY_DAYS, - TRIP_REPORT_IMAGE_TITLE, - studyLogIds); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.java deleted file mode 100644 index 295fb18..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest; - -public class PresignTripReportImageRequestFixture { - private String originFilename = "test.png"; - - public PresignTripReportImageRequest build() { - return new PresignTripReportImageRequest(originFilename); - } - - public PresignTripReportImageRequestFixture withOriginFilename(String originFilename) { - this.originFilename = originFilename; - return this; - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/TripReportFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/TripReportFixture.java deleted file mode 100644 index e436a12..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/TripReportFixture.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.factory.TripReportFactory; -import com.ject.studytrip.trip.domain.model.TripReport; -import org.springframework.test.util.ReflectionTestUtils; - -public class TripReportFixture { - private static final String TRIP_REPORT_TITLE = "TEST TITLE"; - private static final String TRIP_REPORT_CONTENT = "TEST CONTENT"; - private static final String TRIP_START_DATE = "2018.01.01"; - private static final String TRIP_END_DATE = "2018.01.31"; - private static final long STUDY_LOG_COUNT = 10L; - private static final long TRIP_REPORT_TOTAL_FOCUS_HOURS = 100L; - private static final long TRIP_REPORT_STUDY_DAYS = 10L; - private static final String TRIP_REPORT_IMAGE_TITLE = "TEST IMAGE TITLE"; - - public static TripReport createTripReport(Member member) { - return TripReportFactory.create( - member, - TRIP_REPORT_TITLE, - TRIP_REPORT_CONTENT, - TRIP_START_DATE, - TRIP_END_DATE, - STUDY_LOG_COUNT, - TRIP_REPORT_TOTAL_FOCUS_HOURS, - TRIP_REPORT_STUDY_DAYS, - TRIP_REPORT_IMAGE_TITLE); - } - - public static TripReport createTripReportWithId(Long id, Member member) { - TripReport tripReport = - TripReportFactory.create( - member, - TRIP_REPORT_TITLE, - TRIP_REPORT_CONTENT, - TRIP_START_DATE, - TRIP_END_DATE, - STUDY_LOG_COUNT, - TRIP_REPORT_TOTAL_FOCUS_HOURS, - TRIP_REPORT_STUDY_DAYS, - TRIP_REPORT_IMAGE_TITLE); - ReflectionTestUtils.setField(tripReport, "id", id); - - return tripReport; - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/TripReportStudyLogFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/TripReportStudyLogFixture.java deleted file mode 100644 index d0e0b6a..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/TripReportStudyLogFixture.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.studylog.domain.model.StudyLog; -import com.ject.studytrip.trip.domain.factory.TripReportStudyLogFactory; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.model.TripReportStudyLog; -import org.springframework.test.util.ReflectionTestUtils; - -public class TripReportStudyLogFixture { - - public static TripReportStudyLog createTripReportStudyLog( - TripReport tripReport, StudyLog studyLog) { - return TripReportStudyLogFactory.create(tripReport, studyLog); - } - - public static TripReportStudyLog createTripReportStudyLogWithId( - Long id, TripReport tripReport, StudyLog studyLog) { - TripReportStudyLog tripReportStudyLog = - TripReportStudyLogFactory.create(tripReport, studyLog); - ReflectionTestUtils.setField(tripReportStudyLog, "id", id); - - return tripReportStudyLog; - } -} diff --git a/src/test/java/com/ject/studytrip/trip/helper/TripReportTestHelper.java b/src/test/java/com/ject/studytrip/trip/helper/TripReportTestHelper.java deleted file mode 100644 index 9f993b4..0000000 --- a/src/test/java/com/ject/studytrip/trip/helper/TripReportTestHelper.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.ject.studytrip.trip.helper; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.domain.repository.TripReportRepository; -import com.ject.studytrip.trip.fixture.TripReportFixture; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class TripReportTestHelper { - - @Autowired private TripReportRepository tripReportRepository; - - public TripReport saveTripReport(Member member) { - TripReport tripReport = TripReportFixture.createTripReport(member); - return tripReportRepository.save(tripReport); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.java b/src/test/java/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.java deleted file mode 100644 index 85ea951..0000000 --- a/src/test/java/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.java +++ /dev/null @@ -1,966 +0,0 @@ -package com.ject.studytrip.trip.presentation.controller; - -import static com.ject.studytrip.auth.fixture.TokenFixture.TOKEN_PREFIX; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.ject.studytrip.BaseIntegrationTest; -import com.ject.studytrip.auth.domain.error.AuthErrorCode; -import com.ject.studytrip.auth.fixture.TokenFixture; -import com.ject.studytrip.auth.helper.TokenTestHelper; -import com.ject.studytrip.global.exception.error.CommonErrorCode; -import com.ject.studytrip.image.domain.error.ImageErrorCode; -import com.ject.studytrip.image.infra.s3.provider.S3ImageStorageProvider; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.helper.MemberTestHelper; -import com.ject.studytrip.mission.domain.model.DailyMission; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.mission.helper.DailyMissionTestHelper; -import com.ject.studytrip.mission.helper.MissionTestHelper; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.stamp.helper.StampTestHelper; -import com.ject.studytrip.studylog.domain.error.StudyLogErrorCode; -import com.ject.studytrip.studylog.domain.model.StudyLog; -import com.ject.studytrip.studylog.helper.StudyLogDailyMissionTestHelper; -import com.ject.studytrip.studylog.helper.StudyLogTestHelper; -import com.ject.studytrip.trip.domain.error.TripErrorCode; -import com.ject.studytrip.trip.domain.error.TripReportErrorCode; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.domain.model.TripReport; -import com.ject.studytrip.trip.fixture.ConfirmTripReportImageRequestFixture; -import com.ject.studytrip.trip.fixture.CreateTripReportRequestFixture; -import com.ject.studytrip.trip.fixture.PresignTripReportImageRequestFixture; -import com.ject.studytrip.trip.helper.DailyGoalTestHelper; -import com.ject.studytrip.trip.helper.TripReportTestHelper; -import com.ject.studytrip.trip.helper.TripTestHelper; -import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest; -import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest; -import java.util.List; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.web.servlet.ResultActions; - -@DisplayName("TripReportController 통합 테스트") -class TripReportControllerIntegrationTest extends BaseIntegrationTest { - @Autowired private MemberTestHelper memberTestHelper; - @Autowired private TokenTestHelper tokenTestHelper; - @Autowired private TripTestHelper tripTestHelper; - @Autowired private StampTestHelper stampTestHelper; - @Autowired private MissionTestHelper missionTestHelper; - @Autowired private DailyGoalTestHelper dailyGoalTestHelper; - @Autowired private StudyLogTestHelper studyLogTestHelper; - @Autowired private DailyMissionTestHelper dailyMissionTestHelper; - @Autowired private StudyLogDailyMissionTestHelper studyLogDailyMissionTestHelper; - @Autowired private TripReportTestHelper tripReportTestHelper; - - @MockitoBean S3ImageStorageProvider s3ImageStorageProvider; - - private String accessToken; - private String newAccessToken; - - private Member member; - private Trip courseTrip; - - private TripReport tripReport; - private StudyLog studyLog1; - private StudyLog studyLog2; - - @BeforeEach - void setUp() { - member = memberTestHelper.saveMember(); - Member newMember = memberTestHelper.saveMember("test@kakao.com", "TEST NICKNAME"); - - accessToken = - tokenTestHelper.createAccessToken( - member.getId().toString(), member.getRole().name()); - newAccessToken = - tokenTestHelper.createAccessToken( - newMember.getId().toString(), newMember.getRole().name()); - - courseTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE); - Stamp stamp = stampTestHelper.saveStamp(courseTrip, 1); - DailyGoal dailyGoal = dailyGoalTestHelper.saveDailyGoal(courseTrip); - Mission mission = missionTestHelper.saveMission(stamp); - DailyMission dailyMission = dailyMissionTestHelper.saveDailyMission(mission, dailyGoal); - tripReport = tripReportTestHelper.saveTripReport(member); - studyLog1 = studyLogTestHelper.saveStudyLog(member, dailyGoal); - studyLog2 = studyLogTestHelper.saveStudyLog(member, dailyGoal); - studyLogDailyMissionTestHelper.saveStudyLogDailyMissions(studyLog2, dailyMission); - } - - @Nested - @DisplayName("여행 회고 API") - class LoadTripRetrospect { - private static final String DEFAULT_PAGE = "0"; - private static final String DEFAULT_PAGE_SIZE = "5"; - - private ResultActions getResultActions( - String accessToken, Object tripId, String page, String size) throws Exception { - return mockMvc.perform( - get("/api/trips/{tripId}/retrospect", tripId) - .param("page", page) - .param("size", size) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + accessToken)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // when - ResultActions resultActions = - getResultActions("", courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("Request Param 페이징 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exception { - // given - String page = "test"; - String size = "test"; - - // when - ResultActions resultActions = getResultActions(accessToken, courseTrip, page, size); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("Request Param 페이징 데이터가 유효하지 않으면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenWhenPagingParameterIsInvalid() throws Exception { - // given - String page = "-1"; - String size = "100"; - - // when - ResultActions resultActions = - getResultActions(accessToken, courseTrip.getId(), page, size); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_NOT_VALID - .getStatus() - .value())); - } - - @Test - @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception { - // given - String invalidTripId = "abc"; - - // when - ResultActions resultActions = - getResultActions(accessToken, invalidTripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 여행일 경우 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripAlreadyDeleted() throws Exception { - // given - courseTrip.updateDeletedAt(); - - // when - ResultActions resultActions = - getResultActions( - accessToken, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_ALREADY_DELETED.getMessage())); - } - - @Test - @DisplayName("아직 완료되지 않은 여행일 경우 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripDoesNotCompleted() throws Exception { - // given - Trip courseTrip2 = tripTestHelper.saveTrip(member, TripCategory.COURSE); - - // when - ResultActions resultActions = - getResultActions( - accessToken, courseTrip2.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.TRIP_NOT_COMPLETED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_NOT_COMPLETED.getMessage())); - } - - @Test - @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") - void shouldReturnForbiddenWhenNotTripOwner() throws Exception { - // when - ResultActions resultActions = - getResultActions( - newAccessToken, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.NOT_TRIP_OWNER.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 여행 ID가 들어오면 404 Not Found를 반환한다.") - void shouldReturnNotFoundWhenTripIdIsInvalid() throws Exception { - // given - Long invalidTripId = 10000L; - - // when - ResultActions resultActions = - getResultActions(accessToken, invalidTripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("유효한 여행 ID가 들어오면 여행 회고 정보를 반환한다.") - void shouldReturnTripRetrospectWhenTripIdIsValid() throws Exception { - // when - ResultActions result = getResultActions(accessToken, courseTrip.getId(), "1", "10"); - - // then - result.andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data").isNotEmpty()) - .andExpect(jsonPath("$.data.name").isString()) - .andExpect(jsonPath("$.data.totalFocusHours").isNumber()) - .andExpect(jsonPath("$.data.studyLogCount").isNumber()) - .andExpect(jsonPath("$.data.studyLogIds").isArray()) - .andExpect(jsonPath("$.data.studyDays").isNumber()) - .andExpect(jsonPath("$.data.history").isNotEmpty()); - } - } - - @Nested - @DisplayName("여행 리포트 목록 조회 API") - class LoadTripReports { - private ResultActions getResultActions(String accessToken, Object tripId) throws Exception { - return mockMvc.perform( - get("/api/trip-reports", tripId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + accessToken)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // when - ResultActions resultActions = getResultActions("", courseTrip.getId()); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("유효한 멤버 ID가 들어오면 여행 리포트 목록을 반환한다.") - void shouldReturnLoadTripReportsWhenMemberIdIsValid() throws Exception { - // given - Long memberId = member.getId(); - - // when - ResultActions resultActions = getResultActions(accessToken, memberId); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data").exists()) - .andExpect(jsonPath("$.data.summary").exists()) - .andExpect(jsonPath("$.data.tripReports").isArray()); - } - } - - @Nested - @DisplayName("여행 리포트 상세 조회 API") - class LoadTripReport { - private static final String DEFAULT_PAGE = "0"; - private static final String DEFAULT_PAGE_SIZE = "5"; - - private ResultActions getResultActions( - String accessToken, Object tripReportId, String page, String size) - throws Exception { - return mockMvc.perform( - get("/api/trip-reports/{tripReportId}", tripReportId) - .param("page", page) - .param("size", size) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + accessToken)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // when - ResultActions resultActions = - getResultActions("", courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("Request Param 페이징 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exception { - // given - String page = "test"; - String size = "test"; - - // when - ResultActions resultActions = getResultActions(accessToken, courseTrip, page, size); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("Request Param 페이징 데이터가 유효하지 않으면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenWhenPagingParameterIsInvalid() throws Exception { - // given - String page = "-1"; - String size = "100"; - - // when - ResultActions resultActions = - getResultActions(accessToken, courseTrip.getId(), page, size); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_NOT_VALID - .getStatus() - .value())); - } - - @Test - @DisplayName("PathVariable 여행 리포트 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripReportIdTypeMismatch() throws Exception { - // given - String invalidId = "abc"; - - // when - ResultActions resultActions = - getResultActions(accessToken, invalidId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("여행 리포트의 소유자가 아니라면 403 Forbidden을 반환한다.") - void shouldReturnForbiddenWhenNotTripReportOwner() throws Exception { - // when - ResultActions resultActions = - getResultActions( - newAccessToken, tripReport.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - TripReportErrorCode.NOT_TRIP_REPORT_OWNER - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 여행 리포트 ID가 들어오면 404 Not Found를 반환한다.") - void shouldReturnNotFoundWhenTripReportIdIsInvalid() throws Exception { - // given - Long invalidId = -1L; - - // when - ResultActions resultActions = - getResultActions(accessToken, invalidId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - TripReportErrorCode.TRIP_REPORT_NOT_FOUND - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("이미 삭제된 여행 리포트일 경우 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripReportAlreadyDeleted() throws Exception { - // given - tripReport.updateDeletedAt(); - - // when - ResultActions resultActions = - getResultActions( - accessToken, tripReport.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value( - TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED - .getMessage())); - } - - @Test - @DisplayName("유효한 여행 리포트 ID가 들어오면 여행 리포트를 반환한다.") - void shouldReturnTripReportWhenTripReportIdIsValid() throws Exception { - // given - Long tripReportId = tripReport.getId(); - - // when - ResultActions resultActions = getResultActions(accessToken, tripReportId, "1", "10"); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data").exists()) - .andExpect(jsonPath("$.data.tripReportId").value(tripReportId)) - .andExpect(jsonPath("$.data.title").value(tripReport.getTitle())) - .andExpect(jsonPath("$.data.content").value(tripReport.getContent())) - .andExpect(jsonPath("$.data.startDate").value(tripReport.getStartDate())) - .andExpect(jsonPath("$.data.endDate").value(tripReport.getEndDate())) - .andExpect( - jsonPath("$.data.totalFocusHours") - .value(tripReport.getTotalFocusHours())) - .andExpect( - jsonPath("$.data.studyLogCount").value(tripReport.getStudyLogCount())) - .andExpect(jsonPath("$.data.studyDays").value(tripReport.getStudyDays())) - .andExpect(jsonPath("$.data.imageTitle").value(tripReport.getImageTitle())) - .andExpect(jsonPath("$.data.imageUrl").value(tripReport.getImageUrl())) - .andExpect(jsonPath("$.data.history").exists()) - .andExpect(jsonPath("$.data.history.studyLogs").isArray()) - .andExpect(jsonPath("$.data.history.hasNext").isBoolean()); - } - } - - @Nested - @DisplayName("여행 리포트 생성 API") - class CreateTripReport { - private final CreateTripReportRequestFixture fixture = new CreateTripReportRequestFixture(); - - private ResultActions getResultActions(String accessToken, CreateTripReportRequest request) - throws Exception { - return mockMvc.perform( - post("/api/trip-reports") - .header( - HttpHeaders.AUTHORIZATION, - TokenFixture.TOKEN_PREFIX + accessToken) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // given - CreateTripReportRequest request = fixture.build(); - - // when - ResultActions resultActions = getResultActions("", request); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("요청한 학습 로그 ID 목록으로 해당 학습 로그를 조회하고, 하나라도 일치하지 않는 경우 404 NotFound를 반환한다.") - void shouldReturnNotFoundWhenAnyStudyLogIdDoesNotExist() throws Exception { - // given - CreateTripReportRequest request = - fixture.withStudyLogIds(List.of(studyLog1.getId(), -1L)).build(); - - // when - ResultActions resultActions = getResultActions(accessToken, request); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - StudyLogErrorCode.STUDY_LOG_NOT_FOUND - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(StudyLogErrorCode.STUDY_LOG_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("조회된 학습 로그 목록 중 삭제된 학습 로그가 존재하면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenStudyLogAlreadyDeleted() throws Exception { - // given - studyLog1.updateDeletedAt(); - List studyLogIds = List.of(studyLog1.getId(), studyLog2.getId()); - CreateTripReportRequest request = fixture.withStudyLogIds(studyLogIds).build(); - - // when - ResultActions resultActions = getResultActions(accessToken, request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - StudyLogErrorCode.STUDY_LOG_ALREADY_DELETED - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value( - StudyLogErrorCode.STUDY_LOG_ALREADY_DELETED - .getMessage())); - } - - @Test - @DisplayName("유효한 요청이 들어오면 여행 리포트를 생성하고 반환한다.") - void shouldCreateTripReportWhenRequestIsValid() throws Exception { - // given - List studyLogIds = List.of(studyLog1.getId(), studyLog2.getId()); - CreateTripReportRequest request = fixture.withStudyLogIds(studyLogIds).build(); - - // when - ResultActions resultActions = getResultActions(accessToken, request); - - // then - resultActions - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.tripReportId").isNumber()); - } - } - - @Nested - @DisplayName("여행 리포트 삭제 API") - class DeleteTripReport { - private ResultActions getResultActions(String accessToken, Object tripReportId) - throws Exception { - return mockMvc.perform( - delete("/api/trip-reports/{tripReportId}", tripReportId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + accessToken)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // when - ResultActions resultActions = getResultActions("", tripReport.getId()); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("PathVariable 여행 리포트 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripReportIdTypeMismatch() throws Exception { - // given - String invalidId = "abc"; - - // when - ResultActions resultActions = getResultActions(accessToken, invalidId); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())); - } - - @Test - @DisplayName("여행 리포트의 소유자가 아니라면 403 Forbidden을 반환한다.") - void shouldReturnForbiddenWhenNotTripReportOwner() throws Exception { - // when - ResultActions resultActions = getResultActions(newAccessToken, tripReport.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - TripReportErrorCode.NOT_TRIP_REPORT_OWNER - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 여행 리포트 ID가 들어오면 404 Not Found를 반환한다.") - void shouldReturnNotFoundWhenTripReportIdIsInvalid() throws Exception { - // given - Long invalidId = -1L; - - // when - ResultActions resultActions = getResultActions(accessToken, invalidId); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - TripReportErrorCode.TRIP_REPORT_NOT_FOUND - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("이미 삭제된 여행 리포트일 경우 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripReportAlreadyDeleted() throws Exception { - // given - tripReport.updateDeletedAt(); - - // when - ResultActions resultActions = getResultActions(accessToken, tripReport.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value( - TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED - .getMessage())); - } - - @Test - @DisplayName("여행 리포트 ID가 유효하면 여행 리포트를 삭제한다.") - void shouldDeleteTripReportWhenTripReportIdIsValid() throws Exception { - // given - Long tripReportId = tripReport.getId(); - - // when - ResultActions resultActions = getResultActions(accessToken, tripReportId); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data").doesNotExist()); - } - } - - @Nested - @DisplayName("여행 리포트 이미지 Presigned URL 발급 API") - class IssuePresignedUrl { - private static final String PRESIGNED_URL = "/api/trip-reports/%d/images/presigned"; - - private final PresignTripReportImageRequestFixture fixture = - new PresignTripReportImageRequestFixture(); - - private ResultActions getResultActions( - String accessToken, Long tripReportId, PresignTripReportImageRequest request) - throws Exception { - return mockMvc.perform( - post(String.format(PRESIGNED_URL, tripReportId)) - .header( - HttpHeaders.AUTHORIZATION, - TokenFixture.TOKEN_PREFIX + accessToken) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // given - PresignTripReportImageRequest request = fixture.build(); - - // when - ResultActions resultActions = getResultActions("", tripReport.getId(), request); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(AuthErrorCode.UNAUTHENTICATED.getMessage())); - } - - @Test - @DisplayName("파일명이 비어있으면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenFilenameIsEmpty() throws Exception { - // given - PresignTripReportImageRequest request = fixture.withOriginFilename("").build(); - - // when - ResultActions resultActions = - getResultActions(accessToken, tripReport.getId(), request); - - // then - resultActions.andExpect(status().isBadRequest()); - } - - @Test - @DisplayName("유효하지 않은 확장자는 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenExtensionIsInvalid() throws Exception { - // given - PresignTripReportImageRequest request = fixture.withOriginFilename("test.pdf").build(); - - // when - ResultActions resultActions = - getResultActions(accessToken, tripReport.getId(), request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - ImageErrorCode.INVALID_IMAGE_EXTENSION - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(ImageErrorCode.INVALID_IMAGE_EXTENSION.getMessage())); - } - - @Test - @DisplayName("유효한 파일명으로 Presigned URL을 발급한다.") - void shouldIssuePresignedUrlWhenFilenameIsValid() throws Exception { - // given - PresignTripReportImageRequest request = fixture.build(); - given(s3ImageStorageProvider.issuePresignedUrl(anyString())) - .willReturn("https://mocked-presigned-url.com"); - - // when - ResultActions resultActions = - getResultActions(accessToken, tripReport.getId(), request); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) - .andExpect(jsonPath("$.data.presignedUrl").isNotEmpty()) - .andExpect(jsonPath("$.data.tmpKey").isNotEmpty()) - .andExpect( - jsonPath("$.data.tmpKey") - .value(Matchers.startsWith("tmp/trip-reports/"))) - .andExpect( - jsonPath("$.data.tmpKey") - .value(Matchers.containsString(tripReport.getId().toString()))); - - verify(s3ImageStorageProvider).issuePresignedUrl(anyString()); - } - } - - @Nested - @DisplayName("여행 리포트 이미지 확정 API") - class ConfirmImage { - private static final String CONFIRM_URL = "/api/trip-reports/%d/images/confirm"; - - private final ConfirmTripReportImageRequestFixture fixture = - new ConfirmTripReportImageRequestFixture(); - - private ResultActions getResultActions( - String accessToken, Long tripReportId, ConfirmTripReportImageRequest request) - throws Exception { - return mockMvc.perform( - post(String.format(CONFIRM_URL, tripReportId)) - .header( - HttpHeaders.AUTHORIZATION, - TokenFixture.TOKEN_PREFIX + accessToken) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // given - ConfirmTripReportImageRequest request = fixture.build(); - - // when - ResultActions resultActions = getResultActions("", tripReport.getId(), request); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(AuthErrorCode.UNAUTHENTICATED.getMessage())); - } - - @Test - @DisplayName("tmpKey가 비어있으면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTmpKeyIsEmpty() throws Exception { - // given - ConfirmTripReportImageRequest request = fixture.withTmpKey("").build(); - - // when - ResultActions resultActions = - getResultActions(accessToken, tripReport.getId(), request); - - // then - resultActions.andExpect(status().isBadRequest()); - } - } -} diff --git a/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.kt new file mode 100644 index 0000000..cf360c6 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.kt @@ -0,0 +1,62 @@ +package com.ject.studytrip.dummy.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.stamp.domain.model.Stamp +import com.ject.studytrip.stamp.fixture.StampFixture +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.fixture.TripFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.InjectMocks + +@DisplayName("DummyMissionCommandService 단위 테스트") +class DummyMissionCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var dummyMissionCommandService: DummyMissionCommandService + + private lateinit var courseStamp: Stamp + private lateinit var exploreStamp: Stamp + + @BeforeEach + fun setUp() { + val member = MemberFixture.createMemberFromKakao() + val courseTrip = TripFixture(member, TripCategory.COURSE).create() + val exploreTrip = TripFixture(member, TripCategory.EXPLORE).create() + courseStamp = StampFixture(courseTrip, DUMMY_STAMP_COUNT).create() + exploreStamp = StampFixture(exploreTrip, 0).create() + } + + companion object { + private const val DUMMY_STAMP_COUNT = 10 + } + + @Nested + @DisplayName("createDummyMission 메서드는") + inner class CreateDummyMission { + @Test + @DisplayName("코스형 스탬프에 대한 더미 미션을 생성하고 반환한다.") + fun shouldReturnDummyMissionForCourseStamp() { + // when + val result = dummyMissionCommandService.createDummyMission(courseStamp) + + // then + assertThat(result).isNotNull + assertThat(result.stamp.stampOrder).isPositive + } + + @Test + @DisplayName("탐혐형 스탬프에 대한 더미 미션을 생성하고 반환한다.") + fun shouldReturnDummyMissionForExploreStamp() { + // when + val result = dummyMissionCommandService.createDummyMission(exploreStamp) + + // then + assertThat(result).isNotNull + assertThat(result.stamp.stampOrder).isZero + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.kt new file mode 100644 index 0000000..d45a03c --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.kt @@ -0,0 +1,56 @@ +package com.ject.studytrip.dummy.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.fixture.TripFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.InjectMocks + +@DisplayName("DummyStampCommandService 단위 테스트") +class DummyStampCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var dummyStampCommandService: DummyStampCommandService + + private lateinit var courseTrip: Trip + private lateinit var exploreTrip: Trip + + @BeforeEach + fun setUp() { + val member = MemberFixture.createMemberFromKakao() + courseTrip = TripFixture(member, TripCategory.COURSE).create() + exploreTrip = TripFixture(member, TripCategory.EXPLORE).create() + } + + companion object { + private const val DUMMY_STAMP_COUNT = 10 + } + + @Test + @DisplayName("코스형 여행에 대한 더미 스탬프를 생성하고 반환한다.") + fun shouldReturnDummyStampForCourseTrip() { + // when + val result = dummyStampCommandService.createDummyStamp(courseTrip, DUMMY_STAMP_COUNT) + + // then + assertThat(result).isNotNull + assertThat(result.stampOrder).isPositive + assertThat(result.endDate).isNotNull + } + + @Test + @DisplayName("탐험형 여행에 대한 더미 스탬프를 생성하고 반환한다.") + fun shouldReturnDummyStampForExploreTrip() { + // when + val result = dummyStampCommandService.createDummyStamp(exploreTrip, DUMMY_STAMP_COUNT) + + // then + assertThat(result).isNotNull + assertThat(result.stampOrder).isZero + assertThat(result.endDate).isNull() + } +} diff --git a/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.kt new file mode 100644 index 0000000..20e497a --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/dummy/application/service/DummyTripCommandServiceTest.kt @@ -0,0 +1,62 @@ +package com.ject.studytrip.dummy.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.InjectMocks + +@DisplayName("DummyTripCommandService 단위 테스트") +class DummyTripCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var dummyTripCommandService: DummyTripCommandService + + private lateinit var member: Member + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakao() + } + + companion object { + private const val DUMMY_TRIP_COUNT = 10 + } + + @Nested + @DisplayName("createDummyTrip 메서드는") + inner class CreateDummyTrip { + @Test + @DisplayName("코스형 더미 여행을 생성하고 반환한다.") + fun shouldReturnDummyCourseTripWhenCategoryIsCourse() { + // given + val category = "COURSE" + + // when + val result = dummyTripCommandService.createDummyTrip(member, category, DUMMY_TRIP_COUNT) + + // then + assertThat(result).isNotNull + assertThat(result.category.name).isEqualTo(category) + assertThat(result.endDate).isNotNull + } + + @Test + @DisplayName("탐험형 더미 여행을 생성하고 반환한다.") + fun shouldReturnDummyExploreTripWhenCategoryIsExplore() { + // given + val category = "EXPLORE" + + // when + val result = dummyTripCommandService.createDummyTrip(member, category, DUMMY_TRIP_COUNT) + + // then + assertThat(result).isNotNull + assertThat(result.category.name).isEqualTo(category) + assertThat(result.endDate).isNull() + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.kt new file mode 100644 index 0000000..b19bef4 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyMissionControllerIntegrationTest.kt @@ -0,0 +1,137 @@ +package com.ject.studytrip.dummy.presentation.controller + +import com.ject.studytrip.BaseIntegrationTest +import com.ject.studytrip.auth.domain.error.AuthErrorCode +import com.ject.studytrip.auth.fixture.TokenFixture +import com.ject.studytrip.auth.helper.TokenTestHelper +import com.ject.studytrip.global.exception.error.CommonErrorCode +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.helper.MemberTestHelper +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@DisplayName("DummyMissionController 통합 테스트") +class DummyMissionControllerIntegrationTest : BaseIntegrationTest() { + @Autowired private lateinit var memberTestHelper: MemberTestHelper + + @Autowired private lateinit var tokenTestHelper: TokenTestHelper + + private lateinit var member: Member + private lateinit var token: String + + @BeforeEach + fun setUp() { + member = memberTestHelper.saveMember() + token = tokenTestHelper.createAccessToken(member.id.toString(), member.role.name) + } + + companion object { + private const val BASE_DUMMY_MISSION_URL = "/api/dummies/missions" + private const val COURSE_CATEGORY = "COURSE" + private const val EXPLORE_CATEGORY = "EXPLORE" + private const val DUMMY_MISSION_COUNT = 10 + } + + @Nested + @DisplayName("더미 미션 목록 조회 API") + inner class LoadDummyMissions { + private fun getResultActions( + token: String, + category: String, + count: Any, + ): ResultActions = + mockMvc.perform( + get(BASE_DUMMY_MISSION_URL) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .param("category", category) + .param("count", count.toString()) + .contentType(MediaType.APPLICATION_JSON), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", COURSE_CATEGORY, DUMMY_MISSION_COUNT) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("Request Param 카테고리 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenCategoryParameterTypeMismatch() { + // given + val category = "INVALID" + + // when + val resultActions = getResultActions(token, category, DUMMY_MISSION_COUNT) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.CONSTRAINT_VIOLATION.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.CONSTRAINT_VIOLATION.message)) + } + + @Test + @DisplayName("Request Param 더미 데이터 개수가 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenCountParameterIsInvalid() { + // given + val count = -1 + + // when + val resultActions = getResultActions(token, COURSE_CATEGORY, count) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.CONSTRAINT_VIOLATION.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.CONSTRAINT_VIOLATION.message)) + } + + @Test + @DisplayName("COURSE 카테고리가 들어오면 더미 미션 목록를 반환한다. (DB 저장 X)") + fun shouldReturnDummyMissionsWhenCategoryIsCourse() { + // when + val resultActions = getResultActions(token, COURSE_CATEGORY, DUMMY_MISSION_COUNT) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isArray) + } + + @Test + @DisplayName("EXPLORE 카테고리가 들어오면 더미 미션 목록를 반환한다. (DB 저장 X)") + fun shouldReturnDummyMissionsWhenCategoryIsExplore() { + // when + val resultActions = getResultActions(token, EXPLORE_CATEGORY, DUMMY_MISSION_COUNT) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isArray) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.kt new file mode 100644 index 0000000..ef882c7 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/dummy/presentation/controller/DummyStampControllerIntegrationTest.kt @@ -0,0 +1,139 @@ +package com.ject.studytrip.dummy.presentation.controller + +import com.ject.studytrip.BaseIntegrationTest +import com.ject.studytrip.auth.domain.error.AuthErrorCode +import com.ject.studytrip.auth.fixture.TokenFixture +import com.ject.studytrip.auth.helper.TokenTestHelper +import com.ject.studytrip.global.exception.error.CommonErrorCode +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.helper.MemberTestHelper +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@DisplayName("DummyStampController 통합 테스트") +class DummyStampControllerIntegrationTest : BaseIntegrationTest() { + @Autowired private lateinit var memberTestHelper: MemberTestHelper + + @Autowired private lateinit var tokenTestHelper: TokenTestHelper + + private lateinit var member: Member + private lateinit var token: String + + @BeforeEach + fun setUp() { + member = memberTestHelper.saveMember() + token = tokenTestHelper.createAccessToken(member.id.toString(), member.role.name) + } + + companion object { + private const val BASE_DUMMY_STAMP_URL = "/api/dummies/stamps" + private const val COURSE_CATEGORY = "COURSE" + private const val EXPLORE_CATEGORY = "EXPLORE" + private const val DUMMY_STAMP_COUNT = 10 + } + + @Nested + @DisplayName("더미 스탬프 목록 조회 API") + inner class LoadDummyMissions { + private fun getResultActions( + token: String, + category: String, + count: Any, + ): ResultActions = + mockMvc.perform( + get(BASE_DUMMY_STAMP_URL) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .param("category", category) + .param("count", count.toString()) + .contentType(MediaType.APPLICATION_JSON), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", COURSE_CATEGORY, DUMMY_STAMP_COUNT) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("Request Param 카테고리 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenCategoryParameterTypeMismatch() { + // given + val category = "INVALID" + + // when + val resultActions = getResultActions(token, category, DUMMY_STAMP_COUNT) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.CONSTRAINT_VIOLATION.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.CONSTRAINT_VIOLATION.message)) + } + + @Test + @DisplayName("Request Param 더미 데이터 개수가 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenCountParameterIsInvalid() { + // given + val count = -1 + + // when + val resultActions = getResultActions(token, COURSE_CATEGORY, count) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.CONSTRAINT_VIOLATION.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.CONSTRAINT_VIOLATION.message)) + } + + @Test + @DisplayName("COURSE 카테고리가 들어오면 코스형 더미 스탬프 목록를 반환한다. (DB 저장 X)") + fun shouldReturnDummyCourseStampsWhenCategoryIsCourse() { + // when + val resultActions = getResultActions(token, COURSE_CATEGORY, DUMMY_STAMP_COUNT) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isArray) + .andExpect(jsonPath("$.data[0].stampOrder").value(1)) + } + + @Test + @DisplayName("EXPLORE 카테고리가 들어오면 탐험형 더미 스탬프 목록를 반환한다. (DB 저장 X)") + fun shouldReturnDummyExploreStampsWhenCategoryIsExplore() { + // when + val resultActions = getResultActions(token, EXPLORE_CATEGORY, DUMMY_STAMP_COUNT) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isArray) + .andExpect(jsonPath("$.data[0].stampOrder").value(0)) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt index 5306c20..4d85b6e 100644 --- a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt @@ -62,7 +62,7 @@ class StudyLogQueryServiceTest : BaseUnitTest() { dailyGoal = DailyGoalFixture(courseTrip).createWithId(1L) studyLog1 = StudyLogFixture(member, dailyGoal).createWithId(1L) studyLog2 = StudyLogFixture(member, dailyGoal).createWithId(2L) - tripReport = TripReportFixture.createTripReportWithId(1L, member) + tripReport = TripReportFixture(member).createWithId(1L) } @Nested diff --git a/src/test/kotlin/com/ject/studytrip/studylog/helper/StudyLogTestHelper.kt b/src/test/kotlin/com/ject/studytrip/studylog/helper/StudyLogTestHelper.kt index c42aa5f..f9ccb87 100644 --- a/src/test/kotlin/com/ject/studytrip/studylog/helper/StudyLogTestHelper.kt +++ b/src/test/kotlin/com/ject/studytrip/studylog/helper/StudyLogTestHelper.kt @@ -15,4 +15,9 @@ class StudyLogTestHelper( member: Member, dailyGoal: DailyGoal, ): StudyLog = studyLogRepository.save(StudyLogFixture(member, dailyGoal).create()) + + fun saveDeletedStudyLog( + member: Member, + dailyGoal: DailyGoal, + ): StudyLog = studyLogRepository.save(StudyLogFixture(member, dailyGoal).create().also { it.updateDeletedAt() }) } diff --git a/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.kt new file mode 100644 index 0000000..3d6e009 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.kt @@ -0,0 +1,196 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.studylog.fixture.StudyLogFixture +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.repository.TripReportCommandRepository +import com.ject.studytrip.trip.domain.repository.TripReportRepository +import com.ject.studytrip.trip.fixture.CreateTripReportRequestFixture +import com.ject.studytrip.trip.fixture.DailyGoalFixture +import com.ject.studytrip.trip.fixture.TripFixture +import com.ject.studytrip.trip.fixture.TripReportFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.kotlin.any + +@DisplayName("TripReportCommandService 단위 테스트") +class TripReportCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var tripReportCommandService: TripReportCommandService + + @Mock + private lateinit var tripReportRepository: TripReportRepository + + @Mock + private lateinit var tripReportCommandRepository: TripReportCommandRepository + + private lateinit var member: Member + private lateinit var studyLogIds: List + private lateinit var tripReport: TripReport + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + val trip = TripFixture(member, TripCategory.COURSE).create() + val dailyGoal = DailyGoalFixture(trip).create() + val studyLog1 = StudyLogFixture(member, dailyGoal).createWithId(1L) + val studyLog2 = StudyLogFixture(member, dailyGoal).createWithId(2L) + studyLogIds = listOf(studyLog1.id, studyLog2.id) + tripReport = TripReportFixture(member).create() + } + + @Nested + @DisplayName("createTripReport 메서드는") + inner class CreateTripReport { + private val fixture = CreateTripReportRequestFixture() + + @Test + @DisplayName("요청이 유효하면 여행 리포트를 생성하고 반환한다.") + fun shouldCreateAndReturnTripReportWhenRequestIsValid() { + // given + val request = fixture.withStudyLogIds(studyLogIds).build() + given(tripReportRepository.save(any())).willReturn(tripReport) + + // when + val result = tripReportCommandService.createTripReport(member, request) + + // then + assertThat(result).isEqualTo(tripReport) + } + } + + @Nested + @DisplayName("updateImageUrl 메서드는") + inner class UpdateImageUrl { + private val newImageUrl = "https://cdn.example.com/trip-reports/1/image.jpg" + + @Test + @DisplayName("유효한 여행 리포트의 이미지 URL을 수정한다.") + fun shouldUpdateTripReportImageUrlWhenTripReportIsValid() { + // given + val oldImageUrl = tripReport.imageUrl + + // when + tripReportCommandService.updateImageUrl(tripReport, newImageUrl) + + // then + assertThat(tripReport.imageUrl).isEqualTo(newImageUrl) + assertThat(tripReport.imageUrl).isNotEqualTo(oldImageUrl) + } + } + + @Nested + @DisplayName("deleteTripReport 메서드는") + inner class DeleteTripReport { + @Test + @DisplayName("여행 리포트가 삭제될 때 deletedAt 필드를 현재 시간으로 업데이트한다. (소프트 삭제)") + fun shouldUpdateDeletedAtWhenTripReportIsDeleted() { + // when + tripReportCommandService.deleteTripReport(tripReport) + + // then + assertThat(tripReport.deletedAt).isNotNull + } + } + + @Nested + @DisplayName("hardDeleteTripReports 메서드는") + inner class HardDeleteTripReports { + @Test + @DisplayName("삭제된 여행 리포트가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenDeletedTripReportsDoNotExist() { + // given + given(tripReportCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L) + + // when + val result = tripReportCommandService.hardDeleteTripReports() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 여행 리포트가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenDeletedTripReportsExist() { + // given + given(tripReportCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L) + + // when + val result = tripReportCommandService.hardDeleteTripReports() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteTripReportsOwnedByDeletedMember 메서드는") + inner class HardDeleteTripReportsOwnedByDeletedMember { + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenTripReportsOwnedByDeletedMemberDoNotExist() { + // given + given(tripReportCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(0L) + + // when + val result = tripReportCommandService.hardDeleteTripReportsOwnedByDeletedMember() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenTripReportsOwnedByDeletedMemberExist() { + // given + given(tripReportCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(5L) + + // when + val result = tripReportCommandService.hardDeleteTripReportsOwnedByDeletedMember() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteTripReportsOwnedByMember 메서드는") + inner class HardDeleteTripReportsOwnedByMember { + @Test + @DisplayName("특정 멤버가 소유한 여행 리포트가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenTripReportsOwnedByMemberDoNotExist() { + // given + val memberId = -1L + given(tripReportCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L) + + // when + val result = tripReportCommandService.hardDeleteTripReportsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("특정 멤버가 소유한 여행 리포트가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenTripReportsOwnedByMemberExist() { + // given + val memberId = member.id + given(tripReportCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L) + + // when + val result = tripReportCommandService.hardDeleteTripReportsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(5L) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.kt new file mode 100644 index 0000000..6cec695 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.kt @@ -0,0 +1,207 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.trip.domain.error.TripReportErrorCode +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.repository.TripReportQueryRepository +import com.ject.studytrip.trip.domain.repository.TripReportRepository +import com.ject.studytrip.trip.fixture.TripReportFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.kotlin.given +import java.util.Optional + +@DisplayName("TripReportQueryService 단위 테스트") +class TripReportQueryServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var tripReportQueryService: TripReportQueryService + + @Mock + private lateinit var tripReportRepository: TripReportRepository + + @Mock + private lateinit var tripReportQueryRepository: TripReportQueryRepository + + private lateinit var member: Member + private lateinit var tripReport1: TripReport + private lateinit var tripReport2: TripReport + private lateinit var tripReports: List + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + tripReport1 = TripReportFixture(member).createWithId(1L) + tripReport2 = TripReportFixture(member).createWithId(2L) + tripReports = listOf(tripReport1, tripReport2) + } + + @Nested + @DisplayName("getTripReport 메서드는") + inner class GetTripReport { + @Test + @DisplayName("여행 리포트가 존재하지 않으면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripReportDoesNotExist() { + // given + val tripReportId = -1L + given(tripReportRepository.findById(tripReportId)).willReturn(Optional.empty()) + + // when + val exception = assertThrows { tripReportQueryService.getTripReport(tripReportId) } + + // then + assertThat(exception.message).isEqualTo(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.message) + } + + @Test + @DisplayName("여행 리포트가 존재하면 여행을 반환한다.") + fun shouldReturnTripWhenTripReportExists() { + // given + val tripReportId = tripReport1.id + given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)) + + // when + val result = tripReportQueryService.getTripReport(tripReportId) + + // then + assertThat(result).isEqualTo(tripReport1) + } + } + + @Nested + @DisplayName("getValidTripReport 메서드는") + inner class GetValidTripReport { + @Test + @DisplayName("여행 리포트가 존재하지 않으면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripReportDoesNotExist() { + // given + val tripReportId = -1L + given(tripReportRepository.findById(tripReportId)).willReturn(Optional.empty()) + + // when + val exception = assertThrows { tripReportQueryService.getValidTripReport(member.id, tripReportId) } + + // then + assertThat(exception.message).isEqualTo(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.message) + } + + @Test + @DisplayName("멤버가 여행 리포트의 소유자가 아니라면 예외가 발생한다.") + fun shouldThrowExceptionWhenMemberIsNotTripReportOwner() { + // given + val memberId = -1L + val tripReportId = tripReport1.id + given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)) + + // when + val exception = assertThrows { tripReportQueryService.getValidTripReport(memberId, tripReportId) } + + // then + assertThat(exception.message).isEqualTo(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.message) + } + + @Test + @DisplayName("여행 리포트가 이미 삭제되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripReportAlreadyDeleted() { + // given + val tripReportId = tripReport1.id + tripReport1.updateDeletedAt() + given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)) + + // when + val exception = assertThrows { tripReportQueryService.getValidTripReport(member.id, tripReportId) } + + // then + assertThat(exception.message).isEqualTo(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED.message) + } + + @Test + @DisplayName("여행 리포트가 존재하면 여행을 반환한다.") + fun shouldReturnTripWhenTripReportExists() { + // given + val tripReportId = tripReport1.id + given(tripReportRepository.findById(tripReportId)).willReturn(Optional.of(tripReport1)) + + // when + val result = tripReportQueryService.getValidTripReport(member.id, tripReportId) + + // then + assertThat(result).isEqualTo(tripReport1) + } + } + + @Nested + @DisplayName("getTripReportsByMemberId 메서드는") + inner class GetTripReportsByMemberId { + @Test + @DisplayName("여행 리포트가 존재하지 않으면 빈 리스트를 반환한다.") + fun shouldReturnEmptyListWhenTripReportDoesNotExist() { + // given + val memberId = member.id + given(tripReportQueryRepository.findAllActiveByMemberId(memberId)).willReturn(emptyList()) + + // when + val result = tripReportQueryService.getTripReportsByMemberId(memberId) + + // then + assertThat(result).isEmpty() + } + + @Test + @DisplayName("여행 리포트가 하나라도 존재하면 특정 멤버가 생성한 여행 리포트 목록을 반환한다.") + fun shouldReturnTripReports() { + // given + val memberId = member.id + given(tripReportQueryRepository.findAllActiveByMemberId(memberId)).willReturn(tripReports) + + // when + val result = tripReportQueryService.getTripReportsByMemberId(memberId) + + // then + assertThat(result).hasSize(2) + assertThat(result).containsExactly(tripReport1, tripReport2) + } + } + + @Nested + @DisplayName("getTripReportImageUrlsByMemberId 메서드는") + inner class GetTripReportImageUrlsByMemberId { + @Test + @DisplayName("이미지가 존재하지 않으면 빈 리스트를 반환한다.") + fun shouldReturnEmptyListWhenImagesDoNotExist() { + // given + val memberId = member.id + given(tripReportQueryRepository.findImageUrlsByMemberId(memberId)).willReturn(emptyList()) + + // when + val result = tripReportQueryService.getTripReportImageUrlsByMemberId(memberId) + + // then + assertThat(result).isEmpty() + } + + @Test + @DisplayName("이미지가 존재하면 여행 리포트 이미지 URL 목록을 반환한다.") + fun shouldReturnTripReportImageUrlsWhenImagesExist() { + // given + val memberId = member.id + val imageUrls = listOf("https://cdn.example.com/reports/1.jpg", "https://cdn.example.com/reports/2.jpg") + given(tripReportQueryRepository.findImageUrlsByMemberId(memberId)).willReturn(imageUrls) + + // when + val result = tripReportQueryService.getTripReportImageUrlsByMemberId(memberId) + + // then + assertThat(result).hasSize(2) + assertThat(result).isEqualTo(imageUrls) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.kt new file mode 100644 index 0000000..904a763 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.kt @@ -0,0 +1,127 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.studylog.domain.model.StudyLog +import com.ject.studytrip.studylog.fixture.StudyLogFixture +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogCommandRepository +import com.ject.studytrip.trip.domain.repository.TripReportStudyLogRepository +import com.ject.studytrip.trip.fixture.DailyGoalFixture +import com.ject.studytrip.trip.fixture.TripFixture +import com.ject.studytrip.trip.fixture.TripReportFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.anyList +import org.mockito.BDDMockito.given +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.verify + +@DisplayName("TripReportStudyLogCommandService 단위 테스트") +class TripReportStudyLogCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var tripReportStudyLogCommandService: TripReportStudyLogCommandService + + @Mock + private lateinit var tripReportStudyLogRepository: TripReportStudyLogRepository + + @Mock + private lateinit var tripReportStudyLogCommandRepository: TripReportStudyLogCommandRepository + + private lateinit var member: Member + private lateinit var tripReport: TripReport + private lateinit var studyLogs: List + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + val trip = TripFixture(member, TripCategory.COURSE).create() + val dailyGoal = DailyGoalFixture(trip).create() + val studyLog1 = StudyLogFixture(member, dailyGoal).createWithId(1L) + val studyLog2 = StudyLogFixture(member, dailyGoal).createWithId(2L) + tripReport = TripReportFixture(member).createWithId(1L) + studyLogs = listOf(studyLog1, studyLog2) + } + + @Nested + @DisplayName("createTripReportStudyLogs 메서드는") + inner class CreateTripReportStudyLogs { + @Test + @DisplayName("여행 리포트 학습 로그를 생성한다.") + fun shouldCreateTripReportStudyLogs() { + // when + tripReportStudyLogCommandService.createTripReportStudyLogs(tripReport, studyLogs) + + // then + verify(tripReportStudyLogRepository).saveAll(anyList()) + } + } + + @Nested + @DisplayName("hardDeleteTripReportStudyLogsOwnedByDeletedMember 메서드는") + inner class HardDeleteTripReportStudyLogsOwnedByDeletedMember { + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트 학습 로그가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenTripReportStudyLogsOwnedByDeletedMemberDoNotExist() { + // given + given(tripReportStudyLogCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(0L) + + // when + val result = tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsOwnedByDeletedMember() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 멤버가 소유한 여행 리포트 학습 로그가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenTripReportStudyLogsOwnedByDeletedMemberExist() { + // given + given(tripReportStudyLogCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(5L) + + // when + val result = tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsOwnedByDeletedMember() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteTripReportStudyLogsOwnedByMember 메서드는") + inner class HardDeleteTripReportStudyLogsOwnedByMember { + @Test + @DisplayName("특정 멤버가 소유한 여행 리포트 학습 로그가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenTripReportStudyLogsOwnedByMemberDoNotExist() { + // given + val memberId = member.id + given(tripReportStudyLogCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L) + + // when + val result = tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("특정 멤버가 소유한 여행 리포트 학습 로그가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenTripReportStudyLogsOwnedByMemberExist() { + // given + val memberId = member.id + given(tripReportStudyLogCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L) + + // when + val result = tripReportStudyLogCommandService.hardDeleteTripReportStudyLogsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(5L) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.kt new file mode 100644 index 0000000..ae718f3 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/ConfirmTripReportImageRequestFixture.kt @@ -0,0 +1,11 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest + +class ConfirmTripReportImageRequestFixture { + var tmpKey: String = "tmp/trip-reports/1/test.jpg" + + fun withTmpKey(tmpKey: String): ConfirmTripReportImageRequestFixture = apply { this.tmpKey = tmpKey } + + fun build(): ConfirmTripReportImageRequest = ConfirmTripReportImageRequest(tmpKey) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.kt new file mode 100644 index 0000000..10da1b3 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateTripReportRequestFixture.kt @@ -0,0 +1,30 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest + +class CreateTripReportRequestFixture { + var title: String = "TEST 여행 리포트 제목" + var content: String = "TEST 여행 리포트 내용" + var startDate: String = "2018.01.01" + var endDate: String = "2018.01.31" + var studyLogCount: Long = 10L + var totalFocusHours: Long = 100L + var studyDays: Long = 10L + var imageTitle: String = "TEST 여행 리포트 이미지 제목" + var studyLogIds: List = emptyList() + + fun withStudyLogIds(studyLogIds: List): CreateTripReportRequestFixture = apply { this.studyLogIds = studyLogIds } + + fun build(): CreateTripReportRequest = + CreateTripReportRequest( + title, + content, + startDate, + endDate, + studyLogCount, + totalFocusHours, + studyDays, + imageTitle, + studyLogIds, + ) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.kt new file mode 100644 index 0000000..4baf9df --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/PresignTripReportImageRequestFixture.kt @@ -0,0 +1,11 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest + +class PresignTripReportImageRequestFixture { + var originFilename: String = "test.jpg" + + fun withOriginFilename(originFilename: String): PresignTripReportImageRequestFixture = apply { this.originFilename = originFilename } + + fun build(): PresignTripReportImageRequest = PresignTripReportImageRequest(originFilename) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/TripReportFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/TripReportFixture.kt new file mode 100644 index 0000000..2f6a160 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/TripReportFixture.kt @@ -0,0 +1,24 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.factory.TripReportFactory +import com.ject.studytrip.trip.domain.model.TripReport +import org.springframework.test.util.ReflectionTestUtils + +class TripReportFixture( + private val member: Member, +) { + var title: String = "TEST 여행 리포트 제목" + var content: String = "TEST 여행 리포트 내용" + var startDate: String = "2018.01.01" + var endDate: String = "2018.01.31" + var studyLogCount: Long = 10L + var totalFocusHours: Long = 100L + var studyDays: Long = 10L + var imageTitle: String = "TEST 여행 리포트 이미지 제목" + + fun create(): TripReport = + TripReportFactory.create(member, title, content, startDate, endDate, studyLogCount, totalFocusHours, studyDays, imageTitle) + + fun createWithId(id: Long): TripReport = create().also { ReflectionTestUtils.setField(it, "id", id) } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/helper/TripReportTestHelper.kt b/src/test/kotlin/com/ject/studytrip/trip/helper/TripReportTestHelper.kt new file mode 100644 index 0000000..d3d363d --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/helper/TripReportTestHelper.kt @@ -0,0 +1,21 @@ +package com.ject.studytrip.trip.helper + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.domain.repository.TripReportRepository +import com.ject.studytrip.trip.fixture.TripReportFixture +import org.springframework.stereotype.Component + +@Component +class TripReportTestHelper( + private val tripReportRepository: TripReportRepository, +) { + fun saveTripReport(member: Member): TripReport = tripReportRepository.save(TripReportFixture(member).create()) + + fun saveDeletedTripReport(member: Member): TripReport = + tripReportRepository.save( + TripReportFixture(member).create().also { + it.updateDeletedAt() + }, + ) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.kt index 8b3c9e6..853fa84 100644 --- a/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.kt +++ b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.kt @@ -181,6 +181,7 @@ class TripControllerIntegrationTest : BaseIntegrationTest() { resultActions .andExpect(status().isCreated) .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.CREATED.value())) .andExpect(jsonPath("$.data.tripId").isNumber) } } @@ -468,7 +469,7 @@ class TripControllerIntegrationTest : BaseIntegrationTest() { } @Test - @DisplayName("특정 여행을 수정한다.") + @DisplayName("특정 여행을 삭제한다.") fun shouldDeleteTrip() { // when val resultActions = getResultActions(token, courseTrip.id) diff --git a/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.kt new file mode 100644 index 0000000..7a8d3fc --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.kt @@ -0,0 +1,820 @@ +package com.ject.studytrip.trip.presentation.controller + +import com.ject.studytrip.BaseIntegrationTest +import com.ject.studytrip.auth.domain.error.AuthErrorCode +import com.ject.studytrip.auth.fixture.TokenFixture +import com.ject.studytrip.auth.helper.TokenTestHelper +import com.ject.studytrip.global.exception.error.CommonErrorCode +import com.ject.studytrip.image.domain.error.ImageErrorCode +import com.ject.studytrip.image.infra.s3.provider.S3ImageStorageProvider +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.domain.model.MemberRole +import com.ject.studytrip.member.helper.MemberTestHelper +import com.ject.studytrip.mission.helper.DailyMissionTestHelper +import com.ject.studytrip.mission.helper.MissionTestHelper +import com.ject.studytrip.stamp.helper.StampTestHelper +import com.ject.studytrip.studylog.domain.error.StudyLogErrorCode +import com.ject.studytrip.studylog.domain.model.StudyLog +import com.ject.studytrip.studylog.helper.StudyLogDailyMissionTestHelper +import com.ject.studytrip.studylog.helper.StudyLogTestHelper +import com.ject.studytrip.trip.domain.error.TripErrorCode +import com.ject.studytrip.trip.domain.error.TripReportErrorCode +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.model.TripReport +import com.ject.studytrip.trip.fixture.ConfirmTripReportImageRequestFixture +import com.ject.studytrip.trip.fixture.CreateTripReportRequestFixture +import com.ject.studytrip.trip.fixture.PresignTripReportImageRequestFixture +import com.ject.studytrip.trip.helper.DailyGoalTestHelper +import com.ject.studytrip.trip.helper.TripReportTestHelper +import com.ject.studytrip.trip.helper.TripTestHelper +import com.ject.studytrip.trip.presentation.dto.request.ConfirmTripReportImageRequest +import com.ject.studytrip.trip.presentation.dto.request.CreateTripReportRequest +import com.ject.studytrip.trip.presentation.dto.request.PresignTripReportImageRequest +import org.hamcrest.Matchers +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.given +import org.mockito.kotlin.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@DisplayName("TripReportController 통합 테스트") +class TripReportControllerIntegrationTest : BaseIntegrationTest() { + @Autowired private lateinit var memberTestHelper: MemberTestHelper + + @Autowired private lateinit var tokenTestHelper: TokenTestHelper + + @Autowired private lateinit var tripTestHelper: TripTestHelper + + @Autowired private lateinit var stampTestHelper: StampTestHelper + + @Autowired private lateinit var missionTestHelper: MissionTestHelper + + @Autowired private lateinit var dailyGoalTestHelper: DailyGoalTestHelper + + @Autowired private lateinit var studyLogTestHelper: StudyLogTestHelper + + @Autowired private lateinit var dailyMissionTestHelper: DailyMissionTestHelper + + @Autowired private lateinit var studyLogDailyMissionTestHelper: StudyLogDailyMissionTestHelper + + @Autowired private lateinit var tripReportTestHelper: TripReportTestHelper + + @MockitoBean lateinit var s3ImageStorageProvider: S3ImageStorageProvider + + private lateinit var member: Member + private lateinit var token: String + private lateinit var completedTrip: Trip + private lateinit var dailyGoal: DailyGoal + private lateinit var studyLog1: StudyLog + private lateinit var studyLog2: StudyLog + private lateinit var tripReport: TripReport + + private lateinit var newMember: Member + + @BeforeEach + fun setUp() { + member = memberTestHelper.saveMember() + token = tokenTestHelper.createAccessToken(member.id.toString(), MemberRole.ROLE_USER.name) + completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + dailyGoal = dailyGoalTestHelper.saveDailyGoal(completedTrip) + val stamp = stampTestHelper.saveStamp(completedTrip, 1) + val mission = missionTestHelper.saveMission(stamp) + val dailyMission = dailyMissionTestHelper.saveDailyMission(mission, dailyGoal) + studyLog1 = studyLogTestHelper.saveStudyLog(member, dailyGoal) + studyLog2 = studyLogTestHelper.saveStudyLog(member, dailyGoal) + studyLogDailyMissionTestHelper.saveStudyLogDailyMissions(studyLog2, dailyMission) + tripReport = tripReportTestHelper.saveTripReport(member) + + newMember = memberTestHelper.saveMember("test@gmail.com", "test") + } + + companion object { + private const val BASE_TRIP_REPORT_URL = "/api/trip-reports" + private const val DEFAULT_PAGE: String = "0" + private const val DEFAULT_SIZE: String = "5" + } + + @Nested + @DisplayName("여행 리포트 생성 API") + inner class CreateTripReport { + private val fixture = CreateTripReportRequestFixture() + + private fun getResultActions( + token: String, + request: CreateTripReportRequest, + ): ResultActions = + mockMvc.perform( + post(BASE_TRIP_REPORT_URL) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("학습 로그 ID 목록 중 존재하지 않는 ID가 존재하면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenAnyStudyLogIdDoesNotExist() { + // given + val request = fixture.withStudyLogIds(listOf(studyLog1.id, -1L)).build() + + // when + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StudyLogErrorCode.STUDY_LOG_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(StudyLogErrorCode.STUDY_LOG_NOT_FOUND.message)) + } + + @Test + @DisplayName("학습 로그 ID 목록 중 이미 삭제된 ID가 존재하면 404 NotFound를 반환한다.") + fun shouldReturnBadRequestWhenAnyStudyLogAlreadyDeleted() { + // given + val deletedStudyLog = studyLogTestHelper.saveDeletedStudyLog(member, dailyGoal) + val request = fixture.withStudyLogIds(listOf(deletedStudyLog.id, studyLog2.id)).build() + + // when + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StudyLogErrorCode.STUDY_LOG_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StudyLogErrorCode.STUDY_LOG_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 여행 리포트를 생성하고 반환한다.") + fun shouldCreateTripReportWhenRequestIsValid() { + // given + val request = fixture.withStudyLogIds(listOf(studyLog1.id, studyLog2.id)).build() + + // when + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isCreated) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.CREATED.value())) + .andExpect(jsonPath("$.data.tripReportId").isNumber) + } + } + + @Nested + @DisplayName("여행 리포트 삭제 API") + inner class DeleteTripReport { + private fun getResultActions( + token: String, + tripReportId: Any, + ): ResultActions = + mockMvc.perform( + delete("$BASE_TRIP_REPORT_URL/{tripReportId}", tripReportId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", tripReport.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 리포트 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripReportIdTypeMismatch() { + // given + val tripReportId = "abc" + + // when + val resultActions = getResultActions(token, tripReportId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행 리포트가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripReportDoesNotExist() { + // given + val tripReportId = -1L + + // when + val resultActions = getResultActions(token, tripReportId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행 리포트의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripReportOwner() { + // given + val newTripReport = tripReportTestHelper.saveTripReport(newMember) + + // when + val resultActions = getResultActions(token, newTripReport.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.message)) + } + + @Test + @DisplayName("여행 리포트가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripReportAlreadyDeleted() { + // given + val deletedTripReport = tripReportTestHelper.saveDeletedTripReport(member) + + // when + val resultActions = getResultActions(token, deletedTripReport.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("특정 여행 리포트를 삭제한다.") + fun shouldDeleteTripReport() { + // when + val resultActions = getResultActions(token, tripReport.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("여행 리포트 이미지 Presigned URL 발급 API") + inner class IssuePresignedUrl { + private val fixture = PresignTripReportImageRequestFixture() + + private fun getResultActions( + token: String, + tripReportId: Any, + request: PresignTripReportImageRequest, + ): ResultActions = + mockMvc.perform( + post("$BASE_TRIP_REPORT_URL/{tripReportId}/images/presigned", tripReportId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", tripReport.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("파일명이 비어있으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenFilenameIsEmpty() { + // given + val request = fixture.withOriginFilename("").build() + + // when + val resultActions = getResultActions(token, tripReport.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("유효하지 않은 확장자는 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenExtensionIsInvalid() { + // given + val request = fixture.withOriginFilename("test.pdf").build() + + // when + val resultActions = getResultActions(token, tripReport.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(ImageErrorCode.INVALID_IMAGE_EXTENSION.status.value())) + .andExpect(jsonPath("$.data.message").value(ImageErrorCode.INVALID_IMAGE_EXTENSION.message)) + } + + @Test + @DisplayName("파일명이 유효하면 Presigned URL을 발급한다.") + fun shouldIssuePresignedUrlWhenFilenameIsValid() { + // given + val request = fixture.build() + given(s3ImageStorageProvider.issuePresignedUrl(anyString())).willReturn("https://mocked-presigned-url.com") + + // when + val resultActions = getResultActions(token, tripReport.id, request) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.presignedUrl").isNotEmpty) + .andExpect(jsonPath("$.data.tmpKey").isNotEmpty) + .andExpect(jsonPath("$.data.tmpKey").value(Matchers.startsWith("tmp/trip-reports/"))) + .andExpect(jsonPath("$.data.tmpKey").value(Matchers.containsString(tripReport.id.toString()))) + + // S3Provider 호출 검증 + verify(s3ImageStorageProvider).issuePresignedUrl(anyString()) + } + } + + @Nested + @DisplayName("여행 리포트 이미지 확정 API") + inner class ConfirmImage { + private val fixture = ConfirmTripReportImageRequestFixture() + + private fun getResultActions( + token: String, + tripReportId: Any, + request: ConfirmTripReportImageRequest, + ): ResultActions = + mockMvc.perform( + post("$BASE_TRIP_REPORT_URL/{tripReportId}/images/confirm", tripReportId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", tripReport.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 리포트 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStudyLogIdTypeMismatch() { + // given + val tripReportId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripReportId, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("tmpKey가 비어있으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTmpKeyIsEmpty() { + // given + val request = fixture.withTmpKey("").build() + + // when + val resultActions = getResultActions(token, tripReport.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + } + + @Nested + @DisplayName("여행 회고 API") + inner class LoadTripRetrospect { + private fun getResultActions( + token: String, + tripId: Any, + page: String, + size: String, + ): ResultActions = + mockMvc.perform( + get("/api/trips/{tripId}/retrospect", tripId) + .param("page", page) + .param("size", size) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", completedTrip.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("Request Param 페이징 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPagingParameterTypeMismatch() { + // given + val page = "abc" + val size = "abc" + + // when + val resultActions = getResultActions(token, completedTrip.id, page, size) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("Request Param 페이징 데이터가 유효하지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPagingParameterIsInvalid() { + // given + val page = "-1" + val size = "-1" + + // when + val resultActions = getResultActions(token, completedTrip.id, page, size) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.CONSTRAINT_VIOLATION.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.CONSTRAINT_VIOLATION.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + + // when + val resultActions = getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // given + val newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, newTrip.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 아직 완료되지 않았다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripDoesNotCompleted() { + // given + val trip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, trip.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_COMPLETED.message)) + } + + @Test + @DisplayName("유효한 여행 ID가 들어오면 여행 회고 정보를 반환한다.") + fun shouldReturnTripRetrospectWhenTripIdIsValid() { + // when + val resultActions = getResultActions(token, completedTrip.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } + + @Nested + @DisplayName("여행 리포트 목록 조회 API") + inner class LoadTripReports { + private fun getResultActions(token: String): ResultActions = + mockMvc.perform( + get(BASE_TRIP_REPORT_URL) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("") + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("특정 여행 리포트 목록을 조회하고 반환한다.") + fun shouldReturnTripReports() { + // when + val resultActions = getResultActions(token) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } + + @Nested + @DisplayName("여행 리포트 상세 조회 API") + inner class LoadTripReport { + private fun getResultActions( + token: String, + tripReportId: Any, + page: String, + size: String, + ): ResultActions = + mockMvc.perform( + get("$BASE_TRIP_REPORT_URL/{tripReportId}", tripReportId) + .param("page", page) + .param("size", size) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", tripReport.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("Request Param 페이징 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPagingParameterTypeMismatch() { + // given + val page = "abc" + val size = "abc" + + // when + val resultActions = getResultActions(token, tripReport.id, page, size) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("Request Param 페이징 데이터가 유효하지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPagingParameterIsInvalid() { + // given + val page = "-1" + val size = "-1" + + // when + val resultActions = getResultActions(token, tripReport.id, page, size) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.CONSTRAINT_VIOLATION.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.CONSTRAINT_VIOLATION.message)) + } + + @Test + @DisplayName("PathVariable 여행 리포트 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripReportIdTypeMismatch() { + // given + val tripReportId = "abc" + + // when + val resultActions = getResultActions(token, tripReportId, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행 리포트가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripReportDoesNotExist() { + // given + val tripReportId = -1L + + // when + val resultActions = getResultActions(token, tripReportId, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripReportErrorCode.TRIP_REPORT_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행 리포트의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripReportOwner() { + // given + val newTripReport = tripReportTestHelper.saveTripReport(newMember) + + // when + val resultActions = getResultActions(token, newTripReport.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripReportErrorCode.NOT_TRIP_REPORT_OWNER.message)) + } + + @Test + @DisplayName("여행 리포트가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripReportAlreadyDeleted() { + // given + val deletedTripReport = tripReportTestHelper.saveDeletedTripReport(member) + + // when + val resultActions = getResultActions(token, deletedTripReport.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripReportErrorCode.TRIP_REPORT_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("특정 여행 리포트를 상세 조회하고 반환한다.") + fun shouldReturnTripReport() { + // when + val resultActions = getResultActions(token, tripReport.id, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } +}