Skip to content

Commit c751efe

Browse files
committed
Feat: 예약 거절, 취소
1 parent 939aeeb commit c751efe

File tree

4 files changed

+193
-1
lines changed

4 files changed

+193
-1
lines changed

back/src/main/java/com/back/domain/member/member/service/MemberStorage.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import lombok.RequiredArgsConstructor;
1111
import org.springframework.stereotype.Component;
1212

13+
import java.util.Optional;
14+
1315
@Component
1416
@RequiredArgsConstructor
1517
public class MemberStorage {
@@ -27,6 +29,9 @@ public Mentor findMentorByMemberId(Long memberId) {
2729
return mentorRepository.findByMemberIdWithMember(memberId)
2830
.orElseThrow(() -> new ServiceException(MemberErrorCode.NOT_FOUND_MENTOR));
2931
}
32+
public Optional<Mentor> findMentorByMemberOptional(Member member) {
33+
return mentorRepository.findByMemberIdWithMember(member.getId());
34+
}
3035

3136
public Mentee findMenteeByMember(Member member) {
3237
return menteeRepository.findByMemberIdWithMember(member.getId())

back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.domain.mentoring.reservation.controller;
22

3+
import com.back.domain.member.member.entity.Member;
34
import com.back.domain.member.member.service.MemberStorage;
45
import com.back.domain.member.mentee.entity.Mentee;
56
import com.back.domain.member.mentor.entity.Mentor;
@@ -15,6 +16,8 @@
1516
import org.springframework.security.access.prepost.PreAuthorize;
1617
import org.springframework.web.bind.annotation.*;
1718

19+
import java.util.Optional;
20+
1821
@RestController
1922
@RequestMapping("/reservations")
2023
@RequiredArgsConstructor
@@ -42,7 +45,7 @@ public RsData<ReservationResponse> createReservation(
4245
);
4346
}
4447

45-
@PatchMapping("/{reservationId}")
48+
@PatchMapping("/{reservationId}/approve")
4649
@PreAuthorize("hasRole('MENTOR')")
4750
@Operation(summary = "예약 수락", description = "멘토가 멘티의 예약 신청을 수락합니다. 로그인한 멘토만 예약 수락할 수 있습니다.")
4851
public RsData<ReservationResponse> approveReservation(
@@ -58,4 +61,44 @@ public RsData<ReservationResponse> approveReservation(
5861
resDto
5962
);
6063
}
64+
65+
@PatchMapping("/{reservationId}/reject")
66+
@PreAuthorize("hasRole('MENTOR')")
67+
@Operation(summary = "예약 거절", description = "멘토가 멘티의 예약 신청을 거절합니다. 로그인한 멘토만 예약 거절할 수 있습니다.")
68+
public RsData<ReservationResponse> rejectReservation(
69+
@PathVariable Long reservationId
70+
) {
71+
Mentor mentor = memberStorage.findMentorByMember(rq.getActor());
72+
73+
ReservationResponse resDto = reservationService.rejectReservation(mentor, reservationId);
74+
75+
return new RsData<>(
76+
"200",
77+
"예약이 거절되었습니다.",
78+
resDto
79+
);
80+
}
81+
82+
@PatchMapping("/{reservationId}/cancel")
83+
@Operation(summary = "예약 취소", description = "멘토 또는 멘티가 예약을 취소합니다. 로그인 후 예약 취소할 수 있습니다.")
84+
public RsData<ReservationResponse> cancelReservation(
85+
@PathVariable Long reservationId
86+
) {
87+
Member member = rq.getActor();
88+
ReservationResponse resDto;
89+
90+
Optional<Mentor> mentor = memberStorage.findMentorByMemberOptional(member);
91+
if (mentor.isPresent()) {
92+
resDto = reservationService.cancelReservation(mentor.get(), reservationId);
93+
} else {
94+
Mentee mentee = memberStorage.findMenteeByMember(member);
95+
resDto = reservationService.cancelReservation(mentee, reservationId);
96+
}
97+
98+
return new RsData<>(
99+
"200",
100+
"예약이 취소되었습니다.",
101+
resDto
102+
);
103+
}
61104
}

back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public ReservationResponse createReservation(Mentee mentee, ReservationRequest r
4545
.build();
4646

4747
mentorSlot.setReservation(reservation);
48+
// flush 필요...?
4849

4950
reservationRepository.save(reservation);
5051

@@ -69,6 +70,27 @@ public ReservationResponse approveReservation(Mentor mentor, Long reservationId)
6970
}
7071
}
7172

73+
@Transactional
74+
public ReservationResponse rejectReservation(Mentor mentor, Long reservationId) {
75+
Reservation reservation = mentoringStorage.findReservation(reservationId);
76+
reservation.reject(mentor);
77+
return ReservationResponse.from(reservation);
78+
}
79+
80+
@Transactional
81+
public ReservationResponse cancelReservation(Mentor mentor, Long reservationId) {
82+
Reservation reservation = mentoringStorage.findReservation(reservationId);
83+
reservation.cancel(mentor);
84+
return ReservationResponse.from(reservation);
85+
}
86+
87+
@Transactional
88+
public ReservationResponse cancelReservation(Mentee mentee, Long reservationId) {
89+
Reservation reservation = mentoringStorage.findReservation(reservationId);
90+
reservation.cancel(mentee);
91+
return ReservationResponse.from(reservation);
92+
}
93+
7294

7395
// ===== 검증 메서드 =====
7496

back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
import org.mockito.InjectMocks;
3030
import org.mockito.Mock;
3131
import org.mockito.junit.jupiter.MockitoExtension;
32+
import org.springframework.test.util.ReflectionTestUtils;
3233

3334
import java.time.LocalDateTime;
35+
import java.time.temporal.ChronoUnit;
3436
import java.util.List;
3537
import java.util.Optional;
3638

@@ -287,4 +289,124 @@ void throwExceptionOnConcurrentApproval() {
287289
.hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.CONCURRENT_APPROVAL_CONFLICT.getCode());
288290
}
289291
}
292+
293+
@Nested
294+
@DisplayName("예약 거절")
295+
class Describe_rejectReservation {
296+
297+
@Test
298+
@DisplayName("예약 거절 성공")
299+
void rejectReservation() {
300+
// given
301+
when(mentoringStorage.findReservation(reservation.getId()))
302+
.thenReturn(reservation);
303+
304+
// when
305+
ReservationResponse result = reservationService.rejectReservation(mentor, reservation.getId());
306+
307+
// then
308+
assertThat(result.reservation().status()).isEqualTo(ReservationStatus.REJECTED);
309+
assertThat(result.reservation().mentorSlotId()).isEqualTo(mentorSlot2.getId());
310+
assertThat(result.mentor().mentorId()).isEqualTo(mentor.getId());
311+
assertThat(result.mentee().menteeId()).isEqualTo(mentee.getId());
312+
}
313+
314+
@Test
315+
@DisplayName("PENDING 상태가 아니면 거절 불가")
316+
void throwExceptionWhenNotPending() {
317+
// given
318+
reservation.approve(mentor);
319+
320+
when(mentoringStorage.findReservation(reservation.getId()))
321+
.thenReturn(reservation);
322+
323+
// when & then
324+
assertThatThrownBy(() -> reservationService.rejectReservation(mentor, reservation.getId()))
325+
.isInstanceOf(ServiceException.class)
326+
.hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.CANNOT_REJECT.getCode());
327+
}
328+
}
329+
330+
@Nested
331+
@DisplayName("예약 취소")
332+
class Describe_cancelReservation {
333+
334+
@Test
335+
@DisplayName("멘토가 예약 취소 성공")
336+
void cancelReservationByMentor() {
337+
// given
338+
when(mentoringStorage.findReservation(reservation.getId()))
339+
.thenReturn(reservation);
340+
341+
// when
342+
ReservationResponse result = reservationService.cancelReservation(mentor, reservation.getId());
343+
344+
// then
345+
assertThat(result.reservation().status()).isEqualTo(ReservationStatus.CANCELED);
346+
}
347+
348+
@Test
349+
@DisplayName("멘티가 예약 취소 성공")
350+
void cancelReservationByMentee() {
351+
// given
352+
when(mentoringStorage.findReservation(reservation.getId()))
353+
.thenReturn(reservation);
354+
355+
// when
356+
ReservationResponse result = reservationService.cancelReservation(mentee, reservation.getId());
357+
358+
// then
359+
assertThat(result.reservation().status()).isEqualTo(ReservationStatus.CANCELED);
360+
}
361+
362+
@Test
363+
@DisplayName("COMPLETED 상태는 취소 불가")
364+
void throwExceptionWhenCompleted() {
365+
// given
366+
// 완료 상태의 과거 슬롯
367+
LocalDateTime pastTime = LocalDateTime.now().minusDays(1).truncatedTo(ChronoUnit.SECONDS);
368+
MentorSlot pastSlot = MentorSlotFixture.create(3L, mentor, pastTime, pastTime.plusHours(1));
369+
Reservation completedReservation = ReservationFixture.create(2L, mentoring, mentee, pastSlot);
370+
371+
// PENDING -> APPROVED는 미래 시간에 해야 하므로 리플렉션으로 직접 상태 변경
372+
ReflectionTestUtils.setField(completedReservation, "status", ReservationStatus.APPROVED);
373+
completedReservation.complete();
374+
375+
when(mentoringStorage.findReservation(completedReservation.getId()))
376+
.thenReturn(completedReservation);
377+
378+
// when & then
379+
assertThatThrownBy(() -> reservationService.cancelReservation(mentor, completedReservation.getId()))
380+
.isInstanceOf(ServiceException.class)
381+
.hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.CANNOT_CANCEL.getCode());
382+
}
383+
384+
@Test
385+
@DisplayName("다른 멘토는 취소 불가")
386+
void throwExceptionWhenNotMentor() {
387+
// given
388+
Member anotherMentorMember = MemberFixture.create("[email protected]", "Another", "pass123");
389+
Mentor anotherMentor = MentorFixture.create(2L, anotherMentorMember);
390+
when(mentoringStorage.findReservation(reservation.getId()))
391+
.thenReturn(reservation);
392+
393+
// when & then
394+
assertThatThrownBy(() -> reservationService.cancelReservation(anotherMentor, reservation.getId()))
395+
.isInstanceOf(ServiceException.class)
396+
.hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_MENTOR.getCode());
397+
}
398+
399+
@Test
400+
@DisplayName("다른 멘티는 취소 불가")
401+
void throwExceptionWhenNotMentee() {
402+
// given
403+
when(mentoringStorage.findReservation(reservation.getId()))
404+
.thenReturn(reservation);
405+
406+
// when & then
407+
assertThatThrownBy(() -> reservationService.cancelReservation(mentee2, reservation.getId()))
408+
.isInstanceOf(ServiceException.class)
409+
.hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_MENTEE.getCode());
410+
}
411+
}
290412
}

0 commit comments

Comments
 (0)