Skip to content

Commit 6318e87

Browse files
authored
[FU-357] 날짜별 스케줄 검증로직 개선 및 리팩터링 (#91)
* FU-357 feat: 날짜별 스케줄 상태 별 스케줄 충돌 검증 - 예약오픈 <-> 예약중지 간 충돌 불가 - 예약확정 <-> 예약오픈,예약중지 간 충돌 가능 * FU-357 feat: 날짜별 스케줄과 기본스케줄 단위 일치 보장 * FU-357 refactor: 날짜별 스케줄 검증 전담 클래스 분리 서비스 클래스를 간결하게 관리하고, 단일 책임 원칙(SRP)을 따르기 위해 검증 전담 클래스를 분리하였음 * FU-357 refactor: 스케줄 충돌 검증 코드 리팩터링 및 가독성 개선
1 parent c8bd77a commit 6318e87

9 files changed

Lines changed: 274 additions & 125 deletions

File tree

src/main/java/com/foru/freebe/errors/errorcode/ScheduleErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ public enum ScheduleErrorCode implements ErrorCode {
99
DAILY_SCHEDULE_NOT_FOUND(500, "해당하는 날짜별 스케줄을 찾을 수 없습니다."),
1010
DAILY_SCHEDULE_OVERLAP(400, "해당 일정에 이미 등록된 스케줄이 있습니다."),
1111
DAILY_SCHEDULE_IN_PAST(400, "현재 시점 이전의 스케줄은 등록할 수 없습니다."),
12-
START_TIME_AFTER_END_TIME(400, "시작시간과 종료시간이 올바르지 않습니다.");
12+
START_TIME_AFTER_END_TIME(400, "시작시간과 종료시간이 올바르지 않습니다."),
13+
INVALID_SCHEDULE_UNIT(400, "기본스케줄 단위와 일치하지 않습니다.");
1314

1415
private final int httpStatus;
1516
private final String message;

src/main/java/com/foru/freebe/member/entity/ScheduleUnit.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.fasterxml.jackson.annotation.JsonProperty;
44

55
public enum ScheduleUnit {
6-
76
@JsonProperty("THIRTY_MINUTES")
87
THIRTY_MINUTES,
98
@JsonProperty("SIXTY_MINUTES")

src/main/java/com/foru/freebe/schedule/repository/DailyScheduleRepository.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import com.foru.freebe.member.entity.Member;
1212
import com.foru.freebe.schedule.entity.DailySchedule;
13+
import com.foru.freebe.schedule.entity.ScheduleStatus;
1314

1415
public interface DailyScheduleRepository extends JpaRepository<DailySchedule, Long> {
1516
Optional<DailySchedule> findByMemberAndId(Member member, Long scheduleId);
@@ -18,7 +19,8 @@ public interface DailyScheduleRepository extends JpaRepository<DailySchedule, Lo
1819

1920
@Query("SELECT ds FROM DailySchedule ds WHERE ds.member = :photographer "
2021
+ "AND ds.date = :date "
21-
+ "AND ((ds.startTime < :endTime AND ds.endTime > :startTime))")
22-
List<DailySchedule> findOverlappingSchedules(Member photographer, LocalDate date, LocalTime startTime,
23-
LocalTime endTime);
22+
+ "AND ((ds.startTime < :endTime AND ds.endTime > :startTime))"
23+
+ "AND ds.scheduleStatus IN (:scheduleStatuses)")
24+
List<DailySchedule> findConflictingSchedulesByStatuses(Member photographer, LocalDate date, LocalTime startTime,
25+
LocalTime endTime, List<ScheduleStatus> scheduleStatuses);
2426
}

src/main/java/com/foru/freebe/schedule/service/BaseScheduleService.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
package com.foru.freebe.schedule.service;
22

33
import java.time.LocalTime;
4-
import java.util.ArrayList;
54
import java.util.List;
65
import java.util.stream.Collectors;
76

87
import org.springframework.stereotype.Service;
98
import org.springframework.transaction.annotation.Transactional;
109

10+
import com.foru.freebe.errors.errorcode.MemberErrorCode;
11+
import com.foru.freebe.errors.errorcode.ScheduleErrorCode;
12+
import com.foru.freebe.errors.exception.RestApiException;
13+
import com.foru.freebe.member.entity.Member;
14+
import com.foru.freebe.member.repository.MemberRepository;
1115
import com.foru.freebe.schedule.dto.BaseScheduleDto;
1216
import com.foru.freebe.schedule.dto.ScheduleUnitDto;
1317
import com.foru.freebe.schedule.entity.BaseSchedule;
1418
import com.foru.freebe.schedule.entity.DayOfWeek;
1519
import com.foru.freebe.schedule.entity.OperationStatus;
1620
import com.foru.freebe.schedule.repository.BaseScheduleRepository;
17-
import com.foru.freebe.errors.errorcode.MemberErrorCode;
18-
import com.foru.freebe.errors.errorcode.ScheduleErrorCode;
19-
import com.foru.freebe.errors.exception.RestApiException;
20-
import com.foru.freebe.member.entity.Member;
21-
import com.foru.freebe.member.repository.MemberRepository;
2221

2322
import lombok.RequiredArgsConstructor;
2423

@@ -62,7 +61,7 @@ public void updateSchedule(BaseScheduleDto baseScheduleDto, Member photographer)
6261
}
6362

6463
public void createDefaultSchedule(Member photographer) {
65-
for(DayOfWeek dayOfWeek : DayOfWeek.values()) {
64+
for (DayOfWeek dayOfWeek : DayOfWeek.values()) {
6665
BaseSchedule baseSchedule = BaseSchedule.builder()
6766
.photographer(photographer)
6867
.dayOfWeek(dayOfWeek)

src/main/java/com/foru/freebe/schedule/service/DailyScheduleService.java

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package com.foru.freebe.schedule.service;
22

3-
import java.time.Clock;
4-
import java.time.LocalDateTime;
5-
import java.time.LocalTime;
63
import java.util.List;
74
import java.util.stream.Collectors;
85

@@ -26,7 +23,7 @@
2623
@Transactional
2724
public class DailyScheduleService {
2825
private final DailyScheduleRepository dailyScheduleRepository;
29-
private final Clock clock;
26+
private final DailyScheduleValidator validator;
3027

3128
public List<DailyScheduleResponse> getDailySchedules(Member photographer, DailyScheduleMonthlyRequest request) {
3229
return dailyScheduleRepository.findByMember(photographer)
@@ -38,9 +35,10 @@ public List<DailyScheduleResponse> getDailySchedules(Member photographer, DailyS
3835
}
3936

4037
public DailyScheduleAddResponse addDailySchedule(Member photographer, DailyScheduleRequest request) {
41-
validateTimeRange(request.getStartTime(), request.getEndTime());
42-
validateScheduleInFuture(request);
43-
validateScheduleOverlap(photographer, request);
38+
validator.validateTimeRange(request.getStartTime(), request.getEndTime());
39+
validator.validateScheduleUnit(photographer.getScheduleUnit(), request.getStartTime(), request.getEndTime());
40+
validator.validateScheduleInFuture(request);
41+
validator.validateConflictingSchedules(photographer, request);
4442

4543
DailySchedule dailySchedule = DailySchedule.builder()
4644
.member(photographer)
@@ -55,13 +53,14 @@ public DailyScheduleAddResponse addDailySchedule(Member photographer, DailySched
5553
}
5654

5755
public void updateDailySchedule(Member photographer, Long scheduleId, DailyScheduleRequest request) {
58-
validateTimeRange(request.getStartTime(), request.getEndTime());
56+
validator.validateTimeRange(request.getStartTime(), request.getEndTime());
57+
validator.validateScheduleUnit(photographer.getScheduleUnit(), request.getStartTime(), request.getEndTime());
5958

6059
DailySchedule dailySchedule = dailyScheduleRepository.findByMemberAndId(photographer, scheduleId)
6160
.orElseThrow(() -> new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_NOT_FOUND));
6261

63-
validateScheduleInFuture(request);
64-
validateScheduleOverlap(photographer, request, scheduleId);
62+
validator.validateScheduleInFuture(request);
63+
validator.validateConflictingSchedules(photographer, request, scheduleId);
6564

6665
dailySchedule.updateScheduleStatus(request.getScheduleStatus());
6766
dailySchedule.updateDate(request.getDate());
@@ -74,41 +73,6 @@ public void deleteDailySchedule(Member photographer, Long scheduleId) {
7473
.orElseThrow(() -> new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_NOT_FOUND)));
7574
}
7675

77-
private void validateScheduleOverlap(Member member, DailyScheduleRequest request) {
78-
List<DailySchedule> overlappingSchedules = dailyScheduleRepository.findOverlappingSchedules(member,
79-
request.getDate(), request.getStartTime(), request.getEndTime());
80-
81-
if (!overlappingSchedules.isEmpty()) {
82-
throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP);
83-
}
84-
}
85-
86-
private void validateTimeRange(LocalTime startTime, LocalTime endTime) {
87-
if (startTime.isAfter(endTime) || startTime.equals(endTime)) {
88-
throw new RestApiException(ScheduleErrorCode.START_TIME_AFTER_END_TIME);
89-
}
90-
}
91-
92-
private void validateScheduleInFuture(DailyScheduleRequest request) {
93-
LocalDateTime requestDateTime = request.getDate().atTime(request.getStartTime());
94-
95-
if (requestDateTime.isBefore(LocalDateTime.now(clock))) {
96-
throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_IN_PAST);
97-
}
98-
}
99-
100-
private void validateScheduleOverlap(Member member, DailyScheduleRequest request, Long scheduleId) {
101-
List<DailySchedule> overlappingSchedules = dailyScheduleRepository.findOverlappingSchedules(member,
102-
request.getDate(), request.getStartTime(), request.getEndTime());
103-
104-
if (overlappingSchedules.size() == 1 && overlappingSchedules.get(0).getId().equals(scheduleId)) {
105-
return;
106-
}
107-
if (!overlappingSchedules.isEmpty()) {
108-
throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP);
109-
}
110-
}
111-
11276
private DailyScheduleResponse toDailyScheduleResponse(DailySchedule dailySchedule) {
11377
return DailyScheduleResponse.builder()
11478
.scheduleId(dailySchedule.getId())
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.foru.freebe.schedule.service;
2+
3+
import java.time.Clock;
4+
import java.time.LocalDateTime;
5+
import java.time.LocalTime;
6+
import java.util.List;
7+
8+
import org.springframework.stereotype.Service;
9+
10+
import com.foru.freebe.errors.errorcode.ScheduleErrorCode;
11+
import com.foru.freebe.errors.exception.RestApiException;
12+
import com.foru.freebe.member.entity.Member;
13+
import com.foru.freebe.member.entity.ScheduleUnit;
14+
import com.foru.freebe.schedule.dto.DailyScheduleRequest;
15+
import com.foru.freebe.schedule.entity.DailySchedule;
16+
import com.foru.freebe.schedule.entity.ScheduleStatus;
17+
import com.foru.freebe.schedule.repository.DailyScheduleRepository;
18+
19+
import jakarta.transaction.Transactional;
20+
import lombok.RequiredArgsConstructor;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
@Transactional
25+
public class DailyScheduleValidator {
26+
private final Clock clock;
27+
private final DailyScheduleRepository dailyScheduleRepository;
28+
29+
public void validateTimeRange(LocalTime startTime, LocalTime endTime) {
30+
if (startTime.isAfter(endTime) || startTime.equals(endTime)) {
31+
throw new RestApiException(ScheduleErrorCode.START_TIME_AFTER_END_TIME);
32+
}
33+
}
34+
35+
public void validateScheduleUnit(ScheduleUnit scheduleUnit, LocalTime startTime, LocalTime endTime) {
36+
switch (scheduleUnit) {
37+
case SIXTY_MINUTES -> {
38+
if (startTime.getMinute() != 0 || endTime.getMinute() != 0) {
39+
throw new RestApiException(ScheduleErrorCode.INVALID_SCHEDULE_UNIT);
40+
}
41+
}
42+
case THIRTY_MINUTES -> {
43+
if (startTime.getMinute() != 0 && startTime.getMinute() != 30) {
44+
throw new RestApiException(ScheduleErrorCode.INVALID_SCHEDULE_UNIT);
45+
} else if (endTime.getMinute() != 0 && endTime.getMinute() != 30) {
46+
throw new RestApiException(ScheduleErrorCode.INVALID_SCHEDULE_UNIT);
47+
}
48+
}
49+
}
50+
}
51+
52+
public void validateScheduleInFuture(DailyScheduleRequest request) {
53+
LocalDateTime requestDateTime = request.getDate().atTime(request.getStartTime());
54+
55+
if (requestDateTime.isBefore(LocalDateTime.now(clock))) {
56+
throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_IN_PAST);
57+
}
58+
}
59+
60+
public void validateConflictingSchedules(Member member, DailyScheduleRequest request) {
61+
List<ScheduleStatus> scheduleStatuses = determineConflictingStatuses(request.getScheduleStatus());
62+
63+
List<DailySchedule> overlappingSchedules = dailyScheduleRepository.findConflictingSchedulesByStatuses(member,
64+
request.getDate(), request.getStartTime(), request.getEndTime(), scheduleStatuses);
65+
66+
if (!overlappingSchedules.isEmpty()) {
67+
throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP);
68+
}
69+
}
70+
71+
public void validateConflictingSchedules(Member member, DailyScheduleRequest request, Long scheduleId) {
72+
List<ScheduleStatus> scheduleStatuses = determineConflictingStatuses(request.getScheduleStatus());
73+
74+
List<DailySchedule> conflictingSchedules = dailyScheduleRepository.findConflictingSchedulesByStatuses(member,
75+
request.getDate(), request.getStartTime(), request.getEndTime(), scheduleStatuses);
76+
77+
if (conflictingSchedules.isEmpty() || isSelfConflictOnly(scheduleId, conflictingSchedules)) {
78+
return;
79+
}
80+
81+
throw new RestApiException(ScheduleErrorCode.DAILY_SCHEDULE_OVERLAP);
82+
}
83+
84+
private List<ScheduleStatus> determineConflictingStatuses(ScheduleStatus scheduleStatus) {
85+
if (scheduleStatus == ScheduleStatus.CONFIRMED) {
86+
return List.of(ScheduleStatus.CONFIRMED);
87+
} else {
88+
return List.of(ScheduleStatus.OPEN, ScheduleStatus.CLOSED);
89+
}
90+
}
91+
92+
private boolean isSelfConflictOnly(Long scheduleId, List<DailySchedule> conflictingSchedules) {
93+
return conflictingSchedules.size() == 1 && conflictingSchedules.get(0).getId().equals(scheduleId);
94+
}
95+
}

src/test/java/com/foru/freebe/schedule/service/BaseScheduleServiceTest.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.mockito.Mockito.*;
44

55
import java.time.LocalTime;
6+
67
import org.junit.jupiter.api.Assertions;
78
import org.junit.jupiter.api.BeforeEach;
89
import org.junit.jupiter.api.DisplayName;
@@ -11,6 +12,7 @@
1112
import org.mockito.InjectMocks;
1213
import org.mockito.Mock;
1314
import org.mockito.junit.jupiter.MockitoExtension;
15+
1416
import com.foru.freebe.member.entity.Member;
1517
import com.foru.freebe.member.entity.Role;
1618
import com.foru.freebe.schedule.dto.BaseScheduleDto;
@@ -21,7 +23,6 @@
2123

2224
@ExtendWith(MockitoExtension.class)
2325
class BaseScheduleServiceTest {
24-
2526
@Mock
2627
private BaseScheduleRepository baseScheduleRepository;
2728

@@ -92,10 +93,12 @@ void updateScheduleForActivation() {
9293
Assertions.assertEquals(LocalTime.of(9, 0), existingBaseSchedule.getStartTime());
9394
Assertions.assertEquals(LocalTime.of(18, 0), existingBaseSchedule.getEndTime());
9495

95-
verify(baseScheduleRepository, times(1)).findByDayOfWeekAndPhotographerId(baseScheduleDto.getDayOfWeek(), photographer.getId());
96+
verify(baseScheduleRepository, times(1)).findByDayOfWeekAndPhotographerId(baseScheduleDto.getDayOfWeek(),
97+
photographer.getId());
9698
}
9799

98-
private BaseScheduleDto createBaseScheduleDto(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime, OperationStatus operationStatus) {
100+
private BaseScheduleDto createBaseScheduleDto(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime,
101+
OperationStatus operationStatus) {
99102
return BaseScheduleDto.builder()
100103
.dayOfWeek(dayOfWeek)
101104
.startTime(startTime)

0 commit comments

Comments
 (0)