feat(ui): B05/B06 구조물 조합 고르기 모달 상단에 M02 구조물집계표 보기 전용 표 (224-1)
- 고른 항목이 속한 층 · 주인의 구조물집계표를 M02 와 같은 표 부품 · 줄 글 도우미(numberLabels) · 숨김 열 규칙(comboView)으로 보임 - 같은 층 다른 조합 = 열만 옮김 · 다른 층 = 표 바뀜 · 편집 화면에서는 숨김 - 새 시험 test_224_1 · test_m02_combo_table 단정 고침 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
This commit is contained in:
@@ -243,6 +243,13 @@ export interface StructureNumbers {
|
||||
export const fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>> =>
|
||||
scoped("/structures/numbers");
|
||||
|
||||
/** 같은 번호 목록을 **고른 층**(+ 주인)에서 — 지금 층 길을 안 바꾸고 읽음(고르기 모달 상단 표 · 224-1) */
|
||||
export const fetchStructureNumbersAt = (
|
||||
layer: Layer,
|
||||
owner: string | null,
|
||||
): Promise<Record<string, StructureNumbers>> =>
|
||||
call(owned(layer, `/layers/${layer}/structures/numbers`, owner));
|
||||
|
||||
/** 구조물집계표조합 한 줄 — 구조물 열 하나(키 `SS` + 6자리 · 한 번 준 키는 안 바뀜) */
|
||||
export interface StructureCombo extends StructureNumbers {
|
||||
키: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
? {}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<ItemPickerResul
|
||||
// 개인에 저장 — 나무를 다시 받고 새 항목을 고름(개인 그룹에 뜸)
|
||||
onCreated: (created) => 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<ItemPickerResul
|
||||
detailView.root,
|
||||
],
|
||||
});
|
||||
const tableWrap = el("div", {
|
||||
className: "ui-item-picker__table-wrap",
|
||||
children: [comboTable.root],
|
||||
});
|
||||
const editHost = el("div", { className: "ui-item-picker__edit-host", attrs: { hidden: "" } });
|
||||
let editView: EditView | null = null;
|
||||
const applyButton = createButton({ label: t("M02_PickerApply"), variant: "filled" });
|
||||
@@ -174,6 +181,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
|
||||
|
||||
const paintDetail = (): void => {
|
||||
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<ItemPickerResul
|
||||
editView = view;
|
||||
editHost.replaceChildren(view.root);
|
||||
cols.hidden = true;
|
||||
tableWrap.hidden = true;
|
||||
actions.hidden = true;
|
||||
headButtons.hidden = true;
|
||||
editHost.hidden = false;
|
||||
@@ -380,6 +389,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
|
||||
editView = null;
|
||||
editHost.hidden = true;
|
||||
cols.hidden = false;
|
||||
tableWrap.hidden = false;
|
||||
actions.hidden = false;
|
||||
headButtons.hidden = false;
|
||||
if (saved) detailView.markEdited();
|
||||
@@ -396,11 +406,12 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
|
||||
beforeClose: async () => {
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user