merge(511-B · 511-C 합치기): origin/sub_laptop_4(511-C 유토 배분 E2 · 판 haul 줄) 받음 — 공유 파일 충돌 풂
- Node 입구 design_calc_node.ts 등록부 밖 id = 시험 길 · 단계 · 화면 길(PAGE_IDS) · haul.view - calc_runner = PAGE_IDS(cross.refresh) · VIEW_IDS(haul.view) 둘 다 받음 - calc_sites.py 머리 글 = B-4 cross · 511-C haul(기본 server) 함께 · 묶음 13 줄 - 시험 309_2 · 309_2_browser · 511_b1 = 열린 줄에 haul 더함 · 화면 시험 「지금 서버」 = haul 기본 server 로 1 줄(바꾼 뒤 2 줄) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
* ground_sample(G1) server = ground 단계가 격자 대신 서버 표본(`B05_Profile_Api_TerrainGrid.ts`)
|
||||
* corridor_mesh(D3) server = 3D 미리보기 워커 안 띄움 · 서버 모형만(`B05_Profile_UI_Corridor_Worker_Client.ts`)
|
||||
* quantity · carry · fixed_browser server = 조작 중 서버 창구(511-A · `design_calc_server.ts`)
|
||||
* haul(E2) 기본 server = /haul-plan · both = 브라우저가 배분 모듈을 그때 불러 셈(511-C · `B05_Profile_Api_HaulPlan.ts`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
@@ -18,6 +19,8 @@ export interface CalcSiteGroup {
|
||||
switchable: boolean;
|
||||
fixed: "both" | "server" | "browser" | null;
|
||||
why: string | null;
|
||||
/** 줄마다 까닭 열쇠(번호 → 열쇠) — 없으면 `why`. */
|
||||
whys?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CalcSitesBoard {
|
||||
@@ -63,6 +66,11 @@ export async function calcOnServer(key: string): Promise<boolean> {
|
||||
return (await (opened ?? openCalcSites()))[key] === "server";
|
||||
}
|
||||
|
||||
/** 판에 both 일 때만 브라우저 셈 — 못 받음 · 받기 전 = 아님(기본 server 인 haul 줄). */
|
||||
export async function calcInBrowser(key: string): Promise<boolean> {
|
||||
return (await (opened ?? openCalcSites()))[key] === "both";
|
||||
}
|
||||
|
||||
/** 동기 자리용 — 받기 전 = both. */
|
||||
export const calcOnServerNow = (key: string): boolean => loaded[key] === "server";
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ import { profileRunners, type ProfileInputs } from "./design_calc_node_profile";
|
||||
import { routeRunners, type RouteInputs } from "./design_calc_node_route";
|
||||
import { sectionsRunners, type SectionsInputs } from "./design_calc_node_sections";
|
||||
import { STAGE_IDS, stageRunners, type StagesInputs } from "./design_calc_node_stages";
|
||||
import { TRIAL_IDS, trialRunners, type TrialInputs } from "./design_calc_node_trial";
|
||||
import { plain, TRIAL_IDS, trialRunners, type TrialInputs } from "./design_calc_node_trial";
|
||||
|
||||
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
|
||||
|
||||
@@ -146,6 +146,14 @@ async function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
: null;
|
||||
return massHaul ? massHaulPayload(massHaul) : null;
|
||||
},
|
||||
// 유토 배분 화면 꼴(/haul-plan 창구 · 511-C) — 인자 그대로(args) · 등록부 밖. 시험 길 표에 두면
|
||||
// 브라우저가 끄는 파일(`plain`)이라 배분 모듈이 첫 묶음에 실림 — 그래서 이 입구에만.
|
||||
"haul.view": () =>
|
||||
plain(
|
||||
computeHaulPlan(
|
||||
...((inputs.args?.["haul.view"] ?? []) as Parameters<typeof computeHaulPlan>),
|
||||
),
|
||||
),
|
||||
// 두 벌 — haul_plan = 거르지 않은 수량 정본(B08) · haul_plan_drawing = 잔진동 거른 그림.
|
||||
"haul.plan": () => {
|
||||
need("masshaul.cumulative");
|
||||
@@ -223,7 +231,7 @@ async function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
const unknown = run.filter((id) => !(id in RUNNERS));
|
||||
const results: Record<string, unknown> = {};
|
||||
if (!unknown.length) for (const id of run) results[id] = await need(id);
|
||||
const outside = [...TRIAL_IDS, ...STAGE_IDS, ...PAGE_IDS];
|
||||
const outside = [...TRIAL_IDS, ...STAGE_IDS, ...PAGE_IDS, "haul.view"];
|
||||
const available = Object.keys(RUNNERS).filter((id) => !outside.includes(id));
|
||||
return { results, unknown, available };
|
||||
}
|
||||
|
||||
@@ -185,8 +185,8 @@ export const FACILITY_ROWS: readonly CalcEntry[] = [
|
||||
ts: `${U}common_util_mass_haul_balance.ts#computeHaulPlan`,
|
||||
runner: "node:haul.plan",
|
||||
pair: null,
|
||||
mirror: [],
|
||||
note: "브라우저 번들 밖(/haul-plan 요청) · 브라우저 실행 여부 협의",
|
||||
mirror: ["test_511_c_haul_node"],
|
||||
note: "기본 /haul-plan(늘 떠 있는 Node haul.view) · 판 haul both = 브라우저 dynamic import(511-C)",
|
||||
},
|
||||
/* ── 배수 · 3D · 추천 ────────────────────────────────────────────────── */
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ function groupRows(
|
||||
changed: () => void,
|
||||
): HTMLElement[][] {
|
||||
const server = (): HTMLInputElement => check(group.fixed !== "browser", true);
|
||||
const why = K(`Why_${group.why ?? group.key}`);
|
||||
const why = (no: string): string => K(`Why_${group.whys?.[no] ?? group.why ?? group.key}`);
|
||||
const head = el("span", {
|
||||
children: [el("strong", { text: K(group.key) }), ` (${group.nos.length})`],
|
||||
});
|
||||
@@ -48,7 +48,7 @@ function groupRows(
|
||||
const browser = (): HTMLInputElement => check(group.fixed !== "server", true);
|
||||
return [
|
||||
[head, browser(), server(), el("span")],
|
||||
...group.nos.map((no) => [calcName(no), browser(), server(), el("span", { text: why })]),
|
||||
...group.nos.map((no) => [calcName(no), browser(), server(), el("span", { text: why(no) })]),
|
||||
];
|
||||
}
|
||||
const boxes = [group.key, ...group.nos].map((id, index) => {
|
||||
@@ -70,7 +70,7 @@ function groupRows(
|
||||
calcName(no),
|
||||
boxes[index + 1],
|
||||
server(),
|
||||
el("span", { text: why }),
|
||||
el("span", { text: why(no) }),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* B05_Profile_Api_HaulPlan.ts
|
||||
* 유토 **배분**(평형선·운반거리·장비)을 서버에서 미리 받아 두는 자리.
|
||||
*
|
||||
* 왜 서버인가(2026-09-06 사용자 확정) — 배분 산식은 노하우가 몰린 자리라 브라우저 번들에
|
||||
* 남기지 않는다. 화면은 누가토량까지만 스스로 내고(`common_util_mass_haul`), 그 결과를
|
||||
* 여기로 보내 배분을 받아 쥔다. 계산은 여전히 한 벌이다 — 서버가 같은 TS 를 Node 로 돈다.
|
||||
* 기본은 서버(계산 자리 판 haul = server) — 화면은 누가토량까지만 스스로 내고(`common_util_mass_haul`),
|
||||
* 그 결과를 여기로 보내 배분을 받아 쥔다. 판 haul = both(요구 321 · 511-C)면 배분 모듈을 **그때만**
|
||||
* dynamic import 해 브라우저가 셈(기본값이면 안 받음). 계산은 한 벌 — 서버도 같은 TS 를 Node 로 돈다.
|
||||
*
|
||||
* **조용히 따라오게 한다** — 편집이 멈추면 뒤에서 물어 두므로, 유토곡선 패널을 펼치는
|
||||
* 순간에는 이미 도착해 있다. 늦게 온 응답은 버린다(최신 요청만 채택).
|
||||
@@ -12,11 +12,30 @@
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { HaulPlan } from "@util/common_util_mass_haul_balance";
|
||||
import type { MassHaulResult } from "@util/common_util_mass_haul";
|
||||
import type { EarthworkConversion, HaulEquipmentLimit } from "@util/common_util_mass_haul_types";
|
||||
import { calcInBrowser } from "../A00_Common/design/design_calc_sites";
|
||||
|
||||
/** 서버가 돌려주는 배분 한 벌 — **화면이 쓰는 꼴 그대로**라 그리기 코드가 손대지 않는다.
|
||||
* `import type` 이라 배분 모듈이 번들에 실리지 않는다(빌드에서 지워진다). */
|
||||
* `import type` 이라 배분 모듈이 첫 묶음에 안 실림(판 both 때만 따로 받음). */
|
||||
export type HaulPlanPayload = HaulPlan | null;
|
||||
|
||||
/** 브라우저 셈 문맥 — 서버 `_mass_haul_context` 와 같은 값(sections/context 가 같은 함수로 냄). */
|
||||
export interface HaulPlanContext {
|
||||
conversion: EarthworkConversion;
|
||||
haulLimits?: HaulEquipmentLimit[];
|
||||
}
|
||||
|
||||
/** 판 haul = both — 서버 /haul-plan(`_run_view`)과 같은 인자로 같은 함수.
|
||||
* ponytail: 채집석 · 구조물 잔토는 서버 `haul_inputs_for` 가 늘 빈 값이라 null — 값 내기 시작하면 sections/context 로 실어 옴. */
|
||||
async function browserHaulPlan(result: unknown, ctx: HaulPlanContext): Promise<HaulPlanPayload> {
|
||||
const { computeHaulPlan } = await import("@util/common_util_mass_haul_balance");
|
||||
return computeHaulPlan(result as MassHaulResult, ctx.haulLimits, {
|
||||
conversion: ctx.conversion,
|
||||
drawing: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** 편집이 멈춘 것으로 볼 시간(ms). 계획고를 연속으로 누르는 동안은 안 보낸다. */
|
||||
const SETTLE_MS = 400;
|
||||
/** 배분 계산 대기 상한 — Node 실행 200ms 대라 넉넉히 잡는다. */
|
||||
@@ -85,8 +104,8 @@ async function postHaulPlan(
|
||||
}
|
||||
|
||||
export interface HaulPlanPrefetch {
|
||||
/** 새 누가토량 결과가 나왔음을 알린다 — 잠잠해지면 서버에 물어본다. */
|
||||
schedule: (result: unknown) => void;
|
||||
/** 새 누가토량 결과가 나왔음을 알린다 — 잠잠해지면 서버에 물어본다(판 both = 브라우저 셈). */
|
||||
schedule: (result: unknown, ctx?: HaulPlanContext | null) => void;
|
||||
/** 지금 쥐고 있는 배분. 아직 못 받았으면 null. */
|
||||
current: () => HaulPlanPayload;
|
||||
/** 화면을 떠날 때 예약을 지운다. */
|
||||
@@ -106,6 +125,7 @@ export function createHaulPlanPrefetch(
|
||||
let sequence = 0;
|
||||
let plan: HaulPlanPayload = null;
|
||||
let pending: unknown = null;
|
||||
let pendingCtx: HaulPlanContext | null = null;
|
||||
/** 마지막으로 보낸 입력의 표식 — 같은 값이면 다시 묻지 않는다. */
|
||||
let sentKey: string | null = null;
|
||||
/** 받은 값을 화면에 반영하려고 다시 그리는 중 — 그 그리기가 거는 요청은 무시한다. */
|
||||
@@ -116,10 +136,18 @@ export function createHaulPlanPrefetch(
|
||||
if (seq === sequence) sentKey = null;
|
||||
}
|
||||
|
||||
async function send(result: unknown, seq: number, key: string): Promise<void> {
|
||||
async function send(
|
||||
result: unknown,
|
||||
ctx: HaulPlanContext | null,
|
||||
seq: number,
|
||||
key: string,
|
||||
): Promise<void> {
|
||||
const route = routeId();
|
||||
if (!route) return forget(seq);
|
||||
const got = await postHaulPlan(projectId, route, result, key);
|
||||
const got =
|
||||
ctx && (await calcInBrowser("haul"))
|
||||
? await browserHaulPlan(result, ctx).catch(() => undefined)
|
||||
: await postHaulPlan(projectId, route, result, key);
|
||||
if (got === undefined) return forget(seq);
|
||||
// 늦게 온 응답은 버린다 — 그 사이 사용자가 계획고를 더 만졌을 수 있다.
|
||||
if (seq !== sequence) return;
|
||||
@@ -136,12 +164,13 @@ export function createHaulPlanPrefetch(
|
||||
}
|
||||
|
||||
return {
|
||||
schedule(result) {
|
||||
schedule(result, ctx) {
|
||||
if (applying) return; // 응답을 반영하려고 다시 그리는 중이다.
|
||||
const key = keyOf(result);
|
||||
if (key === sentKey) return; // 같은 값 — 응답이 부른 재그리기가 되돌아온 것이다.
|
||||
sentKey = key;
|
||||
pending = result;
|
||||
pendingCtx = ctx ?? null;
|
||||
sequence += 1;
|
||||
const seq = sequence;
|
||||
window.clearTimeout(timer);
|
||||
@@ -149,7 +178,7 @@ export function createHaulPlanPrefetch(
|
||||
const route = routeId();
|
||||
const known = route ? kept.get(keptKey(projectId, route, key)) : undefined;
|
||||
if (known !== undefined) return void (plan = known);
|
||||
timer = window.setTimeout(() => void send(pending, seq, key), SETTLE_MS);
|
||||
timer = window.setTimeout(() => void send(pending, pendingCtx, seq, key), SETTLE_MS);
|
||||
},
|
||||
current: () => plan,
|
||||
dispose() {
|
||||
|
||||
@@ -309,7 +309,7 @@ export function createRouteMassHaulPanel(
|
||||
// 배분 선반입도 **여기서** 건다 — 접힘 여부와 무관하게 지나는 자리다(2026-09-06).
|
||||
// 아래 `draw` 안에 두었더니 접힘 가드 뒤라 한 번도 안 불렸고, 그래서 「편집이 멈추면
|
||||
// 조용히 받아 둔다」가 성립하지 않았다(펼친 뒤에야 첫 요청이 나갔다).
|
||||
haulPrefetch?.schedule(picked.result);
|
||||
haulPrefetch?.schedule(picked.result, context);
|
||||
onSummary(badgeValuesFrom(picked.result));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
멈추면 그 결과를 여기로 보내 배분을 받아 쥔다. 그래서 유토곡선 패널을 펼치는 순간이
|
||||
즉시가 된다(미리 받아 뒀으므로).
|
||||
|
||||
**계산은 한 벌이다** — 화면이 쓰던 TS 를 Node 진입점(`B06_Section_Server_Calc_Node.ts`)으로
|
||||
그대로 돌린다(CLAUDE.md 5장). 파이썬으로 옮기면 저장 정본과 화면 값이 갈린다.
|
||||
**계산은 한 벌이다** — 화면이 쓰던 TS(`computeHaulPlan`)를 늘 떠 있는 Node 시험 길 `haul.view`
|
||||
(`design_calc_node_trial.ts` · 511-C)로 그대로 돌린다(CLAUDE.md 5장). 파이썬으로 옮기면 저장 정본과
|
||||
화면 값이 갈린다.
|
||||
|
||||
정본을 만들지 않는다 — 이 응답은 **표시 전용**이다. 저장 정본은 [저장]·[확정] 뒤
|
||||
`recompute_server_side` 가 따로 낸다.
|
||||
@@ -28,21 +29,19 @@ from fastapi.responses import JSONResponse
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import (
|
||||
BUNDLE,
|
||||
_mass_haul_context,
|
||||
conversion_factors_for,
|
||||
haul_inputs_for,
|
||||
haul_limits_for,
|
||||
)
|
||||
from common_util.common_util_calc_runner import run_calc_served
|
||||
from common_util.common_util_derived_store import read_derived, write_derived
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import run_with_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
|
||||
|
||||
_NPM_SCRIPT = "build:server-calc"
|
||||
# 측점 수 상한 — 정상 노선은 수백 곳이다. 그보다 크면 비정상 요청으로 본다.
|
||||
_MAX_POINTS = 5000
|
||||
# 열쇠(누가토량 결과 + 계수 · 장비 경계) → 배분 — 같은 열쇠면 Node 를 다시 안 띄움
|
||||
@@ -64,6 +63,26 @@ def _keep(key: str, plan: Any) -> None:
|
||||
_KEPT.popitem(last=False)
|
||||
|
||||
|
||||
def _run_view(result: dict[str, Any], context: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""화면 꼴 배분(그림용 · 잔진동 거름) — 옛 server-calc `haul_plan_for` 갈래와 같은 인자."""
|
||||
options = {
|
||||
key: context.get(key)
|
||||
for key in (
|
||||
"collected_stone_deduction_m3",
|
||||
"collected_stone_by_ground_m3",
|
||||
"collected_stone_ground_unknown_m3",
|
||||
"structure_spoil_m3",
|
||||
"structure_spoil_points",
|
||||
)
|
||||
}
|
||||
options["conversion"] = context.get("earthwork_conversion")
|
||||
options["drawing"] = True
|
||||
args = [result, context.get("haul_equipment_limits"), options]
|
||||
answer = run_calc_served(["haul.view"], {"args": {"haul.view": args}})
|
||||
results = answer.get("results") if isinstance(answer, dict) else None
|
||||
return {"haul_plan": results["haul.view"]} if isinstance(results, dict) else None
|
||||
|
||||
|
||||
@router.post("/{project_id}/sections/{route_id}/haul-plan", response_model=None)
|
||||
async def compute_haul_plan(
|
||||
project_id: UUID,
|
||||
@@ -114,12 +133,7 @@ async def compute_haul_plan(
|
||||
_keep(key, stored["value"])
|
||||
return JSONResponse(content={"status": "success", "haul_plan": stored["value"]})
|
||||
try:
|
||||
output = await asyncio.to_thread(
|
||||
run_bundle_json,
|
||||
BUNDLE,
|
||||
_NPM_SCRIPT,
|
||||
{"haul_plan_for": result, "context": context},
|
||||
)
|
||||
output = await asyncio.to_thread(_run_view, result, context)
|
||||
except Exception:
|
||||
logger.exception("유토 배분 계산 실패: project_id=%s route_id=%s", project_id, route_id)
|
||||
return JSONResponse(
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
**보안**이다 — 저장 경로가 브라우저 계산이면 유토 배분·운반거리 코드가 번들에 남아야
|
||||
해서 화면에서 안 그려도 뺄 수 없다. 대가는 저장 대기 850ms → 약 980ms(Node 131ms).
|
||||
|
||||
**계산을 다시 짜지 않는다.** 화면이 쓰는 TS 를 Node 진입점(`B06_Section_Server_Calc_Node.ts`)
|
||||
으로 감싸 그대로 돌린다. 파이썬으로 포팅하면 같은 기하가 두 벌이 되어 「그림은 이런데
|
||||
**계산을 다시 짜지 않는다.** 화면이 쓰는 TS 를 늘 떠 있는 Node(`design_calc_node.ts` ·
|
||||
`structure.layouts` · `masshaul.cumulative` · `haul.plan` · 511-C)로 그대로 돌린다.
|
||||
파이썬으로 포팅하면 같은 기하가 두 벌이 되어 「그림은 이런데
|
||||
수량은 저렇다」가 생긴다.
|
||||
|
||||
실패는 던진다 — 부르는 쪽([저장] · [확정])이 `recomputed: false` 로 화면에 알린다(388-2).
|
||||
@@ -38,7 +39,9 @@ from B06_Section.B06_Section_Repository import (
|
||||
merge_longitudinal_section_data,
|
||||
)
|
||||
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_design_batches
|
||||
from common_util.common_util_node_bundle import bundle_stale, run_bundle_json
|
||||
from common_util.common_util_calc_runner import BUNDLE as NODE_BUNDLE
|
||||
from common_util.common_util_calc_runner import run_calc, run_calc_served
|
||||
from common_util.common_util_node_bundle import bundle_stale
|
||||
from common_util.common_util_project_settings import (
|
||||
haul_equipment_limits,
|
||||
mixed_conversion_factors,
|
||||
@@ -56,6 +59,7 @@ from config.config_system import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
# 옛 server-calc 묶음 — 정본 길은 늘 떠 있는 Node(`_run_node`) · 이 묶음은 시험 대조만(B-6 걷음 몫)
|
||||
BUNDLE = ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js"
|
||||
# 곡선부 확폭 입력 칸(파이썬 `curve_widening_args` · TS `SaveSection`)
|
||||
CURVE_KEYS = ("plan_radius_m", "curve_outer_side", "curve_widening_m")
|
||||
@@ -89,16 +93,37 @@ def _node_fingerprint(node_input: dict[str, Any]) -> str:
|
||||
json.dumps(node_input, sort_keys=True, default=str).encode("utf-8"),
|
||||
usedforsecurity=False,
|
||||
)
|
||||
digest.update(str(BUNDLE.stat().st_mtime_ns if BUNDLE.is_file() else 0).encode())
|
||||
digest.update(str(NODE_BUNDLE.stat().st_mtime_ns if NODE_BUNDLE.is_file() else 0).encode())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
_NODE_IDS = ("structure.layouts", "masshaul.cumulative", "haul.plan")
|
||||
|
||||
|
||||
def _run_node(node_input: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""늘 떠 있는 Node(못 부르면 새 프로세스) 셋 → 옛 server-calc 답 꼴
|
||||
`{areas, mass_haul, extra_walls}`."""
|
||||
answer = run_calc_served(_NODE_IDS, node_input)
|
||||
results = answer.get("results") if isinstance(answer, dict) else None
|
||||
if not isinstance(results, dict):
|
||||
results = run_calc(_NODE_IDS, node_input)
|
||||
if not isinstance(results, dict):
|
||||
return None
|
||||
layouts = results.get("structure.layouts") or {}
|
||||
cumulative = results.get("masshaul.cumulative")
|
||||
return {
|
||||
"areas": layouts.get("areas"),
|
||||
"mass_haul": {**cumulative, **(results.get("haul.plan") or {})} if cumulative else None,
|
||||
"extra_walls": layouts.get("extra_walls"),
|
||||
}
|
||||
|
||||
|
||||
def _node_output(node_input: dict[str, Any]) -> Any:
|
||||
"""같은 입력이면 지난 결과 · 아니면 Node(면적 · 유토곡선)를 돌려 기억한다."""
|
||||
key = _node_fingerprint(node_input)
|
||||
if key in _NODE_CACHE and not bundle_stale(BUNDLE):
|
||||
if key in _NODE_CACHE and not bundle_stale(NODE_BUNDLE):
|
||||
return _NODE_CACHE[key]
|
||||
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, node_input)
|
||||
output = _run_node(node_input)
|
||||
if output is None: # 실패는 기억하지 않음 — 다음 [저장]이 다시 셈
|
||||
return None
|
||||
if len(_NODE_CACHE) >= _NODE_CACHE_LIMIT:
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
입력 꼴 = 입구 머리(`detail` · `route_points` · `context` · `excavation`).
|
||||
`run_calc_served` = 계산 자리 「서버」 창구(511-A · `B06_Section_Router_Calc`) —
|
||||
늘 떠 있는 Node 하나(`--serve` · 한 줄 요청 · 한 줄 답) · 죽음 · 묶음 다시 지음 = 다시 띄움 ·
|
||||
시험 길 `TRIAL_IDS` · 묶음 단계 `STAGE_IDS`(511-B) · 화면 길 `PAGE_IDS`(B-4) 도 받음.
|
||||
시험 길 `TRIAL_IDS` · 묶음 단계 `STAGE_IDS`(511-B) · 화면 길 `PAGE_IDS`(B-4) ·
|
||||
유토 배분 화면 꼴 `VIEW_IDS`(511-C) 도 받음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -76,6 +77,8 @@ STAGE_IDS = frozenset(
|
||||
)
|
||||
# 화면 길(`design_calc_node_cross.ts` `PAGE_IDS` · 511-B B-4) — cross = server 저장 노선 다시 셈
|
||||
PAGE_IDS = frozenset({"cross.refresh"})
|
||||
# 유토 배분 화면 꼴(`design_calc_node.ts` `haul.view` · /haul-plan 창구 · 511-C)
|
||||
VIEW_IDS = frozenset({"haul.view"})
|
||||
SERVE_TIMEOUT_S = 60
|
||||
|
||||
|
||||
@@ -149,7 +152,9 @@ atexit.register(_SERVED.stop)
|
||||
|
||||
def run_calc_served(ids: Sequence[str], inputs: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""창구 길 — `{results, ms(Node 셈), wall_ms(파이썬 ↔ Node)}` · `{error}` · 못 돌림 None."""
|
||||
unknown = [item for item in ids if item not in NODE_IDS | TRIAL_IDS | STAGE_IDS | PAGE_IDS]
|
||||
unknown = [
|
||||
item for item in ids if item not in NODE_IDS | TRIAL_IDS | STAGE_IDS | PAGE_IDS | VIEW_IDS
|
||||
]
|
||||
if unknown:
|
||||
raise ValueError(f"서버 계산 id 아님: {', '.join(unknown)}")
|
||||
t0 = time.perf_counter()
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
both = 조작 중 브라우저 셈 + [저장] 서버 정본(지금 등록부 자리) · server = 조작 중에도 서버 길
|
||||
(G1 · D3 = 504-2 · ③ 토량 · ④ 이월 · ⑧ 고정 브라우저(시험용 · fixed 는 정본 칸 표시) = 511-A 창구 ·
|
||||
cross = 511-B B-4 묶음 cross 단계 · 저장 노선 다시 셈).
|
||||
판이 없거나 깨지면 기본값(전부 both = 등록부 그대로). [저장] · [확정] 서버 정본 길은 판을 안 읽음.
|
||||
묶음 12 줄 = 등록부(`design_calc_registry*.ts`) # 전부 · 같은지 시험 `test_309_2_calc_sites.py`.
|
||||
판이 없거나 깨지면 기본값(줄 `default` · 없으면 both = 등록부 그대로 · haul = server).
|
||||
haul(E2 · 511-C · 요구 321) both = 브라우저가 배분 모듈을 그때만 불러 셈 · server = /haul-plan.
|
||||
[저장] · [확정] 서버 정본 길은 판을 안 읽음.
|
||||
묶음 13 줄 = 등록부(`design_calc_registry*.ts`) # 전부 · 같은지 시험 `test_309_2_calc_sites.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -67,12 +69,23 @@ GROUPS: tuple[dict[str, Any], ...] = (
|
||||
},
|
||||
# 폼 기본값 도구(B04 · B08 파이썬 · 고를 것 없음)
|
||||
{"key": "form_default", "nos": ["F9"], "switchable": False, "fixed": "both", "why": None},
|
||||
# 유토 배분 — 기본 server(지금 /haul-plan 그대로) · both = 브라우저 셈(511-C)
|
||||
{
|
||||
"key": "haul",
|
||||
"nos": ["E2"],
|
||||
"switchable": True,
|
||||
"fixed": None,
|
||||
"why": "haul",
|
||||
"default": "server",
|
||||
},
|
||||
# 서버만 — 줄마다 까닭(`whys` · 511-C)
|
||||
{
|
||||
"key": "fixed_server",
|
||||
"nos": ["E2", "G3", "D1", "D2", "N1"],
|
||||
"nos": ["G3", "D1", "D2", "N1"],
|
||||
"switchable": False,
|
||||
"fixed": "server",
|
||||
"why": "server",
|
||||
"whys": {n: f"server_{n}" for n in ("G3", "D1", "D2", "N1")},
|
||||
},
|
||||
{
|
||||
"key": "fixed_browser",
|
||||
@@ -86,17 +99,18 @@ GROUPS: tuple[dict[str, Any], ...] = (
|
||||
ORDER = tuple(g["key"] for g in GROUPS if g["switchable"])
|
||||
SWITCHABLE = frozenset(ORDER)
|
||||
LOCKED = frozenset(g["key"] for g in GROUPS if not g["switchable"])
|
||||
DEFAULTS = {g["key"]: g.get("default", "both") for g in GROUPS if g["switchable"]}
|
||||
|
||||
|
||||
def read_sites() -> dict[str, Any]:
|
||||
"""판 — 없음 · 깨짐 · 모르는 값 = 그 줄 기본값(both)."""
|
||||
"""판 — 없음 · 깨짐 · 모르는 값 = 그 줄 기본값(`DEFAULTS`)."""
|
||||
try:
|
||||
raw = json.loads(PATH.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
raw = {}
|
||||
stored = raw.get("sites") if isinstance(raw, dict) else None
|
||||
stored = stored if isinstance(stored, dict) else {}
|
||||
sites = {k: stored.get(k) if stored.get(k) in VALUES else "both" for k in ORDER}
|
||||
sites = {k: stored.get(k) if stored.get(k) in VALUES else DEFAULTS[k] for k in ORDER}
|
||||
version = raw.get("version") if isinstance(raw, dict) else None
|
||||
return {"version": version if isinstance(version, int) else 0, "sites": sites}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -56,7 +56,7 @@ def test_묶음_여덟_줄_등록부_전부():
|
||||
sites = _registry_sites()
|
||||
assert len(sites) == 45
|
||||
nos = [no for group in calc_sites.GROUPS for no in group["nos"]]
|
||||
assert len(calc_sites.GROUPS) == 12 # 511-B 단계 다섯 · F9 폼 기본값
|
||||
assert len(calc_sites.GROUPS) == 13 # 511-B 단계 다섯 · F9 폼 기본값 · 511-C haul
|
||||
assert len(nos) == len(set(nos)) and set(nos) == set(sites)
|
||||
assert calc_sites.SWITCHABLE == {
|
||||
"ground_sample",
|
||||
@@ -67,13 +67,14 @@ def test_묶음_여덟_줄_등록부_전부():
|
||||
"stations",
|
||||
"profile", # 511-B B-3 엶
|
||||
"cross", # 511-B B-4 끝에 엶
|
||||
"haul", # 511-C E2 엶(기본 server)
|
||||
"fixed_browser",
|
||||
} # 511-A 셋 더 · 511-B B-2 stations · B-5 route
|
||||
for group in calc_sites.GROUPS:
|
||||
got = {sites[no] for no in group["nos"]}
|
||||
if group["fixed"] == "browser":
|
||||
assert got == {"browser_only"}, group["key"]
|
||||
elif group["fixed"] == "server":
|
||||
elif group["fixed"] == "server" or group.get("default") == "server":
|
||||
assert got <= {"server_canon", "server_only"}, group["key"]
|
||||
else:
|
||||
assert got == {"browser_now"}, group["key"]
|
||||
@@ -83,17 +84,19 @@ def test_묶음_여덟_줄_등록부_전부():
|
||||
|
||||
def test_커밋한_판_기본값_없음_깨짐(tmp_path, monkeypatch):
|
||||
committed = json.loads(calc_sites.PATH.read_text(encoding="utf-8"))
|
||||
assert committed["sites"] == dict.fromkeys(calc_sites.SWITCHABLE, "both")
|
||||
assert set(calc_sites.read_sites()["sites"].values()) == {"both"}
|
||||
assert calc_sites.DEFAULTS == {**dict.fromkeys(calc_sites.SWITCHABLE, "both"), "haul": "server"}
|
||||
assert set(committed["sites"].items()) <= set(calc_sites.DEFAULTS.items())
|
||||
assert calc_sites.read_sites()["sites"] == calc_sites.DEFAULTS # 판 파일 haul 없음 = server
|
||||
monkeypatch.setattr(calc_sites, "PATH", tmp_path / "none.json")
|
||||
assert calc_sites.read_sites() == {
|
||||
"version": 0,
|
||||
"sites": dict.fromkeys(calc_sites.SWITCHABLE, "both"),
|
||||
}
|
||||
assert calc_sites.read_sites() == {"version": 0, "sites": calc_sites.DEFAULTS}
|
||||
broken = tmp_path / "broken.json"
|
||||
broken.write_text('{"sites": {"ground_sample": "server", "corridor_mesh": "x"', "utf-8")
|
||||
monkeypatch.setattr(calc_sites, "PATH", broken)
|
||||
assert set(calc_sites.read_sites()["sites"].values()) == {"both"}
|
||||
assert calc_sites.read_sites()["sites"] == calc_sites.DEFAULTS
|
||||
odd = tmp_path / "odd.json"
|
||||
odd.write_text('{"sites": {"haul": "x", "quantity": "server"}}', "utf-8")
|
||||
monkeypatch.setattr(calc_sites, "PATH", odd)
|
||||
assert calc_sites.read_sites()["sites"] == {**calc_sites.DEFAULTS, "quantity": "server"}
|
||||
|
||||
|
||||
class _Cursor:
|
||||
@@ -167,7 +170,7 @@ def test_잠금_모르는_줄_400_감사_한_줄(client):
|
||||
done = http.put(url, json={"sites": {"ground_sample": "server"}})
|
||||
assert done.status_code == 200 and done.json()["changed"] == ["ground_sample"]
|
||||
saved = json.loads(board.read_text("utf-8"))
|
||||
sites = {**dict.fromkeys(calc_sites.SWITCHABLE, "both"), "ground_sample": "server"}
|
||||
sites = {**calc_sites.DEFAULTS, "ground_sample": "server"}
|
||||
assert saved == {"version": 2, "sites": sites}
|
||||
inserts = [args for sql, args in log if "system_audit_logs" in sql]
|
||||
assert len(inserts) == 1
|
||||
|
||||
@@ -148,7 +148,7 @@ def test_시스템_관리자_카드_창_G1_서버_길(browser):
|
||||
)
|
||||
heads = [row for row in rows if row["head"]]
|
||||
calcs = [row for row in rows if not row["head"]]
|
||||
assert len(heads) == 12 and len(calcs) == 45 # 511-B 단계 다섯 줄 · F9 폼 기본값
|
||||
assert len(heads) == 13 and len(calcs) == 45 # 511-B 단계 다섯 줄 · F9 · 511-C haul
|
||||
whys = {row["name"].split(" · ")[0]: row["why"] for row in calcs}
|
||||
for no, n in {"F2": 5}.items():
|
||||
assert whys[no] == f"짝 모으는 중 · B-{n}", no
|
||||
@@ -159,11 +159,12 @@ def test_시스템_관리자_카드_창_G1_서버_길(browser):
|
||||
opened += ["R1", "R3", "R5", "R6", "R7", "F3"] # 511-B B-5 노선 · B-2 측점
|
||||
opened += ["P1", "P2", "P3", "P4", "P8"] # 511-B B-3 종단 엶
|
||||
opened += ["G2", "G4", "C1", "C2", "C3", "C5", "C6", "F4", "F5", "F6"] # 511-B B-4
|
||||
opened += ["E2"] # 511-C 유토 배분 엶(기본 server = 체크 꺼짐)
|
||||
opened += ["R2", "R4", "R8", "P7", "C7", "D4"] # ⑧ 시험용
|
||||
assert [name.split(" · ")[0] for name in editable] == opened
|
||||
assert all(row["name"].split(" · ")[1] for row in calcs) # 이름 = 등록부 label
|
||||
assert all(row["boxes"][1][1] for row in calcs) # 서버 정본 칸은 늘 잠금
|
||||
assert "지금 서버 0 줄" in dialog.inner_text()
|
||||
assert "지금 서버 1 줄" in dialog.inner_text() # haul 기본 server(511-C)
|
||||
SHOTS.mkdir(parents=True, exist_ok=True)
|
||||
dialog.screenshot(path=str(SHOTS / "light.png"))
|
||||
page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')")
|
||||
@@ -171,7 +172,7 @@ def test_시스템_관리자_카드_창_G1_서버_길(browser):
|
||||
page.evaluate("document.documentElement.setAttribute('data-theme', 'light')")
|
||||
|
||||
page.click("input[data-calc-site='ground_sample']")
|
||||
assert "지금 서버 1 줄" in dialog.inner_text()
|
||||
assert "지금 서버 2 줄" in dialog.inner_text()
|
||||
assert not page.is_checked("input[data-calc-no='G1']") # 머리 따라 줄도 꺼짐
|
||||
page.locator(".b01-dashboard__calc-sites .ui-modal__actions button").click()
|
||||
page.wait_for_selector(".b01-dashboard__calc-sites", state="detached", timeout=30000)
|
||||
|
||||
@@ -23,15 +23,15 @@ def _patch(monkeypatch, outputs: list) -> list:
|
||||
async def _none(_project_id):
|
||||
return {}
|
||||
|
||||
def _run(_bundle, _script, payload):
|
||||
calls.append(payload)
|
||||
def _run(result, context):
|
||||
calls.append(result)
|
||||
return outputs.pop(0)
|
||||
|
||||
monkeypatch.setattr(router, "haul_inputs_for", _none)
|
||||
monkeypatch.setattr(router, "conversion_factors_for", _none)
|
||||
monkeypatch.setattr(router, "haul_limits_for", _none)
|
||||
monkeypatch.setattr(router, "_mass_haul_context", lambda *_: {"limits": []})
|
||||
monkeypatch.setattr(router, "run_bundle_json", _run)
|
||||
monkeypatch.setattr(router, "_run_view", _run)
|
||||
router._KEPT.clear()
|
||||
return calls
|
||||
|
||||
|
||||
@@ -48,12 +48,12 @@ def node(monkeypatch, tmp_path):
|
||||
bundle.write_text("//", encoding="utf-8")
|
||||
calls: list = []
|
||||
|
||||
def fake_run(path, script, payload):
|
||||
def fake_run(payload):
|
||||
calls.append(payload)
|
||||
return {"areas": [], "mass_haul": {"total": len(calls)}}
|
||||
|
||||
monkeypatch.setattr(prebuild, "BUNDLE", bundle)
|
||||
monkeypatch.setattr(prebuild, "run_bundle_json", fake_run)
|
||||
monkeypatch.setattr(prebuild, "NODE_BUNDLE", bundle)
|
||||
monkeypatch.setattr(prebuild, "_run_node", fake_run)
|
||||
monkeypatch.setattr(prebuild, "bundle_stale", lambda path: False)
|
||||
monkeypatch.setattr(prebuild, "_NODE_CACHE", {})
|
||||
return SimpleNamespace(calls=calls, bundle=bundle)
|
||||
@@ -105,7 +105,7 @@ def test_TS_가_번들보다_새로우면_다시(node, monkeypatch):
|
||||
|
||||
|
||||
def test_실패는_기억_안_함(node, monkeypatch):
|
||||
monkeypatch.setattr(prebuild, "run_bundle_json", lambda *a: node.calls.append(a) or None)
|
||||
monkeypatch.setattr(prebuild, "_run_node", lambda *a: node.calls.append(a) or None)
|
||||
assert prebuild._node_output(_input()) is None
|
||||
assert prebuild._node_output(_input()) is None
|
||||
assert len(node.calls) == 2
|
||||
@@ -241,13 +241,13 @@ def test_같은_입력_두_번이면_Node_0_쓰기_0(node, monkeypatch, tmp_path
|
||||
written_haul.append(data_patch)
|
||||
stored["data"]["mass_haul"] = data_patch["mass_haul"]
|
||||
|
||||
def fake_run(path, script, payload):
|
||||
def fake_run(payload):
|
||||
node.calls.append(payload)
|
||||
# Node 에 간 입력이 곧 지문 입력 — 다른 입력 길 없음
|
||||
assert set(payload) == {"detail", "context"}
|
||||
return {"areas": [{"chainage_m": 0.0, "cut_area_m2": 5.0}], "mass_haul": {"total": 9.0}}
|
||||
|
||||
monkeypatch.setattr(prebuild, "run_bundle_json", fake_run)
|
||||
monkeypatch.setattr(prebuild, "_run_node", fake_run)
|
||||
monkeypatch.setattr(b06_router, "get_section_detail", detail)
|
||||
monkeypatch.setattr(prebuild, "run_with_connection", run_with)
|
||||
monkeypatch.setattr(prebuild, "resolve_stored_project_path", lambda path: str(tmp_path))
|
||||
|
||||
@@ -266,7 +266,16 @@ def test_서버_쪽_코드_브라우저_밖(loaded, reach) -> None:
|
||||
if row["site"] in ("server_canon", "server_only") and row["ts"]
|
||||
]
|
||||
assert server # 유토 배분(common_util_mass_haul_balance)
|
||||
assert [path for path in server if path in browser] == []
|
||||
# 511-C(요구 321) — 유토 배분은 판 haul = both 때만 dynamic import(첫 묶음 밖) · 정적 import 0
|
||||
lazy = "common_util/common_util_mass_haul_balance.ts"
|
||||
assert [path for path in server if path in browser] == [lazy]
|
||||
static = re.compile(
|
||||
r'^import\s+(?!type\s)[^;]*?from\s+"@util/common_util_mass_haul_balance"', re.M
|
||||
)
|
||||
ts_files = [p for p in browser if p.endswith(".ts")]
|
||||
assert [p for p in ts_files if static.search((ROOT / p).read_text(encoding="utf-8"))] == []
|
||||
haul_api = (ROOT / "B05_Profile" / "B05_Profile_Api_HaulPlan.ts").read_text(encoding="utf-8")
|
||||
assert 'await import("@util/common_util_mass_haul_balance")' in haul_api
|
||||
assert NODE_ENTRY not in browser
|
||||
assert reach[REGISTRY] == sorted(REGISTRY_PARTS) # 순수 자료 — 제 몫 파일만(나눔 · 함수 안 끔)
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ def test_판_다섯_줄_잠금_기본값_그대로():
|
||||
"stations", # B-2 끝 = 엶(511-B B-2)
|
||||
"profile", # B-3 엶(511-B · test_511_b3_profile_node)
|
||||
"cross",
|
||||
"haul", # 511-C E2 엶(기본 server)
|
||||
"fixed_browser",
|
||||
}
|
||||
committed = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""511-C (요구 320-4) — 유토 배분 E2 = 늘 떠 있는 Node(TS 한 벌) · 옛 server-calc 묶음 값과 0 차.
|
||||
|
||||
못박는 것
|
||||
- /haul-plan 창구 새 길(`_run_view` → 시험 길 `haul.view`) = 옛 server-calc `haul_plan_for` 갈래
|
||||
= 브라우저 자리 함수(`computeHaulPlan` 바로 부름) — 프로젝트 일곱
|
||||
(저장 유토곡선 · DB 에서 한 번 뜬 `fixtures/haul_511_c.json`)
|
||||
× 문맥(기본 · 장비 경계 없음 · 구조물 잔토 · 채집석) × 그림 · 수량.
|
||||
- 끝 경우 — 측점 하나(배분 없음 null) · 둘 · 전부 0 · 전부 절토 · 전부 성토.
|
||||
- [저장] 정본 새 길(`Server_Calc_Prebuild._node_output` → 늘 떠 있는 Node 셋) = 옛 server-calc
|
||||
`{detail, context}` 답(면적 · 다단 벽 · 유토곡선 + 배분 두 벌) — golden_472 · 문맥 셋.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_511_c_haul_node.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B06_Section import B06_Section_Router_HaulPlan as haul_router # noqa: E402
|
||||
from B06_Section import B06_Section_Server_Calc_Prebuild as prebuild # noqa: E402
|
||||
from common_util.common_util_node_bundle import run_bundle_json # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
shutil.which("node") is None or not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="node · 프론트엔드 의존성 없음",
|
||||
)
|
||||
|
||||
PROJECTS = json.loads((HERE / "fixtures" / "haul_511_c.json").read_text(encoding="utf-8"))
|
||||
GOLDEN = HERE / "fixtures" / "golden_472"
|
||||
SPOIL_POINTS = [
|
||||
{"chainage_m": 40.0, "spoil_m3": 35.5, "ground_type": "soil"},
|
||||
{"chainage_m": 260.0, "spoil_m3": 12.25, "ground_type": "ripping_rock"},
|
||||
{"chainage_m": 600.0, "spoil_m3": 8.0, "ground_type": None},
|
||||
]
|
||||
|
||||
|
||||
def _result(payload: dict) -> dict:
|
||||
"""저장 유토곡선 → 누가토량 결과 꼴(최소 · 최대 누계는 측점에서)."""
|
||||
cums = [p["cumulative_volume_m3"] for p in payload["points"]]
|
||||
return {
|
||||
**payload,
|
||||
"min_cumulative_m3": min(cums, default=0.0),
|
||||
"max_cumulative_m3": max(cums, default=0.0),
|
||||
}
|
||||
|
||||
|
||||
def _contexts() -> dict[str, dict]:
|
||||
base = prebuild._mass_haul_context()
|
||||
return {
|
||||
"기본": base,
|
||||
"장비 경계 없음": {**base, "haul_equipment_limits": None},
|
||||
"구조물 잔토": {
|
||||
**base,
|
||||
"structure_spoil_points": SPOIL_POINTS,
|
||||
"structure_spoil_m3": 55.75,
|
||||
},
|
||||
"채집석": {
|
||||
**base,
|
||||
"collected_stone_deduction_m3": 120.0,
|
||||
"collected_stone_by_ground_m3": {"soil": 40.0, "blasting_rock": 30.0},
|
||||
"collected_stone_ground_unknown_m3": 5.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _options(context: dict, drawing: bool) -> dict:
|
||||
keys = (
|
||||
"collected_stone_deduction_m3",
|
||||
"collected_stone_by_ground_m3",
|
||||
"collected_stone_ground_unknown_m3",
|
||||
"structure_spoil_m3",
|
||||
"structure_spoil_points",
|
||||
)
|
||||
return {
|
||||
**{k: context.get(k) for k in keys},
|
||||
"conversion": context.get("earthwork_conversion"),
|
||||
"drawing": drawing,
|
||||
}
|
||||
|
||||
|
||||
def _browser(tmp_path: Path, calls: list[dict]) -> list:
|
||||
job = tmp_path / "job.json"
|
||||
job.write_text(json.dumps(calls, ensure_ascii=False), encoding="utf-8")
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_511_a_mirror.cjs"), str(job)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _old_view(result: dict, context: dict):
|
||||
out = run_bundle_json(
|
||||
prebuild.BUNDLE, prebuild._NPM_SCRIPT, {"haul_plan_for": result, "context": context}
|
||||
)
|
||||
assert isinstance(out, dict), out
|
||||
return out["haul_plan"]
|
||||
|
||||
|
||||
def _edge_results() -> dict[str, dict]:
|
||||
conv = PROJECTS["483"]["conversion"]
|
||||
|
||||
def make(nets: list[float]) -> dict:
|
||||
points, cum = [], 0.0
|
||||
for index, net in enumerate(nets):
|
||||
cum += net
|
||||
points.append(
|
||||
{
|
||||
"station_id": f"s{index}",
|
||||
"chainage_m": index * 20.0,
|
||||
"net_volume_m3": net,
|
||||
"cumulative_volume_m3": cum,
|
||||
"cut_soil_m3": max(net, 0.0),
|
||||
"cut_rock_m3": 0.0,
|
||||
"cut_rr_m3": 0.0,
|
||||
"cut_br_m3": 0.0,
|
||||
"net_area_m2": net / 20.0,
|
||||
"fill_m3": max(-net, 0.0),
|
||||
}
|
||||
)
|
||||
return _result({"points": points, "conversion": conv})
|
||||
|
||||
return {
|
||||
"측점 하나": make([0.0]),
|
||||
"측점 둘": make([0.0, 50.0]),
|
||||
"전부 0": make([0.0] * 6),
|
||||
"전부 절토": make([0.0, 30.0, 45.0, 10.0, 80.0]),
|
||||
"전부 성토": make([0.0, -30.0, -45.0, -10.0, -80.0]),
|
||||
"오르내림": make([0.0, 120.0, -200.0, 90.0, -15.0, 60.0, -140.0, 70.0]),
|
||||
}
|
||||
|
||||
|
||||
def test_창구_새_길_옛_묶음_브라우저_같음(tmp_path):
|
||||
cases = [
|
||||
(f"{rid}·{name}", _result(payload), context)
|
||||
for rid, payload in PROJECTS.items()
|
||||
for name, context in _contexts().items()
|
||||
] + [(f"끝·{name}", result, _contexts()["기본"]) for name, result in _edge_results().items()]
|
||||
calls = [
|
||||
{
|
||||
"mod": "common_util/common_util_mass_haul_balance.ts",
|
||||
"fn": "computeHaulPlan",
|
||||
"args": [result, context.get("haul_equipment_limits"), _options(context, drawing)],
|
||||
}
|
||||
for _, result, context in cases
|
||||
for drawing in (True, False)
|
||||
]
|
||||
browser = _browser(tmp_path, calls)
|
||||
checked = 0
|
||||
for index, (label, result, context) in enumerate(cases):
|
||||
new = haul_router._run_view(result, context)
|
||||
assert new is not None, label
|
||||
assert new["haul_plan"] == _old_view(result, context), label
|
||||
assert new["haul_plan"] == browser[2 * index], label
|
||||
quantity = haul_router.run_calc_served(
|
||||
["haul.view"],
|
||||
{
|
||||
"args": {
|
||||
"haul.view": [
|
||||
result,
|
||||
context.get("haul_equipment_limits"),
|
||||
_options(context, False),
|
||||
]
|
||||
}
|
||||
},
|
||||
)["results"]["haul.view"]
|
||||
assert quantity == browser[2 * index + 1], label
|
||||
checked += 1
|
||||
assert checked == len(PROJECTS) * 4 + 6
|
||||
assert browser[2 * len(PROJECTS) * 4] is None # 측점 하나 = 배분 없음
|
||||
assert any(item and item["transfers"] for item in browser) # 빈 배분만 견준 것 아님
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["기본", "구조물 잔토", "채집석"])
|
||||
def test_저장_정본_새_길_옛_묶음_같음(name, monkeypatch):
|
||||
monkeypatch.setattr(prebuild, "_NODE_CACHE", {})
|
||||
detail = json.loads((GOLDEN / "detail.json").read_text(encoding="utf-8"))
|
||||
node_input = {"detail": detail, "context": _contexts()[name]}
|
||||
old = run_bundle_json(prebuild.BUNDLE, prebuild._NPM_SCRIPT, copy.deepcopy(node_input))
|
||||
new = prebuild._node_output(copy.deepcopy(node_input))
|
||||
assert new == old
|
||||
assert new["mass_haul"]["haul_plan"] and new["mass_haul"]["haul_plan_drawing"]
|
||||
@@ -97,6 +97,7 @@ export const ui_locales_m1 = {
|
||||
B01_CalcSites_profile: ["종단 단계", "Profile stage"],
|
||||
B01_CalcSites_cross: ["횡단 단계", "Cross stage"],
|
||||
B01_CalcSites_form_default: ["폼 기본값 도구", "Form default tool"],
|
||||
B01_CalcSites_haul: ["유토 배분", "Haul plan"],
|
||||
B01_CalcSites_fixed_server: ["고정 서버", "Fixed server"],
|
||||
B01_CalcSites_fixed_browser: ["고정 브라우저", "Fixed browser"],
|
||||
B01_CalcSites_Why_ground_sample: [
|
||||
@@ -141,6 +142,26 @@ export const ui_locales_m1 = {
|
||||
"서버 단독(GIS · 래스터 · 값어치)",
|
||||
"Server only (GIS · raster · value)",
|
||||
],
|
||||
B01_CalcSites_Why_haul: [
|
||||
"켜면 브라우저가 배분 모듈을 그때 받아 셈 · 끄면(기본) 잠잠 뒤 서버 /haul-plan",
|
||||
"On = browser loads the haul module then calcs · off (default) = server /haul-plan after a pause",
|
||||
],
|
||||
B01_CalcSites_Why_server_G3: [
|
||||
"셈이 아니라 DB 씀(넓힘 저장) · 미리 보일 값 없음",
|
||||
"Writes the DB (widening save), not a calc · nothing to preview",
|
||||
],
|
||||
B01_CalcSites_Why_server_D1: [
|
||||
"래스터 흐름 · 유역 다각형(GIS 라이브러리) · DEM 수십 MB",
|
||||
"Raster flow · basin polygons (GIS libraries) · DEM tens of MB",
|
||||
],
|
||||
B01_CalcSites_Why_server_D2: [
|
||||
"배수유역(D1) 흐름 누적 래스터 위에서만 셈",
|
||||
"Runs only on the D1 flow-accumulation raster",
|
||||
],
|
||||
B01_CalcSites_Why_server_N1: [
|
||||
"큰 지형 파일 · 세류 래스터 탐색(scipy) · 6~87 s",
|
||||
"Large terrain file · stream raster search (scipy) · 6–87 s",
|
||||
],
|
||||
B01_CalcSites_Why_browser: [
|
||||
"시험용 — 끄면 서버로 돌림(느림) · P7 · D4 = 서버 값 · 끌기 도구(R2 · R4 · R8 · C7) = 대조만",
|
||||
"Test only — off = server (slow) · P7 · D4 = server value · drag tools (R2 · R4 · R8 · C7) = compare only",
|
||||
|
||||
Reference in New Issue
Block a user