[OT-152][FEAT]: 플레이리스트(홈&재생목록) API 및 추천 엔진 로직 구현#95
Conversation
Walkthrough미디어 중심 조회로 API를 재설계하고 플레이리스트 기능을 별도 모듈로 분리했습니다. 전략 패턴 기반의 추천/인기/히스토리/북마크/태그 플로우와 태그 가중치 기반 개인화 로직(PreferenceService, 전략 구현체, 리포지토리 쿼리)이 추가되었고, 기존 ContentListElement DTO 및 콘텐츠 내장 플레이리스트 엔드포인트가 제거되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant PlaylistController
participant PlaylistService
participant Strategy as PlaylistStrategy
participant MediaRepo as MediaRepository
participant PrefService as PlaylistPreferenceService
Client->>PlaylistController: GET /playlists/recommend (excludeMediaId,page,size)
PlaylistController->>PlaylistService: getPlaylists(condition, pageable)
PlaylistService->>PlaylistService: determineStrategyKey(condition)
PlaylistService->>Strategy: getPlaylist(condition, pageable)
alt Recommend
Strategy->>PrefService: getTotalTagScores(memberId)
PrefService-->>Strategy: tagScores
Strategy->>MediaRepo: findRecommendedMedias(tagScores, excludeMediaId, limit, offset)
else Other
Strategy->>MediaRepo: findTrendingPlaylists / findHistoryPlaylists / findBookmarkedPlaylists / findPlaylistsByTag
end
MediaRepo-->>Strategy: Page<Media>
Strategy-->>PlaylistService: Page<Media>
PlaylistService->>PlaylistService: to PlaylistResponse, build PageResponse
PlaylistService-->>PlaylistController: PageResponse<PlaylistResponse>
PlaylistController-->>Client: SuccessResponse<PageResponse<PlaylistResponse>>
sequenceDiagram
participant Home as HomeScreen
participant Controller as PlaylistController
participant Service as PlaylistService
participant TagStrategy as TagPlaylistStrategy
participant PrefService as PlaylistPreferenceService
participant MediaRepo as MediaRepository
Home->>Controller: GET /playlists/tags/top (index=0,page,size)
Controller->>Service: getTopTagPlaylists(condition(index=0), pageable)
Service->>TagStrategy: getPlaylist(condition, pageable)
TagStrategy->>PrefService: getTopTags(memberId)
PrefService-->>TagStrategy: topTags
TagStrategy->>MediaRepo: findPlaylistsByTag(topTagId, excludeMediaId, limit)
MediaRepo-->>TagStrategy: Page<Media>
TagStrategy-->>Service: Page<Media>
Service->>PrefService: getTopTags(memberId) (for metadata)
Service-->>Controller: TopTagPlaylistResponse(category, tag, medias)
Controller-->>Home: SuccessResponse<TopTagPlaylistResponse>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesController.java (1)
16-16:⚠️ Potential issue | 🟡 Minor사용하지 않는 import를 제거해 주세요.
io.micrometer.core.ipc.http.HttpSender.Response는 이 파일에서 사용되지 않습니다.🧹 사용하지 않는 import 제거
-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, Remove the unused import line "import io.micrometer.core.ipc.http.HttpSender.Response;" from SeriesController.java; locate the import section at the top of the file (look for the SeriesController class and its imports) and delete that specific import so there are no unused imports referenced by the SeriesController class.
🧹 Nitpick comments (8)
apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java (2)
61-71: 리스트 변수명 suffix 규칙을 맞춰주세요.
topTagIds,allTags는 List 타입이므로topTagIdList,allTagList처럼Listsuffix를 붙이는 편이 프로젝트 규칙과 일치합니다.As per coding guidelines,
Collection variable names with List suffix.🤖 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/playlist/service/PlaylistPreferenceService.java` around lines 61 - 71, Rename the List variables to follow the project's "List" suffix convention: change topTagIds to topTagIdList and allTags to allTagList, update the declaration where tagScores is processed (the stream producing topTagIds), update any subsequent references (the isEmpty() check, and usages like tagRepository.findAll() result handling), and return allTagList.stream().limit(3).collect(...) so all references (including topTagIdList and allTagList) are consistently renamed.
68-71: fallback에서 전체 태그 로드+셔플은 트래픽 증가 시 비용이 큽니다.매 요청마다
findAll()후shuffle하면 태그 수가 늘수록 부담이 커집니다. 활성 태그 기준으로 DB에서 3개만 가져오는 방식(전용 쿼리/페이지 제한)으로 바꾸는 것을 권장합니다.🤖 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/playlist/service/PlaylistPreferenceService.java` around lines 68 - 71, The fallback currently loads all tags and shuffles them (topTagIds.isEmpty() -> tagRepository.findAll() + Collections.shuffle), which is expensive; change PlaylistPreferenceService to query only three active tags directly from the database instead of findAll(), e.g. add a repository method (like TagRepository.findTop3ByActiveTrue() or a `@Query` with LIMIT 3 / ORDER BY RAND()/random function or use PageRequest.of(0,3)) and call that method in the fallback branch so only three rows are returned, avoiding in-memory shuffle and full-table reads.modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java (1)
57-67: 주석 처리된 이전 쿼리 블록은 제거하는 편이 좋습니다.현재 구현과 무관한 과거 쿼리가 길게 남아 있어 추후 유지보수 시 혼동 포인트가 됩니다.
🤖 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 57 - 67, Remove the legacy commented JPQL block and the unused method signature from ContentsRepository to avoid confusion: delete the commented `@Query`(...) and the commented Optional<Contents> findByIdAndStatusAndMedia_PublicStatus(...) lines inside the class (ContentsRepository) so only active code remains; if you need to preserve the snippet for history, rely on VCS instead of leaving it inline.modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java (1)
17-28: 주석 처리된 코드 제거를 고려해 주세요.새로운
findByMediaIdAndStatusAndPublicStatus메서드 구현은 올바르며 PR 목표에 맞게 mediaId 기반 조회로 전환되었습니다. 다만, 17-22번 라인의 주석 처리된 코드는 버전 관리 시스템에서 이력을 추적할 수 있으므로 제거하는 것이 깔끔합니다.♻️ 주석 처리된 코드 제거 제안
public interface SeriesRepository extends JpaRepository<Series, Long>, SeriesRepositoryCustom { - // 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); - `@Query`("SELECT s FROM Series s JOIN FETCH s.media m WHERE m.id = :mediaId AND s.status = :status AND m.publicStatus = :publicStatus") Optional<Series> findByMediaIdAndStatusAndPublicStatus(🤖 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 - 28, Remove the commented-out legacy methods from SeriesRepository (the commented Optional<Series> findByIdAndStatusAndMedia_PublicStatus and the `@Query` findByIdWithMedia block) since the new method findByMediaIdAndStatusAndPublicStatus replaces them; keep the new `@Query-based` findByMediaIdAndStatusAndPublicStatus implementation intact, then run a quick compile/IDE cleanup to drop any now-unused imports or warnings and verify no other code references the removed method names.apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java (1)
35-39: Swagger 파라미터 설명 업데이트를 권장합니다.path 변수가
seriesId에서mediaId로 변경되었지만,@Parameterdescription은 여전히 "시리즈 ID"로 되어 있습니다. API 사용자의 혼란을 방지하기 위해 설명을 명확히 업데이트하는 것을 권장합니다.📝 Swagger 설명 업데이트 제안
- `@Parameter`(description = "시리즈 ID", required = true, example = "1") `@PathVariable`("mediaId") Long mediaId, + `@Parameter`(description = "시리즈 미디어 ID", required = true, example = "1") `@PathVariable`("mediaId") Long mediaId,55번 라인의
getSeriesContents메서드에도 동일하게 적용해 주세요.🤖 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 35 - 39, The `@Parameter` description for the path variable has stale text: update the description on the getSeriesDetail method (and similarly on getSeriesContents) so it matches the actual path variable name and meaning (mediaId) — e.g., change the `@Parameter`(description = "시리즈 ID", ...) tied to `@PathVariable`("mediaId") in getSeriesDetail and the matching annotation in getSeriesContents to a clear description like "미디어 ID" or "series media ID" to avoid confusion in Swagger.apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistService.java (1)
63-63: 컬렉션 변수명에List접미사를 맞춰주세요.
topTags를topTagList로 변경하면 가이드라인과 일관성이 맞습니다.As per coding guidelines "Collection variable names with
Listsuffix".🤖 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/playlist/service/PlaylistService.java` at line 63, Rename the collection variable topTags to topTagList in PlaylistService where you assign from preferenceService.getTopTags(condition.getMemberId()); update all usages in the same method/class to the new name (topTagList) to follow the "List" suffix guideline and avoid compile errors; ensure imports/closures and any related local references are adjusted accordingly.apps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/TagPlaylistStrategy.java (1)
40-40: 컬렉션 변수명은List접미사로 통일해 주세요.
topTags대신topTagList처럼 타입이 드러나는 이름이 가이드라인과 일치합니다.As per coding guidelines "Collection variable names with
Listsuffix".🤖 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/playlist/service/strategy/TagPlaylistStrategy.java` at line 40, Rename the collection variable topTags in TagPlaylistStrategy to follow the guideline using a List suffix (e.g., topTagList): update the declaration and all usages where preferenceService.getTopTags(condition.getMemberId()) is assigned/used so the variable name change is consistent throughout the method/class (references in any loops, streams, or method calls must be updated).modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java (1)
142-143: 리스트 변수명은List접미사로 통일해 주세요.
content대신contentList/mediaList처럼 컬렉션 타입이 드러나는 이름으로 맞추는 것이 좋습니다.As per coding guidelines "Collection variable names with
Listsuffix".Also applies to: 166-167, 192-193, 219-220
🤖 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/media/repository/MediaRepositoryImpl.java` around lines 142 - 143, In MediaRepositoryImpl, rename collection variables that use the generic name content to include a List suffix (e.g., content -> contentList or mediaList) so they follow the "Collection variable names with List suffix" guideline; update all occurrences and usages within the same method(s) (including the other instances noted around lines 166-167, 192-193, 219-220) to the new names to preserve compilation and behavior, and ensure any related variable declarations, returns, or local references are renamed consistently.
🤖 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/playlist/controller/PlaylistController.java`:
- Around line 22-25: PlaylistController currently lacks explicit role-based
access control; add Spring Security annotations to enforce "USER/EDITOR/ADMIN"
checks for all playlist endpoints by annotating the controller or each API
method (e.g., on class PlaylistController or methods from PlaylistApi) with an
appropriate authorization expression such as
`@PreAuthorize`("hasAnyRole('USER','EDITOR','ADMIN')") or method-level
`@RolesAllowed` values, ensuring sensitive endpoints are limited to those roles
and adjusting expressions per endpoint if stricter roles are required.
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java`:
- Around line 74-75: The call in PlaylistPreferenceService that returns
tagRepository.findAllById(topTagIds) can return entities in DB order and break
the topTagIds ranking; change it to fetch the tags by IDs (using
tagRepository.findAllById or a custom query) and then reorder the resulting
List<Tag> to match the original topTagIds sequence before returning — e.g.,
build a Map<Id, Tag> from the fetched results and produce a List by iterating
topTagIds and looking up each id (ensure null-safety for missing ids). This
preserves the computed rank order (topTagIds) without relying on DB return
ordering.
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistService.java`:
- Around line 68-70: The metadata assembly currently uses condition.getIndex()
without checking for negative values, which can throw when calling
topTags.get(index); in PlaylistService update the guard to validate index is
within bounds by ensuring condition.getIndex() != null && condition.getIndex()
>= 0 && condition.getIndex() < topTags.size() before accessing topTags, so the
Tag targetTag = topTags.get(condition.getIndex()) call is only executed for
valid indices.
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/TagPlaylistStrategy.java`:
- Around line 37-57: The code in TagPlaylistStrategy leaves targetTagId null
when both tagId and index are absent and does not check for negative index
values; update the logic in the block that uses
preferenceService.getTopTags(condition.getMemberId()) so you first handle the
case where both condition.getTagId()/targetTagId and condition.getIndex() are
null by returning Page.empty(pageable), then validate condition.getIndex() is >=
0 before using it; when index is out of bounds or negative also return
Page.empty(pageable). Keep the existing behavior of setting targetTagId =
topTags.get(condition.getIndex()).getId() only after the new non-negative and
bounds checks so mediaRepository.findMediasByTagId is never called with a
null/invalid tag id.
In
`@modules/common-web/src/main/java/com/ott/common/web/exception/ErrorCode.java`:
- Around line 46-53: The ErrorCode enum has reassigned code values for existing
constants (e.g., CONTENT_NOT_FOUND, SERIES_NOT_FOUND, CATEGORY_NOT_FOUND,
TAG_NOT_FOUND, MEDIA_NOT_FOUND, COMMENT_NOT_FOUND, BOOKMARK_NOT_FOUND) which
will break external contracts; revert those constants to their original code
strings and preserve their HttpStatus/message, and if you need new error
conditions add new enum entries with new unique codes in a separate code range
instead of changing existing codes so clients and monitoring rules remain
stable.
In
`@modules/domain/src/main/java/com/ott/domain/likes/repository/LikesRepository.java`:
- Around line 24-31: 현재 findRecentTagIdsByMemberId 쿼리는 Likes와 MediaTag를 조인한 뒤
Pageable을 적용해 "조인된 태그 행 100개"를 제한하므로 미디어당 태그 수에 의해 좋아요 샘플이 편향됩니다; 대신 2단계로 변경하세요:
먼저 LikesRepository에 최근 좋아요 100건만 조회하는 메서드(예: findRecentLikesMediaIdsByMemberId
or findRecentLikesByMemberId)를 추가해 memberId와 status로 ORDER BY l.createdDate DESC
+ Pageable(limit 100)로 media.id만 반환하고, 그 결과 mediaId 리스트를 사용해 MediaTag에서 WHERE
media.id IN (:mediaIds)로 태그들을 조회해 최종 tag id 목록을 반환하도록 로직을 분리(즉,
findRecentTagIdsByMemberId는 제거하거나 호출 흐름을 바꿔)하세요.
In
`@modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java`:
- Around line 26-40: The repository methods lack a mediaType filter so
unintended media types can leak into playlist/medias queries; update the
signatures of findTrendingPlaylists, findHistoryPlaylists,
findBookmarkedPlaylists, findPlaylistsByTag, findMediasByTagId, and
findRecommendedMedias to accept a mediaType parameter (e.g., MediaType mediaType
or an equivalent enum/int) and propagate that parameter into the implementation
and any PlaylistCondition construction so the repository layer can filter by
mediaType; ensure callers (service/condition builders) are updated to supply the
mediaType when invoking these methods.
In
`@modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java`:
- Around line 180-188: The countQuery used when building the Page (the
JPAQuery<Long> named countQuery passed to PageableExecutionUtils.getPage) is
missing the same filters and joins applied to the content query (notably the
isActiveAndPublic predicate and any join conditions used for
history/bookmark/tag methods), causing totalElements to be inflated; fix each
method (the content query and its corresponding countQuery in
MediaRepositoryImpl where playback, memberId, excludeMediaId and excludeId are
used) by applying the identical where predicates and joins (reuse the same
isActiveAndPublic(), excludeId(...) and any join-building logic) when
constructing countQuery so the count reflects the same filtered dataset as the
content query.
- Around line 264-271: The empty tagScores fallback currently returns a query
that can re-include the media being viewed; update the branch in
MediaRepositoryImpl where tagScores.isEmpty() is handled to also apply the
excludeMediaId filter — i.e., add a where condition excluding
media.id.eq(excludeMediaId) (guarded for null/absent excludeMediaId or use .ne
when present) to the queryFactory.selectFrom(media) chain so the returned
results never include the excluded media id.
In
`@modules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java`:
- Around line 19-28: The current findRecentTagIdsByMemberId query limits JOIN
rows (tags) with Pageable instead of limiting Playback records first; change to
a two-step approach: add a method like
findRecentPlaybackIdsByMemberId(`@Param`("memberId") Long memberId,
`@Param`("status") Status status, Pageable pageable) that returns List<Long>
playbackIds (select p.id ordered by p.modifiedDate desc), then change/replace
findRecentTagIdsByMemberId to accept a List<Long> playbackIds and query tag ids
by joining MediaTag mt to Playback p with p.id IN :playbackIds (e.g., SELECT
mt.tag.id FROM MediaTag mt JOIN Playback p ON p.contents.media.id = mt.media.id
WHERE p.id IN :playbackIds) so you reliably limit to the most recent 100
Playback records before expanding their tags.
---
Outside diff comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesController.java`:
- Line 16: Remove the unused import line "import
io.micrometer.core.ipc.http.HttpSender.Response;" from SeriesController.java;
locate the import section at the top of the file (look for the SeriesController
class and its imports) and delete that specific import so there are no unused
imports referenced by the SeriesController class.
---
Nitpick comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java`:
- Around line 61-71: Rename the List variables to follow the project's "List"
suffix convention: change topTagIds to topTagIdList and allTags to allTagList,
update the declaration where tagScores is processed (the stream producing
topTagIds), update any subsequent references (the isEmpty() check, and usages
like tagRepository.findAll() result handling), and return
allTagList.stream().limit(3).collect(...) so all references (including
topTagIdList and allTagList) are consistently renamed.
- Around line 68-71: The fallback currently loads all tags and shuffles them
(topTagIds.isEmpty() -> tagRepository.findAll() + Collections.shuffle), which is
expensive; change PlaylistPreferenceService to query only three active tags
directly from the database instead of findAll(), e.g. add a repository method
(like TagRepository.findTop3ByActiveTrue() or a `@Query` with LIMIT 3 / ORDER BY
RAND()/random function or use PageRequest.of(0,3)) and call that method in the
fallback branch so only three rows are returned, avoiding in-memory shuffle and
full-table reads.
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistService.java`:
- Line 63: Rename the collection variable topTags to topTagList in
PlaylistService where you assign from
preferenceService.getTopTags(condition.getMemberId()); update all usages in the
same method/class to the new name (topTagList) to follow the "List" suffix
guideline and avoid compile errors; ensure imports/closures and any related
local references are adjusted accordingly.
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/TagPlaylistStrategy.java`:
- Line 40: Rename the collection variable topTags in TagPlaylistStrategy to
follow the guideline using a List suffix (e.g., topTagList): update the
declaration and all usages where
preferenceService.getTopTags(condition.getMemberId()) is assigned/used so the
variable name change is consistent throughout the method/class (references in
any loops, streams, or method calls must be updated).
In
`@apps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.java`:
- Around line 35-39: The `@Parameter` description for the path variable has stale
text: update the description on the getSeriesDetail method (and similarly on
getSeriesContents) so it matches the actual path variable name and meaning
(mediaId) — e.g., change the `@Parameter`(description = "시리즈 ID", ...) tied to
`@PathVariable`("mediaId") in getSeriesDetail and the matching annotation in
getSeriesContents to a clear description like "미디어 ID" or "series media ID" to
avoid confusion in Swagger.
In
`@modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java`:
- Around line 57-67: Remove the legacy commented JPQL block and the unused
method signature from ContentsRepository to avoid confusion: delete the
commented `@Query`(...) and the commented Optional<Contents>
findByIdAndStatusAndMedia_PublicStatus(...) lines inside the class
(ContentsRepository) so only active code remains; if you need to preserve the
snippet for history, rely on VCS instead of leaving it inline.
In
`@modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java`:
- Around line 142-143: In MediaRepositoryImpl, rename collection variables that
use the generic name content to include a List suffix (e.g., content ->
contentList or mediaList) so they follow the "Collection variable names with
List suffix" guideline; update all occurrences and usages within the same
method(s) (including the other instances noted around lines 166-167, 192-193,
219-220) to the new names to preserve compilation and behavior, and ensure any
related variable declarations, returns, or local references are renamed
consistently.
In
`@modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java`:
- Around line 17-28: Remove the commented-out legacy methods from
SeriesRepository (the commented Optional<Series>
findByIdAndStatusAndMedia_PublicStatus and the `@Query` findByIdWithMedia block)
since the new method findByMediaIdAndStatusAndPublicStatus replaces them; keep
the new `@Query-based` findByMediaIdAndStatusAndPublicStatus implementation
intact, then run a quick compile/IDE cleanup to drop any now-unused imports or
warnings and verify no other code references the removed method names.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (34)
apps/api-user/src/main/java/com/ott/api_user/common/dto/ContentListElement.javaapps/api-user/src/main/java/com/ott/api_user/content/controller/ContentsApi.javaapps/api-user/src/main/java/com/ott/api_user/content/controller/ContentsController.javaapps/api-user/src/main/java/com/ott/api_user/content/dto/ContentsDetailResponse.javaapps/api-user/src/main/java/com/ott/api_user/content/service/ContentsService.javaapps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistApi.javaapps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistController.javaapps/api-user/src/main/java/com/ott/api_user/playlist/dto/request/PlaylistCondition.javaapps/api-user/src/main/java/com/ott/api_user/playlist/dto/response/PlaylistResponse.javaapps/api-user/src/main/java/com/ott/api_user/playlist/dto/response/TopTagPlaylistResponse.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistService.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/BookmarkPlaylistStrategy.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/HistoryPlaylistStrategy.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/PlaylistStrategy.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/RecommendPlaylistStrategy.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/TagPlaylistStrategy.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/TrendingPlaylistStrategy.javaapps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesApi.javaapps/api-user/src/main/java/com/ott/api_user/series/controller/SeriesController.javaapps/api-user/src/main/java/com/ott/api_user/series/dto/SeriesContentsResponse.javaapps/api-user/src/main/java/com/ott/api_user/series/dto/SeriesDetailResponse.javaapps/api-user/src/main/java/com/ott/api_user/series/service/SeriesService.javaapps/api-user/src/main/resources/application.ymlmodules/common-web/src/main/java/com/ott/common/web/exception/ErrorCode.javamodules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.javamodules/domain/src/main/java/com/ott/domain/likes/repository/LikesRepository.javamodules/domain/src/main/java/com/ott/domain/media/repository/MediaRepository.javamodules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.javamodules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.javamodules/domain/src/main/java/com/ott/domain/member/repository/MemberRepository.javamodules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.javamodules/domain/src/main/java/com/ott/domain/preferred_tag/repository/PreferredTagRepository.javamodules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java
💤 Files with no reviewable changes (1)
- apps/api-user/src/main/java/com/ott/api_user/common/dto/ContentListElement.java
| CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND, "B101", "콘텐츠를 찾을 수 없습니다"), | ||
| SERIES_NOT_FOUND(HttpStatus.NOT_FOUND, "B102", "시리즈를 찾을 수 없습니다"), | ||
| CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "B103", "카테고리를 찾을 수 없습니다"), | ||
| TAG_NOT_FOUND(HttpStatus.NOT_FOUND, "B104", "태그를 찾을 수 없습니다"), | ||
| MEDIA_NOT_FOUND(HttpStatus.NOT_FOUND, "B105", "미디어를 찾을 수 없습니다"), | ||
| COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "B106", "댓글을 찾을 수 없습니다"), | ||
| BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, "B107", "북마크를 찾을 수 없습니다"), | ||
|
|
There was a problem hiding this comment.
기존 에러 상수의 code 값 변경은 외부 계약을 깨뜨릴 수 있습니다.
CONTENT_NOT_FOUND, SERIES_NOT_FOUND 같은 기존 상수의 code를 재배정하면, 클라이언트 분기 로직과 운영 모니터링 룰이 깨질 수 있습니다. 기존 상수는 기존 code를 유지하고, 신규 케이스만 새 코드 대역으로 추가하는 방식이 더 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@modules/common-web/src/main/java/com/ott/common/web/exception/ErrorCode.java`
around lines 46 - 53, The ErrorCode enum has reassigned code values for existing
constants (e.g., CONTENT_NOT_FOUND, SERIES_NOT_FOUND, CATEGORY_NOT_FOUND,
TAG_NOT_FOUND, MEDIA_NOT_FOUND, COMMENT_NOT_FOUND, BOOKMARK_NOT_FOUND) which
will break external contracts; revert those constants to their original code
strings and preserve their HttpStatus/message, and if you need new error
conditions add new enum entries with new unique codes in a separate code range
instead of changing existing codes so clients and monitoring rules remain
stable.
There was a problem hiding this comment.
이 부분은 클라쪽에서 에러 코드 체계를 맞춰서(아직 진행전이니) 작성하면 되므로 무시하겠습니다
| Page<Media> findTrendingPlaylists(Long excludeMediaId, Pageable pageable); | ||
|
|
||
| // 시청 이력 조회 (최근 시청 순) | ||
| Page<Media> findHistoryPlaylists(Long memberId, Long excludeMediaId, Pageable pageable); | ||
|
|
||
| // 북마크 목록 조회 (최근 찜한 순) | ||
| Page<Media> findBookmarkedPlaylists(Long memberId, Long excludeMediaId, Pageable pageable); | ||
|
|
||
| // 특정 태그 기반 미디어 목록 조회 | ||
| Page<Media> findPlaylistsByTag(Long tagId, Long excludeMediaId, Pageable pageable); | ||
|
|
||
| Page<Media> findMediaListByMediaTypeAndSearchWordAndPublicStatus(Pageable pageable, MediaType mediaType, | ||
| String searchWord, PublicStatus publicStatus); | ||
|
|
||
| Page<Media> findMediaListByMediaTypeAndSearchWordAndPublicStatusAndUploaderId(Pageable pageable, | ||
| MediaType mediaType, String searchWord, PublicStatus publicStatus, Long uploaderId); | ||
| List<Media> findMediasByTagId(Long tagId, Long excludeMediaId, int limit , long offset); | ||
|
|
||
| Page<Media> findOriginMediaListBySearchWord(Pageable pageable, String searchWord); | ||
| } | ||
| List<Media> findRecommendedMedias(Map<Long, Integer> tagScores, Long excludeMediaId, int limit, long offset); |
There was a problem hiding this comment.
플레이리스트 조회 시 mediaType 필터를 전달할 수 없는 시그니처입니다.
PlaylistCondition 기반 재사용 구조라면 저장소 계층까지 mediaType이 내려가야 하는데, 현재 신규 메서드들은 excludeMediaId만 받아 타입 필터를 강제할 수 없습니다. 이 상태면 홈/상세/피드 간 공용 로직에서 의도치 않은 미디어 타입이 섞일 수 있습니다.
♻️ 시그니처 정리 제안
- Page<Media> findTrendingPlaylists(Long excludeMediaId, Pageable pageable);
+ Page<Media> findTrendingPlaylists(MediaType mediaType, Long excludeMediaId, Pageable pageable);
- Page<Media> findHistoryPlaylists(Long memberId, Long excludeMediaId, Pageable pageable);
+ Page<Media> findHistoryPlaylists(Long memberId, MediaType mediaType, Long excludeMediaId, Pageable pageable);
- Page<Media> findBookmarkedPlaylists(Long memberId, Long excludeMediaId, Pageable pageable);
+ Page<Media> findBookmarkedPlaylists(Long memberId, MediaType mediaType, Long excludeMediaId, Pageable pageable);
- Page<Media> findPlaylistsByTag(Long tagId, Long excludeMediaId, Pageable pageable);
+ Page<Media> findPlaylistsByTag(Long tagId, MediaType mediaType, Long excludeMediaId, Pageable pageable);🤖 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/media/repository/MediaRepositoryCustom.java`
around lines 26 - 40, The repository methods lack a mediaType filter so
unintended media types can leak into playlist/medias queries; update the
signatures of findTrendingPlaylists, findHistoryPlaylists,
findBookmarkedPlaylists, findPlaylistsByTag, findMediasByTagId, and
findRecommendedMedias to accept a mediaType parameter (e.g., MediaType mediaType
or an equivalent enum/int) and propagate that parameter into the implementation
and any PlaylistCondition construction so the repository layer can filter by
mediaType; ensure callers (service/condition builders) are updated to supply the
mediaType when invoking these methods.
There was a problem hiding this comment.
이거는,,,, 한번에 처리할게요,,, 모든 조회 코드에 미디어 타입 처리를 안해놨네요 대참사
marulog
left a comment
There was a problem hiding this comment.
모든 DTO에 type 값 넣어주시면 감사하겠습니다.
| @GetMapping | ||
| ResponseEntity<SuccessResponse<PageResponse<PlaylistResponse>>> getPlaylists( | ||
| @Parameter(description = "플레이리스트 조회 조건 (source 필수)", required = true) PlaylistCondition condition, | ||
| @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0") Integer page, |
There was a problem hiding this comment.
min, max나 positive 검증이 필요해 보입니다
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
apps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistController.java (1)
22-25:⚠️ Potential issue | 🟠 Major컨트롤러 레벨 역할 기반 접근 제어가 누락되어 있습니다.
플레이리스트 API 전반에
USER/EDITOR/ADMIN권한 체크를 명시적으로 적용해 주세요.🔒 수정 예시
+import org.springframework.security.access.prepost.PreAuthorize; @@ `@RestController` `@RequiredArgsConstructor` `@RequestMapping`("/playlists") +@PreAuthorize("hasAnyRole('USER','EDITOR','ADMIN')") public class PlaylistController implements PlaylistApi {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/playlist/controller/PlaylistController.java` around lines 22 - 25, Add controller-level role-based access to PlaylistController by annotating the class (PlaylistController) so all endpoints implementing PlaylistApi require one of roles USER, EDITOR, or ADMIN; update the class annotations to use your project's security annotation (e.g., `@PreAuthorize`("hasAnyRole('USER','EDITOR','ADMIN')") or `@RolesAllowed`({"USER","EDITOR","ADMIN"}") depending on configured security) and ensure the necessary security imports are added so every method in PlaylistController enforces the USER/EDITOR/ADMIN check.apps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistApi.java (1)
45-105:⚠️ Potential issue | 🟠 MajorID 파라미터의 양수 검증이 누락되어 잘못된 입력이 통과할 수 있습니다.
tagId와excludeMediaId에@Positive를 붙여 음수/0 ID를 초기에 차단하는 것이 안전합니다.🔧 수정 예시
- ResponseEntity<SuccessResponse<PageResponse<PlaylistResponse>>> getTagPlaylists( - `@Parameter`(description = "태그 ID", required = true) `@PathVariable`(value = "tagId") Long tagId, + ResponseEntity<SuccessResponse<PageResponse<PlaylistResponse>>> getTagPlaylists( + `@Positive` + `@Parameter`(description = "태그 ID", required = true) `@PathVariable`(value = "tagId") Long tagId, - `@Parameter`(description = "현재 영상 ID") `@RequestParam`(value = "excludeMediaId", required = false) Long excludeMediaId, + `@Positive` + `@Parameter`(description = "현재 영상 ID") `@RequestParam`(value = "excludeMediaId", required = false) Long excludeMediaId,🤖 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/playlist/controller/PlaylistApi.java` around lines 45 - 105, Add `@Positive` validation to ID params so negative/zero IDs are rejected: annotate the tagId parameter in getTagPlaylists (the `@PathVariable` Long tagId) with `@Positive`, and annotate all excludeMediaId `@RequestParam` parameters (seen in getTopTagPlaylists, getTagPlaylists, getTrendingPlaylists, getHistoryPlaylists, getBookmarkPlaylists, getSearchPlaylists) with `@Positive` (or `@PositiveOrZero` if zero is allowed) to enforce input validation; update imports if needed and ensure validation annotations are applied consistently across these controller method signatures.
🧹 Nitpick comments (4)
modules/common-web/src/main/java/com/ott/common/web/exception/GlobalExceptionHandler.java (1)
94-100:ConstraintViolationException핸들러 추가는 적절합니다.
@Validated애노테이션 기반 메서드 파라미터 검증 실패 시 발생하는 예외를 처리하여 400 응답을 반환하는 것은 올바른 접근입니다.다만,
MethodArgumentNotValidException핸들러(Line 36)가ex.getBindingResult()를 사용하여 구조화된 필드별 에러 정보를 제공하는 것과 달리, 이 핸들러는ex.getMessage()만 사용합니다. 클라이언트에게 더 유용한 에러 응답을 제공하려면getConstraintViolations()를 활용하여 필드별 에러 정보를 추출하는 것을 고려해 보세요.♻️ 구조화된 에러 응답을 위한 개선 예시
`@ExceptionHandler`(ConstraintViolationException.class) protected ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException ex) { log.warn("ConstraintViolationException: {}", ex.getMessage()); - // C001 에러 코드를 사용하여 400 Bad Request 응답 생성 - ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT, ex.getMessage()); + ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT, ex.getConstraintViolations()); return ResponseEntity.badRequest().body(response); }이 방식을 사용하려면
ErrorResponse.of()메서드에Set<ConstraintViolation<?>>파라미터를 받는 오버로드가 필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/common-web/src/main/java/com/ott/common/web/exception/GlobalExceptionHandler.java` around lines 94 - 100, The current handleConstraintViolationException(ConstraintViolationException ex) only logs ex.getMessage() and returns ErrorResponse.of(ErrorCode.INVALID_INPUT, ex.getMessage()); update it to extract structured field-level errors from ex.getConstraintViolations() and pass them into the response; add or use an overloaded ErrorResponse.of(ErrorCode, Set<ConstraintViolation<?>>) (or map the violations to a simple field->message map/list) and call that from handleConstraintViolationException so the client receives per-field error details (use ConstraintViolation<?>.getPropertyPath() and .getMessage() to build the structured payload).modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java (2)
142-152: 컬렉션 변수명은List접미사로 통일하는 것이 좋습니다.
List<Media>타입 변수content는mediaList처럼 컬렉션임이 드러나는 이름으로 맞추면 가독성과 규칙 일관성이 좋아집니다.♻️ 제안 수정안
- List<Media> content = queryFactory + List<Media> mediaList = queryFactory .selectFrom(media) ... - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + return PageableExecutionUtils.getPage(mediaList, pageable, countQuery::fetchOne);As per coding guidelines, "Collection variable names with
Listsuffix".Also applies to: 166-178, 194-206, 223-235
🤖 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/media/repository/MediaRepositoryImpl.java` around lines 142 - 152, Rename the collection variable named content to mediaList in MediaRepositoryImpl for all QueryDSL query blocks that build List<Media> via queryFactory.selectFrom(media) (i.e., replace local variable content with mediaList in the query block, subsequent uses, and any return/assignment that references it); apply the same rename to the other similar List<Media> variables flagged (the other queryFactory.selectFrom(media) blocks) so collection names consistently end with List and update any dependent code in the same method to use mediaList.
292-300: 추천 쿼리의 태그 스캔 범위를 줄이는 최적화를 권장합니다.현재는
mediaTag전체 조인 후 CASE 합산을 수행하므로 트래픽 증가 시 비용이 커질 수 있습니다.tagScores의 키 집합으로 태그 범위를 먼저 좁히는 편이 유리합니다.⚡ 제안 수정안
+ List<Long> preferredTagIdList = List.copyOf(tagScores.keySet()); return queryFactory.selectFrom(media) .join(mediaTag).on(mediaTag.media.id.eq(media.id)) .where( media.status.eq(Status.ACTIVE), media.publicStatus.eq(PublicStatus.PUBLIC), + mediaTag.tag.id.in(preferredTagIdList), excludeMediaId != null ? media.id.ne(excludeMediaId) : null) .groupBy(media.id) .orderBy(scoreExpression.sum().desc(), media.id.desc())🤖 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/media/repository/MediaRepositoryImpl.java` around lines 292 - 300, The current query in MediaRepositoryImpl scans all mediaTag rows then sums CASE expressions (scoreExpression); narrow that scan by pre-filtering mediaTag to only tags present in tagScores: compute the set/list of tag keys from tagScores.keySet(), and change the join condition on mediaTag (the join(mediaTag).on(...)) to include mediaTag.tag.name.in(tagKeys) (or return early if tagKeys is empty) so the database only examines relevant tag rows before grouping and ordering by scoreExpression.sum().desc().apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java (1)
74-79: Fallback에서 전체 태그 로딩은 트래픽 증가 시 비용이 큽니다.
findAll()+shuffle대신 DB에서 랜덤 3건만 조회하거나, 캐시된 후보군에서 샘플링하는 방식으로 바꾸는 것을 권장합니다.🤖 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/playlist/service/PlaylistPreferenceService.java` around lines 74 - 79, The fallback branch in PlaylistPreferenceService currently loads all tags with tagRepository.findAll() and shuffles them (when topTagIds.isEmpty()), which is expensive; replace this with either (A) a repository-level random sampling method (e.g., add TagRepository method like findRandomTags(int limit) implemented with a DB-side random/order-by-rand or TABLESAMPLE query to return exactly 3 rows) or (B) sample 3 tags from a cached candidate pool (e.g., cachedTagPool or a new in-memory/cached list refreshed periodically) and return those; update the PlaylistPreferenceService fallback to call the new TagRepository.findRandomTags(3) or the cache sampler instead of findAll()+shuffle.
🤖 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/playlist/service/PlaylisStrategytService.java`:
- Around line 59-70: getTopTagPlaylistWithMetadata currently calls
getPlaylists(condition, pageable) before computing the target tag, so the
condition.tagId is never set and the TAG strategy query can be wrong; update the
method (getTopTagPlaylistWithMetadata) to first retrieve topTags via
preferenceService.getTopTags(condition.getMemberId()), check
condition.getIndex() bounds, call condition.setTagId(targetTag.getId()) when a
targetTag exists, then call getPlaylists(condition, pageable) to obtain
mediaPage; keep existing variables TopTagPlaylistResponse.CategoryInfo and
TagInfo logic after mediaPage and ensure you handle null/empty topTags safely.
In
`@modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepository.java`:
- Around line 16-19: The JPQL in MediaTagRepository's findTagIdsByMediaIds is
comparing mt.media (an entity) to a List<Long>, causing a runtime type error;
update the query to use mt.media.id in the WHERE IN clause (i.e., compare
mt.media.id to :mediaIds) so the parameter List<Long> matches the field type.
---
Duplicate comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistApi.java`:
- Around line 45-105: Add `@Positive` validation to ID params so negative/zero IDs
are rejected: annotate the tagId parameter in getTagPlaylists (the `@PathVariable`
Long tagId) with `@Positive`, and annotate all excludeMediaId `@RequestParam`
parameters (seen in getTopTagPlaylists, getTagPlaylists, getTrendingPlaylists,
getHistoryPlaylists, getBookmarkPlaylists, getSearchPlaylists) with `@Positive`
(or `@PositiveOrZero` if zero is allowed) to enforce input validation; update
imports if needed and ensure validation annotations are applied consistently
across these controller method signatures.
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistController.java`:
- Around line 22-25: Add controller-level role-based access to
PlaylistController by annotating the class (PlaylistController) so all endpoints
implementing PlaylistApi require one of roles USER, EDITOR, or ADMIN; update the
class annotations to use your project's security annotation (e.g.,
`@PreAuthorize`("hasAnyRole('USER','EDITOR','ADMIN')") or
`@RolesAllowed`({"USER","EDITOR","ADMIN"}") depending on configured security) and
ensure the necessary security imports are added so every method in
PlaylistController enforces the USER/EDITOR/ADMIN check.
---
Nitpick comments:
In
`@apps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.java`:
- Around line 74-79: The fallback branch in PlaylistPreferenceService currently
loads all tags with tagRepository.findAll() and shuffles them (when
topTagIds.isEmpty()), which is expensive; replace this with either (A) a
repository-level random sampling method (e.g., add TagRepository method like
findRandomTags(int limit) implemented with a DB-side random/order-by-rand or
TABLESAMPLE query to return exactly 3 rows) or (B) sample 3 tags from a cached
candidate pool (e.g., cachedTagPool or a new in-memory/cached list refreshed
periodically) and return those; update the PlaylistPreferenceService fallback to
call the new TagRepository.findRandomTags(3) or the cache sampler instead of
findAll()+shuffle.
In
`@modules/common-web/src/main/java/com/ott/common/web/exception/GlobalExceptionHandler.java`:
- Around line 94-100: The current
handleConstraintViolationException(ConstraintViolationException ex) only logs
ex.getMessage() and returns ErrorResponse.of(ErrorCode.INVALID_INPUT,
ex.getMessage()); update it to extract structured field-level errors from
ex.getConstraintViolations() and pass them into the response; add or use an
overloaded ErrorResponse.of(ErrorCode, Set<ConstraintViolation<?>>) (or map the
violations to a simple field->message map/list) and call that from
handleConstraintViolationException so the client receives per-field error
details (use ConstraintViolation<?>.getPropertyPath() and .getMessage() to build
the structured payload).
In
`@modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java`:
- Around line 142-152: Rename the collection variable named content to mediaList
in MediaRepositoryImpl for all QueryDSL query blocks that build List<Media> via
queryFactory.selectFrom(media) (i.e., replace local variable content with
mediaList in the query block, subsequent uses, and any return/assignment that
references it); apply the same rename to the other similar List<Media> variables
flagged (the other queryFactory.selectFrom(media) blocks) so collection names
consistently end with List and update any dependent code in the same method to
use mediaList.
- Around line 292-300: The current query in MediaRepositoryImpl scans all
mediaTag rows then sums CASE expressions (scoreExpression); narrow that scan by
pre-filtering mediaTag to only tags present in tagScores: compute the set/list
of tag keys from tagScores.keySet(), and change the join condition on mediaTag
(the join(mediaTag).on(...)) to include mediaTag.tag.name.in(tagKeys) (or return
early if tagKeys is empty) so the database only examines relevant tag rows
before grouping and ordering by scoreExpression.sum().desc().
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 1b3e9de3-7124-455b-824b-c49a8a9a5b2d
📒 Files selected for processing (10)
apps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistApi.javaapps/api-user/src/main/java/com/ott/api_user/playlist/controller/PlaylistController.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylisStrategytService.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/PlaylistPreferenceService.javaapps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/TagPlaylistStrategy.javamodules/common-web/src/main/java/com/ott/common/web/exception/GlobalExceptionHandler.javamodules/domain/src/main/java/com/ott/domain/likes/repository/LikesRepository.javamodules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.javamodules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepository.javamodules/domain/src/main/java/com/ott/domain/playback/repository/PlaybackRepository.java
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/api-user/src/main/java/com/ott/api_user/playlist/service/strategy/TagPlaylistStrategy.java
📝 작업 내용
플레이리스트 API RESTful 엔드포인트 분리
단일 API로 처리되던 로직을 목적에 맞게 6개의 직관적인 엔드포인트로 분리 (/recommend, /tags/top, /tags/{tagId}, /trending, /history, /bookmarks)
홈 화면 Top 3 태그 전용 응답 DTO (TopTagPlaylistResponse) 구현
프론트엔드의 화면 렌더링 편의성 및 네트워크 호출 최적화를 위해, 응답 최상단에 카테고리와 태그 메타데이터(CategoryInfo, TagInfo)를 포함하여 반환하도록 설계
excludeMediaId를 활용한 도메인별 셔플(Shuffle) 전략 세분화
기본 excludeMediaId 전략
🏠 홈 화면: 유저가 지금 보고 있는 영상이 없으므로, 프론트엔드가 excludeMediaId를 보내지 않습니다. (null)
🎬 상세 페이지: 유저가 이미 특정 영상을 누르고 들어왔으므로, 중복 추천을 막기 위해 프론트엔드가 excludeMediaId=152처럼 값을 무조건 보냅니다.
셔플 전략
추천 및 취향 태그 영역: 홈 화면(excludeMediaId 미포함)일 경우 디스커버리 UX를 위해 콘텐츠 무작위 셔플. 단, 유저의 Top 3 태그 순위 자체는 고정.
인기 차트 및 시청 이력 영역: 정합성이 중요하므로 셔플 로직을 완벽히 배제하고 점수순/최신순 고정 정렬 유지.
개인화 추천 엔진 아키텍처 설계
1. 다중 가중치 합산 기반의 점수(Score) 산출 로직 구현
유저의 '선호 태그', '시청 이력', '좋아요' 등의 행동 데이터를 수집하여 각각의 가중치를 부여하고 합산하는 추천 알고리즘을 구현했습니다.
-> 자바의 Map 을 활용해 최종 점수를 계산합니다.
-> DB에 복잡한 집계 연산을 위임하지 않고, 자바의 Map<TagId, Score> 구조를 활용해 최종 점수를 계산합니다.
Map의 Key 속성을 활용하여 태그 중복 누적 계산을 방지함과 동시에, Map.merge() 메서드를 통해 같은 태그를 가진 여러 콘텐츠가 발견되더라도 하나의 태그 Key에 대해 점수가 안전하고 빠르게 누적 합산되도록 구현했습니다.
2. 최신 트렌드 반영 및 OOM(메모리 초과) 방지
Pageable(limit=100)을 걸어 최근 행동 100개만 점수에 반영하도록 설계했습니다.이를 통해 서버 메모리를 보호하고, 유저의 '현재 취향'을 가장 민감하게 캐치합니다.
3. QueryDSL CaseBuilder를 활용한 DB 레벨의 연산 최적화
📷 스크린샷
위 빨간 4개의 박스영역은 홈화면에서 사용되며
총 6개의 모든 API 는 홈화면 뿐만 아니라 재생목록에서도 재사용됩니다. 단(excluedMediaId 와 함께 사용함)
+) 검색 API 도 swagger 에 표시함. 단 검색 결과에서 상세 페이지로 진입 시 Recommend 전략으로 대체되어 재생목록에는 추천 리스트가 뜨도록 함.
프론트 호출 방식:
GET /playlists/search?excludeMediaId=152&page=1&size=20위처럼 호출하면 재생목록은 서버 내부에서 추천 리스트로 대체할 수 있게함!
https://www.notion.so/O-T-3182753972b780d58b34c459197bf64b
자세한 API 병렬 호출 방식은 위 노션에 새로 업데이트 해두었습니다.
향후 고도화 전략
PlaylistPreferenceService가 계산한Map<Long, Integer>점수표를 Redis에 하루 단위로 캐싱해두면 DB 부하를 획기적으로 줄일 수 있습니다.→ 어떻게 생각하시는지... 이제 MVP 2차...라고 할 수 있나 암튼 선택과 집중을 할 때 입니다...
☑️ 체크 리스트
#️⃣ 연관된 이슈
💬 리뷰 요구사항
Summary by CodeRabbit
Release Notes