refactor(tokens): A-8 Batch 3 — PreviewPanel / DocumentViewer / +layout(toast)

가장 큰 위험 batch. Phase A 디자인 시스템 정착 마지막 mechanical refactor
(8 파일 8/8 누적 — core components 0 hit 달성).

PreviewPanel (53 hits → 0):
- bg-[var(--sidebar-bg)] → bg-sidebar (메인 aside)
- bg-[var(--bg)]         → bg-bg (input 배경)
- bg-[var(--surface)]    → bg-surface (hover)
- bg-[var(--accent)]     → bg-accent + hover:bg-accent-hover (저장 버튼)
- bg-[var(--error)]      → bg-error (삭제 확인)
- text/border 토큰 일괄 swap
- focus:border-accent (input)
- confidence 색상 (green/amber/red palette)은 plan B3 명시 없어 그대로

DocumentViewer (28 hits → 0):
- 뷰어 본체 bg-surface border-default
- 툴바 bg-sidebar
- 마크다운 편집 탭 bg-surface, edit textarea bg-bg
- 상태별 hover 토큰 swap
- 뉴스 article 태그 blue-900/30 그대로 (lint:tokens 미검출)

+layout.svelte (10 hits → 0):
- nav 잔여 var() (햄버거, 로고, 메뉴 링크) 토큰 swap
- 로딩 텍스트 text-dim
- toast 영역 의미 swap (plan B3 명시):
  * green-900/200  → bg-success/10 + text-success + border-success/30
  * red-900/200    → bg-error/10 + text-error + border-error/30
  * yellow-900/200 → bg-warning/10 + text-warning + border-warning/30
  * blue-900/200   → bg-accent/10 + text-accent + border-accent/30
- class:* 디렉티브 8개 → script TOAST_CLASS dict + dynamic class binding
  (svelte 5에서 슬래시 포함 클래스명을 class: 디렉티브로 못 씀)

검증:
- npm run lint:tokens : 360 → 269 (-91, B3 파일 0 hit)
- 누적 진행: 421 → 269 (-152 / 8 파일 완료, plan 정정 목표 정확 달성)
- npm run build       : 
- npx svelte-check    :  0 errors
- ⚠ 3-risk grep       : hover/border-border/var() 잔여 0건

A-8 종료 시점 상태:
- core components 8 파일: lint:tokens 0 hit 
- routes 7 파일 잔존 (~269): news 92, settings 47, documents/[id] 36,
  +page 28, documents 26, inbox 25, login 15
- lint:tokens 강제화 (pre-commit hook)는 Phase D + F 완료 후 별도 commit

플랜: ~/.claude/plans/compressed-churning-dragon.md §A.4 Batch 3
This commit is contained in:
Hyungi Ahn
2026-04-07 12:14:48 +09:00
parent 8ec89517ee
commit c294df5987
3 changed files with 82 additions and 81 deletions

View File

@@ -125,18 +125,18 @@
}
</script>
<aside class="h-full flex flex-col bg-[var(--sidebar-bg)] border-l border-[var(--border)] overflow-y-auto">
<aside class="h-full flex flex-col bg-sidebar border-l border-default overflow-y-auto">
<!-- 헤더 -->
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--border)] shrink-0">
<div class="flex items-center justify-between px-4 py-3 border-b border-default shrink-0">
<div class="flex items-center gap-2 min-w-0">
<FormatIcon format={doc.file_format} size={16} />
<span class="text-sm font-medium truncate">{doc.title || '제목 없음'}</span>
</div>
<div class="flex items-center gap-1">
<a href="/documents/{doc.id}" class="p-1 rounded hover:bg-[var(--surface)] text-[var(--text-dim)]" title="전체 보기">
<a href="/documents/{doc.id}" class="p-1 rounded hover:bg-surface text-dim" title="전체 보기">
<ExternalLink size={14} />
</a>
<button onclick={onclose} class="p-1 rounded hover:bg-[var(--surface)] text-[var(--text-dim)]" aria-label="닫기">
<button onclick={onclose} class="p-1 rounded hover:bg-surface text-dim" aria-label="닫기">
<X size={16} />
</button>
</div>
@@ -145,31 +145,31 @@
<div class="flex-1 p-4 space-y-4">
<!-- 메모 -->
<div>
<h4 class="text-xs font-semibold text-[var(--text-dim)] uppercase mb-1.5">메모</h4>
<h4 class="text-xs font-semibold text-dim uppercase mb-1.5">메모</h4>
{#if noteEditing}
<textarea
bind:value={noteText}
class="w-full h-24 px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-sm text-[var(--text)] resize-none outline-none focus:border-[var(--accent)]"
class="w-full h-24 px-3 py-2 bg-bg border border-default rounded-lg text-sm text-text resize-none outline-none focus:border-accent"
placeholder="메모 입력..."
></textarea>
<div class="flex gap-2 mt-1.5">
<button
onclick={saveNote}
disabled={noteSaving}
class="flex items-center gap-1 px-2 py-1 text-xs bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] disabled:opacity-50"
class="flex items-center gap-1 px-2 py-1 text-xs bg-accent text-white rounded hover:bg-accent-hover disabled:opacity-50"
>
<Save size={12} /> 저장
</button>
<button
onclick={() => { noteEditing = false; noteText = doc.user_note || ''; }}
class="px-2 py-1 text-xs text-[var(--text-dim)] hover:text-[var(--text)]"
class="px-2 py-1 text-xs text-dim hover:text-text"
>취소</button>
</div>
{:else}
<button
onclick={() => noteEditing = true}
class="w-full text-left px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-sm min-h-[40px]
{noteText ? 'text-[var(--text)]' : 'text-[var(--text-dim)]'}"
class="w-full text-left px-3 py-2 bg-bg border border-default rounded-lg text-sm min-h-[40px]
{noteText ? 'text-text' : 'text-dim'}"
>
{noteText || '메모 추가...'}
</button>
@@ -178,40 +178,40 @@
<!-- 편집 URL -->
<div>
<h4 class="text-xs font-semibold text-[var(--text-dim)] uppercase mb-1.5">편집 링크</h4>
<h4 class="text-xs font-semibold text-dim uppercase mb-1.5">편집 링크</h4>
{#if editUrlEditing}
<div class="flex gap-1">
<input
bind:value={editUrlText}
placeholder="Synology Drive URL 붙여넣기..."
class="flex-1 px-2 py-1 bg-[var(--bg)] border border-[var(--border)] rounded text-xs text-[var(--text)] outline-none focus:border-[var(--accent)]"
class="flex-1 px-2 py-1 bg-bg border border-default rounded text-xs text-text outline-none focus:border-accent"
/>
<button onclick={saveEditUrl} class="px-2 py-1 text-xs bg-[var(--accent)] text-white rounded">저장</button>
<button onclick={() => { editUrlEditing = false; editUrlText = doc.edit_url || ''; }} class="px-2 py-1 text-xs text-[var(--text-dim)]">취소</button>
<button onclick={saveEditUrl} class="px-2 py-1 text-xs bg-accent text-white rounded">저장</button>
<button onclick={() => { editUrlEditing = false; editUrlText = doc.edit_url || ''; }} class="px-2 py-1 text-xs text-dim">취소</button>
</div>
{:else if doc.edit_url}
<div class="flex items-center gap-1">
<a href={doc.edit_url} target="_blank" class="text-xs text-[var(--accent)] truncate hover:underline">{doc.edit_url}</a>
<button onclick={() => editUrlEditing = true} class="text-[10px] text-[var(--text-dim)] hover:text-[var(--text)]">수정</button>
<a href={doc.edit_url} target="_blank" class="text-xs text-accent truncate hover:underline">{doc.edit_url}</a>
<button onclick={() => editUrlEditing = true} class="text-[10px] text-dim hover:text-text">수정</button>
</div>
{:else}
<button
onclick={() => editUrlEditing = true}
class="text-xs text-[var(--text-dim)] hover:text-[var(--accent)]"
class="text-xs text-dim hover:text-accent"
>+ URL 추가</button>
{/if}
</div>
<!-- 태그 -->
<div>
<h4 class="text-xs font-semibold text-[var(--text-dim)] uppercase mb-1.5">태그</h4>
<h4 class="text-xs font-semibold text-dim uppercase mb-1.5">태그</h4>
<div class="flex flex-wrap gap-1 mb-2">
{#each doc.ai_tags || [] as tag}
<span class="inline-flex items-center gap-0.5">
<TagPill {tag} clickable={false} />
<button
onclick={() => removeTag(tag)}
class="text-[var(--text-dim)] hover:text-[var(--error)] text-[10px]"
class="text-dim hover:text-error text-[10px]"
title="삭제"
>×</button>
</span>
@@ -222,14 +222,14 @@
<input
bind:value={newTag}
placeholder="태그 입력..."
class="flex-1 px-2 py-1 bg-[var(--bg)] border border-[var(--border)] rounded text-xs text-[var(--text)] outline-none focus:border-[var(--accent)]"
class="flex-1 px-2 py-1 bg-bg border border-default rounded text-xs text-text outline-none focus:border-accent"
/>
<button type="submit" class="px-2 py-1 text-xs bg-[var(--accent)] text-white rounded">추가</button>
<button type="submit" class="px-2 py-1 text-xs bg-accent text-white rounded">추가</button>
</form>
{:else}
<button
onclick={() => tagEditing = true}
class="flex items-center gap-1 text-xs text-[var(--text-dim)] hover:text-[var(--accent)]"
class="flex items-center gap-1 text-xs text-dim hover:text-accent"
>
<Plus size={12} /> 태그 추가
</button>
@@ -239,12 +239,12 @@
<!-- AI 분류 -->
{#if doc.ai_domain}
<div>
<h4 class="text-xs font-semibold text-[var(--text-dim)] uppercase mb-1.5">분류</h4>
<h4 class="text-xs font-semibold text-dim uppercase mb-1.5">분류</h4>
<!-- domain breadcrumb -->
<div class="flex flex-wrap gap-1 mb-2">
{#each doc.ai_domain.split('/') as part, i}
{#if i > 0}<span class="text-[10px] text-[var(--text-dim)]"></span>{/if}
<span class="text-xs text-[var(--accent)]">{part}</span>
{#if i > 0}<span class="text-[10px] text-dim"></span>{/if}
<span class="text-xs text-accent">{part}</span>
{/each}
</div>
<!-- document_type + confidence -->
@@ -268,30 +268,30 @@
<!-- 파일 정보 -->
<div>
<h4 class="text-xs font-semibold text-[var(--text-dim)] uppercase mb-1.5">정보</h4>
<h4 class="text-xs font-semibold text-dim uppercase mb-1.5">정보</h4>
<dl class="space-y-1.5 text-xs">
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">포맷</dt>
<dt class="text-dim">포맷</dt>
<dd class="uppercase">{doc.file_format}{doc.original_format ? ` (원본: ${doc.original_format})` : ''}</dd>
</div>
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">크기</dt>
<dt class="text-dim">크기</dt>
<dd>{formatSize(doc.file_size)}</dd>
</div>
{#if doc.source_channel}
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">출처</dt>
<dt class="text-dim">출처</dt>
<dd>{doc.source_channel}</dd>
</div>
{/if}
{#if doc.data_origin}
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">구분</dt>
<dt class="text-dim">구분</dt>
<dd>{doc.data_origin}</dd>
</div>
{/if}
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">등록일</dt>
<dt class="text-dim">등록일</dt>
<dd>{formatDate(doc.created_at)}</dd>
</div>
</dl>
@@ -299,42 +299,42 @@
<!-- 처리 상태 -->
<div>
<h4 class="text-xs font-semibold text-[var(--text-dim)] uppercase mb-1.5">처리</h4>
<h4 class="text-xs font-semibold text-dim uppercase mb-1.5">처리</h4>
<dl class="space-y-1 text-xs">
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">추출</dt>
<dd class={doc.extracted_at ? 'text-[var(--success)]' : 'text-[var(--text-dim)]'}>{doc.extracted_at ? '완료' : '대기'}</dd>
<dt class="text-dim">추출</dt>
<dd class={doc.extracted_at ? 'text-success' : 'text-dim'}>{doc.extracted_at ? '완료' : '대기'}</dd>
</div>
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">분류</dt>
<dd class={doc.ai_processed_at ? 'text-[var(--success)]' : 'text-[var(--text-dim)]'}>{doc.ai_processed_at ? '완료' : '대기'}</dd>
<dt class="text-dim">분류</dt>
<dd class={doc.ai_processed_at ? 'text-success' : 'text-dim'}>{doc.ai_processed_at ? '완료' : '대기'}</dd>
</div>
<div class="flex justify-between">
<dt class="text-[var(--text-dim)]">임베딩</dt>
<dd class={doc.embedded_at ? 'text-[var(--success)]' : 'text-[var(--text-dim)]'}>{doc.embedded_at ? '완료' : '대기'}</dd>
<dt class="text-dim">임베딩</dt>
<dd class={doc.embedded_at ? 'text-success' : 'text-dim'}>{doc.embedded_at ? '완료' : '대기'}</dd>
</div>
</dl>
</div>
<!-- 삭제 -->
<div class="pt-2 border-t border-[var(--border)]">
<div class="pt-2 border-t border-default">
{#if deleteConfirm}
<div class="flex items-center gap-2">
<span class="text-xs text-[var(--error)]">정말 삭제?</span>
<span class="text-xs text-error">정말 삭제?</span>
<button
onclick={deleteDoc}
disabled={deleting}
class="px-2 py-1 text-xs bg-[var(--error)] text-white rounded disabled:opacity-50"
class="px-2 py-1 text-xs bg-error text-white rounded disabled:opacity-50"
>{deleting ? '삭제 중...' : '확인'}</button>
<button
onclick={() => deleteConfirm = false}
class="px-2 py-1 text-xs text-[var(--text-dim)]"
class="px-2 py-1 text-xs text-dim"
>취소</button>
</div>
{:else}
<button
onclick={() => deleteConfirm = true}
class="flex items-center gap-1 text-xs text-[var(--text-dim)] hover:text-[var(--error)]"
class="flex items-center gap-1 text-xs text-dim hover:text-error"
>
<Trash2 size={12} /> 문서 삭제
</button>