Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@ public void updateMissionOrders(Long stampId, UpdateMissionOrderRequest request)
}
}

public void updateCompleted(Mission mission) {
MissionPolicy.validateNotDeleted(mission);
MissionPolicy.validateCompleted(mission);

mission.updateCompleted();
}

@Transactional
public void deleteMission(Long stampId, Mission mission) {
validateMissionIsActiveAndBelongsToStamp(stampId, mission);
Expand Down Expand Up @@ -120,10 +113,26 @@ public List<Mission> getValidMissionsWithStamp(List<Long> missionIds) {
return missions;
}

@Transactional
public void completeMission(Mission mission) {
MissionPolicy.validateNotDeleted(mission);
MissionPolicy.validateCompleted(mission);

mission.updateCompleted();
}

public void validateMissionBelongsToStamp(Long stampId, Mission mission) {
MissionPolicy.validateMissionBelongsToStamp(stampId, mission);
}

@Transactional(readOnly = true)
public void validateAllMissionsCompletedByStampId(Long stampId) {
boolean exists =
missionQueryRepository.existsByStampIdAndCompletedIsFalseAndDeletedAtIsNull(
stampId);
MissionPolicy.validateAllCompleted(exists);
}

private void validateMissionIsActiveAndBelongsToStamp(Long stampId, Mission mission) {
MissionPolicy.validateMissionBelongsToStamp(stampId, mission);
MissionPolicy.validateNotDeleted(mission);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum MissionErrorCode implements ErrorCode {
MISSION_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "해당 미션은 이미 삭제되었습니다."),
MISSION_ORDER_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "이미 존재하는 미션 순서입니다."),
MISSION_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "이미 완료된 미션입니다."),
ALL_MISSIONS_NOT_COMPLETED(HttpStatus.BAD_REQUEST, "모든 미션이 완료되지 않았습니다."),

// 403
MISSION_NOT_BELONGS_TO_STAMP(HttpStatus.FORBIDDEN, "해당 미션은 요청한 스탬프에 속하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,10 @@ public static void validateExistAll(List<Mission> foundMissions, List<Long> requ
throw new CustomException(MissionErrorCode.MISSION_NOT_FOUND);
}
}

public static void validateAllCompleted(boolean exists) {
if (exists) {
throw new CustomException(MissionErrorCode.ALL_MISSIONS_NOT_COMPLETED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@

public interface MissionQueryRepository {
List<Mission> findAllByIdsInFetchJoinStamp(List<Long> ids);

boolean existsByStampIdAndCompletedIsFalseAndDeletedAtIsNull(Long stampId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,46 @@ public List<Mission> findAllByIdsInFetchJoinStamp(List<Long> ids) {
.where(mission.id.in(ids))
.fetch();
}

@Override
public boolean existsByStampIdAndCompletedIsFalseAndDeletedAtIsNull(Long stampId) {
Integer hit =
queryFactory
.selectOne()
.from(mission)
.where(
mission.stamp.id.eq(stampId),
mission.completed.isFalse(),
mission.deletedAt.isNull())
.fetchFirst();

return hit != null;
}

// @Override
// public long countByStampIdAndDeletedAtIsNull(Long stampId) {
// Long count =
// queryFactory
// .select(mission.count())
// .from(mission)
// .where(mission.stamp.id.eq(stampId), mission.deletedAt.isNull())
// .fetchOne();
//
// return Optional.ofNullable(count).orElse(0L);
// }
//
// @Override
// public long countByStampIdAndCompletedIsTrueAndDeletedAtIsNull(Long stampId) {
// Long count =
// queryFactory
// .select(mission.count())
// .from(mission)
// .where(
// mission.stamp.id.eq(stampId),
// mission.completed.isTrue(),
// mission.deletedAt.isNull())
// .fetchOne();
//
// return Optional.ofNullable(count).orElse(0L);
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,14 @@ public StampDetail getStamp(Long memberId, Long tripId, Long stampId) {

return StampDetail.from(StampInfo.from(stamp), missionInfos);
}

public void completeStamp(Long memberId, Long tripId, Long stampId) {
Trip trip = tripService.getValidTrip(memberId, tripId);
Stamp stamp = stampService.getValidStamp(trip.getId(), stampId);

missionService.validateAllMissionsCompletedByStampId(stamp.getId());

stampService.completeStamp(stamp);
tripService.increaseCompletedStamps(trip);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -133,6 +134,24 @@ public String getStampNameByTripCategory(TripCategory tripCategory, List<Stamp>
return getExplorationStampName(stamps);
}

@Transactional
public void completeStamp(Stamp stamp) {
StampPolicy.validateCompleted(stamp);

stamp.updateCompleted();
}

public void validateStampBelongsToTrip(Long tripId, Stamp stamp) {
StampPolicy.validateStampBelongsToTrip(tripId, stamp);
}

@Transactional(readOnly = true)
public void validateAllStampsCompletedByTripId(Long tripId) {
boolean exists =
stampQueryRepository.existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(tripId);
StampPolicy.validateAllCompleted(exists);
}

private String getCourseStampName(List<Stamp> stamps) {
return new HashSet<>(stamps).iterator().next().getName();
}
Expand All @@ -159,10 +178,6 @@ private String getExplorationStampName(List<Stamp> stamps) {
.orElse("");
}

public void validateStampBelongsToTrip(Long tripId, Stamp stamp) {
StampPolicy.validateStampBelongsToTrip(tripId, stamp);
}

private void shiftStampOrdersAfterDeleted(Long tripId, int deletedStampOrder) {
List<Stamp> affectedStamps =
stampQueryRepository.findStampsToShiftAfterOrder(tripId, deletedStampOrder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public enum StampErrorCode implements ErrorCode {
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, "모든 스탬프가 완료되지 않았습니다."),

// 403
STAMP_NOT_BELONG_TO_TRIP(HttpStatus.FORBIDDEN, "해당 스탬프는 요청한 여행에 속하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,16 @@ public static void validateStampListNotEmpty(List<Stamp> stamps) {
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public interface StampQueryRepository {
List<Stamp> findStampsToShiftAfterOrder(Long tripId, int deletedOrder);

Optional<Stamp> findFirstIncompleteStampByTripId(Long tripId);

boolean existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(Long tripId);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ject.studytrip.stamp.infra.jpa;
package com.ject.studytrip.stamp.infra.querydsl;

import com.ject.studytrip.stamp.domain.model.QStamp;
import com.ject.studytrip.stamp.domain.model.Stamp;
Expand Down Expand Up @@ -40,4 +40,19 @@ public Optional<Stamp> findFirstIncompleteStampByTripId(Long tripId) {
.orderBy(stamp.stampOrder.asc())
.fetchFirst());
}

@Override
public boolean existsByTripIdAndCompletedIsFalseAndDeletedAtIsNull(Long tripId) {
Integer hit =
queryFactory
.selectOne()
.from(stamp)
.where(
stamp.trip.id.eq(tripId),
stamp.completed.isFalse(),
stamp.deletedAt.isNull())
.fetchOne();

return hit != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@

@Tag(name = "Stamp", description = "스탬프 API")
@RestController
@RequestMapping("/api/trips")
@RequiredArgsConstructor
@Validated
public class StampController {
private final StampFacade stampFacade;

@Operation(summary = "스탬프 등록", description = "특정 여행에 새로운 스탬프를 등록합니다.")
@PostMapping("/api/trips/{tripId}/stamps")
@PostMapping("/{tripId}/stamps")
public ResponseEntity<StandardResponse> createStamp(
@AuthenticationPrincipal String memberId,
@PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId,
Expand All @@ -44,7 +45,7 @@ public ResponseEntity<StandardResponse> createStamp(
}

@Operation(summary = "스탬프 수정", description = "특정 스탬프의 이름을 수정합니다.")
@PatchMapping("/api/trips/{tripId}/stamps/{stampId}")
@PatchMapping("/{tripId}/stamps/{stampId}")
public ResponseEntity<StandardResponse> updateStamp(
@AuthenticationPrincipal String memberId,
@PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId,
Expand All @@ -57,7 +58,7 @@ public ResponseEntity<StandardResponse> updateStamp(
}

@Operation(summary = "스탬프 순서 변경", description = "스탬프 순서를 변경합니다. 스탬프 ID 목록을 최종 순서대로 요청합니다.")
@PutMapping("/api/trips/{tripId}/stamps/orders")
@PutMapping("/{tripId}/stamps/orders")
public ResponseEntity<StandardResponse> updateStampOrders(
@AuthenticationPrincipal String memberId,
@PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId,
Expand All @@ -69,7 +70,7 @@ public ResponseEntity<StandardResponse> updateStampOrders(
}

@Operation(summary = "스탬프 삭제", description = "특정 스탬프를 삭제합니다.")
@DeleteMapping("/api/trips/{tripId}/stamps/{stampId}")
@DeleteMapping("/{tripId}/stamps/{stampId}")
public ResponseEntity<StandardResponse> deleteStamp(
@AuthenticationPrincipal String memberId,
@PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId,
Expand All @@ -81,7 +82,7 @@ public ResponseEntity<StandardResponse> deleteStamp(
}

@Operation(summary = "스탬프 목록 조회", description = "특정 여행의 스탬프 목록을 조회합니다.")
@GetMapping("/api/trips/{tripId}/stamps")
@GetMapping("/{tripId}/stamps")
public ResponseEntity<StandardResponse> loadStampsByTrip(
@AuthenticationPrincipal String memberId, @PathVariable Long tripId) {
List<StampInfo> result = stampFacade.getStampsByTrip(Long.valueOf(memberId), tripId);
Expand All @@ -93,7 +94,7 @@ public ResponseEntity<StandardResponse> loadStampsByTrip(
}

@Operation(summary = "스탬프 상세 조회", description = "특정 여행의 특정 스탬프 상세 정보를 조회합니다.")
@GetMapping("api/trips/{tripId}/stamps/{stampId}")
@GetMapping("/{tripId}/stamps/{stampId}")
public ResponseEntity<StandardResponse> loadStamp(
@AuthenticationPrincipal String memberId,
@PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId,
Expand All @@ -107,4 +108,16 @@ public ResponseEntity<StandardResponse> loadStamp(
LoadStampDetailResponse.of(
result.stampInfo(), result.missionInfos())));
}

@Operation(summary = "스탬프 완료", description = "특정 스탬프 하위의 모든 미션이 완료된 경우에만 스탬프를 완료합니다.")
@PatchMapping("/{tripId}/stamps/{stampId}/complete")
public ResponseEntity<StandardResponse> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ private void createStudyLogDailyMissionsAndCompleteMissions(

// 미션 완료 처리
selectedDailyMissions.forEach(
dailyMission -> missionService.updateCompleted(dailyMission.getMission()));
dailyMission -> missionService.completeMission(dailyMission.getMission()));
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ public TripDetail getTrip(Long memberId, Long tripId) {
return TripDetail.from(TripInfo.from(trip, dDay, progress), stampInfos);
}

public void completeTrip(Long memberId, Long tripId) {
Member member = memberService.getMember(memberId);
Trip trip = tripService.getValidTrip(member.getId(), tripId);

stampService.validateAllStampsCompletedByTripId(trip.getId());

tripService.completeTrip(trip);
}

private Integer calculateDDay(LocalDate endDate) {
if (endDate == null) return null; // NULL 인 경우 무기한 여행

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,16 @@ public TripCount getActiveTripCountsByMemberId(Long memberId) {
memberId, TripCategory.EXPLORE);
return TripCount.of(courseCount, exploreCount);
}

@Transactional
public void completeTrip(Trip trip) {
TripPolicy.validateCompleted(trip);

trip.updateCompleted();
}

@Transactional
public void increaseCompletedStamps(Trip trip) {
trip.increaseCompletedStamps();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum TripErrorCode implements ErrorCode {
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, "이미 삭제된 여행입니다."),

// 403
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/ject/studytrip/trip/domain/model/Trip.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,15 @@ public void decreaseTotalStamps() {
this.totalStamps -= 1;
}

public void updateIsComplete(boolean completed) {
this.completed = completed;
public void updateCompleted() {
this.completed = true;
}

public void updateDeletedAt() {
this.deletedAt = LocalDateTime.now();
}

public void increaseCompletedStamps() {
this.completedStamps += 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ 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);
}
}
}
Loading