Skip to content

Conversation

@dungbik
Copy link
Contributor

@dungbik dungbik commented Aug 13, 2025

📝 변경 내용


✅ 체크리스트

  • 코드가 정상적으로 동작함
  • 테스트 코드 통과함
  • 문서(README 등)를 최신화함
  • 코드 스타일 가이드 준수

💬 기타 참고 사항

Summary by CodeRabbit

  • New Features

    • 그룹 초대에 만료(기본 7일) 및 EXPIRED 상태 도입
    • 매시 정각 자동 만료 스케줄러로 만료된 초대 일괄 정리
    • 회원가입 이벤트가 이메일만 포함하도록 변경(이메일 기준 자동 수락 흐름 조정)
  • Refactor

    • 멤버 수(memberCount) 기반 정원 관리 통합 및 멤버 추가를 GroupMemberPolicyService로 이관(분산 락·Redisson 사용)
    • 초대 수락/검증 흐름에서 만료 및 정원 검사 보강
  • Chores

    • ShedLock/Redisson 의존성 및 스케줄링/Redisson 설정 추가, CI에 Redis 서비스 추가
    • 과부하 응답용 에러코드 추가 및 Redis 비밀번호 기본값 처리 변경
  • API

    • 페이징 파라미터 검증 제약 변경(컨트롤러에서 제약 제거, 문서화엔 제약 유지)

@dungbik dungbik requested a review from stoneTiger0912 August 13, 2025 04:34
@dungbik dungbik self-assigned this Aug 13, 2025
@dungbik dungbik added the enhancement New feature or request label Aug 13, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

스케줄러와 Redis 기반 분산 락(ShedLock/Redisson) 의존성·설정이 추가되고, 그룹 초대에 만료(EXPIRED/expiredAt) 로직 및 일괄 만료 스케줄러가 도입되었습니다. 멤버 추가 책임이 GroupMemberPolicyService로 이동하고 이벤트/레포지토리/컨트롤러 시그니처 일부가 변경되었습니다.

Changes

Cohort / File(s) Change Summary
Build Config
build.gradle
ShedLock 및 Redisson 의존성 추가: shedlock-spring:6.9.2, shedlock-provider-redis-spring:6.9.2, redisson-spring-boot-starter:3.46.0.
Scheduling / ShedLock Config
src/main/java/project/flipnote/common/config/SchedulerConfig.java, src/main/java/project/flipnote/common/config/ShedLockConfig.java
스케줄링 활성화 클래스 추가 및 Redis 기반 ShedLock LockProvider 빈 등록, @EnableSchedulerLock 기본 잠금 설정 추가.
Redisson Config / Properties
src/main/java/project/flipnote/common/config/RedissonConfig.java, src/main/resources/application.yml
RedissonClient 빈 추가(클러스터/싱글 모드 분기). Redis 비밀번호 프로퍼티 기본값 빈 문자열로 변경(${SPRING_DATA_REDIS_PASSWORD:}).
Group Entity
src/main/java/project/flipnote/group/entity/Group.java
memberCount 필드(@column, @Min/@max) 추가 및 초기화, increaseMemberCount() 추가, 가입 가능성 검사에서 memberCount >= maxMember 사용 및 예외 변경.
GroupMember Policy
src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java
신규 서비스: addGroupMember(Long inviteeUserId, Long groupId) — Redisson 분산 락, DB FOR UPDATE 조회, 중복 확인, 멤버 생성/저장 및 memberCount 증가(트랜잭션).
Invitation Entity / Enums / Model
src/main/java/project/flipnote/group/entity/GroupInvitation.java, .../GroupInvitationStatus.java (entity & model)
초대 만료 필드 expiredAt(기본 7일) 추가, isExpired(), validateNotExpired(), getStatus() 추가 및 상태 enum에 EXPIRED 추가. 테이블명·인덱스·유니크 제약 일부 변경.
Invitation Repository
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java
WithGroup JPQL 메서드 제거/대체(derived query) 및 bulkExpire(LocalDateTime now) 추가(@Modifying, JPQL로 PENDING·expiredAt < now → EXPIRED 일괄 업데이트).
Scheduler
src/main/java/project/flipnote/group/scheduler/GroupInvitationExpireScheduler.java
매시간(cron "0 0 * * * *", Asia/Seoul) 실행 스케줄러 추가, ShedLock 분산락 적용, repository.bulkExpire(now) 호출 및 처리 로그.
Invitation Service
src/main/java/project/flipnote/group/service/GroupInvitationService.java
초대 만료 검증 추가, 멤버 추가를 GroupMemberPolicyService로 위임. acceptPendingInvitationsOnRegister 시그니처 변경(이메일만) 및 레포지토리 호출 방식 변경.
Events / Listeners
src/main/java/project/flipnote/auth/service/AuthService.java, src/main/java/project/flipnote/common/event/UserRegisteredEvent.java, src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java
UserRegisteredEvent 레코드에서 userId 제거(이제 이메일만). 이벤트 발행부와 리스너 호출부 및 로그 업데이트.
Error Codes
src/main/java/project/flipnote/common/exception/CommonErrorCode.java, src/main/java/project/flipnote/group/exception/GroupErrorCode.java, src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java
CommonErrorCodeSERVICE_TEMPORARILY_UNAVAILABLE 추가. GroupErrorCodeGROUP_IS_ALREADY_MAX_MEMBER, ALREADY_GROUP_MEMBER 추가. GroupInvitationErrorCode에서 ALREADY_GROUP_MEMBER 제거.
Controller / Docs / Repos
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java, .../GroupInvitationQueryControllerDocs.java, src/main/java/project/flipnote/group/repository/GroupRepository.java
컨트롤러 페이징 파라미터 유효성 애노테이션 제거(컨트롤러) 및 docs 클래스에 애노테이션 추가. GroupRepository.findByIdForUpdate 파라미터 바인딩 변경(@Param("id")).
CI
.github/workflows/ci.yml
GitHub Actions에 Redis 서비스 추가(이미지 redis:7.0-alpine, 헬스체크 포함).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Cron as Spring Scheduler
  participant ShedLock as ShedLock(Redis)
  participant Scheduler as GroupInvitationExpireScheduler
  participant Repo as GroupInvitationRepository
  participant DB as Database

  Cron->>Scheduler: runExpireJob()
  Scheduler->>ShedLock: acquire lock "GroupInvitationExpireScheduler_ExpireJob"
  alt lock acquired
    Scheduler->>Repo: bulkExpire(now)
    Repo->>DB: UPDATE ... SET status=EXPIRED WHERE status=PENDING AND expiredAt < now
    DB-->>Repo: updatedCount
    Repo-->>Scheduler: updatedCount
    Scheduler->>ShedLock: release lock
  else lock not acquired
    ShedLock-->>Scheduler: skip execution
  end
Loading
sequenceDiagram
  autonumber
  participant Service as GroupInvitationService
  participant Repo as GroupInvitationRepository
  participant Invitation as GroupInvitation
  participant Policy as GroupMemberPolicyService
  participant Group as Group

  Service->>Repo: findByIdAndGroup_IdAndInviteeUserIdAndStatus(...)
  Repo-->>Service: GroupInvitation
  Service->>Invitation: validateNotExpired()
  Service->>Policy: addGroupMember(inviteeUserId, groupId)
  alt success or ALREADY_GROUP_MEMBER
    Service->>Invitation: mark ACCEPTED
  else other failure
    Service->>Invitation: handle accordingly
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • stoneTiger0912

Poem

"나는 토끼, 새벽 필드에서 깡충—크론이 울리면 락을 건네네.
만료된 초대는 살며시 사라지고, 레디스가 문을 지키네.
멤버 카운트 톡톡, 정책 서비스가 더해지네.
토끼는 풀밭에서 코골며 웃네, 배포 잘 되길 바라는 마음으로 🐇✨"

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/group-invitation

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (5)
src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (1)

11-17: switch 기본절 제거로 매핑 누락을 컴파일 타임에 탐지하세요

기본절이 있으면 새 enum 상수가 추가되어도 컴파일러가 경고하지 않습니다. 기본절을 제거하면 매핑 누락을 컴파일 타임에 강제할 수 있어 안전합니다.

다음과 같이 수정하세요:

-		return switch (status) {
-			case PENDING -> PENDING;
-			case ACCEPTED -> ACCEPTED;
-			case REJECTED -> REJECTED;
-			case EXPIRED -> EXPIRED;
-			default -> throw new IllegalArgumentException("Unknown GroupInvitationStatus: " + status);
-		};
+		return switch (status) {
+			case PENDING -> PENDING;
+			case ACCEPTED -> ACCEPTED;
+			case REJECTED -> REJECTED;
+			case EXPIRED -> EXPIRED;
+		};

추가로, Line 7의 null 체크는 Objects.requireNonNull(status, "...")로 단순화할 수 있습니다.

src/main/java/project/flipnote/group/entity/Group.java (1)

28-29: 상수 중복 사용 리스크: 상수의 단일 소스화 고려

현재 100 상수는 GroupService.validateMaxMember(>100), GroupJoinService 등 여러 위치에서 하드코딩 혹은 상이한 비교로 사용됩니다. 본 상수를 단일 소스로 노출(예: 도메인 공용 상수 or Config)하거나, 서비스/검증 로직에서 본 상수를 참조하도록 일원화하면 유지보수성이 향상됩니다.

src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)

58-64: 대량 만료 성능 최적화: 인덱스 추가 검토(STATUS, EXPIRED_AT).

해당 벌크 업데이트는 조건절에 status, expiredAt을 사용하므로 (status, expired_at) 복합 인덱스가 있으면 MySQL에서 스캔 비용을 크게 줄일 수 있습니다. 현 테이블 인덱스 목록에는 expired_at 관련 인덱스가 없어 보입니다.

DDL 예시:

  • MySQL
    • CREATE INDEX idx_status_expired_at ON group_invitation (status, expired_at);
  • Postgres
    • CREATE INDEX idx_status_expired_at ON group_invitation (status, expired_at);
src/main/java/project/flipnote/group/scheduler/GroupInvitationExpireScheduler.java (1)

34-37: 대량 업데이트 예외/모니터링 보강 제안.

배치로 리팩토링 전까지는 만료 건수에 대한 메트릭 수집(Micrometer 카운터, 로그 구조화)과 예외 발생 시 경고 알림 연동(Slack/Webhook 등)을 권장합니다. 장애 시 초대 처리 누락/적체를 빠르게 감지할 수 있습니다.

원한다면 Micrometer MeterRegistry를 주입해 updatedCount를 Counter로 기록하는 코드 스니펫을 제공하겠습니다.

src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

75-76: 만료 기간 하드코딩(7일) -> 환경 설정으로 외부화 권장.

DEFAULT_EXPIRATION_DAYS(7L)과 LocalDateTime.now().plusDays(...)는 정책 변경에 취약합니다. 환경 변수/설정값으로 주입받아 테스트 가능성과 운영 유연성을 높이는 것을 권장합니다.

예: @value("${group.invitation.expiration-days:7}") long expirationDays

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 088d44f and 106446a.

📒 Files selected for processing (10)
  • build.gradle (1 hunks)
  • src/main/java/project/flipnote/common/config/SchedulerConfig.java (1 hunks)
  • src/main/java/project/flipnote/common/config/ShedLockConfig.java (1 hunks)
  • src/main/java/project/flipnote/group/entity/Group.java (3 hunks)
  • src/main/java/project/flipnote/group/entity/GroupInvitation.java (4 hunks)
  • src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (1 hunks)
  • src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (2 hunks)
  • src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (2 hunks)
  • src/main/java/project/flipnote/group/scheduler/GroupInvitationExpireScheduler.java (1 hunks)
  • src/main/java/project/flipnote/group/service/GroupInvitationService.java (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/main/java/project/flipnote/common/config/SchedulerConfig.java (1)
src/main/java/project/flipnote/common/config/ShedLockConfig.java (1)
  • Configuration (10-17)
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (3)
src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (1)
  • GroupInvitationResponseStatus (3-5)
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (1)
  • GroupInvitationRespondRequest (6-17)
src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (1)
  • OutgoingGroupInvitationResponse (9-32)
src/main/java/project/flipnote/common/config/ShedLockConfig.java (1)
src/main/java/project/flipnote/common/config/RedisConfig.java (2)
  • RedisConfig (10-21)
  • tokenVersionRedisTemplate (13-20)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (3)
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (1)
  • RequiredArgsConstructor (20-51)
src/main/java/project/flipnote/auth/repository/UserAuthRepository.java (1)
  • Modifying (32-34)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (1)
  • Tag (13-30)
src/main/java/project/flipnote/group/scheduler/GroupInvitationExpireScheduler.java (2)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)
  • RequiredArgsConstructor (35-295)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (1)
  • Slf4j (16-37)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (2)
src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (1)
  • OutgoingGroupInvitationResponse (9-32)
src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java (1)
  • IncomingGroupInvitationResponse (9-26)
src/main/java/project/flipnote/group/entity/Group.java (4)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (2)
  • checkMaxMember (61-71)
  • Transactional (161-205)
src/main/java/project/flipnote/group/service/GroupService.java (1)
  • validateMaxMember (139-143)
src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java (1)
  • Getter (22-65)
src/main/java/project/flipnote/group/entity/GroupMember.java (1)
  • Getter (20-48)
src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (4)
src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (1)
  • GroupInvitationResponseStatus (3-5)
src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (1)
  • OutgoingGroupInvitationResponse (9-32)
src/main/java/project/flipnote/group/model/IncomingGroupInvitationResponse.java (1)
  • IncomingGroupInvitationResponse (9-26)
src/main/java/project/flipnote/group/model/GroupInvitationRespondRequest.java (1)
  • GroupInvitationRespondRequest (6-17)
🔇 Additional comments (4)
src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (1)

4-4: EXPIRED 추가에 따른 API/클라이언트 영향 검증 필요

상태값 EXPIRED가 노출되는 모든 API 응답(예: Incoming/OutgoingGroupInvitationResponse)에 대해 클라이언트/문서/테스트가 새 상태를 처리하는지 확인이 필요합니다. 호환성 보장을 위해 e2e/컨트랙트 테스트 추가를 권장합니다.

src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (1)

4-4: GroupInvitation.status가 STRING으로 저장되고 있어 ORDINAL-shifting 이슈 없음 확인됨

  • src/main/java/project/flipnote/group/entity/GroupInvitation.java (63행): @Enumerated(EnumType.STRING) 적용 확인

따라서 EXPIRED 상수 추가로 인한 기존 데이터 손상 우려가 없습니다.

src/main/java/project/flipnote/common/config/SchedulerConfig.java (1)

6-9: 스케줄링 활성화 구성 LGTM

스케줄러 활성화에 필요한 최소 설정이 명확합니다. ShedLock 구성과 함께 정상 동작할 것으로 보입니다.

src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

82-86: 만료 검증 시 에러 코드 의미 재검토.

만료된 경우 INVITATION_NOT_FOUND를 던지는 것은 API 소비자 입장에서 혼란을 줄 수 있습니다. 도메인적으로 INVITATION_EXPIRED 등 보다 명확한 에러 코드가 적합해 보입니다.

해당 에러 코드가 존재한다면 아래처럼 변경을 검토해 주세요.

-    throw new BizException(GroupInvitationErrorCode.INVITATION_NOT_FOUND);
+    throw new BizException(GroupInvitationErrorCode.INVITATION_EXPIRED);

존재하지 않는다면, 새로운 에러 코드를 정의하는 것을 제안합니다.

Copy link

@coderabbitai coderabbitai bot left a 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

🔭 Outside diff range comments (1)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (1)

12-20: GroupInvitation 도메인 ALREADY_GROUP_MEMBER 에러코드 전환 누락

GroupInvitationService.java(222라인)에서 여전히 GroupErrorCode.ALREADY_GROUP_MEMBER를 사용 중입니다. invitation 전용 에러코드인 GroupInvitationErrorCode.ALREADY_GROUP_MEMBER로 변경해야 합니다.

  • src/main/java/project/flipnote/group/service/GroupInvitationService.java:222 – GroupErrorCode.ALREADY_GROUP_MEMBERGroupInvitationErrorCode.ALREADY_GROUP_MEMBER
  • src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java:32 – 가입/권한 검증 로직으로, GroupErrorCode.ALREADY_GROUP_MEMBER 참조 유지 여부 검토
♻️ Duplicate comments (3)
src/main/java/project/flipnote/group/entity/Group.java (1)

22-26: 이전 리뷰 반영: isFull 의미 오류 제거 및 정원 검사 의미 명확화

isFull/경계값 혼동 문제를 없애고, 정원 검증을 memberCount vs maxMember로 명확히 한 방향성은 적절합니다.

src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

65-67: DB 마이그레이션/백필 필수: expired_at NOT NULL 추가

@column(nullable = false)로 expiredAt을 추가했습니다. 운영에 기존 데이터가 있다면 마이그레이션에서 DEFAULT/백필 없이는 실패하거나 NPE 위험이 있습니다.

다음 스크립트로 마이그레이션 파일에 expired_at 추가/백필이 있는지 확인해 주세요.

#!/bin/bash
echo "== Search migration files for expiredAt/expired_at =="
fd -tf -H ".*\.(sql|yml|yaml|xml|java)" | rg -n "expiredAt|expired_at|group_invitation" -S -n || true
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)

35-40: 벌크 업데이트 후 1차 캐시 자동 동기화 설정 적절

@Modifying(clearAutomatically = true, flushAutomatically = true) 적용 좋아요. 벌크 업데이트로 인한 1차 캐시 불일치 문제를 방지합니다. 트랜잭션은 호출 측(서비스/스케줄러)에서 관리하는 현재 구조가 적절합니다.

🧹 Nitpick comments (5)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (2)

29-31: 메서드 네이밍 컨벤션 정렬 제안

핸들러 메서드가 대문자로 시작합니다. 스프링 관례 및 자바 컨벤션에 맞춰 소문자 시작으로 변경을 권장합니다.

아래처럼 변경을 제안합니다.

- public void HandleUserRegisteredEvent(UserRegisteredEvent event) {
+ public void handleUserRegisteredEvent(UserRegisteredEvent event) {

23-31: Retry 범위 축소 고려

현재 @retryable이 예외 제한 없이 적용되어 비회복성 예외에도 재시도가 발생할 수 있습니다. 네트워크/일시적 예외(예: DataAccessException 계열) 등으로 한정하는 것을 권장합니다.

원한다면 include/exclude 파라미터로 구체화하는 패치를 제공하겠습니다.

src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

44-45: 만료 기간 상수의 설정화 제안

DEFAULT_EXPIRATION_DAYS를 하드코딩하기보다 환경설정(예: application.yml)로 외부화하면 운영 중 정책 변경이 용이합니다. Clock 주입(테스트 가능성 확보)도 고려해 보세요.

예: @value("${group.invitation.expiration-days:7}") Long expirationDays, 그리고 LocalDateTime.now() 대신 Clock 주입.

src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (1)

35-39: 불필요한 getReferenceById 호출 제거하고 이미 로드한 엔티티 재사용

행 잠금으로 조회한 lockedGroup이 있으므로 다시 groupRepository.getReferenceById(groupId)를 호출할 필요가 없습니다. 중복 호출을 제거해 의도를 명확히 하세요.

 GroupMember groupMember = GroupMember.builder()
-    .group(groupRepository.getReferenceById(groupId))
+    .group(lockedGroup)
     .user(em.getReference(UserProfile.class, inviteeUserId))
     .role(GroupMemberRole.MEMBER)
     .build();
src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)

130-154: 페이지 쿼리 튜닝 TODO 남김: Projection 도입 시 레이턴시 감소 기대

현재 엔티티 매핑 후 DTO 변환 중 별도 사용자 닉네임 조회가 있습니다. Projection/DTO 조회로 전환하면 N+1 및 불필요한 필드 로딩을 줄일 수 있습니다. 인덱스는 엔티티에 정의되어 있어 조건과 정렬에 맞추면 효과적입니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 106446a and da44fc0.

📒 Files selected for processing (14)
  • build.gradle (1 hunks)
  • src/main/java/project/flipnote/auth/service/AuthService.java (1 hunks)
  • src/main/java/project/flipnote/common/config/ShedLockConfig.java (1 hunks)
  • src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (0 hunks)
  • src/main/java/project/flipnote/group/entity/Group.java (3 hunks)
  • src/main/java/project/flipnote/group/entity/GroupInvitation.java (4 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/repository/GroupInvitationRepository.java (2 hunks)
  • src/main/java/project/flipnote/group/repository/GroupRepository.java (1 hunks)
  • src/main/java/project/flipnote/group/scheduler/GroupInvitationExpireScheduler.java (1 hunks)
  • src/main/java/project/flipnote/group/service/GroupInvitationService.java (5 hunks)
  • src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/project/flipnote/common/event/UserRegisteredEvent.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • build.gradle
  • src/main/java/project/flipnote/common/config/ShedLockConfig.java
  • src/main/java/project/flipnote/group/scheduler/GroupInvitationExpireScheduler.java
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (1)
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (1)
  • UserRegisteredEvent (3-7)
src/main/java/project/flipnote/auth/service/AuthService.java (2)
src/main/java/project/flipnote/common/event/UserRegisteredEvent.java (1)
  • UserRegisteredEvent (3-7)
src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java (1)
  • UserWithdrawnEvent (3-6)
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (4)
src/main/java/project/flipnote/group/entity/GroupInvitationStatus.java (1)
  • GroupInvitationStatus (3-5)
src/main/java/project/flipnote/group/model/GroupInvitationResponseStatus.java (1)
  • GroupInvitationResponseStatus (3-5)
src/main/java/project/flipnote/group/model/GroupInvitationStatus.java (1)
  • GroupInvitationStatus (3-18)
src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java (1)
  • GroupPermissionStatus (3-5)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (5)
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 (1)
  • GroupJoinErrorCode (76-76)
src/main/java/project/flipnote/group/service/GroupService.java (1)
  • GroupErrorCode (74-74)
src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1)
  • CommonErrorCode (8-17)
src/main/java/project/flipnote/group/repository/GroupRepository.java (3)
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (3)
  • Repository (13-23)
  • findByGroup_IdAndUser_Id (21-21)
  • findByGroupAndUser (15-15)
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/Group.java (1)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
  • checkMaxMember (61-71)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (2)
src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (1)
  • OutgoingGroupInvitationResponse (9-32)
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (1)
  • RequiredArgsConstructor (20-51)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
  • Tag (13-28)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
  • PatchMapping (52-62)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
  • Slf4j (30-244)
🔇 Additional comments (7)
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (1)

15-15: 검증 완료: ALREADY_GROUP_MEMBER 잔존 참조 없음 확인
실행한 검색 결과 GroupInvitationErrorCode.ALREADY_GROUP_MEMBERGROUP_INVITATION_005에 대한 어떠한 참조도 발견되지 않았습니다. 문제 없이 코드 변경을 승인합니다.

src/main/java/project/flipnote/group/exception/GroupErrorCode.java (1)

14-16: 그룹 정원/중복 멤버 에러코드 추가 적절합니다

그룹 도메인에 속하는 정책 위반을 GroupErrorCode로 통합한 것은 일관성 측면에서 좋습니다. API 코드 스페이스도 깔끔해졌습니다.

src/main/java/project/flipnote/group/listener/UserRegisteredEventListener.java (1)

30-31: 레코드 시그니처 일치 확인 완료
UserRegisteredEvent가 String email 단일 파라미터로 정의되어 있어 event.email() 호출과 완벽히 일치합니다. 별도 수정 필요 없습니다.

src/main/java/project/flipnote/auth/service/AuthService.java (2)

64-84: 이벤트 발행 시점 적절

@transaction 내에서 발행하고, 리스너는 AFTER_COMMIT으로 처리하여 롤백 시 후속 작업이 실행되지 않도록 한 설계가 적절합니다.


81-81: UserRegisteredEvent 생성자 시그니처 일치 확인 완료
UserRegisteredEvent 레코드 정의를 확인한 결과, 단일 String email 매개변수를 가지므로 new UserRegisteredEvent(email) 호출과 완벽히 일치합니다. 추가 조치 필요 없습니다.

src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

88-96: 만료 판정/노출 로직 일관성 확보되어 좋습니다

PENDING 상태에서만 시간 기반 만료를 적용하고, getStatus가 EXPIRED를 적절히 매핑하는 현재 구현은 이전 지적사항을 잘 반영합니다.

Also applies to: 98-103

src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)

106-113: 만료 검증 추가는 적절함

수락 로직 시작 전에 validateNotExpired 호출로 만료 응답을 차단하는 흐름은 올바릅니다.

Comment on lines 12 to 16
ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."),
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_INVITATION_005", "이미 그룹 회원입니다.");
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

만료된 초대(Expired) 시나리오용 에러 코드를 추가하는 것이 명확합니다

초대 만료 로직이 도입되었다면, 만료 케이스를 NOT_FOUND로 뭉뚱그리기보다 전용 에러코드로 구분하는 편이 API 소비자 관점에서 명확합니다.

아래와 같이 초대 만료 전용 코드를 추가하는 것을 제안합니다.

 public enum GroupInvitationErrorCode implements ErrorCode {
   ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
   NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
   INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
+  INVITATION_EXPIRED(HttpStatus.GONE, "GROUP_INVITATION_005", "만료된 초대입니다."),
   CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");
  • 상태값은 410 GONE을 권장합니다. 클라이언트가 “만료됨”을 명확히 처리할 수 있습니다.
  • 코드값은 기존 005가 비어 있으므로 재사용을 제안합니다.
📝 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.

Suggested change
ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."),
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_INVITATION_005", "이미 그룹 회원입니다.");
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");
public enum GroupInvitationErrorCode implements ErrorCode {
ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
INVITATION_EXPIRED(HttpStatus.GONE, "GROUP_INVITATION_005", "만료된 초대입니다."),
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java
around lines 12 to 16, add a dedicated enum constant for expired invitations
instead of folding it into NOT_FOUND: declare
EXPIRED_INVITATION(HttpStatus.GONE, "GROUP_INVITATION_005", "초대가 만료되었습니다.") (use
HTTP 410 GONE and the unused code 005), place it with the other constants and
ensure commas/semicolon around the enum list remain correct so the enum
compiles.

Comment on lines 25 to 27
@Transactional
public void addGroupMember(Long inviteeUserId, Long groupId) {
Group lockedGroup = groupRepository.findByIdForUpdate(groupId)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력값 null 가드 추가

inviteeUserId가 null일 가능성(게스트 초대 자동 수락 플로우)이 있습니다. 조기에 방어(validation)하지 않으면 em.getReference에서 NPE/IAE 가능성이 있습니다.

아래처럼 간단한 가드를 추가하세요.

 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;

 @Transactional
 public void addGroupMember(Long inviteeUserId, Long groupId) {
+    Assert.notNull(inviteeUserId, "inviteeUserId must not be null");
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java
around lines 25-27, add an early null-guard for inviteeUserId before calling
em.getReference/findByIdForUpdate: check if inviteeUserId == null and handle it
(either return early/skip the auto-accept flow or throw a clear validation
exception such as IllegalArgumentException with a descriptive message), ensuring
no further code calls em.getReference or performs DB operations when
inviteeUserId is null.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🔭 Outside diff range comments (2)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

30-41: Unique 제약이 모든 상태에 적용되어 재초대가 불가능해집니다 (부분 유니크로 전환 필요).

@UniqueConstraint(name = "uq_group_invitee_user", columnNames = {"group_id", "invitee_user_id"})와 이메일용 제약은 PENDING 외의 상태(ACCEPTED/REJECTED/EXPIRED)까지 막아, 한 번이라도 초대가 존재하면 이후 재초대를 영구적으로 방지합니다. 서비스 로직은 "PENDING 중복만" 방지하고 있어 DB 제약과 불일치하며, 실제로는 INSERT 시 무결성 예외가 터질 수 있습니다.

권장:

  • 엔티티의 uniqueConstraints는 제거
  • DB 레벨에서 부분 유니크 인덱스(예: PostgreSQL)로 PENDING 상태에만 유니크 적용
    • 예) CREATE UNIQUE INDEX ... ON group_invitations(group_id, invitee_user_id) WHERE status = 'PENDING'

아래와 같이 엔티티에서 제약을 제거하고, Flyway 스크립트로 부분 유니크 인덱스를 추가하세요.

 @Table(
-    name = "group_invitations",
+    name = "group_invitations",
     indexes = {
       @Index(name = "idx_group_invitee_user", columnList = "group_id, invitee_user_id, status"),
       @Index(name = "idx_group_invitee_email", columnList = "group_id, invitee_email, status"),
       @Index(name = "idx_invitee_user_status", columnList = "invitee_user_id, status"),
       @Index(name = "idx_invitee_email_status", columnList = "invitee_email, status")
-    },
-    uniqueConstraints = {
-      @UniqueConstraint(name = "uq_group_invitee_user", columnNames = {"group_id", "invitee_user_id"}),
-      @UniqueConstraint(name = "uq_group_invitee_email", columnNames = {"group_id", "invitee_email"})
-    }
+    }
 )

추가로, 기존 테이블명이 바뀌었다면(아래 코멘트 참고) 마이그레이션에서 부분 유니크 인덱스를 정확한 테이블명에 맞춰 생성해야 합니다.

src/main/java/project/flipnote/group/service/GroupInvitationService.java (1)

83-91: 보안/권한 이슈: 다른 그룹의 초대를 삭제할 수 있습니다.

현재 삭제는 findByIdAndStatus(...PENDING)만 확인하고 groupId 매칭을 확인하지 않습니다. 특정 그룹에 초대 권한이 있는 사용자가 다른 그룹의 초대를 삭제할 수 있게 됩니다. groupId 조건을 포함한 조회로 교체하세요.

-GroupInvitation invitation = groupInvitationRepository
-    .findByIdAndStatus(invitationId, GroupInvitationStatus.PENDING)
+GroupInvitation invitation = groupInvitationRepository
+    .findByIdAndGroup_IdAndStatus(invitationId, groupId, GroupInvitationStatus.PENDING)
     .orElseThrow(() -> new BizException(GroupInvitationErrorCode.INVITATION_NOT_FOUND));

Repository에 아래 메서드가 없다면 추가가 필요합니다.

Optional<GroupInvitation> findByIdAndGroup_IdAndStatus(Long id, Long groupId, GroupInvitationStatus status);
♻️ Duplicate comments (4)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

65-66: DB 마이그레이션/백필 필요: NOT NULL 컬럼 추가(expired_at).

@Column(nullable = false)expiredAt이 추가되었습니다. 기존 데이터가 있으면 DEFAULT/백필 없이 NOT NULL 제약 추가 시 실패합니다. 마이그레이션에서 안전한 기본값 또는 백필을 반드시 포함하세요.

확인을 위해 아래 스크립트를 실행해 마이그레이션 파일에서 expired_at 관련 변경이 있는지 점검해 주세요.

#!/bin/bash
echo "== Search migration files for expired_at/backfill =="
fd -tf -H ".*\.(sql|yml|yaml|xml|java)" | rg -n "expiredAt|expired_at|group_invitations" -S -n || true
src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (1)

31-33: 입력값 null 가드 누락: inviteeUserId가 null이면 런타임 오류 발생.

회원가입 연동 자동 수락 플로우 등에서 inviteeUserId가 null일 수 있습니다. 조기 검증으로 방어하세요.

 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;

 @Transactional
 public void addGroupMember(Long inviteeUserId, Long groupId) {
-    // TODO: AOP로 분산락 적용할 수 있도록 수정 예정
+    // TODO: AOP로 분산락 적용할 수 있도록 수정 예정
+    Assert.notNull(inviteeUserId, "inviteeUserId must not be null");
src/main/java/project/flipnote/group/service/GroupInvitationService.java (2)

109-120: 수락 시 멤버 추가 실패의 멱등 처리 필요(ALREADY_GROUP_MEMBER 허용).

이미 멤버인 경우 초대 수락은 멱등적으로 성공 처리하는 것이 일반적입니다. 현재는 addGroupMember에서 ALREADY_GROUP_MEMBER가 발생하면 트랜잭션이 롤백됩니다.

 invitation.respond(req.toEntityStatus());

 if (invitation.getStatus() == GroupInvitationStatus.ACCEPTED) {
-    groupMemberPolicyService.addGroupMember(inviteeUserId, groupId);
+    try {
+        groupMemberPolicyService.addGroupMember(inviteeUserId, groupId);
+    } catch (BizException e) {
+        // 접근자 이름은 실제 구현에 맞게 변경(getErrorCode 등)
+        if (e.getErrorCode() == GroupErrorCode.ALREADY_GROUP_MEMBER) {
+            // 멱등 허용: 이미 멤버면 초대만 ACCEPTED로 유지
+        } else {
+            throw e;
+        }
+    }
 }

184-199: 게스트 자동 수락 로직의 심각한 오류: userId 누락 + 실패해도 ACCEPTED로 저장.

  • 게스트 초대의 inviteeUserId는 null일 수 있어 addGroupMember 호출 시 실패합니다.
  • finally 블록에서 무조건 ACCEPTED로 바꿔 멤버 추가 실패(정원 초과 등)여도 ACCEPTED 저장 → 데이터 불일치/UX 혼란.

회원가입 이메일로 유저를 조회해 userId를 확보하고, 멤버 추가 성공 시에만 ACCEPTED로 전환하세요. 이미 멤버 예외는 멱등 허용하되, 그 외 예외는 상태 유지(PENDING)로 두는 것이 안전합니다.

 @Transactional
 public void acceptPendingInvitationsOnRegister(String inviteeEmail) {
     List<GroupInvitation> invitations = groupInvitationRepository
         .findAllByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING);

-    for (GroupInvitation invitation : invitations) {
-        if (invitation.isExpired()) {
-            continue;
-        }
-
-        try {
-            groupMemberPolicyService.addGroupMember(invitation.getInviteeUserId(), invitation.getGroup().getId());
-        } catch (Exception ignored) {
-        } finally {
-            invitation.respond(GroupInvitationStatus.ACCEPTED);
-        }
-    }
+    // 이메일로 가입된 사용자 조회
+    Long inviteeUserId = userService.findActiveUserByEmail(inviteeEmail)
+        .map(UserProfile::getId)
+        .orElse(null);
+    if (inviteeUserId == null) {
+        // 가입 실패/비활성 등 예외 상황: 조용히 종료(또는 로깅)
+        return;
+    }
+
+    for (GroupInvitation invitation : invitations) {
+        if (invitation.isExpired()) {
+            continue;
+        }
+        try {
+            groupMemberPolicyService.addGroupMember(inviteeUserId, invitation.getGroup().getId());
+            invitation.respond(GroupInvitationStatus.ACCEPTED);
+        } catch (BizException e) {
+            if (e.getErrorCode() == GroupErrorCode.ALREADY_GROUP_MEMBER) {
+                // 멱등 허용
+                invitation.respond(GroupInvitationStatus.ACCEPTED);
+            }
+            // 그 외(정원 초과 등)는 PENDING 유지
+        }
+    }
 }
🧹 Nitpick comments (6)
src/main/java/project/flipnote/common/config/RedissonConfig.java (1)

14-24: @value 기반 문자열 파싱 대신 @ConfigurationProperties로의 전환 고려

Redis 설정은 리스트(클러스터 노드), 옵션 유무 등 타입이 다양합니다. @ConfigurationProperties(prefix = "spring.data.redis")로 타입 세이프한 바인딩을 사용하면 split/trim 로직을 제거하고, yml의 리스트 문법도 자연스럽게 지원합니다. 유지보수성과 테스트 용이성이 크게 좋아집니다.

원하시면 RedissonPropertiesRedissonConfig 리팩토링 패치를 작성해드리겠습니다.

src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1)

12-13: 429 SERVICE_TEMPORARILY_UNAVAILABLE 코드 추가 적절

잠시 후 재시도 유도 시나리오(락 획득 실패 등)에 적합한 상태/메시지 구성이며, 도메인 서비스에서의 활용 의도가 명확합니다. 다만, 현재 CommonErrorCode는 int status를 사용하고, 다른 도메인 에러코드는 HttpStatus 필드를 보관하는 등 스타일이 혼재되어 있습니다. 장기적으로는 통일을 권장합니다.

src/main/java/project/flipnote/group/entity/GroupInvitation.java (2)

75-76: 만료 시점 계산을 엔티티에서 직접 하지 않도록 리팩터링 제안.

LocalDateTime.now()는 테스트 어려움/타임존 이슈를 유발합니다. Clock 주입 또는 서비스 계층에서 만료 시점을 계산해 전달하는 방식으로 분리하는 것을 권장합니다. 또한 DEFAULT_EXPIRATION_DAYS를 설정값으로 외부화하면 운영 중 정책 변경이 용이합니다.


82-86: 만료된 초대를 ‘NOT_FOUND’로 매핑하는 정책 재확인 요청.

만료 시 INVITATION_NOT_FOUND로 예외를 던집니다. 의도적으로 존재 여부를 감추는 정책이라면 OK지만, 클라이언트가 “만료됨”을 구분해야 한다면 별도 에러 코드가 더 명확합니다. 요구사항 의도 재확인 바랍니다.

src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (2)

39-43: Redisson tryLock leaseTime이 너무 짧아 조기 해제 위험.

tryLock(2, 3, SECONDS)는 3초 후 락이 자동 해제됩니다. 처리 시간이 3초를 초과하면 다른 노드가 락을 재획득할 수 있습니다. 현재는 DB 행 잠금으로 2차 방어가 되지만(좋음), 분산락 목적을 약화합니다. Watchdog 사용 또는 충분히 긴 leaseTime으로 조정하세요.

- isLocked = lock.tryLock(2, 3, TimeUnit.SECONDS);
+ // 권장 1: watchdog 사용(leaseTime 생략)
+ isLocked = lock.tryLock(2, TimeUnit.SECONDS);
+ // 권장 2: 혹은 처리 시간 상한을 고려해 leaseTime을 넉넉히 설정
+ // isLocked = lock.tryLock(2, 30, TimeUnit.SECONDS);

34-36: 락 키 프리픽스 상수화 제안.

"문자열 상수" 대신 상수 필드로 관리하면 오타를 줄이고 재사용성이 좋아집니다.

-public void addGroupMember(Long inviteeUserId, Long groupId) {
-    // TODO: AOP로 분산락 적용할 수 있도록 수정 예정
-    String lockKey = "group_member_lock:" + groupId;
+private static final String GROUP_MEMBER_LOCK_PREFIX = "group_member_lock:";
+public void addGroupMember(Long inviteeUserId, Long groupId) {
+    // TODO: AOP로 분산락 적용할 수 있도록 수정 예정
+    String lockKey = GROUP_MEMBER_LOCK_PREFIX + groupId;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da44fc0 and f9927d7.

📒 Files selected for processing (9)
  • src/main/java/project/flipnote/common/config/RedissonConfig.java (1 hunks)
  • src/main/java/project/flipnote/common/exception/CommonErrorCode.java (1 hunks)
  • src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (2 hunks)
  • src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (2 hunks)
  • src/main/java/project/flipnote/group/entity/GroupInvitation.java (4 hunks)
  • src/main/java/project/flipnote/group/repository/GroupRepository.java (1 hunks)
  • src/main/java/project/flipnote/group/service/GroupInvitationService.java (6 hunks)
  • src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (1 hunks)
  • src/main/resources/application.yml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/project/flipnote/group/repository/GroupRepository.java
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (2)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (2)
  • Transactional (161-205)
  • Transactional (100-135)
src/main/java/project/flipnote/group/service/GroupService.java (1)
  • Transactional (51-70)
src/main/java/project/flipnote/common/exception/CommonErrorCode.java (7)
src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
  • ErrorCode (3-10)
src/main/java/project/flipnote/group/exception/GroupErrorCode.java (2)
  • Getter (10-25)
  • Override (21-24)
src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java (2)
  • Getter (10-27)
  • Override (23-26)
src/main/java/project/flipnote/user/exception/UserErrorCode.java (1)
  • Getter (9-24)
src/main/java/project/flipnote/auth/exception/AuthErrorCode.java (1)
  • Getter (9-36)
src/main/java/project/flipnote/cardset/exception/CardSetErrorCode.java (1)
  • Getter (9-23)
src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java (1)
  • Getter (8-26)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (2)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
  • GroupInvitationRepository (15-53)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
  • Tag (13-28)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (6)
src/main/java/project/flipnote/group/entity/Group.java (1)
  • Getter (22-89)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
  • GroupInvitationRepository (15-53)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
  • Tag (13-28)
src/main/java/project/flipnote/group/model/GroupInvitationCreateResponse.java (1)
  • GroupInvitationCreateResponse (3-6)
src/main/java/project/flipnote/group/model/OutgoingGroupInvitationResponse.java (1)
  • OutgoingGroupInvitationResponse (9-32)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
  • RequiredArgsConstructor (23-63)
🔇 Additional comments (4)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)

88-103: 만료 판정/상태 노출 로직 LGTM.

  • PENDING 상태에만 시간 기반 만료 적용
  • EXPIRED 상태는 즉시 만료로 간주
  • getStatus에서 일관되게 EXPIRED 반영

이전 리뷰 피드백이 잘 반영되었습니다.

src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)

53-71: 자기 자신 초대 방지/분기 LGTM.

이메일 동일성 체크로 자기 자신 초대를 막고, 유저/게스트 분기 처리도 명확합니다.


223-227: 이미 그룹 멤버인 사용자 초대 시 에러 코드 일관성 LGTM.

GroupErrorCode.ALREADY_GROUP_MEMBER 사용으로 의미가 명확합니다.


109-113: 확인 완료: GroupInvitationRepositoryfindByIdAndGroup_IdAndInviteeUserIdAndStatus 메서드가 정의되어 있어 문제없습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (2)

109-114: 레포지토리 메서드 존재 여부와 정원 동시성 가드 확인 필요

  • findByIdAndGroup_IdAndInviteeUserIdAndStatus(...) 메서드가 실제 레포지토리에 선언돼 있는지 확인이 필요합니다. 기존에는 findWithGroupBy... JPQL을 사용했습니다. 선언 누락 시 런타임/컴파일 오류가 납니다.
  • 정원(capacity) 초과 방지는 GroupMemberPolicyService.addGroupMember 내부에서 분산락/행락과 함께 수행된다는 전제일 텐데, 해당 보장이 없다면 이전에 지적된 동시성 이슈가 재발할 수 있습니다. 서비스 내부에서 그룹 행 잠금 + 현재 멤버 수 카운트를 확인하고 있는지 점검해 주세요.

다음 스크립트로 확인해 보세요.

#!/bin/bash
# 레포지토리 메서드 존재 여부
fd -a GroupInvitationRepository.java | xargs -I{} rg -n "findByIdAndGroup_IdAndInviteeUserIdAndStatus\\(" {}
fd -a GroupMemberPolicyService.java | xargs -I{} rg -n "class GroupMemberPolicyService|addGroupMember\\(|Lock|Redisson|ShedLock|synchronized|@Lock" -n -A 20 {}

191-209: 치명적: 회원가입 시 대기 초대 자동 수락에서 inviteeUserId 누락 가능 + 광범위 예외 삼키기

  • guest 초대(이메일 기반)의 경우 invitation.getInviteeUserId()는 null일 수 있습니다. 현재 구현은 null을 그대로 addGroupMember에 전달하여 NPE/IAE를 유발할 수 있습니다.
  • catch (Exception ignored)로 모든 예외를 삼키면, 실패 원인을 추적할 수 없고 상태도 PENDING으로 남아 재시도 판단이 어려워집니다. 멱등 허용(ALREADY_GROUP_MEMBER)은 허용하되 그 외는 상태 유지 + 로깅이 바람직합니다.
  • 추가로, 이 시점에서 이메일로 가입한 실제 userId를 조회해 사용해야 합니다.

아래와 같이 수정하세요.

 @Transactional
- public void acceptPendingInvitationsOnRegister(String inviteeEmail) {
-   List<GroupInvitation> invitations = groupInvitationRepository
-     .findAllByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING);
-
-   for (GroupInvitation invitation : invitations) {
-     if (invitation.isExpired()) {
-       continue;
-     }
-
-     try {
-       groupMemberPolicyService.addGroupMember(invitation.getInviteeUserId(), invitation.getGroup().getId());
-       invitation.respond(GroupInvitationStatus.ACCEPTED);
-     } catch (BizException ex) {
-       if (ex.getErrorCode() == GroupErrorCode.ALREADY_GROUP_MEMBER) {
-         invitation.respond(GroupInvitationStatus.ACCEPTED);
-       }
-     } catch (Exception ignored) { }
-   }
- }
+ public void acceptPendingInvitationsOnRegister(String inviteeEmail) {
+   List<GroupInvitation> invitations = groupInvitationRepository
+     .findAllByInviteeEmailAndStatus(inviteeEmail, GroupInvitationStatus.PENDING);
+
+   // 이메일로 가입한 사용자 ID를 확정
+   Long inviteeUserId = userService.findActiveUserByEmail(inviteeEmail)
+     .map(UserProfile::getId)
+     .orElse(null);
+   if (inviteeUserId == null) {
+     return; // 방어적 종료(정상적 환경에서는 거의 발생하지 않음)
+   }
+
+   for (GroupInvitation invitation : invitations) {
+     if (invitation.isExpired()) {
+       continue;
+     }
+
+     try {
+       groupMemberPolicyService.addGroupMember(inviteeUserId, invitation.getGroup().getId());
+       invitation.respond(GroupInvitationStatus.ACCEPTED);
+     } catch (BizException ex) {
+       // 이미 멤버인 경우는 멱등 허용
+       if (ex.getErrorCode() == GroupErrorCode.ALREADY_GROUP_MEMBER) {
+         invitation.respond(GroupInvitationStatus.ACCEPTED);
+       } else {
+         // 그 외(정원 초과/락 타임아웃 등) 실패는 상태 유지(PENDING)하여 재시도 가능
+         // TODO: 적절한 로깅/모니터링 추가
+       }
+     }
+   }
+ }

부가 제안:

  • 가능하다면 초대 엔티티에 inviteeUserId를 바인딩하는 도메인 메서드(ex. bindInviteeUser(Long userId))를 도입해, guest 초대가 ACCEPTED 되는 시점에 userId를 세팅해 두면 데이터 일관성이 좋아집니다.
src/main/java/project/flipnote/group/entity/Group.java (1)

88-89: 그룹 생성 시 memberCount=1 초기화는 실제 멤버 테이블과 불일치 위험

현재 생성자에서 memberCount를 1로 초기화하지만, GroupService.create(...)에서 오너 멤버를 즉시 생성/저장하지 않습니다. 이 경우 “멤버는 0명인데 카운터는 1”인 드리프트가 시작 시점부터 발생합니다. memberCount 초기값을 0으로 두고, 오너 저장 직후 정책 흐름(락 + 카운터 증가)으로 1로 맞추는 방식을 권장합니다.

권장 수정(엔티티):

-		this.memberCount = 1;
+		this.memberCount = 0;

권장 수정(서비스; 파일 외 참고): 그룹 생성 직후 오너를 추가하고 카운터를 증가시키세요. 최소 변경으로는 기존 saveGroupOwner를 호출하고 바로 증가시키는 방법이 있습니다.

// src/main/java/project/flipnote/group/service/GroupService.java (참고)
@Transactional
public GroupCreateResponse create(AuthPrinciple authPrinciple, GroupCreateRequest req) {
    UserProfile userProfile = findUser(authPrinciple);
    validateMaxMember(req.maxMember());

    Group group = createGroup(req);

    // 오너 멤버 저장 + 카운터 증가(잠금/정책 서비스가 있다면 그 경로로 일원화 권장)
    saveGroupOwner(group, userProfile);
    group.increaseMemberCount();

    initializeGroupPermissions(group);
    return GroupCreateResponse.from(group.getId());
}

가능하면 GroupMemberPolicyService를 통한 “잠금 + 멤버 추가 + 카운터 증가” 경로로 일원화해 일관성을 보장해 주세요.

🧹 Nitpick comments (1)
src/main/java/project/flipnote/group/entity/Group.java (1)

97-100: 증가 메서드 내 사전 검증 포함 OK. 감소 메서드 추가 제안

increaseMemberCount에 사전 검증을 내장한 점은 좋습니다. 짝을 이루는 decreaseMemberCount(하한 0 가드)도 마련하면 멤버 탈퇴/강퇴 플로우에서 도메인 정합성 유지가 쉬워집니다. validateJoinable는 도메인 내부에서만 쓰인다면 가시성 축소(private)도 검토해 주세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9927d7 and 1f60691.

📒 Files selected for processing (4)
  • build.gradle (1 hunks)
  • src/main/java/project/flipnote/group/entity/Group.java (3 hunks)
  • src/main/java/project/flipnote/group/repository/GroupRepository.java (1 hunks)
  • src/main/java/project/flipnote/group/service/GroupInvitationService.java (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • build.gradle
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (3)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
  • GroupInvitationRepository (15-53)
src/main/java/project/flipnote/group/entity/GroupInvitation.java (1)
  • Getter (21-70)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java (1)
  • Tag (13-28)
src/main/java/project/flipnote/group/entity/Group.java (3)
src/main/java/project/flipnote/group/entity/GroupMember.java (1)
  • Getter (20-48)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
  • checkMaxMember (61-71)
src/main/java/project/flipnote/group/service/GroupService.java (1)
  • Slf4j (31-145)
src/main/java/project/flipnote/group/repository/GroupRepository.java (1)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (2)
  • GroupInvitationRepository (15-53)
  • Query (19-33)
🔇 Additional comments (10)
src/main/java/project/flipnote/group/service/GroupInvitationService.java (6)

19-19: 에러 코드 import 전환 적절함

멤버십 관련 중복 초대/가입 상태는 Group 도메인의 책임이므로 GroupErrorCode로 전환한 결정이 타당합니다.


42-42: 멤버 추가 책임의 단일화 방향 LGTM

GroupMemberPolicyService 주입을 통해 멤버 추가 정책과 동시성 제어를 한 곳으로 모은 방향이 좋습니다. 이 서비스가 트랜잭션 경계/락/정원 검증을 보장하도록 유지해 주세요.


118-126: ALREADY_GROUP_MEMBER 멱등 허용 처리 적절

수락 시 이미 멤버인 경우를 예외 삼키고 진행하는 멱등 처리 방향이 UX/운영 측면에서 적절합니다. 트랜잭션 롤백 조건도 명확합니다.


234-234: ALREADY_GROUP_MEMBER 에러 코드 도메인 정합성 개선

이미 그룹 멤버 여부는 Group 도메인의 규칙이므로 GroupErrorCode로 통일한 점 좋습니다.


193-193: 레포지토리 파생 쿼리 메서드 존재 확인 완료

  • 파일: src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java
    위치: 27행
    선언: List<GroupInvitation> findAllByInviteeEmailAndStatus(String inviteeEmail, GroupInvitationStatus status);

해당 메서드가 정상적으로 선언되어 있어 런타임 오류 우려가 없습니다.


53-57: 영향 범위 확인 완료

createGroupInvitation 메소드의 파라미터 순서 변경에 맞추어 호출부가 모두 업데이트되어 있어 컴파일/런타임 오류가 발생하지 않습니다.

  • src/main/java/project/flipnote/group/controller/GroupInvitationController.java:36
  • src/main/java/project/flipnote/group/controller/docs/GroupInvitationControllerDocs.java:17
src/main/java/project/flipnote/group/entity/Group.java (1)

91-95: 용량 체크 로직 자체는 타당함

memberCount >= maxMember 시 예외를 던지는 현재 로직은 의도에 부합합니다. 잠금 하에서 호출된다는 전제라면 정합성 유지에 충분합니다.

src/main/java/project/flipnote/group/repository/GroupRepository.java (3)

3-4: Optional 임포트 추가는 무해

Optional 임포트 추가는 문제 없습니다.


17-18: Soft-delete 필터 메서드 시그니처 OK

findByIdAndDeletedAtIsNull 시그니처는 의도에 부합합니다.


19-22: @param 임포트 패키지 수정 및 트랜잭션 경계 확인 결과

  • 잘못된 @Param 임포트가 확인되었습니다.
    파일: src/main/java/project/flipnote/group/repository/GroupRepository.java (라인 10)
    기존 io.lettuce.core.dynamic.annotation.Paramorg.springframework.data.repository.query.Param로 교체 필요
  • findByIdForUpdate 호출부(GroupJoinService, GroupMemberPolicyService)에는 @Transactional(readOnly = true) 설정이 없으므로, 현재는 쓰기 가능한 트랜잭션 경계 내에서 호출되고 있습니다.
--- a/src/main/java/project/flipnote/group/repository/GroupRepository.java
+++ b/src/main/java/project/flipnote/group/repository/GroupRepository.java
@@ -10,7 +10,7 @@
-import io.lettuce.core.dynamic.annotation.Param;
+import org.springframework.data.repository.query.Param;
 
 @Lock(LockModeType.PESSIMISTIC_WRITE)
 @Query("select g from Group g where g.id = :id")

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/main/java/project/flipnote/common/config/RedissonConfig.java (1)

11-11: 이전 리뷰 반영 완료 — Spring StringUtils 사용으로 의존성 정합성 개선

Thymeleaf StringUtils 의존을 제거하고 Spring의 StringUtils로 교체한 점 좋습니다. 설정 레이어에서의 불필요한 뷰 레이어 의존이 제거되었습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f60691 and 2dfac03.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml (1 hunks)
  • src/main/java/project/flipnote/common/config/RedissonConfig.java (1 hunks)
  • src/main/java/project/flipnote/group/repository/GroupRepository.java (1 hunks)
  • src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/project/flipnote/group/repository/GroupRepository.java
  • src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java
🧰 Additional context used
🪛 YAMLlint (1.37.1)
.github/workflows/ci.yml

[warning] 37-37: wrong indentation: expected 8 but found 10

(indentation)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Comment on lines +33 to +42
services:
redis:
image: redis:7.0-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

테스트 프로파일의 Redis 설정과 CI의 Redis 서비스가 일치하는지 확인 필요

CI에서는 비밀번호 없이 localhost:6379로 접근 가능한 Redis를 띄웁니다. 애플리케이션의 테스트 프로파일(application-test.yml 등)이 동일한 호스트/포트/비번 없이 접속하도록 설정돼 있는지 확인하세요. 또한 일부 라이브러리는 spring.redis.* 프리픽스를, 현재 RedissonConfig는 spring.data.redis.* 프리픽스를 사용합니다. 설정 키 불일치 시 접속 실패가 발생할 수 있습니다.

다음 스크립트로 리소스 내 Redis 설정 키를 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# 리소스에서 Redis 관련 설정 키/값을 검색
echo "Searching for Redis properties under src/main/resources and test resources..."
fd -t f -a | grep -E 'src/.*/resources/.*\.(ya?ml|properties)$' | while read -r f; do
  echo "---- $f"
  rg -n -A 1 -e 'spring\.data\.redis\.(host|port|password|cluster\.nodes|ssl)' -e 'spring\.redis\.(host|port|password|cluster\.nodes|ssl)' "$f" || true
done

echo
echo "Checking for application-test profile existence..."
fd -t f -a | grep -E 'src/.*/resources/application(-test)?\.(ya?ml|properties)$' || true

Length of output: 1250


테스트 프로파일에 Redis 설정 누락됨 – CI 서비스와 동기화 필요

현재 CI는 비밀번호 없이 localhost:6379로 Redis를 실행하지만, 리포지토리의 설정 파일에는 spring.data.redis.* 또는 spring.redis.* 관련 키가 전혀 없습니다. 또한 RedissonConfig는 spring.data.redis.* 프리픽스를 사용 중이므로, 테스트 프로파일에 명시적인 설정이 반드시 필요합니다.

조치 사항:

  • src/test/resources/application-test.yml (필요 시 src/main/resources/application.yml) 에 아래와 같이 Redis 접속 정보를 추가
    spring:
      data:
        redis:
          host: localhost
          port: 6379
          # 비밀번호가 없으면 빈 문자열로 지정
          password: ""
  • 또는 애플리케이션 전반에서 사용하는 프리픽스가 spring.redis.* 인 경우, RedissonConfig 쪽을 spring.redis.* 로 통일

파일 위치:

  • src/test/resources/application-test.yml
  • (필요 시) src/main/resources/application.yml
🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 37-37: wrong indentation: expected 8 but found 10

(indentation)

🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 33 to 42: CI starts Redis on
localhost:6379 without auth but the project lacks test Redis properties and
RedissonConfig expects spring.data.redis.*; add explicit Redis settings to the
test profile or unify prefixes. Add spring.data.redis.host: localhost,
spring.data.redis.port: 6379 and spring.data.redis.password: "" to
src/test/resources/application-test.yml (or to
src/main/resources/application.yml if needed), or alternatively change
RedissonConfig to use spring.redis.* everywhere to match existing app
properties; ensure keys and prefix naming in config and code match exactly.

⚠️ Potential issue

Redis 서비스 추가는 적절합니다만, YAML 들여쓰기 경고와 포트 매핑 들여쓰기 수정이 필요합니다

  • YAMLlint가 지적한 대로 ports 항목의 리스트 들여쓰기가 과합니다. 깃허브 액션스 파서에 따라 무시될 수도 있지만, 린트 에러로 CI가 실패할 수 있으니 수정 권장합니다.
  • 기능 측면에서는 Redis 서비스 정의, healthcheck, 포트 매핑 모두 적절합니다. 테스트/빌드가 러너(호스트)에서 직접 Redis에 접속한다면 ports 매핑은 유지하는 것이 맞습니다.

아래처럼 ports의 리스트 들여쓰기를 2칸 줄여주세요.

       ports:
-          - 6379:6379
+        - 6379:6379
📝 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.

Suggested change
services:
redis:
image: redis:7.0-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
services:
redis:
image: redis:7.0-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 37-37: wrong indentation: expected 8 but found 10

(indentation)

🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 33 to 42: YAML lint warns about excessive
indentation for the ports list; reduce the indentation of the ports list items
by two spaces so the dash for "- 6379:6379" is aligned under "ports:" as a
proper YAML sequence item, preserving the existing redis service, image,
healthcheck options and port mapping but fixing the list indentation to satisfy
linters and avoid CI failures.

Comment on lines +16 to +27
@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Value("${spring.data.redis.password:}")
private String password;

@Value("${spring.data.redis.cluster.nodes:}")
private String clusterNodes;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

spring.redis.와 spring.data.redis. 동시 지원 + SSL 플래그 추가 제안

현재 모든 프로퍼티가 spring.data.redis.* 프리픽스를 사용합니다. Spring Boot와 서드파티 오토컨피그(예: Spring Data Redis, ShedLock의 RedisLockProvider)는 일반적으로 spring.redis.*를 기본으로 사용합니다. 양쪽 프리픽스를 모두 지원하면 설정 실수를 줄이고 호환성을 높일 수 있습니다. 또한 TLS 환경을 고려해 ssl 플래그도 주입받을 것을 권장합니다.

아래처럼 키 폴백과 ssl 플래그를 추가해 주세요.

-	@Value("${spring.data.redis.host}")
+	@Value("${spring.redis.host:${spring.data.redis.host}}")
 	private String host;

-	@Value("${spring.data.redis.port}")
+	@Value("${spring.redis.port:${spring.data.redis.port}}")
 	private int port;

-	@Value("${spring.data.redis.password:}")
+	@Value("${spring.redis.password:${spring.data.redis.password:}}")
 	private String password;

-	@Value("${spring.data.redis.cluster.nodes:}")
+	@Value("${spring.redis.cluster.nodes:${spring.data.redis.cluster.nodes:}}")
 	private String clusterNodes;
+
+	@Value("${spring.redis.ssl:${spring.data.redis.ssl:false}}")
+	private boolean ssl;
📝 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.

Suggested change
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password:}")
private String password;
@Value("${spring.data.redis.cluster.nodes:}")
private String clusterNodes;
@Value("${spring.redis.host:${spring.data.redis.host}}")
private String host;
@Value("${spring.redis.port:${spring.data.redis.port}}")
private int port;
@Value("${spring.redis.password:${spring.data.redis.password:}}")
private String password;
@Value("${spring.redis.cluster.nodes:${spring.data.redis.cluster.nodes:}}")
private String clusterNodes;
@Value("${spring.redis.ssl:${spring.data.redis.ssl:false}}")
private boolean ssl;
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/common/config/RedissonConfig.java around lines
16-27, the fields only read spring.data.redis.* properties; update the
injections to fall back to spring.redis.* equivalents and add an ssl flag: read
host from spring.data.redis.host with fallback to spring.redis.host, port from
spring.data.redis.port with fallback to spring.redis.port (use Integer if null),
password from spring.data.redis.password with fallback to spring.redis.password
(default empty), clusterNodes from spring.data.redis.cluster.nodes with fallback
to spring.redis.cluster.nodes (default empty), and add a boolean ssl injected
from spring.data.redis.ssl with fallback to spring.redis.ssl (default false);
ensure null-safe defaults so existing behavior remains unchanged.

Comment on lines +32 to +47
if (!clusterNodes.isBlank()) {
config.useClusterServers()
.addNodeAddress(
Arrays.stream(clusterNodes.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(addr -> addr.startsWith("redis://") || addr.startsWith("rediss://") ? addr :
"redis://" + addr)
.toArray(String[]::new)
)
.setPassword(StringUtils.hasText(password) ? password : null);
} else {
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(StringUtils.hasText(password) ? password : null);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

TLS(rediss://) 및 주소 스킴 일관성 처리

현재 클러스터/싱글 모두 스킴이 하드코딩되거나(싱글: redis://), 클러스터는 기존 스킴만 보존하고 기본값은 redis://로 붙입니다. ssl 설정에 따라 rediss:// 스킴을 일관되게 적용할 수 있도록 스킴 변수를 도입하는 것을 권장합니다.

 	public RedissonClient redissonClient() {
 		Config config = new Config();
 
-		if (!clusterNodes.isBlank()) {
+		final String scheme = ssl ? "rediss" : "redis";
+
+		if (!clusterNodes.isBlank()) {
 			config.useClusterServers()
 				.addNodeAddress(
 					Arrays.stream(clusterNodes.split(","))
 						.map(String::trim)
 						.filter(s -> !s.isEmpty())
-						.map(addr -> addr.startsWith("redis://") || addr.startsWith("rediss://") ? addr :
-							"redis://" + addr)
+						.map(addr ->
+							(addr.startsWith("redis://") || addr.startsWith("rediss://"))
+								? addr
+								: (scheme + "://" + addr)
+						)
 						.toArray(String[]::new)
 				)
 				.setPassword(StringUtils.hasText(password) ? password : null);
 		} else {
 			config.useSingleServer()
-				.setAddress("redis://" + host + ":" + port)
+				.setAddress(scheme + "://" + host + ":" + port)
 				.setPassword(StringUtils.hasText(password) ? password : null);
 		}
📝 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.

Suggested change
if (!clusterNodes.isBlank()) {
config.useClusterServers()
.addNodeAddress(
Arrays.stream(clusterNodes.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(addr -> addr.startsWith("redis://") || addr.startsWith("rediss://") ? addr :
"redis://" + addr)
.toArray(String[]::new)
)
.setPassword(StringUtils.hasText(password) ? password : null);
} else {
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(StringUtils.hasText(password) ? password : null);
}
// introduce a scheme variable to pick redis:// vs rediss:// based on SSL
final String scheme = ssl ? "rediss" : "redis";
if (!clusterNodes.isBlank()) {
config.useClusterServers()
.addNodeAddress(
Arrays.stream(clusterNodes.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(addr ->
(addr.startsWith("redis://") || addr.startsWith("rediss://"))
? addr
: (scheme + "://" + addr)
)
.toArray(String[]::new)
)
.setPassword(StringUtils.hasText(password) ? password : null);
} else {
config.useSingleServer()
.setAddress(scheme + "://" + host + ":" + port)
.setPassword(StringUtils.hasText(password) ? password : null);
}
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/common/config/RedissonConfig.java around lines
32 to 47, introduce a scheme variable (e.g. String scheme = useSsl ? "rediss://"
: "redis://") and use it for both cluster and single-server address
construction: for cluster, preserve existing "redis://" or "rediss://" prefixes
but otherwise prefix with scheme; for single-server, build the address using
scheme + host + ":" + port; keep the existing password handling unchanged.
Ensure the code reads the SSL flag (or config property) to set useSsl and then
consistently applies the derived scheme for all addresses.

@dungbik dungbik merged commit 04e50bf into develop Aug 13, 2025
3 checks passed
@dungbik dungbik deleted the feat/group-invitation branch August 13, 2025 09:27
@coderabbitai coderabbitai bot mentioned this pull request Sep 15, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants