Files
Aislo/resources/tester/test_252_1_b09_schedule.py
T
eomsangdonandClaude Opus 5.5 f87831533c feat(b09): 공기 초안 — 메인 탭 공기 · 공기집계 · 공기산출 (draft 폴더 · 252)
설계내역서 항목 줄 역전개(단위당 장비 시간 · 인) → 작업일수 · 착공 연월부터 달마다 작업가능일 채움 · 공휴일 표 2024~2040 · 본 코드 훅은 draft 없으면 건너뜀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
2026-10-11 01:51:32 +09:00

213 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""PLAN 252 — B09 공기 초안(공기산출 · 공기집계 셈 짝 · 서버 원천 · 사용자 값 다듬기)."""
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B09_Estimation.draft.B09_Draft_Schedule_Calc import compute_schedule # noqa: E402
from B09_Estimation.draft.B09_Draft_Schedule_Source import ( # noqa: E402
clean_schedule,
holiday_table,
schedule_inputs,
unit_resources,
)
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
#: 참고 설계서(25 거창) 공기산출 — [이름, 시공량, 일작업량(= 8 × Q)] · 기타공 2일
GEOCHANG = [
("토사절취", 7641, 1206.4), ("암석절취", 2669, 160), ("암석절취", 297, 48),
("측구터파기", 110, 362.24), ("측구터파기", 103, 20), ("지장목제거", 10782, 4519.76),
("초류종자파종", 2718.4, 2400), ("전석메쌓기", 540, 74.4), ("유입구골막이", 4, 4),
("유출구물받이", 16, 6), ("터파기", 757, 838.24), ("터파기", 57, 80), ("터파기", 57, 48),
("되메우기", 523, 904.16), ("관구체다짐", 324, 180), ("파형강관매설", 185, 160),
("배수관날개벽", 16, 4), ("BOX암거", 5, 0.8), ("BOX암거 난간", 13, 6.4),
("콘크리트포장", 2170, 200), ("혼합석부설", 2870, 1108), ("노견줄떼", 820, 255.2),
] # fmt: skip
#: 같은 설계서 공기집계 — 기후(강수 + 혹서) · 법정공휴일(2026 기준 · 그 설계서 값)
B_REF = [5, 7, 6, 4, 7, 5, 4, 7, 7, 7, 5, 5]
def _bill(rows):
out = [{"kind": "title", "key": "sec:1", "name": "토사", "level": 1, "section": "1"}]
for i, (name, qty, _d) in enumerate(rows):
out.append({"kind": "item", "key": f"r{i}", "name": name, "qty": qty, "level": 2,
"section": "1.2", "unit": "㎥"}) # fmt: skip
return out
def _ref_user():
return {
"rows": {f"r{i}": {"daily": d} for i, (_n, _q, d) in enumerate(GEOCHANG)},
"etc": 2,
"start": {"year": 2026, "month": 3},
"holiday": {f"2026-{m:02d}": b for m, b in enumerate(B_REF, 1)},
}
SRC = {
"units": {},
"holidays": holiday_table(),
"climate": {
"rain": [1.2, 1.4, 3.6, 4.5, 3.6, 4.4, 7.7, 6.9, 5.5, 3.1, 2.2, 1.2],
"heat": [0, 0, 0, 0, 0.5, 1.2, 5.9, 8.4, 0, 0, 0, 0],
},
"start": {"year": 2027, "month": 3},
}
def test_참고_설계서_거창_공기산출_재현():
work = compute_schedule(SRC, _bill(GEOCHANG), _ref_user())["work"]
days = [r["days"] for r in work["rows"] if r["kind"] == "item"]
assert days[:6] == [6.4, 16.7, 6.2, 0.4, 5.2, 2.4] # 설계서 F7 · F8 · F9 · F10 · F11 · F14
assert days[-3:] == [10.9, 2.6, 3.3] # F37 · F38 · F39
assert (work["prep"], work["subtotal"], work["tidy"], work["total"]) == (4, 87, 4, 95)
assert [r["kind"] for r in work["rows"][:2]] == ["head", "item"] # 묶음 줄 먼저
def test_참고_설계서_거창_작업가능일_채움():
cal = compute_schedule(SRC, _bill(GEOCHANG), _ref_user())["calendar"]
months = cal["months"]
# 설계서 3 ~ 7월 작업가능일수(달력 − ROUNDDOWN(A + B − A×B÷달력))
assert [m["avail"] for m in months] == [23, 23, 21, 21, 16]
assert [(m["prep"], m["work"], m["tidy"]) for m in months] == [
(4, 19, 0), (0, 23, 0), (0, 21, 0), (0, 21, 0), (0, 3, 4),
] # fmt: skip
# 다 쓴 달 = 비작업 전부 · 마지막 달 = ROUNDUP(15 × 7 ÷ 16) = 7
assert [m["off_used"] for m in months] == [8, 7, 10, 9, 7]
assert (cal["days"], cal["months_n"], cal["end"]) == (136, 4.5, "2026-07-14")
def test_공휴일_자동_2026():
cal = compute_schedule(SRC, [], {"start": {"year": 2026, "month": 1}, "tidy": 400})
auto = [m["b_auto"] for m in cal["calendar"]["months"][:12]]
# 설계서 값과 다른 달 = 5월 노동절 · 6월 지방선거 · 7월 제헌절(공휴일 법 바뀜 · 목록이 넣음)
assert auto == [5, 7, 6, 4, 8, 6, 5, 7, 7, 7, 5, 5]
may = cal["calendar"]["months"][4]["names"]
assert "5일 어린이날" in may and "25일 부처님오신날 대체 휴일" in may
assert cal["calendar"]["months"][1]["days"] == 28 # 2026.2 = 28일(설계서는 29)
def test_장비_인력_입력_반영률():
src = {**SRC, "units": {
"a": {"machine": {"key": "M1", "name": "굴착기 0.7㎥", "hr": 0.0066}, "labor": 0.01},
"b": {"machine": None, "labor": 1.5},
"c": {"machine": None, "labor": None},
}} # fmt: skip
bill = [
{"kind": "item", "key": "a", "qty": 1000, "section": "1.2", "level": 2},
{"kind": "item", "key": "b", "qty": 80, "section": "2.1", "level": 2},
{"kind": "item", "key": "c", "qty": 5, "section": "2.3", "level": 2},
{"kind": "item", "key": "h", "qty": 9, "section": "3", "level": 2},
{"kind": "item", "key": "mat:x", "qty": 9, "section": "4", "level": 2},
{"kind": "item", "key": "z", "qty": 0, "section": "1.2", "level": 2},
]
rows = {r["key"]: r for r in compute_schedule(src, bill, {})["work"]["rows"]}
a, b, c, h = rows["a"], rows["b"], rows["c"], rows["h"]
# 장비 = 1000 × 0.0066 ÷ 8 = 0.825 → 0.9 · Q = 1 ÷ 0.0066
assert (a["basis"], a["days"], round(a["q"], 2), a["put"]) == ("장비", 0.9, 151.52, 1)
assert (b["basis"], b["days"], b["put"]) == ("인력", 12, 10) # 80 × 1.5 ÷ 10
assert (c["basis"], c["days"]) == ("연결 없음", None)
assert (h["rate"], h["days"]) == (0, 0) # 운반공 기본 0 = 단위당 자원 없어도 0일
assert "mat:x" not in rows and "z" not in rows # 자재대 · 수량 0 = 줄 없음
user = {"rows": {"a": {"units": 2, "rate": 50}, "b": {"crew": 5}, "c": {"daily": 2}}}
rows = {r["key"]: r for r in compute_schedule(src, bill, user)["work"]["rows"]}
assert rows["a"]["days"] == 0.3 # 1000 × 0.5 × 0.0066 ÷ 16 = 0.20625 → 0.3
assert rows["b"]["days"] == 24 and (rows["c"]["basis"], rows["c"]["days"]) == ("입력", 2.5)
def _ts_schedule(src, bill, user):
base = IMPORTS.sub("", (ROOT / "B09_Estimation/B09_Estimation_Calc_Base.ts").read_text("utf-8"))
calc = (ROOT / "B09_Estimation/draft/B09_Draft_Schedule_Calc.ts").read_text("utf-8")
js = (
base + IMPORTS.sub("", calc)
+ f"\nconsole.log(JSON.stringify(computeSchedule({json.dumps(src, ensure_ascii=False)}, "
f"{json.dumps(bill, ensure_ascii=False)}, {json.dumps(user, ensure_ascii=False)})));"
) # fmt: skip
run = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"],
input=js, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
def test_거울_TS_가_같은_값():
src = {**SRC, "units": {"r0": {"machine": {"key": "M", "name": "굴", "hr": 0.0066}},
"r1": {"labor": 1.5}}} # fmt: skip
cases = [
(SRC, _bill(GEOCHANG), _ref_user()),
(src, _bill(GEOCHANG[:4]), {"rows": {"r0": {"rate": 50}},
"start": {"year": 2027, "month": 11},
"climate": {"rain": [9] * 12, "heat": [None] * 12}}),
(SRC, [], {}),
] # fmt: skip
for src_, bill, user in cases:
py = json.loads(json.dumps(compute_schedule(src_, bill, user), ensure_ascii=False))
assert _ts_schedule(src_, bill, user) == py
def test_사용자_값_다듬기():
raw = {
"hours": 9, "tidy": -1, "etc": "x", "start": {"year": 2027, "month": 13},
"climate": {"rain": [1] * 12, "heat": [1, 2]},
"holiday": {"2027-05": 6, "bad": 3, "2027-06": 40},
"rows": {"a": {"rate": 50, "units": 0, "crew": 5, "daily": None, "x": 1},
"b": {"rate": 120}},
"junk": 1,
} # fmt: skip
assert clean_schedule(raw) == {
"hours": 9,
"climate": {"rain": [1] * 12},
"holiday": {"2027-05": 6},
"rows": {"a": {"rate": 50, "crew": 5}},
}
assert clean_schedule(None) == {}
from B09_Estimation.B09_Estimation_Store import clean_user
assert clean_user({"schedule": {"tidy": 3}})["schedule"] == {"tidy": 3}
assert "schedule" not in clean_user({"schedule": {"junk": 1}})
def test_서버_원천_단위당_자원():
sheets = {
"bill": {"rows": [
{"kind": "item", "key": "earth:4", "ref": {"tab": "sg", "key": "S1"}},
{"kind": "item", "key": "hand:x", "ref": {"tab": "dg", "key": "D1"}},
{"kind": "item", "key": "earth:5", "ref": None},
]},
"sg": [{"key": "S1", "lines": [
{"kind": "machine", "ref": {"key": "M1"}, "qty": 0.0066},
{"kind": "machine", "ref": {"key": "M2"}, "qty": 0.002},
{"kind": "labor", "ref": {"key": "L1"}, "qty": 0.01},
]}],
"daega_blocks": [{"key": "D1", "lines": [
{"kind": "labor", "ref": {"key": "L1"}, "qty": 0.5},
{"kind": "sangeun", "ref": {"key": "S1"}, "qty": 2},
{"kind": "labor", "ref": {"key": "L2"}, "qty": 1.0},
]}],
"machine": [{"key": "M1", "name": "굴착기", "spec": "0.7㎥"}],
} # fmt: skip
units = unit_resources(sheets)
assert units["earth:4"]["machine"] == {"key": "M1", "name": "굴착기 0.7㎥", "hr": 0.0066}
assert units["earth:4"]["labor"] == 0.01
# 겹 호표 = 끝까지(산근 2 단위 → 굴착기 0.0132 시간 · 인 0.5 + 0.02 + 1.0)
assert round(units["hand:x"]["machine"]["hr"], 6) == 0.0132
assert round(units["hand:x"]["labor"], 6) == 1.52
assert "earth:5" not in units
got = schedule_inputs(sheets, __import__("datetime").date(2026, 10, 11))
assert got["start"] == {"year": 2027, "month": 3} and "2027" in got["holidays"]
def test_본_코드_훅_지워도_돌게():
store = (ROOT / "B09_Estimation/B09_Estimation_Store.py").read_text("utf-8")
router = (ROOT / "B09_Estimation/B09_Estimation_Router.py").read_text("utf-8")
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "except ImportError" in store and "except ImportError" in router
assert 'import.meta.glob("./draft/*_UI.ts", { eager: true })' in shell