Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1

This commit is contained in:
2026-10-18 15:54:44 +09:00
10 changed files with 479 additions and 10 deletions
+5 -3
View File
@@ -32,7 +32,7 @@ from common_util.common_util_drainage_pipes import (
)
from common_util.common_util_route_geometry import RouteVertex
from common_util.common_util_station_key import chainage_key as station_chainage_key
from common_util.common_util_station_key import station_key
from common_util.common_util_station_key import curve_moves, station_key
from common_util.common_util_structure_station import plan_stations, snap_structure_station
from config.config_system import DRAINAGE_PIPE_MIN_SPACING_M
@@ -156,8 +156,10 @@ def carry_cross_inputs(
"""같은 열쇠 측점으로 횡단 입력을 넘김 → (입력{새 누가거리 0.001: 값}, 구조물 조정{…}, 버린 수).
`old_designs` = 옛 노선 행 {누가거리 0.001: design}. 버린 수 = 설계가 있던 옛 측점 중 새 노선에
같은 열쇠가 없는 것(「노선 밖 고침」).
같은 열쇠가 없는 것(「노선 밖 고침」) · MC 가 움직인 곡선 열쇠는 가까운 새 곡선 측점
(`curve_moves`).
"""
moves = curve_moves(list(old_keys.items()), list(new_keys.items()))
inputs: dict[float, dict[str, Any]] = {}
adjusts: dict[float, dict[str, Any]] = {}
dropped = 0
@@ -165,7 +167,7 @@ def carry_cross_inputs(
design = old_designs.get(round(old_at, 3))
if not isinstance(design, dict):
continue
new_at = new_keys.get(key)
new_at = new_keys.get(moves.get(key, key))
if new_at is None:
dropped += 1
continue
+18 -3
View File
@@ -8,9 +8,10 @@
import asyncio
import logging
from pathlib import Path
from typing import Any
from uuid import UUID
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
@@ -23,8 +24,9 @@ from B05_Profile.B05_Profile_Router_Confirm import (
)
from B05_Profile.B05_Profile_Schema import RouteConfirmRequest, RouteConfirmResponse
from B06_Section.B06_Section_Repository import get_longitudinal_section
from common_util.common_util_auth import get_optional_session
from common_util.common_util_storage import resolve_stored_project_path
from common_util.common_util_workflow_state import complete_stage
from common_util.common_util_workflow_state import complete_stage, start_stage
from config.config_db import get_db_pool
logger = logging.getLogger(__name__)
@@ -140,7 +142,10 @@ async def confirm_latest_route(
@router.post("/{project_id}/route/reset")
async def reset_route_design(project_id: UUID) -> JSONResponse:
async def reset_route_design(
project_id: UUID,
session: dict[str, Any] | None = Depends(get_optional_session), # 사본 가져온이
) -> JSONResponse:
"""B05·B06 설계를 초기값으로 되돌린다 ([초기화] 버튼).
**초기값 스냅샷이 있으면 복원한다**(2026-08-29 사용자 확정, CLAUDE.md 5장). 자동설계
@@ -224,6 +229,10 @@ async def reset_route_design(project_id: UUID) -> JSONResponse:
restored_route_id = await restore_initial_snapshot(
connection, project_root, str(project_id)
)
# 재계산 길(체인)과 같은 단계 — stage 2 · 3 검토 대기 · B07 이후 STALE
async with connection.cursor() as cursor:
await start_stage(cursor, str(project_id), 2)
await start_stage(cursor, str(project_id), 3)
await connection.commit()
except Exception:
await connection.rollback()
@@ -258,6 +267,12 @@ async def reset_route_design(project_id: UUID) -> JSONResponse:
},
)
# M02 구조물 사본을 되돌린 구조물 · 관 지정에 맞춤(실패해도 초기화 유지)
if project_root:
from M02_MasterTemplete.M02_Item_Snapshot import importer, safe_sync
await asyncio.to_thread(safe_sync, project_root, importer(session))
# 옛 경로의 코리도 파일은 주인이 사라졌다 — 함께 지운다(2026-08-28 백로그).
try:
if project_root:
+14 -2
View File
@@ -24,6 +24,7 @@ import { pipeIdAt, pipeIdNear } from "../A00_Common/design/design_pipe_ids";
import { showToast } from "@ui/ui_template_elements";
import {
chainageKey,
curveMoves,
dedupeStationKeys,
looseChainage,
stationKey,
@@ -265,6 +266,13 @@ export function chainageOfKey(
return near ? Number(chainageKey(near.chainageM)) : null;
}
/** 두 노선 목록 사이 MC 가 움직인 곡선 열쇠(432-3 · 서버 `carry_cross_inputs` 와 같음). */
function curveMovesBetween(projectId: string, fromRouteId: number, toRouteId: number) {
const pairs = (routeId: number) =>
(lists.get(scopeOf(projectId, routeId)) ?? []).map((e) => [e.key, e.chainageM] as const);
return curveMoves(pairs(fromRouteId), pairs(toRouteId));
}
function readRaw(name: KeyedState, projectId: string, routeId: number): Raw {
const raw = readState<Raw>(name, projectId, routeId);
return raw && typeof raw === "object" ? raw : {};
@@ -338,6 +346,7 @@ export function carryStationDrafts(
{ quiet = false }: { quiet?: boolean } = {},
): number {
const outside = new Set<string>();
const curves = curveMovesBetween(projectId, fromRouteId, toRouteId);
for (const name of KEYED_STATES) {
const raw = readRaw(name, projectId, fromRouteId);
const next: Raw = { [HINT]: {} };
@@ -345,7 +354,8 @@ export function carryStationDrafts(
// 옛 열쇠(누가거리 · at:)는 옛 목록으로 측점 열쇠부터 · 측점 열쇠(id)는 그대로
const fromAt =
looseChainage(key) === null ? null : chainageOfKey(projectId, fromRouteId, key);
const stable = fromAt === null ? key : keyAt(projectId, fromRouteId, fromAt);
const was = fromAt === null ? key : keyAt(projectId, fromRouteId, fromAt);
const stable = curves.get(was) ?? was;
const toAt = chainageOfKey(projectId, toRouteId, stable, looseChainage(stable) ?? undefined);
const carried = carriedValue(name, value);
if (toAt === null || stable.startsWith("at:")) {
@@ -420,10 +430,12 @@ export function carryChainageStates(
): number {
const facilityAt = facilities.keyAt ?? (() => null);
const outside = new Set<string>();
const curves = curveMovesBetween(projectId, fromRouteId, toRouteId);
const moveAt = (state: string, text: string): string | null => {
const moved = facilities.movedAt?.(Number(text));
if (moved !== null && moved !== undefined) return chainageKey(moved);
const stable = keyAt(projectId, fromRouteId, Number(text));
const was = keyAt(projectId, fromRouteId, Number(text));
const stable = curves.get(was) ?? was;
const structural = stable.startsWith("pipe:") || stable.startsWith("structure:");
// 새 목록에 그 열쇠가 없으면 그 시설의 새 자리 · 없으면 같은 자리 측점(초안 측점 = 저장 측점)
const toAt =
+11 -2
View File
@@ -243,7 +243,11 @@ def wipe_edited_masters(project_root: Path) -> list[str]:
"""
removed: list[str] = []
root = Path(project_root)
for rel in ("B05_Profile/route/structures.json", "B04_PreProcess/drainage/edits"):
for rel in (
"B05_Profile/route/structures.json",
"B05_Profile/route/plus_stations.json",
"B04_PreProcess/drainage/edits",
):
path = root / rel
try:
if path.is_dir():
@@ -269,7 +273,12 @@ def restore_snapshot_files(project_root: Path, route_id: int | None = None) -> N
for tree in _FILE_TREES:
stored = source / tree.replace("/", "__")
if not stored.is_dir() and tree == "B04_PreProcess/drainage":
_copy_tree(source / _LEGACY_DRAINAGE_TREE, root / "B04_PreProcess/drainage/edits")
legacy = source / _LEGACY_DRAINAGE_TREE
edits = root / "B04_PreProcess/drainage/edits"
if legacy.is_dir():
_copy_tree(legacy, edits)
else: # 배수 폴더 없는 스냅샷 — 관 편집은 비움(초기값에 없던 것)
shutil.rmtree(edits, ignore_errors=True)
continue
_copy_tree(stored, root / tree)
+28
View File
@@ -5,6 +5,7 @@
곡선 측점(MC · 252-2) = ``curve:<누가거리 0.01 m>``.
구간형 구조물 시작 · 끝 측점 = ``structure:<id>:start`` · ``:end``(기준 측점은 ``structure:<id>``).
한 목록에 같은 열쇠가 둘이면 둘째부터 ``at:<누가거리>``(``dedupe_station_keys``).
MC 가 움직여 곡선 열쇠가 바뀌면 넘길 때 가까운 새 곡선 측점으로 이음(``curve_moves`` · 432-3).
⚠ 파이썬·TS 짝 — ``common_util_station_key.ts`` ``stationKey`` ↔ ``station_key``.
한쪽을 고치면 다른 쪽도 고치고 거울 시험 ``resources/tester/test_344_3_station_key_mirror.py``.
@@ -79,3 +80,30 @@ def loose_chainage(key: str) -> float | None:
"""옛 초안 열쇠(누가거리 글) · ``at:`` 열쇠의 누가거리 — 측점 열쇠면 None."""
text = key[3:] if key.startswith("at:") else key
return float(text) if _LOOSE.match(text) else None
#: 옮긴 곡선 측점으로 보는 거리(m) — 옛 MC 자리에서 이 안의 새 MC.
CURVE_NEAR_M = 10.0
def curve_moves(before: list[tuple[str, float]], after: list[tuple[str, float]]) -> dict[str, str]:
"""MC 가 움직여 바뀐 ``curve:`` 열쇠 → 가까운 새 곡선 열쇠.
옛 목록에만 있는 곡선 열쇠 → 새 목록에만 있는 가장 가까운 곡선 열쇠(``CURVE_NEAR_M`` 안 ·
새 열쇠 하나에 옛 열쇠 하나 · 옛 누가거리 차례).
"""
was = {key for key, _ in before}
kept = {key for key, _ in after}
free = [(key, at) for key, at in after if key.startswith("curve:") and key not in was]
out: dict[str, str] = {}
for key, at in sorted(before, key=lambda item: item[1]):
if not key.startswith("curve:") or key in kept:
continue
best = -1
for index, (_, to) in enumerate(free):
gap = abs(to - at)
if gap <= CURVE_NEAR_M and (best < 0 or gap < abs(free[best][1] - at)):
best = index
if best >= 0:
out[key] = free.pop(best)[0]
return out
+29
View File
@@ -6,6 +6,7 @@
* 곡선 측점(MC · 252-2) = `curve:<누가거리 0.01 m>`.
* 구간형 구조물의 시작 · 끝 측점 = `structure:<id>:start` · `:end`(기준 측점은 `structure:<id>` 그대로).
* 한 목록에 같은 열쇠가 둘이면 둘째부터 `at:<누가거리>`(`dedupeStationKeys`).
* MC 가 움직여 곡선 열쇠가 바뀌면 넘길 때 가까운 새 곡선 측점으로 이음(`curveMoves` · 432-3).
* 쓰는 곳 = 서버 `B05_Profile_Route_Save_Parts` · 묶음 stations 단계 · B06 세션 횡단 초안(`stationKeysOf`).
*
* ⚠ 파이썬·TS 짝 — `common_util_station_key.py` `station_key` ↔ `stationKey`. 한쪽을 고치면 다른 쪽도
@@ -68,3 +69,31 @@ export function looseChainage(key: string): number | null {
if (!/^-?\d+(\.\d+)?$/.test(text)) return null;
return Number(text);
}
/** 옮긴 곡선 측점으로 보는 거리(m) — 옛 MC 자리에서 이 안의 새 MC. */
export const CURVE_NEAR_M = 10;
/**
* MC 가 움직여 `curve:` 열쇠가 바뀜 — 옛 목록에만 있는 곡선 열쇠 → 새 목록에만 있는 가장 가까운 곡선 열쇠
* (`CURVE_NEAR_M` 안 · 새 열쇠 하나에 옛 열쇠 하나 · 옛 누가거리 차례). 목록 = [열쇠, 누가거리].
*/
export function curveMoves(
before: ReadonlyArray<readonly [string, number]>,
after: ReadonlyArray<readonly [string, number]>,
): Map<string, string> {
const isCurve = (key: string) => key.startsWith("curve:");
const was = new Set(before.map(([key]) => key));
const kept = new Set(after.map(([key]) => key));
const free = after.filter(([key]) => isCurve(key) && !was.has(key));
const out = new Map<string, string>();
for (const [key, at] of [...before].sort((a, b) => a[1] - b[1])) {
if (!isCurve(key) || kept.has(key)) continue;
let best = -1;
free.forEach(([, to], i) => {
const gap = Math.abs(to - at);
if (gap <= CURVE_NEAR_M && (best < 0 || gap < Math.abs(free[best][1] - at))) best = i;
});
if (best >= 0) out.set(key, free.splice(best, 1)[0][0]);
}
return out;
}
@@ -0,0 +1,95 @@
/* 432-3 헬퍼 — MC 가 움직여도 B06 고침이 곡선 측점을 따라가는지.
* node helper_432_3_curve_key.mjs <묶음.mjs>
*/
import { pathToFileURL } from "node:url";
const store = () => {
const m = new Map();
return {
getItem: (k) => (m.has(k) ? m.get(k) : null),
setItem: (k, v) => m.set(k, String(v)),
removeItem: (k) => m.delete(k),
key: (i) => [...m.keys()][i] ?? null,
get length() {
return m.size;
},
clear: () => m.clear(),
};
};
globalThis.window = globalThis;
globalThis.sessionStorage = store();
globalThis.localStorage = store();
const any = new Proxy(function () {}, {
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
apply: () => any,
construct: () => any,
});
globalThis.document = any;
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
const m = await import(pathToFileURL(process.argv[2]).href);
const P = "p432";
const detailOf = (rows) => {
const sections = rows.map(([chainage_m, kind]) => ({
station_id: `s${chainage_m}`,
chainage_m,
kind,
label: String(chainage_m),
samples: [],
}));
return {
longitudinal: { stations: sections, profile_alignment: { policy: { station_interval_m: 20 } } },
cross_sections: sections,
};
};
const base = [
[0, "bp"],
[20, "regular"],
[40, "regular"],
[60, "regular"],
[120, "ep"],
];
const route = (curves) => detailOf([...base, ...curves].sort((a, b) => a[0] - b[0]));
// 옛 노선 7 — MC 둘(47.35 · 90.00)에 카드 고침 · 사면 쉬프트(측구 폭 칸 crossw 는 누가거리 열쇠)
m.useStationKeys(
P,
7,
route([
[47.35, "curve"],
[90, "curve"],
]),
);
const card = { ground_type: "rock", section_mode: "right_cut" };
m.writeKeyed(
"crossdesign",
P,
7,
new Map([
["47.35", card],
["90.00", card],
]),
);
m.writeKeyed("shoulderext", P, 7, new Map([["47.35", { left: 0.5 }]]));
m.writeState("crossw", { 47.35: 3.2 }, P, 7);
// 새 노선 8 — 첫 MC 1.75 m 움직임(R 고침) · 둘째 MC 15 m 움직임(10 m 넘음 = 다른 곡선으로 봄)
m.useStationKeys(
P,
8,
route([
[49.1, "curve"],
[105, "curve"],
]),
);
const outside = m.carryStationDrafts(P, 7, 8, { quiet: true });
const states = m.carryChainageStates(P, 7, 8);
process.stdout.write(
JSON.stringify({
outside,
states,
cards: Object.fromEntries(m.readKeyed("crossdesign", P, 8)),
shoulder: Object.fromEntries(m.readKeyed("shoulderext", P, 8)),
crossw: m.readState("crossw", P, 8),
}),
);
+9
View File
@@ -0,0 +1,9 @@
/* 432-3 시험 묶음 진입점 — MC 가 움직인 곡선 측점 고침 넘김(진짜 모듈 · vite ssr). */
export {
carryChainageStates,
carryStationDrafts,
readKeyed,
useStationKeys,
writeKeyed,
} from "../../B06_Section/B06_Section_Station_Keys";
export { readState, writeState } from "../../A00_Common/b_page_state";
+143
View File
@@ -0,0 +1,143 @@
"""431 [초기화] 빈틈(분석 424 3장).
- 스냅샷 없는 폴백 = 덧 측점(`plus_stations.json`)도 지움
- 배수 폴더 없는 스냅샷 = 관 편집 비움
- 복원 길 = stage 2 · 3 검토 대기 · B07 · B08 STALE · M02 구조물 사본 맞춤(진짜 `safe_sync`)
"""
import asyncio
import json
from contextlib import asynccontextmanager
from pathlib import Path
from uuid import uuid4
from common_util.common_util_initial_snapshot import (
restore_snapshot_files,
snapshot_dir,
wipe_edited_masters,
)
EDITS = "B04_PreProcess/drainage/edits"
def _touch(path: Path, text: str = "{}") -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def test_폴백은_덧_측점도_지움(tmp_path: Path):
_touch(tmp_path / "B05_Profile/route/plus_stations.json")
_touch(tmp_path / "B05_Profile/route/structures.json")
_touch(tmp_path / EDITS / "pipe_points.json")
removed = wipe_edited_masters(tmp_path)
assert "B05_Profile/route/plus_stations.json" in removed
assert not (tmp_path / "B05_Profile/route/plus_stations.json").exists()
assert not (tmp_path / EDITS).exists()
def _snapshot_without_drainage(root: Path) -> None:
_touch(snapshot_dir(root) / "B05_Profile__route" / "planned_route.csv", "x,y\n")
_touch(snapshot_dir(root) / "db.json", json.dumps({"routes": [{"id": 1}]}))
def test_배수_폴더_없는_스냅샷은_관_편집_비움(tmp_path: Path):
_snapshot_without_drainage(tmp_path)
_touch(tmp_path / EDITS / "pipe_points.json", '[{"chainage_m": 3}]')
_touch(tmp_path / "B04_PreProcess/drainage/basins.json") # 분석 산출물은 그대로
restore_snapshot_files(tmp_path)
assert not (tmp_path / EDITS).exists()
assert (tmp_path / "B04_PreProcess/drainage/basins.json").is_file()
def test_옛_스냅샷은_edits_만_되돌림(tmp_path: Path):
_snapshot_without_drainage(tmp_path)
_touch(snapshot_dir(tmp_path) / "B04_PreProcess__drainage__edits/pipe_points.json", "[]")
_touch(tmp_path / EDITS / "pipe_points.json", '[{"chainage_m": 3}]')
restore_snapshot_files(tmp_path)
assert (tmp_path / EDITS / "pipe_points.json").read_text(encoding="utf-8") == "[]"
class _Cursor:
def __init__(self, log: list[tuple[str, tuple]]):
self._log = log
self.rowcount = 1
async def __aenter__(self):
return self
async def __aexit__(self, *_):
return False
async def execute(self, sql, params=None):
self._log.append((" ".join(sql.split()), params))
class _Connection:
def __init__(self, log):
self._log = log
def cursor(self, *_a, **_k):
return _Cursor(self._log)
async def begin(self):
pass
async def commit(self):
self._log.append(("COMMIT", ()))
async def rollback(self):
self._log.append(("ROLLBACK", ()))
class _Pool:
def __init__(self, log):
self._log = log
@asynccontextmanager
async def acquire(self):
yield _Connection(self._log)
def _async(value):
async def run(*_a, **_k):
return value
return run
def test_복원_길은_단계_되돌리고_사본_맞춤(tmp_path: Path, monkeypatch):
from B05_Profile import B05_Profile_Router_Corridor as corridor
from B05_Profile import B05_Profile_Router_Lifecycle as lifecycle
from common_util import common_util_initial_snapshot as snapshot
_snapshot_without_drainage(tmp_path)
_touch(tmp_path / EDITS / "pipe_points.json", "[]")
# 사용자가 넣은 구조물의 사본 — 초기값엔 그 구조물 없음
_touch(tmp_path / "templates/structures/s9/manifest.json", '{"key": "SS9"}')
log: list[tuple[str, tuple]] = []
monkeypatch.setattr(lifecycle, "get_db_pool", lambda: _Pool(log))
monkeypatch.setattr(lifecycle, "get_project_storage_relative_path", _async("p"))
monkeypatch.setattr(lifecycle, "resolve_stored_project_path", lambda _p: str(tmp_path))
monkeypatch.setattr(lifecycle, "get_latest_route", _async({"id": 9}))
monkeypatch.setattr(snapshot, "restore_initial_snapshot", _async(9))
monkeypatch.setattr(corridor, "prune_corridor_files", lambda *_a: 0)
pid = uuid4()
response = asyncio.run(lifecycle.reset_route_design(pid, None))
body = json.loads(response.body)
assert body["restored"] is True and body["route_id"] == 9
started = [
p[3]
for sql, p in log
if sql.startswith("UPDATE project_workflow_stages SET state = 'IN_PROGRESS'")
]
assert started == [2, 3]
stale = [p[1] for sql, p in log if "SET state = 'STALE'" in sql]
assert stale == [2, 3] # stage > 3 (B07 · B08 · B09) 완료분 STALE
assert log.index(("COMMIT", ())) > max(
i for i, (sql, _) in enumerate(log) if "project_workflow_stages" in sql
)
assert not (tmp_path / EDITS).exists()
assert not (tmp_path / "templates/structures/s9").exists()
+127
View File
@@ -0,0 +1,127 @@
"""432-3 MC 곡선 측점 열쇠 — MC 가 움직여도 B06 고침이 그 곡선 측점을 따라감.
- 열쇠 `curve:<누가거리>` 는 MC 가 0.01 m 만 움직여도 바뀜 → 예전엔 「노선 밖 고침」으로 버림.
- 이제 옛 목록에만 있는 곡선 열쇠 → 새 목록에만 있는 가장 가까운 곡선 열쇠(10 m 안 · 하나씩).
- 짝: `common_util_station_key` `curve_moves` ↔ `curveMoves`(거울) · 서버 `carry_cross_inputs` ·
B06 세션 `carryStationDrafts` · `carryChainageStates`(진짜 모듈 · vite ssr).
"""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from B05_Profile.B05_Profile_Route_Save_Parts import carry_cross_inputs
from common_util.common_util_station_key import curve_moves
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
TS = ROOT / "common_util" / "common_util_station_key.ts"
TYPESCRIPT = ROOT / "config" / "node_modules" / "typescript"
CASES = [
# MC 하나 1.75 m 움직임 · 하나 그대로 · 하나 15 m(못 이음)
(
[("end:BP", 0.0), ("curve:47.35", 47.35), ("curve:70.00", 70.0), ("curve:90.00", 90.0)],
[("end:BP", 0.0), ("curve:49.10", 49.1), ("curve:70.00", 70.0), ("curve:105.00", 105.0)],
),
# 두 옛 MC 가 한 새 MC 를 다툼 — 앞엣것이 가장 가까운 것만 · 둘째는 다음 가까운 것
(
[("curve:10.00", 10.0), ("curve:14.00", 14.0)],
[("curve:12.00", 12.0), ("curve:20.00", 20.0)],
),
# 곡선 아닌 열쇠는 안 이음 · 같은 거리 = 앞 차례
(
[("grid:1", 20.0), ("curve:30.00", 30.0)],
[("grid:2", 21.0), ("curve:25", 25.0), ("curve:35", 35.0)],
),
([], []),
]
MIRROR = r"""
const ts = require(process.argv[1]);
const src = require("fs").readFileSync(process.argv[2], "utf8");
const opts = { compilerOptions: { module: ts.ModuleKind.CommonJS } };
const js = ts.transpileModule(src, opts).outputText;
const box = { exports: {} };
new Function("exports", "module", js)(box.exports, box);
const cases = JSON.parse(process.argv[3]);
const moves = cases.map(([a, b]) => Object.fromEntries(box.exports.curveMoves(a, b)));
process.stdout.write(JSON.stringify(moves));
"""
def test_곡선_이음_규칙():
assert curve_moves(*CASES[0]) == {"curve:47.35": "curve:49.10"}
assert curve_moves(*CASES[1]) == {"curve:10.00": "curve:12.00", "curve:14.00": "curve:20.00"}
assert curve_moves(*CASES[2]) == {"curve:30.00": "curve:25"}
def test_파이썬과_TS_같음():
proc = subprocess.run(
["node", "-e", MIRROR, str(TYPESCRIPT), str(TS), json.dumps(CASES)],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
assert json.loads(proc.stdout) == [curve_moves(*case) for case in CASES]
def test_서버_노선_저장_넘김은_움직인_MC_로():
old_keys = dict(CASES[0][0])
new_keys = dict(CASES[0][1])
designs = {47.35: {"ground_type": "rock"}, 90.0: {"ground_type": "soil"}}
inputs, _adjusts, dropped = carry_cross_inputs(old_keys, new_keys, designs, ["ground_type"], [])
assert inputs == {49.1: {"ground_type": "rock"}}
assert dropped == 1 # 15 m 움직인 MC = 다른 곡선 · 「노선 밖 고침」
@pytest.fixture(scope="module")
def session(tmp_path_factory):
work = tmp_path_factory.mktemp("b432")
build = subprocess.run(
[
"node",
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
"build",
"--configLoader",
"runner",
"--ssr",
"../resources/tester/helper_432_3_entry.ts",
"--outDir",
str(work),
"--logLevel",
"error",
],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
timeout=300,
)
assert build.returncode == 0, build.stderr
bundle = work / "bundle.mjs"
shutil.copy(work / "helper_432_3_entry.js", bundle)
proc = subprocess.run(
["node", str(HERE / "helper_432_3_curve_key.mjs"), str(bundle)],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_B06_세션_고침이_움직인_MC_를_따라감(session):
assert set(session["cards"]) == {"49.10"}
assert session["cards"]["49.10"]["ground_type"] == "rock"
assert session["shoulder"] == {"49.10": {"left": 0.5}}
assert session["crossw"] == {"49.10": 3.2}
assert session["outside"] == 1 # 90 m MC 카드 — 15 m 움직임 = 버림
assert session["states"] == 0