feat: 수정 요청 모달을 '할 일 완료 검토' 디자인으로 개편
시안(수정 요청 모달.html)을 반영해 모달 UI를 재구성한다. - 헤더(그라데이션 배지·안내)·스크롤 본문·고정 푸터 구조로 변경 - 항목별 완료/보완을 세그먼트 토글(완료=초록 / 보완=빨강 좌측 바)로 표시, 카테고리 그룹·진행률 바·완료 카운트 동봉 - 보완 항목 수에 따라 안내 배너(warn/good)와 메시지 필수 여부가 전환 - 모든 항목 완료 시 버튼이 '승인 완료'(초록)로 바뀌어 곧바로 승인, 보완 항목이 있으면 '수정 요청 보내기 (N건)'(빨강)으로 메시지와 함께 반려 - 모달 진입 시 전 항목을 '완료'로 초기화(담당자가 완료를 주장하며 요청) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,18 +146,38 @@ async function approveTask() {
|
||||
showApprove.value = false
|
||||
}
|
||||
}
|
||||
// 수정 요청(review→changes) — 지시자 액션. 보완 메시지 필수 + 항목별 완료 체크
|
||||
// 할 일 완료 검토(지시자) — 모든 항목 '완료'면 승인, 보완 항목이 있으면 메시지와 함께 수정 요청
|
||||
const changesNote = ref('')
|
||||
// 항목 ID → 완성 여부(체크=true / X=false). 모달을 열 때 현재 완료 상태로 초기화
|
||||
// 항목 ID → 완료 여부(완료=true / 보완=false)
|
||||
const reviewChecks = ref<Record<string, boolean>>({})
|
||||
const reviewTotal = computed(() => task.value?.checklist.length ?? 0)
|
||||
const reviewDoneCount = computed(
|
||||
() => Object.values(reviewChecks.value).filter(Boolean).length,
|
||||
)
|
||||
// 수정 요청 모달 열기 — 체크리스트 검토 상태를 현재 완료값으로 초기화
|
||||
const reviewTodoCount = computed(() => reviewTotal.value - reviewDoneCount.value)
|
||||
const reviewPct = computed(() =>
|
||||
reviewTotal.value
|
||||
? Math.round((reviewDoneCount.value / reviewTotal.value) * 100)
|
||||
: 0,
|
||||
)
|
||||
// 보완 항목이 없으면 곧바로 승인 가능
|
||||
const reviewAllDone = computed(() => reviewTodoCount.value === 0)
|
||||
// 제출 가능 여부 — 보완 항목이 있으면 메시지 필수
|
||||
const reviewSubmittable = computed(
|
||||
() => reviewAllDone.value || changesNote.value.trim().length > 0,
|
||||
)
|
||||
// 담당자 표기(푸터)
|
||||
const assigneeNames = computed(() => {
|
||||
const names = task.value?.assignees.map((a) => a.name) ?? []
|
||||
if (names.length === 0) return '담당자'
|
||||
if (names.length === 1) return names[0]
|
||||
return `${names[0]} 외 ${names.length - 1}명`
|
||||
})
|
||||
// 수정 요청 모달 열기 — 모든 항목을 '완료'로 초기화(담당자가 완료를 주장하며 요청)
|
||||
function openChanges() {
|
||||
const map: Record<string, boolean> = {}
|
||||
for (const item of task.value?.checklist ?? []) {
|
||||
if (item.id) map[item.id] = item.done
|
||||
if (item.id) map[item.id] = true
|
||||
}
|
||||
reviewChecks.value = map
|
||||
changesNote.value = ''
|
||||
@@ -166,27 +186,37 @@ function openChanges() {
|
||||
function reviewState(item: ChecklistItem): boolean {
|
||||
return item.id ? !!reviewChecks.value[item.id] : false
|
||||
}
|
||||
function toggleReview(item: ChecklistItem) {
|
||||
function setReview(item: ChecklistItem, done: boolean) {
|
||||
if (!item.id) return
|
||||
reviewChecks.value = {
|
||||
...reviewChecks.value,
|
||||
[item.id]: !reviewChecks.value[item.id],
|
||||
}
|
||||
reviewChecks.value = { ...reviewChecks.value, [item.id]: done }
|
||||
}
|
||||
// 카테고리 → 그룹 도트 색 클래스
|
||||
function groupClass(cat: ChecklistCategory): string {
|
||||
const map: Record<ChecklistCategory, string> = {
|
||||
필수: 'g-req',
|
||||
관리: 'g-mng',
|
||||
보안: 'g-sec',
|
||||
부가: 'g-ext',
|
||||
}
|
||||
return map[cat]
|
||||
}
|
||||
// 검토 제출 — 전부 완료면 승인, 보완 항목이 있으면 수정 요청
|
||||
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] }))
|
||||
if (!task.value || !reviewSubmittable.value) return
|
||||
try {
|
||||
task.value = await taskStore.requestChanges(
|
||||
projectId.value,
|
||||
task.value.id,
|
||||
note,
|
||||
checklist.length ? checklist : undefined,
|
||||
)
|
||||
if (reviewAllDone.value) {
|
||||
task.value = await taskStore.approve(projectId.value, task.value.id)
|
||||
} else {
|
||||
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] }))
|
||||
task.value = await taskStore.requestChanges(
|
||||
projectId.value,
|
||||
task.value.id,
|
||||
changesNote.value.trim(),
|
||||
checklist.length ? checklist : undefined,
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// 인터셉터가 알림 처리
|
||||
} finally {
|
||||
@@ -1308,58 +1338,92 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 수정 요청 -->
|
||||
<!-- 할 일 완료 검토(수정 요청 / 승인) -->
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
:class="{ open: showChanges }"
|
||||
@click.self="showChanges = false"
|
||||
>
|
||||
<div
|
||||
class="modal"
|
||||
v-if="task"
|
||||
class="rv-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="할 일 완료 검토"
|
||||
>
|
||||
<div class="modal-body left">
|
||||
<h2 class="modal-title">
|
||||
수정 요청
|
||||
</h2>
|
||||
<p class="modal-desc spaced">
|
||||
완성된 항목은 체크(✓), 보완이 필요한 항목은 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 class="rv-head">
|
||||
<div class="rv-head-top">
|
||||
<div class="rv-badge">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" /></svg>
|
||||
</div>
|
||||
<div class="rv-htext">
|
||||
<div class="rv-title">
|
||||
할 일 완료 검토
|
||||
</div>
|
||||
<div class="rv-sub">
|
||||
완료된 항목은 <span class="ok">완료</span>, 보완이 필요한 항목은 <span class="no">보완</span>으로 표시하세요.
|
||||
보완 항목이 있으면 메시지와 함께 담당자에게 전달되어 업무가 다시 <b>진행 중</b>으로 돌아갑니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="rv-x"
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
@click="showChanges = false"
|
||||
>
|
||||
<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="rv-body">
|
||||
<div
|
||||
v-if="task.checklist.length"
|
||||
class="ck-card"
|
||||
>
|
||||
<div class="ck-top">
|
||||
<span class="ck-top-name">진행률</span>
|
||||
<span class="ck-progress">
|
||||
<span class="pbar"><i
|
||||
:class="{ red: reviewDoneCount === 0 }"
|
||||
:style="{ width: reviewPct + '%' }"
|
||||
/></span>
|
||||
</span>
|
||||
<span class="ck-stat"><b>{{ reviewDoneCount }}</b> / {{ reviewTotal }} 완료</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="group in checklistGroups"
|
||||
:key="group.category"
|
||||
class="review-group"
|
||||
class="ck-group"
|
||||
:class="groupClass(group.category)"
|
||||
>
|
||||
<div
|
||||
class="cat-group-head"
|
||||
:class="categoryClass(group.category)"
|
||||
>
|
||||
<span class="cat-dot" />
|
||||
<span class="cat-name">{{ group.category }}</span>
|
||||
<div class="ck-glabel">
|
||||
<span class="ck-dot" />{{ group.category }}
|
||||
</div>
|
||||
<button
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="review-item"
|
||||
:class="reviewState(item) ? 'ok' : 'no'"
|
||||
@click="toggleReview(item)"
|
||||
class="ck-item"
|
||||
:class="reviewState(item) ? 'done' : 'todo'"
|
||||
>
|
||||
<span class="review-box">
|
||||
<span class="ck-state">
|
||||
<svg
|
||||
v-if="reviewState(item)"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1379,34 +1443,117 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
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>
|
||||
<span class="ck-text">{{ item.text }}</span>
|
||||
<div class="seg">
|
||||
<button
|
||||
type="button"
|
||||
class="done"
|
||||
@click="setReview(item, true)"
|
||||
>
|
||||
완료
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="todo"
|
||||
@click="setReview(item, false)"
|
||||
>
|
||||
보완
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="review-field-label">수정 요청 메시지</label>
|
||||
<textarea
|
||||
v-model="changesNote"
|
||||
class="modal-textarea"
|
||||
placeholder="예) 세로형 카피 가독성이 낮으니 자간과 대비를 조정해주세요."
|
||||
/>
|
||||
<!-- 수정 요청 메시지 -->
|
||||
<div class="rev-sec">
|
||||
<div
|
||||
class="rev-flag"
|
||||
:class="reviewAllDone ? 'good' : 'warn'"
|
||||
>
|
||||
<svg
|
||||
v-if="reviewAllDone"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
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="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" /><path d="M12 9v4M12 17h.01" /></svg>
|
||||
<span v-if="reviewAllDone">
|
||||
모든 항목이 완료되었습니다. 메시지 없이 바로 승인할 수 있어요.
|
||||
</span>
|
||||
<span v-else>
|
||||
<b>{{ reviewTodoCount }}개</b> 항목에 보완이 필요합니다. 무엇을 수정해야 하는지 적어 주세요.
|
||||
</span>
|
||||
</div>
|
||||
<div class="rev-label">
|
||||
수정 요청 메시지
|
||||
<span class="opt">{{ reviewAllDone ? '· 선택' : '· 보완 항목이 있어 필수' }}</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="changesNote"
|
||||
class="rev-ta"
|
||||
:disabled="reviewAllDone"
|
||||
placeholder="예) '로그인 상태 유지' 옵션이 동작하지 않습니다. 체크 후에도 세션이 유지되도록 수정해 주세요."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
|
||||
<!-- 푸터 -->
|
||||
<div class="rv-foot">
|
||||
<span class="foot-note">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7z" /></svg>
|
||||
<span>{{ assigneeNames }} 님에게 전달됩니다</span>
|
||||
</span>
|
||||
<button
|
||||
class="mbtn"
|
||||
class="btn"
|
||||
type="button"
|
||||
@click="showChanges = false"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="mbtn danger"
|
||||
class="btn"
|
||||
:class="reviewAllDone ? 'approve' : 'danger'"
|
||||
type="button"
|
||||
:disabled="!changesNote.trim()"
|
||||
:disabled="!reviewSubmittable"
|
||||
@click="submitChanges"
|
||||
>
|
||||
수정 요청 보내기
|
||||
<svg
|
||||
v-if="reviewAllDone"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
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="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7z" /></svg>
|
||||
{{ reviewAllDone ? '승인 완료' : `수정 요청 보내기 (${reviewTodoCount}건)` }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1606,105 +1753,456 @@ 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 {
|
||||
/* ===== 할 일 완료 검토 모달(수정 요청 / 승인) ===== */
|
||||
.rv-modal {
|
||||
width: 100%;
|
||||
max-width: 37.5rem;
|
||||
max-height: calc(100vh - 3rem);
|
||||
background: var(--panel);
|
||||
border-radius: 1rem;
|
||||
box-shadow: var(--shadow-pop);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.review-title {
|
||||
font-size: 0.813rem;
|
||||
|
||||
/* 헤더 */
|
||||
.rv-head {
|
||||
position: relative;
|
||||
padding: 1.25rem 1.375rem 1.125rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.rv-head-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.rv-badge {
|
||||
width: 2.375rem;
|
||||
height: 2.375rem;
|
||||
border-radius: 0.6875rem;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(140deg, #6366f1, #4338ca);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(79, 70, 229, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
.rv-badge svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
.rv-htext {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 0.0625rem;
|
||||
}
|
||||
.rv-title {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.0187rem;
|
||||
}
|
||||
.rv-sub {
|
||||
font-size: 0.781rem;
|
||||
color: var(--text-2);
|
||||
margin-top: 0.25rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.rv-sub b {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.rv-sub .ok {
|
||||
color: var(--green);
|
||||
font-weight: 700;
|
||||
}
|
||||
.rv-sub .no {
|
||||
color: var(--red);
|
||||
font-weight: 700;
|
||||
}
|
||||
.rv-x {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-3);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rv-x:hover {
|
||||
background: rgba(20, 24, 33, 0.06);
|
||||
color: var(--text);
|
||||
}
|
||||
.review-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
.rv-x svg {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
.review-group + .review-group {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
/* 본문 */
|
||||
.rv-body {
|
||||
overflow-y: auto;
|
||||
padding: 1.125rem 1.375rem 1.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
.review-group .cat-group-head {
|
||||
padding: 0.25rem 0 0.125rem;
|
||||
|
||||
/* 체크리스트 카드 */
|
||||
.ck-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.review-item {
|
||||
.ck-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem 0.8125rem;
|
||||
background: #fbfbfd;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.ck-top-name {
|
||||
font-size: 0.844rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.0125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ck-progress {
|
||||
flex: 1;
|
||||
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;
|
||||
min-width: 0;
|
||||
}
|
||||
.review-item:hover {
|
||||
.pbar {
|
||||
flex: 1;
|
||||
height: 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #e7e9ed;
|
||||
overflow: hidden;
|
||||
min-width: 3.75rem;
|
||||
}
|
||||
.pbar > i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 0.25rem;
|
||||
background: var(--green);
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
.pbar > i.red {
|
||||
background: var(--red);
|
||||
}
|
||||
.ck-stat {
|
||||
font-size: 0.781rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ck-stat b {
|
||||
color: var(--green);
|
||||
}
|
||||
.ck-group {
|
||||
padding: 0.8125rem 1rem 0.25rem;
|
||||
}
|
||||
.ck-group + .ck-group {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.ck-glabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-2);
|
||||
margin-bottom: 0.5625rem;
|
||||
}
|
||||
.ck-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.g-req .ck-dot {
|
||||
background: var(--accent);
|
||||
}
|
||||
.g-mng .ck-dot {
|
||||
background: #1b7a47;
|
||||
}
|
||||
.g-sec .ck-dot {
|
||||
background: #c0394f;
|
||||
}
|
||||
.g-ext .ck-dot {
|
||||
background: var(--amber);
|
||||
}
|
||||
.ck-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8125rem;
|
||||
padding: 0.6875rem 0.75rem;
|
||||
margin-bottom: 0.5625rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5625rem;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
transition:
|
||||
background 0.14s,
|
||||
border-color 0.14s;
|
||||
}
|
||||
.review-box {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
.ck-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.1875rem;
|
||||
border-radius: 0.5625rem 0 0 0.5625rem;
|
||||
background: var(--red);
|
||||
opacity: 0;
|
||||
transition: opacity 0.14s;
|
||||
}
|
||||
.ck-state {
|
||||
width: 1.875rem;
|
||||
height: 1.875rem;
|
||||
border-radius: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1.5px solid var(--border-strong);
|
||||
background: #fff;
|
||||
color: #fff;
|
||||
transition: background 0.14s;
|
||||
}
|
||||
.review-box svg {
|
||||
.ck-state svg {
|
||||
width: 1.0625rem;
|
||||
height: 1.0625rem;
|
||||
}
|
||||
.ck-text {
|
||||
flex: 1;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.00625rem;
|
||||
}
|
||||
|
||||
/* 세그먼트 토글(완료/보완) */
|
||||
.seg {
|
||||
display: flex;
|
||||
background: #f1f2f4;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5625rem;
|
||||
padding: 0.1875rem;
|
||||
gap: 0.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.seg button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-3);
|
||||
padding: 0.375rem 0.6875rem;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.seg button:hover {
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
/* 완료 상태 */
|
||||
.ck-item.done {
|
||||
background: #f4fbf6;
|
||||
border-color: var(--green-border);
|
||||
}
|
||||
.ck-item.done .ck-state {
|
||||
background: var(--green);
|
||||
}
|
||||
.ck-item.done .seg button.done {
|
||||
background: #fff;
|
||||
color: var(--green);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.ck-item.done .seg button.done:hover {
|
||||
color: var(--green);
|
||||
}
|
||||
/* 보완 상태 */
|
||||
.ck-item.todo {
|
||||
background: var(--red-weak);
|
||||
border-color: var(--red-border);
|
||||
}
|
||||
.ck-item.todo::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.ck-item.todo .ck-state {
|
||||
background: var(--red);
|
||||
}
|
||||
.ck-item.todo .seg {
|
||||
background: #f8dde1;
|
||||
border-color: var(--red-border);
|
||||
}
|
||||
.ck-item.todo .seg button.todo {
|
||||
background: #fff;
|
||||
color: var(--red);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.ck-item.todo .seg button.todo:hover {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* 수정 요청 메시지 */
|
||||
.rev-sec {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
.rev-flag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5625rem;
|
||||
font-size: 0.781rem;
|
||||
font-weight: 600;
|
||||
padding: 0.625rem 0.8125rem;
|
||||
border-radius: 0.5625rem;
|
||||
margin-bottom: 0.6875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.rev-flag svg {
|
||||
width: 0.9375rem;
|
||||
height: 0.9375rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rev-flag.warn {
|
||||
color: #a8324a;
|
||||
background: var(--red-weak);
|
||||
border: 1px solid var(--red-border);
|
||||
}
|
||||
.rev-flag.good {
|
||||
color: #1b7a40;
|
||||
background: var(--green-weak);
|
||||
border: 1px solid var(--green-border);
|
||||
}
|
||||
.rev-label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.rev-label .opt {
|
||||
font-size: 0.719rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.rev-ta {
|
||||
width: 100%;
|
||||
min-height: 6rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 0.5625rem;
|
||||
padding: 0.6875rem 0.8125rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
background: #fff;
|
||||
}
|
||||
.rev-ta::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.rev-ta:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-weak);
|
||||
}
|
||||
.rev-ta:disabled {
|
||||
background: #f7f8fa;
|
||||
color: var(--text-3);
|
||||
cursor: not-allowed;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* 푸터 */
|
||||
.rv-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.875rem 1.375rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: #fbfbfd;
|
||||
}
|
||||
.foot-note {
|
||||
margin-right: auto;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4375rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.foot-note svg {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.review-item.ok .review-box {
|
||||
background: var(--green);
|
||||
border-color: var(--green);
|
||||
.rv-foot .btn {
|
||||
height: 2.625rem;
|
||||
padding: 0 1.25rem;
|
||||
border-radius: 0.5625rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--panel);
|
||||
color: var(--text-2);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
line-height: 1;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.review-item.no .review-box {
|
||||
.rv-foot .btn:hover {
|
||||
background: #f7f8fa;
|
||||
color: var(--text);
|
||||
}
|
||||
.rv-foot .btn svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
.rv-foot .btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.rv-foot .btn.danger {
|
||||
background: var(--red);
|
||||
border-color: var(--red);
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 2px rgba(214, 69, 93, 0.4);
|
||||
}
|
||||
.rv-foot .btn.danger:hover {
|
||||
background: var(--red-hover, #c23950);
|
||||
border-color: var(--red-hover, #c23950);
|
||||
color: #fff;
|
||||
}
|
||||
.rv-foot .btn.danger:disabled:hover {
|
||||
background: var(--red);
|
||||
border-color: var(--red);
|
||||
}
|
||||
.review-text {
|
||||
flex: 1;
|
||||
font-size: 0.813rem;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
.rv-foot .btn.approve {
|
||||
background: var(--green);
|
||||
border-color: var(--green);
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 2px rgba(31, 157, 87, 0.38);
|
||||
}
|
||||
.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;
|
||||
.rv-foot .btn.approve:hover {
|
||||
background: #1b8a4d;
|
||||
border-color: #1b8a4d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 체크리스트(읽기 전용) */
|
||||
|
||||
Reference in New Issue
Block a user