Files
Aislo/E02_StabGround/logic/E02_StabGround_Check.ts
T
eomsangdonandClaude Opus 5.5 f0c5232f80 feat(532-3): E02 지반 조건 — 기본값 정본 · 물성 표(지층 추가 · 지움) · 구간 덮어쓰기 · KDS 기준안전율 · 노면 하중 · 지하수 · 지진 · 저장 · 초기화
- 기본값 정본 resources/master_template/stability/ground_defaults.json — 지층 여섯 · 값마다 출처(07 표 · 절) · KDS 11 70 05 표 4.3-1~3 · 노면 10 kPa · 지하수 규칙 · 지진 끔 · 범위
- 07 원문 대조 — 성토 φ 25 → 28(07 §4 종합 28~30 아래값) · 나머지 범위 안
- API GET · PUT /api/projects/{id}/stab/ground · POST …/reset — 작업본 E02_StabGround/ground.json · 초기값 initial_snapshot/E02_StabGround/ground.json(E 생성 직후 기본값 + B06 측점 암 경계) · 측점은 서버 값
- 검사 · 측점 적용 = 파이썬 · TS 짝(E02_StabGround_Model.py ↔ logic/E02_StabGround_Check.ts) · 거울 시험
- 화면 — 카드 넷 · 칸 위 근거 · 기본값 툴팁 · 틀린 칸 표시 · sessionStorage 캐시 · 자동저장 없음
- test_528 E01 시험 — 생성 뒤 E02 초기값 부름 확인 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:00:44 +09:00

187 lines
7.1 KiB
TypeScript

/* =============================================================================
* E02_StabGround_Check.ts
* E02 지반 조건 꼴 · 입력 검사 · 측점 적용(요구 347-2 · 3 · 4 · PLAN 532-3) — 순수 함수만.
*
* ⚠ 파이썬·TS 짝 — `checkGround` ↔ `E02_StabGround_Model.py` `check_ground` ·
* `resolveStations` ↔ `resolve_stations`. 한쪽을 고치면 다른 쪽도 · 거울 시험
* `resources/tester/test_532_3_e02_ground.py`.
* ========================================================================== */
export type WaterGroup = "soil" | "rock";
export type WaterRule = "none" | "surface" | "half_height";
/** 물성 한 줄 = E03_StabSection_Types.Material + 지하수 무리 */
export interface Material {
id: string;
name: string;
gamma_t: number;
gamma_sat: number;
c: number;
phi: number;
water: WaterGroup;
}
export interface Station {
chainage_m: number;
ground_type: string;
soil_thickness_m: number | null;
}
export interface Zone {
from_m: number;
to_m: number;
soil: string | null;
rock: string | null;
soil_thickness_m: number | null;
}
export type SlopeFactors = Record<(typeof SLOPES)[number], number>;
export interface Ground {
version: number;
materials: Material[];
label_map: Record<(typeof LABELS)[number], string>;
stations: Station[];
zones: Zone[];
safety_factors: Record<(typeof CASES)[number], SlopeFactors>;
road_surcharge_kpa: number;
groundwater: Record<(typeof WATER_KEYS)[number], WaterRule>;
seismic: { enabled: boolean; kh: number };
}
export type Limits = Record<string, [number, number]>;
export interface GroundDefaults extends Ground {
note: string;
limits: Limits;
sources: Record<string, string>;
}
export interface CheckError {
path: string;
code: string;
}
export interface ResolvedStation {
chainage_m: number;
ground_type: string;
upper: string | null;
lower: string | null;
soil_thickness_m: number | null;
zoned: boolean;
}
export const MATERIAL_FIELDS = ["gamma_t", "gamma_sat", "c", "phi"] as const;
export const WATER_GROUPS: readonly WaterGroup[] = ["soil", "rock"];
export const LABELS = ["soil", "ripping_rock", "blasting_rock", "fill"] as const;
const ROCK_LABELS = ["ripping_rock", "blasting_rock"];
export const CASES = ["dry", "wet", "seismic", "short_term"] as const;
export const SLOPES = ["cut", "fill", "soft"] as const;
export const WATER_KEYS = ["dry", "wet_soil", "wet_rock"] as const;
export const WATER_RULES: readonly WaterRule[] = ["none", "surface", "half_height"];
type Loose = Record<string, unknown>;
const isNum = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
const asList = (v: unknown): unknown[] => (Array.isArray(v) ? v : []);
const isDict = (v: unknown): v is Loose => typeof v === "object" && v !== null && !Array.isArray(v);
const asDict = (v: unknown): Loose => (isDict(v) ? v : {});
/** 틀린 칸 목록 — 빈 목록 = 통과(code 는 파이썬과 같은 낱말) */
export function checkGround(ground: unknown, limits: Limits): CheckError[] {
const errors: CheckError[] = [];
const bad = (path: string, code: string): void => void errors.push({ path, code });
const ranged = (path: string, value: unknown, key: string): void => {
const [low, high] = limits[key];
if (!isNum(value)) bad(path, "number");
else if (!(low <= value && value <= high)) bad(path, "range");
};
const doc = asDict(ground);
const materials = asList(doc.materials);
if (!materials.length) bad("materials", "empty");
const ids: string[] = [];
const names: string[] = [];
materials.forEach((item, i) => {
const base = `materials.${i}`;
if (!isDict(item)) return bad(base, "type");
const mid = item.id;
if (typeof mid !== "string" || !mid || ids.includes(mid)) bad(`${base}.id`, "id_dup");
ids.push(typeof mid === "string" ? mid : "");
const name = typeof item.name === "string" ? item.name.trim() : "";
const [low, high] = limits.name_length;
if (!(low <= name.length && name.length <= high)) bad(`${base}.name`, "name");
else if (names.includes(name)) bad(`${base}.name`, "name_dup");
names.push(name);
for (const field of MATERIAL_FIELDS) ranged(`${base}.${field}`, item[field], field);
if (isNum(item.gamma_t) && isNum(item.gamma_sat) && item.gamma_sat < item.gamma_t)
bad(`${base}.gamma_sat`, "sat_lt_t");
if (!WATER_GROUPS.includes(item.water as WaterGroup)) bad(`${base}.water`, "enum");
});
const labelMap = asDict(doc.label_map);
for (const label of LABELS) {
const ref = labelMap[label];
if (!ref || !ids.includes(ref as string)) bad(`label_map.${label}`, "ref");
}
asList(doc.zones).forEach((zone, i) => {
const base = `zones.${i}`;
if (!isDict(zone)) return bad(base, "type");
ranged(`${base}.from_m`, zone.from_m, "chainage_m");
ranged(`${base}.to_m`, zone.to_m, "chainage_m");
if (isNum(zone.from_m) && isNum(zone.to_m) && zone.from_m >= zone.to_m)
bad(`${base}.to_m`, "order");
for (const layer of ["soil", "rock"]) {
const ref = zone[layer];
if (ref !== null && ref !== undefined && !ids.includes(ref as string))
bad(`${base}.${layer}`, "ref");
}
if (zone.soil_thickness_m !== null && zone.soil_thickness_m !== undefined)
ranged(`${base}.soil_thickness_m`, zone.soil_thickness_m, "soil_thickness_m");
});
const factors = asDict(doc.safety_factors);
for (const c of CASES)
for (const s of SLOPES)
ranged(`safety_factors.${c}.${s}`, asDict(factors[c])[s], "safety_factor");
ranged("road_surcharge_kpa", doc.road_surcharge_kpa, "road_surcharge_kpa");
const water = asDict(doc.groundwater);
for (const key of WATER_KEYS)
if (!WATER_RULES.includes(water[key] as WaterRule)) bad(`groundwater.${key}`, "enum");
const seismic = asDict(doc.seismic);
if (typeof seismic.enabled !== "boolean") bad("seismic.enabled", "enum");
ranged("seismic.kh", seismic.kh, "kh");
return errors;
}
/** 측점마다 적용 지층 — 위 = 토사 연결 · 아래 = 암 측점의 암 연결 · 두께 = B06 · 구간이 덮어씀
* (뒤 구간이 이김 · 양 끝 포함 · 빈 칸은 안 바꿈) · 두께가 없으면 아래 지층 없음 */
export function resolveStations(ground: Ground): ResolvedStation[] {
return ground.stations.map((station) => {
const chainage = station.chainage_m;
let upper: string | null = ground.label_map.soil ?? null;
let lower: string | null = ROCK_LABELS.includes(station.ground_type)
? (ground.label_map[station.ground_type as (typeof LABELS)[number]] ?? null)
: null;
let thickness = station.soil_thickness_m;
let zoned = false;
for (const zone of ground.zones) {
if (!(zone.from_m <= chainage && chainage <= zone.to_m)) continue;
zoned = true;
if (zone.soil != null) upper = zone.soil;
if (zone.rock != null) lower = zone.rock;
if (zone.soil_thickness_m != null) thickness = zone.soil_thickness_m;
}
if (thickness === null) lower = null; // 경계 없음 = 전부 위 지층
return {
chainage_m: chainage,
ground_type: station.ground_type,
upper,
lower,
soil_thickness_m: lower !== null ? thickness : null,
zoned,
};
});
}