Skip to content

Commit bff9dce

Browse files
robinjoonclaude
andauthored
feat: Task 제목 수정 Mutation 추가 (#21) (#22)
* feat: Task 제목 수정 Mutation 추가 (#21) 본인 소유 할일의 제목을 수정할 수 있는 updateTaskTitle mutation 추가. 소유권 검증 포함, updateStatus와 동일한 패턴 적용. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: updateTaskStatus + updateTaskTitle → updateTask 통합 PR 리뷰 반영: 개별 mutation을 하나의 updateTask로 통합. UpdateTaskInput(id, title?, status?)으로 변경할 필드만 전달. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 881e138 commit bff9dce

9 files changed

Lines changed: 105 additions & 32 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Task 제목 수정 검증 체크리스트
2+
3+
## 필수 항목
4+
- [x] 아키텍처 원칙 준수 (docs/architecture.md 기준)
5+
- [x] 레이어 의존성 규칙 위반 없음
6+
- [x] 테스트 코드 작성 완료 (Domain, Application 필수)
7+
- [x] 모든 테스트 통과
8+
- [x] 기존 테스트 깨지지 않음
9+
10+
## 선택 항목
11+
- [x] 소유권 검증 (본인 할일만 수정 가능)
12+
13+
## PR 리뷰 반영
14+
- [x] updateTaskStatus + updateTaskTitle → updateTask 통합
15+
- [x] UpdateTaskInput에 title?, status? optional 필드
16+
- [x] 기존 updateTaskStatus 관련 코드 제거
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Task 제목 수정 Mutation 추가 계획
2+
3+
> Issue: #21
4+
5+
## 단계
6+
7+
- [x] 1단계: Domain — TaskCommand.UpdateTitle 추가
8+
- [x] 2단계: Domain — TaskRepository.updateTitle 추가
9+
- [x] 3단계: Application — TaskService.updateTitle TDD (RED → GREEN → REFACTOR)
10+
- [x] 4단계: Infrastructure — ExposedTaskRepository.updateTitle 구현
11+
- [x] 5단계: Presentation — GraphQL 스키마 + DataFetcher 추가
12+
- [x] 6단계: DGS Codegen 빌드 및 전체 테스트 검증
13+
14+
## PR 리뷰 반영 — updateTask 통합 리팩토링
15+
16+
- [x] 7단계: Domain — TaskCommand.UpdateStatus + UpdateTitle → Update 통합
17+
- [x] 8단계: Domain — TaskRepository.updateStatus + updateTitle → update 통합
18+
- [x] 9단계: Application — TaskService TDD (기존 테스트 수정 + 새 케이스)
19+
- [x] 10단계: Infrastructure — ExposedTaskRepository.update 통합 구현
20+
- [x] 11단계: Presentation — GraphQL 스키마 updateTask 통합 + DataFetcher 수정
21+
- [x] 12단계: DGS Codegen 빌드 및 전체 테스트 검증

src/main/kotlin/kr/io/team/loop/task/application/service/TaskService.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class TaskService(
2121
fun findAll(query: TaskQuery): List<Task> = taskRepository.findAll(query)
2222

2323
@Transactional
24-
fun updateStatus(
25-
command: TaskCommand.UpdateStatus,
24+
fun update(
25+
command: TaskCommand.Update,
2626
memberId: MemberId,
2727
): Task {
2828
val task =
@@ -31,7 +31,7 @@ class TaskService(
3131
if (!task.isOwnedBy(memberId)) {
3232
throw AccessDeniedException("Task does not belong to member: ${memberId.value}")
3333
}
34-
return taskRepository.updateStatus(command)
34+
return taskRepository.update(command)
3535
}
3636

3737
@Transactional

src/main/kotlin/kr/io/team/loop/task/domain/model/TaskCommand.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ sealed interface TaskCommand {
1313
val taskDate: LocalDate,
1414
) : TaskCommand
1515

16-
data class UpdateStatus(
16+
data class Update(
1717
val taskId: TaskId,
18-
val status: TaskStatus,
18+
val title: TaskTitle? = null,
19+
val status: TaskStatus? = null,
1920
) : TaskCommand
2021

2122
data class Delete(

src/main/kotlin/kr/io/team/loop/task/domain/repository/TaskRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import kr.io.team.loop.task.domain.model.TaskQuery
88
interface TaskRepository {
99
fun save(command: TaskCommand.Create): Task
1010

11-
fun updateStatus(command: TaskCommand.UpdateStatus): Task
11+
fun update(command: TaskCommand.Update): Task
1212

1313
fun delete(command: TaskCommand.Delete)
1414

src/main/kotlin/kr/io/team/loop/task/infrastructure/persistence/ExposedTaskRepository.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ class ExposedTaskRepository : TaskRepository {
4848
)
4949
}
5050

51-
override fun updateStatus(command: TaskCommand.UpdateStatus): Task {
51+
override fun update(command: TaskCommand.Update): Task {
5252
val now = OffsetDateTime.now()
5353
TaskTable.update({ TaskTable.taskId eq command.taskId.value }) {
54-
it[status] = command.status.name
54+
command.title?.let { newTitle -> it[title] = newTitle.value }
55+
command.status?.let { newStatus -> it[status] = newStatus.name }
5556
it[updatedAt] = now
5657
}
5758
return findById(command.taskId)!!

src/main/kotlin/kr/io/team/loop/task/presentation/datafetcher/TaskDataFetcher.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.netflix.graphql.dgs.InputArgument
77
import kotlinx.datetime.LocalDate
88
import kr.io.team.loop.codegen.types.CreateTaskInput
99
import kr.io.team.loop.codegen.types.TaskFilter
10-
import kr.io.team.loop.codegen.types.UpdateTaskStatusInput
10+
import kr.io.team.loop.codegen.types.UpdateTaskInput
1111
import kr.io.team.loop.common.config.Authorize
1212
import kr.io.team.loop.common.domain.GoalId
1313
import kr.io.team.loop.common.domain.MemberId
@@ -56,16 +56,17 @@ class TaskDataFetcher(
5656
}
5757

5858
@DgsMutation
59-
fun updateTaskStatus(
60-
@InputArgument input: UpdateTaskStatusInput,
59+
fun updateTask(
60+
@InputArgument input: UpdateTaskInput,
6161
@Authorize memberId: Long,
6262
): TaskGraphql {
6363
val command =
64-
TaskCommand.UpdateStatus(
64+
TaskCommand.Update(
6565
taskId = TaskId(input.id.toLong()),
66-
status = TaskStatus.valueOf(input.status.name),
66+
title = input.title?.let { TaskTitle(it) },
67+
status = input.status?.let { TaskStatus.valueOf(it.name) },
6768
)
68-
return taskService.updateStatus(command, MemberId(memberId)).toGraphql()
69+
return taskService.update(command, MemberId(memberId)).toGraphql()
6970
}
7071

7172
@DgsMutation

src/main/resources/schema/task.graphqls

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ extend type Mutation {
1313
input: CreateTaskInput!
1414
): Task!
1515

16-
"할일의 완료/미완료 상태를 변경한다. (본인 할일만)"
17-
updateTaskStatus(
18-
"상태 변경 정보"
19-
input: UpdateTaskStatusInput!
16+
"할일을 수정한다. 제목, 상태 등 변경할 필드만 전달한다. (본인 할일만)"
17+
updateTask(
18+
"수정할 할일 정보"
19+
input: UpdateTaskInput!
2020
): Task!
2121

2222
"할일을 삭제한다. (본인 할일만)"
@@ -72,10 +72,12 @@ input CreateTaskInput {
7272
date: String!
7373
}
7474

75-
"""할일 상태 변경 입력"""
76-
input UpdateTaskStatusInput {
77-
"변경할 할일 ID"
75+
"""할일 수정 입력. 변경할 필드만 전달한다."""
76+
input UpdateTaskInput {
77+
"수정할 할일 ID"
7878
id: ID!
79-
"변경할 상태"
80-
status: TaskStatus!
79+
"새 제목 (1~200자, 선택)"
80+
title: String
81+
"새 상태 (선택)"
82+
status: TaskStatus
8183
}

src/test/kotlin/kr/io/team/loop/task/application/service/TaskServiceTest.kt

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,39 +91,70 @@ class TaskServiceTest :
9191
}
9292
}
9393

94-
Given("할일 상태 변경") {
95-
When("본인 할일이면") {
94+
Given("할일 수정") {
95+
When("본인 할일의 상태를 변경하면") {
9696
val updatedTask = savedTask.copy(status = TaskStatus.DONE, updatedAt = Instant.now())
97-
val command = TaskCommand.UpdateStatus(taskId = TaskId(1L), status = TaskStatus.DONE)
97+
val command = TaskCommand.Update(taskId = TaskId(1L), status = TaskStatus.DONE)
9898

9999
every { taskRepository.findById(TaskId(1L)) } returns savedTask
100-
every { taskRepository.updateStatus(command) } returns updatedTask
100+
every { taskRepository.update(command) } returns updatedTask
101101

102-
val result = taskService.updateStatus(command, memberId)
102+
val result = taskService.update(command, memberId)
103103

104104
Then("변경된 할일을 반환한다") {
105105
result.status shouldBe TaskStatus.DONE
106106
}
107107
}
108108

109+
When("본인 할일의 제목을 수정하면") {
110+
val newTitle = TaskTitle("수학 문제 풀기")
111+
val updatedTask = savedTask.copy(title = newTitle, updatedAt = Instant.now())
112+
val command = TaskCommand.Update(taskId = TaskId(1L), title = newTitle)
113+
114+
every { taskRepository.findById(TaskId(1L)) } returns savedTask
115+
every { taskRepository.update(command) } returns updatedTask
116+
117+
val result = taskService.update(command, memberId)
118+
119+
Then("수정된 할일을 반환한다") {
120+
result.title.value shouldBe "수학 문제 풀기"
121+
}
122+
}
123+
124+
When("제목과 상태를 동시에 수정하면") {
125+
val newTitle = TaskTitle("수학 문제 풀기")
126+
val updatedTask = savedTask.copy(title = newTitle, status = TaskStatus.DONE, updatedAt = Instant.now())
127+
val command = TaskCommand.Update(taskId = TaskId(1L), title = newTitle, status = TaskStatus.DONE)
128+
129+
every { taskRepository.findById(TaskId(1L)) } returns savedTask
130+
every { taskRepository.update(command) } returns updatedTask
131+
132+
val result = taskService.update(command, memberId)
133+
134+
Then("수정된 할일을 반환한다") {
135+
result.title.value shouldBe "수학 문제 풀기"
136+
result.status shouldBe TaskStatus.DONE
137+
}
138+
}
139+
109140
When("존재하지 않는 할일이면") {
110-
val command = TaskCommand.UpdateStatus(taskId = TaskId(99L), status = TaskStatus.DONE)
141+
val command = TaskCommand.Update(taskId = TaskId(99L), status = TaskStatus.DONE)
111142
every { taskRepository.findById(TaskId(99L)) } returns null
112143

113144
Then("EntityNotFoundException이 발생한다") {
114145
shouldThrow<EntityNotFoundException> {
115-
taskService.updateStatus(command, memberId)
146+
taskService.update(command, memberId)
116147
}
117148
}
118149
}
119150

120151
When("본인 할일이 아니면") {
121-
val command = TaskCommand.UpdateStatus(taskId = TaskId(1L), status = TaskStatus.DONE)
152+
val command = TaskCommand.Update(taskId = TaskId(1L), status = TaskStatus.DONE)
122153
every { taskRepository.findById(TaskId(1L)) } returns savedTask
123154

124155
Then("AccessDeniedException이 발생한다") {
125156
shouldThrow<AccessDeniedException> {
126-
taskService.updateStatus(command, otherMemberId)
157+
taskService.update(command, otherMemberId)
127158
}
128159
}
129160
}

0 commit comments

Comments
 (0)