Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
263 lines
8.0 KiB
TypeScript
263 lines
8.0 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Logic_Wizard_Atom.ts
|
|
* 값 하나를 고르는 칸 — 직접 값 / 설계 입력 / 표에서 찾기 / 다른 로직 부르기
|
|
* 수량 · 조건 나누기의 각 갈래가 같이 씀 · 고른 것은 넘겨받은 Atom 에 바로 씀(식은 Model 이 조립)
|
|
* ========================================================================== */
|
|
|
|
import { createInputField, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
|
import {
|
|
fetchTableInfo,
|
|
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<string, unknown>)[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<Atom["kind"], string> = {
|
|
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<Atom, { kind: "value" }>, onChange: () => void): HTMLElement {
|
|
return textBox(atom.value, tw("Qty_Number_Ph"), (v) => {
|
|
atom.value = v;
|
|
onChange();
|
|
});
|
|
}
|
|
|
|
function inputBox(atom: Extract<Atom, { kind: "input" }>, onChange: () => void): HTMLElement {
|
|
return textBox(atom.name, tw("Qty_Name"), (v) => {
|
|
atom.name = v.trim();
|
|
onChange();
|
|
});
|
|
}
|
|
|
|
function logicBox(atom: Extract<Atom, { kind: "logic" }>, 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<Atom, { kind: "table" }>,
|
|
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 fetchTableInfo(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<Atom, { kind: "table" }>,
|
|
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],
|
|
});
|
|
}
|