diff --git a/E05_StabReport/E05_StabReport_Area.py b/E05_StabReport/E05_StabReport_Area.py index 843206f31..810d7c93e 100644 --- a/E05_StabReport/E05_StabReport_Area.py +++ b/E05_StabReport/E05_StabReport_Area.py @@ -9,10 +9,10 @@ from __future__ import annotations from pathlib import Path from typing import Any -from E07_StabArea.E07_StabArea_Engine import CLASS_KEYS, RISK_CLASS +from E07_StabArea.E07_StabArea_Engine import CLASS_KEYS, LEVEL_KEYS from E07_StabArea.E07_StabArea_Router import RESULT, load -CLASS_NAMES = ("", "안정", "보통 안정", "준안정", "위험 하한", "위험 상한", "불안정") +LEVEL_NAMES = {"low": "낮음", "mid": "보통", "high": "높음"} def scenario_text(s: dict[str, Any]) -> str: @@ -50,10 +50,13 @@ def _set(sheet: dict[str, Any], a1: str, value: Any, style: int | None = None) - def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[float, str]) -> None: - """s7 — 조건 넷 · 등급별 면적 여섯 · 위험 측점(22 행부터 한 줄씩).""" + """s7 — 조건 넷 · 등급별 면적 여섯 · 측점 등급별 수 · 보통 · 높음 측점(22 행부터 한 줄씩).""" if not area: _set(sheet, "B5", "광역 해석 전 — E07 광역에서 [해석] 뒤 [보고서 갱신]") return + if "station_levels" not in area["summary"]: + _set(sheet, "B5", "옛 판 저장본 — E07 광역에서 [다시 해석] 뒤 [보고서 갱신]") + return s, summary = area["settings"], area["summary"] _set(sheet, "B5", f"중심선 ± {s['band_m']:g} m · 격자 {s['cell_m']:g} m") _set( @@ -67,7 +70,17 @@ def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[f _set(sheet, "B8", str(area["computed_at"]).replace("T", " ")) for k in range(1, 7): _set(sheet, f"C{11 + k}", float(summary["areas_m2"][CLASS_KEYS[k]])) - rows = [r for r in area["stations"] if r.get("worst_class", 0) >= RISK_CLASS] + lo, hi = s["level_lo_pct"], s["level_hi_pct"] + bounds = {"low": f"< {lo:g} %", "mid": f"{lo:g} ~ {hi:g} %", "high": f"≥ {hi:g} %"} + for col, text in zip("EFG", ("측점 등급", "SI<1 셀 비율", "측점 수")): + _set(sheet, f"{col}11", text, 6) + for i, key in enumerate(LEVEL_KEYS): + _set(sheet, f"E{12 + i}", LEVEL_NAMES[key], 7) + _set(sheet, f"F{12 + i}", bounds[key], 7) + _set(sheet, f"G{12 + i}", int(summary["station_levels"][key]), 14) + _set(sheet, "A20", f"3. 위험 측점 — 보통 · 높음 (영향 구역 안 SI < 1 셀 비율 {lo:g} % 이상)") + _set(sheet, "E21", "측점 등급 (SI<1 비율)") + rows = [r for r in area["stations"] if r.get("level") in ("mid", "high")] template = {col: dict(sheet["칸"].get(f"{col}22") or {}) for col in "ABCDEFG"} if not rows: _set(sheet, "A22", "위험 측점 없음") @@ -80,6 +93,6 @@ def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[f _set(sheet, f"B{r}", ch) _set(sheet, f"C{r}", float(row["fs_min"])) _set(sheet, f"D{r}", float(row["si_min"])) - _set(sheet, f"E{r}", CLASS_NAMES[int(row["worst_class"])]) + _set(sheet, f"E{r}", f"{LEVEL_NAMES[row['level']]} ({row['risk_ratio'] * 100:.1f} %)") _set(sheet, f"F{r}", float(row["risk_area_m2"])) _set(sheet, f"G{r}", float(row["slope_max_deg"])) diff --git a/E07_StabArea/E07_StabArea_Engine.py b/E07_StabArea/E07_StabArea_Engine.py index 747dc3281..1ff42172b 100644 --- a/E07_StabArea/E07_StabArea_Engine.py +++ b/E07_StabArea/E07_StabArea_Engine.py @@ -39,6 +39,9 @@ GAMMA_W = 9.81 # kN/m³ SI_EDGES = (1.5, 1.25, 1.0, 0.5) # 등급 1~4 아래 경계 · 5 = (0, 0.5) · 6 = 0 CLASS_KEYS = ("", "stable", "moderate", "quasi", "lower", "upper", "defended") RISK_CLASS = 4 # 이 등급부터 위험(SI < 1) +LEVEL_KEYS = ("low", "mid", "high") # 측점 등급 — 영향 구역 안 위험 셀 비율 기준 +LEVEL_LO_PCT = 5.0 # 낮음 < 이 % ≤ 보통 +LEVEL_HI_PCT = 20.0 # 보통 < 이 % ≤ 높음(이 % 이상 = 높음) FLAT_SLOPE_RAD = 1e-4 # 이보다 평평하면 활동 없음(Fs = ∞) X_STEPS = 20 # SI 확률의 x = R/T 중점 적분 마디 @@ -263,15 +266,28 @@ def band_cells( return (dist <= half_m).reshape(shape), dense[at, 2].reshape(shape) +def station_level( + risk_cells: int, cells: int, lo_pct: float = LEVEL_LO_PCT, hi_pct: float = LEVEL_HI_PCT +) -> str: + """측점 등급 — 위험(SI < 1) 셀 비율 < lo_pct 낮음 · lo_pct 이상 hi_pct 미만 보통 · + hi_pct 이상 높음. + (정수 곱 비교 — 문턱 경계에서 소수 오차 없음)""" + if risk_cells * 100 >= hi_pct * cells: + return "high" + return "mid" if risk_cells * 100 >= lo_pct * cells else "low" + + def station_table( chainages: list[float], cell_chain: np.ndarray, inside: np.ndarray, out: dict[str, np.ndarray], cell_m: float, + lo_pct: float = LEVEL_LO_PCT, + hi_pct: float = LEVEL_HI_PCT, ) -> list[dict[str, Any]]: - """측점마다 맡은 띠 셀(이웃 측점과 중간 누가로 나눔) — - 최소 Fs · SI · 가장 나쁜 등급 · 위험 면적.""" + """측점마다 맡은 띠 셀(이웃 측점과 중간 누가로 나눔 = 앞뒤 ½ 간격 × 띠 폭) — + 최소 Fs · SI · 가장 나쁜 등급 · 위험 면적 · 위험 셀 비율 · 측점 등급.""" chs = np.asarray(sorted(chainages), float) if not len(chs): return [] @@ -288,6 +304,7 @@ def station_table( cls = out["cls"][sel] fs = out["fs"][sel] si = out["si"][sel] + risk = int((cls >= RISK_CLASS).sum()) rows.append( { "chainage_m": float(ch), @@ -295,7 +312,9 @@ def station_table( "fs_min": round(float(np.min(fs)), 3), "si_min": round(float(np.min(si)), 3), "worst_class": int(cls.max()), - "risk_area_m2": round(float((cls >= RISK_CLASS).sum()) * cell_m**2, 1), + "risk_area_m2": round(risk * cell_m**2, 1), + "risk_ratio": round(risk / n, 4), + "level": station_level(risk, n, lo_pct, hi_pct), "slope_max_deg": round(float(np.degrees(np.nanmax(out["slope"][sel]))), 1), } ) diff --git a/E07_StabArea/E07_StabArea_Router.py b/E07_StabArea/E07_StabArea_Router.py index 21b512ec9..159cf7d51 100644 --- a/E07_StabArea/E07_StabArea_Router.py +++ b/E07_StabArea/E07_StabArea_Router.py @@ -46,7 +46,7 @@ from E07_StabArea import E07_StabArea_Engine as engine logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["E07 Stab Area"]) -ENGINE_VERSION = "e07-2" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈 +ENGINE_VERSION = "e07-3" # 엔진 판 — 바뀌면 지문이 달라져 다시 셈 RESULT = Path("E07_StabArea") / "_derived" / "area.json" DEFAULT_MATERIAL = "colluvium" # 표층 붕괴에 가장 취약(지식DB 안정성평가/04 붕적토) RANGE_C_KPA, RANGE_PHI_DEG = ( @@ -76,10 +76,15 @@ class AreaRequest(BaseModel): scenario: Literal["sinmap", "saturated"] = "sinmap" # 표준(습윤지수) · 최악(완전 포화) tr_lo: float = Field(2000.0, ge=1.0, le=1e5) # T/R(m) — SINMAP 기본 보정 2000~3000 tr_hi: float = Field(3000.0, ge=1.0, le=1e5) + # 측점 등급 문턱(%) — 영향 구역 안 SI < 1 셀 비율 · 브레인 정함(사용자 확정 전) + level_lo_pct: float = Field(engine.LEVEL_LO_PCT, gt=0.0, le=100.0) + level_hi_pct: float = Field(engine.LEVEL_HI_PCT, gt=0.0, le=100.0) force: bool = False @model_validator(mode="after") def _ranges(self) -> AreaRequest: + if self.level_hi_pct < self.level_lo_pct: + raise ValueError("측점 등급 문턱: 높음이 보통보다 작습니다.") if self.c_hi < self.c or self.phi_hi < self.phi or self.tr_hi < self.tr_lo: raise ValueError("범위 위가 아래보다 작습니다(c · φ · T/R).") return self @@ -178,7 +183,15 @@ def run( inside, chain = engine.band_cells( z.shape, x0, y0, cell, _route_line(root), float(settings["band_m"]) ) - rows = engine.station_table([s["chainage_m"] for s in stations], chain, inside, out, cell) + rows = engine.station_table( + [s["chainage_m"] for s in stations], + chain, + inside, + out, + cell, + float(settings["level_lo_pct"]), + float(settings["level_hi_pct"]), + ) labels = {round(float(s["chainage_m"]), 3): s.get("label") for s in stations} for row in rows: row["label"] = labels.get(round(row["chainage_m"], 3)) @@ -198,9 +211,7 @@ def run( sum(v for k, v in areas.items() if engine.CLASS_KEYS.index(k) >= engine.RISK_CLASS), 1 ), "fs_min": round(float(np.min(out["fs"][have])), 3) if have.any() else None, - "risk_stations": [ - r["chainage_m"] for r in rows if r.get("worst_class", 0) >= engine.RISK_CLASS - ], + "station_levels": {k: sum(r.get("level") == k for r in rows) for k in engine.LEVEL_KEYS}, "stations": len(rows), } doc = { diff --git a/E07_StabArea/E07_StabArea_UI_Page.ts b/E07_StabArea/E07_StabArea_UI_Page.ts index 7e7726d2e..e23acd3bb 100644 --- a/E07_StabArea/E07_StabArea_UI_Page.ts +++ b/E07_StabArea/E07_StabArea_UI_Page.ts @@ -26,7 +26,8 @@ import { CLASS_COLORS, CLASS_KEYS, FS_STEPS, - RISK_CLASS, + LEVEL_COLORS, + LEVEL_KEYS, decode, type AreaResult, } from "./logic/E07_StabArea_Raster"; @@ -71,6 +72,8 @@ const NUMBER_FIELDS: [string, LocaleKey, "soil" | "rain" | "band"][] = [ ["tr_hi", "E07_TrHi", "rain"], ["band_m", "E07_Band", "band"], ["cell_m", "E07_Cell", "band"], + ["level_lo_pct", "E07_LevelLo", "band"], + ["level_hi_pct", "E07_LevelHi", "band"], ]; const cacheKey = (projectId: string): string => `e07_settings:${projectId}`; @@ -310,7 +313,10 @@ export function renderE07StabArea(root: HTMLElement): void { ["E07_Scenario", scenarioName(result.settings.scenario)], ["E07_Sum_FsMin", s.fs_min === null ? "-" : s.fs_min.toFixed(3)], ["E07_Sum_Risk", `${Math.round(s.risk_area_m2).toLocaleString()} m²`], - ["E07_Sum_RiskStations", `${s.risk_stations.length} / ${s.stations}`], + [ + "E07_Sum_Levels", + `${LEVEL_KEYS.map((k) => `${t(`E07_Level_${k}` as LocaleKey)} ${s.station_levels[k]}`).join(" · ")} / ${s.stations}`, + ], [ "E07_Sum_Covered", `${Math.round(s.covered_area_m2).toLocaleString()} / ${Math.round(s.band_area_m2).toLocaleString()} m²`, @@ -357,26 +363,28 @@ export function renderE07StabArea(root: HTMLElement): void { children: [ el("th", { text: t("E07_Col_Station") }), el("th", { text: t("E07_Col_FsMin") }), - el("th", { text: t("E07_Col_SiMin") }), - el("th", { text: t("E07_Col_Worst") }), + el("th", { text: t("E07_Col_Ratio") }), + el("th", { text: t("E07_Col_Level") }), el("th", { text: t("E07_Col_RiskArea") }), ], }), ...result.stations.map((r) => { - const worst = el("td"); - if (r.worst_class) - worst.append( - swatch(CLASS_COLORS[r.worst_class - 1]), - el("span", { text: t(`E07_Class_${CLASS_KEYS[r.worst_class - 1]}` as LocaleKey) }), + const level = el("td"); + if (r.level) + level.append( + swatch(LEVEL_COLORS[r.level]), + el("span", { text: t(`E07_Level_${r.level}` as LocaleKey) }), ); const tr = el("tr", { - className: (r.worst_class ?? 0) >= RISK_CLASS ? "is-risk" : "", + className: r.level === "high" ? "is-risk" : "", attrs: { "data-chainage": String(r.chainage_m) }, children: [ el("td", { text: shortLabel(r.label, r.chainage_m) }), el("td", { text: r.fs_min === undefined ? "-" : r.fs_min.toFixed(2) }), - el("td", { text: r.si_min === undefined ? "-" : r.si_min.toFixed(2) }), - worst, + el("td", { + text: r.risk_ratio === undefined ? "-" : (r.risk_ratio * 100).toFixed(1), + }), + level, el("td", { text: r.risk_area_m2 === undefined ? "-" : Math.round(r.risk_area_m2).toLocaleString(), diff --git a/E07_StabArea/logic/E07_StabArea_Raster.ts b/E07_StabArea/logic/E07_StabArea_Raster.ts index a9a00b8e7..88ad25692 100644 --- a/E07_StabArea/logic/E07_StabArea_Raster.ts +++ b/E07_StabArea/logic/E07_StabArea_Raster.ts @@ -13,6 +13,8 @@ export interface Grid { ny: number; } +export type LevelKey = "low" | "mid" | "high"; + export interface StationRow { chainage_m: number; label: string | null; @@ -21,6 +23,8 @@ export interface StationRow { si_min?: number; worst_class?: number; risk_area_m2?: number; + risk_ratio?: number; + level?: LevelKey; slope_max_deg?: number; } @@ -30,7 +34,7 @@ export interface AreaSummary { covered_area_m2: number; risk_area_m2: number; fs_min: number | null; - risk_stations: number[]; + station_levels: Record; stations: number; } @@ -62,6 +66,13 @@ export interface Decoded { /** 등급 열쇠(서버 CLASS_KEYS 차례 · 1~6) */ export const CLASS_KEYS = ["stable", "moderate", "quasi", "lower", "upper", "defended"] as const; export const RISK_CLASS = 4; +/** 측점 등급(서버 LEVEL_KEYS 차례) · 색 */ +export const LEVEL_KEYS: readonly LevelKey[] = ["low", "mid", "high"]; +export const LEVEL_COLORS: Record = { + low: "#1a9850", + mid: "#fee08b", + high: "#d73027", +}; /** 등급 색 1~6 — SINMAP 관례(초록 → 빨강) */ export const CLASS_COLORS = ["#1a9850", "#91cf60", "#d9ef8b", "#fee08b", "#fc8d59", "#d73027"]; diff --git a/resources/master_template/report/stab_report.json b/resources/master_template/report/stab_report.json index 0b20ea6d6..469361b41 100644 --- a/resources/master_template/report/stab_report.json +++ b/resources/master_template/report/stab_report.json @@ -2926,7 +2926,7 @@ "서식": 16 }, "A20": { - "값": "3. 위험 측점 (SI < 1 셀이 있는 측점)", + "값": "3. 위험 측점 — 보통 · 높음 (영향 구역 안 SI < 1 셀 비율 5 % 이상)", "서식": 3 }, "B20": { @@ -2967,7 +2967,7 @@ "서식": 6 }, "E21": { - "값": "가장 나쁜 등급", + "값": "측점 등급 (SI<1 비율)", "서식": 6 }, "F21": { diff --git a/resources/master_template/report/stab_report_slots.md b/resources/master_template/report/stab_report_slots.md index c2d8b4d7a..52bb6e56a 100644 --- a/resources/master_template/report/stab_report_slots.md +++ b/resources/master_template/report/stab_report_slots.md @@ -86,6 +86,7 @@ | --- | --- | | `B5` `B6` `B7` `B8` (각 B:H 병합 · 서식 5) | 노선 띠 · 토층 물성 · 시나리오(표준 SINMAP 습윤지수 · 최악 완전 포화) · 해석 때 글 | | `C12`~`C17` | 등급 1~6(안정 · 보통 안정 · 준안정 · 위험 하한 · 위험 상한 · 불안정) 면적 m²(서식 14) · `D` 비율 = 양식 수식 · `C18` 합계 = 양식 수식 | -| 22 행부터 위험 측점 하나 = 한 줄 | A 측점(서식 7) · B 누가거리(17) · C 최소 Fs · D 최소 SI(8) · E 가장 나쁜 등급 글(7) · F 위험 면적 m²(14) · G 최대 경사 °(16) · 없으면 `A22` 「위험 측점 없음」 | +| `E11`~`G14` | 측점 등급 표 — 낮음 · 보통 · 높음별 SI<1 셀 비율 기준 글(7) · 측점 수(14) | +| 22 행부터 보통 · 높음 측점 하나 = 한 줄 | A 측점(서식 7) · B 누가거리(17) · C 최소 Fs · D 최소 SI(8) · E 측점 등급 + SI<1 비율 글(7) · F 위험 면적 m²(14) · G 최대 경사 °(16) · 없으면 `A22` 「위험 측점 없음」 | -- 22 행 서식을 줄마다 복사. 위험 측점 = 맡은 띠 셀에 SI < 1(등급 4 이상)이 있는 측점. +- 22 행 서식을 줄마다 복사. 측점 등급 = 영향 구역(측점 앞뒤 ½ 간격 × 띠 폭) 안 SI < 1(등급 4 이상) 셀 비율 — 낮음 < 5 % · 보통 5~20 % · 높음 ≥ 20 %(문턱 = E07 설정). diff --git a/resources/tester/test_534_3_e05_report.py b/resources/tester/test_534_3_e05_report.py index f88f946c9..5e3b36f1d 100644 --- a/resources/tester/test_534_3_e05_report.py +++ b/resources/tester/test_534_3_e05_report.py @@ -250,6 +250,8 @@ def test_광역요약_E07_저장본(project): "scenario": "sinmap", "tr_lo": 2000.0, "tr_hi": 3000.0, + "level_lo_pct": 5.0, + "level_hi_pct": 20.0, }, "computed_at": "2026-10-25T21:00:00", "stations": [ @@ -260,6 +262,8 @@ def test_광역요약_E07_저장본(project): "si_min": 1.6, "worst_class": 1, "risk_area_m2": 0.0, + "risk_ratio": 0.0, + "level": "low", "slope_max_deg": 12.0, "label": "No.0", }, @@ -270,6 +274,8 @@ def test_광역요약_E07_저장본(project): "si_min": 0.0, "worst_class": 6, "risk_area_m2": 36.0, + "risk_ratio": 1.0, + "level": "high", "slope_max_deg": 41.5, "label": "No.1", }, @@ -280,11 +286,16 @@ def test_광역요약_E07_저장본(project): "si_min": 0.7, "worst_class": 4, "risk_area_m2": 8.0, + "risk_ratio": 0.0556, + "level": "mid", "slope_max_deg": 33.0, "label": "No.2", }, ], - "summary": {"areas_m2": dict(zip(keys, (100.0, 20.0, 12.0, 8.0, 0.0, 36.0)))}, + "summary": { + "areas_m2": dict(zip(keys, (100.0, 20.0, 12.0, 8.0, 0.0, 36.0))), + "station_levels": {"low": 1, "mid": 1, "high": 1}, + }, } path = project / "E07_StabArea" / "_derived" / "area.json" path.parent.mkdir(parents=True) @@ -296,8 +307,11 @@ def test_광역요약_E07_저장본(project): assert [s7[f"C{r}"]["값"] for r in range(12, 18)] == [100.0, 20.0, 12.0, 8.0, 0.0, 36.0] assert [s7[f"A{r}"]["값"] for r in (22, 23)] == ["No.1", "No.2"] and "A24" not in s7 assert ( - s7["E22"]["값"] == "불안정" + s7["E22"]["값"] == "높음 (100.0 %)" + and s7["E23"]["값"] == "보통 (5.6 %)" and s7["F23"]["값"] == 8.0 + and [s7[f"G{r}"]["값"] for r in (12, 13, 14)] == [1, 1, 1] + and s7["F13"]["값"] == "5 ~ 20 %" and s7["B7"]["값"].startswith("표준 — SINMAP") ) vals = book["계산값"]["s7"] diff --git a/resources/tester/test_536_2_e07_area.py b/resources/tester/test_536_2_e07_area.py index f56853037..6bfe08965 100644 --- a/resources/tester/test_536_2_e07_area.py +++ b/resources/tester/test_536_2_e07_area.py @@ -190,6 +190,48 @@ def project(tmp_path, monkeypatch): return tmp_path +def test_측점_등급_문턱_경계(): + """위험 셀 비율 < 5 % 낮음 · 5 % 이상 20 % 미만 보통 · 20 % 이상 높음 (경계 포함은 윗등급).""" + cases = [(0, 100, "low"), (4, 100, "low"), (5, 100, "mid"), (19, 100, "mid")] + cases += [(20, 100, "high"), (100, 100, "high"), (1, 20, "mid"), (1, 21, "low"), (1, 5, "high")] + for risk, cells, want in cases: + assert engine.station_level(risk, cells) == want, (risk, cells) + # 사용자가 바꾼 문턱 + assert engine.station_level(9, 100, 10, 30) == "low" + assert engine.station_level(30, 100, 10, 30) == "high" + + +def test_측점_영향_구역과_비율(): + """측점 0 · 10 · 30 → 영향 구역 = 이웃과 중간 누가로 나눔 · 띠 안 셀만 · 자료 없음 제외.""" + chain = np.arange(0.0, 40.0, 1.0)[None, :] # 1 × 40 셀 · 셀 i 누가 = i + cls = np.ones(chain.shape, np.uint8) + cls[0, [0, 1, 2, 3, 4]] = 4 # 측점 0 영역(0~5 · 정확히 중간 5 는 앞 측점) 6 셀 중 5 → 83 % + cls[0, [6, 7]] = 5 # 측점 10 영역(6~20) 셀 14(19 는 띠 밖) · 위험 6 · 7 · 20 → 21 % + cls[0, [20, 21]] = 6 # 20 은 측점 10 몫 · 측점 30 영역(21~39) 은 21 하나 + cls[0, 39] = 0 # 자료 없음 → 셀 수에서 빠짐(18 셀) 1/18 → 5.6 % + inside = np.ones(chain.shape, bool) + inside[0, 19] = False # 띠 밖 → 빠짐 + out = {k: np.ones(chain.shape) for k in ("fs", "si", "slope")} | {"cls": cls} + rows = engine.station_table([30.0, 0.0, 10.0], chain, inside, out, 2.0) + assert [r["chainage_m"] for r in rows] == [0.0, 10.0, 30.0] + assert [r["cells"] for r in rows] == [6, 14, 18] + assert [r["level"] for r in rows] == ["high", "high", "mid"] + assert rows[0]["risk_ratio"] == round(5 / 6, 4) and rows[1]["risk_ratio"] == round(3 / 14, 4) + assert rows[2]["risk_ratio"] == round(1 / 18, 4) and rows[2]["risk_area_m2"] == 4.0 + # 문턱을 올리면 등급이 내려감 + again = engine.station_table([0.0, 10.0, 30.0], chain, inside, out, 2.0, 15.0, 50.0) + assert [r["level"] for r in again] == ["high", "mid", "low"] + + +def test_측점_문턱_설정_검증(): + base = router_mod.AreaRequest.model_fields + assert (base["level_lo_pct"].default, base["level_hi_pct"].default) == (5.0, 20.0) + ok = dict(gamma_t=18.0, gamma_sat=20.0, c=0.0, c_hi=0.0, phi=30.0, phi_hi=30.0) + router_mod.AreaRequest(**ok, level_lo_pct=10, level_hi_pct=10) + with pytest.raises(ValueError): + router_mod.AreaRequest(**ok, level_lo_pct=30, level_hi_pct=10) + + def test_길_띠_측점표_저장본(project): base = router_mod.defaults(project) assert base["scenario"] == "sinmap" and (base["tr_lo"], base["tr_hi"]) == (2000.0, 3000.0) @@ -216,7 +258,8 @@ def test_길_띠_측점표_저장본(project): rows = doc["stations"] assert [r["chainage_m"] for r in rows] == [0, 20, 40, 60, 80, 100] assert all(r["label"] == f"No.{i}" and r["worst_class"] >= 4 for i, r in enumerate(rows)) - assert summary["risk_stations"] == [0, 20, 40, 60, 80, 100] + assert all(r["level"] == "high" and r["risk_ratio"] > 0.99 for r in rows) # 완전 포화 30° 비탈 + assert summary["station_levels"] == {"low": 0, "mid": 0, "high": 6} assert sum(r["cells"] for r in rows) == (cls > 0).sum() # 지문 = 설정에 따라 바뀜 assert router_mod.fingerprint("s", settings) != router_mod.fingerprint( diff --git a/ui_template/ui_template_locale_e07.ts b/ui_template/ui_template_locale_e07.ts index cf7486a54..a405ebc12 100644 --- a/ui_template/ui_template_locale_e07.ts +++ b/ui_template/ui_template_locale_e07.ts @@ -17,7 +17,7 @@ export const ui_locales_e07 = { E07_ReadOnly: ["보기만 허용된 프로젝트입니다.", "This project is view-only."], E07_Card_Soil: ["토층 물성", "Soil layer"], E07_Card_Rain: ["시나리오 · 포화", "Scenario · saturation"], - E07_Card_Band: ["노선 띠 · 격자", "Route band · grid"], + E07_Card_Band: ["노선 띠 · 격자 · 측점 등급", "Route band · grid · station level"], E07_Material: ["물성(E02 지반 조건)", "Material (E02 ground)"], E07_GammaT: ["γt (kN/m³)", "γt (kN/m³)"], E07_GammaSat: ["γsat (kN/m³)", "γsat (kN/m³)"], @@ -40,6 +40,8 @@ export const ui_locales_e07 = { E07_TrHi: ["T/R 위 (m · 마른 쪽)", "T/R high (m · drier)"], E07_Band: ["띠 폭 ± (m)", "Band half-width (m)"], E07_Cell: ["격자 (m)", "Cell (m)"], + E07_LevelLo: ["측점 보통 문턱 (SI<1 비율 %)", "Station mid threshold (SI<1 share %)"], + E07_LevelHi: ["측점 높음 문턱 (SI<1 비율 %)", "Station high threshold (SI<1 share %)"], E07_Defaults: ["기본값", "Defaults"], E07_Analyze: ["해석", "Analyze"], E07_Reanalyze: ["다시 해석", "Re-analyze"], @@ -74,20 +76,23 @@ export const ui_locales_e07 = { ], E07_Sum_FsMin: ["띠 안 최소 Fs", "Min Fs in band"], E07_Sum_Risk: ["위험 면적 (SI < 1)", "Hazard area (SI < 1)"], - E07_Sum_RiskStations: ["위험 측점", "Hazard stations"], + E07_Sum_Levels: ["측점 등급 (낮음 · 보통 · 높음)", "Station levels (low · mid · high)"], E07_Sum_Covered: ["지형 있는 띠 / 띠", "Band with terrain / band"], E07_Sum_Time: ["해석", "Computed"], E07_Areas_Title: ["등급별 면적", "Area by class"], E07_Stations_Title: ["측점별 위험 (누르면 지도로)", "Hazard by station (click to locate)"], + E07_Level_low: ["낮음", "Low"], + E07_Level_mid: ["보통", "Medium"], + E07_Level_high: ["높음", "High"], E07_Col_Class: ["등급", "Class"], E07_Col_Area: ["면적 (m²)", "Area (m²)"], E07_Col_Station: ["측점", "Station"], E07_Col_FsMin: ["최소 Fs", "Min Fs"], - E07_Col_SiMin: ["최소 SI", "Min SI"], - E07_Col_Worst: ["나쁜 등급", "Worst class"], + E07_Col_Ratio: ["SI<1 비율 (%)", "SI<1 share (%)"], + E07_Col_Level: ["측점 등급", "Station level"], E07_Col_RiskArea: ["위험 m²", "Hazard m²"], E07_Defaults_Source: [ - "기본값 = 근거 후보 · 바꿀 수 있음 — 물성 = E02 붕적토 · T/R 2,000~3,000 m = SINMAP 설명서 기본 보정 · 뿌리 점착 1 kPa = 국내 연구(산림 0~5 kPa · 나지 · 절성토면 0).", - "Defaults are evidence-based candidates and editable — soil = E02 colluvium · T/R 2,000–3,000 m = SINMAP manual default · root cohesion 1 kPa = Korean studies (forest 0–5 kPa · bare / cut slopes 0).", + "기본값 = 근거 후보 · 바꿀 수 있음 — 물성 = E02 붕적토 · T/R 2,000~3,000 m = SINMAP 설명서 기본 보정 · 뿌리 점착 1 kPa = 국내 연구(산림 0~5 kPa · 나지 · 절성토면 0) — 사용자 확정 전 · 측점 등급 문턱 5 % · 20 % = 영향 구역(측점 앞뒤 ½ 간격 × 띠 폭) 안 SI<1 셀 비율 — 사용자 확정 전.", + "Defaults are evidence-based candidates and editable — soil = E02 colluvium · T/R 2,000–3,000 m = SINMAP manual default · root cohesion 1 kPa = Korean studies (forest 0–5 kPa · bare / cut slopes 0) — not yet confirmed by the user · station level thresholds 5 % · 20 % = share of SI<1 cells in the station zone (half-spacing either side × band width) — not yet confirmed by the user.", ], } as const satisfies Record;