Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f1586b5
[BOOK-207] refactor: domain, apis - BookDetailRequest 및 AladinApiHelp…
move-hoon Aug 1, 2025
4b275fc
[BOOK-207] refactor: apis - AladinBookQueryService에서 Companion 객체 사용 …
move-hoon Aug 1, 2025
5fa8152
[BOOK-207] fix: apis, infra - AladinBookLookupRequest 패키지 변경 및 import 정리
move-hoon Aug 1, 2025
da20c14
[BOOK-207] chore: apis - BookDetailResponse에서 불필요한 주석 제거 및 코드 정리
move-hoon Aug 1, 2025
698cbc2
[BOOK-207] chore: apis - Swagger UI 경로를 /swagger-ui로 변경
move-hoon Aug 3, 2025
e26349a
[BOOK-207] feat: apis - Aladin API Manager 클래스 추가
move-hoon Aug 3, 2025
cec68cd
[BOOK-207] feat: apis - AladinBookQueryService 클래스 추가 및 BookQueryServ…
move-hoon Aug 3, 2025
e87dd78
[BOOK-207] feat: apis, infra - AladinBookLookupRequest 및 AladinBookSe…
move-hoon Aug 3, 2025
e5b23c0
[BOOK-207] refactor: apis - getBookDetail 메서드에 userId 파라미터 추가 및 관련 검증…
move-hoon Aug 3, 2025
6bb691c
[BOOK-207] feat: apis - BookUseCase에 AladinBookQueryService 주입을 위한 Qu…
move-hoon Aug 3, 2025
491b538
[BOOK-207] refactor: apis - BookDetailRequest 및 BookSearchRequest의 필드…
move-hoon Aug 3, 2025
76193e2
[BOOK-207] refactor: apis - Aladin API 관련 예외 처리 추가 및 AladinApiManager…
move-hoon Aug 3, 2025
08bb18c
[BOOK-207] refactor: domain, infra - UserBookRepository 및 관련 클래스에서 fi…
move-hoon Aug 3, 2025
d8d0593
[BOOK-207] refactor: global-utils - RegexUtils.kt에서 이메일 및 프로필 이미지 URL…
move-hoon Aug 3, 2025
894c9d5
[BOOK-207] feat: infra - Aladin API 응답을 위한 AladinDetailResponse 및 Ala…
move-hoon Aug 3, 2025
b50c97b
[BOOK-207] chore: apis - AladinBookQueryService의 getBookDetail 메서드에서 …
move-hoon Aug 3, 2025
86ca1b0
[BOOK-207] fix: infra - 중복된 필드 제거
move-hoon Aug 3, 2025
cb8b0e2
[BOOK-207] feat: global-utils - ISBN10을 ISBN13으로 변환하는 IsbnConverter 객…
move-hoon Aug 3, 2025
b923e19
[BOOK-207] refactor: infra - AladinRestClient에서 addCommonQueryParams …
move-hoon Aug 3, 2025
94895cc
[BOOK-207] refactor: apis - BookDetailResponse 및 BookSearchResponse 데…
move-hoon Aug 3, 2025
ee479c6
[BOOK-207] refactor: apis - BookCreateRequest에서 ISBN 처리 로직을 개선하여 ISBN…
move-hoon Aug 3, 2025
4ebdc1a
[BOOK-207] refactor: infra - AladinDetailResponse 및 AladinSearchRespo…
move-hoon Aug 3, 2025
968c794
[BOOK-207] delete: infra - AladinResponseBase.kt 파일 삭제
move-hoon Aug 3, 2025
9e4a8b6
[BOOK-207] refactor: apis - BookUseCase에서 사용자 책 상태를 병합하는 로직 추가 및 코드 정리
move-hoon Aug 3, 2025
cf02829
[BOOK-207] fix: infra - 중복된 필드 제거
move-hoon Aug 3, 2025
0c1d6c7
[BOOK-207] feat: apis - AuthorExtractor 객체 추가 및 BookDetailResponse에서 …
move-hoon Aug 3, 2025
a84de0f
[BOOK-207] feat: apis - NicknameGenerator 객체 추가하여 랜덤한 닉네임 생성 기능 구현
move-hoon Aug 3, 2025
29a386b
[BOOK-207] refactor: apis, global-utils - IsbnConverter 클래스를 apis 패키지…
move-hoon Aug 3, 2025
e38eaac
[BOOK-207] feat: apis - 책 관련 요청 데이터 클래스에 Swagger 스키마 및 유효성 검사 추가
move-hoon Aug 3, 2025
c3d3765
[BOOK-207] feat: apis - BookDetailResponse에 사용자 책 상태 추가 및 UserBookSer…
move-hoon Aug 3, 2025
62eabda
[BOOK-207] feat: domain - 사용자 책 정보를 조회하는 기능 추가 및 예외 처리 구현
move-hoon Aug 3, 2025
75fd264
[BOOK-207] fix: apis, domain - 사용자 책 상태 조회 기능을 nullable로 변경하고 관련 로직 수정
move-hoon Aug 3, 2025
ea24d9d
[BOOK-207] chore: domain - 불필요한 import문 제거
move-hoon Aug 3, 2025
70fc7ae
[BOOK-207] feat: global-utils, infra - 책 표지 크기 열거형 추가 및 AladinBookLoo…
move-hoon Aug 4, 2025
10ba6bd
[BOOK-207] refactor: apis - BookCreateRequest 및 BookDetailResponse에서 …
move-hoon Aug 4, 2025
414d193
[BOOK-207] refactor: apis - UserBookRegisterRequest에서 bookStatus를 nul…
move-hoon Aug 4, 2025
91b35e8
[BOOK-207] refactor: apis - BookSearchResponse에서 저자 정보를 AuthorExtract…
move-hoon Aug 4, 2025
a99a698
[BOOK-207] refactor: domain, apis, infra - ReadingRecordService에서 Use…
move-hoon Aug 4, 2025
1f73a87
[BOOK-207] chore: apis - CreateReadingRecordRequest에 Swagger 예제 추가 및 …
move-hoon Aug 4, 2025
19cbea0
[BOOK-207] fix: apis - ReadingRecordUseCase에서 userBookService의 매개변수 순…
move-hoon Aug 4, 2025
f9acaa9
[BOOK-207] refactor: infra - 생성자 호출 named parameter 형식으로 변경
move-hoon Aug 4, 2025
804e487
[BOOK-207] refactor: apis - converImageUrl validation 추가
move-hoon Aug 4, 2025
8526a79
[BOOK-207] fix: apis - emotionTags 필드 스웨거 설명 수정
move-hoon Aug 4, 2025
1b917e2
[BOOK-207] refactor: apis - BookDetailResponse에서 기본 페이지 수 상수 추가 및 tot…
move-hoon Aug 4, 2025
61a7d93
[BOOK-207] refactor: apis - 서비스 클래스에 @Validated 및 @Valid 어노테이션 추가하여 유…
move-hoon Aug 4, 2025
033d224
[BOOK-207] fix: apis - 컨트롤러의 dto를 바로 받는 메서드의 경우 @Valid 어노테이션 제거
move-hoon Aug 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.yapp.apis.auth.service

import jakarta.validation.Valid
import org.springframework.stereotype.Service
import org.springframework.validation.annotation.Validated
import org.yapp.apis.auth.dto.request.TokenGenerateRequest
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
import org.yapp.apis.auth.dto.response.RefreshTokenResponse
Expand All @@ -9,14 +11,15 @@ import org.yapp.domain.token.TokenDomainRedisService
import java.util.*

@Service
@Validated
class TokenService(
private val tokenDomainRedisService: TokenDomainRedisService,
) {
fun deleteRefreshTokenByToken(token: String) {
tokenDomainRedisService.deleteRefreshTokenByToken(token)
}

fun saveRefreshToken(tokenGenerateRequest: TokenGenerateRequest): RefreshTokenResponse {
fun saveRefreshToken(@Valid tokenGenerateRequest: TokenGenerateRequest): RefreshTokenResponse {
val token = tokenDomainRedisService.saveRefreshToken(
tokenGenerateRequest.validUserId(),
tokenGenerateRequest.validRefreshToken(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.yapp.apis.auth.service

import jakarta.validation.Valid
import org.springframework.stereotype.Service
import org.springframework.validation.annotation.Validated
import org.yapp.apis.auth.dto.request.FindOrCreateUserRequest
import org.yapp.apis.auth.dto.request.FindUserIdentityRequest
import org.yapp.apis.auth.dto.response.CreateUserResponse
Expand All @@ -13,6 +15,7 @@ import org.yapp.domain.user.vo.UserIdentityVO
import java.util.*

@Service
@Validated
class UserAuthService(
private val userDomainService: UserDomainService
) {
Expand All @@ -33,12 +36,12 @@ class UserAuthService(
}
}

fun findUserIdentityByUserId(findUserIdentityRequest: FindUserIdentityRequest): UserAuthInfoResponse {
fun findUserIdentityByUserId(@Valid findUserIdentityRequest: FindUserIdentityRequest): UserAuthInfoResponse {
val userIdentity = userDomainService.findUserIdentityById(findUserIdentityRequest.validUserId())
return UserAuthInfoResponse.from(userIdentity)
}

fun findOrCreateUser(findOrCreateUserRequest: FindOrCreateUserRequest): CreateUserResponse {
fun findOrCreateUser(@Valid findOrCreateUserRequest: FindOrCreateUserRequest): CreateUserResponse {
userDomainService.findUserByProviderTypeAndProviderId(
findOrCreateUserRequest.validProviderType(),
findOrCreateUserRequest.validProviderId()
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class BookController(
@AuthenticationPrincipal userId: UUID,
@Valid @ModelAttribute request: BookDetailRequest
): ResponseEntity<BookDetailResponse> {
val response = bookUseCase.getBookDetail(request)
val response = bookUseCase.getBookDetail(request, userId)
return ResponseEntity.ok(response)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,96 @@
package org.yapp.apis.book.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import org.yapp.apis.book.dto.response.BookDetailResponse

@Schema(
title = "책 생성 요청",
description = "시스템에 새로운 책 정보를 생성하는 요청 (주로 내부 API에서 사용)"
)
data class BookCreateRequest private constructor(
@field:NotBlank(message = "ISBN은 필수입니다.")
@Schema(
description = "책의 13자리 ISBN 코드",
example = "9788932473901",
required = true,
minLength = 13,
maxLength = 13
)
val isbn: String? = null,

@field:NotBlank(message = "제목은 필수입니다.")
@field:Size(max = 500, message = "제목은 500자 이내여야 합니다.")
@Schema(
description = "책 제목",
example = "해리 포터와 마법사의 돌",
required = true,
maxLength = 500
)
val title: String? = null,

@field:NotBlank(message = "저자는 필수입니다.")
@field:Size(max = 200, message = "저자는 200자 이내여야 합니다.")
@Schema(
description = "저자명 (여러 저자인 경우 쉼표로 구분)",
example = "J.K. 롤링",
required = true,
maxLength = 200
)
val author: String? = null,

@field:NotBlank(message = "출판사는 필수입니다.")
@field:Size(max = 200, message = "출판사는 200자 이내여야 합니다.")
@Schema(
description = "출판사명",
example = "문학수첩",
required = true,
maxLength = 200
)
val publisher: String? = null,

@field:Min(value = 1000, message = "출간연도는 1000년 이후여야 합니다.")
@field:Max(value = 2100, message = "출간연도는 2100년 이전이어야 합니다.")
@Schema(
description = "출간연도 (4자리 년도)",
example = "2000",
minimum = "1000",
maximum = "2100"
)
val publicationYear: Int? = null,

@field:Size(max = 2048, message = "표지 URL은 2048자 이내여야 합니다.")
val coverImageUrl: String,
@field:NotBlank(message = "표지 이미지 URL은 필수입니다.")
@Schema(
description = "책 표지 이미지 URL",
example = "https://image.aladin.co.kr/product/123/45/cover/1234567890123.jpg",
required = true,
maxLength = 2048,
format = "uri"
)
val coverImageUrl: String? = null,

@field:Size(max = 2000, message = "책 설명은 2000자 이내여야 합니다.")
@Schema(
description = "책 소개 및 줄거리",
example = "11살 해리 포터는 이모네 집에서 갖은 구박을 당하며 지낸다...",
maxLength = 2000
)
val description: String? = null
) {
fun validIsbn(): String = isbn!!
fun validTitle(): String = title!!
fun validAuthor(): String = author!!
fun validPublisher(): String = publisher!!
fun validCoverImageUrl(): String = coverImageUrl!!

companion object {

fun from(bookDetail: BookDetailResponse): BookCreateRequest {
val finalIsbn = bookDetail.isbn ?: bookDetail.isbn13
val finalIsbn = bookDetail.isbn13
?: throw IllegalArgumentException("ISBN이 존재하지 않습니다.")

return BookCreateRequest(
Expand All @@ -41,7 +99,7 @@ data class BookCreateRequest private constructor(
author = bookDetail.author,
publisher = bookDetail.publisher,
publicationYear = parsePublicationYear(bookDetail.pubDate),
coverImageUrl = bookDetail.cover,
coverImageUrl = bookDetail.coverImageUrl,
description = bookDetail.description,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
package org.yapp.apis.book.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern // Pattern 어노테이션 추가
import jakarta.validation.constraints.Pattern
import org.yapp.globalutils.util.RegexUtils
import org.yapp.infra.external.aladin.dto.AladinBookLookupRequest

@Schema(
title = "책 상세 정보 요청",
description = "특정 ISBN을 통한 책 상세 정보 조회 요청"
)
data class BookDetailRequest private constructor(
@field:NotBlank(message = "아이템 ID는 필수입니다.")
@field:NotBlank(message = "ISBN은 비어 있을 수 없습니다.")
@field:Pattern(
regexp = RegexUtils.NOT_BLANK_AND_NOT_NULL_STRING_PATTERN,
message = "아이템 ID유효한 ISBN 형식이 아닙니다."
regexp = RegexUtils.ISBN13_PATTERN,
message = "유효한 13자리 ISBN 형식이 아닙니다."
)
val itemId: String? = null,
val itemIdType: String? = "ISBN",
val optResult: List<String>? = null
@Schema(
description = "조회할 책의 13자리 ISBN 코드",
example = "9788932473901",
required = true,
pattern = "\\d{13}",
minLength = 13,
maxLength = 13
)
val isbn: String? = null,
) {

fun validIsbn(): String = itemId!!


fun toAladinRequest(): AladinBookLookupRequest {
return AladinBookLookupRequest.create(
itemId = this.itemId!!,
itemIdType = this.itemIdType ?: "ISBN",
optResult = this.optResult
)
}
fun validIsbn(): String = isbn!!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

안전하지 않은 non-null assertion 사용

validIsbn() 메서드에서 !! 연산자를 사용하는 것은 NPE를 발생시킬 위험이 있습니다.

더 안전한 방식으로 구현하는 것을 권장합니다:

-    fun validIsbn(): String = isbn!!
+    fun validIsbn(): String = isbn ?: throw IllegalStateException("ISBN이 제공되지 않았습니다")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun validIsbn(): String = isbn!!
fun validIsbn(): String = isbn ?: throw IllegalStateException("ISBN이 제공되지 않았습니다")
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt at
line 28, the validIsbn() method uses the non-null assertion operator (!!) on
isbn, which can cause a NullPointerException if isbn is null. To fix this,
modify the method to safely handle null values by either returning a nullable
String, providing a default value, or throwing a controlled exception with a
clear message instead of using !!.


companion object {
fun of(isbn: String?, optResult: List<String>? = null): BookDetailRequest {
fun from(
isbn: String?,
): BookDetailRequest {
return BookDetailRequest(
itemId = isbn,
itemIdType = "ISBN",
optResult = optResult
isbn = isbn,
)
}
Comment on lines +31 to 37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Factory 메서드의 입력 검증 부재

from 메서드가 nullable isbn을 받지만 검증 없이 그대로 전달합니다. 이는 잘못된 입력으로 인한 런타임 오류를 야기할 수 있습니다.

입력 검증을 추가하는 것을 권장합니다:

 companion object {
     fun from(
         isbn: String?,
     ): BookDetailRequest {
+        requireNotNull(isbn) { "ISBN은 필수입니다" }
+        require(isbn.matches(Regex(RegexUtils.ISBN13_PATTERN))) { "유효한 13자리 ISBN 형식이 아닙니다" }
         return BookDetailRequest(
             isbn = isbn,
         )
     }
 }
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt
around lines 31 to 37, the from factory method accepts a nullable isbn parameter
but does not validate it before passing it to the constructor, which can cause
runtime errors. Add input validation to check if isbn is null or empty and throw
an appropriate exception or handle the error before creating the
BookDetailRequest instance.

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,73 @@
package org.yapp.apis.book.dto.request

import org.yapp.infra.external.aladin.dto.AladinBookSearchRequest

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank

@Schema(
title = "책 검색 요청",
description = "알라딘 API를 통한 책 검색 요청 정보"
)
data class BookSearchRequest private constructor(
@field:NotBlank(message = "검색어는 필수입니다.")
@Schema(
description = "검색할 키워드 (제목, 저자, 출판사 등)",
example = "해리포터",
required = true
)
val query: String? = null,

@Schema(
description = "검색 유형",
example = "Title",
allowableValues = ["Title", "Author", "Publisher", "Keyword"],
defaultValue = "Keyword"
)
val queryType: String? = null,

@Schema(
description = "검색 대상",
example = "Book",
allowableValues = ["Book", "Foreign", "Music", "DVD"],
defaultValue = "Book"
)
val searchTarget: String? = null,

@field:Min(value = 1, message = "최대 결과 수는 1 이상이어야 합니다.")
@field:Max(value = 100, message = "최대 결과 수는 100 이하여야 합니다.")
@Schema(
description = "한 번에 가져올 최대 결과 수 (1-100)",
example = "10",
minimum = "1",
maximum = "100",
defaultValue = "10"
)
val maxResults: Int? = null,

@field:Min(value = 1, message = "시작 인덱스는 1 이상이어야 합니다.")
@Schema(
description = "검색 시작 인덱스 (페이징)",
example = "1",
minimum = "1",
defaultValue = "1"
)
val start: Int? = null,

@Schema(
description = "정렬 방식",
example = "Accuracy",
allowableValues = ["Accuracy", "PublishTime", "Title", "SalesPoint"],
defaultValue = "Accuracy"
)
val sort: String? = null,
val cover: String? = null,

@Schema(
description = "카테고리 ID (0: 전체)",
example = "0",
defaultValue = "0"
)
val categoryId: Int? = null
) {

fun validQuery(): String = query!!
fun toAladinRequest(): AladinBookSearchRequest {

return AladinBookSearchRequest.create(
query = this.validQuery(),
queryType = this.queryType,
searchTarget = this.searchTarget,
maxResults = this.maxResults,
start = this.start,
sort = this.sort,
cover = this.cover,
categoryId = this.categoryId
)
}
}
Loading