색상 시스템을 의미 토큰으로 swap. Phase A 디자인 시스템 정착의 첫
mechanical refactor batch (8 파일 중 2 파일).
TagPill: 4가지 prefix별 색상을 의미 토큰화
- @상태/ amber → warning
- #주제/ blue → accent
- $유형/ green → success
- !우선순위/ red → error
- fallback bg-[var(--border)] → bg-default,
text-[var(--text-dim)] → text-dim
UploadDropzone: 드래그 오버레이 + 업로드 진행 영역
- bg-[var(--accent)]/10 → bg-accent/10
- bg-[var(--surface)] → bg-surface
- border-[var(--border)] → border-default
- text-[var(--text-dim)] → text-dim
- 상태별 텍스트: text-success / text-error / text-accent / text-dim
검증:
- npm run lint:tokens : 421 → 407 (-14, B1 파일 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 1
참고: 본 plan은 161ff18(search Phase 0.5 commit)에 styleguide 2개 파일이
의도와 다르게 묶여 main에 들어왔음. 기능 영향 0 — Option A 결정으로
commit history 미수정.
130 lines
3.6 KiB
Svelte
130 lines
3.6 KiB
Svelte
<script>
|
|
import { onMount } from 'svelte';
|
|
import { api } from '$lib/api';
|
|
import { addToast } from '$lib/stores/toast';
|
|
import { Upload } from 'lucide-svelte';
|
|
|
|
let { onupload = () => {} } = $props();
|
|
|
|
let dragging = $state(false);
|
|
let uploading = $state(false);
|
|
let uploadFiles = $state([]);
|
|
let dragCounter = 0;
|
|
|
|
onMount(() => {
|
|
function onDragEnter(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dragCounter++;
|
|
dragging = true;
|
|
}
|
|
|
|
function onDragOver(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
function onDragLeave(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dragCounter--;
|
|
if (dragCounter <= 0) {
|
|
dragging = false;
|
|
dragCounter = 0;
|
|
}
|
|
}
|
|
|
|
function onDrop(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dragging = false;
|
|
dragCounter = 0;
|
|
handleFiles(e.dataTransfer?.files);
|
|
}
|
|
|
|
window.addEventListener('dragenter', onDragEnter);
|
|
window.addEventListener('dragover', onDragOver);
|
|
window.addEventListener('dragleave', onDragLeave);
|
|
window.addEventListener('drop', onDrop);
|
|
|
|
return () => {
|
|
window.removeEventListener('dragenter', onDragEnter);
|
|
window.removeEventListener('dragover', onDragOver);
|
|
window.removeEventListener('dragleave', onDragLeave);
|
|
window.removeEventListener('drop', onDrop);
|
|
};
|
|
});
|
|
|
|
async function handleFiles(fileList) {
|
|
const files = Array.from(fileList || []);
|
|
if (files.length === 0) return;
|
|
|
|
uploading = true;
|
|
uploadFiles = files.map(f => ({ name: f.name, status: 'pending' }));
|
|
|
|
let success = 0;
|
|
let failed = 0;
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
uploadFiles[i].status = 'uploading';
|
|
uploadFiles = [...uploadFiles];
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', files[i]);
|
|
await api('/documents/', { method: 'POST', body: formData });
|
|
uploadFiles[i].status = 'done';
|
|
success++;
|
|
} catch (err) {
|
|
uploadFiles[i].status = 'failed';
|
|
failed++;
|
|
}
|
|
uploadFiles = [...uploadFiles];
|
|
}
|
|
|
|
if (success > 0) {
|
|
addToast('success', `${success}건 업로드 완료${failed > 0 ? `, ${failed}건 실패` : ''}`);
|
|
onupload();
|
|
} else {
|
|
addToast('error', `업로드 실패 (${failed}건)`);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
uploading = false;
|
|
uploadFiles = [];
|
|
}, 3000);
|
|
}
|
|
</script>
|
|
|
|
<!-- 전체 페이지 드래그 오버레이 -->
|
|
{#if dragging}
|
|
<div class="fixed inset-0 z-50 bg-accent/10 border-2 border-dashed border-accent flex items-center justify-center">
|
|
<div class="bg-surface rounded-xl px-8 py-6 shadow-xl text-center">
|
|
<Upload size={32} class="mx-auto mb-2 text-accent" />
|
|
<p class="text-sm font-medium text-accent">여기에 파일을 놓으세요</p>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- 업로드 진행 상태 -->
|
|
{#if uploading && uploadFiles.length > 0}
|
|
<div class="mb-3 bg-surface border border-default rounded-lg p-3">
|
|
<p class="text-xs text-dim mb-2">업로드 중...</p>
|
|
<div class="space-y-1 max-h-32 overflow-y-auto">
|
|
{#each uploadFiles as f}
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="truncate">{f.name}</span>
|
|
<span class={
|
|
f.status === 'done' ? 'text-success' :
|
|
f.status === 'failed' ? 'text-error' :
|
|
f.status === 'uploading' ? 'text-accent' :
|
|
'text-dim'
|
|
}>
|
|
{f.status === 'done' ? '✓' : f.status === 'failed' ? '✗' : f.status === 'uploading' ? '↑' : '…'}
|
|
</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|