fix(B05·B06): 노선 고친 [저장] 계획고 = 서버 옮김 위에 사용자 손댐만 (PLAN 498-2 · 요구 411~417)
- 브라우저 — 노선 바뀐 [저장]의 계획고 본문 = 묶음 계획고 대 저장분 옮김(같은 carryEdits · 이 초안 첫 기준)의 다른 열쇠 · 지운 열쇠만(`gradeTouch` · `routeGradeTouch`) · 못 가르면 옛 길(노선 0 칸 통째) - 서버 — `alignment_cleared` 가 오면 관 · 구조물 · 추가 측점 차례 뒤 새 종단 편집(서버 옮김) 위에 손댐만 얹어 한 번 저장(`touched_alignment` · 얹기 실패도 되돌림 안) - follow 옮김 측점 = stations 단계와 같은 추가 측점(`plus` 넘김) - 재현: 꼬리 추가 측점 ▲ 뒤 앞 교점 끌기 [저장] = 990 m 편집 버림 · ▲ 잃음 · 최대 −0.50 m → 고친 뒤 버림 0 · 끌 때 = 저장 뒤 - 시험: test_498_2_grade_touch 새로 · helper_design_store_save 에 묶음 창구 가짜 하나(옛 길) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
/* 498-2 노선 바뀐 [저장] 계획고 손댐 — 진짜 TS(`logic/B05_Profile_Pipeline_Grade` follow · `gradeTouch`)를
|
||||
* Node 로 돌림. 표준출력 JSON 한 벌.
|
||||
*/
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
function loadTs(file, stubs = {}) {
|
||||
return makeLoader({
|
||||
root: ROOT,
|
||||
cache: false,
|
||||
requireFor: (full) => (request) => {
|
||||
if (stubs[request]) return stubs[request];
|
||||
throw new Error(`stub 없음: ${request} (${file})`);
|
||||
},
|
||||
}).loadTs(path.join(ROOT, file));
|
||||
}
|
||||
|
||||
const straightJudge = loadTs("B05_Profile/logic/B05_Profile_Profile_Straight.ts");
|
||||
const curveLaw = loadTs("B05_Profile/logic/B05_Profile_Profile_CurveLaw.ts");
|
||||
const pivot = loadTs("B05_Profile/logic/B05_Profile_UI_Profile_Pivot.ts", {
|
||||
"./B05_Profile_Profile_Straight": straightJudge,
|
||||
});
|
||||
const alignment = loadTs("B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts", {
|
||||
"./B05_Profile_UI_Profile_Pivot": pivot,
|
||||
"./B05_Profile_Profile_Straight": straightJudge,
|
||||
"./B05_Profile_Profile_CurveLaw": curveLaw,
|
||||
});
|
||||
const carry = loadTs("B05_Profile/logic/B05_Profile_Grade_Carry.ts", {
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
});
|
||||
const grade = loadTs("B05_Profile/logic/B05_Profile_Pipeline_Grade.ts", {
|
||||
"./B05_Profile_Profile_CurveLaw": curveLaw,
|
||||
"../../common_util/common_util_route_follow": {},
|
||||
"../../common_util/common_util_structure_station": {
|
||||
PLUS_OWNER_PREFIX: "plus-",
|
||||
SAME_STATION_M: 0.005,
|
||||
},
|
||||
"./B05_Profile_Grade_Seat": loadTs("B05_Profile/logic/B05_Profile_Grade_Seat.ts", {
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
}),
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
"./B05_Profile_Grade_Carry": carry,
|
||||
"./B05_Profile_Sections_Stations": { SECTION_STATION_INTERVAL_M: 20 },
|
||||
"./B05_Profile_Grade_Ground": loadTs("B05_Profile/logic/B05_Profile_Grade_Ground.ts", {
|
||||
"./B05_Profile_Profile_CurveLaw": curveLaw,
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
}),
|
||||
});
|
||||
|
||||
const routeOf = (line) => {
|
||||
const chainage = [0];
|
||||
for (let i = 1; i < line.length; i += 1)
|
||||
chainage.push(
|
||||
chainage[i - 1] + Math.hypot(line[i][0] - line[i - 1][0], line[i][1] - line[i - 1][1]),
|
||||
);
|
||||
return { line, curves: [], chainage, lengthM: chainage[chainage.length - 1] };
|
||||
};
|
||||
/** 10 m 격자 + 끝 — 묶음 stations 단계 자리. */
|
||||
const gridOf = (route) => {
|
||||
const out = [];
|
||||
for (let s = 0; s < route.lengthM - 1e-6; s += 10) out.push(Math.round(s * 1000) / 1000);
|
||||
return [...out, Math.round(route.lengthM * 1000) / 1000];
|
||||
};
|
||||
|
||||
const SAVED_LINE = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 100],
|
||||
[100, 200],
|
||||
];
|
||||
const STEPS = [
|
||||
[
|
||||
[0, 0],
|
||||
[95, 0],
|
||||
[100, 100],
|
||||
[100, 200],
|
||||
],
|
||||
[
|
||||
[0, 0],
|
||||
[90, 0],
|
||||
[100, 100],
|
||||
[100, 200],
|
||||
],
|
||||
];
|
||||
const BASE = [0, 100, 200, 300].map((chainage_m) => ({ chainage_m, elevation_m: 500 }));
|
||||
const SAVED = { station_offsets: { "250.000": 0.5 }, curve_radii: {} };
|
||||
|
||||
/** 저장 노선에서 노선 걸음 둘 — 걸음 사이 `between` 이 사용자 계획고 조작(묶음 입력 고침). */
|
||||
function run(between, after) {
|
||||
const stage = grade.createFollowStage({
|
||||
savedLine: () => null,
|
||||
stationsAt: (route) => gridOf(route),
|
||||
});
|
||||
const planline = { policy: { station_interval_m: 20 }, basePvi: BASE };
|
||||
let now = SAVED;
|
||||
let before = routeOf(SAVED_LINE);
|
||||
let route = before;
|
||||
STEPS.forEach((line, index) => {
|
||||
route = routeOf(line);
|
||||
const patch = stage({
|
||||
inputs: { route: { nodes: [] }, pipes: null, structures: null, alignment: now },
|
||||
out: { route },
|
||||
before: { route: before, planline },
|
||||
});
|
||||
if (patch.alignment) now = patch.alignment;
|
||||
if (index === 0 && between) now = between(now);
|
||||
before = route;
|
||||
});
|
||||
if (after) now = after(now);
|
||||
const reader = {
|
||||
input: (key) => ({ route: { nodes: [] }, alignment: now })[key] ?? null,
|
||||
output: (key) => (key === "route" ? route : null),
|
||||
};
|
||||
return { now, touch: stage.touch(SAVED, reader) };
|
||||
}
|
||||
|
||||
const bump = (key, by) => (edits) => ({
|
||||
...edits,
|
||||
station_offsets: { ...edits.station_offsets, [key]: (edits.station_offsets[key] ?? 0) + by },
|
||||
});
|
||||
const drop = (edits) => {
|
||||
const offsets = { ...edits.station_offsets };
|
||||
delete offsets[Object.keys(offsets).sort()[0]];
|
||||
return { ...edits, station_offsets: offsets };
|
||||
};
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
plain: run(null, null),
|
||||
lateBump: run(null, bump("60.000", 0.3)),
|
||||
midBump: run(bump("40.000", 0.2), null),
|
||||
removed: run(null, drop),
|
||||
noFirst: grade.createFollowStage({ savedLine: () => null }).touch(SAVED, {
|
||||
input: () => null,
|
||||
output: () => null,
|
||||
}),
|
||||
pure: grade.gradeTouch(
|
||||
{ station_offsets: { "10.000": 1, "20.000": 2.5 }, curve_radii: { "30.000": 80 } },
|
||||
{ station_offsets: { "10.000": 1, "20.000": 2, "40.000": 3 }, curve_radii: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -119,6 +119,8 @@ const STUBS = {
|
||||
// 472-6 한 본문 창구 — 보낸 칸만 적음 · 구조물 실패 = 서버가 그 차례에서 되돌림
|
||||
"./design_save_api": saveApiStub,
|
||||
"./design_leave": { designSignature: () => "", markDesignClean: () => undefined },
|
||||
// 498-2 노선 바뀐 [저장] 계획고 손댐 — 묶음 없음 = 노선 0 칸 통째(옛 길)
|
||||
"./design_pipeline_page": { routeGradeTouch: async () => null },
|
||||
};
|
||||
|
||||
const { loadTs } = makeLoader({
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""498-2 노선 바뀐 [저장] 계획고 — 브라우저는 손댐(입력)만 · 서버가 자기 옮김 위에 한 번.
|
||||
|
||||
- 브라우저(`B05_Profile_Pipeline_Grade` follow `touch` · `gradeTouch`) — 노선만 끌었으면 손댐 0 ·
|
||||
끈 뒤 ▲ = 그 열쇠만 · 끄는 중 ▲ = 그 열쇠 포함 묶음 값 그대로 · 지운 열쇠 = `cleared` ·
|
||||
첫 기준 없으면 null(옛 길 · 0 칸 통째).
|
||||
- 서버(`B06_Section_Router_DesignSave_Route.overlay_edits` · `touched_alignment`) — 손대지 않은 옮김
|
||||
열쇠는 서버 측점 그대로 · 고친 · 지운 열쇠 자리(0.006 m 안)만 걷고 손댐 값을 얹음 · 브라우저 측점
|
||||
열쇠로 서버 열쇠를 덮지 않음(옛 흠 = 두 쪽 측점이 다르면 어긋난 편집을 지움).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B06_Section import B06_Section_Router_DesignSave_Route as moving
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts() -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_498_2_grade_touch.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _empty(touch: dict) -> bool:
|
||||
return not any(touch["edits"].values()) and not any(touch["cleared"].values())
|
||||
|
||||
|
||||
def test_노선만_끌면_손댐_0(ts):
|
||||
assert ts["plain"]["now"]["station_offsets"], "옮긴 편집이 있어야 시험이 뜻을 가짐"
|
||||
assert _empty(ts["plain"]["touch"])
|
||||
|
||||
|
||||
def test_끈_뒤_올림은_그_열쇠만(ts):
|
||||
touch = ts["lateBump"]["touch"]
|
||||
assert touch["edits"]["station_offsets"] == {"60.000": 0.3}
|
||||
assert not any(touch["cleared"].values())
|
||||
|
||||
|
||||
def test_끄는_중_올림은_묶음_값_그대로(ts):
|
||||
now, touch = ts["midBump"]["now"], ts["midBump"]["touch"]
|
||||
assert touch["edits"]["station_offsets"]["40.000"] == pytest.approx(0.2)
|
||||
for key, value in touch["edits"]["station_offsets"].items():
|
||||
assert now["station_offsets"][key] == value
|
||||
|
||||
|
||||
def test_지운_열쇠는_cleared(ts):
|
||||
touch = ts["removed"]["touch"]
|
||||
assert touch["cleared"]["station_offsets"] == ["200.000"]
|
||||
assert not any(touch["edits"].values())
|
||||
|
||||
|
||||
def test_첫_기준_없으면_null_옛_길(ts):
|
||||
assert ts["noFirst"] is None
|
||||
|
||||
|
||||
def test_gradeTouch_순수(ts):
|
||||
assert ts["pure"] == {
|
||||
"edits": {"station_offsets": {"20.000": 2.5}, "curve_radii": {"30.000": 80}},
|
||||
"cleared": {"station_offsets": ["40.000"], "curve_radii": []},
|
||||
}
|
||||
|
||||
|
||||
def test_서버_옮김_위에_손댐만():
|
||||
carried = {
|
||||
"station_offsets": {"960.000": 0.005, "970.000": 0.64, "980.000": 0.014},
|
||||
"curve_radii": {"500.000": 120.0},
|
||||
}
|
||||
touch = {"station_offsets": {"970.003": 0.7, "1000.000": 0.2}, "curve_radii": {}}
|
||||
cleared = moving.AlignmentCleared(station_offsets=["980.000"], curve_radii=["500.004"])
|
||||
assert moving.overlay_edits(carried, touch, cleared) == {
|
||||
# 960 = 손 안 댐(서버 값) · 970 = 0.006 m 안 손댐이 갈음 · 980 지움 · 1000 새 손댐
|
||||
"station_offsets": {"960.000": 0.005, "970.003": 0.7, "1000.000": 0.2},
|
||||
"curve_radii": {},
|
||||
}
|
||||
|
||||
|
||||
def test_손댐_0이면_서버_옮김_그대로():
|
||||
carried = {"station_offsets": {"960.000": 0.005, "970.000": 0.64}, "curve_radii": {}}
|
||||
out = moving.overlay_edits(carried, {}, moving.AlignmentCleared())
|
||||
assert out == {"station_offsets": carried["station_offsets"], "curve_radii": {}}
|
||||
|
||||
|
||||
def test_계획고_본문은_새_종단_편집에서(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
from B03_FileInput import B03_FileInput_Repository as files
|
||||
from B05_Profile.B05_Profile_Schema import ProfileAlignmentSaveRequest
|
||||
from B06_Section import B06_Section_Repository as repo
|
||||
from common_util import common_util_storage as storage
|
||||
from config import config_db
|
||||
|
||||
(tmp_path / "long.json").write_text(
|
||||
json.dumps({"profile_alignment": {"edits": {"station_offsets": {"970.000": 0.64}}}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
class Pool:
|
||||
def acquire(self):
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
return None
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
return False
|
||||
|
||||
async def longitudinal(_connection, _project, route_id):
|
||||
assert route_id == 820, "새 노선 번호로 읽어야 함"
|
||||
return {"longitudinal_file_path": "long.json"}
|
||||
|
||||
async def stored(_connection, _project):
|
||||
return "p"
|
||||
|
||||
monkeypatch.setattr(config_db, "get_db_pool", lambda: Pool())
|
||||
monkeypatch.setattr(repo, "get_longitudinal_section", longitudinal)
|
||||
monkeypatch.setattr(files, "get_project_storage_relative_path", stored)
|
||||
monkeypatch.setattr(storage, "resolve_stored_project_path", lambda _p: str(tmp_path))
|
||||
touch = ProfileAlignmentSaveRequest(route_id=819, station_offsets={"1000.000": 0.2})
|
||||
body = asyncio.run(moving.touched_alignment("pid", 820, touch, moving.AlignmentCleared()))
|
||||
assert body.route_id == 820
|
||||
assert body.station_offsets == {"970.000": 0.64, "1000.000": 0.2}
|
||||
Reference in New Issue
Block a user