diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 90a53dad3..ee3f8e112 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -104,6 +104,13 @@ public enum ErrorCode { // database DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."), + // mentor + ALREADY_MENTOR(HttpStatus.BAD_REQUEST.value(), "이미 멘토로 등록된 사용자입니다."), + MENTOR_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 사용자는 멘토로 등록되어 있지 않습니다."), + MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."), + UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."), + MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."), + // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/mentor/controller/MentoringController.java b/src/main/java/com/example/solidconnection/mentor/controller/MentoringController.java new file mode 100644 index 000000000..9df73e41c --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/controller/MentoringController.java @@ -0,0 +1,83 @@ +package com.example.solidconnection.mentor.controller; + +import com.example.solidconnection.common.resolver.AuthorizedUser; +import com.example.solidconnection.mentor.dto.MentoringApplyRequest; +import com.example.solidconnection.mentor.dto.MentoringApplyResponse; +import com.example.solidconnection.mentor.dto.MentoringCheckResponse; +import com.example.solidconnection.mentor.dto.MentoringConfirmRequest; +import com.example.solidconnection.mentor.dto.MentoringConfirmResponse; +import com.example.solidconnection.mentor.dto.MentoringCountResponse; +import com.example.solidconnection.mentor.dto.MentoringListResponse; +import com.example.solidconnection.mentor.service.MentoringCommandService; +import com.example.solidconnection.mentor.service.MentoringQueryService; +import com.example.solidconnection.security.annotation.RequireRoleAccess; +import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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 +@RequiredArgsConstructor +@RequestMapping("/mentorings") +public class MentoringController { + + private final MentoringCommandService mentoringCommandService; + private final MentoringQueryService mentoringQueryService; + + @RequireRoleAccess(roles = Role.MENTEE) + @PostMapping("/apply") + public ResponseEntity applyMentoring( + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody MentoringApplyRequest mentoringApplyRequest + ) { + MentoringApplyResponse response = mentoringCommandService.applyMentoring(siteUser.getId(), mentoringApplyRequest); + return ResponseEntity.ok(response); + } + + @RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR}) + @GetMapping("/apply") + public ResponseEntity getMentorings( + @AuthorizedUser SiteUser siteUser + ) { + MentoringListResponse responses = mentoringQueryService.getMentorings(siteUser.getId()); + return ResponseEntity.ok(responses); + } + + @RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR}) + @PatchMapping("/{mentoring-id}/apply") + public ResponseEntity confirmMentoring( + @AuthorizedUser SiteUser siteUser, + @PathVariable("mentoring-id") Long mentoringId, + @Valid @RequestBody MentoringConfirmRequest mentoringConfirmRequest + ) { + MentoringConfirmResponse response = mentoringCommandService.confirmMentoring(siteUser.getId(), mentoringId, mentoringConfirmRequest); + return ResponseEntity.ok(response); + } + + @RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR}) + @PatchMapping("/{mentoring-id}/check") + public ResponseEntity checkMentoring( + @AuthorizedUser SiteUser siteUser, + @PathVariable("mentoring-id") Long mentoringId + ) { + MentoringCheckResponse response = mentoringCommandService.checkMentoring(siteUser.getId(), mentoringId); + return ResponseEntity.ok(response); + } + + @RequireRoleAccess(roles = {Role.ADMIN, Role.MENTOR}) + @GetMapping("/check") + public ResponseEntity getUncheckedMentoringsCount( + @AuthorizedUser SiteUser siteUser + ) { + MentoringCountResponse response = mentoringQueryService.getNewMentoringsCount(siteUser.getId()); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java b/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java index 81f9d6177..87fe963a1 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java @@ -45,4 +45,8 @@ public class Mentor { @OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true) private List channels = new ArrayList<>(); + + public void increaseMenteeCount() { + this.menteeCount++; + } } diff --git a/src/main/java/com/example/solidconnection/mentor/domain/Mentoring.java b/src/main/java/com/example/solidconnection/mentor/domain/Mentoring.java index 38811a014..dd2353df6 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/Mentoring.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/Mentoring.java @@ -56,8 +56,28 @@ public class Mentoring { @Column private long menteeId; + public Mentoring(long mentorId, long menteeId, VerifyStatus verifyStatus) { + this.mentorId = mentorId; + this.menteeId = menteeId; + this.verifyStatus = verifyStatus; + } + @PrePersist public void onPrePersist() { this.createdAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); // 나노초 6자리 까지만 저장 } + + public void confirm(VerifyStatus status, String rejectedReason) { + this.verifyStatus = status; + this.rejectedReason = rejectedReason; + this.confirmedAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); + + if (this.checkedAt == null) { + this.checkedAt = this.confirmedAt; + } + } + + public void check() { + this.checkedAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); + } } diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringApplyRequest.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringApplyRequest.java new file mode 100644 index 000000000..27d03afae --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringApplyRequest.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.mentor.dto; + +import jakarta.validation.constraints.NotNull; + +public record MentoringApplyRequest( + @NotNull(message = "멘토 id를 입력해주세요.") + Long mentorId +) { +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringApplyResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringApplyResponse.java new file mode 100644 index 000000000..d77523aa7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringApplyResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.Mentoring; + +public record MentoringApplyResponse( + long mentoringId +) { + + public static MentoringApplyResponse from(Mentoring mentoring) { + return new MentoringApplyResponse(mentoring.getId()); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringCheckResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringCheckResponse.java new file mode 100644 index 000000000..581ddd141 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringCheckResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.mentor.dto; + +public record MentoringCheckResponse( + long mentoringId +) { + + public static MentoringCheckResponse from(long mentoringId) { + return new MentoringCheckResponse(mentoringId); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringConfirmRequest.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringConfirmRequest.java new file mode 100644 index 000000000..8436f263e --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringConfirmRequest.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.common.VerifyStatus; +import jakarta.validation.constraints.NotNull; + +public record MentoringConfirmRequest( + @NotNull(message = "승인 상태를 설정해주세요.") + VerifyStatus status, + + String rejectedReason +) { +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringConfirmResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringConfirmResponse.java new file mode 100644 index 000000000..79ad48bf4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringConfirmResponse.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.Mentoring; + +public record MentoringConfirmResponse( + long mentoringId +) { + public static MentoringConfirmResponse from(Mentoring mentoring) { + return new MentoringConfirmResponse(mentoring.getId()); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringCountResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringCountResponse.java new file mode 100644 index 000000000..428b0b7f3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringCountResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.mentor.dto; + +public record MentoringCountResponse( + int uncheckedCount +) { + + public static MentoringCountResponse from(int uncheckedCount) { + return new MentoringCountResponse(uncheckedCount); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringListResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringListResponse.java new file mode 100644 index 000000000..d943db618 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringListResponse.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.mentor.dto; + +import java.util.List; + +public record MentoringListResponse( + List requests +) { + public static MentoringListResponse from(List requests) { + return new MentoringListResponse(requests); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringResponse.java new file mode 100644 index 000000000..595f28b7b --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringResponse.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.siteuser.domain.SiteUser; + +import java.time.ZonedDateTime; + +public record MentoringResponse( + long mentoringId, + String profileImageUrl, + String nickname, + boolean isChecked, + ZonedDateTime createdAt +) { + public static MentoringResponse from(Mentoring mentoring, SiteUser mentee) { + return new MentoringResponse( + mentoring.getId(), + mentee.getProfileImageUrl(), + mentee.getNickname(), + mentoring.getCheckedAt() != null, + mentoring.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java new file mode 100644 index 000000000..a8d6c91c3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.mentor.repository; + +import com.example.solidconnection.mentor.domain.Mentor; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MentorRepository extends JpaRepository { + + Optional findBySiteUserId(long siteUserId); + + boolean existsBySiteUserId(long siteUserId); +} diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java new file mode 100644 index 000000000..d0bb648eb --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.mentor.repository; + +import com.example.solidconnection.mentor.domain.Mentoring; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MentoringRepository extends JpaRepository { + + List findAllByMentorId(long mentorId); + + int countByMentorIdAndCheckedAtIsNull(long mentorId); +} diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java new file mode 100644 index 000000000..f7a9495ad --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java @@ -0,0 +1,90 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.common.VerifyStatus; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.mentor.dto.MentoringApplyRequest; +import com.example.solidconnection.mentor.dto.MentoringApplyResponse; +import com.example.solidconnection.mentor.dto.MentoringCheckResponse; +import com.example.solidconnection.mentor.dto.MentoringConfirmRequest; +import com.example.solidconnection.mentor.dto.MentoringConfirmResponse; +import com.example.solidconnection.mentor.repository.MentorRepository; +import com.example.solidconnection.mentor.repository.MentoringRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.example.solidconnection.common.exception.ErrorCode.MENTORING_ALREADY_CONFIRMED; +import static com.example.solidconnection.common.exception.ErrorCode.MENTORING_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.REJECTED_REASON_REQUIRED; +import static com.example.solidconnection.common.exception.ErrorCode.UNAUTHORIZED_MENTORING; + +@Service +@RequiredArgsConstructor +public class MentoringCommandService { + + private final MentoringRepository mentoringRepository; + private final MentorRepository mentorRepository; + + @Transactional + public MentoringApplyResponse applyMentoring(long siteUserId, MentoringApplyRequest mentoringApplyRequest) { + Mentoring mentoring = new Mentoring(mentoringApplyRequest.mentorId(), siteUserId, VerifyStatus.PENDING); + + return MentoringApplyResponse.from(mentoringRepository.save(mentoring)); + } + + @Transactional + public MentoringConfirmResponse confirmMentoring(long siteUserId, long mentoringId, MentoringConfirmRequest mentoringConfirmRequest) { + Mentoring mentoring = mentoringRepository.findById(mentoringId) + .orElseThrow(() -> new CustomException(MENTORING_NOT_FOUND)); + + Mentor mentor = mentorRepository.findBySiteUserId(siteUserId) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + + validateMentoringOwnership(mentor, mentoring); + validateMentoringNotConfirmed(mentoring); + + if (mentoringConfirmRequest.status() == VerifyStatus.REJECTED + && (mentoringConfirmRequest.rejectedReason() == null || mentoringConfirmRequest.rejectedReason().isBlank())) { + throw new CustomException(REJECTED_REASON_REQUIRED); + } + + mentoring.confirm(mentoringConfirmRequest.status(), mentoringConfirmRequest.rejectedReason()); + + if (mentoringConfirmRequest.status() == VerifyStatus.APPROVED) { + mentor.increaseMenteeCount(); + } + + return MentoringConfirmResponse.from(mentoring); + } + + private void validateMentoringNotConfirmed(Mentoring mentoring) { + if (mentoring.getVerifyStatus() != VerifyStatus.PENDING) { + throw new CustomException(MENTORING_ALREADY_CONFIRMED); + } + } + + @Transactional + public MentoringCheckResponse checkMentoring(long siteUserId, long mentoringId) { + Mentoring mentoring = mentoringRepository.findById(mentoringId) + .orElseThrow(() -> new CustomException(MENTORING_NOT_FOUND)); + + Mentor mentor = mentorRepository.findBySiteUserId(siteUserId) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + + validateMentoringOwnership(mentor, mentoring); + + mentoring.check(); + + return MentoringCheckResponse.from(mentoring.getId()); + } + + // 멘토는 본인의 멘토링에 대해 confirm 및 check해야 한다. + private void validateMentoringOwnership(Mentor mentor, Mentoring mentoring) { + if (mentoring.getMentorId() != mentor.getId()) { + throw new CustomException(UNAUTHORIZED_MENTORING); + } + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringQueryService.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringQueryService.java new file mode 100644 index 000000000..cf40b73a5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringQueryService.java @@ -0,0 +1,57 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.mentor.dto.MentoringCountResponse; +import com.example.solidconnection.mentor.dto.MentoringListResponse; +import com.example.solidconnection.mentor.dto.MentoringResponse; +import com.example.solidconnection.mentor.repository.MentorRepository; +import com.example.solidconnection.mentor.repository.MentoringRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class MentoringQueryService { + + private final MentoringRepository mentoringRepository; + private final MentorRepository mentorRepository; + private final SiteUserRepository siteUserRepository; + + @Transactional(readOnly = true) + public MentoringListResponse getMentorings(long siteUserId) { + Mentor mentor = mentorRepository.findBySiteUserId(siteUserId) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + + List mentorings = mentoringRepository.findAllByMentorId(mentor.getId()); + List mentoringResponses = mentorings.stream() + .map(mentoring -> { + SiteUser mentee = siteUserRepository.findById(mentoring.getMenteeId()) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + return MentoringResponse.from(mentoring, mentee); + }) + .toList(); + + return MentoringListResponse.from(mentoringResponses); + } + + @Transactional(readOnly = true) + public MentoringCountResponse getNewMentoringsCount(long siteUserId) { + Mentor mentor = mentorRepository.findBySiteUserId(siteUserId) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + + int count = mentoringRepository.countByMentorIdAndCheckedAtIsNull(mentor.getId()); + + return MentoringCountResponse.from(count); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixture.java b/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixture.java new file mode 100644 index 000000000..40718b2da --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixture.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.mentor.domain.Mentor; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class MentorFixture { + + private final MentorFixtureBuilder mentorFixtureBuilder; + + public Mentor 멘토(long siteUserId, long universityId) { + return mentorFixtureBuilder.mentor() + .siteUserId(siteUserId) + .universityId(universityId) + .introduction("멘토 소개") + .passTip("멘토 팁") + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixtureBuilder.java b/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixtureBuilder.java new file mode 100644 index 000000000..d499ecc2a --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/MentorFixtureBuilder.java @@ -0,0 +1,68 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.repository.MentorRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class MentorFixtureBuilder { + + private final MentorRepository mentorRepository; + + private int menteeCount = 0; + private boolean hasBadge = false; + private String introduction; + private String passTip; + private long siteUserId; + private long universityId; + + public MentorFixtureBuilder mentor() { + return new MentorFixtureBuilder(mentorRepository); + } + + public MentorFixtureBuilder menteeCount(int menteeCount) { + this.menteeCount = menteeCount; + return this; + } + + public MentorFixtureBuilder hasBadge(boolean hasBadge) { + this.hasBadge = hasBadge; + return this; + } + + public MentorFixtureBuilder introduction(String introduction) { + this.introduction = introduction; + return this; + } + + public MentorFixtureBuilder passTip(String passTip) { + this.passTip = passTip; + return this; + } + + public MentorFixtureBuilder siteUserId(Long siteUserId) { + this.siteUserId = siteUserId; + return this; + } + + public MentorFixtureBuilder universityId(Long universityId) { + this.universityId = universityId; + return this; + } + + public Mentor create() { + Mentor mentor = new Mentor( + null, + menteeCount, + hasBadge, + introduction, + passTip, + siteUserId, + universityId, + null + ); + return mentorRepository.save(mentor); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/MentoringFixture.java b/src/test/java/com/example/solidconnection/mentor/fixture/MentoringFixture.java new file mode 100644 index 000000000..3d5896662 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/MentoringFixture.java @@ -0,0 +1,60 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.common.VerifyStatus; +import com.example.solidconnection.mentor.domain.Mentoring; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +import java.time.ZonedDateTime; + +import static java.time.ZoneOffset.UTC; +import static java.time.temporal.ChronoUnit.MICROS; + +@TestComponent +@RequiredArgsConstructor +public class MentoringFixture { + + private final MentoringFixtureBuilder mentoringFixtureBuilder; + + public Mentoring 대기중_멘토링(long mentorId, long menteeId) { + return mentoringFixtureBuilder.mentoring() + .mentorId(mentorId) + .menteeId(menteeId) + .create(); + } + + public Mentoring 승인된_멘토링(long mentorId, long menteeId) { + ZonedDateTime now = getCurrentTime(); + return mentoringFixtureBuilder.mentoring() + .mentorId(mentorId) + .menteeId(menteeId) + .verifyStatus(VerifyStatus.APPROVED) + .confirmedAt(now) + .checkedAt(now) + .create(); + } + + public Mentoring 거절된_멘토링(long mentorId, long menteeId, String rejectedReason) { + ZonedDateTime now = getCurrentTime(); + return mentoringFixtureBuilder.mentoring() + .mentorId(mentorId) + .menteeId(menteeId) + .verifyStatus(VerifyStatus.REJECTED) + .rejectedReason(rejectedReason) + .confirmedAt(now) + .checkedAt(now) + .create(); + } + + public Mentoring 확인되지_않은_멘토링(long mentorId, long menteeId) { + return mentoringFixtureBuilder.mentoring() + .mentorId(mentorId) + .menteeId(menteeId) + .checkedAt(null) + .create(); + } + + private ZonedDateTime getCurrentTime() { + return ZonedDateTime.now(UTC).truncatedTo(MICROS); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/MentoringFixtureBuilder.java b/src/test/java/com/example/solidconnection/mentor/fixture/MentoringFixtureBuilder.java new file mode 100644 index 000000000..0c579de5b --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/MentoringFixtureBuilder.java @@ -0,0 +1,77 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.common.VerifyStatus; +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.mentor.repository.MentoringRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +import java.time.ZonedDateTime; + +@TestComponent +@RequiredArgsConstructor +public class MentoringFixtureBuilder { + + private final MentoringRepository mentoringRepository; + + private ZonedDateTime createdAt; + private ZonedDateTime confirmedAt; + private ZonedDateTime checkedAt; + private VerifyStatus verifyStatus = VerifyStatus.PENDING; + private String rejectedReason; + private long mentorId; + private long menteeId; + + public MentoringFixtureBuilder mentoring() { + return new MentoringFixtureBuilder(mentoringRepository); + } + + public MentoringFixtureBuilder createdAt(ZonedDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public MentoringFixtureBuilder confirmedAt(ZonedDateTime confirmedAt) { + this.confirmedAt = confirmedAt; + return this; + } + + public MentoringFixtureBuilder checkedAt(ZonedDateTime checkedAt) { + this.checkedAt = checkedAt; + return this; + } + + public MentoringFixtureBuilder verifyStatus(VerifyStatus verifyStatus) { + this.verifyStatus = verifyStatus; + return this; + } + + public MentoringFixtureBuilder rejectedReason(String rejectedReason) { + this.rejectedReason = rejectedReason; + return this; + } + + public MentoringFixtureBuilder mentorId(long mentorId) { + this.mentorId = mentorId; + return this; + } + + public MentoringFixtureBuilder menteeId(long menteeId) { + this.menteeId = menteeId; + return this; + } + + public Mentoring create() { + Mentoring mentoring = new Mentoring( + null, + createdAt, + confirmedAt, + checkedAt, + verifyStatus, + rejectedReason, + mentorId, + menteeId + ); + return mentoringRepository.save(mentoring); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java new file mode 100644 index 000000000..e9d218716 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java @@ -0,0 +1,232 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.common.VerifyStatus; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.mentor.dto.MentoringApplyRequest; +import com.example.solidconnection.mentor.dto.MentoringApplyResponse; +import com.example.solidconnection.mentor.dto.MentoringCheckResponse; +import com.example.solidconnection.mentor.dto.MentoringConfirmRequest; +import com.example.solidconnection.mentor.dto.MentoringConfirmResponse; +import com.example.solidconnection.mentor.fixture.MentorFixture; +import com.example.solidconnection.mentor.fixture.MentoringFixture; +import com.example.solidconnection.mentor.repository.MentorRepository; +import com.example.solidconnection.mentor.repository.MentoringRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.example.solidconnection.common.exception.ErrorCode.MENTORING_ALREADY_CONFIRMED; +import static com.example.solidconnection.common.exception.ErrorCode.MENTORING_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.REJECTED_REASON_REQUIRED; +import static com.example.solidconnection.common.exception.ErrorCode.UNAUTHORIZED_MENTORING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("멘토링 CUD 서비스 테스트") +class MentoringCommandServiceTest { + + @Autowired + private MentoringCommandService mentoringCommandService; + + @Autowired + private MentorRepository mentorRepository; + + @Autowired + private MentoringRepository mentoringRepository; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private MentorFixture mentorFixture; + + @Autowired + private MentoringFixture mentoringFixture; + + private SiteUser mentorUser1; + private SiteUser mentorUser2; + + private SiteUser menteeUser; + private Mentor mentor1; + private Mentor mentor2; + + @BeforeEach + void setUp() { + mentorUser1 = siteUserFixture.멘토(1, "mentor1"); + menteeUser = siteUserFixture.사용자(2, "mentee1"); + mentorUser2 = siteUserFixture.멘토(3, "mentor2"); + + mentor1 = mentorFixture.멘토(mentorUser1.getId(), 1L); + mentor2 = mentorFixture.멘토(mentorUser2.getId(), 2L); + } + + @Nested + class 멘토링_신청_테스트 { + + @Test + void 멘토링을_성공적으로_신청한다() { + // given + MentoringApplyRequest request = new MentoringApplyRequest(mentor1.getId()); + + // when + MentoringApplyResponse response = mentoringCommandService.applyMentoring(menteeUser.getId(), request); + + // then + Mentoring mentoring = mentoringRepository.findById(response.mentoringId()).orElseThrow(); + + assertAll( + () -> assertThat(mentoring.getMentorId()).isEqualTo(mentor1.getId()), + () -> assertThat(mentoring.getMenteeId()).isEqualTo(menteeUser.getId()), + () -> assertThat(mentoring.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) + ); + } + } + + @Nested + class 멘토링_승인_거절_테스트 { + + @Test + void 멘토링을_성공적으로_승인한다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED, null); + int beforeMenteeCount = mentor1.getMenteeCount(); + + // when + MentoringConfirmResponse response = mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); + + // then + Mentoring confirmedMentoring = mentoringRepository.findById(response.mentoringId()).orElseThrow(); + Mentor mentor = mentorRepository.findById(mentor1.getId()).orElseThrow(); + + assertAll( + () -> assertThat(confirmedMentoring.getVerifyStatus()).isEqualTo(VerifyStatus.APPROVED), + () -> assertThat(confirmedMentoring.getConfirmedAt()).isNotNull(), + () -> assertThat(confirmedMentoring.getCheckedAt()).isNotNull(), + () -> assertThat(mentor.getMenteeCount()).isEqualTo(beforeMenteeCount + 1) + ); + } + + @Test + void 멘토링을_성공적으로_거절한다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + String rejectedReason = "멘토링 거절 사유"; + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.REJECTED, rejectedReason); + int beforeMenteeCount = mentor1.getMenteeCount(); + + // when + MentoringConfirmResponse response = mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); + + // then + Mentoring confirmedMentoring = mentoringRepository.findById(response.mentoringId()).orElseThrow(); + Mentor mentor = mentorRepository.findById(mentor1.getId()).orElseThrow(); + + assertAll( + () -> assertThat(confirmedMentoring.getVerifyStatus()).isEqualTo(VerifyStatus.REJECTED), + () -> assertThat(confirmedMentoring.getRejectedReason()).isEqualTo(rejectedReason), + () -> assertThat(confirmedMentoring.getConfirmedAt()).isNotNull(), + () -> assertThat(confirmedMentoring.getCheckedAt()).isNotNull(), + () -> assertThat(mentor.getMenteeCount()).isEqualTo(beforeMenteeCount) + ); + } + + @Test + void 거절_시_사유가_없으면_예외_응답을_반환한다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.REJECTED, null); + + // when & then + assertThatThrownBy(() -> + mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(REJECTED_REASON_REQUIRED.getMessage()); + } + + @Test + void 다른_멘토의_멘토링을_승인할_수_없다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED, null); + + // when & then + assertThatThrownBy(() -> mentoringCommandService.confirmMentoring(mentorUser2.getId(), mentoring.getId(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(UNAUTHORIZED_MENTORING.getMessage()); + } + + @Test + void 이미_처리된_멘토링은_다시_승인할_수_없다() { + // given + Mentoring mentoring = mentoringFixture.승인된_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED, null); + + // when & then + assertThatThrownBy(() -> mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(MENTORING_ALREADY_CONFIRMED.getMessage()); + } + + @Test + void 존재하지_않는_멘토링_아이디로_요청시_예외_응답을_반환한다() { + // given + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED, null); + long invalidMentoringId = 9999L; + + // when & then + assertThatThrownBy(() -> mentoringCommandService.confirmMentoring(mentorUser1.getId(), invalidMentoringId, request)) + .isInstanceOf(CustomException.class) + .hasMessage(MENTORING_NOT_FOUND.getMessage()); + } + } + + @Nested + class 멘토링_확인_테스트 { + + @Test + void 멘토링을_성공적으로_확인_처리한다() { + // given + Mentoring mentoring = mentoringFixture.확인되지_않은_멘토링(mentor1.getId(), menteeUser.getId()); + + // when + MentoringCheckResponse response = mentoringCommandService.checkMentoring(mentorUser1.getId(), mentoring.getId()); + + // then + Mentoring checked = mentoringRepository.findById(response.mentoringId()).orElseThrow(); + + assertThat(checked.getCheckedAt()).isNotNull(); + } + + @Test + void 다른_멘토의_멘토링은_확인할_수_없다() { + // given + Mentoring mentoring = mentoringFixture.확인되지_않은_멘토링(mentor1.getId(), menteeUser.getId()); + + // when & then + assertThatThrownBy(() -> mentoringCommandService.checkMentoring(mentorUser2.getId(), mentoring.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(UNAUTHORIZED_MENTORING.getMessage()); + } + + @Test + void 존재하지_않는_멘토링_아이디로_요청시_예외_응답을_반환한다() { + // given + long invalidMentoringId = 9999L; + + // when & then + assertThatThrownBy(() -> mentoringCommandService.checkMentoring(mentorUser1.getId(), invalidMentoringId)) + .isInstanceOf(CustomException.class) + .hasMessage(MENTORING_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentoringQueryServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentoringQueryServiceTest.java new file mode 100644 index 000000000..869de8a3c --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/service/MentoringQueryServiceTest.java @@ -0,0 +1,113 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.domain.Mentoring; +import com.example.solidconnection.mentor.dto.MentoringCountResponse; +import com.example.solidconnection.mentor.dto.MentoringListResponse; +import com.example.solidconnection.mentor.dto.MentoringResponse; +import com.example.solidconnection.mentor.fixture.MentorFixture; +import com.example.solidconnection.mentor.fixture.MentoringFixture; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("멘토링 조회 서비스 테스트") +class MentoringQueryServiceTest { + + @Autowired + private MentoringQueryService mentoringQueryService; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private MentorFixture mentorFixture; + + @Autowired + private MentoringFixture mentoringFixture; + + private SiteUser mentorUser; + private SiteUser menteeUser; + private Mentor mentor; + + @BeforeEach + void setUp() { + mentorUser = siteUserFixture.멘토(1, "mentor1"); + menteeUser = siteUserFixture.사용자(2, "mentee1"); + mentor = mentorFixture.멘토(mentorUser.getId(), 1L); + } + + @Nested + class 멘토링_목록_조회_테스트 { + + @Test + void 멘토의_모든_멘토링을_조회한다() { + // given + Mentoring mentoring1 = mentoringFixture.대기중_멘토링(mentor.getId(), menteeUser.getId()); + Mentoring mentoring2 = mentoringFixture.승인된_멘토링(mentor.getId(), menteeUser.getId()); + Mentoring mentoring3 = mentoringFixture.거절된_멘토링(mentor.getId(), menteeUser.getId(), "거절 사유"); + + // when + MentoringListResponse responses = mentoringQueryService.getMentorings(mentorUser.getId()); + + // then + assertAll( + () -> assertThat(responses.requests()).hasSize(3), + () -> assertThat(responses.requests()).extracting(MentoringResponse::mentoringId) + .containsExactlyInAnyOrder( + mentoring1.getId(), + mentoring2.getId(), + mentoring3.getId() + ) + ); + } + + @Test + void 멘토링이_없는_경우_빈_리스트를_반환한다() { + // when + MentoringListResponse responses = mentoringQueryService.getMentorings(mentorUser.getId()); + + // then + assertThat(responses.requests()).isEmpty(); + } + } + + @Nested + class 새_멘토링_개수_조회_테스트 { + + @Test + void 확인되지_않은_멘토링_개수를_반환한다() { + // given + mentoringFixture.확인되지_않은_멘토링(mentor.getId(), menteeUser.getId()); + mentoringFixture.확인되지_않은_멘토링(mentor.getId(), menteeUser.getId()); + mentoringFixture.승인된_멘토링(mentor.getId(), menteeUser.getId()); + + // when + MentoringCountResponse response = mentoringQueryService.getNewMentoringsCount(mentorUser.getId()); + + // then + assertThat(response.uncheckedCount()).isEqualTo(2); + } + + @Test + void 확인되지_않은_멘토링이_없으면_0을_반환한다() { + // given + mentoringFixture.승인된_멘토링(mentor.getId(), menteeUser.getId()); + + // when + MentoringCountResponse response = mentoringQueryService.getNewMentoringsCount(mentorUser.getId()); + + // then + assertThat(response.uncheckedCount()).isZero(); + } + } +}