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 5c8c5412..1c3f3815 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 @@ -4,6 +4,7 @@ import com.pokit.auth.config.ErrorOperation import com.pokit.auth.model.PrincipalUser import com.pokit.auth.model.toDomain import com.pokit.category.exception.CategoryErrorCode +import com.pokit.category.model.CategoryStatus import com.pokit.common.dto.SliceResponseDto import com.pokit.common.wrapper.ResponseWrapper.wrapOk import com.pokit.common.wrapper.ResponseWrapper.wrapSlice @@ -115,6 +116,27 @@ class ContentController( .wrapOk() } + @GetMapping("/uncategorized") + @Operation(summary = "미분류 카테고리 컨텐츠 조회") + fun getUncategorizedContents( + @AuthenticationPrincipal user: PrincipalUser, + @PageableDefault( + page = 0, + size = 10, + sort = ["createdAt"], + direction = Sort.Direction.DESC + ) pageable: Pageable, + ): ResponseEntity> { + return contentUseCase.getContentsByCategoryName( + user.id, + CategoryStatus.UNCATEGORIZED.name, + pageable + ) + .map { it.toResponse() } + .wrapSlice() + .wrapOk() + } + @PostMapping("/{contentId}") @Operation(summary = "컨텐츠 상세조회 API") fun getContent( 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 d2b8cc2e..e13b2c96 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 @@ -1,11 +1,11 @@ package com.pokit.content.dto.response +import com.pokit.category.model.RemindCategory import java.time.format.DateTimeFormatter data class ContentsResponse( val contentId: Long, - val categoryId: Long, - val categoryName: String, + val category: RemindCategory, val data: String, val domain: String, val title: String, @@ -21,8 +21,7 @@ fun ContentsResult.toResponse(): ContentsResponse { return ContentsResponse( contentId = this.contentId, - categoryId = this.categoryId, - categoryName = this.categoryName, + category = this.category, data = this.data, domain = this.domain, title = this.title, 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 3bf2fa60..cb367399 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 @@ -1,5 +1,6 @@ package com.pokit.out.persistence.content.impl +import com.pokit.category.model.CategoryStatus import com.pokit.content.dto.response.ContentsResult import com.pokit.content.dto.request.ContentSearchCondition import com.pokit.content.model.Content @@ -13,6 +14,7 @@ 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.querydsl.core.Tuple +import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.Predicate import com.querydsl.core.types.dsl.DateTimePath import com.querydsl.jpa.impl.JPAQuery @@ -57,7 +59,6 @@ class ContentAdapter( condition: ContentSearchCondition, pageable: Pageable, ): Slice { - var hasNext = false val order = pageable.sort.getOrderFor("createdAt") val query = queryFactory.select(contentEntity, categoryEntity.name, userLogEntity.count()) @@ -75,17 +76,14 @@ class ContentAdapter( dateBetween(condition.startDate, condition.endDate), categoryIn(condition.categoryIds) ) + .offset(pageable.offset) .groupBy(contentEntity) .orderBy(getSort(contentEntity.createdAt, order!!)) .limit(pageable.pageSize + 1L) val contentEntityList = query.fetch() - - if (contentEntityList.size > pageable.pageSize) { - hasNext = true - contentEntityList.removeAt(contentEntityList.size - 1) - } + val hasNext = getHasNext(contentEntityList, pageable) val contents = contentEntityList.map { ContentsResult.of( @@ -98,17 +96,57 @@ class ContentAdapter( return SliceImpl(contents, pageable, hasNext) } + override fun loadByUserIdAndCategoryName(userId: Long, categoryName: String, pageable: Pageable): Slice { + val contents = queryFactory.select(contentEntity, categoryEntity.name, userLogEntity.count()) + .from(contentEntity) + .leftJoin(userLogEntity).on(userLogEntity.contentId.eq(contentEntity.id)) + .join(categoryEntity).on(categoryEntity.id.eq(contentEntity.categoryId)) + .where( + categoryEntity.userId.eq(userId), + categoryEntity.name.eq(categoryName), + 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 contentResults = contents.map { + ContentsResult.of( + it[contentEntity]!!.toDomain(), + CategoryStatus.resolveDisplayName(it[categoryEntity.name]!!), + it[userLogEntity.count()]!! + ) + } + + return SliceImpl(contentResults, pageable, hasNext) + } + override fun loadByContentIds(contentIds: List): List = contentRepository.findByIdIn(contentIds) .map { it.toDomain() } + private fun getHasNext( + contentEntityList: MutableList, + pageable: Pageable, + ): Boolean { + var hasNext = false + if (contentEntityList.size > pageable.pageSize) { + hasNext = true + contentEntityList.removeAt(contentEntityList.size - 1) + } + return hasNext + } + private fun isUnread(read: Boolean?): Predicate? { return read?.let { userLogEntity.id.isNull.or(userLogEntity.type.ne(LogType.READ)) } } - private fun categoryIn(categoryIds: List?): Predicate? { if (categoryIds.isNullOrEmpty()) { return null @@ -154,4 +192,15 @@ class ContentAdapter( if (order.isDescending) property.desc() else property.asc() + private fun getSortOrder(property: DateTimePath, sortField: String, pageable: Pageable): OrderSpecifier<*> { + val order = pageable.sort.getOrderFor(sortField) + ?.direction + ?: Sort.Direction.ASC + + return if (order.isAscending) { + property.asc() + } else { + property.desc() + } + } } diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/persist/ContentEntity.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/persist/ContentEntity.kt index f2ca65d5..976abf35 100644 --- a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/persist/ContentEntity.kt +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/content/persist/ContentEntity.kt @@ -32,6 +32,9 @@ class ContentEntity( @Column(name = "alert_yn") val alertYn: String, + @Column(name = "domain") + val domain: String, + @Column(name = "is_deleted") var deleted: Boolean = false ) : BaseEntity() { @@ -47,7 +50,8 @@ class ContentEntity( data = content.data, title = content.title, memo = content.memo, - alertYn = content.alertYn + alertYn = content.alertYn, + domain = content.domain ) } } @@ -60,5 +64,6 @@ fun ContentEntity.toDomain() = Content( title = this.title, memo = this.memo, alertYn = this.alertYn, + domain = this.domain, createdAt = this.createdAt ) 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 4f0e2106..e4151726 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 @@ -28,6 +28,8 @@ interface ContentUseCase { pageable: Pageable, ): Slice + fun getContentsByCategoryName(userId: Long, categoryName: String, pageable: Pageable): Slice + fun getContent(userId: Long, contentId: Long): GetContentResponse fun getBookmarkContents(userId: Long, 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 76c2189e..09ff7a89 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 @@ -23,6 +23,12 @@ interface ContentPort { pageable: Pageable, ): Slice + fun loadByUserIdAndCategoryName( + userId: Long, + categoryName: String, + pageable: Pageable, + ): Slice + fun deleteByUserId(userId: Long) fun loadByContentIds(contentIds: List): List 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 47c610d3..f93fa6a5 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 @@ -95,6 +95,10 @@ class ContentService( return contents } + override fun getContentsByCategoryName(userId: Long, categoryName: String, pageable: Pageable): Slice = + contentPort.loadByUserIdAndCategoryName(userId, categoryName, pageable) + + @Transactional override fun getContent(userId: Long, contentId: Long): GetContentResponse { val userLog = UserLog( diff --git a/domain/src/main/kotlin/com/pokit/category/model/CategoryStatus.kt b/domain/src/main/kotlin/com/pokit/category/model/CategoryStatus.kt index 49ffe854..5831765a 100644 --- a/domain/src/main/kotlin/com/pokit/category/model/CategoryStatus.kt +++ b/domain/src/main/kotlin/com/pokit/category/model/CategoryStatus.kt @@ -1,5 +1,16 @@ package com.pokit.category.model -enum class CategoryStatus(val displayName: String) { +enum class CategoryStatus( + val displayName: String +) { UNCATEGORIZED("미분류") + ; + + companion object { + fun resolveDisplayName(status: String): String = + entries.find { it.name == status } + ?.displayName + ?: status + + } } 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 5f78136f..f11071d9 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 @@ -1,12 +1,12 @@ package com.pokit.content.dto.response +import com.pokit.category.model.RemindCategory import com.pokit.content.model.Content import java.time.LocalDateTime data class ContentsResult( val contentId: Long, - val categoryId: Long, - val categoryName: String, + val category: RemindCategory, val data: String, val domain: String, val title: String, @@ -20,8 +20,7 @@ data class ContentsResult( fun of(content: Content, categoryName: String, isRead: Long): ContentsResult { return ContentsResult( contentId = content.id, - categoryId = content.categoryId, - categoryName = categoryName, + category = RemindCategory(content.categoryId, categoryName), data = content.data, domain = content.domain, title = content.title, diff --git a/domain/src/main/kotlin/com/pokit/content/dto/response/RemindContentResult.kt b/domain/src/main/kotlin/com/pokit/content/dto/response/RemindContentResult.kt index c0c27190..fa1ab4cd 100644 --- a/domain/src/main/kotlin/com/pokit/content/dto/response/RemindContentResult.kt +++ b/domain/src/main/kotlin/com/pokit/content/dto/response/RemindContentResult.kt @@ -30,7 +30,7 @@ fun Content.toRemindContentResult(isRead: Boolean, category: RemindCategory): Re fun ContentsResult.toRemindContentResult(): RemindContentResult { return RemindContentResult( contentId = this.contentId, - category = RemindCategory(this.categoryId, this.categoryName), + category = this.category, data = this.data, title = this.title, createdAt = this.createdAt,