Skip to content

Commit 61d8b1a

Browse files
committed
feat: plan 시간 겹침 검증 로직 추가
1 parent 00191bc commit 61d8b1a

File tree

3 files changed

+72
-2
lines changed

3 files changed

+72
-2
lines changed

src/main/java/com/back/domain/study/plan/repository/StudyPlanRepository.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,32 @@
22

33
import com.back.domain.study.plan.entity.StudyPlan;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
56
import org.springframework.stereotype.Repository;
67

8+
import java.time.LocalDateTime;
79
import java.util.List;
810

911
@Repository
1012
public interface StudyPlanRepository extends JpaRepository<StudyPlan, Long> {
1113
List<StudyPlan> findByUserId(Long userId);
14+
/* 시간 겹침 조건:
15+
새 계획 시작 시간보다 기존 계획 종료 시간이 늦고 (p.endDate > :newStart),
16+
새 계획 종료 시간보다 기존 계획 시작 시간이 빨라야 한다 (p.startDate < :newEnd).
17+
(종료 시간 == 새 시작 시간)은 허용
18+
*/
19+
@Query("""
20+
SELECT p
21+
FROM StudyPlan p
22+
WHERE p.user.id = :userId
23+
AND (:planIdToExclude IS NULL OR p.id != :planIdToExclude)
24+
AND p.endDate > :newStart
25+
AND p.startDate < :newEnd
26+
""")
27+
List<StudyPlan> findByUserIdAndNotIdAndOverlapsTime(
28+
Long userId,
29+
Long planIdToExclude,
30+
LocalDateTime newStart,
31+
LocalDateTime newEnd
32+
);
1233
}

src/main/java/com/back/domain/study/plan/service/StudyPlanService.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import java.time.LocalDate;
1818
import java.time.LocalDateTime;
19+
import java.time.LocalTime;
1920
import java.time.format.DateTimeFormatter;
2021
import java.time.temporal.ChronoUnit;
2122
import java.util.ArrayList;
@@ -42,6 +43,8 @@ public StudyPlanResponse createStudyPlan(Long userId, StudyPlanRequest request)
4243
// 날짜/시간 검증
4344
validateDateTime(request.getStartDate(), request.getEndDate());
4445

46+
// 시간 겹침 검증
47+
validateTimeConflict(userId, null, request.getStartDate(), request.getEndDate());
4548

4649
StudyPlan studyPlan = new StudyPlan();
4750

@@ -311,6 +314,10 @@ public StudyPlanResponse updateStudyPlan(Long userId, Long planId, StudyPlanRequ
311314
.orElseThrow(() -> new CustomException(ErrorCode.PLAN_NOT_FOUND));
312315

313316
validateUserAccess(originalPlan, userId);
317+
// 날짜/시간 검증
318+
validateDateTime(request.getStartDate(), request.getEndDate());
319+
// 시간 겹침 검증 (원본 계획 ID 제외)
320+
validateTimeConflict(userId, originalPlan.getId(), request.getStartDate(), request.getEndDate());
314321

315322
// 1. 단발성 계획인 경우
316323
if (originalPlan.getRepeatRule() == null) {
@@ -558,7 +565,49 @@ private void validateDateTime(LocalDateTime startDate, LocalDateTime endDate) {
558565
throw new CustomException(ErrorCode.PLAN_INVALID_TIME_RANGE);
559566
}
560567
}
561-
//
568+
//시간 겹침 검증 메서드 (최적화된 DB 쿼리 + 가상 인스턴스 검증 조합)
569+
private void validateTimeConflict(Long userId, Long planIdToExclude, LocalDateTime newStart, LocalDateTime newEnd) {
570+
LocalDate newPlanDate = newStart.toLocalDate();
571+
572+
// 1. DB 쿼리를 통해 요청 시간과 원본 시간대가 겹칠 가능성이 있는 계획들만 로드 (최적화)
573+
// 기존 조회 코드를 이용하려 했으나 성능 문제로 인해 쿼리 작성.
574+
// 조회기능도 리펙토링 예정
575+
List<StudyPlan> conflictingOriginalPlans = studyPlanRepository.findByUserIdAndNotIdAndOverlapsTime(
576+
userId, planIdToExclude, newStart, newEnd
577+
);
578+
579+
if (conflictingOriginalPlans.isEmpty()) {
580+
return;
581+
}
582+
583+
for (StudyPlan plan : conflictingOriginalPlans) {
584+
if (plan.getRepeatRule() == null) {
585+
// 2-1. 단발성 계획: 쿼리에서 이미 시간 범위가 겹친다고 걸러졌지만, 최종 확인
586+
if (isOverlapping(plan.getStartDate(), plan.getEndDate(), newStart, newEnd)) {
587+
throw new CustomException(ErrorCode.PLAN_TIME_CONFLICT);
588+
}
589+
} else {
590+
// 2-2. 반복 계획: 기존 헬퍼를 사용해 요청 날짜의 가상 인스턴스를 생성하고 검사
591+
StudyPlanResponse virtualPlan = createVirtualPlanForDate(plan, newPlanDate);
592+
593+
if (virtualPlan != null) {
594+
// 가상 인스턴스가 존재하고 (삭제되지 않았고)
595+
// 해당 인스턴스의 확정된 시간이 새 계획과 겹치는지 최종 확인
596+
if (isOverlapping(virtualPlan.getStartDate(), virtualPlan.getEndDate(), newStart, newEnd)) {
597+
throw new CustomException(ErrorCode.PLAN_TIME_CONFLICT);
598+
}
599+
}
600+
}
601+
}
602+
}
603+
/*
604+
* 두 시간 범위의 겹침을 확인하는 메서드
605+
* 겹치는 조건: (새로운 시작 시각 < 기존 종료 시각) && (새로운 종료 시각 > 기존 시작 시각)
606+
* (기존 종료 시각 == 새로운 시작 시각)은 겹치지 않는 것으로 간주
607+
*/
608+
private boolean isOverlapping(LocalDateTime existingStart, LocalDateTime existingEnd, LocalDateTime newStart, LocalDateTime newEnd) {
609+
return newStart.isBefore(existingEnd) && newEnd.isAfter(existingStart);
610+
}
562611

563612
private void validateRepeatRuleDate(StudyPlan studyPlan, LocalDate untilDate) {
564613
LocalDate planStartDate = studyPlan.getStartDate().toLocalDate();

src/main/java/com/back/global/exception/ErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public enum ErrorCode {
4343
PLAN_ORIGINAL_REPEAT_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAN_004", "해당 날짜에 원본 반복 계획을 찾을 수 없습니다."),
4444
INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST, "PLAN_005", "날짜 형식이 올바르지 않습니다. (YYYY-MM-DD 형식을 사용해주세요)"),
4545
PLAN_INVALID_TIME_RANGE(HttpStatus.BAD_REQUEST, "PLAN_006", "시작 시간은 종료 시간보다 빨라야 합니다."),
46-
PLAN_CONFLICT(HttpStatus.CONFLICT, "PLAN_007", "이미 존재하는 학습 계획과 시간이 겹칩니다."),
46+
PLAN_TIME_CONFLICT(HttpStatus.CONFLICT, "PLAN_007", "이미 존재하는 학습 계획과 시간이 겹칩니다."),
4747
PLAN_CANNOT_UPDATE(HttpStatus.BAD_REQUEST, "PLAN_008", "수정 스위치 로직 탈출. 어떤 경우인지 파악이 필요합니다."),
4848
REPEAT_INVALID_UNTIL_DATE(HttpStatus.BAD_REQUEST, "REPEAT_001", "반복 계획의 종료 날짜는 시작 날짜 이전일 수 없습니다."),
4949
REPEAT_BYDAY_REQUIRED(HttpStatus.BAD_REQUEST, "REPEAT_002", "주간 반복 계획의 경우 요일(byDay) 정보가 필요합니다."),

0 commit comments

Comments
 (0)