feat(유토곡선): 354-2 종단 기준 숨김 — 그리기 횡단 기준만 · 저장된 종단 범례는 횡단으로 맞춤 · 계산 코드 · 묶음 단계 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
2026-10-16 20:36:15 +09:00
co-authored by Claude Opus 5.5
parent ec02165954
commit f331f2094e
3 changed files with 125 additions and 4 deletions
@@ -6,7 +6,7 @@
* 이 프로그램은 파일 입력 시 기본 설정값으로 B06 몫까지 미리 계산해 둔다 — 종횡단 상세
* 조회(`fetchSectionDetail`)가 지정 설계 없는 측점에 기본 설계를 즉석 계산해 얹으므로
* (`_attach_default_designs`), B05 진입 시점에 이미 **횡단 단면적 기반 정식 곡선**을 그릴
* 수 있다. 그래서 B06과 똑같이 `computeMassHaulSeries`(횡단 기준 + 종단 개략 비교)를 쓰고,
* 수 있다. 그래서 B06과 똑같이 `computeMassHaulSeries`(횡단 기준만 · 종단 기준 숨김 354-2)를 쓰고,
* 범례·토량 분배·balloon·요약도 전부 공용 모듈 그대로 쓴다.
*
* 표시 토글 sessionStorage 키도 B06과 **공유**한다 — 두 화면은 같은 그림의 두 창이라
@@ -173,8 +173,11 @@ function readVisible(): Set<string> {
if (!raw) return new Set(DEFAULT_VISIBLE);
const parsed = JSON.parse(raw) as unknown;
// 기준 키는 라디오라 항상 하나로 눌러 맞춘다(B06 읽기와 같은 규칙).
// 종단 기준은 안 그림(354-2) — 저장된 범례가 종단이면 횡단으로 맞춘다.
return Array.isArray(parsed)
? normalizeVisibleBasis(new Set(parsed.map(String)))
? normalizeVisibleBasis(
new Set(parsed.map((key) => (key === "longitudinal" ? "cross" : String(key)))),
)
: new Set(DEFAULT_VISIBLE);
} catch {
return new Set(DEFAULT_VISIBLE);
@@ -370,7 +373,7 @@ export function createRouteMassHaulPanel(
overlay.hidden = !open;
syncHandlePosition();
// 접혀 있어도 상단줄에는 값이 서야 한다 — 이때는 **횡단 기준만** 적분한다.
// 펼친 경우에는 아래에서 두 기준을 내므로 그 결과로 총괄값을 낸다(계산 한 번).
// 펼친 경우에는 아래에서 낸 계열로 총괄값을 낸다(계산 한 번).
if (!open) {
reportSummary(params);
return;
@@ -400,7 +403,8 @@ export function createRouteMassHaulPanel(
.map((chainage) => `${chainage.toFixed(2)} m`)
.join(" · ")}${stale.length > 5 ? " …" : ""}`
: "다시 계산 중…";
// B06과 같은 계산 — 횡단 기준(정식)과 종단 기준(개략 비교)을 함께 낸다.
// B06과 같은 계산 — 횡단 기준(정식)만 낸다. 종단 기준은 안 셈 · 안 그림 · 범례 단추 없음
// (354-2 · 계산 코드는 공용에 남김).
// 결과를 아껴 두지 **않는다** — 횡단 설계가 제자리에서 갈리므로 객체 동일성으로는
// 바뀐 것을 못 잰다(2026-09-04 캐시를 넣었다가 곡선이 안 따라와 되돌림).
const series: MassHaulSeries[] = computeMassHaulSeries(
@@ -408,6 +412,7 @@ export function createRouteMassHaulPanel(
params.crossSections,
context.conversion,
context.naturalSpoilMinSlope,
["cross"],
);
// 총괄값은 방금 낸 계열에서 꺼낸다 — 접힘 경로처럼 따로 적분하지 않는다.
emitSummary(series);
@@ -0,0 +1,57 @@
/* 354-2 종단 기준 숨김 — 진짜 `readVisible`(B05 유토곡선) · `normalizeVisibleBasis` ·
* `computeMassHaulSeries`(공용)를 잘라 돌려 JSON 으로 냄.
* · 저장된 범례 = 종단 → 횡단으로 맞춤 · 분배 켬은 그대로
* · 기준 ["cross"] = 횡단 계열 하나 · 기본(묶음 단계) = 두 기준 그대로 */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8");
const transpile = (code) =>
ts.transpileModule(code, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const block = (src, head) => {
const at = src.indexOf(head);
if (at < 0) throw new Error(`없음: ${head}`);
return src.slice(at, src.indexOf("\n}", at) + 2).replace(/^export /, "");
};
const util = read("common_util", "common_util_mass_haul.ts");
const mass = read("B05_Profile", "B05_Profile_UI_Profile_MassHaul.ts");
let stored = null;
const src = [
'const SERIES_BASES = ["cross", "longitudinal"];',
'const DEFAULT_VISIBLE = ["cross"];',
block(util, "export function normalizeVisibleBasis("),
block(mass, "function readVisible(): Set<string> {"),
block(util, "export function computeMassHaulSeries("),
].join("\n");
const api = new Function(
"readStateRaw",
"designedSections",
"crossAreaSamples",
"longitudinalAreaSamples",
"integrate",
`${transpile(src)}; return { readVisible, computeMassHaulSeries };`,
)(
() => stored,
(sections) => sections,
() => [{ basis: "cross" }],
() => [{ basis: "longitudinal" }],
(samples) => ({ from: samples[0].basis }),
);
const visible = (raw) => ((stored = raw), [...api.readVisible()].sort());
process.stdout.write(
JSON.stringify({
longitudinal: visible('["longitudinal"]'),
longitudinalBalance: visible('["longitudinal","balance"]'),
cross: visible('["cross"]'),
empty: visible(null),
broken: visible("{"),
crossOnly: api.computeMassHaulSeries({}, [], {}, undefined, ["cross"]).map((s) => s.key),
both: api.computeMassHaulSeries({}, [], {}).map((s) => s.key),
}),
);
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""354-2 유토곡선 종단 기준 숨김 — 안 셈 · 안 그림 · 범례 단추 없음(코드 남김).
- 그리기 = 횡단 기준만(`["cross"]`) · 범례는 낸 계열마다라 종단 단추가 안 섬.
- 저장된 범례가 종단(`longitudinal`)이면 횡단으로 맞춤 · 분배 켬은 그대로.
- 계산 묶음 단계(MassHaul_Stage)는 두 기준 그대로 · 공용 계산 코드 남김.
`helper_354_2_masshaul_cross_only.cjs` 가 진짜 TS 를 잘라 돌림.
"""
import json
import os
import re
import subprocess
from pathlib import Path
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = Path(HERE).parents[1]
@pytest.fixture(scope="module")
def r():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_354_2_masshaul_cross_only.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_저장된_종단_범례는_횡단으로(r):
assert r["longitudinal"] == ["cross"]
assert r["longitudinalBalance"] == ["balance", "cross"]
assert r["cross"] == ["cross"]
assert r["empty"] == ["cross"] and r["broken"] == ["cross"]
def test_기준_횡단만_계산(r):
assert r["crossOnly"] == ["cross"]
assert r["both"] == ["cross", "longitudinal"] # 공용 기본값 = 묶음 단계 그대로
def test_그리기는_횡단만_부름():
mass = (ROOT / "B05_Profile/B05_Profile_UI_Profile_MassHaul.ts").read_text(encoding="utf-8")
calls = re.findall(r"computeMassHaulSeries\(([^;]*?)\)", mass, re.S)
assert calls and all('["cross"]' in call for call in calls)
def test_묶음_단계와_공용_코드는_그대로():
stage = (ROOT / "B05_Profile/logic/B05_Profile_MassHaul_Stage.ts").read_text(encoding="utf-8")
assert '["cross"]' not in stage
util = (ROOT / "common_util/common_util_mass_haul.ts").read_text(encoding="utf-8")
assert 'const SERIES_BASES: MassHaulBasis[] = ["cross", "longitudinal"];' in util
assert "longitudinalAreaSamples(longitudinal, sections)" in util