feat(B09): 설계내역서 레벨 단추 보조 탭 줄로 이동 · 표 위 도구 줄 비움(229-1)

This commit is contained in:
2026-10-10 17:42:26 +09:00
parent dc027cd738
commit 315accf9ca
7 changed files with 126 additions and 33 deletions
-12
View File
@@ -81,18 +81,6 @@
text-align: left;
}
/* 200-2 레벨 도구 줄 — 단추 넷 + 안내 한 줄 */
.b09-estimation__level {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-8);
}
.b09-estimation__level .b09-estimation__hint {
margin: 0;
}
/* 181-2 표 보기 — 목록표에서 고른 호표 줄 구간 전체 붉은 외곽선(머리 accent 밑줄은 덮음) */
.b09-estimation .ui-data-table__table tbody tr.is-pick td {
--pick-t: inset 0 0 0 0 transparent;
+3 -2
View File
@@ -260,11 +260,12 @@ export function renderB09Estimation(root: HTMLElement): void {
}),
);
}
// 200-2 설계내역서 = 표 위 레벨 도구 줄 · 고른 줄 강조 · 202-1 안내 줄 = 표 아래
// 200-2 설계내역서 = 고른 줄 강조 · 229-1 레벨 단추 = 보조 탭 줄 오른쪽 · 202-1 안내 줄 = 표 아래
const bill = (spec as BillTable).menu ? (spec as BillTable) : null;
const table = makeTable(bill ? (i) => pickedClass(bill.rows[i]) : undefined);
const level = bill ? levelBar(table.root, bill) : null;
panel.replaceChildren(...(level ? [level] : []), table.root, ...(hint ? [hint] : []));
if (level) bars.setTools(level.tools);
panel.replaceChildren(table.root, ...(level ? [level.guide] : []), ...(hint ? [hint] : []));
if (focus !== undefined) {
const index = rowIndex(spec.rows, focus);
if (index >= 0) requestAnimationFrame(() => table.focusRow(index));
@@ -1,7 +1,9 @@
/* =============================================================================
* B09_Estimation_UI_Tabs_Bill_Level.ts
* B09 설계내역서 레벨 도구 줄(PLAN 200-2) — 표 위 [→ 들여쓰기] [← 내어쓰기] [↑ 위로] [↓ 아래로] · 키 · 안내 한 줄.
* B09 설계내역서 레벨 단추(PLAN 200-2 · 229-1) — 보조 탭 줄 오른쪽 [→ 들여쓰기] [← 내어쓰기] [↑ 위로] [↓ 아래로] ·
* 키 · 안내 한 줄(표 아래).
*
* 단추 = 보조 탭 줄 도구 자리(`…_UI_TabBar` setTools) · 안내 글 = 표 아래(202-1)
* 동작 = 줄 우클릭 메뉴(`…_UI_Tabs_Bill_Edit` billMenu)의 같은 항목을 부름 — 메뉴에 없으면 단추 흐림 · 키 안 먹음
* 고른 줄 = 표 줄 누름(키로 기억 · 다시 그려도 남음) · 표시 = 줄 class is-focus(Shell 이 줄 class 곁값으로)
* 키 = Tab 들여쓰기 · Shift+Tab 내어쓰기 · Alt+↑↓ 옮기기 · Esc 편집 끝(편집 아니면 고름 풂)
@@ -38,7 +40,7 @@ let picked: string | null = null;
/** 지금 그린 도구 줄 — 키가 부름 */
let live: {
root: HTMLElement;
bar: HTMLElement;
tools: HTMLElement[];
run: (label: string) => void;
sync: () => void;
} | null = null;
@@ -78,7 +80,13 @@ function listenKeys(): void {
document.addEventListener("keydown", (event) => {
if (!live?.root.isConnected || picked === null) return;
const at = document.activeElement;
if (at && at !== document.body && !live.root.contains(at) && !live.bar.contains(at)) return;
if (
at &&
at !== document.body &&
!live.root.contains(at) &&
!live.tools.includes(at as HTMLElement)
)
return;
// Esc = 편집 끝(표에 초점 · 키 받음) · 편집 아닐 때 = 고름 풂
if (event.key === "Escape") {
if (!editing(at)) {
@@ -95,8 +103,11 @@ function listenKeys(): void {
});
}
/** 표 위 레벨 도구 줄 — 표 root 에 줄 누름(고름)을 붙임 */
export function levelBar(root: HTMLElement, table: BillTable): HTMLElement {
/** 레벨 단추 넷(보조 탭 줄로) · 안내 글(표 아래로) — 표 root 에 줄 누름(고름)을 붙임 */
export function levelBar(
root: HTMLElement,
table: BillTable,
): { tools: HTMLElement[]; guide: HTMLElement } {
const index = (): number =>
picked === null ? -1 : table.rows.findIndex((r) => String(r.key) === picked);
const run = (label: string): void => menuActs(table, index()).get(label)?.();
@@ -110,13 +121,10 @@ export function levelBar(root: HTMLElement, table: BillTable): HTMLElement {
button.dataset.b09Level = label;
return button;
});
const guide = document.createElement("span");
guide.className = "b09-estimation__hint";
const guide = document.createElement("p");
guide.className = "ui-general-block__empty";
guide.textContent = GUIDE;
attachHoverInfo(guide, GUIDE_MORE);
const bar = document.createElement("div");
bar.className = "b09-estimation__level";
bar.append(...buttons, guide);
const sync = (): void => {
const acts = menuActs(table, index());
ACTS.forEach(([, label], k) => (buttons[k].disabled = !acts.has(label)));
@@ -132,7 +140,7 @@ export function levelBar(root: HTMLElement, table: BillTable): HTMLElement {
});
attachDrag(root, table, (key) => (picked = key));
sync();
live = { root, bar, run, sync };
live = { root, tools: buttons, run, sync };
listenKeys();
return bar;
return { tools: buttons, guide };
}
@@ -68,7 +68,7 @@ const draw = () => {
const at = (key) => table.rows.findIndex((r) => r.key === key);
const tr = (key) => root.kids[at(key)];
const pick = (key) => root.fire("click", { target: tr(key).kids[0] });
const buttons = () => bar.kids.filter((k) => k.tagName === "BUTTON");
const buttons = () => bar.tools;
const off = () => buttons().map((b) => b.disabled);
const order = () => (user.bill?.rows ?? []).map((e) => [e.key, e.parent ?? null]);
const key = (k, more = {}) => {
@@ -79,7 +79,7 @@ const key = (k, more = {}) => {
const out = {};
draw();
out.labels = buttons().map((b) => [b.label, b.dataset.b09Level, b.variant]);
out.guide = bar.kids.at(-1).textContent;
out.guide = bar.guide.textContent;
out.hover = hovers.length;
out.none = off();
pick("earth:4");
@@ -189,12 +189,14 @@ def test_왼쪽_맞춤_레벨_들여쓰기_글꼴_공용_표():
re.escape(f'{bill} .ui-data-table__input:not([data-k="qty"]) {{') + r"\s*text-align: left;",
css,
)
assert re.search(r"\.b09-estimation__level \{\s*display: flex;", css)
assert ".b09-estimation__level" not in css # 229-1 표 위 도구 줄 없앰
def test_셸_배선():
src = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "makeTable(bill ? (i) => pickedClass(bill.rows[i]) : undefined)" in src
assert "...(level ? [level] : []), table.root" in src
assert "if (level) bars.setTools(level.tools);" in src # 229-1 단추 = 보조 탭 줄
assert "panel.replaceChildren(table.root, ...(level ? [level.guide] : [])" in src
lvl = (B09 / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8")
assert "table.menu(index)" in lvl # 단추 · 키 = 우클릭 메뉴 같은 함수
assert "b09-estimation__level" not in lvl
@@ -105,9 +105,9 @@ def test_묶음_탭_단추가_보조_줄에_안내는_아래에():
def test_잇기_쉘_안내_줄_아래_도구_비움():
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert (
"panel.replaceChildren(...(level ? [level] : []), table.root, ...(hint ? [hint] : []))"
"panel.replaceChildren(table.root, ...(level ? [level.guide] : []), ...(hint ? [hint] : []))"
in shell
) # 200-2 레벨 줄 · 203-1 연결 점검 탭 없앰과 합침
) # 229-1 레벨 단추 = 보조 탭 줄 · 안내 글 = 표 아래 · 안내 줄 = 맨 아래
assert "...(hint ? [hint] : []), table.root" not in shell # 위 자리 없음
assert re.search(r"bars\.draw\(tab\);\s*bars\.setTools\(\[\]\);", shell)
assert "setTools: bars.setTools" in shell
+1 -1
View File
@@ -57,7 +57,7 @@ out.altLeft = press("earth:4", "ArrowLeft", { altKey: true });
user = {}; draw(); pick("earth:5");
doc.activeElement = tr("earth:5").kids[0].kids[0];
out.editing = [key("ArrowRight"), key("ArrowUp"), order()];
out.guide = bar.kids.at(-1).textContent;
out.guide = bar.guide.textContent;
console.log(JSON.stringify(out));
"""
@@ -0,0 +1,94 @@
"""229-1 B09 설계내역서 레벨 단추 = 보조 탭 줄 오른쪽 — 단추 넷 · 다른 탭엔 없음 · 표 위 도구 줄 없음.
- 단추 = 보조 탭 줄 도구 자리(`setTools`) · 안내 글 = 표 아래 · 못 하는 동작 = 흐림(200-2 그대로)
- 쉘 배선 = 소스 글 대조(test_200_2 · test_202_1 과 같은 꼴)
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from resources.tester.helper_b09_group_cards import TABS, run # noqa: E402
B09 = "B09_Estimation/B09_Estimation_UI_"
FILES = [TABS, f"{B09}TabBar.ts", f"{B09}Tabs_Bill_Level.ts"]
# 가짜 DOM 에 줄 찾기(closest · querySelector(All))만 더함 · 끌기 = 빈 함수
PRELUDE = r"""
document.addEventListener = () => {};
const attachDrag = () => {};
E.prototype.closest = function () { for (let e = this; e; e = e.parent) if (e.tagName === "TR") return e; return null; };
E.prototype.querySelectorAll = function () { return this.all("TR").filter((t) => t.cls.has("is-focus")); };
E.prototype.querySelector = function (s) {
const i = s.match(/data-i="(-?\d+)"/)?.[1];
return this.children.find((c) => c.dataset.i === i) ?? null;
};
"""
def test_설계내역서_보조_줄에_단추_넷_다른_탭엔_없음():
out = run(
FILES,
"""
const bars = createTabBars(() => {});
const items = ["들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"].map((t) => [t, () => {}]);
const table = { rows: [{ key: "r1" }], menu: (i) => (i === 0 ? items.slice(1, 3) : []) };
const root = document.createElement("div");
const tr = document.createElement("tr");
tr.dataset.i = "0";
root.append(tr);
const toolsOf = () => bars.sub.children[bars.sub.children.length - 1];
const snap = (lv) => ({
labels: toolsOf().children.map((b) => b.textContent),
levels: toolsOf().children.map((b) => b.dataset.b09Level ?? null),
guideIn: lv ? lv.guide.textContent.length > 0 : null,
});
const show = (tab) => {
bars.draw(tab);
bars.setTools([]);
if (tab !== "bill") return null;
const lv = levelBar(root, table);
bars.setTools(lv.tools);
return lv;
};
const bill = show("bill");
const a = snap(bill);
const disabledBefore = bill.tools.map((b) => b.disabled);
tr.click();
const disabledAfter = bill.tools.map((b) => b.disabled);
show("dg");
const b = snap(null);
show("bill");
const c = snap(null);
console.log(JSON.stringify({
a, disabledBefore, disabledAfter, b: b.labels, c: c.labels,
guideTag: bill.guide.tagName, guideCls: bill.guide.className, subBlank: bars.sub.classList.contains("b09-estimation__tabs--blank"),
}));
""",
PRELUDE,
)
names = ["→ 들여쓰기", "← 내어쓰기", "↑ 위로", "↓ 아래로"]
assert out["a"]["labels"] == names # 설계내역서 = 단추 넷(보조 줄 오른쪽 도구 자리)
assert out["a"]["levels"] == ["들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"]
assert out["a"]["guideIn"] is True # 안내 글 = 단추 줄이 아니라 따로(표 아래로)
assert out["guideTag"] == "P" and "ui-general-block__empty" in out["guideCls"]
assert out["subBlank"] # 하위 탭 없는 묶음 = 빈 보조 줄(숨은 단추 + 도구)
assert out["disabledBefore"] == [True] * 4 # 고른 줄 없음 = 흐림
assert out["disabledAfter"] == [True, False, False, True] # 줄 누름 = 메뉴 항목대로
assert out["b"] == [] # 다른 탭(일위대가표) = 단추 없음
assert out["c"] == names # 설계내역서로 돌아오면 다시 있음
def test_표_위_도구_줄_없음_쉘_배선():
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "if (level) bars.setTools(level.tools);" in shell
assert "panel.replaceChildren(table.root, ...(level ? [level.guide] : [])" in shell
assert "...(level ? [level] : [])" not in shell # 표 위 자리 없음
css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
lvl = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8")
assert "b09-estimation__level" not in css and "b09-estimation__level" not in lvl
# 키 · 단추 = 우클릭 메뉴 같은 항목 · 키 안 바뀜(Tab · Shift+Tab · Alt+↑↓ · 방향키)
assert re.search(r"ACTS\[e\.shiftKey \? 1 : 0\]\[1\]", lvl)