Files
Aislo/M01_MasterData/LogicBuilder/M01_LB_Steps_Cell.ts
T

288 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M01_LB_Steps_Cell.ts
* 로직 만들기 판 문장 보기 공용 — 글 [한, 영] 짝 · 작은 칸(숫자 · 글 · 고르기) · 값이 오는 곳 고르기
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { el } from "@ui/ui_template_elements";
import type { CalcAnswer, CalcLine } from "../M01_MasterData_UI_Logic_Api";
import { formatNumber } from "../M01_MasterData_UI_Logic_Edit";
import type { LbCalc, LbExpr, LbLogic, LbLookup, LbRef } from "./M01_LB_Blocks";
export const TEXT = {
Blocked: ["이 화면에서 읽지 못하는 줄이 있음", "Some lines cannot be read on this screen"],
Cannot: ["이 고침은 지금 할 수 없음", "This change cannot be made now"],
BaseNote: ["표준 로직은 읽기만 — 복사해서 고침", "Standard logic is read-only — copy to edit"],
S1: ["① 입력값", "① Input values"],
S2: ["② 표 조회", "② Table lookup"],
S3: ["③ 계산", "③ Calculation"],
S4: ["④ 항목 연결", "④ Items"],
S5: ["⑤ 결과값", "⑤ Result"],
None: ["없음", "none"],
PickList: ["선택 목록", "choose from"],
Number: ["숫자", "number"],
Range: ["범위", "range"],
Unit: ["단위", "unit"],
Eg: ["예: 3", "e.g. 3"],
NotNumber: ["숫자만 넣음", "Numbers only"],
LkFrom: [" 에서 ", " : "],
LkIs: [" 이 ", " is "],
LkRow: [" 인 줄의 ", " in the row, take "],
LkNum: ["표 조회 ", "Lookup "],
SrcInput: ["입력값 · ", "input · "],
SrcCalc: ["계산 · ", "calc · "],
SrcFixed: ["고정값", "fixed value"],
SrcAny: ["무관(조건에서 뺌)", "any (remove condition)"],
Other: ["다른 일위대가", "Other unit price"],
Hard: ["이 화면에서 못 고치는 값", "Value not editable here"],
Mul: ["×", "×"],
Div: ["÷", "÷"],
Plus: ["+", "+"],
Minus: ["−", "−"],
Mark1: ["에 ", ": add "],
Mark2: [" % 를 더 얹음", " % on top"],
Steps: ["여러 단계로 된 계산 — 읽기만", "Multi-step calculation — read only"],
Cond1: ["위에서 처음 맞는 줄의 값을 씀", "The first matching line from the top is used"],
Subject: ["가", "is"],
Otherwise: ["그 외 →", "Otherwise →"],
Big: ["가운데 큰 값", "the larger"],
Small: ["가운데 작은 값", "the smaller"],
And: ["와", "and"],
RoundA: ["을 소수 ", "round to "],
RoundB: [" 자리까지 ", " decimals, "],
Up: ["올림", "round up"],
Down: ["버림", "round down"],
Half: ["반올림", "round off"],
OpEq: ["이면", "equal to"],
OpNe: ["이 아니면", "not equal to"],
OpGt: ["보다 크면", "greater than"],
OpGe: ["이상이면", "at least"],
OpLt: ["보다 작으면", "less than"],
OpLe: ["이하이면", "at most"],
SlotTable: ["표에서 찾은 값", "Value from table"],
SlotCalc: ["계산한 값", "Calculated value"],
PlaceQty: ["수량", "amount"],
PlaceRate: ["비율", "rate"],
PlaceResult: ["결과값", "result"],
Calc: ["계산 ", "Calc "],
Tag_person: ["노무", "Labor"],
Tag_material: ["재료", "Material"],
Tag_machine: ["장비", "Equipment"],
Tag_logic: ["일위대가", "Unit price"],
Tag_percent: ["비율", "Rate"],
Tag_locked: ["기타", "Other"],
Pre_person: [" 이 ", " works "],
Pre_pick: [" 를 ", " uses "],
Pre_percent: [" 의 ", " of "],
Post_percent: [" % ", " % "],
Repick: ["다른 것으로 다시 고르기", "Choose another"],
Remove: ["지움", "Remove"],
Pending: ["계산 중", "calculating"],
Won: ["원", "KRW"],
Add_person: ["+ 노무", "+ Labor"],
Add_material: ["+ 재료", "+ Material"],
Add_machine: ["+ 장비", "+ Equipment"],
Add_logic: ["+ 일위대가", "+ Unit price"],
Add_percent: ["+ 비율", "+ Rate"],
Base_노무비: ["노무비", "labor cost"],
Base_재료비: ["재료비", "material cost"],
Base_경비: ["경비", "expenses"],
Base_사람줄합: ["노무 줄의 합", "sum of labor lines"],
ResultMoney: ["결과 = 금액", "Result = amount"],
ResultValue: [
"결과 = 값 — 다른 일위대가가 불러 쓸 수 있음",
"Result = value — other unit prices can use it",
],
Total: ["합계", "Total"],
} as const;
export const tx = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex];
export type Ref = LbRef | LbExpr;
export interface Ctx {
lb: LbLogic;
ans: CalcAnswer | null;
ro: boolean;
on: { change(lb: LbLogic): void; tableName?(table: string): string | null };
lines: Map<string, CalcLine>;
/** 바꾸기 — 못 하면 쉬운 말 한 줄 */
commit(make: () => LbLogic): void;
}
/* ------------------------------ 작은 칸 ------------------------------ */
export const span = (cls: string, text: string): HTMLSpanElement =>
el("span", { className: cls, text });
export const chip = (text: string, title?: string): HTMLSpanElement => {
const c = span("m01-lbsteps-chip", text);
if (title) c.title = title;
return c;
};
export const note = (text: string): HTMLSpanElement => span("m01-lbsteps-note", text);
export const sentence = (...parts: (HTMLElement | string)[]): HTMLElement =>
el("div", { className: "m01-lbsteps-sentence", children: parts });
export const parseNum = (text: string): number | null => {
const t = text.replace(/[,\s]/g, "");
return /^[+-]?(\d+\.?\d*|\.\d+)$/.test(t) ? Number(t) : null;
};
export function numBox(ctx: Ctx, value: number | null, commit: (n: number) => void): HTMLElement {
if (ctx.ro) return chip(value === null ? "" : String(value));
const input = el("input", {
className: "ui-input m01-lbsteps-num",
attrs: { type: "text", inputmode: "decimal", placeholder: tx("Eg") },
});
input.value = value === null ? "" : String(value);
input.addEventListener("change", () => {
const n = parseNum(input.value);
input.classList.toggle("is-bad", n === null);
input.title = n === null ? tx("NotNumber") : "";
if (n !== null && n !== value) commit(n);
});
return input;
}
export function textBox(ctx: Ctx, value: string, commit: (v: string) => void): HTMLElement {
if (ctx.ro) return chip(value);
const input = el("input", { className: "ui-input m01-lbsteps-text", attrs: { type: "text" } });
input.value = value;
input.addEventListener("change", () => input.value !== value && commit(input.value.trim()));
return input;
}
export function selectBox(
ctx: Ctx,
options: { value: string; text: string }[],
current: string,
commit: (v: string) => void,
aria: string,
): HTMLElement {
const hit = options.find((o) => o.value === current);
if (ctx.ro) return chip(hit?.text ?? "");
const select = el("select", {
className: "ui-select m01-lbsteps-select",
attrs: { "aria-label": aria },
});
for (const o of options) select.append(el("option", { attrs: { value: o.value }, text: o.text }));
select.value = hit ? current : (options[0]?.value ?? "");
select.addEventListener("change", () => commit(select.value));
return select;
}
export const show = (v: unknown): string => (v === undefined || v === null ? "" : formatNumber(v));
export const valueNote = (v: unknown): HTMLElement | string =>
v === undefined || v === null ? "" : span("m01-lbsteps-value", `= ${show(v)}`);
/* ------------------------------ 값 찾기 ------------------------------ */
export const middleOf = (ctx: Ctx, name: string | null): unknown =>
name !== null && ctx.ans?.ok ? ctx.ans.middle[name] : undefined;
/** 표 조회 값 — 그 값 하나만 이름 붙은 계산이 되어 있을 때만 앎 */
export function lookupValue(ctx: Ctx, lk: LbLookup): unknown {
const calc = ctx.lb.calcs.find(
(c) =>
c.kind === "값" &&
c.name !== null &&
c.parts.length === 1 &&
isRef(c.parts[0], "lookup", lk.id),
);
return middleOf(ctx, calc?.name ?? null);
}
export const isRef = (r: Ref, k: "lookup", id: string): boolean => r.k === k && r.id === id;
export const lookupNo = (ctx: Ctx, id: string): number =>
ctx.lb.lookups.findIndex((l) => l.id === id) + 1;
/** 로직 이름 — 목록을 한 번 받아 채움(키 → 이름) · 못 받으면 「다른 일위대가」 */
export const logicNames = new Map<string, string>();
export function refText(ctx: Ctx, r: Ref): string {
switch (r.k) {
case "input":
return r.name;
case "fixed":
return String(r.value);
case "calc":
return r.name;
case "lookup": {
const lk = ctx.lb.lookups.find((l) => l.id === r.id);
return `${tx("LkNum")}${lookupNo(ctx, r.id)}${lk ? ` · ${lk.col}` : ""}`;
}
case "logic":
return `${logicNames.get(r.key) ?? tx("Other")}${r.field ? ` · ${r.field}` : ""}`;
case "cost":
return tx(`Base_${r.name}`);
default:
return tx("Hard");
}
}
export function refValue(ctx: Ctx, r: Ref): unknown {
if (r.k === "calc") return middleOf(ctx, r.name);
if (r.k === "fixed") return r.value;
if (r.k === "lookup") {
const lk = ctx.lb.lookups.find((l) => l.id === r.id);
return lk ? lookupValue(ctx, lk) : undefined;
}
return undefined;
}
/** 값이 오는 곳 고르기 — 입력값 · 표 조회 · 다른 계산 · 고정값 */
export function refBox(
ctx: Ctx,
ref: Ref,
self: LbCalc | null,
pick: (r: LbRef) => void,
): HTMLElement {
const label = refText(ctx, ref);
const editable = ref.k === "input" || ref.k === "calc" || ref.k === "lookup" || ref.k === "fixed";
if (ctx.ro || !editable) {
return el("span", {
className: "m01-lbsteps-ref",
children: [chip(label), valueNote(refValue(ctx, ref))],
});
}
const options = [
...ctx.lb.inputs.map((i) => ({ value: `i:${i.이름}`, text: `${tx("SrcInput")}${i.이름}` })),
...ctx.lb.lookups.map((l) => ({
value: `l:${l.id}`,
text: refText(ctx, { k: "lookup", id: l.id }),
})),
...ctx.lb.calcs
.filter((c) => c.name !== null && c !== self)
.map((c) => ({ value: `c:${c.name}`, text: `${tx("SrcCalc")}${c.name}` })),
{ value: "f", text: tx("SrcFixed") },
];
const current =
ref.k === "input"
? `i:${ref.name}`
: ref.k === "calc"
? `c:${ref.name}`
: ref.k === "lookup"
? `l:${ref.id}`
: "f";
const toRef = (v: string): LbRef =>
v === "f"
? { k: "fixed", value: 0 }
: v.startsWith("i:")
? { k: "input", name: v.slice(2) }
: v.startsWith("l:")
? { k: "lookup", id: v.slice(2) }
: { k: "calc", name: v.slice(2) };
const parts: (HTMLElement | string)[] = [
selectBox(ctx, options, current, (v) => pick(toRef(v)), tx("S3")),
];
if (ref.k === "fixed") {
parts.push(
numBox(ctx, typeof ref.value === "number" ? ref.value : null, (n) =>
pick({ k: "fixed", value: n }),
),
);
} else parts.push(valueNote(refValue(ctx, ref)));
return el("span", { className: "m01-lbsteps-ref", children: parts });
}