/* ============================================================================= * M01_MasterData_UI_Logic_Wizard_Atom.ts * 값 하나를 고르는 칸 — 직접 값 / 설계 입력 / 표에서 찾기 / 다른 로직 부르기 * 수량 · 조건 나누기의 각 갈래가 같이 씀 · 고른 것은 넘겨받은 Atom 에 바로 씀(식은 Model 이 조립) * ========================================================================== */ import { createInputField, createSelectField, el, showToast } from "@ui/ui_template_elements"; import { fetchTableFull, searchLogics, type LogicBrief, type TableBrief, } from "./M01_MasterData_UI_Logic_Wizard_Api"; import { condChoices, tableInfo, type Atom } from "./M01_MasterData_UI_Logic_Wizard_Model"; import { tw } from "./M01_MasterData_UI_Logic_Wizard_Text"; type Option = { value: string; text: string }; export const select = ( options: Option[], value: string, onChange: (v: string) => void, label?: string, ): HTMLElement => createSelectField({ options, value, onChange, compact: true, label }).root; export const textBox = ( value: string, placeholder: string, onInput: (v: string) => void, label?: string, ): HTMLElement => createInputField({ value, placeholder, onInput, label, type: "text" }).root; /** 원자를 통째로 바꿔 씀(다른 갈래로 옮길 때) */ function reset(atom: Atom, next: Atom): void { for (const k of Object.keys(atom)) delete (atom as unknown as Record)[k]; Object.assign(atom, next); } const fresh = (kind: Atom["kind"]): Atom => kind === "value" ? { kind, value: "" } : kind === "input" ? { kind, name: "" } : kind === "logic" ? { kind, key: "", args: "" } : { kind, table: "", col: "", cond: {} }; /** * `tables` = 원문 절 단계에서 고른 표 · `onChange` = 고른 것이 바뀔 때마다 * `kinds` = 이 자리에서 고를 수 있는 갈래(기본 넷 다) */ export function atomEditor( atom: Atom, tables: TableBrief[], onChange: () => void, kinds: Atom["kind"][] = ["value", "table", "input", "logic"], ): HTMLElement { const host = el("div", { className: "m01w__atom" }); const body = el("div", { className: "m01w__atom-body" }); const names: Record = { value: tw("Qty_Value"), table: tw("Qty_Table"), input: tw("Qty_Input"), logic: tw("Qty_Logic"), }; const draw = (): void => { body.replaceChildren(); if (atom.kind === "value") body.append(valueBox(atom, onChange)); else if (atom.kind === "input") body.append(inputBox(atom, onChange)); else if (atom.kind === "logic") body.append(logicBox(atom, onChange)); else body.append(tableBox(atom, tables, onChange)); }; host.append( select( kinds.map((k) => ({ value: k, text: names[k] })), atom.kind, (v) => { reset(atom, fresh(v as Atom["kind"])); draw(); onChange(); }, ), body, ); draw(); return host; } function valueBox(atom: Extract, onChange: () => void): HTMLElement { return textBox(atom.value, tw("Qty_Number_Ph"), (v) => { atom.value = v; onChange(); }); } function inputBox(atom: Extract, onChange: () => void): HTMLElement { return textBox(atom.name, tw("Qty_Name"), (v) => { atom.name = v.trim(); onChange(); }); } function logicBox(atom: Extract, onChange: () => void): HTMLElement { const list = el("div", { className: "m01w__found" }); const key = createInputField({ value: atom.key, placeholder: tw("Qty_LogicKey"), type: "text" }); key.input.addEventListener("input", () => { atom.key = key.input.value.trim(); onChange(); }); const find = createInputField({ placeholder: tw("Line_Logic_Find"), type: "search" }); let timer = 0; find.input.addEventListener("input", () => { window.clearTimeout(timer); timer = window.setTimeout(() => { const q = find.input.value.trim(); if (!q) return list.replaceChildren(); void searchLogics(q) .then((r) => showLogics(r.logics.slice(0, 8))) .catch((e: unknown) => showToast(e instanceof Error ? e.message : tw("Failed"), "error")); }, 250); }); const showLogics = (found: LogicBrief[]): void => { list.replaceChildren( ...found.map((l) => { const row = el("button", { className: "m01w__row", attrs: { type: "button" }, text: `${l.원문번호} ${l.이름} · ${l.결과단위} · ${l.키}`, }); row.addEventListener("click", () => { atom.key = l.키; key.input.value = l.키; list.replaceChildren(); onChange(); }); return row; }), ); }; const args = createInputField({ value: atom.args, placeholder: tw("Qty_LogicArgs"), type: "text", }); args.input.addEventListener("input", () => { atom.args = args.input.value; onChange(); }); return el("div", { className: "m01w__stack", children: [find.root, list, key.root, args.root] }); } function tableBox( atom: Extract, tables: TableBrief[], onChange: () => void, ): HTMLElement { const host = el("div", { className: "m01w__stack" }); const detail = el("div", { className: "m01w__stack" }); const drawDetail = (): void => { const info = tableInfo.get(atom.table); detail.replaceChildren(); if (!info) return; detail.append( select( [ { value: "", text: "—" }, ...Object.keys(info.값칸 ?? {}).map((c) => ({ value: c, text: c })), ], atom.col, (v) => { atom.col = v; onChange(); }, tw("Qty_Col"), ), ); for (const cond of Object.keys(info.조건 ?? {})) detail.append(condRow(atom, cond, onChange)); }; const load = (key: string): void => { const brief = tables.find((t) => t.키 === key); if (!brief) return (drawDetail(), onChange()); void fetchTableFull(brief.file, key) .then((full) => { tableInfo.set(key, full); for (const c of Object.keys(full.조건 ?? {})) atom.cond[c] ??= { ask: true }; drawDetail(); onChange(); }) .catch((e: unknown) => showToast(e instanceof Error ? e.message : tw("Failed"), "error")); }; const choose = (key: string): void => { atom.table = key; atom.col = ""; atom.cond = {}; load(key); }; host.append( select( [ { value: "", text: "—" }, ...tables.map((t) => ({ value: t.키, text: `${t.이름} · ${t.키}` })), ], atom.table, choose, tw("Qty_Table_Pick"), ), detail, ); if (atom.table && !tableInfo.has(atom.table)) load(atom.table); else drawDetail(); return host; } /** 표의 조건 칸 하나 — 설계자에게 물음 / 붙박이 값 */ function condRow( atom: Extract, cond: string, onChange: () => void, ): HTMLElement { atom.cond[cond] ??= { ask: true }; const choices = condChoices(atom.table, cond); const value = el("div", { className: "m01w__inline" }); const set = (v: string): void => { atom.cond[cond] = { ask: false, value: v }; onChange(); }; const drawValue = (): void => { value.replaceChildren(); const bind = atom.cond[cond]; if (bind.ask) return; if (choices) { const now = bind.value || String(choices[0]); atom.cond[cond] = { ask: false, value: now }; value.append( select( choices.map((c) => ({ value: String(c), text: String(c) })), now, set, ), ); } else value.append(textBox(bind.value, tw("Qty_Number_Ph"), set)); }; const mode = select( [ { value: "ask", text: tw("Qty_Ask") }, { value: "fixed", text: tw("Qty_Fixed") }, ], atom.cond[cond].ask ? "ask" : "fixed", (v) => { atom.cond[cond] = v === "ask" ? { ask: true } : { ask: false, value: "" }; drawValue(); onChange(); }, ); drawValue(); return el("div", { className: "m01w__inline", children: [el("strong", { text: cond }), mode, value], }); }