/* ============================================================================= * 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; /** 바꾸기 — 못 하면 쉬운 말 한 줄 */ 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(); 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 }); }