feat(B05/B06): 옛 계획고 유토곡선 노출 차단 + 사면 미교차 측점 경고
① 저장된 횡단이 지금 계획선과 어긋나면(재계산 예약 상태) 유토곡선을 그리지 않고 「횡단을 지금 계획선에 맞춰 다시 계산하는 중입니다」만 띄움. 옛 계획고로 만든 면적이 2초간 보였다가 정본으로 갈아 끼워지던 것을 없앰(2026-09-03 사용자 확정 「새 값만 보여주기」). 용화 실측 — 65측점 중 56개가 낡음, 계획고 차 −9.36~+5.83m, 저장분 성토 42,050㎥ ↔ 정본 14,881㎥. ② 사면이 계산 반폭 끝까지 원지반과 만나지 않아 면적이 잘린 측점에 카드 경고 표기. 판정은 면적을 내는 엔진 한 곳(B06_Section_Engine_Design)에서 `slope_unclosed` 로 내려 화면·수량·도면이 같은 기준을 봄. 계산은 손대지 않음(2026-09-03 사용자 확정 「영원히 못 만나는 지형이 있을 수 있으니 경고로 대체」). 검증 — 공용 브라우저 B06 실측: 2.6초 재계산 안내만 표시(옛 값 미노출), 4.4초 정본 표시, 사면 미교차 경고 13/65측점. pytest 366 passed·17 skipped, typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,12 @@ export interface RouteMassHaulDrawParams {
|
||||
onSelectStation: (stationId: string) => void;
|
||||
/** 측점선이 아닌 빈 곳 클릭 — 선택 해제(2026-08-04 사용자 지시). */
|
||||
onClearSelection?: () => void;
|
||||
/**
|
||||
* 저장된 횡단이 지금 계획선과 어긋나 재계산이 예약된 상태. 그 사이의 면적은 **옛
|
||||
* 계획고로 만든 값**이라 그리면 사용자가 옛 그림을 보게 된다(2026-09-03 사용자 확정:
|
||||
* 「새 값만 보여주기」). Panel이 `staleDesignChainages`로 판정해 넘긴다.
|
||||
*/
|
||||
pendingRecalc?: boolean;
|
||||
}
|
||||
|
||||
export interface RouteMassHaulPanel {
|
||||
@@ -306,6 +312,13 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
|
||||
note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
// 저장된 횡단이 지금 계획선과 어긋나 있으면 그 면적은 **옛 계획고로 만든 값**이다.
|
||||
// 그걸 잠깐 그렸다가 재계산 결과로 갈아 끼우면 사용자가 옛 그림을 본다(2026-09-03
|
||||
// 사용자 확정: 「새 값만 보여주기」). Panel이 이미 재계산을 예약해 두므로 기다린다.
|
||||
if (params.pendingRecalc) {
|
||||
note("횡단을 지금 계획선에 맞춰 다시 계산하는 중입니다.");
|
||||
return;
|
||||
}
|
||||
// B06과 같은 계산 — 횡단 기준(정식)과 종단 기준(개략 비교)을 함께 낸다.
|
||||
const series: MassHaulSeries[] = computeMassHaulSeries(
|
||||
params.longitudinal,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
createLongitudinalProfile,
|
||||
longitudinalMinimumWidth,
|
||||
} from "../B06_Section/B06_Section_UI_Longitudinal";
|
||||
import { LONG_PAD } from "../B06_Section/B06_Section_UI_Section_Common";
|
||||
import { LONG_PAD, staleDesignChainages } from "../B06_Section/B06_Section_UI_Section_Common";
|
||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { normalizedLongitudinal, toDesignProfile } from "./B05_Profile_UI_Profile_Data";
|
||||
import {
|
||||
@@ -326,6 +326,9 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
// 측점별 횡단 설계(기본 프리뷰 포함)를 입력으로 쓴다 — B06과 같은 재료다.
|
||||
longitudinal: { length_m: longitudinal.length_m, design_profiles: designProfiles },
|
||||
crossSections: detail.cross_sections,
|
||||
// 저장된 횡단이 지금 계획선과 어긋나면 그 면적은 옛 계획고로 만든 값이다 —
|
||||
// Panel이 재계산을 예약해 두므로 유토곡선은 그 결과만 그린다(2026-09-03 사용자 확정).
|
||||
pendingRecalc: staleDesignChainages(detail).length > 0,
|
||||
axis: {
|
||||
maxChainageM: maxChainageOf(longitudinal),
|
||||
// 종단 그래프의 `chainageMapper`와 정확히 같은 매핑이 되도록 반 칸 들여쓰기를
|
||||
|
||||
@@ -435,6 +435,12 @@ export interface CrossDesign {
|
||||
fill_area_m2: number;
|
||||
/** 성토측 자연 지반 경사(rise/run). 자연방토 판정 입력. 성토측이 없으면 null. */
|
||||
fill_ground_slope?: number | null;
|
||||
/**
|
||||
* 사면이 샘플 범위(±계산 반폭) 끝에서도 원지반과 만나지 않은 측점. 면적이 거기서
|
||||
* 잘려 절·성토량이 실제와 다르다 — 계산은 그대로 두고 카드에 경고만 단다
|
||||
* (2026-09-03 사용자 확정: 「영원히 못 만나는 지형이 있을 수 있으니 경고로 대체」).
|
||||
*/
|
||||
slope_unclosed?: boolean;
|
||||
ditch_area_m2: number;
|
||||
design_line: Array<{ offset_m: number; elevation_m: number }>;
|
||||
/** 확정 시 병합되는 암 경계선 오프셋(m). 세션 값이 우선이며 복원 폴백으로 쓴다. */
|
||||
|
||||
@@ -31,6 +31,10 @@ from config.config_system import (
|
||||
)
|
||||
|
||||
|
||||
# 사면이 원지반과 만났다고 볼 높이차(m). 이보다 크면 샘플 끝에서 잘린 것으로 본다.
|
||||
_SLOPE_CLOSE_TOLERANCE_M = 0.01
|
||||
|
||||
|
||||
def _side_role(section_mode: str) -> tuple[str, str]:
|
||||
"""단면유형 → (좌측 역할, 우측 역할). 역할은 'cut' 또는 'fill'."""
|
||||
if section_mode == "left_cut":
|
||||
@@ -533,6 +537,12 @@ def compute_cross_design(
|
||||
# 측구 굴착은 설계선에 포함돼 절토 면적에 자연 반영된다(별도 가산 없음 — 이중계상 방지).
|
||||
cut_area, fill_area = _trapezoid_areas(offsets, diffs)
|
||||
fill_ground_slope = geometry.fill_ground_slope()
|
||||
# 사면이 샘플 범위 끝에서도 원지반과 만나지 않으면 면적이 거기서 잘린다 — 그만큼
|
||||
# 절·성토량이 실제와 다르고 유토곡선도 그 값을 그대로 쌓는다. 영원히 안 만나는
|
||||
# 지형이 있을 수 있으므로 계산은 손대지 않고 **경고만** 낸다(2026-09-03 사용자 확정).
|
||||
slope_unclosed = bool(diffs) and (
|
||||
abs(diffs[0]) > _SLOPE_CLOSE_TOLERANCE_M or abs(diffs[-1]) > _SLOPE_CLOSE_TOLERANCE_M
|
||||
)
|
||||
|
||||
# 절토면적 토사/암반 분리 — 지표면~암반 경계선이 토사, 그 아래가 암이다. 경계선 위치가
|
||||
# 곧 유토곡선 EA/RR/BR 비율을 만들므로, 사용자가 경계선을 올리내리면 이 값이 함께 바뀐다.
|
||||
@@ -629,6 +639,8 @@ def compute_cross_design(
|
||||
"cut_rock_area_m2": round(cut_rock_area, 4),
|
||||
"cut_rock_kind": cut_rock_kind,
|
||||
"fill_area_m2": round(fill_area, 4),
|
||||
# 사면이 샘플 범위 끝까지 원지반을 못 만나 면적이 잘린 측점 — 경고 표기용.
|
||||
"slope_unclosed": slope_unclosed,
|
||||
# 성토측 자연 지반 경사(rise/run) — 자연방토 판정 입력. 성토측이 없으면 None.
|
||||
"fill_ground_slope": (
|
||||
round(fill_ground_slope, 4) if fill_ground_slope is not None else None
|
||||
|
||||
@@ -29,6 +29,7 @@ PREVIEW_DESIGN_FIELDS = (
|
||||
"fill_area_m2",
|
||||
"fill_ground_slope",
|
||||
"design_elevation_m",
|
||||
"slope_unclosed",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -309,6 +309,15 @@ export function createCrossSectionCard(
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "b06-cross-card__meta";
|
||||
// 사면이 계산 반폭 끝까지 원지반을 못 만난 측점 — 면적이 거기서 잘려 유토곡선까지
|
||||
// 그 값을 쌓는다. 계산은 손대지 않고 사실만 알린다(2026-09-03 사용자 확정).
|
||||
if (section.design?.slope_unclosed) {
|
||||
const openSlope = document.createElement("span");
|
||||
openSlope.className = "b06-cross-card__warning";
|
||||
openSlope.textContent = `⚠ ${L("B06_Cross_SlopeUnclosed")}`;
|
||||
openSlope.title = L("B06_Cross_SlopeUnclosed_Tip");
|
||||
meta.append(openSlope);
|
||||
}
|
||||
if (section.structure) {
|
||||
const structure = document.createElement("span");
|
||||
structure.className = "b06-cross-card__structure";
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
L,
|
||||
longitudinalMaxChainage,
|
||||
LONG_PAD,
|
||||
staleDesignChainages,
|
||||
type YScaleOptions,
|
||||
} from "./B06_Section_UI_Section_Common";
|
||||
|
||||
@@ -516,14 +517,19 @@ export function createSectionView(
|
||||
),
|
||||
];
|
||||
|
||||
const series: MassHaulSeries[] = currentConversion
|
||||
? computeMassHaulSeries(
|
||||
detail.longitudinal,
|
||||
detail.cross_sections,
|
||||
currentConversion,
|
||||
currentNaturalSpoilSlope,
|
||||
)
|
||||
: [];
|
||||
// 저장된 횡단이 지금 계획선과 어긋나면 그 면적은 **옛 계획고로 만든 값**이다. 진입
|
||||
// 직후 `reconcileStaleDesigns()`가 다시 계산해 갈아 끼우므로, 그 사이 옛 값으로 곡선을
|
||||
// 그리면 사용자가 옛 그림을 본다(2026-09-03 사용자 확정: 「새 값만 보여주기」).
|
||||
const pendingRecalc = staleDesignChainages(detail).length > 0;
|
||||
const series: MassHaulSeries[] =
|
||||
currentConversion && !pendingRecalc
|
||||
? computeMassHaulSeries(
|
||||
detail.longitudinal,
|
||||
detail.cross_sections,
|
||||
currentConversion,
|
||||
currentNaturalSpoilSlope,
|
||||
)
|
||||
: [];
|
||||
// 토량 분배는 **면을 깐 곡선 하나**(= 켜 둔 첫 곡선)에만 얹는다 — 곡선마다 평형선을
|
||||
// 그리면 계단이 서로 엇갈려 어느 쪽 배분인지 읽히지 않는다.
|
||||
const bandedSeries = series.find((entry) => visibleSeries.has(entry.key));
|
||||
@@ -588,7 +594,9 @@ export function createSectionView(
|
||||
panelCount.textContent = "";
|
||||
panelBody.append(createMassHaulSummary(summarySeries, haulPlan));
|
||||
} else {
|
||||
panelCount.textContent = L(series.length ? "B06_MassHaul_AllHidden" : "B06_MassHaul_Empty");
|
||||
panelCount.textContent = pendingRecalc
|
||||
? L("B06_MassHaul_Recalculating")
|
||||
: L(series.length ? "B06_MassHaul_AllHidden" : "B06_MassHaul_Empty");
|
||||
}
|
||||
chartWrap.scrollLeft = keepScrollLeft;
|
||||
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
||||
|
||||
@@ -232,6 +232,12 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* 사면 미교차 경고 — 면적이 계산 반폭에서 잘린 측점(2026-09-03). */
|
||||
.b06-cross-card__warning {
|
||||
color: var(--color-warning);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 지반유형 세그먼트(D-6): 제목행 1행 내 인라인 배치. 좌측 구분선(3번). */
|
||||
.b06-design__seg--header {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -341,6 +341,13 @@ export const ui_locales_b2 = {
|
||||
"측점에 지반유형을 지정하면 유토곡선이 표시됩니다.",
|
||||
"The mass haul diagram appears once ground types are assigned to stations.",
|
||||
],
|
||||
/* 사면이 계산 반폭(지반 샘플 범위) 끝에서도 원지반과 안 만난 측점 경고.
|
||||
계산은 그대로 두고 사실만 알린다(2026-09-03 사용자 확정). */
|
||||
B06_Cross_SlopeUnclosed: ["사면 미교차", "Slope not closed"],
|
||||
B06_Cross_SlopeUnclosed_Tip: [
|
||||
"사면이 계산 반폭 끝까지 원지반과 만나지 않아 절·성토 면적이 그 자리에서 잘렸습니다. 유토곡선·수량도 잘린 값을 씁니다.",
|
||||
"The slope never meets existing ground within the computed half-width, so the cut/fill area is truncated there. The mass haul curve and quantities use the truncated value.",
|
||||
],
|
||||
B06_MassHaul_CutNatural: ["절토(자연)", "Cut (natural)"],
|
||||
B06_MassHaul_CutCompacted: ["절토(다짐환산)", "Cut (compacted)"],
|
||||
B06_MassHaul_Fill: ["성토", "Fill"],
|
||||
@@ -356,6 +363,12 @@ export const ui_locales_b2 = {
|
||||
"노선 끝의 누가토량 — 0에 가까울수록 절·성토 균형. 곡선이 0선을 넘지 않으면 평형선·운반 블록이 생기지 않습니다.",
|
||||
"Cumulative volume at the end of the route — the closer to zero, the better cut/fill balance. No balance lines or haul blocks appear while the curve never crosses zero.",
|
||||
],
|
||||
/* 저장된 횡단이 옛 계획고로 굳어 있어 재계산을 기다리는 동안 — 옛 값으로 그린 곡선을
|
||||
보여 주지 않는다(2026-09-03 사용자 확정: 「새 값만 보여주기」). */
|
||||
B06_MassHaul_Recalculating: [
|
||||
"횡단을 지금 계획선에 맞춰 다시 계산하는 중입니다.",
|
||||
"Recalculating cross sections against the current profile.",
|
||||
],
|
||||
B06_MassHaul_AllHidden: [
|
||||
"표시할 곡선을 하나 이상 켜 주세요.",
|
||||
"Turn on at least one curve to display.",
|
||||
|
||||
Reference in New Issue
Block a user