Skip to content

Commit 3edcb58

Browse files
committed
Feat: 반복 슬롯 생성
1 parent c46862d commit 3edcb58

File tree

7 files changed

+170
-8
lines changed

7 files changed

+170
-8
lines changed

back/src/main/java/com/back/domain/mentoring/mentoring/controller/MentoringController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import org.springframework.web.bind.annotation.*;
1818

1919
@RestController
20-
@RequestMapping("/mentoring")
20+
@RequestMapping("/mentorings")
2121
@RequiredArgsConstructor
2222
@Tag(name = "MentoringController", description = "멘토링 API")
2323
public class MentoringController {

back/src/main/java/com/back/domain/mentoring/slot/controller/MentorSlotController.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.back.domain.mentoring.slot.controller;
22

33
import com.back.domain.member.member.entity.Member;
4-
import com.back.domain.mentoring.slot.dto.MentorSlotDto;
4+
import com.back.domain.mentoring.slot.dto.request.MentorSlotRepetitionRequest;
55
import com.back.domain.mentoring.slot.dto.request.MentorSlotRequest;
6+
import com.back.domain.mentoring.slot.dto.response.MentorSlotDto;
67
import com.back.domain.mentoring.slot.dto.response.MentorSlotResponse;
78
import com.back.domain.mentoring.slot.service.MentorSlotService;
89
import com.back.global.rq.Rq;
@@ -20,7 +21,7 @@
2021
import java.util.List;
2122

2223
@RestController
23-
@RequestMapping("/mentor-slot")
24+
@RequestMapping("/mentor-slots")
2425
@RequiredArgsConstructor
2526
@Tag(name = "MentorSlotController", description = "멘토 슬롯(멘토의 예약 가능 일정) API")
2627
public class MentorSlotController {
@@ -49,7 +50,6 @@ public RsData<List<MentorSlotDto>> getMyMentorSlots(
4950
);
5051
}
5152

52-
5353
@GetMapping("/available/{mentorId}")
5454
@Operation(summary = "멘토의 예약 가능한 슬롯 목록 조회", description = "멘티가 특정 멘토의 예약 가능한 슬롯 목록을 조회합니다.")
5555
public RsData<List<MentorSlotDto>> getAvailableMentorSlots(
@@ -99,6 +99,21 @@ public RsData<MentorSlotResponse> createMentorSlot(
9999
);
100100
}
101101

102+
@PostMapping("/repetition")
103+
@PreAuthorize("hasRole('MENTOR')")
104+
@Operation(summary = "반복 슬롯 생성", description = "멘토 슬롯을 반복 생성합니다. 로그인한 멘토만 생성할 수 있습니다.")
105+
public RsData<Void> createMentorSlotRepetition(
106+
@RequestBody @Valid MentorSlotRepetitionRequest reqDto
107+
) {
108+
Member member = rq.getActor();
109+
mentorSlotService.createMentorSlotRepetition(reqDto, member);
110+
111+
return new RsData<>(
112+
"201",
113+
"반복 일정을 등록했습니다."
114+
);
115+
}
116+
102117
@PutMapping("/{slotId}")
103118
@Operation(summary = "멘토 슬롯 수정", description = "멘토 슬롯을 수정합니다. 멘토 슬롯 작성자만 접근할 수 있습니다.")
104119
public RsData<MentorSlotResponse> updateMentorSlot(
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.back.domain.mentoring.slot.dto.request;
2+
3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import jakarta.validation.constraints.NotEmpty;
6+
import jakarta.validation.constraints.NotNull;
7+
8+
import java.time.DayOfWeek;
9+
import java.time.LocalDate;
10+
import java.time.LocalTime;
11+
import java.util.List;
12+
13+
public record MentorSlotRepetitionRequest(
14+
@Schema(description = "반복 시작일")
15+
@NotNull
16+
LocalDate repeatStartDate,
17+
18+
@Schema(description = "반복 종료일")
19+
@NotNull
20+
LocalDate repeatEndDate,
21+
22+
@Schema(description = "반복 요일")
23+
@NotEmpty
24+
List<DayOfWeek> daysOfWeek,
25+
26+
@Schema(description = "시작 시간")
27+
@NotNull
28+
@JsonFormat(pattern = "HH:mm:ss")
29+
LocalTime startTime,
30+
31+
@Schema(description = "종료 시간")
32+
@NotNull
33+
@JsonFormat(pattern = "HH:mm:ss")
34+
LocalTime endTime
35+
){
36+
}

back/src/main/java/com/back/domain/mentoring/slot/dto/MentorSlotDto.java renamed to back/src/main/java/com/back/domain/mentoring/slot/dto/response/MentorSlotDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.back.domain.mentoring.slot.dto;
1+
package com.back.domain.mentoring.slot.dto.response;
22

33
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
44
import com.back.domain.mentoring.slot.entity.MentorSlot;

back/src/main/java/com/back/domain/mentoring/slot/service/MentorSlotService.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import com.back.domain.mentoring.mentoring.error.MentoringErrorCode;
88
import com.back.domain.mentoring.mentoring.repository.MentoringRepository;
99
import com.back.domain.mentoring.reservation.repository.ReservationRepository;
10-
import com.back.domain.mentoring.slot.dto.MentorSlotDto;
10+
import com.back.domain.mentoring.slot.dto.request.MentorSlotRepetitionRequest;
1111
import com.back.domain.mentoring.slot.dto.request.MentorSlotRequest;
12+
import com.back.domain.mentoring.slot.dto.response.MentorSlotDto;
1213
import com.back.domain.mentoring.slot.dto.response.MentorSlotResponse;
1314
import com.back.domain.mentoring.slot.entity.MentorSlot;
1415
import com.back.domain.mentoring.slot.error.MentorSlotErrorCode;
@@ -18,7 +19,11 @@
1819
import org.springframework.stereotype.Service;
1920
import org.springframework.transaction.annotation.Transactional;
2021

22+
import java.time.DayOfWeek;
23+
import java.time.LocalDate;
2124
import java.time.LocalDateTime;
25+
import java.time.temporal.TemporalAdjusters;
26+
import java.util.ArrayList;
2227
import java.util.List;
2328

2429
@Service
@@ -81,6 +86,19 @@ public MentorSlotResponse createMentorSlot(MentorSlotRequest reqDto, Member memb
8186
return MentorSlotResponse.from(mentorSlot, mentoring);
8287
}
8388

89+
@Transactional
90+
public void createMentorSlotRepetition(MentorSlotRepetitionRequest reqDto, Member member) {
91+
Mentor mentor = findMentorByMember(member);
92+
93+
List<MentorSlot> mentorSlots = new ArrayList<>();
94+
95+
// 지정한 요일별로 슬롯 목록 생성
96+
for(DayOfWeek targetDayOfWeek : reqDto.daysOfWeek()) {
97+
mentorSlots.addAll(generateSlotsForDayOfWeek(reqDto, targetDayOfWeek, mentor));
98+
}
99+
mentorSlotRepository.saveAll(mentorSlots);
100+
}
101+
84102
@Transactional
85103
public MentorSlotResponse updateMentorSlot(Long slotId, MentorSlotRequest reqDto, Member member) {
86104
Mentor mentor = findMentorByMember(member);
@@ -112,6 +130,42 @@ public void deleteMentorSlot(Long slotId, Member member) {
112130
}
113131

114132

133+
// ===== 반복 슬롯 생성 로직 =====
134+
135+
/**
136+
* 특정 요일에 해당하는 모든 슬롯들을 생성
137+
*/
138+
private List<MentorSlot> generateSlotsForDayOfWeek(MentorSlotRepetitionRequest reqDto, DayOfWeek targetDayOfWeek, Mentor mentor) {
139+
List<MentorSlot> mentorSlots = new ArrayList<>();
140+
LocalDate currentDate = findNextOrSameDayOfWeek(reqDto.repeatStartDate(), targetDayOfWeek);
141+
142+
// 해당 요일에 대해 주 단위로 반복하여 슬롯 생성
143+
while (!currentDate.isAfter(reqDto.repeatEndDate())) {
144+
LocalDateTime startDateTime = LocalDateTime.of(currentDate, reqDto.startTime());
145+
LocalDateTime endDateTime = LocalDateTime.of(currentDate, reqDto.endTime());
146+
147+
validateOverlappingSlots(mentor, startDateTime, endDateTime);
148+
149+
MentorSlot mentorSlot = MentorSlot.builder()
150+
.mentor(mentor)
151+
.startDateTime(startDateTime)
152+
.endDateTime(endDateTime)
153+
.build();
154+
mentorSlots.add(mentorSlot);
155+
156+
currentDate = currentDate.plusWeeks(1);
157+
}
158+
return mentorSlots;
159+
}
160+
161+
/**
162+
* 시작일부터 해당 요일의 첫 번째 날짜를 찾기
163+
*/
164+
private LocalDate findNextOrSameDayOfWeek(LocalDate startDate, DayOfWeek targetDay) {
165+
return startDate.with(TemporalAdjusters.nextOrSame(targetDay));
166+
}
167+
168+
115169
// ===== 헬퍼 메서드 =====
116170

117171
private Mentor findMentorByMember(Member member) {

back/src/test/java/com/back/domain/mentoring/mentoring/controller/MentoringControllerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class MentoringControllerTest {
5050
@Autowired private AuthTokenService authTokenService;
5151

5252
private static final String TOKEN = "accessToken";
53-
private static final String MENTORING_URL = "/mentoring";
53+
private static final String MENTORING_URL = "/mentorings";
5454

5555
private Mentor mentor;
5656
private Mentee mentee;

back/src/test/java/com/back/domain/mentoring/slot/controller/MentorSlotControllerTest.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
import org.springframework.test.web.servlet.ResultActions;
2626
import org.springframework.transaction.annotation.Transactional;
2727

28+
import java.time.DayOfWeek;
2829
import java.time.LocalDateTime;
2930
import java.time.format.DateTimeFormatter;
3031
import java.util.ArrayList;
3132
import java.util.List;
33+
import java.util.Set;
34+
import java.util.stream.Collectors;
3235

3336
import static org.assertj.core.api.Assertions.assertThat;
3437
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
@@ -49,7 +52,7 @@ class MentorSlotControllerTest {
4952
@Autowired private AuthTokenService authTokenService;
5053

5154
private static final String TOKEN = "accessToken";
52-
private static final String MENTOR_SLOT_URL = "/mentor-slot";
55+
private static final String MENTOR_SLOT_URL = "/mentor-slots";
5356

5457
private Mentor mentor;
5558
private String mentorToken;
@@ -226,6 +229,60 @@ void createMentorSlotFailOverlappingSlots() throws Exception {
226229
}
227230

228231

232+
// ===== 슬롯 반복 생성 =====
233+
@Test
234+
@DisplayName("멘토 슬롯 반복 생성 성공")
235+
void createMentorSlotRepetitionSuccess() throws Exception {
236+
String req = """
237+
{
238+
"repeatStartDate": "2025-11-01",
239+
"repeatEndDate": "2025-11-30",
240+
"daysOfWeek": ["MONDAY", "WEDNESDAY", "FRIDAY"],
241+
"startTime": "10:00:00",
242+
"endTime": "11:00:00"
243+
}
244+
""";
245+
246+
long beforeCount = mentorSlotRepository.countByMentorId(mentor.getId());
247+
248+
mvc.perform(
249+
post(MENTOR_SLOT_URL + "/repetition")
250+
.cookie(new Cookie(TOKEN, mentorToken))
251+
.contentType(MediaType.APPLICATION_JSON)
252+
.content(req)
253+
)
254+
.andDo(print())
255+
.andExpect(status().isCreated())
256+
.andExpect(jsonPath("$.resultCode").value("201"))
257+
.andExpect(jsonPath("$.msg").value("반복 일정을 등록했습니다."));
258+
259+
// 11월 월/수/금 = 13개
260+
long afterCount = mentorSlotRepository.countByMentorId(mentor.getId());
261+
assertThat(afterCount - beforeCount).isEqualTo(12);
262+
263+
List<MentorSlot> createdSlots = mentorSlotRepository.findMySlots(
264+
mentor.getId(),
265+
LocalDateTime.of(2025, 11, 1, 0, 0),
266+
LocalDateTime.of(2025, 12, 1, 0, 0)
267+
);
268+
assertThat(createdSlots).hasSize(12);
269+
270+
// 모든 슬롯이 월/수/금인지 검증
271+
Set<DayOfWeek> actualDaysOfWeek = createdSlots.stream()
272+
.map(slot -> slot.getStartDateTime().getDayOfWeek())
273+
.collect(Collectors.toSet());
274+
275+
assertThat(actualDaysOfWeek).containsExactlyInAnyOrder(
276+
DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY
277+
);
278+
279+
// 시간 검증
280+
MentorSlot firstSlot = createdSlots.getFirst();
281+
assertThat(firstSlot.getStartDateTime().getHour()).isEqualTo(10);
282+
assertThat(firstSlot.getEndDateTime().getHour()).isEqualTo(11);
283+
}
284+
285+
229286
// ===== 슬롯 수정 =====
230287

231288
@Test

0 commit comments

Comments
 (0)