feat(476-2 추천 초기 종단): 추천 모달에 적용될 초기 종단 선 · 한 줄 · [적용] 확인 글
- 서버 initial_profile 펴기(세션에 안 적음) · 실패면 이유 한 줄 - 종단 그림에 초기 종단 선(주황 점선 · 끔이면 지반선 + 초기 종단) · 범례 - 한 줄 = 꼭짓점 · 종단곡선 · 직선화 수 · 최대 종단경사(L) - [적용] = 「종단 손 편집은 새 초기 종단으로 바뀜」 확인 뒤 - 시험 test_476_2 · test_421 [적용] 단언 = 확인 창 뒤 고른 후보 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/* 476-2 추천 모달 「적용될 초기 종단」 헬퍼 — `_Initial_Model` · `_Recommend_Profile` 을 그 자리에서
|
||||
* 트랜스파일해 Node 로 돌려 서버 `initial_profile` 펴기 · 한 줄 · 그림 · 캐시 빼기를 JSON 한 벌로.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
function memoryStorage() {
|
||||
const map = new Map();
|
||||
return {
|
||||
getItem: (key) => (map.has(key) ? map.get(key) : null),
|
||||
setItem: (key, value) => map.set(key, String(value)),
|
||||
removeItem: (key) => map.delete(key),
|
||||
key: (index) => [...map.keys()][index] ?? null,
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
_map: map,
|
||||
};
|
||||
}
|
||||
const session = memoryStorage();
|
||||
globalThis.window = { localStorage: memoryStorage(), sessionStorage: session };
|
||||
globalThis.fetch = () => Promise.resolve({ ok: false });
|
||||
|
||||
const STUBS = { "@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback } };
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.endsWith(".css")) return {};
|
||||
if (request.startsWith("@ui/"))
|
||||
return loadTs(path.join(ROOT, "ui_template", `${request.slice(4)}.ts`));
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const model = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Initial_Model.ts"));
|
||||
const prof = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Recommend_Profile.ts"),
|
||||
);
|
||||
|
||||
// 476-1 응답 꼴(분석 2.3) — 결과 옆 `initial_profile`.
|
||||
const initialRaw = {
|
||||
fingerprint: "abc",
|
||||
with_profile: false,
|
||||
curve_length_m: 20,
|
||||
stations_m: [0, 20, 40],
|
||||
ground_m: [100, 103, 101],
|
||||
plan_m: [100, 102, 101],
|
||||
line: [
|
||||
[0, 100],
|
||||
[10, 101],
|
||||
[20, 102],
|
||||
[30, 101.5],
|
||||
[40, 101],
|
||||
],
|
||||
pvi_count: 3,
|
||||
curve_count: 1,
|
||||
straightened: 2,
|
||||
max_grade_pct: 10.04,
|
||||
warnings: [],
|
||||
seconds: 0.1,
|
||||
};
|
||||
const answer = {
|
||||
nodes: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 40, y: 0 },
|
||||
],
|
||||
curves: [],
|
||||
initial_profile: initialRaw,
|
||||
};
|
||||
const off = model.normalizeRouteInitial(answer, "follow", null, null, false);
|
||||
model.writeInitialChoice("p-476", off);
|
||||
const failed = model.normalizeRouteInitial(
|
||||
{ ...answer, initial_profile: { error: "지반고 종단 — 측점이 모자랍니다" } },
|
||||
"follow",
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
const balance = { stationsM: [0, 20, 40], groundM: [100, 103, 101], planM: [100, 101, 101] };
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
parsed: off.initialProfile,
|
||||
cached: model.readInitialChoice("p-476"),
|
||||
line: prof.initialLine(off.initialProfile),
|
||||
svgOff: prof.profileSvg(null, off.initialProfile),
|
||||
svgOn: prof.profileSvg(balance, off.initialProfile),
|
||||
svgOld: prof.profileSvg(balance),
|
||||
legendOff: prof.profileLegend(false, true),
|
||||
failed: failed.initialProfile,
|
||||
failedLine: prof.initialLine(failed.initialProfile),
|
||||
missing: model.normalizeRouteInitial({ ...answer, initial_profile: undefined }, "follow", null)
|
||||
.initialProfile,
|
||||
shortLine: prof.parseInitialProfile({ ...initialRaw, line: [[0, 100]] }),
|
||||
}),
|
||||
);
|
||||
@@ -43,7 +43,7 @@ def test_창_단추와_재계산() -> None:
|
||||
assert "rerun.disabled = busy || invalid.size > 0;" in rec # 늘 켜짐
|
||||
assert 'rerun.addEventListener("click", () => void run(true));' in rec
|
||||
assert "const withProfile = profileOn;" in rec
|
||||
assert "params.onApply(preview);" in rec
|
||||
assert "params.onApply(picked);" in rec # 476 — 확인 창 뒤 미리 본 그 후보
|
||||
|
||||
|
||||
def test_요청과_적용에_종단_반영() -> None:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""476-2 추천 모달 — 「적용될 초기 종단」 선 · 한 줄 · [적용] 확인 글(요구 276).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_476_2_recommend_initial_line.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
HELPER = Path(__file__).with_name("helper_476_2_initial_profile.cjs")
|
||||
|
||||
|
||||
def _out() -> dict:
|
||||
return json.loads(
|
||||
subprocess.run(
|
||||
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", check=True
|
||||
).stdout
|
||||
)
|
||||
|
||||
|
||||
def test_펴기와_캐시_빼기() -> None:
|
||||
out = _out()
|
||||
p = out["parsed"]
|
||||
assert p["withProfile"] is False and p["curveLengthM"] == 20
|
||||
assert p["pviCount"] == 3 and p["curveCount"] == 1 and p["straightened"] == 2
|
||||
assert p["line"][3] == [30, 101.5] and p["planM"] == [100, 102, 101]
|
||||
assert "initialProfile" not in out["cached"] # 세션에 안 적음
|
||||
assert out["missing"] is None # 옛 서버 답
|
||||
assert out["shortLine"] is None # 선 점 하나 = 꼴 틀림
|
||||
|
||||
|
||||
def test_한_줄과_실패_글() -> None:
|
||||
out = _out()
|
||||
assert out["line"] == "초기 종단: 꼭짓점 3 · 종단곡선 1 · 직선화 2 · 최대 종단경사 10.0%(L 20)"
|
||||
assert out["failed"] == {"error": "지반고 종단 — 측점이 모자랍니다"}
|
||||
assert out["failedLine"].startswith("초기 종단을 셈하지 못했습니다 — 지반고 종단")
|
||||
|
||||
|
||||
def test_그림_선() -> None:
|
||||
out = _out()
|
||||
# 끔 = 지반선 + 초기 종단(계획선 빔)
|
||||
assert 'class="is-ground" d="M6.0 90.0L300.0 6.0L594.0 62.0"' in out["svgOff"]
|
||||
assert 'class="is-plan" d=""' in out["svgOff"]
|
||||
initial = 'class="is-initial" d="M6.0 90.0L153.0 62.0L300.0 34.0L447.0 48.0L594.0 62.0"'
|
||||
assert initial in out["svgOff"] and initial in out["svgOn"]
|
||||
assert 'class="is-plan" d="M6.0 90.0L300.0 62.0L594.0 62.0"' in out["svgOn"]
|
||||
assert "is-initial" not in out["svgOld"] # 옛 답 = 지반 · 균형만
|
||||
assert "is-plan" not in out["legendOff"] and "적용될 초기 종단" in out["legendOff"]
|
||||
|
||||
|
||||
def test_모달_배선과_확인_글() -> None:
|
||||
rec = (B05 / "B05_Profile_UI_RouteEdit_Recommend.ts").read_text(encoding="utf-8")
|
||||
assert '<div data-line="initial" hidden></div>' in rec
|
||||
assert "profileSvg(balance, initialShape)" in rec
|
||||
assert 'line("profile").hidden = !balance && !initialShape;' in rec
|
||||
apply = rec[rec.index('applyButton.addEventListener("click"') :][:400]
|
||||
assert 'showConfirmDialog(APPLY_CONFIRM, "적용")' in apply
|
||||
assert apply.index("showConfirmDialog") < apply.index("params.onApply(picked);")
|
||||
assert "종단 손 편집은 새 초기 종단으로 바뀝니다" in rec
|
||||
css = (B05 / "B05_Profile_UI_Style_RouteRecommend.css").read_text(encoding="utf-8")
|
||||
assert ".b05-routerec__profile .is-initial" in css and "var(--map-route" in css
|
||||
Reference in New Issue
Block a user