Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0c66457
[BOOK-96] feat: infra - UserBookEntity 구현 (#29)
minwoo1999 Jul 7, 2025
e36fcf9
[BOOK-96] feat: infra - UserBookRepository 구현 (#29)
minwoo1999 Jul 7, 2025
374356d
[BOOK-96] refactor: infra - UserBookEntity 수정 (#29)
minwoo1999 Jul 7, 2025
7be19e0
[BOOK-96] feat: domain - book,userbook domainservice 분리 (#29)
minwoo1999 Jul 7, 2025
8ac5039
[BOOK-96] feat: domain - UserBook Domain 모델 구현 (#29)
minwoo1999 Jul 7, 2025
d0722ff
[BOOK-96] refactor: domain - Book Domain 모델 수정 (#29)
minwoo1999 Jul 7, 2025
a0882aa
[BOOK-96] feat: apis - 도서등록,내서재 Usecase 구현 (#29)
minwoo1999 Jul 7, 2025
7241fdb
[BOOK-96] feat: apis - Book Exception 정의 (#29)
minwoo1999 Jul 7, 2025
e63d655
[BOOK-96] feat: apis - UserBook Dto 구현 (#29)
minwoo1999 Jul 7, 2025
cbced98
[BOOK-96] refactor: apis - BookDetail DTO 수정 (#29)
minwoo1999 Jul 7, 2025
5cc8b78
[BOOK-96] feat: apis - Controller 도서등록, 내서재 구현 (#29)
minwoo1999 Jul 7, 2025
352a221
[BOOK-96] chore: infra,domain,apis import * 제거
minwoo1999 Jul 10, 2025
d635d9f
[BOOK-96] refator: infra - 상태 변경이 필요한 필드를 var + protected set (#29)
minwoo1999 Jul 10, 2025
1077b85
[BOOK-96] feat: domain - userBook 도메인계층 기능개발 (#29)
minwoo1999 Jul 10, 2025
ea6f796
[BOOK-96] refactor: apis - service계층 분리 (#29)
minwoo1999 Jul 10, 2025
e744548
[BOOK-96] feat: apis - dto 정의 (#29)
minwoo1999 Jul 10, 2025
8772208
[BOOK-96] refactor: apis - usecase service 분리 (#29)
minwoo1999 Jul 10, 2025
d70529f
[BOOK-96] refactor: apis -  AuthenticationPrincipal를 통해 userId 주입 …
minwoo1999 Jul 10, 2025
31b643c
[BOOK-96] refactor: apis - api swagger 명세서 response 수정
minwoo1999 Jul 10, 2025
3be48ab
[BOOK-96] refactor: apis,domain,infra - user validate 메소드 별도로 분리
minwoo1999 Jul 11, 2025
753d309
[BOOK-96] chore: apis - 불필요한 코드 제거 (#29)
minwoo1999 Jul 11, 2025
2931e91
[BOOK-96] refactor: apis,domain - 패키지구조 변경 (#29)
minwoo1999 Jul 12, 2025
e66845a
[BOOK-96] refactor: apis- 패키지구조 변경 (#29)
minwoo1999 Jul 12, 2025
91d69cf
[BOOK-96] refactor: apis- 외부 API 인터페이스 추상화 (#29)
minwoo1999 Jul 12, 2025
3911657
[BOOK-96] refactor: apis- book도메인 객체 domainservice 내부에서 생성 (#29)
minwoo1999 Jul 12, 2025
c1cfc11
[BOOK-96] refactor: apis- BookQueryServiceQualifier 상수처리 (#29)
minwoo1999 Jul 12, 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
Expand Up @@ -2,13 +2,14 @@ package org.yapp.apis.auth.controller

import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*
import org.yapp.apis.auth.dto.request.SocialLoginRequest
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
import org.yapp.apis.auth.dto.response.AuthResponse
import org.yapp.apis.auth.dto.response.UserProfileResponse
import org.yapp.apis.auth.usecase.AuthUseCase
import org.yapp.apis.util.AuthUtils
import java.util.*

/**
* Implementation of the authentication controller API.
Expand All @@ -33,15 +34,13 @@ class AuthController(
}

@PostMapping("/signout")
override fun signOut(@RequestHeader("Authorization") authorization: String): ResponseEntity<Unit> {
val userId = AuthUtils.extractUserIdFromAuthHeader(authorization, authUseCase::getUserIdFromAccessToken)
override fun signOut(@AuthenticationPrincipal userId: UUID): ResponseEntity<Unit> {
authUseCase.signOut(userId)
return ResponseEntity.noContent().build()
}

@GetMapping("/me")
override fun getUserProfile(@RequestHeader("Authorization") authorization: String): ResponseEntity<UserProfileResponse> {
val userId = AuthUtils.extractUserIdFromAuthHeader(authorization, authUseCase::getUserIdFromAccessToken)
override fun getUserProfile(@AuthenticationPrincipal userId: UUID): ResponseEntity<UserProfileResponse> {
val userProfile = authUseCase.getUserProfile(userId)
return ResponseEntity.ok(userProfile)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.yapp.apis.auth.dto.request.SocialLoginRequest
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
import org.yapp.apis.auth.dto.response.AuthResponse
import org.yapp.apis.auth.dto.response.UserProfileResponse
import org.yapp.globalutils.exception.ErrorResponse
import java.util.UUID

/**
* API interface for authentication controller.
*/
@Tag(name = "Authentication", description = "Authentication API")
@RequestMapping("/api/v1/auth")
interface AuthControllerApi {
Expand All @@ -34,11 +37,13 @@ interface AuthControllerApi {
),
ApiResponse(
responseCode = "400",
description = "Invalid request or credentials"
description = "Invalid request or credentials",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
ApiResponse(
responseCode = "409",
description = "Email already in use with a different account"
description = "Email already in use with a different account",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
Expand All @@ -47,7 +52,7 @@ interface AuthControllerApi {

@Operation(
summary = "Refresh token",
description = "Refresh an access token using a refresh token. Returns both a new access token and a new refresh token. The client MUST use the new refresh token for subsequent refreshes, as the old refresh token is deleted from the server."
description = "Refresh an access token using a refresh token. Returns both a new access token and a new refresh token."
)
@ApiResponses(
value = [
Expand All @@ -58,41 +63,34 @@ interface AuthControllerApi {
),
ApiResponse(
responseCode = "400",
description = "Invalid refresh token"
description = "Invalid refresh token",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
ApiResponse(
responseCode = "404",
description = "Refresh token not found"
description = "Refresh token not found",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@PostMapping("/refresh")
fun refreshToken(@RequestBody @Valid request: TokenRefreshRequest): ResponseEntity<AuthResponse>

@Operation(
summary = "Sign out",
description = "Sign out a user by invalidating their refresh token"
)
@Operation(summary = "Sign out", description = "Sign out a user by invalidating their refresh token")
@ApiResponses(
value = [
ApiResponse(
responseCode = "204",
description = "Successful sign out"
),
ApiResponse(responseCode = "204", description = "Successful sign out"),
ApiResponse(
responseCode = "400",
description = "Invalid user ID"
description = "Invalid user ID",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@PostMapping("/signout")
fun signOut(@RequestHeader("Authorization") authorization: String): ResponseEntity<Unit>
fun signOut(@AuthenticationPrincipal userId: UUID): ResponseEntity<Unit>


@Operation(
summary = "Get user profile",
description = "Retrieves profile information for the given user ID."
)
@Operation(summary = "Get user profile", description = "Retrieves profile information for the given user ID.")
@ApiResponses(
value = [
ApiResponse(
Expand All @@ -102,12 +100,11 @@ interface AuthControllerApi {
),
ApiResponse(
responseCode = "404",
description = "User not found"
description = "User not found",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@GetMapping("/me")
fun getUserProfile(
@RequestHeader("Authorization") authorization: String
): ResponseEntity<UserProfileResponse>
fun getUserProfile(@AuthenticationPrincipal userId: UUID): ResponseEntity<UserProfileResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ package org.yapp.apis.book.controller

import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.yapp.apis.book.dto.request.BookDetailRequest
import org.yapp.apis.book.dto.request.BookSearchRequest
import org.yapp.apis.book.dto.request.UserBookRegisterRequest
import org.yapp.apis.book.dto.response.BookDetailResponse
import org.yapp.apis.book.dto.response.BookSearchResponse
import org.yapp.apis.book.dto.response.UserBookResponse
import org.yapp.apis.book.usecase.BookUseCase

import java.util.UUID

@RestController
@RequestMapping("/api/v1/books")
class BookController(
private val bookUseCase: BookUseCase
private val bookUseCase: BookUseCase,
) : BookControllerApi {

@GetMapping("/search")
Expand All @@ -29,7 +34,25 @@ class BookController(
override fun getBookDetail(
@Valid @ModelAttribute request: BookDetailRequest
): ResponseEntity<BookDetailResponse> {
val response = bookUseCase.getBookDetail(request)
val response = bookUseCase.getBookDetail(request.validIsbn())
return ResponseEntity.ok(response)
}

@PutMapping("/upsert")
override fun upsertBookToMyLibrary(
@AuthenticationPrincipal userId: UUID,
@Valid @RequestBody request: UserBookRegisterRequest
): ResponseEntity<UserBookResponse> {
val response = bookUseCase.upsertBookToMyLibrary(userId, request)
return ResponseEntity.ok(response)
}

@GetMapping("/my-library")
override fun getUserLibraryBooks(
@AuthenticationPrincipal userId: UUID
): ResponseEntity<List<UserBookResponse>> {

val response = bookUseCase.getUserLibraryBooks(userId)
return ResponseEntity.ok(response)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,32 @@ package org.yapp.apis.book.controller
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.ExampleObject
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 jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.yapp.apis.book.dto.request.BookDetailRequest
import org.yapp.apis.book.dto.request.BookSearchRequest
import org.yapp.apis.book.dto.request.UserBookRegisterRequest
import org.yapp.apis.book.dto.response.BookDetailResponse
import org.yapp.apis.book.dto.response.BookSearchResponse
import org.yapp.apis.book.dto.response.UserBookResponse
import org.yapp.globalutils.exception.ErrorResponse
import java.util.UUID


/**
* API interface for book controller.
*/
@Tag(name = "Books", description = "도서 정보를 조회하는 API")
@RequestMapping("/api/v1/books")
interface BookControllerApi {

@Operation(
summary = "도서 검색",
description = "키워드를 사용하여 알라딘 도서 정보를 검색합니다."
)
@Operation(summary = "도서 검색", description = "키워드를 사용하여 알라딘 도서 정보를 검색합니다.")
@ApiResponses(
value = [
ApiResponse(
Expand All @@ -37,41 +38,15 @@ interface BookControllerApi {
),
ApiResponse(
responseCode = "400",
description = "잘못된 요청 파라미터"
description = "잘못된 요청 파라미터",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@GetMapping("/search")
fun searchBooks(
@Valid
@Parameter(
description = "도서 검색 요청 객체. 다음 쿼리 파라미터를 포함합니다:<br>" +
"- `query` (필수): 검색어 <br>" +
"- `queryType` (선택): 검색어 타입 (예: Title, Author). 기본값은 All <br>" +
"- `maxResults` (선택): 한 페이지당 결과 개수 (1-50). 기본값 10 <br>" +
"- `start` (선택): 결과 시작 페이지. 기본값 1 <br>" +
"- `sort` (선택): 정렬 방식 (예: PublishTime, SalesPoint). 기본값 Accuracy <br>" +
"- `categoryId` (선택): 카테고리 ID",
examples = [
ExampleObject(name = "기본 검색", value = "http://localhost:8080/api/v1/books/search?query=코틀린"),
ExampleObject(
name = "상세 검색",
value = "http://localhost:8080/api/v1/books/search?query=클린코드&queryType=Title&maxResults=10&sort=PublishTime"
),
ExampleObject(
name = "카테고리 검색",
value = "http://localhost:8080/api/v1/books/search?query=Spring&categoryId=170&start=2&maxResults=5"
)
]
)
request: BookSearchRequest
): ResponseEntity<BookSearchResponse>

fun searchBooks(@Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest): ResponseEntity<BookSearchResponse>

@Operation(
summary = "도서 상세 조회",
description = "특정 도서의 상세 정보를 조회합니다. `itemId`는 쿼리 파라미터로 전달됩니다."
)
@Operation(summary = "도서 상세 조회", description = "특정 도서의 상세 정보를 조회합니다.")
@ApiResponses(
value = [
ApiResponse(
Expand All @@ -81,33 +56,75 @@ interface BookControllerApi {
),
ApiResponse(
responseCode = "400",
description = "잘못된 요청 파라미터 (예: 유효하지 않은 itemId 또는 itemIdType)"
description = "잘못된 요청 파라미터",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
ApiResponse(
responseCode = "404",
description = "해당하는 itemId를 가진 도서를 찾을 수 없습니다."
description = "해당하는 itemId를 가진 도서를 찾을 수 없습니다.",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@GetMapping("/detail")
fun getBookDetail(
@Valid
@Parameter(
description = "도서 상세 조회 요청 객체. 다음 쿼리 파라미터를 포함합니다:<br>" +
"- `itemId` (필수): 조회할 도서의 고유 ID (ISBN, ISBN13, 알라딘 ItemId 등)<br>" +
"- `itemIdType` (선택): `itemId`의 타입 (ISBN, ISBN13, ItemId). 기본값은 ISBN입니다.<br>" +
"- `optResult` (선택): 조회할 부가 정보 목록 (쉼표로 구분). 예시: `BookInfo,Toc,PreviewImg`",
examples = [
ExampleObject(
name = "ISBN으로 상세 조회",
value = "http://localhost:8080/api/v1/books/detail?itemId=9791162241684&itemIdType=ISBN13"
),
ExampleObject(
name = "ISBN 및 부가 정보 포함",
value = "http://localhost:8080/api/v1/books/detail?itemId=8994492040&itemIdType=ISBN&optResult=BookInfo,Toc"
)
]
)
request: BookDetailRequest
): ResponseEntity<BookDetailResponse>
fun getBookDetail(@Valid @Parameter(description = "도서 상세 조회 요청 객체") request: BookDetailRequest): ResponseEntity<BookDetailResponse>

@Operation(summary = "서재에 책 등록 또는 상태 업데이트 (Upsert)", description = "사용자의 서재에 책을 등록하거나, 이미 등록된 책의 상태를 업데이트합니다.")
@ApiResponses(
value = [
ApiResponse(
responseCode = "201",
description = "책이 서재에 성공적으로 등록되었습니다.",
content = [Content(schema = Schema(implementation = UserBookResponse::class))]
),
ApiResponse(
responseCode = "200",
description = "책 상태가 성공적으로 업데이트되었습니다.",
content = [Content(schema = Schema(implementation = UserBookResponse::class))]
),
ApiResponse(
responseCode = "400",
description = "잘못된 요청 파라미터",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
ApiResponse(
responseCode = "404",
description = "존재하지 않는 책 (ISBN 오류)",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@PutMapping("/upsert")
fun upsertBookToMyLibrary(
@AuthenticationPrincipal userId: UUID,
@Valid @RequestBody request: UserBookRegisterRequest
): ResponseEntity<UserBookResponse>
Comment on lines +72 to +101
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

인증 처리와 HTTP 메서드 선택이 개선되었습니다.

과거 리뷰에서 제안된 사항들이 잘 반영되었습니다:

  • @PutMapping 사용으로 멱등성 보장
  • @AuthenticationPrincipal 사용으로 인증 처리 개선
  • 적절한 HTTP 상태 코드와 ErrorResponse 스키마 사용

다만 /upsert 경로명은 여전히 RESTful하지 않습니다. /my-library 또는 /library/books와 같은 리소스 중심의 경로명을 고려해보세요.

🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt
around lines 72 to 101, the endpoint path "/upsert" is not RESTful. Rename the
path to a resource-oriented name such as "/my-library" or "/library/books" to
better align with RESTful API design principles while keeping the existing
method, annotations, and functionality intact.


@Operation(summary = "사용자 서재 조회", description = "현재 사용자의 서재에 등록된 모든 책을 조회합니다.")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "서재 조회 성공",
content = [Content(schema = Schema(implementation = UserBookResponse::class))]
),
ApiResponse(
responseCode = "400",
description = "Invalid request or credentials",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
ApiResponse(
responseCode = "409",
description = "Email already in use with a different account",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
ApiResponse(
responseCode = "404",
description = "사용자를 찾을 수 없습니다.",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
)
@GetMapping("/my-library")
fun getUserLibraryBooks(@AuthenticationPrincipal userId: UUID): ResponseEntity<List<UserBookResponse>>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

ApiResponses 설명이 현재 API와 맞지 않습니다.

라인 113, 118-119의 description들이 다른 API (인증 관련)에서 복사된 것으로 보입니다. 사용자 서재 조회 API에 맞게 수정해주세요.

-                responseCode = "400",
-                description = "Invalid request or credentials",
+                responseCode = "400", 
+                description = "잘못된 요청",
                 content = [Content(schema = Schema(implementation = ErrorResponse::class))]
             ),
             ApiResponse(
-                responseCode = "409",
-                description = "Email already in use with a different account",
-                content = [Content(schema = Schema(implementation = ErrorResponse::class))]
-            ),
-            ApiResponse(
                 responseCode = "404",
                 description = "사용자를 찾을 수 없습니다.",
                 content = [Content(schema = Schema(implementation = ErrorResponse::class))]

또한 409 상태 코드는 이 API에서 발생하지 않을 것 같으니 제거하는 것이 좋겠습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt
between lines 103 and 129, the ApiResponses descriptions for status codes 400,
404, and 409 do not match the user library books retrieval API and seem copied
from an authentication API. Update the descriptions for 400 and 404 to reflect
errors relevant to this API, such as invalid request parameters or user not
found in the library context, and remove the 409 response entirely since it is
not applicable here.

}
Loading