feat: 진행 보고서 작성을 모달 + 카테고리 탭 방식으로 변경

인라인 보고서 폼 제거 → '보고서 작성' 클릭 시 모달로 띄움.
모달 내 항목은 필수/관리/보안/부가 탭으로 구분해 항목별 상태를 선택하고 메모와 함께 전송.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:15:55 +09:00
parent 6577550937
commit 8a52ad0d0a
+166 -89
View File
@@ -334,25 +334,37 @@ function checkpointState(cp: TaskCheckpointView): CheckpointState {
return 'future'
}
// --- 보고서 작성 폼(인라인, 점검일 당일·담당자만) ---
// --- 보고서 작성(모달, 점검일 당일·담당자만) — 카테고리 탭 방식 ---
const reportingCpId = ref<string | null>(null)
const reportItems = ref<{ id: string; text: string; workStatus: ChecklistWorkStatus }[]>([])
const reportItems = ref<
{ id: string; text: string; category: ChecklistCategory; workStatus: ChecklistWorkStatus }[]
>([])
const reportNote = ref('')
const reportSubmitting = ref(false)
const writeTab = ref<ChecklistCategory>('필수')
function openReportForm(cp: TaskCheckpointView) {
if (!task.value) return
reportingCpId.value = cp.id
writeTab.value = '필수'
// 현재(마지막 보고) 상태로 프리필
reportItems.value = task.value.checklist
.filter((c) => c.id)
.map((c) => ({
id: c.id as string,
text: c.text,
category: c.category,
workStatus: c.workStatus ?? 'todo',
}))
reportNote.value = ''
}
// 작성 모달 탭별 항목 / 개수
function writeItemsByCat(cat: ChecklistCategory) {
return reportItems.value.filter((it) => it.category === cat)
}
function writeCatCount(cat: ChecklistCategory): number {
return writeItemsByCat(cat).length
}
function cancelReportForm() {
reportingCpId.value = null
reportItems.value = []
@@ -1271,14 +1283,14 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
class="cp-d-item"
>
<span class="cp-d-date">{{ cp.dateLabel }}</span>
<!-- 당일 + 담당자만 보고서 작성 -->
<!-- 당일 + 담당자만 보고서 작성(모달) -->
<button
v-if="checkpointState(cp) === 'today' && isAssignee"
class="cp-report-btn"
type="button"
@click="reportingCpId === cp.id ? cancelReportForm() : openReportForm(cp)"
@click="openReportForm(cp)"
>
{{ reportingCpId === cp.id ? '취소' : '보고서 작성' }}
보고서 작성
</button>
<button
v-else-if="checkpointState(cp) === 'reported' && canViewReports"
@@ -1296,60 +1308,6 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
>{{ CP_STATE_LABEL[checkpointState(cp)] }}</span>
</li>
</ul>
<!-- 보고서 작성 (인라인) 항목별 상태 선택 + 메모 -->
<div
v-if="reportingCpId"
class="cp-form"
>
<div class="cp-form-title">
진행 상태 보고서
</div>
<div
v-if="reportItems.length"
class="cp-form-items"
>
<div
v-for="it in reportItems"
:key="it.id"
class="cp-form-item"
>
<span class="cp-fi-text">{{ it.text }}</span>
<span class="ws-seg">
<button
v-for="s in WORK_STATUS_OPTIONS"
:key="s"
type="button"
class="ws-btn"
:class="[s, { active: it.workStatus === s }]"
@click="it.workStatus = s"
>{{ WORK_STATUS_LABEL[s] }}</button>
</span>
</div>
</div>
<p
v-else
class="cp-form-empty"
>
등록된 일이 없어 메모만 보고됩니다.
</p>
<textarea
v-model="reportNote"
class="cp-note-input"
rows="2"
placeholder="보고 메모(선택)"
/>
<div class="cp-form-actions">
<button
class="cp-send-btn"
type="button"
:disabled="reportSubmitting"
@click="submitReport"
>
{{ reportSubmitting ? '보내는 중…' : '지시자에게 보내기' }}
</button>
</div>
</div>
</div>
</div>
@@ -1709,6 +1667,121 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
</div>
</div>
</Teleport>
<!-- 중간 점검 보고서 작성 모달 -->
<Teleport to="body">
<div
class="modal-backdrop"
:class="{ open: !!reportingCpId }"
@click.self="cancelReportForm"
>
<div
v-if="reportingCpId"
class="cp-modal"
role="dialog"
aria-modal="true"
aria-label="진행 상태 보고서"
>
<div class="cp-modal-head">
<div>
<div class="cp-modal-title">
진행 상태 보고서
</div>
<div class="cp-modal-sub">
일의 현재 상태를 선택해 지시자에게 보고합니다.
</div>
</div>
<button
class="rv-x"
type="button"
aria-label="닫기"
@click="cancelReportForm"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M18 6 6 18M6 6l12 12" /></svg>
</button>
</div>
<!-- 카테고리 -->
<div
class="cat-tabs"
role="tablist"
>
<button
v-for="tab in CHECKLIST_CATEGORIES"
:key="tab"
type="button"
class="cat-tab"
:class="[categoryClass(tab), { active: writeTab === tab }]"
role="tab"
:aria-selected="writeTab === tab"
@click="writeTab = tab"
>
<span class="cat-dot" />
{{ tab }}
<span class="cat-count">{{ writeCatCount(tab) }}</span>
</button>
</div>
<!-- 활성 항목 + 상태 선택 -->
<div class="cp-modal-items">
<div
v-for="it in writeItemsByCat(writeTab)"
:key="it.id"
class="cp-write-item"
>
<span class="cp-write-text">{{ it.text }}</span>
<span class="ws-seg">
<button
v-for="s in WORK_STATUS_OPTIONS"
:key="s"
type="button"
class="ws-btn"
:class="[s, { active: it.workStatus === s }]"
@click="it.workStatus = s"
>{{ WORK_STATUS_LABEL[s] }}</button>
</span>
</div>
<div
v-if="writeItemsByCat(writeTab).length === 0"
class="cp-modal-empty"
>
'{{ writeTab }}' 항목이 없습니다.
</div>
</div>
<!-- 메모 + 전송 -->
<div class="cp-write-foot">
<textarea
v-model="reportNote"
class="cp-note-input"
rows="2"
placeholder="보고 메모(선택)"
/>
<div class="cp-write-actions">
<button
class="cp-cancel-btn"
type="button"
@click="cancelReportForm"
>
취소
</button>
<button
class="cp-send-btn"
type="button"
:disabled="reportSubmitting"
@click="submitReport"
>
{{ reportSubmitting ? '보내는 중…' : '지시자에게 보내기' }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</AppShell>
</template>
@@ -3411,46 +3484,48 @@ a.file-info {
background: var(--accent-hover);
}
/* 보고서 작성 폼 */
.cp-form {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.625rem;
margin-bottom: 0.75rem;
background: #fafbfc;
}
.cp-form-title {
font-size: 0.781rem;
font-weight: 700;
color: var(--text);
margin-bottom: 0.5rem;
}
.cp-form-items {
/* 보고서 작성 모달 — 항목 + 상태 선택 */
.cp-write-item {
display: flex;
flex-direction: column;
gap: 0.4375rem;
margin-bottom: 0.5rem;
gap: 0.375rem;
padding-bottom: 0.625rem;
border-bottom: 1px solid var(--border);
}
.cp-form-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
.cp-write-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.cp-fi-text {
font-size: 0.75rem;
.cp-write-text {
font-size: 0.8125rem;
color: var(--text);
}
.cp-form-item .ws-seg {
.cp-write-item .ws-seg {
margin-left: 0;
align-self: flex-start;
}
.cp-form-empty {
font-size: 0.75rem;
color: var(--text-3);
margin: 0 0 0.5rem;
.cp-write-foot {
border-top: 1px solid var(--border);
padding: 0.875rem 1.25rem 1rem;
}
.cp-form-actions {
.cp-write-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.cp-cancel-btn {
border: 1px solid var(--border-strong);
background: #fff;
color: var(--text-2);
font-family: inherit;
font-size: 0.781rem;
font-weight: 600;
padding: 0.375rem 0.75rem;
border-radius: var(--radius-sm);
cursor: pointer;
}
.cp-cancel-btn:hover {
background: #f5f6f8;
}
.cp-send-btn {
border: none;
@@ -3459,7 +3534,7 @@ a.file-info {
font-family: inherit;
font-size: 0.781rem;
font-weight: 700;
padding: 0.375rem 0.75rem;
padding: 0.375rem 0.875rem;
border-radius: var(--radius-sm);
cursor: pointer;
}
@@ -3479,7 +3554,7 @@ a.file-info {
font-size: 0.781rem;
color: var(--text);
resize: vertical;
margin-bottom: 0.5rem;
margin-bottom: 0.625rem;
}
.cp-note-input:focus {
outline: none;
@@ -3587,6 +3662,8 @@ a.file-info {
}
/* 모달 항목 목록 */
.cp-modal-items {
flex: 1;
min-height: 0;
padding: 0.75rem 1.25rem 1.25rem;
overflow-y: auto;
display: flex;