Skip to content

Commit 8d849a8

Browse files
authored
Refactor: 멘토 슬롯-예약 1:N 관계로 변경 (#158)
* Refactor: 슬롯:예약 1:N 관계로 변경 * Refactor: 슬롯 목록 조회 시 활성화된 예약 ID 함께 반환 * Refactor: Member객체 아닌 memberId로 조회 * Refactor: 멘티의 기존 예약 시간 중복 확인
1 parent 4b440ea commit 8d849a8

File tree

15 files changed

+130
-131
lines changed

15 files changed

+130
-131
lines changed

back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class Reservation extends BaseEntity {
3737
@JoinColumn(name = "mentee_id", nullable = false)
3838
private Mentee mentee;
3939

40-
@OneToOne(fetch = FetchType.LAZY)
40+
@ManyToOne(fetch = FetchType.LAZY)
4141
@JoinColumn(name = "mentor_slot_id", nullable = false)
4242
private MentorSlot mentorSlot;
4343

@@ -63,13 +63,6 @@ public Reservation(Mentoring mentoring, Mentee mentee, MentorSlot mentorSlot, St
6363

6464
private void updateStatus(ReservationStatus status) {
6565
this.status = status;
66-
67-
// 양방향 동기화
68-
if (status == ReservationStatus.CANCELED || status == ReservationStatus.REJECTED) {
69-
mentorSlot.removeReservation();
70-
} else {
71-
mentorSlot.updateStatus();
72-
}
7366
}
7467

7568
public boolean isMentor(Mentor mentor) {

back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public enum ReservationErrorCode implements ErrorCode {
3030
NOT_AVAILABLE_SLOT("409-1", "이미 예약이 완료된 시간대입니다."),
3131
ALREADY_RESERVED_SLOT("409-2", "이미 예약한 시간대입니다. 예약 목록을 확인해 주세요."),
3232
CONCURRENT_RESERVATION_CONFLICT("409-3", "다른 사용자가 먼저 예약했습니다. 새로고침 후 다시 시도해 주세요."),
33-
CONCURRENT_APPROVAL_CONFLICT("409-4", "이미 수락한 예약입니다.");
33+
CONCURRENT_APPROVAL_CONFLICT("409-4", "이미 수락한 예약입니다."),
34+
OVERLAPPING_TIME("409-5", "이미 해당 시간에 다른 예약이 있습니다. 예약 목록을 확인해 주세요.");
3435

3536
private final String code;
3637
private final String message;

back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.data.jpa.repository.Query;
1010
import org.springframework.data.repository.query.Param;
1111

12+
import java.time.LocalDateTime;
1213
import java.util.List;
1314
import java.util.Optional;
1415

@@ -20,22 +21,22 @@ public interface ReservationRepository extends JpaRepository<Reservation, Long>
2021
SELECT r
2122
FROM Reservation r
2223
WHERE r.id = :reservationId
23-
AND (r.mentee.member = :member
24-
OR r.mentor.member = :member)
24+
AND (r.mentee.member.id = :memberId
25+
OR r.mentor.member.id = :memberId)
2526
""")
2627
Optional<Reservation> findByIdAndMember(
2728
@Param("reservationId") Long reservationId,
28-
@Param("member") Member member
29+
@Param("memberId") Long memberId
2930
);
3031

3132
@Query("""
3233
SELECT r
3334
FROM Reservation r
34-
WHERE r.mentor.member = :member
35+
WHERE r.mentor.member.id = :memberId
3536
ORDER BY r.mentorSlot.startDateTime DESC
3637
""")
3738
Page<Reservation> findAllByMentorMember(
38-
@Param("member") Member member,
39+
@Param("memberId") Long memberId,
3940
Pageable pageable
4041
);
4142

@@ -58,4 +59,21 @@ Page<Reservation> findAllByMenteeMember(
5859
* - 취소/거절된 예약도 히스토리로 보존
5960
*/
6061
boolean existsByMentorSlotId(Long slotId);
62+
63+
@Query("""
64+
SELECT CASE WHEN COUNT(r) > 0
65+
THEN TRUE
66+
ELSE FALSE
67+
END
68+
FROM Reservation r
69+
WHERE r.mentee.id = :menteeId
70+
AND r.status NOT IN ('REJECTED', 'CANCELED')
71+
AND r.mentorSlot.startDateTime < :end
72+
AND r.mentorSlot.endDateTime > :start
73+
""")
74+
boolean existsOverlappingTimeForMentee(
75+
@Param("menteeId") Long menteeId,
76+
@Param("start") LocalDateTime start,
77+
@Param("end") LocalDateTime end
78+
);
6179
}

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.back.domain.mentoring.reservation.entity.Reservation;
1313
import com.back.domain.mentoring.reservation.error.ReservationErrorCode;
1414
import com.back.domain.mentoring.reservation.repository.ReservationRepository;
15+
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
1516
import com.back.domain.mentoring.slot.entity.MentorSlot;
1617
import com.back.domain.mentoring.slot.service.DateTimeValidator;
1718
import com.back.global.exception.ServiceException;
@@ -40,7 +41,7 @@ public Page<ReservationDto> getReservations(Member member, int page, int size) {
4041
Page<Reservation> reservations;
4142

4243
if (member.getRole() == Member.Role.MENTOR) {
43-
reservations = reservationRepository.findAllByMentorMember(member, pageable);
44+
reservations = reservationRepository.findAllByMentorMember(member.getId(), pageable);
4445
} else {
4546
reservations = reservationRepository.findAllByMenteeMember(member, pageable);
4647
}
@@ -49,7 +50,7 @@ public Page<ReservationDto> getReservations(Member member, int page, int size) {
4950

5051
@Transactional(readOnly = true)
5152
public ReservationResponse getReservation(Member member, Long reservationId) {
52-
Reservation reservation = reservationRepository.findByIdAndMember(reservationId, member)
53+
Reservation reservation = reservationRepository.findByIdAndMember(reservationId, member.getId())
5354
.orElseThrow(() -> new ServiceException(ReservationErrorCode.RESERVATION_NOT_ACCESSIBLE));
5455

5556
return ReservationResponse.from(reservation);
@@ -61,21 +62,20 @@ public ReservationResponse createReservation(Mentee mentee, ReservationRequest r
6162
Mentoring mentoring = mentoringStorage.findMentoring(reqDto.mentoringId());
6263
MentorSlot mentorSlot = mentoringStorage.findMentorSlot(reqDto.mentorSlotId());
6364

64-
validateMentorSlotStatus(mentorSlot, mentee);
6565
DateTimeValidator.validateStartTimeNotInPast(mentorSlot.getStartDateTime());
66+
validateMentorSlotStatus(mentorSlot, mentee);
67+
validateOverlappingTimeForMentee(mentee, mentorSlot);
6668

6769
Reservation reservation = Reservation.builder()
6870
.mentoring(mentoring)
6971
.mentee(mentee)
7072
.mentorSlot(mentorSlot)
7173
.preQuestion(reqDto.preQuestion())
7274
.build();
73-
74-
mentorSlot.setReservation(reservation);
75-
// flush 필요...?
76-
7775
reservationRepository.save(reservation);
7876

77+
mentorSlot.updateStatus(MentorSlotStatus.PENDING);
78+
7979
return ReservationResponse.from(reservation);
8080
} catch (OptimisticLockException e) {
8181
throw new ServiceException(ReservationErrorCode.CONCURRENT_RESERVATION_CONFLICT);
@@ -88,6 +88,7 @@ public ReservationResponse approveReservation(Mentor mentor, Long reservationId)
8888
Reservation reservation = mentoringStorage.findReservation(reservationId);
8989

9090
reservation.approve(mentor);
91+
reservation.getMentorSlot().updateStatus(MentorSlotStatus.APPROVED);
9192

9293
// 세션
9394

@@ -101,13 +102,17 @@ public ReservationResponse approveReservation(Mentor mentor, Long reservationId)
101102
public ReservationResponse rejectReservation(Mentor mentor, Long reservationId) {
102103
Reservation reservation = mentoringStorage.findReservation(reservationId);
103104
reservation.reject(mentor);
105+
reservation.getMentorSlot().updateStatus(MentorSlotStatus.AVAILABLE);
106+
104107
return ReservationResponse.from(reservation);
105108
}
106109

107110
@Transactional
108111
public ReservationResponse cancelReservation(Member member, Long reservationId) {
109112
Reservation reservation = mentoringStorage.findReservation(reservationId);
110113
reservation.cancel(member);
114+
reservation.getMentorSlot().updateStatus(MentorSlotStatus.AVAILABLE);
115+
111116
return ReservationResponse.from(reservation);
112117
}
113118

@@ -127,4 +132,13 @@ private void validateMentorSlotStatus(MentorSlot mentorSlot, Mentee mentee) {
127132
throw new ServiceException(ReservationErrorCode.NOT_AVAILABLE_SLOT);
128133
}
129134
}
135+
136+
private void validateOverlappingTimeForMentee(Mentee mentee, MentorSlot mentorSlot) {
137+
boolean isOverlapping = reservationRepository
138+
.existsOverlappingTimeForMentee(mentee.getId(), mentorSlot.getStartDateTime(), mentorSlot.getEndDateTime());
139+
140+
if (isOverlapping) {
141+
throw new ServiceException(ReservationErrorCode.OVERLAPPING_TIME);
142+
}
143+
}
130144
}
Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.back.domain.mentoring.slot.dto.response;
22

33
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
4-
import com.back.domain.mentoring.slot.entity.MentorSlot;
54
import io.swagger.v3.oas.annotations.media.Schema;
65

76
import java.time.LocalDateTime;
@@ -16,15 +15,8 @@ public record MentorSlotDto(
1615
@Schema(description = "종료 일시")
1716
LocalDateTime endDateTime,
1817
@Schema(description = "멘토 슬롯 상태")
19-
MentorSlotStatus mentorSlotStatus
18+
MentorSlotStatus mentorSlotStatus,
19+
@Schema(description = "활성화된 예약 ID")
20+
Long reservationId
2021
) {
21-
public static MentorSlotDto from(MentorSlot mentorSlot) {
22-
return new MentorSlotDto(
23-
mentorSlot.getId(),
24-
mentorSlot.getMentor().getId(),
25-
mentorSlot.getStartDateTime(),
26-
mentorSlot.getEndDateTime(),
27-
mentorSlot.getStatus()
28-
);
29-
}
3022
}

back/src/main/java/com/back/domain/mentoring/slot/entity/MentorSlot.java

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.back.domain.mentoring.slot.entity;
22

33
import com.back.domain.member.mentor.entity.Mentor;
4-
import com.back.domain.mentoring.reservation.entity.Reservation;
54
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
65
import com.back.global.jpa.BaseEntity;
76
import jakarta.persistence.*;
@@ -25,9 +24,6 @@ public class MentorSlot extends BaseEntity {
2524
@Column(nullable = false)
2625
private LocalDateTime endDateTime;
2726

28-
@OneToOne(mappedBy = "mentorSlot")
29-
private Reservation reservation;
30-
3127
@Enumerated(EnumType.STRING)
3228
@Column(nullable = false)
3329
private MentorSlotStatus status;
@@ -48,41 +44,8 @@ public void updateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) {
4844
this.endDateTime = endDateTime;
4945
}
5046

51-
// =========================
52-
// TODO - 현재 상태
53-
// 1. reservation 필드에는 활성 예약(PENDING, APPROVED)만 세팅
54-
// 2. 취소/거절 예약은 DB에 남기고 reservation 필드에는 연결하지 않음
55-
// 3. 슬롯 재생성 불필요, 상태 기반 isAvailable() 로 새 예약 가능 판단
56-
//
57-
// TODO - 추후 변경
58-
// 1. 1:N 구조로 리팩토링
59-
// - MentorSlot에 여러 Reservation 연결 가능
60-
// - 모든 예약 기록(히스토리) 보존
61-
// 2. 상태 기반 필터링 유지: 활성 예약만 계산 시 사용
62-
// 3. 이벤트 소싱/분석 등 확장 가능하도록 구조 개선
63-
// =========================
64-
65-
public void updateStatus() {
66-
if (reservation == null) {
67-
this.status = MentorSlotStatus.AVAILABLE;
68-
} else {
69-
this.status = switch (reservation.getStatus()) {
70-
case PENDING -> MentorSlotStatus.PENDING;
71-
case APPROVED -> MentorSlotStatus.APPROVED;
72-
case COMPLETED -> MentorSlotStatus.COMPLETED;
73-
case REJECTED, CANCELED -> MentorSlotStatus.AVAILABLE;
74-
};
75-
}
76-
}
77-
78-
public void setReservation(Reservation reservation) {
79-
this.reservation = reservation;
80-
updateStatus();
81-
}
82-
83-
public void removeReservation() {
84-
this.reservation = null;
85-
this.status = MentorSlotStatus.AVAILABLE;
47+
public void updateStatus(MentorSlotStatus status) {
48+
this.status = status;
8649
}
8750

8851
/**
@@ -91,7 +54,7 @@ public void removeReservation() {
9154
* - 예약이 취소/거절된 경우 true
9255
*/
9356
public boolean isAvailable() {
94-
return reservation == null;
57+
return status == MentorSlotStatus.AVAILABLE;
9558
}
9659

9760
public boolean isOwnerBy(Mentor mentor) {

back/src/main/java/com/back/domain/mentoring/slot/repository/MentorSlotRepository.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.domain.mentoring.slot.repository;
22

3+
import com.back.domain.mentoring.slot.dto.response.MentorSlotDto;
34
import com.back.domain.mentoring.slot.entity.MentorSlot;
45
import org.springframework.data.jpa.repository.JpaRepository;
56
import org.springframework.data.jpa.repository.Query;
@@ -17,35 +18,51 @@ public interface MentorSlotRepository extends JpaRepository<MentorSlot, Long> {
1718
void deleteAllByMentorId(Long mentorId);
1819

1920
@Query("""
20-
SELECT ms
21+
SELECT new com.back.domain.mentoring.slot.dto.response.MentorSlotDto(
22+
ms.id,
23+
ms.mentor.id,
24+
ms.startDateTime,
25+
ms.endDateTime,
26+
ms.status,
27+
r.id
28+
)
2129
FROM MentorSlot ms
30+
LEFT JOIN Reservation r
31+
ON ms.id = r.mentorSlot.id
32+
AND r.status IN ('PENDING', 'APPROVED', 'COMPLETED')
2233
WHERE ms.mentor.id = :mentorId
2334
AND ms.startDateTime < :end
2435
AND ms.endDateTime >= :start
2536
ORDER BY ms.startDateTime ASC
2637
""")
27-
List<MentorSlot> findMySlots(
38+
List<MentorSlotDto> findMySlots(
2839
@Param("mentorId") Long mentorId,
2940
@Param("start") LocalDateTime start,
3041
@Param("end") LocalDateTime end
3142
);
3243

3344
@Query("""
34-
SELECT ms
45+
SELECT new com.back.domain.mentoring.slot.dto.response.MentorSlotDto(
46+
ms.id,
47+
ms.mentor.id,
48+
ms.startDateTime,
49+
ms.endDateTime,
50+
ms.status,
51+
NULL
52+
)
3553
FROM MentorSlot ms
3654
WHERE ms.mentor.id = :mentorId
3755
AND ms.status = 'AVAILABLE'
3856
AND ms.startDateTime < :end
3957
AND ms.endDateTime >= :start
4058
ORDER BY ms.startDateTime ASC
4159
""")
42-
List<MentorSlot> findAvailableSlots(
60+
List<MentorSlotDto> findAvailableSlots(
4361
@Param("mentorId") Long mentorId,
4462
@Param("start") LocalDateTime start,
4563
@Param("end") LocalDateTime end
4664
);
4765

48-
// TODO: 현재는 시간 겹침만 체크, 추후 1:N 구조 시 활성 예약 기준으로 변경
4966
@Query("""
5067
SELECT CASE WHEN COUNT(ms) > 0
5168
THEN TRUE

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,14 @@ public class MentorSlotService {
3333
public List<MentorSlotDto> getMyMentorSlots(Mentor mentor, LocalDateTime startDate, LocalDateTime endDate) {
3434
DateTimeValidator.validateTime(startDate, endDate);
3535

36-
List<MentorSlot> availableSlots = mentorSlotRepository.findMySlots(mentor.getId(), startDate, endDate);
37-
38-
return availableSlots.stream()
39-
.map(MentorSlotDto::from)
40-
.toList();
36+
return mentorSlotRepository.findMySlots(mentor.getId(), startDate, endDate);
4137
}
4238

4339
@Transactional(readOnly = true)
4440
public List<MentorSlotDto> getAvailableMentorSlots(Long mentorId, LocalDateTime startDate, LocalDateTime endDate) {
4541
DateTimeValidator.validateTime(startDate, endDate);
4642

47-
List<MentorSlot> availableSlots = mentorSlotRepository.findAvailableSlots(mentorId, startDate, endDate);
48-
49-
return availableSlots.stream()
50-
.map(MentorSlotDto::from)
51-
.toList();
43+
return mentorSlotRepository.findAvailableSlots(mentorId, startDate, endDate);
5244
}
5345

5446
@Transactional(readOnly = true)

back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.back.domain.member.member.entity.Member;
44
import com.back.domain.member.member.service.AuthTokenService;
5-
import com.back.domain.member.mentee.entity.Mentee;
65
import com.back.domain.member.mentor.entity.Mentor;
76
import com.back.domain.mentoring.mentoring.entity.Mentoring;
87
import com.back.domain.mentoring.reservation.entity.Reservation;
@@ -49,7 +48,6 @@ class ReservationControllerTest {
4948
private static final String RESERVATION_URL = "/reservations";
5049

5150
private Mentor mentor;
52-
private Mentee mentee;
5351
private Mentoring mentoring;
5452
private MentorSlot mentorSlot;
5553
private String menteeToken;
@@ -62,7 +60,7 @@ void setUp() {
6260

6361
// Mentee
6462
Member menteeMember = memberFixture.createMenteeMember();
65-
mentee = memberFixture.createMentee(menteeMember);
63+
memberFixture.createMentee(menteeMember);
6664
menteeToken = authTokenService.genAccessToken(menteeMember);
6765

6866
// Mentoring, MentorSlot

0 commit comments

Comments
 (0)