Files
Aislo/B05_Profile/B05_Profile_Api_HaulPlan.ts
T
eomsangdonandClaude Opus 5.5 1458010944 refactor(454 계산 줄 유토 단계): 안 쓰는 masshaul · haul 단계 걷음 (433-3)
- 계산 줄 masshaul(유토곡선 계열) · haul(흙 배분 POST) 단계 지움 — 읽는 화면 없음 · 조작마다 헛 셈 · 헛 POST
- 유토곡선 판 계산 · createHaulPlanPrefetch · postHaulPlan 남김 · 표준단면 도착 때 cross 다시 먹이기 남김
- 탐침 시험: 끝 누가토량은 공용 계산 헬퍼(helper_454_final_cumulative) · 뒤 단계 늦은 응답 = corridor · 묶음 흙 배분 POST 0 단언

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:33:18 +09:00

161 lines
6.9 KiB
TypeScript

/* =============================================================================
* B05_Profile_Api_HaulPlan.ts
* 유토 **배분**(평형선·운반거리·장비)을 서버에서 미리 받아 두는 자리.
*
* 왜 서버인가(2026-09-06 사용자 확정) — 배분 산식은 노하우가 몰린 자리라 브라우저 번들에
* 남기지 않는다. 화면은 누가토량까지만 스스로 내고(`common_util_mass_haul`), 그 결과를
* 여기로 보내 배분을 받아 쥔다. 계산은 여전히 한 벌이다 — 서버가 같은 TS 를 Node 로 돈다.
*
* **조용히 따라오게 한다** — 편집이 멈추면 뒤에서 물어 두므로, 유토곡선 패널을 펼치는
* 순간에는 이미 도착해 있다. 늦게 온 응답은 버린다(최신 요청만 채택).
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import type { HaulPlan } from "@util/common_util_mass_haul_balance";
/** 서버가 돌려주는 배분 한 벌 — **화면이 쓰는 꼴 그대로**라 그리기 코드가 손대지 않는다.
* `import type` 이라 배분 모듈이 번들에 실리지 않는다(빌드에서 지워진다). */
export type HaulPlanPayload = HaulPlan | null;
/** 편집이 멈춘 것으로 볼 시간(ms). 계획고를 연속으로 누르는 동안은 안 보낸다. */
const SETTLE_MS = 400;
/** 배분 계산 대기 상한 — Node 실행 200ms 대라 넉넉히 잡는다. */
const TIMEOUT_MS = 20000;
/** 열쇠별로 받아 둔 배분 — 되돌리기 · 보기 오감처럼 같은 누가토량이 다시 오면 묻지 않음(342-1). */
const KEPT_PLANS = 8;
/** 받은 배분 — `프로젝트:노선|열쇠` → 배분 · 오래된 것부터 버림 · 선반입이 씀. */
const kept = new Map<string, HaulPlanPayload>();
/**
* 입력이 달라졌는지 가리는 표식. 응답이 오면 곡선을 다시 그리고, 그리기가 다시
* `schedule` 을 부르므로 **표식이 없으면 요청이 끝없이 되돌아온다**(2026-09-06 실측).
* 누가토량 결과 전체 — 끝 · 가운데 누계만 보던 옛 표식은 사이 측점이 바뀌어도 같았다(342-1).
*/
function keyOf(result: unknown): string {
const points = (result as { points?: unknown[] })?.points;
if (!Array.isArray(points) || !points.length) return "empty";
return JSON.stringify(points);
}
const keptKey = (projectId: string, route: number, key: string): string =>
`${projectId}:${route}|${key}`;
function keep(at: string, plan: HaulPlanPayload): void {
kept.delete(at);
kept.set(at, plan);
if (kept.size > KEPT_PLANS) kept.delete(kept.keys().next().value as string);
}
/** 서버에 배분을 물음 — 받으면 기억 · 못 받으면(실패 · 끊김) undefined. `signal` 이 끊으면 함께 끊음. */
async function postHaulPlan(
projectId: string,
route: number,
result: unknown,
key: string,
signal?: AbortSignal,
): Promise<HaulPlanPayload | undefined> {
if (signal?.aborted) return undefined;
const controller = new AbortController();
const abort = window.setTimeout(() => controller.abort(), TIMEOUT_MS);
const relay = () => controller.abort();
signal?.addEventListener("abort", relay);
try {
const response = await fetch(
`${API_BASE_URL}/projects/${projectId}/sections/${route}/haul-plan`,
{
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ result }),
signal: controller.signal,
},
);
if (!response.ok) return undefined;
const payload = (await response.json()) as { haul_plan?: HaulPlanPayload };
const plan = payload.haul_plan ?? null;
keep(keptKey(projectId, route, key), plan);
return plan;
} catch {
return undefined; // 배분을 못 받아도 곡선 자체는 그대로 보인다 — 화면을 막지 않는다.
} finally {
window.clearTimeout(abort);
signal?.removeEventListener("abort", relay);
}
}
export interface HaulPlanPrefetch {
/** 새 누가토량 결과가 나왔음을 알린다 — 잠잠해지면 서버에 물어본다. */
schedule: (result: unknown) => void;
/** 지금 쥐고 있는 배분. 아직 못 받았으면 null. */
current: () => HaulPlanPayload;
/** 화면을 떠날 때 예약을 지운다. */
dispose: () => void;
}
/**
* 배분 선반입기를 만든다. `onReady` 는 값이 새로 도착했을 때만 불린다 —
* 부르는 쪽은 그때 곡선을 다시 그리면 된다.
*/
export function createHaulPlanPrefetch(
projectId: string,
routeId: () => number | undefined,
onReady: () => void,
): HaulPlanPrefetch {
let timer = 0;
let sequence = 0;
let plan: HaulPlanPayload = null;
let pending: unknown = null;
/** 마지막으로 보낸 입력의 표식 — 같은 값이면 다시 묻지 않는다. */
let sentKey: string | null = null;
/** 받은 값을 화면에 반영하려고 다시 그리는 중 — 그 그리기가 거는 요청은 무시한다. */
let applying = false;
/** 못 받은 열쇠는 잊는다 — 다음 그리기가 같은 값으로 다시 보냄(342-1). */
function forget(seq: number): void {
if (seq === sequence) sentKey = null;
}
async function send(result: unknown, seq: number, key: string): Promise<void> {
const route = routeId();
if (!route) return forget(seq);
const got = await postHaulPlan(projectId, route, result, key);
if (got === undefined) return forget(seq);
// 늦게 온 응답은 버린다 — 그 사이 사용자가 계획고를 더 만졌을 수 있다.
if (seq !== sequence) return;
plan = got;
// 다시 그리기가 `schedule` 을 되부르므로 그 한 바퀴를 막는다. 표식(`sentKey`)만으로는
// 부족했다 — 반영 뒤 누계가 아주 조금 달라지는 경로가 있어 2~3바퀴가 더 돌았다
// (2026-09-06 보조 창 실측: 편집이 멈춘 뒤 2.8~6.3초에 걸쳐 3건).
applying = true;
try {
onReady();
} finally {
applying = false;
}
}
return {
schedule(result) {
if (applying) return; // 응답을 반영하려고 다시 그리는 중이다.
const key = keyOf(result);
if (key === sentKey) return; // 같은 값 — 응답이 부른 재그리기가 되돌아온 것이다.
sentKey = key;
pending = result;
sequence += 1;
const seq = sequence;
window.clearTimeout(timer);
// 받아 둔 열쇠면 묻지 않고 그대로 — 그리는 쪽은 이 뒤에 `current()` 를 읽는다.
const route = routeId();
const known = route ? kept.get(keptKey(projectId, route, key)) : undefined;
if (known !== undefined) return void (plan = known);
timer = window.setTimeout(() => void send(pending, seq, key), SETTLE_MS);
},
current: () => plan,
dispose() {
window.clearTimeout(timer);
sequence += 1; // 남아 있는 응답을 모두 무효로 만든다.
},
};
}