diff --git a/adapters/in-web/src/main/kotlin/com/pokit/category/v2/dto/request/CreateCategoryRequestV2.kt b/adapters/in-web/src/main/kotlin/com/pokit/category/v2/dto/request/CreateCategoryRequestV2.kt index 5d59a293..1d2bb645 100644 --- a/adapters/in-web/src/main/kotlin/com/pokit/category/v2/dto/request/CreateCategoryRequestV2.kt +++ b/adapters/in-web/src/main/kotlin/com/pokit/category/v2/dto/request/CreateCategoryRequestV2.kt @@ -3,6 +3,7 @@ package com.pokit.category.v2.dto.request import com.pokit.category.dto.CategoryCommand import com.pokit.category.model.OpenType import com.pokit.user.model.InterestType +import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size @@ -11,8 +12,10 @@ data class CreateCategoryRequestV2( @field:Size(min = 1, max = 10, message = "최대 10자까지 입력 가능합니다.") val categoryName: String, val categoryImageId: Int, + @field:Schema(description = "PUBLIC / PRIVATE 중 하나여야 합니다.") val openType: String, - val keywordType: String, + @field:Schema(description = "관심사 리스트랑 같은 리스트 쓰니 그 중 하나 선택") + val keywordType: String = "default", ) internal fun CreateCategoryRequestV2.toDto() = CategoryCommand( diff --git a/adapters/in-web/src/main/kotlin/com/pokit/content/ContentController.kt b/adapters/in-web/src/main/kotlin/com/pokit/content/ContentController.kt index a5111636..bd0eb2fb 100644 --- a/adapters/in-web/src/main/kotlin/com/pokit/content/ContentController.kt +++ b/adapters/in-web/src/main/kotlin/com/pokit/content/ContentController.kt @@ -199,9 +199,28 @@ class ContentController( @PathVariable("contentId") contentId: Long, @RequestBody request: UpdateThumbnailRequest ): ResponseEntity { - return contentUseCase.updateThumbnail(user.id, contentId,request.toDto()) + return contentUseCase.updateThumbnail(user.id, contentId, request.toDto()) .wrapOk() } + @GetMapping("/recommended") + @Operation(summary = "추천 컨텐츠 목록 조회 API", description = "keyword 생략이나 비워서 보내면 전체보기 적용") + fun getRecommendedContents( + @AuthenticationPrincipal user: PrincipalUser, + @PageableDefault( + page = 0, + size = 10, + sort = ["createdAt"], + direction = Sort.Direction.DESC + ) pageable: Pageable, + @RequestParam("keyword") keyword: String?, + ): ResponseEntity> { + return contentUseCase.getRecommendedContent(user.id, keyword, pageable) + .map { it.toResponse() } + .wrapSlice() + .wrapOk() + + } + } diff --git a/adapters/in-web/src/main/kotlin/com/pokit/content/dto/response/ContentsResponse.kt b/adapters/in-web/src/main/kotlin/com/pokit/content/dto/response/ContentsResponse.kt index 2c965558..b25e3327 100644 --- a/adapters/in-web/src/main/kotlin/com/pokit/content/dto/response/ContentsResponse.kt +++ b/adapters/in-web/src/main/kotlin/com/pokit/content/dto/response/ContentsResponse.kt @@ -15,7 +15,8 @@ data class ContentsResponse( val createdAt: String, val isRead: Boolean, val thumbNail: String, - val isFavorite: Boolean + val isFavorite: Boolean, + val keyword: String, ) fun ContentsResult.toResponse(): ContentsResponse { @@ -32,6 +33,7 @@ fun ContentsResult.toResponse(): ContentsResponse { createdAt = this.createdAt.format(formatter), isRead = this.isRead, thumbNail = this.thumbNail, - isFavorite = this.isFavorite + isFavorite = this.isFavorite, + keyword = this.keyword, ) } diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/impl/ContentAdapter.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/impl/ContentAdapter.kt index 13065bf7..49843936 100644 --- a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/impl/ContentAdapter.kt +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/impl/ContentAdapter.kt @@ -15,6 +15,7 @@ import com.pokit.out.persistence.content.persist.ContentRepository import com.pokit.out.persistence.content.persist.QContentEntity.contentEntity import com.pokit.out.persistence.content.persist.toDomain import com.pokit.out.persistence.log.persist.QUserLogEntity.userLogEntity +import com.pokit.user.model.InterestType import com.querydsl.core.Tuple import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.Predicate @@ -214,6 +215,38 @@ class ContentAdapter( contentRepository.updateCategoryId(contentIds, categoryId) } + override fun loadAllByKeyword(userId: Long, searchKeywords: List, pageable: Pageable): Slice { + val contents = queryFactory.select(contentEntity, categoryEntity.name, categoryEntity.keyword) + .from(contentEntity) + .join(categoryEntity).on(contentEntity.categoryId.eq(categoryEntity.id)) + .where( + categoryEntity.openType.eq(OpenType.PUBLIC), + categoryEntity.userId.ne(userId), + categoryEntity.keyword.`in`(searchKeywords), + categoryEntity.deleted.isFalse, + contentEntity.deleted.isFalse + ) + .offset(pageable.offset) + .groupBy(contentEntity) + .limit((pageable.pageSize + 1).toLong()) + .orderBy(getSortOrder(contentEntity.createdAt, "createdAt", pageable)) + .fetch() + + val hasNext = getHasNext(contents, pageable) + + val contentsResults = contents.map { + ContentsResult.of( + it[contentEntity]!!.toDomain(), + it[categoryEntity.name]!!, + 0, + 0, + it[categoryEntity.keyword]!!.kor + ) + } + + return SliceImpl(contentsResults, pageable, hasNext) + } + override fun loadByContentIds(contentIds: List): List = contentRepository.findByIdIn(contentIds) .map { it.toDomain() } diff --git a/application/src/main/kotlin/com/pokit/content/port/in/ContentUseCase.kt b/application/src/main/kotlin/com/pokit/content/port/in/ContentUseCase.kt index 91c3b038..4a64a09f 100644 --- a/application/src/main/kotlin/com/pokit/content/port/in/ContentUseCase.kt +++ b/application/src/main/kotlin/com/pokit/content/port/in/ContentUseCase.kt @@ -48,4 +48,6 @@ interface ContentUseCase { fun categorize(userId: Long, command: CategorizeCommand) fun updateThumbnail(userId: Long, contentId: Long, thumbnail: String): Content + + fun getRecommendedContent(userId: Long, keyword: String?, pageable: Pageable): Slice } diff --git a/application/src/main/kotlin/com/pokit/content/port/out/ContentPort.kt b/application/src/main/kotlin/com/pokit/content/port/out/ContentPort.kt index 281c8573..ae89c726 100644 --- a/application/src/main/kotlin/com/pokit/content/port/out/ContentPort.kt +++ b/application/src/main/kotlin/com/pokit/content/port/out/ContentPort.kt @@ -6,6 +6,7 @@ import com.pokit.content.dto.response.ContentsResult import com.pokit.content.dto.response.SharedContentResult import com.pokit.content.model.Content import com.pokit.content.model.ContentWithUser +import com.pokit.user.model.InterestType import org.springframework.data.domain.Pageable import org.springframework.data.domain.Slice @@ -52,4 +53,6 @@ interface ContentPort { fun loadAllByUserIdAndContentIds(userId: Long, contentIds: List): List fun updateCategoryId(contents: List, categoryId: Long) + + fun loadAllByKeyword(userId: Long, searchKeywords: List, pageable: Pageable): Slice } diff --git a/application/src/main/kotlin/com/pokit/content/port/service/ContentService.kt b/application/src/main/kotlin/com/pokit/content/port/service/ContentService.kt index 1a783c3f..cbed0d6b 100644 --- a/application/src/main/kotlin/com/pokit/content/port/service/ContentService.kt +++ b/application/src/main/kotlin/com/pokit/content/port/service/ContentService.kt @@ -26,7 +26,9 @@ import com.pokit.content.port.out.ContentPort import com.pokit.log.model.LogType import com.pokit.log.model.UserLog import com.pokit.log.port.out.UserLogPort +import com.pokit.user.model.InterestType import com.pokit.user.model.User +import com.pokit.user.port.out.InterestPort import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Pageable import org.springframework.data.domain.Slice @@ -42,7 +44,8 @@ class ContentService( private val categoryPort: CategoryPort, private val userLogPort: UserLogPort, private val publisher: ApplicationEventPublisher, - private val contentCountPort: ContentCountPort + private val contentCountPort: ContentCountPort, + private val interestPort: InterestPort, ) : ContentUseCase { companion object { private const val MIN_CONTENT_COUNT = 3 @@ -196,6 +199,15 @@ class ContentService( return contentPort.persist(content) } + override fun getRecommendedContent(userId: Long, keyword: String?, pageable: Pageable): Slice { + val searchKeyword = keyword?.let { + listOf(InterestType.of(it)) + } ?: interestPort.loadByUserId(userId).map { + it.interestType + } + return contentPort.loadAllByKeyword(userId, searchKeyword, pageable) + } + private fun verifyContent(userId: Long, contentId: Long): Content { return contentPort.loadByUserIdAndId(userId, contentId) ?: throw NotFoundCustomException(ContentErrorCode.NOT_FOUND_CONTENT) diff --git a/domain/src/main/kotlin/com/pokit/content/dto/response/ContentsResult.kt b/domain/src/main/kotlin/com/pokit/content/dto/response/ContentsResult.kt index a08a0e5b..5f2eab2b 100644 --- a/domain/src/main/kotlin/com/pokit/content/dto/response/ContentsResult.kt +++ b/domain/src/main/kotlin/com/pokit/content/dto/response/ContentsResult.kt @@ -16,10 +16,17 @@ data class ContentsResult( val createdAt: LocalDateTime, val isRead: Boolean, val thumbNail: String, - val isFavorite: Boolean + val isFavorite: Boolean, + val keyword: String ) { companion object { - fun of(content: Content, categoryName: String, isRead: Long, isFavorite: Long): ContentsResult { + fun of( + content: Content, + categoryName: String, + isRead: Long, + isFavorite: Long, + keyword: String = "default" + ): ContentsResult { return ContentsResult( contentId = content.id, category = RemindCategory(content.categoryId, categoryName), @@ -31,7 +38,8 @@ data class ContentsResult( createdAt = content.createdAt, isRead = isRead > 0, thumbNail = content.thumbNail ?: ContentDefault.THUMB_NAIL, - isFavorite = isFavorite > 0 + isFavorite = isFavorite > 0, + keyword = keyword ) } }