From a6b39d0f9a5b6c3b93e4526d30218a4d84306411 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Wed, 1 Oct 2025 11:43:51 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix=20:=20byDay=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=88=98=EC=A0=95=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20+=20refect:=20byDay=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/plan/dto/StudyPlanRequest.java | 3 +- .../study/plan/dto/StudyPlanResponse.java | 12 +- .../domain/study/plan/entity/RepeatRule.java | 2 +- .../plan/entity/RepeatRuleEmbeddable.java | 2 +- .../domain/study/plan/entity/RepeatType.java | 6 - .../study/plan/service/StudyPlanService.java | 112 ++++++++++-------- .../controller/StudyPlanControllerTest.java | 34 +++--- 7 files changed, 85 insertions(+), 86 deletions(-) delete mode 100644 src/main/java/com/back/domain/study/plan/entity/RepeatType.java diff --git a/src/main/java/com/back/domain/study/plan/dto/StudyPlanRequest.java b/src/main/java/com/back/domain/study/plan/dto/StudyPlanRequest.java index 6a1c7d42..53935bb8 100644 --- a/src/main/java/com/back/domain/study/plan/dto/StudyPlanRequest.java +++ b/src/main/java/com/back/domain/study/plan/dto/StudyPlanRequest.java @@ -1,6 +1,7 @@ package com.back.domain.study.plan.dto; import com.back.domain.study.plan.entity.Color; +import com.back.domain.study.plan.entity.DayOfWeek; import com.back.domain.study.plan.entity.Frequency; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; @@ -37,7 +38,7 @@ public class StudyPlanRequest { public static class RepeatRuleRequest { private Frequency frequency; private Integer intervalValue; - private List byDay = new ArrayList<>(); // 문자열 리스트 + private List byDay = new ArrayList<>(); @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private String untilDate; // "2025-12-31" 형태 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 5e1dda0a..851f5f7b 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 Frequency frequency; private Integer repeatInterval; // byDay 필드는 이미 List으로 선언되어 있음. - private List byDay = new ArrayList<>(); // "MON" 형태의 문자열 리스트 + private List byDay = new ArrayList<>(); // "MON" 형태의 문자열 리스트 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private LocalDate untilDate; @@ -60,16 +60,6 @@ public RepeatRuleResponse(com.back.domain.study.plan.entity.RepeatRule repeatRul } } - public List getByDaysList() { - if (byDay == null || byDay.isEmpty()) { - return List.of(); - } - - // List의 각 요소를 DayOfWeek enum으로 변환하여 반환 - return byDay.stream() - .map(com.back.domain.study.plan.entity.DayOfWeek::valueOf) - .collect(Collectors.toList()); - } } //엔티티를 DTO로 변환하는 생성자 public StudyPlanResponse(StudyPlan studyPlan) { 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 7b758a77..afe60418 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 @@ -29,7 +29,7 @@ public class RepeatRule extends BaseEntity { //요일은 응답에 들어있는 요일을 그대로 저장 (예: "WED") @Column(name = "by_day") - private List byDay = new ArrayList<>(); + private List byDay = new ArrayList<>(); private LocalDate untilDate; } diff --git a/src/main/java/com/back/domain/study/plan/entity/RepeatRuleEmbeddable.java b/src/main/java/com/back/domain/study/plan/entity/RepeatRuleEmbeddable.java index 2e5552d3..ac12aef9 100644 --- a/src/main/java/com/back/domain/study/plan/entity/RepeatRuleEmbeddable.java +++ b/src/main/java/com/back/domain/study/plan/entity/RepeatRuleEmbeddable.java @@ -22,6 +22,6 @@ public class RepeatRuleEmbeddable { private Frequency frequency; private Integer intervalValue; - private List byDay = new ArrayList<>(); + private List byDay = new ArrayList<>(); private LocalDate untilDate; // LocalDateTime → LocalDate 변경 } \ No newline at end of file diff --git a/src/main/java/com/back/domain/study/plan/entity/RepeatType.java b/src/main/java/com/back/domain/study/plan/entity/RepeatType.java deleted file mode 100644 index ff7238ba..00000000 --- a/src/main/java/com/back/domain/study/plan/entity/RepeatType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.back.domain.study.plan.entity; - -//삭제 예정 -public enum RepeatType { - NONE, DAILY, WEEKLY, MONTHLY -} 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 3493c4d4..6e6f8ea6 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,6 +1,5 @@ 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.*; @@ -69,19 +68,9 @@ private RepeatRule createRepeatRule(StudyPlanRequest.RepeatRuleRequest request, repeatRule.setStudyPlan(studyPlan); repeatRule.setFrequency(request.getFrequency()); repeatRule.setRepeatInterval(request.getIntervalValue() != null ? request.getIntervalValue() : 1); + // byDay 설정 (WEEKLY인 경우에만 의미 있음) + getByDayInWeekly(request, studyPlan, repeatRule); - // byDay 설정 (WEEKLY 인 경우에만) - if (request.getFrequency() == Frequency.WEEKLY) { - // 1. byDay가 없으면 시작일 요일을 자동으로 설정 (현재 구현 의도 반영) - if(request.getByDay() == null || request.getByDay().isEmpty()) { - String startDayOfWeek = studyPlan.getStartDate().getDayOfWeek().name().substring(0, 3); - // *가정: RepeatRule.byDay는 List 타입으로 가정 - repeatRule.setByDay(List.of(startDayOfWeek)); - } else { - // 2. byDay가 있다면 요청 값을 사용 (List to List 매핑 확인) - repeatRule.setByDay(request.getByDay()); - } - } // untilDate 설정 및 검증 LocalDate untilDate; @@ -335,6 +324,10 @@ public StudyPlanResponse updateStudyPlan(Long userId, Long planId, StudyPlanRequ switch (updateType) { case ORIGINAL_PLAN_UPDATE: + // 요청에 반복 규칙이 있으면 반복 규칙 수정 후 원본 계획 수정 + if (request.getRepeatRule() != null) { + updateRepeatRule(originalPlan.getRepeatRule(), request.getRepeatRule(), originalPlan); + } return updateOriginalPlan(originalPlan, request); case REPEAT_INSTANCE_CREATE: @@ -390,11 +383,6 @@ private StudyPlanResponse updateOriginalPlan(StudyPlan originalPlan, StudyPlanRe if (request.getEndDate() != null) originalPlan.setEndDate(request.getEndDate()); if (request.getColor() != null) originalPlan.setColor(request.getColor()); - // 요청에 반복 규칙이 있고 원본 반복성 계획인 경우에만 반복 규칙 수정 - if (request.getRepeatRule() != null && originalPlan.getRepeatRule() != null) { - updateRepeatRule(originalPlan.getRepeatRule(), request.getRepeatRule()); - } - StudyPlan savedPlan = studyPlanRepository.save(originalPlan); return new StudyPlanResponse(savedPlan); } @@ -412,7 +400,7 @@ private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPla exception.setStudyPlan(originalPlan); exception.setExceptionDate(exceptionDate); exception.setExceptionType(StudyPlanException.ExceptionType.MODIFIED); - exception.setApplyScope(applyScope); // 파라미터로 받은 applyScope + exception.setApplyScope(applyScope); // 수정된 내용 설정 if (request.getSubject() != null) exception.setModifiedSubject(request.getSubject()); @@ -422,24 +410,10 @@ private StudyPlanResponse createRepeatException(StudyPlan originalPlan, StudyPla // 반복 규칙 수정. 요청에 반복 규칙이 있으면 설정 if (request.getRepeatRule() != null) { - RepeatRuleEmbeddable embeddable = new RepeatRuleEmbeddable(); - embeddable.setFrequency(request.getRepeatRule().getFrequency()); - embeddable.setIntervalValue(request.getRepeatRule().getIntervalValue()); - embeddable.setByDay(request.getRepeatRule().getByDay()); - - if (request.getRepeatRule().getUntilDate() != null && !request.getRepeatRule().getUntilDate().isEmpty()) { - try { - LocalDate untilDate = LocalDate.parse(request.getRepeatRule().getUntilDate()); - embeddable.setUntilDate(untilDate); - } catch (Exception e) { - throw new CustomException(ErrorCode.INVALID_DATE_FORMAT); - } - } - + RepeatRuleEmbeddable embeddable = createRepeatRuleEmbeddable(request.getRepeatRule(), request.getStartDate()); exception.setModifiedRepeatRule(embeddable); } - studyPlanExceptionRepository.save(exception); return createVirtualPlanForDate(originalPlan, exceptionDate); } @@ -458,25 +432,12 @@ private StudyPlanResponse updateExistingException(StudyPlan originalPlan, StudyP if (request.getEndDate() != null) existingException.setModifiedEndDate(request.getEndDate()); if (request.getColor() != null) existingException.setModifiedColor(request.getColor()); - // ApplyScope도 업데이트 (사용자가 범위를 변경할 수 있음) + // ApplyScope도 업데이트 existingException.setApplyScope(applyScope); // 반복 규칙 수정사항 있으면 예외 안에 추가 (embeddable) if (request.getRepeatRule() != null) { - RepeatRuleEmbeddable embeddable = new RepeatRuleEmbeddable(); - embeddable.setFrequency(request.getRepeatRule().getFrequency()); - embeddable.setIntervalValue(request.getRepeatRule().getIntervalValue()); - embeddable.setByDay(request.getRepeatRule().getByDay()); - - if (request.getRepeatRule().getUntilDate() != null && !request.getRepeatRule().getUntilDate().isEmpty()) { - try { - LocalDate untilDate = LocalDate.parse(request.getRepeatRule().getUntilDate()); - embeddable.setUntilDate(untilDate); - } catch (Exception e) { - throw new CustomException(ErrorCode.INVALID_DATE_FORMAT); - } - } - + RepeatRuleEmbeddable embeddable = createRepeatRuleEmbeddable(request.getRepeatRule(), request.getStartDate()); existingException.setModifiedRepeatRule(embeddable); } @@ -484,12 +445,13 @@ private StudyPlanResponse updateExistingException(StudyPlan originalPlan, StudyP return createVirtualPlanForDate(originalPlan, exceptionDate); } - // 원본의 반복 룰 수정 (엔티티) - private void updateRepeatRule(RepeatRule repeatRule, StudyPlanRequest.RepeatRuleRequest request) { + private void updateRepeatRule(RepeatRule repeatRule, StudyPlanRequest.RepeatRuleRequest request, StudyPlan studyPlan) { if (request.getFrequency() != null) repeatRule.setFrequency(request.getFrequency()); if (request.getIntervalValue() != null) repeatRule.setRepeatInterval(request.getIntervalValue()); - if (request.getByDay() != null) repeatRule.setByDay(request.getByDay()); + + // byDay 자동 설정 (기존 메서드 재사용) + getByDayInWeekly(request, studyPlan, repeatRule); if (request.getUntilDate() != null && !request.getUntilDate().isEmpty()) { try { @@ -501,6 +463,27 @@ private void updateRepeatRule(RepeatRule repeatRule, StudyPlanRequest.RepeatRule } } + // RepeatRuleEmbeddable 생성 헬퍼 메서드 (중복 코드 제거) + private RepeatRuleEmbeddable createRepeatRuleEmbeddable(StudyPlanRequest.RepeatRuleRequest request, LocalDateTime startDate) { + RepeatRuleEmbeddable embeddable = new RepeatRuleEmbeddable(); + embeddable.setFrequency(request.getFrequency()); + embeddable.setIntervalValue(request.getIntervalValue()); + + // byDay 자동 설정 (오버로딩된 메서드 사용) + getByDayInWeekly(request, startDate, embeddable); + + if (request.getUntilDate() != null && !request.getUntilDate().isEmpty()) { + try { + LocalDate untilDate = LocalDate.parse(request.getUntilDate()); + embeddable.setUntilDate(untilDate); + } catch (Exception e) { + throw new CustomException(ErrorCode.INVALID_DATE_FORMAT); + } + } + + return embeddable; + } + // ==================== 삭제 =================== @Transactional public void deleteStudyPlan(Long userId, Long planId, LocalDate selectedDate, ApplyScope applyScope) { @@ -617,5 +600,30 @@ private void validateRepeatRuleDate(StudyPlan studyPlan, LocalDate untilDate) { throw new CustomException(ErrorCode.REPEAT_INVALID_UNTIL_DATE); } } + // WEEKLY인 경우 빈 byDay 처리 메서드 (RepeatRule용) + private void getByDayInWeekly(StudyPlanRequest.RepeatRuleRequest request, StudyPlan studyPlan, RepeatRule repeatRule) { + // byDay 설정 (WEEKLY 인 경우에만) + if (request.getFrequency() == Frequency.WEEKLY) { + // 1. byDay가 없으면 시작일 요일을 자동으로 설정 + if(request.getByDay() == null || request.getByDay().isEmpty()) { + DayOfWeek startDay = DayOfWeek.valueOf(studyPlan.getStartDate().getDayOfWeek().name().substring(0,3)); + repeatRule.setByDay(List.of(startDay)); + } else { + // 2. byDay가 있다면 요청 값을 사용 + repeatRule.setByDay(request.getByDay()); + } + } + } + // WEEKLY인 경우 빈 byDay 처리 메서드 (RepeatRuleEmbeddable용 - 오버로딩) + private void getByDayInWeekly(StudyPlanRequest.RepeatRuleRequest request, LocalDateTime startDate, RepeatRuleEmbeddable embeddable) { + if (request.getFrequency() == Frequency.WEEKLY) { + if (request.getByDay() == null || request.getByDay().isEmpty()) { + DayOfWeek startDay = DayOfWeek.valueOf(startDate.getDayOfWeek().name().substring(0, 3)); + embeddable.setByDay(List.of(startDay)); + } else { + embeddable.setByDay(request.getByDay()); + } + } + } } diff --git a/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java b/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java index 62ff8ad2..233446da 100644 --- a/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java +++ b/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java @@ -36,6 +36,8 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -200,7 +202,7 @@ void t2() throws Exception { .andExpect(jsonPath("$.data.endDate").value("2025-09-26T11:46:00")) .andExpect(jsonPath("$.data.repeatRule.frequency").value("DAILY")) .andExpect(jsonPath("$.data.repeatRule.repeatInterval").value(1)) - .andExpect(jsonPath("$.data.repeatRule.byDay", Matchers.hasSize(0))) + .andExpect(jsonPath("$.data.repeatRule.byDay", hasSize(0))) .andExpect(jsonPath("$.data.repeatRule.untilDate").value("2025-12-31")); } @@ -295,7 +297,7 @@ void t5() throws Exception { .andExpect(jsonPath("$.message").value("해당 날짜의 계획을 조회했습니다.")) .andExpect(jsonPath("$.data.date").value("2025-09-29")) .andExpect(jsonPath("$.data.totalCount").value(1)) - .andExpect(jsonPath("$.data.plans", Matchers.hasSize(1))) + .andExpect(jsonPath("$.data.plans", hasSize(1))) .andExpect(jsonPath("$.data.plans[0].subject").value("Java 공부")) .andExpect(jsonPath("$.data.plans[0].color").value("BLUE")) .andExpect(jsonPath("$.data.plans[0].startDate").value("2025-09-29T09:00:00")) @@ -318,7 +320,7 @@ void t6() throws Exception { .andExpect(jsonPath("$.message").value("해당 날짜의 계획을 조회했습니다.")) .andExpect(jsonPath("$.data.date").value("2025-09-01")) .andExpect(jsonPath("$.data.totalCount").value(0)) - .andExpect(jsonPath("$.data.plans", Matchers.hasSize(0))); + .andExpect(jsonPath("$.data.plans", hasSize(0))); } @Test @DisplayName("계획 조회 - 기간별 조회") @@ -357,7 +359,7 @@ void t7() throws Exception { .andExpect(handler().methodName("getStudyPlansForPeriod")) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.message").value("기간별 계획을 조회했습니다.")) - .andExpect(jsonPath("$.data", Matchers.hasSize(7))); + .andExpect(jsonPath("$.data", hasSize(7))); } @Test @@ -403,12 +405,13 @@ void t9() throws Exception { .andExpect(jsonPath("$.data.subject").value("수정된 최종 계획 (PUT)")) .andExpect(jsonPath("$.data.color").value("RED")) .andExpect(jsonPath("$.data.startDate").value("2025-10-10T14:00:00")) - .andExpect(jsonPath("$.data.endDate").value("2025-10-10T16:00:00")); + .andExpect(jsonPath("$.data.endDate").value("2025-10-10T16:00:00")) + .andExpect(jsonPath("$.data.repeatRule").doesNotExist()); } @Test - @DisplayName("계획 수정 - 단발성 (반복 규칙 추가 시도)") + @DisplayName("계획 수정 - 단발성 (반복 규칙 추가 시도 + WEEKLY 인 경우 byDay 자동 적용)") void t9_1() throws Exception { StudyPlan originalPlan = createSinglePlan(); Long planId = originalPlan.getId(); @@ -423,7 +426,7 @@ void t9_1() throws Exception { "endDate": "2025-10-01T12:00:00", "color": "RED", "repeatRule": { - "frequency": "DAILY", + "frequency": "WEEKLY", "repeatInterval": 1, "untilDate": "2025-12-31" } @@ -434,11 +437,14 @@ void t9_1() throws Exception { resultActions .andExpect(status().isOk()) // 400 Bad Request .andExpect(handler().handlerType(StudyPlanController.class)) - .andExpect(jsonPath("$.success").value(true)); + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.subject").value("수정된 최종 계획 (PUT)")) + .andExpect(jsonPath("$.data.repeatRule.byDay", hasSize(1))) + .andExpect(jsonPath("$.data.repeatRule.byDay[0]").value("WED")); } @Test - @DisplayName("계획 수정 - 반복성 단일 수정") + @DisplayName("계획 수정 - 반복성 가상 계획 단일 수정") void t10() throws Exception { StudyPlan originalPlan = createDailyPlan(); Long planId = originalPlan.getId(); @@ -467,13 +473,12 @@ void t10() throws Exception { .andExpect(jsonPath("$.data.color").value("BLUE")) .andExpect(jsonPath("$.data.startDate").value("2025-10-10T14:00:00")) .andExpect(jsonPath("$.data.endDate").value("2025-10-10T16:00:00")); - - //조회가 잘 되는지도 검증 - mvc.perform(get("/api/plans/date/2025-10-10") + //원본은 변경사항 없이 조회가 잘 되는지도 검증 + mvc.perform(get("/api/plans/date/2025-10-01") .header("Authorization", "Bearer faketoken") .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) - .andExpect(jsonPath("$.data.plans[0].subject").value("수정된 반복 계획 (PUT)")); + .andExpect(jsonPath("$.data.plans[0].subject").value("매일 반복 계획")); } @Test @@ -509,6 +514,7 @@ void t11() throws Exception { .andExpect(jsonPath("$.data.plans[0].subject").value("매일 반복 계획")); } + @Test @DisplayName("계획 삭제 - 단발성 단독 삭제") void t12() throws Exception { @@ -617,7 +623,7 @@ void t15() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) //검색 결과가 없다면 빈 배열 - .andExpect(jsonPath("$.data", Matchers.hasSize(0))); + .andExpect(jsonPath("$.data", hasSize(0))); } From 86ecc649a197e7e47d300025716fd8195c2c45d9 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Wed, 1 Oct 2025 12:40:32 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refect:=20=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=9C=20=EA=B3=84=ED=9A=8D=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/controller/StudyPlanController.java | 7 +++-- .../plan/dto/StudyPlanDeleteResponse.java | 28 +++++++++++++++++++ .../study/plan/service/StudyPlanService.java | 20 +++++++++---- 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.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 a1d7e295..6f163c04 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.StudyPlanDeleteResponse; import com.back.domain.study.plan.dto.StudyPlanRequest; import com.back.domain.study.plan.dto.StudyPlanListResponse; import com.back.domain.study.plan.dto.StudyPlanResponse; @@ -111,15 +112,15 @@ public ResponseEntity> updateStudyPlan( description = "기존 학습 계획을 삭제합니다. 반복 계획의 경우 적용 범위를 applyScope로 설정 할 수 있으며" + "클라이언트에서는 paln에 repeat_rule이 있으면 반복 계획으로 간주하고 반드시 apply_scope를 쿼리 파라미터로 넘겨야 합니다." + "repeat_rule이 없으면 단발성 계획으로 간주하여 삭제 범위를 설정 할 필요가 없으므로 apply_scope를 넘기지 않아도 됩니다.") - public ResponseEntity> deleteStudyPlan( + public ResponseEntity> deleteStudyPlan( @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long planId, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate selectedDate, @RequestParam(name = "applyScope", required = true) ApplyScope applyScope) { Long userId = user.getUserId(); - studyPlanService.deleteStudyPlan(userId, planId, selectedDate, applyScope); - return ResponseEntity.ok(RsData.success("학습 계획이 성공적으로 삭제되었습니다.")); + StudyPlanDeleteResponse response = studyPlanService.deleteStudyPlan(userId, planId, selectedDate, applyScope); + return ResponseEntity.ok(RsData.success("학습 계획이 성공적으로 삭제되었습니다.",response)); } } diff --git a/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.java b/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.java new file mode 100644 index 00000000..07cce335 --- /dev/null +++ b/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.java @@ -0,0 +1,28 @@ +package com.back.domain.study.plan.dto; + +import com.back.domain.study.plan.entity.ApplyScope; +import com.back.domain.study.plan.entity.Color; +import com.back.domain.study.plan.entity.StudyPlan; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public class StudyPlanDeleteResponse { + private Long id; + private String subject; + private LocalDateTime startDate; + private LocalDateTime endDate; + private Color color; + private LocalDate deletedDate; // 삭제된 날짜 + private ApplyScope applyScope; // 삭제 범위 + + public StudyPlanDeleteResponse(StudyPlanResponse plan, ApplyScope applyScope) { + this.id = plan.getId(); + this.subject = plan.getSubject(); + this.startDate = plan.getStartDate(); + this.endDate = plan.getEndDate(); + this.color = plan.getColor(); + this.deletedDate = plan.getStartDate().toLocalDate(); + this.applyScope = applyScope; + } +} 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 6e6f8ea6..66b09017 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.StudyPlanDeleteResponse; import com.back.domain.study.plan.dto.StudyPlanRequest; import com.back.domain.study.plan.dto.StudyPlanResponse; import com.back.domain.study.plan.entity.*; @@ -486,20 +487,29 @@ private RepeatRuleEmbeddable createRepeatRuleEmbeddable(StudyPlanRequest.RepeatR // ==================== 삭제 =================== @Transactional - public void deleteStudyPlan(Long userId, Long planId, LocalDate selectedDate, ApplyScope applyScope) { + public StudyPlanDeleteResponse deleteStudyPlan(Long userId, Long planId, LocalDate selectedDate, ApplyScope applyScope) { StudyPlan studyPlan = studyPlanRepository.findById(planId) .orElseThrow(() -> new CustomException(ErrorCode.PLAN_NOT_FOUND)); validateUserAccess(studyPlan, userId); - // 단발성 계획 삭제 (반복 룰이 null이거나 applyScope가 null인 경우) + // 삭제 전 정보 조회 + StudyPlanResponse deletedPlan; + if (studyPlan.getRepeatRule() == null || applyScope == null) { + // 단발성 계획 + deletedPlan = new StudyPlanResponse(studyPlan); studyPlanRepository.delete(studyPlan); - return; + } else { + // 반복성 계획 - 가상 계획 조회 + deletedPlan = createVirtualPlanForDate(studyPlan, selectedDate); + if (deletedPlan == null) { + throw new CustomException(ErrorCode.PLAN_NOT_FOUND); + } + deleteRepeatPlan(studyPlan, selectedDate, applyScope); } - // 반복성 계획 삭제 - applyScope에 따른 처리 - deleteRepeatPlan(studyPlan, selectedDate, applyScope); + return new StudyPlanDeleteResponse(deletedPlan, applyScope); } private void deleteRepeatPlan(StudyPlan studyPlan, LocalDate selectedDate, ApplyScope applyScope) { From e9788ffebd76c1b94493f39dacec65cd7bef8fe9 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Wed, 1 Oct 2025 12:45:36 +0900 Subject: [PATCH 3/5] =?UTF-8?q?test:=20=EC=82=AD=EC=A0=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyPlanControllerTest.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java b/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java index 233446da..d585d993 100644 --- a/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java +++ b/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java @@ -530,7 +530,12 @@ void t12() throws Exception { .andExpect(status().isOk()) // 200 OK .andExpect(handler().handlerType(StudyPlanController.class)) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")); + .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")) + .andExpect(jsonPath("$.data.id").value(planId)) + .andExpect(jsonPath("$.data.subject").value(originalPlan.getSubject())) + .andExpect(jsonPath("$.data.color").value(originalPlan.getColor())) + .andExpect(jsonPath("$.data.deletedDate").value("2025-10-01")) + .andExpect(jsonPath("$.data.applyScope").value("THIS_ONLY")); // DB에서 실제로 삭제되었는지 확인 boolean exists = studyPlanRepository.existsById(planId); @@ -552,7 +557,14 @@ void t13() throws Exception { .andExpect(status().isOk()) // 200 OK .andExpect(handler().handlerType(StudyPlanController.class)) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")); + .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")) + .andExpect(jsonPath("$.data.id").value(planId)) + .andExpect(jsonPath("$.data.subject").value(originalPlan.getSubject())) + .andExpect(jsonPath("$.data.deletedDate").value("2025-10-01")) + .andExpect(jsonPath("$.data.applyScope").value("THIS_ONLY")) + .andExpect(jsonPath("$.data.startDate").value("2025-10-01T09:00:00")) + .andExpect(jsonPath("$.data.endDate").value("2025-10-01T11:00:00")); + // 10월 1일에 해당하는 계획은 없어야함 mvc.perform(get("/api/plans/date/2025-10-01") .header("Authorization", "Bearer faketoken") @@ -583,7 +595,10 @@ void t14() throws Exception { .andExpect(status().isOk()) // 200 OK .andExpect(handler().handlerType(StudyPlanController.class)) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")); + .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")) + .andExpect(jsonPath("$.data.id").value(planId)) + .andExpect(jsonPath("$.data.deletedDate").value("2025-10-01")) + .andExpect(jsonPath("$.data.applyScope").value("FROM_THIS_DATE")); // 10월 10일에 해당하는 계획도 없어야함 mvc.perform(get("/api/plans/date/2025-10-10") @@ -608,7 +623,12 @@ void t15() throws Exception { .andExpect(status().isOk()) // 200 OK .andExpect(handler().handlerType(StudyPlanController.class)) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")); + .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")) + .andExpect(jsonPath("$.data.id").value(planId)) + .andExpect(jsonPath("$.data.deletedDate").value("2025-10-10")) + .andExpect(jsonPath("$.data.applyScope").value("FROM_THIS_DATE")) + .andExpect(jsonPath("$.data.startDate").value("2025-10-10T09:00:00")) + .andExpect(jsonPath("$.data.endDate").value("2025-10-10T11:00:00")); // 10월 1일에 해당하는 계획은 있어야함 mvc.perform(get("/api/plans/date/2025-10-01") @@ -617,7 +637,7 @@ void t15() throws Exception { .andDo(print()) .andExpect(jsonPath("$.data.totalCount").value(1)); - // 10.10 이후에 해당하는 계획은 없어야함 + // 10.10 이후에 해당하는 계획은 없어야함 mvc.perform(get("/api/plans?start=2025-10-10&end=2025-10-15") .header("Authorization", "Bearer faketoken") .contentType(MediaType.APPLICATION_JSON)) @@ -626,5 +646,4 @@ void t15() throws Exception { .andExpect(jsonPath("$.data", hasSize(0))); } - } \ No newline at end of file From e306916fc0a769b83b08ae59c528951712b000a4 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Wed, 1 Oct 2025 12:58:15 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=EC=A3=BC=EC=84=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/back/domain/study/plan/dto/StudyPlanResponse.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 851f5f7b..cd865401 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 @@ -43,8 +43,7 @@ public class StudyPlanResponse { public static class RepeatRuleResponse { private Frequency frequency; private Integer repeatInterval; - // byDay 필드는 이미 List으로 선언되어 있음. - private List byDay = new ArrayList<>(); // "MON" 형태의 문자열 리스트 + private List byDay = new ArrayList<>(); // "MON" 형태의 enum 문자열 리스트 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private LocalDate untilDate; From e42e102b0fba68cd55ae909721b208d31eb02b65 Mon Sep 17 00:00:00 2001 From: KSH0326 Date: Wed, 1 Oct 2025 14:32:53 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20dto=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=88=84=EB=9D=BD=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/study/plan/dto/StudyPlanDeleteResponse.java | 7 ++++++- .../study/plan/controller/StudyPlanControllerTest.java | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.java b/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.java index 07cce335..c4451c13 100644 --- a/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.java +++ b/src/main/java/com/back/domain/study/plan/dto/StudyPlanDeleteResponse.java @@ -2,11 +2,16 @@ import com.back.domain.study.plan.entity.ApplyScope; import com.back.domain.study.plan.entity.Color; -import com.back.domain.study.plan.entity.StudyPlan; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDate; import java.time.LocalDateTime; +@Getter +@NoArgsConstructor +@AllArgsConstructor public class StudyPlanDeleteResponse { private Long id; private String subject; diff --git a/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java b/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java index d585d993..116a3a87 100644 --- a/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java +++ b/src/test/java/com/back/domain/study/plan/controller/StudyPlanControllerTest.java @@ -533,7 +533,7 @@ void t12() throws Exception { .andExpect(jsonPath("$.message").value("학습 계획이 성공적으로 삭제되었습니다.")) .andExpect(jsonPath("$.data.id").value(planId)) .andExpect(jsonPath("$.data.subject").value(originalPlan.getSubject())) - .andExpect(jsonPath("$.data.color").value(originalPlan.getColor())) + .andExpect(jsonPath("$.data.color").value(Color.RED.name())) .andExpect(jsonPath("$.data.deletedDate").value("2025-10-01")) .andExpect(jsonPath("$.data.applyScope").value("THIS_ONLY")); @@ -562,8 +562,8 @@ void t13() throws Exception { .andExpect(jsonPath("$.data.subject").value(originalPlan.getSubject())) .andExpect(jsonPath("$.data.deletedDate").value("2025-10-01")) .andExpect(jsonPath("$.data.applyScope").value("THIS_ONLY")) - .andExpect(jsonPath("$.data.startDate").value("2025-10-01T09:00:00")) - .andExpect(jsonPath("$.data.endDate").value("2025-10-01T11:00:00")); + .andExpect(jsonPath("$.data.startDate").value("2025-10-01T12:00:00")) + .andExpect(jsonPath("$.data.endDate").value("2025-10-01T13:00:00")); // 10월 1일에 해당하는 계획은 없어야함 mvc.perform(get("/api/plans/date/2025-10-01") @@ -627,8 +627,8 @@ void t15() throws Exception { .andExpect(jsonPath("$.data.id").value(planId)) .andExpect(jsonPath("$.data.deletedDate").value("2025-10-10")) .andExpect(jsonPath("$.data.applyScope").value("FROM_THIS_DATE")) - .andExpect(jsonPath("$.data.startDate").value("2025-10-10T09:00:00")) - .andExpect(jsonPath("$.data.endDate").value("2025-10-10T11:00:00")); + .andExpect(jsonPath("$.data.startDate").value("2025-10-10T12:00:00")) + .andExpect(jsonPath("$.data.endDate").value("2025-10-10T13:00:00")); // 10월 1일에 해당하는 계획은 있어야함 mvc.perform(get("/api/plans/date/2025-10-01")