feat(566-1): E05 보고서 측점 = No.+거리(61+14.5) — 공용 파이썬 짝 common_util_station.py(TS stationLabel · inferStationInterval 거울) · 이름 사전 누가로 셈 · 해석 때 굳은 이름보다 사전 먼저(광역 · 보강 · 암반 · 확률)

시험 534_3 기대 「No.n」 → 「n+0」(견본 label 칸은 이제 안 읽힘) · 545_1 공용 짝 거울 덧.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 20:23:15 +09:00
co-authored by Claude Opus 5.5
parent 0c78438cb1
commit 3767049ae8
9 changed files with 103 additions and 12 deletions
+1 -1
View File
@@ -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"]))
+8 -1
View File
@@ -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:
+1 -1
View File
@@ -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 (
+1 -1
View File
@@ -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")]
+1 -1
View File
@@ -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"
+38
View File
@@ -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]
+1
View File
@@ -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`).
+5 -3
View File
@@ -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)
@@ -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_표기_꼴():