Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
);
|
||||
@@ -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";
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user