Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/plan/#21-task-title-update/checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Task 제목 수정 검증 체크리스트

## 필수 항목
- [x] 아키텍처 원칙 준수 (docs/architecture.md 기준)
- [x] 레이어 의존성 규칙 위반 없음
- [x] 테스트 코드 작성 완료 (Domain, Application 필수)
- [x] 모든 테스트 통과
- [x] 기존 테스트 깨지지 않음

## 선택 항목
- [x] 소유권 검증 (본인 할일만 수정 가능)

## PR 리뷰 반영
- [x] updateTaskStatus + updateTaskTitle → updateTask 통합
- [x] UpdateTaskInput에 title?, status? optional 필드
- [x] 기존 updateTaskStatus 관련 코드 제거
21 changes: 21 additions & 0 deletions docs/plan/#21-task-title-update/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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 빌드 및 전체 테스트 검증

## 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 빌드 및 전체 테스트 검증
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class TaskService(
fun findAll(query: TaskQuery): List<Task> = taskRepository.findAll(query)

@Transactional
fun updateStatus(
command: TaskCommand.UpdateStatus,
fun update(
command: TaskCommand.Update,
memberId: MemberId,
): Task {
val task =
Expand All @@ -31,7 +31,7 @@ class TaskService(
if (!task.isOwnedBy(memberId)) {
throw AccessDeniedException("Task does not belong to member: ${memberId.value}")
}
return taskRepository.updateStatus(command)
return taskRepository.update(command)
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ sealed interface TaskCommand {
val taskDate: LocalDate,
) : TaskCommand

data class UpdateStatus(
data class Update(
val taskId: TaskId,
val status: TaskStatus,
val title: TaskTitle? = null,
val status: TaskStatus? = null,
) : TaskCommand

data class Delete(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +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 update(command: TaskCommand.Update): Task

fun delete(command: TaskCommand.Delete)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +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
command.title?.let { newTitle -> it[title] = newTitle.value }
command.status?.let { newStatus -> it[status] = newStatus.name }
it[updatedAt] = now
}
return findById(command.taskId)!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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.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
Expand Down Expand Up @@ -56,16 +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()
return taskService.update(command, MemberId(memberId)).toGraphql()
}

@DgsMutation
Expand Down
20 changes: 11 additions & 9 deletions src/main/resources/schema/task.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ extend type Mutation {
input: CreateTaskInput!
): Task!

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

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

"""할일 상태 변경 입력"""
input UpdateTaskStatusInput {
"변경할 할일 ID"
"""할일 수정 입력. 변경할 필드만 전달한다."""
input UpdateTaskInput {
"수정할 할일 ID"
id: ID!
"변경할 상태"
status: TaskStatus!
"새 제목 (1~200자, 선택)"
title: String
"새 상태 (선택)"
status: TaskStatus
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,39 +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 newTitle = TaskTitle("수학 문제 풀기")
val updatedTask = savedTask.copy(title = newTitle, updatedAt = Instant.now())
val command = TaskCommand.Update(taskId = TaskId(1L), title = newTitle)

every { taskRepository.findById(TaskId(1L)) } returns savedTask
every { taskRepository.update(command) } returns updatedTask

val result = taskService.update(command, memberId)

Then("수정된 할일을 반환한다") {
result.title.value shouldBe "수학 문제 풀기"
}
}

When("제목과 상태를 동시에 수정하면") {
val newTitle = TaskTitle("수학 문제 풀기")
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.update(command) } returns updatedTask

val result = taskService.update(command, memberId)

Then("수정된 할일을 반환한다") {
result.title.value shouldBe "수학 문제 풀기"
result.status shouldBe TaskStatus.DONE
}
}

When("존재하지 않는 할일이면") {
val command = TaskCommand.UpdateStatus(taskId = TaskId(99L), status = TaskStatus.DONE)
val command = TaskCommand.Update(taskId = TaskId(99L), status = TaskStatus.DONE)
every { taskRepository.findById(TaskId(99L)) } returns null

Then("EntityNotFoundException이 발생한다") {
shouldThrow<EntityNotFoundException> {
taskService.updateStatus(command, memberId)
taskService.update(command, memberId)
}
}
}

When("본인 할일이 아니면") {
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

Then("AccessDeniedException이 발생한다") {
shouldThrow<AccessDeniedException> {
taskService.updateStatus(command, otherMemberId)
taskService.update(command, otherMemberId)
}
}
}
Expand Down