fix(M02): 개인 층 지우기에 판을 보냄 · 저장 안 한 고침이 있으면 머리 메뉴·뒤로 가기로 떠날 때도 확인(취소하면 M02 에 남고 주소 되돌림) · 내 양식·회사 공식 저장 뒤 열던 양식 유지

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148XFUpPfpiuTjxF1EK9c95
This commit is contained in:
2026-09-25 17:19:24 +09:00
co-authored by Claude Sonnet 5
parent 7b2c22d920
commit 847f0e44de
5 changed files with 40 additions and 6 deletions
+21 -2
View File
@@ -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();
+7
View File
@@ -65,6 +65,13 @@ const routeTable: Partial<Record<RoutePath, () => Promise<PageRenderer>>> = {
(await import("../M02_MasterTemplete/M02_MasterTemplete_UI_Page")).renderM02MasterTemplate,
};
/** 페이지가 떠나기 전에 묻는 자리 — false 를 돌려주면 이동을 취소(주소도 되돌림). 이동이 끝나면 비워짐. */
let leaveGuard: (() => Promise<boolean>) | null = null;
export const setLeaveGuard = (guard: (() => Promise<boolean>) | null): void => {
leaveGuard = guard;
};
export const takeLeaveGuard = (): (() => Promise<boolean>) | null => leaveGuard;
/** 로그인 여부 (토큰 존재 확인) */
export async function isAuthenticated(): Promise<boolean> {
const { fetchSession } = await import("../A06_Login/A06_Login_Api_Fetch");
@@ -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}`;
@@ -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);
@@ -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));