From 1b718e0874cbf68c22735f4918255098eb30d375 Mon Sep 17 00:00:00 2001 From: songhyeonpk Date: Mon, 14 Jul 2025 16:37:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8A=A4=ED=83=AC=ED=94=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 특정 여행의 스탬프 생성 기능 구현 * feat: 스탬프 이름 및 마감일 수정 기능 구현 * feat: 스탬프 순서 변경 기능 구현 * feat: 스탬프 삭제 기능 구현 * feat: 여행별 스탬프 목록 조회 기능 구현 * feat: 스탬프 조회 및 해당 스탬프의 미션 목록 조회 기능 구현 * feat: 스탬프 등록, 삭제에 따라 여행의 총 스탬프 수를 업데이트하는 로직 추가 * feat: 특정 스탬프의 미션 목록 조회 로직 추가 * feat: 검증된 Trip 정보를 조회하는 로직 추가 * test: TripServiceTest에 총 스탬프 수 증가, 감소 테스트 추가 * test: StampTestHelper 클래스 추가 * test: UpdateStampRequestFixture 클래스 추가 * test: StampFixture.createStampWithId 메서드 추가 * test: StampControllerIntegrationTest - 통합 테스트 추가 * test: StampServiceTest - 단위 테스트 추가 --- .../mission/application/dto/MissionInfo.java | 24 + .../application/service/MissionService.java | 20 + .../domain/repository/MissionRepository.java | 8 + .../infra/jpa/MissionJpaRepository.java | 9 + .../infra/jpa/MissionRepositoryAdapter.java | 18 + .../dto/response/LoadMissionInfoResponse.java | 15 + .../stamp/application/dto/StampDetail.java | 10 + .../stamp/application/facade/StampFacade.java | 77 + .../application/service/StampService.java | 89 +- .../stamp/domain/error/StampErrorCode.java | 9 + .../studytrip/stamp/domain/model/Stamp.java | 13 + .../stamp/domain/policy/StampPolicy.java | 19 + .../repository/StampQueryRepository.java | 8 + .../domain/repository/StampRepository.java | 9 +- .../stamp/infra/jpa/StampJpaRepository.java | 4 +- .../jpa/StampQueryRepositoryAdapter.java | 28 + .../infra/jpa/StampRepositoryAdapter.java | 20 +- .../controller/StampController.java | 110 ++ .../UpdateStampNameAndDeadlineRequest.java | 11 + .../dto/request/UpdateStampOrderRequest.java | 8 + .../dto/response/CreateStampResponse.java | 10 + .../dto/response/LoadStampDetailResponse.java | 11 +- .../dto/response/LoadStampInfoResponse.java | 20 + .../trip/application/service/TripService.java | 26 +- .../studytrip/trip/domain/model/Trip.java | 8 + .../trip/domain/policy/TripPolicy.java | 2 +- .../dto/response/LoadTripDetailResponse.java | 6 +- .../application/service/StampServiceTest.java | 593 ++++++-- .../studytrip/stamp/fixture/StampFixture.java | 11 +- .../fixture/UpdateStampRequestFixture.java | 35 + .../stamp/helper/StampTestHelper.java | 25 + .../StampControllerIntegrationTest.java | 1303 +++++++++++++++++ .../application/service/TripServiceTest.java | 28 +- .../studytrip/trip/fixture/TripFixture.java | 9 +- 34 files changed, 2446 insertions(+), 150 deletions(-) create mode 100644 src/main/java/com/ject/studytrip/mission/application/dto/MissionInfo.java create mode 100644 src/main/java/com/ject/studytrip/mission/application/service/MissionService.java create mode 100644 src/main/java/com/ject/studytrip/mission/domain/repository/MissionRepository.java create mode 100644 src/main/java/com/ject/studytrip/mission/infra/jpa/MissionJpaRepository.java create mode 100644 src/main/java/com/ject/studytrip/mission/infra/jpa/MissionRepositoryAdapter.java create mode 100644 src/main/java/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.java create mode 100644 src/main/java/com/ject/studytrip/stamp/application/dto/StampDetail.java create mode 100644 src/main/java/com/ject/studytrip/stamp/application/facade/StampFacade.java create mode 100644 src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java create mode 100644 src/main/java/com/ject/studytrip/stamp/infra/jpa/StampQueryRepositoryAdapter.java create mode 100644 src/main/java/com/ject/studytrip/stamp/presentation/controller/StampController.java create mode 100644 src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampNameAndDeadlineRequest.java create mode 100644 src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.java create mode 100644 src/main/java/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.java create mode 100644 src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.java create mode 100644 src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.java create mode 100644 src/test/java/com/ject/studytrip/stamp/helper/StampTestHelper.java create mode 100644 src/test/java/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.java diff --git a/src/main/java/com/ject/studytrip/mission/application/dto/MissionInfo.java b/src/main/java/com/ject/studytrip/mission/application/dto/MissionInfo.java new file mode 100644 index 0000000..de0b2dc --- /dev/null +++ b/src/main/java/com/ject/studytrip/mission/application/dto/MissionInfo.java @@ -0,0 +1,24 @@ +package com.ject.studytrip.mission.application.dto; + +import com.ject.studytrip.global.util.DateUtil; +import com.ject.studytrip.mission.domain.model.Mission; + +public record MissionInfo( + Long missionId, + String missionName, + int missionOrder, + boolean completed, + String createdAt, + String updatedAt, + String deletedAt) { + public static MissionInfo from(Mission mission) { + return new MissionInfo( + mission.getId(), + mission.getName(), + mission.getMissionOrder(), + mission.isCompleted(), + DateUtil.formatDateTime(mission.getCreatedAt()), + DateUtil.formatDateTime(mission.getUpdatedAt()), + DateUtil.formatDateTime(mission.getDeletedAt())); + } +} diff --git a/src/main/java/com/ject/studytrip/mission/application/service/MissionService.java b/src/main/java/com/ject/studytrip/mission/application/service/MissionService.java new file mode 100644 index 0000000..9be7425 --- /dev/null +++ b/src/main/java/com/ject/studytrip/mission/application/service/MissionService.java @@ -0,0 +1,20 @@ +package com.ject.studytrip.mission.application.service; + +import com.ject.studytrip.mission.application.dto.MissionInfo; +import com.ject.studytrip.mission.domain.model.Mission; +import com.ject.studytrip.mission.domain.repository.MissionRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MissionService { + private final MissionRepository missionRepository; + + public List getMissionsByStamp(Long stampId) { + List missions = missionRepository.findAllByStampIdOrderByMissionOrder(stampId); + + return missions.stream().map(MissionInfo::from).toList(); + } +} diff --git a/src/main/java/com/ject/studytrip/mission/domain/repository/MissionRepository.java b/src/main/java/com/ject/studytrip/mission/domain/repository/MissionRepository.java new file mode 100644 index 0000000..0adf441 --- /dev/null +++ b/src/main/java/com/ject/studytrip/mission/domain/repository/MissionRepository.java @@ -0,0 +1,8 @@ +package com.ject.studytrip.mission.domain.repository; + +import com.ject.studytrip.mission.domain.model.Mission; +import java.util.List; + +public interface MissionRepository { + List findAllByStampIdOrderByMissionOrder(Long stampId); +} diff --git a/src/main/java/com/ject/studytrip/mission/infra/jpa/MissionJpaRepository.java b/src/main/java/com/ject/studytrip/mission/infra/jpa/MissionJpaRepository.java new file mode 100644 index 0000000..2475e3c --- /dev/null +++ b/src/main/java/com/ject/studytrip/mission/infra/jpa/MissionJpaRepository.java @@ -0,0 +1,9 @@ +package com.ject.studytrip.mission.infra.jpa; + +import com.ject.studytrip.mission.domain.model.Mission; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MissionJpaRepository extends JpaRepository { + List findAllByStampIdOrderByMissionOrder(Long stampId); +} diff --git a/src/main/java/com/ject/studytrip/mission/infra/jpa/MissionRepositoryAdapter.java b/src/main/java/com/ject/studytrip/mission/infra/jpa/MissionRepositoryAdapter.java new file mode 100644 index 0000000..4c9296c --- /dev/null +++ b/src/main/java/com/ject/studytrip/mission/infra/jpa/MissionRepositoryAdapter.java @@ -0,0 +1,18 @@ +package com.ject.studytrip.mission.infra.jpa; + +import com.ject.studytrip.mission.domain.model.Mission; +import com.ject.studytrip.mission.domain.repository.MissionRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MissionRepositoryAdapter implements MissionRepository { + private final MissionJpaRepository missionJpaRepository; + + @Override + public List findAllByStampIdOrderByMissionOrder(Long stampId) { + return missionJpaRepository.findAllByStampIdOrderByMissionOrder(stampId); + } +} diff --git a/src/main/java/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.java b/src/main/java/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.java new file mode 100644 index 0000000..6cf1285 --- /dev/null +++ b/src/main/java/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.java @@ -0,0 +1,15 @@ +package com.ject.studytrip.mission.presentation.dto.response; + +import com.ject.studytrip.mission.application.dto.MissionInfo; +import io.swagger.v3.oas.annotations.media.Schema; + +public record LoadMissionInfoResponse( + @Schema(description = "미션 ID") Long missionId, + @Schema(description = "미션 이름") String missionName, + @Schema(description = "미션 순서") int missionOrder, + @Schema(description = "미션 완료여부") boolean completed) { + public static LoadMissionInfoResponse of(MissionInfo info) { + return new LoadMissionInfoResponse( + info.missionId(), info.missionName(), info.missionOrder(), info.completed()); + } +} diff --git a/src/main/java/com/ject/studytrip/stamp/application/dto/StampDetail.java b/src/main/java/com/ject/studytrip/stamp/application/dto/StampDetail.java new file mode 100644 index 0000000..59b008c --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/application/dto/StampDetail.java @@ -0,0 +1,10 @@ +package com.ject.studytrip.stamp.application.dto; + +import com.ject.studytrip.mission.application.dto.MissionInfo; +import java.util.List; + +public record StampDetail(StampInfo stampInfo, List missionInfos) { + public static StampDetail from(StampInfo stampInfo, List missionInfos) { + return new StampDetail(stampInfo, missionInfos); + } +} diff --git a/src/main/java/com/ject/studytrip/stamp/application/facade/StampFacade.java b/src/main/java/com/ject/studytrip/stamp/application/facade/StampFacade.java new file mode 100644 index 0000000..c07a1fd --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/application/facade/StampFacade.java @@ -0,0 +1,77 @@ +package com.ject.studytrip.stamp.application.facade; + +import com.ject.studytrip.mission.application.dto.MissionInfo; +import com.ject.studytrip.mission.application.service.MissionService; +import com.ject.studytrip.stamp.application.dto.StampDetail; +import com.ject.studytrip.stamp.application.dto.StampInfo; +import com.ject.studytrip.stamp.application.service.StampService; +import com.ject.studytrip.stamp.domain.model.Stamp; +import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampNameAndDeadlineRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; +import com.ject.studytrip.trip.application.service.TripService; +import com.ject.studytrip.trip.domain.model.Trip; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class StampFacade { + private final TripService tripService; + private final StampService stampService; + private final MissionService missionService; + + @Transactional + public StampInfo createStamp(Long memberId, Long tripId, CreateStampRequest request) { + Trip trip = tripService.getValidTrip(memberId, tripId); + Stamp stamp = stampService.createStamp(trip, request); + + tripService.increaseTotalStamps(trip); + + return StampInfo.from(stamp); + } + + @Transactional + public void updateStampNameAndDeadline( + Long memberId, Long tripId, Long stampId, UpdateStampNameAndDeadlineRequest request) { + Trip trip = tripService.getValidTrip(memberId, tripId); + Stamp stamp = stampService.getValidStamp(trip.getId(), stampId); + + stampService.updateStampNameAndDeadline(trip, stamp, request); + } + + @Transactional + public void updateStampOrders(Long memberId, Long tripId, UpdateStampOrderRequest request) { + Trip trip = tripService.getValidTrip(memberId, tripId); + + stampService.updateStampsOrders(trip, request); + } + + @Transactional + public void deleteStamp(Long memberId, Long tripId, Long stampId) { + Trip trip = tripService.getValidTrip(memberId, tripId); + Stamp stamp = stampService.getValidStamp(trip.getId(), stampId); + + stampService.deleteStamp(trip.getId(), trip.getCategory(), stamp); + tripService.decreaseTotalStamps(trip); + + // TODO : 추후 삭제로직 업데이트 (연쇄 삭제 처리) + } + + public List getStampsByTrip(Long memberId, Long tripId) { + Trip trip = tripService.getValidTrip(memberId, tripId); + List stamps = stampService.getStampsByTripId(trip.getId()); + + return stamps.stream().map(StampInfo::from).toList(); + } + + public StampDetail getStamp(Long memberId, Long tripId, Long stampId) { + Trip trip = tripService.getValidTrip(memberId, tripId); + Stamp stamp = stampService.getValidStamp(trip.getId(), stampId); + List missionInfos = missionService.getMissionsByStamp(stamp.getId()); + + return StampDetail.from(StampInfo.from(stamp), missionInfos); + } +} diff --git a/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java b/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java index b310f43..13c3c94 100644 --- a/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java +++ b/src/main/java/com/ject/studytrip/stamp/application/service/StampService.java @@ -1,13 +1,22 @@ package com.ject.studytrip.stamp.application.service; +import com.ject.studytrip.global.exception.CustomException; +import com.ject.studytrip.stamp.domain.error.StampErrorCode; import com.ject.studytrip.stamp.domain.factory.StampFactory; import com.ject.studytrip.stamp.domain.model.Stamp; import com.ject.studytrip.stamp.domain.policy.StampPolicy; +import com.ject.studytrip.stamp.domain.repository.StampQueryRepository; import com.ject.studytrip.stamp.domain.repository.StampRepository; import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampNameAndDeadlineRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; import com.ject.studytrip.trip.domain.model.Trip; import com.ject.studytrip.trip.domain.model.TripCategory; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -16,6 +25,23 @@ public class StampService { private final StampRepository stampRepository; + private final StampQueryRepository stampQueryRepository; + + public Stamp createStamp(Trip trip, CreateStampRequest request) { + Stamp newStamp = + StampFactory.create(trip, request.name(), request.order(), request.deadline()); + + StampPolicy.validateStampDeadline(trip.getEndDate(), List.of(newStamp)); + + List existingStamps = + stampRepository.findAllByTripIdAndDeletedAtIsNull(trip.getId()); + List combinedStamps = new ArrayList<>(existingStamps); + combinedStamps.add(newStamp); + + StampPolicy.validateStampOrders(trip.getCategory(), combinedStamps); + + return stampRepository.save(newStamp); + } public void createStamps(Trip trip, List requests) { List stamps = @@ -35,6 +61,38 @@ public void createStamps(Trip trip, List requests) { stampRepository.saveAll(stamps); } + public void updateStampNameAndDeadline( + Trip trip, Stamp stamp, UpdateStampNameAndDeadlineRequest request) { + stamp.update(request.name(), request.deadline()); + + StampPolicy.validateStampDeadline(trip.getEndDate(), List.of(stamp)); + } + + public void updateStampsOrders(Trip trip, UpdateStampOrderRequest request) { + // 배치 조회 (ID 목록 기준으로 조회하지만 순서는 보장되지 않음) + List stamps = stampRepository.findAllByIdIn(request.orderedStampIds()); + + StampPolicy.validateUpdateStampOrders( + trip.getCategory(), request.orderedStampIds(), stamps); + stamps.forEach( + stamp -> { + StampPolicy.validateStampBelongsToTrip(trip.getId(), stamp); + StampPolicy.validateNotDeleted(stamp); + }); + + // 조회된 스탬프 ID 를 기준으로 매핑 + Map stampMap = + stamps.stream().collect(Collectors.toMap(Stamp::getId, Function.identity())); + + // 요청에서 전달된 ID 순서를 기준으로 스탬프 리스트 재정렬 + List orderedStamps = request.orderedStampIds().stream().map(stampMap::get).toList(); + + int newOrder = 1; + for (Stamp stamp : orderedStamps) { + stamp.updateStampOrder(newOrder++); + } + } + public void updateStampsOrderByTripCategoryChange(Long tripId, TripCategory newCategory) { List stamps = stampRepository.findAllByTripIdOrderByDeadlineAsc(tripId); @@ -49,7 +107,36 @@ public void updateStampsOrderByTripCategoryChange(Long tripId, TripCategory newC } } + public void deleteStamp(Long tripId, TripCategory tripCategory, Stamp stamp) { + stamp.updateDeletedAt(); + + if (tripCategory == TripCategory.COURSE) { + shiftStampOrdersAfterDeleted(tripId, stamp.getStampOrder()); + } + } + public List getStampsByTripId(Long tripId) { - return stampRepository.findAllByTripId(tripId); + return stampRepository.findAllByTripIdAndDeletedAtIsNull(tripId); + } + + public Stamp getValidStamp(Long tripId, Long stampId) { + Stamp stamp = + stampRepository + .findById(stampId) + .orElseThrow(() -> new CustomException(StampErrorCode.STAMP_NOT_FOUND)); + + StampPolicy.validateStampBelongsToTrip(tripId, stamp); + StampPolicy.validateNotDeleted(stamp); + + return stamp; + } + + private void shiftStampOrdersAfterDeleted(Long tripId, int deletedStampOrder) { + List affectedStamps = + stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedStampOrder); + + for (Stamp stamp : affectedStamps) { + stamp.updateStampOrder(stamp.getStampOrder() - 1); + } } } diff --git a/src/main/java/com/ject/studytrip/stamp/domain/error/StampErrorCode.java b/src/main/java/com/ject/studytrip/stamp/domain/error/StampErrorCode.java index da152da..5cfd051 100644 --- a/src/main/java/com/ject/studytrip/stamp/domain/error/StampErrorCode.java +++ b/src/main/java/com/ject/studytrip/stamp/domain/error/StampErrorCode.java @@ -14,6 +14,15 @@ public enum StampErrorCode implements ErrorCode { INVALID_STAMP_ORDER_RANGE_FOR_COURSE_TRIP( HttpStatus.BAD_REQUEST, "코스형 여행의 스탬프 순서의 범위는 최소 1 이상 또는 최대 총 스탬프 개수여야 합니다."), DUPLICATE_STAMP_ORDER_FOR_COURSE_TRIP(HttpStatus.BAD_REQUEST, "코스형 여행의 스탬프 순서에 중복된 값이 존재합니다."), + STAMP_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 스탬프입니다."), + CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP(HttpStatus.BAD_REQUEST, "탐험형 여행의 스탬프 순서는 변경할 수 없습니다."), + INVALID_STAMP_ID_IN_REQUEST(HttpStatus.BAD_REQUEST, "존재하지 않는 스탬프 ID가 포함되어 있습니다. "), + + // 403 + STAMP_NOT_BELONG_TO_TRIP(HttpStatus.FORBIDDEN, "해당 스탬프는 요청한 여행에 속하지 않습니다."), + + // 404 + STAMP_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 스탬프가 존재하지 않습니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/ject/studytrip/stamp/domain/model/Stamp.java b/src/main/java/com/ject/studytrip/stamp/domain/model/Stamp.java index baf74cd..be747f3 100644 --- a/src/main/java/com/ject/studytrip/stamp/domain/model/Stamp.java +++ b/src/main/java/com/ject/studytrip/stamp/domain/model/Stamp.java @@ -1,9 +1,13 @@ package com.ject.studytrip.stamp.domain.model; +import static org.springframework.util.StringUtils.hasText; + import com.ject.studytrip.global.common.entity.BaseTimeEntity; import com.ject.studytrip.trip.domain.model.Trip; import jakarta.persistence.*; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; import lombok.*; @Entity @@ -41,7 +45,16 @@ public static Stamp of(Trip trip, String name, int stampOrder, LocalDate deadlin .build(); } + public void update(String name, LocalDate deadline) { + if (hasText(name)) this.name = name; + if (Objects.nonNull(deadline)) this.deadline = deadline; + } + public void updateStampOrder(int newOrder) { this.stampOrder = newOrder; } + + public void updateDeletedAt() { + this.deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/ject/studytrip/stamp/domain/policy/StampPolicy.java b/src/main/java/com/ject/studytrip/stamp/domain/policy/StampPolicy.java index ec98a47..ee02dab 100644 --- a/src/main/java/com/ject/studytrip/stamp/domain/policy/StampPolicy.java +++ b/src/main/java/com/ject/studytrip/stamp/domain/policy/StampPolicy.java @@ -13,6 +13,16 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class StampPolicy { + public static void validateStampBelongsToTrip(Long tripId, Stamp stamp) { + if (!stamp.getTrip().getId().equals(tripId)) + throw new CustomException(StampErrorCode.STAMP_NOT_BELONG_TO_TRIP); + } + + public static void validateNotDeleted(Stamp stamp) { + if (stamp.getDeletedAt() != null) + throw new CustomException(StampErrorCode.STAMP_ALREADY_DELETED); + } + public static void validateStampDeadline(LocalDate tripEndDate, List stamps) { if (tripEndDate == null || stamps.isEmpty()) return; @@ -51,4 +61,13 @@ public static void validateStampOrders(TripCategory tripCategory, List st } } } + + public static void validateUpdateStampOrders( + TripCategory tripCategory, List orderedStampIds, List savedStamps) { + if (tripCategory == TripCategory.EXPLORE && !orderedStampIds.isEmpty()) + throw new CustomException(StampErrorCode.CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP); + + if (orderedStampIds.size() != savedStamps.size()) + throw new CustomException(StampErrorCode.INVALID_STAMP_ID_IN_REQUEST); + } } diff --git a/src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java b/src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java new file mode 100644 index 0000000..ff7e86b --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/domain/repository/StampQueryRepository.java @@ -0,0 +1,8 @@ +package com.ject.studytrip.stamp.domain.repository; + +import com.ject.studytrip.stamp.domain.model.Stamp; +import java.util.List; + +public interface StampQueryRepository { + List findStampsToShiftAfterOrder(Long tripId, int deletedOrder); +} diff --git a/src/main/java/com/ject/studytrip/stamp/domain/repository/StampRepository.java b/src/main/java/com/ject/studytrip/stamp/domain/repository/StampRepository.java index f24899e..91cb0c8 100644 --- a/src/main/java/com/ject/studytrip/stamp/domain/repository/StampRepository.java +++ b/src/main/java/com/ject/studytrip/stamp/domain/repository/StampRepository.java @@ -2,11 +2,18 @@ import com.ject.studytrip.stamp.domain.model.Stamp; import java.util.List; +import java.util.Optional; public interface StampRepository { + Stamp save(Stamp stamp); + List saveAll(List stamps); - List findAllByTripId(Long tripId); + Optional findById(Long stampId); + + List findAllByIdIn(List ids); + + List findAllByTripIdAndDeletedAtIsNull(Long tripId); List findAllByTripIdOrderByDeadlineAsc(Long tripId); } diff --git a/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.java b/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.java index ee437ec..baedf96 100644 --- a/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.java +++ b/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.java @@ -5,7 +5,9 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface StampJpaRepository extends JpaRepository { - List findAllByTripId(Long tripId); + List findAllByIdIn(List ids); + + List findAllByTripIdAndDeletedAtIsNull(Long tripId); List findAllByTripIdOrderByDeadlineAsc(Long tripId); } diff --git a/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampQueryRepositoryAdapter.java b/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampQueryRepositoryAdapter.java new file mode 100644 index 0000000..472e055 --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampQueryRepositoryAdapter.java @@ -0,0 +1,28 @@ +package com.ject.studytrip.stamp.infra.jpa; + +import com.ject.studytrip.stamp.domain.model.QStamp; +import com.ject.studytrip.stamp.domain.model.Stamp; +import com.ject.studytrip.stamp.domain.repository.StampQueryRepository; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class StampQueryRepositoryAdapter implements StampQueryRepository { + private final JPAQueryFactory queryFactory; + private final QStamp stamp = QStamp.stamp; + + @Override + public List findStampsToShiftAfterOrder(Long tripId, int deletedOrder) { + return queryFactory + .selectFrom(stamp) + .where( + stamp.trip.id.eq(tripId), + stamp.stampOrder.gt(deletedOrder), + stamp.deletedAt.isNull()) + .orderBy(stamp.stampOrder.asc()) + .fetch(); + } +} diff --git a/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.java b/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.java index fb0c11f..a1937ba 100644 --- a/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.java +++ b/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.java @@ -3,6 +3,7 @@ import com.ject.studytrip.stamp.domain.model.Stamp; import com.ject.studytrip.stamp.domain.repository.StampRepository; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -11,14 +12,29 @@ public class StampRepositoryAdapter implements StampRepository { private final StampJpaRepository stampJpaRepository; + @Override + public Stamp save(Stamp stamp) { + return stampJpaRepository.save(stamp); + } + @Override public List saveAll(List stamps) { return stampJpaRepository.saveAll(stamps); } @Override - public List findAllByTripId(Long tripId) { - return stampJpaRepository.findAllByTripId(tripId); + public Optional findById(Long stampId) { + return stampJpaRepository.findById(stampId); + } + + @Override + public List findAllByIdIn(List ids) { + return stampJpaRepository.findAllByIdIn(ids); + } + + @Override + public List findAllByTripIdAndDeletedAtIsNull(Long tripId) { + return stampJpaRepository.findAllByTripIdAndDeletedAtIsNull(tripId); } @Override diff --git a/src/main/java/com/ject/studytrip/stamp/presentation/controller/StampController.java b/src/main/java/com/ject/studytrip/stamp/presentation/controller/StampController.java new file mode 100644 index 0000000..d31b5e5 --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/presentation/controller/StampController.java @@ -0,0 +1,110 @@ +package com.ject.studytrip.stamp.presentation.controller; + +import com.ject.studytrip.global.common.response.StandardResponse; +import com.ject.studytrip.stamp.application.dto.StampDetail; +import com.ject.studytrip.stamp.application.dto.StampInfo; +import com.ject.studytrip.stamp.application.facade.StampFacade; +import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampNameAndDeadlineRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; +import com.ject.studytrip.stamp.presentation.dto.response.CreateStampResponse; +import com.ject.studytrip.stamp.presentation.dto.response.LoadStampDetailResponse; +import com.ject.studytrip.stamp.presentation.dto.response.LoadStampInfoResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +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.*; + +@Tag(name = "Stamp", description = "스탬프 API") +@RestController +@RequiredArgsConstructor +@Validated +public class StampController { + private final StampFacade stampFacade; + + @Operation(summary = "스탬프 등록", description = "특정 여행에 새로운 스탬프를 등록합니다.") + @PostMapping("/api/trips/{tripId}/stamps") + public ResponseEntity createStamp( + @AuthenticationPrincipal String memberId, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, + @RequestBody @Valid CreateStampRequest request) { + StampInfo result = stampFacade.createStamp(Long.valueOf(memberId), tripId, request); + + return ResponseEntity.status(HttpStatus.CREATED) + .body( + StandardResponse.success( + HttpStatus.CREATED.value(), CreateStampResponse.of(result))); + } + + @Operation(summary = "스탬프 이름, 마감일 수정", description = "특정 스탬프의 이름과 마감일을 수정합니다.") + @PatchMapping("/api/trips/{tripId}/stamps/{stampId}") + public ResponseEntity updateStamp( + @AuthenticationPrincipal String memberId, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, + @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") Long stampId, + @RequestBody @Valid UpdateStampNameAndDeadlineRequest request) { + stampFacade.updateStampNameAndDeadline(Long.valueOf(memberId), tripId, stampId, request); + + return ResponseEntity.status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)); + } + + @Operation(summary = "스탬프 순서 변경", description = "스탬프 순서를 변경합니다. 스탬프 ID 목록을 최종 순서대로 요청합니다.") + @PutMapping("/api/trips/{tripId}/stamps/orders") + public ResponseEntity updateStampOrders( + @AuthenticationPrincipal String memberId, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, + @RequestBody @Valid UpdateStampOrderRequest request) { + stampFacade.updateStampOrders(Long.valueOf(memberId), tripId, request); + + return ResponseEntity.status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)); + } + + @Operation(summary = "스탬프 삭제", description = "특정 스탬프를 삭제합니다.") + @DeleteMapping("/api/trips/{tripId}/stamps/{stampId}") + public ResponseEntity deleteStamp( + @AuthenticationPrincipal String memberId, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, + @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") Long stampId) { + stampFacade.deleteStamp(Long.valueOf(memberId), tripId, stampId); + + return ResponseEntity.status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)); + } + + @Operation(summary = "스탬프 목록 조회", description = "특정 여행의 스탬프 목록을 조회합니다.") + @GetMapping("/api/trips/{tripId}/stamps") + public ResponseEntity loadStampsByTrip( + @AuthenticationPrincipal String memberId, @PathVariable Long tripId) { + List result = stampFacade.getStampsByTrip(Long.valueOf(memberId), tripId); + List response = + result.stream().map(LoadStampInfoResponse::of).toList(); + + return ResponseEntity.status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), response)); + } + + @Operation(summary = "스탬프 상세 조회", description = "특정 여행의 특정 스탬프 상세 정보를 조회합니다.") + @GetMapping("api/trips/{tripId}/stamps/{stampId}") + public ResponseEntity loadStamp( + @AuthenticationPrincipal String memberId, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, + @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") Long stampId) { + StampDetail result = stampFacade.getStamp(Long.valueOf(memberId), tripId, stampId); + + return ResponseEntity.status(HttpStatus.OK) + .body( + StandardResponse.success( + HttpStatus.OK.value(), + LoadStampDetailResponse.of( + result.stampInfo(), result.missionInfos()))); + } +} diff --git a/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampNameAndDeadlineRequest.java b/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampNameAndDeadlineRequest.java new file mode 100644 index 0000000..00d8f3e --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampNameAndDeadlineRequest.java @@ -0,0 +1,11 @@ +package com.ject.studytrip.stamp.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.FutureOrPresent; +import java.time.LocalDate; + +public record UpdateStampNameAndDeadlineRequest( + @Schema(description = "수정할 스탬프 이름") String name, + @Schema(description = "수정할 스탬프 마감일") + @FutureOrPresent(message = "스탬프 마감일은 현재 날짜보다 과거일 수 없습니다.") + LocalDate deadline) {} diff --git a/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.java b/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.java new file mode 100644 index 0000000..f7fa06e --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.java @@ -0,0 +1,8 @@ +package com.ject.studytrip.stamp.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record UpdateStampOrderRequest( + @Schema(description = "변경된 순서를 반영한 스탬프 ID 목록 (앞에서부터 순서대로 정렬)") + List orderedStampIds) {} diff --git a/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.java b/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.java new file mode 100644 index 0000000..b358b1f --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.java @@ -0,0 +1,10 @@ +package com.ject.studytrip.stamp.presentation.dto.response; + +import com.ject.studytrip.stamp.application.dto.StampInfo; +import io.swagger.v3.oas.annotations.media.Schema; + +public record CreateStampResponse(@Schema(description = "스탬프 ID") Long stampId) { + public static CreateStampResponse of(StampInfo info) { + return new CreateStampResponse(info.stampId()); + } +} diff --git a/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.java b/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.java index 6f3c287..e9b5c86 100644 --- a/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.java +++ b/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.java @@ -1,20 +1,25 @@ package com.ject.studytrip.stamp.presentation.dto.response; +import com.ject.studytrip.mission.application.dto.MissionInfo; +import com.ject.studytrip.mission.presentation.dto.response.LoadMissionInfoResponse; import com.ject.studytrip.stamp.application.dto.StampInfo; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; public record LoadStampDetailResponse( @Schema(description = "스탬프 ID") Long stampId, @Schema(description = "스탬프 이름") String stampName, @Schema(description = "스탬프 순서") int stampOrder, @Schema(description = "스탬프 마감일") String stampDeadline, - @Schema(description = "스탬프 완료 여부") boolean completed) { - public static LoadStampDetailResponse of(StampInfo stampInfo) { + @Schema(description = "스탬프 완료 여부") boolean completed, + @Schema(description = "미션 목록") List missions) { + public static LoadStampDetailResponse of(StampInfo stampInfo, List missionInfos) { return new LoadStampDetailResponse( stampInfo.stampId(), stampInfo.stampName(), stampInfo.stampOrder(), stampInfo.deadline(), - stampInfo.completed()); + stampInfo.completed(), + missionInfos.stream().map(LoadMissionInfoResponse::of).toList()); } } diff --git a/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.java b/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.java new file mode 100644 index 0000000..84ae988 --- /dev/null +++ b/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.java @@ -0,0 +1,20 @@ +package com.ject.studytrip.stamp.presentation.dto.response; + +import com.ject.studytrip.stamp.application.dto.StampInfo; +import io.swagger.v3.oas.annotations.media.Schema; + +public record LoadStampInfoResponse( + @Schema(description = "스탬프 ID") Long stampId, + @Schema(description = "스탬프 이름") String stampName, + @Schema(description = "스탬프 순서") int stampOrder, + @Schema(description = "스탬프 마감일") String stampDeadline, + @Schema(description = "스탬프 완료 여부") boolean completed) { + public static LoadStampInfoResponse of(StampInfo info) { + return new LoadStampInfoResponse( + info.stampId(), + info.stampName(), + info.stampOrder(), + info.deadline(), + info.completed()); + } +} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripService.java index fad633a..ed26833 100644 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripService.java +++ b/src/main/java/com/ject/studytrip/trip/application/service/TripService.java @@ -47,15 +47,25 @@ public void updateTrip(Long memberId, Trip trip, UpdateTripRequest request) { TripPolicy.validateOwner(memberId, trip); TripPolicy.validateEndDateIsNotBeforeStartDate(trip.getStartDate(), request.endDate()); - TripPolicy.validateDeleted(trip); + TripPolicy.validateNotDeleted(trip); trip.update(request.name(), request.memo(), category, request.endDate()); } + public void increaseTotalStamps(Trip trip) { + trip.increaseTotalStamps(); + } + + public void decreaseTotalStamps(Trip trip) { + trip.decreaseTotalStamps(); + } + public void deleteTrip(Long memberId, Trip trip) { TripPolicy.validateOwner(memberId, trip); trip.updateDeletedAt(); + + // TODO : 삭제는 로직을 더 구상해본 후 추후 리팩토링 } public Trip getTrip(Long tripId) { @@ -64,7 +74,19 @@ public Trip getTrip(Long tripId) { .findById(tripId) .orElseThrow(() -> new CustomException(TripErrorCode.TRIP_NOT_FOUND)); - TripPolicy.validateDeleted(trip); + TripPolicy.validateNotDeleted(trip); + + return trip; + } + + public Trip getValidTrip(Long memberId, Long tripId) { + Trip trip = + tripRepository + .findById(tripId) + .orElseThrow(() -> new CustomException(TripErrorCode.TRIP_NOT_FOUND)); + + TripPolicy.validateOwner(memberId, trip); + TripPolicy.validateNotDeleted(trip); return trip; } diff --git a/src/main/java/com/ject/studytrip/trip/domain/model/Trip.java b/src/main/java/com/ject/studytrip/trip/domain/model/Trip.java index f6def08..7bc704d 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/model/Trip.java +++ b/src/main/java/com/ject/studytrip/trip/domain/model/Trip.java @@ -86,6 +86,14 @@ public void update(String name, String memo, TripCategory category, LocalDate en // if (Objects.nonNull(deadline)) this.deadline = deadline; // } + public void increaseTotalStamps() { + this.totalStamps += 1; + } + + public void decreaseTotalStamps() { + this.totalStamps -= 1; + } + public void updateIsComplete(boolean completed) { this.completed = completed; } diff --git a/src/main/java/com/ject/studytrip/trip/domain/policy/TripPolicy.java b/src/main/java/com/ject/studytrip/trip/domain/policy/TripPolicy.java index b4da3eb..3b09f87 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/policy/TripPolicy.java +++ b/src/main/java/com/ject/studytrip/trip/domain/policy/TripPolicy.java @@ -33,7 +33,7 @@ public static void validateMinimumStamps(CreateTripRequest request) { throw new CustomException(TripErrorCode.TRIP_STAMP_REQUIRED); } - public static void validateDeleted(Trip trip) { + public static void validateNotDeleted(Trip trip) { if (trip.getDeletedAt() != null) throw new CustomException(TripErrorCode.TRIP_ALREADY_DELETED); } diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.java index e03f119..4964c6e 100644 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.java +++ b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.java @@ -1,7 +1,7 @@ package com.ject.studytrip.trip.presentation.dto.response; import com.ject.studytrip.stamp.application.dto.StampInfo; -import com.ject.studytrip.stamp.presentation.dto.response.LoadStampDetailResponse; +import com.ject.studytrip.stamp.presentation.dto.response.LoadStampInfoResponse; import com.ject.studytrip.trip.application.dto.TripInfo; import com.ject.studytrip.trip.domain.model.TripCategory; import io.swagger.v3.oas.annotations.media.Schema; @@ -19,7 +19,7 @@ public record LoadTripDetailResponse( @Schema(description = "완료된 총 스탬프 수") int completedStamps, @Schema(description = "진행률") Integer progress, @Schema(description = "여행 완료 여부") boolean completed, - @Schema(description = "여행에 속한 스탬프 목록") List stamps) { + @Schema(description = "여행에 속한 스탬프 목록") List stamps) { public static LoadTripDetailResponse of(TripInfo tripInfo, List stampInfos) { return new LoadTripDetailResponse( tripInfo.tripId(), @@ -33,6 +33,6 @@ public static LoadTripDetailResponse of(TripInfo tripInfo, List stamp tripInfo.completedStamps(), tripInfo.progress(), tripInfo.completed(), - stampInfos.stream().map(LoadStampDetailResponse::of).toList()); + stampInfos.stream().map(LoadStampInfoResponse::of).toList()); } } diff --git a/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java b/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java index 8d303c1..e5abb49 100644 --- a/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java +++ b/src/test/java/com/ject/studytrip/stamp/application/service/StampServiceTest.java @@ -12,19 +12,21 @@ import com.ject.studytrip.member.domain.model.Member; import com.ject.studytrip.member.fixture.MemberFixture; import com.ject.studytrip.stamp.domain.error.StampErrorCode; -import com.ject.studytrip.stamp.domain.factory.StampFactory; import com.ject.studytrip.stamp.domain.model.Stamp; +import com.ject.studytrip.stamp.domain.repository.StampQueryRepository; import com.ject.studytrip.stamp.domain.repository.StampRepository; import com.ject.studytrip.stamp.fixture.CreateStampRequestFixture; +import com.ject.studytrip.stamp.fixture.StampFixture; +import com.ject.studytrip.stamp.fixture.UpdateStampRequestFixture; import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampNameAndDeadlineRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; import com.ject.studytrip.trip.domain.model.Trip; import com.ject.studytrip.trip.domain.model.TripCategory; import com.ject.studytrip.trip.fixture.TripFixture; import java.time.LocalDate; -import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -34,190 +36,527 @@ @DisplayName("StampService 단위 테스트") public class StampServiceTest extends BaseUnitTest { - private static final String STAMP_NAME = "STAMP NAME"; - private static final LocalDate STAMP_DEAD_LINE = LocalDate.now().plusDays(7); + private static final LocalDate PAST_DATE = LocalDate.now().minusDays(1); @InjectMocks private StampService stampService; @Mock private StampRepository stampRepository; + @Mock private StampQueryRepository stampQueryRepository; + private Member member; private Trip courseTrip; private Trip exploreTrip; + private Stamp courseStamp1; + private Stamp courseStamp2; @BeforeEach void setup() { - Member member = MemberFixture.createMemberFromKakao(); - courseTrip = TripFixture.createTrip(member, TripCategory.COURSE); - exploreTrip = TripFixture.createTrip(member, TripCategory.EXPLORE); + member = MemberFixture.createMemberFromKakao(); + courseTrip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE); + exploreTrip = TripFixture.createTripWithId(2L, member, TripCategory.EXPLORE); + courseStamp1 = StampFixture.createStampWithId(1L, courseTrip, 1); + courseStamp2 = StampFixture.createStampWithId(2L, courseTrip, 2); } @Nested @DisplayName("스탬프를 생성한다") class CreateStamp { + private final CreateStampRequestFixture fixture = new CreateStampRequestFixture(); + + @Nested + @DisplayName("단일 스탬프 생성") + class CreateSingleStamp { + + @Test + @DisplayName("유효한 요청으로 스탬프를 생성하면 스탬프가 저장되고 반환된다") + void shouldCreateValidStamp() { + // given + CreateStampRequest request = fixture.build(); + + Stamp saved = + Stamp.of(courseTrip, request.name(), request.order(), request.deadline()); + given(stampRepository.save(any())).willReturn(saved); + + // when + Stamp stamp = stampService.createStamp(courseTrip, request); + + // then + verify(stampRepository).save(any()); + assertThat(stamp.getName()).isEqualTo(saved.getName()); + assertThat(stamp.getStampOrder()).isEqualTo(saved.getStampOrder()); + assertThat(stamp.getDeadline()).isEqualTo(saved.getDeadline()); + } + + @Test + @DisplayName("스탬프의 마감일이 과거라면 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineCannotBeInPast() { + // given + CreateStampRequest request = fixture.withDeadline(PAST_DATE).build(); + + // when & given + assertThatThrownBy(() -> stampService.createStamp(courseTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.STAMP_DEADLINE_CANNOT_BE_IN_PAST.getMessage()); + } + + @Test + @DisplayName("스탬프의 마감일이 여행 종료일보다 이후라면 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineExceedsTripEndDate() { + // given + CreateStampRequest request = + fixture.withDeadline(courseTrip.getEndDate().plusDays(1)).build(); + + // when & then + assertThatThrownBy(() -> stampService.createStamp(courseTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.STAMP_DEADLINE_EXCEEDS_TRIP_END_DATE.getMessage()); + } + + @Test + @DisplayName("탐험형 여행에 순서가 지정된 스탬프를 등록하면 예외가 발생한다") + void shouldThrowExceptionWhenOrderSpecifiedForExploreTrip() { + // given + CreateStampRequest request = fixture.withStampOrder(1).build(); + + // when & then + assertThatThrownBy(() -> stampService.createStamp(exploreTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.INVALID_STAMP_ORDER_FOR_EXPLORATION_TRIP + .getMessage()); + } + + @Test + @DisplayName("코스형 여행에 순서가 유효 범위를 벗어난 경우 예외가 발생한다") + void shouldThrowExceptionWhenOrderOutOfRange() { + // given + CreateStampRequest request = fixture.withStampOrder(1000).build(); + + // when & then + assertThatThrownBy(() -> stampService.createStamp(courseTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.INVALID_STAMP_ORDER_RANGE_FOR_COURSE_TRIP + .getMessage()); + } + + @Test + @DisplayName("코스형 여행에 중복된 순서의 스탬프를 등록하면 예외가 발생한다") + void shouldThrowExceptionWhenDuplicateOrderForCourseTrip() { + // given + CreateStampRequest request = fixture.withStampOrder(1).build(); + given(stampRepository.findAllByTripIdAndDeletedAtIsNull(courseTrip.getId())) + .willReturn(List.of(courseStamp1)); + + // when & then + assertThatThrownBy(() -> stampService.createStamp(courseTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.DUPLICATE_STAMP_ORDER_FOR_COURSE_TRIP.getMessage()); + } + } + + @Nested + @DisplayName("여러개 스탬프 생성") + class CreateStamps { + @Test + @DisplayName("코스형 여행의 유효한 스탬프 리스트를 넘기면 저장된다") + void shouldCreateStampsForCourseTrip() { + // given + List requests = List.of(fixture.build()); + + // when + stampService.createStamps(courseTrip, requests); + + // then + verify(stampRepository).saveAll(anyList()); + } + + @Test + @DisplayName("탐험형 여행의 유효한 스탬프 리스트를 넘기면 저장된다") + void shouldCreateStampsForExploreTrip() { + // given + List requests = List.of(fixture.withStampOrder(0).build()); + + // when + stampService.createStamps(exploreTrip, requests); + + // then + verify(stampRepository).saveAll(anyList()); + } + + @Test + @DisplayName("스탬프의 마감일이 과거라면 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineCannotBeInPast() { + // given + List requests = + List.of(fixture.withDeadline(PAST_DATE).build()); + + // when & then + assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.STAMP_DEADLINE_CANNOT_BE_IN_PAST.getMessage()); + } + + @Test + @DisplayName("스탬프의 마감일이 여행 종료일보다 이후일 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineExceedsTripEndDate() { + // given + List requests = + List.of(fixture.withDeadline(courseTrip.getEndDate().plusDays(1)).build()); + + // when & then + assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.STAMP_DEADLINE_EXCEEDS_TRIP_END_DATE.getMessage()); + } + + @Test + @DisplayName("탐험형 여행에서 순서가 1 이상이면 예외가 발생한다") + void shouldThrowExceptionWhenOrderExistsInExploreTrip() { + // given + List requests = List.of(fixture.withStampOrder(1).build()); + + // when & then + assertThatThrownBy(() -> stampService.createStamps(exploreTrip, requests)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.INVALID_STAMP_ORDER_FOR_EXPLORATION_TRIP + .getMessage()); + } + + @Test + @DisplayName("코스형 여행에서 순서가 1 미만 또는 총 개수 초과라면 예외가 발생한다") + void shouldThrowExceptionWhenStampOrderIsOutOfRangeForCourseTrip() { + // given + List requests = List.of(fixture.withStampOrder(2).build()); + + // when & then + assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.INVALID_STAMP_ORDER_RANGE_FOR_COURSE_TRIP + .getMessage()); + } + + @Test + @DisplayName("코스형 여행에서 순서가 중복되면 예외가 발생한다") + void shouldThrowExceptionWhenDuplicateOrderInCourseTrip() { + // given + List requests = + List.of( + fixture.withStampOrder(1).build(), + fixture.withStampOrder(1).build()); + + // when & then + assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.DUPLICATE_STAMP_ORDER_FOR_COURSE_TRIP.getMessage()); + } + } + } + + @Nested + @DisplayName("스탬프를 수정한다") + class UpdateStamp { + private final UpdateStampRequestFixture fixture = new UpdateStampRequestFixture(); + + @Nested + @DisplayName("스탬프 이름 또는 마감일 수정") + class UpdateStampNameOrDeadline { + + @Test + @DisplayName("유효한 정보로 스탬프의 이름 또는 마감일을 수정하면 스탬프가 업데이트된다") + void shouldUpdateStampNameOrDeadline() { + // given + UpdateStampNameAndDeadlineRequest request = fixture.buildUpdateNameAndDeadline(); + + // when + stampService.updateStampNameAndDeadline(courseTrip, courseStamp1, request); + + // then + assertThat(courseStamp1.getName()).isEqualTo(request.name()); + assertThat(courseStamp1.getDeadline()).isEqualTo(request.deadline()); + } + + @Test + @DisplayName("스탬프의 마감일이 과거라면 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineCannotBeInPast() { + // given + UpdateStampNameAndDeadlineRequest request = + fixture.withDeadline(PAST_DATE).buildUpdateNameAndDeadline(); + + // when & then + assertThatThrownBy( + () -> + stampService.updateStampNameAndDeadline( + courseTrip, courseStamp1, request)) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.STAMP_DEADLINE_CANNOT_BE_IN_PAST.getMessage()); + } + + @Test + @DisplayName("스탬프의 마감일이 여행 종료일보다 이후일 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineExceedsTripEndDate() { + // given + UpdateStampNameAndDeadlineRequest request = + fixture.withDeadline(courseTrip.getEndDate().plusDays(1)) + .buildUpdateNameAndDeadline(); + + // when & then + assertThatThrownBy( + () -> + stampService.updateStampNameAndDeadline( + courseTrip, courseStamp1, request)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.STAMP_DEADLINE_EXCEEDS_TRIP_END_DATE.getMessage()); + } + } + + @Nested + @DisplayName("스탬프 순서 수정") + class UpdateStampOrders { + + @Test + @DisplayName("코스형 여행에서 클라이언트가 전달한 스탬프 ID 리스트 순서에 따라 스탬프의 순서를 수정한다") + void shouldUpdateStampOrderForCourseTrip() { + // given + UpdateStampOrderRequest request = + fixture.withOrderedStampIds(List.of(2L, 1L)).buildUpdateOrders(); + + given(stampRepository.findAllByIdIn(request.orderedStampIds())) + .willReturn(List.of(courseStamp1, courseStamp2)); + + // when + stampService.updateStampsOrders(courseTrip, request); + + // then + assertThat(courseStamp2.getStampOrder()).isEqualTo(1); + assertThat(courseStamp1.getStampOrder()).isEqualTo(2); + } + + @Test + @DisplayName("탐험형 여행의 스탬프 순서를 수정하면 예외가 발생한다") + void shouldThrowExceptionWhenTripIsExplorationType() { + // given + UpdateStampOrderRequest request = fixture.buildUpdateOrders(); + + // when & then + assertThatThrownBy(() -> stampService.updateStampsOrders(exploreTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage( + StampErrorCode.CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP + .getMessage()); + } + + @Test + @DisplayName("유효하지 않는 스탬프 ID일 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampNotFoundById() { + // given + UpdateStampOrderRequest request = + fixture.withOrderedStampIds(List.of(1000L, 1001L)).buildUpdateOrders(); + + // when & then + assertThatThrownBy(() -> stampService.updateStampsOrders(courseTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.INVALID_STAMP_ID_IN_REQUEST.getMessage()); + } + + @Test + @DisplayName("여행에 속한 스탬프가 아닐 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampDoesNotBelongToTrip() { + // given + UpdateStampOrderRequest request = fixture.buildUpdateOrders(); + + Trip newTrip = TripFixture.createTripWithId(3L, member, TripCategory.COURSE); + given(stampRepository.findAllByIdIn(request.orderedStampIds())) + .willReturn(List.of(courseStamp1, courseStamp2)); + + // when & then + assertThatThrownBy(() -> stampService.updateStampsOrders(newTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.STAMP_NOT_BELONG_TO_TRIP.getMessage()); + } + + @Test + @DisplayName("삭제된 스탬프일 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampAlreadyDeleted() { + // given + UpdateStampOrderRequest request = fixture.buildUpdateOrders(); + + courseStamp1.updateDeletedAt(); + given(stampRepository.findAllByIdIn(request.orderedStampIds())) + .willReturn(List.of(courseStamp1, courseStamp2)); + + // when & then + assertThatThrownBy(() -> stampService.updateStampsOrders(courseTrip, request)) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.STAMP_ALREADY_DELETED.getMessage()); + } + } + + @Nested + @DisplayName("여행 카테고리에 변경사항에 따른 스탬프 순서 수정") + class UpdateStampOrdersForUpdateTripCategory { + + @Test + @DisplayName("여행의 카테고리가 탐험형으로 수정되면 소속된 모든 스탬프의 순서를 0으로 수정한다") + void shouldSetAllStampOrdersToZeroWhenCategoryChangesToExplore() { + // given + given(stampRepository.findAllByTripIdOrderByDeadlineAsc(courseTrip.getId())) + .willReturn(List.of(courseStamp1, courseStamp2)); + + // when + stampService.updateStampsOrderByTripCategoryChange( + courseTrip.getId(), TripCategory.EXPLORE); + + // then + assertThat(courseStamp1.getStampOrder()).isEqualTo(0); + assertThat(courseStamp2.getStampOrder()).isEqualTo(0); + } + + @Test + @DisplayName("여행의 카테고리가 코스형으로 수정되면 소속된 모든 스탬프의 순서를 마감일이 이른 순으로 1부터 순차적으로 순서를 수정한다") + void shouldSetSequentialStampOrdersWhenCategoryChangesToCourse() { + // given + courseStamp1.updateStampOrder(0); + courseStamp2.updateStampOrder(0); + + given(stampRepository.findAllByTripIdOrderByDeadlineAsc(exploreTrip.getId())) + .willReturn(List.of(courseStamp1, courseStamp2)); + + // when + stampService.updateStampsOrderByTripCategoryChange( + exploreTrip.getId(), TripCategory.COURSE); + + // then + assertThat(courseStamp1.getStampOrder()).isEqualTo(1); + assertThat(courseStamp2.getStampOrder()).isEqualTo(2); + } + } + } + + @Nested + @DisplayName("스탬프를 삭제한다") + class DeleteStamp { @Test - @DisplayName("코스형 여행의 유효한 스탬프 리스트를 넘기면 저장된다") - void shouldCreateStampsForCourseTrip() { + @DisplayName("코스형 여행의 스탬프 삭제 시 deletedAt 필드를 현재 시각으로 설정하고, 삭제된 스탬프 이후 순서들을 하나씩 앞당긴다") + void shouldDeleteCourseTripStamp() { // given - List requests = List.of(new CreateStampRequestFixture().build()); + given( + stampQueryRepository.findStampsToShiftAfterOrder( + courseTrip.getId(), courseStamp1.getStampOrder())) + .willReturn(List.of(courseStamp2)); // when - stampService.createStamps(courseTrip, requests); + stampService.deleteStamp(courseTrip.getId(), courseTrip.getCategory(), courseStamp1); // then - verify(stampRepository).saveAll(anyList()); + assertThat(courseStamp1.getDeletedAt()).isNotNull(); + assertThat(courseStamp2.getStampOrder()).isEqualTo(1); } @Test - @DisplayName("탐험형 여행의 유효한 스탬프 리스트를 넘기면 저장된다") - void shouldCreateStampsForExploreTrip() { + @DisplayName("탐험형 여행의 스탬프 삭제 시 deletedAt 필드를 현재 시각으로 설정한다") + void shouldDeleteExploreTripStamp() { // given - List requests = - List.of(new CreateStampRequestFixture().withStampOrder(0).build()); + Stamp exploreStamp = StampFixture.createStamp(exploreTrip, 0); // when - stampService.createStamps(exploreTrip, requests); + stampService.deleteStamp(exploreTrip.getId(), exploreTrip.getCategory(), exploreStamp); // then - verify(stampRepository).saveAll(anyList()); + assertThat(exploreStamp.getDeletedAt()).isNotNull(); } + } + + @Nested + @DisplayName("스탬프 목록을 조회한다") + class ListStamps { @Test - @DisplayName("스탬프의 마감일이 과거라면 예외가 발생한다") - void shouldThrowExceptionWhenStampDeadlineCannotBeInPast() { + @DisplayName("유효한 여행 ID로 삭제된 스탬프를 제외한 스탬프 목록을 조회한다") + void shouldGetStampsByTripId() { // given - List requests = - List.of( - new CreateStampRequestFixture() - .withDeadline(LocalDate.now().minusDays(1)) - .build()); + given(stampRepository.findAllByTripIdAndDeletedAtIsNull(courseTrip.getId())) + .willReturn(List.of(courseStamp1, courseStamp2)); - // when & then - assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_DEADLINE_CANNOT_BE_IN_PAST.getMessage()); - } + // when + List stamps = stampService.getStampsByTripId(courseTrip.getId()); - @Test - @DisplayName("스탬프의 마감일이 여행 종료일보다 이후일 경우 예외가 발생한다") - void shouldThrowExceptionWhenStampDeadlineIsAfterTripEndDate() { - // given - List requests = - List.of( - new CreateStampRequestFixture() - .withDeadline(courseTrip.getEndDate().plusDays(1)) - .build()); + // then + verify(stampRepository).findAllByTripIdAndDeletedAtIsNull(any()); - // when & then - assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_DEADLINE_EXCEEDS_TRIP_END_DATE.getMessage()); + assertThat(stamps.isEmpty()).isFalse(); + assertThat(stamps.size()).isEqualTo(2); } + } + + @Nested + @DisplayName("스탬프를 조회한다") + class GetStamp { @Test - @DisplayName("탐험형 여행에서 순서가 1 이상이면 예외가 발생한다") - void shouldThrowExceptionWhenOrderExistsInExploreTrip() { + @DisplayName("스탬프 ID로 스탬프를 조회하고, 여행 소속 및 삭제 여부를 검증하고 반환한다") + void shouldGetStampReturnValidStamp() { // given - List requests = - List.of(new CreateStampRequestFixture().withStampOrder(1).build()); + given(stampRepository.findById(any())).willReturn(Optional.ofNullable(courseStamp1)); - // when & then - assertThatThrownBy(() -> stampService.createStamps(exploreTrip, requests)) - .isInstanceOf(CustomException.class) - .hasMessage( - StampErrorCode.INVALID_STAMP_ORDER_FOR_EXPLORATION_TRIP.getMessage()); - } + // when + Stamp stamp = stampService.getValidStamp(courseTrip.getId(), courseStamp1.getId()); - @Test - @DisplayName("코스형 여행에서 순서가 1 미만 또는 총 개수 초과라면 예외가 발생한다") - void shouldThrowExceptionWhenStampOrderIsOutOfRangeForCourseTrip() { - // given - List requests = - List.of(new CreateStampRequestFixture().withStampOrder(2).build()); + // then + verify(stampRepository).findById(any()); - // when & then - assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) - .isInstanceOf(CustomException.class) - .hasMessage( - StampErrorCode.INVALID_STAMP_ORDER_RANGE_FOR_COURSE_TRIP.getMessage()); + assertThat(stamp.getId()).isEqualTo(courseStamp1.getId()); } @Test - @DisplayName("코스형 여행에서 순서가 중복되면 예외가 발생한다") - void shouldThrowExceptionWhenDuplicateOrderInCourseTrip() { + @DisplayName("유효하지 않은 스탬프 ID일 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampNotFoundById() { // given - List requests = - List.of( - new CreateStampRequestFixture().withStampOrder(1).build(), - new CreateStampRequestFixture().withStampOrder(1).build()); + Long stampId = 1000L; // when & then - assertThatThrownBy(() -> stampService.createStamps(courseTrip, requests)) + assertThatThrownBy(() -> stampService.getValidStamp(courseTrip.getId(), stampId)) .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.DUPLICATE_STAMP_ORDER_FOR_COURSE_TRIP.getMessage()); + .hasMessage(StampErrorCode.STAMP_NOT_FOUND.getMessage()); } - } - - @Nested - @DisplayName("스탬프를 수정한다") - class UpdateStamp { @Test - @DisplayName("여행의 카테고리가 탐험형으로 수정되면 소속된 모든 스탬프의 순서를 0으로 수정한다") - void shouldSetAllStampOrdersToZeroWhenCategoryChangesToExplore() { + @DisplayName("여행에 속한 스탬프가 아닐 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampDoesNotBelongToTrip() { // given - Stamp stamp = spy(StampFactory.create(courseTrip, STAMP_NAME, 1, STAMP_DEAD_LINE)); - List stamps = List.of(stamp); - - given(stampRepository.findAllByTripIdOrderByDeadlineAsc(courseTrip.getId())) - .willReturn(stamps); + given(stampRepository.findById(any())).willReturn(Optional.ofNullable(courseStamp1)); - // when - stampService.updateStampsOrderByTripCategoryChange( - courseTrip.getId(), TripCategory.EXPLORE); - - // then - assertThat(stamp.getStampOrder()).isEqualTo(0); + // when & then + assertThatThrownBy( + () -> + stampService.getValidStamp( + exploreTrip.getId(), courseStamp1.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.STAMP_NOT_BELONG_TO_TRIP.getMessage()); } @Test - @DisplayName("여행의 카테고리가 코스형으로 수정되면 소속된 모든 스탬프의 순서를 마감일이 이른 순으로 1부터 순차적으로 순서를 수정한다") - void shouldSetSequentialStampOrdersWhenCategoryChangesToCourse() { + @DisplayName("삭제된 스탬프일 경우 예외가 발생한다") + void shouldThrowExceptionWhenStampAlreadyDeleted() { // given - Stamp stamp1 = - spy( - StampFactory.create( - exploreTrip, STAMP_NAME, 0, STAMP_DEAD_LINE.plusDays(1))); - Stamp stamp2 = spy(StampFactory.create(exploreTrip, STAMP_NAME, 0, STAMP_DEAD_LINE)); - List stamps = - Stream.of(stamp1, stamp2) - .sorted(Comparator.comparing(Stamp::getDeadline)) - .collect(Collectors.toList()); - - given(stampRepository.findAllByTripIdOrderByDeadlineAsc(exploreTrip.getId())) - .willReturn(stamps); + courseStamp1.updateDeletedAt(); - // when - stampService.updateStampsOrderByTripCategoryChange( - exploreTrip.getId(), TripCategory.COURSE); + given(stampRepository.findById(any())).willReturn(Optional.ofNullable(courseStamp1)); - // then - assertThat(stamp2.getStampOrder()).isEqualTo(1); - assertThat(stamp1.getStampOrder()).isEqualTo(2); - } - } - - @Nested - @DisplayName("스탬프 목록을 조회한다") - class ListStamps { - - @Test - @DisplayName("유효한 여행 ID로 스탬프 목록을 조회한다") - void shouldGetStampsByTripId() { - // when - stampService.getStampsByTripId(courseTrip.getId()); - - // then - verify(stampRepository).findAllByTripId(any()); + // when & then + assertThatThrownBy( + () -> + stampService.getValidStamp( + courseStamp1.getId(), courseStamp1.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(StampErrorCode.STAMP_ALREADY_DELETED.getMessage()); } } } diff --git a/src/test/java/com/ject/studytrip/stamp/fixture/StampFixture.java b/src/test/java/com/ject/studytrip/stamp/fixture/StampFixture.java index 1226b31..cf1e839 100644 --- a/src/test/java/com/ject/studytrip/stamp/fixture/StampFixture.java +++ b/src/test/java/com/ject/studytrip/stamp/fixture/StampFixture.java @@ -1,14 +1,23 @@ package com.ject.studytrip.stamp.fixture; +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 java.time.LocalDate; +import org.springframework.test.util.ReflectionTestUtils; public class StampFixture { private static final String STAMP_NAME = "TEST STAMP NAME"; private static final LocalDate STAMP_DEAD_LINE = LocalDate.now().plusDays(7); public static Stamp createStamp(Trip trip, int order) { - return Stamp.of(trip, STAMP_NAME, order, STAMP_DEAD_LINE); + return StampFactory.create(trip, STAMP_NAME, order, STAMP_DEAD_LINE); + } + + public static Stamp createStampWithId(Long id, Trip trip, int order) { + Stamp stamp = StampFactory.create(trip, STAMP_NAME, order, STAMP_DEAD_LINE); + ReflectionTestUtils.setField(stamp, "id", id); + + return stamp; } } diff --git a/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.java b/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.java new file mode 100644 index 0000000..57ae914 --- /dev/null +++ b/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.java @@ -0,0 +1,35 @@ +package com.ject.studytrip.stamp.fixture; + +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampNameAndDeadlineRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; +import java.time.LocalDate; +import java.util.List; + +public class UpdateStampRequestFixture { + private String name = "TEST STAMP"; + private List orderedStampIds = List.of(1L, 2L); + private LocalDate deadline = LocalDate.now().plusDays(1); + + public UpdateStampRequestFixture withName(String name) { + this.name = name; + return this; + } + + public UpdateStampRequestFixture withOrderedStampIds(List orderedStampIds) { + this.orderedStampIds = orderedStampIds; + return this; + } + + public UpdateStampRequestFixture withDeadline(LocalDate deadline) { + this.deadline = deadline; + return this; + } + + public UpdateStampNameAndDeadlineRequest buildUpdateNameAndDeadline() { + return new UpdateStampNameAndDeadlineRequest(name, deadline); + } + + public UpdateStampOrderRequest buildUpdateOrders() { + return new UpdateStampOrderRequest(orderedStampIds); + } +} diff --git a/src/test/java/com/ject/studytrip/stamp/helper/StampTestHelper.java b/src/test/java/com/ject/studytrip/stamp/helper/StampTestHelper.java new file mode 100644 index 0000000..241ba2e --- /dev/null +++ b/src/test/java/com/ject/studytrip/stamp/helper/StampTestHelper.java @@ -0,0 +1,25 @@ +package com.ject.studytrip.stamp.helper; + +import com.ject.studytrip.stamp.domain.model.Stamp; +import com.ject.studytrip.stamp.domain.repository.StampRepository; +import com.ject.studytrip.stamp.fixture.StampFixture; +import com.ject.studytrip.trip.domain.model.Trip; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class StampTestHelper { + @Autowired private StampRepository stampRepository; + + public Stamp saveStamp(Trip trip, int order) { + Stamp stamp = StampFixture.createStamp(trip, order); + return stampRepository.save(stamp); + } + + public Stamp saveDeletedStamp(Trip trip, int order) { + Stamp stamp = StampFixture.createStamp(trip, order); + stamp.updateDeletedAt(); + + return stampRepository.save(stamp); + } +} diff --git a/src/test/java/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.java b/src/test/java/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.java new file mode 100644 index 0000000..fa68b08 --- /dev/null +++ b/src/test/java/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.java @@ -0,0 +1,1303 @@ +package com.ject.studytrip.stamp.presentation.controller; + +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.member.domain.model.Member; +import com.ject.studytrip.member.helper.MemberTestHelper; +import com.ject.studytrip.stamp.domain.error.StampErrorCode; +import com.ject.studytrip.stamp.domain.model.Stamp; +import com.ject.studytrip.stamp.fixture.CreateStampRequestFixture; +import com.ject.studytrip.stamp.fixture.UpdateStampRequestFixture; +import com.ject.studytrip.stamp.helper.StampTestHelper; +import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampNameAndDeadlineRequest; +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; +import com.ject.studytrip.trip.domain.error.TripErrorCode; +import com.ject.studytrip.trip.domain.model.Trip; +import com.ject.studytrip.trip.domain.model.TripCategory; +import com.ject.studytrip.trip.helper.TripTestHelper; +import java.time.LocalDate; +import java.util.List; +import org.apache.http.HttpHeaders; +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.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +public class StampControllerIntegrationTest extends BaseIntegrationTest { + private static final int NEXT_STAMP_ORDER = 3; + + @Autowired private MemberTestHelper memberTestHelper; + @Autowired private TripTestHelper tripTestHelper; + @Autowired private StampTestHelper stampTestHelper; + @Autowired private TokenTestHelper tokenTestHelper; + + private String token; + private Member member; + private Trip courseTrip; + private Trip exploreTrip; + private Stamp courseStamp1; + private Stamp courseStamp2; + + @BeforeEach + void setup() { + member = memberTestHelper.saveMember(); + token = + tokenTestHelper.createAccessToken( + member.getId().toString(), member.getRole().name()); + courseTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); + exploreTrip = tripTestHelper.saveTrip(member, TripCategory.EXPLORE); + courseStamp1 = stampTestHelper.saveStamp(courseTrip, 1); + courseStamp2 = stampTestHelper.saveStamp(courseTrip, 2); + } + + @Nested + @DisplayName("스탬프 생성 API") + class CreateStamp { + private final CreateStampRequestFixture createStampRequestFixture = + new CreateStampRequestFixture(); + + private ResultActions getResultActions( + String token, Object tripId, CreateStampRequest request) throws Exception { + return mockMvc.perform( + post("/api/trips/{tripId}/stamps", tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.TOKEN_PREFIX + token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + } + + @Test + @DisplayName("유효한 요청으로 특정 여행의 스탬프를 생성하고, 여행 총 스탬프 수가 증가한다") + void shouldCreateStamp() throws Exception { + // given + CreateStampRequest request = + createStampRequestFixture.withStampOrder(NEXT_STAMP_ORDER).build(); + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.stampId").isNumber()); + } + + @Test + @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") + void shouldThrowExceptionWhenUnauthenticated() throws Exception { + // given + CreateStampRequest request = createStampRequestFixture.build(); + + // when + ResultActions resultActions = getResultActions("", courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenTripIdTypeMismatch() throws Exception { + // given + String tripId = "abc"; + CreateStampRequest request = createStampRequestFixture.build(); + // when + ResultActions resultActions = getResultActions(token, tripId, request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("스탬프를 생성하는데 필요한 필수 요청 값이 누락되거나 유효하지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenInvalidRequiredFields() throws Exception { + // given + CreateStampRequest request = createStampRequestFixture.withName("").build(); + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_NOT_VALID + .getStatus() + .value())); + } + + @Test + @DisplayName("스탬프 마감일이 과거일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenDeadlineIsInThePast() throws Exception { + // given + CreateStampRequest request = + createStampRequestFixture.withDeadline(LocalDate.now().minusDays(1)).build(); + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_NOT_VALID + .getStatus() + .value())); + } + + @Test + @DisplayName("유효하지 않은 여행 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidTripId() throws Exception { + // given + Long tripId = 10000L; + CreateStampRequest request = createStampRequestFixture.build(); + + // when + ResultActions resultActions = getResultActions(token, tripId, request); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 여행의 소유자가 아닐 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenNotTripOwner() throws Exception { + // given + Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); + Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); + CreateStampRequest request = createStampRequestFixture.build(); + + // when + ResultActions resultActions = getResultActions(token, newTrip.getId(), request); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); + } + + @Test + @DisplayName("삭제된 여행일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyTrip() throws Exception { + // given + Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); + CreateStampRequest request = createStampRequestFixture.build(); + + // when + ResultActions resultActions = getResultActions(token, deleted.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())); + } + + @Test + @DisplayName("스탬프 마감일이 여행의 종료일보다 이후일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineIsAfterTripEndDate() throws Exception { + // given + CreateStampRequest request = + createStampRequestFixture + .withDeadline(courseTrip.getEndDate().plusDays(1)) + .build(); + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_DEADLINE_EXCEEDS_TRIP_END_DATE + .getStatus() + .value())); + } + + @Test + @DisplayName("탐험형 여행에 순서가 존재하는 스탬프를 추가하면 400 예외가 발생한다") + void shouldThrowExceptionWhenStampOrderExistsInExplorationTrip() throws Exception { + // given + CreateStampRequest request = createStampRequestFixture.build(); + + // when + ResultActions resultActions = getResultActions(token, exploreTrip.getId(), request); + + // when & then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.INVALID_STAMP_ORDER_FOR_EXPLORATION_TRIP + .getStatus() + .value())); + } + + @Test + @DisplayName("코스형 여행에 유효하지 않은 순서(중복, 범위 이탈)가 존재하는 스탬프를 추가하면 400 예외가 발생한다") + void shouldThrowExceptionWhenStampOrderOutOfRangeInCourseTrip() throws Exception { + // given + CreateStampRequest request = createStampRequestFixture.build(); + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // when & then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.INVALID_STAMP_ORDER_RANGE_FOR_COURSE_TRIP + .getStatus() + .value())); + } + } + + @Nested + @DisplayName("스탬프 수정 API") + class UpdateStamp { + private final UpdateStampRequestFixture updateStampRequestFixture = + new UpdateStampRequestFixture(); + + @Nested + @DisplayName("스탬프 이름, 마감일 수정") + class UpdateNameAndDeadline { + private ResultActions getResultActions( + String token, + Object tripId, + Object stampId, + UpdateStampNameAndDeadlineRequest request) + throws Exception { + return mockMvc.perform( + patch("/api/trips/{tripId}/stamps/{stampId}", tripId, stampId) + .header( + HttpHeaders.AUTHORIZATION, + TokenFixture.TOKEN_PREFIX + token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + } + + @Test + @DisplayName("유효한 요청으로 스탬프의 이름과 마감일을 수정한다") + void shouldUpdateStampNameAndDeadline() throws Exception { + // given + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, courseTrip.getId(), courseStamp1.getId(), request); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") + void shouldThrowExceptionWhenUnauthenticated() throws Exception { + // given + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + // when + ResultActions resultActions = + getResultActions("", courseTrip.getId(), courseStamp1.getId(), request); + + // then + resultActions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenTripIdTypeMismatch() throws Exception { + // given + String tripId = "abc"; + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, tripId, courseStamp1.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenStampIdTypeMismatch() throws Exception { + // given + String stampId = "abc"; + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + // when + ResultActions resultActions = + getResultActions(token, courseTrip.getId(), stampId, request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("수정한 스탬프 마감일이 과거일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenStampDeadlineCannotBeInPast() throws Exception { + // given + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture + .withDeadline(LocalDate.now().minusDays(1)) + .buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, courseTrip.getId(), courseStamp1.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_NOT_VALID + .getStatus() + .value())); + } + + @Test + @DisplayName("유효하지 않은 여행 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidTripId() throws Exception { + // given + Long tripId = 10000L; + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, tripId, courseStamp1.getId(), request); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 사용자가 여행의 소유자가 아닐 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenNotTripOwner() throws Exception { + // given + Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); + Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); + Stamp newStamp = stampTestHelper.saveStamp(newTrip, 1); + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, newTrip.getId(), newStamp.getId(), request); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); + } + + @Test + @DisplayName("삭제된 여행일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedTrip() throws Exception { + // given + Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, deleted.getId(), courseStamp1.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + TripErrorCode.TRIP_ALREADY_DELETED + .getStatus() + .value())); + } + + @Test + @DisplayName("유효하지 않은 스탬프 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidStampId() throws Exception { + // given + Long stampId = 10000L; + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, courseTrip.getId(), stampId, request); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(StampErrorCode.STAMP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("여행에 속한 스탬프가 아닌 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenStampTripMisMatch() throws Exception { + // given + Stamp newStamp = stampTestHelper.saveStamp(exploreTrip, 0); + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, courseTrip.getId(), newStamp.getId(), request); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_NOT_BELONG_TO_TRIP + .getStatus() + .value())); + } + + @Test + @DisplayName("삭제된 스탬프일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedStamp() throws Exception { + // given + Stamp newStamp = stampTestHelper.saveDeletedStamp(exploreTrip, 0); + UpdateStampNameAndDeadlineRequest request = + updateStampRequestFixture.buildUpdateNameAndDeadline(); + + // when + ResultActions resultActions = + getResultActions(token, exploreTrip.getId(), newStamp.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_ALREADY_DELETED + .getStatus() + .value())); + } + } + + @Nested + @DisplayName("스탬프 순서 수정") + class UpdateOrders { + + private ResultActions getResultActions( + String token, Object tripId, UpdateStampOrderRequest request) throws Exception { + return mockMvc.perform( + put("/api/trips/{tripId}/stamps/orders", tripId) + .header( + HttpHeaders.AUTHORIZATION, + TokenFixture.TOKEN_PREFIX + token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(request))); + } + + @Test + @DisplayName("유효한 요청으로 스탬프의 순서를 수정한다") + void shouldUpdateStampOrders() throws Exception { + // given + UpdateStampOrderRequest request = + new UpdateStampRequestFixture() + .withOrderedStampIds( + List.of(courseStamp2.getId(), courseStamp1.getId())) + .buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") + void shouldThrowExceptionWhenUnauthenticated() throws Exception { + // given + UpdateStampOrderRequest request = updateStampRequestFixture.buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions("", courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenTripIdTypeMismatch() throws Exception { + // given + String tripId = "abc"; + UpdateStampOrderRequest request = updateStampRequestFixture.buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, tripId, request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("유효하지 않은 여행 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidTripId() throws Exception { + // given + Long tripId = 10000L; + UpdateStampOrderRequest request = updateStampRequestFixture.buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, tripId, request); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 사용자가 여행의 소유자가 아닐 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenNotTripOwner() throws Exception { + // given + Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); + Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); + UpdateStampOrderRequest request = updateStampRequestFixture.buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, newTrip.getId(), request); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); + } + + @Test + @DisplayName("삭제된 여행일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedTrip() throws Exception { + // given + Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); + UpdateStampOrderRequest request = updateStampRequestFixture.buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, deleted.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + TripErrorCode.TRIP_ALREADY_DELETED + .getStatus() + .value())); + } + + @Test + @DisplayName("탐험형 여행이지만 스탬프 순서 변경을 요청한 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenRequestUpdateStampOrderForExploreTrip() throws Exception { + // given + Stamp exploreStamp = stampTestHelper.saveStamp(exploreTrip, 0); + UpdateStampOrderRequest request = updateStampRequestFixture.buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, exploreTrip.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode + .CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP + .getStatus() + .value())); + } + + @Test + @DisplayName("순서 변경을 요청한 ID 리스트에 존재하지 않는 스탬프가 있는 경우 400 예외가 발생한다") + void shouldThrow400WhenStampIdInUpdateOrderRequestIsInvalid() throws Exception { + // given + UpdateStampOrderRequest request = + updateStampRequestFixture + .withOrderedStampIds(List.of(100L, 200L)) + .buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.INVALID_STAMP_ID_IN_REQUEST + .getStatus() + .value())); + } + + @Test + @DisplayName("요청한 여행에 속한 스탬프가 아닌 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenStampTripMisMatch() throws Exception { + // given + Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); + UpdateStampOrderRequest request = + updateStampRequestFixture + .withOrderedStampIds( + List.of(courseStamp1.getId(), courseStamp2.getId())) + .buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, newTrip.getId(), request); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_NOT_BELONG_TO_TRIP + .getStatus() + .value())); + } + + @Test + @DisplayName("삭제된 스탬프일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedStamp() throws Exception { + // given + Stamp newStamp = stampTestHelper.saveDeletedStamp(courseTrip, NEXT_STAMP_ORDER); + UpdateStampOrderRequest request = + updateStampRequestFixture + .withOrderedStampIds( + List.of( + newStamp.getId(), + courseStamp2.getId(), + courseStamp1.getId())) + .buildUpdateOrders(); + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), request); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_ALREADY_DELETED + .getStatus() + .value())); + } + } + } + + @Nested + @DisplayName("스탬프 삭제 API") + class DeleteStamp { + private ResultActions getResultActions(String token, Object tripId, Object stampId) + throws Exception { + return mockMvc.perform( + delete("/api/trips/{tripId}/stamps/{stampId}", tripId, stampId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.TOKEN_PREFIX + token)); + } + + @Test + @DisplayName("여행의 특정 스탬프를 삭제하고, 여행의 총 스탬프 수가 감소한다") + void shouldDeleteStamp() throws Exception { + // when + ResultActions resultActions = + getResultActions(token, courseTrip.getId(), courseStamp1.getId()); + + // then + resultActions.andExpect(status().isOk()).andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") + void shouldThrowExceptionWhenUnauthenticated() throws Exception { + // when + ResultActions resultActions = + getResultActions("", courseTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenTripIdTypeMismatch() throws Exception { + // given + String tripId = "abc"; + + // when + ResultActions resultActions = getResultActions(token, tripId, courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenStampIdTypeMismatch() throws Exception { + // given + String stampId = "abc"; + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), stampId); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("유효하지 않은 여행 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidTripId() throws Exception { + // given + Long tripId = 10000L; + + // when + ResultActions resultActions = getResultActions(token, tripId, courseStamp1.getId()); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 사용자가 여행의 소유자가 아닐 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenNotTripOwner() throws Exception { + // given + Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); + Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); + + // when + ResultActions resultActions = + getResultActions(token, newTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); + } + + @Test + @DisplayName("삭제된 여행일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedTrip() throws Exception { + // given + Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); + + // when + ResultActions resultActions = + getResultActions(token, deleted.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())); + } + + @Test + @DisplayName("유효하지 않은 스탬프 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidStampId() throws Exception { + // given + Long stampId = 10000L; + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), stampId); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(StampErrorCode.STAMP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 여행에 속한 스탬프가 아닌 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenStampTripMisMatch() throws Exception { + // given + Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); + + // when + ResultActions resultActions = + getResultActions(token, newTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_NOT_BELONG_TO_TRIP + .getStatus() + .value())); + } + } + + @Nested + @DisplayName("스탬프 목록 조회 API") + class ListStamps { + private ResultActions getResultActions(String token, Object tripId) throws Exception { + return mockMvc.perform( + get("/api/trips/{tripId}/stamps", tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.TOKEN_PREFIX + token)); + } + + @Test + @DisplayName("여행 ID로 여행에 속한 스탬프 목록을 조회하고 반환한다") + void shouldGetStampsByTripIdReturnStamps() throws Exception { + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId()); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isNotEmpty()); + } + + @Test + @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") + void shouldThrowExceptionWhenUnauthenticated() 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("PathVariable 여행 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenTripIdTypeMismatch() throws Exception { + // given + String tripId = "abc"; + + // when + ResultActions resultActions = getResultActions(token, tripId); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("유효하지 않은 여행 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidTripId() throws Exception { + // given + Long tripId = 10000L; + + // when + ResultActions resultActions = getResultActions(token, tripId); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 사용자가 여행의 소유자가 아닐 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenNotTripOwner() throws Exception { + // given + Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); + Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); + + // when + ResultActions resultActions = getResultActions(token, newTrip.getId()); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); + } + + @Test + @DisplayName("삭제된 여행일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedTrip() throws Exception { + // given + Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); + + // when + ResultActions resultActions = getResultActions(token, deleted.getId()); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())); + } + } + + @Nested + @DisplayName("스탬프 상세 조회 API") + class GetStamp { + private ResultActions getResultActions(String token, Object tripId, Object stampId) + throws Exception { + return mockMvc.perform( + get("/api/trips/{tripId}/stamps/{stampId}", tripId, stampId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.TOKEN_PREFIX + token)); + } + + @Test + @DisplayName("특정 스탬프 ID로 조회하고 스탬프 정보를 반환한다") + void shouldGetStampReturnStampInfo() throws Exception { + // when + ResultActions resultActions = + getResultActions(token, courseTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isNotEmpty()); + } + + @Test + @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") + void shouldThrowExceptionWhenUnauthenticated() throws Exception { + // when + ResultActions resultActions = + getResultActions("", courseTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenTripIdTypeMismatch() throws Exception { + // given + String tripId = "abc"; + + // when + ResultActions resultActions = getResultActions(token, tripId, courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 예외가 발생한다") + void shouldThrowExceptionWhenStampIdTypeMismatch() throws Exception { + // given + String stampId = "abc"; + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), stampId); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH + .getStatus() + .value())); + } + + @Test + @DisplayName("유효하지 않은 여행 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidTripId() throws Exception { + // given + Long tripId = 10000L; + + // when + ResultActions resultActions = getResultActions(token, tripId, courseStamp1.getId()); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 사용자가 여행의 소유자가 아닐 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenNotTripOwner() throws Exception { + // given + Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); + Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); + + // when + ResultActions resultActions = + getResultActions(token, newTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); + } + + @Test + @DisplayName("삭제된 여행일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedTrip() throws Exception { + // given + Trip deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); + + // when + ResultActions resultActions = + getResultActions(token, deletedTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())); + } + + @Test + @DisplayName("유효하지 않은 스탬프 ID 라면 404 예외가 발생한다") + void shouldThrowExceptionWhenInvalidStampId() throws Exception { + // given + Long stampId = 10000L; + + // when + ResultActions resultActions = getResultActions(token, courseTrip.getId(), stampId); + + // when & then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value(StampErrorCode.STAMP_NOT_FOUND.getStatus().value())); + } + + @Test + @DisplayName("요청한 스탬프가 해당 여행에 속해있지 않을 경우 403 예외가 발생한다") + void shouldThrowExceptionWhenStampTripMisMatch() throws Exception { + // given + Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); + + // when + ResultActions resultActions = + getResultActions(token, newTrip.getId(), courseStamp1.getId()); + + // then + resultActions + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_NOT_BELONG_TO_TRIP + .getStatus() + .value())); + } + + @Test + @DisplayName("삭제된 스탬프일 경우 400 예외가 발생한다") + void shouldThrowExceptionWhenAlreadyDeletedStamp() throws Exception { + // given + Stamp deletedStamp = stampTestHelper.saveDeletedStamp(exploreTrip, 0); + + // when + ResultActions resultActions = + getResultActions(token, exploreTrip.getId(), deletedStamp.getId()); + + // then + resultActions + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect( + jsonPath("$.status") + .value( + StampErrorCode.STAMP_ALREADY_DELETED + .getStatus() + .value())); + } + } +} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java index c3dd8c5..cb45bfe 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/TripServiceTest.java @@ -49,7 +49,7 @@ public class TripServiceTest extends BaseUnitTest { @BeforeEach void setup() { member = MemberFixture.createMemberFromKakaoWithId(1L); - trip = TripFixture.createTripWithId(1L, member); + trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE); } @Nested @@ -199,6 +199,32 @@ void shouldThrowExceptionWhenAlreadyDeleted() { .isInstanceOf(CustomException.class) .hasMessageContaining(TripErrorCode.TRIP_ALREADY_DELETED.getMessage()); } + + @Test + @DisplayName("여행의 총 스탬프 수를 +1 증가시킨다") + void shouldIncreaseTotalStamps() { + // given + int tripTotalStamps = trip.getTotalStamps(); + + // when + trip.increaseTotalStamps(); + + // then + assertThat(trip.getTotalStamps()).isEqualTo(tripTotalStamps + 1); + } + + @Test + @DisplayName("여행의 총 스탬프 수를 -1 감소시킨다") + void shouldDecreaseTotalStamps() { + // given + int tripTotalStamps = trip.getTotalStamps(); + + // when + trip.decreaseTotalStamps(); + + // then + assertThat(trip.getTotalStamps()).isEqualTo(tripTotalStamps - 1); + } } @Nested diff --git a/src/test/java/com/ject/studytrip/trip/fixture/TripFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/TripFixture.java index e8900b4..a23960c 100644 --- a/src/test/java/com/ject/studytrip/trip/fixture/TripFixture.java +++ b/src/test/java/com/ject/studytrip/trip/fixture/TripFixture.java @@ -19,15 +19,10 @@ public static Trip createTrip(Member member, TripCategory category) { member, TRIP_NAME, TRIP_MEMO, category, TRIP_END_DATE, TRIP_TOTAL_STAMPS); } - public static Trip createTripWithId(Long id, Member member) { + public static Trip createTripWithId(Long id, Member member, TripCategory category) { Trip trip = TripFactory.create( - member, - TRIP_NAME, - TRIP_MEMO, - TRIP_CATEGORY_COURSE, - TRIP_END_DATE, - TRIP_TOTAL_STAMPS); + member, TRIP_NAME, TRIP_MEMO, category, TRIP_END_DATE, TRIP_TOTAL_STAMPS); ReflectionTestUtils.setField(trip, "id", id); return trip;