260719_11
This commit is contained in:
@@ -27,6 +27,9 @@ export interface DesignDrawingListResponse {
|
||||
drawings: DesignDrawingItem[];
|
||||
}
|
||||
|
||||
/** 수량 산출표 값 (미산정 항목은 null). 백엔드 `_quantity_table`의 키와 대응. */
|
||||
export type QuantityTable = Record<string, number | null>;
|
||||
|
||||
export interface DesignDrawingResponse {
|
||||
status: string;
|
||||
project_id: string;
|
||||
@@ -36,6 +39,7 @@ export interface DesignDrawingResponse {
|
||||
label: string;
|
||||
drawing: CadDrawing;
|
||||
confirmed: boolean;
|
||||
quantity_table?: QuantityTable | null;
|
||||
}
|
||||
|
||||
export interface DesignDrawingConfirmResponse {
|
||||
@@ -79,10 +83,11 @@ export function confirmDesignDrawing(
|
||||
projectId: string,
|
||||
drawingId: string,
|
||||
drawing: CadDrawing,
|
||||
quantityTable?: QuantityTable | null,
|
||||
): Promise<DesignDrawingConfirmResponse> {
|
||||
return requestJson(
|
||||
`/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}/confirm`,
|
||||
{ method: "PUT", body: JSON.stringify({ drawing }) },
|
||||
{ method: "PUT", body: JSON.stringify({ drawing, quantity_table: quantityTable ?? null }) },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,129 +160,49 @@ def _line_entity(
|
||||
}
|
||||
|
||||
|
||||
def _text_entity(
|
||||
drawing_id: str,
|
||||
index: int,
|
||||
label: str,
|
||||
point: tuple[float, float],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"id": str(
|
||||
uuid5(
|
||||
UUID("8ce96e1d-17e8-457b-b46e-329456701225"),
|
||||
f"{drawing_id}:{index}",
|
||||
)
|
||||
),
|
||||
"type": "Text",
|
||||
"lineColor": "#cbd5e1",
|
||||
"lineWidth": 1,
|
||||
"layerId": "b07-quantity-table",
|
||||
"shapeData": {
|
||||
"label": label,
|
||||
"basePoint": {"x": point[0], "y": point[1]},
|
||||
"options": {
|
||||
"textDirection": {"x": 1, "y": 0},
|
||||
"textAlign": "left",
|
||||
"textColor": "#cbd5e1",
|
||||
"fontSize": 0.32,
|
||||
"fontFamily": "Noto Sans KR",
|
||||
},
|
||||
},
|
||||
}
|
||||
# 수량 산출표 항목 키 (프론트 편집 테이블과 1:1 대응). center_z→지반고,
|
||||
# planned_elevation_m→계획고, cut/fill은 파생값, 나머지는 source["quantities"]에서 읽는다.
|
||||
_QUANTITY_ITEM_KEYS = (
|
||||
"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",
|
||||
)
|
||||
|
||||
|
||||
def _quantity_rows(source: dict[str, Any]) -> list[tuple[str, str, str]]:
|
||||
ground = source.get("center_z")
|
||||
planned = source.get("planned_elevation_m", source.get("design_elevation_m"))
|
||||
cut = (
|
||||
max(float(ground) - float(planned), 0.0)
|
||||
if isinstance(ground, (int, float)) and isinstance(planned, (int, float))
|
||||
else None
|
||||
)
|
||||
fill = (
|
||||
max(float(planned) - float(ground), 0.0)
|
||||
if isinstance(ground, (int, float)) and isinstance(planned, (int, float))
|
||||
else None
|
||||
)
|
||||
def _quantity_table(source: dict[str, Any]) -> dict[str, float | None]:
|
||||
"""횡단면 원본에서 편집 가능한 수량 산출표 값을 구조화한다.
|
||||
|
||||
아직 산정되지 않은 값은 None으로 두어 프론트 입력칸에서 사용자가 채운다.
|
||||
절토고/성토고(cut/fill)는 지반고·계획고에서 파생한 초기값이다.
|
||||
"""
|
||||
|
||||
def num(value: Any) -> float | None:
|
||||
return float(value) if isinstance(value, (int, float)) else None
|
||||
|
||||
ground = num(source.get("center_z"))
|
||||
planned = num(source.get("planned_elevation_m", source.get("design_elevation_m")))
|
||||
cut = max(ground - planned, 0.0) if ground is not None and planned is not None else None
|
||||
fill = max(planned - ground, 0.0) if ground is not None and planned is not None else None
|
||||
quantities = source.get("quantities") if isinstance(source.get("quantities"), dict) else {}
|
||||
|
||||
def value(number: Any) -> str:
|
||||
return f"{float(number):.3f}" if isinstance(number, (int, float)) else "-"
|
||||
|
||||
return [
|
||||
("기본", "측점", str(source.get("label", "-"))),
|
||||
("기본", "지반고", value(ground)),
|
||||
("기본", "계획고", value(planned)),
|
||||
("기본", "절토고", value(cut)),
|
||||
("기본", "성토고", value(fill)),
|
||||
("흙깎기", "토사", value(quantities.get("cut_soil"))),
|
||||
("흙깎기", "연암", value(quantities.get("cut_soft_rock"))),
|
||||
("흙깎기", "보통암", value(quantities.get("cut_rock"))),
|
||||
("옆도랑파기", "토사", value(quantities.get("ditch_soil"))),
|
||||
("옆도랑파기", "연암", value(quantities.get("ditch_soft_rock"))),
|
||||
("옆도랑파기", "보통암", value(quantities.get("ditch_rock"))),
|
||||
("비탈보호공", "성토면", value(quantities.get("fill_slope_protection"))),
|
||||
("비탈보호공", "절토면", value(quantities.get("cut_slope_protection"))),
|
||||
("기타", "지장목제거", value(quantities.get("tree_removal"))),
|
||||
("기타", "흙쌓기", value(quantities.get("embankment"))),
|
||||
("기타", "제근", value(quantities.get("grubbing"))),
|
||||
("기타", "노면고르기", value(quantities.get("surface_grading"))),
|
||||
]
|
||||
|
||||
|
||||
def _quantity_table_entities(
|
||||
source: dict[str, Any], drawing_id: str, points: list[tuple[float, float]]
|
||||
) -> list[dict[str, Any]]:
|
||||
if not points:
|
||||
return []
|
||||
rows = [("구분", "항목", "값"), *_quantity_rows(source)]
|
||||
row_height = 0.75
|
||||
column_widths = (3.2, 3.2, 3.0)
|
||||
left = max(point[0] for point in points) + 2.0
|
||||
top = max(point[1] for point in points)
|
||||
right = left + sum(column_widths)
|
||||
bottom = top - row_height * len(rows)
|
||||
entities: list[dict[str, Any]] = []
|
||||
for row_index in range(len(rows) + 1):
|
||||
y = top - row_index * row_height
|
||||
entities.append(
|
||||
_line_entity(
|
||||
f"{drawing_id}:table-h",
|
||||
row_index,
|
||||
(left, y),
|
||||
(right, y),
|
||||
"b07-quantity-table",
|
||||
"#64748b",
|
||||
)
|
||||
)
|
||||
x_positions = [left]
|
||||
for width in column_widths:
|
||||
x_positions.append(x_positions[-1] + width)
|
||||
for column_index, x in enumerate(x_positions):
|
||||
entities.append(
|
||||
_line_entity(
|
||||
f"{drawing_id}:table-v",
|
||||
column_index,
|
||||
(x, top),
|
||||
(x, bottom),
|
||||
"b07-quantity-table",
|
||||
"#64748b",
|
||||
)
|
||||
)
|
||||
text_index = 0
|
||||
for row_index, row in enumerate(rows):
|
||||
y = top - row_index * row_height - 0.5
|
||||
for column_index, label in enumerate(row):
|
||||
entities.append(
|
||||
_text_entity(
|
||||
drawing_id,
|
||||
text_index,
|
||||
label,
|
||||
(x_positions[column_index] + 0.12, y),
|
||||
)
|
||||
)
|
||||
text_index += 1
|
||||
return entities
|
||||
table: dict[str, float | None] = {
|
||||
"ground": ground,
|
||||
"planned": planned,
|
||||
"cut": cut,
|
||||
"fill": fill,
|
||||
}
|
||||
for key in _QUANTITY_ITEM_KEYS:
|
||||
table[key] = num(quantities.get(key))
|
||||
return table
|
||||
|
||||
|
||||
def _cad_drawing(source: dict[str, Any], drawing_id: str, kind: str) -> dict[str, Any]:
|
||||
@@ -318,8 +238,7 @@ def _cad_drawing(source: dict[str, Any], drawing_id: str, kind: str) -> dict[str
|
||||
"children": children,
|
||||
}
|
||||
)
|
||||
if kind == "cross":
|
||||
entities.extend(_quantity_table_entities(source, drawing_id, points))
|
||||
# 수량 산출표는 정적 도면 엔티티가 아니라 편집 가능한 HTML 테이블로 분리되었다.
|
||||
return {
|
||||
"entities": entities,
|
||||
"layers": [
|
||||
@@ -329,25 +248,26 @@ def _cad_drawing(source: dict[str, Any], drawing_id: str, kind: str) -> dict[str
|
||||
"isVisible": True,
|
||||
"isLocked": False,
|
||||
},
|
||||
{
|
||||
"id": "b07-quantity-table",
|
||||
"name": "Quantity Table",
|
||||
"isVisible": True,
|
||||
"isLocked": False,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _read_drawing(
|
||||
project_root: Path, longitudinal_path: Path, drawing_id: str
|
||||
) -> tuple[str, str, dict[str, Any], bool]:
|
||||
) -> tuple[str, str, dict[str, Any], bool, dict[str, float | None] | None]:
|
||||
"""(kind, label, drawing, confirmed, quantity_table)를 반환한다.
|
||||
|
||||
quantity_table은 횡단도에서만 채워지며, 확정본은 manifest에 저장된 사용자
|
||||
편집값을 우선하고 없으면 원본에서 파생한 초기값을 계산한다.
|
||||
"""
|
||||
manifest_entry = _read_manifest(project_root)["drawings"].get(drawing_id, {})
|
||||
saved_path = _design_root(project_root) / "drawings" / f"{drawing_id}.json"
|
||||
if manifest_entry.get("confirmed") and saved_path.is_file():
|
||||
kind = "longitudinal" if drawing_id == "longitudinal" else "cross"
|
||||
label = str(manifest_entry.get("label") or drawing_id)
|
||||
return kind, label, _read_json(saved_path), True
|
||||
stored_table = manifest_entry.get("quantity_table")
|
||||
table = stored_table if kind == "cross" and isinstance(stored_table, dict) else None
|
||||
return kind, label, _read_json(saved_path), True, table
|
||||
if drawing_id == "longitudinal":
|
||||
source = _read_json(longitudinal_path)
|
||||
return (
|
||||
@@ -355,6 +275,7 @@ def _read_drawing(
|
||||
"종단도 전체",
|
||||
_cad_drawing(source, drawing_id, "longitudinal"),
|
||||
False,
|
||||
None,
|
||||
)
|
||||
|
||||
if not _CROSS_ID.fullmatch(drawing_id):
|
||||
@@ -364,7 +285,7 @@ def _read_drawing(
|
||||
raise FileNotFoundError("요청한 횡단도를 찾을 수 없습니다.")
|
||||
source = _read_json(path)
|
||||
label = str(source.get("label") or drawing_id)
|
||||
return "cross", label, _cad_drawing(source, drawing_id, "cross"), False
|
||||
return "cross", label, _cad_drawing(source, drawing_id, "cross"), False, _quantity_table(source)
|
||||
|
||||
|
||||
def _store_confirmed_drawing(
|
||||
@@ -372,6 +293,7 @@ def _store_confirmed_drawing(
|
||||
item: DesignDrawingItem,
|
||||
drawing: dict[str, Any],
|
||||
expected_ids: set[str],
|
||||
quantity_table: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
if not isinstance(drawing.get("entities"), list) or not isinstance(drawing.get("layers"), list):
|
||||
raise ValueError("CAD 도면 스키마가 올바르지 않습니다.")
|
||||
@@ -383,12 +305,15 @@ def _store_confirmed_drawing(
|
||||
temporary.replace(path)
|
||||
|
||||
manifest = _read_manifest(project_root)
|
||||
manifest["drawings"][item.id] = {
|
||||
entry: dict[str, Any] = {
|
||||
"kind": item.kind,
|
||||
"label": item.label,
|
||||
"confirmed": True,
|
||||
"file": f"drawings/{item.id}.json",
|
||||
}
|
||||
if item.kind == "cross" and isinstance(quantity_table, dict):
|
||||
entry["quantity_table"] = quantity_table
|
||||
manifest["drawings"][item.id] = entry
|
||||
_write_manifest(project_root, manifest)
|
||||
confirmed_ids = {
|
||||
item_id for item_id, entry in manifest["drawings"].items() if entry.get("confirmed")
|
||||
@@ -434,7 +359,7 @@ async def get_design_drawing(
|
||||
"""선택한 도면 원본 한 건만 읽어 CAD 스키마로 변환한다."""
|
||||
try:
|
||||
route_id, project_root, longitudinal_path = await _confirmed_source(project_id)
|
||||
kind, label, drawing, confirmed = await asyncio.to_thread(
|
||||
kind, label, drawing, confirmed, quantity_table = await asyncio.to_thread(
|
||||
_read_drawing, project_root, longitudinal_path, drawing_id
|
||||
)
|
||||
return DesignDrawingResponse(
|
||||
@@ -445,6 +370,7 @@ async def get_design_drawing(
|
||||
label=label,
|
||||
drawing=drawing,
|
||||
confirmed=confirmed,
|
||||
quantity_table=quantity_table,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
@@ -482,6 +408,7 @@ async def confirm_design_drawing(
|
||||
item,
|
||||
request.drawing,
|
||||
{candidate.id for candidate in items},
|
||||
request.quantity_table,
|
||||
)
|
||||
|
||||
pool = get_db_pool()
|
||||
|
||||
@@ -35,12 +35,16 @@ class DesignDrawingResponse(BaseModel):
|
||||
label: str
|
||||
drawing: dict[str, Any]
|
||||
confirmed: bool = False
|
||||
# 횡단도 편집용 수량 산출표 값 (미산정 항목은 null). 종단도는 None.
|
||||
quantity_table: dict[str, float | None] | None = None
|
||||
|
||||
|
||||
class DesignDrawingConfirmRequest(BaseModel):
|
||||
"""CAD 앱에서 직렬화한 현재 편집 도면."""
|
||||
|
||||
drawing: dict[str, Any]
|
||||
# 사용자가 편집한 수량 산출표 값 (횡단도 확정 시 영구 저장).
|
||||
quantity_table: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class DesignDrawingConfirmResponse(BaseModel):
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type CadDrawing,
|
||||
type DesignDrawingItem,
|
||||
} from "./B07_wf4_DesignDetail_Api_Fetch";
|
||||
import { buildQuantityTable } from "./B07_wf4_DesignDetail_UI_QuantityTable";
|
||||
|
||||
/** B07 독립형 CAD 정적 경로 (main.py 마운트, dev는 vite proxy 위임) */
|
||||
const B07_CAD_APP_URL = "/b07-cad/index.html";
|
||||
@@ -181,6 +182,11 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
license.textContent = "Drawing engine based on OpenWebCAD · MIT License";
|
||||
cadHost.append(frame, license);
|
||||
|
||||
// CAD 영역 + 하단 수량 산출표를 세로로 묶는 메인 콘텐츠
|
||||
const mainContent = document.createElement("div");
|
||||
mainContent.className = "b07-main-stack";
|
||||
mainContent.append(cadHost);
|
||||
|
||||
let cadReady = false;
|
||||
let pendingDrawing: CadDrawing | undefined;
|
||||
let currentDrawing: DesignDrawingItem | undefined;
|
||||
@@ -218,6 +224,11 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
cadHost.dataset.loading = "false";
|
||||
cadHost.dataset.error =
|
||||
@@ -246,7 +257,12 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const drawing = await requestCadDrawing();
|
||||
const result = await confirmDesignDrawing(projectId, currentDrawing.id, drawing);
|
||||
const result = await confirmDesignDrawing(
|
||||
projectId,
|
||||
currentDrawing.id,
|
||||
drawing,
|
||||
currentDrawing.kind === "cross" ? quantityTable.getValues() : null,
|
||||
);
|
||||
currentConfirmed = true;
|
||||
currentDrawing.confirmed = true;
|
||||
confirmButton.disabled = true;
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/* =============================================================================
|
||||
* 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user