refactor(B07,B08): 도면 코드 B08_DesignDetail 이동 + 수량 슬롯 B07_Quantity 확보
- B07_wf4_DesignDetail(8파일+openwebcad) -> B08_DesignDetail로 git mv (이력 보존) - B08_wf5_Quantity -> B07_Quantity (구 수량 백업 zip 보관 폴더) - 파생 문자열 일괄 전환: /b07-cad -> /b08-cad 정적 서빙, CAD 레이어 b07-* -> b08-*, postMessage aislo:b07:* -> aislo:b08:*, 패키지명 aislo-b08-cad, CSS .b08-*, 라우트 키/슬러그(B08_DESIGN_DETAIL/b08-design-detail, B07_QUANTITY/b07-quantity) - 상세설계 워크플로우 stage 4 -> 5 (확정/무효화 전이 3곳), 확정 완료 시 B09로 이동 - WORKFLOW_STEP_ROUTES 순서 재배열: index4=수량(B07), index5=상세설계(B08) - [임시] B08 이동 테스트 버튼 제거, openwebcad dist 재빌드 - 주석 의미 정렬: 수량 인계 주석 B08->B07, 도면 참조 B07->B08 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -20,8 +20,8 @@ export const WORKFLOW_STEP_ROUTES: readonly RoutePath[] = [
|
||||
ROUTES.B04_PREPROCESS,
|
||||
ROUTES.B05_PROFILE,
|
||||
ROUTES.B06_SECTION,
|
||||
ROUTES.B07_WF4_DESIGN_DETAIL,
|
||||
ROUTES.B08_WF5_QUANTITY,
|
||||
ROUTES.B07_QUANTITY,
|
||||
ROUTES.B08_DESIGN_DETAIL,
|
||||
ROUTES.B09_WF6_ESTIMATION,
|
||||
];
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ const FOOTERLESS_ROUTES: readonly RoutePath[] = [
|
||||
ROUTES.B04_PREPROCESS,
|
||||
ROUTES.B05_PROFILE,
|
||||
ROUTES.B06_SECTION,
|
||||
ROUTES.B07_WF4_DESIGN_DETAIL,
|
||||
ROUTES.B08_WF5_QUANTITY,
|
||||
ROUTES.B07_QUANTITY,
|
||||
ROUTES.B08_DESIGN_DETAIL,
|
||||
ROUTES.B09_WF6_ESTIMATION,
|
||||
ROUTES.B10_PAYMENT,
|
||||
ROUTES.B11_STATUS,
|
||||
|
||||
@@ -45,10 +45,10 @@ const routeTable: Partial<Record<RoutePath, () => Promise<PageRenderer>>> = {
|
||||
(await import("../B05_Profile/B05_Profile_UI_Page")).renderB05Route,
|
||||
[ROUTES.B06_SECTION]: async () =>
|
||||
(await import("../B06_Section/B06_Section_UI_Page")).renderB06ProfileCross,
|
||||
[ROUTES.B07_WF4_DESIGN_DETAIL]: async () =>
|
||||
(await import("../B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_Page")).renderB07DesignDetail,
|
||||
// B08_WF5_QUANTITY: 재작성 예정으로 기존 구현을 0_old_260726_codex.zip에 백업 후 제거.
|
||||
// B07_QUANTITY: 재작성 예정으로 기존 구현을 0_old_260726_codex.zip에 백업 후 제거.
|
||||
// 등록을 빼두면 renderPlaceholder가 "준비 중" 화면을 띄운다. 새 페이지 완성 시 여기에 다시 추가.
|
||||
[ROUTES.B08_DESIGN_DETAIL]: async () =>
|
||||
(await import("../B08_DesignDetail/B08_DesignDetail_UI_Page")).renderB08DesignDetail,
|
||||
[ROUTES.B09_WF6_ESTIMATION]: async () =>
|
||||
(await import("../B09_wf6_Estimation/B09_wf6_Estimation_UI_Page")).renderB09Estimation,
|
||||
[ROUTES.B10_PAYMENT]: async () =>
|
||||
@@ -109,8 +109,8 @@ export async function renderCurrentRoute(outlet: HTMLElement): Promise<void> {
|
||||
ROUTES.B04_PREPROCESS,
|
||||
ROUTES.B05_PROFILE,
|
||||
ROUTES.B06_SECTION,
|
||||
ROUTES.B07_WF4_DESIGN_DETAIL,
|
||||
ROUTES.B08_WF5_QUANTITY,
|
||||
ROUTES.B08_DESIGN_DETAIL,
|
||||
ROUTES.B07_QUANTITY,
|
||||
ROUTES.B09_WF6_ESTIMATION,
|
||||
ROUTES.B10_PAYMENT,
|
||||
ROUTES.B11_STATUS,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
계획선을 "폴리라인 샘플"이 아니라 **변화점(PVI) 구조**로 1차 표현하고, 도면
|
||||
테이블(구배·절토고·성토고·계획고·지반고·누가거리·거리·측점·곡선)에 필요한 값을
|
||||
전부 여기서 파생시킨다. 폴리라인 샘플은 이 구조에서 만들어지므로 B06/B07은
|
||||
전부 여기서 파생시킨다. 폴리라인 샘플은 이 구조에서 만들어지므로 B06/B08은
|
||||
기존 `design_profiles[].samples` 계약을 그대로 쓴다.
|
||||
|
||||
기하 규칙 (사용자 확정 사항):
|
||||
@@ -366,7 +366,7 @@ def build_alignment(
|
||||
segments = _segments(pvi_s, pvi_z)
|
||||
# 샘플 격자에 변화점(PVI)과 종단곡선 시·종점(BVC/EVC)을 합쳐서 평가한다. 격자만 쓰면
|
||||
# 격자 사이에 놓인 변화점(배관 구조물 자리처럼 임의 chainage에 승격된 점)의 모서리를
|
||||
# 정본 계획선이 잘라먹어, 종단곡선 R이 보이지 않고 B06 계획고·B07 CAD 계획선이 그
|
||||
# 정본 계획선이 잘라먹어, 종단곡선 R이 보이지 않고 B06 계획고·B08 CAD 계획선이 그
|
||||
# 지점에서 어긋난다(2026-08-03 사용자 보고). 프론트(buildAlignment)와 같은 규칙이다.
|
||||
extra_points = [pvi_s] + [
|
||||
np.array([curve["bvc_m"], curve["chainage_m"], curve["evc_m"]]) for curve in curves
|
||||
|
||||
@@ -71,7 +71,7 @@ def _profile_entry(
|
||||
) -> dict[str, Any]:
|
||||
"""`design_profiles` 배열 계약(기존 스키마)에 맞춰 계획선 한 벌을 만든다.
|
||||
|
||||
B06 횡단 계획고와 B07 CAD 계획선 레이어는 `samples`만 사용하므로, 선형 구조가
|
||||
B06 횡단 계획고와 B08 CAD 계획선 레이어는 `samples`만 사용하므로, 선형 구조가
|
||||
바뀌어도 하류 단계는 그대로 동작한다.
|
||||
"""
|
||||
balance = alignment["balance"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
지반유형(토사/리핑암/발파암)과 단면유형(좌절/우절/양절/양성)에 따라 표준횡단
|
||||
설계선을 구성하고, 지반선과의 차이로 절·성토 단면적을 산출한다. B06에서 사용자가
|
||||
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B07 상세설계에서
|
||||
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B08 상세설계에서
|
||||
확정치로 대체된다.
|
||||
|
||||
표준단면 기하는 config STANDARD_CROSS_SECTION(단일 진실 원천)을 읽고, B06 설정
|
||||
|
||||
@@ -390,7 +390,7 @@ async def merge_cross_section_design_by_round(
|
||||
) -> bool:
|
||||
"""정수 chainage(m) 측점의 data.design에 patch를 병합 저장한다.
|
||||
|
||||
B07 도면 확정(전체 설계 재계산 결과) 및 확정 해제(status 되돌림)에서 사용한다.
|
||||
B08 도면 확정(전체 설계 재계산 결과) 및 확정 해제(status 되돌림)에서 사용한다.
|
||||
도면 ID가 정수 m라 ROUND로 매칭한다. 갱신 여부를 반환한다.
|
||||
"""
|
||||
async with connection.cursor() as cursor:
|
||||
@@ -428,7 +428,7 @@ async def get_cross_section_design(
|
||||
) -> dict[str, Any] | None:
|
||||
"""정수 chainage(m)에 해당하는 측점의 잠정 설계(data.design)를 반환한다.
|
||||
|
||||
B07이 도면 ID(cross_{정수m}m)로 조회하므로 ROUND로 근사 매칭한다.
|
||||
B08이 도면 ID(cross_{정수m}m)로 조회하므로 ROUND로 근사 매칭한다.
|
||||
"""
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
|
||||
@@ -758,7 +758,7 @@ async def compute_cross_section_design(
|
||||
two_stage_slope=request.two_stage_slope,
|
||||
ditch_enabled=request.ditch_enabled,
|
||||
)
|
||||
# B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
|
||||
# B06 지정 시점은 잠정치. B08 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
|
||||
design["status"] = "provisional"
|
||||
# 법정 근거 문구 표기용 — 사용자가 포장을 바꿔도 제안 여부는 그대로 남긴다.
|
||||
design["pavement_suggested"] = pavement_suggested
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* B07 상세 설계 도면 목록·단건 API 클라이언트. */
|
||||
/* B08 상세 설계 도면 목록·단건 API 클라이언트. */
|
||||
|
||||
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B07 CAD 도면 조립 엔진 — B06/B05 산출물을 openwebcad 엔티티로 직렬화한다.
|
||||
"""B08 CAD 도면 조립 엔진 — B06/B05 산출물을 openwebcad 엔티티로 직렬화한다.
|
||||
|
||||
납품 도면 양식 복제:
|
||||
- 종단도: 30측점 N분할 + 하단 측점 테이블(곡선/측점/거리/추가거리/지반고/
|
||||
@@ -18,17 +18,17 @@ from typing import Any
|
||||
from uuid import UUID, uuid5
|
||||
|
||||
# 레이어 정의 — 지표면선은 상세설계 제어 대상에서 제외하므로 잠금한다(N-1-3).
|
||||
GROUND_LAYER_ID = "b07-ground"
|
||||
GROUND_LAYER_ID = "b08-ground"
|
||||
GROUND_COLOR = "#f5f7fa"
|
||||
DESIGN_LAYER_ID = "b07-design"
|
||||
DESIGN_LAYER_ID = "b08-design"
|
||||
DESIGN_COLOR = "#b794f6"
|
||||
STRUCTURE_LAYER_ID = "b07-structure"
|
||||
STRUCTURE_LAYER_ID = "b08-structure"
|
||||
STRUCTURE_COLOR = "#f6d55c"
|
||||
ROCK_LAYER_ID = "b07-rock-boundary"
|
||||
ROCK_LAYER_ID = "b08-rock-boundary"
|
||||
ROCK_COLOR = "#f59e0b"
|
||||
FRAME_LAYER_ID = "b07-frame"
|
||||
LONG_TABLE_LAYER_ID = "b07-long-table"
|
||||
CROSS_TABLE_LAYER_ID = "b07-cross-table"
|
||||
FRAME_LAYER_ID = "b08-frame"
|
||||
LONG_TABLE_LAYER_ID = "b08-long-table"
|
||||
CROSS_TABLE_LAYER_ID = "b08-cross-table"
|
||||
TABLE_LINE_COLOR = "#8ea0b5"
|
||||
TABLE_LABEL_COLOR = "#e8edf4"
|
||||
TABLE_VALUE_COLOR = "#ffe066"
|
||||
@@ -45,7 +45,7 @@ _POLY_NS = UUID("9dd28aab-cee5-4df6-b8ae-b9167fbde9a8")
|
||||
# v4: 종단 그래프 축·회색 측점 세로선·기준선(datum) + 테이블 눈금·세로쓰기·구배 원 표기.
|
||||
# v5: 횡단 콘텐츠 bbox 중심 정렬(경사 드리프트 제거) + 외곽 테두리 제거.
|
||||
# v6: 외곽 테두리 복구 (콘텐츠 중심 정렬 유지, 노선 공통 크기 사각형).
|
||||
# v7: 종단도 A1 도각 템플릿 프레임 병합 (b07-frame 잠금 레이어).
|
||||
# v7: 종단도 A1 도각 템플릿 프레임 병합 (b08-frame 잠금 레이어).
|
||||
DRAWING_FORMAT = 7
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ def _cross_line_entities(
|
||||
design: dict[str, Any] | None,
|
||||
dy: float = 0.0,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""횡단 설계선을 설계(b07-design)/구조물(b07-structure, 측구 구간)로 분리한다."""
|
||||
"""횡단 설계선을 설계(b08-design)/구조물(b08-structure, 측구 구간)로 분리한다."""
|
||||
points: list[tuple[float, float]] = []
|
||||
for point in design_line if isinstance(design_line, list) else []:
|
||||
if not isinstance(point, dict):
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B07 종단도 CAD 조립 — 30측점 N분할 + 그래프 축·격자 + 하단 측점 테이블.
|
||||
"""B08 종단도 CAD 조립 — 30측점 N분할 + 그래프 축·격자 + 하단 측점 테이블.
|
||||
|
||||
납품 양식:
|
||||
- 그래프 영역: 좌측 Y축(표고 눈금·라벨), 기준선(X축, 최저 표고에서 5m 이상
|
||||
@@ -15,7 +15,7 @@ import math
|
||||
from typing import Any
|
||||
from uuid import uuid5
|
||||
|
||||
from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Engine_Cad import (
|
||||
from B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
|
||||
_ENTITY_NS,
|
||||
DESIGN_COLOR,
|
||||
DESIGN_LAYER_ID,
|
||||
@@ -41,14 +41,14 @@ from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Engine_Cad import (
|
||||
polyline_entity,
|
||||
station_no_label,
|
||||
)
|
||||
from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Engine_Template import (
|
||||
from B08_DesignDetail.B08_DesignDetail_Engine_Template import (
|
||||
entities_bbox,
|
||||
frame_entities,
|
||||
)
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
|
||||
# 종단 전용 레이어: 그래프 축·격자(잠금 — 참조용, 편집 제외).
|
||||
LONG_GRID_LAYER_ID = "b07-long-grid"
|
||||
LONG_GRID_LAYER_ID = "b08-long-grid"
|
||||
GRID_COLOR = "#5b6572"
|
||||
|
||||
_VERTICAL = (0.0, 1.0) # 세로쓰기(아래→위)
|
||||
@@ -1,8 +1,8 @@
|
||||
"""B07 도각 템플릿 병합 — openwebcad JSON 템플릿을 도면 콘텐츠 둘레에 배치한다.
|
||||
"""B08 도각 템플릿 병합 — openwebcad JSON 템플릿을 도면 콘텐츠 둘레에 배치한다.
|
||||
|
||||
resources/dwg_analysis/templete/의 사전 변환 템플릿(A1 도각 등)을 로드해,
|
||||
도면 콘텐츠 bbox에 맞춰 균등 스케일·이동시킨 뒤 잠금 프레임 레이어
|
||||
(b07-frame) 엔티티로 병합한다. 콘텐츠 좌표(m)는 건드리지 않는다 —
|
||||
(b08-frame) 엔티티로 병합한다. 콘텐츠 좌표(m)는 건드리지 않는다 —
|
||||
템플릿 쪽을 확대해 콘텐츠를 감싼다.
|
||||
|
||||
A1 템플릿 기하(변환 시점 고정값): 전체 840x594, 하단 y17~47 표제란,
|
||||
@@ -15,7 +15,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid5
|
||||
|
||||
from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Engine_Cad import (
|
||||
from B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
|
||||
_ENTITY_NS,
|
||||
FRAME_LAYER_ID,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B06 확정 종·횡단 산출물을 B07 CAD 도면으로 변환하는 라우터."""
|
||||
"""B06 확정 종·횡단 산출물을 B08 CAD 도면으로 변환하는 라우터."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
@@ -20,7 +20,7 @@ from B06_Section.B06_Section_Repository import (
|
||||
get_longitudinal_section,
|
||||
merge_cross_section_design_by_round,
|
||||
)
|
||||
from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Engine_Cad import (
|
||||
from B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
|
||||
DRAWING_FORMAT,
|
||||
QUANTITY_VALUE_KEYS,
|
||||
build_cross_drawing,
|
||||
@@ -28,11 +28,11 @@ from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Engine_Cad import (
|
||||
infer_station_interval,
|
||||
station_no_label,
|
||||
)
|
||||
from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Engine_Cad_Long import (
|
||||
from B08_DesignDetail.B08_DesignDetail_Engine_Cad_Long import (
|
||||
build_longitudinal_drawing,
|
||||
longitudinal_chunks,
|
||||
)
|
||||
from B07_wf4_DesignDetail.B07_wf4_DesignDetail_Schema import (
|
||||
from B08_DesignDetail.B08_DesignDetail_Schema import (
|
||||
DesignDrawingConfirmRequest,
|
||||
DesignDrawingConfirmResponse,
|
||||
DesignDrawingInvalidateResponse,
|
||||
@@ -46,11 +46,11 @@ from common_util.common_util_workflow_state import complete_stage, start_stage
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
|
||||
router = APIRouter(prefix="/api/projects", tags=["B08 Design Detail"])
|
||||
|
||||
_CROSS_ID = re.compile(r"^cross_(\d+)m$")
|
||||
_LONG_ID = re.compile(r"^longitudinal(?:_(\d+))?$")
|
||||
_STAGE_DIR = "B07_wf4_DesignDetail"
|
||||
_STAGE_DIR = "B08_DesignDetail"
|
||||
|
||||
|
||||
async def _confirmed_source(project_id: UUID) -> tuple[int, Path, Path]:
|
||||
@@ -363,7 +363,7 @@ def _invalidate_drawing(project_root: Path, drawing_id: str) -> None:
|
||||
async def get_design_drawing_list(
|
||||
project_id: UUID,
|
||||
) -> DesignDrawingListResponse | JSONResponse:
|
||||
"""B07 좌측 패널용 도면 메타데이터만 캐시한다."""
|
||||
"""B08 좌측 패널용 도면 메타데이터만 캐시한다."""
|
||||
try:
|
||||
route_id, project_root, longitudinal_path = await _confirmed_source(project_id)
|
||||
drawings = await asyncio.to_thread(_drawing_list, project_root, longitudinal_path)
|
||||
@@ -375,7 +375,7 @@ async def get_design_drawing_list(
|
||||
except PermissionError as exc:
|
||||
return JSONResponse(status_code=409, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
logger.exception("B07 도면 목록 조회 실패: project_id=%s", project_id)
|
||||
logger.exception("B08 도면 목록 조회 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "상세 설계 도면 목록을 읽지 못했습니다."},
|
||||
@@ -420,7 +420,7 @@ async def get_design_drawing(
|
||||
return JSONResponse(status_code=409, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"B07 단건 도면 조회 실패: project_id=%s drawing_id=%s", project_id, drawing_id
|
||||
"B08 단건 도면 조회 실패: project_id=%s drawing_id=%s", project_id, drawing_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
@@ -433,7 +433,7 @@ def _recompute_confirmed_design(
|
||||
) -> dict[str, Any]:
|
||||
"""B06 지정값과 현재 계획고로 절·성토 단면적을 재계산해 확정치(status=confirmed)로 만든다.
|
||||
|
||||
B07 CAD에는 아직 편집 가능한 설계선이 없으므로, 저장된 지정값(지반유형·단면유형·
|
||||
B08 CAD에는 아직 편집 가능한 설계선이 없으므로, 저장된 지정값(지반유형·단면유형·
|
||||
측구위치)과 계획고로 동일 엔진을 재실행해 확정 시점 값을 고정한다.
|
||||
"""
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
@@ -505,7 +505,7 @@ async def confirm_design_drawing(
|
||||
)
|
||||
except (ValueError, KeyError, FileNotFoundError, OSError):
|
||||
logger.warning(
|
||||
"B07 확정 단면적 재계산 실패 (도면 확정은 유지): drawing_id=%s",
|
||||
"B08 확정 단면적 재계산 실패 (도면 확정은 유지): drawing_id=%s",
|
||||
drawing_id,
|
||||
exc_info=True,
|
||||
)
|
||||
@@ -522,9 +522,9 @@ async def confirm_design_drawing(
|
||||
)
|
||||
async with connection.cursor() as cursor:
|
||||
if all_confirmed:
|
||||
await complete_stage(cursor, str(project_id), 4)
|
||||
await complete_stage(cursor, str(project_id), 5)
|
||||
else:
|
||||
await start_stage(cursor, str(project_id), 4)
|
||||
await start_stage(cursor, str(project_id), 5)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
@@ -543,7 +543,7 @@ async def confirm_design_drawing(
|
||||
except PermissionError as exc:
|
||||
return JSONResponse(status_code=409, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
logger.exception("B07 도면 확정 실패: project_id=%s drawing_id=%s", project_id, drawing_id)
|
||||
logger.exception("B08 도면 확정 실패: project_id=%s drawing_id=%s", project_id, drawing_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "상세 설계 도면을 확정하지 못했습니다."},
|
||||
@@ -557,7 +557,7 @@ async def confirm_design_drawing(
|
||||
async def invalidate_design_drawing(
|
||||
project_id: UUID, drawing_id: str
|
||||
) -> DesignDrawingInvalidateResponse | JSONResponse:
|
||||
"""확정 도면 편집 시 B07 및 이후 단계를 미확정 상태로 되돌린다."""
|
||||
"""확정 도면 편집 시 B08 및 이후 단계를 미확정 상태로 되돌린다."""
|
||||
try:
|
||||
route_id, project_root, longitudinal_path = await _confirmed_source(project_id)
|
||||
items = await asyncio.to_thread(_drawing_list, project_root, longitudinal_path)
|
||||
@@ -579,7 +579,7 @@ async def invalidate_design_drawing(
|
||||
patch={"status": "provisional"},
|
||||
)
|
||||
async with connection.cursor() as cursor:
|
||||
await start_stage(cursor, str(project_id), 4)
|
||||
await start_stage(cursor, str(project_id), 5)
|
||||
await connection.commit()
|
||||
except Exception:
|
||||
await connection.rollback()
|
||||
@@ -594,7 +594,7 @@ async def invalidate_design_drawing(
|
||||
return JSONResponse(status_code=409, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"B07 도면 확정 해제 실패: project_id=%s drawing_id=%s", project_id, drawing_id
|
||||
"B08 도면 확정 해제 실패: project_id=%s drawing_id=%s", project_id, drawing_id
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B07 상세 설계 도면 목록·단건 응답 모델."""
|
||||
"""B08 상세 설계 도면 목록·단건 응답 모델."""
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -16,7 +16,7 @@ class DesignDrawingItem(BaseModel):
|
||||
|
||||
|
||||
class DesignDrawingListResponse(BaseModel):
|
||||
"""B07 진입 시 캐시할 경량 도면 목록."""
|
||||
"""B08 진입 시 캐시할 경량 도면 목록."""
|
||||
|
||||
status: str = "success"
|
||||
project_id: str
|
||||
@@ -50,7 +50,7 @@ class DesignDrawingConfirmRequest(BaseModel):
|
||||
|
||||
|
||||
class DesignDrawingConfirmResponse(BaseModel):
|
||||
"""도면별 확정 및 B07 전체 완료 상태."""
|
||||
"""도면별 확정 및 B08 전체 완료 상태."""
|
||||
|
||||
status: str = "success"
|
||||
project_id: str
|
||||
@@ -1,16 +1,16 @@
|
||||
/* =============================================================================
|
||||
* B07_wf4_DesignDetail_UI_Page.ts
|
||||
* B08_DesignDetail_UI_Page.ts
|
||||
* 로그인 후 07: 4차 워크플로우 (상세 설계) — 독립형 2D CAD 임베드
|
||||
*
|
||||
* B07_wf4_DesignDetail/openwebcad를 프로젝트 소유 B07 CAD 앱으로 빌드하여
|
||||
* /b07-cad 경로로 서빙한다. 업무 도면은 추후 same-origin postMessage로
|
||||
* B08_DesignDetail/openwebcad를 프로젝트 소유 B08 CAD 앱으로 빌드하여
|
||||
* /b08-cad 경로로 서빙한다. 업무 도면은 추후 same-origin postMessage로
|
||||
* JSON만 전달하며 DXF/DWG 파싱은 이 브라우저 앱에서 수행하지 않는다.
|
||||
*
|
||||
* 레이아웃 (사용자 지시): 사이드 패널 빈 상태 유지 + 상세 영역 CAD 화면.
|
||||
* 제약 준수 (frontend.md §2 3단 레이아웃): createWorkflowLayout 재사용.
|
||||
* ========================================================================== */
|
||||
|
||||
import "./B07_wf4_DesignDetail_UI_Style.css";
|
||||
import "./B08_DesignDetail_UI_Style.css";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import {
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
type DesignDrawingItem,
|
||||
type DesignDrawingResponse,
|
||||
type QuantityTable,
|
||||
} from "./B07_wf4_DesignDetail_Api_Fetch";
|
||||
} from "./B08_DesignDetail_Api_Fetch";
|
||||
|
||||
/** CAD 앱 수량 패널로 넘기는 설계 컨텍스트 (openwebcad DesignMeta와 동일 형식). */
|
||||
interface DesignMeta {
|
||||
@@ -57,22 +57,22 @@ interface SaveResult {
|
||||
quantityTable: QuantityTable | null;
|
||||
}
|
||||
|
||||
/** B07 독립형 CAD 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
|
||||
const B07_CAD_APP_URL = "/b07-cad/index.html";
|
||||
/** B08 독립형 CAD 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
|
||||
const B08_CAD_APP_URL = "/b08-cad/index.html";
|
||||
|
||||
/** locale 헬퍼 */
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
const CAD_LOAD_MESSAGE = "aislo:b07:load-drawing";
|
||||
const CAD_READY_MESSAGE = "aislo:b07:drawing-ready";
|
||||
const CAD_LOADED_MESSAGE = "aislo:b07:drawing-loaded";
|
||||
const CAD_ERROR_MESSAGE = "aislo:b07:drawing-error";
|
||||
const CAD_CHANGED_MESSAGE = "aislo:b07:drawing-changed";
|
||||
const CAD_SAVE_REQUEST_MESSAGE = "aislo:b07:save-request";
|
||||
const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b07:save-response";
|
||||
const CAD_NAVIGATE_MESSAGE = "aislo:b07:navigate";
|
||||
const CAD_LOAD_MESSAGE = "aislo:b08:load-drawing";
|
||||
const CAD_READY_MESSAGE = "aislo:b08:drawing-ready";
|
||||
const CAD_LOADED_MESSAGE = "aislo:b08:drawing-loaded";
|
||||
const CAD_ERROR_MESSAGE = "aislo:b08:drawing-error";
|
||||
const CAD_CHANGED_MESSAGE = "aislo:b08:drawing-changed";
|
||||
const CAD_SAVE_REQUEST_MESSAGE = "aislo:b08:save-request";
|
||||
const CAD_SAVE_RESPONSE_MESSAGE = "aislo:b08:save-response";
|
||||
const CAD_NAVIGATE_MESSAGE = "aislo:b08:navigate";
|
||||
|
||||
/** 측점 간격을 연속 chainage 차이의 최빈값으로 추정한다 (B06 그래프와 동일 방식). */
|
||||
function inferStationInterval(chainages: number[]): number {
|
||||
@@ -112,9 +112,9 @@ function buildDrawingSidePanel(
|
||||
errorMessage?: string,
|
||||
): HTMLDivElement {
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b07-drawing-list";
|
||||
panel.className = "b08-drawing-list";
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "b07-drawing-list__heading";
|
||||
heading.className = "b08-drawing-list__heading";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = "설계 도면";
|
||||
const count = document.createElement("span");
|
||||
@@ -124,7 +124,7 @@ function buildDrawingSidePanel(
|
||||
|
||||
if (errorMessage || drawings.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "b07-drawing-list__empty";
|
||||
empty.className = "b08-drawing-list__empty";
|
||||
empty.textContent = errorMessage ?? "확정된 종·횡단 도면이 없습니다.";
|
||||
panel.append(empty);
|
||||
return panel;
|
||||
@@ -138,7 +138,7 @@ function buildDrawingSidePanel(
|
||||
if (!items.length) continue;
|
||||
const section = document.createElement("section");
|
||||
// ui-sidebar-section: 사이드 컨테이너 공통 외곽선(B04~B06과 통일, 2026-08-06 사용자 지시).
|
||||
section.className = "b07-drawing-group ui-collapsible ui-sidebar-section";
|
||||
section.className = "b08-drawing-group ui-collapsible ui-sidebar-section";
|
||||
section.dataset.kind = kind;
|
||||
const sectionTitle = document.createElement("h3");
|
||||
sectionTitle.className = "ui-collapsible__title";
|
||||
@@ -147,11 +147,11 @@ function buildDrawingSidePanel(
|
||||
for (const drawing of items) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b07-drawing-button";
|
||||
button.className = "b08-drawing-button";
|
||||
button.dataset.drawingId = drawing.id;
|
||||
button.dataset.confirmed = String(drawing.confirmed);
|
||||
const name = document.createElement("span");
|
||||
name.className = "b07-drawing-button__name";
|
||||
name.className = "b08-drawing-button__name";
|
||||
name.textContent =
|
||||
drawing.kind === "cross" && typeof drawing.chainage_m === "number"
|
||||
? stationLabel(drawing.chainage_m, stationInterval)
|
||||
@@ -191,67 +191,67 @@ function ditchLabel(design: CrossDesignInfo): string {
|
||||
|
||||
function infoRow(label: string, value: string): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "b07-info__row";
|
||||
row.className = "b08-info__row";
|
||||
const key = document.createElement("span");
|
||||
key.className = "b07-info__key";
|
||||
key.className = "b08-info__key";
|
||||
key.textContent = label;
|
||||
const val = document.createElement("span");
|
||||
val.className = "b07-info__val";
|
||||
val.className = "b08-info__val";
|
||||
val.textContent = value;
|
||||
row.append(key, val);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B07 확정 시 재계산). */
|
||||
/** 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B08 확정 시 재계산). */
|
||||
function buildDesignInfoPanel(title: string, design: CrossDesignInfo | null): HTMLElement {
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b07-info";
|
||||
panel.className = "b08-info";
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "b07-info__heading";
|
||||
heading.className = "b08-info__heading";
|
||||
const stationName = document.createElement("strong");
|
||||
stationName.textContent = `${L("B07_Info_Station")} ${title}`;
|
||||
stationName.textContent = `${L("B08_Info_Station")} ${title}`;
|
||||
const confirmed = design?.status === "confirmed";
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `b07-info__badge${confirmed ? " b07-info__badge--confirmed" : ""}`;
|
||||
badge.textContent = confirmed ? L("B07_Info_Confirmed") : L("B07_Info_Provisional");
|
||||
badge.className = `b08-info__badge${confirmed ? " b08-info__badge--confirmed" : ""}`;
|
||||
badge.textContent = confirmed ? L("B08_Info_Confirmed") : L("B08_Info_Provisional");
|
||||
heading.append(stationName, badge);
|
||||
panel.append(heading);
|
||||
|
||||
if (!design) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "b07-info__empty";
|
||||
empty.textContent = L("B07_Info_NoDesign");
|
||||
empty.className = "b08-info__empty";
|
||||
empty.textContent = L("B08_Info_NoDesign");
|
||||
panel.append(empty);
|
||||
return panel;
|
||||
}
|
||||
|
||||
const ground = document.createElement("section");
|
||||
ground.className = "b07-info__block";
|
||||
ground.className = "b08-info__block";
|
||||
const groundTitle = document.createElement("h4");
|
||||
groundTitle.textContent = L("B07_Info_Ground_Title");
|
||||
groundTitle.textContent = L("B08_Info_Ground_Title");
|
||||
ground.append(
|
||||
groundTitle,
|
||||
infoRow(L("B07_Info_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])),
|
||||
infoRow(L("B07_Info_CutSide"), cutSideLabel(design.section_mode)),
|
||||
infoRow(L("B08_Info_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])),
|
||||
infoRow(L("B08_Info_CutSide"), cutSideLabel(design.section_mode)),
|
||||
infoRow(
|
||||
L("B07_Info_DitchSide"),
|
||||
L("B08_Info_DitchSide"),
|
||||
design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"),
|
||||
),
|
||||
);
|
||||
|
||||
const plan = document.createElement("section");
|
||||
plan.className = "b07-info__block";
|
||||
plan.className = "b08-info__block";
|
||||
const planTitle = document.createElement("h4");
|
||||
planTitle.textContent = L("B07_Info_Plan_Title");
|
||||
planTitle.textContent = L("B08_Info_Plan_Title");
|
||||
plan.append(
|
||||
planTitle,
|
||||
infoRow(L("B07_Info_DesignElevation"), `${design.design_elevation_m.toFixed(2)}m`),
|
||||
infoRow(L("B07_Info_CutSlope"), `1:${design.cut_slope_ratio}`),
|
||||
infoRow(L("B07_Info_FillSlope"), `1:${design.fill_slope_ratio}`),
|
||||
infoRow(L("B07_Info_RoadWidth"), `${design.roadbed_width_m.toFixed(2)}m`),
|
||||
infoRow(L("B07_Info_Ditch"), ditchLabel(design)),
|
||||
infoRow(L("B07_Info_CutArea"), `${design.cut_area_m2.toFixed(2)}㎡`),
|
||||
infoRow(L("B07_Info_FillArea"), `${design.fill_area_m2.toFixed(2)}㎡`),
|
||||
infoRow(L("B08_Info_DesignElevation"), `${design.design_elevation_m.toFixed(2)}m`),
|
||||
infoRow(L("B08_Info_CutSlope"), `1:${design.cut_slope_ratio}`),
|
||||
infoRow(L("B08_Info_FillSlope"), `1:${design.fill_slope_ratio}`),
|
||||
infoRow(L("B08_Info_RoadWidth"), `${design.roadbed_width_m.toFixed(2)}m`),
|
||||
infoRow(L("B08_Info_Ditch"), ditchLabel(design)),
|
||||
infoRow(L("B08_Info_CutArea"), `${design.cut_area_m2.toFixed(2)}㎡`),
|
||||
infoRow(L("B08_Info_FillArea"), `${design.fill_area_m2.toFixed(2)}㎡`),
|
||||
);
|
||||
|
||||
panel.append(ground, plan);
|
||||
@@ -261,7 +261,7 @@ function buildDesignInfoPanel(title: string, design: CrossDesignInfo | null): HT
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
export async function renderB08DesignDetail(root: HTMLElement): Promise<void> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
let workflowState: WorkflowState | undefined;
|
||||
let drawings: DesignDrawingItem[] = [];
|
||||
@@ -281,14 +281,14 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
|
||||
const cadHost = document.createElement("div");
|
||||
cadHost.className = "b07-cad-host";
|
||||
cadHost.className = "b08-cad-host";
|
||||
const frame = document.createElement("iframe");
|
||||
frame.className = "b07-cad-frame";
|
||||
frame.src = B07_CAD_APP_URL;
|
||||
frame.title = L("B07_Design_Title");
|
||||
frame.className = "b08-cad-frame";
|
||||
frame.src = B08_CAD_APP_URL;
|
||||
frame.title = L("B08_Design_Title");
|
||||
const license = document.createElement("a");
|
||||
license.className = "b07-cad-license";
|
||||
license.href = "/b07-cad/THIRD_PARTY_LICENSES.txt";
|
||||
license.className = "b08-cad-license";
|
||||
license.href = "/b08-cad/THIRD_PARTY_LICENSES.txt";
|
||||
license.target = "_blank";
|
||||
license.rel = "noreferrer";
|
||||
license.textContent = "Drawing engine based on OpenWebCAD · MIT License";
|
||||
@@ -311,7 +311,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
let resolveSave: ((payload: SaveResult) => void) | undefined;
|
||||
let drawingListEl: HTMLElement | undefined;
|
||||
const infoPanelHost = document.createElement("div");
|
||||
infoPanelHost.className = "b07-info-host";
|
||||
infoPanelHost.className = "b08-info-host";
|
||||
|
||||
const updateInfoPanel = (drawing: DesignDrawingItem, response: DesignDrawingResponse): void => {
|
||||
if (drawing.kind !== "cross") {
|
||||
@@ -334,11 +334,11 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
|
||||
const findButton = (drawingId: string) =>
|
||||
drawingListEl?.querySelector<HTMLButtonElement>(
|
||||
`.b07-drawing-button[data-drawing-id="${drawingId}"]`,
|
||||
`.b08-drawing-button[data-drawing-id="${drawingId}"]`,
|
||||
) ?? undefined;
|
||||
|
||||
const highlightActive = (drawingId: string) => {
|
||||
drawingListEl?.querySelectorAll<HTMLButtonElement>(".b07-drawing-button").forEach((item) => {
|
||||
drawingListEl?.querySelectorAll<HTMLButtonElement>(".b08-drawing-button").forEach((item) => {
|
||||
item.dataset.active = String(item.dataset.drawingId === drawingId);
|
||||
});
|
||||
};
|
||||
@@ -442,7 +442,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
showToast("현재 도면을 확정하고 저장했습니다.", "success");
|
||||
if (result.all_confirmed) {
|
||||
allDrawingsConfirmed = true;
|
||||
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[5]);
|
||||
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[6]);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(
|
||||
@@ -510,27 +510,17 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
);
|
||||
drawingListEl = drawingPanel;
|
||||
const confirmActions = document.createElement("div");
|
||||
// b07-drawing-actions는 자체 sticky 하단 고정(B07 Style.css) — 공용 ui-sidebar-actions와
|
||||
// b08-drawing-actions는 자체 sticky 하단 고정(B08 Style.css) — 공용 ui-sidebar-actions와
|
||||
// 같은 동작이라 중복 적용하지 않는다(2026-08-05 확인).
|
||||
confirmActions.className = "b07-drawing-actions";
|
||||
confirmActions.className = "b08-drawing-actions";
|
||||
confirmActions.append(confirmButton);
|
||||
|
||||
// TODO: [임시 테스트용] B08 강제 이동 버튼 (테스트 완료 후 제거 예정)
|
||||
const tempB08Btn = createButton({
|
||||
label: "[임시] B08 이동",
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
if (projectId) goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[5]);
|
||||
},
|
||||
});
|
||||
confirmActions.append(tempB08Btn);
|
||||
|
||||
drawingPanel.append(infoPanelHost, confirmActions);
|
||||
|
||||
const layout = createWorkflowLayout({
|
||||
title: L("B07_Design_Title"),
|
||||
title: L("B08_Design_Title"),
|
||||
steps: workflowSteps(),
|
||||
activeStep: 4,
|
||||
activeStep: 5,
|
||||
leftPanel: drawingPanel,
|
||||
mainContent: cadHost,
|
||||
stages: workflowState?.stages,
|
||||
@@ -538,14 +528,14 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
onStepClick: (stepIndex) => {
|
||||
if (!projectId) return;
|
||||
if (stepIndex > 4 && !allDrawingsConfirmed) {
|
||||
if (stepIndex > 5 && !allDrawingsConfirmed) {
|
||||
showToast("모든 설계 도면을 확정한 뒤 다음 단계로 이동할 수 있습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
||||
},
|
||||
});
|
||||
layout.root.classList.add("b07-design-layout");
|
||||
layout.root.classList.add("b08-design-layout");
|
||||
root.replaceChildren(layout.root);
|
||||
|
||||
// 페이지 진입 시 첫 도면(종단도)을 자동 선택 — 빈 CAD 화면 방지.
|
||||
@@ -1,23 +1,23 @@
|
||||
/* =============================================================================
|
||||
* B07_wf4_DesignDetail_UI_Style.css
|
||||
* B08_DesignDetail_UI_Style.css
|
||||
* 상세 설계(독립형 2D CAD) 화면 스타일 — theme.css 변수만 사용
|
||||
* ========================================================================== */
|
||||
|
||||
/* 워크플로우 레이아웃 높이 (B06 패턴 준수) */
|
||||
.b07-design-layout {
|
||||
.b08-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 {
|
||||
.b08-design-layout .ui-workflow-layout__body,
|
||||
.b08-design-layout .ui-workflow-layout__main {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* B06 확정 산출물 도면 목록 */
|
||||
.b07-drawing-list {
|
||||
.b08-drawing-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
@@ -26,7 +26,7 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.b07-drawing-list__heading {
|
||||
.b08-drawing-list__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -34,13 +34,13 @@
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b07-drawing-list__heading span,
|
||||
.b07-drawing-list__empty {
|
||||
.b08-drawing-list__heading span,
|
||||
.b08-drawing-list__empty {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.b07-drawing-group {
|
||||
.b08-drawing-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
@@ -50,13 +50,13 @@
|
||||
}
|
||||
|
||||
/* 횡단도는 2열 배치 */
|
||||
.b07-drawing-group[data-kind="cross"] {
|
||||
.b08-drawing-group[data-kind="cross"] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b07-drawing-group h3 {
|
||||
.b08-drawing-group h3 {
|
||||
margin: var(--spacing-8) 0 var(--spacing-4);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
@@ -64,11 +64,11 @@
|
||||
}
|
||||
|
||||
/* 그리드 제목은 두 열을 가로지른다 */
|
||||
.b07-drawing-group[data-kind="cross"] h3 {
|
||||
.b08-drawing-group[data-kind="cross"] h3 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.b07-drawing-button {
|
||||
.b08-drawing-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -86,34 +86,34 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.b07-drawing-button__name {
|
||||
.b08-drawing-button__name {
|
||||
overflow: hidden;
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.b07-drawing-button:hover,
|
||||
.b07-drawing-button[data-active="true"] {
|
||||
.b08-drawing-button:hover,
|
||||
.b08-drawing-button[data-active="true"] {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-mist-violet);
|
||||
}
|
||||
|
||||
.b07-drawing-button[data-active="true"] {
|
||||
.b08-drawing-button[data-active="true"] {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 확정: 좌측 띠 + 측점 글자색을 함께 성공색으로 반영 */
|
||||
.b07-drawing-button[data-confirmed="true"] {
|
||||
.b08-drawing-button[data-confirmed="true"] {
|
||||
border-color: color-mix(in srgb, var(--color-success) 35%, var(--color-border));
|
||||
border-left-color: var(--color-success);
|
||||
}
|
||||
|
||||
.b07-drawing-button[data-confirmed="true"] .b07-drawing-button__name {
|
||||
.b08-drawing-button[data-confirmed="true"] .b08-drawing-button__name {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.b07-drawing-actions {
|
||||
.b08-drawing-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
@@ -121,12 +121,12 @@
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.b07-drawing-actions > button {
|
||||
.b08-drawing-actions > button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* CAD 뷰어 호스트 (상세 페이지 영역) */
|
||||
.b07-cad-host {
|
||||
.b08-cad-host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -136,8 +136,8 @@
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
/* B07 독립형 CAD 앱 임베드 */
|
||||
.b07-cad-frame {
|
||||
/* B08 독립형 CAD 앱 임베드 */
|
||||
.b08-cad-frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
@@ -145,7 +145,7 @@
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.b07-cad-license {
|
||||
.b08-cad-license {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
@@ -156,17 +156,17 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.b07-cad-license:hover {
|
||||
.b08-cad-license:hover {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 선택 횡단도의 지반정보/계획정보 (잠정치) */
|
||||
.b07-info-host:empty {
|
||||
.b08-info-host:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b07-info {
|
||||
.b08-info {
|
||||
margin-top: var(--spacing-16);
|
||||
padding-top: var(--spacing-16);
|
||||
border-top: 1px solid var(--color-border);
|
||||
@@ -175,14 +175,14 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-info__heading {
|
||||
.b08-info__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-info__badge {
|
||||
.b08-info__badge {
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
color: var(--color-primary);
|
||||
@@ -190,40 +190,40 @@
|
||||
border-radius: var(--radius-buttons);
|
||||
}
|
||||
|
||||
.b07-info__badge--confirmed {
|
||||
.b08-info__badge--confirmed {
|
||||
color: var(--color-surface);
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.b07-info__block {
|
||||
.b08-info__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.b07-info__block h4 {
|
||||
.b08-info__block h4 {
|
||||
margin: var(--spacing-8) 0 2px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.b07-info__row {
|
||||
.b08-info__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.b07-info__key {
|
||||
.b08-info__key {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.b07-info__val {
|
||||
.b08-info__val {
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.b07-info__empty {
|
||||
.b08-info__empty {
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
# Aislo B07 2D Drawing
|
||||
# Aislo B08 2D Drawing
|
||||
|
||||
This directory is maintained as part of the Aislo project and is not connected
|
||||
to an upstream Git repository. It provides the independent browser-based 2D
|
||||
drawing workspace used only by the B07 page.
|
||||
drawing workspace used only by the B08 page.
|
||||
|
||||
The drawing engine is based on OpenWebCAD by Bert Verhelst and retains its MIT
|
||||
license notice in `LICENSE.md` and `public/THIRD_PARTY_LICENSES.txt`.
|
||||
@@ -10,7 +10,7 @@ license notice in `LICENSE.md` and `public/THIRD_PARTY_LICENSES.txt`.
|
||||
## Aislo integration boundary
|
||||
|
||||
- Business drawings enter the iframe only as same-origin JSON messages with
|
||||
type `aislo:b07:load-drawing`.
|
||||
type `aislo:b08:load-drawing`.
|
||||
- Browser-side DXF/DWG parsing is intentionally excluded.
|
||||
- DXF, DWG and PDF input/output are separate future modules and require a new
|
||||
dependency-license review before implementation.
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "aislo-b07-cad",
|
||||
"name": "aislo-b08-cad",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aislo-b07-cad",
|
||||
"name": "aislo-b08-cad",
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "aislo-b07-cad",
|
||||
"name": "aislo-b08-cad",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@@ -1,4 +1,4 @@
|
||||
Aislo B07 2D Drawing - Third-Party Notices
|
||||
Aislo B08 2D Drawing - Third-Party Notices
|
||||
|
||||
This application is based in part on OpenWebCAD.
|
||||
|
||||
|
Before Width: | Height: | Size: 729 B After Width: | Height: | Size: 729 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
@@ -38,7 +38,7 @@ export enum HtmlEvent {
|
||||
DRAWING_CHANGED = 'DRAWING_CHANGED',
|
||||
}
|
||||
|
||||
/** 부모(B07 페이지)가 도면과 함께 넘기는 설계 컨텍스트 (수량 패널 표시용). */
|
||||
/** 부모(B08 페이지)가 도면과 함께 넘기는 설계 컨텍스트 (수량 패널 표시용). */
|
||||
export interface DesignMeta {
|
||||
kind: 'cross' | 'longitudinal';
|
||||
/** 패널 제목 (측점 라벨, 예: "2+0.0" 또는 "종단도 전체") */
|
||||
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 455 B After Width: | Height: | Size: 455 B |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
@@ -5,7 +5,7 @@ import { getDesignMeta } from '../state';
|
||||
|
||||
/**
|
||||
* 도면 내비게이션 바 (CAD 화면 하단 중심).
|
||||
* 수량 산출표는 HTML 테이블 대신 도면 자체의 CAD 테이블 레이어(b07-cross-table)로
|
||||
* 수량 산출표는 HTML 테이블 대신 도면 자체의 CAD 테이블 레이어(b08-cross-table)로
|
||||
* 렌더되므로, 여기서는 제목·측점정보·확정상태·이전/다음 이동만 담당한다.
|
||||
*/
|
||||
export const QuantityPanel: FC = () => {
|
||||
@@ -225,7 +225,7 @@ export const Toolbar: FC = () => {
|
||||
<header className="cad-titlebar controls">
|
||||
<div className="cad-brand">
|
||||
<strong>Aislo CAD</strong>
|
||||
<span>B07 상세 설계</span>
|
||||
<span>B08 상세 설계</span>
|
||||
</div>
|
||||
<div className="cad-file-state">
|
||||
<span className="cad-file-state__dot" />
|
||||
@@ -23,7 +23,7 @@ export function calculateAngleGuidesAndSnapPoints() {
|
||||
const screenScale = screenCanvasDrawController.getScreenScale();
|
||||
const worldMouseLocation = screenCanvasDrawController.getWorldMouseLocation();
|
||||
// 스냅 후보: 공간 인덱스로 마우스 주변만 조회 (전 엔티티 O(n²) 교차 계산 제거),
|
||||
// 잠금 레이어(b07-frame 등 참조용)는 스냅 대상에서 제외한다.
|
||||
// 잠금 레이어(b08-frame 등 참조용)는 스냅 대상에서 제외한다.
|
||||
const maxSnapDistance = SNAP_POINT_DISTANCE / screenScale;
|
||||
const entities = queryEntitiesNearPoint(
|
||||
worldMouseLocation.x,
|
||||