fix(B05): 유토 배분 선반입이 접힌 상태에서 안 돌던 것 · 응답이 요청을 다시 부르던 되먹임

- schedule 호출이 draw 의 접힘 가드 뒤에 있어 패널을 접어 두면 한 번도 안 불렸음.
  그래서 「편집이 멈추면 조용히 받아 둔다」가 성립하지 않고 펼친 뒤에야 첫 요청이 나갔음
  (보조 창 실측: 편집 6회에 요청 0건). 접힘 여부와 무관하게 지나는 emitSummary 로 옮김 —
  거기서 이미 고른 결과를 쓰므로 계산 비용 0.
- 응답이 오면 곡선을 다시 그리고, 그리기가 다시 schedule 을 불러 요청이 끝없이
  되돌아왔음. 입력 표식(측점 수 + 마지막·중간 누계)을 두어 같은 값이면 안 보냄.

자체검증(공용 브라우저, 패널 접은 채 계획고 6회 편집) — 요청 1건, 그 뒤 4초 동안
추가 0건. 고치기 전에는 편집 중 0건 + 펼친 뒤 1건 + 되먹임 1건이었음.
시험 400 통과·17 건너뜀, typecheck 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 23:13:05 +09:00
co-authored by Claude Opus 5
parent 4df5d0a7ea
commit c8468e789d
2 changed files with 23 additions and 2 deletions
+18
View File
@@ -44,6 +44,21 @@ export function createHaulPlanPrefetch(
let sequence = 0;
let plan: HaulPlanPayload = null;
let pending: unknown = null;
/** 마지막으로 보낸 입력의 표식 — 같은 값이면 다시 묻지 않는다. */
let sentKey: string | null = null;
/**
* 입력이 달라졌는지 가리는 짧은 표식. 응답이 오면 곡선을 다시 그리고, 그리기가 다시
* `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();
@@ -76,6 +91,9 @@ export function createHaulPlanPrefetch(
return {
schedule(result) {
const key = keyOf(result);
if (key === sentKey) return; // 같은 값 — 응답이 부른 재그리기가 되돌아온 것이다.
sentKey = key;
pending = result;
sequence += 1;
const seq = sequence;
@@ -338,6 +338,10 @@ export function createRouteMassHaulPanel(
// 총괄값 기준은 **횡단 고정**이다(2026-09-06 사용자: 종단 기준 곡선은 뒤에 없앨 예정).
// 범례에서 무엇을 켜 두었든 배지·상단줄이 흔들리지 않는다.
const picked = series.find((entry) => entry.basis === "cross") ?? series[0];
// 배분 선반입도 **여기서** 건다 — 접힘 여부와 무관하게 지나는 자리다(2026-09-06).
// 아래 `draw` 안에 두었더니 접힘 가드 뒤라 한 번도 안 불렸고, 그래서 「편집이 멈추면
// 조용히 받아 둔다」가 성립하지 않았다(펼친 뒤에야 첫 요청이 나갔다).
haulPrefetch?.schedule(picked.result);
onSummary(badgeValuesFrom(picked.result));
}
@@ -407,9 +411,8 @@ export function createRouteMassHaulPanel(
// 토량 분배는 켜 둔 첫 곡선(정식 우선)에만 얹는다 — B06과 같은 규칙.
const banded = series.find((entry) => visible.has(entry.key));
// 배분은 **서버가 만든다** — 그 산식을 번들에 남기지 않으려고 옮겼다(2026-09-06).
// 편집이 멈추면 선반입기가 뒤에서 받아 두므로 펼칠 때는 이미 도착해 있다.
// 요청은 `emitSummary` 가 걸어 두므로(접힘 여부와 무관) 펼칠 때는 이미 도착해 있다.
// 아직 못 받았으면 곡선만 그린다(분배 도형은 값이 오면 다시 그려진다).
if (banded) haulPrefetch?.schedule(banded.result);
const haulPlan: HaulPlan | null =
banded && visible.has(MASS_HAUL_BALANCE_KEY) ? (haulPrefetch?.current() ?? null) : null;
// 요약 막대는 2026-09-06 사용자 지시로 뺐다 — 총괄값은 종단 상단줄이 늘 보여 주고,