diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_Api_Fetch.ts b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_Api_Fetch.ts new file mode 100644 index 00000000..cfcdb4e1 --- /dev/null +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_Api_Fetch.ts @@ -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(path: string): Promise { + 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 { + const query = stations === undefined ? "" : `?stations=${stations}`; + return requestJson(`/b07/poc/sample-drawing${query}`); +} + +/** PoC 샘플 도면 DXF 다운로드 URL (ezdxf 쓰기 왕복 검증용). */ +export function getPocSampleDrawingDxfUrl(): string { + return `${API_BASE_URL}/b07/poc/sample-drawing/dxf`; +} diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_Router.py b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_Router.py new file mode 100644 index 00000000..721bbd1a --- /dev/null +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_Router.py @@ -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"'}, + ) diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_CadViewer.ts b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_CadViewer.ts new file mode 100644 index 00000000..212b8149 --- /dev/null +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_CadViewer.ts @@ -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; +} diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts index 1be7410e..a2c104ef 100644 --- a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page.ts @@ -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 { - 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(); + } } diff --git a/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Style.css b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Style.css new file mode 100644 index 00000000..f4b810af --- /dev/null +++ b/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Style.css @@ -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; +} diff --git a/config/config_cad_gpl_stub.ts b/config/config_cad_gpl_stub.ts new file mode 100644 index 00000000..79b28b49 --- /dev/null +++ b/config/config_cad_gpl_stub.ts @@ -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); + } +} diff --git a/config/package-lock.json b/config/package-lock.json index 87cef9ca..951309bb 100644 --- a/config/package-lock.json +++ b/config/package-lock.json @@ -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", diff --git a/main.py b/main.py index 6f024925..0a90c515 100644 --- a/main.py +++ b/main.py @@ -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) # ───────────────────────────────────────────────────────────────────────── diff --git a/package-lock.json b/package-lock.json index 58013c94..2823225a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7d17e182..8932fc2a 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/requirements.txt b/requirements.txt index e5f5002c..552d8a45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tsconfig.json b/tsconfig.json index 44c4d72f..90272aae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"], diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index 45bf8b49..972af8f8 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -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"], diff --git a/vite.config.ts b/vite.config.ts index 5d04666f..9d390bf8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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: {