From d8fd31a229f910a14ae1efdee65306f849e05cae Mon Sep 17 00:00:00 2001 From: laura-jung <166522604+laura-jung@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:29:42 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat(subtask):=20=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=ED=83=9C=EC=8A=A4=ED=81=AC=20=EC=99=84=EB=A3=8C=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EB=B3=80=EA=B2=BD=20dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/SubtaskStatusUpdateRequest.java | 8 ++++++++ .../dto/response/SubtaskStatusChangeResponse.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java create mode 100644 src/main/java/com/Timo/Timo/domain/todo/dto/response/SubtaskStatusChangeResponse.java diff --git a/src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java b/src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java new file mode 100644 index 00000000..b99d9958 --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java @@ -0,0 +1,8 @@ +package com.Timo.Timo.domain.todo.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record SubtaskStatusUpdateRequest( + @JsonProperty("isCompleted") + Boolean isCompleted +) { } diff --git a/src/main/java/com/Timo/Timo/domain/todo/dto/response/SubtaskStatusChangeResponse.java b/src/main/java/com/Timo/Timo/domain/todo/dto/response/SubtaskStatusChangeResponse.java new file mode 100644 index 00000000..8fbd72eb --- /dev/null +++ b/src/main/java/com/Timo/Timo/domain/todo/dto/response/SubtaskStatusChangeResponse.java @@ -0,0 +1,15 @@ +package com.Timo.Timo.domain.todo.dto.response; + +import com.Timo.Timo.domain.todo.entity.Subtask; + +public record SubtaskStatusChangeResponse( + Long subtaskId, + Boolean completed +) { + public static SubtaskStatusChangeResponse from(Subtask subtask) { + return new SubtaskStatusChangeResponse( + subtask.getId(), + subtask.isCompleted() + ); + } +} From a64656c74e763b9f2cbfc4ecaf460971f7365ae2 Mon Sep 17 00:00:00 2001 From: laura-jung <166522604+laura-jung@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:29:51 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(subtask):=20=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=ED=83=9C=EC=8A=A4=ED=81=AC=20=EC=99=84=EB=A3=8C=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/Timo/Timo/domain/todo/entity/Subtask.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/Timo/Timo/domain/todo/entity/Subtask.java b/src/main/java/com/Timo/Timo/domain/todo/entity/Subtask.java index ea51ba1a..1c8cc2b3 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/entity/Subtask.java +++ b/src/main/java/com/Timo/Timo/domain/todo/entity/Subtask.java @@ -50,4 +50,8 @@ public static Subtask of(String content, int sortOrder) { void assignTodo(Todo todo) { this.todo = todo; } + + public void updateCompleted(boolean completed) { + this.completed = completed; + } } From 78cbd7456770e6dc92c74afccb4375abc650387c Mon Sep 17 00:00:00 2001 From: laura-jung <166522604+laura-jung@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:30:01 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat(subtask):=20=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=ED=83=9C=EC=8A=A4=ED=81=AC=20=EC=99=84=EB=A3=8C=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EA=B4=80=EB=A0=A8=20=EC=9D=91=EB=8B=B5=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/Timo/Timo/domain/todo/exception/TodoErrorCode.java | 4 +++- .../com/Timo/Timo/domain/todo/exception/TodoSuccessCode.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java b/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java index 1de82e79..4d8263ed 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java +++ b/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java @@ -16,7 +16,9 @@ public enum TodoErrorCode implements BaseErrorCode { TODO_NOT_FOUND(HttpStatus.NOT_FOUND, "TODO_404", "존재하지 않는 TODO입니다."), MAX_COUNT_EXCEEDED(HttpStatus.CONFLICT, "TODO_409", "해당 날짜의 투두가 최대 개수(20개)를 초과했습니다."), IS_COMPLETED_REQUIRED(HttpStatus.BAD_REQUEST, "COMMON_400", "isCompleted는 필수입니다."), - TIMER_RUNNING(HttpStatus.CONFLICT, "TODO_409", "타이머가 실행 중인 TODO는 변경할 수 없습니다. 타이머를 먼저 종료해주세요."); + SUBTASK_NOT_FOUND(HttpStatus.NOT_FOUND, "TODO_404", "존재하지 않는 하위 투두입니다."), + TIMER_RUNNING(HttpStatus.CONFLICT, "TODO_409", "타이머가 실행 중인 TODO는 변경할 수 없습니다. 타이머를 먼저 종료해주세요."), + ; private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/Timo/Timo/domain/todo/exception/TodoSuccessCode.java b/src/main/java/com/Timo/Timo/domain/todo/exception/TodoSuccessCode.java index 96bdf57a..3c86c06f 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/exception/TodoSuccessCode.java +++ b/src/main/java/com/Timo/Timo/domain/todo/exception/TodoSuccessCode.java @@ -12,7 +12,9 @@ public enum TodoSuccessCode implements BaseSuccessCode { CREATED(HttpStatus.CREATED, "TODO가 생성되었습니다."), - STATUS_CHANGED(HttpStatus.OK, "TODO 상태가 변경되었습니다."); + STATUS_CHANGED(HttpStatus.OK, "TODO 상태가 변경되었습니다."), + SUBTASK_STATUS_CHANGED(HttpStatus.OK, "하위 태스크 상태가 변경되었습니다."), + ; private final HttpStatus httpStatus; private final String message; From d14471164d76c30a6358899a0170eea3b434b739 Mon Sep 17 00:00:00 2001 From: laura-jung <166522604+laura-jung@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:30:09 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat(subtask):=20=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=ED=83=9C=EC=8A=A4=ED=81=AC=20=EC=99=84=EB=A3=8C=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Timo/domain/todo/service/TodoService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java b/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java index 3beab77a..96f4aed4 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java +++ b/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java @@ -10,10 +10,13 @@ import com.Timo.Timo.domain.tag.exception.TagErrorCode; import com.Timo.Timo.domain.tag.repository.TagRepository; import com.Timo.Timo.domain.timer.service.TimerService; +import com.Timo.Timo.domain.todo.dto.request.SubtaskStatusUpdateRequest; import com.Timo.Timo.domain.todo.dto.request.TodoCreateRequest; import com.Timo.Timo.domain.todo.dto.request.TodoStatusUpdateRequest; +import com.Timo.Timo.domain.todo.dto.response.SubtaskStatusChangeResponse; import com.Timo.Timo.domain.todo.dto.response.TodoCreateResponse; import com.Timo.Timo.domain.todo.dto.response.TodoStatusChangeResponse; +import com.Timo.Timo.domain.todo.entity.Subtask; import com.Timo.Timo.domain.todo.entity.Todo; import com.Timo.Timo.domain.todo.entity.TodoInstance; import com.Timo.Timo.domain.todo.enums.RepeatType; @@ -96,6 +99,25 @@ public TodoStatusChangeResponse changeCompletion(Long userId, Long todoId, TodoS return TodoStatusChangeResponse.from(todoId, instance); } + @Transactional + public SubtaskStatusChangeResponse changeSubtaskCompletion( + Long userId, Long todoId, Long subtaskId, SubtaskStatusUpdateRequest request) { + if (request.isCompleted() == null) { + throw new CustomException(TodoErrorCode.IS_COMPLETED_REQUIRED); + } + + Todo todo = todoRepository.findByIdAndUser_Id(todoId, userId) + .orElseThrow(() -> new CustomException(TodoErrorCode.TODO_NOT_FOUND)); + + Subtask subtask = todo.getSubtasks().stream() + .filter(it -> it.getId().equals(subtaskId)) + .findFirst() + .orElseThrow(() -> new CustomException(TodoErrorCode.SUBTASK_NOT_FOUND)); + + subtask.updateCompleted(request.isCompleted()); + return SubtaskStatusChangeResponse.from(subtask); + } + private LocalDate resolveDate(Long userId, LocalDate requestedDate) { if (requestedDate != null) { return requestedDate; From 9c483cc74ebddd11b5290d0da1437972e5112156 Mon Sep 17 00:00:00 2001 From: laura-jung <166522604+laura-jung@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:30:22 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat(subtask):=20=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=ED=83=9C=EC=8A=A4=ED=81=AC=20=EC=99=84=EB=A3=8C=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20controller=20=EB=B0=8F=20swagger=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo/controller/TodoController.java | 18 +++++ .../domain/todo/docs/TodoControllerDocs.java | 74 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/main/java/com/Timo/Timo/domain/todo/controller/TodoController.java b/src/main/java/com/Timo/Timo/domain/todo/controller/TodoController.java index 967b4901..a1bff7fc 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/controller/TodoController.java +++ b/src/main/java/com/Timo/Timo/domain/todo/controller/TodoController.java @@ -10,8 +10,10 @@ import org.springframework.web.bind.annotation.RestController; import com.Timo.Timo.domain.todo.docs.TodoControllerDocs; + import com.Timo.Timo.domain.todo.dto.request.SubtaskStatusUpdateRequest; import com.Timo.Timo.domain.todo.dto.request.TodoCreateRequest; import com.Timo.Timo.domain.todo.dto.request.TodoStatusUpdateRequest; +import com.Timo.Timo.domain.todo.dto.response.SubtaskStatusChangeResponse; import com.Timo.Timo.domain.todo.dto.response.TodoCreateResponse; import com.Timo.Timo.domain.todo.dto.response.TodoStatusChangeResponse; import com.Timo.Timo.domain.todo.exception.TodoSuccessCode; @@ -57,4 +59,20 @@ public ResponseEntity> changeTodoStatus( .status(TodoSuccessCode.STATUS_CHANGED.getHttpStatus()) .body(BaseResponse.onSuccess(TodoSuccessCode.STATUS_CHANGED, response)); } + + @Override + @PatchMapping("/{todoId}/subtasks/{subtaskId}/status") + public ResponseEntity> changeSubtaskStatus( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long todoId, + @PathVariable Long subtaskId, + @Valid @RequestBody SubtaskStatusUpdateRequest request + ) { + SubtaskStatusChangeResponse response = + todoService.changeSubtaskCompletion(userDetails.getUserId(), todoId, subtaskId, request); + + return ResponseEntity + .status(TodoSuccessCode.SUBTASK_STATUS_CHANGED.getHttpStatus()) + .body(BaseResponse.onSuccess(TodoSuccessCode.SUBTASK_STATUS_CHANGED, response)); + } } diff --git a/src/main/java/com/Timo/Timo/domain/todo/docs/TodoControllerDocs.java b/src/main/java/com/Timo/Timo/domain/todo/docs/TodoControllerDocs.java index 3c008189..465b8ea7 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/docs/TodoControllerDocs.java +++ b/src/main/java/com/Timo/Timo/domain/todo/docs/TodoControllerDocs.java @@ -2,8 +2,10 @@ import org.springframework.http.ResponseEntity; +import com.Timo.Timo.domain.todo.dto.request.SubtaskStatusUpdateRequest; import com.Timo.Timo.domain.todo.dto.request.TodoCreateRequest; import com.Timo.Timo.domain.todo.dto.request.TodoStatusUpdateRequest; +import com.Timo.Timo.domain.todo.dto.response.SubtaskStatusChangeResponse; import com.Timo.Timo.domain.todo.dto.response.TodoCreateResponse; import com.Timo.Timo.domain.todo.dto.response.TodoStatusChangeResponse; import com.Timo.Timo.global.auth.principal.CustomUserDetails; @@ -197,4 +199,76 @@ ResponseEntity> changeTodoStatus( @Parameter(description = "대상 TODO ID", example = "145") Long todoId, TodoStatusUpdateRequest request ); + + @Operation( + summary = "하위 태스크 완료 상태 변경", + description = """ + 하위 태스크의 완료 여부를 변경합니다. + + todoId로 소유한 TODO를 확인한 뒤, 해당 TODO에 속한 subtaskId의 완료 상태를 변경합니다. + + Swagger UI 오른쪽 위의 Authorize 버튼을 눌러 유효한 Access Token을 입력해야 합니다. + """ + ) + @RequestBody( + required = true, + description = "하위 태스크 완료 상태 변경 요청", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = SubtaskStatusUpdateRequest.class), + examples = @ExampleObject( + name = "하위 태스크 완료 상태 변경 요청 예시", + value = """ + { + "isCompleted": true + } + """ + ) + ) + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "하위 태스크 완료 상태 변경 성공", + useReturnTypeSchema = true + ), + @ApiResponse( + responseCode = "400", + description = "isCompleted가 누락된 경우", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDto.class) + ) + ), + @ApiResponse( + responseCode = "401", + description = "Access Token이 없거나 만료되었거나 유효하지 않은 경우", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDto.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 TODO이거나 해당 TODO에 속하지 않는 하위 태스크인 경우", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDto.class) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDto.class) + ) + ) + }) + ResponseEntity> changeSubtaskStatus( + @Parameter(hidden = true) CustomUserDetails userDetails, + @Parameter(description = "대상 TODO ID", example = "5") Long todoId, + @Parameter(description = "대상 하위 태스크 ID", example = "2") Long subtaskId, + SubtaskStatusUpdateRequest request + ); } From be3ba02dbcefc387668351f040a8529d2cbe7302 Mon Sep 17 00:00:00 2001 From: laura-jung <166522604+laura-jung@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:47:21 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20notnull=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo/dto/request/SubtaskStatusUpdateRequest.java | 3 +++ .../domain/todo/dto/request/TodoStatusUpdateRequest.java | 3 +++ .../Timo/Timo/domain/todo/exception/TodoErrorCode.java | 1 - .../com/Timo/Timo/domain/todo/service/TodoService.java | 8 -------- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java b/src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java index b99d9958..0d87c51c 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java +++ b/src/main/java/com/Timo/Timo/domain/todo/dto/request/SubtaskStatusUpdateRequest.java @@ -2,7 +2,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; + public record SubtaskStatusUpdateRequest( + @NotNull @JsonProperty("isCompleted") Boolean isCompleted ) { } diff --git a/src/main/java/com/Timo/Timo/domain/todo/dto/request/TodoStatusUpdateRequest.java b/src/main/java/com/Timo/Timo/domain/todo/dto/request/TodoStatusUpdateRequest.java index cf359cbc..30be5632 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/dto/request/TodoStatusUpdateRequest.java +++ b/src/main/java/com/Timo/Timo/domain/todo/dto/request/TodoStatusUpdateRequest.java @@ -4,7 +4,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; + public record TodoStatusUpdateRequest( + @NotNull @JsonProperty("isCompleted") Boolean isCompleted, diff --git a/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java b/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java index 4d8263ed..327ad3a0 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java +++ b/src/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java @@ -15,7 +15,6 @@ public enum TodoErrorCode implements BaseErrorCode { INVALID_TITLE(HttpStatus.BAD_REQUEST, "TODO_400", "투두명은 한국어 20자/영어 30자를 초과할 수 없습니다."), TODO_NOT_FOUND(HttpStatus.NOT_FOUND, "TODO_404", "존재하지 않는 TODO입니다."), MAX_COUNT_EXCEEDED(HttpStatus.CONFLICT, "TODO_409", "해당 날짜의 투두가 최대 개수(20개)를 초과했습니다."), - IS_COMPLETED_REQUIRED(HttpStatus.BAD_REQUEST, "COMMON_400", "isCompleted는 필수입니다."), SUBTASK_NOT_FOUND(HttpStatus.NOT_FOUND, "TODO_404", "존재하지 않는 하위 투두입니다."), TIMER_RUNNING(HttpStatus.CONFLICT, "TODO_409", "타이머가 실행 중인 TODO는 변경할 수 없습니다. 타이머를 먼저 종료해주세요."), ; diff --git a/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java b/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java index 96f4aed4..b7ac695e 100644 --- a/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java +++ b/src/main/java/com/Timo/Timo/domain/todo/service/TodoService.java @@ -79,10 +79,6 @@ public TodoCreateResponse createTodo(Long userId, TodoCreateRequest request) { @Transactional public TodoStatusChangeResponse changeCompletion(Long userId, Long todoId, TodoStatusUpdateRequest request) { - if (request.isCompleted() == null) { - throw new CustomException(TodoErrorCode.IS_COMPLETED_REQUIRED); - } - Todo todo = todoRepository.findByIdAndUser_Id(todoId, userId) .orElseThrow(() -> new CustomException(TodoErrorCode.TODO_NOT_FOUND)); @@ -102,10 +98,6 @@ public TodoStatusChangeResponse changeCompletion(Long userId, Long todoId, TodoS @Transactional public SubtaskStatusChangeResponse changeSubtaskCompletion( Long userId, Long todoId, Long subtaskId, SubtaskStatusUpdateRequest request) { - if (request.isCompleted() == null) { - throw new CustomException(TodoErrorCode.IS_COMPLETED_REQUIRED); - } - Todo todo = todoRepository.findByIdAndUser_Id(todoId, userId) .orElseThrow(() -> new CustomException(TodoErrorCode.TODO_NOT_FOUND));