- 판 calc_sites.json = route · stations · profile · cross 모두 both · 열린 줄 = 네 줄 모두 · 잠긴 단계 줄 = follow(pair_B5) 하나
- Node 입구 design_calc_node.ts = 입력 꼴 · runner 표 양쪽(cross · sections · route · profile) · NODE_IDS = facility.attach · cross.enforce · stations.list · stations.sections · route.polyline · stations.plan
- /calc 창구 Router_Calc = 서버 측점 생성 거절 · 경로 칸 거절 · {model_id} 격자 서버가 정함(B-2 · 경로 막기) 뒤 단계 · 화면 길 서버 맥락(B-4) — 단계 격자 칸이 없을 때만 설계 지형 격자로 채움
- 시험 309_2 · 309_2_browser · 472_3 · 511_b1 = 양쪽 열린 줄 합 · 잠금 예시 = follow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
6.6 KiB
Python
145 lines
6.6 KiB
Python
"""계산 자리 「서버」 창구(PLAN 511-A · 요구 320-4) — 조작 중 계산을 서버가 같은 TS 로.
|
|
|
|
POST /api/projects/{id}/calc {run: [id…], inputs: {…}} → {results, ms, wall_ms}
|
|
|
|
브라우저가 계산 자리 판(`calc_sites.json`)에서 묶음을 「서버」로 둔 때만 부름
|
|
(`design_calc_server.ts`).
|
|
권한 = 프로젝트 접근(`protected_with_company`) · 호출량 = `main.py` 미들웨어 · 본문 상한 `MAX_BODY`.
|
|
파일 경로 = 서버가 프로젝트 id 로 정함(그 storage 안 DTM 격자) · 입력에 경로 칸 = 400.
|
|
실행 = 늘 떠 있는 Node 하나(`common_util_calc_runner.run_calc_served`) ·
|
|
정본 저장 길은 이 창구를 안 씀.
|
|
단계(`stage.*`) · 화면 길(`cross.refresh`) = 서버가 DTM 격자 경로 · 구조물 정의를 채움 —
|
|
화면이 보낸 경로는 버림(파일 경로를 화면에 맡기지 않음 · DTM 아닌 지표면 = 격자 없음 · 511-B B-4).
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from common_util.common_util_calc_runner import (
|
|
PAGE_IDS,
|
|
SERVER_ONLY_IDS,
|
|
STAGE_IDS,
|
|
run_calc_served,
|
|
)
|
|
from config.config_db import get_db_pool
|
|
|
|
router = APIRouter(prefix="/api/projects", tags=["B06 Calc Server"])
|
|
|
|
#: 본문 상한 — 종횡단 상세 한 벌(1.3~1.5 MB) 여유 · 넘으면 413.
|
|
MAX_BODY = 8 * 1024 * 1024
|
|
|
|
|
|
def _error(status: int, message: str) -> JSONResponse:
|
|
return JSONResponse(status_code=status, content={"status": "error", "message": message})
|
|
|
|
|
|
async def _server_grid(project_id: UUID, inputs: dict) -> str | None:
|
|
"""파일 경로는 브라우저 값을 안 씀 — 단계 격자 = `{model_id, smooth, design}` → 이 프로젝트
|
|
storage 안 DTM 격자 경로(서버가 정함) · 경로 칸(`stages.grid.path` · `stations.grid`) = 거절."""
|
|
stations = inputs.get("stations")
|
|
if isinstance(stations, dict) and "grid" in stations:
|
|
return "격자 경로는 서버가 정함"
|
|
stages = inputs.get("stages")
|
|
grid = stages.get("grid") if isinstance(stages, dict) else None
|
|
if grid is None:
|
|
return None
|
|
if not isinstance(grid, dict) or "path" in grid:
|
|
return "격자 경로는 서버가 정함 — {model_id, smooth, design}"
|
|
model_id, design = grid.get("model_id"), grid.get("design", "las")
|
|
if (
|
|
not isinstance(model_id, int)
|
|
or isinstance(model_id, bool)
|
|
or design not in ("las", "sheet")
|
|
):
|
|
return "격자 = {model_id: 정수, smooth, design: las | sheet}"
|
|
from B05_Profile.B05_Profile_Router_Terrain import dtm_grid_npz
|
|
from common_util.common_util_surface_sampler import dtm_grid_file
|
|
|
|
async with get_db_pool().acquire() as connection:
|
|
try:
|
|
npz = await dtm_grid_npz(connection, project_id, model_id, bool(grid.get("smooth")))
|
|
except LookupError as exc:
|
|
return str(exc)
|
|
path = await asyncio.to_thread(dtm_grid_file, npz)
|
|
stages["grid"] = {"path": str(path), "design": design}
|
|
return None
|
|
|
|
|
|
@router.post("/{project_id}/calc", response_model=None)
|
|
async def run_calc_endpoint(project_id: UUID, request: Request) -> dict | JSONResponse:
|
|
declared = request.headers.get("content-length")
|
|
if declared and declared.isdigit() and int(declared) > MAX_BODY:
|
|
return _error(413, "계산 입력이 너무 큼")
|
|
body = b""
|
|
async for chunk in request.stream():
|
|
body += chunk
|
|
if len(body) > MAX_BODY:
|
|
return _error(413, "계산 입력이 너무 큼")
|
|
try:
|
|
payload = json.loads(body)
|
|
except ValueError:
|
|
return _error(400, "JSON 아님")
|
|
run = payload.get("run") if isinstance(payload, dict) else None
|
|
inputs = payload.get("inputs", {}) if isinstance(payload, dict) else None
|
|
if not isinstance(run, list) or not all(isinstance(i, str) for i in run):
|
|
return _error(400, "run = id 목록")
|
|
if not isinstance(inputs, dict):
|
|
return _error(400, "inputs = 객체")
|
|
if SERVER_ONLY_IDS.intersection(run):
|
|
return _error(400, "서버 측점 생성은 창구 밖")
|
|
problem = await _server_grid(project_id, inputs)
|
|
if problem:
|
|
return _error(400, problem)
|
|
if set(run) & (STAGE_IDS | PAGE_IDS):
|
|
await _server_context(project_id, inputs)
|
|
if {"stations.list", "stations.plan"} & set(run) and isinstance(inputs.get("stations"), dict):
|
|
# 심는 측점 · 카드 이름 표 = 서버 구조물 정의(서버 측점 생성과 같은 표 · 511-B B-2)
|
|
from B05_Profile.B05_Profile_Engine_Sections_Node import structure_table
|
|
|
|
inputs["stations"]["definition"] = (await asyncio.to_thread(structure_table))[1]
|
|
try:
|
|
answer = await asyncio.to_thread(run_calc_served, run, inputs)
|
|
except ValueError as exc:
|
|
return _error(400, str(exc))
|
|
if answer is None:
|
|
return _error(503, "서버 계산을 못 돌림")
|
|
if "error" in answer:
|
|
return _error(422, str(answer["error"]))
|
|
return {"status": "success", "project_id": str(project_id), **answer}
|
|
|
|
|
|
async def _server_context(project_id: UUID, inputs: dict) -> None:
|
|
"""단계 · 화면 길 입력에 서버 맥락을 채움 — DTM 격자 이진 경로(없으면 None) ·
|
|
구조물 정의(M02 반영)."""
|
|
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
|
from B05_Profile.B05_Profile_Structures_Schema import load_definition
|
|
from B06_Section.B06_Section_Server_Calc_Prebuild import design_grid_file
|
|
from common_util.common_util_storage import resolve_stored_project_path
|
|
from common_util.common_util_surface_confirmation import (
|
|
design_terrain,
|
|
get_surface_confirmation_params,
|
|
)
|
|
from config.config_db import run_with_connection
|
|
|
|
inputs["definition"] = load_definition()
|
|
stages = inputs.get("stages")
|
|
# 단계 격자 = 화면이 `{model_id…}` 로 보낸 때는 `_server_grid` 가 정함 — 칸이 없을 때만 채움
|
|
fill_stages = isinstance(stages, dict) and "grid" not in stages
|
|
if not fill_stages and not isinstance(inputs.get("cross_refresh"), dict):
|
|
return
|
|
stored, params = await asyncio.gather(
|
|
run_with_connection(get_project_storage_relative_path, project_id),
|
|
run_with_connection(get_surface_confirmation_params, str(project_id)),
|
|
)
|
|
root = Path(resolve_stored_project_path(stored))
|
|
grid = await asyncio.to_thread(design_grid_file, root, params)
|
|
if fill_stages:
|
|
stages["grid"] = {"path": str(grid), "design": design_terrain(params)} if grid else None
|
|
if isinstance(inputs.get("cross_refresh"), dict):
|
|
inputs["cross_refresh"]["grid"] = str(grid) if grid else None
|