/* ============================================================================= * M01_LB_Entry_Result.ts * 로직 만들기 판 오른쪽 결과 칸의 도구 — 예시값 띠 · 빈틈 검사 · 기대값 시험표(Entry 가 부름) * 빈틈 · 시험표 셈은 Check(sub8) 것 · 시험표 줄은 화면 초안(sessionStorage)만 · 저장 안 함(정함 7) * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el } from "@ui/ui_template_elements"; import type { CalcAnswer, LogicInput } from "../M01_MasterData_UI_Logic_Api"; import { formatNumber } from "../M01_MasterData_UI_Logic_Edit"; import type { LbTables } from "./M01_LB_Api"; import type { LbLogic } from "./M01_LB_Blocks"; import { gapSkipped, gaps, loadExpectRows, pickValues, runExpect, saveExpectRows, type ExpectRow, } from "./M01_LB_Check"; const TEXT = { Examples: ["예시값", "Example values"], Examples_Note: [ "예시값은 계산을 보려는 값 — 저장 안 됨", "Example values are only for trying — not saved", ], Pick: ["선택", "Choose"], Gaps: ["빈틈 검사", "Check for gaps"], Gaps_None: [ "빈틈 없음 — 모든 선택 조합에 맞는 줄이 하나씩 있음", "No gaps — every combination finds one row", ], Gaps_No: ["해당 없음", "No match"], Gaps_Dup: ["중복", "Duplicate"], Gaps_Rows: ["줄 {v}", "rows {v}"], Gaps_Skip: [ "못 따진 표 조회 {n} 개 — 입력값이 숫자라 조합을 셀 수 없음", "{n} lookups not checked — numeric inputs", ], Gaps_More: ["그 밖 {n} 건", "{n} more"], Expect: ["기대값 시험표", "Expected-value table"], Expect_Add: ["지금 예시값으로 한 줄 더하기", "Add a row from the current examples"], Expect_Run: ["모두 돌리기", "Run all"], Expect_Total: ["합계", "Total"], Expect_Ok: ["맞음", "Match"], Expect_Bad: ["다름 — 나온 값 {v}", "Differs — got {v}"], Expect_Del: ["삭제", "Delete"], Expect_None: [ "줄 없음 — 예시값을 정한 뒤 한 줄 더하기", "No rows — set examples, then add a row", ], Expect_Note: [ "시험표는 이 화면에만 남음 — 저장 안 됨", "The table stays on this screen only — not saved", ], } as const; type TextKey = keyof typeof TEXT; const tr = (key: TextKey, vars: Record = {}): string => Object.entries(vars).reduce( (text, [k, v]) => text.replace(`{${k}}`, String(v)), (TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]) as string, ); /** 볼 값 이름 — 합계는 「계」 · 값 결과는 「결과」 */ const totalKey = (ans: CalcAnswer | null): "계" | "결과" => ans?.ok && ans.result !== undefined ? "결과" : "계"; /** 예시값 띠 — 입력값마다 선택 또는 숫자 · 바꾸면 onChange(새 예시값) · 저장 안 함 */ export function drawExamples( host: HTMLElement, inputs: LogicInput[], trial: Record, onChange: (trial: Record) => void, ): void { host.hidden = !inputs.length; if (!inputs.length) return void host.replaceChildren(); const state = { ...trial }; const field = (spec: LogicInput): HTMLElement => { const label = spec.단위 ? `${spec.이름} (${spec.단위})` : spec.이름; let box: HTMLInputElement | HTMLSelectElement; if (spec.고르기?.length) { box = el("select", { className: "ui-select ui-select--sm", children: [ el("option", { attrs: { value: "" }, text: tr("Pick") }), ...spec.고르기.map((v) => el("option", { attrs: { value: String(v) }, text: String(v) })), ], }); box.value = state[spec.이름] === undefined ? "" : String(state[spec.이름]); box.addEventListener("change", () => { const picked = spec.고르기?.find((v) => String(v) === box.value); if (picked === undefined) delete state[spec.이름]; else state[spec.이름] = picked; onChange({ ...state }); }); } else { box = el("input", { className: "ui-input ui-input--sm", attrs: { type: "number", ...(spec.범위 ? { min: String(spec.범위[0]), max: String(spec.범위[1]) } : {}), }, }); box.value = state[spec.이름] == null ? "" : String(state[spec.이름]); box.addEventListener("change", () => { if (box.value === "") delete state[spec.이름]; else state[spec.이름] = Number(box.value); onChange({ ...state }); }); } box.setAttribute("aria-label", spec.이름); return el("label", { className: "m01-lb__example", children: [el("span", { text: label }), box], }); }; host.replaceChildren( el("strong", { text: tr("Examples") }), el("div", { className: "m01-lb__example-list", children: inputs.map(field) }), el("p", { className: "m01-lb__muted", text: tr("Examples_Note") }), ); } const condText = (conds: Record): string => Object.entries(conds) .map(([k, v]) => `${k} ${Array.isArray(v) ? v.join("~") : String(v)}`) .join(" · "); /** 빈틈 검사 · 기대값 시험표 — 화면 초안 열쇠 = 로직 번호(새 로직은 「새」) */ export function renderChecks( host: HTMLElement, s: { lb(): LbLogic; tables(): LbTables; trial(): Record; ans(): CalcAnswer | null; draftKey: string; }, ): void { const gapHost = el("div", { className: "m01-lb__check-out" }); const rowsHost = el("div", { className: "m01-lb__check-out" }); let rows: ExpectRow[] = loadExpectRows(s.draftKey); let results: (boolean | number | null)[] = []; const keep = (): void => saveExpectRows(s.draftKey, rows); const runGaps = (): void => { const found = gaps(s.lb(), s.tables()); const skipped = gapSkipped(s.lb(), s.tables()).length; const lines = found.slice(0, 20).map((g) => el("li", { text: [ g.kind === "없음" ? tr("Gaps_No") : tr("Gaps_Dup"), condText(g.conds), g.kind === "중복" ? tr("Gaps_Rows", { v: g.rows.map((r) => r + 1).join(", ") }) : "", ] .filter(Boolean) .join(" — "), }), ); gapHost.replaceChildren( found.length ? el("ul", { children: lines }) : el("p", { className: "m01-lb__muted", text: tr("Gaps_None") }), ...(found.length > 20 ? [el("p", { text: tr("Gaps_More", { n: found.length - 20 }) })] : []), ...(skipped ? [el("p", { className: "m01-lb__muted", text: tr("Gaps_Skip", { n: skipped }) })] : []), ); }; const drawRows = (): void => { if (!rows.length) { rowsHost.replaceChildren(el("p", { className: "m01-lb__muted", text: tr("Expect_None") })); return; } rowsHost.replaceChildren( ...rows.map((row, i) => { const key = Object.keys(row.expect)[0] ?? "계"; const box = el("input", { className: "ui-input ui-input--sm", attrs: { type: "number", "aria-label": tr("Expect_Total") }, }); box.value = String(row.expect[key] ?? ""); box.addEventListener("change", () => { row.expect = { [key]: Number(box.value) }; results[i] = null; keep(); }); const r = results[i]; return el("div", { className: "m01-lb__expect-row", children: [ el("span", { text: condText(row.inputs) || "—" }), box, el("span", { className: r === true ? "is-ok" : typeof r === "number" ? "is-bad" : "", text: r === true ? tr("Expect_Ok") : typeof r === "number" ? tr("Expect_Bad", { v: formatNumber(r) }) : "", }), createButton({ label: tr("Expect_Del"), variant: "ghost", size: "sm", onClick: () => { rows = rows.filter((_, j) => j !== i); results = results.filter((_, j) => j !== i); keep(); drawRows(); }, }), ], }); }), ); }; const addRow = (): void => { const ans = s.ans(); const key = totalKey(ans); const now = ans?.ok ? pickValues(ans)[key] : undefined; rows = [...rows, { inputs: { ...s.trial() }, expect: { [key]: now ?? 0 } }]; keep(); drawRows(); }; const runRows = async (): Promise => { const got = await runExpect(s.lb(), s.tables(), rows); results = got.map((g, i) => { const key = Object.keys(rows[i].expect)[0] ?? "계"; return g.ok ? true : (g.got[key] ?? null); }); drawRows(); }; host.replaceChildren( el("div", { className: "m01-lb__check", children: [ createButton({ label: tr("Gaps"), variant: "ghost", size: "sm", onClick: runGaps }), gapHost, el("strong", { text: tr("Expect") }), el("div", { className: "m01-lb__check-tools", children: [ createButton({ label: tr("Expect_Add"), variant: "ghost", size: "sm", onClick: addRow, }), createButton({ label: tr("Expect_Run"), variant: "ghost", size: "sm", onClick: () => void runRows(), }), ], }), rowsHost, el("p", { className: "m01-lb__muted", text: tr("Expect_Note") }), ], }), ); drawRows(); }