Uh oh!
There was an error while loading. Please reload this page.
- 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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
74108c0ac9f657653578b64780bf38b3ad3File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| publicvoidcreateChannels(List<Channel> channels) { | |
| for(Channelchannel : channels) { | |
| channel.updateMentor(this); | |
| this.channels.add(channel); | |
| } | |
| } | |
| publicvoidcreateChannels(List<Channel> channels) { | |
| if (channels == null) { | |
| return; | |
| } | |
| for(Channelchannel : channels) { | |
| channel.updateMentor(this); | |
| this.channels.add(channel); | |
| } | |
| } |
| publicvoidcreateChannels(List<Channel> channels) { | |
| for(Channelchannel : channels) { | |
| channel.updateMentor(this); | |
| this.channels.add(channel); | |
| } | |
| } | |
| publicvoidcreateChannels(List<Channel> channels) { | |
| if (channels == null || channels.isEmpty()) { | |
| return; | |
| } | |
| for(Channelchannel : channels) { | |
| channel.updateMentor(this); | |
| this.channels.add(channel); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/mentor/domain/Mentor.java around
lines 100 to 105, the new createChannels(List<Channel> channels) method can
throw a NullPointerException if the channels parameter is null; add a
null-safety guard such as returning immediately when channels is null or
replacing it with Collections.emptyList(), then iterate over the safe list and
for each channel call channel.updateMentor(this) and add to this.channels;
ensure the method handles an empty list gracefully and does not modify state
when input is null.
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.
프론트에서 무조건 리스트를 보내준다 가정 해도 null 체크를 하는것이 좋아?
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, 좋은 질문입니다. 프론트엔드에서 무조건 리스트를 보낸다고 해도 null 체크를 하는 것이 더 안전합니다.
Null 체크를 권장하는 이유:
다양한 호출 경로: 이 메서드는 컨트롤러를 통한 API 호출뿐만 아니라, 백엔드 내부의 다른 서비스나 테스트 코드에서도 호출될 수 있습니다.
미래의 변경사항: 나중에 코드가 리팩토링되거나 새로운 기능이 추가될 때, 다른 경로에서 이 메서드를 호출할 수 있습니다.
방어적 프로그래밍: 도메인 엔티티의 public 메서드는 잘못된 입력으로부터 자신을 보호하는 것이 좋습니다.
대안:
Null 체크가 과하다고 생각되신다면, 서비스 레이어에서 검증하거나 @NonNull 어노테이션을 사용하는 방법도 있습니다:
publicvoidcreateChannels(@NonNullList<Channel> channels) {
for(Channelchannel : channels) {
channel.updateMentor(this);
this.channels.add(channel);
}
}다만 도메인 엔티티 레벨에서 간단한 null 체크를 추가하는 것이 가장 안전하고 명시적입니다. 최종 결정은 팀의 코딩 컨벤션에 따라 하시면 됩니다!
🧠 Learnings used
Learnt from: nayonsoso
Repo: solid-connection/solid-connect-server PR: 426
File: src/main/java/com/example/solidconnection/mentor/domain/Mentor.java:80-80
Timestamp: 2025-08-02T21:44:08.950Z
Learning: Mentor.updateChannels() 메서드에서 채널 삭제 시 originalChannelSize를 메서드 시작 시점에 고정하여 사용하므로, 반복문 내에서 리스트 크기가 변경되어도 올바른 수의 채널이 삭제된다. i < originalChannelSize 조건으로 넘치는 채널을 정확히 제거할 수 있다.
Learnt from: nayonsoso
Repo: solid-connection/solid-connect-server PR: 375
File: src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java:47-53
Timestamp: 2025-07-05T17:54:42.475Z
Learning: MentorMyPageService에서 PUT 메서드 구현 시 전체 채널을 새로 생성하여 교체하는 방식을 사용하는 것이 PUT의 의미론적 특성과 일치하며, 트랜잭션 로킹 관점에서도 합리적인 접근이다.
| 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.
Uh oh!There was an error while loading. Please reload this page.
Comment on lines
+10
to
+23
| ||
| ) { | ||
| } | ||
| 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.
ContributorAuthor 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
| ||
| 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.
Uh oh!
There was an error while loading. Please reload this page.
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에서 역할 할당 로직만 추가되면 완벽하게 동작할 것 같습니다! 👍