Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8e1431c
feat: 멘토링 신청 구현
whqtker Jun 30, 2025
8877521
feat: 멘토링 요청 목록 조회, 멘토링 수락/거절 구현
whqtker Jul 1, 2025
6e299a3
feat: 멘토링 확인 구현
whqtker Jul 1, 2025
03c79e3
feat: 멘토링 신규 신청 건수 조회 구현
whqtker Jul 1, 2025
e30dd3b
chore: 전반적으로 로직 수정
whqtker Jul 1, 2025
37c7bf6
chore: 불필요한 개행 제거
whqtker Jul 1, 2025
95df8a3
chore: rebase 시 반영되지 않는 로직 추가
whqtker Jul 2, 2025
11d1b32
chore: 멘토 관련 로직에 권한 부여
whqtker Jul 2, 2025
77896f1
test: 멘토 관련 픽스처 생성
whqtker Jul 2, 2025
e049bbb
test: 멘토링 조회 서비스 테스트 구현
whqtker Jul 2, 2025
564135a
fix: 이미 멘토인 사용자 검증 로직 수정
whqtker Jul 2, 2025
c0c96b2
chore: 멘토가 자기 자신에 대해 멘토링을 신청하는 경우 검증 삭제
whqtker Jul 2, 2025
c4de140
chore: 미사용 테스트 메서드 삭제
whqtker Jul 2, 2025
21198fd
test: 멘토링 CUD 테스트 구현
whqtker Jul 2, 2025
b8c9e9c
refactor: List 자체를 Response로 리턴하도록 수정
whqtker Jul 2, 2025
52f21d0
refactor: Long -> long으로 변경
whqtker Jul 2, 2025
a897b0e
refactor: 개행 추가
whqtker Jul 2, 2025
b4a2cc5
refactor: 처음 개행 추가, Long -> long
whqtker Jul 2, 2025
36723f7
refactor: 메서드 간 개행 추가
whqtker Jul 2, 2025
91a1784
chore: 불필요한 테스트 코드 제거
whqtker Jul 2, 2025
f88a75d
refactor: 테스트 코드 리팩터링
whqtker Jul 2, 2025
045bd50
refactor: 컨벤션에 맞게 코드 수정
whqtker Jul 2, 2025
ceac93e
refactor: 비즈니스 로직 흐름에 맞게 순서 변경
whqtker Jul 2, 2025
d7e81e4
chore: 코드 컨벤션 따르도록 수정
whqtker Jul 4, 2025
6783242
refactor: 멘토 여부 검증을 서비스 말고 컨트롤러에서 처리하도록 수정
whqtker Jul 4, 2025
020f1cb
chore: 컨벤션 관련 수정
whqtker Jul 4, 2025
adc7121
refactor: API 명세에 맞게 응답 수정
whqtker Jul 4, 2025
deb9ff2
chore: 오타 수정
whqtker Jul 4, 2025
04a3288
refactor: 빌더 패턴 제거
whqtker Jul 4, 2025
a8be7a6
chore: validateMentoringOwnership 메서드 바디 수정
whqtker Jul 6, 2025
10f44bc
chore: 코딩 컨벤션에 맞도록 수정
whqtker Jul 6, 2025
d059e93
chore: 코딩 컨벤션에 맞도록 수정
whqtker Jul 7, 2025
0cf6c20
chore: 코딩 컨벤션에 맞도록 수정
whqtker Jul 7, 2025
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 @@ -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 토큰을 처리할 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MentoringApplyResponse> 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<MentoringListResponse> 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<MentoringConfirmResponse> 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<MentoringCheckResponse> 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<MentoringCountResponse> getUncheckedMentoringsCount(
@AuthorizedUser SiteUser siteUser
) {
MentoringCountResponse response = mentoringQueryService.getNewMentoringsCount(siteUser.getId());
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ public class Mentor {

@OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Channel> channels = new ArrayList<>();

public void increaseMenteeCount() {
this.menteeCount++;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.solidconnection.mentor.dto;

import jakarta.validation.constraints.NotNull;

public record MentoringApplyRequest(
@NotNull(message = "멘토 id를 입력해주세요.")
Long mentorId
) {
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.solidconnection.mentor.dto;

import java.util.List;

public record MentoringListResponse(
List<MentoringResponse> requests
) {
public static MentoringListResponse from(List<MentoringResponse> requests) {
return new MentoringListResponse(requests);
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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<Mentor, Long> {

Optional<Mentor> findBySiteUserId(long siteUserId);

boolean existsBySiteUserId(long siteUserId);
}
Original file line number Diff line number Diff line change
@@ -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<Mentoring, Long> {

List<Mentoring> findAllByMentorId(long mentorId);

int countByMentorIdAndCheckedAtIsNull(long mentorId);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Comment on lines +49 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 도메인 생성자에서 검증하기로 하지 않았나요?
#367

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

논의되기 전에 작성한 내용이라 남아 있는 것 같습니다 ㅠㅠ 음 .. 그냥 이 PR에서 적용시킬까요 ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 곳도 고쳐야하니 새 pr에서 해도 좋은 거 같아요 전!


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);
}
}
}
Loading
Loading