Skip to content

Commit 7dba116

Browse files
authored
[Feat] 멘토 슬롯 등록, 수정 구현 (#53)
* feat: 멘토 슬롯 생성 * feat: 멘토 슬롯 수정
1 parent 7ef35b6 commit 7dba116

File tree

14 files changed

+602
-19
lines changed

14 files changed

+602
-19
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ public RsData<MentoringPagingResponse> getMentorings(
4141
public RsData<MentoringResponse> getMentoring(
4242
@PathVariable Long mentoringId
4343
) {
44-
MentoringResponse mentoring = mentoringService.getMentoring(mentoringId);
44+
MentoringResponse resDto = mentoringService.getMentoring(mentoringId);
4545

4646
return new RsData<>(
4747
"200",
4848
"멘토링을 조회하였습니다.",
49-
mentoring
49+
resDto
5050
);
5151
}
5252

back/src/main/java/com/back/domain/mentoring/mentoring/repository/MentoringRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import com.back.domain.mentoring.mentoring.entity.Mentoring;
44
import org.springframework.data.jpa.repository.JpaRepository;
55

6+
import java.util.List;
67
import java.util.Optional;
78

89
public interface MentoringRepository extends JpaRepository<Mentoring, Long>, MentoringRepositoryCustom {
10+
List<Mentoring> findByMentorId(Long mentorId);
911
Optional<Mentoring> findTopByOrderByIdDesc();
10-
1112
boolean existsByMentorId(Long mentorId);
1213
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.back.domain.mentoring.slot.controller;
2+
3+
import com.back.domain.member.member.entity.Member;
4+
import com.back.domain.mentoring.slot.dto.request.MentorSlotRequest;
5+
import com.back.domain.mentoring.slot.dto.response.MentorSlotResponse;
6+
import com.back.domain.mentoring.slot.service.MentorSlotService;
7+
import com.back.global.rq.Rq;
8+
import com.back.global.rsData.RsData;
9+
import jakarta.validation.Valid;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.security.access.prepost.PreAuthorize;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
@RestController
15+
@RequestMapping("/mentor-slot")
16+
@RequiredArgsConstructor
17+
public class MentorSlotController {
18+
19+
private final MentorSlotService mentorSlotService;
20+
private final Rq rq;
21+
22+
@PostMapping
23+
@PreAuthorize("hasRole('MENTOR')")
24+
public RsData<MentorSlotResponse> createMentorSlot(
25+
@RequestBody @Valid MentorSlotRequest reqDto
26+
) {
27+
Member member = rq.getActor();
28+
MentorSlotResponse resDto = mentorSlotService.createMentorSlot(reqDto, member);
29+
30+
return new RsData<>(
31+
"201",
32+
"멘토링 예약 일정을 등록했습니다.",
33+
resDto
34+
);
35+
}
36+
37+
@PutMapping("/{slotId}")
38+
public RsData<MentorSlotResponse> updateMentorSlot(
39+
@PathVariable Long slotId,
40+
@RequestBody @Valid MentorSlotRequest reqDto
41+
) {
42+
Member member = rq.getActor();
43+
MentorSlotResponse resDto = mentorSlotService.updateMentorSlot(slotId, reqDto, member);
44+
45+
return new RsData<>(
46+
"200",
47+
"멘토링 예약 일정이 수정되었습니다.",
48+
resDto
49+
);
50+
}
51+
52+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.back.domain.mentoring.slot.dto;
2+
3+
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
6+
import java.time.LocalDateTime;
7+
8+
public record MentorSlotDto(
9+
@Schema(description = "멘토링 슬롯 ID")
10+
Long mentorSlotId,
11+
@Schema(description = "시작 일시")
12+
LocalDateTime startDateTime,
13+
@Schema(description = "종료 일시")
14+
LocalDateTime endDateTime,
15+
@Schema(description = "멘토 슬롯 상태")
16+
MentorSlotStatus mentorSlotStatus
17+
) {
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.back.domain.mentoring.slot.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
import java.time.LocalDateTime;
7+
8+
public record MentorSlotRequest(
9+
@Schema(description = "멘토 ID")
10+
@NotNull
11+
Long mentorId,
12+
13+
@Schema(description = "시작 일시")
14+
@NotNull
15+
LocalDateTime startDateTime,
16+
17+
@Schema(description = "종료 일시")
18+
@NotNull
19+
LocalDateTime endDateTime
20+
) {
21+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.back.domain.mentoring.slot.dto.response;
2+
3+
import com.back.domain.mentoring.mentoring.entity.Mentoring;
4+
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
5+
import com.back.domain.mentoring.slot.entity.MentorSlot;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
8+
import java.time.LocalDateTime;
9+
10+
public record MentorSlotResponse(
11+
@Schema(description = "멘토링 슬롯 ID")
12+
Long mentorSlotId,
13+
14+
@Schema(description = "멘토 ID")
15+
Long mentorId,
16+
17+
@Schema(description = "멘토링 ID")
18+
Long mentoringId,
19+
20+
@Schema(description = "멘토링 제목")
21+
String mentoringTitle,
22+
23+
@Schema(description = "시작 일시")
24+
LocalDateTime startDateTime,
25+
26+
@Schema(description = "종료 일시")
27+
LocalDateTime endDateTime,
28+
29+
@Schema(description = "멘토 슬롯 상태")
30+
MentorSlotStatus mentorSlotStatus,
31+
32+
@Schema(description = "생성일")
33+
LocalDateTime createDate,
34+
35+
@Schema(description = "수정일")
36+
LocalDateTime modifyDate
37+
) {
38+
public static MentorSlotResponse from(MentorSlot mentorSlot, Mentoring mentoring) {
39+
return new MentorSlotResponse(
40+
mentorSlot.getId(),
41+
mentorSlot.getMentor().getId(),
42+
mentoring.getId(),
43+
mentoring.getTitle(),
44+
mentorSlot.getStartDateTime(),
45+
mentorSlot.getEndDateTime(),
46+
mentorSlot.getStatus(),
47+
mentorSlot.getCreateDate(),
48+
mentorSlot.getModifyDate()
49+
);
50+
}
51+
}

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

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,53 @@ public class MentorSlot extends BaseEntity {
2929
@OneToOne(mappedBy = "mentorSlot")
3030
private Reservation reservation;
3131

32+
@Enumerated(EnumType.STRING)
33+
@Column(nullable = false)
34+
private MentorSlotStatus status;
35+
3236
@Builder
3337
public MentorSlot(Mentor mentor, LocalDateTime startDateTime, LocalDateTime endDateTime) {
3438
this.mentor = mentor;
3539
this.startDateTime = startDateTime;
3640
this.endDateTime = endDateTime;
41+
this.status = MentorSlotStatus.AVAILABLE;
3742
}
3843

39-
public MentorSlotStatus getStatus() {
44+
// =========================
45+
// TODO - 현재 상태
46+
// 1. reservation 필드에는 활성 예약(PENDING, APPROVED)만 세팅
47+
// 2. 취소/거절 예약은 DB에 남기고 reservation 필드에는 연결하지 않음
48+
// 3. 슬롯 재생성 불필요, 상태 기반 isAvailable() 로 새 예약 가능 판단
49+
//
50+
// TODO - 추후 변경
51+
// 1. 1:N 구조로 리팩토링
52+
// - MentorSlot에 여러 Reservation 연결 가능
53+
// - 모든 예약 기록(히스토리) 보존
54+
// 2. 상태 기반 필터링 유지: 활성 예약만 계산 시 사용
55+
// 3. 이벤트 소싱/분석 등 확장 가능하도록 구조 개선
56+
// =========================
57+
58+
public void updateStatus() {
4059
if (reservation == null) {
41-
return MentorSlotStatus.AVAILABLE;
60+
this.status = MentorSlotStatus.AVAILABLE;
61+
} else {
62+
this.status = switch (reservation.getStatus()) {
63+
case PENDING -> MentorSlotStatus.PENDING;
64+
case APPROVED -> MentorSlotStatus.APPROVED;
65+
case COMPLETED -> MentorSlotStatus.COMPLETED;
66+
case REJECTED, CANCELED -> MentorSlotStatus.AVAILABLE;
67+
};
4268
}
43-
44-
return switch (reservation.getStatus()) {
45-
case PENDING -> MentorSlotStatus.PENDING;
46-
case APPROVED -> MentorSlotStatus.APPROVED;
47-
case COMPLETED -> MentorSlotStatus.COMPLETED;
48-
default -> MentorSlotStatus.AVAILABLE;
49-
};
5069
}
5170

5271
public boolean isAvailable() {
5372
return reservation == null ||
5473
reservation.getStatus().equals(ReservationStatus.REJECTED) ||
5574
reservation.getStatus().equals(ReservationStatus.CANCELED);
5675
}
76+
77+
public void update(LocalDateTime startDateTime, LocalDateTime endDateTime) {
78+
this.startDateTime = startDateTime;
79+
this.endDateTime = endDateTime;
80+
}
5781
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.back.domain.mentoring.slot.error;
2+
3+
import com.back.global.exception.ErrorCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public enum MentorSlotErrorCode implements ErrorCode {
10+
11+
// 400 DateTime 체크
12+
START_TIME_REQUIRED("400-1", "시작 일시와 종료 일시는 필수입니다."),
13+
END_TIME_REQUIRED("400-2", "시작 일시와 종료 일시는 필수입니다."),
14+
START_TIME_IN_PAST("400-3", "시작 일시는 현재 이후여야 합니다."),
15+
END_TIME_BEFORE_START("400-4", "종료 일시는 시작 일시보다 이후여야 합니다."),
16+
INSUFFICIENT_SLOT_DURATION("400-5", "슬롯은 최소 30분 이상이어야 합니다."),
17+
18+
// 400 Slot 체크
19+
CANNOT_UPDATE_RESERVED_SLOT("400-6", "예약된 슬롯은 수정할 수 없습니다."),
20+
21+
// 403
22+
NOT_OWNER("403-1", "일정의 소유주가 아닙니다."),
23+
24+
// 404
25+
NOT_FOUND_MENTOR_SLOT("404-1", "일정 정보가 없습니다."),
26+
27+
// 409
28+
OVERLAPPING_SLOT("409-1", "선택한 시간은 이미 예약된 시간대입니다.");
29+
30+
private final String code;
31+
private final String message;
32+
}

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,45 @@
22

33
import com.back.domain.mentoring.slot.entity.MentorSlot;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
7+
8+
import java.time.LocalDateTime;
9+
import java.util.Optional;
510

611
public interface MentorSlotRepository extends JpaRepository<MentorSlot, Long> {
12+
Optional<MentorSlot> findTopByOrderByIdDesc();
713
boolean existsByMentorId(Long mentorId);
8-
914
void deleteAllByMentorId(Long mentorId);
15+
16+
// TODO: 현재는 시간 겹침만 체크, 추후 1:N 구조 시 활성 예약 기준으로 변경
17+
@Query("""
18+
SELECT CASE WHEN COUNT(ms) > 0
19+
THEN TRUE
20+
ELSE FALSE END
21+
FROM MentorSlot ms
22+
WHERE ms.mentor.id = :mentorId
23+
AND (ms.startDateTime < :end AND ms.endDateTime > :start)
24+
""")
25+
boolean existsOverlappingSlot(
26+
@Param("mentorId") Long mentorId,
27+
@Param("start")LocalDateTime start,
28+
@Param("end") LocalDateTime end
29+
);
30+
31+
@Query("""
32+
SELECT CASE WHEN COUNT(ms) > 0
33+
THEN TRUE
34+
ELSE FALSE END
35+
FROM MentorSlot ms
36+
WHERE ms.mentor.id = :mentorId
37+
AND ms.id != :slotId
38+
AND (ms.startDateTime < :end AND ms.endDateTime > :start)
39+
""")
40+
boolean existsOverlappingExcept(
41+
@Param("mentorId") Long mentorId,
42+
@Param("slotId") Long slotId,
43+
@Param("start") LocalDateTime start,
44+
@Param("end") LocalDateTime end
45+
);
1046
}

0 commit comments

Comments
 (0)