- 기본값 정본 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>
187 lines
7.1 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|