diff --git a/back/src/main/java/com/back/domain/member/member/error/MemberErrorCode.java b/back/src/main/java/com/back/domain/member/member/error/MemberErrorCode.java new file mode 100644 index 00000000..57b7d845 --- /dev/null +++ b/back/src/main/java/com/back/domain/member/member/error/MemberErrorCode.java @@ -0,0 +1,17 @@ +package com.back.domain.member.member.error; + +import com.back.global.exception.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MemberErrorCode implements ErrorCode { + + NOT_FOUND_MEMBER("404-1", "회원을 찾을 수 없습니다."), + NOT_FOUND_MENTOR("404-2", "멘토를 찾을 수 없습니다."), + NOT_FOUND_MENTEE("404-3", "멘티를 찾을 수 없습니다."); + + private final String code; + private final String message; +} diff --git a/back/src/main/java/com/back/domain/member/member/service/MemberStorage.java b/back/src/main/java/com/back/domain/member/member/service/MemberStorage.java new file mode 100644 index 00000000..e47429b4 --- /dev/null +++ b/back/src/main/java/com/back/domain/member/member/service/MemberStorage.java @@ -0,0 +1,33 @@ +package com.back.domain.member.member.service; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.member.member.error.MemberErrorCode; +import com.back.domain.member.mentee.entity.Mentee; +import com.back.domain.member.mentee.repository.MenteeRepository; +import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.member.mentor.repository.MentorRepository; +import com.back.global.exception.ServiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MemberStorage { + + private final MentorRepository mentorRepository; + private final MenteeRepository menteeRepository; + + public Mentor findMentorByMember(Member member) { + return findMentorByMemberId(member.getId()); + } + + public Mentor findMentorByMemberId(Long memberId) { + return mentorRepository.findByMemberId(memberId) + .orElseThrow(() -> new ServiceException(MemberErrorCode.NOT_FOUND_MENTOR)); + } + + public Mentee findMenteeByMember(Member member) { + return menteeRepository.findByMemberId(member.getId()) + .orElseThrow(() -> new ServiceException(MemberErrorCode.NOT_FOUND_MENTEE)); + } +} diff --git a/back/src/main/java/com/back/domain/member/mentee/dto/MenteeDto.java b/back/src/main/java/com/back/domain/member/mentee/dto/MenteeDto.java new file mode 100644 index 00000000..b28031a8 --- /dev/null +++ b/back/src/main/java/com/back/domain/member/mentee/dto/MenteeDto.java @@ -0,0 +1,18 @@ +package com.back.domain.member.mentee.dto; + +import com.back.domain.member.mentee.entity.Mentee; +import io.swagger.v3.oas.annotations.media.Schema; + +public record MenteeDto( + @Schema(description = "멘티 ID") + Long menteeId, + @Schema(description = "멘티명") + String name +) { + public static MenteeDto from(Mentee mentee) { + return new MenteeDto( + mentee.getId(), + mentee.getMember().getName() + ); + } +} diff --git a/back/src/main/java/com/back/domain/member/mentor/dto/MentorDetailDto.java b/back/src/main/java/com/back/domain/member/mentor/dto/MentorDetailDto.java new file mode 100644 index 00000000..59cfebf6 --- /dev/null +++ b/back/src/main/java/com/back/domain/member/mentor/dto/MentorDetailDto.java @@ -0,0 +1,25 @@ +package com.back.domain.member.mentor.dto; + +import com.back.domain.member.mentor.entity.Mentor; +import io.swagger.v3.oas.annotations.media.Schema; + +public record MentorDetailDto( + @Schema(description = "멘토 ID") + Long mentorId, + @Schema(description = "멘토명") + String name, + @Schema(description = "평점") + Double rate, + // TODO: Job id, name + @Schema(description = "연차") + Integer careerYears +) { + public static MentorDetailDto from(Mentor mentor) { + return new MentorDetailDto( + mentor.getId(), + mentor.getMember().getName(), + mentor.getRate(), + mentor.getCareerYears() + ); + } +} diff --git a/back/src/main/java/com/back/domain/member/mentor/dto/MentorDto.java b/back/src/main/java/com/back/domain/member/mentor/dto/MentorDto.java index ecd7a2ef..e76575b9 100644 --- a/back/src/main/java/com/back/domain/member/mentor/dto/MentorDto.java +++ b/back/src/main/java/com/back/domain/member/mentor/dto/MentorDto.java @@ -3,23 +3,16 @@ import com.back.domain.member.mentor.entity.Mentor; import io.swagger.v3.oas.annotations.media.Schema; -public record MentorDto ( +public record MentorDto( @Schema(description = "멘토 ID") Long mentorId, @Schema(description = "멘토명") - String name, - @Schema(description = "평점") - Double rate, - // TODO: Job id, name - @Schema(description = "연차") - Integer careerYears + String name ) { public static MentorDto from(Mentor mentor) { return new MentorDto( mentor.getId(), - mentor.getMember().getName(), - mentor.getRate(), - mentor.getCareerYears() + mentor.getMember().getName() ); } } diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/controller/MentoringController.java b/back/src/main/java/com/back/domain/mentoring/mentoring/controller/MentoringController.java index 111b6679..b846dd96 100644 --- a/back/src/main/java/com/back/domain/mentoring/mentoring/controller/MentoringController.java +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/controller/MentoringController.java @@ -1,7 +1,7 @@ package com.back.domain.mentoring.mentoring.controller; import com.back.domain.member.member.entity.Member; -import com.back.domain.mentoring.mentoring.dto.MentoringDto; +import com.back.domain.mentoring.mentoring.dto.MentoringWithTagsDto; import com.back.domain.mentoring.mentoring.dto.request.MentoringRequest; import com.back.domain.mentoring.mentoring.dto.response.MentoringPagingResponse; import com.back.domain.mentoring.mentoring.dto.response.MentoringResponse; @@ -31,7 +31,7 @@ public RsData getMentorings( @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String keyword ) { - Page mentoringPage = mentoringService.getMentorings(keyword, page, size); + Page mentoringPage = mentoringService.getMentorings(keyword, page, size); MentoringPagingResponse resDto = MentoringPagingResponse.from(mentoringPage); return new RsData<>( diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringDto.java b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringDto.java index 8635e10a..70b61cb9 100644 --- a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringDto.java +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringDto.java @@ -3,21 +3,16 @@ import com.back.domain.mentoring.mentoring.entity.Mentoring; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - public record MentoringDto( @Schema(description = "멘토링 ID") Long mentoringId, @Schema(description = "멘토링 제목") - String title, - @Schema(description = "멘토링 태그") - List tags + String title ) { public static MentoringDto from(Mentoring mentoring) { return new MentoringDto( mentoring.getId(), - mentoring.getTitle(), - mentoring.getTags() + mentoring.getTitle() ); } } diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringWithTagsDto.java b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringWithTagsDto.java new file mode 100644 index 00000000..9a4abd88 --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringWithTagsDto.java @@ -0,0 +1,23 @@ +package com.back.domain.mentoring.mentoring.dto; + +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record MentoringWithTagsDto( + @Schema(description = "멘토링 ID") + Long mentoringId, + @Schema(description = "멘토링 제목") + String title, + @Schema(description = "멘토링 태그") + List tags +) { + public static MentoringWithTagsDto from(Mentoring mentoring) { + return new MentoringWithTagsDto( + mentoring.getId(), + mentoring.getTitle(), + mentoring.getTags() + ); + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringPagingResponse.java b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringPagingResponse.java index 8d5819f8..06dff187 100644 --- a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringPagingResponse.java +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringPagingResponse.java @@ -1,6 +1,6 @@ package com.back.domain.mentoring.mentoring.dto.response; -import com.back.domain.mentoring.mentoring.dto.MentoringDto; +import com.back.domain.mentoring.mentoring.dto.MentoringWithTagsDto; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.data.domain.Page; @@ -8,7 +8,7 @@ public record MentoringPagingResponse( @Schema(description = "멘토링 목록") - List mentorings, + List mentorings, @Schema(description = "현재 페이지 (0부터 시작)") int currentPage, @Schema(description = "총 페이지") @@ -18,7 +18,7 @@ public record MentoringPagingResponse( @Schema(description = "다음 페이지 존재 여부") boolean hasNext ) { - public static MentoringPagingResponse from(Page page) { + public static MentoringPagingResponse from(Page page) { return new MentoringPagingResponse( page.getContent(), page.getNumber(), diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringResponse.java b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringResponse.java index 56e9809b..0054e73f 100644 --- a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringResponse.java +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/response/MentoringResponse.java @@ -1,6 +1,6 @@ package com.back.domain.mentoring.mentoring.dto.response; -import com.back.domain.member.mentor.dto.MentorDto; +import com.back.domain.member.mentor.dto.MentorDetailDto; import com.back.domain.mentoring.mentoring.dto.MentoringDetailDto; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,6 +8,6 @@ public record MentoringResponse( @Schema(description = "멘토링") MentoringDetailDto mentoring, @Schema(description = "멘토") - MentorDto mentor + MentorDetailDto mentor ) { } diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/error/MentoringErrorCode.java b/back/src/main/java/com/back/domain/mentoring/mentoring/error/MentoringErrorCode.java index 05ea5866..074a4506 100644 --- a/back/src/main/java/com/back/domain/mentoring/mentoring/error/MentoringErrorCode.java +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/error/MentoringErrorCode.java @@ -17,6 +17,7 @@ public enum MentoringErrorCode implements ErrorCode { // 404 NOT_FOUND_MENTOR("404-1", "멘토를 찾을 수 없습니다."), NOT_FOUND_MENTORING("404-2", "멘토링을 찾을 수 없습니다."), + NOT_FOUND_MENTEE("404-3", "멘티를 찾을 수 없습니다."), // 409 ALREADY_EXISTS_MENTORING("409-1", "이미 멘토링 정보가 존재합니다."); diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringService.java b/back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringService.java index d3c4bd0a..f0f5ba7b 100644 --- a/back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringService.java +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringService.java @@ -1,11 +1,11 @@ package com.back.domain.mentoring.mentoring.service; import com.back.domain.member.member.entity.Member; -import com.back.domain.member.mentor.dto.MentorDto; +import com.back.domain.member.mentor.dto.MentorDetailDto; import com.back.domain.member.mentor.entity.Mentor; import com.back.domain.member.mentor.repository.MentorRepository; import com.back.domain.mentoring.mentoring.dto.MentoringDetailDto; -import com.back.domain.mentoring.mentoring.dto.MentoringDto; +import com.back.domain.mentoring.mentoring.dto.MentoringWithTagsDto; import com.back.domain.mentoring.mentoring.dto.request.MentoringRequest; import com.back.domain.mentoring.mentoring.dto.response.MentoringResponse; import com.back.domain.mentoring.mentoring.entity.Mentoring; @@ -30,11 +30,11 @@ public class MentoringService { private final MentorSlotRepository mentorSlotRepository; @Transactional(readOnly = true) - public Page getMentorings(String keyword, int page, int size) { + public Page getMentorings(String keyword, int page, int size) { Pageable pageable = PageRequest.of(page, size); return mentoringRepository.searchMentorings(keyword, pageable) - .map(MentoringDto::from); + .map(MentoringWithTagsDto::from); } @Transactional(readOnly = true) @@ -44,7 +44,7 @@ public MentoringResponse getMentoring(Long mentoringId) { return new MentoringResponse( MentoringDetailDto.from(mentoring), - MentorDto.from(mentor) + MentorDetailDto.from(mentor) ); } @@ -69,7 +69,7 @@ public MentoringResponse createMentoring(MentoringRequest reqDto, Member member) return new MentoringResponse( MentoringDetailDto.from(mentoring), - MentorDto.from(mentor) + MentorDetailDto.from(mentor) ); } @@ -84,7 +84,7 @@ public MentoringResponse updateMentoring(Long mentoringId, MentoringRequest reqD return new MentoringResponse( MentoringDetailDto.from(mentoring), - MentorDto.from(mentor) + MentorDetailDto.from(mentor) ); } diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringStorage.java b/back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringStorage.java new file mode 100644 index 00000000..2545d4c9 --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/service/MentoringStorage.java @@ -0,0 +1,31 @@ +package com.back.domain.mentoring.mentoring.service; + +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import com.back.domain.mentoring.mentoring.error.MentoringErrorCode; +import com.back.domain.mentoring.mentoring.repository.MentoringRepository; +import com.back.domain.mentoring.reservation.repository.ReservationRepository; +import com.back.domain.mentoring.slot.entity.MentorSlot; +import com.back.domain.mentoring.slot.error.MentorSlotErrorCode; +import com.back.domain.mentoring.slot.repository.MentorSlotRepository; +import com.back.global.exception.ServiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MentoringStorage { + + private final MentoringRepository mentoringRepository; + private final MentorSlotRepository mentorSlotRepository; + private final ReservationRepository reservationRepository; + + public Mentoring findMentoring(Long mentoringId) { + return mentoringRepository.findById(mentoringId) + .orElseThrow(() -> new ServiceException(MentoringErrorCode.NOT_FOUND_MENTORING)); + } + + public MentorSlot findMentorSlot(Long slotId) { + return mentorSlotRepository.findById(slotId) + .orElseThrow(() -> new ServiceException(MentorSlotErrorCode.NOT_FOUND_MENTOR_SLOT)); + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java b/back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java new file mode 100644 index 00000000..3e2904d1 --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java @@ -0,0 +1,46 @@ +package com.back.domain.mentoring.reservation.controller; + +import com.back.domain.member.member.service.MemberStorage; +import com.back.domain.member.mentee.entity.Mentee; +import com.back.domain.mentoring.reservation.dto.request.ReservationRequest; +import com.back.domain.mentoring.reservation.dto.response.ReservationResponse; +import com.back.domain.mentoring.reservation.service.ReservationService; +import com.back.global.rq.Rq; +import com.back.global.rsData.RsData; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/reservations") +@RequiredArgsConstructor +@Tag(name = "ReservationController", description = "예약 API") +public class ReservationController { + + private final ReservationService reservationService; + private final MemberStorage memberStorage; + private final Rq rq; + + @PostMapping + @PreAuthorize("hasRole('MENTEE')") + @Operation(summary = "예약 신청", description = "멘티가 멘토의 슬롯을 선택해 예약 신청을 합니다. 로그인한 멘티만 예약 신청할 수 있습니다.") + public RsData createReservation( + @RequestBody @Valid ReservationRequest reqDto + ) { + Mentee mentee = memberStorage.findMenteeByMember(rq.getActor()); + + ReservationResponse resDto = reservationService.createReservation(mentee, reqDto); + + return new RsData<>( + "201", + "예약 신청이 완료되었습니다.", + resDto + ); + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/dto/ReservationDetailDto.java b/back/src/main/java/com/back/domain/mentoring/reservation/dto/ReservationDetailDto.java new file mode 100644 index 00000000..b22a195a --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/dto/ReservationDetailDto.java @@ -0,0 +1,41 @@ +package com.back.domain.mentoring.reservation.dto; + +import com.back.domain.mentoring.reservation.constant.ReservationStatus; +import com.back.domain.mentoring.reservation.entity.Reservation; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +public record ReservationDetailDto( + @Schema(description = "예약 ID") + Long reservationId, + @Schema(description = "예약 상태") + ReservationStatus status, + @Schema(description = "사전 질문") + String preQuestion, + + @Schema(description = "멘토 슬롯 ID") + Long mentorSlotId, + @Schema(description = "시작 일시") + LocalDateTime startDateTime, + @Schema(description = "종료 일시") + LocalDateTime endDateTime, + + @Schema(description = "생성일") + LocalDateTime createDate, + @Schema(description = "수정일") + LocalDateTime modifyDate +) { + public static ReservationDetailDto from(Reservation reservation) { + return new ReservationDetailDto( + reservation.getId(), + reservation.getStatus(), + reservation.getPreQuestion(), + reservation.getMentorSlot().getId(), + reservation.getMentorSlot().getStartDateTime(), + reservation.getMentorSlot().getEndDateTime(), + reservation.getCreateDate(), + reservation.getModifyDate() + ); + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/dto/request/ReservationRequest.java b/back/src/main/java/com/back/domain/mentoring/reservation/dto/request/ReservationRequest.java new file mode 100644 index 00000000..01f59559 --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/dto/request/ReservationRequest.java @@ -0,0 +1,22 @@ +package com.back.domain.mentoring.reservation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record ReservationRequest( + @Schema(description = "멘토 ID") + @NotNull + Long mentorId, + + @Schema(description = "멘토 슬롯 ID") + @NotNull + Long mentorSlotId, + + @Schema(description = "멘토링 ID") + @NotNull + Long mentoringId, + + @Schema(description = "사전 질문") + String preQuestion +) { +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/dto/response/ReservationResponse.java b/back/src/main/java/com/back/domain/mentoring/reservation/dto/response/ReservationResponse.java new file mode 100644 index 00000000..2ef3769b --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/dto/response/ReservationResponse.java @@ -0,0 +1,23 @@ +package com.back.domain.mentoring.reservation.dto.response; + +import com.back.domain.member.mentee.dto.MenteeDto; +import com.back.domain.member.mentor.dto.MentorDto; +import com.back.domain.mentoring.mentoring.dto.MentoringDto; +import com.back.domain.mentoring.reservation.dto.ReservationDetailDto; +import com.back.domain.mentoring.reservation.entity.Reservation; + +public record ReservationResponse( + ReservationDetailDto reservation, + MentoringDto mentoring, + MentorDto mentor, + MenteeDto mentee +) { + public static ReservationResponse from(Reservation reservation) { + return new ReservationResponse( + ReservationDetailDto.from(reservation), + MentoringDto.from(reservation.getMentoring()), + MentorDto.from(reservation.getMentor()), + MenteeDto.from(reservation.getMentee()) + ); + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java b/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java index 8749e8a8..a7c193fb 100644 --- a/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java +++ b/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java @@ -57,6 +57,12 @@ public void updateStatus(ReservationStatus status) { // 양방향 동기화 if (status.equals(ReservationStatus.CANCELED) || status.equals(ReservationStatus.REJECTED)) { mentorSlot.removeReservation(); + } else { + mentorSlot.updateStatus(); } } + + public boolean isMentee(Mentee mentee) { + return this.mentee.equals(mentee); + } } diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java b/back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java new file mode 100644 index 00000000..1536757c --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java @@ -0,0 +1,17 @@ +package com.back.domain.mentoring.reservation.error; + +import com.back.global.exception.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ReservationErrorCode implements ErrorCode { + NOT_AVAILABLE_SLOT("400-1", "예약할 수 없는 슬롯입니다."), + ALREADY_RESERVED_SLOT("400-2", "해당 시간에 예약 내역이 있습니다. 예약 목록을 확인해 주세요."), + + NOT_FOUND_RESERVATION("404-1", "예약이 존재하지 않습니다."); + + private final String code; + private final String message; +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java b/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java index 9ff5bc02..4a54f7cf 100644 --- a/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java +++ b/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java @@ -3,7 +3,11 @@ import com.back.domain.mentoring.reservation.entity.Reservation; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface ReservationRepository extends JpaRepository { + Optional findTopByOrderByIdDesc(); + boolean existsByMentoringId(Long mentoringId); /** diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java b/back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java new file mode 100644 index 00000000..2c7ed8e6 --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java @@ -0,0 +1,58 @@ +package com.back.domain.mentoring.reservation.service; + +import com.back.domain.member.mentee.entity.Mentee; +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import com.back.domain.mentoring.mentoring.service.MentoringStorage; +import com.back.domain.mentoring.reservation.dto.request.ReservationRequest; +import com.back.domain.mentoring.reservation.dto.response.ReservationResponse; +import com.back.domain.mentoring.reservation.entity.Reservation; +import com.back.domain.mentoring.reservation.error.ReservationErrorCode; +import com.back.domain.mentoring.reservation.repository.ReservationRepository; +import com.back.domain.mentoring.slot.constant.MentorSlotStatus; +import com.back.domain.mentoring.slot.entity.MentorSlot; +import com.back.domain.mentoring.slot.service.DateTimeValidator; +import com.back.global.exception.ServiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ReservationService { + + private final ReservationRepository reservationRepository; + private final MentoringStorage mentoringStorage; + + @Transactional + public ReservationResponse createReservation(Mentee mentee, ReservationRequest reqDto) { + Mentoring mentoring = mentoringStorage.findMentoring(reqDto.mentoringId()); + MentorSlot mentorSlot = mentoringStorage.findMentorSlot(reqDto.mentorSlotId()); + + validateMentorSlotStatus(mentorSlot, mentee); + DateTimeValidator.validateStartTimeNotInPast(mentorSlot.getStartDateTime()); + + Reservation reservation = Reservation.builder() + .mentoring(mentoring) + .mentee(mentee) + .mentorSlot(mentorSlot) + .preQuestion(reqDto.preQuestion()) + .build(); + reservationRepository.save(reservation); + + return ReservationResponse.from(reservation); + } + + + // ===== 검증 메서드 ===== + + private static void validateMentorSlotStatus(MentorSlot mentorSlot, Mentee mentee) { + if (!mentorSlot.getStatus().equals(MentorSlotStatus.AVAILABLE)) { + if (mentorSlot.getReservation() != null && + mentorSlot.getReservation().isMentee(mentee) + ) { + throw new ServiceException(ReservationErrorCode.ALREADY_RESERVED_SLOT); + } + throw new ServiceException(ReservationErrorCode.NOT_AVAILABLE_SLOT); + } + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/slot/entity/MentorSlot.java b/back/src/main/java/com/back/domain/mentoring/slot/entity/MentorSlot.java index ce5c1e58..05dcfde1 100644 --- a/back/src/main/java/com/back/domain/mentoring/slot/entity/MentorSlot.java +++ b/back/src/main/java/com/back/domain/mentoring/slot/entity/MentorSlot.java @@ -1,7 +1,6 @@ package com.back.domain.mentoring.slot.entity; import com.back.domain.member.mentor.entity.Mentor; -import com.back.domain.mentoring.reservation.constant.ReservationStatus; import com.back.domain.mentoring.reservation.entity.Reservation; import com.back.domain.mentoring.slot.constant.MentorSlotStatus; import com.back.global.jpa.BaseEntity; @@ -80,7 +79,7 @@ public void setReservation(Reservation reservation) { public void removeReservation() { this.reservation = null; - updateStatus(); + this.status = MentorSlotStatus.AVAILABLE; } /** @@ -89,9 +88,7 @@ public void removeReservation() { * - 예약이 취소/거절된 경우 true */ public boolean isAvailable() { - return reservation == null || - reservation.getStatus().equals(ReservationStatus.REJECTED) || - reservation.getStatus().equals(ReservationStatus.CANCELED); + return reservation == null; } public boolean isOwnerBy(Mentor mentor) { diff --git a/back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java b/back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java new file mode 100644 index 00000000..a4a04f01 --- /dev/null +++ b/back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java @@ -0,0 +1,140 @@ +package com.back.domain.mentoring.reservation.controller; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.member.member.service.AuthTokenService; +import com.back.domain.member.mentee.entity.Mentee; +import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import com.back.domain.mentoring.reservation.entity.Reservation; +import com.back.domain.mentoring.reservation.error.ReservationErrorCode; +import com.back.domain.mentoring.reservation.repository.ReservationRepository; +import com.back.domain.mentoring.slot.entity.MentorSlot; +import com.back.fixture.MemberTestFixture; +import com.back.fixture.MentoringFixture; +import com.back.global.exception.ServiceException; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import java.time.format.DateTimeFormatter; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class ReservationControllerTest { + + @Autowired private MockMvc mvc; + @Autowired private MemberTestFixture memberFixture; + @Autowired private MentoringFixture mentoringFixture; + + @Autowired private ReservationRepository reservationRepository; + @Autowired private AuthTokenService authTokenService; + + private static final String TOKEN = "accessToken"; + private static final String RESERVATION_URL = "/reservations"; + + private Mentor mentor; + private Mentee mentee; + private Mentoring mentoring; + private MentorSlot mentorSlot; + private String menteeToken; + + @BeforeEach + void setUp() { + // Mentor + Member mentorMember = memberFixture.createMentorMember(); + mentor = memberFixture.createMentor(mentorMember); + + // Mentee + Member menteeMember = memberFixture.createMenteeMember(); + mentee = memberFixture.createMentee(menteeMember); + menteeToken = authTokenService.genAccessToken(menteeMember); + + // Mentoring, MentorSlot + mentoring = mentoringFixture.createMentoring(mentor); + mentorSlot = mentoringFixture.createMentorSlot(mentor); + } + + @Test + @DisplayName("멘티가 멘토에게 예약 신청 성공") + void createReservationSuccess() throws Exception { + ResultActions resultActions = performCreateReservation(); + + Reservation reservation = reservationRepository.findTopByOrderByIdDesc() + .orElseThrow(() -> new ServiceException(ReservationErrorCode.NOT_FOUND_RESERVATION)); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + + resultActions + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.resultCode").value("201")) + .andExpect(jsonPath("$.msg").value("예약 신청이 완료되었습니다.")) + .andExpect(jsonPath("$.data.reservation.reservationId").value(reservation.getId())) + .andExpect(jsonPath("$.data.reservation.status").value("PENDING")) + .andExpect(jsonPath("$.data.reservation.preQuestion").value(reservation.getPreQuestion())) + .andExpect(jsonPath("$.data.reservation.mentorSlotId").value(mentorSlot.getId())) + .andExpect(jsonPath("$.data.reservation.startDateTime").value(mentorSlot.getStartDateTime().format(formatter))) + .andExpect(jsonPath("$.data.reservation.endDateTime").value(mentorSlot.getEndDateTime().format(formatter))); + } + + @Test + @DisplayName("멘티가 멘토에게 예약 신청 실패 - 예약 가능한 상태가 아닌 경우") + void createReservationFailNotAvailable() throws Exception { + Member menteeMember = memberFixture.createMenteeMember(); + Mentee mentee2 = memberFixture.createMentee(menteeMember); + mentoringFixture.createReservation(mentoring, mentee2, mentorSlot); + + performCreateReservation() + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.resultCode").value("400-1")) + .andExpect(jsonPath("$.msg").value("예약할 수 없는 슬롯입니다.")); + } + + @Test + @DisplayName("멘티가 멘토에게 예약 신청 실패 - 예약 가능한 상태가 아닌 경우") + void createReservationFailAlreadyReservation() throws Exception { + mentoringFixture.createReservation(mentoring, mentee, mentorSlot); + + performCreateReservation() + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.resultCode").value("400-2")) + .andExpect(jsonPath("$.msg").value("해당 시간에 예약 내역이 있습니다. 예약 목록을 확인해 주세요.")); + } + + + // ===== perform ===== + + private ResultActions performCreateReservation() throws Exception { + String req = """ + { + "mentorId": %d, + "mentorSlotId": %d, + "mentoringId": %d, + "preQuestion": "질문" + } + """.formatted(mentor.getId(), mentorSlot.getId(), mentoring.getId()); + + return mvc.perform( + post(RESERVATION_URL) + .cookie(new Cookie(TOKEN, menteeToken)) + .contentType(MediaType.APPLICATION_JSON) + .content(req) + ) + .andDo(print()) + .andExpect(handler().handlerType(ReservationController.class)) + .andExpect(handler().methodName("createReservation")); + } + +} \ No newline at end of file