Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts
T
eomsangdonandClaude Sonnet 5.5 2c87f91f36 fix(B05·B06): 고르기 모달 [해제] 가 [저장] 에서 사라지던 흠 · 항목 더하기 창 빈 칸 안내 (PLAN 26장)
- 구조물 저장은 세션 초안을 보냄(횡단 보기에서 한 지정 · 해제가 종단 보기 메모리 때문에 사라지던 것)
- 관 시설 보관소는 서버 응답을 정본으로(집계 항목 칸이 없으면 미지정 · 옛 지정이 되살아나던 것)
- 항목 더하기 · 새 항목 창: 빈 이름 · 빈 단위에 칸 빨간 테두리 + 한 줄

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
2026-09-30 10:56:13 +09:00

143 lines
5.2 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_TreePanel_Dialogs.ts
* 항목 나무 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기 · 저장/버림 물음.
* 모두 약속(Promise)으로 답함 — 닫기 · Esc · 바깥 누르기 = 취소(null / "cancel").
* ========================================================================== */
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { openModal } from "@ui/ui_template_modal";
export interface TextField {
key: string;
label: string;
value?: string;
/** 비워도 됨 — 아니면 비운 채 [확인] 하면 그 칸에 안내 글 */
optional?: boolean;
}
/** 글 칸 하나 이상을 받음 — 확인이면 칸 이름 → 글(앞뒤 공백 뺌) */
export function askText(
title: string,
fields: TextField[],
okLabel = L("M02_TreeOk"),
): Promise<Record<string, string> | null> {
return new Promise((resolve) => {
let done = false;
const finish = (value: Record<string, string> | null): void => {
if (done) return;
done = true;
resolve(value);
};
openModal({
title,
closeLabel: L("M02_Close"),
dialogClass: "m02-modal m02-treedlg",
beforeClose: () => (finish(null), true),
mount: (body, close) => {
const inputs = fields.map((f) => ({
f,
field: createInputField({ label: f.label, value: f.value }),
}));
const ok = createButton({ label: okLabel, variant: "filled" });
const submit = (): void => {
// 빈 칸은 그 칸에 빨간 테두리 + 한 줄(칸마다) — 토스트는 창 뒤에 가려 안 보였음
const out: Record<string, string> = {};
let bad = false;
for (const { f, field } of inputs) {
out[f.key] = field.input.value.trim();
const empty = !out[f.key] && !f.optional;
field.setError(empty ? L("M02_FieldNeeded").replace("{label}", f.label) : undefined);
bad ||= empty;
}
if (bad) return;
finish(out);
close();
};
ok.addEventListener("click", submit);
for (const { field } of inputs) {
field.input.addEventListener("input", () => field.setError(undefined));
field.input.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.isComposing) submit();
});
}
body.append(...inputs.map(({ field }) => field.root), ok);
inputs[0]?.field.input.select();
},
});
});
}
/** 옮길 폴더 고르기 — `folders` 에서 `blocked` 인 것은 뺌 · 맨 위(폴더 밖)는 늘 있음 */
export function askFolder(
title: string,
folders: string[][],
blocked: (path: string[]) => boolean,
current: string[],
): Promise<string[] | null> {
return new Promise((resolve) => {
let done = false;
const finish = (value: string[] | null): void => {
if (done) return;
done = true;
resolve(value);
};
const choices = [[], ...folders.filter((path) => !blocked(path))];
openModal({
title,
closeLabel: L("M02_Close"),
dialogClass: "m02-modal m02-treedlg",
beforeClose: () => (finish(null), true),
mount: (body, close) => {
const field = createSelectField({
label: L("M02_TreeMoveTo"),
options: choices.map((path, i) => ({
value: String(i),
text: path.length ? path.join(" / ") : L("M02_TreeTop"),
})),
value: String(choices.findIndex((path) => path.join("|") === current.join("|"))),
});
const ok = createButton({ label: L("M02_TreeOk"), variant: "filled" });
ok.addEventListener("click", () => {
finish(choices[Number(field.select.value)] ?? []);
close();
});
body.append(field.root, ok);
},
});
});
}
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] }));
},
});
});
}