feat(B07,B08): 워크플로 순서 교환 — 상세설계를 수량산출 앞으로

횡단설계(B06) 다음을 상세설계 → 수량산출 → 설계도서 순으로 재배열하고,
폴더 번호가 흐름과 일치하도록 이름을 맞바꾼다.

- B08_DesignDetail → B07_DesignDetail, B07_Quantity → B08_Quantity
  (파일 접두어·식별자·라우트·locale 키 전량 스왑)
- STAGE_KEYS 4=DESIGN_DETAIL, 5=QUANTITY 스왑 + 라우터 stage 리터럴 교체
- CAD 마운트 /b08-cad → /b07-cad (main.py·vite proxy·iframe URL),
  openwebcad Toolbar 라벨 B07로 수정 후 재빌드
- 유지: openwebcad postMessage 프로토콜 aislo:b08:*·패키지명(내부 식별자)
- 기존 프로젝트 storage 폴더 rename + project_manifest 갱신,
  DB project_workflow_stages stage_no 4↔5 행 스왑 완료

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 16:16:46 +09:00
co-authored by Claude Fable 5
parent f99f1f68d3
commit 6e195afb69
199 changed files with 227 additions and 227 deletions
+2 -2
View File
@@ -82,7 +82,7 @@ export interface PendingWorkflowOptions {
steps: string[];
/** 현재 활성 단계 인덱스 */
activeStep: number;
/** 본문은 준비 중이지만 좌측 패널만 먼저 붙일 때 (B07 수량 확정 버튼 등) */
/** 본문은 준비 중이지만 좌측 패널만 먼저 붙일 때 (B08 수량 확정 버튼 등) */
leftPanel?: HTMLElement;
}
@@ -126,8 +126,8 @@ export function workflowSteps(): string[] {
L("WF_Step_Surface"),
L("WF_Step_Route"),
L("WF_Step_ProfileCross"),
L("WF_Step_Quantity"),
L("WF_Step_DesignDetail"),
L("WF_Step_Quantity"),
L("WF_Step_Estimation"),
];
}
+2 -2
View File
@@ -23,8 +23,8 @@ export const WORKFLOW_STEP_ROUTES: readonly RoutePath[] = [
ROUTES.B04_PREPROCESS,
ROUTES.B05_PROFILE,
ROUTES.B06_SECTION,
ROUTES.B07_QUANTITY,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B07_DESIGN_DETAIL,
ROUTES.B08_QUANTITY,
ROUTES.B09_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_QUANTITY,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B07_DESIGN_DETAIL,
ROUTES.B08_QUANTITY,
ROUTES.B09_ESTIMATION,
ROUTES.B10_PAYMENT,
ROUTES.B11_STATUS,
+8 -8
View File
@@ -45,12 +45,12 @@ 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,
// B07_QUANTITY: 수량 본문은 재작성 예정(구 구현은 B07_Quantity/0_old_260726_codex.zip 백업).
// 지금은 셸 페이지 — 좌측 [확정]으로 stage 4를 닫고 B08 상세설계로 넘어간다.
[ROUTES.B07_QUANTITY]: async () =>
(await import("../B07_Quantity/B07_Quantity_UI_Page")).renderB07Quantity,
[ROUTES.B08_DESIGN_DETAIL]: async () =>
(await import("../B08_DesignDetail/B08_DesignDetail_UI_Page")).renderB08DesignDetail,
[ROUTES.B07_DESIGN_DETAIL]: async () =>
(await import("../B07_DesignDetail/B07_DesignDetail_UI_Page")).renderB07DesignDetail,
// B08_QUANTITY: 수량 본문은 재작성 예정(구 구현은 B08_Quantity/0_old_260726_codex.zip 백업).
// 지금은 셸 페이지 — 좌측 [확정]으로 stage 5를 닫고 B09 설계도서로 넘어간다.
[ROUTES.B08_QUANTITY]: async () =>
(await import("../B08_Quantity/B08_Quantity_UI_Page")).renderB08Quantity,
[ROUTES.B09_ESTIMATION]: async () =>
(await import("../B09_Estimation/B09_Estimation_UI_Page")).renderB09Estimation,
[ROUTES.B10_PAYMENT]: async () =>
@@ -111,8 +111,8 @@ export async function renderCurrentRoute(outlet: HTMLElement): Promise<void> {
ROUTES.B04_PREPROCESS,
ROUTES.B05_PROFILE,
ROUTES.B06_SECTION,
ROUTES.B07_QUANTITY,
ROUTES.B08_DESIGN_DETAIL,
ROUTES.B07_DESIGN_DETAIL,
ROUTES.B08_QUANTITY,
ROUTES.B09_ESTIMATION,
ROUTES.B10_PAYMENT,
ROUTES.B11_STATUS,
+1 -1
View File
@@ -2,7 +2,7 @@
지반유형(토사/리핑암/발파암)과 단면유형(좌절/우절/양절/양성)에 따라 표준횡단
설계선을 구성하고, 지반선과의 차이로 절·성토 단면적을 산출한다. B06에서 사용자가
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B08 상세설계에서
버튼을 누를 때 즉시 호출되며, 여기서 나온 값은 잠정치로 저장되고 B07 상세설계에서
확정치로 대체된다.
표준단면 기하는 config STANDARD_CROSS_SECTION(단일 진실 원천)을 읽고, B06 설정
+1 -1
View File
@@ -390,7 +390,7 @@ async def merge_cross_section_design_by_round(
) -> bool:
"""정수 chainage(m) 측점의 data.design에 patch를 병합 저장한다.
B08 도면 확정(전체 설계 재계산 결과) 및 확정 해제(status 되돌림)에서 사용한다.
B07 도면 확정(전체 설계 재계산 결과) 및 확정 해제(status 되돌림)에서 사용한다.
도면 ID가 정수 m라 ROUND로 매칭한다. 갱신 여부를 반환한다.
"""
async with connection.cursor() as cursor:
+1 -1
View File
@@ -604,7 +604,7 @@ async def compute_cross_section_design(
two_stage_slope=request.two_stage_slope,
ditch_enabled=request.ditch_enabled,
)
# B06 지정 시점은 잠정치. B08 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
# B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다.
design["status"] = "provisional"
# 법정 근거 문구 표기용 — 사용자가 포장을 바꿔도 제안 여부는 그대로 남긴다.
design["pavement_suggested"] = pavement_suggested
+1 -1
View File
@@ -94,7 +94,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
const DISPLAY_HALF_WIDTH_DEFAULT_M = 12;
// 하단 액션 [종단 이동][임시저장][확정] — 종단 이동은 저장 없이 B05로 페이지 이동만,
// 확정은 임시저장과 같은 내용 저장 후 stage 2·3을 닫고 B07 수량으로 넘어간다
// 확정은 임시저장과 같은 내용 저장 후 stage 2·3을 닫고 B07 상세설계로 넘어간다
// (2026-08-08 워크플로우 재정의).
const goProfileButton = createButton({
label: L("B06_Profile_Btn_GoProfile"),
@@ -1,4 +1,4 @@
/* B08 상세 설계 도면 목록·단건 API 클라이언트. */
/* B07 상세 설계 도면 목록·단건 API 클라이언트. */
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
@@ -1,4 +1,4 @@
"""B08 CAD 도면 조립 엔진 — B06/B05 산출물을 openwebcad 엔티티로 직렬화한다.
"""B07 CAD 도면 조립 엔진 — B06/B05 산출물을 openwebcad 엔티티로 직렬화한다.
납품 도면 양식 복제:
- 종단도: 30측점 N분할 + 하단 측점 테이블(곡선/측점/거리/추가거리/지반고/
@@ -1,4 +1,4 @@
"""B08 종단도 CAD 조립 — 30측점 N분할 + 그래프 축·격자 + 하단 측점 테이블.
"""B07 종단도 CAD 조립 — 30측점 N분할 + 그래프 축·격자 + 하단 측점 테이블.
납품 양식:
- 그래프 영역: 좌측 Y축(표고 눈금·라벨), 기준선(X축, 최저 표고에서 5m 이상
@@ -15,7 +15,7 @@ import math
from typing import Any
from uuid import uuid5
from B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
_ENTITY_NS,
DESIGN_COLOR,
DESIGN_LAYER_ID,
@@ -41,7 +41,7 @@ from B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
polyline_entity,
station_no_label,
)
from B08_DesignDetail.B08_DesignDetail_Engine_Template import (
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
entities_bbox,
frame_entities,
)
@@ -1,4 +1,4 @@
"""B08 도각 템플릿 병합 — openwebcad JSON 템플릿을 도면 콘텐츠 둘레에 배치한다.
"""B07 도각 템플릿 병합 — openwebcad JSON 템플릿을 도면 콘텐츠 둘레에 배치한다.
resources/template_2dDrawing/ 사전 변환 템플릿(A1 도각 ) 로드해,
도면 콘텐츠 bbox에 맞춰 균등 스케일·이동시킨 잠금 프레임 레이어
@@ -15,7 +15,7 @@ from pathlib import Path
from typing import Any
from uuid import uuid5
from B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
_ENTITY_NS,
FRAME_LAYER_ID,
)
@@ -1,4 +1,4 @@
"""B06 확정 종·횡단 산출물을 B08 CAD 도면으로 변환하는 라우터."""
"""B06 확정 종·횡단 산출물을 B07 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 B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
DRAWING_FORMAT,
QUANTITY_VALUE_KEYS,
build_cross_drawing,
@@ -28,11 +28,11 @@ from B08_DesignDetail.B08_DesignDetail_Engine_Cad import (
infer_station_interval,
station_no_label,
)
from B08_DesignDetail.B08_DesignDetail_Engine_Cad_Long import (
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import (
build_longitudinal_drawing,
longitudinal_chunks,
)
from B08_DesignDetail.B08_DesignDetail_Schema import (
from B07_DesignDetail.B07_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=["B08 Design Detail"])
router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
_CROSS_ID = re.compile(r"^cross_(\d+)m$")
_LONG_ID = re.compile(r"^longitudinal(?:_(\d+))?$")
_STAGE_DIR = "B08_DesignDetail"
_STAGE_DIR = "B07_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:
"""B08 좌측 패널용 도면 메타데이터만 캐시한다."""
"""B07 좌측 패널용 도면 메타데이터만 캐시한다."""
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("B08 도면 목록 조회 실패: project_id=%s", project_id)
logger.exception("B07 도면 목록 조회 실패: 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(
"B08 단건 도면 조회 실패: project_id=%s drawing_id=%s", project_id, drawing_id
"B07 단건 도면 조회 실패: 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)로 만든다.
B08 CAD에는 아직 편집 가능한 설계선이 없으므로, 저장된 지정값(지반유형·단면유형·
B07 CAD에는 아직 편집 가능한 설계선이 없으므로, 저장된 지정값(지반유형·단면유형·
측구위치) 계획고로 동일 엔진을 재실행해 확정 시점 값을 고정한다.
"""
longitudinal = _read_json(longitudinal_path)
@@ -505,7 +505,7 @@ async def confirm_design_drawing(
)
except (ValueError, KeyError, FileNotFoundError, OSError):
logger.warning(
"B08 확정 단면적 재계산 실패 (도면 확정은 유지): drawing_id=%s",
"B07 확정 단면적 재계산 실패 (도면 확정은 유지): 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), 5)
await complete_stage(cursor, str(project_id), 4)
else:
await start_stage(cursor, str(project_id), 5)
await start_stage(cursor, str(project_id), 4)
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("B08 도면 확정 실패: project_id=%s drawing_id=%s", project_id, drawing_id)
logger.exception("B07 도면 확정 실패: 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:
"""확정 도면 편집 시 B08 및 이후 단계를 미확정 상태로 되돌린다."""
"""확정 도면 편집 시 B07 및 이후 단계를 미확정 상태로 되돌린다."""
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), 5)
await start_stage(cursor, str(project_id), 4)
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(
"B08 도면 확정 해제 실패: project_id=%s drawing_id=%s", project_id, drawing_id
"B07 도면 확정 해제 실패: project_id=%s drawing_id=%s", project_id, drawing_id
)
return JSONResponse(
status_code=500,
@@ -1,4 +1,4 @@
"""B08 상세 설계 도면 목록·단건 응답 모델."""
"""B07 상세 설계 도면 목록·단건 응답 모델."""
from typing import Any, Literal
@@ -16,7 +16,7 @@ class DesignDrawingItem(BaseModel):
class DesignDrawingListResponse(BaseModel):
"""B08 진입 시 캐시할 경량 도면 목록."""
"""B07 진입 시 캐시할 경량 도면 목록."""
status: str = "success"
project_id: str
@@ -50,7 +50,7 @@ class DesignDrawingConfirmRequest(BaseModel):
class DesignDrawingConfirmResponse(BaseModel):
"""도면별 확정 및 B08 전체 완료 상태."""
"""도면별 확정 및 B07 전체 완료 상태."""
status: str = "success"
project_id: str
@@ -1,16 +1,16 @@
/* =============================================================================
* B08_DesignDetail_UI_Page.ts
* B07_DesignDetail_UI_Page.ts
* 07: 4차 ( ) 2D CAD
*
* B08_DesignDetail/openwebcad를 B08 CAD
* /b08-cad . same-origin postMessage
* B07_DesignDetail/openwebcad를 B07 CAD
* /b07-cad . same-origin postMessage
* JSON만 DXF/DWG .
*
* ( ): + CAD .
* (frontend.md §2 3 ): createWorkflowLayout .
* ========================================================================== */
import "./B08_DesignDetail_UI_Style.css";
import "./B07_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 "./B08_DesignDetail_Api_Fetch";
} from "./B07_DesignDetail_Api_Fetch";
/** CAD 앱 수량 패널로 넘기는 설계 컨텍스트 (openwebcad DesignMeta와 동일 형식). */
interface DesignMeta {
@@ -57,8 +57,8 @@ interface SaveResult {
quantityTable: QuantityTable | null;
}
/** B08 독립형 CAD 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
const B08_CAD_APP_URL = "/b08-cad/index.html";
/** B07 독립형 CAD 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
const B07_CAD_APP_URL = "/b07-cad/index.html";
/** locale 헬퍼 */
function L(key: keyof typeof ui_locales): string {
@@ -112,9 +112,9 @@ function buildDrawingSidePanel(
errorMessage?: string,
): HTMLDivElement {
const panel = document.createElement("div");
panel.className = "b08-drawing-list";
panel.className = "b07-drawing-list";
const heading = document.createElement("div");
heading.className = "b08-drawing-list__heading";
heading.className = "b07-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 = "b08-drawing-list__empty";
empty.className = "b07-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 = "b08-drawing-group ui-collapsible ui-sidebar-section";
section.className = "b07-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 = "b08-drawing-button";
button.className = "b07-drawing-button";
button.dataset.drawingId = drawing.id;
button.dataset.confirmed = String(drawing.confirmed);
const name = document.createElement("span");
name.className = "b08-drawing-button__name";
name.className = "b07-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 = "b08-info__row";
row.className = "b07-info__row";
const key = document.createElement("span");
key.className = "b08-info__key";
key.className = "b07-info__key";
key.textContent = label;
const val = document.createElement("span");
val.className = "b08-info__val";
val.className = "b07-info__val";
val.textContent = value;
row.append(key, val);
return row;
}
/** 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B08 확정 시 재계산). */
/** 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B07 확정 시 재계산). */
function buildDesignInfoPanel(title: string, design: CrossDesignInfo | null): HTMLElement {
const panel = document.createElement("div");
panel.className = "b08-info";
panel.className = "b07-info";
const heading = document.createElement("div");
heading.className = "b08-info__heading";
heading.className = "b07-info__heading";
const stationName = document.createElement("strong");
stationName.textContent = `${L("B08_Info_Station")} ${title}`;
stationName.textContent = `${L("B07_Info_Station")} ${title}`;
const confirmed = design?.status === "confirmed";
const badge = document.createElement("span");
badge.className = `b08-info__badge${confirmed ? " b08-info__badge--confirmed" : ""}`;
badge.textContent = confirmed ? L("B08_Info_Confirmed") : L("B08_Info_Provisional");
badge.className = `b07-info__badge${confirmed ? " b07-info__badge--confirmed" : ""}`;
badge.textContent = confirmed ? L("B07_Info_Confirmed") : L("B07_Info_Provisional");
heading.append(stationName, badge);
panel.append(heading);
if (!design) {
const empty = document.createElement("p");
empty.className = "b08-info__empty";
empty.textContent = L("B08_Info_NoDesign");
empty.className = "b07-info__empty";
empty.textContent = L("B07_Info_NoDesign");
panel.append(empty);
return panel;
}
const ground = document.createElement("section");
ground.className = "b08-info__block";
ground.className = "b07-info__block";
const groundTitle = document.createElement("h4");
groundTitle.textContent = L("B08_Info_Ground_Title");
groundTitle.textContent = L("B07_Info_Ground_Title");
ground.append(
groundTitle,
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_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])),
infoRow(L("B07_Info_CutSide"), cutSideLabel(design.section_mode)),
infoRow(
L("B08_Info_DitchSide"),
L("B07_Info_DitchSide"),
design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"),
),
);
const plan = document.createElement("section");
plan.className = "b08-info__block";
plan.className = "b07-info__block";
const planTitle = document.createElement("h4");
planTitle.textContent = L("B08_Info_Plan_Title");
planTitle.textContent = L("B07_Info_Plan_Title");
plan.append(
planTitle,
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)}`),
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)}`),
);
panel.append(ground, plan);
@@ -261,7 +261,7 @@ function buildDesignInfoPanel(title: string, design: CrossDesignInfo | null): HT
/* -----------------------------------------------------------------------------
*
* -------------------------------------------------------------------------- */
export async function renderB08DesignDetail(root: HTMLElement): Promise<void> {
export async function renderB07DesignDetail(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 renderB08DesignDetail(root: HTMLElement): Promise<void> {
}
const cadHost = document.createElement("div");
cadHost.className = "b08-cad-host";
cadHost.className = "b07-cad-host";
const frame = document.createElement("iframe");
frame.className = "b08-cad-frame";
frame.src = B08_CAD_APP_URL;
frame.title = L("B08_Design_Title");
frame.className = "b07-cad-frame";
frame.src = B07_CAD_APP_URL;
frame.title = L("B07_Design_Title");
const license = document.createElement("a");
license.className = "b08-cad-license";
license.href = "/b08-cad/THIRD_PARTY_LICENSES.txt";
license.className = "b07-cad-license";
license.href = "/b07-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 renderB08DesignDetail(root: HTMLElement): Promise<void> {
let resolveSave: ((payload: SaveResult) => void) | undefined;
let drawingListEl: HTMLElement | undefined;
const infoPanelHost = document.createElement("div");
infoPanelHost.className = "b08-info-host";
infoPanelHost.className = "b07-info-host";
const updateInfoPanel = (drawing: DesignDrawingItem, response: DesignDrawingResponse): void => {
if (drawing.kind !== "cross") {
@@ -334,11 +334,11 @@ export async function renderB08DesignDetail(root: HTMLElement): Promise<void> {
const findButton = (drawingId: string) =>
drawingListEl?.querySelector<HTMLButtonElement>(
`.b08-drawing-button[data-drawing-id="${drawingId}"]`,
`.b07-drawing-button[data-drawing-id="${drawingId}"]`,
) ?? undefined;
const highlightActive = (drawingId: string) => {
drawingListEl?.querySelectorAll<HTMLButtonElement>(".b08-drawing-button").forEach((item) => {
drawingListEl?.querySelectorAll<HTMLButtonElement>(".b07-drawing-button").forEach((item) => {
item.dataset.active = String(item.dataset.drawingId === drawingId);
});
};
@@ -442,7 +442,7 @@ export async function renderB08DesignDetail(root: HTMLElement): Promise<void> {
showToast("현재 도면을 확정하고 저장했습니다.", "success");
if (result.all_confirmed) {
allDrawingsConfirmed = true;
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[6]);
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[5]);
}
} catch (error) {
showToast(
@@ -511,16 +511,16 @@ export async function renderB08DesignDetail(root: HTMLElement): Promise<void> {
drawingListEl = drawingPanel;
const confirmActions = document.createElement("div");
// 하단 고정은 공용 ui-sidebar-actions로 통일 — 사이드 본문이 [스크롤 영역][액션 줄]로
// 쪼개지고 액션 줄은 스크롤 밖에 남는다(2026-08-18 사용자 지시, B04~B08 공통).
confirmActions.className = "b08-drawing-actions ui-sidebar-actions";
// 쪼개지고 액션 줄은 스크롤 밖에 남는다(2026-08-18 사용자 지시, B04~B07 공통).
confirmActions.className = "b07-drawing-actions ui-sidebar-actions";
confirmActions.append(confirmButton);
drawingPanel.append(infoPanelHost, confirmActions);
const layout = createWorkflowLayout({
title: L("B08_Design_Title"),
title: L("B07_Design_Title"),
steps: workflowSteps(),
activeStep: 5,
activeStep: 4,
leftPanel: drawingPanel,
mainContent: cadHost,
stages: workflowState?.stages,
@@ -535,7 +535,7 @@ export async function renderB08DesignDetail(root: HTMLElement): Promise<void> {
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
},
});
layout.root.classList.add("b08-design-layout");
layout.root.classList.add("b07-design-layout");
root.replaceChildren(layout.root);
// 페이지 진입 시 첫 도면(종단도)을 자동 선택 — 빈 CAD 화면 방지.
@@ -1,23 +1,23 @@
/* =============================================================================
* B08_DesignDetail_UI_Style.css
* B07_DesignDetail_UI_Style.css
* 상세 설계(독립형 2D CAD) 화면 스타일 theme.css 변수만 사용
* ========================================================================== */
/* 워크플로우 레이아웃 높이 (B06 패턴 준수) */
.b08-design-layout {
.b07-design-layout {
height: calc(100vh - var(--spacing-64));
height: calc(100dvh - var(--spacing-64));
min-height: 0;
}
.b08-design-layout .ui-workflow-layout__body,
.b08-design-layout .ui-workflow-layout__main {
.b07-design-layout .ui-workflow-layout__body,
.b07-design-layout .ui-workflow-layout__main {
height: 100%;
min-height: 0;
}
/* B06 확정 산출물 도면 목록 */
.b08-drawing-list {
.b07-drawing-list {
display: flex;
flex-direction: column;
gap: var(--spacing-12);
@@ -26,7 +26,7 @@
overflow-y: auto;
}
.b08-drawing-list__heading {
.b07-drawing-list__heading {
display: flex;
align-items: center;
justify-content: space-between;
@@ -34,13 +34,13 @@
border-bottom: 1px solid var(--color-border);
}
.b08-drawing-list__heading span,
.b08-drawing-list__empty {
.b07-drawing-list__heading span,
.b07-drawing-list__empty {
color: var(--color-text-muted);
font-size: var(--text-body-sm);
}
.b08-drawing-group {
.b07-drawing-group {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
@@ -50,13 +50,13 @@
}
/* 횡단도는 2열 배치 */
.b08-drawing-group[data-kind="cross"] {
.b07-drawing-group[data-kind="cross"] {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-4);
}
.b08-drawing-group h3 {
.b07-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 @@
}
/* 그리드 제목은 두 열을 가로지른다 */
.b08-drawing-group[data-kind="cross"] h3 {
.b07-drawing-group[data-kind="cross"] h3 {
grid-column: 1 / -1;
}
.b08-drawing-button {
.b07-drawing-button {
display: flex;
align-items: center;
justify-content: center;
@@ -86,45 +86,45 @@
text-align: center;
}
.b08-drawing-button__name {
.b07-drawing-button__name {
overflow: hidden;
font-size: var(--text-body-sm);
white-space: nowrap;
text-overflow: ellipsis;
}
.b08-drawing-button:hover,
.b08-drawing-button[data-active="true"] {
.b07-drawing-button:hover,
.b07-drawing-button[data-active="true"] {
border-color: var(--color-border);
background: var(--color-mist-violet);
}
.b08-drawing-button[data-active="true"] {
.b07-drawing-button[data-active="true"] {
color: var(--color-primary);
}
/* 확정: 좌측 띠 + 측점 글자색을 함께 성공색으로 반영 */
.b08-drawing-button[data-confirmed="true"] {
.b07-drawing-button[data-confirmed="true"] {
border-color: color-mix(in srgb, var(--color-success) 35%, var(--color-border));
border-left-color: var(--color-success);
}
.b08-drawing-button[data-confirmed="true"] .b08-drawing-button__name {
.b07-drawing-button[data-confirmed="true"] .b07-drawing-button__name {
color: var(--color-success);
}
/* 하단 고정·배경·상단 구분선은 공용 ui-sidebar-actions가 맡는다(2026-08-18 통합).
여기서는 B08 고유 여백만 남긴다. */
.b08-drawing-actions {
여기서는 B07 고유 여백만 남긴다. */
.b07-drawing-actions {
padding-top: var(--spacing-12);
}
.b08-drawing-actions > button {
.b07-drawing-actions > button {
width: 100%;
}
/* CAD 뷰어 호스트 (상세 페이지 영역) */
.b08-cad-host {
.b07-cad-host {
position: relative;
width: 100%;
height: 100%;
@@ -134,8 +134,8 @@
background-color: var(--color-surface);
}
/* B08 독립형 CAD 앱 임베드 */
.b08-cad-frame {
/* B07 독립형 CAD 앱 임베드 */
.b07-cad-frame {
position: absolute;
inset: 0;
width: 100%;
@@ -143,7 +143,7 @@
border: 0;
}
.b08-cad-license {
.b07-cad-license {
position: absolute;
z-index: 2;
right: 10px;
@@ -154,17 +154,17 @@
text-decoration: none;
}
.b08-cad-license:hover {
.b07-cad-license:hover {
color: var(--color-text-muted);
text-decoration: underline;
}
/* 선택 횡단도의 지반정보/계획정보 (잠정치) */
.b08-info-host:empty {
.b07-info-host:empty {
display: none;
}
.b08-info {
.b07-info {
margin-top: var(--spacing-16);
padding-top: var(--spacing-16);
border-top: 1px solid var(--color-border);
@@ -173,14 +173,14 @@
gap: var(--spacing-8);
}
.b08-info__heading {
.b07-info__heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-8);
}
.b08-info__badge {
.b07-info__badge {
padding: 1px 6px;
font-size: 10px;
color: var(--color-primary);
@@ -188,40 +188,40 @@
border-radius: var(--radius-buttons);
}
.b08-info__badge--confirmed {
.b07-info__badge--confirmed {
color: var(--color-surface);
background: var(--color-success);
}
.b08-info__block {
.b07-info__block {
display: flex;
flex-direction: column;
gap: 2px;
}
.b08-info__block h4 {
.b07-info__block h4 {
margin: var(--spacing-8) 0 2px;
font-size: 0.78rem;
color: var(--color-text-muted);
}
.b08-info__row {
.b07-info__row {
display: flex;
justify-content: space-between;
gap: var(--spacing-8);
font-size: 0.76rem;
}
.b08-info__key {
.b07-info__key {
color: var(--color-text-muted);
}
.b08-info__val {
.b07-info__val {
color: var(--color-text);
font-variant-numeric: tabular-nums;
}
.b08-info__empty {
.b07-info__empty {
font-size: 0.76rem;
color: var(--color-text-muted);
}

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

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

@@ -225,7 +225,7 @@ export const Toolbar: FC = () => {
<header className="cad-titlebar controls">
<div className="cad-brand">
<strong>Aislo CAD</strong>
<span>B08 </span>
<span>B07 </span>
</div>
<div className="cad-file-state">
<span className="cad-file-state__dot" />

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