feat: 진행 보고 작성/열람 모달 UI 개편(시안 반영)

시안(진행 상태 보고서/받은 진행 보고) 기준으로 두 모달 전면 개편:
- 전체 탭 + 카테고리 탭(색상 dot·건수), 전체 탭에서 카테고리별 그룹 헤더로 묶음
- 작성(작업자): 항목별 상태색 드롭다운(StatusSelect, body Teleport), 보고 메모, 하단 상태 요약(대기/진행/완료 N)
- 열람(지시자·담당자): '진행 보고 도착' 태그 + 보고자 아바타/이름/점검일·시각, 읽기전용 상태 배지, 보고자 메모 박스
- 신규 StatusSelect 컴포넌트 추가, 기존 cp-modal/ws-* 스타일 정리, 미사용 DropdownSelect import 제거

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:18:43 +09:00
parent 1df0f2e4e2
commit 1f7d7dcebb
2 changed files with 766 additions and 320 deletions
+199
View File
@@ -0,0 +1,199 @@
<script setup lang="ts">
// 진행 상태(대기/진행 중/완료) 선택 드롭다운 — 상태색 테마. 메뉴는 body Teleport(스크롤 클리핑 방지).
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import type { ChecklistWorkStatus } from '@/mock/relay.mock'
const props = defineProps<{ modelValue: ChecklistWorkStatus }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: ChecklistWorkStatus): void }>()
const META: Record<ChecklistWorkStatus, { label: string; color: string; weak: string }> = {
todo: { label: '대기', color: '#9298a3', weak: '#eef0f2' },
doing: { label: '진행 중', color: '#2563eb', weak: '#e9f0fd' },
done: { label: '완료', color: '#1f9d57', weak: '#e7f5ed' },
}
const ORDER: ChecklistWorkStatus[] = ['todo', 'doing', 'done']
const cur = computed(() => META[props.modelValue])
const open = ref(false)
const triggerRef = ref<HTMLElement | null>(null)
const menuRef = ref<HTMLElement | null>(null)
const menuStyle = ref<Record<string, string>>({})
function positionMenu(): void {
const el = triggerRef.value
if (!el) return
const r = el.getBoundingClientRect()
menuStyle.value = {
top: `${r.bottom + 5}px`,
right: `${Math.max(0, window.innerWidth - r.right)}px`,
}
}
function toggle(): void {
open.value = !open.value
if (open.value) void nextTick(positionMenu)
}
function pick(v: ChecklistWorkStatus): void {
emit('update:modelValue', v)
open.value = false
}
function onDoc(e: MouseEvent): void {
if (!open.value) return
const t = e.target as Node
if (triggerRef.value?.contains(t) || menuRef.value?.contains(t)) return
open.value = false
}
function onReposition(): void {
if (open.value) open.value = false
}
onMounted(() => {
document.addEventListener('click', onDoc)
window.addEventListener('scroll', onReposition, true)
window.addEventListener('resize', onReposition)
})
onBeforeUnmount(() => {
document.removeEventListener('click', onDoc)
window.removeEventListener('scroll', onReposition, true)
window.removeEventListener('resize', onReposition)
})
</script>
<template>
<div class="ss">
<button
ref="triggerRef"
type="button"
class="ss-btn"
:class="{ open }"
:style="{ '--c': cur.color, '--w': cur.weak }"
@click="toggle"
>
<span class="ss-dot" />
<span class="ss-lbl">{{ cur.label }}</span>
<svg
class="ss-chev"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="m6 9 6 6 6-6" /></svg>
</button>
<Teleport to="body">
<div
v-if="open"
ref="menuRef"
class="ss-menu"
:style="menuStyle"
>
<button
v-for="k in ORDER"
:key="k"
type="button"
class="ss-opt"
:class="{ on: k === modelValue }"
@click="pick(k)"
>
<span
class="ss-dot"
:style="{ background: META[k].color }"
/>{{ META[k].label }}
<svg
v-if="k === modelValue"
class="ss-check"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M20 6 9 17l-5-5" /></svg>
</button>
</div>
</Teleport>
</div>
</template>
<style scoped>
.ss {
position: relative;
flex-shrink: 0;
}
.ss-btn {
display: inline-flex;
align-items: center;
gap: 7px;
height: 30px;
min-width: 100px;
padding: 0 9px 0 11px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: var(--w);
color: var(--c);
font-family: inherit;
font-size: 0.781rem;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
}
.ss-btn.open {
border-color: var(--c);
}
.ss-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--c);
flex-shrink: 0;
}
.ss-btn .ss-lbl {
flex: 1;
text-align: left;
}
.ss-chev {
width: 14px;
height: 14px;
}
.ss-menu {
position: fixed;
z-index: 1000;
width: 9rem;
background: #fff;
border: 1px solid var(--border);
border-radius: 9px;
box-shadow: 0 8px 24px rgba(20, 24, 33, 0.16);
padding: 5px;
}
.ss-opt {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
border: none;
background: none;
font-family: inherit;
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-2);
padding: 8px;
border-radius: 6px;
cursor: pointer;
text-align: left;
}
.ss-opt:hover {
background: #f4f5f7;
}
.ss-opt.on {
color: var(--text);
}
.ss-opt .ss-dot {
width: 9px;
height: 9px;
}
.ss-opt .ss-check {
margin-left: auto;
color: var(--text);
width: 15px;
height: 15px;
}
</style>
File diff suppressed because it is too large Load Diff