/* ============================================================================= * B09_Estimation_UI_Live.ts * B09 조작 중 서버 셈(PLAN 702-3) — 새 산근(단가연결 · 로직 입력값)은 서버 M01 엔진 몫 → * 바뀌면 그 자리에서 뒤로 POST …/estimation/calc {user}(저장 안 함 · GET 꼴 답) · * 차례 번호 · 옛 요청 끊기(`design_pipeline` 꼴) · 오는 동안 그 줄 금액 칸만 「셈 중」 · 화면 잠금 없음 * * 기준 = payload.user(서버가 그 셈에 쓴 사용자 값) — 로직 연결 지문이 다른 줄 = 기다림 · * 기다리는 줄은 옛 산근 값을 안 씀(sg_hand 에서 뺌) · 못 받으면 알림 뒤 빈칸(같은 지문 다시 안 보냄) * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { showToast } from "@ui/ui_template_elements"; import type { Row } from "./B09_Estimation_Calc_Base"; export const WAITING = "셈 중"; /** 줄 키 → 로직 연결 지문(코드 · 입력값 · 켜쌓기율 · 고름) */ function logicSig(user: Row | null | undefined): Map { const out = new Map(); for (const r of user?.bill?.rows || []) { const ref = r.ref; if (ref?.tab === "logic") out.set( r.key, JSON.stringify([ref.code, ref.given ?? {}, ref.rate ?? null, ref.picks ?? null]), ); } return out; } /** 서버 셈을 기다리는 줄 — 로직 연결이 서버 기준(payload.user)과 다름 */ export function waitingKeys(payload: Row, user: Row): Set { const known = logicSig(payload.user); const out = new Set(); for (const [key, sig] of logicSig(user)) if (known.get(key) !== sig) out.add(key); return out; } /** 셈 입력 — 기다리는 줄은 옛 산근 단가를 안 씀 */ export function withoutWaiting(payload: Row, wait: Set): Row { if (!wait.size) return payload; const hand = { ...(payload.sheets?.sg_hand || {}) }; for (const key of wait) delete hand[key]; return { ...payload, sheets: { ...payload.sheets, sg_hand: hand } }; } /** 기다리는 줄 금액 칸 = 「셈 중」(셈이 끝난 뒤 · 계에는 안 듦) */ export function markWaiting(sheets: Row, wait: Set): void { for (const r of sheets.bill?.rows || []) if (wait.has(r.key) && r.sum == null) r.sum = WAITING; } /** settle(next, sent) — next = 서버 답(못 받음 = null) · sent = 보낸 사용자 값 */ export function createLiveCalc( projectId: string, settle: (next: Row | null, sent: Row) => void, ): { busy: () => boolean; kick: (wait: Set, user: Row) => void } { let seq = 0; let ctrl: AbortController | null = null; let sentSig = ""; let failedSig = ""; return { busy: () => ctrl !== null, /** 기다리는 줄 있으면 서버 셈(같은 것이 가는 중 · 못 받은 것이면 그대로) · 없으면 가던 것 끊음 */ kick(wait, user) { const sig = JSON.stringify([...logicSig(user)]); if (wait.size && (sig === failedSig || (ctrl && sig === sentSig))) return; ctrl?.abort(); ctrl = null; seq += 1; if (!wait.size) return; const mine = seq; const me = new AbortController(); ctrl = me; sentSig = sig; fetch(`${API_BASE_URL}/projects/${projectId}/estimation/calc`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user }), signal: me.signal, }) .then(async (response) => { const body = (await response.json().catch(() => null)) as Row | null; if (!response.ok || !body) { const detail = body?.detail; throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`); } return body; }) .then( (next) => { if (mine !== seq) return; ctrl = null; settle(next, user); }, (error: unknown) => { if (mine !== seq || me.signal.aborted) return; ctrl = null; failedSig = sig; showToast( `새 단가산출 단가를 못 받음 — [저장] 때 다시 셈 · ${String(error)}`, "warning", ); settle(null, user); }, ); }, }; }