-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 지원서가 APPROVED 인 유저의 멘토 생성 기능 추가 #562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
74108c0
ac9f657
653578b
64780bf
38b3ad3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -53,6 +53,20 @@ public class Mentor extends BaseEntity { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private List<Channel> channels = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Mentor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String introduction, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String passTip, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long siteUserId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Long universityId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long termId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.introduction = introduction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.passTip = passTip; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.siteUserId = siteUserId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.universityId = universityId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.termId = termId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void increaseMenteeCount() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.menteeCount++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -82,4 +96,11 @@ public void updateChannels(List<Channel> channels) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void createChannels(List<Channel> channels) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for(Channel channel : channels) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| channel.updateMentor(this); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.channels.add(channel); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+100
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major
새로운 잠재적 이슈: Null 체크를 추가하여 안전성을 높이는 것을 권장합니다: public void createChannels(List<Channel> channels) {
+ if (channels == null) {
+ return; // 또는 throw new IllegalArgumentException("channels must not be null");
+ }
for(Channel channel : channels) {
channel.updateMentor(this);
this.channels.add(channel);
}
}또는 더 간결하게: public void createChannels(List<Channel> channels) {
+ if (channels == null || channels.isEmpty()) {
+ return;
+ }
for(Channel channel : channels) {
channel.updateMentor(this);
this.channels.add(channel);
}
}📝 Committable suggestion
Suggested change
Suggested change
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프론트에서 무조건 리스트를 보내준다 가정 해도 null 체크를 하는것이 좋아? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sukangpunch, 좋은 질문입니다. 프론트엔드에서 무조건 리스트를 보낸다고 해도 null 체크를 하는 것이 더 안전합니다. Null 체크를 권장하는 이유:
대안: Null 체크가 과하다고 생각되신다면, 서비스 레이어에서 검증하거나 public void createChannels(@NonNull List<Channel> channels) {
for(Channel channel : channels) {
channel.updateMentor(this);
this.channels.add(channel);
}
}다만 도메인 엔티티 레벨에서 간단한 null 체크를 추가하는 것이 가장 안전하고 명시적입니다. 최종 결정은 팀의 코딩 컨벤션에 따라 하시면 됩니다! 🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,23 @@ | |
| import com.example.solidconnection.mentor.domain.UniversitySelectType; | ||
| import com.example.solidconnection.siteuser.domain.ExchangeStatus; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| public record MentorApplicationRequest( | ||
| @NotNull(message = "교환 상태를 입력해주세요.") | ||
| @JsonProperty("preparationStatus") | ||
| ExchangeStatus exchangeStatus, | ||
|
|
||
| @NotNull(message = "대학교 선택 유형을 입력해주세요.") | ||
| UniversitySelectType universitySelectType, | ||
|
|
||
| @NotNull(message = "국가를 입력해주세요") | ||
| String country, | ||
| Long universityId | ||
|
|
||
| Long universityId, | ||
|
|
||
| @NotBlank(message = "학기를 입력해주세요.") | ||
| String term | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+10
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainValidation 어노테이션이 적절히 추가되었습니다. universityId 검증 여부를 확인해주세요. 변경사항을 정리하면 다음과 같습니다:
다만, 🏁 Script executed: #!/bin/bash
# universityId가 필수 필드인지 확인하기 위해 서비스 로직과 도메인 모델을 검토
# MentorApplication 도메인에서 universityId 사용 패턴 확인
ast-grep --pattern $'class MentorApplication {
$$$
universityId
$$$
}'
# MentorApplicationService에서 universityId 검증 로직 확인
rg -n -A5 -B5 "universityId" --type=java -g '**/MentorApplicationService.java'
# universitySelectType enum 정의 확인 (UNDECIDED 같은 옵션이 있는지)
fd -e java UniversitySelectType.java --exec cat {}Length of output: 1688 universityId 필드에 @NotNull 어노테이션을 추가해주세요. 검증 결과를 정리하면 다음과 같습니다:
🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai MentorApplication 에서 universityId 는 null 일수 있어 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sukangpunch, 명확한 설명 감사합니다!
현재 구현이 올바르며, ( •_• ) ✏️ Learnings added
🧠 Learnings used |
||
| ) { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.example.solidconnection.mentor.dto; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import java.util.List; | ||
|
|
||
| public record MentorMyPageCreateRequest( | ||
| @NotBlank(message = "자기소개를 입력해주세요.") | ||
| String introduction, | ||
|
|
||
| @NotBlank(message = "합격 레시피를 입력해주세요.") | ||
| String passTip, | ||
|
|
||
| @NotNull | ||
| @Valid | ||
| List<ChannelRequest> channels | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| package com.example.solidconnection.mentor.service; | ||
|
|
||
| import static com.example.solidconnection.common.exception.ErrorCode.CHANNEL_REGISTRATION_LIMIT_EXCEEDED; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_ALREADY_EXISTS; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.TERM_NOT_FOUND; | ||
| import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND; | ||
|
|
@@ -9,9 +11,13 @@ | |
| import com.example.solidconnection.common.exception.CustomException; | ||
| import com.example.solidconnection.mentor.domain.Channel; | ||
| import com.example.solidconnection.mentor.domain.Mentor; | ||
| import com.example.solidconnection.mentor.domain.MentorApplication; | ||
| import com.example.solidconnection.mentor.domain.MentorApplicationStatus; | ||
| import com.example.solidconnection.mentor.dto.ChannelRequest; | ||
| import com.example.solidconnection.mentor.dto.MentorMyPageCreateRequest; | ||
| import com.example.solidconnection.mentor.dto.MentorMyPageResponse; | ||
| import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest; | ||
| import com.example.solidconnection.mentor.repository.MentorApplicationRepository; | ||
| import com.example.solidconnection.mentor.repository.MentorRepository; | ||
| import com.example.solidconnection.siteuser.domain.SiteUser; | ||
| import com.example.solidconnection.siteuser.repository.SiteUserRepository; | ||
|
|
@@ -36,6 +42,7 @@ public class MentorMyPageService { | |
| private final SiteUserRepository siteUserRepository; | ||
| private final UniversityRepository universityRepository; | ||
| private final TermRepository termRepository; | ||
| private final MentorApplicationRepository mentorApplicationRepository; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public MentorMyPageResponse getMentorMyPage(long siteUserId) { | ||
|
|
@@ -61,18 +68,53 @@ public void updateMentorMyPage(long siteUserId, MentorMyPageUpdateRequest reques | |
| updateChannel(request.channels(), mentor); | ||
| } | ||
|
|
||
| private void updateChannel(List<ChannelRequest> channelRequests, Mentor mentor) { | ||
| List<Channel> newChannels = buildChannels(channelRequests); | ||
| mentor.updateChannels(newChannels); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그 방식도 괜찮은 듯 합니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분은 정말 개인의 성향에 따라 느끼는 바가 다른 거 같아요 코멘트 의도가 단순 궁금해서였고, 구현하신 근거가 분명하셔서 굳이 바꿀 필요는 없을 거 같습니다 ! |
||
|
|
||
| @Transactional | ||
| public void createMentorMyPage(long siteUserId, MentorMyPageCreateRequest request) { | ||
| validateUserCanCreateMentor(siteUserId); | ||
| validateChannelRegistrationLimit(request.channels()); | ||
| MentorApplication mentorApplication = mentorApplicationRepository.findBySiteUserIdAndMentorApplicationStatus(siteUserId, MentorApplicationStatus.APPROVED) | ||
| .orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND)); | ||
|
|
||
| Mentor mentor = new Mentor( | ||
| request.introduction(), | ||
| request.passTip(), | ||
| siteUserId, | ||
| mentorApplication.getUniversityId(), | ||
| mentorApplication.getTermId() | ||
| ); | ||
|
|
||
| createChannels(request.channels(), mentor); | ||
| mentorRepository.save(mentor); | ||
| } | ||
|
Comment on lines
+76
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainAPPROVED 지원서에서 universityId가 null인 경우에 대한 처리 검토가 필요해 보입니다.
Based on learnings Also applies to: 95-99 🏁 Script executed: #!/bin/bash
# Mentor 및 MentorApplication에서 universityId의 null 가능성과 사용 패턴을 빠르게 훑어보기 위한 스크립트입니다.
# 1) Mentor.universityId 필드 선언과 생성자/팩토리에서의 사용 위치 확인
rg -n "universityId" src/main/java/com/example/solidconnection/mentor -C3
# 2) MentorApplication.universityId가 어디에서 세팅/검증되는지 확인
rg -n "universityId" src/main/java/com/example/solidconnection/mentor -C3
# 3) MentoryMyPageService에서 universityId 관련 사용 위치 재확인
rg -n "getUniversityId" src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java -C3Length of output: 23853 Mentor.universityId 필드 타입 불일치로 인한 null 언박싱 위험 코드 검증 결과, 다음 문제들을 발견했습니다:
해결 권장사항 (선택 사항)
현재
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai 다음 pr에 admin 멘토 지원서 approve/reject 하는 기능을 추가 할건데, 거기서 대학 증명 자료는 있지만, 현재 서버가 가지고 있지 않은 대학에 대해서는(universityId가 null 인경우) 대학 데이터를 만들고 지원서에 넣은 다음 approve 하려고 해 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sukangpunch 아, 다음 PR에서 admin이 승인 시점에 대학 데이터를 생성하고 universityId를 채운 다음 APPROVED로 전환하는 구조군요! 그렇다면 제가 우려했던 부분이 비즈니스 로직 수준에서 해결되는 것으로 이해했습니다. 설계 방향에 대한 정리:
이 접근은 합리적이고 관심사의 분리(separation of concerns)가 잘 되어 있다고 생각합니다! 참고로 추가 고려사항: 만약 혹시라도 admin 플로우에서 예외가 발생하거나 데이터 정합성 문제가 생길 경우를 대비하여, MentorApplication mentorApplication = mentorApplicationRepository
.findBySiteUserIdAndMentorApplicationStatus(siteUserId, MentorApplicationStatus.APPROVED)
.orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND));
// 방어적 검증 (선택 사항)
if (mentorApplication.getUniversityId() == null) {
throw new CustomException(MENTOR_APPLICATION_INVALID); // 또는 적절한 에러 코드
}하지만 이는 선택 사항이며, admin 승인 플로우에서 충분히 보장된다면 불필요한 코드일 수 있습니다. 팀의 방어적 프로그래밍 정책에 따라 결정하시면 될 것 같습니다! ✏️ Learnings added
🧠 Learnings used |
||
|
|
||
| private void validateUserCanCreateMentor(long siteUserId) { | ||
| if (mentorRepository.existsBySiteUserId(siteUserId)) { | ||
| throw new CustomException(MENTOR_ALREADY_EXISTS); | ||
| } | ||
| } | ||
|
|
||
| private void validateChannelRegistrationLimit(List<ChannelRequest> channelRequests) { | ||
| if (channelRequests.size() > CHANNEL_REGISTRATION_LIMIT) { | ||
| throw new CustomException(CHANNEL_REGISTRATION_LIMIT_EXCEEDED); | ||
| } | ||
| } | ||
|
|
||
| private void updateChannel(List<ChannelRequest> channelRequests, Mentor mentor) { | ||
| private void createChannels(List<ChannelRequest> channelRequests, Mentor mentor) { | ||
| List<Channel> newChannels = buildChannels(channelRequests); | ||
| mentor.createChannels(newChannels); | ||
| } | ||
|
|
||
| private List<Channel> buildChannels(List<ChannelRequest> channelRequests) { | ||
| int sequence = CHANNEL_SEQUENCE_START_NUMBER; | ||
| List<Channel> newChannels = new ArrayList<>(); | ||
| for (ChannelRequest request : channelRequests) { | ||
| newChannels.add(new Channel(sequence++, request.type(), request.url())); | ||
| } | ||
| mentor.updateChannels(newChannels); | ||
| return newChannels; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| ALTER TABLE mentor_application | ||
| ADD COLUMN term_id BIGINT NOT NULL; | ||
|
|
||
| ALTER TABLE mentor_application | ||
| ADD CONSTRAINT fk_mentor_application_term_id | ||
| FOREIGN KEY (term_id) REFERENCES term(id); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
역할 기반 접근 제어에 잠재적 문제가 있습니다.
이 엔드포인트는
@RequireRoleAccess(roles = Role.MENTOR)를 사용하고 있는데, 이는 모순을 야기합니다:문제:
MENTOR역할을 가진 사용자만 접근할 수 있습니다.고려사항:
Role.USER)로 변경하고, 서비스 레이어에서 APPROVED 지원서 존재 여부를 확인합니다.createMentorMyPage가 아니라completeMentorMyPage같은 이름이 더 적절합니다.현재 비즈니스 플로우를 명확히 하고, 그에 맞게 접근 제어를 조정해주세요.
현재 플로우를 확인하기 위해 다음을 검토해주세요:
🏁 Script executed:
Length of output: 11628
🏁 Script executed:
Length of output: 34228
🏁 Script executed:
Length of output: 6720
@RequireRoleAccess데코레이터가 서비스 로직과 모순됩니다 — 엔드포인트에 도달 불가능한 상태입니다.코드베이스를 검증한 결과, 핵심 문제가 확인되었습니다.
1. 역할 할당 코드가 존재하지 않음
MENTOR으로 업데이트하는 로직이 없습니다.MentorMyPageService.createMentorMyPage()는 Mentor 엔티티만 생성하며 역할을 변경하지 않습니다.MentorApplicationService에서도 신청 승인 시 역할 할당 코드가 없습니다.2. 해결 불가능한 접근 제어 체크
필수 조치:
멘토 신청 승인 시 역할 업데이트
MentorApplicationService또는 별도의 admin 승인 서비스에서 신청 승인 시 사용자 역할을Role.MENTOR으로 변경하세요.컨트롤러 가드 재검토
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 기능은, 아직 admin에서 멘토 지원서 approve 기능을 추가하지 않았기 때문이야, 다음 pr에 해당 내용을 추가할 예정
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sukangpunch, 명확한 설명 감사합니다! 단계적 구현 방식을 이해했습니다.
현재 상황 확인:
Role.MENTOR할당 로직 추가다음 PR에서 확인해주세요:
Role.MENTOR로 업데이트하는 로직이 포함되어야 합니다.현재 설계는 최종 플로우를 고려한 것으로 보이므로, 다음 PR에서 역할 할당 로직만 추가되면 완벽하게 동작할 것 같습니다! 👍