-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 기록 수정, 삭제 기능 추가 #94
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 all commits
c09baa6
0e7dea4
30d127a
f2dc790
18ec786
c916e72
fd429ff
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 |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ 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.CreateReadingRecordRequest | ||
| import org.yapp.apis.readingrecord.dto.request.UpdateReadingRecordRequest | ||
| import org.yapp.apis.readingrecord.dto.response.ReadingRecordPageResponse | ||
| import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse | ||
| import org.yapp.apis.readingrecord.dto.response.SeedStatsResponse | ||
|
|
@@ -131,4 +132,58 @@ interface ReadingRecordControllerApi { | |
| @AuthenticationPrincipal userId: UUID, | ||
| @PathVariable userBookId: UUID | ||
| ): ResponseEntity<SeedStatsResponse> | ||
|
|
||
| @Operation( | ||
| summary = "독서 기록 수정", | ||
| description = "독서 기록 ID로 독서 기록을 수정합니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "200", | ||
| 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))] | ||
| ) | ||
| ] | ||
| ) | ||
| @PatchMapping("/{readingRecordId}") | ||
| fun updateReadingRecord( | ||
| @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, | ||
| @PathVariable @Parameter(description = "수정할 독서 기록 ID") readingRecordId: UUID, | ||
| @Valid @RequestBody @Parameter(description = "독서 기록 수정 요청 객체") request: UpdateReadingRecordRequest | ||
| ): ResponseEntity<ReadingRecordResponse> | ||
|
|
||
|
Comment on lines
+160
to
+166
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) API 문서에 401 Unauthorized 응답을 추가하세요 인증이 필요한 엔드포인트이므로 401 응답을 명시하는 것이 좋습니다. @ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "독서 기록 수정 성공",
content = [Content(schema = Schema(implementation = ReadingRecordResponse::class))]
),
+ ApiResponse(
+ responseCode = "401",
+ description = "인증 필요",
+ content = [Content(schema = Schema(implementation = ErrorResponse::class))]
+ ),
ApiResponse(
responseCode = "400",
description = "잘못된 요청",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
),
🤖 Prompt for AI Agents |
||
| @Operation( | ||
| summary = "독서 기록 삭제", | ||
| description = "독서 기록 ID로 독서 기록을 삭제합니다." | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "204", | ||
| description = "독서 기록 삭제 성공" | ||
| ), | ||
| 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> | ||
| } | ||
|
Comment on lines
+184
to
189
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) 삭제 응답 타입은 ResponseEntity가 관례적입니다 삭제는 본문이 없으므로 - ): ResponseEntity<Unit>
+ ): ResponseEntity<Void>구현 클래스 변경 예시(다른 파일): // apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt
override fun deleteReadingRecord(
@AuthenticationPrincipal userId: UUID,
@PathVariable readingRecordId: UUID
): ResponseEntity<Void> {
readingRecordUseCase.deleteReadingRecord(userId, readingRecordId)
return ResponseEntity.noContent().build()
}🤖 Prompt for AI Agents |
||
|
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. 해당 request에서는 Validation이 없는데 의도하신건 지 궁금합니다!! 만약 없다면 @Valid 어노테이션 + valid function 패턴을 하는게 좋을 것 같아서요! |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||
| 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.Size | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @Schema( | ||||||||||||||||||||||||||||||||||
| name = "UpdateReadingRecordRequest", | ||||||||||||||||||||||||||||||||||
| description = "독서 기록 수정 요청", | ||||||||||||||||||||||||||||||||||
| example = """ | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| "pageNumber": 50, | ||||||||||||||||||||||||||||||||||
| "quote": "수정된 기억에 남는 문장입니다.", | ||||||||||||||||||||||||||||||||||
| "review": "수정된 감상평입니다.", | ||||||||||||||||||||||||||||||||||
| "emotionTags": ["놀라움"] | ||||||||||||||||||||||||||||||||||
|
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. 저희 예시에 놀라움 대신, 진짜 있는 태그로 하면 좋을 것 같아요! |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+17
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) OpenAPI 예시를 최대 3개 제약에 맞게 보완하세요 스키마 설명은 “최대 3개”이나 예시는 1개만 포함되어 있어 오해 소지가 있습니다. 2~3개의 예시를 넣어 제약을 명확히 전달하는 것을 권장합니다. 예시를 다음과 같이 조정: - "emotionTags": ["놀라움"]
+ "emotionTags": ["놀라움", "영감"]📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| data class UpdateReadingRecordRequest private constructor( | ||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
| @field:Min(1, message = "페이지 번호는 1 이상이어야 합니다.") | ||||||||||||||||||||||||||||||||||
| @field:Max(9999, message = "페이지 번호는 9999 이하여야 합니다.") | ||||||||||||||||||||||||||||||||||
| @field:Schema(description = "수정할 페이지 번호", example = "50") | ||||||||||||||||||||||||||||||||||
| val pageNumber: Int?, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.") | ||||||||||||||||||||||||||||||||||
| @field:Schema(description = "수정할 기억에 남는 문장", example = "수정된 기억에 남는 문장입니다.") | ||||||||||||||||||||||||||||||||||
| val quote: String?, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.") | ||||||||||||||||||||||||||||||||||
| @field:Schema(description = "수정할 감상평", example = "수정된 감상평입니다.") | ||||||||||||||||||||||||||||||||||
| val review: String?, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+33
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. 💡 Verification agent 🧩 Analysis chainquote/review의 공백 문자열 차단 필요 (도메인 규칙과 정합성 확보) 도메인에서 공백을 허용하지 않는다면 DTO 단계에서 @notblank로 조기에 검증하는 것이 좋습니다. 필드는 nullable이라 전달된 경우에만 제약이 적용됩니다. 적용 diff: @@
- @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.")
+ @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.")
+ @field:NotBlank(message = "기억에 남는 문장은 비어 있을 수 없습니다.")
@@
- @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.")
+ @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.")
+ @field:NotBlank(message = "감상평은 비어 있을 수 없습니다.")추가로 import가 필요합니다(아래 별도 코멘트 참조). 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "---- Searching for validation annotations usage ----"
rg -n "@field:NotBlank|@NotBlank|@field:Size|@Size|jakarta.validation|javax.validation|@field:Pattern|@Pattern" || true
echo
echo "---- Showing target file ----"
sed -n '1,240p' apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/UpdateReadingRecordRequest.kt || trueLength of output: 15245 Update DTO는 nullable이므로 @notblank 대신 "전달된 경우만 공백 차단"하는 검증 사용 권고 UpdateReadingRecordRequest의 quote/review는 nullable(선택) 필드이므로 @notblank를 붙이면 null(미전달)도 거부되어 업데이트 용도와 맞지 않습니다. 전달된 경우에만 공백(공백 문자만 있는 값)을 차단하려면 @field:Pattern(regexp = ".\S.") 처럼 패턴 검증을 사용하세요. 수정 대상
권장 적용 diff: @@
-import jakarta.validation.constraints.Size
+import jakarta.validation.constraints.Size
+import jakarta.validation.constraints.Pattern
@@
- @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.")
- @field:Schema(description = "수정할 기억에 남는 문장", example = "수정된 기억에 남는 문장입니다.")
- val quote: String?,
+ @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.")
+ @field:Pattern(regexp = ".*\\S.*", message = "기억에 남는 문장은 비어 있을 수 없습니다.")
+ @field:Schema(description = "수정할 기억에 남는 문장", example = "수정된 기억에 남는 문장입니다.")
+ val quote: String?,
@@
- @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.")
- @field:Schema(description = "수정할 감상평", example = "수정된 감상평입니다.")
- val review: String?,
+ @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.")
+ @field:Pattern(regexp = ".*\\S.*", message = "감상평은 비어 있을 수 없습니다.")
+ @field:Schema(description = "수정할 감상평", example = "수정된 감상평입니다.")
+ val review: String?,추가 유의사항: 만약 빈 문자열("")을 수신 시 자동으로 null로 정규화하려는 의도라면(@pattern 대신) 서비스 계층에서 "" → null로 변환하는 전략을 택하세요. 또한 현재 DTO의 validX() 메서드들이 !!를 사용하므로 null 가능성을 고려해 호출 위치를 확인하세요. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| @field:Size(max = 3, message = "감정 태그는 최대 3개까지 가능합니다.") | ||||||||||||||||||||||||||||||||||
| @field:Schema(description = "수정할 감정 태그 목록", example = """["따뜻함","즐거움","슬픔","깨달음"]""") | ||||||||||||||||||||||||||||||||||
| val emotionTags: List<@Size(max = 10, message = "감정 태그는 10자를 초과할 수 없습니다.") String>? | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| fun validPageNumber(): Int = pageNumber!! | ||||||||||||||||||||||||||||||||||
| fun validQuote(): String = quote!! | ||||||||||||||||||||||||||||||||||
| fun validReview(): String = review!! | ||||||||||||||||||||||||||||||||||
| fun validEmotionTags(): List<String> = emotionTags!! | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package org.yapp.apis.readingrecord.service | |
| import org.springframework.data.domain.Page | ||
| import org.springframework.data.domain.Pageable | ||
| import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest | ||
| import org.yapp.apis.readingrecord.dto.request.UpdateReadingRecordRequest | ||
| import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse | ||
| import org.yapp.domain.readingrecord.ReadingRecordDomainService | ||
| import org.yapp.domain.readingrecord.ReadingRecordSortType | ||
|
|
@@ -45,4 +46,24 @@ class ReadingRecordService( | |
| val page = readingRecordDomainService.findReadingRecordsByDynamicCondition(userBookId, sort, pageable) | ||
| return page.map { ReadingRecordResponse.from(it) } | ||
| } | ||
|
|
||
| fun updateReadingRecord( | ||
| readingRecordId: UUID, | ||
| request: UpdateReadingRecordRequest | ||
| ): ReadingRecordResponse { | ||
| val readingRecordInfoVO = readingRecordDomainService.modifyReadingRecord( | ||
| readingRecordId = readingRecordId, | ||
| pageNumber = request.validPageNumber(), | ||
| quote = request.validQuote(), | ||
| review = request.validReview(), | ||
| emotionTags = request.validEmotionTags() | ||
| ) | ||
| return ReadingRecordResponse.from(readingRecordInfoVO) | ||
| } | ||
|
Comment on lines
+50
to
+62
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. 🛠️ Refactor suggestion PATCH 부분 업데이트가 NPE로 실패할 수 있음: DTO 프로퍼티를 그대로 전달
적용 diff: val readingRecordInfoVO = readingRecordDomainService.modifyReadingRecord(
readingRecordId = readingRecordId,
- pageNumber = request.validPageNumber(),
- quote = request.validQuote(),
- review = request.validReview(),
- emotionTags = request.validEmotionTags()
+ pageNumber = request.pageNumber,
+ quote = request.quote,
+ review = request.review,
+ emotionTags = request.emotionTags
)참고: createReadingRecord 쪽도 동일 패턴을 사용 중이면, 동일하게 프로퍼티 접근으로 교체해 주세요(변경 범위 밖이라 예시만 첨부). 예시: readingRecordDomainService.createReadingRecord(
userBookId = userBookId,
pageNumber = request.pageNumber,
quote = request.quote,
review = request.review,
emotionTags = request.emotionTags
)🤖 Prompt for AI Agents |
||
|
|
||
| fun deleteReadingRecord( | ||
| readingRecordId: UUID | ||
| ) { | ||
| readingRecordDomainService.deleteReadingRecord(readingRecordId) | ||
| } | ||
|
Comment on lines
+64
to
+68
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. 💡 Verification agent 🧩 Analysis chain삭제 정책 점검: 멱등성, soft-delete 일관성, 권한 검증 경계
아래 스크립트로 관련 구현을 빠르게 확인할 수 있습니다(레포 루트에서 실행). 🏁 Script executed: #!/bin/bash
# deleteReadingRecord 구현 및 삭제/soft-delete 흔적 탐색
rg -n -A 12 -B 6 'deleteReadingRecord\(' --type kotlin
rg -n -A 4 -B 4 'deletedAt' --type kotlin
rg -n -A 6 -B 2 'decreaseReadingRecordCount\(|readingRecordCount' --type kotlin
# 하드 삭제 호출 여부
rg -n -A 2 -B 2 'deleteById\(' --type kotlinLength of output: 49912 🏁 Script executed: #!/bin/bash
set -euo pipefail
files=(
"domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt"
"domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt"
"domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt"
"infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt"
"infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt"
"domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt"
"domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt"
"domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt"
)
for f in "${files[@]}"; do
if [ -f "$f" ]; then
printf "\n==== FILE: %s ====\n\n" "$f"
sed -n '1,240p' "$f" || true
else
printf "\n==== FILE NOT FOUND: %s ====\n\n" "$f"
fi
doneLength of output: 29026 조치 필요: 삭제(soft-delete) 흐름은 확인됐으나 — 멱등성·권한 검증 보강 필요 요약:
주의가 필요한 위치:
권장 조치(간단히)
간단한 참고 코드 위치(검토/수정 권장):
수정이 필요한 상태입니다. |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -58,6 +58,21 @@ data class ReadingRecord private constructor( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun update( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageNumber: Int?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quote: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| review: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emotionTags: List<String>? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): ReadingRecord { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.copy( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageNumber = pageNumber?.let { PageNumber.newInstance(it) } ?: this.pageNumber, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quote = quote?.let { Quote.newInstance(it) } ?: this.quote, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| review = review?.let { Review.newInstance(it) } ?: this.review, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emotionTags = emotionTags?.map { EmotionTag.newInstance(it) } ?: this.emotionTags, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updatedAt = LocalDateTime.now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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) 시간 의존성 주입 고려 (테스트 용이성/일관성)
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
+74
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. 🛠️ Refactor suggestion 업데이트 입력값 정규화(트림/중복 제거) 및 no-op 최적화 제안
아래처럼 개선을 제안합니다. fun update(
pageNumber: Int?,
quote: String?,
review: String?,
emotionTags: List<String>?
): ReadingRecord {
- return this.copy(
- pageNumber = pageNumber?.let { PageNumber.newInstance(it) } ?: this.pageNumber,
- quote = quote?.let { Quote.newInstance(it) } ?: this.quote,
- review = review?.let { Review.newInstance(it) } ?: this.review,
- emotionTags = emotionTags?.map { EmotionTag.newInstance(it) } ?: this.emotionTags,
- updatedAt = LocalDateTime.now()
- )
+ if (pageNumber == null && quote == null && review == null && emotionTags == null) {
+ return this
+ }
+ return this.copy(
+ pageNumber = pageNumber?.let { PageNumber.newInstance(it) } ?: this.pageNumber,
+ quote = quote?.trim()?.let { Quote.newInstance(it) } ?: this.quote,
+ review = review?.trim()?.let { Review.newInstance(it) } ?: this.review,
+ emotionTags = emotionTags
+ ?.map { it.trim() }
+ ?.filter { it.isNotBlank() }
+ ?.distinct()
+ ?.map { EmotionTag.newInstance(it) }
+ ?: this.emotionTags,
+ updatedAt = LocalDateTime.now()
+ )
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @JvmInline | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value class Id(val value: UUID) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| companion object { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -114,4 +129,8 @@ data class ReadingRecord private constructor( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun delete(): ReadingRecord { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.copy(deletedAt = LocalDateTime.now()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+133
to
+135
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. 💡 Verification agent 🧩 Analysis chainsoft-delete 가드 및 updatedAt 동기화 권장 + 실제 삭제 로직과의 정합성 확인
제안 변경: - fun delete(): ReadingRecord {
- return this.copy(deletedAt = LocalDateTime.now())
- }
+ fun delete(): ReadingRecord {
+ require(deletedAt == null) { "이미 삭제된 독서 기록입니다." }
+ val now = LocalDateTime.now()
+ return this.copy(
+ deletedAt = now,
+ updatedAt = now
+ )
+ }다음 스크립트로 인프라/서비스 레이어에서 실제 삭제가 수행되는지, soft-delete와의 혼용 여부를 확인해 주세요. 🏁 Script executed: #!/bin/bash
# 삭제 관련 흐름 및 deletedAt 사용 검증
echo "== delete use-case/service 진입점 탐색 =="
rg -n -A 5 -B 3 "deleteReadingRecord\(" --type kotlin
echo
echo "== ReadingRecordRepository.deleteById 사용 여부 =="
rg -n -A 2 -B 2 "deleteById\(" --type kotlin
echo
echo "== QueryDSL에서 deletedAt.isNull() 필터 확인 =="
rg -n -A 3 -B 3 "deletedAt\.isNull\(\)" --type kotlin
echo
echo "== 삭제 시 태그 삭제 흐름 확인(태그 레포) =="
rg -n -A 3 -B 3 "deleteAllByReadingRecordId" --type kotlinLength of output: 14099 🏁 Script executed: #!/bin/bash
set -euo pipefail
files=(
"domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt"
"domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt"
"domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt"
"infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt"
"infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt"
"infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt"
"infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt"
)
for f in "${files[@]}"; do
echo "== FILE: $f =="
if [ -f "$f" ]; then
nl -ba -w3 -s': ' "$f"
else
echo "MISSING: $f"
fi
echo
doneLength of output: 1150 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== show ReadingRecord.kt =="
sed -n '1,240p' domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt || true
echo
echo "== show ReadingRecordDomainService.kt =="
sed -n '1,260p' domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt || true
echo
echo "== show ReadingRecordRepository.kt =="
sed -n '1,200p' domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt || true
echo
echo "== show infra ReadingRecordRepositoryImpl.kt =="
sed -n '1,200p' infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt || true
echo
echo "== show JpaReadingRecordQuerydslRepositoryImpl.kt =="
sed -n '1,240p' infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt || true
echo
echo "== show ReadingRecordTagRepositoryImpl.kt =="
sed -n '1,200p' infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt || true
echo
echo "== search for ReadingRecordTagEntity file(s) =="
rg -n "ReadingRecordTagEntity" || true
echo
echo "== show any ReadingRecordTagEntity file if found =="
for f in $(rg -n --files-with-matches "class .*ReadingRecordTagEntity" || true); do
echo "---- $f ----"
sed -n '1,240p' "$f" || true
doneLength of output: 23587 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate ReadingRecordEntity files =="
rg -n --files-with-matches "class .*ReadingRecordEntity" || true
echo
echo "== show ReadingRecordEntity (if found) =="
for f in $(rg --files-with-matches "class .*ReadingRecordEntity" || true); do
echo "---- $f ----"
sed -n '1,320p' "$f" || true
done
echo
echo "== show JpaReadingRecordRepository files =="
rg -n --files-with-matches "interface .*JpaReadingRecordRepository" || true
for f in $(rg --files-with-matches "interface .*JpaReadingRecordRepository" || true); do
echo "---- $f ----"
sed -n '1,240p' "$f" || true
done
echo
echo "== search for @SQLDelete usages =="
rg -n "@SQLDelete" || true
echo
echo "== search for deleted_at / deletedAt column in infra entities =="
rg -n "deletedAt|deleted_at" infra || trueLength of output: 8925 soft-delete 흐름 정합성 확인 — infra(@SQLDelete) 기반 soft-delete 사용 중, 도메인 delete()만 수정하면 반영되지 않습니다 검증 요약: ReadingRecordDomainService.deleteReadingRecord는 readingRecordRepository.deleteById(...)를 호출하고, 엔티티(ReadingRecordEntity / ReadingRecordTagEntity 등)에 @SQLDelete가 있어 JPA 레벨에서 DB의 deleted_at을 업데이트(soft-delete)합니다. 따라서 도메인 내 delete() 변경만으로는 실제 DB의 updated_at 동기화가 되지 않으며, 연관 태그 처리가 누락되어 있을 수 있습니다. 조치 제안(우선순위 순):
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
💡 Verification agent
🧩 Analysis chain
검색 결과 개인화 유지 여부 확인 및 선택적 사용자 컨텍스트 허용 제안
검색을 공개로 전환한 의도는 타당합니다. 다만 사용자가 인증된 경우에 한해 개인화(내 서재 상태 표기)를 유지하고 싶다면, 파라미터를 선택적(기본 null)로 열어두는 방식이 가장 간단합니다. 시그니처 호환성도 그대로 유지됩니다.
해당 범위 내 적용 diff:
컨트롤러는 인증이 있으면 userId를 전달하고(없으면 null), 게이트웨이에서는 permitAll 유지로 비인증 접근도 허용하는 구조가 됩니다. 제품 요구사항이 “항상 비개인화 응답”이라면 현 상태로도 무방합니다. 요구사항을 한 번만 확인해 주세요.
🏁 Script executed:
Length of output: 8225
searchBooks에 선택적 userId 파라미터 추가 제안 — 인증 시 개인화 유지, 비인증 시 비개인화 유지
검증 결과: 현재 searchBooks는 mergeWithUserBookStatus(..., null)를 호출해 항상 비개인화 응답을 반환합니다. 인증된 사용자에 한해 개인화(내 서재 상태 표기)를 유지하려면 userId를 선택적 파라미터로 열어두고 컨트롤러에서 인증 시 전달하도록 변경하는 것이 안전합니다.
수정이 필요한 위치:
적용 예시(diff):
운영 요구가 “항상 비개인화”라면 현재 상태 유지 가능하지만, 인증 시 개인화 허용이 필요하면 위 변경을 적용해 주세요.
📝 Committable suggestion
🤖 Prompt for AI Agents