fix(M02): 재검증 흠 넷 — 같은 이름 토스트 · 쓸 수 없는 글자 막기(화면·서버 400) · 저장 안 한 고침 버리기 확인 · 덮어쓰기 확인과 개인·회사 지우기 단추

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:01:47 +09:00
co-authored by Claude Sonnet 5
parent 2aa4c80577
commit 6555313a22
8 changed files with 114 additions and 27 deletions
@@ -4,7 +4,7 @@
* 부품 파일이 없거나 못 읽어도 페이지는 뜸 — 빈 자리 글.
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import {
readFilled,
@@ -48,6 +48,8 @@ export interface MainHandle {
root: HTMLElement;
open: (sel: Selection | null) => Promise<void>;
current: () => Selection | null;
/** 저장 안 한 고침이 있으면 버릴지 물음 — 취소면 false */
confirmLeave: () => Promise<boolean>;
}
/** 이 층에서 고칠 수 있나 — 시스템은 관리자만 · 회사는 여기서 못 고침(공식으로 저장으로만) */
@@ -75,6 +77,21 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
let loaded: unknown = null;
let editor: EditorHandle | null = null;
let seq = 0;
let dirty = false;
const guard = (event: BeforeUnloadEvent): void => {
if (!root.isConnected) return window.removeEventListener("beforeunload", guard);
if (!dirty) return;
event.preventDefault();
event.returnValue = "";
};
window.addEventListener("beforeunload", guard);
const confirmLeave = async (): Promise<boolean> => {
if (!dirty) return true;
if (!(await showConfirmDialog(L("M02_DiscardConfirm")))) return false;
dirty = false;
return true;
};
const showNotice = (nodes: (HTMLElement | string)[]): void => {
notice.replaceChildren(...nodes);
@@ -102,7 +119,10 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
"createSheet",
);
if (create) {
editor = create(host, doc, { mode: at.layer === "project" ? "project" : "master" });
editor = create(host, doc, {
mode: at.layer === "project" ? "project" : "master",
onChange: () => (dirty = !readOnly),
});
return;
}
return empty(L("M02_NoSheet"));
@@ -118,6 +138,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
name: at.name,
layer: at.layer,
projectId: at.projectId,
onChanged: (d: boolean) => (dirty = d && !readOnly),
});
return;
}
@@ -126,6 +147,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
async function open(next: Selection | null): Promise<void> {
const mine = ++seq;
dirty = false;
drop();
showNotice([]);
sel = next;
@@ -166,6 +188,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
const info = await saveTemplate(at.layer, at.kind, at.name, at.projectId, version, doc);
version = info.판;
loaded = doc;
dirty = false;
showNotice([]);
showToast(L("M02_Saved"), "success");
onSaved?.(); // 좌측 목록의 판을 새 판으로
@@ -182,8 +205,10 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
}
save.addEventListener("click", () => void doSave());
reload.addEventListener("click", () => sel && void open(sel));
reload.addEventListener("click", async () => {
if (sel && (await confirmLeave())) void open(sel);
});
bar();
empty("");
return { root, open, current: () => sel };
return { root, open, current: () => sel, confirmLeave };
}