Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -44,9 +44,13 @@ import {
|
||||
restoreSectionDetail,
|
||||
swapSectionStations,
|
||||
} from "../../B06_Section/B06_Section_Section_Store";
|
||||
import { refreshCrossDesigns } from "../../B06_Section/B06_Section_Cross_Refresh";
|
||||
import {
|
||||
refreshCrossDesigns,
|
||||
type CrossRefreshInput,
|
||||
} from "../../B06_Section/B06_Section_Cross_Refresh";
|
||||
import { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist";
|
||||
import { detailCross } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import type { CrossOutput, PipelineStation } from "./logic/design_pipeline_types";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -229,16 +233,17 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
||||
const spans = bermSpansFromStructures(Array.isArray(structures) ? structures : []);
|
||||
return (chainageM) => bermSpecAt(spans, chainageM);
|
||||
},
|
||||
// 저장 노선 횡단 = 다시 셈 창구 한 벌(365-4) — 카드 · 유토곡선 · [저장] 패치가 보는 상세 설계를 고치고 그 값을
|
||||
// 묶음 출력으로(발자국 · 평면 횡단 판) · 측점이 상세와 다르면(갈아끼움 전) 묶음 엔진
|
||||
savedCross: async (detail, routeId, stations) => {
|
||||
if (!detailCross(detail, stations)) return null;
|
||||
const edits = profile.pipelineSource().edits;
|
||||
const updated = await refreshCrossDesigns({ projectId, routeId, detail, edits });
|
||||
if (updated.length) crossDesignsUpdated(shared, updated);
|
||||
profile.redraw();
|
||||
return detailCross(detail, stations);
|
||||
},
|
||||
// 저장 노선 횡단 = 다시 셈 창구 한 벌(365-4) — 카드 · 유토곡선 · [저장] 패치가 보는 상세 설계를 늘 고치고 그
|
||||
// 값을 묶음 출력으로(발자국 · 평면 횡단 판) · 측점이 상세와 다르면(갈아끼움 전) 묶음 엔진(439)
|
||||
savedCross: (detail, routeId, stations) =>
|
||||
refreshSavedCross(
|
||||
{ projectId, routeId, detail, edits: profile.pipelineSource().edits },
|
||||
stations,
|
||||
(updated) => {
|
||||
if (updated.length) crossDesignsUpdated(shared, updated);
|
||||
profile.redraw();
|
||||
},
|
||||
),
|
||||
plantedBase, // 저장 노선 종단 기준 = 입력이 심는 측점(386-3 · 보기 심기 무관)
|
||||
structureAreas: (detail) => {
|
||||
// 초안 상세에도 저장 안 한 조절(옹벽 · 세월 · BOX · 집수 · 417 M3) — 면적 얹기 앞
|
||||
@@ -262,6 +267,19 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장 노선 cross 차례(387-4 · 439) — 카드 자료(상세 설계)는 **늘** 다시 셈 · 묶음에 돌려줄 값만 측점 대조
|
||||
* (상세 측점 ≠ 묶음 측점이면 null = 묶음 엔진). 대조가 앞서면 1 mm 어긋남에 카드 단추가 안 먹었다(요구 266).
|
||||
*/
|
||||
export async function refreshSavedCross(
|
||||
input: CrossRefreshInput,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
onUpdated: (updated: number[]) => void,
|
||||
): Promise<CrossOutput | null> {
|
||||
onUpdated(await refreshCrossDesigns(input));
|
||||
return detailCross(input.detail, stations);
|
||||
}
|
||||
|
||||
/** 「구조물 배치」 폼 한 벌 — 알림은 지금 보기로, 상세 옵션은 횡단 보기에서만 그린다. */
|
||||
export function sharedStructures(shared: DesignPageShared): StructuresSection {
|
||||
if (shared.structures) return shared.structures;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { clearState, readState, stateKey, writeState } from "../A00_Common/b_pag
|
||||
import { seedPageHistory } from "../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import type { CrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
||||
import type {
|
||||
CrossDesign,
|
||||
CrossDesignRequest,
|
||||
@@ -306,6 +307,8 @@ export async function previewCrossDesigns(
|
||||
>;
|
||||
/** 측점별 암 절토 경사비(chainage 키 → 1:n 의 n). 0 은 「표준값을 씀」이다. */
|
||||
cutSlopeRatios?: Record<string, number>;
|
||||
/** 측점별 카드 선택(chainage 키 → 지반 · 측구 · 복합경사 …) 세션값 — 저장분 위에 얹음(439). */
|
||||
designChoices?: Record<string, CrossDesignChoice>;
|
||||
},
|
||||
): Promise<CrossDesignPreviewResponse> {
|
||||
return requestJson<CrossDesignPreviewResponse>(
|
||||
@@ -319,6 +322,7 @@ export async function previewCrossDesigns(
|
||||
rock_boundary_offsets: options?.rockBoundaryOffsets ?? null,
|
||||
berms: options?.berms ?? null,
|
||||
cut_slope_ratios: options?.cutSlopeRatios ?? null,
|
||||
design_choices: options?.designChoices ?? null,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -183,7 +183,7 @@ export function staleKeyAgainstScreen(detail: SectionDetailResponse, routeId: nu
|
||||
/**
|
||||
* 전 측점 횡단을 현재 계획선으로 다시 계산해 `detail.cross_sections[].design`을 제자리 교체한다.
|
||||
* 돌려주는 값은 설계 · 면적이 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다.
|
||||
* 부르는 곳은 계산 묶음 cross 단계 하나(`design_shared` savedCross · 387-4) — 화면은 `pipelineCross` 로 부탁.
|
||||
* 부르는 곳은 계산 묶음 cross 단계 하나(`design_shared` refreshSavedCross · 387-4) — 화면은 `pipelineCross` 로 부탁.
|
||||
* 계획선 ▲ 한 번에 값이 바뀌는 측점은 몇 개뿐 — 전부 돌려주면 카드를 다 갈아 끼워 멈춤(54-22).
|
||||
*/
|
||||
export async function refreshCrossDesigns(input: CrossRefreshInput): Promise<number[]> {
|
||||
@@ -486,6 +486,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
/** 옛 데이터 폴백 — 선형 저장분이 없어 브라우저가 계획고를 풀 수 없을 때만 쓴다. */
|
||||
async function refreshFromServer(input: CrossRefreshInput): Promise<number[]> {
|
||||
const { projectId, routeId, detail, edits, shouldApply } = input;
|
||||
const choices = crossDesignChoices(projectId, routeId);
|
||||
const response = await previewCrossDesigns(
|
||||
projectId,
|
||||
routeId,
|
||||
@@ -498,6 +499,13 @@ async function refreshFromServer(input: CrossRefreshInput): Promise<number[]> {
|
||||
// 측점별 암 절토 경사도 세션값이라 함께 싣는다 — 안 실으면 서버 폴백에서만
|
||||
// 사용자 경사가 조용히 표준값으로 되돌아간다(2026-09-07).
|
||||
cutSlopeRatios: readCutSlopeSession(projectId, routeId),
|
||||
// 카드 단추 선택(지반 · 측구 · 복합경사)도 — 안 실으면 폴백에서만 단추가 안 먹음(439) · 열쇠 = 측점 자리
|
||||
designChoices: Object.fromEntries(
|
||||
detail.cross_sections.flatMap((section) => {
|
||||
const choice = choices.get(rockKey(section.chainage_m));
|
||||
return choice ? [[section.chainage_m.toFixed(3), choice]] : [];
|
||||
}),
|
||||
),
|
||||
},
|
||||
);
|
||||
if (shouldApply && !shouldApply()) return [];
|
||||
|
||||
@@ -494,6 +494,18 @@ async def regenerate_route_sections(
|
||||
)
|
||||
|
||||
|
||||
def _with_design_choices(
|
||||
designs: list[dict[str, Any]], choices: dict[str, dict[str, Any]] | None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""카드 선택 세션값을 저장분 위에 얹은 행 목록(439) — 뒤 행이 같은 측점 앞 행을 덮음."""
|
||||
stored = {round(float(r["chainage_m"]), 3): r.get("design") or {} for r in designs}
|
||||
picked = {round(float(key), 3): choice for key, choice in (choices or {}).items()}
|
||||
return designs + [
|
||||
{"chainage_m": key, "design": {**stored.get(key, {}), **choice}}
|
||||
for key, choice in picked.items()
|
||||
]
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/sections/{route_id}/cross-design/preview",
|
||||
response_model=CrossDesignPreviewResponse,
|
||||
@@ -522,6 +534,7 @@ async def preview_cross_designs(
|
||||
)
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
designs = await get_cross_section_designs(connection, route_id)
|
||||
designs = _with_design_choices(designs, request.design_choices)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
detail = await asyncio.to_thread(
|
||||
_read_section_detail,
|
||||
|
||||
@@ -400,6 +400,8 @@ class CrossDesignPreviewRequest(BaseModel):
|
||||
# 측점별 암 절토 경사비(chainage 키 → 1:n 의 n). 위와 같은 성격 — 확정 전 세션값을
|
||||
# 재계산에 반영한다(2026-09-07). **0 은 「표준값을 씀」**(되돌리기)이다.
|
||||
cut_slope_ratios: dict[str, float] | None = None
|
||||
# 측점별 카드 선택(chainage 키 → 지반 · 단면 · 측구 · 복합경사). 위와 같은 성격 — 저장분 위에 얹음(439).
|
||||
design_choices: dict[str, dict[str, Any]] | None = None
|
||||
|
||||
def edits(self) -> dict[str, Any]:
|
||||
return {"station_offsets": self.station_offsets, "curve_radii": self.curve_radii}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/* PLAN 439 헬퍼 — 저장 노선 cross 차례(`refreshSavedCross`)가 카드 단추(복합경사 · 측구 · 지반)를 늘 다시 셈:
|
||||
* 묶음 측점 = 상세 측점(돌려줌) · 다름 1 mm 넘게(null = 묶음 엔진 · 카드 자료는 고침) · 서버 폴백에 카드 선택 실음.
|
||||
* node helper_266_card_toggle.mjs <묶음.mjs> <job.json>
|
||||
* 짝 = `test_266_card_toggle_mismatch.py` · 표준출력 JSON 한 벌.
|
||||
*/
|
||||
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;
|
||||
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
globalThis.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {} });
|
||||
globalThis.addEventListener = () => undefined;
|
||||
globalThis.removeEventListener = () => undefined;
|
||||
globalThis.screen = { width: 1920, height: 1080 };
|
||||
globalThis.location = {
|
||||
href: "http://localhost/",
|
||||
pathname: "/",
|
||||
search: "",
|
||||
hash: "",
|
||||
origin: "http://localhost",
|
||||
};
|
||||
|
||||
const job = JSON.parse(readFileSync(process.argv[3], "utf8"));
|
||||
// 서버 폴백 창구만 받아 적음 — 그 밖 요청은 404(격자 없음 = 넓힘 안 함)
|
||||
const previews = [];
|
||||
globalThis.fetch = async (url, init) => {
|
||||
if (String(url).includes("/cross-design/preview")) {
|
||||
previews.push(JSON.parse(init.body));
|
||||
return Response.json({ designs: [] });
|
||||
}
|
||||
return new Response("{}", { status: 404 });
|
||||
};
|
||||
|
||||
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||
const R = 7;
|
||||
const EDITS = { station_offsets: {}, curve_radii: {} };
|
||||
const alignment = {
|
||||
stations: job.sections.map((s) => ({ chainage_m: s.chainage_m, plan_elevation_m: s.plan_z })),
|
||||
samples: job.sections.map((s) => ({ chainage_m: s.chainage_m, elevation_m: s.plan_z })),
|
||||
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
|
||||
};
|
||||
const detailOf = (withLine) => ({
|
||||
longitudinal: withLine
|
||||
? { length_m: 1000, stations: [], profile_alignment: { policy: {} } }
|
||||
: { length_m: 1000, stations: [] },
|
||||
cross_sections: job.sections.map(({ plan_z, ...section }) => ({
|
||||
...structuredClone(section),
|
||||
design: {
|
||||
ground_type: "ripping_rock",
|
||||
section_mode: job.mode,
|
||||
design_elevation_m: plan_z,
|
||||
},
|
||||
})),
|
||||
});
|
||||
const pick = (design) => ({
|
||||
ground_type: design.ground_type,
|
||||
two_stage_slope: design.two_stage_slope ?? null,
|
||||
ditch_enabled: design.ditch_enabled ?? null,
|
||||
cut: design.cut_area_m2 ?? null,
|
||||
fill: design.fill_area_m2 ?? null,
|
||||
line: JSON.stringify(design.design_line ?? null),
|
||||
});
|
||||
|
||||
/** 카드 단추 줄 차례 — 처음 셈 → 복합경사 켬 → 측구 끔 → 지반 흙. `shift` = 묶음 측점 어긋남(m). */
|
||||
async function toggles(projectId, shift) {
|
||||
m.writeState("std-cross", job.standard, projectId);
|
||||
m.rememberRockBoundaryDefault(projectId, job.rockDefault);
|
||||
const detail = detailOf(true);
|
||||
const target = detail.cross_sections[job.target];
|
||||
const stations = detail.cross_sections.map((s, i) => ({
|
||||
key: `s${i}`,
|
||||
chainageM: s.chainage_m + shift,
|
||||
}));
|
||||
const steps = [];
|
||||
const run = async (tag) => {
|
||||
const updated = [];
|
||||
const out = await m.refreshSavedCross(
|
||||
{ projectId, routeId: R, detail, edits: EDITS, alignment },
|
||||
stations,
|
||||
(rows) => updated.push(...rows),
|
||||
);
|
||||
steps.push({
|
||||
tag,
|
||||
out: out ? Object.keys(out.byKey).length : null,
|
||||
updated,
|
||||
...pick(target.design),
|
||||
});
|
||||
};
|
||||
await run("open");
|
||||
const choice = {
|
||||
ground_type: "ripping_rock",
|
||||
section_mode: job.mode,
|
||||
ditch_side: null,
|
||||
ditch_type: "standard",
|
||||
paved: false,
|
||||
two_stage_slope: false,
|
||||
ditch_choice: null,
|
||||
};
|
||||
for (const [tag, over] of [
|
||||
["two_stage", { two_stage_slope: true }],
|
||||
["ditch_off", { ditch_choice: false }],
|
||||
["soil", { ground_type: "soil" }],
|
||||
]) {
|
||||
Object.assign(choice, over);
|
||||
m.writeCrossDesignChoice(projectId, R, target.chainage_m, { ...choice });
|
||||
await run(tag);
|
||||
}
|
||||
return { target: target.chainage_m, steps };
|
||||
}
|
||||
|
||||
const out = { same: await toggles("p266a", 0), mismatch: await toggles("p266b", 0.002) };
|
||||
|
||||
/* 서버 폴백(선형 저장분 없음) — 카드 선택이 요청에 실림 · 열쇠 = 측점 자리 */
|
||||
{
|
||||
const projectId = "p266c";
|
||||
m.writeState("std-cross", job.standard, projectId);
|
||||
const detail = detailOf(false);
|
||||
const at = detail.cross_sections[job.target].chainage_m;
|
||||
m.writeCrossDesignChoice(projectId, R, at, {
|
||||
ground_type: "soil",
|
||||
section_mode: job.mode,
|
||||
two_stage_slope: true,
|
||||
ditch_choice: false,
|
||||
});
|
||||
await m.refreshSavedCross({ projectId, routeId: R, detail, edits: EDITS }, [], () => undefined);
|
||||
out.server = { at, choices: previews.at(-1)?.design_choices ?? null };
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,5 @@
|
||||
/* PLAN 439 시험 묶음 진입점 — 저장 노선 cross 차례(카드 단추 → 다시 셈 · 측점 어긋남) · 진짜 모듈 · vite ssr. */
|
||||
export { refreshSavedCross } from "../../A00_Common/design/design_shared";
|
||||
export { writeCrossDesignChoice } from "../../B06_Section/B06_Section_Cross_Design_Session";
|
||||
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
|
||||
export { writeState } from "../../A00_Common/b_page_state";
|
||||
@@ -0,0 +1,137 @@
|
||||
"""PLAN 439 — B06 카드 머리 줄 단추가 저장 노선에서 늘 반영됨(요구 266).
|
||||
|
||||
못박는 것
|
||||
- 저장 노선 cross 차례(`refreshSavedCross`) = 카드 자료(상세 설계)를 **늘** 다시 셈 —
|
||||
복합경사 · 측구 · 지반 단추마다 그 측점 설계선 · 면적이 바뀜.
|
||||
- 묶음 측점 = 상세 측점이면 돌려줌 · 1 mm 넘게 다르면 null(묶음 엔진) — 그래도 카드 자료는 고침.
|
||||
- 서버 폴백(선형 저장분 없음)에도 카드 선택이 실림(열쇠 = 측점 자리) · 서버는 저장분 위에 얹음.
|
||||
짝: `helper_266_card_toggle.mjs` · 진입점 `helper_266_card_toggle_entry.ts` ·
|
||||
지형 · 노선 틀 = `test_346_2_pipeline_cross_page.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
for path in (ROOT, HERE):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from test_346_2_pipeline_cross_page import _fake, _line, _sections # noqa: E402
|
||||
|
||||
from B06_Section.B06_Section_Router import _with_design_choices # noqa: E402
|
||||
from common_util.common_util_surface_sampler import DtmGridSampler # noqa: E402
|
||||
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M # noqa: E402
|
||||
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
CUT_DEPTH_M = 3.0
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r(tmp_path_factory) -> dict:
|
||||
work = tmp_path_factory.mktemp("b439")
|
||||
build = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_266_card_toggle_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_266_card_toggle_entry.js", bundle)
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
npz, nodes = _fake(Path(folder))
|
||||
sampler = DtmGridSampler.from_npz(npz)
|
||||
rows = _sections(_line(nodes), sampler, [])["cross_sections"]
|
||||
middle = len(rows) // 2
|
||||
picked = rows[middle - 1 : middle + 2]
|
||||
sections = [{**s, "plan_z": float(s["center_z"]) - CUT_DEPTH_M} for s in picked]
|
||||
job = {
|
||||
"sections": sections,
|
||||
"target": 1,
|
||||
"mode": f"{picked[1].get('uphill_side') or 'left'}_cut",
|
||||
"standard": STANDARD_CROSS_SECTION,
|
||||
"rockDefault": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
|
||||
}
|
||||
path = Path(folder) / "job.json"
|
||||
path.write_text(json.dumps(job), encoding="utf-8")
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_266_card_toggle.mjs"), str(bundle), str(path)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _check_toggles(case: dict, returned) -> None:
|
||||
steps = {step["tag"]: step for step in case["steps"]}
|
||||
assert steps["open"]["cut"] > 0.5 # 깎기 측점
|
||||
before = steps["open"]
|
||||
for tag in ("two_stage", "ditch_off", "soil"):
|
||||
step = steps[tag]
|
||||
assert case["target"] in step["updated"], tag # 카드 다시 그림 대상
|
||||
assert step["line"] != before["line"], tag # 카드 설계선 바뀜
|
||||
assert step["out"] == returned, tag
|
||||
before = step
|
||||
assert steps["two_stage"]["two_stage_slope"] is True
|
||||
assert steps["ditch_off"]["ditch_enabled"] is False
|
||||
assert steps["soil"]["ground_type"] == "soil"
|
||||
assert steps["soil"]["cut"] != steps["open"]["cut"] # 면적 바뀜
|
||||
|
||||
|
||||
def test_측점_같음_카드_단추마다_설계선_면적_바뀜_돌려줌(r):
|
||||
_check_toggles(r["same"], 3)
|
||||
|
||||
|
||||
def test_측점_어긋남_카드_자료는_고침_돌려줄_값만_null(r):
|
||||
_check_toggles(r["mismatch"], None)
|
||||
|
||||
|
||||
def test_서버_폴백에_카드_선택_실음(r):
|
||||
key = f"{r['server']['at']:.3f}"
|
||||
choice = (r["server"]["choices"] or {}).get(key)
|
||||
assert choice is not None, r["server"]
|
||||
assert choice["ground_type"] == "soil"
|
||||
assert choice["two_stage_slope"] is True
|
||||
assert choice["ditch_choice"] is False
|
||||
|
||||
|
||||
def test_서버_카드_선택이_저장분_위에_얹힘():
|
||||
stored = [{"chainage_m": 40.0, "design": {"ground_type": "ripping_rock", "berm": {"w": 1}}}]
|
||||
rows = _with_design_choices(stored, {"40.000": {"ground_type": "soil"}, "60.123": {"x": 1}})
|
||||
by_key = {round(float(r["chainage_m"]), 3): r["design"] for r in rows} # 서버 읽는 꼴
|
||||
assert by_key[40.0] == {"ground_type": "soil", "berm": {"w": 1}}
|
||||
assert by_key[60.123] == {"x": 1}
|
||||
assert _with_design_choices(stored, None) == stored
|
||||
Reference in New Issue
Block a user