Files
Aislo/B09_Estimation/draft/B09_Draft_Schedule_Calc.py
T
eomsangdonandClaude Opus 5.5 fe2ed66268 feat(B09): 264-2 공기 옵션 셈 짝 · 다듬기 · 시험
- 셈 짝(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
2026-10-11 16:40:21 +09:00

315 lines
13 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.
"""B09 공기 셈 짝(PLAN 252-1 · 사용자 161번 · 초안 — draft 폴더째 지우면 사라짐).
짝 = `B09_Draft_Schedule_Calc.ts`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py` · `test_264_2_b09_schedule_options.py`).
근거 = 실무 설계서 공기 탭 식(거창 25 · 울진 24 · 울진소광 25) — 값 베끼기가 아니라 식만:
공기산출 설계내역서 항목 줄마다 작업일수 = ROUNDUP(시공량 × 반영률 ÷ 일작업량, 1)
일작업량 = 장비 — 일 작업시간 × 대수 ÷ 단위당 주 장비 시간
(= 일 작업시간 × Q · Q = 1 ÷ 단위당 시간)
인력 — 투입 인원 ÷ 단위당 인(장비 없는 줄)
입력 — 사용자 값(연결 없는 줄 · 덮어쓰기)
단위당 자원 = 서버 역전개(`…_Source.unit_resources` · 호표 구성 1 단위)
반영률 = 다른 공종과 겹침(100 = 혼자 · 50 = 반 겹침 · 0 = 다 겹침) · 운반공 기본 0
소계 = ROUND(Σ 작업일수 + 기타, 0) · 총작업일수 = ROUND(준비 + 소계 + 정리, 0)
공기집계 착공일부터 달마다 — 달력일수 · 기후여건 A = ROUNDUP(강수일 + 혹서일, 1) ·
법정공휴일 B = 일요일 + 공휴일(대체 · 선거일 포함 · 사용자 덮개) ·
중복 C = A × B ÷ 달력 ·
비작업일수 = ROUNDDOWN(A + B − C, 0) · 작업가능일수 = 달력 − 비작업
준비 → 작업 → 정리 차례로 작업가능일수를 채움 · 다 쓴 달 = 그 달 비작업 전부 ·
마지막 달(덜 쓴 달) = ROUNDUP(비작업 × 쓴 날 ÷ 작업가능일, 0)
공사기간 = Σ(쓴 날 + 비작업) · 준공 예정일 = 착공일 + 공사기간 − 1
옵션(264 · user.schedule · 기본값 = 위 식 그대로) — 착공일(1일 아니면 첫 달 = 그날부터 ·
A = 날수 비례) · 기후 자료(원천 climates 고르기 · 달마다 덮개가 이김) ·
쉬는 날(일 · 토일) · 공휴일 빼기(대체 · 선거일 · 노동절 · 제헌절 — 이름으로 가름) ·
투입 기본(장비 대 · 인력 인) · 반영률 기본(운반공 · 그 밖) ·
주 장비 기준(가장 긴 장비 · 장비 시간 합) · 중복 C 끝수(그대로 · 소수 1자리 반올림) ·
마지막 달 비작업(쓴 비율 올림 · 전부)
"""
from __future__ import annotations
import datetime
from typing import Any
from B09_Estimation.B09_Estimation_Calc_Base import num, round_half, rounddown, roundup
#: 사용자 값 기본(264 패널 · 표 칸 같은 값)
DEFAULTS = {
"hours": 8,
"units": 1,
"crew": 10,
"prep_docs": 2,
"prep_setup": 2,
"tidy": 4,
"etc": 0,
"rate_haul": 0,
"rate_base": 100,
}
#: 공휴일 빼기 갈래 — 이름에 든 글
HOLIDAY_KINDS = {
"substitute": ("대체",),
"election": ("선거",),
"labor": ("노동절", "근로자"),
"constitution": ("제헌절",),
}
#: 공사 아닌 공종(자재대 · 관급자재대) · 기본 반영률 운반공(= 다른 공종과 겹침)
SKIP_TOP = ("4", "G")
OVERLAP_TOP = ("3",)
MAX_MONTHS = 120
def _top(section: Any) -> str:
return str(section or "").split(".")[0]
def _cfg(user: dict[str, Any], key: str) -> float:
value = num(user.get(key))
return DEFAULTS[key] if value is None or value < 0 else value
def _work_row(item: dict[str, Any], unit: dict[str, Any] | None, cfg: dict, base: dict) -> dict:
hours = base["hours"]
qty = num(item.get("qty")) or 0.0
rate = num(cfg.get("rate"))
if rate is None:
rate = base["rate_haul"] if _top(item.get("section")) in OVERLAP_TOP else base["rate_base"]
daily_in = num(cfg.get("daily"))
machine = (unit or {}).get("machine")
total = (unit or {}).get("machine_sum")
if machine and base["sum"] and total and (num(total.get("hr")) or 0) > 0:
many = f" 외 {total['n'] - 1}종" if (total.get("n") or 1) > 1 else ""
machine = {**machine, "name": f"{machine.get('name') or ''}{many}", "hr": total["hr"]}
labor = num((unit or {}).get("labor"))
out = {
"kind": "item",
"key": item["key"],
"name": item.get("name") or "",
"spec": item.get("spec") or "",
"unit": item.get("unit") or "",
"lv": item.get("level") or 1,
"qty": qty,
"rate": rate,
"basis": "",
"who": "",
"per": None,
"put": None,
"q": None,
"daily": None,
"daily_in": daily_in,
"days": None,
}
if daily_in is not None and daily_in > 0:
out.update(basis="입력", daily=daily_in)
need = qty * rate / 100 / daily_in
elif machine and (num(machine.get("hr")) or 0) > 0:
hr = num(machine["hr"])
units = num(cfg.get("units")) or base["units"]
out.update(basis="장비", who=machine.get("name") or "", per=hr, put=units)
out.update(q=1 / hr, daily=hours * units / hr)
need = qty * rate / 100 * hr / (hours * units)
elif labor is not None and labor > 0:
crew = num(cfg.get("crew")) or base["crew"]
out.update(basis="인력", who="인력", per=labor, put=crew, daily=crew / labor)
need = qty * rate / 100 * labor / crew
else:
out["basis"] = "연결 없음"
out["days"] = 0.0 if rate == 0 else None # 다 겹침 = 일작업량 없어도 0
return out
out["days"] = roundup(need, 1)
return out
def work_table(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict) -> dict:
"""공기산출 — 설계내역서 차례(묶음 줄 = 그 아래 셀 줄이 있을 때만) · 준비 · 기타 · 정리."""
hours = _cfg(user, "hours") or DEFAULTS["hours"]
base = {
"hours": hours,
"units": _cfg(user, "units") or DEFAULTS["units"],
"crew": _cfg(user, "crew") or DEFAULTS["crew"],
"rate_haul": min(_cfg(user, "rate_haul"), 100),
"rate_base": min(_cfg(user, "rate_base"), 100),
"sum": user.get("machine_basis") == "sum",
}
units = src.get("units") or {}
cfgs = user.get("rows") or {}
rows: list[dict[str, Any]] = []
pending: list[list[Any]] = [] # [줄, 넣음]
for row in bill_rows:
level = row.get("level") or 1
if row.get("kind") != "item":
pending = [p for p in pending if (p[0].get("level") or 1) < level]
pending.append([row, False])
continue
key = str(row.get("key") or "")
qty = num(row.get("qty"))
if (
_top(row.get("section")) in SKIP_TOP
or key.startswith("mat:")
or key == "fee"
or qty is None
or qty <= 0
):
continue
for p in pending:
if not p[1]:
p[1] = True
head = p[0]
rows.append(
{
"kind": "head",
"key": head["key"],
"name": head.get("name") or "",
"lv": head.get("level") or 1,
} # fmt: skip
)
rows.append(_work_row(row, units.get(key), cfgs.get(key) or {}, base))
docs = _cfg(user, "prep_docs")
setup = _cfg(user, "prep_setup")
etc = _cfg(user, "etc")
tidy = _cfg(user, "tidy")
days = sum(r["days"] or 0 for r in rows if r["kind"] == "item")
subtotal = round_half(days + etc, 0)
missing = sum(1 for r in rows if r["kind"] == "item" and r["days"] is None)
return {
"hours": hours,
"rows": rows,
"prep_docs": docs,
"prep_setup": setup,
"prep": docs + setup,
"etc": etc,
"subtotal": subtotal,
"tidy": tidy,
"total": round_half(docs + setup + subtotal + tidy, 0),
"missing": missing,
}
def _month_days(year: int, month: int) -> int:
nxt = datetime.date(year + month // 12, month % 12 + 1, 1)
return (nxt - datetime.date(year, month, 1)).days
def _counted(name: str, off: list[str]) -> bool:
"""공휴일 넣음 — 「;」 로 묶인 이름 중 하나라도 뺀 갈래가 아니면 넣음."""
for part in name.split(";"):
kinds = [k for k, words in HOLIDAY_KINDS.items() if any(w in part for w in words)]
if not any(k in off for k in kinds):
return True
return False
def _holidays(
src: dict[str, Any], year: int, month: int, first: int, user: dict[str, Any]
) -> tuple[int, list[str]]:
"""그 달(first 일부터) 법정공휴일 수(쉬는 요일 + 공휴일 겹침 한 번) · 공휴일 이름(「일 이름」)."""
head = f"{year:04d}-{month:02d}-"
off = user.get("holiday_off") or []
named = {
d: n
for d, n in (src.get("holidays") or {}).get(str(year)) or []
if d.startswith(head) and int(d[-2:]) >= first and _counted(n, off)
}
rest = (5, 6) if user.get("rest") == "satsun" else (6,)
count = 0
for day in range(first, _month_days(year, month) + 1):
iso = f"{head}{day:02d}"
if datetime.date(year, month, day).weekday() in rest or iso in named:
count += 1
return count, [f"{int(d[-2:])}일 {n}" for d, n in sorted(named.items())]
def climate_base(src: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
"""고른 기후 자료(원천 climates) · 없으면 원천 기본값."""
return (src.get("climates") or {}).get(user.get("climate_set")) or src.get("climate") or {}
def _climate(src: dict[str, Any], user: dict[str, Any], kind: str, month: int) -> float:
mine = ((user.get("climate") or {}).get(kind) or [None] * 12)[month - 1]
if num(mine) is not None:
return num(mine)
return num((climate_base(src, user).get(kind) or [0] * 12)[month - 1]) or 0.0
def month_table(src: dict[str, Any], user: dict[str, Any], parts: list[float]) -> dict[str, Any]:
"""공기집계 — parts = [준비, 작업, 정리] 일수 · 착공일부터 채움."""
start = {**(src.get("start") or {}), **(user.get("start") or {})}
year, month = int(start.get("year") or 2000), int(start.get("month") or 1)
first = min(max(int(start.get("day") or 1), 1), _month_days(year, month))
begin = datetime.date(year, month, first)
left = [max(p, 0) for p in parts]
months: list[dict[str, Any]] = []
total = 0
while sum(left) > 0 or not months:
if len(months) >= MAX_MONTHS:
break
full = _month_days(year, month)
days = full - first + 1
rain = _climate(src, user, "rain", month)
heat = _climate(src, user, "heat", month)
a = roundup(rain + heat if first == 1 else (rain + heat) * days / full, 1)
auto, names = _holidays(src, year, month, first, user)
mine = num((user.get("holiday") or {}).get(f"{year:04d}-{month:02d}"))
b = mine if mine is not None else auto
c = a * b / days
if user.get("c_round") == "round1":
c = round_half(c, 1)
off = rounddown(a + b - c, 0)
avail = days - off
cap = max(avail, 0)
use = [0.0, 0.0, 0.0]
room = cap
for i in range(3):
take = min(room, left[i])
use[i] = take
left[i] -= take
room -= take
used = sum(use)
if cap <= 0:
off_used = days
elif used >= cap or user.get("last_off") == "all":
off_used = off
else:
off_used = roundup(off * used / cap, 0)
span = used + off_used
total += span
months.append(
{
"year": year,
"month": month,
"days": days,
"rain": rain,
"heat": heat,
"a": a,
"b": b,
"b_auto": auto,
"names": names,
"c": c,
"off": off,
"avail": avail,
"prep": use[0],
"work": use[1],
"tidy": use[2],
"used": used,
"off_used": off_used,
"span": span,
} # fmt: skip
)
year, month = (year + 1, 1) if month == 12 else (year, month + 1)
first = 1
end = begin + datetime.timedelta(days=max(total, 1) - 1)
return {
"start": {"year": begin.year, "month": begin.month, "day": begin.day},
"months": months,
"days": total,
"months_n": round_half(total / 30, 1),
"end": end.isoformat(),
}
def compute_schedule(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict) -> dict:
"""공기 한 벌 — {work: 공기산출, calendar: 공기집계}."""
user = user or {}
work = work_table(src or {}, bill_rows or [], user)
parts = [work["prep"], work["subtotal"], work["tidy"]]
return {"work": work, "calendar": month_table(src or {}, user, parts)}