From 847f0e44defd291e244f6b9b97fadf3f85dd9ea7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 25 Sep 2026 17:19:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=20=EA=B0=9C=EC=9D=B8=20=EC=B8=B5=20?= =?UTF-8?q?=EC=A7=80=EC=9A=B0=EA=B8=B0=EC=97=90=20=ED=8C=90=EC=9D=84=20?= =?UTF-8?q?=EB=B3=B4=EB=83=84=20=C2=B7=20=EC=A0=80=EC=9E=A5=20=EC=95=88=20?= =?UTF-8?q?=ED=95=9C=20=EA=B3=A0=EC=B9=A8=EC=9D=B4=20=EC=9E=88=EC=9C=BC?= =?UTF-8?q?=EB=A9=B4=20=EB=A8=B8=EB=A6=AC=20=EB=A9=94=EB=89=B4=C2=B7?= =?UTF-8?q?=EB=92=A4=EB=A1=9C=20=EA=B0=80=EA=B8=B0=EB=A1=9C=20=EB=96=A0?= =?UTF-8?q?=EB=82=A0=20=EB=95=8C=EB=8F=84=20=ED=99=95=EC=9D=B8(=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=ED=95=98=EB=A9=B4=20M02=20=EC=97=90=20=EB=82=A8?= =?UTF-8?q?=EA=B3=A0=20=EC=A3=BC=EC=86=8C=20=EB=90=98=EB=8F=8C=EB=A6=BC)?= =?UTF-8?q?=20=C2=B7=20=EB=82=B4=20=EC=96=91=EC=8B=9D=C2=B7=ED=9A=8C?= =?UTF-8?q?=EC=82=AC=20=EA=B3=B5=EC=8B=9D=20=EC=A0=80=EC=9E=A5=20=EB=92=A4?= =?UTF-8?q?=20=EC=97=B4=EB=8D=98=20=EC=96=91=EC=8B=9D=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0148XFUpPfpiuTjxF1EK9c95 --- A00_Common/main.ts | 23 +++++++++++++++++-- A00_Common/router.ts | 7 ++++++ .../M02_MasterTemplete_Api_Fetch.ts | 5 ++-- .../M02_MasterTemplete_UI_Main.ts | 2 ++ .../M02_MasterTemplete_UI_Side.ts | 9 ++++++-- 5 files changed, 40 insertions(+), 6 deletions(-) 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));