/* ============================================================================= * M01_MasterData_UI_Logic_Drive.ts * 셈 구동(PLAN 799-2) — 화면 없이 「입력값 → 서버 셈 → 답」을 돌림. 시험 계산 칸(`Logic_Calc`)이 씀 * `driveRun` = 한 번 셈(`/calc` + `/text`) · `autoFill` = 자동값 채움 — 로직 상세 구동은 840-1 에 걷음 * 셈 자리 = 서버(CLAUDE.md 5 장 ③) · 화면 · sessionStorage 없음 · 고른 재료(`CalcPick`) 는 값 객체에 딸림 * ========================================================================== */ import { fetchAuto, runCalc, runText, type CalcAnswer, type CalcLine, type LogicRow, type TextAnswer, } from "./M01_MasterData_UI_Logic_Api"; import { pickBody } from "./M01_MasterData_UI_Logic_CalcPick"; export interface DriveContext { /** 저장된 키 — 새 로직은 null */ savedKey: string | null; /** 저장 전 새 초안이면 비움 — 서버가 임시 자리에 얹어 셈 */ file?: string; row: LogicRow; dirty: () => boolean; /** 로직마다 넣은 값 — 다시 그려도 남음 */ values: Record; onLines?: (lines: CalcLine[] | null) => void; /** 읽는 식 줄(`/text`)도 같이 받고 싶을 때 — 호출이 `onLines` 보다 먼저 */ onText?: (answer: TextAnswer) => void; } /** 칸에 든 값 → 엔진 입력 — 빈 칸은 안 보냄(엔진이 「입력 없음」 으로 멈춤) · 고르기는 원래 값 그대로 */ function inputsOf(row: LogicRow, values: Record): Record { const inputs: Record = {}; for (const spec of row.입력 ?? []) { const raw = (values[spec.이름] ?? "").trim(); if (raw === "") continue; const option = spec.고르기?.find((o) => String(o) === raw); // 글 "35" 로 보내면 「고르기 밖」 inputs[spec.이름] = option !== undefined ? option : Number.isNaN(Number(raw)) ? raw : Number(raw); } return inputs; } const draftBody = (ctx: DriveContext): { row?: LogicRow; file?: string } => ctx.dirty() || ctx.savedKey === null ? { row: ctx.row, file: ctx.file } : {}; /** 한 번 셈 — 답을 `onText` → `onLines` 차례로 알리고 돌려줌(`draft` = 저장 전 고침으로 셈) */ export async function driveRun( ctx: DriveContext, inputs = inputsOf(ctx.row, ctx.values), ): Promise<{ answer: CalcAnswer; draft: boolean }> { const draft = ctx.dirty() || ctx.savedKey === null; const body = { key: ctx.savedKey ?? ctx.row.키, inputs, ...pickBody(ctx.values), ...draftBody(ctx), }; const answer = await runCalc(body); if (ctx.onText) ctx.onText(await runText(body)); ctx.onLines?.(answer.ok ? (answer.lines ?? null) : null); return { answer, draft }; } /** 자동값으로 빈 칸 채움 — 채웠으면 true */ export async function autoFill(ctx: DriveContext): Promise { if (ctx.savedKey === null) return false; const auto = await fetchAuto(ctx.savedKey); const names = (ctx.row.입력 ?? []) .map((spec) => spec.이름) .filter((n) => auto[n] !== undefined && auto[n] !== null && !(ctx.values[n] ?? "").trim()); for (const n of names) ctx.values[n] = String(auto[n]); return names.length > 0; }