diff --git a/src/main/java/com/backend/detailgoal/application/DetailGoalService.java b/src/main/java/com/backend/detailgoal/application/DetailGoalService.java index 1b1e731..4578a94 100644 --- a/src/main/java/com/backend/detailgoal/application/DetailGoalService.java +++ b/src/main/java/com/backend/detailgoal/application/DetailGoalService.java @@ -26,7 +26,7 @@ public class DetailGoalService { public List getDetailGoalList(Long goalId) { - List detailGoalList = detailGoalRepository.findDetailGoalsByGoalIdAndIsDeletedFalse(goalId); + List detailGoalList = detailGoalRepository.findAllByGoalIdAndIsDeletedFalse(goalId); return detailGoalList.stream().map(DetailGoalListResponse::from).collect(Collectors.toList()); } @@ -44,7 +44,7 @@ public DetailGoal saveDetailGoal(Long goalId, DetailGoalSaveRequest detailGoalSa detailGoalRepository.save(detailGoal); Goal goal = goalRepository.getByIdAndIsDeletedFalse(goalId); - goal.increaseEntireDetailGoalCnt(); + goal.increaseEntireDetailGoalCnt(); // 전체 하위 목표 개수 증가 return detailGoal; } @@ -55,7 +55,12 @@ public GoalCompletedResponse removeDetailGoal(Long detailGoalId) detailGoal.remove(); Goal goal = goalRepository.getByIdAndIsDeletedFalse(detailGoal.getGoalId()); - goal.decreaseEntireDetailGoalCnt(); + goal.decreaseEntireDetailGoalCnt(); // 전체 하위 목표 감소 + + if(detailGoal.getIsCompleted()) // 만약 이미 성취된 목표였다면, 성취된 목표 개수까지 함께 제거 + { + goal.decreaseCompletedDetailGoalCnt(); + } return new GoalCompletedResponse(goal.checkGoalCompleted()); } @@ -76,13 +81,14 @@ public GoalCompletedResponse completeDetailGoal(Long detailGoalId) { DetailGoal detailGoal = detailGoalRepository.getByIdAndIsDeletedFalse(detailGoalId); detailGoal.complete(); + Goal goal = goalRepository.getByIdAndIsDeletedFalse(detailGoal.getGoalId()); - goal.increaseCompletedDetailGoalCnt(); + goal.increaseCompletedDetailGoalCnt(); // 성공한 개수 체크 + return new GoalCompletedResponse(goal.checkGoalCompleted()); } - @Transactional public void inCompleteDetailGoal(Long detailGoalId) { diff --git a/src/main/java/com/backend/detailgoal/application/dto/response/DetailGoalResponse.java b/src/main/java/com/backend/detailgoal/application/dto/response/DetailGoalResponse.java index 16d4e50..e90049b 100644 --- a/src/main/java/com/backend/detailgoal/application/dto/response/DetailGoalResponse.java +++ b/src/main/java/com/backend/detailgoal/application/dto/response/DetailGoalResponse.java @@ -16,7 +16,7 @@ public record DetailGoalResponse( String title, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "a KK:mm", timezone = "Asia/Seoul") LocalTime alarmTime, List alarmDays, diff --git a/src/main/java/com/backend/detailgoal/domain/DetailGoalRepository.java b/src/main/java/com/backend/detailgoal/domain/DetailGoalRepository.java index 90c6883..52cc95b 100644 --- a/src/main/java/com/backend/detailgoal/domain/DetailGoalRepository.java +++ b/src/main/java/com/backend/detailgoal/domain/DetailGoalRepository.java @@ -8,10 +8,11 @@ public interface DetailGoalRepository extends JpaRepository { - List findDetailGoalsByGoalIdAndIsDeletedFalse(Long goalId); + List findAllByGoalIdAndIsDeletedFalse(Long goalId); default DetailGoal getByIdAndIsDeletedFalse(Long detailGoalId){ return findById(detailGoalId).orElseThrow(() -> new BusinessException(ErrorCode.DETAIL_GOAL_NOT_FOUND)); } + } diff --git a/src/main/java/com/backend/detailgoal/presentation/DetailGoalController.java b/src/main/java/com/backend/detailgoal/presentation/DetailGoalController.java index f3e8ff1..8da4995 100644 --- a/src/main/java/com/backend/detailgoal/presentation/DetailGoalController.java +++ b/src/main/java/com/backend/detailgoal/presentation/DetailGoalController.java @@ -5,6 +5,7 @@ import com.backend.detailgoal.presentation.dto.request.DetailGoalUpdateRequest; import com.backend.global.common.response.CustomResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -22,21 +23,21 @@ public class DetailGoalController { @Operation(summary = "하위 목표 리스트 조회", description = "하위 목표 리스트를 조회하는 API 입니다.") @GetMapping("/goals/{id}/detail-goals") - public ResponseEntity getDetailGoalList(@PathVariable Long id) + public ResponseEntity getDetailGoalList(@Parameter(description = "상위 목표 ID") @PathVariable Long id) { return CustomResponse.success(SELECT_SUCCESS, detailGoalService.getDetailGoalList(id)); } @Operation(summary = "하위 목표 상세 조회", description = "하위 목표를 상세 조회하는 API 입니다.") @GetMapping("/detail-goals/{id}") - public ResponseEntity getDetailGoal(@PathVariable Long id) + public ResponseEntity getDetailGoal(@Parameter(description = "하위 목표 ID") @PathVariable Long id) { return CustomResponse.success(SELECT_SUCCESS, detailGoalService.getDetailGoal(id)); } - @Operation(summary = "하위 목표 리스트 생성", description = "하위 목표를 생성하는 API 입니다.") + @Operation(summary = "하위 목표 생성", description = "하위 목표를 생성하는 API 입니다.") @PostMapping("/goals/{id}/detail-goals") - public ResponseEntity saveDetailGoal(@PathVariable Long id, @RequestBody @Valid DetailGoalSaveRequest detailGoalSaveRequest) + public ResponseEntity saveDetailGoal(@Parameter(description = "상위 목표 ID") @PathVariable Long id, @RequestBody @Valid DetailGoalSaveRequest detailGoalSaveRequest) { detailGoalService.saveDetailGoal(id, detailGoalSaveRequest); return CustomResponse.success(INSERT_SUCCESS); @@ -44,7 +45,7 @@ public ResponseEntity saveDetailGoal(@PathVariable Long id, @Req @Operation(summary = "하위 목표 수정", description = "하위 목표를 수정하는 API 입니다.") @PatchMapping("/detail-goals/{id}") - public ResponseEntity updateDetailGoal(@PathVariable Long id, @RequestBody @Valid DetailGoalUpdateRequest detailGoalUpdateRequest) + public ResponseEntity updateDetailGoal(@Parameter(description = "하위 목표 ID") @PathVariable Long id, @RequestBody @Valid DetailGoalUpdateRequest detailGoalUpdateRequest) { detailGoalService.updateDetailGoal(id, detailGoalUpdateRequest); return CustomResponse.success(UPDATE_SUCCESS); @@ -52,7 +53,7 @@ public ResponseEntity updateDetailGoal(@PathVariable Long id, @R @Operation(summary = "하위 목표 삭제", description = "하위 목표를 삭제하는 API 입니다.") @DeleteMapping("/detail-goals/{id}") - public ResponseEntity removeDetailGoal(@PathVariable Long id) + public ResponseEntity removeDetailGoal(@Parameter(description = "하위 목표 ID") @PathVariable Long id) { return CustomResponse.success(DELETE_SUCCESS, detailGoalService.removeDetailGoal(id)); @@ -60,14 +61,14 @@ public ResponseEntity removeDetailGoal(@PathVariable Long id) @Operation(summary = "하위 목표 달성", description = "하위 목표를 달성하는 API 입니다.") @PatchMapping("/detail-goals/{id}/complete") - public ResponseEntity completeDetailGoal(@PathVariable Long id) + public ResponseEntity completeDetailGoal(@Parameter(description = "하위 목표 ID") @PathVariable Long id) { return CustomResponse.success(UPDATE_SUCCESS, detailGoalService.completeDetailGoal(id)); } @Operation(summary = "하위 목표 달성 취소", description = "하위 목표 달성을 취소하는 API 입니다.") @PatchMapping("/detail-goals/{id}/incomplete") - public ResponseEntity incompleteDetailGoal(@PathVariable Long id) + public ResponseEntity incompleteDetailGoal(@Parameter(description = "하위 목표 ID") @PathVariable Long id) { detailGoalService.inCompleteDetailGoal(id); return CustomResponse.success(UPDATE_SUCCESS); diff --git a/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalSaveRequest.java b/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalSaveRequest.java index b011142..f692cb6 100644 --- a/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalSaveRequest.java +++ b/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalSaveRequest.java @@ -2,6 +2,7 @@ import com.backend.detailgoal.domain.DetailGoal; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -13,14 +14,18 @@ public record DetailGoalSaveRequest( @Size(max = 15, message = "상위 목표 제목은 15자를 초과할 수 없습니다.") + @Schema(description = "하위 목표 제목", example = "오픽 노잼 IH 시리즈 보기") String title, @NotNull(message = "알림 설정 여부는 빈값일 수 없습니다.") + @Schema(description = "알람 수행 여부") Boolean alarmEnabled, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "a KK:mm", timezone = "Asia/Seoul") + @Schema(description = "알람 받을 시각", example = "오후 11:30") LocalTime alarmTime, + @Schema(description = "요일 정보", examples = {"MONDAY", "TUESDAY", "FRIDAY"}) List alarmDays ) { diff --git a/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalUpdateRequest.java b/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalUpdateRequest.java index 9635a2b..a3f08a9 100644 --- a/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalUpdateRequest.java +++ b/src/main/java/com/backend/detailgoal/presentation/dto/request/DetailGoalUpdateRequest.java @@ -1,6 +1,7 @@ package com.backend.detailgoal.presentation.dto.request; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -11,14 +12,18 @@ public record DetailGoalUpdateRequest( @Size(max = 15, message = "상위 목표 제목은 15자를 초과할 수 없습니다.") + @Schema(description = "하위 목표 제목", example = "오픽 노잼 IH 영상 보기") String title, @NotNull(message = "알림 설정 여부는 빈값일 수 없습니다.") + @Schema(description = "알람 수행 여부") Boolean alarmEnabled, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "a KK:mm", timezone = "Asia/Seoul") + @Schema(description = "알람 받을 시각", example = "오후 11:30") LocalTime alarmTime, + @Schema(description = "요일 정보", example = "MONDAY, TUESDAY, FRIDAY") List alarmDays ) { } diff --git a/src/main/java/com/backend/global/common/code/ErrorCode.java b/src/main/java/com/backend/global/common/code/ErrorCode.java index 01113a8..6c743f5 100644 --- a/src/main/java/com/backend/global/common/code/ErrorCode.java +++ b/src/main/java/com/backend/global/common/code/ErrorCode.java @@ -63,6 +63,8 @@ public enum ErrorCode { COMPLETED_DETAIL_GOAL_CNT_INVALID(BAD_REQUEST.value(), "GOAL-003", "성공한 하위목표 개수가 0개 일때는 뺄 수 없습니다."), + RECOVER_GOAL_IMPOSSIBLE(BAD_REQUEST.value(), "GOAL-004", "상위 목표가 보관함에 있을때 채움함으로 복구할 수 있습니다."), + /* Detail Goal */ DETAIL_GOAL_NOT_FOUND(NOT_FOUND.value(), "DETAIL-GOAL-001", "하위 목표가 존재하지 않습니다."), diff --git a/src/main/java/com/backend/global/event/GoalEventHandler.java b/src/main/java/com/backend/global/event/GoalEventHandler.java index 09f49ca..33cf60a 100644 --- a/src/main/java/com/backend/global/event/GoalEventHandler.java +++ b/src/main/java/com/backend/global/event/GoalEventHandler.java @@ -22,7 +22,7 @@ public class GoalEventHandler { @Transactional(propagation = Propagation.REQUIRES_NEW) public void removeDetailGoalList(RemoveRelatedDetailGoalEvent event) { - List detailGoalList = detailGoalRepository.findDetailGoalsByGoalIdAndIsDeletedFalse(event.goalId()); + List detailGoalList = detailGoalRepository.findAllByGoalIdAndIsDeletedFalse(event.goalId()); detailGoalList.forEach((DetailGoal::remove)); } diff --git a/src/main/java/com/backend/goal/application/GoalService.java b/src/main/java/com/backend/goal/application/GoalService.java index 4329a8c..c14f349 100644 --- a/src/main/java/com/backend/goal/application/GoalService.java +++ b/src/main/java/com/backend/goal/application/GoalService.java @@ -1,9 +1,13 @@ package com.backend.goal.application; +import com.backend.detailgoal.application.dto.response.DetailGoalResponse; +import com.backend.detailgoal.domain.DetailGoal; import com.backend.goal.application.dto.response.GoalCountResponse; import com.backend.goal.application.dto.response.GoalListResponse; +import com.backend.goal.application.dto.response.RetrospectEnabledGoalCountResponse; import com.backend.goal.domain.*; import com.backend.goal.application.dto.response.GoalResponse; +import com.backend.goal.presentation.dto.GoalRecoverRequest; import com.backend.goal.presentation.dto.GoalSaveRequest; import com.backend.goal.presentation.dto.GoalUpdateRequest; import lombok.RequiredArgsConstructor; @@ -47,6 +51,12 @@ public GoalCountResponse getGoalCounts() return new GoalCountResponse(statusCounts); } + public RetrospectEnabledGoalCountResponse getGoalCountRetrospectEnabled() + { + Long count = goalQueryRepository.getGoalCountRetrospectEnabled(); + return new RetrospectEnabledGoalCountResponse(count); + } + @Transactional public Long saveGoal(final Long memberId, final GoalSaveRequest goalSaveRequest) @@ -71,4 +81,11 @@ public void removeGoal(Long goalId) applicationEventPublisher.publishEvent(new RemoveRelatedDetailGoalEvent(goal.getId())); } + + @Transactional + public void recoverGoal(Long goalId, GoalRecoverRequest goalRecoverRequest) + { + Goal goal = goalRepository.getByIdAndIsDeletedFalse(goalId); + goal.recover(goalRecoverRequest.startDate(), goalRecoverRequest.endDate(), goalRecoverRequest.reminderEnabled()); + } } diff --git a/src/main/java/com/backend/goal/application/dto/response/RetrospectEnabledGoalCountResponse.java b/src/main/java/com/backend/goal/application/dto/response/RetrospectEnabledGoalCountResponse.java new file mode 100644 index 0000000..867d0c9 --- /dev/null +++ b/src/main/java/com/backend/goal/application/dto/response/RetrospectEnabledGoalCountResponse.java @@ -0,0 +1,6 @@ +package com.backend.goal.application.dto.response; + +public record RetrospectEnabledGoalCountResponse( + Long count +) { +} diff --git a/src/main/java/com/backend/goal/domain/Goal.java b/src/main/java/com/backend/goal/domain/Goal.java index 4b42b63..ede640c 100644 --- a/src/main/java/com/backend/goal/domain/Goal.java +++ b/src/main/java/com/backend/goal/domain/Goal.java @@ -9,7 +9,8 @@ import lombok.NoArgsConstructor; import java.time.LocalDate; import java.time.temporal.ChronoUnit; -import java.util.Objects; + +import static com.backend.global.common.code.ErrorCode.RECOVER_GOAL_IMPOSSIBLE; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -70,9 +71,9 @@ private void init() hasRetrospect = Boolean.FALSE; entireDetailGoalCnt = 0; completedDetailGoalCnt = 0; - goalStatus = GoalStatus.PROCESS; } + public void increaseEntireDetailGoalCnt() { this.entireDetailGoalCnt +=1; @@ -106,6 +107,12 @@ public void decreaseCompletedDetailGoalCnt() public boolean checkGoalCompleted() { + // 만약 전체 개수가 0개라면 체크 하면 안됨 + if (entireDetailGoalCnt == 0) + { + return false; + } + return completedDetailGoalCnt == entireDetailGoalCnt; } @@ -118,7 +125,7 @@ public void update(final String title, final LocalDate startDate, final LocalDat this.reminderEnabled = reminderEnabled; } - public Goal(final Long memberId, final String title, final LocalDate startDate, final LocalDate endDate, final Boolean reminderEnabled) + public Goal(final Long memberId, final String title, final LocalDate startDate, final LocalDate endDate, final Boolean reminderEnabled, final GoalStatus goalStatus) { validateTitleLength(title); validatePeriod(startDate, endDate); @@ -127,8 +134,23 @@ public Goal(final Long memberId, final String title, final LocalDate startDate, this.startDate = startDate; this.endDate = endDate; this.reminderEnabled = reminderEnabled; + this.goalStatus = goalStatus; + } + + public void recover(final LocalDate startDate, final LocalDate endDate, final Boolean reminderEnabled) + { + if(!isRecoveringEnable()) + { + throw new BusinessException(RECOVER_GOAL_IMPOSSIBLE); + } + + this.goalStatus = GoalStatus.PROCESS; + this.reminderEnabled = reminderEnabled; + this.startDate = startDate; + this.endDate = endDate; } + private void validateTitleLength(final String title) { if (title.length() > MAX_TITLE_LENGTH) { @@ -137,6 +159,7 @@ private void validateTitleLength(final String title) { } private void validatePeriod(final LocalDate startDate, final LocalDate endDate) { + if (startDate.isAfter(endDate)) { throw new IllegalArgumentException("종료일시가 시작일시보다 이전일 수 없습니다."); } @@ -147,7 +170,7 @@ private void validatePeriod(final LocalDate startDate, final LocalDate endDate) } } - public Long calculateDday(LocalDate now) + public Long calculateDday(final LocalDate now) { if(now.isAfter(endDate)) { @@ -160,4 +183,8 @@ public Long calculateDday(LocalDate now) private boolean isNotValidDateTimeRange(final LocalDate date) { return date.isBefore(MIN_DATE) || date.isAfter(MAX_DATE); } + + private boolean isRecoveringEnable() { + return goalStatus.equals(GoalStatus.STORE); + } } diff --git a/src/main/java/com/backend/goal/domain/GoalQueryRepository.java b/src/main/java/com/backend/goal/domain/GoalQueryRepository.java index 08ea67f..62751ce 100644 --- a/src/main/java/com/backend/goal/domain/GoalQueryRepository.java +++ b/src/main/java/com/backend/goal/domain/GoalQueryRepository.java @@ -46,6 +46,18 @@ public Slice getGoalList(Long goalId, Pageable pageable, GoalStatus goalSt return new SliceImpl<>(goalList, pageable, hasNext); } + public Long getGoalCountRetrospectEnabled() + { + return query.select(goal.count()) + .from(goal) + .where( + goal.isDeleted.isFalse(), // 삭제 되지 않은 것들만 조회 + goal.hasRetrospect.isFalse(), // 아직 회고를 작성하지 않는 것들 조회 + goal.goalStatus.eq(GoalStatus.COMPLETE) // 완료상태인것들 체크 + ) + .fetchOne(); + } + public Map getStatusCounts() { diff --git a/src/main/java/com/backend/goal/domain/GoalRepository.java b/src/main/java/com/backend/goal/domain/GoalRepository.java index 4ba7e93..8e18724 100644 --- a/src/main/java/com/backend/goal/domain/GoalRepository.java +++ b/src/main/java/com/backend/goal/domain/GoalRepository.java @@ -4,7 +4,7 @@ import com.backend.global.exception.BusinessException; import org.springframework.data.jpa.repository.JpaRepository; - +import java.util.List; public interface GoalRepository extends JpaRepository { @@ -14,5 +14,4 @@ default Goal getByIdAndIsDeletedFalse(Long id){ return findById(id).orElseThrow(() -> {throw new BusinessException(ErrorCode.GOAL_NOT_FOUND); }); } - } diff --git a/src/main/java/com/backend/goal/presentation/GoalController.java b/src/main/java/com/backend/goal/presentation/GoalController.java index 34eede4..0902ea7 100644 --- a/src/main/java/com/backend/goal/presentation/GoalController.java +++ b/src/main/java/com/backend/goal/presentation/GoalController.java @@ -2,6 +2,7 @@ import com.backend.global.common.response.CustomResponse; import com.backend.goal.application.GoalService; +import com.backend.goal.presentation.dto.GoalRecoverRequest; import com.backend.goal.presentation.dto.GoalSaveRequest; import com.backend.goal.presentation.dto.GoalUpdateRequest; import io.swagger.v3.oas.annotations.Operation; @@ -18,52 +19,69 @@ @RestController @RequiredArgsConstructor +@RequestMapping("/goals") @Tag(name = "goal", description = "상위 목표 API") public class GoalController { private final GoalService goalService; @Operation(summary = "상위 목표 리스트 조회", description = "상위 목표 리스트를 조회하는 API 입니다.") - @GetMapping("/goals") + @GetMapping public ResponseEntity getGoalList( @Parameter(hidden = true) @PageableDefault(size = 10) Pageable pageable, - @RequestParam(required = false) Long lastId, - @RequestParam String goalStatus) + @Parameter(description = "처음 조회 시 Null 전달, 이후부터는 이전 응답 데이터 중 마지막 ID를 전달") @RequestParam(required = false) Long lastId, + @Parameter(description = "store(보관함), process(채움함), complete(완료함) 중 하나로 호출") @RequestParam String goalStatus) { return CustomResponse.success(SELECT_SUCCESS,goalService.getGoalList(lastId,pageable,goalStatus)); } @Operation(summary = "상위 목표 상태별 개수 조회", description = "상위 목표 상태별 개수를 조회하는 API 입니다.") - @GetMapping("/goals/count") + @GetMapping("/count") public ResponseEntity getGoalCounts() { return CustomResponse.success(SELECT_SUCCESS,goalService.getGoalCounts()); } + @Operation(summary = "회고 작성 가능한 목표 개수 조회", description = "회고 작성 가능한 목표 개수를 조회하는 API 입니다.") + @GetMapping("/retrospect-enabled/count") + public ResponseEntity getRetrospectEnabledGoalCount() + { + return CustomResponse.success(SELECT_SUCCESS,goalService.getGoalCountRetrospectEnabled()); + } + @Operation(summary = "상위 목표 삭제", description = "상위 목표를 삭제하는 API 입니다.") - @DeleteMapping("/goals/{id}") - public ResponseEntity removeGoal(@PathVariable Long id) + @DeleteMapping("/{id}") + public ResponseEntity removeGoal(@Parameter(description = "상위 목표 ID") @PathVariable Long id) { goalService.removeGoal(id); return CustomResponse.success(DELETE_SUCCESS); } + @Operation(summary = "보관함 내 상위 목표 복구", description = "보관함에 들어간 상위 목표를 복구하는 API 입니다.") + @PatchMapping("/{id}/recover") + public ResponseEntity recoverGoal(@Parameter(description = "상위 목표 ID") @PathVariable Long id, @RequestBody @Valid GoalRecoverRequest goalRecoverRequest) + { + goalService.recoverGoal(id, goalRecoverRequest); + return CustomResponse.success(UPDATE_SUCCESS); + } + @Operation(summary = "상위 목표 수정", description = "상위 목표를 수정하는 API 입니다.") - @PatchMapping("/goals/{id}") + @PatchMapping("/{id}") public ResponseEntity updateGoal(@RequestBody @Valid GoalUpdateRequest goalSaveRequest) { return CustomResponse.success(UPDATE_SUCCESS, goalService.updateGoal(goalSaveRequest)); } - @Operation(summary = "상위 목표 생성", description = "상위 목표를 생성하는 API 입니다.") - @PostMapping("/goals") + @PostMapping public ResponseEntity saveGoal(@RequestBody @Valid GoalSaveRequest goalSaveRequest) { // 아직 유저 식별 값으로 뭐가 들어올지 몰라 1L로 설정해놨습니다. goalService.saveGoal(1L, goalSaveRequest); return CustomResponse.success(INSERT_SUCCESS); } + + } diff --git a/src/main/java/com/backend/goal/presentation/dto/GoalRecoverRequest.java b/src/main/java/com/backend/goal/presentation/dto/GoalRecoverRequest.java new file mode 100644 index 0000000..7e24b63 --- /dev/null +++ b/src/main/java/com/backend/goal/presentation/dto/GoalRecoverRequest.java @@ -0,0 +1,26 @@ +package com.backend.goal.presentation.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +public record GoalRecoverRequest( + + @Schema(example = "2023-08-27", description = "현재 날짜보다 뒤의 날짜를 골라야 합니다") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") + LocalDate startDate, + + @Schema(example = "2023-08-28", description = "현재 날짜보다 뒤의 날짜를 골라야 합니다") + @FutureOrPresent(message = "상위 목표 종료 일자는 과거 시점일 수 없습니다") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") + LocalDate endDate, + + @Schema(description = "리마인드 설정 여부") + @NotNull(message = "리마인드 알림 여부를 필수적으로 선택해야 합니다") + Boolean reminderEnabled + +) { +} diff --git a/src/main/java/com/backend/goal/presentation/dto/GoalSaveRequest.java b/src/main/java/com/backend/goal/presentation/dto/GoalSaveRequest.java index 20b49e4..0736b5f 100644 --- a/src/main/java/com/backend/goal/presentation/dto/GoalSaveRequest.java +++ b/src/main/java/com/backend/goal/presentation/dto/GoalSaveRequest.java @@ -1,6 +1,7 @@ package com.backend.goal.presentation.dto; import com.backend.goal.domain.Goal; +import com.backend.goal.domain.GoalStatus; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; @@ -17,12 +18,11 @@ public record GoalSaveRequest( @Size(max = 15, message = "상위 목표 제목은 15자를 초과할 수 없습니다") String title, - @Schema(example = "2023-8-27", description = "현재 날짜보다 뒤의 날짜를 골라야 합니다") - @FutureOrPresent(message = "상위 목표 시작 일자는 과거 시점일 수 없습니다") + @Schema(example = "2023-08-27", description = "현재 날짜보다 뒤의 날짜를 골라야 합니다") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") LocalDate startDate, - @Schema(example = "2023-8-28", description = "현재 날짜보다 뒤의 날짜를 골라야 합니다") + @Schema(example = "2023-08-28", description = "현재 날짜보다 뒤의 날짜를 골라야 합니다") @FutureOrPresent(message = "상위 목표 종료 일자는 과거 시점일 수 없습니다") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") LocalDate endDate, @@ -34,7 +34,7 @@ public record GoalSaveRequest( public Goal toEntity(Long memberId) { - return new Goal(memberId, title,startDate,endDate,reminderEnabled); + return new Goal(memberId, title,startDate,endDate,reminderEnabled, GoalStatus.PROCESS); } } diff --git a/src/test/java/com/backend/detailgoal/application/DetailGoalServiceTest.java b/src/test/java/com/backend/detailgoal/application/DetailGoalServiceTest.java index 533c9d5..b25cfae 100644 --- a/src/test/java/com/backend/detailgoal/application/DetailGoalServiceTest.java +++ b/src/test/java/com/backend/detailgoal/application/DetailGoalServiceTest.java @@ -10,6 +10,7 @@ import com.backend.global.DatabaseCleaner; import com.backend.goal.domain.Goal; import com.backend.goal.domain.GoalRepository; +import com.backend.goal.domain.GoalStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -52,7 +53,7 @@ void setUp() { void 하위목표를_생성하면_상위목표의_하위목표_카운트가_증가한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); DetailGoalSaveRequest detailGoalSaveRequest = new DetailGoalSaveRequest("테스트 제목", true, LocalTime.of(10, 0), List.of("MONDAY", "TUESDAY")); @@ -90,7 +91,7 @@ void setUp() { void 하위목표를_삭제하면_상위목표의_하위목표_카운트가_감소한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); DetailGoalSaveRequest detailGoalSaveRequest = new DetailGoalSaveRequest("테스트 제목", true, LocalTime.of(10, 0), List.of("MONDAY", "TUESDAY")); @@ -130,7 +131,7 @@ void setUp() { void 하위_목표_리스트를_조회한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); for(int i = 0; i < 5; i++) @@ -151,7 +152,7 @@ void setUp() { void 하위목표를_체크하면_완료상태로_변한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); DetailGoal detailGoal = new DetailGoal(savedGoal.getId(), "테스트 제목", false, true, List.of(DayOfWeek.MONDAY), LocalTime.of(10, 0)); @@ -161,7 +162,7 @@ void setUp() { detailGoalService.completeDetailGoal(savedDetailGoal.getId()); // then - List result = detailGoalRepository.findDetailGoalsByGoalIdAndIsDeletedFalse(savedDetailGoal.getGoalId()); + List result = detailGoalRepository.findAllByGoalIdAndIsDeletedFalse(savedDetailGoal.getGoalId()); assertThat(result.get(0).getIsCompleted()).isTrue(); } @@ -170,7 +171,7 @@ void setUp() { void 하위목표를_체크해제_하면_미완료_상태로_변한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); DetailGoal detailGoal = new DetailGoal(savedGoal.getId(), "테스트 제목", true, true, List.of(DayOfWeek.MONDAY), LocalTime.of(10, 0)); @@ -181,7 +182,7 @@ void setUp() { detailGoalService.inCompleteDetailGoal(savedDetailGoal.getId()); // then - List result = detailGoalRepository.findDetailGoalsByGoalIdAndIsDeletedFalse(savedDetailGoal.getGoalId()); + List result = detailGoalRepository.findAllByGoalIdAndIsDeletedFalse(savedDetailGoal.getGoalId()); assertThat(result.get(0).getIsCompleted()).isFalse(); } @@ -190,7 +191,7 @@ void setUp() { void 완료_하위목표_개수와_전체_하위목표_개수가_같아지면_상위목표가_성공한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); DetailGoalSaveRequest detailGoalSaveRequest = new DetailGoalSaveRequest("테스트 제목", true, LocalTime.of(10, 0), List.of("MONDAY", "TUESDAY")); @@ -212,7 +213,7 @@ void setUp() { void 하위목표를_삭제했을때_전체_하위목표_개수와_달성한_하위목표_개수가_같아지면_상위목표가_성공한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 20), LocalDate.of(2023, 8, 24), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); DetailGoalSaveRequest detailGoalSaveRequest = new DetailGoalSaveRequest("테스트 제목", true, LocalTime.of(10, 0), List.of("MONDAY", "TUESDAY")); diff --git a/src/test/java/com/backend/goal/application/GoalServiceTest.java b/src/test/java/com/backend/goal/application/GoalServiceTest.java index cdf1678..369e3d6 100644 --- a/src/test/java/com/backend/goal/application/GoalServiceTest.java +++ b/src/test/java/com/backend/goal/application/GoalServiceTest.java @@ -3,8 +3,11 @@ import com.backend.global.DatabaseCleaner; import com.backend.goal.application.dto.response.GoalCountResponse; import com.backend.goal.application.dto.response.GoalListResponse; +import com.backend.goal.application.dto.response.RetrospectEnabledGoalCountResponse; import com.backend.goal.domain.Goal; import com.backend.goal.domain.GoalRepository; +import com.backend.goal.domain.GoalStatus; +import com.backend.goal.presentation.dto.GoalRecoverRequest; import com.backend.goal.presentation.dto.GoalSaveRequest; import com.backend.goal.presentation.dto.GoalUpdateRequest; import org.assertj.core.api.Assertions; @@ -15,11 +18,14 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + import java.time.LocalDate; @SpringBootTest @ActiveProfiles("test") +@Transactional public class GoalServiceTest { @Autowired @@ -137,7 +143,7 @@ void setUp() { { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true, GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); GoalUpdateRequest goalUpdateRequest = new GoalUpdateRequest(savedGoal.getId(), "수정된 제목", LocalDate.now(), LocalDate.now(), false); @@ -154,7 +160,7 @@ void setUp() { void 상위목표를_삭제할수_있다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true , GoalStatus.PROCESS); Goal savedGoal = goalRepository.save(goal); // when @@ -170,8 +176,8 @@ void setUp() { void 상위목표_상태에_따라_통계를_제공한다() { // given - Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true); - Goal savedGoal = goalRepository.save(goal); + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true, GoalStatus.PROCESS); + goalRepository.save(goal); // when GoalCountResponse goalCounts = goalService.getGoalCounts(); @@ -179,4 +185,41 @@ void setUp() { // then Assertions.assertThat(goalCounts.counts().keySet()).hasSize(3); } + + @DisplayName("상위 목표를 보관함에서 채움함으로 복구한다") + @Test + void 상위목표를_보관함에서_채움함으로_복구한다() + { + // given + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true, GoalStatus.STORE); + Goal savedGoal = goalRepository.save(goal); + GoalRecoverRequest goalRecoverRequest = new GoalRecoverRequest(LocalDate.of(2023, 8, 1), LocalDate.of(2023, 9, 30), false); + + // when + goalService.recoverGoal(savedGoal.getId(), goalRecoverRequest); + + // then + Goal recoverdGoal = goalRepository.getById(savedGoal.getId()); + Assertions.assertThat(recoverdGoal.getEndDate()).isEqualTo(LocalDate.of(2023, 9, 30)); + Assertions.assertThat(recoverdGoal.getGoalStatus()).isEqualTo(GoalStatus.PROCESS); + Assertions.assertThat(recoverdGoal.getReminderEnabled()).isFalse(); + } + + @DisplayName("완료함의_목표들중_회고가능한_목표수를_계산한다") + @Test + void 완료함의_목표들중_회고가능한_목표수를_계산한다() + { + // given + for(int i =0; i < 10; i++) + { + Goal goal = new Goal(1L, "테스트 제목", LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1), true, GoalStatus.COMPLETE); + goalRepository.save(goal); + } + + // when + RetrospectEnabledGoalCountResponse count = goalService.getGoalCountRetrospectEnabled(); + + // then + Assertions.assertThat(count.count()).isEqualTo(10); + } } diff --git a/src/test/java/com/backend/goal/domain/GoalTest.java b/src/test/java/com/backend/goal/domain/GoalTest.java index ad75e30..0c1cf12 100644 --- a/src/test/java/com/backend/goal/domain/GoalTest.java +++ b/src/test/java/com/backend/goal/domain/GoalTest.java @@ -21,7 +21,7 @@ public class GoalTest { // when & then assertThatThrownBy(() -> { - new Goal(1L, "테스트 제목", startDate, endDate, true); + new Goal(1L, "테스트 제목", startDate, endDate, true, GoalStatus.PROCESS); }).isInstanceOf(IllegalArgumentException.class); } @@ -35,7 +35,7 @@ public class GoalTest { // when & then assertThatThrownBy(() -> { - new Goal(1L, "테스트 제목", startDate, endDate, true); + new Goal(1L, "테스트 제목", startDate, endDate, true, GoalStatus.PROCESS); }).isInstanceOf(IllegalArgumentException.class); } @@ -49,7 +49,7 @@ public class GoalTest { // when & then assertThatThrownBy(() -> { - new Goal(1L, "테스트 제목", startDate, endDate, true); + new Goal(1L, "테스트 제목", startDate, endDate, true, GoalStatus.PROCESS); }).isInstanceOf(IllegalArgumentException.class); } @@ -64,7 +64,7 @@ public class GoalTest { // when & then assertThatThrownBy(() -> { - new Goal(1L, title, startDate, endDate, true); + new Goal(1L, title, startDate, endDate, true, GoalStatus.PROCESS); }).isInstanceOf(IllegalArgumentException.class); } @@ -75,7 +75,7 @@ public class GoalTest { // given LocalDate startDate = LocalDate.of(2023,7,1); LocalDate endDate = LocalDate.of(2023,8,10); - Goal goal = new Goal(1L, "테스트 제목", startDate, endDate, true); + Goal goal = new Goal(1L, "테스트 제목", startDate, endDate, true, GoalStatus.PROCESS); // when Long dDay = goal.calculateDday(LocalDate.of(2023, 7, 1)); @@ -91,7 +91,7 @@ public class GoalTest { // given LocalDate startDate = LocalDate.of(2023,7,1); LocalDate endDate = LocalDate.of(2023,8,10); - Goal goal = new Goal(1L, "테스트 제목", startDate, endDate, true); + Goal goal = new Goal(1L, "테스트 제목", startDate, endDate, true, GoalStatus.PROCESS); // when & then assertThatThrownBy(() -> { @@ -106,7 +106,7 @@ public class GoalTest { // given LocalDate startDate = LocalDate.of(2023,7,1); LocalDate endDate = LocalDate.of(2023,8,10); - Goal goal = new Goal(1L, "테스트 제목", startDate, endDate, true); + Goal goal = new Goal(1L, "테스트 제목", startDate, endDate, true, GoalStatus.PROCESS); // when Long dDay = goal.calculateDday(LocalDate.of(2023, 8, 10));