/* ============================================================================= * A00_Common/sheet_formula_cell.ts * 줄 식 시트 식 칸 한 벌(PLAN 792 · 794-1) — M02 단가산출 시트 · M01 로직 시트가 같이 씀. * 칠 때마다 검사(`check` · 150 ms 쉼 · 앞 요청 끊음) → 틀리면 빨간 칸 + 까닭 · 알림 한 줄 * 바꿀 때(Enter · 떠남) — 빈 칸 · 원래 식 그대로 = `put(null)` · 되면 `put(엔진 식)` · 틀리면 안 넣음 * 치는 중 글 = `drafts`(다시 그려도 안 잃음) · 부품 = 입력 · 글 · 서버 검사 길은 부르는 쪽 몫 * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import { createInputField } from "@ui/ui_template_input"; import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createText } from "@ui/ui_template_text"; const TEXT = { Formula: ["식", "Formula"], Hint: ["예) 무게 / 운반량 × (1 + 할증율 / 100)", "e.g. weight / load × 1.2"], Bad: ["식이 틀려 안 넣음", "Invalid formula — not applied"], Checking: ["검사 중", "Checking…"], } as const satisfies Record; const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0]; /** 검사 답 — 되나 · 까닭 · 알림(없으면 빈) · 엔진 식 */ export interface FormulaVerdict { 되나: boolean; 까닭: string[]; 알림?: string[]; 식: string | null; } export interface FormulaCellOptions { /** 시트 칸 열쇠(초점 되찾기 · 치는 중 글) */ key: string; /** 칸 그릇 이름표 값(`data-sheet-fx`) */ spot: string; shown: string; /** 원래 식 글 — 그대로 두면 `put(null)` */ origin: string; drafts: Map; check: (text: string, signal: AbortSignal) => Promise; put: (engine: string | null) => void; className?: string; } export function createFormulaCell(o: FormulaCellOptions): HTMLElement { const box = createInputField({ value: o.drafts.get(o.key) ?? o.shown, compact: true, size: 24, placeholder: t("Hint"), }); box.input.setAttribute("aria-label", t("Formula")); box.input.dataset.sheetCell = o.key; box.input.spellcheck = false; const note = createText("", "muted", 12); note.dataset.sheetCheck = o.key; let timer = 0; let ctl: AbortController | null = null; const run = (text: string): Promise => { ctl?.abort(); const mine = (ctl = new AbortController()); note.textContent = t("Checking"); return o.check(text, mine.signal).then( (got) => { if (mine !== ctl) return got; box.setError(got.되나 ? undefined : got.까닭.join(" · ")); note.textContent = (got.알림 ?? []).join(" · "); return got; }, (error: unknown) => { if (mine !== ctl || mine.signal.aborted) return null; box.setError(error instanceof Error ? error.message : String(error)); note.textContent = ""; return null; }, ); }; box.input.addEventListener("input", () => { o.drafts.set(o.key, box.input.value); window.clearTimeout(timer); const text = box.input.value.trim(); if (!text) return void (ctl?.abort(), box.setError(undefined), (note.textContent = "")); timer = window.setTimeout(() => void run(text), 150); }); box.input.addEventListener("change", async () => { window.clearTimeout(timer); const text = box.input.value.trim(); if (!text || text === o.origin) { o.drafts.delete(o.key); return o.put(null); } const got = await run(text); if (!got?.되나 || !got.식) return void (note.textContent = t("Bad")); o.drafts.delete(o.key); o.put(got.식); }); return el("div", { className: o.className ?? "m02-sks__fx", attrs: { "data-sheet-fx": o.spot }, children: [box.root, note], }); }