-
Notifications
You must be signed in to change notification settings - Fork 3
[Fix][jjaeroong]: 팟 상세조회 시, 채소이름에서 역할명으로 조회되도록 변경 및 회원가입 시 역할 다중선택 가능 #452
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
Conversation
[Fix][jjaeroong]: 팟 상세조회 시, 채소이름에서 역할명으로 조회되도록 변경 및 회원가입 시 역할 다중선택 가능
|
Caution Review failedThe pull request is closed. Walkthrough역할 모델을 단일(Role)에서 다중(List/List)로 전환하고, enum 상수 PLANNING을 PLAN으로 변경했습니다. 이에 따라 엔티티·DTO·컨버터·서비스·리포지토리의 필드·메서드 시그니처·쿼리 및 문서 문자열이 업데이트되었고, 일부 검색/캐시 변환 로직이 제거 또는 조정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant PC as PotCommandService
participant PR as PotRepository
participant PMC as PotMemberConverter
participant PMR as PotMemberRepository
U->>PC: createPot(request)
PC->>PR: save(pot)
PR-->>PC: savedPot
PC->>PMC: toCreatorEntity(user, savedPot, request.potRole)
PMC-->>PC: PotMember(owner=true)
PC->>PMR: save(creator PotMember)
PMR-->>PC: savedCreatorMember
PC-->>U: response (pot + creator member)
sequenceDiagram
autonumber
actor U as User
participant TQ as TaskQueryService
participant TBR as TaskboardRepository
participant TR as TaskRepository
participant PMR as PotMemberRepository
participant TBC as TaskBoardConverter
U->>TQ: getTasksFromDate(potId, date)
TQ->>TBR: findByPotPotIdAndDeadLine(potId, date)
TBR-->>TQ: taskboards[]
TQ->>TR: findByTaskboardIn(taskboards)
TR-->>TQ: tasks[]
TQ->>PMR: findCreatorRolesByPotAndUserIds(potId, creatorIds)
PMR-->>TQ: [(userId, roleName)]*
loop for each taskboard
TQ->>TBC: toDto(taskboard, participants, creatorRole)
TBC-->>TQ: TaskPreviewDTO
end
TQ-->>U: TaskPreviewDTO[]
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (3)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
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.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/main/java/stackpot/stackpot/user/entity/enums/Role.java (1)
22-36: null 입력 시 NPE 가능성 — fromString() 방어 로직 추가 권장
roleName.toUpperCase()에서 null 입력 시 NPE가 발생합니다. 아래처럼 null/blank 방어와 trim을 추가해 주세요.- public static Role fromString(String roleName) { - try { - return Role.valueOf(roleName.toUpperCase()); - } catch (IllegalArgumentException e) { - return UNKNOWN; - } - } + public static Role fromString(String roleName) { + if (roleName == null || roleName.isBlank()) { + return UNKNOWN; + } + try { + return Role.valueOf(roleName.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + }src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (1)
31-52: 탈퇴/NULL 사용자 처리 분기와 불일치:userId에서 NPE 위험위쪽 분기에서
entity.getUser()가null인 케이스를 고려하지만, 아래 빌더에서entity.getUser().getId()를 무조건 호출합니다. NPE 발생 가능.- .userId(entity.getUser().getId()) + .userId(entity.getUser() != null ? entity.getUser().getId() : null)src/main/java/stackpot/stackpot/task/service/TaskCommandServiceImpl.java (2)
87-94: modifyTask 참가자 검증 누락: 다른 팟의 멤버가 할당될 수 있음
findAllById(requestedParticipantIds)는 pot 범위를 제한하지 않아 교차-팟 데이터 오염/권한 이슈 위험이 있습니다.createTask와 동일하게 pot 기준으로 필터링하세요.- List<Long> requestedParticipantIds = request.getParticipants() != null ? request.getParticipants() : List.of(); - List<PotMember> participants = potMemberRepository.findAllById(requestedParticipantIds); + List<Long> requestedParticipantIds = request.getParticipants() != null ? request.getParticipants() : List.of(); + List<PotMember> validParticipants = potMemberRepository.findByPotId(potId); + List<PotMember> participants = validParticipants.stream() + .filter(pm -> requestedParticipantIds.contains(pm.getPotMemberId())) + .collect(Collectors.toList());
39-41: createTask 트랜잭션 경계 추가 필요Taskboard 저장 후 Task 일괄 저장까지 원자성을 보장하지 않습니다. 부분 커밋 방지를 위해 트랜잭션 지정 권장.
@Override - public MyPotTaskResponseDto createTask(Long potId, MyPotTaskRequestDto.create request) { + @Transactional + public MyPotTaskResponseDto createTask(Long potId, MyPotTaskRequestDto.create request) {src/main/java/stackpot/stackpot/pot/converter/PotConverter.java (1)
82-99: userPotRole null 체크 및 기본값 할당 필요- .userPotRole(RoleNameMapper.getKoreanRoleName(userPotRole.name())) + .userPotRole(userPotRole != null + ? RoleNameMapper.getKoreanRoleName(userPotRole.name()) + : "알 수 없음")또한 CompletedPotResponseDto.memberCounts의 키가 영어 enum→한글명으로 변경된 영향 범위(응답 파서·정렬·표시 로직) 점검 바랍니다.
src/main/java/stackpot/stackpot/task/converter/TaskBoardConverter.java (1)
66-69: 참여자 닉네임도 역할명으로 일관화 필요참여자 닉네임 구성에서도 채소명이 아니라 역할명으로 표기되어야 목적에 부합합니다.
- .nickName(participant.getUser().getNickname() + " " + RoleNameMapper.mapRoleName(participant.getRoleName().toString())) + .nickName(participant.getUser().getNickname() + " " + RoleNameMapper.getKoreanRoleName(participant.getRoleName().name()))
🧹 Nitpick comments (38)
src/main/java/stackpot/stackpot/task/repository/TaskboardRepository.java (1)
45-45: 불필요한 공백 라인 제거 권장마지막 닫는 중괄호 앞의 공백 라인은 팀 포매터(spotless/google-java-format 등) 규칙과 불일치할 수 있습니다. 불필요한 diff를 줄이기 위해 제거를 권장합니다.
- }src/main/java/stackpot/stackpot/apiPayload/code/status/ErrorStatus.java (1)
69-69: 에러 메시지의 유효 Role 목록 정렬/표현 통일 제안PLAN 반영은 적절합니다. 가독성을 위해 Role 정의 순서와 동일하게 정렬하는 것을 추천합니다.
- INVALID_ROLE(HttpStatus.BAD_REQUEST, "ROLE4000", "Role 형식이 올바르지 않습니다 (FRONTEND / DESIGN / BACKEND / PLAN)"), + INVALID_ROLE(HttpStatus.BAD_REQUEST, "ROLE4000", "Role 형식이 올바르지 않습니다 (BACKEND / FRONTEND / DESIGN / PLAN)"),src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationCommandServiceImpl.java (1)
87-94: runAsync 공용 풀 사용은 블로킹 IO에 비권장 — 전용 Executor 또는 @async 권장메일 전송은 블로킹 IO 가능성이 높습니다. 공용
ForkJoinPool대신 전용TaskExecutor(@async + ThreadPoolTaskExecutor)를 사용해 풀 고갈을 방지하세요.src/main/java/stackpot/stackpot/user/dto/request/UserUpdateRequestDto.java (1)
18-21: List 요소 단위 유효성 검증으로 전환 권장필드 수준
@ValidRole만으로는 List 요소 각각이 검증되지 않을 수 있습니다. 컨테이너 요소 애너테이션으로 명확히 해주세요. 스웨거 설명도 복수형으로 맞추는 것을 권장합니다.- @ValidRole - @Schema(description = "역할") - private List<Role> roles; + @Schema(description = "역할 목록", example = "[\"PLAN\", \"DEV\"]") + private List<@ValidRole Role> roles;src/main/java/stackpot/stackpot/feed/dto/FeedCacheDto.java (1)
20-20: writerRoles는 기본값을 빈 리스트로 초기화해 null 응답 방지캐시 DTO 특성상 NPE/불안정 직렬화를 피하려면 기본값을 제공하세요.
- private List<String> writerRoles; + @Builder.Default + private List<String> writerRoles = java.util.Collections.emptyList();src/main/java/stackpot/stackpot/task/repository/TaskRepository.java (1)
37-38: ID 기반 조회 메서드 추가 권장
현재TaskQueryServiceImpl.java:124에서findByTaskboardIn(boards)호출 시Taskboard엔티티 컬렉션을 전달하고 있습니다. 영속성 컨텍스트 로딩 부담을 줄이려면 리포지토리에List<Task> findByTaskboard_TaskboardIdIn(Collection<Long> taskboardIds);메서드를 추가하고, 해당 호출부를 ID 목록 기반 조회로 전환하세요.
[src/main/java/stackpot/stackpot/task/service/TaskQueryServiceImpl.java:124]src/main/java/stackpot/stackpot/user/repository/TempUserRepository.java (1)
16-22: fetch join 중복 row 방지 위해 DISTINCT 권장단일 엔티티를 id로 조회하므로 문제될 가능성은 낮지만, 컬렉션 fetch join 시 SQL 레벨에서 중복을 제거하는
distinct를 붙여두면 안전합니다(가독성도 개선).- @Query(""" - select tu + @Query(""" + select distinct tu from TempUser tu left join fetch tu.roles where tu.id = :id """)src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (3)
39-41: 채소명 → 역할명 전환 필요 여부 확인PR 요약에 따르면 “팟 상세조회 시 채소이름이 아닌 역할명으로 조회”가 목표입니다. 응답 표시도 역할명으로 통일하려면
mapRoleName(채소명)대신getKoreanRoleName(역할명)사용이 맞는지 확인 바랍니다.- nicknameWithRole = entity.getUser().getNickname() + " " + RoleNameMapper.mapRoleName(roleName); + nicknameWithRole = entity.getUser().getNickname() + " " + RoleNameMapper.getKoreanRoleName(roleName);Also applies to: 77-79
84-84: owner 하드코딩 제거멤버 DTO에서도 실제
entity.isOwner()를 그대로 노출하면 일관성이 좋아집니다.- .owner(false) + .owner(entity.isOwner())
21-26: 첫 번째 Role 선택 로직 명시화 및 필요 시 null-safe 처리
User.roles는 기본 생성 시new ArrayList<>()로 초기화되어 실제 운영 환경의 JPA 로딩에서는 null이 발생하지 않지만, Lombok@Builder등을 통해 생성될 경우엔 null이 될 수 있습니다. 필요하다면user.getRoles()==null || user.getRoles().isEmpty()와 같이 방어 로직을 추가하고, 단순히 첫 번째 요소가 아닌 비즈니스 우선순위에 기반해 Role을 선택하는 기준을 명확히 구현하세요.src/main/java/stackpot/stackpot/user/dto/response/UserSignUpResponseDto.java (1)
19-21: 응답 roles 표현식 명확화 및 스키마 예시 추가
List<String>이 “역할 코드(BACKEND/PLAN)”인지 “한글명(백엔드/기획)”인지 명시해 주세요. 또한 Swagger 예시를 추가하면 클라이언트 정합성이 좋아집니다.- @Schema(description = "역할") - private List<String> roles; + @Schema(description = "역할 코드 목록", example = "[\"BACKEND\",\"PLAN\"]") + private List<String> roles;역할 한글명으로 응답할 계획이라면 예시를
["백엔드","기획"]으로 바꾸고, 전역적으로 동일한 규칙을 적용해 주세요.src/main/java/stackpot/stackpot/feed/dto/FeedResponseDto.java (1)
41-41: writerRole → writerRoles(List) 전환: 기본값과 빌더 사용성 보완 제안
- 응답 안정성을 위해 null 대신 빈 리스트를 기본값으로 유지하는 편이 좋습니다. 또한 빌더에서 단건 추가가 가능하도록 @Singular 적용을 권장합니다.
아래와 같이 변경을 제안합니다:
- private List<String> writerRoles; + @Builder.Default + @Singular("writerRole") + private List<String> writerRoles = Collections.emptyList();- private List<String> writerRoles; + @Builder.Default + @Singular("writerRole") + private List<String> writerRoles = Collections.emptyList();추가 확인:
- API 스키마가 Role → List으로 변경되므로, 외부/FE 소비자가 필드 타입 변경을 인지했는지 문서와 스웨거 스펙을 업데이트했는지 점검 바랍니다.
Also applies to: 62-62
src/main/java/stackpot/stackpot/pot/repository/PotMemberRepository.java (2)
100-107: 메서드명(creator)과 JPQL 조건 불일치 가능성 + 타입 안정성 개선
- creator 전용 조회로 보이지만(owner=True 조건이 없음) 현재 JPQL은 임의 사용자에 대한 역할을 모두 반환합니다. 의도대로라면 owner 필터를 추가하거나 메서드명을 findRolesByPotAndUserIds로 변경하세요.
where pm.pot.potId = :potId and pm.user.id in :userIds + and pm.owner = true
- List<Object[]> 반환은 타입 안전하지 않습니다. 인터페이스 기반 Projection을 사용하세요.
- List<Object[]> findCreatorRolesByPotAndUserIds(@Param("potId") Long potId, - @Param("userIds") Collection<Long> userIds); + List<UserIdRoleProjection> findCreatorRolesByPotAndUserIds(@Param("potId") Long potId, + @Param("userIds") Collection<Long> userIds);프로젝션 인터페이스(새 파일 또는 동일 패키지)에 추가:
package stackpot.stackpot.pot.repository; import stackpot.stackpot.user.entity.enums.Role; public interface UserIdRoleProjection { Long getUserId(); Role getRole(); }
- IN (:userIds) 에 빈 컬렉션이 전달되면 JPQL 구문 오류가 납니다. 상위 서비스 단에서 빈 컬렉션 시 조기 반환하도록 가드 로직을 추가하세요.
15-15: 컬렉션 타입 일관성동일 리포지토리의 유사 메서드들은 대부분 List를 사용합니다. 파라미터 타입을 List로 맞추면 일관성이 좋아지고 호출부 변환 비용을 줄일 수 있습니다.
src/main/java/stackpot/stackpot/pot/service/potMember/PotMemberCommandServiceImpl.java (2)
89-93: Stream.toList()는 불변 리스트 반환 — 변이 필요 시 Collectors.toList() 사용chatRoomInfoCommandService.createChatRoomInfo 내부에서 리스트 변이가 있다면 실패합니다. 안전하게 가려면 가변 리스트로 만들어 전달하세요.
List<Long> potMemberIds = Stream.concat( Stream.of(creatorPM.getPotMemberId()), savedMembers.stream().map(PotMember::getPotMemberId) - ).toList(); + ).collect(Collectors.toList());
96-99: 응답에 생성자까지 포함하도록 변경 — API 호환성 확인 필요기존에 “승인된 신규 멤버만 반환”하던 계약이었다면 응답 크기/내용이 달라져 FE가 오동작할 수 있습니다. 계약 변경 여부를 명시하고 E2E 테스트를 갱신하세요. 또한 위와 같은 이유로 불변 리스트 대신 가변 리스트 사용을 권장합니다.
- return Stream.concat(Stream.of(creatorPM), savedMembers.stream()) - .map(potMemberConverter::toDto) - .toList(); + return Stream.concat(Stream.of(creatorPM), savedMembers.stream()) + .map(potMemberConverter::toDto) + .collect(Collectors.toList());src/main/java/stackpot/stackpot/user/converter/UserConverter.java (1)
34-35: roles 정렬로 응답 안정화(비결정적 순서 방지)JPA
@ElementCollection은 순서를 보장하지 않습니다. 클라이언트 캐시/스냅샷 테스트 안정화를 위해 정렬 적용을 권장합니다.적용 예시(이 범위 내 변경):
- .roles(user.getRoleNames()) + .roles(user.getRoleNames().stream().sorted().collect(java.util.stream.Collectors.toList()))파일 상단에 다음 import가 없다면 추가가 필요합니다:
import java.util.stream.Collectors;src/main/java/stackpot/stackpot/user/dto/response/UserResponseDto.java (3)
21-22: Swagger 예시 추가로 문서 가독성 향상역할이 다중값임을 명시해 주세요.
- private List<String> roles; // 역할 + @Schema(description = "역할(영문 상수명 배열)", example = "[\"PLAN\",\"DESIGN\"]") + private List<String> roles; // 역할
40-42: login 응답 roles에도 예시 반영문서 일관성 유지.
- @Schema(description = "역할") - private List<String> roles; + @Schema(description = "역할(영문 상수명 배열)", example = "[\"PLAN\",\"DESIGN\"]") + private List<String> roles;
54-55: UserInfoDto.roles에도 예시 반영동일 포맷으로 문서화 권장.
- private List<String> roles; + @Schema(description = "역할(영문 상수명 배열)", example = "[\"PLAN\",\"DESIGN\"]") + private List<String> roles;src/main/java/stackpot/stackpot/task/service/TaskCommandServiceImpl.java (2)
52-56: contains O(n) 비용 최적화(Set 사용)요청 ID가 많을 때 필터링이 O(n^2)입니다. Set으로 변경.
- List<Long> requestedParticipantIds = request.getParticipants() != null ? request.getParticipants() : List.of(); + java.util.Set<Long> requestedParticipantIds = request.getParticipants() != null ? new java.util.HashSet<>(request.getParticipants()) : java.util.Set.of(); List<PotMember> validParticipants = potMemberRepository.findByPotId(potId); List<PotMember> participants = validParticipants.stream() - .filter(potMember -> requestedParticipantIds.contains(potMember.getPotMemberId())) + .filter(potMember -> requestedParticipantIds.contains(potMember.getPotMemberId())) .collect(Collectors.toList());modifyTask(라인 87-94)도 동일 패턴으로 Set 사용 권장.
65-66: 사소한 포맷팅
,뒤 공백 누락.- MyPotTaskResponseDto response = taskboardConverter.toDTO(taskboard, participants,creatorRole); + MyPotTaskResponseDto response = taskboardConverter.toDTO(taskboard, participants, creatorRole);src/main/java/stackpot/stackpot/pot/converter/PotDetailConverter.java (2)
57-58: 중복 필드 설정 제거: potRecruitmentDeadline 두 번 설정됨Builder에 동일 필드를 두 번 세팅합니다(라인 48, 57). 중복 제거 바랍니다.
- .potRecruitmentDeadline(pot.getPotRecruitmentDeadline())
33-37: toMap 중복 키 안전성 확인
recruitmentRole이 중복될 경우Collectors.toMap이 예외를 던집니다. 데이터 무결성이 보장되지 않으면 머지 함수를 지정하세요..collect(Collectors.toMap( rd -> rd.getRecruitmentRole().name(), PotRecruitmentDetails::getRecruitmentCount, Integer::sum ))src/main/java/stackpot/stackpot/user/entity/TempUser.java (2)
34-39: 역할 순서 보존이 필요하면 @OrderColumn 추가 고려클라이언트에 표시 순서 요구가 있다면 순서를 보장하세요(마이그레이션 필요).
@ElementCollection(targetClass = Role.class) @Enumerated(EnumType.STRING) @CollectionTable(name = "temp_user_roles", joinColumns = @JoinColumn(name = "temp_user_id")) - @Column(name = "role") + @Column(name = "role") + @OrderColumn(name = "position") private List<Role> roles = new ArrayList<>();
34-39: 조인 테이블 인덱스/성능 제안
temp_user_roles(temp_user_id)인덱스를 추가하면 역할 조회 성능이 향상됩니다(특히 가입/프로필 조회 경로).예시 DDL:
CREATE INDEX IF NOT EXISTS idx_temp_user_roles_user ON temp_user_roles(temp_user_id);src/main/java/stackpot/stackpot/pot/converter/PotConverter.java (1)
67-75: userRoles null-세이프 처리 + 닉네임 생성 유틸 재사용 제안
- user.getRoleNames()가 null일 경우 직렬화/클라이언트 단 파싱 이슈가 날 수 있습니다. 빈 리스트로 방어 코드를 권장합니다.
- 닉네임 뒤 "새싹" 접미사는 RoleNameMapper.getWriterNickname(user) 유틸로 일관 처리 가능해 보입니다.
- 메서드명 오타(toPrviewDto → toPreviewDto)도 추후 정리 권장합니다.
적용 예시:
return PotPreviewResponseDto.builder() .userId(user.getId()) - .userRoles(user.getRoleNames()) - .userNickname(user.getNickname() + " 새싹") + .userRoles(user.getRoleNames() == null ? List.of() : user.getRoleNames()) + .userNickname(RoleNameMapper.getWriterNickname(user)) .potId(pot.getPotId())src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java (1)
100-106: 작성자 역할명 표기 일관화 제안 (한글/미정값)
UNKNOWN대신알 수 없음으로 통일하고, 역할명 매핑에RoleNameMapper.getKoreanRoleName(creatorRole.name())활용을 권장합니다.
PotDetailConverter 호출부(7→8 인자) 반영도 모두 확인되었습니다.- String creatorRoleName = creatorRole != null ? creatorRole.name() : "UNKNOWN"; + String creatorRoleName = creatorRole != null + ? RoleNameMapper.getKoreanRoleName(creatorRole.name()) + : "알 수 없음";src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationQueryServiceImpl.java (2)
17-17: 와일드카드 import 지양가독성/충돌 방지/정적 분석을 위해 명시적 import를 권장합니다.
-import stackpot.stackpot.pot.repository.*; +import stackpot.stackpot.pot.repository.PotApplicationRepository; +import stackpot.stackpot.pot.repository.PotCommentRepository; +import stackpot.stackpot.pot.repository.PotMemberRepository; +import stackpot.stackpot.pot.repository.PotRepository; +import stackpot.stackpot.pot.repository.PotSaveRepository;
74-80: 작성자 역할명 표기 일관화(한글/미정 값) 제안
creatorRole.name()과 "UNKNOWN"은 노출 문자열로 부적절합니다. 한글 매핑과 미정 시 기본값을 통일하세요.- Role creatorRole = potMemberRepository - .findRoleByUserId(pot.getPotId(), pot.getUser().getId()) - .orElse(null); - String creatorRoleName = creatorRole != null ? creatorRole.name() : "UNKNOWN"; + Role creatorRole = potMemberRepository + .findRoleByUserId(pot.getPotId(), pot.getUser().getId()) + .orElse(null); + String creatorRoleName = RoleNameMapper.getKoreanRoleName( + creatorRole != null ? creatorRole.name() : null);src/main/java/stackpot/stackpot/user/entity/User.java (1)
59-63: interests 컬렉션도 @Builder.Default 적용 권장동일한 이유로
interests도 null 방지 기본값을 빌더에 반영하세요.- private List<String> interests = new ArrayList<>(); // List<String> 사용 + @Builder.Default + private List<String> interests = new ArrayList<>(); // List<String> 사용src/main/java/stackpot/stackpot/task/service/TaskQueryServiceImpl.java (3)
58-62: 참여자 중복 제거 로직을 ID 기준으로 통일
.distinct()는PotMember의equals/hashCode구현에 의존합니다. 아래처럼 ID 기반으로 중복 제거하면 일관되고 안전합니다(아래는 본 블록 인근 라인에 대한 교체 제안).- List<PotMember> participants = tasks.stream() - .map(Task::getPotMember) - .distinct() - .collect(Collectors.toList()); + List<PotMember> participants = tasks.stream() + .map(Task::getPotMember) + .collect(Collectors.toMap(PotMember::getPotMemberId, pm -> pm, (a, b) -> a)) + .values().stream().toList();
81-85: viewDetailTask 도 동일하게 ID 기준 중복 제거 권장위 프리뷰와 동일한 방식으로 통일하면 불필요한 중복을 확실히 제거합니다.
- List<PotMember> participants = tasks.stream() - .map(Task::getPotMember) - .distinct() - .collect(Collectors.toList()); + List<PotMember> participants = tasks.stream() + .map(Task::getPotMember) + .collect(Collectors.toMap(PotMember::getPotMemberId, pm -> pm, (a, b) -> a)) + .values().stream().toList();
111-154: 배치 최적화 좋습니다. 추가 제안: 읽기 전용 트랜잭션, 타입 세이프 프로젝션, 중복 로직 정리현재 구현은 성능상 좋습니다. 아래 3가지만 다듬으면 더 견고해집니다.
- 읽기 전용 트랜잭션: Spring의
@Transactional(readOnly = true)사용 권장(플러시 방지, 힌트 전달).- 타입 프로젝션:
Object[]캐스팅 대신 인터페이스 기반 프로젝션으로 컴파일 타임 타입 안정성 확보.- 참여자 산출 로직 DRY: 프리뷰/디테일/본 메서드의 참여자 중복 제거 로직을 private 헬퍼로 추출.
변경 예시(메서드 시그니처 라인 내 diff):
- @Transactional + @Transactional(readOnly = true) public List<MyPotTaskPreViewResponseDto> getTasksFromDate(Long potId, LocalDate date) {추가 코드(파일 상단 import와 리포지토리 프로젝션 예):
// import 교체 import org.springframework.transaction.annotation.Transactional; // 타입 프로젝션 public interface CreatorRoleRow { Long getUserId(); Role getRole(); } // PotMemberRepository List<CreatorRoleRow> findCreatorRolesByPotAndUserIds(Long potId, Set<Long> userIds); // 매핑 Map<Long, Role> creatorRoleMap = potMemberRepository .findCreatorRolesByPotAndUserIds(potId, creatorIds).stream() .collect(Collectors.toMap(CreatorRoleRow::getUserId, CreatorRoleRow::getRole, (a,b)->a));src/main/java/stackpot/stackpot/feed/converter/FeedConverter.java (2)
36-36: writerRoles 목록 정규화(Null/중복/순서) 및 캐시 경로와의 일관성 확보 제안
- 동일 응답 스키마라도 DB 경로(getRoleNames)와 캐시 경로(getWriterRoles) 간 정렬/중복 제거 기준이 다르면 스냅샷 테스트나 클라이언트 비교 로직에서 불안정성이 생깁니다.
- NPE 방어도 함께 제안합니다.
다음과 같이 공용 헬퍼를 두고 모두 동일하게 적용하면 안전합니다.
- .writerRoles(feed.getUser().getRoleNames()) + .writerRoles(normalizeRoleNames(feed.getUser().getRoleNames()))- .writerRoles(feed.getUser().getRoleNames()) + .writerRoles(normalizeRoleNames(feed.getUser().getRoleNames()))- .writerRoles(feed.getUser().getRoleNames()) + .writerRoles(normalizeRoleNames(feed.getUser().getRoleNames()))- .writerRoles(feed.getWriterRoles()) + .writerRoles(normalizeRoleNames(feed.getWriterRoles()))- .writerRoles(feed.getUser().getRoleNames()) + .writerRoles(normalizeRoleNames(feed.getUser().getRoleNames()))추가 메서드(파일 하단 등 적절한 위치):
private List<String> normalizeRoleNames(List<String> roles) { if (roles == null) return java.util.Collections.emptyList(); return roles.stream().filter(Objects::nonNull).distinct().sorted().toList(); }추가 import:
import java.util.Collections; import java.util.Objects;캐시 저장 시점(FeedCacheDto 생성)에서도 동일 정규화가 반영되는지 확인 부탁드립니다.
Also applies to: 66-66, 101-101, 130-130, 147-147
66-66: writer 표기 규칙 일관성 확인본 메서드는
writer = nickname + " 새싹"을 사용하고, 다른 경로는RoleNameMapper.getWriterNickname(user)를 사용합니다. 동일 사용자에 대해 경로별 표기가 달라질 수 있어 혼란을 유발합니다. 하나의 규칙으로 통일해 주세요.src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java (2)
160-163: 탈퇴 사용자 판별 로직 중복 제거
user.getRoles().contains(Role.UNKNOWN)체크가 다수 반복됩니다. 헬퍼 메서드로 추출하면 가독성과 변경 용이성이 좋아집니다.예시:
private boolean isWithdrawn(User u) { return u.getRoles() != null && u.getRoles().contains(Role.UNKNOWN); }사용:
- if (user.getRoles().contains(Role.UNKNOWN)){ + if (isWithdrawn(user)) {Also applies to: 172-177, 185-188, 201-203
265-270: TempUser 캐스팅 안전성 보강
(TempUser) auth.getPrincipal()는 컨텍스트에 따라ClassCastException위험이 있습니다.instanceof가드로 방어하고 적절한 예외로 처리하세요.예시:
- Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - Long tempUserId = ((TempUser) auth.getPrincipal()).getId(); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + Object principal = auth != null ? auth.getPrincipal() : null; + if (!(principal instanceof TempUser temp)) { + throw new TokenHandler(ErrorStatus.INVALID_ACCESS_TOKEN); // 프로젝트 기준의 적절한 에러로 변경 + } + Long tempUserId = temp.getId();
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (32)
src/main/java/stackpot/stackpot/apiPayload/code/status/ErrorStatus.java(1 hunks)src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java(1 hunks)src/main/java/stackpot/stackpot/feed/controller/FeedController.java(3 hunks)src/main/java/stackpot/stackpot/feed/converter/FeedConverter.java(5 hunks)src/main/java/stackpot/stackpot/feed/dto/FeedCacheDto.java(2 hunks)src/main/java/stackpot/stackpot/feed/dto/FeedResponseDto.java(2 hunks)src/main/java/stackpot/stackpot/feed/dto/FeedSearchResponseDto.java(2 hunks)src/main/java/stackpot/stackpot/pot/converter/PotConverter.java(2 hunks)src/main/java/stackpot/stackpot/pot/converter/PotDetailConverter.java(2 hunks)src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java(2 hunks)src/main/java/stackpot/stackpot/pot/dto/PotPreviewResponseDto.java(1 hunks)src/main/java/stackpot/stackpot/pot/repository/PotMemberRepository.java(2 hunks)src/main/java/stackpot/stackpot/pot/service/pot/PotCommandServiceImpl.java(1 hunks)src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java(3 hunks)src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationCommandServiceImpl.java(2 hunks)src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationQueryServiceImpl.java(3 hunks)src/main/java/stackpot/stackpot/pot/service/potMember/PotMemberCommandServiceImpl.java(2 hunks)src/main/java/stackpot/stackpot/task/converter/TaskBoardConverter.java(2 hunks)src/main/java/stackpot/stackpot/task/repository/TaskRepository.java(2 hunks)src/main/java/stackpot/stackpot/task/repository/TaskboardRepository.java(1 hunks)src/main/java/stackpot/stackpot/task/service/TaskCommandServiceImpl.java(3 hunks)src/main/java/stackpot/stackpot/task/service/TaskQueryServiceImpl.java(3 hunks)src/main/java/stackpot/stackpot/user/converter/UserConverter.java(3 hunks)src/main/java/stackpot/stackpot/user/dto/request/UserRequestDto.java(1 hunks)src/main/java/stackpot/stackpot/user/dto/request/UserUpdateRequestDto.java(1 hunks)src/main/java/stackpot/stackpot/user/dto/response/UserResponseDto.java(3 hunks)src/main/java/stackpot/stackpot/user/dto/response/UserSignUpResponseDto.java(2 hunks)src/main/java/stackpot/stackpot/user/entity/TempUser.java(3 hunks)src/main/java/stackpot/stackpot/user/entity/User.java(4 hunks)src/main/java/stackpot/stackpot/user/entity/enums/Role.java(1 hunks)src/main/java/stackpot/stackpot/user/repository/TempUserRepository.java(1 hunks)src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java(11 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationCommandServiceImpl.java (1)
src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java (1)
RoleNameMapper(7-43)
src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationQueryServiceImpl.java (1)
src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java (1)
RoleNameMapper(7-43)
src/main/java/stackpot/stackpot/task/converter/TaskBoardConverter.java (3)
src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java (1)
RoleNameMapper(7-43)src/main/java/stackpot/stackpot/common/util/DateFormatter.java (1)
DateFormatter(7-47)src/main/java/stackpot/stackpot/common/util/DdayCounter.java (1)
DdayCounter(6-24)
src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java (1)
src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java (1)
RoleNameMapper(7-43)
🔇 Additional comments (25)
src/main/java/stackpot/stackpot/feed/controller/FeedController.java (2)
53-53: 카테고리 파라미터 설명의 PLAN 치환 LGTM문서 예시와 실제 허용값이 일치하는지만 재확인 부탁드립니다(ALL 포함 여부).
91-91: PATCH 문서의 카테고리 enum 값 PLAN 반영 OK생성 API와 동일한 셋으로 유지되어 일관성 좋습니다. Swagger 스키마/예시도 함께 갱신되었는지 확인해 주세요.
src/main/java/stackpot/stackpot/feed/dto/FeedSearchResponseDto.java (1)
5-6: creatorRole → List 타입 변경 검증 필요
- SearchService/Converter/Repository 매핑 로직에서 단일 String → List 변환 반영 여부 점검
- Swagger 스키마 응답에 배열로 노출되는지 확인 (기존 클라이언트 호환성)
- 선택 사항: NPE 방지를 위해 필드 초기값으로 빈 리스트 할당 고려
src/main/java/stackpot/stackpot/pot/service/pot/PotCommandServiceImpl.java (1)
78-80: 리뷰 무시:potRole는String이 아닌Role열거형 반환
PotRequestDto.getPotRole()가Role타입이므로String.valueOf기반 제안은 컴파일 오류 발생. 대신@NotNull검증 또는 엔티티 변환 시null체크를 적용하세요.Likely an incorrect or invalid review comment.
src/main/java/stackpot/stackpot/feed/dto/FeedCacheDto.java (1)
7-7: 필요한 import 추가
List사용을 위한 import 추가는 적절합니다.src/main/java/stackpot/stackpot/pot/dto/PotPreviewResponseDto.java (1)
14-14:userRoles기본값 적용 문제 없음
PotPreviewResponseDto.userRoles에@Builder.Default와Collections.emptyList()를 설정하여 null 방지 권장.
다른 DTO(MyPotTodoResponseDTO,PotDetailResponseDto)의userRole(단수) 필드와는 별도이므로 호환성 영향 없습니다.src/main/java/stackpot/stackpot/task/repository/TaskRepository.java (1)
37-38: 대량 조회 메서드 추가 LGTM
findByTaskboardIn으로 배치 조회가 가능해집니다.src/main/java/stackpot/stackpot/common/util/RoleNameMapper.java (1)
13-14: 레거시 역할 키 'PLANNING' 정상 매핑 위해 normalizeRoleKey 추가
DB나 외부 입력 값으로 여전히 'PLANNING'이 들어올 경우 미매핑 에러가 발생할 수 있으니, RoleNameMapper#getKoreanRole 및 getRoleFromPot 호출부에서normalizeRoleKey(‘PLANNING’→‘PLAN’)를 먼저 적용해 주세요.src/main/java/stackpot/stackpot/pot/converter/PotMemberConverter.java (1)
88-95: 예외 처리 중복 제거
Role.fromString이 내부에서 이미IllegalArgumentException을 캐치해UNKNOWN으로 폴백하므로, 추가적인try–catch블록은 불필요합니다.Likely an incorrect or invalid review comment.
src/main/java/stackpot/stackpot/user/converter/UserConverter.java (2)
15-16: 멀티 역할 매핑 전환 LGTM
TempUser#getRoleNames()로의 전환이 응답 스키마(List)와 일치합니다.
55-56: 역할 표기 형식 확인(영문 상수 vs. 한글명/표기 정책)현재는 enum 상수명(List)을 그대로 노출합니다. 기획에서 “역할명”을 한글(예: “기획”)로 요구한다면 매퍼를 통해 변환 필요합니다. 정책 확인 부탁드립니다.
src/main/java/stackpot/stackpot/task/service/TaskCommandServiceImpl.java (1)
61-64: 역할 조회 메서드 시그니처/의미 확인
findRoleByUserId(potId, userId)이름 대비 인자 구성이 potId+userId입니다. 저장소 메서드 네이밍/인덱스 설계를 일치시켜 주세요(예:findRoleByPotIdAndUserId).src/main/java/stackpot/stackpot/pot/converter/PotDetailConverter.java (1)
41-44: userRole 의미 변경 확인(현재 사용자 vs. 팟 개설자 역할)
userRole에creatorRoleName을 넣고 있습니다. 필드 명/문서가 “사용자”를 가리킨다면 오해 소지가 있습니다. 스키마/클라이언트 사용처와 일치하는지 확인하고 필요 시creatorRole로 명확화 또는 주석 보완을 권장합니다.src/main/java/stackpot/stackpot/user/entity/TempUser.java (2)
34-39: 역할 다중 선택을 위한 ElementCollection 매핑 적절전용 조인 테이블(
temp_user_roles)과 ENUM 저장 방식이 명확합니다.
50-54: 역할 노출 정책/정렬 확인
Enum#name()을 그대로 노출합니다. 한글 표기/정렬 정책이 있다면 여기서 변환/정렬을 고려하세요(또는 컨버터 단에서 일관 처리).src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java (2)
61-72: 역할 수 포맷 한글화 적용 LGTM
RoleNameMapper.getKoreanRoleName으로 포맷한 문자열 구성은 목적(채소명 → 역할명)과 일치합니다.
95-98: 상세 모집역할 한글화 LGTM채소명이 아닌 역할명(한글)으로의 포맷 변경이 일관됩니다.
src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationQueryServiceImpl.java (2)
33-33: PotMemberRepository 주입 추가 LGTM작성자 역할 조회 용도로 필요한 의존성 추가가 명확합니다.
70-71: 모집역할 한글 표기 LGTM채소 → 역할명(한글) 전환과 일치합니다.
src/main/java/stackpot/stackpot/task/converter/TaskBoardConverter.java (1)
72-89: TaskBoardConverter.toDto: 역할명 매핑 및 Enum 재해석 정리- String creatorRoleKo = RoleNameMapper.mapRoleName(creatorRole.name()); + String creatorRoleKo = RoleNameMapper.getKoreanRoleName(creatorRole.name()); … - .creatorNickname(taskboard.getUser().getNickname() + " " + creatorRoleKo) - .creatorRole(Role.valueOf(creatorRole.name())) + .creatorNickname(taskboard.getUser().getNickname() + " " + creatorRoleKo) // 닉네임 + 역할명(한글) + .creatorRole(creatorRole == null ? Role.UNKNOWN : creatorRole)변경 후 실제 응답 샘플에 “백엔드/프론트엔드/디자인/기획”이 올바르게 표시되는지 확인해주세요.
src/main/java/stackpot/stackpot/user/entity/User.java (2)
112-117: 삭제 처리 로직의 역할 초기화 OK기존 역할 비우고
UNKNOWN단일 값으로 재설정하는 접근이 명확합니다. 현재 구현으로는 중복 삽입 가능성도 없습니다.
139-143: getRoleNames() 구현 적절Null-safe 처리 및 스트림 매핑이 간결합니다.
src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java (3)
119-121: login 응답의 roles 타입/널 안전성 재확인
user.getRoleNames()가 DTO가 기대하는 타입(List)과 일치하는지, null을 반환하지 않는지 확인 필요.- 신규 유저 분기에서
Collections.emptyList()사용은 적절합니다.Also applies to: 132-136
300-304: 트랜잭션 내 저장/정리 순서 적절신규 User 저장 후 TempUser 삭제, 그리고 신규 토큰 발급의 순서가 자연스럽습니다. 트랜잭션 경계 내에서 일관성 보장이 됩니다.
359-360: 회원탈퇴 후 블랙리스트 등록 위치 적절연관 데이터 정리 후 토큰 블랙리스트 처리로 순서가 명확합니다. 만료시간을 기존 토큰에서 산출하는 것도 합리적입니다.
| @Operation(summary = "Feed 생성 API", | ||
| description = "Feed를 생성하는 API입니다.\n" + | ||
| "- categories: 다중 선택 가능하며 enum입니다. [ALL, BACKEND, FRONTEND, DESIGN, PLANNING] \n" + | ||
| "- categories: 다중 선택 가능하며 enum입니다. [ALL, BACKEND, FRONTEND, DESIGN, PLAN] \n" + |
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.
💡 Verification agent
🧩 Analysis chain
Swagger 문서의 카테고리 enum 값 PLAN 반영 OK — Category 정의와 싱크 확인
문서 갱신은 적절합니다. Category enum에 실제로 PLAN이 존재하는지, 이전 PLANNING이 남아있지 않은지 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# Category enum에 PLAN이 정의되어 있는지와 PLANNING 잔존 여부 점검
rg -nP -C2 'enum\s+Category\b' --type=java
rg -nP '\bPLAN(NING)?\b' src/main/java/stackpot/stackpot/feed --type=javaLength of output: 1171
Category enum과 Swagger 문서 카테고리 불일치: ‘PLAN’ vs ‘PLANNING’
FeedController 내 문서에서 ‘[ALL, BACKEND, FRONTEND, DESIGN, PLAN]’으로 표기하고 있으나, 실제 Category enum에는 ‘PLANNING’이 정의되어 있습니다. 문서와 enum 이름을 일치시키거나, enum에 ‘PLAN’을 추가/이름 변경하세요.
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/feed/controller/FeedController.java around
line 36, the Swagger/documentation lists category values as "[ALL, BACKEND,
FRONTEND, DESIGN, PLAN]" but the actual Category enum defines "PLANNING"; update
the documentation to match the enum (change "PLAN" to "PLANNING") or
alternatively rename/add the enum value to "PLAN" so names are consistent; make
the change in the Swagger string/annotation, ensure any examples/tests
referencing the old name are updated, and run a build to verify no references
remain to the mismatched value.
| Role userPotRole = potMemberRepository.findRoleByUserId(pot.getPotId(), user.getId()) | ||
| .orElse(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.
userPotRole null 처리로 인한 하위 NPE 유발 경로
여기서 .orElse(null)을 반환하도록 변경되어 PotConverter.toCompletedPotResponseDto에서 NPE가 발생합니다. 컨버터를 null-세이프로 고치거나(권장, 별도 코멘트 참조) 여기서도 안전하게 문자열로 변환해 넘기는 방식 중 하나로 정리 필요합니다.
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java
around lines 73 to 75, the call
potMemberRepository.findRoleByUserId(...).orElse(null) allows userPotRole to be
null which leads to a downstream NPE in PotConverter.toCompletedPotResponseDto;
instead avoid returning null by mapping the Optional to a safe String (e.g.,
.map(Role::toString).orElse("UNKNOWN") or the appropriate default role string)
before passing into the converter, or pass an Optional/nullable-aware value the
converter expects; implement the chosen approach consistently here so the
converter no longer receives a null and cannot NPE.
| Role userPotRole = potMemberRepository.findRoleByUserId(pot.getPotId(), user.getId()) | ||
| .orElse(null); // or throw if required | ||
|
|
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.
동일 NPE 경로 재발 및 역할 표기 일관성
- 여기서도
.orElse(null)로 동일한 NPE 경로가 생깁니다(컨버터에서 null-세이프 처리 필요). - 참고: 바로 위
formattedMembers는 여전히mapRoleName(채소명)을 사용 중이라, 같은 “완료 팟 조회” API 간에도 한글/채소 표기가 섞일 수 있습니다. 역할명으로의 표기 일원화를 권장합니다.
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/pot/service/pot/PotQueryServiceImpl.java
around lines 239 to 241, remove the unsafe use of .orElse(null) for Role
retrieval and instead handle the Optional explicitly (either .orElseThrow with a
clear exception or conditional logic that returns a safe default), update the
converter/mapper to be null-safe so it never dereferences a missing Role, and
make role-labels consistent by replacing uses of mapRoleName/vegetable-style
labels with the canonical role name field (e.g., role.getName()) so
formattedMembers and this lookup produce uniform role strings across the
"completed pot" API responses.
| String applicantRole = user.getRoles().stream() | ||
| .map(role -> RoleNameMapper.mapRoleName(role.name())) | ||
| .orElse("멤버"); | ||
|
|
||
| .collect(Collectors.joining(", ")); | ||
| CompletableFuture.runAsync(() -> emailService.sendSupportNotification( |
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.
💡 Verification agent
🧩 Analysis chain
이메일 본문에 ‘채소명’이 사용됨 + 공백 누락 + roles null 가능성 — 한국어 역할명 사용 및 NPE 방지
PR 목표(채소이름 → 역할명 전환)와 일관되게, 이메일에서도 RoleNameMapper.getKoreanRoleName 사용을 권장합니다. 또한 user.getRoles()가 null일 경우 NPE가 발생할 수 있으며, 닉네임과 역할 문자열 사이 공백/표기 누락이 있습니다.
- String appliedRole = application.getPotRole().name();
- String appliedRoleName = RoleNameMapper.mapRoleName(appliedRole);
- String applicantRole = user.getRoles().stream()
- .map(role -> RoleNameMapper.mapRoleName(role.name()))
- .collect(Collectors.joining(", "));
+ String appliedRole = application.getPotRole().name();
+ String appliedRoleName = RoleNameMapper.getKoreanRoleName(appliedRole); // 예: BACKEND -> "백엔드"
+ String applicantRoles = java.util.Optional.ofNullable(user.getRoles())
+ .orElseGet(java.util.List::of)
+ .stream()
+ .map(role -> RoleNameMapper.getKoreanRoleName(role.name()))
+ .collect(Collectors.joining(", "));
CompletableFuture.runAsync(() -> emailService.sendSupportNotification(
pot.getUser().getEmail(),
pot.getPotName(),
- user.getNickname() + applicantRole,
+ user.getNickname() + (applicantRoles.isBlank() ? "" : " (" + applicantRoles + ")"),
appliedRoleName,
appliedRole,
Optional.ofNullable(user.getUserIntroduction()).orElse("없음")
));검증 포인트:
- 이메일 템플릿이 채소명이 아닌 역할명(백엔드/프론트엔드/디자인/기획)을 기대하는지 확인.
- 멀티 역할 표기 형태(쉼표, 괄호 포함)가 디자인과 합의되었는지 확인.
🏁 Script executed:
#!/bin/bash
# 1. sendSupportNotification 정의 및 호출부 확인
rg -nH -- "sendSupportNotification" -C3 -- "*.java"
# 2. User 클래스의 roles 필드 선언과 getRoles() 구현 확인
rg -nH -- "class User" -C3 -- "*.java"
rg -nH -- "List<.*Role" -C3 -- "*.java"
rg -nH -- "getRoles" -C3 -- "*.java"Length of output: 707
다음 스크립트로 이메일 템플릿, User.getRoles null 가능성, RoleNameMapper 등을 재검증하세요.
🏁 Script executed:
#!/bin/bash
echo "1) sendSupportNotification 호출 위치:"
rg -nH -C3 'sendSupportNotification' .
echo ""
echo "2) EmailService 인터페이스 및 메서드 시그니처 확인:"
EMAIL_SVC=$(fd 'EmailService.java' -t f | head -n 1)
if [ -n "$EMAIL_SVC" ]; then
echo "→ $EMAIL_SVC"
sed -n '1,200p' "$EMAIL_SVC"
else
echo "EmailService.java 파일을 찾을 수 없습니다."
fi
echo ""
echo "3) User 클래스의 roles 필드 및 getRoles() 구현 확인:"
USER_FILE=$(fd 'User.java' -t f | head -n 1)
if [ -n "$USER_FILE" ]; then
echo "→ $USER_FILE"
sed -n '1,200p' "$USER_FILE"
else
echo "User.java 파일을 찾을 수 없습니다."
fi
echo ""
echo "4) RoleNameMapper.getKoreanRoleName 구현부 확인:"
ROLE_MAPPER=$(fd 'RoleNameMapper.java' -t f | head -n 1)
if [ -n "$ROLE_MAPPER" ]; then
echo "→ $ROLE_MAPPER"
sed -n '1,200p' "$ROLE_MAPPER"
else
echo "RoleNameMapper.java 파일을 찾을 수 없습니다."
fi
echo ""
echo "5) 이메일 템플릿 내 'vegetable' 또는 '채소' 키워드 검색:"
rg -nH -C2 'vegetable' --glob '*.ftl' --glob '*.html' --glob '*.properties' --glob '*.yml'
rg -nH -C2 '채소' --glob '*.ftl' --glob '*.html' --glob '*.properties' --glob '*.yml'
echo ""
echo "6) 리소스 폴더 상위 20개 파일 목록:"
find src/main/resources -type f | head -n 20Length of output: 7752
getKoreanRoleName 사용, NPE 방지 및 닉네임-역할 포맷 수정
RoleNameMapper.mapRoleName→RoleNameMapper.getKoreanRoleName로 채소명이 아닌 역할명(백엔드/프론트엔드/디자인/기획) 출력user.getRoles()null 시 빈 리스트 처리하여 NPE 방지- 닉네임 뒤에 역할이 있을 경우
" (역할1, 역할2)"형태로 공백 및 괄호 추가
- String appliedRoleName = RoleNameMapper.mapRoleName(appliedRole);
- String applicantRole = user.getRoles().stream()
- .map(role -> RoleNameMapper.mapRoleName(role.name()))
- .collect(Collectors.joining(", "));
+ String appliedRoleName = RoleNameMapper.getKoreanRoleName(appliedRole); // BACKEND -> "백엔드"
+ String applicantRoles = Optional.ofNullable(user.getRoles())
+ .orElseGet(List::of)
+ .stream()
+ .map(role -> RoleNameMapper.getKoreanRoleName(role.name()))
+ .collect(Collectors.joining(", "));
CompletableFuture.runAsync(() -> emailService.sendSupportNotification(
pot.getUser().getEmail(),
pot.getPotName(),
- user.getNickname() + applicantRole,
+ user.getNickname() + (applicantRoles.isBlank() ? "" : " (" + applicantRoles + ")"),
appliedRoleName,
appliedRole,
Optional.ofNullable(user.getUserIntroduction()).orElse("없음")
));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| String applicantRole = user.getRoles().stream() | |
| .map(role -> RoleNameMapper.mapRoleName(role.name())) | |
| .orElse("멤버"); | |
| .collect(Collectors.joining(", ")); | |
| CompletableFuture.runAsync(() -> emailService.sendSupportNotification( | |
| String appliedRoleName = RoleNameMapper.getKoreanRoleName(appliedRole); // BACKEND -> "백엔드" | |
| String applicantRoles = Optional.ofNullable(user.getRoles()) | |
| .orElseGet(List::of) | |
| .stream() | |
| .map(role -> RoleNameMapper.getKoreanRoleName(role.name())) | |
| .collect(Collectors.joining(", ")); | |
| CompletableFuture.runAsync(() -> emailService.sendSupportNotification( | |
| pot.getUser().getEmail(), | |
| pot.getPotName(), | |
| user.getNickname() + (applicantRoles.isBlank() ? "" : " (" + applicantRoles + ")"), | |
| appliedRoleName, | |
| appliedRole, | |
| Optional.ofNullable(user.getUserIntroduction()).orElse("없음") | |
| )); |
🤖 Prompt for AI Agents
In
src/main/java/stackpot/stackpot/pot/service/potApplication/PotApplicationCommandServiceImpl.java
around lines 84 to 87, replace RoleNameMapper.mapRoleName usage with
RoleNameMapper.getKoreanRoleName, guard user.getRoles() against null by treating
it as an empty collection before streaming to avoid NPE, and change the
applicant role string assembly so that if there are roles the nickname is
followed by a space and roles in parentheses like "nickname (역할1, 역할2)"
otherwise just the nickname; make these changes before passing values to
emailService.sendSupportNotification.
src/main/java/stackpot/stackpot/pot/service/potMember/PotMemberCommandServiceImpl.java
Show resolved
Hide resolved
| FRONTEND("버섯", "프론트엔드"), | ||
| DESIGN("브로콜리", "디자인"), | ||
| PLANNING("당근", "기획"), | ||
| PLAN("당근", "기획"), |
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.
💡 Verification agent
🧩 Analysis chain
PLANNING → PLAN 변경 일관성 확인 요청
변경 자체는 적절합니다. 레거시 참조(문자열/Swagger/문서/DB 시드)에 PLANNING이 남아있지 않은지 한 번 더 확인해 주세요.
다음 스크립트로 잔존 참조를 점검해 주세요:
🏁 Script executed:
#!/bin/bash
# PLANNING 잔존 참조 검색
rg -nP -C2 '\bPLANNING\b' --type=java --type=properties --type=ymlLength of output: 103
🏁 Script executed:
#!/bin/bash
# PLANNING 잔존 참조 재검증
rg -nP -C2 '\bPLANNING\b' -g '*.java' -g '*.properties' -g '*.yml' -g '*.yaml' -g '*.sql'Length of output: 1009
PLANNING → PLAN 잔여 참조 2곳 수정 필요
- src/main/java/stackpot/stackpot/pot/controller/PotController.java:41 – Swagger 설명 문자열 내
PLANNING→PLAN - src/main/java/stackpot/stackpot/feed/entity/enums/Category.java:8 – enum 값
PLANNING→PLAN
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/entity/enums/Role.java around line 10
and the PR note, update the remaining two references where the enum name was not
renamed: in src/main/java/stackpot/stackpot/pot/controller/PotController.java at
line 41 change the Swagger description string from "PLANNING" to "PLAN", and in
src/main/java/stackpot/stackpot/feed/entity/enums/Category.java at line 8 rename
the enum constant from PLANNING to PLAN; after renaming, fix any compile errors
or usages that reference the old PLANNING name and run a build/tests to verify
everything compiles.
| @ElementCollection(targetClass = Role.class) | ||
| @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) | ||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = true, length = 255) | ||
| private Role role; // 역할 | ||
| @Column(name = "role") | ||
| private List<Role> roles = new ArrayList<>(); | ||
|
|
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.
🛠️ Refactor suggestion
@builder 사용 시 컬렉션 초기값 무시 문제 → NPE/영속 오류 예방 조치
Lombok @builder는 필드 초기값을 무시합니다. 현재 roles가 null로 생성될 수 있어 NPE 및 @ElementCollection 영속 시 문제를 유발할 수 있습니다. 또한 동일 역할 중복 삽입을 DB 레벨에서 방지하는 제약도 권장합니다.
- @ElementCollection(targetClass = Role.class)
- @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
+ @ElementCollection(targetClass = Role.class)
+ @CollectionTable(
+ name = "user_roles",
+ joinColumns = @JoinColumn(name = "user_id"),
+ uniqueConstraints = @UniqueConstraint(columnNames = {"user_id","role"})
+ )
@Enumerated(EnumType.STRING)
@Column(name = "role")
- private List<Role> roles = new ArrayList<>();
+ @Builder.Default
+ private List<Role> roles = new ArrayList<>();또한 Enum 상수명이 PLANNING → PLAN으로 변경되었다면, 문자열로 저장된 기존 데이터의 마이그레이션이 필요합니다. 예:
- user_roles.role, pot_member.role_name 등 “PLANNING” → “PLAN” 업데이트
- 배포 전 DDL/DML 스크립트 적용 및 롤백 플랜 준비
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @ElementCollection(targetClass = Role.class) | |
| @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) | |
| @Enumerated(EnumType.STRING) | |
| @Column(nullable = true, length = 255) | |
| private Role role; // 역할 | |
| @Column(name = "role") | |
| private List<Role> roles = new ArrayList<>(); | |
| @ElementCollection(targetClass = Role.class) | |
| @CollectionTable( | |
| name = "user_roles", | |
| joinColumns = @JoinColumn(name = "user_id"), | |
| uniqueConstraints = @UniqueConstraint(columnNames = {"user_id","role"}) | |
| ) | |
| @Enumerated(EnumType.STRING) | |
| @Column(name = "role") | |
| @Builder.Default | |
| private List<Role> roles = new ArrayList<>(); |
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/entity/User.java around lines 53-58,
Lombok's @Builder ignores field initializers so roles can be null causing NPEs
and JPA persistence errors; update the class to ensure roles is always non-null
when using the builder (e.g., annotate with @Builder.Default or initialize in a
@NoArgsConstructor/@AllArgsConstructor or builder-specific method) and ensure
add/remove methods defensively handle null; add a DB-level uniqueness constraint
on user_roles (user_id, role) to prevent duplicate role entries; and if the enum
constant was renamed from PLANNING to PLAN, prepare and apply a migration script
to update existing string values (user_roles.role and any other role_name
columns) before deploying, plus a rollback plan.
| tempUser.setRoles(request.getRoles()); | ||
|
|
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.
🛠️ Refactor suggestion
회원가입 시 roles 입력값 검증 및 정규화 필요
클라이언트에서 전달되는 request.getRoles()에 대해 null/빈 목록/중복/허용되지 않은 값(whitelist) 검증이 필요합니다. 권한 상승 및 쓰레기 데이터 유입을 방지하세요.
예시:
- tempUser.setRoles(request.getRoles());
+ List<Role> roles = Optional.ofNullable(request.getRoles()).orElse(Collections.emptyList())
+ .stream().filter(Objects::nonNull).distinct().toList();
+ // TODO: whitelist/최대 선택개수 제한 검증 추가
+ tempUser.setRoles(roles);추가 import:
import java.util.Objects;
import java.util.Optional;🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java
around lines 99-100, request.getRoles() is assigned directly to tempUser without
validation; validate and normalize the roles by checking for null/empty,
trimming and lowercasing each entry, removing duplicates, filtering against an
allowed whitelist, and if invalid either throw a validation exception or assign
a safe default role; use Objects/Optional to handle nulls and collect the
sanitized set/list before calling tempUser.setRoles(sanitizedRoles).
| if (requestDto.getRoles() != null && !requestDto.getRoles().isEmpty()) { | ||
| user.setRoles(requestDto.getRoles()); // | ||
| } |
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.
🛠️ Refactor suggestion
프로필 업데이트 시 roles 변경 검증(권한/정합성)
프로필 수정에서 roles를 임의로 바꿀 수 있으면 권한 상승 우려가 있습니다. 허용된 역할만 반영, 중복 제거, 최대 선택 개수 제한 등을 적용하세요.
예시:
- if (requestDto.getRoles() != null && !requestDto.getRoles().isEmpty()) {
- user.setRoles(requestDto.getRoles()); //
- }
+ if (requestDto.getRoles() != null) {
+ List<Role> sanitized = requestDto.getRoles().stream()
+ .filter(allowedRoles::contains) // TODO: allowedRoles 정의
+ .filter(Objects::nonNull).distinct()
+ .limit(5) // TODO: 정책에 맞게 조정
+ .toList();
+ user.setRoles(sanitized);
+ }추가 import: import java.util.Objects;
Committable suggestion skipped: line range outside the PR's diff.
| String intro = tempUser.getRoles().isEmpty() | ||
| ? nickname + " 새싹입니다." | ||
| : tempUser.getRoles().stream() | ||
| .map(Role::getKoreanName) | ||
| .collect(Collectors.joining(", ")) + "에 관심있는 " + nickname + " 새싹입니다."; | ||
|
|
||
| List<String> interests = tempUser.getInterest() != null | ||
| ? new ArrayList<>(tempUser.getInterest()) | ||
| : new ArrayList<>(); | ||
|
|
||
| List<Role> roles = tempUser.getRoles() != null | ||
| ? new ArrayList<>(tempUser.getRoles()) | ||
| : new ArrayList<>(); | ||
|
|
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.
NPE 가능성: tempUser.getRoles()가 null일 때 intro 생성에서 터질 수 있음
tempUser.getRoles().isEmpty() 및 이후 stream() 호출은 roles가 null이면 NPE입니다. 먼저 roles를 안전하게 준비하고 intro를 생성하세요.
수정안:
- String intro = tempUser.getRoles().isEmpty()
- ? nickname + " 새싹입니다."
- : tempUser.getRoles().stream()
- .map(Role::getKoreanName)
- .collect(Collectors.joining(", ")) + "에 관심있는 " + nickname + " 새싹입니다.";
-
- List<String> interests = tempUser.getInterest() != null
- ? new ArrayList<>(tempUser.getInterest())
- : new ArrayList<>();
-
- List<Role> roles = tempUser.getRoles() != null
- ? new ArrayList<>(tempUser.getRoles())
- : new ArrayList<>();
+ List<Role> roles = tempUser.getRoles() != null
+ ? tempUser.getRoles().stream().filter(Objects::nonNull).distinct().toList()
+ : Collections.emptyList();
+
+ String intro = roles.isEmpty()
+ ? nickname + " 새싹입니다."
+ : roles.stream()
+ .map(Role::getKoreanName)
+ .distinct()
+ .collect(Collectors.joining(", ")) + "에 관심있는 " + nickname + " 새싹입니다.";
+
+ List<String> interests = tempUser.getInterest() != null
+ ? tempUser.getInterest().stream().filter(Objects::nonNull).distinct().toList()
+ : Collections.emptyList();추가 import:
import java.util.Objects;🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/user/service/UserCommandServiceImpl.java
around lines 273 to 286, tempUser.getRoles() can be null causing an NPE when
building intro; first initialize a non-null List<Role> roles (e.g., roles =
tempUser.getRoles() != null ? new ArrayList<>(tempUser.getRoles()) : new
ArrayList<>()), then use roles.isEmpty() and roles.stream() to build the intro;
ensure you also import java.util.Objects if you plan to use
Objects.requireNonNullElse or similar null-safe helpers.
[Chore][jjaeroong]: 스웨거 PLANNING에서 PLAN으로 변경
PR 타입(하나 이상의 PR 타입을 선택해주세요)
반영 브랜치
ex) feat/login -> dev
작업 내용
ex) 로그인 시, 구글 소셜 로그인 기능을 추가했습니다.
테스트 결과
ex) 베이스 브랜치에 포함되기 위한 코드는 모두 정상적으로 동작해야 합니다. 결과물에 대한 스크린샷, GIF, 혹은 라이브
Summary by CodeRabbit