# -*- coding: utf-8 -*- """PLAN 846-2(요구 879 · 881 · 882 · 883) — 기계 줄 시간당 작업량 그리기 · 제어 구역 · 기계 보기. ① 일당(GC000088) — 「시간당 작업량 = 시공량 560 ㎥/일 ÷ (1 대 × 8 시간) = 70 ㎥/시간」 줄 먼저 · 비목 줄 「59,020 원/시간 ÷ 70 ㎥/시간 = 843.1 원」 · 화면 셈 = 서버 셈(금액 그대로) ② 시간당 식 덮음 — 작업량 틀 풀이(q · K · f · E · cm 값 줄 + 한 층 분수 Q 식) · 비목 ÷ Q · 변수 E 고침 → Q 그만큼 · 화면 셈 = 서버 셈 ③ 왼쪽 제어 구역 — 방식 고르기 · 시간당 = 그 식 변수 칸 · 식 없는 기계 = 잠김 글 ④ 전수 — 기계 작업량 식이 있는 문서를 모두 시간당 식으로 바꿔 그려도 안 멈춤 · 겹분수 · 단위 규칙 어긋남 0 · 화면 셈 = 서버 셈 ⑤ 기계 보기 서버 — 작업량 틀 문서 · 표 연결을 실음 """ from __future__ import annotations import copy import importlib import json import os import subprocess from decimal import Decimal import pytest from M01_MasterData import M01_MasterData_Store as m01 # noqa: F401 (scripts 길) from M02_MasterTemplete import M02_Doc_Copy as dc mr = importlib.import_module("master_doc_record") HERE = os.path.dirname(os.path.abspath(__file__)) WORK = "작업량" def _js(payload: dict): proc = subprocess.run( ["node", os.path.join(HERE, "helper_845_2_unit_show.cjs")], input=json.dumps(dc.ml._norm(payload), ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", ) assert proc.returncode == 0, proc.stderr[-3000:] return json.loads(proc.stdout) @pytest.fixture(scope="module") def base(): docs = dc.folder() return docs, dc.ml.Links(dc.skc.whole_(), docs) def _case(base, key: str, doc: dict): rec = mr.Rec(base[1]) rec.prefetch(doc) ans = dc.mc.calc(doc, rec) return {"키": key, "문서": doc, "연결": rec.out(), "기계": False}, ans def _hourly(doc: dict, machine: str, key: str, vals: dict | None = None) -> dict: out = copy.deepcopy(doc) w = {"방식": "시간당", "식": key, **({"변수": vals} if vals else {})} out["기계덮음"] = {machine: {WORK: w}} return out def _num(v): return None if v is None else float(Decimal(str(v))) @pytest.fixture(scope="module") def gc88(base): d = base[0]["GC000088"] cases, py = [], {} for key, doc in [ ("일당", d), ("시간당", _hourly(d, "0201-0100", "GC001147")), ("E", _hourly(d, "0201-0100", "GC001147", {"E입력": 0.8})), ]: c, a = _case(base, key, doc) cases.append(c) py[key] = a got = _js({"할일": "전수", "문서들": cases, "보기": [c["키"] for c in cases]}) return {g["키"]: g for g in got}, py def test_일당_시간당_작업량_줄_먼저_비목_나눔(gc88): g, py = gc88 rows = [r["t"] for r in g["일당"]["오른쪽"]] work = ( r"품셈 일당⟦\text{시간당 작업량}⟧ = ⟦\text{시공량}\ 560\ \text{㎥/일} \div " r"\left(1\ \text{대} \times 8\ \text{시간}\right)⟧ = ⟦70\ \text{㎥/시간}⟧" ) assert work in rows labor = "노무비 : 59,020 원/시간 ⟦{}\\div 70\\ \\text{㎥/시간}⟧ = 843.1 원" assert labor in rows assert rows.index(work) < rows.index(labor) # 작업량 줄 먼저 assert g["일당"]["bad"] == [] assert _num(py["일당"]["계"]) == g["일당"]["셈"]["계"] == 1893 # 금액 그대로 · 화면 = 서버 def test_시간당_식_풀이_비목_나눔_금액(gc88): g, py = gc88 rows = [r["t"] for r in g["시간당"]["오른쪽"]] text = "\n".join(rows) assert ( "⟦q = 1\\ \\text{㎥}⟧⟦K = 0.9⟧⟦f = 0.7⟧⟦E = 0.75⟧⟦\\mathit{cm} = 21\\ \\text{sec}⟧" in text ) assert ( r"⟦Q⟧ = ⟦\dfrac{3{,}600 \times q \times K \times f \times E}{\mathit{cm}}⟧ = " r"⟦\dfrac{3{,}600 \times 1 \times 0.9 \times 0.7 \times 0.75}{21}⟧ = ⟦81\ \text{㎥/시간}⟧" ) in text assert "노무비 : 59,020 원/시간 ⟦{}\\div 81\\ \\text{㎥/시간}⟧ = 728.6 원" in rows assert any(t.startswith("시간당 식") for t in rows) # 머리 작은 글 = 방식 assert g["시간당"]["bad"] == [] assert _num(py["시간당"]["계"]) == g["시간당"]["셈"]["계"] != g["일당"]["셈"]["계"] def test_시간당_변수_E_고침_그만큼(gc88): g, py = gc88 text = "\n".join(r["t"] for r in g["E"]["오른쪽"]) assert "⟦E = 0.8⟧" in text and r"= ⟦86.4\ \text{㎥/시간}⟧" in text # 81 × 0.8 / 0.75 assert "노무비 : 59,020 원/시간 ⟦{}\\div 86.4\\ \\text{㎥/시간}⟧ = 683.1 원" in text assert _num(py["E"]["계"]) == g["E"]["셈"]["계"] w = next(r["작업량"] for r in g["E"]["셈"]["줄"] if r["작업량"]) assert w["꼴"] == "시간당" and w["값"] == 86.4 and w["변수"]["E입력"] == 0.8 def test_왼쪽_제어_구역_방식_변수_칸(gc88): g, _ = gc88 day = [r["t"] for r in g["일당"]["왼쪽"]] hour = [r["t"] for r in g["E"]["왼쪽"]] assert "품셈 일당시간당 식" in [t.strip() for t in day] # 고르기 두 칸 assert not any(t.strip().startswith("버킷용량") for t in day) # 일당 = 변수 칸 없음 assert "버킷용량 q=[1]㎥" in [t.strip() for t in hour] assert "체적환산계수 f=[0.7]" in [t.strip() for t in hour] e = next(t.strip() for t in hour if t.strip().startswith("E입력")) assert e.startswith("E입력=[0.8]고침") # 고친 칸 표지 @pytest.fixture(scope="module") def every(base): """작업량 자리가 있는 문서 전수 — 일당 그대로 + 기계 식이 있으면 시간당 식으로.""" docs = base[0] plain, hourly, kinds, nofit = [], [], {}, [] for key, doc in sorted(docs.items()): c, a = _case(base, key, doc) works = [r[WORK] for r in a.get("줄") or [] if isinstance(r, dict) and r.get(WORK)] if not works: continue plain.append(c) for w in works: kinds.setdefault(w["꼴"], set()).add(key) cover = { w["기계"]: {WORK: {"방식": "시간당", "식": w["식들"][0]}} for w in works if w["식들"] } if not cover: nofit.append(key) continue h = copy.deepcopy(doc) h["기계덮음"] = cover hc, ha = _case(base, f"{key}~시간당", h) hc["py계"] = _num(ha.get("계")) hourly.append(hc) return plain, hourly, kinds, nofit def test_전수_일당_시간당_차림_어긋남_0(every): plain, hourly, kinds, nofit = every assert len(kinds.get("일당", ())) >= 200 and len(kinds.get("식", ())) >= 40 want = {c["키"]: c.get("py계") for c in hourly} got = _js({"할일": "전수", "문서들": plain + hourly, "보기": list(want)}) assert len(got) == len(plain) + len(hourly) bad = {g["키"]: g["bad"][:2] for g in got if g["bad"]} assert bad == {} assert all(g["같음"] for g in got) off = { g["키"]: (want[g["키"]], g["셈"]["계"]) for g in got if g["키"] in want and want[g["키"]] != g["셈"]["계"] } assert off == {} # 시간당 식 화면 셈 = 서버 셈 def test_식_없는_기계_잠김_글(base, every): _, _, _, nofit = every assert nofit c, _ = _case(base, nofit[0], base[0][nofit[0]]) g = _js({"할일": "전수", "문서들": [c], "보기": [c["키"]]})[0] assert any("시간당 작업량 식 없는 기계" in r["t"] for r in g["왼쪽"]) def test_기계_보기_서버_작업량_연결(): from M02_MasterTemplete import M02_MyMachine_Sheet as sheet folder = os.path.join(HERE, "..", "master_data", "기계") rows = (json.load(open(os.path.join(folder, f), encoding="utf-8")) for f in os.listdir(folder)) row = next(r for r in rows if r.get("원문번호") == "0201-0100") got = sheet.sheet(row) keys = [s["식"] for s in row["값"][WORK]] assert "GC001147" in keys assert all(k in got["연결"]["문서"] for k in keys) assert got["연결"]["표"] # 작업량 틀이 읽는 표(버킷계수 · 작업효율 · 싸이클)