-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 독서 기록 V2 API 및 대분류/세부 감정 시스템 구현 #146
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
Merged
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
49f5d27
[BOOK-480] refactor: apis - emotionTags는 빈 리스트도 허용이 되기 때문에 valid 패턴 제거
move-hoon b3c30f2
[BOOK-480] feat: infra - 감정 시스템 DB 스키마 마이그레이션 추가
move-hoon 3a85de7
[BOOK-480] feat: domain - PrimaryEmotion 대분류 감정 enum 추가
move-hoon 3b2914b
[BOOK-480] feat: domain - DetailTag 세부감정 도메인 및 Repository 인터페이스 추가
move-hoon 84b94c8
[BOOK-480] feat: domain - ReadingRecordDetailTag 연결 도메인 및 Repository …
move-hoon 4045ca8
[BOOK-480] feat: infra - DetailTag 엔티티 및 Repository 구현체 추가
move-hoon aa4cc4a
[BOOK-480] feat: infra - ReadingRecordDetailTag 엔티티 및 Repository 구현체 추가
move-hoon ee429a8
[BOOK-480] feat: domain, infra - ReadingRecord에 primaryEmotion 필드 추가
move-hoon 1b0caa4
[BOOK-480] feat: domain - ReadingRecordInfoVO에 V2용 detailEmotions 필드 추가
move-hoon 900feed
[BOOK-480] feat: domain - DetailTagDomainService 추가
move-hoon 28c7d82
[BOOK-480] feat: domain - ReadingRecordDetailTagDomainService 추가
move-hoon ce821c6
[BOOK-480] refactor: domain - ReadingRecordDomainService V2 메서드 추가 및 …
move-hoon 8af8806
[BOOK-480] feat: apis - V2 Request/Response DTO 추가
move-hoon ff5b0b6
[BOOK-480] feat: apis - ReadingRecordServiceV2 ApplicationService 추가
move-hoon 0cdf8d9
[BOOK-480] feat: apis - ReadingRecordUseCaseV2 유스케이스 추가
move-hoon 31d58a3
[BOOK-480] feat: apis - ReadingRecordControllerV2 컨트롤러 추가
move-hoon 87b8b6c
[BOOK-480] feat: apis - EmotionController 감정 목록 조회 API 추가
move-hoon ad44b1d
[BOOK-480] fix: apis - V1 ReadingRecordResponse pageNumber nullable 호…
move-hoon 92f2b5d
[BOOK-480] refactor: apis - private constructor 및 정적 팩토리 메서드로 패턴 변경
move-hoon 29a8fb9
[BOOK-480] fix: apis - 독서 기록 목록 조회 (V2) 명세 오류 수정
move-hoon 38498b2
[BOOK-480] fix: apis - 누락된 NotNull 어노테이션 추가
move-hoon 1f39fa1
[BOOK-480] chore: apis - pageNumber가 선택 사항임을 명시
move-hoon 71c5f90
[BOOK-480] fix: apis - primaryEmotion 변경 시 detailEmotion 데이터 일관성 보장
move-hoon ab30e00
[BOOK-480] refactor: apis - 독시 기록에 대한 소유권 검증 로직 추가
move-hoon 7553515
[BOOK-480] refactor: apis - readingRecords를 불변 리스트로 변경
move-hoon 45bdbbd
[BOOK-480] chore: apis - 403 에러 명세 추가
move-hoon 598bfdb
[BOOK-480] refactor: apis - 기존 단언 패턴으로 valid 메서드 구현방식 변경
move-hoon ed4a5b7
[BOOK-480] chore: apis - Void 타입 대신 Kotlin의 Unit 타입으로 변경
move-hoon 4a5e4ec
[BOOK-480] refactor: apis - 코드레빗 리뷰 반영
move-hoon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionController.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package org.yapp.apis.emotion.controller | ||
|
|
||
| import org.springframework.http.ResponseEntity | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.springframework.web.bind.annotation.RestController | ||
| import org.yapp.apis.emotion.dto.response.EmotionListResponse | ||
| import org.yapp.apis.emotion.service.EmotionService | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v2/emotions") | ||
| class EmotionController( | ||
| private val emotionService: EmotionService | ||
| ) : EmotionControllerApi { | ||
|
|
||
| @GetMapping | ||
| override fun getEmotions(): ResponseEntity<EmotionListResponse> { | ||
| val response = emotionService.getEmotionList() | ||
| return ResponseEntity.ok(response) | ||
| } | ||
| } |
33 changes: 33 additions & 0 deletions
33
apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionControllerApi.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package org.yapp.apis.emotion.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.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.yapp.apis.emotion.dto.response.EmotionListResponse | ||
|
|
||
| @Tag(name = "Emotions", description = "감정 관련 API") | ||
| @RequestMapping("/api/v2/emotions") | ||
| interface EmotionControllerApi { | ||
|
|
||
| @Operation( | ||
| summary = "감정 목록 조회", | ||
| description = "대분류 감정과 세부 감정 목록을 조회합니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "200", | ||
| description = "감정 목록 조회 성공", | ||
| content = [Content(schema = Schema(implementation = EmotionListResponse::class))] | ||
| ) | ||
| ] | ||
| ) | ||
| @GetMapping | ||
| fun getEmotions(): ResponseEntity<EmotionListResponse> | ||
| } |
72 changes: 72 additions & 0 deletions
72
apis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package org.yapp.apis.emotion.dto.response | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema | ||
| import org.yapp.domain.detailtag.DetailTag | ||
| import org.yapp.domain.readingrecord.PrimaryEmotion | ||
| import java.util.UUID | ||
|
|
||
| @Schema(name = "EmotionListResponse", description = "감정 목록 응답") | ||
| data class EmotionListResponse private constructor( | ||
| @field:Schema(description = "감정 그룹 목록") | ||
| val emotions: List<EmotionGroupDto> | ||
| ) { | ||
| companion object { | ||
| fun from(detailTags: List<DetailTag>): EmotionListResponse { | ||
| val grouped = detailTags.groupBy { it.primaryEmotion } | ||
|
|
||
| val emotions = PrimaryEmotion.entries.map { primary -> | ||
| EmotionGroupDto.of( | ||
| code = primary.name, | ||
| displayName = primary.displayName, | ||
| detailEmotions = grouped[primary] | ||
| ?.sortedBy { it.displayOrder } | ||
| ?.map { EmotionDetailDto.of(id = it.id.value, name = it.name) } | ||
| ?: emptyList() | ||
| ) | ||
| } | ||
|
|
||
| return EmotionListResponse(emotions = emotions) | ||
| } | ||
| } | ||
|
|
||
| @Schema(name = "EmotionGroupDto", description = "감정 그룹 (대분류 + 세부감정)") | ||
| data class EmotionGroupDto private constructor( | ||
| @field:Schema(description = "대분류 코드", example = "JOY") | ||
| val code: String, | ||
|
|
||
| @field:Schema(description = "대분류 표시 이름", example = "즐거움") | ||
| val displayName: String, | ||
|
|
||
| @field:Schema(description = "세부 감정 목록") | ||
| val detailEmotions: List<EmotionDetailDto> | ||
| ) { | ||
| companion object { | ||
| fun of( | ||
| code: String, | ||
| displayName: String, | ||
| detailEmotions: List<EmotionDetailDto> | ||
| ): EmotionGroupDto { | ||
| return EmotionGroupDto( | ||
| code = code, | ||
| displayName = displayName, | ||
| detailEmotions = detailEmotions | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Schema(name = "EmotionDetailDto", description = "세부 감정") | ||
| data class EmotionDetailDto private constructor( | ||
| @field:Schema(description = "세부 감정 ID", example = "123e4567-e89b-12d3-a456-426614174000") | ||
| val id: UUID, | ||
|
|
||
| @field:Schema(description = "세부 감정 이름", example = "설레는") | ||
| val name: String | ||
| ) { | ||
| companion object { | ||
| fun of(id: UUID, name: String): EmotionDetailDto { | ||
| return EmotionDetailDto(id = id, name = name) | ||
| } | ||
| } | ||
| } | ||
| } |
16 changes: 16 additions & 0 deletions
16
apis/src/main/kotlin/org/yapp/apis/emotion/service/EmotionService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package org.yapp.apis.emotion.service | ||
|
|
||
| import org.yapp.apis.emotion.dto.response.EmotionListResponse | ||
| import org.yapp.domain.detailtag.DetailTagDomainService | ||
| import org.yapp.globalutils.annotation.ApplicationService | ||
|
|
||
| @ApplicationService | ||
| class EmotionService( | ||
| private val detailTagDomainService: DetailTagDomainService | ||
| ) { | ||
| fun getEmotionList(): EmotionListResponse { | ||
| val detailTags = detailTagDomainService.findAll() | ||
| return EmotionListResponse.from(detailTags) | ||
| } | ||
| } | ||
|
|
175 changes: 175 additions & 0 deletions
175
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| 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.* | ||
| import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequestV2 | ||
| import org.yapp.apis.readingrecord.dto.request.UpdateReadingRecordRequestV2 | ||
| import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponseV2 | ||
| import org.yapp.domain.readingrecord.ReadingRecordSortType | ||
| import org.yapp.globalutils.exception.ErrorResponse | ||
| import java.util.* | ||
|
|
||
| @Tag(name = "Reading Records V2", description = "독서 기록 관련 API (V2)") | ||
| @RequestMapping("/api/v2/reading-records") | ||
| interface ReadingRecordControllerApiV2 { | ||
|
|
||
| @Operation( | ||
| summary = "독서 기록 생성 (V2)", | ||
| description = "사용자의 책에 대한 독서 기록을 생성합니다. 대분류 감정은 필수, 세부 감정은 선택입니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "201", | ||
| description = "독서 기록 생성 성공", | ||
| content = [Content(schema = Schema(implementation = ReadingRecordResponseV2::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "400", | ||
| description = "잘못된 요청", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "403", | ||
| 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: CreateReadingRecordRequestV2 | ||
| ): ResponseEntity<ReadingRecordResponseV2> | ||
|
|
||
| @Operation( | ||
| summary = "독서 기록 상세 조회 (V2)", | ||
| description = "독서 기록 ID로 독서 기록 상세 정보를 조회합니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "200", | ||
| description = "독서 기록 상세 조회 성공", | ||
| content = [Content(schema = Schema(implementation = ReadingRecordResponseV2::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "403", | ||
| description = "해당 독서 기록에 대한 접근 권한이 없음", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "404", | ||
| description = "사용자 또는 독서 기록을 찾을 수 없음", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ) | ||
| ] | ||
| ) | ||
| @GetMapping("/detail/{readingRecordId}") | ||
| fun getReadingRecordDetail( | ||
| @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, | ||
| @PathVariable @Parameter(description = "조회할 독서 기록 ID") readingRecordId: UUID | ||
| ): ResponseEntity<ReadingRecordResponseV2> | ||
|
|
||
| @Operation( | ||
| summary = "독서 기록 목록 조회 (V2)", | ||
| description = "사용자의 책에 대한 독서 기록을 페이징하여 조회합니다. sort 파라미터가 지정된 경우 해당 정렬이 우선 적용되며, 지정하지 않으면 기본 정렬(updatedAt DESC)이 적용됩니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "200", | ||
| description = "독서 기록 목록 조회 성공" | ||
| ), | ||
| 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, UPDATED_DATE_ASC, UPDATED_DATE_DESC). 지정 시 Pageable의 sort보다 우선 적용됨" | ||
| ) sort: ReadingRecordSortType?, | ||
| @PageableDefault(size = 10, sort = ["updatedAt"], direction = Sort.Direction.DESC) | ||
| @Parameter(description = "페이지네이션 정보 (기본값: 10개). 정렬은 sort 파라미터로 제어되며, Pageable의 sort는 무시됩니다.") pageable: Pageable | ||
| ): ResponseEntity<Page<ReadingRecordResponseV2>> | ||
|
|
||
| @Operation( | ||
| summary = "독서 기록 수정 (V2)", | ||
| description = "독서 기록을 수정합니다. 대분류 감정과 세부 감정을 변경할 수 있습니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "200", | ||
| description = "독서 기록 수정 성공", | ||
| content = [Content(schema = Schema(implementation = ReadingRecordResponseV2::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "403", | ||
| description = "해당 독서 기록에 대한 접근 권한이 없음", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "404", | ||
| description = "독서 기록을 찾을 수 없음", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ) | ||
| ] | ||
| ) | ||
| @PutMapping("/{readingRecordId}") | ||
| fun updateReadingRecord( | ||
| @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, | ||
| @PathVariable @Parameter(description = "수정할 독서 기록 ID") readingRecordId: UUID, | ||
| @Valid @RequestBody @Parameter(description = "독서 기록 수정 요청 객체") request: UpdateReadingRecordRequestV2 | ||
| ): ResponseEntity<ReadingRecordResponseV2> | ||
|
|
||
| @Operation( | ||
| summary = "독서 기록 삭제", | ||
| description = "독서 기록을 삭제합니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse(responseCode = "204", description = "독서 기록 삭제 성공"), | ||
| ApiResponse( | ||
| responseCode = "403", | ||
| description = "해당 독서 기록에 대한 접근 권한이 없음", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "404", | ||
| description = "독서 기록을 찾을 수 없음", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ) | ||
| ] | ||
| ) | ||
| @DeleteMapping("/{readingRecordId}") | ||
| fun deleteReadingRecord( | ||
| @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, | ||
| @PathVariable @Parameter(description = "삭제할 독서 기록 ID") readingRecordId: UUID | ||
| ): ResponseEntity<Unit> | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.