feat(846-2): 기계 줄 시간당 작업량 화면 — 산출근거 「시간당 작업량」 줄 먼저 · 비목 ÷ 시간당 작업량 · 제어 구역 일당 / 시간당 식 고르기 · 변수 칸 · 기계 보기 작업량 식
- 산출근거: 일당 「시간당 작업량 = 시공량 560 ㎥/일 ÷ (1 대 × 8 시간) = 70 ㎥/시간」 · 시간당 식 = q · K · f · E · cm 값 줄 + 한 층 분수 Q 식 · 비목 줄 「59,020 원/시간 ÷ 70 ㎥/시간 = 843.1 원」(금액 그대로) - 제어 구역: 「시간당 작업량」 묶음 맨 위 — 방식 고르기(식 없는 기계 = 잠김 + 까닭) · 시간당 = 작업량 틀 변수 칸(고침 · 원래 값) · 덮음 = 기계덮음.<기계>.작업량 - 기계 보기: 왼쪽 작업량 카드(기계 정본 변수) · 오른쪽 풀이 · 서버가 작업량 틀 연결을 실음(M02_MyMachine_Sheet._work) - 시험: test_846_2_work_view 새로 · test_845_2 GC000088 비목 줄 기대값을 새 꼴로 고침(846-2 표시 변경) · 헬퍼 가짜 DOM 이 진짜 DOM 처럼 요소를 옮김(같은 요소 두 줄 재사용 흠을 잡게) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,9 @@ class FakeNode {
|
||||
append(...xs) {
|
||||
for (const x of xs) {
|
||||
if (x === null || x === undefined) continue;
|
||||
// 진짜 DOM 처럼 — 이미 다른 곳에 붙은 요소는 옮김(같은 요소를 두 줄에 쓰면 앞 줄에서 빠짐)
|
||||
if (x instanceof FakeNode && x.parentNode) x.parentNode.childNodes = x.parentNode.childNodes.filter((c) => c !== x);
|
||||
if (x instanceof FakeNode) x.parentNode = this;
|
||||
this.childNodes.push(x instanceof FakeNode ? x : new FakeText(String(x)));
|
||||
}
|
||||
}
|
||||
@@ -283,7 +286,7 @@ function left(c) {
|
||||
const rows = [];
|
||||
const walk = (n) => {
|
||||
for (const ch of n.children) {
|
||||
if (ch.cls().includes("lstk-frow")) rows.push({ sym: ch.dataset.sym ?? ch.dataset.mctlField ?? "", t: textOf(ch) });
|
||||
if (ch.cls().includes("lstk-frow") || ch.cls().includes("lstk-work")) rows.push({ sym: ch.dataset.sym ?? ch.dataset.mctlField ?? "", t: textOf(ch) });
|
||||
walk(ch);
|
||||
}
|
||||
};
|
||||
@@ -291,6 +294,14 @@ function left(c) {
|
||||
return [...opts.buildOptions(doc, {}, true, noop).map((x) => ({ sym: `opt:${x.dataset.option}`, t: textOf(x) })), ...rows];
|
||||
}
|
||||
|
||||
/** 셈 답 요약(846-2 — 계 · 줄마다 작업량 · 금액) */
|
||||
function sums(c) {
|
||||
const cache = cacheMod.createDocCache();
|
||||
cache.merge(c.연결 || {});
|
||||
const a = calc.calcDoc(c.문서, cache.links, {});
|
||||
return { 계: a.계 ?? null, 줄: (a.줄 || []).map((r) => ({ 금액: r.금액 ?? null, 작업량: r.작업량 ?? null })) };
|
||||
}
|
||||
|
||||
/** 전수 지킴 — 줄 글에서 규칙 어긋남만 */
|
||||
const MONEY = /^(.+?) : (?!—)(.*\S)$/;
|
||||
function guard(c, machine) {
|
||||
@@ -319,7 +330,7 @@ let res;
|
||||
if (input.할일 === "전수") {
|
||||
res = input.문서들.map((c) => {
|
||||
const g = guard(c, machineNames);
|
||||
return { 키: c.키, ...g, ...(show.has(c.키) ? { 오른쪽: right(c), 왼쪽: left(c) } : {}) };
|
||||
return { 키: c.키, ...g, ...(show.has(c.키) ? { 오른쪽: right(c), 왼쪽: left(c), 셈: sums(c) } : {}) };
|
||||
});
|
||||
} else if (input.할일 === "단위") {
|
||||
res = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
① 전수 1,520 — 산출근거(펼침 모두)를 진짜 코드로 차린 줄 글:
|
||||
금액 · 소계 · 합계 = 「원」 · 수량이 식이면 괄호로 식 전체에 단위(「÷ 560 시간」 꼴 없음) ·
|
||||
메타 단위(「… 따름」 · 「수(…)」) 글 0 · 기계경비식(맨 위 · 펼침) 분수 한 층 · 문서 자료 안 바뀜
|
||||
② GC000088 — 사용자 그림 자리: 「59,020 원/시간 × (1 × 8 ÷ 560) 시간 = 843.1 원」 · 펼친 굴착기 = 상여 · 휴지 · 손료계수 중간 줄
|
||||
② GC000088 — 사용자 그림 자리: 「59,020 원/시간 ÷ 70 ㎥/시간 = 843.1 원」(846-2) · 펼친 굴착기 = 상여 · 휴지 · 손료계수 중간 줄
|
||||
③ 왼쪽 — 기계 제어 구역 값 칸 단위 = 오른쪽 펼침과 같은 글(천원 · 10⁻⁷/시간 · ℓ/hr) · 단위는 칸 밖 바로 뒤
|
||||
④ 단위 규칙 낱개 — 메타 · 설명 괄호 · 10⁻⁷ · 기계 칸 연결 · 금액 줄 자리
|
||||
"""
|
||||
@@ -71,11 +71,9 @@ def test_전수_규칙_어긋남_0(world):
|
||||
|
||||
def test_GC000088_비목_줄_단위_자리(world):
|
||||
rows = [r["t"] for r in world["GC000088"]["오른쪽"]]
|
||||
labor = next(t for t in rows if t.startswith("노무비 : ") and "×" in t)
|
||||
assert labor.startswith(
|
||||
"노무비 : 59,020 원/시간 × ⟦\\left(1 \\times 8 \\div 560\\right)⟧ 시간 = "
|
||||
)
|
||||
assert labor.endswith(" 원")
|
||||
# 846-2 기계 줄 = 「÷ 시간당 작업량」(시간 수량 식 「× (1 × 8 ÷ 560) 시간」 대신)
|
||||
labor = next(t for t in rows if t.startswith("노무비 : 59,020"))
|
||||
assert labor == "노무비 : 59,020 원/시간 ⟦{}\\div 70\\ \\text{㎥/시간}⟧ = 843.1 원"
|
||||
assert any(t.startswith("소 계 : ") and t.endswith(" 원") for t in rows)
|
||||
assert any(t.startswith("계 : ") and t.endswith(" 원") for t in rows)
|
||||
# 펼친 굴착기 시간당 경비 — 겹분수 대신 중간 줄
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# -*- 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["연결"]["표"] # 작업량 틀이 읽는 표(버킷계수 · 작업효율 · 싸이클)
|
||||
Reference in New Issue
Block a user