diff --git a/backend/src/main/java/com/staccato/travel/controller/TravelController.java b/backend/src/main/java/com/staccato/travel/controller/TravelController.java index 7023b0b78..c0832e266 100644 --- a/backend/src/main/java/com/staccato/travel/controller/TravelController.java +++ b/backend/src/main/java/com/staccato/travel/controller/TravelController.java @@ -7,6 +7,7 @@ import org.springframework.http.ResponseEntity; 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; @@ -59,4 +60,12 @@ public ResponseEntity updateTravel( travelService.updateTravel(travelRequest, travelId); return ResponseEntity.ok().build(); } + + @DeleteMapping("/{travelId}") + public ResponseEntity deleteTravel( + @PathVariable @Min(value = 1L, message = "여행 식별자는 양수로 이루어져야 합니다.") Long travelId, + @MemberId Long memberId) { + travelService.deleteTravel(travelId); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/staccato/travel/service/TravelService.java b/backend/src/main/java/com/staccato/travel/service/TravelService.java index 0bd620304..735d0ade7 100644 --- a/backend/src/main/java/com/staccato/travel/service/TravelService.java +++ b/backend/src/main/java/com/staccato/travel/service/TravelService.java @@ -19,6 +19,7 @@ import com.staccato.visit.domain.Visit; import com.staccato.visit.domain.VisitImage; import com.staccato.visit.repository.VisitImageRepository; +import com.staccato.visit.repository.VisitLogRepository; import com.staccato.visit.repository.VisitRepository; import com.staccato.visit.service.dto.response.VisitResponse; @@ -31,6 +32,7 @@ public class TravelService { private final TravelRepository travelRepository; private final TravelMemberRepository travelMemberRepository; private final VisitRepository visitRepository; + private final VisitLogRepository visitLogRepository; private final MemberRepository memberRepository; private final VisitImageRepository visitImageRepository; @@ -55,14 +57,6 @@ private Member getMemberById(long memberId) { .orElseThrow(() -> new IllegalArgumentException("Invalid Operation")); } - @Transactional - public void updateTravel(TravelRequest travelRequest, Long travelId) { - Travel updatedTravel = travelRequest.toTravel(); - Travel originTravel = getTravelById(travelId); - List visits = visitRepository.findAllByTravelIdAndIsDeletedIsFalse(travelId); - originTravel.update(updatedTravel, visits); - } - public TravelResponses readAllTravels(long memberId, Integer year) { return Optional.ofNullable(year) .map(y -> readAllByYear(memberId, y)) @@ -86,6 +80,34 @@ private TravelResponses getTravelResponses(List travelMembers) { return TravelResponses.from(travels); } + @Transactional + public void updateTravel(TravelRequest travelRequest, Long travelId) { + Travel updatedTravel = travelRequest.toTravel(); + Travel originTravel = getTravelById(travelId); + List visits = visitRepository.findAllByTravelIdAndIsDeletedIsFalse(travelId); + originTravel.update(updatedTravel, visits); + } + + @Transactional + public void deleteTravel(Long travelId) { + validateVisitExistsByTravelId(travelId); + visitRepository.findAllByTravelIdAndIsDeletedIsFalse(travelId) + .forEach(visit -> deleteVisits(visit.getId())); + travelRepository.deleteById(travelId); + } + + private void validateVisitExistsByTravelId(Long travelId) { + if (visitRepository.existsByTravelId(travelId)) { + throw new StaccatoException("해당 여행 상세에 방문 기록이 남아있어 삭제할 수 없습니다."); + } + } + + private void deleteVisits(long visitId) { + visitLogRepository.deleteByVisitId(visitId); + visitImageRepository.deleteByVisitId(visitId); + visitRepository.deleteById(visitId); + } + public TravelDetailResponse readTravelById(long travelId) { Travel travel = getTravelById(travelId); List visitResponses = getVisitResponses(visitRepository.findAllByTravelIdAndIsDeletedIsFalse(travelId)); diff --git a/backend/src/main/java/com/staccato/visit/repository/VisitRepository.java b/backend/src/main/java/com/staccato/visit/repository/VisitRepository.java index 0b5b8a58d..4497fac32 100644 --- a/backend/src/main/java/com/staccato/visit/repository/VisitRepository.java +++ b/backend/src/main/java/com/staccato/visit/repository/VisitRepository.java @@ -3,10 +3,19 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import com.staccato.visit.domain.Visit; public interface VisitRepository extends JpaRepository { List findAllByTravelIdAndIsDeletedIsFalse(@Param("travelId") long travelId); + + @Modifying(clearAutomatically = true) + @Query("UPDATE Visit vi SET vi.isDeleted = true WHERE vi.travel.id = :travelId") + void deleteByTravelId(@Param("travelId") Long travelId); + + @Query("SELECT CASE WHEN COUNT(v) > 0 THEN true ELSE false END FROM Visit v WHERE v.travel.id = :travelId") + boolean existsByTravelId(@Param("travelId") Long travelId); } diff --git a/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java b/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java index 54dc59312..410bc91e9 100644 --- a/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java +++ b/backend/src/test/java/com/staccato/travel/controller/TravelIntegrationTest.java @@ -254,4 +254,21 @@ private TravelRequest createTravelRequest(int year) { LocalDate.of(year, 7, 1), LocalDate.of(year, 7, 10)); } + + @DisplayName("사용자가 여행 상세 삭제를 요청하면, 여행 상세를 삭제한다.") + @Test + void deleteTravel() { + // given + Long travelId = 1L; + TravelRequest travelRequest = new TravelRequest("https://example.com/travels/geumohrm.jpg", "2023 여름 휴가", "친구들과 함께한 여름 휴가 여행", LocalDate.of(2023, 7, 1), LocalDate.of(2023, 7, 10)); + createTravel(travelRequest); + + // when & then + RestAssured.given().pathParam("travelId", travelId).log().all() + .header(HttpHeaders.AUTHORIZATION, USER_AUTHORIZATION) + .contentType(ContentType.JSON) + .when().delete("/travels/{travelId}") + .then().log().all() + .assertThat().statusCode(HttpStatus.OK.value()); + } } diff --git a/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java b/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java index ffa618445..3d48cbc65 100644 --- a/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java +++ b/backend/src/test/java/com/staccato/travel/service/TravelServiceTest.java @@ -133,15 +133,8 @@ private Visit saveVisit(Pin pin, long otherId) { @Test void updateTravel() { // given - Long travelId = 1L; - Travel originTravel = new Travel( - "https://example.com/travels/geumohrm.jpg", - "2023 여름 휴가", - "친구들과 함께한 여름 휴가 여행", - LocalDate.of(2023, 7, 1), - LocalDate.of(2023, 7, 10) - ); - travelRepository.save(originTravel); + Travel travel = createAndSaveTravel(2023); + Long travelId = travel.getId(); TravelRequest updatedTravel = new TravelRequest( "https://example.com/travels/geumohrm.jpg", "2023 신나는 여름 휴가", @@ -152,29 +145,34 @@ void updateTravel() { // when travelService.updateTravel(updatedTravel, travelId); - Travel travel = travelRepository.findById(travelId).get(); + Travel foundedTravel = travelRepository.findById(travelId).get(); // then assertAll( - () -> assertThat(travel.getId()).isEqualTo(travelId), - () -> assertThat(travel.getTitle()).isEqualTo(updatedTravel.travelTitle()), - () -> assertThat(travel.getDescription()).isEqualTo(updatedTravel.description()), - () -> assertThat(travel.getStartAt()).isEqualTo(updatedTravel.startAt()), - () -> assertThat(travel.getEndAt()).isEqualTo(updatedTravel.endAt()) + () -> assertThat(foundedTravel.getId()).isEqualTo(travelId), + () -> assertThat(foundedTravel.getTitle()).isEqualTo(updatedTravel.travelTitle()), + () -> assertThat(foundedTravel.getDescription()).isEqualTo(updatedTravel.description()), + () -> assertThat(foundedTravel.getStartAt()).isEqualTo(updatedTravel.startAt()), + () -> assertThat(foundedTravel.getEndAt()).isEqualTo(updatedTravel.endAt()) + ); + } + + private Travel createAndSaveTravel(int year) { + Travel travel = new Travel( + "https://example.com/travels/geumohrm.jpg", + year + " 여름 휴가", + "친구들과 함께한 여름 휴가 여행", + LocalDate.of(year, 7, 1), + LocalDate.of(year, 7, 10) ); + return travelRepository.save(travel); } @DisplayName("존재하지 않는 여행 상세를 수정하려 할 경우 예외가 발생한다.") @Test void failUpdateTravel() { // given - TravelRequest travelRequest = new TravelRequest( - "https://example.com/travels/geumohrm.jpg", - "2023 여름 휴가", - "친구들과 함께한 여름 휴가 여행", - LocalDate.of(2023, 7, 1), - LocalDate.of(2023, 7, 10) - ); + TravelRequest travelRequest = createTravelRequest(2023); // when & then assertThatThrownBy(() -> travelService.updateTravel(travelRequest, 1L)) @@ -182,6 +180,34 @@ void failUpdateTravel() { .hasMessage("요청하신 여행을 찾을 수 없어요."); } + @DisplayName("여행 식별값을 통해 여행 상세를 삭제한다.") + @Test + void deleteTravel() { + // given + Travel travel = createAndSaveTravel(2023); + + // when + travelService.deleteTravel(travel.getId()); + + // then + Travel foundTravel = travelRepository.findById(travel.getId()).get(); + assertThat(foundTravel.getIsDeleted()).isTrue(); + } + + @DisplayName("방문기록이 존재하는 여행 상세에 삭제를 시도할 경우 예외가 발생한다.") + @Test + void failDeleteTravel() { + // given + Travel travel = createAndSaveTravel(2023); + Pin pin = pinRepository.save(Pin.builder().place("Sample Place").address("Sample Address").build()); + visitRepository.save(Visit.builder().visitedAt(LocalDate.now()).pin(pin).travel(travel).build()); + + // when & then + assertThatThrownBy(() -> travelService.deleteTravel(travel.getId())) + .isInstanceOf(StaccatoException.class) + .hasMessage("해당 여행 상세에 방문 기록이 남아있어 삭제할 수 없습니다."); + } + @DisplayName("존재하지 않는 여행 상세를 조회하려고 할 경우 예외가 발생한다.") @Test void failReadTravel() {