Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_5' into sub_laptop_6

This commit is contained in:
2026-10-10 16:31:43 +09:00
11 changed files with 453 additions and 10 deletions
@@ -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
+5 -2
View File
@@ -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"
+15
View File
@@ -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],
+12 -1
View File
@@ -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();
},
};
}
+1
View File
@@ -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"],