1616
1717import java .time .LocalDate ;
1818import java .time .LocalDateTime ;
19+ import java .time .LocalTime ;
1920import java .time .format .DateTimeFormatter ;
2021import java .time .temporal .ChronoUnit ;
2122import 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 ();
0 commit comments