feat(M02): 저장 안 한 고침 물음 단추 셋 — 취소 · 버리고 넘어감 · 저장 후 넘어감, settle 카드 저장 실패 흠(86-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements";
|
||||
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import type {
|
||||
@@ -68,7 +69,7 @@ const TEXT = {
|
||||
noFieldNumber: "{value} 없음",
|
||||
noSheet: "스프레드시트를 못 띄움 — {value}",
|
||||
readFailed: "{value} 파일을 못 읽음 — {why}",
|
||||
discard: "카드에 저장 안 한 고침이 있음 — 버리고 다른 항목을 열까요?",
|
||||
discard: "카드에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
|
||||
pills: "알약",
|
||||
pillsTip: "아래 알약 줄 보임 · 숨김",
|
||||
take: "가져오기",
|
||||
@@ -179,6 +180,9 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
|
||||
const numberOf = (kind: CardKind, id: string): string => opts.numberOf?.(kind, id) ?? "";
|
||||
const anyDirty = (): boolean => cards.some((c) => c.saver.dirty());
|
||||
const saveAll = async (): Promise<void> => {
|
||||
for (const c of cards) if (c.saver.dirty() && !(await c.saver.run())) throw new Error();
|
||||
};
|
||||
const reload = (): void => void load(column ? { id: column, label } : null, true);
|
||||
|
||||
/** 카드 틀 하나 — 제목 옆 = ? · 제목 줄 오른쪽 = [저장] · [〇〇 수정](빈 단추 한 모양) · 부제 = 번호 · 이름 */
|
||||
@@ -461,7 +465,12 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
|
||||
async function load(col: { id: string; label: string } | null, again = false): Promise<void> {
|
||||
// 카드 고침을 저장 안 했으면 버릴지 물음(한 번) — 취소면 보던 열 그대로
|
||||
if (!again && anyDirty() && col?.id !== column && !(await showConfirmDialog(TEXT.discard)))
|
||||
if (
|
||||
!again &&
|
||||
anyDirty() &&
|
||||
col?.id !== column &&
|
||||
(await leaveOrStay(TEXT.discard, { save: saveAll, stillDirty: anyDirty })) === "stay"
|
||||
)
|
||||
return;
|
||||
const mine = ++seq;
|
||||
drop();
|
||||
@@ -530,9 +539,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
},
|
||||
column: () => column,
|
||||
dirty: anyDirty,
|
||||
saveAll: async () => {
|
||||
for (const c of cards) if (c.saver.dirty() && !(await c.saver.run())) throw new Error();
|
||||
},
|
||||
saveAll,
|
||||
discard: () => {
|
||||
drop();
|
||||
opts.onDirty?.(false);
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* 글자는 이 파일에 둠(도면 부품 `M02_MasterTemplete_Drawing.ts` 와 같은 방식).
|
||||
* ========================================================================== */
|
||||
|
||||
import { showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import {
|
||||
clearPart,
|
||||
@@ -252,9 +253,12 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
if ((col?.id ?? null) !== column && cards.dirty()) {
|
||||
// 카드에 저장 안 한 고침 — 버릴지 물음 · 머물면 표의 고른 열을 되돌림
|
||||
const from = column;
|
||||
void showConfirmDialog(TEXT.discardCards).then((ok) => {
|
||||
if (!ok) return void (from && opts.keepColumn?.(from));
|
||||
cards.discard(); // 한 번 물었음 — 카드가 다시 묻지 않게
|
||||
void leaveOrStay(TEXT.discardCards, {
|
||||
save: cards.saveAll,
|
||||
stillDirty: cards.dirty,
|
||||
discard: cards.discard, // 한 번 물었음 — 카드가 다시 묻지 않게
|
||||
}).then((got) => {
|
||||
if (got === "stay") return void (from && opts.keepColumn?.(from));
|
||||
void load(col);
|
||||
});
|
||||
return;
|
||||
@@ -272,7 +276,12 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
const toNo = to && numbers[to.id]?.[PART_FIELD[kind]];
|
||||
if (!to || !toNo) return void showToast(TEXT.saveFirst, "error");
|
||||
// 카드에 저장 안 한 고침 — 가져온 뒤 카드를 다시 읽어 버려지니 먼저 물음
|
||||
if (cards.dirty() && !(await showConfirmDialog(TEXT.discardCards))) return;
|
||||
if (
|
||||
cards.dirty() &&
|
||||
(await leaveOrStay(TEXT.discardCards, { save: cards.saveAll, stillDirty: cards.dirty })) ===
|
||||
"stay"
|
||||
)
|
||||
return;
|
||||
const target = `${toNo} ${columnHead(to)}`;
|
||||
const here = { layer: getActiveLayer(), owner: getActiveOwner() };
|
||||
const same = (from: From): boolean => from.layer === here.layer && from.owner === here.owner;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_TreePanel_Dialogs.ts
|
||||
* 구조물 라이브러리 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기 · 저장/버림 물음.
|
||||
* 구조물 라이브러리 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기.
|
||||
* 모두 약속(Promise)으로 답함 — 닫기 · Esc · 바깥 누르기 = 취소(null / "cancel").
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -183,36 +183,3 @@ export function askFolder(
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export type Settle = "save" | "discard" | "cancel";
|
||||
|
||||
/** 저장 안 한 표 고침 — 저장하고 계속 · 버리고 계속 · 취소 */
|
||||
export function askSettle(): Promise<Settle> {
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (value: Settle): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
resolve(value);
|
||||
};
|
||||
openModal({
|
||||
title: L("M02_TreeUnsaved"),
|
||||
closeLabel: L("M02_TreeCancel"),
|
||||
dialogClass: "m02-modal m02-treedlg",
|
||||
beforeClose: () => (finish("cancel"), true),
|
||||
mount: (body, close) => {
|
||||
const choose = (value: Settle) => () => {
|
||||
finish(value);
|
||||
close();
|
||||
};
|
||||
const save = createButton({ label: L("M02_TreeSaveGo"), variant: "filled" });
|
||||
const discard = createButton({ label: L("M02_TreeDiscardGo"), variant: "danger" });
|
||||
const cancel = createButton({ label: L("M02_TreeCancel"), variant: "ghost" });
|
||||
save.addEventListener("click", choose("save"));
|
||||
discard.addEventListener("click", choose("discard"));
|
||||
cancel.addEventListener("click", choose("cancel"));
|
||||
body.append(el("div", { className: "m02-side__row", children: [save, discard, cancel] }));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { setLeaveGuard } from "../A00_Common/router";
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
|
||||
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
@@ -26,7 +27,6 @@ import {
|
||||
type StructureColumn,
|
||||
type StructurePanelHandle,
|
||||
} from "./M02_MasterTemplete_Structure";
|
||||
import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel";
|
||||
import { createAreaToDrawing, sentRanges } from "./M02_MasterTemplete_AreaToDrawing";
|
||||
import { detailHelp, HELP_TITLE } from "./M02_MasterTemplete_CardHelp";
|
||||
@@ -202,19 +202,26 @@ export function createMain(
|
||||
event.returnValue = "";
|
||||
};
|
||||
window.addEventListener("beforeunload", guard);
|
||||
const confirmLeave = async (): Promise<boolean> => {
|
||||
if (!busy()) return true; // 카드 고침(도면 · 엑셀)도 같은 물음
|
||||
if (!(await showConfirmDialog(L("M02_DiscardConfirm")))) {
|
||||
// 취소 — 초점을 시트(고른 칸 입력 자리) · 집계표 · 도면으로 돌려 키보드 그대로
|
||||
host
|
||||
.querySelector<HTMLElement>(".ss-input, .ui-sheet, iframe")
|
||||
?.focus({ preventScroll: true });
|
||||
return false;
|
||||
}
|
||||
dirty = false;
|
||||
toDrawing.drop();
|
||||
return true;
|
||||
/**
|
||||
* 저장 안 한 고침 물음(떠나기 · 나무 고치기 한 몸) — 단추 셋 [취소] [버리고 넘어감] [저장 후 넘어감].
|
||||
* "clean" = 고침 없음 · "saved" · "discarded" · false = 머묾(취소 · 저장 실패 · 판 다름 · 카드 저장 실패).
|
||||
* 열린 양식이 없는 화면(옵션 정의 등)은 저장할 것이 없어 2단추 그대로.
|
||||
*/
|
||||
const askUnsaved = async (message: string): Promise<false | "clean" | "saved" | "discarded"> => {
|
||||
if (!busy()) return "clean"; // 카드 고침(도면 · 엑셀)도 같은 물음
|
||||
const drop_ = (): void => {
|
||||
dirty = false;
|
||||
toDrawing.drop();
|
||||
};
|
||||
let got: "saved" | "discarded" | "stay";
|
||||
if (sel) got = await leaveOrStay(message, { save: doSave, stillDirty: busy, discard: drop_ });
|
||||
else got = (await showConfirmDialog(message)) ? (drop_(), "discarded") : "stay";
|
||||
if (got !== "stay") return got;
|
||||
// 머묾 — 초점을 시트(고른 칸 입력 자리) · 집계표 · 도면으로 돌려 키보드 그대로
|
||||
host.querySelector<HTMLElement>(".ss-input, .ui-sheet, iframe")?.focus({ preventScroll: true });
|
||||
return false;
|
||||
};
|
||||
const confirmLeave = async (): Promise<boolean> => !!(await askUnsaved(L("M02_DiscardConfirm")));
|
||||
setLeaveGuard(confirmLeave); // 머리 메뉴 · 뒤로 가기로 떠날 때도 물음
|
||||
|
||||
const showNotice = (nodes: (HTMLElement | string)[]): void => {
|
||||
@@ -573,15 +580,9 @@ export function createMain(
|
||||
}
|
||||
|
||||
async function settle(): Promise<false | "clean" | "changed"> {
|
||||
if (!busy()) return "clean";
|
||||
const choice = await askSettle();
|
||||
if (choice === "cancel") return false;
|
||||
if (choice === "save") {
|
||||
await doSave();
|
||||
return dirty ? false : "changed"; // 저장이 막히면(판 다름 등) 나무도 안 고침
|
||||
}
|
||||
dirty = false; // 버림 — 화면을 서버 그대로 다시 읽음
|
||||
if (sel) await open(sel);
|
||||
const got = await askUnsaved(L("M02_TreeUnsaved"));
|
||||
if (got === false || got === "clean") return got;
|
||||
if (got === "discarded" && sel) await open(sel); // 버림 — 화면을 서버 그대로 다시 읽음
|
||||
return "changed";
|
||||
}
|
||||
|
||||
|
||||
@@ -66,11 +66,14 @@ def test_알약은_전체_편집기와_같은_부품():
|
||||
def test_저장_안_한_카드_고침_물음():
|
||||
struct = _text(M02 / "M02_MasterTemplete_Structure.ts")
|
||||
show = struct.split("const show = ")[1].split("const pickPrice")[0]
|
||||
assert "cards.dirty()" in show and "showConfirmDialog(TEXT.discardCards)" in show
|
||||
assert "cards.dirty()" in show and "leaveOrStay(TEXT.discardCards," in show
|
||||
assert "opts.keepColumn?.(from)" in show
|
||||
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
|
||||
assert "const busy = (): boolean => dirty || !!panel?.dirty() || toDrawing.waiting();" in main
|
||||
assert main.count("busy()") >= 3 # 떠나기 · 새로고침 막기 · 나무 고침(settle)
|
||||
assert main.count("busy()") >= 2 # 떠나기 · 새로고침 막기
|
||||
assert (
|
||||
'askUnsaved(L("M02_TreeUnsaved"))' in main
|
||||
) # 나무 고침(settle)도 같은 물음 · 카드 저장 실패까지 봄
|
||||
assert main.index("await panel.saveCards();") < main.index("await panel.reload();")
|
||||
assert "keepColumn: (colId) => sheet?.selectColumn(colId)" in main
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def test_메뉴는_숨기고_저장은_도면_칸만():
|
||||
def test_저장_안_한_카드_고침은_물음():
|
||||
cards = _text("M02_MasterTemplete_ItemCards.ts")
|
||||
# 물음 한 번(31-7) — 어느 카드든(도면 · 엑셀) 고침이 있으면 · 틀의 저장 표시(saver)로 봄
|
||||
assert "if (!again && anyDirty() && col?.id !== column && !(await showConfirmDialog(" in cards
|
||||
assert "(await leaveOrStay(TEXT.discard, { save: saveAll, stillDirty: anyDirty })) === \"stay\"" in cards
|
||||
assert "const anyDirty = (): boolean => cards.some((c) => c.saver.dirty());" in cards
|
||||
assert "dirty: anyDirty," in cards
|
||||
assert "dirty: () => cards.dirty()," in _text("M02_MasterTemplete_Structure.ts")
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""저장 안 한 고침 3단추 물음(PLAN 86-1 · `ui_template_leave_dialog.ts`) — Node 로 TS 를 돌려 판정.
|
||||
|
||||
못박는 것
|
||||
- 단추 순서 [취소] [버리고 넘어감] [저장 후 넘어감] · 기본 초점 = 취소 · Esc = 취소.
|
||||
- 저장 후 넘어감: 저장 성공 + 고침 없음 = saved / 저장 던짐 · 고침 남음 = stay(머묾).
|
||||
- 버리고 넘어감: discard 갈고리 호출 · 취소: 아무것도 안 부름.
|
||||
- M02 자리(UI_Main · Structure · ItemCards)가 이 물음을 씀.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
SCRIPT = r"""
|
||||
const fs = require("fs");
|
||||
const ts = require(process.argv[1] + "/config/node_modules/typescript");
|
||||
const src = fs.readFileSync(process.argv[1] + "/ui_template/ui_template_leave_dialog.ts", "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
let focused = null;
|
||||
let overlay = null;
|
||||
let buttons = [];
|
||||
const node = (tag, o = {}) => ({
|
||||
tag, ...o, kids: [], on: {},
|
||||
append(...x) { this.kids.push(...x); },
|
||||
addEventListener(k, f) { this.on[k] = f; },
|
||||
remove() { overlay = null; },
|
||||
});
|
||||
const stubs = {
|
||||
"./ui_template_elements": {
|
||||
el: node,
|
||||
createButton: (o) => {
|
||||
const b = node("button", { label: o.label, variant: o.variant });
|
||||
b.focus = () => { focused = b.label; };
|
||||
b.click = o.onClick;
|
||||
buttons.push(b);
|
||||
return b;
|
||||
},
|
||||
},
|
||||
"./ui_template_locale": { t: (k) => k },
|
||||
};
|
||||
globalThis.document = { activeElement: null, body: { append(x) { overlay = x; } } };
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (n) => stubs[n] ?? require(n));
|
||||
const { leaveOrStay } = box.exports;
|
||||
|
||||
const run = async (pick, hooks) => {
|
||||
buttons = [];
|
||||
focused = null;
|
||||
const calls = [];
|
||||
const wrap = {
|
||||
save: async () => { calls.push("save"); if (hooks.throws) throw new Error("x"); },
|
||||
stillDirty: () => !!hooks.left,
|
||||
discard: () => calls.push("discard"),
|
||||
};
|
||||
const p = leaveOrStay("msg", wrap);
|
||||
const labels = buttons.map((b) => b.label);
|
||||
const first = focused;
|
||||
if (pick === "esc") overlay.on.keydown({ key: "Escape", stopPropagation() {} });
|
||||
else buttons[pick].click();
|
||||
return { got: await p, calls, labels, first };
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const out = {
|
||||
cancel: await run(0, {}),
|
||||
esc: await run("esc", {}),
|
||||
discard: await run(1, {}),
|
||||
save: await run(2, {}),
|
||||
saveThrows: await run(2, { throws: true }),
|
||||
saveLeft: await run(2, { left: true }),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})();
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
done = subprocess.run(
|
||||
["node", "-e", SCRIPT, str(ROOT)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_단추_순서_기본_초점_취소(got):
|
||||
row = got["cancel"]
|
||||
assert row["labels"] == ["M02_TreeCancel", "M02_TreeDiscardGo", "M02_TreeSaveGo"]
|
||||
assert row["first"] == "M02_TreeCancel"
|
||||
|
||||
|
||||
def test_취소_Esc는_머묾(got):
|
||||
assert got["cancel"]["got"] == "stay" and got["cancel"]["calls"] == []
|
||||
assert got["esc"]["got"] == "stay" and got["esc"]["calls"] == []
|
||||
|
||||
|
||||
def test_버리고_넘어감(got):
|
||||
assert got["discard"]["got"] == "discarded"
|
||||
assert got["discard"]["calls"] == ["discard"]
|
||||
|
||||
|
||||
def test_저장_후_넘어감과_실패_머묾(got):
|
||||
assert got["save"] == {**got["save"], "got": "saved", "calls": ["save"]}
|
||||
assert got["saveThrows"]["got"] == "stay" # 저장 던짐
|
||||
assert got["saveLeft"]["got"] == "stay" # 저장 뒤에도 고침이 남음(카드 저장 실패 등)
|
||||
|
||||
|
||||
def test_M02_자리가_3단추_물음을_씀():
|
||||
main = (M02 / "M02_MasterTemplete_UI_Main.ts").read_text(encoding="utf-8")
|
||||
assert "leaveOrStay(message, { save: doSave, stillDirty: busy" in main
|
||||
assert "askSettle" not in main
|
||||
assert "!(await showConfirmDialog(L(" not in main
|
||||
cards = (M02 / "M02_MasterTemplete_ItemCards.ts").read_text(encoding="utf-8")
|
||||
assert "leaveOrStay(TEXT.discard" in cards
|
||||
struct = (M02 / "M02_MasterTemplete_Structure.ts").read_text(encoding="utf-8")
|
||||
assert struct.count("leaveOrStay(TEXT.discardCards") == 2
|
||||
@@ -0,0 +1,84 @@
|
||||
/* =============================================================================
|
||||
* ui_template_leave_dialog.ts
|
||||
* 저장 안 한 고침이 있을 때 넘어가기 전 물음 — 단추 셋: [취소] [버리고 넘어감] [저장 후 넘어감](주 단추).
|
||||
* 기본 초점 = 취소 · Esc = 취소 · 저장이 실패하거나 고침이 남으면 머묾.
|
||||
* 몸은 공용 확인 창(`showConfirmDialog`)과 같은 틀(.ui-confirm).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "./ui_template_elements";
|
||||
import { t as L } from "./ui_template_locale";
|
||||
|
||||
export type LeaveChoice = "save" | "discard" | "cancel";
|
||||
|
||||
/** 3단추 물음 — 닫기 · Esc = "cancel" */
|
||||
export function askLeave(message: string): Promise<LeaveChoice> {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = el("div", { className: "ui-confirm" });
|
||||
const panel = el("div", { className: "ui-confirm__panel" });
|
||||
const actions = el("div", { className: "ui-confirm__actions" });
|
||||
const back = document.activeElement as HTMLElement | null; // 닫힐 때 초점을 돌려놓을 자리
|
||||
const done = (choice: LeaveChoice): void => {
|
||||
overlay.remove();
|
||||
if (back?.isConnected && back !== document.body) back.focus({ preventScroll: true });
|
||||
resolve(choice);
|
||||
};
|
||||
const cancel = createButton({
|
||||
label: L("M02_TreeCancel"),
|
||||
variant: "ghost",
|
||||
onClick: () => done("cancel"),
|
||||
});
|
||||
actions.append(
|
||||
cancel,
|
||||
createButton({
|
||||
label: L("M02_TreeDiscardGo"),
|
||||
variant: "danger",
|
||||
onClick: () => done("discard"),
|
||||
}),
|
||||
createButton({
|
||||
label: L("M02_TreeSaveGo"),
|
||||
variant: "filled",
|
||||
onClick: () => done("save"),
|
||||
}),
|
||||
);
|
||||
overlay.addEventListener("keydown", (ev) => {
|
||||
if (ev.key !== "Escape") return;
|
||||
ev.stopPropagation(); // 뒤 창 · 페이지 Esc 까지 닫지 않음
|
||||
done("cancel");
|
||||
});
|
||||
panel.append(el("p", { className: "ui-confirm__message", text: message }), actions);
|
||||
overlay.append(panel);
|
||||
document.body.append(overlay);
|
||||
cancel.focus(); // 창에 초점 — 뒤 칸에 글이 들어가지 않고 Esc 가 먹음
|
||||
});
|
||||
}
|
||||
|
||||
export interface LeaveHooks {
|
||||
/** 저장 한 번 — 실패는 던지거나 `stillDirty` 로 알림(알림 글은 저장 길이 띄움) */
|
||||
save: () => Promise<unknown>;
|
||||
/** 저장 뒤에도 고침이 남았나 — 남으면 머묾 */
|
||||
stillDirty: () => boolean;
|
||||
/** [버리고 넘어감] 때 */
|
||||
discard?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 물음 → 고름대로 처리. 넘어가도 되면 "saved" | "discarded", 머물면 "stay".
|
||||
* 저장 실패(던짐) · 저장 뒤 고침 남음 · 취소 = "stay".
|
||||
*/
|
||||
export async function leaveOrStay(
|
||||
message: string,
|
||||
hooks: LeaveHooks,
|
||||
): Promise<"saved" | "discarded" | "stay"> {
|
||||
const choice = await askLeave(message);
|
||||
if (choice === "cancel") return "stay";
|
||||
if (choice === "discard") {
|
||||
await hooks.discard?.();
|
||||
return "discarded";
|
||||
}
|
||||
try {
|
||||
await hooks.save();
|
||||
} catch {
|
||||
return "stay";
|
||||
}
|
||||
return hooks.stillDirty() ? "stay" : "saved";
|
||||
}
|
||||
@@ -154,8 +154,8 @@ export const ui_locales_m2 = {
|
||||
"저장 안 한 표 고침이 있음 — 나무를 고치기 전에 처리",
|
||||
"Unsaved table edits — settle them before changing the tree",
|
||||
],
|
||||
M02_TreeSaveGo: ["저장하고 계속", "Save and continue"],
|
||||
M02_TreeDiscardGo: ["버리고 계속", "Discard and continue"],
|
||||
M02_TreeSaveGo: ["저장 후 넘어감", "Save and continue"],
|
||||
M02_TreeDiscardGo: ["버리고 넘어감", "Discard and continue"],
|
||||
M02_TreeDone: ["나무를 고침", "Tree updated"],
|
||||
M02_PracticeTitle: ["실무문서", "Practice documents"],
|
||||
M02_PracticeRow: ["실무문서 목록", "Practice document list"],
|
||||
|
||||
Reference in New Issue
Block a user