목록/사이드바 영역의 var() 토큰을 의미 토큰으로 swap. Phase A 디자인 시스템 정착의 두 번째 mechanical refactor batch (8 파일 중 5/8 누적). Sidebar: - bg-[var(--sidebar-bg)] → bg-sidebar (이름 변경) - border-[var(--border)] → border-default - text-[var(--text)] → text-text - text-[var(--text-dim)] → text-dim - bg-[var(--accent)]/15 → bg-accent/15 - hover:bg-[var(--surface)] → hover:bg-surface - domain 색상 inline style (DOMAIN_COLORS)은 그대로 유지 DocumentCard: - bg/border/text/hover 토큰 일괄 swap - DOMAIN_COLORS의 var(--domain-*) 유지 (plan B2 비고) - blue-400/blue-900/30 (news icon, data_origin work) 그대로 (lint:tokens 미검출 + plan 명시 없음) DocumentTable: - 헤더 + 행 + selected 상태 + 컬럼 텍스트 일괄 swap - border-l-[var(--accent)] → border-l-accent - border-default/30 opacity suffix (행 구분선) v4 시각 검증 필요 검증: - npm run lint:tokens : 407 → 360 (-47, B2 파일 0 hit) - npm run build : ✅ - npx svelte-check : ✅ 0 errors - ⚠ 3-risk grep : hover/border-border/var() 잔여 0건 플랜: ~/.claude/plans/compressed-churning-dragon.md §A.4 Batch 2
143 lines
4.5 KiB
Svelte
143 lines
4.5 KiB
Svelte
<script>
|
|
import { goto } from '$app/navigation';
|
|
import FormatIcon from './FormatIcon.svelte';
|
|
|
|
let { items = [], selectedId = null, onselect = null } = $props();
|
|
let sortKey = $state('created_at');
|
|
let sortOrder = $state('desc');
|
|
|
|
// localStorage에서 정렬 상태 복원
|
|
if (typeof localStorage !== 'undefined') {
|
|
const saved = localStorage.getItem('tableSort');
|
|
if (saved) {
|
|
try {
|
|
const { key, order } = JSON.parse(saved);
|
|
sortKey = key;
|
|
sortOrder = order;
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
function toggleSort(key) {
|
|
if (sortKey === key) {
|
|
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortKey = key;
|
|
sortOrder = 'asc';
|
|
}
|
|
if (typeof localStorage !== 'undefined') {
|
|
localStorage.setItem('tableSort', JSON.stringify({ key: sortKey, order: sortOrder }));
|
|
}
|
|
}
|
|
|
|
// stable sort
|
|
let sortedItems = $derived(() => {
|
|
const arr = [...items];
|
|
arr.sort((a, b) => {
|
|
let va = a[sortKey] ?? '';
|
|
let vb = b[sortKey] ?? '';
|
|
if (typeof va === 'string') va = va.toLowerCase();
|
|
if (typeof vb === 'string') vb = vb.toLowerCase();
|
|
if (va === vb) return a.id - b.id;
|
|
if (sortOrder === 'asc') return va > vb ? 1 : -1;
|
|
return va < vb ? 1 : -1;
|
|
});
|
|
return arr;
|
|
});
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes) return '-';
|
|
if (bytes < 1024) return `${bytes}B`;
|
|
if (bytes < 1048576) return `${(bytes / 1024).toFixed(0)}KB`;
|
|
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
}
|
|
|
|
function handleClick(doc) {
|
|
if (typeof window !== 'undefined' && window.innerWidth < 1024) {
|
|
goto(`/documents/${doc.id}`);
|
|
return;
|
|
}
|
|
if (onselect) onselect(doc);
|
|
else goto(`/documents/${doc.id}`);
|
|
}
|
|
|
|
const DOMAIN_COLORS = {
|
|
'Philosophy': 'var(--domain-philosophy)',
|
|
'Language': 'var(--domain-language)',
|
|
'Engineering': 'var(--domain-engineering)',
|
|
'Industrial_Safety': 'var(--domain-safety)',
|
|
'Programming': 'var(--domain-programming)',
|
|
'General': 'var(--domain-general)',
|
|
'Reference': 'var(--domain-reference)',
|
|
};
|
|
|
|
function getDomainColor(domain) {
|
|
if (!domain) return 'var(--border)';
|
|
const top = domain.split('/')[0];
|
|
return DOMAIN_COLORS[top] || 'var(--border)';
|
|
}
|
|
|
|
const columns = [
|
|
{ key: 'title', label: '이름', flex: 'flex-1' },
|
|
{ key: 'ai_domain', label: '분류', width: 'w-48' },
|
|
{ key: 'document_type', label: '타입', width: 'w-24' },
|
|
{ key: 'file_size', label: '크기', width: 'w-20' },
|
|
{ key: 'created_at', label: '등록일', width: 'w-20' },
|
|
];
|
|
</script>
|
|
|
|
<div class="w-full">
|
|
<!-- 헤더 -->
|
|
<div class="flex items-center gap-1 px-2 py-1.5 border-b border-default text-[10px] text-dim uppercase tracking-wider">
|
|
{#each columns as col}
|
|
<button
|
|
onclick={() => toggleSort(col.key)}
|
|
class="flex items-center gap-1 {col.flex || col.width || ''} px-1 hover:text-text transition-colors text-left"
|
|
>
|
|
{col.label}
|
|
{#if sortKey === col.key}
|
|
<span class="text-accent">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- 행 -->
|
|
{#each sortedItems() as doc}
|
|
<button
|
|
onclick={() => handleClick(doc)}
|
|
class="flex items-center gap-1 px-2 py-1.5 w-full text-left border-b border-default/30 hover:bg-surface transition-colors group
|
|
{selectedId === doc.id ? 'bg-accent/5 border-l-2 border-l-accent' : ''}"
|
|
>
|
|
<!-- 이름 -->
|
|
<div class="flex-1 flex items-center gap-2 min-w-0">
|
|
<span class="w-1 h-4 rounded-full shrink-0" style="background: {getDomainColor(doc.ai_domain)}"></span>
|
|
<FormatIcon format={doc.file_format} size={14} />
|
|
<span class="text-xs truncate group-hover:text-accent">{doc.title || '제목 없음'}</span>
|
|
</div>
|
|
<!-- 분류 -->
|
|
<div class="w-48 text-[10px] text-dim truncate">
|
|
{doc.ai_domain?.replace('Industrial_Safety/', 'IS/') || '-'}
|
|
</div>
|
|
<!-- 타입 -->
|
|
<div class="w-24 text-[10px] text-dim">
|
|
{doc.document_type || doc.file_format?.toUpperCase() || '-'}
|
|
</div>
|
|
<!-- 크기 -->
|
|
<div class="w-20 text-[10px] text-dim text-right">
|
|
{formatSize(doc.file_size)}
|
|
</div>
|
|
<!-- 등록일 -->
|
|
<div class="w-20 text-[10px] text-dim text-right">
|
|
{formatDate(doc.created_at)}
|
|
</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|