From c74c1dff8c026db2fc0394a8e5efa5e949bda51b Mon Sep 17 00:00:00 2001 From: sispo3314 Date: Thu, 12 Feb 2026 04:10:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refact:=20=EB=AA=A9=ED=91=9C=20patch=20API?= =?UTF-8?q?=EC=9D=98=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/reading/presentation/dto/CreateReadingGoalRequest.kt | 0 .../{UpsertReadingGoalRequest.kt => UpdateReadingGoalRequest.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/CreateReadingGoalRequest.kt rename src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/{UpsertReadingGoalRequest.kt => UpdateReadingGoalRequest.kt} (100%) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/CreateReadingGoalRequest.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/CreateReadingGoalRequest.kt new file mode 100644 index 0000000..e69de29 diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpsertReadingGoalRequest.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpdateReadingGoalRequest.kt similarity index 100% rename from src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpsertReadingGoalRequest.kt rename to src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpdateReadingGoalRequest.kt From 28bab35f44b5de4625ebb93b828944c674210e64 Mon Sep 17 00:00:00 2001 From: sispo3314 Date: Thu, 12 Feb 2026 04:12:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refact:=20=EB=AA=A9=ED=91=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reading/presentation/ReadingController.kt | 88 ++++++++++++++----- .../dto/CreateReadingGoalRequest.kt | 21 +++++ .../dto/UpdateReadingGoalRequest.kt | 15 ++-- 3 files changed, 94 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/ReadingController.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/ReadingController.kt index 1146420..a7a3f0a 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/ReadingController.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/ReadingController.kt @@ -4,12 +4,13 @@ import com.stepbookstep.server.domain.book.domain.BookRepository import com.stepbookstep.server.domain.reading.application.ReadingGoalService import com.stepbookstep.server.domain.reading.application.ReadingLogService import com.stepbookstep.server.domain.reading.presentation.dto.BookReadingDetailResponse +import com.stepbookstep.server.domain.reading.presentation.dto.CreateReadingGoalRequest import com.stepbookstep.server.domain.reading.presentation.dto.CreateReadingLogRequest import com.stepbookstep.server.domain.reading.presentation.dto.CreateReadingLogResponse import com.stepbookstep.server.domain.reading.presentation.dto.ReadingGoalResponse import com.stepbookstep.server.domain.reading.presentation.dto.RoutineItem import com.stepbookstep.server.domain.reading.presentation.dto.RoutineListResponse -import com.stepbookstep.server.domain.reading.presentation.dto.UpsertReadingGoalRequest +import com.stepbookstep.server.domain.reading.presentation.dto.UpdateReadingGoalRequest import com.stepbookstep.server.global.response.ApiResponse import com.stepbookstep.server.global.response.CustomException import com.stepbookstep.server.global.response.ErrorCode @@ -20,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable @@ -65,31 +67,19 @@ class ReadingController( } @Operation( - summary = "독서 목표 생성/수정/삭제", + summary = "독서 목표 생성", description = """ - 독서 목표를 생성, 수정, 삭제합니다. - - 생성/수정: period, metric, targetAmount를 모두 포함 - - 삭제: delete=true 명시 + 새로운 독서 목표를 생성합니다. + - period, metric, targetAmount 모두 필수 + - 이미 활성 목표가 있으면 해당 목표를 수정합니다. """ ) - @PatchMapping("/books/{bookId}/goals") - fun upsertOrDeleteGoal( + @PostMapping("/books/{bookId}/goals") + fun createGoal( @Parameter(description = "도서 ID") @PathVariable bookId: Long, @Parameter(hidden = true) @LoginUserId userId: Long, - @Valid @RequestBody request: UpsertReadingGoalRequest - ): ResponseEntity> { - // 삭제 요청인 경우 - if (request.delete == true) { - readingGoalService.deleteGoal(userId, bookId) - return ResponseEntity.ok(ApiResponse.ok(null)) - } - - // 생성/수정 요청 - 필수 필드 검증 - if (request.period == null || request.metric == null || request.targetAmount == null) { - throw CustomException(ErrorCode.INVALID_INPUT) - } - - // 생성/수정 요청인 경우 + @Valid @RequestBody request: CreateReadingGoalRequest + ): ResponseEntity> { val goal = readingGoalService.upsertGoal( userId = userId, bookId = bookId, @@ -104,12 +94,64 @@ class ReadingController( val response = ReadingGoalResponse.from( goal = goalWithProgress.goal, currentProgress = goalWithProgress.currentProgress, - achievedAmount = goalWithProgress.achievedAmount // 추가! + achievedAmount = goalWithProgress.achievedAmount + ) + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.created(response)) + } + + @Operation( + summary = "독서 목표 수정", + description = """ + 기존 독서 목표를 수정합니다. + - period, metric, targetAmount 중 변경할 필드만 전달 가능 + - 활성 목표가 없으면 404 + """ + ) + @PatchMapping("/books/{bookId}/goals") + fun updateGoal( + @Parameter(description = "도서 ID") @PathVariable bookId: Long, + @Parameter(hidden = true) @LoginUserId userId: Long, + @Valid @RequestBody request: UpdateReadingGoalRequest + ): ResponseEntity> { + // 기존 활성 목표가 있어야 수정 가능 + val existing = readingGoalService.getActiveGoalWithProgress(userId, bookId) + ?: throw CustomException(ErrorCode.GOAL_NOT_FOUND) + + val goal = readingGoalService.upsertGoal( + userId = userId, + bookId = bookId, + period = request.period ?: existing.goal.period, + metric = request.metric ?: existing.goal.metric, + targetAmount = request.targetAmount ?: existing.goal.targetAmount + ) + + val goalWithProgress = readingGoalService.getActiveGoalWithProgress(userId, bookId) + ?: throw CustomException(ErrorCode.GOAL_NOT_FOUND) + + val response = ReadingGoalResponse.from( + goal = goalWithProgress.goal, + currentProgress = goalWithProgress.currentProgress, + achievedAmount = goalWithProgress.achievedAmount ) return ResponseEntity.ok(ApiResponse.ok(response)) } + @Operation( + summary = "독서 목표 삭제", + description = "활성 독서 목표를 삭제(비활성화)합니다." + ) + @DeleteMapping("/books/{bookId}/goals") + fun deleteGoal( + @Parameter(description = "도서 ID") @PathVariable bookId: Long, + @Parameter(hidden = true) @LoginUserId userId: Long + ): ResponseEntity> { + readingGoalService.deleteGoal(userId, bookId) + return ResponseEntity.ok(ApiResponse.ok(null)) + } + @Operation(summary = "책 목표 조회", description = "특정 책의 독서 목표를 조회합니다. 완독/중지 상태에서도 비활성화된 목표를 표시합니다.") @GetMapping("/books/{bookId}/goals") fun getGoal( @@ -122,7 +164,7 @@ class ReadingController( ReadingGoalResponse.from( goal = it.goal, currentProgress = it.currentProgress, - achievedAmount = it.achievedAmount // 추가! + achievedAmount = it.achievedAmount ) } diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/CreateReadingGoalRequest.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/CreateReadingGoalRequest.kt index e69de29..9e04d53 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/CreateReadingGoalRequest.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/CreateReadingGoalRequest.kt @@ -0,0 +1,21 @@ +package com.stepbookstep.server.domain.reading.presentation.dto + +import com.stepbookstep.server.domain.reading.domain.GoalMetric +import com.stepbookstep.server.domain.reading.domain.GoalPeriod +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive + +/** + * 독서 목표 생성 요청 + */ +data class CreateReadingGoalRequest( + @field:NotNull(message = "period는 필수입니다") + val period: GoalPeriod, + + @field:NotNull(message = "metric은 필수입니다") + val metric: GoalMetric, + + @field:NotNull(message = "targetAmount는 필수입니다") + @field:Positive(message = "targetAmount는 1 이상이어야 합니다") + val targetAmount: Int +) \ No newline at end of file diff --git a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpdateReadingGoalRequest.kt b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpdateReadingGoalRequest.kt index a715355..c266dab 100644 --- a/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpdateReadingGoalRequest.kt +++ b/src/main/kotlin/com/stepbookstep/server/domain/reading/presentation/dto/UpdateReadingGoalRequest.kt @@ -2,16 +2,17 @@ package com.stepbookstep.server.domain.reading.presentation.dto import com.stepbookstep.server.domain.reading.domain.GoalMetric import com.stepbookstep.server.domain.reading.domain.GoalPeriod +import jakarta.validation.constraints.Positive /** - * 독서 목표 생성/수정/삭제 요청 - * - * - 삭제: delete = true (다른 필드는 무시됨) - * - 생성/수정: period, metric, targetAmount 모두 필수 + * 독서 목표 수정 요청 + * - 변경할 필드만 전달 + * - 전달하지 않은 필드는 기존 값 유지 */ -data class UpsertReadingGoalRequest( +data class UpdateReadingGoalRequest( val period: GoalPeriod? = null, val metric: GoalMetric? = null, - val targetAmount: Int? = null, - val delete: Boolean = false + + @field:Positive(message = "targetAmount는 1 이상이어야 합니다") + val targetAmount: Int? = null ) \ No newline at end of file