From 3767049ae8101d4225b2a90fcfa327eb7268bdc2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 20:23:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(566-1):=20E05=20=EB=B3=B4=EA=B3=A0?= =?UTF-8?q?=EC=84=9C=20=EC=B8=A1=EC=A0=90=20=3D=20No.+=EA=B1=B0=EB=A6=AC(6?= =?UTF-8?q?1+14.5)=20=E2=80=94=20=EA=B3=B5=EC=9A=A9=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=EC=8D=AC=20=EC=A7=9D=20common=5Futil=5Fstation.py(TS=20station?= =?UTF-8?q?Label=20=C2=B7=20inferStationInterval=20=EA=B1=B0=EC=9A=B8)=20?= =?UTF-8?q?=C2=B7=20=EC=9D=B4=EB=A6=84=20=EC=82=AC=EC=A0=84=20=EB=88=84?= =?UTF-8?q?=EA=B0=80=EB=A1=9C=20=EC=85=88=20=C2=B7=20=ED=95=B4=EC=84=9D=20?= =?UTF-8?q?=EB=95=8C=20=EA=B5=B3=EC=9D=80=20=EC=9D=B4=EB=A6=84=EB=B3=B4?= =?UTF-8?q?=EB=8B=A4=20=EC=82=AC=EC=A0=84=20=EB=A8=BC=EC=A0=80(=EA=B4=91?= =?UTF-8?q?=EC=97=AD=20=C2=B7=20=EB=B3=B4=EA=B0=95=20=C2=B7=20=EC=95=94?= =?UTF-8?q?=EB=B0=98=20=C2=B7=20=ED=99=95=EB=A5=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 시험 534_3 기대 「No.n」 → 「n+0」(견본 label 칸은 이제 안 읽힘) · 545_1 공용 짝 거울 덧. Co-Authored-By: Claude Opus 5.5 --- E05_StabReport/E05_StabReport_Area.py | 2 +- E05_StabReport/E05_StabReport_Cells.py | 9 +++- E05_StabReport/E05_StabReport_Prob.py | 2 +- E05_StabReport/E05_StabReport_Reinforce.py | 2 +- E05_StabReport/E05_StabReport_Rock.py | 2 +- common_util/common_util_station.py | 38 ++++++++++++++ common_util/common_util_station.ts | 1 + resources/tester/test_534_3_e05_report.py | 8 +-- .../tester/test_545_1_station_label_mirror.py | 51 +++++++++++++++++-- 9 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 common_util/common_util_station.py diff --git a/E05_StabReport/E05_StabReport_Area.py b/E05_StabReport/E05_StabReport_Area.py index 87633ad7a..ec0eb0754 100644 --- a/E05_StabReport/E05_StabReport_Area.py +++ b/E05_StabReport/E05_StabReport_Area.py @@ -121,7 +121,7 @@ def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[f for col, cell in template.items(): sheet["칸"][f"{col}{r}"] = dict(cell) ch = float(row["chainage_m"]) - _set(sheet, f"A{r}", row.get("label") or labels.get(round(ch, 3)) or f"{ch:.2f}") + _set(sheet, f"A{r}", labels.get(round(ch, 3)) or row.get("label") or f"{ch:.2f}") _set(sheet, f"B{r}", ch) _set(sheet, f"C{r}", float(row["fs_min"])) _set(sheet, f"D{r}", float(row["si_min"])) diff --git a/E05_StabReport/E05_StabReport_Cells.py b/E05_StabReport/E05_StabReport_Cells.py index fedf4c4ec..8fcf86cfb 100644 --- a/E05_StabReport/E05_StabReport_Cells.py +++ b/E05_StabReport/E05_StabReport_Cells.py @@ -8,6 +8,8 @@ from __future__ import annotations import re from typing import Any +from common_util.common_util_station import infer_station_interval, station_label + CASES = ("dry", "wet", "seismic") SIDE_NAMES = {"cut": "깎기", "fill": "쌓기"} METHOD_NAMES = { @@ -88,7 +90,12 @@ def _seismic(ground: dict[str, Any]) -> bool: def _labels(stations: list[dict[str, Any]]) -> dict[float, str]: - return {round(float(s["chainage_m"]), 3): s.get("label") or "" for s in stations} + """누가 → 측점 이름(`61+14.5` · 간격 = 측점 목록 최빈 · 복사본 label 안 씀).""" + interval = infer_station_interval(stations) + return { + round(float(s["chainage_m"]), 3): station_label(float(s["chainage_m"]), interval) + for s in stations + } def _label_at(labels: dict[float, str], chainage: float) -> str: diff --git a/E05_StabReport/E05_StabReport_Prob.py b/E05_StabReport/E05_StabReport_Prob.py index 4388dce08..9540fb94c 100644 --- a/E05_StabReport/E05_StabReport_Prob.py +++ b/E05_StabReport/E05_StabReport_Prob.py @@ -84,7 +84,7 @@ def fill_prob(sheet: dict[str, Any], recs: list[dict[str, Any]], labels: dict) - def name(rec: dict[str, Any]) -> str: ch = float(rec["chainage_m"]) - return rec.get("label") or labels.get(round(ch, 3)) or f"{ch:.2f}" + return labels.get(round(ch, 3)) or rec.get("label") or f"{ch:.2f}" def where(rec: dict[str, Any]) -> str: return ( diff --git a/E05_StabReport/E05_StabReport_Reinforce.py b/E05_StabReport/E05_StabReport_Reinforce.py index fa8b9865a..e68bcaff3 100644 --- a/E05_StabReport/E05_StabReport_Reinforce.py +++ b/E05_StabReport/E05_StabReport_Reinforce.py @@ -119,7 +119,7 @@ def fill_reinforce(sheet: dict[str, Any], recs: list[dict[str, Any]], labels: di def name(rec: dict[str, Any]) -> str: ch = float(rec["chainage_m"]) - return rec.get("label") or labels.get(round(ch, 3)) or f"{ch:.2f}" + return labels.get(round(ch, 3)) or rec.get("label") or f"{ch:.2f}" r = 6 slopes = [x for x in recs if x["type"] in ("slope", "support")] diff --git a/E05_StabReport/E05_StabReport_Rock.py b/E05_StabReport/E05_StabReport_Rock.py index 7e4e85c07..79c08c8d8 100644 --- a/E05_StabReport/E05_StabReport_Rock.py +++ b/E05_StabReport/E05_StabReport_Rock.py @@ -180,7 +180,7 @@ def _station_text(labels: dict, chainage: Any) -> Any: def _station_rows(sheet, put_row, r: int, entry: dict[str, Any], labels: dict) -> int: slope = entry["slope"] ch = float(entry["chainage_m"]) - name = entry.get("label") or labels.get(round(ch, 3)) or f"{ch:.2f}" + name = labels.get(round(ch, 3)) or entry.get("label") or f"{ch:.2f}" side = ( f"{SIDE_NAMES.get(slope['side'], slope['side'])} · {slope['dip']:g}° / " f"{slope['dip_direction']:g}° · 높이 {slope['height_m']:.1f} m" diff --git a/common_util/common_util_station.py b/common_util/common_util_station.py new file mode 100644 index 000000000..27e5d244e --- /dev/null +++ b/common_util/common_util_station.py @@ -0,0 +1,38 @@ +"""측점 표기 한 벌 — 파이썬 짝(TS `common_util/common_util_station.ts` · 거울 시험 `test_545_1`). + +`station_label` ↔ `stationLabel` · `infer_station_interval` ↔ `inferStationInterval` — 한쪽을 고치면 다른 쪽도. +가져오는 것이 없는 순수 함수(E05 보고서 등 무거운 모듈 없이 씀). +""" + +from __future__ import annotations + +import math +from typing import Any + + +def station_label(chainage: float, interval: float, decimals: int = 1) -> str: + """`측점번호+잔여거리`(예 `61+14.5` · `decimals=0` 이면 거리 정수).""" + safe = interval if interval > 0 else 1.0 + number = math.floor((chainage + 1e-6) / safe) + rest = chainage - number * safe + if abs(rest) < 0.05: + rest = 0.0 + if rest >= safe - 0.05: + number, rest = number + 1, 0.0 + scale = 10 if decimals else 1 + shown = math.floor(rest * scale + 0.5) / scale # JS Math.round + return f"{number}+{int(shown) if shown == int(shown) else shown}" + + +def infer_station_interval(stations: list[dict[str, Any]]) -> float: + """측점 목록의 최빈 간격(0.1 m 반올림 · 같으면 큰 쪽 · 없으면 1).""" + counts: dict[float, int] = {} + for prev, cur in zip(stations, stations[1:]): + difference = float(cur["chainage_m"]) - float(prev["chainage_m"]) + if difference <= 0: + continue + rounded = math.floor(difference * 10 + 0.5) / 10 + counts[rounded] = counts.get(rounded, 0) + 1 + if not counts: + return 1 + return max(counts.items(), key=lambda item: (item[1], item[0]))[0] diff --git a/common_util/common_util_station.ts b/common_util/common_util_station.ts index 5cd2cd085..01181dfb6 100644 --- a/common_util/common_util_station.ts +++ b/common_util/common_util_station.ts @@ -3,6 +3,7 @@ * 측점 화면 표기 한 벌 — B05 · B06 목록 · 폼 · 툴팁 · 종단 표시가 모두 이것을 씀 * (`common_util_svg` 가 다시 내보냄 · B05 는 `formatStation` 이름으로 가져다 씀). 가져오는 것이 없는 * 순수 함수라 서버 Node · 시험 헬퍼에서도 그대로 돎. + * 파이썬 짝 = `common_util/common_util_station.py`(stationLabel · inferStationInterval · 거울 시험 `test_545_1`). * ========================================================================== */ /** 측점 라벨 — "STA" 접두사 없이 `측점번호+잔여거리`(예: `4+0` · `5+20`). diff --git a/resources/tester/test_534_3_e05_report.py b/resources/tester/test_534_3_e05_report.py index f130b9a86..f4021c226 100644 --- a/resources/tester/test_534_3_e05_report.py +++ b/resources/tester/test_534_3_e05_report.py @@ -240,7 +240,9 @@ def test_처음_만듦_측점수_NG_그림(project): s10 = by["부록_절편"]["칸"] block = 3 + SLICES + 1 assert by["부록_절편"]["인쇄"]["쪽나눔"] == [block * (i + 1) for i in range(len(REPS))] - assert "건기" in s10["A1"]["값"] and s10["A1"]["값"].startswith("부록 ① 절편 계산표 — No.3") + assert "건기" in s10["A1"]["값"] and s10["A1"]["값"].startswith( + "부록 ① 절편 계산표 — 3+0" + ) # 566-1 누가 → No.+거리 assert s10["A4"]["값"] == 1 and s10["M4"]["값"] == pytest.approx(6.0 * 1.31) assert s10[f"A{block}"]["값"] == "합계" assert s10[f"E{block}"]["값"] == pytest.approx(sum(9.0 + i for i in range(SLICES))) @@ -346,7 +348,7 @@ def test_광역요약_E07_저장본(project): book, _ = store.load(project) s7 = _sheets(book)["광역요약"]["칸"] 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[f"A{r}"]["값"] for r in (22, 23)] == ["1+0", "2+0"] and "A24" not in s7 assert ( s7["E22"]["값"] == "높음 (100.0 %)" and s7["E23"]["값"] == "보통 (5.6 %)" @@ -387,7 +389,7 @@ def test_갱신_사용자몫_불변_덮임(project): assert ( by["표지"]["칸"]["C10"]["값"] == "산림청" and by["개요"]["칸"]["A22"]["값"] == "사용자 의견" ) - assert by["측점표"]["칸"]["A6"]["값"] == "No.0" # 프로그램 몫은 다시 씀 + assert by["측점표"]["칸"]["A6"]["값"] == "0+0" # 프로그램 몫은 다시 씀 assert book["계산값"]["s2"]["B15"] == {"수": str(len(NG_AT) - 1)} # 수식이 새 값을 따라감 assert meta["fingerprint"] == build.analysis_fingerprint(project) assert store.reset(project) diff --git a/resources/tester/test_545_1_station_label_mirror.py b/resources/tester/test_545_1_station_label_mirror.py index b17f30706..721310e23 100644 --- a/resources/tester/test_545_1_station_label_mirror.py +++ b/resources/tester/test_545_1_station_label_mirror.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -"""545-1 측점 표기 거울 — D04 `station_label`(py) ↔ `common_util_station.ts` `stationLabel(…, 1)`.""" +"""545-1 측점 표기 거울 — D04 `station_label` · `common_util_station.py`(py) ↔ `common_util_station.ts`. + +566-1 넓힘: 공용 파이썬 짝 `station_label(…, decimals 0 · 1)` · `infer_station_interval` 도 TS 와 같음.""" import json import subprocess @@ -10,6 +12,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) +from common_util.common_util_station import infer_station_interval # noqa: E402 +from common_util.common_util_station import station_label as common_label # noqa: E402 from D04_PlanConfirm.D04_PlanConfirm_Export import station_label # noqa: E402 TS = PROJECT_ROOT / "common_util" / "common_util_station.ts" @@ -38,17 +42,56 @@ new Function("exports", "module", js)(box.exports, box); process.stdout.write(JSON.stringify(JSON.parse(process.argv[3]).map((v) => box.exports.stationLabel(v, 20, 1)))); """ +COMMON = r""" +const ts = require(process.argv[1]); +const src = require("fs").readFileSync(process.argv[2], "utf8"); +const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText; +const box = { exports: {} }; +new Function("exports", "module", js)(box.exports, box); +const m = box.exports; +const { values, intervals, lists } = JSON.parse(process.argv[3]); +process.stdout.write(JSON.stringify({ + labels: intervals.flatMap((i) => values.flatMap((v) => [m.stationLabel(v, i, 0), m.stationLabel(v, i, 1)])), + inferred: lists.map((l) => m.inferStationInterval(l.map((c) => ({ chainage_m: c })))), +})); +""" +INTERVALS = [20, 10, 25.5, 0] +LISTS = [ + [], + [0], + [0, 20, 40, 47.3, 60, 80], + [0, 10, 20, 30, 50, 70], + [0, 5, 5, 3, 13], + [0, 19.96, 40], +] -def test_파이썬과_TS_측점_표기_같음(): + +def _node(script: str, payload) -> str: proc = subprocess.run( - ["node", "-e", SCRIPT, str(TYPESCRIPT), str(TS), json.dumps(VALUES)], + ["node", "-e", script, str(TYPESCRIPT), str(TS), json.dumps(payload)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr - assert json.loads(proc.stdout) == [station_label(v) for v in VALUES] + return proc.stdout + + +def test_파이썬과_TS_측점_표기_같음(): + assert json.loads(_node(SCRIPT, VALUES)) == [station_label(v) for v in VALUES] + + +def test_공용_파이썬_짝_TS_와_같음(): + got = json.loads(_node(COMMON, {"values": VALUES, "intervals": INTERVALS, "lists": LISTS})) + assert got["labels"] == [ + common_label(v, i, d) for i in INTERVALS for v in VALUES for d in (0, 1) + ] + assert got["inferred"] == [ + infer_station_interval([{"chainage_m": c} for c in lst]) for lst in LISTS + ] + assert common_label(1234.5, 20) == "61+14.5" and common_label(1234.5, 20, 0) == "61+15" + assert got["inferred"][2:4] == [20, 10] # 최빈 · 10 m 셋 > 20 m 둘 def test_표기_꼴():