perf(B05/B06): 진입 시 중복 서버 호출 두 건 제거

- 확정 지표면: 목록(surface/models)으로 id 를 찾고 3D 단계에서 surface/confirmed
  를 또 부르던 것을 확정 응답 한 번으로 합침. 모델 id·범위가 그 응답에 다 있다.
- 종횡단 설정값(sections/context): ④ 계산 결과로 보고 세션에 담음. B05·B06 을
  오갈 때마다 다시 묻지 않는다. 노선 캐시를 버리는 자리에서 함께 버린다.

검증: tsc --noEmit 통과, pytest 389 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 15:07:47 +09:00
co-authored by Claude Opus 5
parent a360c0caba
commit 8e3c1f91fc
5 changed files with 37 additions and 18 deletions
+4
View File
@@ -130,6 +130,10 @@ export const STATE_REGISTRY = {
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
latest: { bucket: "result", scope: "project", legacy: (p) => `b05:latest:${p}` },
/** 종횡단 설정값(config 상수 + 프로젝트 임도 종류 + 노선 id). B05·B06 이 같은 값을
* 받아 쓰므로 화면을 오갈 때마다 다시 묻지 않는다(2026-09-06 호출 정리).
* 노선이 바뀌면 `latest` 와 함께 버린다. */
"section-context": { bucket: "result", scope: "project" },
/** 횡단 상세 응답 — 예전에는 메모리에만 있어 페이지를 떠나면 사라졌다. */
"section-detail": { bucket: "result", scope: "route" },
/** 서버가 준 규격 횡단 기본값 — 사용자가 만든 값이 아니라 **기억해 둔 서버 값**이다. */
+2
View File
@@ -323,4 +323,6 @@ export function writeRouteLatestCache(projectId: string, value: RouteLatestRespo
export function clearRouteLatestCache(projectId: string): void {
clearState("latest", projectId);
// 설정값에 노선 id 가 실려 있어 함께 버려야 옛 노선을 가리키지 않는다(2026-09-06).
clearState("section-context", projectId);
}
+12 -11
View File
@@ -13,8 +13,7 @@ import {
} from "../A00_Common/b_workflow_nav";
import {
fetchConfirmedSurface,
listSurfaceModels,
type SurfaceModelSummary,
type SurfaceConfirmedResponse,
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import {
fetchLatestRoute,
@@ -169,7 +168,10 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
* 선택은 3D·그래프·사이드바가 서로를 갱신하므로, `selectionSyncing` 가드로 재진입을 막아
* 무한 재귀(스택 오버플로우·프리즈)를 방지한다.
*/
let confirmedSurface: SurfaceModelSummary | null = null;
/** 확정 지표면 — 모델 id 와 3D 로딩에 필요한 범위만 들고 있는다(2026-09-06 호출 정리).
* 예전에는 목록(`listSurfaceModels`)으로 id 를 찾고 3D 단계에서 `surface/confirmed`
* 를 또 불러 요청이 두 번 나갔다. 확정 응답 하나에 둘 다 들어 있다. */
let confirmedSurface: SurfaceConfirmedResponse | null = null;
let latest: RouteLatestResponse | null = null;
let roadWidths = DEFAULT_ROAD_WIDTHS;
let currentSectionDetail: SectionDetailResponse | null = null;
@@ -685,9 +687,9 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
latest = latestResponse;
advanceLoading("확정 지표면 모델을 확인하는 중…");
// ③ 확정 지표면 모델 목록.
const models = await listSurfaceModels(activeProjectId);
confirmedSurface = models.models.find((model) => model.status === "CONFIRMED") ?? null;
// ③ 확정 지표면 모델 id·범위를 한 번에 받는다.
const confirmed = await fetchConfirmedSurface(activeProjectId);
confirmedSurface = confirmed.model_id === null ? null : confirmed;
if (!confirmedSurface) {
// 새 자료가 올라와 옛 결과가 지워진 상태 — 여기서 보여 줄 게 없다.
leaveForDashboard();
@@ -716,16 +718,15 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
const [surface, current] = [confirmedSurface, latest];
if (!surface || !current) return;
try {
// 가장자리만 받는다 — 포인트클라우드 전체(수십 MB)는 안 받는다.
const confirmed = await fetchConfirmedSurface(activeProjectId);
if (!confirmed.bounds) throw new Error("지표면 범위 정보를 찾을 수 없습니다.");
// 범위는 ③에서 받아 둔 확정 응답에 이미 들어 있다 — 다시 부르지 않는다.
if (!surface.bounds) throw new Error("지표면 범위 정보를 찾을 수 없습니다.");
await viewer.loadSurface(
activeProjectId,
surface.id,
surface.model_id as number,
current.surface_params.method,
current.surface_params.smooth,
current.surface_params.contour_interval_m,
toBounds(confirmed.bounds),
toBounds(surface.bounds),
);
renderLatest(current); // 지형이 올라온 뒤에야 마커·측점선이 지형 위에 놓인다.
if (currentSectionDetail) renderStationLines(currentSectionDetail);
+4 -4
View File
@@ -16,7 +16,6 @@ import {
showToast,
} from "@ui/ui_template_elements";
import { navigateTo } from "../A00_Common/router";
import type { SurfaceModelSummary } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import {
clearRouteLatestCache,
confirmRoute,
@@ -46,7 +45,8 @@ function L(key: keyof typeof ui_locales): string {
export interface PageActionContext {
projectId: string;
latest: () => RouteLatestResponse | null;
confirmedSurface: () => SurfaceModelSummary | null;
/** 확정 지표면 응답 — 모델 id 는 `model_id` 다(2026-09-06 호출 정리). */
confirmedSurface: () => { model_id: number | null } | null;
routeReady: () => boolean;
viewer: () => ReturnType<typeof createRouteViewer>;
panel: () => ReturnType<typeof createRoutePanel>;
@@ -77,7 +77,7 @@ export async function solveRouteAction(ctx: PageActionContext): Promise<void> {
filter_key: latest.surface_params.source_filter,
method: latest.surface_params.method,
smooth: latest.surface_params.smooth,
surface_model_id: confirmedSurface.id,
surface_model_id: confirmedSurface.model_id ?? undefined,
algorithm: values.algorithm,
bp: routePoint(points.bp),
ep: routePoint(points.ep),
@@ -149,7 +149,7 @@ export async function tempSaveAction(ctx: PageActionContext): Promise<void> {
filter_key: latest?.surface_params.source_filter,
method: latest?.surface_params.method,
smooth: latest?.surface_params.smooth,
surface_model_id: ctx.confirmedSurface()?.id,
surface_model_id: ctx.confirmedSurface()?.model_id ?? undefined,
irregular_stations: ctx
.bridge()
.irregularStations()
+15 -3
View File
@@ -16,6 +16,7 @@
* 기존 호출 코드가 깨지지 않도록 여기서 그대로 다시 내보낸다.
* ========================================================================== */
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
import type {
CrossDesign,
@@ -58,10 +59,21 @@ async function requestJson<T>(path: string, init: RequestInit): Promise<T> {
}
/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */
/** 종횡단 설정값 — ④ 계산 결과라 세션에 담아 두고 화면을 오갈 때마다 다시 묻지 않는다
* (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */
export async function fetchSectionContext(projectId: string): Promise<SectionContextResponse> {
return requestJson<SectionContextResponse>(`/projects/${projectId}/sections/context`, {
method: "GET",
});
const cached = readState<SectionContextResponse>("section-context", projectId);
if (cached) return cached;
const fresh = await requestJson<SectionContextResponse>(
`/projects/${projectId}/sections/context`,
{ method: "GET" },
);
writeState("section-context", fresh, projectId);
return fresh;
}
export function clearSectionContextCache(projectId: string): void {
clearState("section-context", projectId);
}
/** 경로의 종단면 요약을 조회한다. */