feat(search): Phase 0.5 RRF fusion + 강한 신호 boost
기존 weighted-sum merge를 Reciprocal Rank Fusion으로 교체. 정확 키워드 매치에서 RRF가 평탄화되는 문제는 boost로 보완. 신규 모듈 app/services/search_fusion.py: - FusionStrategy ABC - LegacyWeightedSum : 기존 _merge_results 동작 (A/B 비교용) - RRFOnly : 순수 RRF, k=60 - RRFWithBoost : RRF + title/tags/법령조문/high-text-score boost (default) - normalize_display_scores: SearchResult.score를 [0..1] 랭크 기반 정규화 (프론트엔드가 score*100을 % 표시하므로 RRF 원본 점수 노출 시 표시 깨짐) search.py: - ?fusion=legacy|rrf|rrf_boost 파라미터 (default rrf_boost) - _merge_results 제거 (LegacyWeightedSum에 흡수) - pre-fusion confidence: hybrid는 raw text/vector 신호로 계산 (fused score는 fusion 전략마다 스케일이 달라 일관 비교 불가) - timing에 fusion_ms 추가 - debug notes에 fusion 전략 표시 telemetry: - compute_confidence_hybrid(text_results, vector_results) 헬퍼 - record_search_event에 confidence override 파라미터 run_eval.py: - --fusion CLI 옵션, call_search 쿼리 파라미터에 전달 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
452
frontend/src/routes/__styleguide/+page.svelte
Normal file
452
frontend/src/routes/__styleguide/+page.svelte
Normal file
@@ -0,0 +1,452 @@
|
||||
<script lang="ts">
|
||||
// 디자인 시스템 styleguide. dev only — +page.ts 에서 가드.
|
||||
// Phase A 산출물 전체 시각 검증 + Modal stack / Drawer 동작 데모.
|
||||
import {
|
||||
Search,
|
||||
Mail,
|
||||
User,
|
||||
Trash2,
|
||||
Plus,
|
||||
Save,
|
||||
ExternalLink,
|
||||
Settings,
|
||||
Inbox,
|
||||
FileText,
|
||||
Link2,
|
||||
Menu,
|
||||
} from 'lucide-svelte';
|
||||
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import Skeleton from '$lib/components/ui/Skeleton.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import TextInput from '$lib/components/ui/TextInput.svelte';
|
||||
import Textarea from '$lib/components/ui/Textarea.svelte';
|
||||
import Select from '$lib/components/ui/Select.svelte';
|
||||
import Drawer from '$lib/components/ui/Drawer.svelte';
|
||||
import Modal from '$lib/components/ui/Modal.svelte';
|
||||
import ConfirmDialog from '$lib/components/ui/ConfirmDialog.svelte';
|
||||
import Tabs from '$lib/components/ui/Tabs.svelte';
|
||||
|
||||
import { ui } from '$lib/stores/uiState.svelte';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
|
||||
// ── 폼 데모 상태 ────────────────────────────────────────
|
||||
let textValue = $state('');
|
||||
let textWithError = $state('잘못된값');
|
||||
let textareaValue = $state('');
|
||||
let selectValue = $state('');
|
||||
let tabValue = $state('overview');
|
||||
|
||||
// ── Modal 데모 상태 ─────────────────────────────────────
|
||||
function openSimpleModal() {
|
||||
ui.openModal('demo-simple');
|
||||
}
|
||||
function openNestedModal() {
|
||||
ui.openModal('demo-nested');
|
||||
}
|
||||
function openConfirm() {
|
||||
ui.openModal('demo-confirm');
|
||||
}
|
||||
function handleConfirm() {
|
||||
addToast('success', '확인 처리됨');
|
||||
}
|
||||
|
||||
// ── Drawer 데모 ────────────────────────────────────────
|
||||
function openSidebarDrawer() {
|
||||
ui.openDrawer('sidebar');
|
||||
}
|
||||
function openMetaDrawer() {
|
||||
ui.openDrawer('meta');
|
||||
}
|
||||
|
||||
// ── 색상 토큰 (시각 swatch용) ────────────────────────────
|
||||
const colorTokens = [
|
||||
{ name: 'bg', class: 'bg-bg' },
|
||||
{ name: 'surface', class: 'bg-surface' },
|
||||
{ name: 'surface-hover', class: 'bg-surface-hover' },
|
||||
{ name: 'surface-active', class: 'bg-surface-active' },
|
||||
{ name: 'sidebar', class: 'bg-sidebar' },
|
||||
{ name: 'default (border)', class: 'bg-default' },
|
||||
{ name: 'border-strong', class: 'bg-border-strong' },
|
||||
{ name: 'text', class: 'bg-text' },
|
||||
{ name: 'dim', class: 'bg-dim' },
|
||||
{ name: 'faint', class: 'bg-faint' },
|
||||
{ name: 'accent', class: 'bg-accent' },
|
||||
{ name: 'accent-hover', class: 'bg-accent-hover' },
|
||||
{ name: 'success', class: 'bg-success' },
|
||||
{ name: 'warning', class: 'bg-warning' },
|
||||
{ name: 'error', class: 'bg-error' },
|
||||
];
|
||||
|
||||
const domainTokens = [
|
||||
{ name: 'philosophy', class: 'bg-domain-philosophy' },
|
||||
{ name: 'language', class: 'bg-domain-language' },
|
||||
{ name: 'engineering', class: 'bg-domain-engineering' },
|
||||
{ name: 'safety', class: 'bg-domain-safety' },
|
||||
{ name: 'programming', class: 'bg-domain-programming' },
|
||||
{ name: 'general', class: 'bg-domain-general' },
|
||||
{ name: 'reference', class: 'bg-domain-reference' },
|
||||
];
|
||||
|
||||
const radiusTokens = [
|
||||
{ name: 'sm', class: 'rounded-sm' },
|
||||
{ name: 'md', class: 'rounded-md' },
|
||||
{ name: 'lg', class: 'rounded-lg' },
|
||||
{ name: 'card', class: 'rounded-card' },
|
||||
];
|
||||
|
||||
const selectOptions = [
|
||||
{ value: 'knowledge', label: 'Knowledge' },
|
||||
{ value: 'reference', label: 'Reference' },
|
||||
{ value: 'inbox', label: 'Inbox' },
|
||||
{ value: 'archived', label: 'Archived (disabled)', disabled: true },
|
||||
];
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: '개요' },
|
||||
{ id: 'edit', label: '편집' },
|
||||
{ id: 'history', label: '이력' },
|
||||
{ id: 'disabled', label: '비활성', disabled: true },
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Styleguide — Document Server</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="max-w-5xl mx-auto p-6 space-y-12">
|
||||
<header class="border-b border-default pb-4">
|
||||
<h1 class="text-2xl font-bold text-text">디자인 시스템 styleguide</h1>
|
||||
<p class="text-sm text-dim mt-1">
|
||||
Phase A 산출물 — 토큰, 프리미티브, layer 컴포넌트 검증. <span class="text-faint">dev only.</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- ═════ 색상 토큰 ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">색상 토큰</h2>
|
||||
<div class="grid grid-cols-3 md:grid-cols-5 gap-3">
|
||||
{#each colorTokens as t (t.name)}
|
||||
<div class="space-y-1.5">
|
||||
<div class={'h-12 rounded-md border border-default ' + t.class}></div>
|
||||
<p class="text-[10px] text-dim font-mono">{t.name}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xs font-semibold text-faint uppercase pt-3">domain</h3>
|
||||
<div class="grid grid-cols-3 md:grid-cols-7 gap-3">
|
||||
{#each domainTokens as t (t.name)}
|
||||
<div class="space-y-1.5">
|
||||
<div class={'h-12 rounded-md border border-default ' + t.class}></div>
|
||||
<p class="text-[10px] text-dim font-mono">{t.name}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Radius ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Radius</h2>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{#each radiusTokens as r (r.name)}
|
||||
<div class="space-y-1.5 text-center">
|
||||
<div class={'w-16 h-16 bg-surface border border-default ' + r.class}></div>
|
||||
<p class="text-[10px] text-dim font-mono">{r.name}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Button ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Button</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
<p class="text-xs text-faint">variant × size</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="danger">Danger</Button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="primary" size="sm">sm Primary</Button>
|
||||
<Button variant="secondary" size="sm">sm Secondary</Button>
|
||||
<Button variant="ghost" size="sm">sm Ghost</Button>
|
||||
<Button variant="danger" size="sm">sm Danger</Button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-faint pt-2">아이콘 + 상태</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="primary" icon={Plus}>새 문서</Button>
|
||||
<Button variant="secondary" icon={Save}>저장</Button>
|
||||
<Button variant="secondary" icon={ExternalLink} iconPosition="right">새 탭에서 열기</Button>
|
||||
<Button variant="primary" loading>저장 중…</Button>
|
||||
<Button variant="secondary" disabled>비활성</Button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-faint pt-2">href (자동 a 변환)</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="ghost" href="/" icon={ExternalLink}>홈으로</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ IconButton ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">IconButton</h2>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<IconButton icon={Menu} aria-label="메뉴" variant="ghost" />
|
||||
<IconButton icon={Search} aria-label="검색" variant="ghost" />
|
||||
<IconButton icon={Settings} aria-label="설정" variant="secondary" />
|
||||
<IconButton icon={Save} aria-label="저장" variant="primary" />
|
||||
<IconButton icon={Trash2} aria-label="삭제" variant="danger" />
|
||||
<IconButton icon={Save} aria-label="저장 중" variant="primary" loading />
|
||||
<IconButton icon={Trash2} aria-label="비활성" variant="ghost" disabled />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Card ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Card</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<p class="text-sm font-medium text-text">기본 padded</p>
|
||||
<p class="text-xs text-dim mt-1">bg-surface + rounded-card + border-default</p>
|
||||
</Card>
|
||||
<Card padded={false}>
|
||||
<div class="p-3">
|
||||
<p class="text-sm font-medium text-text">padded={false}</p>
|
||||
<p class="text-xs text-dim mt-1">내부에서 직접 padding 제어</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card interactive onclick={() => addToast('info', 'Card 클릭됨')}>
|
||||
<p class="text-sm font-medium text-text">interactive</p>
|
||||
<p class="text-xs text-dim mt-1">클릭 시 hover + button 시맨틱</p>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Badge ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Badge</h2>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<Badge tone="neutral">neutral</Badge>
|
||||
<Badge tone="success">success</Badge>
|
||||
<Badge tone="warning">warning</Badge>
|
||||
<Badge tone="error">error</Badge>
|
||||
<Badge tone="accent">accent</Badge>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<Badge tone="neutral" size="sm">sm neutral</Badge>
|
||||
<Badge tone="success" size="sm">완료</Badge>
|
||||
<Badge tone="warning" size="sm">대기</Badge>
|
||||
<Badge tone="error" size="sm">실패</Badge>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Skeleton ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Skeleton</h2>
|
||||
<div class="space-y-2">
|
||||
<Skeleton w="w-1/3" h="h-3" />
|
||||
<Skeleton w="w-2/3" h="h-4" />
|
||||
<Skeleton w="w-full" h="h-20" rounded="card" />
|
||||
<div class="flex gap-3 items-center">
|
||||
<Skeleton w="w-12" h="h-12" rounded="full" />
|
||||
<div class="flex-1 space-y-2">
|
||||
<Skeleton h="h-3" w="w-1/2" />
|
||||
<Skeleton h="h-3" w="w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ EmptyState ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">EmptyState</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card padded={false}>
|
||||
<EmptyState
|
||||
icon={Inbox}
|
||||
title="Inbox가 비어 있습니다"
|
||||
description="새 파일이 들어오면 자동으로 표시됩니다."
|
||||
/>
|
||||
</Card>
|
||||
<Card padded={false}>
|
||||
<EmptyState
|
||||
icon={Link2}
|
||||
title="추후 지원"
|
||||
description="벡터 유사도 기반 추천은 백엔드 API 추가 후 활성화됩니다."
|
||||
>
|
||||
<Button variant="secondary" size="sm" icon={ExternalLink}>관련 이슈 보기</Button>
|
||||
</EmptyState>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ TextInput / Textarea / Select ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">입력 프리미티브</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<TextInput
|
||||
label="제목"
|
||||
placeholder="문서 제목 입력"
|
||||
hint="여기에 도움말이 들어갑니다."
|
||||
bind:value={textValue}
|
||||
/>
|
||||
<TextInput
|
||||
label="이메일"
|
||||
type="email"
|
||||
leadingIcon={Mail}
|
||||
placeholder="you@example.com"
|
||||
autocomplete="email"
|
||||
/>
|
||||
<TextInput
|
||||
label="검색어"
|
||||
placeholder="키워드"
|
||||
leadingIcon={Search}
|
||||
trailingIcon={ExternalLink}
|
||||
/>
|
||||
<TextInput label="에러 상태" bind:value={textWithError} error="값이 유효하지 않습니다." />
|
||||
|
||||
<TextInput label="비활성" placeholder="readonly + disabled" disabled value="고정값" />
|
||||
<TextInput label="필수" required placeholder="반드시 입력" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Textarea
|
||||
label="메모"
|
||||
placeholder="자유 형식 메모"
|
||||
rows={3}
|
||||
bind:value={textareaValue}
|
||||
hint="autoGrow={'{false}'} 기본"
|
||||
/>
|
||||
<Textarea label="autoGrow" placeholder="입력하면 늘어남" autoGrow maxRows={6} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Select
|
||||
label="도메인"
|
||||
options={selectOptions}
|
||||
placeholder="선택…"
|
||||
bind:value={selectValue}
|
||||
/>
|
||||
<Select label="에러" options={selectOptions} error="필수 항목" />
|
||||
<Select label="비활성" options={selectOptions} disabled value="knowledge" />
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-faint">
|
||||
현재 값:
|
||||
<code class="text-text">{JSON.stringify({ textValue, textareaValue, selectValue })}</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Tabs ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Tabs</h2>
|
||||
<Card padded={false}>
|
||||
<Tabs {tabs} bind:value={tabValue}>
|
||||
{#snippet children(activeId)}
|
||||
<div class="p-5">
|
||||
{#if activeId === 'overview'}
|
||||
<p class="text-sm text-text">📋 개요 패널 — 좌우 화살표 키로 탭 이동.</p>
|
||||
{:else if activeId === 'edit'}
|
||||
<p class="text-sm text-text">✏️ 편집 패널.</p>
|
||||
{:else if activeId === 'history'}
|
||||
<p class="text-sm text-text">🕓 이력 패널.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Drawer ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Drawer (단일 slot)</h2>
|
||||
<p class="text-xs text-faint">
|
||||
둘 다 열어 보세요. sidebar 열려 있을 때 meta를 누르면 sidebar가 자동으로 닫힙니다.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="secondary" icon={Menu} onclick={openSidebarDrawer}>Sidebar drawer</Button>
|
||||
<Button variant="secondary" icon={FileText} onclick={openMetaDrawer}>Meta drawer (오른쪽)</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Modal ═════ -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Modal (stack 지원)</h2>
|
||||
<p class="text-xs text-faint">
|
||||
Simple modal 안에서 "또 열기"를 누르면 nested modal이 위에 쌓입니다. Esc는 가장 위 modal부터 닫습니다.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="primary" onclick={openSimpleModal}>Simple Modal 열기</Button>
|
||||
<Button variant="danger" icon={Trash2} onclick={openConfirm}>ConfirmDialog 열기</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═════ Toast ═════ -->
|
||||
<section class="space-y-3 pb-12">
|
||||
<h2 class="text-sm font-semibold text-dim uppercase tracking-wider">Toast</h2>
|
||||
<p class="text-xs text-faint">현재 토스트는 기존 +layout.svelte 렌더러 사용 — A-8에서 토큰 swap 예정.</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="ghost" size="sm" onclick={() => addToast('success', '저장 완료')}>success</Button>
|
||||
<Button variant="ghost" size="sm" onclick={() => addToast('error', '저장 실패')}>error</Button>
|
||||
<Button variant="ghost" size="sm" onclick={() => addToast('warning', '경고')}>warning</Button>
|
||||
<Button variant="ghost" size="sm" onclick={() => addToast('info', '정보')}>info</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═════ 데모용 Drawer/Modal 인스턴스 ═════ -->
|
||||
|
||||
<Drawer id="sidebar" side="left" width="sidebar" aria-label="Sidebar drawer 데모">
|
||||
<div class="p-5 space-y-3">
|
||||
<h3 class="text-sm font-semibold text-text">Sidebar drawer</h3>
|
||||
<p class="text-xs text-dim">id="sidebar". 단일 slot이므로 meta drawer가 열리면 자동으로 닫힙니다.</p>
|
||||
<Button variant="ghost" size="sm" onclick={() => ui.closeDrawer()}>닫기</Button>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<Drawer id="meta" side="right" width="rail" aria-label="Meta drawer 데모">
|
||||
<div class="p-5 space-y-3">
|
||||
<h3 class="text-sm font-semibold text-text">Meta drawer</h3>
|
||||
<p class="text-xs text-dim">id="meta". 모바일/태블릿에서 메타 패널 폴백으로 쓰일 layer.</p>
|
||||
<Button variant="ghost" size="sm" onclick={() => ui.closeDrawer()}>닫기</Button>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<Modal id="demo-simple" title="Simple modal">
|
||||
<div class="space-y-3">
|
||||
<p>이 모달은 stack에 1개만 있습니다. 안에서 "또 열기"를 누르면 nested modal이 쌓입니다.</p>
|
||||
<p class="text-xs text-faint">stack 인덱스로 z-index가 자동 계산되어 backdrop이 정확히 겹칩니다.</p>
|
||||
</div>
|
||||
{#snippet footer()}
|
||||
<Button variant="ghost" size="sm" onclick={() => ui.closeModal('demo-simple')}>닫기</Button>
|
||||
<Button variant="primary" size="sm" onclick={openNestedModal}>또 열기 (nested)</Button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<Modal id="demo-nested" title="Nested modal" size="sm">
|
||||
<p>nested modal입니다. Esc를 누르면 이것만 닫히고, 아래 simple modal은 그대로 남습니다.</p>
|
||||
{#snippet footer()}
|
||||
<Button variant="primary" size="sm" onclick={() => ui.closeTopModal()}>이것만 닫기</Button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
id="demo-confirm"
|
||||
title="문서를 삭제할까요?"
|
||||
message="이 작업은 되돌릴 수 없습니다. 선택한 문서가 영구적으로 제거됩니다."
|
||||
confirmLabel="삭제"
|
||||
tone="danger"
|
||||
onconfirm={handleConfirm}
|
||||
/>
|
||||
9
frontend/src/routes/__styleguide/+page.ts
Normal file
9
frontend/src/routes/__styleguide/+page.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
// dev 모드에서만 접근 가능. 프로덕션 빌드에서는 / 로 리다이렉트.
|
||||
export const load = () => {
|
||||
if (!dev) {
|
||||
throw redirect(307, '/');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user