Skip to content

[OT-119][FEAT]: 북마크 API 구현#72

Merged
marulog merged 18 commits intodevelopfrom
OT-119-feature/boomark-api
Feb 27, 2026
Merged

[OT-119][FEAT]: 북마크 API 구현#72
marulog merged 18 commits intodevelopfrom
OT-119-feature/boomark-api

Conversation

@marulog
Copy link
Copy Markdown
Collaborator

@marulog marulog commented Feb 26, 2026

📝 작업 내용

이번 PR에서 작업한 내용을 적어주세요

  • 북마크 수정 API(등록, 삭제)
  • 북마크 콘텐츠 조회 API
  • 북마크 숏폼 조회 API

📷 스크린샷

  • 북마크 수정
    기존 상태 : memberId = 1, mediaId = 1, status = ACTIVE, bookmarkCount = 151
image image

변경 후
변경 후 상태 : memberId = 1, mediaId = 1, status = DELETE , bookmarkCount = 150

image image image
  • 북마크된 콘텐츠 조회
image
  • 북마크된 숏폼 조회
image

☑️ 체크 리스트

체크 리스트를 확인해주세요

  • 테스트는 잘 통과했나요?
  • 충돌을 해결했나요?
  • 이슈는 등록했나요?
  • 라벨은 등록했나요?

#️⃣ 연관된 이슈

close #67

💬 리뷰 요구사항

쿼리쪽 자세히 봐주시면 감사하겠습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 콘텐츠 북마크 등록/취소 엔드포인트 추가(로그인 사용자 대상)
    • 북마크한 미디어/시리즈 목록 조회(페이지네이션) 추가
    • 북마크한 숏폼 콘텐츠 목록 조회(페이지네이션) 추가
    • 미디어별 북마크 수 자동 증감 반영
  • 버그 수정 / 개선

    • 오류 코드 정정 및 북마크 관련 오류 코드 추가로 응답 표준화

@marulog marulog self-assigned this Feb 26, 2026
@marulog marulog added the feat 새로운 기능 구현 label Feb 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 26, 2026

Walkthrough

북마크 기능 추가: API 인터페이스 및 컨트롤러, 요청/응답 DTO, 비즈니스 서비스, 리포지토리 조회 확장, Media 엔티티 북마크 카운트 메서드, 에러 코드(B00*/BOOKMARK_NOT_FOUND) 추가 및 오타 수정이 도입됨.

Changes

Cohort / File(s) Summary
API 계층
apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java, apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java
북마크 REST 인터페이스 및 컨트롤러 추가. 편집(토글)과 미디어/숏폼 북마크 목록 조회 엔드포인트 정의·구현, 인증된 memberId 주입 및 페이지네이션 처리.
DTO (요청/응답)
apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/request/BookmarkRequest.java, apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkMediaResponse.java, apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkShortFormResponse.java
요청 DTO(BookmarkRequest mediaId 검증) 추가. 북마크 엔티티 → 응답 DTO 변환용 정적 팩토리(from)와 응답 필드 정의 추가.
서비스 계층
apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java
북마크 토글(editBookmark) 로직, 미디어/숏폼 북마크 목록 페이징 조회 및 DTO 매핑 구현. 멤버·미디어 존재 검증 및 Media.bookmarkCount 증감 처리 포함.
도메인 리포지토리
modules/domain/src/main/java/com/ott/domain/bookmark/repository/BookmarkRepository.java
memberId+mediaId 단건 조회 및 미디어타입별 페이징 조회 메서드 추가(@EntityGraph 적용). 기존 existsBy... 유지.
도메인 엔티티
modules/domain/src/main/java/com/ott/domain/media/domain/Media.java
bookmarkCount 증감용 public 메서드 increaseBookmarkCount()decreaseBookmarkCount()(0 미만 방지) 추가.
공통 에러 코드
modules/common-web/src/main/java/com/ott/common/web/exception/ErrorCode.java
오타 정정(MEDIA_NOT_FOUND) 및 BOOKMARK_NOT_FOUND enum 항목 추가.
기타 수정
apps/api-user/src/main/java/com/ott/api_user/likes/service/LikesService.java
ShortFormRepository 의존 제거 및 에러코드 오타(MEDIA_NOT_FOUNT→MEDIA_NOT_FOUND) 수정(내부 변경).

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant BC as BookmarkController
    participant BS as BookmarkService
    participant MR as MediaRepository
    participant BR as BookmarkRepository
    participant DB as Database

    rect rgba(0, 100, 200, 0.5)
    Note over Client,DB: 북마크 편집(토글) 흐름
    Client->>BC: POST /bookmarks (memberId, mediaId)
    BC->>BS: editBookmark(memberId, mediaId)
    BS->>MR: findById(mediaId)
    MR->>DB: SELECT media WHERE id=?
    DB-->>MR: Media
    MR-->>BS: Media
    BS->>BR: findByMemberIdAndMediaId(memberId, targetMediaId)
    BR->>DB: SELECT bookmark WHERE memberId=? AND mediaId=?
    DB-->>BR: Bookmark? / null
    BR-->>BS: Optional<Bookmark>
    alt existing ACTIVE
        BS->>BR: update status=DELETE
        BS->>MR: decreaseBookmarkCount()
    else existing DELETE
        BS->>BR: update status=ACTIVE
        BS->>MR: increaseBookmarkCount()
    else not exists
        BS->>BR: insert new ACTIVE bookmark
        BS->>MR: increaseBookmarkCount()
    end
    BR->>DB: INSERT/UPDATE
    DB-->>BR: OK
    BS-->>BC: 완료
    BC-->>Client: 200 OK
    end
Loading
sequenceDiagram
    actor Client
    participant BC as BookmarkController
    participant BS as BookmarkService
    participant BR as BookmarkRepository
    participant DB as Database

    rect rgba(0, 150, 100, 0.5)
    Note over Client,DB: 미디어 북마크 목록 조회
    Client->>BC: GET /bookmarks/me/contents?page=&size=
    BC->>BS: getBookmarkMediaList(memberId, page, size)
    BS->>BR: findByMemberIdAndStatusAndMedia_MediaTypeIn(..., pageable)
    BR->>DB: SELECT ... (페이징)
    DB-->>BR: Page<Bookmark>
    BR-->>BS: Page<Bookmark>
    BS->>BS: Bookmark -> BookmarkMediaResponse 변환
    BS-->>BC: PageResponse<BookmarkMediaResponse>
    BC-->>Client: 200 OK
    end
Loading
sequenceDiagram
    actor Client
    participant BC as BookmarkController
    participant BS as BookmarkService
    participant BR as BookmarkRepository
    participant DB as Database

    rect rgba(150, 100, 0, 0.5)
    Note over Client,DB: 숏폼 북마크 목록 조회
    Client->>BC: GET /bookmarks/me/short-form?page=&size=
    BC->>BS: getBookmarkShortFormList(memberId, page, size)
    BS->>BR: findByMemberIdAndStatusAndMedia_MediaType(..., pageable)
    BR->>DB: SELECT ... (페이징)
    DB-->>BR: Page<Bookmark>
    BR-->>BS: Page<Bookmark>
    BS->>BS: Bookmark -> BookmarkShortFormResponse 변환
    BS-->>BC: PageResponse<BookmarkShortFormResponse>
    BC-->>Client: 200 OK
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • arlen02-01
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 대부분의 변경사항은 북마크 API 구현과 관련이 있으나, LikesService의 ShortFormRepository 제거는 연결된 이슈의 범위를 벗어나 있습니다. LikesService.java의 ShortFormRepository 제거는 별도의 이슈로 분리하거나 이 PR의 범위에서 제거하는 것을 권장합니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 북마크 API 구현이라는 핵심 변경사항을 명확하게 설명하고 있으며, 변경 사항의 주요 내용을 잘 반영하고 있습니다.
Linked Issues check ✅ Passed PR이 연결된 이슈 #67의 모든 주요 요구사항을 충족합니다: 북마크 등록(editBookmark), 북마크 취소(soft delete), 북마크 콘텐츠 조회(getBookmarkMediaList), 북마크 숏폼 조회(getBookmarkShortFormList) API가 모두 구현되었습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch OT-119-feature/boomark-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java`:
- Around line 67-70: The getBookmarkShortFormList method in BookmarkAPI is
missing the authentication injection on the memberId parameter; annotate the
Long memberId parameter with `@AuthenticationPrincipal` (or the project’s custom
principal annotation/type) so the authenticated user id is injected for
authorization checks, and add the required import for the annotation or adjust
the parameter type to your custom principal (e.g., CustomUserDetails) if needed
to extract the memberId.
- Line 53: Change the page-size validation from `@Min`(0) to `@Min`(1) in the
BookmarkAPI controller so requests with size=0 no longer reach
PageRequest.of(...) and trigger IllegalArgumentException; update the
`@RequestParam` annotations for the size parameter (the occurrences in the
BookmarkAPI method signatures around the controller methods) to use `@Min`(1)
while keeping the defaultValue ("10") intact.
- Around line 13-38: Remove the Swagger RequestBody import and use Spring's
org.springframework.web.bind.annotation.RequestBody for the editBookmark method
parameter (look for method editBookmark and its `@RequestBody` usage), add the
missing `@AuthenticationPrincipal` annotation to the memberId parameter of
getBookmarkShortFormList (match the pattern used in editBookmark and other
methods), and change the `@Min` constraint on any size pagination parameters from
`@Min`(0) to `@Min`(1) (update size annotations in getBookmarkShortFormList and any
other methods using size to ensure pages start at 1).

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java`:
- Around line 42-44: The BookmarkController implementation is missing parameter
validation annotations so the interface constraints don't enforce request
validation; update the controller method signatures in BookmarkController to
annotate the page parameter with `@PositiveOrZero` and the size parameter with
`@Min`(1) `@Max`(100), and add `@Validated` to the BookmarkController class so Spring
will apply those validations at runtime (apply these to both occurrences of the
handler method that accept page and size).
- Around line 31-35: Add method-level role checks to the BookmarkController by
annotating the controller methods (e.g., editBookmark and the other methods at
the same spots) with a PreAuthorize expression that restricts access to allowed
roles; for example, add `@PreAuthorize`("hasAnyRole('USER','EDITOR','ADMIN')") on
the editBookmark method (and likewise on the methods at the other reported
locations) and import org.springframework.security.access.prepost.PreAuthorize
so the controller enforces role-based access in addition to authentication.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/request/BookmarkRequest.java`:
- Around line 14-16: The mediaId field in BookmarkRequest only forbids null but
allows 0 or negative values; update the validation on the mediaId property in
the BookmarkRequest class by adding `@Positive` (or `@Min`(1)) alongside `@NotNull` so
only positive IDs are accepted, and adjust any import/validation messages
accordingly to reference mediaId.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java`:
- Around line 41-73: The editBookmark method currently toggles bookmark.status
and updates findMedia.bookmarkCount in separate read-modify steps which can race
under concurrent requests; change the flow to acquire a DB lock or perform
atomic updates: load the Bookmark (via
bookmarkRepository.findByMemberIdAndMediaId or a new repository method) using a
PESSIMISTIC_WRITE lock (or, alternatively, use a single repository update query
that flips status and adjusts media.bookmarkCount atomically), and if absent
create the Bookmark inside the same locked/transactional context after loading
Member (memberRepository.findById) and then update
findMedia.increaseBookmarkCount()/decreaseBookmarkCount() within that same
transaction/lock so the status toggle and media count change are applied
deterministically; ensure the method (editBookmark) runs in a transactional
boundary while using the locked repository methods or atomic update queries.

In
`@modules/domain/src/main/java/com/ott/domain/bookmark/repository/BookmarkRepository.java`:
- Around line 22-28: Both repository methods
(findByMemberIdAndStatusAndMedia_MediaTypeIn and
findByMemberIdAndStatusAndMedia_MediaType) return paged results without a
deterministic sort; update their signatures to enforce a consistent ordering
(e.g., newest first). Fix by either renaming the methods to include an explicit
ordering such as OrderByCreatedDateDesc or OrderByUpdatedAtDesc (e.g.,
findByMemberIdAndStatusAndMedia_MediaTypeInOrderByCreatedDateDesc) or add an
explicit Sort parameter and ensure callers provide PageRequest with
Sort.by(Sort.Direction.DESC, "createdDate" or "updatedAt"); apply the same
change to both methods and ensure the chosen timestamp field
(createdDate/updatedAt) matches the entity field name used in Bookmark/media.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c466756 and 3fd856e.

📒 Files selected for processing (12)
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/.gitkeep
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/.gitkeep
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/request/BookmarkRequest.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkMediaResponse.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkShortFormResponse.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/service/.gitkeep
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java
  • modules/common-web/src/main/java/com/ott/common/web/exception/ErrorCode.java
  • modules/domain/src/main/java/com/ott/domain/bookmark/repository/BookmarkRepository.java
  • modules/domain/src/main/java/com/ott/domain/media/domain/Media.java

Comment thread apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java Outdated
Comment thread apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java Outdated
@phonil
Copy link
Copy Markdown
Contributor

phonil commented Feb 26, 2026

@coderabbitai

이번 PR 리뷰에서 coderabbit/coderabbit-guidelines.md 읽었는지 알려줘.
그리고 안 읽었으면 읽고 리뷰해주는데, 리뷰 하나당 댓글 하나씩 달아줘.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 26, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@phonil

이번 PR 리뷰에서 coderabbit/coderabbit-guidelines.md 파일을 아직 읽지 않았습니다. 지금 바로 해당 파일을 읽고 가이드라인에 따라 리뷰를 진행하겠습니다.


🏁 Script executed:

#!/bin/bash
# Read the coderabbit guidelines file
cat coderabbit/coderabbit-guidelines.md

Length of output: 12253


@phonil

가이드라인 파일을 읽었습니다! 이제 가이드라인에 따라 PR을 상세히 리뷰하겠습니다.

먼저 주요 파일들의 실제 코드를 확인하여 정확한 리뷰를 진행하겠습니다.


🏁 Script executed:

#!/bin/bash

# BookmarkService 파일 확인
echo "=== BookmarkService.java ==="
cat apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java

echo -e "\n=== BookmarkMediaResponse.java ==="
cat apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkMediaResponse.java

echo -e "\n=== BookmarkShortFormResponse.java ==="
cat apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkShortFormResponse.java

echo -e "\n=== BookmarkRequest.java ==="
cat apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/request/BookmarkRequest.java

echo -e "\n=== BookmarkRepository.java ==="
cat modules/domain/src/main/java/com/ott/domain/bookmark/repository/BookmarkRepository.java

echo -e "\n=== BookmarkController.java ==="
cat apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java

Length of output: 11578


가이드라인을 바탕으로 코드를 검토했습니다. 발견된 이슈들을 하나씩 댓글로 남기겠습니다.


[P1] 페이지 파라미터 타입 변경 필요

근거: apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java Line 30, 40

문제: 페이지/사이즈 파라미터가 int (primitive type)으로 선언되어 있습니다.

가이드라인:

페이지/사이즈 파라미터는 Integer (boxed type) 사용.

수정 제안:

// 현재
public ResponseEntity<SuccessResponse<PageResponse<BookmarkMediaResponse>>> getBookmarkMediaList(
        `@RequestParam`(defaultValue = "0") int page,
        `@RequestParam`(defaultValue = "10") int size,
        `@AuthenticationPrincipal` Long memberId)

// 수정
public ResponseEntity<SuccessResponse<PageResponse<BookmarkMediaResponse>>> getBookmarkMediaList(
        `@RequestParam`(defaultValue = "0") Integer page,
        `@RequestParam`(defaultValue = "10") Integer size,
        `@AuthenticationPrincipal` Long memberId)

동일하게 getBookmarkShortFormList 메서드도 수정 필요합니다.


[P1] Repository에서 QueryDSL 사용 필요

근거: modules/domain/src/main/java/com/ott/domain/bookmark/repository/BookmarkRepository.java Line 19-27

문제: Spring Data JPA의 메서드 이름 기반 쿼리와 @EntityGraph를 사용하고 있습니다.

가이드라인:

JPQL 사용 금지 — 모든 커스텀 쿼리는 QueryDSL로 작성.

동적 쿼리가 필요한 도메인은 반드시 3파일 구조:

  • XxxRepository.java → extends JpaRepository<E, Long>, XxxRepositoryCustom
  • XxxRepositoryCustom.java → interface (커스텀 메서드 시그니처)
  • XxxRepositoryImpl.java → implements XxxRepositoryCustom (QueryDSL 구현)

수정 제안:

  1. BookmarkRepositoryCustom 인터페이스 생성
  2. BookmarkRepositoryImpl 구현체에서 QueryDSL로 fetchJoin 사용
  3. BookmarkRepositoryBookmarkRepositoryCustom 상속
// BookmarkRepositoryCustom.java
public interface BookmarkRepositoryCustom {
    Page<Bookmark> findBookmarkMediaList(Long memberId, Status status, 
                                          List<MediaType> mediaTypes, Pageable pageable);
    Page<Bookmark> findBookmarkShortFormList(Long memberId, Status status, 
                                              MediaType mediaType, Pageable pageable);
}

// BookmarkRepositoryImpl.java에서 QueryDSL fetchJoin 사용

[P2] Response DTO를 record로 변경 필요

근거:

  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkMediaResponse.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkShortFormResponse.java

문제: Response DTO가 class로 구현되어 있습니다.

가이드라인:

Response DTO

  • record 사용.

수정 제안:

`@Schema`(description = "북마크한 콘텐츠 목록 응답 DTO")
public record BookmarkMediaResponse(
        `@Schema`(type = "Long", description = "미디어 ID", example = "1")
        Long mediaId,
        
        `@Schema`(type = "String", description = "미디어 제목", example = "어서와요 김마루의 숲")
        String title,
        
        `@Schema`(type = "String", description = "미디어 설명", example = "허거덩의 숲에서 힐링을 즐겨봐요~")
        String description,
        
        `@Schema`(type = "String", description = "포스터 URL", example = "https://cdn.ott.com/posters/1.jpg")
        String posterUrl
) {
    public static BookmarkMediaResponse from(Bookmark bookmark) {
        return new BookmarkMediaResponse(
                bookmark.getMedia().getId(),
                bookmark.getMedia().getTitle(),
                bookmark.getMedia().getDescription(),
                bookmark.getMedia().getPosterUrl()
        );
    }
}

BookmarkShortFormResponse도 동일하게 변경 필요합니다.


[P2] @Schema에 type 속성 추가 필요

근거:

  • BookmarkMediaResponse.java Line 13-22
  • BookmarkShortFormResponse.java Line 13-22

문제: @Schema 어노테이션에 type 속성이 누락되어 있습니다.

가이드라인:

모든 필드에 @Schema(type = "...", description = "...", example = "...") 필수.
type은 문자열로 명시: "Long", "String", "List<String>" 등.

수정 제안:

`@Schema`(type = "Long", description = "미디어 ID", example = "1")
private Long mediaId;

`@Schema`(type = "String", description = "미디어 제목", example = "어서와요 김마루의 숲")
private String title;

모든 필드에 type 속성을 추가해주세요.


[P2] Request DTO를 record로 변경 필요

근거: apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/request/BookmarkRequest.java

문제: 단순한 요청 DTO가 class로 구현되어 있습니다.

가이드라인:

Request DTO

  • 단순 요청: record + 필드에 검증 어노테이션.

수정 제안:

`@Schema`(description = "북마크 등록/취소 요청 DTO")
public record BookmarkRequest(
        `@NotNull`(message = "mediaId는 필수입니다.")
        `@Schema`(type = "Long", description = "북마크할 미디어 ID", example = "1")
        Long mediaId
) {}

[P2] @RequestParam에 value 명시 필요

근거: apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java Line 30-31, 40-41

문제: @RequestParamvalue 속성이 명시되어 있지 않습니다.

가이드라인:

@RequestParamvalue 명시: @RequestParam(value = "page", defaultValue = "0").

수정 제안:

public ResponseEntity<SuccessResponse<PageResponse<BookmarkMediaResponse>>> getBookmarkMediaList(
        `@RequestParam`(value = "page", defaultValue = "0") Integer page,
        `@RequestParam`(value = "size", defaultValue = "10") Integer size,
        `@AuthenticationPrincipal` Long memberId)

[P2] @Transactional 메서드 단위 명시 권장

근거: apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java Line 26

문제: 클래스 레벨에 @Transactional이 선언되어 있고, readOnly=true인 메서드만 별도로 어노테이션을 추가했습니다.

가이드라인:

B. @Transactional 선언 위치

현재 두 가지 스타일 혼용:

  • 메서드 단위 명시 (BackOfficeSeriesService, BackOfficeMemberService) 선택

수정 제안:

`@Service`
`@RequiredArgsConstructor`
public class BookmarkService {  // 클래스 레벨 `@Transactional` 제거
    
    `@Transactional`  // 각 메서드마다 명시
    public void editBookmark(Long memberId, Long mediaId) {
        // ...
    }
    
    `@Transactional`(readOnly = true)
    public PageResponse<BookmarkMediaResponse> getBookmarkMediaList(...) {
        // ...
    }
    
    `@Transactional`(readOnly = true)
    public PageResponse<BookmarkShortFormResponse> getBookmarkShortFormList(...) {
        // ...
    }
}

이상으로 가이드라인 기반 리뷰를 완료했습니다. 특히 P1 이슈(페이지 파라미터 타입, QueryDSL 사용)는 머지 전에 보완하시는 것을 권장드립니다! 🙂

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.

♻️ Duplicate comments (3)
apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java (2)

67-71: ⚠️ Potential issue | 🔴 Critical

getBookmarkShortFormListmemberId@AuthenticationPrincipal 어노테이션이 누락되었습니다.

Line 70의 memberId 파라미터에 @AuthenticationPrincipal이 없어 인증된 사용자 ID가 주입되지 않습니다. 다른 메서드들(Line 38, 54)과 동일하게 어노테이션을 추가해야 합니다.

As per coding guidelines Strict USER/EDITOR/ADMIN authorization checks.

🔒 수정 예시
     ResponseEntity<SuccessResponse<PageResponse<BookmarkShortFormResponse>>> getBookmarkShortFormList(
             `@Parameter`(description = "페이지 번호 (0부터 시작)") `@RequestParam`(defaultValue = "0") `@Min`(0) Integer page,
             `@Parameter`(description = "페이지 크기") `@RequestParam`(defaultValue = "10") `@Min`(0) `@Max`(100) Integer size,
-            `@Parameter`(hidden = true) Long memberId
+            `@Parameter`(hidden = true) `@AuthenticationPrincipal` Long memberId
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java`
around lines 67 - 71, The parameter memberId in the controller method
getBookmarkShortFormList is missing the `@AuthenticationPrincipal` annotation so
the authenticated user ID won't be injected; update the method signature in
BookmarkAPI to annotate the Long memberId parameter with
`@AuthenticationPrincipal` (matching the other methods on the class) to ensure
correct injection and downstream authorization checks.

52-53: ⚠️ Potential issue | 🟠 Major

페이지 크기 검증이 @Min(1)이어야 합니다.

Line 53과 69의 size 파라미터에 @Min(0)이 적용되어 있어 size=0이 허용됩니다. Spring Data의 PageRequest.of(page, size)size >= 1을 요구하므로 size=0 요청 시 IllegalArgumentException이 발생합니다.

🛠️ 수정 예시
     ResponseEntity<SuccessResponse<PageResponse<BookmarkMediaResponse>>> getBookmarkMediaList(
             `@Parameter`(description = "페이지 번호 (0부터 시작)", required = true) `@RequestParam`(defaultValue = "0") `@Min`(0) Integer page,
-            `@Parameter`(description = "페이지 크기", required = true) `@RequestParam`(defaultValue = "10") `@Min`(0) `@Max`(100) Integer size,
+            `@Parameter`(description = "페이지 크기", required = true) `@RequestParam`(defaultValue = "10") `@Min`(1) `@Max`(100) Integer size,
             `@Parameter`(hidden = true) `@AuthenticationPrincipal` Long memberId
     );

Line 69에도 동일하게 적용하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java`
around lines 52 - 53, In BookmarkAPI update the request parameter validation for
the page size: the handler methods that declare the Integer parameter named
"size" currently use `@Min`(0) which allows size=0 and will cause
PageRequest.of(page, size) to throw; change those annotations to `@Min`(1)
(replace `@Min`(0) with `@Min`(1)) for the "size" parameter in the BookmarkAPI
controller methods so only positive page sizes are accepted.
apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java (1)

44-51: ⚠️ Potential issue | 🟠 Major

컨트롤러 메서드에 페이지네이션 파라미터 검증 어노테이션이 적용되지 않았습니다.

검증 어노테이션 임포트(Line 6-8)는 추가되었지만 실제 파라미터에 적용되지 않았습니다. 인터페이스의 검증 제약은 문서용이며 런타임 검증을 강제하지 않으므로, 구현 클래스에서 직접 어노테이션을 적용해야 합니다.

🛠️ 수정 예시
     public ResponseEntity<SuccessResponse<PageResponse<BookmarkMediaResponse>>> getBookmarkMediaList(
-            `@RequestParam`(defaultValue = "0") Integer page,
-            `@RequestParam`(defaultValue = "10") Integer size,
+            `@PositiveOrZero` `@RequestParam`(defaultValue = "0") Integer page,
+            `@Min`(1) `@Max`(100) `@RequestParam`(defaultValue = "10") Integer size,
             `@AuthenticationPrincipal` Long memberId) {

getBookmarkShortFormList 메서드(Line 55-58)에도 동일하게 적용하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java`
around lines 44 - 51, The pagination parameters in BookmarkController's methods
are missing runtime validation: add validation annotations to the method
parameters in the implementation (e.g., annotate getBookmarkMediaList and
getBookmarkShortFormList parameters: annotate page with `@PositiveOrZero` or
`@Min`(0) and size with `@Min`(1) (and optionally `@Max`(100) if you want an upper
bound)), and ensure the controller class is annotated with `@Validated` so those
parameter constraints are enforced at runtime; apply these annotations to the
method signatures for bookmarkService.getBookmarkMediaList(...) and the short
form equivalent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java`:
- Around line 67-71: The parameter memberId in the controller method
getBookmarkShortFormList is missing the `@AuthenticationPrincipal` annotation so
the authenticated user ID won't be injected; update the method signature in
BookmarkAPI to annotate the Long memberId parameter with
`@AuthenticationPrincipal` (matching the other methods on the class) to ensure
correct injection and downstream authorization checks.
- Around line 52-53: In BookmarkAPI update the request parameter validation for
the page size: the handler methods that declare the Integer parameter named
"size" currently use `@Min`(0) which allows size=0 and will cause
PageRequest.of(page, size) to throw; change those annotations to `@Min`(1)
(replace `@Min`(0) with `@Min`(1)) for the "size" parameter in the BookmarkAPI
controller methods so only positive page sizes are accepted.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java`:
- Around line 44-51: The pagination parameters in BookmarkController's methods
are missing runtime validation: add validation annotations to the method
parameters in the implementation (e.g., annotate getBookmarkMediaList and
getBookmarkShortFormList parameters: annotate page with `@PositiveOrZero` or
`@Min`(0) and size with `@Min`(1) (and optionally `@Max`(100) if you want an upper
bound)), and ensure the controller class is annotated with `@Validated` so those
parameter constraints are enforced at runtime; apply these annotations to the
method signatures for bookmarkService.getBookmarkMediaList(...) and the short
form equivalent.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd856e and 7e149cc.

📒 Files selected for processing (7)
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/request/BookmarkRequest.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkMediaResponse.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkShortFormResponse.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java
  • modules/domain/src/main/java/com/ott/domain/bookmark/repository/BookmarkRepository.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/dto/response/BookmarkMediaResponse.java

Copy link
Copy Markdown
Contributor

@phonil phonil left a comment

Choose a reason for hiding this comment

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

고생하셨어요~~

Comment thread modules/common-web/src/main/java/com/ott/common/web/exception/ErrorCode.java Outdated
@marulog marulog requested a review from arlen02-01 February 26, 2026 08:27
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.

♻️ Duplicate comments (3)
apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java (2)

33-58: ⚠️ Potential issue | 🟠 Major

메서드 단위 역할 검증이 빠져 있습니다.

현재는 인증 주체만 주입되고, 엔드포인트별 USER/EDITOR/ADMIN 역할 제한이 코드에 명시되지 않습니다. 메서드에 @PreAuthorize(또는 동등한 정책)로 역할 체크를 명확히 추가해 주세요.

As per coding guidelines, "Strict USER/EDITOR/ADMIN authorization checks".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java`
around lines 33 - 58, Add explicit method-level authorization to enforce
USER/EDITOR/ADMIN roles on these endpoints: annotate editBookmark,
getBookmarkMediaList, and getBookmarkShortFormList with a PreAuthorize
expression (e.g. `@PreAuthorize`("hasAnyRole('USER','EDITOR','ADMIN')") or your
project's equivalent) so the injected `@AuthenticationPrincipal` is validated
against allowed roles; ensure you import the annotation and that method security
is enabled in the application so the checks take effect.

45-57: ⚠️ Potential issue | 🟠 Major

페이지 파라미터 검증이 실제로 적용되지 않습니다.

page/size에 제약 애너테이션이 없고 클래스에 @Validated도 없어, 음수/0/과대값이 그대로 서비스로 전달될 수 있습니다.

제안 수정안
-@RestController
+@RestController
+@Validated
 `@RequiredArgsConstructor`
 `@RequestMapping`("/bookmarks")
 public class BookmarkController implements BookmarkAPI  {
@@
     public ResponseEntity<SuccessResponse<PageResponse<BookmarkMediaResponse>>> getBookmarkMediaList(
-            `@RequestParam`(defaultValue = "0") Integer page,
-            `@RequestParam`(defaultValue = "10") Integer size,
+            `@PositiveOrZero` `@RequestParam`(defaultValue = "0") Integer page,
+            `@Min`(1) `@Max`(100) `@RequestParam`(defaultValue = "10") Integer size,
             `@AuthenticationPrincipal` Long memberId) {
@@
     public ResponseEntity<SuccessResponse<PageResponse<BookmarkShortFormResponse>>> getBookmarkShortFormList(
-            `@RequestParam`(defaultValue = "0") Integer page,
-            `@RequestParam`(defaultValue = "10") Integer size,
+            `@PositiveOrZero` `@RequestParam`(defaultValue = "0") Integer page,
+            `@Min`(1) `@Max`(100) `@RequestParam`(defaultValue = "10") Integer size,
             `@AuthenticationPrincipal` Long memberId) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java`
around lines 45 - 57, The page/size parameters in BookmarkController methods
(e.g., getBookmarkShortFormList and getBookmarkMediaList) lack validation and
`@Validated` on the controller, so invalid values can reach the service; add
javax.validation annotations to the parameters (e.g., `@Min`(0) for page and
`@Min`(1) or `@Positive` for size, and optionally `@Max`(100) for size) and annotate
the controller class with `@Validated` so Spring enforces them before calling
bookmarkService.getBookmarkMediaList / getBookmarkShortFormList; adjust
parameter types if needed to support validation and update any API docs/tests
accordingly.
apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java (1)

55-80: ⚠️ Potential issue | 🟠 Major

동시 요청에서 북마크 상태/카운트 정합성이 깨질 수 있습니다.

현재 토글과 카운트 변경이 분리된 read-modify-write 흐름이라, 동일 사용자·미디어에 대한 동시 요청에서 결과가 비결정적으로 흔들릴 수 있습니다. 유니크 제약 + 비관적 락(또는 원자적 update)으로 한 트랜잭션 내에서 상태 전환과 카운트 반영을 묶어주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java`
around lines 55 - 80, The toggle logic is vulnerable to race conditions because
it reads and then writes bookmark and media counts separately; modify the flow
to perform the read-modify-write inside a single transaction with a DB lock or
atomic update: mark the service method as `@Transactional` and change the lookup
to a locked read (e.g., use a repository method annotated with
`@Lock`(PESSIMISTIC_WRITE) like findByMemberIdAndMediaId) so the block that calls
bookmark.updateStatus(...) and
targetMedia.increaseBookmarkCount()/decreaseBookmarkCount() executes under the
same transaction and lock; for the insert-path, ensure you handle unique
constraint violations (retry or load-with-lock after catching
DataIntegrityViolation) instead of blind save, and use
memberRepository.findById(...) inside the same transaction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java`:
- Around line 33-58: Add explicit method-level authorization to enforce
USER/EDITOR/ADMIN roles on these endpoints: annotate editBookmark,
getBookmarkMediaList, and getBookmarkShortFormList with a PreAuthorize
expression (e.g. `@PreAuthorize`("hasAnyRole('USER','EDITOR','ADMIN')") or your
project's equivalent) so the injected `@AuthenticationPrincipal` is validated
against allowed roles; ensure you import the annotation and that method security
is enabled in the application so the checks take effect.
- Around line 45-57: The page/size parameters in BookmarkController methods
(e.g., getBookmarkShortFormList and getBookmarkMediaList) lack validation and
`@Validated` on the controller, so invalid values can reach the service; add
javax.validation annotations to the parameters (e.g., `@Min`(0) for page and
`@Min`(1) or `@Positive` for size, and optionally `@Max`(100) for size) and annotate
the controller class with `@Validated` so Spring enforces them before calling
bookmarkService.getBookmarkMediaList / getBookmarkShortFormList; adjust
parameter types if needed to support validation and update any API docs/tests
accordingly.

In
`@apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java`:
- Around line 55-80: The toggle logic is vulnerable to race conditions because
it reads and then writes bookmark and media counts separately; modify the flow
to perform the read-modify-write inside a single transaction with a DB lock or
atomic update: mark the service method as `@Transactional` and change the lookup
to a locked read (e.g., use a repository method annotated with
`@Lock`(PESSIMISTIC_WRITE) like findByMemberIdAndMediaId) so the block that calls
bookmark.updateStatus(...) and
targetMedia.increaseBookmarkCount()/decreaseBookmarkCount() executes under the
same transaction and lock; for the insert-path, ensure you handle unique
constraint violations (retry or load-with-lock after catching
DataIntegrityViolation) instead of blind save, and use
memberRepository.findById(...) inside the same transaction.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e149cc and 2cb41df.

📒 Files selected for processing (6)
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkController.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/service/BookmarkService.java
  • apps/api-user/src/main/java/com/ott/api_user/likes/service/LikesService.java
  • modules/common-web/src/main/java/com/ott/common/web/exception/ErrorCode.java
  • modules/domain/src/main/java/com/ott/domain/media/domain/Media.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • modules/domain/src/main/java/com/ott/domain/media/domain/Media.java
  • apps/api-user/src/main/java/com/ott/api_user/bookmark/controller/BookmarkAPI.java

Copy link
Copy Markdown
Contributor

@arlen02-01 arlen02-01 left a comment

Choose a reason for hiding this comment

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

확인 완료 했습니다! 고생하셨습니다~ ^^7

@marulog marulog merged commit b6a3e7b into develop Feb 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새로운 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[OT-119] [FEAT]: 북마크 API 개발

3 participants