Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -126,17 +126,15 @@ export function createProfileSections(deps: ProfileSectionsDeps) {
|
||||
try {
|
||||
renderSections(detail, routeId);
|
||||
// 구 확정분에 남은 비정규 측점(자유 텍스트)을 구조물 정본으로 1회 이관한다 — 멱등이지만
|
||||
// 탭 수명 동안 노선마다 한 번만 시도한다(2026-08-17 · 09-06 실측).
|
||||
const legacy = detail.longitudinal.stations
|
||||
.filter((station) => station.kind === "irregular")
|
||||
.map((station) => ({
|
||||
chainage_m: station.chainage_m,
|
||||
structure: station.structure ?? "",
|
||||
}));
|
||||
// 탭 수명 동안 노선마다 한 번만 시도한다(2026-08-17 · 09-06 실측). 측점은 서버가 저장
|
||||
// 종단 파일에서 읽음 — 이 상세는 초안이 섞일 수 있어 보내지 않음(452).
|
||||
const hasLegacy = detail.longitudinal.stations.some(
|
||||
(station) => station.kind === "irregular",
|
||||
);
|
||||
const migrateKey = `${projectId}:${routeId}`;
|
||||
if (legacy.length && !migratedRoutes.has(migrateKey)) {
|
||||
if (hasLegacy && !migratedRoutes.has(migrateKey)) {
|
||||
migratedRoutes.add(migrateKey);
|
||||
void migrateLegacyStations(projectId, legacy)
|
||||
void migrateLegacyStations(projectId, routeId)
|
||||
.then((result) => {
|
||||
if (result.migrated > 0) {
|
||||
showToast(`구 구조물 측점 ${result.migrated}건을 구조물 목록으로 옮겼습니다.`);
|
||||
|
||||
@@ -313,14 +313,15 @@ export async function saveStructures(
|
||||
}
|
||||
|
||||
/** 구 비정규 측점(자유 텍스트)을 구조물 정본으로 이관한다. 멱등 — 배관은 관 지점
|
||||
* 정본 소관이라 서버가 걸러내고, 이미 정본에 있는 (타입, 위치)는 건너뛴다. */
|
||||
* 정본 소관이라 서버가 걸러내고, 이미 정본에 있는 (타입, 위치)는 건너뛴다.
|
||||
* 측점은 서버가 저장 종단 파일에서 읽음 — 화면 상세(초안 섞임)를 안 보냄(452). */
|
||||
export async function migrateLegacyStations(
|
||||
projectId: string,
|
||||
stations: Array<{ chainage_m: number; structure: string }>,
|
||||
routeId: number,
|
||||
): Promise<{ status: string; migrated: number; revision: number }> {
|
||||
return requestJson(`/projects/${projectId}/route/structures/migrate`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ stations }),
|
||||
body: JSON.stringify({ route_id: routeId }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -93,19 +93,25 @@ def add_crossing_pipes(
|
||||
|
||||
def _facility_owners(
|
||||
pipes: Sequence[PipePoint], structures: Sequence[Any]
|
||||
) -> dict[float, tuple[str, str | None]]:
|
||||
"""측점 자리(0.01 m) → (관 · 구조물, id). 같은 자리면 관이 먼저(`plan_stations` 이름 규칙)."""
|
||||
owners: dict[float, tuple[str, str | None]] = {}
|
||||
) -> dict[float, tuple[str, str | None, str]]:
|
||||
"""측점 자리(0.01 m) → (관 · 구조물, id, 역할 anchor · start · end). 같은 자리면 관이 먼저
|
||||
(`plan_stations` 이름 규칙)."""
|
||||
owners: dict[float, tuple[str, str | None, str]] = {}
|
||||
for pipe in pipes:
|
||||
for mark in plan_stations([pipe], []):
|
||||
owners.setdefault(
|
||||
round(float(mark["chainage_m"]), 2), ("pipe", getattr(pipe, "id", None))
|
||||
round(float(mark["chainage_m"]), 2),
|
||||
("pipe", getattr(pipe, "id", None), str(mark.get("role") or "anchor")),
|
||||
)
|
||||
for item in structures:
|
||||
for mark in plan_stations([], [item]):
|
||||
owners.setdefault(
|
||||
round(float(mark["chainage_m"]), 2),
|
||||
("structure", getattr(item, "structure_id", None)),
|
||||
(
|
||||
"structure",
|
||||
getattr(item, "structure_id", None),
|
||||
str(mark.get("role") or "anchor"),
|
||||
),
|
||||
)
|
||||
return owners
|
||||
|
||||
@@ -118,8 +124,9 @@ def station_keys(
|
||||
) -> dict[str, float]:
|
||||
"""측점 열쇠(`common_util_station_key.station_key` 한 벌) → 누가거리.
|
||||
|
||||
관 · 구조물 자리 > 곡선 측점(`curve:` · 252-2) > 시종점 > 일반 격자(간격 배수인 것만) ·
|
||||
id 없는 관 · 구조물은 `at:` ·
|
||||
시종점(맨 앞 · 맨 끝 = 관 · 구조물이 얹혀도 `end:BP/EP` · 화면 `buildStations` · B06
|
||||
`stationKeysOf` 와 같음 · 452) > 관 · 구조물 자리(구간형 시작 · 끝 `:start` · `:end`) >
|
||||
곡선 측점(`curve:` · 252-2) > 일반 격자(간격 배수인 것만) · id 없는 관 · 구조물은 `at:` ·
|
||||
같은 열쇠가 둘이면 뒤엣것은 `at:`(B06 `stationKeysOf` 와 같음).
|
||||
"""
|
||||
owners = _facility_owners(pipes, structures)
|
||||
@@ -128,14 +135,14 @@ def station_keys(
|
||||
out: dict[str, float] = {}
|
||||
for index, chainage in enumerate(values):
|
||||
owner = owners.get(round(chainage, 2))
|
||||
if owner is not None:
|
||||
key = station_key(owner[0], chainage, interval_m, owner[1])
|
||||
elif rows[index].get("kind") == "curve":
|
||||
key = station_key("curve", chainage, interval_m)
|
||||
elif index == 0:
|
||||
if index == 0:
|
||||
key = station_key("bp", chainage, interval_m)
|
||||
elif index == len(values) - 1:
|
||||
key = station_key("ep", chainage, interval_m)
|
||||
elif owner is not None:
|
||||
key = station_key(owner[0], chainage, interval_m, owner[1], role=owner[2])
|
||||
elif rows[index].get("kind") == "curve":
|
||||
key = station_key("curve", chainage, interval_m)
|
||||
elif interval_m > 0 and abs(chainage - round(chainage / interval_m) * interval_m) < _SAME_M:
|
||||
key = station_key("regular", chainage, interval_m)
|
||||
else:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -16,7 +17,7 @@ from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Repository import get_latest_route
|
||||
@@ -239,9 +240,43 @@ async def read_structures(project_id: UUID) -> StructureListResponse | JSONRespo
|
||||
|
||||
|
||||
class StructureMigrateRequest(BaseModel):
|
||||
"""구 비정규 측점 이관 요청 — 화면이 복원한 `{chainage_m, structure}` 목록 그대로."""
|
||||
"""구 비정규 측점 이관 요청 — 노선 id 만(없으면 최신 노선).
|
||||
|
||||
stations: list[dict[str, Any]] = Field(default_factory=list)
|
||||
측점은 서버가 저장 종단 파일에서 읽음(452)."""
|
||||
|
||||
route_id: int | None = None
|
||||
|
||||
|
||||
async def _saved_irregular_stations(
|
||||
project_id: UUID, root: str, route_id: int | None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""저장 종단 정본의 비정규 측점 `{chainage_m, structure}` — 이관 원천. 화면 상세는 초안
|
||||
(옮긴 관 자리)이 섞일 수 있어 안 받음(452 · [저장] 없이 초안을 정본에 쓰지 않음)."""
|
||||
from B06_Section.B06_Section_Repository import get_longitudinal_section
|
||||
from B06_Section.B06_Section_Router_Design import resolve_longitudinal_path
|
||||
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
if route_id is None:
|
||||
latest = await get_latest_route(connection, project_id)
|
||||
route_id = latest["id"] if latest else None
|
||||
row = (
|
||||
await get_longitudinal_section(connection, project_id, route_id)
|
||||
if route_id is not None
|
||||
else None
|
||||
)
|
||||
if not row or not row.get("longitudinal_file_path"):
|
||||
return []
|
||||
try:
|
||||
path = resolve_longitudinal_path(Path(root), str(row["longitudinal_file_path"]))
|
||||
stations = json.loads(path.read_text(encoding="utf-8")).get("stations") or []
|
||||
except (OSError, ValueError, AttributeError):
|
||||
return []
|
||||
return [
|
||||
{"chainage_m": station["chainage_m"], "structure": station.get("structure") or ""}
|
||||
for station in stations
|
||||
if station.get("kind") == "irregular" and station.get("chainage_m") is not None
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{project_id}/route/structures/migrate", response_model=None)
|
||||
@@ -267,7 +302,8 @@ async def migrate_structures(project_id: UUID, payload: StructureMigrateRequest)
|
||||
plus_chainages(root),
|
||||
)
|
||||
]
|
||||
plan = migrate_irregular_stations(payload.stations, planted)
|
||||
legacy = await _saved_irregular_stations(project_id, root, payload.route_id)
|
||||
plan = migrate_irregular_stations(legacy, planted)
|
||||
# 표식 = "타입@위치". 원천(종단 정본의 비정규 측점)은 원복용으로 그대로 두고,
|
||||
# 옮긴 이력만 남긴다 — 그래야 사용자가 지운 구조물이 재진입 때 되살아나지 않는다
|
||||
# (2026-08-24 사용자: 초기 계산값은 원복용, 사용자 수정 1세트가 최종본).
|
||||
|
||||
@@ -247,6 +247,7 @@ async def _regenerate_structure_stations(
|
||||
_section_options_from_stored,
|
||||
drop_rows_off_stations,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Confirm import _prune_cross_files
|
||||
|
||||
stored_options, crs_epsg, longitudinal = await _generation_inputs(project_id, route, snapshot)
|
||||
|
||||
@@ -269,6 +270,8 @@ async def _regenerate_structure_stations(
|
||||
summary = _merge_irregular_into_longitudinal(
|
||||
project_root, str(longitudinal["longitudinal_file_path"]), stations
|
||||
)
|
||||
# 측점을 고친 그 자리에서 옛 자리 cross_*.json 도 지움(관 PUT · 452)
|
||||
_prune_cross_files(project_root, str(longitudinal["longitudinal_file_path"]))
|
||||
return len(stations), summary
|
||||
|
||||
made, summary = await asyncio.to_thread(_regenerate)
|
||||
|
||||
@@ -148,7 +148,8 @@ export function useStructureIds(
|
||||
const looksLikePipe = (section: CrossSection): boolean =>
|
||||
Boolean(section.culvert || section.box) || /^.+ D\d+/.test(section.structure ?? "");
|
||||
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). */
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). 맨 앞 · 맨 끝 = 관 · 구조물이
|
||||
* 얹혀도(서버 병합이 종류를 irregular 로 갈음해도) `end:BP/EP` — 묶음 `buildStations` · 서버 `station_keys` 와 한 규칙(452). */
|
||||
export function stationKeysOf(
|
||||
projectId: string | null,
|
||||
sections: ReadonlyArray<CrossSection>,
|
||||
@@ -156,10 +157,12 @@ export function stationKeysOf(
|
||||
): Keyed[] {
|
||||
const known = projectId ? (owners.get(projectId) ?? []) : [];
|
||||
const planned = known.some((owner) => owner.kind === "pipe");
|
||||
const keys = sections.map((section) => {
|
||||
const keys = sections.map((section, index) => {
|
||||
const at = section.chainage_m;
|
||||
let key: string;
|
||||
if (section.kind === "irregular") {
|
||||
if (index === 0) key = stationKey("bp", at, intervalM);
|
||||
else if (index === sections.length - 1) key = stationKey("ep", at, intervalM);
|
||||
else if (section.kind === "irregular") {
|
||||
const owner = known.find((entry) => Math.abs(entry.at - at) < NEAR_M);
|
||||
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind, owner.role);
|
||||
else if (looksLikePipe(section)) {
|
||||
|
||||
@@ -146,7 +146,15 @@ m.followEditorPipes(fresh2, m.createFacilityStore(), [
|
||||
]);
|
||||
out.fresh = [fresh.pipes(), fresh2.pipes()].map((list) => list.map((p) => [p.id, p.chainage_m]));
|
||||
|
||||
/* 6. B06 측점 열쇠 — 서버 관 id 도 관(계산 묶음 `pipe:` 와 같음) */
|
||||
/* 6. B06 측점 열쇠 — 서버 관 id 도 관(계산 묶음 `pipe:` 와 같음) · 맨 앞 · 맨 끝 = 시종점(452)이라 BP · EP 사이에 둠 */
|
||||
const keysOf = (project, list) =>
|
||||
m
|
||||
.stationKeysOf(
|
||||
project,
|
||||
[{ chainage_m: 0, kind: "bp" }, ...list, { chainage_m: 400, kind: "ep" }],
|
||||
20,
|
||||
)
|
||||
.slice(1, -1);
|
||||
const planned = [
|
||||
{ chainage_m: 80.17, owners: ["p8017"], role: "anchor" },
|
||||
{ chainage_m: 52.5, owners: ["st-1"], role: "anchor" },
|
||||
@@ -156,9 +164,9 @@ const sections = [
|
||||
{ chainage_m: 80.17, kind: "irregular" },
|
||||
];
|
||||
m.useStationOwners(P, planned, [{ id: "p8017", chainage_m: 80.17 }]);
|
||||
out.keys = m.stationKeysOf(P, sections, 20).map((k) => k.key);
|
||||
out.keys = keysOf(P, sections).map((k) => k.key);
|
||||
m.useStationOwners(P, planned);
|
||||
out.keysNoPipes = m.stationKeysOf(P, sections, 20).map((k) => k.key);
|
||||
out.keysNoPipes = keysOf(P, sections).map((k) => k.key);
|
||||
out.pipelineKey = m.pipelineStationKey("pipe", "p8017");
|
||||
|
||||
/* 7. 411-E — 관 계획을 아는 동안 옛 자리 관 측점(옮겨 간 자리)은 세션 id 를 안 지음 · 헛 id 정리 */
|
||||
@@ -171,10 +179,10 @@ const staleSections = [
|
||||
{ chainage_m: 80, kind: "irregular", structure: "파형강관 D800", culvert: {} },
|
||||
{ chainage_m: 150, kind: "irregular", structure: "파형강관 D800", culvert: {} }, // 옛 자리
|
||||
];
|
||||
out.staleKeys = m.stationKeysOf(E, staleSections, 20).map((k) => k.key);
|
||||
out.staleKeys = keysOf(E, staleSections).map((k) => k.key);
|
||||
out.staleBook = bookSize();
|
||||
m.useStationOwners(E, []); // 관 계획 모름(초안 없음) = 옛 갈래 그대로 지음
|
||||
out.legacyKey = m.stationKeysOf(E, [staleSections[1]], 20)[0].key;
|
||||
out.legacyKey = keysOf(E, [staleSections[1]])[0].key;
|
||||
out.legacyBook = bookSize();
|
||||
m.prunePipeIds([80], E); // 지금 관 자리만 남김
|
||||
out.pruned = { size: bookSize(), at80: m.pipeIdAt(80, E), e1 };
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/* 452 시험 묶음 진입점 — B06 측점 열쇠(시종점 규칙)를 vite ssr 로 묶어 Node 헬퍼가 부름. */
|
||||
export { stationKeysOf, useStructureIds } from "../../B06_Section/B06_Section_Station_Keys";
|
||||
@@ -0,0 +1,36 @@
|
||||
/* 452 헬퍼 — B06 `stationKeysOf` 열쇠를 JSON 으로.
|
||||
* node helper_452_keys.mjs <묶음.mjs> <입력.json>
|
||||
* 입력 = { sections: [{chainage_m, kind}], structures: [{structure_id, chainage_m, start_m, end_m}], interval }.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
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;
|
||||
|
||||
const [bundlePath, inputPath] = process.argv.slice(2);
|
||||
const m = await import(pathToFileURL(bundlePath).href);
|
||||
const input = JSON.parse(readFileSync(inputPath, "utf8"));
|
||||
m.useStructureIds("p452", input.structures);
|
||||
const sections = input.sections.map((s) => ({ station_id: `s${s.chainage_m}`, samples: [], ...s }));
|
||||
process.stdout.write(JSON.stringify(m.stationKeysOf("p452", sections, input.interval)));
|
||||
@@ -123,9 +123,9 @@ def test_측점_열쇠_규칙():
|
||||
"at:33.30": 33.3, # id 없는 관
|
||||
"grid:2": 40.0,
|
||||
"pipe:p-7": 50.0,
|
||||
"structure:w1": 60.0, # 구간 시작 측점이 먼저 열쇠를 가짐
|
||||
"at:77.00": 77.0, # 같은 구조물 열쇠 둘째 — at:
|
||||
"at:80.00": 80.0,
|
||||
"structure:w1:start": 60.0, # 구간형 시작 · 끝 = :start · :end(452 · 화면과 한 규칙)
|
||||
"structure:w1": 77.0,
|
||||
"structure:w1:end": 80.0,
|
||||
"end:EP": 95.5,
|
||||
}
|
||||
assert "pipe:0" not in keys and "pipe:1" not in keys # 파일 차례 열쇠 없음
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""452 서버 — 구 구조물 이관 · 옛 횡단 파일 · 시종점 열쇠 (433-17① · 433-10 · 433-18).
|
||||
|
||||
못박는 것
|
||||
- 이관 원천 = 저장 종단 파일의 비정규 측점 — 화면 본문(초안 관 자리 섞임)은 안 믿음 ·
|
||||
저장 관 자리면 structures.json · pipe_points.json 그대로.
|
||||
- 관 PUT 끝 측점 다시 뜸(`_regenerate_structure_stations`) = 옛 자리 cross_*.json 도 지움.
|
||||
- 시종점 열쇠 한 규칙 — 맨 앞 · 맨 끝은 구조물이 얹혀도(병합이 종류를 irregular 로 갈음해도)
|
||||
`end:BP/EP` · 구간형 시작 · 끝 = `:start` · `:end` · 서버 `station_keys` = B06 `stationKeysOf`.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import B05_Profile.B05_Profile_Structures_Router as router_module # noqa: E402
|
||||
import B06_Section.B06_Section_Repository as b06_repo # noqa: E402
|
||||
from B05_Profile.B05_Profile_Engine_Sections import cross_filename # noqa: E402
|
||||
from B05_Profile.B05_Profile_Route_Save_Parts import station_keys # noqa: E402
|
||||
from common_util.common_util_drainage_pipes import pipe_points_path_in # noqa: E402
|
||||
|
||||
PROJECT_ID = "45245245-2452-4524-5245-245245245245"
|
||||
LONG_REL = "B06_Section/longitudinal/longitudinal.json"
|
||||
|
||||
|
||||
def _write_longitudinal(project: Path, stations):
|
||||
path = project / LONG_REL
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps({"stations": stations}), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
# ── 433-17① 이관 = 저장 종단 파일 ─────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def migrate_client(tmp_path, monkeypatch):
|
||||
project = tmp_path / "project"
|
||||
|
||||
async def fake_root(project_id):
|
||||
return str(project)
|
||||
|
||||
async def fake_length(project_id):
|
||||
return 400.0
|
||||
|
||||
async def fake_latest(connection, project_id):
|
||||
return {"id": 7}
|
||||
|
||||
async def fake_longitudinal(connection, project_id, route_id):
|
||||
assert route_id == 7
|
||||
return {"longitudinal_file_path": LONG_REL}
|
||||
|
||||
class _Acquire:
|
||||
async def __aenter__(self):
|
||||
return object()
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(router_module, "_project_root", fake_root)
|
||||
monkeypatch.setattr(router_module, "_route_length", fake_length)
|
||||
monkeypatch.setattr(router_module, "get_latest_route", fake_latest)
|
||||
monkeypatch.setattr(router_module, "get_db_pool", lambda: SimpleNamespace(acquire=_Acquire))
|
||||
monkeypatch.setattr(b06_repo, "get_longitudinal_section", fake_longitudinal)
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
return TestClient(app), project
|
||||
|
||||
|
||||
def test_초안_측점_본문은_버리고_저장_관_자리는_이관_안_함(migrate_client):
|
||||
client, project = migrate_client
|
||||
pipes = pipe_points_path_in(project)
|
||||
pipes.parent.mkdir(parents=True, exist_ok=True)
|
||||
pipes.write_text(
|
||||
json.dumps({"route_signature": "s", "points": [{"chainage_m": 351.0, "id": "p1"}]}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
_write_longitudinal(
|
||||
project,
|
||||
[
|
||||
{"chainage_m": 0.0, "kind": "bp"},
|
||||
{"chainage_m": 351.0, "kind": "irregular", "structure": "파형강관 D800"},
|
||||
{"chainage_m": 400.0, "kind": "ep"},
|
||||
],
|
||||
)
|
||||
before = pipes.read_bytes()
|
||||
# 화면이 초안(관 351 → 364) 상세를 들고 와도 — 옛 본문 꼴을 보내 봄
|
||||
body = {"route_id": 7, "stations": [{"chainage_m": 364.0, "structure": "낙석방지책"}]}
|
||||
response = client.post(f"/api/projects/{PROJECT_ID}/route/structures/migrate", json=body)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["migrated"] == 0
|
||||
assert not (project / "B05_Profile" / "route" / "structures.json").exists()
|
||||
assert pipes.read_bytes() == before
|
||||
|
||||
|
||||
def test_저장_종단의_옛_자유글_측점은_이관(migrate_client):
|
||||
client, project = migrate_client
|
||||
_write_longitudinal(
|
||||
project,
|
||||
[
|
||||
{"chainage_m": 0.0, "kind": "bp"},
|
||||
{"chainage_m": 260.0, "kind": "irregular", "structure": "낙석방지책"},
|
||||
{"chainage_m": 400.0, "kind": "ep"},
|
||||
],
|
||||
)
|
||||
response = client.post(
|
||||
f"/api/projects/{PROJECT_ID}/route/structures/migrate", json={"route_id": None}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["migrated"] == 1
|
||||
stored = client.get(f"/api/projects/{PROJECT_ID}/route/structures").json()["structures"]
|
||||
assert [(item["type_id"], item["chainage_m"]) for item in stored] == [("etc", 260.0)]
|
||||
|
||||
|
||||
def test_저장_종단이_없으면_아무것도_안_함(migrate_client):
|
||||
client, project = migrate_client
|
||||
response = client.post(f"/api/projects/{PROJECT_ID}/route/structures/migrate", json={})
|
||||
assert response.status_code == 200 and response.json()["migrated"] == 0
|
||||
assert not (project / "B05_Profile" / "route" / "structures.json").exists()
|
||||
|
||||
|
||||
# ── 433-10 관 PUT 측점 다시 뜸 = 옛 cross 파일 지움 ─────────────────────────
|
||||
|
||||
|
||||
def test_관_옮김_측점_다시_뜸이_옛_자리_횡단_파일을_지움(tmp_path, monkeypatch):
|
||||
import B05_Profile.B05_Profile_Engine_Sections as engine
|
||||
import B06_Section.B06_Section_Router_Stations as stations_module
|
||||
|
||||
_write_longitudinal(
|
||||
tmp_path,
|
||||
[
|
||||
{"chainage_m": 0.0, "kind": "bp"},
|
||||
{"chainage_m": 351.0, "kind": "irregular", "structure": "파형강관 D800"},
|
||||
{"chainage_m": 400.0, "kind": "ep"},
|
||||
],
|
||||
)
|
||||
cross_dir = tmp_path / "B06_Section" / "cross_sections"
|
||||
cross_dir.mkdir(parents=True)
|
||||
for at in (0.0, 351.0, 400.0):
|
||||
(cross_dir / cross_filename(at)).write_text("{}", encoding="utf-8")
|
||||
|
||||
def fake_generate(*args, **kwargs):
|
||||
(cross_dir / cross_filename(361.0)).write_text("{}", encoding="utf-8")
|
||||
return [{"chainage_m": 361.0, "kind": "irregular", "structure": "파형강관 D800"}]
|
||||
|
||||
async def fake_inputs(project_id, route, snapshot):
|
||||
return None, None, {"longitudinal_file_path": LONG_REL}
|
||||
|
||||
async def fake_run(fn, *args):
|
||||
return None # DB 행 맞춤 · 등급 요약은 이 시험 밖
|
||||
|
||||
monkeypatch.setattr(engine, "_load_route_polyline", lambda *a: [[0.0, 0.0], [400.0, 0.0]])
|
||||
monkeypatch.setattr(engine, "_load_pipe_points", lambda *a: [])
|
||||
monkeypatch.setattr(engine, "resolve_extra_stations", lambda *a: [])
|
||||
monkeypatch.setattr(engine, "generate_irregular_sections", fake_generate)
|
||||
monkeypatch.setattr(stations_module, "_generation_inputs", fake_inputs)
|
||||
monkeypatch.setattr(stations_module, "run_with_connection", fake_run)
|
||||
|
||||
made = asyncio.run(
|
||||
stations_module._regenerate_structure_stations(
|
||||
PROJECT_ID,
|
||||
tmp_path,
|
||||
{"id": 7, "route_data_path": "route.json"},
|
||||
{"filter_key": "f", "method": "m"},
|
||||
)
|
||||
)
|
||||
assert made == 1
|
||||
assert sorted(p.name for p in cross_dir.glob("cross_*.json")) == sorted(
|
||||
cross_filename(at) for at in (0.0, 361.0, 400.0)
|
||||
)
|
||||
|
||||
|
||||
# ── 433-18 시종점 열쇠 한 규칙 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
STRUCTURES = [
|
||||
{"structure_id": "w1", "type_id": "retaining_wall", "chainage_m": 80.0, "start_m": 60.0},
|
||||
]
|
||||
# 구간형 옹벽 끝 = 노선 끝(EP) — 병합이 EP 행 종류를 irregular 로 갈음한 저장분 꼴
|
||||
SECTIONS = [
|
||||
{"chainage_m": 0.0, "kind": "bp"},
|
||||
{"chainage_m": 20.0, "kind": "regular"},
|
||||
{"chainage_m": 40.0, "kind": "regular"},
|
||||
{"chainage_m": 60.0, "kind": "irregular", "structure": "옹벽"},
|
||||
{"chainage_m": 80.0, "kind": "irregular", "structure": "옹벽"},
|
||||
{"chainage_m": 100.0, "kind": "irregular", "structure": "옹벽"},
|
||||
]
|
||||
|
||||
|
||||
def _structures_ns():
|
||||
return [SimpleNamespace(**item, end_m=100.0, options={}) for item in STRUCTURES]
|
||||
|
||||
|
||||
def test_서버_시종점_열쇠가_구조물보다_먼저():
|
||||
keys = station_keys(SECTIONS, [], _structures_ns(), 20.0)
|
||||
assert keys == {
|
||||
"end:BP": 0.0,
|
||||
"grid:1": 20.0,
|
||||
"grid:2": 40.0,
|
||||
"structure:w1:start": 60.0,
|
||||
"structure:w1": 80.0,
|
||||
"end:EP": 100.0,
|
||||
}
|
||||
|
||||
|
||||
def test_B06_열쇠가_서버와_같음(tmp_path):
|
||||
work = tmp_path / "b452"
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_452_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_452_entry.js", bundle)
|
||||
source = work / "input.json"
|
||||
source.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"sections": SECTIONS,
|
||||
"structures": [{**item, "end_m": 100.0} for item in STRUCTURES],
|
||||
"interval": 20.0,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_452_keys.mjs"), str(bundle), str(source)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
ts = {entry["key"]: entry["chainageM"] for entry in json.loads(proc.stdout)}
|
||||
assert ts == station_keys(SECTIONS, [], _structures_ns(), 20.0)
|
||||
|
||||
|
||||
def test_노선_저장_이월_EP_고침이_새_EP_에_남음():
|
||||
from B05_Profile.B05_Profile_Route_Save_Parts import carry_cross_inputs
|
||||
|
||||
old_keys = station_keys(SECTIONS, [], _structures_ns(), 20.0)
|
||||
# 노선이 늘어 옹벽이 따라감 — 끝(EP) 110 · 시작 70 · 기준 90
|
||||
moved = [
|
||||
SimpleNamespace(
|
||||
structure_id="w1",
|
||||
type_id="retaining_wall",
|
||||
chainage_m=90.0,
|
||||
start_m=70.0,
|
||||
end_m=110.0,
|
||||
options={},
|
||||
)
|
||||
]
|
||||
new_sections = [
|
||||
{"chainage_m": at, "kind": "regular"} for at in (0.0, 20.0, 40.0, 60.0, 70.0, 80.0, 90.0)
|
||||
] + [{"chainage_m": 100.0, "kind": "regular"}, {"chainage_m": 110.0, "kind": "irregular"}]
|
||||
new_keys = station_keys(new_sections, [], moved, 20.0)
|
||||
designs = {100.0: {"ground_type": "rock"}, 60.0: {"ground_type": "weathered"}}
|
||||
inputs, _, dropped = carry_cross_inputs(old_keys, new_keys, designs, ("ground_type",), ())
|
||||
assert inputs[110.0] == {"ground_type": "rock"} # EP 고침 = 새 EP
|
||||
assert inputs[70.0] == {"ground_type": "weathered"} # 구간 시작 고침 = 따라간 시작
|
||||
assert dropped == 0
|
||||
@@ -30,7 +30,13 @@ def _make_client(tmp_path, monkeypatch, invalidated_calls, *, stale_ok=True, rou
|
||||
|
||||
monkeypatch.setattr(router_module, "_project_root", fake_project_root)
|
||||
monkeypatch.setattr(router_module, "_invalidate_downstream", fake_invalidate)
|
||||
|
||||
async def fake_saved_irregular(project_id, root, route_id):
|
||||
# 이관 원천 = 저장 종단 파일(452) — 본문 대신 여기서 같은 측점을 줌
|
||||
return _migrate_payload()["stations"]
|
||||
|
||||
monkeypatch.setattr(router_module, "_route_length", fake_route_length)
|
||||
monkeypatch.setattr(router_module, "_saved_irregular_stations", fake_saved_irregular)
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
return TestClient(app)
|
||||
@@ -298,7 +304,11 @@ def test_migrate_keeps_existing_structures(client, tmp_path):
|
||||
assert len(stored) == 3 # 기존 1 + 이관 2(기슭막이는 관 지점으로)
|
||||
|
||||
|
||||
def test_migrate_empty_list_is_noop(client):
|
||||
def test_migrate_empty_list_is_noop(client, monkeypatch):
|
||||
async def none_saved(project_id, root, route_id):
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(router_module, "_saved_irregular_stations", none_saved)
|
||||
response = client.post(
|
||||
f"/api/projects/{PROJECT_ID}/route/structures/migrate", json={"stations": []}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user