Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
274 lines
9.5 KiB
TypeScript
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();
|
|
}
|