// app.js — 포트폴리오 SPA 진입점 // 라우트 표를 만들고 #app 에 라우터를 붙인다. // 이 파일 하나만 index.html 에서 type="module" 로 읽어 들인다. import { createRouter, navigate } from './router.js'; import { toast } from './ui.js'; import { ApiError } from './api.js'; import { view as homeView } from './views/home.js'; import { view as postView } from './views/post.js'; import { view as aboutView } from './views/about.js'; import { view as adminView, editorView, categoriesView } from './views/admin.js'; /** 라우트 표. 배열 순서대로 매칭하므로 하위 경로를 먼저 둔다. */ const ROUTES = [ { pattern: '#/', view: homeView, title: '김낙준 · 작업' }, { pattern: '#/post/:postNo', view: postView, title: '김낙준 · 작업' }, { pattern: '#/about', view: aboutView, title: '김낙준 · 소개' }, // '#/admin' 보다 먼저 와야 한다. 뒤에 두면 영원히 매칭되지 않는다. { pattern: '#/admin/new', view: editorView, title: '김낙준 · 새 글' }, { pattern: '#/admin/edit/:postNo', view: editorView, title: '김낙준 · 글 수정' }, { pattern: '#/admin/categories', view: categoriesView, title: '김낙준 · 카테고리' }, { pattern: '#/admin', view: adminView, title: '김낙준 · 관리자' }, // 알 수 없는 주소는 홈으로 흡수한다. { pattern: '*', view: homeView, title: '김낙준 · 작업' } ]; /** 현재 경로에 해당하는 상단 내비 링크에 .is-active 를 붙인다. */ function syncNav(path) { const links = document.querySelectorAll('.nav__link'); for (const link of links) { const href = link.getAttribute('href') || ''; const target = href.charAt(0) === '#' ? href.slice(1) : href; const normalized = target === '/' || target === '' ? '/' : target; // 홈은 '/' 와 '/post/*' 를 모두 자기 것으로 본다. const active = normalized === '/' ? (path === '/' || path.indexOf('/post/') === 0) : path === normalized || path.indexOf(normalized + '/') === 0; link.classList.toggle('is-active', active); if (active) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current'); } } /** ApiError 만 사용자에게 알린다. 그 밖의 오류는 콘솔에 맡긴다. */ function reportUnhandled(reason) { if (reason instanceof ApiError) { toast(reason.message || '요청을 처리하지 못했습니다.', 'error'); } } function boot() { const outlet = document.getElementById('app'); if (!outlet) return; window.addEventListener('route:change', (ev) => { const detail = (ev && ev.detail) || {}; syncNav(String(detail.path || '/')); }); const adminBtn = document.getElementById('nav-admin'); if (adminBtn) { adminBtn.addEventListener('click', () => navigate('#/admin')); } window.addEventListener('unhandledrejection', (ev) => { reportUnhandled(ev && ev.reason); }); window.addEventListener('error', (ev) => { reportUnhandled(ev && ev.error); }); createRouter(ROUTES, outlet); syncNav(String((location.hash || '#/').replace(/^#/, '').split('?')[0] || '/')); } console.log('%c김낙준 포트폴리오', 'color:#6D48F0;font-weight:700', '· ready'); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot, { once: true }); } else { boot(); }