B07 WebCAD 도입 Phase 1 PoC 기초 환경 및 뷰어 셸 구성

This commit is contained in:
2026-07-19 13:27:28 +09:00
parent f63665e581
commit 039c4d0292
14 changed files with 810 additions and 5 deletions
@@ -0,0 +1,71 @@
/* =============================================================================
* B07_wf4_DesignDetail_Api_Fetch.ts
* 4차 워크플로우(상세 설계) API 클라이언트 — WebCAD PoC 임시 계약
*
* 백엔드 계약 (B07_wf4_DesignDetail_Router.py):
* GET /api/b07/poc/sample-drawing → 샘플 종단·횡단 도면 기하 JSON
* GET /api/b07/poc/sample-drawing/dxf → 동일 도면 DXF 다운로드
*
* ⚠️ PoC 전용: Phase 2에서 실제 B06 산출 데이터 계약으로 대체된다.
* 규칙: 오류 응답 {status:"error", message:"..."}을 Error로 변환.
* ========================================================================== */
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
export interface CadLayerJson {
name: string;
color_aci: number;
}
export interface CadEntityJson {
type: "LWPOLYLINE" | "LINE" | "TEXT";
layer: string;
/** LWPOLYLINE */
points?: [number, number][];
closed?: boolean;
/** LINE */
start?: [number, number];
end?: [number, number];
/** TEXT */
text?: string;
insert?: [number, number];
height?: number;
rotation?: number;
}
export interface CadDrawingJson {
layers: CadLayerJson[];
entities: CadEntityJson[];
}
/** 공통 fetch 헬퍼: 타임아웃 + 인증 쿠키 + 오류 응답 변환. */
async function requestJson<T>(path: string): Promise<T> {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), API_TIMEOUT_MS);
try {
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "GET",
credentials: "include",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
});
const payload = (await response.json()) as T & { message?: string };
if (!response.ok) {
throw new Error(payload.message ?? `HTTP ${response.status}`);
}
return payload;
} finally {
window.clearTimeout(timeoutId);
}
}
/** PoC 샘플 도면(종단·횡단 모사)의 기하 JSON을 조회한다. */
export async function fetchPocSampleDrawing(stations?: number): Promise<CadDrawingJson> {
const query = stations === undefined ? "" : `?stations=${stations}`;
return requestJson<CadDrawingJson>(`/b07/poc/sample-drawing${query}`);
}
/** PoC 샘플 도면 DXF 다운로드 URL (ezdxf 쓰기 왕복 검증용). */
export function getPocSampleDrawingDxfUrl(): string {
return `${API_BASE_URL}/b07/poc/sample-drawing/dxf`;
}
@@ -0,0 +1,190 @@
"""B07 상세 설계 FastAPI 라우터 — WebCAD PoC 임시 API.
⚠️ PoC 전용: DB·워크플로우 연동 없음. 종단·횡단도 형태를 모사한 샘플 도면을
ezdxf(MIT)로 생성하여 (1) 기하 JSON, (2) DXF 파일로 제공한다.
브라우저는 DXF를 파싱하지 않고 JSON만 렌더링한다 (GPL 배제 아키텍처).
Phase 2 본 구현 시 실제 B06 산출 데이터 기반으로 대체된다.
"""
import io
import logging
from typing import Any
import ezdxf
from fastapi import APIRouter, Query
from fastapi.responses import Response
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/b07", tags=["B07 Design Detail"])
# PoC 샘플 도면 레이어 구성 (name, ACI 색번호)
_POC_LAYERS: list[tuple[str, int]] = [
("GRID", 8), # 격자 — 회색
("GROUND", 42), # 자연 지반선 — 갈색 계열
("DESIGN", 1), # 계획선 — 빨강
("CENTER", 4), # 중심선 — 하늘색
("LABEL", 7), # 문자 — 흰색/검정
]
_VERTICAL_EXAGGERATION = 2.0
_BASE_ELEVATION = 100.0
def _ground_elevation(station: float) -> float:
"""샘플 자연 지반고(측점 거리 기반 결정적 파형)."""
import math
return 102.0 + station * 0.05 + 1.8 * math.sin(station / 37.0) + 0.6 * math.sin(station / 11.0)
def _design_elevation(station: float) -> float:
"""샘플 계획고(직선 종단 기울기 4%)."""
return 102.5 + station * 0.04
def _profile_y(elevation: float) -> float:
"""표고 → 종단면도 Y 좌표(수직 과장 적용)."""
return (elevation - _BASE_ELEVATION) * _VERTICAL_EXAGGERATION
def _build_sample_doc(stations: int, interval: float) -> "ezdxf.document.Drawing":
"""종단면도 + 횡단면도(3개소)를 모사한 샘플 DXF 문서 생성."""
doc = ezdxf.new("R2010", setup=True)
for name, aci in _POC_LAYERS:
doc.layers.add(name, color=aci)
msp = doc.modelspace()
length = (stations - 1) * interval
ground = [(i * interval, _ground_elevation(i * interval)) for i in range(stations)]
design = [(i * interval, _design_elevation(i * interval)) for i in range(stations)]
top = max(_profile_y(e) for _, e in ground) + 8.0
# --- 종단면도: 격자(측점 세로선 + 기준 가로선) ---
msp.add_lwpolyline(
[(0, 0), (length, 0), (length, top), (0, top)],
close=True,
dxfattribs={"layer": "GRID"},
)
for x, _ in ground:
msp.add_line((x, 0), (x, top), dxfattribs={"layer": "GRID"})
# --- 지반선·계획선 ---
msp.add_lwpolyline([(x, _profile_y(e)) for x, e in ground], dxfattribs={"layer": "GROUND"})
msp.add_lwpolyline([(x, _profile_y(e)) for x, e in design], dxfattribs={"layer": "DESIGN"})
# --- 측점 라벨·표고 문자 ---
for index, (x, elevation) in enumerate(ground):
msp.add_text(
f"No.{index}",
height=1.6,
dxfattribs={"layer": "LABEL", "insert": (x - 2.0, -4.0)},
)
msp.add_text(
f"{elevation:.2f}",
height=1.2,
rotation=90,
dxfattribs={"layer": "LABEL", "insert": (x + 0.5, _profile_y(elevation) + 1.5)},
)
msp.add_text(
"종단면도 (PoC)",
height=3.0,
dxfattribs={"layer": "LABEL", "insert": (length / 2 - 12.0, top + 4.0)},
)
# --- 횡단면도 3개소 (종단면도 아래 배치) ---
section_base_y = -30.0
half_width = 10.0
road_half = 2.0
for section_no in range(3):
cx = section_no * 34.0 + 10.0
design_y = section_base_y + 4.0
points = []
for offset_step in range(-5, 6):
offset = offset_step * (half_width / 5.0)
rel = 0.12 * offset + 0.02 * offset * offset * (1 if section_no % 2 else -1)
points.append((cx + offset, design_y + rel + 1.2))
msp.add_lwpolyline(points, dxfattribs={"layer": "GROUND"})
msp.add_line(
(cx - road_half, design_y),
(cx + road_half, design_y),
dxfattribs={"layer": "DESIGN"},
)
msp.add_line(
(cx, design_y - 2.5),
(cx, design_y + 4.5),
dxfattribs={"layer": "CENTER"},
)
msp.add_text(
f"No.{section_no} 횡단",
height=1.4,
dxfattribs={"layer": "LABEL", "insert": (cx - 4.0, section_base_y - 3.0)},
)
return doc
def _doc_to_geometry_json(doc: "ezdxf.document.Drawing") -> dict[str, Any]:
"""DXF 문서 → 뷰어 렌더링용 기하 JSON (LWPOLYLINE/LINE/TEXT만 사용)."""
entities: list[dict[str, Any]] = []
for entity in doc.modelspace():
kind = entity.dxftype()
if kind == "LWPOLYLINE":
entities.append(
{
"type": "LWPOLYLINE",
"layer": entity.dxf.layer,
"points": [[round(x, 4), round(y, 4)] for x, y, *_ in entity.get_points()],
"closed": bool(entity.closed),
}
)
elif kind == "LINE":
entities.append(
{
"type": "LINE",
"layer": entity.dxf.layer,
"start": [round(entity.dxf.start.x, 4), round(entity.dxf.start.y, 4)],
"end": [round(entity.dxf.end.x, 4), round(entity.dxf.end.y, 4)],
}
)
elif kind == "TEXT":
entities.append(
{
"type": "TEXT",
"layer": entity.dxf.layer,
"text": entity.dxf.text,
"insert": [round(entity.dxf.insert.x, 4), round(entity.dxf.insert.y, 4)],
"height": entity.dxf.height,
"rotation": entity.dxf.rotation,
}
)
return {
"layers": [{"name": name, "color_aci": aci} for name, aci in _POC_LAYERS],
"entities": entities,
}
@router.get("/poc/sample-drawing")
async def get_poc_sample_drawing(
stations: int = Query(default=11, ge=2, le=2000),
interval: float = Query(default=20.0, gt=0, le=100.0),
) -> dict[str, Any]:
"""샘플 종단·횡단 도면의 기하 JSON을 반환한다 (stations 증가로 부하 테스트 가능)."""
doc = _build_sample_doc(stations, interval)
payload = _doc_to_geometry_json(doc)
logger.info("B07 PoC sample drawing generated: %d entities", len(payload["entities"]))
return payload
@router.get("/poc/sample-drawing/dxf")
async def download_poc_sample_drawing(
stations: int = Query(default=11, ge=2, le=2000),
interval: float = Query(default=20.0, gt=0, le=100.0),
) -> Response:
"""동일 샘플 도면을 DXF 파일로 반환한다 (ezdxf 쓰기 왕복 검증용)."""
doc = _build_sample_doc(stations, interval)
buffer = io.StringIO()
doc.write(buffer)
return Response(
content=buffer.getvalue().encode("utf-8"),
media_type="application/dxf",
headers={"Content-Disposition": 'attachment; filename="b07_poc_sample.dxf"'},
)
@@ -0,0 +1,119 @@
/* =============================================================================
* B07_wf4_DesignDetail_UI_CadViewer.ts
* WebCAD PoC 뷰어 — cad-simple-viewer(MIT) 부트스트랩 + 기하 JSON 렌더링
*
* 아키텍처 (PLAN.md WebCAD 합의):
* - 브라우저는 DXF/DWG를 파싱하지 않는다. GPL 컨버터는 vite 별칭 스텁으로 차단.
* - 서버(ezdxf)가 보낸 기하 JSON을 data-model(MIT) API로 도면 DB에 직접 구성.
* - AcApDocManager는 싱글톤이므로 뷰어 컨테이너를 모듈 수준에 유지하고
* 페이지 재진입 시 새 레이아웃에 재부착한다.
* ========================================================================== */
import { AcApDocManager } from "@mlightcad/cad-simple-viewer";
import {
AcCmColor,
AcDbLayerTableRecord,
AcDbLine,
AcDbPolyline,
AcDbText,
AcGePoint2d,
AcGePoint3d,
} from "@mlightcad/data-model";
import type { CadDrawingJson, CadEntityJson } from "./B07_wf4_DesignDetail_Api_Fetch";
/** 뷰어 캔버스를 담는 영속 컨테이너 (SPA 라우팅 간 유지) */
let viewerHolder: HTMLDivElement | null = null;
/** 도면 JSON이 이미 DB에 적재되었는지 여부 (PoC: 1회 적재) */
let drawingLoaded = false;
function ensureViewerHolder(): HTMLDivElement {
if (!viewerHolder) {
viewerHolder = document.createElement("div");
viewerHolder.className = "b07-cad-canvas";
}
return viewerHolder;
}
function ensureDocManager(holder: HTMLDivElement): AcApDocManager {
try {
return AcApDocManager.instance;
} catch {
// 최초 1회 생성 — 워커 미배포 환경이므로 MTEXT는 메인 스레드 렌더링 사용.
AcApDocManager.createInstance({
container: holder,
autoResize: true,
useMainThreadDraw: true,
});
return AcApDocManager.instance;
}
}
function toEntity(json: CadEntityJson): AcDbLine | AcDbPolyline | AcDbText | null {
if (json.type === "LWPOLYLINE" && json.points && json.points.length >= 2) {
const polyline = new AcDbPolyline();
json.points.forEach(([x, y], index) => {
polyline.addVertexAt(index, new AcGePoint2d(x, y));
});
polyline.closed = json.closed ?? false;
polyline.layer = json.layer;
return polyline;
}
if (json.type === "LINE" && json.start && json.end) {
const line = new AcDbLine(
new AcGePoint3d(json.start[0], json.start[1], 0),
new AcGePoint3d(json.end[0], json.end[1], 0),
);
line.layer = json.layer;
return line;
}
if (json.type === "TEXT" && json.text && json.insert) {
const text = new AcDbText();
text.textString = json.text;
text.position = new AcGePoint3d(json.insert[0], json.insert[1], 0);
text.height = json.height ?? 2.5;
text.rotation = ((json.rotation ?? 0) * Math.PI) / 180;
text.layer = json.layer;
return text;
}
return null;
}
function loadDrawingIntoDatabase(manager: AcApDocManager, drawing: CadDrawingJson): number {
const db = manager.curDocument.database;
db.createDefaultData();
for (const layerJson of drawing.layers) {
const color = new AcCmColor();
color.colorIndex = layerJson.color_aci;
db.tables.layerTable.add(new AcDbLayerTableRecord({ name: layerJson.name, color }));
}
const modelSpace = db.tables.blockTable.modelSpace;
let appended = 0;
for (const entityJson of drawing.entities) {
const entity = toEntity(entityJson);
if (entity) {
modelSpace.appendEntity(entity);
appended += 1;
}
}
return appended;
}
/**
* PoC CAD 뷰어를 host에 부착하고 서버 기하 JSON을 렌더링한다.
* @returns 적재된 엔티티 수 (재진입 시 0 — 이미 적재됨)
*/
export function mountPocCadViewer(host: HTMLElement, drawing: CadDrawingJson): number {
const holder = ensureViewerHolder();
host.append(holder);
const manager = ensureDocManager(holder);
let appended = 0;
if (!drawingLoaded) {
appended = loadDrawingIntoDatabase(manager, drawing);
drawingLoaded = true;
}
manager.curView.zoomToFitDrawing();
return appended;
}
@@ -1,26 +1,90 @@
/* =============================================================================
* B07_wf4_DesignDetail_UI_Page.ts
* 로그인 후 07: 4차 워크플로우 (상세 설계)
* 로그인 후 07: 4차 워크플로우 (상세 설계) — WebCAD PoC
*
* ⚠️ 본문 준비 중 — 워크플로우 셸(헤더+스텝바)만 구성. 추후 구체화.
* 레이아웃 (PLAN.md WebCAD 사용자 지시):
* - 사이드 패널: 빈 상태 유지 (B06→B07 전달 데이터 스펙 미확정)
* - 상세 페이지 영역: CAD 뷰어 (서버 기하 JSON 렌더링, DXF 파싱 없음)
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowLayout 재사용.
* ========================================================================== */
import "./B07_wf4_DesignDetail_UI_Style.css";
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { renderPendingWorkflow, workflowSteps } from "../A00_Common/b_page_scaffold";
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
import { workflowSteps } from "../A00_Common/b_page_scaffold";
import {
fetchWorkflowState,
goToWorkflowStage,
WORKFLOW_STEP_ROUTES,
type WorkflowState,
} from "../A00_Common/b_workflow_nav";
import { fetchPocSampleDrawing } from "./B07_wf4_DesignDetail_Api_Fetch";
import { mountPocCadViewer } from "./B07_wf4_DesignDetail_UI_CadViewer";
/** locale 헬퍼 */
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
/** 빈 사이드 패널 (전달 데이터 확정 후 구성 예정) */
function buildEmptySidePanel(): HTMLDivElement {
const panel = document.createElement("div");
panel.className = "b07-side-empty";
const icon = document.createElement("div");
icon.className = "b07-side-empty__icon";
icon.textContent = "🏗️";
const msg = document.createElement("p");
msg.className = "b07-side-empty__msg";
msg.textContent = L("B07_Cad_Side_Pending");
panel.append(icon, msg);
return panel;
}
/* -----------------------------------------------------------------------------
* 페이지 진입점
* -------------------------------------------------------------------------- */
export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
await renderPendingWorkflow(root, {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
let workflowState: WorkflowState | undefined;
if (projectId) {
try {
workflowState = await fetchWorkflowState(projectId);
} catch {
/* 조회 실패 시 stages 미전달 → 전체 이동 허용 */
}
}
const cadHost = document.createElement("div");
cadHost.className = "b07-cad-host";
const status = document.createElement("div");
status.className = "b07-cad-status";
status.textContent = L("B07_Cad_Loading");
cadHost.append(status);
const layout = createWorkflowLayout({
title: L("B07_Design_Title"),
steps: workflowSteps(),
activeStep: 4,
leftPanel: buildEmptySidePanel(),
mainContent: cadHost,
stages: workflowState?.stages,
currentStage: workflowState?.current_stage,
routes: WORKFLOW_STEP_ROUTES,
onStepClick: (stepIndex) => {
if (projectId) goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
},
});
layout.root.classList.add("b07-design-layout");
root.replaceChildren(layout.root);
try {
const drawing = await fetchPocSampleDrawing();
mountPocCadViewer(cadHost, drawing);
status.remove();
} catch (error) {
status.textContent = `${L("B07_Cad_Load_Failed")} ${
error instanceof Error ? error.message : ""
}`.trim();
}
}
@@ -0,0 +1,74 @@
/* =============================================================================
* B07_wf4_DesignDetail_UI_Style.css
* 상세 설계(WebCAD PoC) 화면 스타일 — theme.css 변수만 사용
* ========================================================================== */
/* 워크플로우 레이아웃 높이 (B06 패턴 준수) */
.b07-design-layout {
height: calc(100vh - var(--spacing-64));
height: calc(100dvh - var(--spacing-64));
min-height: 0;
}
.b07-design-layout .ui-workflow-layout__body,
.b07-design-layout .ui-workflow-layout__main {
height: 100%;
min-height: 0;
}
/* 빈 사이드 패널 (B06 전달 데이터 확정 후 구성 예정) */
.b07-side-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-16);
height: 100%;
min-height: 200px;
padding: var(--spacing-24);
border: 1px dashed var(--color-border);
border-radius: var(--radius-cards);
color: var(--color-text-muted);
text-align: center;
}
.b07-side-empty__icon {
font-size: 32px;
line-height: 1;
}
.b07-side-empty__msg {
font-size: var(--text-body-sm);
}
/* CAD 뷰어 호스트 (상세 페이지 영역) */
.b07-cad-host {
position: relative;
width: 100%;
height: 100%;
min-height: 420px;
overflow: hidden;
border-radius: var(--radius-cards);
background-color: var(--color-surface);
}
.b07-cad-canvas {
position: absolute;
inset: 0;
}
.b07-cad-canvas canvas {
display: block;
}
/* 로딩·오류 상태 메시지 */
.b07-cad-status {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
font-size: var(--text-body-sm);
pointer-events: none;
}
+28
View File
@@ -0,0 +1,28 @@
/* =============================================================================
* config_cad_gpl_stub.ts
* GPL 차단 스텁 — @mlightcad/dxf-json-converter / @mlightcad/libredwg-converter 대체
*
* cad-simple-viewer(MIT)는 브라우저에서 DXF/DWG 파일을 직접 파싱하기 위해
* GPL-3.0 컨버터 패키지를 import 하지만, 상용 배포본에 GPL 코드를 포함하지 않기
* 위해 vite.config.ts 별칭으로 이 스텁이 대신 번들된다.
*
* 도면 파싱은 전적으로 백엔드(ezdxf, MIT)가 수행하고 브라우저는 기하 JSON만
* 받으므로, 이 컨버터가 호출될 일은 없다. cad-simple-viewer의 registerConverters()는
* 생성자 예외를 try/catch로 흡수하므로 뷰어 코어는 정상 동작한다.
* ========================================================================== */
const GPL_STUB_MESSAGE =
"브라우저 내 DXF/DWG 파싱은 비활성화되어 있습니다 (GPL-3.0 패키지 제외). " +
"도면은 서버(ezdxf)에서 JSON으로 변환되어 전달됩니다.";
export class AcDbDxfConverter {
constructor() {
throw new Error(GPL_STUB_MESSAGE);
}
}
export class AcDbLibreDwgConverter {
constructor() {
throw new Error(GPL_STUB_MESSAGE);
}
}
+118
View File
@@ -8,6 +8,9 @@
"name": "forest-road-webapp",
"version": "0.1.0",
"dependencies": {
"@mlightcad/cad-simple-viewer": "^1.5.7",
"@mlightcad/data-model": "~1.10.7",
"lodash-es": "4.17.21",
"maplibre-gl": "^5.24.0",
"three": "^0.185.0"
},
@@ -60,6 +63,15 @@
"tslib": "^2.4.0"
}
},
"node_modules/@lukeed/csprng": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
"integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz",
@@ -173,6 +185,59 @@
"pbf": "bin/pbf"
}
},
"node_modules/@mlightcad/cad-simple-viewer": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@mlightcad/cad-simple-viewer/-/cad-simple-viewer-1.5.7.tgz",
"integrity": "sha512-6I65HubIkzb1oNJ7k2bF+v9mwnfp2qKUsezLp0ksPbWC5de3+tgu20qK/xB/GXyfl9JnG8NDUIXWtCgOQ+bVsg==",
"license": "MIT",
"peerDependencies": {
"@mlightcad/data-model": "^1.10.0",
"@mlightcad/dxf-json-converter": "^1.10.0",
"lodash-es": "4.17.21",
"three": "^0.172.0"
}
},
"node_modules/@mlightcad/common": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/common/-/common-1.10.7.tgz",
"integrity": "sha512-wCDbuE+qdqayDJnlxsGT0eSLo+ORpWPHqezDTHzo4NqFmC7U1GwQuHuKOjtqq5xYfPDm0C6MUqEFVaW8n85gwQ==",
"license": "MIT",
"dependencies": {
"loglevel": "^1.9.1"
}
},
"node_modules/@mlightcad/data-model": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/data-model/-/data-model-1.10.7.tgz",
"integrity": "sha512-5dDzrs9SYbZVvHhfAc/+cd+ZSlXokf8JA/+MKjjmp75TTeBrYIDFv1fXiNqiz/ULU18h3bG9qoS9uFIhBBVkAg==",
"license": "MIT",
"dependencies": {
"@mlightcad/common": "1.10.7",
"@mlightcad/geometry-engine": "3.10.7",
"@mlightcad/graphic-interface": "3.10.7",
"iconv-lite": "^0.7.0",
"uid": "^2.0.2"
}
},
"node_modules/@mlightcad/geometry-engine": {
"version": "3.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/geometry-engine/-/geometry-engine-3.10.7.tgz",
"integrity": "sha512-/Eje+NphvACTiKTDTfydUYp284udQ43rEUcpY2PUfmAXh0YiGGYodMLpPjX5G0VinhSQy+X+/F4q+h2Xn99Ehg==",
"license": "MIT",
"peerDependencies": {
"@mlightcad/common": "1.10.7"
}
},
"node_modules/@mlightcad/graphic-interface": {
"version": "3.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/graphic-interface/-/graphic-interface-3.10.7.tgz",
"integrity": "sha512-Yt3v4wg+k5RxOzMcbdKHNlLFcXuTX+wDWlm8UpydjHEcrc0RGzTC3sccecEEit53h/9fRAjId2qsEpl5wL3D8g==",
"license": "MIT",
"peerDependencies": {
"@mlightcad/common": "1.10.7",
"@mlightcad/geometry-engine": "3.10.7"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
@@ -591,6 +656,22 @@
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
@@ -864,6 +945,25 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/maplibre-gl": {
"version": "5.24.0",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz",
@@ -1077,6 +1177,12 @@
"@rolldown/binding-win32-x64-msvc": "1.1.4"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1138,6 +1244,18 @@
"node": ">=14.17"
}
},
"node_modules/uid": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz",
"integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==",
"license": "MIT",
"dependencies": {
"@lukeed/csprng": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+2
View File
@@ -33,6 +33,7 @@ from B04_wf1_Surface.B04_wf1_Surface_Router_GIS import router as b04_surface_gis
from B04_wf1_Surface.B04_wf1_Surface_Router_GIS import tiles_router
from B05_wf2_Route.B05_wf2_Route_Router import router as b05_route_router
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Router import router as b06_section_router
from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Router import router as b07_design_router
from common_util.common_util_auth import require_company, verify_session
from common_util.common_util_resource_monitor import sample_resources_loop
from config.config_db import close_db_pool, get_db_pool, init_db_pool
@@ -265,6 +266,7 @@ app.include_router(b04_surface_gis_router, dependencies=protected_with_company)
app.include_router(tiles_router, dependencies=protected_with_company)
app.include_router(b05_route_router, dependencies=protected_with_company)
app.include_router(b06_section_router, dependencies=protected_with_company)
app.include_router(b07_design_router, dependencies=protected_with_company)
# ─────────────────────────────────────────────────────────────────────────
+118
View File
@@ -8,6 +8,9 @@
"name": "forest-road-webapp",
"version": "0.1.0",
"dependencies": {
"@mlightcad/cad-simple-viewer": "^1.5.7",
"@mlightcad/data-model": "~1.10.7",
"lodash-es": "4.17.21",
"maplibre-gl": "^5.24.0",
"three": "^0.185.0"
},
@@ -24,6 +27,15 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@lukeed/csprng": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
"integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.3",
"license": "MIT",
@@ -113,6 +125,59 @@
"pbf": "bin/pbf"
}
},
"node_modules/@mlightcad/cad-simple-viewer": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@mlightcad/cad-simple-viewer/-/cad-simple-viewer-1.5.7.tgz",
"integrity": "sha512-6I65HubIkzb1oNJ7k2bF+v9mwnfp2qKUsezLp0ksPbWC5de3+tgu20qK/xB/GXyfl9JnG8NDUIXWtCgOQ+bVsg==",
"license": "MIT",
"peerDependencies": {
"@mlightcad/data-model": "^1.10.0",
"@mlightcad/dxf-json-converter": "^1.10.0",
"lodash-es": "4.17.21",
"three": "^0.172.0"
}
},
"node_modules/@mlightcad/common": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/common/-/common-1.10.7.tgz",
"integrity": "sha512-wCDbuE+qdqayDJnlxsGT0eSLo+ORpWPHqezDTHzo4NqFmC7U1GwQuHuKOjtqq5xYfPDm0C6MUqEFVaW8n85gwQ==",
"license": "MIT",
"dependencies": {
"loglevel": "^1.9.1"
}
},
"node_modules/@mlightcad/data-model": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/data-model/-/data-model-1.10.7.tgz",
"integrity": "sha512-5dDzrs9SYbZVvHhfAc/+cd+ZSlXokf8JA/+MKjjmp75TTeBrYIDFv1fXiNqiz/ULU18h3bG9qoS9uFIhBBVkAg==",
"license": "MIT",
"dependencies": {
"@mlightcad/common": "1.10.7",
"@mlightcad/geometry-engine": "3.10.7",
"@mlightcad/graphic-interface": "3.10.7",
"iconv-lite": "^0.7.0",
"uid": "^2.0.2"
}
},
"node_modules/@mlightcad/geometry-engine": {
"version": "3.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/geometry-engine/-/geometry-engine-3.10.7.tgz",
"integrity": "sha512-/Eje+NphvACTiKTDTfydUYp284udQ43rEUcpY2PUfmAXh0YiGGYodMLpPjX5G0VinhSQy+X+/F4q+h2Xn99Ehg==",
"license": "MIT",
"peerDependencies": {
"@mlightcad/common": "1.10.7"
}
},
"node_modules/@mlightcad/graphic-interface": {
"version": "3.10.7",
"resolved": "https://registry.npmjs.org/@mlightcad/graphic-interface/-/graphic-interface-3.10.7.tgz",
"integrity": "sha512-Yt3v4wg+k5RxOzMcbdKHNlLFcXuTX+wDWlm8UpydjHEcrc0RGzTC3sccecEEit53h/9fRAjId2qsEpl5wL3D8g==",
"license": "MIT",
"peerDependencies": {
"@mlightcad/common": "1.10.7",
"@mlightcad/geometry-engine": "3.10.7"
}
},
"node_modules/@oxc-project/types": {
"version": "0.137.0",
"dev": true,
@@ -220,6 +285,22 @@
"version": "3.4.4",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"license": "MIT"
@@ -275,6 +356,25 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/maplibre-gl": {
"version": "5.24.0",
"license": "BSD-3-Clause",
@@ -460,6 +560,12 @@
"@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"dev": true,
@@ -503,6 +609,18 @@
"node": ">=14.17"
}
},
"node_modules/uid": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz",
"integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==",
"license": "MIT",
"dependencies": {
"@lukeed/csprng": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+3
View File
@@ -11,6 +11,9 @@
"format": "node ./config/node_modules/prettier/bin/prettier.cjs --write \"../**/*.{ts,css,html}\""
},
"dependencies": {
"@mlightcad/cad-simple-viewer": "^1.5.7",
"@mlightcad/data-model": "~1.10.7",
"lodash-es": "4.17.21",
"maplibre-gl": "^5.24.0",
"three": "^0.185.0"
},
+1
View File
@@ -9,6 +9,7 @@ click-plugins==1.1.1.2
cligj==0.7.2
colorama==0.4.6
et_xmlfile==2.0.0
ezdxf==1.4.4
fastapi==0.104.1
bcrypt>=4.1,<5
email-validator>=2.1,<3
+3 -1
View File
@@ -29,7 +29,9 @@
"@util/*": ["common_util/*"],
"three": ["config/node_modules/@types/three"],
"three/*": ["config/node_modules/@types/three/*"],
"maplibre-gl": ["config/node_modules/maplibre-gl"]
"maplibre-gl": ["config/node_modules/maplibre-gl"],
"@mlightcad/cad-simple-viewer": ["config/node_modules/@mlightcad/cad-simple-viewer"],
"@mlightcad/data-model": ["config/node_modules/@mlightcad/data-model"]
}
},
"include": ["A00_Common", "A0*", "B0*", "B1*", "ui_template", "config/**/*", "common_util"],
+6
View File
@@ -799,6 +799,12 @@ export const ui_locales = {
/* --- B07_wf4_DesignDetail 상세 설계 --- */
B07_Design_Title: ["4차 · 상세 설계", "Step 4 · Detailed Design"],
B07_Cad_Side_Pending: [
"사이드 패널은 전달 데이터 확정 후 구성됩니다.",
"Side panel will be configured after the upstream data spec is finalized.",
],
B07_Cad_Loading: ["도면을 불러오는 중...", "Loading drawing..."],
B07_Cad_Load_Failed: ["도면을 불러오지 못했습니다.", "Failed to load drawing."],
/* --- B08_wf5_Quantity 수량 산출 --- */
B08_Quantity_Title: ["5차 · 수량 산출", "Step 5 · Quantity Takeoff"],
+9
View File
@@ -21,6 +21,15 @@ export default {
"@util": resolve(__dirname, "./common_util"),
three: resolve(__dirname, "./config/node_modules/three"),
"maplibre-gl": resolve(__dirname, "./config/node_modules/maplibre-gl"),
// WebCAD(B07) — GPL 컨버터는 스텁으로 차단, MIT 패키지만 실제 번들
"@mlightcad/dxf-json-converter": resolve(__dirname, "./config/config_cad_gpl_stub.ts"),
"@mlightcad/libredwg-converter": resolve(__dirname, "./config/config_cad_gpl_stub.ts"),
"@mlightcad/cad-simple-viewer": resolve(
__dirname,
"./config/node_modules/@mlightcad/cad-simple-viewer",
),
"@mlightcad/data-model": resolve(__dirname, "./config/node_modules/@mlightcad/data-model"),
"lodash-es": resolve(__dirname, "./config/node_modules/lodash-es"),
},
},
server: {