feat: improve combination search and offline build

This commit is contained in:
2026-08-18 20:59:20 +09:00
parent ee3498d146
commit d0f7654998
20 changed files with 552 additions and 99 deletions
+127
View File
@@ -0,0 +1,127 @@
<script lang="ts">
import { onMount } from 'svelte';
import HomePage from '../src/routes/+page.svelte';
import CardsPage from '../src/routes/cards/+page.svelte';
import AnalysisPage from '../src/routes/analysis/+page.svelte';
import CombinationPage from '../src/routes/combination/+page.svelte';
import DataPage from '../src/routes/data/+page.svelte';
import DataCardsPage from '../src/routes/data/cards/+page.svelte';
import DataBoardsPage from '../src/routes/data/boards/+page.svelte';
import { setOfflinePage } from './page-store';
const routes = new Map([
['/', HomePage],
['/cards', CardsPage],
['/analysis', AnalysisPage],
['/combination', CombinationPage],
['/data', DataPage],
['/data/cards', DataCardsPage],
['/data/boards', DataBoardsPage]
]);
function pathFromHash() {
const path = window.location.hash.replace(/^#/, '') || '/';
return routes.has(path) ? path : '/';
}
let currentPath = pathFromHash();
let CurrentPage = routes.get(currentPath) ?? HomePage;
setOfflinePage(currentPath);
function syncRoute() {
currentPath = pathFromHash();
CurrentPage = routes.get(currentPath) ?? HomePage;
setOfflinePage(currentPath);
window.scrollTo({ top: 0 });
}
onMount(() => {
const rewriteInternalLinks = () => {
for (const anchor of document.querySelectorAll<HTMLAnchorElement>('a[href^="/"]')) {
anchor.href = `#${anchor.getAttribute('href')}`;
}
};
const handleClick = (event: MouseEvent) => {
const target = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[href]') : null;
const href = target?.getAttribute('href');
if (!href?.startsWith('/') || target?.target === '_blank') return;
event.preventDefault();
window.location.hash = `#${href}`;
};
rewriteInternalLinks();
const linkObserver = new MutationObserver(rewriteInternalLinks);
linkObserver.observe(document.body, { childList: true, subtree: true });
window.addEventListener('hashchange', syncRoute);
document.addEventListener('click', handleClick);
return () => {
window.removeEventListener('hashchange', syncRoute);
document.removeEventListener('click', handleClick);
linkObserver.disconnect();
};
});
</script>
<header class="site-nav">
<div class="nav-inner">
<a class="brand" href="#/" aria-label="홀로도리 계산기 홈">홀로도리 계산기</a>
<nav aria-label="주요 메뉴">
<a href="#/cards" aria-current={currentPath.startsWith('/cards') ? 'page' : undefined}>카드 목록</a>
<a href="#/analysis" aria-current={currentPath.startsWith('/analysis') ? 'page' : undefined}>배치 분석</a>
<a href="#/combination" aria-current={currentPath.startsWith('/combination') ? 'page' : undefined}>조합 탐색</a>
<a href="#/data" aria-current={currentPath.startsWith('/data') ? 'page' : undefined}>사용자 데이터</a>
</nav>
</div>
</header>
<svelte:component this={CurrentPage} />
<style>
:global(*) { box-sizing: border-box; }
:global(body) { margin: 0; background: #101319; color: #e8ebf1; font-family: Pretendard, "Noto Sans KR", sans-serif; }
.site-nav {
position: sticky;
z-index: 20;
top: 0;
border-bottom: 1px solid rgb(255 255 255 / 8%);
background: rgb(16 19 25 / 88%);
backdrop-filter: blur(14px);
}
.nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
width: min(var(--page-max-width), calc(100% - var(--page-gutter)));
min-height: 62px;
margin: 0 auto;
}
.brand {
color: #f3f5f8;
font-size: 16px;
font-weight: 850;
letter-spacing: -.03em;
text-decoration: none;
}
nav { display: flex; align-items: center; gap: 4px; }
nav a {
padding: 8px 12px;
border-radius: 9px;
color: #929cad;
font-size: 13px;
font-weight: 750;
text-decoration: none;
}
nav a:hover { color: #e2e6ed; background: rgb(255 255 255 / 5%); }
nav a[aria-current='page'] { color: #f1a1c3; background: rgb(239 139 182 / 12%); }
a:focus-visible { outline: 3px solid rgb(239 139 182 / 28%); outline-offset: 2px; }
@media (max-width: 560px) {
:global(:root) { --page-gutter: 24px; }
.nav-inner { width: calc(100% - 24px); min-height: 54px; }
.brand { font-size: 15px; }
nav a { padding: 7px 9px; font-size: 14px; }
}
</style>