-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: [FN-96][FN-97][FN-99][FN-110][FN-123] 그룹 초대 기능 #27
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
|
Warning Rate limit exceeded@dungbik has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 10 minutes and 50 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (5)
Walkthrough사용자 회원가입 시 UserRegisteredEvent 발행 및 리스너 추가. 그룹 초대 도메인(엔티티, 서비스, 레포지토리, 컨트롤러/문서, DTO) 신규 도입과 초대 조회 API 추가. Swagger 서버 URL 설정 지원. 사용자 도메인에 이메일 조회/닉네임 매핑 추가. 권한 체크 API 추가. 일부 에러코드/그룹 검증 로직 수정. PageResponse 도입. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthService
participant EventPublisher
participant Listener as UserRegisteredEventListener
participant GIService as GroupInvitationService
Client->>AuthService: register()
AuthService->>EventPublisher: publish(UserRegisteredEvent)
EventPublisher-->>Listener: UserRegisteredEvent (after commit)
Listener->>GIService: acceptPendingInvitationsOnRegister(userId, email)
GIService->>GIService: 이메일 기반 PENDING 초대 수락 및 멤버 추가
sequenceDiagram
participant Client
participant Ctrl as GroupInvitationController
participant Svc as GroupInvitationService
participant UserSvc as UserService
Client->>Ctrl: POST /groups/{id}/invitations
Ctrl->>Svc: createGroupInvitation(auth, groupId, req)
Svc->>UserSvc: findActiveUserByEmail(email)
Svc->>Svc: 권한/중복/본인초대 검증 후 초대 생성
Svc-->>Ctrl: GroupInvitationCreateResponse
Ctrl-->>Client: 201 Created
sequenceDiagram
participant Client
participant Ctrl as GroupInvitationController
participant Svc as GroupInvitationService
Client->>Ctrl: PATCH /groups/{gid}/invitations/{iid}
Ctrl->>Svc: respondToGroupInvitation(userId, gid, iid, req)
Svc->>Svc: PENDING 조회 및 상태 반영
alt ACCEPTED
Svc->>Svc: addGroupMember(gid, userId)
end
Svc-->>Ctrl: 200 OK
Ctrl-->>Client: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
✨ 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
CodeRabbit Configuration File (
|
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: 19
🔭 Outside diff range comments (1)
src/main/java/project/flipnote/common/config/SwaggerConfig.java (1)
20-36: Server URL 조건부 등록으로 환경 의존성 최소화고정된 서버를 항상 추가하면, 프로필 전환/프록시/HTTPS 환경에서 Swagger "Try it out"이 오작동할 수 있습니다. 값이 있을 때만 서버를 추가하거나(권장), 아예 서버를 추가하지 않아 현재 오리진을 사용하도록 두세요.
적용 제안(diff):
@Bean public OpenAPI openApi() { - return new OpenAPI() - .addServersItem(new Server().url(serverUrl)) - .addSecurityItem( + OpenAPI openAPI = new OpenAPI() + .addSecurityItem( new SecurityRequirement() .addList("access-token") ) .components(new Components() .addSecuritySchemes("access-token", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT")) ) - .info(apiInfo()); + .info(apiInfo()); + if (serverUrl != null && !serverUrl.isBlank()) { + openAPI.addServersItem(new Server().url(serverUrl)); + } + return openAPI; }참고: 위 변경으로 추가 import는 필요 없습니다. 대안으로 서버 URL을 아예 제거하면 Swagger UI가 현재 오리진을 사용합니다(가장 안전).
🧹 Nitpick comments (18)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (1)
12-12: 중복/포맷 회귀 방지를 위한 단위 테스트 추가 제안향후 코드 리팩터링 시 코드 문자열의 중복/포맷 일탈을 방지하기 위해 간단한 테스트를 추가하는 것을 권장합니다.
원하시면 다른 ErrorCode enum까지 스캔하는 공용 테스트도 드릴 수 있습니다.
package project.flipnote.common.security.exception; import static org.junit.jupiter.api.Assertions.*; import java.util.*; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class SecurityErrorCodeTest { @Test void codes_areUnique_andFollowPattern() { List<String> codes = Arrays.stream(SecurityErrorCode.values()) .map(SecurityErrorCode::getCode) .collect(Collectors.toList()); assertEquals( codes.size(), new HashSet<>(codes).size(), "SecurityErrorCode.code 중복이 있습니다."); assertTrue( codes.stream().allMatch(c -> c.matches("SECURITY_\\d+")), "코드는 SECURITY_### 패턴을 따라야 합니다."); } }src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (1)
3-5: 상태 네이밍 컨벤션 통일 검토 (ACCEPTED/REJECTED vs 기존 ACCEPT/REJECT)GroupJoinStatus(ACCEPT/REJECT)와 시제가 달라 도메인 전반의 일관성에서 약간의 혼동 여지가 있습니다. 응답용 상태에 과거형을 쓰는 의도가 명확하다면 유지해도 무방하나, 컨벤션을 문서화하거나 통일을 검토해 주세요.
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (1)
3-6: 정적 팩토리 메서드(from) 추가로 DTO 일관성 유지동일 패턴의 DTO들(GroupCreateResponse 등)이
from(...)정적 팩토리를 제공하고 있습니다. 본 레코드에도 추가하면 일관성이 좋아집니다.public record GroupInvitationCreateResponse( Long invitationId ) { + public static GroupInvitationCreateResponse from(Long invitationId) { + return new GroupInvitationCreateResponse(invitationId); + } }src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (1)
3-7: 이벤트 로깅 시 PII(이메일) 노출 주의record의 기본 toString()은 email을 그대로 출력합니다. 리스너/리커버 로직에서 이벤트 전체를 로그로 남기지 말고 userId 중심으로 로그하거나 이메일을 마스킹해 주세요. 운영 로깅에서 PII 최소화가 필요합니다.
src/main/java/project/flipnote/group/model/UserIdNickname.java (1)
3-6: Projection의 패키지 위치 재검토해당 프로젝션은 user 레이어(repository/service)에서 사용됩니다.
group.model보다는user.model또는user.repository.projection등 사용자 도메인 쪽으로 이동하면 응집도가 높아지고 순환 의존 가능성이 줄어듭니다.src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (1)
12-26: 에러 케이스 확장 제안: '이미 그룹 멤버' 구분
ALREADY_INVITED와 별도로, 초대 대상이 이미 그룹 멤버인 경우를 구분하는 에러 코드(예:INVITEE_ALREADY_MEMBER)가 있으면 클라이언트가 더 적절한 메시지를 안내할 수 있습니다. 서비스에서 해당 케이스를 다른 에러 코드로 매핑할지 검토해 주세요.src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (1)
3-5: 상태 네이밍 일관성 제안 (ACCEPTED/REJECTED vs ACCEPT/REJECT)groupjoin 영역의 GroupJoinStatus(ACCEPT, REJECT, PENDING, CANCEL)와 네이밍이 달라 혼선 가능성이 있습니다. 두 도메인의 상태 네이밍/시제(과거형 vs 원형)를 통일해 가독성과 유지보수성을 높이는 것을 권장합니다.
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2)
21-22: 중복 멤버 방지 및 성능을 위한 유니크 제약/인덱스 권장(group_id, user_id) 조합에 대해 DB 레벨 유니크 제약과 인덱스를 추가해 중복 멤버 삽입을 방지하고 조회 성능을 보장하는 것을 권장합니다. 예) group_members 테이블에 유니크 인덱스 및 인덱스 추가.
21-22: 네이밍 컨벤션 정합성동일 리포지토리의
existsByGroup_idAndUser_id와 본 메서드(findByGroup_IdAndUser_Id)의 네이밍 표기가 혼재합니다. 파생 쿼리 메서드의 프로퍼티 구분 표기를 일관되게(Group_Id형태) 맞추길 권장합니다.src/main/java/project/flipnote/group/service/GroupService.java (1)
72-72: 반환 타입 boolean 사용 권장래퍼 타입(Boolean) 대신 원시 타입(boolean)을 사용해 불필요한 박싱을 피하고 널 가능성도 제거하세요.
- public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) { + public boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) {src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (1)
11-15: 향후 확장성 고려: default 분기에서 예외 처리현 시점엔 두 상태만 존재하나, 추후 상태 추가 시 안전을 위해 default에서 명시적 예외를 던지는 것이 좋습니다.
public GroupInvitationStatus toEntityStatus() { - return switch (status) { - case ACCEPTED -> GroupInvitationStatus.ACCEPTED; - case REJECTED -> GroupInvitationStatus.REJECTED; - }; + return switch (status) { + case ACCEPTED -> GroupInvitationStatus.ACCEPTED; + case REJECTED -> GroupInvitationStatus.REJECTED; + default -> throw new IllegalArgumentException("지원하지 않는 상태입니다: " + status); + }; }src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (2)
28-30: 메서드 네이밍 컨벤션 준수 (camelCase)Java 컨벤션에 맞춰 메서드명을 소문자 시작으로 변경해 주세요.
- public void HandleUserRegisteredEvent(UserRegisteredEvent event) { + public void handleUserRegisteredEvent(UserRegisteredEvent event) {
23-26: 재시도 대상(Exception 범위) 한정 권장현재 모든 예외에 대해 재시도합니다. 일시적 오류(예: 데이터 접근 일시 실패)에 한정하는 것이 바람직합니다. 필요 시
retryFor/exclude를 활용해 비가역(BizException 등) 예외는 재시도에서 제외하세요.@Retryable( - maxAttempts = 3, - backoff = @Backoff(delay = 2000, multiplier = 2) + maxAttempts = 3, + retryFor = { org.springframework.dao.TransientDataAccessException.class }, + backoff = @Backoff(delay = 2000, multiplier = 2) )src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (1)
16-17: createdAt 직렬화 포맷에 타임존/표준(ISO-8601) 명시 권장서버/클라이언트 타임존 불일치로 혼선이 생길 수 있습니다. ISO-8601(예: OffsetDateTime/Instant) 사용 또는 timezone 속성 명시를 권장합니다.
예시:
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - LocalDateTime createdAt + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") // ISO-8601 with offset + OffsetDateTime createdAt또는 전역 Jackson 설정으로 일관성 있게 관리하는 방법도 고려해 주세요.
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (2)
25-26: 파라미터 명명 일관성(nit): InviteeEmail → inviteeEmail코드 컨벤션에 맞춰 소문자 카멜 케이스로 정리해 주세요.
-List<GroupInvitation> findAllByInviteeEmailAndStatus(String InviteeEmail, GroupInvitationStatus status); +List<GroupInvitation> findAllByInviteeEmailAndStatus(String inviteeEmail, GroupInvitationStatus status);
13-26: 쿼리 성능을 위한 인덱스 점검다음 조건들에 대한 인덱스를 GroupInvitation 엔티티에 추가하는 것을 권장합니다:
- (groupId, status), (groupId, inviteeUserId), (groupId, inviteeEmail), (inviteeEmail, status)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
53-54: 삭제/응답 API는 204 No Content 고려 (선택)성공 시 바디가 없으므로 204가 더 적합합니다. 현행 200도 무방합니다.
-return ResponseEntity.ok().build(); +return ResponseEntity.noContent().build();Also applies to: 65-66
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
222-231: 게스트 초대 생성 시 이메일 정규화 보장상위에서 정규화된 이메일을 넘기는 것이 바람직하지만, 안전하게 메서드 내에서도 방어 코드를 두는 것을 권장합니다.
private Long createGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { + String email = inviteeEmail == null ? null : inviteeEmail.trim().toLowerCase(java.util.Locale.ROOT); - if (groupInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { + if (groupInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, email)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } ... - .inviteeEmail(inviteeEmail) + .inviteeEmail(email)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
src/main/java/project/flipnote/auth/service/AuthService.java(2 hunks)src/main/java/project/flipnote/common/config/SwaggerConfig.java(2 hunks)src/main/java/project/flipnote/common/event/UserRegisteredEvent.java(1 hunks)src/main/java/project/flipnote/common/response/PageResponse.java(1 hunks)src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java(1 hunks)src/main/java/project/flipnote/group/controller/GroupInvitationController.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitation.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupErrorCode.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java(1 hunks)src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java(1 hunks)src/main/java/project/flipnote/group/model/UserIdNickname.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(1 hunks)src/main/java/project/flipnote/group/service/GroupService.java(2 hunks)src/main/java/project/flipnote/user/repository/UserProfileRepository.java(2 hunks)src/main/java/project/flipnote/user/service/UserService.java(5 hunks)src/main/resources/application.yml(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (20)
src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (4)
src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java (1)
GroupPermissionStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/group/entity/GroupMemberRole.java (1)
GroupMemberRole(3-5)
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (3)
src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (2)
Getter(22-65)updateStatus(62-64)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)
src/main/java/project/flipnote/auth/service/AuthService.java (3)
src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java (3)
userAuth(31-34)Async(23-35)Slf4j(16-41)src/main/java/project/flipnote/auth/entity/UserAuth.java (1)
Getter(18-76)src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (1)
Modifying(32-34)
src/main/java/project/flipnote/group/model/UserIdNickname.java (2)
src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)src/main/java/project/flipnote/user/model/UserInfoResponse.java (1)
UserInfoResponse(5-14)
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (8)
src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)src/main/java/project/flipnote/common/dto/UserCreateCommand.java (1)
UserCreateCommand(3-12)src/main/java/project/flipnote/auth/model/UserRegisterRequest.java (1)
UserRegisterRequest(11-40)src/main/java/project/flipnote/auth/model/UserRegisterResponse.java (1)
UserRegisterResponse(3-10)src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java (1)
EmailVerificationSendEvent(3-7)src/main/java/project/flipnote/auth/event/PasswordResetCreateEvent.java (1)
PasswordResetCreateEvent(3-7)src/main/java/project/flipnote/auth/model/UserLoginRequest.java (1)
UserLoginRequest(7-15)src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java (1)
PasswordResetCreateRequest(6-10)
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (2)
Repository(12-19)existsByGroup_idAndUser_id(18-18)src/main/java/project/flipnote/group/entity/GroupMember.java (1)
Getter(20-48)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (4)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (3)
GroupJoinErrorCode(76-76)GroupErrorCode(64-64)GroupErrorCode(52-52)src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (2)
Getter(8-26)Override(22-25)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)src/main/java/project/flipnote/user/exception/UserErrorCode.java (1)
Getter(9-24)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (1)
src/main/java/project/flipnote/common/security/jwt/JwtComponent.java (1)
SecurityErrorCode(111-111)
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (7)
src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (1)
CreateCardSetResponse(4-10)
src/main/java/project/flipnote/user/repository/UserProfileRepository.java (2)
src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (5)
findByEmailAndStatus(17-17)findByUserIdAndStatus(27-27)UserAuthRepository(13-35)findByIdAndStatus(25-25)Modifying(21-23)src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Builder(53-69)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (4)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java (4)
Slf4j(16-41)Async(23-35)Recover(37-40)userAuth(31-34)src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java (1)
EmailVerificationEventListener(17-39)src/main/java/project/flipnote/auth/listener/PasswordResetCreateEventListener.java (2)
PasswordResetCreateEventListener(17-39)handlePasswordResetCreateEvent(24-33)
src/main/java/project/flipnote/group/service/GroupService.java (5)
src/main/java/project/flipnote/group/entity/GroupPermission.java (2)
Entity(12-29)Builder(25-28)src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java (1)
Repository(9-12)src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java (1)
Repository(11-14)src/main/java/project/flipnote/group/entity/GroupRolePermission.java (1)
Entity(11-41)src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
hasPermission(74-85)
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (4)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRequest.java (1)
GroupJoinRequest(3-6)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondRequest.java (1)
GroupJoinRespondRequest(5-8)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (4)
src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java (3)
AuthControllerDocs(19-53)Operation(45-46)Operation(30-31)src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java (1)
Tag(10-28)src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)src/main/java/project/flipnote/group/controller/GroupController.java (1)
RequiredArgsConstructor(18-31)
src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (3)
src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java (1)
EmailVerificationRequest(6-11)src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java (1)
PasswordResetCreateRequest(6-10)src/main/java/project/flipnote/auth/model/EmailVerifyRequest.java (1)
EmailVerifyRequest(8-17)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
RequiredArgsConstructor(27-80)src/main/java/project/flipnote/user/service/UserService.java (1)
RequiredArgsConstructor(27-118)src/main/java/project/flipnote/group/controller/GroupController.java (1)
RequiredArgsConstructor(18-31)
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (6)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (2)
Getter(8-26)Override(22-25)src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1)
CommonErrorCode(8-17)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
ErrorCode(3-10)src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java (1)
GroupPermissionStatus(3-5)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (2)
Getter(22-65)Builder(46-60)
src/main/java/project/flipnote/user/service/UserService.java (1)
src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (2)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
Repository(12-19)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)
🔇 Additional comments (7)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (1)
12-12: SECURITY_001 적용 확인 완료 및 중복 없음
- 기존 “SECURITY_0021” 참조는 모두 제거됨
- “SECURITY_001”은
SecurityErrorCode.java내 TOKEN_EXPIRED 정의 부분에서만 사용 중이며, 다른 중복 정의나 참조가 없음- TOKEN_EXPIRED 는
JwtComponent.java(103행) 에서만 참조됨코드 변경 자체에는 문제가 없으므로 머지 가능합니다.
다만, 클라이언트(웹/앱) 및 API 문서/Swagger, 프론트·앱 상수 등 외부 연동 지점도 함께 동기화해 주세요.src/main/java/project/flipnote/group/exception/GroupErrorCode.java (1)
14-15: USER_NOT_IN_GROUP의 HTTP 상태 재검토현재 NOT_FOUND를 사용하고 있습니다. 권한 맥락에서는 403(FORBIDDEN)도 후보가 될 수 있습니다. 다만 GroupJoinErrorCode와의 일관성(동일 메시지/NOT_FOUND 유지)도 장점이므로 팀 컨벤션을 확정해 주세요.
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
21-22: ID 기반 조회 메서드 추가 적절권한 체크에서 엔티티 참조 없이 ID로 조회 가능한 점이 명확하고 효율적입니다. 사용 목적에 부합합니다.
src/main/java/project/flipnote/user/repository/UserProfileRepository.java (1)
20-21: 활성 사용자 이메일 조회 메서드 적절기존 패턴(findByXAndStatus)에 부합하며 의도가 명확합니다.
src/main/java/project/flipnote/group/service/GroupService.java (1)
80-83: getReferenceById 활용 Good읽기 전용 존재 여부 확인에 프록시 참조를 사용하는 선택이 적절합니다. 불필요한 SELECT를 줄이는 데 도움이 됩니다.
src/main/java/project/flipnote/common/response/PageResponse.java (1)
19-31: 페이지 변환 로직 명확하며 적절Spring Data Page를 API 스펙으로 잘 매핑했습니다. 실사용에 충분합니다.
src/main/java/project/flipnote/user/service/UserService.java (1)
55-59: 내부 헬퍼findActiveUserByIdOrThrow적용 리팩터링 LGTM중복 제거와 예외 일관성 측면에서 개선됐습니다. 호출부 교체 문제 없습니다.
Also applies to: 63-73, 75-79, 81-85
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/group/controller/GroupInvitationController.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/group/entity/GroupInvitation.java
Outdated
Show resolved
Hide resolved
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | ||
|
|
||
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( |
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
GroupPermission null 안전성 보강
findByName가 null을 반환할 경우 NPE 위험이 있습니다. 존재하지 않을 때 예외를 명시적으로 던지거나 Optional 기반 메서드를 도입하세요.
- GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ if (groupPermission == null) {
+ // TODO: 적절한 에러코드로 교체
+ throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER);
+ }📝 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.
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | |
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( | |
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | |
| if (groupPermission == null) { | |
| // TODO: 적절한 에러코드로 교체 | |
| throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER); | |
| } | |
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( |
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/service/GroupService.java around lines
77 to 79, calling groupPermissionRepository.findByName(...) can return null
which may cause an NPE; update the method to handle absence explicitly by either
(a) changing the repository method to return Optional<GroupPermission> and use
orElseThrow(...) to throw a clear, domain-specific exception when not found, or
(b) check the returned value for null and throw a specific RuntimeException
(e.g., GroupPermissionNotFoundException) before proceeding to call
groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(...);
ensure the thrown exception contains the missing permission name for debugging.
src/main/java/project/flipnote/user/repository/UserProfileRepository.java
Show resolved
Hide resolved
| public Optional<UserProfile> findActiveUserByEmail(String email) { | ||
| return userProfileRepository.findByEmailAndStatus(email, UserStatus.ACTIVE); | ||
| } |
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
이메일 조회는 대소문자/공백 정규화 필요
이메일은 대소문자 구분 없이 취급되는 경우가 많습니다. 저장 시 소문자 통일을 가정하지 않는다면, 조회 전 정규화(trim + toLowerCase(Locale.ROOT)) 또는 Repository 메서드에 IgnoreCase 파생 쿼리 사용을 권장합니다.
적용 예시:
public Optional<UserProfile> findActiveUserByEmail(String email) {
- return userProfileRepository.findByEmailAndStatus(email, UserStatus.ACTIVE);
+ if (email == null) return Optional.empty();
+ String normalized = email.trim().toLowerCase(java.util.Locale.ROOT);
+ return userProfileRepository.findByEmailAndStatus(normalized, UserStatus.ACTIVE);
}Repository에 findByEmailIgnoreCaseAndStatus 추가도 대안입니다.
🏁 Script executed:
#!/bin/bash
# UserProfileRepository 인터페이스 위치 확인
rg "interface UserProfileRepository" -n src
# 현재 정의된 findByEmailAndStatus 메서드 검색
rg "findByEmailAndStatus" -n src
# IgnoreCase 파생 쿼리 메서드 존재 여부 확인
rg "findByEmailIgnoreCaseAndStatus" -n srcLength of output: 873
🏁 Script executed:
#!/bin/bash
# Repository에 저장될 때 이메일이 정규화되는지 확인
rg "userProfileRepository.save" -n src
# 엔티티에서 이메일 필드 정의 확인
rg "class UserProfile" -n src
# 엔티티에서 이메일 설정 메서드 확인
rg "setEmail" -n src
# 전체 코드에서 toLowerCase 호출 여부 확인
rg "toLowerCase" -n srcLength of output: 787
이메일 조회 시 정규화 필요 (대소문자·공백 처리)
UserService.findActiveUserByEmail 메서드
(파일: src/main/java/project/flipnote/user/service/UserService.java, 93라인)에서 조회 전 입력 이메일을 trim() 및 toLowerCase(Locale.ROOT)로 정규화하거나, 리포지토리 메서드를 findByEmailIgnoreCaseAndStatus로 변경해야 합니다.
예시:
public Optional<UserProfile> findActiveUserByEmail(String email) {
- return userProfileRepository.findByEmailAndStatus(email, UserStatus.ACTIVE);
+ if (email == null || email.isBlank()) {
+ return Optional.empty();
+ }
+ String normalized = email.trim().toLowerCase(Locale.ROOT);
+ return userProfileRepository.findByEmailAndStatus(normalized, UserStatus.ACTIVE);
}또는 리포지토리에 아래 메서드를 추가:
Optional<UserProfile> findByEmailIgnoreCaseAndStatus(String email, UserStatus status);추가로, 회원가입/저장 로직에서도 엔티티 저장 전 이메일을 정규화 처리하는 방법을 고려하세요.
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/user/service/UserService.java around lines 92
to 94, the findActiveUserByEmail method queries raw input email which can fail
due to case or surrounding whitespace; normalize the input before querying by
applying trim() and toLowerCase(Locale.ROOT) (or alternatively change the
repository call to use a case-insensitive finder such as
findByEmailIgnoreCaseAndStatus and add that method to the repository), and also
ensure signup/persist logic normalizes emails before saving the UserProfile
entity so stored values are consistent.
| public Map<Long, String> getIdAndNicknames(List<Long> inviteeUserIds) { | ||
| List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds); | ||
|
|
||
| return idAndNicknames.stream() | ||
| .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); | ||
| } |
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 ID 방어 및 중복 제거 필요
현재 null ID가 포함되면 IN 절 바인딩 오류가 발생할 수 있습니다. 또한 중복 ID는 불필요한 조회를 유발합니다. 빈 입력일 때의 조기 반환도 함께 권장합니다.
-public Map<Long, String> getIdAndNicknames(List<Long> inviteeUserIds) {
- List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds);
-
- return idAndNicknames.stream()
- .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname));
-}
+public Map<Long, String> getIdAndNicknames(List<Long> inviteeUserIds) {
+ if (inviteeUserIds == null || inviteeUserIds.isEmpty()) {
+ return java.util.Collections.emptyMap();
+ }
+ List<Long> ids = inviteeUserIds.stream()
+ .filter(java.util.Objects::nonNull)
+ .distinct()
+ .toList();
+ if (ids.isEmpty()) {
+ return java.util.Collections.emptyMap();
+ }
+ List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(ids);
+ return idAndNicknames.stream()
+ .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname));
+}📝 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.
| public Map<Long, String> getIdAndNicknames(List<Long> inviteeUserIds) { | |
| List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds); | |
| return idAndNicknames.stream() | |
| .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); | |
| } | |
| public Map<Long, String> getIdAndNicknames(List<Long> inviteeUserIds) { | |
| - List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds); | |
| - | |
| - return idAndNicknames.stream() | |
| - .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); | |
| + if (inviteeUserIds == null || inviteeUserIds.isEmpty()) { | |
| + return java.util.Collections.emptyMap(); | |
| + } | |
| + | |
| + List<Long> ids = inviteeUserIds.stream() | |
| + .filter(java.util.Objects::nonNull) | |
| + .distinct() | |
| + .toList(); | |
| + if (ids.isEmpty()) { | |
| + return java.util.Collections.emptyMap(); | |
| + } | |
| + | |
| + List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(ids); | |
| + return idAndNicknames.stream() | |
| + .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/user/service/UserService.java around lines 112
to 117, guard against empty input, null IDs and duplicate IDs before calling the
repository: if inviteeUserIds is null or empty return an empty map immediately;
otherwise filter out null entries and create a set (or otherwise deduplicate) to
avoid duplicate bindings on the IN clause; call findIdAndNicknameByIdIn with the
cleaned/deduped list; and when collecting the result into a map handle potential
duplicate keys by keeping the first value (or explicitly resolve collisions) and
ensure you return an empty map if the repository returns 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.
Actionable comments posted: 24
🧹 Nitpick comments (19)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (1)
9-20: ErrorCode 구현의 status 타입 일관성 제안현재 Security/Common은
int status, Auth/CardSet은HttpStatus를 들고getStatus()로 int 변환합니다. 팀 규칙에 맞춰 한쪽으로 통일하면 가독성과 유지보수성이 좋아집니다. 보수적 선택은 현 상태 유지, 일관성 지향이면 아래와 같이 HttpStatus 보유 패턴 권장.예시(참고용):
@Getter @RequiredArgsConstructor public enum SecurityErrorCode implements ErrorCode { TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "SECURITY_001", "토큰이 만료되었습니다."), NOT_VALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "SECURITY_002", "올바르지 않은 토큰입니다."), UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "SECURITY_003", "인증이 필요합니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "SECURITY_004", "권한이 없습니다."); private final HttpStatus httpStatus; private final String code; private final String message; @Override public int getStatus() { return httpStatus.value(); } }src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (1)
3-6: 다른 Response 레코드들과의 일관성: 정적 팩토리 추가 제안프로젝트 내 Response 레코드들에서 from(…) 정적 팩토리를 제공하는 패턴이 많습니다. 동일 패턴을 적용하면 가독성과 일관성이 좋아집니다.
적용 예(diff):
public record GroupInvitationCreateResponse( Long invitationId ) { + public static GroupInvitationCreateResponse from(Long invitationId) { + return new GroupInvitationCreateResponse(invitationId); + } }src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (1)
6-10: 이메일 최대 길이 제한 추가 제안RFC 기준 이메일 길이 상한(일반적으로 254자)을 함께 검증하면 DB 컬럼 제약/인덱스와의 불일치 이슈를 줄일 수 있습니다.
적용 예(diff):
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; public record GroupInvitationCreateRequest( - @Email @NotBlank + @Email @NotBlank @Size(max = 254) String email ) { }src/main/java/project/flipnote/common/response/PageResponse.java (1)
7-17: 현업에서 자주 쓰는 메타 추가 제안: numberOfElements응답에 현재 페이지의 원소 수(numberOfElements)를 포함하면 FE에서 별도 계산 없이 바로 사용 가능합니다.
적용 예(diff):
public record PageResponse<T>( List<T> content, int page, int size, long totalElements, int totalPages, + int numberOfElements, boolean first, boolean last, boolean hasNext, boolean hasPrevious ) { public static <T> PageResponse<T> from(Page<T> page) { return new PageResponse<>( page.getContent(), page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages(), + page.getNumberOfElements(), page.isFirst(), page.isLast(), page.hasNext(), page.hasPrevious() ); } }Also applies to: 19-31
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
21-21: 파생 쿼리 네이밍 일관성 제안동일 리포지토리 내
existsByGroup_idAndUser_id와 이번findByGroup_IdAndUser_Id가 스타일이 달라(대소/언더스코어 조합) 가독성이 떨어집니다. 한쪽 스타일로 통일을 고려해 주세요. 예)existsByGroup_IdAndUser_Id.src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (1)
3-5: 응답용 enum 분리 적절API 입력 전용으로 ACCEPTED/REJECTED만 제공하는 분리가 명확합니다. Swagger 문서화를 위해 @Schema(description) 추가도 고려해 볼 수 있습니다.
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (1)
6-16: API→엔티티 상태 매핑 명확입력 enum을 엔티티 enum으로 명확히 매핑하고 있어 좋습니다. 추후 enum 값 추가 시 컴파일 타임에 누락을 감지할 수 있습니다.
Swagger 문서 가독성을 위해
@Schema(description, example) 추가를 고려해 주세요.src/main/java/project/flipnote/user/service/UserService.java (1)
112-117: Map 변환 시 중복 키 방어 로직 고려
Collectors.toMap은 키 중복 시IllegalStateException을 던집니다. 입력 리스트에 중복 ID가 들어올 가능성이 전혀 없다면 괜찮지만, 방어적으로 merge 함수(예: 첫 값 유지)를 지정해도 좋습니다.예:
.collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname, (a,b) -> a))src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (1)
20-28: 빈 문자열 대신 null 반환 + 직렬화 정책 정리 제안서비스에서 닉네임이 없을 때 빈 문자열을 넣고 있어 응답 소비자가 “값 없음”과 빈 문자열을 구분하기 어렵습니다. null을 사용하고 @JsonInclude(Include.NON_NULL)로 직렬화 제외를 권장합니다.
예시:
+import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + - public record GroupInvitationResponse( +@JsonInclude(Include.NON_NULL) +public record GroupInvitationResponse( ... - String inviteeNickname, + String inviteeNickname, ... ) { public static GroupInvitationResponse from(GroupInvitation invitation, String inviteeNickname) { - return new GroupInvitationResponse(..., inviteeNickname, ...); + return new GroupInvitationResponse(..., (inviteeNickname == null || inviteeNickname.isBlank()) ? null : inviteeNickname, ...); } }src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (1)
28-30: 메서드 네이밍 컨벤션 준수메서드명은 lowerCamelCase가 관례입니다. 동일 프로젝트의 다른 리스너들과도 일관되지 않습니다.
- public void HandleUserRegisteredEvent(UserRegisteredEvent event) { + public void handleUserRegisteredEvent(UserRegisteredEvent event) {src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (1)
13-16: 오류 코드 보강 제안: 상태/중복/멤버십 관련 시나리오다음 케이스에 대한 전용 코드가 있으면 핸들링과 응답을 명확히 할 수 있습니다.
- 이미 처리된 초대(상태 전이 불가)
- 이미 그룹 멤버인 사용자
- 그룹 정원 초과
예시 상수:
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."), CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."); + ,INVITATION_ALREADY_PROCESSED(HttpStatus.CONFLICT, "GROUP_INVITATION_005", "이미 처리된 초대입니다.") + ,ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_INVITATION_006", "이미 그룹 멤버입니다.") + ,GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_INVITATION_007", "그룹 정원이 가득 찼습니다.")서비스/엔티티 전이 검증과 함께 사용하면 응답 일관성이 좋아집니다.
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (2)
25-25: 파라미터 네이밍 컨벤션(Naming) 정리메서드 파라미터명이 대문자로 시작합니다. Java 컨벤션에 맞게 소문자로 시작하도록 정리해 주세요. 기능엔 영향 없으나 가독성/일관성 이슈입니다.
- List<GroupInvitation> findAllByInviteeEmailAndStatus(String InviteeEmail, GroupInvitationStatus status); + List<GroupInvitation> findAllByInviteeEmailAndStatus(String inviteeEmail, GroupInvitationStatus status);
21-23: 긴 시그니처 가독성 개선(옵션)findByIdAndGroupIdAndInviteeUserIdAndStatus는 길어 가독성이 떨어집니다. Query 메서드를 유지하더라도 개행 정리 또는 @query 사용을 검토해 주세요.
- Optional<GroupInvitation> findByIdAndGroupIdAndInviteeUserIdAndStatus(Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status); + Optional<GroupInvitation> findByIdAndGroupIdAndInviteeUserIdAndStatus( + Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status + );src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
19-20: Swagger 파라미터 가독성 개선 및 AuthPrinciple 숨김 권장
- 문서 상에서 AuthPrinciple은 노출할 필요가 없으니 hidden 처리 권장.
- page/size 기본값, 제약 조건을 명시하면 API 사용성이 좋아집니다.
예시(diff):
- ResponseEntity<GroupInvitationCreateResponse> createGroupInvitation( - Long groupId, GroupInvitationCreateRequest req, AuthPrinciple authPrinciple - ); + ResponseEntity<GroupInvitationCreateResponse> createGroupInvitation( + @Parameter(description = "그룹 ID", example = "1") Long groupId, + GroupInvitationCreateRequest req, + @Parameter(hidden = true) AuthPrinciple authPrinciple + ); - ResponseEntity<Void> deleteGroupInvitation(Long groupId, Long invitationId, AuthPrinciple authPrinciple); + ResponseEntity<Void> deleteGroupInvitation( + @Parameter(description = "그룹 ID", example = "1") Long groupId, + @Parameter(description = "초대 ID", example = "10") Long invitationId, + @Parameter(hidden = true) AuthPrinciple authPrinciple + ); - ResponseEntity<Void> respondToGroupInvitation( - Long groupId, Long invitationId, GroupInvitationRespondRequest req, AuthPrinciple authPrinciple - ); + ResponseEntity<Void> respondToGroupInvitation( + @Parameter(description = "그룹 ID", example = "1") Long groupId, + @Parameter(description = "초대 ID", example = "10") Long invitationId, + GroupInvitationRespondRequest req, + @Parameter(hidden = true) AuthPrinciple authPrinciple + ); - ResponseEntity<PageResponse<GroupInvitationResponse>> getGroupInvitations( - Long groupId, - int page, - int size, - AuthPrinciple authPrinciple - ); + ResponseEntity<PageResponse<GroupInvitationResponse>> getGroupInvitations( + @Parameter(description = "그룹 ID", example = "1") Long groupId, + @Parameter(description = "페이지 번호(0-base)", example = "0") int page, + @Parameter(description = "페이지 크기", example = "20") int size, + @Parameter(hidden = true) AuthPrinciple authPrinciple + );추가 임포트:
import io.swagger.v3.oas.annotations.Parameter;Also applies to: 23-24, 27-28, 31-36
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (3)
45-54: DELETE 응답 코드는 204 No Content 권장리소스 삭제 성공 시 빈 본문과 함께 204가 더 적절합니다. 현재 200 OK를 반환하고 있어 REST 관례와 다릅니다.
@DeleteMapping("/{invitationId}") public ResponseEntity<Void> deleteGroupInvitation( @PathVariable("groupId") Long groupId, @PathVariable("invitationId") Long invitationId, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { groupInvitationService.deleteGroupInvitation(authPrinciple.userId(), groupId, invitationId); - return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build(); }
56-66: PATCH 응답도 204 No Content 권장상태 변경 성공 후 반환 본문이 없다면 204가 더 적절합니다.
@PatchMapping("/{invitationId}") public ResponseEntity<Void> respondToGroupInvitation( @PathVariable("groupId") Long groupId, @PathVariable("invitationId") Long invitationId, @Valid @RequestBody GroupInvitationRespondRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { groupInvitationService.respondToGroupInvitation(authPrinciple.userId(), groupId, invitationId, req); - return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build(); }
34-43: 201 Created 시 Location 헤더 설정 고려생성 성공 시, 생성된 리소스의 URI를 Location 헤더에 제공하면 클라이언트 UX가 좋아집니다.
예시(응답 본문은 유지하고 Location만 추가):
@PostMapping public ResponseEntity<GroupInvitationCreateResponse> createGroupInvitation( @PathVariable("groupId") Long groupId, @Valid @RequestBody GroupInvitationCreateRequest req, @AuthenticationPrincipal AuthPrinciple authPrinciple ) { GroupInvitationCreateResponse res = groupInvitationService.createGroupInvitation(authPrinciple, groupId, req); - return ResponseEntity.status(HttpStatus.CREATED).body(res); + URI location = URI.create(String.format("/v1/groups/%d/invitations/%d", groupId, res.getInvitationId())); + return ResponseEntity.created(location).body(res); }참고: GroupInvitationCreateResponse 접근자 명(res.getInvitationId() vs res.invitationId())는 실제 타입에 맞춰 조정 필요.
src/main/java/project/flipnote/group/service/GroupInvitationService.java (2)
112-117: enum 비교는 == 사용 권장자바 enum 비교는 equals 대신 ==가 관례적이며, NPE 위험도 없습니다.
- if (Objects.equals(invitation.getStatus(), GroupInvitationStatus.ACCEPTED)) { + if (invitation.getStatus() == GroupInvitationStatus.ACCEPTED) { // TODO: GroupMember 에서 group과 user의 id만 가지고 있도록 수정 addGroupMember(inviteeUserId, groupId); }
222-236: 게스트 초대 중복 조건 보완(이메일/회원 초대 교차 케이스)게스트 초대가 이미 존재하는 상태에서 동일 이메일의 사용자가 가입 후 회원 초대를 생성하려는 경우(혹은 그 반대), ALREADY_INVITED 검사가 유형별로만 이뤄져 중복 초대를 허용할 수 있습니다. 이메일 기준의 중복도 함께 고려하는 것이 안전합니다.
예시(둘 중 한쪽에 교차 검사 추가):
private Long createGuestInvitation(Long inviterUserId, Long groupId, String inviteeEmail) { + // 해당 이메일의 활성 사용자 존재 여부 확인 후, 회원 초대 중복도 함께 차단 + userService.findActiveUserByEmail(inviteeEmail).ifPresent(inviteeUser -> { + if (groupInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { + throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); + } + if (groupMemberRepository.existsByGroupIdAndUserId(groupId, inviteeUser.getId())) { + throw new BizException(GroupInvitationErrorCode.ALREADY_MEMBER); + } + }); if (groupInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
src/main/java/project/flipnote/auth/service/AuthService.java(2 hunks)src/main/java/project/flipnote/common/config/SwaggerConfig.java(2 hunks)src/main/java/project/flipnote/common/event/UserRegisteredEvent.java(1 hunks)src/main/java/project/flipnote/common/response/PageResponse.java(1 hunks)src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java(1 hunks)src/main/java/project/flipnote/group/controller/GroupInvitationController.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitation.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupErrorCode.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java(1 hunks)src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java(1 hunks)src/main/java/project/flipnote/group/model/UserIdNickname.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(1 hunks)src/main/java/project/flipnote/group/service/GroupService.java(2 hunks)src/main/java/project/flipnote/user/repository/UserProfileRepository.java(2 hunks)src/main/java/project/flipnote/user/service/UserService.java(5 hunks)src/main/resources/application.yml(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (23)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (3)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/user/service/UserService.java (1)
RequiredArgsConstructor(27-118)src/main/java/project/flipnote/group/controller/GroupController.java (2)
RequiredArgsConstructor(18-31)PostMapping(24-30)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (3)
src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java (3)
AuthControllerDocs(19-53)Operation(45-46)Operation(39-40)src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java (1)
Tag(10-28)src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (3)
src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java (1)
EmailVerificationSendEvent(3-7)src/main/java/project/flipnote/auth/event/PasswordResetCreateEvent.java (1)
PasswordResetCreateEvent(3-7)
src/main/java/project/flipnote/group/service/GroupService.java (5)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
hasPermission(74-85)src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java (1)
Repository(9-12)src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java (2)
Repository(11-14)existsByGroupAndRoleAndGroupPermission(13-13)src/main/java/project/flipnote/group/entity/GroupPermission.java (1)
Entity(12-29)src/main/java/project/flipnote/group/entity/GroupRolePermission.java (1)
Entity(11-41)
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (2)
src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (2)
Getter(22-65)updateStatus(62-64)
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (7)
src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)
src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (7)
src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java (1)
GroupPermissionStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (1)
Getter(8-26)src/main/java/project/flipnote/group/entity/GroupPermission.java (1)
Entity(12-29)
src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (3)
src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java (1)
EmailVerificationRequest(6-11)src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java (1)
PasswordResetCreateRequest(6-10)src/main/java/project/flipnote/auth/model/EmailVerifyRequest.java (1)
EmailVerifyRequest(8-17)
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (2)
Repository(12-19)existsByGroup_idAndUser_id(18-18)
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (5)
src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondRequest.java (1)
GroupJoinRespondRequest(5-8)
src/main/java/project/flipnote/user/service/UserService.java (2)
src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (1)
findByEmailAndStatus(17-17)
src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (9)
src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/group/model/GroupCreateRequest.java (1)
GroupCreateRequest(6-29)src/main/java/project/flipnote/user/model/UserUpdateResponse.java (1)
UserUpdateResponse(5-18)
src/main/java/project/flipnote/group/model/UserIdNickname.java (3)
src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
Repository(12-19)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (5)
src/main/java/project/flipnote/common/security/jwt/JwtComponent.java (1)
SecurityErrorCode(111-111)src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1)
CommonErrorCode(8-17)src/main/java/project/flipnote/auth/exception/AuthErrorCode.java (1)
Getter(9-36)src/main/java/project/flipnote/common/security/exception/CustomSecurityException.java (1)
CustomSecurityException(7-12)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)
src/main/java/project/flipnote/user/repository/UserProfileRepository.java (3)
src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (5)
UserAuthRepository(13-35)findByEmailAndStatus(17-17)findByUserIdAndStatus(27-27)Modifying(21-23)findByIdAndStatus(25-25)src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
Repository(12-19)
src/main/java/project/flipnote/common/response/PageResponse.java (10)
src/main/java/project/flipnote/user/model/UserUpdateResponse.java (1)
UserUpdateResponse(5-18)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/user/model/SocialLinksResponse.java (1)
SocialLinksResponse(7-18)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/common/response/ApiResponse.java (1)
ApiResponse(14-64)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/image/model/ImageUploadResponseDto.java (1)
ImageUploadResponseDto(5-11)
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (5)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (2)
Getter(8-26)Override(22-25)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1)
CommonErrorCode(8-17)src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
ErrorCode(3-10)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (2)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
RequiredArgsConstructor(27-80)src/main/java/project/flipnote/user/service/UserService.java (1)
RequiredArgsConstructor(27-118)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (3)
src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (2)
Getter(8-26)Override(22-25)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (3)
GroupJoinErrorCode(76-76)GroupErrorCode(52-52)GroupErrorCode(64-64)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (5)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/auth/service/AuthService.java (1)
Slf4j(45-230)src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java (3)
Slf4j(16-41)Async(23-35)Recover(37-40)src/main/java/project/flipnote/auth/listener/PasswordResetCreateEventListener.java (1)
PasswordResetCreateEventListener(17-39)src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java (1)
EmailVerificationEventListener(17-39)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (6)
src/main/java/project/flipnote/group/repository/GroupRepository.java (1)
Repository(15-22)src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
Repository(12-19)src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java (1)
Repository(9-12)src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java (1)
Repository(11-14)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)
Repository(8-10)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (2)
Getter(22-65)Builder(46-60)
src/main/java/project/flipnote/auth/service/AuthService.java (3)
src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java (3)
Async(23-35)Slf4j(16-41)userAuth(31-34)src/main/java/project/flipnote/auth/entity/UserAuth.java (1)
Getter(18-76)src/main/java/project/flipnote/auth/model/UserRegisterResponse.java (1)
UserRegisterResponse(3-10)
🔇 Additional comments (10)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (2)
12-12: 오타 교정 및 코드 체계 정렬(LGTM)코드 체계를 001~004로 정렬한 점 좋습니다. 메시지/HTTP 상태도 적절합니다.
12-12: 호환성 문제 없음: ‘SECURITY_0021’ 직접 참조 미발견 및 변경 승인레포지토리 내에서 기존 코드값 “SECURITY_0021”에 대한 직접 참조를 검색한 결과가 없으며, 에러 코드를 이용하는 부분(
JwtComponent.java에서SecurityErrorCode.TOKEN_EXPIRED참조)에서도 문자열을 하드코딩하지 않고 상수를 사용하고 있어 외부/내부 호환성에 영향이 없습니다.
- 레포지토리 전체에 “SECURITY_0021” 직접 참조 없음
- TOKEN_EXPIRED 문자열은 상수 참조만 사용 중
- 문서·테스트 스냅샷 등에서 토큰 만료 메시지 하드코딩된 부분 미발견
변경을 승인하며, 외부 사용자에게 노출되는 계약이므로 마이그레이션 노트 또는 CHANGELOG에 해당 변경사항을 명시해 주세요.
src/main/java/project/flipnote/group/model/UserIdNickname.java (1)
3-6: LGTM — Spring Data Projection으로 적합UserProfile의 id/nickname과 정확히 매칭되는 단순 프로젝션으로 적절합니다.
src/main/java/project/flipnote/common/response/PageResponse.java (1)
7-17: 페이지 번호 0/1 기반 합의 확인 필요page가 Page.getNumber()를 그대로 노출하므로 0-베이스입니다. FE가 1-베이스를 기대한다면 여기서 +1 하거나, 컨트롤러/문서에서 명시해야 합니다.
FE 기대값 확인(문서/스웨거/기존 API) 후 필요한 경우 변환 적용을 권장합니다.
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (1)
3-7: 레코드 이벤트 정의 적절불변 이벤트 페이로드로 record 채택이 적절합니다. 추가 코멘트 없습니다.
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
21-21: ID 기반 조회 메서드 추가 좋습니다권한/멤버십 체크에 필요한 조회로 적절합니다.
src/main/java/project/flipnote/auth/service/AuthService.java (1)
38-38: 이벤트 타입 의존성 추가 OK회원가입 후속 플로우 확장을 위한 이벤트 의존성 추가는 적절합니다.
src/main/java/project/flipnote/user/repository/UserProfileRepository.java (1)
20-22: 신규 조회 메서드 추가 적절
findByEmailAndStatus는 활성 유저 조회에 유용합니다.findIdAndNicknameByIdIn은 필요한 필드만 투영해 효율적입니다.src/main/java/project/flipnote/user/service/UserService.java (2)
55-56: 활성 유저 조회 중앙화 좋습니다
findActiveUserByIdOrThrow로 조회/예외 처리를 일원화한 점이 가독성과 유지보수성에 이롭습니다.Also applies to: 63-64, 76-76, 82-82
92-94: 이메일 기반 활성 유저 조회 추가 적절후속 기능(초대/가입 연계)에 유용한 조회입니다.
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/group/controller/GroupInvitationController.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/group/entity/GroupInvitation.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/group/service/GroupInvitationService.java
Show resolved
Hide resolved
| public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) { | ||
| GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow( | ||
| () -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP) | ||
| ); | ||
|
|
||
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | ||
|
|
||
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( | ||
| groupRepository.getReferenceById(groupId), | ||
| groupMember.getRole(), | ||
| groupPermission | ||
| ); | ||
| } |
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
그룹/권한 미존재 케이스 명확화 및 NPE 방지 필요
findByGroup_IdAndUser_Id로 멤버십 검증은 적절합니다.groupPermissionRepository.findByName(...)가Optional이 아니므로, 권한이 미리 초기화되지 않은 경우 NPE가 날 수 있습니다. 예외를 명시적으로 던지거나 Optional로 감싸는 리팩터를 고려해 주세요.groupRepository.getReferenceById(groupId)는 존재하지 않는 그룹에도 프록시를 반환하여 최종 결과가 단순히 false가 될 수 있습니다. 그룹 미존재는 별도 에러로 구분하는 것이 UX에 유리합니다(예: GROUP_NOT_FOUND).- 반환 타입은 원시
boolean이면 충분합니다.
권장 리팩터(개념적 방향):
- 그룹 존재 체크:
findById(...).orElseThrow(...) - 권한 조회:
Optional<GroupPermission>로 바꾸거나 null 체크 후 명시적 예외 처리
그룹/권한 초기화 보장을 위해 초기 권한 세팅 로직이 확실히 실행되는지 확인:
🏁 Script executed:
#!/bin/bash
# 초기 권한 설정 메서드가 그룹 생성 시 호출되는지 검색
rg -n "initializeGroupPermissions\\(|GroupPermissionRepository\\.findAll\\(" -A 3Length of output: 930
파일: GroupService.java – 그룹/권한 유효성 검증 및 반환 타입 수정 필요
- 그룹 존재 여부를
findById로 먼저 검증해GROUP_NOT_FOUND를 명확히 구분 - 권한 조회 시
findByName→ Optional 혹은 명시적 예외 처리로 NPE 방지 (권한 미존재 시PERMISSION_NOT_FOUND) - 반환 타입을 primitive
boolean으로 변경
수정 예시:
- public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus status) {
- GroupMember member = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId)
- .orElseThrow(() -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP));
-
- GroupPermission perm = groupPermissionRepository.findByName(status);
-
- return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(
- groupRepository.getReferenceById(groupId),
- member.getRole(),
- perm
- );
- }
+ public boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus status) {
+ // 1) 그룹 존재 여부 검증
+ Group group = groupRepository.findById(groupId)
+ .orElseThrow(() -> new BizException(GroupErrorCode.GROUP_NOT_FOUND));
+
+ // 2) 사용자 멤버십 검증
+ GroupMember member = groupMemberRepository
+ .findByGroupAndUser(group, new User(userId))
+ .orElseThrow(() -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP));
+
+ // 3) 권한 존재 여부 검증
+ GroupPermission perm = groupPermissionRepository.findByName(status)
+ .orElseThrow(() -> new BizException(GroupErrorCode.PERMISSION_NOT_FOUND));
+
+ // 4) 최종 권한 체크
+ return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(
+ group,
+ member.getRole(),
+ perm
+ );
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/service/GroupService.java around lines
72 to 84, validate the group exists first using
groupRepository.findById(groupId) and throw
BizException(GroupErrorCode.GROUP_NOT_FOUND) if absent; change
groupPermissionRepository.findByName(...) to use an Optional (or null-check) and
throw BizException(GroupErrorCode.PERMISSION_NOT_FOUND) when the permission is
missing to avoid NPE; use the loaded Group entity (from findById) instead of
getReferenceById, and change the method return type from Boolean to primitive
boolean, returning the primitive result of
groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(...).
src/main/java/project/flipnote/user/repository/UserProfileRepository.java
Outdated
Show resolved
Hide resolved
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: 9
🧹 Nitpick comments (19)
src/main/java/project/flipnote/common/config/SwaggerConfig.java (1)
23-23: 서버 URL을 항상 강제 설정 시 프록시/컨텍스트 경로 환경에서 오작동 가능 — 값이 있을 때만 추가 고려리버스 프록시나 별도 컨텍스트 경로가 있는 배포에서 고정 URL이 실제 접근 경로와 달라질 수 있습니다. springdoc는 기본적으로 요청(및 X-Forwarded-*) 기반으로 서버 URL을 계산하므로, 프로퍼티가 지정된 경우에만 서버 항목을 추가하는 것이 안전합니다.
아래와 같이 조건부 추가를 고려해보세요(개념 예시):
OpenAPI openAPI = new OpenAPI(); if (serverUrl != null && !serverUrl.isBlank()) { openAPI.addServersItem(new Server().url(serverUrl)); } return openAPI .addSecurityItem(new SecurityRequirement().addList("access-token")) .components(new Components().addSecuritySchemes( "access-token", new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT") )) .info(apiInfo());이렇게 하면 프록시 환경에서는 자동 감지를 이용하고, 필요 시에만 명시적 서버를 적용할 수 있습니다.
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (1)
17-19: 상태 필드 타입 일관성 제안(HttpStatus 보관 방식)다른 에러코드(enum)들은 HttpStatus를 보관하고 getStatus에서 value()를 노출합니다. 본 enum은 int를 보관 중이라 유지보수 관점에서 혼선이 있을 수 있습니다. HttpStatus를 보관하도록 통일하는 리팩터링을 고려해 주세요.
src/main/java/project/flipnote/group/model/UserIdNickname.java (1)
3-6: Projection 용도 명확화 주석 제안Spring Data JPA 인터페이스 기반 프로젝션으로 보입니다. getId()/getNickname()이 실제 컬럼/프로퍼티명과 매칭됨을 주석으로 명시하면 가독성이 좋아집니다.
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (1)
3-5: 상태 전이 규칙 명시/검증 권장도메인 로직 측면에서 PENDING → ACCEPTED/REJECTED 전이만 허용 등 전이 규칙을 서비스 계층에서 명시/검증하고, 주석 또는 문서로 남겨두면 유지보수에 도움이 됩니다.
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (1)
3-7: 이벤트 페이로드(이메일) 정규화 및 주석 보강 제안
- 이메일은 퍼블리셔 측에서 trim/lowercase 등 정규화된 값인지 확인이 필요합니다(동일성 비교 오류 방지).
- 내부 이벤트라도 PII 포함이므로 사용 목적/범위를 간단히 주석으로 남겨두면 좋습니다.
퍼블리셔(AuthService 등)에서 이벤트 발행 시점에 이메일 정규화가 보장되는지 확인 부탁드립니다.
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (1)
15-15: 에러 코드 추가 적정, 모듈 간 메시지 중복 주의USER_NOT_IN_GROUP 추가는 서비스 검증에 유용합니다. 다만 groupjoin 모듈에도 유사 메시지가 있어, 클라이언트가 코드로 분기하는 경우 문서에서 차이를 분명히 해두는 것을 권장합니다.
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (1)
3-6: 응답 레코드의 팩토리 메서드 일관성다른 응답 모델들처럼 정적 팩토리 from(...)를 제공하면 컨트롤러/서비스 단에서 생성 일관성이 좋아집니다.
아래처럼 보강을 제안합니다:
public record GroupInvitationCreateResponse( Long invitationId ) { + public static GroupInvitationCreateResponse from(Long invitationId) { + return new GroupInvitationCreateResponse(invitationId); + } }src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (1)
3-10: 이메일 입력 정규화 및 Swagger 예제 추가 제안
- 이메일 비교/조회 일관성을 위해 trim + lowercase 정규화를 권장합니다(레코드의 compact canonical ctor 활용).
- API 문서 가독성을 위해 예제값 추가를 제안합니다.
아래 수정안을 고려해 주세요:
package project.flipnote.group.model; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import io.swagger.v3.oas.annotations.media.Schema; public record GroupInvitationCreateRequest( - @Email @NotBlank - String email + @Email @NotBlank + @Schema(example = "user@example.com") + String email ) { + // compact canonical constructor: 필드 할당 전에 매개변수 정규화 + public GroupInvitationCreateRequest { + if (email != null) { + email = email.trim().toLowerCase(); + } + } }src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
21-22: 멤버 조회 메서드 추가 LGTM그룹/유저 ID로 조회하는 선택지가 생겨 상위 서비스 코드 단순화에 도움이 됩니다.
기존
existsByGroup_idAndUser_id와 네이밍 컨벤션을 맞춰existsByGroup_IdAndUser_Id형태로 통일하는 것을 고려해 주세요(스프링 데이터 파서 일관성).src/main/java/project/flipnote/user/repository/UserProfileRepository.java (1)
3-3: 이메일/상태 조회, ID→닉네임 프로젝션 추가 LGTM두 메서드 모두 사용처 요구에 적합합니다. 대량 ID 입력 시 IN 절 한계가 있을 수 있으니 상위 서비스에서 빈 리스트 early-return, 대량 입력은 배치/청크 분할을 고려해 주세요.
Also applies to: 8-8, 20-23
src/main/java/project/flipnote/common/response/PageResponse.java (1)
1-32: 공통 페이징 응답 도입 LGTM필드 매핑이 정확합니다. 클라이언트 혼선을 줄이기 위해 page 번호가 0 기반임을 Swagger 스키마/문서에 명시하면 좋습니다.
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (1)
11-16: 검증 보장 및 매핑 위치 개선 제안
@NotNull이 적용되어 있으므로 컨트롤러에서@Valid가 반드시 활성화되어야 합니다. 해당 엔드포인트에@Valid적용 여부 확인 부탁드립니다.- 매핑 책임을
GroupInvitationResponseStatusenum 내부로 이동시키면 중복/분산을 줄일 수 있습니다. 예:status.toEntityStatus().컨트롤러 확인 체크리스트:
@RequestBody GroupInvitationRespondRequest req파라미터에@Valid적용 여부src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (2)
22-30: Retry 대상 축소 제안현재 설정은 비즈니스 예외 등 비회복성 예외까지 재시도될 수 있습니다. DB/트랜잭션 계열의 일시적 예외로 한정하세요.
예시:
- @Retryable( - maxAttempts = 3, - backoff = @Backoff(delay = 2000, multiplier = 2) - ) + @Retryable( + include = { + org.springframework.dao.ConcurrencyFailureException.class, + org.springframework.dao.DeadlockLoserDataAccessException.class, + org.springframework.dao.CannotAcquireLockException.class, + org.springframework.dao.TransientDataAccessResourceException.class + }, + maxAttempts = 3, + backoff = @Backoff(delay = 2000, multiplier = 2) + )또한, 재시도 시에도 멱등성을 보장하도록
GroupInvitationService에서 그룹 멤버 추가 전에 중복 여부를 검사하는 방어 로직을 권장합니다(예:existsByGroup_IdAndUser_Id활용).
32-35: PII(이메일) 로그 노출 최소화에러 로그에 이메일이 그대로 노출됩니다. 사용자 식별에는
userId만으로 충분하거나, 이메일을 마스킹하여 기록하는 것이 안전합니다.예시:
- log.error("회원가입 후속 처리 예외 발생: userId={}, email={}", event.userId(), event.email(), ex); + log.error("회원가입 후속 처리 예외 발생: userId={}, email(masked)={}", event.userId(), maskEmail(event.email()), ex);마스킹 유틸이 없다면 간단히 앞/도메인만 남기는 형태의 정규식 기반 마스킹 함수를 추가해 주세요.
src/main/java/project/flipnote/user/service/UserService.java (1)
112-117: 빈 리스트 처리 개선 필요
getIdAndNicknames메서드에서 빈 리스트가 전달될 경우 불필요한 DB 쿼리가 실행될 수 있습니다.public Map<Long, String> getIdAndNicknames(List<Long> inviteeUserIds) { + if (inviteeUserIds == null || inviteeUserIds.isEmpty()) { + return Map.of(); + } List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds); return idAndNicknames.stream() .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); }src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
50-52: 상태 변경 검증 로직 추가 고려현재 상태와 변경하려는 상태를 검증하는 로직을 추가하면 더 안전한 상태 관리가 가능합니다.
public void respond(GroupInvitationStatus status) { + if (this.status != GroupInvitationStatus.PENDING) { + throw new IllegalStateException("이미 응답한 초대입니다."); + } + if (status == GroupInvitationStatus.PENDING) { + throw new IllegalArgumentException("PENDING 상태로는 변경할 수 없습니다."); + } this.status = status; }src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
147-151: 닉네임 기본값 처리 개선빈 문자열 대신 더 명확한 기본값을 사용하는 것이 좋습니다.
Page<GroupInvitationResponse> res = invitationPage.map( (invitation) -> GroupInvitationResponse.from( - invitation, idAndNicknames.getOrDefault(invitation.getInviteeUserId(), "") + invitation, idAndNicknames.getOrDefault(invitation.getInviteeUserId(), null) ) );
164-173: 트랜잭션 내 반복 작업 성능 고려여러 초대를 처리할 때 각각 개별적으로 처리하면 성능 이슈가 발생할 수 있습니다.
배치 처리나 벌크 연산을 고려해보세요. 예를 들어:
- 모든 초대 상태를 한 번에 업데이트
- GroupMember 엔티티들을 한 번에 저장
135-135: TODO 항목 추적 필요코드에 여러 TODO 주석이 있습니다:
- Line 135: Projection 및 카운트 쿼리 튜닝
- Line 209: 초대받은 회원 알림 전송
- Line 234: 초대받은 비회원 이메일 전송
이러한 TODO 항목들을 이슈로 생성하여 추적하시겠습니까?
Also applies to: 209-209, 234-234
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
src/main/java/project/flipnote/auth/service/AuthService.java(2 hunks)src/main/java/project/flipnote/common/config/SwaggerConfig.java(2 hunks)src/main/java/project/flipnote/common/event/UserRegisteredEvent.java(1 hunks)src/main/java/project/flipnote/common/response/PageResponse.java(1 hunks)src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java(1 hunks)src/main/java/project/flipnote/group/controller/GroupInvitationController.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitation.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupErrorCode.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java(1 hunks)src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java(1 hunks)src/main/java/project/flipnote/group/model/UserIdNickname.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(1 hunks)src/main/java/project/flipnote/group/service/GroupService.java(2 hunks)src/main/java/project/flipnote/user/repository/UserProfileRepository.java(2 hunks)src/main/java/project/flipnote/user/service/UserService.java(5 hunks)src/main/resources/application.yml(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (20)
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (9)
src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRequest.java (1)
GroupJoinRequest(3-6)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (1)
CreateCardSetResponse(4-10)
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (2)
src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (2)
Getter(22-65)updateStatus(62-64)
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (2)
Repository(12-19)existsByGroup_idAndUser_id(18-18)src/main/java/project/flipnote/group/repository/GroupRepository.java (2)
findById(17-17)Repository(15-22)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (4)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (3)
GroupJoinErrorCode(76-76)GroupErrorCode(52-52)GroupErrorCode(64-64)src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (1)
Getter(8-26)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
GroupErrorCode(48-48)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (2)
src/main/java/project/flipnote/common/security/jwt/JwtComponent.java (1)
SecurityErrorCode(111-111)src/main/java/project/flipnote/auth/exception/AuthErrorCode.java (1)
Getter(9-36)
src/main/java/project/flipnote/group/service/GroupService.java (5)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
hasPermission(74-85)src/main/java/project/flipnote/group/entity/GroupPermission.java (2)
Entity(12-29)Builder(25-28)src/main/java/project/flipnote/group/entity/GroupRolePermission.java (1)
Entity(11-41)src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java (1)
Repository(9-12)src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java (1)
Repository(11-14)
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (1)
src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (1)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)
src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (5)
src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)
src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (4)
src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java (1)
EmailVerificationRequest(6-11)src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java (1)
PasswordResetCreateRequest(6-10)src/main/java/project/flipnote/auth/model/EmailVerifyRequest.java (1)
EmailVerifyRequest(8-17)src/main/java/project/flipnote/auth/model/UserLoginRequest.java (1)
UserLoginRequest(7-15)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (3)
src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java (3)
AuthControllerDocs(19-53)Operation(45-46)Operation(30-31)src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java (1)
Tag(10-28)
src/main/java/project/flipnote/user/repository/UserProfileRepository.java (2)
src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (3)
findByEmailAndStatus(17-17)UserAuthRepository(13-35)findByIdAndStatus(25-25)src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)
src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (9)
src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupCreateRequest.java (1)
GroupCreateRequest(6-29)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/user/model/UserUpdateResponse.java (1)
UserUpdateResponse(5-18)
src/main/java/project/flipnote/user/service/UserService.java (1)
src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)
src/main/java/project/flipnote/common/response/PageResponse.java (10)
src/main/java/project/flipnote/user/model/UserUpdateResponse.java (1)
UserUpdateResponse(5-18)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/user/model/SocialLinksResponse.java (1)
SocialLinksResponse(7-18)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/common/response/ApiResponse.java (1)
ApiResponse(14-64)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/user/model/MyInfoResponse.java (1)
MyInfoResponse(9-38)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (5)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (2)
Getter(8-26)Override(22-25)src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1)
CommonErrorCode(8-17)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
ErrorCode(3-10)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
src/main/java/project/flipnote/user/service/UserService.java (1)
RequiredArgsConstructor(27-118)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (4)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (2)
Getter(22-65)Builder(46-60)src/main/java/project/flipnote/group/entity/GroupPermission.java (1)
Entity(12-29)src/main/java/project/flipnote/group/entity/GroupMember.java (1)
Getter(20-48)src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java (1)
GroupPermissionStatus(3-5)
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (2)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondRequest.java (1)
GroupJoinRespondRequest(5-8)
🔇 Additional comments (7)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (1)
12-12: TOKEN_EXPIRED 에러 코드 변경 내부 참조 검증 결과내부 코드베이스에서는
TOKEN_EXPIRED변경이 아래 한 곳에서만 참조되고 있습니다. 외부 API 소비자·문서·모니터링 룰 등에서 기존 문자열을 사용 중일 수 있으니 별도 확인이 필요합니다.
- src/main/java/project/flipnote/common/security/jwt/JwtComponent.java:103
→throw new CustomSecurityException(SecurityErrorCode.TOKEN_EXPIRED);src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (1)
3-5: API 상태 분리 설계 적절요청/응답에서 PENDING을 배제하고 ACCEPTED/REJECTED만 허용하는 분리는 유효성 검증과 도메인 보호에 유리합니다. 현재 사용처와의 매핑(엔티티 상태)만 일관 유지하면 됩니다.
src/main/java/project/flipnote/user/service/UserService.java (1)
92-94: 메서드 접근 제어자 변경 불필요
findActiveUserByEmail메서드는project.flipnote.group.service.GroupInvitationService에서 사용되고 있으므로 public 접근 제어자를 유지해야 합니다. package-private으로 변경 시 컴파일 오류가 발생하므로 접근 제어자 변경은 필요 없습니다.Likely an incorrect or invalid review comment.
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (1)
1-26: 에러 코드 구현이 적절합니다다른 에러 코드 enum들과 일관된 패턴을 따르고 있으며, 그룹 초대 기능에 필요한 에러 케이스들을 잘 정의하고 있습니다.
src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (1)
9-30: 응답 모델 구현이 적절합니다Record 타입을 사용한 불변 객체 구현과 명확한 필드 네이밍이 좋습니다. 날짜 포맷도 API 응답에 적합합니다.
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
14-37: API 문서 인터페이스 구현이 적절합니다Swagger 문서화를 위한 인터페이스가 일관된 패턴을 따르고 있으며, 한국어 설명이 명확합니다.
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
27-80: 컨트롤러 구현이 적절합니다REST API 규칙을 잘 따르고 있으며, 적절한 HTTP 상태 코드와 검증 로직이 구현되어 있습니다.
| @Builder | ||
| public GroupInvitation(Long groupId, Long inviterUserId, Long inviteeUserId, String inviteeEmail) { | ||
| this.groupId = groupId; | ||
| this.inviterUserId = inviterUserId; | ||
| this.inviteeUserId = inviteeUserId; | ||
| this.inviteeEmail = inviteeEmail; | ||
| this.status = GroupInvitationStatus.PENDING; | ||
| } |
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 검증 로직 추가 필요
초대 대상 정보가 모두 null인 경우를 방지하기 위한 검증이 필요합니다.
@Builder
public GroupInvitation(Long groupId, Long inviterUserId, Long inviteeUserId, String inviteeEmail) {
+ if (inviteeUserId == null && inviteeEmail == null) {
+ throw new IllegalArgumentException("초대 대상 정보(userId 또는 email)가 필요합니다.");
+ }
this.groupId = groupId;
this.inviterUserId = inviterUserId;
this.inviteeUserId = inviteeUserId;
this.inviteeEmail = inviteeEmail;
this.status = GroupInvitationStatus.PENDING;
}📝 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.
| @Builder | |
| public GroupInvitation(Long groupId, Long inviterUserId, Long inviteeUserId, String inviteeEmail) { | |
| this.groupId = groupId; | |
| this.inviterUserId = inviterUserId; | |
| this.inviteeUserId = inviteeUserId; | |
| this.inviteeEmail = inviteeEmail; | |
| this.status = GroupInvitationStatus.PENDING; | |
| } | |
| @Builder | |
| public GroupInvitation(Long groupId, Long inviterUserId, Long inviteeUserId, String inviteeEmail) { | |
| if (inviteeUserId == null && inviteeEmail == null) { | |
| throw new IllegalArgumentException("초대 대상 정보(userId 또는 email)가 필요합니다."); | |
| } | |
| this.groupId = groupId; | |
| this.inviterUserId = inviterUserId; | |
| this.inviteeUserId = inviteeUserId; | |
| this.inviteeEmail = inviteeEmail; | |
| this.status = GroupInvitationStatus.PENDING; | |
| } |
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/entity/GroupInvitation.java around lines
41 to 48, add validation in the builder-backed constructor to ensure invitee
information is present: check that at least one of inviteeUserId or inviteeEmail
is non-null (and non-empty for email if you want stricter check) and if both are
null throw an IllegalArgumentException with a clear message like "Either
inviteeUserId or inviteeEmail must be provided"; keep the rest of field
assignments and default status assignment unchanged.
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java
Show resolved
Hide resolved
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/group/service/GroupInvitationService.java
Show resolved
Hide resolved
src/main/java/project/flipnote/group/service/GroupInvitationService.java
Show resolved
Hide resolved
| public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) { | ||
| GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow( | ||
| () -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP) | ||
| ); | ||
|
|
||
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | ||
|
|
||
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( | ||
| groupRepository.getReferenceById(groupId), | ||
| groupMember.getRole(), | ||
| groupPermission | ||
| ); | ||
| } |
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
Null 안전성 및 반환 타입 정교화
GroupPermissionRepository#findByName가 존재하지 않을 경우null을 반환할 수 있어 NPE 위험이 있습니다. 예외로 fail-fast 하거나 기본적으로false처리 등 명시적 분기 추가가 필요합니다.- 불필요한 박싱을 줄이기 위해 반환 타입은
Boolean대신boolean을 권장합니다.
아래처럼 보완을 제안합니다.
- public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) {
+ public boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) {
GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow(
() -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP)
);
- GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ if (groupPermission == null) {
+ // 권한 시드 누락 등 시스템 설정 오류. 적절한 에러코드가 있다면 교체하세요.
+ throw new IllegalStateException("Unknown group permission: " + groupPermissionStatus);
+ }
return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(
groupRepository.getReferenceById(groupId),
groupMember.getRole(),
groupPermission
);
}📝 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.
| public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) { | |
| GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow( | |
| () -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP) | |
| ); | |
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | |
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( | |
| groupRepository.getReferenceById(groupId), | |
| groupMember.getRole(), | |
| groupPermission | |
| ); | |
| } | |
| public boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) { | |
| GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow( | |
| () -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP) | |
| ); | |
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | |
| if (groupPermission == null) { | |
| // 권한 시드 누락 등 시스템 설정 오류. 적절한 에러코드가 있다면 교체하세요. | |
| throw new IllegalStateException("Unknown group permission: " + groupPermissionStatus); | |
| } | |
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( | |
| groupRepository.getReferenceById(groupId), | |
| groupMember.getRole(), | |
| groupPermission | |
| ); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/service/GroupService.java around lines
72 to 84, the method should be made null-safe and avoid unnecessary boxing:
change the return type from Boolean to primitive boolean, call
groupPermissionRepository.findByName(...) and if it returns null throw a
BizException (e.g. GroupErrorCode.PERMISSION_NOT_FOUND) or otherwise fail-fast,
then use the non-null GroupPermission in
groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(...) and
return the primitive boolean result to eliminate NPE risk and unnecessary
boxing.
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: 18
🧹 Nitpick comments (20)
src/main/java/project/flipnote/group/model/UserIdNickname.java (2)
3-6: 패키지 경계 재검토 (프로젝션 위치 조정 제안)이 프로젝션은 UserProfileRepository(사용자 도메인)에서 사용됩니다. group.model 아래에 두면 user → group 방향 의존이 생겨 도메인 결합도가 커집니다. user.model(또는 common.projection)로 이동하는 것을 권장합니다.
3-6: 프로젝션 매핑 주의점 간단 주석 추가 요청엔티티/쿼리 alias와 접근자 명(getId, getNickname)이 정확히 매칭되어야 합니다. 간단한 Javadoc 주석으로 사용처/매핑 규칙을 남겨주세요.
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (1)
3-6: 다른 Response 레코드와의 일관성: 정적 팩토리 추가 제안프로젝트 내 다수의 응답 레코드가 from(...) 정적 팩토리를 제공합니다. 동일 패턴 유지가 가독성/일관성에 도움이 됩니다.
public record GroupInvitationCreateResponse( Long invitationId ) { + public static GroupInvitationCreateResponse from(Long invitationId) { + return new GroupInvitationCreateResponse(invitationId); + } }src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (1)
3-5: 네이밍 정합성: Respond vs Response 혼재 해소 제안Request DTO가 "Respond"를 쓰는 반면, 상태 enum은 "Response"로 되어 있어 혼동 여지가 있습니다.
GroupInvitationRespondStatus로의 리네이밍을 제안합니다. (참조 파일 전반 사용처 일괄 변경 필요)또한, 마지막 세미콜론은 필수는 아니므로 제거 가능합니다.
리네이밍(이 파일 기준) 예:
-package project.flipnote.group.model; +package project.flipnote.group.model; -public enum GroupInvitationResponseStatus { - ACCEPTED, REJECTED; -} +public enum GroupInvitationRespondStatus { + ACCEPTED, REJECTED +}src/main/java/project/flipnote/common/response/PageResponse.java (2)
19-31: DTO 변환 편의용 오버로드 추가 제안(from(Page, Function<S,R> mapper))서비스 계층에서 Page → PageResponse 변환 시 유틸 오버로드가 유용합니다. 아래와 같이 mapper를 받는 정적 메서드 추가를 제안합니다. (import 추가 포함)
package project.flipnote.common.response; import java.util.List; +import java.util.function.Function; import org.springframework.data.domain.Page; public record PageResponse<T>( @@ public static <T> PageResponse<T> from(Page<T> page) { return new PageResponse<>( page.getContent(), page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages(), page.isFirst(), page.isLast(), page.hasNext(), page.hasPrevious() ); } + + /** + * Page<S>를 PageResponse<R>로 변환합니다. page 번호는 0 기반입니다. + */ + public static <S, R> PageResponse<R> from(Page<S> page, Function<S, R> mapper) { + return new PageResponse<>( + page.getContent().stream().map(mapper).toList(), + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages(), + page.isFirst(), + page.isLast(), + page.hasNext(), + page.hasPrevious() + ); + } }
7-17: page 인덱스 기준 명시를 위한 간단 Javadoc 권장API 소비자 관점에서 page가 0 기반임을 레코드/팩토리 메서드 Javadoc에 명시하면 혼동을 줄일 수 있습니다.
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
50-52: 상태 전이 검증 추가 권장임의의 상태로의 전이가 허용됩니다. PENDING→ACCEPTED/REJECTED 등 유효 전이만 허용하고, 동일 상태로의 idempotent 처리도 고려해 주세요.
public void respond(GroupInvitationStatus next) { if (this.status == next) { return; // idempotent } if (this.status != GroupInvitationStatus.PENDING) { throw new IllegalStateException("처리된 초대는 변경할 수 없습니다."); } if (next == GroupInvitationStatus.PENDING) { throw new IllegalArgumentException("PENDING으로의 역전이는 허용되지 않습니다."); } this.status = next; }src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
25-25: 파라미터 네이밍 컨벤션 일치첫 글자 대문자(InviteeEmail)로 선언되어 컨벤션과 불일치합니다. 소문자로 정리해 주세요.
-List<GroupInvitation> findAllByInviteeEmailAndStatus(String InviteeEmail, GroupInvitationStatus status); +List<GroupInvitation> findAllByInviteeEmailAndStatus(String inviteeEmail, GroupInvitationStatus status);참고: 이메일 중복 문제를 회피하려면 저장 전 소문자(normalize) 처리도 권장됩니다.
src/main/resources/application.yml (1)
93-95: 프로파일별 설정 분리/외부화 권장로컬 URL이 기본값으로 커밋되어 있어 비로컬 환경에서 오해를 줄 수 있습니다. 환경변수로 외부화해 주세요.
springdoc: server: - url: http://localhost:8080 + url: ${SPRINGDOC_SERVER_URL:http://localhost:8080}운영/스테이징 프로파일에서 SPRINGDOC_SERVER_URL이 올바르게 주입되는지 확인 부탁드립니다.
src/main/java/project/flipnote/common/config/SwaggerConfig.java (1)
17-19: serverUrl 미설정 대비 및 조건부 서버 추가 권장프로파일에 따라 springdoc.server.url 미지정 시 기동 실패/빈 URL 문제가 생길 수 있습니다. 기본값 제공 및 조건부로 서버 항목을 추가해 주세요.
- @Value("${springdoc.server.url}") + @Value("${springdoc.server.url:}") private String serverUrl; @Bean public OpenAPI openApi() { - return new OpenAPI() - .addServersItem(new Server().url(serverUrl)) + OpenAPI openAPI = new OpenAPI(); + if (serverUrl != null && !serverUrl.isBlank()) { + openAPI.addServersItem(new Server().url(serverUrl)); + } + return openAPI .addSecurityItem( new SecurityRequirement() .addList("access-token") ) .components(new Components() .addSecuritySchemes("access-token", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT")) ) .info(apiInfo()); }Also applies to: 21-36
src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (1)
6-10: Swagger 문서 품질 개선(예시 추가 제안)요청 본문 스키마에 예시를 제공하면 사용성이 좋아집니다. 예:
+import io.swagger.v3.oas.annotations.media.Schema; ... public record GroupInvitationCreateRequest( - @Email @NotBlank - String email + @Email @NotBlank + @Schema(example = "invitee@example.com") + String email ) {}src/main/java/project/flipnote/user/repository/UserProfileRepository.java (2)
22-22: 파생 쿼리 메서드 이름 개선 제안프로젝션 타입으로 반환하면 select 컬럼이 자동 축소되므로, 메서드명은 보통
findByIdIn정도로 간결히 유지합니다. 현재findIdAndNicknameByIdIn은 의미상冗長하며 혼동을 줄 수 있습니다.권장 변경:
- List<UserIdNickname> findIdAndNicknameByIdIn(List<Long> ids); + List<UserIdNickname> findByIdIn(List<Long> ids);사용처(UserService)도 함께 업데이트 필요.
8-8: 패키징 의존 방향 개선user 레이어의 Repository가 group.model의 프로젝션(UserIdNickname)을 참조하고 있습니다. 도메인 간 결합도를 낮추려면:
- 프로젝션을 user.model 또는 common.dto 쪽으로 이동하거나,
- Repository 내부에 중첩 인터페이스로 프로젝션을 정의하는 방식을 고려해 주세요.
이동 시 API 표면은 동일하게 유지할 수 있습니다.
Also applies to: 22-22
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (1)
6-16: 문서 가독성 보완(예시 추가)스키마 예시가 있으면 클라이언트 구현이 쉬워집니다.
+import io.swagger.v3.oas.annotations.media.Schema; ... public record GroupInvitationRespondRequest( - @NotNull - GroupInvitationResponseStatus status + @NotNull + @Schema(example = "ACCEPTED", description = "ACCEPTED 또는 REJECTED") + GroupInvitationResponseStatus status ) { ... }src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (2)
28-29: 메서드 네이밍 컨벤션Java 메서드는 lowerCamelCase가 기본입니다.
HandleUserRegisteredEvent→handleUserRegisteredEvent.- public void HandleUserRegisteredEvent(UserRegisteredEvent event) { + public void handleUserRegisteredEvent(UserRegisteredEvent event) {
22-26: Retry 대상 예외 범위 축소 제안비즈니스 예외(BizException 등)는 재시도 무의미할 수 있습니다. 네트워크/일시 오류만 재시도하도록 include/exclude를 명시하는 것을 권장합니다.
- @Retryable( - maxAttempts = 3, - backoff = @Backoff(delay = 2000, multiplier = 2) - ) + @Retryable( + maxAttempts = 3, + include = { RuntimeException.class }, + exclude = { project.flipnote.common.exception.BizException.class }, + backoff = @Backoff(delay = 2000, multiplier = 2) + )src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (1)
16-17: 시간대 명시 또는 글로벌 설정 일관화 권장@jsonformat에 timezone이 없으면 서버 기본 타임존에 의존합니다. API 일관성을 위해 고정 타임존(예: UTC) 또는 서비스 표준(예: Asia/Seoul)을 명시하거나, 글로벌 ObjectMapper 설정으로 통일하는 것을 권장합니다.
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
17-36: 보안 스키마 명시로 Swagger 문서 정확도 향상다른 Docs 인터페이스(UserControllerDocs)와 일관되게 SecurityRequirement를 명시하면, 접근 토큰 요구사항이 문서에 반영됩니다.
+import io.swagger.v3.oas.annotations.security.SecurityRequirement; ... @Tag(name = "Group Invitation", description = "Group Invitation API") public interface GroupInvitationControllerDocs { - @Operation(summary = "그룹 초대") + @Operation(summary = "그룹 초대", security = { @SecurityRequirement(name = "access-token") }) ResponseEntity<GroupInvitationCreateResponse> createGroupInvitation(...); - @Operation(summary = "그룹 초대 취소") + @Operation(summary = "그룹 초대 취소", security = { @SecurityRequirement(name = "access-token") }) ResponseEntity<Void> deleteGroupInvitation(...); - @Operation(summary = "그룹 초대 응답") + @Operation(summary = "그룹 초대 응답", security = { @SecurityRequirement(name = "access-token") }) ResponseEntity<Void> respondToGroupInvitation(...); - @Operation(summary = "그룹 초대 목록 조회") + @Operation(summary = "그룹 초대 목록 조회", security = { @SecurityRequirement(name = "access-token") }) ResponseEntity<PageResponse<GroupInvitationResponse>> getGroupInvitations(...); }src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
53-54: 삭제 응답 코드 204 No Content 권장리소스 삭제 시 204가 더 관례적입니다.
- return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build();src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
130-137: 페이징 파라미터 방어 코드 제안(안전성/남용 방지)page/size에 음수나 과도한 값이 들어올 수 있습니다. 방어적으로 보정해 주세요(예: page >= 0, 1 <= size <= 100).
- PageRequest pageRequest = PageRequest.of(page, size); + int safePage = Math.max(0, page); + int safeSize = Math.max(1, Math.min(100, size)); + PageRequest pageRequest = PageRequest.of(safePage, safeSize);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
src/main/java/project/flipnote/auth/service/AuthService.java(2 hunks)src/main/java/project/flipnote/common/config/SwaggerConfig.java(2 hunks)src/main/java/project/flipnote/common/event/UserRegisteredEvent.java(1 hunks)src/main/java/project/flipnote/common/response/PageResponse.java(1 hunks)src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java(1 hunks)src/main/java/project/flipnote/group/controller/GroupInvitationController.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitation.java(1 hunks)src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupErrorCode.java(1 hunks)src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java(1 hunks)src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponse.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java(1 hunks)src/main/java/project/flipnote/group/model/UserIdNickname.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(1 hunks)src/main/java/project/flipnote/group/service/GroupService.java(2 hunks)src/main/java/project/flipnote/user/repository/UserProfileRepository.java(2 hunks)src/main/java/project/flipnote/user/service/UserService.java(5 hunks)src/main/resources/application.yml(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (20)
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (3)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (2)
Repository(12-19)existsByGroup_idAndUser_id(18-18)src/main/java/project/flipnote/group/repository/GroupRepository.java (3)
findById(17-17)Repository(15-22)Lock(19-21)src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (1)
findByUserIdAndStatus(27-27)
src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (6)
src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java (1)
GroupPermissionStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/entity/GroupPermission.java (1)
Entity(12-29)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (5)
src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java (1)
GroupPermissionStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (2)
Getter(22-65)updateStatus(62-64)src/main/java/project/flipnote/group/entity/GroupPermission.java (1)
Entity(12-29)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)
src/main/java/project/flipnote/user/repository/UserProfileRepository.java (1)
src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (3)
UserAuthRepository(13-35)findByUserIdAndStatus(27-27)findByEmailAndStatus(17-17)
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (8)
src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (1)
CreateCardSetResponse(4-10)
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java (4)
src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1)
CommonErrorCode(8-17)src/main/java/project/flipnote/common/security/jwt/JwtComponent.java (1)
SecurityErrorCode(111-111)src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
ErrorCode(3-10)src/main/java/project/flipnote/common/security/exception/CustomSecurityException.java (1)
CustomSecurityException(7-12)
src/main/java/project/flipnote/auth/service/AuthService.java (3)
src/main/java/project/flipnote/auth/entity/UserAuth.java (2)
Getter(18-76)changePassword(73-75)src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java (3)
userAuth(31-34)Async(23-35)Slf4j(16-41)src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (3)
src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)src/main/java/project/flipnote/auth/event/PasswordResetCreateEvent.java (1)
PasswordResetCreateEvent(3-7)src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java (1)
EmailVerificationSendEvent(3-7)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (2)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (3)
Getter(22-65)Builder(46-60)updateStatus(62-64)src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
Slf4j(30-244)
src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (4)
src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java (1)
EmailVerificationRequest(6-11)src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java (1)
PasswordResetCreateRequest(6-10)src/main/java/project/flipnote/auth/model/EmailVerifyRequest.java (1)
EmailVerifyRequest(8-17)src/main/java/project/flipnote/auth/model/UserRegisterRequest.java (1)
UserRegisterRequest(11-40)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (3)
src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java (1)
Tag(10-28)src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)src/main/java/project/flipnote/group/controller/GroupController.java (1)
RequiredArgsConstructor(18-31)
src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (10)
src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupCreateRequest.java (1)
GroupCreateRequest(6-29)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/groupjoin/model/GroupJoinRequest.java (1)
GroupJoinRequest(3-6)src/main/java/project/flipnote/user/model/UserUpdateResponse.java (1)
UserUpdateResponse(5-18)
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (2)
src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondRequest.java (1)
GroupJoinRespondRequest(5-8)
src/main/java/project/flipnote/common/response/PageResponse.java (10)
src/main/java/project/flipnote/user/model/UserUpdateResponse.java (1)
UserUpdateResponse(5-18)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/user/model/SocialLinksResponse.java (1)
SocialLinksResponse(7-18)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/user/model/MyInfoResponse.java (1)
MyInfoResponse(9-38)src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (1)
CreateCardSetResponse(4-10)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)
src/main/java/project/flipnote/group/service/GroupService.java (5)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
hasPermission(74-85)src/main/java/project/flipnote/group/entity/GroupPermission.java (2)
Entity(12-29)Builder(25-28)src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java (1)
Repository(9-12)src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java (1)
Repository(11-14)src/main/java/project/flipnote/group/entity/GroupRolePermission.java (1)
Entity(11-41)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
RequiredArgsConstructor(27-80)src/main/java/project/flipnote/user/service/UserService.java (1)
RequiredArgsConstructor(27-118)src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (2)
Transactional(100-135)Transactional(161-205)
src/main/java/project/flipnote/user/service/UserService.java (2)
src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (1)
findByEmailAndStatus(17-17)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (2)
src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (2)
Getter(8-26)Override(22-25)src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (3)
GroupJoinErrorCode(76-76)GroupErrorCode(64-64)GroupErrorCode(52-52)
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (4)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (2)
Getter(8-26)Override(22-25)src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
Getter(9-23)src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
ErrorCode(3-10)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (5)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
RequiredArgsConstructor(33-255)src/main/java/project/flipnote/auth/service/AuthService.java (1)
Slf4j(45-230)src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java (4)
Slf4j(16-41)Async(23-35)Recover(37-40)userAuth(31-34)src/main/java/project/flipnote/auth/listener/PasswordResetCreateEventListener.java (2)
PasswordResetCreateEventListener(17-39)handlePasswordResetCreateEvent(24-33)src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java (1)
EmailVerificationEventListener(17-39)
🔇 Additional comments (10)
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (1)
3-4: GroupInvitation 엔티티의 status 필드가 EnumType.STRING으로 매핑되어 있습니다.검증 결과
src/main/java/project/flipnote/group/entity/GroupInvitation.java파일 37–39행에@Enumerated(EnumType.STRING) @Column(nullable = false) private GroupInvitationStatus status;어노테이션이 적용된 것을 확인했습니다. ORDINAL 저장으로 인한 데이터 불일치 위험이 없습니다.
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (1)
15-15: USER_NOT_IN_GROUP 추가 적절권한 체크 경로에서 404로 매핑하여 정보 노출을 줄이는 선택이 일관적이며 합리적입니다. 관련 서비스/컨트롤러에서 이 에러 코드가 기대대로 매핑되는지도 함께 점검해 주세요.
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (1)
10-26: 에러 코드 정의 적절합니다프로젝트의 ErrorCode 패턴과 일관되고, HTTP 상태/코드/메시지 매핑이 명확합니다. 서비스에서의 사용과도 적합해 보입니다.
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
21-22: 메서드 추가 LGTMID 기반 조회 메서드가 권한 체크 등에서 유용하며, 기존 네이밍 컨벤션과도 일치합니다.
src/main/java/project/flipnote/group/model/GroupInvitationCreateRequest.java (1)
6-10: DTO 유효성 정의 적절 (LGTM)이메일 입력에 @Email, @notblank 제약을 적용한 점 좋습니다. 컨트롤러에서 @Valid 사용으로 유효성 검증 경로도 확보되어 있습니다.
src/main/java/project/flipnote/user/repository/UserProfileRepository.java (1)
20-21: 상태 기반 이메일 조회 추가 (LGTM)활성 사용자 조회에 필요한 메서드가 명확합니다. 기존 패턴(예: UserAuthRepository.findByEmailAndStatus)과도 일관적입니다.
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (1)
6-16: 상태 변환 로직 명확 (LGTM)응답 상태 → 엔티티 상태 매핑이 명확하며, switch exhaustiveness로 신규 상태 추가 시 컴파일 타임에 잡히는 점도 좋습니다.
src/main/java/project/flipnote/user/service/UserService.java (1)
92-94: 이메일 기반 활성 사용자 조회 추가 (LGTM)서비스 경로에서 Optional로 노출하는 설계 적절합니다. 상위 계층에서 존재 유무에 따라 분기하기 좋습니다.
src/main/java/project/flipnote/group/model/GroupInvitationResponse.java (1)
9-30: 초대 대상 이메일 공개 범위 검토 필요Response에
inviteeEmail을 노출합니다. 초대자 외 권한자들이 목록을 조회할 수 있다면, 이메일(PII) 노출 범위가 정책과 부합하는지 확인해 주세요. 필요 시 마스킹(예: ab***@domain.com) 또는 최소 권한자만 접근하도록 제한이 필요할 수 있습니다.보안 확인용 체크리스트:
- getGroupInvitations 엔드포인트는 초대 권한자(예: 그룹 관리자)로만 접근 가능한가?
- 내부 정책상 초대 대상자의 이메일 전체 노출이 허용되는가? 아니면 마스킹/부분 노출이 요구되는가?
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
34-43: 생성 엔드포인트 구현 (LGTM)권한 주체 추출(@AuthenticationPrincipal)과 @Valid 바디 검증, 201 Created 반환이 적절합니다.
src/main/java/project/flipnote/common/security/exception/SecurityErrorCode.java
Show resolved
Hide resolved
src/main/java/project/flipnote/group/controller/GroupInvitationController.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/group/service/GroupInvitationService.java
Show resolved
Hide resolved
src/main/java/project/flipnote/group/service/GroupInvitationService.java
Show resolved
Hide resolved
src/main/java/project/flipnote/group/service/GroupInvitationService.java
Show resolved
Hide resolved
| public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) { | ||
| GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow( | ||
| () -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP) | ||
| ); | ||
|
|
||
| GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus); | ||
|
|
||
| return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( | ||
| groupRepository.getReferenceById(groupId), | ||
| groupMember.getRole(), | ||
| groupPermission | ||
| ); | ||
| } |
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
권한 체크 로직 보완: Null 안전성·반환 타입·에러코드 확인
- GroupPermissionRepository.findByName(...)가 null이면 NPE 위험이 있습니다. 명시적으로 검증 후 의미 있는 BizException을 던져 주세요.
- 반환 타입은 primitive boolean이면 박싱 비용과 NPE 위험이 사라집니다.
- GroupErrorCode.USER_NOT_IN_GROUP 상수가 실제 정의되어 있는지 확인 부탁드립니다(미정의 시 컴파일 오류).
- public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) {
+ public boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) {
GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId).orElseThrow(
() -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP)
);
- GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ if (groupPermission == null) {
+ // TODO: 적절한 에러코드로 교체 (예: GroupErrorCode.GROUP_PERMISSION_NOT_FOUND)
+ throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER); // placeholder: 실제 에러코드로 변경 필요
+ }
return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(
groupRepository.getReferenceById(groupId),
groupMember.getRole(),
groupPermission
);
}에러코드가 준비되어 있다면 placeholder를 해당 코드로 변경해 드리겠습니다.
추가 검증을 위해 다음 스크립트를 실행해 주세요:
🏁 Script executed:
#!/bin/bash
# 1. GroupPermissionRepository 인터페이스 및 findByName 반환 타입 확인
rg "interface GroupPermissionRepository" -A 5 -n
rg "findByName" -n src
# 2. GroupErrorCode 열거형 정의 및 관련 상수 확인
rg "enum GroupErrorCode" -A 20 -n
rg "USER_NOT_IN_GROUP" -n
rg "GROUP_PERMISSION_NOT_FOUND" -nLength of output: 3076
권한 체크 로직 강화: Null 안전성·반환 타입·에러코드 추가
- 반환 타입을
Boolean에서 primitiveboolean으로 변경하여 불필요한 박싱과 NPE 위험을 제거하세요. findByName(...)는 엔티티가 없으면null을 반환하므로,groupPermission이null일 때BizException을 던지도록 null 검증을 추가하세요.- null 예외 처리용으로
GroupErrorCode.GROUP_PERMISSION_NOT_FOUND(또는 적절한 에러코드)를GroupErrorCodeenum에 정의하고 사용하세요.
다음과 같이 수정 제안드립니다:
- public Boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) {
+ public boolean hasPermission(Long groupId, Long userId, GroupPermissionStatus groupPermissionStatus) {
GroupMember groupMember = groupMemberRepository.findByGroup_IdAndUser_Id(groupId, userId)
.orElseThrow(() -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP));
- GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ GroupPermission groupPermission = groupPermissionRepository.findByName(groupPermissionStatus);
+ if (groupPermission == null) {
+ throw new BizException(GroupErrorCode.GROUP_PERMISSION_NOT_FOUND);
+ }
return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission(
groupRepository.getReferenceById(groupId),
groupMember.getRole(),
groupPermission
);
}GroupErrorCodeenum에 아래 값 추가를 잊지 마세요:GROUP_PERMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_004", "권한 정보가 존재하지 않습니다.");
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/service/GroupService.java around lines
72 to 84, the permission check currently returns a boxed Boolean and does not
validate that groupPermission exists; change the method return type to primitive
boolean, call groupPermissionRepository.findByName(...) and if it returns null
throw a BizException with GroupErrorCode.GROUP_PERMISSION_NOT_FOUND, then
perform the existsByGroupAndRoleAndGroupPermission check and return its boolean
result; also ensure GroupErrorCode enum contains
GROUP_PERMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_004", "권한 정보가 존재하지
않습니다.") so the thrown exception uses that code.
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: 0
♻️ Duplicate comments (14)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (3)
25-25: 파라미터 명명 일관성 및 이메일 대소문자 처리/페이지네이션 보강
- 파라미터
InviteeEmail대문자 시작은 컨벤션 위반입니다. camelCase로 수정해 주세요.- 이메일은 대소문자 무시 매칭이 일반적입니다. IgnoreCase 파생 쿼리 추가를 고려해 주세요.
- 목록 조회는 Page 기반 정합성이 좋아서 Pageable 변형을 함께 제공하는 것을 권장합니다.
-List<GroupInvitation> findAllByInviteeEmailAndStatus(String InviteeEmail, GroupInvitationStatus status); +List<GroupInvitation> findAllByInviteeEmailAndStatus(String inviteeEmail, GroupInvitationStatus status); +Page<GroupInvitation> findAllByInviteeEmailIgnoreCaseAndStatus(String inviteeEmail, GroupInvitationStatus status, Pageable pageable);
15-18: PENDING 한정 중복 초대 검사 메서드 추가 필요현재 existsBy… 메서드는 상태를 고려하지 않아 과거 ACCEPTED/REJECTED 기록도 재초대를 막을 수 있습니다. PENDING에 대해서만 중복 방지를 적용하도록 상태 조건 포함 메서드를 추가해 주세요.
적용 예시:
public interface GroupInvitationRepository extends JpaRepository<GroupInvitation, Long> { boolean existsByGroupIdAndInviteeUserId(Long groupId, Long inviteeUserId); boolean existsByGroupIdAndInviteeEmail(Long groupId, String inviteeEmail); + + // PENDING 한정 중복 검사 + boolean existsByGroupIdAndInviteeUserIdAndStatus(Long groupId, Long inviteeUserId, GroupInvitationStatus status); + boolean existsByGroupIdAndInviteeEmailAndStatus(Long groupId, String inviteeEmail, GroupInvitationStatus status);서비스 레이어(createUserInvitation/createGuestInvitation)에서도 GroupInvitationStatus.PENDING 기준으로 위 메서드를 사용하도록 교체해 주세요.
19-21: 삭제/응답 시 groupId 범위 검증 메서드 보강id+status만으로 조회하면 타 그룹 리소스 접근 가능성이 있습니다. groupId를 포함한 조회 메서드를 추가해 사용해 주세요. 현재
findByIdAndGroupIdAndInviteeUserIdAndStatus가 있지만, 초대 삭제/응답 흐름에서 초대 수신자/발신자 외의 그룹 관리 권한으로 처리하는 케이스를 위해findByIdAndGroupIdAndStatus가 유용합니다.Optional<GroupInvitation> findByIdAndStatus(Long id, GroupInvitationStatus status); Optional<GroupInvitation> findByIdAndGroupIdAndInviteeUserIdAndStatus(Long id, Long groupId, Long inviteeUserId, GroupInvitationStatus status); + +Optional<GroupInvitation> findByIdAndGroupIdAndStatus(Long id, Long groupId, GroupInvitationStatus status);src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
12-26: 모든 Operation에 보안 요구사항(@securityrequirement) 명시 누락실제 컨트롤러는 인증이 필요하므로 Swagger 문서에도 access-token 스키마를 명시해 주세요. 기존 UserControllerDocs와 일관성을 맞추는 것이 좋습니다.
package project.flipnote.group.controller.docs; import org.springframework.http.ResponseEntity; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; ... @Tag(name = "Group Invitation", description = "Group Invitation API") public interface GroupInvitationControllerDocs { - @Operation(summary = "그룹 초대") + @Operation(summary = "그룹 초대", security = { @SecurityRequirement(name = "access-token") }) ResponseEntity<GroupInvitationCreateResponse> createGroupInvitation( Long groupId, GroupInvitationCreateRequest req, AuthPrinciple authPrinciple ); - @Operation(summary = "그룹 초대 취소") + @Operation(summary = "그룹 초대 취소", security = { @SecurityRequirement(name = "access-token") }) ResponseEntity<Void> deleteGroupInvitation(Long groupId, Long invitationId, AuthPrinciple authPrinciple); - @Operation(summary = "그룹 초대 응답") + @Operation(summary = "그룹 초대 응답", security = { @SecurityRequirement(name = "access-token") }) ResponseEntity<Void> respondToGroupInvitation( Long groupId, Long invitationId, GroupInvitationRespondRequest req, AuthPrinciple authPrinciple ); }src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (2)
26-37: 페이지 파라미터 유효성 검증 추가 권장음수 page/size 입력 방지를 위해 Bean Validation을 적용하세요.
+import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Max; @GetMapping("/groups/{groupId}/invitations") public ResponseEntity<PageResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations( @PathVariable("groupId") Long groupId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, + @Min(0) @RequestParam(defaultValue = "0") int page, + @Min(1) @Max(100) @RequestParam(defaultValue = "20") int size, @AuthenticationPrincipal AuthPrinciple authPrinciple ) {
39-49: 페이지 파라미터 유효성 검증 추가 권장음수 page/size 입력 방지를 위해 Bean Validation을 적용하세요.
@GetMapping("/group-invitations/incoming") public ResponseEntity<PageResponse<IncomingGroupInvitationResponse>> getIncomingInvitations( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, + @Min(0) @RequestParam(defaultValue = "0") int page, + @Min(1) @Max(100) @RequestParam(defaultValue = "20") int size, @AuthenticationPrincipal AuthPrinciple authPrinciple ) {src/main/java/project/flipnote/group/service/GroupInvitationService.java (8)
54-71: 이메일 정규화 및 동시성 처리 개선 필요
- 대소문자/공백 차이로 인한 자기 초대 우회 가능성이 있습니다.
- 동시성 상황에서 중복 초대 레이스 컨디션이 발생할 수 있습니다.
-String inviterUserEmail = authPrinciple.email(); -String inviteeEmail = req.email(); +String inviterUserEmail = authPrinciple.email(); +String inviteeEmail = req.email(); +String normalizedInviter = inviterUserEmail == null ? null : inviterUserEmail.trim().toLowerCase(); +String normalizedInvitee = inviteeEmail == null ? null : inviteeEmail.trim().toLowerCase(); -if (Objects.equals(inviterUserEmail, inviteeEmail)) { +if (Objects.equals(normalizedInviter, normalizedInvitee)) { throw new BizException(GroupInvitationErrorCode.CANNOT_INVITE_SELF); } -Long invitationId = userService.findActiveUserByEmail(inviteeEmail) +Long invitationId = userService.findActiveUserByEmail(normalizedInvitee) .map(inviteeUser -> createUserInvitation(inviterUserId, groupId, inviteeUser)) - .orElseGet(() -> createGuestInvitation(inviterUserId, groupId, inviteeEmail)); + .orElseGet(() -> createGuestInvitation(inviterUserId, groupId, normalizedInvitee));
81-90: 중요한 보안 이슈: IDOR 취약점 존재현재 invitationId와 PENDING 상태만으로 조회하여 다른 그룹의 초대를 삭제할 수 있습니다. groupId 검증이 필요합니다.
-GroupInvitation invitation = groupInvitationRepository - .findByIdAndStatus(invitationId, GroupInvitationStatus.PENDING) +GroupInvitation invitation = groupInvitationRepository + .findByIdAndGroupIdAndStatus(invitationId, groupId, GroupInvitationStatus.PENDING) .orElseThrow(() -> new BizException(GroupInvitationErrorCode.INVITATION_NOT_FOUND));다음 스크립트로 해당 Repository 메서드가 존재하는지 확인해주세요:
#!/bin/bash # GroupInvitationRepository에서 필요한 메서드가 존재하는지 확인 rg "findByIdAndGroupIdAndStatus" src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java
101-119: 초대 수락 시 그룹 용량 및 중복 멤버 검증 누락초대를 수락할 때 그룹 최대 인원과 중복 멤버 여부를 확인하지 않습니다.
if (Objects.equals(invitation.getStatus(), GroupInvitationStatus.ACCEPTED)) { + // 그룹 최대 인원 확인 (GroupJoinService와 동일한 로직 필요) + Group group = groupRepository.getReferenceById(groupId); + long currentMemberCount = groupMemberRepository.countByGroup_Id(groupId); + if (currentMemberCount >= group.getMaxMember()) { + throw new BizException(GroupJoinErrorCode.GROUP_IS_ALREADY_MAX_MEMBER); + } // TODO: GroupMember 에서 group과 user의 id만 가지고 있도록 수정 addGroupMember(inviteeUserId, groupId); }
131-155: Null 처리 로직 개선 필요게스트 초대의 경우
inviteeUserId가 null일 수 있으므로 올바른 필터링이 필요합니다.List<Long> inviteeUserIds = invitationPage.getContent() .stream() - .filter(Objects::nonNull) .map(GroupInvitation::getInviteeUserId) + .filter(Objects::nonNull) + .distinct() .toList();
181-191: 회원가입 시 자동 수락에서 멤버 중복 방지 필요동일 그룹에 이미 멤버인 경우 중복 insert가 발생할 수 있습니다.
for (GroupInvitation invitation : invitations) { invitation.respond(GroupInvitationStatus.ACCEPTED); - - addGroupMember(inviteeUserId, invitation.getGroupId()); + // 이미 멤버면 건너뛰기 + if (!groupMemberRepository.existsByGroup_idAndUser_id(invitation.getGroupId(), inviteeUserId)) { + addGroupMember(inviteeUserId, invitation.getGroupId()); + } }
214-230: 사용자 초대 시 멤버십 상태 및 중복 초대 검증 강화 필요
- 이미 그룹 멤버인 사용자 초대를 방지해야 합니다.
- PENDING 상태의 초대만 중복으로 간주해야 합니다.
private Long createUserInvitation(Long inviterUserId, Long groupId, UserProfile inviteeUser) { + // 이미 그룹 멤버인지 확인 + if (groupMemberRepository.existsByGroup_idAndUser_id(groupId, inviteeUser.getId())) { + throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); + } - if (groupInvitationRepository.existsByGroupIdAndInviteeUserId(groupId, inviteeUser.getId())) { + // PENDING 상태의 초대만 중복으로 간주 + if (groupInvitationRepository.existsByGroupIdAndInviteeUserIdAndStatus( + groupId, inviteeUser.getId(), GroupInvitationStatus.PENDING)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); } + // 이메일 기반 중복 초대 확인 + if (groupInvitationRepository.existsByGroupIdAndInviteeEmailAndStatus( + groupId, inviteeUser.getEmail(), GroupInvitationStatus.PENDING)) { + throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); + }
240-255: 게스트 초대의 중복 판단을 PENDING 상태로 제한 필요거절/만료된 과거 초대가 있어도 재초대가 가능해야 합니다.
-if (groupInvitationRepository.existsByGroupIdAndInviteeEmail(groupId, inviteeEmail)) { +if (groupInvitationRepository.existsByGroupIdAndInviteeEmailAndStatus( + groupId, inviteeEmail, GroupInvitationStatus.PENDING)) { throw new BizException(GroupInvitationErrorCode.ALREADY_INVITED); }
264-272: 그룹 멤버 중복 추가 방지 로직 필요중복 멤버 생성을 방지하기 위한 idempotent 처리가 필요합니다.
private void addGroupMember(Long inviteeUserId, Long groupId) { + // 이미 멤버인지 확인 (idempotent) + if (groupMemberRepository.existsByGroup_idAndUser_id(groupId, inviteeUserId)) { + return; + } GroupMember groupMember = GroupMember.builder() .group(groupRepository.getReferenceById(groupId)) .user(em.getReference(UserProfile.class, inviteeUserId)) .role(GroupMemberRole.MEMBER) .build(); groupMemberRepository.save(groupMember); }
🧹 Nitpick comments (5)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
23-27: 조회 패턴 최적화를 위한 색인(Index) 권장groupId, inviteeUserId, inviteeEmail, status 조합으로 조회가 빈번합니다. DB 인덱스를 추가해 주세요. 예: GroupInvitation 엔티티에 복합 인덱스
엔티티 예시(참고, 다른 파일에 적용 필요):
// @Table(name = "group_invitations", indexes = { // @Index(name = "idx_ginv_group_status", columnList = "groupId,status"), // @Index(name = "idx_ginv_invitee_user_status", columnList = "inviteeUserId,status"), // @Index(name = "idx_ginv_invitee_email_status", columnList = "inviteeEmail,status") // })src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (2)
6-17: Null 체크 간결화 및 switch default 제거 제안
- Objects.requireNonNull로 NPE 명확화하면 가독성이 좋아집니다.
- enum switch는 모든 상수를 열거했으므로 default는 불필요합니다(향후 상수 추가 시 컴파일 타임 검출 장점).
- public static GroupInvitationStatus from(project.flipnote.group.entity.GroupInvitationStatus status) { - if (status == null) { - throw new IllegalArgumentException("GroupInvitationStatus is null"); - } - - return switch (status) { - case PENDING -> PENDING; - case ACCEPTED -> ACCEPTED; - case REJECTED -> REJECTED; - default -> throw new IllegalArgumentException("Unknown GroupInvitationStatus: " + status); - }; - } + public static GroupInvitationStatus from(project.flipnote.group.entity.GroupInvitationStatus status) { + java.util.Objects.requireNonNull(status, "GroupInvitationStatus is null"); + return switch (status) { + case PENDING -> PENDING; + case ACCEPTED -> ACCEPTED; + case REJECTED -> REJECTED; + }; + }
3-4: 엔티티/모델 enum 동명이인으로 인한 혼동 가능성모델과 엔티티에서 모두 GroupInvitationStatus 이름을 사용하고 있어 정규화가 필요합니다. 모델을 ApiGroupInvitationStatus(혹은 GroupInvitationStatusModel)로 리네이밍하면 가독성과 유지보수성이 개선됩니다. 대책이 어렵다면 최소한 Javadoc에 “entity enum과 구분되는 API 표기 전용 enum”임을 명시해 주세요.
src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java (1)
14-15: JsonFormat 타임존 명시 고려패턴만 지정하면 서버/런타임 타임존에 따라 응답이 달라질 수 있습니다. 전역 설정과 일치하도록 timezone 속성(예: UTC 혹은 Asia/Seoul)을 명시해 주세요.
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (1)
9-19: 게스트 초대(null 필드) 대응 및 PII 최소화
- 게스트 초대의 경우 inviteeUserId/inviteeNickname이 null일 수 있습니다. 불필요한 null 노출을 줄이기 위해 NON_NULL 직렬화를 권장합니다.
- 외부로 inviteeEmail 노출이 필요한지 비즈니스 재확인 필요합니다(개인정보 최소화 원칙). 내부 운영/초대 발신자 전용이라면 OK, 그 외엔 마스킹/제거 고려.
package project.flipnote.group.model; import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import project.flipnote.group.entity.GroupInvitation; +@JsonInclude(Include.NON_NULL) public record OutgoingGroupInvitationResponse( Long invitationId, Long inviterUserId, Long inviteeUserId, String inviteeEmail, String inviteeNickname, GroupInvitationStatus status,추가로 createdAt의 타임존도 Incoming과 동일하게 맞추는 것을 권장합니다.
- @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")검증 요청:
- 해당 DTO를 반환하는 API가 적절한 권한(초대 발신자/그룹 관리자)에만 노출되는지 확인해 주세요.
- 이메일 노출이 꼭 필요한지, 필요하다면 마스킹 정책(예: local-part 일부 별표 처리)을 적용할지 논의가 필요합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java(1 hunks)src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java(1 hunks)src/main/java/project/flipnote/group/model/GroupInvitationStatus.java(1 hunks)src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java(1 hunks)src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java
🧰 Additional context used
🧬 Code Graph Analysis (7)
src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (3)
src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java (1)
GroupJoinStatus(3-5)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
updateStatus(62-64)src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (2)
src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java (1)
Tag(10-28)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (4)
src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java (2)
AuthControllerDocs(19-53)Operation(45-46)src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java (1)
Tag(10-28)src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (1)
Tag(14-28)src/main/java/project/flipnote/group/controller/GroupController.java (1)
RequiredArgsConstructor(18-31)
src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java (7)
src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)
src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (8)
src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java (1)
GroupJoinResponse(5-11)src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
GroupCreateResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java (1)
GroupJoinRespondResponse(3-9)src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
GroupJoinListResponse(7-13)src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
GroupInfoResponse(3-9)src/main/java/project/flipnote/group/model/GroupMemberInfoResponse.java (1)
GroupMemberInfoResponse(3-9)src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (1)
FindGroupJoinListMeResponse(7-13)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (7)
src/main/java/project/flipnote/group/repository/GroupRepository.java (1)
Repository(15-22)src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
Repository(12-19)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
Repository(13-20)src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java (1)
Repository(9-12)src/main/java/project/flipnote/user/repository/UserProfileRepository.java (1)
UserProfileRepository(10-17)src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)
Repository(8-10)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
RequiredArgsConstructor(23-63)src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (1)
RequiredArgsConstructor(19-50)src/main/java/project/flipnote/user/service/UserService.java (1)
RequiredArgsConstructor(27-118)
🔇 Additional comments (5)
src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java (1)
9-16: 구조 적절합니다레코드 구성 및 매핑이 명확하고 일관적입니다.
src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (1)
12-29: LGTM! Swagger 문서화가 잘 정의되었습니다.API 문서화 인터페이스가 올바르게 구성되어 있습니다. 한국어 요약도 명확하고 파라미터 타입도 적절합니다.
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (3)
30-39: LGTM! 그룹 초대 생성 API가 올바르게 구현되었습니다.HTTP 상태 코드 201 Created 사용과 요청 검증이 적절합니다.
41-50: LGTM! 그룹 초대 삭제 API가 올바르게 구현되었습니다.적절한 파라미터 매핑과 응답 처리가 되어 있습니다.
52-62: LGTM! 그룹 초대 응답 API가 올바르게 구현되었습니다.요청 검증과 서비스 호출이 적절히 구현되어 있습니다.
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: 1
♻️ Duplicate comments (5)
src/main/java/project/flipnote/user/service/UserService.java (2)
92-94: 이메일 조회 시 정규화 처리가 필요합니다.이메일 조회 전에 입력값의 공백 제거 및 대소문자 정규화가 필요합니다. 현재 구현은 대소문자나 공백으로 인한 조회 실패 가능성이 있습니다.
제안 수정:
public Optional<UserProfile> findActiveUserByEmail(String email) { - return userProfileRepository.findByEmailAndStatus(email, UserStatus.ACTIVE); + if (email == null || email.isBlank()) { + return Optional.empty(); + } + String normalized = email.trim().toLowerCase(java.util.Locale.ROOT); + return userProfileRepository.findByEmailAndStatus(normalized, UserStatus.ACTIVE); }
112-117: 입력값 방어 로직 및 중복 제거가 필요합니다.현재 구현은 null/빈 리스트 입력 시 JPA
IN ()오류와 중복 ID로 인한toMap충돌 가능성이 있습니다.제안 수정:
public Map<Long, String> getIdAndNicknames(List<Long> inviteeUserIds) { + if (inviteeUserIds == null || inviteeUserIds.isEmpty()) { + return java.util.Collections.emptyMap(); + } + List<Long> distinctIds = inviteeUserIds.stream() + .filter(java.util.Objects::nonNull) + .distinct() + .toList(); + if (distinctIds.isEmpty()) { + return java.util.Collections.emptyMap(); + } - List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(inviteeUserIds); + List<UserIdNickname> idAndNicknames = userProfileRepository.findIdAndNicknameByIdIn(distinctIds); return idAndNicknames.stream() - .collect(Collectors.toMap(UserIdNickname::getId, UserIdNickname::getNickname)); + .collect(Collectors.toMap( + UserIdNickname::getId, + UserIdNickname::getNickname, + (existing, replacement) -> existing + )); }src/main/java/project/flipnote/group/entity/GroupInvitation.java (2)
58-65: 초대 대상 검증 로직 부족생성자에서
inviteeUserId와inviteeEmail중 정확히 하나만 설정되어야 하는 불변조건 검증이 누락되었습니다. 두 값이 모두 null이거나 모두 설정된 상태를 방지해야 합니다.@Builder public GroupInvitation(Group group, Long inviterUserId, Long inviteeUserId, String inviteeEmail) { + // 초대 대상은 회원 ID 또는 이메일 중 정확히 하나만 설정되어야 함 + boolean hasUser = inviteeUserId != null; + boolean hasEmail = inviteeEmail != null && !inviteeEmail.isBlank(); + if (hasUser == hasEmail) { + throw new IllegalArgumentException("초대 대상은 회원 ID 또는 이메일 중 하나만 설정되어야 합니다."); + } this.group = group; this.inviterUserId = inviterUserId; this.inviteeUserId = inviteeUserId; this.inviteeEmail = inviteeEmail; this.status = GroupInvitationStatus.PENDING; }
67-69: 상태 전이 검증 로직 부재
respond메서드에서 임의의 상태 변경이 가능합니다. PENDING → ACCEPTED/REJECTED로만 전이를 허용하고, 이미 처리된 초대의 재변경을 막아야 합니다.public void respond(GroupInvitationStatus status) { + if (this.status != GroupInvitationStatus.PENDING) { + throw new IllegalStateException("이미 처리된 초대입니다. 현재 상태=" + this.status); + } + if (status == null || status == GroupInvitationStatus.PENDING) { + throw new IllegalArgumentException("유효하지 않은 상태 전이입니다: " + status); + } this.status = status; }src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
17-19: PENDING 상태 필터링 메서드 추가 필요현재 존재 여부 검사 메서드들이 상태를 고려하지 않아 과거 ACCEPTED/REJECTED 기록이 있어도 재초대를 막을 수 있습니다. PENDING 상태만 중복으로 간주하는 메서드 추가가 필요합니다.
boolean existsByGroup_IdAndInviteeUserId(Long groupId, Long inviteeUserId); boolean existsByGroup_IdAndInviteeEmail(Long groupId, String inviteeEmail); + +// PENDING 상태만 중복 검사 +boolean existsByGroup_IdAndInviteeUserIdAndStatus(Long groupId, Long inviteeUserId, GroupInvitationStatus status); +boolean existsByGroup_IdAndInviteeEmailAndStatus(Long groupId, String inviteeEmail, GroupInvitationStatus status);서비스에서 GroupInvitationStatus.PENDING을 전달하여 대기 중인 초대만 중복으로 간주하도록 수정해주세요.
🧹 Nitpick comments (1)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
41-51: 이메일 기반 조회에서 대소문자 구분 문제이메일 조회 시 대소문자 구분으로 인한 누락 가능성이 있습니다. 이메일 정규화가 확실하지 않다면 IgnoreCase 메서드 추가를 고려해주세요.
List<GroupInvitation> findAllWithGroupByInviteeEmailAndStatus( @Param("inviteeEmail") String inviteeEmail, @Param("status") GroupInvitationStatus status ); + +// 대소문자 구분 없는 이메일 조회 +@Query(""" + SELECT gi + FROM GroupInvitation gi + JOIN FETCH gi.group g + WHERE LOWER(gi.inviteeEmail) = LOWER(:inviteeEmail) + AND gi.status = :status + """) +List<GroupInvitation> findAllWithGroupByInviteeEmailIgnoreCaseAndStatus( + @Param("inviteeEmail") String inviteeEmail, + @Param("status") GroupInvitationStatus status +);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java(1 hunks)src/main/java/project/flipnote/group/entity/Group.java(2 hunks)src/main/java/project/flipnote/group/entity/GroupInvitation.java(1 hunks)src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java(1 hunks)src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java(1 hunks)src/main/java/project/flipnote/group/service/GroupInvitationService.java(1 hunks)src/main/java/project/flipnote/user/model/UserIdNickname.java(1 hunks)src/main/java/project/flipnote/user/repository/UserProfileRepository.java(2 hunks)src/main/java/project/flipnote/user/service/UserService.java(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- src/main/java/project/flipnote/user/repository/UserProfileRepository.java
- src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java
- src/main/java/project/flipnote/group/service/GroupInvitationService.java
- src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java
- src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java
- src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/project/flipnote/user/model/UserIdNickname.java (2)
src/main/java/project/flipnote/infra/oauth/model/OAuth2UserInfo.java (1)
OAuth2UserInfo(3-11)src/main/java/project/flipnote/user/model/UserInfoResponse.java (1)
UserInfoResponse(5-14)
src/main/java/project/flipnote/group/entity/Group.java (2)
src/main/java/project/flipnote/group/service/GroupService.java (1)
validateMaxMember(124-128)src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (4)
checkMaxMember(61-71)GroupJoinErrorCode(76-76)GroupErrorCode(64-64)GroupErrorCode(52-52)
src/main/java/project/flipnote/user/service/UserService.java (3)
src/main/java/project/flipnote/auth/service/AuthService.java (1)
findActiveAuthAccountByEmail(195-198)src/main/java/project/flipnote/user/entity/UserProfile.java (1)
Getter(22-83)src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (1)
findByEmailAndStatus(17-17)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (4)
src/main/java/project/flipnote/group/repository/GroupRepository.java (1)
Repository(15-22)src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
Repository(12-19)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
Repository(13-20)src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
Getter(22-65)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (3)
Getter(22-65)Builder(46-60)updateStatus(62-64)
🔇 Additional comments (8)
src/main/java/project/flipnote/user/model/UserIdNickname.java (1)
1-6: 새로운 projection 인터페이스가 잘 설계되었습니다!Spring Data JPA의 projection 패턴을 적절히 활용하여 필요한 필드만 선택적으로 조회할 수 있도록 구현되었습니다. ID와 닉네임만 조회하는 용도로 성능상 이점이 있습니다.
src/main/java/project/flipnote/user/service/UserService.java (3)
3-7: import 구문이 적절하게 추가되었습니다.새로 추가된 기능들에 필요한 import들이 정확히 포함되어 있습니다.
17-17: UserIdNickname import가 적절히 추가되었습니다.새로 생성한 projection 인터페이스를 정확히 import했습니다.
55-55: 메서드명 변경이 일관성 있게 적용되었습니다.
findActiveUserById에서findActiveUserByIdOrThrow로 이름을 변경하여 메서드의 예외 발생 동작을 명확히 표현했습니다. 모든 호출 지점이 일관되게 업데이트되었습니다.Also applies to: 63-63, 76-76, 82-82, 87-87
src/main/java/project/flipnote/group/entity/Group.java (2)
19-20: 신규 import 추가 확인그룹 초대 기능을 위한 예외 처리 관련 import가 올바르게 추가되었습니다.
73-77: validateJoinable 메서드 호출 위치 확인 완료GroupInvitationService 클래스에서 validateJoinable이 아래 두 곳에서 호출되고 있어, 그룹 초대 기능에 적절히 활용되고 있습니다.
- src/main/java/project/flipnote/group/service/GroupInvitationService.java
• invitation.getGroup().validateJoinable() (초대 조회 후)- src/main/java/project/flipnote/group/service/GroupInvitationService.java
• group.validateJoinable() (초대 수락 반복문 내)해당 메서드는 이미 필요한 지점에서 실행되고 있으므로 추가 수정 사항이 없습니다.
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
24-36: 테이블 제약조건과 인덱스가 적절히 설정됨그룹 초대 기능에 필요한 인덱스와 유니크 제약조건이 잘 정의되었습니다:
- 조회 패턴에 맞는 인덱스 설정
- 중복 초대 방지를 위한 유니크 제약조건
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
23-37: 쿼리 성능 검토 완료
기본 키(id) 조회와 unique 제약 인덱스(group_id, invitee_user_id)를 활용하며, JOIN FETCH 시 외래 키 인덱스(group_id)도 사용됩니다. 추가적인 인덱스나 최적화 조치가 필요하지 않습니다.
| public void validateJoinable() { | ||
| if (maxMember < 1 || maxMember >= 100) { | ||
| throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER); | ||
| } | ||
| } |
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.
maxMember 유효성 검증 범위 불일치 문제
Line 74에서 maxMember >= 100으로 상한을 99로 제한하는데, 이는 기존 JPA 어노테이션(@Max(100))과 GroupService.validateMaxMember()의 1-100 범위와 일치하지 않습니다.
관련 코드:
- Line 49:
@Max(100)어노테이션은 100까지 허용 GroupService.validateMaxMember(): 1 <= maxMember <= 100 범위 검증
다음 중 하나로 통일해주세요:
public void validateJoinable() {
- if (maxMember < 1 || maxMember >= 100) {
+ if (maxMember < 1 || maxMember > 100) {
throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER);
}
}또는 JPA 어노테이션과 GroupService 로직을 99로 통일하세요.
📝 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.
| public void validateJoinable() { | |
| if (maxMember < 1 || maxMember >= 100) { | |
| throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER); | |
| } | |
| } | |
| public void validateJoinable() { | |
| if (maxMember < 1 || maxMember > 100) { | |
| throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/entity/Group.java around lines 73 to 77,
the upper bound check currently disallows 100 (uses maxMember >= 100) which
contradicts the @Max(100) JPA annotation and GroupService.validateMaxMember
(1–100). Change the validation to allow 100 by using a strict greater-than check
(maxMember > 100) so the method enforces 1 <= maxMember <= 100 and stays
consistent with the annotation and service validation.
📝 변경 내용
✅ 체크리스트
💬 기타 참고 사항
Summary by CodeRabbit
신기능
개선