Skip to content

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

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

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

Conversation

@dungbik
Copy link
Copy Markdown
Contributor

@dungbik dungbik commented Sep 3, 2025

📝 변경 내용


✅ 체크리스트

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

💬 기타 참고 사항

Summary by CodeRabbit

  • New Features
    • 카드셋 북마크 기능 추가: 북마크 추가/삭제 및 내 북마크 목록 조회(페이지네이션). 모든 엔드포인트는 인증 필요.
  • Documentation
    • Swagger에 북마크 API 문서 추가.
  • Refactor
    • 여러 목록 API의 페이지 크기를 요청값과 동일하게 정규화(getSize() 그대로 사용). 카드셋/그룹 초대/좋아요/공통 페이징에서 페이지당 아이템 수가 예측 가능해졌습니다.

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

coderabbitai Bot commented Sep 3, 2025

Walkthrough

북마크 기능을 신규 도입. 컨트롤러/문서 인터페이스 추가, 엔티티/리포지토리/에러코드/모델 정의, 서비스 계층(정책/코어/타깃 페처) 구현, 카드셋 전용 페처 추가. 좋아요/카드셋/공통 페이징 일부 시그니처와 페이지 크기 로직을 Set 기반 및 size 사용으로 정비. IdResponse 레코드 추가.

Changes

Cohort / File(s) Summary
Bookmark API 컨트롤러 & 문서
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java, .../bookmark/controller/docs/BookmarkControllerDocs.java
v1 북마크 REST 엔드포인트 추가(POST/DELETE/GET). Swagger 문서 인터페이스 도입. 인증 주체로 userId 추출.
Bookmark 도메인 & 저장소
.../bookmark/entity/Bookmark.java, .../bookmark/entity/BookmarkTargetType.java, .../bookmark/repository/BookmarkRepository.java, .../bookmark/exception/BookmarkErrorCode.java
북마크 엔티티 및 인덱스/유니크 제약 정의, 타깃 타입 enum 추가, JPA 리포지토리 쿼리 메서드 3종 추가, 에러코드 집약.
Bookmark 모델
.../bookmark/model/BookmarkResponse.java, .../bookmark/model/BookmarkSearchRequest.java, .../bookmark/model/BookmarkTargetResponse.java, .../bookmark/model/BookmarkTargetType.java, .../bookmark/model/CardSetBookmarkResponse.java
응답/검색 요청/타깃 응답 베이스/외부 API용 타깃 타입 및 카드셋 응답 모델 추가. 모델→도메인 타입 매핑 추가.
Bookmark 서비스 코어
.../bookmark/service/BookmarkService.java, .../bookmark/service/BookmarkPolicyService.java, .../bookmark/service/BookmarkTargetFetchService.java
추가/삭제/조회 비즈니스 로직 구현, 정책 검증(중복/존재 여부), 타깃 페처 라우팅/초기화 및 위임 추가.
Bookmark 타깃 페처(카드셋)
.../bookmark/service/fetcher/BookmarkCardSetFetcher.java, .../bookmark/service/fetcher/BookmarkTargetFetcher.java
카드셋용 페처 구현 및 페처 SPI 인터페이스 도입. 존재 확인과 다건 조회(Map 반환) 제공.
공통 페이징 정비
.../common/model/request/PagingRequest.java, .../like/model/LikeSearchRequest.java, .../group/model/GroupInvitationListRequest.java, .../cardset/model/CardSetSearchRequest.java
PageRequest 크기 계산에서 +1 제거(size 그대로 사용). 정렬/페이지 계산은 유지.
CardSet 서비스 적응
.../cardset/service/CardSetService.java
getCardSetsByIds 인자 타입을 List→Set으로 변경. 호출부 적응.
Like 모듈 정비
.../like/entity/Like.java, .../like/model/LikeTargetTypeRequest.java, .../like/service/LikeService.java, .../like/service/fetcher/LikeTargetFetcher.java, .../like/service/fetcher/LikeCardSetFetcher.java
테이블 인덱스/제약 이름 및 컬럼명 정합화, switch 표현식 간소화, targetIds Set 사용, 페처 시그니처 List→Set, 카드셋 페처 클래스명 변경.
공통 응답 래퍼
.../common/model/response/IdResponse.java
식별자 응답 레코드 추가 및 정적 팩토리 제공.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant C as BookmarkController
  participant S as BookmarkService
  participant P as BookmarkPolicyService
  participant R as BookmarkRepository
  participant F as BookmarkTargetFetchService

  rect rgb(240,248,255)
  note over U,C: POST /v1/bookmarks/{targetType}/{targetId}
  U->>C: addBookmark(targetType, targetId)
  C->>S: addBookmark(userId, domainTargetType, targetId)
  S->>P: validateBookmarkNotExists(type, userId, targetId)
  P-->>S: ok
  S->>P: validateTargetExists(type, targetId)
  P->>F: existsByTypeAndId(type, targetId)
  F-->>P: boolean
  P-->>S: ok or throws
  S->>R: save(new Bookmark)
  R-->>S: Bookmark(id)
  S-->>C: IdResponse(id)
  C-->>U: 201 Created (id)
  end
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant C as BookmarkController
  participant S as BookmarkService
  participant R as BookmarkRepository
  participant F as BookmarkTargetFetchService

  rect rgb(245,255,250)
  note over U,C: GET /v1/bookmarks/{targetType}?page,size
  U->>C: getBookmarks(targetType, req)
  C->>S: getBookmarks(userId, domainTargetType, req)
  S->>R: findAllByTargetTypeAndUserId(type, userId, PageRequest)
  R-->>S: Page<Bookmark>
  S->>S: map targetIds(Set) & bookmarkedAt
  S->>F: fetchByTypeAndIds(type, targetIds)
  F-->>S: Map<id, TargetResponse>
  S-->>C: PagingResponse<BookmarkResponse>
  C-->>U: 200 OK (paged list)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • stoneTiger0912

Poem

폴짝, 폴짝 새 북마크 길 열렸네 🐇
카드셋 향해 귀를 쫑긋, id를 품고 뛰었지
세트로 모은 Set, 한 알도 흘리지 않게
페처가 길잡이, 정책이 지켜보고
Created! 하고 별 하나 꽂아 둔다 ✨

✨ 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
🪧 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 or @coderabbit 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
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: 4

🧹 Nitpick comments (30)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)

29-31: sortBy 공백/트림 처리로 런타임 오류 가능성 축소

sortBy에 공백만 전달되면 isEmpty()가 통과하지 못해 잘못된 정렬 필드로 진입할 수 있습니다. 트림 처리로 방어해 주세요.

아래와 같이 최소 변경으로 보완 가능합니다.

-		if (sortBy == null || sortBy.isEmpty()) {
+		if (sortBy == null || sortBy.trim().isEmpty()) {
 			return PageRequest.of(page - 1, size);
 		} else {
 			Sort.Direction direction;
 			try {
 				direction = Sort.Direction.fromString(order);
 			} catch (IllegalArgumentException e) {
 				direction = Sort.Direction.DESC;
 			}
 
-			return PageRequest.of(page - 1, size, Sort.by(direction, sortBy));
+			final String normalizedSortBy = sortBy.trim();
+			return PageRequest.of(page - 1, size, Sort.by(direction, normalizedSortBy));
 		}

Also applies to: 39-40

src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (1)

23-25: sortBy 트림 처리(사소)

허용 필드 체크 전에 공백 제거를 권장합니다. 공백 입력으로 허용 필드 누락되는 케이스를 줄입니다.

-		String sortBy = this.getSortBy();
-		String effectiveSortBy = (sortBy != null && ALLOWED_SORT_FIELDS.contains(sortBy)) ? sortBy : "id";
+		String sortByRaw = this.getSortBy();
+		String sortBy = (sortByRaw != null) ? sortByRaw.trim() : null;
+		String effectiveSortBy = (sortBy != null && ALLOWED_SORT_FIELDS.contains(sortBy)) ? sortBy : "id";
src/main/java/project/flipnote/common/model/response/IdResponse.java (1)

1-10: 스웨거 스키마 메타데이터 추가 제안(사소)

API 문서 가독성을 위해 Schema 설명/예시를 부여하면 좋습니다.

 package project.flipnote.common.model.response;
 
+import io.swagger.v3.oas.annotations.media.Schema;
+
-public record IdResponse(
-	Long id
-) {
+@Schema(name = "IdResponse", description = "식별자만 반환하는 공통 응답 DTO")
+public record IdResponse(
+	@Schema(description = "리소스 식별자", example = "123")
+	Long id
+) {
 	public static IdResponse from(Long id) {
 		return new IdResponse(id);
 	}
 }
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)

4-4: getCardSetsByIds 시그니처를 Collection로 변경 및 null/empty 처리 추가
BookmarkCardSetFetcher, LikeCardSetFetcher 모두 Set만 전달하므로 시그니처 변경 후 호출부 수정 불필요합니다.

적용 예시(diff):

- public List<CardSetSummaryResponse> getCardSetsByIds(Set<Long> targetIds) {
-   // TODO: MSA로 전환시 전용 DTO로 변경 필요
-   return cardSetRepository.findAllById(targetIds).stream()
+ public List<CardSetSummaryResponse> getCardSetsByIds(Collection<Long> targetIds) {
+   if (targetIds == null || targetIds.isEmpty()) {
+     return List.of();
+   }
+   // TODO: MSA로 전환시 전용 DTO로 변경 필요
+   Set<Long> ids = new HashSet<>(targetIds);
+   return cardSetRepository.findAllById(ids).stream()
      .map(CardSetSummaryResponse::from)
      .toList();
 }

추가 import:

import java.util.Collection;
import java.util.HashSet;
src/main/java/project/flipnote/like/service/LikeService.java (1)

6-6: targetIds Set 사용 개선 및 null 타깃 대응 검토

  • keySet() 뷰 대신 방어적 복사 권장. 비어있을 때 조기 반환하면 불필요한 호출을 줄일 수 있습니다.
  • 삭제 등으로 일부 타깃이 누락될 때 targetMap.get(...)가 null이 될 수 있습니다. 필터링/에러 처리 정책을 정해주세요.
  • 로컬 캐스트에 대한 경고 억제를 추가하면 빌드 노이즈를 줄일 수 있습니다.

적용 예시(diff):

- Set<Long> targetIds = likedAtMap.keySet();
+ Set<Long> targetIds = Set.copyOf(likedAtMap.keySet());
+ if (targetIds.isEmpty()) {
+   return PagingResponse.from(Page.empty(req.getPageRequest()));
+ }
- LikeTargetFetcher<T> fetcher = (LikeTargetFetcher<T>)fetcherMap.get(targetType);
+ @SuppressWarnings("unchecked")
+ LikeTargetFetcher<T> fetcher = (LikeTargetFetcher<T>) fetcherMap.get(targetType);

정책 확인:

  • 타깃 삭제 시 동작(숨김/제외/에러)을 명확히 해주세요. 현 구현은 null 타깃을 그대로 응답에 실을 수 있습니다.

Also applies to: 115-116, 118-121

src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)

1-5: 기반 추상 타입 도입 LGTM

  • 북마크 타깃 공통 계약으로 충분합니다. 이후 공통 필드가 늘지 않는다면 interface로 전환하는 것도 선택지입니다(현 단계에선 유지 권장).
src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1)

12-12: 컨트랙트 명시와 빈 입력 최적화 제안

ids가 비어있을 때는 빈 Map 반환, 존재하지 않는 ID는 결과에서 누락 등의 동작을 인터페이스 주석으로 명확히 해주세요. 구현체에서는 ids.isEmpty() 시 즉시 반환하도록 가이드하면 DB 호출을 피할 수 있습니다.

원하시면 Javadoc 초안 제공하겠습니다.

src/main/java/project/flipnote/bookmark/model/BookmarkSearchRequest.java (1)

10-18: 중복 제거를 위한 공통화 제안

LikeSearchRequest와 동일 로직이 반복됩니다. PagingRequest에 “기본 정렬(id DESC)” 헬퍼(예: protected PageRequest byIdDesc())를 두고 여기서는 그 메서드를 호출하는 형태로 중복을 줄이는 것을 제안합니다.

src/main/java/project/flipnote/like/model/LikeTargetTypeRequest.java (1)

8-12: 메서드명 일관성(nitpick)

LikeTypeRequest에는 toDomain(), 여기에는 toDomainType()으로 혼재되어 있습니다. 팀 컨벤션에 맞춰 통일을 고려해 주세요.

src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (2)

28-32: 빈 Set 처리로 불필요한 호출 방지

ids가 비어있을 때 즉시 빈 Map을 반환하면 DB 호출을 피할 수 있습니다.

다음과 같이 가볍게 가드 추가를 권장합니다:

 @Override
- public Map<Long, CardSetLikeResponse> fetchByIds(Set<Long> ids) {
+ public Map<Long, CardSetLikeResponse> fetchByIds(Set<Long> ids) {
+   if (ids.isEmpty()) {
+     return Map.of();
+   }
    return cardSetService.getCardSetsByIds(ids).stream()
      .map(CardSetLikeResponse::from)
      .collect(Collectors.toMap(LikeTargetResponse::getId, Function.identity()));
 }

31-31: toMap 병합 전략 지정(nitpick)

이상 케이스(서비스가 중복 ID를 반환)에서 IllegalStateException을 피하려면 병합 전략을 명시하세요. 첫 값 우선 예시는 아래와 같습니다.

- .collect(Collectors.toMap(LikeTargetResponse::getId, Function.identity()));
+ .collect(Collectors.toMap(LikeTargetResponse::getId, Function.identity(), (a, b) -> a));
src/main/java/project/flipnote/bookmark/model/BookmarkTargetType.java (3)

3-4: API enum 네이밍 일관성 확인(Like 모듈과 불일치: card_sets vs card_set)

LikeTypeRequest는 card_set(단수)인데, 여기서는 card_sets(복수)입니다. 외부 API 계약(요청/문서/스웨거)과 상호 운용되는지 확인하고, 가능하면 단수로 통일을 권장합니다. 변경 시 호환성(컨트롤러 바인딩/문서/프론트 호출) 영향도 점검 필요.

다음과 같이 단수로 통일 가능합니다(신규 API라면 권장):

 public enum BookmarkTargetType {
-	card_sets;
+	card_set;
 }
 
 public project.flipnote.bookmark.entity.BookmarkTargetType toDomainType() {
 	return switch (this) {
-		case card_sets -> project.flipnote.bookmark.entity.BookmarkTargetType.CARD_SET;
+		case card_set -> project.flipnote.bookmark.entity.BookmarkTargetType.CARD_SET;
 	};
 }

6-9: FQN(완전수준명) 최소화로 가독성 개선

동일한 이름의 모델/도메인 enum 충돌 때문에 반환 타입에는 FQN이 필요하지만, 상수는 static import로 줄일 수 있습니다.

 package project.flipnote.bookmark.model;
 
+import static project.flipnote.bookmark.entity.BookmarkTargetType.CARD_SET;
+
 public enum BookmarkTargetType {
 	...
 	public project.flipnote.bookmark.entity.BookmarkTargetType toDomainType() {
 		return switch (this) {
-			case card_sets -> project.flipnote.bookmark.entity.BookmarkTargetType.CARD_SET;
+			case card_sets -> CARD_SET;
 		};
 	}
 }

6-10: 메서드명 일관성(toDomainType → toDomain) 제안

LikeTypeRequest는 toDomain()을 사용합니다. 명명 규칙을 맞추면 검색성과 일관성이 좋아집니다. 리네임 시 참조처 변환 필요.

-	public project.flipnote.bookmark.entity.BookmarkTargetType toDomainType() {
+	public project.flipnote.bookmark.entity.BookmarkTargetType toDomain() {
 		return switch (this) {
 			case card_sets -> CARD_SET;
 		};
 	}
src/main/java/project/flipnote/bookmark/model/CardSetBookmarkResponse.java (1)

8-17: DTO 불변성 강화(세터 제거, 필드 final) 제안

응답 DTO는 가급적 불변성이 좋습니다. @DaTa(세터 포함) 대신 @Getter + final 필드로 변경을 권장합니다. 직렬화 스키마 변화는 없습니다.

 import lombok.AllArgsConstructor;
-import lombok.Data;
+import lombok.Getter;
 import lombok.EqualsAndHashCode;
 ...
-@EqualsAndHashCode(callSuper = true)
-@AllArgsConstructor
-@Data
+@EqualsAndHashCode(callSuper = true)
+@AllArgsConstructor
+@Getter
 public class CardSetBookmarkResponse extends BookmarkTargetResponse {
-	private Long id;
-	private String name;
+	private final Long id;
+	private final String name;
 }
src/main/java/project/flipnote/bookmark/model/BookmarkResponse.java (1)

10-17: record로 단순화(선택): 보일러플레이트 제거 및 불변성 확보

이 클래스는 값 그 자체 역할이므로 record로 전환이 적합합니다. 프로젝트 내 NotificationResponse/SocialLinkResponse도 record를 사용 중입니다.

-import lombok.AllArgsConstructor;
-import lombok.Data;
-
-@AllArgsConstructor
-@Data
-public class BookmarkResponse<T extends BookmarkTargetResponse> {
-	private T target;
-
-	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-	private LocalDateTime bookmarkedAt;
-}
+public record BookmarkResponse<T extends BookmarkTargetResponse>(
+	T target,
+	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime bookmarkedAt
+) {}
src/main/java/project/flipnote/bookmark/entity/Bookmark.java (1)

21-35: 명명 전략 의존성 점검

제약/인덱스는 snake_case 컬럼명을 참조합니다. @column(name=...) 지정 없이 암시적 네이밍 전략에 의존하므로, 글로벌 설정이 snake_case로 유지되는지 확인 필요합니다. 그렇지 않다면 명시적 name 지정이 안전합니다.

-	@Enumerated(EnumType.STRING)
-	@Column(nullable = false)
-	private BookmarkTargetType targetType;
+	@Enumerated(EnumType.STRING)
+	@Column(name = "target_type", nullable = false)
+	private BookmarkTargetType targetType;
 
-	@Column(nullable = false)
-	private Long targetId;
+	@Column(name = "target_id", nullable = false)
+	private Long targetId;
 
-	@Column(nullable = false)
-	private Long userId;
+	@Column(name = "user_id", nullable = false)
+	private Long userId;
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)

12-18: 읽기 전용 트랜잭션 힌트 추가(선택)

정책 검증은 조회만 수행하므로 readOnly 트랜잭션을 명시하면 미세 최적화와 의도 표시에 도움이 됩니다(상위 서비스 트랜잭션과도 호환).

 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 ...
 @RequiredArgsConstructor
 @Service
+@Transactional(readOnly = true)
 public class BookmarkPolicyService {
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)

21-21: 제네릭 타입 선언 검토 필요

현재 BookmarkTargetFetchService<T extends BookmarkTargetResponse>로 선언되어 있지만, 사용하는 곳에서는 BookmarkTargetFetchService<BookmarkTargetResponse>로 사용됩니다. 제네릭 타입이 필요한지 검토해보세요.

-public class BookmarkTargetFetchService<T extends BookmarkTargetResponse> {
+public class BookmarkTargetFetchService {

-	private final List<BookmarkTargetFetcher<T>> fetchers;
+	private final List<BookmarkTargetFetcher<? extends BookmarkTargetResponse>> fetchers;

-	private Map<BookmarkTargetType, BookmarkTargetFetcher<T>> fetcherMap;
+	private Map<BookmarkTargetType, BookmarkTargetFetcher<? extends BookmarkTargetResponse>> fetcherMap;
src/main/java/project/flipnote/bookmark/controller/docs/BookmarkControllerDocs.java (4)

5-15: Swagger에 인증 주체 숨김 + 경로 파라미터 메타데이터 명시

AuthPrinciple가 Swagger 파라미터로 노출되지 않도록 숨기고, targetType/targetId에 간단한 설명을 부여하면 문서 가독성이 좋아집니다.

 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@
 	@Operation(summary = "즐겨찾기 추가", security = {@SecurityRequirement(name = "access-token")})
-	ResponseEntity<IdResponse> addBookmark(BookmarkTargetType targetType, Long targetId, AuthPrinciple authPrinciple);
+	ResponseEntity<IdResponse> addBookmark(
+		@Parameter(description = "즐겨찾기 대상 타입", required = true) BookmarkTargetType targetType,
+		@Parameter(description = "즐겨찾기 대상 ID", required = true) Long targetId,
+		@Parameter(hidden = true) AuthPrinciple authPrinciple
+	);

Also applies to: 19-21


22-27: 삭제 API에도 인증 주체 숨김 주석 추가

삭제 API에도 동일하게 적용해 Swagger 노이즈를 줄입니다.

 	@Operation(summary = "즐겨찾기 제거", security = {@SecurityRequirement(name = "access-token")})
 	ResponseEntity<IdResponse> deleteBookmark(
-		BookmarkTargetType targetType,
-		Long targetId,
-		AuthPrinciple authPrinciple
+		@Parameter(description = "즐겨찾기 대상 타입", required = true) BookmarkTargetType targetType,
+		@Parameter(description = "즐겨찾기 대상 ID", required = true) Long targetId,
+		@Parameter(hidden = true) AuthPrinciple authPrinciple
 	);

29-34: 목록 조회 파라미터 문서 강화 (선택)

req는 복합 객체라 @ParameterObject로 문서화하면 필드 단위로 Swagger에 노출됩니다. 적용을 원하시면 springdoc의 org.springdoc.core.annotations.ParameterObject를 import 후 아래처럼 주석을 추가하세요. 또한 authPrinciple은 hidden 처리 권장.

원하시면 제가 적용 패치도 만들어 드립니다.


16-35: 응답 일관성 검토

Like 모듈은 add/remove가 ResponseEntity<Void>를 반환하는 반면, Bookmark는 IdResponse를 반환합니다. 팀 차원의 API 일관성 정책을 확인해 통일을 검토해 주세요(특히 DELETE의 200(+body) vs 204 무응답).

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

54-58: DataIntegrityViolationException 포괄 매핑 축소

현재는 모든 DataIntegrityViolationException을 BOOKMARK_ALREADY_EXISTS로 매핑합니다. 유니크 제약 위반 외의 무결성 오류(예: 컬럼 길이, FK 등)까지 오매핑될 수 있습니다. 루트 원인이 유니크 제약 위반일 때만 변환하고, 그 외는 원인을 로깅 후 재던지기 권장.

 		try {
 			bookmarkRepository.save(bookmark);
 		} catch (DataIntegrityViolationException e) {
-			throw new BizException(BookmarkErrorCode.BOOKMARK_ALREADY_EXISTS);
+			Throwable t = e;
+			while (t != null) {
+				if (t instanceof org.hibernate.exception.ConstraintViolationException) {
+					throw new BizException(BookmarkErrorCode.BOOKMARK_ALREADY_EXISTS);
+				}
+				t = t.getCause();
+			}
+			throw e; // 다른 무결성 오류는 그대로 전파
 		}

96-112: 불필요한 Map 생성 제거 + 네이밍 정리

likedAtMap 생성 없이 바로 createdAt을 사용하면 메모리/연산이 줄고, 변수 명도 도메인에 맞게 정리됩니다.

-		Map<Long, LocalDateTime> likedAtMap = bookmarkPage.stream()
-			.collect(Collectors.toMap(Bookmark::getTargetId, Bookmark::getCreatedAt));
-		Set<Long> targetIds = likedAtMap.keySet();
+		Set<Long> targetIds = bookmarkPage.stream()
+			.map(Bookmark::getTargetId)
+			.collect(Collectors.toSet());
@@
-		Page<BookmarkResponse<BookmarkTargetResponse>> content
-			= bookmarkPage.map(bookmark ->
-			new BookmarkResponse<>(
-				targetMap.get(bookmark.getTargetId()),
-				likedAtMap.get(bookmark.getTargetId())
-			)
-		);
+		Page<BookmarkResponse<BookmarkTargetResponse>> content = bookmarkPage.map(bookmark ->
+			new BookmarkResponse<>(
+				targetMap.get(bookmark.getTargetId()),
+				bookmark.getCreatedAt()
+			)
+		);

102-110: 타겟 누락 대비

fetchByTypeAndIds가 없는 타겟을 무시하고 Map을 반환할 경우 targetMap.get(...)가 null이 될 수 있습니다(타겟 삭제 등). NPE는 아니더라도 응답 스키마에 null target이 들어가면 클라이언트 호환성 이슈가 생길 수 있습니다. 선택지:

  • 누락 시 해당 북마크를 필터링하고 PageImpl로 재구성(총합 조정)
  • 혹은 누락 시 PLACEHOLDER 응답을 내려 클라가 graceful 처리

원하시면 어느 쪽으로 갈지 결정 후 패치 드리겠습니다.


45-47: 검증 순서 미세조정 (선택)

의미상 타겟 존재 확인을 먼저 하고, 그 다음 중복 확인을 하는 편이 에러 메시지 해석이 직관적입니다.

-		bookmarkPolicyService.validateBookmarkNotExists(targetType, targetId, userId);
-		bookmarkPolicyService.validateTargetExists(targetType, targetId);
+		bookmarkPolicyService.validateTargetExists(targetType, targetId);
+		bookmarkPolicyService.validateBookmarkNotExists(targetType, targetId, userId);
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (3)

3-15: 경로 파라미터 유효성 검증 활성화 (@validated + @positive)

PathVariable에 @positive를 붙이고, 클래스에 @validated를 추가하면 음수/0 ID 요청을 조기에 차단할 수 있습니다.

 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@
-import jakarta.validation.Valid;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
@@
-@RestController
+@RestController
+@Validated
 @RequestMapping("/v1/bookmarks/{targetType}")
 public class BookmarkController implements BookmarkControllerDocs {
@@
-	public ResponseEntity<IdResponse> addBookmark(
+	public ResponseEntity<IdResponse> addBookmark(
 		@PathVariable("targetType") BookmarkTargetType targetType,
-		@PathVariable("targetId") Long targetId,
+		@PathVariable("targetId") @Positive Long targetId,
 		@AuthenticationPrincipal AuthPrinciple authPrinciple
 	) {
@@
 	public ResponseEntity<IdResponse> deleteBookmark(
 		@PathVariable("targetType") BookmarkTargetType targetType,
-		@PathVariable("targetId") Long targetId,
+		@PathVariable("targetId") @Positive Long targetId,
 		@AuthenticationPrincipal AuthPrinciple authPrinciple
 	) {

Also applies to: 26-29, 35-37, 46-48


41-42: 생성 Location 헤더 고려 (선택)

201을 쓰는 경우 Location 헤더로 리소스 식별자를 제공하면 REST 합치성이 좋아집니다. 예: /v1/bookmarks/{targetType}/{targetId}.


52-53: DELETE 응답 규약 합의

팀 규약에 따라 204 No Content(+빈 바디)로 통일할지, 현재처럼 200 + IdResponse를 유지할지 합의 필요. Like 모듈과의 일관성도 함께 점검해 주세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between daff3de and c8403dd.

📒 Files selected for processing (27)
  • src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/controller/docs/BookmarkControllerDocs.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/entity/Bookmark.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/entity/BookmarkTargetType.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/exception/BookmarkErrorCode.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkResponse.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkSearchRequest.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkTargetType.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/CardSetBookmarkResponse.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1 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/model/CardSetSearchRequest.java (1 hunks)
  • src/main/java/project/flipnote/cardset/service/CardSetService.java (2 hunks)
  • src/main/java/project/flipnote/common/model/request/PagingRequest.java (2 hunks)
  • src/main/java/project/flipnote/common/model/response/IdResponse.java (1 hunks)
  • src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1 hunks)
  • src/main/java/project/flipnote/like/entity/Like.java (1 hunks)
  • src/main/java/project/flipnote/like/model/LikeSearchRequest.java (1 hunks)
  • src/main/java/project/flipnote/like/model/LikeTargetTypeRequest.java (1 hunks)
  • src/main/java/project/flipnote/like/service/LikeService.java (2 hunks)
  • src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (3 hunks)
  • src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (27)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (3)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (2)
  • LikeTargetResponse (3-5)
  • getId (4-4)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (1)
  • EqualsAndHashCode (8-18)
src/main/java/project/flipnote/like/model/LikeResponse.java (1)
  • AllArgsConstructor (10-17)
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (2)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkTargetFetcher.java (3)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (1)
  • RequiredArgsConstructor (16-33)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
  • LikeTargetResponse (3-5)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (3)
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (2)
  • Getter (13-60)
  • Schema (45-59)
src/main/java/project/flipnote/group/model/GroupListRequest.java (1)
  • Override (16-19)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
  • Override (20-23)
src/main/java/project/flipnote/common/model/response/IdResponse.java (5)
src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (1)
  • CreateCardSetResponse (4-10)
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/auth/model/UserRegisterResponse.java (1)
  • UserRegisterResponse (3-10)
src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
  • GroupInfoResponse (3-9)
src/main/java/project/flipnote/bookmark/model/BookmarkSearchRequest.java (3)
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (1)
  • Getter (12-35)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)
  • Getter (12-42)
src/main/java/project/flipnote/like/model/LikeSearchRequest.java (1)
  • Getter (10-18)
src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (4)
src/main/java/project/flipnote/group/model/GroupListRequest.java (1)
  • Override (16-19)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
  • Override (20-23)
src/main/java/project/flipnote/group/controller/docs/GroupInvitationQueryControllerDocs.java (2)
  • Operation (24-28)
  • Tag (14-29)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
  • findAllByGroup_Id (24-24)
src/main/java/project/flipnote/like/model/LikeSearchRequest.java (3)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
  • Override (20-23)
src/main/java/project/flipnote/group/model/GroupListRequest.java (1)
  • Override (16-19)
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (1)
  • Getter (13-60)
src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java (1)
src/main/java/project/flipnote/like/repository/LikeRepository.java (1)
  • LikeRepository (12-18)
src/main/java/project/flipnote/like/model/LikeTargetTypeRequest.java (2)
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (2)
  • toDomain (8-13)
  • LikeTypeRequest (5-14)
src/main/java/project/flipnote/common/entity/LikeType.java (1)
  • LikeType (3-5)
src/main/java/project/flipnote/like/service/LikeService.java (2)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (3)
  • RequiredArgsConstructor (16-33)
  • Override (27-32)
  • Override (22-25)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (1)
  • EqualsAndHashCode (8-18)
src/main/java/project/flipnote/bookmark/entity/BookmarkTargetType.java (3)
src/main/java/project/flipnote/common/entity/LikeType.java (1)
  • LikeType (3-5)
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (1)
  • LikeTypeRequest (5-14)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (1)
  • Override (22-25)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetType.java (2)
src/main/java/project/flipnote/common/entity/LikeType.java (1)
  • LikeType (3-5)
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (2)
  • LikeTypeRequest (5-14)
  • toDomain (8-13)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (4)
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (1)
  • RequiredArgsConstructor (26-66)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (2)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
  • LikeTargetResponse (3-5)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (3)
  • RequiredArgsConstructor (16-33)
  • Override (27-32)
  • Override (22-25)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (3)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/model/CardSetBookmarkResponse.java (2)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (2)
  • EqualsAndHashCode (8-18)
  • from (15-17)
src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java (2)
  • CardSetSummaryResponse (5-24)
  • from (14-23)
src/main/java/project/flipnote/bookmark/exception/BookmarkErrorCode.java (5)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
  • ErrorCode (3-10)
src/main/java/project/flipnote/common/exception/BizException.java (1)
  • BizException (6-11)
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (2)
src/main/java/project/flipnote/group/model/GroupListRequest.java (1)
  • Override (16-19)
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (1)
  • Getter (13-60)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (5)
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (1)
  • RequiredArgsConstructor (26-66)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1)
  • RequiredArgsConstructor (15-37)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/controller/docs/BookmarkControllerDocs.java (2)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/like/controller/docs/LikeControllerDocs.java (1)
  • Tag (15-30)
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (2)
  • Override (27-32)
  • RequiredArgsConstructor (16-33)
src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (3)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (1)
  • EqualsAndHashCode (8-18)
src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java (1)
  • Slf4j (18-47)
src/main/java/project/flipnote/cardset/repository/CardSetMetadataRepository.java (1)
  • CardSetMetadataRepository (10-23)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (4)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (1)
  • RequiredArgsConstructor (16-33)
src/main/java/project/flipnote/bookmark/model/BookmarkResponse.java (3)
src/main/java/project/flipnote/like/model/LikeResponse.java (1)
  • AllArgsConstructor (10-17)
src/main/java/project/flipnote/notification/model/NotificationResponse.java (1)
  • NotificationResponse (10-35)
src/main/java/project/flipnote/user/model/SocialLinkResponse.java (1)
  • SocialLinkResponse (9-26)
src/main/java/project/flipnote/like/entity/Like.java (1)
src/main/java/project/flipnote/like/repository/LikeRepository.java (1)
  • LikeRepository (12-18)
src/main/java/project/flipnote/bookmark/entity/Bookmark.java (2)
src/main/java/project/flipnote/like/entity/Like.java (1)
  • Getter (19-59)
src/main/java/project/flipnote/common/entity/BaseEntity.java (1)
  • BaseEntity (14-26)
⏰ 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 (22)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)

28-41: size+1 제거 영향 검증 필요

  • hasNext 계산을 위해 size+1 페이징을 사용하던 서비스/컨트롤러/클라이언트 로직이 남아있지 않은지 수동으로 확인
  • 통합 테스트 및 README/API 스펙의 페이지 크기(size) 정의를 모두 최신 상태로 갱신했는지 검증하세요.
src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1)

12-13: LGTM: 페이징 size 통일

size를 그대로 사용하는 변경이 상위 PagingRequest와 일관되며 정렬 기준(id DESC)도 적절합니다.

src/main/java/project/flipnote/bookmark/entity/BookmarkTargetType.java (2)

3-5: LGTM: 도메인 enum 추가

Bookmark 대상 타입으로 CARD_SET 도입 합리적입니다. LikeType과의 명명 일관성도 좋습니다.


3-5: Enum 영속화 방식 확인 (@Enumerated(EnumType.STRING) 권장)

Bookmark 엔티티에서 이 enum을 저장한다면 ORDINAL 기본값을 피하기 위해 STRING 매핑을 보장해 주세요. 마이그레이션 및 확장 시 안전합니다.

예시:

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private BookmarkTargetType targetType;
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (1)

21-34: LGTM: size 통일 및 정렬 파싱 기본값 처리 적절

size 통일, order 파싱 실패 시 DESC 폴백 등 기본기가 잘 잡혀 있습니다.

src/main/java/project/flipnote/common/model/response/IdResponse.java (1)

3-10: LGTM: 공통 Id 응답 레코드 도입

단순 식별자 반환용으로 재사용성이 높고, 정적 팩토리도 일관적입니다.

src/main/java/project/flipnote/like/model/LikeSearchRequest.java (1)

16-16: size + 1 제거 일관화, LGTM
getPage()에 @Min(1) 및 기본값 1 설정이 적용되어 있어 getPage() - 1 사용 시 음수가 발생하지 않음이 확인되었습니다.

src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1)

12-12: Set 기반 시그니처 변경 LGTM

중복 제거·순서 비의존적 배치 조회에 더 적합합니다. 호출부/구현체와의 일관성도 좋아요.

src/main/java/project/flipnote/bookmark/model/BookmarkSearchRequest.java (1)

15-17: 기본 정렬 고정(DESC, id) 구현 LGTM

LikeSearchRequest와 동일한 패턴으로 일관성 있습니다.

src/main/java/project/flipnote/like/model/LikeTargetTypeRequest.java (1)

9-11: switch expression 전환 LGTM

열거형 추가 시 컴파일 타임에 누락을 잡아주므로 안전성이 높아졌습니다.

src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (1)

28-29: 시그니처 변경 반영 확인 CardSetService.getCardSetsByIds가 Set 시그니처로 변경되었으며, BookmarkCardSetFetcher와 LikeCardSetFetcher의 fetchByIds도 모두 Set로 일치합니다.

src/main/java/project/flipnote/bookmark/model/CardSetBookmarkResponse.java (1)

15-17: CardSetSummaryResponse → DTO 매핑 적절

ID/이름 매핑이 명확하고 Like 모듈의 CardSetLikeResponse와 일관적입니다.

src/main/java/project/flipnote/bookmark/entity/Bookmark.java (1)

39-58: 엔티티 구성 전반적으로 적절

유니크 제약과 BaseEntity 상속(createdAt 활용)이 정책/서비스와 잘 맞습니다.

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

19-29: 사전 검증 로직 타당

  • 대상 존재 검증
  • 중복 북마크 방지
    서비스의 DataIntegrityViolationException 캐치와 함께 이중 방어로 합리적입니다.
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkTargetFetcher.java (1)

9-15: 깔끔한 인터페이스 설계입니다.

제네릭 타입을 활용한 인터페이스 설계가 적절하며, 북마크 대상별로 구현체를 만들어 확장성을 고려한 구조입니다. Like 기능과 유사한 패턴으로 일관성도 좋습니다.

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

12-18: Spring Data JPA 쿼리 메서드가 적절합니다.

LikeRepository와 동일한 패턴의 쿼리 메서드들로 일관성 있는 설계입니다. 복합 조건 쿼리들이 비즈니스 요구사항을 잘 반영하고 있습니다.

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

27-31: 초기화 로직이 적절합니다.

@PostConstruct를 사용한 fetcher 맵 초기화가 깔끔하게 구현되었습니다. 타입별 fetcher 매핑이 효율적으로 처리됩니다.


48-55: Fetcher 검증 로직이 견고합니다.

존재하지 않는 타입에 대한 적절한 예외 처리가 되어 있어 런타임 안정성을 보장합니다.

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

32-36: 스트림 처리 로직이 효율적입니다.

CardSetService에서 가져온 데이터를 BookmarkResponse로 변환하는 로직이 깔끔하게 구현되었습니다. LikeCardSetFetcher와 동일한 패턴으로 일관성도 좋습니다.


17-17: CardSetService.getCardSetsByIds 시그니처 확인 완료
확인 결과 public List<CardSetSummaryResponse> getCardSetsByIds(Set<Long>) 시그니처가 변경되지 않아, BookmarkCardSetFetcher와 LikeCardSetFetcher 간 호환성에 문제가 없습니다.

src/main/java/project/flipnote/bookmark/exception/BookmarkErrorCode.java (2)

11-15: 에러 코드 정의가 체계적입니다.

북마크 기능의 주요 예외 상황들을 적절한 HTTP 상태 코드와 함께 정의했습니다. 한국어 메시지도 명확하게 작성되었습니다.


21-24: ErrorCode 인터페이스 구현이 올바릅니다.

공통 ErrorCode 인터페이스의 getStatus() 메서드를 적절히 구현하여 일관성을 유지했습니다.

Comment thread src/main/java/project/flipnote/bookmark/entity/Bookmark.java
Comment thread src/main/java/project/flipnote/like/entity/Like.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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)

27-41: page/size/order가 null로 바인딩될 때 NPE 위험 — null-safe 기본값과 공백 트림 처리 필요

Spring 바인딩에서 빈 문자열("")이나 null이 들어오면 현재 초기값(1, 10, "desc")이 덮여서 page - 1 또는 Sort.Direction.fromString(order)에서 NPE/IAE가 발생할 수 있습니다. 컨트롤러에서 @Valid를 누락한 경우 더 취약합니다.

다음처럼 null/공백 방어 로직과 트림을 추가해 주세요.

@@
-import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort;
+import org.springframework.util.StringUtils;
@@
 	@Schema(hidden = true)
 	public PageRequest getPageRequest() {
-		if (sortBy == null || sortBy.isEmpty()) {
-			return PageRequest.of(page - 1, size);
-		} else {
-			Sort.Direction direction;
-			try {
-				direction = Sort.Direction.fromString(order);
-			} catch (IllegalArgumentException e) {
-				direction = Sort.Direction.DESC;
-			}
-
-			return PageRequest.of(page - 1, size, Sort.by(direction, sortBy));
-		}
+		final int safePage = (page == null || page < 1) ? 1 : page;
+		final int safeSize = (size == null || size < 1 || size > 30) ? 10 : size;
+		final String safeSortBy = StringUtils.hasText(sortBy) ? sortBy.trim() : null;
+		final String safeOrder = StringUtils.hasText(order) ? order.trim() : "desc";
+
+		Sort.Direction direction;
+		try {
+			direction = Sort.Direction.fromString(safeOrder);
+		} catch (IllegalArgumentException e) {
+			direction = Sort.Direction.DESC;
+		}
+
+		if (!StringUtils.hasText(safeSortBy)) {
+			return PageRequest.of(safePage - 1, safeSize);
+		}
+		return PageRequest.of(safePage - 1, safeSize, Sort.by(direction, safeSortBy));
 	}
🧹 Nitpick comments (36)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)

16-25: Bean Validation 보강(@NotNull) 또는 컨트롤러 @Valid 강제 필요

현재 @Min/@max만으로는 null 입력을 막지 못합니다. 위 null-safe 보완과 병행하여 다음 둘 중 하나를 권장합니다.

  • 옵션 A: 필드에 @NotNull 추가
  • 옵션 B: 모든 컨트롤러 파라미터에서 해당 Request에 @Valid 적용 보장

원하시면 전체 컨트롤러에 대한 적용 여부 점검 스크립트를 제공할 수 있습니다.

src/main/java/project/flipnote/common/model/response/IdResponse.java (1)

7-9: API 계약 확인: 필드명이 id로 통일되는지 점검 필요

다른 응답 DTO들은 domain별 필드명(cardSetId, groupId 등)을 사용합니다. 북마크/좋아요 API 응답에서 프론트가 id라는 공통 필드를 기대하는지 확인해 주세요.

src/main/java/project/flipnote/like/service/LikeService.java (2)

115-115: keySet 뷰 대신 스냅샷 Set 사용 권장

keySet()은 Map의 뷰라 이후 변동에 영향받을 수 있습니다. 스냅샷으로 안전하게 넘기는 편이 좋습니다.

-        Set<Long> targetIds = likedAtMap.keySet();
+        Set<Long> targetIds = Set.copyOf(likedAtMap.keySet());

124-126: 타깃 누락시 NPE/Null 응답 가능성

fetchByIds가 일부 ID를 반환하지 않으면 targetMap.get(...)이 null이 되어 LikeResponse 생성 시 NPE 또는 null 페이로드가 생길 수 있습니다. 데이터 무결성(FK/ON DELETE CASCADE 또는 소프트삭제 훅)으로 예방하거나, 누락 ID를 로깅/필터링하여 PageImpl로 재구성하는 처리 검토를 권장합니다.

src/main/java/project/flipnote/like/model/LikeSearchRequest.java (1)

16-16: 페이지 사이즈 +1 제거로 페이지네이션 의미 변화

기존(size+1)로 hasNext 판단을 하던 소비자가 있었다면 동작이 바뀝니다. API 문서와 프론트 처리(다음 페이지 유무 계산)를 함께 업데이트했는지 확인 부탁드립니다.

src/main/java/project/flipnote/like/model/LikeTargetTypeRequest.java (1)

9-11: switch 표현식으로 전환 👍 (컴파일타임 누락 방지)

default를 제거해 신규 enum 상수 추가 시 컴파일 타임에 매핑 누락을 감지할 수 있어 좋습니다. 메서드명(toDomain vs toDomainType) 프로젝트 전반과의 명명 일관성만 한 번 점검해 주세요.

src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1)

12-12: 인터페이스 Set 변경에 따른 구현체·호출부 영향 점검 및 호환성 브리징 메서드 추가 권장

  • LikeCardSetFetcher(LikeTargetFetcher 구현체)·BookmarkCardSetFetcher(BookmarkTargetFetcher 구현체) 존재하므로 외부 모듈 포함 영향 범위 확인 필요
  • 입력 순서 보장이 필요하면 LinkedHashSet 변환 또는 반환 시 순서 보존형 Map(예: LinkedHashMap) 사용 계약 명시
  • 호환성을 위해 LikeTargetFetcher.java 인터페이스에 아래 default 브리징 메서드 추가 권장
 public interface LikeTargetFetcher<T extends LikeTargetResponse> {
   Map<Long, T> fetchByIds(Set<Long> ids);
+
+  default Map<Long, T> fetchByIds(java.util.List<Long> ids) {
+    return fetchByIds(new java.util.LinkedHashSet<>(ids));
+  }
 }
src/main/java/project/flipnote/bookmark/model/BookmarkResponse.java (2)

15-16: 응답 시간 포맷/타임존 명시 권장

현재 "yyyy-MM-dd HH:mm:ss"는 오프셋이 없어 클라이언트 혼동 여지 있습니다. ISO 8601 + UTC로 통일을 권장합니다(LikeResponse도 함께 정비 권장).

-  @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+  @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
   private LocalDateTime bookmarkedAt;

10-12: 직렬화 전용이면 record로 단순화 가능

불변 DTO라면 Lombok 대신 record 사용으로 보일러플레이트 제거 가능(제네릭 record 지원).

-@AllArgsConstructor
-@Data
-public class BookmarkResponse<T extends BookmarkTargetResponse> {
-  private T target;
-  @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
-  private LocalDateTime bookmarkedAt;
-}
+public record BookmarkResponse<T extends BookmarkTargetResponse>(
+  T target,
+  @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
+  LocalDateTime bookmarkedAt
+) {}
src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (2)

28-31: toMap의 기본 HashMap은 순서 비결정적 — 입력 순서 보존형 수집으로 교체 권장

상위 호출부가 순서를 기대한다면 LinkedHashMap으로 수집하세요.

-  return cardSetService.getCardSetsByIds(ids).stream()
-    .map(CardSetLikeResponse::from)
-    .collect(Collectors.toMap(LikeTargetResponse::getId, Function.identity()));
+  return cardSetService.getCardSetsByIds(ids).stream()
+    .map(CardSetLikeResponse::from)
+    .collect(Collectors.toMap(
+      LikeTargetResponse::getId,
+      Function.identity(),
+      (a, b) -> a,
+      java.util.LinkedHashMap::new
+    ));

28-28: 빈 입력 조기 반환으로 불필요한 I/O 방지

ids가 비었을 때 레포 호출을 건너뛰면 미세 최적화됩니다.

 @Override
 public Map<Long, CardSetLikeResponse> fetchByIds(Set<Long> ids) {
-  return cardSetService.getCardSetsByIds(ids).stream()
+  if (ids == null || ids.isEmpty()) return java.util.Collections.emptyMap();
+  return cardSetService.getCardSetsByIds(ids).stream()
src/main/java/project/flipnote/cardset/service/CardSetService.java (2)

215-221: readOnly 트랜잭션 깨짐 — 읽기 전용으로 명시 전환

클래스 레벨 readOnly=true인데, 메서드에 @transactional을 다시 붙이면서 쓰기 트랜잭션으로 다운그레이드됩니다. 불필요한 더티체킹을 유발할 수 있어 readOnly로 고정하세요.

-@Transactional
+@Transactional(readOnly = true)
 public List<CardSetSummaryResponse> getCardSetsByIds(Set<Long> targetIds) {

216-221: 빈 Set 처리 및 조기 반환

빈 입력 시 DB 호출 생략 권장.

 public List<CardSetSummaryResponse> getCardSetsByIds(Set<Long> targetIds) {
-  // TODO: MSA로 전환시 전용 DTO로 변경 필요
-  return cardSetRepository.findAllById(targetIds).stream()
+  // TODO: MSA로 전환시 전용 DTO로 변경 필요
+  if (targetIds == null || targetIds.isEmpty()) {
+    return java.util.Collections.emptyList();
+  }
+  return cardSetRepository.findAllById(targetIds).stream()
     .map(CardSetSummaryResponse::from)
     .toList();
 }

또한 호출측이 입력 순서를 기대한다면 이 메서드에서 정렬 보장(예: ID 오름차순)을 명시적으로 수행하는 방안을 검토하세요.

src/main/java/project/flipnote/bookmark/model/BookmarkTargetType.java (1)

3-9: API 명칭 일관성: card_sets → card_set로 통일 제안

LikeTypeRequest가 card_set(단수)인 것과 불일치합니다. 클라이언트 혼란을 줄이려면 단수로 맞추는 것을 권장합니다.

 public enum BookmarkTargetType {
-  card_sets;
+  card_set;
 
   public project.flipnote.bookmark.entity.BookmarkTargetType toDomainType() {
     return switch (this) {
-      case card_sets -> project.flipnote.bookmark.entity.BookmarkTargetType.CARD_SET;
+      case card_set -> project.flipnote.bookmark.entity.BookmarkTargetType.CARD_SET;
     };
   }
 }
src/main/java/project/flipnote/like/entity/Like.java (3)

24-28: 인덱스 중복(유니크와 동일) 제거 + 조회 패턴에 맞춘 인덱스 재구성 제안

현재 비유니크 인덱스와 유니크 제약의 컬럼 구성이 동일해 중복 인덱스가 생성됩니다. 카드(좋아요) 목록 조회 패턴(findByTargetTypeAndUserId(..., Pageable))을 고려하면 (target_type, user_id[, 정렬키]) 형태가 더 이점이 큽니다. 아래처럼 교체를 권장합니다.

-    @Index(
-      name = "idx_likes_targettype_targetid_userid",
-      columnList = "target_type, target_id, user_id"
-    )
+    @Index(
+      name = "idx_likes_targettype_userid_id",
+      columnList = "target_type, user_id, id"
+    )

대안: 목록 정렬이 created_at 기준이라면 id 대신 created_at 사용을 검토하세요(JPA @Index는 정렬 방향 지정 불가. 필요 시 마이그레이션 도구로 생성).


30-34: 유니크 제약만으로 존재 여부 조회 최적화 충분 — 비유니크 중복 인덱스는 제거 권장

유니크 제약(=유니크 인덱스)이 existsByTargetTypeAndTargetIdAndUserId 쿼리를 이미 커버합니다. 동일 컬럼 순서의 비유니크 인덱스는 중복이므로 유지비만 증가시킵니다. 위 코멘트의 인덱스 재구성과 함께 중복 제거를 고려해 주세요.


43-51: DDL 자동생성 시 네이밍 전략 의존 제거: @column(name=…) 명시 권장

Index/Unique에서 snake_case 컬럼명을 사용하므로, 물리 네이밍 전략 미설정 시 스키마 생성이 실패할 수 있습니다. 안전하게 컬럼명을 명시해 주세요.

-  @Enumerated(EnumType.STRING)
-  @Column(nullable = false)
-  private LikeTargetType targetType;
+  @Enumerated(EnumType.STRING)
+  @Column(name = "target_type", nullable = false)
+  private LikeTargetType targetType;
 
-  @Column(nullable = false)
-  private Long targetId;
+  @Column(name = "target_id", nullable = false)
+  private Long targetId;
 
-  @Column(nullable = false)
-  private Long userId;
+  @Column(name = "user_id", nullable = false)
+  private Long userId;
src/main/java/project/flipnote/bookmark/model/CardSetBookmarkResponse.java (2)

8-16: 응답 DTO는 불변으로 유지 권장(@DaTa@Getter + final)

외부 반환용 DTO는 변경 불가가 안전합니다. Lombok @DaTa가 setter를 노출하므로 불변화로 전환을 권장합니다(동일 패턴의 CardSetLikeResponse도 함께 정비 고려).

-@EqualsAndHashCode(callSuper = true)
-@AllArgsConstructor
-@Data
+@EqualsAndHashCode(callSuper = true)
+@lombok.Getter
+@lombok.AllArgsConstructor
 public class CardSetBookmarkResponse extends BookmarkTargetResponse {
-  private Long id;
-  private String name;
+  private final Long id;
+  private final String name;

8-10: callSuper=true는 의미가 크지 않음(부모 필드 없음)

BookmarkTargetResponse에 필드가 없으므로 callSuper=true는 실효가 없습니다. 유지해도 무해하나 제거해도 동일 동작입니다.

src/main/java/project/flipnote/bookmark/entity/Bookmark.java (2)

23-28: 유니크 제약과 동일 구성의 인덱스는 중복 — 목록 조회용으로 재구성 또는 제거 제안

현재 인덱스와 유니크 제약이 모두 (target_type, user_id, target_id)로 동일합니다. 유니크 제약이 유니크 인덱스를 만들므로 중복입니다. 목록 조회(findAllByTargetTypeAndUserId(..., Pageable)) 최적화를 위해 created_at(또는 id)을 포함한 인덱스로 교체를 권장합니다.

-    @Index(
-      name = "idx_bookmarks_targettype_userid_targetid",
-      columnList = "target_type, user_id, target_id"
-    )
+    @Index(
+      name = "idx_bookmarks_targettype_userid_createdat",
+      columnList = "target_type, user_id, created_at"
+    )

정렬 기준이 id라면 created_at 대신 id를 사용하세요.


43-51: @column(name=…) 명시로 네이밍 전략 의존 제거

likes와 동일한 이유로 물리 컬럼명 지정이 안전합니다.

-  @Enumerated(EnumType.STRING)
-  @Column(nullable = false)
-  private BookmarkTargetType targetType;
+  @Enumerated(EnumType.STRING)
+  @Column(name = "target_type", nullable = false)
+  private BookmarkTargetType targetType;
 
-  @Column(nullable = false)
-  private Long targetId;
+  @Column(name = "target_id", nullable = false)
+  private Long targetId;
 
-  @Column(nullable = false)
-  private Long userId;
+  @Column(name = "user_id", nullable = false)
+  private Long userId;
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkTargetFetcher.java (1)

9-15: 인터페이스 계약 명확화(JavaDoc)와 빈 입력 최적화 권장

fetchByIds 반환 맵의 포함/누락 규약(존재하지 않는 ID 처리), 빈 Set 입력 처리 등을 문서화하면 상호운용성이 좋아집니다.

 public interface BookmarkTargetFetcher<T extends BookmarkTargetResponse> {
-  BookmarkTargetType getTargetType();
+  /**
+   * 이 페처가 담당하는 즐겨찾기 대상 타입
+   */
+  BookmarkTargetType getTargetType();
 
-  boolean existsById(Long targetId);
+  /**
+   * 단일 ID 존재 여부
+   */
+  boolean existsById(Long targetId);
 
-  Map<Long, T> fetchByIds(Set<Long> ids);
+  /**
+   * 다건 조회.
+   * 규약:
+   * - 입력 ids가 비어있으면 빈 Map 반환.
+   * - 반환 Map은 '존재하는 ID'만 키로 포함(미존재 ID는 키 누락).
+   */
+  Map<Long, T> fetchByIds(Set<Long> ids);
 }
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (2)

12-14: 읽기 전용 트랜잭션 적용으로 Repository 호출 비용 최적화

정책 검증은 읽기 연산이므로 클래스 레벨 readOnly 트랜잭션을 권장합니다(서비스 addBookmark는 별도 @transactional로 오버라이드됨).

-@RequiredArgsConstructor
-@Service
+@RequiredArgsConstructor
+@org.springframework.transaction.annotation.Transactional(readOnly = true)
+@Service
 public class BookmarkPolicyService {

19-29: 경합 상황(삭제/생성 레이스)에 대한 최종 일관성 재검토

addBookmark에서 존재 검증 후 저장 사이 레이스로 대상이 삭제될 수 있습니다. 현재 설계가 이를 허용한다면 OK입니다. 아니라면 저장 직후 재검증/정책 변경 또는 조회 시 누락 타깃 필터링을 고려해 주세요. getBookmarks에서 targetMap.get(...)가 null일 수 있어 응답 target이 null이 될 위험이 있습니다.

가능한 보완(BookmarkService.getBookmarks 내):

- new BookmarkResponse<>(
-   targetMap.get(bookmark.getTargetId()),
-   likedAtMap.get(bookmark.getTargetId())
- )
+ var target = targetMap.get(bookmark.getTargetId());
+ if (target == null) return null; // map 단계에서 null 제거
+ return new BookmarkResponse<>(target, likedAtMap.get(bookmark.getTargetId()));

그리고 Page.map 이후의 null 제거가 필요하므로, stream 변환 또는 별도 조회 흐름으로의 전환을 검토해 주세요.

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

12-18: DB 유니크 제약 및 조회 인덱스 확인 필요

서비스에서 DataIntegrityViolationException을 통해 중복을 방지하려면 DB에 (user_id, target_type, target_id) 유니크 제약이 반드시 있어야 합니다. 또한 목록 조회(findAllByTargetTypeAndUserId) 성능을 위해 (target_type, user_id) 인덱스가 필요합니다. 마이그레이션/엔티티에 반영됐는지 확인 부탁드립니다.

예시 (DDL):

ALTER TABLE bookmark
  ADD CONSTRAINT uk_bookmark_user_type_target UNIQUE (user_id, target_type, target_id);

CREATE INDEX idx_bookmark_type_user ON bookmark (target_type, user_id);

엔티티 예시:

@Table(
  name = "bookmark",
  uniqueConstraints = @UniqueConstraint(name = "uk_bookmark_user_type_target",
                                        columnNames = {"user_id","target_type","target_id"}),
  indexes = @Index(name = "idx_bookmark_type_user", columnList = "target_type,user_id")
)

17-17: 메서드 네이밍 일관성(선택): findAllBy → findBy

LikeRepository와 일관성을 위해 findAllByTargetTypeAndUserIdfindByTargetTypeAndUserId로 정리하는 것을 제안드립니다. 기능 차이는 없고 가독성만 개선됩니다.

적용 예:

- Page<Bookmark> findAllByTargetTypeAndUserId(BookmarkTargetType targetType, Long userId, Pageable pageable);
+ Page<Bookmark> findByTargetTypeAndUserId(BookmarkTargetType targetType, Long userId, Pageable pageable);
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (2)

29-31: EnumMap 사용으로 미세 최적화(선택)

키가 enum이므로 EnumMap을 쓰면 메모리·성능 이점이 있습니다.

+ import java.util.EnumMap;
@@
- this.fetcherMap = this.fetchers.stream()
-   .collect(Collectors.toMap(BookmarkTargetFetcher::getTargetType, Function.identity()));
+ this.fetcherMap = this.fetchers.stream()
+   .collect(Collectors.toMap(
+     BookmarkTargetFetcher::getTargetType,
+     Function.identity(),
+     (a,b) -> a,
+     () -> new EnumMap<>(BookmarkTargetType.class)
+   ));

39-46: 빈 집합 입력 시 조기 반환으로 불필요한 호출 방지(선택)

ids가 비어있을 때 하위 서비스 호출을 건너뛰면 효율적입니다.

+ import java.util.Collections;
@@
 public Map<Long, T> fetchByTypeAndIds(
   BookmarkTargetType targetType,
   Set<Long> targetIds
 ) {
+   if (targetIds == null || targetIds.isEmpty()) {
+     return Collections.emptyMap();
+   }
   BookmarkTargetFetcher<T> targetFetcher = getFetcher(targetType);
   return targetFetcher.fetchByIds(targetIds);
 }
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1)

31-36: ids 비어있을 때 DB 호출 회피(선택)

입력 ids가 비어있다면 바로 빈 Map을 반환해 쿼리를 생략하세요.

+ import java.util.Collections;
@@
 @Override
 public Map<Long, CardSetBookmarkResponse> fetchByIds(Set<Long> ids) {
+   if (ids == null || ids.isEmpty()) {
+     return Collections.emptyMap();
+   }
   return cardSetService.getCardSetsByIds(ids).stream()
     .map(CardSetBookmarkResponse::from)
     .collect(Collectors.toMap(CardSetBookmarkResponse::getId, Function.identity()));
 }
src/main/java/project/flipnote/bookmark/exception/BookmarkErrorCode.java (1)

14-14: BOOKMARK_FETCHER_NOT_FOUND의 상태 코드 재검토(선택)

클라이언트가 지원하지 않는 targetType을 요청한 경우라면 500보다는 400(BAD_REQUEST)이 더 적절할 수 있습니다. 서버 설정 누락이라면 500 유지가 맞습니다. 의도에 맞춰 결정 부탁드립니다.

- BOOKMARK_FETCHER_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "BOOKMARK_003", "현재 즐겨찾기 할 수 없는 대상입니다."),
+ BOOKMARK_FETCHER_NOT_FOUND(HttpStatus.BAD_REQUEST, "BOOKMARK_003", "현재 즐겨찾기 할 수 없는 대상입니다."),
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (3)

33-42: 201 Created 시 Location 헤더 설정 권장

리소스 생성 시 Location을 반환하면 클라이언트 UX가 좋아집니다. 현재 북마크 식별이 (targetType, targetId, userId) 조합이라면 해당 경로를 Location으로 노출하는 방식을 고려해 주세요.

+import java.net.URI;
 ...
-    return ResponseEntity.status(HttpStatus.CREATED).body(res);
+    return ResponseEntity
+        .created(URI.create(String.format("/v1/bookmarks/%s/%d", targetType, targetId)))
+        .body(res);

35-36: PathVariable enum 매핑의 대소문자/에러 응답 일관성

BookmarkTargetType 경로값의 허용 포맷(대소문자, 별칭)과 잘못된 값 입력 시 에러 코드(400 vs 404)를 문서/컨버터로 명확히 해두는 것을 권장합니다. 필요 시 String→Enum Converter로 소문자도 수용하세요.


44-53: DELETE의 멱등성 고려(선택)

존재하지 않는 경우 404를 던지는 현재 설계도 OK입니다만, 클라이언트 단순화를 원한다면 멱등 DELETE(항상 204/200)로 전환하는 옵션도 있습니다.

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

98-111: 불필요한 Map 생성 제거 및 NPE 방지

likedAtMap은 불필요하며(값은 bookmark.getCreatedAt()으로 대체 가능), targetMap 미스 시 NPE 가능성이 있습니다. 메모리/가독성 개선과 널 안전 처리를 권장합니다.

- Map<Long, LocalDateTime> likedAtMap = bookmarkPage.stream()
-   .collect(Collectors.toMap(Bookmark::getTargetId, Bookmark::getCreatedAt));
- Set<Long> targetIds = likedAtMap.keySet();
+ Set<Long> targetIds = bookmarkPage.stream()
+   .map(Bookmark::getTargetId)
+   .collect(Collectors.toSet());
 ...
- Page<BookmarkResponse<BookmarkTargetResponse>> content
-   = bookmarkPage.map(bookmark ->
-   new BookmarkResponse<>(
-     targetMap.get(bookmark.getTargetId()),
-     likedAtMap.get(bookmark.getTargetId())
-   )
- );
+ Page<BookmarkResponse<BookmarkTargetResponse>> content = bookmarkPage.map(b -> {
+   BookmarkTargetResponse target = targetMap.get(b.getTargetId());
+   // TODO: 정책 결정 필요 - 미존재 타깃 처리(필터링/플레이스홀더/정리 작업)
+   return new BookmarkResponse<>(target, b.getCreatedAt());
+ });

추가로, BookmarkResponse가 target=null을 허용하는지 확인 부탁드립니다.


72-80: DELETE 멱등성(선택)과 레이스 대응

현재는 미존재 시 예외를 던집니다. 멱등 동작이 필요하면 존재하면 삭제, 아니면 성공 처리로 바꿀 수 있습니다. 동시 삭제 레이스에도 안정적입니다.


44-61: DB 유니크 제약 및 인덱스 확인

중복 방지는 애플리케이션 검사 + DB 유니크 제약이 함께 가야 안전합니다. 또한 목록 조회용으로 다음 인덱스를 권장합니다.

  • UNIQUE(target_type, user_id, target_id)
  • INDEX(target_type, user_id, created_at DESC) 또는 (user_id, target_type, created_at DESC)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between daff3de and 5db1b94.

📒 Files selected for processing (27)
  • src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/controller/docs/BookmarkControllerDocs.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/entity/Bookmark.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/entity/BookmarkTargetType.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/exception/BookmarkErrorCode.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkResponse.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkSearchRequest.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/BookmarkTargetType.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/model/CardSetBookmarkResponse.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1 hunks)
  • src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1 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/model/CardSetSearchRequest.java (1 hunks)
  • src/main/java/project/flipnote/cardset/service/CardSetService.java (2 hunks)
  • src/main/java/project/flipnote/common/model/request/PagingRequest.java (2 hunks)
  • src/main/java/project/flipnote/common/model/response/IdResponse.java (1 hunks)
  • src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1 hunks)
  • src/main/java/project/flipnote/like/entity/Like.java (1 hunks)
  • src/main/java/project/flipnote/like/model/LikeSearchRequest.java (1 hunks)
  • src/main/java/project/flipnote/like/model/LikeTargetTypeRequest.java (1 hunks)
  • src/main/java/project/flipnote/like/service/LikeService.java (2 hunks)
  • src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (3 hunks)
  • src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (27)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (3)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (2)
  • LikeTargetResponse (3-5)
  • getId (4-4)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (1)
  • EqualsAndHashCode (8-18)
src/main/java/project/flipnote/like/model/LikeResponse.java (1)
  • AllArgsConstructor (10-17)
src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (2)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
  • LikeTargetResponse (3-5)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (3)
  • RequiredArgsConstructor (16-33)
  • Override (27-32)
  • Override (22-25)
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (4)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/common/model/response/IdResponse.java (4)
src/main/java/project/flipnote/cardset/model/CreateCardSetResponse.java (1)
  • CreateCardSetResponse (4-10)
src/main/java/project/flipnote/group/model/GroupCreateResponse.java (1)
  • GroupCreateResponse (3-9)
src/main/java/project/flipnote/auth/model/UserRegisterResponse.java (1)
  • UserRegisterResponse (3-10)
src/main/java/project/flipnote/group/model/GroupInfoResponse.java (1)
  • GroupInfoResponse (3-9)
src/main/java/project/flipnote/bookmark/model/BookmarkSearchRequest.java (2)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)
  • Getter (12-42)
src/main/java/project/flipnote/like/model/LikeSearchRequest.java (1)
  • Getter (10-18)
src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (3)
src/main/java/project/flipnote/group/model/GroupListRequest.java (2)
  • Override (16-19)
  • Setter (10-20)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
  • Override (20-23)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (1)
  • findAllByGroup_Id (24-24)
src/main/java/project/flipnote/like/model/LikeTargetTypeRequest.java (2)
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (2)
  • toDomain (8-13)
  • LikeTypeRequest (5-14)
src/main/java/project/flipnote/common/entity/LikeType.java (1)
  • LikeType (3-5)
src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (1)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (1)
  • EqualsAndHashCode (8-18)
src/main/java/project/flipnote/like/model/LikeSearchRequest.java (3)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
  • Override (20-23)
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (1)
  • Getter (13-60)
src/main/java/project/flipnote/group/model/GroupListRequest.java (1)
  • Override (16-19)
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (2)
  • Override (27-32)
  • RequiredArgsConstructor (16-33)
src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (2)
src/main/java/project/flipnote/cardset/controller/docs/CardSetControllerDocs.java (1)
  • Tag (12-19)
src/main/java/project/flipnote/cardset/controller/CardSetController.java (1)
  • RequiredArgsConstructor (17-32)
src/main/java/project/flipnote/bookmark/entity/BookmarkTargetType.java (2)
src/main/java/project/flipnote/common/entity/LikeType.java (1)
  • LikeType (3-5)
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (1)
  • LikeTypeRequest (5-14)
src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java (1)
src/main/java/project/flipnote/like/repository/LikeRepository.java (1)
  • LikeRepository (12-18)
src/main/java/project/flipnote/like/service/LikeService.java (1)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (2)
  • RequiredArgsConstructor (16-33)
  • Override (27-32)
src/main/java/project/flipnote/bookmark/controller/docs/BookmarkControllerDocs.java (1)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/model/CardSetBookmarkResponse.java (3)
src/main/java/project/flipnote/bookmark/model/BookmarkResponse.java (1)
  • AllArgsConstructor (10-17)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (2)
  • EqualsAndHashCode (8-18)
  • from (15-17)
src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java (2)
  • CardSetSummaryResponse (5-24)
  • from (14-23)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetType.java (2)
src/main/java/project/flipnote/common/entity/LikeType.java (1)
  • LikeType (3-5)
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (2)
  • LikeTypeRequest (5-14)
  • toDomain (8-13)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (3)
src/main/java/project/flipnote/common/model/request/CursorPagingRequest.java (2)
  • Schema (45-59)
  • Getter (13-60)
src/main/java/project/flipnote/group/model/GroupListRequest.java (1)
  • Override (16-19)
src/main/java/project/flipnote/notification/model/NotificationListRequest.java (1)
  • Override (20-23)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkTargetFetcher.java (1)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (3)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/entity/Bookmark.java (2)
src/main/java/project/flipnote/like/entity/Like.java (1)
  • Getter (19-59)
src/main/java/project/flipnote/common/entity/BaseEntity.java (1)
  • BaseEntity (14-26)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (5)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (1)
  • RequiredArgsConstructor (26-66)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1)
  • RequiredArgsConstructor (15-37)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (4)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (1)
  • RequiredArgsConstructor (15-37)
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
  • BookmarkTargetResponse (3-5)
src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java (4)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/like/service/fetcher/LikeCardSetFetcher.java (1)
  • RequiredArgsConstructor (16-33)
src/main/java/project/flipnote/bookmark/exception/BookmarkErrorCode.java (4)
src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java (1)
  • RequiredArgsConstructor (12-30)
src/main/java/project/flipnote/bookmark/service/BookmarkService.java (1)
  • RequiredArgsConstructor (25-114)
src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java (1)
  • RequiredArgsConstructor (19-56)
src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
  • ErrorCode (3-10)
src/main/java/project/flipnote/bookmark/model/BookmarkResponse.java (2)
src/main/java/project/flipnote/like/model/LikeResponse.java (1)
  • AllArgsConstructor (10-17)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
  • LikeTargetResponse (3-5)
src/main/java/project/flipnote/like/entity/Like.java (1)
src/main/java/project/flipnote/like/repository/LikeRepository.java (2)
  • LikeRepository (12-18)
  • findByTypeAndTargetIdAndUserId (15-15)
🔇 Additional comments (15)
src/main/java/project/flipnote/bookmark/entity/BookmarkTargetType.java (1)

3-5: 도메인 enum 추가 LGTM

LikeType와의 네이밍/값 일관성이 유지되고, 현재 스코프(CARD_SET)에도 적절합니다.

src/main/java/project/flipnote/common/model/request/PagingRequest.java (1)

27-41: 컨트롤러 파라미터에 @Valid 적용 여부 전수 점검 필요
PagingRequest를 상속한 DTO(page/size 제약)를 사용하는 모든 컨트롤러 메서드 파라미터에 @Valid 애노테이션이 빠지지 않았는지 수동으로 확인해 주세요.

src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (1)

33-34: getSize()+1 사용 흔적 없음. hasNext 계산 로직 최종 확인 필요
과거 getSize() + 1 기반 로직이 코드베이스에서 발견되지 않으므로 이번 변경으로 인한 영향은 없을 것으로 보입니다. 서비스/응답 조립부에서 Page 기반으로 hasNext를 계산하는지 최종 점검해 주세요.

src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (1)

12-12: size+1 → size 정정 LGTM

Repository가 Page를 반환하므로 off-by-one 제거가 타당합니다. 상위 변경(PagingRequest)과도 일관적입니다.

src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)

3-5: 추상 응답 베이스 타입 추가 LGTM

LikeTargetResponse와 대칭 구조로 직관적입니다. 런타임 직렬화에도 문제 없습니다.

src/main/java/project/flipnote/common/model/response/IdResponse.java (1)

3-10: 단일 ID 응답 레코드 도입, 간결하고 일관성 좋습니다

레코드 + 정적 팩토리(from) 패턴이 프로젝트 내 다른 응답들과 톤이 맞습니다. 별도 이슈 없습니다.

src/main/java/project/flipnote/bookmark/model/BookmarkSearchRequest.java (1)

14-17: 페이지네이션/정렬 강제 로직 OK (id DESC, 0-based 페이지)

LikeSearchRequest와 동일 패턴으로 일관성 좋습니다. 컨트롤러에서 @Valid가 적용되어 @Min/@max 검증이 실제로 동작하는지 한 번만 확인해 주세요.

src/main/java/project/flipnote/like/service/LikeService.java (1)

6-6: 설계 검증 완료: Set 시그니처 및 getTargetType() 일관 적용 확인
모든 LikeTargetFetcher 인터페이스 및 구현체에 Map<Long, T> fetchByIds(Set ids)와 getTargetType()가 일관되게 적용되었으며, List 기반 호출이나 구 getLikeType() 메서드도 잔존하지 않습니다.

src/main/java/project/flipnote/like/entity/Like.java (1)

37-58: LikeRepository 메서드 시그니처가 이미 targetType 기반으로 정상 반영되어 있어 추가 수정 불필요

src/main/java/project/flipnote/bookmark/entity/Bookmark.java (1)

21-35: 마이그레이션 스크립트 확인 필요
운영 배포 시 스키마 드리프트 방지를 위해 Flyway/Liquibase 기반의 테이블/인덱스/제약 마이그레이션 스크립트(src/main/resources/db/migration 또는 db/changelog) 존재 여부를 검증하고, 없다면 추가하세요.

src/main/java/project/flipnote/bookmark/controller/docs/BookmarkControllerDocs.java (1)

19-21: IdResponse 의미를 명확히 문서화 필요

현재 서비스 구현은 IdResponse에 “북마크 엔티티 ID”를 담아 반환합니다. API 소비자가 혼동하지 않도록 summary 혹은 description에 명시해주세요.

예시:

- @Operation(summary = "즐겨찾기 추가", security = {@SecurityRequirement(name = "access-token")})
+ @Operation(
+   summary = "즐겨찾기 추가",
+   description = "응답(IdResponse.id)은 생성된 북마크 엔티티 ID입니다.",
+   security = {@SecurityRequirement(name = "access-token")}
+ )

동일하게 삭제 API에도 같은 취지를 반영하는 것을 권장합니다.

src/main/java/project/flipnote/bookmark/exception/BookmarkErrorCode.java (1)

11-25: 에러 코드 구성 좋습니다

HTTP 상태·코드·메시지 매핑이 명확하고 사용처와도 일관됩니다.

src/main/java/project/flipnote/bookmark/controller/BookmarkController.java (2)

31-31: 의존성 주입/구조 OK

불변 필드 + 생성자 주입(@requiredargsconstructor) 구성 깔끔합니다.


35-38: /v1/bookmarks/ 경로는 공개 허용(requestMatchers(...).permitAll()) 대상에 포함되지 않아 SecurityConfig의 anyRequest().authenticated()에 의해 인증이 필수입니다. @AuthenticationPrincipal이 null로 주입될 우려가 없습니다.

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

30-33: 구성/트랜잭션 어노테이션 적절

클래스 레벨 readOnly + 메서드별 쓰기 트랜잭션 분리 적절합니다.

@dungbik dungbik merged commit f55c3e8 into develop Sep 6, 2025
3 checks passed
@dungbik dungbik deleted the feat/bookmark branch September 6, 2025 07:54
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