fix(B06): 측구 「선택」과 「결과」를 가름 — 한 번 저장되면 자동이 안 돌던 자리

⚠⚠ 한 칸(`ditch_enabled`)에 **두 뜻**이 담겨 있었음.
결과(실제 섰나)를 그대로 다시 입력으로 넣어 읽었으므로 **한 번 저장되면 자동 판정이
영영 다시 안 돌았음.** 계획고를 내려 절토가 생겨도 측구가 안 서고 **아무 말도 안 나왔음.**

- **선택** `ditch_choice` (없음 = 자동) · **결과** `ditch_enabled` (실제 섰나)로 가름
- 자동 판정을 **먼저** 하고 선택을 그 위에 얹음. 양성 단면은 선택보다 기하가 먼저
- ⚠ **옛 저장분은 자동값과 다를 때만 선택으로 살림** — 같으면 자동이 그렇게 냈던 것이고
  다르면 사용자가 일부러 바꾼 것임. 설계 의도를 잃지 않으면서 굳은 값을 푼 것
  (오늘 `length_m` 에서 겪은 「저장분이 걸린 자리」를 같은 방식으로 처리)
- 화면 카드는 이제 **선택**을 읽어 상태를 세우고, 표시는 **결과**를 보임
- 파이썬·TS 짝 양쪽 + 호출부 여섯 곳(재계산·저장·확정·사토장·요청 스키마)

시험 다섯 추가 — 「결과를 다시 넣어도 자동이 계속 돎」이 그 잠금. 전체 616 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 13:10:47 +09:00
co-authored by Claude Opus 5
parent f569cb8498
commit 01c96e9a11
13 changed files with 91 additions and 23 deletions
+8
View File
@@ -93,6 +93,9 @@ export interface CrossDesignOptions {
* 짝: 파이썬 `compute_cross_design(cut_slope_ratio=…)`. */
cutSlopeRatio?: number | null;
ditchEnabled?: boolean | null;
/** 측구를 둘지 **사용자가 정한 선택**(`null` = 자동 판정).
* 짝: 파이썬 `compute_cross_design(ditch_choice=…)`. */
ditchChoice?: boolean | null;
/** 세월교 월류 높이만큼 노면을 통째로 내린다(m). */
surfaceDropM?: number;
/** 이 측점의 평면 곡선반경(m) — 곡선부 확폭을 정하는 입력. 직선이면 null. */
@@ -136,7 +139,10 @@ export interface CrossDesignResult {
widening_right_m?: number;
cross_slope_pct: number;
ditch: Record<string, unknown>;
/** 결과 — **실제로 섰나**. */
ditch_enabled: boolean;
/** 선택 — **사용자가 정한 것**(`null` = 자동). 결과와 갈라 둔다(2026-09-09). */
ditch_choice: boolean | null;
paved: boolean;
road_edges: { left: CrossDesignEdge; right: CrossDesignEdge };
carriageway_edges: { left: CrossDesignEdge; right: CrossDesignEdge };
@@ -348,6 +354,7 @@ export function computeCrossDesign(
rockBoundaryOffsetM,
twoStageSlope: enableTwoStage,
ditchEnabled: options.ditchEnabled ?? null,
ditchChoice: options.ditchChoice ?? null,
berm: options.berm ?? null,
});
@@ -508,6 +515,7 @@ export function computeCrossDesign(
cross_slope_pct: round4(crossSlopePct),
ditch: ditchSpec,
ditch_enabled: geometry.hasDitch,
ditch_choice: geometry.ditchChoice,
paved,
road_edges: {
left: {
@@ -106,6 +106,8 @@ export class SectionGeometry {
ditchType: string;
slopePerOffset: number;
hasDitch: boolean;
/** 사용자가 정한 선택(`null` = 자동). 결과(`hasDitch`)와 다른 값이다. */
ditchChoice: boolean | null;
ditchPoints: Array<[number, number]> = [];
private groundAt: ((offsetM: number) => number) | null;
private rockOffset: number;
@@ -127,7 +129,10 @@ export class SectionGeometry {
soilCutRatio: number | null;
rockBoundaryOffsetM: number | null;
twoStageSlope: boolean;
/** 옛 저장분의 결과값 — **자동값과 다를 때만** 선택으로 살린다. */
ditchEnabled: boolean | null;
/** 사용자가 정한 선택(없으면 자동). */
ditchChoice?: boolean | null;
/** 곡선부 확폭(m) — 붙는 쪽만 값이 있고 반대쪽은 0이다. */
wideningLeftM?: number;
wideningRightM?: number;
@@ -167,17 +172,33 @@ export class SectionGeometry {
groundAt(-this.rightExtent) > this.roadZ(-this.rightExtent) + 1e-3 ? "cut" : "fill";
}
// 측구 생성 여부(D-1).
// 측구 생성 여부(D-1) — **자동 판정이 먼저, 사용자 선택이 그 위**(2026-09-09 정리).
//
// ⚠⚠ **한 칸에 두 뜻이 담겨 있던 자리다**(짝: 파이썬 `_SectionGeometry`). 결과
// (`ditch_enabled` = 실제 생성됨)를 그대로 다시 입력으로 넣어 읽었으므로 **한 번
// 저장되면 자동 판정이 영영 다시 안 돌았다.** 계획고를 내려 절토가 생겨도 측구가
// 안 서고 아무 말도 안 나왔다. ⇒ **선택은 `ditchChoice`**(없음 = 자동), **결과는
// `hasDitch`** 로 가른다.
// ⚠ 옛 저장분은 `ditchEnabled` 로 온다. 그 값은 **자동값과 다를 때만 뜻이 있다** —
// 같으면 자동이 그렇게 냈던 것이고, 다르면 사용자가 일부러 바꾼 것이다.
let autoDitch: boolean;
if (params.sectionMode === "both_fill") {
this.hasDitch = false;
} else if (params.ditchEnabled !== null && params.ditchEnabled !== undefined) {
this.hasDitch = params.ditchEnabled;
autoDitch = false;
} else if (groundAt !== null) {
const ditchEdge = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent;
this.hasDitch = groundAt(ditchEdge) > this.roadZ(ditchEdge) + 1e-3;
autoDitch = groundAt(ditchEdge) > this.roadZ(ditchEdge) + 1e-3;
} else {
this.hasDitch = true;
autoDitch = true; // groundAt 이 없으면 보수적으로 생성
}
let choice = params.ditchChoice ?? null;
const legacy = params.ditchEnabled;
if (choice === null && legacy !== null && legacy !== undefined) {
choice = Boolean(legacy) === autoDitch ? null : Boolean(legacy);
}
// 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저다.
this.hasDitch = choice === null || params.sectionMode === "both_fill" ? autoDitch : choice;
/** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */
this.ditchChoice = choice;
// 측구 꼭짓점(측구측 노면 끝 기준, 바깥 방향 부호 적용).
const edgeOffset = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent;