Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetFormula.ts
T
eomsangdonandClaude Opus 5.5 2e7328d3a5 feat(794-1): M01 관리자 줄 식 시트 — 로직 상세 첫 보기 「시트」 · 식 칸 칠 때마다 검사 · 기존 [저장] 길
- 로직 상세 보기 첫 칸 「시트」(기본) — 이름 | 식 | 값 | 비목 · 근거 · 옵션(시험 값) · 변수 · 계산 · 자원 · 율 · 덧줄 · 단계 · 끝수 · 소계 · 합계 · 상태줄 지금 · 고치기 전
- 식 글 = 이름 꼴(M02 단가산출 시트와 같은 번역 · 새 서버 M01_MasterData_Sheet.py · POST /api/m01/logic/sheet · /logic/sheet/check · 저장 때와 같은 엔진 검사 · 쓰기 없음)
- 식 고침 = 시스템 관리자만 · 되는 식만 줄 사본에 · 캐시 → 기존 [저장](개정 확인 · 서버 검사 422) · [원래로]
- 식 칸 몸 = 공용 A00_Common/sheet_formula_cell.ts — M02 단가산출 시트도 같은 것을 씀(동작 그대로)
- 자체 로직(GX) 편집기 식 날 입력칸 → 읽기만(시트 식 칸으로 갈음)
- 시험: test_794_1_m01_sheet 새로 · test_790_1 식 칸 문구 자리를 공용 파일로 옮김(식 칸 몸이 공용으로 감) · test_717_2b · test_722 LogicLab.ts 줄 상한 627 → 637(시트 이음 10 줄 · 셈 · 고침은 시트 파일로 뺌 · 700 아래)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:45:48 +09:00

235 lines
8.4 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Sangeun_SheetFormula.ts
* 단가산출(SK) 시트 식 칸(PLAN 792 · E4 · _분석_650-H3 2 장 「식 고침」) — 시트(`_Sangeun_Sheet`)가 부름.
* 식 칸 = 이름 꼴 글(`[노임 보통인부] × 무게 / 운반량`) · 칠 때마다 검사 길(`formula-check` · 옛 요청 끊기)
* → 틀리면 빨간 칸 + 까닭 한 줄 · 되면 바꿀 때(Enter · 떠남) 엔진 식을 문서에(`setFormula`) → 서버 셈
* 변수 줄 = 이름 · 식 · 값 · [+ 변수 줄] · 빼기(`addVar` · `editVar` · `dropVar`)
* 빈 칸 · M01 식 그대로 = 식 덮개 풂 · 같은 자리 상수 · 표값 · 품 덮개는 식 덮개와 같이 못 둠 → 걷고 넣음
* 문서 고치기 = `logic/M02_Sangeun_Formula_Model`(792-1 짝) · 금액은 늘 서버 셈 · 자동저장 없음
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
import { createButton } from "@ui/ui_template_button";
import { createCalcSheetRow } from "@ui/ui_template_calc_row";
import { el } from "@ui/ui_template_elements";
import { createInputField } from "@ui/ui_template_input";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createFormulaCell } from "../A00_Common/sheet_formula_cell";
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
import type { EditCtx } from "./M02_MasterTemplete_Sangeun_SheetEdit";
import {
addVar,
dropVar,
editVar,
setFormula,
type FormulaDoc,
type VarLine,
} from "./logic/M02_Sangeun_Formula_Model";
import { num, type SpotInfo } from "./logic/M02_Sangeun_Sheet_Model";
const TEXT = {
Edit: ["식 고침", "Edit formula"],
Close: ["식 닫기", "Close formula"],
Var: ["변수", "Variable"],
VarName: ["변수 이름", "Variable name"],
AddVar: ["+ 변수 줄", "+ Variable"],
DropVar: ["변수 빼기", "Remove variable"],
} as const satisfies Record<string, readonly [string, string]>;
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
export interface CheckAnswer {
되나: boolean;
까닭: string[];
알림: string[];
/** 엔진 식(문서에 넣을 것) */
식: string;
/** 이름 꼴 글 */
글: string;
}
export interface FormulaCtx extends EditCtx {
scope: { layer: Layer; owner: string | null };
/** 식 칸 연 자리(화면 캐시) */
open: Set<string>;
/** 치는 중 글(다시 그려도 안 잃음) — 넣으면 지움 */
drafts: Map<string, string>;
redraw: () => void;
}
const q = (c: FormulaCtx): string =>
c.scope.owner && c.scope.layer !== "system" ? `?owner=${encodeURIComponent(c.scope.owner)}` : "";
/** 검사 길(792-1) — 셈 · 저장 없음 */
export const checkFormula = (
c: FormulaCtx,
spot: string,
text: string,
signal?: AbortSignal,
): Promise<CheckAnswer> =>
call<CheckAnswer>(`/layers/${c.scope.layer}/sangeun/formula-check${q(c)}`, {
method: "POST",
body: JSON.stringify({ 문서: c.doc, 자리: spot, 식: text }),
signal,
});
/** 식 덮개와 같이 못 두는 같은 자리 상수 · 표값 · 품 덮개 걷음(배율은 둠) */
export function clearNums<T extends FormulaDoc>(doc: T, spot: string): T {
const out: FormulaDoc = { ...doc };
for (const name of ["상수덮개", "표값덮개"] as const) {
const all = { ...(doc[name] ?? {}) };
delete all[spot];
if (Object.keys(all).length) out[name] = all;
else delete out[name];
}
const one = doc.덮개?.[spot];
if (one && "qty" in one) {
const rest = { ...one };
delete rest.qty;
const covers = { ...doc.덮개 };
if (Object.keys(rest).length) covers[spot] = rest;
else delete covers[spot];
out.덮개 = covers;
}
return out as T;
}
/** 식 칸 — 칠 때마다 검사(앞 요청 끊음) · 바꿀 때 넣음 · `put(엔진 식 | null)` · 몸 = 공용 `sheet_formula_cell` */
function formulaBox(
c: FormulaCtx,
spot: string,
key: string,
shown: string,
origin: string,
put: (engine: string | null) => void,
): HTMLElement {
return createFormulaCell({
key,
spot,
shown,
origin,
drafts: c.drafts,
check: (text, signal) => checkFormula(c, spot, text, signal),
put,
});
}
/** 줄 식 칸 — 열었거나 식 덮개 있는 자리만 · 못 고치는 자리(로직 · 율 더한줄) = null */
export function rowFormula(
c: FormulaCtx,
spot: string,
info: SpotInfo | undefined,
): HTMLElement | null {
if (!c.edit || !spot || !info || (!c.open.has(spot) && !info.식덮개)) return null;
const shown = info.식글 ?? info.식 ?? "";
const origin = info.원식글 ?? "";
return formulaBox(c, spot, `${spot}|f`, shown, origin, (engine) => {
const doc = engine ? clearNums(c.doc as FormulaDoc, spot) : (c.doc as FormulaDoc);
c.apply(setFormula(doc, spot, engine));
});
}
/** 꼬리 [식 고침] · [식 닫기] — 식 덮개 있으면 늘 열림(닫기 없음) */
export function formulaToggle(
c: FormulaCtx,
spot: string,
info: SpotInfo | undefined,
): HTMLElement[] {
const addOne = /^add:#\d+$/.test(spot); // 더한줄 수량식(율 · 로직 줄은 부르는 쪽이 뺌)
if (!c.edit || !spot || !info || info.식덮개 || (info.식 == null && !addOne)) return [];
const on = c.open.has(spot);
const b = createButton({
label: on ? t("Close") : t("Edit"),
variant: "ghost",
size: 22,
onClick: () => {
if (on) c.open.delete(spot);
else c.open.add(spot);
c.drafts.delete(`${spot}|f`);
c.focus(on ? "" : `${spot}|f`);
c.redraw();
},
});
b.dataset.sheetFxOpen = spot;
return [b];
}
const vars = (c: FormulaCtx): VarLine[] => (c.doc as FormulaDoc).변수줄 ?? [];
/** 자리 뒤 변수 줄들(이어 붙은 변수까지) */
export function varRows(c: FormulaCtx, after: (v: VarLine) => boolean): HTMLElement[] {
return vars(c).flatMap((v) =>
after(v) ? [varRow(c, v), ...varRows(c, (x) => x.자리 === `mid:${v.이름}`)] : [],
);
}
function varRow(c: FormulaCtx, v: VarLine): HTMLElement {
const spot = `mid:${v.이름}`;
const info = c.spots?.자리[spot];
const label = c.edit
? (() => {
const box = createInputField({ value: v.이름, compact: true, size: 24 });
box.input.setAttribute("aria-label", t("VarName"));
box.input.dataset.sheetCell = `var|${v.이름}`;
box.input.addEventListener("change", () => {
const to = box.input.value.trim();
if (!to || to === v.이름 || vars(c).some((x) => x.이름 === to))
return void (box.input.value = v.이름);
c.focus(`var|${to}`);
c.apply(editVar(c.doc as FormulaDoc, v.이름, { 이름: to }));
});
return box.root;
})()
: el("span", { className: "ui-calc-row__label", text: v.이름 });
const middle = c.edit
? formulaBox(c, spot, `${spot}|f`, info?.식글 ?? v.식, "", (engine) =>
c.apply(editVar(c.doc as FormulaDoc, v.이름, { 식: engine ?? "0" })),
)
: el("span", { className: "ui-calc-formula", text: info?.식글 ?? v.식 });
const value = el("span", {
className: "ui-calc-row__out",
text: typeof info?.지금 === "number" ? num(info.지금) : "—",
});
const tail = el("span", {
children: [
createBadge(t("Var"), "accent"),
...(c.edit
? [
createButton({
label: t("DropVar"),
variant: "danger-ghost",
size: 22,
onClick: () => c.apply(dropVar(c.doc as FormulaDoc, v.이름)),
}),
]
: []),
],
});
const row = createCalcSheetRow(label, middle, value, tail);
row.dataset.sheetKind = "변수";
row.dataset.sheetSpot = spot;
row.classList.add("is-changed");
return row;
}
/** [+ 변수 줄] — 맨 위(자리 "") · 이름 = 안 겹치는 「변수n」 */
export function addVarButton(c: FormulaCtx): HTMLElement[] {
if (!c.edit) return [];
const b = createButton({
label: t("AddVar"),
variant: "ghost",
size: 22,
onClick: () => {
const used = new Set([
...vars(c).map((v) => v.이름),
...(c.spots?.이름표 ?? []).map((x) => x.글),
]);
let n = 1;
while (used.has(`변수${n}`)) n++;
c.focus(`var|변수${n}`);
c.apply(addVar(c.doc as FormulaDoc, { 이름: `변수${n}`, 식: "0", 자리: "" }, 0));
},
});
b.dataset.sheetAddVar = "";
return [b];
}