@
feat(B06): 구조물 면적·유토곡선 서버 계산 자리 마련 + 상단측 저장 누락 수정 계산 자리 일원화(CLAUDE.md 5장) — 브라우저에서만 돌던 두 계산을 서버가 같은 TS 로 한 번 더 돌려 정본에 얹음. 파이썬 포팅 금지(기하가 두 벌이 되면 그림과 수량이 갈림). - B06_Section_Server_Calc_Node.ts 신설 — 구조물 폐회로 면적 계산 후 그 위에서 유토곡선을 쌓음(화면과 같은 순서). balloon 위치는 서버가 만들지 않음. - B06_Section_Structure_Layouts.ts 신설 — 정본만 읽는 제어기 흉내를 B07 도면에서 떼어 공용화. B07·서버가 같은 한 벌을 씀. - common_util_node_bundle.py 신설 — 번들 빌드·실행 배관 공용화(코리도도 이걸 씀). - 전처리 체인(초기값 스냅샷 앞)·[저장]·[확정]에서 서버 재계산 호출. - 상단측(측구 방향) 변경이 B05 [임시저장]에만 실리던 것을 B06 [저장]·[확정]에도 실음 — flushUphillOverrides. - 죽은 세션 등록 항목 pipes 제거(읽는 곳도 쓰는 곳도 없었음). 검증: tsc --noEmit 통과, pytest 386 passed, tmp/tests/test_b06_server_calc_node.mjs 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> @
This commit is contained in:
+3
-1
@@ -57,4 +57,6 @@ tmp/
|
||||
**/.obsidian/
|
||||
# 코리도 서버 사전 생성 번들 — `npm run build:corridor` 산출물(2026-09-04).
|
||||
# 소스가 바뀌면 서버가 스스로 다시 만든다(B05_Profile_Corridor_Prebuild.py).
|
||||
config/corridor_node/
|
||||
config/corridor_node/
|
||||
# 횡단 서버 재계산 번들 — `npm run build:server-calc` 산출물(2026-09-06).
|
||||
config/server_calc_node/
|
||||
@@ -92,8 +92,9 @@ export const STATE_REGISTRY = {
|
||||
structures: { bucket: "draft", scope: "project", legacy: (p) => `b05:structures:${p}` },
|
||||
/** 3D 램프로 바꾼 측점 상단측(측구 방향). */
|
||||
uphill: { bucket: "draft", scope: "project", legacy: (p) => `b05:uphill:${p}` },
|
||||
/** 관 위치(되돌리기 스냅샷이 함께 본다). */
|
||||
pipes: { bucket: "draft", scope: "project", legacy: () => "b05:pipes" },
|
||||
/* `pipes`(옛 `b05:pipes`)는 2026-09-06 에 뺐다 — 읽는 곳도 쓰는 곳도 없었다.
|
||||
관 위치의 정본은 `pipe_points.json` 이고, 화면은 [저장] 때 `savePipes()` 로 바로
|
||||
내보낸다. 초안처럼 보이는 이름만 남아 대응표에서 「저장 자리 없음」으로 잡혔다. */
|
||||
/** B05 에서 고른 구조물을 B06 이 이어받는 자리 — 예전 `aislo:structure-pick:*`. */
|
||||
"structure-pick": {
|
||||
bucket: "draft",
|
||||
|
||||
@@ -280,6 +280,17 @@ async def run_auto_design_chain(
|
||||
except Exception: # noqa: BLE001 — 코리도 실패가 체인을 막지는 않는다
|
||||
logger.exception("코리도 사전 생성 실패: project_id=%s", project_id)
|
||||
|
||||
# 구조물 폐회로 면적 + 유토곡선 — 화면이 쓰는 TS 를 서버에서 한 번 돌려 정본에
|
||||
# 얹는다(2026-09-06). 이걸 빼면 B06 을 안 연 프로젝트의 수량이 구조물을 모르는
|
||||
# 표준값으로 남고 유토곡선은 아예 없다. 초기값 스냅샷 **앞**이라 되돌릴
|
||||
# 기준선에도 보정이 담긴다.
|
||||
try:
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import recompute_server_side
|
||||
|
||||
await recompute_server_side(project_id, route_id)
|
||||
except Exception: # noqa: BLE001 — 재계산 실패가 체인을 막지는 않는다
|
||||
logger.exception("횡단 서버 재계산 실패: project_id=%s", project_id)
|
||||
|
||||
# 초기값 스냅샷 — 여기가 [초기화]가 되돌릴 기준선이다(CLAUDE.md 5장).
|
||||
# 체인 규약대로 실패는 비치명적이다: 계산 결과는 그대로 두고 실패 마커만 남겨
|
||||
# [초기화]가 재계산으로 얼버무리지 않게 한다(2026-09-02 사용자 확정).
|
||||
|
||||
@@ -266,6 +266,19 @@ export async function confirmRoute(
|
||||
});
|
||||
}
|
||||
|
||||
/** 세션에 쌓인 상단측(측구 방향) 변경분을 정본으로 내보낸다 — B06 [저장]·[확정]용.
|
||||
* 3D 램프 클릭은 B05 화면에서만 생기지만 저장 버튼은 B06 에도 있다(B05·B06 은 한 페이지).
|
||||
* B06 에서 저장하면 이 값이 세션에만 남아 확정 뒤 옛 측구 방향이 그대로 쓰였다
|
||||
* (2026-09-06 대응표 조사). 비어 있으면 요청을 내지 않는다. */
|
||||
export async function flushUphillOverrides(projectId: string): Promise<void> {
|
||||
const stored = readState<Record<string, "left" | "right">>("uphill", projectId);
|
||||
const overrides = Object.entries(stored ?? {})
|
||||
.filter(([, side]) => side === "left" || side === "right")
|
||||
.map(([chainage, side]) => ({ chainage_m: Number(chainage), side }));
|
||||
if (!overrides.length) return;
|
||||
await confirmRoute(projectId, { uphill_overrides: overrides }, false);
|
||||
}
|
||||
|
||||
/** [초기화] 응답 — 초기 자동 계산 상태로 재구성된 경로. */
|
||||
export interface RouteResetResponse {
|
||||
status: string;
|
||||
|
||||
@@ -16,8 +16,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -25,76 +23,13 @@ from uuid import UUID
|
||||
|
||||
from B05_Profile.B05_Profile_Repository import get_route_points
|
||||
from B05_Profile.B05_Profile_Router_Corridor import corridor_path
|
||||
from common_util.common_util_node_bundle import build_bundle, bundle_stale, run_node
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUNDLE = ROOT / "config" / "corridor_node" / "B05_Profile_Corridor_Node.js"
|
||||
# 번들이 낡았는지 재는 대상 — 빌더 계통이 걸쳐 있는 폴더.
|
||||
_SOURCE_DIRS = ("B05_Profile", "B06_Section", "common_util")
|
||||
# 번들 만들기·실행 상한(초). 실측 번들 0.1초, 빌드 65측점 3초 수준이라 넉넉하다.
|
||||
_BUILD_TIMEOUT_S = 300
|
||||
_RUN_TIMEOUT_S = 600
|
||||
|
||||
|
||||
def _node_env() -> dict[str, str]:
|
||||
"""config/node_modules를 쓰는 프론트엔드 프로세스 환경(main.py와 같은 규약)."""
|
||||
env = os.environ.copy()
|
||||
node_modules = ROOT / "config" / "node_modules"
|
||||
env["PATH"] = f"{node_modules / '.bin'}{os.pathsep}{env.get('PATH', '')}"
|
||||
env["NODE_PATH"] = str(node_modules)
|
||||
return env
|
||||
|
||||
|
||||
def _bundle_stale() -> bool:
|
||||
"""번들이 없거나 TS 원본보다 오래됐으면 참.
|
||||
|
||||
번들이 낡으면 서버와 화면이 **다른 기하**를 만든다 — 이 판정이 그것을 막는 유일한
|
||||
장치다. 개발 중에는 `npm run build`를 따로 돌리지 않으므로 여기서 스스로 갱신한다.
|
||||
"""
|
||||
if not BUNDLE.is_file():
|
||||
return True
|
||||
built_at = BUNDLE.stat().st_mtime
|
||||
for directory in _SOURCE_DIRS:
|
||||
for path in (ROOT / directory).rglob("*.ts"):
|
||||
if path.stat().st_mtime > built_at:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _build_bundle() -> bool:
|
||||
result = subprocess.run( # noqa: S602 — 고정 명령, 사용자 입력 없음
|
||||
"npm run build:corridor",
|
||||
shell=True,
|
||||
cwd=str(ROOT),
|
||||
env=_node_env(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=_BUILD_TIMEOUT_S,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error("코리도 번들 빌드 실패:\n%s", result.stderr)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _run_node(input_path: Path, output_path: Path) -> int:
|
||||
result = subprocess.run( # noqa: S603 — 고정 실행 파일, 인자는 임시 파일 경로뿐
|
||||
["node", str(BUNDLE), str(input_path), str(output_path)],
|
||||
cwd=str(ROOT),
|
||||
env=_node_env(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=_RUN_TIMEOUT_S,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning("코리도 사전 생성 실패(끝 코드 %s): %s", result.returncode, result.stderr)
|
||||
return result.returncode
|
||||
|
||||
|
||||
async def _section_detail(project_id: UUID | str, route_id: int) -> dict[str, Any] | None:
|
||||
@@ -121,7 +56,7 @@ async def prebuild_corridor(project_id: UUID | str, route_id: int, project_root:
|
||||
logger.info("코리도 사전 생성 건너뜀 — 노선 점이 부족함 (route_id=%s)", route_id)
|
||||
return False
|
||||
|
||||
if _bundle_stale() and not await asyncio.to_thread(_build_bundle):
|
||||
if bundle_stale(BUNDLE) and not await asyncio.to_thread(build_bundle, "build:corridor"):
|
||||
return False
|
||||
|
||||
target = corridor_path(Path(project_root), route_id)
|
||||
@@ -133,7 +68,7 @@ async def prebuild_corridor(project_id: UUID | str, route_id: int, project_root:
|
||||
json.dumps({"detail": detail, "route_points": points}, default=float),
|
||||
"utf-8",
|
||||
)
|
||||
if await asyncio.to_thread(_run_node, source, built) != 0:
|
||||
if await asyncio.to_thread(run_node, BUNDLE, source, built) != 0:
|
||||
return False
|
||||
await asyncio.to_thread(target.parent.mkdir, parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(_replace, built, target)
|
||||
|
||||
@@ -147,6 +147,25 @@ async def _apply_section_edits(
|
||||
|
||||
|
||||
@router.post("/{project_id}/sections/{route_id}/save", response_model=SectionConfirmResponse)
|
||||
async def _recompute_server_side(project_id: UUID, route_id: int) -> None:
|
||||
"""구조물 면적·유토곡선을 서버가 다시 계산해 정본에 얹는다(2026-09-06).
|
||||
|
||||
브라우저가 보낸 값을 그대로 받아 적지 않는다 — 정본은 서버가 만든다
|
||||
(CLAUDE.md 5장 「계산 자리」). 사용자 조작(patch)이 먼저 들어간 **뒤**에 돌아야
|
||||
바뀐 벽 위치가 면적·유토곡선에 실린다. 실패는 비치명적이다.
|
||||
"""
|
||||
try:
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import recompute_server_side
|
||||
|
||||
await recompute_server_side(project_id, route_id)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"횡단 서버 재계산 실패 (저장은 유지): project_id=%s route_id=%s",
|
||||
project_id,
|
||||
route_id,
|
||||
)
|
||||
|
||||
|
||||
async def save_sections(
|
||||
project_id: UUID,
|
||||
route_id: int,
|
||||
@@ -200,6 +219,7 @@ async def save_sections(
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
await _recompute_server_side(project_id, route_id)
|
||||
return SectionConfirmResponse(
|
||||
project_id=str(project_id), route_id=route_id, confirmed=False
|
||||
)
|
||||
@@ -284,6 +304,8 @@ async def confirm_sections(
|
||||
await connection.rollback()
|
||||
raise
|
||||
|
||||
await _recompute_server_side(project_id, route_id)
|
||||
|
||||
# 측구 방향(design.ditch_side) 변경을 B05 종단 정본 stations.uphill_side에 역반영한다(E-7).
|
||||
# 파일 기반·비치명적: 실패해도 확정은 유지한다.
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Server_Calc_Node.ts
|
||||
* 브라우저에서만 돌던 횡단 계산을 **서버가 한 번 돌리는** 진입점 — 구조물 폐회로
|
||||
* 절·성토 면적 + 그것을 쌓아 만든 유토곡선.
|
||||
*
|
||||
* 왜 있나(2026-09-06, CLAUDE.md 5장 「계산 자리」) — 이 두 값은 지금까지 브라우저에서만
|
||||
* 나왔다. 사용자가 B06 을 한 번도 안 열면 값이 없고, 저장·확정 뒤 서버가 다시 계산하면
|
||||
* 구조물을 모르는 표준값으로 되돌아갔다. 코리도(`B05_Profile_Corridor_Node.ts`)와 같은
|
||||
* 방식으로 **브라우저가 쓰는 코드를 서버가 그대로 실행**한다 — 계산을 두 벌로 짜지 않는다.
|
||||
*
|
||||
* 순서가 중요하다: 면적 보정을 **먼저** 얹고 그 위에서 유토곡선을 쌓는다. 화면도 같은
|
||||
* 순서다(카드를 그리며 면적을 고친 뒤 유토곡선을 낸다).
|
||||
*
|
||||
* 실행: node <번들> <입력.json> <출력.json>
|
||||
* 입력 { detail: 종횡단 상세(API와 같은 꼴), context: { earthwork_conversion,
|
||||
* natural_spoil_min_ground_slope, haul_equipment_limits } }
|
||||
* 출력 { areas: [{ chainage_m, cut_area_m2, … }], mass_haul: {…} | null }
|
||||
* — areas 는 **구조물 트림이 있는 측점만**. 나머지는 표준 계산값이 이미 맞다.
|
||||
* 끝 코드: 0 성공 / 2 인자 오류
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { computeStructureAreas } from "@util/common_util_cross_structure_areas";
|
||||
import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
|
||||
import { computeHaulPlan } from "@util/common_util_mass_haul_balance";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { computeStoredLayouts, trimOfLayouts } from "./B06_Section_Structure_Layouts";
|
||||
|
||||
interface ServerCalcInput {
|
||||
detail: SectionDetailResponse;
|
||||
context?: {
|
||||
earthwork_conversion?: Parameters<typeof computeMassHaul>[1];
|
||||
natural_spoil_min_ground_slope?: number | null;
|
||||
haul_equipment_limits?: Parameters<typeof computeHaulPlan>[1];
|
||||
};
|
||||
}
|
||||
|
||||
const [inputPath, outputPath] = process.argv.slice(2);
|
||||
if (!inputPath || !outputPath) {
|
||||
console.error("사용법: node <번들> <입력.json> <출력.json>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const input = JSON.parse(readFileSync(inputPath, "utf8")) as ServerCalcInput;
|
||||
const sections: CrossSection[] = input.detail?.cross_sections ?? [];
|
||||
|
||||
/** 화면(`B06_Section_UI_Cross_View.applyStructureAreas`)과 같은 입력을 만든다. */
|
||||
function areasOf(section: CrossSection): Record<string, number> | null {
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
if (!layouts) return null;
|
||||
const trim = trimOfLayouts(layouts);
|
||||
const design = layouts.design;
|
||||
if (!trim || !Array.isArray(design.design_line) || design.design_line.length < 2) return null;
|
||||
const ground = section.samples
|
||||
.filter((sample) => sample.valid !== false && typeof sample.elevation_m === "number")
|
||||
.map((sample) => ({ offset: sample.offset_m ?? 0, elevation: sample.elevation_m as number }))
|
||||
.sort((a, b) => a.offset - b.offset);
|
||||
const areas = computeStructureAreas({
|
||||
designLine: design.design_line as Array<{ offset_m: number; elevation_m: number }>,
|
||||
ground,
|
||||
trim,
|
||||
rockBoundaryOffsetM:
|
||||
typeof design.rock_boundary_offset_m === "number" ? design.rock_boundary_offset_m : null,
|
||||
});
|
||||
if (!areas) return null;
|
||||
const round = (value: number): number => Number(value.toFixed(4));
|
||||
const out: Record<string, number> = {
|
||||
chainage_m: section.chainage_m,
|
||||
cut_area_m2: round(areas.cutAreaM2),
|
||||
fill_area_m2: round(areas.fillAreaM2),
|
||||
};
|
||||
// 토사·암 분리는 원래 값이 있을 때만 덮는다 — 화면 규칙과 같다.
|
||||
if (typeof design.cut_soil_area_m2 === "number") {
|
||||
out.cut_soil_area_m2 = round(areas.cutSoilAreaM2);
|
||||
out.cut_rock_area_m2 = round(areas.cutRockAreaM2);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const areas = sections.map(areasOf).filter((entry): entry is Record<string, number> => !!entry);
|
||||
// 보정값을 **자리에서** 얹는다 — 유토곡선이 고쳐진 면적 위에서 쌓이게 한다.
|
||||
areas.forEach((row) => {
|
||||
const section = sections.find((item) => item.chainage_m === row.chainage_m);
|
||||
const design = section?.design;
|
||||
if (!design) return;
|
||||
for (const key of ["cut_area_m2", "fill_area_m2", "cut_soil_area_m2", "cut_rock_area_m2"]) {
|
||||
if (typeof row[key] === "number") (design as Record<string, unknown>)[key] = row[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 유토곡선 — balloon 위치는 사용자 화면값이라 서버가 만들지 않는다(파이썬이 보존).
|
||||
const conversion = input.context?.earthwork_conversion;
|
||||
const result = conversion
|
||||
? computeMassHaul(sections, conversion, input.context?.natural_spoil_min_ground_slope ?? undefined)
|
||||
: null;
|
||||
const massHaul = result
|
||||
? massHaulPayload(result, computeHaulPlan(result, input.context?.haul_equipment_limits))
|
||||
: null;
|
||||
|
||||
writeFileSync(outputPath, JSON.stringify({ areas, mass_haul: massHaul }));
|
||||
@@ -0,0 +1,125 @@
|
||||
"""브라우저에서만 돌던 횡단 계산을 **서버가** 돌려 정본에 남긴다(2026-09-06).
|
||||
|
||||
대상 둘 —
|
||||
① 구조물이 선 측점의 절·성토 면적: 기슭막이·세월교·BOX암거가 서면 성토 사면이 벽에서
|
||||
끊겨 지반선과 설계선이 이루는 폐회로가 달라진다.
|
||||
② 그 면적을 쌓아 만드는 유토곡선.
|
||||
|
||||
왜 — 둘 다 화면에서만 돌아, 사용자가 B06 을 한 번도 안 열면 값이 없고 저장·확정 뒤에는
|
||||
구조물을 모르는 표준값으로 되돌아갔다(CLAUDE.md 5장 「계산 자리」 — 금지 항목이던 자리).
|
||||
|
||||
**계산을 다시 짜지 않는다.** 화면이 쓰는 TS 를 Node 진입점(`B06_Section_Server_Calc_Node.ts`)
|
||||
으로 감싸 그대로 돌린다. 파이썬으로 포팅하면 같은 기하가 두 벌이 되어 「그림은 이런데
|
||||
수량은 저렇다」가 생긴다.
|
||||
|
||||
실패는 비치명적이다 — 보정 전(표준) 값이 그대로 남고 화면은 예전처럼 스스로 고친다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from B06_Section.B06_Section_Repository import (
|
||||
get_longitudinal_section,
|
||||
merge_cross_section_design_patch,
|
||||
merge_longitudinal_section_data,
|
||||
)
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
from config.config_db import get_db_pool
|
||||
from config.config_system import (
|
||||
EARTHWORK_CONVERSION_FACTORS,
|
||||
EARTHWORK_HAUL_EQUIPMENT_LIMITS_M,
|
||||
NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUNDLE = ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js"
|
||||
_NPM_SCRIPT = "build:server-calc"
|
||||
# 정본에 얹는 값만 받는다 — Node 가 다른 키를 내도 설계 데이터에 흘리지 않는다.
|
||||
_AREA_KEYS = ("cut_area_m2", "fill_area_m2", "cut_soil_area_m2", "cut_rock_area_m2")
|
||||
|
||||
|
||||
def _mass_haul_context() -> dict[str, Any]:
|
||||
"""유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다."""
|
||||
return {
|
||||
"earthwork_conversion": EARTHWORK_CONVERSION_FACTORS,
|
||||
"natural_spoil_min_ground_slope": NATURAL_SPOIL_MIN_GROUND_SLOPE,
|
||||
"haul_equipment_limits": [
|
||||
{"key": key, "max_distance_m": limit}
|
||||
for key, limit in EARTHWORK_HAUL_EQUIPMENT_LIMITS_M
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def recompute_server_side(project_id: UUID | str, route_id: int) -> int:
|
||||
"""구조물 면적·유토곡선을 다시 계산해 정본에 얹는다. 고친 측점 수를 돌려준다."""
|
||||
from B06_Section.B06_Section_Router import get_section_detail
|
||||
|
||||
project_uuid = UUID(str(project_id))
|
||||
response = await get_section_detail(project_uuid, route_id)
|
||||
payload = getattr(response, "model_dump", None)
|
||||
if payload is None: # JSONResponse = 실패
|
||||
logger.warning("서버 재계산: 종횡단 상세를 못 받음 (route_id=%s)", route_id)
|
||||
return 0
|
||||
|
||||
output = await asyncio.to_thread(
|
||||
run_bundle_json,
|
||||
BUNDLE,
|
||||
_NPM_SCRIPT,
|
||||
{"detail": payload(mode="json"), "context": _mass_haul_context()},
|
||||
)
|
||||
if not isinstance(output, dict):
|
||||
return 0
|
||||
rows = output.get("areas")
|
||||
mass_haul = output.get("mass_haul")
|
||||
|
||||
updated = 0
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
# balloon 위치는 **사용자가 끌어 옮긴 화면값**이다 — 서버가 만들지 않으므로
|
||||
# 저장분에서 떼어 새 유토곡선에 도로 붙인다(2026-09-06).
|
||||
if isinstance(mass_haul, dict):
|
||||
existing = await get_longitudinal_section(connection, project_uuid, route_id)
|
||||
stored = (existing or {}).get("data") or {}
|
||||
offsets = (stored.get("mass_haul") or {}).get("balloon_offsets")
|
||||
if offsets is not None:
|
||||
mass_haul["balloon_offsets"] = offsets
|
||||
|
||||
await connection.begin()
|
||||
try:
|
||||
for row in rows if isinstance(rows, list) else []:
|
||||
patch: dict[str, Any] = {
|
||||
key: float(row[key])
|
||||
for key in _AREA_KEYS
|
||||
if isinstance(row.get(key), (int, float))
|
||||
}
|
||||
if not patch:
|
||||
continue
|
||||
if await merge_cross_section_design_patch(
|
||||
connection,
|
||||
route_id=route_id,
|
||||
chainage_m=float(row["chainage_m"]),
|
||||
patch=patch,
|
||||
):
|
||||
updated += 1
|
||||
if isinstance(mass_haul, dict):
|
||||
await merge_longitudinal_section_data(
|
||||
connection, route_id=route_id, data_patch={"mass_haul": mass_haul}
|
||||
)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
raise
|
||||
logger.info(
|
||||
"서버 재계산: route_id=%s 구조물 측점 %s곳 갱신, 유토곡선 %s",
|
||||
route_id,
|
||||
updated,
|
||||
"갱신" if isinstance(mass_haul, dict) else "없음",
|
||||
)
|
||||
return updated
|
||||
@@ -0,0 +1,107 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Structure_Layouts.ts
|
||||
* 정본(`section.design`)만 읽어 **구조물 기하 한 벌**을 내는 자리 — 화면·조작 없이 돈다.
|
||||
*
|
||||
* 왜 있나(2026-09-06, CLAUDE.md 5장 「계산 자리」) — 같은 기하를 세 곳이 쓴다:
|
||||
* ① B06 횡단 카드(사용자 조작 중) ② B07 도면 작도 ③ 서버 초기 계산(Node 진입점).
|
||||
* ①은 조작 제어기를 물고 돌아야 하고, ②·③은 저장된 값만 보면 된다. ②가 갖고 있던
|
||||
* 「제어기 흉내내기」를 여기로 옮겨 ③이 그대로 쓴다 — 기하를 두 벌로 만들지 않는다.
|
||||
*
|
||||
* DOM·SVG 를 부르지 않는다. 브라우저에서도 Node 에서도 같은 결과가 나와야 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
|
||||
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { computeCardCulvert, culvertLinkFor } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import type {
|
||||
ExtraWallControl,
|
||||
InletStructureControl,
|
||||
RevetOffsetControl,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { computeFordLayout, DEFAULT_FORD_WALL_ADJUST } from "./B06_Section_UI_Cross_Ford_Geom";
|
||||
import { computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
|
||||
|
||||
/** 같은 측점으로 볼 누가거리 오차(m) — 정본 반올림 자릿수보다 크게 잡는다. */
|
||||
const CHAINAGE_TOLERANCE_M = 0.02;
|
||||
|
||||
function storedWallAdjust(section: CrossSection, role: string): WallAdjust {
|
||||
const stored = section.design?.revet_adjust?.[role];
|
||||
return stored ? { ...ZERO_ADJUST, ...(stored as Partial<WallAdjust>) } : { ...ZERO_ADJUST };
|
||||
}
|
||||
|
||||
/* 정본만 읽는 조작값 — 편집하지 않으므로 되받기·토스트는 빈 동작이다. */
|
||||
const revetOffset: RevetOffsetControl = {
|
||||
adjustFor: (section, role) => storedWallAdjust(section, role),
|
||||
storedAdjustFor: (section, role) =>
|
||||
section.design?.revet_adjust?.[role] ? storedWallAdjust(section, role) : null,
|
||||
selectedFor: () => null,
|
||||
highlightFor: () => null,
|
||||
select: () => undefined,
|
||||
syncApplied: () => undefined,
|
||||
update: () => undefined,
|
||||
reset: () => undefined,
|
||||
};
|
||||
|
||||
const extraWalls: ExtraWallControl = {
|
||||
countFor: (section, side = "outlet") => section.design?.extra_wall_counts?.[side] ?? 0,
|
||||
setCount: () => undefined,
|
||||
equalize: () => undefined,
|
||||
consumeEqualize: () => false,
|
||||
syncCount: () => undefined,
|
||||
};
|
||||
|
||||
const inletStructure: InletStructureControl = {
|
||||
valueFor: (section) => section.design?.inlet_structure ?? "auto",
|
||||
adjustFor: (section) => ({ ...DEFAULT_BASIN_ADJUST, ...(section.design?.basin_adjust ?? {}) }),
|
||||
set: () => undefined,
|
||||
updateAdjust: () => undefined,
|
||||
resetAdjust: () => undefined,
|
||||
};
|
||||
|
||||
/** 한 측점의 구조물 기하 한 벌 — 설계선 트림과 구조물 그리기가 같은 결과를 나눠 쓴다. */
|
||||
export function computeStoredLayouts(section: CrossSection, sections: readonly CrossSection[]) {
|
||||
const design = section.design;
|
||||
if (!design) return null;
|
||||
const designZAt = (chainageM: number): number | null => {
|
||||
const found = sections.find(
|
||||
(item) => Math.abs(item.chainage_m - chainageM) <= CHAINAGE_TOLERANCE_M,
|
||||
);
|
||||
return found?.design?.design_elevation_m ?? null;
|
||||
};
|
||||
const link = section.culvert ? undefined : culvertLinkFor(section, sections, designZAt);
|
||||
const culvert = computeCardCulvert(
|
||||
section,
|
||||
section.samples,
|
||||
null,
|
||||
revetOffset,
|
||||
inletStructure,
|
||||
extraWalls,
|
||||
link,
|
||||
);
|
||||
const box = computeBoxLayout(section, section.samples, {
|
||||
left: { ...DEFAULT_BOX_SIDE_ADJUST, ...(design.box_adjust?.left ?? {}) },
|
||||
right: { ...DEFAULT_BOX_SIDE_ADJUST, ...(design.box_adjust?.right ?? {}) },
|
||||
});
|
||||
const ford = computeFordLayout(section, section.samples, {
|
||||
inlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(design.ford_adjust?.inlet ?? {}) },
|
||||
outlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(design.ford_adjust?.outlet ?? {}) },
|
||||
});
|
||||
// 독립 기슭막이(옛 D군 경로) — 배관 세트가 붙었거나 옆에서 이어져 오면 그쪽이 그린다.
|
||||
const own =
|
||||
!section.culvert && !link ? computeRevetmentLayout(section, design.revet_adjust?.own) : null;
|
||||
return { design, link, culvert, box, ford, own };
|
||||
}
|
||||
|
||||
export type StoredLayouts = NonNullable<ReturnType<typeof computeStoredLayouts>>;
|
||||
|
||||
/** 실제로 그려지는 설계선의 트림 — B06 카드와 **같은 우선순위**로 고른다. */
|
||||
export function trimOfLayouts(layouts: StoredLayouts) {
|
||||
return (
|
||||
layouts.culvert?.designTrim ??
|
||||
layouts.ford?.designTrim ??
|
||||
layouts.box?.designTrim ??
|
||||
layouts.own?.designTrim
|
||||
);
|
||||
}
|
||||
@@ -705,8 +705,8 @@ export function createCrossSectionCard(
|
||||
* 성토 사면이 벽에서 끊기고 그 바깥은 벽·성토부선이 대신 그리므로 폐회로가 달라진다.
|
||||
* 트림이 없으면(구조물 없는 측점) 아무것도 하지 않는다.
|
||||
*
|
||||
* ⚠ 서버는 아직 구조물 기하를 모른다 — 저장·확정 뒤 서버가 다시 계산하면 표준 값으로
|
||||
* 돌아간다(계획서 3-2 남은 몫).
|
||||
* 조작 중 즉시 반영용이다. 정본은 [저장]·[확정] 때 서버가 같은 코드
|
||||
* (`B06_Section_Structure_Areas_Node.ts`)로 다시 계산해 얹는다(2026-09-06).
|
||||
*/
|
||||
function applyStructureAreas(section: CrossSection, trim: DesignTrim | undefined | null): void {
|
||||
const design = section.design;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches";
|
||||
import type { StandardCrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_Section_UI_Section_View";
|
||||
@@ -194,6 +195,11 @@ async function flushPendingEdits(ctx: SectionPersistContext, projectId: string):
|
||||
// 조정창 구간값은 세션에만 있다 — 정본 payload를 모으기 전에 내보낸다
|
||||
// (CLAUDE.md 5장: 영구저장은 [저장]·[확정]에서만).
|
||||
await ctx.flushCulvertOptions();
|
||||
// B05 3D에서 바꾼 상단측(측구 방향)도 여기서 내보낸다 — 예전에는 B05 [임시저장]에만
|
||||
// 실려, B06에서 저장·확정하면 세션에만 남아 옛 방향이 정본에 그대로 있었다
|
||||
// (2026-09-06). 서버가 종단 정본과 저장된 횡단 설계를 함께 갱신하므로 아래
|
||||
// 횡단 patch 저장보다 **먼저** 나가야 사용자 수정이 위에 얹힌다.
|
||||
await flushUphillOverrides(projectId).catch(() => undefined);
|
||||
// B05에서 만지고 넘어온 구조물 조작분도 여기서 정본에 남긴다. 실패해도 횡단
|
||||
// 저장까지 막지는 않는다 — 미저장분은 세션에 남으므로 다시 시도할 수 있다
|
||||
// (2026-08-29 실측: 타입이 거절되자 sections/save가 아예 나가지 않았다).
|
||||
|
||||
@@ -14,40 +14,19 @@
|
||||
|
||||
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { loadSectionDetail } from "../B06_Section/B06_Section_Section_Store";
|
||||
import {
|
||||
computeStoredLayouts as computeLayouts,
|
||||
type StoredLayouts,
|
||||
} from "../B06_Section/B06_Section_Structure_Layouts";
|
||||
import { appendBoxOverlay } from "../B06_Section/B06_Section_UI_Cross_Box";
|
||||
import {
|
||||
computeBoxLayout,
|
||||
DEFAULT_BOX_SIDE_ADJUST,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Box_Geom";
|
||||
import { appendCulvertOverlay } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||||
import {
|
||||
DEFAULT_BASIN_ADJUST,
|
||||
ZERO_ADJUST,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { WallAdjust } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
computeCardCulvert,
|
||||
culvertLinkFor,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
|
||||
import type {
|
||||
ExtraWallControl,
|
||||
InletStructureControl,
|
||||
RevetOffsetControl,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { appendFordOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford";
|
||||
import {
|
||||
computeFordLayout,
|
||||
DEFAULT_FORD_WALL_ADJUST,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Ford_Geom";
|
||||
import { appendFordPavementOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
|
||||
import {
|
||||
appendCrossDesignOverlay,
|
||||
appendPavementOverlay,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Design";
|
||||
import {
|
||||
appendRevetmentOverlay,
|
||||
computeRevetmentLayout,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Revetment";
|
||||
import { appendRevetmentOverlay } from "../B06_Section/B06_Section_UI_Cross_Revetment";
|
||||
|
||||
/** 서버가 도면에 실어 보내는 측점별 실좌표(m) → 종이(mm) 변환값. */
|
||||
export interface CrossPlacement {
|
||||
@@ -440,77 +419,7 @@ function entityBox(entity: Record<string, unknown>): number[] | null {
|
||||
return xs.length ? [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)] : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 정본(design)만 읽는 조작값 — B07은 편집하지 않으므로 되받기·토스트는 빈 동작이다.
|
||||
// ---------------------------------------------------------------------------
|
||||
function storedWallAdjust(section: CrossSection, role: string): WallAdjust {
|
||||
const stored = section.design?.revet_adjust?.[role];
|
||||
return stored ? { ...ZERO_ADJUST, ...(stored as Partial<WallAdjust>) } : { ...ZERO_ADJUST };
|
||||
}
|
||||
|
||||
const revetOffset: RevetOffsetControl = {
|
||||
adjustFor: (section, role) => storedWallAdjust(section, role),
|
||||
storedAdjustFor: (section, role) =>
|
||||
section.design?.revet_adjust?.[role] ? storedWallAdjust(section, role) : null,
|
||||
selectedFor: () => null,
|
||||
highlightFor: () => null,
|
||||
select: () => undefined,
|
||||
syncApplied: () => undefined,
|
||||
update: () => undefined,
|
||||
reset: () => undefined,
|
||||
};
|
||||
|
||||
const extraWalls: ExtraWallControl = {
|
||||
countFor: (section, side = "outlet") => section.design?.extra_wall_counts?.[side] ?? 0,
|
||||
setCount: () => undefined,
|
||||
equalize: () => undefined,
|
||||
consumeEqualize: () => false,
|
||||
syncCount: () => undefined,
|
||||
};
|
||||
|
||||
const inletStructure: InletStructureControl = {
|
||||
valueFor: (section) => section.design?.inlet_structure ?? "auto",
|
||||
adjustFor: (section) => ({ ...DEFAULT_BASIN_ADJUST, ...(section.design?.basin_adjust ?? {}) }),
|
||||
set: () => undefined,
|
||||
updateAdjust: () => undefined,
|
||||
resetAdjust: () => undefined,
|
||||
};
|
||||
|
||||
/** 한 측점의 구조물 기하 한 벌 — 설계선 트림과 구조물 그리기가 같은 결과를 나눠 쓴다. */
|
||||
function computeLayouts(section: CrossSection, sections: CrossSection[]) {
|
||||
const design = section.design;
|
||||
if (!design) return null;
|
||||
const designZAt = (chainageM: number): number | null => {
|
||||
const found = sections.find(
|
||||
(item) => Math.abs(item.chainage_m - chainageM) <= CHAINAGE_TOLERANCE_M,
|
||||
);
|
||||
return found?.design?.design_elevation_m ?? null;
|
||||
};
|
||||
const link = section.culvert ? undefined : culvertLinkFor(section, sections, designZAt);
|
||||
const culvert = computeCardCulvert(
|
||||
section,
|
||||
section.samples,
|
||||
null,
|
||||
revetOffset,
|
||||
inletStructure,
|
||||
extraWalls,
|
||||
link,
|
||||
);
|
||||
const box = computeBoxLayout(section, section.samples, {
|
||||
left: { ...DEFAULT_BOX_SIDE_ADJUST, ...(design.box_adjust?.left ?? {}) },
|
||||
right: { ...DEFAULT_BOX_SIDE_ADJUST, ...(design.box_adjust?.right ?? {}) },
|
||||
});
|
||||
const ford = computeFordLayout(section, section.samples, {
|
||||
inlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(design.ford_adjust?.inlet ?? {}) },
|
||||
outlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(design.ford_adjust?.outlet ?? {}) },
|
||||
});
|
||||
// 독립 기슭막이(옛 D군 경로) — 배관 세트가 붙었거나 옆에서 이어져 오면 그쪽이 그린다.
|
||||
const own =
|
||||
!section.culvert && !link ? computeRevetmentLayout(section, design.revet_adjust?.own) : null;
|
||||
return { design, link, culvert, box, ford, own };
|
||||
}
|
||||
|
||||
type Layouts = NonNullable<ReturnType<typeof computeLayouts>>;
|
||||
type Layouts = StoredLayouts;
|
||||
|
||||
/**
|
||||
* 설계선(+포장층)을 그린다. **구조물이 깎아 낸 설계선**(designTrim)을 B06 카드와 같은
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
* 여기서는 **그리는 쪽이 이미 만든 트림 값**(`designTrim`)을 그대로 받는다. 화면과 면적이
|
||||
* 같은 입력을 쓰므로 "그림은 이런데 수량은 저렇다"가 생기지 않는다.
|
||||
*
|
||||
* ⚠ 파이썬 짝이 아직 없다 — 서버는 구조물 기하를 모른다(계획서 3-2). 그래서 이 보정은
|
||||
* **브라우저 계산에만** 실린다. 저장·확정 뒤 서버가 다시 계산하면 표준 설계선 값으로
|
||||
* 돌아간다. 서버 쪽 반영은 별도 작업이다.
|
||||
* 파이썬 짝은 만들지 않는다 — 서버도 **이 파일을 그대로 실행**한다
|
||||
* (`B06_Section_Structure_Areas_Node.ts` → 번들, 2026-09-06). 전처리 체인 끝과
|
||||
* [저장]·[확정] 때 서버가 돌려 정본에 얹으므로, 브라우저를 한 번도 안 열어도 값이 선다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { splitCutAreas, trapezoidAreas } from "./common_util_cross_design_areas";
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""브라우저용 TS 를 **서버에서 그대로 실행**하기 위한 공통 배관(2026-09-06 분리).
|
||||
|
||||
CLAUDE.md 5장 「계산 자리」 — 같은 계산을 파이썬으로 다시 짜지 않고, 화면이 쓰는 TS 를
|
||||
Node 진입점으로 감싸 서버가 부른다. 코리도(`B05_Profile_Corridor_Prebuild`)가 첫 사례고
|
||||
구조물 면적(`B06_Section_Structure_Areas_Prebuild`)이 뒤따르면서, 번들 빌드·실행 배관이
|
||||
두 벌이 되어 여기로 모았다. **계산은 이 파일에 없다** — 실행 껍데기만 있다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
# 번들이 낡았는지 재는 대상 — 기하 계통이 걸쳐 있는 폴더.
|
||||
SOURCE_DIRS = ("B05_Profile", "B06_Section", "common_util")
|
||||
# 번들 만들기·실행 상한(초). 실측 번들 실행 0.1초, 빌드 3초 수준이라 넉넉하다.
|
||||
BUILD_TIMEOUT_S = 300
|
||||
RUN_TIMEOUT_S = 600
|
||||
|
||||
|
||||
def node_env() -> dict[str, str]:
|
||||
"""config/node_modules를 쓰는 프론트엔드 프로세스 환경(main.py와 같은 규약)."""
|
||||
env = os.environ.copy()
|
||||
node_modules = ROOT / "config" / "node_modules"
|
||||
env["PATH"] = f"{node_modules / '.bin'}{os.pathsep}{env.get('PATH', '')}"
|
||||
env["NODE_PATH"] = str(node_modules)
|
||||
return env
|
||||
|
||||
|
||||
def bundle_stale(bundle: Path) -> bool:
|
||||
"""번들이 없거나 TS 원본보다 오래됐으면 참.
|
||||
|
||||
번들이 낡으면 서버와 화면이 **다른 값**을 만든다 — 이 판정이 그것을 막는 유일한
|
||||
장치다. 개발 중에는 `npm run build`를 따로 돌리지 않으므로 여기서 스스로 갱신한다.
|
||||
"""
|
||||
if not bundle.is_file():
|
||||
return True
|
||||
built_at = bundle.stat().st_mtime
|
||||
for directory in SOURCE_DIRS:
|
||||
for path in (ROOT / directory).rglob("*.ts"):
|
||||
if path.stat().st_mtime > built_at:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def build_bundle(npm_script: str) -> bool:
|
||||
result = subprocess.run( # noqa: S602 — 고정 명령, 사용자 입력 없음
|
||||
f"npm run {npm_script}",
|
||||
shell=True,
|
||||
cwd=str(ROOT),
|
||||
env=node_env(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=BUILD_TIMEOUT_S,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error("Node 번들 빌드 실패(%s):\n%s", npm_script, result.stderr)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run_node(bundle: Path, input_path: Path, output_path: Path) -> int:
|
||||
result = subprocess.run( # noqa: S603 — 고정 실행 파일, 인자는 임시 파일 경로뿐
|
||||
["node", str(bundle), str(input_path), str(output_path)],
|
||||
cwd=str(ROOT),
|
||||
env=node_env(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=RUN_TIMEOUT_S,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning(
|
||||
"Node 실행 실패(%s, 끝 코드 %s): %s", bundle.name, result.returncode, result.stderr
|
||||
)
|
||||
return result.returncode
|
||||
|
||||
|
||||
def run_bundle_json(bundle: Path, npm_script: str, payload: dict[str, Any]) -> Any | None:
|
||||
"""입력을 JSON 으로 넘겨 실행하고 결과 JSON 을 돌려준다. 실패는 None.
|
||||
|
||||
결과가 작을 때만 쓸 것 — 코리도처럼 큰 산출물은 파일로 받아 그대로 옮겨야 한다.
|
||||
"""
|
||||
if bundle_stale(bundle) and not build_bundle(npm_script):
|
||||
return None
|
||||
with tempfile.TemporaryDirectory(prefix="node_bundle_") as workdir:
|
||||
source = Path(workdir) / "input.json"
|
||||
result = Path(workdir) / "output.json"
|
||||
source.write_text(json.dumps(payload, default=float), encoding="utf-8")
|
||||
if run_node(bundle, source, result) != 0:
|
||||
return None
|
||||
return json.loads(result.read_text(encoding="utf-8"))
|
||||
+2
-1
@@ -5,8 +5,9 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node ./config/node_modules/vite/bin/vite.js --configLoader runner",
|
||||
"build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:b07-cad",
|
||||
"build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:server-calc && npm run build:b07-cad",
|
||||
"build:corridor": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B05_Profile/B05_Profile_Corridor_Node.ts --outDir ../config/corridor_node",
|
||||
"build:server-calc": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B06_Section/B06_Section_Server_Calc_Node.ts --outDir ../config/server_calc_node",
|
||||
"install:b07-cad": "npm --prefix B07_DesignDetail/openwebcad install",
|
||||
"build:b07-cad": "npm run install:b07-cad && npm --prefix B07_DesignDetail/openwebcad run build",
|
||||
"preview": "node ./config/node_modules/vite/bin/vite.js preview --configLoader runner",
|
||||
|
||||
Reference in New Issue
Block a user