From b342f8f7846a1b5e1718855bcbd5ea4fa8672026 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 11 Oct 2026 14:00:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(b09):=20=EC=9C=84=20=ED=83=AD=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=EB=82=B4=EC=97=AD=20=ED=95=9C=20=EB=AC=B6=EC=9D=8C=20?= =?UTF-8?q?=C2=B7=20=EC=84=A4=EA=B3=84=EB=82=B4=EC=97=AD=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A8=EA=B0=80=EC=97=B0=EA=B2=B0=20=EC=B9=B8=20=EB=9E=A8?= =?UTF-8?q?=ED=94=84=20(257-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 위 탭 총괄설계내역서 · 설계내역서 = 「설계내역」 한 묶음(총괄 · 설계내역서) - 단가연결 칸 = 램프(초록 연결 · 노랑 없음) · 풍선 = 글 · 머리 「단가↵연결」 · 폭 44 - 시험 test_257_1 · test_181_1 · test_229_1 기대값 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY --- B09_Estimation/B09_Estimation_UI_Page.css | 24 +++++++- B09_Estimation/B09_Estimation_UI_Tabs.ts | 3 +- B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts | 20 ++++++- resources/tester/test_181_1_b09_tab_groups.py | 23 ++++---- .../test_229_1_b09_level_buttons_subbar.py | 2 +- resources/tester/test_257_1_b09_tabs_lamp.py | 58 +++++++++++++++++++ 6 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 resources/tester/test_257_1_b09_tabs_lamp.py diff --git a/B09_Estimation/B09_Estimation_UI_Page.css b/B09_Estimation/B09_Estimation_UI_Page.css index 56f923c40..2d29972ac 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.css +++ b/B09_Estimation/B09_Estimation_UI_Page.css @@ -30,9 +30,9 @@ visibility: hidden; } -/* 표 머리 — 좁은 열에서 말줄임 대신 줄바꿈 */ +/* 표 머리 — 좁은 열에서 말줄임 대신 줄바꿈(257-1 머리 글 안 줄바꿈 글자도 살림) */ [data-b09-table] thead th { - white-space: normal; + white-space: pre-line; overflow-wrap: anywhere; } @@ -136,6 +136,26 @@ font-weight: var(--font-weight-bold); } +/* 257-1 설계내역서 단가연결 칸 = 램프(B08 구조물집계표 램프 꼴 · 글 숨김 · 풍선 = 글) — 노랑 = 없음 · 초록 = 연결 */ +.b09-estimation .ui-data-table__table tbody td.b09-bill__lamp { + font-size: 0; + text-align: center; +} + +.b09-estimation .ui-data-table__table tbody td.b09-bill__lamp::before { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-warning); + vertical-align: middle; +} + +.b09-estimation .ui-data-table__table tbody td.b09-bill__lamp.is-linked::before { + background: var(--color-success); +} + /* 184-3 설계내역서 고친 칸 — 자동 값과 다른 칸(풍선 = 자동 값) */ .b09-estimation .ui-data-table__table tbody td.is-edited { background: color-mix(in srgb, var(--color-warning) 22%, var(--color-canvas)); diff --git a/B09_Estimation/B09_Estimation_UI_Tabs.ts b/B09_Estimation/B09_Estimation_UI_Tabs.ts index 3dea31f92..6c5b8dd3b 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs.ts @@ -59,8 +59,7 @@ export const tabName = (id: string): string => TABS.find(([t]) => t === id)?.[1] /** 위 탭(181-1) — [위 탭 이름, 아래 탭들] · 아래 탭 하나뿐 = 위 탭 단독 · 첫 아래 탭 = 위 탭 키 */ export const TAB_GROUPS: Array<[string, TabId[]]> = [ ["공사원가계산서", ["cost"]], - ["총괄설계내역서", ["total"]], - ["설계내역서", ["bill"]], + ["설계내역", ["total", "bill"]], ["일위대가", ["dg_list", "dg"]], ["단가산출", ["sg_list", "sg"]], ["환율및기초자료", ["basis"]], diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts index dd0322a7b..3bb3ec7be 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts @@ -34,10 +34,12 @@ export const buildBill: TabBuild = (ctx): BillTable => { const bill: Row = sheets.bill || { rows: [], totals: null }; // 손질 · 메뉴 = 셈 줄 그대로(raw) · 표 = 번호 붙인 이름(no = 레벨 1 · 2 묶음 차례) const raw: DataRow[] = bill.rows; + // 257-1 연결 칸 = 램프(글은 CSS 가 숨김 · 풍선 = 글) — 연결 = 호표 줄(ref) 또는 손 줄 비고(대가N) + const linkText = (r: Row): string => (!r.auto && r.note ? r.note : r.ref ? "연결됨" : "단가연결"); const rows: DataRow[] = raw.map((r: Row) => ({ ...r, name: r.no ? `${r.no} ${r.name}` : r.name, - connect: linkable(r) ? `${!r.auto && r.note ? r.note : r.ref ? "연결됨" : "단가연결"} ▾` : "", + connect: linkable(r) ? `${linkText(r)} ▾` : "", })); const direct: Row = bill.totals?.direct || {}; const pair = (key: string, label: string) => [ @@ -57,7 +59,7 @@ export const buildBill: TabBuild = (ctx): BillTable => { ...pair("labor", "노무비"), ...pair("material", "재료비"), ...pair("expense", "경비"), - text("connect", "단가연결", 96), + { ...text("connect", "단가\n연결", 44), title: "단가연결" }, text("note", "비고", 200), ], rows, @@ -88,9 +90,21 @@ export const buildBill: TabBuild = (ctx): BillTable => { if (r.key !== "fee") return editCell(ctx, raw, i, key, v); setUser({ ...user, estimate: { ...(user.estimate || {}), procurement_fee_pct: v } }); }, - cellClass: (i, key) => (edited(rows[i], key) ? "is-edited" : null), + cellClass: (i, key) => { + const r = rows[i]; + const lamp = + key === "connect" && r && linkable(raw[i]) + ? `b09-bill__lamp${linkText(raw[i]) === "단가연결" ? "" : " is-linked"}` + : ""; + return [lamp, edited(r, key) ? "is-edited" : ""].filter(Boolean).join(" ") || null; + }, tip: (i, key) => { const r = rows[i]; + if (r && key === "connect" && linkable(raw[i])) { + const auto = (r.auto_ref as Row | null)?.key ?? "빈칸"; + const back = edited(r, key) ? ` — 자동 값 ${auto} — 우클릭 [자동값으로 되돌리기]` : ""; + return linkText(raw[i]) + back; + } if (r && r.kind !== "item" && SUM_KEYS.includes(key)) return "수식 = 아래 항목 합(읽기 전용)"; if (!r || !edited(r, key)) return null; const auto = key === "connect" ? (r.auto_ref as Row | null)?.key : r[`auto_${key}`]; diff --git a/resources/tester/test_181_1_b09_tab_groups.py b/resources/tester/test_181_1_b09_tab_groups.py index 12f27f1d5..09b83bd94 100644 --- a/resources/tester/test_181_1_b09_tab_groups.py +++ b/resources/tester/test_181_1_b09_tab_groups.py @@ -1,4 +1,4 @@ -"""181-1 B09 탭 두 층 — 위 탭 9(203-1 연결 점검 뺌) · 아래 탭 묶음(사용자 30번) · 줄 그리기 · 마지막 본 아래 탭. +"""181-1 B09 탭 두 층 — 위 탭 8(203-1 연결 점검 뺌 · 257-1 설계내역 한 묶음) · 아래 탭 묶음(사용자 30번) · 줄 그리기 · 마지막 본 아래 탭. - 위 탭 = `TAB_GROUPS`(`TABS` 18 키를 빠짐 · 겹침 없이 묶음) · `TABS`(엑셀 차례)는 그대로 - 줄 = 위 md · 아래 sm · 아래 탭 없는 위 탭 = 빈 보조 줄(B08 꼴) @@ -19,8 +19,7 @@ BAR = "B09_Estimation/B09_Estimation_UI_TabBar.ts" # 사용자 30번 — 위 탭 차례 = 지금 탭 차례(묶음 첫 자리) · 아래 = 글에 적힌 차례 EXPECTED = [ ["공사원가계산서", ["cost"]], - ["총괄설계내역서", ["total"]], - ["설계내역서", ["bill"]], + ["설계내역", ["total", "bill"]], ["일위대가", ["dg_list", "dg"]], ["단가산출", ["sg_list", "sg"]], ["환율및기초자료", ["basis"]], @@ -42,7 +41,7 @@ def test_묶음_표_사용자_30번과_같고_18_탭이_한_번씩(): # 위 탭 차례 = 엑셀 차례에서 묶음이 처음 나오는 자리 firsts = list(dict.fromkeys(out["of"])) assert firsts == [g[0] for g in EXPECTED] - assert len(firsts) == 9 + assert len(firsts) == 8 def test_줄_그리기_두_층_빈_줄_마지막_본_아래_탭(): @@ -60,26 +59,26 @@ const view = () => ({ }); bars.draw('dg'); const a = view(); -bars.top.children[3].click(); // 켜진 위 탭 다시 누름 = 아무 일 없음 +bars.top.children[2].click(); // 켜진 위 탭 다시 누름 = 아무 일 없음 bars.sub.children[0].click(); // 일위대가목록표 bars.draw('compare'); // 묶음을 넘는 이동 = 위 탭 따라 켜짐 const b = view(); bars.draw('sum_material'); bars.draw('dg'); -bars.top.children[7].click(); // 재료 = 마지막 본 아래 탭(재료비수량금액집계표) -bars.top.children[6].click(); // 중기 = 본 적 없음 → 첫 아래 탭 +bars.top.children[6].click(); // 재료 = 마지막 본 아래 탭(재료비수량금액집계표) +bars.top.children[5].click(); // 중기 = 본 적 없음 → 첫 아래 탭 bars.draw('cost'); const c = view(); -bars.top.children[1].click(); // 총괄설계내역서 = 단독 위 탭 +bars.top.children[1].click(); // 설계내역 = 첫 아래 탭(총괄) bars.sub.children[0].click(); // 빈 줄 단추(숨김) = 누름 없음 console.log(JSON.stringify({ a, b, c, picks })); """, ) a, b, c = out["a"], out["b"], out["c"] - assert len(a["top"]) == 9 and [t[0] for t in a["top"]][:5] == [ - "공사원가계산서", "총괄설계내역서", "설계내역서", "일위대가", "단가산출" + assert len(a["top"]) == 8 and [t[0] for t in a["top"]][:5] == [ + "공사원가계산서", "설계내역", "일위대가", "단가산출", "환율및기초자료" ] # fmt: skip - assert [t[2] for t in a["top"]] == ["false"] * 3 + ["true"] + ["false"] * 5 + assert [t[2] for t in a["top"]] == ["false"] * 2 + ["true"] + ["false"] * 5 assert all("ui-btn--ghost" in t[1] for t in a["top"] if t[2] == "false") assert a["sub"] == [["일위대가목록표", "false", "dg_list"], ["일위대가표", "true", "dg"]] assert not a["blank"] and a["role"] == "tablist" @@ -88,7 +87,7 @@ console.log(JSON.stringify({ a, b, c, picks })); ["자재단가대비표", "true", "compare"], ["재료비수량금액집계표", "false", "sum_material"], ] # fmt: skip - assert b["top"][7][2] == "true" and b["top"][3][2] == "false" + assert b["top"][6][2] == "true" and b["top"][2][2] == "false" # 단독 위 탭 — 위 단추가 탭 키도 가짐 · 아래 줄 = 숨긴 단추 한 개 assert c["blank"] and c["role"] == "presentation" and len(c["sub"]) == 1 assert c["top"][0][3] == "cost" and c["top"][0][4] == "cost" and c["top"][3][4] is None diff --git a/resources/tester/test_229_1_b09_level_buttons_subbar.py b/resources/tester/test_229_1_b09_level_buttons_subbar.py index 2d40334c7..50a605e4a 100644 --- a/resources/tester/test_229_1_b09_level_buttons_subbar.py +++ b/resources/tester/test_229_1_b09_level_buttons_subbar.py @@ -75,7 +75,7 @@ console.log(JSON.stringify({ assert out["a"]["levels"] == ["들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"] assert out["a"]["guideIn"] is True # 안내 글 = 단추 줄이 아니라 따로(표 아래로) assert out["guideTag"] == "P" and "ui-general-block__empty" in out["guideCls"] - assert out["subBlank"] # 하위 탭 없는 묶음 = 빈 보조 줄(숨은 단추 + 도구) + assert not out["subBlank"] # 257-1 설계내역 = 아래 탭 둘(총괄 · 설계내역서) · 빈 보조 줄 아님 assert out["disabledBefore"] == [True] * 4 # 고른 줄 없음 = 흐림 assert out["disabledAfter"] == [True, False, False, True] # 줄 누름 = 메뉴 항목대로 assert out["b"] == [] # 다른 탭(일위대가표) = 단추 없음 diff --git a/resources/tester/test_257_1_b09_tabs_lamp.py b/resources/tester/test_257_1_b09_tabs_lamp.py new file mode 100644 index 000000000..4a15af2be --- /dev/null +++ b/resources/tester/test_257_1_b09_tabs_lamp.py @@ -0,0 +1,58 @@ +"""257-1 B09 위 탭 「설계내역」(총괄설계내역서 · 설계내역서 한 묶음) · 설계내역서 단가연결 칸 램프. + +- 위 탭 = `TAB_GROUPS` 「설계내역」[total, bill] · 옛 두 위 탭 없음 +- 단가연결 칸 = 줄 칸 class `b09-bill__lamp`(+ 연결 `is-linked`) · 풍선 = 글(대가N · 연결됨 · 단가연결) +- 머리 = 「단가↵연결」 · 폭 44 · 머리 CSS pre-line +""" + +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.test_184_3_b09_bill_menu import ROWS, SRC, _node, _ts # noqa: E402 + +JS = """ +const ctx = { sheets: { bill: { rows: __ROWS__, totals: null } }, payload: {}, user: {}, setUser() {} }; +const table = buildBill(ctx); +const at = (key) => table.rows.findIndex((r) => r.key === key); +const keys = ["earth:4", "earth:5", "hand:a", "mat:M1", "fee", "sec:1"]; +const col = table.columns.find((c) => c.key === "connect"); +console.log(JSON.stringify({ + col: { label: col.label, width: col.width, title: col.title }, + cls: keys.map((k) => table.cellClass(at(k), "connect")), + tip: keys.map((k) => table.tip(at(k), "connect")), + other: table.cellClass(at("earth:4"), "qty"), + groups: TAB_GROUPS.map(([label, tabs]) => [label, tabs]), +})); +""" + + +def test_램프_칸_class_풍선_머리(): + body = JS.replace("__ROWS__", json.dumps(ROWS, ensure_ascii=False)) + out = json.loads(_node(_ts([*SRC, "UI_Tabs_Bill"]) + body)) + assert out["col"] == {"label": "단가\n연결", "width": 44, "title": "단가연결"} + assert out["cls"] == [ + "b09-bill__lamp is-linked", # 자동 줄 · 호표 연결 + "b09-bill__lamp", # 연결 없음 = 노랑 + "b09-bill__lamp is-linked", # 손 줄 · 비고 대가3 + None, # 자재 줄 = 연결 대상 아님 + None, + None, # 묶음 줄 + ] + assert out["tip"] == ["연결됨", "단가연결", "대가3", None, None, None] + assert out["other"] is None + labels = [g[0] for g in out["groups"]] + assert "설계내역" in labels and "총괄설계내역서" not in labels and "설계내역서" not in labels + assert ["설계내역", ["total", "bill"]] in out["groups"] + + +def test_CSS_램프_초록_노랑_머리_줄바꿈(): + css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") + assert "td.b09-bill__lamp::before" in css and "var(--color-warning)" in css + assert "td.b09-bill__lamp.is-linked::before" in css and "var(--color-success)" in css + head = css.split("[data-b09-table] thead th {", 1)[1].split("}", 1)[0] + assert "white-space: pre-line" in head