feat(sheet): 층마다 열 더하기(분류1 · 분류2 · 분류3) · 마스터 줄 = 일위대가 · 도면 · 면적산출 · 재료산출
- 분류2 더하기 안 됨 고침 — 옛 「열 더하기」는 고른 열 맨 아래 글을 「새 열」로 바꿔 넣어, 같은 글이면 머리가 가로로 합쳐져 새 열이 안 보였음 · 같은 무리에 같은 글이면 뒤에 수를 붙임 - 단추 = 층 이름 + 더하기(「분류1 더하기」 · 「분류2 더하기」 · 「분류3 더하기」) · 고른 열과 그 층 위 머리가 같은 이웃 끝에 둠 · 아래 층은 빈칸 - 마스터 줄에서 식 · 들어갈 것 줄 지움 · 산출근거 줄 → 면적산출 · 재료산출 줄(areaLabel · materialLabel · 조회 후 선택 · 글 열은 빈칸 잠김) - 쓰지 않게 된 열 딱지 글 · 줄임 겉모양 지움 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""표 도면 줄 · 칸 고르기 신호 — `ui_template_sheet_ops.ts` 를 Node 로 바로 돌려 봄.
|
||||
|
||||
도면 줄 `s:drawing` · 산출근거 줄 `s:basis` = master 만 · 일위대가 줄 바로 아래 차례(키보드) · project 에는 없음.
|
||||
마스터 줄 = 일위대가 · 도면 `s:drawing` · 면적산출 `s:area` · 재료산출 `s:material`(식 · 들어갈 것 줄 없음) ·
|
||||
master 만 · 단위 줄 바로 아래 차례(키보드) · project 에는 없음.
|
||||
`selectionOf` = 격자가 `onSelect` 로 넘기는 모양({row, col, colId} · 모르는 열은 col -1 · colId null).
|
||||
"""
|
||||
|
||||
@@ -25,7 +26,7 @@ console.log(JSON.stringify({
|
||||
master: navRows(doc, "master"),
|
||||
project: navRows(doc, "project"),
|
||||
pick: [selectionOf(doc, "s:drawing", "c2"), selectionOf(doc, "d:r1", "c1"), selectionOf(doc, "s:unit", "zz")],
|
||||
blank: ["s:price", "s:drawing", "s:basis", "s:desc", "d:r1"].map((k) => [isBlankPick(k, { id: "t", 머리: [], 꼴: "글" }), isBlankPick(k, { id: "n", 머리: [], 꼴: "수" })]),
|
||||
blank: ["s:price", "s:drawing", "s:area", "s:material", "s:unit", "d:r1"].map((k) => [isBlankPick(k, { id: "t", 머리: [], 꼴: "글" }), isBlankPick(k, { id: "n", 머리: [], 꼴: "수" })]),
|
||||
}));
|
||||
"""
|
||||
|
||||
@@ -52,7 +53,7 @@ def _run() -> dict:
|
||||
def test_drawing_row_under_price_master_only():
|
||||
got = _run()
|
||||
assert got["master"] == [
|
||||
"s:unit", "s:formula", "s:desc", "s:price", "s:drawing", "s:basis", "d:r1", "t:t1",
|
||||
"s:unit", "s:price", "s:drawing", "s:area", "s:material", "d:r1", "t:t1",
|
||||
] # fmt: skip
|
||||
assert got["project"] == ["s:unit", "d:r1", "t:t1"]
|
||||
|
||||
@@ -67,5 +68,5 @@ def test_selection_shape():
|
||||
|
||||
def test_text_column_pick_rows_blank():
|
||||
assert _run()["blank"] == [
|
||||
[True, False], [True, False], [True, False], [False, False], [False, False],
|
||||
[True, False], [True, False], [True, False], [True, False], [False, False], [False, False],
|
||||
] # fmt: skip
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""표 둘째 층 묶음 더하기 · 층 이름 고치기 — `ui_template_sheet_ops.ts` 를 Node 로 바로 돌려 봄.
|
||||
"""표 층마다 열 더하기 · 층 이름 고치기 — `ui_template_sheet_ops.ts` 를 Node 로 바로 돌려 봄.
|
||||
|
||||
`addGroup` = 고른 열과 같은 맨 위 묶음(종류) 끝에 `[종류, 새 분류, …, 새 열]` · 다른 종류 열은 안 건드림.
|
||||
`addLevel` = 고른 열과 그 층 위 머리가 같은 이웃 끝에 `[…위 머리, 새 글, "" …]` · 같은 무리에 같은 글이
|
||||
있으면 뒤에 수(같은 머리는 가로로 합쳐져 새 열이 안 보였음 — 「분류2 더하기 안 됨」).
|
||||
`setLayerName` = 층 이름(줄 머리 글) · 모자란 층은 빈 글로 채움.
|
||||
"""
|
||||
|
||||
@@ -19,20 +20,27 @@ OPS = ROOT / "ui_template" / "sheet" / "ui_template_sheet_ops.ts"
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
SCRIPT = """
|
||||
const { addGroup, setLayerName } = await import(process.argv[1]);
|
||||
const { addLevel, setLayerName } = await import(process.argv[1]);
|
||||
const base = JSON.parse(process.argv[2]);
|
||||
const out = {};
|
||||
const heads = (doc) => doc.열.map((c) => [c.id, c.머리]);
|
||||
let doc = structuredClone(base);
|
||||
out.id = addGroup(doc, "a1", 3, "새 분류", "새 열");
|
||||
out.after_a1 = doc.열.map((c) => [c.id, c.머리]);
|
||||
out.id = addLevel(doc, "a1", 1, 4, "새 분류1");
|
||||
out.level1 = heads(doc);
|
||||
doc = structuredClone(base);
|
||||
addGroup(doc, "b1", 4, "G", "L");
|
||||
out.deep = doc.열.at(-1).머리;
|
||||
addLevel(doc, "a1", 2, 4, "새 분류2");
|
||||
addLevel(doc, "a1", 2, 4, "새 분류2");
|
||||
out.level2 = heads(doc);
|
||||
doc = structuredClone(base);
|
||||
addGroup(doc, null, 3, "G", "L");
|
||||
out.none = doc.열.at(-1).머리;
|
||||
addLevel(doc, "a2", 3, 4, "새 분류3");
|
||||
out.level3 = heads(doc);
|
||||
doc = structuredClone(base);
|
||||
setLayerName(doc, 1, " 분류1 ");
|
||||
addLevel(doc, "b1", 2, 4, "L");
|
||||
out.merged = doc.열.at(-1).머리;
|
||||
doc = structuredClone(base);
|
||||
out.none = addLevel(doc, "zz", 1, 4, "G");
|
||||
doc = structuredClone(base);
|
||||
setLayerName(doc, 1, " 공법 ");
|
||||
out.layers = doc.층;
|
||||
doc = structuredClone(base);
|
||||
delete doc.층;
|
||||
@@ -45,12 +53,12 @@ DOC = {
|
||||
"양식": "시험",
|
||||
"종류": "표",
|
||||
"판": 1,
|
||||
"층": ["종류", "공법", "규격"],
|
||||
"층": ["종류", "분류1", "분류2", "분류3"],
|
||||
"열": [
|
||||
{"id": "a1", "머리": ["가", "x", "1"], "단위": "m"},
|
||||
{"id": "a2", "머리": ["가", "x", "2"]},
|
||||
{"id": "a3", "머리": ["가", "y", None]},
|
||||
{"id": "b1", "머리": ["나", None, None]},
|
||||
{"id": "a1", "머리": ["가", "x", "1", ""], "단위": "m"},
|
||||
{"id": "a2", "머리": ["가", "x", "2", ""]},
|
||||
{"id": "a3", "머리": ["가", "y", None, None]},
|
||||
{"id": "b1", "머리": ["나", None, None, None]},
|
||||
],
|
||||
"줄": [],
|
||||
}
|
||||
@@ -66,25 +74,40 @@ def _run() -> dict:
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_group_goes_to_end_of_same_top():
|
||||
def test_level1_goes_to_end_of_same_kind():
|
||||
got = _run()
|
||||
assert got["id"] == "c1"
|
||||
assert got["after_a1"] == [
|
||||
["a1", ["가", "x", "1"]],
|
||||
["a2", ["가", "x", "2"]],
|
||||
["a3", ["가", "y", None]],
|
||||
["c1", ["가", "새 분류", "새 열"]],
|
||||
["b1", ["나", None, None]],
|
||||
assert got["level1"] == [
|
||||
["a1", ["가", "x", "1", ""]],
|
||||
["a2", ["가", "x", "2", ""]],
|
||||
["a3", ["가", "y", None, None]],
|
||||
["c1", ["가", "새 분류1", "", ""]],
|
||||
["b1", ["나", None, None, None]],
|
||||
]
|
||||
|
||||
|
||||
def test_group_depth_and_no_selection():
|
||||
def test_level2_twice_gets_distinct_labels():
|
||||
assert _run()["level2"] == [
|
||||
["a1", ["가", "x", "1", ""]],
|
||||
["a2", ["가", "x", "2", ""]],
|
||||
["c1", ["가", "x", "새 분류2", ""]],
|
||||
["c2", ["가", "x", "새 분류2 2", ""]],
|
||||
["a3", ["가", "y", None, None]],
|
||||
["b1", ["나", None, None, None]],
|
||||
]
|
||||
|
||||
|
||||
def test_level3_right_after_same_prefix():
|
||||
assert _run()["level3"][2] == ["c1", ["가", "x", "2", "새 분류3"]]
|
||||
|
||||
|
||||
def test_merged_prefix_and_no_column():
|
||||
got = _run()
|
||||
assert got["deep"] == ["나", "G", None, "L"]
|
||||
assert got["none"] == ["G", "G", "L"]
|
||||
assert got["merged"] == ["나", None, "L", ""]
|
||||
assert got["none"] is None
|
||||
|
||||
|
||||
def test_layer_name():
|
||||
got = _run()
|
||||
assert got["layers"] == ["종류", "분류1", "규격"]
|
||||
assert got["layers"] == ["종류", "공법", "분류2", "분류3"]
|
||||
assert got["padded"] == ["", "", "분류2"]
|
||||
|
||||
@@ -151,38 +151,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ui-sheet__clamp {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
}
|
||||
|
||||
.ui-sheet__chip {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ui-sheet__chip.is-bound {
|
||||
background: color-mix(in srgb, var(--color-success) 18%, var(--color-canvas));
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.ui-sheet__chip.is-calc {
|
||||
background: color-mix(in srgb, var(--color-warning) 22%, var(--color-canvas));
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.ui-sheet__chip.is-hand {
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.ui-sheet__headhint {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/* =============================================================================
|
||||
* ui_template_sheet.ts
|
||||
* 엑셀처럼 도는 표 부품 — `createSheet(칸, 문서, {mode, onChange, drawingLabel, basisLabel, onSelect, onPick})`
|
||||
* 엑셀처럼 도는 표 부품 — `createSheet(칸, 문서, {mode, onChange, drawingLabel, areaLabel, materialLabel, onSelect, onPick})`
|
||||
* → `{getDoc, setDoc, recalc, destroy}`.
|
||||
*
|
||||
* master = 시스템 관리자 양식 고치기 — 머리 · 층 이름 · 단위 · 식 · 들어갈 것 · 일위대가 · 줄 · 열 더하기·지우기 ·
|
||||
* 둘째 층 묶음 더하기 · 손 열.
|
||||
* 도면 · 산출근거 줄 = `drawingLabel` · `basisLabel` 글 · 칸을 고르면 `onSelect` ·
|
||||
* `onPick` 을 주면 일위대가 · 도면 · 산출근거 칸은 치지 않고 조회 후 선택.
|
||||
* master = 시스템 관리자 양식 고치기 — 머리 · 층 이름 · 단위 · 일위대가 · 줄 더하기·지우기 ·
|
||||
* 층마다 열 더하기(「분류1 더하기」 … 층 이름 + 더하기) · 열 지우기 · 손 열.
|
||||
* 도면 · 면적산출 · 재료산출 줄 = `drawingLabel` · `areaLabel` · `materialLabel` 글 ·
|
||||
* 칸을 고르면 `onSelect` · `onPick` 을 주면 일위대가 · 도면 · 면적산출 · 재료산출 칸은
|
||||
* 치지 않고 조회 후 선택.
|
||||
* project = 프로젝트 표 — 바인딩 · 계산 열 잠금 · 손 열만 입력 · 「전구간」 줄 맨 위 · 쪽줄마다 쪽.
|
||||
* 칸을 고치면 문서를 그 자리에서 바꾸고 같은 풀이(`_recalc`)로 즉시 다시 그림 — 서버 왕복 없음.
|
||||
* 저장은 부른 쪽 몫(`onChange` 로 문서를 받음 · 자동저장 없음 · [저장] 때 서버가 Node 로 다시 풂).
|
||||
@@ -16,19 +17,17 @@
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { startEdit } from "../../M01_MasterData/M01_MasterData_UI_Cells";
|
||||
import {
|
||||
addColumn,
|
||||
addGroup,
|
||||
addLevel,
|
||||
addRow,
|
||||
canDeleteRow,
|
||||
deleteColumn,
|
||||
deleteRow,
|
||||
isBlankPick,
|
||||
KEY_BASIS,
|
||||
KEY_DRAWING,
|
||||
KEY_PRICE,
|
||||
KEY_UNIT,
|
||||
navRows,
|
||||
PICK_ROWS,
|
||||
REF_ROWS,
|
||||
selectionOf,
|
||||
type SheetMode,
|
||||
setCell,
|
||||
@@ -51,13 +50,14 @@ export interface SheetOptions {
|
||||
onChange?: (doc: SheetDoc) => void;
|
||||
/** 도면 줄 칸 글(마스터 첫 쪽 · 열 id) — 없거나 빈 글이면 「없음」 · 글이 바뀌면 `recalc()` 로 다시 그림. */
|
||||
drawingLabel?: (colId: string) => string;
|
||||
/** 산출근거 줄 칸 글(표번) — `drawingLabel` 과 같음. */
|
||||
basisLabel?: (colId: string) => string;
|
||||
/** 면적산출 · 재료산출 줄 칸 글(번호) — `drawingLabel` 과 같음. */
|
||||
areaLabel?: (colId: string) => string;
|
||||
materialLabel?: (colId: string) => string;
|
||||
/** 칸을 고름 — 누르기 · 방향키 · Tab. */
|
||||
onSelect?: (sel: SheetSelection) => void;
|
||||
/** 조회 후 선택 — master 일위대가 · 도면 · 산출근거 칸을 두 번 누르기 · Enter · F2 로 부름.
|
||||
* 글을 돌려주면 일위대가 칸에 넣음 · 도면 · 산출근거는 부른 쪽이 가져온 뒤 `recalc()`.
|
||||
* 없으면 일위대가는 글로 고침 · 도면 · 산출근거는 읽기만. */
|
||||
/** 조회 후 선택 — master 일위대가 · 도면 · 면적산출 · 재료산출 칸을 두 번 누르기 · Enter · F2 로 부름.
|
||||
* 글을 돌려주면 일위대가 칸에 넣음 · 번호 글 줄은 부른 쪽이 가져온 뒤 `recalc()`.
|
||||
* 없으면 일위대가는 글로 고침 · 번호 글 줄은 읽기만. */
|
||||
onPick?: (sel: SheetSelection) => Promise<string | null | undefined> | void;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
|
||||
result: { 계산: {}, 합계: {}, 오류: [] },
|
||||
sel: null,
|
||||
drawingLabel: opts.drawingLabel,
|
||||
basisLabel: opts.basisLabel,
|
||||
areaLabel: opts.areaLabel,
|
||||
materialLabel: opts.materialLabel,
|
||||
pick: !!opts.onPick,
|
||||
};
|
||||
const scroll = el("div", { className: "ui-sheet__scroll" });
|
||||
@@ -110,17 +111,17 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
|
||||
state.sel = next && state.sel ? { r: next, c: state.sel.c } : null;
|
||||
changed();
|
||||
});
|
||||
const colAdd = button(st("Col_Add"), () => {
|
||||
const id = addColumn(state.doc, state.sel?.c ?? null, st("Col_New"));
|
||||
/** `level` 층 새 칸 열 — 고른 열 무리 끝에(층 이름이 있으면 「새 분류2」 · 없으면 「새 열」) */
|
||||
const addAt = (level: number): void => {
|
||||
const at = state.sel?.c;
|
||||
if (!at) return;
|
||||
const name = state.doc.층?.[level]?.trim();
|
||||
const label = name ? `${st("New_Prefix")}${name}` : st("Col_New");
|
||||
const id = addLevel(state.doc, at, level, headDepth(state.doc.열, state.doc.층), label);
|
||||
if (!id) return;
|
||||
state.sel = { r: state.sel?.r ?? KEY_UNIT, c: id };
|
||||
changed();
|
||||
});
|
||||
const groupAdd = button(st("Group_Add"), () => {
|
||||
const depth = headDepth(state.doc.열, state.doc.층);
|
||||
const id = addGroup(state.doc, state.sel?.c ?? null, depth, st("Group_New"), st("Col_New"));
|
||||
state.sel = { r: state.sel?.r ?? KEY_UNIT, c: id };
|
||||
changed();
|
||||
});
|
||||
};
|
||||
const colDelete = button(st("Col_Delete"), () => {
|
||||
const id = state.sel?.c;
|
||||
if (!id || state.doc.열.length <= 1) return;
|
||||
@@ -138,20 +139,34 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
|
||||
changed();
|
||||
});
|
||||
toolbar.append(rowAdd, rowDelete);
|
||||
if (opts.mode === "master") toolbar.append(colAdd, groupAdd, colDelete, colHand);
|
||||
if (opts.mode === "master") toolbar.append(colDelete, colHand);
|
||||
|
||||
// 층마다 열 더하기 단추 — 층이 둘 이상이면 둘째 층부터(「분류1 더하기」 …) · 하나면 「열 더하기」 ·
|
||||
// 층 이름 · 층 수가 바뀌면 다시 만듦
|
||||
let levelButtons: HTMLButtonElement[] = [];
|
||||
let levelKey = "";
|
||||
const syncLevels = (hasCol: boolean): void => {
|
||||
const depth = headDepth(state.doc.열, state.doc.층);
|
||||
const levels = depth < 2 ? [0] : Array.from({ length: depth - 1 }, (_, i) => i + 1);
|
||||
const labels = levels.map((level) => {
|
||||
const name = state.doc.층?.[level]?.trim();
|
||||
return name ? `${name}${st("Add_Suffix")}` : st("Col_Add");
|
||||
});
|
||||
if (labels.join("|") !== levelKey) {
|
||||
levelKey = labels.join("|");
|
||||
for (const b of levelButtons) b.remove();
|
||||
levelButtons = levels.map((level, i) => button(labels[i], () => addAt(level)));
|
||||
colDelete.before(...levelButtons);
|
||||
}
|
||||
for (const b of levelButtons) b.disabled = !hasCol;
|
||||
};
|
||||
|
||||
const syncToolbar = (): void => {
|
||||
const rowId = rowIdOf(state.sel?.r);
|
||||
const row = state.doc.줄.find((r) => r.id === rowId);
|
||||
rowDelete.disabled = !row || !canDeleteRow(state.mode, row);
|
||||
const col = state.sel ? colOf(state.sel.c) : undefined;
|
||||
// 둘째 층 묶음 — 층이 셋 이상일 때만 · 이름은 둘째 층 이름(「분류1 더하기」)
|
||||
const group = state.doc.층?.[1]?.trim();
|
||||
groupAdd.hidden = headDepth(state.doc.열, state.doc.층) < 3;
|
||||
groupAdd.disabled = !col;
|
||||
groupAdd.querySelector(".ui-btn__label")!.textContent = group
|
||||
? `${group}${st("Add_Suffix")}`
|
||||
: st("Group_Add");
|
||||
if (opts.mode === "master") syncLevels(!!col);
|
||||
colDelete.disabled = !col || state.doc.열.length <= 1;
|
||||
colHand.disabled = !col;
|
||||
colHand.classList.toggle("is-on", !!col?.손);
|
||||
@@ -203,10 +218,8 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
|
||||
// ── 칸 고치기 ─────────────────────────────────────────────────────
|
||||
const rawText = (key: string, col: SheetColumn): string => {
|
||||
if (key === KEY_UNIT) return col.단위 ?? "";
|
||||
if (key === "s:formula") return col.식 ?? "";
|
||||
if (key === "s:desc") return col.설명 ?? "";
|
||||
if (key === KEY_PRICE) return col.일위대가 ?? "";
|
||||
if (key === KEY_DRAWING || key === KEY_BASIS) return refText(state, key, col.id);
|
||||
if (REF_ROWS.includes(key)) return refText(state, key, col.id);
|
||||
const row = state.doc.줄.find((r) => `d:${r.id}` === key);
|
||||
const formula = row?.식?.[col.id] ?? col.식;
|
||||
if (formula) return `=${formula}`;
|
||||
@@ -214,13 +227,9 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
|
||||
return raw === null || raw === undefined ? "" : String(raw);
|
||||
};
|
||||
const write = (key: string, col: SheetColumn, text: string): void => {
|
||||
if (key === KEY_DRAWING || key === KEY_BASIS) return; // 읽기만
|
||||
if (REF_ROWS.includes(key)) return; // 읽기만
|
||||
const value = text.trim();
|
||||
if (key === KEY_UNIT) col.단위 = value || null;
|
||||
else if (key === "s:formula") {
|
||||
if (value) col.식 = value;
|
||||
else delete col.식;
|
||||
} else if (key === "s:desc") col.설명 = value;
|
||||
else if (key === KEY_PRICE) col.일위대가 = value || null;
|
||||
else setCell(state.doc, key.slice(2), col, text);
|
||||
changed();
|
||||
|
||||
@@ -21,14 +21,17 @@ const PAGE_ROWS = 50;
|
||||
export const KEY_UNIT = "s:unit";
|
||||
export const KEY_PRICE = "s:price";
|
||||
export const KEY_DRAWING = "s:drawing";
|
||||
export const KEY_BASIS = "s:basis";
|
||||
/** 마스터 전용 줄(첫 쪽 단위 줄 아래 차례) — 도면 · 산출근거 줄은 손으로 못 침. */
|
||||
export const MASTER_ROWS = ["s:formula", "s:desc", KEY_PRICE, KEY_DRAWING, KEY_BASIS] as const;
|
||||
export const KEY_AREA = "s:area";
|
||||
export const KEY_MATERIAL = "s:material";
|
||||
/** 마스터 전용 줄(첫 쪽 단위 줄 아래 차례) — 도면 · 면적산출 · 재료산출 줄은 손으로 못 침(번호 글). */
|
||||
export const MASTER_ROWS = [KEY_PRICE, KEY_DRAWING, KEY_AREA, KEY_MATERIAL] as const;
|
||||
export type MasterRowKey = (typeof MASTER_ROWS)[number];
|
||||
/** 번호 글 줄 — 부른 쪽이 준 글(`refLabel`)만 보임. */
|
||||
export const REF_ROWS: readonly string[] = [KEY_DRAWING, KEY_AREA, KEY_MATERIAL];
|
||||
/** 조회 후 선택 줄 — `onPick` 을 주면 이 줄 칸은 치지 않고 고름. */
|
||||
export const PICK_ROWS: readonly string[] = [KEY_PRICE, KEY_DRAWING, KEY_BASIS];
|
||||
export const PICK_ROWS: readonly string[] = MASTER_ROWS;
|
||||
|
||||
/** 글 열(NO · 측점 · 비고 같은 꼴=글)의 일위대가 · 도면 · 산출근거 칸 — 빈칸 · 잠김. */
|
||||
/** 글 열(NO · 측점 · 비고 같은 꼴=글)의 일위대가 · 도면 · 면적산출 · 재료산출 칸 — 빈칸 · 잠김. */
|
||||
export const isBlankPick = (key: string, col: SheetColumn): boolean =>
|
||||
PICK_ROWS.includes(key) && col.꼴 === "글";
|
||||
|
||||
@@ -113,47 +116,32 @@ export function deleteRow(doc: SheetDoc, id: string): void {
|
||||
doc.줄 = doc.줄.filter((r) => r.id !== id);
|
||||
}
|
||||
|
||||
/** `after` 열 오른쪽에 같은 묶음의 새 열 — 윗머리는 그대로 · 맨 아래 글만 「새 열」. */
|
||||
export function addColumn(doc: SheetDoc, after: string | null, label: string): string {
|
||||
const id = freshId(
|
||||
"c",
|
||||
doc.열.map((c) => c.id),
|
||||
);
|
||||
const at = doc.열.findIndex((c) => c.id === after);
|
||||
const base = at < 0 ? null : doc.열[at];
|
||||
const head = base ? [...base.머리] : [label];
|
||||
if (base) {
|
||||
let last = head.length - 1;
|
||||
while (last > 0 && head[last] === null) last -= 1;
|
||||
head[last] = label;
|
||||
}
|
||||
const col: SheetColumn = { id, 머리: head, 단위: base?.단위 ?? null, 꼴: "수" };
|
||||
doc.열.splice(at < 0 ? doc.열.length : at + 1, 0, col);
|
||||
return id;
|
||||
}
|
||||
|
||||
/** `after` 열과 같은 맨 위 묶음(종류) 끝에 둘째 층 새 묶음 열 — `[위, group, …null, leaf]`(층 `depth` 가
|
||||
* 3 이상일 때 · 가운데 층은 위 칸과 합침) · 새 열 id. */
|
||||
export function addGroup(
|
||||
/** `level` 층 새 칸 열 — `after` 열과 그 층 위 머리(`머리[0..level-1]`)가 같은 이웃 끝에 둠 ·
|
||||
* `[…위 머리, label, "" …]`(아래 층은 빈칸) · 같은 무리에 같은 글이 있으면 뒤에 수를 붙임
|
||||
* (같은 머리는 가로로 합쳐져 새 열이 안 보임) · 새 열 id(`after` 가 없으면 null). */
|
||||
export function addLevel(
|
||||
doc: SheetDoc,
|
||||
after: string | null,
|
||||
after: string,
|
||||
level: number,
|
||||
depth: number,
|
||||
group: string,
|
||||
leaf: string,
|
||||
): string {
|
||||
label: string,
|
||||
): string | null {
|
||||
let at = doc.열.findIndex((c) => c.id === after);
|
||||
if (at < 0) return null;
|
||||
const base = doc.열[at];
|
||||
const top = Array.from({ length: level }, (_, i) => base.머리[i] ?? null);
|
||||
const mates = (col: SheetColumn): boolean => top.every((h, i) => (col.머리[i] ?? null) === h);
|
||||
while (at + 1 < doc.열.length && mates(doc.열[at + 1])) at += 1;
|
||||
const used = new Set(doc.열.filter(mates).map((c) => c.머리[level]));
|
||||
let text = label;
|
||||
for (let n = 2; used.has(text); n += 1) text = `${label} ${n}`;
|
||||
const head: (string | null)[] = [...top, text];
|
||||
while (head.length < depth) head.push("");
|
||||
const id = freshId(
|
||||
"c",
|
||||
doc.열.map((c) => c.id),
|
||||
);
|
||||
let at = doc.열.findIndex((c) => c.id === after);
|
||||
const base = at < 0 ? null : doc.열[at];
|
||||
const top = base?.머리[0] ?? group;
|
||||
if (base) while (at + 1 < doc.열.length && doc.열[at + 1].머리[0] === top) at += 1;
|
||||
const head: (string | null)[] = [top, group];
|
||||
while (head.length < depth - 1) head.push(null);
|
||||
head.push(leaf);
|
||||
const col: SheetColumn = { id, 머리: head, 단위: base?.단위 ?? null, 꼴: "수" };
|
||||
doc.열.splice(at < 0 ? doc.열.length : at + 1, 0, col);
|
||||
doc.열.splice(at + 1, 0, { id, 머리: head, 단위: base.단위 ?? null, 꼴: "수" });
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 표 그리기 — 문서 + 풀이 결과 → 쪽마다 `<table>`(머리 층 · 단위 줄 · 본문 · 합계 줄).
|
||||
*
|
||||
* 칸마다 `data-r`(줄 열쇠) · `data-c`(열 id) — 고치기 · 키보드는 격자(`ui_template_sheet.ts`)가 위임으로 받음.
|
||||
* 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|formula|desc|price|drawing|basis`.
|
||||
* master = 값 대신 열마다 식 · 들어갈 것 · 일위대가 · 도면 · 산출근거 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽).
|
||||
* 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|price|drawing|area|material`.
|
||||
* master = 본문 위에 열마다 일위대가 · 도면 · 면적산출 · 재료산출 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽).
|
||||
* 머리 층 줄 머리 칸은 `data-layer`(층 차례) — master 에서 층 이름을 고침.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -16,13 +16,14 @@ import {
|
||||
isBoundColumn,
|
||||
isBlankPick,
|
||||
isCalcColumn,
|
||||
KEY_BASIS,
|
||||
KEY_DRAWING,
|
||||
KEY_AREA,
|
||||
KEY_MATERIAL,
|
||||
KEY_UNIT,
|
||||
MASTER_ROWS,
|
||||
type MasterRowKey,
|
||||
PICK_ROWS,
|
||||
pagePlan,
|
||||
REF_ROWS,
|
||||
type SheetMode,
|
||||
} from "./ui_template_sheet_ops";
|
||||
import { groupDigits, isNotice, totalFormula } from "./ui_template_sheet_recalc";
|
||||
@@ -38,17 +39,21 @@ export interface RenderState {
|
||||
mode: SheetMode;
|
||||
result: SheetResult;
|
||||
sel: { r: string; c: string } | null;
|
||||
/** 도면 · 산출근거 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */
|
||||
/** 도면 · 면적산출 · 재료산출 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */
|
||||
drawingLabel?: (colId: string) => string;
|
||||
basisLabel?: (colId: string) => string;
|
||||
areaLabel?: (colId: string) => string;
|
||||
materialLabel?: (colId: string) => string;
|
||||
/** 조회 후 선택 줄(`PICK_ROWS`)을 고름으로 받나 — 칸 겉모양만. */
|
||||
pick?: boolean;
|
||||
}
|
||||
|
||||
/** 도면 · 산출근거 줄 칸 글. */
|
||||
/** 도면 · 면적산출 · 재료산출 줄 칸 글. */
|
||||
export const refText = (state: RenderState, key: string, colId: string): string =>
|
||||
(key === KEY_BASIS ? state.basisLabel : state.drawingLabel)?.(colId)?.trim() ||
|
||||
st("Drawing_None");
|
||||
(key === KEY_AREA
|
||||
? state.areaLabel
|
||||
: key === KEY_MATERIAL
|
||||
? state.materialLabel
|
||||
: state.drawingLabel)?.(colId)?.trim() || st("Drawing_None");
|
||||
|
||||
/** 열 폭 — 저장 폭(없으면 기본) · 머리 글이 안 잘리는 최소 폭보다 좁지 않게. */
|
||||
const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map<string, number>): number =>
|
||||
@@ -63,9 +68,9 @@ function headFont(): string {
|
||||
return `600 ${rem * 0.82}px ${getComputedStyle(document.body).fontFamily}`;
|
||||
}
|
||||
|
||||
/** 칸을 고칠 수 있나 — 줄 열쇠 기준(머리 칸은 따로) · 도면 · 산출근거 줄은 손으로 못 침. */
|
||||
/** 칸을 고칠 수 있나 — 줄 열쇠 기준(머리 칸은 따로) · 번호 글 줄은 손으로 못 침. */
|
||||
export function keyEditable(state: RenderState, key: string, col: SheetColumn): boolean {
|
||||
if (key === KEY_DRAWING || key === KEY_BASIS || isBlankPick(key, col)) return false;
|
||||
if (REF_ROWS.includes(key) || isBlankPick(key, col)) return false;
|
||||
if (key.startsWith("s:")) return state.mode === "master";
|
||||
if (key.startsWith("t:")) return false;
|
||||
const row = state.doc.줄.find((r) => `d:${r.id}` === key);
|
||||
@@ -75,15 +80,11 @@ export function keyEditable(state: RenderState, key: string, col: SheetColumn):
|
||||
const shown = (text: string | undefined): string =>
|
||||
!text || text === "0" ? "" : groupDigits(text);
|
||||
|
||||
/** 열 딱지 — 식이 먼저(계산 열에 바인딩이 붙어 있어도 계산) · 바인딩 = 설계값(펼침이면 값마다 열) · 손 = 손 입력. */
|
||||
function kindOf(col: SheetColumn): { cls: string; label: string } | null {
|
||||
if (isCalcColumn(col)) return { cls: "is-calc", label: st("Kind_Calc") };
|
||||
if (isBoundColumn(col)) {
|
||||
const label = st("Kind_Bound");
|
||||
return { cls: "is-bound", label: col.펼침 ? `${label} · ${st("Kind_Spread")}` : label };
|
||||
}
|
||||
if (col.손) return { cls: "is-hand", label: st("Kind_Hand") };
|
||||
return null;
|
||||
/** 열 겉모양 — 식이 먼저(계산 열에 바인딩이 붙어 있어도 계산) · 바인딩 = 설계값 · 손 = 손 입력. */
|
||||
function kindClass(col: SheetColumn): string | null {
|
||||
if (isCalcColumn(col)) return "is-calc";
|
||||
if (isBoundColumn(col)) return "is-bound";
|
||||
return col.손 ? "is-hand" : null;
|
||||
}
|
||||
|
||||
export function renderSheet(state: RenderState): HTMLElement {
|
||||
@@ -94,7 +95,7 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
const names = columnNames(cols);
|
||||
const font = headFont();
|
||||
const mins = headMinWidths(cols, depth, font);
|
||||
// 줄 머리 칸 폭 — 층 이름 · 단위 · 식 · 들어갈 것 · 일위대가 · 도면 · 산출근거 · 합계 이름이 안 잘리게
|
||||
// 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 재료산출 · 합계 이름이 안 잘리게
|
||||
const GUTTER = Math.min(
|
||||
GUTTER_MAX,
|
||||
Math.max(
|
||||
@@ -102,11 +103,10 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
...[
|
||||
...(doc.층 ?? []),
|
||||
st("Row_Unit"),
|
||||
st("Row_Formula"),
|
||||
st("Row_Desc"),
|
||||
st("Row_Unit_Price"),
|
||||
st("Row_Drawing"),
|
||||
st("Row_Basis"),
|
||||
st("Row_Area"),
|
||||
st("Row_Material"),
|
||||
...(doc.합계줄 ?? []).map((t) => t.이름),
|
||||
].map((label) => Math.ceil(textWidth(label, font)) + 16),
|
||||
),
|
||||
@@ -131,8 +131,8 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
|
||||
const bodyCell = (key: string, col: SheetColumn, j: number, text: string): HTMLElement => {
|
||||
const td = el("td", { text, attrs: { "data-r": key, "data-c": col.id } });
|
||||
const kind = kindOf(col);
|
||||
if (kind) td.classList.add(kind.cls);
|
||||
const kind = kindClass(col);
|
||||
if (kind) td.classList.add(kind);
|
||||
if (col.꼴 !== "글") td.classList.add("is-num");
|
||||
td.classList.add(keyEditable(state, key, col) ? "is-editable" : "is-locked");
|
||||
const why = errors.get(`${key}|${col.id}`);
|
||||
@@ -170,11 +170,10 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
const masterRow = (key: MasterRowKey): HTMLElement => {
|
||||
const label = (
|
||||
{
|
||||
"s:formula": "Row_Formula",
|
||||
"s:desc": "Row_Desc",
|
||||
"s:price": "Row_Unit_Price",
|
||||
"s:drawing": "Row_Drawing",
|
||||
"s:basis": "Row_Basis",
|
||||
"s:area": "Row_Area",
|
||||
"s:material": "Row_Material",
|
||||
} as const
|
||||
)[key];
|
||||
const tr = el("tr", { className: "ui-sheet__meta", children: [gutter("th", st(label))] });
|
||||
@@ -182,39 +181,12 @@ export function renderSheet(state: RenderState): HTMLElement {
|
||||
const blank = isBlankPick(key, col);
|
||||
const text = blank
|
||||
? ""
|
||||
: key === "s:formula"
|
||||
? formulaLabel(col.식 ?? "", names, col.id)
|
||||
: key === "s:desc"
|
||||
? ""
|
||||
: key === KEY_DRAWING || key === KEY_BASIS
|
||||
? refText(state, key, col.id)
|
||||
: (col.일위대가 ?? "");
|
||||
: REF_ROWS.includes(key)
|
||||
? refText(state, key, col.id)
|
||||
: (col.일위대가 ?? "");
|
||||
const td = bodyCell(key, col, j, text);
|
||||
td.classList.remove("is-num");
|
||||
if (state.pick && PICK_ROWS.includes(key) && !blank) td.classList.add("is-pick");
|
||||
if (key === "s:formula" && col.식) {
|
||||
// 같은 종류 열은 짧은 이름 · 풍선은 긴 이름 + 고칠 때 쓰는 id 식
|
||||
td.textContent = "";
|
||||
td.append(el("div", { className: "ui-sheet__clamp", text }));
|
||||
td.title = `${formulaLabel(col.식, names)}\n${col.식}`;
|
||||
}
|
||||
const kind = kindOf(col);
|
||||
if (key === "s:desc") {
|
||||
// 두세 줄로 자름 — 전문은 마우스를 올리면(title)
|
||||
const desc = col.설명 ?? "";
|
||||
td.title = desc;
|
||||
td.append(
|
||||
el("div", {
|
||||
className: "ui-sheet__clamp",
|
||||
children: [
|
||||
...(kind
|
||||
? [el("span", { className: `ui-sheet__chip ${kind.cls}`, text: kind.label })]
|
||||
: []),
|
||||
desc,
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
tr.append(td);
|
||||
});
|
||||
return tr;
|
||||
|
||||
@@ -13,19 +13,13 @@ const TEXT = {
|
||||
Col_Hand: ["손 입력 열", "Manual column"],
|
||||
Col_New: ["새 열", "New column"],
|
||||
Row_Unit: ["단위", "Unit"],
|
||||
Row_Formula: ["식", "Formula"],
|
||||
Row_Desc: ["들어갈 것", "Source"],
|
||||
Row_Unit_Price: ["일위대가", "Unit price"],
|
||||
Row_Drawing: ["도면", "Drawing"],
|
||||
Row_Basis: ["산출근거", "Basis"],
|
||||
Group_Add: ["묶음 더하기", "Add group"],
|
||||
Group_New: ["새 분류", "New group"],
|
||||
Row_Area: ["면적산출", "Area"],
|
||||
Row_Material: ["재료산출", "Material"],
|
||||
Add_Suffix: [" 더하기", " +"],
|
||||
New_Prefix: ["새 ", "New "],
|
||||
Drawing_None: ["없음", "None"],
|
||||
Kind_Bound: ["설계값", "Design"],
|
||||
Kind_Calc: ["계산", "Calc"],
|
||||
Kind_Hand: ["손 입력", "Manual"],
|
||||
Kind_Spread: ["값마다 열", "Per value"],
|
||||
Page: ["쪽", "page"],
|
||||
Head_Hint: [
|
||||
"/ 로 이으면 층으로 나뉨 (예 관공/Φ800/관매설) · 적게 쓰면 남은 층은 위 칸과 합침",
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface SheetDoc {
|
||||
보기?: SheetView;
|
||||
}
|
||||
|
||||
/** 고른 칸 — `row` = 줄 열쇠(`d:<줄id>` · `t:<합계id>` · `s:unit|formula|desc|price|drawing`) ·
|
||||
/** 고른 칸 — `row` = 줄 열쇠(`d:<줄id>` · `t:<합계id>` · `s:unit|price|drawing|area|material`) ·
|
||||
* `col` = 열 차례(0부터 · 없으면 -1) · `colId` = 열 id(없으면 null). */
|
||||
export interface SheetSelection {
|
||||
row: string;
|
||||
|
||||
Reference in New Issue
Block a user