diff --git a/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java b/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java index 03ccec7..2c4a868 100644 --- a/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java +++ b/src/main/java/com/ject/studytrip/member/application/facade/MemberFacade.java @@ -81,7 +81,7 @@ public void deleteMember(Long memberId) { @Transactional(readOnly = true) public MemberDetail getMemberDetail(Long memberId) { Member member = memberQueryService.getValidMember(memberId); - TripCount tripCount = tripQueryService.getActiveTripCountsByMemberId(memberId); + TripCount tripCount = tripQueryService.getActiveTripCountByMemberId(memberId); long studyLogCount = studyLogQueryService.getActiveStudyLogCountByMemberId(memberId); MemberInfo memberInfo = MemberInfo.from(member); @@ -163,11 +163,11 @@ private void cascadeHardDeleteByMemberId(Long memberId) { pomodoroCommandService.hardDeletePomodorosOwnedByMember(memberId); studyLogCommandService.hardDeleteStudyLogsOwnedByMember(memberId); dailyMissionCommandService.hardDeleteDailyMissionsOwnedByMember(memberId); - dailyGoalCommandService.hardDeleteDailyGoalsByMember(memberId); + dailyGoalCommandService.hardDeleteDailyGoalsOwnedByMember(memberId); missionCommandService.hardDeleteMissionsOwnedByMember(memberId); - stampCommandService.hardDeleteStampsByMember(memberId); - tripCommandService.hardDeleteTripsByMember(memberId); + stampCommandService.hardDeleteStampsOwnedByMember(memberId); + tripCommandService.hardDeleteTripsOwnedByMember(memberId); memberCommandService.hardDeleteMemberById(memberId); } } diff --git a/src/main/java/com/ject/studytrip/member/presentation/dto/response/LoadMemberDetailResponse.java b/src/main/java/com/ject/studytrip/member/presentation/dto/response/LoadMemberDetailResponse.java index bb72fb5..8490a0a 100644 --- a/src/main/java/com/ject/studytrip/member/presentation/dto/response/LoadMemberDetailResponse.java +++ b/src/main/java/com/ject/studytrip/member/presentation/dto/response/LoadMemberDetailResponse.java @@ -22,8 +22,8 @@ public static LoadMemberDetailResponse of( memberInfo.nickname(), memberInfo.profileImage(), memberInfo.category(), - tripCount.course(), - tripCount.explore(), + tripCount.getCourse(), + tripCount.getExplore(), studyLogCount); } } 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 deleted file mode 100644 index 59b008c..0000000 --- a/src/main/java/com/ject/studytrip/stamp/application/dto/StampDetail.java +++ /dev/null @@ -1,10 +0,0 @@ -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/dto/StampInfo.java b/src/main/java/com/ject/studytrip/stamp/application/dto/StampInfo.java deleted file mode 100644 index a4b3190..0000000 --- a/src/main/java/com/ject/studytrip/stamp/application/dto/StampInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ject.studytrip.stamp.application.dto; - -import com.ject.studytrip.global.util.DateUtil; -import com.ject.studytrip.stamp.domain.model.Stamp; - -public record StampInfo( - Long stampId, - String stampName, - int stampOrder, - String endDate, - int totalMissions, - int completedMissions, - boolean completed, - String createdAt, - String updatedAt, - String deletedAt) { - public static StampInfo from(Stamp stamp) { - return new StampInfo( - stamp.getId(), - stamp.getName(), - stamp.getStampOrder(), - DateUtil.formatDate(stamp.getEndDate()), - stamp.getTotalMissions(), - stamp.getCompletedMissions(), - stamp.isCompleted(), - DateUtil.formatDateTime(stamp.getCreatedAt()), - DateUtil.formatDateTime(stamp.getUpdatedAt()), - DateUtil.formatDateTime(stamp.getDeletedAt())); - } -} diff --git a/src/main/java/com/ject/studytrip/stamp/application/dto/StampsInfo.java b/src/main/java/com/ject/studytrip/stamp/application/dto/StampsInfo.java deleted file mode 100644 index bf432d6..0000000 --- a/src/main/java/com/ject/studytrip/stamp/application/dto/StampsInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.stamp.application.dto; - -import java.util.List; - -public record StampsInfo(List stampsInfos) { - public static StampsInfo of(List stampsInfos) { - return new StampsInfo(stampsInfos); - } -} 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 deleted file mode 100644 index 937598f..0000000 --- a/src/main/java/com/ject/studytrip/stamp/application/facade/StampFacade.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.ject.studytrip.stamp.application.facade; - -import static com.ject.studytrip.global.common.constants.CacheNameConstants.*; - -import com.ject.studytrip.mission.application.dto.MissionInfo; -import com.ject.studytrip.mission.application.service.MissionCommandService; -import com.ject.studytrip.mission.application.service.MissionQueryService; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.stamp.application.dto.StampDetail; -import com.ject.studytrip.stamp.application.dto.StampInfo; -import com.ject.studytrip.stamp.application.dto.StampsInfo; -import com.ject.studytrip.stamp.application.service.StampCommandService; -import com.ject.studytrip.stamp.application.service.StampQueryService; -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.UpdateStampOrderRequest; -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest; -import com.ject.studytrip.trip.application.service.TripCommandService; -import com.ject.studytrip.trip.application.service.TripQueryService; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cache.annotation.Caching; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class StampFacade { - private final TripQueryService tripQueryService; - private final StampQueryService stampQueryService; - private final MissionQueryService missionQueryService; - - private final TripCommandService tripCommandService; - private final StampCommandService stampCommandService; - private final MissionCommandService missionCommandService; - - @Caching( - evict = { - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)"), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict(cacheNames = TRIPS, allEntries = true) - }) - @Transactional - public StampInfo createStamp(Long memberId, Long tripId, CreateStampRequest request) { - Trip trip = tripQueryService.getValidTrip(memberId, tripId); - int nextOrder = stampQueryService.getNextStampOrderByTrip(trip); - Stamp stamp = stampCommandService.createStamp(trip, nextOrder, request); - tripCommandService.increaseTotalStamps(trip); - - return StampInfo.from(stamp); - } - - @Caching( - evict = { - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)"), - @CacheEvict( - cacheNames = STAMP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)"), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict(cacheNames = TRIPS, allEntries = true) - }) - @Transactional - public void updateStamp(Long memberId, Long tripId, Long stampId, UpdateStampRequest request) { - Trip trip = tripQueryService.getValidTrip(memberId, tripId); - Stamp stamp = stampQueryService.getValidStamp(trip.getId(), stampId); - - stampCommandService.updateStamp(trip, stamp, request); - } - - @Caching( - evict = { - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)"), - @CacheEvict(cacheNames = STAMP, allEntries = true), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict(cacheNames = TRIPS, allEntries = true) - }) - @Transactional - public void updateStampOrders(Long memberId, Long tripId, UpdateStampOrderRequest request) { - Trip trip = tripQueryService.getValidTrip(memberId, tripId); - - stampCommandService.updateStampOrders(trip, request); - } - - @Caching( - evict = { - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)"), - @CacheEvict( - cacheNames = STAMP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)"), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict(cacheNames = TRIPS, allEntries = true) - }) - @Transactional - public void deleteStamp(Long memberId, Long tripId, Long stampId) { - Trip trip = tripQueryService.getValidTrip(memberId, tripId); - Stamp stamp = stampQueryService.getValidStamp(trip.getId(), stampId); - - stampCommandService.deleteStamp(stamp); - shiftStampOrdersIfTripCategoryIsCourse(trip, stamp.getStampOrder()); - tripCommandService.decreaseTotalStamps(trip); - } - - @Cacheable( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)") - @Transactional(readOnly = true) - public StampsInfo getStampsByTrip(Long memberId, Long tripId) { - Trip trip = tripQueryService.getValidTrip(memberId, tripId); - List stamps = stampQueryService.getStampsByTripId(trip.getId()); - - return StampsInfo.of(stamps.stream().map(StampInfo::from).toList()); - } - - @Cacheable( - cacheNames = STAMP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)") - @Transactional(readOnly = true) - public StampDetail getStamp(Long memberId, Long tripId, Long stampId) { - Trip trip = tripQueryService.getValidTrip(memberId, tripId); - Stamp stamp = stampQueryService.getValidStamp(trip.getId(), stampId); - List missions = missionQueryService.getMissionsByStampId(stamp.getId()); - - return StampDetail.from( - StampInfo.from(stamp), missions.stream().map(MissionInfo::from).toList()); - } - - @Caching( - evict = { - @CacheEvict( - cacheNames = STAMP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)"), - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)"), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict(cacheNames = TRIPS, allEntries = true) - }) - @Transactional - public void completeStamp(Long memberId, Long tripId, Long stampId) { - Trip trip = tripQueryService.getValidTrip(memberId, tripId); - Stamp stamp = stampQueryService.getValidStamp(trip.getId(), stampId); - - missionCommandService.validateAllMissionsCompletedByStampId(stamp.getId()); - - stampCommandService.completeStamp(stamp); - tripCommandService.increaseCompletedStamps(trip); - } - - private void shiftStampOrdersIfTripCategoryIsCourse(Trip trip, int stampOrder) { - if (trip.getCategory() != TripCategory.COURSE) return; - - List affectedStamps = - stampQueryService.getStampsToShiftAfterDeleted(trip.getId(), stampOrder); - - stampCommandService.shiftStampOrders(affectedStamps); - } -} diff --git a/src/main/java/com/ject/studytrip/stamp/application/service/StampCommandService.java b/src/main/java/com/ject/studytrip/stamp/application/service/StampCommandService.java deleted file mode 100644 index 30e974f..0000000 --- a/src/main/java/com/ject/studytrip/stamp/application/service/StampCommandService.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.ject.studytrip.stamp.application.service; - -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.StampCommandRepository; -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.UpdateStampOrderRequest; -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.time.LocalDate; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class StampCommandService { - private final StampRepository stampRepository; - private final StampCommandRepository stampCommandRepository; - - public Stamp createStamp(Trip trip, int nextOrder, CreateStampRequest request) { - Stamp stamp = StampFactory.create(trip, request.name(), nextOrder, request.endDate()); - StampPolicy.validateEndDate(trip.getEndDate(), stamp.getEndDate()); - - return stampRepository.save(stamp); - } - - public void createStamps(Trip trip, int nextOrder, List requests) { - if (requests == null || requests.isEmpty()) return; - - final List stamps = - switch (trip.getCategory()) { - // 탐험형 여행일 경우 - // order 0 으로 전부 고정 - case EXPLORE -> - requests.stream() - .map( - stamp -> - StampFactory.create( - trip, stamp.name(), 0, stamp.endDate())) - .toList(); - - // 코스형 여행일 경우 - // nextOrder 부터 1씩 증가하며 order 저장 - case COURSE -> { - int order = nextOrder; - - List stampList = new ArrayList<>(); - for (CreateStampRequest request : requests) { - Stamp stamp = - StampFactory.create( - trip, request.name(), order++, request.endDate()); - stampList.add(stamp); - } - - yield stampList; - } - }; - - stamps.forEach(stamp -> StampPolicy.validateEndDate(trip.getEndDate(), stamp.getEndDate())); - - stampRepository.saveAll(stamps); - } - - public void updateStamp(Trip trip, Stamp stamp, UpdateStampRequest request) { - stamp.updateName(request.name()); - - LocalDate endDate = request.endDate(); - StampPolicy.validateEndDate(trip.getEndDate(), endDate); - stamp.updateEndDate(endDate); - } - - public void updateStampOrders(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 updateStampOrdersByTripCategoryChange(Long tripId, TripCategory newCategory) { - List stamps = stampRepository.findAllByTripIdOrderByCreatedAtAsc(tripId); - - if (newCategory == TripCategory.EXPLORE) { - stamps.forEach(stamp -> stamp.updateStampOrder(0)); - return; - } - - int order = 1; - for (Stamp stamp : stamps) { - stamp.updateStampOrder(order++); - } - } - - public void deleteStamp(Stamp stamp) { - stamp.updateDeletedAt(); - } - - public void completeStamp(Stamp stamp) { - StampPolicy.validateCompleted(stamp); - - stamp.updateCompleted(); - } - - public void shiftStampOrders(List affectedStamps) { - for (Stamp stamp : affectedStamps) { - stamp.updateStampOrder(stamp.getStampOrder() - 1); - } - } - - public void validateStampBelongsToTrip(Long tripId, Stamp stamp) { - StampPolicy.validateStampBelongsToTrip(tripId, stamp); - } - - public void validateAllStampsCompletedByTripId(Long tripId) { - boolean exists = - stampCommandRepository.existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(tripId); - - StampPolicy.validateAllCompleted(exists); - } - - public long hardDeleteStamps() { - return stampCommandRepository.deleteAllByDeletedAtIsNotNull(); - } - - public long hardDeleteStampsOwnedByDeletedTrip() { - return stampCommandRepository.deleteAllByDeletedTripOwner(); - } - - public void increaseTotalMissions(Stamp stamp) { - stamp.increaseTotalMissions(); - } - - public void decreaseTotalMissions(Stamp stamp) { - stamp.decreaseTotalMissions(); - } - - public void increaseCompletedMissions(Stamp stamp, int count) { - stamp.increaseCompletedMissions(count); - } - - public long hardDeleteStampsByMember(Long memberId) { - return stampCommandRepository.deleteAllByMemberId(memberId); - } -} diff --git a/src/main/java/com/ject/studytrip/stamp/application/service/StampQueryService.java b/src/main/java/com/ject/studytrip/stamp/application/service/StampQueryService.java deleted file mode 100644 index e2a22d4..0000000 --- a/src/main/java/com/ject/studytrip/stamp/application/service/StampQueryService.java +++ /dev/null @@ -1,95 +0,0 @@ -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.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.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.util.Comparator; -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; - -@Service -@RequiredArgsConstructor -public class StampQueryService { - private final StampRepository stampRepository; - private final StampQueryRepository stampQueryRepository; - - 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); - StampPolicy.validateCompleted(stamp); - - return stamp; - } - - public List getStampsByTripId(Long tripId) { - return stampRepository.findAllByTripIdAndDeletedAtIsNull(tripId); - } - - public Stamp getFirstInCompleteStampForCourseTrip(Long tripId) { - return stampQueryRepository - .findFirstIncompleteStampByTripId(tripId) - .orElseThrow(() -> new CustomException(StampErrorCode.STAMP_NOT_FOUND)); - } - - public String getStampNameByTripCategory(TripCategory tripCategory, List stamps) { - // 스탬프 목록이 비어있지 않은지 검증 - StampPolicy.validateStampListNotEmpty(stamps); - stamps.forEach(StampPolicy::validateNotDeleted); - - if (tripCategory == TripCategory.COURSE) { - // 코스형 여행은 상위 검증에서 동일한 스탬프인지 검증이 완료된 상태이므로 - // 스탬프 리스트에서 첫 번째 스탬프의 이름을 추출해도 안전 - return stamps.get(0).getName(); - } - - return getExplorationStampName(stamps); - } - - public int getNextStampOrderByTrip(Trip trip) { - if (trip.getCategory() != TripCategory.COURSE) { - return 0; - } - - return stampQueryRepository.findNextStampOrderByTripId(trip.getId()); - } - - public List getStampsToShiftAfterDeleted(Long tripId, int deletedStampOrder) { - return stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedStampOrder); - } - - private String getExplorationStampName(List stamps) { - // 스탬프별 개수 집계 - Map stampCountMap = - stamps.stream() - .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); - - // 최대 개수를 가진 스탬프들 찾기 - long maxCount = stampCountMap.values().stream().max(Long::compareTo).orElse(0L); - - List maxCountStamps = - stampCountMap.entrySet().stream() - .filter(entry -> entry.getValue().equals(maxCount)) - .map(Map.Entry::getKey) - .toList(); - - // 가장 이른 생성 시간을 가진 스탬프 선택 - return maxCountStamps.stream() - .min(Comparator.comparing(Stamp::getCreatedAt)) - .map(Stamp::getName) - .orElse(""); - } -} 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 deleted file mode 100644 index f8d32b0..0000000 --- a/src/main/java/com/ject/studytrip/stamp/domain/error/StampErrorCode.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.ject.studytrip.stamp.domain.error; - -import com.ject.studytrip.global.exception.error.ErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@RequiredArgsConstructor -public enum StampErrorCode implements ErrorCode { - // 400 - INVALID_STAMP_ORDER_FOR_EXPLORATION_TRIP( - HttpStatus.BAD_REQUEST, "탐험형 여행에서는 스탬프 순서를 지정할 수 없으며, 항상 0이여야 합니다. "), - 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가 포함되어 있습니다. "), - STAMP_LIST_CANNOT_BE_EMPTY(HttpStatus.BAD_REQUEST, "스탬프 목록은 비어있을 수 없습니다."), - STAMP_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "이미 완료된 스탬프입니다."), - ALL_STAMPS_NOT_COMPLETED(HttpStatus.BAD_REQUEST, "모든 스탬프가 완료되지 않았습니다."), - STAMP_END_DATE_CANNOT_BE_IN_PAST(HttpStatus.BAD_REQUEST, "스탬프의 종료일은 과거일 수 없습니다."), - STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED( - HttpStatus.BAD_REQUEST, "스탬프의 종료일은 여행 종료일보다 이후일 수 없습니다."), - - // 403 - STAMP_NOT_BELONGS_TO_TRIP(HttpStatus.FORBIDDEN, "해당 스탬프는 요청한 여행에 속하지 않습니다."), - - // 404 - STAMP_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 스탬프가 존재하지 않습니다."), - ; - - private final HttpStatus status; - private final String message; - - @Override - public String getName() { - return this.name(); - } - - @Override - public HttpStatus getStatus() { - return this.status; - } - - @Override - public String getMessage() { - return this.message; - } -} diff --git a/src/main/java/com/ject/studytrip/stamp/domain/factory/StampFactory.java b/src/main/java/com/ject/studytrip/stamp/domain/factory/StampFactory.java deleted file mode 100644 index 1e8a0b0..0000000 --- a/src/main/java/com/ject/studytrip/stamp/domain/factory/StampFactory.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ject.studytrip.stamp.domain.factory; - -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.domain.model.Trip; -import java.time.LocalDate; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class StampFactory { - public static Stamp create(Trip trip, String name, int stampOrder, LocalDate endDate) { - return Stamp.of(trip, name, stampOrder, endDate); - } -} 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 deleted file mode 100644 index 8edd110..0000000 --- a/src/main/java/com/ject/studytrip/stamp/domain/policy/StampPolicy.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.ject.studytrip.stamp.domain.policy; - -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.stamp.domain.error.StampErrorCode; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.time.LocalDate; -import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@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_BELONGS_TO_TRIP); - } - - public static void validateNotDeleted(Stamp stamp) { - if (stamp.getDeletedAt() != null) - throw new CustomException(StampErrorCode.STAMP_ALREADY_DELETED); - } - - 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); - } - - public static void validateStampListNotEmpty(List stamps) { - if (stamps.isEmpty()) { - throw new CustomException(StampErrorCode.STAMP_LIST_CANNOT_BE_EMPTY); - } - } - - public static void validateCompleted(Stamp stamp) { - if (stamp.isCompleted()) { - throw new CustomException(StampErrorCode.STAMP_ALREADY_COMPLETED); - } - } - - public static void validateAllCompleted(boolean exists) { - if (exists) { - throw new CustomException(StampErrorCode.ALL_STAMPS_NOT_COMPLETED); - } - } - - public static void validateEndDate(LocalDate tripEndDate, LocalDate stampEndDate) { - if (stampEndDate == null) return; - - // 스탬프 종료일이 과거일 경우 - LocalDate today = LocalDate.now(); - if (stampEndDate.isBefore(today)) { - throw new CustomException(StampErrorCode.STAMP_END_DATE_CANNOT_BE_IN_PAST); - } - - if (tripEndDate == null) return; - - // 스탬프 종료일이 여행 종료일 이후일 경우 - if (stampEndDate.isAfter(tripEndDate)) { - throw new CustomException( - StampErrorCode.STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED); - } - } -} 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 deleted file mode 100644 index 8aef203..0000000 --- a/src/main/java/com/ject/studytrip/stamp/domain/repository/StampRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.ject.studytrip.stamp.domain.repository; - -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); - - Optional findById(Long stampId); - - List findAllByIdIn(List ids); - - List findAllByTripIdAndDeletedAtIsNull(Long tripId); - - List findAllByTripIdOrderByCreatedAtAsc(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 deleted file mode 100644 index 5b1a017..0000000 --- a/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ject.studytrip.stamp.infra.jpa; - -import com.ject.studytrip.stamp.domain.model.Stamp; -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface StampJpaRepository extends JpaRepository { - List findAllByIdIn(List ids); - - List findAllByTripIdAndDeletedAtIsNull(Long tripId); - - List findAllByTripIdOrderByCreatedAtAsc(Long tripId); -} 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 deleted file mode 100644 index 239c180..0000000 --- a/src/main/java/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.ject.studytrip.stamp.infra.jpa; - -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; - -@Repository -@RequiredArgsConstructor -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 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 - public List findAllByTripIdOrderByCreatedAtAsc(Long tripId) { - return stampJpaRepository.findAllByTripIdOrderByCreatedAtAsc(tripId); - } -} 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 deleted file mode 100644 index 5a3c18f..0000000 --- a/src/main/java/com/ject/studytrip/stamp/presentation/controller/StampController.java +++ /dev/null @@ -1,132 +0,0 @@ -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.dto.StampsInfo; -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.UpdateStampOrderRequest; -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest; -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 -@RequestMapping("/api/trips") -@RequiredArgsConstructor -@Validated -public class StampController { - private final StampFacade stampFacade; - - @Operation(summary = "스탬프 등록", description = "특정 여행에 새로운 스탬프를 등록합니다.") - @PostMapping("/{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 = - """ - 특정 스탬프의 이름과 종료일을 수정합니다. - - - 이름과 종료일 중 변경하지 않는 필드는 요청 바디에서 생략해도 됩니다. - - 종료일을 '없음'으로 변경하려면 `endDate: null`로 명시적으로 전달해야 합니다. - """) - @PatchMapping("/{tripId}/stamps/{stampId}") - public ResponseEntity updateStamp( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") Long stampId, - @RequestBody @Valid UpdateStampRequest request) { - stampFacade.updateStamp(Long.valueOf(memberId), tripId, stampId, request); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } - - @Operation(summary = "스탬프 순서 변경", description = "스탬프 순서를 변경합니다. 스탬프 ID 목록을 최종 순서대로 요청합니다.") - @PutMapping("/{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("/{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("/{tripId}/stamps") - public ResponseEntity loadStampsByTrip( - @AuthenticationPrincipal String memberId, @PathVariable Long tripId) { - StampsInfo result = stampFacade.getStampsByTrip(Long.valueOf(memberId), tripId); - List response = - result.stampsInfos().stream().map(LoadStampInfoResponse::of).toList(); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), response)); - } - - @Operation(summary = "스탬프 상세 조회", description = "특정 여행의 특정 스탬프 상세 정보를 조회합니다.") - @GetMapping("/{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()))); - } - - @Operation(summary = "스탬프 완료", description = "특정 스탬프 하위의 모든 미션이 완료된 경우에만 스탬프를 완료합니다.") - @PatchMapping("/{tripId}/stamps/{stampId}/complete") - public ResponseEntity completeStamp( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") Long stampId) { - stampFacade.completeStamp(Long.valueOf(memberId), tripId, stampId); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } -} diff --git a/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/CreateStampRequest.java b/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/CreateStampRequest.java deleted file mode 100644 index 0c45d86..0000000 --- a/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/CreateStampRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ject.studytrip.stamp.presentation.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.FutureOrPresent; -import jakarta.validation.constraints.NotEmpty; -import java.time.LocalDate; - -public record CreateStampRequest( - @Schema(description = "스탬프 이름") @NotEmpty(message = "스탬프 이름은 필수 요청 값입니다.") String name, - @Schema(description = "스탬프 종료일") @FutureOrPresent(message = "스탬프 종료일은 현재 날짜보다 과거일 수 없습니다.") - LocalDate endDate) {} 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 deleted file mode 100644 index f7fa06e..0000000 --- a/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -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/request/UpdateStampRequest.java b/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampRequest.java deleted file mode 100644 index 02428e7..0000000 --- a/src/main/java/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -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 UpdateStampRequest( - @Schema(description = "수정할 스탬프 이름") String name, - @Schema(description = "수정할 스탬프 종료일") - @FutureOrPresent(message = "스탬프 종료일은 현재 날짜보다 과거일 수 없습니다.") - LocalDate endDate) {} 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 deleted file mode 100644 index b358b1f..0000000 --- a/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 29476e6..0000000 --- a/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -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 endDate, - @Schema(description = "스탬프에 속한 총 미션 수") int totalMissions, - @Schema(description = "스탬프에 속한 완료된 미션 수") int completedMissions, - @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.endDate(), - stampInfo.totalMissions(), - stampInfo.completedMissions(), - 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 deleted file mode 100644 index 3a425a4..0000000 --- a/src/main/java/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -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 endDate, - @Schema(description = "스탬프에 속한 총 미션 개수") int totalMissions, - @Schema(description = "스탬프에 속한 완료된 미션 개수") int completedMissions, - @Schema(description = "스탬프 완료 여부") boolean completed) { - public static LoadStampInfoResponse of(StampInfo info) { - return new LoadStampInfoResponse( - info.stampId(), - info.stampName(), - info.stampOrder(), - info.endDate(), - info.totalMissions(), - info.completedMissions(), - info.completed()); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/DailyGoalDetail.java b/src/main/java/com/ject/studytrip/trip/application/dto/DailyGoalDetail.java deleted file mode 100644 index fc66f22..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/DailyGoalDetail.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.mission.application.dto.DailyMissionInfo; -import com.ject.studytrip.pomodoro.application.dto.PomodoroInfo; -import java.util.List; - -public record DailyGoalDetail( - DailyGoalInfo dailyGoalInfo, - PomodoroInfo pomodoroInfo, - List dailyMissionInfos) { - public static DailyGoalDetail from( - DailyGoalInfo dailyGoalInfo, - PomodoroInfo pomodoroInfo, - List dailyMissionInfos) { - return new DailyGoalDetail(dailyGoalInfo, pomodoroInfo, dailyMissionInfos); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/DailyGoalInfo.java b/src/main/java/com/ject/studytrip/trip/application/dto/DailyGoalInfo.java deleted file mode 100644 index 6f9e37f..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/DailyGoalInfo.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.global.util.DateUtil; -import com.ject.studytrip.trip.domain.model.DailyGoal; - -public record DailyGoalInfo( - Long dailyGoalId, - String title, - boolean completed, - String createdAt, - String updatedAt, - String deletedAt) { - public static DailyGoalInfo from(DailyGoal dailyGoal) { - return new DailyGoalInfo( - dailyGoal.getId(), - dailyGoal.getTitle(), - dailyGoal.isCompleted(), - DateUtil.formatDateTime(dailyGoal.getCreatedAt()), - DateUtil.formatDateTime(dailyGoal.getUpdatedAt()), - DateUtil.formatDateTime(dailyGoal.getDeletedAt())); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripCategoryInfo.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripCategoryInfo.java deleted file mode 100644 index 6edcb7c..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripCategoryInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.trip.domain.model.TripCategory; - -public record TripCategoryInfo(String name, String value) { - public static TripCategoryInfo from(TripCategory category) { - return new TripCategoryInfo(category.name(), category.getValue()); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripCount.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripCount.java deleted file mode 100644 index c5d5082..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripCount.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -public record TripCount(long course, long explore) { - public static TripCount of(long course, long explore) { - return new TripCount(course, explore); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripDetail.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripDetail.java deleted file mode 100644 index e960b89..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripDetail.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.stamp.application.dto.StampInfo; -import java.util.List; - -public record TripDetail(TripInfo tripInfo, List stampInfos) { - public static TripDetail from(TripInfo tripInfo, List stampInfos) { - return new TripDetail(tripInfo, stampInfos); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripInfo.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripInfo.java deleted file mode 100644 index 29fa0d1..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripInfo.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import com.ject.studytrip.global.util.DateUtil; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; - -public record TripInfo( - Long tripId, - String tripName, - String tripMemo, - TripCategory tripCategory, - String startDate, - String endDate, - Integer dDay, - int totalStamps, - int completedStamps, - Integer progress, - boolean completed, - String createdAt, - String updatedAt, - String deletedAt) { - public static TripInfo from(Trip trip, Integer dDay, Integer progress) { - return new TripInfo( - trip.getId(), - trip.getName(), - trip.getMemo(), - trip.getCategory(), - DateUtil.formatDate(trip.getStartDate()), - DateUtil.formatDate(trip.getEndDate()), - dDay, - trip.getTotalStamps(), - trip.getCompletedStamps(), - progress, - trip.isCompleted(), - DateUtil.formatDateTime(trip.getCreatedAt()), - DateUtil.formatDateTime(trip.getUpdatedAt()), - DateUtil.formatDateTime(trip.getDeletedAt())); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/dto/TripSliceInfo.java b/src/main/java/com/ject/studytrip/trip/application/dto/TripSliceInfo.java deleted file mode 100644 index 57fa8c4..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/dto/TripSliceInfo.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.trip.application.dto; - -import java.util.List; - -public record TripSliceInfo(List tripInfos, boolean hasNext) { - public static TripSliceInfo of(List tripInfos, boolean hasNext) { - return new TripSliceInfo(tripInfos, hasNext); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/facade/DailyGoalFacade.java b/src/main/java/com/ject/studytrip/trip/application/facade/DailyGoalFacade.java deleted file mode 100644 index bbfd71a..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/facade/DailyGoalFacade.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.ject.studytrip.trip.application.facade; - -import static com.ject.studytrip.global.common.constants.CacheNameConstants.DAILY_GOAL; - -import com.ject.studytrip.member.application.service.MemberQueryService; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.mission.application.dto.DailyMissionInfo; -import com.ject.studytrip.mission.application.service.DailyMissionCommandService; -import com.ject.studytrip.mission.application.service.DailyMissionQueryService; -import com.ject.studytrip.mission.application.service.MissionCommandService; -import com.ject.studytrip.mission.application.service.MissionQueryService; -import com.ject.studytrip.mission.domain.model.DailyMission; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.pomodoro.application.dto.PomodoroInfo; -import com.ject.studytrip.pomodoro.application.service.PomodoroCommandService; -import com.ject.studytrip.pomodoro.application.service.PomodoroQueryService; -import com.ject.studytrip.pomodoro.domain.model.Pomodoro; -import com.ject.studytrip.stamp.application.service.StampCommandService; -import com.ject.studytrip.stamp.application.service.StampQueryService; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.application.dto.DailyGoalDetail; -import com.ject.studytrip.trip.application.dto.DailyGoalInfo; -import com.ject.studytrip.trip.application.service.DailyGoalCommandService; -import com.ject.studytrip.trip.application.service.DailyGoalQueryService; -import com.ject.studytrip.trip.application.service.TripQueryService; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class DailyGoalFacade { - private final MemberQueryService memberQueryService; - private final TripQueryService tripQueryService; - private final StampQueryService stampQueryService; - private final DailyMissionQueryService dailyMissionQueryService; - private final DailyGoalQueryService dailyGoalQueryService; - private final PomodoroQueryService pomodoroQueryService; - - private final StampCommandService stampCommandService; - private final MissionQueryService missionQueryService; - private final MissionCommandService missionCommandService; - private final DailyMissionCommandService dailyMissionCommandService; - private final DailyGoalCommandService dailyGoalCommandService; - private final PomodoroCommandService pomodoroCommandService; - - @Transactional - public DailyGoalInfo createDailyGoal( - Long memberId, Long tripId, CreateDailyGoalRequest request) { - Trip trip = getValidTripOwnedByMember(memberId, tripId); - List missions = getValidMissionsByTripCategory(trip, request.missionIds()); - - // 스탬프 이름을 추출해 title 설정 - String title = determineTitleByStamps(trip.getCategory(), missions); - DailyGoal dailyGoal = dailyGoalCommandService.createDailyGoal(trip, title); - - dailyMissionCommandService.createDailyMissions(dailyGoal, missions); - pomodoroCommandService.createPomodoro(dailyGoal, request.pomodoro()); - - return DailyGoalInfo.from(dailyGoal); - } - - @CacheEvict( - cacheNames = DAILY_GOAL, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).dailyGoal(#memberId, #tripId, #dailyGoalId)") - @Transactional - public void updateDailyGoal( - Long memberId, Long tripId, Long dailyGoalId, UpdateDailyGoalRequest request) { - Trip trip = getValidTripOwnedByMember(memberId, tripId); - DailyGoal dailyGoal = dailyGoalQueryService.getValidDailyGoal(trip.getId(), dailyGoalId); - - // 삭제할 데일리 미션이 있을 경우 - if (request.deleteDailyMissionIds() != null && !request.deleteDailyMissionIds().isEmpty()) { - List deleteDailyMissions = - dailyMissionQueryService.getValidDailyMissionsByIds( - dailyGoal.getId(), request.deleteDailyMissionIds()); - deleteDailyMissions.forEach(dailyMissionCommandService::deleteDailyMission); - } - - // 새로 추가할 미션이 있을 경우 - if (request.addMissionIds() != null && !request.addMissionIds().isEmpty()) { - List addMissions = - getValidMissionsByTripCategory(trip, request.addMissionIds()); - dailyMissionCommandService.createDailyMissions(dailyGoal, addMissions); - } - } - - @CacheEvict( - cacheNames = DAILY_GOAL, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).dailyGoal(#memberId, #tripId, #dailyGoalId)") - @Transactional - public void deleteDailyGoal(Long memberId, Long tripId, Long dailyGoalId) { - Trip trip = getValidTripOwnedByMember(memberId, tripId); - DailyGoal dailyGoal = dailyGoalQueryService.getValidDailyGoal(trip.getId(), dailyGoalId); - Pomodoro pomodoro = pomodoroQueryService.getValidPomodoroByDailyGoalId(dailyGoal.getId()); - - // 뽀모도로 삭제 - pomodoroCommandService.deletePomodoro(pomodoro); - - // 데일리 미션 삭제 - List dailyMissions = - dailyMissionQueryService.getDailyMissionsByDailyGoalId(dailyGoal.getId()); - for (DailyMission dailyMission : dailyMissions) { - dailyMissionCommandService.deleteDailyMission(dailyMission); - } - - // 데일리 목표 삭제 - dailyGoalCommandService.deleteDailyGoal(dailyGoal); - } - - @Cacheable( - cacheNames = DAILY_GOAL, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).dailyGoal(#memberId, #tripId, #dailyGoalId)") - @Transactional(readOnly = true) - public DailyGoalDetail getDailyGoal(Long memberId, Long tripId, Long dailyGoalId) { - Trip trip = getValidTripOwnedByMember(memberId, tripId); - DailyGoal dailyGoal = dailyGoalQueryService.getValidDailyGoal(trip.getId(), dailyGoalId); - Pomodoro pomodoro = pomodoroQueryService.getValidPomodoroByDailyGoalId(dailyGoal.getId()); - List dailyMissions = - dailyMissionQueryService.getDailyMissionsByDailyGoalId(dailyGoal.getId()); - - return DailyGoalDetail.from( - DailyGoalInfo.from(dailyGoal), - PomodoroInfo.from(pomodoro), - dailyMissions.stream().map(DailyMissionInfo::from).toList()); - } - - private Trip getValidTripOwnedByMember(Long memberId, Long tripId) { - Member member = memberQueryService.getValidMember(memberId); - - return tripQueryService.getValidTrip(member.getId(), tripId); - } - - private List getValidMissionsByTripCategory(Trip trip, List missionIds) { - List missions = missionQueryService.getValidMissionsByIds(missionIds); - - for (Mission mission : missions) { - stampCommandService.validateStampBelongsToTrip(trip.getId(), mission.getStamp()); - } - - // 코스형 여행일 경우, 현재 진행중인 스탬프를 조회하고 요청한 미션들이 해당 스탬프에 속해 있는 미션들인지 검증 - if (trip.getCategory() == TripCategory.COURSE) { - Long currentStampId = - stampQueryService.getFirstInCompleteStampForCourseTrip(trip.getId()).getId(); - - missionCommandService.validateMissionsBelongsToStamp(currentStampId, missions); - } - - return missions; - } - - private String determineTitleByStamps(TripCategory tripCategory, List missions) { - List stamps = missions.stream().map(Mission::getStamp).toList(); - - return stampQueryService.getStampNameByTripCategory(tripCategory, stamps); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/facade/TripFacade.java b/src/main/java/com/ject/studytrip/trip/application/facade/TripFacade.java deleted file mode 100644 index 0bce38b..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/facade/TripFacade.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.ject.studytrip.trip.application.facade; - -import static com.ject.studytrip.global.common.constants.CacheNameConstants.*; - -import com.ject.studytrip.member.application.service.MemberQueryService; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.stamp.application.dto.StampInfo; -import com.ject.studytrip.stamp.application.service.StampCommandService; -import com.ject.studytrip.stamp.application.service.StampQueryService; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.trip.application.dto.TripCategoryInfo; -import com.ject.studytrip.trip.application.dto.TripDetail; -import com.ject.studytrip.trip.application.dto.TripInfo; -import com.ject.studytrip.trip.application.dto.TripSliceInfo; -import com.ject.studytrip.trip.application.service.TripCommandService; -import com.ject.studytrip.trip.application.service.TripQueryService; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest; -import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cache.annotation.Caching; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class TripFacade { - private final MemberQueryService memberQueryService; - private final TripQueryService tripQueryService; - private final StampQueryService stampQueryService; - - private final TripCommandService tripCommandService; - private final StampCommandService stampCommandService; - - @Transactional(readOnly = true) - public List loadTripCategories() { - return Arrays.stream(TripCategory.values()).map(TripCategoryInfo::from).toList(); - } - - @CacheEvict(cacheNames = TRIPS, allEntries = true) - @Transactional - public TripInfo createTrip(Long memberId, CreateTripRequest request) { - Member member = memberQueryService.getValidMember(memberId); - Trip trip = tripCommandService.createTrip(member, request); - int nextOrder = stampQueryService.getNextStampOrderByTrip(trip); - stampCommandService.createStamps(trip, nextOrder, request.stamps()); - - return TripInfo.from(trip, null, null); - } - - @Caching( - evict = { - @CacheEvict(cacheNames = TRIPS, allEntries = true), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)"), - @CacheEvict(cacheNames = STAMP, allEntries = true) - }) - @Transactional - public void updateTrip(Long memberId, Long tripId, UpdateTripRequest request) { - Member member = memberQueryService.getValidMember(memberId); - Trip trip = tripQueryService.getValidTrip(member.getId(), tripId); - - tripCommandService.updateTrip(trip, request); - - if (request.category() != null) - stampCommandService.updateStampOrdersByTripCategoryChange( - trip.getId(), TripCategory.from(request.category())); - } - - @Caching( - evict = { - @CacheEvict(cacheNames = TRIPS, allEntries = true), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)") - }) - @Transactional - public void deleteTrip(Long memberId, Long tripId) { - Member member = memberQueryService.getValidMember(memberId); - Trip trip = tripQueryService.getValidTrip(member.getId(), tripId); - - tripCommandService.deleteTrip(trip); - } - - @Cacheable( - cacheNames = TRIPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trips(#memberId, #page, #size)") - @Transactional(readOnly = true) - public TripSliceInfo getTripsByMember(Long memberId, int page, int size) { - Slice tripSlice = tripQueryService.getTripsSliceByMemberId(memberId, page, size); - - List tripInfos = - tripSlice.getContent().stream() - .map( - trip -> { - Integer dDay = calculateDDay(trip.getEndDate()); - int progress = - calculateProgress( - trip.getTotalStamps(), - trip.getCompletedStamps()); - return TripInfo.from(trip, dDay, progress); - }) - .sorted( - Comparator.comparing( - TripInfo::dDay, - Comparator.nullsLast(Comparator.naturalOrder()))) - .toList(); - - return TripSliceInfo.of(tripInfos, tripSlice.hasNext()); - } - - @Cacheable( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)") - @Transactional(readOnly = true) - public TripDetail getTrip(Long memberId, Long tripId) { - Member member = memberQueryService.getValidMember(memberId); - Trip trip = tripQueryService.getValidTrip(member.getId(), tripId); - - int dDay = calculateDDay(trip.getEndDate()); - int progress = calculateProgress(trip.getTotalStamps(), trip.getCompletedStamps()); - - List stamps = stampQueryService.getStampsByTripId(trip.getId()); - List stampInfos = stamps.stream().map(StampInfo::from).toList(); - - return TripDetail.from(TripInfo.from(trip, dDay, progress), stampInfos); - } - - @Caching( - evict = { - @CacheEvict(cacheNames = TRIPS, allEntries = true), - @CacheEvict( - cacheNames = TRIP, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)"), - @CacheEvict( - cacheNames = STAMPS, - key = - "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)") - }) - @Transactional - public void completeTrip(Long memberId, Long tripId) { - Member member = memberQueryService.getValidMember(memberId); - Trip trip = tripQueryService.getValidTrip(member.getId(), tripId); - - stampCommandService.validateAllStampsCompletedByTripId(trip.getId()); - - tripCommandService.completeTrip(trip); - } - - private Integer calculateDDay(LocalDate endDate) { - if (endDate == null) return null; // NULL 인 경우 무기한 여행 - - LocalDate today = LocalDate.now(); - - return (int) ChronoUnit.DAYS.between(today, endDate); - } - - private int calculateProgress(int totalStamps, int completedStamps) { - if (totalStamps == 0) return 0; - - double ratio = (double) completedStamps / totalStamps; - - return (int) (ratio * 100); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalCommandService.java b/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalCommandService.java deleted file mode 100644 index fc28fb1..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalCommandService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import com.ject.studytrip.trip.domain.factory.DailyGoalFactory; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.repository.DailyGoalCommandRepository; -import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class DailyGoalCommandService { - public final DailyGoalRepository dailyGoalRepository; - public final DailyGoalCommandRepository dailyGoalCommandRepository; - - public DailyGoal createDailyGoal(Trip trip, String title) { - DailyGoal dailyGoal = DailyGoalFactory.create(trip, title); - - return dailyGoalRepository.save(dailyGoal); - } - - public void deleteDailyGoal(DailyGoal dailyGoal) { - dailyGoal.updateDeletedAt(); - } - - public long hardDeleteDailyGoals() { - return dailyGoalCommandRepository.deleteAllByDeletedAtIsNotNull(); - } - - public long hardDeleteDailyGoalsOwnedByDeletedTrip() { - return dailyGoalCommandRepository.deleteAllByDeletedTripOwner(); - } - - public long hardDeleteDailyGoalsByMember(Long memberId) { - return dailyGoalCommandRepository.deleteAllByMemberId(memberId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalQueryService.java b/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalQueryService.java deleted file mode 100644 index ca031aa..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/service/DailyGoalQueryService.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.policy.DailyGoalPolicy; -import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class DailyGoalQueryService { - public final DailyGoalRepository dailyGoalRepository; - - public DailyGoal getValidDailyGoal(Long tripId, Long dailyGoalId) { - DailyGoal dailyGoal = - dailyGoalRepository - .findById(dailyGoalId) - .orElseThrow( - () -> new CustomException(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND)); - - DailyGoalPolicy.validateBelongsToTrip(dailyGoal, tripId); - DailyGoalPolicy.validateNotDeleted(dailyGoal); - - return dailyGoal; - } - - public List getCompleteDailyGoalsByTripId(Long tripId) { - return dailyGoalRepository.findAllByTripIdAndCompletedIsTrue(tripId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripCommandService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripCommandService.java deleted file mode 100644 index 3520238..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripCommandService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.factory.TripFactory; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.domain.policy.TripPolicy; -import com.ject.studytrip.trip.domain.repository.TripCommandRepository; -import com.ject.studytrip.trip.domain.repository.TripRepository; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest; -import java.time.LocalDate; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class TripCommandService { - private final TripRepository tripRepository; - private final TripCommandRepository tripCommandRepository; - - public Trip createTrip(Member member, CreateTripRequest request) { - TripCategory category = TripCategory.from(request.category()); - - TripPolicy.validateEndDateByCategory(category, request.endDate()); - TripPolicy.validateEndDateIsNotBeforeStartDate(LocalDate.now(), request.endDate()); - TripPolicy.validateMinimumStamps(request); - - Trip trip = - TripFactory.create( - member, - request.name(), - request.memo(), - category, - request.endDate(), - request.stamps().size()); - - return tripRepository.save(trip); - } - - public void updateTrip(Trip trip, UpdateTripRequest request) { - TripCategory category = null; - if (request.category() != null) category = TripCategory.from(request.category()); - - TripPolicy.validateEndDateIsNotBeforeStartDate(trip.getStartDate(), request.endDate()); - - 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(Trip trip) { - trip.updateDeletedAt(); - } - - public void completeTrip(Trip trip) { - TripPolicy.validateCompleted(trip); - - trip.updateCompleted(); - } - - public void increaseCompletedStamps(Trip trip) { - trip.increaseCompletedStamps(); - } - - public long hardDeleteTrips() { - return tripCommandRepository.deleteAllByDeletedAtIsNotNull(); - } - - public long hardDeleteTripsOwnedByDeletedMember() { - return tripCommandRepository.deleteAllByDeletedMemberOwner(); - } - - public long hardDeleteTripsByMember(Long memberId) { - return tripCommandRepository.deleteAllByMemberId(memberId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/application/service/TripQueryService.java b/src/main/java/com/ject/studytrip/trip/application/service/TripQueryService.java deleted file mode 100644 index 841ac34..0000000 --- a/src/main/java/com/ject/studytrip/trip/application/service/TripQueryService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.trip.application.dto.TripCount; -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.domain.policy.TripPolicy; -import com.ject.studytrip.trip.domain.repository.TripQueryRepository; -import com.ject.studytrip.trip.domain.repository.TripRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class TripQueryService { - private final TripRepository tripRepository; - private final TripQueryRepository tripQueryRepository; - - public Trip getTrip(Long tripId) { - return tripRepository - .findById(tripId) - .orElseThrow(() -> new CustomException(TripErrorCode.TRIP_NOT_FOUND)); - } - - 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); - TripPolicy.validateCompleted(trip); - - return trip; - } - - public Slice getTripsSliceByMemberId(Long memberId, int page, int size) { - return tripQueryRepository.findSliceByMemberIdAndCompletedFalseAndDeletedAtIsNull( - memberId, PageRequest.of(page, size)); - } - - public TripCount getActiveTripCountsByMemberId(Long memberId) { - long courseCount = - tripQueryRepository.countActiveTripsByMemberIdAndCategory( - memberId, TripCategory.COURSE); - long exploreCount = - tripQueryRepository.countActiveTripsByMemberIdAndCategory( - memberId, TripCategory.EXPLORE); - - return TripCount.of(courseCount, exploreCount); - } - - public Trip getValidCompletedTrip(Long memberId, Long tripId) { - Trip trip = - tripRepository - .findById(tripId) - .orElseThrow(() -> new CustomException(TripErrorCode.TRIP_NOT_FOUND)); - - TripPolicy.validateOwner(memberId, trip); - TripPolicy.validateNotDeleted(trip); - TripPolicy.validateNotCompleted(trip); - - return trip; - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/error/DailyGoalErrorCode.java b/src/main/java/com/ject/studytrip/trip/domain/error/DailyGoalErrorCode.java deleted file mode 100644 index 76f0ba6..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/error/DailyGoalErrorCode.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ject.studytrip.trip.domain.error; - -import com.ject.studytrip.global.exception.error.ErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@RequiredArgsConstructor -public enum DailyGoalErrorCode implements ErrorCode { - // 400 - DAILY_GOAL_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 데일리 목표입니다."), - - // 403 - DAILY_GOAL_NOT_BELONG_TO_TRIP(HttpStatus.FORBIDDEN, "해당 데일리 목표는 요청한 여행에 속하지 않습니다."), - - // 404 - DAILY_GOAL_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 데일리 목표가 존재하지 않습니다."), - ; - - private final HttpStatus status; - private final String message; - - @Override - public String getName() { - return this.name(); - } - - @Override - public HttpStatus getStatus() { - return this.status; - } - - @Override - public String getMessage() { - return this.message; - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/error/TripErrorCode.java b/src/main/java/com/ject/studytrip/trip/domain/error/TripErrorCode.java deleted file mode 100644 index a513aab..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/error/TripErrorCode.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.ject.studytrip.trip.domain.error; - -import com.ject.studytrip.global.exception.error.ErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@RequiredArgsConstructor -public enum TripErrorCode implements ErrorCode { - // 400 - TRIP_CATEGORY_REQUIRED(HttpStatus.BAD_REQUEST, "여행 카테고리는 필수입니다."), - INVALID_TRIP_CATEGORY(HttpStatus.BAD_REQUEST, "여행 카테고리가 누락되었거나 올바르지 않습니다."), - TRIP_STAMP_REQUIRED(HttpStatus.BAD_REQUEST, "여행을 생성하려면 최소 1개의 스탬프가 필요합니다."), - TRIP_END_DATE_BEFORE_START_DATE(HttpStatus.BAD_REQUEST, "여행 종료일은 시작일보다 이후여야 합니다."), - COURSE_TRIP_END_DATE_REQUIRED(HttpStatus.BAD_REQUEST, "코스형 여행은 종료일이 필수입니다."), - TRIP_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "이미 완료된 여행입니다."), - TRIP_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 여행입니다."), - TRIP_NOT_COMPLETED(HttpStatus.BAD_REQUEST, "여행이 아직 완료되지 않았습니다."), - - // 403 - NOT_TRIP_OWNER(HttpStatus.FORBIDDEN, "요청한 여행 정보를 수정/삭제할 권한이 부족합니다."), - - // 404 - TRIP_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 여행이 존재하지 않습니다."), - ; - - private final HttpStatus status; - private final String message; - - @Override - public String getName() { - return this.name(); - } - - @Override - public HttpStatus getStatus() { - return this.status; - } - - @Override - public String getMessage() { - return this.message; - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/factory/DailyGoalFactory.java b/src/main/java/com/ject/studytrip/trip/domain/factory/DailyGoalFactory.java deleted file mode 100644 index 214830a..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/factory/DailyGoalFactory.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ject.studytrip.trip.domain.factory; - -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class DailyGoalFactory { - public static DailyGoal create(Trip trip, String title) { - return DailyGoal.of(trip, title); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/factory/TripFactory.java b/src/main/java/com/ject/studytrip/trip/domain/factory/TripFactory.java deleted file mode 100644 index f64f170..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/factory/TripFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ject.studytrip.trip.domain.factory; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.time.LocalDate; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TripFactory { - public static Trip create( - Member member, - String name, - String memo, - TripCategory category, - LocalDate endDate, - int totalStamps) { - return Trip.of(member, name, memo, category, endDate, totalStamps); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/model/TripCategory.java b/src/main/java/com/ject/studytrip/trip/domain/model/TripCategory.java index f7f9b02..8031da4 100644 --- a/src/main/java/com/ject/studytrip/trip/domain/model/TripCategory.java +++ b/src/main/java/com/ject/studytrip/trip/domain/model/TripCategory.java @@ -3,10 +3,8 @@ import com.ject.studytrip.global.exception.CustomException; import com.ject.studytrip.trip.domain.error.TripErrorCode; import java.util.Arrays; -import lombok.Getter; import lombok.RequiredArgsConstructor; -@Getter @RequiredArgsConstructor public enum TripCategory { COURSE("코스형"), @@ -15,10 +13,11 @@ public enum TripCategory { private final String value; - public static TripCategory from(String name) { - if (name == null || name.isBlank()) - throw new CustomException(TripErrorCode.TRIP_CATEGORY_REQUIRED); + public String getValue() { + return this.value; + } + public static TripCategory from(String name) { return Arrays.stream(TripCategory.values()) .filter(category -> category.name().equalsIgnoreCase(name)) .findFirst() diff --git a/src/main/java/com/ject/studytrip/trip/domain/policy/DailyGoalPolicy.java b/src/main/java/com/ject/studytrip/trip/domain/policy/DailyGoalPolicy.java deleted file mode 100644 index f68e5a2..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/policy/DailyGoalPolicy.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.ject.studytrip.trip.domain.policy; - -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class DailyGoalPolicy { - public static void validateBelongsToTrip(DailyGoal dailyGoal, Long tripId) { - if (!dailyGoal.getTrip().getId().equals(tripId)) - throw new CustomException(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONG_TO_TRIP); - } - - public static void validateNotDeleted(DailyGoal dailyGoal) { - if (dailyGoal.getDeletedAt() != null) - throw new CustomException(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED); - } -} 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 deleted file mode 100644 index dc86173..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/policy/TripPolicy.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ject.studytrip.trip.domain.policy; - -import com.ject.studytrip.global.exception.CustomException; -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.presentation.dto.request.CreateTripRequest; -import java.time.LocalDate; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TripPolicy { - public static void validateOwner(Long memberId, Trip trip) { - if (!trip.getMember().getId().equals(memberId)) { - throw new CustomException(TripErrorCode.NOT_TRIP_OWNER); - } - } - - public static void validateEndDateByCategory(TripCategory category, LocalDate endDate) { - if (category == TripCategory.EXPLORE) return; - if (endDate == null) throw new CustomException(TripErrorCode.COURSE_TRIP_END_DATE_REQUIRED); - } - - public static void validateEndDateIsNotBeforeStartDate(LocalDate startDate, LocalDate endDate) { - if (startDate == null || endDate == null) return; - if (endDate.isBefore(startDate)) - throw new CustomException(TripErrorCode.TRIP_END_DATE_BEFORE_START_DATE); - } - - public static void validateMinimumStamps(CreateTripRequest request) { - if (request.stamps() == null || request.stamps().size() < 1) - throw new CustomException(TripErrorCode.TRIP_STAMP_REQUIRED); - } - - public static void validateNotDeleted(Trip trip) { - if (trip.getDeletedAt() != null) - throw new CustomException(TripErrorCode.TRIP_ALREADY_DELETED); - } - - public static void validateCompleted(Trip trip) { - if (trip.isCompleted()) { - throw new CustomException(TripErrorCode.TRIP_ALREADY_COMPLETED); - } - } - - public static void validateNotCompleted(Trip trip) { - if (!trip.isCompleted()) { - throw new CustomException(TripErrorCode.TRIP_NOT_COMPLETED); - } - } -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/DailyGoalRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/DailyGoalRepository.java deleted file mode 100644 index d1a663f..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/repository/DailyGoalRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ject.studytrip.trip.domain.repository; - -import com.ject.studytrip.trip.domain.model.DailyGoal; -import java.util.List; -import java.util.Optional; - -public interface DailyGoalRepository { - - DailyGoal save(DailyGoal dailyGoal); - - Optional findById(Long id); - - List findAllByTripIdAndCompletedIsTrue(Long tripId); -} diff --git a/src/main/java/com/ject/studytrip/trip/domain/repository/TripRepository.java b/src/main/java/com/ject/studytrip/trip/domain/repository/TripRepository.java deleted file mode 100644 index 8c25bc2..0000000 --- a/src/main/java/com/ject/studytrip/trip/domain/repository/TripRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ject.studytrip.trip.domain.repository; - -import com.ject.studytrip.trip.domain.model.Trip; -import java.util.Optional; - -public interface TripRepository { - Optional findById(Long id); - - Trip save(Trip trip); -} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/DailyGoalJpaRepository.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/DailyGoalJpaRepository.java deleted file mode 100644 index 6bf2916..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/DailyGoalJpaRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.DailyGoal; -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface DailyGoalJpaRepository extends JpaRepository { - List findAllByTripIdAndCompletedIsTrue(Long tripId); -} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/DailyGoalRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/DailyGoalRepositoryAdapter.java deleted file mode 100644 index caa1bf2..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/DailyGoalRepositoryAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class DailyGoalRepositoryAdapter implements DailyGoalRepository { - private final DailyGoalJpaRepository dailyGoalJpaRepository; - - @Override - public DailyGoal save(DailyGoal dailyGoal) { - return dailyGoalJpaRepository.save(dailyGoal); - } - - @Override - public Optional findById(Long id) { - return dailyGoalJpaRepository.findById(id); - } - - @Override - public List findAllByTripIdAndCompletedIsTrue(Long tripId) { - return dailyGoalJpaRepository.findAllByTripIdAndCompletedIsTrue(tripId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripJpaRepository.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripJpaRepository.java deleted file mode 100644 index fe7934c..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripJpaRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.Trip; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface TripJpaRepository extends JpaRepository {} diff --git a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripRepositoryAdapter.java b/src/main/java/com/ject/studytrip/trip/infra/jpa/TripRepositoryAdapter.java deleted file mode 100644 index c5aecae..0000000 --- a/src/main/java/com/ject/studytrip/trip/infra/jpa/TripRepositoryAdapter.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.ject.studytrip.trip.infra.jpa; - -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.repository.TripRepository; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class TripRepositoryAdapter implements TripRepository { - private final TripJpaRepository tripJpaRepository; - - @Override - public Optional findById(Long id) { - return tripJpaRepository.findById(id); - } - - @Override - public Trip save(Trip trip) { - return tripJpaRepository.save(trip); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/controller/DailyGoalController.java b/src/main/java/com/ject/studytrip/trip/presentation/controller/DailyGoalController.java deleted file mode 100644 index 57ff150..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/controller/DailyGoalController.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.ject.studytrip.trip.presentation.controller; - -import com.ject.studytrip.global.common.response.StandardResponse; -import com.ject.studytrip.trip.application.dto.DailyGoalDetail; -import com.ject.studytrip.trip.application.dto.DailyGoalInfo; -import com.ject.studytrip.trip.application.facade.DailyGoalFacade; -import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest; -import com.ject.studytrip.trip.presentation.dto.response.CreateDailyGoalResponse; -import com.ject.studytrip.trip.presentation.dto.response.LoadDailyGoalDetailResponse; -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 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 = "DailyGoal", description = "데일리 목표 API") -@RestController -@RequiredArgsConstructor -@Validated -public class DailyGoalController { - private final DailyGoalFacade dailyGoalFacade; - - @Operation(summary = "데일리 목표 생성", description = "데일리 목표를 생성하는 API 입니다.") - @PostMapping("/api/trips/{tripId}/daily-goals") - public ResponseEntity createDailyGoal( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @RequestBody @Valid CreateDailyGoalRequest request) { - DailyGoalInfo result = - dailyGoalFacade.createDailyGoal(Long.valueOf(memberId), tripId, request); - - return ResponseEntity.status(HttpStatus.CREATED) - .body( - StandardResponse.success( - HttpStatus.CREATED.value(), CreateDailyGoalResponse.of(result))); - } - - @Operation(summary = "데일리 목표 수정", description = "데일리 목표를 수정하는 API 입니다.") - @PatchMapping("/api/trips/{tripId}/daily-goals/{dailyGoalId}") - public ResponseEntity updateDailyGoal( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @PathVariable @NotNull(message = "데일리 목표 ID는 필수 요청 파라미터입니다.") Long dailyGoalId, - @RequestBody UpdateDailyGoalRequest request) { - dailyGoalFacade.updateDailyGoal(Long.valueOf(memberId), tripId, dailyGoalId, request); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } - - @Operation(summary = "데일리 목표 삭제", description = "데일리 목표를 삭제하는 API 입니다.") - @DeleteMapping("/api/trips/{tripId}/daily-goals/{dailyGoalId}") - public ResponseEntity deleteDailyGoal( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @PathVariable @NotNull(message = "데일리 목표 ID는 필수 요청 파라미터입니다.") Long dailyGoalId) { - dailyGoalFacade.deleteDailyGoal(Long.valueOf(memberId), tripId, dailyGoalId); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } - - @Operation(summary = "특정 데일리 목표 조회", description = "특정 데일리 목표를 조회하는 API 입니다.") - @GetMapping("/api/trips/{tripId}/daily-goals/{dailyGoalId}") - public ResponseEntity loadDailyGoal( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @PathVariable @NotNull(message = "데일리 목표 ID는 필수 요청 파라미터입니다.") Long dailyGoalId) { - DailyGoalDetail result = - dailyGoalFacade.getDailyGoal(Long.valueOf(memberId), tripId, dailyGoalId); - - return ResponseEntity.status(HttpStatus.OK) - .body( - StandardResponse.success( - HttpStatus.OK.value(), - LoadDailyGoalDetailResponse.of( - result.dailyGoalInfo(), - result.pomodoroInfo(), - result.dailyMissionInfos()))); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/controller/TripController.java b/src/main/java/com/ject/studytrip/trip/presentation/controller/TripController.java deleted file mode 100644 index 0489d6e..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/controller/TripController.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.ject.studytrip.trip.presentation.controller; - -import com.ject.studytrip.global.common.response.StandardResponse; -import com.ject.studytrip.trip.application.dto.TripCategoryInfo; -import com.ject.studytrip.trip.application.dto.TripDetail; -import com.ject.studytrip.trip.application.dto.TripInfo; -import com.ject.studytrip.trip.application.dto.TripSliceInfo; -import com.ject.studytrip.trip.application.facade.TripFacade; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest; -import com.ject.studytrip.trip.presentation.dto.response.*; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import 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 = "Trip", description = "여행 API") -@RestController -@RequestMapping("/api/trips") -@RequiredArgsConstructor -@Validated -public class TripController { - private final TripFacade tripFacade; - - @Operation(summary = "여행 카테고리 목록 조회", description = "여행 카테고리 목록을 조회하는 API 입니다.") - @GetMapping("/categories") - public ResponseEntity loadTripCategories() { - List result = tripFacade.loadTripCategories(); - List response = - result.stream().map(LoadTripCategoryResponse::of).toList(); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), response)); - } - - @Operation( - summary = "여행 생성", - description = "새로운 여행을 생성하는 API 입니다. 여행을 생성하는 동시에 1개 이상의 스탬프를 함께 생성합니다.") - @PostMapping - public ResponseEntity createTrip( - @AuthenticationPrincipal String memberId, - @RequestBody @Valid CreateTripRequest request) { - TripInfo result = tripFacade.createTrip(Long.valueOf(memberId), request); - - return ResponseEntity.status(HttpStatus.CREATED) - .body( - StandardResponse.success( - HttpStatus.CREATED.value(), - CreateTripResponse.of(result.tripId()))); - } - - @Operation(summary = "여행 수정", description = "여행을 수정하는 API 입니다. PATCH 매핑으로 수정을 원하는 필드만 요청합니다.") - @PatchMapping("/{tripId}") - public ResponseEntity updateTrip( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId, - @RequestBody @Valid UpdateTripRequest request) { - tripFacade.updateTrip(Long.valueOf(memberId), tripId, request); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } - - @Operation(summary = "여행 삭제", description = "특정 여행을 삭제하는 API 입니다.") - @DeleteMapping("/{tripId}") - public ResponseEntity deleteTrip( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId) { - tripFacade.deleteTrip(Long.valueOf(memberId), tripId); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } - - @Operation( - summary = "여행 목록 조회", - description = - "여행 목록을 조회하는 API 입니다. 무한 스크롤을 위해 슬라이스를 적용하고, D-DAY 정보가 이른 순으로 정렬합니다. 완료되지 않은 여행 목록만 조회합니다.") - @GetMapping - public ResponseEntity loadTrips( - @AuthenticationPrincipal String memberId, - @RequestParam(name = "page", defaultValue = "0") @Min(0) int page, - @RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) int size) { - TripSliceInfo result = tripFacade.getTripsByMember(Long.valueOf(memberId), page, size); - - return ResponseEntity.status(HttpStatus.OK) - .body( - StandardResponse.success( - HttpStatus.OK.value(), - LoadTripsSliceResponse.of(result.tripInfos(), result.hasNext()))); - } - - @Operation(summary = "여행 상세 조회", description = "특정 여행을 조회하는 API 입니다.") - @GetMapping("/{tripId}") - public ResponseEntity loadTripDetail( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId) { - TripDetail result = tripFacade.getTrip(Long.valueOf(memberId), tripId); - - return ResponseEntity.status(HttpStatus.OK) - .body( - StandardResponse.success( - HttpStatus.OK.value(), - LoadTripDetailResponse.of(result.tripInfo(), result.stampInfos()))); - } - - @Operation(summary = "여행 완료", description = "특정 여행 하위의 모든 스탬프가 완료된 경우에만 여행을 완료합니다.") - @PatchMapping("/{tripId}/complete") - public ResponseEntity completeTrip( - @AuthenticationPrincipal String memberId, - @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId) { - tripFacade.completeTrip(Long.valueOf(memberId), tripId); - - return ResponseEntity.status(HttpStatus.OK) - .body(StandardResponse.success(HttpStatus.OK.value(), null)); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateDailyGoalRequest.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateDailyGoalRequest.java deleted file mode 100644 index 454e947..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateDailyGoalRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.request; - -import com.ject.studytrip.pomodoro.presentation.dto.request.CreatePomodoroRequest; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import java.util.List; - -public record CreateDailyGoalRequest( - @Schema(name = "뽀모도로") @Valid @NotNull(message = "뽀모도로 정보는 필수 요청 값입니다.") - CreatePomodoroRequest pomodoro, - @Schema(name = "수행할 미션 ID 목록") @NotEmpty(message = "수행할 미션 목록은 필수 요청 값입니다.") - List missionIds) {} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateTripRequest.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateTripRequest.java deleted file mode 100644 index bf22645..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/CreateTripRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.request; - -import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.FutureOrPresent; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import java.time.LocalDate; -import java.util.List; - -public record CreateTripRequest( - @Schema(description = "여행 이름") @NotEmpty(message = "여행 이름은 필수 요청 값입니다.") String name, - @Schema(description = "여행 메모") String memo, - @Schema(description = "여행 카테고리") - @NotNull(message = "여행 카테고리는 필수 요청 값입니다.") - @Pattern( - regexp = "^(COURSE|EXPLORE)$", - message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.") - String category, - @Schema(description = "여행 종료일") @FutureOrPresent(message = "여행 종료일은 현재 날짜보다 과거일 수 없습니다.") - LocalDate endDate, - @Schema(description = "여행 스탬프 목록") @Valid @NotEmpty(message = "스탬프는 최소 1개 이상 함께 등록해야합니다.") - List stamps) {} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/UpdateDailyGoalRequest.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/request/UpdateDailyGoalRequest.java deleted file mode 100644 index fd7dd87..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/UpdateDailyGoalRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -public record UpdateDailyGoalRequest( - @Schema(name = "삭제할 데일리 미션 ID 목록") List deleteDailyMissionIds, - @Schema(name = "추가할 미션 ID 목록") List addMissionIds) {} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/UpdateTripRequest.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/request/UpdateTripRequest.java deleted file mode 100644 index d8b560a..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/request/UpdateTripRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.FutureOrPresent; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import java.time.LocalDate; - -public record UpdateTripRequest( - @Schema(description = "수정할 여행 이름") @Size(min = 1, message = "여행 이름은 최소 1글자 이상이여야 합니다.") - String name, - @Schema(description = "수정할 여행 메모") String memo, - @Schema(description = "수정할 여행 카테고리") - @Pattern( - regexp = "^(COURSE|EXPLORE)$", - message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.") - String category, - @Schema(description = "수정할 여행 종료일") - @FutureOrPresent(message = "여행 종료일은 현재 날짜보다 과거일 수 없습니다.") - LocalDate endDate) {} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateDailyGoalResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateDailyGoalResponse.java deleted file mode 100644 index ec3b2e1..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateDailyGoalResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.trip.application.dto.DailyGoalInfo; -import io.swagger.v3.oas.annotations.media.Schema; - -public record CreateDailyGoalResponse(@Schema(name = "데일리 목표 ID") Long dailyGoalId) { - public static CreateDailyGoalResponse of(DailyGoalInfo dailyGoalInfo) { - return new CreateDailyGoalResponse(dailyGoalInfo.dailyGoalId()); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateTripResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateTripResponse.java deleted file mode 100644 index 09947e8..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/CreateTripResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record CreateTripResponse(@Schema(description = "여행 ID") Long tripId) { - public static CreateTripResponse of(Long tripId) { - return new CreateTripResponse(tripId); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadDailyGoalDetailResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadDailyGoalDetailResponse.java deleted file mode 100644 index 94e187a..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadDailyGoalDetailResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.mission.application.dto.DailyMissionInfo; -import com.ject.studytrip.pomodoro.application.dto.PomodoroInfo; -import com.ject.studytrip.trip.application.dto.DailyGoalInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -public record LoadDailyGoalDetailResponse( - @Schema(name = "데일리 목표 ID") Long dailyGoalId, - @Schema(name = "데일리 목표 제목(스탬프 이름)") String title, - @Schema(name = "데일리 목표 완료 여부") boolean completed, - @Schema(name = "뽀모도로 정보") DailyGoalPomodoroResponse pomodoro, - @Schema(name = "수행할 데일리 미션 목록") List dailyMissions) { - - public static LoadDailyGoalDetailResponse of( - DailyGoalInfo dailyGoalInfo, - PomodoroInfo pomodoroInfo, - List dailyMissionInfos) { - return new LoadDailyGoalDetailResponse( - dailyGoalInfo.dailyGoalId(), - dailyGoalInfo.title(), - dailyGoalInfo.completed(), - DailyGoalPomodoroResponse.of(pomodoroInfo), - dailyMissionInfos.stream().map(DailyGoalMissionResponse::of).toList()); - } - - public record DailyGoalPomodoroResponse( - @Schema(name = "뽀모도로 ID") Long pomodoroId, - @Schema(name = "뽀모도로 집중 시간(분)") int focusDurationInMinute, - @Schema(name = "뽀모도로 집중 세션 개수") int focusSessionCount) { - public static DailyGoalPomodoroResponse of(PomodoroInfo info) { - return new DailyGoalPomodoroResponse( - info.getPomodoroId(), - info.getFocusDurationInMinute(), - info.getFocusSessionCount()); - } - } - - public record DailyGoalMissionResponse( - @Schema(name = "데일리 미션 ID") Long dailyMissionId, - @Schema(name = "미션 이름") String missionName) { - public static DailyGoalMissionResponse of(DailyMissionInfo info) { - return new DailyGoalMissionResponse( - info.getDailyMissionId(), info.getMissionInfo().getMissionName()); - } - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripCategoryResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripCategoryResponse.java deleted file mode 100644 index 86ebf7f..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripCategoryResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.trip.application.dto.TripCategoryInfo; - -public record LoadTripCategoryResponse(String name, String value) { - public static LoadTripCategoryResponse of(TripCategoryInfo info) { - return new LoadTripCategoryResponse(info.name(), info.value()); - } -} 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 deleted file mode 100644 index 4964c6e..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.stamp.application.dto.StampInfo; -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; -import java.util.List; - -public record LoadTripDetailResponse( - @Schema(description = "여행 ID") Long tripId, - @Schema(description = "여행 이름") String name, - @Schema(description = "여행 메모") String memo, - @Schema(description = "여행 카테고리") TripCategory category, - @Schema(description = "여행 시작일") String startDate, - @Schema(description = "여행 종료일") String endDate, - @Schema(description = "D-DAY") Integer dDay, - @Schema(description = "여행의 총 스탬프 수") int totalStamps, - @Schema(description = "완료된 총 스탬프 수") int completedStamps, - @Schema(description = "진행률") Integer progress, - @Schema(description = "여행 완료 여부") boolean completed, - @Schema(description = "여행에 속한 스탬프 목록") List stamps) { - public static LoadTripDetailResponse of(TripInfo tripInfo, List stampInfos) { - return new LoadTripDetailResponse( - tripInfo.tripId(), - tripInfo.tripName(), - tripInfo.tripMemo(), - tripInfo.tripCategory(), - tripInfo.startDate(), - tripInfo.endDate(), - tripInfo.dDay(), - tripInfo.totalStamps(), - tripInfo.completedStamps(), - tripInfo.progress(), - tripInfo.completed(), - stampInfos.stream().map(LoadStampInfoResponse::of).toList()); - } -} diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java index d43dda3..41bda14 100644 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java +++ b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripRetrospectDetailResponse.java @@ -21,9 +21,9 @@ public static LoadTripRetrospectDetailResponse of( TripInfo tripInfo, StudyLogSliceInfo studyLogDetailSlice) { return new LoadTripRetrospectDetailResponse( - tripInfo.tripName(), - tripInfo.startDate(), - tripInfo.endDate(), + tripInfo.getTripName(), + tripInfo.getStartDate(), + tripInfo.getEndDate(), tripRetrospectSummary.totalFocusHours(), tripRetrospectSummary.studyLogCount(), tripRetrospectSummary.studyDays(), diff --git a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripsSliceResponse.java b/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripsSliceResponse.java deleted file mode 100644 index 2bf8936..0000000 --- a/src/main/java/com/ject/studytrip/trip/presentation/dto/response/LoadTripsSliceResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ject.studytrip.trip.presentation.dto.response; - -import com.ject.studytrip.trip.application.dto.TripInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -public record LoadTripsSliceResponse( - @Schema(description = "여행 목록") List tripInfos, - @Schema(description = "다음 데이터 존재 여부") boolean hasNext) { - public static LoadTripsSliceResponse of(List infos, boolean hasNext) { - return new LoadTripsSliceResponse(infos, hasNext); - } -} diff --git a/src/main/kotlin/com/ject/studytrip/mission/application/facade/MissionFacade.kt b/src/main/kotlin/com/ject/studytrip/mission/application/facade/MissionFacade.kt index 826f72c..b9c0897 100644 --- a/src/main/kotlin/com/ject/studytrip/mission/application/facade/MissionFacade.kt +++ b/src/main/kotlin/com/ject/studytrip/mission/application/facade/MissionFacade.kt @@ -16,6 +16,7 @@ import com.ject.studytrip.stamp.application.service.StampQueryService import com.ject.studytrip.stamp.domain.model.Stamp import com.ject.studytrip.trip.application.service.TripQueryService import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Caching import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -128,13 +129,9 @@ class MissionFacade( stampCommandService.decreaseTotalMissions(stamp) } - @Caching( - evict = [ - CacheEvict( - cacheNames = [MISSIONS], - key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).missions(#memberId, #tripId, #stampId)", - ), - ], + @Cacheable( + cacheNames = [MISSIONS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).missions(#memberId, #tripId, #stampId)", ) @Transactional(readOnly = true) fun getMissionsByStamp( diff --git a/src/main/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryService.kt b/src/main/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryService.kt index ee59f19..e3e891b 100644 --- a/src/main/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryService.kt +++ b/src/main/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryService.kt @@ -41,7 +41,7 @@ class DailyMissionQueryService( ) { DailyMissionPolicy.validateExistAll(dailyMissions, dailyMissionIds) dailyMissions.forEach { dailyMission -> - DailyMissionPolicy.validateBelongsToDailyGoal(dailyMission, dailyGoalId) + DailyMissionPolicy.validateDailyMissionBelongsToDailyGoal(dailyMission, dailyGoalId) DailyMissionPolicy.validateNotDeleted(dailyMission) } } diff --git a/src/main/kotlin/com/ject/studytrip/mission/application/service/MissionCommandService.kt b/src/main/kotlin/com/ject/studytrip/mission/application/service/MissionCommandService.kt index 61321e3..098cf80 100644 --- a/src/main/kotlin/com/ject/studytrip/mission/application/service/MissionCommandService.kt +++ b/src/main/kotlin/com/ject/studytrip/mission/application/service/MissionCommandService.kt @@ -38,7 +38,7 @@ class MissionCommandService( mission.updateCompleted() } - fun validateMissionsBelongsToStamp( + fun validateMissionsBelongToStamp( stampId: Long, missions: List, ) = missions.forEach { @@ -47,7 +47,7 @@ class MissionCommandService( fun validateAllMissionsCompletedByStampId(stampId: Long) { val exists = missionCommandRepository.existsByStampIdAndCompletedIsFalseAndDeletedAtIsNull(stampId) - MissionPolicy.validateAllCompleted(exists) + MissionPolicy.validateNotAllCompleted(exists) } fun hardDeleteMissions(): Long = missionCommandRepository.deleteAllByDeletedAtIsNotNull() diff --git a/src/main/kotlin/com/ject/studytrip/mission/domain/policy/DailyMissionPolicy.kt b/src/main/kotlin/com/ject/studytrip/mission/domain/policy/DailyMissionPolicy.kt index 6282ae3..fdf6708 100644 --- a/src/main/kotlin/com/ject/studytrip/mission/domain/policy/DailyMissionPolicy.kt +++ b/src/main/kotlin/com/ject/studytrip/mission/domain/policy/DailyMissionPolicy.kt @@ -11,7 +11,7 @@ object DailyMissionPolicy { } } - fun validateBelongsToDailyGoal( + fun validateDailyMissionBelongsToDailyGoal( dailyMission: DailyMission, dailyGoalId: Long, ) { diff --git a/src/main/kotlin/com/ject/studytrip/mission/domain/policy/MissionPolicy.kt b/src/main/kotlin/com/ject/studytrip/mission/domain/policy/MissionPolicy.kt index a6eaa36..9ced1e7 100644 --- a/src/main/kotlin/com/ject/studytrip/mission/domain/policy/MissionPolicy.kt +++ b/src/main/kotlin/com/ject/studytrip/mission/domain/policy/MissionPolicy.kt @@ -17,7 +17,7 @@ object MissionPolicy { } } - fun validateAllCompleted(exists: Boolean) { + fun validateNotAllCompleted(exists: Boolean) { if (exists) { throw CustomException(MissionErrorCode.ALL_MISSIONS_NOT_COMPLETED) } diff --git a/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/CreateMissionResponse.kt b/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/CreateMissionResponse.kt index baac955..54d3a30 100644 --- a/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/CreateMissionResponse.kt +++ b/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/CreateMissionResponse.kt @@ -9,6 +9,6 @@ data class CreateMissionResponse( ) { companion object { @JvmStatic - fun of(info: MissionInfo): CreateMissionResponse = CreateMissionResponse(info.missionId) + fun of(missionInfo: MissionInfo): CreateMissionResponse = CreateMissionResponse(missionInfo.missionId) } } diff --git a/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.kt b/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.kt index 4824be1..1245918 100644 --- a/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.kt +++ b/src/main/kotlin/com/ject/studytrip/mission/presentation/dto/response/LoadMissionInfoResponse.kt @@ -13,6 +13,7 @@ data class LoadMissionInfoResponse( ) { companion object { @JvmStatic - fun of(info: MissionInfo): LoadMissionInfoResponse = LoadMissionInfoResponse(info.missionId, info.missionName, info.completed) + fun of(missionInfo: MissionInfo): LoadMissionInfoResponse = + LoadMissionInfoResponse(missionInfo.missionId, missionInfo.missionName, missionInfo.completed) } } diff --git a/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampDetail.kt b/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampDetail.kt new file mode 100644 index 0000000..dd77cd3 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampDetail.kt @@ -0,0 +1,16 @@ +package com.ject.studytrip.stamp.application.dto + +import com.ject.studytrip.mission.application.dto.MissionInfo + +data class StampDetail( + val stampInfo: StampInfo, + val missionInfos: List, +) { + companion object { + @JvmStatic + fun from( + stampInfo: StampInfo, + missionInfos: List, + ): StampDetail = StampDetail(stampInfo, missionInfos) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampInfo.kt b/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampInfo.kt new file mode 100644 index 0000000..1130574 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampInfo.kt @@ -0,0 +1,34 @@ +package com.ject.studytrip.stamp.application.dto + +import com.ject.studytrip.global.util.DateUtil +import com.ject.studytrip.stamp.domain.model.Stamp + +data class StampInfo( + val stampId: Long, + val stampName: String, + val stampOrder: Int, + val endDate: String, + val totalMissions: Int, + val completedMissions: Int, + val completed: Boolean, + val createdAt: String, + val updatedAt: String, + val deletedAt: String?, +) { + companion object { + @JvmStatic + fun from(stamp: Stamp): StampInfo = + StampInfo( + stamp.id, + stamp.name, + stamp.stampOrder, + DateUtil.formatDate(stamp.endDate), + stamp.totalMissions, + stamp.completedMissions, + stamp.isCompleted, + DateUtil.formatDateTime(stamp.createdAt), + DateUtil.formatDateTime(stamp.updatedAt), + stamp.deletedAt?.let { DateUtil.formatDateTime(it) }, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampsInfo.kt b/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampsInfo.kt new file mode 100644 index 0000000..9da91d4 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/application/dto/StampsInfo.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.stamp.application.dto + +data class StampsInfo( + val stampInfos: List, +) { + companion object { + @JvmStatic + fun of(stampInfos: List): StampsInfo = StampsInfo(stampInfos) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/application/facade/StampFacade.kt b/src/main/kotlin/com/ject/studytrip/stamp/application/facade/StampFacade.kt new file mode 100644 index 0000000..cad31f2 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/application/facade/StampFacade.kt @@ -0,0 +1,227 @@ +package com.ject.studytrip.stamp.application.facade + +import com.ject.studytrip.global.common.constants.CacheNameConstants.STAMP +import com.ject.studytrip.global.common.constants.CacheNameConstants.STAMPS +import com.ject.studytrip.global.common.constants.CacheNameConstants.TRIP +import com.ject.studytrip.global.common.constants.CacheNameConstants.TRIPS +import com.ject.studytrip.mission.application.dto.MissionInfo +import com.ject.studytrip.mission.application.service.MissionCommandService +import com.ject.studytrip.mission.application.service.MissionQueryService +import com.ject.studytrip.stamp.application.dto.StampDetail +import com.ject.studytrip.stamp.application.dto.StampInfo +import com.ject.studytrip.stamp.application.dto.StampsInfo +import com.ject.studytrip.stamp.application.service.StampCommandService +import com.ject.studytrip.stamp.application.service.StampQueryService +import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest +import com.ject.studytrip.trip.application.service.TripCommandService +import com.ject.studytrip.trip.application.service.TripQueryService +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.Cacheable +import org.springframework.cache.annotation.Caching +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class StampFacade( + // Query Service + private val tripQueryService: TripQueryService, + private val stampQueryService: StampQueryService, + private val missionQueryService: MissionQueryService, + // Command Service + private val tripCommandService: TripCommandService, + private val stampCommandService: StampCommandService, + private val missionCommandService: MissionCommandService, +) { + @Caching( + evict = [ + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict(cacheNames = [TRIPS], allEntries = true), + ], + ) + @Transactional + fun createStamp( + memberId: Long, + tripId: Long, + request: CreateStampRequest, + ): StampInfo { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val nextOrder = stampQueryService.getNextStampOrderByTrip(trip) + val stamp = stampCommandService.createStamp(trip, nextOrder, request) + + tripCommandService.increaseTotalStamps(trip) + + return StampInfo.from(stamp) + } + + @Caching( + evict = [ + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + CacheEvict( + cacheNames = [STAMP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)", + ), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict(cacheNames = [TRIPS], allEntries = true), + ], + ) + @Transactional + fun updateStamp( + memberId: Long, + tripId: Long, + stampId: Long, + request: UpdateStampRequest, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val stamp = stampQueryService.getValidStamp(trip.id, stampId) + + stampCommandService.updateStamp(trip, stamp, request) + } + + @Caching( + evict = [ + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + CacheEvict(cacheNames = [STAMP], allEntries = true), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict(cacheNames = [TRIPS], allEntries = true), + ], + ) + @Transactional + fun updateStampOrders( + memberId: Long, + tripId: Long, + request: UpdateStampOrderRequest, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + + stampCommandService.updateStampOrders(trip, request) + } + + @Caching( + evict = [ + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + CacheEvict( + cacheNames = [STAMP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)", + ), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict(cacheNames = [TRIPS], allEntries = true), + ], + ) + @Transactional + fun deleteStamp( + memberId: Long, + tripId: Long, + stampId: Long, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val stamp = stampQueryService.getValidStamp(trip.id, stampId) + + stampCommandService.deleteStamp(stamp) + shiftStampOrdersIfTripCategoryIsCourse(trip, stamp.stampOrder) + tripCommandService.decreaseTotalStamps(trip) + } + + @Caching( + evict = [ + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + CacheEvict( + cacheNames = [STAMP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)", + ), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict(cacheNames = [TRIPS], allEntries = true), + ], + ) + @Transactional + fun completeStamp( + memberId: Long, + tripId: Long, + stampId: Long, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val stamp = stampQueryService.getValidStamp(trip.id, stampId) + + missionCommandService.validateAllMissionsCompletedByStampId(stamp.id) + + stampCommandService.completeStamp(stamp) + tripCommandService.increaseCompletedStamps(trip) + } + + @Cacheable( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ) + @Transactional(readOnly = true) + fun getStampsByTrip( + memberId: Long, + tripId: Long, + ): StampsInfo { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val stamps = stampQueryService.getStampsByTripId(trip.id) + + return StampsInfo.of(stamps.map { StampInfo.from(it) }) + } + + @Cacheable( + cacheNames = [STAMP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamp(#memberId, #tripId, #stampId)", + ) + @Transactional(readOnly = true) + fun getStamp( + memberId: Long, + tripId: Long, + stampId: Long, + ): StampDetail { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val stamp = stampQueryService.getValidStamp(trip.id, stampId) + val missions = missionQueryService.getMissionsByStampId(stamp.id) + + return StampDetail.from(StampInfo.from(stamp), missions.map { MissionInfo.from(it) }) + } + + private fun shiftStampOrdersIfTripCategoryIsCourse( + trip: Trip, + stampOrder: Int, + ) { + if (trip.category != TripCategory.COURSE) return + + val affectedStamps = stampQueryService.getStampsToShiftAfterDeleted(trip.id, stampOrder) + + stampCommandService.shiftStampOrders(affectedStamps) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/application/service/StampCommandService.kt b/src/main/kotlin/com/ject/studytrip/stamp/application/service/StampCommandService.kt new file mode 100644 index 0000000..eeaa799 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/application/service/StampCommandService.kt @@ -0,0 +1,143 @@ +package com.ject.studytrip.stamp.application.service + +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.StampCommandRepository +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.UpdateStampOrderRequest +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import org.springframework.stereotype.Service + +@Service +class StampCommandService( + private val stampRepository: StampRepository, + private val stampCommandRepository: StampCommandRepository, +) { + fun createStamp( + trip: Trip, + nextOrder: Int, + request: CreateStampRequest, + ): Stamp { + val stamp = StampFactory.create(trip, request.name, nextOrder, request.endDate) + + StampPolicy.validateNotStampEndDateAfterTripEndDate(trip.endDate, stamp.endDate) + + return stampRepository.save(stamp) + } + + fun createStamps( + trip: Trip, + nextOrder: Int, + requests: List?, + ) { + if (requests.isNullOrEmpty()) return + + val stamps: List = + when (trip.category) { + // 탐험형 여행이라면 order 0 으로 전부 고정 + TripCategory.EXPLORE -> { + requests.map { request -> + StampFactory.create(trip, request.name, 0, request.endDate) + } + } + + // 코스형 여행이라면 nextOrder 부터 1씩 증가 + TripCategory.COURSE -> { + requests.mapIndexed { index, request -> + StampFactory.create(trip, request.name, nextOrder + index, request.endDate) + } + } + } + + stamps.forEach { stamp -> + StampPolicy.validateNotStampEndDateAfterTripEndDate(trip.endDate, stamp.endDate) + } + + stampRepository.saveAll(stamps) + } + + fun updateStamp( + trip: Trip, + stamp: Stamp, + request: UpdateStampRequest, + ) { + stamp.updateName(request.name) + + StampPolicy.validateNotStampEndDateAfterTripEndDate(trip.endDate, request.endDate) + + stamp.updateEndDate(request.endDate) + } + + fun updateStampOrders( + trip: Trip, + request: UpdateStampOrderRequest, + ) { + // 배치 조회 (ID 목록 기준으로 조회하지만 순서는 보장되지 않음) + val stamps = stampRepository.findAllByIdIn(request.orderedStampIds) + + StampPolicy.validateUpdateStampOrders(trip.category, request.orderedStampIds, stamps) + stamps.forEach { stamp -> + StampPolicy.validateStampBelongsToTrip(trip.id, stamp) + StampPolicy.validateNotDeleted(stamp) + StampPolicy.validateNotCompleted(stamp) + } + + // 조회된 스탬프 ID 를 기준으로 매핑 + val stampMap = stamps.associateBy { it.id } + + // 요청에서 전달된 ID 순서를 기준으로 스탬프 리스트 재정렬 + val orderedStamps = request.orderedStampIds.mapNotNull { stampMap[it] } + + orderedStamps.forEachIndexed { index, stamp -> stamp.updateStampOrder(index + 1) } + } + + fun updateStampOrdersByTripCategoryChange( + tripId: Long, + newCategory: TripCategory, + ) { + val stamps = stampRepository.findAllByTripIdOrderByCreatedAtAsc(tripId) + + when (newCategory) { + // 탐험형 + TripCategory.EXPLORE -> stamps.forEach { stamp -> stamp.updateStampOrder(0) } + + // 코스형 + TripCategory.COURSE -> stamps.forEachIndexed { index, stamp -> stamp.updateStampOrder(index + 1) } + } + } + + fun deleteStamp(stamp: Stamp) = stamp.updateDeletedAt() + + fun completeStamp(stamp: Stamp) = stamp.updateCompleted() + + fun shiftStampOrders(affectedStamps: List) = affectedStamps.forEach { stamp -> stamp.updateStampOrder(stamp.stampOrder - 1) } + + fun increaseTotalMissions(stamp: Stamp) = stamp.increaseTotalMissions() + + fun decreaseTotalMissions(stamp: Stamp) = stamp.decreaseTotalMissions() + + fun increaseCompletedMissions( + stamp: Stamp, + count: Int, + ) = stamp.increaseCompletedMissions(count) + + fun validateStampBelongsToTrip( + tripId: Long, + stamp: Stamp, + ) = StampPolicy.validateStampBelongsToTrip(tripId, stamp) + + fun validateAllStampsCompletedByTripId(tripId: Long) { + val exists = stampCommandRepository.existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(tripId) + StampPolicy.validateNotAllCompleted(exists) + } + + fun hardDeleteStamps() = stampCommandRepository.deleteAllByDeletedAtIsNotNull() + + fun hardDeleteStampsOwnedByDeletedTrip() = stampCommandRepository.deleteAllByDeletedTripOwner() + + fun hardDeleteStampsOwnedByMember(memberId: Long) = stampCommandRepository.deleteAllByMemberId(memberId) +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/application/service/StampQueryService.kt b/src/main/kotlin/com/ject/studytrip/stamp/application/service/StampQueryService.kt new file mode 100644 index 0000000..e01ad3a --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/application/service/StampQueryService.kt @@ -0,0 +1,85 @@ +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.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.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import org.springframework.stereotype.Service + +@Service +class StampQueryService( + private val stampRepository: StampRepository, + private val stampQueryRepository: StampQueryRepository, +) { + fun getValidStamp( + tripId: Long, + stampId: Long, + ): Stamp { + val stamp = + stampRepository + .findById(stampId) + .orElseThrow { CustomException(StampErrorCode.STAMP_NOT_FOUND) } + + StampPolicy.validateStampBelongsToTrip(tripId, stamp) + StampPolicy.validateNotDeleted(stamp) + StampPolicy.validateNotCompleted(stamp) + + return stamp + } + + fun getStampsByTripId(tripId: Long): List = stampRepository.findAllByTripIdAndDeletedAtIsNull(tripId) + + fun getFirstInProcessingStampsForCourseTrip(tripId: Long): Stamp = + stampQueryRepository + .findFirstIncompleteStampByTripId(tripId) + .orElseThrow { CustomException(StampErrorCode.STAMP_NOT_FOUND) } + + fun getStampNameByTripCategory( + tripCategory: TripCategory, + stamps: List, + ): String { + // 스탬프 목록이 비어있지 않은지 검증 + StampPolicy.validateNotStampListEmpty(stamps) + stamps.forEach { stamp -> + StampPolicy.validateNotDeleted(stamp) + StampPolicy.validateNotCompleted(stamp) + } + + // 코스형 여행은 상위 검증에서 동일한 스탬프인지 검증이 완료된 상태이므로, 스탬프 리스트에서 첫번째 스탬프 이름을 추출해도 안전 + if (tripCategory == TripCategory.COURSE) return stamps.first().name + + return getExploreStampName(stamps) + } + + fun getNextStampOrderByTrip(trip: Trip): Int { + if (trip.category != TripCategory.COURSE) return 0 + + return stampQueryRepository.findNextStampOrderByTripId(trip.id) + } + + fun getStampsToShiftAfterDeleted( + tripId: Long, + deletedStampOrder: Int, + ): List = stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedStampOrder) + + private fun getExploreStampName(stamps: List): String { + // 스탬프별 개수 집계 + val stampCountMap = stamps.groupingBy { it }.eachCount().mapValues { it.value.toLong() } + + // 최대 개수 + val maxCount = stampCountMap.values.maxOrNull() ?: 0L + + // 최대 개수를 가진 스탬프들 찾기 + val maxCountStamps: List = stampCountMap.filterValues { it == maxCount }.keys.toList() + + // 가장 빠른 생성 시간을 가진 스탬프 선택 + return maxCountStamps + .minByOrNull { it.createdAt } + ?.name + ?: "" + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/domain/error/StampErrorCode.kt b/src/main/kotlin/com/ject/studytrip/stamp/domain/error/StampErrorCode.kt new file mode 100644 index 0000000..5e70a7e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/domain/error/StampErrorCode.kt @@ -0,0 +1,31 @@ +package com.ject.studytrip.stamp.domain.error + +import com.ject.studytrip.global.exception.error.ErrorCode +import org.springframework.http.HttpStatus + +enum class StampErrorCode( + private val status: HttpStatus, + private val message: String, +) : ErrorCode { + // 400 + STAMP_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 스탬프입니다."), + STAMP_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "이미 완료된 스탬프입니다."), + ALL_STAMPS_NOT_COMPLETED(HttpStatus.BAD_REQUEST, "모든 스탬프가 완료되지 않았습니다."), + STAMP_LIST_NOT_EMPTY(HttpStatus.BAD_REQUEST, "스탬프 목록은 비어있을 수 없습니다."), + STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "스탬프 종료일은 여행 종료일보다 이후일 수 없습니다."), + CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP(HttpStatus.BAD_REQUEST, "탐험형 여행의 스탬프 순서는 변경할 수 없습니다."), + INVALID_STAMP_ID_IN_REQUEST(HttpStatus.BAD_REQUEST, "유효하지 않은 스탬프 ID가 요청에 포함되어 있습니다."), + + // 403 + STAMP_NOT_BELONGS_TO_TRIP(HttpStatus.FORBIDDEN, "해당 스탬프는 요청한 여행에 속하지 않습니다."), + + // 404 + STAMP_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 스탬프를 찾을 수 없습니다."), + ; + + override fun getName(): String = name + + override fun getStatus(): HttpStatus = status + + override fun getMessage(): String = message +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/domain/factory/StampFactory.kt b/src/main/kotlin/com/ject/studytrip/stamp/domain/factory/StampFactory.kt new file mode 100644 index 0000000..5f498bd --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/domain/factory/StampFactory.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.stamp.domain.factory + +import com.ject.studytrip.stamp.domain.model.Stamp +import com.ject.studytrip.trip.domain.model.Trip +import java.time.LocalDate + +object StampFactory { + @JvmStatic + fun create( + trip: Trip, + name: String, + stampOrder: Int, + endDate: LocalDate?, + ): Stamp = Stamp.of(trip, name, stampOrder, endDate) +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/domain/policy/StampPolicy.kt b/src/main/kotlin/com/ject/studytrip/stamp/domain/policy/StampPolicy.kt new file mode 100644 index 0000000..9bdf4a2 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/domain/policy/StampPolicy.kt @@ -0,0 +1,68 @@ +package com.ject.studytrip.stamp.domain.policy + +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.stamp.domain.error.StampErrorCode +import com.ject.studytrip.stamp.domain.model.Stamp +import com.ject.studytrip.trip.domain.model.TripCategory +import java.time.LocalDate + +object StampPolicy { + fun validateNotDeleted(stamp: Stamp) { + if (stamp.isDeleted) { + throw CustomException(StampErrorCode.STAMP_ALREADY_DELETED) + } + } + + fun validateNotCompleted(stamp: Stamp) { + if (stamp.isCompleted) { + throw CustomException(StampErrorCode.STAMP_ALREADY_COMPLETED) + } + } + + fun validateNotAllCompleted(exists: Boolean) { + if (exists) { + throw CustomException(StampErrorCode.ALL_STAMPS_NOT_COMPLETED) + } + } + + fun validateNotStampListEmpty(stamps: List) { + if (stamps.isEmpty()) { + throw CustomException(StampErrorCode.STAMP_LIST_NOT_EMPTY) + } + } + + fun validateNotStampEndDateAfterTripEndDate( + tripEndDate: LocalDate?, + stampEndDate: LocalDate?, + ) { + if (tripEndDate == null || stampEndDate == null) return + + // 스탬프 종료일이 여행 종료일 이후일 경우 + if (stampEndDate.isAfter(tripEndDate)) { + throw CustomException(StampErrorCode.STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED) + } + } + + fun validateUpdateStampOrders( + tripCategory: TripCategory, + orderedStampIds: List, + savedStamps: List, + ) { + if (tripCategory == TripCategory.EXPLORE && orderedStampIds.isNotEmpty()) { + throw CustomException(StampErrorCode.CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP) + } + + if (orderedStampIds.size != savedStamps.size) { + throw CustomException(StampErrorCode.INVALID_STAMP_ID_IN_REQUEST) + } + } + + fun validateStampBelongsToTrip( + tripId: Long, + stamp: Stamp, + ) { + if (stamp.trip.id != tripId) { + throw CustomException(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP) + } + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/domain/repository/StampRepository.kt b/src/main/kotlin/com/ject/studytrip/stamp/domain/repository/StampRepository.kt new file mode 100644 index 0000000..e81ca06 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/domain/repository/StampRepository.kt @@ -0,0 +1,18 @@ +package com.ject.studytrip.stamp.domain.repository + +import com.ject.studytrip.stamp.domain.model.Stamp +import java.util.Optional + +interface StampRepository { + fun save(stamp: Stamp): Stamp + + fun saveAll(stamps: List): List + + fun findById(stampId: Long): Optional + + fun findAllByIdIn(stampIds: List): List + + fun findAllByTripIdAndDeletedAtIsNull(tripId: Long): List + + fun findAllByTripIdOrderByCreatedAtAsc(tripId: Long): List +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.kt b/src/main/kotlin/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.kt new file mode 100644 index 0000000..08fc3c1 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/infra/jpa/StampJpaRepository.kt @@ -0,0 +1,12 @@ +package com.ject.studytrip.stamp.infra.jpa + +import com.ject.studytrip.stamp.domain.model.Stamp +import org.springframework.data.jpa.repository.JpaRepository + +interface StampJpaRepository : JpaRepository { + fun findAllByIdIn(stampIds: List): List + + fun findAllByTripIdAndDeletedAtIsNull(tripId: Long): List + + fun findAllByTripIdOrderByCreatedAtAsc(tripId: Long): List +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.kt b/src/main/kotlin/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.kt new file mode 100644 index 0000000..f07f4e4 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/infra/jpa/StampRepositoryAdapter.kt @@ -0,0 +1,24 @@ +package com.ject.studytrip.stamp.infra.jpa + +import com.ject.studytrip.stamp.domain.model.Stamp +import com.ject.studytrip.stamp.domain.repository.StampRepository +import org.springframework.stereotype.Repository +import java.util.Optional + +@Repository +class StampRepositoryAdapter( + private val stampJpaRepository: StampJpaRepository, +) : StampRepository { + override fun save(stamp: Stamp): Stamp = stampJpaRepository.save(stamp) + + override fun saveAll(stamps: List): List = stampJpaRepository.saveAll(stamps) + + override fun findById(stampId: Long): Optional = stampJpaRepository.findById(stampId) + + override fun findAllByIdIn(stampIds: List): List = stampJpaRepository.findAllByIdIn(stampIds) + + override fun findAllByTripIdAndDeletedAtIsNull(tripId: Long): List = stampJpaRepository.findAllByTripIdAndDeletedAtIsNull(tripId) + + override fun findAllByTripIdOrderByCreatedAtAsc(tripId: Long): List = + stampJpaRepository.findAllByTripIdOrderByCreatedAtAsc(tripId) +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/presentation/controller/StampController.kt b/src/main/kotlin/com/ject/studytrip/stamp/presentation/controller/StampController.kt new file mode 100644 index 0000000..1a09644 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/presentation/controller/StampController.kt @@ -0,0 +1,134 @@ +package com.ject.studytrip.stamp.presentation.controller + +import com.ject.studytrip.global.common.response.StandardResponse +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.UpdateStampOrderRequest +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest +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 org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Stamp", description = "스탬프 API") +@RestController +@RequestMapping("/api/trips/{tripId}/stamps") +@Validated +class StampController( + private val stampFacade: StampFacade, +) { + @Operation(summary = "스탬프 생성", description = "특정 여행에 새로운 스탬프를 생성합니다.") + @PostMapping + fun createStamp( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @RequestBody @Valid request: CreateStampRequest, + ): ResponseEntity { + val result = stampFacade.createStamp(memberId.toLong(), tripId, request) + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(StandardResponse.success(HttpStatus.CREATED.value(), CreateStampResponse.of(result))) + } + + @Operation(summary = "스탬프 수정", description = "특정 스탬프를 수정합니다. 종료일을 '없음'으로 변경하려면 요청 바디를 생략해서 전달해야 합니다.") + @PatchMapping("/{stampId}") + fun updateStamp( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") stampId: Long, + @RequestBody @Valid request: UpdateStampRequest, + ): ResponseEntity { + stampFacade.updateStamp(memberId.toLong(), tripId, stampId, request) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "스탬프 순서 변경", description = "특정 스탬프의 순서를 변경합니다.") + @PutMapping("/orders") + fun updateStampOrders( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @RequestBody @Valid request: UpdateStampOrderRequest, + ): ResponseEntity { + stampFacade.updateStampOrders(memberId.toLong(), tripId, request) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "스탬프 삭제", description = "특정 스탬프를 삭제합니다.") + @DeleteMapping("/{stampId}") + fun deleteStamp( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") stampId: Long, + ): ResponseEntity { + stampFacade.deleteStamp(memberId.toLong(), tripId, stampId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "스탬프 완료", description = "특정 스탬프의 모든 미션이 완료된 경우에만 스탬프를 완료합니다") + @PatchMapping("/{stampId}/complete") + fun completeStamp( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") stampId: Long, + ): ResponseEntity { + stampFacade.completeStamp(memberId.toLong(), tripId, stampId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "스탬프 목록 조회", description = "특정 여행의 스탬프 목록을 조회합니다.") + @GetMapping + fun loadStampsByTrip( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + ): ResponseEntity { + val result = stampFacade.getStampsByTrip(memberId.toLong(), tripId) + val responses = result.stampInfos.map { LoadStampInfoResponse.of(it) } + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), responses)) + } + + @Operation(summary = "스탬프 상세 조회", description = "특정 스탬프를 상세 조회합니다.") + @GetMapping("/{stampId}") + fun loadStamp( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @PathVariable @NotNull(message = "스탬프 ID는 필수 요청 파라미터입니다.") stampId: Long, + ): ResponseEntity { + val result = stampFacade.getStamp(memberId.toLong(), tripId, stampId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), LoadStampDetailResponse.of(result.stampInfo, result.missionInfos))) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/CreateStampRequest.kt b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/CreateStampRequest.kt new file mode 100644 index 0000000..96506d5 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/CreateStampRequest.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.stamp.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.FutureOrPresent +import jakarta.validation.constraints.NotBlank +import java.time.LocalDate + +data class CreateStampRequest( + @field:Schema(description = "스탬프 이름") + @field:NotBlank(message = "스탬프 이름은 필수 요청 값입니다.") + val name: String, + @field:Schema(description = "스탬프 종료일") + @field:FutureOrPresent(message = "스탬프 종료일은 현재 날짜보다 과거일 수 없습니다.") + val endDate: LocalDate? = null, +) diff --git a/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.kt b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.kt new file mode 100644 index 0000000..e495ca0 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampOrderRequest.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.stamp.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotEmpty + +data class UpdateStampOrderRequest( + @field:Schema(description = "변경된 순서를 반영한 스탬프 ID 목록") + @field:NotEmpty(message = "스탬프 순서를 변경하려면 최소 1개 이상의 ID가 필요합니다.") + val orderedStampIds: List, +) diff --git a/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampRequest.kt b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampRequest.kt new file mode 100644 index 0000000..c3cab34 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/request/UpdateStampRequest.kt @@ -0,0 +1,13 @@ +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 + +data class UpdateStampRequest( + @field:Schema(description = "수정할 스탬프 이름") + val name: String? = null, + @field:Schema(description = "수정할 스탬프 종료일") + @field:FutureOrPresent(message = "스탬프 종료일은 현재 날짜보다 과거일 수 없습니다.") + val endDate: LocalDate? = null, +) diff --git a/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.kt b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.kt new file mode 100644 index 0000000..2da5478 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/CreateStampResponse.kt @@ -0,0 +1,14 @@ +package com.ject.studytrip.stamp.presentation.dto.response + +import com.ject.studytrip.stamp.application.dto.StampInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateStampResponse( + @field:Schema(description = "스탬프 ID") + val stampId: Long, +) { + companion object { + @JvmStatic + fun of(stampInfo: StampInfo): CreateStampResponse = CreateStampResponse(stampInfo.stampId) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.kt b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.kt new file mode 100644 index 0000000..aa51de2 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/LoadStampDetailResponse.kt @@ -0,0 +1,43 @@ +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 + +data class LoadStampDetailResponse( + @field:Schema(description = "스탬프 ID") + val stampId: Long, + @field:Schema(description = "스탬프 이름") + val stampName: String, + @field:Schema(description = "스탬프 순서") + val stampOrder: Int, + @field:Schema(description = "스탬프 종료일") + val endDate: String, + @field:Schema(description = "스탬프에 속한 총 미션 수") + val totalMissions: Int, + @field:Schema(description = "스탬프에 속한 완료된 미션 수") + val completedMissions: Int, + @field:Schema(description = "스탬프 완료 여부") + val completed: Boolean, + @field:Schema(description = "미션 목록") + val missions: List, +) { + companion object { + @JvmStatic + fun of( + stampInfo: StampInfo, + missionInfos: List, + ): LoadStampDetailResponse = + LoadStampDetailResponse( + stampInfo.stampId, + stampInfo.stampName, + stampInfo.stampOrder, + stampInfo.endDate, + stampInfo.totalMissions, + stampInfo.completedMissions, + stampInfo.completed, + missionInfos.map(LoadMissionInfoResponse::of), + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.kt b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.kt new file mode 100644 index 0000000..07d39dd --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/stamp/presentation/dto/response/LoadStampInfoResponse.kt @@ -0,0 +1,35 @@ +package com.ject.studytrip.stamp.presentation.dto.response + +import com.ject.studytrip.stamp.application.dto.StampInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadStampInfoResponse( + @field:Schema(description = "스탬프 ID") + val stampId: Long, + @field:Schema(description = "스탬프 이름") + val stampName: String, + @field:Schema(description = "스탬프 순서") + val stampOrder: Int, + @field:Schema(description = "스탬프 종료일") + val endDate: String, + @field:Schema(description = "스탬프에 속한 총 미션 수") + val totalMissions: Int, + @field:Schema(description = "스탬프에 속한 완료된 미션 수") + val completedMissions: Int, + @field:Schema(description = "스탬프 완료 여부") + val completed: Boolean, +) { + companion object { + @JvmStatic + fun of(stampInfo: StampInfo): LoadStampInfoResponse = + LoadStampInfoResponse( + stampInfo.stampId, + stampInfo.stampName, + stampInfo.stampOrder, + stampInfo.endDate, + stampInfo.totalMissions, + stampInfo.completedMissions, + stampInfo.completed, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/studylog/domain/repository/StudyLogRepository.kt b/src/main/kotlin/com/ject/studytrip/studylog/domain/repository/StudyLogRepository.kt index 22da36b..1078625 100644 --- a/src/main/kotlin/com/ject/studytrip/studylog/domain/repository/StudyLogRepository.kt +++ b/src/main/kotlin/com/ject/studytrip/studylog/domain/repository/StudyLogRepository.kt @@ -8,5 +8,5 @@ interface StudyLogRepository { fun findById(studyLogId: Long): Optional - fun findAllByIdIn(studyLogIds: Collection): List + fun findAllByIdIn(studyLogIds: List): List } diff --git a/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogJpaRepository.kt b/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogJpaRepository.kt index 80ccd82..f2412f9 100644 --- a/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogJpaRepository.kt +++ b/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogJpaRepository.kt @@ -4,5 +4,5 @@ import com.ject.studytrip.studylog.domain.model.StudyLog import org.springframework.data.jpa.repository.JpaRepository interface StudyLogJpaRepository : JpaRepository { - fun findAllByIdIn(studyLogIds: Collection): List + fun findAllByIdIn(studyLogIds: List): List } diff --git a/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogRepositoryAdapter.kt b/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogRepositoryAdapter.kt index 3126793..54a84ab 100644 --- a/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogRepositoryAdapter.kt +++ b/src/main/kotlin/com/ject/studytrip/studylog/infra/jpa/StudyLogRepositoryAdapter.kt @@ -13,5 +13,5 @@ class StudyLogRepositoryAdapter( override fun findById(studyLogId: Long): Optional = studyLogJpaRepository.findById(studyLogId) - override fun findAllByIdIn(studyLogIds: Collection): List = studyLogJpaRepository.findAllByIdIn(studyLogIds) + override fun findAllByIdIn(studyLogIds: List): List = studyLogJpaRepository.findAllByIdIn(studyLogIds) } diff --git a/src/main/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogController.kt b/src/main/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogController.kt index bda7925..0e38d97 100644 --- a/src/main/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogController.kt +++ b/src/main/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogController.kt @@ -47,7 +47,7 @@ class StudyLogController( .body(StandardResponse.success(HttpStatus.CREATED.value(), CreateStudyLogResponse.of(result))) } - @Operation(summary = "여행의 학습 로그 목록 조회", description = "특정 여행의 학습 로그 목록을 조회합니다. 슬라이스를 적용하고 정렬 옵션 LATEST(최신순)/OLDEST(과거순)을 적용합니다.") + @Operation(summary = "학습 로그 목록 조회", description = "특정 여행의 학습 로그 목록을 조회합니다. 슬라이스를 적용하고 정렬 옵션 LATEST(최신순)/OLDEST(과거순)을 적용합니다.") @GetMapping("/api/trips/{tripId}/study-logs") fun loadStudyLogsByTrip( @AuthenticationPrincipal memberId: String, diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/DailyGoalDetail.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/DailyGoalDetail.kt new file mode 100644 index 0000000..dd44add --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/DailyGoalDetail.kt @@ -0,0 +1,19 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.mission.application.dto.DailyMissionInfo +import com.ject.studytrip.pomodoro.application.dto.PomodoroInfo + +data class DailyGoalDetail( + val dailyGoalInfo: DailyGoalInfo, + val pomodoroInfo: PomodoroInfo, + val dailyMissionInfos: List, +) { + companion object { + @JvmStatic + fun from( + dailyGoalInfo: DailyGoalInfo, + pomodoroInfo: PomodoroInfo, + dailyMissionInfos: List, + ): DailyGoalDetail = DailyGoalDetail(dailyGoalInfo, pomodoroInfo, dailyMissionInfos) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/DailyGoalInfo.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/DailyGoalInfo.kt new file mode 100644 index 0000000..26af3ee --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/DailyGoalInfo.kt @@ -0,0 +1,26 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.global.util.DateUtil +import com.ject.studytrip.trip.domain.model.DailyGoal + +data class DailyGoalInfo( + val dailyGoalId: Long, + val title: String, + val completed: Boolean, + val createdAt: String, + val updatedAt: String, + val deletedAt: String?, +) { + companion object { + @JvmStatic + fun from(dailyGoal: DailyGoal): DailyGoalInfo = + DailyGoalInfo( + dailyGoal.id, + dailyGoal.title, + dailyGoal.isCompleted, + DateUtil.formatDateTime(dailyGoal.createdAt), + DateUtil.formatDateTime(dailyGoal.updatedAt), + dailyGoal.deletedAt?.let { DateUtil.formatDateTime(it) }, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripCategoryInfo.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripCategoryInfo.kt new file mode 100644 index 0000000..8e96f10 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripCategoryInfo.kt @@ -0,0 +1,13 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.trip.domain.model.TripCategory + +data class TripCategoryInfo( + val name: String, + val value: String, +) { + companion object { + @JvmStatic + fun from(category: TripCategory): TripCategoryInfo = TripCategoryInfo(category.name, category.getValue()) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripCount.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripCount.kt new file mode 100644 index 0000000..cdd89a5 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripCount.kt @@ -0,0 +1,14 @@ +package com.ject.studytrip.trip.application.dto + +data class TripCount( + val course: Long, + val explore: Long, +) { + companion object { + @JvmStatic + fun of( + course: Long, + explore: Long, + ): TripCount = TripCount(course, explore) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripDetail.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripDetail.kt new file mode 100644 index 0000000..e5e346e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripDetail.kt @@ -0,0 +1,16 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.stamp.application.dto.StampInfo + +data class TripDetail( + val tripInfo: TripInfo, + val stampInfos: List, +) { + companion object { + @JvmStatic + fun from( + tripInfo: TripInfo, + stampInfos: List, + ): TripDetail = TripDetail(tripInfo, stampInfos) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripInfo.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripInfo.kt new file mode 100644 index 0000000..51da4cf --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripInfo.kt @@ -0,0 +1,47 @@ +package com.ject.studytrip.trip.application.dto + +import com.ject.studytrip.global.util.DateUtil +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory + +data class TripInfo( + val tripId: Long, + val tripName: String, + val tripMemo: String, + val tripCategory: TripCategory, + val startDate: String, + val endDate: String?, + val dDay: Int?, + val totalStamps: Int, + val completedStamps: Int, + val progress: Int?, + val completed: Boolean, + val createdAt: String, + val updatedAt: String, + val deletedAt: String?, +) { + companion object { + @JvmStatic + fun from( + trip: Trip, + dDay: Int?, + progress: Int?, + ): TripInfo = + TripInfo( + trip.id, + trip.name, + trip.memo, + trip.category, + DateUtil.formatDate(trip.startDate), + trip.endDate?.let { DateUtil.formatDate(it) }, + dDay, + trip.totalStamps, + trip.completedStamps, + progress, + trip.isCompleted, + DateUtil.formatDateTime(trip.createdAt), + DateUtil.formatDateTime(trip.updatedAt), + trip.deletedAt?.let { DateUtil.formatDateTime(it) }, + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripSliceInfo.kt b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripSliceInfo.kt new file mode 100644 index 0000000..6531948 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/dto/TripSliceInfo.kt @@ -0,0 +1,14 @@ +package com.ject.studytrip.trip.application.dto + +data class TripSliceInfo( + val tripInfos: List, + val hasNext: Boolean, +) { + companion object { + @JvmStatic + fun of( + tripInfos: List, + hasNext: Boolean, + ): TripSliceInfo = TripSliceInfo(tripInfos, hasNext) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/facade/DailyGoalFacade.kt b/src/main/kotlin/com/ject/studytrip/trip/application/facade/DailyGoalFacade.kt new file mode 100644 index 0000000..b237a19 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/facade/DailyGoalFacade.kt @@ -0,0 +1,150 @@ +package com.ject.studytrip.trip.application.facade + +import com.ject.studytrip.global.common.constants.CacheNameConstants.DAILY_GOAL +import com.ject.studytrip.mission.application.dto.DailyMissionInfo +import com.ject.studytrip.mission.application.service.DailyMissionCommandService +import com.ject.studytrip.mission.application.service.DailyMissionQueryService +import com.ject.studytrip.mission.application.service.MissionCommandService +import com.ject.studytrip.mission.application.service.MissionQueryService +import com.ject.studytrip.mission.domain.model.Mission +import com.ject.studytrip.pomodoro.application.dto.PomodoroInfo +import com.ject.studytrip.pomodoro.application.service.PomodoroCommandService +import com.ject.studytrip.pomodoro.application.service.PomodoroQueryService +import com.ject.studytrip.stamp.application.service.StampCommandService +import com.ject.studytrip.stamp.application.service.StampQueryService +import com.ject.studytrip.trip.application.dto.DailyGoalDetail +import com.ject.studytrip.trip.application.dto.DailyGoalInfo +import com.ject.studytrip.trip.application.service.DailyGoalCommandService +import com.ject.studytrip.trip.application.service.DailyGoalQueryService +import com.ject.studytrip.trip.application.service.TripQueryService +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest +import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest +import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class DailyGoalFacade( + // Query Service + private val tripQueryService: TripQueryService, + private val stampQueryService: StampQueryService, + private val missionQueryService: MissionQueryService, + private val dailyGoalQueryService: DailyGoalQueryService, + private val dailyMissionQueryService: DailyMissionQueryService, + private val pomodoroQueryService: PomodoroQueryService, + // Command Service + private val stampCommandService: StampCommandService, + private val missionCommandService: MissionCommandService, + private val dailyGoalCommandService: DailyGoalCommandService, + private val dailyMissionCommandService: DailyMissionCommandService, + private val pomodoroCommandService: PomodoroCommandService, +) { + @Transactional + fun createDailyGoal( + memberId: Long, + tripId: Long, + request: CreateDailyGoalRequest, + ): DailyGoalInfo { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val missions = getValidMissionsByTripCategory(trip, request.missionIds) + val title = stampQueryService.getStampNameByTripCategory(trip.category, missions.map { it.stamp }) + + val dailyGoal = dailyGoalCommandService.createDailyGoal(trip, title) + dailyMissionCommandService.createDailyMissions(dailyGoal, missions) + pomodoroCommandService.createPomodoro(dailyGoal, request.pomodoro) + + return DailyGoalInfo.from(dailyGoal) + } + + @CacheEvict( + cacheNames = [DAILY_GOAL], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).dailyGoal(#memberId, #tripId, #dailyGoalId)", + ) + @Transactional + fun updateDailyGoal( + memberId: Long, + tripId: Long, + dailyGoalId: Long, + request: UpdateDailyGoalRequest, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val dailyGoal = dailyGoalQueryService.getValidDailyGoal(trip.id, dailyGoalId) + + // 삭제할 데일리 미션이 있을 경우 + if (request.deleteDailyMissionIds.isNotEmpty()) { + val deleteDailyMissions = dailyMissionQueryService.getValidDailyMissionsByIds(dailyGoal.id, request.deleteDailyMissionIds) + deleteDailyMissions.forEach { dailyMissionCommandService.deleteDailyMission(it) } + } + + // 추가할 미션이 있을 경우 + if (request.addMissionIds.isNotEmpty()) { + val addMissions = getValidMissionsByTripCategory(trip, request.addMissionIds) + dailyMissionCommandService.createDailyMissions(dailyGoal, addMissions) + } + } + + @CacheEvict( + cacheNames = [DAILY_GOAL], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).dailyGoal(#memberId, #tripId, #dailyGoalId)", + ) + @Transactional + fun deleteDailyGoal( + memberId: Long, + tripId: Long, + dailyGoalId: Long, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val dailyGoal = dailyGoalQueryService.getValidDailyGoal(trip.id, dailyGoalId) + val pomodoro = pomodoroQueryService.getValidPomodoroByDailyGoalId(dailyGoal.id) + + // 뽀모도로 삭제 + pomodoroCommandService.deletePomodoro(pomodoro) + + // 데일리 미션 삭제 + val dailyMissions = dailyMissionQueryService.getDailyMissionsByDailyGoalId(dailyGoal.id) + dailyMissions.forEach { dailyMissionCommandService.deleteDailyMission(it) } + + // 데일리 목표 삭제 + dailyGoalCommandService.deleteDailyGoal(dailyGoal) + } + + @Cacheable( + cacheNames = [DAILY_GOAL], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).dailyGoal(#memberId, #tripId, #dailyGoalId)", + ) + @Transactional(readOnly = true) + fun getDailyGoal( + memberId: Long, + tripId: Long, + dailyGoalId: Long, + ): DailyGoalDetail { + val trip = tripQueryService.getValidTrip(memberId, tripId) + val dailyGoal = dailyGoalQueryService.getValidDailyGoal(trip.id, dailyGoalId) + val pomodoro = pomodoroQueryService.getValidPomodoroByDailyGoalId(dailyGoal.id) + val dailyMissions = dailyMissionQueryService.getDailyMissionsByDailyGoalId(dailyGoal.id) + + return DailyGoalDetail.from( + DailyGoalInfo.from(dailyGoal), + PomodoroInfo.from(pomodoro), + dailyMissions.map(DailyMissionInfo::from), + ) + } + + private fun getValidMissionsByTripCategory( + trip: Trip, + missionIds: List, + ): List { + val missions = missionQueryService.getValidMissionsByIds(missionIds) + missions.forEach { stampCommandService.validateStampBelongsToTrip(trip.id, it.stamp) } + + if (trip.category == TripCategory.COURSE) { + val stampId = stampQueryService.getFirstInProcessingStampsForCourseTrip(trip.id).id + missionCommandService.validateMissionsBelongToStamp(stampId, missions) + } + + return missions + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/facade/TripFacade.kt b/src/main/kotlin/com/ject/studytrip/trip/application/facade/TripFacade.kt new file mode 100644 index 0000000..5aeb688 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/facade/TripFacade.kt @@ -0,0 +1,195 @@ +package com.ject.studytrip.trip.application.facade + +import com.ject.studytrip.global.common.constants.CacheNameConstants.STAMP +import com.ject.studytrip.global.common.constants.CacheNameConstants.STAMPS +import com.ject.studytrip.global.common.constants.CacheNameConstants.TRIP +import com.ject.studytrip.global.common.constants.CacheNameConstants.TRIPS +import com.ject.studytrip.member.application.service.MemberQueryService +import com.ject.studytrip.stamp.application.dto.StampInfo +import com.ject.studytrip.stamp.application.service.StampCommandService +import com.ject.studytrip.stamp.application.service.StampQueryService +import com.ject.studytrip.trip.application.dto.TripCategoryInfo +import com.ject.studytrip.trip.application.dto.TripDetail +import com.ject.studytrip.trip.application.dto.TripInfo +import com.ject.studytrip.trip.application.dto.TripSliceInfo +import com.ject.studytrip.trip.application.service.TripCommandService +import com.ject.studytrip.trip.application.service.TripQueryService +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest +import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest +import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.Cacheable +import org.springframework.cache.annotation.Caching +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +@Component +class TripFacade( + // Query Service + private val memberQueryService: MemberQueryService, + private val tripQueryService: TripQueryService, + private val stampQueryService: StampQueryService, + // Command Service + private val tripCommandService: TripCommandService, + private val stampCommandService: StampCommandService, +) { + @CacheEvict(cacheNames = [TRIPS], allEntries = true) + @Transactional + fun createTrip( + memberId: Long, + request: CreateTripRequest, + ): TripInfo { + val member = memberQueryService.getValidMember(memberId) + val trip = tripCommandService.createTrip(member, request) + + val nextOrder = stampQueryService.getNextStampOrderByTrip(trip) + stampCommandService.createStamps(trip, nextOrder, request.stamps) + + return TripInfo.from(trip, null, null) + } + + @Caching( + evict = [ + CacheEvict(cacheNames = [TRIPS], allEntries = true), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + CacheEvict(cacheNames = [STAMP], allEntries = true), + ], + ) + @Transactional + fun updateTrip( + memberId: Long, + tripId: Long, + request: UpdateTripRequest, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + + tripCommandService.updateTrip(trip, request) + + if (request.category != null) { + stampCommandService.updateStampOrdersByTripCategoryChange(trip.id, TripCategory.from(request.category)) + } + } + + @Caching( + evict = [ + CacheEvict(cacheNames = [TRIPS], allEntries = true), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + ], + ) + @Transactional + fun deleteTrip( + memberId: Long, + tripId: Long, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + + tripCommandService.deleteTrip(trip) + } + + @Caching( + evict = [ + CacheEvict(cacheNames = [TRIPS], allEntries = true), + CacheEvict( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ), + CacheEvict( + cacheNames = [STAMPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).stamps(#memberId, #tripId)", + ), + ], + ) + @Transactional + fun completeTrip( + memberId: Long, + tripId: Long, + ) { + val trip = tripQueryService.getValidTrip(memberId, tripId) + + stampCommandService.validateAllStampsCompletedByTripId(trip.id) + + tripCommandService.completeTrip(trip) + } + + @Transactional(readOnly = true) + fun loadTripCategories(): List = TripCategory.entries.map(TripCategoryInfo::from) + + @Cacheable( + cacheNames = [TRIPS], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trips(#memberId, #page, #size)", + ) + @Transactional(readOnly = true) + fun getTripsByMember( + memberId: Long, + page: Int, + size: Int, + ): TripSliceInfo { + val tripSlice = tripQueryService.getTripsSliceByMemberId(memberId, page, size) + + val tripInfos: List = + tripSlice.content + .map { trip -> + val dDay = calculateDDay(trip.endDate) + val progress = calculateProgress(trip.totalStamps, trip.completedStamps) + TripInfo.from(trip, dDay, progress) + }.sortedWith(compareBy(nullsLast()) { it.dDay }) + + return TripSliceInfo.of(tripInfos, tripSlice.hasNext()) + } + + @Cacheable( + cacheNames = [TRIP], + key = "T(com.ject.studytrip.global.common.factory.CacheKeyFactory).trip(#memberId, #tripId)", + ) + @Transactional(readOnly = true) + fun getTrip( + memberId: Long, + tripId: Long, + ): TripDetail { + val member = memberQueryService.getValidMember(memberId) + val trip = tripQueryService.getValidTrip(member.id, tripId) + + val dDay = calculateDDay(trip.endDate) + val progress: Int = calculateProgress(trip.totalStamps, trip.completedStamps) + + val stamps = stampQueryService.getStampsByTripId(trip.id) + val stampInfos = stamps.map { StampInfo.from(it) } + + return TripDetail.from(TripInfo.from(trip, dDay, progress), stampInfos) + } + + private fun calculateDDay(endDate: LocalDate?): Int? { + if (endDate == null) return null // 무기한 여행 + + val today = LocalDate.now() + + return ChronoUnit.DAYS.between(today, endDate).toInt() + } + + private fun calculateProgress( + totalStamps: Int, + completedStamps: Int, + ): Int { + if (totalStamps == 0) return 0 + + val ratio = completedStamps.toDouble() / totalStamps.toDouble() + + return (ratio * 100).toInt() + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/service/DailyGoalCommandService.kt b/src/main/kotlin/com/ject/studytrip/trip/application/service/DailyGoalCommandService.kt new file mode 100644 index 0000000..d952dcd --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/service/DailyGoalCommandService.kt @@ -0,0 +1,31 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.trip.domain.factory.DailyGoalFactory +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.repository.DailyGoalCommandRepository +import com.ject.studytrip.trip.domain.repository.DailyGoalRepository +import org.springframework.stereotype.Service + +@Service +class DailyGoalCommandService( + private val dailyGoalRepository: DailyGoalRepository, + private val dailyGoalCommandRepository: DailyGoalCommandRepository, +) { + fun createDailyGoal( + trip: Trip, + title: String, + ): DailyGoal { + val dailyGoal = DailyGoalFactory.create(trip, title) + + return dailyGoalRepository.save(dailyGoal) + } + + fun deleteDailyGoal(dailyGoal: DailyGoal) = dailyGoal.updateDeletedAt() + + fun hardDeleteDailyGoals() = dailyGoalCommandRepository.deleteAllByDeletedAtIsNotNull() + + fun hardDeleteDailyGoalsOwnedByDeletedTrip() = dailyGoalCommandRepository.deleteAllByDeletedTripOwner() + + fun hardDeleteDailyGoalsOwnedByMember(memberId: Long) = dailyGoalCommandRepository.deleteAllByMemberId(memberId) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/service/DailyGoalQueryService.kt b/src/main/kotlin/com/ject/studytrip/trip/application/service/DailyGoalQueryService.kt new file mode 100644 index 0000000..1cfa57b --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/service/DailyGoalQueryService.kt @@ -0,0 +1,28 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.policy.DailyGoalPolicy +import com.ject.studytrip.trip.domain.repository.DailyGoalRepository +import org.springframework.stereotype.Service + +@Service +class DailyGoalQueryService( + private val dailyGoalRepository: DailyGoalRepository, +) { + fun getValidDailyGoal( + tripId: Long, + dailyGoalId: Long, + ): DailyGoal { + val dailyGoal = + dailyGoalRepository + .findById(dailyGoalId) + .orElseThrow { CustomException(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND) } + + DailyGoalPolicy.validateDailyGoalBelongsToTrip(dailyGoal, tripId) + DailyGoalPolicy.validateNotDeleted(dailyGoal) + + return dailyGoal + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/service/TripCommandService.kt b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripCommandService.kt new file mode 100644 index 0000000..7811b14 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripCommandService.kt @@ -0,0 +1,56 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.factory.TripFactory +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.policy.TripPolicy +import com.ject.studytrip.trip.domain.repository.TripCommandRepository +import com.ject.studytrip.trip.domain.repository.TripRepository +import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest +import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest +import org.springframework.stereotype.Service + +@Service +class TripCommandService( + private val tripRepository: TripRepository, + private val tripCommandRepository: TripCommandRepository, +) { + fun createTrip( + member: Member, + request: CreateTripRequest, + ): Trip { + val category = TripCategory.from(request.category) + + TripPolicy.validateEndDateByCategory(category, request.endDate) + + val trip = TripFactory.create(member, request.name, request.memo, category, request.endDate, request.stamps.size) + + return tripRepository.save(trip) + } + + fun updateTrip( + trip: Trip, + request: UpdateTripRequest, + ) { + val category = request.category?.let { TripCategory.from(it) } + + trip.update(request.name, request.memo, category, request.endDate) + } + + fun deleteTrip(trip: Trip) = trip.updateDeletedAt() + + fun completeTrip(trip: Trip) = trip.updateCompleted() + + fun increaseTotalStamps(trip: Trip) = trip.increaseTotalStamps() + + fun decreaseTotalStamps(trip: Trip) = trip.decreaseTotalStamps() + + fun increaseCompletedStamps(trip: Trip) = trip.increaseCompletedStamps() + + fun hardDeleteTrips(): Long = tripCommandRepository.deleteAllByDeletedAtIsNotNull() + + fun hardDeleteTripsOwnedByDeletedMember(): Long = tripCommandRepository.deleteAllByDeletedMemberOwner() + + fun hardDeleteTripsOwnedByMember(memberId: Long): Long = tripCommandRepository.deleteAllByMemberId(memberId) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/application/service/TripQueryService.kt b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripQueryService.kt new file mode 100644 index 0000000..464b84e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/application/service/TripQueryService.kt @@ -0,0 +1,64 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.trip.application.dto.TripCount +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.domain.policy.TripPolicy +import com.ject.studytrip.trip.domain.repository.TripQueryRepository +import com.ject.studytrip.trip.domain.repository.TripRepository +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Slice +import org.springframework.stereotype.Service + +@Service +class TripQueryService( + private val tripRepository: TripRepository, + private val tripQueryRepository: TripQueryRepository, +) { + fun getValidTrip( + memberId: Long, + tripId: Long, + ): Trip { + val trip = + tripRepository + .findById(tripId) + .orElseThrow { CustomException(TripErrorCode.TRIP_NOT_FOUND) } + + TripPolicy.validateOwner(memberId, trip) + TripPolicy.validateNotDeleted(trip) + TripPolicy.validateNotCompleted(trip) + + return trip + } + + fun getTripsSliceByMemberId( + memberId: Long, + page: Int, + size: Int, + ): Slice = tripQueryRepository.findSliceByMemberIdAndCompletedFalseAndDeletedAtIsNull(memberId, PageRequest.of(page, size)) + + fun getActiveTripCountByMemberId(memberId: Long): TripCount { + val courseCount = tripQueryRepository.countActiveTripsByMemberIdAndCategory(memberId, TripCategory.COURSE) + val exploreCount = tripQueryRepository.countActiveTripsByMemberIdAndCategory(memberId, TripCategory.EXPLORE) + + return TripCount.of(courseCount, exploreCount) + } + + fun getValidCompletedTrip( + memberId: Long, + tripId: Long, + ): Trip { + val trip = + tripRepository + .findById(tripId) + .orElseThrow { CustomException(TripErrorCode.TRIP_NOT_FOUND) } + + TripPolicy.validateOwner(memberId, trip) + TripPolicy.validateNotDeleted(trip) + TripPolicy.validateCompleted(trip) + + return trip + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/error/DailyGoalErrorCode.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/error/DailyGoalErrorCode.kt new file mode 100644 index 0000000..141d7b1 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/error/DailyGoalErrorCode.kt @@ -0,0 +1,25 @@ +package com.ject.studytrip.trip.domain.error + +import com.ject.studytrip.global.exception.error.ErrorCode +import org.springframework.http.HttpStatus + +enum class DailyGoalErrorCode( + private val status: HttpStatus, + private val message: String, +) : ErrorCode { + // 400 + DAILY_GOAL_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 데일리 목표입니다."), + + // 403 + DAILY_GOAL_NOT_BELONGS_TO_TRIP(HttpStatus.FORBIDDEN, "해당 데일리 목표는 요청한 여행에 속하지 않습니다."), + + // 404 + DAILY_GOAL_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 데일리 목표를 찾을 수 없습니다."), + ; + + override fun getName(): String = name + + override fun getStatus(): HttpStatus = status + + override fun getMessage(): String = message +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/error/TripErrorCode.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/error/TripErrorCode.kt new file mode 100644 index 0000000..548d35e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/error/TripErrorCode.kt @@ -0,0 +1,29 @@ +package com.ject.studytrip.trip.domain.error + +import com.ject.studytrip.global.exception.error.ErrorCode +import org.springframework.http.HttpStatus + +enum class TripErrorCode( + private val status: HttpStatus, + private val message: String, +) : ErrorCode { + // 400 + TRIP_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 여행입니다."), + TRIP_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "이미 완료된 여행입니다."), + TRIP_NOT_COMPLETED(HttpStatus.BAD_REQUEST, "여행이 아직 완료되지 않았습니다."), + COURSE_TRIP_END_DATE_REQUIRED(HttpStatus.BAD_REQUEST, "코스형 여행은 종료일이 필수입니다."), + INVALID_TRIP_CATEGORY(HttpStatus.BAD_REQUEST, "여행 카테고리가 누락되거나 올바르지 않습니다."), + + // 403 + NOT_TRIP_OWNER(HttpStatus.FORBIDDEN, "여행을 수정/삭제할 권한이 없습니다."), + + // 404 + TRIP_NOT_FOUND(HttpStatus.NOT_FOUND, "요창한 여행을 찾을 수 없습니다."), + ; + + override fun getName(): String = name + + override fun getStatus(): HttpStatus = status + + override fun getMessage(): String = message +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/factory/DailyGoalFactory.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/DailyGoalFactory.kt new file mode 100644 index 0000000..8c4a162 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/DailyGoalFactory.kt @@ -0,0 +1,12 @@ +package com.ject.studytrip.trip.domain.factory + +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip + +object DailyGoalFactory { + @JvmStatic + fun create( + trip: Trip, + title: String, + ): DailyGoal = DailyGoal.of(trip, title) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripFactory.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripFactory.kt new file mode 100644 index 0000000..84a7a81 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/factory/TripFactory.kt @@ -0,0 +1,18 @@ +package com.ject.studytrip.trip.domain.factory + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import java.time.LocalDate + +object TripFactory { + @JvmStatic + fun create( + member: Member, + name: String, + memo: String?, + category: TripCategory, + endDate: LocalDate?, + totalStamps: Int, + ): Trip = Trip.of(member, name, memo, category, endDate, totalStamps) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/policy/DailyGoalPolicy.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/policy/DailyGoalPolicy.kt new file mode 100644 index 0000000..612f3ec --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/policy/DailyGoalPolicy.kt @@ -0,0 +1,22 @@ +package com.ject.studytrip.trip.domain.policy + +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode +import com.ject.studytrip.trip.domain.model.DailyGoal + +object DailyGoalPolicy { + fun validateNotDeleted(dailyGoal: DailyGoal) { + if (dailyGoal.isDeleted) { + throw CustomException(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED) + } + } + + fun validateDailyGoalBelongsToTrip( + dailyGoal: DailyGoal, + tripId: Long, + ) { + if (dailyGoal.trip.id != tripId) { + throw CustomException(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP) + } + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/policy/TripPolicy.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/policy/TripPolicy.kt new file mode 100644 index 0000000..e8d691d --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/policy/TripPolicy.kt @@ -0,0 +1,45 @@ +package com.ject.studytrip.trip.domain.policy + +import com.ject.studytrip.global.exception.CustomException +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 java.time.LocalDate + +object TripPolicy { + fun validateNotDeleted(trip: Trip) { + if (trip.isDeleted) { + throw CustomException(TripErrorCode.TRIP_ALREADY_DELETED) + } + } + + fun validateNotCompleted(trip: Trip) { + if (trip.isCompleted) { + throw CustomException(TripErrorCode.TRIP_ALREADY_COMPLETED) + } + } + + fun validateCompleted(trip: Trip) { + if (!trip.isCompleted) { + throw CustomException(TripErrorCode.TRIP_NOT_COMPLETED) + } + } + + fun validateEndDateByCategory( + category: TripCategory, + endDate: LocalDate?, + ) { + if (category == TripCategory.COURSE && endDate == null) { + throw CustomException(TripErrorCode.COURSE_TRIP_END_DATE_REQUIRED) + } + } + + fun validateOwner( + memberId: Long, + trip: Trip, + ) { + if (trip.member.id != memberId) { + throw CustomException(TripErrorCode.NOT_TRIP_OWNER) + } + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/repository/DailyGoalRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/DailyGoalRepository.kt new file mode 100644 index 0000000..61702bf --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/DailyGoalRepository.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.trip.domain.repository + +import com.ject.studytrip.trip.domain.model.DailyGoal +import java.util.Optional + +interface DailyGoalRepository { + fun save(dailyGoal: DailyGoal): DailyGoal + + fun findById(dailyGoalId: Long): Optional +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripRepository.kt new file mode 100644 index 0000000..784e15b --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/domain/repository/TripRepository.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.trip.domain.repository + +import com.ject.studytrip.trip.domain.model.Trip +import java.util.Optional + +interface TripRepository { + fun findById(tripId: Long): Optional + + fun save(trip: Trip): Trip +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/DailyGoalJpaRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/DailyGoalJpaRepository.kt new file mode 100644 index 0000000..9e9756f --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/DailyGoalJpaRepository.kt @@ -0,0 +1,6 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.DailyGoal +import org.springframework.data.jpa.repository.JpaRepository + +interface DailyGoalJpaRepository : JpaRepository diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/DailyGoalRepositoryAdapter.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/DailyGoalRepositoryAdapter.kt new file mode 100644 index 0000000..ca7a6e2 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/DailyGoalRepositoryAdapter.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.repository.DailyGoalRepository +import org.springframework.stereotype.Repository +import java.util.Optional + +@Repository +class DailyGoalRepositoryAdapter( + private val dailyGoalJpaRepository: DailyGoalJpaRepository, +) : DailyGoalRepository { + override fun save(dailyGoal: DailyGoal): DailyGoal = dailyGoalJpaRepository.save(dailyGoal) + + override fun findById(dailyGoalId: Long): Optional = dailyGoalJpaRepository.findById(dailyGoalId) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripJpaRepository.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripJpaRepository.kt new file mode 100644 index 0000000..2b91611 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripJpaRepository.kt @@ -0,0 +1,6 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.Trip +import org.springframework.data.jpa.repository.JpaRepository + +interface TripJpaRepository : JpaRepository diff --git a/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripRepositoryAdapter.kt b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripRepositoryAdapter.kt new file mode 100644 index 0000000..384450f --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/infra/jpa/TripRepositoryAdapter.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.trip.infra.jpa + +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.repository.TripRepository +import org.springframework.stereotype.Repository +import java.util.Optional + +@Repository +class TripRepositoryAdapter( + private val tripJpaRepository: TripJpaRepository, +) : TripRepository { + override fun findById(tripId: Long): Optional = tripJpaRepository.findById(tripId) + + override fun save(trip: Trip): Trip = tripJpaRepository.save(trip) +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/DailyGoalController.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/DailyGoalController.kt new file mode 100644 index 0000000..145788f --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/DailyGoalController.kt @@ -0,0 +1,90 @@ +package com.ject.studytrip.trip.presentation.controller + +import com.ject.studytrip.global.common.response.StandardResponse +import com.ject.studytrip.trip.application.facade.DailyGoalFacade +import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest +import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest +import com.ject.studytrip.trip.presentation.dto.response.CreateDailyGoalResponse +import com.ject.studytrip.trip.presentation.dto.response.LoadDailyGoalDetailResponse +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 org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "DailyGoal", description = "데일리 목표 API") +@RestController +@RequestMapping("/api/trips/{tripId}/daily-goals") +@Validated +class DailyGoalController( + private val dailyGoalFacade: DailyGoalFacade, +) { + @Operation(summary = "데일리 목표 생성", description = "특정 여행에 새로운 데일리 목표를 생성합니다.") + @PostMapping + fun createDailyGoal( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @RequestBody @Valid request: CreateDailyGoalRequest, + ): ResponseEntity { + val result = dailyGoalFacade.createDailyGoal(memberId.toLong(), tripId, request) + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(StandardResponse.success(HttpStatus.CREATED.value(), CreateDailyGoalResponse.of(result))) + } + + @Operation(summary = "데일리 목표 수정", description = "특정 데일리 목표를 수정합니다.") + @PatchMapping("/{dailyGoalId}") + fun updateDailyGoal( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @PathVariable @NotNull(message = "데일리 목표 ID는 필수 요청 파라미터입니다.") dailyGoalId: Long, + @RequestBody request: UpdateDailyGoalRequest, + ): ResponseEntity { + dailyGoalFacade.updateDailyGoal(memberId.toLong(), tripId, dailyGoalId, request) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "데일리 목표 삭제", description = "특정 데일리 목표를 삭제합니다.") + @DeleteMapping("/{dailyGoalId}") + fun deleteDailyGaol( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @PathVariable @NotNull(message = "데일리 목표 ID는 필수 요청 파라미터입니다.") dailyGoalId: Long, + ): ResponseEntity { + dailyGoalFacade.deleteDailyGoal(memberId.toLong(), tripId, dailyGoalId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "데일리 목표 상세 조회", description = "특정 데일리 목표를 상세 조회합니다.") + @GetMapping("/{dailyGoalId}") + fun loadDailyGoal( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @PathVariable @NotNull(message = "데일리 목표 ID는 필수 요청 파라미터입니다.") dailyGoalId: Long, + ): ResponseEntity { + val result = dailyGoalFacade.getDailyGoal(memberId.toLong(), tripId, dailyGoalId) + val response = LoadDailyGoalDetailResponse.of(result.dailyGoalInfo, result.pomodoroInfo, result.dailyMissionInfos) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), response)) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripController.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripController.kt new file mode 100644 index 0000000..830fe2e --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/controller/TripController.kt @@ -0,0 +1,128 @@ +package com.ject.studytrip.trip.presentation.controller + +import com.ject.studytrip.global.common.response.StandardResponse +import com.ject.studytrip.trip.application.facade.TripFacade +import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest +import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest +import com.ject.studytrip.trip.presentation.dto.response.CreateTripResponse +import com.ject.studytrip.trip.presentation.dto.response.LoadTripCategoryResponse +import com.ject.studytrip.trip.presentation.dto.response.LoadTripDetailResponse +import com.ject.studytrip.trip.presentation.dto.response.LoadTripsSliceResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Trip", description = "여행 API") +@RestController +@RequestMapping("/api/trips") +@Validated +class TripController( + private val tripFacade: TripFacade, +) { + @Operation(summary = "여행 생성", description = "새로운 여행을 생성합니다. 여행을 생성하는 동시에 스탬프를 1개 이상 함께 생성합니다.") + @PostMapping + fun createTrip( + @AuthenticationPrincipal memberId: String, + @RequestBody @Valid request: CreateTripRequest, + ): ResponseEntity { + val result = tripFacade.createTrip(memberId.toLong(), request) + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(StandardResponse.success(HttpStatus.CREATED.value(), CreateTripResponse.of(result))) + } + + @Operation(summary = "여행 수정", description = "특정 여행을 수정합니다.") + @PatchMapping("/{tripId}") + fun updateTrip( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + @RequestBody @Valid request: UpdateTripRequest, + ): ResponseEntity { + tripFacade.updateTrip(memberId.toLong(), tripId, request) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "여행 삭제", description = "특정 여행을 삭제합니다.") + @DeleteMapping("/{tripId}") + fun deleteTrip( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + ): ResponseEntity { + tripFacade.deleteTrip(memberId.toLong(), tripId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "여행 완료", description = "특정 여행의 모든 스탬프가 완료된 경우에만 여행을 완료합니다.") + @PatchMapping("/{tripId}/complete") + fun completeTrip( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + ): ResponseEntity { + tripFacade.completeTrip(memberId.toLong(), tripId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), null)) + } + + @Operation(summary = "여행 카테고리 목록 조회", description = "여행 카테고리 목록을 조회합니다.") + @GetMapping("/categories") + fun loadTripCategories(): ResponseEntity { + val result = tripFacade.loadTripCategories() + val responses = result.map(LoadTripCategoryResponse::of) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), responses)) + } + + @Operation(summary = "특정 멤버의 여행 목록을 조회합니다. 슬라이스를 적용하고 D-DAY 정보가 이른 순으로 정렬합니다") + @GetMapping + fun loadTrips( + @AuthenticationPrincipal memberId: String, + @RequestParam(name = "page", defaultValue = "0") @Min(0) page: Int, + @RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) size: Int, + ): ResponseEntity { + val result = tripFacade.getTripsByMember(memberId.toLong(), page, size) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), LoadTripsSliceResponse.of(result.tripInfos, result.hasNext))) + } + + @Operation(summary = "여행 상세 조회", description = "특정 여행을 상세 조회합니다.") + @GetMapping("/{tripId}") + fun loadTrip( + @AuthenticationPrincipal memberId: String, + @PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") tripId: Long, + ): ResponseEntity { + val result = tripFacade.getTrip(memberId.toLong(), tripId) + + return ResponseEntity + .status(HttpStatus.OK) + .body(StandardResponse.success(HttpStatus.OK.value(), LoadTripDetailResponse.of(result.tripInfo, result.stampInfos))) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateDailyGoalRequest.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateDailyGoalRequest.kt new file mode 100644 index 0000000..a705c79 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateDailyGoalRequest.kt @@ -0,0 +1,19 @@ +package com.ject.studytrip.trip.presentation.dto.request + +import com.ject.studytrip.pomodoro.presentation.dto.request.CreatePomodoroRequest +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull + +data class CreateDailyGoalRequest( + @field:Schema(name = "뽀모도로") + @field:Valid + val pomodoro: CreatePomodoroRequest, + @field:Schema(name = "미션 ID 목록") + @field:NotEmpty(message = "미션 목록은 필수 요청 값입니다.") + val missionIds: List< + @NotNull(message = "미션 ID는 필수 요청 값입니다.") + Long, + >, +) diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateTripRequest.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateTripRequest.kt new file mode 100644 index 0000000..01fcb50 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/CreateTripRequest.kt @@ -0,0 +1,30 @@ +package com.ject.studytrip.trip.presentation.dto.request + +import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.FutureOrPresent +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.Pattern +import java.time.LocalDate + +data class CreateTripRequest( + @field:Schema(description = "여행 이름") + @field:NotEmpty(message = "여행 이름은 필수 요청 값입니다.") + val name: String, + @field:Schema(description = "여행 메모") + val memo: String?, + @field:Schema(description = "여행 카테고리") + @field:Pattern( + regexp = "^(COURSE|EXPLORE)$", + message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.", + ) + val category: String, + @field:Schema(description = "여행 종료일") + @field:FutureOrPresent(message = "여행 종료일은 현재 날짜보다 과거일 수 없습니다.") + val endDate: LocalDate?, + @field:Schema(description = "여행 스탬프 목록") + @field:Valid + @field:NotEmpty(message = "스탬프는 최소 1개 이상 함께 등록해야 합니다.") + val stamps: List, +) diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/UpdateDailyGoalRequest.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/UpdateDailyGoalRequest.kt new file mode 100644 index 0000000..d6f5eed --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/UpdateDailyGoalRequest.kt @@ -0,0 +1,10 @@ +package com.ject.studytrip.trip.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema + +data class UpdateDailyGoalRequest( + @field:Schema(name = "삭제할 데일리 미션 ID 목록") + val deleteDailyMissionIds: List, + @field:Schema(name = "추가할 미션 ID 목록") + val addMissionIds: List, +) diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/UpdateTripRequest.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/UpdateTripRequest.kt new file mode 100644 index 0000000..9c8df2b --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/request/UpdateTripRequest.kt @@ -0,0 +1,24 @@ +package com.ject.studytrip.trip.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.FutureOrPresent +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import java.time.LocalDate + +data class UpdateTripRequest( + @field:Schema(description = "수정할 여행 이름") + @field:Size(min = 1, message = "여행 이름은 최소 1글자 이상이여야 합니다.") + val name: String?, + @field:Schema(description = "수정할 여행 메모") + val memo: String?, + @field:Schema(description = "수정할 여행 카테고리") + @field:Pattern( + regexp = "^(COURSE|EXPLORE)$", + message = "여행 카테고리는 COURSE, EXPLORE 중 하나여야 합니다.", + ) + val category: String?, + @field:Schema(description = "수정할 여행 종료일") + @field:FutureOrPresent(message = "여행 종료일은 현재 날짜보다 과거일 수 없습니다.") + val endDate: LocalDate?, +) diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateDailyGoalResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateDailyGoalResponse.kt new file mode 100644 index 0000000..13f3a27 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateDailyGoalResponse.kt @@ -0,0 +1,14 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.trip.application.dto.DailyGoalInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateDailyGoalResponse( + @field:Schema(description = "데일리 목표 ID") + val dailyGoalId: Long, +) { + companion object { + @JvmStatic + fun of(dailyGoalInfo: DailyGoalInfo): CreateDailyGoalResponse = CreateDailyGoalResponse(dailyGoalInfo.dailyGoalId) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateTripResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateTripResponse.kt new file mode 100644 index 0000000..854b383 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/CreateTripResponse.kt @@ -0,0 +1,14 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.trip.application.dto.TripInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateTripResponse( + @field:Schema(description = "여행 ID") + val tripId: Long, +) { + companion object { + @JvmStatic + fun of(tripInfo: TripInfo): CreateTripResponse = CreateTripResponse(tripInfo.tripId) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadDailyGoalDetailResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadDailyGoalDetailResponse.kt new file mode 100644 index 0000000..33f6271 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadDailyGoalDetailResponse.kt @@ -0,0 +1,68 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.mission.application.dto.DailyMissionInfo +import com.ject.studytrip.pomodoro.application.dto.PomodoroInfo +import com.ject.studytrip.trip.application.dto.DailyGoalInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadDailyGoalDetailResponse( + @field:Schema(description = "데일리 목표 ID") + val dailyGoalId: Long, + @field:Schema(description = "데일리 목표 제목 (스탬프 이름)") + val title: String, + @field:Schema(description = "데일리 목표 완료 여부") + val completed: Boolean, + @field:Schema(description = "뽀모도로 정보") + val pomodoro: DailyGoalPomodoroResponse, + @field:Schema(description = "수행할 데일리 미션 목록") + val dailyMissions: List, +) { + companion object { + @JvmStatic + fun of( + dailyGoalInfo: DailyGoalInfo, + pomodoroInfo: PomodoroInfo, + dailyMissionInfos: List, + ): LoadDailyGoalDetailResponse = + LoadDailyGoalDetailResponse( + dailyGoalInfo.dailyGoalId, + dailyGoalInfo.title, + dailyGoalInfo.completed, + DailyGoalPomodoroResponse.of(pomodoroInfo), + dailyMissionInfos.map(DailyGoalMissionResponse::of), + ) + } + + data class DailyGoalPomodoroResponse( + @field:Schema(description = "뽀모도로 ID") + val pomodoroId: Long, + @field:Schema(description = "뽀모도로 집중 시간(분)") + val focusDurationInMinute: Int, + @field:Schema(description = "뽀모도로 집중 세션 개수") + val focusSessionCount: Int, + ) { + companion object { + fun of(pomodoroInfo: PomodoroInfo): DailyGoalPomodoroResponse = + DailyGoalPomodoroResponse( + pomodoroInfo.pomodoroId, + pomodoroInfo.focusDurationInMinute, + pomodoroInfo.focusSessionCount, + ) + } + } + + data class DailyGoalMissionResponse( + @field:Schema(description = "데일리 미션 ID") + val dailyMissionId: Long, + @field:Schema(description = "미션 이름") + val missionName: String, + ) { + companion object { + fun of(dailyMissionInfo: DailyMissionInfo): DailyGoalMissionResponse = + DailyGoalMissionResponse( + dailyMissionInfo.dailyMissionId, + dailyMissionInfo.missionInfo.missionName, + ) + } + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripCategoryResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripCategoryResponse.kt new file mode 100644 index 0000000..becdb4d --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripCategoryResponse.kt @@ -0,0 +1,17 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.trip.application.dto.TripCategoryInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadTripCategoryResponse( + @field:Schema(description = "여행 카테고리 이름") + val name: String, + @field:Schema(description = "여행 카테고리 표시 값") + val value: String, +) { + companion object { + @JvmStatic + fun of(tripCategoryInfo: TripCategoryInfo): LoadTripCategoryResponse = + LoadTripCategoryResponse(tripCategoryInfo.name, tripCategoryInfo.value) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.kt new file mode 100644 index 0000000..03efad3 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripDetailResponse.kt @@ -0,0 +1,56 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.stamp.application.dto.StampInfo +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 + +data class LoadTripDetailResponse( + @field:Schema(description = "여행 ID") + val tripId: Long, + @field:Schema(description = "여행 이름") + val name: String, + @field:Schema(description = "여행 메모") + val memo: String, + @field:Schema(description = "여행 카테고리") + val category: TripCategory, + @field:Schema(description = "여행 시작일") + val startDate: String, + @field:Schema(description = "여행 종료일") + val endDate: String?, + @field:Schema(description = "D-DAY") + val dDay: Int?, + @field:Schema(description = "여행의 총 스탬프 수") + val totalStamps: Int, + @field:Schema(description = "완료된 총 스탬프 수") + val completedStamps: Int, + @field:Schema(description = "진행률") + val progress: Int?, + @field:Schema(description = "여행 완료 여부") + val completed: Boolean, + @field:Schema(description = "여행에 속한 스탬프 목록") + val stamps: List, +) { + companion object { + @JvmStatic + fun of( + tripInfo: TripInfo, + stampInfos: List, + ): LoadTripDetailResponse = + LoadTripDetailResponse( + tripInfo.tripId, + tripInfo.tripName, + tripInfo.tripMemo, + tripInfo.tripCategory, + tripInfo.startDate, + tripInfo.endDate, + tripInfo.dDay, + tripInfo.totalStamps, + tripInfo.completedStamps, + tripInfo.progress, + tripInfo.completed, + stampInfos.map(LoadStampInfoResponse::of), + ) + } +} diff --git a/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripsSliceResponse.kt b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripsSliceResponse.kt new file mode 100644 index 0000000..1e27ea9 --- /dev/null +++ b/src/main/kotlin/com/ject/studytrip/trip/presentation/dto/response/LoadTripsSliceResponse.kt @@ -0,0 +1,19 @@ +package com.ject.studytrip.trip.presentation.dto.response + +import com.ject.studytrip.trip.application.dto.TripInfo +import io.swagger.v3.oas.annotations.media.Schema + +data class LoadTripsSliceResponse( + @field:Schema(description = "여행 목록") + val tripInfos: List, + @field:Schema(description = "다음 데이터 존재 여부") + val hasNext: Boolean, +) { + companion object { + @JvmStatic + fun of( + tripInfos: List, + hasNext: Boolean, + ): LoadTripsSliceResponse = LoadTripsSliceResponse(tripInfos, hasNext) + } +} diff --git a/src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java b/src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java index 821ba76..825ceef 100644 --- a/src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java +++ b/src/test/java/com/ject/studytrip/dummy/application/service/DummyMissionCommandServiceTest.java @@ -28,7 +28,7 @@ class DummyMissionCommandServiceTest extends BaseUnitTest { @BeforeEach void setUp() { Member member = MemberFixture.createMemberFromKakao(); - courseTrip = TripFixture.createTrip(member, TripCategory.COURSE); + courseTrip = new TripFixture(member, TripCategory.COURSE).create(); } @Nested @@ -39,7 +39,7 @@ class CreateDummyMission { @DisplayName("특정 스탬프가 들어오면 더미 미션을 생성하고 리턴한다.") void shouldReturnDummyMissionForStamp() { // given - Stamp stamp = StampFixture.createStamp(courseTrip, COUNT); + Stamp stamp = new StampFixture(courseTrip, COUNT).create(); // when Mission result = dummyMissionCommandService.createDummyMission(stamp); diff --git a/src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java b/src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java index b890bb9..f0cfc64 100644 --- a/src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java +++ b/src/test/java/com/ject/studytrip/dummy/application/service/DummyStampCommandServiceTest.java @@ -36,7 +36,7 @@ class CreateDummyStamp { @DisplayName("코스형 여행이 들어오면 코스형 더미 스탬프를 생성하고 반환한다.") void shouldReturnDummyCourseStampForCourseTrip() { // given - Trip courseTrip = TripFixture.createTrip(member, TripCategory.COURSE); + Trip courseTrip = new TripFixture(member, TripCategory.COURSE).create(); // when Stamp result = dummyStampCommandService.createDummyStamp(courseTrip, COUNT); @@ -52,7 +52,7 @@ void shouldReturnDummyCourseStampForCourseTrip() { @DisplayName("탐험형 여행이 들어오면 탐험형 더미 스탬프를 생성하고 반환한다.") void shouldReturnDummyExploreStampForExploreTrip() { // given - Trip exploreTrip = TripFixture.createTrip(member, TripCategory.EXPLORE); + Trip exploreTrip = new TripFixture(member, TripCategory.EXPLORE).create(); // when Stamp result = dummyStampCommandService.createDummyStamp(exploreTrip, COUNT); diff --git a/src/test/java/com/ject/studytrip/stamp/application/service/StampCommandServiceTest.java b/src/test/java/com/ject/studytrip/stamp/application/service/StampCommandServiceTest.java deleted file mode 100644 index 395b065..0000000 --- a/src/test/java/com/ject/studytrip/stamp/application/service/StampCommandServiceTest.java +++ /dev/null @@ -1,617 +0,0 @@ -package com.ject.studytrip.stamp.application.service; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.stamp.domain.error.StampErrorCode; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.stamp.domain.repository.StampCommandRepository; -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.UpdateStampOrderRequestFixture; -import com.ject.studytrip.stamp.fixture.UpdateStampRequestFixture; -import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest; -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.util.List; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -@DisplayName("StampCommandService 단위 테스트") -class StampCommandServiceTest extends BaseUnitTest { - @InjectMocks private StampCommandService stampCommandService; - @Mock private StampRepository stampRepository; - @Mock private StampCommandRepository stampCommandRepository; - - private Member member; - private Trip courseTrip; - private Trip exploreTrip; - private Stamp courseStamp1; - private Stamp courseStamp2; - private Stamp exploreStamp1; - - @BeforeEach - void setup() { - 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); - exploreStamp1 = StampFixture.createStampWithId(3L, exploreTrip, 0); - } - - @Nested - @DisplayName("createStamp 메서드는") - class CreateStamp { - private final CreateStampRequestFixture fixture = new CreateStampRequestFixture(); - - @Test - @DisplayName("스탬프 종료일이 과거 날짜라면 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsInPast() { - // given - int nextOrder = courseStamp2.getStampOrder() + 1; - CreateStampRequest request = fixture.withEndDateInPast().build(); - - // when & then - assertThatThrownBy( - () -> stampCommandService.createStamp(courseTrip, nextOrder, request)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_END_DATE_CANNOT_BE_IN_PAST.getMessage()); - } - - @Test - @DisplayName("스탬프 종료일이 여행 종료일보다 이후라면 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsAfterTripEndDate() { - // given - int nextOrder = courseStamp2.getStampOrder() + 1; - CreateStampRequest request = fixture.withEndDateAfterTripEndDate().build(); - - // when & then - assertThatThrownBy( - () -> stampCommandService.createStamp(courseTrip, nextOrder, request)) - .isInstanceOf(CustomException.class) - .hasMessage( - StampErrorCode.STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED - .getMessage()); - } - - @Test - @DisplayName("탐험형 여행에서는 order가 항상 0으로 저장된다") - void shouldCreateExploreStampWithOrderZero() { - // given - CreateStampRequest request = fixture.build(); - Stamp exploreStamp2 = StampFixture.createStampWithName(exploreTrip, request.name(), 0); - given(stampRepository.save(any(Stamp.class))).willReturn(exploreStamp2); - - // when - exploreStamp2 = stampCommandService.createStamp(exploreTrip, 0, request); - - // then - assertThat(exploreStamp2.getTrip()).isEqualTo(exploreTrip); - assertThat(exploreStamp2.getName()).isEqualTo(request.name()); - assertThat(exploreStamp2.getStampOrder()).isEqualTo(0); - } - - @Test - @DisplayName("코스형 여행에서는 마지막 order 다음 값으로 저장된다") - void shouldCreateCourseStampWithNextSequentialOrder() { - // given - int nextOrder = courseStamp2.getStampOrder() + 1; - CreateStampRequest request = fixture.build(); - Stamp courseStamp3 = - StampFixture.createStampWithName(courseTrip, request.name(), nextOrder); - given(stampRepository.save(any(Stamp.class))).willReturn(courseStamp3); - - // when - courseStamp3 = stampCommandService.createStamp(courseTrip, nextOrder, request); - - // then - assertThat(courseStamp3.getStampOrder()).isEqualTo(3); - } - } - - @Nested - @DisplayName("createStamps 메서드는") - class CreateStamps { - private final CreateStampRequestFixture fixture = new CreateStampRequestFixture(); - - @Test - @DisplayName("탐험형 여행에서는 전달한 모든 스탬프의 order가 0으로 저장된다") - void shouldCreateExploreStampsWithOrderZero() { - // given - List requests = List.of(fixture.build(), fixture.build()); - - // when - stampCommandService.createStamps(exploreTrip, 0, requests); - - // then - verify(stampRepository).saveAll(anyList()); - } - - @Test - @DisplayName("코스형 여행에서는 마지막 order 다음 값부터 순차적으로 저장된다") - void shouldCreateCourseStampsSequentiallyFromNextOrder() { - // given - int nextOrder = courseStamp2.getStampOrder() + 1; - List requests = List.of(fixture.build(), fixture.build()); - - // when - stampCommandService.createStamps(courseTrip, nextOrder, requests); - - // then - verify(stampRepository).saveAll(anyList()); - } - } - - @Nested - @DisplayName("updateStamp 메서드는") - class UpdateStamp { - private final UpdateStampRequestFixture fixture = new UpdateStampRequestFixture(); - - @Test - @DisplayName("유효한 정보로 스탬프의 이름을 수정하면 스탬프가 업데이트된다") - void shouldUpdateStampName() { - - // given - UpdateStampRequest request = fixture.buildUpdateName(); - - // when - stampCommandService.updateStamp(courseTrip, courseStamp1, request); - - // then - assertThat(courseStamp1.getName()).isEqualTo(request.name()); - } - - @Test - @DisplayName("유효한 정보로 스탬프의 종료일을 수정하면 스탬프가 업데이트된다") - void shouldUpdateStampEndDate() { - // given - UpdateStampRequest request = fixture.buildUpdateEndDate(); - - // when - stampCommandService.updateStamp(courseTrip, courseStamp1, request); - - // then - assertThat(courseStamp1.getEndDate()).isEqualTo(request.endDate()); - } - - @Test - @DisplayName("과거 날짜로 스탬프의 종료일을 수정하면 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsInPast() { - // given - UpdateStampRequest request = fixture.withEndDateInPast().buildUpdateEndDate(); - - // when & then - assertThatThrownBy( - () -> - stampCommandService.updateStamp( - courseTrip, courseStamp1, request)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_END_DATE_CANNOT_BE_IN_PAST.getMessage()); - } - - @Test - @DisplayName("여행 종료일보다 이후 날짜로 스탬프의 종료일을 수정하면 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsAfterTripEndDate() { - // given - UpdateStampRequest request = fixture.withEndDateAfterTripEndDate().buildUpdateEndDate(); - - // when & then - assertThatThrownBy( - () -> - stampCommandService.updateStamp( - courseTrip, courseStamp1, request)) - .isInstanceOf(CustomException.class) - .hasMessage( - StampErrorCode.STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED - .getMessage()); - } - } - - @Nested - @DisplayName("updateStampOrders 메서드는") - class UpdateStampOrders { - private final UpdateStampOrderRequestFixture fixture = new UpdateStampOrderRequestFixture(); - - @Test - @DisplayName("탐험형 여행의 스탬프 순서를 수정하면 예외가 발생한다") - void shouldThrowExceptionWhenTripIsExplorationType() { - // given - UpdateStampOrderRequest request = fixture.buildUpdateOrders(); - - // when & then - assertThatThrownBy(() -> stampCommandService.updateStampOrders(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(() -> stampCommandService.updateStampOrders(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(() -> stampCommandService.updateStampOrders(newTrip, request)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_NOT_BELONGS_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(() -> stampCommandService.updateStampOrders(courseTrip, request)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_ALREADY_DELETED.getMessage()); - } - - @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 - stampCommandService.updateStampOrders(courseTrip, request); - - // then - assertThat(courseStamp2.getStampOrder()).isEqualTo(1); - assertThat(courseStamp1.getStampOrder()).isEqualTo(2); - } - } - - @Nested - @DisplayName("updateStampOrdersForUpdateTripCategory 메서드는") - class UpdateStampOrdersForUpdateTripCategory { - - @Test - @DisplayName("여행의 카테고리가 탐험형으로 수정되면 소속된 모든 스탬프의 순서를 0으로 수정한다") - void shouldSetAllStampOrdersToZeroWhenCategoryChangesToExplore() { - // given - given(stampRepository.findAllByTripIdOrderByCreatedAtAsc(courseTrip.getId())) - .willReturn(List.of(courseStamp1, courseStamp2)); - - // when - stampCommandService.updateStampOrdersByTripCategoryChange( - 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.findAllByTripIdOrderByCreatedAtAsc(exploreTrip.getId())) - .willReturn(List.of(courseStamp1, courseStamp2)); - - // when - stampCommandService.updateStampOrdersByTripCategoryChange( - exploreTrip.getId(), TripCategory.COURSE); - - // then - assertThat(courseStamp1.getStampOrder()).isEqualTo(1); - assertThat(courseStamp2.getStampOrder()).isEqualTo(2); - } - } - - @Nested - @DisplayName("deleteStamp 메서드는") - class DeleteStamp { - - @Test - @DisplayName("코스형 여행의 스탬프 삭제 시 deletedAt 필드를 현재 시각으로 설정한다.") - void shouldDeleteCourseTripStamp() { - // when - stampCommandService.deleteStamp(courseStamp1); - - // then - assertThat(courseStamp1.getDeletedAt()).isNotNull(); - } - - @Test - @DisplayName("탐험형 여행의 스탬프 삭제 시 deletedAt 필드를 현재 시각으로 설정한다.") - void shouldDeleteExploreTripStamp() { - // when - stampCommandService.deleteStamp(exploreStamp1); - - // then - assertThat(exploreStamp1.getDeletedAt()).isNotNull(); - } - } - - @Nested - @DisplayName("completeStamp 메서드는") - class CompleteStamp { - - @Test - @DisplayName("이미 완료된 스탬프이면 예외가 발생한다.") - void shouldThrowExceptionWhenStampIsAlreadyCompleted() { - // given - exploreStamp1.updateCompleted(); - - // when & then - assertThatThrownBy(() -> stampCommandService.completeStamp(exploreStamp1)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_ALREADY_COMPLETED.getMessage()); - } - - @Test - @DisplayName("유효한 스탬프가 들어오면, completed 필드를 true로 업데이트한다.") - void shouldCompleteStamp() { - // when - stampCommandService.completeStamp(exploreStamp1); - - // then - assertThat(exploreStamp1.isCompleted()).isTrue(); - } - } - - @Nested - @DisplayName("shiftStampOrders 메서드는") - class ShiftStampOrders { - - @Test - @DisplayName("시프트할 스탬프가 존재하지 않으면 아무 동작도 수행하지 않는다.") - void shouldDoNothingWhenStampsToShiftDoNotExist() { - // when - stampCommandService.shiftStampOrders(List.of()); - - // then - assertThat(courseStamp2.getStampOrder()).isEqualTo(2); - } - - @Test - @DisplayName("시프트할 스탬프가 존재하면 각 스탬프의 order를 1씩 감소시킨다.") - void shouldDecreaseOrdersByOneWhenStampsToShiftExist() { - // when - stampCommandService.shiftStampOrders(List.of(courseStamp2)); - - // then - assertThat(courseStamp2.getStampOrder()).isEqualTo(1); - } - } - - @Nested - @DisplayName("validateAllStampsCompletedByTripId 메서드는") - class ValidateAllStampsCompletedByTripId { - - @Test - @DisplayName("특정 여행 하위의 스탬프가 하나라도 완료되지 않았다면 예외가 발생한다.") - void shouldThrowExceptionWhenAnyStampIsNotCompleted() { - // given - Long tripId = courseTrip.getId(); - given( - stampCommandRepository - .existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(tripId)) - .willReturn(true); - - // when & then - Assertions.assertThatThrownBy( - () -> stampCommandService.validateAllStampsCompletedByTripId(tripId)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.ALL_STAMPS_NOT_COMPLETED.getMessage()); - } - - @Test - @DisplayName("특정 여행 하위의 모든 스탬프가 완료되면 예외가 발생하지 않는다.") - void shouldPassWhenAllStampsAreCompleted() { - // given - Long tripId = courseTrip.getId(); - given( - stampCommandRepository - .existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(tripId)) - .willReturn(false); - - // when & then - assertDoesNotThrow( - () -> stampCommandService.validateAllStampsCompletedByTripId(tripId)); - } - } - - @Nested - @DisplayName("hardDeleteStamps 메서드는") - class HardDeleteStamps { - - @Test - @DisplayName("삭제된 스탬프가 없으면 0을 반환한다.") - void shouldReturnZeroWhenDeletedStampsDoNotExist() { - // given - given(stampCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); - - // when - long result = stampCommandService.hardDeleteStamps(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 스탬프가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenDeletedStampsExist() { - // given - given(stampCommandRepository.deleteAllByDeletedTripOwner()).willReturn(5L); - - // when - long result = stampCommandService.hardDeleteStampsOwnedByDeletedTrip(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteStampsOwnedByDeletedTrip 메서드는") - class HardDeleteStampsOwnedByDeletedTrip { - - @Test - @DisplayName("삭제된 여행이 소유한 스탬프가 없으면 0을 반환한다.") - void shouldReturnZeroWhenStampsOwnedByDeletedTripDoNotExist() { - // given - given(stampCommandRepository.deleteAllByDeletedTripOwner()).willReturn(0L); - - // when - long result = stampCommandService.hardDeleteStampsOwnedByDeletedTrip(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 여행이 소유한 스탬프가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenStampsOwnedByDeletedTripExist() { - // given - given(stampCommandRepository.deleteAllByDeletedTripOwner()).willReturn(5L); - - // when - long result = stampCommandService.hardDeleteStampsOwnedByDeletedTrip(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("increaseTotalMissions 메서드는") - class IncreaseTotalMissions { - - @Test - @DisplayName("스탬프의 총 미션 수를 1 증가시킨다") - void shouldIncreaseTotalMissions() { - // given - int initialTotalMissions = courseStamp1.getTotalMissions(); - - // when - stampCommandService.increaseTotalMissions(courseStamp1); - - // then - assertThat(courseStamp1.getTotalMissions()).isEqualTo(initialTotalMissions + 1); - } - } - - @Nested - @DisplayName("decreaseTotalMissions 메서드는") - class DecreaseTotalMissions { - - @Test - @DisplayName("스탬프의 총 미션 수를 1 감소시킨다") - void shouldDecreaseTotalMissions() { - // given - courseStamp1.increaseTotalMissions(); - courseStamp1.increaseTotalMissions(); - int initialTotalMissions = courseStamp1.getTotalMissions(); - - // when - stampCommandService.decreaseTotalMissions(courseStamp1); - - // then - assertThat(courseStamp1.getTotalMissions()).isEqualTo(initialTotalMissions - 1); - } - } - - @Nested - @DisplayName("increaseCompletedMissions 메서드는") - class IncreaseCompletedMissions { - - @Test - @DisplayName("스탬프의 완료된 미션 수를 지정된 개수만큼 증가시킨다") - void shouldIncreaseCompletedMissions() { - // given - int initialCompletedMissions = courseStamp1.getCompletedMissions(); - int increaseCount = 3; - - // when - stampCommandService.increaseCompletedMissions(courseStamp1, increaseCount); - - // then - assertThat(courseStamp1.getCompletedMissions()) - .isEqualTo(initialCompletedMissions + increaseCount); - } - } - - @Nested - @DisplayName("hardDeleteStampsByMember 메서드는") - class HardDeleteStampsByMember { - - @Test - @DisplayName("특정 멤버가 소유한 스탬프가 없으면 0을 반환한다.") - void shouldReturnZeroWhenStampsOwnedByMemberDoNotExist() { - // given - Long memberId = 1L; - given(stampCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L); - - // when - long result = stampCommandService.hardDeleteStampsByMember(memberId); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("특정 멤버가 소유한 스탬프가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenStampsOwnedByMemberExist() { - // given - Long memberId = 1L; - given(stampCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L); - - // when - long result = stampCommandService.hardDeleteStampsByMember(memberId); - - // then - assertThat(result).isEqualTo(5L); - } - } -} diff --git a/src/test/java/com/ject/studytrip/stamp/application/service/StampQueryServiceTest.java b/src/test/java/com/ject/studytrip/stamp/application/service/StampQueryServiceTest.java deleted file mode 100644 index 90efe60..0000000 --- a/src/test/java/com/ject/studytrip/stamp/application/service/StampQueryServiceTest.java +++ /dev/null @@ -1,340 +0,0 @@ -package com.ject.studytrip.stamp.application.service; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.stamp.domain.error.StampErrorCode; -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.StampFixture; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.fixture.TripFixture; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.test.util.ReflectionTestUtils; - -@DisplayName("StampQueryService 단위 테스트") -class StampQueryServiceTest extends BaseUnitTest { - @InjectMocks private StampQueryService stampQueryService; - @Mock private StampRepository stampRepository; - @Mock private StampQueryRepository stampQueryRepository; - - private Trip courseTrip; - private Trip exploreTrip; - private Stamp courseStamp1; - private Stamp courseStamp2; - - @BeforeEach - void setup() { - Member 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("getValidStamp 메서드는") - class GetValidStamp { - - @Test - @DisplayName("유효하지 않은 스탬프 ID일 경우 예외가 발생한다") - void shouldThrowExceptionWhenStampNotFoundById() { - // given - Long stampId = 1000L; - - // when & then - assertThatThrownBy(() -> stampQueryService.getValidStamp(courseTrip.getId(), stampId)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("여행에 속한 스탬프가 아닐 경우 예외가 발생한다") - void shouldThrowExceptionWhenStampDoesNotBelongToTrip() { - // given - given(stampRepository.findById(any())).willReturn(Optional.ofNullable(courseStamp1)); - - // when & then - assertThatThrownBy( - () -> - stampQueryService.getValidStamp( - exploreTrip.getId(), courseStamp1.getId())) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.getMessage()); - } - - @Test - @DisplayName("삭제된 스탬프일 경우 예외가 발생한다") - void shouldThrowExceptionWhenStampAlreadyDeleted() { - // given - courseStamp1.updateDeletedAt(); - Long tripId = courseTrip.getId(); - Long stampId = courseStamp1.getId(); - given(stampRepository.findById(stampId)).willReturn(Optional.ofNullable(courseStamp1)); - - // when & then - assertThatThrownBy(() -> stampQueryService.getValidStamp(tripId, stampId)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_ALREADY_DELETED.getMessage()); - } - - @Test - @DisplayName("완료된 스탬프일 경우 예외가 발생한다.") - void shouldThrowExceptionWhenStampAlreadyCompleted() { - // given - courseStamp1.updateCompleted(); - Long tripId = courseTrip.getId(); - Long stampId = courseStamp1.getId(); - given(stampRepository.findById(stampId)).willReturn(Optional.ofNullable(courseStamp1)); - - // when & then - assertThatThrownBy(() -> stampQueryService.getValidStamp(tripId, stampId)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_ALREADY_COMPLETED.getMessage()); - } - - @Test - @DisplayName("스탬프 ID로 스탬프를 조회하고, 여행 소속 및 삭제 여부를 검증하고 반환한다") - void shouldGetStampReturnValidStamp() { - // given - given(stampRepository.findById(any())).willReturn(Optional.ofNullable(courseStamp1)); - - // when - Stamp stamp = stampQueryService.getValidStamp(courseTrip.getId(), courseStamp1.getId()); - - // then - assertThat(stamp.getId()).isEqualTo(courseStamp1.getId()); - } - } - - @Nested - @DisplayName("getStampsByTripId 메서드는") - class GetStampsByTripId { - - @Test - @DisplayName("유효한 여행 ID로 삭제된 스탬프를 제외한 스탬프 목록을 조회한다") - void shouldGetStampsByTripId() { - // given - given(stampRepository.findAllByTripIdAndDeletedAtIsNull(courseTrip.getId())) - .willReturn(List.of(courseStamp1, courseStamp2)); - - // when - List stamps = stampQueryService.getStampsByTripId(courseTrip.getId()); - - // then - verify(stampRepository).findAllByTripIdAndDeletedAtIsNull(any()); - - assertThat(stamps.isEmpty()).isFalse(); - assertThat(stamps.size()).isEqualTo(2); - } - } - - @Nested - @DisplayName("getFirstInCompleteStampForCourseTrip 메서드는") - class GetFirstInCompleteStampForCourseTrip { - - @Test - @DisplayName("코스형 여행의 현재 진행중인 스탬프가 존재하지 않으면 예외가 발생한다") - void shouldThrowExceptionWhenNoIncompleteStampExistsForCourseTrip() { - // given - given(stampQueryRepository.findFirstIncompleteStampByTripId(any())) - .willReturn(Optional.empty()); - - // when & then - assertThatThrownBy( - () -> - stampQueryService.getFirstInCompleteStampForCourseTrip( - courseTrip.getId())) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("코스형 여행 ID로 현재 진행중인 스탬프를 조회하고 반한환다") - void shouldReturnFirstIncompleteStampForCourseTrip() { - // given - given(stampQueryRepository.findFirstIncompleteStampByTripId(any())) - .willReturn(Optional.ofNullable(courseStamp1)); - - // when - Stamp stamp = - stampQueryService.getFirstInCompleteStampForCourseTrip(courseTrip.getId()); - - // then - assertThat(stamp.getId()).isEqualTo(courseStamp1.getId()); - assertThat(stamp.isCompleted()).isFalse(); - } - } - - @Nested - @DisplayName("getStampNameByTripCategory 메서드는") - class GetStampNameByTripCategory { - - @Test - @DisplayName("스탬프 목록이 비어있을 경우 예외가 발생한다") - void shouldThrowExceptionWhenStampListIsEmpty() { - // given - List emptyStamps = List.of(); - - // when & then - assertThatThrownBy( - () -> - stampQueryService.getStampNameByTripCategory( - TripCategory.COURSE, emptyStamps)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_LIST_CANNOT_BE_EMPTY.getMessage()); - } - - @Test - @DisplayName("스탬프가 삭제된 상태일 경우 예외가 발생한다") - void shouldThrowExceptionWhenStampIsDeleted() { - // given - courseStamp1.updateDeletedAt(); - List stamps = List.of(courseStamp1); - - // when & then - assertThatThrownBy( - () -> - stampQueryService.getStampNameByTripCategory( - TripCategory.COURSE, stamps)) - .isInstanceOf(CustomException.class) - .hasMessage(StampErrorCode.STAMP_ALREADY_DELETED.getMessage()); - } - - @Test - @DisplayName("코스형 여행일 경우 선택한 미션들이 하나의 스탬프에 속하므로 해당 스탬프의 이름을 반환한다") - void shouldReturnStampNameForCourseTrip() { - // given - Stamp stamp = StampFixture.createStamp(courseTrip, 1); - List stamps = List.of(stamp); - - // when - String result = - stampQueryService.getStampNameByTripCategory(TripCategory.COURSE, stamps); - - // then - assertThat(result).isEqualTo(stamp.getName()); - } - - @Test - @DisplayName("탐험형 여행일 경우 선택한 미션들의 스탬프 중 가장 많이 포함된 스탬프의 이름을 제목으로 반환한다") - void shouldReturnMostFrequentStampNameForExplorationTrip() { - // given - Stamp stamp1 = StampFixture.createStamp(exploreTrip, 0); - Stamp stamp2 = StampFixture.createStamp(exploreTrip, 0); - List stamps = List.of(stamp1, stamp1, stamp2); - - // then - String result = - stampQueryService.getStampNameByTripCategory(TripCategory.EXPLORE, stamps); - - // when - assertThat(result).isEqualTo(stamp1.getName()); - } - - @Test - @DisplayName("탐험형 여행이면서 가장 많이 포함된 스탬프가 2개 이상일 경우 createdAt이 가장 빠른 스탬프의 이름을 반환한다") - void shouldReturnEarliestStampNameWhenFrequencyIsSame() { - // given - Stamp stamp1 = StampFixture.createStamp(exploreTrip, 0); - ReflectionTestUtils.setField(stamp1, "createdAt", LocalDateTime.now()); - Stamp stamp2 = StampFixture.createStamp(exploreTrip, 0); - ReflectionTestUtils.setField(stamp2, "createdAt", LocalDateTime.now().minusDays(1)); - List stamps = List.of(stamp1, stamp1, stamp2, stamp2); - - // when - String result = - stampQueryService.getStampNameByTripCategory(TripCategory.EXPLORE, stamps); - - // then - assertThat(result).isEqualTo(stamp2.getName()); - } - } - - @Nested - @DisplayName("getNextStampOrderByTrip 메서드는") - class GetNextStampOrderByTrip { - - @Test - @DisplayName("탐험형 여행일 경우 0을 반환한다.") - void shouldReturnZeroForExploreTrip() { - // when - int result = stampQueryService.getNextStampOrderByTrip(exploreTrip); - - // then - assertThat(result).isEqualTo(0); - } - - @Test - @DisplayName("코스형 여행일 경우 다음 스탬프 순서를 반환한다.") - void shouldReturnNextStampOrderForCourseTrip() { - // given - given(stampQueryRepository.findNextStampOrderByTripId(courseTrip.getId())) - .willReturn(3); - - // when - int result = stampQueryService.getNextStampOrderByTrip(courseTrip); - - // then - assertThat(result).isEqualTo(3); - } - } - - @Nested - @DisplayName("getStampsToShiftAfterDeleted 메서드는") - class GetStampsToShiftAfterDeleted { - - @Test - @DisplayName("시프트할 스탬프가 존재하지 않으면 빈 리스트를 반환한다.") - void shouldReturnEmptyListWhenStampsToShiftDoNotExist() { - // given - Long tripId = courseTrip.getId(); - int deletedOrder = courseStamp2.getStampOrder(); - given(stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedOrder)) - .willReturn(List.of()); - - // when - List result = - stampQueryService.getStampsToShiftAfterDeleted(tripId, deletedOrder); - - // then - assertThat(result).isNotNull(); - assertThat(result).isEqualTo(List.of()); - } - - @Test - @DisplayName("시프트할 스탬프가 존재하면 스탬프 리스트를 반환한다.") - void shouldReturnStampsWhenStampsToShiftExist() { - // given - Long tripId = courseTrip.getId(); - int deletedOrder = courseStamp1.getStampOrder(); - given(stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedOrder)) - .willReturn(List.of(courseStamp2)); - - // when - List result = - stampQueryService.getStampsToShiftAfterDeleted(tripId, deletedOrder); - - // then - assertThat(result).isNotNull(); - assertThat(result).isEqualTo(List.of(courseStamp2)); - } - } -} diff --git a/src/test/java/com/ject/studytrip/stamp/fixture/CreateStampRequestFixture.java b/src/test/java/com/ject/studytrip/stamp/fixture/CreateStampRequestFixture.java deleted file mode 100644 index 2733c58..0000000 --- a/src/test/java/com/ject/studytrip/stamp/fixture/CreateStampRequestFixture.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ject.studytrip.stamp.fixture; - -import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; -import java.time.LocalDate; - -public class CreateStampRequestFixture { - - private String name = "TEST STAMP"; - private java.time.LocalDate endDate = java.time.LocalDate.now().plusDays(7); - - public CreateStampRequestFixture withName(String name) { - this.name = name; - return this; - } - - public CreateStampRequestFixture withEndDateInPast() { - this.endDate = LocalDate.now().minusDays(1); - return this; - } - - public CreateStampRequestFixture withEndDateAfterTripEndDate() { - this.endDate = LocalDate.now().plusDays(100); - return this; - } - - public CreateStampRequest build() { - return new CreateStampRequest(name, endDate); - } -} diff --git a/src/test/java/com/ject/studytrip/stamp/fixture/StampFixture.java b/src/test/java/com/ject/studytrip/stamp/fixture/StampFixture.java deleted file mode 100644 index 1ab4a95..0000000 --- a/src/test/java/com/ject/studytrip/stamp/fixture/StampFixture.java +++ /dev/null @@ -1,27 +0,0 @@ -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 org.springframework.test.util.ReflectionTestUtils; - -public class StampFixture { - private static final String STAMP_NAME = "TEST STAMP NAME"; - private static final java.time.LocalDate DEFAULT_END_DATE = - java.time.LocalDate.now().plusDays(7); - - public static Stamp createStamp(Trip trip, int order) { - return StampFactory.create(trip, STAMP_NAME, order, DEFAULT_END_DATE); - } - - public static Stamp createStampWithId(Long id, Trip trip, int order) { - Stamp stamp = StampFactory.create(trip, STAMP_NAME, order, DEFAULT_END_DATE); - ReflectionTestUtils.setField(stamp, "id", id); - - return stamp; - } - - public static Stamp createStampWithName(Trip trip, String name, int order) { - return StampFactory.create(trip, name, order, DEFAULT_END_DATE); - } -} diff --git a/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampOrderRequestFixture.java b/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampOrderRequestFixture.java deleted file mode 100644 index 39e2ac7..0000000 --- a/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampOrderRequestFixture.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.ject.studytrip.stamp.fixture; - -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest; -import java.util.List; - -public class UpdateStampOrderRequestFixture { - private List orderedStampIds = List.of(1L, 2L); - - public UpdateStampOrderRequest buildUpdateOrders() { - return new UpdateStampOrderRequest(orderedStampIds); - } - - public UpdateStampOrderRequestFixture withOrderedStampIds(List orderedStampIds) { - this.orderedStampIds = orderedStampIds; - return this; - } -} diff --git a/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.java b/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.java deleted file mode 100644 index b09e866..0000000 --- a/src/test/java/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.ject.studytrip.stamp.fixture; - -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest; -import java.time.LocalDate; - -public class UpdateStampRequestFixture { - private String name = "TEST STAMP"; - private java.time.LocalDate endDate = java.time.LocalDate.now().plusDays(7); - - public UpdateStampRequestFixture withName(String name) { - this.name = name; - return this; - } - - public UpdateStampRequestFixture withEndDateInPast() { - this.endDate = LocalDate.now().minusDays(1); - return this; - } - - public UpdateStampRequestFixture withEndDateAfterTripEndDate() { - this.endDate = LocalDate.now().plusDays(100); - return this; - } - - public UpdateStampRequest buildUpdateName() { - return new UpdateStampRequest(name, null); - } - - public UpdateStampRequest buildUpdateEndDate() { - return new UpdateStampRequest(null, endDate); - } -} diff --git a/src/test/java/com/ject/studytrip/stamp/helper/StampTestHelper.java b/src/test/java/com/ject/studytrip/stamp/helper/StampTestHelper.java deleted file mode 100644 index 9276b99..0000000 --- a/src/test/java/com/ject/studytrip/stamp/helper/StampTestHelper.java +++ /dev/null @@ -1,36 +0,0 @@ -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); - } - - public Stamp saveCompletedStamp(Trip trip, int order) { - Stamp stamp = StampFixture.createStamp(trip, order); - stamp.updateCompleted(); - - return stampRepository.save(stamp); - } - - public Stamp getStamp(Long stampId) { - return stampRepository.findById(stampId).orElseThrow(); - } -} 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 deleted file mode 100644 index 46244b0..0000000 --- a/src/test/java/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.java +++ /dev/null @@ -1,1463 +0,0 @@ -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.mission.domain.error.MissionErrorCode; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.mission.helper.MissionTestHelper; -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.UpdateStampOrderRequestFixture; -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.UpdateStampOrderRequest; -import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest; -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.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.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.ResultActions; - -@DisplayName("StampController 통합 테스트") -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 MissionTestHelper missionTestHelper; - @Autowired private TokenTestHelper tokenTestHelper; - - private String token; - private Member member; - private Trip courseTrip; - private Trip exploreTrip; - private Stamp courseStamp1; - private Stamp courseStamp2; - private Mission courseMission1; - private Mission courseMission2; - - private String newToken; - - @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); - courseMission1 = missionTestHelper.saveMission(courseStamp1); - courseMission2 = missionTestHelper.saveMission(courseStamp1); - - Member newMember = memberTestHelper.saveMember("test@kakao.com", "TEST NICKNAME"); - newToken = - tokenTestHelper.createAccessToken( - newMember.getId().toString(), newMember.getRole().name()); - } - - @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.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("유효하지 않은 여행 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())); - } - } - - @Nested - @DisplayName("스탬프 수정 API") - class UpdateStamp { - private final UpdateStampRequestFixture updateStampRequestFixture = - new UpdateStampRequestFixture(); - private final UpdateStampOrderRequestFixture updateStampOrderRequestFixture = - new UpdateStampOrderRequestFixture(); - - @Nested - @DisplayName("스탬프 이름 수정") - class UpdateName { - private ResultActions getResultActions( - String token, Object tripId, Object stampId, UpdateStampRequest 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 shouldUpdateStampName() throws Exception { - // given - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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 - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - // 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"; - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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"; - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - // 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("유효하지 않은 여행 ID 라면 404 예외가 발생한다") - void shouldThrowExceptionWhenInvalidTripId() throws Exception { - // given - Long tripId = 10000L; - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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); - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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); - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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; - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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); - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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_BELONGS_TO_TRIP - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 스탬프일 경우 400 예외가 발생한다") - void shouldThrowExceptionWhenAlreadyDeletedStamp() throws Exception { - // given - Stamp newStamp = stampTestHelper.saveDeletedStamp(exploreTrip, 0); - UpdateStampRequest request = updateStampRequestFixture.buildUpdateName(); - - // 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 = - updateStampOrderRequestFixture - .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 = - updateStampOrderRequestFixture.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 = - updateStampOrderRequestFixture.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 = - updateStampOrderRequestFixture.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 = - updateStampOrderRequestFixture.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 = - updateStampOrderRequestFixture.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 - UpdateStampOrderRequest request = - updateStampOrderRequestFixture.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 = - updateStampOrderRequestFixture - .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 = - updateStampOrderRequestFixture - .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_BELONGS_TO_TRIP - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 스탬프일 경우 400 예외가 발생한다") - void shouldThrowExceptionWhenAlreadyDeletedStamp() throws Exception { - // given - Stamp newStamp = stampTestHelper.saveDeletedStamp(courseTrip, NEXT_STAMP_ORDER); - UpdateStampOrderRequest request = - updateStampOrderRequestFixture - .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_BELONGS_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_BELONGS_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())); - } - } - - @Nested - @DisplayName("스탬프 완료 API") - class CompleteStamp { - private ResultActions getResultActions(String token, Object tripId, Object stampId) - throws Exception { - return mockMvc.perform( - patch("/api/trips/{tripId}/stamps/{stampId}/complete", tripId, stampId) - .header(HttpHeaders.AUTHORIZATION, TokenFixture.TOKEN_PREFIX + token)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() 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())) - .andExpect( - jsonPath("$.data.message") - .value(AuthErrorCode.UNAUTHENTICATED.getMessage())); - } - - @Test - @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception { - // given - String invalidTripId = "abc"; - - // when - ResultActions resultActions = - getResultActions(token, invalidTripId, courseStamp1.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getMessage())); - } - - @Test - @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenStampIdTypeMismatch() throws Exception { - // given - String invalidStampId = "def"; - - // when - ResultActions resultActions = - getResultActions(token, courseTrip.getId(), invalidStampId); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getMessage())); - } - - @Test - @DisplayName("삭제된 여행일 경우 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripAlreadyDeleted() throws Exception { - // given - courseTrip.updateDeletedAt(); - - // when - ResultActions resultActions = - getResultActions(token, courseTrip.getId(), courseStamp1.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_ALREADY_DELETED.getMessage())); - } - - @Test - @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") - void shouldReturnForbiddenWhenNotTripOwner() throws Exception { - // when - ResultActions resultActions = - getResultActions(newToken, courseTrip.getId(), courseStamp1.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.NOT_TRIP_OWNER.getMessage())); - } - - @Test - @DisplayName("스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") - void shouldReturnForbiddenWhenStampNotBelongToTrip() throws Exception { - // when - ResultActions resultActions = - getResultActions(token, exploreTrip.getId(), courseStamp1.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 여행 ID가 들어오면 404 Not Found를 반환한다.") - void shouldReturnNotFoundWhenTripIdIsInvalid() throws Exception { - // given - Long invalidTripId = 10000L; - - // when - ResultActions resultActions = - getResultActions(token, invalidTripId, courseStamp1.getId()); - - // when & then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 스탬프 ID가 들어오면 404 Not Found를 반환한다.") - void shouldReturnNotFoundWhenStampIdIsInvalid() throws Exception { - // given - Long invalidStampId = 10000L; - - // when - ResultActions resultActions = - getResultActions(token, courseTrip.getId(), invalidStampId); - - // when & then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(StampErrorCode.STAMP_NOT_FOUND.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(StampErrorCode.STAMP_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("특정 스탬프 하위의 미션이 하나라도 완료되지 않았다면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenAnyMissionIsNotCompleted() throws Exception { - // given - courseMission1.updateCompleted(); - - // when - ResultActions resultActions = - getResultActions(token, courseTrip.getId(), courseStamp1.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - MissionErrorCode.ALL_MISSIONS_NOT_COMPLETED - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value( - MissionErrorCode.ALL_MISSIONS_NOT_COMPLETED - .getMessage())); - } - - @Test - @DisplayName("스탬프가 이미 완료되었다면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenStampAlreadyCompleted() throws Exception { - // given - courseMission1.updateCompleted(); - courseMission2.updateCompleted(); - courseStamp1.updateCompleted(); - - // when - ResultActions resultActions = - getResultActions(token, courseTrip.getId(), courseStamp1.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - StampErrorCode.STAMP_ALREADY_COMPLETED - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(StampErrorCode.STAMP_ALREADY_COMPLETED.getMessage())); - } - - @Test - @DisplayName("특정 스탬프 하위의 모든 미션이 완료되었다면 스탬프를 완료합니다.") - void shouldCompleteStamp() throws Exception { - // given - courseMission1.updateCompleted(); - courseMission2.updateCompleted(); - - // when - ResultActions resultActions = - getResultActions(token, courseTrip.getId(), courseStamp2.getId()); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalCommandServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalCommandServiceTest.java deleted file mode 100644 index ac976ca..0000000 --- a/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalCommandServiceTest.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.domain.repository.DailyGoalCommandRepository; -import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; -import com.ject.studytrip.trip.fixture.DailyGoalFixture; -import com.ject.studytrip.trip.fixture.TripFixture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -@DisplayName("DailyGoalCommandService 단위 테스트") -class DailyGoalCommandServiceTest extends BaseUnitTest { - @InjectMocks private DailyGoalCommandService dailyGoalCommandService; - @Mock private DailyGoalRepository dailyGoalRepository; - @Mock private DailyGoalCommandRepository dailyGoalCommandRepository; - - private Trip trip; - private DailyGoal dailyGoal; - - @BeforeEach - void setUp() { - Member member = MemberFixture.createMemberFromKakaoWithId(1L); - trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE); - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, trip); - } - - @Nested - @DisplayName("createDailyGoal 메서드는") - class CreateDailyGoal { - - @Test - @DisplayName("여행에 속한 데일리 목표를 생성하고 저장된 값을 반환한다") - void shouldCreateAndSaveDailyGoal() { - // given - String title = "TEST TITLE"; - given(dailyGoalRepository.save(any())).willReturn(dailyGoal); - - // when - DailyGoal result = dailyGoalCommandService.createDailyGoal(trip, title); - - // then - assertThat(result).isNotNull(); - assertThat(result.getTrip().getId()).isEqualTo(trip.getId()); - } - } - - @Nested - @DisplayName("deleteDailyGoal 메서드는") - class DeleteDailyGoal { - - @Test - @DisplayName("deletedAt을 현재 시간으로 설정한다") - void shouldSoftDeleteDailyGoal() { - // when - dailyGoalCommandService.deleteDailyGoal(dailyGoal); - - // then - assertThat(dailyGoal.getDeletedAt()).isNotNull(); - } - } - - @Nested - @DisplayName("hardDeleteDailyGoals 메서드는") - class HardDeleteDailyGoals { - - @Test - @DisplayName("삭제된 데일리 목표가 없으면 0을 반환한다.") - void shouldReturnZeroWhenDeletedDailyGoalsDoNotExist() { - // given - given(dailyGoalCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); - - // when - long result = dailyGoalCommandService.hardDeleteDailyGoals(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 데일리 목표가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenDeletedDailyGoalsExist() { - // given - given(dailyGoalCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); - - // when - long result = dailyGoalCommandService.hardDeleteDailyGoals(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteDailyGoalsOwnedByDeletedTrip 메서드는") - class HardDeleteDailyGoalsOwnedByDeletedTrip { - - @Test - @DisplayName("삭제된 여행이 소유한 데일리 목표가 없으면 0을 반환한다.") - void shouldReturnZeroWhenDailyGoalsOwnedByDeletedTripDoNotExist() { - // given - given(dailyGoalCommandRepository.deleteAllByDeletedTripOwner()).willReturn(0L); - - // when - long result = dailyGoalCommandService.hardDeleteDailyGoalsOwnedByDeletedTrip(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 여행이 소유한 데일리 목표가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenDailyGoalsOwnedByDeletedTripExist() { - // given - given(dailyGoalCommandRepository.deleteAllByDeletedTripOwner()).willReturn(5L); - - // when - long result = dailyGoalCommandService.hardDeleteDailyGoalsOwnedByDeletedTrip(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteDailyGoalsByMember 메서드는") - class HardDeleteDailyGoalsByMember { - - @Test - @DisplayName("특정 멤버가 소유한 데일리 목표가 없으면 0을 반환한다.") - void shouldReturnZeroWhenDailyGoalsOwnedByMemberDoNotExist() { - // given - Long memberId = 1L; - given(dailyGoalCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L); - - // when - long result = dailyGoalCommandService.hardDeleteDailyGoalsByMember(memberId); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("특정 멤버가 소유한 데일리 목표가 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenDailyGoalsOwnedByMemberExist() { - // given - Long memberId = 1L; - given(dailyGoalCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L); - - // when - long result = dailyGoalCommandService.hardDeleteDailyGoalsByMember(memberId); - - // then - assertThat(result).isEqualTo(5L); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalQueryServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalQueryServiceTest.java deleted file mode 100644 index 49f8a0f..0000000 --- a/src/test/java/com/ject/studytrip/trip/application/service/DailyGoalQueryServiceTest.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; -import com.ject.studytrip.trip.fixture.DailyGoalFixture; -import com.ject.studytrip.trip.fixture.TripFixture; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -@DisplayName("DailyGoalQueryService 단위 테스트") -class DailyGoalQueryServiceTest extends BaseUnitTest { - @InjectMocks private DailyGoalQueryService dailyGoalQueryService; - @Mock private DailyGoalRepository dailyGoalRepository; - - private Member member; - private Trip trip; - private DailyGoal dailyGoal1; - private DailyGoal dailyGoal2; - - @BeforeEach - void setUp() { - member = MemberFixture.createMemberFromKakaoWithId(1L); - trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE); - dailyGoal1 = DailyGoalFixture.createDailyGoalWithId(1L, trip); - dailyGoal2 = DailyGoalFixture.createDailyGoalWithId(2L, trip); - } - - @Nested - @DisplayName("getDailyGoal 메서드는") - class GetDailyGoal { - - @Test - @DisplayName("ID로 조회된 데일리 목표가 trip에 속하고 삭제되지 않았다면 반환한다") - void shouldReturnValidDailyGoal() { - // given - Long dailyGoalId = dailyGoal1.getId(); - given(dailyGoalRepository.findById(dailyGoalId)).willReturn(Optional.of(dailyGoal1)); - - // when - DailyGoal result = dailyGoalQueryService.getValidDailyGoal(trip.getId(), dailyGoalId); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(dailyGoalId); - } - - @Test - @DisplayName("데일리 목표가 존재하지 않으면 예외가 발생한다") - void shouldThrowExceptionWhenDailyGoalNotFound() { - // given - Long invalidId = -1L; - given(dailyGoalRepository.findById(invalidId)).willReturn(Optional.empty()); - - // when & then - assertThatThrownBy( - () -> dailyGoalQueryService.getValidDailyGoal(trip.getId(), invalidId)) - .isInstanceOf(CustomException.class) - .hasMessage(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("다른 여행에 속한 데일리 목표일 경우 예외가 발생한다") - void shouldThrowExceptionWhenNotBelongToTrip() { - // given - Long dailyGoalId = dailyGoal1.getId(); - Trip otherTrip = TripFixture.createTripWithId(999L, member, TripCategory.COURSE); - given(dailyGoalRepository.findById(dailyGoalId)).willReturn(Optional.of(dailyGoal1)); - - // when & then - assertThatThrownBy( - () -> - dailyGoalQueryService.getValidDailyGoal( - otherTrip.getId(), dailyGoalId)) - .isInstanceOf(CustomException.class) - .hasMessage(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONG_TO_TRIP.getMessage()); - } - - @Test - @DisplayName("삭제된 데일리 목표일 경우 예외가 발생한다") - void shouldThrowExceptionWhenDeletedDailyGoal() { - // given - dailyGoal1.updateDeletedAt(); - Long dailyGoalId = dailyGoal1.getId(); - given(dailyGoalRepository.findById(dailyGoalId)).willReturn(Optional.of(dailyGoal1)); - - // when & then - assertThatThrownBy( - () -> - dailyGoalQueryService.getValidDailyGoal( - trip.getId(), dailyGoalId)) - .isInstanceOf(CustomException.class) - .hasMessage(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.getMessage()); - } - } - - @Nested - @DisplayName("getCompleteDailyGoalsByTrip 메서드는") - class GetCompleteDailyGoalsByTrip { - - @Test - @DisplayName("데일리 목표가 완료되지 않았다면 빈 목록을 반환한다.") - void shouldReturnEmptyListWhenDailyGoalsNotCompleted() { - // given - Long tripId = trip.getId(); - given(dailyGoalRepository.findAllByTripIdAndCompletedIsTrue(tripId)) - .willReturn(List.of()); - - // when - List result = dailyGoalQueryService.getCompleteDailyGoalsByTripId(tripId); - - // then - assertThat(result.size()).isEqualTo(0); - } - - @Test - @DisplayName("데일리 목표가 완료되었다면 완료된 데일리 목표 목록을 반환한다.") - void shouldReturnCompleteDailyGoalsWhenDailyGoalsCompleted() { - // given - dailyGoal1.updateCompleted(); - dailyGoal2.updateCompleted(); - Long tripId = trip.getId(); - given(dailyGoalRepository.findAllByTripIdAndCompletedIsTrue(tripId)) - .willReturn(List.of(dailyGoal1, dailyGoal2)); - - // when - List result = dailyGoalQueryService.getCompleteDailyGoalsByTripId(tripId); - - // then - assertThat(result.size()).isEqualTo(2); - assertThat(result.get(0).getId()).isEqualTo(dailyGoal1.getId()); - assertThat(result.get(1).getId()).isEqualTo(dailyGoal2.getId()); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripCommandServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripCommandServiceTest.java deleted file mode 100644 index 88ad92a..0000000 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripCommandServiceTest.java +++ /dev/null @@ -1,359 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.trip.domain.error.TripErrorCode; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.domain.repository.TripCommandRepository; -import com.ject.studytrip.trip.domain.repository.TripRepository; -import com.ject.studytrip.trip.fixture.CreateTripRequestFixture; -import com.ject.studytrip.trip.fixture.TripFixture; -import com.ject.studytrip.trip.fixture.UpdateTripRequestFixture; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest; -import java.time.LocalDate; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -@DisplayName("TripCommandService 단위 테스트") -class TripCommandServiceTest extends BaseUnitTest { - private static final String NEW_TRIP_NAME = "NEW TRIP NAME"; - - @InjectMocks private TripCommandService tripCommandService; - @Mock private TripRepository tripRepository; - @Mock private TripCommandRepository tripCommandRepository; - - private Member member; - private Trip trip; - - @BeforeEach - void setup() { - member = MemberFixture.createMemberFromKakaoWithId(1L); - trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE); - } - - @Nested - @DisplayName("createTrip 메서드는") - class CreateTrip { - - @Test - @DisplayName("여행 정보를 생성해 DB에 저장하고, 저장된 Trip을 반환한다") - void shouldSaveTripReturnTrip() { - // given - CreateTripRequest request = new CreateTripRequestFixture().build(); - given(tripRepository.save(any())).willReturn(trip); - - // when - Trip saved = tripCommandService.createTrip(member, request); - - // then - assertThat(saved).isNotNull(); - assertThat(saved.getId()).isEqualTo(trip.getId()); - } - - @Test - @DisplayName("여행 카테고리가 누락되었을 경우 예외가 발생한다") - void shouldThrowExceptionWhenMissingTripCategory() { - // given - CreateTripRequest request = new CreateTripRequestFixture().withCategory(null).build(); - - // when & then - assertThatThrownBy(() -> tripCommandService.createTrip(member, request)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(TripErrorCode.TRIP_CATEGORY_REQUIRED.getMessage()); - } - - @Test - @DisplayName("여행 카테고리가 유효하지 않은 경우 예외가 발생한다") - void shouldThrowExceptionWhenInvalidTripCategory() { - // given - CreateTripRequest request = new CreateTripRequestFixture().withCategory("test").build(); - - // when & then - assertThatThrownBy(() -> tripCommandService.createTrip(member, request)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(TripErrorCode.INVALID_TRIP_CATEGORY.getMessage()); - } - - @Test - @DisplayName("코스형 여행인데 종료일이 null일 경우 예외가 발생한다") - void shouldThrowExceptionWhenCourseTripEndDateIsNull() { - // given - CreateTripRequest request = - new CreateTripRequestFixture() - .withCategory(TripCategory.COURSE.name()) - .withEndDate(null) - .build(); - - // When & Then - assertThatThrownBy(() -> tripCommandService.createTrip(member, request)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(TripErrorCode.COURSE_TRIP_END_DATE_REQUIRED.getMessage()); - } - - @Test - @DisplayName("여행 종료일이 시작일보다 이전일 경우 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsBeforeStartDate() { - // given - CreateTripRequest request = - new CreateTripRequestFixture() - .withEndDate(LocalDate.now().minusDays(7)) - .build(); - - // When & Then - assertThatThrownBy(() -> tripCommandService.createTrip(member, request)) - .isInstanceOf(CustomException.class) - .hasMessageContaining( - TripErrorCode.TRIP_END_DATE_BEFORE_START_DATE.getMessage()); - } - - @Test - @DisplayName("함께 등록할 여행 스탬프 목록이 비어있으면 예외가 발생한다") - void shouldThrowExceptionWhenStampsIsEmpty() { - // given - CreateTripRequest request = - new CreateTripRequestFixture().withStamps(List.of()).build(); - - // When & Then - assertThatThrownBy(() -> tripCommandService.createTrip(member, request)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(TripErrorCode.TRIP_STAMP_REQUIRED.getMessage()); - } - } - - @Nested - @DisplayName("updateTrip 메서드는") - class UpdateTrip { - - @Test - @DisplayName("특정 여행의 정보를 수정하고 DB에 반영한다") - void shouldUpdateTrip() { - // given - UpdateTripRequest request = - new UpdateTripRequestFixture().withName(NEW_TRIP_NAME).build(); - - // When - tripCommandService.updateTrip(trip, request); - - // Then - assertThat(trip.getName()).isEqualTo(NEW_TRIP_NAME); - } - - @Test - @DisplayName("여행 종료일이 시작일보다 이전일 경우 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsBeforeStartDate() { - // given - UpdateTripRequest request = - new UpdateTripRequestFixture() - .withEndDate(trip.getStartDate().minusDays(1)) - .build(); - - // When & Then - assertThatThrownBy(() -> tripCommandService.updateTrip(trip, request)) - .isInstanceOf(CustomException.class) - .hasMessageContaining( - TripErrorCode.TRIP_END_DATE_BEFORE_START_DATE.getMessage()); - } - } - - @Nested - @DisplayName("increaseTotalStamps 메서드는") - class IncreaseTotalStamps { - - @Test - @DisplayName("여행의 총 스탬프 수를 +1 증가시킨다") - void shouldIncreaseTotalStamps() { - // given - int tripTotalStamps = trip.getTotalStamps(); - - // when - trip.increaseTotalStamps(); - - // then - assertThat(trip.getTotalStamps()).isEqualTo(tripTotalStamps + 1); - } - } - - @Nested - @DisplayName("decreaseTotalStamps 메서드는") - class DecreaseTotalStamps { - - @Test - @DisplayName("여행의 총 스탬프 수를 -1 감소시킨다") - void shouldDecreaseTotalStamps() { - // given - int tripTotalStamps = trip.getTotalStamps(); - - // when - trip.decreaseTotalStamps(); - - // then - assertThat(trip.getTotalStamps()).isEqualTo(tripTotalStamps - 1); - } - } - - @Nested - @DisplayName("deleteTrip 메서드는") - class DeleteTrip { - - @Test - @DisplayName("특정 여행을 deletedAt 필드를 현재 시간으로 업데이트한다") - void shouldDeleteTripForUpdateDeletedAt() { - // when - tripCommandService.deleteTrip(trip); - - // then - assertThat(trip.getDeletedAt()).isNotNull(); - } - } - - @Nested - @DisplayName("completeTrip 메서드는") - class CompleteTrip { - - @Test - @DisplayName("이미 완료된 여행이면 예외가 발생한다.") - void shouldThrowExceptionWhenTripIsAlreadyCompleted() { - // given - trip.updateCompleted(); - - // when & then - assertThatThrownBy(() -> tripCommandService.completeTrip(trip)) - .isInstanceOf(CustomException.class) - .hasMessage(TripErrorCode.TRIP_ALREADY_COMPLETED.getMessage()); - } - - @Test - @DisplayName("유효한 여행이 들어오면, completed 필드를 true로 업데이트한다.") - void shouldCompleteStamp() { - // when - tripCommandService.completeTrip(trip); - - // then - assertThat(trip.isCompleted()).isTrue(); - } - } - - @Nested - @DisplayName("increaseCompletedStamps 메서드는") - class IncreaseCompletedStamps { - - @Test - @DisplayName("유효한 여행이 들어오면, Trip의 completedStamps 필드를 1 증가시킨다.") - void shouldIncreaseCompletedStamps() { - // when - tripCommandService.increaseCompletedStamps(trip); - - // then - assertThat(trip.getCompletedStamps()).isEqualTo(1); - } - } - - @Nested - @DisplayName("hardDeleteTrips 메서드는") - class HardDeleteTrips { - - @Test - @DisplayName("삭제된 여행이 없으면 0을 반환한다.") - void shouldReturnZeroWhenDeletedTripsDoNotExist() { - // given - given(tripCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L); - - // when - long result = tripCommandService.hardDeleteTrips(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 여행이 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenDeletedTripsExist() { - // given - given(tripCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L); - - // when - long result = tripCommandService.hardDeleteTrips(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteTripsOwnedByDeletedMember 메서드는") - class HardDeleteTripsOwnedByDeletedMember { - - @Test - @DisplayName("삭제된 멤버가 소유한 여행이 없으면 0을 반환한다.") - void shouldReturnZeroWhenTripsOwnedByDeletedMemberDoNotExist() { - // given - given(tripCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(0L); - - // when - long result = tripCommandService.hardDeleteTripsOwnedByDeletedMember(); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("삭제된 멤버가 소유한 여행이 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenTripsOwnedByDeletedMemberExist() { - // given - given(tripCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(5L); - - // when - long result = tripCommandService.hardDeleteTripsOwnedByDeletedMember(); - - // then - assertThat(result).isEqualTo(5L); - } - } - - @Nested - @DisplayName("hardDeleteTripsByMember 메서드는") - class HardDeleteTripsByMember { - - @Test - @DisplayName("특정 멤버가 소유한 여행이 없으면 0을 반환한다.") - void shouldReturnZeroWhenTripsOwnedByMemberDoNotExist() { - // given - Long memberId = 1L; - given(tripCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L); - - // when - long result = tripCommandService.hardDeleteTripsByMember(memberId); - - // then - assertThat(result).isEqualTo(0L); - } - - @Test - @DisplayName("특정 멤버가 소유한 여행이 있으면 해당 개수를 반환한다.") - void shouldReturnCountWhenTripsOwnedByMemberExist() { - // given - Long memberId = 1L; - given(tripCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L); - - // when - long result = tripCommandService.hardDeleteTripsByMember(memberId); - - // then - assertThat(result).isEqualTo(5L); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripQueryServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripQueryServiceTest.java deleted file mode 100644 index 6785706..0000000 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripQueryServiceTest.java +++ /dev/null @@ -1,282 +0,0 @@ -package com.ject.studytrip.trip.application.service; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -import com.ject.studytrip.BaseUnitTest; -import com.ject.studytrip.global.exception.CustomException; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.fixture.MemberFixture; -import com.ject.studytrip.trip.application.dto.TripCount; -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.domain.repository.TripQueryRepository; -import com.ject.studytrip.trip.domain.repository.TripRepository; -import com.ject.studytrip.trip.fixture.TripFixture; -import java.util.List; -import java.util.Optional; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; - -@DisplayName("TripQueryService 단위 테스트") -class TripQueryServiceTest extends BaseUnitTest { - private static final int DEFAULT_PAGE = 0; - private static final int DEFAULT_SIZE = 5; - - @InjectMocks private TripQueryService tripQueryService; - @Mock private TripRepository tripRepository; - @Mock private TripQueryRepository tripQueryRepository; - - private Member member; - private Trip trip; - - @BeforeEach - void setup() { - member = MemberFixture.createMemberFromKakaoWithId(1L); - trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE); - } - - @Nested - @DisplayName("getTrip 메서드는") - class GetTrip { - - @Test - @DisplayName("존재하지 않는 여행 ID로 조회하면 예외가 발생한다.") - void shouldThrowExceptionWhenTripIdNotFound() { - // given - Long invalidId = -1L; - given(tripRepository.findById(invalidId)).willReturn(Optional.empty()); - - // when & then - Assertions.assertThatThrownBy(() -> tripQueryService.getTrip(invalidId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripErrorCode.TRIP_NOT_FOUND.getMessage()); - } - - @Test - @DisplayName("특정 여행 ID로 DB에서 조회한 후 반환한다") - void shouldGetTripByTripIdReturnTrip() { - // given - Long tripId = trip.getId(); - given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)); - - // when - Trip result = tripQueryService.getTrip(tripId); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(tripId); - } - } - - @Nested - @DisplayName("getValidTrip 메서드는") - class GetValidTrip { - - @Test - @DisplayName("여행의 소유자가 아닐 경우 예외가 발생한다") - void shouldThrowExceptionWhenNotTripOwner() { - // given - Member newMember = MemberFixture.createMemberFromKakao(); - Long tripId = trip.getId(); - given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)); - - // When & Then - assertThatThrownBy(() -> tripQueryService.getValidTrip(newMember.getId(), tripId)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(TripErrorCode.NOT_TRIP_OWNER.getMessage()); - } - - @Test - @DisplayName("이미 삭제된 여행일 경우 예외가 발생한다") - void shouldThrowExceptionWhenAlreadyTrip() { - // given - trip.updateDeletedAt(); - Long deletedId = trip.getId(); - given(tripRepository.findById(deletedId)).willReturn(Optional.of(trip)); - - // when & then - assertThatThrownBy(() -> tripQueryService.getValidTrip(member.getId(), deletedId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripErrorCode.TRIP_ALREADY_DELETED.getMessage()); - } - - @Test - @DisplayName("이미 완료된 여행일 경우 예외가 발생한다") - void shouldThrowExceptionWhenTripAlreadyCompleted() { - // given - trip.updateCompleted(); - Long tripId = trip.getId(); - given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)); - - // when & then - assertThatThrownBy(() -> tripQueryService.getValidTrip(member.getId(), tripId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripErrorCode.TRIP_ALREADY_COMPLETED.getMessage()); - } - - @Test - @DisplayName("특정 여행 ID로 DB에서 조회한 후 유효한 여행을 반환한다") - void shouldGetTripByTripIdReturnValidTrip() { - // given - Long tripId = trip.getId(); - given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)); - - // when - Trip result = tripQueryService.getValidTrip(member.getId(), trip.getId()); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(tripId); - } - } - - @Nested - @DisplayName("getTripsSliceByMemberId 메서드는") - class GetTripsSliceByMemberId { - - @Test - @DisplayName("로그인된 사용자의 완료되지 않고 삭제되지 않은 여행 목록을 DB에서 조회하고 슬라이스 처리해 반환한다") - void shouldGetTripsReturnSlicePaged() { - // given - Long memberId = member.getId(); - List trips = List.of(trip); - Pageable pageable = PageRequest.of(DEFAULT_PAGE, DEFAULT_SIZE); - Slice results = new SliceImpl<>(trips, pageable, false); - given( - tripQueryRepository - .findSliceByMemberIdAndCompletedFalseAndDeletedAtIsNull( - memberId, pageable)) - .willReturn(results); - - // when - Slice sliceTrips = - tripQueryService.getTripsSliceByMemberId(memberId, DEFAULT_PAGE, DEFAULT_SIZE); - - // then - assertThat(sliceTrips.hasContent()).isTrue(); - assertThat(sliceTrips.hasNext()).isFalse(); - } - } - - @Nested - @DisplayName("getActiveTripCountsByMemberId 메서드는") - class GetActiveTripCountsByMemberId { - - @Test - @DisplayName("해당 멤버의 여행이 존재하지 않으면 0을 반환한다.") - void shouldReturnZeroWhenTripDoesNotExistForMember() { - // given - Long memberId = member.getId(); - given( - tripQueryRepository.countActiveTripsByMemberIdAndCategory( - memberId, TripCategory.COURSE)) - .willReturn(0L); - given( - tripQueryRepository.countActiveTripsByMemberIdAndCategory( - memberId, TripCategory.EXPLORE)) - .willReturn(0L); - - // when - TripCount result = tripQueryService.getActiveTripCountsByMemberId(memberId); - - // then - assertThat(result.course()).isZero(); - assertThat(result.explore()).isZero(); - } - - @Test - @DisplayName("코스형과 탐험형 여행 개수를 각각 조회하여 TripCount를 반환한다.") - void shouldReturnTripCountByCategory() { - // given - Long memberId = member.getId(); - given( - tripQueryRepository.countActiveTripsByMemberIdAndCategory( - memberId, TripCategory.COURSE)) - .willReturn(3L); - given( - tripQueryRepository.countActiveTripsByMemberIdAndCategory( - memberId, TripCategory.EXPLORE)) - .willReturn(2L); - - // when - TripCount result = tripQueryService.getActiveTripCountsByMemberId(memberId); - - // then - assertThat(result.course()).isEqualTo(3L); - assertThat(result.explore()).isEqualTo(2L); - } - } - - @Nested - @DisplayName("getValidCompletedTrip 메서드는") - class GetValidCompletedTrip { - - @Test - @DisplayName("여행의 소유자가 아닐 경우 예외가 발생한다") - void shouldThrowExceptionWhenNotTripOwner() { - // given - Member newMember = MemberFixture.createMemberFromKakao(); - Long tripId = trip.getId(); - given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)); - - // When & Then - assertThatThrownBy(() -> tripQueryService.getValidTrip(newMember.getId(), tripId)) - .isInstanceOf(CustomException.class) - .hasMessageContaining(TripErrorCode.NOT_TRIP_OWNER.getMessage()); - } - - @Test - @DisplayName("이미 삭제된 여행일 경우 예외가 발생한다") - void shouldThrowExceptionWhenTripIsDeleted() { - // given - trip.updateDeletedAt(); - Long deletedId = trip.getId(); - given(tripRepository.findById(deletedId)).willReturn(Optional.of(trip)); - - // when & then - assertThatThrownBy(() -> tripQueryService.getValidTrip(member.getId(), deletedId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripErrorCode.TRIP_ALREADY_DELETED.getMessage()); - } - - @Test - @DisplayName("여행이 아직 완료되지 않은 경우 예외가 발생한다") - void shouldThrowExceptionWhenTripDoesNotCompletedYet() { - // given - Long tripId = trip.getId(); - given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)); - - // when & then - assertThatThrownBy(() -> tripQueryService.getValidCompletedTrip(member.getId(), tripId)) - .isInstanceOf(CustomException.class) - .hasMessage(TripErrorCode.TRIP_NOT_COMPLETED.getMessage()); - } - - @Test - @DisplayName("여행이 이미 완료되었다면 여행을 반환한다.") - void shouldReturnTripWhenTripAlreadyCompleted() { - // given - trip.updateCompleted(); - given(tripRepository.findById(trip.getId())).willReturn(Optional.of(trip)); - - // when - Trip result = tripQueryService.getValidCompletedTrip(member.getId(), trip.getId()); - - // then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(trip.getId()); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java b/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java index f4486eb..06059d0 100644 --- a/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java +++ b/src/test/java/com/ject/studytrip/trip/application/service/TripReportStudyLogCommandServiceTest.java @@ -38,8 +38,8 @@ class TripReportStudyLogCommandServiceTest extends BaseUnitTest { @BeforeEach void setUp() { Member member = MemberFixture.createMemberFromKakaoWithId(1L); - Trip trip = TripFixture.createTrip(member, TripCategory.COURSE); - DailyGoal dailyGoal = DailyGoalFixture.createDailyGoal(trip); + Trip trip = new TripFixture(member, TripCategory.COURSE).create(); + DailyGoal dailyGoal = new DailyGoalFixture(trip).create(); StudyLog studyLog1 = new StudyLogFixture(member, dailyGoal).createWithId(1L); StudyLog studyLog2 = new StudyLogFixture(member, dailyGoal).createWithId(2L); tripReport = TripReportFixture.createTripReportWithId(1L, member); diff --git a/src/test/java/com/ject/studytrip/trip/fixture/CreateDailyGoalRequestFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/CreateDailyGoalRequestFixture.java deleted file mode 100644 index 66e882c..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/CreateDailyGoalRequestFixture.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.pomodoro.presentation.dto.request.CreatePomodoroRequest; -import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest; -import java.util.List; - -public class CreateDailyGoalRequestFixture { - - private CreatePomodoroRequest pomodoro = new CreatePomodoroRequest(30, 1); - private List missionIds = List.of(1L, 2L); - - public CreateDailyGoalRequestFixture withPomodoro(CreatePomodoroRequest pomodoro) { - this.pomodoro = pomodoro; - return this; - } - - public CreateDailyGoalRequestFixture withMissionIds(List missionIds) { - this.missionIds = missionIds; - return this; - } - - public CreateDailyGoalRequest build() { - return new CreateDailyGoalRequest(pomodoro, missionIds); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/CreateTripRequestFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/CreateTripRequestFixture.java deleted file mode 100644 index c8aee58..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/CreateTripRequestFixture.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.stamp.fixture.CreateStampRequestFixture; -import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest; -import java.time.LocalDate; -import java.util.List; - -public class CreateTripRequestFixture { - - private String name = "TEST 여행"; - private String memo = "TEST 여행입니다."; - private String category = TripCategory.COURSE.name(); - private LocalDate endDate = LocalDate.now().plusDays(10); - private List stamps = List.of(new CreateStampRequestFixture().build()); - - public CreateTripRequestFixture withName(String name) { - this.name = name; - return this; - } - - public CreateTripRequestFixture withMemo(String memo) { - this.memo = memo; - return this; - } - - public CreateTripRequestFixture withEndDate(LocalDate endDate) { - this.endDate = endDate; - return this; - } - - public CreateTripRequestFixture withCategory(String category) { - this.category = category; - return this; - } - - public CreateTripRequestFixture withStamps(List stamps) { - this.stamps = stamps; - return this; - } - - public CreateTripRequest build() { - return new CreateTripRequest(name, memo, category, endDate, stamps); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/DailyGoalFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/DailyGoalFixture.java deleted file mode 100644 index 149a3f5..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/DailyGoalFixture.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.trip.domain.factory.DailyGoalFactory; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import org.springframework.test.util.ReflectionTestUtils; - -public class DailyGoalFixture { - private static final String DEFAULT_TITLE = "TEST TITLE"; - - public static DailyGoal createDailyGoal(Trip trip) { - return DailyGoalFactory.create(trip, DEFAULT_TITLE); - } - - public static DailyGoal createDailyGoalWithId(Long id, Trip trip) { - DailyGoal dailyGoal = DailyGoalFactory.create(trip, DEFAULT_TITLE); - ReflectionTestUtils.setField(dailyGoal, "id", id); - - return dailyGoal; - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/TripFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/TripFixture.java deleted file mode 100644 index 49b9017..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/TripFixture.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.factory.TripFactory; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import java.time.LocalDate; -import org.springframework.test.util.ReflectionTestUtils; - -public class TripFixture { - private static final String TRIP_NAME = "TEST TRIP NAME"; - private static final String TRIP_MEMO = "TEST TRIP MEMO"; - private static final LocalDate TRIP_END_DATE = LocalDate.now().plusDays(7); - private static final int TRIP_TOTAL_STAMPS = 1; - - public static Trip createTrip(Member member, TripCategory category) { - return TripFactory.create( - member, TRIP_NAME, TRIP_MEMO, category, TRIP_END_DATE, TRIP_TOTAL_STAMPS); - } - - public static Trip createTripWithId(Long id, Member member, TripCategory category) { - Trip trip = - TripFactory.create( - member, TRIP_NAME, TRIP_MEMO, category, TRIP_END_DATE, TRIP_TOTAL_STAMPS); - ReflectionTestUtils.setField(trip, "id", id); - - return trip; - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/UpdateDailyGoalRequestFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/UpdateDailyGoalRequestFixture.java deleted file mode 100644 index 6269f93..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/UpdateDailyGoalRequestFixture.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest; -import java.util.List; - -public class UpdateDailyGoalRequestFixture { - private List deleteDailyMissionIds = null; - private List addMissionIds = null; - - public UpdateDailyGoalRequestFixture withDeleteDailyMissionIds( - List deleteDailyMissionIds) { - this.deleteDailyMissionIds = deleteDailyMissionIds; - return this; - } - - public UpdateDailyGoalRequestFixture withAddMissionIds(List addMissionIds) { - this.addMissionIds = addMissionIds; - return this; - } - - public UpdateDailyGoalRequest build() { - return new UpdateDailyGoalRequest(deleteDailyMissionIds, addMissionIds); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/fixture/UpdateTripRequestFixture.java b/src/test/java/com/ject/studytrip/trip/fixture/UpdateTripRequestFixture.java deleted file mode 100644 index daa2045..0000000 --- a/src/test/java/com/ject/studytrip/trip/fixture/UpdateTripRequestFixture.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ject.studytrip.trip.fixture; - -import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest; -import java.time.LocalDate; - -public class UpdateTripRequestFixture { - - private String name = null; - private String memo = null; - private String category = null; - private LocalDate endDate = null; - - public UpdateTripRequestFixture withName(String name) { - this.name = name; - return this; - } - - public UpdateTripRequestFixture withMemo(String memo) { - this.memo = memo; - return this; - } - - public UpdateTripRequestFixture withEndDate(LocalDate endDate) { - this.endDate = endDate; - return this; - } - - public UpdateTripRequestFixture withCategory(String category) { - this.category = category; - return this; - } - - public UpdateTripRequest build() { - return new UpdateTripRequest(name, memo, category, endDate); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/helper/DailyGoalTestHelper.java b/src/test/java/com/ject/studytrip/trip/helper/DailyGoalTestHelper.java deleted file mode 100644 index 34ec41c..0000000 --- a/src/test/java/com/ject/studytrip/trip/helper/DailyGoalTestHelper.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.ject.studytrip.trip.helper; - -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.repository.DailyGoalRepository; -import com.ject.studytrip.trip.fixture.DailyGoalFixture; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class DailyGoalTestHelper { - - @Autowired private DailyGoalRepository dailyGoalRepository; - - public DailyGoal saveDailyGoal(Trip trip) { - DailyGoal dailyGoal = DailyGoalFixture.createDailyGoal(trip); - return dailyGoalRepository.save(dailyGoal); - } - - public DailyGoal saveDeletedDailyGoal(Trip trip) { - DailyGoal dailyGoal = DailyGoalFixture.createDailyGoal(trip); - dailyGoal.updateDeletedAt(); - - return dailyGoalRepository.save(dailyGoal); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/helper/TripTestHelper.java b/src/test/java/com/ject/studytrip/trip/helper/TripTestHelper.java deleted file mode 100644 index 83a72d3..0000000 --- a/src/test/java/com/ject/studytrip/trip/helper/TripTestHelper.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.ject.studytrip.trip.helper; - -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.domain.repository.TripRepository; -import com.ject.studytrip.trip.fixture.TripFixture; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class TripTestHelper { - - @Autowired private TripRepository tripRepository; - - public Trip saveTrip(Member member, TripCategory category) { - Trip trip = TripFixture.createTrip(member, category); - return tripRepository.save(trip); - } - - public Trip saveDeletedTrip(Member member, TripCategory category) { - Trip trip = TripFixture.createTrip(member, category); - trip.updateDeletedAt(); - return tripRepository.save(trip); - } - - public Trip saveCompletedTrip(Member member, TripCategory category) { - Trip trip = TripFixture.createTrip(member, category); - trip.updateCompleted(); - return tripRepository.save(trip); - } -} diff --git a/src/test/java/com/ject/studytrip/trip/presentation/controller/DailyGoalControllerIntegrationTest.java b/src/test/java/com/ject/studytrip/trip/presentation/controller/DailyGoalControllerIntegrationTest.java deleted file mode 100644 index 1bc4bbf..0000000 --- a/src/test/java/com/ject/studytrip/trip/presentation/controller/DailyGoalControllerIntegrationTest.java +++ /dev/null @@ -1,1347 +0,0 @@ -package com.ject.studytrip.trip.presentation.controller; - -import static com.ject.studytrip.auth.fixture.TokenFixture.TOKEN_PREFIX; -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.helper.TokenTestHelper; -import com.ject.studytrip.global.exception.error.CommonErrorCode; -import com.ject.studytrip.member.domain.model.Member; -import com.ject.studytrip.member.domain.model.MemberRole; -import com.ject.studytrip.member.helper.MemberTestHelper; -import com.ject.studytrip.mission.domain.error.DailyMissionErrorCode; -import com.ject.studytrip.mission.domain.error.MissionErrorCode; -import com.ject.studytrip.mission.domain.model.DailyMission; -import com.ject.studytrip.mission.domain.model.Mission; -import com.ject.studytrip.mission.helper.DailyMissionTestHelper; -import com.ject.studytrip.mission.helper.MissionTestHelper; -import com.ject.studytrip.pomodoro.domain.error.PomodoroErrorCode; -import com.ject.studytrip.pomodoro.domain.model.Pomodoro; -import com.ject.studytrip.pomodoro.helper.PomodoroTestHelper; -import com.ject.studytrip.pomodoro.presentation.dto.request.CreatePomodoroRequest; -import com.ject.studytrip.stamp.domain.error.StampErrorCode; -import com.ject.studytrip.stamp.domain.model.Stamp; -import com.ject.studytrip.stamp.helper.StampTestHelper; -import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode; -import com.ject.studytrip.trip.domain.error.TripErrorCode; -import com.ject.studytrip.trip.domain.model.DailyGoal; -import com.ject.studytrip.trip.domain.model.Trip; -import com.ject.studytrip.trip.domain.model.TripCategory; -import com.ject.studytrip.trip.fixture.CreateDailyGoalRequestFixture; -import com.ject.studytrip.trip.fixture.UpdateDailyGoalRequestFixture; -import com.ject.studytrip.trip.helper.DailyGoalTestHelper; -import com.ject.studytrip.trip.helper.TripTestHelper; -import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.ResultActions; - -@DisplayName("DailyGoalController 통합 테스트") -public class DailyGoalControllerIntegrationTest extends BaseIntegrationTest { - @Autowired private MemberTestHelper memberTestHelper; - @Autowired private TripTestHelper tripTestHelper; - @Autowired private StampTestHelper stampTestHelper; - @Autowired private MissionTestHelper missionTestHelper; - @Autowired private DailyGoalTestHelper dailyGoalTestHelper; - @Autowired private PomodoroTestHelper pomodoroTestHelper; - @Autowired private DailyMissionTestHelper dailyMissionTestHelper; - @Autowired private TokenTestHelper tokenTestHelper; - - private Member member; - private Trip trip; - private Stamp stamp; - private Mission firstMission; - private Mission secondMission; - private DailyGoal dailyGoal; - private DailyMission dailyMission; - private String token; - - @BeforeEach - void setUp() { - member = memberTestHelper.saveMember(); - trip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - stamp = stampTestHelper.saveStamp(trip, 1); - firstMission = missionTestHelper.saveMission(stamp); - secondMission = missionTestHelper.saveMission(stamp); - dailyGoal = dailyGoalTestHelper.saveDailyGoal(trip); - dailyMission = dailyMissionTestHelper.saveDailyMission(firstMission, dailyGoal); - token = - tokenTestHelper.createAccessToken( - member.getId().toString(), MemberRole.ROLE_USER.name()); - } - - @Nested - @DisplayName("데일리 목표 생성 API") - class CreateDailyGoal { - private final CreateDailyGoalRequestFixture fixture = new CreateDailyGoalRequestFixture(); - - private ResultActions getResultActions( - String token, Object tripId, CreateDailyGoalRequest request) throws Exception { - return mockMvc.perform( - post("/api/trips/{tripId}/daily-goals", tripId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - } - - @Test - @DisplayName("유효한 요청으로 데일리 목표와 뽀모도로, 데일리 미션을 함께 생성한다") - void shouldCreateDailyGoalWithPomodoroAndDailyMissions() throws Exception { - // given - CreateDailyGoalRequest request = - fixture.withMissionIds(List.of(firstMission.getId(), secondMission.getId())) - .build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // then - resultActions - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.dailyGoalId").isNumber()); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 Unauthorized를 반환한다") - void shouldReturnUnauthorizedWhenUnauthenticated() throws Exception { - // given - CreateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = getResultActions("", trip.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception { - // given - String tripId = "abc"; - CreateDailyGoalRequest request = fixture.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenInvalidRequiredFields() throws Exception { - // given - CreateDailyGoalRequest request = - fixture.withPomodoro(null).withMissionIds(List.of()).build(); - // when - ResultActions resultActions = getResultActions(token, trip.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("데일리 목표의 뽀모도로 정보 중 집중 시간이 1분 미만이면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenDailyGoalPomodoroFocusTimeInMinuteIsLessThanOneMinute() - throws Exception { - // given - CreateDailyGoalRequest request = - fixture.withPomodoro(new CreatePomodoroRequest(0, 1)).build(); - // when - ResultActions resultActions = getResultActions(token, trip.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("데일리 목표의 뽀모도로 정보 중 집중 세션이 1개 미만이면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenDailyGoalPomodoroFocusSessionCountIsLessThanOne() - throws Exception { - // given - CreateDailyGoalRequest request = - fixture.withPomodoro(new CreatePomodoroRequest(30, 0)).build(); - // when - ResultActions resultActions = getResultActions(token, trip.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 NotFound를 반환한다") - void shouldReturnNotFoundWhenInvalidTripId() throws Exception { - // given - Long tripId = 10000L; - CreateDailyGoalRequest request = fixture.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 Forbidden을 반환한다") - void shouldReturnForbiddenWhenNotTripOwner() throws Exception { - // given - Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); - Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); - CreateDailyGoalRequest request = fixture.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAlreadyTrip() throws Exception { - // given - Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); - CreateDailyGoalRequest request = fixture.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("요청한 미션 ID 목록으로 해당 미션을 조회하고, 하나라도 일치하지 않는 경우 404 NotFound를 반환한다") - void shouldReturnNotFoundWhenAnyMissionIdDoesNotExist() throws Exception { - // given - CreateDailyGoalRequest request = - fixture.withMissionIds(List.of(firstMission.getId(), 1000L)).build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(MissionErrorCode.MISSION_NOT_FOUND.getStatus().value())); - } - - @Test - @DisplayName("코스형 여행에서 현재 진행중인 스탬프(완료되지 않은 가장 첫번째 스탬프)가 없을 경우 404 NotFound를 반환한다") - void shouldReturnNotFoundWhenNotFoundWhenNoIncompleteStampExists() throws Exception { - // given - Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - stampTestHelper.saveCompletedStamp(newTrip, 1); - CreateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = getResultActions(token, newTrip.getId(), request); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(StampErrorCode.STAMP_NOT_FOUND.getStatus().value())); - } - - @Test - @DisplayName("조회된 미션들의 스탬프 정보를 확인해 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다") - void shouldReturnForbiddenWhenMissionsStampsDoesNotBelongToTrip() throws Exception { - // given - Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - stampTestHelper.saveStamp(newTrip, 1); - CreateDailyGoalRequest request = - fixture.withMissionIds(List.of(firstMission.getId())).build(); - - // 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_BELONGS_TO_TRIP - .getStatus() - .value())); - } - - @Test - @DisplayName("조회된 미션들 중 삭제된 미션이 존재하면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenMissionIsDeleted() throws Exception { - // given - Mission deleted = missionTestHelper.saveDeletedMission(stamp); - CreateDailyGoalRequest request = - fixture.withMissionIds(List.of(deleted.getId())).build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - MissionErrorCode.MISSION_ALREADY_DELETED - .getStatus() - .value())); - } - - @Test - @DisplayName("조회된 미션들 중 완료된 미션이 존재하면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenMissionIsAlreadyCompleted() throws Exception { - // given - Mission completed = missionTestHelper.saveCompletedMission(stamp); - CreateDailyGoalRequest request = - fixture.withMissionIds(List.of(completed.getId())).build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - MissionErrorCode.MISSION_ALREADY_COMPLETED - .getStatus() - .value())); - } - - @Test - @DisplayName("코스형 여행에서 조회된 미션들이 현재 진행중인 스탬프에 속하지 않은 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenMissionNotBelongToCurrentStamp() throws Exception { - // given - Stamp newStamp = stampTestHelper.saveStamp(trip, 2); - Mission newMission = missionTestHelper.saveMission(newStamp); - CreateDailyGoalRequest request = - fixture.withMissionIds(List.of(newMission.getId())).build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - MissionErrorCode.MISSION_NOT_BELONGS_TO_STAMP - .getStatus() - .value())); - } - } - - @Nested - @DisplayName("데일리 목표 수정 API") - class UpdateDailyGoal { - private final UpdateDailyGoalRequestFixture fixture = new UpdateDailyGoalRequestFixture(); - - private ResultActions getResultActions( - String token, Object tripId, Object dailyGoalId, UpdateDailyGoalRequest request) - throws Exception { - return mockMvc.perform( - patch("/api/trips/{tripId}/daily-goals/{dailyGoalId}", tripId, dailyGoalId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - } - - @Test - @DisplayName("유효한 요청으로 특정 데일리 목표에 속한 데일리 미션을 수정한다") - void shouldUpdateDailyGoal() throws Exception { - // given - Mission addMission = missionTestHelper.saveMission(stamp); - UpdateDailyGoalRequest request = - fixture.withDeleteDailyMissionIds(List.of(dailyMission.getId())) - .withAddMissionIds(List.of(addMission.getId())) - .build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions.andExpect(status().isOk()).andExpect(jsonPath("$.success").value(true)); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 Unauthorized를 반환한다") - void shouldReturnUnauthorizedWhenUnauthenticated() throws Exception { - // given - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions("", trip.getId(), dailyGoal.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception { - // given - String tripId = "abc"; - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, tripId, dailyGoal.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenDailyGoalIdTypeMismatch() throws Exception { - // given - String dailyGoalId = "abc"; - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoalId, 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 NotFound를 반환한다") - void shouldReturnNotFoundWhenInvalidTripId() throws Exception { - // given - Long tripId = 10000L; - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, tripId, dailyGoal.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 Forbidden을 반환한다") - void shouldReturnForbiddenWhenNotTripOwner() throws Exception { - // given - Member newMember = memberTestHelper.saveMember("test@gmail.com", "TEST"); - Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, newTrip.getId(), dailyGoal.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAlreadyTrip() throws Exception { - // given - Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, deleted.getId(), dailyGoal.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 NotFound를 반환한다") - void shouldReturnNotFoundWhenInvalidDailyGoalId() throws Exception { - // given - Long dailyGoalId = 10000L; - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoalId, request); - - // when & then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND - .getStatus() - .value())); - } - - @Test - @DisplayName("조회된 데일리 목표가 요청한 여행에 속하지 않을 경우 403 Forbidden을 반환한다") - void shouldReturnForbiddenWhenDailyGoalNotBelongToTrip() throws Exception { - // given - Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - DailyGoal newDailyGoal = dailyGoalTestHelper.saveDailyGoal(newTrip); - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), newDailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_NOT_BELONG_TO_TRIP - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 데일리 목표일 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAlreadyDailyGoal() throws Exception { - // given - DailyGoal deleted = dailyGoalTestHelper.saveDeletedDailyGoal(trip); - UpdateDailyGoalRequest request = fixture.build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), deleted.getId(), request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제를 요청한 데일리 미션 ID 개수와 조회된 데일리 미션 개수가 다르면 404 NotFound를 반환한다") - void shouldReturnNotFoundWhenAnyDeleteTargetDailyMissionDoesNotExist() throws Exception { - // given - List ids = List.of(dailyMission.getId(), 1000L); - UpdateDailyGoalRequest request = fixture.withDeleteDailyMissionIds(ids).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyMissionErrorCode.DAILY_MISSION_NOT_FOUND - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제를 요청한 데일리 미션이 요청한 데일리 목표에 속하지 않으면 403 Forbidden을 반환한다") - void shouldReturnForbiddenWhenDeleteTargetDailyMissionDoesNotBelongToDailyGoal() - throws Exception { - // given - DailyGoal newDailyGoal = dailyGoalTestHelper.saveDailyGoal(trip); - DailyMission newDailyMission = - dailyMissionTestHelper.saveDailyMission(firstMission, newDailyGoal); - UpdateDailyGoalRequest request = - fixture.withDeleteDailyMissionIds(List.of(newDailyMission.getId())).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyMissionErrorCode - .DAILY_MISSION_NOT_BELONGS_TO_DAILY_GOAL - .getStatus() - .value())); - } - - @Test - @DisplayName("데일리 미션이 이미 삭제된 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenDeleteTargetDailyMissionIsAlreadyDeleted() throws Exception { - // given - dailyMission.updateDeletedAt(); - UpdateDailyGoalRequest request = - fixture.withDeleteDailyMissionIds(List.of(dailyMission.getId())).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyMissionErrorCode.DAILY_MISSION_ALREADY_DELETED - .getStatus() - .value())); - } - - @Test - @DisplayName("추가할 미션 ID 목록으로 해당 미션을 조회하고, 하나라도 일치하지 않는 경우 404 NotFound를 반환한다") - void shouldReturnNotFoundWhenAnyAddMissionIdDoesNotExist() throws Exception { - // given - UpdateDailyGoalRequest request = - fixture.withAddMissionIds(List.of(100L, 200L, 300L)).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(MissionErrorCode.MISSION_NOT_FOUND.getStatus().value())); - } - - @Test - @DisplayName("코스형 여행에서 현재 진행중인 스탬프(완료되지 않은 가장 첫번째 스탬프)가 없을 경우 404 NotFound를 반환한다") - void shouldReturnNotFoundWhenNoIncompleteStampExists() throws Exception { - // given - Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - stampTestHelper.saveCompletedStamp(newTrip, 1); - DailyGoal newDailyGoal = dailyGoalTestHelper.saveDailyGoal(newTrip); - UpdateDailyGoalRequest request = fixture.withAddMissionIds(List.of(1L)).build(); - - // when - ResultActions resultActions = - getResultActions(token, newTrip.getId(), newDailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(StampErrorCode.STAMP_NOT_FOUND.getStatus().value())); - } - - @Test - @DisplayName("새로 추가할 미션들의 스탬프 정보를 확인해 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다") - void shouldReturnForbiddenWhenAddMissionsStampsDoesNotBelongToTrip() throws Exception { - // given - Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - Stamp newStamp = stampTestHelper.saveStamp(newTrip, 1); - Mission newMission = missionTestHelper.saveMission(newStamp); - UpdateDailyGoalRequest request = - fixture.withAddMissionIds(List.of(newMission.getId())).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP - .getStatus() - .value())); - } - - @Test - @DisplayName("새로 추가할 미션들 중 삭제된 미션이 존재하면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAnyAddMissionIsDeleted() throws Exception { - // given - Mission deleted = missionTestHelper.saveDeletedMission(stamp); - UpdateDailyGoalRequest request = - fixture.withAddMissionIds(List.of(deleted.getId())).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - MissionErrorCode.MISSION_ALREADY_DELETED - .getStatus() - .value())); - } - - @Test - @DisplayName("새로 추가할 미션들 중 완료된 미션이 존재하면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAnyAddMissionIsAlreadyCompleted() throws Exception { - // given - Mission completed = missionTestHelper.saveCompletedMission(stamp); - UpdateDailyGoalRequest request = - fixture.withAddMissionIds(List.of(completed.getId())).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - MissionErrorCode.MISSION_ALREADY_COMPLETED - .getStatus() - .value())); - } - - @Test - @DisplayName("코스형 여행에서 새로 추가한 미션들이 현재 진행중인 스탬프에 속하지 않은 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAddMissionsDoNotBelongToCurrentStamp() throws Exception { - // given - Stamp newStamp = stampTestHelper.saveStamp(trip, 2); - Mission newMission = missionTestHelper.saveMission(newStamp); - UpdateDailyGoalRequest request = - fixture.withAddMissionIds(List.of(newMission.getId())).build(); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), dailyGoal.getId(), request); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - MissionErrorCode.MISSION_NOT_BELONGS_TO_STAMP - .getStatus() - .value())); - } - } - - @Nested - @DisplayName("데일리 목표 삭제 API") - class DeleteDailyGoal { - - private ResultActions getResultActions(String token, Object tripId, Object dailyGoalId) - throws Exception { - return mockMvc.perform( - delete("/api/trips/{tripId}/daily-goals/{dailyGoalId}", tripId, dailyGoalId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token)); - } - - @Test - @DisplayName("유효한 요청으로 데일리 목표와 뽀모도로, 데일리 미션을 삭제한다") - void shouldDeleteDailyGoalAndPomodoroAndDailyMissions() throws Exception { - // given - pomodoroTestHelper.savePomodoro(dailyGoal); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoal.getId()); - - // then - resultActions.andExpect(status().isOk()).andExpect(jsonPath("$.success").value(true)); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 Unauthorized를 반환한다") - void shouldReturnUnauthorizedWhenUnauthenticated() throws Exception { - // when - ResultActions resultActions = getResultActions("", trip.getId(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception { - // given - String tripId = "abc"; - - // when - ResultActions resultActions = getResultActions(token, tripId, dailyGoal.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenDailyGoalIdTypeMismatch() throws Exception { - // given - String dailyGoalId = "abc"; - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoalId); - - // 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 NotFound를 반환한다") - void shouldReturnNotFoundWhenInvalidTripId() throws Exception { - // given - Long tripId = 10000L; - - // when - ResultActions resultActions = getResultActions(token, tripId, dailyGoal.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 Forbidden을 반환한다") - void shouldReturnForbiddenWhenNotTripOwner() 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(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); - } - - @Test - @DisplayName("삭제된 여행일 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAlreadyTrip() throws Exception { - // given - Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); - - // when - ResultActions resultActions = - getResultActions(token, deleted.getId(), dailyGoal.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 NotFound를 반환한다") - void shouldReturnNotFoundWhenInvalidDailyGoalId() throws Exception { - // given - Long dailyGoalId = 10000L; - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoalId); - - // when & then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND - .getStatus() - .value())); - } - - @Test - @DisplayName("조회된 데일리 목표가 요청한 여행에 속하지 않을 경우 403 Forbidden을 반환한다") - void shouldReturnForbiddenWhenDailyGoalNotBelongToTrip() throws Exception { - // given - Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - DailyGoal newDailyGoal = dailyGoalTestHelper.saveDailyGoal(newTrip); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), newDailyGoal.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_NOT_BELONG_TO_TRIP - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 데일리 목표일 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAlreadyDailyGoal() throws Exception { - // given - DailyGoal deleted = dailyGoalTestHelper.saveDeletedDailyGoal(trip); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), deleted.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED - .getStatus() - .value())); - } - - @Test - @DisplayName("데일리 목표 ID로 뽀모도로를 조회하고 존재하지 않을 경우 404 NotFound를 반환한다") - void shouldReturnNotFoundWhenPomodoroDoesNotExist() throws Exception { - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - PomodoroErrorCode.POMODORO_NOT_FOUND - .getStatus() - .value())); - } - - @Test - @DisplayName("데일리 목표 ID로 뽀모도로를 조회하고 이미 삭제된 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenPomodoroIsAlreadyDeleted() throws Exception { - // given - pomodoroTestHelper.saveDeletedPomodoro(dailyGoal); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - PomodoroErrorCode.POMODORO_ALREADY_DELETED - .getStatus() - .value())); - } - } - - @Nested - @DisplayName("데일리 목표 조회 API") - class GetDailyGoal { - - private ResultActions getResultActions(String token, Object tripId, Object dailyGoalId) - throws Exception { - return mockMvc.perform( - get("/api/trips/{tripId}/daily-goals/{dailyGoalId}", tripId, dailyGoalId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token)); - } - - @Test - @DisplayName("유효한 정보로 특정 데일리 목표를 조회하고 해당 뽀모도로와 데일리 미션을 함께 반환한다") - void shouldReturnDailyGoal() throws Exception { - // given - Pomodoro pomodoro = pomodoroTestHelper.savePomodoro(dailyGoal); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.dailyGoalId").value(dailyGoal.getId())) - .andExpect(jsonPath("$.data.pomodoro.pomodoroId").value(pomodoro.getId())) - .andExpect(jsonPath("$.data.dailyMissions").isNotEmpty()); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 Unauthorized를 반환한다") - void shouldReturnUnauthorizedWhenUnauthenticated() throws Exception { - // when - ResultActions resultActions = getResultActions("", trip.getId(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception { - // given - String tripId = "abc"; - - // when - ResultActions resultActions = getResultActions(token, tripId, dailyGoal.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 Bad Request를 반환한다") - void shouldReturnBadRequestWhenDailyGoalIdTypeMismatch() throws Exception { - // given - String dailyGoalId = "abc"; - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoalId); - - // 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 NotFound를 반환한다") - void shouldReturnNotFoundWhenInvalidTripId() throws Exception { - // given - Long tripId = 10000L; - - // when - ResultActions resultActions = getResultActions(token, tripId, dailyGoal.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 Forbidden을 반환한다") - void shouldReturnForbiddenWhenNotTripOwner() 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(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); - } - - @Test - @DisplayName("삭제된 여행일 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAlreadyTrip() throws Exception { - // given - Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); - - // when - ResultActions resultActions = - getResultActions(token, deleted.getId(), dailyGoal.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 NotFound를 반환한다") - void shouldReturnNotFoundWhenInvalidDailyGoalId() throws Exception { - // given - Long dailyGoalId = 10000L; - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoalId); - - // when & then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND - .getStatus() - .value())); - } - - @Test - @DisplayName("조회된 데일리 목표가 요청한 여행에 속하지 않을 경우 403 Forbidden을 반환한다") - void shouldReturnForbiddenWhenDailyGoalNotBelongToTrip() throws Exception { - // given - Trip newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - DailyGoal newDailyGoal = dailyGoalTestHelper.saveDailyGoal(newTrip); - - // when - ResultActions resultActions = - getResultActions(token, trip.getId(), newDailyGoal.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_NOT_BELONG_TO_TRIP - .getStatus() - .value())); - } - - @Test - @DisplayName("삭제된 데일리 목표일 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenAlreadyDailyGoal() throws Exception { - // given - DailyGoal deleted = dailyGoalTestHelper.saveDeletedDailyGoal(trip); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), deleted.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED - .getStatus() - .value())); - } - - @Test - @DisplayName("데일리 목표 ID로 뽀모도로를 조회하고 존재하지 않을 경우 404 NotFound를 반환한다") - void shouldReturnNotFoundWhenPomodoroDoesNotExist() throws Exception { - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - PomodoroErrorCode.POMODORO_NOT_FOUND - .getStatus() - .value())); - } - - @Test - @DisplayName("데일리 목표 ID로 뽀모도로를 조회하고 이미 삭제된 경우 400 Bad Request를 반환한다") - void shouldReturnBadRequestWhenPomodoroIsAlreadyDeleted() throws Exception { - // given - pomodoroTestHelper.saveDeletedPomodoro(dailyGoal); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), dailyGoal.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - PomodoroErrorCode.POMODORO_ALREADY_DELETED - .getStatus() - .value())); - } - } -} diff --git a/src/test/java/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.java b/src/test/java/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.java deleted file mode 100644 index 1b95087..0000000 --- a/src/test/java/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.java +++ /dev/null @@ -1,772 +0,0 @@ -package com.ject.studytrip.trip.presentation.controller; - -import static com.ject.studytrip.auth.fixture.TokenFixture.TOKEN_PREFIX; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -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.common.response.StandardResponse; -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.helper.StampTestHelper; -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.fixture.CreateTripRequestFixture; -import com.ject.studytrip.trip.fixture.UpdateTripRequestFixture; -import com.ject.studytrip.trip.helper.TripTestHelper; -import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest; -import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest; -import com.ject.studytrip.trip.presentation.dto.response.LoadTripCategoryResponse; -import com.ject.studytrip.trip.presentation.dto.response.LoadTripDetailResponse; -import com.ject.studytrip.trip.presentation.dto.response.LoadTripsSliceResponse; -import java.time.LocalDate; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.ResultActions; - -@DisplayName("TripController 통합 테스트") -public class TripControllerIntegrationTest extends BaseIntegrationTest { - private static final String TRIP_CATEGORY_COURSE = TripCategory.COURSE.name(); - private static final String TRIP_CATEGORY_EXPLORE = TripCategory.EXPLORE.name(); - - @Autowired private TokenTestHelper tokenTestHelper; - @Autowired private MemberTestHelper memberTestHelper; - @Autowired private TripTestHelper tripTestHelper; - @Autowired private StampTestHelper stampTestHelper; - - private Member member; - private Trip trip; - private String token; - private String newToken; - private Stamp stamp1; - private Stamp stamp2; - - @BeforeEach - void setup() { - member = memberTestHelper.saveMember(); - token = - tokenTestHelper.createAccessToken( - member.getId().toString(), member.getRole().name()); - trip = tripTestHelper.saveTrip(member, TripCategory.COURSE); - stamp1 = stampTestHelper.saveStamp(trip, 1); - stamp2 = stampTestHelper.saveStamp(trip, 2); - - Member newMember = memberTestHelper.saveMember("test@kakao.com", "TEST NICKNAME"); - newToken = - tokenTestHelper.createAccessToken( - newMember.getId().toString(), newMember.getRole().name()); - } - - @Nested - @DisplayName("여행 카테고리 목록 조회 API") - class GetTripCategory { - - private ResultActions getResultActions() throws Exception { - return mockMvc.perform(get("/api/trips/categories")); - } - - @Test - @DisplayName("여행 카테고리 종류를 조회한다") - void shouldGetTripCategories() throws Exception { - // when - ResultActions resultActions = getResultActions(); - - // then - resultActions.andExpect(status().isOk()); - - StandardResponse response = parseResponse(resultActions, StandardResponse.class); - Object rawData = response.data(); - List categoryResponses = - objectMapper.convertValue(rawData, List.class); - - assertThat(categoryResponses.isEmpty()).isFalse(); - assertThat(categoryResponses.size()).isEqualTo(2); - } - } - - @Nested - @DisplayName("여행 생성 API") - class CreateTrip { - - private ResultActions getResultActions(String token, CreateTripRequest request) - throws Exception { - return mockMvc.perform( - post("/api/trips") - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - } - - @Test - @DisplayName("유효한 요청으로 여행 정보를 생성한다") - void shouldCreateTrip() throws Exception { - // given - CreateTripRequest request = new CreateTripRequestFixture().build(); - - // when - ResultActions resultActions = getResultActions(token, request); - - // then - resultActions.andExpect(status().isCreated()); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") - void shouldThrowExceptionWhenUnauthenticated() throws Exception { - // given - CreateTripRequest request = new CreateTripRequestFixture().build(); - - // when - ResultActions resultActions = getResultActions("", request); - - // then - resultActions.andExpect(status().is(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("여행을 생성하는데 필요한 필수 요청 값이 누락되거나 유효하지 않으면 400 예외가 발생한다") - void shouldThrowExceptionWhenInvalidRequiredFields() throws Exception { - // given - CreateTripRequest nonTripNameRequest = - new CreateTripRequestFixture() - .withName("") - .withCategory("test") - .withEndDate(null) - .build(); - - // when - ResultActions resultActions = getResultActions(token, nonTripNameRequest); - - // then - resultActions.andExpect( - status().is(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getStatus().value())); - } - - @Test - @DisplayName("여행 종료일이 과거일 경우 400 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsInThePast() throws Exception { - // given - CreateTripRequest invalidDateRequest = - new CreateTripRequestFixture() - .withEndDate(LocalDate.now().minusDays(10)) - .build(); - - // when - ResultActions resultActions = getResultActions(token, invalidDateRequest); - - // then - resultActions.andExpect( - status().is(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getStatus().value())); - } - - @Test - @DisplayName("여행의 카테고리가 코스형이고 종료일이 존재하지 않으면 400 예외가 발생한다") - void shouldThrowExceptionWhenCourseTripHasNoEndDate() throws Exception { - // given - CreateTripRequest request = - new CreateTripRequestFixture() - .withCategory(TRIP_CATEGORY_COURSE) - .withEndDate(null) - .build(); - - // when - ResultActions resultActions = getResultActions(token, request); - - // then - resultActions.andExpect( - status().is(TripErrorCode.COURSE_TRIP_END_DATE_REQUIRED.getStatus().value())); - } - - @Test - @DisplayName("함께 요청한 여행의 스탬프 목록이 없으면 400 예외가 발생한다") - void shouldThrowExceptionWhenRequestStampsIsEmpty() throws Exception { - // given - CreateTripRequest request = - new CreateTripRequestFixture().withStamps(List.of()).build(); - - // when - ResultActions resultActions = getResultActions(token, request); - - // when & then - resultActions.andExpect( - status().is(TripErrorCode.TRIP_STAMP_REQUIRED.getStatus().value())); - } - } - - @Nested - @DisplayName("여행 수정 API") - class UpdateTrip { - - private ResultActions getResultActions( - String token, Object tripId, UpdateTripRequest request) throws Exception { - return mockMvc.perform( - patch("/api/trips/{tripId}", tripId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))); - } - - @Test - @DisplayName("특정 여행 정보를 수정한다") - void shouldUpdateTrip() throws Exception { - // given - UpdateTripRequest request = new UpdateTripRequestFixture().withName("여행 이름 수정").build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // then - resultActions.andExpect(status().isOk()); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") - void shouldThrowExceptionWhenUnauthenticated() throws Exception { - // given - UpdateTripRequest request = new UpdateTripRequestFixture().build(); - - // when - ResultActions resultActions = getResultActions("", trip.getId(), request); - - // then - resultActions.andExpect(status().is(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 예외가 발생한다") - void shouldThrowExceptionWhenTripIdTypeMismatch() throws Exception { - // given - String tripId = "abc"; - UpdateTripRequest request = - new UpdateTripRequestFixture() - .withEndDate(LocalDate.now().minusDays(7)) - .build(); - - // when - ResultActions resultActions = getResultActions(token, tripId, request); - - // then - resultActions.andExpect( - status().is(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.getStatus().value())); - } - - @Test - @DisplayName("수정 요청한 여행 종료일이 과거일 경우 400 예외가 발생한다") - void shouldThrowExceptionWhenEndDateIsInThePast() throws Exception { - // given - UpdateTripRequest request = - new UpdateTripRequestFixture() - .withEndDate(LocalDate.now().minusDays(7)) - .build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // then - resultActions.andExpect( - status().is(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getStatus().value())); - } - - @Test - @DisplayName("유효하지 않은 여행 ID 라면 404 예외가 발생한다") - void shouldThrowExceptionWhenInvalidTripId() throws Exception { - // given - Long tripId = 10000L; - UpdateTripRequest request = new UpdateTripRequestFixture().build(); - - // when - ResultActions resultActions = getResultActions(token, tripId, request); - - // when & then - resultActions.andExpect(status().is(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); - } - - @Test - @DisplayName("수정할 권한이 없으면 403 예외가 발생한다") - void shouldThrowExceptionWhenUpdatingTripWithoutPermission() throws Exception { - // given - Member newMember = memberTestHelper.saveMember("test@gmail.com", "test"); - Trip newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE); - UpdateTripRequest request = new UpdateTripRequestFixture().build(); - - // when - ResultActions resultActions = getResultActions(token, newTrip.getId(), request); - - // then - resultActions.andExpect(status().is(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); - } - - @Test - @DisplayName("수정 요청한 여행 정보가 이미 삭제된 여행이라면 400 예외가 발생한다") - void shouldThrowExceptionWhenAlreadyDeletedTrip() throws Exception { - // given - Trip deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); - UpdateTripRequest request = new UpdateTripRequestFixture().build(); - - // when - ResultActions resultActions = getResultActions(token, deletedTrip.getId(), request); - - // then - resultActions.andExpect( - status().is(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())); - } - - @Test - @DisplayName("종료 날짜가 시작 날짜보다 이전인 경우 400 예외가 발생한다") - void shouldThrowExceptionWhenUpdatingTripWithInvalidEndDate() throws Exception { - // given - UpdateTripRequest request = - new UpdateTripRequestFixture() - .withEndDate(trip.getStartDate().minusDays(1)) - .build(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId(), request); - - // when & then - resultActions.andExpect( - status().is(TripErrorCode.TRIP_END_DATE_BEFORE_START_DATE.getStatus().value())); - } - } - - @Nested - @DisplayName("여행 삭제 API") - class DeleteTrip { - - private ResultActions getResultActions(String token, Object tripId) throws Exception { - return mockMvc.perform( - delete("/api/trips/{tripId}", tripId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token)); - } - - @Test - @DisplayName("특정 여행 정보를 삭제한다") - void shouldDeleteTrip() throws Exception { - // when - ResultActions resultActions = getResultActions(token, trip.getId()); - - // then - resultActions.andExpect(status().isOk()); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") - void shouldThrowExceptionWhenUnauthenticated() throws Exception { - // when - ResultActions resultActions = getResultActions("", trip.getId()); - - // then - resultActions.andExpect(status().is(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().is(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().is(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())); - } - - @Test - @DisplayName("여행을 삭제할 권한이 없으면 403 예외가 발생한다") - void shouldThrowExceptionWhenNoPermission() 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()); - - // when & then - resultActions.andExpect(status().is(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); - } - } - - @Nested - @DisplayName("여행 상세 조회 API") - class GetTrip { - private ResultActions getResultActions(String token, Object tripId) throws Exception { - return mockMvc.perform( - get("/api/trips/{tripId}", tripId) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token)); - } - - @Test - @DisplayName("여행 ID로 특정 여행을 상세 조회한다") - void shouldLoadTripByTripId() throws Exception { - // When - ResultActions resultActions = getResultActions(token, trip.getId()); - - // Then - resultActions.andExpect(status().isOk()); - - StandardResponse response = parseResponse(resultActions, StandardResponse.class); - Object rawData = response.data(); - LoadTripDetailResponse detailResponse = - objectMapper.convertValue(rawData, LoadTripDetailResponse.class); - - assertThat(detailResponse.tripId()).isEqualTo(trip.getId()); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") - void shouldThrowExceptionWhenUnauthenticated() throws Exception { - // When - ResultActions resultActions = getResultActions("", trip.getId()); - - // Then - resultActions.andExpect(status().is(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("PathVariable 여행 ID 값의 타입이 올바르지 않으면 400 예외가 발생한다") - void shouldThrowExceptionWhenTripIdTypeMissMatch() throws Exception { - // given - String tripId = "abc"; - - // when - ResultActions resultActions = getResultActions(token, tripId); - - // then - resultActions.andExpect( - status().is(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().is(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().is(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())); - } - - @Test - @DisplayName("이미 삭제된 여행일 경우 400 예외가 발생한다") - void shouldThrowExceptionWhenAlreadyDeleted() throws Exception { - // given - Trip deleted = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE); - - // when - ResultActions resultActions = getResultActions(token, deleted.getId()); - - // when & then - resultActions.andExpect( - status().is(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())); - } - } - - @Nested - @DisplayName("여행 목록 조회 API") - class ListTrips { - private ResultActions getResultActions(String token, String page, String size) - throws Exception { - return mockMvc.perform( - get("/api/trips") - .param("page", page) - .param("size", size) - .header(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + token)); - } - - @Test - @DisplayName("로그인한 사용자의 여행 목록을 조회하고 슬라이스 처리한다") - void shouldLoadTripsWithSlicePaging() throws Exception { - // Given - String page = "0"; - String size = "5"; - - // When - ResultActions resultActions = getResultActions(token, page, size); - - // Then - resultActions.andExpect(status().isOk()); - - StandardResponse response = parseResponse(resultActions, StandardResponse.class); - Object rawData = response.data(); - LoadTripsSliceResponse sliceResponse = - objectMapper.convertValue(rawData, LoadTripsSliceResponse.class); - - assertThat(sliceResponse.tripInfos().size()).isEqualTo(1); - assertThat(sliceResponse.hasNext()).isFalse(); - } - - @Test - @DisplayName("인증되지 않은 사용자일 경우 401 예외가 발생한다") - void shouldThrowExceptionWhenUnauthenticated() throws Exception { - // Given - String page = "0"; - String size = "5"; - - // when - ResultActions resultActions = getResultActions("", page, size); - - // Then - resultActions.andExpect(status().is(AuthErrorCode.UNAUTHENTICATED.getStatus().value())); - } - - @Test - @DisplayName("페이징 파라미터 타입이 올바르지 않으면 400 예외가 발생한다") - void shouldThrowExceptionWhenPagingParameterTypeMismatch() throws Exception { - // Given - String page = "test"; - String size = "test"; - - // when - ResultActions resultActions = getResultActions(token, page, size); - - // Then - resultActions.andExpect( - status().is(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.getStatus().value())); - } - - @Test - @DisplayName("페이징 파라미터가 유효하지 않으면 400 예외가 발생한다") - void shouldThrowExceptionWhenPagingParameterIsInvalid() throws Exception { - // Given - String page = "-1"; - String size = "100"; - - // when - ResultActions resultActions = getResultActions(token, page, size); - - // Then - resultActions.andExpect( - status().is(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getStatus().value())); - } - } - - @Nested - @DisplayName("여행 완료 API") - class CompleteTrip { - private ResultActions getResultActions(String token, Object tripId) throws Exception { - return mockMvc.perform( - patch("/api/trips/{tripId}/complete", tripId) - .header( - org.apache.http.HttpHeaders.AUTHORIZATION, - TokenFixture.TOKEN_PREFIX + token)); - } - - @Test - @DisplayName("Access Token이 없으면 401 Unauthorized를 반환한다.") - void shouldReturnUnauthorizedWhenAccessTokenIsMissing() throws Exception { - // when - ResultActions resultActions = getResultActions("", trip.getId()); - - // then - resultActions - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(AuthErrorCode.UNAUTHENTICATED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(AuthErrorCode.UNAUTHENTICATED.getMessage())); - } - - @Test - @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception { - // given - String invalidTripId = "abc"; - - // when - ResultActions resultActions = getResultActions(token, invalidTripId); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value( - CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH - .getMessage())); - } - - @Test - @DisplayName("삭제된 여행일 경우 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripAlreadyDeleted() throws Exception { - // given - trip.updateDeletedAt(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.TRIP_ALREADY_DELETED.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_ALREADY_DELETED.getMessage())); - } - - @Test - @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") - void shouldReturnForbiddenWhenNotTripOwner() throws Exception { - // when - ResultActions resultActions = getResultActions(newToken, trip.getId()); - - // then - resultActions - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.NOT_TRIP_OWNER.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.NOT_TRIP_OWNER.getMessage())); - } - - @Test - @DisplayName("유효하지 않은 여행 ID가 들어오면 404 Not Found를 반환한다.") - void shouldReturnNotFoundWhenTripIdIsInvalid() throws Exception { - // given - Long invalidTripId = 10000L; - - // when - ResultActions resultActions = getResultActions(token, invalidTripId); - - // when & then - resultActions - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value(TripErrorCode.TRIP_NOT_FOUND.getStatus().value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_NOT_FOUND.getMessage())); - } - - @Test - @DisplayName("특정 여행 하위의 스탬프가 하나라도 완료되지 않았다면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenAnyStampIsNotCompleted() throws Exception { - // given - - // when - ResultActions resultActions = getResultActions(token, trip.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - StampErrorCode.ALL_STAMPS_NOT_COMPLETED - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(StampErrorCode.ALL_STAMPS_NOT_COMPLETED.getMessage())); - } - - @Test - @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") - void shouldReturnBadRequestWhenTripAlreadyCompleted() throws Exception { - // given - stamp1.updateCompleted(); - stamp2.updateCompleted(); - trip.updateCompleted(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId()); - - // then - resultActions - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect( - jsonPath("$.status") - .value( - TripErrorCode.TRIP_ALREADY_COMPLETED - .getStatus() - .value())) - .andExpect( - jsonPath("$.data.message") - .value(TripErrorCode.TRIP_ALREADY_COMPLETED.getMessage())); - } - - @Test - @DisplayName("특정 여행 하위의 모든 스탬프가 완료되었다면 여행을 완료합니다.") - void shouldCompleteTrip() throws Exception { - // given - stamp1.updateCompleted(); - stamp2.updateCompleted(); - - // when - ResultActions resultActions = getResultActions(token, trip.getId()); - - // then - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())); - } - } -} diff --git a/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionCommandServiceTest.kt index 8c7d79e..df8dc70 100644 --- a/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionCommandServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionCommandServiceTest.kt @@ -43,10 +43,10 @@ class DailyMissionCommandServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { member = MemberFixture.createMemberFromKakaoWithId(1L) - val trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - val stamp = StampFixture.createStampWithId(1L, trip, 1) + val trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + val stamp = StampFixture(trip, 1).createWithId(1L) mission = MissionFixture(stamp).createWithId(1L) - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, trip) + dailyGoal = DailyGoalFixture(trip).createWithId(1L) dailyMission = DailyMissionFixture(mission, dailyGoal).createWithId(1L) } diff --git a/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryServiceTest.kt index 91c52f5..12d16e9 100644 --- a/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/mission/application/service/DailyMissionQueryServiceTest.kt @@ -43,10 +43,10 @@ class DailyMissionQueryServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { val member = MemberFixture.createMemberFromKakaoWithId(1L) - trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - val stamp = StampFixture.createStampWithId(1L, trip, 1) + trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + val stamp = StampFixture(trip, 1).createWithId(1L) val mission = MissionFixture(stamp).createWithId(1L) - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, trip) + dailyGoal = DailyGoalFixture(trip).createWithId(1L) dailyMission = DailyMissionFixture(mission, dailyGoal).createWithId(1L) } @@ -72,7 +72,7 @@ class DailyMissionQueryServiceTest : BaseUnitTest() { @DisplayName("특정 데일리 목표에 속하지 않은 데일리 미션이 하나라도 존재하면 예외가 발생한다.") fun shouldThrowExceptionWhenDailyMissionsNotBelongToDailyGoal() { // given - val newDailyGoal = DailyGoalFixture.createDailyGoalWithId(2L, trip) + val newDailyGoal = DailyGoalFixture(trip).createWithId(2L) val dailyMissionIds = listOf(dailyMission.id) given(dailyMissionRepository.findAllByIdIn(dailyMissionIds)).willReturn(listOf(dailyMission)) @@ -143,7 +143,7 @@ class DailyMissionQueryServiceTest : BaseUnitTest() { @DisplayName("특정 데일리 목표에 속하지 않은 데일리 미션이 하나라도 존재하면 예외가 발생한다.") fun shouldThrowExceptionWhenDailyMissionsNotBelongToDailyGoal() { // given - val newDailyGoal = DailyGoalFixture.createDailyGoalWithId(2L, trip) + val newDailyGoal = DailyGoalFixture(trip).createWithId(2L) val dailyMissionIds = listOf(dailyMission.id) given(dailyMissionQueryRepository.findAllWithMissionAndStampByIds(dailyMissionIds)).willReturn(listOf(dailyMission)) diff --git a/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionCommandServiceTest.kt index 7bce540..e231089 100644 --- a/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionCommandServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionCommandServiceTest.kt @@ -2,6 +2,7 @@ package com.ject.studytrip.mission.application.service import com.ject.studytrip.BaseUnitTest import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.member.domain.model.Member import com.ject.studytrip.member.fixture.MemberFixture import com.ject.studytrip.mission.domain.error.MissionErrorCode import com.ject.studytrip.mission.domain.model.Mission @@ -37,6 +38,7 @@ class MissionCommandServiceTest : BaseUnitTest() { @Mock private lateinit var missionCommandRepository: MissionCommandRepository + private lateinit var member: Member private lateinit var courseStamp: Stamp private lateinit var exploreStamp: Stamp private lateinit var courseMission: Mission @@ -49,11 +51,11 @@ class MissionCommandServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { - val member = MemberFixture.createMemberFromKakaoWithId(1L) - val courseTrip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - val exploreTrip = TripFixture.createTripWithId(2L, member, TripCategory.EXPLORE) - courseStamp = StampFixture.createStampWithId(1L, courseTrip, 1) - exploreStamp = StampFixture.createStampWithId(2L, exploreTrip, 0) + member = MemberFixture.createMemberFromKakaoWithId(1L) + val courseTrip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + val exploreTrip = TripFixture(member, TripCategory.EXPLORE).createWithId(2L) + courseStamp = StampFixture(courseTrip, 1).createWithId(1L) + exploreStamp = StampFixture(exploreTrip, 0).createWithId(2L) courseMission = MissionFixture(courseStamp).createWithId(1L) exploreMission1 = MissionFixture(exploreStamp).createWithId(2L) exploreMission2 = MissionFixture(exploreStamp).createWithId(3L) @@ -104,8 +106,8 @@ class MissionCommandServiceTest : BaseUnitTest() { @DisplayName("특정 미션의 이름을 수정한다.") fun shouldUpdateMissionWhenNameIsPresent() { // given - val existingName = courseMission.name val request = fixture.withName(NEW_MISSION_NAME).build() + val existingName = courseMission.name // when missionCommandService.updateMissionNameIfPresent(courseMission, request) @@ -166,13 +168,13 @@ class MissionCommandServiceTest : BaseUnitTest() { missionCommandService.completeMission(exploreMission1) // then - assertThat(exploreMission1.isCompleted).isTrue() + assertThat(exploreMission1.isCompleted).isTrue } } @Nested - @DisplayName("validateMissionsBelongsToStamp 메서드는") - inner class ValidateMissionsBelongsToStamp { + @DisplayName("validateMissionsBelongToStamp 메서드는") + inner class ValidateMissionsBelongToStamp { @Test @DisplayName("특정 스탬프에 속하지 않은 미션이 하나라도 존재하면 예외가 발생한다.") fun shouldThrowExceptionWhenMissionsNotBelongToStamp() { @@ -181,7 +183,7 @@ class MissionCommandServiceTest : BaseUnitTest() { // when val exception = - assertThrows { missionCommandService.validateMissionsBelongsToStamp(exploreStamp.id, missions) } + assertThrows { missionCommandService.validateMissionsBelongToStamp(exploreStamp.id, missions) } // then assertThat(exception.message).isEqualTo(MissionErrorCode.MISSION_NOT_BELONGS_TO_STAMP.message) @@ -194,7 +196,7 @@ class MissionCommandServiceTest : BaseUnitTest() { val missions = listOf(exploreMission1, exploreMission2) // when & then - assertDoesNotThrow { missionCommandService.validateMissionsBelongsToStamp(exploreStamp.id, missions) } + assertDoesNotThrow { missionCommandService.validateMissionsBelongToStamp(exploreStamp.id, missions) } } } @@ -294,7 +296,7 @@ class MissionCommandServiceTest : BaseUnitTest() { @DisplayName("특정 멤버가 소유한 미션이 하나라도 존재하지 않으면 0을 반환한다.") fun shouldReturnZeroWhenMissionsOwnedByMemberDoNotExist() { // given - val memberId = -1L + val memberId = member.id given(missionCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L) // when @@ -308,7 +310,7 @@ class MissionCommandServiceTest : BaseUnitTest() { @DisplayName("특정 멤버가 소유한 미션이 하나라도 존재하면 해당 개수를 반환한다.") fun shouldReturnCountWhenMissionsOwnedByMemberExist() { // given - val memberId = -1L + val memberId = member.id given(missionCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L) // when diff --git a/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionQueryServiceTest.kt index 5c88bdd..6479337 100644 --- a/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionQueryServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/mission/application/service/MissionQueryServiceTest.kt @@ -43,10 +43,10 @@ class MissionQueryServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { val member = MemberFixture.createMemberFromKakao() - val courseTrip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - val exploreTrip = TripFixture.createTripWithId(2L, member, TripCategory.EXPLORE) - courseStamp = StampFixture.createStampWithId(1L, courseTrip, 1) - exploreStamp = StampFixture.createStampWithId(2L, exploreTrip, 0) + val courseTrip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + val exploreTrip = TripFixture(member, TripCategory.EXPLORE).createWithId(2L) + courseStamp = StampFixture(courseTrip, 1).createWithId(1L) + exploreStamp = StampFixture(exploreTrip, 0).createWithId(2L) courseMission = MissionFixture(courseStamp).createWithId(1L) exploreMission1 = MissionFixture(exploreStamp).createWithId(2L) exploreMission2 = MissionFixture(exploreStamp).createWithId(3L) diff --git a/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroCommandServiceTest.kt index eb448c7..6813e1c 100644 --- a/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroCommandServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroCommandServiceTest.kt @@ -43,8 +43,8 @@ class PomodoroCommandServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { member = MemberFixture.createMemberFromKakaoWithId(1L) - val trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, trip) + val trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + dailyGoal = DailyGoalFixture(trip).createWithId(1L) pomodoro = PomodoroFixture(dailyGoal).createWithId(1L) } diff --git a/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroQueryServiceTest.kt index bee498a..1f69e7a 100644 --- a/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroQueryServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/pomodoro/application/service/PomodoroQueryServiceTest.kt @@ -42,8 +42,8 @@ class PomodoroQueryServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { val member = MemberFixture.createMemberFromKakaoWithId(1L) - trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, trip) + trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + dailyGoal = DailyGoalFixture(trip).createWithId(1L) pomodoro = PomodoroFixture(dailyGoal).createWithId(1L) } diff --git a/src/test/kotlin/com/ject/studytrip/pomodoro/fixture/CreatePomodoroRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/pomodoro/fixture/CreatePomodoroRequestFixture.kt index 69504be..607c80a 100644 --- a/src/test/kotlin/com/ject/studytrip/pomodoro/fixture/CreatePomodoroRequestFixture.kt +++ b/src/test/kotlin/com/ject/studytrip/pomodoro/fixture/CreatePomodoroRequestFixture.kt @@ -6,5 +6,10 @@ class CreatePomodoroRequestFixture { var focusDurationInMinute: Int = 25 var focusSessionCount: Int = 4 + fun withFocusDurationInMinute(focusDurationInMinute: Int): CreatePomodoroRequestFixture = + apply { this.focusDurationInMinute = focusDurationInMinute } + + fun withFocusSessionCount(focusSessionCount: Int): CreatePomodoroRequestFixture = apply { this.focusSessionCount = focusSessionCount } + fun build(): CreatePomodoroRequest = CreatePomodoroRequest(focusDurationInMinute, focusSessionCount) } diff --git a/src/test/kotlin/com/ject/studytrip/stamp/application/service/StampCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/stamp/application/service/StampCommandServiceTest.kt new file mode 100644 index 0000000..53701c1 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/application/service/StampCommandServiceTest.kt @@ -0,0 +1,569 @@ +package com.ject.studytrip.stamp.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.stamp.domain.error.StampErrorCode +import com.ject.studytrip.stamp.domain.model.Stamp +import com.ject.studytrip.stamp.domain.repository.StampCommandRepository +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.UpdateStampOrderRequestFixture +import com.ject.studytrip.stamp.fixture.UpdateStampRequestFixture +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.fixture.TripFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.mockito.ArgumentMatchers.anyList +import org.mockito.BDDMockito.given +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import java.time.LocalDate + +@DisplayName("StampCommandService 단위 테스트") +class StampCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var stampCommandService: StampCommandService + + @Mock + private lateinit var stampRepository: StampRepository + + @Mock + private lateinit var stampCommandRepository: StampCommandRepository + + private lateinit var member: Member + private lateinit var courseTrip: Trip + private lateinit var exploreTrip: Trip + private lateinit var courseStamp1: Stamp + private lateinit var courseStamp2: Stamp + private lateinit var exploreStamp1: Stamp + private lateinit var exploreStamp2: Stamp + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + courseTrip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + exploreTrip = TripFixture(member, TripCategory.EXPLORE).createWithId(2L) + courseStamp1 = StampFixture(courseTrip, 1).createWithId(1L) + courseStamp2 = StampFixture(courseTrip, 2).createWithId(2L) + exploreStamp1 = StampFixture(exploreTrip, 0).createWithId(3L) + exploreStamp2 = StampFixture(exploreTrip, 0).createWithId(4L) + } + + @Nested + @DisplayName("createStamp 메서드는") + inner class CreateStamp { + private val fixture = CreateStampRequestFixture() + + @Test + @DisplayName("스탬프 종료일이 여행 종료일보다 이후라면 예외가 발생한다.") + fun shouldThrowExceptionWhenEndDateIsAfterTripEndDate() { + // given + val nextOrder = courseStamp2.stampOrder + 1 + val request = fixture.withEndDateAfterTripEndDate().build() + + // when + val exception = assertThrows { stampCommandService.createStamp(courseTrip, nextOrder, request) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED.message) + } + + @Test + @DisplayName("코스형 여행에 대한 스탬프를 생성하고 반환한다.") + fun shouldCreateAndReturnStampWithNextOrderForCourseTrip() { + // given + val nextOrder = courseStamp1.stampOrder + 1 + val request = fixture.build() + given(stampRepository.save(any())).willReturn(courseStamp2) + + // when + val result = stampCommandService.createStamp(courseTrip, nextOrder, request) + + // then + assertThat(result).isEqualTo(courseStamp2) + assertThat(result.stampOrder).isEqualTo(nextOrder) + } + + @Test + @DisplayName("탐험형 여행에 대한 스탬프를 생성하고 반환한다.") + fun shouldCreateAndReturnStampWithZeroOrderForExploreTrip() { + // given + val request = fixture.withEndDate(null).build() + given(stampRepository.save(any())).willReturn(exploreStamp2) + + // when + val result = stampCommandService.createStamp(exploreTrip, 0, request) + + // then + assertThat(result).isEqualTo(exploreStamp2) + assertThat(result.stampOrder).isEqualTo(0) + } + } + + @Nested + @DisplayName("createStamps 메서드는") + inner class CreateStamps { + private val fixture = CreateStampRequestFixture() + + @Test + @DisplayName("코스형 여행에 대한 스탬프 목록을 생성하고 반환한다.") + fun shouldCreateAndReturnStampsWithNextOrderForCourseTrip() { + // given + val requests = listOf(fixture.build(), fixture.build()) + given(stampRepository.saveAll(anyList())).willReturn(listOf(courseStamp1, courseStamp2)) + + // when + stampCommandService.createStamps(courseTrip, 1, requests) + + // then + verify(stampRepository).saveAll(anyList()) + } + + @Test + @DisplayName("탐험형 여행에 대한 스탬프 목록을 생성하고 반환한다.") + fun shouldCreateAndReturnStampsWithZeroOrderForExploreTrip() { + // given + val requests = listOf(fixture.withEndDate(null).build(), fixture.withEndDate(null).build()) + given(stampRepository.saveAll(anyList())).willReturn(listOf(exploreStamp1, exploreStamp2)) + + // when + stampCommandService.createStamps(exploreTrip, 0, requests) + + // then + verify(stampRepository).saveAll(anyList()) + } + } + + @Nested + @DisplayName("updateStamp 메서드는") + inner class UpdateStamp { + private val fixture = UpdateStampRequestFixture() + + @Test + @DisplayName("스탬프 종료일을 여행 종료일보다 이후 날짜로 수정하면 예외가 발생한다.") + fun shouldThrowExceptionWhenEndDateIsAfterTripEndDate() { + // given + val request = fixture.withEndTime(LocalDate.now().plusDays(100)).build() + + // when + val exception = assertThrows { stampCommandService.updateStamp(courseTrip, courseStamp1, request) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_END_DATE_AFTER_TRIP_END_DATE_NOT_ALLOWED.message) + } + + @Test + @DisplayName("유효한 요청이 들어오면 스탬프 이름을 수정한다.") + fun shouldUpdateStampNameWhenRequestIsValid() { + // given + val request = fixture.withEndTime(null).build() + + // when + stampCommandService.updateStamp(courseTrip, courseStamp1, request) + + // then + assertThat(courseStamp1.name).isEqualTo(request.name) + } + + @Test + @DisplayName("유효한 요청이 들어오면 스탬프 종료일을 수정한다.") + fun shouldUpdateStampEndDateWhenRequestIsValid() { + // given + val request = fixture.withName(null).build() + + // when + stampCommandService.updateStamp(courseTrip, courseStamp1, request) + + // then + assertThat(courseStamp1.endDate).isEqualTo(request.endDate) + } + + @Test + @DisplayName("유효한 요청이 들어오면 스탬프 이름과 종료일을 수정한다.") + fun shouldUpdateStampNameAndEndDateWhenRequestIsValid() { + // given + val request = fixture.build() + + // when + stampCommandService.updateStamp(courseTrip, courseStamp1, request) + + // then + assertThat(courseStamp1.name).isEqualTo(request.name) + assertThat(courseStamp1.endDate).isEqualTo(request.endDate) + } + } + + @Nested + @DisplayName("updateStampOrders 메서드는") + inner class UpdateStampOrders { + private val fixture = UpdateStampOrderRequestFixture() + + @Test + @DisplayName("탐험형 여행의 스탬프 순서를 수정하면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripIsExploreType() { + // given + val request = fixture.build() + + // when + val exception = assertThrows { stampCommandService.updateStampOrders(exploreTrip, request) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP.message) + } + + @Test + @DisplayName("요청에 존재하지 않는 스탬프 ID가 포함되어 있다면 예외가 발생한다.") + fun shouldThrowExceptionWhenRequestIsInvalid() { + // given + val request = fixture.withOrderedStampIds(listOf(1000L, 1001L)).build() + + // when + val exception = assertThrows { stampCommandService.updateStampOrders(courseTrip, request) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.INVALID_STAMP_ID_IN_REQUEST.message) + } + + @Test + @DisplayName("특정 여행에 속하지 않은 스탬프가 하나라도 존재하면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampsNotBelongToTrip() { + // given + val request = fixture.build() + val newTrip = TripFixture(member, TripCategory.COURSE).createWithId(3L) + given(stampRepository.findAllByIdIn(request.orderedStampIds)).willReturn(listOf(courseStamp1, courseStamp2)) + + // when + val exception = assertThrows { stampCommandService.updateStampOrders(newTrip, request) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampAlreadyDeleted() { + // given + val request = fixture.build() + courseStamp1.updateDeletedAt() + given(stampRepository.findAllByIdIn(request.orderedStampIds)).willReturn(listOf(courseStamp1, courseStamp2)) + + // when + val exception = assertThrows { stampCommandService.updateStampOrders(courseTrip, request) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_ALREADY_DELETED.message) + } + + @Test + @DisplayName("스탬프가 이미 완료되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampAlreadyCompleted() { + // given + val request = fixture.build() + courseStamp1.updateCompleted() + given(stampRepository.findAllByIdIn(request.orderedStampIds)).willReturn(listOf(courseStamp1, courseStamp2)) + + // when + val exception = assertThrows { stampCommandService.updateStampOrders(courseTrip, request) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_ALREADY_COMPLETED.message) + } + + @Test + @DisplayName("코스형 여행에서 요청으로 들어온 스탬프 ID 리스트 순서에 따라 스탬프 순서를 수정한다.") + fun shouldUpdateStampOrdersForCourseTrip() { + // given + val request = fixture.withOrderedStampIds(listOf(2L, 1L)).build() + given(stampRepository.findAllByIdIn(request.orderedStampIds)).willReturn(listOf(courseStamp1, courseStamp2)) + + // when + stampCommandService.updateStampOrders(courseTrip, request) + + // then + assertThat(courseStamp2.stampOrder).isEqualTo(1) + assertThat(courseStamp1.stampOrder).isEqualTo(2) + } + } + + @Nested + @DisplayName("updateStampOrdersByTripCategoryChange 메서드는") + inner class UpdateStampOrdersByTripCategoryChange { + @Test + @DisplayName("여행 카테고리가 탐험형으로 변경되면 소속되어 있던 모든 스탬프 순서를 0으로 초기화한다.") + fun shouldResetStampOrdersWhenTripCategoryChangesToExplore() { + // given + given(stampRepository.findAllByTripIdOrderByCreatedAtAsc(courseTrip.id)).willReturn(listOf(courseStamp1, courseStamp2)) + + // when + stampCommandService.updateStampOrdersByTripCategoryChange(courseTrip.id, TripCategory.EXPLORE) + + // then + assertThat(courseStamp1.stampOrder).isEqualTo(0) + assertThat(courseStamp2.stampOrder).isEqualTo(0) + } + + @Test + @DisplayName("여행 카테고리가 코스형으로 변경되면 소속되어 있던 모든 스탬프 순서를 생성일 기준으로 1부터 초기화합니다.") + fun shouldSetStampOrdersWhenCategoryChangesToCourse() { + // given + given(stampRepository.findAllByTripIdOrderByCreatedAtAsc(exploreTrip.id)).willReturn(listOf(exploreStamp1, exploreStamp2)) + + // when + stampCommandService.updateStampOrdersByTripCategoryChange(exploreTrip.id, TripCategory.COURSE) + + // then + assertThat(exploreStamp1.stampOrder).isEqualTo(1) + assertThat(exploreStamp2.stampOrder).isEqualTo(2) + } + } + + @Nested + @DisplayName("deleteStamp 메서드는") + inner class DeleteStamp { + @Test + @DisplayName("스탬프가 삭제될 때 deletedAt 필드를 현재 시간으로 업데이트한다. (소프트 삭제)") + fun shouldUpdateDeletedAtWhenStampIsDeleted() { + // when + stampCommandService.deleteStamp(courseStamp1) + + // then + assertThat(courseStamp1.deletedAt).isNotNull + } + } + + @Nested + @DisplayName("completeStamp 메서드는") + inner class CompleteStamp { + @Test + @DisplayName("스탬프가 완료될 때 completed 필드를 true로 업데이트한다.") + fun shouldUpdateCompletedWhenStampIsCompleted() { + // when + stampCommandService.completeStamp(courseStamp1) + + // then + assertThat(courseStamp1.isCompleted).isTrue + } + } + + @Nested + @DisplayName("shiftStampOrders 메서드는") + inner class ShiftStampOrders { + @Test + @DisplayName("시프트할 스탬프가 존재하지 않으면 기존 스탬프 순서를 변경하지 않는다.") + fun shouldNotChangeStampOrdersWhenStampsToShiftDoNotExist() { + // when + stampCommandService.shiftStampOrders(listOf()) + + // then + assertThat(courseStamp2.stampOrder).isEqualTo(2) + } + + @Test + @DisplayName("시프트할 스탬프가 존재하면 각 스탬프 순서를 1씩 감소시킨다.") + fun shouldDecreaseStampOrdersByOneWhenStampsToShiftExist() { + // when + stampCommandService.shiftStampOrders(listOf(courseStamp2)) + + // then + assertThat(courseStamp2.stampOrder).isEqualTo(1) + } + } + + @Nested + @DisplayName("increaseTotalMissions 메서드는") + inner class IncreaseTotalMissions { + @Test + @DisplayName("스탬프의 총 미션 수를 증가시킨다.") + fun shouldIncreaseTotalMissions() { + // given + val existingTotalMissions = courseStamp1.totalMissions + + // when + stampCommandService.increaseTotalMissions(courseStamp1) + + // then + assertThat(courseStamp1.totalMissions).isEqualTo(existingTotalMissions + 1) + } + + @Test + @DisplayName("스탬프의 총 미션 수를 1 감소시킨다.") + fun shouldDecreaseTotalMissions() { + // given + courseStamp1.increaseTotalMissions() + val existingTotalMissions = courseStamp1.totalMissions + + // when + stampCommandService.decreaseTotalMissions(courseStamp1) + + // then + assertThat(courseStamp1.totalMissions).isEqualTo(existingTotalMissions - 1) + } + } + + @Nested + @DisplayName("increaseCompletedMissions 메서드는") + inner class IncreaseCompletedMissions { + @Test + @DisplayName("지정된 개수만큼 스탬프의 완료된 미션 수를 증가시킨다.") + fun shouldIncreaseCompletedMissions() { + // given + val existingCompletedMissions = courseStamp1.completedMissions + val count = 2 + + // when + stampCommandService.increaseCompletedMissions(courseStamp1, count) + + // then + assertThat(courseStamp1.completedMissions).isEqualTo(existingCompletedMissions + count) + } + } + + @Nested + @DisplayName("validateStampBelongsToTrip 메서드는") + inner class ValidateStampBelongsToTrip { + @Test + @DisplayName("특정 여행에 속하지 않은 스탬프가 존재하면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampNotBelongToTrip() { + // given + val newTrip = TripFixture(member, TripCategory.COURSE).createWithId(3L) + + // when + val exception = assertThrows { stampCommandService.validateStampBelongsToTrip(newTrip.id, courseStamp1) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message) + } + } + + @Nested + @DisplayName("validateAllStampsCompletedByTripId 메서드는") + inner class ValidateAllStampsCompletedByTripId { + @Test + @DisplayName("특정 여행의 어떤 스탬프가 완료되지 않았다면 예외가 발생한다.") + fun shouldThrowExceptionWhenAnyStampIsNotCompleted() { + // given + val tripId = courseTrip.id + given(stampCommandRepository.existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(tripId)).willReturn(true) + + // when + val exception = assertThrows { stampCommandService.validateAllStampsCompletedByTripId(tripId) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.ALL_STAMPS_NOT_COMPLETED.message) + } + + @Test + @DisplayName("특정 여행의 모든 스탬프가 완료되었다면 예외가 발생하지 않는다.") + fun shouldPassWhenAllStampsAreCompleted() { + // given + val tripId = courseTrip.id + given(stampCommandRepository.existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(tripId)).willReturn(false) + + // when & then + assertDoesNotThrow { stampCommandService.validateAllStampsCompletedByTripId(tripId) } + } + } + + @Nested + @DisplayName("hardDeleteStamps 메서드는") + inner class HardDeleteStamps { + @Test + @DisplayName("삭제된 스탬프가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenDeletedStampsDoNotExist() { + // given + given(stampCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L) + + // when + val result = stampCommandService.hardDeleteStamps() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 스탬프가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenDeletedStampsExist() { + // given + given(stampCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L) + + // when + val result = stampCommandService.hardDeleteStamps() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteStampsOwnedByDeletedTrip 메서드는") + inner class HardDeleteStampsOwnedByDeletedTrip { + @Test + @DisplayName("삭제된 여행이 소유한 스탬프가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenStampsOwnedByDeletedTripDoNotExist() { + // given + given(stampCommandRepository.deleteAllByDeletedTripOwner()).willReturn(0L) + + // when + val result = stampCommandService.hardDeleteStampsOwnedByDeletedTrip() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 여행이 소유한 스탬프가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenStampsOwnedByDeletedStampExist() { + // given + given(stampCommandRepository.deleteAllByDeletedTripOwner()).willReturn(5L) + + // when + val result = stampCommandService.hardDeleteStampsOwnedByDeletedTrip() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteStampsOwnedByMember 메서드는") + inner class HardDeleteStampsOwnedByMember { + @Test + @DisplayName("특정 멤버가 소유한 스탬프가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenStampsOwnedByMemberDoNotExist() { + // given + val memberId = member.id + given(stampCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L) + + // when + val result = stampCommandService.hardDeleteStampsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("특정 멤버가 소유한 스탬프가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenStampsOwnedByMemberExist() { + // given + val memberId = member.id + given(stampCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L) + + // when + val result = stampCommandService.hardDeleteStampsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(5L) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/stamp/application/service/StampQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/stamp/application/service/StampQueryServiceTest.kt new file mode 100644 index 0000000..9712f5a --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/application/service/StampQueryServiceTest.kt @@ -0,0 +1,319 @@ +package com.ject.studytrip.stamp.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.stamp.domain.error.StampErrorCode +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.StampFixture +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.fixture.TripFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.BDDMockito.given +import org.mockito.InjectMocks +import org.mockito.Mock +import org.springframework.test.util.ReflectionTestUtils +import java.time.LocalDateTime +import java.util.Optional + +@DisplayName("StampQueryService 단위 테스트") +class StampQueryServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var stampQueryService: StampQueryService + + @Mock + private lateinit var stampRepository: StampRepository + + @Mock + private lateinit var stampQueryRepository: StampQueryRepository + + private lateinit var courseTrip: Trip + private lateinit var exploreTrip: Trip + private lateinit var courseStamp1: Stamp + private lateinit var courseStamp2: Stamp + private lateinit var exploreStamp1: Stamp + private lateinit var exploreStamp2: Stamp + + @BeforeEach + fun setUp() { + val member = MemberFixture.createMemberFromKakaoWithId(1L) + courseTrip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + exploreTrip = TripFixture(member, TripCategory.EXPLORE).createWithId(2L) + courseStamp1 = StampFixture(courseTrip, 1).createWithId(1L) + courseStamp2 = StampFixture(courseTrip, 2).createWithId(2L) + exploreStamp1 = StampFixture(exploreTrip, 0).createWithId(3L) + exploreStamp2 = StampFixture(exploreTrip, 0).createWithId(4L) + } + + @Nested + @DisplayName("getValidStamp 메서드는") + inner class GetValidStamp { + @Test + @DisplayName("스탬프가 존재하지 않으면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampDoesNotExist() { + // given + val stampId = -1L + + // when + val exception = assertThrows { stampQueryService.getValidStamp(courseTrip.id, stampId) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_NOT_FOUND.message) + } + + @Test + @DisplayName("특정 스탬프가 다른 여행에 속한다면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampNotBelongToTrip() { + // given + val stampId = courseStamp1.id + given(stampRepository.findById(stampId)).willReturn(Optional.of(courseStamp1)) + + // when + val exception = assertThrows { stampQueryService.getValidStamp(exploreTrip.id, stampId) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampAlreadyDeleted() { + // given + val stampId = courseStamp1.id + courseStamp1.updateDeletedAt() + given(stampRepository.findById(stampId)).willReturn(Optional.of(courseStamp1)) + + // when + val exception = assertThrows { stampQueryService.getValidStamp(courseTrip.id, stampId) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_ALREADY_DELETED.message) + } + + @Test + @DisplayName("스탬프가 이미 완료되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampAlreadyCompleted() { + // given + val stampId = courseStamp1.id + courseStamp1.updateCompleted() + given(stampRepository.findById(stampId)).willReturn(Optional.of(courseStamp1)) + + // when + val exception = assertThrows { stampQueryService.getValidStamp(courseTrip.id, stampId) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_ALREADY_COMPLETED.message) + } + + @Test + @DisplayName("특정 여행에 속한 스탬프가 존재하면 스탬프를 조회하고 반환한다.") + fun shouldReturnStampWhenStampBelongsToTrip() { + // given + val stampId = courseStamp1.id + given(stampRepository.findById(stampId)).willReturn(Optional.of(courseStamp1)) + + // when + val result = stampQueryService.getValidStamp(courseTrip.id, stampId) + + // then + assertThat(result).isEqualTo(courseStamp1) + } + } + + @Nested + @DisplayName("getStampsByTripId 메서드는") + inner class GetStampsByTripId { + @Test + @DisplayName("특정 여행에 속한 삭제되지 않은 스탬프 목록을 조회하고 반환한다.") + fun shouldReturnStampsByTripIdAndDeletedAtIsNull() { + // given + val tripId = courseTrip.id + given(stampRepository.findAllByTripIdAndDeletedAtIsNull(tripId)).willReturn(listOf(courseStamp1, courseStamp2)) + + // when + val result = stampQueryService.getStampsByTripId(tripId) + + // then + assertThat(result).hasSize(2) + assertThat(result).containsExactly(courseStamp1, courseStamp2) + } + } + + @Nested + @DisplayName("getFirstInProcessingStampsForCourseTrip 메서드는") + inner class GetFirstInProcessingStampsForCourseTrip { + @Test + @DisplayName("특정 코스형 여행에서 진행 중인 스탬프가 존재하지 않으면 예외가 발생한다.") + fun shouldThrowExceptionWhenProgressStampDoesNotExistForCourseTrip() { + // given + val tripId = courseTrip.id + courseStamp1.updateCompleted() + courseStamp2.updateCompleted() + given(stampQueryRepository.findFirstIncompleteStampByTripId(tripId)).willReturn(Optional.empty()) + + // when + val exception = assertThrows { stampQueryService.getFirstInProcessingStampsForCourseTrip(tripId) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_NOT_FOUND.message) + } + + @Test + @DisplayName("특정 코스형 여행에서 진행 중인 첫번째 스탬프를 조회하고 반환한다.") + fun shouldReturnFirstProcessingStampForCourseTrip() { + // given + val tripId = courseTrip.id + given(stampQueryRepository.findFirstIncompleteStampByTripId(tripId)).willReturn(Optional.of(courseStamp1)) + + // when + val result = stampQueryService.getFirstInProcessingStampsForCourseTrip(tripId) + + // then + assertThat(result).isEqualTo(courseStamp1) + assertThat(result.isDeleted).isFalse + assertThat(result.isCompleted).isFalse + } + } + + @Nested + @DisplayName("getStampNameByTripCategory 메서드는") + inner class GetStampNameByTripCategory { + @Test + @DisplayName("스탬프 목록이 비어있다면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampListIsEmpty() { + // given + val stamps = emptyList() + + // when + val exception = assertThrows { stampQueryService.getStampNameByTripCategory(TripCategory.COURSE, stamps) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_LIST_NOT_EMPTY.message) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenStampAlreadyDeleted() { + // given + courseStamp1.updateDeletedAt() + val stamps = listOf(courseStamp1) + // when + val exception = assertThrows { stampQueryService.getStampNameByTripCategory(TripCategory.COURSE, stamps) } + + // then + assertThat(exception.message).isEqualTo(StampErrorCode.STAMP_ALREADY_DELETED.message) + } + + @Test + @DisplayName("코스형 여행일 경우, 스탬프 리스트에서 첫번째 스탬프 이름을 반환한다.") + fun shouldReturnFirstStampNameForCourseTrip() { + // given + val stamps = listOf(courseStamp1, courseStamp2) + + // when + val result = stampQueryService.getStampNameByTripCategory(TripCategory.COURSE, stamps) + + // then + assertThat(result).isEqualTo(courseStamp1.name) + } + + @Test + @DisplayName("탐험형 여행일 경우, 선택한 미션들의 스탬프 중 가장 많이 포함된 스탬프 이름을 반환한다.") + fun shouldReturnMostFrequentStampNameForExploreTrip() { + // given + val stamps = listOf(exploreStamp1, exploreStamp2, exploreStamp2, exploreStamp2) + + // when + val result = stampQueryService.getStampNameByTripCategory(TripCategory.EXPLORE, stamps) + + // then + assertThat(result).isEqualTo(exploreStamp2.name) + } + + @Test + @DisplayName("탐험형 여행일 경우, 가장 많이 포함된 스탬프가 2개 이상이라면 생성일이 가장 빠른 스탬프 이름을 반환한다.") + fun shouldReturnEarliestStampNameWhenMostFrequentStampsAreMultiple() { + // given + ReflectionTestUtils.setField(exploreStamp1, "createdAt", LocalDateTime.now()) + ReflectionTestUtils.setField(exploreStamp2, "createdAt", LocalDateTime.now().minusDays(1)) + val stamps = listOf(exploreStamp1, exploreStamp1, exploreStamp2, exploreStamp2) + + // when + val result = stampQueryService.getStampNameByTripCategory(TripCategory.EXPLORE, stamps) + + // then + assertThat(result).isEqualTo(exploreStamp2.name) + } + } + + @Nested + @DisplayName("getNextStampOrderByTrip 메서드는") + inner class GetNextStampOrderByTrip { + @Test + @DisplayName("탐험형 여행이라면 0을 반환한다.") + fun shouldReturnZeroForExploreTrip() { + // when + val result = stampQueryService.getNextStampOrderByTrip(exploreTrip) + + // then + assertThat(result).isEqualTo(0) + } + + @Test + @DisplayName("코스형 여행이라면 다음 스탬프 순서를 반환한다.") + fun shouldReturnNextStampOrderForCourseTrip() { + // given + given(stampQueryRepository.findNextStampOrderByTripId(courseTrip.id)).willReturn(3) + + // when + val result = stampQueryService.getNextStampOrderByTrip(courseTrip) + + // then + assertThat(result).isEqualTo(3) + } + } + + @Nested + @DisplayName("getStampsToShiftAfterDeleted 메서드는") + inner class GetStampsToShiftAfterDeleted { + @Test + @DisplayName("시프트할 스탬프가 존재하지 않으면 빈 리스트를 반환한다.") + fun shouldReturnEmptyListWhenStampsToShiftDoNotExist() { + // given + val tripId = courseTrip.id + val deletedOrder = courseStamp2.stampOrder + courseStamp2.updateDeletedAt() + given(stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedOrder)).willReturn(emptyList()) + + // when + val result = stampQueryService.getStampsToShiftAfterDeleted(tripId, deletedOrder) + + // then + assertThat(result).isEqualTo(emptyList()) + } + + @Test + @DisplayName("시프트할 스탬프 목록을 조회하고 반환한다.") + fun shouldReturnStampsToShiftAfterDeleted() { + // given + val tripId = courseTrip.id + val deletedOrder = courseStamp1.stampOrder + given(stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedOrder)).willReturn(listOf(courseStamp2)) + + // when + val result = stampQueryService.getStampsToShiftAfterDeleted(tripId, deletedOrder) + + // then + assertThat(result).hasSize(1) + assertThat(result).containsExactly(courseStamp2) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/stamp/fixture/CreateStampRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/stamp/fixture/CreateStampRequestFixture.kt new file mode 100644 index 0000000..e65d953 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/fixture/CreateStampRequestFixture.kt @@ -0,0 +1,19 @@ +package com.ject.studytrip.stamp.fixture + +import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest +import java.time.LocalDate + +class CreateStampRequestFixture { + var name: String = "TEST 스탬프 이름" + var endDate: LocalDate? = LocalDate.now().plusDays(7) + + fun withName(name: String): CreateStampRequestFixture = apply { this.name = name } + + fun withEndDate(endDate: LocalDate?): CreateStampRequestFixture = apply { this.endDate = endDate } + + fun withEndDateInPast(): CreateStampRequestFixture = apply { this.endDate = LocalDate.now().minusDays(1) } + + fun withEndDateAfterTripEndDate(): CreateStampRequestFixture = apply { this.endDate = LocalDate.now().plusDays(100) } + + fun build() = CreateStampRequest(name, endDate) +} diff --git a/src/test/kotlin/com/ject/studytrip/stamp/fixture/StampFixture.kt b/src/test/kotlin/com/ject/studytrip/stamp/fixture/StampFixture.kt new file mode 100644 index 0000000..76e9fc3 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/fixture/StampFixture.kt @@ -0,0 +1,22 @@ +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 org.springframework.test.util.ReflectionTestUtils +import java.time.LocalDate + +class StampFixture( + private val trip: Trip, + private val order: Int, +) { + var name: String = "TEST 스탬프 이름" + var endTime: LocalDate = LocalDate.now().plusDays(7) + + fun create(): Stamp = StampFactory.create(trip, name, order, endTime) + + fun createWithId(id: Long): Stamp = + create().also { + ReflectionTestUtils.setField(it, "id", id) + } +} diff --git a/src/test/kotlin/com/ject/studytrip/stamp/fixture/UpdateStampOrderRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/stamp/fixture/UpdateStampOrderRequestFixture.kt new file mode 100644 index 0000000..2dbcbe2 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/fixture/UpdateStampOrderRequestFixture.kt @@ -0,0 +1,11 @@ +package com.ject.studytrip.stamp.fixture + +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampOrderRequest + +class UpdateStampOrderRequestFixture { + var orderedStampIds: List = listOf(1L, 2L) + + fun withOrderedStampIds(orderedStampIds: List): UpdateStampOrderRequestFixture = apply { this.orderedStampIds = orderedStampIds } + + fun build(): UpdateStampOrderRequest = UpdateStampOrderRequest(orderedStampIds) +} diff --git a/src/test/kotlin/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.kt new file mode 100644 index 0000000..c5ad086 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/fixture/UpdateStampRequestFixture.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.stamp.fixture + +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest +import java.time.LocalDate + +class UpdateStampRequestFixture { + var name: String? = "TEST 새로운 스탬프 이름" + var endDate: LocalDate? = LocalDate.now().plusDays(7) + + fun withName(name: String?): UpdateStampRequestFixture = apply { this.name = name } + + fun withEndTime(endTime: LocalDate?): UpdateStampRequestFixture = apply { this.endDate = endTime } + + fun build() = UpdateStampRequest(name, endDate) +} diff --git a/src/test/kotlin/com/ject/studytrip/stamp/helper/StampTestHelper.kt b/src/test/kotlin/com/ject/studytrip/stamp/helper/StampTestHelper.kt new file mode 100644 index 0000000..cd54065 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/helper/StampTestHelper.kt @@ -0,0 +1,34 @@ +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.stereotype.Component + +@Component +class StampTestHelper( + private val stampRepository: StampRepository, +) { + fun saveStamp( + trip: Trip, + order: Int, + ): Stamp = stampRepository.save(StampFixture(trip, order).create()) + + fun saveDeletedStamp( + trip: Trip, + order: Int, + ): Stamp = stampRepository.save(StampFixture(trip, order).create().also { it.updateDeletedAt() }) + + fun saveCompletedStamp( + trip: Trip, + order: Int, + ): Stamp = + stampRepository.save( + StampFixture(trip, order).create().also { + it.updateCompleted() + }, + ) + + fun getStamp(stampId: Long): Stamp = stampRepository.findById(stampId).orElseThrow() +} diff --git a/src/test/kotlin/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.kt new file mode 100644 index 0000000..9dc0cd7 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/stamp/presentation/controller/StampControllerIntegrationTest.kt @@ -0,0 +1,1463 @@ +package com.ject.studytrip.stamp.presentation.controller + +import com.ject.studytrip.BaseIntegrationTest +import com.ject.studytrip.auth.domain.error.AuthErrorCode +import com.ject.studytrip.auth.fixture.TokenFixture +import com.ject.studytrip.auth.helper.TokenTestHelper +import com.ject.studytrip.global.exception.error.CommonErrorCode +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.domain.model.MemberRole +import com.ject.studytrip.member.helper.MemberTestHelper +import com.ject.studytrip.mission.domain.error.MissionErrorCode +import com.ject.studytrip.mission.helper.MissionTestHelper +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.UpdateStampOrderRequestFixture +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.UpdateStampOrderRequest +import com.ject.studytrip.stamp.presentation.dto.request.UpdateStampRequest +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 org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@DisplayName("StampController 통합 테스트") +class StampControllerIntegrationTest : BaseIntegrationTest() { + @Autowired private lateinit var memberTestHelper: MemberTestHelper + + @Autowired private lateinit var tokenTestHelper: TokenTestHelper + + @Autowired private lateinit var tripTestHelper: TripTestHelper + + @Autowired private lateinit var stampTestHelper: StampTestHelper + + @Autowired private lateinit var missionTestHelper: MissionTestHelper + + private lateinit var member: Member + private lateinit var token: String + + // 코스형 + private lateinit var courseTrip: Trip + private lateinit var courseStamp1: Stamp + private lateinit var courseStamp2: Stamp + + // 탐험형 + private lateinit var exploreTrip: Trip + private lateinit var exploreStamp1: Stamp + private lateinit var exploreStamp2: Stamp + + // 새로운 여행 + private lateinit var newTrip: Trip + + @BeforeEach + fun setUp() { + member = memberTestHelper.saveMember() + token = tokenTestHelper.createAccessToken(member.id.toString(), MemberRole.ROLE_USER.name) + + courseTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + courseStamp1 = stampTestHelper.saveStamp(courseTrip, 1) + courseStamp2 = stampTestHelper.saveStamp(courseTrip, 2) + + exploreTrip = tripTestHelper.saveTrip(member, TripCategory.EXPLORE) + exploreStamp1 = stampTestHelper.saveStamp(exploreTrip, 0) + exploreStamp2 = stampTestHelper.saveStamp(exploreTrip, 0) + + val newMember = memberTestHelper.saveMember("test@gmail.com", "test") + newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE) + } + + companion object { + private const val BASE_STAMP_URL = "/api/trips/{tripId}/stamps" + } + + @Nested + @DisplayName("스탬프 생성 API") + inner class CreateStamp { + private val fixture = CreateStampRequestFixture() + + private fun getResultActions( + token: String, + tripId: Any, + request: CreateStampRequest, + ): ResultActions = + mockMvc.perform( + post(BASE_STAMP_URL, tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", courseTrip.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("CreateStampRequest 이름이 비어있으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenRequestNameIsBlank() { + // given + val request = fixture.withName(" ").build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, newTrip.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, deletedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, completedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 스탬프를 생성하고 반환한다.") + fun shouldCreateAndReturnStampWhenRequestIsValid() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isCreated) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.stampId").isNumber) + } + } + + @Nested + @DisplayName("스탬프 수정 API") + inner class UpdateStamp { + private val fixture = UpdateStampRequestFixture() + + private fun getResultActions( + token: String, + tripId: Any, + stampId: Any, + request: UpdateStampRequest, + ): ResultActions = + mockMvc.perform( + patch("$BASE_STAMP_URL/{stampId}", tripId, stampId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", courseTrip.id, courseStamp1.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampIdTypeMismatch() { + // given + val stampId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, newTrip.id, courseStamp1.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, deletedTrip.id, courseStamp1.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, completedTrip.id, courseStamp1.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("스탬프가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenStampDoesNotExist() { + // given + val stampId = -1L + val request = fixture.build() + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_FOUND.message)) + } + + @Test + @DisplayName("스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenStampNotBelongToTrip() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, courseTrip.id, exploreStamp1.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyDeleted() { + // given + val deletedStamp = stampTestHelper.saveDeletedStamp(courseTrip, 3) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, courseTrip.id, deletedStamp.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("스탬프가 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyCompleted() { + // given + val completedStamp = stampTestHelper.saveCompletedStamp(courseTrip, 3) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, courseTrip.id, completedStamp.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 스탬프를 수정한다.") + fun shouldUpdateStampWhenRequestIsValid() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, courseTrip.id, courseStamp1.id, request) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("스탬프 순서 변경 API") + inner class UpdateStampOrders { + private val fixture = UpdateStampOrderRequestFixture() + + private fun getResultActions( + token: String, + tripId: Any, + request: UpdateStampOrderRequest, + ): ResultActions = + mockMvc.perform( + put("$BASE_STAMP_URL/orders", tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", courseTrip.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, newTrip.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, deletedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, completedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("탐험형 여행이 스탬프 순서를 변경한다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampOrdersAreUpdatedForExploreTrip() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, exploreTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.CANNOT_UPDATE_ORDER_FOR_EXPLORATION_TRIP.message)) + } + + @Test + @DisplayName("UpdateOrderRequest에 존재하지 않는 스탬프 ID가 포함되어 있다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenRequestContainsNonExistentStampId() { + // given + val request = fixture.withOrderedStampIds(listOf(1000L, 2000L)).build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.INVALID_STAMP_ID_IN_REQUEST.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.INVALID_STAMP_ID_IN_REQUEST.message)) + } + + @Test + @DisplayName("스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenStampNotBelongToTrip() { + // given + val request = fixture.withOrderedStampIds(listOf(courseStamp1.id, courseStamp2.id, exploreStamp1.id)).build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyDeleted() { + // given + val deletedStamp = stampTestHelper.saveDeletedStamp(courseTrip, 3) + val request = fixture.withOrderedStampIds(listOf(courseStamp1.id, courseStamp2.id, deletedStamp.id)).build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("스탬프가 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyCompleted() { + // given + val completedStamp = stampTestHelper.saveCompletedStamp(courseTrip, 3) + val request = fixture.withOrderedStampIds(listOf(courseStamp1.id, courseStamp2.id, completedStamp.id)).build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 스탬프 순서를 변경한다.") + fun shouldUpdateStampOrdersWhenRequestIsValid() { + // given + val request = fixture.withOrderedStampIds(listOf(courseStamp2.id, courseStamp1.id)).build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("스탬프 삭제 API") + inner class DeleteStamp { + private fun getResultActions( + token: String, + tripId: Any, + stampId: Any, + ): ResultActions = + mockMvc.perform( + delete("$BASE_STAMP_URL/{stampId}", tripId, stampId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", courseTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampIdTypeMismatch() { + // given + val stampId = "abc" + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("스탬프가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenStampDoesNotExist() { + // given + val stampId = -1L + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_FOUND.message)) + } + + @Test + @DisplayName("스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenStampNotBelongToTrip() { + // when + val resultActions = getResultActions(token, courseTrip.id, exploreStamp1.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyDeleted() { + // given + val deletedStamp = stampTestHelper.saveDeletedStamp(courseTrip, 3) + + // when + val resultActions = getResultActions(token, courseTrip.id, deletedStamp.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("스탬프가 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyCompleted() { + // given + val completedStamp = stampTestHelper.saveCompletedStamp(courseTrip, 3) + + // when + val resultActions = getResultActions(token, courseTrip.id, completedStamp.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("특정 스탬프를 삭제한다.") + fun shouldDeleteStamp() { + // when + val resultActions = getResultActions(token, courseTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("스탬프 완료 API") + inner class CompleteStamp { + private fun getResultActions( + token: String, + tripId: Any, + stampId: Any, + ): ResultActions = + mockMvc.perform( + patch("$BASE_STAMP_URL/{stampId}/complete", tripId, stampId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", courseTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampIdTypeMismatch() { + // given + val stampId = "abc" + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("스탬프가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenStampDoesNotExist() { + // given + val stampId = -1L + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_FOUND.message)) + } + + @Test + @DisplayName("스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenStampNotBelongToTrip() { + // when + val resultActions = getResultActions(token, courseTrip.id, exploreStamp1.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyDeleted() { + // given + val deletedStamp = stampTestHelper.saveDeletedStamp(courseTrip, 3) + + // when + val resultActions = getResultActions(token, courseTrip.id, deletedStamp.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("스탬프가 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyCompleted() { + // given + val completedStamp = stampTestHelper.saveCompletedStamp(courseTrip, 3) + + // when + val resultActions = getResultActions(token, courseTrip.id, completedStamp.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("특정 스탬프의 어떤 미션이 완료되지 않았다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenAnyMissionIsNotCompleted() { + // given + missionTestHelper.saveMission(courseStamp1) + missionTestHelper.saveCompletedMission(courseStamp1) + + // when + val resultActions = getResultActions(token, courseTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.ALL_MISSIONS_NOT_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.ALL_MISSIONS_NOT_COMPLETED.message)) + } + + @Test + @DisplayName("특정 스탬프의 모든 미션이 완료되었다면 스탬프를 완료한다.") + fun shouldCompleteStampWhenAllMissionsAreCompleted() { + // given + missionTestHelper.saveCompletedMission(courseStamp1) + missionTestHelper.saveCompletedMission(courseStamp1) + + // when + val resultActions = getResultActions(token, courseTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("스탬프 목록 조회 API") + inner class LoadStampsByTrip { + private fun getResultActions( + token: String, + tripId: Any, + ): ResultActions = + mockMvc.perform( + get(BASE_STAMP_URL, tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", courseTrip.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("특정 스탬프의 미션 목록을 조회하고 반환한다.") + fun shouldReturnStampsByTrip() { + // when + val resultActions = getResultActions(token, courseTrip.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } + + @Nested + @DisplayName("스탬프 상세 조회 API") + inner class LoadStamp { + private fun getResultActions( + token: String, + tripId: Any, + stampId: Any, + ): ResultActions = + mockMvc.perform( + get("$BASE_STAMP_URL/{stampId}", tripId, stampId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", courseTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("PathVariable 스탬프 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampIdTypeMismatch() { + // given + val stampId = "abc" + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId, courseStamp1.id) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("스탬프가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenStampDoesNotExist() { + // given + val stampId = -1L + + // when + val resultActions = getResultActions(token, courseTrip.id, stampId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_FOUND.message)) + } + + @Test + @DisplayName("스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenStampNotBelongToTrip() { + // when + val resultActions = getResultActions(token, courseTrip.id, exploreStamp1.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("스탬프가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyDeleted() { + // given + val deletedStamp = stampTestHelper.saveDeletedStamp(courseTrip, 3) + + // when + val resultActions = getResultActions(token, courseTrip.id, deletedStamp.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("스탬프가 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenStampAlreadyCompleted() { + // given + val completedStamp = stampTestHelper.saveCompletedStamp(courseTrip, 3) + + // when + val resultActions = getResultActions(token, courseTrip.id, completedStamp.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("특정 스탬프를 상세 조회하고 반환한다.") + fun shouldReturnStamp() { + // when + val resultActions = getResultActions(token, courseTrip.id, courseStamp1.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogCommandServiceTest.kt index d5de4fc..cd7d45e 100644 --- a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogCommandServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogCommandServiceTest.kt @@ -45,8 +45,8 @@ class StudyLogCommandServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { member = MemberFixture.createMemberFromKakaoWithId(1L) - courseTrip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, courseTrip) + courseTrip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + dailyGoal = DailyGoalFixture(courseTrip).createWithId(1L) studyLog = StudyLogFixture(member, dailyGoal).createWithId(1L) } diff --git a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionCommandServiceTest.kt index d47df70..e2ef99a 100644 --- a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionCommandServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionCommandServiceTest.kt @@ -48,11 +48,11 @@ class StudyLogDailyMissionCommandServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { member = MemberFixture.createMemberFromKakaoWithId(1L) - val trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - val stamp = StampFixture.createStampWithId(1L, trip, 1) + val trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + val stamp = StampFixture(trip, 1).createWithId(1L) mission1 = MissionFixture(stamp).createWithId(1L) mission2 = MissionFixture(stamp).createWithId(2L) - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, trip) + dailyGoal = DailyGoalFixture(trip).createWithId(1L) dailyMission1 = DailyMissionFixture(mission1, dailyGoal).createWithId(1L) dailyMission2 = DailyMissionFixture(mission2, dailyGoal).createWithId(2L) studyLog = StudyLogFixture(member, dailyGoal).createWithId(1L) diff --git a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionQueryServiceTest.kt index 34f2aab..f5ed9af 100644 --- a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionQueryServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogDailyMissionQueryServiceTest.kt @@ -39,10 +39,10 @@ class StudyLogDailyMissionQueryServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { val member = MemberFixture.createMemberFromKakaoWithId(1L) - val trip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - val stamp = StampFixture.createStampWithId(1L, trip, 1) + val trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + val stamp = StampFixture(trip, 1).createWithId(1L) val mission = MissionFixture(stamp).createWithId(1L) - val dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, trip) + val dailyGoal = DailyGoalFixture(trip).createWithId(1L) val dailyMission1 = DailyMissionFixture(mission, dailyGoal).createWithId(1L) val dailyMission2 = DailyMissionFixture(mission, dailyGoal).createWithId(2L) studyLog1 = StudyLogFixture(member, dailyGoal).createWithId(1L) diff --git a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt index 0574914..5306c20 100644 --- a/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt +++ b/src/test/kotlin/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.kt @@ -58,8 +58,8 @@ class StudyLogQueryServiceTest : BaseUnitTest() { @BeforeEach fun setUp() { member = MemberFixture.createMemberFromKakaoWithId(1L) - courseTrip = TripFixture.createTripWithId(1L, member, TripCategory.COURSE) - dailyGoal = DailyGoalFixture.createDailyGoalWithId(1L, courseTrip) + courseTrip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + dailyGoal = DailyGoalFixture(courseTrip).createWithId(1L) studyLog1 = StudyLogFixture(member, dailyGoal).createWithId(1L) studyLog2 = StudyLogFixture(member, dailyGoal).createWithId(2L) tripReport = TripReportFixture.createTripReportWithId(1L, member) diff --git a/src/test/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogControllerIntegrationTest.kt index 5066064..5bf4dc8 100644 --- a/src/test/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogControllerIntegrationTest.kt +++ b/src/test/kotlin/com/ject/studytrip/studylog/presentation/controller/StudyLogControllerIntegrationTest.kt @@ -293,7 +293,7 @@ class StudyLogControllerIntegrationTest : BaseIntegrationTest() { @Test @DisplayName("데일리 목표가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") - fun shouldReturnForbiddenWhenDailyGoalDoesNotBelongToTrip() { + fun shouldReturnForbiddenWhenDailyGoalNotBelongToTrip() { // given val newTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) val newDailyGoal = dailyGoalTestHelper.saveDailyGoal(newTrip) @@ -306,8 +306,8 @@ class StudyLogControllerIntegrationTest : BaseIntegrationTest() { resultActions .andExpect(status().isForbidden) .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONG_TO_TRIP.status.value())) - .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONG_TO_TRIP.message)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.message)) } @Test @@ -347,7 +347,7 @@ class StudyLogControllerIntegrationTest : BaseIntegrationTest() { @Test @DisplayName("데일리 미션이 요청한 데일리 목표에 속하지 않으면 403 Forbidden을 반환한다.") - fun shouldReturnForbiddenWhenDailyMissionDoesNotBelongToDailyGoal() { + fun shouldReturnForbiddenWhenDailyMissionNotBelongToDailyGoal() { // given val newDailyGoal = dailyGoalTestHelper.saveDailyGoal(trip) val request = fixture.withSelectedDailyMissionIds(listOf(dailyMission.id)).build() diff --git a/src/test/kotlin/com/ject/studytrip/trip/application/service/DailyGoalCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/trip/application/service/DailyGoalCommandServiceTest.kt new file mode 100644 index 0000000..ded166d --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/application/service/DailyGoalCommandServiceTest.kt @@ -0,0 +1,169 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.repository.DailyGoalCommandRepository +import com.ject.studytrip.trip.domain.repository.DailyGoalRepository +import com.ject.studytrip.trip.fixture.DailyGoalFixture +import com.ject.studytrip.trip.fixture.TripFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.kotlin.any + +@DisplayName("DailyGoalCommandService 단위 테스트") +class DailyGoalCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var dailyGoalCommandService: DailyGoalCommandService + + @Mock + private lateinit var dailyGoalRepository: DailyGoalRepository + + @Mock + private lateinit var dailyGoalCommandRepository: DailyGoalCommandRepository + + private lateinit var member: Member + private lateinit var trip: Trip + private lateinit var dailyGoal: DailyGoal + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + dailyGoal = DailyGoalFixture(trip).createWithId(1L) + } + + @Nested + @DisplayName("createDailyGoal 메서드는") + inner class CreateDailyGoal { + @Test + @DisplayName("데일리 목표를 생성하고 반환한다.") + fun shouldCreateAndReturnDailyGoal() { + // given + val title = dailyGoal.title + given(dailyGoalRepository.save(any())).willReturn(dailyGoal) + + // when + val result = dailyGoalCommandService.createDailyGoal(trip, title) + + // then + assertThat(result).isEqualTo(dailyGoal) + assertThat(dailyGoal.trip).isEqualTo(trip) + } + } + + @Nested + @DisplayName("deleteDailyGoal 메서드는") + inner class DeleteDailyGoal { + @Test + @DisplayName("데일리 목표가 삭제될 때 deletedAt 필드를 현재 시간으로 업데이트한다. (소프트 삭제)") + fun shouldUpdateDeletedAtWhenDailyGoalIsDeleted() { + // when + dailyGoalCommandService.deleteDailyGoal(dailyGoal) + + // then + assertThat(dailyGoal.deletedAt).isNotNull + } + } + + @Nested + @DisplayName("hardDeleteDailyGoals 메서드는") + inner class HardDeleteDailyGoals { + @Test + @DisplayName("삭제된 데일리 목표가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenDeletedDailyGoalsDoNotExist() { + // given + given(dailyGoalCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L) + + // when + val result = dailyGoalCommandService.hardDeleteDailyGoals() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 데일리 목표가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenDeletedDailyGoalsExist() { + // given + given(dailyGoalCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L) + + // when + val result = dailyGoalCommandService.hardDeleteDailyGoals() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteDailyGoalsOwnedByDeletedTrip 메서드는") + inner class HardDeleteDailyGoalsOwnedByDeletedTrip { + @Test + @DisplayName("삭제된 여행이 소유한 데일리 목표가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenDailyGoalsOwnedByDeletedTripDoNotExist() { + // given + given(dailyGoalCommandRepository.deleteAllByDeletedTripOwner()).willReturn(0L) + + // when + val result = dailyGoalCommandService.hardDeleteDailyGoalsOwnedByDeletedTrip() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 여행이 소유한 데일리 목표가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenDailyGoalsOwnedByDeletedTripExist() { + // given + given(dailyGoalCommandRepository.deleteAllByDeletedTripOwner()).willReturn(5L) + + // when + val result = dailyGoalCommandService.hardDeleteDailyGoalsOwnedByDeletedTrip() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteDailyGoalsOwnedByMember 메서드는") + inner class HardDeleteDailyGoalsOwnedByMember { + @Test + @DisplayName("특정 멤버가 소유한 데일리 목표가 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenDailyGoalsOwnedByMemberDoNotExist() { + // given + val memberId = member.id + given(dailyGoalCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L) + + // when + val result = dailyGoalCommandService.hardDeleteDailyGoalsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("특정 멤버가 소유한 데일리 목표가 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenDailyGoalsOwnedByMemberExist() { + // given + val memberId = member.id + given(dailyGoalCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L) + + // when + val result = dailyGoalCommandService.hardDeleteDailyGoalsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(5L) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/application/service/DailyGoalQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/trip/application/service/DailyGoalQueryServiceTest.kt new file mode 100644 index 0000000..118c040 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/application/service/DailyGoalQueryServiceTest.kt @@ -0,0 +1,105 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.repository.DailyGoalRepository +import com.ject.studytrip.trip.fixture.DailyGoalFixture +import com.ject.studytrip.trip.fixture.TripFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.BDDMockito.given +import org.mockito.InjectMocks +import org.mockito.Mock +import java.util.Optional + +@DisplayName("DailyGoalQueryService 단위 테스트") +class DailyGoalQueryServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var dailyGoalQueryService: DailyGoalQueryService + + @Mock + private lateinit var dailyGoalRepository: DailyGoalRepository + + private lateinit var member: Member + private lateinit var trip: Trip + private lateinit var dailyGoal: DailyGoal + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + dailyGoal = DailyGoalFixture(trip).createWithId(1L) + } + + @Nested + @DisplayName("getValidDailyGoal 메서드는") + inner class GetValidDailyGoal { + @Test + @DisplayName("데일리 목표가 존재하지 않으면 예외가 발생한다.") + fun shouldThrowExceptionWhenDailyGoalDoesNotExist() { + // given + val dailyGoalId = -1L + given(dailyGoalRepository.findById(dailyGoalId)).willReturn(Optional.empty()) + + // when + val exception = assertThrows { dailyGoalQueryService.getValidDailyGoal(trip.id, dailyGoalId) } + + // then + assertThat(exception.message).isEqualTo(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.message) + } + + @Test + @DisplayName("특정 데일리 목표가 다른 여행에 속한다면 예외가 발생한다.") + fun shouldThrowExceptionWhenDailyGoalNotBelongToTrip() { + // given + val dailyGoalId = dailyGoal.id + val newTrip = TripFixture(member, TripCategory.COURSE).createWithId(2L) + given(dailyGoalRepository.findById(dailyGoalId)).willReturn(Optional.of(dailyGoal)) + + // when + val exception = assertThrows { dailyGoalQueryService.getValidDailyGoal(newTrip.id, dailyGoalId) } + + // then + assertThat(exception.message).isEqualTo(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.message) + } + + @Test + @DisplayName("데일리 목표가 이미 삭제되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenDailyGoalAlreadyDeleted() { + // given + val dailyGoalId = dailyGoal.id + dailyGoal.updateDeletedAt() + given(dailyGoalRepository.findById(dailyGoalId)).willReturn(Optional.of(dailyGoal)) + + // when + val exception = assertThrows { dailyGoalQueryService.getValidDailyGoal(trip.id, dailyGoalId) } + + // then + assertThat(exception.message).isEqualTo(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.message) + } + + @Test + @DisplayName("특정 여행에 속한 데일리 목표가 존재하면 데일리 목표를 조회하고 반환한다.") + fun shouldReturnDailyGoalWhenDailyGoalBelongsToTrip() { + // given + val dailyGoalId = dailyGoal.id + given(dailyGoalRepository.findById(dailyGoalId)).willReturn(Optional.of(dailyGoal)) + + // when + val result = dailyGoalQueryService.getValidDailyGoal(trip.id, dailyGoalId) + + // then + assertThat(result).isEqualTo(dailyGoal) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/application/service/TripCommandServiceTest.kt b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripCommandServiceTest.kt new file mode 100644 index 0000000..eacf188 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripCommandServiceTest.kt @@ -0,0 +1,343 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.trip.domain.error.TripErrorCode +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.repository.TripCommandRepository +import com.ject.studytrip.trip.domain.repository.TripRepository +import com.ject.studytrip.trip.fixture.CreateTripRequestFixture +import com.ject.studytrip.trip.fixture.TripFixture +import com.ject.studytrip.trip.fixture.UpdateTripRequestFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.given +import java.time.LocalDate + +@DisplayName("TripCommandService 단위 테스트") +class TripCommandServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var tripCommandService: TripCommandService + + @Mock + private lateinit var tripRepository: TripRepository + + @Mock + private lateinit var tripCommandRepository: TripCommandRepository + + private lateinit var member: Member + private lateinit var courseTrip: Trip + private lateinit var exploreTrip: Trip + + companion object { + private const val NEW_TRIP_NAME = "새로운 여행 이름" + private const val NEW_TRIP_MEMO = "새로운 여행 메모" + private const val NEW_TRIP_CATEGORY = "EXPLORE" + private val NEW_TRIP_END_DATE = LocalDate.now().plusDays(100) + } + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + courseTrip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + exploreTrip = TripFixture(member, TripCategory.EXPLORE).createWithId(2L) + } + + @Nested + @DisplayName("createTrip 메서드는") + inner class CreateTrip { + private val fixture = CreateTripRequestFixture() + + @Test + @DisplayName("코스형 여행의 종료일이 null이면 예외가 발생한다.") + fun shouldThrowExceptionWhenCourseTripEndDateIsNull() { + // given + val request = fixture.withCategory(TripCategory.COURSE.name).withEndDate(null).build() + + // when + val exception = assertThrows { tripCommandService.createTrip(member, request) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.COURSE_TRIP_END_DATE_REQUIRED.message) + } + + @Test + @DisplayName("요청이 유효하면 코스형 여행을 생성하고 반환한다.") + fun shouldCreateAndReturnCourseTripWhenRequestIsValid() { + // given + val request = fixture.withCategory(TripCategory.COURSE.name).build() + given(tripRepository.save(any())).willReturn(courseTrip) + + // when + val result = tripCommandService.createTrip(member, request) + + // then + assertThat(result).isEqualTo(courseTrip) + assertThat(result.category).isEqualTo(TripCategory.COURSE) + assertThat(result.member).isEqualTo(member) + } + + @Test + @DisplayName("요청이 유효하면 탐험형 여행을 생성하고 반환한다.") + fun shouldCreateAndReturnExploreTripWhenRequestIsValid() { + // given + val request = fixture.withCategory(TripCategory.EXPLORE.name).build() + given(tripRepository.save(any())).willReturn(exploreTrip) + + // when + val result = tripCommandService.createTrip(member, request) + + // then + assertThat(result).isEqualTo(exploreTrip) + assertThat(result.category).isEqualTo(TripCategory.EXPLORE) + assertThat(result.member).isEqualTo(member) + } + } + + @Nested + @DisplayName("updateTrip 메서드는") + inner class UpdateTrip { + val fixture = UpdateTripRequestFixture() + + @Test + @DisplayName("특정 여행의 이름을 수정한다.") + fun shouldUpdateTripWhenNameIsPresent() { + // given + val request = fixture.withName(NEW_TRIP_NAME).build() + val existingName = courseTrip.name + + // when + tripCommandService.updateTrip(courseTrip, request) + + // then + assertThat(courseTrip.name).isEqualTo(NEW_TRIP_NAME) + assertThat(courseTrip.name).isNotEqualTo(existingName) + } + + @Test + @DisplayName("특정 여행의 메모를 수정한다.") + fun shouldUpdateTripWhenMemoIsPresent() { + // given + val request = fixture.withMemo(NEW_TRIP_MEMO).build() + val existingMemo = courseTrip.memo + + // when + tripCommandService.updateTrip(courseTrip, request) + + // then + assertThat(courseTrip.memo).isEqualTo(NEW_TRIP_MEMO) + assertThat(courseTrip.memo).isNotEqualTo(existingMemo) + } + + @Test + @DisplayName("특정 여행의 카테고리를 수정한다.") + fun shouldUpdateTripWhenCategoryIsPresent() { + // given + val request = fixture.withCategory(NEW_TRIP_CATEGORY).build() + val existingCategory = courseTrip.category.name + + // when + tripCommandService.updateTrip(courseTrip, request) + + // then + assertThat(courseTrip.category.name).isEqualTo(NEW_TRIP_CATEGORY) + assertThat(courseTrip.category.name).isNotEqualTo(existingCategory) + } + + @Test + @DisplayName("특정 여행의 카테고리를 수정한다.") + fun shouldUpdateTripWhenEndDateIsPresent() { + // given + val request = fixture.withEndDate(NEW_TRIP_END_DATE).build() + val existingEndDate = courseTrip.endDate + + // when + tripCommandService.updateTrip(courseTrip, request) + + // then + assertThat(courseTrip.endDate).isEqualTo(NEW_TRIP_END_DATE) + assertThat(courseTrip.endDate).isNotEqualTo(existingEndDate) + } + } + + @Nested + @DisplayName("deleteTrip 메서드는") + inner class DeleteTrip { + @Test + @DisplayName("여행이 삭제될 때 deletedAt 필드를 현재 시간으로 업데이트한다. (소프트 삭제)") + fun shouldUpdateDeletedAtWhenTripIsDeleted() { + // when + tripCommandService.deleteTrip(courseTrip) + + // then + assertThat(courseTrip.deletedAt).isNotNull + } + } + + @Nested + @DisplayName("completeTrip 메서드는") + inner class CompleteTrip { + @Test + @DisplayName("미션이 완료될 때 completed 필드를 true로 업데이트한다.") + fun shouldUpdateCompletedWhenTripIsCompleted() { + // when + tripCommandService.completeTrip(courseTrip) + + // then + assertThat(courseTrip.isCompleted).isTrue + } + } + + @Nested + @DisplayName("increaseTotalStamps 메서드는") + inner class IncreaseTotalStamps { + @Test + @DisplayName("여행의 총 스탬프 수를 +1 증가시킨다.") + fun shouldIncreaseTotalStamps() { + // given + val existingTotalStamps = courseTrip.totalStamps + + // when + tripCommandService.increaseTotalStamps(courseTrip) + + // then + assertThat(courseTrip.totalStamps).isEqualTo(existingTotalStamps + 1) + } + } + + @Nested + @DisplayName("decreaseTotalStamps 메서드는") + inner class DecreaseTotalStamps { + @Test + @DisplayName("여행의 총 스탬프 수를 -1 감소시킨다.") + fun shouldDecreaseTotalStamps() { + // given + val existingTotalStamps = courseTrip.totalStamps + + // when + tripCommandService.decreaseTotalStamps(courseTrip) + + // then + assertThat(courseTrip.totalStamps).isEqualTo(existingTotalStamps - 1) + } + } + + @Nested + @DisplayName("increaseCompletedStamps 메서드는") + inner class IncreaseCompletedStamps { + @Test + @DisplayName("여행의 완료된 총 스탬프 수를 +1 증가시킨다.") + fun shouldIncreaseCompletedStamps() { + // given + val existingCompletedStamps = courseTrip.completedStamps + + // when + tripCommandService.increaseCompletedStamps(courseTrip) + + // then + assertThat(courseTrip.completedStamps).isEqualTo(existingCompletedStamps + 1) + } + } + + @Nested + @DisplayName("hardDeleteTrips 메서드는") + inner class HardDeleteTrips { + @Test + @DisplayName("삭제된 여행이 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenDeletedTripsDoNotExist() { + // given + given(tripCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(0L) + + // when + val result = tripCommandService.hardDeleteTrips() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 여행이 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenDeletedTripsExist() { + // given + given(tripCommandRepository.deleteAllByDeletedAtIsNotNull()).willReturn(5L) + + // when + val result = tripCommandService.hardDeleteTrips() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteTripsOwnedByDeletedMember 메서드는") + inner class HardDeleteTripsOwnedByDeletedMember { + @Test + @DisplayName("삭제된 멤버가 소유한 여행이 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenTripsOwnedByDeletedMemberDoNotExist() { + // given + given(tripCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(0L) + + // when + val result = tripCommandService.hardDeleteTripsOwnedByDeletedMember() + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("삭제된 멤버가 소유한 여행이 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenTripsOwnedByDeletedMemberExist() { + // given + given(tripCommandRepository.deleteAllByDeletedMemberOwner()).willReturn(5L) + + // when + val result = tripCommandService.hardDeleteTripsOwnedByDeletedMember() + + // then + assertThat(result).isEqualTo(5L) + } + } + + @Nested + @DisplayName("hardDeleteTripsByMember 메서드는") + inner class HardDeleteTripsByMember { + @Test + @DisplayName("특정 멤버가 소유한 여행이 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenTripsOwnedByMemberDoNotExist() { + // given + val memberId = member.id + given(tripCommandRepository.deleteAllByMemberId(memberId)).willReturn(0L) + + // when + val result = tripCommandService.hardDeleteTripsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(0L) + } + + @Test + @DisplayName("특정 멤버가 소유한 여행이 하나라도 존재하면 해당 개수를 반환한다.") + fun shouldReturnCountWhenTripsOwnedByMemberExist() { + // given + val memberId = member.id + given(tripCommandRepository.deleteAllByMemberId(memberId)).willReturn(5L) + + // when + val result = tripCommandService.hardDeleteTripsOwnedByMember(memberId) + + // then + assertThat(result).isEqualTo(5L) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/application/service/TripQueryServiceTest.kt b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripQueryServiceTest.kt new file mode 100644 index 0000000..cfc57ba --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/application/service/TripQueryServiceTest.kt @@ -0,0 +1,264 @@ +package com.ject.studytrip.trip.application.service + +import com.ject.studytrip.BaseUnitTest +import com.ject.studytrip.global.exception.CustomException +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.fixture.MemberFixture +import com.ject.studytrip.trip.domain.error.TripErrorCode +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.repository.TripQueryRepository +import com.ject.studytrip.trip.domain.repository.TripRepository +import com.ject.studytrip.trip.fixture.TripFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.kotlin.given +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.SliceImpl +import java.util.Optional + +@DisplayName("TripQueryService 단위 테스트") +class TripQueryServiceTest : BaseUnitTest() { + @InjectMocks + private lateinit var tripQueryService: TripQueryService + + @Mock + private lateinit var tripRepository: TripRepository + + @Mock + private lateinit var tripQueryRepository: TripQueryRepository + + private lateinit var member: Member + private lateinit var trip: Trip + + private val pageable: Pageable = PageRequest.of(DEFAULT_PAGE, DEFAULT_SIZE) + + companion object { + private const val DEFAULT_PAGE = 0 + private const val DEFAULT_SIZE = 5 + } + + @BeforeEach + fun setUp() { + member = MemberFixture.createMemberFromKakaoWithId(1L) + trip = TripFixture(member, TripCategory.COURSE).createWithId(1L) + } + + @Nested + @DisplayName("getValidTrip 메서드는") + inner class GetValidTrip { + @Test + @DisplayName("여행이 존재하지 않으면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripDoesNotExist() { + // given + val tripId = -1L + given(tripRepository.findById(tripId)).willReturn(Optional.empty()) + + // when + val exception = assertThrows { tripQueryService.getValidTrip(member.id, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.TRIP_NOT_FOUND.message) + } + + @Test + @DisplayName("멤버가 여행의 소유자가 아니라면 예외가 발생한다.") + fun shouldThrowExceptionWhenMemberIsNotTripOwner() { + // given + val memberId = -1L + val tripId = trip.id + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val exception = assertThrows { tripQueryService.getValidTrip(memberId, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.NOT_TRIP_OWNER.message) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripAlreadyDeleted() { + // given + val tripId = trip.id + trip.updateDeletedAt() + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val exception = assertThrows { tripQueryService.getValidTrip(member.id, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.TRIP_ALREADY_DELETED.message) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripAlreadyCompleted() { + // given + val tripId = trip.id + trip.updateCompleted() + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val exception = assertThrows { tripQueryService.getValidTrip(member.id, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.TRIP_ALREADY_COMPLETED.message) + } + + @Test + @DisplayName("여행이 존재하면 여행을 반환한다.") + fun shouldReturnTripWhenTripExists() { + // given + val tripId = trip.id + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val result = tripQueryService.getValidTrip(member.id, tripId) + + // then + assertThat(result).isEqualTo(trip) + } + } + + @Nested + @DisplayName("getTripsSliceByMemberId 메서드는") + inner class GetTripsSliceByMemberId { + @Test + @DisplayName("특정 멤버에 대한 여행 목록을 페이징 처리하여 반환한다.") + fun shouldReturnTripsSliceByMemberIdPaged() { + // given + val memberId = member.id + val trips = listOf(trip) + val mockSlice = SliceImpl(trips, pageable, false) + given(tripQueryRepository.findSliceByMemberIdAndCompletedFalseAndDeletedAtIsNull(memberId, pageable)).willReturn(mockSlice) + + // when + val result = tripQueryService.getTripsSliceByMemberId(memberId, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + assertThat(result.content).hasSize(trips.size) + assertThat(result).containsExactly(trip) + } + } + + @Nested + @DisplayName("getActiveTripCountByMemberId 메서드는") + inner class GetActiveTripCountByMemberId { + @Test + @DisplayName("특정 멤버의 여행이 하나라도 존재하지 않으면 0을 반환한다.") + fun shouldReturnZeroWhenTripDoesNotExistForMember() { + // given + val memberId = member.id + given(tripQueryRepository.countActiveTripsByMemberIdAndCategory(memberId, TripCategory.COURSE)).willReturn(0L) + given(tripQueryRepository.countActiveTripsByMemberIdAndCategory(memberId, TripCategory.EXPLORE)).willReturn(0L) + + // when + val result = tripQueryService.getActiveTripCountByMemberId(memberId) + + // then + assertThat(result.course).isEqualTo(0L) + assertThat(result.explore).isEqualTo(0L) + } + + @Test + @DisplayName("특정 멤버의 코스형, 탐험형 여행 개수를 TripCount에 담아서 반환한다.") + fun shouldReturnTripCountByMemberId() { + // given + val memberId = member.id + given(tripQueryRepository.countActiveTripsByMemberIdAndCategory(memberId, TripCategory.COURSE)).willReturn(3L) + given(tripQueryRepository.countActiveTripsByMemberIdAndCategory(memberId, TripCategory.EXPLORE)).willReturn(2L) + + // when + val result = tripQueryService.getActiveTripCountByMemberId(memberId) + + // then + assertThat(result.course).isEqualTo(3L) + assertThat(result.explore).isEqualTo(2L) + } + } + + @Nested + @DisplayName("getValidCompletedTrip 메서드는") + inner class GetValidCompletedTrip { + @Test + @DisplayName("여행이 존재하지 않으면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripDoesNotExist() { + // given + val tripId = -1L + given(tripRepository.findById(tripId)).willReturn(Optional.empty()) + + // when + val exception = assertThrows { tripQueryService.getValidCompletedTrip(member.id, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.TRIP_NOT_FOUND.message) + } + + @Test + @DisplayName("멤버가 여행의 소유자가 아니라면 예외가 발생한다.") + fun shouldThrowExceptionWhenMemberIsNotTripOwner() { + // given + val memberId = -1L + val tripId = trip.id + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val exception = assertThrows { tripQueryService.getValidCompletedTrip(memberId, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.NOT_TRIP_OWNER.message) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripAlreadyDeleted() { + // given + val tripId = trip.id + trip.updateDeletedAt() + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val exception = assertThrows { tripQueryService.getValidCompletedTrip(member.id, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.TRIP_ALREADY_DELETED.message) + } + + @Test + @DisplayName("여행이 아직 완료되지 않았다면 예외가 발생한다.") + fun shouldThrowExceptionWhenTripIsNotCompleted() { + // given + val tripId = trip.id + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val exception = assertThrows { tripQueryService.getValidCompletedTrip(member.id, tripId) } + + // then + assertThat(exception.message).isEqualTo(TripErrorCode.TRIP_NOT_COMPLETED.message) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 여행을 반환한다.") + fun shouldReturnTripWhenTripAlreadyCompleted() { + // given + val tripId = trip.id + trip.updateCompleted() + given(tripRepository.findById(tripId)).willReturn(Optional.of(trip)) + + // when + val result = tripQueryService.getValidCompletedTrip(member.id, tripId) + + // then + assertThat(result).isEqualTo(trip) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateDailyGoalRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateDailyGoalRequestFixture.kt new file mode 100644 index 0000000..586023d --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateDailyGoalRequestFixture.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.pomodoro.presentation.dto.request.CreatePomodoroRequest +import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest + +class CreateDailyGoalRequestFixture { + var pomodoro: CreatePomodoroRequest = CreatePomodoroRequest(30, 1) + var missionIds: List = emptyList() + + fun withPomodoro(pomodoro: CreatePomodoroRequest): CreateDailyGoalRequestFixture = apply { this.pomodoro = pomodoro } + + fun withMissionIds(missionIds: List): CreateDailyGoalRequestFixture = apply { this.missionIds = missionIds } + + fun build(): CreateDailyGoalRequest = CreateDailyGoalRequest(pomodoro, missionIds) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateTripRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateTripRequestFixture.kt new file mode 100644 index 0000000..0c29c24 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/CreateTripRequestFixture.kt @@ -0,0 +1,20 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.stamp.presentation.dto.request.CreateStampRequest +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest +import java.time.LocalDate + +class CreateTripRequestFixture { + var name: String = "TEST 여행 이름" + var memo: String = "TEST 여행 메모" + var category: String = TripCategory.COURSE.name + var endDate: LocalDate? = LocalDate.now().plusDays(10) + var stamps: List = listOf(CreateStampRequest("TEST 스탬프 이름", LocalDate.now().plusDays(5))) + + fun withCategory(category: String): CreateTripRequestFixture = apply { this.category = category } + + fun withEndDate(endDate: LocalDate?): CreateTripRequestFixture = apply { this.endDate = endDate } + + fun build(): CreateTripRequest = CreateTripRequest(name, memo, category, endDate, stamps) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/DailyGoalFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/DailyGoalFixture.kt new file mode 100644 index 0000000..3fb57de --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/DailyGoalFixture.kt @@ -0,0 +1,19 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.trip.domain.factory.DailyGoalFactory +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip +import org.springframework.test.util.ReflectionTestUtils + +class DailyGoalFixture( + private val trip: Trip, +) { + var title: String = "TEST 데일리 목표 제목" + + fun create(): DailyGoal = DailyGoalFactory.create(trip, title) + + fun createWithId(id: Long): DailyGoal = + create().also { + ReflectionTestUtils.setField(it, "id", id) + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/TripFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/TripFixture.kt new file mode 100644 index 0000000..74bae7a --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/TripFixture.kt @@ -0,0 +1,25 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.factory.TripFactory +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import org.springframework.test.util.ReflectionTestUtils +import java.time.LocalDate + +class TripFixture( + private val member: Member, + private val category: TripCategory, +) { + var name: String = "TEST 여행 이름" + var memo: String = "TEST 여행 메모" + var endDate: LocalDate = LocalDate.now().plusDays(7) + var totalStamps: Int = 1 + + fun create(): Trip = TripFactory.create(member, name, memo, category, endDate, totalStamps) + + fun createWithId(id: Long): Trip = + create().also { + ReflectionTestUtils.setField(it, "id", id) + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/UpdateDailyGoalRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/UpdateDailyGoalRequestFixture.kt new file mode 100644 index 0000000..92dee22 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/UpdateDailyGoalRequestFixture.kt @@ -0,0 +1,15 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest + +class UpdateDailyGoalRequestFixture { + var deleteDailyMissionIds: List = emptyList() + var addMissionIds: List = emptyList() + + fun withDeleteDailyMissionIds(deleteDailyMissionIds: List): UpdateDailyGoalRequestFixture = + apply { this.deleteDailyMissionIds = deleteDailyMissionIds } + + fun withAddMissionIds(addMissionIds: List): UpdateDailyGoalRequestFixture = apply { this.addMissionIds = addMissionIds } + + fun build(): UpdateDailyGoalRequest = UpdateDailyGoalRequest(deleteDailyMissionIds, addMissionIds) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/fixture/UpdateTripRequestFixture.kt b/src/test/kotlin/com/ject/studytrip/trip/fixture/UpdateTripRequestFixture.kt new file mode 100644 index 0000000..194a741 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/fixture/UpdateTripRequestFixture.kt @@ -0,0 +1,33 @@ +package com.ject.studytrip.trip.fixture + +import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest +import java.time.LocalDate + +class UpdateTripRequestFixture { + private var name: String? = null + private var memo: String? = null + private var category: String? = null + private var endDate: LocalDate? = null + + fun withName(name: String): UpdateTripRequestFixture { + this.name = name + return this + } + + fun withMemo(memo: String): UpdateTripRequestFixture { + this.memo = memo + return this + } + + fun withCategory(category: String): UpdateTripRequestFixture { + this.category = category + return this + } + + fun withEndDate(endDate: LocalDate): UpdateTripRequestFixture { + this.endDate = endDate + return this + } + + fun build(): UpdateTripRequest = UpdateTripRequest(name, memo, category, endDate) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/helper/DailyGoalTestHelper.kt b/src/test/kotlin/com/ject/studytrip/trip/helper/DailyGoalTestHelper.kt new file mode 100644 index 0000000..fa70d65 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/helper/DailyGoalTestHelper.kt @@ -0,0 +1,17 @@ +package com.ject.studytrip.trip.helper + +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.repository.DailyGoalRepository +import com.ject.studytrip.trip.fixture.DailyGoalFixture +import org.springframework.stereotype.Component + +@Component +class DailyGoalTestHelper( + private val dailyGoalRepository: DailyGoalRepository, +) { + fun saveDailyGoal(trip: Trip): DailyGoal = dailyGoalRepository.save(DailyGoalFixture(trip).create()) + + fun saveDeletedDailyGoal(trip: Trip): DailyGoal = + dailyGoalRepository.save(DailyGoalFixture(trip).create().also { it.updateDeletedAt() }) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/helper/TripTestHelper.kt b/src/test/kotlin/com/ject/studytrip/trip/helper/TripTestHelper.kt new file mode 100644 index 0000000..a8edd91 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/helper/TripTestHelper.kt @@ -0,0 +1,28 @@ +package com.ject.studytrip.trip.helper + +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.domain.repository.TripRepository +import com.ject.studytrip.trip.fixture.TripFixture +import org.springframework.stereotype.Component + +@Component +class TripTestHelper( + private val tripRepository: TripRepository, +) { + fun saveTrip( + member: Member, + category: TripCategory, + ): Trip = tripRepository.save(TripFixture(member, category).create()) + + fun saveDeletedTrip( + member: Member, + category: TripCategory, + ): Trip = tripRepository.save(TripFixture(member, category).create().also { it.updateDeletedAt() }) + + fun saveCompletedTrip( + member: Member, + category: TripCategory, + ): Trip = tripRepository.save(TripFixture(member, category).create().also { it.updateCompleted() }) +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/DailyGoalControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/DailyGoalControllerIntegrationTest.kt new file mode 100644 index 0000000..8011350 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/DailyGoalControllerIntegrationTest.kt @@ -0,0 +1,1217 @@ +package com.ject.studytrip.trip.presentation.controller + +import com.ject.studytrip.BaseIntegrationTest +import com.ject.studytrip.auth.domain.error.AuthErrorCode +import com.ject.studytrip.auth.fixture.TokenFixture +import com.ject.studytrip.auth.helper.TokenTestHelper +import com.ject.studytrip.global.exception.error.CommonErrorCode +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.domain.model.MemberRole +import com.ject.studytrip.member.helper.MemberTestHelper +import com.ject.studytrip.mission.domain.error.DailyMissionErrorCode +import com.ject.studytrip.mission.domain.error.MissionErrorCode +import com.ject.studytrip.mission.domain.model.DailyMission +import com.ject.studytrip.mission.domain.model.Mission +import com.ject.studytrip.mission.helper.DailyMissionTestHelper +import com.ject.studytrip.mission.helper.MissionTestHelper +import com.ject.studytrip.pomodoro.domain.error.PomodoroErrorCode +import com.ject.studytrip.pomodoro.fixture.CreatePomodoroRequestFixture +import com.ject.studytrip.pomodoro.helper.PomodoroTestHelper +import com.ject.studytrip.stamp.domain.error.StampErrorCode +import com.ject.studytrip.stamp.domain.model.Stamp +import com.ject.studytrip.stamp.helper.StampTestHelper +import com.ject.studytrip.trip.domain.error.DailyGoalErrorCode +import com.ject.studytrip.trip.domain.error.TripErrorCode +import com.ject.studytrip.trip.domain.model.DailyGoal +import com.ject.studytrip.trip.domain.model.Trip +import com.ject.studytrip.trip.domain.model.TripCategory +import com.ject.studytrip.trip.fixture.CreateDailyGoalRequestFixture +import com.ject.studytrip.trip.fixture.UpdateDailyGoalRequestFixture +import com.ject.studytrip.trip.helper.DailyGoalTestHelper +import com.ject.studytrip.trip.helper.TripTestHelper +import com.ject.studytrip.trip.presentation.dto.request.CreateDailyGoalRequest +import com.ject.studytrip.trip.presentation.dto.request.UpdateDailyGoalRequest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@DisplayName("DailyGoalController 통합 테스트") +class DailyGoalControllerIntegrationTest : BaseIntegrationTest() { + @Autowired private lateinit var memberTestHelper: MemberTestHelper + + @Autowired private lateinit var tokenTestHelper: TokenTestHelper + + @Autowired private lateinit var tripTestHelper: TripTestHelper + + @Autowired private lateinit var stampTestHelper: StampTestHelper + + @Autowired private lateinit var missionTestHelper: MissionTestHelper + + @Autowired private lateinit var dailyGoalTestHelper: DailyGoalTestHelper + + @Autowired private lateinit var dailyMissionTestHelper: DailyMissionTestHelper + + @Autowired private lateinit var pomodoroTestHelper: PomodoroTestHelper + + private lateinit var member: Member + private lateinit var token: String + private lateinit var trip: Trip + private lateinit var stamp: Stamp + private lateinit var mission1: Mission + private lateinit var mission2: Mission + private lateinit var dailyGoal: DailyGoal + private lateinit var dailyMission: DailyMission + + // 새로운 여행 + private lateinit var newTrip: Trip + + @BeforeEach + fun setUp() { + member = memberTestHelper.saveMember() + token = tokenTestHelper.createAccessToken(member.id.toString(), MemberRole.ROLE_USER.name) + trip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + stamp = stampTestHelper.saveStamp(trip, 1) + mission1 = missionTestHelper.saveMission(stamp) + mission2 = missionTestHelper.saveMission(stamp) + dailyGoal = dailyGoalTestHelper.saveDailyGoal(trip) + dailyMission = dailyMissionTestHelper.saveDailyMission(mission1, dailyGoal) + + val newMember = memberTestHelper.saveMember("test@gmail.com", "test") + newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE) + } + + companion object { + private const val BASE_DAILY_GOAL_URL = "/api/trips/{tripId}/daily-goals" + } + + @Nested + @DisplayName("데일리 목표 생성 API") + inner class CreateDailyGoal { + private val fixture = CreateDailyGoalRequestFixture() + + private fun getResultActions( + token: String, + tripId: Any, + request: CreateDailyGoalRequest, + ): ResultActions = + mockMvc.perform( + post(BASE_DAILY_GOAL_URL, tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions("", trip.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("CreateDailyGoalRequest 뽀모도로 집중 시간이 1분 미만이면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalPomodoroFocusTimeInMinuteIsLessThanOneMinute() { + // given + val pomodoro = CreatePomodoroRequestFixture().withFocusDurationInMinute(0).build() + val request = fixture.withPomodoro(pomodoro).withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("CreateDailyGoalRequest 뽀모도로 집중 세션이 1개 미만이면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalPomodoroFocusSessionCountIsLessThanOne() { + // given + val pomodoro = CreatePomodoroRequestFixture().withFocusSessionCount(0).build() + val request = fixture.withPomodoro(pomodoro).withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // given + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, newTrip.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, deletedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, completedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("어떤 미션이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenAnyMissionDoesNotExist() { + // given + val request = fixture.withMissionIds(listOf(mission1.id, -1L)).build() + + // when + val resultActions = getResultActions(token, trip.id, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_NOT_FOUND.message)) + } + + @Test + @DisplayName("어떤 미션이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenAnyMissionAlreadyDeleted() { + // given + val deletedMission = missionTestHelper.saveDeletedMission(stamp) + val request = fixture.withMissionIds(listOf(mission1.id, deletedMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("어떤 미션이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenAnyMissionAlreadyCompleted() { + // given + val completedMission = missionTestHelper.saveCompletedMission(stamp) + val request = fixture.withMissionIds(listOf(mission1.id, completedMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("어떤 미션의 스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenAnyMissionStampNotBelongToTrip() { + // given + val otherTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + stampTestHelper.saveStamp(otherTrip, 2) + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, otherTrip.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("특정 코스형 여행에서 진행 중인 스탬프가 존재하지 않으면 예외가 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenProgressStampDoesNotExistForCourseTrip() { + // given + val otherTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + val completedStamp = stampTestHelper.saveCompletedStamp(otherTrip, 2) + val otherMission = missionTestHelper.saveMission(completedStamp) + val request = fixture.withMissionIds(listOf(otherMission.id)).build() + + // when + val resultActions = getResultActions(token, otherTrip.id, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_FOUND.message)) + } + + @Test + @DisplayName("어떤 미션이 스탬프에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnBadRequestWhenMissionNotBelongToStamp() { + // given + val newStamp = stampTestHelper.saveStamp(trip, 2) + val newMission = missionTestHelper.saveMission(newStamp) + val request = fixture.withMissionIds(listOf(newMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_NOT_BELONGS_TO_STAMP.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_NOT_BELONGS_TO_STAMP.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 데일리 목표, 뽀모도로, 데일리 미션들을 생성하고, 데일리 목표를 반환한다.") + fun shouldCreateAndReturnDailyGoalWhenRequestIsValid() { + // given + val request = fixture.withMissionIds(listOf(mission1.id, mission2.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, request) + + // then + resultActions + .andExpect(status().isCreated) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.dailyGoalId").isNumber) + } + } + + @Nested + @DisplayName("데일리 목표 수정 API") + inner class UpdateDailyGoal { + private val fixture = UpdateDailyGoalRequestFixture() + + private fun getResultActions( + token: String, + tripId: Any, + dailyGoalId: Any, + request: UpdateDailyGoalRequest, + ): ResultActions = + mockMvc.perform( + patch("$BASE_DAILY_GOAL_URL/{dailyGoalId}", tripId, dailyGoalId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("PathVariable 데일리 목표 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalIdTypeMismatch() { + // given + val dailyGoalId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoalId, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, newTrip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, deletedTrip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, completedTrip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("데일리 목표가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenDailyGoalDoesNotExist() { + // given + val dailyGoalId = -1L + val request = fixture.build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoalId, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.message)) + } + + @Test + @DisplayName("데일리 목표가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenDailyGoalNotBelongToTrip() { + // given + val otherTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, otherTrip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("데일리 목표가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalAlreadyDeleted() { + // given + val deletedDailyGoal = dailyGoalTestHelper.saveDeletedDailyGoal(trip) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, trip.id, deletedDailyGoal.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("어떤 데일리 미션이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenAnyDailyMissionDoesNotExist() { + // given + val request = fixture.withDeleteDailyMissionIds(listOf(dailyMission.id, -1L)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyMissionErrorCode.DAILY_MISSION_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyMissionErrorCode.DAILY_MISSION_NOT_FOUND.message)) + } + + @Test + @DisplayName("어떤 데일리 미션이 요청한 데일리 목표에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenAnyDailyMissionNotBelongToDailyGoal() { + // given + val otherDailyGoal = dailyGoalTestHelper.saveDeletedDailyGoal(trip) + val otherDailyMission = dailyMissionTestHelper.saveDailyMission(mission1, otherDailyGoal) + val request = fixture.withDeleteDailyMissionIds(listOf(otherDailyMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyMissionErrorCode.DAILY_MISSION_NOT_BELONGS_TO_DAILY_GOAL.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyMissionErrorCode.DAILY_MISSION_NOT_BELONGS_TO_DAILY_GOAL.message)) + } + + @Test + @DisplayName("어떤 데일리 미션이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenAnyDailyMissionAlreadyDeleted() { + // given + val deletedDailyMission = dailyMissionTestHelper.saveDeletedDailyMission(mission1, dailyGoal) + val request = fixture.withDeleteDailyMissionIds(listOf(dailyMission.id, deletedDailyMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyMissionErrorCode.DAILY_MISSION_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyMissionErrorCode.DAILY_MISSION_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("어떤 미션이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenAnyMissionDoesNotExist() { + // given + val request = fixture.withAddMissionIds(listOf(mission1.id, mission2.id, -1L)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_NOT_FOUND.message)) + } + + @Test + @DisplayName("어떤 미션이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenAnyMissionAlreadyDeleted() { + // given + val deletedMission = missionTestHelper.saveDeletedMission(stamp) + val request = fixture.withAddMissionIds(listOf(mission1.id, deletedMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("어떤 미션이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenAnyMissionAlreadyCompleted() { + // given + val completedMission = missionTestHelper.saveCompletedMission(stamp) + val request = fixture.withAddMissionIds(listOf(mission1.id, completedMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("어떤 미션의 스탬프가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenAnyMissionStampNotBelongToTrip() { + // given + val otherTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + val otherStamp = stampTestHelper.saveStamp(otherTrip, 2) + val otherMission = missionTestHelper.saveMission(otherStamp) + val request = fixture.withAddMissionIds(listOf(otherMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("특정 코스형 여행에서 진행 중인 스탬프가 존재하지 않으면 예외가 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenProgressStampDoesNotExistForCourseTrip() { + // given + val otherTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + val completedStamp = stampTestHelper.saveCompletedStamp(otherTrip, 2) + val otherDailyGoal = dailyGoalTestHelper.saveDailyGoal(otherTrip) + val otherMission = missionTestHelper.saveMission(completedStamp) + val request = fixture.withAddMissionIds(listOf(otherMission.id)).build() + + // when + val resultActions = getResultActions(token, otherTrip.id, otherDailyGoal.id, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.STAMP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.STAMP_NOT_FOUND.message)) + } + + @Test + @DisplayName("어떤 미션이 스탬프에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnBadRequestWhenMissionNotBelongToStamp() { + // given + val newStamp = stampTestHelper.saveStamp(trip, 2) + val newMission = missionTestHelper.saveMission(newStamp) + val request = fixture.withAddMissionIds(listOf(newMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(MissionErrorCode.MISSION_NOT_BELONGS_TO_STAMP.status.value())) + .andExpect(jsonPath("$.data.message").value(MissionErrorCode.MISSION_NOT_BELONGS_TO_STAMP.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 데일리 목표를 수정한다.") + fun shouldUpdateDailyGoalWhenRequestIsValid() { + // given + val request = fixture.withDeleteDailyMissionIds(listOf(dailyMission.id)).build() + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id, request) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("데일리 목표 삭제 API") + inner class DeleteDailyGoal { + private fun getResultActions( + token: String, + tripId: Any, + dailyGoalId: Any, + ): ResultActions = + mockMvc.perform( + delete("$BASE_DAILY_GOAL_URL/{dailyGoalId}", tripId, dailyGoalId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + + // when + val resultActions = getResultActions(token, tripId, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("PathVariable 데일리 목표 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalIdTypeMismatch() { + // given + val dailyGoalId = "abc" + + // when + val resultActions = getResultActions(token, trip.id, dailyGoalId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId, dailyGoal.id) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("데일리 목표가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenDailyGoalDoesNotExist() { + // given + val dailyGoalId = -1L + + // when + val resultActions = getResultActions(token, trip.id, dailyGoalId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.message)) + } + + @Test + @DisplayName("데일리 목표가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenDailyGoalNotBelongToTrip() { + // given + val newDailyGoal = dailyGoalTestHelper.saveDailyGoal(newTrip) + pomodoroTestHelper.savePomodoro(newDailyGoal) + + // when + val resultActions = getResultActions(token, trip.id, newDailyGoal.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("데일리 목표가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalAlreadyDeleted() { + // given + val deletedDailyGoal = dailyGoalTestHelper.saveDeletedDailyGoal(trip) + + // when + val resultActions = getResultActions(token, trip.id, deletedDailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("뽀모도로가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenPomodoroDoesNotExist() { + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(PomodoroErrorCode.POMODORO_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(PomodoroErrorCode.POMODORO_NOT_FOUND.message)) + } + + @Test + @DisplayName("뽀모도로가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPomodoroAlreadyDeleted() { + // given + pomodoroTestHelper.saveDeletedPomodoro(dailyGoal) + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(PomodoroErrorCode.POMODORO_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(PomodoroErrorCode.POMODORO_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("데일리 목표, 뽀모도로, 데일리 미션들을 삭제한다.") + fun shouldDeleteDailyGoalAndPomodoroAndDailyMissions() { + // given + pomodoroTestHelper.savePomodoro(dailyGoal) + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("데일리 목표 상세 조회 API") + inner class LoadDailyGoal { + private fun getResultActions( + token: String, + tripId: Any, + dailyGoalId: Any, + ): ResultActions = + mockMvc.perform( + get("$BASE_DAILY_GOAL_URL/{dailyGoalId}", tripId, dailyGoalId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + + // when + val resultActions = getResultActions(token, tripId, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("PathVariable 데일리 목표 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalIdTypeMismatch() { + // given + val dailyGoalId = "abc" + + // when + val resultActions = getResultActions(token, trip.id, dailyGoalId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId, dailyGoal.id) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("데일리 목표가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenDailyGoalDoesNotExist() { + // given + val dailyGoalId = -1L + + // when + val resultActions = getResultActions(token, trip.id, dailyGoalId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_FOUND.message)) + } + + @Test + @DisplayName("데일리 목표가 요청한 여행에 속하지 않으면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenDailyGoalNotBelongToTrip() { + // given + val newDailyGoal = dailyGoalTestHelper.saveDailyGoal(newTrip) + pomodoroTestHelper.savePomodoro(newDailyGoal) + + // when + val resultActions = getResultActions(token, trip.id, newDailyGoal.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_NOT_BELONGS_TO_TRIP.message)) + } + + @Test + @DisplayName("데일리 목표가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenDailyGoalAlreadyDeleted() { + // given + val deletedDailyGoal = dailyGoalTestHelper.saveDeletedDailyGoal(trip) + + // when + val resultActions = getResultActions(token, trip.id, deletedDailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(DailyGoalErrorCode.DAILY_GOAL_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("뽀모도로가 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenPomodoroDoesNotExist() { + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(PomodoroErrorCode.POMODORO_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(PomodoroErrorCode.POMODORO_NOT_FOUND.message)) + } + + @Test + @DisplayName("뽀모도로가 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPomodoroAlreadyDeleted() { + // given + pomodoroTestHelper.saveDeletedPomodoro(dailyGoal) + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(PomodoroErrorCode.POMODORO_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(PomodoroErrorCode.POMODORO_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("특정 데일리 목표를 상세 조회하고 반환한다.") + fun shouldReturnDailyGoal() { + // given + pomodoroTestHelper.savePomodoro(dailyGoal) + + // when + val resultActions = getResultActions(token, trip.id, dailyGoal.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } +} diff --git a/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.kt b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.kt new file mode 100644 index 0000000..8b3c9e6 --- /dev/null +++ b/src/test/kotlin/com/ject/studytrip/trip/presentation/controller/TripControllerIntegrationTest.kt @@ -0,0 +1,850 @@ +package com.ject.studytrip.trip.presentation.controller + +import com.ject.studytrip.BaseIntegrationTest +import com.ject.studytrip.auth.domain.error.AuthErrorCode +import com.ject.studytrip.auth.fixture.TokenFixture +import com.ject.studytrip.auth.helper.TokenTestHelper +import com.ject.studytrip.global.exception.error.CommonErrorCode +import com.ject.studytrip.member.domain.model.Member +import com.ject.studytrip.member.domain.model.MemberRole +import com.ject.studytrip.member.helper.MemberTestHelper +import com.ject.studytrip.stamp.domain.error.StampErrorCode +import com.ject.studytrip.stamp.helper.StampTestHelper +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.fixture.CreateTripRequestFixture +import com.ject.studytrip.trip.fixture.UpdateTripRequestFixture +import com.ject.studytrip.trip.helper.TripTestHelper +import com.ject.studytrip.trip.presentation.dto.request.CreateTripRequest +import com.ject.studytrip.trip.presentation.dto.request.UpdateTripRequest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.time.LocalDate + +@DisplayName("TripController 통합 테스트") +class TripControllerIntegrationTest : BaseIntegrationTest() { + @Autowired private lateinit var memberTestHelper: MemberTestHelper + + @Autowired private lateinit var tokenTestHelper: TokenTestHelper + + @Autowired private lateinit var tripTestHelper: TripTestHelper + + @Autowired private lateinit var stampTestHelper: StampTestHelper + + private lateinit var member: Member + private lateinit var token: String + private lateinit var courseTrip: Trip + + // 새로운 여행 + private lateinit var newTrip: Trip + + @BeforeEach + fun setUp() { + member = memberTestHelper.saveMember() + token = tokenTestHelper.createAccessToken(member.id.toString(), MemberRole.ROLE_USER.name) + courseTrip = tripTestHelper.saveTrip(member, TripCategory.COURSE) + + val newMember = memberTestHelper.saveMember("test@gmail.com", "test") + newTrip = tripTestHelper.saveTrip(newMember, TripCategory.COURSE) + } + + companion object { + private const val BASE_TRIP_URL = "/api/trips" + private const val DEFAULT_PAGE: String = "0" + private const val DEFAULT_SIZE: String = "5" + } + + @Nested + @DisplayName("여행 생성 API") + inner class CreateTrip { + private val fixture = CreateTripRequestFixture() + + private fun getResultActions( + token: String, + request: CreateTripRequest, + ): ResultActions = + mockMvc.perform( + post(BASE_TRIP_URL) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("CreateMissionRequest 카테고리가 유효하지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenRequestCategoryIsInvalid() { + // given + val request = fixture.withCategory("TEST").build() + + // when + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("CreateMissionRequest 종료일이 과거이면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenRequestEndDateIsInThePast() { + // given + val request = fixture.withEndDate(LocalDate.now().minusDays(10)).build() + + // when + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("여행 카테고리가 COURSE이지만, 종료일이 null이라면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenRequestEndDateIsNullForCourseTrip() { + // given + val request = fixture.withCategory(TripCategory.COURSE.name).withEndDate(null).build() + + // when + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.COURSE_TRIP_END_DATE_REQUIRED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.COURSE_TRIP_END_DATE_REQUIRED.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 코스형 여행을 생성하고 반환한다.") + fun shouldCreateAndReturnCourseTripWhenRequestIsValid() { + // given + val request = fixture.build() + + // then + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isCreated) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.tripId").isNumber) + } + + @Test + @DisplayName("유효한 요청이 들어오면 탐험형 여행을 생성하고 반환한다.") + fun shouldCreateAndReturnExploreTripWhenRequestIsValid() { + // given + val request = fixture.withCategory(TripCategory.EXPLORE.name).withEndDate(null).build() + + // then + val resultActions = getResultActions(token, request) + + // then + resultActions + .andExpect(status().isCreated) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.tripId").isNumber) + } + } + + @Nested + @DisplayName("여행 수정 API") + inner class UpdateTrip { + private val fixture = UpdateTripRequestFixture() + + private fun getResultActions( + token: String, + tripId: Any, + request: UpdateTripRequest, + ): ResultActions = + mockMvc.perform( + patch("$BASE_TRIP_URL/{tripId}", tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions("", courseTrip.id, request) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("UpdateMissionRequest 카테고리가 유효하지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenRequestCategoryIsInvalid() { + // given + val request = fixture.withCategory("TEST").build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("UpdateMissionRequest 종료일이 과거이면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenRequestEndDateIsInThePast() { + // given + val request = fixture.withEndDate(LocalDate.now().minusDays(10)).build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + val request = fixture.build() + + // when + val resultActions = getResultActions(token, tripId, request) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // given + val request = fixture.build() + + // when + val resultActions = getResultActions(token, newTrip.id, request) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, deletedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + val request = fixture.build() + + // when + val resultActions = getResultActions(token, completedTrip.id, request) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("유효한 요청이 들어오면 여행을 수정한다.") + fun shouldUpdateTripWhenRequestIsValid() { + // given + val request = fixture.withEndDate(LocalDate.now().plusDays(3)).build() + + // when + val resultActions = getResultActions(token, courseTrip.id, request) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("여행 삭제 API") + inner class DeleteTrip { + private fun getResultActions( + token: String, + tripId: Any, + ): ResultActions = + mockMvc.perform( + delete("$BASE_TRIP_URL/{tripId}", tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", courseTrip.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("특정 여행을 수정한다.") + fun shouldDeleteTrip() { + // when + val resultActions = getResultActions(token, courseTrip.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("여행 완료 API") + inner class CompleteTrip { + private fun getResultActions( + token: String, + tripId: Any, + ): ResultActions = + mockMvc.perform( + patch("$BASE_TRIP_URL/{tripId}/complete", tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", courseTrip.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("특정 여행의 어떤 스탬프가 완료되지 않았다면 예외가 발생한다.") + fun shouldReturnBadRequestWhenAnyStampIsNotCompleted() { + // given + stampTestHelper.saveStamp(courseTrip, 1) + + // when + val resultActions = getResultActions(token, courseTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(StampErrorCode.ALL_STAMPS_NOT_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(StampErrorCode.ALL_STAMPS_NOT_COMPLETED.message)) + } + + @Test + @DisplayName("특정 여행의 모든 스탬프가 완료되었다면 여행을 완료한다.") + fun shouldCompleteStampWhenAllStampsAreCompleted() { + // given + stampTestHelper.saveCompletedStamp(courseTrip, 1) + stampTestHelper.saveCompletedStamp(courseTrip, 2) + + // when + val resultActions = getResultActions(token, courseTrip.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + } + } + + @Nested + @DisplayName("여행 카테고리 목록 조회 API") + inner class LoadTripCategories { + private fun getResultActions(): ResultActions = mockMvc.perform(get("$BASE_TRIP_URL/categories")) + + @Test + @DisplayName("여행 카테고리 목록을 조회하고 반환한다.") + fun shouldReturnTripCategories() { + // when + val resultActions = getResultActions() + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } + + @Nested + @DisplayName("여행 목록 조회 API") + inner class LoadTrips { + private fun getResultActions( + token: String, + page: String, + size: String, + ): ResultActions = + mockMvc.perform( + get(BASE_TRIP_URL) + .param("page", page) + .param("size", size) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("Request Param 페이징 데이터 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPagingParameterTypeMismatch() { + // given + val page = "abc" + val size = "abc" + + // when + val resultActions = getResultActions(token, page, size) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("Request Param 페이징 데이터가 유효하지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenPagingParameterIsInvalid() { + // given + val page = "-1" + val size = "-1" + + // when + val resultActions = getResultActions(token, page, size) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.CONSTRAINT_VIOLATION.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.CONSTRAINT_VIOLATION.message)) + } + + @Test + @DisplayName("여행 목록을 슬라이스 처리하여 반환한다.") + fun shouldReturnTripSliceByMember() { + // when + val resultActions = getResultActions(token, DEFAULT_PAGE, DEFAULT_SIZE) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data.tripInfos").isNotEmpty) + .andExpect(jsonPath("$.data.hasNext").value(false)) + } + } + + @Nested + @DisplayName("여행 상세 조회 API") + inner class LoadTrip { + private fun getResultActions( + token: String, + tripId: Any, + ): ResultActions = + mockMvc.perform( + get("$BASE_TRIP_URL/{tripId}", tripId) + .header(HttpHeaders.AUTHORIZATION, TokenFixture.authorization(token)), + ) + + @Test + @DisplayName("인증되지 않은 사용자라면 401 Unauthorized를 반환한다.") + fun shouldReturnUnauthorizedWhenUnauthenticated() { + // when + val resultActions = getResultActions("", courseTrip.id) + + // then + resultActions + .andExpect(status().isUnauthorized) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(AuthErrorCode.UNAUTHENTICATED.status.value())) + .andExpect(jsonPath("$.data.message").value(AuthErrorCode.UNAUTHENTICATED.message)) + } + + @Test + @DisplayName("PathVariable 여행 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripIdTypeMismatch() { + // given + val tripId = "abc" + + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.status.value())) + .andExpect(jsonPath("$.data.message").value(CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH.message)) + } + + @Test + @DisplayName("여행이 존재하지 않으면 404 NotFound를 반환한다.") + fun shouldReturnNotFoundWhenTripDoesNotExist() { + // given + val tripId = -1L + + // when + val resultActions = getResultActions(token, tripId) + + // then + resultActions + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_NOT_FOUND.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_NOT_FOUND.message)) + } + + @Test + @DisplayName("여행의 소유자가 아니라면 403 Forbidden을 반환한다.") + fun shouldReturnForbiddenWhenMemberIsNotTripOwner() { + // when + val resultActions = getResultActions(token, newTrip.id) + + // then + resultActions + .andExpect(status().isForbidden) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.NOT_TRIP_OWNER.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.NOT_TRIP_OWNER.message)) + } + + @Test + @DisplayName("여행이 이미 삭제되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyDeleted() { + // given + val deletedTrip = tripTestHelper.saveDeletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, deletedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_DELETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_DELETED.message)) + } + + @Test + @DisplayName("여행이 이미 완료되었다면 400 Bad Request를 반환한다.") + fun shouldReturnBadRequestWhenTripAlreadyCompleted() { + // given + val completedTrip = tripTestHelper.saveCompletedTrip(member, TripCategory.COURSE) + + // when + val resultActions = getResultActions(token, completedTrip.id) + + // then + resultActions + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(TripErrorCode.TRIP_ALREADY_COMPLETED.status.value())) + .andExpect(jsonPath("$.data.message").value(TripErrorCode.TRIP_ALREADY_COMPLETED.message)) + } + + @Test + @DisplayName("특정 여행을 상세 조회합니다.") + fun shouldReturnTrip() { + // when + val resultActions = getResultActions(token, courseTrip.id) + + // then + resultActions + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data").isNotEmpty) + } + } +}