Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -37,7 +38,7 @@ public class StudyPlanRequest {
public static class RepeatRuleRequest {
private Frequency frequency;
private Integer intervalValue;
private List<String> byDay = new ArrayList<>(); // 문자열 리스트
private List<DayOfWeek> byDay = new ArrayList<>();

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private String untilDate; // "2025-12-31" 형태
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static class RepeatRuleResponse {
private Frequency frequency;
private Integer repeatInterval;
// byDay 필드는 이미 List<String>으로 선언되어 있음.
private List<String> byDay = new ArrayList<>(); // "MON" 형태의 문자열 리스트
private List<DayOfWeek> byDay = new ArrayList<>(); // "MON" 형태의 문자열 리스트

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate untilDate;
Expand All @@ -60,16 +60,6 @@ public RepeatRuleResponse(com.back.domain.study.plan.entity.RepeatRule repeatRul
}
}

public List<DayOfWeek> getByDaysList() {
if (byDay == null || byDay.isEmpty()) {
return List.of();
}

// List<String>의 각 요소를 DayOfWeek enum으로 변환하여 반환
return byDay.stream()
.map(com.back.domain.study.plan.entity.DayOfWeek::valueOf)
.collect(Collectors.toList());
}
}
//엔티티를 DTO로 변환하는 생성자
public StudyPlanResponse(StudyPlan studyPlan) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class RepeatRule extends BaseEntity {

//요일은 응답에 들어있는 요일을 그대로 저장 (예: "WED")
@Column(name = "by_day")
private List<String> byDay = new ArrayList<>();
private List<DayOfWeek> byDay = new ArrayList<>();

private LocalDate untilDate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public class RepeatRuleEmbeddable {
private Frequency frequency;

private Integer intervalValue;
private List<String> byDay = new ArrayList<>();
private List<DayOfWeek> byDay = new ArrayList<>();
private LocalDate untilDate; // LocalDateTime → LocalDate 변경
}

This file was deleted.

112 changes: 60 additions & 52 deletions src/main/java/com/back/domain/study/plan/service/StudyPlanService.java
Original file line number Diff line number Diff line change
@@ -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.*;
Expand Down Expand Up @@ -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<String> 타입으로 가정
repeatRule.setByDay(List.of(startDayOfWeek));
} else {
// 2. byDay가 있다면 요청 값을 사용 (List<String> to List<String> 매핑 확인)
repeatRule.setByDay(request.getByDay());
}
}
// untilDate 설정 및 검증
LocalDate untilDate;

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
}
Expand All @@ -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());
Expand All @@ -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);
}
Expand All @@ -458,38 +432,26 @@ 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);
}

studyPlanExceptionRepository.save(existingException);
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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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());
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));

}
Expand Down Expand Up @@ -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"))
Expand All @@ -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("계획 조회 - 기간별 조회")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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"
}
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -509,6 +514,7 @@ void t11() throws Exception {
.andExpect(jsonPath("$.data.plans[0].subject").value("매일 반복 계획"));
}


@Test
@DisplayName("계획 삭제 - 단발성 단독 삭제")
void t12() throws Exception {
Expand Down Expand Up @@ -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)));
}


Expand Down