Feat: [FN-144][FN-160][FN-161] 즐겨찾기 기능#42
Conversation
Walkthrough북마크 기능을 신규 도입. 컨트롤러/문서 인터페이스 추가, 엔티티/리포지토리/에러코드/모델 정의, 서비스 계층(정책/코어/타깃 페처) 구현, 카드셋 전용 페처 추가. 좋아요/카드셋/공통 페이징 일부 시그니처와 페이지 크기 로직을 Set 기반 및 size 사용으로 정비. IdResponse 레코드 추가. Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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.
📒 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) 구현 LGTMLikeSearchRequest와 동일한 패턴으로 일관성 있습니다.
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() 메서드를 적절히 구현하여 일관성을 유지했습니다.
There was a problem hiding this comment.
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 보완과 병행하여 다음 둘 중 하나를 권장합니다.
원하시면 전체 컨트롤러에 대한 적용 여부 점검 스크립트를 제공할 수 있습니다.
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 → findByLikeRepository와 일관성을 위해
findAllByTargetTypeAndUserId를findByTargetTypeAndUserId로 정리하는 것을 제안드립니다. 기능 차이는 없고 가독성만 개선됩니다.적용 예:
- 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.
📒 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 추가 LGTMLikeType와의 네이밍/값 일관성이 유지되고, 현재 스코프(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 정정 LGTMRepository가 Page를 반환하므로 off-by-one 제거가 타당합니다. 상위 변경(PagingRequest)과도 일관적입니다.
src/main/java/project/flipnote/bookmark/model/BookmarkTargetResponse.java (1)
3-5: 추상 응답 베이스 타입 추가 LGTMLikeTargetResponse와 대칭 구조로 직관적입니다. 런타임 직렬화에도 문제 없습니다.
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 + 메서드별 쓰기 트랜잭션 분리 적절합니다.
📝 변경 내용
✅ 체크리스트
💬 기타 참고 사항
Summary by CodeRabbit