diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index e59d594fb..7c6f6e109 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -243,6 +243,13 @@ export interface StructureNumbers { export const fetchStructureNumbers = (): Promise> => scoped("/structures/numbers"); +/** 같은 번호 목록을 **고른 층**(+ 주인)에서 — 지금 층 길을 안 바꾸고 읽음(고르기 모달 상단 표 · 224-1) */ +export const fetchStructureNumbersAt = ( + layer: Layer, + owner: string | null, +): Promise> => + call(owned(layer, `/layers/${layer}/structures/numbers`, owner)); + /** 구조물집계표조합 한 줄 — 구조물 열 하나(키 `SS` + 6자리 · 한 번 준 키는 안 바뀜) */ export interface StructureCombo extends StructureNumbers { 키: string; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 881a4607b..f4b98ca00 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -86,6 +86,17 @@ export function laborSumState( return numbers?.합계상태 ? { state: numbers.합계상태, why: numbers.합계까닭 ?? "" } : undefined; } +/** 구조물 조합 표 번호 · 합계 줄 글(86-3 · 101-1) — 관리 화면과 고르기 모달 상단 표(224-1)가 같은 한 벌 */ +export function numberLabels(numbers: () => ColumnNumbers) { + return { + laborLabel: (colId: string) => numbers()[colId]?.일위대가번 ?? "", + drawingLabel: (colId: string) => numbers()[colId]?.도번 ?? "", + areaLabel: (colId: string) => numbers()[colId]?.면적번 ?? "", + laborSumLabel: (colId: string) => laborSumText(numbers()[colId]), + laborSumState: (colId: string) => laborSumState(numbers()[colId]), + }; +} + /** 집계표 열 — 머리 글(제목행 종류 · 분류1 · 분류2 · 분류3) · 꼴만 씀 */ export interface StructureColumn { id: string; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index c27d84018..b4c77c65f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -22,8 +22,7 @@ import { comboMerge, comboView, createStructurePanel, - laborSumState, - laborSumText, + numberLabels, pillsFor, STRUCTURE_TABLE, type StructureColumn, @@ -381,11 +380,7 @@ export function createMain( onChange: () => (dirty = !readOnly), ...(struct ? { - laborLabel: (colId: string) => struct.numbers()[colId]?.일위대가번 ?? "", - drawingLabel: (colId: string) => struct.numbers()[colId]?.도번 ?? "", - areaLabel: (colId: string) => struct.numbers()[colId]?.면적번 ?? "", - laborSumLabel: (colId: string) => laborSumText(struct.numbers()[colId]), // 86-3 - laborSumState: (colId: string) => laborSumState(struct.numbers()[colId]), // 101-1 + ...numberLabels(struct.numbers), // 번호 · 일위대가 합계 줄(86-3 · 101-1) — 고르기 모달 상단 표와 한 벌 combo: { ...(readOnly || !extras.onHeadRename ? {} diff --git a/resources/tester/helper_224_1_picker_top_table.cjs b/resources/tester/helper_224_1_picker_top_table.cjs new file mode 100644 index 000000000..a7b1e700a --- /dev/null +++ b/resources/tester/helper_224_1_picker_top_table.cjs @@ -0,0 +1,167 @@ +/* 고르기 모달 상단 표(224-1) 시험 헬퍼 — `ui_template_item_picker_table.ts` 를 트랜스파일해 + * 표 부품 · 집계표 모듈 · 서버 읽기를 대역으로 갈아 끼우고 돌려 부름 기록을 JSON 한 벌로 냄. + * 동적 `import()` 도 CommonJS 변환에서 같은 `require` 길을 타 대역이 먹음. + * ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const UI = path.join(ROOT, "ui_template"); +const log = []; +const state = { numbersFail: false, tableFail: null }; + +const node = (props = {}) => ({ + className: props.className ?? "", + hidden: "attrs" in props && props.attrs && "hidden" in props.attrs, + text: props.text ?? "", + children: props.children ?? [], + replaceChildren(...kids) { + this.children = kids; + }, +}); + +const STUBS = { + "./ui_template_elements": { el: (_tag, props) => node(props) }, + "./ui_template_locale": { t: (key) => (key === "M02_PickerTableFailed" ? "표 실패 {value}" : "읽는 중") }, + "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch": { + readTemplate: async (...args) => { + log.push(["readTemplate", ...args]); + if (state.tableFail) throw new Error(state.tableFail); + return { 문서: { 열: [], tag: `${args[0]}|${args[4]}` } }; + }, + fetchStructureNumbersAt: async (...args) => { + log.push(["numbers", ...args]); + if (state.numbersFail) throw new Error("numbers down"); + return { rv1: { 도번: "SD1" } }; + }, + }, + "../M02_MasterTemplete/M02_MasterTemplete_Structure": { + STRUCTURE_TABLE: "구조물집계표", + comboView: (doc) => ({ ...doc, comboView: true }), + numberLabels: (numbers) => ({ laborLabel: () => "", seenNumbers: numbers() }), + }, + "./sheet/ui_template_sheet": { + createSheet: (host, doc, opts) => { + const entry = { doc, opts, selected: [], destroyed: false, host }; + log.push(["createSheet", entry]); + host.replaceChildren({ sheet: true }); + return { + selectColumn: (id) => entry.selected.push(id), + destroy: () => (entry.destroyed = true), + }; + }, + }, +}; + +const cache = new Map(); +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + if (request.startsWith(".")) return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); + return require(request); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + cache.set(full, box.exports); + return box.exports; +} + +const flush = () => new Promise((resolve) => setTimeout(resolve, 20)); +const row = (key, 열, layer, owner) => ({ key, 열, layer, owner }); +const names = () => log.map((entry) => entry[0]); +const creates = () => log.filter((entry) => entry[0] === "createSheet").map((entry) => entry[1]); + +(async () => { + const { createComboTable } = loadTs(path.join(UI, "ui_template_item_picker_table.ts")); + const data = loadTs(path.join(UI, "ui_template_item_picker_data.ts")); + const out = {}; + const mine = row("SS1", "rv1", "personal", "1/3"); + const sameLayer = row("SS2", "rv2", "personal", "1/3"); + const master = row("SS9", "rv9", "system", null); + + const table = createComboTable(); + out.startHidden = table.root.hidden; + table.show(null); + out.nullHidden = table.root.hidden; + out.nullCalls = names(); + + table.show(mine); + out.shownHidden = table.root.hidden; + await flush(); + out.firstReads = log.filter((e) => e[0] !== "createSheet"); + const first = creates()[0]; + out.first = { + count: creates().length, + readOnly: first.opts.readOnly, + lockHead: first.opts.lockHead, + mode: first.opts.mode, + combo: !!first.opts.combo, + comboDoc: first.doc.comboView, + docTag: first.doc.tag, + numbersSeen: first.opts.seenNumbers, + selected: first.selected, + }; + + log.length = 0; + table.show(sameLayer); // 같은 층 다른 조합 — 표 그대로 · 열만 + await flush(); + out.sameLayer = { calls: names(), selected: first.selected, destroyed: first.destroyed }; + + log.length = 0; + table.show(master); // 다른 층 — 그 층 표로 바뀜 + await flush(); + out.otherLayer = { + reads: log.filter((e) => e[0] === "readTemplate"), + creates: creates().length, + oldDestroyed: first.destroyed, + selected: creates()[0].selected, + docTag: creates()[0].doc.tag, + }; + + log.length = 0; + table.show(null); + out.hideAgain = table.root.hidden; + + // 읽는 사이에 다른 조합을 고름 — 마지막 것만 그려짐 + log.length = 0; + table.show(mine); + table.show(master); + await flush(); + out.race = { creates: creates().length, docTag: creates()[0]?.doc.tag }; + + // 번호 읽기 실패 — 표는 그려짐 + log.length = 0; + state.numbersFail = true; + const lone = createComboTable(); + lone.show(mine); + await flush(); + out.numbersFail = { creates: creates().length, seen: creates()[0]?.opts.seenNumbers }; + state.numbersFail = false; + + // 표 읽기 실패 — 까닭 글 · 표 없음 + log.length = 0; + state.tableFail = "그 층은 아직 만든 것이 없음"; + const broken = createComboTable(); + broken.show(mine); + await flush(); + out.tableFail = { creates: creates().length, text: broken.root.children[0].text }; + + out.keys = { + mineSame: data.tableKey(mine) === data.tableKey(sameLayer), + layerDiffers: data.tableKey(mine) !== data.tableKey(master), + ownerDiffers: data.tableKey(mine) !== data.tableKey(row("SS1", "rv1", "personal", "1/4")), + mine: data.tableKey(mine), + }; + console.log(JSON.stringify(out)); +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/resources/tester/test_224_1_picker_top_table.py b/resources/tester/test_224_1_picker_top_table.py new file mode 100644 index 000000000..e01ed490e --- /dev/null +++ b/resources/tester/test_224_1_picker_top_table.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +"""PLAN 224-1 — B05/B06 구조물 조합 고르기 모달 맨 위 = M02 구조물조합 상단 표(구조물집계표). + +① 행동(Node · 대역): 모달 상단 표 부품(`ui_template_item_picker_table.ts`)을 표 부품 · 집계표 모듈 · + 서버 읽기 대역 위에서 돌려 — 고른 항목의 층 표를 보기 전용으로 그리고, 같은 층 다른 조합 = 열만 옮김 · + 다른 층 = 표가 바뀜 · 읽는 사이 바뀌면 마지막 것만 · 번호 읽기 실패해도 표 · 표 읽기 실패 = 까닭 글. +② 같은 부품(소스): M02 관리 화면 · 모달이 같은 `createSheet` · 같은 줄 글 도우미(`numberLabels`) · 같은 + 숨김 열 규칙(`comboView`)을 씀 · 모달이 상단 표를 배선(고르면 보임 · 편집 화면에선 숨김). +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +UI = ROOT / "ui_template" +M02 = ROOT / "M02_MasterTemplete" + + +def _text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +@pytest.fixture(scope="module") +def out(): + proc = subprocess.run( + ["node", str(HERE / "helper_224_1_picker_top_table.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_처음엔_숨김_고르면_보임_비우면_다시_숨김(out): + assert out["startHidden"] is True + assert out["nullHidden"] is True + assert out["nullCalls"] == [] # 고른 것 없으면 서버도 안 부름 + assert out["shownHidden"] is False + assert out["hideAgain"] is True + + +def test_고른_항목_층의_집계표를_보기_전용으로(out): + # 그 층 · 그 주인의 구조물집계표 + 번호 목록 + assert out["firstReads"] == [ + ["readTemplate", "personal", "table", "구조물집계표", None, "1/3"], + ["numbers", "personal", "1/3"], + ] + first = out["first"] + assert first["count"] == 1 + assert first["readOnly"] is True # 보기 전용 — 고침은 M02 에서 + assert first["lockHead"] is True + assert first["combo"] is True # M02 「구조물 조합」 상단 표 꼴 + assert first["comboDoc"] is True # 숨김 열 규칙 = M02 와 같은 `comboView` + assert first["docTag"] == "personal|1/3" + assert first["numbersSeen"] == {"rv1": {"도번": "SD1"}} # 줄 글 도우미가 같은 번호 목록을 받음 + + +def test_같은_층_다른_조합은_열만_옮김_다른_층은_표가_바뀜(out): + same = out["sameLayer"] + assert same["calls"] == [] # 서버 · 표 다시 안 그림 + assert same["selected"] == ["rv1", "rv2"] + assert same["destroyed"] is False + other = out["otherLayer"] + assert other["reads"] == [["readTemplate", "system", "table", "구조물집계표", None, None]] + assert other["creates"] == 1 + assert other["oldDestroyed"] is True # 옛 표를 거둠 + assert other["selected"] == ["rv9"] + assert other["docTag"] == "system|null" + + +def test_읽는_사이_바뀌면_마지막_조합만(out): + assert out["race"] == {"creates": 1, "docTag": "system|null"} + + +def test_번호_읽기_실패해도_표는_그려지고_표_읽기_실패는_까닭_글(out): + assert out["numbersFail"] == {"creates": 1, "seen": {}} + assert out["tableFail"] == {"creates": 0, "text": "표 실패 그 층은 아직 만든 것이 없음"} + + +def test_표_키_같은_층_주인이면_같고_층_주인이_다르면_다름(out): + assert out["keys"] == { + "mineSame": True, + "layerDiffers": True, + "ownerDiffers": True, + "mine": "personal|1/3", + } + + +def test_모달과_M02_가_같은_표_부품_줄_글_도우미_숨김_열_규칙(): + table = _text(UI / "ui_template_item_picker_table.ts") + main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") + struct = _text(M02 / "M02_MasterTemplete_Structure.ts") + # 같은 표 부품(관리 화면은 이 파일을 glob 으로 받음) + assert 'import("./sheet/ui_template_sheet")' in table + assert '"../ui_template/sheet/ui_template_sheet.ts"' in main + # 보기 전용 · 상단 표 꼴 + assert "readOnly: true," in table and "combo: {}," in table + # 줄 글 도우미 한 벌 — 둘 다 같은 함수 + assert "export function numberLabels(" in struct + assert "...struct.numberLabels(() => numbers)," in table + assert "...numberLabels(struct.numbers)" in main + # 숨김 열 규칙 한 벌 + assert "struct.comboView(got.문서)" in table + assert "comboView(doc)" in main + # 서버 읽기 = 고른 층 · 주인 · 같은 집계표 이름 + assert 'readTemplate(layer, "table", struct.STRUCTURE_TABLE, null, owner)' in table + assert 'STRUCTURE_TABLE = "구조물집계표"' in struct + + +def test_모달_배선_고르면_보이고_편집_화면에서는_숨김(): + ts = _text(UI / "ui_template_item_picker.ts") + assert "createComboTable()" in ts + assert "comboTable.show(chosen);" in ts # 고른 항목이 바뀔 때마다 + assert "body.append(tableWrap, cols, editHost," in ts # 맨 위 + assert ts.count("tableWrap.hidden = true;") == 1 # 편집 화면 열 때 + assert ts.count("tableWrap.hidden = false;") == 1 # 돌아올 때 + assert "comboTable.destroy();" in ts # 닫을 때 표 거둠 + + +def test_표_높이_제한과_숨김_css(): + css = _text(UI / "ui_template_item_picker.css") + assert ".ui-item-picker__table .ui-sheet__scroll {" in css + assert "max-height: 26vh;" in css + assert ".ui-item-picker__table-wrap[hidden]" in css + + +def test_번호_읽기_층_길_추가_지금_층을_안_바꿈(): + api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts") + assert "export const fetchStructureNumbersAt = (" in api + assert "call(owned(layer, `/layers/${layer}/structures/numbers`, owner));" in api diff --git a/resources/tester/test_m02_combo_table.py b/resources/tester/test_m02_combo_table.py index 0d7ba931c..11802c88e 100644 --- a/resources/tester/test_m02_combo_table.py +++ b/resources/tester/test_m02_combo_table.py @@ -163,8 +163,11 @@ def test_일위대가_합계_줄_표_부품과_연결(): render = _text(SHEET / "ui_template_sheet_render.ts") assert "state.combo && state.laborSumLabel" in render and "masterRow(KEY_SUM)" in render main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") - assert "laborSumLabel: (colId: string) => laborSumText(struct.numbers()[colId])" in main - assert "laborSumState: (colId: string) => laborSumState(struct.numbers()[colId])" in main + # 줄 글은 도우미 한 벌(`numberLabels` · 고르기 모달 상단 표와 같음 · 224-1) + assert "...numberLabels(struct.numbers)" in main + helper = _text(M02 / "M02_MasterTemplete_Structure.ts") + assert "laborSumLabel: (colId: string) => laborSumText(numbers()[colId])" in helper + assert "laborSumState: (colId: string) => laborSumState(numbers()[colId])" in helper assert "(기준)" not in _text(M02 / "M02_MasterTemplete_Structure.ts") # 101-1 글 지움 assert "is-sum-${sum.state}" in render and "td.is-sum-error" in _text( SHEET / "ui_template_sheet.css" diff --git a/ui_template/ui_template_item_picker.css b/ui_template/ui_template_item_picker.css index b84f490ed..c91228696 100644 --- a/ui_template/ui_template_item_picker.css +++ b/ui_template/ui_template_item_picker.css @@ -52,6 +52,21 @@ overflow: auto; } +/* 상단 표(224-1) — M02 구조물집계표 같은 부품 · 높이만 줄임(표 안에서 굴림) */ +.ui-item-picker__table-wrap { + flex: none; + min-width: 0; +} + +.ui-item-picker__table .ui-sheet__scroll { + max-height: 26vh; +} + +.ui-item-picker__table-wrap[hidden], +.ui-item-picker__table[hidden] { + display: none; +} + .ui-item-picker__cols[hidden], .ui-item-picker__actions[hidden], .ui-item-picker .ui-modal__actions[hidden], diff --git a/ui_template/ui_template_item_picker.ts b/ui_template/ui_template_item_picker.ts index fe68930e6..6eb58469e 100644 --- a/ui_template/ui_template_item_picker.ts +++ b/ui_template/ui_template_item_picker.ts @@ -8,6 +8,7 @@ * * 26-3b — 카드 · 편집 · [재적용] · [개인에 저장] 은 **고르는 구조물의 복사본**(`structureId`) 기준. * 나무 개인 그룹 · 폴더를 오른쪽 클릭하면 「새 항목」(프로젝트에서 새 항목 → 개인 층에도 만들고 폼에 지정). + * 224-1 — 맨 위 = M02 구조물조합 상단 표(구조물집계표 · 보기 전용 · 고른 항목의 열이 골라짐). * 173-2 — 나무 = M02 나무 꼴: 층 단추 셋(하나만 보임 · 뿌리 줄 없음) · 개인 맨 위 「새 항목」 = 빈 자리 오른쪽 클릭. * ========================================================================== */ @@ -43,6 +44,7 @@ import { askFields } from "./ui_template_item_picker_ask"; import { projectItems, type CreatedItem } from "./ui_template_item_picker_project"; import { createItemDetail, type EditRequest } from "./ui_template_item_picker_detail"; import { createEditView, type EditView } from "./ui_template_item_picker_edit"; +import { createComboTable } from "./ui_template_item_picker_table"; export type { SummaryItem } from "./ui_template_item_picker_data"; @@ -146,6 +148,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise void load(created.key), }); + const comboTable = createComboTable(); const cols = el("div", { className: "ui-item-picker__cols", children: [ @@ -156,6 +159,10 @@ export function openItemPicker(opts: ItemPickerOptions): Promise { detailView.show(chosen); + comboTable.show(chosen); const layer = chosen ? layerOf(chosen) : null; applyButton.disabled = !chosen; clearButton.disabled = !opts.current; @@ -369,6 +377,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise { if (editView && !(await editView.leave())) return false; editView?.destroy(); + comboTable.destroy(); resolve({ action: "cancel" }); return true; }, mount: (body) => { - body.append(cols, editHost, ...(opts.projectId ? [actions] : [])); + body.append(tableWrap, cols, editHost, ...(opts.projectId ? [actions] : [])); paintDetail(); void load(null); }, diff --git a/ui_template/ui_template_item_picker_data.ts b/ui_template/ui_template_item_picker_data.ts index 45dd71628..16aaf25e4 100644 --- a/ui_template/ui_template_item_picker_data.ts +++ b/ui_template/ui_template_item_picker_data.ts @@ -85,6 +85,9 @@ export const ownerOf = (row: PickerRow): string | null => /** 같은 항목의 자리표 — 층 · 주인 · 키 */ export const rowId = (row: PickerRow): string => `${layerOf(row)}|${ownerOf(row) ?? ""}|${row.key}`; +/** 상단 표(224-1)가 보이는 집계표 = 고른 항목이 속한 층(+ 주인) 것 — 같은 키면 표는 그대로 두고 열만 옮김 */ +export const tableKey = (row: PickerRow): string => `${layerOf(row)}|${ownerOf(row) ?? ""}`; + export function toSummary(row: PickerRow): SummaryItem { return { layer: layerOf(row), owner: ownerOf(row), key: row.key, name: itemName(row) }; } diff --git a/ui_template/ui_template_item_picker_table.ts b/ui_template/ui_template_item_picker_table.ts new file mode 100644 index 000000000..5d742be8a --- /dev/null +++ b/ui_template/ui_template_item_picker_table.ts @@ -0,0 +1,94 @@ +/* ============================================================================= + * ui_template_item_picker_table.ts + * 항목 고르기 모달 상단 표(PLAN 224-1) — M02 「구조물 조합」 페이지 상단의 구조물집계표를 **같은 부품** + * (`ui_template/sheet` `createSheet` · 줄 글 = `numberLabels` · 숨김 열 = `comboView`)으로 보임. + * 보기 전용(`readOnly` · 고침은 M02 에서) · 고른 항목이 속한 층(+ 주인)의 표 · 고른 항목의 열이 밝게 골라짐. + * 같은 층의 다른 항목을 고르면 표는 그대로 두고 열만 옮김 · 다른 층 항목이면 그 층 표로 바뀜. + * 표 부품 · 집계표 모듈은 처음 보일 때만 `import()`(B05 · B06 번들에 안 섞임). + * ========================================================================== */ + +import { el } from "./ui_template_elements"; +import { + fetchStructureNumbersAt, + readTemplate, +} from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch"; +import { layerOf, ownerOf, tableKey, type PickerRow } from "./ui_template_item_picker_data"; +import { t } from "./ui_template_locale"; +import type { SheetDoc, SheetHandle } from "./sheet/ui_template_sheet"; + +export interface ComboTable { + root: HTMLElement; + /** 고른 항목 — `null` 이면 표를 숨김 */ + show: (row: PickerRow | null) => void; + destroy: () => void; +} + +const failed = (error: unknown): string => + t("M02_PickerTableFailed").replace( + "{value}", + error instanceof Error ? error.message : String(error), + ); + +export function createComboTable(): ComboTable { + const root = el("div", { className: "ui-item-picker__table", attrs: { hidden: "" } }); + let seq = 0; + let shownKey: string | null = null; + let sheet: SheetHandle | null = null; + /** 가장 나중에 고른 열 — 표를 읽는 사이에 바뀌면 읽은 뒤 이 열을 고름 */ + let column = ""; + + const drop = (): void => { + sheet?.destroy(); + sheet = null; + shownKey = null; + }; + + async function paint(row: PickerRow, key: string, mine: number): Promise { + drop(); + root.replaceChildren( + el("p", { className: "ui-item-picker__hint", text: t("M02_PickerLoading") }), + ); + try { + const layer = layerOf(row); + const owner = ownerOf(row); + const [{ createSheet }, struct] = await Promise.all([ + import("./sheet/ui_template_sheet"), + import("../M02_MasterTemplete/M02_MasterTemplete_Structure"), + ]); + const [got, numbers] = await Promise.all([ + readTemplate(layer, "table", struct.STRUCTURE_TABLE, null, owner), + fetchStructureNumbersAt(layer, owner).catch(() => ({})), + ]); + if (mine !== seq) return; + sheet = createSheet(root, struct.comboView(got.문서) as SheetDoc, { + mode: "master", + readOnly: true, + lockHead: true, + combo: {}, + ...struct.numberLabels(() => numbers), + }); + shownKey = key; + sheet.selectColumn(column); + } catch (error) { + if (mine === seq) + root.replaceChildren(el("p", { className: "ui-item-picker__hint", text: failed(error) })); + } + } + + return { + root, + show: (row) => { + const mine = ++seq; + root.hidden = !row; + if (!row) return; + column = row.열; + const key = tableKey(row); + if (sheet && key === shownKey) return sheet.selectColumn(column); + void paint(row, key, mine); + }, + destroy: () => { + seq += 1; + drop(); + }, + }; +} diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 6a13b6696..31fc267ea 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -80,6 +80,7 @@ export const ui_locales_m2 = { M02_PickerNoMatch: ["찾는 항목 없음", "No matching items"], M02_PickerLoading: ["읽는 중…", "Loading…"], M02_PickerLoadFailed: ["항목을 못 읽음 — {value}", "Failed to load items — {value}"], + M02_PickerTableFailed: ["상단 표를 못 읽음 — {value}", "Failed to load the top table — {value}"], M02_PickerDrawing: ["도면", "Drawing"], M02_PickerArea: ["면적산출", "Area takeoff"], M02_PickerUnitPrice: ["일위대가", "Unit price"],