법정 위반 표시가 없던 유일한 자리였음(평면 곡선반경·성토사면 길이는 이미 있음). 절토 경사비는 표준 횡단면 설정에서 오는 입력값일 뿐이라 범위를 벗어나도 아무 표시가 없었음. - 기준 — 별표2(지식DB `01_임도/02_상세설계/절토_비탈면.md` §1): 경암 1:0.3~0.8 · 연암 1:0.5~1.2 · 토사 1:0.8~1.5. **작업임도는 규정 없음 → 검사 제외.** - 판정은 **구간별 경사**(`cut_slope_segments`)로 함. 소단이 서면 실효 경사가 완만해져 **위반이 사라진 것처럼** 보임(폭 1.0·간격 2 이면 1:1 이 1:1.71). 그 함정을 시험으로 못박음. - 표시는 **기존 방식 그대로** — 성토사면·미폐합 경고와 같은 자리·같은 클래스. 새 방식 안 만듦. - 지반유형(리핑암·발파암) → 별표2 줄 매핑은 법령 근거가 아니라 **프로그램 설정**이라 표준 횡단면 설정에서 **사용자가 고르게** 함(2026-09-07 사용자 확정). 기본값 리핑암 → 연암 · 발파암 → 경암. 표준단면과 함께 저장돼 이미 사용자 값 계통임. - 기준값은 서버가 컨텍스트로 내려보냄 — 화면에 상수를 복제하지 않음. 시험 7건 — 별표2 값 · 작업임도 제외 · 매핑이 기본값일 뿐 · 구간별 판정 · **소단이 있어도 위반이 안 사라짐** · 경계값 통과 · 카드에 같은 방식으로 붙는지. ⚠ 실화면 — 사용자 선택 칸은 섰음(리핑암 연암 · 발파암 경암). 다만 이 프로젝트 저장분 63측점에 `cut_slope_segments` 가 아직 없어(오늘 새로 생긴 키) 경고가 뜨는 것은 못 봄. 재계산이 한 번 돌면 채워짐 — 지금 경고 0건은 맞는 동작임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
5.9 KiB
TypeScript
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>;
|
|
/** 지반유형(리핑암·발파암) → 별표2 줄. 사용자가 바꿀 수 있는 **설정값**이다. */
|
|
classOf: Record<string, 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,
|
|
classOf: context?.cut_slope_class_default ?? {},
|
|
exemptGrades: context?.cut_slope_exempt_grades ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 이 측점의 절토 구간이 별표2 범위를 벗어났는지.
|
|
*
|
|
* · 등급이 면제(작업임도)면 빈 목록.
|
|
* · 구간이 없으면(성토 측점 등) 빈 목록.
|
|
* · 암 구간은 측점의 `cut_rock_kind` 로 별표2 줄을 고른다 — 구간에는 `rock`/`soil` 만 있다.
|
|
*/
|
|
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; cut_rock_kind?: string | null } | undefined;
|
|
const segments = design?.cut_slope_segments;
|
|
if (!Array.isArray(segments) || !segments.length) return [];
|
|
|
|
const rockKey = criteria.classOf[design?.cut_rock_kind ?? ""] ?? "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;
|
|
}
|
|
|
|
/** 사용자가 고른 매핑(리핑암·발파암 → 별표2 줄)을 얹는다. 없으면 기본값 그대로. */
|
|
export function applyCutSlopeClassChoice(choice: Record<string, string> | null | undefined): void {
|
|
if (!remembered || !choice) return;
|
|
remembered = { ...remembered, classOf: { ...remembered.classOf, ...choice } };
|
|
}
|
|
|
|
/** 지금 측점의 위반 목록 — 기준이 없거나 면제 등급이면 빈 목록. */
|
|
export function violationsOf(section: CrossSection): CutSlopeViolation[] {
|
|
return cutSlopeViolations(section, remembered, rememberedGrade);
|
|
}
|