Files
Aislo/resources/tester/test_171_3_b09_group_tabs.py
T
eomsangdonandClaude Opus 5.5 40ed04b15f feat(ui): 묶음 카드 공용 양식 · B09 묶음 탭 셋 카드 기본(171-2 · 171-3)
- 171-2 새 공용 ui_template_group_cards(.ts · .css) — 머리(호표 · 명칭 · 규격 · 단위 · 합계) · 몸 줄(이름 · 규격 · 수량 · 단위 · 합계 · 비고) · 합계 줄(노무 · 재료 · 경비 · 계)
- 글자 14px 한 벌 · 줄 간격 좁게(공용 카드 16px 끼움 없음) · 숫자 오른쪽 · 줄 금액 = 합계만 + 비목 셋 풍선
- 가로 열 수 1~4(기본 2) · 묶음 12 넘으면 요약 보기(머리 · 합계만 · 머리 누름 = 펼침 · 모두 펼침 · 접기)
- 157-2 산근 카드 파일 갈음(지움)
- 171-3 일위대가표 · 중기사용료 · 단가산출근거 = 카드 기본 · [표로 보기] = 표 그대로 · 보기 · 열 수 기억(b_page_state 두 줄 · 탭마다)
- 값 · 풍선 · 칸 누름 이동 = 표 줄 그대로 · 집계표 · 설계내역서 · B08 대상 밖
- 시험 test_171_2 · test_171_3 · 공용 도우미(가짜 DOM) · test_157_2 갈음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
2026-10-10 08:46:56 +09:00

177 lines
9.0 KiB
Python

"""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('dg'), readGroupCols('dg')];"
"\nrememberGroup('b09-group-view', 'dg', 'table');"
"\nrememberGroup('b09-group-cols', 'sg', 3);"
"\nconst b = [readGroupView('dg'), readGroupView('sg'), readGroupView('machine_blocks'),"
" 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", "cards", "cards", 3, 2, 2]
assert json.loads(out["kept"]["b09-group-view"]) == {"dg": "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 = [];"
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: (l) => gone.push(l),"
" redraw: () => redraws++, makeTable: () => ({ root: document.createElement('table'),"
" focusRow: (i) => focused.push(i) }) };"
"\nconst [bar, view] = renderGroupPanel(o);"
"\nconst label = bar.children[0].textContent;"
"\nview.querySelector('[data-at=\"2\"]').click();"
"\nview.querySelector('[data-at=\"0\"]').click();"
"\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');"
"\nbar.children[0].click();"
"\nconst [bar2, view2] = renderGroupPanel(o);"
"\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, redraws,"
" label2: bar2.children[0].textContent, tag2: view2.tagName, focused,"
" kept: Object.fromEntries(kept) }));",
RAW,
)
assert out["label"] == "표로 보기" and out["table"] == "dg" # 기본 = 카드
assert out["gone"] == [{"tab": "labor", "key": "L00005"}, None] # 줄 누름 = 표와 같은 이동
assert out["lit"] is True # 이동해 온 줄(키 DG1|1 = 표 3번째 줄) 강조
assert out["redraws"] == 1
assert out["label2"] == "호표별 보기" and out["tag2"] == "TABLE" and out["focused"] == [3]
assert json.loads(out["kept"]["b09-group-view"]) == {"dg": "table"}
def test_잇기():
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "if (isGroupTab(tab)) {" in shell and "renderGroupPanel({ tab, spec," 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 = 대상 밖