Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-10-13 08:05:54 +09:00
19 changed files with 880 additions and 94 deletions
@@ -62,20 +62,19 @@ def test_157_2_두_보기_같은_값():
def test_157_2_단추_바꿈():
out = _node(
"\nconsole.log(JSON.stringify([viewLabel('table'), viewLabel('cards'),"
" flipView('table'), flipView(flipView('table'))]));"
)
assert out == ["박스보기", "표로 보기", "cards", "table"]
out = _node("\nconsole.log(JSON.stringify([viewLabel('table'), viewLabel('cards')]));")
assert out == ["호표보기", "박스보기"] # 304-1 단추 글 = 그 보기 이름
def test_157_2_기억_실패_때_카드():
out = _node(
"\nconst a = [readGroupView('sg', null), readGroupView('sg', { sg: 'x' }),"
" readGroupView('sg', { dg: 'table' }), readGroupView('sg', { sg: 'table' })];"
"\nconst a = [readGroupView(null), readGroupView({ sg: 'x' }), readGroupView({ dg: 'table' }),"
" readGroupView({ sg: 'table' }), readGroupView('table'), readGroupView('x'),"
" readGroupView({ dg: 'cards', sg: 'table' })];"
"\nconsole.log(JSON.stringify(a));"
)
assert out == ["cards", "cards", "cards", "table"]
# 304-1 한 값 · 옛 탭별 값 = dg 먼저 · 없으면 다른 탭 · 모르는 값 = 카드
assert out == ["cards", "cards", "table", "table", "table", "cards", "cards"]
def test_157_2_화면_잇기():
+14 -15
View File
@@ -118,27 +118,27 @@ def test_단가산출근거_카드():
assert out["links"] == [None, {"tab": "labor", "key": "LB2"}]
def test_보기_기억_탭마다():
def test_보기_한_값_열_수_탭마다():
out = run(
[COMMON, TABS, GROUPS],
"\nconst a = [readGroupView('dg'), readGroupCols('dg')];"
"\nrememberGroup('b09-group-view', 'dg', 'table');"
"\nconst a = [readGroupView(), readGroupCols('dg')];"
"\nkept.set('b09-group-view', JSON.stringify({ machine_blocks: 'table' }));" # 옛 탭별 값
"\nrememberGroup('b09-group-cols', 'sg', 3);"
"\nconst b = [readGroupView('dg'), readGroupView('sg'), readGroupView('machine_blocks'),"
"\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", "cards", "cards", 3, 2, 2]
assert json.loads(out["kept"]["b09-group-view"]) == {"dg": "table"}
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_화면_보기_바꿈_누름_이동():
def test_화면_보기_고름_누름_이동():
out = run(
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
@@ -147,26 +147,25 @@ def test_화면_보기_바꿈_누름_이동():
" redraw: () => redraws++, setTools: (t) => calls.push(t), makeTable: () => ({ root: document.createElement('table'),"
" focusRow: (i) => focused.push(i) }) };"
"\nconst [view] = renderGroupPanel(o);"
"\nconst btn = calls[0].find((e) => e.dataset.b09GroupView);"
"\nconst label = btn.textContent;"
"\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();"
"\nbtn.click();"
"\nconst before = calls.length; chooseGroupView('table');"
"\nconst [view2] = renderGroupPanel(o);"
"\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws,"
" label2: calls[1].find((e) => e.dataset.b09GroupView).textContent, tag2: view2.tagName, focused,"
" tools2: calls.length - before, tag2: view2.tagName, focused,"
" kept: Object.fromEntries(kept) }));",
RAW,
)
assert out["label"] == "표로 보기" and out["table"] == "dg" # 기본 = 카드
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"] == 1
assert out["label2"] == "박스보기" and out["tag2"] == "TABLE" and out["focused"] == [3]
assert json.loads(out["kept"]["b09-group-view"]) == {"dg": "table"}
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_잇기():
+3 -3
View File
@@ -1,6 +1,6 @@
"""181-2 박스보기 — 이름 · 접기 없음 · 목록표에서 고른 호표 = 박스 붉은 외곽선 · 표 보기 = 줄 구간 전체.
- 단추 글 「박스보기」 ↔ 「표로 보기」 · 요약(접기) 보기 지움
- 단추 글 「박스보기」 · 「호표보기」(304-1 좌측 스위치) · 요약(접기) 보기 지움
- 표 보기 = 고른 호표 줄 구간(키 `|` 앞머리 같은 줄) 모두 `is-pick` · 첫 줄 `is-pick-top` · 끝 줄 `is-pick-end`
- 붉은 색 = `--color-danger`(B06 횡단 카드 고름 꼴)
"""
@@ -52,7 +52,7 @@ def test_표_보기_줄_class_넘김():
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {}, setTools: () => {},"
" makeTable: (e) => { extra = e; return { root: document.createElement('table'),"
" focusRow: () => {} }; } };"
"\nchosen.set('dg', 'table');"
"\nchooseGroupView('table');"
"\nrenderGroupPanel(o);"
"\nconsole.log(JSON.stringify(spec.rows.map((_, i) => extra(i))));",
)
@@ -63,7 +63,7 @@ def test_표_보기_줄_class_넘김():
def test_글_접기_없음_CSS_잇기():
groups = (ROOT / GROUPS).read_text("utf-8")
assert '"표로 보기" : "박스보기"' in groups and "호표별 보기" not in groups
assert '"박스보기" : "호표보기"' in groups and "호표별 보기" not in groups # 304-1 글
common = (ROOT / COMMON).read_text("utf-8")
for gone in ("isSummary", "GROUP_SUMMARY_OVER", "is-closed", "모두 펼침", "모두 접기"):
assert gone not in common
+1 -1
View File
@@ -286,4 +286,4 @@ def test_내역_초기화_배선():
assert "showConfirmDialog(" in block and "if (!ok) return;" in block
assert "const { bill: _bill, ...rest } = user;" in block
assert 'run("/estimation", "PUT"' in block and "{ user: rest }" in block
assert "actions.append(save, reset);" in src
assert "createDock([save, reset]," in src # 303-1 하단 고정 줄 맨 아래 줄
+8 -8
View File
@@ -1,6 +1,6 @@
"""185-2 B09 묶음 탭 보기 바꿈 단추 글 한 줄 · 가로 열 수 단추와 같은 줄.
"""185-2 B09 묶음 탭 가로 열 수 단추 한 줄 · 보기 고름 = 좌측 하단(304-1 · 탭 줄에 없음).
- 보기 바꿈 단추 · 가로 열 수 = 도구 묶음(`setTools` · 202-1 로 페이지 보조 탭 줄 오른쪽) · 카드 판 머리 띠 = 묶음 수만
- 가로 열 수 = 도구 묶음(`setTools` · 202-1 로 페이지 보조 탭 줄 오른쪽) · 카드 판 머리 띠 = 묶음 수만
- 단추 · 라벨 = `white-space: nowrap` · 도구 묶음 = 줄바꿈 없음(`flex-wrap: nowrap`)
"""
@@ -31,7 +31,7 @@ def _panel(view: str, hint: str):
return run(
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
f"\nchosen.set('dg', '{view}');"
f"\nchooseGroupView('{view}');"
"\nconst tools = [];"
f"\nconst o = {{ tab: 'dg', spec, hint: {hint}, goto: () => {{}}, redraw: () => {{}},"
" setTools: (t) => tools.push(...t),"
@@ -39,7 +39,7 @@ def _panel(view: str, hint: str):
"\nconst out = renderGroupPanel(o);"
"\nconst btn = tools.find((e) => e.dataset.b09GroupView);"
"\nconst bar = out[0].all('.ui-group-cards__bar')[0] ?? null;"
"\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn.textContent,"
"\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn ? btn.textContent : null, nTools: tools.length,"
" cols: tools.flatMap((e) => e.all('[data-group-cols]')).map((b) => b.dataset.groupCols),"
" barBtn: bar ? bar.all('[data-b09-group-view]').length : null,"
" barCols: bar ? bar.all('[data-group-cols]').length : null }));",
@@ -49,7 +49,7 @@ def _panel(view: str, hint: str):
def test_카드_보기_단추가_열수_단추와_같은_도구_묶음에():
out = _panel("cards", "null")
assert out["label"] == "표로 보기"
assert out["label"] is None # 304-1 보기 단추 = 좌측 하단(탭 줄에 없음)
assert out["cols"] == ["1", "2", "3", "4"] # 같은 묶음에 열 수 단추 넷
assert out["barBtn"] == 0 and out["barCols"] == 0 # 카드 판 머리 띠에는 없음
assert out["n"] == 1 # 안내 글 없으면 카드 판 하나뿐
@@ -63,8 +63,8 @@ def test_카드_보기_안내_글은_카드_판_아래에():
def test_표_보기_단추는_도구_묶음에():
out = _panel("table", "null")
assert out["label"] == "박스보기" and out["n"] == 1
assert out["cols"] == [] # 표 보기 = 열 수 단추 없음
assert out["label"] is None and out["n"] == 1
assert out["cols"] == [] and out["nTools"] == 0 # 표 보기 = 도구 없음
def test_CSS_한_줄_nowrap():
@@ -81,4 +81,4 @@ def test_CSS_한_줄_nowrap():
page, ".b09-estimation__tools .ui-btn,\n.b09-estimation__tools .ui-group-cards__label"
)
assert "white-space: nowrap" in tools
assert "actions: [toggle]" in (ROOT / GROUPS).read_text("utf-8")
assert "toggle" not in (ROOT / GROUPS).read_text("utf-8") # 304-1 탭 줄 토글 걷음
@@ -36,7 +36,7 @@ def _panel(view: str, hint: str):
return run(
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
f"\nchosen.set('dg', '{view}');"
f"\nchooseGroupView('{view}');"
f"\nconst hint = {hint};"
"\nconst o = { tab: 'dg', spec, hint, goto: () => {}, redraw: () => {}, setTools: () => {},"
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };"
@@ -80,7 +80,7 @@ def test_묶음_탭_단추가_보조_줄에_안내는_아래에():
"\nconst hint = document.createElement('p');"
"\nconst res = {};"
"\nfor (const view of ['cards', 'table']) {"
"\n chosen.set('dg', view);"
"\n chooseGroupView(view);"
"\n bars.draw('dg'); bars.setTools([]);"
"\n const panel = renderGroupPanel({ tab: 'dg', spec, hint, goto: () => {}, redraw: () => {},"
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }),"
@@ -95,8 +95,9 @@ def test_묶음_탭_단추가_보조_줄에_안내는_아래에():
"\nconsole.log(JSON.stringify(res));",
RAW,
)
assert out["cards"]["inRow"] == ["표로 보기"] and out["cards"]["cols"] == 4
assert out["table"]["inRow"] == ["박스보기"] and out["table"]["cols"] == 0
# 304-1 보기 고름 = 좌측 하단 스위치(보조 줄에 없음) · 가로 열 수 = 보조 줄 그대로
assert out["cards"]["inRow"] == [] and out["cards"]["cols"] == 4
assert out["table"]["inRow"] == [] and out["table"]["cols"] == 0
for view in ("cards", "table"):
assert out[view]["inPanel"] == 0 # 판 안에는 단추 없음
assert out[view]["hintLast"] and out[view]["n"] == 2 # 판 + 안내 줄(맨 아래)
@@ -112,8 +112,9 @@ def test_B09_표_만드는_길_한_곳_더블클릭_기본():
calls = [
p.name for p in _sources("B09_Estimation") if "createDataTable({" in p.read_text("utf-8")
]
assert calls == ["B09_Estimation_UI_Shell.ts"]
shell = (ROOT / "B09_Estimation" / calls[0]).read_text("utf-8")
# 303-2 팝업 창 = 보기 전용(고칠 칸 없음) · 옵션 = 같은 표 짜기(Builders)
assert calls == ["B09_Estimation_UI_Popup.ts", "B09_Estimation_UI_Shell.ts"]
shell = (ROOT / "B09_Estimation" / calls[1]).read_text("utf-8")
assert "...tableOptionsOf(spec, tab)," in shell # 303-2 옵션 짜기 = Builders 한 곳
builders = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Builders.ts").read_text("utf-8")
assert "editOnDouble: spec.editOnDouble ?? true," in builders
@@ -135,5 +136,6 @@ def test_B08_고칠_칸_있는_표_모두_editOnDouble():
def test_고르기_메뉴_이동_그대로():
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "if (spec.link && !spec.editable?.(i, key)) goto(spec.link(i, key));" in shell
assert "if (spec.link && !spec.editable?.(i, key))" in shell # 303-2 누른 줄 = 팝업 자리
assert 'goto(spec.link(i, key), made.root.querySelector(`tr[data-i="${i}"]`));' in shell
assert "spec.choices?.(Number(cell.parentElement?.dataset.i), column.key)" in shell
@@ -60,7 +60,7 @@ def test_표_빨강_하나_누름_키_풂():
" return { root, focusRow: (i, mark) => scrolled.push([i, mark]) }; };"
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {},"
" setTools: () => {}, makeTable };"
"\nchosen.set('dg', 'table');"
"\nchooseGroupView('table');"
"\nconst marks = () => [root.children.map((tr) => tr.className || null),"
" spec.rows.map((_, i) => extra(i))];"
"\nconst out = {};"
@@ -205,6 +205,6 @@ def test_패널_훅_draft_없으면_빈칸():
tabs = (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs.ts").read_text("utf-8")
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
ui = (ROOT / "B09_Estimation/draft/B09_Draft_Schedule_UI.ts").read_text("utf-8")
assert "{ builders: {}, extend: [], panels: [] }" in tabs # draft 없으면 카드 없음
assert "{ builders: {}, extend: [], panels: [], actions: [] }" in tabs # draft 없으면 카드 없음
assert "...DRAFT.panels.map((make) => make(() => user, setUser))" in shell
assert "DRAFT.panels.push(" in ui
+329
View File
@@ -0,0 +1,329 @@
"""303 B09 행 고름 = 팝업 · 하이퍼링크 스위치 · 좌측 하단 줄.
303-1 좌측 하단 고정 줄 — [박스보기|호표보기] · [하이퍼링크|팝업] · `DRAFT.actions`(비면 줄 없음) ·
[?][저장][내역 초기화](도움말 줄) · 스위치 = pref global · 기본 = 팝업
303-2 표 짜기 = `…_UI_Builders`(화면 · 팝업 · 인쇄 같은 옵션)
"""
import json
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 COMMON, GROUPS, TABS, run # noqa: E402
B09 = ROOT / "B09_Estimation"
DOCK = "B09_Estimation/B09_Estimation_UI_Dock.ts"
BUILDERS = "B09_Estimation/B09_Estimation_UI_Builders.ts"
DOCK_JS = (
"\nconst save = document.createElement('button'); save.textContent = '저장';"
"\nconst reset = document.createElement('button'); reset.textContent = '내역 초기화';"
"\nconst shape = (dock) => dock.children.map((r) => ({ sw: r.dataset.b09Switch ?? null,"
" draft: 'b09DraftActions' in r.dataset, help: 'helpRow' in r.dataset,"
" text: r.children.map((b) => b.textContent) }));"
)
def test_하단_줄_차례_초안_단추_없으면_줄_없음():
out = run(
[COMMON, TABS, GROUPS, DOCK],
DOCK_JS + "\nconst a = createDock([save, reset], () => {});"
"\nDRAFT.actions.push(() => { const b = document.createElement('button');"
" b.textContent = '인쇄 미리보기'; return b; });"
"\nconst b = createDock([save, reset], () => {});"
"\nconsole.log(JSON.stringify({ cls: a.className, a: shape(a), b: shape(b) }));",
)
assert out["cls"] == "ui-sidebar-actions b09-dock" # 바닥 고정 = 공용 액션 줄 한 벌
view = {"sw": "view", "draft": False, "help": False, "text": ["박스보기", "호표보기"]}
link = {"sw": "link", "draft": False, "help": False, "text": ["하이퍼링크", "팝업"]}
main = {"sw": None, "draft": False, "help": True, "text": ["저장", "내역 초기화"]}
assert out["a"] == [view, link, main] # 초안 단추 없음 = 줄 없음
draft = {"sw": None, "draft": True, "help": False, "text": ["인쇄 미리보기"]}
assert out["b"] == [view, link, draft, main] # 초안 단추 = [저장] 줄 위
def test_스위치_기본_팝업_누름_pref():
out = run(
[COMMON, TABS, GROUPS, DOCK],
DOCK_JS + "\nconst dock = createDock([save, reset], () => {});"
"\nconst row = dock.children[1];"
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-pressed']);"
"\nconst a = [linkMode(), pressed()];"
"\nrow.children[0].click();"
"\nconst b = [linkMode(), pressed(), kept.get('b09-link-mode')];"
"\nrow.children[1].click();"
"\nconst c = [linkMode(), pressed(), kept.get('b09-link-mode')];"
"\nkept.set('b09-link-mode', '\"x\"');"
"\nconsole.log(JSON.stringify({ a, b, c, odd: linkMode() }));",
)
assert out["a"] == ["popup", ["false", "true"]] # 기본 = 팝업
assert out["b"] == ["link", ["true", "false"], '"link"']
assert out["c"] == ["popup", ["false", "true"], '"popup"']
assert out["odd"] == "popup" # 모르는 값 = 팝업
def test_DRAFT_actions_자리():
tabs = (B09 / "B09_Estimation_UI_Tabs.ts").read_text("utf-8")
assert "actions: Array<() => HTMLElement>;" in tabs
assert "{ builders: {}, extend: [], panels: [], actions: [] }" in tabs
state = (ROOT / "A00_Common/b_page_state.ts").read_text("utf-8")
assert '"b09-link-mode": { bucket: "pref", scope: "global" },' in state
css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8")
m = re.search(r"\.b09-dock\.ui-sidebar-actions\s*\{([^}]*)\}", css)
assert m and "flex-direction: column" in m.group(1) # 줄 여럿 세로
# 표 짜기 — 탭 짜기 18 개 대신 가짜(줄 하나) · 옵션 = 표 그대로 넘김
FAKE_BUILDS = "".join(
f"\nconst {name} = () => ({{ columns: [], rows: [{{ key: '{name}' }}] }});"
for name in re.findall(
r":\s*(build\w+),",
(ROOT / BUILDERS).read_text("utf-8"),
)
)
def test_Builders_화면_같은_옵션():
out = run(
[TABS, BUILDERS],
"\nDRAFT.builders.draft_x = () => '안내';"
"\nconst spec = { columns: [{ key: 'a', label: 'A' }], rows: [{ key: 'r' }], frozen: 2,"
" editable: () => true, onEdit: () => {}, rowClass: () => 'x', cellClass: () => 'y',"
" level: { key: 'a', of: () => 1 }, mergeSame: ['a'], link: () => null, menu: () => [] };"
"\nconst o = tableOptionsOf(spec, 'bill');"
"\nconst plain = tableOptionsOf({ columns: [], rows: [], editOnDouble: false });"
"\nconsole.log(JSON.stringify({ keys: Object.keys(o).sort(), caption: o.caption,"
" same: ['columns', 'rows', 'editable', 'onEdit', 'rowClass', 'cellClass', 'level', 'mergeSame']"
".every((k) => o[k] === spec[k]), frozen: o.frozen, total: o.total, empty: o.empty,"
" dbl: [o.editOnDouble, plain.editOnDouble], key: [o.keyEdit, plain.keyEdit],"
" plainCaption: plain.caption ?? null, plainFrozen: plain.frozen,"
" bill: tabTableOf('bill', {}).rows[0].key, draft: tabTableOf('draft_x', {}),"
" none: tabTableOf('nope', {}) }));",
FAKE_BUILDS,
)
assert out["keys"] == sorted(
[
"columns",
"rows",
"total",
"frozen",
"empty",
"caption",
"editable",
"editOnDouble",
"keyEdit",
"onEdit",
"rowClass",
"cellClass",
"level",
"mergeSame",
]
) # 누름 · 메뉴 · 풍선 없음(그리는 쪽 몫)
assert out["caption"] == "설계내역서" and out["same"] is True
assert out["frozen"] == 2 and out["total"] is None and out["empty"] == ""
assert out["dbl"] == [True, False] # 243-1 기본 = 더블클릭
assert out["key"] == [False, True] # 설계내역서(메뉴) Enter · F2 = 레벨 키 몫
assert out["plainCaption"] is None and out["plainFrozen"] == 0
assert out["bill"] == "buildBill" and out["draft"] == "안내" and out["none"] is None
def test_화면이_Builders_옵션을_씀():
shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "...tableOptionsOf(spec, tab)," in shell
assert "const spec = tabTableOf(tab, ctx);" in shell
assert "BUILDERS" not in shell
# ── 303-2 팝업 창 ──────────────────────────────────────────────────────────
POPUP = "B09_Estimation/B09_Estimation_UI_Popup.ts"
def _fn(path: str, name: str) -> str:
"""파일에서 함수 하나 꺼냄(맨 앞 `export` 뗌) — 이어 붙이면 이름이 부딪히는 파일 대신."""
src = (ROOT / path).read_text("utf-8")
m = re.search(rf"export function {name}\(.*?\n}}\n", src, re.S)
assert m, name
return m.group(0).replace("export ", "", 1)
POPUP_PRELUDE = (
FAKE_BUILDS
+ "\nconst GAP = 6;\n"
+ _fn("ui_template/ui_template_hover_info.ts", "placeHoverInfo")
+ _fn("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts", "subtreeEnd")
+ r"""
E.prototype.remove = function () {
if (this.parent) this.parent.children = this.parent.children.filter((c) => c !== this);
this.parent = null;
};
E.prototype.getBoundingClientRect = function () {
return this.rect ?? { left: 0, top: 0, right: 0, bottom: 0, width: 400, height: 300 };
};
document.body = new E("body");
const winKeys = [];
const hashFns = [];
globalThis.window = { innerWidth: 1200, innerHeight: 800,
addEventListener: (t, f) => (t === "keydown" ? winKeys : hashFns).push(f) };
const esc = () => {
const ev = { key: "Escape", stopped: false, preventDefault() {}, stopPropagation() { ev.stopped = true; } };
winKeys.forEach((f) => f(ev));
return ev.stopped;
};
const tables = [];
const createDataTable = (o) => {
const root = document.createElement("div");
o.rows.forEach((r, i) => {
const tr = document.createElement("tr");
tr.dataset.i = String(i);
tr.rect = { left: 40, top: 300 + i * 28, right: 900, bottom: 328 + i * 28 };
root.append(tr);
});
tables.push(o);
return { root };
};
"""
)
SCENE = r"""
const col = [{ key: "name", label: "명칭", group: ["머리"] }];
const specs = {
bill: { columns: col, menu: () => [], level: { key: "name", of: (i) => [1, 2, 2, 1][i] },
rows: [{ key: "b1", level: 1 }, { key: "b2", level: 2 }, { key: "b3", level: 2 }, { key: "b4", level: 1 }],
rowClass: (i) => (i === 0 ? "is-title" : null),
editable: (i, k) => k === "qty",
link: (i) => ({ tab: "labor", key: "L1" }) },
labor: { columns: col, rows: [{ key: "L0" }, { key: "L1" }, { key: "L2" }],
link: (i) => (i === 1 ? { tab: "dg", key: "D1|1" } : null) },
dg: { columns: col, rows: [{ key: "D1" }, { key: "D1|1" }, { key: "D1|합계" }, { key: "D2" }] },
};
const gone = [];
const base = { specOf: (t) => specs[t] ?? null, go: (l) => gone.push(l) };
const anchor = document.createElement("tr");
anchor.rect = { left: 40, top: 72, right: 900, bottom: 100 };
const boxes = () => document.body.children.map((b) => b.dataset.b09Popup);
const rows = (k) => tables[k].rows.map((r) => r.key);
const cls = (k) => tables[k].rows.map((_, i) => tables[k].rowClass(i));
"""
def _popup(body: str):
return run([TABS, GROUPS, BUILDERS, POPUP], SCENE + body, POPUP_PRELUDE)
def test_강조_범위_줄_수():
out = _popup(
"\nconsole.log(JSON.stringify(["
"popupRange('bill', specs.bill, 'b1'), popupRange('bill', specs.bill, 'b4'),"
"popupRange('labor', specs.labor, 'L1'), popupRange('dg', specs.dg, 'D1|1'),"
"popupRange('labor', specs.labor, 'X')]));"
)
# 설계내역서 = 그 줄 + 자손 · 그 밖 = 한 줄 · 묶음 = 호표 구간 · 없음 = null
assert out == [[0, 2], [3, 3], [1, 1], [0, 2], None]
def test_창_열_제목_강조_줄_자리():
out = _popup(
"\nopenRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });"
"\nconst box = document.body.children[0];"
"\nconst o = tables[0];"
"\nconsole.log(JSON.stringify({ boxes: boxes(), n: popupCount(), cls: box.className,"
" head: box.children[0].children.map((e) => e.textContent), rows: rows(0), rowCls: cls(0),"
" columns: o.columns === specs.bill.columns, caption: o.caption, total: o.total,"
" editable: o.editable ?? null, lv: [0, 1, 2].map((i) => o.level.of(i)),"
" table: box.children[1].dataset.b09Table, left: box.style.left, top: box.style.top }));"
)
assert out["boxes"] == ["bill"] and out["n"] == 1
assert out["cls"] == "b09-estimation b09-popup" # 창 안 = b09 표 꼴(빨강 테두리)
assert out["head"] == ["설계내역서", "탭으로", "×"]
assert out["rows"] == ["b1", "b2", "b3"] # 그 줄 + 자손
assert out["rowCls"] == ["is-title is-pick is-pick-top", "is-pick", "is-pick is-pick-end"]
assert out["columns"] is True and out["caption"] == "설계내역서" # 열 제목(머리 여러 줄) 그대로
assert out["total"] is None and out["editable"] is None # 합계 · 고칠 칸 없음
assert out["lv"] == [1, 2, 2] and out["table"] == "bill"
assert out["left"] == "40px" and out["top"] == "106px" # 누른 줄 아래(간격 6)
def test_이어진_창_겹침_Esc_차례():
out = _popup(
"\nopenRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });"
"\ntables[0].onCellClick(1, 'qty');" # 고칠 칸 = 안 엶
"\nconst a = boxes();"
"\ntables[0].onCellClick(1, 'name');" # 창 안 링크 줄 = 이어진 창
"\nconst second = document.body.children[1];"
"\ntables[1].onCellClick(0, 'name');" # 창 안 줄 0 = 원래 줄 1(L1) → dg
"\nconst b = boxes();"
"\nconst dg = rows(2);"
"\nconst e1 = esc(); const c = boxes();"
"\nconst e2 = esc(); const e3 = esc(); const e4 = esc();"
"\nconsole.log(JSON.stringify({ a, b, dg, top2: second.style.top, c, e: [e1, e2, e3, e4],"
" d: boxes(), gone }));"
)
assert out["a"] == ["bill"]
assert out["b"] == ["bill", "labor", "dg"] # 겹쳐 위에(뒤 DOM = 위)
assert out["dg"] == ["D1", "D1|1", "D1|합계"] # 호표 구간
assert out["top2"] == "362px" # 그 줄(창 안 2번째 줄) 곁
assert out["c"] == ["bill", "labor"] # Esc = 맨 위 하나
assert out["e"] == [True, True, True, False] # 창 없으면 Esc 안 가로챔
assert out["d"] == [] and out["gone"] == []
def test_닫기_탭으로_새_누름_주소():
out = _popup(
"\nconst open = (key = 'b1') => openRowPopup({ ...base, link: { tab: 'bill', key }, anchor });"
"\nopen(); tables[0].onCellClick(0, 'name');"
"\nconst head = (k) => document.body.children[k].children[0].children;"
"\nhead(0)[2].click();" # 아래 창 × = 그 창 + 위 창
"\nconst a = boxes();"
"\nopen(); tables[2].onCellClick(0, 'name'); open('b4');" # 화면 새 누름 = 있던 창 모두 닫고 새로
"\nconst b = [boxes(), rows(4)];"
"\nhead(0)[1].click();" # [탭으로] = 옮기고 모두 닫음
"\nconst c = [boxes(), [...gone]];"
"\nopen(); hashFns.forEach((f) => f());"
"\nconst d = boxes();"
"\nopenRowPopup({ ...base, link: { tab: 'labor', key: 'X' }, anchor });" # 보일 줄 없음 = 옮김
"\nconsole.log(JSON.stringify({ a, b, c, d, e: [boxes(), gone.length] }));"
)
assert out["a"] == []
assert out["b"] == [["bill"], ["b4"]]
assert out["c"] == [[], [{"tab": "bill", "key": "b4"}]]
assert out["d"] == [] # 주소 바뀜 = 모두 닫음
assert out["e"] == [[], 2]
def test_창_자리_화면_안():
out = _popup(
"\nconst v = { width: 1200, height: 800 };"
"\nconsole.log(JSON.stringify(["
"popupPlace({ left: 40, top: 700, right: 900, bottom: 728 }, { width: 400, height: 300 }, v),"
"popupPlace({ left: 1000, top: 100, right: 1190, bottom: 128 }, { width: 400, height: 300 }, v),"
"popupPlace({ left: 40, top: 400, right: 900, bottom: 428 }, { width: 400, height: 700 }, v)]));"
)
assert out[0] == {"x": 40, "y": 394} # 아래 모자람 = 위
assert out[1] == {"x": 794, "y": 134} # 오른쪽 화면 안
assert out[2] == {"x": 40, "y": 92} # 위아래 다 모자람 = 화면 안으로 밀어 넣음
def test_화면_잇기_모드별_누름_길():
shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert (
'if (linkMode() === "link") return show(link.tab, link.key);' in shell
) # 하이퍼링크 그대로
assert "openRowPopup({" in shell and "go: (to) => show(to.tab, to.key)," in shell
assert "closePopups(); // 303-2 탭 바꿈" in shell # 탭 바꿈 = 모두 닫음
assert 'goto(spec.link(i, key), made.root.querySelector(`tr[data-i="${i}"]`));' in shell
assert "if (spec.link && !spec.editable?.(i, key))" in shell # 고칠 칸 = 안 옮김
assert (
"createCheckPanel(refresh, (link) => show(link.tab, link.key));" in shell
) # 연결 없음 = 옮김
groups = (B09 / "B09_Estimation_UI_Tabs_Groups.ts").read_text("utf-8")
assert 'cards.querySelector(`[data-at="${i}"]`)' in groups # 박스 카드 누름도 같은 길
popup = (ROOT / POPUP).read_text("utf-8")
assert "...tableOptionsOf(spec, tab)," in popup # 화면과 같은 표 짜기
css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8")
m = re.search(r"\.b09-estimation\.b09-popup\s*\{([^}]*)\}", css)
assert m and "max-width: 90vw" in m.group(1) and "position: fixed" in m.group(1)
@@ -0,0 +1,103 @@
"""304-1 B09 박스보기 · 호표보기 = 좌측 하단 한 설정(묶음 탭 셋 함께).
- 한 값(`b09-group-view` · pref global)이 dg · machine_blocks · sg 셋에 함께 · 기본 = 박스(카드)
- 옛 탭별 값(`{탭: 보기}`) = 첫 읽기에 한 값으로(dg · 없으면 다른 탭 · 없으면 기본) 옮겨 적음
- 보조 탭 줄 보기 토글 걷음 · 가로 열 수 단추(박스 보기)는 그대로
- 좌측 하단 스위치 [박스보기|호표보기] 누름 = 값 바꿈 · 다시 그림 부름
"""
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
LABOR = "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts"
DOCK = "B09_Estimation/B09_Estimation_UI_Dock.ts"
DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0}
SPEC = (
"\nconst spec = buildDaegaBlocks({ sheets: { daega_blocks: ["
+ json.dumps(DG, ensure_ascii=False)
+ "] } });"
"\nconst tools = [];"
"\nconst draw = (tab) => renderGroupPanel({ tab, spec, hint: null, goto: () => {},"
" setTools: (t) => tools.push(...t),"
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) })[0];"
"\nconst kinds = () => ['dg', 'machine_blocks', 'sg'].map((t) => draw(t).tagName);"
)
def test_한_설정_셋_탭_함께():
out = run(
[COMMON, TABS, LABOR, GROUPS],
SPEC + "\nconst a = kinds();"
"\nchooseGroupView('table');"
"\nconst b = kinds();"
"\nconsole.log(JSON.stringify({ a, b, kept: Object.fromEntries(kept),"
" toggles: tools.filter((e) => e.dataset.b09GroupView).length,"
" cols: tools.flatMap((e) => e.all('[data-group-cols]')).length }));",
RAW,
)
assert out["a"] == ["DIV"] * 3 # 기본 = 박스
assert out["b"] == ["TABLE"] * 3 # 한 값이 셋 탭 함께
assert json.loads(out["kept"]["b09-group-view"]) == "table"
assert out["toggles"] == 0 # 탭 줄 보기 토글 없음
assert out["cols"] == 4 * 3 # 박스 보기 가로 열 수 단추 = 탭 줄 그대로
def test_옛_탭별_값_첫_읽기에_한_값으로():
cases = [
({"dg": "table", "sg": "cards"}, "table"),
({"dg": "cards", "sg": "table"}, "cards"), # dg 먼저
({"sg": "table"}, "table"), # dg 없으면 다른 탭
({"machine_blocks": "table", "sg": "cards"}, "table"),
({}, "cards"), # 없으면 기본
]
for old, want in cases:
out = run(
[COMMON, TABS, LABOR, GROUPS],
"\nkept.set('b09-group-view', "
+ json.dumps(json.dumps(old))
+ ");"
+ SPEC
+ "\nconst got = kinds();"
"\nconsole.log(JSON.stringify({ got, kept: Object.fromEntries(kept) }));",
RAW,
)
assert out["got"] == ["TABLE" if want == "table" else "DIV"] * 3, old
assert json.loads(out["kept"]["b09-group-view"]) == want, old # 옮겨 적음
def test_좌측_스위치_누름():
out = run(
[COMMON, TABS, GROUPS, DOCK],
"\nlet redraws = 0;"
"\nconst dock = createDock([document.createElement('button')], () => redraws++);"
"\nconst row = dock.children[0];"
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-pressed']);"
"\nconst labels = row.children.map((b) => b.textContent);"
"\nconst a = pressed();"
"\nrow.children[0].click();" # 이미 박스 = 그대로
"\nconst b = [redraws, pressed()];"
"\nrow.children[1].click();"
"\nconsole.log(JSON.stringify({ name: row.dataset.b09Switch, labels, a, b, c: [redraws, pressed()],"
" view: groupView(), kept: Object.fromEntries(kept) }));",
)
assert out["name"] == "view" and out["labels"] == ["박스보기", "호표보기"]
assert out["a"] == ["true", "false"] # 기본 = 박스보기
assert out["b"] == [0, ["true", "false"]]
assert out["c"] == [1, ["false", "true"]] and out["view"] == "table"
assert json.loads(out["kept"]["b09-group-view"]) == "table" # [저장] 없이 pref
def test_잇기():
groups = (ROOT / GROUPS).read_text("utf-8")
assert "toggle" not in groups and "redraw" not in groups and "createButton" not in groups
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "createDock([save, reset], () => isGroupTab(current) && show(current))" in shell
state = (ROOT / "A00_Common/b_page_state.ts").read_text("utf-8")
assert '"b09-group-view": { bucket: "pref", scope: "global" },' in state