diff --git a/B09_Estimation/B09_Estimation_Calc_Bill.py b/B09_Estimation/B09_Estimation_Calc_Bill.py index 05866a5e3..581099089 100644 --- a/B09_Estimation/B09_Estimation_Calc_Bill.py +++ b/B09_Estimation/B09_Estimation_Calc_Bill.py @@ -1,7 +1,7 @@ """B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · 파일 · M01 안 읽음. ⚠ 짝 = `B09_Estimation_Calc_Bill.ts`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 · - 거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨). + 거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨) · `test_184_2_b09_bill_free.py`(자유 편집). 실무 엑셀 설계내역서 꼴 · 공종 나무 = 엑셀 꼴 고정(`SECTIONS` · 공구 안 나눔): 1 토공사(1.1~1.6) · 2 구조물공(2.1~2.3) · 3 운반공 · 4 자재대 · 관급자재대(제외금액) @@ -12,17 +12,34 @@ 조달수수료 = ROUNDDOWN(관급 순자재대 × 율 ÷ 100, 0)(율 = 사용자 입력 · 빈칸 시작) 제목 줄 = 아래 항목 금액 합(엑셀 SUMIF — 빈 줄은 0 이 아니라 「빈 줄 n」 으로 셈) 빈칸 = 수량이나 단가를 낸 원천이 없으면 그 줄 금액 None + 「연결 없음」 목록. -손질(166-2) = 사용자 값 `user.bill` — `qty{줄 키: 수}` 자동 줄 수량 덮어쓰기(키 지우면 자동) · - `rows[]` 손 줄(`hand:` · 공종 · `after` 앞 줄 키 뒤에 끼움 · 명칭 · 규격 · 단위 · 수량 · - 연결 `ref`) · - 단가 = `ref_price(ref)` 하나 — dg = 일위대가목록표 · sg = 단가산출근거목록표 · +단가 = `ref_price(ref)` 하나 — dg = 일위대가목록표 · sg = 단가산출근거목록표 · logic = M01 로직 새 산근(서버가 `sg_hand{줄 키: 산근 키}` 로 줌 · 그전엔 빈칸 「[저장] 뒤」) · 그 밖 = `inputs.hopyo` · 비고 = 「대가n」 · 「산근 n호표」(155-3 꼴). -레벨(182) = 손 줄 `kind: "group"`(묶음 줄 · 수량 · 연결 없음) · `parent`(묶음 손 줄 키 · 없음 = 공종 바로 - 아래) · 줄마다 `level`(1 = 1 토공사 · 2 = 1.1 · 3 = 그 안 항목 · 손 묶음 + 1) · - 묶음 금액 = 자손 항목 합 · 공종 합 · `sums` = 항목만(묶음 줄 안 셈 · 원가계산서 그대로). -원가계산서로 넘길 합 = `sums`(직접재료비 · 직접노무비 · 산출경비 · 관급 순자재대 — 145-4 이음 꼴 · -빈 줄이 있으면 None) · 화면 표시용 = `totals`(아는 금액 합 · 빈 줄 수 · 조달수수료). +자유 편집(184) = 기본 로직 표 = 초기값(저장 안 함) · 사용자 값 `user.bill` v2 = 덮개(계약 — 184-3 화면): + {"v": 2, + "rows": [표 차례 전체(order · 부모 먼저 · 자손 이어서) { + "key": 줄 키, "parent": 묶음 줄 키(`sec:*` · `hand:*`) | 없음 = 맨 위, + 자동 줄(`sec:*` · `earth:` · `st:` · `haul:` · `mat:`) = 고친 칸만 — 칸 없음 = 자동(B08 따라감) · + 값 = 지킴 · null = 일부러 빈칸 · 칸 = name · spec · unit · qty · ref + (제목 `sec:*` = name 만 · `mat:` = ref 없음 · `fee` = 덮개 없음 · 차례에 안 넣음 — `sec:G` 끝 고정) + 새 줄(`hand:*`) = 모든 칸 + "kind": "group"(묶음 · 수량 · 연결 없음) | 없음 = 항목 · + "cost": "direct"(맨 위 새 묶음 · 없음도 direct)}], + "gone": [지운 자동 줄 키 — `sec:G` · `fee` · `mat:` 는 안 받음(자재 줄 주인 = 대비표 표시)]} + 덮개 없음 = 기본 표 그대로 · [초기화] = `bill` 지움 · + `rows` 에 없고 `gone` 에도 없는 자동 줄(B08 새 열 · 대비표 새 표시) = 기본 부모 끝 + 비고 「새 자동 줄」 + (기본 부모가 없으면 표 끝 맨 위) · 원천이 안 낸 키 = 안 보임 · + 부모 못 찾음 · 묶음 아님 · 돎 = 자동 줄 기본 부모 / 새 줄 맨 위 · `sec:G` = 맨 위 고정 · + 관급 자재 줄 = `sec:G` 밖 못 감(밖이면 `sec:G` 아래) · 깊이 5 넘음 = 레벨 4 조상 아래. + 옛 꼴(v 없음 · 166-2 · 182) = `qty{줄 키: 수}` 수량 덮개 + `rows[]` 손 줄(`section` · `after` 앞 줄 + 뒤에 끼움 · `parent` = 같은 공종 손 묶음) — 그대로 읽음(화면 첫 손질 때 v2 로 굳힘). +셈 출력 줄 칸(계약) = kind("title" 자동 묶음 · "group" 새 묶음 · "item") · key · name(번호 뺀 이름) · + parent(실제 부모 키 · None = 맨 위) · level(1~5 = 깊이) · no(레벨 1 · 2 묶음 차례 번호 「1」 · 「1.1」 · + 관급 · 항목 = "") · cost("direct" | "supply" = 맨 위 조상 갈래 · `sec:G` 만 supply) · + auto(True = 자동 줄) | hand(True = 새 줄) · auto_{name|spec|unit|qty|ref}(고친 칸만 = 자동값) · + section(가장 가까운 제목 공종 · 없으면 None). +묶음 금액 = 자손 항목 합 + 「빈 줄 n」 · 구조물 블록 줄 키 꼬리 = 대가 블록 키(대가 번호 밀림에 안 흔들림). +원가계산서로 넘길 합 = `sums`(cost direct 항목의 직접재료비 · 직접노무비 · 산출경비 · supply 항목의 관급 +순자재대 — 145-4 이음 꼴 · 빈 줄이 있으면 None) · 화면 표시용 = `totals`(아는 금액 합 · 빈 줄 수 · 조달수수료). """ from __future__ import annotations diff --git a/B09_Estimation/B09_Estimation_Calc_Bill.ts b/B09_Estimation/B09_Estimation_Calc_Bill.ts index 3bc38f886..00adfd349 100644 --- a/B09_Estimation/B09_Estimation_Calc_Bill.ts +++ b/B09_Estimation/B09_Estimation_Calc_Bill.ts @@ -3,17 +3,16 @@ * B09 설계내역서 셈(PLAN 145-3) — 화면 없음 · 저장 안 함 · import 는 짝 바탕 셈 하나(시험이 이어 붙임). * * ⚠ 짝 = `B09_Estimation_Calc_Bill.py`(같은 식 · 같은 차례 · 같은 끝수) — 한쪽을 고치면 다른 쪽도 · - * 거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨). + * 거울 시험 `resources/tester/test_145_3_b09_bill.py` · `test_182_b09_bill_level.py`(레벨) · `test_184_2_b09_bill_free.py`(자유 편집). * * 공종 나무 = 엑셀 꼴 고정(1 토공사 · 2 구조물공 · 3 운반공 · 4 자재대 · 관급자재대(제외금액)) · 공구 안 나눔 * 항목 = 서버가 B08 수량으로 짠 줄 + 대비표 「별도」(4 자재대) · 「관급」(관급자재대) 자재 + 조달수수료 * 금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 조달수수료 = ROUNDDOWN(순자재대 × 율 ÷ 100, 0) - * 제목 줄 = 아래 항목 금액 합(빈 줄은 「빈 줄 n」) · 원가계산서로 넘길 합 = sums(빈 줄 있으면 null) - * 손질(166-2) = `user.bill` — qty{줄 키: 수} 자동 줄 수량 덮어쓰기 · rows[] 손 줄(after 뒤에 끼움) · + * 묶음 금액 = 자손 항목 합(빈 줄은 「빈 줄 n」) · 원가계산서로 넘길 합 = sums(빈 줄 있으면 null) * 단가 = `refPrice(ref)` 하나(dg 대가 목록 · sg 산근 목록 · logic = 서버 sg_hand 산근 · 그 밖 hopyo) - * 레벨(182) = 손 줄 `kind: "group"`(묶음 줄 · 수량 · 연결 없음) · `parent`(묶음 손 줄 키 · 없음 = 공종 바로 아래) · - * 줄마다 `level`(1 = 1 토공사 · 2 = 1.1 · 3 = 그 안 항목 · 손 묶음 + 1) · 묶음 금액 = 자손 항목 합 · - * 공종 합 · sums = 항목만(묶음 줄 안 셈 · 원가계산서 그대로) + * 자유 편집(184) = `user.bill` v2 덮개 {v: 2, rows: [표 차례 · 고친 칸 · 새 줄], gone: [지운 자동 줄]} · + * 옛 꼴(qty · 손 줄 after · parent) 그대로 읽음 · 저장 꼴 · 출력 줄 칸(계약) = 짝 .py 머리 + * 출력 줄 칸 = kind · key · name · parent · level · no · cost · auto | hand · auto_{칸} · section * ========================================================================== */ import { diff --git a/B09_Estimation/B09_Estimation_Store.py b/B09_Estimation/B09_Estimation_Store.py index 905be8362..fb208992c 100644 --- a/B09_Estimation/B09_Estimation_Store.py +++ b/B09_Estimation/B09_Estimation_Store.py @@ -6,7 +6,9 @@ [저장] 때 책에 없는 새 줄만 더함(있는 줄 안 바꿈) user_values.json 사용자 값 {"materials": {자재 키: {applied?, supply?, quote?, quote_page?, pick?}}, "estimate": {…}, - "bill": {"qty": {줄 키: 수}, "rows": [손 줄]}}(166-2 설계내역서 손질) + "bill": {"v": 2, "rows": [표 차례 · 고친 칸 · 새 줄], "gone": [키]}} + (184 설계내역서 덮개 — 꼴은 `B09_Estimation_Calc_Bill` 머리 · + 옛 꼴 {"qty", "rows"}(166-2 · 182)도 그대로 다듬음) — 고친 칸만(키가 있으면 그 값 · null = 빈칸 · 없으면 기본) quotes/<자재 키>/ 견적서 파일(166-4 · 여러 개) — 폴더가 목록(키의 못 쓰는 글자는 %XX) · diff --git a/B09_Estimation/B09_Estimation_UI_Page.css b/B09_Estimation/B09_Estimation_UI_Page.css index a553161a5..909506900 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.css +++ b/B09_Estimation/B09_Estimation_UI_Page.css @@ -44,6 +44,12 @@ gap: var(--spacing-8); } +/* 185-2 보기 단추 글 한 줄 */ +.b09-estimation__bar .ui-btn { + flex: none; + white-space: nowrap; +} + /* 182 설계내역서 레벨 — 글자 크기(줄 높이 28px 고정 → 16px 까지) · 첫 칸 들여쓰기 1em × (레벨 − 1) */ .b09-estimation .ui-data-table__table tbody tr.is-lv1 td { font-size: var(--text-body); diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts index 8b523847e..0bc6e9f87 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts @@ -177,7 +177,7 @@ function numberFormat(spec: TabTable, key: string): (v: number | null | undefine /** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */ const chosen = new Map(); -/** 묶음 탭 — 머리(안내 + 보기 단추) · 고른 보기(카드 · 표) */ +/** 묶음 탭 — 카드 = 안내 + 카드 판(보기 단추는 가로 열 수 줄) · 표 = 머리(안내 + 보기 단추) + 표 */ export function renderGroupPanel(o: { tab: TabId; spec: TabTable; @@ -199,9 +199,6 @@ export function renderGroupPanel(o: { }, }); toggle.dataset.b09GroupView = view; - const bar = document.createElement("div"); - bar.className = "b09-estimation__bar"; - bar.append(...(o.hint ? [o.hint] : []), toggle); const at = o.focus === undefined ? -1 : rowIndex(o.spec.rows, o.focus); if (view === "cards") { const first = o.spec.columns[0]?.key ?? ""; @@ -213,10 +210,14 @@ export function renderGroupPanel(o: { focus: at, money: numberFormat(o.spec, "sum"), qty: numberFormat(o.spec, "qty"), + actions: [toggle], // 185-2 가로 열 수 단추와 한 줄 }); cards.dataset.b09Table = o.tab; - return [bar, cards]; + return [...(o.hint ? [o.hint] : []), cards]; } + const bar = document.createElement("div"); + bar.className = "b09-estimation__bar"; + bar.append(...(o.hint ? [o.hint] : []), toggle); const range = pickRange(o.spec.rows, o.focus); const table = o.makeTable((i) => pickClass(range, i)); if (at >= 0) requestAnimationFrame(() => table.focusRow(at)); diff --git a/resources/tester/test_171_3_b09_group_tabs.py b/resources/tester/test_171_3_b09_group_tabs.py index cd5dd8e83..aaee869e7 100644 --- a/resources/tester/test_171_3_b09_group_tabs.py +++ b/resources/tester/test_171_3_b09_group_tabs.py @@ -146,13 +146,14 @@ def test_화면_보기_바꿈_누름_이동(): "\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;" + "\nconst [view] = renderGroupPanel(o);" + "\nconst btn = view.querySelector('[data-b09-group-view]');" + "\nconst label = btn.textContent;" "\nview.querySelector('[data-at=\"2\"]').click();" "\nview.querySelector('[data-at=\"0\"]').click();" "\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');" "\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');" - "\nbar.children[0].click();" + "\nbtn.click();" "\nconst [bar2, view2] = renderGroupPanel(o);" "\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws," " label2: bar2.children[0].textContent, tag2: view2.tagName, focused," diff --git a/resources/tester/test_185_2_b09_view_bar.py b/resources/tester/test_185_2_b09_view_bar.py new file mode 100644 index 000000000..7ddc65174 --- /dev/null +++ b/resources/tester/test_185_2_b09_view_bar.py @@ -0,0 +1,76 @@ +"""185-2 B09 묶음 탭 도구 줄 — 보기 바꿈 단추 글 한 줄 · 가로 열 수 단추와 같은 줄. + +- 카드 보기 = 보기 바꿈 단추가 가로 열 수 줄(`ui-group-cards__bar`) 안 · 표 보기 = 머리 줄(`b09-estimation__bar`) +- 단추 · 라벨 = `white-space: nowrap` · 도구 줄 = 줄바꿈 없음(`flex-wrap: nowrap`) +""" + +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, RAW, TABS, run # noqa: E402 + +B09 = "B09_Estimation/B09_Estimation_UI_Tabs_" +DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0} +HINT = "(() => { const h = document.createElement('p'); h.className = 'hint'; return h; })()" + + +def _block(css: str, selector: str) -> str: + m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) + assert m, selector + return m.group(1) + + +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"\nconst o = {{ tab: 'dg', spec, hint: {hint}, goto: () => {{}}, redraw: () => {{}}," + " makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };" + "\nconst out = renderGroupPanel(o);" + "\nconst top = out.find((e) => e.all('[data-b09-group-view]').length || e.dataset.b09GroupView);" + "\nconst btn = top.dataset.b09GroupView ? top : top.querySelector('[data-b09-group-view]');" + "\nconst bar = btn.closest('.ui-group-cards__bar') ?? btn.closest('.b09-estimation__bar') ?? top;" + "\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn.textContent," + " bar: bar.className, cols: bar.all('[data-group-cols]').map((b) => b.dataset.groupCols) }));", + RAW, + ) + + +def test_카드_보기_단추가_가로_열수_줄에(): + out = _panel("cards", "null") + assert out["bar"] == "ui-group-cards__bar" and out["label"] == "표로 보기" + assert out["cols"] == ["1", "2", "3", "4"] # 같은 줄에 열 수 단추 넷 + assert out["n"] == 1 # 안내 글 없으면 카드 판 하나뿐 + + +def test_카드_보기_안내_글은_카드_위에_따로(): + out = _panel("cards", HINT) + assert out["n"] == 2 and out["first"] == "hint" + assert out["bar"] == "ui-group-cards__bar" + + +def test_표_보기_단추는_머리_줄(): + out = _panel("table", "null") + assert out["bar"] == "b09-estimation__bar" and out["label"] == "박스보기" + assert out["cols"] == [] # 표 보기 = 열 수 단추 없음 + + +def test_CSS_한_줄_nowrap(): + cards = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") + assert "flex-wrap: nowrap" in _block(cards, ".ui-group-cards__bar") + item = _block( + cards, + ".ui-group-cards__bar .ui-btn,\n.ui-group-cards__label,\n.ui-group-cards__count", + ) + assert "white-space: nowrap" in item and "flex: none" in item + page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") + assert "white-space: nowrap" in _block(page, ".b09-estimation__bar .ui-btn") + assert "actions: [toggle]" in (ROOT / GROUPS).read_text("utf-8") diff --git a/ui_template/ui_template_group_cards.css b/ui_template/ui_template_group_cards.css index 3fa11762a..58499235f 100644 --- a/ui_template/ui_template_group_cards.css +++ b/ui_template/ui_template_group_cards.css @@ -10,11 +10,19 @@ .ui-group-cards__bar { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; gap: var(--spacing-8); } +/* 185-2 도구 줄 = 한 줄 · 단추 · 글 줄바꿈 없음 */ +.ui-group-cards__bar .ui-btn, +.ui-group-cards__label, +.ui-group-cards__count { + flex: none; + white-space: nowrap; +} + .ui-group-cards__cols { display: inline-flex; align-items: center;