refactor: stores 분리 — toast / uiState 단일 책임화
UX/UI 개편 Phase A-2. lib/stores/ui.ts에 섞여 있던 toast 시스템과 UI layer 상태(미사용 dead export 포함)를 의미 단위로 분리한다. 한 파일이 비대해지는 시나리오를 처음부터 차단(plan 8대 원칙 #7). - lib/stores/toast.ts 신규 — toasts/addToast/removeToast (Toast interface export) - lib/stores/uiState.svelte.ts 신규 — drawer 단일 slot + modal stack 클래스 (5대 원칙 #2) · openDrawer/closeDrawer/isDrawerOpen · openModal/closeTopModal/isModalOpen/modalIndex/topModal · handleEscape (modal stack 우선 → drawer) - lib/stores/ui.ts 삭제 — sidebarOpen/selectedDocId는 어디서도 import되지 않은 dead export였음 - 11개 파일 import 경로 갱신: \$lib/stores/ui → \$lib/stores/toast uiState는 아직 어디서도 사용 안 함 — Phase B에서 sidebar/meta drawer가 전환될 때 ui.openDrawer('sidebar') 형태로 채택. 동작 변경 0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { api, getAccessToken } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { ExternalLink, Save, RefreshCw } from 'lucide-svelte';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { X, ExternalLink, Plus, Save, Trash2 } from 'lucide-svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import FormatIcon from './FormatIcon.svelte';
|
||||
import TagPill from './TagPill.svelte';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import { Upload } from 'lucide-svelte';
|
||||
|
||||
let { onupload = () => {} } = $props();
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const sidebarOpen = writable(true);
|
||||
export const selectedDocId = writable<number | null>(null);
|
||||
|
||||
// Toast 시스템
|
||||
interface Toast {
|
||||
// Toast 시스템 — UI state(drawer/modal)와 분리된 알림 전용 store
|
||||
export interface Toast {
|
||||
id: number;
|
||||
type: 'success' | 'error' | 'warning' | 'info';
|
||||
message: string;
|
||||
@@ -15,7 +12,7 @@ export const toasts = writable<Toast[]>([]);
|
||||
|
||||
export function addToast(type: Toast['type'], message: string, duration = 5000) {
|
||||
const id = ++toastId;
|
||||
toasts.update(t => [...t, { id, type, message }]);
|
||||
toasts.update((t) => [...t, { id, type, message }]);
|
||||
if (duration > 0) {
|
||||
setTimeout(() => removeToast(id), duration);
|
||||
}
|
||||
@@ -23,5 +20,5 @@ export function addToast(type: Toast['type'], message: string, duration = 5000)
|
||||
}
|
||||
|
||||
export function removeToast(id: number) {
|
||||
toasts.update(t => t.filter(toast => toast.id !== id));
|
||||
toasts.update((t) => t.filter((toast) => toast.id !== id));
|
||||
}
|
||||
57
frontend/src/lib/stores/uiState.svelte.ts
Normal file
57
frontend/src/lib/stores/uiState.svelte.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// 중앙 UI layer 상태 — drawer는 단일 slot, modal은 stack.
|
||||
// drawer/modal/toast 동시 오픈 시 focus/scroll/Esc 충돌을 차단하기 위한 single source.
|
||||
// (toast는 별도 store. drawer가 persistent inline panel(예: xl+ meta rail)일 때는
|
||||
// 여기 시스템 밖이다 — 그저 레이아웃의 일부.)
|
||||
|
||||
type Drawer = { id: 'sidebar' | 'meta' } | null;
|
||||
type Modal = { id: string };
|
||||
|
||||
class UIState {
|
||||
drawer = $state<Drawer>(null);
|
||||
modalStack = $state<Modal[]>([]);
|
||||
|
||||
// ── Drawer (단일 slot) ──────────────────────────────
|
||||
openDrawer(id: 'sidebar' | 'meta') {
|
||||
// 새 drawer 열면 이전 drawer는 자동으로 사라진다 (단일 slot)
|
||||
this.drawer = { id };
|
||||
}
|
||||
closeDrawer() {
|
||||
this.drawer = null;
|
||||
}
|
||||
isDrawerOpen(id: 'sidebar' | 'meta') {
|
||||
return this.drawer?.id === id;
|
||||
}
|
||||
|
||||
// ── Modal (stack — confirm 위에 nested 가능) ─────────
|
||||
openModal(id: string) {
|
||||
this.modalStack = [...this.modalStack, { id }];
|
||||
}
|
||||
closeTopModal() {
|
||||
this.modalStack = this.modalStack.slice(0, -1);
|
||||
}
|
||||
closeModal(id: string) {
|
||||
this.modalStack = this.modalStack.filter((m) => m.id !== id);
|
||||
}
|
||||
isModalOpen(id: string) {
|
||||
return this.modalStack.some((m) => m.id === id);
|
||||
}
|
||||
// 특정 modal의 stack 인덱스 (z-index 계산용; 없으면 -1)
|
||||
modalIndex(id: string) {
|
||||
return this.modalStack.findIndex((m) => m.id === id);
|
||||
}
|
||||
get topModal() {
|
||||
return this.modalStack[this.modalStack.length - 1] ?? null;
|
||||
}
|
||||
|
||||
// ── 글로벌 Esc 핸들러 ────────────────────────────────
|
||||
// 우선순위: 가장 위 modal → drawer
|
||||
handleEscape() {
|
||||
if (this.modalStack.length > 0) {
|
||||
this.closeTopModal();
|
||||
} else if (this.drawer) {
|
||||
this.closeDrawer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ui = new UIState();
|
||||
@@ -4,7 +4,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { isAuthenticated, user, tryRefresh, logout } from '$lib/stores/auth';
|
||||
import { toasts, removeToast } from '$lib/stores/ui';
|
||||
import { toasts, removeToast } from '$lib/stores/toast';
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
import '../app.css';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
|
||||
let dashboard = null;
|
||||
let loading = true;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import { Info } from 'lucide-svelte';
|
||||
import { List, LayoutGrid } from 'lucide-svelte';
|
||||
import DocumentCard from '$lib/components/DocumentCard.svelte';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { api, getAccessToken } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import TagPill from '$lib/components/TagPill.svelte';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
|
||||
let documents = [];
|
||||
let loading = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { login } from '$lib/stores/auth';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
import { user } from '$lib/stores/auth';
|
||||
|
||||
let currentPassword = '';
|
||||
|
||||
Reference in New Issue
Block a user