This commit is contained in:
2026-07-25 19:09:57 +09:00
parent 497a7026c2
commit f459e0cd6b
32 changed files with 2324 additions and 726 deletions
@@ -1,94 +1,15 @@
import { type FC, useCallback, useEffect, useRef, useState } from 'react';
import { type FC, useCallback, useEffect, 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>
);
import { requestDrawingNavigation } from '../integration/aislo-drawing-bridge';
import { getDesignMeta } from '../state';
/**
* 횡단도 수량 산출표 (CAD 화면 내부, 하단 중심 접이식 패널).
* 헤더(제목·측점정보·확정상태·이전/다음)는 접어도 항상 보인다.
* 도면 내비게이션 바 (CAD 화면 하단 중심).
* 수량 산출표는 HTML 테이블 대신 도면 자체의 CAD 테이블 레이어(b07-cross-table)로
* 렌더되므로, 여기서는 제목·측점정보·확정상태·이전/다음 이동만 담당한다.
*/
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(() => {
@@ -96,35 +17,10 @@ export const QuantityPanel: FC = () => {
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={meta.kind !== 'cross' || collapsed}>
<section className="cad-qtable controls" data-collapsed="true">
<header className="cad-qtable__header">
<button
type="button"
@@ -155,117 +51,7 @@ export const QuantityPanel: FC = () => {
>
›
</button>
{meta.kind === 'cross' && (
<button
type="button"
className="cad-qtable__collapse"
title={collapsed ? '표 펼치기' : '표 접기'}
onClick={() => setCollapsed((value) => !value)}
>
{collapsed ? '▲' : '▼'}
</button>
)}
</header>
{/* 표는 종단도에서도 DOM에 남겨 패널 폭을 통일한다 (CSS로만 숨김) */}
<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>
);
};
@@ -104,7 +104,7 @@ export const Toolbar: FC = () => {
const [selectedCount, setSelectedCount] = useState(0);
const [selectedType, setSelectedType] = useState('선택 없음');
const [instruction, setInstruction] = useState('명령을 입력하거나 도구를 선택하십시오.');
const [panelTab, setPanelTab] = useState<'properties' | 'layers'>('properties');
const [panelTab, setPanelTab] = useState<'properties' | 'layers'>('layers');
const [panelCollapsed, setPanelCollapsed] = useState(false);
const [snap, setSnap] = useState(getSnapEnabled());
const [grid, setGrid] = useState(getGridEnabled());