Files
Aislo/B09_Estimation/B09_Estimation_UI_Live.ts
T
eomsangdonandClaude Opus 5.5 59a694a7ed feat(B09): 단가연결 즉시 금액(뒤에서 서버 셈) · [저장] 단추 · 옮길 때 물음(PLAN 702-3)
- 새 산근(로직 연결 · 입력값) = 연결 즉시 뒤에서 POST …/estimation/calc(화면 캐시로 셈 ·
  저장 안 함) · 차례 번호 · 옛 요청 끊기 · 오는 동안 그 줄 금액 칸만 「셈 중」 · 잠금 없음
  (새 `B09_Estimation_UI_Live.ts`) · 기준 = payload.user(서버가 그 셈에 쓴 값)
- 조작 중 셈 = 마지막 GET · [저장] 의 B08 원천 다시 씀(120 s · 0.6 → 0.15 s) · [저장] · GET 은 늘 새로 읽음
- [저장] 단추 = 왼쪽 아래 [초기화] 옆(B08 과 같은 부품 · 꼴) · 고침 없으면 알림만
- 단계 이동 · [>] = 저장 안 한 고침 있으면 공용 떠남 물음(취소 · 폐기 · 저장) — 옛 자동 저장 없앰
- 서고 사본 payload 의 user 받음(서버 기준) — 화면 캐시와 다른 로직 줄은 뒤에서 다시 셈
- 낡은 시험 고침: 481-2 · 298-2(자동 저장 → 물음 · [저장] 단추) · 184-3 · 304(단추 줄) ·
  204-2(user 받음) · 200-2(701-3 띠 · 702-1 펼침 뒤 셸 꼴)
- 확인 = test_702_3 3 · B09 관련 312 통과 · typecheck 0 · 검증_sub5 T6 11 누름 · 연결 뒤 0.32~0.49 s 금액

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:11:44 +09:00

107 lines
4.3 KiB
TypeScript

/* =============================================================================
* 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<string, string> {
const out = new Map<string, string>();
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<string> {
const known = logicSig(payload.user);
const out = new Set<string>();
for (const [key, sig] of logicSig(user)) if (known.get(key) !== sig) out.add(key);
return out;
}
/** 셈 입력 — 기다리는 줄은 옛 산근 단가를 안 씀 */
export function withoutWaiting(payload: Row, wait: Set<string>): 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<string>): 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<string>, 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);
},
);
},
};
}