Files
Aislo/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts
T
eomsangdonandClaude Opus 5 1e55322f2f feat(B06): 다단 기슭막이 구간값을 단별로 손수 잡는다
다단은 소유 벽 연장에 종속이라 길이·전/후를 따로 못 잡았다. 계곡부·능선부에서는
아래 단일수록 연장이 달라져 자동 규칙으로 못 잡으므로 수동 입력으로 간다.
조정창 UI는 기준벽 것을 그대로 쓴다(길이·기준측점 전·후 세 행).

- Const: EXTRA_SPAN_DEFAULT(10m·5/5) + applySpanPatch()로 길이↔전/후 산식을
  기준벽과 공용화
- Station_Controls: StructureSpanControl에 tierValuesFor/updateTier 추가. 값은
  세션(b06:extraspan)에 담고 캐시(design.extra_spans)에도 얹는다. 단 수를 줄이면
  그 단의 구간값도 지운다
- View_Structure: 다단 키면 단별 제어기로, 기준벽은 종전 경로
- Wire: culvertReach에 단별 연장을 더하고, 링크 카드에서 각 단을 자기 구간값으로
  판정한다(tierSpanOf)
- Corridor_Structures: 3D 스윕 범위를 단별 구간값으로
- 저장: CrossSectionPatch.extra_spans(프론트 payload + WallSpanPatch 스키마 +
  확정 병합) — [저장]·[확정] 때 정본에 남는다

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

352 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B06_Section_UI_Cross_Culvert_Const.ts
* 배수관 세트 **치수 상수·형태별 한계** — 기하 계산(`_Cross_Culvert_Geom.ts`)에서
* 분리했다(700줄 제한). 값의 근거는 각 상수 주석에 지식DB 출처로 남긴다.
* ========================================================================== */
import type { CrossSection, CulvertSideSpec } from "./B06_Section_Api_Fetch";
import type { SpanRole, SpanValues } from "./B06_Section_UI_Cross_Culvert_Wire";
/** 관 위 최소 토피(m) — 프론트 정의처(백엔드는 `B06_Section_Engine_Culvert.MIN_PIPE_COVER_M`).
* ⚠ 임도 배수관 토피 규정이 지식DB에 없어 별표2 교량·암거 "복토 50㎝ 이상"을 교차
* 참조로 채택(2026-08-19 사용자 확정). B05 배수관 측점 계획고 하향 차단 기준이자,
* 절토측 유입 벽에서 관 invert의 상한(노면 − 관경 − 토피)을 정한다. */
export const MIN_PIPE_COVER_M = 0.5;
/** 기슭막이 벽 두께(m) — 실무 견치돌 뒷길이 관측치 45㎝(울진 L3=45). 표시용 형상 값. */
export const REVET_THICKNESS_M = 0.45;
/** 기슭막이 전면 기울기(1:n) — 돌쌓기 전면 1:0.3(교본 7-3). 벽이 사면 쪽으로 기운다. */
export const REVET_LEAN_RATIO = 0.3;
/** 돌붙임 두께(m) — 실무 돌붙임 L3=45(울진 1공구 수량집계표). */
export const PITCHING_THICKNESS_M = 0.45;
/** 사다리꼴(배면부) 상단 폭(m) — 띠 두께의 절반(2026-08-20 사용자 확정). */
export const REVET_TRAP_TOP_M = REVET_THICKNESS_M / 2;
/**
* 기슭막이 여유고(m) — 관 상단에서 벽 상단까지의 간격. 유입·유출 **공통**이다.
*
* ⚠ 확정값 아님(2026-08-21 사용자 지정). 지식DB에 임도 배수관 기슭막이 여유고
* 규정이 없다(`통수단면.md` §여유고 — "임도·KDS 원문에 수치 없음"). 관 상단 최소
* 토피 0.5m와 같은 값을 잠정 채택했고, 추후 **사용자 입력값**으로 교체 예정이다.
*
* 이 값 때문에 벽 높이는 좌·우가 같은 규칙으로 정해진다 — 관이 경사져 벽 밑(invert)
* 이 서로 달라도 관 상단 위 간격은 항상 같다.
*/
export const REVET_FREEBOARD_M = 0.5;
/** 기슭막이 높이 눈금(m) — 계산 높이를 이 단위로 올린다(2026-08-21 사용자 확정). */
export const REVET_HEIGHT_STEP_M = 0.1;
/** 관경에 여유고를 더해 눈금(0.1m)으로 올린 기슭막이 높이(m). */
export function revetTargetHeight(diameterM: number): number {
const raw = diameterM + REVET_FREEBOARD_M;
return Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M;
}
/**
* 관 벽 두께(m) — 관종별. 지식DB 조회(2026-08-20):
* · 파형강관: 강판 두께 **1.6T~3.2T**(교본 부록 4-6 §2.5.2, 표-1 표준형 강관 치수,
* KS D 3590). Ø1000 이상은 1.6~3.2 전 규격 사용 — 중간값 2.7T 채택.
* · 흄관·VR관(원심력 철근콘크리트관): 관경별 벽 두께 표가 지식DB에 없어
* 관경의 **1/12**을 적용한다(⚠ 근거 미확정 — KS F 4403 확보 시 교체).
* 도면상 내경·외경 2줄을 긋기 위한 값이다.
*/
export function pipeWallThicknessM(kind: string | null, diameterM: number): number {
if (kind && kind.includes("파형강관")) return 0.0027;
return Math.max(diameterM / 12, 0.05);
}
/**
* 기슭막이 근입 깊이(m) — 벽 바닥이 원지반 아래로 묻히는 깊이.
*
* 지식DB 조회 결과(2026-08-20):
* · 기슭막이 자체의 근입 깊이 **규정 없음** — 사방교본은 "기초 세굴 위험 시
* 밑다짐·바닥막이·수제 병행"만 정한다(`00_구조물_선정기준.md` §4).
* · 흙막이: "하부 땅밀림 방지 위해 **본바닥(원지반)에 기초**"(`흙막이.md`) —
* 원지반에 묻어야 한다는 원칙만 있고 수치는 없다.
* · 실무(울진소광 구조도): 돌기슭막이 **기초형 = 기초콘크리트 H=0.5**,
* 깬돌 3=45·1:0.3 (`_원단위라이브러리_울진소광.md` §4).
* → 실무 기초콘크리트 높이 0.5m를 근입 깊이로 채택한다. 임도 법정 확정값이 아니라
* **실무 관측 기반**이므로 확정 기준 확보 시 교체한다.
*/
export const REVET_EMBED_DEPTH_M = 0.5;
/**
* 기슭막이 형태별 높이 한계(m) — 지식DB `돌쌓기.md` §1 / `흙막이.md` §2:
* 돌 **찰쌓기 3.0m 이하**, 돌 **메쌓기 2.0m 이하**(교본 7-3 돌흙막이 기준),
* 돌망태 2m 정도. 큰돌쌓기는 총높이 한계 없음(1일 시공 1.5m 제한만).
* 초과 시 콘크리트 옹벽 등 상위 구조물 검토 대상이다.
* 통나무·목재틀·바자는 지식DB에 수치가 없어 **2.0m**로 둔다(2026-08-29 사용자 확정).
*/
export function revetHeightLimit(form: string | null | undefined): number {
if (!form) return 3.0;
if (form.includes("메")) return 2.0;
if (form.includes("찰")) return 3.0;
if (form.includes("돌망태")) return 2.0;
if (form.includes("통나무") || form.includes("목재") || form.includes("바자")) return 2.0;
if (form.includes("콘크리트")) return 5.0; // 프로젝트 확정값 — materialLimit과 동일
return 3.0;
}
/* ── 기슭막이 재질·높이 조작(2026-08-22 사용자 확정) ─────────────────────
* 높이 조작이 교본 형태별 한계와 부딪히면 **재질을 바꿔야** 더 올릴 수 있다:
* 메쌓기 2.0 / 찰쌓기 3.0(돌쌓기.md §1) / 콘크리트 5.0(프로젝트 기본값,
* 외부 기준 미확인). 높이 제어·표기·한계 기준은 모두 **순수 높이**(벽 바닥~상단,
* 근입 0.5m 포함) — 배관 기슭막이 2026-08-23, 추가·독립 기슭막이 2026-08-29
* 사용자 확정으로 기준을 하나로 맞췄다. */
export type RevetMaterial = "dry" | "wet" | "concrete";
/**
* 기슭막이 **형태** — B05 폼(`B05_Profile_UI_Drainage_Facility_Fields`)과 같은 한 벌이다.
* 조정창 선택지도 이 목록을 쓴다(2026-08-29 사용자: "B05 폼 형태로 가는 게 맞다").
* 높이 한계는 `revetHeightLimit(form)`, 도형의 돌 표현은 `materialFromForm(form)`이 정한다.
*/
export const REVET_FORMS = [
"돌쌓기(찰)",
"돌쌓기(메)",
"콘크리트",
"돌망태",
"통나무·목재틀",
"바자",
];
/** 형태 미지정 시 기본 — B05 독립 기슭막이 폼 기본값과 같다. */
export const REVET_FORM_DEFAULT = "돌쌓기(메)";
/**
* 조작값 `m`을 형태 문자열로 읽는다. `m`은 2026-08-29부터 **형태**를 담지만, 그 전
* 저장분은 재질 코드(dry/wet/concrete)라 그 셋은 대응 형태로 옮긴다. 조작이 없으면
* 구조물 정본의 형태(`spec.revet_form`)를 따른다.
*/
export function revetFormOf(m: string | null | undefined, specForm?: string | null): string {
if (m === "dry") return "돌쌓기(메)";
if (m === "wet") return "돌쌓기(찰)";
if (m === "concrete") return "콘크리트";
return m || specForm || REVET_FORM_DEFAULT;
}
export const REVET_MATERIALS: RevetMaterial[] = ["dry", "wet", "concrete"];
export function materialLimit(material: RevetMaterial): number {
return material === "dry" ? 2.0 : material === "wet" ? 3.0 : 5.0;
}
export function materialLabel(material: RevetMaterial): string {
return material === "dry" ? "메쌓기" : material === "wet" ? "찰쌓기" : "콘크리트";
}
/** 백엔드 spec 형식 문자열("돌쌓기(메)" 등) → 재질 기본값. 기본은 메쌓기. */
export function materialFromForm(form: string | null | undefined): RevetMaterial {
if (form?.includes("찰")) return "wet";
if (form?.includes("콘크리트")) return "concrete";
return "dry";
}
/**
* 벽 한 매의 제원(형태·재질·높이) — 배관 벽과 독립 기슭막이가 같이 쓴다.
*
* 조정창이 고르는 값은 **형태**(B05 폼과 같은 6종 — 2026-08-29 사용자)이고, 재질(돌
* 표현)과 높이 한계는 형태에서 파생된다. 높이 기준은 **순수 높이**(바닥~상단, 근입
* 0.5m 포함). 독립 기슭막이(관 숨김)는 관경이 없어 하한이 근입뿐이고 기본 높이는
* 사용자가 고른 형태 높이(`revet_height_m`)다 — 배관은 관이 벽 밖으로 삐져나오지
* 않도록 관경 기준 최소 높이를 하한·기본으로 쓴다.
*/
export function revetWallSpec(
spec: CulvertSideSpec,
adjust: { h?: number | null; m?: string | null },
hiddenPipe: boolean,
diameterM: number,
): { form: string; material: RevetMaterial; limit: number; pureHeight: number; height: number } {
const form = revetFormOf(adjust.m, spec.revet_form);
const limit = revetHeightLimit(form);
const floor = hiddenPipe ? REVET_EMBED_DEPTH_M : pipeWallMinPureHeight(diameterM);
const preferred = hiddenPipe ? (spec.revet_height_m ?? floor) : floor;
const pureHeight = Math.min(Math.max(adjust.h ?? preferred, floor), Math.max(limit, floor));
return {
form,
material: materialFromForm(form),
limit,
pureHeight,
height: pureHeight - REVET_EMBED_DEPTH_M,
};
}
/** 관 접속 절·성토선 물매 — 0도 대신 3도(2026-08-22 사용자). 방향은 물 흐름을
* 따른다: 유입측은 계류 쪽이 높고(물이 관으로), 유출측은 계류 쪽이 낮다(관에서 밖으로). */
export const PIPE_CONNECT_GRADE = Math.tan((3 * Math.PI) / 180);
/**
* 배관 기슭막이 관 하부 여유(m) — 관 invert 아래 벽 바닥까지의 깊이(2026-08-23
* 사용자 확정: 배관 기준 아래 0.5·위 0.5 여유). 값은 근입 깊이와 같은 0.5라
* 형상은 종전과 동일하고, **순수 높이 하한 = 관경 + 위 0.5 + 아래 0.5**가 된다.
* 종전 고정 하한·기본 2.0m(관경 무관)는 폐기.
*/
export function pipeWallMinPureHeight(diameterM: number): number {
return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;
}
/** 일반(추가) 기슭막이 높이 조작 하한(m) — 프로젝트 기본값 0.5m. */
export const EXTRA_WALL_MIN_HEIGHT_M = 0.5;
/** 일반(추가) 기슭막이 기본 높이(m). */
export const EXTRA_WALL_DEFAULT_HEIGHT_M = 1.5;
/**
* 성토 비탈 기울기(1:n) 허용 범위 — 지식DB `성토_비탈면.md` §1:
* "**1:1.2~2.0** 범위에서 토질·용수 등 지형여건을 종합 고려해 설정".
* 1.2는 **범위의 급한 끝**이지 고정값이 아니다 — 배수관 구조물이 서는 측점에서는
* 사면선이 기슭막이 이음선 상단점을 지나도록 이 범위 안에서 물매를 정한다
* (2026-08-21 사용자 지시).
*/
export const FILL_SLOPE_RATIO_MIN = 1.2;
export const FILL_SLOPE_RATIO_MAX = 2.0;
/** 성토사면 길이 한계(m) — `성토_비탈면.md` §2 "5m 이내". 초과 시 옹벽·석축 등 의무. */
export const FILL_SLOPE_MAX_LENGTH_M = 5.0;
/**
* 배관 기슭막이 기본 자리 — 노견에서 **성토사면 경사길이 4m 지점**(2026-08-23 사용자
* 확정). 상하(d) 조작의 0점은 성토선 길이 0 지점(벽 상단 = 노견)이고, d는 그 점에서
* 1:1.2 성토선을 타고 내려간 수평 성분(m)이다. 기본 자리가 안 되는 지형(매몰·역경사·
* 토피)은 배치 풀이의 기존 한계 논리가 안쪽으로 되돌린다.
*/
export const PIPE_WALL_DEFAULT_SLOPE_M = 4.0;
/** 기본 자리(경사길이 4m)의 수평 성분(m) — d 기본값. */
export const PIPE_WALL_DEFAULT_RUN_M =
(PIPE_WALL_DEFAULT_SLOPE_M * FILL_SLOPE_RATIO_MIN) / Math.hypot(1, FILL_SLOPE_RATIO_MIN);
/**
* 성토 전체고(노견 − 원지반) 기준(m) — 이 높이를 넘으면 1:1.2로도 사면이 5m를 넘어
* 구조물이 필요해진다(3m × √(1+1.2²) = 4.69m, 3.2m에서 5.0m). 기슭막이를 자연 사면
* 끝이 아니라 **사면길이 5m 지점**으로 끌어올리는 판정에 쓴다(2026-08-21 사용자 지시).
*/
export const FILL_STRUCTURE_HEIGHT_M = 3.0;
/**
* 성토 사면이 성립하는 **최소 성토고**(m) — 노견 − 벽 이음선 상단.
*
* 이보다 낮은 자리에 벽을 세우면 사면선이 노견에서 벽 상단으로 **올라가** 역전된다
* (2026-08-21 사용자 지적). 자동 배치는 물매 조건이 이미 막지만, 조정창으로 손수
* 미는 경우는 이 하한으로 막는다.
*/
export const FILL_MIN_RISE_M = 0.3;
/**
* 집수정 바닥 여유(m) — **관 하단 아래**로 두는 깊이. 상부 여유는 기슭막이와 같은
* `REVET_FREEBOARD_M`(0.5)을 쓴다. 집수정 깊이 = 바닥 여유 + 관경 + 상부 여유
* (2026-08-21 사용자 확정 — 종전에는 성토 경사선과의 교점으로 높이를 잡았다).
*/
export const BASIN_BOTTOM_CLEARANCE_M = 0.5;
/**
* 집수정 내부 높이(m) — 바닥판이 있는 ㄴ(L)·ㄷ형의 **바닥 윗면~벽 상단** 깊이.
* 프로젝트 기본값 1.2m(2026-08-22 사용자 확정). 외부 기준은 확인되지 않았다.
* I형(공유벽뿐)은 이 값을 쓰지 않고 노견 끝 접점까지 벽을 키운다.
*/
export const BASIN_INNER_HEIGHT_M = 1.2;
/**
* 집수정 전환 기준 성토사면 길이(m) — 유입측 성토사면이 이 길이 **이하**면 기슭막이
* +배관을 두기 어려워 집수정이 기본이 된다(2026-08-22 사용자 확정). 절토측 유입은
* 성토사면이 없으므로 자동 포함. 사용자 선택(기슭막이 유지)은 향후 추가 예정.
*/
export const BASIN_MAX_FILL_SLOPE_M = 3.0;
/** 집수정 부재(벽·바닥판) 두께(m) — 2026-08-22 사용자 지정 0.2. */
export const BASIN_MEMBER_THICKNESS_M = 0.2;
/* ---------------------------------------------------------------------------
* 구조물의 종방향 점유 구간(기준측점 전/후 m)
*
* 기슭막이 연장(길이·전/후)과 집수정 연장을 **한 곳에서** 정한다 — B06 링크 판정
* (`_Cross_Culvert_Wire.ts`)과 3D 스윕(`B05_Profile_UI_Corridor_Structures.ts`)이
* 서로 다른 값을 쓰면 2D와 3D가 어긋난다(2026-08-24).
* ------------------------------------------------------------------------- */
/** 기준측점 전/후 몫(m). */
export interface StructureSpan {
beforeM: number;
afterM: number;
}
/** 길이 + 전/후 저장분에서 구간을 확정 — 전/후가 없으면 길이 절반씩. */
function spanFrom(
lengthM: number,
beforeM: number | null | undefined,
afterM: number | null | undefined,
): StructureSpan {
const before = Math.max(beforeM ?? lengthM / 2, 0);
const after = Math.max(afterM ?? Math.max(lengthM - before, 0), 0);
return { beforeM: before, afterM: after };
}
/** 기슭막이 한 벽의 종방향 구간 — 기본 10m(전 5·후 5). */
export function revetSpanOfSpec(spec: CulvertSideSpec | undefined): StructureSpan {
return spanFrom(spec?.revet_length_m ?? 10, spec?.revet_before_m, spec?.revet_after_m);
}
/** 집수정의 종방향 구간 — 기본 길이 2m, 전/후 각 1m(2026-08-24 사용자). */
export function basinSpanOfSpec(spec: CulvertSideSpec | undefined): StructureSpan {
const length = Math.max(spec?.basin_length_m ?? 2, 0.1);
return spanFrom(length, spec?.basin_before_m, spec?.basin_after_m);
}
/** 역할별 옵션 키 — 배수관 레지스트리 옵션 이름 그대로다. */
export const SPAN_OPTION_KEYS: Record<SpanRole, { length: string; before: string; after: string }> =
{
inlet: {
length: "inlet_revet_length_m",
before: "inlet_revet_before_m",
after: "inlet_revet_after_m",
},
outlet: {
length: "outlet_revet_length_m",
before: "outlet_revet_before_m",
after: "outlet_revet_after_m",
},
basin: {
length: "inlet_basin_length_m",
before: "inlet_basin_before_m",
after: "inlet_basin_after_m",
},
};
/**
* 다단(추가) 기슭막이 한 단의 **기본 구간값** — 고정 10m(전 5·후 5, 2026-08-29 사용자
* 확정). 기준벽 연장을 상속하지 않는다: 계곡부·능선부에서 아래 단일수록 연장이 달라져
* 자동 규칙으로 못 잡고 사용자가 손으로 넣는다.
*/
export const EXTRA_SPAN_DEFAULT: SpanValues = { lengthM: 10, beforeM: 5, afterM: 5 };
/**
* 구간값 한 벌에 조작을 얹는다 — 기준벽·다단이 같은 산식을 쓴다.
* 길이를 바꾸면 **늘어난 몫만** 지금 비율대로 나눠 담는다(매번 총길이에서 비율로 다시
* 계산하면 0.1m 반올림이 쌓여 5.0/5.0이 5.6/5.4로 어긋난다 — 2026-08-24 화면 실측).
* 전·후를 바꾸면 길이는 둘의 합이다.
*/
export function applySpanPatch(current: SpanValues, patch: Partial<SpanValues>): SpanValues {
let { beforeM, afterM } = current;
if (patch.lengthM !== undefined) {
const total = Math.max(patch.lengthM, 0);
const delta = total - current.lengthM;
const ratio = current.lengthM > 1e-9 ? current.beforeM / current.lengthM : 0.5;
beforeM = Math.max(Math.round((current.beforeM + delta * ratio) * 10) / 10, 0);
afterM = Math.max(Math.round((total - beforeM) * 10) / 10, 0);
}
if (patch.beforeM !== undefined) beforeM = Math.max(patch.beforeM, 0);
if (patch.afterM !== undefined) afterM = Math.max(patch.afterM, 0);
return { lengthM: Math.round((beforeM + afterM) * 10) / 10, beforeM, afterM };
}
/** 소유 측점의 구간값(길이·전/후) — 저장 옵션과 기본값을 함께 푼 결과. */
export const spanValuesOf = (owner: CrossSection, role: SpanRole): SpanValues | null => {
const spec = role === "outlet" ? owner.culvert?.outlet : owner.culvert?.inlet;
if (!spec) return null;
const span = role === "basin" ? basinSpanOfSpec(spec) : revetSpanOfSpec(spec);
return {
lengthM: Math.round((span.beforeM + span.afterM) * 10) / 10,
beforeM: Math.round(span.beforeM * 10) / 10,
afterM: Math.round(span.afterM * 10) / 10,
};
};