diff --git a/frontend/src/composables/useTask.ts b/frontend/src/composables/useTask.ts index 0c17c55..cbe0f9e 100644 --- a/frontend/src/composables/useTask.ts +++ b/frontend/src/composables/useTask.ts @@ -6,6 +6,7 @@ import type { ApiProjectTaskListResult, ApiTaskDetail, ChecklistReviewItem, + ChecklistWorkStatus, CreateTaskPayload, TaskListQuery, UpdateTaskPayload, @@ -68,6 +69,32 @@ export function useTask() { })) as unknown as ApiTaskDetail } + /* ===================== 중간 점검(체크포인트) ===================== */ + + // 체크리스트 진행 상태 일괄 갱신(담당자) + async function updateWorkStatus( + projectId: string, + taskId: number, + items: { id: string; workStatus: ChecklistWorkStatus }[], + ): Promise { + return (await api.patch(`/projects/${projectId}/tasks/${taskId}/work-status`, { + items, + })) as unknown as ApiTaskDetail + } + + // 중간 점검 보고(담당자) — 점검일~마감 구간 + async function submitCheckpointReport( + projectId: string, + taskId: number, + checkpointId: string, + note?: string, + ): Promise { + return (await api.post( + `/projects/${projectId}/tasks/${taskId}/checkpoints/${checkpointId}/report`, + { ...(note ? { note } : {}) }, + )) as unknown as ApiTaskDetail + } + /* ===================== 댓글 / 답글 (5단계) ===================== */ async function addComment( @@ -156,6 +183,8 @@ export function useTask() { update, remove, changeStatus, + updateWorkStatus, + submitCheckpointReport, addComment, addReply, uploadFile, diff --git a/frontend/src/mock/relay.mock.ts b/frontend/src/mock/relay.mock.ts index 518e6c5..c85f687 100644 --- a/frontend/src/mock/relay.mock.ts +++ b/frontend/src/mock/relay.mock.ts @@ -36,6 +36,9 @@ export const CHECKLIST_CATEGORIES: ChecklistCategory[] = [ '부가', ] +/** 작업자 진행 상태(자가보고) — 미착수/진행중/완료 */ +export type ChecklistWorkStatus = 'todo' | 'doing' | 'done' + /** 체크리스트 항목 */ export interface ChecklistItem { /** 항목 ID(API 연동 시) */ @@ -45,6 +48,30 @@ export interface ChecklistItem { category: ChecklistCategory /** 보완 내용(수정 요청 시 항목별 입력) — 없으면 null */ reviewNote?: string | null + /** 작업자 진행 상태(자가보고) */ + workStatus?: ChecklistWorkStatus +} + +/** 중간 점검일(표시용) */ +export interface TaskCheckpointView { + id: string + /** ISO — 보고 가능 구간(점검일~마감) 계산용 */ + checkAt: string + /** 표시 라벨 */ + dateLabel: string +} + +/** 중간 점검 보고(스냅샷, 표시용) */ +export interface CheckpointReportView { + id: string + checkpointId: string + reporterName: string + note: string | null + items: { text: string; workStatus: ChecklistWorkStatus }[] + /** 점검일 표시 */ + dateLabel: string + /** 보고 시각(상대) */ + timeLabel: string } /** 프로젝트 — 작업의 기본 단위(우선) */ @@ -138,6 +165,8 @@ export interface TaskDetail { issuer: User dueLabel: string dday: string + /** 원본 마감 ISO(중간 점검 보고 가능 구간 계산용) — 없으면 null */ + dueAt: string | null files: Attachment[] /** 승인 요청 정보(승인 대기 상태일 때) */ approval?: { @@ -148,6 +177,10 @@ export interface TaskDetail { /** 수정 요청 메시지(수정 요청 상태일 때) */ changesNote?: string issuedLabel: string + /** 중간 점검일 목록 */ + checkpoints: TaskCheckpointView[] + /** 중간 점검 보고 이력(최신순) */ + checkpointReports: CheckpointReportView[] } /** D-day 강조 단계 */ diff --git a/frontend/src/pages/relay/TaskCreatePage.vue b/frontend/src/pages/relay/TaskCreatePage.vue index 95c627e..381a163 100644 --- a/frontend/src/pages/relay/TaskCreatePage.vue +++ b/frontend/src/pages/relay/TaskCreatePage.vue @@ -99,6 +99,7 @@ async function loadTaskForEdit() { category: c.category, })) existingFiles.value = t.files + checkpoints.value = t.checkpoints.map((c) => toDateInput(c.checkAt)).sort() } catch { // 404 등은 인터셉터가 알림 처리 } @@ -159,6 +160,22 @@ function onAssigneeBlur() { // --- 마감기한 --- const dueDate = ref('') // yyyy-MM-dd (input[type=date]) +// --- 중간 점검일(여러 개) — 작업자가 진행 상태를 보고하는 기준일 --- +const checkpoints = ref([]) // yyyy-MM-dd 목록 +const newCheckpoint = ref('') +function addCheckpoint() { + const d = newCheckpoint.value + if (!d) return + if (!checkpoints.value.includes(d)) { + checkpoints.value.push(d) + checkpoints.value.sort() + } + newCheckpoint.value = '' +} +function removeCheckpoint(d: string) { + checkpoints.value = checkpoints.value.filter((x) => x !== d) +} + // 체크리스트 (생성 시 항목만 등록 — 완료여부는 항상 false 로 생성) // 항목은 카테고리(탭: 필수/관리/보안/부가)별로 추가한다. const checklist = ref([]) @@ -414,6 +431,7 @@ async function submitTask() { .filter((p) => p.length > 0) const assigneeIds = assignees.value.map((a) => a.id) const dueIso = dueDate.value ? new Date(dueDate.value).toISOString() : null + const checkpointIsos = checkpoints.value.map((d) => new Date(d).toISOString()) let tid: number if (isEdit.value && taskId.value !== null) { @@ -428,6 +446,7 @@ async function submitTask() { ? { id: c.id, text: c.text, category: c.category } : { text: c.text, category: c.category }, ), + checkpoints: checkpointIsos, }) tid = updated.id } else { @@ -439,6 +458,7 @@ async function submitTask() { checklist: checklist.value.length ? checklist.value.map((c) => ({ text: c.text, category: c.category })) : undefined, + checkpoints: checkpointIsos.length ? checkpointIsos : undefined, }) tid = created.id } @@ -1032,6 +1052,50 @@ function goBack() { > +
+
+ 중간 점검일 · 선택 · 여러 개 +
+
+ + +
+
    +
  • + {{ d }} + +
  • +
+

+ 담당자가 점검일부터 마감 전까지 진행 상태를 보고할 수 있습니다. +

+
+
첨부파일 !!task.value && task.value.assignees.some((a) => a.id === me.value.id), ) + +/* ============================================================ + * 중간 점검(체크포인트) — 작업자 진행 상태 자가보고 + 보고 전송 + * ============================================================ */ +const WORK_STATUS_LABEL: Record = { + todo: '미착수', + doing: '진행 중', + done: '완료', +} +const WORK_STATUS_ORDER: ChecklistWorkStatus[] = ['todo', 'doing', 'done'] +const workSaving = ref(false) +const reportNote = ref('') +const reporting = ref(false) + +// 작업자: 항목 진행 상태 변경(상시) — 담당자만, id 있는 항목만 +async function setItemWorkStatus(item: ChecklistItem, status: ChecklistWorkStatus) { + if (!task.value || !item.id || workSaving.value) return + if ((item.workStatus ?? 'todo') === status) return + workSaving.value = true + try { + task.value = await taskStore.updateWorkStatus(projectId.value, task.value.id, [ + { id: item.id, workStatus: status }, + ]) + } catch { + // 인터셉터가 알림 처리 + } finally { + workSaving.value = false + } +} + +// 보고 가능 구간: 점검일 당일 ~ 마감 전(마감 없으면 점검일 이후 상시). 담당자만. +function isReportable(cp: TaskCheckpointView): boolean { + if (!isAssignee.value || !task.value) return false + const now = Date.now() + if (now < new Date(cp.checkAt).getTime()) return false + if (task.value.dueAt && now >= new Date(task.value.dueAt).getTime()) return false + return true +} +// 보고 불가 사유(버튼 대신 표시) +function checkpointStateLabel(cp: TaskCheckpointView): string { + if (!task.value) return '' + const now = Date.now() + if (now < new Date(cp.checkAt).getTime()) return '점검일 전' + if (task.value.dueAt && now >= new Date(task.value.dueAt).getTime()) return '마감 종료' + return '' +} + +// 중간 점검 보고 전송 — 현재 진행 상태를 스냅샷으로 기록 + 지시자 알림 +async function sendReport(cp: TaskCheckpointView) { + if (!task.value || reporting.value) return + reporting.value = true + try { + task.value = await taskStore.submitCheckpointReport( + projectId.value, + task.value.id, + cp.id, + reportNote.value.trim() || undefined, + ) + reportNote.value = '' + } catch { + // 인터셉터가 알림 처리 + } finally { + reporting.value = false + } +} // 담당자 액션 카드 테마(시안): changes→빨강 / review→앰버 / done→초록 / 그 외→accent const assigneeCardClass = computed(() => { switch (task.value?.status) { @@ -595,6 +662,26 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { v-if="!item.done && isChangesReview" class="citem-tag" >보완 필요 + + + + + {{ WORK_STATUS_LABEL[item.workStatus ?? 'todo'] }}
+ +
+
+
+ 중간 점검 +
+
    +
  • + {{ cp.dateLabel }} + + {{ checkpointStateLabel(cp) }} +
  • +
+