feat: 수정 요청 시 할 일 항목별 완료 체크/X 검토 추가
지시자가 승인을 검토하며 수정 요청할 때, 단순 텍스트 입력에 더해 할 일 목록에서 어떤 항목이 완성/미완성인지 체크(✓)·X로 표시할 수 있게 한다. - 백엔드: ApprovalChangesDto 에 항목별 검토 결과(checklist) 추가, requestChanges 가 전달된 항목만 done 상태로 반영(미전달 항목은 보존) - 프론트: 수정 요청 모달에 카테고리별 체크/X 토글 UI 추가, 검토 결과를 note 와 함께 전송 - 테스트: 전달된 항목만 완료 체크가 반영되는지 검증(9 tests) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,13 @@ import {
|
||||
CHECKLIST_CATEGORIES,
|
||||
type Attachment,
|
||||
type ChecklistCategory,
|
||||
type ChecklistItem,
|
||||
type Comment,
|
||||
type TaskDetail,
|
||||
type TaskStatus,
|
||||
type User,
|
||||
} from '@/mock/relay.mock'
|
||||
import type { ChecklistReviewItem } from '@/types/task'
|
||||
import { useTaskStore } from '@/stores/task.store'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
@@ -144,14 +146,47 @@ async function approveTask() {
|
||||
showApprove.value = false
|
||||
}
|
||||
}
|
||||
// 수정 요청(review→changes) — 지시자 액션. 보완 메시지 필수
|
||||
// 수정 요청(review→changes) — 지시자 액션. 보완 메시지 필수 + 항목별 완료 체크
|
||||
const changesNote = ref('')
|
||||
// 항목 ID → 완성 여부(체크=true / X=false). 모달을 열 때 현재 완료 상태로 초기화
|
||||
const reviewChecks = ref<Record<string, boolean>>({})
|
||||
const reviewDoneCount = computed(
|
||||
() => Object.values(reviewChecks.value).filter(Boolean).length,
|
||||
)
|
||||
// 수정 요청 모달 열기 — 체크리스트 검토 상태를 현재 완료값으로 초기화
|
||||
function openChanges() {
|
||||
const map: Record<string, boolean> = {}
|
||||
for (const item of task.value?.checklist ?? []) {
|
||||
if (item.id) map[item.id] = item.done
|
||||
}
|
||||
reviewChecks.value = map
|
||||
changesNote.value = ''
|
||||
showChanges.value = true
|
||||
}
|
||||
function reviewState(item: ChecklistItem): boolean {
|
||||
return item.id ? !!reviewChecks.value[item.id] : false
|
||||
}
|
||||
function toggleReview(item: ChecklistItem) {
|
||||
if (!item.id) return
|
||||
reviewChecks.value = {
|
||||
...reviewChecks.value,
|
||||
[item.id]: !reviewChecks.value[item.id],
|
||||
}
|
||||
}
|
||||
async function submitChanges() {
|
||||
if (!task.value) return
|
||||
const note = changesNote.value.trim()
|
||||
if (!note) return
|
||||
const checklist: ChecklistReviewItem[] = (task.value.checklist ?? [])
|
||||
.filter((c): c is ChecklistItem & { id: string } => !!c.id)
|
||||
.map((c) => ({ id: c.id, done: !!reviewChecks.value[c.id] }))
|
||||
try {
|
||||
task.value = await taskStore.requestChanges(projectId.value, task.value.id, note)
|
||||
task.value = await taskStore.requestChanges(
|
||||
projectId.value,
|
||||
task.value.id,
|
||||
note,
|
||||
checklist.length ? checklist : undefined,
|
||||
)
|
||||
} catch {
|
||||
// 인터셉터가 알림 처리
|
||||
} finally {
|
||||
@@ -823,7 +858,7 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
<button
|
||||
class="ap-btn reject"
|
||||
type="button"
|
||||
@click="showChanges = true"
|
||||
@click="openChanges"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1289,8 +1324,68 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
수정 요청
|
||||
</h2>
|
||||
<p class="modal-desc spaced">
|
||||
보완이 필요한 내용을 적어주세요. 담당자에게 전달되며 업무가 다시 <b>진행 중</b>으로 돌아갑니다.
|
||||
완성된 항목은 체크(✓), 보완이 필요한 항목은 X로 표시한 뒤 메시지를 적어주세요.
|
||||
담당자에게 전달되며 업무가 다시 <b>진행 중</b>으로 돌아갑니다.
|
||||
</p>
|
||||
|
||||
<!-- 할 일 완료 검토 — 항목별 체크/X -->
|
||||
<div
|
||||
v-if="task && task.checklist.length"
|
||||
class="review-block"
|
||||
>
|
||||
<div class="review-head">
|
||||
<span class="review-title">할 일 완료 검토</span>
|
||||
<span class="review-count">
|
||||
{{ reviewDoneCount }} / {{ task.checklist.length }} 완성
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="group in checklistGroups"
|
||||
:key="group.category"
|
||||
class="review-group"
|
||||
>
|
||||
<div
|
||||
class="cat-group-head"
|
||||
:class="categoryClass(group.category)"
|
||||
>
|
||||
<span class="cat-dot" />
|
||||
<span class="cat-name">{{ group.category }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="review-item"
|
||||
:class="reviewState(item) ? 'ok' : 'no'"
|
||||
@click="toggleReview(item)"
|
||||
>
|
||||
<span class="review-box">
|
||||
<svg
|
||||
v-if="reviewState(item)"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
<svg
|
||||
v-else
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M18 6 6 18M6 6l12 12" /></svg>
|
||||
</span>
|
||||
<span class="review-text">{{ item.text }}</span>
|
||||
<span class="review-tag">{{ reviewState(item) ? '완성' : '미완성' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="review-field-label">수정 요청 메시지</label>
|
||||
<textarea
|
||||
v-model="changesNote"
|
||||
class="modal-textarea"
|
||||
@@ -1511,6 +1606,107 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 수정 요청 모달 — 할 일 완료 검토(체크/X) */
|
||||
.review-block {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--bg-soft, #f9fafb);
|
||||
}
|
||||
.review-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.review-title {
|
||||
font-size: 0.813rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.review-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.review-group + .review-group {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.review-group .cat-group-head {
|
||||
padding: 0.25rem 0 0.125rem;
|
||||
}
|
||||
.review-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.4375rem 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition:
|
||||
background 0.12s,
|
||||
border-color 0.12s;
|
||||
}
|
||||
.review-item:hover {
|
||||
background: #fff;
|
||||
}
|
||||
.review-box {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1.5px solid var(--border-strong);
|
||||
background: #fff;
|
||||
color: #fff;
|
||||
}
|
||||
.review-box svg {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
.review-item.ok .review-box {
|
||||
background: var(--green);
|
||||
border-color: var(--green);
|
||||
}
|
||||
.review-item.no .review-box {
|
||||
background: var(--red);
|
||||
border-color: var(--red);
|
||||
}
|
||||
.review-text {
|
||||
flex: 1;
|
||||
font-size: 0.813rem;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.review-tag {
|
||||
font-size: 0.688rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
padding: 0.0625rem 0.4375rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.review-item.ok .review-tag {
|
||||
color: #1b7a47;
|
||||
background: #e7f3ee;
|
||||
}
|
||||
.review-item.no .review-tag {
|
||||
color: #c0394f;
|
||||
background: #fdeef0;
|
||||
}
|
||||
.review-field-label {
|
||||
display: block;
|
||||
font-size: 0.781rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
/* 체크리스트(읽기 전용) */
|
||||
.checklist {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user