diff --git a/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java b/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java index bc197f3..322df98 100644 --- a/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java +++ b/src/main/java/com/ject/studytrip/cleanup/application/facade/HardDeleteFacade.java @@ -67,6 +67,7 @@ public class HardDeleteFacade { private static final String TRIPS = "trips"; private static final String DAILY_GOALS_OWNED_BY_DELETED_TRIP = "dailyGoalsOwnedByDeletedTrip"; private static final String DAILY_GOALS = "dailyGoals"; + private static final String TRIP_REPORTS = "tripReports"; private static final String MEMBERS = "members"; @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -147,6 +148,9 @@ private void deleteTripReports(Map phases) { executor.run( TRIP_REPORTS_OWNED_BY_DELETED_MEMBER, tripReportCommandService::hardDeleteTripReportsOwnedByDeletedMember)); + phases.put( + TRIP_REPORTS, + executor.run(TRIP_REPORTS, tripReportCommandService::hardDeleteTripReports)); } private void deleteStudyLogs(Map phases) { 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 index 5163da7..eaae684 100644 --- a/src/main/java/com/ject/studytrip/trip/application/facade/TripReportFacade.java +++ b/src/main/java/com/ject/studytrip/trip/application/facade/TripReportFacade.java @@ -104,6 +104,15 @@ public TripReportInfo createTripReport(Long memberId, CreateTripReportRequest re return TripReportInfo.from(tripReport); } + @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) { 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 index 0c4fc7b..6ba8578 100644 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java +++ b/src/main/java/com/ject/studytrip/trip/application/service/TripReportCommandService.java @@ -35,6 +35,14 @@ public void updateImageUrl(TripReport tripReport, String imageUrl) { tripReport.updateImageUrl(imageUrl); } + public void deleteTripReport(TripReport tripReport) { + tripReport.updateDeletedAt(); + } + + public long hardDeleteTripReports() { + return tripReportQueryRepository.deleteAllByDeletedAtIsNotNull(); + } + public long hardDeleteTripReportsOwnedByDeletedMember() { return tripReportQueryRepository.deleteAllByDeletedMemberOwner(); } 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 index 9e9696f..c09e80e 100644 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripReportQueryService.java +++ b/src/main/java/com/ject/studytrip/trip/application/service/TripReportQueryService.java @@ -30,11 +30,13 @@ public TripReport getValidTripReport(Long memberId, Long tripReportId) { TripReportErrorCode.TRIP_REPORT_NOT_FOUND)); TripReportPolicy.validateOwner(memberId, tripReport); + TripReportPolicy.validateNotDeleted(tripReport); return tripReport; } public List getTripReportsByMemberId(Long memberId) { - return tripReportRepository.findAllByMemberIdOrderByCreatedAtDesc(memberId); + return tripReportRepository.findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc( + 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 index 4ecfc53..1da3920 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/error/TripReportErrorCode.java +++ b/src/main/java/com/ject/studytrip/trip/domain/error/TripReportErrorCode.java @@ -6,6 +6,9 @@ @RequiredArgsConstructor public enum TripReportErrorCode implements ErrorCode { + // 400 + TRIP_REPORT_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 여행 리포트입니다."), + // 403 NOT_TRIP_REPORT_OWNER(HttpStatus.FORBIDDEN, "요청한 여행 리포트 정보를 조회할 권한이 없습니다."), diff --git a/src/main/java/com/ject/studytrip/trip/domain/model/TripReport.java b/src/main/java/com/ject/studytrip/trip/domain/model/TripReport.java index b479e7e..db3f0d3 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/model/TripReport.java +++ b/src/main/java/com/ject/studytrip/trip/domain/model/TripReport.java @@ -5,6 +5,7 @@ import com.ject.studytrip.global.common.entity.BaseTimeEntity; import com.ject.studytrip.member.domain.model.Member; import jakarta.persistence.*; +import java.time.LocalDateTime; import lombok.*; @Entity @@ -72,4 +73,8 @@ public static TripReport of( public void updateImageUrl(String imageUrl) { if (hasText(imageUrl)) this.imageUrl = imageUrl; } + + public void updateDeletedAt() { + this.deletedAt = LocalDateTime.now(); + } } 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 index d9fdcb2..ac29c8a 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/policy/TripReportPolicy.java +++ b/src/main/java/com/ject/studytrip/trip/domain/policy/TripReportPolicy.java @@ -13,4 +13,9 @@ public static void validateOwner(Long memberId, TripReport tripReport) { 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 820d62b..1a7bb2d 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,5 +1,7 @@ package com.ject.studytrip.trip.domain.repository; public interface TripReportQueryRepository { + long deleteAllByDeletedAtIsNotNull(); + long deleteAllByDeletedMemberOwner(); } 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 index add4b81..4b9f05f 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportRepository.java +++ b/src/main/java/com/ject/studytrip/trip/domain/repository/TripReportRepository.java @@ -7,7 +7,7 @@ public interface TripReportRepository { Optional findById(Long tripReportId); - List findAllByMemberIdOrderByCreatedAtDesc(Long memberId); + List findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc(Long memberId); TripReport save(TripReport tripReport); } 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 index e1213f0..fcdf417 100644 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.java +++ b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportJpaRepository.java @@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface TripReportJpaRepository extends JpaRepository { - List findAllByMember_IdOrderByCreatedAtDesc(Long memberId); + 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 index 80aaac4..d3b08a6 100644 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripReportRepositoryAdapter.java @@ -18,8 +18,9 @@ public Optional findById(Long tripReportId) { } @Override - public List findAllByMemberIdOrderByCreatedAtDesc(Long memberId) { - return tripReportJpaRepository.findAllByMember_IdOrderByCreatedAtDesc(memberId); + public List findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc(Long memberId) { + return tripReportJpaRepository.findAllByMember_IdAndDeletedAtIsNullOrderByCreatedAtDesc( + memberId); } @Override 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 056ed12..cee985d 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 @@ -15,6 +15,11 @@ public class TripReportQueryRepositoryAdapter implements TripReportQueryReposito private final QTripReport tripReport = QTripReport.tripReport; private final QMember member = QMember.member; + @Override + public long deleteAllByDeletedAtIsNotNull() { + return queryFactory.delete(tripReport).where(tripReport.deletedAt.isNotNull()).execute(); + } + @Override public long deleteAllByDeletedMemberOwner() { 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 index 63a14f6..853ff12 100644 --- a/src/main/java/com/ject/studytrip/trip/presentation/controller/TripReportController.java +++ b/src/main/java/com/ject/studytrip/trip/presentation/controller/TripReportController.java @@ -92,6 +92,17 @@ public ResponseEntity createTripReport( 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 = 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 index 72969e9..338d6e9 100644 --- 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 @@ -29,6 +29,7 @@ private static TripReportSummary of(List tripReportInfos) { private record LoadTripReportInfoResponse( @Schema(description = "여행 리포트 ID") Long tripReportId, + @Schema(description = "여행 리포트 제목") String title, @Schema(description = "여행 시작일 (여행 회고)") String startDate, @Schema(description = "여행 종료일 (여행 회고)") String endDate, @Schema(description = "총 학습 시간") long totalFocusHours, @@ -36,6 +37,7 @@ private record LoadTripReportInfoResponse( private static LoadTripReportInfoResponse of(TripReportInfo tripReportInfo) { return new LoadTripReportInfoResponse( tripReportInfo.tripReportId(), + tripReportInfo.title(), tripReportInfo.startDate(), tripReportInfo.endDate(), tripReportInfo.totalFocusHours(), 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 index 371d8e1..8e69201 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/TripReportCommandServiceTest.java @@ -75,6 +75,52 @@ void shouldUpdateImageUrlWhenTripReportIsValid() { } } + @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(tripReportQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); + + // when + long result = tripReportCommandService.hardDeleteTripReports(); + + // then + assertThat(result).isEqualTo(0L); + } + + @Test + @DisplayName("삭제된 여행 리포트가 하나라도 있으면 해당 개수를 반환한다.") + void shouldReturnCountWhenDeletedTripReportExist() { + // given + given(tripReportQueryRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); + + // when + long result = tripReportCommandService.hardDeleteTripReports(); + + // then + assertThat(result).isEqualTo(5L); + } + } + @Nested @DisplayName("hardDeleteTripReportsOwnedByDeletedMember 메서드는") class HardDeleteTripReportsOwnedByDeletedMember { 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 index a2398d3..480aac5 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/TripReportQueryServiceTest.java @@ -58,6 +58,23 @@ void shouldThrowExceptionWhenTripReportDoNotExist() { .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() { @@ -140,7 +157,10 @@ class GetTripReportsByMemberId { void shouldReturnEmptyListWhenTripReportDoNotExist() { // given Long memberId = member.getId(); - given(tripReportRepository.findAllByMemberIdOrderByCreatedAtDesc(memberId)) + given( + tripReportRepository + .findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc( + memberId)) .willReturn(List.of()); // when @@ -155,7 +175,10 @@ void shouldReturnEmptyListWhenTripReportDoNotExist() { void shouldReturnTripReportsWhenTripReportExists() { // given Long memberId = member.getId(); - given(tripReportRepository.findAllByMemberIdOrderByCreatedAtDesc(memberId)) + given( + tripReportRepository + .findAllByMemberIdAndDeletedAtIsNullOrderByCreatedAtDesc( + memberId)) .willReturn(List.of(tripReport1, tripReport2)); // when 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 index 6cdb77e..72abf59 100644 --- a/src/test/java/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.java +++ b/src/test/java/com/ject/studytrip/trip/presentation/controller/TripReportControllerIntegrationTest.java @@ -4,8 +4,7 @@ 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.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +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; @@ -138,7 +137,7 @@ void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { @Test @DisplayName("Request Param 페이징 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다") void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exception { - // Given + // given String page = "test"; String size = "test"; @@ -160,7 +159,7 @@ void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exceptio @Test @DisplayName("Request Param 페이징 데이터가 유효하지 않으면 400 Bad Request를 반환한다") void shouldReturnBadRequestWhenWhenPagingParameterIsInvalid() throws Exception { - // Given + // given String page = "-1"; String size = "100"; @@ -278,7 +277,7 @@ void shouldReturnNotFoundWhenTripIdIsInvalid() throws Exception { ResultActions resultActions = getResultActions(accessToken, invalidTripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - // when & then + // then resultActions .andExpect(status().isNotFound()) .andExpect(jsonPath("$.success").value(false)) @@ -386,7 +385,7 @@ void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { @Test @DisplayName("Request Param 페이징 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다") void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exception { - // Given + // given String page = "test"; String size = "test"; @@ -408,7 +407,7 @@ void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exceptio @Test @DisplayName("Request Param 페이징 데이터가 유효하지 않으면 400 Bad Request를 반환한다") void shouldReturnBadRequestWhenWhenPagingParameterIsInvalid() throws Exception { - // Given + // given String page = "-1"; String size = "100"; @@ -483,7 +482,7 @@ void shouldReturnNotFoundWhenTripReportIdIsInvalid() throws Exception { ResultActions resultActions = getResultActions(accessToken, invalidId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE); - // when & then + // then resultActions .andExpect(status().isNotFound()) .andExpect(jsonPath("$.success").value(false)) @@ -498,6 +497,34 @@ void shouldReturnNotFoundWhenTripReportIdIsInvalid() throws Exception { .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 { @@ -636,6 +663,140 @@ void shouldCreateTripReportWhenRequestIsValid() throws Exception { } } + @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 {