Skip to content

[OT-71][FEAT]: 시리즈 상세 및 에피소드 목록 조회 API 구현 #58

Merged
yubin012 merged 13 commits intodevelopfrom
OT-71-feature/user-series-detail
Feb 23, 2026
Merged

[OT-71][FEAT]: 시리즈 상세 및 에피소드 목록 조회 API 구현 #58
yubin012 merged 13 commits intodevelopfrom
OT-71-feature/user-series-detail

Conversation

@yubin012
Copy link
Copy Markdown
Contributor

@yubin012 yubin012 commented Feb 22, 2026

📝 작업 내용

1. 구현 API

  • GET /series/{seriesId}
    • 선택한 시리즈의 상세 정보를 조회합니다. (시리즈 상세 페이지에서 사용)
    • 연관 데이터(태그, 카테고리, 좋아요/북마크 여부)를 함께 매핑하여 반환.
  • GET /series/{seriesId}/contents
    • 특정 시리즈에 속한 에피소드(콘텐츠) 목록을 조회합니다. (시리즈 상세 페이지에서 사용되며, 페이징 처리 사용)

프론트엔드가 아래 이미지와 같이 시리즈 상세페이지에 접속하면 위의 두 API 가 병렬로 사용됨. (ex Promise.all )
image

참고

  1. 좋아요/북마크 활성화 여부를 위해 응답 객체에서 포함하였음.
  2. 오른쪽 상단의 에피소드 목록의 각 시청기록 흔적이 있는데 이는 시청이력 API 조회 구현 시 필드를 추가할 예정. (IN 쿼리 사용 예정)
    • 지금은 임시 값인 '0' 으로 고정한 상태로 응답객체에 포함하였음.

2. v2 ERD 구조 변경 반영

  • 시리즈와 콘텐츠의 공통 정보가 'Media' 엔티티로 분리됨에 따라 Series/Contents 조회 시 Media 데이터를 결합하여 응답하도록 로직 수정.
  • Service 계층 설계 및 데이터 흐름
      1. 메인 데이터 조회: SeriesRepository를 통해 Series와 부모인 Media 정보를 조회.
      1. 메타 데이터 병렬 조회: 추출된 mediaId를 기반으로 TagRepository와 CategoryRepository를 호출하여 리스트 형태의 부가 정보 조회.
      1. 불러온 데이터들을 Builder를 통해 하나의 DTO 로 반환

3. 인증/인가 연동

  • 기존 하드코딩된 meberId 를 제거하고, OAuth 환경에 맞춰 @AuthenticationPrincipal을 통해 실제 로그인 유저의 ID를 추출하도록 변경.


📷 테스트 스크린샷

1. DB 데이터 삽입

image image

2. Swagger 테스트 화면


image

참고

  • 현재 프로제트의 인증 방식은 보안 강호하를 위해 헤더가 아닌 HttpOnly 쿠키 방식을 채택했으므로 스웨거의 Authorize(자물쇠) 버튼을 통한 토큰 수동 주입이 따로 필요하지 않다.
  • 같은 브라우저 탭에서 카카오 로그인을 완료하면 브라우저에 'accessToken' 쿠키가 저장되어 스웨거 창으로 넘어가도 자동으로 쿠키를 포함하여 인증이 통과된다!

http://localhost:8080/series/30
image
image

http://localhost:8080/series/30/contents?page=0&size=24
image
image

🚨 주요 기술적 고민 및 트러블 슈팅

1. 트러블 슈팅 및 해결 방안
프로젝트의 application.ymlopen-in-view: false가 적용되어 있어, Service 로직(트랜잭션) 종료 후 Controller에서 Proxy 객체인 Media 데이터에 접근할 때 영속성 컨텍스트(Session) 종료로 인한 500 에러가 발생했습니다. 이를 조회 목적에 맞춰 두 가지 다른 방식으로 최적화하여 해결했습니다.

문제 코드 지점

// SeriesService.java
Series series = seriesRepository.findByIdAndStatusAndMedia_PublicStatus(seriesId, Status.ACTIVE, PublicStatus.PUBLIC)
            .orElseThrow(); // 이미 여기서 DB 가 닫힘. (Lazy Loading 때문)
Long mediaId = series.getMedia().getId();

따라서 단건 조회인 GET /series/{seriesId} 에서는
Fetch Join 을 적용하여 쿼리 1번으로 부모와 자식을 한번에 가져와 영속화시킴으로써, 세션이 닫혀도 데이터에 접근이 가능하도록 하였습니다.

다만, 다건 조회인 GET /series/{seriesId}/contents 에서는
페이징 처리를 사용하였는데 페이징 쿼리에 Fetch Join 을 적용하면 JPA 가 DB 에서 모든 데이터를 서버 메모리에 끌고 오기 때문에 OOM 을 유발할 수 있다고 판단해
@EntityGraph 를 적용하여 페이징문제와 N+1 문제를 동시에 방어하였습니다.

2. 기술적 고민
현재 seriesService 코드에서는 하나의 상세 화면을 그리기 위해 Series, Contents, Tag, Category, Bookmark, Likes 등 많은 Repository를 참조하는 상황 발생.
이렇게 되면 단일 책임의 위배가 되어 서비스 코드가 복잡해지고
도메인 간의 결합도가 높아지는 문제가 발생합니다..
그래서 Facade 패턴을 도입하여 상위 클래스를 만들어야하는지, 아니면 QueryDsl 을 도입하여(미루고 미뤘는데 결국 고도화는 QueryDSL 로 통일해야하는지...) 고민이 됩니다.

또한 시리즈 조회에서 지금은 단건 조회 + Media 와 1:1 관계라 Fetch Join 을 사용하였지만
추후 플레이리스트나 시리즈 목록 조회 구현 시에는 Fetch Join 뿐만 아니라 EntityGraph 를 사용하더라도 문제가 발생할 것 같네요
결론적으로, 현재는 직관적으로 Repository 를 직접 호출하는 방식을 채택했지만 결국에는 QueryDSL 을 도입한 DTO 직접 조회 방식으로 통일해야할듯하네요

☑️ 체크 리스트

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

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

#️⃣ 연관된 이슈

ex) #34

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
트러블 슈팅 및 기술적 고민 부분을 자세히 봐주었으면 합니다!!

Summary by CodeRabbit

  • 새로운 기능

    • 시리즈 상세 조회 및 시리즈별 콘텐츠 페이징 조회 API 추가
    • 시리즈 관련 응답 DTO(상세·콘텐츠 항목) 추가
    • 시리즈 서비스 로직 및 재생·북마크·좋아요 상태 반영 지원 추가
    • 북마크·좋아요·카테고리·태그·콘텐츠·재생 조회용 저장소 인터페이스 추가
  • 개선

    • 검색 API 페이지네이션 문서화(페이지/사이즈 기본값 명시) 개선
  • 설정

    • JWT 비밀값 환경변수 기본값 처리 개선
  • 잡무

    • 로컬 설정 파일 무시를 위한 .gitignore 항목 추가
    • 코드 포맷팅 소소한 정리

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

시리즈 조회 및 페이징 컨텐츠 API(인터페이스·컨트롤러·서비스), 관련 DTO와 여러 JPA 리포지토리 추가되었고, Search API 파라미터 스키마와 .gitignore 및 application.yml 설정이 일부 변경되었습니다.

Changes

Cohort / File(s) Summary
Configuration & Ignore
\.gitignore, apps/api-user/src/main/resources/application.yml
.gitignoreapplication-local.yml 추가; application.ymljwt.secret에 빈 기본값(${JWT_SECRET_BASE64:}) 지정.
Search API 파라미터
apps/api-user/src/main/java/com/ott/api_user/search/controller/SearchApi.java
pagesize 파라미터에 Swagger @Schema(defaultValue=...) 메타데이터를 명시하도록 어노테이션 조정.
Series API 인터페이스 & 컨트롤러
apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java, .../SeriesController.java
시리즈 상세 및 시리즈 컨텐츠 페이징용 API 인터페이스 추가 및 이를 구현하는 컨트롤러 추가(엔드포인트: GET /series/{seriesId}, GET /series/{seriesId}/contents).
Series 서비스 로직
apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java
시리즈 검증, 태그·카테고리 조회, 북마크·좋아요 검사, 컨텐츠 페이징 조회 및 DTO 매핑을 수행하는 서비스 메서드 2개 추가.
Series DTOs
apps/api-user/src/main/java/com/ott/api_user/series/dto/SeriesDetailResponse.java, .../SeriesContentsResponse.java
시리즈 상세 응답 및 시리즈 컨텐츠 항목 DTO 추가(필드 및 Swagger 메타데이터 포함, positionSec은 TODO/플레이백 자리 표시자).
저장소: Bookmark / Likes / Playback
modules/domain/.../BookmarkRepository.java, .../LikesRepository.java, .../PlaybackRepository.java
Bookmark, Likes, Playback용 JPA 리포지토리 인터페이스 추가(존재 여부 확인 메서드 등, Playback은 주석형 스텁 포함).
저장소: Tag / Category / Contents / Series
modules/domain/.../TagRepository.java, .../CategoryRepository.java, .../ContentsRepository.java, .../SeriesRepository.java
태그·카테고리 이름 조회용 JPQL 메서드 추가, Contents 페이징 조회 메서드(@EntityGraph 포함) 추가, Series join-fetch 조회 쿼리 추가.
경미한 포맷 변경
modules/common-web/src/main/java/com/ott/common/web/response/SuccessResponse.java
공백/포맷 수정(기능·API 변경 없음).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SeriesController
    participant SeriesService
    participant SeriesRepository
    participant TagRepository
    participant CategoryRepository
    participant BookmarkRepository
    participant LikesRepository

    Client->>SeriesController: GET /series/{seriesId}
    SeriesController->>SeriesService: getSeriesDetail(seriesId, memberId)
    SeriesService->>SeriesRepository: findByIdWithMedia(seriesId, status, publicStatus)
    SeriesRepository-->>SeriesService: Series
    SeriesService->>TagRepository: findTagNamesByMediaId(mediaId, status)
    TagRepository-->>SeriesService: List<String>
    SeriesService->>CategoryRepository: findCategoryNamesByMediaId(mediaId, status)
    CategoryRepository-->>SeriesService: List<String>
    SeriesService->>BookmarkRepository: existsByMemberIdAndMediaIdAndStatus(...)
    BookmarkRepository-->>SeriesService: boolean
    SeriesService->>LikesRepository: existsByMemberIdAndMediaIdAndStatus(...)
    LikesRepository-->>SeriesService: boolean
    SeriesService-->>SeriesController: SeriesDetailResponse
    SeriesController-->>Client: ResponseEntity<SuccessResponse<SeriesDetailResponse>>
Loading
sequenceDiagram
    participant Client
    participant SeriesController
    participant SeriesService
    participant SeriesRepository
    participant ContentsRepository
    participant Database

    Client->>SeriesController: GET /series/{seriesId}/contents?page=0&size=24
    SeriesController->>SeriesService: getSeriesContents(seriesId, page, size, memberId)
    SeriesService->>SeriesRepository: findByIdWithMedia(seriesId, status, publicStatus)
    SeriesRepository->>Database: Query series
    Database-->>SeriesRepository: Series entity
    SeriesRepository-->>SeriesService: Optional<Series>
    SeriesService->>ContentsRepository: findBySeriesIdAndStatusAndMedia_PublicStatusOrderByIdAsc(..., Pageable)
    ContentsRepository->>Database: Query contents with pagination
    Database-->>ContentsRepository: Page<Contents>
    ContentsRepository-->>SeriesService: Page<Contents>
    SeriesService-->>SeriesController: PageResponse<SeriesContentsResponse>
    SeriesController-->>Client: ResponseEntity<SuccessResponse<PageResponse>>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • arlen02-01
  • marulog

Poem

🐇 깡충깡충 코드 밭을 건너왔네,
시리즈 길을 닦아 컨트롤러가 반겼네.
서비스가 달려 태그·카테고리 모으고,
저장소들끼리 속삭이며 페이지를 채우네.
당근 들고 배포 축하할게요! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '[OT-71][FEAT]: 시리즈 상세 및 에피소드 목록 조회 API 구현'으로, 변경사항의 핵심인 시리즈 상세 정보 및 에피소드 목록 조회 API 구현을 명확하고 구체적으로 설명합니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch OT-71-feature/user-series-detail

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

@yubin012 yubin012 self-assigned this Feb 22, 2026
@yubin012 yubin012 added the feat 새로운 기능 구현 label Feb 22, 2026
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

🧹 Nitpick comments (6)
modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java (1)

7-29: 주석 처리된 코드 블록 제거

대량의 주석 처리된 코드가 남아있습니다. 버전 관리 시스템에서 히스토리를 추적할 수 있으므로 사용하지 않는 코드는 삭제하는 것이 좋습니다.

🧹 주석 코드 제거
 import org.springframework.data.jpa.repository.EntityGraph;
 
-// import java.util.List;
-//
-// import org.springframework.data.domain.Pageable;
-// import org.springframework.data.jpa.repository.JpaRepository;
-// import org.springframework.data.jpa.repository.Query;
-// import org.springframework.data.repository.query.Param;
-//
-// import com.ott.domain.common.Status;
-// import com.ott.domain.contents.domain.Contents;
-//
-// public interface ContentsRepository extends JpaRepository<Contents, Long> {
-//
-// // 제목에 검색어 포함, 상태 ACTIVE, 시리즈 없는 콘텐츠만 검색 (최신순 정렬)
-// `@Query`("SELECT c FROM Contents c " +
-// "WHERE LOWER(c.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " +
-// "AND c.status = :status " +
-// "AND c.series IS NULL " +
-// "ORDER BY c.createdDate DESC")
-// List<Contents> searchLatest(`@Param`("keyword") String searchWord,
-// `@Param`("status") Status status,
-// Pageable pageable);
-//
-// }
-
 import org.springframework.data.jpa.repository.JpaRepository;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java`
around lines 7 - 29, The file contains a large commented-out block including an
old ContentsRepository JpaRepository interface and a searchLatest query; remove
the entire commented code (the import block and the commented interface with
searchLatest) so only active source remains, leaving no dead commented code
behind; ensure no references to ContentsRepository or searchLatest remain
uncommented elsewhere before committing.
modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java (1)

17-22: 주석 처리된 코드 제거 권장

주석 처리된 메서드 선언(Lines 17-18)은 정리해야 합니다. JPQL JOIN FETCH 쿼리가 이미 동일한 기능을 더 효율적으로 구현하고 있으므로 불필요한 주석은 삭제하는 것이 좋습니다.

findByIdWithMedia 쿼리 자체는 LazyInitializationException 해결에 적합하며, open-in-view: false 설정과 잘 맞습니다.

🧹 주석 코드 제거
-        // Optional<Series> findByIdAndStatusAndMedia_PublicStatus(Long id, Status
-        // status, PublicStatus publicStatus);
         `@Query`("SELECT s FROM Series s JOIN FETCH s.media m WHERE s.id = :id AND s.status = :status AND m.publicStatus = :publicStatus")
         Optional<Series> findByIdWithMedia(`@Param`("id") Long id,
                         `@Param`("status") Status status,
                         `@Param`("publicStatus") PublicStatus publicStatus);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java`
around lines 17 - 22, Remove the stale commented method declaration for
Optional<Series> findByIdAndStatusAndMedia_PublicStatus from SeriesRepository;
the active `@Query` method findByIdWithMedia already implements the behavior with
JOIN FETCH, so delete the commented lines to clean up the file and keep the
repository focused on the findByIdWithMedia method.
apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java (1)

45-45: Raw type PageResponse 사용

컨트롤러와 마찬가지로, 인터페이스에서도 PageResponse<SeriesContentsResponse>로 제네릭 타입을 명시하면 API 문서화와 타입 안전성이 향상됩니다.

♻️ 제안하는 수정
-        ResponseEntity<SuccessResponse<PageResponse>> getSeriesContents(
+        ResponseEntity<SuccessResponse<PageResponse<SeriesContentsResponse>>> getSeriesContents(

추가로, SeriesContentsResponse를 import 해야 합니다:

import com.ott.api_user.series.dto.SeriesContentsResponse;
🤖 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/series/controller/SeriesApi.java`
at line 45, The controller interface method getSeriesContents currently uses a
raw PageResponse (ResponseEntity<SuccessResponse<PageResponse>>
getSeriesContents); change its return/generic type to use
PageResponse<SeriesContentsResponse> to restore type safety and improve API
docs, and add the import for com.ott.api_user.series.dto.SeriesContentsResponse;
update the method signature and any related SuccessResponse/PageResponse usages
to reference the generic PageResponse<SeriesContentsResponse>.
apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesController.java (1)

37-37: Raw type 사용으로 인한 타입 안전성 문제

PageResponse가 제네릭 타입 파라미터 없이 사용되고 있습니다. 타입 안전성을 위해 PageResponse<SeriesContentsResponse>로 명시하는 것을 권장합니다.

♻️ 제안하는 수정
     `@Override`
-    public ResponseEntity<SuccessResponse<PageResponse>> getSeriesContents(
+    public ResponseEntity<SuccessResponse<PageResponse<SeriesContentsResponse>>> getSeriesContents(
             `@PathVariable`(value = "seriesId") Long seriesId,
             `@RequestParam`(value = "page") Integer pageParam,
             `@RequestParam`(value = "size") Integer sizeParam,
             `@AuthenticationPrincipal` Long memberId) {

-        PageResponse response = seriesService.getSeriesContents(seriesId, pageParam, sizeParam, memberId);
+        PageResponse<SeriesContentsResponse> response = seriesService.getSeriesContents(seriesId, pageParam, sizeParam, memberId);

         return ResponseEntity.ok(SuccessResponse.of(response));
     }
🤖 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/series/controller/SeriesController.java`
at line 37, The method getSeriesContents currently uses the raw type
PageResponse which breaks type safety; update the method signature and any
related usages to use the generic type PageResponse<SeriesContentsResponse>
(i.e. change ResponseEntity<SuccessResponse<PageResponse>> to
ResponseEntity<SuccessResponse<PageResponse<SeriesContentsResponse>>>), update
any local variables, return statements and imports that reference PageResponse
to the generic form, and ensure SeriesContentsResponse is the correct DTO type
used for the page elements.
apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java (1)

74-74: Raw type PageResponse 사용

메서드 반환 타입에 제네릭 파라미터를 명시하면 타입 안전성이 향상됩니다.

♻️ 제안하는 수정
-    public PageResponse getSeriesContents(Long seriesId, int page, int size, Long memberId) {
+    public PageResponse<SeriesContentsResponse> getSeriesContents(Long seriesId, int page, int size, 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/series/service/SeriesService.java`
at line 74, The method SeriesService.getSeriesContents currently uses the raw
type PageResponse which loses type safety; change its signature to return a
parameterized PageResponse<T> where T is the actual DTO/entity returned by the
method (for example PageResponse<SeriesContentDto> or
PageResponse<SeriesContentResponse>), update all internal variables/return
expressions and callers to use the same generic type, and add the necessary
imports so the compiler recognizes the generic PageResponse type throughout the
SeriesService class.
apps/api-user/src/main/java/com/ott/api_user/series/dto/SeriesDetailResponse.java (1)

37-38: Swagger 예시 형식이 실제 타입과 불일치합니다.

tags 필드의 example 값이 "드라마, 범죄, 수사"로 단일 문자열처럼 보이지만, 실제 타입은 List<String>입니다. Swagger UI에서 올바른 형태로 표시되도록 수정을 권장합니다.

📝 제안하는 수정
-    `@Schema`(description = "태그 목록", example = "드라마, 범죄, 수사")
+    `@Schema`(description = "태그 목록", example = "[\"드라마\", \"범죄\", \"수사\"]")
     private List<String> tags;
🤖 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/series/dto/SeriesDetailResponse.java`
around lines 37 - 38, The Swagger example for the List<String> field tags in
SeriesDetailResponse is wrong (it shows a single comma-separated string); update
the annotation on the tags field so the example reflects a JSON array (e.g. set
example = "[\"드라마\",\"범죄\",\"수사\"]") or replace `@Schema` with an
`@ArraySchema/`@Schema pair so each element example is a string — ensure you
modify the annotation on the tags field in class SeriesDetailResponse
accordingly.
🤖 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/series/controller/SeriesApi.java`:
- Line 20: Remove the unused import jakarta.websocket.server.PathParam from the
SeriesApi file; locate the import statement in the SeriesApi class and delete
it, and if you actually intended to annotate controller method parameters,
replace it with the correct annotation such as
org.springframework.web.bind.annotation.PathVariable (or the appropriate
framework PathParam) on the controller method signatures.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesController.java`:
- Line 4: Remove the unused import org.springframework.security.access.method.P
from the top of SeriesController.java; locate the import statement (import
org.springframework.security.access.method.P;) and delete it so there are no
unused imports reported by the compiler or linter.
- Line 17: Remove the unused WebSocket import jakarta.websocket.server.PathParam
from SeriesController; the controller already uses Spring's `@PathVariable` so
delete the import line referencing PathParam to clean up unused imports and
avoid confusion with WebSocket annotations.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java`:
- Line 4: Remove the unused and incorrect import of java.util.Locale.Category
from SeriesService; locate the import statement referencing Locale.Category in
SeriesService.java and delete it (ensure no other code depends on
Locale.Category), then run a build or organize imports to confirm there are no
remaining unused imports.

In `@apps/api-user/src/main/resources/application.yml`:
- Around line 20-21: Remove the empty-default placeholders for the OAuth
credentials so the app fails fast when env vars are missing: in application.yml
replace "client-id: ${KAKAO_CLIENT_ID:}" and "client-secret:
${KAKAO_CLIENT_SECRET:}" with placeholders that have no empty default (e.g.,
"client-id: ${KAKAO_CLIENT_ID}" and "client-secret: ${KAKAO_CLIENT_SECRET}") so
Spring will surface a missing-property error at startup for KAKAO_CLIENT_ID and
KAKAO_CLIENT_SECRET (keys: client-id, client-secret).

In
`@modules/domain/src/main/java/com/ott/domain/category/repository/CategoryRepository.java`:
- Around line 13-23: The query in CategoryRepository.findCategoryNamesByMediaId
hardcodes the 'ACTIVE' status strings; change it to use a parameter or a shared
status constant/enum instead (e.g., pass :status or reference Status.ACTIVE) to
match TagRepository's refactor approach—update the `@Query` to replace each
'ACTIVE' literal with a parameter (e.g., :status) and add a method parameter
`@Param`("status") String status (or adjust to use the shared Status enum type) so
all status checks are consistent and not hardcoded.

In
`@modules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java`:
- Line 11: The repository method findByMemberIdAndContentsIdAndStatus currently
returns Optional<Playback> but the Playback entity has no uniqueness guarantee
for the (member_id, contents_id, status) tuple, so either enforce uniqueness at
the entity/table level or change the repository contract: Option A - add
`@Table`(uniqueConstraints = `@UniqueConstraint`(columnNames = {"member_id",
"contents_id", "status"})) to the Playback entity class to ensure a single row
per combination; Option B - change the repository method signature to
List<Playback> findByMemberIdAndContentsIdAndStatus(...) or rename to
findFirstByMemberIdAndContentsIdAndStatus or
findTopByMemberIdAndContentsIdAndStatus to explicitly accept multiple results;
pick one option and update the Playback entity or the PlaybackRepository method
accordingly.

In
`@modules/domain/src/main/java/com/ott/domain/tag/repository/TagRepository.java`:
- Around line 14-22: The JPQL in findTagNamesByMediaId hardcodes 'ACTIVE';
change it to accept status parameters instead: update the query in TagRepository
(method findTagNamesByMediaId) to replace t.status = 'ACTIVE' and mt.status =
'ACTIVE' with parameters (e.g., :tagStatus and :mtStatus or a single :status)
and add corresponding `@Param` parameters to the method signature so callers pass
the Status enum value (or its name) rather than relying on a literal string.

---

Nitpick comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java`:
- Line 45: The controller interface method getSeriesContents currently uses a
raw PageResponse (ResponseEntity<SuccessResponse<PageResponse>>
getSeriesContents); change its return/generic type to use
PageResponse<SeriesContentsResponse> to restore type safety and improve API
docs, and add the import for com.ott.api_user.series.dto.SeriesContentsResponse;
update the method signature and any related SuccessResponse/PageResponse usages
to reference the generic PageResponse<SeriesContentsResponse>.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesController.java`:
- Line 37: The method getSeriesContents currently uses the raw type PageResponse
which breaks type safety; update the method signature and any related usages to
use the generic type PageResponse<SeriesContentsResponse> (i.e. change
ResponseEntity<SuccessResponse<PageResponse>> to
ResponseEntity<SuccessResponse<PageResponse<SeriesContentsResponse>>>), update
any local variables, return statements and imports that reference PageResponse
to the generic form, and ensure SeriesContentsResponse is the correct DTO type
used for the page elements.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/dto/SeriesDetailResponse.java`:
- Around line 37-38: The Swagger example for the List<String> field tags in
SeriesDetailResponse is wrong (it shows a single comma-separated string); update
the annotation on the tags field so the example reflects a JSON array (e.g. set
example = "[\"드라마\",\"범죄\",\"수사\"]") or replace `@Schema` with an
`@ArraySchema/`@Schema pair so each element example is a string — ensure you
modify the annotation on the tags field in class SeriesDetailResponse
accordingly.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java`:
- Line 74: The method SeriesService.getSeriesContents currently uses the raw
type PageResponse which loses type safety; change its signature to return a
parameterized PageResponse<T> where T is the actual DTO/entity returned by the
method (for example PageResponse<SeriesContentDto> or
PageResponse<SeriesContentResponse>), update all internal variables/return
expressions and callers to use the same generic type, and add the necessary
imports so the compiler recognizes the generic PageResponse type throughout the
SeriesService class.

In
`@modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java`:
- Around line 7-29: The file contains a large commented-out block including an
old ContentsRepository JpaRepository interface and a searchLatest query; remove
the entire commented code (the import block and the commented interface with
searchLatest) so only active source remains, leaving no dead commented code
behind; ensure no references to ContentsRepository or searchLatest remain
uncommented elsewhere before committing.

In
`@modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java`:
- Around line 17-22: Remove the stale commented method declaration for
Optional<Series> findByIdAndStatusAndMedia_PublicStatus from SeriesRepository;
the active `@Query` method findByIdWithMedia already implements the behavior with
JOIN FETCH, so delete the commented lines to clean up the file and keep the
repository focused on the findByIdWithMedia method.

Comment thread apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java Outdated
Comment thread apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java Outdated
Comment thread apps/api-user/src/main/resources/application.yml Outdated
Comment thread modules/domain/src/main/java/com/ott/domain/tag/repository/TagRepository.java Outdated
@yubin012 yubin012 changed the title [OT-71][FEAT]: [OT-71][FEAT]: 시리즈 상세 및 에피소드 목록 조회 API 구현 (V2 ERD 적용 및 쿼리 최적화) Feb 22, 2026
@yubin012 yubin012 changed the title [OT-71][FEAT]: 시리즈 상세 및 에피소드 목록 조회 API 구현 (V2 ERD 적용 및 쿼리 최적화) [OT-71][FEAT]: 시리즈 상세 및 에피소드 목록 조회 API 구현 Feb 22, 2026
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: 2

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

Inline comments:
In
`@modules/domain/src/main/java/com/ott/domain/category/repository/CategoryRepository.java`:
- Around line 13-22: JPQL 문자열에 남아있는 '+' 문자가 파싱 오류를 유발하므로 CategoryRepository의
`@Query` 어노테이션 안 쿼리(SELECT DISTINCT c.name FROM MediaTag mt JOIN mt.tag t JOIN
t.category c WHERE mt.media.id = :mediaId AND mt.status = :status AND t.status =
:status AND c.status = :status)를 찾아 쿼리 문자열 내 “+” 잔여물을 제거하여 순수한 JPQL로 교체하세요;
`@Query` 어노테이션과 해당 쿼리를 정의한 메서드(예: CategoryRepository의 관련 조회 메서드)를 수정하면 됩니다.

In
`@modules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java`:
- Around line 10-12: The query method findFirstByMemberIdAndContentsIdAndStatus
in PlaybackRepository does not guarantee "latest" because it lacks an explicit
ordering; update the repository method signature to include an ordering suffix
(e.g., OrderByCreatedAtDesc or OrderByUpdatedAtDesc or OrderByIdDesc according
to your actual "latest" column) or add a Sort parameter so the database returns
the newest record deterministically; modify the method
findFirstByMemberIdAndContentsIdAndStatus to include the chosen OrderBy...Desc
or change it to findFirstByMemberIdAndContentsIdAndStatus(Long memberId, Long
contentsId, Status status, Sort sort) and adjust callers accordingly.

Comment on lines +10 to +12
public interface PlaybackRepository extends JpaRepository<Playback, Long> {
// 가장 최신으로 하나만 가져오기
Optional<Playback> findFirstByMemberIdAndContentsIdAndStatus(Long memberId, Long contentsId, Status status);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Feb 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

“최신” 보장이 안 됩니다 — 정렬 기준을 메서드 시그니처에 포함하세요.

findFirstBy...는 정렬이 없으면 DB 기본 순서에 의존합니다. “가장 최신”을 의도한다면 OrderBy...Desc(예: createdAt/updatedAt/id) 또는 Sort 파라미터로 정렬을 강제해야 합니다.
아래는 예시이며, 실제 최신 기준 컬럼에 맞춰 조정해 주세요.

✅ 예시 수정안
-    // 가장 최신으로 하나만 가져오기
-    Optional<Playback> findFirstByMemberIdAndContentsIdAndStatus(Long memberId, Long contentsId, Status status);
+    // 가장 최신으로 하나만 가져오기 (최신 기준 컬럼에 맞춰 수정)
+    Optional<Playback> findFirstByMemberIdAndContentsIdAndStatusOrderByCreatedAtDesc(
+        Long memberId, Long contentsId, Status status
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java`
around lines 10 - 12, The query method findFirstByMemberIdAndContentsIdAndStatus
in PlaybackRepository does not guarantee "latest" because it lacks an explicit
ordering; update the repository method signature to include an ordering suffix
(e.g., OrderByCreatedAtDesc or OrderByUpdatedAtDesc or OrderByIdDesc according
to your actual "latest" column) or add a Sort parameter so the database returns
the newest record deterministically; modify the method
findFirstByMemberIdAndContentsIdAndStatus to include the chosen OrderBy...Desc
or change it to findFirstByMemberIdAndContentsIdAndStatus(Long memberId, Long
contentsId, Status status, Sort sort) and adjust callers accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

이 부분은 아직 사용하지 않는 리포지토리라서 Playback API 구현 방향에 따라 수정하도록 하겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

@coderabbit-user
I don’t see any images attached to this thread or the PR description section relevant to this comment, so there’s nothing to describe.

알겠습니다. 현재 PlaybackRepository는 미사용 상태이므로, Playback API 설계가 결정된 후에 “최신” 기준(예: updatedAt/createdAt/최신 재생 이벤트 시각 등)에 맞춰 시그니처를 정리하시는 방향에 동의합니다.

제안 드리는 옵션:

  • 확정형: 기준 컬럼이 정해지면 OrderBy...Desc를 메서드명에 포함
    • 예) Optional findFirstByMemberIdAndContentsIdAndStatusOrderByUpdatedAtDesc(...)
  • 유연형: 지금 당장 결정 미루고 Sort 인자를 받아 호출부에서 결정
    • 예) Optional findFirstByMemberIdAndContentsIdAndStatus(Long memberId, Long contentsId, Status status, Sort sort)

원하시면, 후속 작업을 잊지 않도록 “Playback 최신 조회 정렬 기준 확정 및 Repository 시그니처 반영” 이슈를 생성해 드릴게요. 생성 여부 알려주세요. 또한, 당장은 코드 변경 없이 메서드 위에 TODO 주석으로 “정렬 기준 확정 시 OrderBy…Desc 또는 Sort 파라미터로 변경” 정도만 남겨두는 것도 좋겠습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate Playback entity file =="
fd -t f -a "Playback.java"

echo
echo "== Show Playback entity header and fields (first 200 lines) =="
PLAYBACK_FILE="$(fd -t f -a "Playback.java" | head -n 1 || true)"
if [[ -n "${PLAYBACK_FILE:-}" ]]; then
  nl -ba "$PLAYBACK_FILE" | sed -n '1,200p'
else
  echo "Playback.java not found"
fi

echo
echo "== Check for BaseTimeEntity usage across domain =="
rg -nP --type=java -C2 'class\s+\w+\s+extends\s+BaseTimeEntity' modules/domain/src/main/java || true

echo
echo "== Search for createdAt/updatedAt fields in Playback =="
if [[ -n "${PLAYBACK_FILE:-}" ]]; then
  rg -nP -C2 '\b(createdAt|updatedAt)\b' "$PLAYBACK_FILE" || true
fi

echo
echo "== Search for any usages of the repository method (should be none) =="
rg -nP -C2 'findFirstByMemberIdAndContentsIdAndStatus\s*\(' || true

echo
echo "== See if repository methods elsewhere use Sort param (style check) =="
rg -nP --type=java -C2 '\bSort\s+\w+\)' modules || true

Length of output: 232


@coderabbit-user
I don’t see any images attached to this thread or the PR description section relevant to this comment.

재실행 스크립트를 올렸습니다. 이번에는 nl 대신 awk로 번호를 붙여 Playback/Repository/BaseTimeEntity 내용을 확인하고, 코드베이스에서 OrderBy…Desc/Sort 패턴 사용 사례도 스캔합니다. 결과가 나오면 그 기준(예: createdAt/updatedAt/playedAt 등)에 맞춰 정렬 방식 권고안을 구체화하겠습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate Playback entity file =="
PLAYBACK_FILE="$(fd -t f -a 'Playback.java' | head -n 1 || true)"
echo "${PLAYBACK_FILE:-Playback.java not found}"

echo
echo "== Show Playback entity (first 200 lines, numbered) =="
if [[ -n "${PLAYBACK_FILE:-}" && -f "$PLAYBACK_FILE" ]]; then
  awk 'NR<=200 {printf "%6d  %s\n", NR, $0}' "$PLAYBACK_FILE"
else
  echo "Playback.java not found"
fi

echo
echo "== Search for timestamp-like fields in Playback =="
if [[ -n "${PLAYBACK_FILE:-}" && -f "$PLAYBACK_FILE" ]]; then
  rg -nP -C2 '\b(createdAt|updatedAt|playedAt|watchedAt|position|lastPosition|endedAt|modifiedAt)\b' "$PLAYBACK_FILE" || true
fi

echo
echo "== Find BaseTimeEntity and inspect timestamp fields =="
BASE_TIME_FILE="$(fd -t f -a 'BaseTimeEntity.java' | head -n 1 || true)"
echo "BaseTimeEntity: ${BASE_TIME_FILE:-not found}"
if [[ -n "${BASE_TIME_FILE:-}" && -f "$BASE_TIME_FILE" ]]; then
  rg -nP -C2 '@CreatedDate|@LastModifiedDate|createdAt|updatedAt' "$BASE_TIME_FILE" || true
  echo
  echo "-- BaseTimeEntity (first 200 lines) --"
  awk 'NR<=200 {printf "%6d  %s\n", NR, $0}' "$BASE_TIME_FILE"
fi

echo
echo "== Entities extending BaseTimeEntity (style scan) =="
rg -nP --type=java -C2 'class\s+\w+\s+extends\s+BaseTimeEntity' modules/domain/src/main/java || true

echo
echo "== Locate PlaybackRepository file and preview =="
REPO_FILE="$(fd -t f -a 'PlaybackRepository.java' | head -n 1 || true)"
echo "${REPO_FILE:-PlaybackRepository.java not found}"
if [[ -n "${REPO_FILE:-}" && -f "$REPO_FILE" ]]; then
  awk 'NR<=200 {printf "%6d  %s\n", NR, $0}' "$REPO_FILE"
fi

echo
echo "== Search for any usages of the current repository method =="
rg -nP -C2 'findFirstByMemberIdAndContentsIdAndStatus\s*\(' || true

echo
echo "== Repository style: methods using explicit OrderBy...Desc =="
rg -nP --type=java -C1 'find(First|Top)\w*OrderBy\w+(Asc|Desc)\s*\(' modules/domain/src/main/java || true

echo
echo "== Repository style: methods accepting Sort parameter =="
rg -nP --type=java -C2 '\b(Sort|Pageable)\b' modules/domain/src/main/java | rg -nP -C0 'Repository|interface|find|findAll|search|query' || true

Length of output: 15163


@coderabbit-user
I don’t see images attached to this thread.

확인 결과:

  • Playback 엔티티는 BaseEntity를 상속하고, 자체 필드는 id/member/contents/positionSec만 있습니다.
  • PlaybackRepository의 메서드는 현재 주석 처리되어 있고(미사용), 코드베이스 내 사용처도 없습니다.

사용자 의견대로 Playback API 설계 이후에 정렬 기준을 확정하는 것으로 두겠습니다. 다만 놓치지 않도록 아래 중 하나로 정리만 해두면 좋겠습니다.

권장 정리안

  • 코드에 TODO 주석 추가
    • TODO: “Playback 최신 조회 기준 확정 후, OrderByCreatedAtDesc/OrderByUpdatedAtDesc 또는 Sort 파라미터 방식으로 메서드 공개”
  • 기준 컬럼이 BaseEntity에 존재한다면(예: createdAt/updatedAt):
    • Optional findFirstByMemberIdAndContentsIdAndStatusOrderByUpdatedAtDesc(Long memberId, Long contentsId, Status status);
    • 또는 createdAt 기준으로 동일 패턴
  • 기준을 호출부에서 결정하려면:
    • Optional findFirstByMemberIdAndContentsIdAndStatus(Long memberId, Long contentsId, Status status, Sort sort);

인덱스 제안(후속 작업 시)

  • “최신” 단건 조회 성능을 위해 아래 중 하나를 고려해 주세요.
    • @Index(name = "idx_playback_member_contents_status_updated_at", columnList = "member_id, contents_id, status, updated_at DESC")
    • 또는 created_at 기준일 경우 updated_at → created_at 대체

원하시면 “Playback 최신 조회 정렬 기준 확정 및 Repository 시그니처 반영” 이슈를 생성해 드릴게요. 생성 여부 알려주세요. 감사합니다!


✏️ Learnings added
Learnt from: yubin012
Repo: OpenTheTaste/backend PR: 58
File: modules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java:0-0
Timestamp: 2026-02-23T02:47:27.140Z
Learning: modules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java 는 현재 미사용이며, Playback API 구현 방향에 따라 '최신' 정렬 기준(예: updatedAt/createdAt 등)을 확정한 뒤 메서드 시그니처(예: OrderBy...Desc 또는 Sort 파라미터)로 반영하기로 함.

Learnt from: yubin012
Repo: OpenTheTaste/backend PR: 58
File: modules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java:0-0
Timestamp: 2026-02-23T02:47:27.140Z
Learning: PlaybackRepository는 현재 미사용이며, 메서드는 주석 처리됨. Playback 엔티티(com.ott.domain.playback.domain.Playback)는 BaseEntity를 상속하고 자체 타임스탬프 필드는 없으므로, ‘최신’ 기준은 BaseEntity(createdAt/updatedAt) 존재 여부에 따라 OrderBy…Desc 또는 Sort 파라미터 방식으로 결정하기로 함.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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.

고생하셨습니다!
Status.ACTIVE 이건 저는 우선 뺐는데 MVP2에서 정해봅시다~

그리고 지금 당장 수정 안 해도 되지만 나중에 꼭 반영해야 하는 것은 노션 **'이슈 관리 페이지'**에 작성해주세요!

@Parameter(description = "시리즈 ID", required = true) @PathVariable("seriesId") Long seriesId,
@Parameter(description = "페이지 번호 (0부터 시작)", schema = @Schema(defaultValue = "0")) @RequestParam("page") Integer page,
@Parameter(description = "페이지 크기", schema = @Schema(defaultValue = "24")) @RequestParam("size") Integer size,
@Parameter(hidden = true) Long memberId // 토큰에서 추출 (스웨거에서는 숨김)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@AuthenticationPrincipal
여기도 붙이면 좋을 것 같아요~

.build();
}).collect(Collectors.toList());

PageInfo pageInfo = PageInfo.builder()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PageInfo 클래스에 toPageInfo() 함수를 만들어놨어요!
안에서 builder 사용하니 조금 더 간결해요. 아래처럼 쓰는 건 어떤가요?

PageInfo pageInfo = PageInfo.toPageInfo(
                contentsPage.getNumber(),
                contentsPage.getTotalPages(),
                contentsPage.getSize()
        );

}

// 시리즈 콘텐츠 목록 조회 (페이징)
public PageResponse getSeriesContents(Long seriesId, int page, int size, Long memberId) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PageResponse 클래스가 타입으로 선언되어 있습니다. PageResponse

메소드 반환 타입을 아래처럼 바꾸면 반환 타입을 한 눈에 볼 수 있어서 좋을 것 같아요

PageResponse<SeriesContentsResponse>

Status.ACTIVE);
Boolean isLiked = likesRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE);

return SeriesDetailResponse.builder()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

제안

저는 Mapper 클래스를 만들어서 서비스에서 DTO 변환 로직을 Mapper로 위임하는데 이건 어떤가요?
저는 서비스에 코드가 넘 많아서 보기 어려울까봐 추가했어요


@Operation(summary = "시리즈 콘텐츠 목록 조회", description = "특정 시리즈에 속한 콘텐츠(에피소드) 목록을 페이징하여 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "시리즈 콘텐츠 목록 조회 성공", content = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

저는 아래와 같이 구성했어요! PageResponse만 넣으면 그 안에 List가 어떤 타입으로 이뤄져 있는지 몰라서 0번으로 dataList가 어떻게 구성되어 있는지 알려줍니다.

@Operation(summary = "숏폼 목록 조회", description = "숏폼 목록을 페이징으로 조회합니다.")
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "0", description = "조회 성공 - 페이징 dataList 구성",
                    content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ShortFormListResponse.class)))}
            ),
            @ApiResponse(
                    responseCode = "200", description = "숏폼 목록 조회 성공",
                    content = {@Content(mediaType = "application/json", schema = @Schema(implementation = PageResponse.class))}
            ),
            @ApiResponse(
                    responseCode = "400", description = "숏폼 목록 조회 실패",
                    content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}
            )
    })

@Builder
@AllArgsConstructor
@Schema(description = "시리즈 상세 조회 응답 DTO")
public class SeriesDetailResponse {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

DTO에 record 타입 쓰심은 어떠신지..? ㅋㅋ..
이건 노션 '이슈 관리 페이지'에 적어놓고 나중에 해도 괜찮아요

Comment on lines +39 to +41
PageResponse response = seriesService.getSeriesContents(seriesId, pageParam, sizeParam, memberId);

return ResponseEntity.ok(SuccessResponse.of(response));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이렇게 해도 좋을 것 같아요!

return ResponseEntity.ok(
        SuccessResponse.of(seriesService.getSeriesContents(seriesId, pageParam, sizeParam, memberId))
);

// private final PlaybackRepository playbackRepository;

// 시리즈 상세 조회
public SeriesDetailResponse getSeriesDetail(Long seriesId, Long memberId) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Transaction(readOnly = true) 

붙여도 괜찮아보여요

- SeriesSerivce 내부 DTO 빌더 로직을 정적 팩토리 메서드로 분리
- SeriesService 에 @transactional(readOnly = true) 적용하여 성능 최적화
- PageResponse 의 반환 타입을 제너릭으로 명시
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: 3

🧹 Nitpick comments (4)
apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java (2)

35-35: 인터페이스에 @AuthenticationPrincipal 어노테이션이 누락되었습니다.

memberId 파라미터에 @AuthenticationPrincipal 어노테이션이 인터페이스에 선언되어 있지 않습니다. 컨트롤러 구현체에서 추가하고 있지만, 인터페이스에도 명시하는 것이 일관성 있습니다.

♻️ 제안하는 수정
-                       `@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/series/controller/SeriesApi.java`
at line 35, In the SeriesApi interface, add the `@AuthenticationPrincipal`
annotation to the memberId parameter (currently annotated only with
`@Parameter`(hidden = true)) so the interface signature matches the controller
implementation; update the method signature in SeriesApi to include
`@AuthenticationPrincipal` Long memberId and ensure the AuthenticationPrincipal
import is added alongside the existing `@Parameter` import to keep Swagger hiding
intact.

48-49: pagesize 파라미터에 기본값이 설정되지 않았습니다.

@Schema(defaultValue = "0")은 Swagger 문서용이며, 실제 요청 시 파라미터가 누락되면 예외가 발생합니다. @RequestParamdefaultValue를 설정하거나 required = false로 변경하는 것이 좋습니다.

♻️ 제안하는 수정
-                       `@Parameter`(description = "페이지 번호 (0부터 시작)", schema = `@Schema`(defaultValue = "0")) `@RequestParam`("page") Integer page,
-                       `@Parameter`(description = "페이지 크기", schema = `@Schema`(defaultValue = "24")) `@RequestParam`("size") Integer size,
+                       `@Parameter`(description = "페이지 번호 (0부터 시작)", schema = `@Schema`(defaultValue = "0")) `@RequestParam`(value = "page", defaultValue = "0") Integer page,
+                       `@Parameter`(description = "페이지 크기", schema = `@Schema`(defaultValue = "24")) `@RequestParam`(value = "size", defaultValue = "24") Integer size,
🤖 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/series/controller/SeriesApi.java`
around lines 48 - 49, 페이지와 사이즈 쿼리 파라미터에 Swagger용 `@Schema만` 지정되어 실제 요청에서 누락되면 예외가
발생하므로, `@RequestParam`("page") Integer page와 `@RequestParam`("size") Integer size에
각각 defaultValue="0" 및 defaultValue="24"를 추가하거나 required = false로 변경하고 메서드(예: 해당
컨트롤러 메서드) 내부에서 null 체크 후 기본값을 할당하도록 수정하세요; 대안으로 파라미터 타입을 Integer에서 int로 바꿔 기본값을
`@RequestParam의` defaultValue로 주면 더 안전합니다.
apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java (2)

67-68: 시리즈 존재 여부 확인을 위해 전체 엔티티를 조회하고 있습니다.

존재 여부만 확인하려면 existsById 또는 count 쿼리가 더 효율적입니다. 현재 구현은 Media까지 fetch join하여 불필요한 데이터를 로드합니다.

♻️ 제안하는 수정
-               seriesRepository.findByIdWithMedia(seriesId, Status.ACTIVE, PublicStatus.PUBLIC)
-                               .orElseThrow(() -> new BusinessException(ErrorCode.SERIES_NOT_FOUND));
+               if (!seriesRepository.existsByIdAndStatusAndMedia_PublicStatus(seriesId, Status.ACTIVE, PublicStatus.PUBLIC)) {
+                       throw new BusinessException(ErrorCode.SERIES_NOT_FOUND);
+               }

SeriesRepository에 다음 메서드 추가가 필요합니다:

boolean existsByIdAndStatusAndMedia_PublicStatus(Long id, Status status, PublicStatus publicStatus);
🤖 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/series/service/SeriesService.java`
around lines 67 - 68, The current call to
seriesRepository.findByIdWithMedia(...) in SeriesService loads the full entity
(including Media) just to check existence; replace that lookup with a
lightweight existence check by adding a repository method boolean
existsByIdAndStatusAndMedia_PublicStatus(Long id, Status status, PublicStatus
publicStatus) to SeriesRepository and using it in SeriesService to throw new
BusinessException(ErrorCode.SERIES_NOT_FOUND) when it returns false; update the
SeriesService line that uses findByIdWithMedia(...) to call the new existsBy...
method and keep the same exception logic.

26-27: 사용되지 않는 import가 있습니다.

PlaybackRepositoryPlayback이 import되어 있지만 현재 사용되지 않습니다(주석 처리됨). 사용 시점에 추가하는 것이 좋습니다.

🧹 제안하는 수정
-import com.ott.domain.playback.domain.Playback;
-import com.ott.domain.playback.repository.PlaybackRepository;
🤖 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/series/service/SeriesService.java`
around lines 26 - 27, Remove the unused imports to clean up SeriesService.java:
delete the imports for Playback and PlaybackRepository (symbols: Playback,
PlaybackRepository) since they are not referenced in the current code, or
alternatively reintroduce their usage only when you actually need them (e.g., in
methods referring to playback data) so the imports match real references; ensure
there are no lingering commented-out code that relies on those imports before
removing.
🤖 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/series/controller/SeriesController.java`:
- Line 16: SeriesController에 불필요하게 남아 있는 import
io.micrometer.core.ipc.http.HttpSender.Response를 제거하세요; 해당 심볼은 코드에서 사용되지 않으므로
SeriesController 클래스의 import 목록에서 이 라인을 삭제하고 빌드/컴파일 오류가 없는지 확인하세요.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/dto/SeriesDetailResponse.java`:
- Around line 49-62: In SeriesDetailResponse.of, the code calls
categories.isEmpty() which will NPE if categories is null; update the null-safe
check for the category assignment (the builder().category(...) call) to first
test whether categories is null or empty (e.g., categories == null ||
categories.isEmpty()) and return null when so, otherwise use categories.get(0);
adjust SeriesDetailResponse.of to use this null-safe expression when setting
category.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java`:
- Around line 55-57: Handle null memberId before calling repository methods: if
memberId is null, set isBookmarked and isLiked to false instead of invoking
bookmarkRepository.existsByMemberIdAndMediaIdAndStatus(...) and
likesRepository.existsByMemberIdAndMediaIdAndStatus(...). Update the logic in
SeriesService (use the memberId variable and the isBookmarked/isLiked locals) to
short-circuit and return false for unauthenticated users to avoid passing null
into the repository calls.

---

Nitpick comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java`:
- Line 35: In the SeriesApi interface, add the `@AuthenticationPrincipal`
annotation to the memberId parameter (currently annotated only with
`@Parameter`(hidden = true)) so the interface signature matches the controller
implementation; update the method signature in SeriesApi to include
`@AuthenticationPrincipal` Long memberId and ensure the AuthenticationPrincipal
import is added alongside the existing `@Parameter` import to keep Swagger hiding
intact.
- Around line 48-49: 페이지와 사이즈 쿼리 파라미터에 Swagger용 `@Schema만` 지정되어 실제 요청에서 누락되면 예외가
발생하므로, `@RequestParam`("page") Integer page와 `@RequestParam`("size") Integer size에
각각 defaultValue="0" 및 defaultValue="24"를 추가하거나 required = false로 변경하고 메서드(예: 해당
컨트롤러 메서드) 내부에서 null 체크 후 기본값을 할당하도록 수정하세요; 대안으로 파라미터 타입을 Integer에서 int로 바꿔 기본값을
`@RequestParam의` defaultValue로 주면 더 안전합니다.

In
`@apps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.java`:
- Around line 67-68: The current call to seriesRepository.findByIdWithMedia(...)
in SeriesService loads the full entity (including Media) just to check
existence; replace that lookup with a lightweight existence check by adding a
repository method boolean existsByIdAndStatusAndMedia_PublicStatus(Long id,
Status status, PublicStatus publicStatus) to SeriesRepository and using it in
SeriesService to throw new BusinessException(ErrorCode.SERIES_NOT_FOUND) when it
returns false; update the SeriesService line that uses findByIdWithMedia(...) to
call the new existsBy... method and keep the same exception logic.
- Around line 26-27: Remove the unused imports to clean up SeriesService.java:
delete the imports for Playback and PlaybackRepository (symbols: Playback,
PlaybackRepository) since they are not referenced in the current code, or
alternatively reintroduce their usage only when you actually need them (e.g., in
methods referring to playback data) so the imports match real references; ensure
there are no lingering commented-out code that relies on those imports before
removing.

import com.ott.common.web.response.PageResponse;
import com.ott.common.web.response.SuccessResponse;

import io.micrometer.core.ipc.http.HttpSender.Response;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

사용되지 않는 import가 있습니다.

io.micrometer.core.ipc.http.HttpSender.Response는 코드에서 사용되지 않습니다. 제거해주세요.

🧹 제안하는 수정
-import io.micrometer.core.ipc.http.HttpSender.Response;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import io.micrometer.core.ipc.http.HttpSender.Response;
🤖 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/series/controller/SeriesController.java`
at line 16, SeriesController에 불필요하게 남아 있는 import
io.micrometer.core.ipc.http.HttpSender.Response를 제거하세요; 해당 심볼은 코드에서 사용되지 않으므로
SeriesController 클래스의 import 목록에서 이 라인을 삭제하고 빌드/컴파일 오류가 없는지 확인하세요.

Comment on lines +49 to +62
public static SeriesDetailResponse of(Series series, List<String> tags, List<String> categories,
Boolean isBookmarked, Boolean isLiked) {
return SeriesDetailResponse.builder()
.id(series.getId())
.actors(series.getActors())
.title(series.getMedia().getTitle())
.description(series.getMedia().getDescription())
.posterUrl(series.getMedia().getPosterUrl())
.thumbnailUrl(series.getMedia().getThumbnailUrl())
.category(categories.isEmpty() ? null : categories.get(0))
.tags(tags)
.isBookmarked(isBookmarked)
.isLiked(isLiked)
.build();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

categories가 null인 경우 NPE가 발생할 수 있습니다.

categories.isEmpty() 호출 전에 categories가 null이면 NullPointerException이 발생합니다. null-safe 처리를 추가하는 것이 좋습니다.

🛡️ 제안하는 수정
-               .category(categories.isEmpty() ? null : categories.get(0))
+               .category(categories == null || categories.isEmpty() ? null : categories.get(0))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static SeriesDetailResponse of(Series series, List<String> tags, List<String> categories,
Boolean isBookmarked, Boolean isLiked) {
return SeriesDetailResponse.builder()
.id(series.getId())
.actors(series.getActors())
.title(series.getMedia().getTitle())
.description(series.getMedia().getDescription())
.posterUrl(series.getMedia().getPosterUrl())
.thumbnailUrl(series.getMedia().getThumbnailUrl())
.category(categories.isEmpty() ? null : categories.get(0))
.tags(tags)
.isBookmarked(isBookmarked)
.isLiked(isLiked)
.build();
public static SeriesDetailResponse of(Series series, List<String> tags, List<String> categories,
Boolean isBookmarked, Boolean isLiked) {
return SeriesDetailResponse.builder()
.id(series.getId())
.actors(series.getActors())
.title(series.getMedia().getTitle())
.description(series.getMedia().getDescription())
.posterUrl(series.getMedia().getPosterUrl())
.thumbnailUrl(series.getMedia().getThumbnailUrl())
.category(categories == null || categories.isEmpty() ? null : categories.get(0))
.tags(tags)
.isBookmarked(isBookmarked)
.isLiked(isLiked)
.build();
🤖 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/series/dto/SeriesDetailResponse.java`
around lines 49 - 62, In SeriesDetailResponse.of, the code calls
categories.isEmpty() which will NPE if categories is null; update the null-safe
check for the category assignment (the builder().category(...) call) to first
test whether categories is null or empty (e.g., categories == null ||
categories.isEmpty()) and return null when so, otherwise use categories.get(0);
adjust SeriesDetailResponse.of to use this null-safe expression when setting
category.

Comment on lines +55 to +57
Boolean isBookmarked = bookmarkRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId,
Status.ACTIVE);
Boolean isLiked = likesRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

비인증 사용자(memberId가 null)에 대한 처리가 필요합니다.

memberId가 null인 경우(비로그인 사용자) existsByMemberIdAndMediaIdAndStatus 호출 시 예상치 못한 결과가 반환될 수 있습니다. null 체크 후 기본값(false)을 반환하는 것이 안전합니다.

🛡️ 제안하는 수정
-               Boolean isBookmarked = bookmarkRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId,
-                               Status.ACTIVE);
-               Boolean isLiked = likesRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE);
+               Boolean isBookmarked = memberId != null
+                               ? bookmarkRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE)
+                               : false;
+               Boolean isLiked = memberId != null
+                               ? likesRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE)
+                               : false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Boolean isBookmarked = bookmarkRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId,
Status.ACTIVE);
Boolean isLiked = likesRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE);
Boolean isBookmarked = memberId != null
? bookmarkRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE)
: false;
Boolean isLiked = memberId != null
? likesRepository.existsByMemberIdAndMediaIdAndStatus(memberId, mediaId, Status.ACTIVE)
: false;
🤖 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/series/service/SeriesService.java`
around lines 55 - 57, Handle null memberId before calling repository methods: if
memberId is null, set isBookmarked and isLiked to false instead of invoking
bookmarkRepository.existsByMemberIdAndMediaIdAndStatus(...) and
likesRepository.existsByMemberIdAndMediaIdAndStatus(...). Update the logic in
SeriesService (use the memberId variable and the isBookmarked/isLiked locals) to
short-circuit and return false for unauthenticated users to avoid passing null
into the repository calls.

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.

2 participants