Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ public RsData<MentoringPagingResponse> getMentorings(
public RsData<MentoringResponse> getMentoring(
@PathVariable Long mentoringId
) {
MentoringResponse mentoring = mentoringService.getMentoring(mentoringId);
MentoringResponse resDto = mentoringService.getMentoring(mentoringId);

return new RsData<>(
"200",
"멘토링을 조회하였습니다.",
mentoring
resDto
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import com.back.domain.mentoring.mentoring.entity.Mentoring;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface MentoringRepository extends JpaRepository<Mentoring, Long>, MentoringRepositoryCustom {
List<Mentoring> findByMentorId(Long mentorId);
Optional<Mentoring> findTopByOrderByIdDesc();

boolean existsByMentorId(Long mentorId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.back.domain.mentoring.slot.controller;

import com.back.domain.member.member.entity.Member;
import com.back.domain.mentoring.slot.dto.request.MentorSlotRequest;
import com.back.domain.mentoring.slot.dto.response.MentorSlotResponse;
import com.back.domain.mentoring.slot.service.MentorSlotService;
import com.back.global.rq.Rq;
import com.back.global.rsData.RsData;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mentor-slot")
@RequiredArgsConstructor
public class MentorSlotController {

private final MentorSlotService mentorSlotService;
private final Rq rq;

@PostMapping
@PreAuthorize("hasRole('MENTOR')")
public RsData<MentorSlotResponse> createMentorSlot(
@RequestBody @Valid MentorSlotRequest reqDto
) {
Member member = rq.getActor();
MentorSlotResponse resDto = mentorSlotService.createMentorSlot(reqDto, member);

return new RsData<>(
"201",
"멘토링 예약 일정을 등록했습니다.",
resDto
);
}

@PutMapping("/{slotId}")
public RsData<MentorSlotResponse> updateMentorSlot(
@PathVariable Long slotId,
@RequestBody @Valid MentorSlotRequest reqDto
) {
Member member = rq.getActor();
MentorSlotResponse resDto = mentorSlotService.updateMentorSlot(slotId, reqDto, member);

return new RsData<>(
"200",
"멘토링 예약 일정이 수정되었습니다.",
resDto
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.back.domain.mentoring.slot.dto;

import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
import io.swagger.v3.oas.annotations.media.Schema;

import java.time.LocalDateTime;

public record MentorSlotDto(
@Schema(description = "멘토링 슬롯 ID")
Long mentorSlotId,
@Schema(description = "시작 일시")
LocalDateTime startDateTime,
@Schema(description = "종료 일시")
LocalDateTime endDateTime,
@Schema(description = "멘토 슬롯 상태")
MentorSlotStatus mentorSlotStatus
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.mentoring.slot.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;

public record MentorSlotRequest(
@Schema(description = "멘토 ID")
@NotNull
Long mentorId,

@Schema(description = "시작 일시")
@NotNull
LocalDateTime startDateTime,

@Schema(description = "종료 일시")
@NotNull
LocalDateTime endDateTime
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.back.domain.mentoring.slot.dto.response;

import com.back.domain.mentoring.mentoring.entity.Mentoring;
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
import com.back.domain.mentoring.slot.entity.MentorSlot;
import io.swagger.v3.oas.annotations.media.Schema;

import java.time.LocalDateTime;

public record MentorSlotResponse(
@Schema(description = "멘토링 슬롯 ID")
Long mentorSlotId,

@Schema(description = "멘토 ID")
Long mentorId,

@Schema(description = "멘토링 ID")
Long mentoringId,

@Schema(description = "멘토링 제목")
String mentoringTitle,

@Schema(description = "시작 일시")
LocalDateTime startDateTime,

@Schema(description = "종료 일시")
LocalDateTime endDateTime,

@Schema(description = "멘토 슬롯 상태")
MentorSlotStatus mentorSlotStatus,

@Schema(description = "생성일")
LocalDateTime createDate,

@Schema(description = "수정일")
LocalDateTime modifyDate
) {
public static MentorSlotResponse from(MentorSlot mentorSlot, Mentoring mentoring) {
return new MentorSlotResponse(
mentorSlot.getId(),
mentorSlot.getMentor().getId(),
mentoring.getId(),
mentoring.getTitle(),
mentorSlot.getStartDateTime(),
mentorSlot.getEndDateTime(),
mentorSlot.getStatus(),
mentorSlot.getCreateDate(),
mentorSlot.getModifyDate()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,53 @@ public class MentorSlot extends BaseEntity {
@OneToOne(mappedBy = "mentorSlot")
private Reservation reservation;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private MentorSlotStatus status;

@Builder
public MentorSlot(Mentor mentor, LocalDateTime startDateTime, LocalDateTime endDateTime) {
this.mentor = mentor;
this.startDateTime = startDateTime;
this.endDateTime = endDateTime;
this.status = MentorSlotStatus.AVAILABLE;
}

public MentorSlotStatus getStatus() {
// =========================
// TODO - 현재 상태
// 1. reservation 필드에는 활성 예약(PENDING, APPROVED)만 세팅
// 2. 취소/거절 예약은 DB에 남기고 reservation 필드에는 연결하지 않음
// 3. 슬롯 재생성 불필요, 상태 기반 isAvailable() 로 새 예약 가능 판단
//
// TODO - 추후 변경
// 1. 1:N 구조로 리팩토링
// - MentorSlot에 여러 Reservation 연결 가능
// - 모든 예약 기록(히스토리) 보존
// 2. 상태 기반 필터링 유지: 활성 예약만 계산 시 사용
// 3. 이벤트 소싱/분석 등 확장 가능하도록 구조 개선
// =========================

public void updateStatus() {
if (reservation == null) {
return MentorSlotStatus.AVAILABLE;
this.status = MentorSlotStatus.AVAILABLE;
} else {
this.status = switch (reservation.getStatus()) {
case PENDING -> MentorSlotStatus.PENDING;
case APPROVED -> MentorSlotStatus.APPROVED;
case COMPLETED -> MentorSlotStatus.COMPLETED;
case REJECTED, CANCELED -> MentorSlotStatus.AVAILABLE;
};
}

return switch (reservation.getStatus()) {
case PENDING -> MentorSlotStatus.PENDING;
case APPROVED -> MentorSlotStatus.APPROVED;
case COMPLETED -> MentorSlotStatus.COMPLETED;
default -> MentorSlotStatus.AVAILABLE;
};
}

public boolean isAvailable() {
return reservation == null ||
reservation.getStatus().equals(ReservationStatus.REJECTED) ||
reservation.getStatus().equals(ReservationStatus.CANCELED);
}

public void update(LocalDateTime startDateTime, LocalDateTime endDateTime) {
this.startDateTime = startDateTime;
this.endDateTime = endDateTime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.back.domain.mentoring.slot.error;

import com.back.global.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MentorSlotErrorCode implements ErrorCode {

// 400 DateTime 체크
START_TIME_REQUIRED("400-1", "시작 일시와 종료 일시는 필수입니다."),
END_TIME_REQUIRED("400-2", "시작 일시와 종료 일시는 필수입니다."),
START_TIME_IN_PAST("400-3", "시작 일시는 현재 이후여야 합니다."),
END_TIME_BEFORE_START("400-4", "종료 일시는 시작 일시보다 이후여야 합니다."),
INSUFFICIENT_SLOT_DURATION("400-5", "슬롯은 최소 30분 이상이어야 합니다."),

// 400 Slot 체크
CANNOT_UPDATE_RESERVED_SLOT("400-6", "예약된 슬롯은 수정할 수 없습니다."),

// 403
NOT_OWNER("403-1", "일정의 소유주가 아닙니다."),

// 404
NOT_FOUND_MENTOR_SLOT("404-1", "일정 정보가 없습니다."),

// 409
OVERLAPPING_SLOT("409-1", "선택한 시간은 이미 예약된 시간대입니다.");

private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,45 @@

import com.back.domain.mentoring.slot.entity.MentorSlot;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.Optional;

public interface MentorSlotRepository extends JpaRepository<MentorSlot, Long> {
Optional<MentorSlot> findTopByOrderByIdDesc();
boolean existsByMentorId(Long mentorId);

void deleteAllByMentorId(Long mentorId);

// TODO: 현재는 시간 겹침만 체크, 추후 1:N 구조 시 활성 예약 기준으로 변경
@Query("""
SELECT CASE WHEN COUNT(ms) > 0
THEN TRUE
ELSE FALSE END
FROM MentorSlot ms
WHERE ms.mentor.id = :mentorId
AND (ms.startDateTime < :end AND ms.endDateTime > :start)
""")
boolean existsOverlappingSlot(
@Param("mentorId") Long mentorId,
@Param("start")LocalDateTime start,
@Param("end") LocalDateTime end
);

@Query("""
SELECT CASE WHEN COUNT(ms) > 0
THEN TRUE
ELSE FALSE END
FROM MentorSlot ms
WHERE ms.mentor.id = :mentorId
AND ms.id != :slotId
AND (ms.startDateTime < :end AND ms.endDateTime > :start)
""")
boolean existsOverlappingExcept(
@Param("mentorId") Long mentorId,
@Param("slotId") Long slotId,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end
);
}
Loading