feat(B08): 자재 셋 값 없는 줄 숨김 · 구조물 탭 표 넷 공용 카드 안(143-3)

- 총괄 · 콘크리트 · 자재수량 — 수량 · 금액 · 손 단위수량 없는 줄 숨김(화면만 · 고친 칸은 원래 줄 자리로 저장)
- 구조물 집계표 · 자재 셋 — 공용 카드 안(토공 표와 같은 꼴)
- 시험 — 새 test_143_3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
2026-10-09 14:59:58 +09:00
co-authored by Claude Opus 5.5
parent 14c3f01f11
commit 22a54abe99
3 changed files with 66 additions and 20 deletions
+34 -18
View File
@@ -7,11 +7,13 @@
* 고칠 칸 = 단위수량 · ㎥당 배합 · 할증 % — 조작 = 캐시(`b08-materials`) + 짝 계산(`Material_Calc.ts`)으로 바로
* 총괄 모래 · 자갈 · 시멘트 할증 = 좌측 패널 값(PLAN 124 · 표에서 안 고침 · 패널 조작에 바로 다시 셈)
* [저장] = PUT …/quantity/materials → 서버가 정본으로 다시 셈 · 캐시 비움 · 자동 저장 없음
* 값 없는 줄은 숨김(화면만 · 셈 · 열 · 단위수량은 그대로) · 표는 공용 카드 안(PLAN 143-3)
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import {
createButton,
createCard,
hideLoadingOverlay,
showLoadingOverlay,
showToast,
@@ -49,6 +51,12 @@ const HINTS: Record<MaterialSheetId, string> = {
concrete: "㎥당 = 산림품셈 12-1-3 (B) 배합 · 건축 9-1-1 모르터 기본(무근은 빈칸) — 고칠 수 있음",
quantity: "수량 = 구조물 수량 × 단위수량 · 단위수량은 빈칸(사용자 입력) · 관매설은 1",
};
const HIDDEN = "값 없는 줄은 숨김";
/** 보일 줄 차례(원래 줄 번호) — 값 없는 줄은 숨김(143-3) */
const shown = <T>(rows: readonly T[], has: (row: T) => boolean): number[] =>
rows.flatMap((row, i) => (has(row) ? [i] : []));
const anyValue = (cells: Cells): boolean => Object.values(cells).some((v) => v != null);
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
@@ -99,7 +107,9 @@ interface Table {
function quantityTable(s: MaterialSheets, o: MaterialOverrides): Table {
const { materials, rows, total } = s.quantity;
const rowsOut: DataRow[] = rows.flatMap((row) => {
const keep = shown(rows, (row) => row.qty != null || anyValue(row.units));
const rowsOut: DataRow[] = keep.flatMap((at) => {
const row = rows[at];
const unit: DataRow = {
name: row.name,
spec: row.spec,
@@ -137,7 +147,7 @@ function quantityTable(s: MaterialSheets, o: MaterialOverrides): Table {
total: totalRow,
frozen: 5,
editable: (i, key) => i % 2 === 0 && key.startsWith("m:"),
target: (i, key) => [o.units, rows[i / 2].id, key.slice(2)],
target: (i, key) => [o.units, rows[keep[i / 2]].id, key.slice(2)],
};
}
@@ -149,9 +159,11 @@ const MIX_LABEL: Record<(typeof MIX_FIELDS)[number], [string, string, string]> =
function concreteTable(s: MaterialSheets, o: MaterialOverrides): Table {
const { rows, total } = s.concrete;
const rowsOut: DataRow[] = rows.map((row, i) => {
const keep = shown(rows, (row) => row.qty != null || anyValue(row.amounts));
const rowsOut: DataRow[] = keep.map((at, i) => {
const row = rows[at];
const out: DataRow = {
name: i > 0 && rows[i - 1].name === row.name ? "" : row.name,
name: i > 0 && rows[keep[i - 1]].name === row.name ? "" : row.name,
spec: row.spec,
qty: row.qty,
};
@@ -187,12 +199,13 @@ function concreteTable(s: MaterialSheets, o: MaterialOverrides): Table {
total: totalRow,
frozen: 2,
editable: (_i, key) => key.startsWith("per:"),
target: (i, key) => [o.mix, rows[i].key, key.slice(4)],
target: (i, key) => [o.mix, rows[keep[i]].key, key.slice(4)],
};
}
function totalTable(s: MaterialSheets, o: MaterialOverrides): Table {
const { rows } = s.material;
const keep = shown(rows, (row) => row.qty != null || row.amount != null);
return {
columns: [
{ key: "group", label: "구분", width: 96 },
@@ -203,19 +216,22 @@ function totalTable(s: MaterialSheets, o: MaterialOverrides): Table {
numberColumn({ key: "amount", label: "합계", width: 96 }),
{ key: "note", label: "비고", width: 96 },
],
rows: rows.map((row, i) => ({
group: i > 0 && rows[i - 1].group === row.group ? "" : row.group,
spec: row.spec,
unit: row.unit,
qty: row.qty,
surcharge: row.surcharge,
amount: row.amount,
note: row.panel ? "할증 = 좌측 패널" : "",
})),
rows: keep.map((at, i) => {
const row = rows[at];
return {
group: i > 0 && rows[keep[i - 1]].group === row.group ? "" : row.group,
spec: row.spec,
unit: row.unit,
qty: row.qty,
surcharge: row.surcharge,
amount: row.amount,
note: row.panel ? "할증 = 좌측 패널" : "",
};
}),
total: null,
frozen: 2,
editable: (i, key) => key === "surcharge" && !rows[i].panel,
target: (i) => [o.surcharge, rows[i].key, null],
editable: (i, key) => key === "surcharge" && !rows[keep[i]].panel,
target: (i) => [o.surcharge, rows[keep[i]].key, null],
};
}
@@ -254,7 +270,7 @@ export async function renderMaterialSheet(
const toolbar = document.createElement("div");
toolbar.className = "b08-material__toolbar";
const notes = document.createElement("div");
notes.append(note(BASIS), note(HINTS[sheet]));
notes.append(note(BASIS), note(`${HINTS[sheet]} · ${HIDDEN}`));
if (sheet !== "quantity") {
// 빈 까닭 한 줄(131) — 단위수량 빈 줄 · 서버 알림 · 빈 칸은 빈칸 그대로
const blank = blankUnitRows(sheets);
@@ -300,7 +316,7 @@ export async function renderMaterialSheet(
built = BUILD[sheet](sheets, overrides);
table.setRows(built.rows, built.total);
};
host.replaceChildren(toolbar, table.root);
host.replaceChildren(createCard({ body: [toolbar, table.root] }));
onOptionsChange(host, () => {
sheets = calc();
redraw();
+2 -2
View File
@@ -16,7 +16,7 @@
import { API_BASE_URL } from "@config/config_frontend";
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { showToast } from "@ui/ui_template_elements";
import { createCard, showToast } from "@ui/ui_template_elements";
import { fetchStructures } from "../B05_Profile/B05_Profile_Api_Structures";
import { fetchLatestRoute, readRouteLatestCache } from "../B05_Profile/B05_Profile_Api_Fetch";
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
@@ -238,7 +238,7 @@ export async function renderStructureSummary(host: HTMLElement, projectId: strin
: note(L("B08_Quantity_Summary_NoStation"));
let table = draw();
parts.push(table);
host.replaceChildren(...parts);
host.replaceChildren(createCard({ body: parts })); // 공용 카드 안(토공 표와 같은 꼴 · 143-3)
onOptionsChange(host, () => {
const next = draw();
table.replaceWith(next);
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""B08 구조물 탭 — 자재 셋 값 없는 줄 숨김 · 표 넷 공용 카드 안(PLAN 143-3).
숨김은 화면만 — 셈 · 열 · 단위수량은 그대로 · 고친 칸은 원래 줄 번호(`keep`)로 저장 자리를 찾음.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
MATERIAL = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8")
SUMMARY = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
def test_자재_셋_값_없는_줄_숨김():
assert MATERIAL.count("const keep = shown(rows, ") == 3 # 총괄 · 콘크리트 · 자재수량
assert "row.qty != null || anyValue(row.units)" in MATERIAL # 손 단위수량 있으면 보임
assert "row.qty != null || anyValue(row.amounts)" in MATERIAL
assert "row.qty != null || row.amount != null" in MATERIAL
# 고친 칸 → 원래 줄(숨긴 줄 건너뜀)
for target in (
"rows[keep[i / 2]].id",
"rows[keep[i]].key, key.slice(4)",
"rows[keep[i]].panel",
):
assert target in MATERIAL, target
def test_표_넷_공용_카드_안():
assert "createCard({ body: [toolbar, table.root] })" in MATERIAL
assert "createCard({ body: parts })" in SUMMARY