Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -72,5 +72,7 @@ config/b07_extent_node/
|
||||
config/b07_note_node/
|
||||
# 계산 등록부 서버 Node 입구 번들 — `npm run build:design-calc` 산출물(472-3).
|
||||
config/design_calc_node/
|
||||
# E03 안정성 모형 벽 도형 번들 — `npm run build:e03-walls` 산출물(532-4).
|
||||
config/e03_walls_node/
|
||||
|
||||
~$*
|
||||
|
||||
@@ -7,6 +7,8 @@ ground_defaults.json`(M02 마스터 층). 물성 한 줄 = `E03_StabSection_Type
|
||||
⚠ 파이썬·TS 짝 — `check_ground` ↔ `logic/E02_StabGround_Check.ts` `checkGround` ·
|
||||
`resolve_stations` ↔ `resolveStations`. 한쪽을 고치면 다른 쪽도 · 거울 시험
|
||||
`resources/tester/test_532_3_e02_ground.py`.
|
||||
지하수 → 엔진 꼴(`station_groundwater` · 서버 단독 · 짝 없음) —
|
||||
시험 `test_532_4_stab_walls_water.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,6 +19,8 @@ import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULTS_PATH = (
|
||||
PROJECT_ROOT / "resources" / "master_template" / "stability" / "ground_defaults.json"
|
||||
@@ -32,6 +36,11 @@ CASES = ("dry", "wet", "seismic", "short_term")
|
||||
SLOPES = ("cut", "fill", "soft")
|
||||
WATER_KEYS = ("dry", "wet_soil", "wet_rock")
|
||||
WATER_RULES = ("none", "surface", "half_height")
|
||||
# 우기 지하수 갈래 — kds = 위 규칙(wet_soil · wet_rock · 기본) ·
|
||||
# depth_ratio = 지표 − 비탈 높이 × 비율 · ru = 간극수압비 ·
|
||||
# line = 사용자 수위선(구간 · [offset, 지표 아래 깊이]) · 덮지 않은 측점은 kds
|
||||
WET_MODES = ("kds", "depth_ratio", "ru", "line")
|
||||
WATER_EXTRA_KEYS = ("wet_mode", "depth_ratio", "ru", "lines")
|
||||
# 정본에 남기는 칸 — 그 밖(limits · sources · note)은 기본값 파일에만
|
||||
GROUND_KEYS = (
|
||||
"version",
|
||||
@@ -63,6 +72,16 @@ def pick(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
return copy.deepcopy({key: doc[key] for key in GROUND_KEYS if key in doc})
|
||||
|
||||
|
||||
def upgrade(ground: dict[str, Any], defaults: dict[str, Any]) -> dict[str, Any]:
|
||||
"""옛 정본(지하수 갈래 칸 전) = 빠진 지하수 칸만 기본값으로 채운 사본 · 있는 칸은 그대로."""
|
||||
out = copy.deepcopy(ground)
|
||||
water = out.setdefault("groundwater", {})
|
||||
if isinstance(water, dict):
|
||||
for key in WATER_EXTRA_KEYS:
|
||||
water.setdefault(key, copy.deepcopy(defaults["groundwater"][key]))
|
||||
return out
|
||||
|
||||
|
||||
def build_initial(defaults: dict[str, Any], stations: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""초기값 = 기본값 정본 칸 + B06 측점(암 경계)."""
|
||||
ground = pick(defaults)
|
||||
@@ -88,7 +107,7 @@ def _dict(value: Any) -> dict:
|
||||
|
||||
def check_ground(ground: dict[str, Any], limits: dict[str, list[float]]) -> list[dict[str, str]]:
|
||||
"""틀린 칸 목록 [{path, code}] — 빈 목록 = 통과. code = number · range · order · sat_lt_t ·
|
||||
name · name_dup · id_dup · enum · ref · empty · type."""
|
||||
name · name_dup · id_dup · enum · ref · empty · few_points · type."""
|
||||
errors: list[dict[str, str]] = []
|
||||
|
||||
def bad(path: str, code: str) -> None:
|
||||
@@ -161,6 +180,36 @@ def check_ground(ground: dict[str, Any], limits: dict[str, list[float]]) -> list
|
||||
for key in WATER_KEYS:
|
||||
if water.get(key) not in WATER_RULES:
|
||||
bad(f"groundwater.{key}", "enum")
|
||||
if water.get("wet_mode") not in WET_MODES:
|
||||
bad("groundwater.wet_mode", "enum")
|
||||
ranged("groundwater.depth_ratio", water.get("depth_ratio"), "depth_ratio")
|
||||
ranged("groundwater.ru", water.get("ru"), "ru")
|
||||
if not isinstance(water.get("lines"), list):
|
||||
bad("groundwater.lines", "type")
|
||||
for i, line in enumerate(_list(water.get("lines"))):
|
||||
base = f"groundwater.lines.{i}"
|
||||
if not isinstance(line, dict):
|
||||
bad(base, "type")
|
||||
continue
|
||||
ranged(f"{base}.from_m", line.get("from_m"), "chainage_m")
|
||||
ranged(f"{base}.to_m", line.get("to_m"), "chainage_m")
|
||||
if _num(line.get("from_m")) and _num(line.get("to_m")):
|
||||
if line["from_m"] >= line["to_m"]:
|
||||
bad(f"{base}.to_m", "order")
|
||||
points = _list(line.get("points"))
|
||||
if len(points) < 2:
|
||||
bad(f"{base}.points", "few_points")
|
||||
last = None
|
||||
for k, point in enumerate(points):
|
||||
if not isinstance(point, list) or len(point) != 2:
|
||||
bad(f"{base}.points.{k}", "type")
|
||||
continue
|
||||
ranged(f"{base}.points.{k}.0", point[0], "offset_m")
|
||||
ranged(f"{base}.points.{k}.1", point[1], "water_depth_m")
|
||||
if _num(point[0]):
|
||||
if last is not None and point[0] <= last:
|
||||
bad(f"{base}.points.{k}.0", "order")
|
||||
last = point[0]
|
||||
seismic = _dict(ground.get("seismic"))
|
||||
if not isinstance(seismic.get("enabled"), bool):
|
||||
bad("seismic.enabled", "enum")
|
||||
@@ -187,7 +236,12 @@ def clean(ground: dict[str, Any]) -> dict[str, Any]:
|
||||
]
|
||||
factors = out["safety_factors"]
|
||||
out["safety_factors"] = {c: {s: factors[c][s] for s in SLOPES} for c in CASES}
|
||||
out["groundwater"] = {key: out["groundwater"][key] for key in WATER_KEYS}
|
||||
water = out["groundwater"]
|
||||
out["groundwater"] = {key: water[key] for key in (*WATER_KEYS, *WATER_EXTRA_KEYS)}
|
||||
out["groundwater"]["lines"] = [
|
||||
{"from_m": w["from_m"], "to_m": w["to_m"], "points": [list(p) for p in w["points"]]}
|
||||
for w in water["lines"]
|
||||
]
|
||||
out["seismic"] = {"enabled": out["seismic"]["enabled"], "kh": out["seismic"]["kh"]}
|
||||
return out
|
||||
|
||||
@@ -228,3 +282,80 @@ def resolve_stations(ground: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _face_water(ground: dict[str, Any], chainage_m: float) -> str:
|
||||
"""측점 비탈 겉 지층의 지하수 무리 — 토층 두께 0 = 아래(암) 지층 · 아니면 위 지층 ·
|
||||
측점 없음 = soil."""
|
||||
names = {m["id"]: m for m in ground.get("materials") or []}
|
||||
for row in resolve_stations(ground):
|
||||
if abs(row["chainage_m"] - chainage_m) < 1e-3:
|
||||
face = row["lower"] if row["lower"] and row["soil_thickness_m"] == 0 else row["upper"]
|
||||
return (names.get(face) or {}).get("water", "soil")
|
||||
return "soil"
|
||||
|
||||
|
||||
def groundwater_for(
|
||||
ground: dict[str, Any],
|
||||
condition: str,
|
||||
*,
|
||||
chainage_m: float,
|
||||
surface: list,
|
||||
heights: dict[str, float],
|
||||
) -> dict[str, Any]:
|
||||
"""E02 지하수 규칙 → 엔진 꼴 `{mode: none | ru | line}`(E03 Types `Groundwater`) — 측점마다.
|
||||
|
||||
surface = 공사 뒤 지표 [[x, z]](좌 = +x) · heights = 비탈 높이 {left, right} m(x ≥ 0 = 좌 높이).
|
||||
건기 · 지진 · 단기 = `dry` 규칙 · 우기 = `wet_mode` 갈래
|
||||
(kds = 겉 지층 무리로 wet_soil | wet_rock).
|
||||
"""
|
||||
water = ground.get("groundwater") or {}
|
||||
sx, sz = zip(*sorted((float(x), float(z)) for x, z in surface))
|
||||
|
||||
def below(depth_at, extra=()) -> dict[str, Any]:
|
||||
# 좌 · 우 높이가 달라 x = 0 에서 끊길 수 있음 — 0 양쪽 점을 넣어 계단으로
|
||||
xs = sorted({*sx, *extra, -1e-6, 1e-6})
|
||||
zs = np.interp(xs, sx, sz) - np.array([depth_at(x) for x in xs])
|
||||
return {"mode": "line", "line": [[float(x), float(z)] for x, z in zip(xs, zs)]}
|
||||
|
||||
def height(x: float) -> float:
|
||||
return float(heights.get("left" if x >= 0 else "right") or 0.0)
|
||||
|
||||
def rule(name: str) -> dict[str, Any]:
|
||||
if name == "surface":
|
||||
return below(lambda x: 0.0)
|
||||
if name == "half_height":
|
||||
return below(lambda x: 0.5 * height(x))
|
||||
return {"mode": "none"}
|
||||
|
||||
if condition != "wet":
|
||||
return rule(water.get("dry", "none"))
|
||||
mode = water.get("wet_mode", "kds")
|
||||
if mode == "ru":
|
||||
return {"mode": "ru", "ru": float(water["ru"])}
|
||||
if mode == "depth_ratio":
|
||||
ratio = float(water["depth_ratio"])
|
||||
return below(lambda x: ratio * height(x))
|
||||
if mode == "line":
|
||||
hit = None
|
||||
for item in water.get("lines") or []: # 뒤 구간이 이김(지층 구간과 같은 규칙)
|
||||
if item["from_m"] <= chainage_m <= item["to_m"]:
|
||||
hit = item
|
||||
if hit is not None:
|
||||
ox, depth = zip(*sorted((float(o), float(d)) for o, d in hit["points"]))
|
||||
return below(lambda x: float(np.interp(x, ox, depth)), ox)
|
||||
group = _face_water(ground, chainage_m)
|
||||
return rule(water.get("wet_rock" if group == "rock" else "wet_soil", "none"))
|
||||
|
||||
|
||||
def station_groundwater(ground: dict[str, Any], condition: str, model: dict[str, Any]) -> dict:
|
||||
"""E03 측점 모형(`section_model_from_b06` 결과) → 그 측점 지하수 — 부르는 쪽이
|
||||
`model["groundwater"] = station_groundwater(ground, cond, model)` 로 갈아 끼움."""
|
||||
heights = {side["side"]: side["height_m"] for side in model.get("sides") or []}
|
||||
return groundwater_for(
|
||||
ground,
|
||||
condition,
|
||||
chainage_m=float((model.get("station") or {}).get("chainage_m") or 0.0),
|
||||
surface=model["surface"],
|
||||
heights=heights,
|
||||
)
|
||||
|
||||
@@ -112,10 +112,12 @@ async def get_ground(
|
||||
root, project = await _project(project_id)
|
||||
initial = ensure_initial(root)
|
||||
working = _read_working(root)
|
||||
defaults = model.load_defaults()
|
||||
return {
|
||||
"ground": working or initial,
|
||||
# 옛 정본(지하수 갈래 칸 전) = 빠진 칸만 기본값으로 채워 보냄(파일은 [저장] 때 씀)
|
||||
"ground": model.upgrade(working or initial, defaults),
|
||||
"saved": working is not None,
|
||||
"defaults": model.load_defaults(),
|
||||
"defaults": defaults,
|
||||
"editable": _editable(session, project),
|
||||
}
|
||||
|
||||
@@ -141,4 +143,4 @@ async def reset_ground(project_id: UUID) -> dict[str, Any]:
|
||||
root, _ = await _project(project_id)
|
||||
initial = ensure_initial(root)
|
||||
atomic_write_json(model.ground_path(root), initial)
|
||||
return {"ground": initial}
|
||||
return {"ground": model.upgrade(initial, model.load_defaults())}
|
||||
|
||||
@@ -42,6 +42,8 @@ function limitKey(path: string): string {
|
||||
const last = path.split(".").pop() ?? "";
|
||||
if (path.startsWith("safety_factors.")) return "safety_factor";
|
||||
if (last === "from_m" || last === "to_m") return "chainage_m";
|
||||
if (/\.points\.\d+\.0$/.test(path)) return "offset_m";
|
||||
if (/\.points\.\d+\.1$/.test(path)) return "water_depth_m";
|
||||
return last;
|
||||
}
|
||||
|
||||
@@ -141,6 +143,8 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
const cached = keepCache ? sessionStorage.getItem(cacheKey(projectId)) : null;
|
||||
if (cached) ground = JSON.parse(cached) as Ground;
|
||||
else sessionStorage.removeItem(cacheKey(projectId));
|
||||
// 옛 캐시(지하수 갈래 칸 전) — 빠진 칸은 서버 값(서버가 기본값으로 채워 보냄)
|
||||
ground.groundwater = { ...body.ground.groundwater, ...ground.groundwater };
|
||||
dirty = Boolean(cached);
|
||||
view.ground = ground;
|
||||
view.defaults = body.defaults;
|
||||
|
||||
@@ -170,3 +170,17 @@
|
||||
.e02-card--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* 지하수 카드 — 사용자 수위선 점(offset · 깊이) 줄 */
|
||||
.e02-points {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.e02-point {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* E02_StabGround_UI_Tables.ts
|
||||
* E02 지반 조건 본문 카드 넷(요구 347-2 · 3 · 4) — 물성 표(지층 추가 · 지움 · B06 지반 연결) ·
|
||||
* 층 두께 · 구간 덮어쓰기 + 측점별 적용 · 기준안전율 · 하중 · 지하수 · 지진. 카드마다 [기본값].
|
||||
* 층 두께 · 구간 덮어쓰기 + 측점별 적용 · 기준안전율 · 하중 · 지진 · 지하수(`_UI_Water`). 카드마다 [기본값].
|
||||
* 칸 = data-path(검사 열쇠 · 파이썬과 같음) · data-src(근거 열쇠) · data-def(기본값 글).
|
||||
* 값 바꿈 = view.change()(캐시 · 검사) · 줄 더함 · 지움 · [기본값] = view.rebuild().
|
||||
* ========================================================================== */
|
||||
@@ -16,13 +16,12 @@ import {
|
||||
MATERIAL_FIELDS,
|
||||
SLOPES,
|
||||
WATER_GROUPS,
|
||||
WATER_KEYS,
|
||||
WATER_RULES,
|
||||
resolveStations,
|
||||
type Ground,
|
||||
type GroundDefaults,
|
||||
type Material,
|
||||
} from "./logic/E02_StabGround_Check";
|
||||
import { waterCard } from "./E02_StabGround_UI_Water";
|
||||
|
||||
export interface E02View {
|
||||
ground: Ground;
|
||||
@@ -33,7 +32,7 @@ export interface E02View {
|
||||
}
|
||||
|
||||
const L = (key: string): string => t(key as LocaleKey);
|
||||
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
||||
export const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
||||
const FIELD_LABEL: Record<(typeof MATERIAL_FIELDS)[number], LocaleKey> = {
|
||||
gamma_t: "E02_Col_GammaT",
|
||||
gamma_sat: "E02_Col_GammaSat",
|
||||
@@ -42,7 +41,7 @@ const FIELD_LABEL: Record<(typeof MATERIAL_FIELDS)[number], LocaleKey> = {
|
||||
};
|
||||
|
||||
/** 숫자 칸 — 빈 칸 = null(nullable) · 아니면 숫자 아님으로 검사에 걸림 */
|
||||
function numInput(
|
||||
export function numInput(
|
||||
view: E02View,
|
||||
path: string,
|
||||
value: number | null,
|
||||
@@ -68,7 +67,7 @@ function numInput(
|
||||
return input;
|
||||
}
|
||||
|
||||
function selectInput(
|
||||
export function selectInput(
|
||||
view: E02View,
|
||||
path: string,
|
||||
options: { value: string; text: string }[],
|
||||
@@ -92,7 +91,7 @@ function selectInput(
|
||||
return select;
|
||||
}
|
||||
|
||||
function card(
|
||||
export function card(
|
||||
title: LocaleKey,
|
||||
...body: HTMLElement[]
|
||||
): { section: HTMLElement; head: HTMLElement } {
|
||||
@@ -103,14 +102,18 @@ function card(
|
||||
return { section, head };
|
||||
}
|
||||
|
||||
function headButtons(view: E02View, head: HTMLElement, ...buttons: HTMLButtonElement[]): void {
|
||||
export function headButtons(
|
||||
view: E02View,
|
||||
head: HTMLElement,
|
||||
...buttons: HTMLButtonElement[]
|
||||
): void {
|
||||
if (!view.editable) return;
|
||||
const row = el("div", { className: "e02-card__actions" });
|
||||
row.append(...buttons);
|
||||
head.append(row);
|
||||
}
|
||||
|
||||
function table(headers: string[], rows: HTMLElement[][]): HTMLTableElement {
|
||||
export function table(headers: string[], rows: HTMLElement[][]): HTMLTableElement {
|
||||
const node = el("table", { className: "e02-table" }) as HTMLTableElement;
|
||||
const tr = node.createTHead().insertRow();
|
||||
for (const text of headers) tr.append(el("th", { text }));
|
||||
@@ -405,7 +408,7 @@ function safetyCard(view: E02View): HTMLElement {
|
||||
return section;
|
||||
}
|
||||
|
||||
/* --- 4. 노면 하중 · 지하수 · 지진 --- */
|
||||
/* --- 4. 노면 하중 · 지진 (지하수 = `E02_StabGround_UI_Water`) --- */
|
||||
function loadsCard(view: E02View): HTMLElement {
|
||||
const g = view.ground;
|
||||
const d = view.defaults;
|
||||
@@ -429,19 +432,6 @@ function loadsCard(view: E02View): HTMLElement {
|
||||
kh.disabled = !view.editable || !g.seismic.enabled;
|
||||
view.change();
|
||||
});
|
||||
const water = WATER_KEYS.map((key) =>
|
||||
field(
|
||||
L(`E02_Water_${key}`),
|
||||
selectInput(
|
||||
view,
|
||||
`groundwater.${key}`,
|
||||
WATER_RULES.map((r) => ({ value: r, text: L(`E02_Rule_${r}`) })),
|
||||
g.groundwater[key],
|
||||
(v) => (g.groundwater[key] = v as (typeof WATER_RULES)[number]),
|
||||
{ src: `groundwater.${key}`, def: L(`E02_Rule_${d.groundwater[key]}`) },
|
||||
),
|
||||
),
|
||||
);
|
||||
const grid = el("div", { className: "e02-grid" });
|
||||
grid.append(
|
||||
field(
|
||||
@@ -457,7 +447,6 @@ function loadsCard(view: E02View): HTMLElement {
|
||||
},
|
||||
),
|
||||
),
|
||||
...water,
|
||||
seismic.root,
|
||||
field(t("E02_Kh"), kh),
|
||||
);
|
||||
@@ -471,7 +460,6 @@ function loadsCard(view: E02View): HTMLElement {
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
g.road_surcharge_kpa = d.road_surcharge_kpa;
|
||||
g.groundwater = clone(d.groundwater);
|
||||
g.seismic = clone(d.seismic);
|
||||
view.rebuild();
|
||||
},
|
||||
@@ -485,6 +473,12 @@ export function buildCards(view: E02View): { root: HTMLElement; refreshStations:
|
||||
const root = el("div", { className: "e02-cards" });
|
||||
const zones = zonesCard(view);
|
||||
zones.section.classList.add("e02-card--wide");
|
||||
root.append(materialsCard(view), safetyCard(view), loadsCard(view), zones.section);
|
||||
root.append(
|
||||
materialsCard(view),
|
||||
safetyCard(view),
|
||||
loadsCard(view),
|
||||
waterCard(view),
|
||||
zones.section,
|
||||
);
|
||||
return { root, refreshStations: zones.refresh };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/* =============================================================================
|
||||
* E02_StabGround_UI_Water.ts
|
||||
* E02 지하수 카드(요구 347 · 351 · PLAN 532-3) — 건기 규칙 · 우기 갈래 넷(KDS 기본 · 지표 아래 깊이 비율 ·
|
||||
* r_u · 사용자 수위선 구간) · 「KDS 기본 · 우기 기준안전율 1.2 와 짝」 안내 · [기본값].
|
||||
* 엔진 꼴로 바꾸기는 서버(`E02_StabGround_Model.station_groundwater`) — 여기는 값만 고침.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { WATER_KEYS, WATER_RULES, WET_MODES, type WetMode } from "./logic/E02_StabGround_Check";
|
||||
import {
|
||||
card,
|
||||
clone,
|
||||
headButtons,
|
||||
numInput,
|
||||
selectInput,
|
||||
table,
|
||||
type E02View,
|
||||
} from "./E02_StabGround_UI_Tables";
|
||||
|
||||
const L = (key: string): string => t(key as LocaleKey);
|
||||
|
||||
function field(label: string, input: HTMLElement): HTMLElement {
|
||||
const node = el("label", { className: "e02-field" });
|
||||
node.append(el("span", { text: label }), input);
|
||||
return node;
|
||||
}
|
||||
|
||||
function removeButton(view: E02View, onClick: () => void): HTMLButtonElement {
|
||||
const button = createButton({
|
||||
label: t("E02_Btn_Remove"),
|
||||
variant: "danger-ghost",
|
||||
size: "sm",
|
||||
onClick,
|
||||
});
|
||||
button.hidden = !view.editable;
|
||||
return button;
|
||||
}
|
||||
|
||||
/** 사용자 수위선 구간 표 — 줄마다 시작 · 끝 · 점(offset · 깊이) · [점 추가] · [지움] */
|
||||
function linesBody(view: E02View): HTMLElement {
|
||||
const water = view.ground.groundwater;
|
||||
if (!water.lines.length) return el("p", { className: "e02-empty", text: t("E02_Line_Empty") });
|
||||
const rows = water.lines.map((line, i) => {
|
||||
const base = `groundwater.lines.${i}`;
|
||||
const points = el("div", { className: "e02-points" });
|
||||
points.dataset.path = `${base}.points`;
|
||||
line.points.forEach((point, k) => {
|
||||
const pair = el("span", { className: "e02-point" });
|
||||
pair.append(
|
||||
numInput(view, `${base}.points.${k}.0`, point[0], (v) => (point[0] = v as number)),
|
||||
numInput(view, `${base}.points.${k}.1`, point[1], (v) => (point[1] = v as number)),
|
||||
removeButton(view, () => {
|
||||
line.points.splice(k, 1);
|
||||
view.rebuild();
|
||||
}),
|
||||
);
|
||||
points.append(pair);
|
||||
});
|
||||
const addPoint = createButton({
|
||||
label: t("E02_Btn_AddPoint"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
const last = line.points[line.points.length - 1];
|
||||
line.points.push(last ? [last[0] + 5, last[1]] : [0, 1]);
|
||||
view.rebuild();
|
||||
},
|
||||
});
|
||||
addPoint.hidden = !view.editable;
|
||||
points.append(addPoint);
|
||||
return [
|
||||
numInput(view, `${base}.from_m`, line.from_m, (v) => (line.from_m = v as number)),
|
||||
numInput(view, `${base}.to_m`, line.to_m, (v) => (line.to_m = v as number)),
|
||||
points,
|
||||
removeButton(view, () => {
|
||||
water.lines.splice(i, 1);
|
||||
view.rebuild();
|
||||
}),
|
||||
];
|
||||
});
|
||||
return table([t("E02_Col_From"), t("E02_Col_To"), t("E02_Col_Points"), ""], rows);
|
||||
}
|
||||
|
||||
export function waterCard(view: E02View): HTMLElement {
|
||||
const water = view.ground.groundwater;
|
||||
const d = view.defaults.groundwater;
|
||||
const rule = (key: (typeof WATER_KEYS)[number]): HTMLElement =>
|
||||
field(
|
||||
L(`E02_Water_${key}`),
|
||||
selectInput(
|
||||
view,
|
||||
`groundwater.${key}`,
|
||||
WATER_RULES.map((r) => ({ value: r, text: L(`E02_Rule_${r}`) })),
|
||||
water[key],
|
||||
(v) => (water[key] = v as (typeof WATER_RULES)[number]),
|
||||
{ src: `groundwater.${key}`, def: L(`E02_Rule_${d[key]}`) },
|
||||
),
|
||||
);
|
||||
const mode = selectInput(
|
||||
view,
|
||||
"groundwater.wet_mode",
|
||||
WET_MODES.map((m) => ({ value: m, text: L(`E02_WetMode_${m}`) })),
|
||||
water.wet_mode,
|
||||
(v) => {
|
||||
water.wet_mode = v as WetMode;
|
||||
view.rebuild();
|
||||
},
|
||||
{ src: "groundwater.wet_mode", def: L(`E02_WetMode_${d.wet_mode}`) },
|
||||
);
|
||||
const grid = el("div", { className: "e02-grid" });
|
||||
grid.append(rule("dry"), field(t("E02_WetMode"), mode));
|
||||
const body: HTMLElement[] = [];
|
||||
if (water.wet_mode === "kds") grid.append(rule("wet_soil"), rule("wet_rock"));
|
||||
if (water.wet_mode === "depth_ratio")
|
||||
grid.append(
|
||||
field(
|
||||
t("E02_DepthRatio"),
|
||||
numInput(
|
||||
view,
|
||||
"groundwater.depth_ratio",
|
||||
water.depth_ratio,
|
||||
(v) => (water.depth_ratio = v as number),
|
||||
{ src: "groundwater.depth_ratio", def: d.depth_ratio },
|
||||
),
|
||||
),
|
||||
);
|
||||
if (water.wet_mode === "ru")
|
||||
grid.append(
|
||||
field(
|
||||
t("E02_Ru"),
|
||||
numInput(view, "groundwater.ru", water.ru, (v) => (water.ru = v as number), {
|
||||
src: "groundwater.ru",
|
||||
def: d.ru,
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (water.wet_mode === "line")
|
||||
body.push(el("p", { className: "e02-hint", text: t("E02_Line_Hint") }), linesBody(view));
|
||||
|
||||
const { section, head } = card(
|
||||
"E02_Card_Water",
|
||||
el("p", { className: "e02-hint", text: t("E02_Water_Hint") }),
|
||||
grid,
|
||||
...body,
|
||||
);
|
||||
const stations = view.ground.stations;
|
||||
const first = stations[0]?.chainage_m ?? 0;
|
||||
const last = stations[stations.length - 1]?.chainage_m ?? 100;
|
||||
const buttons = [
|
||||
createButton({
|
||||
label: t("E02_Btn_Defaults"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
view.ground.groundwater = clone(d);
|
||||
view.rebuild();
|
||||
},
|
||||
}),
|
||||
];
|
||||
if (water.wet_mode === "line")
|
||||
buttons.unshift(
|
||||
createButton({
|
||||
label: t("E02_Btn_AddLine"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
water.lines.push({
|
||||
from_m: first,
|
||||
to_m: last > first ? last : first + 20,
|
||||
points: [
|
||||
[-10, 1],
|
||||
[10, 1],
|
||||
],
|
||||
});
|
||||
view.rebuild();
|
||||
},
|
||||
}),
|
||||
);
|
||||
headButtons(view, head, ...buttons);
|
||||
return section;
|
||||
}
|
||||
@@ -9,6 +9,25 @@
|
||||
|
||||
export type WaterGroup = "soil" | "rock";
|
||||
export type WaterRule = "none" | "surface" | "half_height";
|
||||
/** 우기 지하수 갈래 — kds = wet_soil · wet_rock 규칙(기본) · depth_ratio · ru · line(파이썬 `WET_MODES`) */
|
||||
export type WetMode = "kds" | "depth_ratio" | "ru" | "line";
|
||||
|
||||
/** 사용자 수위선 한 구간 — 점 = [offset m(좌 +), 공사 뒤 지표 아래 깊이 m] · offset 오름차순 */
|
||||
export interface WaterLine {
|
||||
from_m: number;
|
||||
to_m: number;
|
||||
points: [number, number][];
|
||||
}
|
||||
|
||||
export interface Groundwater {
|
||||
dry: WaterRule;
|
||||
wet_soil: WaterRule;
|
||||
wet_rock: WaterRule;
|
||||
wet_mode: WetMode;
|
||||
depth_ratio: number;
|
||||
ru: number;
|
||||
lines: WaterLine[];
|
||||
}
|
||||
|
||||
/** 물성 한 줄 = E03_StabSection_Types.Material + 지하수 무리 */
|
||||
export interface Material {
|
||||
@@ -45,7 +64,7 @@ export interface Ground {
|
||||
zones: Zone[];
|
||||
safety_factors: Record<(typeof CASES)[number], SlopeFactors>;
|
||||
road_surcharge_kpa: number;
|
||||
groundwater: Record<(typeof WATER_KEYS)[number], WaterRule>;
|
||||
groundwater: Groundwater;
|
||||
seismic: { enabled: boolean; kh: number };
|
||||
}
|
||||
|
||||
@@ -79,6 +98,7 @@ 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"];
|
||||
export const WET_MODES: readonly WetMode[] = ["kds", "depth_ratio", "ru", "line"];
|
||||
|
||||
type Loose = Record<string, unknown>;
|
||||
const isNum = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
|
||||
@@ -149,6 +169,30 @@ export function checkGround(ground: unknown, limits: Limits): CheckError[] {
|
||||
const water = asDict(doc.groundwater);
|
||||
for (const key of WATER_KEYS)
|
||||
if (!WATER_RULES.includes(water[key] as WaterRule)) bad(`groundwater.${key}`, "enum");
|
||||
if (!WET_MODES.includes(water.wet_mode as WetMode)) bad("groundwater.wet_mode", "enum");
|
||||
ranged("groundwater.depth_ratio", water.depth_ratio, "depth_ratio");
|
||||
ranged("groundwater.ru", water.ru, "ru");
|
||||
if (!Array.isArray(water.lines)) bad("groundwater.lines", "type");
|
||||
asList(water.lines).forEach((line, i) => {
|
||||
const base = `groundwater.lines.${i}`;
|
||||
if (!isDict(line)) return bad(base, "type");
|
||||
ranged(`${base}.from_m`, line.from_m, "chainage_m");
|
||||
ranged(`${base}.to_m`, line.to_m, "chainage_m");
|
||||
if (isNum(line.from_m) && isNum(line.to_m) && line.from_m >= line.to_m)
|
||||
bad(`${base}.to_m`, "order");
|
||||
const points = asList(line.points);
|
||||
if (points.length < 2) bad(`${base}.points`, "few_points");
|
||||
let last: number | null = null;
|
||||
points.forEach((point, k) => {
|
||||
if (!Array.isArray(point) || point.length !== 2) return bad(`${base}.points.${k}`, "type");
|
||||
ranged(`${base}.points.${k}.0`, point[0], "offset_m");
|
||||
ranged(`${base}.points.${k}.1`, point[1], "water_depth_m");
|
||||
if (isNum(point[0])) {
|
||||
if (last !== null && point[0] <= last) bad(`${base}.points.${k}.0`, "order");
|
||||
last = point[0];
|
||||
}
|
||||
});
|
||||
});
|
||||
const seismic = asDict(doc.seismic);
|
||||
if (typeof seismic.enabled !== "boolean") bad("seismic.enabled", "enum");
|
||||
ranged("seismic.kh", seismic.kh, "kh");
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""E03 2D 모형의 벽(옹벽 · 석축 · 돌쌓기 · 관 기슭막이) — 복사본 → 측점별 walls
|
||||
(532-4 · 분석 345-D ② ⑤).
|
||||
|
||||
도형은 짓지 않음 — B06 화면 TS 를 서버 Node 로 돌림(`E03_StabSection_Walls_Node.ts` · 5장 ②).
|
||||
얹기(어느 측점에 어느 벽) = B06 `attach_saved_shapes` 그대로(복사본 = 원본 프로젝트 배치).
|
||||
벽 재료 = 모형 `WALL_MATERIAL`(고강도 — 활동면이 벽을 못 지남) · 꼴 = `build_section_model(walls)`.
|
||||
벽이 선 측점은 지표도 B06 면적과 같은 「그려지는 설계선」(벽 상단에서 끊김)으로 바꿔 넘김.
|
||||
|
||||
부르는 법(E03 분석 길):
|
||||
found = walls_by_station(root) # 복사본 루트 → {chainage_key: entry}
|
||||
entry = found.get(chainage_key(cross["chainage_m"]))
|
||||
model = section_model_from_b06(cross, design_with_walls(design, entry), walls=walls_of(entry))
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_node_bundle import ROOT, build_bundle, bundle_stale, run_bundle_json
|
||||
from common_util.common_util_station_key import chainage_key
|
||||
from E01_StabRegister import E01_StabRegister_Source as source
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import WALL_MATERIAL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NPM_SCRIPT = "build:e03-walls"
|
||||
ENTRY = ROOT / "E03_StabSection" / "E03_StabSection_Walls_Node.ts"
|
||||
BUNDLE = ROOT / "config" / "e03_walls_node" / "E03_StabSection_Walls_Node.js"
|
||||
|
||||
|
||||
def _sections(root: Path) -> list[dict[str, Any]]:
|
||||
"""복사본 측점 → B06 상세 꼴(chainage_m · 중심 · samples · design)
|
||||
+ 형상 얹기(revetment · culvert …)."""
|
||||
from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes
|
||||
|
||||
sections = []
|
||||
for station in source.stations(root):
|
||||
cross = source.cross_section(root, station["chainage_m"])
|
||||
sections.append(
|
||||
{
|
||||
**cross,
|
||||
"center_x": station.get("center_x"),
|
||||
"center_y": station.get("center_y"),
|
||||
}
|
||||
)
|
||||
route = source.db_snapshot(root).get("route") or {}
|
||||
attach_saved_shapes(root, sections, route.get("route_data_path"))
|
||||
return sections
|
||||
|
||||
|
||||
def _fresh_bundle() -> bool:
|
||||
"""입구 TS 는 `SOURCE_DIRS` 밖(E03) — 입구가 묶음보다 새로우면 여기서 다시 지음."""
|
||||
stale = bundle_stale(BUNDLE) or ENTRY.stat().st_mtime > BUNDLE.stat().st_mtime
|
||||
return not stale or build_bundle(NPM_SCRIPT)
|
||||
|
||||
|
||||
def walls_by_station(root: Path | str) -> dict[str, dict[str, Any]]:
|
||||
"""복사본 루트 → {chainage_key: {walls: [모형 벽 …], design_line: [[offset, z] …] | None}}.
|
||||
|
||||
벽 · 트림이 없는 측점은 안 실음(그 측점 모형 = 종전 그대로). Node 실패는 던짐 — 벽을 빼고
|
||||
해석하면 활동면이 벽을 지난 값이 조용히 남음.
|
||||
"""
|
||||
return walls_for_sections(_sections(Path(root)))
|
||||
|
||||
|
||||
def walls_for_sections(sections: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||
"""형상 얹은 B06 횡단 목록 → 측점 entry(`walls_by_station` 몸 · 시험이 바로 부름)."""
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_definition
|
||||
|
||||
if not any(s.get("revetment") or s.get("culvert") for s in sections):
|
||||
return {}
|
||||
if not _fresh_bundle():
|
||||
raise RuntimeError("E03 벽 도형 번들을 못 지음")
|
||||
payload = json.loads(
|
||||
json.dumps({"definition": load_definition(), "sections": sections}, default=str)
|
||||
)
|
||||
out = run_bundle_json(BUNDLE, NPM_SCRIPT, payload)
|
||||
if not isinstance(out, dict):
|
||||
raise RuntimeError("E03 벽 도형 Node 실패")
|
||||
found: dict[str, dict[str, Any]] = {}
|
||||
for row in out["stations"]:
|
||||
walls = [
|
||||
{
|
||||
"material_id": WALL_MATERIAL["id"],
|
||||
"kind": w["kind"],
|
||||
"name": w.get("name"),
|
||||
"structure_id": w.get("structure_id"),
|
||||
"form": w.get("form"),
|
||||
"polygon": w["polygon"],
|
||||
}
|
||||
for w in row["walls"]
|
||||
]
|
||||
if walls or row.get("design_line"):
|
||||
found[chainage_key(float(row["chainage_m"]))] = {
|
||||
"walls": walls,
|
||||
"design_line": row.get("design_line"),
|
||||
}
|
||||
return found
|
||||
|
||||
|
||||
def walls_of(entry: dict[str, Any] | None) -> list[dict[str, Any]] | None:
|
||||
"""측점 entry → `section_model_from_b06(walls=…)` 값(없으면 None = 벽 없음)."""
|
||||
return (entry or {}).get("walls") or None
|
||||
|
||||
|
||||
def design_with_walls(design: dict[str, Any], entry: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""벽이 선 측점 = 설계선을 그려지는 선(벽 상단에서 끊김)으로 바꾼 사본 · 아니면 그대로."""
|
||||
line = (entry or {}).get("design_line")
|
||||
if not line:
|
||||
return design
|
||||
return {**design, "design_line": [{"offset_m": o, "elevation_m": z} for o, z in line]}
|
||||
@@ -0,0 +1,102 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_Walls_Node.ts
|
||||
* E 2D 해석 모형의 벽(옹벽 · 석축 · 돌쌓기 · 관 기슭막이) 단면 꼭짓점을 **서버가** 내는 입구(532-4).
|
||||
*
|
||||
* 도형은 B06 화면이 그리는 TS 그대로(`computeStoredLayouts` — 정본만 읽는 한 벌 · CLAUDE.md 5장 ②) ·
|
||||
* 지표 = B06 면적과 같은 「실제로 그려지는 설계선」(`drawnProfile` · 벽 상단에서 끊김). 여기에 계산 없음.
|
||||
* 파이썬 껍데기 = `E03_StabSection_Walls.py` · 번들 = `npm run build:e03-walls`.
|
||||
*
|
||||
* 실행: node <번들> <입력.json> <출력.json>
|
||||
* 입력 { definition?: 구조물 정의 · sections: 형상 얹은 B06 횡단(chainage_m · samples · design · revetment …) }
|
||||
* 출력 { stations: [{ chainage_m, walls: [{ kind, name, structure_id, form, polygon: [[offset, z], …] }],
|
||||
* design_line: [[offset, z], …] | null(트림 없음 = 설계선 그대로) }] } — 입력 차례 그대로
|
||||
* 끝 코드: 0 성공 / 2 인자 오류
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { drawnProfile } from "@util/common_util_cross_structure_areas";
|
||||
import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog";
|
||||
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { WallLayout } from "../B06_Section/logic/B06_Section_Cross_Culvert_Types";
|
||||
import {
|
||||
computeStoredLayouts,
|
||||
trimOfLayouts,
|
||||
} from "../B06_Section/logic/B06_Section_Structure_Layouts";
|
||||
|
||||
/** 트림 경계 바로 바깥 점 — 벽 쪽 연직 단차를 살림(`computeStructureAreas` 와 같은 값). */
|
||||
const BOUNDARY_EPS_M = 1e-6;
|
||||
|
||||
type Pair = [number, number];
|
||||
|
||||
function stationWalls(section: CrossSection, sections: readonly CrossSection[]) {
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
const walls: Array<{
|
||||
kind: string;
|
||||
name: string | null;
|
||||
structure_id: string | null;
|
||||
form: string | null;
|
||||
polygon: Pair[];
|
||||
}> = [];
|
||||
const push = (kind: string, list: readonly WallLayout[] | undefined, name: string | null) => {
|
||||
for (const wall of list ?? []) {
|
||||
if (wall.points.length >= 3)
|
||||
walls.push({
|
||||
kind,
|
||||
name,
|
||||
structure_id: kind === "own" ? (layouts?.own?.structureId ?? null) : null,
|
||||
form: wall.form,
|
||||
polygon: wall.points.map((p) => [p.offset, p.elevation]),
|
||||
});
|
||||
}
|
||||
};
|
||||
push("own", layouts?.own?.walls, layouts?.own?.name ?? null);
|
||||
push("culvert", layouts?.culvert?.walls, "기슭막이");
|
||||
push("culvert", layouts?.culvert?.basinExtras, "기슭막이");
|
||||
const trim = layouts ? trimOfLayouts(layouts) : null;
|
||||
const line = layouts?.design.design_line;
|
||||
let design_line: Pair[] | null = null;
|
||||
if (trim && Array.isArray(line) && line.length >= 2) {
|
||||
const ground = section.samples
|
||||
.filter((s) => s.valid !== false && typeof s.elevation_m === "number")
|
||||
.map((s) => ({ offset: s.offset_m ?? 0, elevation: s.elevation_m as number }))
|
||||
.sort((a, b) => a.offset - b.offset);
|
||||
const profile = drawnProfile({
|
||||
designLine: line as Array<{ offset_m: number; elevation_m: number }>,
|
||||
ground,
|
||||
trim,
|
||||
});
|
||||
if (profile && ground.length >= 2) {
|
||||
const lo = ground[0].offset;
|
||||
const hi = ground[ground.length - 1].offset;
|
||||
const grid = new Set<number>();
|
||||
const add = (o: number) => {
|
||||
if (o >= lo && o <= hi) grid.add(Math.round(o * 1e6) / 1e6);
|
||||
};
|
||||
ground.forEach((g) => add(g.offset));
|
||||
line.forEach((p) => add(p.offset_m));
|
||||
for (const b of [trim.minOffset, trim.maxOffset])
|
||||
[b, b - BOUNDARY_EPS_M, b + BOUNDARY_EPS_M].forEach(add);
|
||||
trim.minSlope?.points.forEach((p) => add(p.offset));
|
||||
trim.maxSlope?.points.forEach((p) => add(p.offset));
|
||||
design_line = [...grid].sort((a, b) => a - b).map((o) => [o, profile.drawnZ(o)]);
|
||||
}
|
||||
}
|
||||
return { chainage_m: section.chainage_m, walls, design_line };
|
||||
}
|
||||
|
||||
const [inputPath, outputPath] = process.argv.slice(2);
|
||||
if (!inputPath || !outputPath) {
|
||||
console.error("사용법: node <번들> <입력.json> <출력.json>");
|
||||
process.exit(2);
|
||||
}
|
||||
const input = JSON.parse(readFileSync(inputPath, "utf8")) as {
|
||||
definition?: CatalogDefinition | null;
|
||||
sections: CrossSection[];
|
||||
};
|
||||
if (input.definition) useCatalogDefinition(input.definition);
|
||||
const sections = input.sections ?? [];
|
||||
writeFileSync(
|
||||
outputPath,
|
||||
JSON.stringify({ stations: sections.map((s) => stationWalls(s, sections)) }),
|
||||
"utf8",
|
||||
);
|
||||
@@ -72,8 +72,9 @@ function interpolator(
|
||||
};
|
||||
}
|
||||
|
||||
/** 폐회로의 위(실제로 그려지는 설계선)·아래(지반선) 보간기 — 면적과 벽 겹침이 같은 선을 봄. */
|
||||
function drawnProfile(
|
||||
/** 폐회로의 위(실제로 그려지는 설계선)·아래(지반선) 보간기 — 면적과 벽 겹침이 같은 선을 봄 ·
|
||||
* E03 안정성 모형 지표도 이 선(`E03_StabSection_Walls_Node.ts`). */
|
||||
export function drawnProfile(
|
||||
input: StructureAreaInput,
|
||||
): { drawnZ: (offset: number) => number; ground: (offset: number) => number } | null {
|
||||
const design = interpolator(
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"build:b07-extent": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Cross_Extent_Node.ts --outDir ../config/b07_extent_node",
|
||||
"build:b07-note": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Note_Options_Node.ts --outDir ../config/b07_note_node",
|
||||
"build:design-calc": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../A00_Common/design/logic/design_calc_node.ts --outDir ../config/design_calc_node",
|
||||
"build:e03-walls": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../E03_StabSection/E03_StabSection_Walls_Node.ts --outDir ../config/e03_walls_node",
|
||||
"install:b07-cad": "npm --prefix B07_DesignDetail/openwebcad install",
|
||||
"build:b07-cad": "npm run install:b07-cad && npm --prefix B07_DesignDetail/openwebcad run build",
|
||||
"preview": "node ./config/node_modules/vite/bin/vite.js preview --configLoader runner",
|
||||
|
||||
@@ -19,13 +19,25 @@
|
||||
"short_term": { "cut": 1.1, "fill": 1.1, "soft": 1.1 }
|
||||
},
|
||||
"road_surcharge_kpa": 10.0,
|
||||
"groundwater": { "dry": "none", "wet_soil": "surface", "wet_rock": "half_height" },
|
||||
"groundwater": {
|
||||
"dry": "none",
|
||||
"wet_soil": "surface",
|
||||
"wet_rock": "half_height",
|
||||
"wet_mode": "kds",
|
||||
"depth_ratio": 0.5,
|
||||
"ru": 0.2,
|
||||
"lines": []
|
||||
},
|
||||
"seismic": { "enabled": false, "kh": 0.0 },
|
||||
"limits": {
|
||||
"gamma_t": [10.0, 30.0],
|
||||
"gamma_sat": [10.0, 30.0],
|
||||
"c": [0.0, 1000.0],
|
||||
"phi": [0.0, 60.0],
|
||||
"depth_ratio": [0.0, 1.0],
|
||||
"ru": [0.0, 0.9],
|
||||
"offset_m": [-200.0, 200.0],
|
||||
"water_depth_m": [0.0, 50.0],
|
||||
"safety_factor": [1.0, 3.0],
|
||||
"road_surcharge_kpa": [0.0, 100.0],
|
||||
"kh": [0.0, 0.5],
|
||||
@@ -66,6 +78,10 @@
|
||||
"groundwater.dry": "KDS 11 70 05 표 4.3-3 건기 = 지하수 없음",
|
||||
"groundwater.wet_soil": "KDS 11 70 05 표 4.3-3 우기 토층 · 풍화암 = 지하수위 결정(지표 포화 가정)",
|
||||
"groundwater.wet_rock": "KDS 11 70 05 표 4.3-3 우기 연암 · 경암 = 비탈면 높이 ½ 심도까지 포화",
|
||||
"groundwater.wet_mode": "우기 지하수 규칙 — 기본 = KDS 11 70 05 표 4.3-3(위 두 칸 · 우기 기준안전율 깎기 1.2 와 짝) · 그 밖 갈래는 사용자 판단",
|
||||
"groundwater.depth_ratio": "지표 아래 깊이 = 비탈 높이 × 비율 — 근거 없음 · 사용자 입력(0.5 = KDS 암반 ½ 와 같은 값)",
|
||||
"groundwater.ru": "간극수압비 u = r_u · γ·h — 근거 없음 · 사용자 입력",
|
||||
"groundwater.lines": "사용자 수위선 — 구간(누가 m) · 점(offset m · 공사 뒤 지표 아래 깊이 m) · 덮지 않은 측점은 KDS 규칙",
|
||||
"seismic.enabled": "KDS 11 90 00 1.8.1 ② 영향 범위에 주구조물 없으면 비적용 · 임도는 발주자 협의 — 기본 끔",
|
||||
"seismic.kh": "유사정적 수평지진계수 — 지반가속도의 비율로 사용자 입력(KDS 11 70 05 표 4.3 지진 시)",
|
||||
"stations": "B06 횡단 암 경계(지반선 평행 · 기본 −0.5 m · 물량 가름용 값) — 토사 측점은 경계 없음"
|
||||
|
||||
@@ -79,6 +79,26 @@ CHECK_CASES = [
|
||||
_bad(lambda g: g["groundwater"].update(wet_rock="full")),
|
||||
_bad(lambda g: g.update(seismic={"enabled": "yes", "kh": 0.6})),
|
||||
_bad(lambda g: g.update(label_map=[], safety_factors=None, groundwater="x")),
|
||||
# 지하수 갈래(532-3 덧) — 갈래 · 비율 · r_u · 수위선 구간 · 점
|
||||
_bad(lambda g: g["groundwater"].update(wet_mode="rain", depth_ratio=1.5, ru=None)),
|
||||
_bad(lambda g: g["groundwater"].update(ru=0.95, lines="x")),
|
||||
_bad(
|
||||
lambda g: g["groundwater"].update(
|
||||
wet_mode="line",
|
||||
lines=[
|
||||
{"from_m": 40, "to_m": 20, "points": [[5, 1]]},
|
||||
{"from_m": 0, "to_m": 20, "points": [[5, 1], [5, 2], [300, -1], "p", [1, 2, 3]]},
|
||||
7,
|
||||
],
|
||||
)
|
||||
),
|
||||
_ground(
|
||||
groundwater={
|
||||
**DEFAULTS["groundwater"],
|
||||
"wet_mode": "line",
|
||||
"lines": [{"from_m": 0, "to_m": 20, "points": [[-10, 2], [0, 1.5], [10, 2]]}],
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
RESOLVE_CASES = [
|
||||
@@ -124,6 +144,10 @@ def test_기본값_꼴은_Types_그대로():
|
||||
"dry": "none",
|
||||
"wet_soil": "surface",
|
||||
"wet_rock": "half_height",
|
||||
"wet_mode": "kds",
|
||||
"depth_ratio": 0.5,
|
||||
"ru": 0.2,
|
||||
"lines": [],
|
||||
}
|
||||
assert DEFAULTS["seismic"] == {"enabled": False, "kh": 0.0}
|
||||
# 07 §4 종합 성토 φ 28~30 — 브레인 기본 25 는 범위 밖이라 아래값 28
|
||||
@@ -152,6 +176,23 @@ def test_범위_검사():
|
||||
("safety_factors.wet.soft", "range"),
|
||||
}
|
||||
assert codes(CHECK_CASES[17]) == {("seismic.enabled", "enum"), ("seismic.kh", "range")}
|
||||
assert codes(CHECK_CASES[19]) == {
|
||||
("groundwater.wet_mode", "enum"),
|
||||
("groundwater.depth_ratio", "range"),
|
||||
("groundwater.ru", "number"),
|
||||
}
|
||||
assert codes(CHECK_CASES[20]) == {("groundwater.ru", "range"), ("groundwater.lines", "type")}
|
||||
assert codes(CHECK_CASES[21]) == {
|
||||
("groundwater.lines.0.to_m", "order"),
|
||||
("groundwater.lines.0.points", "few_points"),
|
||||
("groundwater.lines.1.points.1.0", "order"),
|
||||
("groundwater.lines.1.points.2.0", "range"),
|
||||
("groundwater.lines.1.points.2.1", "range"),
|
||||
("groundwater.lines.1.points.3", "type"),
|
||||
("groundwater.lines.1.points.4", "type"),
|
||||
("groundwater.lines.2", "type"),
|
||||
}
|
||||
assert codes(CHECK_CASES[22]) == set()
|
||||
|
||||
|
||||
def test_파이썬과_TS_같음():
|
||||
@@ -168,7 +209,7 @@ def test_파이썬과_TS_같음():
|
||||
out = json.loads(proc.stdout)
|
||||
assert out["check"] == [model.check_ground(g, LIMITS) for g in CHECK_CASES]
|
||||
assert out["resolve"] == [model.resolve_stations(g) for g in RESOLVE_CASES]
|
||||
assert all(out["check"][1:]) # 틀린 판은 모두 걸림
|
||||
assert all(out["check"][1:-1]) and out["check"][-1] == [] # 틀린 판은 모두 걸림 · 끝 판 통과
|
||||
|
||||
|
||||
def test_측점_적용():
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""532-4 E03 벽(옹벽 · 석축 · 돌쌓기) · 532-3 덧 E02 지하수 갈래.
|
||||
|
||||
- 벽 = B06 화면 TS(`computeStoredLayouts`)를 서버 Node 묶음(`build:e03-walls`)으로 —
|
||||
벽 선 측점 임계면이 벽 폴리곤을 안 지남(모서리 스침 ≤ 0.2 m · 절편 폭 아래) ·
|
||||
벽 없는 측점 = 종전 그대로 · 복사본(구조물 정본 → B06 얹기) 끝까지.
|
||||
- 지하수 = 건기 · 우기 갈래 넷(KDS · 깊이 비율 · r_u · 사용자 수위선) → 엔진 꼴 ·
|
||||
[저장] · [초기화] · 옛 정본 채움.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_532_4_stab_walls_water.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from shapely.geometry import LineString, Polygon
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from E02_StabGround import E02_StabGround_Model as ground_model # noqa: E402
|
||||
from E02_StabGround import E02_StabGround_Router as router # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Types as types # noqa: E402
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import section_model_from_b06 # noqa: E402
|
||||
from E03_StabSection.E03_StabSection_Engine_Search import analyze # noqa: E402
|
||||
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station # noqa: E402
|
||||
from E03_StabSection.E03_StabSection_Walls import ( # noqa: E402
|
||||
design_with_walls,
|
||||
walls_by_station,
|
||||
walls_for_sections,
|
||||
walls_of,
|
||||
)
|
||||
|
||||
# 오른쪽(−) 산 · 왼쪽(+) 골 · 노면 −2~2 · 좌 성토 1:1.5(test_532_2 와 같은 단면)
|
||||
GROUND = [(x / 2, 10.0 - 0.6 * (x / 2)) for x in range(-40, 41)]
|
||||
LINE = [(-20.0, 22.0), (-6.0, 13.6), (-2.6, 10.0), (-2.0, 10.0), (2.0, 10.0), (14.0, 2.0)]
|
||||
DESIGN = {
|
||||
"section_mode": "right_cut",
|
||||
"ground_type": "soil",
|
||||
"road_edges": {
|
||||
"left": {"offset_m": 2.0, "elevation_m": 10.0},
|
||||
"right": {"offset_m": -2.0, "elevation_m": 10.0},
|
||||
},
|
||||
"carriageway_edges": {
|
||||
"left": {"offset_m": 1.5, "elevation_m": 10.0},
|
||||
"right": {"offset_m": -1.5, "elevation_m": 10.0},
|
||||
},
|
||||
"cross_slope_pct": 0,
|
||||
"design_elevation_m": 10.0,
|
||||
"design_line": [{"offset_m": a, "elevation_m": b} for a, b in LINE],
|
||||
}
|
||||
CROSS = {
|
||||
"chainage_m": 100.0,
|
||||
"label": "STA.0+100",
|
||||
"samples": [{"offset_m": a, "elevation_m": b, "valid": True} for a, b in GROUND],
|
||||
"design": DESIGN,
|
||||
}
|
||||
WALL = {
|
||||
"structure_id": "w1",
|
||||
"type_id": "masonry_wet",
|
||||
"name": "돌쌓기(찰)",
|
||||
"start_m": 90.0,
|
||||
"end_m": 110.0,
|
||||
"anchor_m": 100.0,
|
||||
"form": "돌쌓기(찰)",
|
||||
"height_m": 3.0,
|
||||
"tiers": 1,
|
||||
"extras": [],
|
||||
}
|
||||
FAST = {"conditions": ("dry",), "methods": ("bishop", "janbu")}
|
||||
CORNER_M = 0.2 # 임계면이 벽 모서리를 스치는 길이 한도(절편 폭 아래)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def found():
|
||||
return walls_for_sections([{**CROSS, "revetment": WALL}, {**CROSS, "chainage_m": 120.0}])
|
||||
|
||||
|
||||
def test_벽_도형_B06_산식(found):
|
||||
assert list(found) == ["100.00"] # 벽 없는 측점은 안 실음
|
||||
entry = found["100.00"]
|
||||
(wall,) = entry["walls"]
|
||||
assert wall["material_id"] == "wall" and wall["kind"] == "own"
|
||||
poly = Polygon(wall["polygon"])
|
||||
assert poly.is_valid and len(wall["polygon"]) == 4
|
||||
# 높이 = 바닥(근입 0.5 포함) ~ 상단 3 m · 상단 = 노견 높이 · 성토 쪽(좌 +)
|
||||
zs = [p[1] for p in wall["polygon"]]
|
||||
assert max(zs) - min(zs) == pytest.approx(3.0) and max(zs) == pytest.approx(10.0)
|
||||
assert min(p[0] for p in wall["polygon"]) > 0
|
||||
# 지표 = 그려지는 설계선 — 벽 상단(노견)에서 끊겨 바깥은 원지반
|
||||
line = entry["design_line"]
|
||||
gx, gz = zip(*GROUND)
|
||||
out = [(x, z) for x, z in line if x > 2.0 + 1e-3]
|
||||
assert out and all(abs(z - np.interp(x, gx, gz)) < 1e-6 for x, z in out)
|
||||
|
||||
|
||||
def test_임계면이_벽을_안_지남(found):
|
||||
entry = found["100.00"]
|
||||
design = design_with_walls(DESIGN, entry)
|
||||
res = analyze_station(CROSS, design, walls=walls_of(entry), **FAST)
|
||||
poly = Polygon(entry["walls"][0]["polygon"])
|
||||
fill = [c for c in res["cases"] if c["role"] == "fill"]
|
||||
assert fill
|
||||
for case in fill:
|
||||
for got in case["result"]["methods"].values():
|
||||
assert got["min"]["F"] > 0
|
||||
assert LineString(got["min"]["points"]).intersection(poly).length <= CORNER_M
|
||||
plain = analyze_station(CROSS, DESIGN, **FAST)
|
||||
# 벽이 성토 비탈을 대신함 — 성토 쪽 최소 F 가 달라짐 · 절토 쪽은 그대로
|
||||
for a, b in zip(plain["cases"], res["cases"]):
|
||||
if a["role"] == "cut":
|
||||
assert a["min_F"] == pytest.approx(b["min_F"], abs=1e-9)
|
||||
else:
|
||||
assert a["min_F"]["bishop"] != pytest.approx(b["min_F"]["bishop"], abs=1e-3)
|
||||
|
||||
|
||||
def test_벽_없는_측점_모형_그대로():
|
||||
assert design_with_walls(DESIGN, None) is DESIGN and walls_of(None) is None
|
||||
a = section_model_from_b06(CROSS, DESIGN)
|
||||
b = section_model_from_b06(CROSS, design_with_walls(DESIGN, None), walls=walls_of(None))
|
||||
assert json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
|
||||
|
||||
|
||||
def _write_copy(root: Path, with_wall: bool) -> Path:
|
||||
"""E01 동결 복사본 꼴 — db.json(측점 행 · 노선) · 횡단 파일 · 구조물 정본."""
|
||||
from B05_Profile.B05_Profile_Structures_Repository import save_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
|
||||
source = root / "E01_StabRegister" / "source"
|
||||
(source / "B06_Section" / "cross_sections").mkdir(parents=True)
|
||||
(source / "copy_manifest.json").write_text("{}", encoding="utf-8")
|
||||
rows = []
|
||||
for k, chainage in enumerate((80.0, 100.0)):
|
||||
rel = f"B06_Section/cross_sections/cross_{int(chainage):06d}.json"
|
||||
cross = {k: v for k, v in CROSS.items() if k != "design"}
|
||||
cross.update(chainage_m=chainage, center_x=float(chainage), center_y=0.0)
|
||||
(source / rel).write_text(json.dumps(cross), encoding="utf-8")
|
||||
rows.append(
|
||||
{
|
||||
"chainage_m": chainage,
|
||||
"sequence_num": k,
|
||||
"data": json.dumps({"design": DESIGN}),
|
||||
"cross_section_file_path": rel,
|
||||
}
|
||||
)
|
||||
db = {"cross_sections": rows, "route": {"route_data_path": None}}
|
||||
(source / "db.json").write_text(json.dumps(db), encoding="utf-8")
|
||||
if with_wall:
|
||||
item = StructureInstance(
|
||||
type_id="masonry_wet",
|
||||
placement="interval",
|
||||
chainage_m=100,
|
||||
start_m=95,
|
||||
end_m=105,
|
||||
options={"height_m": 3.0},
|
||||
)
|
||||
save_structures(str(source), [item], base_revision=0)
|
||||
return source
|
||||
|
||||
|
||||
def test_복사본_구조물_정본에서_측점별_벽(tmp_path):
|
||||
source = _write_copy(tmp_path / "a", with_wall=True)
|
||||
got = walls_by_station(source)
|
||||
assert list(got) == ["100.00"]
|
||||
(wall,) = got["100.00"]["walls"]
|
||||
assert wall["name"] == "돌쌓기(찰)" and len(wall["polygon"]) == 4
|
||||
assert walls_by_station(_write_copy(tmp_path / "b", with_wall=False)) == {}
|
||||
|
||||
|
||||
# ---- 지하수 갈래 -----------------------------------------------------------------
|
||||
STATIONS = [
|
||||
{"chainage_m": 0.0, "ground_type": "soil", "soil_thickness_m": None},
|
||||
{"chainage_m": 20.0, "ground_type": "blasting_rock", "soil_thickness_m": 0.0},
|
||||
]
|
||||
SURFACE = [[-10.0, 16.0], [-2.0, 10.0], [2.0, 10.0], [10.0, 4.0]]
|
||||
HEIGHTS = {"left": 6.0, "right": 4.0}
|
||||
|
||||
|
||||
def _water(chainage=0.0, condition="wet", **groundwater):
|
||||
ground = ground_model.build_initial(ground_model.load_defaults(), copy.deepcopy(STATIONS))
|
||||
ground["groundwater"].update(groundwater)
|
||||
out = ground_model.groundwater_for(
|
||||
ground, condition, chainage_m=chainage, surface=SURFACE, heights=HEIGHTS
|
||||
)
|
||||
assert types.groundwater(out)["mode"] == out["mode"] # 엔진 꼴 그대로
|
||||
return out
|
||||
|
||||
|
||||
def _depth(out):
|
||||
sx, sz = zip(*SURFACE)
|
||||
return {round(x, 6): round(float(np.interp(x, sx, sz)) - z, 6) for x, z in out["line"]}
|
||||
|
||||
|
||||
def test_지하수_건기_KDS():
|
||||
assert _water(condition="dry") == {"mode": "none"}
|
||||
assert _water(condition="seismic") == {"mode": "none"}
|
||||
assert set(_depth(_water()).values()) == {0.0} # 토사 측점 = 지표 포화
|
||||
rock = _depth(_water(chainage=20.0)) # 연암(겉 = 암 · 두께 0) = 비탈 높이 ½
|
||||
assert rock[10.0] == 3.0 and rock[-10.0] == 2.0
|
||||
|
||||
|
||||
def test_지하수_깊이비율_ru_수위선():
|
||||
ratio = _depth(_water(wet_mode="depth_ratio", depth_ratio=0.25))
|
||||
assert ratio[10.0] == 1.5 and ratio[-10.0] == 1.0 and ratio[2.0] == 1.5
|
||||
assert _water(wet_mode="ru", ru=0.3) == {"mode": "ru", "ru": 0.3}
|
||||
lines = [
|
||||
{"from_m": 0.0, "to_m": 10.0, "points": [[-5.0, 2.0], [5.0, 1.0]]},
|
||||
{"from_m": 0.0, "to_m": 5.0, "points": [[-5.0, 3.0], [5.0, 3.0]]}, # 뒤 구간이 이김
|
||||
]
|
||||
user = _depth(_water(wet_mode="line", lines=lines))
|
||||
assert set(user.values()) == {3.0}
|
||||
one = _depth(_water(wet_mode="line", lines=lines[:1]))
|
||||
assert one[-10.0] == 2.0 and one[2.0] == 1.3 and one[10.0] == 1.0 # 끝은 끝값
|
||||
# 덮지 않은 측점 = KDS
|
||||
assert set(_depth(_water(chainage=20.0, wet_mode="line", lines=lines)).values()) == {2.0, 3.0}
|
||||
|
||||
|
||||
def test_지하수_측점_모형에_끼움():
|
||||
model = section_model_from_b06(CROSS, DESIGN, wet=True)
|
||||
ground = ground_model.build_initial(ground_model.load_defaults(), [])
|
||||
ground["groundwater"].update(wet_mode="depth_ratio", depth_ratio=0.5)
|
||||
gw = ground_model.station_groundwater(ground, "wet", model)
|
||||
left = next(s["height_m"] for s in model["sides"] if s["side"] == "left")
|
||||
sx, sz = zip(*model["surface"])
|
||||
x, z = next(p for p in gw["line"] if p[0] > 1)
|
||||
assert float(np.interp(x, sx, sz)) - z == pytest.approx(0.5 * left)
|
||||
model["groundwater"] = types.groundwater(gw)
|
||||
side = next(s for s in model["sides"] if s["side"] == "left")
|
||||
res = analyze(model, direction=side["direction"], limits=side["limits"], methods=("bishop",))
|
||||
assert res["methods"]["bishop"]["min"]["F"] > 0 # 끼운 모형이 엔진에서 돎
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project(tmp_path, monkeypatch):
|
||||
async def fake_project(_project_id):
|
||||
return tmp_path, {"company_id": 1, "project_kind": "STABILITY"}
|
||||
|
||||
monkeypatch.setattr(router, "_project", fake_project)
|
||||
monkeypatch.setattr(router, "_b06_stations", lambda _root: copy.deepcopy(STATIONS))
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_지하수_저장_초기화_옛정본(project):
|
||||
pid = UUID("9872e9a7-e483-427b-a6b2-72cdbc063bc3")
|
||||
session = {"role": "USER", "company_id": 1}
|
||||
first = asyncio.run(router.get_ground(pid, session))
|
||||
ground = copy.deepcopy(first["ground"])
|
||||
assert ground["groundwater"]["wet_mode"] == "kds"
|
||||
line = {"from_m": 0.0, "to_m": 20.0, "points": [[10.0, 2.0], [-10.0, 1.0]][::-1]}
|
||||
ground["groundwater"].update(wet_mode="line", lines=[line], ru=0.35)
|
||||
asyncio.run(router.put_ground(pid, router.GroundRequest(ground=ground)))
|
||||
stored = json.loads(ground_model.ground_path(project).read_text(encoding="utf-8"))
|
||||
assert stored["groundwater"] == {**ground["groundwater"], "lines": [line]}
|
||||
bad = copy.deepcopy(ground)
|
||||
bad["groundwater"]["lines"][0]["points"] = [[1.0, 1.0]]
|
||||
with pytest.raises(router.HTTPException):
|
||||
asyncio.run(router.put_ground(pid, router.GroundRequest(ground=bad)))
|
||||
back = asyncio.run(router.reset_ground(pid))["ground"]
|
||||
assert back["groundwater"]["wet_mode"] == "kds" and back["groundwater"]["lines"] == []
|
||||
# 옛 정본(갈래 칸 없음) = GET 이 기본값으로 채움 · 있는 칸은 그대로
|
||||
old = json.loads(ground_model.ground_path(project).read_text(encoding="utf-8"))
|
||||
old["groundwater"] = {"dry": "none", "wet_soil": "none", "wet_rock": "half_height"}
|
||||
ground_model.ground_path(project).write_text(json.dumps(old), encoding="utf-8")
|
||||
got = asyncio.run(router.get_ground(pid, session))["ground"]["groundwater"]
|
||||
assert got["wet_soil"] == "none" and got["wet_mode"] == "kds" and got["lines"] == []
|
||||
@@ -13,7 +13,7 @@ export const ui_locales_e = {
|
||||
E02_Card_Zones: ["층 두께 · 구간 덮어쓰기", "Layer Thickness · Zone Overrides"],
|
||||
E02_Card_Stations: ["측점별 적용 지층", "Layers per Station"],
|
||||
E02_Card_Safety: ["기준안전율", "Required Factors of Safety"],
|
||||
E02_Card_Loads: ["노면 하중 · 지하수 · 지진", "Surcharge · Groundwater · Seismic"],
|
||||
E02_Card_Loads: ["노면 하중 · 지진", "Surcharge · Seismic"],
|
||||
E02_Note: [
|
||||
"문헌 추정치 · 현장 시험 없음 — 값은 바꿀 수 있습니다. 칸에 마우스를 올리면 근거가 보입니다.",
|
||||
"Literature estimates without field tests — values are editable. Hover a cell for its source.",
|
||||
@@ -86,6 +86,32 @@ export const ui_locales_e = {
|
||||
E02_Rule_half_height: ["비탈 높이 ½ 포화", "Saturated to ½ height"],
|
||||
E02_Seismic: ["지진 검토", "Seismic check"],
|
||||
E02_Kh: ["수평지진계수 kh", "Horizontal coefficient kh"],
|
||||
E02_Card_Water: ["지하수", "Groundwater"],
|
||||
E02_Water_Hint: [
|
||||
"기본 = KDS 11 70 05 표 4.3-3(우기 토사 · 풍화암 지표 포화 · 암반 비탈 높이 ½) — 우기 기준안전율 깎기 1.2 와 짝. 다른 규칙을 고르면 기준안전율도 함께 검토하세요.",
|
||||
"Default = KDS 11 70 05 Table 4.3-3 (wet: soil · weathered rock saturated to surface · rock to ½ slope height) — pairs with wet cut FS 1.2. Review the factors when choosing another rule.",
|
||||
],
|
||||
E02_WetMode: ["우기 규칙", "Wet-season rule"],
|
||||
E02_WetMode_kds: ["KDS 기본", "KDS default"],
|
||||
E02_WetMode_depth_ratio: [
|
||||
"지표 아래 깊이 = 비탈 높이 × 비율",
|
||||
"Depth below surface = slope height × ratio",
|
||||
],
|
||||
E02_WetMode_ru: ["간극수압비 r_u", "Pore pressure ratio r_u"],
|
||||
E02_WetMode_line: ["사용자 수위선", "User water line"],
|
||||
E02_DepthRatio: ["깊이 비율", "Depth ratio"],
|
||||
E02_Ru: ["r_u", "r_u"],
|
||||
E02_Line_Hint: [
|
||||
"구간마다 점 = offset(m · 좌 +) · 공사 뒤 지표 아래 깊이(m). 덮지 않은 측점은 KDS 규칙.",
|
||||
"Per zone, points = offset (m, left +) · depth below finished surface (m). Uncovered stations use the KDS rule.",
|
||||
],
|
||||
E02_Line_Empty: [
|
||||
"수위선 구간이 없습니다 — 모든 측점이 KDS 규칙.",
|
||||
"No water-line zones — all stations use the KDS rule.",
|
||||
],
|
||||
E02_Btn_AddLine: ["수위선 구간 추가", "Add water line"],
|
||||
E02_Btn_AddPoint: ["점 추가", "Add point"],
|
||||
E02_Col_Points: ["점 (offset · 깊이)", "Points (offset · depth)"],
|
||||
|
||||
/* --- 상태 · 알림 --- */
|
||||
E02_Unsaved: ["저장 안 된 변경이 있습니다.", "There are unsaved changes."],
|
||||
@@ -113,4 +139,5 @@ export const ui_locales_e = {
|
||||
E02_Err_ref: ["지층을 고르세요", "Pick a layer"],
|
||||
E02_Err_empty: ["지층이 하나는 있어야 합니다", "At least one layer is required"],
|
||||
E02_Err_type: ["꼴이 틀립니다", "Invalid shape"],
|
||||
E02_Err_few_points: ["점이 둘 이상 있어야 합니다", "At least two points are required"],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
Reference in New Issue
Block a user