- 로직 상세 보기 첫 칸 「시트」(기본) — 이름 | 식 | 값 | 비목 · 근거 · 옵션(시험 값) · 변수 · 계산 · 자원 · 율 · 덧줄 · 단계 · 끝수 · 소계 · 합계 · 상태줄 지금 · 고치기 전 - 식 글 = 이름 꼴(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>
235 lines
8.4 KiB
TypeScript
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];
|
|
}
|