diff --git a/M02_MasterTemplete/M02_Doc_FromNative.py b/M02_MasterTemplete/M02_Doc_FromNative.py index 125ac650b..3180dc8ce 100644 --- a/M02_MasterTemplete/M02_Doc_FromNative.py +++ b/M02_MasterTemplete/M02_Doc_FromNative.py @@ -50,6 +50,9 @@ def pack(got: dict, name: str) -> dict[str, Any]: """파서 답 → 둘 꼴(셈 · 변환에 쓰는 칸만 · JSON).""" out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []} out |= {"엑셀": True} if got.get("엑셀") else {} # 812-1 엑셀 읽기기(`M02_San_Xlsx`) — 같은 꼴 + out |= ( + {"기계구성": got["기계구성"]} if got.get("기계구성") else {} + ) # 821-4 EST 원파일 기계 구성 head = ("코드", "번", "이름", "규격", "단위", "저장") if got.get( "엑셀" @@ -104,7 +107,15 @@ def numbers(saved: dict) -> dict[int, dict]: def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]: - """원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError.""" + """원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError · + 기계 찾기 = M01 만(821-4 — 못 이은 기계 → 내 기계는 `M02_San_MyMachine.attach`).""" + from M02_MasterTemplete.M02_Machine_Find import m01_only + + with m01_only(): + return _emit(saved, no, keys) + + +def _emit(saved: dict, no: int, keys: bool) -> dict[str, Any]: blk = numbers(saved)[int(no)] if saved["꼴"] == "EST": names = {b["코드"]: n for n, b in numbers(saved).items()} diff --git a/M02_MasterTemplete/M02_Doc_Import.py b/M02_MasterTemplete/M02_Doc_Import.py index a019b1b44..0e4d53ca5 100644 --- a/M02_MasterTemplete/M02_Doc_Import.py +++ b/M02_MasterTemplete/M02_Doc_Import.py @@ -325,6 +325,8 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None: if not item.get("참조") and var["연결"].get("값") != excel: continue # SK 덮개 단가 = 사용자 값 link, note = _link_of(item) + if mine := (item.get("자원") or {}).get("내기계"): # 821-4 꼬리표 「내 기계」 + note = f"내 기계 {mine}" + ("" if link else " · 시간당 값 직접 — 엑셀 단가 그대로") if note: var["설명"] = note if link is None: diff --git a/M02_MasterTemplete/M02_Machine_Find.py b/M02_MasterTemplete/M02_Machine_Find.py index b0de98932..fe6fb7ef0 100644 --- a/M02_MasterTemplete/M02_Machine_Find.py +++ b/M02_MasterTemplete/M02_Machine_Find.py @@ -19,6 +19,9 @@ import math import re import time import unicodedata +from collections.abc import Iterator +from contextlib import contextmanager +from contextvars import ContextVar from functools import lru_cache from typing import Any @@ -150,10 +153,26 @@ def _table(whole: Any) -> tuple[list[Row], frozenset[str], dict[str, Row]]: _SEEN: list = [0.0, None, None] # (때, 표, 층) — M01 · 내 기계 판 확인은 층마다 1 초에 한 번 +_M01_ONLY: ContextVar[bool] = ContextVar("m01_only", default=False) + + +@contextmanager +def m01_only() -> Iterator[None]: + """이 안의 찾기 = M01 기계만(821-4 불러오기 — 내 기계 잇기는 `M02_San_MyMachine.attach` 한 곳).""" + token = _M01_ONLY.set(True) + try: + yield + finally: + _M01_ONLY.reset(token) + + def table() -> tuple[list[Row], frozenset[str], dict[str, Row]]: + 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 + if _M01_ONLY.get(): + return _table(m01.loaded()[1]) now, here = time.monotonic(), store.root() if _SEEN[1] is None or now - _SEEN[0] > 1.0 or _SEEN[2] != here: _SEEN[:] = [now, _table(mym.whole()), here] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts index d7b67663e..2290bcb87 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts @@ -5,6 +5,7 @@ * 바꾸기 전후 금액 · [원본 글 보기] * · 줄 2 = 이음 없는 자원 = 표지 + [이음 고르기] * · 줄 2-2 = 소요량 · 계수 이음(797-2 · 후보 집계 + [이음 보기] → `Hopyo_QtyLinks` 모달) + * · 줄 2-3 = 못 이은 기계 → 내 기계(821-4) — 「내 기계 생성 n」 · 줄마다 「내 기계」 꼬리표 · 개인 층 밖 = 「만들어질 내 기계 n」 * · 경쟁사 로직 그대로 — 기초로 바꾸기 · 근거 기초 제안 없음(809-1 · 요구 703) * 부품 = 단추 · 배지 · 글 · 입력 · 띠 · 표 · 모달(날 단추 · 입력 없음) · 서버 길은 부르는 쪽. * ========================================================================== */ @@ -47,6 +48,12 @@ const TEXT = { Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"], QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"], QtyOpen: ["이음 보기", "Show links"], + MyMade: ["내 기계 생성 {n}", "My machines created {n}"], + MyUsed: ["내 기계 이음 {n}", "Linked to my machines {n}"], + MyPlan: ["만들어질 내 기계 {n}", "My machines to create {n}"], + MyTag: ["내 기계", "My machine"], + MyNew: ["이번에 만듦 · 값 확인 필요", "Created now · check values"], + MyOld: ["있던 내 기계에 이음", "Linked to an existing one"], Close: ["닫기", "Close"], } as const satisfies Record; export const tx = (k: keyof typeof TEXT): string => @@ -116,6 +123,34 @@ function checkBadge(doc: ImportedDoc): HTMLElement { return b; } +/** 못 이은 기계 → 내 기계(821-4) — 생성 수 · 이은 수 · 줄마다 꼬리표 · 예정 수 */ +function myMachines(src: NonNullable["원본"]): HTMLElement | null { + const got = src?.내기계 ?? []; + const plan = src?.내기계예정 ?? []; + if (!got.length && !plan.length) return null; + const made = got.filter((m) => m.새로).length; + const items: HTMLElement[] = []; + if (made) items.push(createBadge(tx("MyMade").replace("{n}", String(made)), "info")); + if (got.length > made) items.push(createBadge(tx("MyUsed").replace("{n}", String(got.length - made)), "neutral")); // prettier-ignore + for (const m of got) { + const b = createBadge( + `${tx("MyTag")} · ${[m.이름, m.규격].filter(Boolean).join(" ")}`, + "accent", + ); + b.title = `${m.키} · ${m.새로 ? tx("MyNew") : tx("MyOld")}`; + b.dataset.importedMine = m.키; + items.push(b); + } + if (plan.length) { + const b = createBadge(tx("MyPlan").replace("{n}", String(plan.length)), "warning"); + b.title = plan.map((m) => [m.이름, m.규격].filter(Boolean).join(" ")).join("\n"); + items.push(b); + } + const bar = createToolbar({ items }); + bar.dataset.importedMineBar = String(made); + return bar; +} + /** 머리 판 */ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement { const meta = s.doc.가져온 ?? {}; @@ -163,6 +198,8 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement { ]; kids.push(createToolbar({ items: [...tags, ...items] })); } + const mine = myMachines(src); + if (here && mine) kids.push(mine); if (here && s.qty?.자리) { const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty }); open.dataset.importedQty = String(s.qty.자리); diff --git a/M02_MasterTemplete/M02_San_Book.py b/M02_MasterTemplete/M02_San_Book.py index 151f4c99b..148f659f2 100644 --- a/M02_MasterTemplete/M02_San_Book.py +++ b/M02_MasterTemplete/M02_San_Book.py @@ -21,6 +21,7 @@ STM_SHEETS = ( "재료비목록표", "경비목록표", "중기목록표", + "중기사용료", # 821-4 기계 구성(못 이은 기계 → 내 기계 값) ) EST_SHEETS = ( "단가산출총괄표", diff --git a/M02_MasterTemplete/M02_San_Estx.py b/M02_MasterTemplete/M02_San_Estx.py index 098f665b8..353208dea 100644 --- a/M02_MasterTemplete/M02_San_Estx.py +++ b/M02_MasterTemplete/M02_San_Estx.py @@ -444,5 +444,8 @@ def read(data: bytes) -> dict[str, Any]: blk["줄합"] = head_sum(lines) out[key].append({**blk, "줄": lines, "표지": tags}) out["자원"] = {f"{LINK[k[0]]}:{k[1]}": v for k, v in res.items()} + from M02_MasterTemplete.M02_San_MyMachine import est_makes + + out["기계구성"] = est_makes(t) # 821-4 못 이은 기계 → 내 기계 값 out["표지"] = sorted(warn) + ["옵션(datestoption.opt) · 원가 서식(wongadat) 안 읽음"] return out diff --git a/M02_MasterTemplete/M02_San_MyMachine.py b/M02_MasterTemplete/M02_San_MyMachine.py new file mode 100644 index 000000000..ce545e86a --- /dev/null +++ b/M02_MasterTemplete/M02_San_MyMachine.py @@ -0,0 +1,312 @@ +"""불러오기 못 이은 기계 → 내 기계 자동 생성(PLAN 821-4 · 요구서_796 4 장 · 방향 장부 Q1 · S) — 서버 단독. + +구성 읽기(그 파일 값 → 우리 틀 값 · 경쟁사 조종원 노임 환산 ×16/12 ×25/20 은 안 가져옴): + EST 원파일 MachMainDat(`est_makes` · 올릴 때 둘 꼴 칸 `기계구성`) · EST 엑셀 「기계경비」 · + STmate 엑셀 「중기사용료」(`xlsx_makes` · 둔 산근 시트에서 그때 읽음) → {열쇠: 구성} + 구성 = {이름, 규격, 손료 [[계수 10⁻⁷, 가격 원, 달러?]], 주연료종류, 주연료 ℓ, 잡재료율 %, 조종원 인, + 조종원직종(글), 전력 kWh} · 열쇠 = 자원 코드 · 「이름|규격」(표기 정규화 `M02_Machine_Find.norm`). +고름(`plan`) = 가져온 로직 호표 기계 줄 가운데 이음 없음 · 찾기 판정 「못 찾음」(별칭 · 표기 · 같은 파일 대조 뒤 · + 후보 여럿 = 사람이 고름이라 안 만듦) · 조합 기계 = 조각마다(첫 조각 = 첫 손료 + 연료 · 조종원 · + 다음 조각 = 그 차례 손료만 · 구성 없는 조합 = 안 만듦). +몸 = 구성 있음 → 방식 바로(가격 천원 · 달러 = × 환율 FX000001 + 비고 · 손료계수 · 주연료 → 우리 유가 이름 · + 잡품 % · 조종원 → 우리 직종 원문번호 · 전력) · 못 이은 연료 · 직종 = 칸 비움 + 비고 「확인 필요」 · + 구성 없음 → 직접(그 파일 시간당 노 · 재 · 경). +잇기(`attach` · 층 저장 길 `M02_Sangeun_FromHopyo.make` 만) = 그 층 내 기계 같은 이름 · 규격이면 그 줄 · 없으면 + 새로(`M02_MyMachine.create`) · 바로 = 기계 줄 이음 키 · 로직 줄 = 시간당 부름 글 / 직접 = 이음 없이 자원 + `내기계` 키(셈 = 그 파일 값 · [우리 단가로] = 내 기계 시간당 · `M02_Sangeun_Imported.row_of`) · + 원본 `내기계` [{키, 이름, 규격, 새로}] · 개인 층 아님 = 안 만듦(원본 `내기계예정` [{이름, 규격}] 만). +금액(엑셀 단가 그대로)은 줄 값을 안 건드려 그대로. +""" + +from __future__ import annotations + +import re +from typing import Any + +from M02_MasterTemplete.M02_Machine_Find import NONE, norm + +NOTE = "불러오기 자동 생성 — {file} · 값 확인 필요" +CHECK = "확인 필요" +COST = {"L": "노무비", "M": "재료비", "S": "경비"} + + +def key_of(name: Any, spec: Any) -> str: + return f"{norm(name)}|{norm(spec)}" + + +def _num(v: Any) -> float | None: + if isinstance(v, bool) or v is None: + return None + if isinstance(v, (int, float)): + return float(v) + s = str(v).strip().replace(",", "") + return float(s) if re.fullmatch(r"-?\d+(?:\.\d*)?|-?\.\d+", s) else None + + +def _blank(name: str = "", spec: str = "") -> dict[str, Any]: + return {"이름": name, "규격": spec, "손료": []} + + +def _fuel(make: dict, kind: Any, litre: Any) -> None: + """주연료 종류 · ℓ(처음 것) — 종류 「전력」 = 전력 kWh 칸.""" + if re.sub(r"\s", "", str(kind or "")) == "전력" and (_num(litre) or 0) > 0: + make["전력"] = _num(litre) + elif make.get("주연료") is None and (_num(litre) or 0) > 0: + make["주연료종류"], make["주연료"] = str(kind or "").strip(), _num(litre) + + +# ── 구성 읽기 ───────────────────────────────────────────────────────── +def est_makes(t: dict[str, list[dict]]) -> dict[str, dict]: + """EST 원파일 MachMainDat → {자원 코드: 구성} — TitleCode 머리 다음 줄부터 빈 줄까지 · + Index -3 손료(GN 계수 · TU 가격 · 단위 「$」 = 달러) · -2 「주연료」(Size 종류) · 연료 이름 줄(선박용경유 등 ℓ) · + 잡품 · 잡유 % · 전력 KWH · -1 조종원(직종 · GN 인).""" + heads = {r.get("Code"): r for r in t.get("MachTitleDat", [])} + out: dict[str, dict] = {} + cur = None + for r in t.get("MachMainDat", []): + if r.get("TitleCode"): + head = heads.get(r["TitleCode"]) or r + cur = out.setdefault( + r["TitleCode"], _blank(head.get("Name") or "", head.get("Size") or "") + ) + elif not r.get("Name"): + cur = None + elif cur is not None: + i, gn, word = r.get("Index"), r.get("GN"), re.sub(r"\s", "", str(r["Name"])) + unit = str(r.get("Unite") or "").strip().upper() + if i == -3 and gn: + cur["손료"].append([gn, r.get("TU"), unit == "$"]) + elif i == -2 and word == "주연료": + _fuel(cur, r.get("Size"), gn) + elif i == -2 and word in ("잡품", "잡유") and cur.get("잡재료율") is None: + cur["잡재료율"] = gn + elif i == -2 and word == "전력": + cur["전력"] = gn + elif i == -2 and unit == "ℓ": + _fuel(cur, r["Name"], gn) + elif i == -1 and (gn or 0) > 0 and not cur.get("조종원"): + cur["조종원"], cur["조종원직종"] = gn, str(r["Name"]).strip() + return {k: v for k, v in out.items() if v["손료"]} + + +def _est_sheet(rows: list[list]) -> dict[str, dict]: + """EST 엑셀 「기계경비」 — 「N. 이름 · 규격」 머리 아래 빈 줄까지: 「손료 가격 ₩×(달러 $×) 계수 × 10(-7)」 · + 「주연료 종류 ℓ」 · 「잡품 주연료의 % 」 · 「직종 인수 인」 · 「전력 … kWh」.""" + out: dict[str, dict] = {} + cur = None + for row in rows: + v = [c for c in row if c not in (None, "")] + head = re.match(r"\d+\.\s*(.+)", str(v[0])) if v else None + if head: + spec = v[1] if len(v) > 1 and isinstance(v[1], str) else "" + cur = out.setdefault(key_of(head.group(1), spec), _blank(head.group(1).strip(), spec)) + elif not v: + cur = None + elif cur is not None: + word = re.sub(r"\s", "", str(v[0])) + mark = next((x for x in v if x in ("₩×", "$×")), None) + if "손료" in word and mark and (at := v.index(mark) + 1) < len(v): + cur["손료"].append([_num(v[at]), _num(v[1]), mark == "$×"]) + elif word == "주연료" and len(v) > 2: + _fuel(cur, v[1], v[2]) + elif word in ("잡품", "잡유") and len(v) > 2 and cur.get("잡재료율") is None: + cur["잡재료율"] = _num(v[2]) + elif word == "전력" and len(v) > 2: + cur["전력"] = _num(v[1]) if _num(v[1]) is not None else _num(v[2]) + elif len(v) > 2 and v[2] == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"): + cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip() + elif len(v) > 3 and str(v[3]).upper() in ("ℓ", "L") and (_num(v[2]) or 0) > 0: + _fuel(cur, v[0], v[2]) + return {k: m for k, m in out.items() if m["손료"] and None not in m["손료"][0][:2]} + + +def _stm_sheet(rows: list[list]) -> dict[str, dict]: + """STmate 엑셀 「중기사용료」 — 「제 N 호표」 뒤 머리 [이름, 규격, 시간, 코드] · 손료 [이름, 규격, 계수(천원당), + 「천원」, 가격 천원 …] · 조종원 [직종, 인, 「인」 …] · 연료 [이름, 등급, ℓ, 「L」 …] · 잡품 [잡품, 주연료의%, % …] · + 「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 10⁴ · 가격 = 천원 × 1000 원).""" + out: dict[str, dict] = {} + cur = None + for row in rows: + v = [c for c in row if c not in (None, "")] + first = re.sub(r"\s", "", str(v[0])) if v else "" + if re.fullmatch(r"제\d+호표", first): + cur = {} + continue + if cur is None or not v: + continue + if first == "합계": + cur = None + continue + if ( + not cur + and len(v) >= 4 + and str(v[2]) in ("시간", "hr") + and re.fullmatch(r"X\d+", str(v[-1])) + ): + cur.update(_blank(str(v[0]), str(v[1]))) + out[str(v[-1])] = out[key_of(v[0], v[1])] = cur + elif not cur: + continue + elif len(v) > 4 and str(v[3]) == "천원" and _num(v[2]) and _num(v[4]): + cur["손료"].append([_num(v[2]) * 10_000, _num(v[4]) * 1000, False]) + elif len(v) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"): + cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip() + elif first == "잡품" and len(v) > 2 and cur.get("잡재료율") is None: + cur["잡재료율"] = _num(v[2]) + elif len(v) > 3 and str(v[3]).upper() in ("L", "ℓ"): + _fuel(cur, v[0], v[2]) + elif len(v) > 3 and str(v[3]).upper() == "KWH": + cur["전력"] = _num(v[2]) + return {k: m for k, m in out.items() if m.get("손료")} + + +def xlsx_makes(sheets: dict[str, list]) -> dict[str, dict]: + """엑셀 산근 시트 → 구성(EST 「기계경비」 · STmate 「중기사용료」 · 없음 = {}).""" + if sheets.get("기계경비"): + return _est_sheet(sheets["기계경비"]) + if sheets.get("중기사용료"): + return _stm_sheet(sheets["중기사용료"]) + return {} + + +# ── 고름 · 몸 ───────────────────────────────────────────────────────── +def _todo(logic: dict) -> list[tuple[dict, int]]: + """(줄, 조각 차례) — 기계 줄 · 이음 없음 · 찾기 판정 「못 찾음」.""" + out, n = [], 0 + for line in logic.get("호표") or []: + res = line.get("자원") or {} + if res.get("갈래") != "기계": + continue + n = n + 1 if res.get("조합") else 0 + if line.get("이음") or res.get("조합식") or res.get("내기계"): + continue + if (res.get("찾기") or {}).get("판정") == NONE: + out.append((line, n)) + return out + + +def _make_of(res: dict, makes: dict) -> dict | None: + return makes.get(str(res.get("코드") or "")) or makes.get( + key_of(res.get("이름"), res.get("규격")) + ) + + +def _kinds(make: dict, notes: list[str]) -> dict[str, Any]: + """연료 · 조종원 → 우리 유가 이름 · 직종 원문번호(못 이음 = 칸 비움 + 비고).""" + from M01_MasterData import M01_MasterData_Store as m01 + + index, out = m01.loaded()[1], {} + fuel, litre = str(make.get("주연료종류") or ""), make.get("주연료") or 0 + if litre: + ours = next( + (n for n in ("경유", "휘발유", "실내등유") if n == re.sub(r"\s", "", fuel)), None + ) + ours = ours or next((n for n in ("경유", "휘발유", "실내등유") if fuel.startswith(n)), None) + if ours and f"{ours}:전국평균" in (index.numbers.get("MO") or {}): + out |= {"주연료": litre, "주연료종류": ours} + out |= {"잡재료율": make["잡재료율"]} if make.get("잡재료율") else {} + else: + notes.append(f"주연료 「{fuel}」 {litre:g}ℓ — 우리 유가에 못 이음({CHECK})") + if make.get("조종원"): + job = norm(make.get("조종원직종")) + hit = [k for k, r in (index.numbers.get("LB") or {}).items() if norm(r.get("이름")) == job] + if len(hit) == 1: + out |= {"조종원": make["조종원"], "조종원직종": hit[0]} + else: + notes.append( + f"조종원 「{make.get('조종원직종')}」 {make['조종원']:g}인 — 우리 직종에 못 이음({CHECK})" + ) + if make.get("전력"): + out["전력"] = make["전력"] + return out + + +def body(make: dict | None, n: int, line: dict, file: str) -> dict | None: + """내 기계 몸(`M02_MyMachine.create`) — 구성 있음 = 바로 · 없음 = 직접(조합 조각 = 안 만듦).""" + from M01_MasterData import M01_MasterData_Store as m01 + + res = line.get("자원") or {} + found = res.get("찾기") or {} + name = str(found.get("이름") or res.get("이름") or line.get("이름") or "").strip() + spec = str((found.get("규격") if found.get("이름") else res.get("규격")) or "").strip() + head = {"이름": name, "규격": spec} + notes = [NOTE.format(file=file)] + if make is None: + if n or res.get("조합첫"): + return None + got = res.get("단가") or {} + pack = line.get("값묶음") or {} + hour = {v: _num(got.get(c)) or _num(pack.get(v)) or 0 for c, v in COST.items()} + return {**head, "방식": "직접", "시간당": hour, "비고": " · ".join(notes)} + if n >= len(make["손료"]): + return None + coef, price, usd = make["손료"][n] + if usd: + rate = float(m01.loaded()[1].index["FX"]["FX000001"]["값"]) + notes.append(f"가격 ${price:g} × 환율 {rate:,} 원/USD") + price = price * rate + vals: dict[str, Any] = {"가격": round(float(price) / 1000, 4), "손료계수": coef} + if n == 0: + vals |= _kinds(make, notes) + return {**head, "방식": "바로", "값": vals, "비고": " · ".join(notes)} + + +def plan(logic: dict, makes: dict, file: str) -> list[dict]: + """만들(또는 이을) 내 기계 [{줄, 열쇠, 몸}] — 글 · 층 안 건드림.""" + out = [] + for line, n in _todo(logic): + got = body(_make_of(line.get("자원") or {}, makes), n, line, file) + if got: + out.append({"줄": line, "열쇠": key_of(got["이름"], got["규격"]), "몸": got}) + return out + + +def preview(out: dict, makes: dict) -> None: + """미리 보기 · 분석 길 — 원본 `내기계예정` [{이름, 규격}](안 만듦).""" + todo = plan(out["로직"], makes, str(out["원본"].get("파일") or "")) + seen = {t["열쇠"]: {"이름": t["몸"]["이름"], "규격": t["몸"]["규격"]} for t in todo} + if seen: + out["원본"]["내기계예정"] = list(seen.values()) + + +# ── 잇기(층 쓰기) ───────────────────────────────────────────────────── +def link(line: dict, row: dict) -> None: + """가져온 줄 → 내 기계 — 바로 = 기계 줄 키 · 로직 줄 시간당 부름 글 / 직접 = 자원 `내기계` 만.""" + from M02_MasterTemplete.M02_San_Link import hourly_text + + line["자원"]["내기계"] = row["키"] + if row.get("방식") == "직접": + return + line["이음"] = hourly_text(row["키"]) if line.get("종류") == "로직" else row["키"] + + +def attach(out: dict, makes: dict) -> list[dict]: + """층 저장 길 — 못 이은 기계 줄을 그 층 내 기계(같은 이름 · 규격) · 새 내 기계에 이음 · 원본 `내기계`.""" + from M02_MasterTemplete import M02_MasterTemplete_Store as store + from M02_MasterTemplete import M02_MyMachine as mym + + src = out["원본"] + todo = plan(out["로직"], makes, str(src.get("파일") or "")) + if not todo: + return [] + try: + mym._user() + except store.StoreError: # 개인 층 아님 — 안 만듦(예정만) + preview(out, makes) + return [] + done: dict[str, dict] = {} + for one in todo: + if one["열쇠"] not in done: + have = next( + (r for r in mym.rows() if key_of(r.get("이름"), r.get("규격")) == one["열쇠"]), None + ) + row = have or mym.create(one["몸"])["줄"] + done[one["열쇠"]] = {"줄": row, "새로": have is None} + link(one["줄"], done[one["열쇠"]]["줄"]) + made = [ + {**{k: d["줄"][k] for k in ("키", "이름", "규격", "방식")}, "새로": d["새로"]} + for d in done.values() + ] + src["내기계"] = made + src.pop("내기계예정", None) + return made diff --git a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py index 9aed1d97d..30613e4a9 100644 --- a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py +++ b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py @@ -25,6 +25,7 @@ from M02_MasterTemplete import M02_Doc_FromNative as fn from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_San_Emit as em +from M02_MasterTemplete import M02_San_MyMachine as mm DIRNAME = "산근" RELOAD = "가져온 식 자료 없음 — 엑셀을 다시 올림" @@ -109,7 +110,15 @@ def has(folder: str | Path, key: str) -> bool: def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]: - """HP 단산 블록 하나 → `{로직, 원본}`(원파일 우선 · 아니면 엑셀 읽기기 같은 길 · 이음 = M01 키).""" + """HP 단산 블록 하나 → `{로직, 원본}`(원파일 우선 · 아니면 엑셀 읽기기 같은 길 · 이음 = M01 키) · + 못 이은 기계 = 원본 `내기계예정`(821-4 · 안 만듦).""" + out, makes = _emit(folder, key, no) + mm.preview(out, makes) + return out + + +def _emit(folder: str | Path, key: str, no: int) -> tuple[dict[str, Any], dict]: + """(블록 `{로직, 원본}`, 그 파일 기계 구성 — 원파일 둘 꼴 `기계구성` · 엑셀 = 둔 시트 · 821-4).""" hopyo_store.read(folder, key) # 없는 HP = 404 path = _path(folder, key) try: @@ -118,13 +127,13 @@ def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]: raise store.StoreError(409, RELOAD) from e native = saved.get("원파일") if isinstance(native, dict) and int(no) in fn.numbers(native): # 원파일 우선(810-2) - return fn.emit(native, int(no)) + return fn.emit(native, int(no)), native.get("기계구성") or {} if not saved.get("시트"): raise store.StoreError(404, f"없는 단산{no}") excel = _excel_of(path, saved) if int(no) not in fn.numbers(excel): raise store.StoreError(404, f"없는 단산{no}") - return fn.emit(excel, int(no)) + return fn.emit(excel, int(no)), mm.xlsx_makes(saved["시트"]) def _inner( @@ -173,7 +182,7 @@ def make(source: Path, key: str, no: int, name: str = "", _trail: tuple = ()) -> 안쪽 산근 블록은 형제 SK 를 먼저 만들어 참조(797-1).""" from M02_MasterTemplete import M02_Sangeun_Imported as imp - out = emit(source, key, no) + out, makes = _emit(source, key, no) out["원본"]["가져온때"] = utc_now_text() # 원문 패널 「어디서 왔나」(807-1) _link_inner(source, key, no, out, _trail) try: # 소요량 · 계수 후보 표지(797-2 · 사람 확인) — 못 셈 = 표지 없이 만듦 @@ -182,13 +191,15 @@ def make(source: Path, key: str, no: int, name: str = "", _trail: tuple = ()) -> out["원본"]["표지"] += qty.flags(out["로직"]) except Exception: # noqa: BLE001 — 역색인 짓기 실패가 가져온 식 만들기를 막지 않음 pass + made = mm.attach(out, makes) # 821-4 못 이은 기계 = 내 기계(그 층 같은 것 · 새로) + mine = {"내기계": made} if made else {} title = str(name or "").strip() or str(out["로직"].get("이름") or f"단산{no}") try: got = imp.create(out["로직"], out["원본"], title) - return {**got, "다시씀": False} + return {**got, "다시씀": False, **mine} except store.StoreError as e: same = (e.detail or {}).get("같은것") if isinstance(e.detail, dict) else None if e.status != 409 or not same: raise got = imp.create(out["로직"], out["원본"], title, use=same[0]["키"]) - return {**got, "다시씀": True} + return {**got, "다시씀": True, **mine} diff --git a/M02_MasterTemplete/M02_Sangeun_Imported.py b/M02_MasterTemplete/M02_Sangeun_Imported.py index ae10be471..ddfa375dd 100644 --- a/M02_MasterTemplete/M02_Sangeun_Imported.py +++ b/M02_MasterTemplete/M02_Sangeun_Imported.py @@ -63,9 +63,10 @@ def _mf() -> Any: def _whole() -> Any: - from M01_MasterData import M01_MasterData_Store as m01 + """M01 + 지금 층 내 기계(821-4 · 못 이은 기계를 이은 개인 키 `EQP…`).""" + from M02_MasterTemplete import M02_MyMachine as mym - return m01.loaded()[1] + return mym.whole() def is_imported(doc: Any) -> bool: @@ -80,7 +81,7 @@ def hourly_of(key: str) -> dict: """이음 기계(EQ) 시간당 노 · 재 · 경 — M01 판이 바뀌면 다시(캐시 = M01 묶음 하나에 매임).""" from M02_MasterTemplete import M02_Machine_Hourly as machine - whole = _whole() + whole = _whole() # 내 기계 고침 = 색인이 바뀜 → 다시 if _HOURLY["whole"] is not whole: _HOURLY.update(whole=whole, rows={}) if key not in _HOURLY["rows"]: @@ -112,6 +113,8 @@ def row_of(doc: Any) -> dict | None: "참조" ): # 안쪽 산근 = 형제 SK 1 단위 값(셈이 채움 · 엑셀 값묶음은 얼린 사본으로 둠) item |= {"요소": None, "값묶음": None, "기준": "우리" if ours else "엑셀"} + if ours and not link and (mine := (one.get("자원") or {}).get("내기계")): + _direct(item, one, mine) # 821-4 시간당 값 직접 내기계 = 그 줄 값 if ours and link: item.pop("단가", None) item.pop("값묶음", None) @@ -124,6 +127,18 @@ def row_of(doc: Any) -> dict | None: return {**row, "키": doc.get("키"), "호표": items} +def _direct(item: dict, one: dict, key: str) -> None: + """[우리 단가로] 이음 없이 이은 내 기계(시간당 값 직접 · 821-4) — 단가 = 그 비목 · 값묶음 = 셋 · + 지운 내 기계 = 엑셀 값 그대로.""" + got = hourly_of(key) + if not got.get("ok"): + return + if "단가" in item and one.get("비목") in COSTS: + item["단가"] = sd.plain(Decimal(str(got[one["비목"]]))) + elif "값묶음" in item: + item["값묶음"] = {k: sd.plain(Decimal(str(got[k]))) for k in COSTS} + + def base_row(doc: dict) -> dict[str, Any]: """바탕 없는 SK 바탕 줄(`sd.empty_row`) — 가져온 식 = 그 줄 · 아니면 입력 · 줄 없음.""" return row_of(doc) or { @@ -314,7 +329,7 @@ def unlinked(doc: dict) -> list[dict]: {"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)} | ({"견적": True} if (one.get("자원") or {}).get("갈래") == "고정" else {}) for n, one in enumerate(doc[KEY]["로직"].get("호표") or []) - if not one.get("이음") and not one.get("참조") + if not one.get("이음") and not one.get("참조") and not (one.get("자원") or {}).get("내기계") ] diff --git a/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts b/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts index cde16bbac..6d5a8598e 100644 --- a/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts +++ b/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts @@ -27,6 +27,10 @@ export interface ImportedMeta { 갈래?: string; 엑셀금액?: Money4; 글줄?: { 행?: number | null; 글?: string; 자리?: string; 값?: number | null }[]; + /** 못 이은 기계 → 내 기계(821-4 · 새로 = 이번에 만듦 · 아니면 그 층 같은 것) */ + 내기계?: { 키: string; 이름: string; 규격?: string; 방식?: string; 새로?: boolean }[]; + /** 개인 층 밖에서 연 것 — 만들어질 내 기계(안 만듦) */ + 내기계예정?: { 이름: string; 규격?: string }[]; }; 검산?: { 같음: boolean; diff --git a/resources/knowledge/technical_info/01_임도/05_원가정보/기계경비_산정.md b/resources/knowledge/technical_info/01_임도/05_원가정보/기계경비_산정.md index d58b85698..0af974c56 100644 --- a/resources/knowledge/technical_info/01_임도/05_원가정보/기계경비_산정.md +++ b/resources/knowledge/technical_info/01_임도/05_원가정보/기계경비_산정.md @@ -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 은 안 가져옴 · 달러 표기 = 환율 적용 · 비고 「값 확인 필요」) · 구성 없는 파일은 시간당 값을 그대로 담음. diff --git a/resources/master_data/_키대장.json b/resources/master_data/_키대장.json index 46a4a4b24..c2c51ec34 100644 --- a/resources/master_data/_키대장.json +++ b/resources/master_data/_키대장.json @@ -1,6 +1,6 @@ { "다음": {"LB": 409, "EQ": 669, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 338, "CC": 528, "GC": 1196, "QC": 2060, "GX": 5, "UA": 38}, - "폐기": {"MT000001": "자재품목 휘발유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000002": "자재품목 경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000003": "자재품목 선박용경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000004": "자재품목 중유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000005": "자재품목 등유 — 값이 비고 글뿐 · 유가 테이블로 감", "LB000399": "이름 「〃」 — 직종이 아니라 되풀이 기호 · 소요량 13-6-9 에서 윗줄 직종으로 바로잡음", "MN": "재료_나라장터자재 — old 로 옮김 · 로직은 자재품목·품셈재료로 갈아 끼움", "LB000372": "미확보 고급기능사 — 측량노임 항공사진고급기능사 옛이름으로 옮김", "LB000393": "미확보 리베팅공 — 건설노임 철골공 옛이름으로 옮김(2010 통합표 연번10)", "LB000405": "미확보 Belt Conveyor 설치공 — 품셈 13-10-1 주② 배분으로 다섯 직종에 나눔", "MT000006": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "MT000007": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "LB000368": "미확보 기술사 — 기술사(건설) 옛이름으로 옮김", "LB000369": "미확보 특급기술자 — 특급기술자(건설) 옛이름으로 옮김", "LB000370": "미확보 고급기술자 — 고급기술자(건설) 옛이름으로 옮김", "LB000361": "미확보 중급기술자 — 중급기술자(건설) 옛이름으로 옮김", "LB000364": "미확보 초급기술자 — 초급기술자(건설) 옛이름으로 옮김", "LB000365": "미확보 고급숙련기술자 — 고급숙련기술자(건설) 옛이름으로 옮김", "LB000366": "미확보 중급숙련기술자 — 중급숙련기술자(건설) 옛이름으로 옮김", "LB000367": "미확보 초급숙련기술자 — 초급숙련기술자(건설) 옛이름으로 옮김", "LB000389": "미확보 기계기사 — 초급기술자(기계·설비) 옛이름으로 옮김", "LB000388": "미확보 기계산업기사 — 초급숙련기술자(기계·설비) 옛이름으로 옮김", "MO000055": {"원문번호": "주택용저압:기본_200이하", "파일": "재료_유가전력.json"}, "MO000056": {"원문번호": "주택용저압:기본_201~400", "파일": "재료_유가전력.json"}, "MO000057": {"원문번호": "주택용저압:기본_400초과", "파일": "재료_유가전력.json"}, "MO000059": {"원문번호": "주택용저압:전력량_다음200", "파일": "재료_유가전력.json"}, "MO000060": {"원문번호": "주택용저압:전력량_400초과", "파일": "재료_유가전력.json"}, "MO000061": {"원문번호": "주택용저압:월간최저요금", "파일": "재료_유가전력.json"}, "MO000062": {"원문번호": "일반용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000063": {"원문번호": "일반용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000064": {"원문번호": "일반용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000065": {"원문번호": "일반용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000066": {"원문번호": "산업용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000067": {"원문번호": "산업용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000068": {"원문번호": "산업용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000069": {"원문번호": "산업용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000070": {"원문번호": "임시전력갑:기본요금적용", "파일": "재료_유가전력.json"}, "MO000071": {"원문번호": "임시전력갑:전력량요금적용", "파일": "재료_유가전력.json"}, "MO000072": {"원문번호": "임시전력갑:월간최저요금", "파일": "재료_유가전력.json"}, "GC001145": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001148": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001151": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001156": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001159": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001166": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)"}, + "폐기": {"MT000001": "자재품목 휘발유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000002": "자재품목 경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000003": "자재품목 선박용경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000004": "자재품목 중유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000005": "자재품목 등유 — 값이 비고 글뿐 · 유가 테이블로 감", "LB000399": "이름 「〃」 — 직종이 아니라 되풀이 기호 · 소요량 13-6-9 에서 윗줄 직종으로 바로잡음", "MN": "재료_나라장터자재 — old 로 옮김 · 로직은 자재품목·품셈재료로 갈아 끼움", "LB000372": "미확보 고급기능사 — 측량노임 항공사진고급기능사 옛이름으로 옮김", "LB000393": "미확보 리베팅공 — 건설노임 철골공 옛이름으로 옮김(2010 통합표 연번10)", "LB000405": "미확보 Belt Conveyor 설치공 — 품셈 13-10-1 주② 배분으로 다섯 직종에 나눔", "MT000006": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "MT000007": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "LB000368": "미확보 기술사 — 기술사(건설) 옛이름으로 옮김", "LB000369": "미확보 특급기술자 — 특급기술자(건설) 옛이름으로 옮김", "LB000370": "미확보 고급기술자 — 고급기술자(건설) 옛이름으로 옮김", "LB000361": "미확보 중급기술자 — 중급기술자(건설) 옛이름으로 옮김", "LB000364": "미확보 초급기술자 — 초급기술자(건설) 옛이름으로 옮김", "LB000365": "미확보 고급숙련기술자 — 고급숙련기술자(건설) 옛이름으로 옮김", "LB000366": "미확보 중급숙련기술자 — 중급숙련기술자(건설) 옛이름으로 옮김", "LB000367": "미확보 초급숙련기술자 — 초급숙련기술자(건설) 옛이름으로 옮김", "LB000389": "미확보 기계기사 — 초급기술자(기계·설비) 옛이름으로 옮김", "LB000388": "미확보 기계산업기사 — 초급숙련기술자(기계·설비) 옛이름으로 옮김", "MO000055": {"원문번호": "주택용저압:기본_200이하", "파일": "재료_유가전력.json"}, "MO000056": {"원문번호": "주택용저압:기본_201~400", "파일": "재료_유가전력.json"}, "MO000057": {"원문번호": "주택용저압:기본_400초과", "파일": "재료_유가전력.json"}, "MO000059": {"원문번호": "주택용저압:전력량_다음200", "파일": "재료_유가전력.json"}, "MO000060": {"원문번호": "주택용저압:전력량_400초과", "파일": "재료_유가전력.json"}, "MO000061": {"원문번호": "주택용저압:월간최저요금", "파일": "재료_유가전력.json"}, "MO000062": {"원문번호": "일반용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000063": {"원문번호": "일반용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000064": {"원문번호": "일반용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000065": {"원문번호": "일반용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000066": {"원문번호": "산업용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000067": {"원문번호": "산업용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000068": {"원문번호": "산업용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000069": {"원문번호": "산업용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000070": {"원문번호": "임시전력갑:기본요금적용", "파일": "재료_유가전력.json"}, "MO000071": {"원문번호": "임시전력갑:전력량요금적용", "파일": "재료_유가전력.json"}, "MO000072": {"원문번호": "임시전력갑:월간최저요금", "파일": "재료_유가전력.json"}, "GC001145": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001148": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001151": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001156": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001159": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001166": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "EQ000656": "자체 발전기 5.5㎾ — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000657": "자체 발전기 7㎾ — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000658": "자체 발전기 10㎾ — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000659": "자체 대형 브레이커 0.18㎥ — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000660": "자체 에어 호스 1.91㎝ — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000661": "자체 모노펌프 5-20㎥/hr — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000662": "자체 파워트로웰 3.73㎾ — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000663": "자체 콘크리트펌프용 파이프 150㎜×1m — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000664": "자체 콘크리트펌프용 파이프 150㎜×2m — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000665": "자체 콘크리트펌프용 파이프 150㎜×3m — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000666": "자체 콘크리트펌프용 파이프 곡관 150㎜ 45° — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000667": "자체 콘크리트펌프용 파이프 곡관 150㎜ 60° — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "EQ000668": "자체 콘크리트펌프용 파이프 곡관 150㎜ 90° — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번", "GC001195": "공통 8 파워트로웰 시간당 경비 — 품셈에 없는 기계 = 사용자 내 기계(요구서 796 · 821-4) · 결번"}, "키": { "CC000001": {"원문번호": "공통 1-3-1 콘크리트 및 포장용 재료", "파일": "계수_건설품셈_01장_적용기준.json"}, "CC000002": {"원문번호": "공통 1-3-1 노상 및 노반재료", "파일": "계수_건설품셈_01장_적용기준.json"}, @@ -1342,19 +1342,6 @@ "EQ000653": {"원문번호": "2-2-11 트럭탑재형크레인", "파일": "기계.json"}, "EQ000654": {"원문번호": "2-2-11 타이어형크레인", "파일": "기계.json"}, "EQ000655": {"원문번호": "10-9 덤프트럭(4륜구동) 1ton", "파일": "기계.json"}, - "EQ000656": {"원문번호": "자체 발전기 5.5㎾", "파일": "기계.json"}, - "EQ000657": {"원문번호": "자체 발전기 7㎾", "파일": "기계.json"}, - "EQ000658": {"원문번호": "자체 발전기 10㎾", "파일": "기계.json"}, - "EQ000659": {"원문번호": "자체 대형 브레이커 0.18㎥", "파일": "기계.json"}, - "EQ000660": {"원문번호": "자체 에어 호스 1.91㎝", "파일": "기계.json"}, - "EQ000661": {"원문번호": "자체 모노펌프 5-20㎥/hr", "파일": "기계.json"}, - "EQ000662": {"원문번호": "자체 파워트로웰 3.73㎾", "파일": "기계.json"}, - "EQ000663": {"원문번호": "자체 콘크리트펌프용 파이프 150㎜×1m", "파일": "기계.json"}, - "EQ000664": {"원문번호": "자체 콘크리트펌프용 파이프 150㎜×2m", "파일": "기계.json"}, - "EQ000665": {"원문번호": "자체 콘크리트펌프용 파이프 150㎜×3m", "파일": "기계.json"}, - "EQ000666": {"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 45°", "파일": "기계.json"}, - "EQ000667": {"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 60°", "파일": "기계.json"}, - "EQ000668": {"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 90°", "파일": "기계.json"}, "FX000001": {"원문번호": "0000001", "파일": "환율_한국은행환율.json"}, "FX000002": {"원문번호": "0000002", "파일": "환율_한국은행환율.json"}, "FX000003": {"원문번호": "0000003", "파일": "환율_한국은행환율.json"}, @@ -2548,7 +2535,6 @@ "GC001192": {"원문번호": "공통 1-5-4 건설폐기물 발생량", "파일": "로직_건설품셈_01장_적용기준.json"}, "GC001193": {"원문번호": "공통 1-5-6 공사용수", "파일": "로직_건설품셈_01장_적용기준.json"}, "GC001194": {"원문번호": "공통 3-2-1 잔토처리(인력)", "파일": "로직_건설품셈_03장_토공사.json"}, - "GC001195": {"원문번호": "공통 8 파워트로웰 시간당 경비", "파일": "로직_건설품셈_08장_건설기계.json"}, "GF000001": {"원문번호": "3-1 경계표시", "파일": "로직_산림품셈_03장_작업장_관리.json"}, "GF000002": {"원문번호": "3-3 작업로 설치", "파일": "로직_산림품셈_03장_작업장_관리.json"}, "GF000003": {"원문번호": "3-4-2 임산물 운반로 신설 작업량", "파일": "로직_산림품셈_03장_작업장_관리.json"}, diff --git a/resources/master_data/scripts/build_기계_소형보조_820_1.py b/resources/master_data/scripts/build_기계_소형보조_820_1.py deleted file mode 100644 index 27c0fcbed..000000000 --- a/resources/master_data/scripts/build_기계_소형보조_820_1.py +++ /dev/null @@ -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() diff --git a/resources/master_data/기계.json b/resources/master_data/기계.json index 37dce1745..0942ee3b3 100644 --- a/resources/master_data/기계.json +++ b/resources/master_data/기계.json @@ -39717,355 +39717,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) 또는 원화 갱신 필요" } ] } diff --git a/resources/master_data/로직_건설품셈_08장_건설기계.json b/resources/master_data/로직_건설품셈_08장_건설기계.json index 5bbd111d3..929d896f8 100644 --- a/resources/master_data/로직_건설품셈_08장_건설기계.json +++ b/resources/master_data/로직_건설품셈_08장_건설기계.json @@ -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", diff --git a/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json b/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json index 526d0e7f9..d637e805c 100644 --- a/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json +++ b/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json @@ -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}, - "끝전": "비목버림" - } } ] } diff --git a/resources/tester/test_820_1_small_machines.py b/resources/tester/test_820_1_small_machines.py deleted file mode 100644 index 5c5b01eae..000000000 --- a/resources/tester/test_820_1_small_machines.py +++ /dev/null @@ -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 diff --git a/resources/tester/test_821_4_import_my_machine.py b/resources/tester/test_821_4_import_my_machine.py new file mode 100644 index 000000000..f2ab13905 --- /dev/null +++ b/resources/tester/test_821_4_import_my_machine.py @@ -0,0 +1,216 @@ +# 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 — 같은 임시 층 + +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)] + + +@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 = 이제 품셈에 없음 +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() -> None: + """EST main 504 「콘크리트 바닥 마감(Power Trowel)」 = 원파일 저장 금액 546(파워트로웰 = 이제 못 이음 · 엑셀 값).""" + import test_810_2_native_doc as t + + out = fn.emit(_main(), 504) + _, _, got = t.doc_and_money(out) + assert float(got["계"]) == 546 diff --git a/resources/tester/test_est_global_vars.py b/resources/tester/test_est_global_vars.py new file mode 100644 index 000000000..77ab9c581 --- /dev/null +++ b/resources/tester/test_est_global_vars.py @@ -0,0 +1,45 @@ +"""EST 테스트 앱 단가산출 전역변수(원본 \x05 표지 — 호표를 넘어 값 유지, 지정·해제, 목록).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def test_global_variable_crosses_groups_and_toggle(): + code = r""" + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({ structuredClone }); + for (const f of ['native-math.js', 'native-formula.js', 'native-text.js', 'native-global-vars.js']) + vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, { filename: f }); + const T = vm.runInContext('EstText', ctx), G = vm.runInContext('EstGlobalVars', ctx); + const rows = [ + { Index: 1, Code: 'D1', LineData: '호표1' }, + { Index: 0, SCheck: 0, LineData: '\x05Q\x05 = 2 × 3 = \x126\x12' }, + { Index: 1, Code: 'D2', LineData: '호표2' }, + { Index: 0, SCheck: -1, LineData: '노무 = Q × 10 = \x120\x12' }, + { Index: 1, Code: 'D3', LineData: '호표3' }, + { Index: 0, SCheck: 0, LineData: 'R = 1 = \x121\x12' }, + { Index: 1, Code: 'D4', LineData: '호표4' }, + { Index: 0, SCheck: -1, LineData: '노무 = R × 10 = \x120\x12' }, + ]; + const r = T.calculate(rows, {}); + let bad = ''; try { G.toggle('가 = 3', 0); } catch (e) { bad = e.message.slice(0, 12); } + process.stdout.write(JSON.stringify({ + used: r.rows[3].LineData.match(/\x12([^\x12]+)\x12/)[1], kept: r.rows[1].LineData.startsWith('\x05Q\x05'), + local: r.errors.length, globals: r.globals, + on: G.toggle('Q = 2', 0), off: G.toggle('⟦전역⟧Q⟦전역⟧ = 2', 5), bad, + clean: T.clean('\x05Q\x05 = 1'), shown: T.display('\x05V1\x05'), back: T.encode('⟦전역⟧V1⟦전역⟧') === '\x05V1\x05', + })); + """ + out = json.loads(subprocess.check_output(["node", "-e", code], cwd=ROOT).decode("utf-8")) + assert out["used"] == "60" + assert out["kept"] is True + assert out["local"] == 1 # R 은 전역이 아니라 D4 에서 못 찾음 + assert out["globals"] == [{"name": "Q", "value": 6, "index": 1}] + assert out["on"] == "⟦전역⟧Q⟦전역⟧ = 2" + assert out["off"] == "Q = 2" + assert out["bad"] == "선택한 변수는 전역변수" + assert out["clean"] == "Q = 1" + assert out["shown"] == "⟦전역⟧V1⟦전역⟧" and out["back"] is True diff --git a/resources/tester/test_est_jdl_compare.py b/resources/tester/test_est_jdl_compare.py new file mode 100644 index 000000000..df53faf60 --- /dev/null +++ b/resources/tester/test_est_jdl_compare.py @@ -0,0 +1,32 @@ +"""EST 테스트 앱 JDLExcel과 단가비교(원본 ActionJDLCompare — Excel 코드 칸이 코드를 품으면 같은 행 합계와 TC 비교).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def test_compare_contains_code_limit_and_xls_tables(): + code = r""" + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({}); + vm.runInContext(fs.readFileSync('tools/est-test/native-jdl-compare.js', 'utf8'), ctx); + const J = vm.runInContext('EstJdlCompare', ctx); + const rows = [{ Code: 'U001', Name: '가', TC: 1000 }, { Code: 'U002', Name: '나', TC: 500 }, + { Code: 'U003', Name: '다', TC: 7 }, { Code: '', Name: '머리' }]; + const table = [['코드', '', '합계'], ['[U001] 가', '', '1,000'], ['U002', '', 499]]; + const out = J.compare(rows, table, 0, 2).map((r) => [r.Code, r.OTC, r.GN, r.Checked]); + const far = Array.from({ length: 60 }, () => ['x']); far.push(['U009', '', 5]); + const limit = J.compare([{ Code: 'U009', TC: 5 }], far, 0, 2)[0].OTC; + const xls = J.xlsTables([{ name: 'S1', cells: { '0,0': { value: 'U001' }, '0,7': { value: 1000 }, '2,1': { value: 'b' } } }]); + process.stdout.write(JSON.stringify({ out, limit, xls: xls.S1 })); + """ + out = json.loads(subprocess.check_output(["node", "-e", code], cwd=ROOT).decode("utf-8")) + assert out["out"] == [ + ["U001", 1000, 0, False], + ["U002", 499, 1, True], + ["U003", None, None, True], + ] + assert out["limit"] is None + assert out["xls"][0][0] == "U001" and out["xls"][0][7] == 1000 and out["xls"][2][1] == "b" diff --git a/resources/tester/test_est_multi_search.py b/resources/tester/test_est_multi_search.py index 69928c982..149df1a1a 100644 --- a/resources/tester/test_est_multi_search.py +++ b/resources/tester/test_est_multi_search.py @@ -34,3 +34,22 @@ def test_search_finds_words_in_selected_kinds(tmp_path): assert result["none"]["total"] == 0 assert result["excluded"]["total"] == 0 assert result["err"] == ["검색어를", "검색할 "] + + +def test_search_price_range_and_main_data_only(tmp_path): + sample = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) + (tmp_path / ("a" * 24 + ".json")).write_text(json.dumps(sample), encoding="utf-8") + (tmp_path / ("c" * 24 + ".json")).write_text(json.dumps(sample), encoding="utf-8") + script = """ + const s = require('./tools/est-test/search-store.cjs'), dir = process.argv[1]; + const range = s.search(dir, '1~999999999', ['5']); + const exact = s.search(dir, String(range.hits[0].price), ['5']); + const only = s.search(dir, '1~999999999', ['5'], '', ['c'.repeat(24)]); + process.stdout.write(JSON.stringify({ range: range.hits.map((h) => h.price), exact: exact.total, only: only.hits.map((h) => h.id), total: range.total })); + """ + out = json.loads( + subprocess.check_output(["node", "-e", script, str(tmp_path)], cwd=ROOT).decode("utf-8") + ) + assert out["range"] and all(1 <= p <= 999999999 for p in out["range"]) + assert out["exact"] >= 2 + assert set(out["only"]) == {"c" * 24} and len(out["only"]) == out["total"] // 2 diff --git a/resources/tester/test_est_row_links.py b/resources/tester/test_est_row_links.py new file mode 100644 index 000000000..15fb0d47c --- /dev/null +++ b/resources/tester/test_est_row_links.py @@ -0,0 +1,53 @@ +"""EST 테스트 앱 줄 연결 도구(원본 비목별 단가 불러오기 · SUM 색인지정 · 하위데이타 연결/끊기).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def run_js(code: str): + runner = f""" + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({{ structuredClone }}); + for (const f of ['native-autolink.js', 'native-row-links.js']) + vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }}); + process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx))); + """ + return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8")) + + +def test_cost_part_sum_index_and_toggle(): + out = run_js( + r"""(() => { + const L = EstRowLinks, p = { tables: { + CostTitleDat: [{ Code: 'D1', Name: '거푸집', Size: '합판', Unite: 'm2', TC: 60, NC: 40, GC: 15, JC: 5, SC: 0 }], + StuffcDat: [{ Code: 'M1', Name: '시멘트', Size: '40kg', Unite: '포', TC: 5000, GC: 5000 }], + LaborcDat: [], HeadTitleDat: [], MachTitleDat: [], SilJukDat: [], EstiMateDat: [] } }; + const r = { LinkIndex: 104, Code: 'D1', NU: 1, GU: 1, JU: 1, SU: 1, TU: 9 }; + L.costPart(p, r, 3); + const jc = [r.NU, r.GU, r.JU, r.SU, r.OCheck, r.TU]; + L.costPart(p, r, 0); + const all = [r.NU, r.GU, r.JU, r.OCheck]; + let refused = ''; try { L.costPart(p, { LinkIndex: 5 }, 0); } catch (e) { refused = e.message; } + const est = { Code: '', Name: '시멘트', Size: '40kg', Unite: '포', LinkIndex: 0 }; + const linked = [L.toggle(p, 'EstMainDat', est), est.Code, est.LinkIndex, est.GU, est.TU]; + const unlinked = [L.toggle(p, 'EstMainDat', est), est.LinkIndex, est.Code]; + let none = ''; try { L.toggle(p, 'EstMainDat', { Name: '없음', LinkIndex: 0 }); } catch (e) { none = e.message; } + const skip = L.toggle(p, 'EstMainDat', { LinkIndex: 54 }); + const s = { LinkIndex: 0, Index: 0 }; L.sumIndex('EstMainDat', s, 3); + const bad = []; for (const row of [{ Level: 1, GN: 0 }, { LinkIndex: 53 }, { Index: 1 }]) + try { L.sumIndex('EstMainDat', row, 1); } catch (e) { bad.push(e.message.slice(0, 6)); } + return { jc, all, refused, linked, unlinked, none, skip, skind: s.SKind, bad }; + })()""" + ) + assert out["jc"] == [0, 0, 5, 0, 3, 9] + assert out["all"] == [40, 15, 5, 0] + assert out["refused"] == "노임,자재,표준시장단가는 적용할수 없습니다." + assert out["linked"] == ["link", "M1", 5, 5000, 5000] + assert out["unlinked"] == ["unlink", 0, "M1"] + assert out["none"] == "해당 하위data를 찾지못해 연결시키지 못했습니다." + assert out["skip"] == "skip" + assert out["skind"] == 3 + assert out["bad"] == ["레벨지정한 ", "현재라인의 ", "현재라인의 "] diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 5f8404ec5..02631c567 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -489,6 +489,23 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일). - [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지. - 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구. + - [x] 수량산출서 삽입·선택공종 삭제(원본 TFRMMULLANG btnInsert·btnSelectDel) — 「삽입」: 현재 산출서 앞에 공종명칭·규격을 물어 새 산출서, 「선택공종 삭제」: 고른 여러 산출서를 한 번에 지움. + - 검증완료: 산출서·바인딩 시험 7개 통과. ORCA 브라우저에서 둘째 위치에 「끼운 공종/1m」 삽입, 1·3번째 고른 삭제 → 「끼운 공종」만 남음, 샘플 복구. + - [x] 새내역서 파일로저장(원본 mnuSaveNewEst 0x113384c) — 현재 화면은 그대로, 표 12개의 「_」 당초값 칸 비움·신규 표시 끔·원가 안전관리비 특례 줄(CalcIndex 4)→1·당초 사본 뺀 새 작업을 이름 붙여 서버에 따로 저장하고 내려받기. 재계산 없음. + - 검증완료: 바인딩 시험 통과. ORCA 브라우저에서 GN_ 5·신규 표시·당초 사본 둔 샘플 → 새 작업에는 GN_ 없음·신규 끔·당초 사본 없음, 현재 화면은 GN_ 5 그대로, 시험 작업 지우고 샘플 복구. + - [x] 단가산출 전역변수(원본 mnuCostSetGlobalVar 0xf053cc · mnuCostGlobalvarList · 전역표 0x93c22c — spec_cost_tools §2) — LineData 의 이름(화면 ⟦전역⟧) 로 대입한 변수는 호표가 바뀌어도 남아 다른 호표에서 표지 없이 써도 찾음(그 호표 변수 → 전역표 차례). 「전역변수 지정,해제」: 마지막 산출근거 칸 커서 아래 낱말(영문자 포함 [0-9A-Za-z_])을 감싸거나 풂(원본 거부 문구). 「전역변수 목록」: 처음부터 계산한 대입마다 변수명·값·이동. 원본처럼 전역표는 저장하지 않고 계산 때마다 다시 만듦. + - 검증완료: `test_est_global_vars.py`(다른 호표 Q×10 = 60, 표지 유지, 비전역 R 은 못 찾음, 지정·해제·거부, 표시·되돌림) 및 REA 대조 39개 통과. ORCA 브라우저에서 샘플 단가산출 q 줄 지정 → 목록 「q 0.7」·이동으로 그 줄 선택, 도급액 불변, 샘플 복구. + - [x] JDLExcel과 단가비교(원본 ActionJDLCompare, 숨은 Ctrl+Alt+J → TfrmJDLExcelCompare) — 일위대가·단가산출·기계경비 탭, 시트번호·코드칼럼(A)·합계셀칼럼(H), Excel(.xlsx·.xls — `/api/read-xls`) 1행~(호표 수+50)행에서 코드를 품은 칸의 같은 행 합계와 TC 비교, 차이·없음 표시. 보기 전용. 웹은 메뉴 단추로 엶. + - 검증완료: `test_est_jdl_compare.py`(같음·차이 1·없음·+50행 한계·xls 셀 변환) 통과. ORCA 브라우저에서 일위대가 8개 중 같음 1·차이 10·없음 6 표시. + - 제외: TfrmMultiEditor(인쇄 부품의 설계용 편집기 — 실행 파일에 클래스 없음), Est Plus(구버전 .est) 단가비교·내보내기(숨은 단축키, 394바이트 레코드 형식 — 구버전 불러오기와 함께 남음). + - [x] 검색 결과 근거자료보기(원본 TfrmFoundDetail btnClose 0xefa7e8) — 통합 검색 결과의 일위·단산·기계 줄 「근거」: 그 작업의 호표 세부 줄(단산은 산출근거 글)과 총액·노무·재료·경비, 「적용」은 그 결과를 고르고 검색 창으로(가져오기는 검색 창에서). 고른 결과는 창을 오가도 유지. + - 검증완료: 바인딩·통합 검색 시험 통과. ORCA 브라우저에서 「거푸집」 일위 → 「[합판거푸집]의 근거자료보기」 총액 53,029·노무 38,289·재료 14,740·세부 6줄, 적용 → 그 결과 체크. + - [x] ES 정보(원본 mnuESInfo → TfrmESInfo 0x10b838c) — 기준시점·비교시점·최종기성일·선급금지급일(기본 오늘)·선급금지급액을 `project.esInfo` 에 기록(원본 [EscInfo] SDATE·CDATE·ACDATE·PreDATE·PreCost). 원본도 ES 제출 파일 작성에만 씀 — 웹 물가변동 계산과 잇지 않음, ESTX 에는 남지 않음. + - 검증완료: 바인딩 시험 통과. ORCA 브라우저에서 기본 오늘 · 기준시점 2025-01-15·선급금 1,000,000 저장 → 다시 열면 그대로, 지워 복구. + - [x] 설계참고철(Main Data)과 단가 범위 검색(원본 TfrmMainTableList 「MAIN DATA(설계참고철) 추가 및 삭제」 · TfrmFolderSerch 「Main Data에서 검색」·범위 검색 0xefefb0) — 통합 검색 「설계참고철 관리」에서 저장된 작업 중 최대 10개 지정(양식 보관함 종류 options), 검색위치 「설계참고철」이면 그 작업만, 검색어 「a~b」·숫자 하나는 단가 범위·같음. 원본의 검색중지는 서버 검색이 바로 끝나 두지 않음. + - 검증완료: `test_est_multi_search.py` 2개(범위·같음·지정 작업만) 통과. ORCA 브라우저에서 2개 지정 → 「1000~2000」 24건 모두 지정 작업·범위 안, 목록 지워 복구. + - [x] 줄 연결 도구(원본 TfrmHead popupCostNC·GC·JC·All 0xf1b54c · popMainSumIndex · mnuDataLink 0xf00e6c — 디스어셈블 근거) — 비목별 단가 불러오기(일위대가 줄: 하위 견적·일위·단산·기계에서 고른 비목 단가만 남기고 나머지 0, 기준비목 지정, 노임·자재·표준 거부), SUM 색인지정(해제·A~Z → 구간 SKind, 레벨·소계·도급액·조달수수료·호표머리 줄 거부), 하위데이타 연결/끊기(연결 줄은 끊고 값 유지, 끊긴 줄은 일위→단산→기계→표준→견적→자재→노임 차례로 코드 또는 품명·규격·단위 맞춤으로 연결, 자재는 재료단가·노임은 노무단가 = 원본 TC). 원본은 초점 한 줄, 웹은 고른 줄 모두. + - 검증완료: `test_est_row_links.py`(경비만 5·전체 40/15/5, 거부, 자재 연결 M1·5000·끊기, 못 찾음 문구, 색인 3·거부 3종) 통과. ORCA 브라우저에서 일위대가 합판거푸집 노무비만 → 0, 재료비만 → 재료단가 34,666·금액 13,173, 내역 일위 연결 줄 끊기 → 다시 연결 103·도급액 불변, SUM 색인 B, 샘플 복구. - [x] 묶음 인쇄 간지 설정값 일괄적용·기본양식에저장·전체선택/선택해제(원본 TFRMESTPRINTPRIVATE btnGangiAllSet·btnSaveToMain, TFRMESTPRINT2 btnNoselect) — 간지 인쇄·간지 겹테두리를 모든 서식에 같게, 고른 서식·간지 설정을 「묶음기본양식」(양식 보관함 종류 print)으로 저장해 묶음 설정 없는 공사에서 열 때 씀. - 검증완료: 묶음 인쇄·바인딩 시험 통과. ORCA 브라우저에서 선택해제 0 · 전체선택 40, 겹테두리 끔 → 미리보기 간지 테두리 없음, 기본양식 저장 뒤 설정 지우고 다시 열면 1개·겹테두리 끔 복원, 양식 지우고 샘플 복구. - [x] 갑지 공사금액 자동변경(원본 TFRMGABGI popupCostChange) — 「공사금액 자동변경」 체크 시 고른 원가 비목·방식을 기억해 인쇄 때마다 현재 원가 금액으로 공사금액 글을 다시 만듦(다시 열면 고른 비목·방식 그대로). diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 104e034f8..64716fc48 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -60,6 +60,11 @@ + + + + + @@ -93,6 +98,7 @@

+ >
@@ -241,6 +252,8 @@ > diff --git a/tools/est-test/native-business-actions.js b/tools/est-test/native-business-actions.js index 3b3a696b1..c0f860ad5 100644 --- a/tools/est-test/native-business-actions.js +++ b/tools/est-test/native-business-actions.js @@ -612,7 +612,7 @@ const EstBusinessActions = (() => { (typeof EstCleanup !== "undefined" && EstCleanup.handle(name, state)) || (typeof EstNameReg !== "undefined" && EstNameReg.handle(name, state)) || (typeof EstTidy !== "undefined" && EstTidy.handle(name, state)) || - (typeof EstMultiSearch !== "undefined" && EstMultiSearch.handle(name, state)) || + (typeof EstMultiSearch !== "undefined" && EstMultiSearch.handle(name, state, node)) || (typeof EstDocs !== "undefined" && EstDocs.handle(name, state, node)) || (typeof EstWongaTools !== "undefined" && EstWongaTools.handle(name, state)) || (typeof EstEstimatePrint !== "undefined" && EstEstimatePrint.handle(name, state)) || @@ -631,7 +631,12 @@ const EstBusinessActions = (() => { (typeof EstDocForms !== "undefined" && EstDocForms.handle(name, state, node)) || (typeof EstBlockFile !== "undefined" && EstBlockFile.handle(name, state)) || (typeof EstDisplay !== "undefined" && EstDisplay.handle(name, state)) || - (typeof EstCellClip !== "undefined" && EstCellClip.handle(name, state)) + (typeof EstCellClip !== "undefined" && EstCellClip.handle(name, state)) || + (typeof EstRowLinks !== "undefined" && EstRowLinks.handle(name, state)) || + (typeof EstEsInfo !== "undefined" && EstEsInfo.handle(name, state)) || + (typeof EstJdlCompare !== "undefined" && EstJdlCompare.handle(name, state, node)) || + (typeof EstGlobalVars !== "undefined" && EstGlobalVars.handle(name, state, node)) || + (typeof EstNewEstimate !== "undefined" && EstNewEstimate.handle(name, state)) ) { EstApp.render(); } else if (name === "insert-annual") { diff --git a/tools/est-test/native-es-info.js b/tools/est-test/native-es-info.js new file mode 100644 index 000000000..3ac1e0a80 --- /dev/null +++ b/tools/est-test/native-es-info.js @@ -0,0 +1,30 @@ +// 원본 「ES 정보」(ES도구 mnuESInfo → TfrmESInfo 0x10b838c) — 조달청 물가변동(ES) 제출용 기준시점·비교시점·최종기성일· +// 선급금지급일·선급금지급액. 원본은 작업 폴더 ProjectDataInfo.Ini [EscInfo] SDATE·CDATE·ACDATE·PreDATE·PreCost 에 두고 +// 날짜 기본값은 오늘. 웹은 project.esInfo 에 둔다(물가변동 계산과는 직접 잇지 않음 — 원본도 ES 제출 파일 작성에만 씀). +const EstEsInfo = (() => { + const today = () => new Date().toISOString().slice(0, 10); + function dialog(state) { + const e = state.project.esInfo || {}; + EstActions.form( + "ES 정보", + [ + { name: "SDATE", label: "기준시점", type: "date", value: e.SDATE || today() }, + { name: "CDATE", label: "비교시점", type: "date", value: e.CDATE || today() }, + { name: "ACDATE", label: "최종기성일", type: "date", value: e.ACDATE || today() }, + { name: "PreDATE", label: "선급금지급일", type: "date", value: e.PreDATE || today() }, + { name: "PreCost", label: "선급금지급액(원)", type: "number", value: e.PreCost ?? 0 }, + ], + (values) => { + if (!Number.isFinite(values.PreCost) || values.PreCost < 0) + throw Error("선급금지급액은 0 이상 수치입니다."); + EstApp.change((project) => (project.esInfo = { ...values }), false); + }, + ); + } + function handle(name, state) { + if (name !== "es-info") return false; + dialog(state); + return true; + } + return { handle }; +})(); diff --git a/tools/est-test/native-excel-map.js b/tools/est-test/native-excel-map.js index 8ef890712..1d0713238 100644 --- a/tools/est-test/native-excel-map.js +++ b/tools/est-test/native-excel-map.js @@ -299,5 +299,16 @@ const EstExcelMap = (() => { return true; } - return { fields, columnIndex, columnName, records, rows, insert, load, shift, handle }; + return { + fields, + columnIndex, + columnName, + records, + rows, + insert, + load, + shift, + sheetTables, + handle, + }; })(); diff --git a/tools/est-test/native-global-vars.js b/tools/est-test/native-global-vars.js new file mode 100644 index 000000000..66f134bf8 --- /dev/null +++ b/tools/est-test/native-global-vars.js @@ -0,0 +1,70 @@ +// 원본 단가산출 「전역변수 지정,해제」(mnuCostSetGlobalVar 0xf053cc)·「전역변수 목록」(mnuCostGlobalvarList → TfrmGlobalVarList). +// 지정·해제: 마지막으로 누른 산출근거 칸의 커서 아래 낱말([0-9A-Za-z_], 영문자 하나 이상)을 ⟦전역⟧ 표지로 감싸거나 푼다. +// 목록: 단가산출을 처음부터 계산해 전역변수 대입마다 변수명·값과 그 줄로 이동. +const EstGlobalVars = (() => { + const MARK = "⟦전역⟧"; + let last = null; + if (typeof document !== "undefined" && document.addEventListener) + document.addEventListener("focusin", (event) => { + const t = event.target.closest?.('[data-table="CostMainDat"][data-field="LineData"]'); + if (t) last = t; + }); + + function toggle(text, caret) { + const word = /[0-9A-Za-z_]/; + let a = caret, + b = caret; + while (a > 0 && word.test(text[a - 1])) a--; + while (b < text.length && word.test(text[b])) b++; + const name = text.slice(a, b); + if (!name || !/[A-Za-z]/.test(name)) + throw Error( + '선택한 변수는 전역변수로 지정할수 없습니다.\n\n변수는 알파벳과 숫자,"_"로 이루어져야합니다.\n\n예) Q , V1 , L_1', + ); + const before = text.slice(0, a), + after = text.slice(b); + if (before.endsWith(MARK) && after.startsWith(MARK)) + return before.slice(0, -MARK.length) + name + after.slice(MARK.length); + return `${before}${MARK}${name}${MARK}${after}`; + } + + function list(state) { + const out = EstText.calculate(state.project.tables.CostMainDat || [], state.project.options); + const { esc, number } = EstApp; + EstApp.showDialog( + "전역변수 목록", + out.globals.length + ? `${out.globals.map((g) => ``).join("")}
변수명값이동
${esc(g.name)}${number(g.value)}
` + : "

전역변수가 없습니다. 단가산출 산출근거의 변수에 커서를 두고 「전역변수 지정,해제」를 누르세요.

", + ); + } + + function handle(name, state, node) { + if (name === "global-var") { + if (!last) throw Error("단가산출 산출근거 칸의 변수에 커서를 두세요."); + const index = Number(last.dataset.row), + text = last.value, + next = toggle(text, last.selectionStart ?? 0); + EstApp.change( + (project) => (project.tables.CostMainDat[index].LineData = EstText.encode(next)), + ); + } else if (name === "global-var-list") list(state); + else if (name === "global-var-go") { + EstApp.byId("dialog").close(); + const index = Number(node.dataset.row); + state.view = "CostMainDat"; + const rows = state.project.tables.CostMainDat; + let head = index; + while (head > 0 && !(Number(rows[head].Index) > 0)) head--; + state.group = rows[head]?.Code || state.group; + state.selection.clear(); + state.selection.add(index); + EstApp.render(); + document + .querySelector(`[data-table="CostMainDat"][data-row="${index}"][data-field="LineData"]`) + ?.scrollIntoView({ block: "center" }); + } else return false; + return true; + } + return { toggle, handle }; +})(); diff --git a/tools/est-test/native-jdl-compare.js b/tools/est-test/native-jdl-compare.js new file mode 100644 index 000000000..c809143e0 --- /dev/null +++ b/tools/est-test/native-jdl-compare.js @@ -0,0 +1,124 @@ +// 원본 「JDLExcel과 단가비교」(ActionJDLCompare, 숨은 단축키 Ctrl+Alt+J → TfrmJDLExcelCompare) — 일위대가·단가산출·기계경비 호표마다 +// Excel 의 코드 열에서 그 코드를 품은 셀(Pos(코드, 셀) > 0)을 1행~(호표 수+50)행 안에서 찾아 같은 행 합계 열 값과 TC 를 비교한다. +// 차이(|TC − Excel|)가 있으면 표시, 못 찾으면 「없음」. 자료는 바꾸지 않는다(보기 전용). Excel 은 .xlsx·.xls 모두. +const EstJdlCompare = (() => { + const TABLES = [ + ["HeadTitleDat", "일위대가"], + ["CostTitleDat", "단가산출"], + ["MachTitleDat", "기계경비"], + ]; + function compare(rows, table, codeCol, totalCol) { + const limit = rows.length + 50; + return rows + .filter((r) => String(r.Code ?? "").trim()) + .map((r) => { + const code = String(r.Code).trim(); + let found = null; + for (let i = 0; i < Math.min(limit, table.length); i++) { + const cell = table[i]?.[codeCol]; + if (cell != null && String(cell).includes(code)) { + found = Number(String(table[i][totalCol] ?? "").replace(/,/g, "")) || 0; + break; + } + } + const tc = Number(r.TC) || 0; + return { + Code: code, + Name: r.Name ?? "", + Size: r.Size ?? "", + Unite: r.Unite ?? "", + TC: tc, + OTC: found, + GN: found == null ? null : Math.abs(tc - found), + Checked: found == null || Math.abs(tc - found) > 1e-9, + }; + }); + } + // .xls(BIFF) 셀 → 행 배열 + function xlsTables(sheets) { + const out = {}; + for (const s of sheets) { + const table = []; + for (const [k, c] of Object.entries(s.cells)) { + const [r, col] = k.split(",").map(Number); + (table[r] ||= [])[col] = c.value; + } + out[s.name] = table; + } + return out; + } + + function form(state) { + const c = (state.jdlCompare ||= { + tab: 0, + sheet: 1, + code: "A", + total: "H", + tables: null, + result: [], + }), + { esc, number } = EstApp, + diff = c.result.filter((r) => r.Checked).length; + EstApp.showDialog( + "JDL엑셀 단가 비교", + `
${TABLES.map(([, l], i) => ``).join("")}
${c.tables ? `시트 ${Object.keys(c.tables).length}개` : "Excel 을 여세요"} · 다른 금액·없음 ${diff}건
${c.result.map((r) => ``).join("")}
코드품명규격단위EST 금액Excel 금액차이
${esc(r.Code)}${esc(r.Name)}${esc(r.Size)}${esc(r.Unite)}${number(r.TC)}${r.OTC == null ? "없음" : number(r.OTC)}${r.GN == null ? "" : number(r.GN)}
`, + ); + } + function read(c) { + const v = (n) => document.querySelector(`#dialog-content [name="${n}"]`)?.value; + c.sheet = Math.max(1, Number(v("jc-sheet")) || 1); + c.code = String(v("jc-code") || "A").toUpperCase(); + c.total = String(v("jc-total") || "H").toUpperCase(); + } + async function run(name, state, node) { + if (name === "jdl-compare") return form(state); + const c = state.jdlCompare; + read(c); + if (name === "jdl-compare-tab") { + c.tab = Number(node.dataset.i); + c.result = []; + } else if (name === "jdl-compare-open") { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".xlsx,.xls"; + input.addEventListener("change", async () => { + try { + const file = input.files[0]; + if (!file) return; + const body = await file.arrayBuffer(); + c.tables = /\.xls$/i.test(file.name) + ? xlsTables(await EstApp.api("/api/read-xls", { method: "POST", body })) + : EstExcelMap.sheetTables( + await EstApp.api("/api/import-xlsx", { method: "POST", body }), + ); + } catch (error) { + state.notice = error.message; + } + form(state); + }); + return input.click(); + } else if (name === "jdl-compare-run") { + if (!c.tables) throw Error("엑셀파일을 먼저 여세요."); + const table = Object.values(c.tables)[c.sheet - 1]; + if (!table) throw Error(`시트 ${c.sheet} 이 없습니다.`); + const col = (x) => EstExcelMap.columnIndex(x); + c.result = compare( + state.project.tables[TABLES[c.tab][0]] || [], + table, + col(c.code), + col(c.total), + ); + } + form(state); + } + const names = new Set(["jdl-compare", "jdl-compare-tab", "jdl-compare-open", "jdl-compare-run"]); + function handle(name, state, node) { + if (!names.has(name)) return false; + run(name, state, node).catch((error) => { + state.notice = error.message; + EstApp.render(); + }); + return true; + } + return { compare, xlsTables, handle }; +})(); diff --git a/tools/est-test/native-multi-search.js b/tools/est-test/native-multi-search.js index 0825a5727..c98b7416b 100644 --- a/tools/est-test/native-multi-search.js +++ b/tools/est-test/native-multi-search.js @@ -18,16 +18,25 @@ const EstMultiSearch = (() => { const rows = saved.hits .map( (h, i) => - `${esc(h.project)}${esc(h.label)}${esc(h.Code)}${esc(h.Name)}${esc(h.Size)}${esc(h.Unite)}${EstApp.number(h.price)}`, + `${[103, 104, 105].includes(h.kind) ? `` : ""}${esc(h.project)}${esc(h.label)}${esc(h.Code)}${esc(h.Name)}${esc(h.Size)}${esc(h.Unite)}${EstApp.number(h.price)}`, ) .join(""); EstApp.showDialog( "통합 검색", - `

검색위치: 이 앱에 저장된 다른 작업

${kinds.map(([k, label]) => ``).join("")}
${rows}
작업구분코드품명규격단위단가

찾은 자료 ${saved.total}건${saved.total > saved.hits.length ? ` 중 앞 ${saved.hits.length}건` : ""}

`, + `

검색위치: · 검색어에 「10000~20000」처럼 쓰면 단가 범위

${kinds.map(([k, label]) => ``).join("")}
${rows}
작업구분코드품명규격단위단가

찾은 자료 ${saved.total}건${saved.total > saved.hits.length ? ` 중 앞 ${saved.hits.length}건` : ""}

`, ); } - async function run(name, state) { + async function run(name, state, node) { + if (name === "multi-search-detail") return detail(state, Number(node.dataset.i)); + if (name === "multi-search-pick") + (state.multiSearch.picked ||= new Set()).add(Number(node.dataset.i)); + if (name === "multi-search-pick" || name === "multi-search-back") return form(state); + if (name === "main-list") return mainForm(state); + if (name === "main-list-save") { + await mainSave(); + return form(state); + } if (name === "multi-search") form(state); else if (name === "multi-search-run") { const saved = state.multiSearch; @@ -35,9 +44,17 @@ const EstMultiSearch = (() => { saved.kinds = [...document.querySelectorAll('#dialog-content [name="ms-kind"]:checked')].map( (n) => Number(n.value), ); + saved.where = document.querySelector('#dialog-content [name="ms-where"]:checked')?.value; const params = new URLSearchParams({ q: saved.query, kinds: saved.kinds.join(",") }); + if (saved.where === "main") { + const ids = await mainList(); + if (!ids.length) + throw Error("설계참고철에 등록한 작업이 없습니다. 설계참고철 관리에서 추가하세요."); + params.set("only", ids.join(",")); + } if (state.project.id) params.set("exclude", state.project.id); Object.assign(saved, await EstApp.api(`/api/search?${params}`)); + saved.picked = new Set(); form(state); } else if (name === "multi-search-take") { const picked = [...document.querySelectorAll('#dialog-content [name="ms-hit"]:checked')].map( @@ -65,10 +82,78 @@ const EstMultiSearch = (() => { EstApp.render(); } } - const names = new Set(["multi-search", "multi-search-run", "multi-search-take"]); - function handle(name, state) { + // 설계참고철(TfrmMainTableList 「MAIN DATA 추가 및 삭제」): 저장된 작업 중 최대 10개 — 양식 보관함(종류 options, 이름 「설계참고철」) + const MAIN = "설계참고철"; + async function mainList() { + return ( + (await EstApp.api("/api/forms")).find((f) => f.kind === "options" && f.name === MAIN)?.data + ?.ids || [] + ); + } + async function mainForm(state) { + const ids = await mainList(), + projects = await EstApp.api("/api/projects"), + { esc } = EstApp; + EstApp.showDialog( + "MAIN DATA(설계참고철) 추가 및 삭제", + `

통합 검색 「설계참고철」에서 찾을 작업을 고르세요(최대 10개).

${projects.map((p) => ``).join("")}
`, + ); + } + async function mainSave() { + const ids = [...document.querySelectorAll('#dialog-content [name="ml-id"]:checked')].map( + (n) => n.value, + ); + if (ids.length > 10) throw Error("Main Data는 10개 이상 추가할수없습니다."); + await EstApp.api("/api/forms", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ kind: "options", name: MAIN, data: { ids } }), + }); + } + // 근거자료보기(TfrmFoundDetail): 찾은 일위·단산·기계 호표의 세부 줄과 총액·노무·재료·경비, 「적용」 = 그 결과를 고름 + const BODY = { 103: "HeadMainDat", 104: "CostMainDat", 105: "MachMainDat" }; + async function detail(state, i) { + remember(state); + const hit = state.multiSearch.hits[i], + project = await EstApp.api(`/api/projects/${hit.id}`), + { esc, number } = EstApp, + rows = project.tables[BODY[hit.kind]] || [], + group = EstNative.groups(rows).get(hit.Code); + if (!group) throw Error("근거 자료를 찾지 못했습니다."); + const head = group.rows[0], + body = group.rows + .slice(1) + .map((r) => + hit.kind === 104 + ? `${esc(EstText.clean(r.LineData || ""))}` + : `${esc(r.Code)}${esc(r.Name)}${esc(r.Size)}${esc(r.Unite)}${number(r.GN)}`, + ) + .join(""); + EstApp.showDialog( + `[${hit.Name}]의 근거자료보기`, + `

총 액 : ${number(head.TC)} · 노무비 : ${number(head.NC)} · 재료비 : ${number(head.GC)} · 경 비 : ${number(head.JC)}

${body}
`, + ); + } + function remember(state) { + const boxes = document.querySelectorAll('#dialog-content [name="ms-hit"]'); + if (boxes.length) + state.multiSearch.picked = new Set( + [...boxes].filter((n) => n.checked).map((n) => Number(n.value)), + ); + } + const names = new Set([ + "multi-search", + "multi-search-run", + "multi-search-take", + "main-list", + "main-list-save", + "multi-search-detail", + "multi-search-pick", + "multi-search-back", + ]); + function handle(name, state, node) { if (!names.has(name)) return false; - run(name, state).catch((error) => { + run(name, state, node).catch((error) => { state.notice = error.message; EstApp.render(); }); diff --git a/tools/est-test/native-new-estimate.js b/tools/est-test/native-new-estimate.js new file mode 100644 index 000000000..e7d5c9d9b --- /dev/null +++ b/tools/est-test/native-new-estimate.js @@ -0,0 +1,54 @@ +// 원본 「새내역서 파일로저장」(mnuSaveNewEst 0x113384c) — 현재 문서는 그대로 두고 설계변경 흔적을 지운 새 작업을 따로 저장. +// 표 12개(노임·자재·견적·표준·기계기준·기계 본문/호표·단산 본문/호표·일위 본문/호표·내역서)의 「_」로 끝나는 칸(당초값)을 비우고 +// 신규 표시(NewItem)를 끄며, 원가 안전관리비 특례 줄(CalcIndex 4)은 1 로, 당초 사본(baseline)은 뺀다. 재계산 없음. +const EstNewEstimate = (() => { + const TABLES = [ + "LaborcDat", + "StuffcDat", + "EstiMateDat", + "SilJukDat", + "MachGijunDat", + "MachMainDat", + "MachTitleDat", + "CostMainDat", + "CostTitleDat", + "HeadMainDat", + "HeadTitleDat", + "EstMainDat", + ]; + function build(project, name) { + const next = structuredClone(project); + delete next.id; + delete next.baseline; + next.name = name; + for (const table of TABLES) + for (const row of next.tables[table] || []) { + for (const key of Object.keys(row)) if (key.endsWith("_")) row[key] = null; + if ("NewItem" in row) row.NewItem = false; + } + for (const row of next.tables.Wonga || []) + if ((Number(row.CalcIndex) & 0xff) === 4) row.CalcIndex = 1; + return next; + } + async function run(state) { + const name = prompt("새 내역서 이름", `${state.project.name || "내역서"} (새 내역서)`)?.trim(); + if (!name) return; + const result = await EstApp.api("/api/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(build(state.project, name)), + }); + EstApp.download(`${result.name}.estweb.json`, JSON.stringify(result, null, 2)); + state.notice = `새 내역서 「${result.name}」를 따로 저장했습니다. 지금 화면은 그대로입니다.`; + EstApp.render(); + } + function handle(name, state) { + if (name !== "save-new-estimate") return false; + run(state).catch((error) => { + state.notice = error.message; + EstApp.render(); + }); + return true; + } + return { build, handle }; +})(); diff --git a/tools/est-test/native-quantity-sheets.js b/tools/est-test/native-quantity-sheets.js index 9c0f64371..0e07d6441 100644 --- a/tools/est-test/native-quantity-sheets.js +++ b/tools/est-test/native-quantity-sheets.js @@ -129,7 +129,7 @@ const EstQuantitySheets = (() => { ``, ) .join(""); - return `
`; + return `
`; } function handle(name, state, node) { @@ -153,6 +153,55 @@ const EstQuantitySheets = (() => { state.quantitySheet = first.Code; return true; } + if (name === "quantity-sheet-insert") { + // 삽입(TFRMMULLANG btnInsert): 현재 산출서 앞에 새 산출서, 공종명칭(품명·규격) 입력 + const title = prompt("공종명칭", "새 수량산출서"); + if (title == null) return true; + const size = prompt("규격", "") ?? ""; + EstApp.change((project) => { + const sheets = (project.quantitySheets ||= []), + at = Math.max( + 0, + sheets.findIndex((s) => s.Code === code), + ), + next = `Q${EstApp.uid()}`; + sheets.splice(at, 0, { + Code: next, + Name: title.trim() || "새 수량산출서", + Size: size.trim(), + rows: [], + items: [], + options: { digits: 2, roundMode: 2 }, + }); + state.quantitySheet = next; + }, false); + return true; + } + if (name === "quantity-sheet-multi-delete") { + // 선택공종 삭제(btnSelectDel): 고른 산출서들을 한 번에 지움 + const { esc } = EstApp; + EstApp.showDialog( + "선택공종 삭제", + `
${(state.project.quantitySheets || []).map((s) => ``).join("")}
`, + ); + return true; + } + if (name === "quantity-sheet-multi-delete-ok") { + const codes = new Set( + [...document.querySelectorAll('#dialog-content [name="qd-sheet"]:checked')].map( + (n) => n.value, + ), + ); + if (!codes.size) throw Error("지울 산출서를 고르세요."); + if (!confirm(`산출서 ${codes.size}개를 삭제하시겠습니까?`)) return true; + EstApp.byId("dialog").close(); + EstApp.change((project) => { + project.quantitySheets = project.quantitySheets.filter((s) => !codes.has(s.Code)); + if (codes.has(state.quantitySheet)) + state.quantitySheet = project.quantitySheets[0]?.Code || "manual"; + }, false); + return true; + } if (name === "quantity-sheet-copy") { EstApp.change((project) => { state.quantitySheet = copy(project, code, EstApp.uid()); diff --git a/tools/est-test/native-row-links.js b/tools/est-test/native-row-links.js new file mode 100644 index 000000000..254fff34a --- /dev/null +++ b/tools/est-test/native-row-links.js @@ -0,0 +1,160 @@ +// 원본 줄 단위 연결 도구(spec_part 1~3): +// 비목별 단가 불러오기(TfrmHead popupCostNC·GC·JC·All 0xf1b54c) — 일위대가 줄의 하위 자료(견적·일위·단산·기계)에서 한 비목 단가만 +// 남기고 나머지 단가 0, 기준비목(OCheck) := 고른 비목(0 전체·1 재료·2 노무·3 경비). 노임·자재·표준 줄은 거부. +// SUM 색인지정(popMainSumIndex, TfrmSetSumIndex): 고른 줄 SKind := 해제(0)·A(1)…Z(26) — 비율 줄(62)이 코드 글자로 모아 더함. +// 하위데이타 연결/끊기(mnuDataLink 0xf00e6c): 연결 줄은 끊고(LinkIndex 0, 값은 남김), 끊긴 줄은 일위→단산→기계→표준→견적→자재→노임 +// 차례로 코드(없으면 품명·규격·단위 맞춤)를 찾아 코드·명칭·규격·단위·단가를 가져와 연결. 원본은 초점 한 줄, 웹은 고른 줄 모두. +const EstRowLinks = (() => { + const num = (v) => Number(v) || 0; + const SRC = { + 101: "EstiMateDat", + 103: "HeadTitleDat", + 104: "CostTitleDat", + 105: "MachTitleDat", + 21: "SilJukDat", + 5: "StuffcDat", + 6: "LaborcDat", + }; + const ORDER = [103, 104, 105, 21, 101, 5, 6]; + const LINKED = new Set([5, 6, 21, 101, 103, 104, 105, 211, 212, 213, 214, 215]); + const norm = (s) => String(s ?? "").replace(/\s/g, ""); + function byCode(project, kind, code) { + const c = norm(code); + return c ? (project.tables[SRC[kind]] || []).find((r) => norm(r.Code) === c) : null; + } + + function costPart(project, row, tag) { + const kind = num(row.LinkIndex); + if ([5, 6, 21].includes(kind)) throw Error("노임,자재,표준시장단가는 적용할수 없습니다."); + const src = SRC[kind] ? byCode(project, kind, row.Code) : null, + v = (k) => num(src?.[k]); + row.OCheck = tag; + row.NU = row.GU = row.JU = row.SU = 0; + if (tag === 0) Object.assign(row, { NU: v("NC"), GU: v("GC"), JU: v("JC"), SU: v("SC") }); + else if (tag === 2) row.NU = v("NC"); + else if (tag === 1) row.GU = v("GC"); + else Object.assign(row, { JU: v("JC"), SU: v("SC") }); + } + + function sumIndex(view, row, k) { + if (view === "EstMainDat") { + if (num(row.Level) > 0 && !num(row.GN)) + throw Error("레벨지정한 라인의 값을 더할값으로 지정할수없습니다."); + if ([52, 53, 55, 57].includes(num(row.LinkIndex)) || num(row.Index) !== 0) + throw Error("현재라인의 값은 더할값으로 지정할수없습니다."); + } else if (num(row.LinkIndex) === 53 || num(row.Index) > 0) + throw Error("현재라인의 값은 더할값으로 지정할수없습니다."); + row.SKind = k; + } + + function toggle(project, view, row) { + const li = num(row.LinkIndex); + if (li) { + if (LINKED.has(li)) row.LinkIndex = 0; + return li && LINKED.has(li) ? "unlink" : "skip"; + } + const tag = { EstMainDat: 1, HeadMainDat: 2, MachMainDat: 4 }[view] ?? 0; + for (const kind of ORDER) { + const src = norm(row.Code) + ? byCode(project, kind, row.Code) + : EstAutoLink.find(project, kind, EstAutoLink.key(row, tag)); + if (!src) continue; + Object.assign(row, { + Code: src.Code, + Name: src.Name ?? "", + Size: src.Size ?? "", + Unite: src.Unite ?? "", + LinkIndex: kind, + NU: num(src.NC), + GU: num(src.GC), + JU: num(src.JC), + SU: num(src.SC), + }); + if (kind === 5) row.GU = num(src.TC); + if (kind === 6) row.NU = num(src.TC); + row.TU = row.NU + row.GU + row.JU + row.SU; + return "link"; + } + throw Error("해당 하위data를 찾지못해 연결시키지 못했습니다."); + } + + function picked(state, views) { + const rows = state.project.tables[state.view] || [], + list = [...state.selection].filter((i) => rows[i]).sort((a, b) => a - b); + if (!views.includes(state.view) || !list.length) + throw Error( + `${views.length > 2 ? "내역서·일위대가·단가산출·기계경비" : "내역서·일위대가"}에서 줄을 고르세요.`, + ); + return list; + } + + function handle(name, state) { + if (name === "cost-part") { + if (state.view !== "HeadMainDat") throw Error("일위대가 화면에서 쓰세요."); + const list = picked(state, ["HeadMainDat"]); + EstActions.form( + "비목별 단가 불러오기", + [ + { + name: "tag", + label: "불러올 비목", + value: 0, + choices: [ + [0, "전체비목 불러오기"], + [2, "노무비만 불러오기"], + [1, "재료비만 불러오기"], + [3, "경비만 불러오기"], + ], + }, + ], + (values) => { + const probe = structuredClone(state.project); + list.forEach((i) => costPart(probe, probe.tables.HeadMainDat[i], Number(values.tag))); + EstApp.change((project) => + list.forEach((i) => + costPart(project, project.tables.HeadMainDat[i], Number(values.tag)), + ), + ); + }, + ); + } else if (name === "sum-index") { + const list = picked(state, ["EstMainDat", "HeadMainDat"]); + EstActions.form( + "Sum 색인", + [ + { + name: "k", + label: "색인", + value: 1, + choices: [ + [0, "해제"], + ...Array.from({ length: 26 }, (_, i) => [i + 1, String.fromCharCode(65 + i)]), + ], + }, + ], + (values) => { + const rows = state.project.tables[state.view]; + list.forEach((i) => sumIndex(state.view, structuredClone(rows[i]), 0)); + EstApp.change((project) => + list.forEach((i) => + sumIndex(state.view, project.tables[state.view][i], Number(values.k)), + ), + ); + }, + ); + } else if (name === "data-link") { + const list = picked(state, ["EstMainDat", "HeadMainDat", "CostMainDat", "MachMainDat"]); + const probe = structuredClone(state.project); + list.forEach((i) => toggle(probe, state.view, probe.tables[state.view][i])); + let out = []; + EstApp.change((project) => { + out = list.map((i) => toggle(project, state.view, project.tables[state.view][i])); + }); + const linked = out.filter((r) => r === "link").length, + unlinked = out.filter((r) => r === "unlink").length; + state.notice = `연결 ${linked}줄 · 끊기 ${unlinked}줄`; + } else return false; + return true; + } + return { costPart, sumIndex, toggle, handle }; +})(); diff --git a/tools/est-test/native-text.js b/tools/est-test/native-text.js index 71d0c8ed5..29f4715ae 100644 --- a/tools/est-test/native-text.js +++ b/tools/est-test/native-text.js @@ -1,4 +1,6 @@ // REA f2a1fc/f254c8: LineData 표지와 일반·성분·마지막 합계의 정밀도. +// 전역변수(spec_cost_tools §2 · 0x93c22c): \x05이름\x05 표지로 대입한 변수는 호표가 바뀌어도 남아(전역표) 다른 호표에서 표지 없이 써도 찾는다. +// 찾는 차례 = 그 호표 변수 → 전역표. 표지는 계산 동안 떼고 계산 뒤 같은 낱말에 다시 붙인다. const EstText = (() => { const { num, components, floating, sum } = EstMath; const marker = /\x12([-\d.,]+)\x12/; @@ -6,7 +8,7 @@ const EstText = (() => { const clean = (s) => String(s || "") .replace(/[\x06\x07][^\x06\x07]*[\x06\x07]/g, "") - .replace(/[\x03\x0c\x0e\x11\x12\x13\x15\x1a]/g, ""); + .replace(/[\x03\x05\x0c\x0e\x11\x12\x13\x15\x1a]/g, ""); const numeric = (s) => Number(String(s).replaceAll(",", "")); const sumCurrency = (values) => values.reduce((n, value) => n + EstMath.even(num(value) * 10000), 0) / 10000; @@ -56,8 +58,10 @@ const EstText = (() => { titles = [], errors = [], checks = []; + const globals = Object.create(null), + globalList = []; let head = null, - variables = Object.create(null), + variables = Object.create(globals), changed = new Map(), currentSource = null, finalOverride = null, @@ -94,7 +98,7 @@ const EstText = (() => { if (num(row.Index) > 0) { finish(); head = row; - variables = Object.create(null); + variables = Object.create(globals); changed = new Map(); currentSource = null; finalOverride = null; @@ -170,7 +174,10 @@ const EstText = (() => { errors.push(`${head.Code}:${index + 1} ${error.message}`); } } - let text = original.replace(/\{([+-])\}/g, "$1"); + const globalNames = [...original.matchAll(/\x05([A-Za-z][A-Za-z0-9_]*)\x05/g)].map( + (m) => m[1], + ); + let text = original.replace(/\x05/g, "").replace(/\{([+-])\}/g, "$1"); let selectionError = ""; if (num(row.LinkIndex) === 62) { text = text.replace(/(\x07([A-Z])\x07\s*)\x1a[^\x1a]*\x1a/g, (whole, reference, group) => { @@ -599,16 +606,28 @@ const EstText = (() => { errors.push(`${head.Code}:${index + 1} ${error.message}`); row.Error = error.message; } + for (const name of globalNames) { + if (Object.hasOwn(variables, name)) { + globals[name] = variables[name]; + globalList.push({ name, value: variables[name], index }); + } + if (!String(row.LineData).includes(`\x05${name}\x05`)) + row.LineData = String(row.LineData).replace( + new RegExp(`(? { }; const display = (text) => String(text || "").replace( - /[\x03\x06\x07\x0c\x0e\x11\x12\x13\x15\x16\x1a]/g, + /[\x03\x05\x06\x07\x0c\x0e\x11\x12\x13\x15\x16\x1a]/g, (ch) => `⟦${names[ch]}⟧`, ); const encode = (text) => diff --git a/tools/est-test/search-store.cjs b/tools/est-test/search-store.cjs index 6823841e1..428f29c8e 100644 --- a/tools/est-test/search-store.cjs +++ b/tools/est-test/search-store.cjs @@ -19,12 +19,14 @@ const fold = (value) => .toLowerCase() .replace(/\s+/g, ""); -function search(directory, query, kinds, exclude = "") { +// only = 설계참고철(Main Data) 작업 id 목록 — 있으면 그 작업만. 검색어가 「a~b」 또는 숫자 하나면 단가(TC) 범위·같음(원본 0xefefb0). +function search(directory, query, kinds, exclude = "", only = null) { const words = String(query ?? "") .split(/\s+/) .map(fold) .filter(Boolean); if (!words.length) throw Error("검색어를 입력하세요."); + const range = /^(\d+(?:\.\d+)?)(?:~(\d+(?:\.\d+)?))?$/.exec(String(query).replace(/[\s,]/g, "")); const wanted = (kinds?.length ? kinds : Object.keys(sources)) .map(Number) .filter((k) => sources[k]); @@ -34,7 +36,7 @@ function search(directory, query, kinds, exclude = "") { let total = 0; for (const name of fs.readdirSync(directory).filter((n) => /^[a-f0-9]{24}\.json$/.test(n))) { const id = name.slice(0, -5); - if (id === exclude) continue; + if (id === exclude || (only && !only.includes(id))) continue; let project; try { project = JSON.parse(fs.readFileSync(path.join(directory, name), "utf8")); @@ -45,8 +47,15 @@ function search(directory, query, kinds, exclude = "") { const [table, label] = sources[kind]; for (const row of project.tables?.[table] || []) { if (!row?.Code) continue; - const text = fold(`${row.Code} ${row.Name} ${row.Size}`); - if (!words.every((w) => text.includes(w))) continue; + if (range) { + const tc = Number(row.TC) || 0, + lo = Number(range[1]), + hi = range[2] == null ? lo : Number(range[2]); + if (tc < Math.min(lo, hi) || tc > Math.max(lo, hi)) continue; + } else { + const text = fold(`${row.Code} ${row.Name} ${row.Size}`); + if (!words.every((w) => text.includes(w))) continue; + } total++; if (hits.length < LIMIT) hits.push({ diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 62924800f..f0b3eed65 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -5,6 +5,7 @@ const http = require("node:http"), vm = require("node:vm"), crypto = require("node:crypto"); const { importEstx, table: dbisamTable } = require("./import-estx.cjs"); +const { readXls } = require("./xls-read.cjs"); const exchange = require("./exchange.cjs"); const { exportEstx } = require("./export-estx.cjs"); const jdl = require("./jdl.cjs"); @@ -81,6 +82,11 @@ const allowed = new Set([ "native-block-file.js", "native-display.js", "native-cell-clip.js", + "native-row-links.js", + "native-es-info.js", + "native-jdl-compare.js", + "native-global-vars.js", + "native-new-estimate.js", "native-wonga-tools.js", "native-estimate-print.js", "native-xml-check.js", @@ -200,6 +206,7 @@ const server = http.createServer(async (req, res) => { url.searchParams.get("q"), kinds, url.searchParams.get("exclude") || "", + url.searchParams.get("only") ? url.searchParams.get("only").split(",") : null, ), ); return; @@ -242,6 +249,10 @@ const server = http.createServer(async (req, res) => { reply(res, 200, importEstx(bytes, name)); return; } + if (req.method === "POST" && url.pathname === "/api/read-xls") { + reply(res, 200, readXls(await body(req))); + return; + } if (req.method === "POST" && url.pathname === "/api/pps-table") { // 조달청 코드 목록(원본 System\MainSys\PPS_STUFFC.dat · PPS_HEAD.dat, DBISAM) — 코드·명칭·규격·단위만 const rows = dbisamTable(await body(req), null, []);