From 97a505208994d58a410c9fb7b57e04234ca777b0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 04:12:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(536-1):=20E06=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=20=ED=81=AC=EA=B8=B0=200.1=20m=C2=B3=20?= =?UTF-8?q?=E2=80=94=20=EC=9E=91=EC=9D=80=20=EB=B8=94=EB=A1=9D=20=ED=8C=90?= =?UTF-8?q?=EC=A0=95=20=EC=A0=9C=EC=99=B8=20=C2=B7=20=EC=86=8C=EA=B7=9C?= =?UTF-8?q?=EB=AA=A8=20=EB=82=99=EC=84=9D=20=EA=B4=80=EB=A6=AC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=C2=B7=20=EC=84=A4=EC=A0=95=20=EC=B9=B8=20[?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B0=92]=20=C2=B7=20=EA=B1=B0=EB=A5=B8=20?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=20=EC=88=98=20=C2=B7=20E05=20=EC=95=94?= =?UTF-8?q?=EB=B0=98=20=EC=8B=9C=ED=8A=B8=20=C2=B7=20=EB=8F=84=EC=9B=80?= =?UTF-8?q?=EB=A7=90=20=C2=B7=20=EC=8B=9C=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- E05_StabReport/E05_StabReport_Rock.py | 15 ++- E06_StabRock/E06_StabRock_Model.py | 30 ++++-- E06_StabRock/E06_StabRock_Router.py | 1 + E06_StabRock/E06_StabRock_UI_Joints.ts | 27 +++++ E06_StabRock/E06_StabRock_UI_Result.ts | 20 +++- E06_StabRock/E06_StabRock_UI_Style.css | 6 ++ resources/help/E06/10_칸_뜻.md | 2 + resources/tester/test_536_1_e06_rock.py | 132 ++++++++++++++++++++++++ ui_template/ui_template_locale_e06.ts | 11 ++ 9 files changed, 235 insertions(+), 9 deletions(-) diff --git a/E05_StabReport/E05_StabReport_Rock.py b/E05_StabReport/E05_StabReport_Rock.py index 5c2ef3eae..7e4e85c07 100644 --- a/E05_StabReport/E05_StabReport_Rock.py +++ b/E05_StabReport/E05_StabReport_Rock.py @@ -53,6 +53,8 @@ def _summary(rock: dict[str, Any]) -> str: + f" — 위험 모드 평면 {s['modes']['planar']} · 쐐기 {s['modes']['wedge']} · " f"전도 {s['modes']['toppling']}" ) + if s.get("small_blocks"): + text += f" · 소규모 블록 {s['small_blocks']} (판정에서 뺌 · 낙석 관리)" if rock["stale"]: text += " · 입력이 해석 뒤 바뀜 — E06 [해석] 뒤 [보고서 갱신]" return text @@ -100,7 +102,16 @@ def fill_rock(sheet: dict[str, Any], rock: dict[str, Any], labels: dict) -> None if bolt["force"] else "볼트 없음" ) - _set(sheet, "B8", f"{crack_text} · {bolt_text}") + min_block = s.get("min_block_m3", d["min_block_m3"]) + _set( + sheet, + "B8", + f"{crack_text} · {bolt_text} · " + + mark( + f"최소 블록 {min_block:g} m³ (이보다 작으면 판정에서 뺌)", + min_block == d["min_block_m3"], + ), + ) _set(sheet, "B9", _summary(rock)) tpl = {r: _row_styles(sheet, r) for r in (11, 12, 13, 15, 16, 17)} @@ -186,6 +197,8 @@ def _station_rows(sheet, put_row, r: int, entry: dict[str, Any], labels: dict) - notes.append("들림") elif case.get("sliding") in SLIDE_NAMES: notes.append(SLIDE_NAMES[case["sliding"]].format(a=ids[0], b=ids[-1])) + if case.get("small"): + notes.append("소규모 · 낙석 관리") if case.get("persistence_short"): notes.append("지속성 < 활동면") cond = CONDITION_NAMES.get(case["condition"], case["condition"]) diff --git a/E06_StabRock/E06_StabRock_Model.py b/E06_StabRock/E06_StabRock_Model.py index d67907ac5..e5db61549 100644 --- a/E06_StabRock/E06_StabRock_Model.py +++ b/E06_StabRock/E06_StabRock_Model.py @@ -40,6 +40,7 @@ DEFAULT_SETTINGS: dict[str, Any] = { "lateral_planar_deg": engine.LATERAL_PLANAR_DEG, "lateral_toppling_deg": engine.LATERAL_TOPPLING_DEG, "gamma_r": None, # None = E02 측점 암 물성 γt + "min_block_m3": 0.1, # 이보다 작은 블록은 판정에서 뺌(SWedge 최소 쐐기 크기 · 소규모 = 낙석 관리) "water_fill_wet": 0.5, # 우기 = 균열 깊이 · 활동면 높이 ½(KDS 11 70 05 표 4.3-3 암반) "crack": {"mode": "none", "z": None}, "bolt": {"force": 0.0, "plunge": 15.0}, @@ -56,6 +57,7 @@ LIMITS: dict[str, list[float]] = { "lateral_deg": [5.0, 45.0], "gamma_r": [15.0, 35.0], "water_fill_wet": [0.0, 1.0], + "min_block_m3": [0.0, 1000.0], "crack_z": [0.1, 100.0], "bolt_force": [0.0, 5000.0], "bolt_plunge": [-30.0, 60.0], @@ -145,6 +147,8 @@ def check_joints(doc: dict[str, Any]) -> list[dict[str, str]]: bad("settings.gamma_r") if not _in(settings.get("water_fill_wet"), "water_fill_wet"): bad("settings.water_fill_wet") + if not _in(settings.get("min_block_m3"), "min_block_m3"): + bad("settings.min_block_m3") crack = settings.get("crack") if isinstance(settings.get("crack"), dict) else {} if crack.get("mode") not in CRACK_MODES: bad("settings.crack.mode", "enum") @@ -186,6 +190,7 @@ def clean(doc: dict[str, Any]) -> dict[str, Any]: "lateral_toppling_deg": float(s["lateral_toppling_deg"]), "gamma_r": None if s.get("gamma_r") is None else float(s["gamma_r"]), "water_fill_wet": float(s["water_fill_wet"]), + "min_block_m3": float(s["min_block_m3"]), "crack": { "mode": crack["mode"], "z": float(crack["z"]) if crack["mode"] == "depth" else None, @@ -282,6 +287,7 @@ def _cases_for(slope: dict, joints: list[dict], kin: dict, settings: dict, gamma upper = {"dip_direction": slope["dip_direction"], "dip": slope["upper_dip"]} by_id = {j["id"]: j for j in joints} bolt = settings["bolt"] + min_block = settings["min_block_m3"] crack = settings["crack"] if settings["crack"]["mode"] != "none" else None out = [] for jid in kin["planar"]: @@ -304,6 +310,7 @@ def _cases_for(slope: dict, joints: list[dict], kin: dict, settings: dict, gamma short = ( r is not None and j.get("persistence_m") is not None and j["persistence_m"] < r["A"] ) + volume = r["W"] / gamma if r else None # 평면 = 폭 1 m 당 면적(m²) → m³ out.append( { "mode": "planar", @@ -312,6 +319,8 @@ def _cases_for(slope: dict, joints: list[dict], kin: dict, settings: dict, gamma "F": _fmt(r["F"]) if r else None, "formed": r is not None, "persistence_short": short, + "volume": _fmt(volume), + "small": volume is not None and volume < min_block, "W": _fmt(r["W"]) if r else None, "A": _fmt(r["A"]) if r else None, "z": _fmt(r["z"]) if r else None, @@ -353,6 +362,7 @@ def _cases_for(slope: dict, joints: list[dict], kin: dict, settings: dict, gamma "sliding": r["mode"] if r else None, "W": _fmt(r["W"]) if r else None, "volume": _fmt(r["volume"]) if r else None, + "small": r is not None and r["volume"] < min_block, } ) return out @@ -384,7 +394,11 @@ def analyze(source: Path, joints_doc: dict, ground: dict) -> list[dict[str, Any] for case in cases: need = (factors.get(case["condition"]) or {}).get("cut") case["required"] = need - case["ok"] = None if case["F"] is None or need is None else case["F"] >= need + case["ok"] = ( + None + if case["F"] is None or need is None or case["small"] + else case["F"] >= need + ) out.append( { "key": key, @@ -406,18 +420,22 @@ def analyze(source: Path, joints_doc: dict, ground: dict) -> list[dict[str, Any] def _verdict(kin: dict, cases: list[dict]) -> dict[str, Any]: - """판정 — NG(기준 미달 하나라도) · check(전도만 · 블록 안 생김) · OK · none(위험 모드 없음).""" - values = [c["F"] for c in cases if c["F"] is not None] + """판정 — NG(기준 미달 하나라도) · check(전도만 · 블록 안 생김) · OK · none(위험 모드 없음 · + 블록이 모두 소규모). 소규모 블록(최소 블록 크기 미만)은 판정 · 최소 Fs 에서 뺌.""" + judged = [c for c in cases if not c["small"]] + values = [c["F"] for c in judged if c["F"] is not None] min_f = min(values) if values else None - if any(c["ok"] is False for c in cases): + if any(c["ok"] is False for c in judged): verdict = "NG" - elif kin["toppling"] or (cases and not values): + elif kin["toppling"] or (judged and not values): verdict = "check" - elif cases: + elif judged: verdict = "OK" else: verdict = "none" + small = {(c["mode"], tuple(c["ids"])) for c in cases if c["small"]} return { + "small_blocks": len(small), "counts": { "planar": len(kin["planar"]), "wedge": len(kin["wedge"]), diff --git a/E06_StabRock/E06_StabRock_Router.py b/E06_StabRock/E06_StabRock_Router.py index e6547ea12..ee9a5e44b 100644 --- a/E06_StabRock/E06_StabRock_Router.py +++ b/E06_StabRock/E06_StabRock_Router.py @@ -149,6 +149,7 @@ def _summary(stations: list[dict[str, Any]]) -> dict[str, Any]: return { "slopes": len(stations), "verdicts": verdicts, + "small_blocks": sum(item["small_blocks"] for item in stations), "modes": { mode: sum(item["counts"][mode] for item in stations) for mode in ("planar", "wedge", "toppling") diff --git a/E06_StabRock/E06_StabRock_UI_Joints.ts b/E06_StabRock/E06_StabRock_UI_Joints.ts index 6350ae45f..d64d4fecb 100644 --- a/E06_StabRock/E06_StabRock_UI_Joints.ts +++ b/E06_StabRock/E06_StabRock_UI_Joints.ts @@ -29,6 +29,7 @@ export interface RockSettings { lateral_toppling_deg: number; gamma_r: number | null; water_fill_wet: number; + min_block_m3: number; crack: { mode: "none" | "critical" | "depth"; z: number | null }; bolt: { force: number; plunge: number }; } @@ -191,6 +192,30 @@ function select( return box; } +/** 최소 블록 크기 칸 + [기본값] — 기본값 = 근거 후보(사용자 확정 전) */ +function minBlockControl(view: JointsView, s: RockSettings): HTMLElement { + const reset = createButton({ + label: t("E06_Defaults"), + variant: "ghost", + size: "sm", + onClick: () => { + s.min_block_m3 = view.defaults.settings.min_block_m3; + view.change(); + view.rebuild(); + }, + }); + reset.disabled = !view.editable; + const box = el("span", { className: "e06-setting__ctl" }); + box.append( + numberInput(view, s.min_block_m3, "min_block_m3", (v) => (s.min_block_m3 = v ?? NaN), { + step: "0.01", + path: "settings.min_block_m3", + }), + reset, + ); + return box; +} + export function buildJointCards(view: JointsView): HTMLElement[] { const s = view.doc.settings; const setsCard = createPanelCard(t("E06_Card_Joints"), false); @@ -260,6 +285,7 @@ export function buildJointCards(view: JointsView): HTMLElement[] { path: "settings.water_fill_wet", }), ), + settingRow("E06_Set_MinBlock", minBlockControl(view, s)), settingRow( "E06_Set_Crack", select( @@ -299,6 +325,7 @@ export function buildJointCards(view: JointsView): HTMLElement[] { }), ), el("p", { className: "e06-hint", text: t("E06_SettingsHint") }), + el("p", { className: "e06-hint", text: t("E06_MinBlockHint") }), ); return [setsCard, setCard]; } diff --git a/E06_StabRock/E06_StabRock_UI_Result.ts b/E06_StabRock/E06_StabRock_UI_Result.ts index a2d3352f8..b0ce05257 100644 --- a/E06_StabRock/E06_StabRock_UI_Result.ts +++ b/E06_StabRock/E06_StabRock_UI_Result.ts @@ -16,6 +16,7 @@ export interface RockCase { F: number | null; formed: boolean; persistence_short: boolean; + small: boolean; required: number | null; ok: boolean | null; sliding?: string | null; @@ -44,6 +45,7 @@ export interface StationEntry { cases: RockCase[]; counts: { planar: number; wedge: number; toppling: number }; min_F: number | null; + small_blocks: number; verdict: Verdict; } @@ -53,6 +55,7 @@ export interface RockResult { slopes: number; verdicts: Partial>; modes: Record; + small_blocks: number; }; stations: StationEntry[]; } @@ -200,7 +203,13 @@ export function buildDetail(e: StationEntry): HTMLElement { table.append(el("thead", { children: [head] }), el("tbody")); for (const c of e.cases) { const verdict: Verdict = c.F === null ? "check" : c.ok === false ? "NG" : "OK"; - const note = !c.formed ? t("E06_NotFormed") : c.persistence_short ? t("E06_Persist") : ""; + const note = !c.formed + ? t("E06_NotFormed") + : c.small + ? t("E06_Small") + : c.persistence_short + ? t("E06_Persist") + : ""; const tr = el("tr", { attrs: { "data-mode": c.mode, "data-cond": c.condition } }); tr.append( el("td", { text: t(`E06_Mode_${c.mode}` as LocaleKey) }), @@ -208,11 +217,18 @@ export function buildDetail(e: StationEntry): HTMLElement { el("td", { text: t(`E06_Cond_${c.condition}` as LocaleKey) }), el("td", { text: `${fmt(c.F, 3)}${note ? ` ${note}` : ""}` }), el("td", { text: fmt(c.required) }), - el("td", { children: [badge(verdict)] }), + el("td", { children: [c.small ? el("span", { text: "–" }) : badge(verdict)] }), ); table.tBodies[0].append(tr); } box.append(table); + if (e.small_blocks) + box.append( + el("p", { + className: "e06-hint", + text: t("E06_SmallCount").replace("{n}", String(e.small_blocks)), + }), + ); if (kin.toppling.length) box.append(el("p", { className: "e06-hint", text: t("E06_ToppleNote") })); return box; diff --git a/E06_StabRock/E06_StabRock_UI_Style.css b/E06_StabRock/E06_StabRock_UI_Style.css index 47b4a8b49..eb347f792 100644 --- a/E06_StabRock/E06_StabRock_UI_Style.css +++ b/E06_StabRock/E06_StabRock_UI_Style.css @@ -134,6 +134,12 @@ select.e06-input { width: 5.5em; } +.e06-setting__ctl { + display: flex; + align-items: center; + gap: var(--spacing-4); +} + /* ---- 가운데 ---- */ .e06-toolbar { display: flex; diff --git a/resources/help/E06/10_칸_뜻.md b/resources/help/E06/10_칸_뜻.md index 24ec84671..a341c55e0 100644 --- a/resources/help/E06/10_칸_뜻.md +++ b/resources/help/E06/10_칸_뜻.md @@ -28,6 +28,7 @@ | 전도 측방 한계 (°) | 절리가 사면 반대로 기운 방향 차이가 이 각 안이면 전도 후보 | 5 ~ 45 | 20 | | 암 단위중량 (kN/m³ · 비우면 E02) | 덩이 무게 계산 값 · 비우면 지반 조건(E02)의 그 측점 암 물성 γt · 그것도 없으면 25 | 15 ~ 35 | 빈 칸 | | 우기 수위 비 (0~1) | 우기 조건 물 차는 정도 · 0 = 마름 · 1 = 가득 참 | 0 ~ 1 | 0.5 | +| 최소 블록 크기 (m³) | 이보다 작은 블록은 판정(N.G · O.K)과 최소 Fs 에서 빼고 「소규모 · 낙석 관리」로 표시 · 평면은 폭 1 m 당 부피 · 0 = 거르지 않음 · [기본값] 으로 되돌림 | 0 ~ 1,000 | 0.1 | | 인장균열 | 사면 윗면에서 아래로 갈라진 틈을 볼지 · 없음 · 임계(최소 안전율 자리) · 깊이 지정 | - | 없음 | | 균열 깊이 (m) | 「깊이 지정」 때만 보임 · 윗면에서 잰 균열 깊이 | 0.1 ~ 100 | 1 | | 볼트 힘 (평면 kN/m · 쐐기 kN) | 록볼트 · 앵커가 잡아 주는 힘 · 평면은 폭 1 m 당 · 쐐기는 덩이 하나당 | 0 ~ 5,000 | 0 | @@ -35,6 +36,7 @@ - 측방 한계 기본 20° = 상용 Dips 기본 · 지식DB 03 은 전도에 30° 를 씀 — 화면에서 바꿀 수 있음. - 우기 수위 비 기본 0.5 = 비탈 높이 ½ 수위(KDS 11 70 05 암반). +- 최소 블록 크기 기본 0.1 m³ = 상용 쐐기 해석(SWedge)의 최소 쐐기 크기 옵션과 같은 뜻(사용자 확정 전). 아주 얇은 쐐기가 들림으로 N.G 를 내는 것을 막음. 거른 블록 수는 비탈 상세와 보고서 요약에 나옴. - 인장균열 「임계」 = 균열 자리를 마루 뒤로 옮겨 가며 안전율이 가장 낮은 자리를 찾음. 균열 없는 쪽이 더 낮으면 균열 없음으로 셈. 평면 무너짐에만 씀. - 볼트 = 수동 지지력(덩이가 움직이면 버팀). 0 이면 보강 없음. diff --git a/resources/tester/test_536_1_e06_rock.py b/resources/tester/test_536_1_e06_rock.py index 9bfdfd844..fb06963f5 100644 --- a/resources/tester/test_536_1_e06_rock.py +++ b/resources/tester/test_536_1_e06_rock.py @@ -497,3 +497,135 @@ def test_e05_rock_sheet(monkeypatch): rock.fill_rock(sheet, old, {}) assert sheet["칸"]["A13"]["값"] == doc["sets"][0]["name"] assert sheet["칸"]["G13"]["값"] == "-" # 구간 모름 + + +# ---- 최소 블록 크기 (요구 351 · 536-1 덧) -------------------------------------------- +def _cases_with_volume(monkeypatch, volume: float, min_block: float): + """엔진을 가짜 부피로 바꿔 경계(< 최소 블록)만 본다 — 평면(W = γ·면적) · 쐐기 둘.""" + gamma = 25.0 + monkeypatch.setattr( + model.engine, + "plane_fs", + lambda **_k: {"F": 0.5, "W": volume * gamma, "A": 5.0, "z": 0.0}, + ) + monkeypatch.setattr( + model.engine, + "wedge_fs", + lambda **_k: {"F": 0.0, "volume": volume, "line_length": 2.0, "mode": "lift", "W": 1.0}, + ) + joints = [ + {"id": "J1", "dip_direction": 90, "dip": 40, "phi": 30, "c": 0}, + {"id": "J2", "dip_direction": 200, "dip": 50, "phi": 30, "c": 0}, + ] + slope = {"dip_direction": 90, "dip": 68, "upper_dip": 0, "height_m": 5} + kin = {"planar": ["J1"], "wedge": [{"ids": ["J1", "J2"]}], "toppling": []} + settings = dict(model.DEFAULT_SETTINGS, min_block_m3=min_block) + return model._cases_for(slope, joints, kin, settings, gamma, [("dry", 0.0, 0.0)]) + + +def test_min_block_default(): + assert model.DEFAULT_SETTINGS["min_block_m3"] == 0.1 + assert model.upgrade({"sets": [], "settings": {}})["settings"]["min_block_m3"] == 0.1 + + +@pytest.mark.parametrize( + "volume,small", [(0.0999, True), (0.1, False), (0.3, False), (0.0003, True)] +) +def test_min_block_boundary(monkeypatch, volume, small): + """최소 블록 0.1 m³ — 미만만 소규모(경계 = 거르지 않음) · 평면 · 쐐기 같은 규칙.""" + cases = _cases_with_volume(monkeypatch, volume, 0.1) + assert [c["small"] for c in cases] == [small, small] + + +def test_min_block_zero_keeps_all(monkeypatch): + cases = _cases_with_volume(monkeypatch, 0.0003, 0.0) + assert not any(c["small"] for c in cases) + + +def test_min_block_verdict_and_counts(monkeypatch): + """얇은 쐐기(들림 Fs 0)가 N.G 를 내던 자리 — 거르면 판정 · 최소 Fs 에서 빠지고 거른 블록 수가 남음.""" + kin = {"planar": ["J1"], "wedge": [{"ids": ["J1", "J2"]}], "toppling": []} + for min_block, verdict, n_small in ((0.0, "NG", 0), (0.1, "none", 2)): + cases = _cases_with_volume(monkeypatch, 0.0003, min_block) + for c in cases: + c["required"] = 1.2 + c["ok"] = None if c["small"] else c["F"] >= 1.2 + out = model._verdict(kin, cases) + assert (out["verdict"], out["small_blocks"]) == (verdict, n_small) + assert out["min_F"] == (0.0 if not n_small else None) + # 큰 블록 + 작은 블록 섞임 — 작은 것만 빠지고 큰 것으로 판정 + big = dict(_cases_with_volume(monkeypatch, 0.5, 0.1)[0], required=1.2, ok=False) + tiny = dict(_cases_with_volume(monkeypatch, 0.0003, 0.1)[1], required=1.2, ok=None) + tiny["F"] = 0.0 + out = model._verdict(kin, [big, tiny]) + assert (out["verdict"], out["min_F"], out["small_blocks"]) == ("NG", 0.5, 1) + + +def test_min_block_check_range(): + doc = _doc() + doc["settings"]["min_block_m3"] = -1 + assert {"path": "settings.min_block_m3", "code": "range"} in model.check_joints(doc) + doc["settings"]["min_block_m3"] = 2.5 + assert model.check_joints(doc) == [] + assert model.clean(doc)["settings"]["min_block_m3"] == 2.5 + + +def test_analyze_min_block_filter(monkeypatch): + """측점 해석 전체 길 — 최소 블록을 크게 주면 모든 경우 소규모 · ok 없음 · 판정 none.""" + cross, design = _cross("left") + monkeypatch.setattr( + model, + "rock_stations", + lambda _s: [{"chainage_m": 20.0, "label": "STA", "ground_type": "ripping_rock"}], + ) + monkeypatch.setattr(model.src, "cross_section", lambda _s, _c: {**cross, "design": design}) + ground = { + "materials": [], + "stations": [], + "safety_factors": {"dry": {"cut": 1.5}, "wet": {"cut": 1.2}}, + "seismic": {"enabled": False}, + } + base = model.analyze(None, _doc(), ground)[0] + assert not any(c["small"] for c in base["cases"]) and base["small_blocks"] == 0 + doc = _doc() + doc["settings"]["min_block_m3"] = 1000.0 + e = model.analyze(None, doc, ground)[0] + assert all(c["small"] and c["ok"] is None for c in e["cases"]) + assert (e["verdict"], e["min_F"], e["small_blocks"]) == ("none", None, 1) + + +def test_e05_rock_sheet_small(monkeypatch): + """E05 s11 — 최소 블록 글 · 소규모 요약 · 줄 비고.""" + from E05_StabReport import E05_StabReport_Build as build + from E05_StabReport import E05_StabReport_Rock as rock + from E06_StabRock import E06_StabRock_Router as router + + cross, design = _cross("left") + monkeypatch.setattr( + model, + "rock_stations", + lambda _s: [{"chainage_m": 20.0, "label": "STA", "ground_type": "ripping_rock"}], + ) + monkeypatch.setattr(model.src, "cross_section", lambda _s, _c: {**cross, "design": design}) + ground = { + "materials": [], + "stations": [], + "safety_factors": {"dry": {"cut": 1.5}, "wet": {"cut": 1.2}}, + "seismic": {"enabled": False}, + } + doc = _doc() + doc["settings"]["min_block_m3"] = 1000.0 + stations = model.analyze(None, doc, ground) + value = { + "settings": doc["settings"], + "sets": doc["sets"], + "summary": router._summary(stations), + "stations": stations, + "stale": False, + } + sheet = next(s for s in build.form()["시트"] if s["id"] == "s11") + rock.fill_rock(sheet, value, {}) + cells = sheet["칸"] + assert "최소 블록 1000 m³" in cells["B8"]["값"] and "사용자 입력" in cells["B8"]["값"] + assert "소규모 블록 1" in cells["B9"]["값"] + assert any("소규모 · 낙석 관리" in str(c.get("값")) for c in cells.values()) diff --git a/ui_template/ui_template_locale_e06.ts b/ui_template/ui_template_locale_e06.ts index 8eeed1d8d..83273a815 100644 --- a/ui_template/ui_template_locale_e06.ts +++ b/ui_template/ui_template_locale_e06.ts @@ -40,6 +40,12 @@ export const ui_locales_e06 = { "우기 수위 비 (0~1 · 균열 · 활동면 높이 대비)", "Wet water ratio (0–1, of crack / sliding-plane height)", ], + E06_Set_MinBlock: ["최소 블록 크기 (m³)", "Minimum block size (m³)"], + E06_Defaults: ["기본값", "Default"], + E06_MinBlockHint: [ + "최소 블록 크기 기본 0.1 m³ = 상용 쐐기 해석(SWedge)의 최소 쐐기 크기 옵션과 같은 뜻 — 그보다 작은 블록은 판정에서 빼고 「소규모 · 낙석 관리」로 표시 (평면은 폭 1 m 당 부피) · 사용자 확정 전.", + "Default minimum block 0.1 m³ = same meaning as the minimum wedge size option of commercial wedge analysis (SWedge): smaller blocks are left out of the verdict and flagged small — rockfall management (planar = volume per 1 m width) · not yet confirmed by user.", + ], E06_Set_Crack: ["인장균열", "Tension crack"], E06_Crack_None: ["없음", "None"], E06_Crack_Critical: ["임계(최소 안전율 자리)", "Critical (minimum FS)"], @@ -116,6 +122,11 @@ export const ui_locales_e06 = { E06_Cond_dry: ["건기", "Dry"], E06_Cond_wet: ["우기", "Wet"], E06_Cond_seismic: ["지진", "Seismic"], + E06_Small: ["(소규모 · 낙석 관리)", "(small · rockfall management)"], + E06_SmallCount: [ + "소규모 블록 {n}개 · 낙석 관리 — 판정에서 뺌", + "{n} small block(s) · rockfall management — not judged", + ], E06_NotFormed: ["(블록 안 생김)", "(no block)"], E06_Persist: ["(지속성 < 활동면)", "(persistence < plane)"], E06_ToppleNote: [