Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Main_Actions.ts
T
eomsangdonandClaude Opus 5.5 f5d3347d1e fix(808-1): 줄 쌓기 수정 3차 — 수량 칸 식 글 칸 + 출처 꼬리표 · 글꼴 M01 = M02 · M02 머리 [단가 갱신][재생성] · 일위대가 줄 모달 일원화
- 수량 칸(요구 701-1) = 늘 식 글 칸(Q_특별인부) + 출처 꼬리표(표 = 표 고르기) · 눌러도 안 바뀜 · 표 값 기호에 수 = 그 값만(직접 입력) · 꼬리표 한 곳(sourceTag)
- 글꼴(701-2) = 줄 쌓기 화면이 정함(14px · M02 가 부모 16px 을 받던 것)
- M02 머리(701-3) = [초기화] 와 [되돌림] 뜻 다름 → [초기화] 이름 「M01 값으로」 · [단가 갱신][재생성] = 보기 고르기 옆 머리 칸(수식 보기만)
- 일위대가 단가산출 줄 모달(701-5) = 페이지 머리와 같음(보기 고르기 · [단가 갱신] · [재생성] · [저장])
- 일위대가 로직 줄 모달(701-4 · 5) = 옛 [옵션] · [단가산출로도 저장] · [항목 추가] · [율 줄 추가] 걷음 · 보기 고르기 없이 상세만 + 안내(온 단가산출) ·
  줄 문서 키 SK-000000 이 수식 문서를 읽어 404 · 「못 읽음」 이던 것 고침
- 시험: test_804_7 낡은 소스 기대(꼬리표 조건 글) 고침 + 808-1 · test_806_1 808-1 모달

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 18:52:03 +09:00

187 lines
8.0 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Main_Actions.ts
* 단가산출 · 일위대가 상세 머리 단추 [초기화] [삭제](740-2 · 738-2 · 요구 579-3 · 581) — 차례 … [초기화] [삭제] [저장].
* [삭제] = 735 경고 창(쓰는 곳 표 · 키) → 나무 지우기 → 목록으로 · 쓰는 곳 없으면 한 줄 물음.
* [초기화] = 서버 `-reset` 미리 보기(걷힐 고침 수) → 물음 → 그 층에 씀 → 다시 읽음.
* 덮개 · 뺀줄 · 더한줄 단가를 M01 키 값으로 되돌림 · 옵션(입력값) · 기본정보는 그대로(서버 740-1).
* 둘 다 쓰기 권한이 있는 층에서만 보임(`update`) · 상세에 저장 안 한 고침이 있으면 먼저 물음.
* 단가산출 상세(804-8 · M01 단가산출 기초와 같은 단추 규칙) = [새로고침] 숨김 · [되돌림] = 물음 → 다시 읽음(수식 캐시도 비움) ·
* [되돌림] · [저장] = 고침 있을 때만(`dim`)
* 808-1(요구 701-3) — [초기화] 이름 = 「M01 값으로」(뜻대로 · [되돌림] 과 구분) · 그 왼쪽 = 수식 [단가 갱신] · [재생성](머리 칸)
* ========================================================================== */
import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { askConfirm } from "@ui/ui_template_modal";
import {
editPrecomboTree,
fetchPrecomboTree,
resetLibrary,
type LibraryKind,
} from "./M02_MasterTemplete_Api_Fetch";
import { askLibraryDelete } from "./M02_MasterTemplete_Library_Delete";
import { precomboList } from "./M02_MasterTemplete_UI_Side_Precombo";
import { sangeunList } from "./M02_MasterTemplete_UI_Side_Sangeun";
import type { Selection } from "./M02_MasterTemplete_UI_Main";
const TEXT = {
Reset: ["M01 값으로", "To M01"], // 808-1 — 저장본을 M01 키 값으로 씀([되돌림] = 저장 안 한 고침 버림과 다름)
ResetTip: [
"고친 값을 연결된 M01 키 값으로 되돌림 — 옵션 · 기본정보는 그대로",
"Revert edited values to their linked M01 key values — options and basic info stay",
],
Delete: ["삭제", "Delete"],
DeleteTip: [
"이 항목을 지움 — 쓰는 곳이 있으면 먼저 알림",
"Delete this item — warns first if used",
],
ResetAsk: [
"고친 값 {n} 개를 M01 값으로 되돌림{k} — 옵션 · 기본정보는 그대로",
"Revert {n} edited value(s) to M01 values{k} — options and basic info stay",
],
Restored: [" (빼 둔 줄 {k} 개 되살림)", " ({k} removed line(s) restored)"],
Nothing: ["되돌릴 고침이 없음", "Nothing to revert"],
Done: ["M01 값으로 되돌렸음", "Reverted to M01 values"],
DeleteAsk: ["「{name}」 을(를) 지울까?", "Delete 「{name}」?"],
Deleted: ["지웠음", "Deleted"],
Undo: ["되돌림", "Revert"],
UndoAsk: [
"저장 안 한 고친 것(기본정보 · 수식)을 버리고 저장 뒤 상태로 되돌릴까?",
"Discard unsaved edits (info · formula) and revert to the saved state?",
],
Failed: ["못 했음 — {why}", "Failed — {why}"],
} as const;
const tx = (key: keyof typeof TEXT): string =>
String(TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]);
const fill = (text: string, vars: Record<string, string | number>): string =>
Object.entries(vars).reduce((s, [k, v]) => s.replace(`{${k}}`, String(v)), text);
export interface MainActionsCtx {
sel: () => Selection | null;
/** 지금 읽은 문서의 판 */
version: () => string;
/** 지금 읽은 문서(저장본) — 뺀 줄 수를 셈 */
loaded: () => unknown;
confirmLeave: () => Promise<boolean>;
open: (sel: Selection | null) => Promise<void>;
/** 좌측 목록을 다시 읽음 */
changed: () => void;
/** 페이지 머리 단추 — 단가산출 상세에서 숨김 · 흐림 */
page: { reload: HTMLButtonElement; save: HTMLButtonElement };
}
/** 문서 안 모든 `뺀줄` 길이 합(단가산출 · 일위대가 줄 안 덮개까지) */
function removedCount(node: unknown): number {
if (Array.isArray(node)) return node.reduce<number>((n, x) => n + removedCount(x), 0);
if (!node || typeof node !== "object") return 0;
return Object.entries(node as Record<string, unknown>).reduce(
(n, [k, v]) => n + (k === "뺀줄" && Array.isArray(v) ? v.length : removedCount(v)),
0,
);
}
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
export function createMainActions(ctx: MainActionsCtx): {
reset: HTMLButtonElement;
del: HTMLButtonElement;
undo: HTMLButtonElement;
update: (writable: boolean) => void;
/** 고침 있음 — 단가산출 상세의 [되돌림] · [저장] 흐림 */
dim: (dirty: boolean) => void;
} {
const reset = createButton({ label: tx("Reset"), variant: "ghost", iconStart: "rotateCcw" });
const del = createButton({ label: tx("Delete"), variant: "danger-ghost", iconStart: "trash" });
const undo = createButton({ label: tx("Undo"), variant: "ghost", iconStart: "undo" });
undo.dataset.mainUndo = "";
reset.title = tx("ResetTip");
del.title = tx("DeleteTip");
reset.dataset.mainReset = "";
del.dataset.mainDelete = "";
const target = (): { at: Selection; kind: LibraryKind } | null => {
const at = ctx.sel();
const kind = at?.kind;
return at && at.name && (kind === "sangeun" || kind === "precombo") ? { at, kind } : null;
};
reset.addEventListener("click", async () => {
const got = target();
if (!got || !(await ctx.confirmLeave())) return;
const { at, kind } = got;
try {
const look = await resetLibrary(kind, at.layer, at.owner ?? null, {
키: at.name,
미리보기: true,
});
if (!look.걷힘) return void showToast(tx("Nothing"), "success");
const k = removedCount(ctx.loaded());
const message = fill(tx("ResetAsk"), {
n: look.걷힘,
k: k ? fill(tx("Restored"), { k }) : "",
});
if (!(await askConfirm({ message, okLabel: tx("Reset"), danger: true }))) return;
await resetLibrary(kind, at.layer, at.owner ?? null, {
키: at.name,
미리보기: false,
판: ctx.version(),
});
await ctx.open(at); // 다시 그림
ctx.changed();
showToast(tx("Done"), "success");
} catch (error) {
showToast(fill(tx("Failed"), { why: why(error) }), "error");
}
});
del.addEventListener("click", async () => {
const got = target();
if (!got || !(await ctx.confirmLeave())) return;
const { at, kind } = got;
const owner = at.owner ?? null;
try {
const tree = await fetchPrecomboTree(at.layer, owner, kind);
const pick = { 경로: tree.자리?.[at.name] ?? [], 키: at.name };
const ask = fill(tx("DeleteAsk"), { name: at.label ?? at.name });
const ans = await askLibraryDelete(kind, at.layer, owner, [pick], ask, tx("Delete"));
if (!ans.ok) return;
await editPrecomboTree(
at.layer,
{ op: "delete", 대상: [pick], ...(ans.sure ? { 확인: true } : {}) },
tree.판,
owner,
kind,
);
const scope = { layer: at.layer, owner, canWrite: true } as Parameters<typeof sangeunList>[0];
await ctx.open(kind === "sangeun" ? sangeunList(scope) : precomboList(scope)); // 목록으로
ctx.changed();
showToast(tx("Deleted"), "success");
} catch (error) {
showToast(fill(tx("Failed"), { why: why(error) }), "error");
}
});
undo.addEventListener("click", async () => {
const at = ctx.sel();
if (at && (await showConfirmDialog(tx("UndoAsk"), tx("Undo")))) await ctx.open(at);
});
const sk = (): boolean => target()?.kind === "sangeun";
const dim = (dirty: boolean): void => {
undo.disabled = !dirty;
ctx.page.save.disabled = sk() && !dirty;
};
return {
reset,
del,
undo,
update: (writable) => {
const on = writable && !!target();
reset.hidden = del.hidden = !on;
undo.hidden = !on || !sk();
if (sk()) ctx.page.reload.hidden = true;
dim(false);
},
dim,
};
}