표식(측점 수 + 누계)만으로는 부족했음 — 응답을 반영해 다시 그린 뒤 누계가 아주 조금 달라지는 경로가 있어 편집이 멈춘 뒤에도 2~3바퀴가 더 돌았음(보조 창 10초 관찰: 2.8~6.3초에 걸쳐 3건). onReady 로 다시 그리는 동안 applying 을 세워 그 그리기가 거는 schedule 을 무시함. 자체검증(공용 브라우저, 접은 채 계획고 6회 편집 후 10초 관찰 3회) — 3/3/1건 -> 2/1/1건. 편집하는 동안은 종전대로 0건(400ms 안정화 유지). typecheck 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
121 lines
5.4 KiB
TypeScript
121 lines
5.4 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;
|
|
|
|
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;
|
|
|
|
/**
|
|
* 입력이 달라졌는지 가리는 짧은 표식. 응답이 오면 곡선을 다시 그리고, 그리기가 다시
|
|
* `schedule` 을 부르므로 **표식이 없으면 요청이 끝없이 되돌아온다**(2026-09-06 실측).
|
|
* 누가토량이 바뀌면 마지막 누계와 측점 수 중 하나는 반드시 달라진다.
|
|
*/
|
|
function keyOf(result: unknown): string {
|
|
const points = (result as { points?: Array<{ cumulative_volume_m3?: number }> })?.points;
|
|
if (!Array.isArray(points) || !points.length) return "empty";
|
|
const last = points[points.length - 1]?.cumulative_volume_m3 ?? 0;
|
|
const mid = points[Math.floor(points.length / 2)]?.cumulative_volume_m3 ?? 0;
|
|
return `${points.length}:${last.toFixed(3)}:${mid.toFixed(3)}`;
|
|
}
|
|
|
|
async function send(result: unknown, seq: number): Promise<void> {
|
|
const route = routeId();
|
|
if (!route) return;
|
|
const controller = new AbortController();
|
|
const abort = window.setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
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;
|
|
const payload = (await response.json()) as { haul_plan?: HaulPlanPayload };
|
|
// 늦게 온 응답은 버린다 — 그 사이 사용자가 계획고를 더 만졌을 수 있다.
|
|
if (seq !== sequence) return;
|
|
plan = payload.haul_plan ?? null;
|
|
// 다시 그리기가 `schedule` 을 되부르므로 그 한 바퀴를 막는다. 표식(`sentKey`)만으로는
|
|
// 부족했다 — 반영 뒤 누계가 아주 조금 달라지는 경로가 있어 2~3바퀴가 더 돌았다
|
|
// (2026-09-06 보조 창 실측: 편집이 멈춘 뒤 2.8~6.3초에 걸쳐 3건).
|
|
applying = true;
|
|
try {
|
|
onReady();
|
|
} finally {
|
|
applying = false;
|
|
}
|
|
} catch {
|
|
// 배분을 못 받아도 곡선 자체는 그대로 보인다 — 화면을 막지 않는다.
|
|
} finally {
|
|
window.clearTimeout(abort);
|
|
}
|
|
}
|
|
|
|
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);
|
|
timer = window.setTimeout(() => void send(pending, seq), SETTLE_MS);
|
|
},
|
|
current: () => plan,
|
|
dispose() {
|
|
window.clearTimeout(timer);
|
|
sequence += 1; // 남아 있는 응답을 모두 무효로 만든다.
|
|
},
|
|
};
|
|
}
|