From 2d6149b4bc41a78f7b847cf1eb4b6d7a7da38534 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 18:43:59 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[REFACTOR]:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ott/api_admin/series/service/BackOfficeSeriesService.java | 2 +- .../com/ott/domain/media/repository/MediaRepositoryCustom.java | 2 +- .../com/ott/domain/media/repository/MediaRepositoryImpl.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 5d11f8e..05f7770 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 @@ -41,7 +41,7 @@ public PageResponse getSeries(int page, int size, String sea Pageable pageable = PageRequest.of(page, size); // 1. 미디어 중 시리즈 대상 페이징 - Page mediaPage = mediaRepository.findMediaListByMediaType(pageable, MediaType.SERIES, searchWord); + Page mediaPage = mediaRepository.findMediaListByMediaTypeAndSearchWord(pageable, MediaType.SERIES, searchWord); // 2. 조회된 미디어 ID 목록 추출 List mediaIdList = mediaPage.getContent().stream() 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 7a7af15..a0f886e 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 @@ -7,5 +7,5 @@ public interface MediaRepositoryCustom { - Page findMediaListByMediaType(Pageable pageable, MediaType mediaType, String searchWord); + Page findMediaListByMediaTypeAndSearchWord(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 4c2b2f9..425e45c 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 @@ -21,7 +21,7 @@ public class MediaRepositoryImpl implements MediaRepositoryCustom { private final JPAQueryFactory queryFactory; @Override - public Page findMediaListByMediaType(Pageable pageable, MediaType mediaType, String searchWord) { + public Page findMediaListByMediaTypeAndSearchWord(Pageable pageable, MediaType mediaType, String searchWord) { List mediaList = queryFactory .selectFrom(media) .where( From ac67586866ef9c4c29019bb8497efd9a430f319a 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 19:14:29 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[FEAT]:=20=EB=B0=B1=EC=98=A4=ED=94=BC?= =?UTF-8?q?=EC=8A=A4=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BackOfficeContentsApi.java | 47 +++++++++++++++++ .../BackOfficeContentsController.java | 34 ++++++++++++ .../dto/response/ContentsListResponse.java | 27 ++++++++++ .../mapper/BackOfficeContentsMapper.java | 22 ++++++++ .../service/BackOfficeContentsService.java | 52 +++++++++++++++++++ .../repository/MediaRepositoryCustom.java | 2 + .../media/repository/MediaRepositoryImpl.java | 39 ++++++++++++++ 7 files changed, 223 insertions(+) create mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java create mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java create mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsListResponse.java create mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java create mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.java diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java new file mode 100644 index 0000000..ad7c630 --- /dev/null +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java @@ -0,0 +1,47 @@ +package com.ott.api_admin.content.controller; + +import com.ott.api_admin.content.dto.response.ContentsListResponse; +import com.ott.common.web.exception.ErrorResponse; +import com.ott.common.web.response.PageResponse; +import com.ott.common.web.response.SuccessResponse; +import com.ott.domain.common.PublicStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; + +@Tag(name = "BackOffice Contents API", description = "[백오피스] 콘텐츠 관리 API") +public interface BackOfficeContentsApi { + + @Operation(summary = "콘텐츠 목록 조회", description = "콘텐츠 목록을 페이징으로 조회합니다. - ADMIN 권한 필요.") + @ApiResponses(value = { + @ApiResponse( + responseCode = "0", description = "조회 성공 - 페이징 dataList 구성", + content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ContentsListResponse.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))} + ), + @ApiResponse( + responseCode = "403", description = "접근 권한 없음 (ADMIN만 접근 가능)", + content = {@Content(mediaType = "application/json")} + ) + }) + ResponseEntity>> getContents( + @Parameter(description = "조회할 페이지의 번호를 입력해주세요. **page는 0부터 시작합니다**", required = true) @RequestParam(value = "page", defaultValue = "0") Integer page, + @Parameter(description = "한 페이지 당 최대 항목 개수를 입력해주세요. 기본값은 10입니다.", required = true) @RequestParam(value = "size", defaultValue = "10") Integer size, + @Parameter(description = "제목 부분일치 검색어. 미입력 시 전체 목록을 조회합니다.", required = false) @RequestParam(value = "searchWord", required = false) String searchWord, + @Parameter(description = "공개 여부. 공개/비공개로 나뉩니다.", required = false, example = "PUBLIC") @RequestParam(value = "publicStatus", required = false) PublicStatus publicStatus + ); +} diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java new file mode 100644 index 0000000..1e20c5f --- /dev/null +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java @@ -0,0 +1,34 @@ +package com.ott.api_admin.content.controller; + +import com.ott.api_admin.content.dto.response.ContentsListResponse; +import com.ott.api_admin.content.service.BackOfficeContentsService; +import com.ott.common.web.response.PageResponse; +import com.ott.common.web.response.SuccessResponse; +import com.ott.domain.common.PublicStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/back-office") +@RequiredArgsConstructor +public class BackOfficeContentsController implements BackOfficeContentsApi { + + private final BackOfficeContentsService backOfficeContentsService; + + @Override + @GetMapping("/admin/contents") + public ResponseEntity>> getContents( + @RequestParam(value = "page", defaultValue = "0") Integer page, + @RequestParam(value = "size", defaultValue = "10") Integer size, + @RequestParam(value = "searchWord", required = false) String searchWord, + @RequestParam(value = "publicStatus", required = false) PublicStatus publicStatus + ) { + return ResponseEntity.ok( + SuccessResponse.of(backOfficeContentsService.getContents(page, size, searchWord, publicStatus)) + ); + } +} diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsListResponse.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsListResponse.java new file mode 100644 index 0000000..4a5addb --- /dev/null +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsListResponse.java @@ -0,0 +1,27 @@ +package com.ott.api_admin.content.dto.response; + +import com.ott.domain.common.PublicStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.util.List; + +@Schema(description = "콘텐츠 목록 조회 응답") +public record ContentsListResponse( + + @Schema(type = "Long", description = "미디어 ID", example = "1") + Long mediaId, + + @Schema(type = "String", description = "포스터(세로, 5:7) URL", example = "https://cdn.example.com/thumbnail.jpg") + String poster_url, + + @Schema(type = "String", description = "콘텐츠 제목", example = "기생충") + String title, + + @Schema(type = "String", description = "공개 여부", example = "PUBLIC") + PublicStatus publicStatus, + + @Schema(type = "LocalDate", description = "업로드일", example = "2026-01-15") + LocalDate uploadedDate +) { +} diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java new file mode 100644 index 0000000..2838e5e --- /dev/null +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java @@ -0,0 +1,22 @@ +package com.ott.api_admin.content.mapper; + +import com.ott.api_admin.content.dto.response.ContentsListResponse; +import com.ott.domain.media.domain.Media; +import com.ott.domain.media_tag.domain.MediaTag; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class BackOfficeContentsMapper { + + public ContentsListResponse toContentsListResponse(Media media) { + return new ContentsListResponse( + media.getId(), + media.getPosterUrl(), + media.getTitle(), + media.getPublicStatus(), + media.getCreatedDate().toLocalDate() + ); + } +} diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.java new file mode 100644 index 0000000..5e7a8e0 --- /dev/null +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.java @@ -0,0 +1,52 @@ +package com.ott.api_admin.content.service; + +import com.ott.api_admin.content.dto.response.ContentsListResponse; +import com.ott.api_admin.content.mapper.BackOfficeContentsMapper; +import com.ott.common.web.response.PageInfo; +import com.ott.common.web.response.PageResponse; +import com.ott.domain.common.MediaType; +import com.ott.domain.common.PublicStatus; +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.domain.member.domain.Role; +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 BackOfficeContentsService { + + private final BackOfficeContentsMapper backOfficeContentsMapper; + + private final MediaRepository mediaRepository; + + @Transactional(readOnly = true) + public PageResponse getContents(int page, int size, String searchWord, PublicStatus publicStatus) { + Pageable pageable = PageRequest.of(page, size); + + // 미디어 중 콘텐츠 대상 페이징 + Page mediaPage = mediaRepository.findMediaListByMediaTypeAndSearchWordAndPublicStatus(pageable, MediaType.CONTENTS, searchWord, publicStatus); + + List responseList = mediaPage.getContent().stream() + .map(backOfficeContentsMapper::toContentsListResponse) + .toList(); + + PageInfo pageInfo = PageInfo.toPageInfo( + mediaPage.getNumber(), + mediaPage.getTotalPages(), + mediaPage.getSize() + ); + return PageResponse.toPageResponse(pageInfo, responseList); + } +} 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 a0f886e..395e008 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,6 +1,7 @@ package com.ott.domain.media.repository; import com.ott.domain.common.MediaType; +import com.ott.domain.common.PublicStatus; import com.ott.domain.media.domain.Media; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -8,4 +9,5 @@ public interface MediaRepositoryCustom { Page findMediaListByMediaTypeAndSearchWord(Pageable pageable, MediaType mediaType, String searchWord); + Page findMediaListByMediaTypeAndSearchWordAndPublicStatus(Pageable pageable, MediaType mediaType, String searchWord, PublicStatus publicStatus); } 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 425e45c..5ae5b98 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,6 +1,7 @@ package com.ott.domain.media.repository; import com.ott.domain.common.MediaType; +import com.ott.domain.common.PublicStatus; import com.ott.domain.media.domain.Media; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; @@ -44,9 +45,47 @@ public Page findMediaListByMediaTypeAndSearchWord(Pageable pageable, Medi return PageableExecutionUtils.getPage(mediaList, pageable, countQuery::fetchOne); } + @Override + public Page findMediaListByMediaTypeAndSearchWordAndPublicStatus(Pageable pageable, MediaType mediaType, String searchWord, PublicStatus publicStatus) { + List mediaList = queryFactory + .selectFrom(media) + .where( + mediaTypeEq(mediaType), + titleContains(searchWord), + publicStatusEq(publicStatus) + ) + .orderBy(media.createdDate.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(media.count()) + .from(media) + .where( + mediaTypeEq(mediaType), + titleContains(searchWord), + publicStatusEq(publicStatus) + ); + + return PageableExecutionUtils.getPage(mediaList, pageable, countQuery::fetchOne); + } + private BooleanExpression titleContains(String searchWord) { if (StringUtils.hasText(searchWord)) return media.title.contains(searchWord); return null; } + + private BooleanExpression mediaTypeEq(MediaType mediaType) { + if (mediaType != null) + return media.mediaType.eq(mediaType); + return null; + } + + private BooleanExpression publicStatusEq(PublicStatus publicStatus) { + if (publicStatus != null) + return media.publicStatus.eq(publicStatus); + return null; + } } From b2587f88ccfa348cb94b7a6c195cf17f9fb7763a 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 21:50:35 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[CHORE]:=20.gitkeep=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ott/api_admin/content/controller/.gitkeep | 0 .../src/main/java/com/ott/api_admin/content/dto/.gitkeep | 0 .../src/main/java/com/ott/api_admin/content/service/.gitkeep | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/controller/.gitkeep delete mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/dto/.gitkeep delete mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/service/.gitkeep diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/.gitkeep b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/.gitkeep b/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/service/.gitkeep b/apps/api-admin/src/main/java/com/ott/api_admin/content/service/.gitkeep deleted file mode 100644 index e69de29..0000000 From 033e25fd5bd9c25cdfac5f22ffd95c307563822c 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 22:52:41 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[FEAT]:=20=EB=B0=B1=EC=98=A4=ED=94=BC?= =?UTF-8?q?=EC=8A=A4=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BackOfficeContentsApi.java | 21 +++++++ .../BackOfficeContentsController.java | 12 ++++ .../dto/response/ContentsDetailResponse.java | 57 +++++++++++++++++++ .../mapper/BackOfficeContentsMapper.java | 38 +++++++++++++ .../service/BackOfficeContentsService.java | 35 ++++++++++-- .../repository/ContentsRepository.java | 27 ++++----- .../repository/ContentsRepositoryCustom.java | 10 ++++ .../repository/ContentsRepositoryImpl.java | 32 +++++++++++ 8 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java create mode 100644 modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryCustom.java create mode 100644 modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java index ad7c630..2d946c3 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java @@ -1,5 +1,6 @@ package com.ott.api_admin.content.controller; +import com.ott.api_admin.content.dto.response.ContentsDetailResponse; import com.ott.api_admin.content.dto.response.ContentsListResponse; import com.ott.common.web.exception.ErrorResponse; import com.ott.common.web.response.PageResponse; @@ -14,6 +15,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "BackOffice Contents API", description = "[백오피스] 콘텐츠 관리 API") @@ -44,4 +46,23 @@ ResponseEntity>> getContents( @Parameter(description = "제목 부분일치 검색어. 미입력 시 전체 목록을 조회합니다.", required = false) @RequestParam(value = "searchWord", required = false) String searchWord, @Parameter(description = "공개 여부. 공개/비공개로 나뉩니다.", required = false, example = "PUBLIC") @RequestParam(value = "publicStatus", required = false) PublicStatus publicStatus ); + + @Operation(summary = "콘텐츠 상세 조회", description = "콘텐츠 상세 정보를 조회합니다. - ADMIN 권한 필요.") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", description = "콘텐츠 상세 조회 성공", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ContentsDetailResponse.class))} + ), + @ApiResponse( + responseCode = "400", description = "시리즈 상세 조회 실패", + content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))} + ), + @ApiResponse( + responseCode = "403", description = "접근 권한 없음 (ADMIN만 접근 가능)", + content = {@Content(mediaType = "application/json")} + ) + }) + ResponseEntity> getContentsDetail( + @Parameter(description = "조회할 콘텐츠의 미디어 ID", required = true) @PathVariable Long mediaId + ); } diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java index 1e20c5f..8000bd4 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java @@ -1,5 +1,6 @@ package com.ott.api_admin.content.controller; +import com.ott.api_admin.content.dto.response.ContentsDetailResponse; import com.ott.api_admin.content.dto.response.ContentsListResponse; import com.ott.api_admin.content.service.BackOfficeContentsService; import com.ott.common.web.response.PageResponse; @@ -8,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -31,4 +33,14 @@ public ResponseEntity>> getCo SuccessResponse.of(backOfficeContentsService.getContents(page, size, searchWord, publicStatus)) ); } + + @Override + @GetMapping("/admin/contents/{mediaId}") + public ResponseEntity> getContentsDetail( + @PathVariable Long mediaId + ) { + return ResponseEntity.ok( + SuccessResponse.of(backOfficeContentsService.getContentsDetail(mediaId)) + ); + } } diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java new file mode 100644 index 0000000..6d97ee9 --- /dev/null +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java @@ -0,0 +1,57 @@ +package com.ott.api_admin.content.dto.response; + +import com.ott.domain.common.PublicStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.util.List; + +@Schema(description = "콘텐츠 상세 조회 응답") +public record ContentsDetailResponse( + + @Schema(type = "Long", description = "콘텐츠 ID", example = "1") + Long contentsId, + + @Schema(type = "String", description = "포스터 URL", example = "https://cdn.example.com/poster.jpg") + String posterUrl, + + @Schema(type = "String", description = "썸네일 URL", example = "https://cdn.example.com/thumb.jpg") + String thumbnailUrl, + + @Schema(type = "String", description = "콘텐츠 제목", example = "기생충") + String title, + + @Schema(type = "String", description = "콘텐츠 설명", example = "봉준호 감독의 블랙코미디 스릴러") + String description, + + @Schema(type = "String", description = "출연진", example = "송강호, 이선균") + String actors, + + @Schema(type = "String", description = "소속 시리즈 제목 (없으면 null)", example = "비밀의 숲") + String seriesTitle, + + @Schema(type = "String", description = "업로더 닉네임", example = "관리자") + String uploaderNickname, + + @Schema(type = "Integer", description = "영상 길이(초)", example = "7200") + Integer duration, + + @Schema(type = "Integer", description = "영상 크기(바이트)", example = "1048576") + Integer videoSize, + + @Schema(type = "String", description = "카테고리명", example = "드라마") + String categoryName, + + @Schema(type = "List", description = "태그 이름 목록", example = "[\"스릴러\", \"추리\"]") + List tagNameList, + + @Schema(type = "String", description = "공개 여부", example = "PUBLIC") + PublicStatus publicStatus, + + @Schema(type = "Long", description = "북마크 수", example = "150") + Long bookmarkCount, + + @Schema(type = "LocalDate", description = "업로드일", example = "2026-01-15") + LocalDate uploadedDate +) { +} diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java index 2838e5e..3897988 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/mapper/BackOfficeContentsMapper.java @@ -1,6 +1,8 @@ package com.ott.api_admin.content.mapper; +import com.ott.api_admin.content.dto.response.ContentsDetailResponse; import com.ott.api_admin.content.dto.response.ContentsListResponse; +import com.ott.domain.contents.domain.Contents; import com.ott.domain.media.domain.Media; import com.ott.domain.media_tag.domain.MediaTag; import org.springframework.stereotype.Component; @@ -19,4 +21,40 @@ public ContentsListResponse toContentsListResponse(Media media) { media.getCreatedDate().toLocalDate() ); } + + public ContentsDetailResponse toContentsDetailResponse(Contents contents, Media media, String uploaderNickname, String seriesTitle, List mediaTagList) { + String categoryName = extractCategoryName(mediaTagList); + List tagNameList = extractTagNameList(mediaTagList); + + return new ContentsDetailResponse( + contents.getId(), + media.getPosterUrl(), + media.getThumbnailUrl(), + media.getTitle(), + media.getDescription(), + contents.getActors(), + seriesTitle, + uploaderNickname, + contents.getDuration(), + contents.getVideoSize(), + categoryName, + tagNameList, + media.getPublicStatus(), + media.getBookmarkCount(), + media.getCreatedDate().toLocalDate() + ); + } + + 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/content/service/BackOfficeContentsService.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.java index 5e7a8e0..a5e3a46 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.java @@ -1,16 +1,20 @@ package com.ott.api_admin.content.service; +import com.ott.api_admin.content.dto.response.ContentsDetailResponse; import com.ott.api_admin.content.dto.response.ContentsListResponse; import com.ott.api_admin.content.mapper.BackOfficeContentsMapper; +import com.ott.common.web.exception.BusinessException; +import com.ott.common.web.exception.ErrorCode; import com.ott.common.web.response.PageInfo; import com.ott.common.web.response.PageResponse; import com.ott.domain.common.MediaType; import com.ott.domain.common.PublicStatus; +import com.ott.domain.contents.domain.Contents; +import com.ott.domain.contents.repository.ContentsRepository; 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.domain.member.domain.Role; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -18,10 +22,7 @@ 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 @@ -30,6 +31,8 @@ public class BackOfficeContentsService { private final BackOfficeContentsMapper backOfficeContentsMapper; private final MediaRepository mediaRepository; + private final MediaTagRepository mediaTagRepository; + private final ContentsRepository contentsRepository; @Transactional(readOnly = true) public PageResponse getContents(int page, int size, String searchWord, PublicStatus publicStatus) { @@ -49,4 +52,28 @@ public PageResponse getContents(int page, int size, String ); return PageResponse.toPageResponse(pageInfo, responseList); } + + @Transactional(readOnly = true) + public ContentsDetailResponse getContentsDetail(Long mediaId) { + // 1. Contents + Media + Uploader + Series + Series.media 한 번에 조회 + Contents contents = contentsRepository.findWithMediaAndUploaderByMediaId(mediaId) + .orElseThrow(() -> new BusinessException(ErrorCode.CONTENT_NOT_FOUND)); + + Media media = contents.getMedia(); + String uploaderNickname = media.getUploader().getNickname(); + + // 2. 소속 시리즈 제목 및 태그 추출 + Long originMediaId = mediaId; + String seriesTitle = null; + if (contents.getSeries() != null) { + Media originMedia = contents.getSeries().getMedia(); + originMediaId = originMedia.getId(); + seriesTitle = originMedia.getTitle(); + } + + // 3. 태그 조회 + List mediaTagList = mediaTagRepository.findWithTagAndCategoryByMediaId(originMediaId); + + return backOfficeContentsMapper.toContentsDetailResponse(contents, media, uploaderNickname, seriesTitle, mediaTagList); + } } diff --git a/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java index 9fd59b6..e5777a8 100644 --- a/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java +++ b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepository.java @@ -1,18 +1,11 @@ -//package com.ott.domain.contents.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.contents.domain.Contents; -// -//public interface ContentsRepository extends JpaRepository { -// -// // 제목에 검색어 포함, 상태 ACTIVE, 시리즈 없는 콘텐츠만 검색 (최신순 정렬) +package com.ott.domain.contents.repository; + +import com.ott.domain.contents.domain.Contents; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ContentsRepository extends JpaRepository, ContentsRepositoryCustom { + + // 제목에 검색어 포함, 상태 ACTIVE, 시리즈 없는 콘텐츠만 검색 (최신순 정렬) // @Query("SELECT c FROM Contents c " + // "WHERE LOWER(c.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " + // "AND c.status = :status " + @@ -20,5 +13,5 @@ // "ORDER BY c.createdDate DESC") // List searchLatest(@Param("keyword") String searchWord, @Param("status") Status status, // Pageable pageable); -// -//} \ No newline at end of file + +} \ No newline at end of file diff --git a/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryCustom.java b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryCustom.java new file mode 100644 index 0000000..2c8564e --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.ott.domain.contents.repository; + +import com.ott.domain.contents.domain.Contents; + +import java.util.Optional; + +public interface ContentsRepositoryCustom { + + Optional findWithMediaAndUploaderByMediaId(Long mediaId); +} diff --git a/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java new file mode 100644 index 0000000..b6bcc70 --- /dev/null +++ b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java @@ -0,0 +1,32 @@ +package com.ott.domain.contents.repository; + +import com.ott.domain.contents.domain.Contents; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.util.Optional; + +import static com.ott.domain.contents.domain.QContents.contents; +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 ContentsRepositoryImpl implements ContentsRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findWithMediaAndUploaderByMediaId(Long mediaId) { + Contents result = queryFactory + .selectFrom(contents) + .join(contents.media, media).fetchJoin() + .join(media.uploader, member).fetchJoin() + .leftJoin(contents.series, series).fetchJoin() + .leftJoin(series.media).fetchJoin() + .where(media.id.eq(mediaId)) + .fetchOne(); + + return Optional.ofNullable(result); + } +} From e15776187b06d12bc962ed334fd455052ef77701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sun, 22 Feb 2026 17:27:09 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[FEAT]:=20=EC=8B=9C=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=EB=AA=A9=EB=A1=9D=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 콘텐츠 업로드 시 모달에서 드롭다운으로 사용 --- .../controller/BackOfficeSeriesApi.java | 26 +++++++++++++++ .../BackOfficeSeriesController.java | 13 ++++++++ .../dto/response/SeriesTitleListResponse.java | 13 ++++++++ .../series/mapper/BackOfficeSeriesMapper.java | 8 +++++ .../service/BackOfficeSeriesService.java | 21 ++++++++++++ .../repository/SeriesRepositoryCustom.java | 4 +++ .../repository/SeriesRepositoryImpl.java | 33 +++++++++++++++++++ 7 files changed, 118 insertions(+) create mode 100644 apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesTitleListResponse.java 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 c535e77..7270949 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 @@ -2,6 +2,7 @@ import com.ott.api_admin.series.dto.response.SeriesDetailResponse; import com.ott.api_admin.series.dto.response.SeriesListResponse; +import com.ott.api_admin.series.dto.response.SeriesTitleListResponse; import com.ott.common.web.exception.ErrorResponse; import com.ott.common.web.response.PageResponse; import com.ott.common.web.response.SuccessResponse; @@ -45,6 +46,31 @@ ResponseEntity>> getSeries( @Parameter(description = "제목 부분일치 검색어. 미입력 시 전체 목록을 조회합니다.", required = false) @RequestParam(value = "searchWord", required = false) String searchWord ); + @Operation(summary = "시리즈 제목 목록 조회 (콘텐츠 업로드 페이지)", description = "시리즈 목록을 페이징으로 조회합니다. - ADMIN 권한 필요.") + @ApiResponses(value = { + @ApiResponse( + responseCode = "0", description = "조회 성공 - 페이징 dataList 구성", + content = {@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = SeriesTitleListResponse.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))} + ), + @ApiResponse( + responseCode = "403", description = "접근 권한 없음 (ADMIN만 접근 가능)", + content = {@Content(mediaType = "application/json")} + ) + }) + ResponseEntity>> getSeriesTitle( + @Parameter(description = "조회할 페이지의 번호를 입력해주세요. **page는 0부터 시작합니다**", required = true) @RequestParam(value = "page", defaultValue = "0") Integer page, + @Parameter(description = "한 페이지 당 최대 항목 개수를 입력해주세요. 기본값은 10입니다.", required = true) @RequestParam(value = "size", defaultValue = "10") Integer size, + @Parameter(description = "제목 부분일치 검색어. 미입력 시 전체 목록을 조회합니다.", required = false) @RequestParam(value = "searchWord", required = false) String searchWord + ); + @Operation(summary = "시리즈 상세 조회", description = "시리즈의 상세 정보를 조회합니다. - ADMIN 권한 필요.") @ApiResponses(value = { @ApiResponse( 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 528985d..cb7fcde 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 @@ -2,6 +2,7 @@ import com.ott.api_admin.series.dto.response.SeriesDetailResponse; import com.ott.api_admin.series.dto.response.SeriesListResponse; +import com.ott.api_admin.series.dto.response.SeriesTitleListResponse; import com.ott.api_admin.series.service.BackOfficeSeriesService; import com.ott.common.web.response.PageResponse; import com.ott.common.web.response.SuccessResponse; @@ -28,6 +29,18 @@ public ResponseEntity>> getSeri ); } + @Override + @GetMapping("/admin/series/titles") + public ResponseEntity>> getSeriesTitle( + @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.getSeriesTitle(page, size, searchWord)) + ); + } + @Override @GetMapping("/admin/series/{mediaId}") public ResponseEntity> getSeriesDetail(@PathVariable("mediaId") Long mediaId) { diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesTitleListResponse.java b/apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesTitleListResponse.java new file mode 100644 index 0000000..e3a8055 --- /dev/null +++ b/apps/api-admin/src/main/java/com/ott/api_admin/series/dto/response/SeriesTitleListResponse.java @@ -0,0 +1,13 @@ +package com.ott.api_admin.series.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record SeriesTitleListResponse( + + @Schema(type = "Long", description = "시리즈 ID", example = "1") + Long seriesId, + + @Schema(type = "String", description = "시리즈 제목", example = "비밀의 숲") + String title +) { +} 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 7580f09..7e6506d 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 @@ -2,6 +2,7 @@ import com.ott.api_admin.series.dto.response.SeriesDetailResponse; import com.ott.api_admin.series.dto.response.SeriesListResponse; +import com.ott.api_admin.series.dto.response.SeriesTitleListResponse; import com.ott.domain.media.domain.Media; import com.ott.domain.media_tag.domain.MediaTag; import com.ott.domain.series.domain.Series; @@ -26,6 +27,13 @@ public SeriesListResponse toSeriesListResponse(Media media, List media ); } + public SeriesTitleListResponse toSeriesTitleList(Series series) { + return new SeriesTitleListResponse( + series.getId(), + series.getMedia().getTitle() + ); + } + public SeriesDetailResponse toSeriesDetailResponse(Series series, Media media, String uploaderName, List mediaTagList) { String categoryName = extractCategoryName(mediaTagList); List tagNameList = extractTagNameList(mediaTagList); 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 05f7770..9f28a11 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,7 +1,9 @@ package com.ott.api_admin.series.service; +import com.ott.api_admin.content.dto.response.ContentsListResponse; import com.ott.api_admin.series.dto.response.SeriesDetailResponse; import com.ott.api_admin.series.dto.response.SeriesListResponse; +import com.ott.api_admin.series.dto.response.SeriesTitleListResponse; import com.ott.api_admin.series.mapper.BackOfficeSeriesMapper; import com.ott.common.web.exception.BusinessException; import com.ott.common.web.exception.ErrorCode; @@ -69,6 +71,25 @@ public PageResponse getSeries(int page, int size, String sea return PageResponse.toPageResponse(pageInfo, responseList); } + @Transactional(readOnly = true) + public PageResponse getSeriesTitle(Integer page, Integer size, String searchWord) { + Pageable pageable = PageRequest.of(page, size); + + // 시리즈 + 미디어 페이징 + Page seriesPage = seriesRepository.findSeriesListWithMediaBySearchWord(pageable, searchWord); + + List responseList = seriesPage.getContent().stream() + .map(backOfficeSeriesMapper::toSeriesTitleList) + .toList(); + + PageInfo pageInfo = PageInfo.toPageInfo( + seriesPage.getNumber(), + seriesPage.getTotalPages(), + seriesPage.getSize() + ); + return PageResponse.toPageResponse(pageInfo, responseList); + } + @Transactional(readOnly = true) public SeriesDetailResponse getSeriesDetail(Long mediaId) { // 1. Series + Media + Uploader 한 번에 조회 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 aae6a81..d361b5c 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,14 @@ 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 findSeriesListWithMediaBySearchWord(Pageable pageable, String searchWord); + 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 daf832c..f7402d4 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,9 +1,16 @@ 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 java.util.Optional; import static com.ott.domain.media.domain.QMedia.media; @@ -26,4 +33,30 @@ public Optional findWithMediaAndUploaderByMediaId(Long mediaId) { return Optional.ofNullable(result); } + + @Override + public Page findSeriesListWithMediaBySearchWord(Pageable pageable, String searchWord) { + List seriesList = queryFactory + .selectFrom(series) + .join(series.media, media).fetchJoin() + .where(titleContains(searchWord)) + .orderBy(series.createdDate.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(series.count()) + .from(series) + .join(series.media, media) + .where(titleContains(searchWord)); + + return PageableExecutionUtils.getPage(seriesList, pageable, countQuery::fetchOne); + } + + private BooleanExpression titleContains(String searchWord) { + if (StringUtils.hasText(searchWord)) + return media.title.contains(searchWord); + return null; + } } From dea9bca897e742de7177720e72e0b1e211f23510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sun, 22 Feb 2026 17:47:23 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[CHORE]:=20Admin=20=EC=A0=84=EC=9A=A9=20API?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/controller/BackOfficeContentsController.java | 6 +++--- .../member/controller/BackOfficeMemberController.java | 6 +++--- .../series/controller/BackOfficeSeriesController.java | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java index 8000bd4..093daec 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsController.java @@ -15,14 +15,14 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/back-office") +@RequestMapping("/back-office/admin/contents") @RequiredArgsConstructor public class BackOfficeContentsController implements BackOfficeContentsApi { private final BackOfficeContentsService backOfficeContentsService; @Override - @GetMapping("/admin/contents") + @GetMapping public ResponseEntity>> getContents( @RequestParam(value = "page", defaultValue = "0") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @@ -35,7 +35,7 @@ public ResponseEntity>> getCo } @Override - @GetMapping("/admin/contents/{mediaId}") + @GetMapping("/{mediaId}") public ResponseEntity> getContentsDetail( @PathVariable Long mediaId ) { diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/member/controller/BackOfficeMemberController.java b/apps/api-admin/src/main/java/com/ott/api_admin/member/controller/BackOfficeMemberController.java index 4650d6d..c455d48 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/member/controller/BackOfficeMemberController.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/member/controller/BackOfficeMemberController.java @@ -12,14 +12,14 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/back-office") +@RequestMapping("/back-office/admin/members") @RequiredArgsConstructor public class BackOfficeMemberController implements BackOfficeMemberApi { private final BackOfficeMemberService backOfficeMemberService; @Override - @GetMapping("/admin/members") + @GetMapping public ResponseEntity>> getMemberList( @RequestParam(value = "page", defaultValue = "0") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @@ -32,7 +32,7 @@ public ResponseEntity>> getMemb } @Override - @PatchMapping("/admin/members/{memberId}/role") + @PatchMapping("/{memberId}/role") public ResponseEntity changeRole( @PathVariable("memberId") Long memberId, @Valid @RequestBody ChangeRoleRequest changeRoleRequest 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 cb7fcde..5786f8e 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 @@ -11,14 +11,14 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/back-office") +@RequestMapping("/back-office/admin/series") @RequiredArgsConstructor public class BackOfficeSeriesController implements BackOfficeSeriesApi { private final BackOfficeSeriesService backOfficeSeriesService; @Override - @GetMapping("/admin/series") + @GetMapping public ResponseEntity>> getSeries( @RequestParam(value = "page", defaultValue = "0") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @@ -30,7 +30,7 @@ public ResponseEntity>> getSeri } @Override - @GetMapping("/admin/series/titles") + @GetMapping("/titles") public ResponseEntity>> getSeriesTitle( @RequestParam(value = "page", defaultValue = "0") Integer page, @RequestParam(value = "size", defaultValue = "10") Integer size, @@ -42,7 +42,7 @@ public ResponseEntity>> ge } @Override - @GetMapping("/admin/series/{mediaId}") + @GetMapping("/{mediaId}") public ResponseEntity> getSeriesDetail(@PathVariable("mediaId") Long mediaId) { return ResponseEntity.ok( SuccessResponse.of(backOfficeSeriesService.getSeriesDetail(mediaId)) From baebafe03ccf9b86eaea22d594b152a0f579f0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sun, 22 Feb 2026 18:54:33 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[CHORE]:=20swagger=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20query=20alias=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api_admin/content/controller/BackOfficeContentsApi.java | 2 +- .../domain/contents/repository/ContentsRepositoryImpl.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java index 2d946c3..e818a5d 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/controller/BackOfficeContentsApi.java @@ -54,7 +54,7 @@ ResponseEntity>> getContents( content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ContentsDetailResponse.class))} ), @ApiResponse( - responseCode = "400", description = "시리즈 상세 조회 실패", + responseCode = "400", description = "콘텐츠 상세 조회 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))} ), @ApiResponse( diff --git a/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java index b6bcc70..8d62958 100644 --- a/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java +++ b/modules/domain/src/main/java/com/ott/domain/contents/repository/ContentsRepositoryImpl.java @@ -1,6 +1,7 @@ package com.ott.domain.contents.repository; import com.ott.domain.contents.domain.Contents; +import com.ott.domain.media.domain.QMedia; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -18,12 +19,13 @@ public class ContentsRepositoryImpl implements ContentsRepositoryCustom { @Override public Optional findWithMediaAndUploaderByMediaId(Long mediaId) { + QMedia seriesMedia = new QMedia("seriesMedia"); Contents result = queryFactory .selectFrom(contents) .join(contents.media, media).fetchJoin() .join(media.uploader, member).fetchJoin() .leftJoin(contents.series, series).fetchJoin() - .leftJoin(series.media).fetchJoin() + .leftJoin(series.media, seriesMedia).fetchJoin() .where(media.id.eq(mediaId)) .fetchOne(); From 799cee16fd987e2fc75359675e6a6b9650b0b27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=8A=B9=EC=9A=B0?= Date: Sun, 22 Feb 2026 19:26:56 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[CHORE]:=20=EC=98=81=EC=83=81=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api_admin/content/dto/response/ContentsDetailResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java b/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java index 6d97ee9..7c2e7ef 100644 --- a/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java +++ b/apps/api-admin/src/main/java/com/ott/api_admin/content/dto/response/ContentsDetailResponse.java @@ -36,7 +36,7 @@ public record ContentsDetailResponse( @Schema(type = "Integer", description = "영상 길이(초)", example = "7200") Integer duration, - @Schema(type = "Integer", description = "영상 크기(바이트)", example = "1048576") + @Schema(type = "Integer", description = "영상 크기(KB)", example = "1048576") Integer videoSize, @Schema(type = "String", description = "카테고리명", example = "드라마")