diff --git a/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt index 85868587..2d273923 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt @@ -27,10 +27,8 @@ import java.util.UUID @UseCase @Transactional(readOnly = true) class BookUseCase( - @Qualifier(BookQueryServiceQualifier.ALADIN) private val bookQueryService: BookQueryService, - private val userAuthService: UserAuthService, private val userBookService: UserBookService, private val bookManagementService: BookManagementService diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt index 0c4e4a9f..749f9e75 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt @@ -3,7 +3,6 @@ package org.yapp.apis.readingrecord.usecase import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.transaction.annotation.Transactional -import org.springframework.beans.factory.annotation.Qualifier import org.yapp.apis.auth.service.UserAuthService import org.yapp.apis.book.service.UserBookService import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest @@ -11,10 +10,7 @@ import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse import org.yapp.apis.readingrecord.service.ReadingRecordService import org.yapp.domain.readingrecord.ReadingRecordSortType import org.yapp.globalutils.annotation.UseCase -import org.yapp.apis.book.constant.BookQueryServiceQualifier -import org.yapp.apis.book.service.BookQueryService -import org.yapp.domain.book.BookDomainService -import java.util.UUID +import java.util.* @UseCase @Transactional(readOnly = true) @@ -22,9 +18,6 @@ class ReadingRecordUseCase( private val readingRecordService: ReadingRecordService, private val userAuthService: UserAuthService, private val userBookService: UserBookService, - @Qualifier(BookQueryServiceQualifier.ALADIN) - private val bookQueryService: BookQueryService, - private val bookDomainService: BookDomainService ) { @Transactional fun createReadingRecord( diff --git a/apis/src/main/kotlin/org/yapp/apis/seed/controller/SeedController.kt b/apis/src/main/kotlin/org/yapp/apis/seed/controller/SeedController.kt new file mode 100644 index 00000000..4cafbbc9 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/seed/controller/SeedController.kt @@ -0,0 +1,23 @@ +package org.yapp.apis.seed.controller + +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.yapp.apis.seed.dto.response.SeedStatsResponse +import org.yapp.apis.seed.usecase.SeedUseCase +import java.util.* + +@RestController +@RequestMapping("/api/v1/seeds") +class SeedController( + private val seedUseCase: SeedUseCase +) : SeedControllerApi { + + override fun getSeedStats( + @AuthenticationPrincipal userId: UUID + ): ResponseEntity { + val stats = seedUseCase.getSeedStats(userId) + return ResponseEntity.ok(stats) + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/seed/controller/SeedControllerApi.kt b/apis/src/main/kotlin/org/yapp/apis/seed/controller/SeedControllerApi.kt new file mode 100644 index 00000000..e0d2c4f1 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/seed/controller/SeedControllerApi.kt @@ -0,0 +1,41 @@ +package org.yapp.apis.seed.controller + +import io.swagger.v3.oas.annotations.Operation +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.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.yapp.apis.seed.dto.response.SeedStatsResponse +import org.yapp.globalutils.exception.ErrorResponse +import java.util.* + +@Tag(name = "Seed", description = "씨앗(감정 태그) 관련 API") +interface SeedControllerApi { + + @Operation( + summary = "씨앗 통계 조회", + description = "사용자가 모은 감정 태그별 씨앗 개수를 조회합니다." + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "씨앗 통계 조회 성공", + content = [Content(schema = Schema(implementation = SeedStatsResponse::class))] + ), + ApiResponse( + responseCode = "404", + description = "사용자를 찾을 수 없음", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ) + ] + ) + @GetMapping("/stats") + fun getSeedStats( + @AuthenticationPrincipal userId: UUID + ): ResponseEntity +} \ No newline at end of file diff --git a/apis/src/main/kotlin/org/yapp/apis/seed/dto/response/SeedStatsResponse.kt b/apis/src/main/kotlin/org/yapp/apis/seed/dto/response/SeedStatsResponse.kt new file mode 100644 index 00000000..63f9df3c --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/seed/dto/response/SeedStatsResponse.kt @@ -0,0 +1,31 @@ +package org.yapp.apis.seed.dto.response + +import org.yapp.domain.readingrecordtag.vo.TagStatsVO +import org.yapp.globalutils.tag.GeneralEmotionTagCategory + +data class SeedStatsResponse private constructor( + val categories: List +) { + data class SeedCategoryStats private constructor( + val name: String, + val count: Int + ) { + companion object { + fun of(name: String, count: Int): SeedCategoryStats { + return SeedCategoryStats(name, count) + } + } + } + + companion object { + fun from(tagStatsVO: TagStatsVO): SeedStatsResponse { + val categories = GeneralEmotionTagCategory.entries.map { category -> + SeedCategoryStats.of( + name = category.displayName, + count = tagStatsVO.categoryStats.getOrDefault(category, 0) + ) + } + return SeedStatsResponse(categories) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/seed/service/SeedService.kt b/apis/src/main/kotlin/org/yapp/apis/seed/service/SeedService.kt new file mode 100644 index 00000000..aab6358e --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/seed/service/SeedService.kt @@ -0,0 +1,21 @@ +package org.yapp.apis.seed.service + +import org.springframework.stereotype.Service +import org.yapp.apis.seed.dto.response.SeedStatsResponse +import org.yapp.domain.readingrecordtag.ReadingRecordTagDomainService +import org.yapp.globalutils.tag.GeneralEmotionTagCategory +import java.util.* + +@Service +class SeedService( + private val readingRecordTagDomainService: ReadingRecordTagDomainService +) { + fun getSeedStatsByUserId(userId: UUID): SeedStatsResponse { + val tagStatsVO = readingRecordTagDomainService.countTagsByUserIdAndCategories( + userId = userId, + categories = GeneralEmotionTagCategory.entries.map { it.displayName } + ) + + return SeedStatsResponse.from(tagStatsVO) + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/seed/usecase/SeedUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/seed/usecase/SeedUseCase.kt new file mode 100644 index 00000000..fb78ae60 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/seed/usecase/SeedUseCase.kt @@ -0,0 +1,20 @@ +package org.yapp.apis.seed.usecase + +import org.springframework.transaction.annotation.Transactional +import org.yapp.apis.auth.service.UserAuthService +import org.yapp.apis.seed.dto.response.SeedStatsResponse +import org.yapp.apis.seed.service.SeedService +import org.yapp.globalutils.annotation.UseCase +import java.util.* + +@UseCase +@Transactional(readOnly = true) +class SeedUseCase( + private val userAuthService: UserAuthService, + private val seedService: SeedService +) { + fun getSeedStats(userId: UUID): SeedStatsResponse { + userAuthService.validateUserExists(userId) + return seedService.getSeedStatsByUserId(userId) + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagDomainService.kt new file mode 100644 index 00000000..9f71507c --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagDomainService.kt @@ -0,0 +1,15 @@ +package org.yapp.domain.readingrecordtag + +import org.yapp.domain.readingrecordtag.vo.TagStatsVO +import org.yapp.globalutils.annotation.DomainService +import java.util.* + +@DomainService +class ReadingRecordTagDomainService( + private val readingRecordTagRepository: ReadingRecordTagRepository +) { + fun countTagsByUserIdAndCategories(userId: UUID, categories: List): TagStatsVO { + val categoryStats = readingRecordTagRepository.countTagsByUserIdAndCategories(userId, categories) + return TagStatsVO.newInstance(categoryStats) + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt index 0adf7682..985efb22 100644 --- a/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt @@ -5,4 +5,5 @@ import java.util.UUID interface ReadingRecordTagRepository { fun saveAll(readingRecordTags: List): List fun findByReadingRecordId(readingRecordId: UUID): List + fun countTagsByUserIdAndCategories(userId: UUID, categories: List): Map } diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/vo/TagStatsVO.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/vo/TagStatsVO.kt new file mode 100644 index 00000000..8c82f679 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/vo/TagStatsVO.kt @@ -0,0 +1,28 @@ +package org.yapp.domain.readingrecordtag.vo + +import org.yapp.globalutils.tag.GeneralEmotionTagCategory +import java.util.EnumMap + +data class TagStatsVO private constructor( + val categoryStats: EnumMap +) { + init { + categoryStats.values.forEach { count -> + require(count >= 0) { "태그 개수는 0 이상이어야 합니다." } + } + } + + companion object { + fun newInstance(categoryStats: Map): TagStatsVO { + val enumStats = EnumMap(GeneralEmotionTagCategory::class.java) + + categoryStats.forEach { (displayName, count) -> + GeneralEmotionTagCategory.fromDisplayName(displayName)?.let { enumCategory -> + enumStats[enumCategory] = count + } + } + + return TagStatsVO(enumStats) + } + } +} diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/tag/GeneralEmotionTagCategory.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/tag/GeneralEmotionTagCategory.kt new file mode 100644 index 00000000..2714ed7b --- /dev/null +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/tag/GeneralEmotionTagCategory.kt @@ -0,0 +1,18 @@ +package org.yapp.globalutils.tag + +enum class GeneralEmotionTagCategory( + val displayName: String +) { + WARMTH("따뜻함"), + JOY("즐거움"), + TENSION("긴장감"), + SADNESS("슬픔"); + + companion object { + private val BY_DISPLAY_NAME = entries.associateBy { it.displayName } + + fun fromDisplayName(displayName: String): GeneralEmotionTagCategory? { + return BY_DISPLAY_NAME[displayName] + } + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagQuerydslRepository.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagQuerydslRepository.kt new file mode 100644 index 00000000..f22bfb81 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagQuerydslRepository.kt @@ -0,0 +1,7 @@ +package org.yapp.infra.readingrecordtag.repository + +import java.util.* + +interface JpaReadingRecordTagQuerydslRepository { + fun countTagsByUserIdAndCategories(userId: UUID, categories: List): Map +} \ No newline at end of file diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt index 18045acd..b316c33d 100644 --- a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt @@ -4,6 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository import org.yapp.infra.readingrecordtag.entity.ReadingRecordTagEntity import java.util.UUID -interface JpaReadingRecordTagRepository : JpaRepository { +interface JpaReadingRecordTagRepository : JpaRepository, JpaReadingRecordTagQuerydslRepository { fun findByReadingRecordId(readingRecordId: UUID): List } diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/JpaReadingRecordTagQuerydslRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/JpaReadingRecordTagQuerydslRepositoryImpl.kt new file mode 100644 index 00000000..eb432296 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/JpaReadingRecordTagQuerydslRepositoryImpl.kt @@ -0,0 +1,68 @@ +package org.yapp.infra.readingrecordtag.repository.impl + +import com.querydsl.core.types.dsl.BooleanExpression +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.stereotype.Repository +import org.yapp.infra.readingrecord.entity.QReadingRecordEntity +import org.yapp.infra.readingrecordtag.entity.QReadingRecordTagEntity +import org.yapp.infra.readingrecordtag.repository.JpaReadingRecordTagQuerydslRepository +import org.yapp.infra.tag.entity.QTagEntity +import org.yapp.infra.userbook.entity.QUserBookEntity +import java.util.* + +@Repository +class JpaReadingRecordTagQuerydslRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : JpaReadingRecordTagQuerydslRepository { + + private val readingRecordTag = QReadingRecordTagEntity.readingRecordTagEntity + private val readingRecord = QReadingRecordEntity.readingRecordEntity + private val userBook = QUserBookEntity.userBookEntity + private val tag = QTagEntity.tagEntity + + override fun countTagsByUserIdAndCategories( + userId: UUID, + categories: List + ): Map { + if (categories.isEmpty()) { + return emptyMap() + } + + val results = queryFactory + .select(tag.name, readingRecordTag.count()) + .from(readingRecordTag) + .join(readingRecord).on(readingRecordTag.readingRecordId.eq(readingRecord.id)) + .join(userBook).on(readingRecord.userBookId.eq(userBook.id)) + .join(tag).on(readingRecordTag.tagId.eq(tag.id)) + .where( + userBook.userIdEq(userId), + readingRecord.isNotDeleted(), + readingRecordTag.isNotDeleted(), + tag.nameIn(categories) + ) + .groupBy(tag.name) + .fetch() + + return results.associate { tuple -> + val tagName = tuple[tag.name] ?: "" + val count = tuple[readingRecordTag.count()]?.toInt() ?: 0 + tagName to count + } + } + + private fun QUserBookEntity.userIdEq(userId: UUID): BooleanExpression { + return this.userId.eq(userId) + } + + private fun QReadingRecordEntity.isNotDeleted(): BooleanExpression { + return this.deletedAt.isNull + } + + private fun QReadingRecordTagEntity.isNotDeleted(): BooleanExpression { + return this.deletedAt.isNull + } + + private fun QTagEntity.nameIn(categories: List): BooleanExpression { + return this.name.`in`(categories) + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt similarity index 63% rename from infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt rename to infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt index 58e29896..bad43986 100644 --- a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt @@ -1,9 +1,11 @@ -package org.yapp.infra.readingrecordtag.repository +package org.yapp.infra.readingrecordtag.repository.impl import org.springframework.stereotype.Repository import org.yapp.domain.readingrecordtag.ReadingRecordTag import org.yapp.domain.readingrecordtag.ReadingRecordTagRepository import org.yapp.infra.readingrecordtag.entity.ReadingRecordTagEntity +import org.yapp.infra.readingrecordtag.repository.JpaReadingRecordTagRepository +import java.util.* @Repository class ReadingRecordTagRepositoryImpl( @@ -14,7 +16,11 @@ class ReadingRecordTagRepositoryImpl( return jpaReadingRecordTagRepository.saveAll(entities).map { it.toDomain() } } - override fun findByReadingRecordId(readingRecordId: java.util.UUID): List { + override fun findByReadingRecordId(readingRecordId: UUID): List { return jpaReadingRecordTagRepository.findByReadingRecordId(readingRecordId).map { it.toDomain() } } -} \ No newline at end of file + + override fun countTagsByUserIdAndCategories(userId: UUID, categories: List): Map { + return jpaReadingRecordTagRepository.countTagsByUserIdAndCategories(userId, categories) + } +}