Merge remote-tracking branch 'origin/sub_laptop_3' into sub_laptop_5

This commit is contained in:
2026-10-11 14:02:19 +09:00
6 changed files with 110 additions and 20 deletions
+22 -2
View File
@@ -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));
+1 -2
View File
@@ -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"]],
+17 -3
View File
@@ -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}`];
+11 -12
View File
@@ -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
@@ -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"] == [] # 다른 탭(일위대가표) = 단추 없음
@@ -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