- 셈 짝(Py · TS) 새 옵션 — 착공일(그날부터 · A 날수 비례) · 기후 자료 고르기 · 쉬는 날 · 공휴일 빼기 · 투입 기본 · 반영률 기본 · 주 장비 기준(장비 시간 합) · 중복 C 끝수 · 마지막 달 비작업 - 원천 = 기후 셋(거창 25 · 울진 24 · 울진 소광 25 · 0에서 직접) · 단위당 장비 시간 합 - clean_schedule 새 칸(모르는 값 · 범위 밖 버림) - 시험 test_264_2 — 기본값 = 지금 결과 · 옵션마다 값 바뀜 · 거울 TS = Py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
211 lines
8.7 KiB
Python
211 lines
8.7 KiB
Python
"""PLAN 264 — B09 공기 옵션(착공일 · 기후 자료 · 쉬는 날 · 공휴일 빼기 · 투입 · 준비 정리 · 반영률 ·
|
||
주 장비 기준 · 중복 C 끝수 · 마지막 달) — 옵션마다 결과가 바뀜 · 다듬기 · 거울 TS = Py · 패널 훅."""
|
||
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT))
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
|
||
from B09_Estimation.draft.B09_Draft_Schedule_Calc import compute_schedule # noqa: E402
|
||
from B09_Estimation.draft.B09_Draft_Schedule_Source import ( # noqa: E402
|
||
CLIMATES,
|
||
clean_schedule,
|
||
schedule_inputs,
|
||
unit_resources,
|
||
)
|
||
from test_252_1_b09_schedule import GEOCHANG, SRC, _bill, _ref_user, _ts_schedule # noqa: E402
|
||
|
||
SRC2 = {**SRC, "climates": CLIMATES}
|
||
UNITS = {
|
||
"a": {
|
||
"machine": {"key": "M1", "name": "굴착기 0.7㎥", "hr": 0.0066},
|
||
"labor": 0.01,
|
||
"machine_sum": {"hr": 0.0086, "n": 2},
|
||
},
|
||
"b": {"machine": None, "labor": 1.5},
|
||
}
|
||
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": "h", "qty": 9, "section": "3", "level": 2},
|
||
]
|
||
SRC3 = {**SRC2, "units": UNITS}
|
||
YEAR_2026 = {"start": {"year": 2026, "month": 1}, "tidy": 400}
|
||
|
||
|
||
def _plain():
|
||
"""거창 재현 사용자 값에서 법정공휴일 덮개만 뺌(자동 B 를 봄)."""
|
||
user = _ref_user()
|
||
del user["holiday"]
|
||
return user
|
||
|
||
|
||
def _cal(user, src=SRC2, bill=None):
|
||
return compute_schedule(src, _bill(GEOCHANG) if bill is None else bill, user)["calendar"]
|
||
|
||
|
||
def _rows(user):
|
||
return {r["key"]: r for r in compute_schedule(SRC3, BILL, user)["work"]["rows"]}
|
||
|
||
|
||
def test_기본값_그대로():
|
||
base = compute_schedule(SRC, _bill(GEOCHANG), _ref_user())
|
||
assert compute_schedule(SRC2, _bill(GEOCHANG), _ref_user()) == base
|
||
picked = {**_ref_user(), "climate_set": "geochang25", "rest": "sun", "machine_basis": "max",
|
||
"c_round": "none", "last_off": "ratio", "holiday_off": [], "units": 1, "crew": 10,
|
||
"rate_haul": 0, "rate_base": 100, "hours": 8, "prep_docs": 2, "prep_setup": 2,
|
||
"tidy": 4} # fmt: skip
|
||
assert compute_schedule(SRC2, _bill(GEOCHANG), picked) == base
|
||
assert (base["work"]["total"], base["calendar"]["days"]) == (95, 136)
|
||
|
||
|
||
def test_1_착공일_그날부터():
|
||
user = {**_plain(), "start": {"year": 2026, "month": 3, "day": 15}}
|
||
cal = _cal(user)
|
||
first = cal["months"][0]
|
||
# 3.15 ~ 3.31 = 17일 · A = ROUNDUP(3.6 × 17 ÷ 31, 1) = 2.0 · 일요일 15 · 22 · 29
|
||
assert (first["days"], first["a"], first["b_auto"]) == (17, 2.0, 3)
|
||
assert cal["start"] == {"year": 2026, "month": 3, "day": 15}
|
||
assert cal["end"] > _cal(_plain())["end"]
|
||
assert cal["months"][1]["days"] == 30 # 둘째 달부터 달 전체
|
||
|
||
|
||
def test_2_기후_자료_고르기():
|
||
m = _cal({**_plain(), "climate_set": "uljin24"})["months"][0]
|
||
assert (m["rain"], m["heat"]) == (2.9, 0)
|
||
m = _cal({**_plain(), "climate_set": "sogwang25"})["months"][5 - 3]
|
||
assert (m["rain"], m["heat"]) == (2.1, 0) # 5월
|
||
assert all(m["a"] == 0 for m in _cal({**_plain(), "climate_set": "zero"})["months"])
|
||
user = {**_plain(), "climate_set": "uljin24", "climate": {"rain": [9] * 12}}
|
||
assert _cal(user)["months"][0]["rain"] == 9 # 달마다 덮개가 이김
|
||
|
||
|
||
def test_3_쉬는_날_토일():
|
||
base = _cal(_plain())["months"][0]["b_auto"]
|
||
assert _cal({**_plain(), "rest": "satsun"})["months"][0]["b_auto"] == base + 4 # 2026.3 토 4번
|
||
|
||
|
||
def test_4_공휴일_빼기():
|
||
def auto(off):
|
||
cal = _cal({**YEAR_2026, "holiday_off": off}, bill=[])
|
||
return [m["b_auto"] for m in cal["months"][:12]]
|
||
|
||
full = auto([])
|
||
assert full == [5, 7, 6, 4, 8, 6, 5, 7, 7, 7, 5, 5]
|
||
assert auto(["labor"])[4] == 7 # 5.1 노동절
|
||
assert auto(["substitute"])[4] == 7 # 5.25 부처님오신날 대체
|
||
assert auto(["election"])[5] == 5 # 6.3 지방선거
|
||
assert auto(["constitution"])[6] == 4 # 7.17 제헌절
|
||
may = _cal({**YEAR_2026, "holiday_off": ["labor"]}, bill=[])["months"][4]["names"]
|
||
assert not any("노동절" in n for n in may)
|
||
|
||
|
||
def test_5_일_작업시간():
|
||
assert _rows({})["a"]["days"] == 0.9
|
||
assert _rows({"hours": 4})["a"]["days"] == 1.7 # 1000 × 0.0066 ÷ 4 = 1.65
|
||
|
||
|
||
def test_6_투입_기본():
|
||
rows = _rows({"units": 2, "crew": 5})
|
||
assert (rows["a"]["put"], rows["a"]["days"]) == (2, 0.5) # 0.4125 → 0.5
|
||
assert (rows["b"]["put"], rows["b"]["days"]) == (5, 24)
|
||
rows = _rows({"crew": 5, "rows": {"b": {"crew": 20}}})
|
||
assert rows["b"]["days"] == 6 # 줄마다 덮개가 이김
|
||
|
||
|
||
def test_7_준비_정리_기타():
|
||
work = compute_schedule(SRC3, BILL, {"prep_docs": 3, "prep_setup": 1, "tidy": 6, "etc": 2})
|
||
w = work["work"]
|
||
assert (w["prep"], w["tidy"], w["etc"]) == (4, 6, 2)
|
||
assert w["total"] == 4 + round(0.9 + 12 + 0 + 2) + 6
|
||
|
||
|
||
def test_8_반영률_기본():
|
||
rows = _rows({"rate_haul": 100, "rate_base": 50})
|
||
assert (rows["h"]["rate"], rows["a"]["rate"], rows["b"]["rate"]) == (100, 50, 50)
|
||
assert rows["b"]["days"] == 6
|
||
assert _rows({"rate_base": 50, "rows": {"b": {"rate": 100}}})["b"]["days"] == 12
|
||
|
||
|
||
def test_9_주_장비_장비_시간_합():
|
||
a = _rows({"machine_basis": "sum"})["a"]
|
||
assert (a["per"], a["who"], a["days"]) == (0.0086, "굴착기 0.7㎥ 외 1종", 1.1) # 8.6 ÷ 8
|
||
assert _rows({})["a"]["per"] == 0.0066
|
||
|
||
|
||
def test_10_중복일수_끝수():
|
||
base = _cal(_plain())["months"]
|
||
got = _cal({**_plain(), "c_round": "round1"})["months"]
|
||
assert all(m["c"] == round(m["c"], 1) for m in got)
|
||
assert [m["c"] for m in got] != [m["c"] for m in base]
|
||
|
||
|
||
def test_11_마지막_달_비작업_전부():
|
||
base = _cal(_ref_user())["months"][-1]
|
||
last = _cal({**_ref_user(), "last_off": "all"})
|
||
assert (base["off_used"], base["off"]) == (7, 15)
|
||
assert last["months"][-1]["off_used"] == 15 and last["days"] == 136 + 8
|
||
|
||
|
||
def test_다듬기_새_칸():
|
||
raw = {
|
||
"units": 2, "crew": 0, "rate_haul": 50, "rate_base": 120,
|
||
"climate_set": "uljin24", "rest": "sat", "machine_basis": "sum",
|
||
"c_round": "round1", "last_off": "x", "holiday_off": ["labor", "junk", "election"],
|
||
"start": {"year": 2027, "month": 2, "day": 30},
|
||
} # fmt: skip
|
||
assert clean_schedule(raw) == {
|
||
"rate_haul": 50,
|
||
"units": 2,
|
||
"climate_set": "uljin24",
|
||
"machine_basis": "sum",
|
||
"c_round": "round1",
|
||
"holiday_off": ["election", "labor"],
|
||
"start": {"year": 2027, "month": 2},
|
||
}
|
||
got = clean_schedule({"start": {"year": 2027, "month": 3, "day": 15}, "holiday_off": "x"})
|
||
assert got == {"start": {"year": 2027, "month": 3, "day": 15}}
|
||
|
||
|
||
def test_원천_기후_셋_장비_합():
|
||
sheets = {
|
||
"bill": {"rows": [{"kind": "item", "key": "earth:4", "ref": {"tab": "sg", "key": "S1"}}]},
|
||
"sg": [{"key": "S1", "lines": [
|
||
{"kind": "machine", "ref": {"key": "M1"}, "qty": 0.0066},
|
||
{"kind": "machine", "ref": {"key": "M2"}, "qty": 0.002},
|
||
]}],
|
||
"machine": [],
|
||
} # fmt: skip
|
||
unit = unit_resources(sheets)["earth:4"]
|
||
assert unit["machine_sum"]["n"] == 2 and round(unit["machine_sum"]["hr"], 6) == 0.0086
|
||
got = schedule_inputs(sheets)
|
||
assert list(got["climates"]) == ["geochang25", "uljin24", "sogwang25", "zero"]
|
||
assert got["climates"]["geochang25"]["rain"] == got["climate"]["rain"]
|
||
|
||
|
||
def test_거울_TS_옵션():
|
||
cases = [
|
||
(SRC2, _bill(GEOCHANG), {**_plain(), "start": {"year": 2026, "month": 3, "day": 15},
|
||
"climate_set": "sogwang25", "rest": "satsun",
|
||
"holiday_off": ["substitute", "constitution"],
|
||
"c_round": "round1", "last_off": "all"}),
|
||
(SRC3, BILL, {"machine_basis": "sum", "units": 3, "crew": 7, "rate_haul": 30,
|
||
"rate_base": 80, "hours": 9, "prep_docs": 1, "tidy": 2, "etc": 1,
|
||
"climate_set": "zero", "start": {"year": 2027, "month": 12, "day": 31}}),
|
||
] # 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_패널_훅_draft_없으면_빈칸():
|
||
tabs = (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs.ts").read_text("utf-8")
|
||
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||
ui = (ROOT / "B09_Estimation/draft/B09_Draft_Schedule_UI.ts").read_text("utf-8")
|
||
assert "{ builders: {}, extend: [], panels: [] }" in tabs # draft 없으면 카드 없음
|
||
assert "...DRAFT.panels.map((make) => make(() => user, setUser))" in shell
|
||
assert "DRAFT.panels.push(" in ui
|