Files
Aislo/B05_Profile/B05_Profile_UI_Profile_Preview.ts
T
eomsangdonandClaude Opus 5.5 25eea13ab2 perf(계산묶음): 349-0s 끌기 한 걸음 = 종단도 한 번 그림 — 다시 그림 한 틀 모음 · 초안 동안 저장 횡단 다시 셈 없음 · 그리기 몫 기록
- 묶음 기록 runs[].work — 알림 받는 쪽 이름별 ms(가게 · 관 · 구조물 · 새 상세 · 종단도 · 다시 셈 대기 · 횡단 카드) · 그리기 몫(종단도 · 유토곡선) · 쉼(profile 뒤 쉬는 동안)
- 종단도 다시 그림 = 다음 틀 한 번(drawSoon) — 갈아끼움 render · 정책 · 이어 공사 기준 · 비정규 측점 · 새 상세 알림 · 다시 셈 뒤가 각각 바로 그리던 것(끌기 한 걸음 6~8 번 → 1 번 · 되돌리기 5 → 1)
- 노선 초안 동안 종단도 횡단 다시 셈(crossPreview)이 갈아끼운 초안 상세를 안 셈(첫 끌기 한 번 통째 다시 셈 없앰)
- 시험 test_349_0s_draw_work 4(다섯 걸음 · 걸음마다 그리기 1 · work 칸 · 되돌리기 그대로 · 다시 셈 0) · 230-2 도움이 drawSoon 도 읽음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-16 17:42:54 +09:00

149 lines
6.8 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Profile_Preview.ts
* 계획선 편집 → 횡단 설계 프리뷰 반영 (패널 본체에서 분리, 2026-09-02 · 700줄 한계).
*
* 계획고가 바뀌면 측점별 횡단 단면적도 바뀐다 — 서버에 한 번 물어 전 측점을 다시 계산해
* **공유 캐시가 들고 있는 같은 객체**를 제자리 갱신한다. 그래야 횡단 기준 유토곡선과
* 3D 예상형상이 같은 값으로 따라온다(2026-08-03·08-23 사용자 보고).
*
* 끌기 중에는 계속 호출되므로 디바운스하고, 늦게 온 응답은 seq 비교로 버린다.
* 노선 초안 동안은 서버에 안 물음 — 계산 묶음 cross 가 그 차례를 끝낼 때까지만 「다시 계산 중」 · 그 뒤 남은
* 낡음은 곡선을 그리고 그 측점만 표시(342-1 · 349-0f).
* ========================================================================== */
import type { AlignmentEdits, ProfileAlignment } from "./logic/B05_Profile_UI_Profile_Alignment";
import { holdScreenAlignment, refreshCrossDesigns } from "../B06_Section/B06_Section_Cross_Refresh";
import { staleChainages } from "../B06_Section/B06_Section_UI_Section_Common";
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
import { pagePipeline } from "../A00_Common/design/design_pipeline_page";
import { useDesignStore } from "../A00_Common/design/design_store";
import type { DesignPipeline, PipelineEvent } from "../A00_Common/design/logic/design_pipeline";
/** cross 를 지났다고 볼 단계(cross 가 같은 입력이라 건너뛰어도 뒤 단계가 알림). */
const AFTER_CROSS = new Set(["cross", "footprint", "masshaul"]);
/** 묶음 차례가 cross 를 못 낸 채 끝나도(단계 실패) 「다시 계산 중」을 푸는 때(ms). */
const DRAFT_WAIT_MS = 3000;
export interface CrossPreviewContext {
projectId: string;
detail: () => SectionDetailResponse | null;
routeId: () => number | null;
edits: () => AlignmentEdits;
/** 화면이 쥔 계획선(측점 한 벌) — 다시 셈 · B06 · 3D 가 같은 선을 받음(342-1). */
alignment: () => ProfileAlignment | null;
/** 부를 때마다 읽음 — 보기마다 다름. */
debounceMs: () => number;
/** 반영이 끝난 뒤 다시 그린다 — 다시 계산한 측점 누가거리를 넘긴다(횡단 카드 갱신용). */
onApplied: (updated: number[]) => void;
}
export interface CrossPreview {
/** 편집이 있을 때마다 부른다 — 마지막 값만 서버로 나간다. */
schedule: () => void;
/**
* 그리기가 잰 낡음 지문 — 새 지문일 때만 `schedule`(같은 낡음 되풀이 · 서버 폴백 반복 막음 · 317-1).
* 돌려주는 값 = 다시 셈을 기다리는 중인가. 다시 셈 뒤에도 남은 낡음은 false — 곡선은 그리고
* 그 측점만 표시(342-1 · 09-03 「새 값만 보여주기」를 바꿈 · 사용자 확정 10-16).
* `draft` = 노선 초안 동안 — 서버에 안 묻고 묶음 cross 차례만 기다림(349-0f).
*/
scheduleIfStale: (key: string, draft?: boolean) => boolean;
/** 패널을 걷을 때 대기 중인 요청 타이머를 끈다. */
dispose: () => void;
}
export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview {
let timer = 0;
let seq = 0;
let lastStale = "";
/** 다시 셈 예약 · 진행 중 — 이 동안의 낡음은 「다시 계산 중」. */
let busy = false;
let warned = "";
const release = holdScreenAlignment((routeId) =>
routeId === ctx.routeId() ? ctx.alignment() : null,
);
/** 노선 초안 동안 기다리는 묶음 차례 — 그 차례 cross(또는 뒤 단계)가 오면 풂. */
let awaiting: number | null = null;
let waitTimer = 0;
let watched: DesignPipeline | null = null;
const onStage = (event: PipelineEvent): void => {
if (event.kind === "stage" && event.generation === awaiting && AFTER_CROSS.has(event.stage))
awaiting = null;
};
const stopGeneration = useDesignStore(ctx.projectId, null).subscribe("generation", (gen) => {
const pipe = pagePipeline(ctx.projectId);
if (!pipe || gen === null) return;
if (watched !== pipe) (watched = pipe).subscribe(onStage, "다시 셈 대기");
awaiting = gen;
window.clearTimeout(waitTimer);
waitTimer = window.setTimeout(() => {
if (awaiting !== gen) return;
awaiting = null;
ctx.onApplied([]);
}, DRAFT_WAIT_MS);
});
const warnLeft = (key: string): void => {
if (!key || key === warned) return;
warned = key;
console.warn(
`[횡단] 다시 셈 뒤에도 낡은 측점 ${staleChainages(key).length}곳 — 곡선은 그리고 그 측점만 표시`,
staleChainages(key),
);
};
const preview: CrossPreview = {
scheduleIfStale(key, draft = false) {
if (draft) {
if (awaiting === null) warnLeft(key);
return key !== "" && awaiting !== null;
}
if (key && key !== lastStale) preview.schedule();
else if (!busy) warnLeft(key);
lastStale = key;
return key !== "" && busy;
},
schedule() {
if (!ctx.detail() || ctx.routeId() === null) return;
busy = true;
window.clearTimeout(timer);
timer = window.setTimeout(() => {
const detail = ctx.detail();
const routeId = ctx.routeId();
if (!detail || routeId === null) return void (busy = false);
const current = (seq += 1);
// 재계산은 B06과 **같은 창구**를 쓴다 — 표준 단면값·암 경계 오프셋이 빠지면 서버가
// 다른 설계를 그려 같은 데이터가 두 화면에서 다른 값이 된다(2026-09-03 일원화).
const isCurrent = (): boolean =>
current === seq && ctx.detail() === detail && ctx.routeId() === routeId;
void refreshCrossDesigns({
projectId: ctx.projectId,
routeId,
detail,
edits: ctx.edits(),
alignment: ctx.alignment(),
// 늦게 온 옛 응답이 새 설계를 덮지 않게 반영 직전에 한 번 더 확인한다.
shouldApply: isCurrent,
})
.then((updated) => {
if (current === seq) busy = false;
// 남은 낡음이 있으면 바뀐 것이 없어도 다시 그림 — 「다시 계산 중」에 멈추지 않게(342-1)
if (updated.length || lastStale) ctx.onApplied(updated);
})
.catch(() => {
/* 프리뷰 실패는 무시 — 화면의 계획선은 그대로 두고 다음 편집에서 다시 시도한다. */
if (current !== seq) return;
busy = false;
if (lastStale) ctx.onApplied([]);
});
}, ctx.debounceMs());
},
dispose() {
window.clearTimeout(timer);
window.clearTimeout(waitTimer);
stopGeneration();
release();
},
};
return preview;
}