feat(B08): 2탭 구조물도 — 집계표 조합마다 보조 탭 · M02 도면(도각) + 면적산출 카드 보기 전용

- 새 B08_Quantity_UI_Drawing.ts · .css — 조합 목록 · 차례 = 집계표와 같음(B08_Quantity_Summary_Data.summaryColumns)
- 한 장 = M02 항목 카드(createItemCards) 그대로 · 조합 원본 층 readTemplate 읽기만 · editable false · [저장] · [수정] 없음
- 안내 글 — 조합 없음 · M02 에 없는 조합 · 도번 없는 조합
- B08_Quantity_UI_Page.ts 2탭에 붙이는 두 줄 · locale_b4 글 넷
- 시험 test_61_2_b08_structure_sheets(탭 붙음 · 읽기만 · 같은 열 · 글 키)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
2026-10-06 18:17:41 +09:00
co-authored by Claude Opus 5.5
parent 2a3ef43710
commit 0cc7ff67eb
5 changed files with 188 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
/* B08 2탭 구조물도(PLAN 61-2) — 보조 탭 · 카드 둘(도면 · 면적산출). 색은 테마 변수만. */
.b08-drawing__tabs .is-missing {
color: var(--color-danger);
}
/* M02 카드 칸은 셋(도면 · 일위대가 · 면적산출) — 여기는 둘 · 도면을 넓게 */
.b08-drawing__cards .m02-struct {
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
}
@media (max-width: 960px) {
.b08-drawing__cards .m02-struct {
grid-template-columns: minmax(0, 1fr);
}
}
+129
View File
@@ -0,0 +1,129 @@
/* =============================================================================
* B08_Quantity_UI_Drawing.ts
* 2탭 구조물도(PLAN 61-2) — 집계표 열(저장된 구조물 조합)마다 보조 탭 · 한 장 = 그 조합의
* M02 도면(도각 포함 · 보기 전용 CAD) + 면적산출 — M02 항목 카드(`M02_MasterTemplete_ItemCards`) 그대로.
*
* 조합 목록 · 차례 = 집계표와 같음(`B08_Quantity_Summary_Data`)
* 읽기만 — 조합 원본 층(`readTemplate`)을 읽고 카드는 [저장] · [수정] 없음 · B05/B06 자료도 읽기만
* 카드 · CAD · 스프레드시트는 이 탭을 열 때만 `import()`
* ========================================================================== */
import "./B08_Quantity_UI_Drawing.css";
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton } from "@ui/ui_template_elements";
import { loadRows as loadPickerRows } from "@ui/ui_template_item_picker";
import { layerOf, pathOf, rowId, type PickerRow } from "@ui/ui_template_item_picker_data";
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
import { fetchStructures } from "../B05_Profile/B05_Profile_Api_Structures";
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import { summaryTooltip } from "../common_util/common_util_summary_item";
import {
comboId,
summaryColumns,
type ComboColumn,
type ComboLabel,
} from "./B08_Quantity_Summary_Data";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
function note(text: string): HTMLElement {
const node = document.createElement("p");
node.className = "ui-general-block__empty";
node.textContent = text;
return node;
}
/** 집계표와 같은 열 + 열마다 M02 줄(없으면 M02 에 없는 조합). */
async function loadColumns(
projectId: string,
): Promise<{ columns: ComboColumn[]; rows: Map<string, PickerRow> }> {
const [structures, pickerRows, pipes] = await Promise.all([
fetchStructures(projectId),
loadPickerRows(),
// 배수 분석 전이면 관이 없음 — 구조물만으로
fetchDetailPipePoints(projectId).then(
(response) => response.pipe_points,
() => [],
),
]);
const rows = new Map(pickerRows.map((row) => [rowId(row), row]));
const describe = (item: Parameters<typeof comboId>[0]): ComboLabel | null => {
const row = rows.get(comboId(item));
if (!row) return null;
const [group = "", ...rest] = pathOf(row);
return { group, name: rest.join(" ") || group || row.key };
};
return { columns: summaryColumns([...structures.structures, ...pipes], describe), rows };
}
/** 탭 몸통을 그림 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. */
export async function renderStructureSheets(host: HTMLElement, projectId: string): Promise<void> {
host.replaceChildren(note(L("B08_Quantity_Drawing_Loading")));
try {
const [{ columns, rows }, { createItemCards }] = await Promise.all([
loadColumns(projectId),
import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards"),
]);
if (!host.isConnected) return; // 그새 다른 탭으로 옮김
if (!columns.length) return void host.replaceChildren(note(L("B08_Quantity_Summary_NoCombo")));
let row: PickerRow | null = null;
const source = (): ItemSource => {
const layer = row ? layerOf(row) : "system";
return {
layer,
projectId: null,
read: (part, column) => readTemplate(layer, part, column, null),
};
};
const cards = createItemCards({
source,
editable: () => false,
onEdit: () => undefined,
warnOnFail: () => false, // 못 읽으면 카드에 「없음」
});
const tabs = document.createElement("div");
tabs.className = "b08-quantity__tabs b08-drawing__tabs";
tabs.setAttribute("role", "tablist");
const notice = document.createElement("div");
const sheet = document.createElement("div");
sheet.className = "b08-drawing__cards";
sheet.append(cards.root);
const select = (picked: ComboColumn): void => {
tabs.replaceChildren(
...columns.map((column) => {
const button = createButton({
label: column.group ? `${column.group} ${column.name}` : column.name,
variant: column === picked ? "filled" : "ghost",
size: "sm",
onClick: () => column !== picked && select(column),
});
button.setAttribute("role", "tab");
button.setAttribute("aria-selected", String(column === picked));
button.dataset.b08Combo = column.id;
button.title = summaryTooltip(column.item);
if (column.missing) button.classList.add("is-missing");
return button;
}),
);
row = rows.get(picked.id) ?? null;
if (!row?.열) {
notice.replaceChildren(note(L("B08_Quantity_Drawing_Missing")));
return void cards.load(null);
}
notice.replaceChildren(...(row.도번 ? [] : [note(L("B08_Quantity_Drawing_NoDrawing"))]));
void cards.load({ id: row.열, label: picked.name });
};
host.replaceChildren(tabs, notice, sheet);
select(columns[0]);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
host.replaceChildren(note(`${L("B08_Quantity_Drawing_Failed")} — ${reason}`));
}
}
+2
View File
@@ -16,6 +16,7 @@ import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
import { workflowSteps } from "../A00_Common/b_page_scaffold";
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
import { renderStructureSummary } from "./B08_Quantity_UI_Summary";
import { renderStructureSheets } from "./B08_Quantity_UI_Drawing";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
@@ -38,6 +39,7 @@ function renderTab(host: HTMLElement, tab: TabId, projectId: string | null): voi
void renderStructureSummary(view, projectId);
return;
}
if (tab === "sheet" && projectId) return void renderStructureSheets(view, projectId);
const pending = document.createElement("p");
pending.className = "ui-general-block__empty";
pending.textContent = L("B08_Quantity_Tab_Pending");
@@ -0,0 +1,28 @@
"""61-2 B08 2탭 구조물도 — 탭에 붙음 · 읽기만 · 집계표와 같은 조합 목록 · 안내 글 키."""
import re
from pathlib import Path
DRAWING = Path("B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8")
PAGE = Path("B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8")
LOCALE = Path("ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8")
def test_sheet_tab_wired():
assert 'tab === "sheet"' in PAGE and "renderStructureSheets(view, projectId)" in PAGE
def test_read_only():
# 쓰는 길이 없음 — 저장 · 고침 · PUT/POST 없음 · 카드는 고칠 수 없음
for word in ("saveTemplate", "write:", "method:", "sheetEdit", "onImport", "onClear"):
assert word not in DRAWING, word
assert "editable: () => false" in DRAWING
def test_same_columns_as_summary():
assert "summaryColumns(" in DRAWING and "./B08_Quantity_Summary_Data" in DRAWING
def test_locale_keys_exist():
for key in set(re.findall(r'L\("([A-Za-z0-9_]+)"\)', DRAWING)):
assert f"{key}:" in LOCALE or key.startswith("B08_Quantity_Summary_"), key
+13
View File
@@ -19,6 +19,19 @@ export const ui_locales_b4 = {
],
B08_Quantity_Tab_Hydrology: ["수리집수면적표", "Hydraulic Catchment Table"],
B08_Quantity_Tab_Pending: ["차례대로 만들 탭입니다.", "This tab comes later, in order."],
B08_Quantity_Drawing_Loading: ["구조물도를 불러오는 중…", "Loading structure sheets…"],
B08_Quantity_Drawing_Failed: [
"구조물도를 불러오지 못했습니다",
"Failed to load structure sheets",
],
B08_Quantity_Drawing_Missing: [
"M02 에 없는 조합입니다 — 도면 · 면적산출을 보일 수 없습니다.",
"This combination is not in M02 — no drawing or area sheet to show.",
],
B08_Quantity_Drawing_NoDrawing: [
"이 조합은 도면이 없습니다 — M02 구조물 조합에서 그립니다.",
"This combination has no drawing — draw it in M02 structure combinations.",
],
B08_Quantity_Summary_Station: ["측점", "Station"],
B08_Quantity_Summary_MissingGroup: ["M02 에 없음", "Not in M02"],
B08_Quantity_Summary_Loading: ["구조물 집계표를 불러오는 중…", "Loading the structure summary…"],