Files
Aislo/M01_MasterData/LogicBuilder/M01_LB_Entry_Result.ts
T

274 lines
9.5 KiB
TypeScript

/* =============================================================================
* 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, string | number> = {}): 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<string, unknown>,
onChange: (trial: Record<string, unknown>) => 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, unknown>): 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<string, unknown>;
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<void> => {
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();
}