feat(532-5): E03 2D 해석에 벽(옹벽 · 석축) 연결 · 횡단 없는 측점 띠(E02 · E03)

- analyze2d 가 측점마다 벽(walls_by_station · 복사 지문으로 기억) · 그려지는 설계선을 넘김 · 벽 있는 측점만 지문에 벽 · 벽 없는 측점 = 종전 지문
- 측점 보기 = 같은 벽 모형 · 수위선 = E02 고른 지하수 갈래(station_groundwater)
- 화면 — 「반영 전」 안내 걷음 · 벽 회색 빗금 · 결과 「벽 있음」 · 목록 벽 표시
- result2d · stab/ground 에 missing_stations 칸 덧붙임 · 화면 위 띠(펼침 목록)
- E03 지반 조건 = E02 화면과 같은 upgrade(옛 정본 지하수 칸 기본값)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 21:19:59 +09:00
co-authored by Claude Opus 5.5
parent 17810f00ab
commit 164024ab43
15 changed files with 199 additions and 33 deletions
@@ -0,0 +1,21 @@
/* 빠진 횡단 띠 — E02 · E03 화면 위(E01_StabRegister_UI_Missing.ts) */
.e-missing {
margin: 0 0 8px;
padding: 6px 10px;
border: 1px solid var(--color-warning);
border-left-width: 4px;
border-radius: var(--radius-sm);
background: var(--color-surface-raised);
color: var(--color-text);
font-size: 13px;
}
.e-missing > summary {
cursor: pointer;
font-weight: 600;
}
.e-missing > p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
@@ -0,0 +1,24 @@
/* =============================================================================
* E01_StabRegister_UI_Missing.ts
* 빠진 횡단 띠(요구 351 · PLAN 532-5) — E02 · E03 화면 위 「횡단 없는 측점 N 개 — 해석에서 빠짐」.
* 목록 = 서버 `missing_stations`(E01 복사본에 횡단 파일이 없는 측점 누가거리) · 누르면 펼침.
* ========================================================================== */
import "./E01_StabRegister_UI_Missing.css";
import { el } from "@ui/ui_template_elements";
import { t } from "@ui/ui_template_locale";
/** 빠진 측점이 없으면 빈 칸(띠 없음) */
export function buildMissingBanner(missing: number[] | undefined): HTMLElement {
const list = missing ?? [];
if (!list.length) return el("div");
const box = el("details", {
className: "e-missing",
attrs: { "data-role": "missing-stations", "data-count": String(list.length) },
});
box.append(
el("summary", { text: t("E_Missing_Head").replace("{n}", String(list.length)) }),
el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }),
);
return box;
}
+9 -1
View File
@@ -1,6 +1,7 @@
"""E02 지반 조건 API (요구 347-2 · 3 · 4 · PLAN 532-3).
- `GET /api/projects/{id}/stab/ground` — 작업본(없으면 초기값) + 기본값 + 고칠 수 있는지
+ 횡단 없는 측점(`missing_stations` · 화면 띠)
- `PUT …/stab/ground` — [저장] 검사 뒤 작업본 `E02_StabGround/ground.json`
· 측점(B06 암 경계)은 서버 값
- `POST …/stab/ground/reset` — [초기화] 초기값을 작업본에 덮어씀(재계산 아님)
@@ -27,7 +28,7 @@ from common_util.common_util_project_copy import MANIFEST_NAME
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import get_db_pool
from config.config_system_design import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
from E01_StabRegister.E01_StabRegister_Source import db_snapshot, source_dir
from E01_StabRegister.E01_StabRegister_Source import db_snapshot, missing_stations, source_dir
from E02_StabGround import E02_StabGround_Model as model
router = APIRouter(prefix="/api/projects", tags=["E02 Stab Ground"])
@@ -96,6 +97,12 @@ async def seed_initial(project_id: str) -> None:
ensure_initial(root)
def _missing(project_root: Path) -> list[float]:
"""횡단 없어 해석에서 빠진 측점 — 복사본이 없으면 빈 목록."""
root = source_dir(project_root)
return missing_stations(root) if (root / MANIFEST_NAME).is_file() else []
def _read_working(project_root: Path) -> dict[str, Any] | None:
path = model.ground_path(project_root)
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
@@ -119,6 +126,7 @@ async def get_ground(
"saved": working is not None,
"defaults": defaults,
"editable": _editable(session, project),
"missing_stations": _missing(root),
}
+5 -1
View File
@@ -20,6 +20,7 @@ import {
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import { mountGroupStep } from "../A00_Common/group_workflow";
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
import { buildCards, type E02View } from "./E02_StabGround_UI_Tables";
import {
checkGround,
@@ -33,6 +34,7 @@ interface GroundResponse {
saved?: boolean;
defaults?: GroundDefaults;
editable?: boolean;
missing_stations?: number[];
}
const cacheKey = (projectId: string): string => `e02_ground_cache:${projectId}`;
@@ -95,6 +97,7 @@ export function renderE02StabGround(root: HTMLElement): void {
let view: E02View | null = null;
let refreshStations = (): void => {};
let dirty = false;
let missing: number[] = [];
const paintStatus = (errors: CheckError[]): void => {
const lines: string[] = [];
@@ -127,7 +130,7 @@ export function renderE02StabGround(root: HTMLElement): void {
if (!view) return;
const cards = buildCards(view);
refreshStations = cards.refreshStations;
main.replaceChildren(cards.root);
main.replaceChildren(buildMissingBanner(missing), cards.root);
mark();
};
@@ -221,6 +224,7 @@ export function renderE02StabGround(root: HTMLElement): void {
void (async () => {
try {
const body = await request(`/projects/${projectId}/stab/ground`);
missing = body.missing_stations ?? [];
view = {
ground: body.ground,
defaults: body.defaults as GroundDefaults,
+47 -15
View File
@@ -2,7 +2,7 @@
- `POST /api/projects/{id}/stab/analyze2d` — 전 측점 해석(뒤에서 · 바뀐 측점만) · 같은 지문 = 안 셈
- `GET …/stab/progress` — 진행률
- `GET …/stab/result2d` — 요약 + 측점 목록(판정 · 대표 단면)
- `GET …/stab/result2d` — 요약 + 측점 목록(판정 · 대표 단면 · 벽 수) + 횡단 없는 측점
- `GET …/stab/section/{station_key}` — 그 측점 지층 폴리곤 · 지하수선 · 임계면 · 후보 · 절편 · 격자
- `PUT …/stab/representative` — 대표 단면 켬/끔 → 작업본 `E03_StabSection/representative.json`
- `GET …/stab/corridor3d` — 복사본 노선면(1 m 솎음 · 3D 색칠 보기용)
@@ -10,8 +10,8 @@
입력 = E01 복사본(측점 · 횡단 · DTM) + E02 지반 조건(작업본 · 없으면 초기값)
계산 자리 = ③ 서버 단독.
결과 = 파생 저장본 `_derived/stab2d_station/<측점 지문>` · `_derived/stab2d_summary/<전체 지문>` —
측점 지문 = 엔진 코드 · 복사 지문 · 물성 · 조건 · 하중 · 그 측점 지층. 공유 회사(보기만)는
`require_project_access` 가 POST · PUT 를 막음.
측점 지문 = 엔진 코드 · 복사 지문 · 물성 · 조건 · 하중 · 지하수 갈래 · 그 측점 지층 · 벽.
공유 회사(보기만)는 `require_project_access` 가 POST · PUT 를 막음.
"""
from __future__ import annotations
@@ -49,6 +49,7 @@ from E01_StabRegister import E01_StabRegister_Source as src
from E02_StabGround import E02_StabGround_Model as ground_model
from E02_StabGround import E02_StabGround_Router as e02
from E03_StabSection import E03_StabSection_View as view
from E03_StabSection import E03_StabSection_Walls as walls_mod
from E03_StabSection import E03_StabSection_Worker as worker
from E03_StabSection.E03_StabSection_Engine_Model import (
ground_from_cross,
@@ -94,8 +95,9 @@ def engine_hash() -> str:
def _ground(root: Path) -> dict[str, Any]:
"""E02 지반 조건 — 작업본 · 없으면 초기값."""
return e02._read_working(root) or e02.ensure_initial(root)
"""E02 지반 조건 — 작업본 · 없으면 초기값(옛 정본 = 빠진 지하수 칸 기본값 · E02 화면과 같음)."""
ground = e02._read_working(root) or e02.ensure_initial(root)
return ground_model.upgrade(ground, ground_model.load_defaults())
def _layers(resolved: dict[str, Any] | None) -> list | None:
@@ -109,6 +111,12 @@ def _layers(resolved: dict[str, Any] | None) -> list | None:
return [[resolved["upper"], None]]
@lru_cache(maxsize=4)
def _walls(source: str, _copy_fp: str) -> dict[str, dict[str, Any]]:
"""측점별 벽(532-4 · Node 한 번 약 2 s) — 복사본은 동결이라 복사 지문으로 기억."""
return walls_mod.walls_by_station(source)
def plan_run(root: Path, ground: dict[str, Any]) -> dict[str, Any]:
"""측점마다 입력 · 지문 + 전체 지문(판정 기준 포함) — 복사본이 없으면 400."""
try:
@@ -126,10 +134,16 @@ def plan_run(root: Path, ground: dict[str, Any]) -> dict[str, Any]:
"road_surcharge_kpa": float(ground.get("road_surcharge_kpa") or 0.0),
}
resolved = {chainage_key(r["chainage_m"]): r for r in ground_model.resolve_stations(ground)}
try:
found = _walls(str(source), copy_fp)
except RuntimeError as error:
raise HTTPException(status_code=500, detail=str(error)) from error
items = []
for station in stations:
key = chainage_key(station["chainage_m"])
layers = _layers(resolved.get(key))
entry = found.get(key)
parts = [engine_hash(), copy_fp, shared, key, layers] + ([entry] if entry else [])
items.append(
{
"key": key,
@@ -138,7 +152,8 @@ def plan_run(root: Path, ground: dict[str, Any]) -> dict[str, Any]:
"ground_type": station.get("ground_type"),
"center": [station.get("center_x"), station.get("center_y")],
"layers": layers,
"fp": derived_key(engine_hash(), copy_fp, shared, key, layers),
"walls": entry,
"fp": derived_key(*parts), # 벽 없는 측점 = 종전 지문 꼴
}
)
factors = ground.get("safety_factors") or {}
@@ -153,7 +168,12 @@ def _station_input(source: Path, item: dict[str, Any], sampler: Any) -> dict[str
ground = widen_ground(cross, sampler) if sampler else ground_from_cross(cross)
except (KeyError, TypeError, ValueError):
ground = ground_from_cross(cross)
return {"cross": cross, "design": cross["design"], "ground": [list(p) for p in ground]}
return {
"cross": cross,
"design": walls_mod.design_with_walls(cross["design"], item.get("walls")),
"walls": walls_mod.walls_of(item.get("walls")),
"ground": [list(p) for p in ground],
}
def _sampler(source: Path) -> Any:
@@ -166,7 +186,12 @@ def _sampler(source: Path) -> Any:
def _meta(item: dict[str, Any], design: dict[str, Any]) -> dict[str, Any]:
keys = ("key", "chainage_m", "label", "ground_type", "center", "fp")
return {**{k: item[k] for k in keys}, "section_mode": design.get("section_mode")}
walls = len(walls_mod.walls_of(item.get("walls")) or [])
return {
**{k: item[k] for k in keys},
"section_mode": design.get("section_mode"),
"walls": walls,
}
def _run_worker(job: dict[str, Any], emit: Any) -> None:
@@ -323,8 +348,9 @@ async def result2d(
summary, fresh = _latest_summary(root, plan["fp"])
editable = e02._editable(session, project)
running = bool((_JOBS.get(str(project_id)) or {}).get("running"))
missing = src.missing_stations(plan["source"]) # 횡단 없어 해석에서 빠진 측점(화면 띠)
if not summary:
rows = [view.station_row(item, None, plan["factors"]) for item in plan["items"]]
rows = [view.station_row(_meta(item, {}), None, plan["factors"]) for item in plan["items"]]
view.mark_representative(rows, _overrides(root))
return {
"status": "none",
@@ -334,6 +360,7 @@ async def result2d(
"conditions": plan["shared"]["conditions"],
"factors": plan["factors"],
"stations": rows,
"missing_stations": missing,
}
rows = summary["stations"]
view.mark_representative(rows, _overrides(root))
@@ -344,6 +371,7 @@ async def result2d(
"running": running,
**{k: v for k, v in summary.items() if k != "stations"},
"stations": rows,
"missing_stations": missing,
}
@@ -360,18 +388,22 @@ async def section(project_id: UUID, station_key: str) -> dict[str, Any]:
raise HTTPException(status_code=404, detail="그 측점 해석 결과가 없습니다.")
value = saved["value"]
cross = src.cross_section(plan["source"], row["chainage_m"])
entry = next((it["walls"] for it in plan["items"] if it["key"] == station_key), None)
design = walls_mod.design_with_walls(cross["design"], entry)
kw = {
"materials": plan["shared"]["materials"],
"layers": value["inputs"]["layers"],
"ground": [tuple(p) for p in value["inputs"]["ground"]],
"road_surcharge_kpa": plan["shared"]["road_surcharge_kpa"],
"walls": walls_mod.walls_of(entry),
}
dry = section_model_from_b06(cross, cross["design"], **kw)
wet = section_model_from_b06(cross, cross["design"], wet=True, **kw)
water = {
cond: (wet["groundwater"].get("line") if cond == "wet" else None)
for cond in summary["conditions"]
}
dry = section_model_from_b06(cross, design, **kw)
water = {}
for cond in summary["conditions"]:
# 수위선 = E02 고른 갈래(r_u · 없음 = 선 없음)
model = section_model_from_b06(cross, design, wet=cond == "wet", **kw)
gw = ground_model.station_groundwater(ground, cond, model)
water[cond] = gw.get("line") if gw.get("mode") == "line" else None
out = view.section_view(row, value["out"], dry, water)
out["fresh"] = fresh
return out
+6 -1
View File
@@ -82,7 +82,12 @@ export function buildList(
onToggle(row.key);
});
tr.append(
el("td", { text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key }),
el("td", {
text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key,
children: row.walls
? [el("span", { className: "e03-wall-tag", text: "▦", attrs: { title: t("E03_Walls") } })]
: [],
}),
el("td", {
text: row.sides.length
? roleText(row, { cut: t("E03_CutShort"), fill: t("E03_FillShort") })
+10 -5
View File
@@ -20,12 +20,14 @@ import {
} from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import { mountGroupStep } from "../A00_Common/group_workflow";
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
import { buildList } from "./E03_StabSection_UI_List";
import { buildPlan } from "./E03_StabSection_UI_Plan";
import { buildResult } from "./E03_StabSection_UI_Result";
import {
buildLegends,
createSectionCanvas,
WALL_ID,
type SectionToggles,
} from "./E03_StabSection_UI_Section";
import { openView3d } from "./E03_StabSection_UI_View3D";
@@ -46,6 +48,8 @@ interface Result2d {
computed_at?: string;
summary?: Record<string, unknown>;
stations: StationRow[];
/** 횡단 없어 해석에서 빠진 측점 누가거리(E01 복사본) */
missing_stations?: number[];
}
interface Progress {
@@ -156,6 +160,7 @@ export function renderE03StabSection(root: HTMLElement): void {
panel.append(controls, ngBox, status, listHost, actions);
// ---- 본문 ----
const missingHost = el("div");
const planHost = el("div");
const toolbar = el("div", { className: "e03-card__head" });
const sectionCard = el("div", { className: "e03-card" });
@@ -168,7 +173,7 @@ export function renderE03StabSection(root: HTMLElement): void {
const resultHost = el("div", { className: "e03-result", attrs: { style: "min-height: 0" } });
const body = el("div", { className: "e03-body" });
body.append(sectionCard, resultHost);
main.append(planHost, body);
main.append(missingHost, planHost, body);
const repOf = (row: StationRow): { on: boolean; user: boolean | null } => {
const server = row.representative ?? { auto: [], user: null, on: false };
@@ -290,10 +295,9 @@ export function renderE03StabSection(root: HTMLElement): void {
toolbar.append(title, sideField.root, flags);
const draw = { view, analysis, toggles, picked };
canvas.render(draw);
legendHost.append(
buildLegends(draw),
el("p", { className: "e03-muted", text: t("E03_NoWalls") }),
);
legendHost.append(buildLegends(draw));
if (view.regions.some((r) => r.material_id === WALL_ID))
legendHost.append(el("p", { className: "e03-muted", text: t("E03_Walls_Hint") }));
resultHost.append(
buildResult(view, analysis, picked, (i) => {
picked = i;
@@ -335,6 +339,7 @@ export function renderE03StabSection(root: HTMLElement): void {
result = await api<Result2d>(`${base}/result2d`);
if (before === "none" && result.status === "ok") active = null; // 첫 해석 뒤 = 가장 낮은 측점으로
sections.clear();
missingHost.replaceChildren(buildMissingBanner(result.missing_stations));
const conds = result.conditions;
if (!conds.includes(condition)) condition = conds[0] ?? "dry";
condField.setOptions(
+6 -3
View File
@@ -17,7 +17,7 @@ import {
type Point,
type SectionView,
} from "./logic/E03_StabSection_Logic";
import { materialClass, svgEl } from "./E03_StabSection_UI_Section";
import { WALL_ID, materialClass, svgEl } from "./E03_StabSection_UI_Section";
const fmt = (v: number | null | undefined, d = 2): string =>
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
@@ -32,7 +32,7 @@ export function verdictChip(ok: boolean | null): HTMLElement {
});
}
function kpi(analysis: Analysis): HTMLElement {
function kpi(analysis: Analysis, walls: number): HTMLElement {
const c = analysis.critical;
const dl = el("dl", { className: "e03-kpi" });
const row = (label: string, value: HTMLElement | string, big = false): void => {
@@ -54,6 +54,8 @@ function kpi(analysis: Analysis): HTMLElement {
: "-",
);
row(t("E03_Height"), `${fmt(analysis.height_m)} m`);
if (walls)
row(t("E03_Walls"), el("span", { text: String(walls), attrs: { "data-role": "walls" } }));
if (c?.center) {
row(t("E03_Center"), `${fmt(c.center[0])}, ${fmt(c.center[1])}`);
row(t("E03_Radius"), `${fmt(c.radius)} m`);
@@ -284,7 +286,8 @@ export function buildResult(
}
const head = (key: LocaleKey): HTMLElement =>
el("h3", { className: "e03-card__title", text: t(key) });
root.append(head("E03_Result"), kpi(analysis), methodTable(analysis));
const walls = view.regions.filter((r) => r.material_id === WALL_ID).length;
root.append(head("E03_Result"), kpi(analysis, walls), methodTable(analysis));
root.append(head("E03_Slices"), sliceTable(analysis, picked, onPick));
root.append(head("E03_Fbd"), freeBody(view, analysis, picked));
root.append(head("E03_Graph"), stressGraph(analysis, picked));
+32 -3
View File
@@ -1,11 +1,12 @@
/* =============================================================================
* E03_StabSection_UI_Section.ts
* E03 가운데 횡단(PLAN 532-3) — 지층 색 · 지표 · 지하수선 · 노면 하중 · 등안전율 색 지도(원호 중심 격자
* E03 가운데 횡단(PLAN 532-3) — 지층 색 · 벽(회색 빗금 · 532-5) · 지표 · 지하수선 · 노면 하중 · 등안전율 색 지도(원호 중심 격자
* 최소 안전율) · 원호 중심 점 · 다른 후보(옅게) · 임계 활동면(굵게 · 중심 · 반경) · 절편(누르면 고름).
* 도형 좌표 = 결과 좌표 그대로 · 묶음 transform(`viewMatrix`) 하나 — 계산은 logic.
* 줌 · 팬 = 공용 `attachSvgZoomPan`(휠 = 줌 · 가운데 끌기 = 팬 · 두 번 누름 = 원배율).
* ========================================================================== */
import "./E03_StabSection_UI_Walls.css";
import { t } from "@ui/ui_template_locale";
import { el } from "@ui/ui_template_elements";
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
@@ -27,6 +28,8 @@ import {
const SVG_NS = "http://www.w3.org/2000/svg";
const PAD = 28;
const MAX_SCALE = 30;
/** 벽(옹벽 · 석축) 물성 id — 서버 `WALL_MATERIAL` */
export const WALL_ID = "wall";
export function svgEl<K extends keyof SVGElementTagNameMap>(
tag: K,
@@ -87,7 +90,9 @@ export function buildLegends(draw: SectionDraw): HTMLElement {
for (const m of draw.view.materials.filter((x) => used.has(x.id))) {
const item = el("span", { className: "e03-legend__item" });
item.append(
el("span", { className: `e03-legend__swatch ${materialClass(draw.view, m.id)}` }),
el("span", {
className: `e03-legend__swatch ${m.id === WALL_ID ? "e03-wall-swatch" : materialClass(draw.view, m.id)}`,
}),
m.name,
);
box.append(item);
@@ -121,10 +126,34 @@ export function createSectionCanvas(onPick: (index: number | null) => void): Sec
const ns = { "vector-effect": "non-scaling-stroke" };
const unit = 1 / fit.k; // 1 px 의 결과 좌표 길이
// 벽 빗금 — 결과 좌표 패턴(화면 6 px 간격)
const hatch = svgEl("pattern", {
id: "e03-wall-hatch",
patternUnits: "userSpaceOnUse",
width: 6 * unit,
height: 6 * unit,
patternTransform: "rotate(45)",
});
hatch.append(
svgEl("rect", { width: 6 * unit, height: 6 * unit, class: "e03-wall-hatch__bg" }),
svgEl("line", {
x1: 0,
y1: 0,
x2: 0,
y2: 6 * unit,
"stroke-width": 1.5 * unit,
class: "e03-wall-hatch__line",
}),
);
const defs = svgEl("defs");
defs.append(hatch);
world.append(defs);
for (const region of view.regions) {
const wall = region.material_id === WALL_ID;
const ring = svgEl("polygon", {
points: pointsText(region.polygon),
class: `e03-region ${materialClass(view, region.material_id)}`,
class: wall ? "e03-wall" : `e03-region ${materialClass(view, region.material_id)}`,
...(wall ? { fill: "url(#e03-wall-hatch)", "data-role": "wall" } : {}),
...ns,
});
world.append(ring);
@@ -0,0 +1,23 @@
/* E03 횡단 벽(옹벽 · 석축) 회색 빗금 · 목록 표시(532-5) */
.e03-wall {
stroke: var(--e03-surface-line);
stroke-width: 1;
}
.e03-wall-hatch__bg {
fill: #9a9a9a;
fill-opacity: 0.35;
}
.e03-wall-hatch__line {
stroke: #6b6b6b;
}
.e03-wall-swatch {
background: repeating-linear-gradient(45deg, #6b6b6b 0 2px, #c8c8c8 2px 5px);
}
.e03-wall-tag {
margin-left: 4px;
color: var(--color-text-secondary);
}
+1
View File
@@ -76,6 +76,7 @@ def station_row(
"ground_type": meta.get("ground_type"),
"center": meta.get("center"),
"fp": meta.get("fp"),
"walls": meta.get("walls") or 0,
"sides": list(sides.values()),
"cases": cases,
"min": minimum,
+3 -1
View File
@@ -3,7 +3,7 @@
`python -m E03_StabSection.E03_StabSection_Worker <job.json>` — 서버(`Router.run_all`)가 띄움.
측점마다 프로세스로 나눠 돌리고 끝날 때마다 파생 저장본을 쓰고 한 줄(JSON)을 찍음 → 서버 진행률.
서버 프로세스에서 바로 프로세스 풀을 열면 자식마다 `main.py` 를 다시 읽어 이 모듈을 입구로 둠.
job = {root, keep, kwargs, workers, items: [{fp, meta, layers, cross, design, ground}]}.
job = {root, keep, kwargs, workers, items: [{fp, meta, layers, cross, design, walls, ground}]}.
"""
from __future__ import annotations
@@ -24,6 +24,8 @@ logger = logging.getLogger(__name__)
def _analyze(item: dict[str, Any], kwargs: dict[str, Any]) -> dict[str, Any]:
kw = {**kwargs, "layers": item["layers"], "ground": [tuple(p) for p in item["ground"]]}
if item.get("walls"):
kw["walls"] = item["walls"]
return analyze_station(item["cross"], item["design"], **kw)
@@ -31,6 +31,8 @@ export interface StationRow {
min: Partial<Record<Condition, number | null>>;
verdict: Partial<Record<Condition, Verdict>>;
error: string | null;
/** 그 측점 벽(옹벽 · 석축) 수 — 옛 저장본은 없음 */
walls?: number;
representative?: { auto: string[]; user: boolean | null; on: boolean };
}
+6
View File
@@ -140,4 +140,10 @@ export const ui_locales_e = {
E02_Err_empty: ["지층이 하나는 있어야 합니다", "At least one layer is required"],
E02_Err_type: ["꼴이 틀립니다", "Invalid shape"],
E02_Err_few_points: ["점이 둘 이상 있어야 합니다", "At least two points are required"],
/* --- E02 · E03 빠진 횡단 띠 --- */
E_Missing_Head: [
"횡단 없는 측점 {n} 개 — 해석에서 빠짐",
"{n} stations without a cross-section — left out of the analysis",
],
} as const satisfies Record<string, LocaleEntry>;
+4 -3
View File
@@ -73,9 +73,10 @@ export const ui_locales_e03 = {
E03_Legend_Map: ["등안전율(Fs)", "Safety factor"],
E03_Legend_Layers: ["지층", "Layers"],
E03_Offset: ["거리", "Offset"],
E03_NoWalls: [
"옹벽 · 석축은 아직 해석에 반영 전입니다(벽 없는 단면으로 셈).",
"Retaining walls and masonry are not yet reflected (analyzed without walls).",
E03_Walls: ["벽 있음", "Walls"],
E03_Walls_Hint: [
"옹벽 · 석축(회색 빗금)은 강한 물체로 넣어 활동면이 지나지 않습니다.",
"Retaining walls (gray hatch) are modeled as rigid bodies — slip surfaces do not cross them.",
],
/* --- 우측 결과 --- */