From 76df1e93354039f417ff6d0c1585be17674e52ae Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 23 Aug 2026 17:40:48 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=A2=85=EB=8B=A8=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=EA=B3=A0=20=EB=B3=80=EA=B2=BD=EC=9D=84=20=ED=9A=A1?= =?UTF-8?q?=EB=8B=A8=20=EC=84=A4=EA=B3=84=EC=97=90=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20-=20B05=20=EC=A7=84=EC=9E=85=20=EC=8B=9C?= =?UTF-8?q?=EC=97=90=EB=8F=84=20=EC=9E=AC=EA=B3=84=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-08-23 사용자 보고: 종단 그래프의 계획고 변화가 B06 횡단도에 반영 안 됨. 원인: 횡단 설계는 계산 당시 계획고(design.design_elevation_m) 기준으로 설계선 좌표를 굳혀 둔다. 계획선이 뒤에 바뀌면(사용자 편집·확정, 배수 최소고 같은 백엔드 규칙 도입) 설계선은 옛 자리에 남는데, 화면의 계획고 십자선과 3D 예상형상은 최신 계획선을 쓴다 - 두 기준이 어긋난다. 실측으로 한 노선에서 최대 2.21m 어긋남 확인. 지금까지 이 보정은 B06 진입 때만 돌아서, B05의 횡단 기준 유토곡선과 3D는 옛 설계선을 계속 썼다. - staleDesignChainages() 신설(B06_Section_UI_Section_Common) - 계획선 보간값과 계산 기준의 차이로 재계산 대상을 판정하는 B05·B06 공용 규칙. - B05 Profile_Panel: 진입 렌더에서 어긋남이 있으면 프리뷰 1회로 맞춘다. - B06 Page: 같은 공용 규칙을 쓰도록 통일(옛 암 2단계 필드 누락 조건은 유지). 검증: tsc 통과 · pytest 191 passed(신규 stale 판정 6건) · 공용 브라우저 실측 - 계획고·지반고 배치 대조(전 측점 일치, 렌더 역산 RMS 0.004~0.03m), 측점 중심 좌표가 노선 폴리라인과 거리 0, 인위 stale 1건을 B05 진입으로 자동 복구(stale 1 to 0, 계획고 540.895 복원). Co-Authored-By: Claude Fable 5 --- B05_Profile/B05_Profile_UI_Profile_Panel.ts | 5 ++++ B06_Section/B06_Section_UI_Page.ts | 9 +++--- B06_Section/B06_Section_UI_Section_Common.ts | 29 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel.ts b/B05_Profile/B05_Profile_UI_Profile_Panel.ts index 6985a5ff..07489011 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel.ts @@ -29,6 +29,7 @@ import { createProgressCircle } from "@ui/ui_template_progress"; import { showToast } from "@ui/ui_template_elements"; import { saveProfileAlignment } from "./B05_Profile_Api_Fetch"; import { previewCrossDesigns } from "../B06_Section/B06_Section_Api_Fetch"; +import { staleDesignChainages } from "../B06_Section/B06_Section_UI_Section_Common"; import { findMinCoverViolations, minCoverPoints, @@ -598,6 +599,10 @@ export function createRouteProfilePanel( ); draw(); requestAnimationFrame(draw); + // 저장된 횡단 설계가 옛 계획고로 굳어 있으면 진입 즉시 한 번 맞춘다 — + // 지금까지는 B06에 들어가야만 고쳐져, B05의 횡단 기준 유토곡선과 3D + // 예상형상이 옛 설계선을 그대로 썼다(2026-08-23 사용자 보고 · 실측 확인). + if (staleDesignChainages(nextDetail).length) scheduleCrossPreview(); }, /** 라이브 계획선 샘플(편집 반영분) — 3D 측점 바·코리도가 이걸 본다(2026-08-23). * 편집 전·base 없음이면 null — 호출부는 정본 design_profiles로 폴백한다. */ diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index c9fd8f79..e53e5b9f 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -41,7 +41,7 @@ import { import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul"; import { computeHaulPlan } from "@util/common_util_mass_haul_balance"; import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view"; -import { designElevationAt } from "./B06_Section_UI_Section_Common"; +import { staleDesignChainages } from "./B06_Section_UI_Section_Common"; import { createStandardPanel, type StandardPanelController } from "./B06_Section_UI_Standard_Panel"; import "./B06_Section_UI_Style.css"; import "./B06_Section_UI_Style_Cross.css"; @@ -203,13 +203,14 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { */ async function reconcileStaleDesigns(): Promise { if (!sectionDetail || !projectId || currentRouteId === null) return; - const profiles = sectionDetail.longitudinal.design_profiles; + // 계획고 어긋남은 B05와 **같은 규칙**으로 판정한다(공용 staleDesignChainages). + // 옛 암 2단계 필드 누락은 B06 전용 조건이라 여기서 더한다. + const staleByPlan = new Set(staleDesignChainages(sectionDetail)); const stale = sectionDetail.cross_sections.filter((section) => { const design = section.design; if (!design) return false; if (design.geometry_preset === "rock" && design.two_stage_slope === undefined) return true; - const planZ = designElevationAt(profiles, section.chainage_m); - return planZ !== undefined && Math.abs(planZ - design.design_elevation_m) > 1e-3; + return staleByPlan.has(section.chainage_m); }); if (!stale.length) return; showLoadingOverlay(); diff --git a/B06_Section/B06_Section_UI_Section_Common.ts b/B06_Section/B06_Section_UI_Section_Common.ts index 6b46112d..9cdca760 100644 --- a/B06_Section/B06_Section_UI_Section_Common.ts +++ b/B06_Section/B06_Section_UI_Section_Common.ts @@ -68,6 +68,35 @@ export function validElevation( * 측점 chainage 위치의 계획고를 계획선 샘플에서 선형보간한다. * 범위를 벗어나면 양 끝값으로 클램프하며, 계획선이 없으면 undefined를 반환해 지반고 폴백을 유도한다. */ +/** + * 저장된 횡단 설계가 **현재 계획선과 어긋난 측점**을 찾는다(B05·B06 공용 규칙). + * + * 횡단 설계는 계산 당시 계획고(`design.design_elevation_m`)를 기준으로 설계선 좌표를 + * 굳혀 둔다. 계획선이 그 뒤에 바뀌면(사용자 편집·확정, 배수 최소고 같은 백엔드 규칙 + * 도입) 설계선은 옛 계획고 자리에 남는데 화면의 계획고 십자선·3D는 최신 계획선을 쓴다 + * — 두 기준이 어긋나 횡단도·3D가 종단을 안 따라오는 것처럼 보인다(2026-08-23 실측: + * 한 노선에서 최대 2.21m 어긋남). 재계산 대상을 한 규칙으로 판정해 두 화면이 같은 + * 시점에 같은 조치를 하게 한다. + */ +export function staleDesignChainages( + detail: { + longitudinal: { design_profiles?: DesignProfile[] }; + cross_sections: Array<{ chainage_m: number; design?: { design_elevation_m: number } | null }>; + }, + toleranceM = 1e-3, +): number[] { + const profiles = detail.longitudinal.design_profiles; + if (!profiles?.length) return []; + return detail.cross_sections + .filter((section) => { + const design = section.design; + if (!design) return false; + const planned = designElevationAt(profiles, section.chainage_m); + return planned !== undefined && Math.abs(planned - design.design_elevation_m) > toleranceM; + }) + .map((section) => section.chainage_m); +} + export function designElevationAt( designProfiles: DesignProfile[] | undefined, chainageM: number,