/* ============================================================================= * M01_LB_Api.ts * 로직 만들기 판 Api — 고친 표로 예시값 계산(0.3초 · 최신만) · 표를 쓰는 로직(사용처) · 표 읽기 * 서버 = `M01_LB_Calc.py`(`/api/m01/lb/*`) · 모양은 `docs/raw/M01_쉬운단가/로직판_A_정함.md` 「맞출 모양」 * 시험: `resources/tester/test_m01_lb_calc.py` * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { fetchTable, fetchTables } from "../M01_MasterData_Api_Fetch"; import { ApiError, type CalcAnswer, type LogicRow } from "../M01_MasterData_UI_Logic_Api"; import { safeReason } from "../M01_MasterData_UI_Logic_Text"; import { plainError } from "../EasyCalc/M01_EasyCalc_Api"; /** 표 키 → 표 사본(고친 값 포함 · 표 통째) — 서버가 메모리에서만 바꿔 끼움 */ export type LbTables = Record; /** 표를 쓰는 로직 한 줄 — `via` = 표 「용도」 에 적힘 · 로직 글에 표 키가 있음 · 둘 다 */ export interface LbTableUser { key: string; 이름: string; via: "용도" | "글" | "둘"; } /** 표 한 벌 — `file` · `version` 은 표가 든 파일 */ export interface LbTable { file: string; version: string; table: Record & { 줄: Record[] }; } const CALC_WAIT_MS = 300; const TABLE_GROUPS = ["소요량", "계수"]; // 표가 드는 그룹(계수 표가 소요량 파일에 든 것도 있음) async function send(path: string, body?: unknown): Promise { let response: Response; try { response = await fetch(`${API_BASE_URL}/m01/lb${path}`, { method: body === undefined ? "GET" : "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); } catch { throw new ApiError(0, ""); } const data = (await response.json().catch(() => ({}))) as { detail?: unknown } & T; if (!response.ok) throw new ApiError(response.status, data.detail ?? response.statusText); return data; } /** 로직이 받는 입력값만 — 모르는 이름을 보내면 서버가 멈춤 */ function inputsFor(row: LogicRow, inputs: Record): Record { return Object.fromEntries( (row.입력 ?? []).filter((i) => i.이름 in inputs).map((i) => [i.이름, inputs[i.이름]]), ); } /** 계산 멈춤 까닭 — 표 키(`QF000421`)가 섞인 글은 뭉뚱그림 */ function plainStop(reason: string): string { const plain = safeReason(reason); return /[A-Z]{2}\d{6}/.test(plain) ? safeReason("{}") : plain; } let calcSeq = 0; let calcTimer: ReturnType | undefined; /** 0.3초 뒤 고친 표로 서버 계산 · 최신 응답만 `onDone` · 멈춤 · 실패는 `plain` 쉬운 말 */ export function lbCalcLater( row: LogicRow, inputs: Record, tables: LbTables, onDone: (ans: CalcAnswer | null, plain: string | null) => void, ): void { const mine = ++calcSeq; clearTimeout(calcTimer); calcTimer = setTimeout(() => { send("/calc", { row, inputs: inputsFor(row, inputs), tables }) .then((ans) => { if (mine === calcSeq) onDone(ans, ans.ok ? null : plainStop(ans.reason)); }) .catch((e: unknown) => { if (mine === calcSeq) onDone(null, plainError(e)); }); }, CALC_WAIT_MS); } /** 기다림 없이 바로 셈(기대값 시험표처럼 여러 줄을 차례로) — `lbCalcLater` 의 최신만과 안 걸림. * 서버에 못 닿으면 null · 멈춤(ok false)은 답 그대로 · 둘 다 `onPlain` 에 쉬운 말 */ export async function lbCalcNow( row: LogicRow, inputs: Record, tables: LbTables, onPlain?: (plain: string) => void, ): Promise { try { const ans = await send("/calc", { row, inputs: inputsFor(row, inputs), tables }); if (!ans.ok) onPlain?.(plainStop(ans.reason)); return ans; } catch (e: unknown) { onPlain?.(plainError(e)); return null; } } /** 사용처 — 그 표를 쓰는 로직(표 「용도」 + 로직 글 대조) */ export async function tableUsers(key: string): Promise { const got = await send<{ users: LbTableUser[] }>( `/table-users?${new URLSearchParams({ key }).toString()}`, ); return got.users; } /** 표 키 하나로 표 통째 — 든 파일을 찾아(소요량 · 계수) Api_Fetch 의 표 읽기로 */ export async function fetchLbTable(key: string): Promise { for (const group of TABLE_GROUPS) { const page = await fetchTables(group, "", "", key, 1, 5); const head = page.tables.find((t) => t.키 === key); if (head) return { file: head.file, ...(await fetchTable(head.file, key)) }; } throw new ApiError(404, `없는 표 「${key}」`); }