"""171-3 B09 묶음 탭 셋 = 묶음 카드 기본 · [표로 보기] = 표 그대로 · 보기 기억(탭마다). - 일위대가표 · 중기사용료 · 단가산출근거 — 표 줄을 호표마다 묶음(값 · 풍선 · 누름 이동 = 표와 같음) - 기억 = `b_page_state` 두 줄(`b09-group-view` · `b09-group-cols`) · 탭마다 따로 · 모르는 값 = 카드 · 2 - 집계표 넷 · 설계내역서 · B08 = 대상 밖 """ import json 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 COMMON, GROUPS, RAW, TABS, run # noqa: E402 B09 = "B09_Estimation/B09_Estimation_UI_Tabs_" DG = { "id": "DG1", "no": 1, "name": "찰쌓기", "spec": "기계=굴착기 0.6㎥", "spec_raw": "기계=0201-0070", "unit": "m2", "kind": "품셈", "cite": "건설표준품셈 7-1-2", "lines": [ {"kind": "labor", "key": "L00005", "name": "석공", "qty": 0.072, "unit": "인", "price": 258935, "amount": 18643.3, "ref_no": "노무3", "prices": {"labor": 258935, "material": None, "expense": None}, "amounts": {"labor": 18643.3, "material": None, "expense": None}}, {"kind": "sangeun", "key": "SG1", "name": "뒷채움", "qty": 0.5, "unit": "m3", "price": 8328, "amount": 4164, "ref_no": "산근 1호표", "prices": {"labor": 6878, "material": 538, "expense": 912}, "amounts": {"labor": 3439, "material": 269, "expense": 456}}, ], "sum": 22807, "labor": 22082, "material": 269, "expense": 456, "blank": 1, "blank_names": ["모래"], } # fmt: skip MB = { "key": "EQ1", "name": "굴착기", "spec": "0.6㎥", "unit": "hr", "lines": [ {"kind": "expense", "name": "손료", "spec": "", "qty": 1, "unit": "hr", "price": 13041, "amount": 13041, "ref": None}, {"kind": "labor", "name": "운전사", "spec": "", "qty": 1, "unit": "인", "price": 55700, "amount": 55700, "ref": {"key": "LB9"}}, ], "sum": 68741, "labor": 55700, "material": 0, "expense": 13041, } # fmt: skip SG = { "key": "SG:A", "no": 1, "name": "뒷채움", "unit": "m3", "spec": "규격=150", "spec_raw": "규격=150", "head": ["뒷채움 / m3", "산림품셈 13-3-2 적용"], "lines": [ {"kind": "labor", "name": "보통인부", "ref": {"tab": "labor", "key": "LB2"}, "why": None, "labor": 2979.8, "material": 0.0, "expense": 0.0, "text": ["보통인부 : 165,545 * 0.018 = 2,979.8"]}, ], "labor": 2979, "material": 0, "expense": 0, "sum": 2979, "why": None, "foot": "계 : 2,979", } # fmt: skip # 카드 → 판정 거리(머리 · 몸 줄 · 합계 · 표 줄 번호 · 값이 표와 같은가 · 누름 이동) DUMP = """ const dump = (tab, t) => { const cards = tabGroups(tab, t); const first = t.columns[0].key; return { n: t.rows.length, heads: cards.map((c) => [c.no, c.name, c.spec, c.specTip, c.unit, c.sum]), lines: cards.flatMap((c) => c.lines.map((l) => [l.at, l.tag ?? null, l.name, l.qty ?? null, l.sum, l.parts, l.note])), foot: cards.map((c) => [c.foot.at, c.foot.parts, c.foot.sum, c.foot.note, c.foot.tip]), ats: cards.flatMap((c) => [c.at, ...c.lines.map((l) => l.at), c.foot.at]), same: cards.every((c) => c.lines.every((l) => l.sum === (t.rows[l.at].sum ?? null))), links: cards.flatMap((c) => c.lines.map((l) => (l.link ? t.link(l.at, first) : null))), }; }; """ def _tab(files, build, sheets, tab): body = f"\nconsole.log(JSON.stringify(dump('{tab}', {build}({{ sheets: {json.dumps(sheets, ensure_ascii=False)} }}))));" prelude = "" if any("Sangeun" in f for f in files) else RAW # raw 짝 = 산근 파일에 있음 return run([COMMON, TABS, *files, GROUPS], DUMP + body, prelude) def test_일위대가표_카드(): out = _tab([f"{B09}Labor.ts"], "buildDaegaBlocks", {"daega_blocks": [DG]}, "dg") assert out["heads"] == [ ["제 1 호표", "찰쌓기", "기계=굴착기 0.6㎥", "원문번호 기계=0201-0070", "m2", 22807] ] assert out["lines"] == [ [1, None, "건설표준품셈 7-1-2", None, None, [None, None, None], "품셈 인용"], [2, None, "석공", 0.072, 18643.3, [18643.3, None, None], "노무3"], [3, None, "뒷채움", 0.5, 4164, [3439, 269, 456], "산근 1호표"], ] assert out["foot"] == [[4, [22082, 269, 456], 22807, "빈 줄 1", "빈 줄 — 모래"]] assert out["ats"] == list(range(out["n"])) and out["same"] is True assert out["links"] == [None, {"tab": "labor", "key": "L00005"}, {"tab": "sg", "key": "SG1"}] def test_중기사용료_카드(): out = _tab([f"{B09}Base.ts"], "buildMachineBlocks", {"machine_blocks": [MB]}, "machine_blocks") assert out["heads"] == [["제 1 호표", "굴착기", "0.6㎥", None, "hr", 68741]] assert out["lines"] == [ [1, None, "손료", 1, 13041, [None, None, 13041], ""], [2, None, "운전사", 1, 55700, [55700, None, None], ""], ] assert out["foot"] == [[3, [55700, 0, 13041], 68741, "", None]] assert out["ats"] == list(range(out["n"])) and out["same"] is True assert out["links"] == [None, {"tab": "basis", "key": "LB9"}] def test_단가산출근거_카드(): out = _tab([f"{B09}Sangeun.ts"], "buildSg", {"sg": [SG]}, "sg") assert out["heads"] == [["제 1 호표", "뒷채움", "규격=150", None, "m3", 2979]] assert out["lines"] == [ [1, "", "산림품셈 13-3-2 적용", None, None, [None, None, None], ""], [2, "노무", "보통인부 : 165,545 * 0.018 = 2,979.8", None, 2979.8, [2979.8, 0, 0], ""], ] assert out["foot"][0][:3] == [3, [2979, 0, 0], 2979] assert out["ats"] == list(range(out["n"])) and out["same"] is True assert out["links"] == [None, {"tab": "labor", "key": "LB2"}] def test_보기_한_값_열_수_탭마다(): out = run( [COMMON, TABS, GROUPS], "\nconst a = [readGroupView(), readGroupCols('dg')];" "\nkept.set('b09-group-view', JSON.stringify({ machine_blocks: 'table' }));" # 옛 탭별 값 "\nrememberGroup('b09-group-cols', 'sg', 3);" "\nconst b = [groupView(), readGroupView(), readGroupView()," " readGroupCols('sg'), readGroupCols('dg'), readGroupCols('dg', { dg: 9 })];" "\nconsole.log(JSON.stringify({ a, b, kept: Object.fromEntries(kept)," " tabs: GROUP_TABS, other: ['bill', 'sum_material', 'sum_labor', 'sum_expense'," " 'sum_machine', 'dg_list'].map(isGroupTab) }));", ) assert out["a"] == ["cards", 2] # 기본 = 카드 · 가로 2 assert out["b"] == ["table", "table", "table", 3, 2, 2] # 304-1 셋 탭 한 값 assert json.loads(out["kept"]["b09-group-view"]) == "table" # 첫 읽기에 옛 값 → 한 값 assert json.loads(out["kept"]["b09-group-cols"]) == {"sg": 3} assert out["tabs"] == ["dg", "machine_blocks", "sg"] assert out["other"] == [False] * 6 # 집계표 넷 · 설계내역서 · 목록표 = 대상 밖 def test_화면_보기_고름_누름_이동(): out = run( [COMMON, TABS, f"{B09}Labor.ts", GROUPS], f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});" "\nconst gone = []; let redraws = 0; const focused = []; const calls = [];" "\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: (l) => gone.push(l)," " redraw: () => redraws++, setTools: (t) => calls.push(t), makeTable: () => ({ root: document.createElement('table')," " focusRow: (i) => focused.push(i) }) };" "\nconst [view] = renderGroupPanel(o);" "\nconst label = calls.flat().filter((e) => e.dataset.b09GroupView).length;" "\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');" "\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');" "\nview.querySelector('[data-at=\"2\"]').click();" "\nview.querySelector('[data-at=\"0\"]').click();" "\nconst before = calls.length; chooseGroupView('table');" "\nconst [view2] = renderGroupPanel(o);" "\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws," " tools2: calls.length - before, tag2: view2.tagName, focused," " kept: Object.fromEntries(kept) }));", RAW, ) assert out["label"] == 0 and out["table"] == "dg" # 기본 = 카드 · 304-1 탭 줄 보기 단추 없음 assert out["gone"] == [{"tab": "labor", "key": "L00005"}, None] # 줄 누름 = 표와 같은 이동 assert out["lit"] is False # 248-1 이동해 온 줄 = 빨강 하나(줄 강조 안 붙임) assert out["box"] is True # 그 호표 박스 = 붉은 외곽선(181-2) assert out["redraws"] == 0 assert out["tools2"] == 0 and out["tag2"] == "TABLE" and out["focused"] == [3] assert json.loads(out["kept"]["b09-group-view"]) == "table" def test_잇기(): shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") assert ( "if (isGroupTab(tab)) {" in shell and "renderGroupPanel({" in shell and "setTools: bars.setTools" in shell ) state = (ROOT / "A00_Common" / "b_page_state.ts").read_text("utf-8") assert '"b09-group-view": { bucket: "pref", scope: "global" },' in state assert '"b09-group-cols": { bucket: "pref", scope: "global" },' in state b08 = "".join(p.read_text("utf-8") for p in (ROOT / "B08_Quantity").glob("*.ts")) assert "createGroupCards" not in b08 # B08 = 대상 밖