From b701bebfcb254adbd166b473343baeba85fda2fd Mon Sep 17 00:00:00 2001 From: robinjoon Date: Fri, 6 Mar 2026 20:03:53 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Task=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20Mutation=20=EC=B6=94=EA=B0=80=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 본인 소유 할일의 제목을 수정할 수 있는 updateTaskTitle mutation 추가. 소유권 검증 포함, updateStatus와 동일한 패턴 적용. Co-Authored-By: Claude Opus 4.6 --- docs/plan/#21-task-title-update/checklist.md | 11 ++++++ docs/plan/#21-task-title-update/plan.md | 12 ++++++ .../task/application/service/TaskService.kt | 14 +++++++ .../loop/task/domain/model/TaskCommand.kt | 5 +++ .../task/domain/repository/TaskRepository.kt | 2 + .../persistence/ExposedTaskRepository.kt | 9 +++++ .../datafetcher/TaskDataFetcher.kt | 14 +++++++ src/main/resources/schema/task.graphqls | 14 +++++++ .../application/service/TaskServiceTest.kt | 39 +++++++++++++++++++ 9 files changed, 120 insertions(+) create mode 100644 docs/plan/#21-task-title-update/checklist.md create mode 100644 docs/plan/#21-task-title-update/plan.md diff --git a/docs/plan/#21-task-title-update/checklist.md b/docs/plan/#21-task-title-update/checklist.md new file mode 100644 index 0000000..4861432 --- /dev/null +++ b/docs/plan/#21-task-title-update/checklist.md @@ -0,0 +1,11 @@ +# Task 제목 수정 검증 체크리스트 + +## 필수 항목 +- [x] 아키텍처 원칙 준수 (docs/architecture.md 기준) +- [x] 레이어 의존성 규칙 위반 없음 +- [x] 테스트 코드 작성 완료 (Domain, Application 필수) +- [x] 모든 테스트 통과 +- [x] 기존 테스트 깨지지 않음 + +## 선택 항목 +- [x] 소유권 검증 (본인 할일만 수정 가능) diff --git a/docs/plan/#21-task-title-update/plan.md b/docs/plan/#21-task-title-update/plan.md new file mode 100644 index 0000000..30cb072 --- /dev/null +++ b/docs/plan/#21-task-title-update/plan.md @@ -0,0 +1,12 @@ +# Task 제목 수정 Mutation 추가 계획 + +> Issue: #21 + +## 단계 + +- [x] 1단계: Domain — TaskCommand.UpdateTitle 추가 +- [x] 2단계: Domain — TaskRepository.updateTitle 추가 +- [x] 3단계: Application — TaskService.updateTitle TDD (RED → GREEN → REFACTOR) +- [x] 4단계: Infrastructure — ExposedTaskRepository.updateTitle 구현 +- [x] 5단계: Presentation — GraphQL 스키마 + DataFetcher 추가 +- [x] 6단계: DGS Codegen 빌드 및 전체 테스트 검증 diff --git a/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt b/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt index 9df1635..01c150b 100644 --- a/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt +++ b/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt @@ -34,6 +34,20 @@ class TaskService( return taskRepository.updateStatus(command) } + @Transactional + fun updateTitle( + command: TaskCommand.UpdateTitle, + memberId: MemberId, + ): Task { + val task = + taskRepository.findById(command.taskId) + ?: throw EntityNotFoundException("Task not found: ${command.taskId.value}") + if (!task.isOwnedBy(memberId)) { + throw AccessDeniedException("Task does not belong to member: ${memberId.value}") + } + return taskRepository.updateTitle(command) + } + @Transactional fun delete( command: TaskCommand.Delete, diff --git a/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt b/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt index 4f52a39..c81ff99 100644 --- a/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt +++ b/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt @@ -18,6 +18,11 @@ sealed interface TaskCommand { val status: TaskStatus, ) : TaskCommand + data class UpdateTitle( + val taskId: TaskId, + val title: TaskTitle, + ) : TaskCommand + data class Delete( val taskId: TaskId, ) : TaskCommand diff --git a/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt b/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt index 1c33bf0..c45c9ad 100644 --- a/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt +++ b/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt @@ -10,6 +10,8 @@ interface TaskRepository { fun updateStatus(command: TaskCommand.UpdateStatus): Task + fun updateTitle(command: TaskCommand.UpdateTitle): Task + fun delete(command: TaskCommand.Delete) fun findAll(query: TaskQuery): List diff --git a/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt b/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt index 98ec23c..3110cba 100644 --- a/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt +++ b/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt @@ -57,6 +57,15 @@ class ExposedTaskRepository : TaskRepository { return findById(command.taskId)!! } + override fun updateTitle(command: TaskCommand.UpdateTitle): Task { + val now = OffsetDateTime.now() + TaskTable.update({ TaskTable.taskId eq command.taskId.value }) { + it[title] = command.title.value + it[updatedAt] = now + } + return findById(command.taskId)!! + } + override fun delete(command: TaskCommand.Delete) { TaskTable.deleteWhere { taskId eq command.taskId.value } } diff --git a/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt b/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt index 38ef7de..1205c77 100644 --- a/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt +++ b/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt @@ -8,6 +8,7 @@ import kotlinx.datetime.LocalDate import kr.io.team.loop.codegen.types.CreateTaskInput import kr.io.team.loop.codegen.types.TaskFilter import kr.io.team.loop.codegen.types.UpdateTaskStatusInput +import kr.io.team.loop.codegen.types.UpdateTaskTitleInput import kr.io.team.loop.common.config.Authorize import kr.io.team.loop.common.domain.GoalId import kr.io.team.loop.common.domain.MemberId @@ -68,6 +69,19 @@ class TaskDataFetcher( return taskService.updateStatus(command, MemberId(memberId)).toGraphql() } + @DgsMutation + fun updateTaskTitle( + @InputArgument input: UpdateTaskTitleInput, + @Authorize memberId: Long, + ): TaskGraphql { + val command = + TaskCommand.UpdateTitle( + taskId = TaskId(input.id.toLong()), + title = TaskTitle(input.title), + ) + return taskService.updateTitle(command, MemberId(memberId)).toGraphql() + } + @DgsMutation fun deleteTask( @InputArgument id: String, diff --git a/src/main/resources/schema/task.graphqls b/src/main/resources/schema/task.graphqls index fa6b010..6ea3d68 100644 --- a/src/main/resources/schema/task.graphqls +++ b/src/main/resources/schema/task.graphqls @@ -19,6 +19,12 @@ extend type Mutation { input: UpdateTaskStatusInput! ): Task! + "할일의 제목을 수정한다. (본인 할일만)" + updateTaskTitle( + "제목 수정 정보" + input: UpdateTaskTitleInput! + ): Task! + "할일을 삭제한다. (본인 할일만)" deleteTask( "삭제할 할일 ID" @@ -79,3 +85,11 @@ input UpdateTaskStatusInput { "변경할 상태" status: TaskStatus! } + +"""할일 제목 수정 입력""" +input UpdateTaskTitleInput { + "수정할 할일 ID" + id: ID! + "새 제목 (1~200자)" + title: String! +} diff --git a/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt b/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt index 57b443e..3849c54 100644 --- a/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt +++ b/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt @@ -129,6 +129,45 @@ class TaskServiceTest : } } + Given("할일 제목 수정 시") { + When("본인 할일이면") { + val newTitle = TaskTitle("수학 문제 풀기") + val updatedTask = savedTask.copy(title = newTitle, updatedAt = Instant.now()) + val command = TaskCommand.UpdateTitle(taskId = TaskId(1L), title = newTitle) + + every { taskRepository.findById(TaskId(1L)) } returns savedTask + every { taskRepository.updateTitle(command) } returns updatedTask + + val result = taskService.updateTitle(command, memberId) + + Then("수정된 할일을 반환한다") { + result.title.value shouldBe "수학 문제 풀기" + } + } + + When("존재하지 않는 할일이면") { + val command = TaskCommand.UpdateTitle(taskId = TaskId(99L), title = TaskTitle("새 제목")) + every { taskRepository.findById(TaskId(99L)) } returns null + + Then("EntityNotFoundException이 발생한다") { + shouldThrow { + taskService.updateTitle(command, memberId) + } + } + } + + When("본인 할일이 아니면") { + val command = TaskCommand.UpdateTitle(taskId = TaskId(1L), title = TaskTitle("새 제목")) + every { taskRepository.findById(TaskId(1L)) } returns savedTask + + Then("AccessDeniedException이 발생한다") { + shouldThrow { + taskService.updateTitle(command, otherMemberId) + } + } + } + } + Given("할일 삭제 시") { When("본인 할일이면") { val command = TaskCommand.Delete(taskId = TaskId(1L)) From a02abf386ea3d60a00e196de6d8ae082b9874828 Mon Sep 17 00:00:00 2001 From: robinjoon Date: Fri, 6 Mar 2026 20:59:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20updateTaskStatus=20+=20updateTa?= =?UTF-8?q?skTitle=20=E2=86=92=20updateTask=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 리뷰 반영: 개별 mutation을 하나의 updateTask로 통합. UpdateTaskInput(id, title?, status?)으로 변경할 필드만 전달. Co-Authored-By: Claude Opus 4.6 --- docs/plan/#21-task-title-update/checklist.md | 5 ++ docs/plan/#21-task-title-update/plan.md | 9 +++ .../task/application/service/TaskService.kt | 20 +------ .../loop/task/domain/model/TaskCommand.kt | 10 +--- .../task/domain/repository/TaskRepository.kt | 4 +- .../persistence/ExposedTaskRepository.kt | 14 +---- .../datafetcher/TaskDataFetcher.kt | 27 +++------ src/main/resources/schema/task.graphqls | 32 ++++------- .../application/service/TaskServiceTest.kt | 56 ++++++++----------- 9 files changed, 65 insertions(+), 112 deletions(-) diff --git a/docs/plan/#21-task-title-update/checklist.md b/docs/plan/#21-task-title-update/checklist.md index 4861432..3b052fa 100644 --- a/docs/plan/#21-task-title-update/checklist.md +++ b/docs/plan/#21-task-title-update/checklist.md @@ -9,3 +9,8 @@ ## 선택 항목 - [x] 소유권 검증 (본인 할일만 수정 가능) + +## PR 리뷰 반영 +- [x] updateTaskStatus + updateTaskTitle → updateTask 통합 +- [x] UpdateTaskInput에 title?, status? optional 필드 +- [x] 기존 updateTaskStatus 관련 코드 제거 diff --git a/docs/plan/#21-task-title-update/plan.md b/docs/plan/#21-task-title-update/plan.md index 30cb072..31f5086 100644 --- a/docs/plan/#21-task-title-update/plan.md +++ b/docs/plan/#21-task-title-update/plan.md @@ -10,3 +10,12 @@ - [x] 4단계: Infrastructure — ExposedTaskRepository.updateTitle 구현 - [x] 5단계: Presentation — GraphQL 스키마 + DataFetcher 추가 - [x] 6단계: DGS Codegen 빌드 및 전체 테스트 검증 + +## PR 리뷰 반영 — updateTask 통합 리팩토링 + +- [x] 7단계: Domain — TaskCommand.UpdateStatus + UpdateTitle → Update 통합 +- [x] 8단계: Domain — TaskRepository.updateStatus + updateTitle → update 통합 +- [x] 9단계: Application — TaskService TDD (기존 테스트 수정 + 새 케이스) +- [x] 10단계: Infrastructure — ExposedTaskRepository.update 통합 구현 +- [x] 11단계: Presentation — GraphQL 스키마 updateTask 통합 + DataFetcher 수정 +- [x] 12단계: DGS Codegen 빌드 및 전체 테스트 검증 diff --git a/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt b/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt index 01c150b..cf352df 100644 --- a/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt +++ b/src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt @@ -21,8 +21,8 @@ class TaskService( fun findAll(query: TaskQuery): List = taskRepository.findAll(query) @Transactional - fun updateStatus( - command: TaskCommand.UpdateStatus, + fun update( + command: TaskCommand.Update, memberId: MemberId, ): Task { val task = @@ -31,21 +31,7 @@ class TaskService( if (!task.isOwnedBy(memberId)) { throw AccessDeniedException("Task does not belong to member: ${memberId.value}") } - return taskRepository.updateStatus(command) - } - - @Transactional - fun updateTitle( - command: TaskCommand.UpdateTitle, - memberId: MemberId, - ): Task { - val task = - taskRepository.findById(command.taskId) - ?: throw EntityNotFoundException("Task not found: ${command.taskId.value}") - if (!task.isOwnedBy(memberId)) { - throw AccessDeniedException("Task does not belong to member: ${memberId.value}") - } - return taskRepository.updateTitle(command) + return taskRepository.update(command) } @Transactional diff --git a/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt b/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt index c81ff99..6447389 100644 --- a/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt +++ b/src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt @@ -13,14 +13,10 @@ sealed interface TaskCommand { val taskDate: LocalDate, ) : TaskCommand - data class UpdateStatus( + data class Update( val taskId: TaskId, - val status: TaskStatus, - ) : TaskCommand - - data class UpdateTitle( - val taskId: TaskId, - val title: TaskTitle, + val title: TaskTitle? = null, + val status: TaskStatus? = null, ) : TaskCommand data class Delete( diff --git a/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt b/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt index c45c9ad..78a43ea 100644 --- a/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt +++ b/src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt @@ -8,9 +8,7 @@ import kr.io.team.loop.task.domain.model.TaskQuery interface TaskRepository { fun save(command: TaskCommand.Create): Task - fun updateStatus(command: TaskCommand.UpdateStatus): Task - - fun updateTitle(command: TaskCommand.UpdateTitle): Task + fun update(command: TaskCommand.Update): Task fun delete(command: TaskCommand.Delete) diff --git a/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt b/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt index 3110cba..6627715 100644 --- a/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt +++ b/src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt @@ -48,19 +48,11 @@ class ExposedTaskRepository : TaskRepository { ) } - override fun updateStatus(command: TaskCommand.UpdateStatus): Task { + override fun update(command: TaskCommand.Update): Task { val now = OffsetDateTime.now() TaskTable.update({ TaskTable.taskId eq command.taskId.value }) { - it[status] = command.status.name - it[updatedAt] = now - } - return findById(command.taskId)!! - } - - override fun updateTitle(command: TaskCommand.UpdateTitle): Task { - val now = OffsetDateTime.now() - TaskTable.update({ TaskTable.taskId eq command.taskId.value }) { - it[title] = command.title.value + command.title?.let { newTitle -> it[title] = newTitle.value } + command.status?.let { newStatus -> it[status] = newStatus.name } it[updatedAt] = now } return findById(command.taskId)!! diff --git a/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt b/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt index 1205c77..04a113e 100644 --- a/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt +++ b/src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt @@ -7,8 +7,7 @@ import com.netflix.graphql.dgs.InputArgument import kotlinx.datetime.LocalDate import kr.io.team.loop.codegen.types.CreateTaskInput import kr.io.team.loop.codegen.types.TaskFilter -import kr.io.team.loop.codegen.types.UpdateTaskStatusInput -import kr.io.team.loop.codegen.types.UpdateTaskTitleInput +import kr.io.team.loop.codegen.types.UpdateTaskInput import kr.io.team.loop.common.config.Authorize import kr.io.team.loop.common.domain.GoalId import kr.io.team.loop.common.domain.MemberId @@ -57,29 +56,17 @@ class TaskDataFetcher( } @DgsMutation - fun updateTaskStatus( - @InputArgument input: UpdateTaskStatusInput, + fun updateTask( + @InputArgument input: UpdateTaskInput, @Authorize memberId: Long, ): TaskGraphql { val command = - TaskCommand.UpdateStatus( + TaskCommand.Update( taskId = TaskId(input.id.toLong()), - status = TaskStatus.valueOf(input.status.name), + title = input.title?.let { TaskTitle(it) }, + status = input.status?.let { TaskStatus.valueOf(it.name) }, ) - return taskService.updateStatus(command, MemberId(memberId)).toGraphql() - } - - @DgsMutation - fun updateTaskTitle( - @InputArgument input: UpdateTaskTitleInput, - @Authorize memberId: Long, - ): TaskGraphql { - val command = - TaskCommand.UpdateTitle( - taskId = TaskId(input.id.toLong()), - title = TaskTitle(input.title), - ) - return taskService.updateTitle(command, MemberId(memberId)).toGraphql() + return taskService.update(command, MemberId(memberId)).toGraphql() } @DgsMutation diff --git a/src/main/resources/schema/task.graphqls b/src/main/resources/schema/task.graphqls index 6ea3d68..f2e9d47 100644 --- a/src/main/resources/schema/task.graphqls +++ b/src/main/resources/schema/task.graphqls @@ -13,16 +13,10 @@ extend type Mutation { input: CreateTaskInput! ): Task! - "할일의 완료/미완료 상태를 변경한다. (본인 할일만)" - updateTaskStatus( - "상태 변경 정보" - input: UpdateTaskStatusInput! - ): Task! - - "할일의 제목을 수정한다. (본인 할일만)" - updateTaskTitle( - "제목 수정 정보" - input: UpdateTaskTitleInput! + "할일을 수정한다. 제목, 상태 등 변경할 필드만 전달한다. (본인 할일만)" + updateTask( + "수정할 할일 정보" + input: UpdateTaskInput! ): Task! "할일을 삭제한다. (본인 할일만)" @@ -78,18 +72,12 @@ input CreateTaskInput { date: String! } -"""할일 상태 변경 입력""" -input UpdateTaskStatusInput { - "변경할 할일 ID" - id: ID! - "변경할 상태" - status: TaskStatus! -} - -"""할일 제목 수정 입력""" -input UpdateTaskTitleInput { +"""할일 수정 입력. 변경할 필드만 전달한다.""" +input UpdateTaskInput { "수정할 할일 ID" id: ID! - "새 제목 (1~200자)" - title: String! + "새 제목 (1~200자, 선택)" + title: String + "새 상태 (선택)" + status: TaskStatus } diff --git a/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt b/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt index 3849c54..de6b7e2 100644 --- a/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt +++ b/src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt @@ -91,78 +91,70 @@ class TaskServiceTest : } } - Given("할일 상태 변경 시") { - When("본인 할일이면") { + Given("할일 수정 시") { + When("본인 할일의 상태를 변경하면") { val updatedTask = savedTask.copy(status = TaskStatus.DONE, updatedAt = Instant.now()) - val command = TaskCommand.UpdateStatus(taskId = TaskId(1L), status = TaskStatus.DONE) + val command = TaskCommand.Update(taskId = TaskId(1L), status = TaskStatus.DONE) every { taskRepository.findById(TaskId(1L)) } returns savedTask - every { taskRepository.updateStatus(command) } returns updatedTask + every { taskRepository.update(command) } returns updatedTask - val result = taskService.updateStatus(command, memberId) + val result = taskService.update(command, memberId) Then("변경된 할일을 반환한다") { result.status shouldBe TaskStatus.DONE } } - When("존재하지 않는 할일이면") { - val command = TaskCommand.UpdateStatus(taskId = TaskId(99L), status = TaskStatus.DONE) - every { taskRepository.findById(TaskId(99L)) } returns null - - Then("EntityNotFoundException이 발생한다") { - shouldThrow { - taskService.updateStatus(command, memberId) - } - } - } + When("본인 할일의 제목을 수정하면") { + val newTitle = TaskTitle("수학 문제 풀기") + val updatedTask = savedTask.copy(title = newTitle, updatedAt = Instant.now()) + val command = TaskCommand.Update(taskId = TaskId(1L), title = newTitle) - When("본인 할일이 아니면") { - val command = TaskCommand.UpdateStatus(taskId = TaskId(1L), status = TaskStatus.DONE) every { taskRepository.findById(TaskId(1L)) } returns savedTask + every { taskRepository.update(command) } returns updatedTask - Then("AccessDeniedException이 발생한다") { - shouldThrow { - taskService.updateStatus(command, otherMemberId) - } + val result = taskService.update(command, memberId) + + Then("수정된 할일을 반환한다") { + result.title.value shouldBe "수학 문제 풀기" } } - } - Given("할일 제목 수정 시") { - When("본인 할일이면") { + When("제목과 상태를 동시에 수정하면") { val newTitle = TaskTitle("수학 문제 풀기") - val updatedTask = savedTask.copy(title = newTitle, updatedAt = Instant.now()) - val command = TaskCommand.UpdateTitle(taskId = TaskId(1L), title = newTitle) + val updatedTask = savedTask.copy(title = newTitle, status = TaskStatus.DONE, updatedAt = Instant.now()) + val command = TaskCommand.Update(taskId = TaskId(1L), title = newTitle, status = TaskStatus.DONE) every { taskRepository.findById(TaskId(1L)) } returns savedTask - every { taskRepository.updateTitle(command) } returns updatedTask + every { taskRepository.update(command) } returns updatedTask - val result = taskService.updateTitle(command, memberId) + val result = taskService.update(command, memberId) Then("수정된 할일을 반환한다") { result.title.value shouldBe "수학 문제 풀기" + result.status shouldBe TaskStatus.DONE } } When("존재하지 않는 할일이면") { - val command = TaskCommand.UpdateTitle(taskId = TaskId(99L), title = TaskTitle("새 제목")) + val command = TaskCommand.Update(taskId = TaskId(99L), status = TaskStatus.DONE) every { taskRepository.findById(TaskId(99L)) } returns null Then("EntityNotFoundException이 발생한다") { shouldThrow { - taskService.updateTitle(command, memberId) + taskService.update(command, memberId) } } } When("본인 할일이 아니면") { - val command = TaskCommand.UpdateTitle(taskId = TaskId(1L), title = TaskTitle("새 제목")) + val command = TaskCommand.Update(taskId = TaskId(1L), status = TaskStatus.DONE) every { taskRepository.findById(TaskId(1L)) } returns savedTask Then("AccessDeniedException이 발생한다") { shouldThrow { - taskService.updateTitle(command, otherMemberId) + taskService.update(command, otherMemberId) } } }