diff --git a/A00_Common/main.ts b/A00_Common/main.ts index 529fdbc5..192dc84f 100644 --- a/A00_Common/main.ts +++ b/A00_Common/main.ts @@ -12,7 +12,13 @@ import "@ui/ui_template_theme.css"; import { injectBaseStyles } from "@ui/ui_template_elements"; import { ROUTES, type RoutePath } from "@config/config_frontend"; import { initThemeAndLang, injectShellStyles, renderShell } from "./app_shell"; -import { currentRoute, renderCurrentRoute, ensureInitialHash } from "./router"; +import { + currentRoute, + renderCurrentRoute, + ensureInitialHash, + setLeaveGuard, + takeLeaveGuard, +} from "./router"; const FOOTERLESS_ROUTES: readonly RoutePath[] = [ ROUTES.B01_ACCOUNT, @@ -44,14 +50,27 @@ function bootstrap(): void { initThemeAndLang(); // 3. 셸 렌더 + 현재 라우트 렌더를 함께 수행하는 헬퍼 + let shown = ""; const renderAll = (): void => { + shown = currentRoute(); const showFooter = !FOOTERLESS_ROUTES.includes(currentRoute()); const outlet = renderShell(appRoot, showFooter); // 헤더/푸터(로그인·언어 반영) 갱신 void renderCurrentRoute(outlet); // 아울렛에 페이지 렌더 }; // 4. hashchange 1회 구독 (리스너 누적 방지) - window.addEventListener("hashchange", renderAll); + window.addEventListener("hashchange", () => { + const guard = takeLeaveGuard(); + if (!guard || currentRoute() === shown) return renderAll(); + void guard().then((ok) => { + if (ok) { + setLeaveGuard(null); + renderAll(); + } else { + history.replaceState(null, "", `#/${shown}`); // 취소 — 머문 채 주소만 되돌림 + } + }); + }); // 5. 최초 진입 ensureInitialHash(); diff --git a/A00_Common/router.ts b/A00_Common/router.ts index 38f59c3d..ae56148f 100644 --- a/A00_Common/router.ts +++ b/A00_Common/router.ts @@ -65,6 +65,13 @@ const routeTable: Partial Promise>> = { (await import("../M02_MasterTemplete/M02_MasterTemplete_UI_Page")).renderM02MasterTemplate, }; +/** 페이지가 떠나기 전에 묻는 자리 — false 를 돌려주면 이동을 취소(주소도 되돌림). 이동이 끝나면 비워짐. */ +let leaveGuard: (() => Promise) | null = null; +export const setLeaveGuard = (guard: (() => Promise) | null): void => { + leaveGuard = guard; +}; +export const takeLeaveGuard = (): (() => Promise) | null => leaveGuard; + /** 로그인 여부 (토큰 존재 확인) */ export async function isAuthenticated(): Promise { const { fetchSession } = await import("../A06_Login/A06_Login_Api_Fetch"); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 9fe59d66..ca67f4c9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -83,13 +83,14 @@ export const saveTemplate = ( export const deleteTemplate = (kind: Kind, name: string, 판: string): Promise<{ ok: boolean }> => call(`/templates/${kind}/${enc(name)}?판=${enc(판)}`, { method: "DELETE" }); -/** 개인·회사 층에서 지움 — 서버 길은 sub4 (없으면 404) */ +/** 개인·회사 층에서 지움 — 판이 맞을 때만(다르면 409) */ export const deleteLayerTemplate = ( layer: Layer, kind: Kind, name: string, + 판: string, ): Promise<{ ok: boolean }> => - call(`/layers/${layer}/templates/${kind}/${enc(name)}`, { method: "DELETE" }); + call(`/layers/${layer}/templates/${kind}/${enc(name)}?판=${enc(판)}`, { method: "DELETE" }); /* --- 프로젝트 층 단추 다섯 --- */ const project = (id: string, tail: string): string => `/projects/${enc(id)}/templates/${tail}`; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 516c1bae..97cb7ddd 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -4,6 +4,7 @@ * 부품 파일이 없거나 못 읽어도 페이지는 뜸 — 빈 자리 글. * ========================================================================== */ +import { setLeaveGuard } from "../A00_Common/router"; import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { @@ -92,6 +93,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle { dirty = false; return true; }; + setLeaveGuard(confirmLeave); // 머리 메뉴 · 뒤로 가기로 떠날 때도 물음 const showNotice = (nodes: (HTMLElement | string)[]): void => { notice.replaceChildren(...nodes); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index b9474ecc..4d30e180 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -365,7 +365,7 @@ export function buildSide(opt: SideOptions): SideHandle { if (!(await showConfirmDialog(L("M02_DeleteConfirm").replace("{value}", info.이름)))) return; try { if (layer === "system") await deleteTemplate(info.종류, info.이름, info.판); - else await deleteLayerTemplate(layer, info.종류, info.이름); + else await deleteLayerTemplate(layer, info.종류, info.이름, info.판); showToast(L("M02_Deleted"), "success"); opt.onOpen(null); await refresh(); @@ -406,7 +406,12 @@ export function buildSide(opt: SideOptions): SideHandle { } catch (error) { return failed(error); } - void act(() => saveProjectAs(projectId, to, cur.kind, cur.name)); + try { + await saveProjectAs(projectId, to, cur.kind, cur.name); + showToast(L("M02_Done"), "success"); // 열던 양식은 그대로 둠 + } catch (error) { + failed(error); + } }; projectButton(L("M02_ProjectReset"), async () => { if (await showConfirmDialog(L("M02_ResetConfirm"))) void act(() => resetProject(projectId));