From 4bb3c4d4ac0b3f57e6386a58d6c16f0349acb528 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Thu, 25 Sep 2025 16:13:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/controller/StudyPlanController.java | 13 +++-- .../plan/dto/StudyPlanDeleteRequest.java | 19 +++++++ .../study/plan/dto/StudyPlanResponse.java | 2 +- .../domain/study/plan/entity/RepeatRule.java | 2 +- .../study/plan/entity/StudyPlanException.java | 2 +- .../StudyPlanExceptionRepository.java | 3 +- .../study/plan/service/StudyPlanService.java | 51 +++++++++++++++++-- 7 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteRequest.java diff --git a/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java b/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java index 638d9e56..c257215f 100644 --- a/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java +++ b/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java @@ -1,5 +1,6 @@ package com.back.domain.study.plan.controller; +import com.back.domain.study.plan.dto.StudyPlanDeleteRequest; import com.back.domain.study.plan.dto.StudyPlanRequest; import com.back.domain.study.plan.dto.StudyPlanListResponse; import com.back.domain.study.plan.dto.StudyPlanResponse; @@ -85,9 +86,15 @@ public ResponseEntity> updateStudyPlan( // ==================== 삭제 =================== @DeleteMapping("/{planId}") - public ResponseEntity> deleteStudyPlan(@PathVariable Long planId) { - //studyPlanService.deleteStudyPlan(planId); - return ResponseEntity.ok(RsData.success("학습 계획이 성공적으로 삭제되었습니다.", null)); + public ResponseEntity> deleteStudyPlan( + // @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long planId, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate selectedDate, + @RequestBody(required = false) StudyPlanDeleteRequest request) { + Long userId = 1L; // 임시 + + studyPlanService.deleteStudyPlan(userId, planId, selectedDate, request); + return ResponseEntity.ok(RsData.success("학습 계획이 성공적으로 삭제되었습니다.")); } diff --git a/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteRequest.java b/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteRequest.java new file mode 100644 index 00000000..6b9b8624 --- /dev/null +++ b/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteRequest.java @@ -0,0 +1,19 @@ +package com.back.domain.study.plan.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class StudyPlanDeleteRequest { + private DeleteScope deleteScope; + + public enum DeleteScope { + THIS_ONLY, // 이 날짜만 + FROM_THIS_DATE // 이 날짜부터 이후 모든 날짜 + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/study/plan/dto/StudyPlanResponse.java b/src/main/java/com/back/domain/study/plan/dto/StudyPlanResponse.java index 8f4e1cf2..5438b6a7 100644 --- a/src/main/java/com/back/domain/study/plan/dto/StudyPlanResponse.java +++ b/src/main/java/com/back/domain/study/plan/dto/StudyPlanResponse.java @@ -44,7 +44,7 @@ public static class RepeatRuleResponse { private Integer repeatInterval; private String byDay; // "MON" 형태의 문자열 - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private LocalDate untilDate; public RepeatRuleResponse(com.back.domain.study.plan.entity.RepeatRule repeatRule) { diff --git a/src/main/java/com/back/domain/study/plan/entity/RepeatRule.java b/src/main/java/com/back/domain/study/plan/entity/RepeatRule.java index fe1d7268..2acca212 100644 --- a/src/main/java/com/back/domain/study/plan/entity/RepeatRule.java +++ b/src/main/java/com/back/domain/study/plan/entity/RepeatRule.java @@ -23,7 +23,7 @@ public class RepeatRule extends BaseEntity { private Frequency frequency; @Column(name = "interval_value", nullable = false) - private int RepeatInterval; + private int repeatInterval; //요일은 계획 날짜에 따라 자동 설정 @Column(name = "by_day") diff --git a/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java b/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java index d150cd5c..5a7718f6 100644 --- a/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java +++ b/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java @@ -26,7 +26,7 @@ public class StudyPlanException extends BaseEntity { // 예외가 발생한 날짜 @Column(name = "exception_date", nullable = false) - private LocalDateTime exceptionDate; + private LocalDate exceptionDate; //예외 유형 (수정 / 삭제) @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java b/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java index 1a634b79..b15476e2 100644 --- a/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java +++ b/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -32,5 +33,5 @@ List findByStudyPlanIdAndExceptionDateBetween(@Param("planId @Query("SELECT spe FROM StudyPlanException spe WHERE spe.studyPlan.id = :planId " + "AND DATE(spe.exceptionDate) = DATE(:targetDate)") Optional findByPlanIdAndDate(@Param("planId") Long planId, - @Param("targetDate") LocalDateTime targetDate); + @Param("targetDate") LocalDate targetDate); } diff --git a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java index 418df4a3..a517cfa7 100644 --- a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java +++ b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java @@ -1,5 +1,6 @@ package com.back.domain.study.plan.service; +import com.back.domain.study.plan.dto.StudyPlanDeleteRequest; import com.back.domain.study.plan.dto.StudyPlanRequest; import com.back.domain.study.plan.dto.StudyPlanResponse; import com.back.domain.study.plan.entity.RepeatRule; @@ -201,7 +202,7 @@ private boolean shouldRepeatOnDate(StudyPlan originalPlan, LocalDate targetDate) private StudyPlanException getEffectiveException(Long planId, LocalDate targetDate) { // 해당 날짜에 직접적인 예외가 있는지 확인 Optional directException = studyPlanExceptionRepository - .findByPlanIdAndDate(planId, targetDate.atStartOfDay()); + .findByPlanIdAndDate(planId, targetDate); if (directException.isPresent()) { return directException.get(); } @@ -320,7 +321,7 @@ private UpdateType determineUpdateType(StudyPlan originalPlan, StudyPlanRequest // 1-2. 반복 계획에서 다른 날짜인 경우 -> 기존 예외 확인 Optional existingException = studyPlanExceptionRepository - .findByPlanIdAndDate(originalPlan.getId(), requestDate.atStartOfDay()); + .findByPlanIdAndDate(originalPlan.getId(), requestDate); if (existingException.isPresent()) { return UpdateType.REPEAT_INSTANCE_UPDATE; // 기존 예외 수정 @@ -357,7 +358,7 @@ private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPla StudyPlanException exception = new StudyPlanException(); exception.setStudyPlan(originalPlan); - exception.setExceptionDate(exceptionDate.atStartOfDay()); + exception.setExceptionDate(exceptionDate); exception.setExceptionType(StudyPlanException.ExceptionType.MODIFIED); exception.setApplyScope(applyScope); // 파라미터로 받은 applyScope @@ -396,7 +397,7 @@ private StudyPlanResponse updateExistingException(StudyPlan originalPlan, StudyP LocalDate exceptionDate = request.getStartDate().toLocalDate(); StudyPlanException existingException = studyPlanExceptionRepository - .findByPlanIdAndDate(originalPlan.getId(), exceptionDate.atStartOfDay()) + .findByPlanIdAndDate(originalPlan.getId(), exceptionDate) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND)); // 기존 예외 정보 업데이트 @@ -449,7 +450,49 @@ private void updateRepeatRule(RepeatRule repeatRule, StudyPlanRequest.RepeatRule } // ==================== 삭제 =================== + @Transactional + public void deleteStudyPlan(Long userId, Long planId, LocalDate selectedDate, StudyPlanDeleteRequest request) { + StudyPlan studyPlan = studyPlanRepository.findById(planId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND)); + + validateUserAccess(studyPlan, userId); + // 단발성 계획 삭제 (request가 null이거나 deleteScope가 없는 경우) + if (studyPlan.getRepeatRule() == null || request == null || request.getDeleteScope() == null) { + studyPlanRepository.delete(studyPlan); + return; + } + + // 반복성 계획 삭제 - deleteScope에 따른 처리 + deleteRepeatPlan(studyPlan, selectedDate, request.getDeleteScope()); + } + + private void deleteRepeatPlan(StudyPlan studyPlan, LocalDate selectedDate, StudyPlanDeleteRequest.DeleteScope deleteScope) { + switch (deleteScope) { + case FROM_THIS_DATE: + // 원본 날짜부터 삭제하는 경우 = 전체 계획 삭제 + if (selectedDate.equals(studyPlan.getStartDate().toLocalDate())) { + studyPlanRepository.delete(studyPlan); // CASCADE로 RepeatRule, Exception 모두 삭제 + } else { + // 중간 날짜부터 삭제하는 경우 = untilDate 수정 + RepeatRule repeatRule = studyPlan.getRepeatRule(); + LocalDate newUntilDate = selectedDate.minusDays(1); + repeatRule.setUntilDate(newUntilDate); + studyPlanRepository.save(studyPlan); + } + break; + + case THIS_ONLY: + // 선택한 날짜만 삭제 - 예외 생성 + StudyPlanException exception = new StudyPlanException(); + exception.setStudyPlan(studyPlan); + exception.setExceptionDate(selectedDate); + exception.setExceptionType(StudyPlanException.ExceptionType.DELETED); + exception.setApplyScope(StudyPlanException.ApplyScope.THIS_ONLY); + studyPlanExceptionRepository.save(exception); + break; + } + } // ==================== 유틸 =================== // 인가 (작성자 일치 확인) From dafe4085e0d47c75fc25ed44a3f63748b4bd2931 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Thu, 25 Sep 2025 16:31:30 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refect:=20custom=20error=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/plan/service/StudyPlanService.java | 18 ++++++++---------- .../com/back/global/exception/ErrorCode.java | 7 +++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java index a517cfa7..a9a6772c 100644 --- a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java +++ b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java @@ -282,7 +282,7 @@ private enum UpdateType { @Transactional public StudyPlanResponse updateStudyPlan(Long userId, Long planId, StudyPlanRequest request, StudyPlanException.ApplyScope applyScope) { StudyPlan originalPlan = studyPlanRepository.findById(planId) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND)); + .orElseThrow(() -> new CustomException(ErrorCode.PLAN_NOT_FOUND)); validateUserAccess(originalPlan, userId); @@ -319,7 +319,7 @@ private UpdateType determineUpdateType(StudyPlan originalPlan, StudyPlanRequest return UpdateType.ORIGINAL_PLAN_UPDATE; } - // 1-2. 반복 계획에서 다른 날짜인 경우 -> 기존 예외 확인 + // 1-2. 반복 계획에서 다른 날짜인 경우 -> 기존 예외 존재 유무 확인 Optional existingException = studyPlanExceptionRepository .findByPlanIdAndDate(originalPlan.getId(), requestDate); @@ -353,7 +353,7 @@ private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPla // 해당 날짜에 실제로 반복 계획이 있는지 확인 if (!shouldRepeatOnDate(originalPlan, exceptionDate)) { - throw new CustomException(ErrorCode.BAD_REQUEST); + throw new CustomException(ErrorCode.PLAN_ORIGINAL_REPEAT_NOT_FOUND); } StudyPlanException exception = new StudyPlanException(); @@ -380,7 +380,7 @@ private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPla LocalDate untilDate = LocalDate.parse(request.getRepeatRule().getUntilDate()); embeddable.setUntilDate(untilDate); } catch (Exception e) { - throw new CustomException(ErrorCode.BAD_REQUEST); + throw new CustomException(ErrorCode.PLAN_INVALID_DATE_FORMAT); } } @@ -398,7 +398,7 @@ private StudyPlanResponse updateExistingException(StudyPlan originalPlan, StudyP StudyPlanException existingException = studyPlanExceptionRepository .findByPlanIdAndDate(originalPlan.getId(), exceptionDate) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND)); + .orElse(null); // 기존 예외 정보 업데이트 if (request.getSubject() != null) existingException.setModifiedSubject(request.getSubject()); @@ -421,7 +421,7 @@ private StudyPlanResponse updateExistingException(StudyPlan originalPlan, StudyP LocalDate untilDate = LocalDate.parse(request.getRepeatRule().getUntilDate()); embeddable.setUntilDate(untilDate); } catch (Exception e) { - throw new CustomException(ErrorCode.BAD_REQUEST); + throw new CustomException(ErrorCode.PLAN_INVALID_DATE_FORMAT); } } @@ -444,7 +444,7 @@ private void updateRepeatRule(RepeatRule repeatRule, StudyPlanRequest.RepeatRule LocalDate untilDate = LocalDate.parse(request.getUntilDate()); repeatRule.setUntilDate(untilDate); } catch (Exception e) { - throw new CustomException(ErrorCode.BAD_REQUEST); + throw new CustomException(ErrorCode.PLAN_INVALID_DATE_FORMAT); } } } @@ -453,7 +453,7 @@ private void updateRepeatRule(RepeatRule repeatRule, StudyPlanRequest.RepeatRule @Transactional public void deleteStudyPlan(Long userId, Long planId, LocalDate selectedDate, StudyPlanDeleteRequest request) { StudyPlan studyPlan = studyPlanRepository.findById(planId) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND)); + .orElseThrow(() -> new CustomException(ErrorCode.PLAN_NOT_FOUND)); validateUserAccess(studyPlan, userId); @@ -503,6 +503,4 @@ private void validateUserAccess(StudyPlan studyPlan, Long userId) { } - - } diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index 466b22c8..64d70f37 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -32,6 +32,13 @@ public enum ErrorCode { CANNOT_KICK_HOST(HttpStatus.BAD_REQUEST, "ROOM_010", "방장은 추방할 수 없습니다."), CANNOT_CHANGE_HOST_ROLE(HttpStatus.BAD_REQUEST, "ROOM_011", "방장의 권한은 변경할 수 없습니다."), + // ======================== 스터디 플래너 관련 ======================== + PLAN_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAN_001", "존재하지 않는 학습 계획입니다."), + PLAN_FORBIDDEN(HttpStatus.FORBIDDEN, "PLAN_002", "학습 계획에 대한 접근 권한이 없습니다."), + PLAN_EXCEPTION_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAN_003", "학습 계획의 예외가 존재하지 않습니다."), + PLAN_ORIGINAL_REPEAT_NOT_FOUND(HttpStatus.BAD_REQUEST, "PLAN_004", "해당 날짜에 원본 반복 계획을 찾을 수 없습니다."), + PLAN_INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST, "PLAN_005", "날짜 형식이 올바르지 않습니다. (YYYY-MM-DD 형식을 사용해주세요)"), + // ======================== 메시지 관련 ======================== MESSAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "MESSAGE_001", "존재하지 않는 메시지입니다."), MESSAGE_FORBIDDEN(HttpStatus.FORBIDDEN, "MESSAGE_002", "자신의 메시지만 삭제할 수 있습니다."), From 80f8dcb2f201074ae941b696e37d3ffb3fd56308 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Thu, 25 Sep 2025 16:50:45 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refect:=20=EC=88=98=EC=A0=95,=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20scope=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/controller/StudyPlanController.java | 13 +++++---- .../domain/study/plan/entity/ApplyScope.java | 6 ++++ .../study/plan/entity/StudyPlanException.java | 5 ---- .../StudyPlanExceptionRepository.java | 3 +- .../study/plan/service/StudyPlanService.java | 29 +++++++++---------- 5 files changed, 28 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/back/domain/study/plan/entity/ApplyScope.java diff --git a/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java b/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java index c257215f..5fffdf6f 100644 --- a/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java +++ b/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java @@ -4,7 +4,7 @@ import com.back.domain.study.plan.dto.StudyPlanRequest; import com.back.domain.study.plan.dto.StudyPlanListResponse; import com.back.domain.study.plan.dto.StudyPlanResponse; -import com.back.domain.study.plan.entity.StudyPlanException; +import com.back.domain.study.plan.entity.ApplyScope; import com.back.domain.study.plan.service.StudyPlanService; import com.back.global.common.dto.RsData; import com.back.global.security.CustomUserDetails; @@ -74,7 +74,7 @@ public ResponseEntity> updateStudyPlan( // @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long planId, @RequestBody StudyPlanRequest request, - @RequestParam(required = false, defaultValue = "THIS_ONLY") StudyPlanException.ApplyScope applyScope) { + @RequestParam(required = false, defaultValue = "THIS_ONLY") ApplyScope applyScope) { // Long userId = user.getId(); Long userId = 1L; // 임시 @@ -85,18 +85,19 @@ public ResponseEntity> updateStudyPlan( // ==================== 삭제 =================== + // 플랜 아이디는 원본의 아이디를 받음 + // 가상인지 원본인지는 서비스에서 원본과 날짜를 대조해 판단 + // 삭제 적용 범위를 쿼리 파라미터로 받음 (THIS_ONLY, FROM_THIS_DATE) @DeleteMapping("/{planId}") public ResponseEntity> deleteStudyPlan( // @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long planId, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate selectedDate, - @RequestBody(required = false) StudyPlanDeleteRequest request) { + @RequestParam(required = false) ApplyScope applyScope) { Long userId = 1L; // 임시 - studyPlanService.deleteStudyPlan(userId, planId, selectedDate, request); + studyPlanService.deleteStudyPlan(userId, planId, selectedDate, applyScope); return ResponseEntity.ok(RsData.success("학습 계획이 성공적으로 삭제되었습니다.")); } - - } diff --git a/src/main/java/com/back/domain/study/plan/entity/ApplyScope.java b/src/main/java/com/back/domain/study/plan/entity/ApplyScope.java new file mode 100644 index 00000000..9b1a90aa --- /dev/null +++ b/src/main/java/com/back/domain/study/plan/entity/ApplyScope.java @@ -0,0 +1,6 @@ +package com.back.domain.study.plan.entity; + +public enum ApplyScope { + THIS_ONLY, // 이 날짜만 + FROM_THIS_DATE // 이 날짜부터 이후 모든 날짜 +} diff --git a/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java b/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java index 5a7718f6..2137a6ae 100644 --- a/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java +++ b/src/main/java/com/back/domain/study/plan/entity/StudyPlanException.java @@ -57,11 +57,6 @@ public enum ExceptionType { MODIFIED // 해당 날짜 수정 } - public enum ApplyScope { - THIS_ONLY, // 이 날짜만 - FROM_THIS_DATE // 이 날짜부터 이후 모든 날짜 - } - @Embedded @AttributeOverrides({ @AttributeOverride(name = "frequency", column = @Column(name = "modified_frequency")), diff --git a/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java b/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java index b15476e2..71fba8d7 100644 --- a/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java +++ b/src/main/java/com/back/domain/study/plan/repository/StudyPlanExceptionRepository.java @@ -1,5 +1,6 @@ package com.back.domain.study.plan.repository; +import com.back.domain.study.plan.entity.ApplyScope; import com.back.domain.study.plan.entity.StudyPlanException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -20,7 +21,7 @@ public interface StudyPlanExceptionRepository extends JpaRepository findByStudyPlanIdAndApplyScopeAndExceptionDateBefore( @Param("planId") Long planId, - @Param("applyScope") StudyPlanException.ApplyScope applyScope, + @Param("applyScope") ApplyScope applyScope, @Param("targetDate") LocalDateTime targetDate); // 특정 계획의 특정 기간 동안(start~end)의 예외를 조회 @Query("SELECT spe FROM StudyPlanException spe WHERE spe.studyPlan.id = :planId " + diff --git a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java index a9a6772c..084cb33b 100644 --- a/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java +++ b/src/main/java/com/back/domain/study/plan/service/StudyPlanService.java @@ -3,10 +3,7 @@ import com.back.domain.study.plan.dto.StudyPlanDeleteRequest; import com.back.domain.study.plan.dto.StudyPlanRequest; import com.back.domain.study.plan.dto.StudyPlanResponse; -import com.back.domain.study.plan.entity.RepeatRule; -import com.back.domain.study.plan.entity.RepeatRuleEmbeddable; -import com.back.domain.study.plan.entity.StudyPlan; -import com.back.domain.study.plan.entity.StudyPlanException; +import com.back.domain.study.plan.entity.*; import com.back.domain.study.plan.repository.StudyPlanExceptionRepository; import com.back.domain.study.plan.repository.StudyPlanRepository; import com.back.global.exception.CustomException; @@ -211,7 +208,7 @@ private StudyPlanException getEffectiveException(Long planId, LocalDate targetDa List scopeExceptions = studyPlanExceptionRepository .findByStudyPlanIdAndApplyScopeAndExceptionDateBefore( planId, - StudyPlanException.ApplyScope.FROM_THIS_DATE, + ApplyScope.FROM_THIS_DATE, targetDate.atStartOfDay() ); @@ -280,7 +277,7 @@ private enum UpdateType { REPEAT_INSTANCE_UPDATE // 기존 예외 수정 } @Transactional - public StudyPlanResponse updateStudyPlan(Long userId, Long planId, StudyPlanRequest request, StudyPlanException.ApplyScope applyScope) { + public StudyPlanResponse updateStudyPlan(Long userId, Long planId, StudyPlanRequest request, ApplyScope applyScope) { StudyPlan originalPlan = studyPlanRepository.findById(planId) .orElseThrow(() -> new CustomException(ErrorCode.PLAN_NOT_FOUND)); @@ -348,7 +345,7 @@ private StudyPlanResponse updateOriginalPlan(StudyPlan originalPlan, StudyPlanRe } // 새로운 예외 추가 - private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPlanRequest request, StudyPlanException.ApplyScope applyScope) { + private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPlanRequest request, ApplyScope applyScope) { LocalDate exceptionDate = request.getStartDate().toLocalDate(); // 해당 날짜에 실제로 반복 계획이 있는지 확인 @@ -393,7 +390,7 @@ private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPla } // 기존 예외 수정 - private StudyPlanResponse updateExistingException(StudyPlan originalPlan, StudyPlanRequest request, StudyPlanException.ApplyScope applyScope) { + private StudyPlanResponse updateExistingException(StudyPlan originalPlan, StudyPlanRequest request, ApplyScope applyScope) { LocalDate exceptionDate = request.getStartDate().toLocalDate(); StudyPlanException existingException = studyPlanExceptionRepository @@ -451,24 +448,24 @@ private void updateRepeatRule(RepeatRule repeatRule, StudyPlanRequest.RepeatRule // ==================== 삭제 =================== @Transactional - public void deleteStudyPlan(Long userId, Long planId, LocalDate selectedDate, StudyPlanDeleteRequest request) { + public void deleteStudyPlan(Long userId, Long planId, LocalDate selectedDate, ApplyScope applyScope) { StudyPlan studyPlan = studyPlanRepository.findById(planId) .orElseThrow(() -> new CustomException(ErrorCode.PLAN_NOT_FOUND)); validateUserAccess(studyPlan, userId); - // 단발성 계획 삭제 (request가 null이거나 deleteScope가 없는 경우) - if (studyPlan.getRepeatRule() == null || request == null || request.getDeleteScope() == null) { + // 단발성 계획 삭제 (반복 룰이 null이거나 applyScope가 null인 경우) + if (studyPlan.getRepeatRule() == null || applyScope == null ) { studyPlanRepository.delete(studyPlan); return; } - // 반복성 계획 삭제 - deleteScope에 따른 처리 - deleteRepeatPlan(studyPlan, selectedDate, request.getDeleteScope()); + // 반복성 계획 삭제 - applyScope에 따른 처리 + deleteRepeatPlan(studyPlan, selectedDate, applyScope); } - private void deleteRepeatPlan(StudyPlan studyPlan, LocalDate selectedDate, StudyPlanDeleteRequest.DeleteScope deleteScope) { - switch (deleteScope) { + private void deleteRepeatPlan(StudyPlan studyPlan, LocalDate selectedDate, ApplyScope applyScope) { + switch (applyScope) { case FROM_THIS_DATE: // 원본 날짜부터 삭제하는 경우 = 전체 계획 삭제 if (selectedDate.equals(studyPlan.getStartDate().toLocalDate())) { @@ -488,7 +485,7 @@ private void deleteRepeatPlan(StudyPlan studyPlan, LocalDate selectedDate, Study exception.setStudyPlan(studyPlan); exception.setExceptionDate(selectedDate); exception.setExceptionType(StudyPlanException.ExceptionType.DELETED); - exception.setApplyScope(StudyPlanException.ApplyScope.THIS_ONLY); + exception.setApplyScope(ApplyScope.THIS_ONLY); studyPlanExceptionRepository.save(exception); break; } From ab83458601ccbd169f932e7bc8628a576fa37551 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Thu, 25 Sep 2025 17:37:20 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20plan=20API=20swagger=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/controller/StudyPlanController.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java b/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java index 5fffdf6f..f8e7543f 100644 --- a/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java +++ b/src/main/java/com/back/domain/study/plan/controller/StudyPlanController.java @@ -1,6 +1,5 @@ package com.back.domain.study.plan.controller; -import com.back.domain.study.plan.dto.StudyPlanDeleteRequest; import com.back.domain.study.plan.dto.StudyPlanRequest; import com.back.domain.study.plan.dto.StudyPlanListResponse; import com.back.domain.study.plan.dto.StudyPlanResponse; @@ -8,6 +7,8 @@ import com.back.domain.study.plan.service.StudyPlanService; import com.back.global.common.dto.RsData; import com.back.global.security.CustomUserDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; @@ -20,10 +21,15 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/plans") +@Tag(name = "StudyPlan", description = "학습 계획 관련 API") public class StudyPlanController { private final StudyPlanService studyPlanService; // ==================== 생성 =================== @PostMapping + @Operation( summary = "학습 계획 생성", + description = "새로운 학습 계획을 생성합니다. 반복 계획 생성 시 최초 계획이 원본 계획으로서 db에 저장되고" + + " 이후 반복되는 계획들은 가상 계획으로서 db에는 없지만 조회 시 가상으로 생성됩니다") + public ResponseEntity> createStudyPlan( // 로그인 유저 정보 받기 @AuthenticationPrincipal CustomUserDetails user, @RequestBody StudyPlanRequest request) { @@ -36,6 +42,10 @@ public ResponseEntity> createStudyPlan( // ==================== 조회 =================== // 특정 날짜의 계획들 조회. date 형식: YYYY-MM-DD @GetMapping("/date/{date}") + @Operation( + summary = "특정 날짜의 학습 계획 조회", + description = "지정 날짜에 해당하는 모든 학습 계획을 조회합니다." + ) public ResponseEntity> getStudyPlansForDate( @AuthenticationPrincipal CustomUserDetails user, @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { @@ -51,6 +61,10 @@ public ResponseEntity> getStudyPlansForDate( // 기간별 계획 조회. start, end 형식: YYYY-MM-DD @GetMapping + @Operation( + summary = "기간별 학습 계획 조회", + description = "기간에 해당하는 모든 학습 계획을 조회합니다." + ) public ResponseEntity>> getStudyPlansForPeriod( // @AuthenticationPrincipal CustomUserDetails user, @RequestParam("start") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @@ -70,6 +84,11 @@ public ResponseEntity>> getStudyPlansForPeriod( // 가상인지 원본인지는 서비스에서 원본과 날짜를 대조해 판단 // 수정 적용 범위를 쿼리 파라미터로 받음 (THIS_ONLY, FROM_THIS_DATE) @PutMapping("/{planId}") + @Operation( + summary = "학습 계획 수정", + description = "기존 학습 계획을 수정합니다. 반복 계획의 경우 적용 범위를 applyScope로 설정 할 수 있으며" + + "클라이언트에서는 paln에 repeat_rule이 있으면 반복 계획으로 간주하고 반드시 apply_scope를 쿼리 파라미터로 넘겨야 합니다." + + "repeat_rule이 없으면 단발성 계획으로 간주하여 수정 범위를 설정 할 필요가 없으므로 apply_scope를 넘기지 않아도 됩니다.") public ResponseEntity> updateStudyPlan( // @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long planId, @@ -83,12 +102,16 @@ public ResponseEntity> updateStudyPlan( } - // ==================== 삭제 =================== // 플랜 아이디는 원본의 아이디를 받음 // 가상인지 원본인지는 서비스에서 원본과 날짜를 대조해 판단 // 삭제 적용 범위를 쿼리 파라미터로 받음 (THIS_ONLY, FROM_THIS_DATE) @DeleteMapping("/{planId}") + @Operation( + summary = "학습 계획 삭제", + description = "기존 학습 계획을 삭제합니다. 반복 계획의 경우 적용 범위를 applyScope로 설정 할 수 있으며" + + "클라이언트에서는 paln에 repeat_rule이 있으면 반복 계획으로 간주하고 반드시 apply_scope를 쿼리 파라미터로 넘겨야 합니다." + + "repeat_rule이 없으면 단발성 계획으로 간주하여 삭제 범위를 설정 할 필요가 없으므로 apply_scope를 넘기지 않아도 됩니다.") public ResponseEntity> deleteStudyPlan( // @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long planId,