fix: 업무 완료일을 마감일이 아닌 실제 완료 시각으로 표시

목록의 "N월 N일 완료" 라벨이 완료 시각이 아니라 마감일(dueDate)을 쓰고 있어,
마감일을 지시 당일로 잡은 업무는 지시 날짜가 완료일처럼 보였다.
완료 시각을 저장하는 컬럼 자체가 없던 것이 원인이다.

- tasks.completed_at 추가 — done 진입 시 기록, 이탈 시 null(재승인 시 갱신)
- 목록/내 업무 응답에 completedAt 노출, taskDueInfo 가 이 값으로 라벨 생성
- 기존 완료 업무는 활동 로그(task.status_changed)의 시각으로 백필
- 백필 불가 건과 마감일 없는 완료 업무는 날짜 없이 "완료"만 표시

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:10:48 +09:00
parent 38b3762b2c
commit 65d7abcd68
8 changed files with 91 additions and 11 deletions
+26 -8
View File
@@ -183,15 +183,33 @@ export interface TaskDueInfo {
ddayLevel: DdayLevel
}
/** dueDate(ISO|null) + 완료여부로 마감 표시 정보 계산 */
export function taskDueInfo(dueDate: string | null, isDone: boolean): TaskDueInfo {
if (!dueDate) {
return { dateLabel: '', dday: '', listLabel: '', overdue: false, ddayLevel: '' }
/**
* dueDate(ISO|null) + 완료여부로 마감 표시 정보 계산
*
* 완료 업무의 목록 라벨은 마감일이 아니라 실제 완료 시각(completedAt)을 쓴다.
* 완료 기록이 없는 과거 업무(백필 불가 건)는 날짜 없이 '완료'만 표시한다.
*/
export function taskDueInfo(
dueDate: string | null,
isDone: boolean,
completedAt: string | null = null,
): TaskDueInfo {
// 완료 업무의 목록 라벨 — 마감일 유무와 무관하게 완료 시각으로 결정
const doneListLabel = (): string => {
const label = completedAt ? monthDayKo(completedAt) : ''
return label ? `${label} 완료` : '완료'
}
const empty: TaskDueInfo = {
dateLabel: '',
dday: '',
listLabel: isDone ? doneListLabel() : '',
overdue: false,
ddayLevel: '',
}
if (!dueDate) return empty
const due = new Date(dueDate)
if (Number.isNaN(due.getTime())) {
return { dateLabel: '', dday: '', listLabel: '', overdue: false, ddayLevel: '' }
}
if (Number.isNaN(due.getTime())) return empty
const dateLabel = `${due.getMonth() + 1}${due.getDate()}일 (${WEEKDAYS_KO[due.getDay()]})`
// 자정 기준 일 수 차이
@@ -204,7 +222,7 @@ export function taskDueInfo(dueDate: string | null, isDone: boolean): TaskDueInf
return {
dateLabel,
dday: '',
listLabel: `${monthDayKo(due)} 완료`,
listLabel: doneListLabel(),
overdue: false,
ddayLevel: '',
}
+2 -2
View File
@@ -42,7 +42,7 @@ const ISSUED_LABEL: Record<TaskStatus, string> = {
// API 행 → 담당 뷰 행 (누가 지시했는지 표시)
function toAssignedRow(api: ApiMyTaskRow): MyTaskRow {
const due = taskDueInfo(api.dueDate, api.status === 'done')
const due = taskDueInfo(api.dueDate, api.status === 'done', api.completedAt)
const [, total] = api.checklist
const isDone = api.status === 'done'
return {
@@ -64,7 +64,7 @@ function toAssignedRow(api: ApiMyTaskRow): MyTaskRow {
// API 행 → 지시 뷰 행 (담당자 미니 아바타 + 승인 대기 강조)
function toIssuedRow(api: ApiMyTaskRow): MyTaskRow {
const due = taskDueInfo(api.dueDate, api.status === 'done')
const due = taskDueInfo(api.dueDate, api.status === 'done', api.completedAt)
const [, total] = api.checklist
const isDone = api.status === 'done'
const isReview = api.status === 'review'
+1 -1
View File
@@ -56,7 +56,7 @@ const UNKNOWN_USER: UserView = {
// API 업무 목록 행 → 화면용 ProjectTask
function toProjectTaskView(api: ApiProjectTask): ProjectTaskView {
const due = taskDueInfo(api.dueDate, api.status === 'done')
const due = taskDueInfo(api.dueDate, api.status === 'done', api.completedAt)
const [, total] = api.checklist
return {
id: api.id,
+2
View File
@@ -21,6 +21,8 @@ export interface ApiMyTaskRow {
title: string
status: TaskStatus
dueDate: string | null
// 완료 시각(done 이 아니거나 기록이 없으면 null)
completedAt: string | null
checklist: [number, number] // [완료, 전체]
assignees: ApiMember[]
issuer: ApiMember | null
+2
View File
@@ -18,6 +18,8 @@ export interface ApiProjectTask {
title: string
status: TaskStatus
dueDate: string | null
// 완료 시각(done 이 아니거나 기록이 없으면 null)
completedAt: string | null
checklist: [number, number] // [완료, 전체]
commentCount: number
fileCount: number // 첨부 파일 수