260720_1
This commit is contained in:
@@ -34,8 +34,26 @@ import {
|
||||
invalidateDesignDrawing,
|
||||
type CadDrawing,
|
||||
type DesignDrawingItem,
|
||||
type DesignDrawingResponse,
|
||||
type QuantityTable,
|
||||
} from "./B07_wf4_DesignDetail_Api_Fetch";
|
||||
import { buildQuantityTable } from "./B07_wf4_DesignDetail_UI_QuantityTable";
|
||||
|
||||
/** CAD 앱 수량 패널로 넘기는 설계 컨텍스트 (openwebcad DesignMeta와 동일 형식). */
|
||||
interface DesignMeta {
|
||||
kind: "cross" | "longitudinal";
|
||||
title: string;
|
||||
info: string;
|
||||
confirmed: boolean;
|
||||
quantityTable: QuantityTable | null;
|
||||
hasPrev: boolean;
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
/** CAD 저장 응답 (도면 + 편집된 수량표). */
|
||||
interface SaveResult {
|
||||
drawing: CadDrawing;
|
||||
quantityTable: QuantityTable | null;
|
||||
}
|
||||
|
||||
/** B07 독립형 CAD 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
|
||||
const B07_CAD_APP_URL = "/b07-cad/index.html";
|
||||
@@ -52,6 +70,7 @@ 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";
|
||||
|
||||
/** 측점 간격을 연속 chainage 차이의 최빈값으로 추정한다 (B06 그래프와 동일 방식). */
|
||||
function inferStationInterval(chainages: number[]): number {
|
||||
@@ -86,7 +105,8 @@ function stationLabel(chainage: number, interval: number): string {
|
||||
/** B06 확정 산출물 기반 도면 목록 패널. */
|
||||
function buildDrawingSidePanel(
|
||||
drawings: DesignDrawingItem[],
|
||||
onSelect: (drawing: DesignDrawingItem, button: HTMLButtonElement) => Promise<void>,
|
||||
stationInterval: number,
|
||||
onSelect: (drawing: DesignDrawingItem) => void,
|
||||
errorMessage?: string,
|
||||
): HTMLDivElement {
|
||||
const panel = document.createElement("div");
|
||||
@@ -108,11 +128,6 @@ function buildDrawingSidePanel(
|
||||
return panel;
|
||||
}
|
||||
|
||||
const crossChainages = drawings
|
||||
.filter((item) => item.kind === "cross" && typeof item.chainage_m === "number")
|
||||
.map((item) => item.chainage_m as number);
|
||||
const stationInterval = inferStationInterval(crossChainages);
|
||||
|
||||
const groups: [string, DesignDrawingItem["kind"], DesignDrawingItem[]][] = [
|
||||
["종단도", "longitudinal", drawings.filter((item) => item.kind === "longitudinal")],
|
||||
["횡단도", "cross", drawings.filter((item) => item.kind === "cross")],
|
||||
@@ -138,7 +153,7 @@ function buildDrawingSidePanel(
|
||||
? stationLabel(drawing.chainage_m, stationInterval)
|
||||
: drawing.label;
|
||||
button.append(name);
|
||||
button.addEventListener("click", () => void onSelect(drawing, button));
|
||||
button.addEventListener("click", () => onSelect(drawing));
|
||||
section.append(button);
|
||||
}
|
||||
panel.append(section);
|
||||
@@ -182,66 +197,99 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
license.textContent = "Drawing engine based on OpenWebCAD · MIT License";
|
||||
cadHost.append(frame, license);
|
||||
|
||||
const crossChainages = drawings
|
||||
.filter((item) => item.kind === "cross" && typeof item.chainage_m === "number")
|
||||
.map((item) => item.chainage_m as number);
|
||||
const stationInterval = inferStationInterval(crossChainages);
|
||||
|
||||
let cadReady = false;
|
||||
let pendingDrawing: CadDrawing | undefined;
|
||||
let pendingLoad: { drawing: CadDrawing; meta: DesignMeta } | undefined;
|
||||
let currentDrawing: DesignDrawingItem | undefined;
|
||||
let currentButton: HTMLButtonElement | undefined;
|
||||
let currentIndex = -1;
|
||||
let currentConfirmed = false;
|
||||
let allDrawingsConfirmed = drawings.length > 0 && drawings.every((item) => item.confirmed);
|
||||
let resolveSave: ((drawing: CadDrawing) => void) | undefined;
|
||||
let resolveSave: ((payload: SaveResult) => void) | undefined;
|
||||
let drawingListEl: HTMLElement | undefined;
|
||||
|
||||
// 편집 가능한 수량 산출표 (횡단도 선택 시 CAD 영역 위에 오버레이).
|
||||
// 값 편집은 CAD 도면 변경과 동일하게 확정 상태를 롤백시킨다.
|
||||
const quantityTable = buildQuantityTable(() => void invalidateCurrentDrawing());
|
||||
cadHost.append(quantityTable.element);
|
||||
const confirmButton = createButton({
|
||||
label: "현재 도면 확정",
|
||||
variant: "filled",
|
||||
onClick: () => void confirmCurrentDrawing(),
|
||||
});
|
||||
confirmButton.disabled = true;
|
||||
const sendDrawing = (drawing: CadDrawing) => {
|
||||
pendingDrawing = drawing;
|
||||
if (!cadReady) return;
|
||||
frame.contentWindow?.postMessage({ type: CAD_LOAD_MESSAGE, drawing }, window.location.origin);
|
||||
pendingDrawing = undefined;
|
||||
};
|
||||
const selectDrawing = async (drawing: DesignDrawingItem, button: HTMLButtonElement) => {
|
||||
if (!projectId) return;
|
||||
const buttons = button
|
||||
.closest(".b07-drawing-list")
|
||||
?.querySelectorAll<HTMLButtonElement>(".b07-drawing-button");
|
||||
buttons?.forEach((item) => {
|
||||
item.disabled = true;
|
||||
item.dataset.active = String(item === button);
|
||||
|
||||
const findButton = (drawingId: string) =>
|
||||
drawingListEl?.querySelector<HTMLButtonElement>(
|
||||
`.b07-drawing-button[data-drawing-id="${drawingId}"]`,
|
||||
) ?? undefined;
|
||||
|
||||
const highlightActive = (drawingId: string) => {
|
||||
drawingListEl?.querySelectorAll<HTMLButtonElement>(".b07-drawing-button").forEach((item) => {
|
||||
item.dataset.active = String(item.dataset.drawingId === drawingId);
|
||||
});
|
||||
button.dataset.loading = "true";
|
||||
};
|
||||
|
||||
const buildMeta = (
|
||||
drawing: DesignDrawingItem,
|
||||
response: DesignDrawingResponse,
|
||||
index: number,
|
||||
): DesignMeta => ({
|
||||
kind: drawing.kind,
|
||||
title:
|
||||
drawing.kind === "cross" && typeof drawing.chainage_m === "number"
|
||||
? stationLabel(drawing.chainage_m, stationInterval)
|
||||
: "종단도 전체",
|
||||
info: drawing.kind === "cross" ? drawing.label : "",
|
||||
confirmed: response.confirmed,
|
||||
quantityTable: response.quantity_table ?? null,
|
||||
hasPrev: index > 0,
|
||||
hasNext: index < drawings.length - 1,
|
||||
});
|
||||
|
||||
const sendLoad = (drawing: CadDrawing, meta: DesignMeta) => {
|
||||
pendingLoad = { drawing, meta };
|
||||
if (!cadReady) return;
|
||||
frame.contentWindow?.postMessage(
|
||||
{ type: CAD_LOAD_MESSAGE, drawing, meta },
|
||||
window.location.origin,
|
||||
);
|
||||
pendingLoad = undefined;
|
||||
};
|
||||
|
||||
const loadDrawing = async (drawing: DesignDrawingItem, index: number) => {
|
||||
if (!projectId) return;
|
||||
highlightActive(drawing.id);
|
||||
const button = findButton(drawing.id);
|
||||
if (button) button.dataset.loading = "true";
|
||||
cadHost.dataset.loading = "true";
|
||||
try {
|
||||
const response = await fetchDesignDrawing(projectId, drawing.id);
|
||||
currentDrawing = drawing;
|
||||
currentButton = button;
|
||||
currentIndex = index;
|
||||
currentConfirmed = response.confirmed;
|
||||
confirmButton.disabled = response.confirmed;
|
||||
sendDrawing(response.drawing);
|
||||
if (drawing.kind === "cross") {
|
||||
quantityTable.update(button.textContent ?? drawing.label, response.quantity_table);
|
||||
} else {
|
||||
quantityTable.element.hidden = true;
|
||||
}
|
||||
sendLoad(response.drawing, buildMeta(drawing, response, index));
|
||||
} catch (error) {
|
||||
cadHost.dataset.loading = "false";
|
||||
cadHost.dataset.error =
|
||||
error instanceof Error ? error.message : "CAD 도면을 불러오지 못했습니다.";
|
||||
} finally {
|
||||
button.dataset.loading = "false";
|
||||
buttons?.forEach((item) => {
|
||||
item.disabled = false;
|
||||
});
|
||||
if (button) button.dataset.loading = "false";
|
||||
}
|
||||
};
|
||||
|
||||
const requestCadDrawing = (): Promise<CadDrawing> =>
|
||||
const selectDrawing = (drawing: DesignDrawingItem) => {
|
||||
void loadDrawing(drawing, drawings.indexOf(drawing));
|
||||
};
|
||||
|
||||
const navigateDrawing = (direction: "prev" | "next") => {
|
||||
if (currentIndex < 0) return;
|
||||
const target = direction === "prev" ? currentIndex - 1 : currentIndex + 1;
|
||||
if (target < 0 || target >= drawings.length) return;
|
||||
void loadDrawing(drawings[target], target);
|
||||
};
|
||||
|
||||
const requestCadDrawing = (): Promise<SaveResult> =>
|
||||
new Promise((resolve, reject) => {
|
||||
resolveSave = resolve;
|
||||
frame.contentWindow?.postMessage({ type: CAD_SAVE_REQUEST_MESSAGE }, window.location.origin);
|
||||
@@ -256,17 +304,18 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
if (!projectId || !currentDrawing) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const drawing = await requestCadDrawing();
|
||||
const saved = await requestCadDrawing();
|
||||
const result = await confirmDesignDrawing(
|
||||
projectId,
|
||||
currentDrawing.id,
|
||||
drawing,
|
||||
currentDrawing.kind === "cross" ? quantityTable.getValues() : null,
|
||||
saved.drawing,
|
||||
currentDrawing.kind === "cross" ? (saved.quantityTable ?? null) : null,
|
||||
);
|
||||
currentConfirmed = true;
|
||||
currentDrawing.confirmed = true;
|
||||
confirmButton.disabled = true;
|
||||
if (currentButton) currentButton.dataset.confirmed = "true";
|
||||
const button = findButton(currentDrawing.id);
|
||||
if (button) button.dataset.confirmed = "true";
|
||||
showToast("현재 도면을 확정하고 저장했습니다.", "success");
|
||||
if (result.all_confirmed) {
|
||||
allDrawingsConfirmed = true;
|
||||
@@ -289,7 +338,8 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
allDrawingsConfirmed = false;
|
||||
currentDrawing.confirmed = false;
|
||||
confirmButton.disabled = false;
|
||||
if (currentButton) currentButton.dataset.confirmed = "false";
|
||||
const button = findButton(currentDrawing.id);
|
||||
if (button) button.dataset.confirmed = "false";
|
||||
if (wasConfirmed) {
|
||||
try {
|
||||
await invalidateDesignDrawing(projectId, currentDrawing.id);
|
||||
@@ -308,24 +358,34 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
type?: string;
|
||||
detail?: string;
|
||||
drawing?: CadDrawing;
|
||||
quantityTable?: QuantityTable | null;
|
||||
direction?: "prev" | "next";
|
||||
};
|
||||
if (message.type === CAD_READY_MESSAGE) {
|
||||
cadReady = true;
|
||||
if (pendingDrawing) sendDrawing(pendingDrawing);
|
||||
if (pendingLoad) sendLoad(pendingLoad.drawing, pendingLoad.meta);
|
||||
} else if (message.type === CAD_LOADED_MESSAGE) {
|
||||
cadHost.dataset.loading = "false";
|
||||
} else if (message.type === CAD_ERROR_MESSAGE) {
|
||||
cadHost.dataset.error = message.detail ?? "CAD 도면을 표시하지 못했습니다.";
|
||||
} else if (message.type === CAD_CHANGED_MESSAGE) {
|
||||
void invalidateCurrentDrawing();
|
||||
} else if (message.type === CAD_NAVIGATE_MESSAGE && message.direction) {
|
||||
navigateDrawing(message.direction);
|
||||
} else if (message.type === CAD_SAVE_RESPONSE_MESSAGE && message.drawing && resolveSave) {
|
||||
const resolve = resolveSave;
|
||||
resolveSave = undefined;
|
||||
resolve(message.drawing);
|
||||
resolve({ drawing: message.drawing, quantityTable: message.quantityTable ?? null });
|
||||
}
|
||||
});
|
||||
|
||||
const drawingPanel = buildDrawingSidePanel(drawings, selectDrawing, drawingError);
|
||||
const drawingPanel = buildDrawingSidePanel(
|
||||
drawings,
|
||||
stationInterval,
|
||||
selectDrawing,
|
||||
drawingError,
|
||||
);
|
||||
drawingListEl = drawingPanel;
|
||||
const confirmActions = document.createElement("div");
|
||||
confirmActions.className = "b07-drawing-actions";
|
||||
confirmActions.append(confirmButton);
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
/* =============================================================================
|
||||
* B07_wf4_DesignDetail_UI_QuantityTable.ts
|
||||
* 횡단도 수량 산출표 (편집 가능). 첨부 양식의 병합셀 구조를 12열 표로 재현한다.
|
||||
*
|
||||
* 값은 백엔드 `_quantity_table`가 내려준 초기값으로 채우되, 사용자가 각 칸을
|
||||
* 직접 수정할 수 있다. 절토고/성토고는 지반고·계획고에서 파생되어 읽기 전용이며
|
||||
* 입력 변경 시 즉시 재계산된다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { QuantityTable } from "./B07_wf4_DesignDetail_Api_Fetch";
|
||||
|
||||
/** 편집 입력이 있는 항목 키 (cut/fill 제외 — 파생 읽기전용). */
|
||||
const EDITABLE_KEYS = [
|
||||
"ground",
|
||||
"planned",
|
||||
"cut_soil",
|
||||
"cut_soft_rock",
|
||||
"cut_rock",
|
||||
"tree_removal",
|
||||
"fill_slope_protection",
|
||||
"cut_slope_protection",
|
||||
"ditch_soil",
|
||||
"ditch_soft_rock",
|
||||
"ditch_rock",
|
||||
"embankment",
|
||||
"grubbing",
|
||||
"surface_grading",
|
||||
] as const;
|
||||
|
||||
export interface QuantityTableController {
|
||||
element: HTMLElement;
|
||||
/** 새 도면 선택 시 값·측점명을 갱신한다. */
|
||||
update(stationTitle: string, values: QuantityTable | null | undefined): void;
|
||||
/** 현재 입력값(파생 cut/fill 포함)을 수집한다. */
|
||||
getValues(): QuantityTable;
|
||||
}
|
||||
|
||||
function round2(value: number): number {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function formatValue(value: number | null | undefined): string {
|
||||
return typeof value === "number" && Number.isFinite(value) ? String(round2(value)) : "";
|
||||
}
|
||||
|
||||
function parseValue(raw: string): number | null {
|
||||
const text = raw.trim();
|
||||
if (!text) return null;
|
||||
const parsed = Number(text);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
/** 편집 가능한 값 입력 칸을 만든다. */
|
||||
function valueCell(
|
||||
key: string,
|
||||
colSpan: number,
|
||||
inputs: Map<string, HTMLInputElement>,
|
||||
options: { readonly?: boolean } = {},
|
||||
): HTMLTableCellElement {
|
||||
const cell = document.createElement("td");
|
||||
cell.colSpan = colSpan;
|
||||
cell.className = "b07-qtable__value";
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.inputMode = "decimal";
|
||||
input.autocomplete = "off";
|
||||
input.dataset.key = key;
|
||||
if (options.readonly) {
|
||||
input.readOnly = true;
|
||||
cell.classList.add("b07-qtable__value--derived");
|
||||
}
|
||||
inputs.set(key, input);
|
||||
cell.append(input);
|
||||
return cell;
|
||||
}
|
||||
|
||||
/** 라벨(헤더) 셀을 만든다. */
|
||||
function labelCell(
|
||||
text: string,
|
||||
colSpan: number,
|
||||
options: { rowSpan?: number; vertical?: boolean } = {},
|
||||
): HTMLTableCellElement {
|
||||
const cell = document.createElement("th");
|
||||
cell.scope = "row";
|
||||
cell.colSpan = colSpan;
|
||||
if (options.rowSpan) cell.rowSpan = options.rowSpan;
|
||||
cell.className = "b07-qtable__label";
|
||||
if (options.vertical) cell.classList.add("b07-qtable__label--vertical");
|
||||
cell.textContent = text;
|
||||
return cell;
|
||||
}
|
||||
|
||||
/**
|
||||
* 첨부 양식과 동일한 편집 가능 수량 산출표를 생성한다.
|
||||
* @param onEdit 사용자가 값을 바꿀 때마다 호출 (확정 상태 롤백 연동용)
|
||||
*/
|
||||
export function buildQuantityTable(onEdit: () => void): QuantityTableController {
|
||||
const inputs = new Map<string, HTMLInputElement>();
|
||||
|
||||
const container = document.createElement("section");
|
||||
container.className = "b07-qtable";
|
||||
container.hidden = true;
|
||||
|
||||
const table = document.createElement("table");
|
||||
const body = document.createElement("tbody");
|
||||
|
||||
// 1행: 측 점 | (측점명)
|
||||
const titleRow = document.createElement("tr");
|
||||
titleRow.append(labelCell("측 점", 2));
|
||||
const titleCell = document.createElement("td");
|
||||
titleCell.colSpan = 10;
|
||||
titleCell.className = "b07-qtable__station";
|
||||
titleRow.append(titleCell);
|
||||
body.append(titleRow);
|
||||
|
||||
// 2행: 지반고 | 계획고 | 절토고 | 성토고
|
||||
const baseRow = document.createElement("tr");
|
||||
baseRow.append(labelCell("지반고", 2), valueCell("ground", 1, inputs));
|
||||
baseRow.append(labelCell("계획고", 2), valueCell("planned", 1, inputs));
|
||||
baseRow.append(labelCell("절토고", 2), valueCell("cut", 1, inputs, { readonly: true }));
|
||||
baseRow.append(labelCell("성토고", 2), valueCell("fill", 1, inputs, { readonly: true }));
|
||||
body.append(baseRow);
|
||||
|
||||
// 3행: 흙깎기 토사 | 지장목제거 | 옆도랑파기 토사
|
||||
const row3 = document.createElement("tr");
|
||||
row3.append(labelCell("흙깎기", 1, { rowSpan: 3, vertical: true }));
|
||||
row3.append(labelCell("토사", 1), valueCell("cut_soil", 2, inputs));
|
||||
row3.append(labelCell("지장목제거", 2), valueCell("tree_removal", 2, inputs));
|
||||
row3.append(labelCell("옆도랑파기", 1, { rowSpan: 3, vertical: true }));
|
||||
row3.append(labelCell("토사", 1), valueCell("ditch_soil", 2, inputs));
|
||||
body.append(row3);
|
||||
|
||||
// 4행: 연암 | 비탈보호공 성토면 | 연암
|
||||
const row4 = document.createElement("tr");
|
||||
row4.append(labelCell("연암", 1), valueCell("cut_soft_rock", 2, inputs));
|
||||
row4.append(labelCell("비탈보호공", 1, { rowSpan: 2, vertical: true }));
|
||||
row4.append(labelCell("성토면", 1), valueCell("fill_slope_protection", 2, inputs));
|
||||
row4.append(labelCell("연암", 1), valueCell("ditch_soft_rock", 2, inputs));
|
||||
body.append(row4);
|
||||
|
||||
// 5행: 보통암 | 절토면 | 보통암
|
||||
const row5 = document.createElement("tr");
|
||||
row5.append(labelCell("보통암", 1), valueCell("cut_rock", 2, inputs));
|
||||
row5.append(labelCell("절토면", 1), valueCell("cut_slope_protection", 2, inputs));
|
||||
row5.append(labelCell("보통암", 1), valueCell("ditch_rock", 2, inputs));
|
||||
body.append(row5);
|
||||
|
||||
// 6행: 흙쌓기 | 제근 | 노면고르기
|
||||
const row6 = document.createElement("tr");
|
||||
row6.append(labelCell("흙쌓기", 2), valueCell("embankment", 2, inputs));
|
||||
row6.append(labelCell("제근", 2), valueCell("grubbing", 2, inputs));
|
||||
row6.append(labelCell("노면고르기", 2), valueCell("surface_grading", 2, inputs));
|
||||
body.append(row6);
|
||||
|
||||
table.append(body);
|
||||
container.append(table);
|
||||
|
||||
const groundInput = inputs.get("ground");
|
||||
const plannedInput = inputs.get("planned");
|
||||
const cutInput = inputs.get("cut");
|
||||
const fillInput = inputs.get("fill");
|
||||
|
||||
const recomputeCutFill = () => {
|
||||
const ground = parseValue(groundInput?.value ?? "");
|
||||
const planned = parseValue(plannedInput?.value ?? "");
|
||||
if (ground !== null && planned !== null) {
|
||||
if (cutInput) cutInput.value = formatValue(Math.max(ground - planned, 0));
|
||||
if (fillInput) fillInput.value = formatValue(Math.max(planned - ground, 0));
|
||||
} else {
|
||||
if (cutInput) cutInput.value = "";
|
||||
if (fillInput) fillInput.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
for (const key of EDITABLE_KEYS) {
|
||||
const input = inputs.get(key);
|
||||
input?.addEventListener("input", () => {
|
||||
if (key === "ground" || key === "planned") recomputeCutFill();
|
||||
onEdit();
|
||||
});
|
||||
}
|
||||
|
||||
const update: QuantityTableController["update"] = (stationTitle, values) => {
|
||||
titleCell.textContent = stationTitle;
|
||||
for (const [key, input] of inputs) {
|
||||
input.value = formatValue(values?.[key]);
|
||||
}
|
||||
recomputeCutFill();
|
||||
container.hidden = false;
|
||||
};
|
||||
|
||||
const getValues: QuantityTableController["getValues"] = () => {
|
||||
const result: QuantityTable = {};
|
||||
for (const [key, input] of inputs) {
|
||||
result[key] = parseValue(input.value);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return { element: container, update, getValues };
|
||||
}
|
||||
@@ -157,89 +157,3 @@
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 수량 산출표 (횡단도 편집 오버레이) — 첨부 양식 재현
|
||||
* -------------------------------------------------------------------------- */
|
||||
.b07-qtable {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
max-width: min(600px, 62%);
|
||||
max-height: 48%;
|
||||
overflow: auto;
|
||||
padding: var(--spacing-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 94%, transparent);
|
||||
box-shadow: 0 6px 18px #0006;
|
||||
}
|
||||
|
||||
.b07-qtable[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b07-qtable table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.b07-qtable th,
|
||||
.b07-qtable td {
|
||||
padding: 2px 3px;
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.b07-qtable__label {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
background: color-mix(in srgb, var(--color-surface) 78%, #000);
|
||||
}
|
||||
|
||||
/* 세로 라벨(흙깎기 / 옆도랑파기 / 비탈보호공) */
|
||||
.b07-qtable__label--vertical {
|
||||
width: 20px;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.b07-qtable__station {
|
||||
color: var(--color-text);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.b07-qtable__value {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.b07-qtable__value input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 3px 4px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.b07-qtable__value input:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
}
|
||||
|
||||
/* 파생 읽기전용 (절토고 / 성토고) */
|
||||
.b07-qtable__value--derived input {
|
||||
color: var(--color-text-muted);
|
||||
background: color-mix(in srgb, var(--color-surface) 68%, #000);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
:root {
|
||||
--cad-title-height: 42px;
|
||||
--cad-ribbon-height: 82px;
|
||||
--cad-command-height: 58px;
|
||||
/* 하단 명령어 입력창 제거 → 높이 0으로 캔버스가 공간을 회수 (추후 사용성 개선 예정) */
|
||||
--cad-command-height: 0px;
|
||||
--cad-status-height: 28px;
|
||||
--cad-panel-width: 248px;
|
||||
font-family: Inter, Pretendard, "Noto Sans KR", system-ui, sans-serif;
|
||||
@@ -343,16 +344,9 @@ body > canvas[data-id="canvas"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 하단 명령어 입력창은 숨김 처리 (마우스 커서 단축키 입력으로 대체) */
|
||||
.cad-command-area {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
right: 0;
|
||||
bottom: var(--cad-status-height);
|
||||
left: var(--cad-panel-width);
|
||||
height: var(--cad-command-height);
|
||||
padding: 5px 10px;
|
||||
background: #151c24f2;
|
||||
border-top: 1px solid #3b4b5a;
|
||||
display: none;
|
||||
}
|
||||
.cad-command-prompt {
|
||||
display: flex;
|
||||
@@ -435,6 +429,130 @@ body > canvas[data-id="canvas"] {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 수량 산출표 패널 (CAD 화면 하단 중심, 접이식) — 첨부 양식
|
||||
* -------------------------------------------------------------------------- */
|
||||
.cad-qtable {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
bottom: calc(var(--cad-status-height) + 8px);
|
||||
left: calc(var(--cad-panel-width) + (100vw - var(--cad-panel-width)) / 2);
|
||||
transform: translateX(-50%);
|
||||
max-width: min(720px, calc(100vw - var(--cad-panel-width) - 24px));
|
||||
border: 1px solid #465565;
|
||||
border-radius: 6px;
|
||||
background: #202b37f2;
|
||||
box-shadow: 0 4px 16px #0009;
|
||||
color: #dce6f2;
|
||||
}
|
||||
|
||||
.cad-qtable__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
.cad-qtable__title {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.cad-qtable__title strong {
|
||||
color: #f7fbff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.cad-qtable__title span {
|
||||
overflow: hidden;
|
||||
color: #91a2b5;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cad-qtable__status {
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
}
|
||||
.cad-qtable__status[data-confirmed="true"] {
|
||||
background: #1f4d33;
|
||||
color: #7fdca4;
|
||||
}
|
||||
.cad-qtable__status[data-confirmed="false"] {
|
||||
background: #4a3a1c;
|
||||
color: #e6c07a;
|
||||
}
|
||||
.cad-qtable__nav,
|
||||
.cad-qtable__collapse {
|
||||
width: 26px;
|
||||
height: 24px;
|
||||
border: 1px solid #40505f;
|
||||
border-radius: 4px;
|
||||
background: #2a3745;
|
||||
color: #cdd8e4;
|
||||
font-size: 13px;
|
||||
}
|
||||
.cad-qtable__nav:hover:not(:disabled),
|
||||
.cad-qtable__collapse:hover {
|
||||
background: #31516b;
|
||||
}
|
||||
.cad-qtable__nav:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cad-qtable__body {
|
||||
padding: 0 8px 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.cad-qtable table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.cad-qtable th,
|
||||
.cad-qtable td {
|
||||
padding: 0;
|
||||
border: 1px solid #40505f;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cad-qtable__label {
|
||||
padding: 2px 5px;
|
||||
color: #a9bccd;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
background: #26333f;
|
||||
}
|
||||
.cad-qtable__label--vertical {
|
||||
width: 18px;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.cad-qtable__value input {
|
||||
width: 48px;
|
||||
padding: 3px 2px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #eaf1f8;
|
||||
font: inherit;
|
||||
text-align: center;
|
||||
}
|
||||
.cad-qtable__value input:focus {
|
||||
outline: 2px solid #4ca6e8;
|
||||
outline-offset: -2px;
|
||||
background: #12324a;
|
||||
}
|
||||
.cad-qtable__value--derived input {
|
||||
color: #8fa3b5;
|
||||
background: #1a242e;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
:root {
|
||||
--cad-panel-width: 200px;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import './App.css';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { QuantityPanel } from './components/QuantityPanel.tsx';
|
||||
import { Toolbar } from './components/Toolbar.tsx';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="cad-app">
|
||||
<Toolbar />
|
||||
<QuantityPanel />
|
||||
<ToastContainer position="bottom-right" theme="light" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,21 @@ export enum HtmlEvent {
|
||||
DRAWING_CHANGED = 'DRAWING_CHANGED',
|
||||
}
|
||||
|
||||
/** 부모(B07 페이지)가 도면과 함께 넘기는 설계 컨텍스트 (수량 패널 표시용). */
|
||||
export interface DesignMeta {
|
||||
kind: 'cross' | 'longitudinal';
|
||||
/** 패널 제목 (측점 라벨, 예: "2+0.0" 또는 "종단도 전체") */
|
||||
title: string;
|
||||
/** 측점 부가 정보 (예: "STA.0+050.000") */
|
||||
info: string;
|
||||
confirmed: boolean;
|
||||
/** 수량 산출표 값 (횡단도만). 미산정 항목은 null. */
|
||||
quantityTable: Record<string, number | null> | null;
|
||||
/** 이전/다음 도면 존재 여부 (경계에서 버튼 비활성화) */
|
||||
hasPrev: boolean;
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
export interface StateMetaData {
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
import { type FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { HtmlEvent } from '../App.types';
|
||||
import {
|
||||
notifyDrawingChangedByTable,
|
||||
requestDrawingNavigation,
|
||||
} from '../integration/aislo-drawing-bridge';
|
||||
import { getDesignMeta, setDesignQuantityTable } from '../state';
|
||||
|
||||
/** 편집 가능한 항목 키 (cut/fill 은 지반고·계획고에서 파생되는 읽기전용). */
|
||||
const EDITABLE_KEYS = [
|
||||
'ground',
|
||||
'planned',
|
||||
'cut_soil',
|
||||
'cut_soft_rock',
|
||||
'cut_rock',
|
||||
'tree_removal',
|
||||
'fill_slope_protection',
|
||||
'cut_slope_protection',
|
||||
'ditch_soil',
|
||||
'ditch_soft_rock',
|
||||
'ditch_rock',
|
||||
'embankment',
|
||||
'grubbing',
|
||||
'surface_grading',
|
||||
] as const;
|
||||
|
||||
function round2(value: number): number {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function formatValue(value: number | null | undefined): string {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? String(round2(value)) : '';
|
||||
}
|
||||
|
||||
function parseValue(raw: string): number | null {
|
||||
const text = raw.trim();
|
||||
if (!text) return null;
|
||||
const parsed = Number(text);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
/** 편집 값 문자열 → 저장용 수치 테이블 (파생 cut/fill 포함) */
|
||||
function toNumericTable(values: Record<string, string>): Record<string, number | null> {
|
||||
const table: Record<string, number | null> = {};
|
||||
for (const key of EDITABLE_KEYS) table[key] = parseValue(values[key] ?? '');
|
||||
const ground = table.ground;
|
||||
const planned = table.planned;
|
||||
if (typeof ground === 'number' && typeof planned === 'number') {
|
||||
table.cut = Math.max(ground - planned, 0);
|
||||
table.fill = Math.max(planned - ground, 0);
|
||||
} else {
|
||||
table.cut = null;
|
||||
table.fill = null;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
interface ValueInputProps {
|
||||
fieldKey: (typeof EDITABLE_KEYS)[number];
|
||||
colSpan: number;
|
||||
values: Record<string, string>;
|
||||
onEdit: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
const ValueInput: FC<ValueInputProps> = ({ fieldKey, colSpan, values, onEdit }) => (
|
||||
<td className="cad-qtable__value" colSpan={colSpan}>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
autoComplete="off"
|
||||
value={values[fieldKey] ?? ''}
|
||||
onChange={(event) => onEdit(fieldKey, event.target.value)}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
|
||||
const DerivedCell: FC<{ value: string }> = ({ value }) => (
|
||||
<td className="cad-qtable__value cad-qtable__value--derived" colSpan={1}>
|
||||
<input type="text" readOnly value={value} tabIndex={-1} />
|
||||
</td>
|
||||
);
|
||||
|
||||
/**
|
||||
* 횡단도 수량 산출표 (CAD 화면 내부, 하단 중심 접이식 패널).
|
||||
* 헤더(제목·측점정보·확정상태·이전/다음)는 접어도 항상 보인다.
|
||||
*/
|
||||
export const QuantityPanel: FC = () => {
|
||||
const [meta, setMeta] = useState(getDesignMeta());
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const identityRef = useRef<string>('');
|
||||
|
||||
const refresh = useCallback(() => setMeta(getDesignMeta()), []);
|
||||
useEffect(() => {
|
||||
window.addEventListener(HtmlEvent.UPDATE_STATE, refresh);
|
||||
return () => window.removeEventListener(HtmlEvent.UPDATE_STATE, refresh);
|
||||
}, [refresh]);
|
||||
|
||||
// 새 도면(측점)으로 바뀌면 입력값을 원본값으로 초기화한다.
|
||||
const identity = meta ? `${meta.kind}:${meta.title}` : '';
|
||||
useEffect(() => {
|
||||
if (!meta) return;
|
||||
if (identityRef.current === identity) return;
|
||||
identityRef.current = identity;
|
||||
const next: Record<string, string> = {};
|
||||
for (const key of EDITABLE_KEYS) next[key] = formatValue(meta.quantityTable?.[key]);
|
||||
setValues(next);
|
||||
}, [identity, meta]);
|
||||
|
||||
if (!meta) return null;
|
||||
|
||||
const handleEdit = (key: string, value: string) => {
|
||||
const nextValues = { ...values, [key]: value };
|
||||
setValues(nextValues);
|
||||
setDesignQuantityTable(toNumericTable(nextValues));
|
||||
notifyDrawingChangedByTable();
|
||||
};
|
||||
|
||||
const ground = parseValue(values.ground ?? '');
|
||||
const planned = parseValue(values.planned ?? '');
|
||||
const cutText =
|
||||
ground !== null && planned !== null ? formatValue(Math.max(ground - planned, 0)) : '';
|
||||
const fillText =
|
||||
ground !== null && planned !== null ? formatValue(Math.max(planned - ground, 0)) : '';
|
||||
|
||||
return (
|
||||
<section className="cad-qtable controls" data-collapsed={collapsed}>
|
||||
<header className="cad-qtable__header">
|
||||
<button
|
||||
type="button"
|
||||
className="cad-qtable__nav"
|
||||
disabled={!meta.hasPrev}
|
||||
title="이전 도면"
|
||||
onClick={() => requestDrawingNavigation('prev')}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div className="cad-qtable__title">
|
||||
<strong>{meta.title}</strong>
|
||||
{meta.info && <span>{meta.info}</span>}
|
||||
<em
|
||||
className="cad-qtable__status"
|
||||
data-confirmed={meta.confirmed}
|
||||
title={meta.confirmed ? '설계 확정됨' : '미확정'}
|
||||
>
|
||||
{meta.confirmed ? '확정' : '미확정'}
|
||||
</em>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cad-qtable__nav"
|
||||
disabled={!meta.hasNext}
|
||||
title="다음 도면"
|
||||
onClick={() => requestDrawingNavigation('next')}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
{meta.kind === 'cross' && (
|
||||
<button
|
||||
type="button"
|
||||
className="cad-qtable__collapse"
|
||||
title={collapsed ? '표 펼치기' : '표 접기'}
|
||||
onClick={() => setCollapsed((value) => !value)}
|
||||
>
|
||||
{collapsed ? '▲' : '▼'}
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{meta.kind === 'cross' && !collapsed && (
|
||||
<div className="cad-qtable__body">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
지반고
|
||||
</th>
|
||||
<ValueInput fieldKey="ground" colSpan={1} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
계획고
|
||||
</th>
|
||||
<ValueInput fieldKey="planned" colSpan={1} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
절토고
|
||||
</th>
|
||||
<DerivedCell value={cutText} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
성토고
|
||||
</th>
|
||||
<DerivedCell value={fillText} />
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label cad-qtable__label--vertical" rowSpan={3}>
|
||||
흙깎기
|
||||
</th>
|
||||
<th className="cad-qtable__label">토사</th>
|
||||
<ValueInput fieldKey="cut_soil" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
지장목제거
|
||||
</th>
|
||||
<ValueInput
|
||||
fieldKey="tree_removal"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
<th className="cad-qtable__label cad-qtable__label--vertical" rowSpan={3}>
|
||||
옆도랑파기
|
||||
</th>
|
||||
<th className="cad-qtable__label">토사</th>
|
||||
<ValueInput fieldKey="ditch_soil" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label">연암</th>
|
||||
<ValueInput
|
||||
fieldKey="cut_soft_rock"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
<th className="cad-qtable__label cad-qtable__label--vertical" rowSpan={2}>
|
||||
비탈보호공
|
||||
</th>
|
||||
<th className="cad-qtable__label">성토면</th>
|
||||
<ValueInput
|
||||
fieldKey="fill_slope_protection"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
<th className="cad-qtable__label">연암</th>
|
||||
<ValueInput
|
||||
fieldKey="ditch_soft_rock"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label">보통암</th>
|
||||
<ValueInput fieldKey="cut_rock" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label">절토면</th>
|
||||
<ValueInput
|
||||
fieldKey="cut_slope_protection"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
<th className="cad-qtable__label">보통암</th>
|
||||
<ValueInput fieldKey="ditch_rock" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
흙쌓기
|
||||
</th>
|
||||
<ValueInput fieldKey="embankment" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
제근
|
||||
</th>
|
||||
<ValueInput fieldKey="grubbing" colSpan={2} values={values} onEdit={handleEdit} />
|
||||
<th className="cad-qtable__label" colSpan={2}>
|
||||
노면고르기
|
||||
</th>
|
||||
<ValueInput
|
||||
fieldKey="surface_grading"
|
||||
colSpan={2}
|
||||
values={values}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -28,6 +28,7 @@ export enum StateVariable {
|
||||
activeLineWidth = 'activeLineWidth',
|
||||
activeLineDash = 'activeLineDash',
|
||||
activeTextStyle = 'activeTextStyle',
|
||||
designMeta = 'designMeta',
|
||||
layers = 'layers',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { JsonDrawingFileSerialized } from '../helpers/import-export-handlers/export-entities-to-json.ts';
|
||||
import { exportEntitiesAndLayersToJsonString } from '../helpers/import-export-handlers/export-entities-to-json.ts';
|
||||
import { getEntitiesAndLayersFromJsonObject } from '../helpers/import-export-handlers/import-entities-from-json.ts';
|
||||
import { HtmlEvent } from '../App.types.ts';
|
||||
import { type DesignMeta, HtmlEvent } from '../App.types.ts';
|
||||
import {
|
||||
getDesignMeta,
|
||||
getScreenCanvasDrawController,
|
||||
setActiveLayerId,
|
||||
setDesignMeta,
|
||||
setEntities,
|
||||
setLayers,
|
||||
} from '../state.ts';
|
||||
@@ -16,10 +18,12 @@ export const AISLO_DRAWING_ERROR_MESSAGE = 'aislo:b07:drawing-error';
|
||||
export const AISLO_DRAWING_CHANGED_MESSAGE = 'aislo:b07:drawing-changed';
|
||||
export const AISLO_DRAWING_SAVE_REQUEST_MESSAGE = 'aislo:b07:save-request';
|
||||
export const AISLO_DRAWING_SAVE_RESPONSE_MESSAGE = 'aislo:b07:save-response';
|
||||
export const AISLO_DRAWING_NAVIGATE_MESSAGE = 'aislo:b07:navigate';
|
||||
|
||||
interface DrawingLoadMessage {
|
||||
type: typeof AISLO_DRAWING_LOAD_MESSAGE;
|
||||
drawing: JsonDrawingFileSerialized;
|
||||
meta?: DesignMeta | null;
|
||||
}
|
||||
|
||||
interface DrawingSaveRequestMessage {
|
||||
@@ -37,6 +41,16 @@ function notifyParent(type: string, payload: Record<string, unknown> = {}) {
|
||||
window.parent.postMessage({ type, ...payload }, window.location.origin);
|
||||
}
|
||||
|
||||
/** 수량 패널에서 이전/다음 도면으로 이동 요청 (부모가 처리). */
|
||||
export function requestDrawingNavigation(direction: 'prev' | 'next') {
|
||||
notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction });
|
||||
}
|
||||
|
||||
/** 수량표 값 편집을 부모에 알린다 (확정 상태 롤백 연동). */
|
||||
export function notifyDrawingChangedByTable() {
|
||||
notifyParent(AISLO_DRAWING_CHANGED_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* B07 parent page와 CAD 앱 사이의 same-origin JSON 경계다.
|
||||
* DXF/DWG 파일이나 파서 객체는 이 경계를 통과하지 않는다.
|
||||
@@ -50,7 +64,9 @@ export function registerAisloDrawingBridge() {
|
||||
const drawing = JSON.parse(
|
||||
await exportEntitiesAndLayersToJsonString()
|
||||
) as JsonDrawingFileSerialized;
|
||||
notifyParent(AISLO_DRAWING_SAVE_RESPONSE_MESSAGE, { drawing });
|
||||
// 편집된 수량표 값을 도면과 함께 부모로 돌려준다.
|
||||
const quantityTable = getDesignMeta()?.quantityTable ?? null;
|
||||
notifyParent(AISLO_DRAWING_SAVE_RESPONSE_MESSAGE, { drawing, quantityTable });
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : 'Unable to serialize drawing';
|
||||
notifyParent(AISLO_DRAWING_ERROR_MESSAGE, { detail });
|
||||
@@ -64,6 +80,8 @@ export function registerAisloDrawingBridge() {
|
||||
setEntities(drawing.entities, false);
|
||||
setLayers(drawing.layers);
|
||||
setActiveLayerId(drawing.layers[0].id);
|
||||
// 설계 컨텍스트(제목·측점정보·확정상태·수량표)를 수량 패널에 반영
|
||||
setDesignMeta(event.data.meta ?? null);
|
||||
getScreenCanvasDrawController().zoomToFitScreen();
|
||||
notifyParent(AISLO_DRAWING_LOADED_MESSAGE);
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isEqual } from 'es-toolkit';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { Actor, MachineSnapshot } from 'xstate';
|
||||
import {
|
||||
type DesignMeta,
|
||||
type HoverPoint,
|
||||
HtmlEvent,
|
||||
type Layer,
|
||||
@@ -164,6 +165,12 @@ let activeLayerId: string = layers[0].id;
|
||||
let snapEnabled = true;
|
||||
let gridEnabled = false;
|
||||
|
||||
/**
|
||||
* 부모(B07 페이지)에서 넘어온 설계 컨텍스트. 수량 산출 패널이 이 값을 읽어
|
||||
* 제목·측점정보·확정상태·수량표를 렌더한다. null이면 패널을 숨긴다.
|
||||
*/
|
||||
let designMeta: DesignMeta | null = null;
|
||||
|
||||
// getters
|
||||
export const getCanvas = () => canvas;
|
||||
export const getActiveToolActor = () => activeToolActor;
|
||||
@@ -215,6 +222,7 @@ export const getActiveLayerId = (): string => {
|
||||
};
|
||||
export const getSnapEnabled = () => snapEnabled;
|
||||
export const getGridEnabled = () => gridEnabled;
|
||||
export const getDesignMeta = (): DesignMeta | null => designMeta;
|
||||
|
||||
// setters
|
||||
export const setCanvas = (newCanvas: HTMLCanvasElement) => {
|
||||
@@ -392,6 +400,16 @@ export const setGridEnabled = (enabled: boolean) => {
|
||||
gridEnabled = enabled;
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
};
|
||||
export const setDesignMeta = (newMeta: DesignMeta | null) => {
|
||||
designMeta = newMeta;
|
||||
triggerReactUpdate(StateVariable.designMeta);
|
||||
};
|
||||
/** 사용자가 편집한 수량표 전체를 반영하고 확정 상태를 롤백한다 (저장 대상). */
|
||||
export const setDesignQuantityTable = (table: Record<string, number | null>) => {
|
||||
if (!designMeta) return;
|
||||
designMeta = { ...designMeta, quantityTable: table, confirmed: false };
|
||||
triggerReactUpdate(StateVariable.designMeta);
|
||||
};
|
||||
|
||||
// Computed setters
|
||||
export const deleteEntities = (entitiesToDelete: Entity[], trackInUndoStack: boolean): Entity[] => {
|
||||
@@ -414,6 +432,7 @@ const reactStateVariables: StateVariable[] = [
|
||||
StateVariable.activeLineWidth,
|
||||
StateVariable.activeLineDash,
|
||||
StateVariable.activeTextStyle,
|
||||
StateVariable.designMeta,
|
||||
StateVariable.screenZoom,
|
||||
StateVariable.layers,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user