Files
Aislo/B06_Section/B06_Section_Cut_Slope_Check.ts
T
eomsangdonandClaude Opus 5 1e596846cd feat(B06): 절토 경사 판정 암질을 설계자가 고르게 — 기본 경암
사용자 확정(2026-09-07): 「연암과 경암도 별도의 버튼으로 선택하게 하고 경암을 기본값으로
선택. 리핑을 할지 발파를 할지는 설계자가 선택 필요함.」

**축이 둘이었음 — 섞지 말 것.**
· **암질**(연암·경암) = 별표2 경사 판정의 기준. 설계자가 고름. 기본 **경암**.
· **굴착 공법**(리핑암·발파암, `cut_rock_kind`) = 어떻게 파는가. 수량·단가 몫.
처음에는 공법에서 암질을 유추하려 했는데 **그것이 잘못 세운 문제**였음. 매핑을 걷어내고
암질 선택 하나로 바꿈 — 표준 횡단면 설정 안, 자리는 그대로.

- config 매핑 상수 → `FOREST_ROAD_CUT_SLOPE_ROCK_QUALITY_DEFAULT = "hard_rock"`.
- 판정에서 `cut_rock_kind` 를 뗌. 수량 쪽 쓰임은 그대로 둠.
- 실측(용화 63측점·구간 124개) — 기본값 경암에서 **위반 0건**. config 기본 절토비 1:0.4 가
  경암 범위(0.3~0.8) 안임. 토사 63구간도 0건. **기존 설계·수량 안 바뀜.**
  (연암으로 고르면 61건 — 1:0.4 가 연암 하한 0.5 밖이라 그때는 설계 검토가 필요함.)

시험 8건 — 별표2 값 · 작업임도 제외 · **암질은 설계자가 고르고 기본은 경암** ·
**공법으로 암질을 추론하지 않음** · 구간별 판정 · 소단이 있어도 위반이 안 사라짐 ·
경계값 통과 · 카드에 같은 방식으로 붙는지.

7-3 회귀에 소단 한 줄 더함 — 「소단은 값이 아니라 **기하 입력**이라 계산 뒤에 베껴 붙이면
`berm` 값만 남고 계단이 안 그려진다」. 저장분 소단을 **계산 전에** 읽어 넣는 순서를 지킴.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 16:42:32 +09:00

138 lines
5.9 KiB
TypeScript

/* =============================================================================
* B06_Section_Cut_Slope_Check.ts
* 절토 비탈 **법정 기울기 검사**(별표2) — 순수 판정만 둔다(그리기는 카드 몫).
*
* 왜 필요한가 — 평면 곡선반경도, 성토사면 길이(5m·기슭막이 의무)도 위반 표시가 있는데
* **절토 경사만 검사가 없었다**(2026-09-07). 절토 경사비는 표준 횡단면 설정에서 오는
* 입력값일 뿐이라 별표2 범위를 벗어나도 아무 표시가 없었다.
*
* ⚠ **실효 경사로 보면 안 된다.** 소단이 서면 사면 전체를 하나로 잰 경사가 완만해져
* **위반이 사라진 것처럼** 보인다(실측: 폭 1.0·간격 2 이면 설계 1:1 이 실효 1:1.71).
* 그래서 `design.cut_slope_segments`(소단을 뺀 **구간별** 경사)를 읽는다.
*
* 기준값·매핑은 **서버가 준 값**을 쓴다(config 상수를 화면에 복제하지 않는다).
* ========================================================================== */
import type { CrossSection, SectionContextResponse } from "./B06_Section_Api_Fetch";
/** 별표2 한 줄 — 수직 1 에 대한 수평(1:n 의 n) 범위. */
export interface CutSlopeLimit {
min: number;
max: number;
}
export interface CutSlopeCriteria {
/** 별표2 범위표 — `hard_rock`·`soft_rock`·`soil`. */
limits: Record<string, CutSlopeLimit>;
/** 절토 경사 판정에 쓸 **암질** — `hard_rock`(경암) / `soft_rock`(연암). 설계자가 고른다.
* ⚠ 굴착 공법(리핑암·발파암)과 다른 축이라 공법에서 추론하지 않는다(2026-09-07 사용자 확정). */
rockQuality: string;
/** 절토 기울기 규정이 없는 등급(작업임도). */
exemptGrades: ReadonlyArray<string>;
}
export interface CutSlopeViolation {
side: string;
ratio: number;
/** 어느 별표2 줄로 판정했나 — 툴팁에 적는다. */
limitKey: string;
limit: CutSlopeLimit;
/** 급한 쪽 위반인지(`steep`) 완만한 쪽 위반인지(`gentle`). */
kind: "steep" | "gentle";
startOffsetM: number;
endOffsetM: number;
}
/** 서버 컨텍스트에서 기준을 꺼낸다. 값이 안 왔으면 null — 검사를 하지 않는다. */
export function criteriaFrom(context: SectionContextResponse | null): CutSlopeCriteria | null {
const limits = context?.cut_slope_limits;
if (!limits || !Object.keys(limits).length) return null;
const table: Record<string, CutSlopeLimit> = {};
for (const [key, pair] of Object.entries(limits)) {
if (Array.isArray(pair) && pair.length >= 2) table[key] = { min: pair[0], max: pair[1] };
}
return {
limits: table,
rockQuality: context?.cut_slope_rock_quality_default ?? "hard_rock",
exemptGrades: context?.cut_slope_exempt_grades ?? [],
};
}
/**
* 이 측점의 절토 구간이 별표2 범위를 벗어났는지.
*
* · 등급이 면제(작업임도)면 빈 목록.
* · 구간이 없으면(성토 측점 등) 빈 목록.
* · 암 구간은 **설계자가 고른 암질**로 판정한다 — 굴착 공법(`cut_rock_kind`)은 보지 않는다.
*/
export function cutSlopeViolations(
section: CrossSection,
criteria: CutSlopeCriteria | null,
gradeClass: string | null,
): CutSlopeViolation[] {
if (!criteria) return [];
if (gradeClass && criteria.exemptGrades.includes(gradeClass)) return [];
const design = section.design as { cut_slope_segments?: unknown } | undefined;
const segments = design?.cut_slope_segments;
if (!Array.isArray(segments) || !segments.length) return [];
const rockKey = criteria.rockQuality || "hard_rock";
const out: CutSlopeViolation[] = [];
for (const raw of segments) {
const segment = raw as {
side?: string;
ratio?: number;
material?: string | null;
start_offset_m?: number;
end_offset_m?: number;
};
const ratio = segment.ratio;
if (typeof ratio !== "number" || !Number.isFinite(ratio)) continue;
const limitKey = segment.material === "rock" ? rockKey : "soil";
const limit = criteria.limits[limitKey];
if (!limit) continue;
// 경계값은 통과 — 1:0.8 은 「0.3~0.8」 안이다.
if (ratio >= limit.min - 1e-9 && ratio <= limit.max + 1e-9) continue;
out.push({
side: segment.side ?? "",
ratio,
limitKey,
limit,
kind: ratio < limit.min ? "steep" : "gentle",
startOffsetM: segment.start_offset_m ?? 0,
endOffsetM: segment.end_offset_m ?? 0,
});
}
return out;
}
/** 별표2 줄 이름 — 화면·툴팁에 적는 말. */
export function limitLabel(key: string): string {
if (key === "hard_rock") return "경암";
if (key === "soft_rock") return "연암";
return "토사";
}
/* ── 기억해 둔 기준 ──────────────────────────────────────────────────────────
* 카드는 그리는 자리마다 컨텍스트를 들고 있지 않다. 표준단면 기본값을 기억해 두는 것과
* 같은 방식으로(`rememberStandardDefaults`) 여기 한 번 담아 두고 카드가 읽는다.
* ------------------------------------------------------------------------ */
let remembered: CutSlopeCriteria | null = null;
let rememberedGrade: string | null = null;
export function rememberCutSlopeCriteria(context: SectionContextResponse | null): void {
remembered = criteriaFrom(context);
rememberedGrade = context?.road_type ?? null;
}
/** 설계자가 고른 암질을 얹는다. 없으면 기본값(경암) 그대로. */
export function applyRockQualityChoice(quality: string | null | undefined): void {
if (!remembered || !quality) return;
remembered = { ...remembered, rockQuality: quality };
}
/** 지금 측점의 위반 목록 — 기준이 없거나 면제 등급이면 빈 목록. */
export function violationsOf(section: CrossSection): CutSlopeViolation[] {
return cutSlopeViolations(section, remembered, rememberedGrade);
}