diff --git a/B08_Quantity/B08_Quantity_UI_Drawing.css b/B08_Quantity/B08_Quantity_UI_Drawing.css new file mode 100644 index 000000000..f72ba222e --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_Drawing.css @@ -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); + } +} diff --git a/B08_Quantity/B08_Quantity_UI_Drawing.ts b/B08_Quantity/B08_Quantity_UI_Drawing.ts new file mode 100644 index 000000000..d4c70a2e4 --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_Drawing.ts @@ -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 }> { + 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[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 { + 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}`)); + } +} diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index 51b04b5d4..67b00b235 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -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"); diff --git a/resources/tester/test_61_2_b08_structure_sheets.py b/resources/tester/test_61_2_b08_structure_sheets.py new file mode 100644 index 000000000..aa077e161 --- /dev/null +++ b/resources/tester/test_61_2_b08_structure_sheets.py @@ -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 diff --git a/ui_template/ui_template_locale_b4.ts b/ui_template/ui_template_locale_b4.ts index 9f0145a56..1a0698a4b 100644 --- a/ui_template/ui_template_locale_b4.ts +++ b/ui_template/ui_template_locale_b4.ts @@ -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…"],