Skip to content

Feat: [FN-144][FN-160][FN-161] 즐겨찾기 기능#43

Merged
dungbik merged 4 commits into
developfrom
feat/bookmark
Sep 15, 2025
Merged

Feat: [FN-144][FN-160][FN-161] 즐겨찾기 기능#43
dungbik merged 4 commits into
developfrom
feat/bookmark

Conversation

@dungbik
Copy link
Copy Markdown
Contributor

@dungbik dungbik commented Sep 7, 2025

📝 변경 내용


✅ 체크리스트

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

💬 기타 참고 사항

Summary by CodeRabbit

  • 신기능
    • 사용자가 그룹을 탈퇴하면 해당 그룹의 비공개 카드셋에 대한 본인 북마크가 자동으로 정리됩니다.
  • 버그 수정
    • 북마크 추가/조회 시 사용자별 보기 권한을 반영하여, 볼 수 없는 대상은 북마크할 수 없고 목록에서 제외됩니다.
    • 카드셋 권한 검증이 개선되어 공개 여부와 그룹 멤버십 기준이 정확히 적용됩니다.

@dungbik dungbik self-assigned this Sep 7, 2025
@dungbik dungbik added the enhancement New feature or request label Sep 7, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 7, 2025

Walkthrough

그룹 탈퇴 이벤트(GroupLeftEvent) 도입과 함께, 트랜잭션 커밋 이후 비동기/재시도 리스너가 사용자별 비공개 카드세트 북마크를 일괄 제거하도록 추가되었고, 북마크·카드세트 관련 API들이 사용자 가시성(viewable) 기반으로 변경되었습니다.

Changes

Cohort / File(s) Summary
Event & Listener
src/main/java/project/flipnote/common/model/event/GroupLeftEvent.java, src/main/java/project/flipnote/bookmark/listener/GroupLeftCleanupBookmarkListener.java
GroupLeftEvent 레코드 추가. AFTER_COMMIT 단계의 비동기(@async)·재시도(@retryable) 리스너 추가하여 그룹 탈퇴 시 해당 사용자에 대한 그룹 내 비공개 카드세트 북마크 일괄 삭제 호출 및 실패 시 recover로 로그.
Retry Config
src/main/java/project/flipnote/common/config/RetryConfig.java
Spring Retry 활성화를 위한 @Configuration 클래스 RetryConfig 추가(@EnableRetry).
Bookmark Repository
src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java
다중 대상 ID 기반 일괄 삭제 메서드 deleteByTargetTypeAndUserIdAndTargetIdIn(...) 추가.
Bookmark Core (policy/service/fetch/fetchers)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java, src/main/java/project/flipnote/bookmark/service/BookmarkService.java, src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java, src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkTargetFetcher.java, src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java
기존 존재 검사에서 사용자 가시성 검사로 API 전환: validateTargetExistsvalidateTargetViewable, existsByTypeAndIdisTargetViewable, fetchByTypeAndIds(..., userId)로 시그니처 확장. BookmarkServiceCardSetService 주입 및 removePrivateCardSetBookmarks(groupId, userId) 추가. 카드세트 페처는 사용자별 가시성 기반 필터링으로 변경.
CardSet Repository / Service / Policy
src/main/java/project/flipnote/cardset/repository/CardSetRepository.java, src/main/java/project/flipnote/cardset/service/CardSetService.java, src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java
정책 메서드 validateCardSetViewable 시그니처 정리(그룹 ID 제거) 및 isCardSetViewable 추가. 서비스에 isCardSetViewable(Long, Long), findViewableCardSetsByIds(Set, Long), findPrivateCardSetIds(Long) 추가. 레포지토리에 그룹 내 비공개 카드세트 ID 조회 쿼리 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant App as Application
  participant TX as Transaction Manager
  participant Pub as Event Publisher
  participant L as GroupLeftCleanupBookmarkListener
  participant BS as BookmarkService
  participant CS as CardSetService
  participant BR as BookmarkRepository
  participant CR as CardSetRepository

  User->>App: 그룹 탈퇴 요청 (groupId, userId)
  App->>TX: 트랜잭션 커밋
  TX-->>Pub: AFTER_COMMIT 이벤트 발행
  Pub-->>L: GroupLeftEvent(groupId, userId)
  note over L: @Async + @Retryable(maxAttempts=3, backoff=2s, multiplier=2)

  L->>BS: removePrivateCardSetBookmarks(groupId, userId)
  BS->>CS: findPrivateCardSetIds(groupId)
  CS->>CR: findPrivateIdsByGroupId(groupId)
  CR-->>CS: Set<Long> privateIds
  alt privateIds 존재
    BS->>BR: deleteByTargetTypeAndUserIdAndTargetIdIn(CARD_SET, userId, ids)
    BR-->>BS: deletedCount
  else 없음
    note over BS: 삭제할 항목 없음(무시)
  end
  note over L,BS: 모든 재시도 실패 시 recover()에서 오류 로그
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • stoneTiger0912

Pre-merge checks (1 passed, 1 warning, 1 inconclusive)

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive 현재 PR 설명은 변경 내용 섹션이 비어 있고 체크리스트만 포함되어 있어 실제 구현된 기능이나 수정된 부분에 대한 구체적인 정보가 부족한 매우 일반적인 형식입니다. 변경 내용 섹션에 각 파일에서 수행된 주요 작업과 기능 추가에 대한 간략한 요약을 추가해 설명을 명확히 보완해 주세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed 이 PR 제목은 주요 변경 사항인 즐겨찾기 기능 추가를 간결하게 요약하고 있으며 관련 이슈 번호를 포함해 어떤 기능이 개선되었는지 명확히 전달합니다.

Poem

토끼가 깡충: 이벤트 와르르 🐇
커밋 뒤 살짝, 북마크를 정리하네.
비공개 카드셋, 조용히 안녕—
가시성 체크로 길을 비추고,
떠나는 발자국엔 흔적만 남기네.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ 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/bookmark

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@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: 7

🧹 Nitpick comments (8)
src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)

4-4: Set 사용 OK, 다만 호출부에서 순서 의존 금지

반환 컬렉션이 Set이면 순서가 보장되지 않습니다. 정렬이 필요하면 List 반환과 정렬 기준을 명시하세요.

src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java (1)

4-4: Collection으로 변경 시 import 업데이트 필요

아래처럼 교체하세요.

-import java.util.Set;
+import java.util.Collection;
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1)

32-36: 빈 ID 집합 단축 반환으로 불필요한 DB 호출 제거

targetIds가 비어있을 때 서비스 호출을 생략하면 낭비를 줄일 수 있습니다.

-	public Map<Long, CardSetBookmarkResponse> fetchByIds(Set<Long> targetIds, Long userId) {
-		return cardSetService.findViewableCardSetsByIds(targetIds, userId).stream()
+	public Map<Long, CardSetBookmarkResponse> fetchByIds(Set<Long> targetIds, Long userId) {
+		if (targetIds == null || targetIds.isEmpty()) {
+			return java.util.Collections.emptyMap();
+		}
+		return cardSetService.findViewableCardSetsByIds(targetIds, userId).stream()
 			.map(CardSetBookmarkResponse::from)
 			.collect(Collectors.toMap(CardSetBookmarkResponse::getId, Function.identity()));
 	}
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)

41-47: 빈 집합 단축 반환으로 미미하지만 확실한 최적화

호출 빈도가 높은 경로라면 의미 있는 미세 최적화입니다.

-	public Map<Long, T> fetchByTypeAndIds(
+	public Map<Long, T> fetchByTypeAndIds(
 		BookmarkTargetType targetType,
 		Set<Long> targetIds,
 		Long userId
 	) {
 		BookmarkTargetFetcher<T> targetFetcher = getFetcher(targetType);
-
-		return targetFetcher.fetchByIds(targetIds, userId);
+		if (targetIds == null || targetIds.isEmpty()) {
+			return java.util.Collections.emptyMap();
+		}
+		return targetFetcher.fetchByIds(targetIds, userId);
 	}
src/main/java/project/flipnote/bookmark/listener/GroupLeftCleanupBookmarkListener.java (2)

23-27: 재시도 예외 범위를 한정하세요 (불필요한 영구 실패 재시도 방지).

현재 @Retryable은 모든 Exception을 재시도 대상으로 간주합니다. 네트워크/DB의 일시적 오류만 재시도하도록 예외를 한정하면 안정성과 관찰성이 좋아집니다.

아래처럼 transient 계열만 포함하도록 제안합니다.

@@
+import org.springframework.dao.CannotAcquireLockException;
+import org.springframework.dao.CannotSerializeTransactionException;
+import org.springframework.dao.ConcurrencyFailureException;
+import org.springframework.dao.QueryTimeoutException;
+import org.springframework.transaction.CannotCreateTransactionException;
@@
-	@Retryable(
-		maxAttempts = 3,
-		backoff = @Backoff(delay = 2000, multiplier = 2)
-	)
+	@Retryable(
+		include = {
+			ConcurrencyFailureException.class,
+			CannotAcquireLockException.class,
+			CannotSerializeTransactionException.class,
+			CannotCreateTransactionException.class,
+			QueryTimeoutException.class
+		},
+		maxAttempts = 3,
+		backoff = @Backoff(delay = 2000, multiplier = 2)
+	)

34-37: Recover에서 후속 조치(경보/재처리 경로)를 추가하면 좋습니다.

로그 외에 알림(예: PagerDuty/Slack) 또는 DLQ/아웃박스 적재로 사후 처리 가능성을 확보해 주세요. 실패 누적 카운터(metric)도 함께 기록하면 운영에 유용합니다.

src/main/java/project/flipnote/cardset/service/CardSetService.java (2)

231-235: 없는 카드셋인 경우 false 반환 의미를 명확히 해주세요.

존재하지 않는 카드셋에도 false를 돌려 404와 동일시하지 않습니다. 호출부가 존재 유무와 접근 권한을 구분해야 한다면 Javadoc에 명시하거나 메서드명을 existsAndViewable 등으로 더 분명히 하는 것을 고려해 주세요.


246-252: N+1 가능성: 정책 검사 반복 호출을 저장소 계층으로 내려 최적화 제안.

findAllById(...).stream().filter(cardSetPolicyService.isCardSetViewable(...))는 카드셋마다 추가 쿼리가 발생할 수 있습니다. 규칙이 허용한다면 “뷰어블” 조건을 리포지토리 쿼리로 푸시다운하여 한 번에 필터링하거나, 호출 전 필요한 멤버십/권한 정보를 일괄 프리패치하는 방식으로 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 f55c3e8 and e767200.

📒 Files selected for processing (11)
  • src/main/java/project/flipnote/bookmark/listener/GroupLeftCleanupBookmarkListener.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java (2 hunks)
  • src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/service/BookmarkService.java (5 hunks)
  • src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkTargetFetcher.java (1 hunks)
  • src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (2 hunks)
  • src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1 hunks)
  • src/main/java/project/flipnote/cardset/service/CardSetService.java (2 hunks)
  • src/main/java/project/flipnote/common/model/event/GroupLeftEvent.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/main/java/project/flipnote/common/model/event/GroupLeftEvent.java (4)
src/main/java/project/flipnote/common/model/event/GroupInvitationCreatedEvent.java (1)
  • GroupInvitationCreatedEvent (3-7)
src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java (1)
  • GroupJoinRequestedEvent (5-10)
src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java (1)
  • UserRegisteredEvent (3-7)
src/main/java/project/flipnote/common/model/event/UserWithdrawnEvent.java (1)
  • UserWithdrawnEvent (3-6)
src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1)
src/main/java/project/flipnote/cardset/entity/CardSet.java (1)
  • Getter (22-69)
src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)
src/main/java/project/flipnote/cardset/entity/CardSet.java (1)
  • Getter (22-69)
src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java (2)
src/main/java/project/flipnote/bookmark/entity/Bookmark.java (1)
  • Getter (19-59)
src/main/java/project/flipnote/like/repository/LikeRepository.java (1)
  • LikeRepository (12-18)
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
src/main/java/project/flipnote/cardset/entity/CardSet.java (1)
  • Getter (22-69)
src/main/java/project/flipnote/bookmark/listener/GroupLeftCleanupBookmarkListener.java (2)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (26-130)
src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java (2)
  • Slf4j (16-40)
  • Async (23-31)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1)
src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (1)
  • RequiredArgsConstructor (16-33)
⏰ 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
🔇 Additional comments (10)
src/main/java/project/flipnote/common/model/event/GroupLeftEvent.java (1)

3-7: LGTM — 이벤트 페이로드 설계 적절

groupId, userId만 담은 불변 record로 충분합니다. 이후 리스너에서의 처리와도 잘 맞습니다.

src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)

19-21: 검증 완료 — BookmarkPolicyService의 모든 호출부가 validateTargetViewable(.., userId)로 업데이트되었습니다.

src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1)

54-58: 조회 권한 검증 위임 깔끔합니다

예외 코드 유지하면서 isCardSetViewable로 위임한 방향성 좋습니다.

src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1)

27-29: 뷰어빌리티 기반 체크로 전환 적절합니다

즐겨찾기 대상의 단순 존재 여부 대신 사용자별 가시성으로 검증한 점 좋습니다.

src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)

33-37: API를 사용자 컨텍스트 인지형으로 확장한 점 👍

isTargetViewableuserId를 도입해 정책 일관성이 좋아졌습니다.

src/main/java/project/flipnote/bookmark/service/BookmarkService.java (4)

21-21: CardSetService 주입으로 도메인 간 결합 명확화

그룹 탈퇴 후 비공개 카드셋 정리를 위해 필요한 의존성 추가가 타당합니다.

Also applies to: 34-34


48-48: 대상 검증을 '존재' → '가시성'으로 변경한 점 동의

사용자별 접근 정책과 즐겨찾기 UX가 일치합니다.


105-105: 목록 조회 시 사용자 컨텍스트 반영 OK

fetchByTypeAndIds(..., userId)로 비가시 대상이 자연스럽게 걸러집니다.


104-106: Null 대상 응답 허용 여부 확인 요청

가시성 필터링으로 targetMap에 없는 항목은 BookmarkResponsetarget이 null이 됩니다. API 계약이 이를 허용하는지, 아니면 해당 북마크를 응답에서 제외해야 하는지 확인이 필요합니다.

src/main/java/project/flipnote/cardset/service/CardSetService.java (1)

146-146: 시그니처 변경 반영 OK.

validateCardSetViewable(cardSet, userId)로의 전환이 일관됩니다.

Comment thread src/main/java/project/flipnote/cardset/repository/CardSetRepository.java Outdated
Comment thread src/main/java/project/flipnote/cardset/service/CardSetService.java
Copy link
Copy Markdown

@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: 0

🧹 Nitpick comments (1)
src/main/java/project/flipnote/common/config/RetryConfig.java (1)

6-8: 환경별 토글 가능하도록 속성 기반 활성화 제안

테스트나 로컬에서 필요 시 Retry를 끌 수 있도록 속성 조건을 두는 방안을 제안합니다. 운영 기본 ON, 필요 시 flipnote.retry.enabled=false로 비활성화.

 package project.flipnote.common.config;

 import org.springframework.context.annotation.Configuration;
 import org.springframework.retry.annotation.EnableRetry;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

 @EnableRetry
 @Configuration
+@ConditionalOnProperty(name = "flipnote.retry.enabled", havingValue = "true", matchIfMissing = true)
 public class RetryConfig {
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e767200 and 1ff5322.

📒 Files selected for processing (5)
  • src/main/java/project/flipnote/bookmark/service/BookmarkService.java (5 hunks)
  • src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (2 hunks)
  • src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1 hunks)
  • src/main/java/project/flipnote/cardset/service/CardSetService.java (2 hunks)
  • src/main/java/project/flipnote/common/config/RetryConfig.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/project/flipnote/cardset/repository/CardSetRepository.java
  • src/main/java/project/flipnote/bookmark/service/BookmarkService.java
  • src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java
  • src/main/java/project/flipnote/cardset/service/CardSetService.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/project/flipnote/common/config/RetryConfig.java (4)
src/main/java/project/flipnote/common/config/SchedulerConfig.java (1)
  • EnableScheduling (6-9)
src/main/java/project/flipnote/common/config/AuditingConfig.java (1)
  • AuditingConfig (6-10)
src/main/java/project/flipnote/common/config/AsyncConfig.java (1)
  • AsyncConfig (12-29)
src/main/java/project/flipnote/common/config/ShedLockConfig.java (1)
  • EnableSchedulerLock (11-19)
🔇 Additional comments (3)
src/main/java/project/flipnote/common/config/RetryConfig.java (3)

6-9: 전역 Retry 활성화 구성 적절

@Retryable/@recover 기반 리스너가 동작하도록 하는 최소 설정으로 충분합니다. 별도 이슈 없어 보입니다.


3-9: AOP/Retry 의존성 확인 완료
build.gradle에 spring-boot-starter-aopspring-retry 의존성이 모두 포함되어 있습니다.


6-8: Async와 Retryable 조합 동작 검증
GroupLeftCleanupBookmarkListener에서 @async@retryable(maxAttempts=3, backoff=@backoff(delay=2000, multiplier=2))가 결합될 때 실제 비즈니스 로직 실행에 재시도가 적용되는지, 재시도 횟수 및 백오프가 의도대로 로그에 기록되는지 확인해 주세요.

@dungbik dungbik merged commit 9a4bc0c into develop Sep 15, 2025
3 checks passed
@dungbik dungbik deleted the feat/bookmark branch September 15, 2025 14:43
@coderabbitai coderabbitai Bot mentioned this pull request Sep 24, 2025
4 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Dec 4, 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