diff --git a/B09_Estimation/B09_Estimation_UI_Page.css b/B09_Estimation/B09_Estimation_UI_Page.css index 9e41af3dc..86cf8fc3c 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.css +++ b/B09_Estimation/B09_Estimation_UI_Page.css @@ -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; diff --git a/B09_Estimation/B09_Estimation_UI_Shell.ts b/B09_Estimation/B09_Estimation_UI_Shell.ts index c3c67a6c6..bd47dacfb 100644 --- a/B09_Estimation/B09_Estimation_UI_Shell.ts +++ b/B09_Estimation/B09_Estimation_UI_Shell.ts @@ -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)); diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts index 2d0b40f52..3ff2cab15 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts @@ -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 }; } diff --git a/resources/tester/test_200_2_b09_bill_level_bar.py b/resources/tester/test_200_2_b09_bill_level_bar.py index f9d610b6c..c37ad1136 100644 --- a/resources/tester/test_200_2_b09_bill_level_bar.py +++ b/resources/tester/test_200_2_b09_bill_level_bar.py @@ -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 diff --git a/resources/tester/test_202_1_table_hint_bottom.py b/resources/tester/test_202_1_table_hint_bottom.py index a4db1829a..35bacaafc 100644 --- a/resources/tester/test_202_1_table_hint_bottom.py +++ b/resources/tester/test_202_1_table_hint_bottom.py @@ -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 diff --git a/resources/tester/test_204_1_b09_bill_drag.py b/resources/tester/test_204_1_b09_bill_drag.py index 20367728b..185ffb4cf 100644 --- a/resources/tester/test_204_1_b09_bill_drag.py +++ b/resources/tester/test_204_1_b09_bill_drag.py @@ -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)); """ diff --git a/resources/tester/test_229_1_b09_level_buttons_subbar.py b/resources/tester/test_229_1_b09_level_buttons_subbar.py new file mode 100644 index 000000000..2d40334c7 --- /dev/null +++ b/resources/tester/test_229_1_b09_level_buttons_subbar.py @@ -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)