/* ============================================================================= * router.ts * 해시 기반 SPA 라우터 + 인증 가드 * * - 각 라우트는 (root) => void | Promise 형태의 렌더 함수를 등록. * - 페이지 렌더 함수는 지연 로딩(dynamic import)으로 초기 번들 최소화. * - PROTECTED_ROUTES는 토큰 없으면 A06_Login으로 리다이렉트. * ========================================================================== */ import { ROUTES, DEFAULT_ROUTE, PROTECTED_ROUTES, AUTH_TOKEN_KEY, type RoutePath, } from "@config/config_frontend"; /** 페이지 렌더 함수 시그니처. root(마운트 대상)를 받아 내용을 채운다. */ export type PageRenderer = (root: HTMLElement) => void | Promise; /** 라우트별 지연 로더 등록 테이블. * 페이지 구현이 완료되는 대로 아래에 한 줄씩 추가한다. */ const routeTable: Partial Promise>> = { [ROUTES.A01_HOME]: async () => (await import("../A01_Home/A01_Home_UI_Page")).renderA01Home, [ROUTES.A02_PROG_DETAIL]: async () => (await import("../A02_ProgDetail/A02_ProgDetail_UI_Page")).renderA02ProgDetail, [ROUTES.A03_COMP_DETAIL]: async () => (await import("../A03_CompDetail/A03_CompDetail_UI_Page")).renderA03CompDetail, [ROUTES.A04_NEWS_HISTORY]: async () => (await import("../A04_NewsHistory/A04_NewsHistory_UI_Page")).renderA04NewsHistory, [ROUTES.A05_EDU_DETAIL]: async () => (await import("../A05_EduDetail/A05_EduDetail_UI_Page")).renderA05EduDetail, [ROUTES.A06_LOGIN]: async () => (await import("../A06_Login/A06_Login_UI_Page")).renderA06Login, [ROUTES.A07_REGISTER]: async () => (await import("../A07_Register/A07_Register_UI_Page")).renderA07Register, [ROUTES.A08_SUPPORT]: async () => (await import("../A08_Support/A08_Support_UI_Page")).renderA08Support, // 로그인 후 (B 그룹) [ROUTES.B01_ACCOUNT]: async () => (await import("../B01_AccountDetail/B01_AccountDetail_UI_Page")).renderB01AccountDetail, [ROUTES.B02_PROJ_REGISTER]: async () => (await import("../B02_ProjRegister/B02_ProjRegister_UI_Page")).renderB02ProjRegister, [ROUTES.B03_FILE_INPUT]: async () => (await import("../B03_FileInput/B03_FileInput_UI_Page")).renderB03FileInput, [ROUTES.B04_WF1_SURFACE]: async () => (await import("../B04_wf1_Surface/B04_wf1_Surface_UI_Page")).renderB04Surface, [ROUTES.B05_WF2_ROUTE]: async () => (await import("../B05_wf2_Route/B05_wf2_Route_UI_Page")).renderB05Route, [ROUTES.B06_WF3_PROFILE_CROSS]: async () => (await import("../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Page")).renderB06ProfileCross, [ROUTES.B07_WF4_DESIGN_DETAIL]: async () => (await import("../B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page")).renderB07DesignDetail, [ROUTES.B08_WF5_QUANTITY]: async () => (await import("../B08_wf5_Quantity/B08_wf5_Quantity_UI_Page")).renderB08Quantity, [ROUTES.B09_WF6_ESTIMATION]: async () => (await import("../B09_wf6_Estimation/B09_wf6_Estimation_UI_Page")).renderB09Estimation, [ROUTES.B10_PAYMENT]: async () => (await import("../B10_Payment/B10_Payment_UI_Page")).renderB10Payment, [ROUTES.B11_STATUS]: async () => (await import("../B11_Status/B11_Status_UI_Page")).renderB11Status, }; /** 로그인 여부 (토큰 존재 확인) */ export function isAuthenticated(): boolean { return localStorage.getItem(AUTH_TOKEN_KEY) !== null; } /** 현재 해시에서 라우트 경로 추출 (#/a01-home → a01-home) */ export function currentRoute(): RoutePath { const hash = window.location.hash.replace(/^#\/?/, "").trim(); return (hash || DEFAULT_ROUTE) as RoutePath; } /** 지정 라우트로 이동 (해시 변경 → hashchange 이벤트로 렌더 트리거) */ export function navigateTo(route: RoutePath): void { const target = `#/${route}`; if (window.location.hash === target) { // 동일 해시면 hashchange가 안 뜨므로 수동 트리거 window.dispatchEvent(new HashChangeEvent("hashchange")); } else { window.location.hash = target; } } /** "준비 중" 임시 플레이스홀더 (아직 구현 안 된 페이지용) */ function renderPlaceholder(root: HTMLElement, route: RoutePath): void { root.innerHTML = ""; const box = document.createElement("div"); box.className = "app-placeholder"; box.textContent = `준비 중인 페이지입니다: ${route}`; root.append(box); } /** * 현재 라우트를 outlet에 렌더. 인증 가드 포함. * (hashchange 구독은 호출측에서 1회만 등록하도록 분리.) */ export async function renderCurrentRoute(outlet: HTMLElement): Promise { const route = currentRoute(); if (PROTECTED_ROUTES.includes(route) && !isAuthenticated()) { navigateTo(ROUTES.A06_LOGIN); return; } const loader = routeTable[route]; if (!loader) { renderPlaceholder(outlet, route); return; } try { const renderer = await loader(); outlet.innerHTML = ""; await renderer(outlet); window.scrollTo(0, 0); } catch (err) { console.error(`[router] 페이지 로드 실패: ${route}`, err); renderPlaceholder(outlet, route); } } /** 최초 진입 시 해시가 없으면 기본 라우트로 정규화 */ export function ensureInitialHash(): void { if (!window.location.hash) { window.location.hash = `#/${DEFAULT_ROUTE}`; } }