Merge remote-tracking branches 'origin/CODEX' and 'origin/main_laptop_1' into sub_laptop_2

This commit is contained in:
2026-11-12 18:06:24 +09:00
68 changed files with 5210 additions and 1010 deletions
@@ -97,21 +97,6 @@ last_updated: 2026-08-15
| 환율·유가 원천과 스냅샷 | ECOS·오피넷 실사 2026-08-14 | [원가_기초데이터_원천 §4](원가_기초데이터_원천.md) |
| ESTX 기계경비 조립 관측 | 2026-07 배포본 재검증 | [원가_기초데이터_원천 §3](원가_기초데이터_원천.md) |
## 7. 품셈에 없는 소형·보조 기계 — EST 구성 기준 · 가격 갱신 필요
## 7. 품셈에 없는 기계 = 사용자 내 기계(요구서 796)
현행 품셈 기계 목록(발전기 25㎾~ 등)에 없는 기계를 EST 단가 구성대로 우리 기계표에 둔 줄. 손료계수 · 연료 · 잡품 · 조종원 = 품셈 값(같은 계열 차용) · 가격 = EST 씨앗(임시) — 조달청 · 행안부 건설기계 표준시장가격 · 제조사 값으로 바꿀 것.
| 기종 | 규격 | 손료계수(10⁻⁷/hr) | 연료 · 잡품 · 전력 | 조종원 | 가격 씨앗(천원) | 시간당 식 | 출처 |
|---|---|---|---|---|---|---|---|
| 발전기 | 5.5㎾ | 2,362(발전기 계열) | 경유 0.625ℓ · 11 % | 일반기계운전사 1 | 1,063 | GC000268 | EST · 2016 사방품셈(안) 같은 꼴 |
| 발전기 | 7㎾ | 2,362 | — | — | 1,828.9 | GC000273(손료만) | 2016 사방품셈(안) 기계경비 |
| 발전기 | 10㎾ | 2,362 | — | — | 3,213 | GC000273 | 2016 사방품셈(안) 기계경비 |
| 대형 브레이커 | 0.18㎥ | 6,601(0.2㎥ 계수) | — | — | 4,559(0.2㎥ 값) | GC000273 | 굴착기 부속 · 우리 0.2㎥ 줄 |
| 에어 호스 | 1.91㎝ | 5,625 | — | — | 105 | GC000273 | EST |
| 모노펌프 | 5-20㎥/hr | 4,346 | — | — | 4,900 | GC000273 | EST |
| 파워트로웰 | 3.73㎾ | 5,313 | 휘발유 1.0ℓ · 10 % · 전력 3.73㎾h | — | 2,680 | GC001195 | EST |
| 콘크리트펌프용 파이프(압송관) | 150㎜ × 1 · 2 · 3m · 곡관 45 · 60 · 90° | 4,500 | — | — | $25 · 37 · 46 · 67 · 84 · 103 × 환율 | GC000273 | EST($ 표기) · 환율 FX000001 |
- 손료 = 버림(가격 × 1,000 × 손료계수 ÷ 10⁷) · 조종원 = 하루 노임 × 1/8(품셈 본문 · EST 의 16/12 · 25/20 뺌).
- 파워트로웰 잡품 = 주연료 줄의 % (전력 줄엔 안 붙임).
- 파이프 가격 = 달러 표기 → 환율 적용값 · 원화 갱신 필요.
품셈 기계표(M01)는 품셈 정본만 둠. 품셈에 없는 기계(발전기 5.5 · 7 · 10㎾ · 소형 브레이커 · 에어 호스 · 모노펌프 · 파워트로웰 · 압송관 등)는 사용자 개인 층의 「내 기계」 — 가격 · 손료계수 · 주연료 ℓ · 잡품 % · 조종원 인 · 직종(우리 노임) · 전력을 사용자가 넣고 시간당 경비는 M01 기계와 같은 식(손료 = 버림(가격 × 1,000 × 손료계수 ÷ 10⁷) · 조종원 = 하루 노임 × 1/8)으로 셈. 경쟁사 불러오기에서 끝내 못 이은 기계는 그 파일 구성(EST 원파일 구성표 · 엑셀 기계경비 · STmate 중기사용료)으로 내 기계를 자동으로 만들고(경쟁사 조종원 노임 환산 ×16/12 · ×25/20 은 안 가져옴 · 달러 표기 = 환율 적용 · 비고 「값 확인 필요」) · 구성 없는 파일은 시간당 값을 그대로 담음.
File diff suppressed because one or more lines are too long
@@ -1,241 +0,0 @@
"""품셈에 없는 소형 · 보조 기계를 우리 기계표 줄로(PLAN 820-1 · 요구 785 ~ 787 · 방향 장부 Q1 · R).
EST 단가 구성 → 우리 줄 — 손료계수 · 연료 · 잡품 · 조종원 = 품셈 값(발전기 계열 2,362 · 대형 브레이커 6,601) ·
가격 = EST 씨앗(출처 「자체」 · 비고 「갱신 필요」) · 콘크리트펌프용 파이프 = 달러값 × 한국은행 환율(FX000001).
계산식:
손료만(브레이커 0.18 · 에어 호스 · 모노펌프 · 파이프 · 발전기 7 · 10㎾) = GC000273 고르기에 더함
발전기 5.5㎾(경유 · 잡품 · 일반기계운전사) = GC000268 고르기에 더함
→ 고르기 더하기는 `build_기계목록_빠진기계_814_1`(로직 · 수식 문서 목록 같이)
파워트로웰(휘발유 · 잡품 · 전력 · 조종원 없음) = 새 기계경비식 GC001195 — 기계 줄 값을 바로 읽음
(`M02_Machine_Hourly._own` 이 고름) · 잡품 = 주연료 줄의 % (전력 줄엔 안 붙임)
더하기만 — 다른 줄 · 서식 그대로 · 두 번 돌려도 같음.
쓰기: venv python resources/master_data/scripts/build_기계_소형보조_820_1.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
DATA = HERE.parent
ROOT = DATA.parents[1]
MACHINE = DATA / "기계.json"
BOOK = DATA / "_키대장.json"
LOGIC = DATA / "로직_건설품셈_08장_건설기계.json"
DOC = DATA / "수식문서" / "수식_건설품셈_08장_건설기계.json"
USD = 1414.9 # 원/USD — 한국은행 환율 FX000001(2026-08-14)
SEED = "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요)"
GEN = {"상각비계수": 1125, "정비비계수": 563, "관리비계수": 674, "손료계수": 2362} # 발전기 계열
BRK = {"상각비계수": 3000, "정비비계수": 2833, "관리비계수": 768, "손료계수": 6601} # 0230-0002
UNIT = {
"가격": "천원", "상각비계수": "10^-7/시간", "정비비계수": "10^-7/시간",
"관리비계수": "10^-7/시간", "손료계수": "10^-7/시간", "주연료": "ℓ/hr", "주연료종류": "",
"잡재료율": "주연료의 %", "조종원": "인/일", "조종원직종": "", "전력": "㎾h/hr",
} # fmt: skip
FIX_SRC = {"발전기": "건설품셈 공통 8-3-8", "브레이커": "건설품셈 공통 8-3-1"}
WAGE = "건설품셈 공통 8-1-3 5 · 건설노임 1050"
SAB = "2016 사방사업표준품셈(안) 기계경비"
PIPE = {"150㎜×1m": 25, "150㎜×2m": 37, "150㎜×3m": 46,
"곡관 150㎜ 45°": 67, "곡관 150㎜ 60°": 84, "곡관 150㎜ 90°": 103} # fmt: skip
def _row(n, gubun, detail, name, spec, value, note, fix=None):
src = {k: (FIX_SRC[fix] if fix and k in GEN else "자체") for k in value}
if "조종원직종" in value:
src["조종원직종"] = WAGE
return {
"키": f"EQ{n:06d}",
"원문번호": f"자체 {name} {spec}",
"원문": "건설품셈",
"구분": gubun,
"상세구분": detail,
"이름": name,
"규격": spec,
"단위": {k: UNIT[k] for k in value},
"값": value,
"출처": src,
"비고": f"{SEED} · {note}",
}
def rows() -> list[dict]:
g, c, t = "[70]기타기계", "[40]콘크리트기계", "[00]토공기계"
fuel5 = {
"주연료": 0.625,
"주연료종류": "경유",
"잡재료율": 11,
"조종원": 1,
"조종원직종": "1050",
}
out = [
_row(656, g, "발전기", "발전기", "5.5㎾", {"가격": 1063, **GEN, **fuel5},
"EST 계 37,760 · 주연료 · 잡품 · 조종원(일반기계운전사) = " + SAB + "과 같음", "발전기"),
_row(657, g, "발전기", "발전기", "7㎾", {"가격": 1828.9, **GEN},
f"가격 = {SAB}(1,828,900 원) · 그 표 = 손료만(연료 · 조종원 없음)", "발전기"),
_row(658, g, "발전기", "발전기", "10㎾", {"가격": 3213, **GEN},
f"가격 = {SAB}(3,213,000 원) · 그 표 = 손료만(연료 · 조종원 없음)", "발전기"),
_row(659, t, "브레이커", "대형 브레이커", "0.18㎥", {"가격": 4559, **BRK},
"손료만(굴착기 부속) · 가격 = 0.2㎥(0230-0002) 값 · 계수 = 0.2㎥ 계수", "브레이커"),
_row(660, g, "에어 호스", "에어 호스", "1.91㎝", {"가격": 105, "손료계수": 5625},
"손료만 · EST 계 59"),
_row(661, g, "모노펌프", "모노펌프", "5-20㎥/hr", {"가격": 4900, "손료계수": 4346},
"손료만 · EST 계 2,129"),
_row(662, c, "파워트로웰", "파워트로웰", "3.73㎾",
{"가격": 2680, "손료계수": 5313, "주연료": 1.0, "주연료종류": "휘발유", "잡재료율": 10, "전력": 3.73},
"주연료 휘발유 · 잡품 · 전력 3.73 ㎾h · 조종원 없음 · EST 계 3,858 · 시간당 경비 = GC001195"),
] # fmt: skip
for i, (spec, usd) in enumerate(PIPE.items()):
out.append(
_row(663 + i, c, "콘크리트펌프용 파이프", "콘크리트펌프용 파이프", spec,
{"가격": round(usd * USD / 1000, 4), "손료계수": 4500},
f"압송관 · 손료만 · ${usd} 표기 · 환율 적용({USD:,} 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요")
) # fmt: skip
return out
EQ = "EQ000662"
LOGIC_ROW = {
"키": "GC001195",
"원문번호": "8",
"구분": "건설품셈 공통",
"상세구분": "08장 건설기계",
"이름": "파워트로웰 시간당 기계경비",
"결과단위": "원/시간",
"출처": "건설품셈 공통 8-1-5 · 8-1-6",
"부품": {"갈래": "기계경비식"},
"소유": "공용",
"비고": f"품셈에 없는 기계 — {SEED} · 기계표 {EQ} 파워트로웰 3.73㎾ 값을 읽음 · 손료 = 가격 × 손료계수 · 잡품 = 주연료 줄의 % · 전력 = 기계표 전력 칸 · 조종원 없음",
"입력": [
{
"이름": "지역",
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
"출처": "건설품셈 공통 8-1-7 5",
},
{
"이름": "전력 계약종별",
"고르기": ["주택용 저압", "일반용(갑)Ⅰ 저압"],
"출처": "유가전력 한전 전기요금표 2025-01-01",
"비고": "품셈·계약예규에 계약종별 규정 없음(전력비는 실비 계상) · 임시전력(갑)은 주택용 저압 요금 그대로(요금표 6.가)",
},
],
"중간": [
{"이름": "손료", "식": f"버림({EQ}.가격 * 1000 * {EQ}.손료계수 / 10000000, 0)", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4"},
],
"호표": [
{"종류": "재료", "요소": "MO:휘발유:{지역}", "이름": "주연료", "단위": "ℓ", "수량": f"{EQ}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1"},
{"종류": "재료", "요소": {"이름": "전력량요금", "계약종별": "입력"}, "이름": "전력", "단위": "㎾h", "수량": f"{EQ}.전력", "비목": "재료비", "출처": "자체"},
],
"덧줄": [
{"이름": "잡재료비", "식": f"줄.'주연료' * {EQ}.잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4 주②"},
{"이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2"},
],
"끝수": "손료 원 미만 버림(건설품셈 공통 8-1-5 12) · 합계 원문 규정 없음",
"변수표": {
"지역": {"기호": "S", "아래": "지역"},
"전력 계약종별": {"기호": "S", "아래": "전력계약종별"},
"손료": {"기호": "C", "아래": "손료"},
},
} # fmt: skip
def _block(obj: dict, nl: str) -> str:
"""줄 하나 — 두 칸 들여쓴 JSON 을 넷 칸 자리에."""
text = json.dumps(obj, ensure_ascii=False, indent=2)
return nl.join(" " + line for line in text.split("\n"))
def _insert_tail(path: Path, objs: list[dict]) -> None:
"""「줄」 목록 끝(`]` 앞)에 더함 — 이미 있는 키는 건너뜀."""
text = path.read_text("utf-8")
have = {r["키"] for r in json.loads(text)["줄"]}
new = [o for o in objs if o["키"] not in have]
if not new:
return
nl = "\r\n" if "\r\n" in text else "\n"
close = text.rstrip().rindex("]")
head = text[:close].rstrip()
body = "".join(f",{nl}{_block(o, nl)}" for o in new)
out = head + body + nl + " " + text[close:]
json.loads(out)
path.write_text(out, "utf-8", newline="")
def _insert_after(path: Path, after_key: str, obj: dict) -> None:
"""키 `after_key` 블록 뒤에 줄 하나 — 이미 있으면 건너뜀."""
text = path.read_text("utf-8")
if f'"키": "{obj["키"]}"' in text:
return
nl = "\r\n" if "\r\n" in text else "\n"
data = json.loads(text)
at = text.index(f'"키": "{after_key}"')
nxt = [r["키"] for r in data["줄"]]
follow = nxt[nxt.index(after_key) + 1]
cut = text.rindex("{", 0, text.index(f'"키": "{follow}"'))
cut = text.rindex(",", at, cut) + 1
out = text[:cut] + nl + _block(obj, nl) + "," + text[cut:]
json.loads(out)
path.write_text(out, "utf-8", newline="")
def _book(machines: list[dict]) -> None:
text = BOOK.read_text("utf-8")
nl = "\r\n" if "\r\n" in text else "\n"
adds = [(r["키"], r["원문번호"], "기계.json", "EQ000655") for r in machines]
adds.append(("GC001195", "공통 8 파워트로웰 시간당 경비", LOGIC.name, "GC001194"))
for key, number, file, after in adds:
if f'"{key}":' in text:
continue
line = f' "{key}": {json.dumps({"원문번호": number, "파일": file}, ensure_ascii=False)},'
prev = max((k for k, *_ in adds if k[:2] == key[:2] and f'"{k}":' in text), default=after)
at = text.index(f' "{prev}":')
end = text.index(nl, at) + len(nl)
text = text[:end] + line + nl + text[end:]
book = json.loads(text)
nums = {"EQ": 669, "GC": 1196}
for tid, n in nums.items():
old = book["다음"][tid]
if old < n:
text = text.replace(f'"{tid}": {old},', f'"{tid}": {n},', 1)
json.loads(text)
BOOK.write_text(text, "utf-8", newline="")
def _doc() -> None:
"""GC001195 수식 문서 — 옛 로직 변환(`master_doc_convert`) · 파일 끝에 더함."""
for p in (str(ROOT), str(HERE)):
if p not in sys.path:
sys.path.insert(0, p)
import master_doc_convert as cv
import master_doc_link as ml
from M01_MasterData import M01_MasterData_Store as m01
data = json.loads(DOC.read_text("utf-8"))
if any(d.get("키") == "GC001195" for d in data["줄"]):
return
files, whole = m01.loaded()
doc, notes = cv.convert(whole, LOGIC_ROW)
if notes:
print("변환 알림:", notes)
data["줄"].append(doc)
DOC.write_text(ml.dump(data), "utf-8", newline="")
def main() -> None:
machines = rows()
_insert_tail(MACHINE, machines)
_book(machines)
_insert_after(LOGIC, "GC000276", LOGIC_ROW)
_doc()
import build_기계목록_빠진기계_814_1 as b814
b814.main()
if __name__ == "__main__":
sys.path.insert(0, str(HERE))
main()
@@ -0,0 +1,78 @@
"""기계 손료계수 · 시간당 경비 미리보기(PLAN 821-2 · 요구서 796 1 장).
⚠ 짝: `common_util/common_util_machine_coef.ts` — 같은 셈 · 같은 차례(거울 시험
`resources/tester/test_821_2_machine_coef.py`). 한쪽만 고치면 화면과 저장이 갈림.
계수(건설품셈 공통 8-1-5 · 시간당 10^-7):
상각비계수 = 상각비율 ÷ 내용시간 · 정비비계수 = 정비비율 ÷ 내용시간 ·
관리비계수 = 연간관리비율 × 평균취득가격 비율 ÷ 연간표준가동시간 ·
평균취득가격 비율 = (1.1 N + 0.9) ÷ 2N · N = 내용시간 ÷ 연간표준가동시간 ·
계수마다 정수로 반올림 · 손료계수 = 셋의 합.
미리보기 = 서버 기계경비식(GC000267 ~)을 기본 옵션(보정 없음)으로 부른 값과 같은 끝수 —
손료 = 가격(천원) × 1000 × 손료계수 ÷ 10^7 원 미만 버림 · 줄 금액은 0.1 원 아래 버림 ·
잡품 = 버린 주연료 금액 × 잡재료율 % · 비목 합은 원 미만 버림(B09 끝수 `master_cut.engine`).
정본은 서버 식(`M02_Machine_Hourly`) — 이것은 화면 즉시 셈용.
"""
from __future__ import annotations
import math
INPUTS = ("내용시간", "연간표준가동시간", "상각비율", "정비비율", "연간관리비율")
COEFS = ("상각비계수", "정비비계수", "관리비계수", "손료계수")
_E = 1e-6 # 끊는 경계 부동소수 흔들림 막음
def _num(v) -> float:
return float(v) if isinstance(v, (int, float)) and not isinstance(v, bool) else 0.0
def _half_up(x: float) -> int:
return math.floor(x + 0.5 + _E)
def _tenth(x: float) -> float:
"""줄 금액 = 0.1 원 아래 버림(B09 끝수 줄 자리)."""
return math.floor(x * 10 + _E) / 10
def coef(life, yearly, dep, rep, mgmt) -> dict | None:
"""(내용시간 · 연간표준가동시간 · 상각 · 정비 · 연간관리 비율) → 계수 넷 · 못 셈 = None."""
life, yearly, dep, rep, mgmt = (_num(x) for x in (life, yearly, dep, rep, mgmt))
if life <= 0 or yearly <= 0:
return None
n = life / yearly
s = _half_up(dep / life * 1e7)
j = _half_up(rep / life * 1e7)
g = _half_up(mgmt * (1.1 * n + 0.9) / (2 * n) / yearly * 1e7)
return {"상각비계수": s, "정비비계수": j, "관리비계수": g, "손료계수": s + j + g}
def coef_of(values: dict) -> dict | None:
"""값 묶음(기계 줄 `값`)에서 품셈 입력 다섯이 다 수일 때만 계수 넷."""
if not all(isinstance(values.get(k), (int, float)) for k in INPUTS):
return None
return coef(*(values[k] for k in INPUTS))
def preview(
values: dict, fuel: float, wage: float, power: float = 0.0, hour: float = 1 / 8
) -> dict:
"""값 묶음 + 유가(원/ℓ) · 노임(원/일) · 전력(원/kWh) · 시간환산 → 시간당 줄 · 비목 · 계."""
loss = math.floor(_num(values.get("가격")) * 1000 * _num(values.get("손료계수")) / 1e7 + _E)
oil = _tenth(_num(values.get("주연료")) * _num(fuel))
misc = _tenth(oil * _num(values.get("잡재료율")) / 100)
elec = _tenth(_num(values.get("전력")) * _num(power))
crew = _tenth(_num(values.get("조종원")) * hour * _num(wage))
labor, material = math.floor(crew + _E), math.floor(oil + misc + elec + _E)
return {
"손료": loss,
"주연료": oil,
"잡품": misc,
"전력": elec,
"조종원": crew,
"노무비": labor,
"재료비": material,
"경비": loss,
"계": labor + material + loss,
}
+3 -353
View File
@@ -33404,10 +33404,9 @@
"조종원": "건설품셈 공통 8-4-8",
"조종원직종": "건설품셈 공통 8-1-3 5"
},
"값원문": {
"8-4-8 주연료": "휘발유1"
},
"비고": "주연료종류: 원문 글을 가름"
"값원문": { "8-4-8 주연료": "휘발유1" },
"비고": "주연료종류: 원문 글을 가름",
"별칭": "파워트로웰"
},
{
"키": "EQ000535",
@@ -39717,355 +39716,6 @@
"가격": "산림품셈 10-9"
},
"비고": "원문 이름: 4륜구동 1톤 덤프트럭 · 중기가격 = 조달청 등재가격(기본값 28,000 천원 = EST 2026-07 「덤프트럭 1.0Ton」 · 바꿀 수 있음) · 작업량은 건설품셈 · 손료 및 운전경비 = 2.5톤 덤프트럭(0602-0025) · 주 연료 = 2.5톤의 80% · 시간당 경비 = GF000145"
},
{
"키": "EQ000656",
"원문번호": "자체 발전기 5.5㎾",
"원문": "건설품셈",
"구분": "[70]기타기계",
"상세구분": "발전기",
"이름": "발전기",
"규격": "5.5㎾",
"단위": {
"가격": "천원",
"상각비계수": "10^-7/시간",
"정비비계수": "10^-7/시간",
"관리비계수": "10^-7/시간",
"손료계수": "10^-7/시간",
"주연료": "ℓ/hr",
"주연료종류": "",
"잡재료율": "주연료의 %",
"조종원": "인/일",
"조종원직종": ""
},
"값": {
"가격": 1063,
"상각비계수": 1125,
"정비비계수": 563,
"관리비계수": 674,
"손료계수": 2362,
"주연료": 0.625,
"주연료종류": "경유",
"잡재료율": 11,
"조종원": 1,
"조종원직종": "1050"
},
"출처": {
"가격": "자체",
"상각비계수": "건설품셈 공통 8-3-8",
"정비비계수": "건설품셈 공통 8-3-8",
"관리비계수": "건설품셈 공통 8-3-8",
"손료계수": "건설품셈 공통 8-3-8",
"주연료": "자체",
"주연료종류": "자체",
"잡재료율": "자체",
"조종원": "자체",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1050"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · EST 계 37,760 · 주연료 · 잡품 · 조종원(일반기계운전사) = 2016 사방사업표준품셈(안) 기계경비과 같음"
},
{
"키": "EQ000657",
"원문번호": "자체 발전기 7㎾",
"원문": "건설품셈",
"구분": "[70]기타기계",
"상세구분": "발전기",
"이름": "발전기",
"규격": "7㎾",
"단위": {
"가격": "천원",
"상각비계수": "10^-7/시간",
"정비비계수": "10^-7/시간",
"관리비계수": "10^-7/시간",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 1828.9,
"상각비계수": 1125,
"정비비계수": 563,
"관리비계수": 674,
"손료계수": 2362
},
"출처": {
"가격": "자체",
"상각비계수": "건설품셈 공통 8-3-8",
"정비비계수": "건설품셈 공통 8-3-8",
"관리비계수": "건설품셈 공통 8-3-8",
"손료계수": "건설품셈 공통 8-3-8"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 가격 = 2016 사방사업표준품셈(안) 기계경비(1,828,900 원) · 그 표 = 손료만(연료 · 조종원 없음)"
},
{
"키": "EQ000658",
"원문번호": "자체 발전기 10㎾",
"원문": "건설품셈",
"구분": "[70]기타기계",
"상세구분": "발전기",
"이름": "발전기",
"규격": "10㎾",
"단위": {
"가격": "천원",
"상각비계수": "10^-7/시간",
"정비비계수": "10^-7/시간",
"관리비계수": "10^-7/시간",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 3213,
"상각비계수": 1125,
"정비비계수": 563,
"관리비계수": 674,
"손료계수": 2362
},
"출처": {
"가격": "자체",
"상각비계수": "건설품셈 공통 8-3-8",
"정비비계수": "건설품셈 공통 8-3-8",
"관리비계수": "건설품셈 공통 8-3-8",
"손료계수": "건설품셈 공통 8-3-8"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 가격 = 2016 사방사업표준품셈(안) 기계경비(3,213,000 원) · 그 표 = 손료만(연료 · 조종원 없음)"
},
{
"키": "EQ000659",
"원문번호": "자체 대형 브레이커 0.18㎥",
"원문": "건설품셈",
"구분": "[00]토공기계",
"상세구분": "브레이커",
"이름": "대형 브레이커",
"규격": "0.18㎥",
"단위": {
"가격": "천원",
"상각비계수": "10^-7/시간",
"정비비계수": "10^-7/시간",
"관리비계수": "10^-7/시간",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 4559,
"상각비계수": 3000,
"정비비계수": 2833,
"관리비계수": 768,
"손료계수": 6601
},
"출처": {
"가격": "자체",
"상각비계수": "건설품셈 공통 8-3-1",
"정비비계수": "건설품셈 공통 8-3-1",
"관리비계수": "건설품셈 공통 8-3-1",
"손료계수": "건설품셈 공통 8-3-1"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 손료만(굴착기 부속) · 가격 = 0.2㎥(0230-0002) 값 · 계수 = 0.2㎥ 계수"
},
{
"키": "EQ000660",
"원문번호": "자체 에어 호스 1.91㎝",
"원문": "건설품셈",
"구분": "[70]기타기계",
"상세구분": "에어 호스",
"이름": "에어 호스",
"규격": "1.91㎝",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 105,
"손료계수": 5625
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 손료만 · EST 계 59"
},
{
"키": "EQ000661",
"원문번호": "자체 모노펌프 5-20㎥/hr",
"원문": "건설품셈",
"구분": "[70]기타기계",
"상세구분": "모노펌프",
"이름": "모노펌프",
"규격": "5-20㎥/hr",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 4900,
"손료계수": 4346
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 손료만 · EST 계 2,129"
},
{
"키": "EQ000662",
"원문번호": "자체 파워트로웰 3.73㎾",
"원문": "건설품셈",
"구분": "[40]콘크리트기계",
"상세구분": "파워트로웰",
"이름": "파워트로웰",
"규격": "3.73㎾",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간",
"주연료": "ℓ/hr",
"주연료종류": "",
"잡재료율": "주연료의 %",
"전력": "㎾h/hr"
},
"값": {
"가격": 2680,
"손료계수": 5313,
"주연료": 1.0,
"주연료종류": "휘발유",
"잡재료율": 10,
"전력": 3.73
},
"출처": {
"가격": "자체",
"손료계수": "자체",
"주연료": "자체",
"주연료종류": "자체",
"잡재료율": "자체",
"전력": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 주연료 휘발유 · 잡품 · 전력 3.73 ㎾h · 조종원 없음 · EST 계 3,858 · 시간당 경비 = GC001195"
},
{
"키": "EQ000663",
"원문번호": "자체 콘크리트펌프용 파이프 150㎜×1m",
"원문": "건설품셈",
"구분": "[40]콘크리트기계",
"상세구분": "콘크리트펌프용 파이프",
"이름": "콘크리트펌프용 파이프",
"규격": "150㎜×1m",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 35.3725,
"손료계수": 4500
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 압송관 · 손료만 · $25 표기 · 환율 적용(1,414.9 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요"
},
{
"키": "EQ000664",
"원문번호": "자체 콘크리트펌프용 파이프 150㎜×2m",
"원문": "건설품셈",
"구분": "[40]콘크리트기계",
"상세구분": "콘크리트펌프용 파이프",
"이름": "콘크리트펌프용 파이프",
"규격": "150㎜×2m",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 52.3513,
"손료계수": 4500
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 압송관 · 손료만 · $37 표기 · 환율 적용(1,414.9 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요"
},
{
"키": "EQ000665",
"원문번호": "자체 콘크리트펌프용 파이프 150㎜×3m",
"원문": "건설품셈",
"구분": "[40]콘크리트기계",
"상세구분": "콘크리트펌프용 파이프",
"이름": "콘크리트펌프용 파이프",
"규격": "150㎜×3m",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 65.0854,
"손료계수": 4500
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 압송관 · 손료만 · $46 표기 · 환율 적용(1,414.9 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요"
},
{
"키": "EQ000666",
"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 45°",
"원문": "건설품셈",
"구분": "[40]콘크리트기계",
"상세구분": "콘크리트펌프용 파이프",
"이름": "콘크리트펌프용 파이프",
"규격": "곡관 150㎜ 45°",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 94.7983,
"손료계수": 4500
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 압송관 · 손료만 · $67 표기 · 환율 적용(1,414.9 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요"
},
{
"키": "EQ000667",
"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 60°",
"원문": "건설품셈",
"구분": "[40]콘크리트기계",
"상세구분": "콘크리트펌프용 파이프",
"이름": "콘크리트펌프용 파이프",
"규격": "곡관 150㎜ 60°",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 118.8516,
"손료계수": 4500
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 압송관 · 손료만 · $84 표기 · 환율 적용(1,414.9 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요"
},
{
"키": "EQ000668",
"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 90°",
"원문": "건설품셈",
"구분": "[40]콘크리트기계",
"상세구분": "콘크리트펌프용 파이프",
"이름": "콘크리트펌프용 파이프",
"규격": "곡관 150㎜ 90°",
"단위": {
"가격": "천원",
"손료계수": "10^-7/시간"
},
"값": {
"가격": 145.7347,
"손료계수": 4500
},
"출처": {
"가격": "자체",
"손료계수": "자체"
},
"비고": "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 압송관 · 손료만 · $103 표기 · 환율 적용(1,414.9 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요"
}
]
}
@@ -284,8 +284,7 @@
"9090-1300",
"9090-1400",
"9090-1500",
"9090-1600",
"자체 발전기 5.5㎾"
"9090-1600"
]
},
{
@@ -1089,18 +1088,7 @@
"9080-1500",
"9080-1750",
"9080-2000",
"9080-3000",
"자체 발전기 7㎾",
"자체 발전기 10㎾",
"자체 대형 브레이커 0.18㎥",
"자체 에어 호스 1.91㎝",
"자체 모노펌프 5-20㎥/hr",
"자체 콘크리트펌프용 파이프 150㎜×1m",
"자체 콘크리트펌프용 파이프 150㎜×2m",
"자체 콘크리트펌프용 파이프 150㎜×3m",
"자체 콘크리트펌프용 파이프 곡관 150㎜ 45°",
"자체 콘크리트펌프용 파이프 곡관 150㎜ 60°",
"자체 콘크리트펌프용 파이프 곡관 150㎜ 90°"
"9080-3000"
]
}
],
@@ -1416,114 +1404,6 @@
"시간환산": { "기호": "Q", "아래": "시간환산" }
}
},
{
"키": "GC001195",
"원문번호": "8",
"구분": "건설품셈 공통",
"상세구분": "08장 건설기계",
"이름": "파워트로웰 시간당 기계경비",
"결과단위": "원/시간",
"출처": "건설품셈 공통 8-1-5 · 8-1-6",
"부품": {
"갈래": "기계경비식"
},
"소유": "공용",
"비고": "품셈에 없는 기계 — EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요) · 기계표 EQ000662 파워트로웰 3.73㎾ 값을 읽음 · 손료 = 가격 × 손료계수 · 잡품 = 주연료 줄의 % · 전력 = 기계표 전력 칸 · 조종원 없음",
"입력": [
{
"이름": "지역",
"고르기": [
"전국평균",
"전국",
"서울",
"경기",
"강원",
"충북",
"충남",
"전북",
"경북",
"경남",
"부산",
"제주",
"대구",
"인천",
"대전",
"울산",
"세종",
"전남광주"
],
"출처": "건설품셈 공통 8-1-7 5"
},
{
"이름": "전력 계약종별",
"고르기": [
"주택용 저압",
"일반용(갑)Ⅰ 저압"
],
"출처": "유가전력 한전 전기요금표 2025-01-01",
"비고": "품셈·계약예규에 계약종별 규정 없음(전력비는 실비 계상) · 임시전력(갑)은 주택용 저압 요금 그대로(요금표 6.가)"
}
],
"중간": [
{
"이름": "손료",
"식": "버림(EQ000662.가격 * 1000 * EQ000662.손료계수 / 10000000, 0)",
"출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4"
}
],
"호표": [
{
"종류": "재료",
"요소": "MO:휘발유:{지역}",
"이름": "주연료",
"단위": "ℓ",
"수량": "EQ000662.주연료",
"비목": "재료비",
"출처": "건설품셈 공통 8-4-1"
},
{
"종류": "재료",
"요소": {
"이름": "전력량요금",
"계약종별": "입력"
},
"이름": "전력",
"단위": "㎾h",
"수량": "EQ000662.전력",
"비목": "재료비",
"출처": "자체"
}
],
"덧줄": [
{
"이름": "잡재료비",
"식": "줄.'주연료' * EQ000662.잡재료율 / 100",
"비목": "재료비",
"출처": "건설품셈 공통 8-4 주②"
},
{
"이름": "기계손료",
"식": "손료",
"비목": "경비",
"출처": "건설품셈 공통 8-1-6 2"
}
],
"끝수": "손료 원 미만 버림(건설품셈 공통 8-1-5 12) · 합계 원문 규정 없음",
"변수표": {
"지역": {
"기호": "S",
"아래": "지역"
},
"전력 계약종별": {
"기호": "S",
"아래": "전력계약종별"
},
"손료": {
"기호": "C",
"아래": "손료"
}
}
},
{
"키": "GC001143",
"원문번호": "8-1-4",
@@ -62,7 +62,7 @@
"갈래": "기계경비식"
},
"변수": [
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "0602-0060", "0602-0105", "1630-0080", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060", "0211-0060", "0211-0080", "0211-0100", "0221-0040", "0221-0070", "0302-0025", "0302-0172", "0302-0229", "0302-0287", "0302-0350", "0302-0500", "0406-0054", "0406-0115", "0406-0161", "0406-0206", "0503-0036", "0602-0200", "0602-0240", "0602-0320", "1106-0015", "1206-0010", "1206-0014", "1209-0001", "1209-0002", "1209-0004", "1209-0006", "1209-0007", "1209-0008", "1209-0013", "1306-0044", "1306-0060", "1406-0025", "1506-0011", "1506-0012", "1506-0015", "1506-0019", "1506-0025", "1506-0030", "1506-0037", "2602-0015", "2602-0025", "2602-0035", "2602-0045", "2702-0040", "3530-0015", "3530-0036", "3601-0204", "3601-0302", "3701-0200", "3805-0120", "4504-0021", "4504-0028", "5405-0150", "5701-0010", "7106-0035", "7505-0125", "7505-0200", "7505-0500", "7505-0700", "9010-0003", "9010-0006", "9010-0010", "9010-0012", "9010-0020", "9010-0022", "9010-0033", "9010-0040", "9010-0044", "9010-0060", "9010-0080", "9020-0010", "9020-0015", "9020-0016", "9020-0022", "9020-0035", "9020-0050", "9020-0072", "9020-0160", "9020-0180", "9020-0200", "9050-0075", "9050-0150", "9050-0450", "9050-0750", "9050-0850", "9090-0800", "9090-1000", "9090-1200", "9090-1300", "9090-1400", "9090-1500", "9090-1600", "자체 발전기 5.5㎾"], "값": "1730-0015"}},
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "0602-0060", "0602-0105", "1630-0080", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060", "0211-0060", "0211-0080", "0211-0100", "0221-0040", "0221-0070", "0302-0025", "0302-0172", "0302-0229", "0302-0287", "0302-0350", "0302-0500", "0406-0054", "0406-0115", "0406-0161", "0406-0206", "0503-0036", "0602-0200", "0602-0240", "0602-0320", "1106-0015", "1206-0010", "1206-0014", "1209-0001", "1209-0002", "1209-0004", "1209-0006", "1209-0007", "1209-0008", "1209-0013", "1306-0044", "1306-0060", "1406-0025", "1506-0011", "1506-0012", "1506-0015", "1506-0019", "1506-0025", "1506-0030", "1506-0037", "2602-0015", "2602-0025", "2602-0035", "2602-0045", "2702-0040", "3530-0015", "3530-0036", "3601-0204", "3601-0302", "3701-0200", "3805-0120", "4504-0021", "4504-0028", "5405-0150", "5701-0010", "7106-0035", "7505-0125", "7505-0200", "7505-0500", "7505-0700", "9010-0003", "9010-0006", "9010-0010", "9010-0012", "9010-0020", "9010-0022", "9010-0033", "9010-0040", "9010-0044", "9010-0060", "9010-0080", "9020-0010", "9020-0015", "9020-0016", "9020-0022", "9020-0035", "9020-0050", "9020-0072", "9020-0160", "9020-0180", "9020-0200", "9050-0075", "9050-0150", "9050-0450", "9050-0750", "9050-0850", "9090-0800", "9090-1000", "9090-1200", "9090-1300", "9090-1400", "9090-1500", "9090-1600"], "값": "1730-0015"}},
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
@@ -306,7 +306,7 @@
"결과단위": "원/시간",
"출처": "건설품셈 공통 8-1-5 · 8-1-6",
"변수": [
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["7620-0003", "7621-0100", "7995-0050", "5210-0013", "5210-0027", "5220-0015", "7730-0150", "6516-0060", "6516-0120", "6516-0250", "6516-0300", "6105-0190", "6202-0060", "0230-0002", "6410-0080", "6410-0100", "6410-0120", "6410-0150", "6410-0200", "6901-0010", "6530-0090", "6530-0120", "6517-0100", "6517-0150", "6517-0200", "6517-0250", "6517-0300", "6510-0100", "6510-0150", "6510-0200", "6510-0250", "6510-0300", "6518-0100", "6518-0150", "6518-0200", "6518-0250", "6518-0300", "5501-0080", "7750-0016", "7750-0025", "7730-0050", "5401-0017", "5202-0127", "5202-0240", "5202-0300", "7611-0200", "7612-0200", "5204-0200", "5204-0300", "5204-0400", "5204-0500", "5204-0600", "5702-0095", "3430-0400", "7431-1300", "7740-0080", "7740-0150", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "2208-5008", "2208-5010", "2208-5012", "2208-5016", "2208-5020", "2105-0002", "2105-0003", "2105-0005", "2105-0010", "2105-0015", "2105-0018", "0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200", "6105-0390", "6202-0125", "7830-0081", "7950-0149", "7951-0066", "7952-0246", "7953-3400", "7954-1119", "7611-0300", "7431-1100", "7431-2300", "7431-2500", "7611-0400", "7611-0500", "7614-0300", "0250-0100", "5210-0010", "5210-0019", "0103-0016", "0103-0019", "0103-0023", "0103-0027", "0103-0032", "0230-0004", "0230-0006", "0230-0007", "0230-0008", "0230-0010", "0240-0007", "0250-0080", "0407-0054", "0407-0092", "0407-0107", "0407-0161", "0407-0206", "0610-0150", "0610-0200", "0610-0240", "2115-0024", "2115-0031", "2115-0036", "2116-0031", "2116-0036", "2117-0022", "2117-0027", "2118-0010", "2210-0145", "3430-0300", "3450-0642", "4115-0100", "4115-0150", "4115-0200", "4115-0300", "4505-0015", "4505-0026", "4611-0075", "5105-0050", "5105-0100", "5105-0150", "5105-0200", "5111-0040", "5111-0050", "5111-0060", "5111-0076", "5111-0091", "5112-0001", "5112-0002", "5112-0003", "5112-0004", "5112-0005", "5113-0001", "5113-0002", "5113-0003", "5113-0004", "5113-0005", "5113-0006", "5113-0007", "5113-0008", "5113-0009", "5113-0010", "5113-0011", "5114-0001", "5114-0002", "5114-0003", "5114-0004", "5114-0005", "5114-0006", "5114-0007", "5114-0008", "5115-0030", "5115-0055", "5115-0075", "5115-0095", "5116-0001", "5116-0002", "5116-0003", "5116-0004", "5116-0005", "5116-0006", "5116-0007", "5116-0008", "5117-0001", "5117-0002", "5117-0003", "5117-0004", "5117-0005", "5117-0006", "5117-0007", "5117-0008", "5118-0001", "5118-0002", "5118-0003", "5118-0004", "5118-0005", "5118-0006", "5118-0007", "5119-0625", "5203-1800", "5203-2200", "5203-2700", "5203-3500", "5203-4500", "5330-0074", "5805-0002", "5805-0003", "5901-0006", "5901-0010", "5901-0016", "6202-0200", "6408-0015", "6408-0020", "6408-0030", "6408-0040", "6408-0050", "6408-0085", "6408-0100", "6515-0090", "6530-0030", "6530-0040", "6530-0045", "6530-0060", "6532-0220", "6550-0130", "7103-0010", "7104-0010", "7202-0008", "7202-0010", "7205-0125", "7206-0020", "7206-0070", "7206-0100", "7430-1100", "7430-1300", "7430-1500", "7430-2300", "7430-2500", "7612-0300", "7612-0400", "7613-0075", "7613-0150", "7613-0300", "7613-0400", "7613-0600", "7613-0900", "7620-0002", "7730-0080", "7730-0100", "7730-0125", "7740-0100", "7770-0004", "7930-0001", "7930-0002", "7930-0003", "7930-0005", "7930-0007", "7930-0010", "7930-0015", "7930-0020", "7930-0025", "7930-0030", "7930-0040", "7930-0050", "7930-0075", "7930-0100", "7935-0180", "7991-0050", "7991-0100", "7991-0500", "7993-0020", "8201-0100", "8202-0100", "8203-0019", "8204-0100", "8205-0100", "9060-0060", "9060-0100", "9060-0200", "9060-0300", "9060-0500", "9060-0600", "9080-0050", "9080-0080", "9080-0100", "9080-0120", "9080-0150", "9080-0200", "9080-0300", "9080-0500", "9080-0700", "9080-1000", "9080-1100", "9080-1400", "9080-1500", "9080-1750", "9080-2000", "9080-3000", "자체 발전기 7㎾", "자체 발전기 10㎾", "자체 대형 브레이커 0.18㎥", "자체 에어 호스 1.91㎝", "자체 모노펌프 5-20㎥/hr", "자체 콘크리트펌프용 파이프 150㎜×1m", "자체 콘크리트펌프용 파이프 150㎜×2m", "자체 콘크리트펌프용 파이프 150㎜×3m", "자체 콘크리트펌프용 파이프 곡관 150㎜ 45°", "자체 콘크리트펌프용 파이프 곡관 150㎜ 60°", "자체 콘크리트펌프용 파이프 곡관 150㎜ 90°"], "값": "7620-0003"}},
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["7620-0003", "7621-0100", "7995-0050", "5210-0013", "5210-0027", "5220-0015", "7730-0150", "6516-0060", "6516-0120", "6516-0250", "6516-0300", "6105-0190", "6202-0060", "0230-0002", "6410-0080", "6410-0100", "6410-0120", "6410-0150", "6410-0200", "6901-0010", "6530-0090", "6530-0120", "6517-0100", "6517-0150", "6517-0200", "6517-0250", "6517-0300", "6510-0100", "6510-0150", "6510-0200", "6510-0250", "6510-0300", "6518-0100", "6518-0150", "6518-0200", "6518-0250", "6518-0300", "5501-0080", "7750-0016", "7750-0025", "7730-0050", "5401-0017", "5202-0127", "5202-0240", "5202-0300", "7611-0200", "7612-0200", "5204-0200", "5204-0300", "5204-0400", "5204-0500", "5204-0600", "5702-0095", "3430-0400", "7431-1300", "7740-0080", "7740-0150", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "2208-5008", "2208-5010", "2208-5012", "2208-5016", "2208-5020", "2105-0002", "2105-0003", "2105-0005", "2105-0010", "2105-0015", "2105-0018", "0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200", "6105-0390", "6202-0125", "7830-0081", "7950-0149", "7951-0066", "7952-0246", "7953-3400", "7954-1119", "7611-0300", "7431-1100", "7431-2300", "7431-2500", "7611-0400", "7611-0500", "7614-0300", "0250-0100", "5210-0010", "5210-0019", "0103-0016", "0103-0019", "0103-0023", "0103-0027", "0103-0032", "0230-0004", "0230-0006", "0230-0007", "0230-0008", "0230-0010", "0240-0007", "0250-0080", "0407-0054", "0407-0092", "0407-0107", "0407-0161", "0407-0206", "0610-0150", "0610-0200", "0610-0240", "2115-0024", "2115-0031", "2115-0036", "2116-0031", "2116-0036", "2117-0022", "2117-0027", "2118-0010", "2210-0145", "3430-0300", "3450-0642", "4115-0100", "4115-0150", "4115-0200", "4115-0300", "4505-0015", "4505-0026", "4611-0075", "5105-0050", "5105-0100", "5105-0150", "5105-0200", "5111-0040", "5111-0050", "5111-0060", "5111-0076", "5111-0091", "5112-0001", "5112-0002", "5112-0003", "5112-0004", "5112-0005", "5113-0001", "5113-0002", "5113-0003", "5113-0004", "5113-0005", "5113-0006", "5113-0007", "5113-0008", "5113-0009", "5113-0010", "5113-0011", "5114-0001", "5114-0002", "5114-0003", "5114-0004", "5114-0005", "5114-0006", "5114-0007", "5114-0008", "5115-0030", "5115-0055", "5115-0075", "5115-0095", "5116-0001", "5116-0002", "5116-0003", "5116-0004", "5116-0005", "5116-0006", "5116-0007", "5116-0008", "5117-0001", "5117-0002", "5117-0003", "5117-0004", "5117-0005", "5117-0006", "5117-0007", "5117-0008", "5118-0001", "5118-0002", "5118-0003", "5118-0004", "5118-0005", "5118-0006", "5118-0007", "5119-0625", "5203-1800", "5203-2200", "5203-2700", "5203-3500", "5203-4500", "5330-0074", "5805-0002", "5805-0003", "5901-0006", "5901-0010", "5901-0016", "6202-0200", "6408-0015", "6408-0020", "6408-0030", "6408-0040", "6408-0050", "6408-0085", "6408-0100", "6515-0090", "6530-0030", "6530-0040", "6530-0045", "6530-0060", "6532-0220", "6550-0130", "7103-0010", "7104-0010", "7202-0008", "7202-0010", "7205-0125", "7206-0020", "7206-0070", "7206-0100", "7430-1100", "7430-1300", "7430-1500", "7430-2300", "7430-2500", "7612-0300", "7612-0400", "7613-0075", "7613-0150", "7613-0300", "7613-0400", "7613-0600", "7613-0900", "7620-0002", "7730-0080", "7730-0100", "7730-0125", "7740-0100", "7770-0004", "7930-0001", "7930-0002", "7930-0003", "7930-0005", "7930-0007", "7930-0010", "7930-0015", "7930-0020", "7930-0025", "7930-0030", "7930-0040", "7930-0050", "7930-0075", "7930-0100", "7935-0180", "7991-0050", "7991-0100", "7991-0500", "7993-0020", "8201-0100", "8202-0100", "8203-0019", "8204-0100", "8205-0100", "9060-0060", "9060-0100", "9060-0200", "9060-0300", "9060-0500", "9060-0600", "9080-0050", "9080-0080", "9080-0100", "9080-0120", "9080-0150", "9080-0200", "9080-0300", "9080-0500", "9080-0700", "9080-1000", "9080-1100", "9080-1400", "9080-1500", "9080-1750", "9080-2000", "9080-3000"], "값": "7620-0003"}},
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
{"열쇠": "k_기계손료", "기호": "k", "아래": "기계손료", "위": "", "이름": "EQ:{기계}.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "손료계수"}}
@@ -2213,43 +2213,6 @@
"비목": {"방법": "버림", "자리": 0},
"끝전": "비목버림"
}
},
{
"꼴": "수식문서",
"판": 1,
"키": "GC001195",
"이름": "파워트로웰 시간당 기계경비",
"결과단위": "원/시간",
"출처": "건설품셈 공통 8-1-5 · 8-1-6",
"부품": {
"갈래": "기계경비식"
},
"변수": [
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
{"열쇠": "전력 계약종별", "기호": "S", "아래": "전력계약종별", "위": "", "이름": "전력 계약종별", "단위": "", "출처": "유가전력 한전 전기요금표 2025-01-01", "연결": {"갈래": "선택", "목록": ["주택용 저압", "일반용(갑)Ⅰ 저압"], "값": "주택용 저압"}},
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
{"열쇠": "P_파워트로웰", "기호": "P", "아래": "파워트로웰", "위": "", "이름": "EQ000662.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000662", "칸": "가격", "계약종별": "S_전력계약종별"}},
{"열쇠": "k_파워트로웰", "기호": "k", "아래": "파워트로웰", "위": "", "이름": "EQ000662.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000662", "칸": "손료계수", "계약종별": "S_전력계약종별"}},
{"열쇠": "P_파워트로웰2", "기호": "P", "아래": "파워트로웰2", "위": "", "이름": "EQ000662.주연료", "단위": "", "연결": {"갈래": "요소", "키": "EQ000662", "칸": "주연료", "계약종별": "S_전력계약종별"}},
{"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}", "계약종별": "S_전력계약종별"}},
{"열쇠": "P_파워트로웰3", "기호": "P", "아래": "파워트로웰3", "위": "", "이름": "EQ000662.전력", "단위": "", "연결": {"갈래": "요소", "키": "EQ000662", "칸": "전력", "계약종별": "S_전력계약종별"}},
{"열쇠": "P_전력", "기호": "P", "아래": "전력", "위": "", "이름": "전력단가", "단위": "", "연결": {"갈래": "요소", "재료": {"이름": "전력량요금", "계약종별": "입력"}, "단위": "㎾h", "계약종별": "S_전력계약종별"}},
{"열쇠": "r_파워트로웰", "기호": "r", "아래": "파워트로웰", "위": "", "이름": "EQ000662.잡재료율", "단위": "", "연결": {"갈래": "요소", "키": "EQ000662", "칸": "잡재료율", "계약종별": "S_전력계약종별"}}
],
"식": [
{"기호": "C_손료", "식": "버림(P_파워트로웰 × 1000 × k_파워트로웰 ÷ 10000000, 0)"}
],
"금액": [
{"이름": "주연료", "단위": "ℓ", "비목": "재료비", "수량": "P_파워트로웰2", "단가": "P_휘발유", "출처": "건설품셈 공통 8-4-1", "기호": "C_주연료"},
{"이름": "전력", "단위": "㎾h", "비목": "재료비", "수량": "P_파워트로웰3", "단가": "P_전력", "출처": "자체"},
{"이름": "잡재료비", "단위": "식", "비목": "재료비", "단가": "C_주연료 × r_파워트로웰 ÷ 100", "출처": "건설품셈 공통 8-4 주②"},
{"이름": "기계손료", "단위": "식", "비목": "경비", "단가": "C_손료", "출처": "건설품셈 공통 8-1-6 2"}
],
"끝수": {
"줄": {"방법": "버림", "자리": 1},
"비목": {"방법": "버림", "자리": 0},
"끝전": "비목버림"
}
}
]
}
+3 -1
View File
@@ -55,7 +55,9 @@ def test_길_앞머리_M02_공용화면():
assert "cp ? cp.route(key, path) : opt.base + path" in scr
assert "{ 문서: doc, 끊음: cut, 판: version }" in scr # 저장 몸
detail = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts")
assert 'tabs.draw(/^SK-\\d{6}$/.test(doc.키 ?? "")' in detail # 저장한 SK 만 · 줄 모달 = 글
assert (
'tabs.draw(/^SK-(?!0{6})\\d{6}$/.test(doc.키 ?? "")' in detail
) # 저장한 SK 만(SK-000000 = 줄 문서)
assert not (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Equation.ts").exists()
main = _src("main.py")
assert '__import__("M02_MasterTemplete.M02_Doc_Screen", fromlist=["router"]).router' in main
@@ -310,7 +310,7 @@ def test_표값_직접_침_별칭_그대로() -> None:
assert a.get("계") and a.get("계") != docs_sum.get(k), (k, a.get("계"))
rows = _src("A00_Common/line_stack_rows.ts")
assert 'if (link.갈래 === "숫자" && link.표)' in rows # 808-1 — 출처 꼬리표 한 곳(`sourceTag`)
assert "const tag = expr ? null : sourceTag(h, id, link);" in rows
assert "const tag = expr ? null : srcTag ? clipTag(srcTag) : sourceTag(h, id, link);" in rows
assert "const from = v?.연결?.표 ?" in _src("common_util/common_util_line_stack.ts")
@@ -1,173 +0,0 @@
"""820-1 지킴 — 품셈에 없는 소형 · 보조 기계 13 줄(EST 구성 씨앗 · 가격 갱신 필요 · 요구 785 ~ 787).
(a) 새 줄 · 값 — 발전기 손료계수 2362 공통(품셈 발전기 계열) · 브레이커 0.18 = 0.2㎥ 계수 · 가격 = 씨앗 ·
파이프 = 달러 × 환율(비고에 $ 값) · 비고 「갱신 필요」
(b) 계산식 — 손료 = 버림(가격 × 1000 × 손료계수 ÷ 10⁷) · 손료만 = GC000273 · 발전기 5.5 = GC000268 ·
파워트로웰 = GC001195(휘발유 + 잡품(주연료 줄의 %) + 전력) · 옛 엔진 = 새 수식 문서 · 시간당 이음 한 길
(c) 이음 — 가져온 이름 · 규격 → 새 줄 · 옛 기계 그대로
(d) 다른 문서 그대로 — 새 고르기는 목록 끝에만 · 기본값 그대로
"""
from __future__ import annotations
import importlib
import json
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
for p in (ROOT, ROOT / "resources/master_data/scripts"):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
from test_815_1_import_link import no_aliases # noqa: E402 — 진짜 마스터 별칭 빼고
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
from M02_MasterTemplete import M02_Doc_Import as di # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
ml = importlib.import_module("master_doc_link")
mc = importlib.import_module("master_doc_calc")
b802 = importlib.import_module("build_수식문서_802_2")
meq = importlib.import_module("master_equation")
NEW = {f"EQ{n:06d}" for n in range(656, 669)}
SEED = { # 키 → (이름, 규격, 가격, 손료계수, 시간당 식)
"EQ000656": ("발전기", "5.5㎾", 1063, 2362, "GC000268"),
"EQ000657": ("발전기", "7㎾", 1828.9, 2362, "GC000273"),
"EQ000658": ("발전기", "10㎾", 3213, 2362, "GC000273"),
"EQ000659": ("대형 브레이커", "0.18㎥", 4559, 6601, "GC000273"),
"EQ000660": ("에어 호스", "1.91㎝", 105, 5625, "GC000273"),
"EQ000661": ("모노펌프", "5-20㎥/hr", 4900, 4346, "GC000273"),
"EQ000662": ("파워트로웰", "3.73㎾", 2680, 5313, "GC001195"),
}
USD = {"150㎜×1m": 25, "150㎜×2m": 37, "150㎜×3m": 46,
"곡관 150㎜ 45°": 67, "곡관 150㎜ 60°": 84, "곡관 150㎜ 90°": 103} # fmt: skip
def _eq() -> dict[str, dict]:
return m01.loaded()[1].index["EQ"]
def _docs():
docs = ml.load_all()
return docs, ml.Links(m01.loaded()[1], docs)
def _loss(price, factor) -> Decimal:
return (Decimal(str(price)) * 1000 * Decimal(factor) / 10_000_000).quantize(Decimal(1), "ROUND_DOWN") # fmt: skip
def test_rows_and_values() -> None:
eq = _eq()
assert NEW <= set(eq)
for key, (name, spec, price, factor, _) in SEED.items():
row = eq[key]
assert (row["이름"], row["규격"]) == (name, spec), key
assert row["값"]["가격"] == Decimal(str(price)) and row["값"]["손료계수"] == factor, key
assert "갱신 필요" in row["비고"] and row["출처"]["가격"] == "자체", key
gens = [r for r in eq.values() if r.get("상세구분") == "발전기"]
assert {int(r["값"]["손료계수"]) for r in gens} == {2362} # 발전기 계열 공통
small, big = eq["EQ000659"]["값"], eq["EQ000031"]["값"]
for k in ("가격", "상각비계수", "정비비계수", "관리비계수", "손료계수"):
assert small[k] == big[k], k
pipes = {r["규격"]: r for r in eq.values() if r.get("상세구분") == "콘크리트펌프용 파이프"}
assert set(pipes) == set(USD)
rate = m01.loaded()[1].index["FX"]["FX000001"]["값"]
for spec, usd in USD.items():
row = pipes[spec]
assert row["값"]["손료계수"] == 4500 and f"${usd} 표기" in row["비고"]
assert abs(float(row["값"]["가격"]) - usd * float(rate) / 1000) < 1e-3
trowel = eq["EQ000662"]["값"]
assert (trowel["주연료종류"], trowel["주연료"], trowel["잡재료율"], trowel["전력"]) == ("휘발유", 1, 10, Decimal("3.73")) # fmt: skip
def test_formula_loss_and_one_way() -> None:
docs, links = _docs()
files, whole = m01.loaded()
logic = {r["키"]: r for _, r in b802.rows_of(files)}
for key in sorted(NEW):
row = _eq()[key]
got = mh.hourly(key)
assert got["ok"], (key, got)
want = SEED.get(key, (None, None, None, None, "GC000273"))[4]
assert got["로직"] == want, key
lg = logic[want]
given = meq.given_of(whole, lg)
changed = {"기계": row["원문번호"]} if mh.takes_machine(lg) else None
new = b802.new_answer(links, docs[want], changed)
old = b802.old_answer(whole, lg, {**given, **(changed or {})})
assert old == new and float(new["계"]) == got["계"], key # 옛 엔진 = 새 문서 = 시간당 이음
assert new["경비"] == _loss(row["값"]["가격"], row["값"]["손료계수"]), (
key
) # 손료 = 가격 × 계수
if want == "GC000273":
assert new["노무비"] == new["재료비"] == 0, key
assert di.hourly_link("EQ000662", "재료비")["키"] == "GC001195"
assert di.hourly_link("EQ000656", "노무비")["입력"]["기계"] == "'자체 발전기 5.5㎾'"
def test_trowel_lines() -> None:
docs, links = _docs()
got = mc.calc(docs["GC001195"], links)
lines = {x["이름"]: x for x in got["줄"]}
assert set(lines) == {"주연료", "전력", "잡재료비", "기계손료"}
assert lines["전력"]["수량"] == Decimal("3.73") and lines["주연료"]["수량"] == 1
assert lines["잡재료비"]["금액"] == (lines["주연료"]["금액"] * Decimal("0.1")).quantize(Decimal("0.1"), "ROUND_DOWN") # fmt: skip
assert got["노무비"] == 0 and got["경비"] == _loss(2680, 5313)
def test_find_links() -> None:
cases = {
("발전기", "5.5kW"): "EQ000656", ("발전기", "7kW"): "EQ000657",
("발전기", "10kW"): "EQ000658", ("발전기", "25kW"): "EQ000452",
("에어 호스", "1.91cm"): "EQ000660", ("에어호스", "1.91cm"): "EQ000660",
("모노펌프", "5-20㎥/hr"): "EQ000661", ("모노펌프", ""): "EQ000661",
("브레이카", "0.18㎥"): "EQ000659", ("대형브레이카", "0.2㎥"): "EQ000031",
("파워트로웰", "3.73kW"): "EQ000662",
("콘크리트펌프용 파이프", "150mm×1m"): "EQ000663",
("콘크리트펌프용 파이프", "150㎜×3m"): "EQ000665",
("콘크리트펌프용 파이프", "곡관 45˚"): "EQ000666",
("콘크리트펌프용 파이프", "곡관 150㎜ 90°"): "EQ000668",
} # fmt: skip
with no_aliases("EQ"):
for (name, spec), key in cases.items():
got = mf.find(name, spec)
assert (got["판정"], got["키"]) == (mf.LINKED, key), (name, spec, got)
def test_other_docs_same() -> None:
"""새 고르기는 기계경비식 목록 끝에만 · 기본값은 옛 기계 그대로."""
text = (ROOT / "resources/master_data/로직_건설품셈_08장_건설기계.json").read_text("utf-8")
codes = {r["원문번호"] for k, r in _eq().items() if k in NEW}
for row in json.loads(text)["줄"]:
for spec in row.get("입력") or []:
pick = [str(c) for c in spec.get("고르기") or []]
mine = [i for i, c in enumerate(pick) if c in codes]
if mine:
assert mine == list(range(len(pick) - len(mine), len(pick))), row["키"]
assert pick[0] not in codes, row["키"]
old = [k for k, r in _eq().items() if k not in NEW]
assert not [k for k in old if str(_eq()[k].get("원문번호") or "").startswith("자체 ")]
def test_blank_input_name_link_roundtrip() -> None:
"""820-2 — 빈칸 든 입력 이름(「전력 계약종별」)이 이음 글 → 읽기 왕복 · EST main 504 번 = 저장 금액 · 우리 단가 셈."""
from M02_MasterTemplete import M02_San_Link as sl
from M02_MasterTemplete import M02_Sangeun_Imported as imp
text = sl.hourly_text("EQ000662")
assert "'전력 계약종별'=" in text
node = di.dc.cv.mf.parse(text)
assert node[0] == "logic" and "전력 계약종별" in [k for k, _ in node[2]]
assert imp._link(text, "로직", "시험", imp._whole()) == text
import test_810_2_native_doc as t
out = t.fn.emit(t.native("EST", "main"), 504)
sk, _, got = t.doc_and_money(out)
assert float(got["계"]) == 546 # 원파일 저장 금액
sk[imp.KEY]["단가기준"] = "우리"
ours = di.run(di.derive(sk, None, t.whole()), sk, t.whole())
assert ours.get("셈됨") is not False and ours["계"] is not None
+178
View File
@@ -0,0 +1,178 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""821-1 내 기계(개인 층 기계 줄) — M01 기계처럼: 만들 때 사본에 복사 · 고쳐도 금액 그대로 + 「단가새값」 ·
[단가 갱신] 으로 반영 · 지워도 마지막 값(임시 storage · M01 정본 안 고침).
① 세 꼴(바로 · 품셈 · 직접) 저장 · 키 · 원문번호 M01 과 안 겹침 ② 시간당 = M01 같은 값 기계와 같음
③ 찾기 · 남의 층 안 보임 ④ SK 사본에 박힘 · 고친 뒤 그대로 + 새값 · 갱신 뒤 반영 · 지운 뒤 마지막 값 ·
층 사이 복사 ⑤ M01 만 쓰는 SK 금액 그대로 ⑥ 권한.
"""
from __future__ import annotations
import json
import pytest
from fastapi import HTTPException
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Machine_Find as mfind
from M02_MasterTemplete import M02_Machine_Hourly as mh
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MyMachine as mym
from M02_MasterTemplete import M02_MyMachine_Router as route
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ — 연료 · 조종원 있음
PCT = "GC001109" # M01 만 쓰는 SK 바탕
NAME = "서브셋임시기계" # 찾기 기종 글(수 = 규격으로 읽음 · 창 표시는 글로)
def _at(folder, fn, *args):
with store.at(folder):
return fn(*args)
def _easy(**vals):
base = {"가격": 1000, "손료계수": 2000, "주연료": 2, "주연료종류": "경유", "잡재료율": 10,
"조종원": 1, "조종원직종": "1048"} # fmt: skip
return {"이름": NAME, "규격": "1㎾", "방식": "바로", "값": base | vals}
def _sk(folder, machine_no):
line = {"종류": "로직", "로직": "GC000268", "입력값": {"기계": machine_no, "지역": "전국평균"},
"수량": "1"} # fmt: skip
body = {"이름": "sub3임시 내기계", "단위": "시간", "더한줄": [line]}
return _at(folder, lambda: refs.create("sangeun", body)["이름"])
def _doc(folder, key):
return _at(folder, lambda: store.read("sangeun", key)["문서"])
def _calc(folder, doc):
return _at(folder, calc.calc_doc, doc, None, False)
def _sum(folder, key):
return _calc(folder, _doc(folder, key))["기준"]["계"]
def test_three_shapes_and_keys(world):
"""① 바로 · 품셈 · 직접 — 키 EQP{사용자}-#### · 원문번호 「개인 …」 · M01 과 안 겹침 · 날."""
me = world["personal"]
easy = _at(me, mym.create, _easy())["줄"]
assert easy["키"] == "EQP42-0001" and easy["원문번호"] == "개인 EQP42-0001"
assert set(easy["값"]) >= {"가격", "손료계수"} and "상각비계수" not in easy["값"]
assert easy["넣은날"] == easy["고친날"] and easy["출처"]["가격"] == "개인"
book = _at(me, mym.create, {"방식": "품셈"}, DIG)["줄"] # M01 기계 복사해 만들기(비율 → 계수)
m01_row = m01.loaded()[1].get(DIG)["값"]
for k in ("상각비계수", "정비비계수", "관리비계수", "손료계수"):
assert float(book["값"][k]) == float(m01_row[k]), k
direct = _at(me, mym.create, {"이름": NAME, "규격": "1㎾", "방식": "직접",
"시간당": {"노무비": 10, "재료비": 20, "경비": 30}}) # fmt: skip
assert direct["시간당"]["계"] == 60 and direct["시간당"]["직접"] is True
m01_eq = m01.loaded()[1]
for row in _at(me, mym.rows):
assert row["키"] not in m01_eq.index["EQ"] and row["원문번호"] not in m01_eq.numbers["EQ"]
assert [r["키"] for r in _at(me, mym.rows)] == ["EQP42-0001", "EQP42-0002", "EQP42-0003"]
assert {x["키"] for x in direct["같은이름"]} == {"EQP42-0001"} # 같은 이름 · 규격 알림
for bad in ({"이름": ""}, {**_easy(), "값": {"가격": 1}}, {**_easy(가격=-1)},
{**_easy(주연료종류="물")}, {**_easy(조종원직종="없음")}): # fmt: skip
with pytest.raises(store.StoreError):
_at(me, mym.create, bad)
def test_hourly_same_as_m01(world):
"""② 굴착기 1.0㎥ 값을 내 기계로(바로 · 품셈) → 시간당 = M01 기계 · 식은 채운 칸으로."""
me = world["personal"]
want = mh.hourly(DIG)
for how in ("바로", "품셈"):
got = _at(me, mym.create, {"방식": how}, DIG)
hour = _at(me, mh.hourly, got["줄"]["키"])
assert hour["ok"] and hour["로직"] == "GC000268", how
for k in ("노무비", "재료비", "경비", "계"):
assert hour[k] == want[k], (how, k)
loss = _at(me, mym.create, _easy(주연료=0, 조종원=0))["시간당"]
assert loss["로직"] == "GC000273" and loss["계"] == loss["경비"] == 200 # 1000×1000×2000/10⁷
assert _at(me, mym.create, _easy(조종원=0))["시간당"]["로직"] == "GC000272"
assert _at(me, mym.create, _easy(주연료=0))["시간당"]["로직"] == "GC000276"
def test_find_and_other_layer_hidden(world):
"""③ 기계 찾기 = 그 층 내 기계도 · 남의 층 · 층 모르는 길 = M01 만."""
me, other = world["personal"], layers.personal_dir(7, 43)
key = _at(me, mym.create, _easy())["줄"]["키"]
got = _at(me, mfind.find, NAME, "1㎾")
assert got["판정"] == mfind.LINKED and got["키"] == key
assert _at(other, mfind.find, NAME, "1㎾")["판정"] == mfind.NONE
assert _at(other, mh.hourly, key)["ok"] is False
assert mh.hourly(key)["ok"] is False # 층 모르는 길
assert key not in _at(other, skc.whole_).index["EQ"]
def test_sk_copy_edit_refresh_delete(world):
"""④ 만들 때 사본에 박힘 → 고쳐도 그대로 + 새값 → [단가 갱신] 반영 → 지워도 마지막 값 · 층 복사."""
me = world["personal"]
made = _at(me, mym.create, _easy())
key, no = made["줄"]["키"], made["줄"]["원문번호"]
sk = _sk(me, no)
copy = _doc(me, sk)["사본"]
assert key in {r["키"] for r in copy["단가"]}
was = _sum(me, sk)
assert float(was) == made["시간당"]["계"]
assert _at(me, mym.one, key)["쓰는곳"] == [{"키": sk, "이름": "sub3임시 내기계"}]
_at(me, mym.update, key, _easy(가격=3000)) # 고침
now = _calc(me, _doc(me, sk))
assert now["기준"]["계"] == was and now["사본"]["단가새값"] is True
assert now["사본"]["지운키"] == []
new = _at(me, mh.hourly, key)["계"]
look = _at(me, skc.preview, _doc(me, sk), skc.PRICE)
assert float(look["전"]["계"]) == float(was) and float(look["후"]["계"]) == new
assert [x["키"] for x in look["바뀐자원"]] == [key]
with store.at(me): # [저장] = 할일 적용
store.write("sangeun", sk, store.read("sangeun", sk)["판"], look["문서"])
assert float(_sum(me, sk)) == new
assert _calc(me, _doc(me, sk))["사본"]["단가새값"] is False
# 층 사이 복사 — 사본이 따라감(받은 층에 그 내 기계 없음 · 마지막 값)
other = layers.personal_dir(7, 43)
moved = _at(other, skc.settle, {**_doc(me, sk), "키": "SK-999999"})
assert float(_calc(other, moved)["기준"]["계"]) == new
gone = _at(me, mym.delete, key)
assert gone["쓰는곳"] == [{"키": sk, "이름": "sub3임시 내기계"}]
after = _calc(me, _doc(me, sk))
assert float(after["기준"]["계"]) == new and after["사본"]["지운키"] == [key]
look = _at(me, skc.preview, _doc(me, sk), skc.PRICE)
assert look["바뀐자원"] == [{"키": key, "이름": NAME, "지워짐": True}]
assert float(look["후"]["계"]) == new
def test_m01_only_sk_same(world):
"""⑤ 내 기계가 있는 층에서도 M01 만 쓰는 SK 금액 · 사본 같음 · 사본 모으기에 내 기계 안 낌."""
me, other = world["personal"], layers.personal_dir(7, 43)
base = {"이름": "sub3임시 M01", "단위": "㎡", "바탕": {"로직": PCT}, "더한줄": []}
plain = _at(other, lambda: refs.create("sangeun", base)["이름"])
_at(me, mym.create, _easy())
mine = _at(me, lambda: refs.create("sangeun", base)["이름"])
a, b = _doc(other, plain)["사본"], _doc(me, mine)["사본"]
assert json.dumps(a["단가"], sort_keys=True) == json.dumps(b["단가"], sort_keys=True)
assert a["판"] == b["판"] and _sum(other, plain) == _sum(me, mine)
def test_rights(world):
"""⑥ 남의 개인 층 = 403(일반) · 회사 관리자 = 됨 · 개인 층 밖 = 404."""
user = {"role": "USER", "company_id": 7, "user_id": 42}
boss = {"role": "ADMIN", "company_id": 7, "user_id": 1}
with pytest.raises(HTTPException) as e:
route._folder(user, "personal", "7/43", True)
assert e.value.status_code == 403
with pytest.raises(HTTPException) as e:
route._folder(user, "company", None, True)
assert e.value.status_code == 404
assert route._folder(user, "personal", None, True) == world["personal"]
assert route._folder(boss, "personal", "7/42") == world["personal"] # 맡은 회사 관리자 읽기
with pytest.raises(store.StoreError): # 개인 층 아닌 자리에서 만들기
_at(world["company"], mym.create, _easy())
+133
View File
@@ -0,0 +1,133 @@
"""PLAN 821-2 기계 계수 계산 짝 — 지킴 시험.
① 계수 전수 대조: M01 기계표에서 품셈 입력 다섯 · 계수 넷이 다 있는 줄 = 파이썬 계수 셈과 같음
(다른 줄 = 원문 표 값 그대로인 점보드릴 2 줄만 · 원문 777 · 셈 777.8).
② 파이썬 · TS 거울: 같은 입력 → 같은 계수 · 같은 시간당 미리보기.
③ 미리보기 = 서버 기계경비식(`M02_Machine_Hourly.hourly`) 견본 스무 대 · 비목 셋 같음.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts"))
import master_machine_coef as mc # noqa: E402
DATA = ROOT / "resources" / "master_data"
TS = ROOT / "common_util" / "common_util_machine_coef.ts"
# 원문 표 값 ≠ 식(정비비계수 0.7 ÷ 9,000 × 10^7 = 777.8 → 원문 777 · 계 2,515) — 식을 비틀지 않음
ORIGINAL_ODD = {"EQ000347", "EQ000348"}
SAMPLES = (
"EQ000001 EQ000006 EQ000022 EQ000026 EQ000040 EQ000051 EQ000073 EQ000076 EQ000126 EQ000151 "
"EQ000176 EQ000201 EQ000226 EQ000229 EQ000246 EQ000326 EQ000401 EQ000434 EQ000455 EQ000544"
).split()
def _load(name: str) -> list[dict]:
return json.loads((DATA / name).read_text(encoding="utf-8"))["줄"]
MACHINES = _load("기계.json")
FUEL = {r["원문번호"]: r["값"] for r in _load("재료_유가전력.json")}
WAGE = {r["원문번호"]: r["값"] for r in _load("인력.json")}
def _full(row: dict) -> bool:
v = row["값"]
return all(isinstance(v.get(k), (int, float)) for k in (*mc.INPUTS, *mc.COEFS))
def _prices(v: dict) -> tuple[float, float]:
"""전국평균 유가 · 노임(직종 비면 기계경비식 조종원직종 첫 고르기 1048)."""
return FUEL.get(f"{v.get('주연료종류')}:전국평균", 0), WAGE.get(
str(v.get("조종원직종") or "1048"), 0
)
def test_coef_matches_master_table():
rows = [r for r in MACHINES if _full(r)]
assert len(rows) >= 590
odd = {r["키"] for r in rows if mc.coef_of(r["값"]) != {k: r["값"][k] for k in mc.COEFS}}
assert odd == ORIGINAL_ODD
def test_coef_excavator_example():
got = mc.coef(10000, 1250, 0.9, 0.7, 0.1)
assert got == {"상각비계수": 900, "정비비계수": 700, "관리비계수": 485, "손료계수": 2085}
assert mc.coef(0, 1250, 0.9, 0.7, 0.1) is None
assert mc.coef_of({"내용시간": 10000}) is None
@pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
reason="프론트엔드 의존성 미설치",
)
def test_ts_mirror(tmp_path):
coef_cases = [[r["값"][k] for k in mc.INPUTS] for r in MACHINES if _full(r)]
coef_cases += [[10000, 1250, 0.9, 0.7, 0.1], [0, 1, 1, 1, 1], [3600, 540, 0.9, 1.15, 0.1]]
hour_cases = []
for r in MACHINES:
fuel, wage = _prices(r["값"])
hour_cases.append([r["값"], fuel, wage, 114.4, 0.125])
hour_cases.append(
[
{"가격": 2680, "손료계수": 5313, "주연료": 1.0, "잡재료율": 10, "전력": 3.73},
2000,
0,
114.4,
0.125,
]
)
src = tmp_path / "in.json"
src.write_text(
json.dumps({"coef": coef_cases, "hour": hour_cases}, ensure_ascii=False), encoding="utf-8"
)
script = f"""
const fs = require("fs");
const ts = require({json.dumps(str(ROOT / "config" / "node_modules" / "typescript"))});
const opt = {{ module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }};
const src = fs.readFileSync({json.dumps(str(TS))}, "utf8");
const js = ts.transpileModule(src, {{ compilerOptions: opt }}).outputText;
const m = {{ exports: {{}} }};
new Function("exports", "module", js)(m.exports, m);
const inp = JSON.parse(fs.readFileSync({json.dumps(str(src))}, "utf8"));
process.stdout.write(JSON.stringify({{
coef: inp.coef.map((c) => m.exports.machineCoef(...c)),
hour: inp.hour.map((h) => m.exports.machineHourly(...h)),
}}));
"""
out = subprocess.run(
["node", "-e", script], capture_output=True, text=True, encoding="utf-8", check=True
)
got = json.loads(out.stdout)
want_coef = [mc.coef(*c) for c in coef_cases]
want_hour = [mc.preview(*h) for h in hour_cases]
assert got["coef"] == want_coef
assert len(got["hour"]) == len(want_hour)
for a, b in zip(got["hour"], want_hour):
assert a == pytest.approx(b, abs=1e-9)
def test_preview_equals_server_hourly():
from M02_MasterTemplete import M02_Machine_Hourly as mh
rows = {r["키"]: r for r in MACHINES}
for key in SAMPLES:
h = mh.hourly(key)
assert h["ok"], (key, h)
v = rows[key]["값"]
p = mc.preview(v, *_prices(v))
assert (p["노무비"], p["재료비"], p["경비"], p["계"]) == (
h["노무비"],
h["재료비"],
h["경비"],
h["계"],
), (key, h["로직"])
+156
View File
@@ -0,0 +1,156 @@
"""PLAN 821-2 M01 기계 저장 — 지킴 시험(임시 사본 폴더만 · 정본 안 씀).
① 새 기계 = 단위 · 출처 자동(마스터 단위 · 「자체」) · 계수 서버 재계산 · 다른 줄 그대로
② 고침 = 그 칸 한 줄만 · 품셈 입력이 같으면 저장 계수 그대로(점보드릴 777) · 바뀌면 다시 셈
③ 기계 길은 시스템 관리자만(아니면 403) · 양식 목록 · 비슷한 기계 찾기 · 쓰는 곳
"""
from __future__ import annotations
import difflib
import json
import shutil
import sys
from pathlib import Path
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from common_util import common_util_auth as auth
from M01_MasterData import M01_MasterData_Router as router_module
from M01_MasterData import M01_MasterData_Store as store
sys.path.insert(
0, str(Path(__file__).resolve().parents[2] / "resources" / "master_data" / "scripts")
)
import master_machine_coef as mc # noqa: E402
REAL = store.FOLDER
MACHINE = "기계.json"
@pytest.fixture
def copy(tmp_path: Path, monkeypatch) -> Path:
for path in REAL.glob("*.json"):
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
shutil.copy(path, tmp_path / path.name)
monkeypatch.setattr(store, "FOLDER", tmp_path)
return tmp_path
def _app(role: str = "SYSTEM_ADMIN") -> TestClient:
app = FastAPI()
app.dependency_overrides[auth.verify_session] = lambda: {
"user_id": 1,
"name": "시험",
"role": role,
"company_id": 1,
}
deps = [Depends(auth.verify_session), Depends(auth.require_system_admin)]
app.include_router(router_module.router, dependencies=deps)
return TestClient(app)
def _save(client: TestClient, version: str, change: dict) -> None:
res = client.post(
"/api/m01/save",
json={"files": [{"file": MACHINE, "version": version, "changes": [change]}]},
)
assert res.status_code == 200, res.text
def _diff(before: str, after: str) -> list[str]:
return [
x
for x in difflib.unified_diff(before.splitlines(), after.splitlines(), lineterm="", n=0)
if x[:1] in "+-" and not x.startswith(("+++", "---"))
]
def test_새_기계_단위_출처_자동_계수_서버_재계산(copy: Path) -> None:
client = _app()
lists = client.get("/api/m01/machine/lists").json()
assert {"경유", "휘발유"} <= {f["value"] for f in lists["fuels"]}
assert any(j["value"] == "1048" for j in lists["jobs"]) and lists["hour"] == 0.125
found = client.get("/api/m01/machine/find", params={"q": "굴착기 1.0㎥"}).json()["rows"]
seed = next(r for r in found if r["키"] == "EQ000022")
before = (copy / MACHINE).read_text(encoding="utf-8")
version = client.get("/api/m01/machine/row", params={"key": "EQ000022"}).json()["version"]
values = {**seed["값"], "내용시간": 8000, "손료계수": 1, "상각비계수": 1} # 계수 = 틀린 값
row = {
"키": "",
"원문번호": "",
**{k: seed[k] for k in ("원문", "구분", "상세구분")},
"이름": "시험기계sub1",
"규격": "9.9㎥",
"단위": {},
"값": values,
"출처": {},
"비고": None,
}
_save(client, version, {"op": "add", "row": row})
after = (copy / MACHINE).read_text(encoding="utf-8")
rows = json.loads(after)["줄"]
new = rows[-1]
assert new["이름"] == "시험기계sub1" and new["키"].startswith("EQ")
assert new["값"] | mc.coef(8000, 1250, 0.9, 0.7, 0.1) == new["값"] # 서버가 다시 셈
assert new["값"]["손료계수"] == 1125 + 875 + 496
assert set(new["단위"]) == set(new["값"]) == set(new["출처"])
assert new["단위"] == {
k: seed_unit for k, seed_unit in _units(before).items() if k in new["값"]
}
assert set(new["출처"].values()) == {"자체"}
assert json.loads(before)["줄"] == rows[:-1] # 다른 줄 그대로
assert all(x.startswith("+") for x in _diff(before, after)) # 글은 더하기만
def _units(text: str) -> dict:
return next(r for r in json.loads(text)["줄"] if r["키"] == "EQ000022")["단위"]
def _edit(client: TestClient, key: str, change) -> tuple[dict, dict]:
got = client.get("/api/m01/machine/row", params={"key": key}).json()
row = got["row"]
_save(client, got["version"], {"op": "edit", "key": key, "row": change(row)})
now = client.get("/api/m01/machine/row", params={"key": key}).json()["row"]
return row, now
def test_고침_그_줄_그_칸만(copy: Path) -> None:
client = _app()
before = (copy / MACHINE).read_text(encoding="utf-8")
row, now = _edit(client, "EQ000022", lambda r: {**r, "값": {**r["값"], "가격": 150000}})
assert now == {**row, "값": {**row["값"], "가격": 150000}}
lines = _diff(before, (copy / MACHINE).read_text(encoding="utf-8"))
assert len(lines) == 2 and "142069" in lines[0] and "150000" in lines[1] # 그 칸 한 줄만
def test_입력_같으면_저장_계수_그대로_바뀌면_다시_셈(copy: Path) -> None:
client = _app()
# 가격만 고침 + 화면이 계수를 틀리게 보내도 — 입력이 같으면 원문 표 값(정비비계수 777) 그대로
row, now = _edit(
client,
"EQ000347",
lambda r: {**r, "값": {**r["값"], "가격": 999, "정비비계수": 778, "손료계수": 1}},
)
assert now["값"] == {**row["값"], "가격": 999}
_, now = _edit(client, "EQ000347", lambda r: {**r, "값": {**r["값"], "내용시간": 10000}})
assert {k: now["값"][k] for k in mc.COEFS} == mc.coef(10000, 800, 0.9, 0.7, 0.1)
def test_기계_길은_시스템_관리자만() -> None:
for role in ("USER", "COMPANY_ADMIN"):
client = _app(role)
assert client.get("/api/m01/machine/lists").status_code == 403
assert client.get("/api/m01/machine/hourly", params={"keys": "EQ000022"}).status_code == 403
assert client.post("/api/m01/save", json={"files": []}).status_code == 403
def test_쓰는_곳_찾기(copy: Path, monkeypatch) -> None:
from M02_MasterTemplete import M02_Template_Layers as layers
monkeypatch.setattr(layers, "layer_files", lambda kind: []) # 공유 storage 안 읽음
uses = _app().get("/api/m01/machine/uses", params={"key": "EQ000022"}).json()["uses"]
assert any("GC000267" in u for u in uses)
assert _app().get("/api/m01/machine/uses", params={"key": "EQ999999"}).status_code == 404
@@ -0,0 +1,144 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""821-3 내 기계 화면 읽기 길(`M02_MyMachine_Look`) — 임시 storage · 쓰기 없음.
① 양식 목록 = M01 기계 화면 목록과 같음(연료 · 직종 · 전력 · 시간환산)
② 비슷한 기계 찾기 = 내 기계(앞) + M01 · 원문번호 = 그 M01 기계 먼저 · 남의 층 내 기계 안 보임
③ 내 품목 = 값을 바로 적은 재료비 줄만 · 같은 품목 묶음 · 쓰는 단가산출 ④ 개인 층 밖 = 404.
"""
from __future__ import annotations
import json
import pytest
from fastapi import HTTPException
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MyMachine as mym
from M02_MasterTemplete import M02_MyMachine_Look as look
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥
NAME = "서브원임시기계"
def _at(folder, fn, *args):
with store.at(folder):
return fn(*args)
def _mine(folder):
row = {"이름": NAME, "규격": "1㎾", "방식": "바로", "값": {"가격": 1000, "손료계수": 2000}}
return _at(folder, mym.create, row)["줄"]
def test_lists_same_as_m01():
"""① 양식 목록 = M01 기계 화면 값."""
from M01_MasterData import M01_MasterData_Machine as m01m
got, want = look.lists(), m01m.lists()
assert set(got) == {"fuels", "jobs", "power", "hour"}
for k in got:
assert got[k] == want[k], k
assert got["fuels"] and got["jobs"] and got["hour"] == 0.125
def test_find_mine_first_and_number(world):
"""② 내 기계 앞 · 원문번호 = 그 M01 기계 먼저 · 남의 층 = 내 기계 안 보임."""
me, other = world["personal"], layers.personal_dir(7, 43)
key = _mine(me)["키"]
rows = _at(me, look.find, NAME)
assert rows[0]["키"] == key and rows[0]["출처"] == "개인" and rows[0]["값"]["손료계수"] == 2000
assert all(r["출처"] == "M01" for r in _at(other, look.find, NAME))
no = str(m01.loaded()[1].get(DIG)["원문번호"])
first = _at(me, look.find, no)[0]
assert first["키"] == DIG and first["원문번호"] == no and first["출처"] == "M01"
assert isinstance(first["값"]["가격"], int | float) and first["구분"] # 수(글이면 미리보기 0)
dig = [r for r in _at(me, look.find, "굴착기") if r["출처"] == "M01"]
assert dig and len({r["키"] for r in dig}) == len(dig) # 겹침 없음
def _sk(folder, key, name, lines):
"""수식 문서 재료비 줄 — (이름, 연결) · 숫자 = 바로 적은 값 · 요소 = 우리 자재."""
vars_, money = [], []
for i, (item, link) in enumerate(lines):
vars_.append({"열쇠": f"P_x{i}", "기호": "P", "아래": f"x{i}", "위": "", "연결": link})
money.append(
{"이름": item, "규격": "10㎜", "단위": "개", "비목": "재료비", "단가": f"P_x{i}"}
)
money.append({"이름": "인부", "단위": "인", "비목": "노무비", "단가": "P_x0"})
doc = {"키": key, "이름": name, "수식": {"문서": {"변수": vars_, "금액": money}}}
path = folder / "sangeun" / f"{key}.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
def test_items_direct_only(world):
"""③ 바로 적은 값 재료만 · 같은 품목 = 한 묶음 · 쓰는 단가산출 · 값 다르면 따로."""
me = world["personal"]
_sk(me, "SK-000001", "가", [("볼트", {"갈래": "숫자", "값": 500}),
("시멘트", {"갈래": "요소", "키": "MT004176"})]) # fmt: skip
_sk(me, "SK-000002", "나", [("볼트", {"갈래": "숫자", "값": 500}),
("볼트", {"갈래": "입력", "값": 700})]) # fmt: skip
got = _at(me, look.items)
assert [(r["이름"], r["값"]) for r in got] == [("볼트", 500), ("볼트", 700)]
assert [u["키"] for u in got[0]["쓰는곳"]] == ["SK-000001", "SK-000002"]
assert got[1]["쓰는곳"] == [{"키": "SK-000002", "이름": "나"}]
assert got[0]["규격"] == "10㎜" and got[0]["단위"] == "개"
assert _at(layers.personal_dir(7, 43), look.items) == []
def test_personal_only(world):
"""④ 개인 층 밖 = 404 · 남의 개인 층 = 403(일반)."""
user = {"role": "USER", "company_id": 7, "user_id": 42}
for layer, owner, code in (("company", None, 404), ("personal", "7/43", 403)):
with pytest.raises(HTTPException) as e:
look.get_items(layer, owner, user)
assert e.value.status_code == code
assert look.get_items("personal", None, user) == {"rows": []}
def _stack_doc(key, no):
"""줄 쌓기 기계 줄 하나(화면이 저장하는 꼴) — 기계='개인 …'."""
var = {"기호": "P", "아래": "새", "위": "", "이름": NAME, "단위": "",
"연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": f"'{no}'"}}} # fmt: skip
line = {"이름": NAME, "단위": "hr", "비목": "묶음", "수량": "1", "단가": "P_새"}
return {"꼴": "수식문서", "판": 1, "키": key, "이름": "sub1임시", "결과단위": "원",
"변수": [var], "식": [], "금액": [line]} # fmt: skip
def test_stack_line_copy(world):
"""⑤ 줄 쌓기로 고른 내 기계 — 미리 셈 됨 · 저장 때 값 사본에 박힘 →
고쳐도 금액 그대로 + 단가새값 → [단가 갱신] 바뀐 자원 1 → 지워도 마지막 값 + 지운키."""
from M02_MasterTemplete import M02_Doc_Sk_Path as path
from M02_MasterTemplete import M02_Labor_Ref as refs
me = world["personal"]
made = _at(me, mym.create, {**_mine_row(), "값": {**_mine_row()["값"], "주연료": 2,
"주연료종류": "경유", "조종원": 1, "조종원직종": "1048"}}) # fmt: skip
key, no, was = made["줄"]["키"], made["줄"]["원문번호"], made["시간당"]["계"]
sk = _at(
me,
lambda: refs.create("sangeun", {"이름": "sub1임시", "단위": "시간", "더한줄": []})["이름"],
)
doc = _stack_doc(sk, no)
look_ = _at(me, path.preview, sk, {"문서": doc})
assert look_["셈"]["계"] == was # 사본에 아직 없는 내 기계도 셈
판 = _at(me, path.read, sk)["판"]
_at(me, path.save, sk, {"문서": doc, "판": 판})
copy = _at(me, lambda: store.read("sangeun", sk)["문서"]["수식"])
assert key in [r["키"] for r in copy["단가"]] # 값 사본에 박힘
_at(me, mym.update, key, {**made["줄"], "값": {**made["줄"]["값"], "가격": 3000}})
now = _at(me, path.read, sk)
assert now["셈"]["계"] == was and now["사본"]["단가새값"] is True
job = _at(me, path.preview, sk, {"할일": "단가갱신"})
assert [x["키"] for x in job["바뀐자원"]] == [key] and job["후"]["계"] > was
_at(me, mym.delete, key)
gone = _at(me, path.read, sk)
assert gone["셈"]["계"] == was and gone["사본"]["지운키"] == [key]
def _mine_row():
return {"이름": NAME, "규격": "1㎾", "방식": "바로", "값": {"가격": 1000, "손료계수": 2000}}
@@ -0,0 +1,238 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""821-4 지킴 — 불러오기에서 끝내 못 이은 기계 = 내 기계 자동 생성(요구서_796 4 · 5 장 · 임시 storage).
① 구성 → 우리 틀 값(EST 원파일 발전기 5.5kW — 손료계수 · 가격 · 경유 0.625ℓ · 잡품 11 % · 일반기계운전사 1 인 ·
경쟁사 노임 환산 안 가져옴) · 엑셀 「기계경비」 · STmate 「중기사용료」 · 구성 없음 = 직접 꼴 · 달러 = 환율
② 층 저장 길(`FromHopyo.make`) = 만듦 · 다시 불러와도 중복 없음 · 같은 이름 있으면 그것 · 미리 보기는 안 만듦 ·
금액(엑셀 단가 그대로) 그대로 · [우리 단가로] = 내 기계 값 · 「못 이음」 에서 빠짐 · 사본에 박힘
③ 820-1 정리 — 13 줄 · GC001195 없음(대장 폐기 · 결번) · 덤프 1 톤 · 체인톱 식 · 별칭 그대로 ·
빈칸 든 입력 이름 이음 글 왕복(820-2 지킴 옮김) · EST main 504 파워트로웰 블록 금액 546 그대로
"""
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Doc_FromNative as fn
from M02_MasterTemplete import M02_Doc_Import as dim
from M02_MasterTemplete import M02_Machine_Hourly as mh
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MyMachine as mym
from M02_MasterTemplete import M02_San_Book as book
from M02_MasterTemplete import M02_San_Estx as estx
from M02_MasterTemplete import M02_San_MyMachine as mm
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
from M02_MasterTemplete import M02_Sangeun_Imported as imp
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
from resources.tester.test_815_1_import_link import aliases, no_aliases # M01 별칭 칸 임시
ROOT = Path(__file__).resolve().parents[2]
MAIN = ROOT / "resources/knowledge/original/경쟁사 프로그램/EST Plus 2/다산소프트/2026-07-main.estx"
GEN = "E0000001016" # EST main 발전기 5.5kW
GONE = [f"EQ{n:06d}" for n in range(656, 669)]
@pytest.fixture(autouse=True)
def _blank_aliases(request):
"""진짜 마스터 기계 별칭(파워트로웰 = EQ000534 등) 유무와 무관 — 별칭 없는 사본에서 잼(820-1 정리 시험만 진짜 마스터)."""
if request.node.name == "test_cleanup_820_1":
yield
return
with no_aliases("EQ"):
yield
@lru_cache(maxsize=1)
def _main_raw() -> bytes:
return MAIN.read_bytes()
@lru_cache(maxsize=1)
def _main() -> dict:
return fn.read(MAIN.name, _main_raw())
def _excel(tag: str) -> dict:
import test_797_1a_import_flags as practice
p = practice.FILES.get(tag)
if not p or not p.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
return book.sheets(p.name, p.read_bytes())
def _line(name: str, spec: str, **res) -> dict:
found = {"판정": "못 찾음", "이름": name, "규격": spec}
return {"종류": "기계", "이름": name, "자원": {"갈래": "기계", "이름": name, "규격": spec, "찾기": found, **res}} # fmt: skip
def _labor(name: str) -> str:
return next(k for k, r in m01.loaded()[1].numbers["LB"].items() if r["이름"] == name)
# ── ① 구성 → 우리 틀 값 ───────────────────────────────────────────────
def test_est_native_generator_values() -> None:
make = estx.makes # 816-2 대조 꼴은 그대로(손료 · 주연료 · 조종원 있음만)
assert make is not None
got = _main()["기계구성"][GEN]
assert got["손료"] == [[2362.0, 1063000.0, False]] and got["주연료"] == 0.625
assert (got["주연료종류"], got["잡재료율"], got["조종원"], got["조종원직종"]) == ("경유", 11.0, 1.0, "일반기계운전사") # fmt: skip
body = mm.body(got, 0, _line("발전기", "5.5kW", 코드=GEN), "main.estx")
want = {"가격": 1063.0, "손료계수": 2362.0, "주연료": 0.625, "주연료종류": "경유", "잡재료율": 11.0,
"조종원": 1.0, "조종원직종": _labor("일반기계운전사")} # fmt: skip
assert body["방식"] == "바로" and body["값"] == want
assert body["비고"].startswith("불러오기 자동 생성 — main.estx · 값 확인 필요")
def test_dollar_and_combo_pieces() -> None:
made = _main()["기계구성"]
pipe = mm.body(made["E0000000623"], 0, _line("콘크리트펌프용 파이프", "150㎜×1m"), "f")
rate = float(m01.loaded()[1].index["FX"]["FX000001"]["값"])
assert abs(pipe["값"]["가격"] - 25 * rate / 1000) < 1e-3 and "$25" in pipe["비고"]
combo = made["E0000001170"] # 대형브레이카 +굴착기(1.0㎥) — 조각 둘
first = mm.body(combo, 0, _line("굴착기", "1.0㎥"), "f")["값"]
second = mm.body(combo, 1, _line("대형브레이카", "1.0㎥"), "f")["값"]
assert first["손료계수"] == 2085 and first["주연료"] == 19.5 and first["조종원"] == 1
assert second == {"가격": 28692.0, "손료계수": 6601.0} # 다음 조각 = 손료만
assert mm.body(None, 1, _line("x", "1"), "f") is None # 구성 없는 조합 조각 = 안 만듦
def test_excel_sheets() -> None:
est = mm.xlsx_makes(_excel("2016사방"))[mm.key_of("발전기", "5.5kW")]
assert est["손료"] == [[2294.0, 1167000.0, False]] and est["주연료"] == 0.625
assert (est["잡재료율"], est["조종원"], est["조종원직종"]) == (11.0, 1.0, "일반기계운전사")
stm = mm.xlsx_makes(_excel("봉화"))["X00001"] # STmate 중기사용료 — 계수(천원당) × 10⁴
assert stm["손료"] == [[2085.0, 62550000.0, False]]
assert (stm["주연료종류"], stm["주연료"], stm["잡재료율"], stm["조종원직종"]) == ("경유", 5.0, 21.0, "건설기계운전사") # fmt: skip
def test_direct_without_make() -> None:
line = _line("소형기계", "1대", 단가={"L": 10, "M": 20, "S": 30})
got = mm.body(None, 0, line, "a.stc")
assert got["방식"] == "직접" and got["시간당"] == {"노무비": 10, "재료비": 20, "경비": 30}
# ── ② 층 저장 길 ──────────────────────────────────────────────────────
TROWEL = 504 # EST main 「콘크리트 바닥 마감(Power Trowel)」 — 파워트로웰 3.73kW = 별칭 없는 사본에서 못 이음(별칭 있으면 EQ000534)
WANT = ("파워트로웰", "3.73kW")
def test_generator_ours_formula(world) -> None:
"""발전기 5.5kW 내 기계 시간당 노무비 = 우리 노임 ÷ 8 × 1 인(EST 의 ×16/12 ×25/20 안 씀)."""
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
body = mm.body(_main()["기계구성"][GEN], 0, _line("발전기", "5.5kW"), "main.estx")
with store.at(me):
key = mym.create(body)["줄"]["키"]
hour = mh.hourly(key)
lb = m01.loaded()[1].numbers["LB"][_labor("일반기계운전사")]
assert hour["ok"] and hour["로직"] == "GC000268"
assert abs(hour["노무비"] - float(lb["값"]) / 8) < 1
def test_make_flow(world) -> None:
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
with store.at(me):
hp = fh.upload_native(me, MAIN.name, _main_raw(), {"이름": "sub3임시"})["키"]
out = fh.emit(me, hp, TROWEL) # 미리 보기 = 안 만듦 · 예정만
assert {"이름": WANT[0], "규격": WANT[1]} in out["원본"]["내기계예정"]
assert mym.rows() == []
got = fh.make(me, hp, TROWEL)
made = {(m["이름"], m["규격"]): m for m in got["내기계"]}
one = made[WANT]
assert one["새로"] and one["키"].startswith("EQP42-") and one["방식"] == "바로"
assert got["검산"]["같음"] # 금액(엑셀 단가 그대로) 그대로
n = len(mym.rows())
again = fh.make(me, hp, TROWEL) # 다시 불러옴 = 중복 없음 · 같은 SK
assert len(mym.rows()) == n and again["다시씀"]
assert not any(m["새로"] for m in again["내기계"])
doc = store.read("sangeun", got["이름"])["문서"]
assert doc[imp.KEY]["원본"]["내기계"][0]["키"] == one["키"]
items = doc[imp.KEY]["로직"]["호표"]
mine = [x for x in items if (x.get("자원") or {}).get("내기계") == one["키"]]
assert mine and all(f"개인 {one['키']}" in str(x.get("이음")) for x in mine)
assert not [
u for u in imp.unlinked(doc) if "파워트로웰" in u["이름"]
] # 「못 이음」 에서 빠짐
row = next(r for r in mym.rows() if r["키"] == one["키"])
assert row["값"]["가격"] == 2680 and row["값"]["주연료종류"] == "휘발유"
assert row["값"]["전력"] == 3.73 and "값 확인 필요" in row["비고"]
ours = imp.to_price(doc, "우리") # [우리 단가로] = 내 기계 값
assert ours["후"]["계"] is not None and ours["후"] != ours["전"]
assert not [u for u in ours["이음없음"] if "파워트로웰" in u["이름"]]
assert one["키"] in {r["키"] for r in (doc.get(skc.KEY) or {}).get("단가") or []} # 사본
conv = dim.derive(doc, None, imp._whole())
assert f"내 기계 {one['키']}" in json.dumps(
conv["변수"], ensure_ascii=False, default=str
) # 꼬리표
def test_reuse_same_name(world) -> None:
me = world["personal"]
me.mkdir(parents=True, exist_ok=True)
with store.at(me):
have = mym.create({"이름": WANT[0], "규격": WANT[1], "방식": "바로", "값": {"가격": 9, "손료계수": 9}}) # fmt: skip
out = fn.emit(_main(), TROWEL)
made = mm.attach(out, _main()["기계구성"])
assert [(m["키"], m["새로"]) for m in made] == [(have["줄"]["키"], False)]
assert len(mym.rows()) == 1
def test_not_personal_makes_nothing(world) -> None:
out = fn.emit(_main(), TROWEL)
with store.at(world["company"]):
assert mm.attach(out, _main()["기계구성"]) == []
assert out["원본"]["내기계예정"] == [{"이름": WANT[0], "규격": WANT[1]}]
# ── ③ 820-1 정리 ─────────────────────────────────────────────────────
def test_cleanup_820_1() -> None:
files, whole = m01.loaded()
eq = whole.index["EQ"]
assert not set(GONE) & set(eq) and "EQ000655" in eq and eq["EQ000655"]["규격"] == "1ton"
assert not [r for r in eq.values() if str(r.get("원문번호") or "").startswith("자체 ")]
logics = {r["키"] for d in files.values() if d.get("그룹") == "로직" for r in d.get("줄") or []}
assert "GC001195" not in logics and "GF000337" in logics and "GF000145" in logics
text = json.dumps(
[d for d in files.values() if d.get("그룹") == "로직"], ensure_ascii=False, default=str
)
assert "자체 발전기" not in text and "자체 콘크리트펌프용" not in text
led = json.loads((ROOT / "resources/master_data/_키대장.json").read_text("utf-8"))
assert all(k in led["폐기"] and k not in led["키"] for k in [*GONE, "GC001195"])
assert led["다음"]["EQ"] == 669 and led["다음"]["GC"] == 1196 # 결번 · 재사용 안 함
alias = [r for r in eq.values() if r.get("별칭")]
assert len(alias) >= 10
def test_blank_input_name_roundtrip() -> None:
"""820-2 지킴 옮김 — 빈칸 든 입력 이름은 따옴표로 이음 글 → 읽기 왕복."""
text = f"로직(GC000268, {dim._q('전력 계약종별')}='일반용(갑)Ⅰ 저압', 기계='0101-0007')"
got = dim._logic_link(text)
assert got and got["입력"]["전력 계약종별"] == "'일반용(갑)Ⅰ 저압'"
def test_main_504_amount_same(world) -> None:
"""EST main 504 = 원파일 저장 금액 546 그대로 — 별칭 없는 사본 = 내 기계 예정 · 별칭 있으면 품셈 EQ000534."""
import test_810_2_native_doc as t
def plan() -> tuple[dict, list]:
out = fn.emit(_main(), 504)
with store.at(world["company"]): # 개인 층 아님 = 안 만들고 예정만 적음
mm.attach(out, _main()["기계구성"])
return out, out["원본"].get("내기계예정") or []
out, todo = plan()
assert {"이름": WANT[0], "규격": WANT[1]} in todo
assert float(t.doc_and_money(out)[2]["계"]) == 546
with aliases("EQ", {"EQ000534": "파워트로웰"}): # 승인 별칭 = 품셈 기계로 이어짐
out, todo = plan()
assert not [m for m in todo if m["이름"] == WANT[0]]
assert "EQ000534" in json.dumps(out, ensure_ascii=False, default=str)
assert float(t.doc_and_money(out)[2]["계"]) == 546
+278
View File
@@ -0,0 +1,278 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""822-1 사본 빈틈 막기(요구 797-1) — 길마다: 만들기 → [저장] → M01 값(임시 · 메모리 덮개)을
기계 가격 · 노임 · 유가 · 자재 값으로 바꿈 → 금액 그대로 + 「단가새값」 → [단가 갱신] → 반영.
길 = 수식 길 줄 쌓기(품셈 기계 · 바꿔 끼운 기계 · 자원 줄 · 단가산출 참조) ·
옛 로직 길(더한줄 로직 · 재료 · 인력) · 가져온 식(단가기준 우리) · 층 복사 · [재생성] ·
빈틈 SK 표시(`사본없음`).
M01 바꿈 = `skc.Copy` 아닌 진짜 색인 사본(`dc.base` 가 못 벗김) · 정본 파일 안 고침.
"""
from __future__ import annotations
import copy
import json
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Doc_Sk_Path as path
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
DIG, DIG_NO, DIG_LOGIC = "EQ000022", "0201-0100", "GC000267" # 굴착기(무한궤도) 1.0㎥
DIG2_NO = "0201-0070" # 바꿔 끼울 굴착기
MAN = "LB000002" # 보통인부
PCT = "GC001109" # 금속기와 해체(인력 둘 + 노무비 3 %)
def _live():
return m01.loaded()[1]
def _move(monkeypatch, *keys: str) -> None:
"""M01 값 두 배 — 키마다 값 칸(기계 = 가격 · 자재 = 물가 칸) · 진짜 색인 사본(Copy 아님) ·
수식 문서 폴더 그대로."""
files, live = m01.loaded()
fake = copy.copy(live)
fake.index, fake.numbers = dict(live.index), dict(live.numbers)
for key in keys:
tid = key[:2]
row = copy.deepcopy(live.index[tid][key])
if tid == "EQ":
row["값"]["가격"] *= 2
elif tid == "MT":
for k in ("물가자료", "유통물가", "물가정보", "거래가격"):
row[k] = row[k] * 2 if row.get(k) else row.get(k)
else:
row["값"] *= 2
fake.index[tid] = {**fake.index[tid], key: row}
if row.get("원문번호"):
fake.numbers[tid] = {**fake.numbers[tid], str(row["원문번호"]): row}
monkeypatch.setattr(m01, "loaded", lambda: (files, fake))
def _at(folder, fn, *args):
with store.at(folder):
return fn(*args)
def _sk(folder, **body) -> str:
body = {"이름": "sub1임시 빈틈", "단위": "시간", "더한줄": [], **body}
return _at(folder, lambda: refs.create("sangeun", body)["이름"])
def _var(sym: str, link: dict, name: str = "") -> dict:
return {"기호": "P", "아래": sym, "위": "", "이름": name or sym, "단위": "", "연결": link}
def _line(sym: str, part: str = "경비") -> dict:
return {"이름": sym, "단위": "hr", "비목": part, "수량": "1", "단가": f"P_{sym}"}
def _stack(folder, key, rows: list[tuple[str, dict, str]]) -> None:
"""줄 쌓기 — 지금 문서에 (기호, 연결, 비목) 줄을 더해 수식 길로 저장."""
got = _at(folder, path.read, key)
doc = copy.deepcopy(got["문서"])
doc.setdefault("변수", []).extend(_var(s, link) for s, link, _ in rows)
doc.setdefault("금액", []).extend(_line(s, part) for s, _, part in rows)
_at(folder, path.save, key, {"문서": doc, "판": got["판"]})
def _sum(folder, key):
return _at(folder, path.read, key)["셈"]["계"]
def _price(folder, key) -> dict:
"""[단가 갱신] 저장 — 바뀐 자원 키 · 뒤 금액."""
look = _at(folder, path.preview, key, {"할일": skc.PRICE})
판 = _at(folder, path.read, key)["판"]
_at(folder, path.save, key, {"할일": skc.PRICE, "판": 판})
return {"바뀐": sorted(x["키"] for x in look["바뀐자원"]), "후": _sum(folder, key)}
MACHINE = {"갈래": "단가산출", "키": DIG_LOGIC, "입력": {"기계": f"'{DIG_NO}'"}}
def _fuel_and_crew(no: str = DIG_NO) -> tuple[str, str]:
"""기계 시간당 식이 읽는 유가(MO) · 조종원 노임(LB) 키."""
w = _live()
row = w.numbers["EQ"][no]
fuel = w.numbers["MO"][f"{row['값']['주연료종류']}:전국평균"]["키"]
crew = w.numbers["LB"][str(row["값"]["조종원직종"])]["키"]
return fuel, crew
# ── 수식 길 줄 쌓기 ───────────────────────────────────────────────────
@pytest.mark.parametrize("what", ["가격", "유가", "노임"])
def test_stack_m01_machine(world, monkeypatch, what):
"""품셈 기계(굴착기 1.0㎥) 줄 쌓기 → 저장 → 기계 가격 · 유가 · 조종원 노임 바뀜 →
금액 그대로 + 단가새값 → [단가 갱신] → 반영."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
was = _sum(me, key)
assert was > 0
fuel, crew = _fuel_and_crew()
moved = {"가격": DIG, "유가": fuel, "노임": crew}[what]
_move(monkeypatch, moved)
now = _at(me, path.read, key)
assert now["셈"]["계"] == was and now["사본"]["단가새값"] is True
got = _price(me, key)
assert got["바뀐"] == [moved] and got["후"] > was
def test_stack_swapped_machine(world, monkeypatch):
"""바꿔 끼운 기계 — 기계 입력을 다른 굴착기로 바꿔 저장 → 그 기계 값이 사본에."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
got = _at(me, path.read, key)
doc = copy.deepcopy(got["문서"])
for v in doc["변수"]:
if (v.get("연결") or {}).get("키") == DIG_LOGIC:
v["연결"]["입력"] = {"기계": f"'{DIG2_NO}'"}
_at(me, path.save, key, {"문서": doc, "판": got["판"]})
was = _sum(me, key)
other = _live().numbers["EQ"][DIG2_NO]["키"]
_move(monkeypatch, other)
assert _sum(me, key) == was
assert _price(me, key)["바뀐"] == [other]
@pytest.mark.parametrize("kind", ["인력", "재료"])
def test_stack_resource(world, monkeypatch, kind):
"""자원 줄 더하기(노임 · 자재 요소) — 저장 뒤 값 바뀌어도 그대로 · 갱신 뒤 반영."""
me = world["personal"]
res = MAN if kind == "인력" else next(iter(_live().index["MT"]))
key = _sk(me)
part = "노무비" if kind == "인력" else "재료비"
_stack(me, key, [("자원", {"갈래": "요소", "키": res}, part)])
was = _sum(me, key)
assert was > 0
_move(monkeypatch, res)
assert _sum(me, key) == was
assert _price(me, key)["바뀐"] == [res]
def test_stack_logic_ref(world, monkeypatch):
"""단가산출 참조(M01 단가산출을 부름) — 그 식이 읽는 노임도 사본에."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("참조", {"갈래": "단가산출", "키": PCT, "입력": {}}, "묶음")])
was = _sum(me, key)
assert was > 0
_move(monkeypatch, MAN)
assert _sum(me, key) == was
assert MAN in _price(me, key)["바뀐"]
# ── 옛 로직 길(더한줄 · 파생) ────────────────────────────────────────
OLD_ADDS = {
"로직 기계": ([{"종류": "로직", "로직": DIG_LOGIC, "입력값": {"기계": DIG_NO}, "수량": 1}],
{"기계": DIG_NO}, DIG),
"재료 요소": ([{"종류": "재료", "요소": "MT000008", "수량": 1}], {}, "MT000008"),
"인력 요소": ([{"종류": "인력", "요소": MAN, "수량": 1}], {}, MAN),
} # fmt: skip
@pytest.mark.parametrize("name", list(OLD_ADDS))
def test_old_added_lines(world, monkeypatch, name):
"""옛 로직 길 더한줄(로직 기계 · 재료 요소 · 인력 요소) — 저장 뒤 그대로 · 갱신 뒤 반영."""
me = world["personal"]
adds, given, moved = OLD_ADDS[name]
key = _sk(me, 더한줄=adds, 입력값=given)
was = _sum(me, key)
assert was > 0
assert _at(me, path.read, key)["사본"]["사본없음"] == 0
_move(monkeypatch, moved)
assert _sum(me, key) == was
got = _price(me, key)
assert moved in got["바뀐"] and got["후"] > was
# ── 가져온 식 · 층 복사 · 재생성 ──────────────────────────────────────
def test_imported_ours(world, monkeypatch):
"""가져온 식 단가기준 우리 — 이음 노임 바뀌어도 그대로 · 갱신 뒤 반영 · 엑셀 기준 = 엑셀 값."""
from M02_MasterTemplete import M02_Sangeun_Imported as imp
me = world["personal"]
man = {"종류": "인력", "이름": "보통인부", "단위": "인", "수량": "0.5", "비목": "노무비"}
row = {
"이름": "sub1임시 가져온",
"결과단위": "원/㎡",
"호표": [{**man, "단가": 165545, "이음": MAN}],
}
key = _at(me, lambda: imp.create(row, None, "sub1임시 가져온")["이름"])
assert _sum(me, key) == 82772 # 엑셀 단가 그대로
doc = _at(me, lambda: store.read("sangeun", key)["문서"])
ours = _at(me, lambda: imp.to_price(doc, "우리")["문서"])
_at(me, lambda: store.write("sangeun", key, store.read("sangeun", key)["판"], ours))
was = _sum(me, key)
_move(monkeypatch, MAN)
assert _sum(me, key) == was
doc = _at(me, lambda: store.read("sangeun", key)["문서"])
got = _at(me, skc.preview, doc, skc.PRICE)
assert [x["키"] for x in got["바뀐자원"]] == [MAN] and got["후"]["계"] > was
def test_layer_copy_keeps_copy(world, monkeypatch):
"""층 복사(개인 → 회사) — 줄 쌓기 기계 사본째 · 원본 M01 값 바뀐 뒤 복사해도 그 값."""
me, co = world["personal"], world["company"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
was = _sum(me, key)
_move(monkeypatch, DIG)
ref = {"층": "personal", "주인": "7/42", "키": key}
with store.at(co):
doc = refs.read_ref(ref)
new = refs.create("sangeun", refs.carry(refs.origin_mark(doc, ref), ("personal", "7/42")))
assert _sum(co, new["이름"]) == was
assert _at(co, path.read, new["이름"])["사본"]["단가새값"] is True
def test_regen_follows_m01(world, monkeypatch):
"""[재생성] — 지금 M01 로 다시 찍음(따로 문서 버림) · 그 뒤 M01 바뀌어도 그대로."""
me = world["personal"]
key = _sk(me, 바탕={"로직": PCT}, 단위="㎡")
_stack(me, key, [("기계", MACHINE, "묶음")])
_move(monkeypatch, MAN)
판 = _at(me, path.read, key)["판"]
_at(me, path.save, key, {"할일": skc.REGEN, "판": 판})
after = _at(me, path.read, key)
assert after["사본"]["따로"] is False and after["사본"]["사본없음"] == 0
assert not after["사본"]["단가새값"]
was = after["셈"]["계"]
_move(monkeypatch, MAN) # 한 번 더(네 배)
assert _sum(me, key) == was
def test_gap_mark_then_save_fills(world, monkeypatch):
"""빈틈 있는 옛 SK — 열면 지금 값으로 셈 + 「사본없음 n」 · 일괄 고쳐 쓰지 않음 ·
[저장] 하면 지금 값으로 굳음(n = 0) · 그 뒤 M01 바뀌어도 그대로."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
file = me / "sangeun" / f"{key}.json"
raw = json.loads(file.read_text(encoding="utf-8"))
vals = raw["문서"]["수식"] if "문서" in raw else raw["수식"]
for part in ("로직", "단가"):
vals[part] = [] # 옛 저장본 흉내 — 줄 쌓기 기계를 사본에 안 박은 때
file.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8")
before = file.read_bytes()
got = _at(me, path.read, key)
assert got["사본"]["사본없음"] > 0
was = got["셈"]["계"]
assert file.read_bytes() == before # 열기 = 안 고쳐 씀
files, live = m01.loaded()
_move(monkeypatch, DIG)
moved = _sum(me, key)
assert moved > was # 빈틈 = 지금 값으로 셈
got = _at(me, path.read, key)
_at(me, path.save, key, {"문서": got["문서"], "판": got["판"]})
now = _at(me, path.read, key)
assert now["사본"]["사본없음"] == 0 and now["셈"]["계"] == moved
monkeypatch.setattr(m01, "loaded", lambda: (files, live)) # 앞 값(절반)
assert _sum(me, key) == moved