From e88332a810a5dcdb9a2ba0be0860d093a8fa2b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sat, 21 Feb 2026 14:26:06 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[FEAT]:=20Media=20Repository=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ott/domain/media/repository/MediaRepository.java | 7 +++++++ .../domain/media/repository/MediaRepositoryCustom.java | 4 ++++ .../domain/media/repository/MediaRepositoryImpl.java | 10 ++++++++++ 3 files changed, 21 insertions(+) create mode 100644 modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepository.java create mode 100644 modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java create mode 100644 modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java diff --git a/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepository.java b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepository.java new file mode 100644 index 0000000..97ffc04 --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepository.java @@ -0,0 +1,7 @@ +package com.ott.domain.media.repository; + +import com.ott.domain.media.domain.Media; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MediaRepository extends JpaRepository, MediaRepositoryCustom { +} diff --git a/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java new file mode 100644 index 0000000..78e32b5 --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java @@ -0,0 +1,4 @@ +package com.ott.domain.media.repository; + +public interface MediaRepositoryCustom { +} diff --git a/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java new file mode 100644 index 0000000..b82f81e --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java @@ -0,0 +1,10 @@ +package com.ott.domain.media.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MediaRepositoryImpl implements MediaRepositoryCustom { + + private final JPAQueryFactory queryFactory; +} From 182dd1d869231835765ef4f490680e13893c80e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sat, 21 Feb 2026 14:42:02 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[FEAT]:=20MediaTagRepository=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media_tag/repository/MediaTagRepository.java | 7 +++++++ .../media_tag/repository/MediaTagRepositoryCustom.java | 4 ++++ .../media_tag/repository/MediaTagRepositoryImpl.java | 10 ++++++++++ 3 files changed, 21 insertions(+) create mode 100644 modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepository.java create mode 100644 modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java create mode 100644 modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java diff --git a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepository.java b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepository.java new file mode 100644 index 0000000..9fd6e01 --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepository.java @@ -0,0 +1,7 @@ +package com.ott.domain.media_tag.repository; + +import com.ott.domain.media_tag.domain.MediaTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MediaTagRepository extends JpaRepository, MediaTagRepositoryCustom { +} diff --git a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java new file mode 100644 index 0000000..b091ef2 --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java @@ -0,0 +1,4 @@ +package com.ott.domain.media_tag.repository; + +public interface MediaTagRepositoryCustom { +} diff --git a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java new file mode 100644 index 0000000..ca00983 --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java @@ -0,0 +1,10 @@ +package com.ott.domain.media_tag.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MediaTagRepositoryImpl implements MediaTagRepositoryCustom { + + private final JPAQueryFactory queryFactory; +} From 2cb461faf7d5e72d95cc916f71f4e889f5f529e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sat, 21 Feb 2026 15:23:41 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[REFACTOR]:=20=EB=B0=B1=EC=98=A4=ED=94=BC?= =?UTF-8?q?=EC=8A=A4=20=EC=8B=9C=EB=A6=AC=EC=A6=88=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B3=80=EA=B2=BD=EC=95=88=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../series/mapper/BackOfficeSeriesMapper.java | 83 ++++++----- .../service/BackOfficeSeriesService.java | 140 +++++++++--------- .../repository/MediaRepositoryCustom.java | 7 + .../media/repository/MediaRepositoryImpl.java | 42 ++++++ .../repository/MediaTagRepositoryCustom.java | 6 + .../repository/MediaTagRepositoryImpl.java | 17 +++ 6 files changed, 184 insertions(+), 111 deletions(-) diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java index d1a4b90..8815ffa 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java @@ -1,30 +1,31 @@ -//package com.ott.api_admin.series.mapper; -// -//import com.ott.api_admin.series.dto.response.SeriesDetailResponse; -//import com.ott.api_admin.series.dto.response.SeriesListResponse; -//import com.ott.domain.series.domain.Series; -//import com.ott.domain.series_tag.domain.SeriesTag; -//import org.springframework.stereotype.Component; -// -//import java.util.List; -// -//@Component -//public class BackOfficeSeriesMapper { -// -// public SeriesListResponse toSeriesListResponse(Series series, List seriesTagList) { -// String categoryName = extractCategoryName(seriesTagList); -// List tagNameList = extractTagNameList(seriesTagList); -// -// return new SeriesListResponse( -// series.getId(), -// series.getThumbnailUrl(), -// series.getTitle(), -// categoryName, -// tagNameList, -// series.getPublicStatus() -// ); -// } -// +package com.ott.api_admin.series.mapper; + +import com.ott.api_admin.series.dto.response.SeriesDetailResponse; +import com.ott.api_admin.series.dto.response.SeriesListResponse; +import com.ott.domain.media.domain.Media; +import com.ott.domain.media_tag.domain.MediaTag; +import com.ott.domain.series.domain.Series; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class BackOfficeSeriesMapper { + + public SeriesListResponse toSeriesListResponse(Media media, List mediaTagList) { + String categoryName = extractCategoryName(mediaTagList); + List tagNameList = extractTagNameList(mediaTagList); + + return new SeriesListResponse( + media.getId(), + media.getThumbnailUrl(), + media.getTitle(), + categoryName, + tagNameList, + media.getPublicStatus() + ); + } + // public SeriesDetailResponse toSeriesDetailResponse(Series series, List seriesTagList) { // String categoryName = extractCategoryName(seriesTagList); // List tagNameList = extractTagNameList(seriesTagList); @@ -43,17 +44,17 @@ // series.getThumbnailUrl() // ); // } -// -// private String extractCategoryName(List seriesTagList) { -// return seriesTagList.stream() -// .findFirst() -// .map(st -> st.getTag().getCategory().getName()) -// .orElse(null); -// } -// -// private List extractTagNameList(List seriesTagList) { -// return seriesTagList.stream() -// .map(st -> st.getTag().getName()) -// .toList(); -// } -//} + + private String extractCategoryName(List mediaTagList) { + return mediaTagList.stream() + .findFirst() + .map(mt -> mt.getTag().getCategory().getName()) + .orElse(null); + } + + private List extractTagNameList(List mediaTagList) { + return mediaTagList.stream() + .map(mt -> mt.getTag().getName()) + .toList(); + } +} diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java index 0615ec6..3a61d7b 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java @@ -1,72 +1,72 @@ -//package com.ott.api_admin.series.service; -// -//import com.ott.api_admin.series.dto.response.SeriesDetailResponse; -//import com.ott.api_admin.series.dto.response.SeriesListResponse; -//import com.ott.api_admin.series.mapper.BackOfficeSeriesMapper; -//import com.ott.common.web.exception.BusinessException; -//import com.ott.common.web.exception.ErrorCode; -//import com.ott.domain.series.repository.SeriesRepository; -//import com.ott.domain.series_tag.repository.SeriesTagRepository; -//import com.ott.common.web.response.PageInfo; -//import com.ott.common.web.response.PageResponse; -//import com.ott.domain.series.domain.Series; -//import com.ott.domain.series_tag.domain.SeriesTag; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.data.domain.PageRequest; -//import org.springframework.data.domain.Pageable; -//import org.springframework.data.domain.Sort; -//import org.springframework.stereotype.Service; -//import org.springframework.transaction.annotation.Transactional; -//import org.springframework.util.StringUtils; -// -//import java.util.Collections; -//import java.util.List; -//import java.util.Map; -//import java.util.stream.Collectors; -// -//@RequiredArgsConstructor -//@Service -//public class BackOfficeSeriesService { -// -// private final BackOfficeSeriesMapper backOfficeSeriesMapper; -// -// private final SeriesRepository seriesRepository; -// private final SeriesTagRepository seriesTagRepository; -// -// @Transactional(readOnly = true) -// public PageResponse getSeries(int page, int size, String searchWord) { -// Pageable pageable = PageRequest.of(page, size); -// -// // 1. keyword 유무에 따라 분기 / 시리즈 대상 페이징 -// Page seriesPage = seriesRepository.findSeriesList(pageable, searchWord); -// -// // 2. 조회된 시리즈 ID 목록 추출 -// List seriesIdList = seriesPage.getContent().stream() -// .map(Series::getId) -// .toList(); -// -// // 3. IN절로 태그 일괄 조회 -// Map> tagListBySeriesId = seriesIdList.isEmpty() -// ? Collections.emptyMap() -// : seriesTagRepository.findWithTagAndCategoryBySeriesIds(seriesIdList).stream() -// .collect(Collectors.groupingBy(st -> st.getSeries().getId())); -// -// List responseList = seriesPage.getContent().stream() -// .map(series -> backOfficeSeriesMapper.toSeriesListResponse( -// series, -// tagListBySeriesId.getOrDefault(series.getId(), List.of()) -// )) -// .toList(); -// -// PageInfo pageInfo = PageInfo.toPageInfo( -// seriesPage.getNumber(), -// seriesPage.getTotalPages(), -// seriesPage.getSize() -// ); -// return PageResponse.toPageResponse(pageInfo, responseList); -// } -// +package com.ott.api_admin.series.service; + +import com.ott.api_admin.series.dto.response.SeriesDetailResponse; +import com.ott.api_admin.series.dto.response.SeriesListResponse; +import com.ott.api_admin.series.mapper.BackOfficeSeriesMapper; +import com.ott.common.web.exception.BusinessException; +import com.ott.common.web.exception.ErrorCode; +import com.ott.domain.common.MediaType; +import com.ott.domain.media.domain.Media; +import com.ott.domain.media.repository.MediaRepository; +import com.ott.domain.media_tag.domain.MediaTag; +import com.ott.domain.media_tag.repository.MediaTagRepository; +import com.ott.common.web.response.PageInfo; +import com.ott.common.web.response.PageResponse; +import com.ott.domain.series.domain.Series; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class BackOfficeSeriesService { + + private final BackOfficeSeriesMapper backOfficeSeriesMapper; + + private final MediaRepository mediaRepository; + private final MediaTagRepository mediaTagRepository; + + @Transactional(readOnly = true) + public PageResponse getSeries(int page, int size, String searchWord) { + Pageable pageable = PageRequest.of(page, size); + + // 1. 미디어 중 시리즈 대상 페이징 + Page mediaPage = mediaRepository.findMediaListByMediaType(pageable, MediaType.SERIES, searchWord); + + // 2. 조회된 미디어 ID 목록 추출 + List mediaIdList = mediaPage.getContent().stream() + .map(Media::getId) + .toList(); + + // 3. IN절로 태그 일괄 조회 + Map> tagListByMediaId = mediaIdList.isEmpty() + ? Collections.emptyMap() + : mediaTagRepository.findWithTagAndCategoryByMediaIds(mediaIdList).stream() + .collect(Collectors.groupingBy(mt -> mt.getMedia().getId())); + + List responseList = mediaPage.getContent().stream() + .map(media -> backOfficeSeriesMapper.toSeriesListResponse( + media, + tagListByMediaId.getOrDefault(media.getId(), List.of()) + )) + .toList(); + + PageInfo pageInfo = PageInfo.toPageInfo( + mediaPage.getNumber(), + mediaPage.getTotalPages(), + mediaPage.getSize() + ); + return PageResponse.toPageResponse(pageInfo, responseList); + } + // @Transactional(readOnly = true) // public SeriesDetailResponse getSeriesDetail(Long seriesId) { // Series series = seriesRepository.findById(seriesId) @@ -77,4 +77,4 @@ // // return backOfficeSeriesMapper.toSeriesDetailResponse(series, seriesTagList); // } -//} +} diff --git a/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java index 78e32b5..7a7af15 100644 --- a/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java +++ b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryCustom.java @@ -1,4 +1,11 @@ package com.ott.domain.media.repository; +import com.ott.domain.common.MediaType; +import com.ott.domain.media.domain.Media; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + public interface MediaRepositoryCustom { + + Page findMediaListByMediaType(Pageable pageable, MediaType mediaType, String searchWord); } diff --git a/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java index b82f81e..4c2b2f9 100644 --- a/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java +++ b/modules/domain/src/main/java/com/ott/domain/media/repository/MediaRepositoryImpl.java @@ -1,10 +1,52 @@ package com.ott.domain.media.repository; +import com.ott.domain.common.MediaType; +import com.ott.domain.media.domain.Media; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.util.StringUtils; + +import java.util.List; + +import static com.ott.domain.media.domain.QMedia.media; @RequiredArgsConstructor public class MediaRepositoryImpl implements MediaRepositoryCustom { private final JPAQueryFactory queryFactory; + + @Override + public Page findMediaListByMediaType(Pageable pageable, MediaType mediaType, String searchWord) { + List mediaList = queryFactory + .selectFrom(media) + .where( + media.mediaType.eq(mediaType), + titleContains(searchWord) + ) + .orderBy(media.createdDate.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(media.count()) + .from(media) + .where( + media.mediaType.eq(mediaType), + titleContains(searchWord) + ); + + return PageableExecutionUtils.getPage(mediaList, pageable, countQuery::fetchOne); + } + + private BooleanExpression titleContains(String searchWord) { + if (StringUtils.hasText(searchWord)) + return media.title.contains(searchWord); + return null; + } } diff --git a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java index b091ef2..22875db 100644 --- a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java +++ b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java @@ -1,4 +1,10 @@ package com.ott.domain.media_tag.repository; +import com.ott.domain.media_tag.domain.MediaTag; + +import java.util.List; + public interface MediaTagRepositoryCustom { + + List findWithTagAndCategoryByMediaIds(List mediaIds); } diff --git a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java index ca00983..9be5af0 100644 --- a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java +++ b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java @@ -1,10 +1,27 @@ package com.ott.domain.media_tag.repository; +import com.ott.domain.media_tag.domain.MediaTag; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import java.util.List; + +import static com.ott.domain.media_tag.domain.QMediaTag.mediaTag; +import static com.ott.domain.tag.domain.QTag.tag; +import static com.ott.domain.category.domain.QCategory.category; + @RequiredArgsConstructor public class MediaTagRepositoryImpl implements MediaTagRepositoryCustom { private final JPAQueryFactory queryFactory; + + @Override + public List findWithTagAndCategoryByMediaIds(List mediaIds) { + return queryFactory + .selectFrom(mediaTag) + .join(mediaTag.tag, tag).fetchJoin() + .join(tag.category, category).fetchJoin() + .where(mediaTag.media.id.in(mediaIds)) + .fetch(); + } } From 0d9782779b37d1c24cdbd03d4ccb961290835ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sat, 21 Feb 2026 15:49:32 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[REFACTOR]:=20=EC=8B=9C=EB=A6=AC=EC=A6=88?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EB=8A=94=20SeriesRepository=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../series/repository/SeriesRepository.java | 46 ++++++------ .../repository/SeriesRepositoryCustom.java | 6 +- .../repository/SeriesRepositoryImpl.java | 74 ++++++++----------- 3 files changed, 55 insertions(+), 71 deletions(-) diff --git a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java index 1cea6a4..e04b10f 100644 --- a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java +++ b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java @@ -1,23 +1,23 @@ -//package com.ott.domain.series.repository; -// -//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.series.domain.Series; -// -//public interface SeriesRepository extends JpaRepository, SeriesRepositoryCustom { -// -// // 제목에 검색어 포함, 상태 ACTIVE인 시리즈 검색 (최신순 정렬) -// @Query("SELECT s FROM Series s " + -// "WHERE LOWER(s.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " + -// "AND s.status = :status " + -// "ORDER BY s.createdDate DESC") -// List searchLatest(@Param("keyword") String keyword, -// @Param("status") Status status, -// Pageable pageable); -//} +package com.ott.domain.series.repository; + +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.series.domain.Series; + +public interface SeriesRepository extends JpaRepository, SeriesRepositoryCustom { + + // 제목에 검색어 포함, 상태 ACTIVE인 시리즈 검색 (최신순 정렬) + @Query("SELECT s FROM Series s " + + "WHERE LOWER(s.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " + + "AND s.status = :status " + + "ORDER BY s.createdDate DESC") + List searchLatest(@Param("keyword") String keyword, + @Param("status") Status status, + Pageable pageable); +} diff --git a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryCustom.java b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryCustom.java index f5fc2b6..aae6a81 100644 --- a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryCustom.java +++ b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryCustom.java @@ -1,10 +1,10 @@ package com.ott.domain.series.repository; import com.ott.domain.series.domain.Series; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; + +import java.util.Optional; public interface SeriesRepositoryCustom { - Page findSeriesList(Pageable pageable, String keyword); + Optional findWithMediaAndUploaderByMediaId(Long mediaId); } diff --git a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryImpl.java index 4dc78ec..daf832c 100644 --- a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryImpl.java +++ b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepositoryImpl.java @@ -1,45 +1,29 @@ -//package com.ott.domain.series.repository; -// -//import com.ott.domain.series.domain.Series; -//import com.querydsl.core.types.dsl.BooleanExpression; -//import com.querydsl.jpa.impl.JPAQuery; -//import com.querydsl.jpa.impl.JPAQueryFactory; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.data.domain.Pageable; -//import org.springframework.data.support.PageableExecutionUtils; -//import org.springframework.util.StringUtils; -// -//import java.util.List; -// -//import static com.ott.domain.series.domain.QSeries.series; -// -//@RequiredArgsConstructor -//public class SeriesRepositoryImpl implements SeriesRepositoryCustom { -// -// private final JPAQueryFactory queryFactory; -// -// @Override -// public Page findSeriesList(Pageable pageable, String searchWord) { -// List seriesList = queryFactory -// .selectFrom(series) -// .where(titleContains(searchWord)) -// .orderBy(series.createdDate.desc()) -// .offset(pageable.getOffset()) -// .limit(pageable.getPageSize()) -// .fetch(); -// -// JPAQuery countQuery = queryFactory -// .select(series.count()) -// .from(series) -// .where(titleContains(searchWord)); -// -// return PageableExecutionUtils.getPage(seriesList, pageable, countQuery::fetchOne); -// } -// -// private BooleanExpression titleContains(String searchWord) { -// if (StringUtils.hasText(searchWord)) -// return series.title.contains(searchWord); -// return null; -// } -//} +package com.ott.domain.series.repository; + +import com.ott.domain.series.domain.Series; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.util.Optional; + +import static com.ott.domain.media.domain.QMedia.media; +import static com.ott.domain.member.domain.QMember.member; +import static com.ott.domain.series.domain.QSeries.series; + +@RequiredArgsConstructor +public class SeriesRepositoryImpl implements SeriesRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findWithMediaAndUploaderByMediaId(Long mediaId) { + Series result = queryFactory + .selectFrom(series) + .join(series.media, media).fetchJoin() + .join(media.uploader, member).fetchJoin() + .where(media.id.eq(mediaId)) + .fetchOne(); + + return Optional.ofNullable(result); + } +} From cf78bf53a032438844f6259b76f2a1b6ea72eb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sat, 21 Feb 2026 16:02:15 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[REFACTOR]:=20Media=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=8B=9C=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BackOfficeSeriesController.java | 76 +++++++++---------- .../dto/response/SeriesListResponse.java | 4 +- .../series/mapper/BackOfficeSeriesMapper.java | 36 ++++----- .../service/BackOfficeSeriesService.java | 26 ++++--- .../repository/MediaTagRepositoryCustom.java | 2 + .../repository/MediaTagRepositoryImpl.java | 10 +++ 6 files changed, 86 insertions(+), 68 deletions(-) diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java index c2ac3a0..30fccf5 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java @@ -1,38 +1,38 @@ -//package com.ott.api_admin.series.controller; -// -//import com.ott.api_admin.series.dto.response.SeriesDetailResponse; -//import com.ott.api_admin.series.dto.response.SeriesListResponse; -//import com.ott.api_admin.series.service.BackOfficeSeriesService; -//import com.ott.common.web.response.PageResponse; -//import com.ott.common.web.response.SuccessResponse; -//import lombok.RequiredArgsConstructor; -//import org.springframework.http.ResponseEntity; -//import org.springframework.web.bind.annotation.*; -// -//@RestController -//@RequestMapping("/back-office") -//@RequiredArgsConstructor -//public class BackOfficeSeriesController implements BackOfficeSeriesApi { -// -// private final BackOfficeSeriesService backOfficeSeriesService; -// -// @Override -// @GetMapping("/admin/series") -// public ResponseEntity>> getSeries( -// @RequestParam(value = "page", defaultValue = "0") Integer page, -// @RequestParam(value = "size", defaultValue = "10") Integer size, -// @RequestParam(value = "searchWord", required = false) String searchWord -// ) { -// return ResponseEntity.ok( -// SuccessResponse.of(backOfficeSeriesService.getSeries(page, size, searchWord)) -// ); -// } -// -// @Override -// @GetMapping("/admin/series/{seriesId}") -// public ResponseEntity> getSeriesDetail(@PathVariable("seriesId") Long seriesId) { -// return ResponseEntity.ok( -// SuccessResponse.of(backOfficeSeriesService.getSeriesDetail(seriesId)) -// ); -// } -//} +package com.ott.api_admin.series.controller; + +import com.ott.api_admin.series.dto.response.SeriesDetailResponse; +import com.ott.api_admin.series.dto.response.SeriesListResponse; +import com.ott.api_admin.series.service.BackOfficeSeriesService; +import com.ott.common.web.response.PageResponse; +import com.ott.common.web.response.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/back-office") +@RequiredArgsConstructor +public class BackOfficeSeriesController implements BackOfficeSeriesApi { + + private final BackOfficeSeriesService backOfficeSeriesService; + + @Override + @GetMapping("/admin/series") + public ResponseEntity>> getSeries( + @RequestParam(value = "page", defaultValue = "0") Integer page, + @RequestParam(value = "size", defaultValue = "10") Integer size, + @RequestParam(value = "searchWord", required = false) String searchWord + ) { + return ResponseEntity.ok( + SuccessResponse.of(backOfficeSeriesService.getSeries(page, size, searchWord)) + ); + } + + @Override + @GetMapping("/admin/series/{seriesId}") + public ResponseEntity> getSeriesDetail(@PathVariable("seriesId") Long seriesId) { + return ResponseEntity.ok( + SuccessResponse.of(backOfficeSeriesService.getSeriesDetail(seriesId)) + ); + } +} diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesListResponse.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesListResponse.java index 4b5b74a..444547a 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesListResponse.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesListResponse.java @@ -8,8 +8,8 @@ @Schema(description = "시리즈 목록 조회 응답") public record SeriesListResponse( - @Schema(type = "Long", description = "시리즈 ID", example = "1") - Long seriesId, + @Schema(type = "Long", description = "미디어 ID (시리즈에서 참조)", example = "1") + Long mediaId, @Schema(type = "String", description = "썸네일 URL", example = "https://cdn.example.com/thumbnail.jpg") String thumbnailUrl, diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java index 8815ffa..7580f09 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/mapper/BackOfficeSeriesMapper.java @@ -26,24 +26,24 @@ public SeriesListResponse toSeriesListResponse(Media media, List media ); } -// public SeriesDetailResponse toSeriesDetailResponse(Series series, List seriesTagList) { -// String categoryName = extractCategoryName(seriesTagList); -// List tagNameList = extractTagNameList(seriesTagList); -// -// return new SeriesDetailResponse( -// series.getId(), -// series.getTitle(), -// series.getDescription(), -// categoryName, -// tagNameList, -// series.getPublicStatus(), -// series.getUploader().getNickname(), -// series.getBookmarkCount(), -// series.getActors(), -// series.getPosterUrl(), -// series.getThumbnailUrl() -// ); -// } + public SeriesDetailResponse toSeriesDetailResponse(Series series, Media media, String uploaderName, List mediaTagList) { + String categoryName = extractCategoryName(mediaTagList); + List tagNameList = extractTagNameList(mediaTagList); + + return new SeriesDetailResponse( + series.getId(), + media.getTitle(), + media.getDescription(), + categoryName, + tagNameList, + media.getPublicStatus(), + uploaderName, + media.getBookmarkCount(), + series.getActors(), + media.getPosterUrl(), + media.getThumbnailUrl() + ); + } private String extractCategoryName(List mediaTagList) { return mediaTagList.stream() diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java index 3a61d7b..5d11f8e 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/service/BackOfficeSeriesService.java @@ -13,6 +13,7 @@ import com.ott.common.web.response.PageInfo; import com.ott.common.web.response.PageResponse; import com.ott.domain.series.domain.Series; +import com.ott.domain.series.repository.SeriesRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -33,6 +34,7 @@ public class BackOfficeSeriesService { private final MediaRepository mediaRepository; private final MediaTagRepository mediaTagRepository; + private final SeriesRepository seriesRepository; @Transactional(readOnly = true) public PageResponse getSeries(int page, int size, String searchWord) { @@ -67,14 +69,18 @@ public PageResponse getSeries(int page, int size, String sea return PageResponse.toPageResponse(pageInfo, responseList); } -// @Transactional(readOnly = true) -// public SeriesDetailResponse getSeriesDetail(Long seriesId) { -// Series series = seriesRepository.findById(seriesId) -// .orElseThrow(() -> new BusinessException(ErrorCode.SERIES_NOT_FOUND)); -// -// List seriesTagList = seriesTagRepository -// .findWithTagAndCategoryBySeriesIds(List.of(seriesId)); -// -// return backOfficeSeriesMapper.toSeriesDetailResponse(series, seriesTagList); -// } + @Transactional(readOnly = true) + public SeriesDetailResponse getSeriesDetail(Long mediaId) { + // 1. Series + Media + Uploader 한 번에 조회 + Series series = seriesRepository.findWithMediaAndUploaderByMediaId(mediaId) + .orElseThrow(() -> new BusinessException(ErrorCode.SERIES_NOT_FOUND)); + + Media media = series.getMedia(); + String uploaderNickname = media.getUploader().getNickname(); + + // 2. 태그 조회 + List mediaTagList = mediaTagRepository.findWithTagAndCategoryByMediaId(mediaId); + + return backOfficeSeriesMapper.toSeriesDetailResponse(series, media, uploaderNickname, mediaTagList); + } } diff --git a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java index 22875db..945d74b 100644 --- a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java +++ b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryCustom.java @@ -7,4 +7,6 @@ public interface MediaTagRepositoryCustom { List findWithTagAndCategoryByMediaIds(List mediaIds); + + List findWithTagAndCategoryByMediaId(Long mediaId); } diff --git a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java index 9be5af0..d3522e3 100644 --- a/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java +++ b/modules/domain/src/main/java/com/ott/domain/media_tag/repository/MediaTagRepositoryImpl.java @@ -24,4 +24,14 @@ public List findWithTagAndCategoryByMediaIds(List mediaIds) { .where(mediaTag.media.id.in(mediaIds)) .fetch(); } + + @Override + public List findWithTagAndCategoryByMediaId(Long mediaId) { + return queryFactory + .selectFrom(mediaTag) + .join(mediaTag.tag, tag).fetchJoin() + .join(tag.category, category).fetchJoin() + .where(mediaTag.media.id.eq(mediaId)) + .fetch(); + } } From e948019d6ba52e51a7155f8f83dd1b165f2f66d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sat, 21 Feb 2026 16:23:44 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[REFACTOR]:=20seriesId=20->=20mediaId?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../series/controller/BackOfficeSeriesApi.java | 2 +- .../controller/BackOfficeSeriesController.java | 6 +++--- .../series/repository/SeriesRepository.java | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesApi.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesApi.java index 1c4b0e5..c535e77 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesApi.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesApi.java @@ -61,6 +61,6 @@ ResponseEntity>> getSeries( ) }) ResponseEntity> getSeriesDetail( - @Parameter(description = "시리즈 ID", required = true, example = "1") @PathVariable Long seriesId + @Parameter(description = "미디어 ID", required = true, example = "1") @PathVariable Long mediaId ); } diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java index 30fccf5..528985d 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/controller/BackOfficeSeriesController.java @@ -29,10 +29,10 @@ public ResponseEntity>> getSeri } @Override - @GetMapping("/admin/series/{seriesId}") - public ResponseEntity> getSeriesDetail(@PathVariable("seriesId") Long seriesId) { + @GetMapping("/admin/series/{mediaId}") + public ResponseEntity> getSeriesDetail(@PathVariable("mediaId") Long mediaId) { return ResponseEntity.ok( - SuccessResponse.of(backOfficeSeriesService.getSeriesDetail(seriesId)) + SuccessResponse.of(backOfficeSeriesService.getSeriesDetail(mediaId)) ); } } diff --git a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java index e04b10f..cb671b4 100644 --- a/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java +++ b/modules/domain/src/main/java/com/ott/domain/series/repository/SeriesRepository.java @@ -12,12 +12,12 @@ public interface SeriesRepository extends JpaRepository, SeriesRepositoryCustom { - // 제목에 검색어 포함, 상태 ACTIVE인 시리즈 검색 (최신순 정렬) - @Query("SELECT s FROM Series s " + - "WHERE LOWER(s.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " + - "AND s.status = :status " + - "ORDER BY s.createdDate DESC") - List searchLatest(@Param("keyword") String keyword, - @Param("status") Status status, - Pageable pageable); +// // 제목에 검색어 포함, 상태 ACTIVE인 시리즈 검색 (최신순 정렬) +// @Query("SELECT s FROM Series s " + +// "WHERE LOWER(s.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " + +// "AND s.status = :status " + +// "ORDER BY s.createdDate DESC") +// List searchLatest(@Param("keyword") String keyword, +// @Param("status") Status status, +// Pageable pageable); }