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>
This commit is contained in:
2026-08-08 10:10:10 +09:00
co-authored by Claude Fable 5
parent 54954a05e5
commit 2a248ff742
198 changed files with 232 additions and 242 deletions
+2 -2
View File
@@ -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,
];
+2 -2
View File
@@ -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,
+5 -5
View File
@@ -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"]
+1 -1
View File
@@ -2,7 +2,7 @@
지반유형(토사/리핑암/발파암)과 단면유형(좌절/우절/양절/양성)에 따라 표준횡단
설계선을 구성하고, 지반선과의 차이로 절·성토 단면적을 산출한다. B06에서 사용자가
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B07 상세설계에서
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B08 상세설계에서
확정치로 대체된다.
표준단면 기하는 config STANDARD_CROSS_SECTION(단일 진실 원천)을 읽고, B06 설정
+2 -2
View File
@@ -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(
+1 -1
View File
@@ -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: 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,

Some files were not shown because too many files have changed in this diff Show More