Merge remote-tracking branch 'origin/dev' into sub_laptop_8

This commit is contained in:
2026-10-10 13:00:30 +09:00
11 changed files with 251 additions and 26 deletions
+27 -10
View File
@@ -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
+5 -6
View File
@@ -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 {
+3 -1
View File
@@ -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) ·
+19
View File
@@ -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);
@@ -112,3 +118,16 @@
background: color-mix(in srgb, var(--color-accent) 22%, var(--color-canvas));
font-weight: var(--font-weight-bold);
}
/* 185-1 총괄 · 원가계산서 레벨 — 글자 크기 = 위 is-lvN · 들여쓰기 = 명칭 칸(is-indent) · 앞 칸(is-flat)은 그대로 */
.b09-estimation .ui-data-table__table tbody tr[class*="is-lv"] td.is-flat:first-child {
padding-left: var(--spacing-8);
}
.b09-estimation .ui-data-table__table tbody tr.is-lv2 td.is-indent {
padding-left: calc(var(--spacing-8) + 1em);
}
.b09-estimation .ui-data-table__table tbody tr.is-lv3 td.is-indent {
padding-left: calc(var(--spacing-8) + 2em);
}
@@ -177,7 +177,7 @@ function numberFormat(spec: TabTable, key: string): (v: number | null | undefine
/** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */
const chosen = new Map<TabId, GroupView>();
/** 묶음 탭 — 머리(안내 + 보기 단추) · 고른 보기(카드 · 표) */
/** 묶음 탭 — 카드 = 안내 + 카드 판(보기 단추는 가로 열 수 줄) · 표 = 머리(안내 + 보기 단추) + 표 */
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));
@@ -8,6 +8,9 @@
* 고칠 칸 = 요율 · 손입력(기초액 · 이윤 보정 · 부가세 조정 · 조달수수료율) ·
* 엑셀에 없는 현행 비목 = 줄만(빈칸)
* 원가계산서 = 총괄 값 거울 + 구성비 · 고칠 칸 = 간접재료비 · 작업설 · 줄 누름 = 총괄 그 줄로
* 레벨(185-1) = 줄 `level`(화면이 정함 · 줄 짜임이 이 파일에 있음 · 셈 짝은 값만) → 182 is-lvN 글자 크기 ·
* 들여쓰기 = 명칭 칸(is-indent) · 앞 칸(is-flat)은 그대로 · 1단 = 굵게(is-title)
* 총괄 = 가.~바. 소계 1 · 그 밖 2 · 원가계산서 = 구분만 1 · 비목 소계 2 · 세목 3
* ========================================================================== */
import type { DataRow } from "@ui/ui_template_data_table";
@@ -74,6 +77,14 @@ function clickEstimate(ctx: TabContext, rowKey: string, column: string, from: un
ctx.setUser({ ...ctx.user, estimate });
}
/** 185-1 레벨 꼴 — 줄 class · 칸 class(들여쓰기 = 명칭 칸 · 앞 칸 그대로) */
const levelClass = (r: DataRow | undefined): string | null =>
r ? `is-lv${r.level}${r.level === 1 ? " is-title" : ""}` : null;
const indentClass =
(first: string, name: string) =>
(_i: number, key: string): string | null =>
key === name ? "is-indent" : key === first ? "is-flat" : null;
function rateText(formula: string, rate: Num): string {
return rate === null ? `${formula}(요율 빈칸)` : formula.replace("율", `${rate}%`);
}
@@ -113,6 +124,7 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
rate: null,
hand: null,
candidate: "",
level: part.endsWith(".") ? 1 : 2,
...extra,
});
const rateRow = ([key, name, rt, formula]: [string, string, string, string], no: number) =>
@@ -197,6 +209,8 @@ export const buildTotal: TabBuild = (ctx): TabTable => {
],
rows,
frozen: 2,
rowClass: (i) => levelClass(rows[i]),
cellClass: indentClass("part", "name"),
hint:
"요율 = M01 조달청 제비율에서 자동(맨 위 고르는 값 · 공종 토목) — 칸에 적으면 사용자 값이 앞섬 · 지우거나 출처 칸 누르면 자동 · " +
"2024 엑셀 요율과 연도 차로 다를 수 있음 · 자동도 없으면 그 줄 · 그 줄을 쓰는 소계 빈칸 · 끝수 = 엑셀 그대로(줄마다 원 버림 · 관급 천원 올림) · " +
@@ -262,6 +276,7 @@ export const buildCost: TabBuild = (ctx): TabTable => {
share: share[key] ?? null,
note: formula[key] ?? "",
link: link ?? "",
level: !part ? 1 : name === "소계" ? 2 : 3,
}));
return {
columns: [
@@ -274,6 +289,8 @@ export const buildCost: TabBuild = (ctx): TabTable => {
],
rows,
frozen: 3,
rowClass: (i) => levelClass(rows[i]),
cellClass: indentClass("group", "name"),
hint: "총괄설계내역서 값 거울 · 구성비 = 총공사비 대비 · 고칠 칸 = 간접재료비 · 작업설(직접재료비가 그만큼 바뀌고 재료비 소계는 그대로 — 엑셀 꼴) · 줄 누름 = 총괄 그 줄",
editable: (i, key) => key === "amount" && String(rows[i]?.key) in HAND,
onEdit: (i, key, value) => editEstimate(ctx, String(rows[i]?.key), key, value),
@@ -47,3 +47,8 @@
## 추가 (182장 뒤 · 23번 보충)
31. 설계 내역서의 내용은 기본적으로 들어간 내용은 고정이 아님. 초기에 기본적으로 만드는 로직은 있지만 이걸 사용자가 수정할 수 있어야하며 추가하는 행과 삭제도 자유로워야함. 다시 말해 자유롭게 작성이 되고 단가 산출과 일위대가에 연결할 수 있어야한다는 것임.
32. 추가로 특정 셀의 텍스트 입력으로 마우스 우클릭이 브라우져 옵션이 보임. 우리프로그램에서는 우클릭으로 브라우져 옵션보이기는 없는 상태임.참고
33. 총관설계 내역서에도 행들이 레벨 구분이 되어야함.
34. 박스보기가 구현되어 있는 버튼에 줄바꿈이 존재함. 1행으로 존재하게 변경하고 같이 존제하는 가로 개수 설정의 버튼들도 같은 행에 존재하게 해줘.
35. 자재단가대비표의에서 견적항목의 경우 단가를 수기입력하는 정보들만 견적서를 첨부하는 글자가 보이게 반영
36. 자재단가대비표의 출처는 있어야하는 정보가 맞으나 현재 페이지에서는 중요한 정보가 아니니 출처정보는 마우스커서를 올린 셀의 정보를 보이게 하는데 숫자들의 의미를 모름 풀어서 짧게 정보를 보여줘.
37. 4번 요청에 이어 프로그램 전역에서 호버기능으로 추가정보를 보여줄때의 공용양식이 있었으면 함.
@@ -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,"
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""185-1 B09 총괄설계내역서 · 공사원가계산서 줄 레벨 — 줄 `level` · 줄 class(182 is-lvN) · 칸 class · CSS.
레벨 = 화면이 정함(줄 짜임이 `B09_Estimation_UI_Tabs_Total.ts` 에 있음 · 셈 짝은 값만 냄 → 거울 없음).
총괄 = 가.~바. 소계 1(굵게) · 그 밖 2 · 원가계산서 = 구분만 1 · 비목 소계 2 · 세목 3.
들여쓰기 = 명칭 칸(is-indent) · 앞 칸(is-flat)은 그대로.
"""
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
B09 = ROOT / "B09_Estimation"
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
SUMS = {"material": 194234417, "labor": 346800554, "expense": 169324165, "supply_net": 69474220}
BODY = f"""
const user = {{ estimate: {{}} }};
const ctx = {{ sheets: {{ total: computeTotal({json.dumps(SUMS)}, user.estimate) }}, payload: {{}}, user, setUser: () => {{}} }};
const pack = (t) => t.rows.map((r, i) => [r.key, r.level, t.rowClass(i), t.cellClass(i, "name"), t.cellClass(i, t.columns[0].key), t.cellClass(i, "amount")]);
console.log(JSON.stringify({{ total: pack(buildTotal(ctx)), cost: pack(buildCost(ctx)) }}));
"""
def _run() -> dict:
names = ("Calc_Base", "Calc_Total", "UI_Tabs", "UI_Tabs_Total")
script = "".join(
IMPORTS.sub("", (B09 / f"B09_Estimation_{n}.ts").read_text(encoding="utf-8")) for n in names
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript"],
input=script + BODY, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_총괄_레벨():
rows = {k: (lv, cls) for k, lv, cls, *_ in _run()["total"]}
top = ["direct", "net_cost", "da", "supply_price", "contract", "grand"]
assert all(rows[k] == (1, "is-lv1 is-title") for k in top)
for k in ["in_price", "in_env", "indirect_labor", "machine", "retirement", "overhead", "vat",
"supply"]: # fmt: skip
assert rows[k] == (2, "is-lv2"), k
assert {lv for lv, _ in rows.values()} == {1, 2}
def test_원가계산서_레벨():
rows = {k: (lv, cls) for k, lv, cls, *_ in _run()["cost"]}
for k in ["overhead", "profit", "supply_price", "vat", "contract", "supply", "grand"]:
assert rows[k] == (1, "is-lv1 is-title"), k
for k in ["material", "labor", "expense_sum"]:
assert rows[k] == (2, "is-lv2"), k
for k in ["direct_material", "scrap", "direct_labor", "indirect_labor", "expense", "safety"]:
assert rows[k] == (3, "is-lv3"), k
def test_칸_class_명칭만_들여씀():
out = _run()
for tab in ("total", "cost"):
for _k, _lv, _cls, name, first, amount in out[tab]:
assert (name, first, amount) == ("is-indent", "is-flat", None)
def test_CSS_레벨_규칙():
css = (B09 / "B09_Estimation_UI_Page.css").read_text(encoding="utf-8")
assert re.search(
r'tr\[class\*="is-lv"\] td\.is-flat:first-child \{\s*padding-left: var\(--spacing-8\);', css
)
for lv, em in ((2, 1), (3, 2)):
assert re.search(
rf"tr\.is-lv{lv} td\.is-indent \{{\s*padding-left: calc\(var\(--spacing-8\) \+ {em}em\);",
css,
)
# 글자 크기 = 182 규칙 재사용(줄 높이 28px → 16px 까지)
assert re.search(r"tr\.is-lv1 td \{\s*font-size: var\(--text-body\);", css)
@@ -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")
+9 -1
View File
@@ -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;