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
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""B09 공기 셈 짝(PLAN 252-1 · 사용자 161번 · 초안 — draft 폴더째 지우면 사라짐).
|
||||
|
||||
짝 = `B09_Draft_Schedule_Calc.ts`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py`).
|
||||
짝 = `B09_Draft_Schedule_Calc.ts`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py` · `test_264_2_b09_schedule_options.py`).
|
||||
근거 = 실무 설계서 공기 탭 식(거창 25 · 울진 24 · 울진소광 25) — 값 베끼기가 아니라 식만:
|
||||
|
||||
공기산출 설계내역서 항목 줄마다 작업일수 = ROUNDUP(시공량 × 반영률 ÷ 일작업량, 1)
|
||||
@@ -11,13 +11,19 @@
|
||||
단위당 자원 = 서버 역전개(`…_Source.unit_resources` · 호표 구성 1 단위)
|
||||
반영률 = 다른 공종과 겹침(100 = 혼자 · 50 = 반 겹침 · 0 = 다 겹침) · 운반공 기본 0
|
||||
소계 = ROUND(Σ 작업일수 + 기타, 0) · 총작업일수 = ROUND(준비 + 소계 + 정리, 0)
|
||||
공기집계 착공 연월부터 달마다 — 달력일수 · 기후여건 A = ROUNDUP(강수일 + 혹서일, 1) ·
|
||||
공기집계 착공일부터 달마다 — 달력일수 · 기후여건 A = ROUNDUP(강수일 + 혹서일, 1) ·
|
||||
법정공휴일 B = 일요일 + 공휴일(대체 · 선거일 포함 · 사용자 덮개) ·
|
||||
중복 C = A × B ÷ 달력 ·
|
||||
비작업일수 = ROUNDDOWN(A + B − C, 0) · 작업가능일수 = 달력 − 비작업
|
||||
준비 → 작업 → 정리 차례로 작업가능일수를 채움 · 다 쓴 달 = 그 달 비작업 전부 ·
|
||||
마지막 달(덜 쓴 달) = ROUNDUP(비작업 × 쓴 날 ÷ 작업가능일, 0)
|
||||
공사기간 = Σ(쓴 날 + 비작업) · 준공 예정일 = 착공(그 달 1일) + 공사기간 − 1
|
||||
공사기간 = Σ(쓴 날 + 비작업) · 준공 예정일 = 착공일 + 공사기간 − 1
|
||||
옵션(264 · user.schedule · 기본값 = 위 식 그대로) — 착공일(1일 아니면 첫 달 = 그날부터 ·
|
||||
A = 날수 비례) · 기후 자료(원천 climates 고르기 · 달마다 덮개가 이김) ·
|
||||
쉬는 날(일 · 토일) · 공휴일 빼기(대체 · 선거일 · 노동절 · 제헌절 — 이름으로 가름) ·
|
||||
투입 기본(장비 대 · 인력 인) · 반영률 기본(운반공 · 그 밖) ·
|
||||
주 장비 기준(가장 긴 장비 · 장비 시간 합) · 중복 C 끝수(그대로 · 소수 1자리 반올림) ·
|
||||
마지막 달 비작업(쓴 비율 올림 · 전부)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,12 +33,26 @@ from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import num, round_half, rounddown, roundup
|
||||
|
||||
HOURS = 8
|
||||
CREW = 10
|
||||
PREP_DOCS = 2
|
||||
PREP_SETUP = 2
|
||||
TIDY = 4
|
||||
#: 공사 아닌 공종(자재대 · 관급자재대) · 기본 반영률 0(운반 = 다른 공종과 겹침)
|
||||
#: 사용자 값 기본(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
|
||||
@@ -42,18 +62,23 @@ def _top(section: Any) -> str:
|
||||
return str(section or "").split(".")[0]
|
||||
|
||||
|
||||
def _cfg(user: dict[str, Any], key: str, default: float) -> float:
|
||||
def _cfg(user: dict[str, Any], key: str) -> float:
|
||||
value = num(user.get(key))
|
||||
return default if value is None or value < 0 else value
|
||||
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, hours: float) -> dict:
|
||||
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 = 0.0 if _top(item.get("section")) in OVERLAP_TOP else 100.0
|
||||
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",
|
||||
@@ -78,12 +103,12 @@ def _work_row(item: dict[str, Any], unit: dict[str, Any] | None, cfg: dict, hour
|
||||
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 1.0
|
||||
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 float(CREW)
|
||||
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:
|
||||
@@ -96,7 +121,15 @@ def _work_row(item: dict[str, Any], unit: dict[str, Any] | None, cfg: dict, hour
|
||||
|
||||
def work_table(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict) -> dict:
|
||||
"""공기산출 — 설계내역서 차례(묶음 줄 = 그 아래 셀 줄이 있을 때만) · 준비 · 기타 · 정리."""
|
||||
hours = _cfg(user, "hours", HOURS) or HOURS
|
||||
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]] = []
|
||||
@@ -129,11 +162,11 @@ def work_table(src: dict[str, Any], bill_rows: list[dict[str, Any]], user: dict)
|
||||
"lv": head.get("level") or 1,
|
||||
} # fmt: skip
|
||||
)
|
||||
rows.append(_work_row(row, units.get(key), cfgs.get(key) or {}, hours))
|
||||
docs = _cfg(user, "prep_docs", PREP_DOCS)
|
||||
setup = _cfg(user, "prep_setup", PREP_SETUP)
|
||||
etc = _cfg(user, "etc", 0)
|
||||
tidy = _cfg(user, "tidy", TIDY)
|
||||
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)
|
||||
@@ -156,45 +189,70 @@ def _month_days(year: int, month: int) -> int:
|
||||
return (nxt - datetime.date(year, month, 1)).days
|
||||
|
||||
|
||||
def _holidays(src: dict[str, Any], year: int, month: int) -> tuple[int, list[str]]:
|
||||
"""그 달 법정공휴일 수(일요일 + 공휴일 겹침 한 번) · 공휴일 이름(「일 이름」)."""
|
||||
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)
|
||||
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(1, _month_days(year, month) + 1):
|
||||
for day in range(first, _month_days(year, month) + 1):
|
||||
iso = f"{head}{day:02d}"
|
||||
if datetime.date(year, month, day).weekday() == 6 or iso in named:
|
||||
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(((src.get("climate") or {}).get(kind) or [0] * 12)[month - 1]) or 0.0
|
||||
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 = [준비, 작업, 정리] 일수 · 착공 연월부터 채움."""
|
||||
"""공기집계 — 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
|
||||
days = _month_days(year, month)
|
||||
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, 1)
|
||||
auto, names = _holidays(src, year, 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)
|
||||
@@ -208,7 +266,7 @@ def month_table(src: dict[str, Any], user: dict[str, Any], parts: list[float]) -
|
||||
used = sum(use)
|
||||
if cap <= 0:
|
||||
off_used = days
|
||||
elif used >= cap:
|
||||
elif used >= cap or user.get("last_off") == "all":
|
||||
off_used = off
|
||||
else:
|
||||
off_used = roundup(off * used / cap, 0)
|
||||
@@ -237,11 +295,10 @@ def month_table(src: dict[str, Any], user: dict[str, Any], parts: list[float]) -
|
||||
} # fmt: skip
|
||||
)
|
||||
year, month = (year + 1, 1) if month == 12 else (year, month + 1)
|
||||
first = months[0]
|
||||
begin = datetime.date(first["year"], first["month"], 1)
|
||||
first = 1
|
||||
end = begin + datetime.timedelta(days=max(total, 1) - 1)
|
||||
return {
|
||||
"start": {"year": first["year"], "month": first["month"]},
|
||||
"start": {"year": begin.year, "month": begin.month, "day": begin.day},
|
||||
"months": months,
|
||||
"days": total,
|
||||
"months_n": round_half(total / 30, 1),
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
/* =============================================================================
|
||||
* B09_Draft_Schedule_Calc.ts
|
||||
* B09 공기 셈 짝(PLAN 252-1 · 사용자 161번 · 초안 — draft 폴더째 지우면 사라짐).
|
||||
* 짝 = `B09_Draft_Schedule_Calc.py`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py`) — 식 설명은 그쪽 머리.
|
||||
* 짝 = `B09_Draft_Schedule_Calc.py`(같은 식 · 거울 시험 `test_252_1_b09_schedule.py` ·
|
||||
* `test_264_2_b09_schedule_options.py`) — 식 설명은 그쪽 머리.
|
||||
* 공기산출 = 설계내역서 항목 줄마다 ROUNDUP(시공량 × 반영률 ÷ 일작업량, 1) · 일작업량 = 장비 · 인력 · 입력
|
||||
* 공기집계 = 착공 연월부터 달마다 작업가능일수(달력 − (A + B − A×B÷달력))로 준비 → 작업 → 정리를 채움
|
||||
* 공기집계 = 착공일부터 달마다 작업가능일수(달력 − (A + B − A×B÷달력))로 준비 → 작업 → 정리를 채움
|
||||
* 옵션(264) = 착공일 · 기후 자료 · 쉬는 날 · 공휴일 빼기 · 투입 기본 · 반영률 기본 · 주 장비 기준 ·
|
||||
* 중복 C 끝수 · 마지막 달 비작업(기본값 = 위 식 그대로)
|
||||
* ========================================================================== */
|
||||
|
||||
import { num, roundHalf, rounddown, roundup, type Row } from "../B09_Estimation_Calc_Base";
|
||||
|
||||
const HOURS = 8;
|
||||
const CREW = 10;
|
||||
const PREP_DOCS = 2;
|
||||
const PREP_SETUP = 2;
|
||||
const TIDY = 4;
|
||||
/** 사용자 값 기본(264 패널 · 표 칸 같은 값) */
|
||||
export const DEFAULTS: Record<string, number> = {
|
||||
hours: 8,
|
||||
units: 1,
|
||||
crew: 10,
|
||||
prep_docs: 2,
|
||||
prep_setup: 2,
|
||||
tidy: 4,
|
||||
etc: 0,
|
||||
rate_haul: 0,
|
||||
rate_base: 100,
|
||||
};
|
||||
/** 공휴일 빼기 갈래 — 이름에 든 글 */
|
||||
export const HOLIDAY_KINDS: Record<string, string[]> = {
|
||||
substitute: ["대체"],
|
||||
election: ["선거"],
|
||||
labor: ["노동절", "근로자"],
|
||||
constitution: ["제헌절"],
|
||||
};
|
||||
const SKIP_TOP = ["4", "G"];
|
||||
const OVERLAP_TOP = ["3"];
|
||||
const MAX_MONTHS = 120;
|
||||
|
||||
const top = (section: unknown): string => String(section ?? "").split(".")[0];
|
||||
|
||||
function cfg(user: Row, key: string, fallback: number): number {
|
||||
function cfg(user: Row, key: string): number {
|
||||
const value = num(user[key]);
|
||||
return value === null || value < 0 ? fallback : value;
|
||||
return value === null || value < 0 ? DEFAULTS[key] : value;
|
||||
}
|
||||
|
||||
function workRow(item: Row, unit: Row | undefined, c: Row, hours: number): Row {
|
||||
function workRow(item: Row, unit: Row | undefined, c: Row, base: Row): Row {
|
||||
const hours: number = base.hours;
|
||||
const qty = num(item.qty) ?? 0;
|
||||
let rate = num(c.rate);
|
||||
if (rate === null) rate = OVERLAP_TOP.includes(top(item.section)) ? 0 : 100;
|
||||
if (rate === null)
|
||||
rate = OVERLAP_TOP.includes(top(item.section)) ? base.rate_haul : base.rate_base;
|
||||
const dailyIn = num(c.daily);
|
||||
const machine = unit?.machine as Row | null | undefined;
|
||||
let machine = unit?.machine as Row | null | undefined;
|
||||
const total = unit?.machine_sum as Row | null | undefined;
|
||||
if (machine && base.sum && total && (num(total.hr) ?? 0) > 0) {
|
||||
const many = (total.n || 1) > 1 ? ` 외 ${total.n - 1}종` : "";
|
||||
machine = { ...machine, name: `${machine.name || ""}${many}`, hr: total.hr };
|
||||
}
|
||||
const labor = num(unit?.labor);
|
||||
const out: Row = {
|
||||
kind: "item",
|
||||
@@ -55,12 +79,12 @@ function workRow(item: Row, unit: Row | undefined, c: Row, hours: number): Row {
|
||||
need = (qty * rate) / 100 / dailyIn;
|
||||
} else if (machine && (num(machine.hr) ?? 0) > 0) {
|
||||
const hr = num(machine.hr) as number;
|
||||
const units = num(c.units) || 1;
|
||||
const units = num(c.units) || base.units;
|
||||
Object.assign(out, { basis: "장비", who: machine.name || "", per: hr, put: units });
|
||||
Object.assign(out, { q: 1 / hr, daily: (hours * units) / hr });
|
||||
need = (((qty * rate) / 100) * hr) / (hours * units);
|
||||
} else if (labor !== null && labor > 0) {
|
||||
const crew = num(c.crew) || CREW;
|
||||
const crew = num(c.crew) || base.crew;
|
||||
Object.assign(out, { basis: "인력", who: "인력", per: labor, put: crew, daily: crew / labor });
|
||||
need = (((qty * rate) / 100) * labor) / crew;
|
||||
} else {
|
||||
@@ -74,7 +98,15 @@ function workRow(item: Row, unit: Row | undefined, c: Row, hours: number): Row {
|
||||
|
||||
/** 공기산출 — 설계내역서 차례(묶음 줄 = 그 아래 셀 줄이 있을 때만) · 준비 · 기타 · 정리 */
|
||||
export function workTable(src: Row, billRows: Row[], user: Row): Row {
|
||||
const hours = cfg(user, "hours", HOURS) || HOURS;
|
||||
const hours = cfg(user, "hours") || DEFAULTS.hours;
|
||||
const base: Row = {
|
||||
hours,
|
||||
units: cfg(user, "units") || DEFAULTS.units,
|
||||
crew: cfg(user, "crew") || DEFAULTS.crew,
|
||||
rate_haul: Math.min(cfg(user, "rate_haul"), 100),
|
||||
rate_base: Math.min(cfg(user, "rate_base"), 100),
|
||||
sum: user.machine_basis === "sum",
|
||||
};
|
||||
const units: Row = src.units || {};
|
||||
const cfgs: Row = user.rows || {};
|
||||
const rows: Row[] = [];
|
||||
@@ -101,12 +133,12 @@ export function workTable(src: Row, billRows: Row[], user: Row): Row {
|
||||
p[1] = true;
|
||||
rows.push({ kind: "head", key: p[0].key, name: p[0].name || "", lv: p[0].level || 1 });
|
||||
}
|
||||
rows.push(workRow(row, units[key], cfgs[key] || {}, hours));
|
||||
rows.push(workRow(row, units[key], cfgs[key] || {}, base));
|
||||
}
|
||||
const docs = cfg(user, "prep_docs", PREP_DOCS);
|
||||
const setup = cfg(user, "prep_setup", PREP_SETUP);
|
||||
const etc = cfg(user, "etc", 0);
|
||||
const tidy = cfg(user, "tidy", TIDY);
|
||||
const docs = cfg(user, "prep_docs");
|
||||
const setup = cfg(user, "prep_setup");
|
||||
const etc = cfg(user, "etc");
|
||||
const tidy = cfg(user, "tidy");
|
||||
const items = rows.filter((r) => r.kind === "item");
|
||||
const days = items.reduce((s: number, r) => s + (r.days || 0), 0);
|
||||
const subtotal = roundHalf(days + etc, 0);
|
||||
@@ -129,16 +161,38 @@ const monthDays = (year: number, month: number): number =>
|
||||
|
||||
const pad = (n: number): string => String(n).padStart(2, "0");
|
||||
|
||||
/** 그 달 법정공휴일 수(일요일 + 공휴일 겹침 한 번) · 공휴일 이름 */
|
||||
function holidays(src: Row, year: number, month: number): [number, string[]] {
|
||||
/** 공휴일 넣음 — 「;」 로 묶인 이름 중 하나라도 뺀 갈래가 아니면 넣음 */
|
||||
function counted(name: string, off: string[]): boolean {
|
||||
for (const part of name.split(";")) {
|
||||
const kinds = Object.keys(HOLIDAY_KINDS).filter((k) =>
|
||||
HOLIDAY_KINDS[k].some((w) => part.includes(w)),
|
||||
);
|
||||
if (!kinds.some((k) => off.includes(k))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 그 달(first 일부터) 법정공휴일 수(쉬는 요일 + 공휴일 겹침 한 번) · 공휴일 이름 */
|
||||
function holidays(
|
||||
src: Row,
|
||||
year: number,
|
||||
month: number,
|
||||
first: number,
|
||||
user: Row,
|
||||
): [number, string[]] {
|
||||
const head = `${String(year).padStart(4, "0")}-${pad(month)}-`;
|
||||
const off: string[] = user.holiday_off || [];
|
||||
const named = new Map<string, string>(
|
||||
((src.holidays || {})[String(year)] || []).filter(([d]: [string]) => d.startsWith(head)),
|
||||
((src.holidays || {})[String(year)] || []).filter(
|
||||
([d, n]: [string, string]) =>
|
||||
d.startsWith(head) && Number(d.slice(-2)) >= first && counted(n, off),
|
||||
),
|
||||
);
|
||||
const rest = user.rest === "satsun" ? [6, 0] : [0];
|
||||
let count = 0;
|
||||
for (let day = 1; day <= monthDays(year, month); day += 1) {
|
||||
const sunday = new Date(Date.UTC(year, month - 1, day)).getUTCDay() === 0;
|
||||
if (sunday || named.has(`${head}${pad(day)}`)) count += 1;
|
||||
for (let day = first; day <= monthDays(year, month); day += 1) {
|
||||
const weekday = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
||||
if (rest.includes(weekday) || named.has(`${head}${pad(day)}`)) count += 1;
|
||||
}
|
||||
const names = [...named.entries()]
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
@@ -146,30 +200,38 @@ function holidays(src: Row, year: number, month: number): [number, string[]] {
|
||||
return [count, names];
|
||||
}
|
||||
|
||||
/** 고른 기후 자료(원천 climates) · 없으면 원천 기본값 */
|
||||
export const climateBase = (src: Row, user: Row): Row =>
|
||||
(src.climates || {})[user.climate_set] || src.climate || {};
|
||||
|
||||
function climate(src: Row, user: Row, kind: string, month: number): number {
|
||||
const mine = num(((user.climate || {})[kind] || [])[month - 1]);
|
||||
if (mine !== null) return mine;
|
||||
return num(((src.climate || {})[kind] || [])[month - 1]) ?? 0;
|
||||
return num((climateBase(src, user)[kind] || [])[month - 1]) ?? 0;
|
||||
}
|
||||
|
||||
/** 공기집계 — parts = [준비, 작업, 정리] 일수 · 착공 연월부터 채움 */
|
||||
/** 공기집계 — parts = [준비, 작업, 정리] 일수 · 착공일부터 채움 */
|
||||
export function monthTable(src: Row, user: Row, parts: number[]): Row {
|
||||
const start: Row = { ...(src.start || {}), ...(user.start || {}) };
|
||||
let year = Math.trunc(Number(start.year) || 2000);
|
||||
let month = Math.trunc(Number(start.month) || 1);
|
||||
let first = Math.min(Math.max(Math.trunc(Number(start.day) || 1), 1), monthDays(year, month));
|
||||
const begin = Date.UTC(year, month - 1, first);
|
||||
const left = parts.map((p) => Math.max(p, 0));
|
||||
const months: Row[] = [];
|
||||
let total = 0;
|
||||
while (left.reduce((s, v) => s + v, 0) > 0 || !months.length) {
|
||||
if (months.length >= MAX_MONTHS) break;
|
||||
const days = monthDays(year, month);
|
||||
const full = monthDays(year, month);
|
||||
const days = full - first + 1;
|
||||
const rain = climate(src, user, "rain", month);
|
||||
const heat = climate(src, user, "heat", month);
|
||||
const a = roundup(rain + heat, 1);
|
||||
const [auto, names] = holidays(src, year, month);
|
||||
const a = roundup(first === 1 ? rain + heat : ((rain + heat) * days) / full, 1);
|
||||
const [auto, names] = holidays(src, year, month, first, user);
|
||||
const mine = num((user.holiday || {})[`${String(year).padStart(4, "0")}-${pad(month)}`]);
|
||||
const b = mine !== null ? mine : auto;
|
||||
const c = (a * b) / days;
|
||||
let c = (a * b) / days;
|
||||
if (user.c_round === "round1") c = roundHalf(c, 1);
|
||||
const off = rounddown(a + b - c, 0);
|
||||
const avail = days - off;
|
||||
const cap = Math.max(avail, 0);
|
||||
@@ -182,7 +244,12 @@ export function monthTable(src: Row, user: Row, parts: number[]): Row {
|
||||
room -= take;
|
||||
}
|
||||
const used = use[0] + use[1] + use[2];
|
||||
const offUsed = cap <= 0 ? days : used >= cap ? off : roundup((off * used) / cap, 0);
|
||||
const offUsed =
|
||||
cap <= 0
|
||||
? days
|
||||
: used >= cap || user.last_off === "all"
|
||||
? off
|
||||
: roundup((off * used) / cap, 0);
|
||||
const span = used + offUsed;
|
||||
total += span;
|
||||
months.push({
|
||||
@@ -209,11 +276,12 @@ export function monthTable(src: Row, user: Row, parts: number[]): Row {
|
||||
year += 1;
|
||||
month = 1;
|
||||
} else month += 1;
|
||||
first = 1;
|
||||
}
|
||||
const first = months[0];
|
||||
const end = new Date(Date.UTC(first.year, first.month - 1, 1) + (Math.max(total, 1) - 1) * 864e5);
|
||||
const from = new Date(begin);
|
||||
const end = new Date(begin + (Math.max(total, 1) - 1) * 864e5);
|
||||
return {
|
||||
start: { year: first.year, month: first.month },
|
||||
start: { year: from.getUTCFullYear(), month: from.getUTCMonth() + 1, day: from.getUTCDate() },
|
||||
months,
|
||||
days: total,
|
||||
months_n: roundHalf(total / 30, 1),
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
공휴일 = `B09_Draft_Schedule_Holidays.json`(2024~2040 · 법정 · 대체 · 확정 선거일 — 공개 `holidays`
|
||||
목록 0.106 에서 뽑음 · 2027 이후 선거일은 날짜가 안 정해져 뺌 · 달마다 사용자 덮개)
|
||||
기후 기본값 = 참고 설계서(25 거창) 월별 강수일(일강수량 5mm 이상) · 혹서일(33℃ 이상) —
|
||||
지역마다 사용자 고침
|
||||
사용자 값 = user["schedule"] — 다듬기 `clean_schedule`(모르는 칸 버림 · 수 칸은 수)
|
||||
고르기(264) = 참고 설계서 셋(거창 25 · 울진 24 · 울진 소광 25 「공사기간산정표」) · 0에서 직접 ·
|
||||
달마다 사용자 덮개가 이김
|
||||
사용자 값 = user["schedule"] — 다듬기 `clean_schedule`(모르는 칸 버림 · 수 칸은 수 · 고르기 칸은 목록 안)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
@@ -22,7 +24,7 @@ from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import num
|
||||
from B09_Estimation.B09_Estimation_Summary import expand, hopyo_lines
|
||||
from B09_Estimation.draft.B09_Draft_Schedule_Calc import compute_schedule
|
||||
from B09_Estimation.draft.B09_Draft_Schedule_Calc import HOLIDAY_KINDS, compute_schedule
|
||||
|
||||
HOLIDAYS = Path(__file__).with_name("B09_Draft_Schedule_Holidays.json")
|
||||
CLIMATE = {
|
||||
@@ -30,8 +32,50 @@ 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],
|
||||
}
|
||||
#: 기후 자료 고르기(264) — 첫 줄 = 기본 · rain_note · heat_note = 공기집계 비고
|
||||
CLIMATES = {
|
||||
"geochang25": {
|
||||
**CLIMATE,
|
||||
"name": "거창 25 참고",
|
||||
"rain_note": "일강수량 5mm 이상",
|
||||
"heat_note": "33℃ 이상",
|
||||
},
|
||||
"uljin24": {
|
||||
"name": "울진 24 참고",
|
||||
"label": "경북 울진(참고 설계서 24 · 강수 = 일강수량 5mm 이상 · 혹서 없음)",
|
||||
"rain": [0.6, 1.3, 2.9, 4.3, 3.2, 3.9, 7.4, 8.3, 4, 2.8, 2.6, 1.4],
|
||||
"heat": [0] * 12,
|
||||
"rain_note": "일강수량 5mm 이상",
|
||||
"heat_note": "혹서 없음(참고 설계서)",
|
||||
},
|
||||
"sogwang25": {
|
||||
"name": "울진 소광 25 참고",
|
||||
"label": "경북 울진 소광(참고 설계서 25 · 강수 = 일강수량 10mm 이상 · 혹서 없음)",
|
||||
"rain": [2.1, 2, 3.3, 4, 2.1, 3.7, 6.3, 7.9, 5, 4.2, 3.6, 1.6],
|
||||
"heat": [0] * 12,
|
||||
"rain_note": "일강수량 10mm 이상",
|
||||
"heat_note": "혹서 없음(참고 설계서)",
|
||||
},
|
||||
"zero": {
|
||||
"name": "0에서 직접",
|
||||
"label": "직접 입력(달마다 강수 · 혹서 칸)",
|
||||
"rain": [0] * 12,
|
||||
"heat": [0] * 12,
|
||||
"rain_note": "직접 입력",
|
||||
"heat_note": "직접 입력",
|
||||
},
|
||||
}
|
||||
START_MONTH = 3
|
||||
NUMBERS = ("hours", "prep_docs", "prep_setup", "etc", "tidy")
|
||||
NUMBERS = ("hours", "prep_docs", "prep_setup", "etc", "tidy", "rate_haul", "rate_base")
|
||||
POSITIVE = ("units", "crew")
|
||||
#: 고르기 칸 — 기본값이 첫째
|
||||
CHOICES = {
|
||||
"climate_set": tuple(CLIMATES),
|
||||
"rest": ("sun", "satsun"),
|
||||
"machine_basis": ("max", "sum"),
|
||||
"c_round": ("none", "round1"),
|
||||
"last_off": ("ratio", "all"),
|
||||
}
|
||||
|
||||
|
||||
def unit_resources(sheets: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
@@ -54,7 +98,13 @@ def unit_resources(sheets: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
name = " ".join(t for t in (src.get("name"), src.get("spec")) if t) or mkey
|
||||
machine = {"key": mkey, "name": name, "hr": hr}
|
||||
labor = sum((used.get("labor") or {}).values())
|
||||
out[row["key"]] = {"machine": machine, "labor": labor if labor > 0 else None}
|
||||
# 264 주 장비 기준 「장비 시간 합」 = 차례로 일하는 장비 · n = 장비 수
|
||||
spent = [hr for hr in hours.values() if hr > 0]
|
||||
out[row["key"]] = {
|
||||
"machine": machine,
|
||||
"labor": labor if labor > 0 else None,
|
||||
"machine_sum": {"hr": sum(spent), "n": len(spent)} if machine else None,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
@@ -71,6 +121,7 @@ def schedule_inputs(sheets: dict[str, Any], today: datetime.date | None = None)
|
||||
"units": unit_resources(sheets),
|
||||
"holidays": holiday_table(),
|
||||
"climate": CLIMATE,
|
||||
"climates": CLIMATES,
|
||||
"start": {"year": today.year + 1, "month": START_MONTH},
|
||||
}
|
||||
|
||||
@@ -100,12 +151,25 @@ def clean_schedule(raw: Any) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {}
|
||||
for field in NUMBERS:
|
||||
v = num(raw.get(field))
|
||||
if v is not None and 0 <= v <= 1000:
|
||||
if v is not None and 0 <= v <= (100 if field.startswith("rate") else 1000):
|
||||
out[field] = v
|
||||
for field in POSITIVE:
|
||||
if (v := _positive(raw.get(field))) is not None and v <= 1e4:
|
||||
out[field] = v
|
||||
for field, allowed in CHOICES.items():
|
||||
if raw.get(field) in allowed:
|
||||
out[field] = raw[field]
|
||||
off = raw.get("holiday_off") if isinstance(raw.get("holiday_off"), list) else []
|
||||
kinds = [k for k in HOLIDAY_KINDS if k in off]
|
||||
if kinds:
|
||||
out["holiday_off"] = kinds
|
||||
start = raw.get("start") if isinstance(raw.get("start"), dict) else {}
|
||||
year, month = num(start.get("year")), num(start.get("month"))
|
||||
if year and month and 2000 <= year <= 2100 and 1 <= month <= 12:
|
||||
out["start"] = {"year": int(year), "month": int(month)}
|
||||
day = num(start.get("day"))
|
||||
if day and 1 < day <= calendar.monthrange(int(year), int(month))[1]:
|
||||
out["start"]["day"] = int(day)
|
||||
climate = raw.get("climate") if isinstance(raw.get("climate"), dict) else {}
|
||||
kept = {k: v for k in ("rain", "heat") if (v := _twelve(climate.get(k)))}
|
||||
if kept:
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user