Skip to content

Commit fa125d1

Browse files
authored
feat: 사용자별 감정 태그 통계 조회 기능 구현 (#70)
* [BOOK-204] feat: apis - 씨앗 통계 조회 API 구현 * SeedController 및 SeedControllerApi 클래스 추가 * 사용자 인증을 통한 씨앗 통계 조회 기능 구현 * Swagger 문서화 주석 추가 * [BOOK-204] chore: apis - BookUseCase 클래스에서 불필요한 주석 제거 및 코드 정리 * [BOOK-204] chore: apis - ReadingRecordUseCase에서 사용하지 않는 import문 제거 및 코드 정리 * [BOOK-204] feat: apis - SeedUseCase 클래스 추가 및 씨앗 통계 조회 기능 구현 * [BOOK-204] feat: domain, apis - SeedService 및 ReadingRecordTagDomainService 클래스 추가 및 태그 통계 기능 구현 * [BOOK-204] feat: domain, infra - ReadingRecordTagRepository에 태그 통계 기능 추가 및 관련 구현체 추가 * [BOOK-204] feat: global-utils - 일반 감정 태그 카테고리 열거형 추가 * [BOOK-204] feat: apis - 씨앗 통계 응답 DTO 추가
1 parent fcafa33 commit fa125d1

File tree

15 files changed

+284
-14
lines changed

15 files changed

+284
-14
lines changed

apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,8 @@ import java.util.UUID
2727
@UseCase
2828
@Transactional(readOnly = true)
2929
class BookUseCase(
30-
3130
@Qualifier(BookQueryServiceQualifier.ALADIN)
3231
private val bookQueryService: BookQueryService,
33-
3432
private val userAuthService: UserAuthService,
3533
private val userBookService: UserBookService,
3634
private val bookManagementService: BookManagementService

apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,21 @@ package org.yapp.apis.readingrecord.usecase
33
import org.springframework.data.domain.Page
44
import org.springframework.data.domain.Pageable
55
import org.springframework.transaction.annotation.Transactional
6-
import org.springframework.beans.factory.annotation.Qualifier
76
import org.yapp.apis.auth.service.UserAuthService
87
import org.yapp.apis.book.service.UserBookService
98
import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest
109
import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse
1110
import org.yapp.apis.readingrecord.service.ReadingRecordService
1211
import org.yapp.domain.readingrecord.ReadingRecordSortType
1312
import org.yapp.globalutils.annotation.UseCase
14-
import org.yapp.apis.book.constant.BookQueryServiceQualifier
15-
import org.yapp.apis.book.service.BookQueryService
16-
import org.yapp.domain.book.BookDomainService
17-
import java.util.UUID
13+
import java.util.*
1814

1915
@UseCase
2016
@Transactional(readOnly = true)
2117
class ReadingRecordUseCase(
2218
private val readingRecordService: ReadingRecordService,
2319
private val userAuthService: UserAuthService,
2420
private val userBookService: UserBookService,
25-
@Qualifier(BookQueryServiceQualifier.ALADIN)
26-
private val bookQueryService: BookQueryService,
27-
private val bookDomainService: BookDomainService
2821
) {
2922
@Transactional
3023
fun createReadingRecord(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.yapp.apis.seed.controller
2+
3+
import org.springframework.http.ResponseEntity
4+
import org.springframework.security.core.annotation.AuthenticationPrincipal
5+
import org.springframework.web.bind.annotation.RequestMapping
6+
import org.springframework.web.bind.annotation.RestController
7+
import org.yapp.apis.seed.dto.response.SeedStatsResponse
8+
import org.yapp.apis.seed.usecase.SeedUseCase
9+
import java.util.*
10+
11+
@RestController
12+
@RequestMapping("/api/v1/seeds")
13+
class SeedController(
14+
private val seedUseCase: SeedUseCase
15+
) : SeedControllerApi {
16+
17+
override fun getSeedStats(
18+
@AuthenticationPrincipal userId: UUID
19+
): ResponseEntity<SeedStatsResponse> {
20+
val stats = seedUseCase.getSeedStats(userId)
21+
return ResponseEntity.ok(stats)
22+
}
23+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.yapp.apis.seed.controller
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.media.Content
5+
import io.swagger.v3.oas.annotations.media.Schema
6+
import io.swagger.v3.oas.annotations.responses.ApiResponse
7+
import io.swagger.v3.oas.annotations.responses.ApiResponses
8+
import io.swagger.v3.oas.annotations.tags.Tag
9+
import org.springframework.http.ResponseEntity
10+
import org.springframework.security.core.annotation.AuthenticationPrincipal
11+
import org.springframework.web.bind.annotation.GetMapping
12+
import org.yapp.apis.seed.dto.response.SeedStatsResponse
13+
import org.yapp.globalutils.exception.ErrorResponse
14+
import java.util.*
15+
16+
@Tag(name = "Seed", description = "씨앗(감정 태그) 관련 API")
17+
interface SeedControllerApi {
18+
19+
@Operation(
20+
summary = "씨앗 통계 조회",
21+
description = "사용자가 모은 감정 태그별 씨앗 개수를 조회합니다."
22+
)
23+
@ApiResponses(
24+
value = [
25+
ApiResponse(
26+
responseCode = "200",
27+
description = "씨앗 통계 조회 성공",
28+
content = [Content(schema = Schema(implementation = SeedStatsResponse::class))]
29+
),
30+
ApiResponse(
31+
responseCode = "404",
32+
description = "사용자를 찾을 수 없음",
33+
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
34+
)
35+
]
36+
)
37+
@GetMapping("/stats")
38+
fun getSeedStats(
39+
@AuthenticationPrincipal userId: UUID
40+
): ResponseEntity<SeedStatsResponse>
41+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.yapp.apis.seed.dto.response
2+
3+
import org.yapp.domain.readingrecordtag.vo.TagStatsVO
4+
import org.yapp.globalutils.tag.GeneralEmotionTagCategory
5+
6+
data class SeedStatsResponse private constructor(
7+
val categories: List<SeedCategoryStats>
8+
) {
9+
data class SeedCategoryStats private constructor(
10+
val name: String,
11+
val count: Int
12+
) {
13+
companion object {
14+
fun of(name: String, count: Int): SeedCategoryStats {
15+
return SeedCategoryStats(name, count)
16+
}
17+
}
18+
}
19+
20+
companion object {
21+
fun from(tagStatsVO: TagStatsVO): SeedStatsResponse {
22+
val categories = GeneralEmotionTagCategory.entries.map { category ->
23+
SeedCategoryStats.of(
24+
name = category.displayName,
25+
count = tagStatsVO.categoryStats.getOrDefault(category, 0)
26+
)
27+
}
28+
return SeedStatsResponse(categories)
29+
}
30+
}
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.yapp.apis.seed.service
2+
3+
import org.springframework.stereotype.Service
4+
import org.yapp.apis.seed.dto.response.SeedStatsResponse
5+
import org.yapp.domain.readingrecordtag.ReadingRecordTagDomainService
6+
import org.yapp.globalutils.tag.GeneralEmotionTagCategory
7+
import java.util.*
8+
9+
@Service
10+
class SeedService(
11+
private val readingRecordTagDomainService: ReadingRecordTagDomainService
12+
) {
13+
fun getSeedStatsByUserId(userId: UUID): SeedStatsResponse {
14+
val tagStatsVO = readingRecordTagDomainService.countTagsByUserIdAndCategories(
15+
userId = userId,
16+
categories = GeneralEmotionTagCategory.entries.map { it.displayName }
17+
)
18+
19+
return SeedStatsResponse.from(tagStatsVO)
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.yapp.apis.seed.usecase
2+
3+
import org.springframework.transaction.annotation.Transactional
4+
import org.yapp.apis.auth.service.UserAuthService
5+
import org.yapp.apis.seed.dto.response.SeedStatsResponse
6+
import org.yapp.apis.seed.service.SeedService
7+
import org.yapp.globalutils.annotation.UseCase
8+
import java.util.*
9+
10+
@UseCase
11+
@Transactional(readOnly = true)
12+
class SeedUseCase(
13+
private val userAuthService: UserAuthService,
14+
private val seedService: SeedService
15+
) {
16+
fun getSeedStats(userId: UUID): SeedStatsResponse {
17+
userAuthService.validateUserExists(userId)
18+
return seedService.getSeedStatsByUserId(userId)
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.yapp.domain.readingrecordtag
2+
3+
import org.yapp.domain.readingrecordtag.vo.TagStatsVO
4+
import org.yapp.globalutils.annotation.DomainService
5+
import java.util.*
6+
7+
@DomainService
8+
class ReadingRecordTagDomainService(
9+
private val readingRecordTagRepository: ReadingRecordTagRepository
10+
) {
11+
fun countTagsByUserIdAndCategories(userId: UUID, categories: List<String>): TagStatsVO {
12+
val categoryStats = readingRecordTagRepository.countTagsByUserIdAndCategories(userId, categories)
13+
return TagStatsVO.newInstance(categoryStats)
14+
}
15+
}

domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ import java.util.UUID
55
interface ReadingRecordTagRepository {
66
fun saveAll(readingRecordTags: List<ReadingRecordTag>): List<ReadingRecordTag>
77
fun findByReadingRecordId(readingRecordId: UUID): List<ReadingRecordTag>
8+
fun countTagsByUserIdAndCategories(userId: UUID, categories: List<String>): Map<String, Int>
89
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.yapp.domain.readingrecordtag.vo
2+
3+
import org.yapp.globalutils.tag.GeneralEmotionTagCategory
4+
import java.util.EnumMap
5+
6+
data class TagStatsVO private constructor(
7+
val categoryStats: EnumMap<GeneralEmotionTagCategory, Int>
8+
) {
9+
init {
10+
categoryStats.values.forEach { count ->
11+
require(count >= 0) { "태그 개수는 0 이상이어야 합니다." }
12+
}
13+
}
14+
15+
companion object {
16+
fun newInstance(categoryStats: Map<String, Int>): TagStatsVO {
17+
val enumStats = EnumMap<GeneralEmotionTagCategory, Int>(GeneralEmotionTagCategory::class.java)
18+
19+
categoryStats.forEach { (displayName, count) ->
20+
GeneralEmotionTagCategory.fromDisplayName(displayName)?.let { enumCategory ->
21+
enumStats[enumCategory] = count
22+
}
23+
}
24+
25+
return TagStatsVO(enumStats)
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)