Merge remote-tracking branches 'origin/sub_laptop_5', 'origin/sub_laptop_6' and 'origin/sub_laptop_7' into sub_laptop_3

This commit is contained in:
2026-09-29 19:53:12 +09:00
15 changed files with 459 additions and 23 deletions
@@ -238,6 +238,23 @@ const mapLines = {
oneDot: model.mapPaths([{ kind: "preview", points: [[5, 5]] }], 460, 340),
};
// ⑦ 되돌리기 · 다시하기가 걸음마다 실은 「현재 추천」을 되살린다(노선과 짝).
const history = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_History.ts"));
const step = (x, rec) => ({
planned: [[x, 0], [x + 1, 1]],
curveOn: [true, true],
curveRadius: [null, null],
curveLock: [null, null],
curveArc: [null, null],
apexLock: [false, false],
picked: -1,
rec,
});
const trail = history.createRouteEditHistory(step(0, { logic: "follow" }));
trail.commit(step(1, { logic: "earthwork" }));
trail.commit(step(2, null));
const historyRecs = [trail.undo(), trail.undo(), trail.redo(), trail.redo()].map((one) => one.rec);
// ④ 캐시 왕복 — 결과 통 프로젝트 칸. 꼴이 틀린 값은 안 읽는다.
model.writeInitialChoice("p-1", plain);
const readBack = model.readInitialChoice("p-1");
@@ -278,5 +295,6 @@ process.stdout.write(
sameAsServer,
initialRoute,
mapLines,
historyRecs,
}),
);
@@ -0,0 +1,106 @@
/* 계획노선 편집 창 **고정 한 벌**(계획서 22-4 · 22-5) 단위검증 헬퍼 —
* `B05_Profile_UI_RouteEdit_Lock.ts` 를 그 자리에서 트랜스파일해 Node 로 돌린다.
*
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_cut_merged_loop.cjs` 머리에 적힌 것과 같다.
* 브라우저 전용인 저장소·팔레트는 가짜로 채운다 — 셈만 본다.
*/
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();
// 브라우저 저장소 흉내 — `b_page_state` 가 읽고 쓰는 자리.
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 local = memoryStorage();
globalThis.window = { localStorage: local, sessionStorage: memoryStorage() };
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.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 lock = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Lock.ts"));
const planned = [
[0, 0],
[100, 0],
[150, 80],
[260, 90],
];
// ① 저장된 것이 없으면 시점 · 종점만 고정.
const fresh = lock.readRouteLocks("p-1");
const freshColumn = lock.lockColumn(planned, fresh);
// ② 시점을 풀고 교각점 둘을 고정해 적는다 → 같은 프로젝트로 다시 읽는다.
const edited = [false, true, true, true];
lock.writeRouteLocks("p-1", planned, edited);
const readBack = lock.readRouteLocks("p-1");
const otherProject = lock.readRouteLocks("p-2");
const storedKeys = [...local._map.keys()];
// ③ 서버가 끝자리만 조금 다른 좌표로 돌려줘도 같은 교각점 · 멀리 옮긴 점은 풀림.
const nudged = [
[0, 0],
[100.03, -0.02],
[151, 80],
[260, 90],
];
const nudgedColumn = lock.lockColumn(nudged, readBack);
// ④ 선 · 호 고정 판정 — 곧은 선은 양 끝, 호는 교각점과 앞뒤.
const straight = [0, 1, 2].map((index) => lock.straightLocked(edited, index));
const arc = [1, 2].map((index) => lock.arcLocked(edited, index));
const arcAll = [1, 2].map((index) => lock.arcLocked([true, true, true, true], index));
// ⑤ 노선을 못 읽은(노드 1개 이하) 채 닫으면 적어 둔 고정을 안 지운다 · 깨진 값은 기본값.
lock.writeRouteLocks("p-1", [], []);
const keptAfterEmpty = lock.readRouteLocks("p-1");
local.setItem("aislo:pref:routeedit-locks:p-3", "{\"start\":false,\"apex\":[[1,\"x\"],[2,3]]}");
const broken = lock.readRouteLocks("p-3");
process.stdout.write(
JSON.stringify({
fresh,
freshColumn,
readBack,
otherProject,
storedKeys,
nudgedColumn,
straight,
arc,
arcAll,
keptAfterEmpty,
broken,
}),
);
@@ -182,3 +182,13 @@ def test_도구_막대는_노선_추천_하나만():
).read()
for name in ("follow", "earthwork", "gentle", "weighted"):
assert f'"{name}"' in recommend
def test_되돌리기_다시하기가_현재_추천을_함께_되살림(results):
"""걸음마다 「현재 추천」이 노선과 짝 — 추천 없음(null)도 그대로 되돌아온다."""
assert results["historyRecs"] == [
{"logic": "earthwork"},
{"logic": "follow"},
{"logic": "earthwork"},
None,
]
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""계획노선 편집 창 — **고정 한 벌**(계획서 22-4 · 22-5) 회귀검증.
시점 · 종점은 기본 고정 · 교각점 고정은 좌표로 다시 짚음 · 곧은 선은 양 끝 · 호는 교각점과
앞뒤 점이 모두 고정일 때 고정. 자리는 화면 상태 보관소 pref 통의 프로젝트 칸.
Node 헬퍼(`helper_b05_routeedit_lock.cjs`)가 TS 를 트랜스파일해 돌리고, 여기서 값을 판정한다.
"""
import json
import os
import subprocess
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture(scope="module")
def results():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_b05_routeedit_lock.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_저장된_것이_없으면_시점_종점만_고정(results):
assert results["fresh"] == {"start": True, "end": True, "apex": []}
assert results["freshColumn"] == [True, False, False, True]
def test_저장소_왕복은_프로젝트마다(results):
assert results["readBack"] == {"start": False, "end": True, "apex": [[100, 0], [150, 80]]}
assert results["otherProject"] == {"start": True, "end": True, "apex": []}
assert results["storedKeys"] == ["aislo:pref:routeedit-locks:p-1"]
def test_교각점은_좌표로_다시_짚음(results):
# 0.05m 안은 같은 점 · 1m 옮긴 점은 풀림 · 시점 · 종점은 자리와 무관하게 역할로.
assert results["nudgedColumn"] == [False, True, False, True]
def test_선과_호_고정_판정(results):
assert results["straight"] == [False, True, True]
assert results["arc"] == [False, True]
assert results["arcAll"] == [True, True]
def test_빈_노선은_안_지우고_깨진_값은_기본값(results):
assert results["keptAfterEmpty"] == results["readBack"]
assert results["broken"] == {"start": False, "end": True, "apex": [[2, 3]]}