Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -4,6 +4,7 @@ import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
@@ -401,8 +402,10 @@ async def get_section_detail(
|
||||
async def regenerate_sections(
|
||||
project_id: UUID, route_id: int, request: SectionRegenerateRequest
|
||||
) -> SectionDetailResponse | JSONResponse:
|
||||
"""표시 옵션의 횡단 반폭으로 종횡단을 재생성해 저장하고 상세를 반환한다."""
|
||||
return await regenerate_route_sections(project_id, route_id, request.cross_half_width_m)
|
||||
"""표시 옵션의 횡단 반폭으로 종횡단을 재생성해 저장 · 상세 반환(사용자 값 유지 · 446)."""
|
||||
from B06_Section.B06_Section_Router_Widen import widen_route_sections
|
||||
|
||||
return await widen_route_sections(project_id, route_id, request.cross_half_width_m)
|
||||
|
||||
|
||||
async def regenerate_route_sections(
|
||||
@@ -411,6 +414,7 @@ async def regenerate_route_sections(
|
||||
cross_half_width_m: float,
|
||||
*,
|
||||
stored_options: dict[str, Any] | None = None,
|
||||
carry: Callable[[aiomysql.Connection], Awaitable[None]] | None = None,
|
||||
) -> SectionDetailResponse | JSONResponse:
|
||||
"""`regenerate_sections` 본체 — `stored_options` 를 주면 DB 최신 종단 옵션 대신 그것을 쓴다.
|
||||
|
||||
@@ -472,6 +476,8 @@ async def regenerate_route_sections(
|
||||
route_id=route_id,
|
||||
sections=sections["cross_sections"],
|
||||
)
|
||||
if carry: # 옛 사용자 값을 새 행에 — 같은 트랜잭션(446)
|
||||
await carry(connection)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""B06 카드 폭 넘김 다시 만들기(446) — 지반 표본을 넓혀 종 · 횡단을 다시 만들되 사용자 값은 그대로.
|
||||
|
||||
`regenerate_route_sections` 는 종 · 횡단 행을 지우고 새로 만든다. 같은 노선 · 같은 측점이므로
|
||||
측점 열쇠(누가거리 0.01 m 안)로 옛 횡단 행의 설계(지반 · 메모 · CAD · 상세 표 · 조절) · 상태를,
|
||||
옛 종단 행의 사용자 키(표준 횡단면 · 유토 풍선 · 계획 입력 · 상태)를 같은 트랜잭션에서 새 행에
|
||||
옮겨 담고, 계획고 편집(종단 정본 `profile_alignment`)을 옛 계획선 통째로 되살린 뒤 [저장]과 같은
|
||||
서버 재계산으로 넓힌 지반 위에 다시 셈. 뒤 단계가 실패하면 종 · 횡단 파일을 옛것으로 되돌림.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import aiomysql
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B06_Section.B06_Section_Repository import get_longitudinal_section
|
||||
from B06_Section.B06_Section_Schema import SectionDetailResponse
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_MATCH_M = 0.01 # 측점 열쇠 — 저장 길(`update_cross_section_design`)과 같은 근사
|
||||
|
||||
|
||||
def carried_longitudinal_data(old: dict[str, Any], new: dict[str, Any]) -> dict[str, Any]:
|
||||
"""새 종단 data — 생성 요약 · 옵션은 새것 · 나머지(표준 횡단면 · 유토 · 계획 입력)는 옛것."""
|
||||
merged = {**old, **new, "options": {**(old.get("options") or {}), **(new.get("options") or {})}}
|
||||
if "grade_overrides" in old: # 사용자 계획 입력 — 다시 만들기는 늘 빈 값으로 냄
|
||||
merged["grade_overrides"] = old["grade_overrides"]
|
||||
return merged
|
||||
|
||||
|
||||
def carried_cross_rows(
|
||||
old_rows: list[dict[str, Any]], new_rows: list[dict[str, Any]]
|
||||
) -> tuple[list[tuple[str, str, int]], int]:
|
||||
"""(새 행 UPDATE 값 [(data, status, id)], 못 옮긴 옛 행 수) — 새 요약 위에 옛 data · 상태."""
|
||||
updates: list[tuple[str, str, int]] = []
|
||||
used: set[int] = set()
|
||||
for row in new_rows:
|
||||
at = float(row["chainage_m"])
|
||||
near = [
|
||||
index
|
||||
for index, old in enumerate(old_rows)
|
||||
if abs(float(old["chainage_m"]) - at) < _MATCH_M
|
||||
]
|
||||
if not near:
|
||||
continue
|
||||
index = max(near, key=lambda i: int(old_rows[i]["id"])) # 같은 측점 둘이면 최신 행
|
||||
used.update(near)
|
||||
old = old_rows[index]
|
||||
data = {**_as_dict(old.get("data")), **_as_dict(row.get("data"))}
|
||||
updates.append((json.dumps(data, ensure_ascii=False), str(old["status"]), int(row["id"])))
|
||||
return updates, len(old_rows) - len(used)
|
||||
|
||||
|
||||
def _as_dict(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, (str, bytes)):
|
||||
value = json.loads(value)
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
async def _cross_rows(connection: aiomysql.Connection, route_id: int) -> list[dict[str, Any]]:
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"SELECT id, chainage_m, data, status FROM cross_sections"
|
||||
" WHERE route_id = %s ORDER BY id",
|
||||
(route_id,),
|
||||
)
|
||||
return list(await cursor.fetchall())
|
||||
|
||||
|
||||
async def widen_route_sections(
|
||||
project_id: UUID, route_id: int, cross_half_width_m: float
|
||||
) -> SectionDetailResponse | JSONResponse:
|
||||
"""표본 폭 넘김 다시 만들기 — 사용자 값을 옮겨 담은 뒤 새 상세를 돌려줌."""
|
||||
from B05_Profile.B05_Profile_Engine_Sections import _STAGE_SUBDIR
|
||||
from B05_Profile.B05_Profile_Route_Save import _Backup, _carry_profile, _read_json
|
||||
from B06_Section.B06_Section_Router import get_section_detail, regenerate_route_sections
|
||||
from B06_Section.B06_Section_Router_Confirm import _recompute_stored_designs
|
||||
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
old_long = await get_longitudinal_section(connection, project_id, route_id)
|
||||
old_rows = await _cross_rows(connection, route_id)
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
relative = str((old_long or {}).get("longitudinal_file_path") or "")
|
||||
old_alignment = (_read_json(project_root / relative) if relative else {}).get(
|
||||
"profile_alignment"
|
||||
) or {}
|
||||
stage_dir = project_root / _STAGE_SUBDIR
|
||||
backup = _Backup(project_root, [], [stage_dir / "longitudinal", stage_dir / "cross_sections"])
|
||||
dropped = 0
|
||||
|
||||
async def carry(connection: aiomysql.Connection) -> None:
|
||||
nonlocal dropped
|
||||
updates, dropped = carried_cross_rows(old_rows, await _cross_rows(connection, route_id))
|
||||
async with connection.cursor() as cursor:
|
||||
if updates:
|
||||
await cursor.executemany(
|
||||
"UPDATE cross_sections SET data = %s, status = %s WHERE id = %s", updates
|
||||
)
|
||||
if old_long:
|
||||
await cursor.execute(
|
||||
"SELECT id, data FROM longitudinal_sections WHERE route_id = %s"
|
||||
" ORDER BY id DESC LIMIT 1",
|
||||
(route_id,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
data = carried_longitudinal_data(old_long.get("data") or {}, _as_dict(row[1]))
|
||||
await cursor.execute(
|
||||
"UPDATE longitudinal_sections SET data = %s, status = %s WHERE id = %s",
|
||||
(json.dumps(data, ensure_ascii=False), old_long["status"], int(row[0])),
|
||||
)
|
||||
|
||||
try:
|
||||
made = await regenerate_route_sections(
|
||||
project_id, route_id, cross_half_width_m, carry=carry
|
||||
)
|
||||
if isinstance(made, JSONResponse):
|
||||
await asyncio.to_thread(backup.restore)
|
||||
return made
|
||||
try: # 같은 노선 — `_carry_profile` 의 옛 계획선 통째 갈래(빈 정점열 = 바뀐 구간 없음)
|
||||
async with pool.acquire() as connection:
|
||||
new_long = await get_longitudinal_section(connection, project_id, route_id)
|
||||
new_relative = str((new_long or {}).get("longitudinal_file_path") or "")
|
||||
await _carry_profile(project_root, new_relative, route_id, old_alignment, [], [])
|
||||
except Exception as exc: # noqa: BLE001 — 계획고 편집을 잃느니 파일을 옛것으로
|
||||
logger.exception("B06 표본 넓힘: 계획고 옮김 실패 project_id=%s", project_id)
|
||||
await asyncio.to_thread(backup.restore)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": f"계획고 편집 옮김 실패: {exc}"},
|
||||
)
|
||||
finally:
|
||||
backup.drop()
|
||||
if dropped:
|
||||
logger.warning(
|
||||
"B06 표본 넓힘: 새 측점에 없는 옛 횡단 행 %d건 project_id=%s", dropped, project_id
|
||||
)
|
||||
await _recompute_stored_designs(project_id, route_id) # [저장]과 같은 서버 재계산(실패 = 로그)
|
||||
return await get_section_detail(project_id, route_id)
|
||||
@@ -1,5 +1,10 @@
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { regenerateSections, type SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { loadSectionDetail } from "./B06_Section_Section_Store";
|
||||
|
||||
@@ -21,7 +26,8 @@ export interface SampleWidenerDeps {
|
||||
* 요청 반폭까지 지반 샘플을 넓히는 함수를 만든다 — 보유 샘플로 충분하면 즉시 true
|
||||
* (재계산 없음). 모자라면 그 폭으로 B05부터 재생성(영구 저장)하고 상세를 다시 받는다.
|
||||
* 느려도 허용(2026-08-06 사용자 확정). 전역 [전체 측점 반영]과 카드 줌 버튼의 반폭
|
||||
* 확대가 같은 경로를 쓴다(2026-08-23 사용자 지시).
|
||||
* 확대가 같은 경로를 쓴다(2026-08-23 사용자 지시). 다시 만들기 전 확인창 · 서버가 측점 설계 ·
|
||||
* 계획고 편집 · 유토 풍선을 옮겨 담음(446).
|
||||
*/
|
||||
export function createSampleWidener(deps: SampleWidenerDeps): (widthM: number) => Promise<boolean> {
|
||||
let running = false;
|
||||
@@ -31,8 +37,14 @@ export function createSampleWidener(deps: SampleWidenerDeps): (widthM: number) =
|
||||
if (widthM <= deps.sampledHalfWidth() + 1e-6) return true;
|
||||
if (running) return false; // 재생성 중 중복 요청은 무시(버튼 연타).
|
||||
running = true;
|
||||
showLoadingOverlay();
|
||||
let shown = false; // 오버레이는 셈 수 — 취소면 안 내림(남의 오버레이까지 내려감)
|
||||
try {
|
||||
const ask = L("B06_Profile_Regenerate_Confirm")
|
||||
.replace("{width}", String(widthM))
|
||||
.replace("{sampled}", String(deps.sampledHalfWidth()));
|
||||
if (!(await showConfirmDialog(ask, L("B06_Profile_Regenerate_Go")))) return false;
|
||||
showLoadingOverlay();
|
||||
shown = true;
|
||||
await regenerateSections(target.projectId, target.routeId, widthM);
|
||||
// 재생성 응답에는 설계가 없다 — 상세를 다시 받아 기본 설계 프리뷰까지 얹는다 · 공유 캐시도
|
||||
// 새 상세로(B05 가 옛 값을 못 보게) · 초안 측점 · 그림 자료도 같은 길(323-4)
|
||||
@@ -46,7 +58,7 @@ export function createSampleWidener(deps: SampleWidenerDeps): (widthM: number) =
|
||||
return false;
|
||||
} finally {
|
||||
running = false;
|
||||
hideLoadingOverlay();
|
||||
if (shown) hideLoadingOverlay();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const STUBS = {
|
||||
showLoadingOverlay: () => calls.overlay++,
|
||||
hideLoadingOverlay: () => {},
|
||||
showToast: () => {},
|
||||
showConfirmDialog: async () => true, // 다시 만들기 확인창(446) — 이 시험은 늘 확인
|
||||
},
|
||||
"./B06_Section_Api_Fetch": {
|
||||
regenerateSections: async (_p, _r, width) => calls.regenerate.push(width),
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* 카드 폭 넘김 다시 만들기 확인창(PLAN 446) 단위검증 헬퍼 — `createSampleWidener` 를 트랜스파일해 돌린다.
|
||||
* 취소 = 재생성 없음 · 오버레이 셈 그대로 · 확인 = 재생성 한 번 · 오버레이 올림 · 내림 짝. 결과는 JSON 한 벌.
|
||||
* 짝 = `test_446_widen_carry.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const calls = { regenerate: [], overlay: 0, asked: [] };
|
||||
let answer = false;
|
||||
const STUBS = {
|
||||
"@ui/ui_template_locale": {
|
||||
currentLanguageIndex: 0,
|
||||
ui_locales: new Proxy({}, { get: (_t, key) => [`${String(key)} {width}/{sampled}`] }),
|
||||
},
|
||||
"@ui/ui_template_elements": {
|
||||
showLoadingOverlay: () => calls.overlay++,
|
||||
hideLoadingOverlay: () => calls.overlay--,
|
||||
showToast: () => {},
|
||||
showConfirmDialog: async (message) => {
|
||||
calls.asked.push(message);
|
||||
return answer;
|
||||
},
|
||||
},
|
||||
"./B06_Section_Api_Fetch": {
|
||||
regenerateSections: async (_p, _r, width) => calls.regenerate.push(width),
|
||||
},
|
||||
"./B06_Section_Section_Store": { loadSectionDetail: async () => ({ cross_sections: [] }) },
|
||||
};
|
||||
|
||||
const file = path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Common.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (r) => STUBS[r]);
|
||||
|
||||
(async () => {
|
||||
const widen = box.exports.createSampleWidener({
|
||||
target: () => ({ projectId: "p", routeId: 1 }),
|
||||
sampledHalfWidth: () => 20,
|
||||
applyDetail: () => {},
|
||||
});
|
||||
const out = {};
|
||||
out.within = await widen(15); // 표본 안 = 묻지 않음
|
||||
out.withinAsked = calls.asked.length;
|
||||
out.cancel = await widen(21);
|
||||
out.cancelCalls = calls.regenerate.length;
|
||||
out.cancelOverlay = calls.overlay;
|
||||
answer = true;
|
||||
out.ok = await widen(21);
|
||||
out.okCalls = [...calls.regenerate];
|
||||
out.okOverlay = calls.overlay;
|
||||
out.asked = calls.asked;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})();
|
||||
@@ -0,0 +1,106 @@
|
||||
"""446 카드 폭 넘김 다시 만들기 — 표본 폭을 넘겨 종 · 횡단을 다시 만들어도 사용자 값 그대로.
|
||||
|
||||
- 횡단 행: 측점 열쇠(0.01 m 안)로 옛 data(설계 · 메모 · CAD · 상세 표 · 조절) · 상태 · 새 요약 이김
|
||||
- 종단 행: 생성 요약 · 생성 옵션은 새것 · 표준 횡단면 · 유토(풍선) · 계획 입력은 옛것
|
||||
- 화면: 다시 만들기 전 확인창 · 취소 = 재생성 없음(`helper_446_widen_confirm.cjs`)
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B06_Section.B06_Section_Router_Widen import carried_cross_rows, carried_longitudinal_data
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
DESIGN = {
|
||||
"ground_type": "soil",
|
||||
"section_mode": "right_cut",
|
||||
"structure_note": "메모",
|
||||
"structure_note_cad": {"lines": [1]},
|
||||
"detail_table": {"rows": 2},
|
||||
"display_half_width_m": 21,
|
||||
"basin_adjust": {"d": 0.3},
|
||||
}
|
||||
|
||||
|
||||
def _old(row_id: int, at: float, design: dict | None = None, status: str = "CONFIRMED") -> dict:
|
||||
data = {"sample_count": 41, "center_z": 100.0, "design": design or DESIGN}
|
||||
return {"id": row_id, "chainage_m": at, "data": json.dumps(data), "status": status}
|
||||
|
||||
|
||||
def _new(row_id: int, at: float) -> dict:
|
||||
return {"id": row_id, "chainage_m": at, "data": json.dumps({"sample_count": 43})}
|
||||
|
||||
|
||||
def test_횡단_행은_측점_열쇠로_설계_상태_그대로() -> None:
|
||||
old = [_old(1, 0.0), _old(2, 20.0), _old(3, 33.456)]
|
||||
new = [_new(11, 0.0), _new(12, 20.0), _new(13, 33.46), _new(14, 40.0)]
|
||||
updates, dropped = carried_cross_rows(old, new)
|
||||
assert dropped == 0
|
||||
by_id = {row_id: (json.loads(data), status) for data, status, row_id in updates}
|
||||
assert set(by_id) == {11, 12, 13} # 40 m = 새 측점 · 옛 값 없음
|
||||
data, status = by_id[13] # 0.004 m 어긋남도 같은 측점
|
||||
assert data["design"] == DESIGN and status == "CONFIRMED"
|
||||
assert data["sample_count"] == 43 and data["center_z"] == 100.0 # 새 요약이 이김 · 옛 칸 남김
|
||||
|
||||
|
||||
def test_같은_측점_둘이면_최신_행_못_옮긴_행은_셈() -> None:
|
||||
newer = {**DESIGN, "structure_note": "새 메모"}
|
||||
old = [_old(1, 20.0), _old(5, 20.0, newer, "DRAFT"), _old(6, 55.0)]
|
||||
updates, dropped = carried_cross_rows(old, [_new(12, 20.0)])
|
||||
data, status, _ = updates[0]
|
||||
assert json.loads(data)["design"]["structure_note"] == "새 메모" and status == "DRAFT"
|
||||
assert dropped == 1 # 55 m — 새 측점에 없음(로그로 알림)
|
||||
|
||||
|
||||
def test_종단_data_는_사용자_키를_옛것으로() -> None:
|
||||
old = {
|
||||
"length_m": 500.0,
|
||||
"options": {"cross_half_width_m": 20, "standard_cross_section": {"road_width_m": 3}},
|
||||
"grade_overrides": {"max_grade_pct": 12},
|
||||
"grade_summary": {"old": 1},
|
||||
"mass_haul": {"balloon_offsets": {"0": [3.0, -4.0]}},
|
||||
}
|
||||
new = {
|
||||
"length_m": 500.0,
|
||||
"options": {"cross_half_width_m": 21, "station_interval_m": 20},
|
||||
"grade_overrides": {},
|
||||
"grade_summary": {"new": 1},
|
||||
}
|
||||
merged = carried_longitudinal_data(old, new)
|
||||
assert merged["options"] == {
|
||||
"cross_half_width_m": 21,
|
||||
"station_interval_m": 20,
|
||||
"standard_cross_section": {"road_width_m": 3},
|
||||
}
|
||||
assert merged["grade_overrides"] == {"max_grade_pct": 12}
|
||||
assert merged["mass_haul"] == {"balloon_offsets": {"0": [3.0, -4.0]}}
|
||||
assert merged["grade_summary"] == {"new": 1} # 계획고 옮김이 다시 씀
|
||||
|
||||
|
||||
def test_반폭_요청은_옮겨_담는_길로() -> None:
|
||||
router = (ROOT / "B06_Section" / "B06_Section_Router.py").read_text(encoding="utf-8")
|
||||
at = router.index("async def regenerate_sections(")
|
||||
body = router[at : router.index("async def regenerate_route_sections(", at)]
|
||||
assert "widen_route_sections(" in body and "regenerate_route_sections(" not in body
|
||||
assert "await carry(connection)" in router # 같은 트랜잭션 안
|
||||
|
||||
|
||||
@pytest.mark.skipif(not (ROOT / "config" / "node_modules").is_dir(), reason="의존성 미설치")
|
||||
def test_다시_만들기_전_확인창_취소면_재생성_없음() -> None:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_446_widen_confirm.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
out = json.loads(done.stdout)
|
||||
assert out["within"] is True and out["withinAsked"] == 0
|
||||
assert out["cancel"] is False and out["cancelCalls"] == 0 and out["cancelOverlay"] == 0
|
||||
assert out["ok"] is True and out["okCalls"] == [21] and out["okOverlay"] == 0
|
||||
assert out["asked"][0].endswith("21/20") # 요청 폭 · 표본 폭이 글에 들어감
|
||||
@@ -162,4 +162,10 @@ export const ui_locales_b3 = {
|
||||
"원가계산은 마스터를 쓰는 얼개로 다시 짓는 중입니다 — 표와 계산은 아직 없습니다.",
|
||||
"Cost estimation is being rebuilt on the master data — no tables or calculations yet.",
|
||||
],
|
||||
/* --- B06 카드 폭 넘김 다시 만들기(446) --- */
|
||||
B06_Profile_Regenerate_Confirm: [
|
||||
"카드 폭 {width} m 가 지반 표본 폭 {sampled} m 를 넘어 종·횡단을 다시 만듭니다. 측점 설계 · 계획고 편집 · 유토 풍선은 그대로 옮깁니다.",
|
||||
"View width {width} m exceeds the sampled ground width {sampled} m — sections will be rebuilt. Station designs, grade edits and haul balloons are kept.",
|
||||
],
|
||||
B06_Profile_Regenerate_Go: ["다시 만들기", "Rebuild"],
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user