Merge remote-tracking branches 'origin/main_laptop_1', 'origin/sub_laptop_7' and 'origin/sub_laptop_8' into sub_laptop_6

This commit is contained in:
2026-10-31 13:49:23 +09:00
16 changed files with 553 additions and 78 deletions
@@ -14,6 +14,7 @@ IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
COMMON = "ui_template/ui_template_group_cards.ts"
TABS = "B09_Estimation/B09_Estimation_UI_Tabs.ts"
GROUPS = "B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts"
GRID = "ui_template/ui_template_hopyo_grid.ts" # 704-3 일위대가표 격자(Tabs_Labor 앞)
# 기억(b_page_state) 대신 Map · 탭 줄(fillTabs) 대신 가짜 button 줄 · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝
STUBS = r"""
+3 -2
View File
@@ -35,10 +35,11 @@ BLOCK = {
def _node(js_body, tab="UI_Tabs_Labor"):
src = "".join(
grid = IMPORTS.sub("", (ROOT / "ui_template/ui_template_hopyo_grid.ts").read_text("utf-8"))
src = grid + "".join(
IMPORTS.sub("", (ROOT / "B09_Estimation" / f"B09_Estimation_{n}.ts").read_text("utf-8"))
for n in ("UI_Tabs", tab)
)
) # 704-3 일위대가표 = 공용 격자
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", src + js_body],
@@ -57,9 +57,14 @@ def test_규격_칸_풍선():
"labor": 1, "material": 0, "expense": 0, "sum": 1}],
} # fmt: skip
sangeun = (B09 / "B09_Estimation_UI_Tabs_Sangeun.ts").read_text("utf-8")
src = re.search(r"^export const raw =.*?;$", sangeun, re.MULTILINE | re.DOTALL)[0] + "".join(
IMPORTS.sub("", (B09 / f"B09_Estimation_{n}.ts").read_text("utf-8"))
for n in ("UI_Tabs", "UI_Tabs_Labor")
grid = IMPORTS.sub("", (ROOT / "ui_template/ui_template_hopyo_grid.ts").read_text("utf-8"))
src = (
grid
+ re.search(r"^export const raw =.*?;$", sangeun, re.MULTILINE | re.DOTALL)[0]
+ "".join(
IMPORTS.sub("", (B09 / f"B09_Estimation_{n}.ts").read_text("utf-8"))
for n in ("UI_Tabs", "UI_Tabs_Labor")
)
) # 산근 탭의 풍선 함수 `raw` 만(같은 이름 상수가 겹쳐 파일째는 못 잇음)
body = f"""
const sheets = {json.dumps(sheets, ensure_ascii=False)};
@@ -131,7 +131,10 @@ def test_B08_B09_적용():
mat = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text("utf-8")
assert '=== row.name ? ""' not in mat and '=== row.group ? ""' not in mat
labor = (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts").read_text("utf-8")
assert 'const CHILD = "ㄴ "' in labor and '" 합 계"' not in labor
grid = (ROOT / "ui_template/ui_template_hopyo_grid.ts").read_text(
"utf-8"
) # 704-3 줄 글 = 공용 격자
assert 'HOPYO_CHILD = "ㄴ "' in grid and '" 합 계"' not in labor + grid
# 셸 칸 → 열 = cellColumn(병합 표에서 cellIndex 가 밀림)
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert ".cellIndex" not in shell and shell.count("cellColumn(") >= 2
@@ -15,7 +15,7 @@ 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
from resources.tester.helper_b09_group_cards import COMMON, GRID, GROUPS, RAW, TABS, run # noqa: E402
B09 = "B09_Estimation/B09_Estimation_UI_"
BAR = f"{B09}TabBar.ts"
@@ -74,7 +74,7 @@ console.log(JSON.stringify({
def test_묶음_탭_단추가_보조_줄에_안내는_아래에():
"""쉘이 하는 대로 — 줄 그림 → 묶음 판 그림(`setTools` = 보조 줄 도구) → 단추는 줄에, 판에는 없음."""
out = run(
[COMMON, TABS, f"{B09}Tabs_Labor.ts", GROUPS, BAR],
[COMMON, TABS, GRID, f"{B09}Tabs_Labor.ts", GROUPS, BAR],
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
"\nconst bars = createTabBars(() => {});"
"\nconst hint = document.createElement('p');"
@@ -14,7 +14,7 @@ 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
from resources.tester.helper_b09_group_cards import COMMON, GRID, GROUPS, RAW, TABS, run # noqa: E402
LABOR = "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts"
DOCK = "B09_Estimation/B09_Estimation_UI_Dock.ts"
@@ -33,7 +33,7 @@ SPEC = (
def test_한_설정_셋_탭_함께():
out = run(
[COMMON, TABS, LABOR, GROUPS],
[COMMON, TABS, GRID, LABOR, GROUPS],
SPEC + "\nconst a = kinds();"
"\nchooseGroupView('table');"
"\nconst b = kinds();"
@@ -59,7 +59,7 @@ def test_옛_탭별_값_첫_읽기에_한_값으로():
]
for old, want in cases:
out = run(
[COMMON, TABS, LABOR, GROUPS],
[COMMON, TABS, GRID, LABOR, GROUPS],
"\nkept.set('b09-group-view', "
+ json.dumps(json.dumps(old))
+ ");"
+142
View File
@@ -0,0 +1,142 @@
"""704-3 일위대가 엑셀 꼴 격자 — 공용 부품 `ui_template_hopyo_grid` 줄 꼴 · M02 문서 + calc → 격자 줄 값.
TS 소스를 이어 붙여 Node(타입 지움)로 돌림 — 값은 받은 그대로(단가 = 기준 ÷ 배 · 금액 = 반영 · 합계 = 반영합).
"""
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
GRID = "ui_template/ui_template_hopyo_grid.ts"
def _node(files, body, prelude=""):
src = "".join(IMPORTS.sub("", (ROOT / f).read_text("utf-8")) for f in files)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"],
input=prelude + src + body, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
LINE = {"name": "돌쌓기", "spec": "뒷길이 35cm", "qty": 2.09, "unit": "㎡", "tag": "품셈",
"prices": {"sum": 52995, "labor": 41250, "expense": 11745},
"amounts": {"sum": 110759.6, "labor": 86212.5, "expense": 24547.1}} # fmt: skip
def test_머리_구성_합계_줄_꼬리표_빈_줄():
blocks = [
{"key": "A", "no": 3, "name": "돌기슭막이", "unit": "m", "cite": "산림품셈 13-2",
"total": {"sum": 110759.6, "labor": 86212.5}, "lines": [LINE, {**LINE, "name": "잡재료",
"qty": None, "prices": {}, "amounts": {}}]},
{"key": "B", "name": "번호 없음", "unit": "식", "total": {}, "lines": [], "blank": 2},
] # fmt: skip
out = _node([GRID], f"\nconst g = hopyoGrid({json.dumps(blocks, ensure_ascii=False)});"
"\nconst cls = g.rows.map((_, i) => hopyoCellClass(g.rows, i, 'tag') ?? hopyoCellClass(g.rows, i, 'note'));"
"\nconsole.log(JSON.stringify({ rows: g.rows, titles: [...g.titles], at: [...g.at], cls,"
" cols: g.columns.map((c) => c.key) }));") # fmt: skip
rows = out["rows"]
assert [r["name"] for r in rows] == [
"제 3 호표 돌기슭막이", "ㄴ 산림품셈 13-2", "ㄴ 돌쌓기", "ㄴ 잡재료", "합 계", "번호 없음", "합 계",
] # fmt: skip
assert rows[0]["sum"] == 110759.6 # 머리 = 1 단위 합계
assert (
rows[2]["labor_price"] == 41250 and rows[2]["labor"] == 86212.5 and rows[2]["qty"] == 2.09
)
assert rows[2]["material_price"] is None and rows[2]["tag"] == "품셈"
assert rows[4]["note"] == "빈 줄 1" and rows[4]["blank_names"] == [
"잡재료"
] # 금액 모르는 줄에서 셈
assert rows[6]["note"] == "빈 줄 2" # 준 빈 줄 수 그대로
assert out["titles"] == [0, 4, 5, 6] and out["at"] == [[2, [0, 0]], [3, [0, 1]]]
assert out["cls"][2] == "ui-hopyo-grid__tag is-pum" and out["cls"][4] == "ui-hopyo-grid__blank"
assert out["cols"][-1] == "tag" and out["cols"][:4] == ["name", "spec", "qty", "unit"]
STUBS = r"""
const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%" })[k] ?? k;
const valueLine = (inputs, values) => ({ text: Object.values(values ?? {}).join(" · "), empty: 0 });
"""
DOC = {
"종류": "일위대가",
"열": "C1",
"일위대가번": "SU-000006",
"단위": "m",
"담은로직": [
{"로직": "GF1", "별칭": None, "입력값": {"뒷길이": "35cm"}, "수량": None, "켜쌓기율": 90},
{"로직": "GF2", "별칭": "고임돌", "입력값": {}, "수량": None},
],
"자재": [
{"코드": "MT1", "이름": "큰돌", "규격": "30cm", "단위": "개", "수량": None},
{
"코드": "MT2",
"이름": "철선",
"규격": "",
"단위": "kg",
"수량": None,
"별도계상": "직접계산",
},
],
}
CALC = {"줄": [{"로직": "GF1", "분모": "㎡", "배": 10, "수량": 2.09,
"기준": {"노무비": 412500, "재료비": 0, "경비": 117450, "계": 529950},
"반영": {"노무비": 86212.5, "재료비": 0, "경비": 24547.05, "계": 110759.55}},
{"로직": "GF2", "분모": "㎥", "배": 1, "수량": None,
"기준": {"노무비": 24816, "재료비": 0, "경비": 0, "계": 24816}, "반영": None}],
"자재": [{"코드": "MT1", "기준단가": 1200, "수량": 5, "반영": 6000},
{"코드": "MT2", "기준단가": 900, "수량": 2, "반영": 1800}],
"반영합": {"노무비": 86212.5, "재료비": 1800, "경비": 24547.05, "계": 112559.55}} # fmt: skip
def test_M02_문서_calc_를_격자_줄로():
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
logics = [
["GF1", {"이름": "돌쌓기 찰쌓기", "입력": [], "결과단위": "㎡"}],
["GF2", {"이름": "고임돌 채집"}],
]
out = _node(files, (
f"\nconst b = laborBlock({json.dumps(DOC, ensure_ascii=False)}, {json.dumps(CALC, ensure_ascii=False)},"
f" new Map({json.dumps(logics, ensure_ascii=False)}));"
"\nconst wait = laborBlock(" + json.dumps(DOC, ensure_ascii=False) + ", null, new Map());"
"\nconsole.log(JSON.stringify({ b, foot: hopyoGrid([b]).rows.at(-1), wait: hopyoGrid([wait]).rows.at(-1) }));"
), STUBS) # fmt: skip
b = out["b"]
assert b["name"] == "SU-000006" and b["unit"] == "m" and b["total"]["sum"] == 112559.55
one, two, mt1, mt2 = b["lines"]
assert (
one["name"] == "돌쌓기 찰쌓기"
and one["spec"] == "35cm · 켜쌓기 90%"
and one["unit"] == "㎡"
)
assert one["prices"] == {
"sum": 52995,
"labor": 41250,
"material": 0,
"expense": 11745,
} # 기준 ÷ 배
assert one["amounts"]["sum"] == 110759.55 and one["tag"] == "품셈"
assert two["name"] == "고임돌" and two["qty"] is None and two["note"] == "수량 없음"
assert mt1["prices"] == {"sum": 1200, "material": 1200} and mt1["note"] == "별도계상 · 합계 밖"
assert (
mt2["amounts"] == {"sum": 1800, "material": 1800}
and mt2["note"] == "MT2"
and "tag" not in mt2
)
assert out["foot"]["note"] == "빈 줄 1" and out["foot"]["blank_names"] == ["고임돌"]
assert out["wait"]["note"] == "" # 계산 중 = 빈 줄 알림 없음
def test_세_곳_같은_부품():
detail = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts").read_text("utf-8")
assert "laborGrid(cur, calc ?? last, logics)" in detail # 카드 · 상세 = 같은 Labor_Detail
m02 = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts").read_text("utf-8")
b09 = (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts").read_text("utf-8")
assert '"@ui/ui_template_hopyo_grid"' in m02 and '"@ui/ui_template_hopyo_grid"' in b09
kit = (ROOT / "ui_template/ui_template_ui_kit_parts.ts").read_text("utf-8")
assert 'key: "hopyo-grid"' in kit # 부품 모음 견본
+3 -1
View File
@@ -360,7 +360,9 @@ def test_좌측_판_자리와_권한():
def test_로직_추가_한_곳과_파일_규칙():
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
assert len(detail.splitlines()) == 693 # 줄 수 그대로(361-2 전국 자재 부르는 줄 둘 · 553-5 보조 글 부품 -2)
assert (
len(detail.splitlines()) == 700
) # 줄 수 그대로(361-2 전국 자재 부르는 줄 둘 · 553-5 보조 글 부품 -2 · 704-3 격자 +7)
assert "withHopyo(library)," in detail
pick = _text("M02_MasterTemplete_Labor_Pick.ts")
assert "more?: () => Promise<PickExtra[]>;" in pick and "...later" in pick