feat(B06): 구조물 목록의 C군 벽을 횡단 면적에 반영

좌측 「구조물 배치」로 넣은 옹벽·돌쌓기 같은 벽이 지금까지 측점만 심고 기하가 없어
절·성토 면적이 그대로였음(2026-09-06 사용자 확정: 반영해야 함).

- 독립 기슭막이가 쓰던 `section.revetment` 제원 자리에 그대로 얹음 — 기하·설계선
  트림·폐회로 면적·3D 가 손대지 않고 따라옴.
- 서버 `B06_Section_Engine_Structures_Wall.py`(저장분) ↔ 브라우저
  `common_util_structure_walls.ts`(미저장 초안) 짝 + 거울 테스트.
- 설치 측 칸이 없는 C군은 성토가 나는 쪽으로 자동 배치, 양측 절토면 세우지 않음.
- 구조물 목록이 바뀌면 B06 이 초안을 얹어 다시 그림.

검증: 40m 돌쌓기(찰) 추가 시 그 측점 성토 5.81 → 5.02㎡. 테스트 390 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 18:59:55 +09:00
co-authored by Claude Opus 5
parent cdb9799e64
commit 1d4568b92f
6 changed files with 302 additions and 16 deletions
+115
View File
@@ -0,0 +1,115 @@
/* =============================================================================
* common_util_structure_walls.ts
* 구조물 정본(C군 사면안정 벽)을 횡단 측점 제원으로 바꾸는 자리 —
* 파이썬 `B06_Section_Engine_Structures_Wall.py` 의 **짝**이다(거울 테스트로 대조).
*
* 왜 두 벌인가(CLAUDE.md 5장) — 서버는 상세를 내려보낼 때 얹어야 하고(저장분 기준),
* 브라우저는 사용자가 **아직 저장하지 않은** 목록으로 즉시 얹어야 한다. 값을 만드는
* 산식이 같아야 하므로 두 파일 머리에 짝임을 적고 거울 테스트를 둔다.
*
* 얹는 것은 제원뿐이다 — 도형·면적은 `B06_Section_UI_Cross_Revetment` 가 그린다.
* ========================================================================== */
/** 구조물 정본 한 건(필요한 칸만). `structures.json` 과 같은 이름을 쓴다. */
export interface WallStructureInput {
structure_id?: string | null;
type_id: string;
placement?: string | null;
chainage_m: number;
start_m?: number | null;
end_m?: number | null;
options?: Record<string, unknown> | null;
}
/** 측점에 얹는 벽 제원 — `section.revetment` 와 같은 꼴. */
export interface WallSpec {
structure_id: string | null;
type_id: string;
name: string;
start_m: number;
end_m: number;
anchor_m: number;
form: string | null;
height_m: number | null;
side: string | null;
tiers: number | null;
lift_m: number | null;
shift_m: number | null;
}
/** 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 파이썬 `_EDGE_TOLERANCE_M`. */
const EDGE_TOLERANCE_M = 0.02;
/** 구조물 종류 → 횡단 기하가 아는 형태 이름 — 파이썬 `_FORM_BY_TYPE` 와 같은 표. */
export const FORM_BY_TYPE: Record<string, string> = {
masonry_wet: "돌쌓기(찰)",
masonry_dry: "돌쌓기(메)",
boulder_masonry: "돌쌓기(메)",
retaining_wall: "콘크리트",
soil_guard: "통나무·목재틀",
};
const num = (value: unknown): number | null =>
typeof value === "number" && Number.isFinite(value) ? value : null;
/**
* C군 벽 구조물을 제원 목록으로 바꾼다. 이름표(`names`)는 타입 레지스트리에서 온다.
* 구간(start·end)이 없는 항목은 건너뛴다 — 벽은 구간형이다.
*/
export function wallSpecsFrom(
structures: readonly WallStructureInput[],
names: ReadonlyMap<string, string>,
): WallSpec[] {
const specs: WallSpec[] = [];
for (const structure of structures) {
const name = names.get(structure.type_id);
if (!name) continue;
const start = num(structure.start_m);
const end = num(structure.end_m);
if (start === null || end === null) continue;
const options = (structure.options ?? {}) as Record<string, unknown>;
specs.push({
structure_id: structure.structure_id ?? null,
type_id: structure.type_id,
name,
start_m: Math.min(start, end),
end_m: Math.max(start, end),
anchor_m: num(structure.chainage_m) ?? Math.min(start, end),
form: (options.form as string) || FORM_BY_TYPE[structure.type_id] || null,
height_m: num(options.height_m),
side: (options.side as string) ?? null,
tiers: num(options.tiers),
lift_m: num(options.lift_m),
shift_m: num(options.shift_m),
});
}
return specs;
}
/**
* 구간 안 측점에 제원을 얹는다(파이썬 `attach_wall_structures`). 얹은 개수를 돌려준다.
* 관 세트가 이미 붙은 측점은 건드리지 않는다 — 한 자리에 두 벽이 겹치면 읽히지 않는다.
*/
export function attachWallSpecs(
sections: Array<Record<string, unknown>>,
specs: readonly WallSpec[],
): number {
if (!specs.length) return 0;
let attached = 0;
for (const section of sections) {
const chainage = num(section.chainage_m);
if (chainage === null) continue;
if (section.culvert || section.revetment) continue;
for (const spec of specs) {
if (
spec.start_m - EDGE_TOLERANCE_M <= chainage &&
chainage <= spec.end_m + EDGE_TOLERANCE_M
) {
section.revetment = spec;
attached += 1;
break;
}
}
}
return attached;
}