This commit is contained in:
2026-07-20 17:27:45 +09:00
parent 2649b7e325
commit ce82feda30
10 changed files with 572 additions and 349 deletions
@@ -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>
);
};