Files
Aislo/A00_Common/router.ts
T
eomsangdonandClaude Fable 5 ec9943417c feat(B07,B09): B07_Quantity 셸 페이지 신설 + B09_Estimation 접두사 정리 + 단계 순서 반영
- B09_wf6_Estimation -> B09_Estimation (폴더·파일·라우트 키/슬러그)
- B07_Quantity_UI_Page.ts 신설: 워크플로우 셸 + 좌측 [확정] 버튼
  (renderPendingWorkflow에 leftPanel 옵션 추가로 공용 셸 재사용)
- B07_Quantity_Router.py 신설: POST /api/projects/{id}/quantity/confirm
  -> complete_stage(4) 전이만 수행(본문 미구현), main.py 등록
- STAGE_KEYS 재정의: FILE_INPUT/PREPROCESS/PROFILE/SECTION/QUANTITY/DESIGN_DETAIL/ESTIMATION
- 스텝바 라벨·아이콘 4↔5 스왑(수량산출이 4차, 상세설계가 5차), A02 소개 문구 스왑
- 라우터 테이블에 b07-quantity 등록(플레이스홀더 제거), locale 키 5종 추가

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 10:15:07 +09:00

163 lines
6.5 KiB
TypeScript

/* =============================================================================
* router.ts
* 해시 기반 SPA 라우터 + 인증 가드
*
* - 각 라우트는 (root) => void | Promise<void> 형태의 렌더 함수를 등록.
* - 페이지 렌더 함수는 지연 로딩(dynamic import)으로 초기 번들 최소화.
* - PROTECTED_ROUTES는 토큰 없으면 A06_Login으로 리다이렉트.
* ========================================================================== */
import { ROUTES, DEFAULT_ROUTE, PROTECTED_ROUTES, type RoutePath } from "@config/config_frontend";
/** 페이지 렌더 함수 시그니처. root(마운트 대상)를 받아 내용을 채운다. */
export type PageRenderer = (root: HTMLElement) => void | Promise<void>;
/** 라우트별 지연 로더 등록 테이블.
* 페이지 구현이 완료되는 대로 아래에 한 줄씩 추가한다. */
const routeTable: Partial<Record<RoutePath, () => Promise<PageRenderer>>> = {
[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_Auth_Page")).renderA06Login,
[ROUTES.A07_REGISTER]: async () =>
(await import("../A07_Register/A07_Register_UI_Auth_Page")).renderA07Register,
[ROUTES.A08_SUPPORT]: async () =>
(await import("../A08_Support/A08_Support_UI_Page")).renderA08Support,
[ROUTES.A09_SECURITY]: async () =>
(await import("../A09_Security/A09_Security_UI_Page")).renderA09Security,
// 로그인 후 (B 그룹)
[ROUTES.B01_ACCOUNT]: async () =>
(await import("../B01_Dashboard/B01_Dashboard_UI_Page")).renderB01Dashboard,
[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_PREPROCESS]: async () =>
(await import("../B04_PreProcess/B04_PreProcess_UI_Page")).renderB04Surface,
[ROUTES.B05_PROFILE]: async () =>
(await import("../B05_Profile/B05_Profile_UI_Page")).renderB05Route,
[ROUTES.B06_SECTION]: async () =>
(await import("../B06_Section/B06_Section_UI_Page")).renderB06ProfileCross,
// B07_QUANTITY: 수량 본문은 재작성 예정(구 구현은 B07_Quantity/0_old_260726_codex.zip 백업).
// 지금은 셸 페이지 — 좌측 [확정]으로 stage 4를 닫고 B08 상세설계로 넘어간다.
[ROUTES.B07_QUANTITY]: async () =>
(await import("../B07_Quantity/B07_Quantity_UI_Page")).renderB07Quantity,
[ROUTES.B08_DESIGN_DETAIL]: async () =>
(await import("../B08_DesignDetail/B08_DesignDetail_UI_Page")).renderB08DesignDetail,
[ROUTES.B09_ESTIMATION]: async () =>
(await import("../B09_Estimation/B09_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,
[ROUTES.B11_LOADING]: async () =>
(await import("../B11_Status/B11_Status_UI_Loading")).renderB11Loading,
};
/** 로그인 여부 (토큰 존재 확인) */
export async function isAuthenticated(): Promise<boolean> {
const { fetchSession } = await import("../A06_Login/A06_Login_Api_Fetch");
return fetchSession();
}
/** 현재 해시에서 라우트 경로 추출 (#/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<void> {
const route = currentRoute();
if (PROTECTED_ROUTES.includes(route) && !(await isAuthenticated())) {
navigateTo(ROUTES.A06_LOGIN);
return;
}
const workflowRoutes: readonly RoutePath[] = [
ROUTES.B02_PROJ_REGISTER,
ROUTES.B03_FILE_INPUT,
ROUTES.B04_PREPROCESS,
ROUTES.B05_PROFILE,
ROUTES.B06_SECTION,
ROUTES.B07_QUANTITY,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B09_ESTIMATION,
ROUTES.B10_PAYMENT,
ROUTES.B11_STATUS,
ROUTES.B11_LOADING,
];
if (workflowRoutes.includes(route)) {
const { fetchSessionUser } = await import("../A06_Login/A06_Login_Api_Fetch");
try {
const user = await fetchSessionUser();
if (!user) {
navigateTo(ROUTES.A06_LOGIN);
return;
}
if (!user.company_id && user.role !== "SYSTEM_ADMIN") {
navigateTo(ROUTES.B01_ACCOUNT);
return;
}
} catch {
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}`;
}
}