From e4a06b1a99694eb1f4c0467249b70492a71c896e Mon Sep 17 00:00:00 2001 From: msk226 Date: Fri, 13 Mar 2026 15:38:26 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=B6=A9=EB=8F=8C=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/spot/code/status/ErrorStatus.java | 1 + .../command/ManageScheduleService.java | 13 +++++++++ .../jpa/ScheduleRepository.java | 4 +++ .../command/ManageScheduleServiceTest.java | 29 +++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/common/api/src/main/java/kr/spot/code/status/ErrorStatus.java b/common/api/src/main/java/kr/spot/code/status/ErrorStatus.java index 2a741ec6..3a9c4477 100644 --- a/common/api/src/main/java/kr/spot/code/status/ErrorStatus.java +++ b/common/api/src/main/java/kr/spot/code/status/ErrorStatus.java @@ -74,6 +74,7 @@ public enum ErrorStatus implements BaseErrorCode { // 일정 관련 _SCHEDULE_NOT_FOUND(404, "SCHEDULE404", "일정을 찾을 수 없습니다."), _SCHEDULE_ACCESS_DENIED(403, "SCHEDULE403", "해당 스터디에 속하는 일정이 아닙니다."), + _SCHEDULE_TIME_CONFLICT(400, "SCHEDULE4001", "동일한 시간대에 이미 일정이 존재합니다."), _SCHEDULE_QR_CODE_ALREADY_ASSIGNED(400, "SCHEDULE4000", "이미 출석 QR 코드가 할당된 일정입니다."), // 출석 관련 diff --git a/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java b/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java index 0696abb3..81f6f548 100644 --- a/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java +++ b/modules/study/src/main/java/kr/spot/schedule/application/command/ManageScheduleService.java @@ -1,6 +1,8 @@ package kr.spot.schedule.application.command; import kr.spot.IdGenerator; +import kr.spot.code.status.ErrorStatus; +import kr.spot.exception.GeneralException; import kr.spot.schedule.domain.Schedule; import kr.spot.schedule.infrastructure.jpa.ScheduleRepository; import kr.spot.schedule.presentation.command.dto.CreateScheduleRequest; @@ -17,6 +19,8 @@ public class ManageScheduleService { private final ScheduleRepository scheduleRepository; public long createSchedule(CreateScheduleRequest request, long studyId, long creatorId) { + validateScheduleTimeConflict(request, studyId); + long scheduleId = idGenerator.nextId(); Schedule schedule = Schedule.of(scheduleId, studyId, creatorId, request.title(), request.locationInfo(), @@ -30,4 +34,13 @@ public void deleteSchedule(long studyId, long scheduleId) { Schedule schedule = scheduleRepository.getById(scheduleId); schedule.delete(studyId); } + + private void validateScheduleTimeConflict(CreateScheduleRequest request, long studyId) { + boolean hasConflict = scheduleRepository.existsByStudyIdAndStartAtLessThanAndEndAtGreaterThan( + studyId, request.endAt(), request.startAt()); + + if (hasConflict) { + throw new GeneralException(ErrorStatus._SCHEDULE_TIME_CONFLICT); + } + } } diff --git a/modules/study/src/main/java/kr/spot/schedule/infrastructure/jpa/ScheduleRepository.java b/modules/study/src/main/java/kr/spot/schedule/infrastructure/jpa/ScheduleRepository.java index 4982c3c4..896506a1 100644 --- a/modules/study/src/main/java/kr/spot/schedule/infrastructure/jpa/ScheduleRepository.java +++ b/modules/study/src/main/java/kr/spot/schedule/infrastructure/jpa/ScheduleRepository.java @@ -1,5 +1,6 @@ package kr.spot.schedule.infrastructure.jpa; +import java.time.LocalDateTime; import kr.spot.code.status.ErrorStatus; import kr.spot.exception.GeneralException; import kr.spot.schedule.domain.Schedule; @@ -7,6 +8,9 @@ public interface ScheduleRepository extends JpaRepository { + boolean existsByStudyIdAndStartAtLessThanAndEndAtGreaterThan( + Long studyId, LocalDateTime endAt, LocalDateTime startAt); + default Schedule getById(long scheduleId) { return findById(scheduleId).orElseThrow( () -> new GeneralException(ErrorStatus._SCHEDULE_NOT_FOUND)); diff --git a/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java b/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java index 80960f48..abedf46f 100644 --- a/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java +++ b/modules/study/src/test/java/kr/spot/schedule/application/command/ManageScheduleServiceTest.java @@ -10,9 +10,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.time.LocalDateTime; import kr.spot.IdGenerator; import kr.spot.code.status.ErrorStatus; import kr.spot.exception.GeneralException; @@ -60,6 +62,8 @@ void should_create_schedule_successfully() { CreateScheduleRequest request = createScheduleRequest(); when(idGenerator.nextId()).thenReturn(generatedId); + when(scheduleRepository.existsByStudyIdAndStartAtLessThanAndEndAtGreaterThan( + STUDY_ID, request.endAt(), request.startAt())).thenReturn(false); when(scheduleRepository.save(any(Schedule.class))) .thenAnswer(invocation -> invocation.getArgument(0)); @@ -89,6 +93,8 @@ TITLE, null, createScheduleRequest().startAt(), createScheduleRequest().endAt() ); when(idGenerator.nextId()).thenReturn(generatedId); + when(scheduleRepository.existsByStudyIdAndStartAtLessThanAndEndAtGreaterThan( + STUDY_ID, request.endAt(), request.startAt())).thenReturn(false); when(scheduleRepository.save(any(Schedule.class))) .thenAnswer(invocation -> invocation.getArgument(0)); @@ -101,6 +107,29 @@ TITLE, null, createScheduleRequest().startAt(), createScheduleRequest().endAt() Schedule capturedSchedule = scheduleCaptor.getValue(); assertThat(capturedSchedule.getLocationMemo()).isNull(); } + + @Test + @DisplayName("동일한 시간대에 일정이 있으면 예외가 발생한다") + void should_throw_exception_when_schedule_time_conflicts() { + // given + CreateScheduleRequest request = new CreateScheduleRequest( + TITLE, + LOCATION_MEMO, + LocalDateTime.of(2025, 1, 15, 15, 0), + LocalDateTime.of(2025, 1, 15, 17, 0) + ); + + when(scheduleRepository.existsByStudyIdAndStartAtLessThanAndEndAtGreaterThan( + STUDY_ID, request.endAt(), request.startAt())).thenReturn(true); + + // when & then + assertThatThrownBy(() -> manageScheduleService.createSchedule(request, STUDY_ID, CREATOR_ID)) + .isInstanceOf(GeneralException.class) + .extracting(ex -> ((GeneralException) ex).getStatus()) + .isEqualTo(ErrorStatus._SCHEDULE_TIME_CONFLICT); + + verify(scheduleRepository, never()).save(any(Schedule.class)); + } } @Nested