88 lines
3.4 KiB
JavaScript
88 lines
3.4 KiB
JavaScript
// 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();
|
|
}
|