Files
Aislo/B06_Section/B06_Section_Cross_Refresh.ts
T
eomsangdonandClaude Opus 5 9b4abecf9d refactor(B05/B06): 유토곡선 일원화 — 보는 노선·횡단 재계산 창구·낡음 판정 통합
같은 데이터를 두 화면에 보여 주는 기능인데 세 곳이 갈려 값이 달랐다(실측: 절토(자연)
B06 3,704.6㎥ ↔ B05 4,526.8㎥).

① 보는 노선 — B05는 최신 경로, B06은 최신 **확정** 경로를 열어 노선을 다시 탐색한
   프로젝트에서 서로 다른 노선을 봤다(route 126 DRAFT ↔ 125 CONFIRMED, 같은 20m
   측점 성토 4.82㎡ ↔ 85.9㎡). `get_workflow_route_context()`(최신 경로) 신설해 B06
   화면 context가 그것을 쓴다. 납품 도면(B07)이 쓰는 확정 전용 창구는 그대로 둔다.

② 횡단 재계산 — 호출이 두 벌이라 인자가 갈렸다(B06만 표준 단면값·암 경계 오프셋 전달,
   보존하는 사용자 부속값도 2개 ↔ 7개). `B06_Section_Cross_Refresh.refreshCrossDesigns()`
   한 창구로 모으고 세션 편집값은 저장소에서 직접 읽어 패널 없는 B05도 같은 값을 보낸다.

③ 낡음 판정 — 「옛 암 2단계 필드 누락」 조건이 B06 페이지에만 있어 B05는 재계산을
   건너뛰었다. 공용 `staleDesignChainages()` 안으로 옮겨 두 화면이 같은 시점에 같은
   조치를 한다.

검증 — 공용 브라우저 실측: 두 화면 모두 route 126, 요약줄 문자열 완전 일치
(`절토(자연) 4,526.8㎥ · 성토 14,881.8㎥ · 토취 10,213.3㎥ · 최종 누가토량 −10,213.3㎥`).
pytest 370 passed·17 skipped(일원화 검사 4건 신설), typecheck·prettier·ruff 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 09:50:14 +09:00

94 lines
4.4 KiB
TypeScript

/* =============================================================================
* B06_Section_Cross_Refresh.ts
* 「현재 계획선에 맞춘 횡단 재계산」 **단일 창구** — B05·B06이 같은 입력으로 같은 결과를 본다.
*
* ── 왜 필요한가 (2026-09-03 사용자 지시) ─────────────────────────────
* 유토곡선은 두 화면이 같은 데이터를 보여 주는 기능인데, 재계산 호출이 두 벌이라 값이
* 갈렸다. 실측 — 같은 프로젝트·같은 시점에 B06 `절토(자연) 3,704.6㎥` ↔ B05 `4,526.8㎥`.
* 원인은 인자였다:
* · B06: `previewCrossDesigns(..., standardPanel.getValues(), { fullDesigns, rockBoundaryOffsets })`
* · B05: `previewCrossDesigns(..., undefined, { fullDesigns })`
* 표준 단면값과 암 경계 오프셋이 빠지면 서버가 다른 설계를 그려 단면적이 달라진다.
* 보존하는 사용자 부속값 목록도 서로 달라(B05는 2개, B06은 7개) B05를 거치면 기슭막이
* 조정 같은 값이 사라졌다.
*
* 그래서 **입력 수집·서버 호출·제자리 반영**을 여기 한 곳으로 모은다. 세션 편집값은
* 패널이 아니라 세션 저장소에서 직접 읽으므로, 패널이 없는 B05도 B06과 같은 값을 보낸다.
* ========================================================================== */
import { previewCrossDesigns } from "./B06_Section_Api_Fetch";
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import { readRockBoundarySession } from "./B06_Section_UI_Page_Persist";
import { readStandardCrossSession } from "./B06_Section_UI_Standard_Panel";
/** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */
export interface CrossRefreshEdits {
station_offsets: Record<string, number>;
curve_radii: Record<string, number>;
}
export interface CrossRefreshInput {
projectId: string;
routeId: number;
/** 제자리 갱신 대상 — 공유 캐시가 들고 있는 그 객체여야 두 화면이 같이 따라온다. */
detail: SectionDetailResponse;
edits: CrossRefreshEdits;
/**
* 응답이 도착한 시점에 **아직 이 결과를 써도 되는지** 묻는다(false면 반영하지 않는다).
* 끌기 중에는 요청이 겹치므로, 늦게 온 옛 응답이 새 설계를 덮지 않게 하는 문지기다.
*/
shouldApply?: () => boolean;
}
/** 서버 설계로 갈아 끼워도 **살려 두는 사용자 부속값** — 화면 조작으로만 생기는 값이다. */
function preserveUserFields(
next: NonNullable<CrossSection["design"]>,
previous: CrossSection["design"],
): NonNullable<CrossSection["design"]> {
if (!previous) return next;
return {
...next,
inlet_structure: previous.inlet_structure,
basin_adjust: previous.basin_adjust,
revet_adjust: previous.revet_adjust,
extra_wall_counts: previous.extra_wall_counts,
extra_spans: previous.extra_spans,
revet_link_detached: previous.revet_link_detached,
revet_follow_grade: previous.revet_follow_grade,
};
}
/**
* 전 측점 횡단을 현재 계획선으로 다시 계산해 `detail.cross_sections[].design`을 제자리 교체한다.
* 돌려주는 값은 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다.
*/
export async function refreshCrossDesigns(input: CrossRefreshInput): Promise<number[]> {
const { projectId, routeId, detail, edits, shouldApply } = input;
// full_designs — 설계선 좌표까지 받아야 3D 코리도·횡단도가 편집 즉시 같은 형상이 된다.
const response = await previewCrossDesigns(
projectId,
routeId,
edits,
readStandardCrossSession(projectId) ?? undefined,
{
fullDesigns: true,
rockBoundaryOffsets: readRockBoundarySession(projectId, routeId),
},
);
if (shouldApply && !shouldApply()) return [];
const designByChainage = new Map(
response.designs.map((entry) => [entry.chainage_m.toFixed(3), entry.design]),
);
const updated: number[] = [];
for (const section of detail.cross_sections) {
const next = designByChainage.get(section.chainage_m.toFixed(3));
if (!next) continue;
section.design = preserveUserFields(
next as NonNullable<CrossSection["design"]>,
section.design,
);
updated.push(section.chainage_m);
}
return updated;
}