From d83c554a8983fd5c9001612ea90f15ccaaf97c3a Mon Sep 17 00:00:00 2001 From: "jay.park" Date: Thu, 17 Jul 2025 19:25:42 +0900 Subject: [PATCH] feat: create schedule v2 --- .../api/controller/v2/ScheduleControllerV2.kt | 40 ++++ .../v2/docs/ScheduleControllerDocsV2.kt | 185 ++++++++++++++++++ .../controller/v2/request/ScheduleRequest.kt | 68 +++++++ .../schedule/ScheduleApplicationService.kt | 64 ++++++ .../core/domain/schedule/Schedule.kt | 26 ++- 5 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/ScheduleControllerV2.kt create mode 100644 noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/docs/ScheduleControllerDocsV2.kt create mode 100644 noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/request/ScheduleRequest.kt diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/ScheduleControllerV2.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/ScheduleControllerV2.kt new file mode 100644 index 0000000..86779b5 --- /dev/null +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/ScheduleControllerV2.kt @@ -0,0 +1,40 @@ +package noweekend.core.api.controller.v2 + +import noweekend.core.api.controller.v1.response.ScheduleResponse +import noweekend.core.api.controller.v2.docs.ScheduleControllerDocsV2 +import noweekend.core.api.controller.v2.request.ScheduleCreateRequestV2 +import noweekend.core.api.controller.v2.request.ScheduleUpdateRequestV2 +import noweekend.core.api.security.annotations.CurrentUserId +import noweekend.core.api.service.schedule.ScheduleApplicationService +import noweekend.core.support.response.ApiResponse +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +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 + +@RestController +@RequestMapping("/api/v2/schedule") +class ScheduleControllerV2( + private val scheduleApplicationService: ScheduleApplicationService, +) : ScheduleControllerDocsV2 { + + @PostMapping + override fun createSchedule( + @CurrentUserId userId: String, + @Validated @RequestBody request: ScheduleCreateRequestV2, + ): ApiResponse { + return ApiResponse.success(scheduleApplicationService.createScheduleV2(userId, request)) + } + + @PutMapping("/{id}") + override fun updateSchedule( + @CurrentUserId userId: String, + @PathVariable id: String, + @Validated @RequestBody request: ScheduleUpdateRequestV2, + ): ApiResponse { + return ApiResponse.success(scheduleApplicationService.updateScheduleV2(userId, id, request)) + } +} diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/docs/ScheduleControllerDocsV2.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/docs/ScheduleControllerDocsV2.kt new file mode 100644 index 0000000..89f73e2 --- /dev/null +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/docs/ScheduleControllerDocsV2.kt @@ -0,0 +1,185 @@ +package noweekend.core.api.controller.v2.docs + +import io.swagger.v3.oas.annotations.Operation +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.parameters.RequestBody +import noweekend.core.api.controller.v1.response.ScheduleResponse +import noweekend.core.api.controller.v2.request.ScheduleCreateRequestV2 +import noweekend.core.api.controller.v2.request.ScheduleUpdateRequestV2 +import noweekend.core.support.response.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse + +interface ScheduleControllerDocsV2 { + + @Operation( + summary = "캘린더: 일정 생성", + description = "일정을 생성합니다.", + requestBody = RequestBody( + required = true, + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ScheduleCreateRequestV2::class), + examples = [ + ExampleObject( + name = "예시 요청", + value = """ +{ + "title": "회의", + "startDateTime": "2024-05-27 10:00:00", + "endDateTime": "2024-05-27 11:00:00", + "category": "COMPANY", + "temperature": 3, + "alarmOption": "FIFTEEN_MINUTES_BEFORE" +} +""", + ), + ], + ), + ], + ), + responses = [ + SwaggerApiResponse( + responseCode = "200", + description = "일정 생성 성공", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ApiResponse::class), + examples = [ + ExampleObject( + name = "예시 응답", + value = """ +{ + "result": "SUCCESS", + "data": { + "id": "abc123", + "title": "회의", + "startDateTime": "2025-05-01T10:00:00", + "endDateTime": "2025-05-01T11:00:00", + "category": "COMPANY", + "temperature": 3, + "alarmOption": "FIFTEEN_MINUTES_BEFORE", + "completed": false + }, + "error": null +} +""", + ), + ], + ), + ], + ), + SwaggerApiResponse( + responseCode = "400", + description = "잘못된 요청", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ApiResponse::class), + examples = [ + ExampleObject( + name = "예시 응답", + value = """ +{ + "result": "ERROR", + "data": null, + "error": { + "code": "INVALID_PARAMETER", + "message": "올바르지 않은 요청입니다.", + "data": {} + } +} +""", + ), + ], + ), + ], + ), + ], + ) + fun createSchedule( + @Schema(hidden = true) userId: String, + request: ScheduleCreateRequestV2, + ): ApiResponse + + @Operation( + summary = "캘린더: 일정 수정", + description = "일정의 시작/종료 시간, 카테고리, 온도, 알람 옵션을 수정합니다.", + requestBody = RequestBody( + required = true, + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ScheduleUpdateRequestV2::class), + ), + ], + ), + responses = [ + SwaggerApiResponse( + responseCode = "200", + description = "일정 수정 성공", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ApiResponse::class), + examples = [ + ExampleObject( + name = "예시 응답", + value = """ +{ + "result": "SUCCESS", + "data": { + "id": "abc123", + "title": "회의", + "startDateTime": "2025-05-01T10:00:00", + "endDateTime": "2025-05-01T11:00:00", + "category": "COMPANY", + "temperature": 3, + "alarmOption": "FIFTEEN_MINUTES_BEFORE", + "completed": false + }, + "error": null +} +""", + ), + ], + ), + ], + ), + SwaggerApiResponse( + responseCode = "400", + description = "잘못된 요청", + content = [ + Content( + mediaType = "application/json", + schema = Schema(implementation = ApiResponse::class), + examples = [ + ExampleObject( + name = "예시 응답", + value = """ +{ + "result": "ERROR", + "data": null, + "error": { + "code": "INVALID_PARAMETER", + "message": "올바르지 않은 요청입니다.", + "data": {} + } +} +""", + ), + ], + ), + ], + ), + ], + ) + fun updateSchedule( + @Schema(hidden = true) userId: String, + id: String, + request: ScheduleUpdateRequestV2, + ): ApiResponse +} diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/request/ScheduleRequest.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/request/ScheduleRequest.kt new file mode 100644 index 0000000..6db50e5 --- /dev/null +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/controller/v2/request/ScheduleRequest.kt @@ -0,0 +1,68 @@ +package noweekend.core.api.controller.v2.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Positive +import noweekend.core.domain.enumerate.AlarmOption +import noweekend.core.domain.enumerate.ScheduleCategory +import java.time.LocalDateTime + +@Schema(description = "일정 생성 요청") +data class ScheduleCreateRequestV2( + @field:NotBlank(message = "제목은 필수입니다.") + @Schema(description = "제목") + val title: String, + + @field:NotNull(message = "시작 시간은 필수입니다.") + @Schema(description = "시작 시간") + val startDateTime: LocalDateTime, + + @field:NotNull(message = "종료 시간은 필수입니다.") + @Schema(description = "종료 시간") + val endDateTime: LocalDateTime, + + @field:NotNull(message = "카테고리는 필수입니다.") + @Schema(description = "카테고리") + val category: ScheduleCategory, + + @field:NotNull(message = "온도는 필수입니다.") + @field:Positive(message = "온도는 양수여야 합니다.") + @field:Max(value = 100, message = "온도는 100 이하여야 합니다.") + @Schema(description = "온도 (감정 등 표현)") + val temperature: Int, + + @field:NotNull(message = "알람 설정은 필수입니다.") + @Schema(description = "알람 설정") + val alarmOption: AlarmOption, +) + +@Schema(description = "일정 수정 요청") +data class ScheduleUpdateRequestV2( + @field:NotBlank(message = "제목은 필수입니다.") + @Schema(description = "제목") + val title: String, + + @field:NotNull(message = "시작 시간은 필수입니다.") + @Schema(description = "시작 시간") + val startDateTime: LocalDateTime, + + @field:NotNull(message = "종료 시간은 필수입니다.") + @Schema(description = "종료 시간") + val endDateTime: LocalDateTime, + + @field:NotNull(message = "카테고리는 필수입니다.") + @Schema(description = "카테고리") + val category: ScheduleCategory, + + @field:NotNull(message = "온도는 필수입니다.") + @field:Positive(message = "온도는 양수여야 합니다.") + @field:Max(value = 100, message = "온도는 100 이하여야 합니다.") + @Schema(description = "온도 (감정 등 표현)") + val temperature: Int, + + @field:NotNull(message = "알람 설정은 필수입니다.") + @Schema(description = "알람 설정") + val alarmOption: AlarmOption, +) diff --git a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/service/schedule/ScheduleApplicationService.kt b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/service/schedule/ScheduleApplicationService.kt index ffeab17..1db710d 100644 --- a/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/service/schedule/ScheduleApplicationService.kt +++ b/noweekend-core/core-api/src/main/kotlin/noweekend/core/api/service/schedule/ScheduleApplicationService.kt @@ -4,6 +4,8 @@ import noweekend.core.api.controller.v1.request.ScheduleCreateRequest import noweekend.core.api.controller.v1.request.ScheduleUpdateRequest import noweekend.core.api.controller.v1.response.DailyScheduleResponse import noweekend.core.api.controller.v1.response.ScheduleResponse +import noweekend.core.api.controller.v2.request.ScheduleCreateRequestV2 +import noweekend.core.api.controller.v2.request.ScheduleUpdateRequestV2 import noweekend.core.domain.schedule.Schedule import noweekend.core.domain.schedule.ScheduleReader import noweekend.core.domain.schedule.ScheduleWriter @@ -25,12 +27,23 @@ interface ScheduleApplicationService { request: ScheduleCreateRequest, ): ScheduleResponse + fun createScheduleV2( + userId: String, + request: ScheduleCreateRequestV2, + ): ScheduleResponse + fun updateSchedule( userId: String, scheduleId: String, request: ScheduleUpdateRequest, ): ScheduleResponse + fun updateScheduleV2( + userId: String, + scheduleId: String, + request: ScheduleUpdateRequestV2, + ): ScheduleResponse + fun updateScheduleState( userId: String, scheduleId: String, @@ -108,6 +121,28 @@ class ScheduleApplicationServiceImpl( return savedSchedule.toResponse() } + override fun createScheduleV2( + userId: String, + request: ScheduleCreateRequestV2, + ): ScheduleResponse { + if (request.startDateTime > request.endDateTime) { + throw CoreException(ErrorType.INVALID_PARAMETER, "startDateTime must greater than endDateTime") + } + + val schedule = Schedule.newScheduleV2( + userId = userId, + title = request.title, + startTime = request.startDateTime, + endTime = request.startDateTime, + category = request.category, + temperature = request.temperature, + alarmOption = request.alarmOption, + ) + + val savedSchedule = scheduleWriter.save(schedule) + return savedSchedule.toResponse() + } + override fun updateSchedule( userId: String, scheduleId: String, @@ -147,6 +182,35 @@ class ScheduleApplicationServiceImpl( return savedSchedule.toResponse() } + override fun updateScheduleV2( + userId: String, + scheduleId: String, + request: ScheduleUpdateRequestV2, + ): ScheduleResponse { + val existingSchedule = scheduleReader.findScheduleById(scheduleId) + ?: throw CoreException(ErrorType.NOT_FOUND_ERROR, "Schedule not found: $scheduleId") + + if (existingSchedule.userId != userId) { + throw CoreException(ErrorType.FORBIDDEN_ERROR, "You don't have permission to update this schedule") + } + + if (request.startDateTime > request.endDateTime) { + throw CoreException(ErrorType.INVALID_PARAMETER, "startDateTime must greater than endDateTime") + } + + val updatedSchedule = existingSchedule.copy( + title = request.title, + startTime = request.startDateTime, + endTime = request.endDateTime, + category = request.category, + temperature = request.temperature, + alarmOption = request.alarmOption, + ) + + val savedSchedule = scheduleWriter.update(updatedSchedule) + return savedSchedule.toResponse() + } + override fun updateScheduleState( userId: String, scheduleId: String, diff --git a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/schedule/Schedule.kt b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/schedule/Schedule.kt index 507bf83..0dcabc1 100644 --- a/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/schedule/Schedule.kt +++ b/noweekend-core/core-domain/src/main/kotlin/noweekend/core/domain/schedule/Schedule.kt @@ -13,7 +13,7 @@ data class Schedule( val endTime: LocalDateTime, val category: ScheduleCategory, val temperature: Int, - val allDay: Boolean, + val allDay: Boolean = false, val alarmOption: AlarmOption, val completed: Boolean, val createdAt: LocalDateTime?, @@ -45,5 +45,29 @@ data class Schedule( updatedAt = null, ) } + + fun newScheduleV2( + userId: String, + title: String, + startTime: LocalDateTime, + endTime: LocalDateTime, + category: ScheduleCategory, + temperature: Int, + alarmOption: AlarmOption, + ): Schedule { + return Schedule( + id = IdGenerator.generate(), + userId = userId, + title = title, + startTime = startTime, + endTime = endTime, + category = category, + temperature = temperature, + alarmOption = alarmOption, + completed = false, + createdAt = null, + updatedAt = null, + ) + } } }