/* ============================================================================= * M01_SL_LogicTools.ts * 시트 로직 — 「다른 로직」 상자 손잡이. 기존 로직을 찾아 고르면 상자 시트를 그 로직의 틀로 채움 * 로직의 입력값마다 입력 알약(「넘길 값」 줄 · 예시값) · 결과는 바깥 값 출력 알약 * 돈 로직 = 노무비 · 재료비 · 경비 · 계 / 돈 아닌 로직(작업량 등) = 결과 * 틀 모양은 Import 의 다른 로직 상자와 한 벌(`outsideOf` 가 「넘길 값」 칸과 알약 이름으로 물음을 만듦) * — 상자 `source` 는 안 씀: 한 상자에 비목별 물음이 여럿이라 알약 이름이 비목을 정함. 상자 `ref` = 로직 번호. * 모양은 docs/raw/M01_쉬운단가/시트로직_정함.md 3 · 4. 이 폴더째 지워도 됨. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, createSelectField, el } from "@ui/ui_template_elements"; import type { Cell } from "../../A00_Common/spreadsheet/spreadsheet_types"; import type { LogicInput, LogicRow } from "../M01_MasterData_UI_Logic_Api"; import { listEasy, openEasy, type EasyListItem } from "../EasyCalc/M01_EasyCalc_Api"; import { slPlain } from "./M01_SL_Api"; import { disconnect, renameNode, type SheetLogic, type SlNode, type SlPill } from "./M01_SL_Model"; import "./M01_SL_LogicTools.css"; const TEXT = { Title: ["다른 로직", "Other logic"], Search: ["로직 이름 찾기", "Find a logic by name"], Find: ["찾기", "Find"], None: ["아직 고르지 않았어요", "Nothing chosen yet"], Chosen: ["고른 로직", "Chosen logic"], NoHit: ["찾은 로직이 없어요", "No logic found"], More: ["더 있어요 — 이름을 더 적어 보세요", "More exist — type more of the name"], Loading: ["가져오는 중", "Loading"], Inputs: ["넘길 값", "Passed values"], NoInputs: ["넘길 값이 없는 로직이에요", "This logic takes no values"], Linked: ["다른 상자에서 이어진 값", "Value from another box"], Pick: ["고르세요", "Choose"], Bad: ["숫자를 넣어 주세요", "Enter a number"], Hint: [ "결과는 바깥 값 — 열 때 · 저장 때 넣어요", "Result is an outside value — filled on open and save", ], Result: ["결과", "Result"], Labor: ["노무비", "Labor cost"], Material: ["재료비", "Material cost"], Expense: ["경비", "Expense"], Sum: ["계", "Total"], Name: ["이름", "Name"], Value: ["값", "Value"], Note: ["설명", "Note"], Pass: ["넘길 값", "Passed value"], Outside: ["바깥 값 — 열 때 · 저장 때 넣음", "Outside value — filled on open and save"], } as const; type TextKey = keyof typeof TEXT; const tt = (key: TextKey): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0]; const LIMIT = 30; /** 로직 결과가 돈인가 */ const isMoney = (unit: string): boolean => unit.startsWith("원"); /** 상자 시트를 다른 로직 틀로 채움 · `trial` = 입력값 예시(열 때 받은 자동값) */ export function applyLogic( sl: SheetLogic, nodeId: string, row: LogicRow, trial: Record = {}, ): SheetLogic { const node = sl.nodes.find((n) => n.id === nodeId); if (!node) return sl; if (node.ref === row.키 && node.pillsIn.length) return sl; // 같은 로직을 또 고름 — 그대로 // 다른 로직을 고르면 옛 선은 끊음(받는 칸 수식도 함께 정리) let work = sl; for (const e of sl.edges) if (e.from.node === nodeId || e.to.node === nodeId) work = disconnect(work, e.id); for (const name of [row.이름, `${row.이름} 2`, `${row.이름} 3`]) { const got = renameNode(work, nodeId, name); if (!got.why) { work = got.sl; break; } } const cells: Record = { A1: { 값: tt("Name") }, B1: { 값: tt("Value") }, C1: { 값: tt("Note") }, }; const pillsIn: SlPill[] = []; const pillsOut: SlPill[] = []; let r = 2; for (const spec of row.입력 ?? []) { const at = `B${r}`; cells[`A${r}`] = { 값: spec.이름 }; const v = trial[spec.이름]; if (typeof v === "number" || typeof v === "string") cells[at] = { 값: v }; cells[`C${r}`] = { 값: tt("Pass") }; pillsIn.push({ name: spec.이름, cell: at, ...(spec.단위 ? { unit: spec.단위 } : {}) }); r++; } const unit = row.결과단위 ?? ""; const money = isMoney(unit); const outs = money ? [tt("Labor"), tt("Material"), tt("Expense"), tt("Sum")] : [tt("Result")]; for (const name of outs) { const at = `B${r}`; cells[`A${r}`] = { 값: name }; cells[`C${r}`] = { 값: tt("Outside") }; pillsIn.push({ name, cell: at, outside: true }); pillsOut.push({ name, cell: at, unit: money ? "원" : unit }); r++; } const 시트 = work.workbook.시트.map((s) => (s.id === node.sheetId ? { ...s, 칸: cells } : s)); return { ...work, workbook: { ...work.workbook, 시트 }, nodes: work.nodes.map((n) => { if (n.id !== nodeId) return n; const next: SlNode = { ...n, ref: row.키, pillsIn, pillsOut, stage: 1 }; delete next.source; return next; }), }; } /** 그 로직의 입력 설명(고르기 · 범위) — 한 번 받은 것은 다시 안 받음 */ const rows = new Map(); /** 다른 로직 상자 손잡이 — 찾기 → 고르기 → 넘길 값 칸 */ export function renderLogicTools( host: HTMLElement, sl: SheetLogic, nodeId: string, on: { change(sl: SheetLogic): void }, ): void { const node = sl.nodes.find((n) => n.id === nodeId); const msg = el("p", { className: "m01-sllogic__msg" }); if (!node) { host.replaceChildren(); return; } const sheet = sl.workbook.시트.find((s) => s.id === node.sheetId); const results = el("ul", { className: "m01-sllogic__list" }); const fieldsHost = el("div", { className: "m01-sllogic__fields" }); const box = el("input", { className: "m01-sllogic__input", attrs: { type: "text", placeholder: tt("Search"), "aria-label": tt("Search") }, }); const emit = (make: () => SheetLogic): void => { msg.textContent = ""; try { on.change(make()); } catch (e) { msg.textContent = slPlain(e); } }; const choose = async (item: EasyListItem): Promise => { msg.textContent = tt("Loading"); try { const opened = await openEasy(item.key); rows.set(item.key, opened.row); msg.textContent = ""; on.change(applyLogic(sl, node.id, opened.row, opened.trial)); } catch (e) { msg.textContent = slPlain(e); } }; const search = async (): Promise => { msg.textContent = tt("Loading"); try { const found = await listEasy(box.value.trim()); msg.textContent = ""; results.replaceChildren( ...(found.length ? found.slice(0, LIMIT).map((it) => { const b = createButton({ label: `${it.name} · ${it.resultUnit}${it.where ? ` · ${it.where}` : ""}`, variant: "ghost", size: "sm", onClick: () => void choose(it), }); return el("li", { children: [b] }); }) : [el("li", { className: "m01-sllogic__muted", text: tt("NoHit") })]), ...(found.length > LIMIT ? [el("li", { className: "m01-sllogic__muted", text: tt("More") })] : []), ); } catch (e) { msg.textContent = slPlain(e); } }; box.addEventListener("keydown", (ev) => { if (ev.key === "Enter") void search(); }); /** 넘길 값 칸 — 고르기가 있으면 고르기 · 아니면 숫자 · 다른 상자에서 이어진 칸은 읽기만 */ const fieldFor = (pill: SlPill, spec: LogicInput | undefined): HTMLElement => { const cur = sheet?.칸[pill.cell]; const linked = cur?.식 !== undefined; const put = (v: number | string): void => emit(() => ({ ...sl, workbook: { ...sl.workbook, 시트: sl.workbook.시트.map((s) => s.id === node.sheetId ? { ...s, 칸: { ...s.칸, [pill.cell]: { 값: v } } } : s, ), }, })); const label = el("span", { text: pill.unit ? `${pill.name} (${pill.unit})` : pill.name }); let control: HTMLElement; if (spec?.고르기?.length && !linked) { control = createSelectField({ options: [ { value: "", text: tt("Pick") }, ...spec.고르기.map((o) => ({ value: String(o), text: String(o) })), ], value: cur?.값 === undefined ? "" : String(cur.값), compact: true, ariaLabel: pill.name, onChange: (v) => { const hit = spec.고르기?.find((o) => String(o) === v); if (hit !== undefined) put(hit); }, }).root; } else { const input = el("input", { className: "m01-sllogic__input", attrs: { type: "text", inputmode: "decimal", "aria-label": pill.name }, }); if (spec?.범위) input.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`; input.value = linked || cur?.값 === undefined ? "" : String(cur.값); input.readOnly = linked; input.addEventListener("change", () => { const raw = input.value.trim(); const n = Number(raw); if (raw === "") return; if (Number.isNaN(n)) msg.textContent = tt("Bad"); else put(n); }); control = input; } return el("label", { className: "m01-sllogic__field", children: [ label, control, ...(linked ? [el("small", { className: "m01-sllogic__muted", text: tt("Linked") })] : []), ], }); }; const drawFields = (row: LogicRow | undefined): void => { const passed = node.pillsIn.filter((p) => !p.outside); fieldsHost.replaceChildren( el("strong", { text: tt("Inputs") }), ...(passed.length ? passed.map((p) => fieldFor( p, row?.입력?.find((i) => i.이름 === p.name), ), ) : [el("p", { className: "m01-sllogic__muted", text: tt("NoInputs") })]), el("p", { className: "m01-sllogic__muted", text: tt("Hint") }), ); }; host.replaceChildren( el("strong", { className: "m01-sllogic__title", text: `${tt("Title")} · ${node.name}` }), el("p", { className: "m01-sllogic__chosen", text: node.ref ? `${tt("Chosen")}: ${node.name}` : tt("None"), }), el("div", { className: "m01-sllogic__search", children: [ box, createButton({ label: tt("Find"), size: "sm", onClick: () => void search() }), ], }), results, ...(node.ref ? [fieldsHost] : []), msg, ); if (node.ref) { const known = rows.get(node.ref); drawFields(known); if (!known) { void openEasy(node.ref).then( (o) => { rows.set(o.row.키, o.row); drawFields(o.row); }, () => undefined, ); } } }