-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 독서기록 API 기능 구현 #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
b06d8f5
bf03a67
48ab1d1
0a33bd5
cc64171
3881e6d
5f665f8
6050cd2
6584426
c50f65c
c2cd5fe
7d2172d
e947c77
dbb12b3
c12c74a
78e9e6c
c4db4f4
c6853af
6786eac
49104b6
d606fdc
93c3875
fb6fd8d
221d774
413f117
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package org.yapp.apis.book.exception | ||
|
|
||
| import org.springframework.http.HttpStatus | ||
| import org.yapp.globalutils.exception.BaseErrorCode | ||
|
|
||
| enum class UserBookErrorCode( | ||
| private val status: HttpStatus, | ||
| private val code: String, | ||
| private val message: String | ||
| ) : BaseErrorCode { | ||
| USER_BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_BOOK_001", "사용자의 책을 찾을 수 없습니다."); | ||
|
|
||
| override fun getHttpStatus(): HttpStatus = status | ||
| override fun getCode(): String = code | ||
| override fun getMessage(): String = message | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package org.yapp.apis.book.exception | ||
|
|
||
| import org.yapp.globalutils.exception.CommonException | ||
|
|
||
| class UserBookNotFoundException( | ||
| errorCode: UserBookErrorCode, | ||
| message: String? = null | ||
| ) : CommonException(errorCode, message) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package org.yapp.apis.readingrecord.controller | ||
|
|
||
| import org.springframework.data.domain.Page | ||
| import org.springframework.data.domain.Pageable | ||
| import org.springframework.data.domain.Sort | ||
| import org.springframework.data.web.PageableDefault | ||
| import org.springframework.http.HttpStatus | ||
| 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.PathVariable | ||
| import org.springframework.web.bind.annotation.PostMapping | ||
| import org.springframework.web.bind.annotation.RequestBody | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.springframework.web.bind.annotation.RequestParam | ||
| import org.springframework.web.bind.annotation.RestController | ||
| import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest | ||
| import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse | ||
| import org.yapp.apis.readingrecord.usecase.ReadingRecordUseCase | ||
| import org.yapp.domain.readingrecord.ReadingRecordSortType | ||
| import java.util.UUID | ||
| import jakarta.validation.Valid | ||
|
|
||
| @RestController | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에 RequestMapping 추가해주시면 감사하겠습니다! (가독성을 위해서) |
||
| @RequestMapping("/api/v1/reading-records") | ||
| class ReadingRecordController( | ||
| private val readingRecordUseCase: ReadingRecordUseCase | ||
| ) : ReadingRecordControllerApi { | ||
|
|
||
| @PostMapping("/{userBookId}") | ||
| override fun createReadingRecord( | ||
| @AuthenticationPrincipal userId: UUID, | ||
| @PathVariable userBookId: UUID, | ||
| @Valid @RequestBody request: CreateReadingRecordRequest | ||
| ): ResponseEntity<ReadingRecordResponse> { | ||
| val response = readingRecordUseCase.createReadingRecord( | ||
| userId = userId, | ||
| userBookId = userBookId, | ||
| request = request | ||
| ) | ||
| return ResponseEntity.status(HttpStatus.CREATED).body(response) | ||
| } | ||
|
|
||
| @GetMapping("/{userBookId}") | ||
| override fun getReadingRecords( | ||
| @AuthenticationPrincipal userId: UUID, | ||
| @PathVariable userBookId: UUID, | ||
| @RequestParam(required = false) sort: ReadingRecordSortType?, | ||
| @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) | ||
| pageable: Pageable | ||
| ): ResponseEntity<Page<ReadingRecordResponse>> { | ||
| val response = readingRecordUseCase.getReadingRecordsByUserBookId( | ||
| userId = userId, | ||
| userBookId = userBookId, | ||
| sort = sort, | ||
| pageable = pageable | ||
| ) | ||
| return ResponseEntity.ok(response) | ||
| } | ||
|
Comment on lines
+25
to
+59
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그리고 getmapping이랑 postmapping도 추가해주시면 감사할 거 같아요 ㅎㅎ (이것도 가독성 때문에 ㅠㅠ) |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,89 @@ | ||||||||||||
| package org.yapp.apis.readingrecord.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.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.data.domain.Page | ||||||||||||
| import org.springframework.data.domain.Pageable | ||||||||||||
| import org.springframework.data.domain.Sort | ||||||||||||
| import org.springframework.data.web.PageableDefault | ||||||||||||
| 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.PathVariable | ||||||||||||
| import org.springframework.web.bind.annotation.PostMapping | ||||||||||||
| import org.springframework.web.bind.annotation.RequestBody | ||||||||||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||||||||||
| import org.springframework.web.bind.annotation.RequestParam | ||||||||||||
| import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest | ||||||||||||
| import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse | ||||||||||||
| import org.yapp.domain.readingrecord.ReadingRecordSortType | ||||||||||||
| import org.yapp.globalutils.exception.ErrorResponse | ||||||||||||
| import java.util.UUID | ||||||||||||
|
|
||||||||||||
| @Tag(name = "Reading Records", description = "독서 기록 관련 API") | ||||||||||||
| @RequestMapping("/api/v1/reading-records") | ||||||||||||
| interface ReadingRecordControllerApi { | ||||||||||||
|
|
||||||||||||
| @Operation( | ||||||||||||
| summary = "독서 기록 생성", | ||||||||||||
| description = "사용자의 책에 대한 독서 기록을 생성합니다." | ||||||||||||
| ) | ||||||||||||
| @ApiResponses( | ||||||||||||
| value = [ | ||||||||||||
| ApiResponse( | ||||||||||||
| responseCode = "201", | ||||||||||||
| description = "독서 기록 생성 성공", | ||||||||||||
| content = [Content(schema = Schema(implementation = ReadingRecordResponse::class))] | ||||||||||||
| ), | ||||||||||||
| ApiResponse( | ||||||||||||
| responseCode = "400", | ||||||||||||
| description = "잘못된 요청", | ||||||||||||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||||||||||||
| ), | ||||||||||||
| ApiResponse( | ||||||||||||
| responseCode = "404", | ||||||||||||
| description = "사용자 또는 책을 찾을 수 없음", | ||||||||||||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||||||||||||
| ) | ||||||||||||
| ] | ||||||||||||
| ) | ||||||||||||
| @PostMapping("/{userBookId}") | ||||||||||||
| fun createReadingRecord( | ||||||||||||
| @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, | ||||||||||||
| @PathVariable @Parameter(description = "독서 기록을 생성할 사용자 책 ID") userBookId: UUID, | ||||||||||||
| @Valid @RequestBody @Parameter(description = "독서 기록 생성 요청 객체") request: CreateReadingRecordRequest | ||||||||||||
| ): ResponseEntity<ReadingRecordResponse> | ||||||||||||
|
|
||||||||||||
| @Operation( | ||||||||||||
| summary = "독서 기록 목록 조회", | ||||||||||||
| description = "사용자의 책에 대한 독서 기록을 페이징하여 조회합니다. 정렬은 페이지 번호 또는 최신 등록순으로 가능합니다." | ||||||||||||
| ) | ||||||||||||
| @ApiResponses( | ||||||||||||
| value = [ | ||||||||||||
| ApiResponse( | ||||||||||||
| responseCode = "200", | ||||||||||||
| description = "독서 기록 목록 조회 성공", | ||||||||||||
| content = [Content(schema = Schema(implementation = ReadingRecordResponse::class))] | ||||||||||||
| ), | ||||||||||||
| ApiResponse( | ||||||||||||
| responseCode = "404", | ||||||||||||
| description = "사용자 또는 책을 찾을 수 없음", | ||||||||||||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||||||||||||
| ) | ||||||||||||
| ] | ||||||||||||
| ) | ||||||||||||
| @GetMapping("/{userBookId}") | ||||||||||||
| fun getReadingRecords( | ||||||||||||
| @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, | ||||||||||||
| @PathVariable @Parameter(description = "독서 기록을 조회할 사용자 책 ID") userBookId: UUID, | ||||||||||||
| @RequestParam(required = false) @Parameter(description = "정렬 방식 (PAGE_NUMBER_ASC, PAGE_NUMBER_DESC, CREATED_DATE_ASC, CREATED_DATE_DESC)") sort: ReadingRecordSortType?, | ||||||||||||
| @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) | ||||||||||||
| @Parameter(description = "페이지네이션 정보 (페이지 번호, 페이지 크기, 정렬 방식)") pageable: Pageable | ||||||||||||
|
Comment on lines
+85
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 정렬 매개변수 설계가 부분적으로 개선되었으나 여전히 모호함이 남아있습니다.
다음 중 하나의 방식으로 개선을 권장합니다: 옵션 1: Pageable만 사용 (권장) - sort: ReadingRecordSortType?,
- @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
+ @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)옵션 2: 커스텀 sort가 Pageable 정렬을 오버라이드하도록 명시 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| ): ResponseEntity<Page<ReadingRecordResponse>> | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package org.yapp.apis.readingrecord.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 | ||
|
|
||
|
|
||
| @Schema(description = "독서 기록 생성 요청") | ||
| data class CreateReadingRecordRequest private constructor( | ||
| @field:Min(1, message = "페이지 번호는 1 이상이어야 합니다.") | ||
| @field:Max(9999, message = "페이지 번호는 9999 이하여야 합니다.") | ||
| @Schema(description = "현재 읽은 페이지 번호", example = "42", required = true) | ||
| val pageNumber: Int? = null, | ||
|
|
||
| @field:NotBlank(message = "기억에 남는 문장은 필수입니다.") | ||
| @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.") | ||
| @Schema(description = "기억에 남는 문장", example = "이것은 기억에 남는 문장입니다.", required = true) | ||
| val quote: String? = null, | ||
|
|
||
| @field:NotBlank(message = "감상평은 필수입니다.") | ||
| @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.") | ||
| @Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.", required = true) | ||
| val review: String? = null, | ||
|
|
||
| @field:Size(max = 3, message = "감정 태그는 최대 3개까지 가능합니다.") | ||
| @Schema(description = "감정 태그 목록 (최대 3개)", example = "[\"감동적\", \"슬픔\", \"희망\"]") | ||
| val emotionTags: List<@Size(max = 10, message = "감정 태그는 10자를 초과할 수 없습니다.") String> = emptyList() | ||
| ) { | ||
| fun validPageNumber(): Int = pageNumber!! | ||
| fun validQuote(): String = quote!! | ||
| fun validReview(): String = review!! | ||
| fun validEmotionTags(): List<String> = emotionTags | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
예외 메시지에서 민감한 정보 노출을 고려해주세요.
메서드 구현은 적절하지만 다음 사항들을 검토해보시기 바랍니다:
userBookId와userId가 직접 노출되고 있습니다. 로그나 클라이언트 응답에서 이러한 내부 ID가 노출될 수 있습니다.findByIdAndUserId(userBookId, userId)와 달리 현재 메서드는(userId, userBookId)순서를 사용합니다. 일관성을 위해 순서를 맞추는 것을 고려해보세요.다음과 같이 수정을 제안합니다:
📝 Committable suggestion
🤖 Prompt for AI Agents