Compare commits
98
Commits
8cd7baea5e
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ff08c3663 | ||
|
|
7fc3f098c7 | ||
|
|
0c2a0b1971 | ||
|
|
9ed50f2b0f | ||
|
|
e4f0250c74 | ||
|
|
bed35e3bc3 | ||
|
|
8914b21bad | ||
|
|
56a3e0538f | ||
|
|
4bd9e1484c | ||
|
|
a70788e585 | ||
|
|
335abf5b22 | ||
|
|
05fc3fc849 | ||
|
|
89c6b8edc4 | ||
|
|
4c30347139 | ||
|
|
542abb86a1 | ||
|
|
a74cc19ef3 | ||
|
|
fedd522b21 | ||
|
|
2e8d8e0d6c | ||
|
|
716f68391f | ||
|
|
5bf08ac5af | ||
|
|
282ec87512 | ||
|
|
32043f3c01 | ||
|
|
5520e3c575 | ||
|
|
a65677ffca | ||
|
|
d1c6806024 | ||
|
|
ff92a46936 | ||
|
|
4e766701e8 | ||
|
|
b606c06a9d | ||
|
|
9ddb4467d2 | ||
|
|
28acddb4b9 | ||
|
|
ae31ac00fb | ||
|
|
6ac48240a1 | ||
|
|
0e97fbb0f8 | ||
|
|
8474dbfb4f | ||
|
|
c3ab4bdc21 | ||
|
|
e404924aef | ||
|
|
3d4b2ec1d1 | ||
|
|
f204844fec | ||
|
|
0b91e37806 | ||
|
|
58a3e039b3 | ||
|
|
9592e1aec5 | ||
|
|
ad9a50c584 | ||
|
|
603c9654ef | ||
|
|
23b12ef94e | ||
|
|
c1872fe41b | ||
|
|
1a0eaf1dee | ||
|
|
cbbe03e81d | ||
|
|
3439e1ec6f | ||
|
|
741805ffde | ||
|
|
da8867b0e7 | ||
|
|
8e3072b95e | ||
|
|
7b8a5850df | ||
|
|
806d5ecc7d | ||
|
|
7317dbe4e5 | ||
|
|
5b4d6e9daf | ||
|
|
a07ad33306 | ||
|
|
09f4de0607 | ||
|
|
5c551c3105 | ||
|
|
221ac33e81 | ||
|
|
aef35a456b | ||
|
|
1724d98ae9 | ||
|
|
8a3ac5ad78 | ||
|
|
cdf0e46914 | ||
|
|
b0c0c923fc | ||
|
|
462e79abe2 | ||
|
|
dbc1263232 | ||
|
|
c2e84eaa3d | ||
|
|
7dd5bd7608 | ||
|
|
85408c100d | ||
|
|
937c5f9a56 | ||
|
|
e6cbe3ddac | ||
|
|
f072aab248 | ||
|
|
852aedf849 | ||
|
|
d25894a473 | ||
|
|
f60c593686 | ||
|
|
252fbf7fee | ||
|
|
af156a3c3a | ||
|
|
fd30e71dba | ||
|
|
b8cd50fd63 | ||
|
|
74faf9cf40 | ||
|
|
eaab09896b | ||
|
|
0c27c0793a | ||
|
|
a83b923363 | ||
|
|
9e59cffbbe | ||
|
|
4e7ebf7f87 | ||
|
|
043bc1ee78 | ||
|
|
2094436577 | ||
|
|
79769e6a47 | ||
|
|
22588ea7c4 | ||
|
|
fbaf2f1d28 | ||
|
|
f1abfc2fb1 | ||
|
|
61bb15fbde | ||
|
|
1e4ce036c6 | ||
|
|
5779943540 | ||
|
|
74446990f8 | ||
|
|
284ea58419 | ||
|
|
624fd3a555 | ||
|
|
82e598b11b |
@@ -10,6 +10,7 @@
|
||||
* 고르면 줄이 생김(닫으면 안 생김) · 있는 줄 = 자원 이름 단추
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { createRowMenu } from "@ui/ui_template_context_menu";
|
||||
import { eqText } from "@ui/ui_template_equation";
|
||||
@@ -97,6 +98,36 @@ const KIND_KEY: Record<MoneyKind, LsKey> = {
|
||||
율: "K_율",
|
||||
};
|
||||
|
||||
/** 가져온 기계 찾기 판정(서버 기계 찾기 · 문서 `가져온.기계`) — 그 단가 기호 칸 */
|
||||
interface FoundMach {
|
||||
기호?: string;
|
||||
판정?: string;
|
||||
찾기어?: string;
|
||||
조합?: boolean;
|
||||
후보?: { 이름?: string; 규격?: string }[];
|
||||
}
|
||||
|
||||
function machineFound(d: FormulaDoc, sym: string): FoundMach | undefined {
|
||||
const head = (d as { 가져온?: { 기계?: FoundMach[] } }).가져온;
|
||||
return head?.기계?.find((x) => x.기호 === sym);
|
||||
}
|
||||
|
||||
/** 못 이은 가져온 기계 줄 표지(고르면 사라짐 — 연결이 단가산출로 바뀜) · 조합 기계 다음 줄 */
|
||||
function machineMark(f: FoundMach | undefined, link: { 갈래?: string }): HTMLElement | null {
|
||||
if (!f) return null;
|
||||
const picked = link.갈래 === "단가산출";
|
||||
const key = f.판정 === "후보 여럿" ? "Mach_Many" : f.판정 === "못 찾음" ? "Mach_None" : null;
|
||||
const badge =
|
||||
key && !picked
|
||||
? createBadge(lt(key), "warning", { size: 18 })
|
||||
: f.조합
|
||||
? createBadge(lt("Mach_Combo"), "neutral", { size: 18 })
|
||||
: null;
|
||||
if (badge && key && f.후보?.length)
|
||||
badge.title = f.후보.map((c) => `${c.이름 ?? ""} ${c.규격 ?? ""}`.trim()).join(" · ");
|
||||
return badge;
|
||||
}
|
||||
|
||||
/** 금액 줄 몸 — 한 줄(804-7 · 요구 687-7): [종류][이름 단추 | 이름 칸 · 단가 식][수량][단위][비목] · 좁으면 접힘 */
|
||||
function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
const d = h.doc;
|
||||
@@ -122,6 +153,8 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
? (h.subDoc(link.키)?.이름 ?? h.name(link.키))
|
||||
: h.name(link.키)
|
||||
: "";
|
||||
// 가져온 기계 줄(812-9) — 찾기 판정 · 조회어 = 기종 이름만(규격 뺌 · 이은 우리 기계 › 정규화 이름)
|
||||
const found = kind === "기계" ? machineFound(d, own) : undefined;
|
||||
const pick = createButton({
|
||||
label: (size && !name.includes(size) ? `${name} ${size}` : name) || "—",
|
||||
variant: "ghost",
|
||||
@@ -133,13 +166,15 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
kind,
|
||||
(p) => h.change((doc) => repick(doc, i, own, kind, p)),
|
||||
undefined,
|
||||
String(mat?.검색어 ?? line.이름 ?? ""),
|
||||
found?.찾기어 ?? String(mat?.검색어 ?? line.이름 ?? ""),
|
||||
),
|
||||
});
|
||||
pick.classList.add("lstk-res");
|
||||
pick.title = `${lt("Repick")}${res ? ` — ${res}` : ""}`;
|
||||
pick.dataset.repick = String(i);
|
||||
head.push(pick);
|
||||
const mark = machineMark(found, link);
|
||||
if (mark) head.push(mark);
|
||||
} else
|
||||
head.push(
|
||||
field(lt("Name"), line.이름, (t) => put((l) => (l.이름 = t)), "m"),
|
||||
|
||||
@@ -248,7 +248,11 @@ function rows(
|
||||
}
|
||||
});
|
||||
if (Object.keys(at).length) scope.번지 = at;
|
||||
const name = [line.이름, (line as { 규격?: string }).규격].filter(Boolean).join(" ");
|
||||
const spec = (line as { 규격?: string }).규격;
|
||||
const name =
|
||||
spec && !String(line.이름 ?? "").includes(spec)
|
||||
? `${line.이름 ?? ""} ${spec}`.trim()
|
||||
: String(line.이름 ?? ""); // 이름에 규격이 들면 한 번만(814-1)
|
||||
const qty =
|
||||
line.수량 !== undefined && line.수량 !== null && line.수량 !== "" ? line.수량 : null;
|
||||
const qtyNode = (cost?: string): (string | HTMLElement)[] =>
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
newVar,
|
||||
setRow,
|
||||
shapeOf,
|
||||
sourceOf,
|
||||
unfold,
|
||||
type StackItem,
|
||||
type StackLine,
|
||||
@@ -254,7 +255,9 @@ export function buildBlock(
|
||||
const was = h.before?.get(id);
|
||||
const freed = !iffy && raw !== undefined && !!was && hasIf(tryParse(was) ?? ["num", 0, "0"]);
|
||||
const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null;
|
||||
const tag = expr ? null : sourceTag(h, id, link);
|
||||
const src = v ? sourceOf(h.doc, v) : null; // 가져온 값(812-3) = 옵션 가로 칸과 같은 값 · 꼬리표만
|
||||
const srcTag = src === "원파일" ? lt("Src_Native") : src === "엑셀" ? lt("Src_Excel") : src;
|
||||
const tag = expr ? null : srcTag ? clipTag(srcTag) : sourceTag(h, id, link);
|
||||
const tools: HTMLElement[] = [];
|
||||
if (sub?.결과 && link?.키)
|
||||
tools.push(
|
||||
|
||||
@@ -51,7 +51,7 @@ export function buildSourced(
|
||||
const n = Number(f.input.value.trim().replace(/,/g, ""));
|
||||
if (f.input.value.trim() === "" || !Number.isFinite(n))
|
||||
return void (f.input.value = String(v.연결?.값 ?? "")); // 수만 — 빈 값 · 글 = 그대로
|
||||
set(id, v, String(n));
|
||||
set(id, v, n); // 수 그대로(글 「0.7」 = 셈 「—」 · 공식 묶음 값 칸 `setRow` 와 같은 꼴 · 812-3)
|
||||
});
|
||||
f.root.dataset.option = id;
|
||||
const tagText = tag === "원파일" ? lt("Src_Native") : tag === "엑셀" ? lt("Src_Excel") : tag;
|
||||
@@ -64,7 +64,7 @@ export function buildSourced(
|
||||
if (guess) f.root.dataset.guess = mark;
|
||||
return guess
|
||||
? cell(f.root, lt("Guess"), "warning", lt("Guess_Hint"))
|
||||
: cell(f.root, lt("Src_Native"), "info", lt("Src_Hint"));
|
||||
: cell(f.root, lt(head.원파일 ? "Src_Native" : "Src_Excel"), "info", lt("Src_Hint")); // 812-1 엑셀 읽기기 = 「엑셀」
|
||||
};
|
||||
if (head.규격) out.push(ro(lt("Spec"), head.규격, "", false));
|
||||
for (const g of head.추정 ?? [])
|
||||
|
||||
@@ -14,7 +14,7 @@ const TEXT = {
|
||||
Add_Item: ["+ 그룹", "+ Group"],
|
||||
Add_Line: ["줄 더함", "Add line"],
|
||||
Add_율: ["율 — 그룹 소계의 %", "Rate — % of subtotal"],
|
||||
Add_식: ["식 — 공식 틀", "Formula"],
|
||||
Add_식: ["식", "Formula"],
|
||||
Del_Item: ["그룹 지움", "Delete group"],
|
||||
Del: ["지움", "Delete"],
|
||||
Item: ["그룹", "Group"],
|
||||
@@ -87,6 +87,9 @@ const TEXT = {
|
||||
],
|
||||
Direct_Hint: ["표 값을 손으로 고침 — 표 연결 끊김", "Table value typed by hand — table link off"],
|
||||
Tbl_Missing: ["표를 못 읽음", "Table not found"],
|
||||
Mach_Many: ["기계 후보 여럿 — 고르기", "Several machines — pick one"],
|
||||
Mach_None: ["기계 못 찾음 — 고르기", "Machine not found — pick one"],
|
||||
Mach_Combo: ["조합 기계 — 경비는 첫 줄", "Combined machine — cost on first line"],
|
||||
Tbl_Hint: ["값 칸을 누르면 그 값이 들어감", "Click a value cell to use it"],
|
||||
Tbl_Bound: [
|
||||
"이 값의 줄은 위 옵션이 고름(다른 줄은 옵션에서 바꿈 · 여기선 칸만)",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""부품 문서(PLAN 813-2 · 요구 722-3 · 724~735) — 수식 문서 머리 칸 `부품` 읽기.
|
||||
|
||||
갈래 = 기계경비식(기계 시간당 경비 셈 방식) · 작업량 틀(`기종` = 기계표 상세구분 목록 ·
|
||||
`규격` = 고른 기계 규격 수를 받는 변수 열쇠) · 계수식(기계 무관 · 돈 아닌 식).
|
||||
틀 = `_수식문서_틀.md` 9 장.
|
||||
- 단가산출 기초 목록 · 찾아 넣기 「다른 단가산출」 목록에서 뺌(`strip`) — 키로 열기 · 셈 · 부르기는 그대로
|
||||
- 기종 → 그 기종 기계가 쓰는 기계경비식 · 작업량 틀(`of_kind` · M01 「기계」 화면)
|
||||
- 기계 원문번호 → 그 기종 작업량 틀 문서(`of_machine` · 줄 쌓기 기계 넣기)
|
||||
쓰기 없음 · 로그인 사용자 모두(마스터 읽기).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Doc as doc_store
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
KINDS = ("기계경비식", "작업량 틀", "계수식")
|
||||
|
||||
|
||||
def parts() -> dict[str, dict]:
|
||||
"""{문서 키: 부품 표지}."""
|
||||
return {k: d["부품"] for k, d in doc_store.docs().items() if isinstance(d.get("부품"), dict)}
|
||||
|
||||
|
||||
def strip(rows: list[dict]) -> list[dict]:
|
||||
"""목록에서 부품 문서를 뺌."""
|
||||
have = parts()
|
||||
return [r for r in rows if str(r.get("키")) not in have]
|
||||
|
||||
|
||||
def _brief(key: str, d: dict) -> dict[str, Any]:
|
||||
tag = d.get("부품") or {}
|
||||
return {
|
||||
"키": key,
|
||||
"이름": d.get("이름") or "",
|
||||
"결과단위": d.get("결과단위") or "",
|
||||
"갈래": tag.get("갈래") or "",
|
||||
"규격": tag.get("규격") or "",
|
||||
}
|
||||
|
||||
|
||||
def _machines() -> list[dict]:
|
||||
files = store.loaded()[0]
|
||||
return list((files.get("기계.json") or {}).get("줄") or [])
|
||||
|
||||
|
||||
def _templates(kind: str) -> list[tuple[str, dict]]:
|
||||
return [
|
||||
(k, d)
|
||||
for k, d in doc_store.docs().items()
|
||||
if (d.get("부품") or {}).get("갈래") == "작업량 틀" and kind in d["부품"].get("기종", [])
|
||||
]
|
||||
|
||||
|
||||
def of_kind(group: str, detail: str = "") -> dict[str, list]:
|
||||
"""M01 화면 — 계수식 = 그 넷 · 기계 = 상세구분 `detail` 기계가 쓰는 기계경비식(기계 수)
|
||||
+ 작업량 틀."""
|
||||
docs = doc_store.docs()
|
||||
if group == "계수식":
|
||||
return {
|
||||
"계수식": [
|
||||
_brief(k, d) for k, d in docs.items() if parts().get(k, {}).get("갈래") == "계수식"
|
||||
]
|
||||
}
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as hourly # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
used: dict[str, int] = {}
|
||||
for m in _machines():
|
||||
if m.get("상세구분") != detail:
|
||||
continue
|
||||
got = hourly.logic_of(str(m.get("원문번호") or ""))
|
||||
if got:
|
||||
used[str(got["키"])] = used.get(str(got["키"]), 0) + 1
|
||||
rule = [{**_brief(k, docs[k]), "기계수": n} for k, n in used.items() if k in docs]
|
||||
return {"기계경비식": rule, "작업량 틀": [_brief(k, d) for k, d in _templates(detail)]}
|
||||
|
||||
|
||||
def of_machine(no: str) -> dict[str, Any]:
|
||||
"""줄 쌓기 기계 넣기 — 기계 원문번호 → {기계 {이름 · 규격 · 상세구분}, 틀 [{… 문서}]}."""
|
||||
row = next((m for m in _machines() if str(m.get("원문번호")) == str(no)), None)
|
||||
if not row:
|
||||
raise store.StoreError(404, f"없는 기계 「{no}」")
|
||||
kind = str(row.get("상세구분") or "")
|
||||
return {
|
||||
"기계": {"이름": row.get("이름"), "규격": row.get("규격") or "", "상세구분": kind},
|
||||
"틀": [{**_brief(k, d), "문서": d} for k, d in _templates(kind)],
|
||||
}
|
||||
@@ -14,6 +14,7 @@ from pydantic import BaseModel
|
||||
|
||||
from M01_MasterData import M01_MasterData_Doc as doc_screen
|
||||
from M01_MasterData import M01_MasterData_Equation as equation
|
||||
from M01_MasterData import M01_MasterData_Parts as parts
|
||||
from M01_MasterData import M01_MasterData_Sheet as line_sheet
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
@@ -205,7 +206,14 @@ def get_logics(
|
||||
sub: str = "", detail: str = "", q: str = "", blocked: int | None = None, owner: str = ""
|
||||
) -> dict:
|
||||
"""줄마다 덧칸(묶음 · 작업량 · 부르는/불리는 수 · 관련 · 713-3) — 읽기만."""
|
||||
return {"logics": store_list.attach(_call(store.logics, sub, detail, q, blocked, owner))}
|
||||
rows = parts.strip(_call(store.logics, sub, detail, q, blocked, owner)) # 부품 문서 뺌(813-2)
|
||||
return {"logics": store_list.attach(rows)}
|
||||
|
||||
|
||||
@router.get("/parts")
|
||||
def get_parts(group: str, detail: str = "") -> dict:
|
||||
"""부품 문서(813-2) — 계수식 · 기계 상세구분의 기계경비식 · 작업량 틀."""
|
||||
return _call(parts.of_kind, group, detail)
|
||||
|
||||
|
||||
@router.get("/logic")
|
||||
|
||||
@@ -571,7 +571,13 @@ export async function mountM01LogicLab(
|
||||
showLoadingOverlay();
|
||||
const openKeyOn = async (key: string): Promise<void> => {
|
||||
if (!(await leave())) return;
|
||||
const found = allLogics.find((x) => x.키 === key);
|
||||
// 목록에 없는 부품 문서(813-2) = 키로 읽어 엶
|
||||
const found =
|
||||
allLogics.find((x) => x.키 === key) ??
|
||||
(await fetchLogic(key).then(
|
||||
(o) => o.logic as unknown as { 구분: string; 키: string },
|
||||
() => null,
|
||||
));
|
||||
if (found) await open(logicId(found.구분, found.키), found.구분, found.키);
|
||||
else showToast(tx("Load_Failed"), "error");
|
||||
};
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
.m01lab-src__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere; /* 제목 한 줄이 길면 접음 */
|
||||
}
|
||||
|
||||
.m01lab-src__body {
|
||||
@@ -40,6 +38,11 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* [주] 글 — 본문(--text-body)보다 한 단 작게 */
|
||||
.m01lab-src__note {
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m01lab-src__file {
|
||||
margin: 0;
|
||||
font-size: var(--text-body-sm);
|
||||
@@ -56,6 +59,11 @@
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 표 머리 · 칸 = 본문과 같은 16 — 좁으면 둘레 칸(m01-logic__scroll)이 가로로 굴림 */
|
||||
.m01lab-src .m01lab-src__table :is(td, th) {
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
/* 출처가 가리킨 항 · 주 — 옅은 바탕 + 왼쪽 막대 */
|
||||
.m01lab-src__hit {
|
||||
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
|
||||
@@ -38,6 +38,9 @@ interface Found {
|
||||
item?: string;
|
||||
}
|
||||
|
||||
/** 절 제목 블록의 글만 「번호 이름」 꼴로 */
|
||||
const headFixed = (b: Block): Block => (b.h ? { ...b, text: headText(b.text ?? "") } : b);
|
||||
|
||||
function blockNode(b: Block): HTMLElement {
|
||||
const node = rawNode(b);
|
||||
if (b.hit) node.classList.add("m01lab-src__hit");
|
||||
@@ -65,6 +68,27 @@ function rawNode(b: Block): HTMLElement {
|
||||
return el("p", { className: "m01lab-src__p", text: b.p ?? "" });
|
||||
}
|
||||
|
||||
/** 출처 글에서 첫 번호 앞까지 = 책 · 부문(예 「건설품셈 공통」) — 제목 한 줄의 앞 */
|
||||
const prefixOf = (src: string): string => {
|
||||
const at = src.search(/\d+(?:-\d+)+/);
|
||||
return (at < 0 ? src : src.slice(0, at)).trim();
|
||||
};
|
||||
|
||||
/** 절 제목 줄 「9-7-1. 이름」 → 「9-7-1 이름」 */
|
||||
const headText = (t: string): string => t.replace(/^(\d+(?:-\d+)*)\.\s*/, "$1 ");
|
||||
|
||||
/** 「[주]」 줄부터 다음 제목 앞까지 글 줄 = 한 단 작은 글자(`m01lab-src__note`) */
|
||||
function noteMarked(blocks: Block[]): HTMLElement[] {
|
||||
let inNote = false;
|
||||
return blocks.map((b) => {
|
||||
if (b.h) inNote = false;
|
||||
else if (b.p?.trim().startsWith("[주]")) inNote = true;
|
||||
const node = blockNode(b);
|
||||
if (inNote && b.p !== undefined) node.classList.add("m01lab-src__note");
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
export interface SourceView {
|
||||
root: HTMLElement;
|
||||
/** 그 출처 글의 원문을 그림 — 지금 보이는 것과 같으면 안 부름 */
|
||||
@@ -121,20 +145,28 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView {
|
||||
.then(({ found }) => {
|
||||
if (shown !== src) return;
|
||||
if (!found.length) return none();
|
||||
// 제목 한 줄 = 「책 부문 + 절 번호 · 이름(개정 꼬리)」 — 절 하나면 원문 제목 줄을 숨김 · 여럿이면 절마다 번호 · 이름 줄 둠
|
||||
const lone = found.length === 1 && found[0].blocks[0]?.h ? found[0] : null;
|
||||
if (found.length > 1 || lone) {
|
||||
const pre = prefixOf(src);
|
||||
title.textContent = lone ? `${pre} ${headText(lone.blocks[0].text ?? "")}`.trim() : pre;
|
||||
}
|
||||
body.replaceChildren(
|
||||
...found.flatMap((f) => [
|
||||
el("p", {
|
||||
className: "ui-text ui-text--muted m01lab-src__file",
|
||||
text: [
|
||||
f.file,
|
||||
f.matched === f.number ? "" : ts("Near", f.number),
|
||||
f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
}),
|
||||
...f.blocks.map(blockNode),
|
||||
]),
|
||||
...found.flatMap((f) => {
|
||||
// 파일 경로는 안 보임 — 가까운 절 · 항 안내만
|
||||
const note = [
|
||||
f.matched === f.number ? "" : ts("Near", f.number),
|
||||
f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
return [
|
||||
...(note
|
||||
? [el("p", { className: "ui-text ui-text--muted m01lab-src__file", text: note })]
|
||||
: []),
|
||||
...noteMarked(f === lone ? f.blocks.slice(1) : f.blocks.map(headFixed)),
|
||||
];
|
||||
}),
|
||||
);
|
||||
// 강조 항을 패널 몸 가운데로 — 몸만 굴림(85-5 · scrollIntoView 는 메인 창까지 굴림)
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
@@ -77,7 +77,7 @@ export function createSourceFind(opts: {
|
||||
unit: x.결과단위 ?? "",
|
||||
onPick: () => {
|
||||
find.setRows([]);
|
||||
find.hint.textContent = `${x.원문번호 || x.키} ${x.이름}`;
|
||||
find.hint.textContent = ""; // 고른 기초 이름은 원문 제목 한 줄이 보임
|
||||
opts.onPick(x);
|
||||
},
|
||||
})),
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "./M01_MasterData_UI_Side";
|
||||
import { loadTable } from "./M01_MasterData_UI_Logic_Note";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import { renderParts } from "./M01_MasterData_UI_Parts";
|
||||
import { renderTables } from "./M01_MasterData_UI_Tables";
|
||||
import type { LogicHandle } from "./M01_MasterData_UI_LogicLab";
|
||||
import "./M01_MasterData_UI_Style.css";
|
||||
@@ -84,6 +85,7 @@ function buildPage(): HTMLElement {
|
||||
attrs: { hidden: "" },
|
||||
});
|
||||
const body = el("div", { className: "m01-master__body" });
|
||||
const partsHost = el("div"); // 기계 · 계수 표 아래 부품 문서(813-2)
|
||||
// 제목 줄 · 찾기 줄 = 「단가산출 로직」 화면과 같은 공용 조각
|
||||
const headBar = buildHeadBar({ title, status: [summary], buttons: [drop, save] });
|
||||
const head = el("div", {
|
||||
@@ -99,6 +101,7 @@ function buildPage(): HTMLElement {
|
||||
notice,
|
||||
el("p", { className: "m01-master__hint", text: L("M01_WideHint") }),
|
||||
body,
|
||||
partsHost,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -173,6 +176,7 @@ function buildPage(): HTMLElement {
|
||||
: pick.group === "요율"
|
||||
? renderRateRows(body, pick, query)
|
||||
: renderRows(body, pick, query);
|
||||
renderParts(partsHost, pick, (key) => openLogicTab(key));
|
||||
showNotice(isStale(pick.file.file, pick.file.version) ? [L("M01_FileStale")] : []);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Parts.ts
|
||||
* M01 「기계」 · 「계수」 표 아래 부품 문서(PLAN 813-2 · 요구 722-3 · 724~732) — 단가산출 기초 목록에서 뺀 보조 문서 자리
|
||||
* 기계 · 상세구분(기종) 고름 = 「시간당 기계경비 계산식」(그 기종 기계가 쓰는 방식 · 기계 수) · 「작업량 공식 틀」(그 기종 틀)
|
||||
* 계수 = 「계수식」(기계 무관 · 돈 아닌 식 넷)
|
||||
* 이름 누름 = 줄 쌓기 읽기 창(`createStackScreen` · 고침 없음) · [단가산출 기초에서 열기] = 지금 수식 탭 그대로 고침
|
||||
* 서버 = `GET /api/m01/parts?group&detail`(`M01_MasterData_Parts.of_kind`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createStackScreen } from "../A00_Common/line_stack_screen";
|
||||
import type { Pick } from "./M01_MasterData_UI_Side";
|
||||
|
||||
interface Part {
|
||||
키: string;
|
||||
이름: string;
|
||||
결과단위: string;
|
||||
기계수?: number;
|
||||
}
|
||||
type Parts = Partial<Record<"기계경비식" | "작업량 틀" | "계수식", Part[]>>;
|
||||
|
||||
const T = {
|
||||
기계경비식: ["시간당 기계경비 계산식", "Hourly machine cost formula"],
|
||||
"작업량 틀": ["작업량 공식 틀", "Output formula templates"],
|
||||
계수식: ["계수식", "Coefficient formulas"],
|
||||
Open: ["단가산출 기초에서 열기", "Open in unit price basis"],
|
||||
Machines: ["기계", "machines"],
|
||||
None: ["없음", "None"],
|
||||
Close: ["닫기", "Close"],
|
||||
} as const;
|
||||
const tx = (k: keyof typeof T): string => T[k][currentLanguageIndex === 1 ? 1 : 0];
|
||||
const KINDS = ["기계경비식", "작업량 틀", "계수식"] as const;
|
||||
|
||||
/** 표 아래 부품 칸 — 기계(상세구분 있음) · 계수만 · 그 밖 = 비움 */
|
||||
export function renderParts(host: HTMLElement, pick: Pick, openLogic: (key: string) => void): void {
|
||||
host.replaceChildren();
|
||||
const group = pick.group === "계수" ? "계수식" : pick.group === "기계" ? "기계" : "";
|
||||
if (!group || (group === "기계" && !pick.detail)) return;
|
||||
let stack: ReturnType<typeof createStackScreen> | null = null;
|
||||
/** 줄 쌓기 읽기 창 — 고침 없음 · [단가산출 기초에서 열기] = 수식 탭에서 고침 */
|
||||
const show = (p: Part): void => {
|
||||
stack ??= createStackScreen({ base: "/m01/logic/doc", admin: false, cache: "m01_parts_view" });
|
||||
const view = stack;
|
||||
openModal({
|
||||
title: `${p.키} · ${p.이름}`,
|
||||
closeLabel: tx("Close"),
|
||||
width: "1100px",
|
||||
wide: true,
|
||||
mount: (body, close, actions) => {
|
||||
actions.append(
|
||||
createButton({
|
||||
label: tx("Open"),
|
||||
variant: "ghost",
|
||||
size: 28,
|
||||
onClick: () => {
|
||||
close();
|
||||
openLogic(p.키);
|
||||
},
|
||||
}),
|
||||
);
|
||||
body.append(view.root);
|
||||
view.draw(p.키, true);
|
||||
},
|
||||
});
|
||||
};
|
||||
const q = new URLSearchParams({ group, detail: pick.detail ?? "" });
|
||||
const mine = (host.dataset.partsAsk = q.toString()); // 늦게 온 옛 답은 버림
|
||||
void fetch(`${API_BASE_URL}/m01/parts?${q}`, { credentials: "include" })
|
||||
.then((r): Promise<Parts> | Parts => (r.ok ? (r.json() as Promise<Parts>) : {}))
|
||||
.catch((): Parts => ({}))
|
||||
.then((got) => {
|
||||
if (host.dataset.partsAsk !== mine) return;
|
||||
const blocks = KINDS.filter((k) => k in got).map((k) => {
|
||||
const list = got[k] ?? [];
|
||||
const items = list.map((p) => {
|
||||
const b = createButton({
|
||||
label: `${p.이름}${p.기계수 ? ` · ${tx("Machines")} ${p.기계수}` : ""}`,
|
||||
variant: "ghost",
|
||||
tone: "link",
|
||||
size: 28,
|
||||
onClick: () => show(p),
|
||||
});
|
||||
b.dataset.part = p.키;
|
||||
return b;
|
||||
});
|
||||
return el("section", {
|
||||
className: "m01-parts__block",
|
||||
attrs: { "data-parts": k },
|
||||
children: [
|
||||
el("h3", { className: "m01-parts__title", text: tx(k) }),
|
||||
items.length
|
||||
? el("div", { className: "m01-parts__list", children: items })
|
||||
: createText(tx("None")),
|
||||
],
|
||||
});
|
||||
});
|
||||
if (blocks.length)
|
||||
host.replaceChildren(el("div", { className: "m01-parts", children: blocks }));
|
||||
});
|
||||
}
|
||||
@@ -170,3 +170,21 @@ table.m01-master__grid {
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 부품 문서(813-2) — 기계 · 계수 표 아래 */
|
||||
.m01-parts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
margin-top: var(--spacing-16);
|
||||
}
|
||||
.m01-parts__title {
|
||||
margin: 0 0 var(--spacing-4);
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.m01-parts__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
STmate = 원문 그대로 `M02_San_Emit.stmate`(원파일 원문 = 엑셀 숨은 G 열 · 단가 = COSTN ·
|
||||
끝전 = 옵션 SAN_TYP 1 비목버림 / 0 합버림_큰나머지 · 810-1 저장값 대조로 확인).
|
||||
블록 번 = 엑셀 길과 같음(EST = 산근 표 차례 1부터 = 「단산N」 · STmate = 코드 숫자).
|
||||
같은 공사 = 엑셀 산근 블록이 모두 원파일 블록과 금액 · 이름이 같음(`same`) — 둘 다 있으면 원파일.
|
||||
일위대가(EST HeadMainDat) → 실무자료 호표 꼴(`hopyo`) · STmate 일위대가 구성은 못 읽음(BDQTY 난독화) →
|
||||
호표 없음(엑셀을 같이 올리면 엑셀 호표 + 원파일 산근).
|
||||
올리기 = 파일 하나(814-1 · 요구 742-1 — 같은 공사 엑셀 붙이기 걷음) · 원파일 = 단가산출표 판(`grid` ·
|
||||
산출근거 머리) + 산출근거 · 일위대가 안 만듦(요구 743-2 · `hopyo` = 단산만).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -52,7 +51,10 @@ def pack(got: dict, name: str) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []}
|
||||
out |= {"엑셀": True} if got.get("엑셀") else {} # 812-1 엑셀 읽기기(`M02_San_Xlsx`) — 같은 꼴
|
||||
head = ("코드", "번", "이름", "규격", "단위", "저장")
|
||||
if got.get("엑셀"): # 엑셀 읽기기 — 줄 행 = 시트 행(원문 카드 · 옛 엑셀 길과 같은 자리)
|
||||
if got.get(
|
||||
"엑셀"
|
||||
): # 엑셀 읽기기 — 줄 행 = 시트 행(원문 카드 · 옛 엑셀 길과 같은 자리) · 블록 표지
|
||||
head += ("표지", "몸", "꼬리")
|
||||
got = {**got, "산근": [_rows(b) for b in got["산근"]]}
|
||||
if got["꼴"] == "EST":
|
||||
out["산근"] = [
|
||||
@@ -106,27 +108,32 @@ def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]:
|
||||
names = {b["코드"]: n for n, b in numbers(saved).items()}
|
||||
out = ne.block(blk, int(no), keys, saved["파일"], names)
|
||||
out["원본"] |= {"원코드": blk["코드"]} | ({"규격": blk["규격"]} if blk.get("규격") else {})
|
||||
return _excel(saved, out)
|
||||
return _excel(saved, out, blk)
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
|
||||
nodes = [{"행": ln["행"], "글": ln["글"], **stm.parse_line(ln["글"])} for ln in blk["줄"]]
|
||||
saved_amt = blk.get("저장") or {}
|
||||
head = {"C": saved_amt.get("T"), "D": saved_amt.get("L"), "E": saved_amt.get("M")}
|
||||
head["F"] = saved_amt.get("S")
|
||||
block = {"코드": blk["코드"], "이름": blk.get("이름"), "몸": [], "꼬리": [], "머리": head}
|
||||
block = {"코드": blk["코드"], "이름": blk.get("이름"), "머리": head}
|
||||
block |= {"몸": blk.get("몸") or [], "꼬리": blk.get("꼬리") or []} # 엑셀 = 줄 금액 · 계 꼬리
|
||||
out = em.stmate(block, nodes, saved["단가"], saved["끝전"], keys, saved["파일"])
|
||||
marks = sum(1 for one in out["원본"]["글줄"] if one.get("표지"))
|
||||
sure = len(out["로직"]["호표"]) + len(out["로직"].get("덧줄") or []) + len(out["로직"]["중간"])
|
||||
out["원본"] |= {"원파일": True, "갈래": "STmate", "확정": {"확정": sure, "표지": marks}}
|
||||
return _excel(saved, out)
|
||||
out["원본"] |= (
|
||||
{"규격": blk["규격"]} if blk.get("규격") else {}
|
||||
) # 연결 찾기 근거 · 이름 꼬리(814-1)
|
||||
return _excel(saved, out, blk)
|
||||
|
||||
|
||||
def _excel(saved: dict, out: dict) -> dict[str, Any]:
|
||||
def _excel(saved: dict, out: dict, blk: dict) -> dict[str, Any]:
|
||||
"""엑셀 읽기기 꼴(812-1) — 원본 `원파일` 거짓 · `엑셀` 참(화면 꼬리표 「엑셀」 · 원코드 = 단산 번)."""
|
||||
if saved.get("엑셀"):
|
||||
out["원본"] |= {"원파일": False, "엑셀": True}
|
||||
out["원본"] |= {"시트": "단가산출"} if saved["꼴"] == "EST" else {}
|
||||
out["원본"].pop("원코드", None)
|
||||
out["원본"]["표지"] += [m for m in blk.get("표지") or [] if m.startswith("참고 · ")]
|
||||
return out
|
||||
|
||||
|
||||
@@ -149,89 +156,52 @@ def inner(saved: dict, logic: dict) -> list[tuple[dict, int | None, str | None]]
|
||||
return out
|
||||
|
||||
|
||||
# ── 같은 공사 · 일위대가 ───────────────────────────────────────────────
|
||||
def same(saved: dict, sheets: dict) -> bool:
|
||||
"""엑셀 산근 블록이 모두 원파일 같은 번 블록과 계 금액(1 원 안)이 같음 = 같은 공사."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
have = numbers(saved)
|
||||
kind = book.kind_of(sheets)
|
||||
if not kind or (kind == "STmate") != (saved["꼴"] == "STmate"):
|
||||
return False
|
||||
if kind == "STmate":
|
||||
excel = {int(b["코드"][1:]): b["머리"].get("C") for b in book.stmate_blocks(sheets)}
|
||||
else:
|
||||
excel = {int(n): t.get("T") for n, t in book.est_prices(sheets)[1].items()}
|
||||
if not excel:
|
||||
return False
|
||||
for n, total in excel.items():
|
||||
blk = have.get(int(n))
|
||||
if blk is None or abs(float(total or 0) - float((blk.get("저장") or {}).get("T") or 0)) > 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
GALLAE = {6: "노임", 5: "자재", 105: "기계", 104: "단가산출", 103: "호표", 21: "기타", 101: "기타"}
|
||||
# ── 단가산출표 판(814-1 · 요구 742-2 · 743-2) ───────────────────────────
|
||||
LIST = "단가산출표"
|
||||
_LIST_HEAD = ("코드", "이름", "규격", "단위", "재료비", "노무비", "경비", "합계")
|
||||
_LIST_COST = ("M", "L", "S", "T")
|
||||
|
||||
|
||||
def hopyo(saved: dict) -> dict[str, Any]:
|
||||
"""EST 일위대가(HeadMainDat) → 실무자료 읽기 꼴(`M02_Hopyo_Excel.read_book` 짝 · 호표 · 단산) ·
|
||||
STmate = 호표 없음(구성 못 읽음)."""
|
||||
blocks = numbers(saved)
|
||||
names = {str(b.get("코드")): n for n, b in blocks.items()}
|
||||
heads = {str(h.get("코드")): n for n, h in enumerate(saved.get("일위") or [], 1)}
|
||||
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리)만 · 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
|
||||
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
|
||||
out["단산"] = [
|
||||
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
|
||||
for n, b in blocks.items()
|
||||
for n, b in numbers(saved).items()
|
||||
]
|
||||
if saved["꼴"] != "EST":
|
||||
return out
|
||||
for n, h in enumerate(saved.get("일위") or [], 1):
|
||||
lines = []
|
||||
for k, ln in enumerate(h.get("줄") or []):
|
||||
link = ln.get("연결")
|
||||
code = str((ln.get("자원") or {}).get("코드") or "")
|
||||
ref = None
|
||||
if link == 104 and code in names:
|
||||
ref = {"종류": "단산", "번": names[code]}
|
||||
elif link == 103 and code in heads:
|
||||
ref = {"종류": "일위", "번": heads[code]}
|
||||
kind = "%" if ln.get("연결") in (52, 58, 62) else GALLAE.get(link or 0, "기타")
|
||||
if kind == "자재" and str(ln.get("단위") or "") == "인":
|
||||
kind = "노임"
|
||||
if not any((ln.get("단가") or {}).values()) and kind not in ("%",):
|
||||
kind = "별도계상" if not ref else kind
|
||||
lines.append(
|
||||
{
|
||||
"차례": k,
|
||||
"이름": ln.get("품명") or "",
|
||||
"규격": ln.get("규격") or "",
|
||||
"수량": _dec(ln.get("수량")),
|
||||
"단위": ln.get("단위") or "",
|
||||
"단가": _cost(ln.get("단가")),
|
||||
"금액": _cost(ln.get("저장")),
|
||||
"비고": "",
|
||||
"참조": ref,
|
||||
"갈래": kind,
|
||||
"행": ln.get("행"),
|
||||
"코드": code or None,
|
||||
}
|
||||
)
|
||||
out["호표"].append(
|
||||
{
|
||||
"번": n,
|
||||
"이름": h.get("이름"),
|
||||
"규격": h.get("규격") or "",
|
||||
"단위": h.get("단위") or "",
|
||||
"금액": _cost(h.get("저장")),
|
||||
"행": [n, n],
|
||||
"줄": lines,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def grid(saved: dict) -> dict[str, Any]:
|
||||
"""원파일 → 단가산출표 한 시트 격자(`M02_Hopyo_Grid.read` 꼴) — 산출근거 머리(코드 · 이름 · 규격 ·
|
||||
단위 · 저장 금액) 한 줄씩 · 행 = 1 머리 + 블록 차례."""
|
||||
cells: dict[str, dict] = {}
|
||||
cols = "ABCDEFGH"
|
||||
for c, head in zip(cols, _LIST_HEAD):
|
||||
cells[f"{c}1"] = {"값": head}
|
||||
rows = []
|
||||
for at, (n, b) in enumerate(numbers(saved).items(), 2):
|
||||
cost = b.get("저장") or {}
|
||||
vals = [b.get("코드"), b.get("이름"), b.get("규격"), b.get("단위")]
|
||||
vals += [_dec(cost.get(k)) for k in _LIST_COST]
|
||||
for c, v in zip(cols, vals):
|
||||
if v not in (None, ""):
|
||||
cells[f"{c}{at}"] = {"값": v if isinstance(v, (int, float)) else str(v)}
|
||||
rows.append({"번": n, "이름": b.get("이름"), "행": [at, at]})
|
||||
sheet = {"id": "s1", "이름": LIST, "칸": cells, "틀고정": {"행": 1}}
|
||||
sheet["열"] = {c: {"폭": w} for c, w in zip(cols, (10, 40, 18, 6, 12, 12, 12, 12))}
|
||||
book = {"종류": "통합문서", "판": 1, "열": "", "서식": [{}], "시트": [sheet], "활성": "s1"}
|
||||
return {
|
||||
"책": book,
|
||||
"호표": [],
|
||||
"단산": rows,
|
||||
"보기": {LIST: "s1"},
|
||||
"원본": {LIST: LIST},
|
||||
"목록": {"호표": [], "단산": [{"번": r["번"], "행": r["행"][0]} for r in rows]},
|
||||
"줄": [],
|
||||
}
|
||||
|
||||
|
||||
def _dec(v: Any) -> int | float | None:
|
||||
"""수 → 엑셀 읽기와 같은 꼴(정수 = int · 아니면 float · 없음 = None)."""
|
||||
if v is None:
|
||||
|
||||
@@ -226,6 +226,8 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
}
|
||||
return link | ({"칸": item["참조비목"]} if item.get("참조비목") else {}), ""
|
||||
key = str(item.get("이음") or "")
|
||||
if not key and (item.get("자원") or {}).get("조합식"):
|
||||
return None, f"우리 조합 식 {item['자원']['조합식']} 에 듦(첫 줄) — 이 줄 값 0"
|
||||
if not key:
|
||||
return None, ""
|
||||
if item.get("종류") == "로직":
|
||||
@@ -233,12 +235,74 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
if key.startswith("EQ"):
|
||||
got = hourly_link(key, str(item.get("비목") or ""))
|
||||
return got, "" if got else f"기계 {key} 시간당 경비 자료 없음 — 엑셀 단가 그대로"
|
||||
if key.startswith("MO") and (got := _oil(key)):
|
||||
return got, ""
|
||||
link = {"갈래": "요소", "키": key}
|
||||
if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음)
|
||||
link["단위"] = str(item.get("단위") or "")
|
||||
return link, ""
|
||||
|
||||
|
||||
OPTION = "_옵션" # 연결이 부르는 옵션 변수(문서에 없으면 `_attach` 가 더함)
|
||||
|
||||
|
||||
def _oil(key: str) -> dict | None:
|
||||
"""유가 · 전력 이음(814-1 · 요구 748) — 우리 기초 꼴: 유가 = `MO:경유:{S_지역}` + 지역 옵션(기본
|
||||
전국평균) · 전력 = 전력량요금 계약종별 고르기 + 계약종별 옵션(기본 일반용(갑)Ⅰ 저압)."""
|
||||
whole = imp._whole()
|
||||
try:
|
||||
row = whole.get(key)
|
||||
except Exception: # noqa: BLE001 — 없는 키 = 보통 요소 연결
|
||||
return None
|
||||
rows = list(whole.index.get("MO", {}).values())
|
||||
if row.get("구분") == "전력":
|
||||
kinds = list(dict.fromkeys(str(r.get("상세구분")) for r in rows if r.get("구분") == "전력" and r.get("이름") == row.get("이름"))) # fmt: skip
|
||||
var = _choice("전력 계약종별", "전력계약종별", kinds, str(row.get("상세구분")))
|
||||
link = {
|
||||
"갈래": "요소",
|
||||
"재료": {"이름": row.get("이름"), "계약종별": "입력"},
|
||||
"단위": "㎾h",
|
||||
}
|
||||
return link | {"계약종별": dc.md.var_id(var), OPTION: var}
|
||||
head = str(row.get("원문번호") or "").split(":")[0]
|
||||
if not head or row.get("구분") != "유류":
|
||||
return None
|
||||
places = [
|
||||
str(r.get("상세구분")) for r in rows if str(r.get("원문번호") or "").split(":")[0] == head
|
||||
]
|
||||
places.sort(key=lambda x: x != "전국평균")
|
||||
var = _choice("지역", "지역", places, str(row.get("상세구분")))
|
||||
return {"갈래": "요소", "키": f"MO:{head}:{{{dc.md.var_id(var)}}}", OPTION: var}
|
||||
|
||||
|
||||
def _choice(key: str, below: str, items: list[str], value: str) -> dict:
|
||||
link = {"갈래": "선택", "목록": items, "값": value}
|
||||
return {
|
||||
"열쇠": key,
|
||||
"기호": "S",
|
||||
"아래": below,
|
||||
"위": "",
|
||||
"이름": key,
|
||||
"단위": "",
|
||||
"연결": link,
|
||||
}
|
||||
|
||||
|
||||
MACHINES = "_기계" # 문서 셈 동안 잠깐 — `derive` 가 머리 `가져온.기계` 로 옮김
|
||||
|
||||
|
||||
def _machine_entry(item: dict, sym: str) -> dict:
|
||||
"""기계 줄 찾기 판정 한 칸 — {기호, 원래 {이름, 규격, 단위}, 판정, 키, 후보, 찾기어, 까닭, 조합?}."""
|
||||
res = item["자원"]
|
||||
got = res["찾기"]
|
||||
one = {
|
||||
"기호": sym,
|
||||
"원래": {"이름": got["이름"], "규격": got["규격"], "단위": item.get("단위")},
|
||||
}
|
||||
one |= {k: got.get(k) for k in ("판정", "키", "후보", "찾기어", "까닭")}
|
||||
return one | {k: True for k in ("조합", "조합첫") if res.get(k)}
|
||||
|
||||
|
||||
def _excel(item: dict) -> Any:
|
||||
if item.get("값묶음"):
|
||||
return {k: Decimal(str(item["값묶음"].get(k) or 0)) for k in imp.COSTS}
|
||||
@@ -246,12 +310,16 @@ def _excel(item: dict) -> Any:
|
||||
|
||||
|
||||
def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로)."""
|
||||
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로) ·
|
||||
기계 찾기 판정(812-9) = 문서 `가져온.기계`(단가 기호마다 · 화면 고르기 · 저장 때 별칭)."""
|
||||
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []}
|
||||
found: list[dict] = doc.setdefault(MACHINES, [])
|
||||
for at, line in enumerate((doc.get("금액") or [])[: len(row["호표"])]):
|
||||
item = src.get(ids[at]) if at < len(ids) else None
|
||||
price = str(line.get("단가") or "")
|
||||
var = by_id.get(price.removesuffix(".계"))
|
||||
if item is not None and var is not None and (item.get("자원") or {}).get("찾기"):
|
||||
found.append(_machine_entry(item, dc.md.var_id(var)))
|
||||
if item is None or var is None or (var.get("연결") or {}).get("갈래") != "숫자":
|
||||
continue
|
||||
excel = _excel(item)
|
||||
@@ -261,7 +329,15 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
if note:
|
||||
var["설명"] = note
|
||||
if link is None:
|
||||
if not note and not item.get("이음"): # 못 이은 까닭(814-1 — 흠 아님 · 우리 정보 없음)
|
||||
from M02_MasterTemplete import M02_San_Link as san_link
|
||||
|
||||
var["설명"] = f"이음 없음 — {san_link.why(item)} · 엑셀 단가 그대로"
|
||||
continue
|
||||
if (opt := link.pop(OPTION, None)) and opt["열쇠"] not in {
|
||||
v.get("열쇠") for v in doc["변수"]
|
||||
}:
|
||||
doc["변수"].append(opt) # 유가 지역 · 전력 계약종별 옵션(문서에 하나)
|
||||
if item.get("참조비목") and isinstance(excel, dict):
|
||||
excel = excel[item["참조비목"]]
|
||||
line["단가"], line["비목"] = dc.md.var_id(var), item["참조비목"]
|
||||
@@ -322,8 +398,13 @@ def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
|
||||
if not (doc.get("금액") or doc.get("결과") or row["호표"] or row.get("덧줄")):
|
||||
_empty(doc) # 빈 산근 = 엑셀 0 원(줄 없음 합) — 「—」 아님
|
||||
marks.append(EMPTY)
|
||||
found = doc.pop(MACHINES, None)
|
||||
head |= {"기계": found} if found else {} # 812-9 기계 찾기 판정(줄 쌓기 고르기 · 저장 때 별칭)
|
||||
doc["가져온"] = head | {"표지": marks} | ({"알림": notes} if notes else {})
|
||||
shape.shape(doc, meta) # 809-1 · 요구 704 — 지은 이름 → 엑셀 글 머리 · EST 기계 세 줄 = 한 줄
|
||||
if found: # 세 줄을 한 줄로 합쳐 사라진 기호의 기계 판정은 뺌
|
||||
alive = {dc.md.var_id(v) for v in doc.get("변수") or []}
|
||||
doc["가져온"]["기계"] = [x for x in found if x["기호"] in alive]
|
||||
return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))}
|
||||
|
||||
|
||||
|
||||
@@ -68,20 +68,26 @@ def _rename_steps(doc: dict, swap: dict[str, str]) -> None:
|
||||
st["줄"] = got
|
||||
|
||||
|
||||
def _bundle_of(doc: dict, at: int) -> list[int] | None:
|
||||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비) — 같은 기계 이음 · 같은 수량."""
|
||||
def _bundle_of(doc: dict, at: int, who: dict[str, str] | None = None) -> list[int] | None:
|
||||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비 · 차례 아무렇게) — 같은 기계 이음 · 같은 수량 ·
|
||||
시간당 경비 이음 없음(숫자)이면 같은 기계 자원(`who` 줄 이름 → 자원 · 814-1)."""
|
||||
money = doc.get("금액") or []
|
||||
three = money[at : at + 3]
|
||||
if len(three) < 3 or [x.get("비목") for x in three] != list(COSTS):
|
||||
if len(three) < 3 or sorted(str(x.get("비목")) for x in three) != sorted(COSTS):
|
||||
return None
|
||||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||||
links = [(vars_.get(str(x.get("단가"))) or {}).get("연결") or {} for x in three]
|
||||
if any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||||
if all(x.get("갈래") == "숫자" and not isinstance(x.get("값"), dict) for x in links):
|
||||
mine = {(who or {}).get(str(x.get("이름"))) for x in three}
|
||||
if len(mine) != 1 or None in mine:
|
||||
return None
|
||||
elif any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||||
return None
|
||||
same = {(x.get("키"), str(x.get("입력"))) for x in links}
|
||||
if len(same) != 1 or [x.get("칸") for x in links] != list(COSTS):
|
||||
elif len({(x.get("키"), str(x.get("입력"))) for x in links}) != 1 or [
|
||||
x.get("칸") for x in links
|
||||
] != [x.get("비목") for x in three]:
|
||||
return None
|
||||
for k in ("수량", "규격", "자리", "합밖"):
|
||||
for k in ("수량", "규격", "자리", "합밖", "번지"):
|
||||
if len({str(x.get(k) or "") for x in three}) != 1:
|
||||
return None
|
||||
names = {str(x.get("이름")) for x in three}
|
||||
@@ -113,30 +119,70 @@ def _rewrite(text: Any, swap: dict[str, str]) -> Any:
|
||||
return pat.sub(lambda m: swap[m.group(0)], text)
|
||||
|
||||
|
||||
def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
"""② EST 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름}."""
|
||||
AFTER = "_뒤" # 옮긴 조합 다음 줄 — 차림이 원본 행 차례로 다시 섞지 않게(다듬기 사이 임시)
|
||||
|
||||
|
||||
def regroup(doc: dict, who: dict[str, str], parts: set[str]) -> None:
|
||||
"""814-1 — 조합 기계 비목 세 줄 사이에 낀 다음 줄(값 0)을 세 줄 뒤로 · 같은 기계끼리 모음
|
||||
(첫 줄 · 다음 줄이 엇갈려 항목이 겹치지 않게 · 값 0 줄이라 셈 그대로). `parts` = 조합 다음 줄 이름."""
|
||||
money = doc.get("금액") or []
|
||||
at = 0
|
||||
while at < len(money):
|
||||
head = who.get(str(money[at].get("이름")))
|
||||
if head is None or str(money[at].get("이름")) in parts:
|
||||
at += 1
|
||||
continue
|
||||
end, mains, rest = at, [], []
|
||||
while end < len(money):
|
||||
name = str(money[end].get("이름"))
|
||||
if name in parts:
|
||||
rest.append(money[end])
|
||||
elif who.get(name) == head and len(mains) < 3:
|
||||
mains.append(money[end])
|
||||
else:
|
||||
break
|
||||
end += 1
|
||||
if len(mains) > 1 and rest:
|
||||
seen = list(dict.fromkeys(who.get(str(x.get("이름"))) for x in rest))
|
||||
rest.sort(key=lambda x: seen.index(who.get(str(x.get("이름"))))) # 같은 기계끼리
|
||||
for x in rest:
|
||||
x[AFTER] = True # 차림(`NativeLayout.apply`) 원본 행 = 앞 줄 행 뒤
|
||||
money[at:end] = mains + rest
|
||||
at = max(end, at + 1)
|
||||
|
||||
|
||||
def machines(doc: dict, title: dict[str, str], who: dict[str, str] | None = None) -> None:
|
||||
"""② 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름} ·
|
||||
`who` = {줄 이름: 기계 자원 열쇠}(시간당 경비 이음 없는 세 줄 · 814-1)."""
|
||||
money = doc.get("금액") or []
|
||||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||||
out, swap, at, used = [], {}, 0, set()
|
||||
while at < len(money):
|
||||
idx = _bundle_of(doc, at)
|
||||
idx = _bundle_of(doc, at, who)
|
||||
if idx is None:
|
||||
out.append(money[at])
|
||||
at += 1
|
||||
continue
|
||||
three = [money[i] for i in idx]
|
||||
first = vars_[str(three[0]["단가"])]
|
||||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||||
excel = {
|
||||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||||
for c, x in zip(COSTS, three)
|
||||
}
|
||||
link["엑셀"] = excel
|
||||
name = title.get(str(three[0].get("이름"))) or str(three[0].get("이름"))
|
||||
three = sorted((money[i] for i in idx), key=lambda x: COSTS.index(x["비목"]))
|
||||
first = vars_[str(money[idx[0]]["단가"])]
|
||||
if first["연결"].get("갈래") == "숫자": # 시간당 경비 이음 없음 = 엑셀 값 묶음
|
||||
link = {
|
||||
"갈래": "숫자",
|
||||
"값": {c: (vars_[str(x["단가"])]["연결"]).get("값") for c, x in zip(COSTS, three)},
|
||||
}
|
||||
else:
|
||||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||||
link["엑셀"] = {
|
||||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||||
for c, x in zip(COSTS, three)
|
||||
}
|
||||
lead = money[idx[0]]
|
||||
name = title.get(str(lead.get("이름"))) or str(lead.get("이름"))
|
||||
first["연결"], first["이름"] = link, f"{name} 단가"
|
||||
if name in {str(x.get("이름")) for x in money} - {str(x.get("이름")) for x in three}:
|
||||
name = f"{name} ({at + 1})" # 같은 이름 줄이 있으면 겹치지 않게
|
||||
line = {k: v for k, v in three[0].items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||||
line = {k: v for k, v in lead.items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||||
line["단가"] = lead["단가"]
|
||||
_rename_steps(doc, {str(x.get("이름")): name for x in three})
|
||||
if any(x.get("기호") for x in three): # 부르던 식 = 번지 칸(엔진 `번지.X.노무비`)
|
||||
place = _place(doc, name, used)
|
||||
@@ -147,7 +193,8 @@ def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
doc["변수"] = [
|
||||
v
|
||||
for v in doc["변수"]
|
||||
if v is first or v.get("열쇠") not in {str(x["단가"]) for x in three[1:]}
|
||||
if v is first
|
||||
or v.get("열쇠") not in {str(x["단가"]) for x in three} - {str(lead["단가"])}
|
||||
]
|
||||
out.append(line)
|
||||
at += 3
|
||||
@@ -161,12 +208,23 @@ def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
|
||||
def shape(doc: dict, meta: dict) -> dict:
|
||||
"""derive 문서 다듬기 — 원본 글줄 · 엔진 줄 자원 이름."""
|
||||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay
|
||||
|
||||
src = meta.get("원본") or {}
|
||||
lay.tag(doc, meta) # 812-3 원파일 차림 — 금액 줄 ↔ 로직 줄(끝에 `apply`)
|
||||
names(doc, list(src.get("글줄") or []))
|
||||
title = {}
|
||||
title, who, parts = {}, {}, set()
|
||||
for item in (meta.get("로직") or {}).get("호표") or []:
|
||||
res = item.get("자원") or {}
|
||||
if res.get("갈래") == "기계" and res.get("이름"):
|
||||
title[str(item.get("이름"))] = " ".join(x for x in (res["이름"], res.get("규격")) if x)
|
||||
machines(doc, title)
|
||||
key = (res.get("코드"), res.get("이름"), res.get("규격"))
|
||||
who[str(item.get("이름"))] = repr(key)
|
||||
if res.get("조합"):
|
||||
parts.add(str(item.get("이름")))
|
||||
regroup(doc, who, parts)
|
||||
machines(doc, title, who)
|
||||
lay.apply(doc, meta)
|
||||
for line in doc.get("금액") or []:
|
||||
line.pop(AFTER, None)
|
||||
return doc
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
② 옵션 풀이 = 그 문서의 선택 · 입력 변수 값을 줄마다 좁힘(줄 수량 · 단가가 의존하는 변수만 돌림 ·
|
||||
값이 가져온 줄 수량 · 단가 묶음에 있거나 0(꺼진 줄)인 값만 남김) → 남은 조합(상한 `CAP_ALL`)을
|
||||
문서 셈 · 입력 변수 후보 = 기본값 · 0 · 가져온 식에 나온 수 · 선택은 가져온 글에 든 값부터.
|
||||
③ 판정 = 줄 대조(수량 · 단가 · 비목 금액 · 율 줄) + 노 · 재 · 경 · 계 —
|
||||
다른 항 0 = 「확정」 · 같은 줄이 하나 넘고 핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」.
|
||||
결과 `{판정, 키, 이름, 옵션, 다른, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`.
|
||||
③ 판정 = 줄 대조 = 로직(812-2 · 요구 712 — `M02_Doc_Match_Note`): 다른 항 = 자원 · 기종 · 단위 환산 ·
|
||||
비목 · 율 구조(대상 · 비목) · 줄 없음 · 이름 · 참고 = 단가 · 끝수 · 율 값 % · 같은 기종 규격 · 수량
|
||||
(값 — 확정을 안 막음 · 812-11 `M02_Doc_Match_Value`) — 다른 항 0 = 「확정」 · 같은 줄이 하나 넘고
|
||||
핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」 · 값이 맞는 조합 없으면 기본 옵션 + 「옵션 확인」.
|
||||
⓪ 기계 한 묶음 + 작업량 식 수량 = 「기계 작업」(우리 기초에 안 붙임 · 요구 728 · 735-4).
|
||||
결과 `{판정, 키, 이름, 옵션, 다른, 참고, 바꿈, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보` ·
|
||||
`바꿈` = [바꾸기] 때 갈아 끼울 기계 줄 [{기호(우리 단가 기호), 기계(가져온 원문번호), 이름, 규격}].
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,6 +26,8 @@ from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Match_Note as note
|
||||
from M02_MasterTemplete import M02_Doc_Match_Value as val
|
||||
|
||||
md, mc, ml = dc.md, dc.mc, dc.ml
|
||||
if not hasattr(
|
||||
@@ -131,8 +137,13 @@ def facts(doc: dict, got: dict) -> list[dict]:
|
||||
"비목": {k: _key(v) for k, v in (row.get("비목") or {}).items() if v},
|
||||
"금액": _key(row.get("금액")),
|
||||
"합밖": bool(row.get("합밖")),
|
||||
"기호": _single(line.get("단가")),
|
||||
"규격": str(line.get("규격") or ""),
|
||||
"수량글": str(line.get("수량") or ""),
|
||||
}
|
||||
)
|
||||
val.mark_combo(doc, out)
|
||||
note.rates(doc, got, out)
|
||||
return out
|
||||
|
||||
|
||||
@@ -281,7 +292,11 @@ def _words(doc: dict) -> str:
|
||||
bits += [x.get("규격") for x in doc.get("금액") or []]
|
||||
for v in doc.get("변수") or []:
|
||||
bits += [str(x) for x in ((v.get("연결") or {}).get("입력") or {}).values()]
|
||||
return " ".join(str(b or "") for b in bits).replace(" ", "")
|
||||
return " ".join(str(b or "") for b in bits).replace(" ", "").replace(",", "")
|
||||
|
||||
|
||||
def _bare(o: Any) -> str:
|
||||
return str(o).replace(" ", "").replace(",", "") # 「D1,800」 = 글 「D1800」
|
||||
|
||||
|
||||
def domains(
|
||||
@@ -297,7 +312,7 @@ def domains(
|
||||
kind, base = link.get("갈래"), link.get("값")
|
||||
if kind == "선택":
|
||||
opts = list(link.get("목록") or [])
|
||||
seen = [o for o in opts if str(o).replace(" ", "") in words and len(str(o)) > 1]
|
||||
seen = [o for o in opts if _bare(o) in words and len(str(o)) > 1]
|
||||
seen.sort(key=lambda o: -len(str(o)))
|
||||
if seen:
|
||||
ev[md.var_id(v)] = set(seen)
|
||||
@@ -405,7 +420,7 @@ def _ratio(a: Any, b: Any) -> str | None:
|
||||
|
||||
|
||||
def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
"""가져온 줄(mine) ↔ M01 줄(ours) → {같음, 짝, 핵심, 다른 [{항, 글}]}."""
|
||||
"""가져온 줄(mine) ↔ M01 줄(ours) → {같음(로직 같은 줄), 짝, 핵심, 다른 [{항, 글}], 참고 [{항, 글}]}."""
|
||||
left, right = list(mine), list(ours)
|
||||
same = paired = 0
|
||||
diffs: list[dict] = []
|
||||
@@ -426,36 +441,46 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
def cls(a: dict, b: dict) -> bool:
|
||||
return a["갈래"] == b["갈래"] or {a["갈래"], b["갈래"]} <= {"기계", "산출"}
|
||||
|
||||
notes: list[dict] = []
|
||||
exact = take(
|
||||
lambda a, b: (
|
||||
a["수량"] == b["수량"] and a["단가"] == b["단가"] and a["비목"] == b["비목"]
|
||||
if core(a, b)
|
||||
else a["갈래"] == b["갈래"] == "율" and a["비목"] == b["비목"]
|
||||
core(a, b)
|
||||
and a["수량"] == b["수량"]
|
||||
and a["단가"] == b["단가"]
|
||||
and a["비목"] == b["비목"]
|
||||
and (a["코드"] == b["코드"] or not (a["코드"] and b["코드"])) # 코드 다름 = 짝 대조
|
||||
and val.same_combo(a, b)
|
||||
)
|
||||
)
|
||||
same += sum(1 for a, _ in exact if a["갈래"] != "율")
|
||||
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["수량"] == b["수량"]):
|
||||
paired += 1
|
||||
if a["갈래"] == "기계" and b["갈래"] == "기계" and a["코드"] != b["코드"]:
|
||||
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
|
||||
elif a["단가"] != b["단가"]:
|
||||
diffs.append({"항": "단가 차이", "글": f"{a['이름']} 단가 {a['단가']} ↔ {b['단가']}"})
|
||||
else:
|
||||
diffs.append({"항": "비목", "글": f"{a['이름']} 비목 다름"})
|
||||
same += len(exact)
|
||||
|
||||
def twin(a: dict, b: dict) -> bool:
|
||||
return core(a, b) and cls(a, b) and a["수량"] == b["수량"]
|
||||
|
||||
for a, b in take(lambda a, b: twin(a, b) and not note.pair(a, b)[0]): # 로직 같은 짝 먼저
|
||||
same += 1
|
||||
notes += note.pair(a, b)[1]
|
||||
for a, b in take(twin):
|
||||
bad, ref = note.pair(a, b)
|
||||
same, paired = (same + 1, paired) if not bad else (same, paired + 1)
|
||||
diffs += bad
|
||||
notes += ref
|
||||
bad, ref, n = val.pairs(take, core, cls, note.pair, _ratio) # 같은 자원 수량 다름 = 값(812-11)
|
||||
same, paired, diffs, notes = same + n, paired + len(bad), diffs + bad, notes + ref
|
||||
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["단가"] == b["단가"]):
|
||||
paired += 1
|
||||
how = _ratio(a["수량"], b["수량"])
|
||||
what = "단위 환산" if how else "수량"
|
||||
diffs.append({"항": what, "글": f"{a['이름']} 수량 {a['수량']} ↔ {b['수량']} {how or ''}"})
|
||||
say = {"항": what, "글": f"{a['이름']} 수량 {a['수량']} ↔ {b['수량']} {how or ''}"}
|
||||
diffs += note.pair(a, b)[0] or [say] # 같은 자원 아님 = 그 로직 까닭(비목 · 자원)
|
||||
for a, b in take(lambda a, b: core(a, b) and a["갈래"] == b["갈래"] == "기계"):
|
||||
paired += 1
|
||||
diffs.append(
|
||||
{"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']} · 수량 다름"}
|
||||
)
|
||||
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율" and a["금액"] == b["금액"]):
|
||||
diffs.append({"항": "비목", "글": f"율 줄 {a['이름']} 비목 다름"})
|
||||
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율"):
|
||||
diffs.append({"항": "율 값", "글": f"{a['이름']} {a['금액']} ↔ {b['이름']} {b['금액']}"})
|
||||
rate_diffs, rate_notes = note.rate_pairs(left, right, take)
|
||||
diffs += rate_diffs
|
||||
notes += rate_notes
|
||||
for side, rows in (("가져온 쪽만", left), ("우리 쪽만", right)):
|
||||
for x in rows:
|
||||
if x["갈래"] == "율":
|
||||
@@ -464,7 +489,15 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
what = "줄 없음"
|
||||
diffs.append({"항": what, "글": f"{side} — {x['이름']}"})
|
||||
n_core = sum(1 for a in mine if a["갈래"] != "율")
|
||||
return {"같음": same, "짝": same + paired, "핵심": n_core, "다른": diffs}
|
||||
swaps = [x.pop("바꿈") for x in notes if "바꿈" in x]
|
||||
return {
|
||||
"같음": same,
|
||||
"짝": same + paired,
|
||||
"핵심": n_core,
|
||||
"다른": diffs,
|
||||
"참고": notes,
|
||||
"바꿈": swaps,
|
||||
}
|
||||
|
||||
|
||||
def _totals(got: dict) -> dict:
|
||||
@@ -473,27 +506,30 @@ def _totals(got: dict) -> dict:
|
||||
|
||||
EXPLAINED = (
|
||||
"이름 다름",
|
||||
"단가 차이",
|
||||
"자원",
|
||||
"산출 단가",
|
||||
"율 대상",
|
||||
"단위 환산",
|
||||
"기계 규격",
|
||||
"비목",
|
||||
"율 값",
|
||||
"율 줄",
|
||||
"공구손료 유무",
|
||||
"끝수",
|
||||
)
|
||||
|
||||
|
||||
def verdict(
|
||||
mine: list[dict], ours: list[dict], got_a: dict, got_b: dict, off: list[dict] = ()
|
||||
) -> dict:
|
||||
"""확정 = 다른 항 0(옵션 글 근거까지 맞음) · 근접 = 핵심 줄 반 넘게 짝 + (같은 줄 있음 또는
|
||||
다른 항이 모두 까닭 있는 갈래 — 단가 · 단위 환산 · 기계 규격 · 비목 · 율) · 그 밖 없음."""
|
||||
"""확정 = 다른 항 0(옵션 글 근거까지 맞음 · 참고는 있어도 됨) · 근접 = 핵심 줄 반 넘게 짝 + (같은 줄
|
||||
있음 또는 다른 항이 모두 까닭 있는 갈래 — 자원 · 단위 환산 · 기계 규격 · 비목 · 율) · 그 밖 없음 ·
|
||||
끝수 = 줄 로직 · 단가 모두 같은데 합만 다름(참고)."""
|
||||
res = compare(mine, ours)
|
||||
res["다른"] = [*off, *res["다른"]]
|
||||
ta, tb = _totals(got_a), _totals(got_b)
|
||||
if not res["다른"] and ta != tb:
|
||||
res["다른"].append({"항": "끝수", "글": f"줄은 같고 합이 다름 {ta} ↔ {tb}"})
|
||||
if not res["다른"] and not res["참고"] and ta != tb:
|
||||
res["참고"].append(
|
||||
{"항": "끝수", "글": f"줄은 같고 합이 다름 {_sum_txt(ta)} ↔ {_sum_txt(tb)}"}
|
||||
)
|
||||
if not res["다른"] and res["같음"]:
|
||||
res["판정"] = SURE
|
||||
elif res["짝"] * 2 >= res["핵심"] + 1 and (
|
||||
@@ -505,6 +541,10 @@ def verdict(
|
||||
return res
|
||||
|
||||
|
||||
def _sum_txt(t: dict) -> str:
|
||||
return " · ".join(f"{k} {note.txt(v)}" for k, v in t.items() if v is not None)
|
||||
|
||||
|
||||
# ── 한 문서 ───────────────────────────────────────────────────────────
|
||||
def _calc(doc: dict, links: Any, given: dict) -> dict | None:
|
||||
try:
|
||||
@@ -526,6 +566,7 @@ def try_doc(
|
||||
dom = narrow(base, links, dom, want, ev)
|
||||
named = [] if same_name(imported, base) else [_unnamed(imported, base)]
|
||||
best: dict | None = None
|
||||
ties: list[tuple] = []
|
||||
for pick in combos(base, dom) or [{}]:
|
||||
got = _calc(base, links, _given(base, pick))
|
||||
if got is None:
|
||||
@@ -533,12 +574,38 @@ def try_doc(
|
||||
off = [_off(base, k, pick[k], ev[k]) for k in ev if k in pick and pick[k] not in ev[k]]
|
||||
off += named
|
||||
res = verdict(mine, facts(base, got), got_imp, got, off)
|
||||
rank = (res["판정"] == SURE, -len(off), res["같음"], res["짝"], -len(res["다른"]))
|
||||
values = sum(1 for x in res["참고"] if x["항"] in val.VALUE) if res["판정"] == SURE else 0
|
||||
rank = (
|
||||
res["판정"] == SURE,
|
||||
-len(off),
|
||||
-values, # 값 참고(규격 · 수량)가 적은 조합 먼저(812-11)
|
||||
res["같음"],
|
||||
res["짝"],
|
||||
-len(res["다른"]),
|
||||
-len(res["참고"]),
|
||||
)
|
||||
if best is None or rank > best["순위"]:
|
||||
best = {**res, "순위": rank, "옵션": pick, "금액": _totals(got)}
|
||||
if res["판정"] == SURE:
|
||||
ties.append((rank, pick))
|
||||
if res["판정"] == SURE and not res["참고"]:
|
||||
break
|
||||
return best or {"판정": NONE, "같음": 0, "짝": 0, "다른": [], "순위": (False, -9, 0, 0, 0)}
|
||||
if best and best["판정"] == SURE and any(x["항"] in val.VALUE[1:] for x in best["참고"]):
|
||||
same = [p for r, p in ties if r == best["순위"]] # 값으로 못 가른 조합(요구 733 ㉢)
|
||||
best["참고"] += val.option_check(base, best["옵션"], same, _qty_vars(base))
|
||||
return best or {
|
||||
"판정": NONE,
|
||||
"같음": 0,
|
||||
"짝": 0,
|
||||
"다른": [],
|
||||
"참고": [],
|
||||
"순위": (False, -9, 0, 0, 0, 0, 0),
|
||||
}
|
||||
|
||||
|
||||
def _qty_vars(doc: dict) -> set[str]:
|
||||
"""금액 줄 수량이 (거쳐서) 부르는 변수."""
|
||||
close = _closure(doc)
|
||||
return set().union(*(_uses(x.get("수량"), close) or set() for x in doc.get("금액") or []))
|
||||
|
||||
|
||||
def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
|
||||
@@ -549,20 +616,25 @@ def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
|
||||
|
||||
|
||||
def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str, Any]:
|
||||
"""가져온 식 문서 → 연결후보 `{판정, 키, 이름, 옵션, 다른, 금액, 후보수}`."""
|
||||
"""가져온 식 문서 → 연결후보 `{판정, 키, 이름, 옵션, 다른, 참고, 금액, 후보수}`."""
|
||||
links = links if isinstance(links, Memo) else Memo(links)
|
||||
imported = note.bare(imported) # 엑셀 이름 규격 · 단위 꼬리 뗌(814-1 · 맞대기만)
|
||||
got_imp = _calc(imported, links_imp, {})
|
||||
if got_imp is None:
|
||||
return {"판정": NONE, "까닭": "가져온 식을 우리 단가로 못 셈", "후보수": 0}
|
||||
mine = facts(imported, got_imp)
|
||||
alone = val.work(imported, mine, note._names)
|
||||
keys = index.candidates(imported)
|
||||
best, best_key = None, None
|
||||
for key in keys:
|
||||
res = try_doc(index.docs[key], links, imported, got_imp, mine, index.regions)
|
||||
if best is None or res["순위"] > best["순위"]:
|
||||
best, best_key = res, key
|
||||
if res["판정"] == SURE:
|
||||
if res["판정"] == SURE and not res["참고"]:
|
||||
break
|
||||
exact = best and best["판정"] == SURE and not {x["항"] for x in best["참고"]} & set(val.CHECK)
|
||||
if alone and not exact: # 기계 작업 — 우리 기초와 수량 · 규격까지 같을 때만 확정으로 붙임
|
||||
return alone
|
||||
if best is None:
|
||||
return {"판정": NONE, "후보수": 0}
|
||||
base = index.docs[best_key]
|
||||
@@ -575,6 +647,8 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
|
||||
"이름": base.get("이름"),
|
||||
"옵션": _shown(base, best["옵션"]),
|
||||
"다른": best["다른"],
|
||||
"참고": best["참고"],
|
||||
"바꿈": best.get("바꿈") or [],
|
||||
"금액": {"가져온": _plain(_totals(got_imp)), "우리": _plain(best["금액"])},
|
||||
"후보수": len(keys),
|
||||
}
|
||||
|
||||
@@ -5,18 +5,24 @@
|
||||
바꾸기(`swap`) = 서버가 다시 찾아 「확정」 일 때만 — 같은 SK 키에 M01 사본 SK(바탕 = 그 기초 ·
|
||||
입력값 = 찾은 옵션 · 802-4a 꼴 · 수식 사본은 저장 때 서버가 찍음)를 씀 · 원래 가져온 식 SK 몸은
|
||||
`가져온보관` 칸에 그대로 둠 · 일위대가 줄 참조 = 키 그대로(SK 내용만 바뀜 → 금액 다시 셈).
|
||||
되돌리기(`back`) = `가져온보관` 몸을 그 키에 다시 씀(가져온 식 금액 그대로).
|
||||
우리 M01 기초는 어느 갈래에서도 안 바뀜(708) · 근접 · 없음 = 바꾸기 409(703).
|
||||
+ 찾은 `바꿈`(같은 기종 규격만 다른 기계 줄 · 812-11 · 요구 733-2) = 사본 문서 그 줄 기계 입력(원문번호)을
|
||||
가져온 규격의 우리 기계로 갈아 끼움(수식 길 저장 = 따로) · 품 · 율 · 단가 · 조건 값은 우리 값 그대로 ·
|
||||
기계가 옵션 기호 · 식이면 안 바꿈.
|
||||
되돌리기(`back`) = `가져온보관` 몸을 그 키에 다시 씀(가져온 식 금액 그대로 · 수식 사본도 보관 몸 그대로).
|
||||
우리 M01 기초는 어느 갈래에서도 안 바뀜(708) · 근접 · 없음 · 기계 작업 = 바꾸기 409(703).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy as cp
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
from M02_MasterTemplete import M02_Doc_Match as dm
|
||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
@@ -33,7 +39,8 @@ def index() -> dm.Index:
|
||||
"""M01 수식 문서 색인 — 폴더가 바뀌면(M01 관리자 고침) 다시."""
|
||||
docs = dc.folder()
|
||||
if _INDEX["문서"] is not docs:
|
||||
_INDEX["문서"], _INDEX["색인"] = docs, dm.Index(docs)
|
||||
aims = {k: d for k, d in docs.items() if not d.get("부품")} # 부품 문서 = 과녁 아님(813-2)
|
||||
_INDEX["문서"], _INDEX["색인"] = docs, dm.Index(aims)
|
||||
return _INDEX["색인"]
|
||||
|
||||
|
||||
@@ -50,7 +57,9 @@ def judge(sk: dict) -> dict[str, Any]:
|
||||
doc = dim.of(sk)
|
||||
links_imp.learn(doc)
|
||||
got = dm.match(doc, links_imp, index(), dm.ml.Links(whole, dc.folder()))
|
||||
return {**got, "때": utc_now_text()}
|
||||
from M02_MasterTemplete import M02_San_Link as san_link # 우리 정보로 셈 줄 수(814-1 띠)
|
||||
|
||||
return {**got, "우리정보": san_link.tally(sk[imp.KEY].get("로직") or {}), "때": utc_now_text()}
|
||||
|
||||
|
||||
def find(key: str, version: str) -> dict[str, Any]:
|
||||
@@ -91,7 +100,36 @@ def swap(key: str, version: str) -> dict[str, Any]:
|
||||
if found.get("판정") != dm.SURE:
|
||||
raise _bad(f"우리 기초와 「확정」 일치가 아님({found.get('판정')}) — 바꾸지 않음", 409)
|
||||
info = store.write(sd.KIND, key, version or got["판"], copy_body(sk, found))
|
||||
return {dm.KEY: found, "판": info.get("판")}
|
||||
done = refit(key, found.get("바꿈") or [])
|
||||
return {dm.KEY: found, "판": done[0] or info.get("판"), "바꾼기계": done[1]}
|
||||
|
||||
|
||||
def refit(key: str, swaps: list[dict]) -> tuple[str | None, list[dict]]:
|
||||
"""사본 문서 기계 줄 → 가져온 규격 우리 기계(그 줄 단가 기호 연결의 입력 「기계」 · 줄 이름 · 규격) →
|
||||
수식 길 저장 · (판, 바꾼 것)."""
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path # 수식 길 — 늦게(돌아 부름 피함)
|
||||
|
||||
if not swaps:
|
||||
return None, []
|
||||
got = path.read(key)
|
||||
doc = cp.deepcopy(got["문서"])
|
||||
done = []
|
||||
for one in swaps:
|
||||
var = next((v for v in doc.get("변수") or [] if dm.md.var_id(v) == one["기호"]), None)
|
||||
link = (var or {}).get("연결") or {}
|
||||
given = link.get("입력") or {}
|
||||
if link.get("갈래") != "단가산출" or not re.fullmatch(r"'[^']+'", str(given.get("기계"))):
|
||||
continue
|
||||
link["입력"] = {**given, "기계": f"'{one['기계']}'"}
|
||||
for line in doc.get("금액") or []:
|
||||
if dm._single(line.get("단가")) == one["기호"]:
|
||||
if line.get("묶음") == line.get("이름"): # 그 줄 이름을 단 묶음 제목도
|
||||
line["묶음"] = f"{one['이름']} {one['규격']}".strip()
|
||||
line["이름"], line["규격"] = one["이름"], one["규격"]
|
||||
done.append(one)
|
||||
if not done:
|
||||
return None, []
|
||||
return path.save(key, {"문서": doc, "판": got["판"]})["판"], done
|
||||
|
||||
|
||||
def back(key: str, version: str) -> dict[str, Any]:
|
||||
@@ -100,5 +138,13 @@ def back(key: str, version: str) -> dict[str, Any]:
|
||||
old = got["문서"].get(KEEP)
|
||||
if not isinstance(old, dict) or not old.get(imp.KEY):
|
||||
raise _bad("되돌릴 가져온 식 없음")
|
||||
info = store.write(sd.KIND, key, version or got["판"], {**old, "키": key})
|
||||
keep = (
|
||||
old.get(dc.KEY) if dc.has(old) else None
|
||||
) # 보관 몸 수식 사본 그대로(바꾼 기계 = 따로 버림)
|
||||
token = dsk.SAVING.set({"키": key, "사본": keep}) if keep else None
|
||||
try:
|
||||
info = store.write(sd.KIND, key, version or got["판"], {**old, "키": key})
|
||||
finally:
|
||||
if token is not None:
|
||||
dsk.SAVING.reset(token)
|
||||
return {"판": info.get("판")}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
"""연결 찾기 — 로직 차이(다른 항) 대 값 차이(참고) 가르기(PLAN 812-2 · 요구 712) — `M02_Doc_Match` 가 부름.
|
||||
|
||||
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기종 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
|
||||
율 구조(있고 없음 · 대상 비목 — 율 값 % 는 참고) · 단위 환산(`M02_Doc_Match_Value`).
|
||||
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 율 값(%) · 같은 기종 규격 · 수량(812-11) ·
|
||||
그로 인한 비목 금액 · 계.
|
||||
율 = 율 줄 셈값 ÷ 대상 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 몫 · 번지 합 = 핵심 줄 전부 ·
|
||||
대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Match_Value as val
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
md = dc.md
|
||||
PRICED = ("노임", "재료", "기계") # 시간당 · 단위 단가 = 값(참고)
|
||||
|
||||
|
||||
# ── 율 구조 ───────────────────────────────────────────────────────────
|
||||
def _names(text: Any) -> set[str]:
|
||||
if text in (None, ""):
|
||||
return set()
|
||||
try:
|
||||
return set(md.names(md.parse(text)))
|
||||
except md.DocError:
|
||||
return set()
|
||||
|
||||
|
||||
def _reach(doc: dict) -> dict[str, set[str]]:
|
||||
"""기호 → 바로 부르는 이름(변수 연결 · 식 줄 · 금액 줄 수량 · 단가)."""
|
||||
direct: dict[str, set[str]] = {}
|
||||
for v in doc.get("변수") or []:
|
||||
direct[md.var_id(v)] = set(md.link_names(v.get("연결") or {}))
|
||||
for line in doc.get("식") or []:
|
||||
direct[str(line.get("기호"))] = _names(line.get("식"))
|
||||
for line in doc.get("금액") or []:
|
||||
if line.get("기호"):
|
||||
direct[str(line["기호"])] = _names(line.get("수량")) | _names(line.get("단가"))
|
||||
return direct
|
||||
|
||||
|
||||
def _close(start: set[str], direct: dict[str, set[str]]) -> set[str]:
|
||||
seen, todo = set(start), list(start)
|
||||
while todo:
|
||||
for n in direct.get(todo.pop(), ()):
|
||||
if n not in seen:
|
||||
seen.add(n)
|
||||
todo.append(n)
|
||||
return seen
|
||||
|
||||
|
||||
def _short(r: Decimal) -> Decimal:
|
||||
return Decimal(f"{r:.6g}").normalize()
|
||||
|
||||
|
||||
def rates(doc: dict, got: dict, facts: list[dict]) -> None:
|
||||
"""율 사실에 `율`(비율 · 못 셈 None) · `대상`({(자원 갈래, 비목)})을 채움(제자리)."""
|
||||
money, rows = doc.get("금액") or [], got.get("줄") or []
|
||||
direct = _reach(doc)
|
||||
core = [f for f in facts if f["갈래"] != "율" and not f["합밖"]]
|
||||
for f in facts:
|
||||
if f["갈래"] != "율":
|
||||
continue
|
||||
line, row = money[f["번"]], rows[f["번"]]
|
||||
reach = _close(_names(line.get("수량")) | _names(line.get("단가")), direct)
|
||||
costs = reach & set(md.COSTS)
|
||||
hits = [c for c in core if _hit(money[c["번"]], reach)]
|
||||
if hits: # 줄 기호 · 줄 수량 · 단가 이름 = 그 줄 금액 전부
|
||||
parts = [(c, tuple(c["비목"])) for c in hits]
|
||||
elif costs: # 비목 이름(「노무비 × 4 %」) = 그 비목 몫만
|
||||
parts = [(c, tuple(k for k in c["비목"] if k in costs)) for c in core]
|
||||
else: # 번지 합 · 못 찾음 = 블록 핵심 줄 전부
|
||||
parts = [(c, tuple(c["비목"])) for c in core]
|
||||
parts = [(c, ks) for c, ks in parts if ks]
|
||||
f["대상"] = frozenset((_cls(c["갈래"]), k) for c, ks in parts for k in ks)
|
||||
f["율"] = None
|
||||
amount = _mul(row.get("수량"), row.get("단가"))
|
||||
if amount is None:
|
||||
continue
|
||||
bases = [sum((_num(c["비목"][k]) for c, ks in parts for k in ks), Decimal(0))]
|
||||
if hits:
|
||||
each = (_mul(rows[c["번"]].get("수량"), rows[c["번"]].get("단가")) for c in hits)
|
||||
bases.append(sum((x or 0 for x in each), Decimal(0)))
|
||||
found = [_short(amount / b) for b in bases if b]
|
||||
if found:
|
||||
f["율"] = min(found, key=lambda r: len(r.as_tuple().digits))
|
||||
|
||||
|
||||
def _hit(line: dict, reach: set[str]) -> bool:
|
||||
parts = _names(line.get("수량")) | _names(line.get("단가"))
|
||||
return bool(line.get("기호") and str(line["기호"]) in reach) or bool(parts and parts <= reach)
|
||||
|
||||
|
||||
def _cls(kind: str) -> str:
|
||||
return "기계" if kind == "산출" else kind # 기계경비 묶음(코드 없음) = 기계
|
||||
|
||||
|
||||
def _num(v: Any) -> Decimal:
|
||||
if isinstance(v, dict):
|
||||
return sum((_num(v.get(k)) for k in md.COSTS), Decimal(0))
|
||||
try:
|
||||
return Decimal(str(v)) if v is not None else Decimal(0)
|
||||
except InvalidOperation:
|
||||
return Decimal(0)
|
||||
|
||||
|
||||
def _mul(q: Any, p: Any) -> Decimal | None:
|
||||
if q is None or p is None:
|
||||
return None
|
||||
return _num(q) * _num(p)
|
||||
|
||||
|
||||
def pct(r: Any) -> str:
|
||||
"""비율 → 「9 %」."""
|
||||
if not isinstance(r, Decimal):
|
||||
return "?"
|
||||
return f"{(r * 100).normalize():f} %"
|
||||
|
||||
|
||||
# ── 짝 줄 ─────────────────────────────────────────────────────────────
|
||||
def txt(v: Any) -> str:
|
||||
"""단가 글 — 수 = 원 단위 쉼표 · 기계 묶음 = 노 · 재 · 경."""
|
||||
if isinstance(v, tuple):
|
||||
return " · ".join(f"{k[0]} {txt(x)}" for k, x in zip(md.COSTS, v) if x)
|
||||
if isinstance(v, Decimal):
|
||||
return f"{v.normalize():,f}"
|
||||
return str(v)
|
||||
|
||||
|
||||
def _plain(text: str) -> str:
|
||||
"""표기 정규화(소리 표기 · 빈칸) — 기계 찾기와 같은 규칙(812-9 `M02_Machine_Find.norm`)."""
|
||||
return mfind.norm(text)
|
||||
|
||||
|
||||
def _head(text: str) -> str:
|
||||
"""기계 이름 머리(괄호 · 규격 앞 한글) — 「머캐덤로울러(자주식)」 → 「머캐덤롤러」."""
|
||||
m = re.match(r"[가-힣]+", _plain(text))
|
||||
return m.group(0) if m else ""
|
||||
|
||||
|
||||
def _same_machine(a: str, b: str) -> bool:
|
||||
"""같은 기종 — 기계표 · 별칭 장부로 둘 다 기종을 찾으면 그 판정(812-9 · 박힌 짝 없음) ·
|
||||
못 찾으면 이름 머리 품음."""
|
||||
got = mfind.same_kind(a, b)
|
||||
if got is not None:
|
||||
return got
|
||||
x, y = _head(a), _head(b)
|
||||
return bool(x and y) and (x in y or y in x)
|
||||
|
||||
|
||||
def _grams(text: str) -> set[str]:
|
||||
t = _plain(text)
|
||||
return {run[i : i + 2] for run in re.findall(r"[가-힣]+", t) for i in range(len(run) - 1)}
|
||||
|
||||
|
||||
def pair(a: dict, b: dict) -> tuple[list[dict], list[dict]]:
|
||||
"""짝 줄 → (다른 항, 참고) — 단가 · 같은 기종 규격만 다르면 참고 · 자원 · 기종 · 비목이 다르면 다른 항."""
|
||||
diffs: list[dict] = []
|
||||
spec: list[dict] = [] # 같은 기종 규격만 다름 = 값(812-11 · 요구 733-2)
|
||||
kinds = {a["갈래"], b["갈래"]}
|
||||
# 기계경비 숫자 묶음(코드 없음 · 원파일 값) ↔ 우리 기계 = 이름 종류로 같은 기계인지
|
||||
loose = kinds == {"기계", "산출"} and not (a if a["갈래"] == "산출" else b)["코드"]
|
||||
combo = not val.same_combo(a, b) # 조합 기계 ↔ 낱 기계 = 다른 기종
|
||||
if a["갈래"] == b["갈래"] == "기계" and (a["코드"] != b["코드"] or combo):
|
||||
if not combo and val.same_machine(a, b):
|
||||
spec.append(val.spec_note(a, b))
|
||||
else:
|
||||
diffs.append(
|
||||
{"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']} — 다른 기종"}
|
||||
)
|
||||
elif loose and not _same_machine(a["이름"], b["이름"]):
|
||||
diffs.append({"항": "기계 규격", "글": f"{a['이름']} ↔ {b['이름']} {b['코드']}"})
|
||||
elif loose:
|
||||
spec += [x for x in (val.loose_note(a, b),) if x]
|
||||
elif a["갈래"] == b["갈래"] == "노임" and a["코드"] != b["코드"]:
|
||||
diffs.append({"항": "자원", "글": f"{a['이름']} {a['코드']} ↔ {b['이름']} {b['코드']}"})
|
||||
elif a["갈래"] == b["갈래"] == "재료" and not _same_stuff(a, b):
|
||||
diffs.append({"항": "자원", "글": f"{a['이름']} ↔ {b['이름']}"})
|
||||
machine = a["갈래"] == b["갈래"] == "기계" or loose # 시간당 단가 구성(운전 노무비 등) = 값
|
||||
priced = a["비목"] and b["비목"] # 한쪽 단가 없음(0) = 값
|
||||
if priced and not machine and set(a["비목"]) != set(b["비목"]):
|
||||
diffs.append(
|
||||
{"항": "비목", "글": f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])}"}
|
||||
)
|
||||
if a["단가"] == b["단가"]:
|
||||
return diffs, spec
|
||||
say = f"{a['이름']} 단가 {txt(a['단가'])} ↔ {txt(b['단가'])}"
|
||||
if (a["갈래"] == b["갈래"] and a["갈래"] in PRICED) or loose:
|
||||
return diffs, [*spec, {"항": "단가 차이", "글": say}]
|
||||
return [*diffs, {"항": "산출 단가", "글": say}], spec
|
||||
|
||||
|
||||
def _same_stuff(a: dict, b: dict) -> bool:
|
||||
"""자재 종류 같음 — 두 코드가 있으면 코드 · 없으면 이름 낱말(한글 두 글자) 겹침."""
|
||||
if a["코드"] and b["코드"] and a["코드"] == b["코드"]:
|
||||
return True
|
||||
return bool(_grams(a["이름"]) & _grams(b["이름"]))
|
||||
|
||||
|
||||
def rate_pairs(left: list[dict], right: list[dict], take: Any) -> tuple[list[dict], list[dict]]:
|
||||
"""율 줄 짝 → (다른 항, 참고) — 구조 = 대상 · 비목 · 같으면 로직 같음(율 값 % 다름 = 참고 · 값은 업데이트됨)."""
|
||||
diffs: list[dict] = []
|
||||
notes: list[dict] = []
|
||||
|
||||
def is_rate(a: dict, b: dict) -> bool:
|
||||
return a["갈래"] == b["갈래"] == "율"
|
||||
|
||||
def same(a: dict, b: dict, *keys: str) -> bool:
|
||||
return is_rate(a, b) and all(
|
||||
(set(a[k]) == set(b[k])) if k == "비목" else a.get(k) == b.get(k) for k in keys
|
||||
)
|
||||
|
||||
def name(a: dict, b: dict) -> str:
|
||||
return f"{a['이름']} {pct(a.get('율'))} ↔ {b['이름']} {pct(b.get('율'))}"
|
||||
|
||||
def target(x: dict) -> str:
|
||||
return "·".join(f"{k} {c}" for k, c in sorted(x.get("대상") or ())) or "없음"
|
||||
|
||||
def value(a: dict, b: dict) -> None:
|
||||
if a.get("율") != b.get("율"):
|
||||
notes.append({"항": "율 값", "글": f"{name(a, b)} — 바꾸면 우리 값"})
|
||||
|
||||
take(lambda a, b: same(a, b, "율", "대상", "비목")) # 로직 · 값 같음
|
||||
for a, b in take(lambda a, b: same(a, b, "대상", "비목")): # 로직 같음 · 율 값만 다름
|
||||
value(a, b)
|
||||
for a, b in take(lambda a, b: same(a, b, "대상")):
|
||||
tool = val.tool_cost(a, b) # 공구손료 비목만 다름 = 값(814-1)
|
||||
(notes if tool else diffs).append(
|
||||
tool or {"항": "비목", "글": f"율 줄 {name(a, b)} 비목 다름"}
|
||||
)
|
||||
value(a, b)
|
||||
for a, b in take(lambda a, b: same(a, b, "율")):
|
||||
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
|
||||
for a, b in take(is_rate):
|
||||
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
|
||||
return diffs, notes
|
||||
|
||||
|
||||
# ── 이름 꼬리(814-1 · 요구 743-3) ─────────────────────────────────────────
|
||||
def bare(doc: dict) -> dict:
|
||||
"""엑셀 단가산출 이름의 규격 · 단위 꼬리(「구조물터파기(인력) 경질토사 / M3」) 뗀 맞댈 문서 —
|
||||
보이는 · 저장 이름은 그대로(연결 찾기만 이 사본을 씀)."""
|
||||
name = str(doc.get("이름") or "")
|
||||
unit = re.sub(r"^원\s*/\s*", "", str(doc.get("결과단위") or "")).strip()
|
||||
spec = str((doc.get("가져온") or {}).get("규격") or "").strip()
|
||||
got = re.sub(rf"\s*/\s*{re.escape(unit)}\s*$", "", name) if unit else name
|
||||
if got != name and spec and got.rstrip().endswith(spec) and got.rstrip() != spec:
|
||||
got = got.rstrip()[: -len(spec)]
|
||||
got = got.strip()
|
||||
return doc if not got or got == name else {**doc, "이름": got}
|
||||
@@ -0,0 +1,233 @@
|
||||
"""연결 찾기 값 기준 넓힘(PLAN 812-11 · 요구 714 · 733 · 734 · 735) — `M02_Doc_Match` 가 부름.
|
||||
|
||||
값(참고 · 확정을 안 막음 · 바꾸면 우리 값):
|
||||
같은 기종 규격만 다름(굴착기 0.7㎥ ↔ 1.0㎥ — 기종 = 기계표 이름 머리 + 괄호 낱말 · 무한궤도 대 타이어 = 다른 기종 ·
|
||||
조합 기계는 구성 줄마다) = 「규격」(바꾸면 가져온 규격 — [바꾸기] 때 그 줄 기계를 갈아 끼움 `바꿈`) ·
|
||||
수량 차이(같은 자원 줄) = 「수량 끝자리」(1 % 안) · 「품 숫자」(가져온 수량 = 수 하나) · 「수량」(조건 값 · 비율 · 표 값).
|
||||
로직(다른 항): 자원 줄 구성(갈래 · 기종 · 노임 직종 · 재료 종류 · 비목) · 줄 없음 · 율 구조 · 단위 환산 =
|
||||
같은 배(× 6 등)가 두 줄 넘게 같이 걸림(결과 단위 기준이 다름) — 한 줄만 배가 맞는 것은 수량 값.
|
||||
기계 작업(`work`) = 가져온 문서가 기계 한 묶음(한 기계 줄 · 조합 기계면 그 구성 줄들)뿐 · 수량이 작업량 식(Q) =
|
||||
우리 기초에 안 붙임(요구 728 · 735-4 — 수량 · 규격까지 같은 우리 기초가 있을 때만 확정) · 공식 꼴이 그 기종
|
||||
「작업량 틀」(813-2 부품)과 같으면 참고에 틀 이름.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
WORK = "기계 작업"
|
||||
SPEC, QTY, TAIL, POOM = "규격", "수량", "수량 끝자리", "품 숫자"
|
||||
VALUE = (SPEC, QTY, TAIL, POOM) # 확정 순위 — 값 참고가 적은 조합 · 기초 먼저
|
||||
CHECK = (*VALUE, "옵션 확인") # 기계 작업 꼴이 확정으로 붙지 않는 참고
|
||||
_NUMBER = re.compile(r"^\s*\d+(?:\.\d+)?\s*$")
|
||||
|
||||
|
||||
# ── 공구손료 율 비목(814-1 · 요구 744 C1) ─────────────────────────────────
|
||||
TOOL = "공구손료 비목"
|
||||
|
||||
|
||||
def tool_cost(a: dict, b: dict) -> dict | None:
|
||||
"""공구손료 율 줄 짝 — 대상은 같고 비목만 다름(실무 재료비 ↔ 우리 경비) = 값(참고 · 확정 안 막음 ·
|
||||
[바꾸기] = 우리 기초 사본 = 우리 비목) · 공구손료 아님 = None."""
|
||||
if "공구" not in str(a.get("이름")) + str(b.get("이름")):
|
||||
return None
|
||||
say = f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])} — 바꾸면 우리 비목"
|
||||
return {"항": TOOL, "글": say}
|
||||
|
||||
|
||||
# ── 기종 ──────────────────────────────────────────────────────────────
|
||||
def row_of(code: Any) -> Any:
|
||||
"""원문번호 → 기계표 줄(`M02_Machine_Find.Row`) · 없음 None."""
|
||||
key = mfind.by_code(code)
|
||||
return mfind.table()[2].get(key) if key else None
|
||||
|
||||
|
||||
def same_kind(a: Any, b: Any) -> bool:
|
||||
"""두 원문번호 = 같은 기종 — 이름 머리 + 괄호 낱말 같음(규격만 다름)."""
|
||||
x, y = row_of(a), row_of(b)
|
||||
return bool(x and y) and x.base == y.base and x.words == y.words
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def _combo(name: str) -> tuple | None:
|
||||
"""조합 기계 이름 → 구성 기종 묶음(정렬) · 낱 기계 = None."""
|
||||
got = mfind.parts(name)
|
||||
return tuple(sorted(mfind.split_name(n)[0] for n, _ in got)) if len(got) > 1 else None
|
||||
|
||||
|
||||
def mark_combo(doc: dict, facts: list[dict]) -> None:
|
||||
"""기계 사실에 `조합`(구성 묶음 · 낱 = None) · `갈라짐`(가져온 조합을 둘로 가른 줄 = 낱 기계로도 맞음)."""
|
||||
head = _machines(doc)
|
||||
for f in facts:
|
||||
if f["갈래"] in ("기계", "산출"):
|
||||
f["조합"] = _combo(f["이름"])
|
||||
f["갈라짐"] = any(head.get(str(f.get("기호")), {}).get(k) for k in ("조합", "조합첫"))
|
||||
|
||||
|
||||
def same_combo(a: dict, b: dict) -> bool:
|
||||
"""조합 여부 같음 — 구성 묶음 같음 · 가른 줄 ↔ 낱 기계도 같음(「굴착기+부착용 집게」 ↔ 굴착기 하나 = 다름)."""
|
||||
x, y = a.get("조합"), b.get("조합")
|
||||
return x == y or (a.get("갈라짐") and y is None) or (b.get("갈라짐") and x is None)
|
||||
|
||||
|
||||
def same_machine(a: dict, b: dict) -> bool:
|
||||
"""같은 기종 기계 줄 — 원문번호 기종 같음 + 조합 구성 같음."""
|
||||
return same_kind(a["코드"], b["코드"]) and same_combo(a, b)
|
||||
|
||||
|
||||
def _eq(code: Any) -> str:
|
||||
r = row_of(code)
|
||||
return f"{r.raw.get('이름')} {r.raw.get('규격') or ''}".strip() if r else str(code)
|
||||
|
||||
|
||||
def spec_note(a: dict, b: dict) -> dict:
|
||||
"""같은 기종 규격 다름 → 참고 「규격」 + 바꿈(우리 줄 단가 기호 → 가져온 원문번호)."""
|
||||
ra = row_of(a["코드"])
|
||||
say = f"규격 다름 {_eq(a['코드'])} ↔ {(row_of(b['코드']).raw.get('규격') or '')}"
|
||||
swap = {"기호": b.get("기호"), "기계": a["코드"], "이름": ra.raw.get("이름"),
|
||||
"규격": ra.raw.get("규격") or ""} # fmt: skip
|
||||
return {"항": SPEC, "글": f"{say} — 바꾸면 가져온 규격", "바꿈": swap}
|
||||
|
||||
|
||||
def loose_note(a: dict, b: dict) -> dict | None:
|
||||
"""못 이은 가져온 기계(원파일 묶음 값) ↔ 우리 기계 — 규격 글이 다르면 「안 바꿈」 참고."""
|
||||
loose, ours = (a, b) if a["갈래"] == "산출" else (b, a)
|
||||
row = row_of(ours["코드"])
|
||||
if not row or not loose.get("규격"):
|
||||
return None
|
||||
if mfind.spec_fit(mfind.tokens(loose["규격"]), row.spec) == "같음":
|
||||
return None
|
||||
say = f"규격 다름 {loose['이름']} {loose['규격']} ↔ {_eq(ours['코드'])}"
|
||||
return {
|
||||
"항": SPEC,
|
||||
"글": f"{say} — 가져온 기계를 우리 표에서 못 찾음 · 바꾸면 우리 기계 그대로",
|
||||
}
|
||||
|
||||
|
||||
# ── 수량 ──────────────────────────────────────────────────────────────
|
||||
def _show(v: Any) -> str:
|
||||
return f"{v:.4g}" if isinstance(v, Decimal) else str(v)
|
||||
|
||||
|
||||
def qty_note(a: dict, b: dict) -> dict:
|
||||
"""같은 자원 줄 수량 다름 → 참고(끝자리 · 품 숫자 · 수량)."""
|
||||
x, y = a["수량"], b["수량"]
|
||||
if isinstance(x, Decimal) and isinstance(y, Decimal) and y and abs(x - y) <= abs(y) / 100:
|
||||
what = TAIL
|
||||
elif _NUMBER.match(str(a.get("수량글") or "")):
|
||||
what = POOM
|
||||
else:
|
||||
what = QTY
|
||||
return {"항": what, "글": f"{a['이름']} 수량 다름 {_show(x)} ↔ {_show(y)} — 바꾸면 우리 값"}
|
||||
|
||||
|
||||
def pairs(
|
||||
take: Any, core: Any, cls: Any, pair: Any, ratio: Any
|
||||
) -> tuple[list[dict], list[dict], int]:
|
||||
"""수량 다른 같은 자원 짝(로직 같음 · `pair` 다른 항 0) → (다른 항 = 단위 환산 무리, 참고, 같은 줄 수) ·
|
||||
`ratio` = 단위 환산 배 글(「× 6」) · 없음 None."""
|
||||
got = take(
|
||||
lambda a, b: core(a, b) and cls(a, b) and a["수량"] != b["수량"] and not pair(a, b)[0]
|
||||
)
|
||||
hows = [ratio(a["수량"], b["수량"]) for a, b in got]
|
||||
many = {
|
||||
h for h in hows if h and hows.count(h) >= 2
|
||||
} # 같은 배가 두 줄 넘게 = 결과 단위 기준 다름
|
||||
diffs: list[dict] = []
|
||||
notes: list[dict] = []
|
||||
for (a, b), how in zip(got, hows):
|
||||
if how in many:
|
||||
diffs.append(
|
||||
{"항": "단위 환산", "글": f"{a['이름']} 수량 {a['수량']} ↔ {b['수량']} {how}"}
|
||||
)
|
||||
else:
|
||||
notes.append(qty_note(a, b))
|
||||
notes += pair(a, b)[1]
|
||||
return diffs, notes, len(got) - sum(1 for h in hows if h in many)
|
||||
|
||||
|
||||
# ── 기계 작업 ─────────────────────────────────────────────────────────
|
||||
def _machines(doc: dict) -> dict[str, dict]:
|
||||
head = (doc.get("가져온") or {}).get("기계")
|
||||
return {str(x.get("기호")): x for x in head or [] if isinstance(x, dict)}
|
||||
|
||||
|
||||
def _one_group(rows: list[dict]) -> bool:
|
||||
if len(rows) == 1:
|
||||
return True
|
||||
return sum(1 for x in rows if x.get("조합첫")) == 1 and all(
|
||||
x.get("조합") or x.get("조합첫") for x in rows
|
||||
)
|
||||
|
||||
|
||||
def _form(text: Any) -> str:
|
||||
"""작업량 식 꼴 — 끝수 함수 벗김 · 빈칸 뺌 · 작은 글자(Cm = cm)."""
|
||||
t = re.sub(r"\s", "", str(text or ""))
|
||||
m = re.match(r"^(?:반올림|버림|올림)\((.*),\d+\)$", t)
|
||||
return (m.group(1) if m else t).lower()
|
||||
|
||||
|
||||
def _q_form(doc: dict, names: set[str]) -> str:
|
||||
for line in doc.get("식") or []:
|
||||
if str(line.get("기호")) in names and str(line.get("기호")).lower().startswith("q"):
|
||||
return _form(line.get("식"))
|
||||
return ""
|
||||
|
||||
|
||||
def work(doc: dict, mine: list[dict], names: Any) -> dict | None:
|
||||
"""가져온 문서가 기계 한 묶음 + 작업량 식 수량이면 {판정 기계 작업, 참고} · 아니면 None
|
||||
(`names` = 식 글 → 쓰는 이름 · 인력 · 재료 · 율 줄이 섞이면 보통 길)."""
|
||||
heads = _machines(doc)
|
||||
if not mine or not heads or any(f["갈래"] == "율" for f in mine):
|
||||
return None
|
||||
rows = [f for f in mine if not f["합밖"]]
|
||||
if not rows or any(
|
||||
str(f.get("기호")) not in heads or f["갈래"] not in ("기계", "산출") for f in rows
|
||||
):
|
||||
return None
|
||||
if not _one_group([heads[str(f["기호"])] for f in rows]):
|
||||
return None
|
||||
used = [names(f.get("수량글")) for f in rows]
|
||||
if not all(used): # 수량이 수뿐 = 작업량 식 아님
|
||||
return None
|
||||
notes = []
|
||||
form = _q_form(doc, set().union(*used))
|
||||
from M01_MasterData import M01_MasterData_Parts as parts # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
for f in rows:
|
||||
if not form or not row_of(f["코드"]):
|
||||
continue
|
||||
got = parts.of_machine(str(f["코드"]))
|
||||
for t in got["틀"]:
|
||||
if any(
|
||||
_form(x.get("식")) == form
|
||||
for x in t["문서"].get("식") or []
|
||||
if x.get("기호") == "Q"
|
||||
):
|
||||
say = f"{got['기계']['이름']} — 작업량 공식 꼴 = {t['키']} {t['이름']}"
|
||||
notes.append({"항": "작업량 틀", "글": say})
|
||||
break
|
||||
return {"판정": WORK, "참고": notes, "후보수": 0}
|
||||
|
||||
|
||||
# ── 옵션 확인 ─────────────────────────────────────────────────────────
|
||||
def option_check(doc: dict, pick: dict, same: list[dict], qty_vars: set[str]) -> list[dict]:
|
||||
"""값이 정확히 맞는 조합이 없고 수량을 정하는 선택이 값으로 안 갈림(같은 순위 조합에 값 여럿) =
|
||||
고른 값(기본 먼저) + 참고 「옵션 확인」."""
|
||||
from M02_MasterTemplete.M02_Doc_Copy import md
|
||||
|
||||
out = []
|
||||
for v in doc.get("변수") or []:
|
||||
vid, link = md.var_id(v), v.get("연결") or {}
|
||||
if link.get("갈래") != "선택" or vid not in qty_vars:
|
||||
continue
|
||||
if len({str(p.get(vid)) for p in same}) > 1:
|
||||
how = "기본값" if pick.get(vid) == link.get("값") else "고른 값"
|
||||
say = f"옵션 확인 — {v.get('이름') or vid} = {pick.get(vid)}({how} · 값으로 못 가름)"
|
||||
out.append({"항": "옵션 확인", "글": say})
|
||||
return out
|
||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
from M02_MasterTemplete import M02_San_Est as est
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
@@ -76,6 +77,9 @@ class Block:
|
||||
self.groups: dict[str, list[tuple[str, float]]] = {} # 구간 글자 → (부름 글, 결과)
|
||||
self.extra: dict[int, dict] = {} # 행 → 소계가 정한 비목(아래 줄)
|
||||
self.got = {"확정": 0, "표지": 0, "추정": 0}
|
||||
self.memo: list[int] = [] # 글 줄 행(812-3 차림 · 분수 그림 위아래 줄 뺌)
|
||||
self.frac: set[int] = set()
|
||||
self.noted: set[tuple] = set() # 기계 표지 단 자원(812-9 · 블록에 한 번)
|
||||
|
||||
# ── 작은 도구 ──────────────────────────────────────────────────
|
||||
def mark(self, ln: dict, why: str) -> None:
|
||||
@@ -182,6 +186,7 @@ class Block:
|
||||
return re.sub(r"\s*([×÷·+*/−-])\s*", r"\1", t).strip().split()
|
||||
|
||||
tops, bottoms, at = cols(i - 1), cols(i + 1), [0]
|
||||
self.frac |= {self.lines[k]["행"] for k in (i - 1, i + 1) if 0 <= k < len(self.lines)}
|
||||
|
||||
def one(_: re.Match) -> str:
|
||||
k = at[0]
|
||||
@@ -294,21 +299,24 @@ class Block:
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def triple(self, rows: list[dict], total: dict | None) -> None:
|
||||
def triple(self, rows: list[dict], total: dict | None, back: int = 0) -> None:
|
||||
"""기계 · 산근 · 견적 세 줄 → 로직 값묶음 한 줄(수량 식 · 자리 같을 때) · 아니면 줄마다."""
|
||||
got = [self.money(ln) for ln in rows]
|
||||
parts = [ln["비목"] for ln in rows]
|
||||
same = all(got) and len({g for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||||
same = all(got) and len({repr(g) for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||||
if not same:
|
||||
for ln, g in zip(rows, got):
|
||||
self.machine_line(ln, g)
|
||||
if total:
|
||||
self.subtotal(total, [call for _, call, _ in self.done[-len(rows) :]])
|
||||
if total: # 소계 = 세 줄 + 바로 앞 같은 기계 낱 줄(`back`)
|
||||
self.subtotal(total, [call for _, call, _ in self.done[-(len(rows) + back) :]])
|
||||
return
|
||||
qty, place = got[0]
|
||||
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
|
||||
r = self.res(rows[0], None, None)
|
||||
r |= {"단가": prices}
|
||||
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
head = next(
|
||||
(b for b in reversed(self.lines[: self.lines.index(rows[0])])
|
||||
if (b.get("자원") or {}).get("코드") == r.get("코드") and not b.get("비목")),
|
||||
@@ -317,7 +325,10 @@ class Block:
|
||||
title = head.get("품명") or r.get("원이름") or r.get("이름") or "줄"
|
||||
name = self.o.names.item(title)
|
||||
one = {"이름": name, "규격": r.get("규격") or "", "단위": r.get("단위") or "", "수량": qty}
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": self.logic_link(r), "자원": r}
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": None, "자원": r}
|
||||
em.first_spec(one, parts)
|
||||
if parts: # 814-1 조합(굴착기 + 브레이커 · 집게) = 우리 조합 식 한 줄
|
||||
one["이음"] = em.combo_link(one, parts) or em.hourly_text(parts[0]["키"])
|
||||
one |= {"자리": place} if place else {}
|
||||
value = total["결과"][-1] if total and total.get("결과") else None
|
||||
for ln in rows: # 노 · 재 · 경 글 = 펼침(맞댈 값은 한 줄 계 — 소계 글 · 없으면 첫 줄)
|
||||
@@ -325,37 +336,39 @@ class Block:
|
||||
at = total or rows[0]
|
||||
value = value if value is not None else sum(ln["결과"][-1] for ln in rows)
|
||||
self.o.add_line(one, True, at["행"], at.get("보임") or "", value)
|
||||
call = self.parts(one, parts, at, rows[0])
|
||||
if total:
|
||||
self.register(total, f"줄.'{name}'")
|
||||
self.register(total, call)
|
||||
self.got["확정"] += 3
|
||||
|
||||
def parts(self, one: dict, parts: list[dict], at: dict, ln: dict) -> str:
|
||||
"""조합 기계 다음 줄(값 0 · 제 이음) · 기계 표지(블록에 한 번) → 이 기계 부름 글(줄 합)."""
|
||||
calls = [f"줄.'{one['이름']}'"]
|
||||
for p in parts[1:]:
|
||||
extra = em.part_line(one, p, self.o.names)
|
||||
self.o.add_line(extra, True, at["행"], at.get("보임") or "", None)
|
||||
calls.append(f"줄.'{extra['이름']}'")
|
||||
seen = (one["자원"].get("이름"), one["자원"].get("규격"))
|
||||
if seen not in self.noted:
|
||||
self.noted.add(seen)
|
||||
for m in em.part_marks(one["자원"], parts):
|
||||
self.mark(ln, m.removeprefix(NOTE))
|
||||
return calls[0] if len(calls) == 1 else f"({' + '.join(calls)})"
|
||||
|
||||
def machine_line(self, ln: dict, got: tuple | None) -> None:
|
||||
qty, place = got if got else ("1", self.frozen(ln, "수량 식 못 읽음")[1])
|
||||
price = (ln.get("연결단가") or [ln["결과"][-1]])[0]
|
||||
r = self.res(ln, ln["비목"], price if got else ln["결과"][-1])
|
||||
r["갈래"] = "기계" if r["갈래"] == "기계" else r["갈래"]
|
||||
name = self.o.names.item(f"{ln.get('품명') or r.get('이름') or '줄'} {COMP[ln['비목']]}")
|
||||
link, _ = em._m01(r["갈래"], r) if self.keys else (None, None)
|
||||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||||
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
one = em.resource(name, r.get("단위"), qty, r, parts[0]["키"] if parts else None)
|
||||
em.first_spec(one, parts)
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def logic_link(self, r: dict) -> str | None:
|
||||
"""기계 = 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 그 밖 = 없음(산근 = 형제 SK 참조)."""
|
||||
if not self.keys or r.get("갈래") != "기계":
|
||||
return None
|
||||
key, _ = em._m01("기계", r)
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
args = "".join(f", {k}={v}" for k, v in got["입력"].items())
|
||||
return f"로직({got['키']}{args})"
|
||||
self.register(ln, self.parts(one, parts, ln, ln))
|
||||
|
||||
def subtotal(
|
||||
self, ln: dict, kids: list[str], part: str | None = None, glyph: bool = True
|
||||
@@ -485,6 +498,7 @@ class Block:
|
||||
|
||||
def text_line(self, ln: dict) -> None:
|
||||
"""글 줄 — 품셈 · 조건 이름만 있는 글(값 없음) = 「추정」 표지(연결 안 함 · 810-3 화면)."""
|
||||
self.memo.append(ln["행"])
|
||||
if _COND.search(ln["보임"]):
|
||||
self.o.glyph(
|
||||
ln["행"], ln["보임"], mark=f"{NOTE}{GUESS} — 품셈 조건 글(값 없음 · 연결 안 함)"
|
||||
@@ -493,6 +507,22 @@ class Block:
|
||||
return
|
||||
self.o.glyph(ln["행"], ln["보임"])
|
||||
|
||||
def tags(self) -> None:
|
||||
"""812-3 차림 표지(`M02_Doc_NativeLayout`) — 글줄 `차림` = 머리(단 > 0 ·
|
||||
소계 구간 첫 글 · 번호 글) · 합계 · 글(근거 · 메모)."""
|
||||
self.o.head[lay.TAG] = True
|
||||
for ln in self.lines:
|
||||
one = self.o.at.get(ln["행"])
|
||||
if one is None or ln["행"] in self.frac or "자리" in one:
|
||||
continue
|
||||
head = (ln.get("단") or 0) > 0 or ln.get("색인") == -20
|
||||
if ln.get("합계"):
|
||||
one[lay.TAG] = "합계"
|
||||
elif head or (ln["행"] in self.memo and lay.NUMBERED.match(one["글"])):
|
||||
one[lay.TAG] = "머리"
|
||||
elif ln["행"] in self.memo:
|
||||
one[lay.TAG] = "글"
|
||||
|
||||
# ── 한 벌 ─────────────────────────────────────────────────────
|
||||
def run(self) -> dict:
|
||||
for k, ln in enumerate(self.lines): # 「노무비 : ‹단가›」(식 · 결과 없음) = 단가 × 1
|
||||
@@ -521,8 +551,22 @@ class Block:
|
||||
rows.append(lines[i + len(rows)])
|
||||
nxt = lines[i + len(rows)] if i + len(rows) < len(lines) else None
|
||||
total = nxt if nxt and nxt.get("연결") == link and nxt.get("비목") == "T" else None
|
||||
back, seen = (
|
||||
0,
|
||||
{r.get("비목") for r in rows},
|
||||
) # 바로 앞 같은 기계 낱 줄(얼린 줄 · 814-1)
|
||||
while i - back > 0 and len(seen) < 3:
|
||||
b = lines[i - back - 1]
|
||||
if (
|
||||
(b.get("자원") or {}).get("코드") != code
|
||||
or b.get("비목") in seen
|
||||
or not b.get("결과")
|
||||
):
|
||||
break
|
||||
seen.add(b.get("비목"))
|
||||
back += 1
|
||||
if all(r.get("결과") for r in rows):
|
||||
self.triple(rows, total)
|
||||
self.triple(rows, total, back)
|
||||
else:
|
||||
for r in rows:
|
||||
self.mark(r, "결과 없는 세 줄")
|
||||
@@ -544,6 +588,7 @@ class Block:
|
||||
elif (ln.get("보임") or "").strip():
|
||||
self.text_line(ln)
|
||||
i += 1
|
||||
self.tags()
|
||||
saved = self.blk.get("저장") or {}
|
||||
check = {COMP[c]: saved.get(c) or 0 for c in COMP} | {"계": saved.get("T") or 0}
|
||||
out = self.o.done("비목버림", None, check, "산근", self.file)
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
"""가져온 단가산출 줄 쌓기 차림(PLAN 812-3 · 요구 713 · 669~677) — 경쟁사 산출근거 꼴.
|
||||
|
||||
금액 · 셈 그대로. `M02_Doc_Import_Shape.shape` 가 부름(`tag` = 다듬기 앞 · `apply` = 끝) ·
|
||||
원본 `차림` 표지 있는 블록만(EST = `M02_Doc_NativeEst` · STmate = `M02_San_Emit.stmate`):
|
||||
항목 = 머리 줄(글줄 `차림` 「머리」 · 「1. …」 「가. …」 글) · 번호 글은 떼고 화면이 매김 ·
|
||||
가 · 나 = `가나다` · STmate 는 계 줄(`단계:`)마다 끊고 그 뒤 첫 글 줄 = 제목 ·
|
||||
머리 앞 줄 = 제목 없는 항목 · 합계 줄(「합계」 · 「소계」 절)만 있는 머리 = 뺌
|
||||
(화면 소계 · 합계가 냄)
|
||||
금액 줄 = 그 자리(원본 행 차례) — EST 는 셈이 같을 때만 금액 차례를 원본 차례로
|
||||
(단계 · 누계 이름 없고 기호 부름이 앞 줄일 때)
|
||||
공식 묶음 = 변수 대입 줄(`mid:` · 쓰는 차례) · 글 줄 = 근거 · 메모(글줄 `차림` 「글」)
|
||||
저장 = 금액 줄 `묶음` = 항목 제목 + 문서 `쌓기`(기본 차림과 다를 때만).
|
||||
파이썬 · TS 짝 — `common_util/common_util_line_stack.ts` `stackOf` · `saveOf` ·
|
||||
`hiddenOf`(안 놓인 기호 자리 · 기본 차림)의 거울 · 거울 시험
|
||||
`resources/tester/test_812_3_native_layout.py`(TS 왕복). 고칠 때 둘을 같이 고침.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SCRIPTS = Path(__file__).resolve().parents[1] / "resources" / "master_data" / "scripts"
|
||||
if str(_SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPTS))
|
||||
|
||||
import master_doc as md # noqa: E402
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd # noqa: E402
|
||||
|
||||
TAG = "차림" # 글줄 칸 — 머리 | 합계 | 글 · 원본 칸 = 차림 낼 블록
|
||||
KEEP = "_로직줄" # 문서 금액 줄 ↔ 로직 줄 차례(다듬기 사이 임시)
|
||||
GANADA = "가나다라마바사아자차카타파하"
|
||||
NUMBERED = re.compile(rf"^\s*'?\s*(\d{{1,2}}|[{GANADA}])\s*[.)]\s*(?!\d)(\S.*)$")
|
||||
_TOTAL = {"합계", "소계", "계"}
|
||||
|
||||
|
||||
def tag(doc: dict, meta: dict) -> None:
|
||||
"""다듬기(이름 · 기계 합침) 앞 — 금액 줄마다 로직 줄 차례(호표 → 덧줄)를 적어 둠."""
|
||||
row = meta.get("로직") or {}
|
||||
n = len(row.get("호표") or []) + len(row.get("덧줄") or [])
|
||||
money = doc.get("금액") or []
|
||||
if (meta.get("원본") or {}).get(TAG) and money and len(money) == n:
|
||||
for k, line in enumerate(money):
|
||||
line[KEEP] = k
|
||||
|
||||
|
||||
def title_of(text: str) -> tuple[str, bool]:
|
||||
"""「 1. 기 계(100%)」 → (「기 계(100%)」, 거짓) · 「'가. 굴삭기'」 → (「굴삭기」, 가나다)."""
|
||||
flat = " ".join(str(text or "").replace("'", " ").split())
|
||||
m = NUMBERED.match(flat)
|
||||
if not m:
|
||||
return flat, False
|
||||
return m.group(2).strip(), m.group(1) in GANADA
|
||||
|
||||
|
||||
# ── 차림 ──────────────────────────────────────────────────────────────
|
||||
def apply(doc: dict, meta: dict) -> None:
|
||||
money = doc.get("금액") or []
|
||||
keys = [line.pop(KEEP, None) for line in money]
|
||||
src = meta.get("원본") or {}
|
||||
if not src.get(TAG) or not money or None in keys:
|
||||
return
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
row = meta.get("로직") or {}
|
||||
ids = sd.line_ids(
|
||||
imp.row_of({imp.KEY: {"로직": row}}) or {}
|
||||
) # 글줄 자리 = 줄 정체(`imp._named`)
|
||||
lines = [*(row.get("호표") or []), *(row.get("덧줄") or [])]
|
||||
by: dict[str, int] = {}
|
||||
for k, (cid, one) in enumerate(zip(ids, lines)):
|
||||
by.setdefault(cid, k)
|
||||
by.setdefault(str(one.get("이름")), k)
|
||||
inv = {k: j for j, k in enumerate(keys)}
|
||||
texts = sorted(
|
||||
(t for t in src.get("글줄") or [] if isinstance(t.get("행"), int)), key=lambda t: t["행"]
|
||||
)
|
||||
at: dict[int, int] = {} # 문서 금액 줄 → 원본 행
|
||||
for t in texts:
|
||||
for spot in t.get("자리들") or ([t["자리"]] if t.get("자리") else []):
|
||||
j = inv.get(by.get(str(spot), -1))
|
||||
if j is not None:
|
||||
at[j] = min(at.get(j, t["행"]), t["행"])
|
||||
rows = _fill([at.get(j) for j in range(len(money))])
|
||||
for j in range(1, len(rows)): # 조합 다음 줄을 세 줄 뒤로 옮긴 것(814-1 `Import_Shape.regroup`)
|
||||
if money[j].get("_뒤"):
|
||||
rows[j] = max(rows[j], rows[j - 1])
|
||||
order = sorted(range(len(money)), key=lambda j: (rows[j], j))
|
||||
if order != list(range(len(money))) and _safe(doc, order):
|
||||
doc["금액"] = money = [money[j] for j in order]
|
||||
rows = [rows[j] for j in order]
|
||||
else:
|
||||
for j in range(1, len(rows)): # 차례 그대로 — 앞 줄보다 위면 앞 줄 자리로
|
||||
rows[j] = max(rows[j], rows[j - 1])
|
||||
items = _items(doc, texts, rows, src.get("갈래") == "STmate")
|
||||
for it in items:
|
||||
for kind, j in it["줄"]:
|
||||
if kind == "금액":
|
||||
money[j].pop("묶음", None)
|
||||
money[j] |= {"묶음": it["제목"]} if it["제목"] else {}
|
||||
raw = [_save_item(it, money) for it in items]
|
||||
doc.pop("쌓기", None)
|
||||
plain = save_of(stack_of(doc), money)
|
||||
got = save_of(stack_of({**doc, "쌓기": raw}), money)
|
||||
if got != plain:
|
||||
doc["쌓기"] = got
|
||||
|
||||
|
||||
def _fill(rows: list[int | None]) -> list[int]:
|
||||
"""원본 행 없는 줄(글줄에 안 붙은 소계 덧줄 등) = 다음 줄 행 · 없으면 앞 줄 행."""
|
||||
out = list(rows)
|
||||
for j in range(len(out) - 1, -1, -1):
|
||||
if out[j] is None and j + 1 < len(out):
|
||||
out[j] = out[j + 1]
|
||||
for j in range(len(out)):
|
||||
if out[j] is None:
|
||||
out[j] = out[j - 1] if j else 0
|
||||
return [int(x or 0) for x in out]
|
||||
|
||||
|
||||
def _safe(doc: dict, order: list[int]) -> bool:
|
||||
"""금액 차례를 바꿔도 셈이 같음 — 단계 없음 · 누계 · 번지 · 단계 이름 안 씀 ·
|
||||
줄 기호 부름 = 앞 줄."""
|
||||
if doc.get("단계"):
|
||||
return False
|
||||
money = doc.get("금액") or []
|
||||
pos = {j: p for p, j in enumerate(order)}
|
||||
sym = {str(x["기호"]): pos[j] for j, x in enumerate(money) if x.get("기호")}
|
||||
for j, line in enumerate(money):
|
||||
for text in (line.get("수량"), line.get("단가")):
|
||||
if text is None or str(text).strip() == "":
|
||||
continue
|
||||
try:
|
||||
got = md.names(md.parse(text))
|
||||
except md.DocError:
|
||||
return False
|
||||
if any(n in md.MONEY_NAMES or n == "계" for n in got):
|
||||
return False
|
||||
if any(n in sym and sym[n] >= pos[j] for n in got):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dict]:
|
||||
"""원본 행 차례로 항목 · 줄을 쌓음 — 줄 = (금액, 문서 줄) | (공식, [기호]) | (글, 글)."""
|
||||
have = set(symbols(doc))
|
||||
hidden = hidden_of(doc)
|
||||
ev: list[tuple[int, int, int, str, Any]] = []
|
||||
for t in texts:
|
||||
mark, text = t.get(TAG), str(t.get("글") or "")
|
||||
spots = t.get("자리들") or ([t["자리"]] if t.get("자리") else [])
|
||||
mids = [s[4:] for s in spots if str(s).startswith("mid:")]
|
||||
if any(str(s).startswith("단계:") for s in spots):
|
||||
ev.append((t["행"], 0, 0, "끝", None))
|
||||
elif mark == "합계":
|
||||
ev.append((t["행"], 0, 0, "합계", None))
|
||||
elif not spots and (mark == "머리" or (mark == "글" and NUMBERED.match(text))):
|
||||
ev.append((t["행"], 0, 0, "머리", title_of(text)))
|
||||
elif not spots and mark == "글" and text.strip():
|
||||
ev.append((t["행"], 1, 0, "글", " ".join(text.replace("'", " ").split())))
|
||||
ids = [m for m in mids if m in have and m not in hidden]
|
||||
if ids:
|
||||
ev.append((t["행"], 1, 0, "공식", ids))
|
||||
ev += [(r, 2, j, "금액", j) for j, r in enumerate(rows)]
|
||||
ev.sort(key=lambda e: e[:3])
|
||||
items: list[dict] = []
|
||||
cur: dict | None = None
|
||||
cut = False # STmate 계 줄 뒤 — 다음 첫 글 줄 = 제목
|
||||
money = doc.get("금액") or []
|
||||
mach = {
|
||||
str(x.get("기호")): x
|
||||
for x in (doc.get("가져온") or {}).get("기계") or []
|
||||
if isinstance(x, dict) and (x.get("조합") or x.get("조합첫"))
|
||||
} # 조합 기계(812-9 · 요구 738-3) = 기계마다 항목 하나 · 제목 = 그 기계
|
||||
|
||||
def new(title: str = "", sub: bool = False) -> dict:
|
||||
it = {"제목": title, "가나다": sub, "줄": [], "합계": False}
|
||||
items.append(it)
|
||||
return it
|
||||
|
||||
for _, _, _, kind, val in ev:
|
||||
if kind == "끝":
|
||||
cur, cut = None, stm
|
||||
elif kind == "합계":
|
||||
if cur is not None:
|
||||
cur["합계"] = True
|
||||
elif kind == "머리" or (kind == "글" and cut and cur is None):
|
||||
title, sub = val if kind == "머리" else title_of(val)
|
||||
if cur is not None and not cur["줄"] and not cur["제목"]:
|
||||
cur["제목"], cur["가나다"] = title, sub
|
||||
else:
|
||||
cur = new(title, sub)
|
||||
cut = False
|
||||
else:
|
||||
one = mach.get(price_sym(money[val]) or "") if kind == "금액" else None
|
||||
if one and one.get("조합") and not (cur and cur.get("_조합") == _mach_title(one)):
|
||||
cur = new(_mach_title(one), bool(cur and cur["가나다"]))
|
||||
cur["_조합"] = _mach_title(one) # 같은 기계 다음 줄(비목 셋)은 한 항목
|
||||
elif one and cur is not None and "+" in cur["제목"]:
|
||||
cur["제목"] = _mach_title(one)
|
||||
cur = cur if cur is not None else new()
|
||||
if kind == "공식" and cur["줄"] and cur["줄"][-1][0] == "공식":
|
||||
cur["줄"][-1][1].extend(x for x in val if x not in cur["줄"][-1][1])
|
||||
else:
|
||||
cur["줄"].append((kind, list(val) if kind == "공식" else val))
|
||||
keep = []
|
||||
for it in items:
|
||||
flat = re.sub(r"\s+", "", it["제목"])
|
||||
if not it["줄"] and (it["합계"] or not flat or flat in _TOTAL):
|
||||
continue
|
||||
keep.append(it)
|
||||
return keep
|
||||
|
||||
|
||||
def _mach_title(one: dict) -> str:
|
||||
"""조합 기계 항목 제목 — 이은 우리 기계 이름 · 규격 › 가져온 이름 · 규격."""
|
||||
first = (one.get("후보") or [{}])[0] if one.get("키") else {}
|
||||
said = first or {
|
||||
"이름": (one.get("원래") or {}).get("이름"),
|
||||
"규격": (one.get("원래") or {}).get("규격"),
|
||||
}
|
||||
return " ".join(str(said.get(k) or "") for k in ("이름", "규격")).strip()
|
||||
|
||||
|
||||
def _save_item(it: dict, money: list[dict]) -> dict:
|
||||
out: dict[str, Any] = {}
|
||||
if it["제목"]:
|
||||
out["제목"] = it["제목"]
|
||||
if it["가나다"]:
|
||||
out["가나다"] = True
|
||||
lines = []
|
||||
for kind, val in it["줄"]:
|
||||
if kind == "금액":
|
||||
lines.append({"금액": val, "이름": money[val].get("이름") or ""})
|
||||
elif kind == "공식":
|
||||
lines.append({"공식": val})
|
||||
else:
|
||||
lines.append({"글": val})
|
||||
return out | {"줄": lines}
|
||||
|
||||
|
||||
# ── 줄 쌓기 모델 거울(`common_util_line_stack.ts`) ──────────────────────
|
||||
def _names_in(text: Any) -> list[str]:
|
||||
if text is None or str(text).strip() == "":
|
||||
return []
|
||||
try:
|
||||
return md.names(md.parse(text))
|
||||
except md.DocError:
|
||||
return []
|
||||
|
||||
|
||||
def symbols(doc: dict) -> list[str]:
|
||||
out = [md.var_id(v) for v in doc.get("변수") or []]
|
||||
for line in doc.get("식") or []:
|
||||
sid = str(line.get("기호") or "")
|
||||
if sid and sid not in out:
|
||||
out.append(sid)
|
||||
return out
|
||||
|
||||
|
||||
def _is_option(v: dict | None) -> bool:
|
||||
return bool(v and v.get("열쇠") and (v.get("연결") or {}).get("갈래") in ("선택", "입력"))
|
||||
|
||||
|
||||
def uses_of(doc: dict) -> dict[str, list[str]]:
|
||||
out: dict[str, list[str]] = {}
|
||||
|
||||
def add(sid: str, more: list[str]) -> None:
|
||||
now = out.setdefault(sid, [])
|
||||
now += [n for n in more if n not in now]
|
||||
|
||||
for v in doc.get("변수") or []:
|
||||
add(md.var_id(v), md.link_names(v["연결"]) if v.get("연결") else [])
|
||||
for line in doc.get("식") or []:
|
||||
add(str(line.get("기호") or ""), _names_in(line.get("식")))
|
||||
return out
|
||||
|
||||
|
||||
def price_sym(line: dict) -> str | None:
|
||||
text = line.get("단가")
|
||||
if text is None or str(text).strip() == "":
|
||||
return None
|
||||
try:
|
||||
n = md.parse(text)
|
||||
except md.DocError:
|
||||
return None
|
||||
if n[0] == "var":
|
||||
return n[1]
|
||||
if n[0] == "field" and n[1][0] == "var":
|
||||
return n[1][1]
|
||||
return None
|
||||
|
||||
|
||||
def machine_syms(doc: dict) -> set[str]:
|
||||
"""가져온 기계 줄 단가 기호(812-9 `가져온.기계`) — 못 이어 숫자 연결이어도 그 줄 기계(이름 단추 · 고르기)."""
|
||||
got = (doc.get("가져온") or {}).get("기계") if isinstance(doc.get("가져온"), dict) else None
|
||||
return {str(x.get("기호")) for x in got or [] if isinstance(x, dict)}
|
||||
|
||||
|
||||
def owned_of(doc: dict) -> dict[int, str]:
|
||||
count: dict[str, int] = {}
|
||||
for ns in (
|
||||
*(_names_in(x.get("수량")) + _names_in(x.get("단가")) for x in doc.get("금액") or []),
|
||||
*uses_of(doc).values(),
|
||||
*([[doc["결과"]]] if doc.get("결과") else []),
|
||||
):
|
||||
for n in ns:
|
||||
count[n] = count.get(n, 0) + 1
|
||||
vars_ = {md.var_id(v): v for v in doc.get("변수") or []}
|
||||
lines = {str(x.get("기호") or "") for x in doc.get("식") or []}
|
||||
mach = machine_syms(doc)
|
||||
out = {}
|
||||
for i, line in enumerate(doc.get("금액") or []):
|
||||
s = price_sym(line)
|
||||
kind = ((vars_.get(s) or {}).get("연결") or {}).get("갈래") if s else None
|
||||
if (
|
||||
s
|
||||
and (kind in ("요소", "단가산출") or s in mach)
|
||||
and count.get(s) == 1
|
||||
and s not in lines
|
||||
):
|
||||
out[i] = s
|
||||
return out
|
||||
|
||||
|
||||
def hidden_of(doc: dict) -> set[str]:
|
||||
"""차림에 안 넣는 기호 — 옵션 · 금액 줄이 가진 연결(가져온 값 옵션 칸은 공식 묶음에도 보임)."""
|
||||
out = set(owned_of(doc).values())
|
||||
out |= {md.var_id(v) for v in doc.get("변수") or [] if _is_option(v)}
|
||||
return out
|
||||
|
||||
|
||||
def _post_order(doc: dict, roots: list[str], uses: dict[str, list[str]]) -> list[str]:
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
stack: list[tuple[str, int]] = []
|
||||
for start in [*roots, *symbols(doc)]: # 되부름 없이(깊은 식 사슬)
|
||||
if start in seen:
|
||||
continue
|
||||
seen.add(start)
|
||||
stack.append((start, 0))
|
||||
while stack:
|
||||
sid, k = stack.pop()
|
||||
kids = uses.get(sid) or []
|
||||
if k < len(kids):
|
||||
stack.append((sid, k + 1))
|
||||
if kids[k] not in seen:
|
||||
seen.add(kids[k])
|
||||
stack.append((kids[k], 0))
|
||||
else:
|
||||
out.append(sid)
|
||||
return out
|
||||
|
||||
|
||||
def _reach(line: dict, uses: dict[str, list[str]]) -> set[str]:
|
||||
out: set[str] = set()
|
||||
todo = _names_in(line.get("수량")) + _names_in(line.get("단가"))
|
||||
while todo:
|
||||
sid = todo.pop()
|
||||
if sid in out:
|
||||
continue
|
||||
out.add(sid)
|
||||
todo += uses.get(sid) or []
|
||||
return out
|
||||
|
||||
|
||||
def stack_of(doc: dict) -> list[dict]:
|
||||
"""`stackOf` — 저장 차림 + 안 놓인 것은 기본 자리 ·
|
||||
줄 = {꼴 금액 i | 꼴 공식 기호 틀? | 꼴 글 글}."""
|
||||
money = doc.get("금액") or []
|
||||
all_ = set(symbols(doc))
|
||||
hidden = hidden_of(doc)
|
||||
items: list[dict] = []
|
||||
where: dict[int, dict] = {}
|
||||
placed: set[str] = set()
|
||||
saved = doc.get("쌓기") if isinstance(doc.get("쌓기"), list) else []
|
||||
taken: set[int] = set()
|
||||
for s in saved:
|
||||
s = s if isinstance(s, dict) else {}
|
||||
it: dict[str, Any] = {
|
||||
"제목": str(s.get("제목") if s.get("제목") is not None else ""),
|
||||
"줄": [],
|
||||
}
|
||||
if s.get("가나다"):
|
||||
it["가나다"] = True
|
||||
for line in s.get("줄") if isinstance(s.get("줄"), list) else []:
|
||||
if isinstance(line, dict) and "금액" in line:
|
||||
i = int(line["금액"])
|
||||
name = line.get("이름")
|
||||
if not (
|
||||
0 <= i < len(money) and (money[i].get("이름") or "") == name and i not in taken
|
||||
):
|
||||
free = (k for k, m in enumerate(money) if k not in taken)
|
||||
i = next((k for k in free if (money[k].get("이름") or "") == name), -1)
|
||||
if i < 0 or i in taken:
|
||||
continue
|
||||
taken.add(i)
|
||||
where[i] = it
|
||||
it["줄"].append({"꼴": "금액", "i": i})
|
||||
elif isinstance(line, dict) and "공식" in line:
|
||||
got = [
|
||||
x
|
||||
for x in line["공식"] or []
|
||||
if x in all_ and x not in hidden and x not in placed
|
||||
]
|
||||
placed |= set(got)
|
||||
one: dict[str, Any] = {"꼴": "공식", "기호": got}
|
||||
if line.get("틀"):
|
||||
one["틀"] = str(line["틀"])
|
||||
if line.get("틀꼴"):
|
||||
one["틀꼴"] = str(line["틀꼴"])
|
||||
it["줄"].append(one)
|
||||
elif isinstance(line, dict) and "글" in line:
|
||||
it["줄"].append(
|
||||
{"꼴": "글", "글": str(line["글"] if line["글"] is not None else "")}
|
||||
)
|
||||
items.append(it)
|
||||
for i, m in enumerate(money):
|
||||
if i in taken:
|
||||
continue
|
||||
title = str(m.get("묶음") if m.get("묶음") is not None else "")
|
||||
prev = where.get(i - 1)
|
||||
if prev is not None and prev["제목"] == title:
|
||||
it = prev
|
||||
at = next(k for k, x in enumerate(prev["줄"]) if x["꼴"] == "금액" and x["i"] == i - 1)
|
||||
prev["줄"].insert(at + 1, {"꼴": "금액", "i": i})
|
||||
else:
|
||||
it = next((x for x in items if x["제목"] == title), None) if saved else None
|
||||
if it is None:
|
||||
it = {"제목": title, "줄": []}
|
||||
items.append(it)
|
||||
it["줄"].append({"꼴": "금액", "i": i})
|
||||
taken.add(i)
|
||||
where[i] = it
|
||||
uses = uses_of(doc)
|
||||
reach = [_reach(m, uses) for m in money]
|
||||
for sid in _post_order(doc, [doc["결과"]] if doc.get("결과") else [], uses):
|
||||
if sid not in all_ or sid in hidden or sid in placed:
|
||||
continue
|
||||
i = next((k for k, r in enumerate(reach) if sid in r), -1)
|
||||
it = where.get(i) if i >= 0 else (items[0] if items else None)
|
||||
if it is None:
|
||||
it = {"제목": "", "줄": []}
|
||||
items.append(it)
|
||||
block = next((x for x in it["줄"] if x["꼴"] == "공식"), None)
|
||||
if block is None:
|
||||
block = {"꼴": "공식", "기호": []}
|
||||
it["줄"].insert(0, block)
|
||||
block["기호"].append(sid)
|
||||
placed.add(sid)
|
||||
return items
|
||||
|
||||
|
||||
def save_of(items: list[dict], money: list[dict]) -> list[dict]:
|
||||
"""`saveOf`(차례 그대로)."""
|
||||
out = []
|
||||
for it in items:
|
||||
one: dict[str, Any] = {}
|
||||
if it["제목"]:
|
||||
one["제목"] = it["제목"]
|
||||
if it.get("가나다"):
|
||||
one["가나다"] = True
|
||||
lines = []
|
||||
for x in it["줄"]:
|
||||
if x["꼴"] == "금액":
|
||||
lines.append({"금액": x["i"], "이름": money[x["i"]].get("이름") or ""})
|
||||
elif x["꼴"] == "글":
|
||||
lines.append({"글": x["글"]})
|
||||
else:
|
||||
lines.append(
|
||||
{"공식": list(x["기호"])}
|
||||
| ({"틀": x["틀"]} if x.get("틀") else {})
|
||||
| ({"틀꼴": x["틀꼴"]} if x.get("틀꼴") else {})
|
||||
)
|
||||
out.append(one | {"줄": lines})
|
||||
return out
|
||||
@@ -4,6 +4,7 @@
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/preview 미리 셈(층 쓰기) {문서?, 끊음?, 할일?}
|
||||
→ {…, 전, 후, 바뀐자원?, 버림?}
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/save 저장(층 쓰기) {문서?, 끊음?, 할일?, 판} → 읽기 꼴
|
||||
· 가져온 기계 줄에 고른 EQ = 이 층 별칭 장부(812-9)
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/match {할일 찾기|바꾸기|되돌리기, 판?}(층 쓰기 · 810-4)
|
||||
→ {연결후보?, 판}(바꾸기 = 확정만 · 아니면 409)
|
||||
⚠ 등록은 `main.py`(로그인만 · 층 권한은 길 안에서).
|
||||
@@ -18,6 +19,7 @@ from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
|
||||
|
||||
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Sangeun Doc"])
|
||||
@@ -84,4 +86,8 @@ def save_doc(
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, path.save, key, body.model_dump(exclude_none=True))
|
||||
got = _call(folder, path.save, key, body.model_dump(exclude_none=True))
|
||||
if body.문서 is not None: # 812-9 — 가져온 기계 줄에 고른 EQ = 이 층 별칭 장부 줄
|
||||
who = {"id": session.get("user_id"), "이름": session.get("name")}
|
||||
_call(folder, mfind.learn, body.문서, who)
|
||||
return got
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
고르면 지금 잇기 길 `{노임|기계|자재|유가: 키}`).
|
||||
자재 = `M02_Hopyo_Material.candidates` 점수(이름 글자 묶음 · 규격) · 단가 = M01 자재 값(엑셀 줄 단위)
|
||||
노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값
|
||||
기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 괄호 낱말 어긋남 깎음 ·
|
||||
단가 없음(기계경비는 로직 셈) · `machine` = 하나로 가름(778-1)
|
||||
기계 = 기계 찾기(812-9 `M02_Machine_Find` — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 규격) 후보 ·
|
||||
단가 없음(기계경비는 로직 셈) · `machine` = 하나로 정할 때만
|
||||
유가(785-3) = MO 이름에 글 이름이 듦(경유 ⊂ 자동차용경유) · 전국 먼저 · 단가 = MO 값
|
||||
차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 단위 줄 단가 · 모르면 점수만).
|
||||
서버 단독 · 읽기만.
|
||||
@@ -21,18 +21,11 @@ from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc, specnums
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import nfkc
|
||||
|
||||
TOP = 3
|
||||
POOL = 5
|
||||
# 괄호 낱말(778-1) — 글에 있으면 EQ 이름에도 있어야 함 · 부속 = 글에 없으면 본 장비 아님
|
||||
FORMS = (
|
||||
("타이어", ("타이어",)),
|
||||
("무한", ("무한궤도",)),
|
||||
("습지", ("습지",)),
|
||||
("트럭탑재", ("트럭탑재",)),
|
||||
)
|
||||
PARTS = ("자동덮개", "부착용", "콤팩터", "습지")
|
||||
|
||||
|
||||
def prices(block: dict[str, Any] | None) -> dict[str, float]:
|
||||
@@ -125,57 +118,24 @@ def oils(name: str) -> list[dict[str, Any]]:
|
||||
]
|
||||
|
||||
|
||||
def _form(text: str, name: Any) -> tuple[bool, bool]:
|
||||
"""괄호 낱말(778-1) — (글 형식 낱말이 EQ 이름에 다 있음, EQ 이름에 글에 없는 부속 있음)."""
|
||||
t, n = nfkc(text), nfkc(name)
|
||||
ok = all(any(w in n for w in ws) for k, ws in FORMS if k in t)
|
||||
return ok, any(w in n and w not in t for w in PARTS)
|
||||
def _eq(one: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = m01.loaded()[1].numbers.get("EQ", {}).get(str(one.get("원문번호")), {})
|
||||
unit = (raw.get("단위") or {}).get("내용시간") if isinstance(raw.get("단위"), dict) else ""
|
||||
return {"키": one["키"], "갈래": "기계", "이름": one["이름"], "규격": one.get("규격") or "",
|
||||
"단위": unit or "시간", "단가": None} # fmt: skip
|
||||
|
||||
|
||||
def narrow(text: str, rows: list[tuple[str, Any]]) -> list[str]:
|
||||
"""EQ `[(키, 이름)]` → 괄호 낱말로 가른 키 — 형식 다름 뺌 · 부속은 남는 것 있을 때만 뺌."""
|
||||
keep = [(c, _form(text, n)[1]) for c, n in rows if _form(text, n)[0]]
|
||||
return [c for c, part in keep if not part] or [c for c, _ in keep]
|
||||
def machine(text: str, *_: Any) -> dict | None:
|
||||
"""기계 글(이름 규격) → EQ 한 줄 — 기계 찾기(812-9 `M02_Machine_Find`)가 하나로 정할 때만."""
|
||||
got = mfind.find(text)
|
||||
if got["판정"] not in (mfind.LINKED, mfind.ALIAS):
|
||||
return None
|
||||
return m01.loaded()[1].numbers.get("EQ", {}).get(str(got["후보"][0]["원문번호"]))
|
||||
|
||||
|
||||
def machine(text: str, kinds: set | None = None, nums: set | None = None) -> dict | None:
|
||||
"""기계 글 → EQ 한 줄 — 갈래 겹침 + 규격 수 같음 · 괄호 낱말로 하나일 때만."""
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
rows = {
|
||||
str(r.get("키")): r
|
||||
for r in m01.loaded()[1].numbers.get("EQ", {}).values()
|
||||
if nums
|
||||
and kinds & mtypes(f"{r.get('상세구분')} {r.get('이름')}")
|
||||
and nums == set(specnums(r.get("규격")))
|
||||
}
|
||||
got = narrow(text, [(k, r.get("이름")) for k, r in rows.items()])
|
||||
return rows[got[0]] if len(got) == 1 else None
|
||||
|
||||
|
||||
def machines(text: str, kinds: set | None = None, nums: set | None = None) -> list[dict]:
|
||||
"""기계 글(이름 규격) → EQ 후보 — 갈래 · 규격 수 없으면 글에서 읽음 · 괄호 낱말 어긋남 깎음."""
|
||||
whole = m01.loaded()[1]
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
mine = hm.words(text)
|
||||
rows = []
|
||||
for row in whole.numbers.get("EQ", {}).values():
|
||||
types = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
|
||||
if not kinds & types:
|
||||
continue
|
||||
spec = set(specnums(row.get("규격")))
|
||||
score = len(kinds & types) / len(kinds | types)
|
||||
score += 1.0 if nums and nums == spec else 0.5 if nums & spec else 0.0
|
||||
score += 0.3 * len(mine & hm.words(row.get("이름")))
|
||||
ok, part = _form(text, row.get("이름"))
|
||||
score -= (0.0 if ok else 1.0) + (0.5 if part else 0.0)
|
||||
one = {"키": str(row.get("키")), "갈래": "기계", "이름": row.get("이름")}
|
||||
unit = (row.get("단위") or {}).get("내용시간") if isinstance(row.get("단위"), dict) else ""
|
||||
rows.append(
|
||||
(score, {**one, "규격": row.get("규격") or "", "단위": unit or "시간", "단가": None})
|
||||
)
|
||||
return _order(rows, None)
|
||||
def machines(text: str, *_: Any) -> list[dict]:
|
||||
"""기계 글 → EQ 후보(보여주기만 · 812-9) — 이음 · 후보 여럿 = 그것 · 못 찾음 = 같은 기종(가까운 규격 먼저)."""
|
||||
return [_eq(one) for one in mfind.find(text)["후보"][:TOP]]
|
||||
|
||||
|
||||
def lost(one: dict[str, Any], want: float | None = None) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -20,13 +20,12 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, specnums
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
_BASE = re.compile(r"(?<!주)(노무비|재료비|경비|인건비)\s*의")
|
||||
@@ -93,13 +92,9 @@ class Linker:
|
||||
pair = (str(row.get("키")), _d(row.get("값")))
|
||||
self.labor.setdefault(str(row.get("이름") or ""), []).append(pair)
|
||||
self.machines = [
|
||||
(str(row.get("키")), mtypes(f"{row.get('상세구분')} {row.get('이름')}"),
|
||||
set(specnums(row.get("규격"))), mm_plain(row.get("이름")), mm_plain(row.get("규격")))
|
||||
(str(row.get("키")), mm_plain(row.get("이름")), mm_plain(row.get("규격")))
|
||||
for row in self.whole.numbers.get("EQ", {}).values()
|
||||
] # fmt: skip
|
||||
self.eq_names = {
|
||||
str(r.get("키")): r.get("이름") for r in self.whole.numbers.get("EQ", {}).values()
|
||||
}
|
||||
]
|
||||
self.near = 0 # 못 이은 이음 후보 수
|
||||
|
||||
def link(self, line: dict[str, Any], price: dict[str, Decimal]) -> dict[str, str | None]:
|
||||
@@ -117,14 +112,12 @@ class Linker:
|
||||
return out
|
||||
|
||||
def _eqs(self, line: dict[str, Any]) -> list[str]:
|
||||
"""기계 줄 → EQ 후보 — 이름 · 규격 글 같음 › 갈래 + 규격 수 · 괄호 낱말."""
|
||||
"""기계 줄 → EQ 후보 — 이름 · 규격 글 같음 › 기계 찾기(812-9 `M02_Machine_Find`)."""
|
||||
name, spec = mm_plain(line.get("이름")), mm_plain(line.get("규격"))
|
||||
same = [c for c, _, _, n, s in self.machines if n == name and s == spec]
|
||||
same = [c for c, n, s in self.machines if n == name and s == spec]
|
||||
if len(same) != 1:
|
||||
text = f"{line.get('이름') or ''} {line.get('규격') or ''}"
|
||||
kinds, nums = mtypes(text), set(specnums(text))
|
||||
same = [c for c, t, s, _, _ in self.machines if kinds & t and nums and nums == s]
|
||||
same = elem.narrow(text, [(c, self.eq_names.get(c)) for c in same])
|
||||
got = mfind.find(line.get("이름"), line.get("규격"))
|
||||
same = [] if got["판정"] == mfind.NONE else [c["키"] for c in got["후보"]]
|
||||
return same
|
||||
|
||||
def candidates(self, line: dict[str, Any]) -> list[str]:
|
||||
|
||||
@@ -82,13 +82,9 @@ def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
grid = hopyo_grid.read(name, data, parsed)
|
||||
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
|
||||
grid = None
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
|
||||
cannot = from_hopyo.keep(
|
||||
store.root(), row["키"], name, data
|
||||
) # 산근 시트(793-3b 가져온 식 열기)
|
||||
return (
|
||||
{**row, "읽기불가": cannot} if cannot else row
|
||||
) # 797-1 가져온 식 못 씀 사유 → 화면 토스트
|
||||
# 산근 시트(793-3b 가져온 식 열기) = HP 를 목록에 보이기 전에 보관(814-1) · 못 씀 사유 → 화면 토스트(797-1)
|
||||
keep = lambda key: from_hopyo.keep(store.root(), key, name, data) # noqa: E731
|
||||
return hopyo_store.save(store.root(), name, parsed, who, grid, mark, keep)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo")
|
||||
|
||||
@@ -20,6 +20,7 @@ from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as est
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
mm, mp = hm.mm, hm.mp
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
@@ -65,6 +66,11 @@ def _rows(table: str) -> dict[str, dict[str, Any]]:
|
||||
|
||||
def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""갈래 하나 → M01 고른 것 후보(LB · MT · MO · EQ · FX) — 유일 짝이면 하나 · 같은 글 한 번."""
|
||||
if kind == "기계": # 812-9 기계 찾기 — 별칭 장부가 바뀌므로 lru 밖(찾기가 기계표를 기억)
|
||||
got = mfind.find(line.get("이름"), line.get("규격"))
|
||||
rows = [] if got["판정"] == mfind.NONE else got["후보"]
|
||||
unit = str(line.get("단위") or "") or "hr"
|
||||
return [pick("기계", x["키"], x["이름"], x.get("규격"), unit) for x in rows]
|
||||
text = tuple(str(line.get(k) or "") for k in ("이름", "규격", "단위"))
|
||||
return copy.deepcopy(_m01_picks(kind, *text, id(m01.loaded()[1])))
|
||||
|
||||
@@ -80,13 +86,6 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
|
||||
]
|
||||
got = elem.labor(str(name or ""))
|
||||
return [pick("인력", x["키"], x["이름"], "", "인", 단가=x["단가"]) for x in got]
|
||||
if kind == "기계":
|
||||
text = f"{name} {spec}"
|
||||
one = elem.machine(text)
|
||||
rows = [one] if one else []
|
||||
got = [{"키": r["키"], "이름": r["이름"], "규격": r.get("규격")} for r in rows]
|
||||
got = got or elem.machines(text)
|
||||
return [pick("기계", x["키"], x["이름"], x.get("규격"), unit or "hr") for x in got]
|
||||
if kind == "유가":
|
||||
return [
|
||||
pick("유가", x["키"], x["이름"], x["규격"], x["단위"], 단가=x["단가"])
|
||||
|
||||
@@ -124,8 +124,10 @@ def save(
|
||||
who: Any,
|
||||
grid: dict[str, Any] | None = None,
|
||||
mark: str | None = None,
|
||||
first: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄 · `mark` = 올린 엑셀 지문."""
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄 · `mark` = 올린 엑셀 지문 ·
|
||||
`first(키)` = HP 를 목록에 보이기 전에 할 일(산근 보관 — 올린 직후 409 막음 · 814-1) · 돌려준 값 = 답 `읽기불가`."""
|
||||
with _LOCK:
|
||||
have = [
|
||||
int(m.group(1)) for p in _dir(folder).glob("HP-*.json") if (m := _KEY.match(p.stem))
|
||||
@@ -145,9 +147,14 @@ def save(
|
||||
if grid is not None:
|
||||
book = {**grid["책"], "열": key}
|
||||
atomic_write_json(_grid_path(folder, key), {**grid, "책": book})
|
||||
cannot = first(key) if first else None
|
||||
atomic_write_json(_path(folder, key), doc)
|
||||
_put_done(folder, log)
|
||||
return {**brief(doc), **({"지문": mark} if mark else {})}
|
||||
return {
|
||||
**brief(doc),
|
||||
**({"지문": mark} if mark else {}),
|
||||
**({"읽기불가": cannot} if cannot else {}),
|
||||
}
|
||||
|
||||
|
||||
def grid(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
|
||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from M01_MasterData import M01_MasterData_Parts as parts
|
||||
from M01_MasterData import M01_MasterData_Router as m01
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
@@ -72,3 +73,9 @@ def post_text(body: m01.CalcBody) -> dict:
|
||||
def get_machine_hourly(key: str) -> dict:
|
||||
"""기계(EQ) 한 시간 값 노 · 재 · 경(736-3 · [항목 추가] 기계 고정 줄 단가)."""
|
||||
return machine.hourly(key)
|
||||
|
||||
|
||||
@router.get("/machine-parts")
|
||||
def get_machine_parts(no: str) -> dict:
|
||||
"""줄 쌓기 기계 넣기(813-2) — 기계 원문번호 → 그 기종 작업량 틀(문서째)."""
|
||||
return m01._call(parts.of_machine, no)
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
"""M02 기계 찾기(PLAN 812-9 · 요구 718 · 719) — 가져온 기계 글(이름 · 규격) → 우리 EQ · 서버 단독.
|
||||
|
||||
과녁 = M01 기계표 전체(EQ) · 부르는 곳 = 불러오기 이음(`M02_Hopyo_Row.m01_picks` · `M02_San_Emit._m01`) ·
|
||||
일위대가 호표(`M02_Hopyo_Elem.machine(s)` · `M02_Hopyo_Fixed._eqs`) · 연결 찾기(`M02_Doc_Match_Note`).
|
||||
차례:
|
||||
1. 별칭 장부(`M02_Hopyo_Ledger` · 마스터 층 + 지금 층 · 짝 갈래 기계) — 이름 + 규격 열쇠 = 그 EQ ·
|
||||
이름만(규격 빈) 열쇠 = 그 EQ 이름으로 바꿔 2 로
|
||||
2. 기종 = EQ 이름(괄호 뗌) · 괄호 붙임 · 상세구분과 같은 글 › 그 글을 품은 EQ(표기 정규화 `norm`)
|
||||
3. 괄호 낱말 — 기계표 괄호에 있는 낱말(무한궤도 · 타이어 · 자주식 …)만 거름 · 표에 없는 낱말(발파 · 리핑)은 안 봄
|
||||
4. 규격 = 수 + 단위(범위 · 여럿) 모두 EQ 규격에 같은 값(단위 한쪽 없음 = 봄) › 없으면 EQ 범위 안
|
||||
하나 = 이음 · 여럿 = 「후보 여럿」 · 0 = 「못 찾음」(가까운 규격으로 대신 잇지 않음)
|
||||
조합 기계(「+」 · 괄호 속 다른 기종 · 「…조합」 · 「덤프트럭(자동덮개)」) = `parts` 로 갈라 각각 찾음 ·
|
||||
규격 없는 쪽은 앞 쪽 규격.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import unicodedata
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
# 소리만 다른 표기 → 한 글(양쪽에 같이 씀 · 차례대로)
|
||||
SOUND = (
|
||||
("로우더", "로더"), ("로울러", "롤러"), ("브레이카", "브레이커"), ("브레카", "브레이커"),
|
||||
("굴삭기", "굴착기"), ("굴삭", "굴착"), ("불도우저", "불도저"), ("램머", "래머"),
|
||||
("크로울러", "크롤러"), ("콤프레샤", "공기압축기"), ("컴프레셔", "공기압축기"),
|
||||
("컴프레서", "공기압축기"), ("컴팩터", "콤팩터"), ("타이어형", "타이어"), ("젯트", "제트"),
|
||||
("후렉시블", "플렉시블"), ("스프레야", "스프레이어"), ("코어", "코아"),
|
||||
) # fmt: skip
|
||||
_UNIT = (
|
||||
("m3/min", "m3/min"), ("m3/분", "m3/min"), ("m3/hr", "m3/hr"), ("m3/h", "m3/hr"),
|
||||
("km/hr", "km/hr"), ("km/h", "km/hr"), ("l/min", "l/min"), ("t/hr", "t/hr"),
|
||||
("kg/cm2", "kg/cm2"), ("kg/min", "kg/min"), ("m3", "m3"), ("톤용", "ton"), ("ton", "ton"),
|
||||
("톤", "ton"), ("t", "ton"), ("kw", "kw"), ("mm", "mm"), ("cm", "cm"), ("kg", "kg"),
|
||||
("km", "km"), ("m", "m"), ("l", "l"), ("hp", "hp"), ("amp", "a"), ("cc", "cc"), ('"', "in"),
|
||||
("inch", "in"), ("인치", "in"),
|
||||
) # fmt: skip
|
||||
_NUM = r"\d+(?:\.\d+)?"
|
||||
_TOKEN = re.compile(
|
||||
rf"({_NUM})(?:\s*([~∼\-])\s*({_NUM}))?\s*("
|
||||
+ "|".join(re.escape(u) for u, _ in _UNIT)
|
||||
+ r")?(?![a-z])"
|
||||
)
|
||||
_PAREN = re.compile(r"[(\[]([^()\[\]]*)[)\]]")
|
||||
_EXTRA = re.compile(
|
||||
r"[:,]?\s*\(?할증율?[^)]*\)?"
|
||||
) # 「0.2㎥:할증120%」 · 「(할증125%)」 = 율(규격 아님)
|
||||
NOTE = "참고 · "
|
||||
LINKED, ALIAS, MANY, NONE = "이어짐", "별칭", "후보 여럿", "못 찾음"
|
||||
SHOW = 8 # 후보 글 · 목록 상한
|
||||
|
||||
|
||||
def norm(text: Any) -> str:
|
||||
"""표기 정규화 — NFKC · 작은 글자 · 빈칸 뺌 · 소리 표기 한 글."""
|
||||
t = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or "")).lower())
|
||||
for old, new in SOUND:
|
||||
t = t.replace(old, new)
|
||||
return t
|
||||
|
||||
|
||||
def _clean(text: Any) -> str:
|
||||
t = unicodedata.normalize("NFKC", str(text or "")).lower()
|
||||
t = _EXTRA.sub(" ", t)
|
||||
return re.sub(r"(?<=\d),(?=\d{3}(?!\d))", "", t) # 천 자리 쉼표
|
||||
|
||||
|
||||
def tokens(text: Any) -> list[tuple[float, float, str | None]]:
|
||||
"""규격 글 → [(아래, 위, 단위)] — 범위 「10~12」 · 여럿 「32m,80~95㎥/hr」 · 단위 없음 = None."""
|
||||
out = []
|
||||
for m in _TOKEN.finditer(_clean(text)):
|
||||
lo, sep, hi, unit = float(m.group(1)), m.group(2), m.group(3), m.group(4)
|
||||
top = float(hi) if hi else lo
|
||||
if sep == "-" and top <= lo: # 「50-2.6m」 = 범위 아님 — 앞 수만
|
||||
top = lo
|
||||
canon = dict(_UNIT).get(unit) if unit else None
|
||||
if canon == "in": # 인치 = ㎝(×2.54 · 우리 표는 ㎝)
|
||||
lo, top, canon = round(lo * 2.54, 6), round(top * 2.54, 6), "cm"
|
||||
out.append((lo, top, canon))
|
||||
return out
|
||||
|
||||
|
||||
def _unit_ok(a: str | None, b: str | None) -> bool:
|
||||
return a is None or b is None or a == b or {a, b} <= {"ton", "t/hr"}
|
||||
|
||||
|
||||
def spec_fit(mine: list, theirs: list) -> str | None:
|
||||
"""가져온 규격 수가 EQ 규격에 다 있음 = 「같음」 · EQ 범위 안 = 「안」 · 아니면 None."""
|
||||
if not mine:
|
||||
return "같음" if not theirs else None
|
||||
same = all(
|
||||
any(abs(a - c) < 1e-9 and abs(b - d) < 1e-9 and _unit_ok(u, w) for c, d, w in theirs)
|
||||
for a, b, u in mine
|
||||
)
|
||||
if same:
|
||||
return "같음"
|
||||
inside = all(
|
||||
any(c - 1e-9 <= a and b <= d + 1e-9 and c < d and _unit_ok(u, w) for c, d, w in theirs)
|
||||
for a, b, u in mine
|
||||
)
|
||||
return "안" if inside else None
|
||||
|
||||
|
||||
def _words(inner: str) -> list[str]:
|
||||
return [norm(w) for w in re.split(r"[\s,;·/]+", inner) if w.strip()]
|
||||
|
||||
|
||||
def split_name(text: Any) -> tuple[str, list[list[str]], list[str]]:
|
||||
"""이름(+규격) 글 → (기종 글 정규화, 괄호 낱말 묶음들, 규격 조각들) — 괄호 속 수 = 규격."""
|
||||
raw = _clean(text)
|
||||
groups, specs = [], []
|
||||
for inner in _PAREN.findall(raw):
|
||||
bits = [b for b in re.split(r"[,;]", inner) if b.strip()]
|
||||
specs += [b for b in bits if re.search(r"\d", b)]
|
||||
groups.append([w for b in bits if not re.search(r"\d", b) for w in _words(b)])
|
||||
base = _PAREN.sub(" ", raw)
|
||||
specs += [m.group(0) for m in _TOKEN.finditer(base)]
|
||||
base = _TOKEN.sub(" ", base)
|
||||
return norm(base), groups, specs
|
||||
|
||||
|
||||
# ── 기계표 ────────────────────────────────────────────────────────────
|
||||
class Row:
|
||||
__slots__ = ("raw", "key", "base", "detail", "joined", "full", "words", "spec", "said")
|
||||
|
||||
def __init__(self, r: dict) -> None:
|
||||
name = str(r.get("이름") or "")
|
||||
self.raw, self.key = r, str(r.get("키"))
|
||||
inner = _PAREN.findall(unicodedata.normalize("NFKC", name))
|
||||
self.words = {w for x in inner for w in _words(x)}
|
||||
self.base = norm(_PAREN.sub("", unicodedata.normalize("NFKC", name)))
|
||||
self.detail = norm(r.get("상세구분"))
|
||||
self.joined = {w + self.base for w in self.words} | {self.base + w for w in self.words}
|
||||
self.full = norm(name)
|
||||
self.spec = tokens(r.get("규격"))
|
||||
self.said = norm(
|
||||
_TOKEN.sub(" ", _clean(r.get("규격")))
|
||||
) # 규격 글 낱말(「더블」 · 「엔진식」)
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _table(_whole: int) -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||||
rows = [Row(r) for r in m01.loaded()[1].numbers.get("EQ", {}).values()]
|
||||
words = frozenset(w for r in rows for w in r.words)
|
||||
return rows, words, {r.key: r for r in rows}
|
||||
|
||||
|
||||
_SEEN: list = [0.0, None] # (때, 표) — M01 판 확인(파일 도장)은 1 초에 한 번
|
||||
|
||||
|
||||
def table() -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||||
now = time.monotonic()
|
||||
if _SEEN[1] is None or now - _SEEN[0] > 1.0:
|
||||
_SEEN[:] = [now, _table(id(m01.loaded()[1]))]
|
||||
return _SEEN[1]
|
||||
|
||||
|
||||
def by_code(code: Any) -> str | None:
|
||||
"""원문번호 → EQ 키."""
|
||||
want = str(code or "").strip("'\"")
|
||||
return next((r.key for r in table()[0] if r.raw.get("원문번호") == want), None)
|
||||
|
||||
|
||||
def kind_rows(base: str) -> list[Row]:
|
||||
"""기종 글 → EQ 줄 — 이름 · 괄호 붙임 같음 › 상세구분 같음 › 그 글을 품음(첫 층만)."""
|
||||
if len(base) < 2:
|
||||
return []
|
||||
rows = table()[0]
|
||||
for test in (
|
||||
lambda r: base == r.base or base in r.joined,
|
||||
lambda r: base == r.detail,
|
||||
lambda r: base in r.full or base in r.detail,
|
||||
):
|
||||
got = [r for r in rows if test(r)]
|
||||
if got:
|
||||
return got
|
||||
return []
|
||||
|
||||
|
||||
def _kind_word(word: str, base: str) -> str | None:
|
||||
"""괄호 낱말이 다른 기종(조합)이면 그 기종 글 — 「부착용집게」 · 「브레이커조합」 · 「자동덮개」."""
|
||||
w = word.removesuffix("조합")
|
||||
if len(w) < 2 or w == base:
|
||||
return None
|
||||
rows = table()[0]
|
||||
if any(w in (r.base, r.detail) for r in rows):
|
||||
return w
|
||||
if any(r.base.startswith(base + w) for r in rows):
|
||||
return base + w
|
||||
return None
|
||||
|
||||
|
||||
# ── 조합 기계 ─────────────────────────────────────────────────────────
|
||||
def parts(name: Any, spec: Any = "") -> list[tuple[str, str]]:
|
||||
"""기계 글 → [(이름, 규격)] — 「+」 · 괄호 속 다른 기종이면 둘 이상 · 규격 없는 쪽 = 앞 쪽 규격 ·
|
||||
하나면 받은 그대로."""
|
||||
text = f"{name or ''} {spec or ''}".strip()
|
||||
known = table()[1]
|
||||
out: list[tuple[str, str]] = []
|
||||
for piece in [p for p in text.split("+") if p.strip()] or [text]:
|
||||
main, base, extra = _clean(piece), split_name(piece)[0], []
|
||||
for m in _PAREN.finditer(_clean(piece)):
|
||||
bits = [b for b in re.split(r"[,;]", m.group(1)) if b.strip()]
|
||||
words = [w for b in bits if not re.search(r"\d", b) for w in _words(b)]
|
||||
kind = next((k for w in words if w not in known and (k := _kind_word(w, base))), None)
|
||||
if kind:
|
||||
main = main.replace(m.group(0), " ")
|
||||
extra.append((kind, ",".join(b for b in bits if re.search(r"\d", b))))
|
||||
bare = _TOKEN.sub(
|
||||
" ", _PAREN.sub(lambda m: " " if re.search(r"\d", m.group(1)) else m.group(0), main)
|
||||
)
|
||||
own = " ".join(split_name(main)[2])
|
||||
out += [(" ".join(bare.split()).strip(" ,"), own), *extra]
|
||||
if len(out) < 2:
|
||||
return [(str(name or ""), str(spec or ""))]
|
||||
first = next((s for _, s in out if s), "")
|
||||
return [(n, s or first) for n, s in out]
|
||||
|
||||
|
||||
# ── 별칭 장부 ─────────────────────────────────────────────────────────
|
||||
def _alias_rows() -> dict[tuple[str, str], list[str]]:
|
||||
"""(이름, 규격) 정규화 → EQ 키들 — 마스터 층 + 지금 층 별칭 장부 기계 짝."""
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
here, top = Path(store.root()), Path(store.FOLDER)
|
||||
folders = [here] + ([top] if here != top else [])
|
||||
files = [(f, hopyo_store._dir(f) / f"{hopyo_store.MEMORY}.json") for f in folders]
|
||||
stamp = tuple((str(f), p.stat().st_mtime_ns if p.is_file() else 0) for f, p in files)
|
||||
return _alias_read(stamp)
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _alias_read(stamp: tuple) -> dict[tuple[str, str], list[str]]:
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
|
||||
out: dict[tuple[str, str], list[str]] = {}
|
||||
for folder, _ in stamp:
|
||||
for row in ledger.read(folder).values():
|
||||
pairs = row.get("짝") or []
|
||||
eqs = [str(p["키"]) for p in pairs if p.get("갈래") == "기계" and p.get("키")]
|
||||
if row.get("갈래") != "짝" or not eqs:
|
||||
continue
|
||||
origin = row.get("원래") or {}
|
||||
out.setdefault((norm(origin.get("품명")), norm(_clean(origin.get("규격")))), eqs)
|
||||
return out
|
||||
|
||||
|
||||
def alias(name: Any, spec: Any) -> list[str]:
|
||||
"""별칭 장부 기계 짝 — 이름 + 규격 열쇠(규격 속 할증 글은 뺌) · 없으면 []."""
|
||||
return _alias_rows().get((norm(name), norm(_clean(spec))), [])
|
||||
|
||||
|
||||
# ── 찾기 ──────────────────────────────────────────────────────────────
|
||||
def _brief(r: Row) -> dict[str, Any]:
|
||||
raw = r.raw
|
||||
return {"키": r.key, "이름": raw.get("이름"), "규격": raw.get("규격") or "",
|
||||
"원문번호": raw.get("원문번호")} # fmt: skip
|
||||
|
||||
|
||||
def _query(rows: list[Row], base: str) -> str:
|
||||
names = {str(r.raw.get("이름")) for r in rows}
|
||||
if len(names) == 1:
|
||||
return names.pop()
|
||||
details = {str(r.raw.get("상세구분")) for r in rows}
|
||||
return details.pop() if len(details) == 1 else base
|
||||
|
||||
|
||||
def _ans(kind: str, rows: list[Row], query: str, why: str = "") -> dict[str, Any]:
|
||||
key = rows[0].key if kind in (LINKED, ALIAS) else None
|
||||
return {"판정": kind, "키": key, "후보": [_brief(r) for r in rows[:SHOW]], "찾기어": query,
|
||||
"까닭": why, "수": len(rows)} # fmt: skip
|
||||
|
||||
|
||||
def find(name: Any, spec: Any = "") -> dict[str, Any]:
|
||||
"""기계 하나(조합 아님) → `{판정, 키, 후보, 찾기어, 까닭, 수}`."""
|
||||
rows_by = table()[2]
|
||||
hit = alias(name, spec)
|
||||
if len(hit) == 1 and hit[0] in rows_by:
|
||||
r = rows_by[hit[0]]
|
||||
return _ans(ALIAS, [r], str(r.raw.get("이름")))
|
||||
base = split_name(name)[0]
|
||||
_, groups, specs = split_name(f"{name or ''} {spec or ''}") # 규격 괄호 낱말 「3.6m(일반용)」
|
||||
groups.append(
|
||||
_words(_TOKEN.sub(" ", _PAREN.sub(" ", _clean(spec))))
|
||||
) # 규격 글 낱말 「탑승유압식」
|
||||
named = alias(name, "")
|
||||
if len(named) == 1 and named[0] in rows_by: # 이름만 별칭 = 그 EQ 기종으로
|
||||
base = rows_by[named[0]].base
|
||||
mine = tokens(" ".join(specs))
|
||||
kinds = kind_rows(base)
|
||||
known = table()[1]
|
||||
rows = kinds
|
||||
for w in (w for g in groups for w in g):
|
||||
if w in known: # 기계표 괄호 낱말 = 꼭 맞아야 함
|
||||
rows = [r for r in rows if w in r.full]
|
||||
elif len(w) > 1 and (kept := [r for r in rows if w in r.said]): # 규격 글 낱말 = 좁힐 때만
|
||||
rows = kept
|
||||
if not mine: # 규격 수 없음 = 그 기종 전부(괄호 · 규격 낱말로 거른 것)
|
||||
got = rows
|
||||
else:
|
||||
same = [r for r in rows if spec_fit(mine, r.spec) == "같음"]
|
||||
got = same or [r for r in rows if spec_fit(mine, r.spec) == "안"]
|
||||
if len(got) == 1: # 이름만 별칭으로 기종을 정했으면 「별칭」
|
||||
return _ans(ALIAS if len(named) == 1 else LINKED, got, str(got[0].raw.get("이름")))
|
||||
if got:
|
||||
return _ans(MANY, got, _query(got, base), f"후보 {len(got)}")
|
||||
if not kinds:
|
||||
return _ans(NONE, [], base, "기종 모름 — 별칭")
|
||||
why = "우리 표에 없음(규격)" if rows else "우리 표에 없음(괄호 낱말)"
|
||||
return _ans(
|
||||
NONE, sorted(rows or kinds, key=lambda r: _gap(mine, r.spec)), _query(kinds, base), why
|
||||
)
|
||||
|
||||
|
||||
def _gap(mine: list, theirs: list) -> float:
|
||||
"""가까운 규격 차례(보여주기만) — 첫 수끼리 비."""
|
||||
if not mine or not theirs or mine[0][0] <= 0 or theirs[0][0] <= 0:
|
||||
return math.inf
|
||||
return abs(math.log(mine[0][0] / theirs[0][0]))
|
||||
|
||||
|
||||
def find_all(name: Any, spec: Any = "") -> list[dict[str, Any]]:
|
||||
"""기계 글 → 부분마다 `find` + {이름, 규격}(조합 = 둘 이상) · 글 통째 별칭(짝 여럿)이 먼저."""
|
||||
whole = alias(name, spec)
|
||||
rows_by = table()[2]
|
||||
if len(whole) > 1 and all(k in rows_by for k in whole):
|
||||
made = [_ans(ALIAS, [rows_by[k]], str(rows_by[k].raw.get("이름"))) for k in whole]
|
||||
return [{**x, "이름": str(name or ""), "규격": str(spec or "")} for x in made]
|
||||
out: list[dict[str, Any]] = []
|
||||
for n, s in parts(name, spec):
|
||||
if out and s and not split_name(n)[2] and s == out[0]["규격"] and out[0]["키"]:
|
||||
s = str(out[0]["후보"][0]["규격"]) # 앞 줄 규격을 받은 쪽 = 이은 우리 규격(단위까지)
|
||||
out.append({**find(n, s), "이름": n, "규격": s})
|
||||
return out
|
||||
|
||||
|
||||
def mark(got: dict[str, Any], name: Any, spec: Any) -> str | None:
|
||||
"""못 이은 기계 표지 글(「참고 · 」 머리) — 이음 · 별칭 = None."""
|
||||
said = f"「{' '.join(x for x in (str(name or ''), str(spec or '')) if x)}」"
|
||||
if got["판정"] == MANY:
|
||||
names = " · ".join(f"{c['이름']} {c['규격']}".strip() for c in got["후보"])
|
||||
return f"{NOTE}기계 후보 {got['수']} — 고르기 {said}: {names}"
|
||||
if got["판정"] == NONE:
|
||||
return f"{NOTE}기계 못 찾음 — {said} {got['까닭']}"
|
||||
return None
|
||||
|
||||
|
||||
def same_kind(a: Any, b: Any) -> bool | None:
|
||||
"""두 기계 이름이 같은 기종인지(연결 찾기) — 둘 다 기종을 찾으면 판정 · 아니면 None."""
|
||||
found = []
|
||||
for t in (a, b):
|
||||
named = alias(t, "")
|
||||
row = table()[2].get(named[0]) if len(named) == 1 else None
|
||||
found.append(kind_rows(row.base if row else split_name(t)[0]))
|
||||
if not all(found):
|
||||
return None
|
||||
return bool({r.base for r in found[0]} & {r.base for r in found[1]})
|
||||
|
||||
|
||||
# ── 고른 것 → 별칭 장부(저장 때) ──────────────────────────────────────
|
||||
def learn(doc: Any, who: Any) -> int:
|
||||
"""가져온 식 문서 [저장] — 기계 줄에 사람이 고른 EQ(찾기 판정과 다름) = 지금 층 별칭 장부 줄
|
||||
(갈래 기계 · 열쇠 「이름|규격|단위」) → M01 별칭 장부 후보로 모임(승인은 M01) · 쓴 줄 수."""
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
head = (doc or {}).get("가져온") if isinstance(doc, dict) else None
|
||||
if not isinstance(head, dict) or not isinstance(head.get("기계"), list):
|
||||
return 0
|
||||
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or [] if isinstance(v, dict)}
|
||||
rows: dict[str, dict] = {}
|
||||
for one in head["기계"]:
|
||||
link = ((by_id.get(str((one or {}).get("기호"))) or {}).get("연결")) or {}
|
||||
code = (link.get("입력") or {}).get("기계") if link.get("갈래") == "단가산출" else None
|
||||
eq = by_code(code)
|
||||
origin = one.get("원래") or {}
|
||||
if not eq or eq == one.get("키") or not origin.get("이름"):
|
||||
continue
|
||||
if alias(origin["이름"], origin.get("규격")) == [eq]: # 이미 장부에 같은 짝
|
||||
continue
|
||||
plain = {"품명": str(origin["이름"]), "규격": str(origin.get("규격") or ""),
|
||||
"단위": str(origin.get("단위") or "hr")} # fmt: skip
|
||||
full = ledger.key({"이름": plain["품명"], "규격": plain["규격"], "단위": plain["단위"]})
|
||||
pair = {"갈래": "기계", "키": eq, "입력값": {}, "배": 1}
|
||||
rows[full] = {"원래": plain, "갈래": "짝", "짝": [pair]}
|
||||
if rows:
|
||||
ledger.put(store.root(), rows, who)
|
||||
return len(rows)
|
||||
@@ -41,6 +41,12 @@ def _rank(row: dict) -> tuple:
|
||||
return (attach, loss, loss and "보정작업" in names, len(_codes(row)), str(row.get("키")))
|
||||
|
||||
|
||||
def logic_of(code: str) -> dict | None:
|
||||
"""기계 원문번호 → 그 기계 시간당 경비 로직(짝 차례 첫째) · 없으면 None(813-2 M01 화면도 씀)."""
|
||||
fit = [r for r in _family() if code in [str(c) for c in _codes(r)]]
|
||||
return min(fit, key=_rank) if fit else None
|
||||
|
||||
|
||||
def hourly(key: str) -> dict[str, Any]:
|
||||
"""`{ok, 이름, 규격, 로직, 원문번호, 노무비, 재료비, 경비, 계}` · 못 셈 = `{ok: False, reason}`."""
|
||||
if not key.startswith("EQ"):
|
||||
@@ -50,10 +56,9 @@ def hourly(key: str) -> dict[str, Any]:
|
||||
except cm.mf.FormulaError as e:
|
||||
return {"ok": False, "reason": str(e)}
|
||||
code = str(row.get("원문번호") or "")
|
||||
fit = [r for r in _family() if code in [str(c) for c in _codes(r)]]
|
||||
if not fit:
|
||||
logic = logic_of(code)
|
||||
if not logic:
|
||||
return {"ok": False, "reason": f"「{row.get('이름')}」 은 시간당 경비를 셀 자료가 없음"}
|
||||
logic = min(fit, key=_rank)
|
||||
args = "".join(
|
||||
f", {i['이름']}='{(i.get('고르기') or [''])[0]}'"
|
||||
for i in logic.get("입력") or []
|
||||
@@ -78,3 +83,24 @@ def hourly(key: str) -> dict[str, Any]:
|
||||
"원문번호": code,
|
||||
**sums,
|
||||
}
|
||||
|
||||
|
||||
def combo(main: str, part: str) -> tuple[str, str] | None:
|
||||
"""조합 기계(굴착기 + 대형 브레이커 · 부착용 집게 · 814-1) → 우리 조합 식 (키, 부름 글)
|
||||
`로직(GC000271, 기계='…', 브레이커='…', 지역='전국평균', …)` — 두 기계가 그 식 고르기에 있을 때만."""
|
||||
try:
|
||||
codes = [str(store.loaded()[1].get(k).get("원문번호") or "") for k in (main, part)]
|
||||
except cm.mf.FormulaError:
|
||||
return None
|
||||
for row in sorted(_family(), key=lambda r: str(r.get("키"))):
|
||||
ins = row.get("입력") or []
|
||||
attach = [i for i in ins if i.get("이름") not in _PLAIN and i.get("고르기")]
|
||||
if len(attach) != 1 or codes[0] not in [str(c) for c in _codes(row)]:
|
||||
continue
|
||||
if codes[1] not in [str(c) for c in attach[0]["고르기"]]:
|
||||
continue
|
||||
given = {"기계": codes[0], attach[0]["이름"]: codes[1]}
|
||||
given |= {i["이름"]: (i.get("고르기") or [""])[0] for i in ins if i["이름"] not in given}
|
||||
args = "".join(f", {k}='{v}'" for k, v in given.items())
|
||||
return str(row["키"]), f"로직({row['키']}{args})"
|
||||
return None
|
||||
|
||||
@@ -101,6 +101,9 @@ export interface HopyoBuiltAll {
|
||||
export interface LedgerPair {
|
||||
로직?: string;
|
||||
자재?: string;
|
||||
/** 고른 것 꼴(785-3 · 812-9 기계) — 갈래 + 키 */
|
||||
갈래?: string;
|
||||
키?: string;
|
||||
입력값: Record<string, unknown>;
|
||||
배: number;
|
||||
몫: number | null;
|
||||
|
||||
@@ -61,7 +61,7 @@ interface EditAnswer {
|
||||
문서: ImportedDoc;
|
||||
전?: Money4 | null;
|
||||
후?: Money4 | null;
|
||||
이음없음?: { 자리: string; 이름: string }[];
|
||||
이음없음?: { 자리: string; 이름: string; 까닭?: string }[];
|
||||
/** qty 답(797-2) */
|
||||
줄?: QtyRow[];
|
||||
집계?: QtyTally;
|
||||
|
||||
@@ -54,7 +54,7 @@ export const tx = (k: keyof typeof TEXT): string =>
|
||||
export interface PanelState {
|
||||
doc: ImportedDoc;
|
||||
/** 이음 없는 자원(서버 price 답) */
|
||||
unlinked: { 자리: string; 이름: string }[];
|
||||
unlinked: { 자리: string; 이름: string; 까닭?: string }[];
|
||||
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
|
||||
qty: QtyTally | null;
|
||||
/** 마지막 단가 바꿈 전후 */
|
||||
@@ -149,7 +149,8 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
const kids: HTMLElement[] = [createToolbar({ items: head })];
|
||||
if (here && s.unlinked.length) {
|
||||
const items = s.unlinked.map((u) => {
|
||||
const b = createButton({ label: `${u.이름} · ${tx("Link")}`, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
|
||||
const label = `${u.이름}${u.까닭 ? ` (${u.까닭})` : ""} · ${tx("Link")}`; // 못 이은 까닭(814-1)
|
||||
const b = createButton({ label, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
|
||||
b.dataset.importedLink = u.자리;
|
||||
return b;
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ export function mergeRows(rows: LedgerRow[], edits: Edits, scope: HopyoScope): L
|
||||
/** 짝 글 — 「GC000198 × 1 · 몫 0.1 · 소량감률=50」 */
|
||||
export const pairText = (p: LedgerPair): string =>
|
||||
[
|
||||
`${p.로직 ?? p.자재 ?? ""} × ${p.배}`,
|
||||
`${p.로직 ?? p.자재 ?? [p.갈래, p.키].filter(Boolean).join(" ")} × ${p.배}`,
|
||||
p.몫 != null ? `몫 ${p.몫}` : "",
|
||||
Object.entries(p.입력값 ?? {})
|
||||
.map(([k, v]) => `${k}=${formatNumber(v)}`)
|
||||
|
||||
@@ -275,7 +275,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
const lastOf = (sid: string): string =>
|
||||
lastColumn(book.시트.find((x) => x.id === sid) ?? first);
|
||||
const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left };
|
||||
nav = ids.일위대가표 ? Nav.createSheetNav(more) : null;
|
||||
nav = ids.일위대가표 || ids.단가산출표 ? Nav.createSheetNav(more) : null; // 원파일 = 단가산출표 판만(814-1)
|
||||
if (nav) warn.after(nav.root);
|
||||
sk = nav ? createSheetSk({ ...opt, target, nav, side: right, ...(opt.onSkChange ? { onChange: opt.onSkChange } : {}) }) : null; // prettier-ignore
|
||||
sheet = createSpreadsheet(left, book, {
|
||||
@@ -290,6 +290,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
if (no !== null && no !== current) mark(no);
|
||||
},
|
||||
});
|
||||
if (!ids.일위대가표 && ids.단가산출표) sk?.at({ 시트: first.id, 칸: "A1" }); // 일위대가 없음 = 단가산출 목록 먼저
|
||||
nav?.marks(briefs);
|
||||
// 머리 행 칸을 눌러 뗌 → 그 호표 영역 고름(끌기 · 방향키 도중엔 안 바꿈 · 줄 행은 그 줄 그대로)
|
||||
left.querySelector(".ss-grid-host")?.addEventListener("mouseup", (e) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Side.ts
|
||||
* 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시 · 785-4) —
|
||||
* · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 엑셀 고르기 창 → 올림 → 본 칸 불러오기 보기
|
||||
* · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 파일 하나 고르기 창(엑셀 · 경쟁사 .estx · .STC · 814-1) → 올림 → 본 칸 불러오기 보기
|
||||
* (`M02_MasterTemplete_Hopyo_Import` · 엑셀 + 오른쪽 호표 목록 · 가운데 상세 페이지 없음 · 누름 하나)
|
||||
* · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임)
|
||||
* · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기
|
||||
@@ -67,7 +67,7 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
});
|
||||
start.classList.add("m02-layers__btn");
|
||||
start.title =
|
||||
"다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내거나 떠나면 올린 파일 지움)";
|
||||
"파일 하나 — 경쟁사 엑셀(.xls · .xlsx) 또는 경쟁사 파일(.estx · .STC) → 우리 정보로 다시 만듦(한시 · 끝내거나 떠나면 올린 파일 지움)";
|
||||
start.dataset.hopyoWizardOpen = "";
|
||||
const seg = el("div", { className: "m02-side__row" });
|
||||
seg.hidden = true;
|
||||
@@ -145,14 +145,10 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
if (!file) return;
|
||||
start.disabled = true;
|
||||
uploadHopyo(scopeOf(pick), file)
|
||||
.then((got: HopyoFile & { 이어서?: boolean; 원파일?: boolean; 붙음?: string[] | null }) => {
|
||||
.then((got: HopyoFile & { 이어서?: boolean; 원파일?: boolean }) => {
|
||||
const tool = /\.estx$/i.test(file.name) ? "EST" : "STmate"; // 810-3 원파일 올림 안내
|
||||
if (got.원파일)
|
||||
showToast(
|
||||
`원파일 · ${tool} — 호표 ${got.호표수}${got.붙음?.length ? ` · 같은 공사 엑셀 ${got.붙음.join(", ")} 에 붙음` : ""}`,
|
||||
"success",
|
||||
);
|
||||
else if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info");
|
||||
if (got.이어서) showToast(`같은 파일 작업 중 — ${got.키} 이어서`, "info");
|
||||
else if (got.원파일) showToast(`${tool} 파일 — 단가산출표 · 산출근거`, "success");
|
||||
else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success");
|
||||
if (got.읽기불가) showToast(`가져온 식으로 열 수 없음 — ${got.읽기불가}`, "warning");
|
||||
return show(pick, got, true);
|
||||
|
||||
@@ -19,6 +19,7 @@ from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_http_cache import cached_file_response
|
||||
from M01_MasterData import M01_MasterData_Parts as parts
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
@@ -40,7 +41,7 @@ def _read(fn, *args):
|
||||
|
||||
@router.get("/logic-lib/logics", dependencies=[Depends(verify_session)])
|
||||
def get_logics() -> dict:
|
||||
return {"logics": _read(m01.logics, "", "", "", None)}
|
||||
return {"logics": parts.strip(_read(m01.logics, "", "", "", None))} # 부품 문서 뺌(813-2)
|
||||
|
||||
|
||||
@router.get("/logic-lib/subs", dependencies=[Depends(verify_session)])
|
||||
|
||||
@@ -34,7 +34,7 @@ const LABEL: Record<Exclude<MatchAction, null>, string> = {
|
||||
};
|
||||
const ASK: Partial<Record<Exclude<MatchAction, null>, string>> = {
|
||||
바꾸기:
|
||||
"이 단가산출을 우리 기초 사본(찾은 옵션)으로 바꿈 — 키 그대로 · 일위대가 금액이 다시 셈됨 · 원래 가져온 식은 보관",
|
||||
"이 단가산출을 우리 기초 사본(찾은 옵션 · 우리 값)으로 바꿈 — 규격만 다른 기계 줄은 가져온 규격의 우리 기계 · 키 그대로 · 일위대가 금액이 다시 셈됨 · 원래 가져온 식은 보관",
|
||||
되돌리기: "우리 기초 사본을 원래 가져온 식으로 되돌림 — 가져온 식 금액 그대로",
|
||||
};
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ function originCard(o: Origin, band: HTMLElement | null): HTMLElement {
|
||||
{ label: tx("Rows"), value: o.행 },
|
||||
{ label: tx("When"), value: o.때 },
|
||||
{ label: tx("Marks"), value: tx("MarksN", o.표지.length) },
|
||||
...(o.확정 ? [{ label: tx("Sure"), value: o.확정 }] : []),
|
||||
...(o.확정 ? [{ label: o.확정이름 ?? tx("Sure"), value: o.확정 }] : []),
|
||||
]),
|
||||
marks,
|
||||
el("strong", { text: tx("Clauses") }),
|
||||
|
||||
@@ -12,11 +12,8 @@ from __future__ import annotations
|
||||
import io
|
||||
import re
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as read_est
|
||||
|
||||
STM_SHEETS = (
|
||||
"단가산출근거",
|
||||
"단가산출근거목록표",
|
||||
@@ -233,23 +230,3 @@ def est_prices(book: dict[str, list]) -> tuple[list[dict[str, Any]], dict[int, d
|
||||
one = {"종류": "산근", "이름": f"단산{n}", "규격": ""}
|
||||
out.append({**one, "비목": k, "값": san[n][k]})
|
||||
return out, san
|
||||
|
||||
|
||||
def est_blocks(book: dict[str, list]) -> list[dict[str, Any]]:
|
||||
"""→ [{번, 이름, 단위, 글 [줄], 글행 [행]}]."""
|
||||
blocks = read_est.read(book.get(read_est.SHEET))
|
||||
keep = ("번", "이름", "단위", "글", "글행")
|
||||
return [{k: b[k] for k in keep} for b in blocks.values()]
|
||||
|
||||
|
||||
def load(path: str | Path) -> dict[str, Any]:
|
||||
"""파일 → {꼴, 블록, 단가(STmate dict | EST 목록), 총괄(EST)}."""
|
||||
path = Path(path)
|
||||
book = sheets(path.name, path.read_bytes())
|
||||
kind = kind_of(book)
|
||||
if kind == "STmate":
|
||||
return {"꼴": kind, "블록": stmate_blocks(book), "단가": stmate_prices(book)}
|
||||
if kind == "EST":
|
||||
prices, san = est_prices(book)
|
||||
return {"꼴": kind, "블록": est_blocks(book), "단가": prices, "총괄": san}
|
||||
raise ValueError("STmate · EST 산근 시트 없음")
|
||||
|
||||
+150
-127
@@ -1,6 +1,6 @@
|
||||
"""경쟁사 산근 → 우리 엔진 로직 줄 꼴 내보내기(PLAN 793-2a · 요구서_652 · eval 없음).
|
||||
|
||||
793-1 줄 나무(`M02_San_Stmate` · `M02_San_Est`) → `{로직, 원본}` — 꼴 = 793-2 가져온 식 SK
|
||||
793-1 줄 나무(`M02_San_Stmate`) → `{로직, 원본}`(EST = 원파일 · 엑셀 둘 다 `M02_Doc_NativeEst` · 812-1) — 꼴 = 793-2 가져온 식 SK
|
||||
만들기 몸(`M02_Sangeun_Imported.py` 머리) · 로직 = `_틀.md` 7 장 꼴(셈 = `master_formula.run` →
|
||||
`master_cut.engine` · `요소` 는 SK 서버가 채움).
|
||||
- 로직 = {이름, 결과단위, 입력 [], 중간 [{이름, 식, 표시?}], 호표,
|
||||
@@ -28,12 +28,13 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Eval as ev
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
from M02_MasterTemplete.M02_San_Link import combo_link, hourly_text # noqa: F401 — 밖에서 em. 으로 부름
|
||||
from M02_MasterTemplete.M02_San_Link import m01 as _m01
|
||||
|
||||
COMP = {"L": "노무비", "M": "재료비", "S": "경비"}
|
||||
_CODE = {v: k for k, v in COMP.items()}
|
||||
_SUM = "@합{}@" # 누계 % 밑 자리(블록 끝에 `finish` 가 채움)
|
||||
KIND = {"L": "인력", "M": "재료", "X": "기계", "D": "단가산출", "B": "단가산출"}
|
||||
KIND_EST = {"노임": "인력", "자재": "재료", "기계": "기계", "견적": "고정", "산근": "단가산출"}
|
||||
RESERVED = {"노무비", "재료비", "경비", "계", "번지", "단계", "줄", "찾기", "로직", "그리고"}
|
||||
RESERVED |= {"또는", "만약", "올림", "버림", "반올림", "최소", "최대"}
|
||||
_KEY = re.compile(r"[A-Z]{2}\d{6}")
|
||||
@@ -176,40 +177,54 @@ def reprice(logic: dict, where: dict, price: dict[str, float]) -> int:
|
||||
return n
|
||||
|
||||
|
||||
def _m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력 · 기계는 지금처럼 조용히 이음 없음). 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
# ── 기계 부분(812-9 · 조합 기계 = 줄 둘) ─────────────────────────────────
|
||||
def machine_parts(src: dict) -> list[dict]:
|
||||
"""기계 자원 → 부분 찾기(`M02_Machine_Find.find_all` · 조합 = 둘 이상) · 기계 아님 = []."""
|
||||
if src.get("갈래") != "기계":
|
||||
return []
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
if kind == "재료":
|
||||
if elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
|
||||
kind = "환율"
|
||||
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
|
||||
got = [
|
||||
{"키": r["키"], "규격": str(r.get("상세구분") or "")}
|
||||
for r in elem.oil_rows(line["이름"])
|
||||
]
|
||||
spec = line["규격"].replace(" ", "")
|
||||
got = (
|
||||
[x for x in got if x["규격"] == spec]
|
||||
or [x for x in got if spec and spec in x["규격"]]
|
||||
or got
|
||||
)
|
||||
return mfind.find_all(src.get("이름"), src.get("규격"))
|
||||
|
||||
|
||||
def first_spec(one: dict, parts: list[dict]) -> dict:
|
||||
"""조합 기계 첫 줄 규격 = 그 기계 규격(칩 글 · 「0.7+브레이카」 아님 · 814-1) — 자원 원문은 그대로."""
|
||||
if len(parts) > 1 and parts[0].get("규격") is not None:
|
||||
one["규격"] = parts[0]["규격"]
|
||||
return one
|
||||
|
||||
|
||||
def part_line(main: dict, part: dict, names: "Names") -> dict:
|
||||
"""조합 기계 다음 줄 — 같은 수량 식 · 값 0(파일 경비 = 한 덩이 = 첫 줄) · 제 이음."""
|
||||
res = {**main["자원"], "이름": part["이름"], "규격": part["규격"], "찾기": part, "조합": True}
|
||||
res.pop("조합첫", None)
|
||||
res["단가"] = dict.fromkeys(res.get("단가") or COMP, 0)
|
||||
title = part["후보"][0]["이름"] if part["키"] else part["이름"]
|
||||
one = {"이름": names.item(str(title)), "규격": part["규격"], "단위": main.get("단위") or ""}
|
||||
one |= {"수량": main["수량"], "자원": res}
|
||||
if main.get("종류") == "로직": # 우리 조합 식(814-1 `조합식`)에 든 다음 줄 = 이음 없음 · 값 0
|
||||
link = None if res.get("조합식") else hourly_text(part["키"])
|
||||
one |= {"종류": "로직", "값묶음": bundle(res), "이음": link}
|
||||
else:
|
||||
got = hrow.m01_picks(kind, line)
|
||||
keys = {x.get("키") for x in got if x.get("키")}
|
||||
if len(keys) == 1:
|
||||
return keys.pop(), None
|
||||
if len(keys) > 1 and kind in ("유가", "환율"):
|
||||
return None, f"{NOTE}{kind} 후보 여럿({len(keys)}) · 사람 확인"
|
||||
return None, None
|
||||
one |= {"종류": main["종류"], "비목": main["비목"], "단가": Decimal(0), "이음": part["키"]}
|
||||
return one | ({"자리": main["자리"]} if main.get("자리") else {})
|
||||
|
||||
|
||||
def part_marks(src: dict, parts: list[dict]) -> list[str]:
|
||||
"""부분 표지 — 못 이은 부분(후보 여럿 · 못 찾음) · 조합 기계(경비 한 덩이 = 첫 줄 · 다음 줄 0)."""
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
out = [m for p in parts if (m := mfind.mark(p, p["이름"], p["규격"]))]
|
||||
if len(parts) > 1:
|
||||
said = " ".join(x for x in (str(src.get("이름") or ""), str(src.get("규격") or "")) if x)
|
||||
names = " + ".join(
|
||||
f"{p['후보'][0]['이름']} {p['후보'][0]['규격']}" if p["키"] else p["이름"]
|
||||
for p in parts
|
||||
)
|
||||
out.append(
|
||||
f"{NOTE}조합 기계 — 경비는 한 덩이 「{said}」 = {names} · 엑셀 값은 첫 줄 · 다음 줄 0"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ── 내보낼 로직 틀 ─────────────────────────────────────────────────────
|
||||
@@ -291,6 +306,7 @@ class _Stm:
|
||||
self.open: list[tuple[int, list[str]]] = [] # 위 단계가 아직 안 담은 단계(엔진 `_cover`)
|
||||
self.out: dict[str, int] = {} # 제외 단계 줄 → 그 계 줄까지 줄 수(합에서 빠지는 때)
|
||||
self.fix: list[tuple[dict, int]] = [] # 누계 % 덧줄 · 그때 줄 수
|
||||
self.noted: set[tuple] = set() # 기계 표지 단 자원(812-9 · 블록에 한 번)
|
||||
|
||||
def brackets(self, node: tuple, src: dict, kept: dict) -> None:
|
||||
"""끼움 `[Q=0.208]` → 중간(왼쪽부터 · 원문 차례)."""
|
||||
@@ -378,6 +394,8 @@ class _Stm:
|
||||
unt = (pr.get("meta") or {}).get("UNT")
|
||||
one_, to = node["자원"]["비목"], node["자원"]["옮김"]
|
||||
name = _label(node) or str(pr.get("이름") or code)
|
||||
if node.get(_MERGED): # 814-1 — 기계 세 줄(노 · 재 · 경) = 기계 한 줄 · 이름 = 기계
|
||||
name = str(pr.get("이름") or code)
|
||||
if unt in ev.SPECIAL: # 특수 % — 처음부터 비목 누계 · 앞 계표시
|
||||
dst = to or {"L": "L", "M": "M", "S": "S"}.get(code[0])
|
||||
if unt == "T%":
|
||||
@@ -396,11 +414,26 @@ class _Stm:
|
||||
if not one_ and len(sole) == 1: # 비목 꼬리 없어도 값 든 비목 하나면 단가 줄(셈 같음)
|
||||
one_, to = sole[0], None
|
||||
res |= {"비목": one_, "옮김": to} if one_ else {}
|
||||
link, many = _m01(res["갈래"], res) if self.keys else (None, None)
|
||||
one = resource(o.names.item(name), pr.get("단위"), cf, res, link)
|
||||
parts = machine_parts(res) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
if parts:
|
||||
res["찾기"], link, many = parts[0], parts[0]["키"], None
|
||||
res |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
else:
|
||||
link, many = _m01(res["갈래"], res) if self.keys else (None, None)
|
||||
one = first_spec(resource(o.names.item(name), pr.get("단위"), cf, res, link), parts)
|
||||
if parts and one["종류"] == "로직":
|
||||
one["이음"] = combo_link(one, parts) or hourly_text(link)
|
||||
self.line(one, node, True, src)
|
||||
if many:
|
||||
o.glyph(src["행"], src["글"], mark=many)
|
||||
for at, glyph in node.get(_MERGED) or []: # 묶은 다른 두 줄 원문 = 같은 자리
|
||||
o.glyph(at, glyph, one["이름"], self.rows.get(at, {}).get("C"))
|
||||
bare = {k: v for k, v in node.items() if k not in ("번지", _MERGED)}
|
||||
for p in parts[1:]:
|
||||
self.line(part_line(one, p, o.names), bare, True, src)
|
||||
seen = (res.get("이름"), res.get("규격"))
|
||||
marks = [] if seen in self.noted else part_marks(res, parts)
|
||||
self.noted.add(seen)
|
||||
for m in [*marks, *([many] if many else [])]:
|
||||
o.glyph(src["행"], src["글"], mark=m)
|
||||
|
||||
def rate(self, node: dict, src: dict, cf: str) -> None:
|
||||
rate, o = node["율"], self.o
|
||||
@@ -478,19 +511,96 @@ def _ratios(meta: dict) -> str:
|
||||
return " · ".join(f"{n} {v}" for n, v in pairs if v is not None and abs(_num(v) - 1) > 1e-9)
|
||||
|
||||
|
||||
_MERGED = "_묶음줄" # 세 줄을 묶은 줄 나무 — 나머지 두 줄 [(행, 글)]
|
||||
_BAD = ("셈 못 함", "엔진 꼴 없음")
|
||||
|
||||
|
||||
def triples(nodes: list[dict]) -> list[list[int]]:
|
||||
"""814-1 — 같은 기계(X 코드)의 노 · 재 · 경 세 참조 줄(같은 수량 식 · 번지 · 빼기 · 옮김 없음) 자리
|
||||
[[i, j, k], …] — 사이 빈 줄은 건너뜀."""
|
||||
out, k = [], 0
|
||||
while k < len(nodes):
|
||||
at, j = [k], k + 1
|
||||
while len(at) < 3 and j < len(nodes):
|
||||
if nodes[j].get("종류") == "빈" and not str(nodes[j].get("글") or "").strip():
|
||||
j += 1
|
||||
continue
|
||||
at.append(j)
|
||||
j += 1
|
||||
if len(at) == 3 and _same_machine([nodes[i] for i in at]):
|
||||
out.append(at)
|
||||
k = j
|
||||
else:
|
||||
k += 1
|
||||
return out
|
||||
|
||||
|
||||
def merge(nodes: list[dict], groups: list[list[int]]) -> list[dict]:
|
||||
"""세 줄 자리 → 비목 없는 한 줄(값묶음 · 조합이면 그 뒤 기계마다 줄) · 나머지 두 줄 원문은 같은 자리."""
|
||||
first = {g[0]: g for g in groups}
|
||||
drop = {i for g in groups for i in g[1:]}
|
||||
out = []
|
||||
for i, node in enumerate(nodes):
|
||||
if i in first:
|
||||
res = {**node["자원"], "비목": None, "옮김": None}
|
||||
rest = [(nodes[j].get("행"), nodes[j].get("글") or "") for j in first[i][1:]]
|
||||
out.append({**node, "자원": res, _MERGED: rest})
|
||||
elif i not in drop:
|
||||
out.append(node)
|
||||
return out
|
||||
|
||||
|
||||
def _same_machine(three: list[dict]) -> bool:
|
||||
if any(x.get("종류") != "참조" or x.get("오류") or not x.get("자원") for x in three):
|
||||
return False
|
||||
res = [x["자원"] for x in three]
|
||||
if not str(res[0].get("코드") or "").startswith("X") or len({r["코드"] for r in res}) != 1:
|
||||
return False
|
||||
if sorted(str(r.get("비목")) for r in res) != ["L", "M", "S"] or any(
|
||||
r.get("옮김") for r in res
|
||||
):
|
||||
return False
|
||||
return all(len({repr(x.get(k)) for x in three}) == 1 for k in ("식", "번지", "빼기"))
|
||||
|
||||
|
||||
def stmate(
|
||||
block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool = False, file: str = ""
|
||||
) -> dict:
|
||||
"""STmate 블록(`M02_San_Book.stmate_blocks` 한 개) + 줄 나무 + 같은 책 단가 → {로직, 원본}.
|
||||
rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키(이음) 찾기."""
|
||||
rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키(이음) 찾기 ·
|
||||
기계 세 줄 = 한 줄(`triples` · 814-1) — 묶어서 셈 못 하는 줄이 생기면 묶지 않은 꼴."""
|
||||
groups = triples(nodes)
|
||||
out = _stmate(block, merge(nodes, groups), prices, rule, keys, file)
|
||||
bad = _bad(out)
|
||||
if not groups or not bad:
|
||||
return out
|
||||
took: list[
|
||||
list[int]
|
||||
] = [] # 묶어서 셈 못 하는 세 줄만 그대로(율 범위 C · 누계 % 가 비목 하나를 부름)
|
||||
base = _bad(_stmate(block, nodes, prices, rule, keys, file))
|
||||
for g in groups:
|
||||
if _bad(_stmate(block, merge(nodes, [*took, g]), prices, rule, keys, file)) <= base:
|
||||
took.append(g)
|
||||
return _stmate(block, merge(nodes, took), prices, rule, keys, file)
|
||||
|
||||
|
||||
def _bad(out: dict) -> int:
|
||||
return sum(1 for m in out["원본"]["표지"] if any(b in m for b in _BAD))
|
||||
|
||||
|
||||
def _stmate(block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool, file: str) -> dict:
|
||||
me = prices.get(block["코드"]) or {}
|
||||
meta = me.get("meta") or {}
|
||||
o = Out(block.get("이름"), me.get("단위"), "STmate", block["코드"])
|
||||
o.head["차림"] = True # 812-3 — 계 단으로 항목 · 글 줄(`M02_Doc_NativeLayout`)
|
||||
st = _Stm(o, block, prices, keys)
|
||||
for node in nodes:
|
||||
kind, src = node["종류"], {"행": node.get("행"), "글": node.get("글") or ""}
|
||||
if kind in ("빈", "글"):
|
||||
if src["글"].strip():
|
||||
if src["글"].strip().strip("'").strip():
|
||||
o.glyph(src["행"], src["글"])
|
||||
o.at[src["행"]]["차림"] = "글" # 812-3 줄 쌓기 글 줄 · 제목(`M02_Doc_NativeLayout`)
|
||||
elif src["글"].strip():
|
||||
o.glyph(src["행"], src["글"])
|
||||
elif kind == "대입":
|
||||
st.assign(node, src)
|
||||
@@ -527,90 +637,3 @@ def stmate(
|
||||
more["계약"] = {"낙찰률": float(meta["NAG"]), "금액": _cols(block["머리"])}
|
||||
end = "큰나머지" if rule.startswith("합버림") else "비목버림"
|
||||
return o.done(end, adjust, _cols(kye), "단가산출근거", file, **more)
|
||||
|
||||
|
||||
# ── EST ────────────────────────────────────────────────────────────────
|
||||
_VAR = re.compile(r"([A-Za-z][A-Za-z0-9_]*)\s*(?:\([^)]*\))?\s*=")
|
||||
|
||||
|
||||
def _est_res(role: dict, value: float, comp: str | None) -> dict:
|
||||
"""EST 단가 토큰 정체 → 자원 이음 열쇠(옮김 = 성분 비목이 단가 비목과 다를 때)."""
|
||||
p = role["정체"]
|
||||
pc = p["비목"] if p["비목"] in COMP else (comp or "S")
|
||||
res = {"꼴": "EST", "갈래": KIND_EST.get(p["종류"], "고정"), "종류": p["종류"]}
|
||||
res |= {"이름": p["이름"], "규격": p["규격"], "단위": None}
|
||||
res |= {"단가": {pc: value}}
|
||||
res |= {"비목": pc, "옮김": comp if comp and comp != pc else None, "열쇠비목": p["비목"]}
|
||||
return res | ({"모호": True} if role.get("모호") else {})
|
||||
|
||||
|
||||
def est(block: dict, lines: list[dict], want: dict, keys: bool = False, file: str = "") -> dict:
|
||||
"""EST 블록(`M02_San_Book.est_blocks` 한 개) + 복원 줄(`M02_San_Est.parse_block`) +
|
||||
총괄표 성분 want {T, L, M, S} → {로직, 원본} · 성분을 낸 줄만 합(나머지 = 합밖)."""
|
||||
des = ev.designate(lines, want)
|
||||
mine = {k: c for c, k in des.items() if isinstance(k, int)}
|
||||
o = Out(block.get("이름"), block.get("단위"), "EST", block.get("번"))
|
||||
rows = block.get("글행") or []
|
||||
if want.get("칸"): # 당초 칸 사용 · 적용 ≠ 당초(797-1a)
|
||||
o.marks.append(f"{NOTE}{want['칸']}")
|
||||
for c, k in des.items():
|
||||
if k is None:
|
||||
o.marks.append(f"성분 {COMP[c]} {want[c]} 을 낸 줄 없음")
|
||||
val = ev.run_est(lines)
|
||||
ref: dict[int, str] = {} # 차례 → 엔진 부름 글(앞줄 · 숨은 합)
|
||||
for L in lines:
|
||||
k, form, kind = L["차례"], L["꼴"], L.get("종류")
|
||||
at, glyph = rows[k] if k < len(rows) else k, L["원글"]
|
||||
comp = mine.get(k)
|
||||
if form == "빈":
|
||||
continue
|
||||
if form == "변수" and L.get("값") is not None:
|
||||
m = _VAR.search(L["원글"])
|
||||
eng = o.names.line(m.group(1) if m else "변수")
|
||||
o.middle(eng, num(L["값"]), None, at, glyph) # 얼린 값(금액엔 보인 값이 박힘)
|
||||
continue
|
||||
if form not in ("돈", "값"):
|
||||
shown = L.get("보인") if form == "글끝" else L.get("값") if form == "식못읽음" else None
|
||||
if shown is None:
|
||||
o.glyph(at, glyph)
|
||||
else: # 금액 줄 못 읽음 — 보인 값 얼림(글줄 값으로 보존)
|
||||
o.glyph(at, glyph, value=shown, mark=f"{NOTE}금액 줄 못 읽음 · 보인 값 얼림")
|
||||
continue
|
||||
head = {} if comp else {"합밖": True}
|
||||
try:
|
||||
place = _place(L.get("규칙"))
|
||||
if k not in val:
|
||||
raise EmitError("보인 값이 되는 식 · 자리 못 찾음 — 보인 값으로 얼림")
|
||||
if kind == "단가줄":
|
||||
i, role = next((i, r) for i, r in L["역할"].items() if r["갈래"] == "단가")
|
||||
price = next(n[1] for n in ex.numbers(L["식"]) if n[2] == i)
|
||||
res = _est_res(role, price, comp)
|
||||
link, many = _m01(res["갈래"], res) if keys else (None, None)
|
||||
qty = text(L["식"], {}, {i: "1"})
|
||||
one = resource(o.names.item(L.get("라벨") or ""), None, qty, res, link)
|
||||
one |= {**head, **({"자리": place} if place else {})}
|
||||
o.add_line(one, True, at, glyph, L["값"])
|
||||
if many:
|
||||
o.glyph(at, glyph, mark=many)
|
||||
if role.get("모호"): # 임의 선택 아님 — 후보를 사람이 고름
|
||||
o.glyph(at, glyph, mark=f"{NOTE}단가 후보 여럿({role['남은']}) · 사람 확인")
|
||||
elif role.get("갈림") in ("문맥", "기계 세 줄"):
|
||||
o.glyph(at, glyph, mark=f"{NOTE}추정 — 단가 정체 근거 {role['갈림']}")
|
||||
ref[k] = f"줄.'{one['이름']}'"
|
||||
continue
|
||||
if kind == "값줄":
|
||||
parts = [ref.get(j) or num(lines[j]["값"]) for j in L["숨은"]["줄"]]
|
||||
formula = " + ".join(parts)
|
||||
else:
|
||||
swap = {i: ref[r["줄"]] for i, r in L["역할"].items() if r.get("줄") in ref}
|
||||
formula = text(L["식"], {}, swap)
|
||||
except EmitError as error:
|
||||
o.glyph(at, glyph, mark=str(error))
|
||||
formula, place = num(L["값"]), None
|
||||
name = o.names.line(L.get("라벨") or "줄")
|
||||
one = {"이름": name, "식": formula, "비목": COMP[comp or L.get("비목") or "S"]}
|
||||
o.add_line(one | head | ({"자리": place} if place else {}), False, at, glyph, L["값"])
|
||||
if k in val:
|
||||
ref[k] = name
|
||||
check = _money(want) | {"계": want["T"]}
|
||||
return o.done("비목버림", None, check, "단가산출", file)
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
"""EST 산근 보이는 글 → 식 복원(PLAN 793-1 · 요구서_652 · 문법 = `_분석_648-A` 3 장 · eval 없음).
|
||||
"""EST 식 글 토큰(PLAN 793-1 · 812-1 · eval 없음) — 원파일 산근 변환(`M02_Doc_NativeEst`)이 쓰는 수 · 토큰 읽기만.
|
||||
|
||||
엑셀엔 식이 없고 인쇄 글뿐 — 「라벨 : 식 = 결과」 로 나눠 식을 되살림. 셈은 `M02_San_Eval.run_est`.
|
||||
|
||||
줄 꼴(`꼴`) — 빈 · 그림(분수 ─) · 글 · 글끝(끝이 결과 아님) · 식못읽음 · 변수 · 값 · 돈.
|
||||
- 돈(`종류` 단가줄 · 참조줄 · 상수줄) — 라벨 · 비목(라벨 끝 노무비 · 재료비 · 경비) ·
|
||||
값(보인 결과) · 식(`M02_San_Expr` 나무) ·
|
||||
역할 {수 토큰 차례: 단가(후보 · 정체 · 모호) | 앞줄(같은 블록 앞 결과 줄 차례) | 상수} ·
|
||||
규칙(보인 결과가 되는 줄 자리 속성 · 기본 버림1)
|
||||
- 값(`종류` 값줄 「소 계 : 1,355.9」) — 숨은 = {갈래 복사 | 합, 줄 [차례], 규칙}(앞 결과 하나 또는
|
||||
바로 앞 돈 · 값 줄 이음 합)
|
||||
- 변수(`종류` 변수식 · 변수값 · 변수기호 · 변수글) — 값 · 규칙 = 얼림(금액엔 반올림된 값이 박힘)
|
||||
옛 엑셀 글 추정 사슬(줄 꼴 · 역할 · 값 역색인 · `parse_block`)은 812-1 엑셀 읽기기(`M02_San_Xlsx`)로 바뀌어 걷음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
from M02_MasterTemplete.M02_San_Book import flat
|
||||
|
||||
COMPW = {"노무비": "L", "재료비": "M", "경비": "S"}
|
||||
NUMRE = r"\d{1,3}(?:,\d{3})+(?:\.\d*)?|\d+(?:\.\d*)?|\.\d+"
|
||||
EPS = ex.EPS
|
||||
OPS = {"×": "*", "x": "*", "X": "*", "*": "*", "÷": "/", "/": "/", "+": "+", "-": "-"}
|
||||
OPS |= {"-": "-", "−": "-"}
|
||||
_UNIT = re.compile(r"\s*(?:[A-Za-z가-힣㎡㎥㎏ℓ㎝㎜°˚’'\"Øø]+(?:\s*/\s*[A-Za-z가-힣㎡㎥㎏ℓ]+)?)")
|
||||
VARLINE = re.compile(r"^\s*(?:[○●◦·\-]\s*)?[A-Za-z][A-Za-z0-9_]*\s*(?:\([^)]*\))?\s*=")
|
||||
RES = re.compile(
|
||||
r"^\s*(?:[A-Z]\s+)?("
|
||||
+ NUMRE
|
||||
+ r")\s*\.?\s*(?:원)?\s*(?:[₩\\]?\s*/\s*\S+)?\s*(?:\([^()]*\))?\s*$"
|
||||
)
|
||||
_PLAIN = re.compile(r"[\s\d.,×÷+\-()A-Za-z가-힣㎡㎥]*")
|
||||
_LABEL = re.compile(r"^(?P<l>[^::=]{0,40}?)\s*[::]\s*(?P<r>.*)$")
|
||||
TAIL = re.compile(r"^\s*(" + NUMRE + r")\s*\.?\s*[::][^=]*$") # 결과 뒤 꼬리 글 「3,094.2 : 채집」
|
||||
_HEADING = re.compile(r"^\s*(\d+|[가-하])\s*[.)]")
|
||||
|
||||
|
||||
def num(s: str) -> float:
|
||||
@@ -112,214 +89,3 @@ def tokens(seg: str) -> list[tuple] | None:
|
||||
if depth != 0 or want or not any(t[0] in ("연산", "%") for t in out):
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def calc(node: tuple, sub: dict | None = None) -> float | None:
|
||||
try:
|
||||
return ex.value(node, {"V": {}, "sub": sub})
|
||||
except ex.ExprError:
|
||||
return None
|
||||
|
||||
|
||||
def label_comp(label: str) -> str | None:
|
||||
w = flat(label)
|
||||
return next((v for k, v in COMPW.items() if w.endswith(k)), None)
|
||||
|
||||
|
||||
def parse_line(text: str) -> dict[str, Any]:
|
||||
"""보이는 글 한 줄 → 꼴 · 라벨 · 결과 · 식 나무(역할 전)."""
|
||||
t = text.strip()
|
||||
if not t:
|
||||
return {"꼴": "빈"}
|
||||
if "─" in t or _PLAIN.fullmatch(t) and "=" not in t and ":" not in t:
|
||||
return {"꼴": "그림" if "─" in t else "글"}
|
||||
label, rhs = "", t
|
||||
mc = _LABEL.match(t)
|
||||
if mc and not re.search(r"\d\s*$", mc.group("l")):
|
||||
label, rhs = mc.group("l"), mc.group("r")
|
||||
if VARLINE.match(rhs) or VARLINE.match(t):
|
||||
return {"꼴": "변수", "조각": rhs.split("=")}
|
||||
if "=" not in rhs:
|
||||
m = re.match(r"^\s*(" + NUMRE + r")\s*\.?\s*$", rhs)
|
||||
if label and m:
|
||||
return {"꼴": "값", "라벨": label, "비목": label_comp(label), "값": num(m.group(1))}
|
||||
return {"꼴": "글"}
|
||||
segs = rhs.split("=")
|
||||
mres = RES.match(segs[-1]) or TAIL.match(segs[-1]) # 꼬리 글은 떼고 읽음(797-1a)
|
||||
if not mres:
|
||||
lead = re.match(r"\s*(" + NUMRE + r")", segs[-1])
|
||||
if (
|
||||
label and lead and re.search(r"\d", "=".join(segs[:-1]))
|
||||
): # 금액 줄 못 읽음 — 보인 값 보존
|
||||
return {"꼴": "글끝", "라벨": label, "보인": num(lead.group(1))}
|
||||
return {"꼴": "글끝"}
|
||||
shown = num(mres.group(1))
|
||||
toks = None
|
||||
for seg in reversed(segs[:-1]):
|
||||
toks = tokens(seg)
|
||||
if toks is None: # 라벨 없는 줄 — 왼쪽 글을 깎아 가며
|
||||
starts = {m.start() for m in re.finditer(NUMRE, seg)}
|
||||
starts |= {m.start() for m in re.finditer(r"\(\s*\d", seg)}
|
||||
for p in sorted(starts):
|
||||
if p and (seg[p - 1].isdigit() or seg[p - 1] in ".,"):
|
||||
continue
|
||||
toks = tokens(seg[p:])
|
||||
if toks:
|
||||
label = label or seg[:p]
|
||||
break
|
||||
if toks:
|
||||
break
|
||||
if not toks:
|
||||
return {"꼴": "식못읽음", "라벨": label, "값": shown}
|
||||
return {"꼴": "돈", "라벨": label, "비목": label_comp(label), "값": shown, "식": ex.parse(toks)}
|
||||
|
||||
|
||||
# ── 블록 풀이 ──────────────────────────────────────────────────────────
|
||||
def _ident(p: dict[str, Any]) -> tuple:
|
||||
return (p["종류"], p["이름"], p["규격"])
|
||||
|
||||
|
||||
def _money(L: dict, prices_by_value: dict, results: list) -> None:
|
||||
"""돈 줄 — 수 토큰 역할(단가 · 앞줄 · 상수) · 다시 셈 · 자리 규칙."""
|
||||
comp = L["비목"]
|
||||
nums = [(n[2], n[1]) for n in ex.numbers(L["식"])]
|
||||
roles: dict[int, dict[str, Any]] = {}
|
||||
price_at = None
|
||||
for i, v in nums:
|
||||
if v >= 1 and any(abs(r[1] - v) < EPS for r in results):
|
||||
continue # 앞 결과와 같은 수 = 참조(단가표 우연 일치보다 먼저)
|
||||
cands = [p for p in prices_by_value.get(v, []) if v >= 1 or (v == 0 and comp)]
|
||||
if comp:
|
||||
cands = [p for p in cands if p["비목"] == comp] or cands
|
||||
if v == 0 and comp and not cands:
|
||||
cands = [{"종류": "0", "이름": "-", "규격": "", "비목": comp, "값": 0.0}]
|
||||
first = i == nums[0][0]
|
||||
if cands and (v >= 100 or (first and v > 0)) or (v == 0 and cands):
|
||||
price_at = i
|
||||
roles[i] = {"갈래": "단가", "후보": cands}
|
||||
break
|
||||
for i, v in nums:
|
||||
if i in roles:
|
||||
continue
|
||||
hit = [r for r in reversed(results) if abs(r[1] - v) < EPS]
|
||||
roles[i] = {"갈래": "앞줄", "줄": hit[0][0]} if hit and v >= 1 else {"갈래": "상수"}
|
||||
L["역할"] = roles
|
||||
L["셈"] = calc(L["식"])
|
||||
L["규칙"] = ex.fit(L["셈"], L["값"])
|
||||
if price_at is not None:
|
||||
L["종류"] = "단가줄"
|
||||
pc = roles[price_at]["후보"]
|
||||
L["비목"] = L["비목"] or (pc[0]["비목"] if len({p["비목"] for p in pc}) == 1 else None)
|
||||
L["후보수"] = len({_ident(p) for p in pc})
|
||||
elif any(r["갈래"] == "앞줄" for r in roles.values()):
|
||||
L["종류"] = "참조줄"
|
||||
else:
|
||||
L["종류"] = "상수줄"
|
||||
|
||||
|
||||
def _value(L: dict, lines: list, results: list) -> None:
|
||||
"""값 줄 — 앞 결과 하나(자리 처리) 또는 바로 앞 돈 · 값 줄 이음 합."""
|
||||
v, how = L["값"], None
|
||||
for r in reversed(results):
|
||||
rule = ex.fit(r[1], v)
|
||||
if rule:
|
||||
how = {"갈래": "복사", "줄": [r[0]], "규칙": rule}
|
||||
break
|
||||
if not how and flat(L["라벨"]) in ("계", "합계"):
|
||||
got: dict[str, dict] = {}
|
||||
for x in reversed(lines):
|
||||
c = x.get("비목")
|
||||
if c and x.get("꼴") in ("돈", "값") and c not in got:
|
||||
got[c] = x
|
||||
if len(got) == 3:
|
||||
break
|
||||
if got:
|
||||
rule = ex.fit(sum(x["값"] for x in got.values()), v)
|
||||
if rule:
|
||||
how = {"갈래": "합", "줄": sorted(x["차례"] for x in got.values()), "규칙": rule}
|
||||
if not how:
|
||||
acc, used = 0.0, []
|
||||
for x in reversed([x for x in lines if x.get("꼴") in ("돈", "값")]):
|
||||
if len(used) >= 25:
|
||||
break
|
||||
acc += x["값"]
|
||||
used.append(x["차례"])
|
||||
rule = ex.fit(acc, v)
|
||||
if rule and len(used) > 1:
|
||||
how = {"갈래": "합", "줄": used[::-1], "규칙": rule}
|
||||
break
|
||||
L["종류"] = "값줄"
|
||||
L["숨은"] = how
|
||||
L["규칙"] = how["규칙"] if how else None
|
||||
|
||||
|
||||
def _var(L: dict) -> None:
|
||||
segs = L.pop("조각")
|
||||
m = re.match(r"^\s*(" + NUMRE + r")", segs[-1]) if len(segs) > 1 else None
|
||||
if not m:
|
||||
L["종류"] = "변수글"
|
||||
return
|
||||
shown, toks = num(m.group(1)), None
|
||||
for seg in reversed(segs[1:-1]):
|
||||
toks = tokens(seg)
|
||||
if toks:
|
||||
break
|
||||
L["값"] = shown
|
||||
L["규칙"] = ex.fit(calc(ex.parse(toks)), shown) if toks else None
|
||||
L["종류"] = "변수식" if toks else ("변수값" if len(segs) == 2 else "변수기호")
|
||||
|
||||
|
||||
def parse_block(text: list[str], prices: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""블록 글 줄 → 복원 줄 목록(역할 · 숨은 합 · 자리 규칙 · 단가 정체)."""
|
||||
by_value = collections.defaultdict(list)
|
||||
for p in prices:
|
||||
by_value[p["값"]].append(p)
|
||||
lines: list[dict[str, Any]] = []
|
||||
results: list[tuple] = [] # (차례, 값, 비목)
|
||||
for k, raw in enumerate(text):
|
||||
L = {"차례": k, "원글": raw, **parse_line(raw)}
|
||||
if L["꼴"] == "돈":
|
||||
_money(L, by_value, results)
|
||||
results.append((k, L["값"], L["비목"]))
|
||||
elif L["꼴"] == "값":
|
||||
_value(L, lines, results)
|
||||
results.append((k, L["값"], L["비목"]))
|
||||
elif L["꼴"] == "변수":
|
||||
_var(L)
|
||||
lines.append(L)
|
||||
identify(lines)
|
||||
return lines
|
||||
|
||||
|
||||
def identify(lines: list[dict[str, Any]]) -> None:
|
||||
"""단가 토큰 정체 — 값 하나 · 기계 세 줄 겹침 · 문맥(라벨 · 머리 글 이름) · 남으면 모호 표지."""
|
||||
head = ""
|
||||
for L in lines:
|
||||
if L["꼴"] in ("글", "글끝") or _HEADING.match(L["원글"]):
|
||||
head = L["원글"]
|
||||
if L.get("종류") != "단가줄":
|
||||
continue
|
||||
role = next(r for r in L["역할"].values() if r["갈래"] == "단가")
|
||||
cands = role["후보"]
|
||||
ids = {_ident(p) for p in cands}
|
||||
how = "값"
|
||||
if len(ids) > 1 and cands[0]["종류"] in ("기계", "산근"):
|
||||
k0, inter = L["차례"], set(ids)
|
||||
for M in lines[max(0, k0 - 3) : k0 + 4]:
|
||||
if M is L or M.get("종류") != "단가줄":
|
||||
continue
|
||||
r2 = next(r for r in M["역할"].values() if r["갈래"] == "단가")
|
||||
ids2 = {_ident(p) for p in r2["후보"]}
|
||||
if ids2 & inter and len(ids2 & inter) < len(inter):
|
||||
inter &= ids2
|
||||
ids, how = inter, "기계 세 줄"
|
||||
if len(ids) > 1:
|
||||
ctx = flat(L["원글"]) + "|" + flat(head)
|
||||
hit = {x for x in ids if x[1] and (x[1] in ctx or x[1][:3] in ctx)}
|
||||
ids, how = ({x for x in hit if not x[2] or x[2] in ctx} or hit or ids), "문맥"
|
||||
role["모호"] = len(ids) != 1
|
||||
role["갈림"] = how
|
||||
role["남은"] = len(ids)
|
||||
pick = sorted(ids)[0]
|
||||
p = next(p for p in cands if _ident(p) == pick)
|
||||
role["정체"] = {"종류": p["종류"], "이름": p["이름"], "규격": p["규격"], "비목": p["비목"]}
|
||||
|
||||
@@ -7,9 +7,8 @@ STmate 셈 규칙(`_분석_649-G2` 1 장) — 줄 금액 = 단가 × (계수 + 1
|
||||
T% = 바로 앞 계표시 값 × 수량 · 계표시 = 아래 단 누계 · `(-=)` = 찍되 위 합에서 뺌 ·
|
||||
`->` = 번지엔 쌓되 합에서 뺌 · 블록 끝전 두 갈래(비목 버림 / 계 버림 + 큰 나머지) ·
|
||||
결과금액 조정(SBM = 계를 한 비목으로) · 계약단가 = 비목 × 낙찰률 원 버림.
|
||||
EST 셈 규칙(같은 문서 2 장) — 줄 = 복원 식(단가 · 앞줄 결과 · 상수) → 줄 자리 속성 ·
|
||||
값 줄 = 숨은 복사 · 합 → 자리 속성 · 블록 성분 = 그 값을 낸 마지막 줄.
|
||||
단가 바뀜 전파 — STmate = 단가 dict 를 바꿔 다시 셈 · EST = `run_est(lines, {정체 열쇠: 새 값})`.
|
||||
단가 바뀜 전파 — 단가 dict 를 바꿔 다시 셈. 옛 EST 엑셀 셈 · 엑셀 검산(`run_est` · `est_check` ·
|
||||
`stmate_check`)은 812-1 엑셀 읽기기로 걷음(EST = `M02_Doc_NativeEst`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,7 +21,6 @@ from M02_MasterTemplete import M02_San_Expr as ex
|
||||
|
||||
COMPS = ("L", "M", "S")
|
||||
SPECIAL = ("L%", "M%", "S%", "T%")
|
||||
TOL_LINE = 0.05
|
||||
|
||||
|
||||
def _zero() -> dict[str, float]:
|
||||
@@ -179,115 +177,3 @@ def stmate_block(block: dict, nodes: list[dict], prices: dict, rule: str) -> dic
|
||||
fin = {c: math.floor(round(kye[c] * nag / 100, 6)) for c in COMPS}
|
||||
fin["T"] = fin["L"] + fin["M"] + fin["S"]
|
||||
return {"줄": lines, "합": tot, "계": kye, "금액": fin, "낙찰": nag}
|
||||
|
||||
|
||||
def stmate_check(block: dict, result: dict) -> dict[str, Any]:
|
||||
"""줄마다 · 블록 검산 — 금액 줄(참조 · 율 · 특수) C~F 대조 · 계표시 줄 · 블록 머리 · 꼬리 계."""
|
||||
bad, n_amt, n_tot, ok_tot = [], 0, 0, 0
|
||||
for r, ln in zip(result["줄"], block["몸"], strict=True):
|
||||
want = {"L": ln["D"] or 0, "M": ln["E"] or 0, "S": ln["F"] or 0}
|
||||
if r["종류"] in ("참조", "율", "특수"):
|
||||
n_amt += 1
|
||||
if not r["값"] or any(abs(r["값"][c] - want[c]) >= TOL_LINE for c in COMPS):
|
||||
bad.append({"행": ln["행"], "엑셀": want, "셈": r["값"], "오류": r["오류"]})
|
||||
elif r["종류"] == "계" and ln["C"] is not None:
|
||||
n_tot += 1
|
||||
ok_tot += all(abs(r["값"][c] - want[c]) < 0.15 for c in COMPS)
|
||||
head = block["머리"]
|
||||
want = {"T": head["C"] or 0, "L": head["D"] or 0, "M": head["E"] or 0, "S": head["F"] or 0}
|
||||
kye = next((t for t in block["꼬리"] if t["A"] == "계"), None)
|
||||
want_k = {"T": kye["C"], "L": kye["D"], "M": kye["E"], "S": kye["F"]} if kye else None
|
||||
same = lambda x, y: all(abs((x[k] or 0) - (y[k] or 0)) < 0.5 for k in "TLMS") # noqa: E731
|
||||
ok = same(result["금액"], want) and (want_k is None or same(result["계"], want_k))
|
||||
return {
|
||||
"맞음": ok,
|
||||
"엑셀": want,
|
||||
"엑셀계": want_k,
|
||||
"금액줄": n_amt,
|
||||
"틀린줄": bad,
|
||||
"계줄": n_tot,
|
||||
"계줄맞음": ok_tot,
|
||||
}
|
||||
|
||||
|
||||
# ── EST ────────────────────────────────────────────────────────────────
|
||||
def key_of(ident: dict[str, Any]) -> tuple:
|
||||
"""EST 단가 정체 → 전파 열쇠 (종류, 이름, 규격, 비목)."""
|
||||
return (ident["종류"], ident["이름"], ident["규격"], ident["비목"])
|
||||
|
||||
|
||||
def run_est(lines: list[dict], newp: dict[tuple, float] | None = None) -> dict[int, float | None]:
|
||||
"""복원 줄 → {차례: 다시 셈 값}(줄 자리 속성 그대로) · newp = {정체 열쇠: 새 단가}."""
|
||||
newp = newp or {}
|
||||
val: dict[int, float | None] = {}
|
||||
for L in lines:
|
||||
k = L["차례"]
|
||||
if L.get("꼴") == "돈" and L.get("규칙"):
|
||||
sub = {}
|
||||
for i, r in L["역할"].items():
|
||||
if r["갈래"] == "단가" and key_of(r["정체"]) in newp:
|
||||
sub[i] = newp[key_of(r["정체"])]
|
||||
elif r["갈래"] == "앞줄" and val.get(r["줄"]) is not None:
|
||||
sub[i] = val[r["줄"]]
|
||||
v = _est_calc(L["식"], sub)
|
||||
val[k] = ex.RULE[L["규칙"]](v) if v is not None else None
|
||||
elif L.get("종류") == "값줄" and L.get("숨은"):
|
||||
how = L["숨은"]
|
||||
s = sum(
|
||||
(val.get(j) if val.get(j) is not None else lines[j]["값"]) or 0 for j in how["줄"]
|
||||
)
|
||||
val[k] = ex.RULE[how["규칙"]](s)
|
||||
return val
|
||||
|
||||
|
||||
def _est_calc(node: tuple, sub: dict) -> float | None:
|
||||
try:
|
||||
return ex.value(node, {"V": {}, "sub": sub})
|
||||
except ex.ExprError:
|
||||
return None
|
||||
|
||||
|
||||
def designate(lines: list[dict], want: dict[str, float]) -> dict[str, Any]:
|
||||
"""블록 성분(총괄표) → 그 값을 낸 줄(마지막 · 라벨 비목 우선) · 0 = 「영」 · 없음 = None."""
|
||||
out: dict[str, Any] = {}
|
||||
kinds = ("단가줄", "참조줄", "상수줄", "값줄")
|
||||
for c in COMPS:
|
||||
w = want[c]
|
||||
if not w:
|
||||
out[c] = "영"
|
||||
continue
|
||||
cand = [L for L in lines if L.get("종류") in kinds and abs(L["값"] - w) < ex.EPS]
|
||||
pick = [L for L in cand if L.get("비목") == c] or cand
|
||||
out[c] = pick[-1]["차례"] if pick else None
|
||||
return out
|
||||
|
||||
|
||||
def est_block(lines: list[dict], des: dict[str, Any], newp: dict | None = None) -> dict[str, Any]:
|
||||
"""성분 줄 → 블록 금액(성분 원 버림 · 계 = 합) · 줄 값."""
|
||||
val = run_est(lines, newp)
|
||||
raw = {
|
||||
c: (0.0 if k == "영" else (val.get(k) if k is not None else None)) for c, k in des.items()
|
||||
}
|
||||
if any(v is None for v in raw.values()):
|
||||
return {"줄": val, "성분": raw, "금액": None}
|
||||
fin = {c: math.floor(round(raw[c], 6)) for c in COMPS}
|
||||
fin["T"] = fin["L"] + fin["M"] + fin["S"]
|
||||
return {"줄": val, "성분": raw, "금액": fin}
|
||||
|
||||
|
||||
def est_check(lines: list[dict], want: dict[str, float]) -> dict[str, Any]:
|
||||
"""블록 검산 — 다시 셈 성분 = 총괄표 · 줄마다 보인 값 대조(돈 · 값 줄)."""
|
||||
des = designate(lines, want)
|
||||
got = est_block(lines, des)
|
||||
val = got["줄"]
|
||||
n, bad = 0, []
|
||||
for L in lines:
|
||||
if L.get("꼴") == "돈" or L.get("종류") == "값줄":
|
||||
n += 1
|
||||
v = val.get(L["차례"])
|
||||
if v is None or abs(v - L["값"]) >= ex.EPS:
|
||||
bad.append({"차례": L["차례"], "글": L["원글"].strip(), "보인": L["값"], "셈": v})
|
||||
fin = got["금액"]
|
||||
ok = fin is not None and all(abs(fin[c] - want[c]) < 0.5 for c in COMPS)
|
||||
ok = ok and abs(want["L"] + want["M"] + want["S"] - want["T"]) < 0.5
|
||||
return {"맞음": ok, "성분줄": des, "금액": fin, "줄수": n, "틀린줄": bad}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""경쟁사 산근 줄 → M01 요소 이음(PLAN 814-1 · `M02_San_Emit` 에서 뗌 — 셈 꼴 글 · 표지 그대로).
|
||||
|
||||
`m01` = 1차 줄 판정 유일 짝(인력 · 재료 · 기계 · 유가 · 환율) · `hourly_text` = 기계 시간당 경비 부름 글 ·
|
||||
`combo_link` = 조합 기계(굴착기 + 브레이커 · 집게) 우리 조합 식 부름 글.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
NOTE = "참고 · " # `M02_San_Emit.NOTE` 와 같음(사람 확인 표지 머리)
|
||||
OIL_BASE = ("전국평균", "일반용(갑)Ⅰ 저압") # 유가 · 전력 기본(요구 748 · 우리 기초 기본값)
|
||||
BASE_REGION = "서울" # 같은 이름 · 규격 자재가 지역마다 있을 때 기본(우리 기초 검색어 꼴)
|
||||
_REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」 · 「,서울」
|
||||
|
||||
|
||||
def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(812-9) 못 이으면 늘 표지. 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
if (
|
||||
kind == "기계"
|
||||
): # 812-9 — 하나 = 이음 · 아니면 표지(후보 여럿 · 못 찾음 · 조용히 넘기지 않음)
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
got = mfind.find(line["이름"], line["규격"])
|
||||
# 찾기 판정 없는 기계 줄 막음(814-1 · 줄 쌓기 표지 · 별칭)
|
||||
src.setdefault("찾기", {**got, "이름": got.get("이름") or line["이름"], "규격": got.get("규격") or line["규격"]}) # fmt: skip
|
||||
return got["키"], mfind.mark(got, line["이름"], line["규격"])
|
||||
if kind == "재료":
|
||||
if elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
|
||||
kind = "환율"
|
||||
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
|
||||
got = [
|
||||
{"키": r["키"], "규격": str(r.get("상세구분") or "")}
|
||||
for r in elem.oil_rows(line["이름"])
|
||||
]
|
||||
spec = line["규격"].replace(" ", "")
|
||||
got = (
|
||||
[x for x in got if x["규격"] == spec]
|
||||
or [x for x in got if spec and spec in x["규격"]]
|
||||
or got
|
||||
)
|
||||
else:
|
||||
got = hrow.m01_picks(kind, line)
|
||||
keys = {x.get("키") for x in got if x.get("키")}
|
||||
if (
|
||||
len(keys) > 1 and kind == "유가"
|
||||
): # 814-1 — 전국평균 · 전력 = 일반용(갑)Ⅰ 저압(지역 · 계약종별은 옵션)
|
||||
keys = {x["키"] for x in got if x["규격"] in OIL_BASE} or keys
|
||||
elif len(keys) > 1 and kind == "재료": # 814-1 — 지역 꼬리 뗀 이름 · 규격 정확 일치
|
||||
keys = exact(line, got) or keys
|
||||
if len(keys) == 1:
|
||||
return keys.pop(), None
|
||||
if len(keys) > 1 and kind in ("유가", "환율"):
|
||||
return None, f"{NOTE}{kind} 후보 여럿({len(keys)}) · 사람 확인"
|
||||
return None, None
|
||||
|
||||
|
||||
def _flat(text: object) -> str:
|
||||
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or ""))).lower()
|
||||
|
||||
|
||||
def region_of(name: object) -> tuple[str, str]:
|
||||
"""자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글)."""
|
||||
text = str(name or "")
|
||||
m = _REGION.search(text)
|
||||
return (text[: m.start()], m.group(1)) if m else (text, "")
|
||||
|
||||
|
||||
def exact(line: dict, got: list[dict]) -> set[str]:
|
||||
"""재료 후보 가운데 지역 꼬리 뗀 이름 · 규격이 줄과 정확히 같은 것(빈칸 · 전각 · 대소문자 정규화) —
|
||||
지역마다 여럿이면 줄 지역 › 서울 › 같은 단위 › 키 차례 첫째 하나 · 없음 = 빈 모음(사람 확인)."""
|
||||
name, mine = region_of(line.get("이름"))
|
||||
want = (_flat(name), _flat(line.get("규격")))
|
||||
hits = [x for x in got if x.get("키") and (_flat(region_of(x["이름"])[0]), _flat(x.get("규격"))) == want] # fmt: skip
|
||||
if not hits:
|
||||
return set()
|
||||
unit = _flat(line.get("단위"))
|
||||
|
||||
def rank(x: dict) -> tuple:
|
||||
where = region_of(x["이름"])[1]
|
||||
return (where != mine if mine else True, where != BASE_REGION, _flat(x.get("단위")) != unit, str(x["키"])) # fmt: skip
|
||||
|
||||
return {min(hits, key=rank)["키"]}
|
||||
|
||||
|
||||
def hourly_text(key: str | None) -> str | None:
|
||||
"""기계 EQ → 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 키 · 자료 없음 = None."""
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
return f"로직({got['키']}{''.join(f', {k}={v}' for k, v in got['입력'].items())})"
|
||||
|
||||
|
||||
def combo_link(one: dict, parts: list[dict]) -> str | None:
|
||||
"""조합 기계 둘이 우리 조합 식(GC000270 · 271) 고르기에 있으면 그 부름 글 · 줄 자원에 `조합식` 키."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||||
|
||||
got = mh.combo(parts[0]["키"], parts[1]["키"]) if len(parts) == 2 and all(p["키"] for p in parts) else None # fmt: skip
|
||||
if got:
|
||||
one["자원"]["조합식"] = got[0]
|
||||
return got[1] if got else None
|
||||
|
||||
|
||||
# ── 못 이은 줄 까닭(814-1 · 요구 745 C2) ─────────────────────────────────
|
||||
FREIGHT = ("운임", "화물", "운반비", "하차")
|
||||
|
||||
|
||||
def why(item: dict) -> str:
|
||||
"""이음 없는 가져온 줄 → 까닭 한 마디(문서 설명 · 띠 · 원문 카드) — 「없음」은 흠이 아니라 우리 정보로
|
||||
못 이은 까닭(자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 자료 없음 · 갈래 밖)."""
|
||||
res = item.get("자원") or {}
|
||||
kind = res.get("갈래") or item.get("종류")
|
||||
name = str(res.get("이름") or item.get("이름") or "")
|
||||
if res.get("조합식"):
|
||||
return f"조합 — 우리 조합 식 {res['조합식']} 에 듦"
|
||||
if kind == "기계":
|
||||
found = res.get("찾기") or {}
|
||||
verdict = found.get("판정")
|
||||
if verdict == "못 찾음":
|
||||
return (
|
||||
"별칭 대기"
|
||||
if str(found.get("까닭") or "").startswith("기종 모름")
|
||||
else "기계 표에 없음"
|
||||
)
|
||||
if verdict == "후보 여럿":
|
||||
return "기계 후보 여럿 — 고름"
|
||||
return "시간당 경비 자료 없음" if found.get("키") else "기계 찾기 없음"
|
||||
if kind == "재료":
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
if elem.oil_rows(name):
|
||||
return "유가 후보 여럿 — 고름"
|
||||
line = {"이름": name, "규격": res.get("규격") or "", "단위": res.get("단위") or ""}
|
||||
if hrow.m01_picks("재료", line):
|
||||
return "자재 후보 여럿 — 고름"
|
||||
return "운임표 없음" if any(w in name for w in FREIGHT) else "자재 없음(우리 자재표)"
|
||||
if kind == "인력":
|
||||
return "노임 없음"
|
||||
if kind == "단가산출":
|
||||
return "안쪽 단가산출 못 이음"
|
||||
return "갈래 밖(견적 · 고정 값)"
|
||||
|
||||
|
||||
def tally(logic: dict) -> dict:
|
||||
"""우리 정보로 셈 — 자원 줄 {됨, 줄, 조합(형제 단가산출 부름뿐)} (연결 판정 「없음」 띠 · 814-1)."""
|
||||
items = logic.get("호표") or []
|
||||
done = sum(
|
||||
1 for x in items if x.get("이음") or x.get("참조") or (x.get("자원") or {}).get("조합식")
|
||||
)
|
||||
inner = [x for x in items if (x.get("자원") or {}).get("갈래") == "단가산출" or x.get("참조")]
|
||||
return {"됨": done, "줄": len(items), "조합": bool(items) and len(inner) == len(items)}
|
||||
@@ -141,8 +141,9 @@ def resources(sheets: dict[str, list]) -> dict[str, list[dict[str, Any]]]:
|
||||
got = []
|
||||
for one in _table(sheets.get(sheet) or [], ()):
|
||||
r = list(one["줄"]) + [None] * 30
|
||||
vals = {k: float(v or 0) for k, v in book.applied(r).items()}
|
||||
got.append({**_drop(one), "표": sheet, "값": vals})
|
||||
vals, note = book.applied_how(r)
|
||||
vals = {k: float(v or 0) for k, v in vals.items()}
|
||||
got.append({**_drop(one), "표": sheet, "값": vals} | ({"칸": note} if note else {}))
|
||||
out[kind] = got
|
||||
return out
|
||||
|
||||
|
||||
@@ -300,6 +300,45 @@ class Lines:
|
||||
self.plain(ln)
|
||||
return self.done(ln)
|
||||
|
||||
def sections(self, saved: dict) -> None:
|
||||
"""절 소계(가 · 나 … 머리 아래 합계 줄 「노무비 : a + b = X」)가 뺀 줄 하나 = 합에 안 든 줄(비목 없음) —
|
||||
절(앞 숫자 머리부터) 안 그 비목 줄 결과 합 − X 가 꼭 한 줄 값이고 나머지 줄 값은 모두 덧셈 글에 있을
|
||||
때만 · 블록 금액도 그 줄을 빼야 맞을 때만(엑셀 글 덧셈 · 원파일 SCheck 0)."""
|
||||
lines, start = self.lines, 0
|
||||
for i, ln in enumerate(lines):
|
||||
text = ln["보임"]
|
||||
if ln["단"] == 1 and re.match(r"^\s*\d", text):
|
||||
start = i
|
||||
if not (ln["합계"] and ln["비목"] in ("L", "M", "S") and ln["결과"] and "+" in text):
|
||||
continue
|
||||
head = next((k for k in range(i - 1, start - 1, -1) if lines[k]["단"] == 1), None)
|
||||
if head is None or not re.match(r"^\s*[가-하]", lines[head]["보임"]):
|
||||
continue
|
||||
part, want = ln["비목"], ln["결과"][-1]
|
||||
kids = [
|
||||
x for x in lines[start:head] if x["비목"] == part and x["결과"] and not x["합계"]
|
||||
]
|
||||
kids += [
|
||||
x for x in lines[start:head] if x["비목"] == part and x["줄참조"] and not x["결과"]
|
||||
]
|
||||
value = {id(x): (x["결과"] or x["줄참조"])[-1] for x in kids}
|
||||
extra = sum(value.values()) - want
|
||||
odd = [x for x in kids if abs(value[id(x)] - extra) < 0.5]
|
||||
colon = _COLON.search(text)
|
||||
adds = [v for _, _, v in _nums(text, colon.end() if colon else 0, text.rfind("="))]
|
||||
rest = [value[id(x)] for x in kids if odd and x is not odd[0]]
|
||||
listed = all(any(_close(v, a) for a in adds) for v in rest)
|
||||
if len(odd) != 1 or abs(extra) < 0.5 or not listed:
|
||||
continue
|
||||
every = sum(
|
||||
(x["결과"] or x["줄참조"])[-1]
|
||||
for x in lines
|
||||
if x["비목"] == part and not x["합계"] and (x["결과"] or x["줄참조"])
|
||||
)
|
||||
block = float(saved.get(part) or 0) # 블록 금액이 그 줄을 빼야 맞을 때만
|
||||
if abs(every - block) >= 1 and abs(every - extra - block) < 1:
|
||||
odd[0]["비목"] = None
|
||||
|
||||
def unused(self) -> None:
|
||||
"""머리 아닌 줄의 기계 이름 — 아래 세 줄이 안 쓰면 글 줄(설명 속 장비 이름)."""
|
||||
for h in self.heads:
|
||||
@@ -597,10 +636,13 @@ def read(sheets: dict[str, list]) -> dict[str, Any]:
|
||||
}
|
||||
if not one:
|
||||
tags.append(f"{sx.NOTE}총괄표에 단산{b['번']} 없음")
|
||||
elif one.get("칸"): # 총괄표 당초 / 적용 칸(797-1a)
|
||||
tags.append(f"참고 · {one['칸']}")
|
||||
work = Lines(look, tags)
|
||||
for src in b["줄"]:
|
||||
work.run(src)
|
||||
work.unused()
|
||||
work.sections(head["저장"] or {})
|
||||
lines = []
|
||||
for ln in work.lines:
|
||||
marks = ln.pop("_marks")
|
||||
|
||||
@@ -59,7 +59,14 @@ def read(sheets: dict[str, list]) -> dict[str, Any]:
|
||||
head = {"코드": blk["코드"], "이름": blk.get("이름") or me.get("이름")}
|
||||
head |= {"규격": me.get("규격"), "단위": me.get("단위"), "저장": _amt(blk["머리"])}
|
||||
_, tot = ev.run_stmate([ln["풀이"] for ln in lines], prices)
|
||||
out["산근"].append({**head, "줄": lines, "줄합": tot, "표지": tags, "엑셀행": blk["행"]})
|
||||
body = [{k: ln.get(k) for k in ("행", "B", "C", "D", "E", "F")} for ln in blk["몸"]]
|
||||
out["산근"].append(
|
||||
{**head, "줄": lines, "줄합": tot, "표지": tags, "엑셀행": blk["행"]}
|
||||
| {
|
||||
"몸": body,
|
||||
"꼬리": blk["꼬리"],
|
||||
} # 엑셀 줄 금액 · 계 꼬리(검산 · `M02_San_Emit.stmate`)
|
||||
)
|
||||
rule, got = vote(out["산근"], prices)
|
||||
out["옵션"] = {"SAN_TYP": RULE[rule]}
|
||||
out["내역"] = None
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
같은 내용 SK 가 있으면 그것(다시씀).
|
||||
블록 번 = 판정 블록 번(STmate = 코드 「D00102」 숫자 · EST = 단가산출 블록 번).
|
||||
STmate 블록 끝전 = 파일 투표(`M02_San_Xlsx_Stmate.vote`).
|
||||
원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) — 있으면 원파일 우선
|
||||
(그 블록 번이 원파일에 있을 때) · 엑셀 올림 때 같은 공사 원파일이 다른 HP 에 있으면 붙임 · 원파일 올림 =
|
||||
같은 공사 엑셀 HP 에 붙임(없으면 원파일로 새 HP · `upload_native`).
|
||||
원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) · 올리기 = 파일 하나
|
||||
(814-1 · 요구 742-1 — 같은 공사 엑셀 · 원파일 붙이기 걷음) · 원파일 = 새 HP(단가산출표 판 격자 · 일위대가 없음 ·
|
||||
`upload_native`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -63,9 +63,7 @@ def keep(folder: str | Path, key: str, name: str, data: bytes) -> str | None:
|
||||
return f"산근 시트 읽기 오류 — {type(e).__name__}: {str(e)[:80]}"
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
got = {"파일": name, "시트": sheets, "엑셀": excel}
|
||||
native = next((n for n in _natives(folder) if fn.same(n, sheets)), None)
|
||||
atomic_write_json(path, got | ({"원파일": native} if native else {}))
|
||||
atomic_write_json(path, {"파일": name, "시트": sheets, "엑셀": excel})
|
||||
return None
|
||||
|
||||
|
||||
@@ -85,23 +83,8 @@ def _excel_of(path: Path, saved: dict) -> dict:
|
||||
|
||||
|
||||
# ── 원파일(810-2) ──────────────────────────────────────────────────────
|
||||
def _saved(folder: str | Path) -> list[tuple[Path, dict]]:
|
||||
out = []
|
||||
for path in sorted((hopyo_store._dir(folder) / DIRNAME).glob("HP-*.json")):
|
||||
try:
|
||||
out.append((path, json.loads(path.read_bytes())))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _natives(folder: str | Path) -> list[dict]:
|
||||
return [d["원파일"] for _, d in _saved(folder) if isinstance(d.get("원파일"), dict)]
|
||||
|
||||
|
||||
def upload_native(folder: str | Path, name: str, data: bytes, who: Any) -> dict[str, Any]:
|
||||
"""원파일 올림 — 같은 공사 엑셀 HP 에 원파일을 붙임(`붙음` 키들 · 첫 HP 답) · 없으면 원파일로
|
||||
새 HP(EST = 일위대가 호표 · STmate = 호표 없음 · 산근만). 같은 파일 다시 = 그 HP(`이어서`)."""
|
||||
"""원파일 올림 — 새 HP(단가산출표 판 격자 · 산근만 · 일위대가 없음 · 814-1) · 같은 파일 다시 = 그 HP(`이어서`)."""
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
if same := next((r for r in hopyo_store.rows(folder) if r.get("지문") == mark), None):
|
||||
return {**same, "이어서": True, "원파일": True}
|
||||
@@ -111,18 +94,13 @@ def upload_native(folder: str | Path, name: str, data: bytes, who: Any) -> dict[
|
||||
native = fn.read(name, data)
|
||||
except fn.NativeError as e:
|
||||
raise store.StoreError(400, str(e)) from e
|
||||
joined = []
|
||||
for path, got in _saved(folder):
|
||||
if got.get("시트") and fn.same(native, got["시트"]):
|
||||
atomic_write_json(path, {**got, "원파일": native})
|
||||
joined.append(path.stem)
|
||||
rows = {r["키"]: r for r in hopyo_store.rows(folder)}
|
||||
if joined and joined[0] in rows:
|
||||
return {**rows[joined[0]], "원파일": True, "붙음": joined}
|
||||
row = hopyo_store.save(folder, name, fn.hopyo(native), who, None, mark)
|
||||
path = _path(folder, row["키"])
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "원파일": native})
|
||||
|
||||
def first(key: str) -> None: # HP 를 목록에 보이기 전에 산근 보관(814-1)
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "원파일": native})
|
||||
|
||||
row = hopyo_store.save(folder, name, fn.hopyo(native), who, fn.grid(native), mark, first)
|
||||
return {**row, "원파일": True}
|
||||
|
||||
|
||||
|
||||
@@ -304,11 +304,13 @@ def same_part(doc: Any) -> dict | None:
|
||||
|
||||
|
||||
def unlinked(doc: dict) -> list[dict]:
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름}]`."""
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름, 까닭}]`(까닭 = 814-1 `M02_San_Link.why`)."""
|
||||
from M02_MasterTemplete import M02_San_Link as link
|
||||
|
||||
row = row_of(doc) or {}
|
||||
ids = sd.line_ids(row)
|
||||
return [
|
||||
{"자리": ids[n], "이름": one.get("이름") or ""}
|
||||
{"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)}
|
||||
for n, one in enumerate(doc[KEY]["로직"].get("호표") or [])
|
||||
if not one.get("이음") and not one.get("참조")
|
||||
]
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
* 가져온 식 ↔ 우리 M01 기초 연결 판정 띠 자료(PLAN 810-4 · 요구 707 · 709 · 703) — 원문 패널 카드 위.
|
||||
* SK `가져온.원본.연결후보`(서버 `M02_Doc_Match` 가 적음) → 띠 글 · 다른 항 · 단추 하나:
|
||||
* 확정 = [우리 기초 사본으로 바꾸기] · 근접 · 없음 = 단추 없음(703) · 안 찾음 = [연결 찾기] ·
|
||||
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기]
|
||||
* 다른 항 = 로직 차이(확정을 막음) · 참고 = 단가 · 끝수 · 율 값 %(값 차이 — 「우리 단가로 셈 ○○ 원」 · 812-2 · 812-4) ·
|
||||
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기] ·
|
||||
* 같은 기종 규격 · 수량 차이 = 참고(812-11) · 기계 작업 = 기계 한 줄 단가산출(우리 기초에 안 붙임 · 단추 없음)
|
||||
* 화면 · DOM · fetch 없음
|
||||
* ========================================================================== */
|
||||
|
||||
export type MatchVerdict = "확정" | "근접" | "없음";
|
||||
export type MatchVerdict = "확정" | "근접" | "없음" | "기계 작업";
|
||||
|
||||
export interface MatchFound {
|
||||
판정?: MatchVerdict;
|
||||
@@ -15,9 +17,13 @@ export interface MatchFound {
|
||||
이름?: string;
|
||||
옵션?: Record<string, string>;
|
||||
다른?: { 항: string; 글: string }[];
|
||||
/** 값 차이(단가 · 끝수 · 율 값 · 규격 · 수량) — 확정을 안 막음 */
|
||||
참고?: { 항: string; 글: string }[];
|
||||
금액?: { 가져온?: Record<string, number | null>; 우리?: Record<string, number | null> };
|
||||
후보수?: number;
|
||||
가까움?: { 키?: string; 이름?: string; 다른?: { 항: string; 글: string }[] };
|
||||
/** 우리 정보로 셈(814-1) — 자원 줄 이음 수 · 형제 단가산출 부름뿐 = 조합 */
|
||||
우리정보?: { 됨?: number; 줄?: number; 조합?: boolean };
|
||||
}
|
||||
|
||||
export type MatchAction = "찾기" | "바꾸기" | "되돌리기" | null;
|
||||
@@ -44,6 +50,29 @@ const opts = (o?: Record<string, unknown>): string => {
|
||||
const lines = (d?: { 항: string; 글: string }[]): string[] =>
|
||||
(d ?? []).map((x) => `${x.항} — ${x.글}`);
|
||||
|
||||
const won = (n?: number | null): string =>
|
||||
n == null ? "" : `${Math.round(n).toLocaleString("ko-KR")} 원`;
|
||||
const QTY = ["수량", "수량 끝자리", "품 숫자"];
|
||||
/** 참고 요약 — 「참고: 단가 다름 3 줄 · 율 값 다름 1 줄 · 우리 단가로 셈 53,218 원」 · 참고 없음 = "" */
|
||||
const noteText = (f: MatchFound): string => {
|
||||
const n = f.참고 ?? [];
|
||||
if (!n.length) return "";
|
||||
const price = n.filter((x) => x.항 === "단가 차이").length;
|
||||
const rate = n.filter((x) => x.항 === "율 값").length;
|
||||
const spec = n.filter((x) => x.항 === "규격").length;
|
||||
const qty = n.filter((x) => QTY.includes(x.항)).length;
|
||||
const bits = [
|
||||
spec ? `규격 다름 ${spec} 줄` : "",
|
||||
qty ? `수량 다름 ${qty} 줄` : "",
|
||||
price ? `단가 다름 ${price} 줄` : "",
|
||||
rate ? `율 값 다름 ${rate} 줄` : "",
|
||||
n.some((x) => x.항 === "옵션 확인") ? "옵션 확인" : "",
|
||||
n.some((x) => x.항 === "끝수") ? "끝수 다름" : "",
|
||||
].filter(Boolean);
|
||||
const ours = won(f.금액?.우리?.계);
|
||||
return ` · 참고: ${bits.join(" · ")}${ours ? ` · 우리 단가로 셈 ${ours}` : ""}`;
|
||||
};
|
||||
|
||||
/** SK → 띠 자료 · 가져온 식도 · 바꾼 SK 도 아니면 null */
|
||||
export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
|
||||
const kept = sk?.가져온보관;
|
||||
@@ -60,12 +89,20 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
|
||||
if (!f?.판정) {
|
||||
return { tone: "info", text: "우리 기초와 연결을 아직 안 찾음", details: [], action: "찾기" };
|
||||
}
|
||||
if (f.판정 === "기계 작업") {
|
||||
return {
|
||||
tone: "info",
|
||||
text: "기계 한 줄 단가산출 — 그대로 씀 · 값은 [단가 갱신]으로 우리 값",
|
||||
details: lines(f.참고),
|
||||
action: null,
|
||||
};
|
||||
}
|
||||
const base = `${f.키 ?? ""} ${f.이름 ?? ""} ${opts(f.옵션)}`.trim();
|
||||
if (f.판정 === "확정") {
|
||||
return {
|
||||
tone: "success",
|
||||
text: `우리 기초 ${base} 와 확정 일치`,
|
||||
details: [],
|
||||
text: `확정 — 로직 같음 · 우리 기초 ${base}${noteText(f)}`,
|
||||
details: lines(f.참고),
|
||||
action: "바꾸기",
|
||||
};
|
||||
}
|
||||
@@ -73,15 +110,19 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
|
||||
const kinds = [...new Set((f.다른 ?? []).map((x) => x.항))].join(" · ");
|
||||
return {
|
||||
tone: "warning",
|
||||
text: `근접 — ${base} · 다른 항: ${kinds}`,
|
||||
details: lines(f.다른),
|
||||
text: `근접 — ${base} · 다른 항: ${kinds}${noteText(f)}`,
|
||||
details: [...lines(f.다른), ...lines(f.참고).map((x) => `참고 ${x}`)],
|
||||
action: null,
|
||||
};
|
||||
}
|
||||
const near = f.가까움;
|
||||
const mine = f.우리정보; // 「없음」 = 흠 아님(814-1 · 요구 745) — 우리 정보로 다시 만든 정도
|
||||
const made = mine?.줄 ? ` — 우리 정보로 셈 ${mine.됨 ?? 0}/${mine.줄} 줄` : "";
|
||||
return {
|
||||
tone: "info",
|
||||
text: `없음 — 우리 기초 후보 ${f.후보수 ?? 0} 개와 맞지 않음`,
|
||||
text: mine?.조합
|
||||
? `조합 — 다른 단가산출 ${mine.줄 ?? 0} 개 묶음(안의 단가산출로 셈)`
|
||||
: `우리 기초 없음${made}`,
|
||||
details: near ? [`가장 가까운 ${near.키 ?? ""} ${near.이름 ?? ""}`, ...lines(near.다른)] : [],
|
||||
action: null,
|
||||
};
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface Origin {
|
||||
요약?: string;
|
||||
/** 원파일 확정 「확정 N · 표지 N · 추정 N · 확정률 N %」 */
|
||||
확정?: string;
|
||||
/** 확정 칸 이름 — 원파일 = 「원파일 확정」 · 엑셀 = 「엑셀 읽기 확정」(814-1) */
|
||||
확정이름?: string;
|
||||
}
|
||||
|
||||
const BOOKS: [RegExp, string][] = [
|
||||
@@ -111,6 +113,7 @@ export function originOf(meta: OriginMeta | null | undefined): Origin | null {
|
||||
...(sure
|
||||
? {
|
||||
확정: `확정 ${sure.확정 ?? 0} · 표지 ${sure.표지 ?? 0} · 추정 ${sure.추정 ?? 0} · 확정률 ${(Math.floor(rate * 1000) / 10).toFixed(1)} %`,
|
||||
확정이름: src.원파일 ? "원파일 확정" : "엑셀 읽기 확정",
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
@@ -99,6 +99,12 @@ export function priceSym(line: MoneyLine): string | null {
|
||||
}
|
||||
|
||||
/** 금액 줄 하나만 단가로 쓰는 연결 기호(요소 · 단가산출) — {줄 차례: 기호} */
|
||||
/** 가져온 기계 줄 단가 기호(812-9 `가져온.기계`) — 못 이어 숫자 연결이어도 그 줄 기계(이름 단추 · 고르기) */
|
||||
export function machineSyms(doc: FormulaDoc): Set<string> {
|
||||
const got = (doc as { 가져온?: { 기계?: { 기호?: string }[] } }).가져온?.기계;
|
||||
return new Set((Array.isArray(got) ? got : []).map((x) => String(x?.기호 ?? "")));
|
||||
}
|
||||
|
||||
export function ownedOf(doc: FormulaDoc): Map<number, string> {
|
||||
const count = new Map<string, number>();
|
||||
const bump = (ns: string[]): void => ns.forEach((n) => count.set(n, (count.get(n) ?? 0) + 1));
|
||||
@@ -107,12 +113,13 @@ export function ownedOf(doc: FormulaDoc): Map<number, string> {
|
||||
if (doc.결과) bump([doc.결과]);
|
||||
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
|
||||
const lines = new Set((doc.식 ?? []).map((l) => String(l.기호 ?? "")));
|
||||
const mach = machineSyms(doc);
|
||||
const out = new Map<number, string>();
|
||||
(doc.금액 ?? []).forEach((l, i) => {
|
||||
const s = priceSym(l);
|
||||
const kind = s ? vars.get(s)?.연결?.갈래 : undefined;
|
||||
if (s && (kind === "요소" || kind === "단가산출") && count.get(s) === 1 && !lines.has(s))
|
||||
out.set(i, s);
|
||||
const mine = kind === "요소" || kind === "단가산출" || (!!s && mach.has(s));
|
||||
if (s && mine && count.get(s) === 1 && !lines.has(s)) out.set(i, s);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -137,6 +144,7 @@ export function kindOf(
|
||||
const lb = link.키 ? String(link.키).startsWith("LB") : line.비목 === "노무비"; // 고르기 전 = 비목으로
|
||||
return lb ? "인력" : "재료";
|
||||
}
|
||||
if (s && machineSyms(doc).has(s)) return "기계"; // 못 이은 가져온 기계 줄(812-9)
|
||||
return "율";
|
||||
}
|
||||
|
||||
@@ -150,10 +158,11 @@ export function sourceOf(doc: FormulaDoc, v: DocVar): string | null {
|
||||
return String((v as { 출처?: string }).출처 ?? (head.원파일 ? "원파일" : "엑셀"));
|
||||
}
|
||||
|
||||
/** 차림에 안 넣는 기호 — 옵션 · 금액 줄이 가진 연결 · 가져온 식 값 옵션 칸 */
|
||||
/** 차림에 안 넣는 기호 — 옵션 · 금액 줄이 가진 연결(가져온 식 값 옵션 칸은 공식 묶음에도 보임 · 같은 값 · 812-3)
|
||||
* 파이썬 거울 = `M02_Doc_NativeLayout.py`(stackOf · saveOf · hiddenOf · 가져온 단가산출 차림) — 고칠 때 같이 고침 */
|
||||
export function hiddenOf(doc: FormulaDoc): Set<string> {
|
||||
const out = new Set(ownedOf(doc).values());
|
||||
for (const v of doc.변수 ?? []) if (isOption(v) || sourceOf(doc, v)) out.add(varId(v));
|
||||
for (const v of doc.변수 ?? []) if (isOption(v)) out.add(varId(v));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ export function repick(doc: FormulaDoc, i: number, id: string, kind: MoneyKind,
|
||||
const now = linkOf(kind, p);
|
||||
if (now.갈래 === "요소" && was.지역) now.지역 = was.지역; // 지역 옵션 이음 그대로
|
||||
if (now.갈래 === "단가산출" && was.키 === now.키) now.입력 = { ...was.입력, ...now.입력 }; // 같은 로직 = 다른 넘길 입력 그대로
|
||||
// 가져온 기계 줄(812-9) — 엑셀 값 기억 그대로(엑셀 단가 기준 금액 안 바뀜 · 못 이은 기계 = 숫자 값 · 묶음)
|
||||
const kept = (was as { 엑셀?: unknown }).엑셀 ?? (was.갈래 === "숫자" ? was.값 : undefined);
|
||||
if (kind === "기계" && kept !== undefined) (now as { 엑셀?: unknown }).엑셀 = kept;
|
||||
const cost = (line as { 비목?: string }).비목 ?? "";
|
||||
if (kind === "기계" && ["노무비", "재료비", "경비"].includes(cost)) now.칸 = cost; // 비목 한 칸 줄 = 그 칸
|
||||
v.연결 = now;
|
||||
v.이름 = p.이름;
|
||||
line.이름 = p.이름;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,72 @@
|
||||
# 불러오기 방향 장부 (요구 743 ~) — 결정만 쌓음 · 코드는 한 번에
|
||||
|
||||
## 0. 기준(요구 745)
|
||||
|
||||
- 불러오기의 목적 = 우리 단가산출 기초를 만드는 것이 아님. **경쟁사 단가산출을 우리 인력 · 자재 · 기계 · 다른 단가산출 정보로 다시 만들어 내는 것**(우리 기초와 같은 꼴로).
|
||||
- 그러므로 잣대 = 「우리 특정 기초와 같은가」가 아니라 **「우리 정보로 만들어 낼 수 있는가」** = 블록의 모든 줄이 우리 요소(노임 · 자재 · 기계 · 유가 · 환율 · 형제 단가산출 · 요율표)에 이어져 [우리 단가로] 셈이 되는가.
|
||||
- 연결 찾기(우리 기초와 맞대기)는 보조 — 확정이면 [바꾸기]로 우리 기초 사본을 쓸 수 있을 뿐, 「없음」이 흠은 아님.
|
||||
- 그래서 앞으로 재는 수 = 요소 이음률(지금 인력 100 · 기계 90.8 · 재료 72.0 · 유가 0 · 형제 100 %) · [우리 단가로] 전부 우리 값(원파일 71.9 · 엑셀 74.6 %) · 못 이은 줄의 까닭별 수.
|
||||
|
||||
기준 = 812-12 정합성 보고(`_보고_812-12_불러오기정합성.md`) · 아티팩트 네 묶음(정합성 · 연결 · 흠 16 · 정할 것 4). 줄마다 상태 = 결정 · 협의 중 · 조사 필요.
|
||||
|
||||
## A. 올리기 · 표현
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| A1 | 올리는 파일 | 하나만(엑셀 또는 경쟁사 확장자) · 「같은 공사 원파일 우선 붙이기」 걷음 | 결정 | 요구 742-1 · 743-1 |
|
||||
| A2 | 확장자 파일(.estx · .STC)의 표현 | 단가산출표(산출근거 머리 목록) + 산출근거만 · 일위대가 제외 | 결정 | 요구 743-2 · 흠 5 |
|
||||
| A3 | 엑셀 단가산출 이름의 규격 · 단위 꼬리 | 보이는 이름은 그대로 · 우리 기초 연결 때만 꼬리 뗀 이름으로 맞댐 | 결정 | 요구 743-3 · 흠 9 |
|
||||
|
||||
## B. 읽기 흠(심각)
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| B1 | STmate 기계 비목 세 줄(노 무 비 · 재 료 비 · 경 비) | 기계 한 줄로 묶음 · 조합이면 묶은 뒤 기계마다 그룹 · 금액 그대로 | 결정 | 요구 744 · 흠 1 · 285 블록 |
|
||||
| B2 | EST 덤프 「노무비 · 재료비 · 경비」 세 줄 + 조합 엇갈림 | 세 줄 묶은 뒤 조합 가름 | 결정 | 요구 744 · 흠 2 · 83 블록 |
|
||||
| B3 | 시간당 경비 계산식이 안 붙는 기계 26 종(대형 브레이커 · 부착용 집게 · 자동덮개시설 · 진동파일 해머 · 로더(타이어) 1.72 …) | 조사(요구 746) = 품셈 원문에 다 있음(손료 산정표 8-3-1 · 8-5-1 에 그 기계 · 8-4-8 주⑤ 부착 장비 조합 셈법) · 우리 기계표 630 대 모두 자료 있음 · 빠진 것 = 시간당 경비 계산식(GC000267~276)의 「기계 목록」이 246 대만 담음 → 384 대(집재장비 · 스크린 · 엔진 · 준설선 · 크러셔 · 부착 장비 …)가 어느 식에도 없음 · 26 종은 그중 경쟁사 파일에 쓰인 것 → 안: 계산식 기계 목록을 기계표 전체로(자료에 따라 식 가족 자동 — 연료 · 조종원 있음 = 기계경비 식 · 없음 = 손료 식) · 이름으로 이음 · 굴착기 + 브레이커 · + 집게는 우리 조합 식(GC000270 · 271) | 결정 — 원문에 있는데 빠진 기계만 계산식 목록에 더함(지어 넣는 자료 0) | 요구 744 · 745 · 746 · 747 · 흠 4 |
|
||||
| B4 | M02 단가산출 목록 65~75 초 | 나중에 화면 속도와 한 번에 | 보류(요구 749) | 흠 3(불러오기 밖 · 화면) |
|
||||
|
||||
## C. 연결 찾기
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| C1 | 공구손료 율의 비목(실무 재료비 ↔ 우리 경비) | 값(참고) · 확정 · [바꾸기] 하면 우리 비목(경비)으로 바꿈 | 결정 | 요구 744 · 정할 것 1 |
|
||||
| C2 | 「없음」 블록의 처리 | 기준 0 으로 봄 — 「품셈 밖」 표지 안 함 · 우리 기초와 맞추지 않음 · 그 블록의 줄이 우리 요소로 다 이어지면 「만들어짐」 · 못 이은 줄만 까닭(자재 없음 · 운임표 없음 · 지자체 품 …)을 표시 · 조합 묶음은 안의 참조로 | 결정 | 요구 745 |
|
||||
| C3 | 후보 고르기(이름 · 절 근거 먼저 · 덧말 뺀 이름 · 엉뚱한 근접 막기) | — | 협의 중 | 흠 6 · 7 · 8 |
|
||||
| C4-유가 | 유가 · 전력 29 줄이 전부 「후보 여럿」 | 조사(요구 747) = 이름은 다 맞음(경유 → 자동차용경유 · 등유 → 실내등유 · 전력 → 전력량요금) · 우리 표가 유종마다 전국평균 + 전국 + 16 지역 = 18 줄 · 전력은 계약종별 2 줄이라 「하나일 때만 잇는」 규칙에 늘 걸림 → 안: 유가 = 전국평균 줄에 잇고 지역은 문서 지역 옵션으로(우리 기초와 같은 꼴) · 전력 = 우리 기초가 쓰는 기본 계약종별에 잇고 계약종별 옵션 · 엑셀 값은 검산용으로 기억 | 결정(요구 748) — 유가 = 전국(우리 기초 기본값과 같은 「전국평균」 줄 · 지역은 옵션 · 사용자가 나중에 바꿈) · 전력 = 「일반용(갑)Ⅰ 저압」 기본(공사용 전력 · 주택용 아님) · 계약종별 옵션 · 엑셀 값은 검산용 | 흠 10 |
|
||||
| C4-재료 | 재료 후보 여럿(EST 원파일 자재 쓰임 1,553 중 289 · 53 종) | 조사(요구 748) = 53 종 가운데 50 종(쓰임 283)은 후보 다섯 안에 **이름 · 규격이 정확히 같은 줄**이 있음(유리알 1호 고휘도 · 합판(내수) 12t · 철못 N 25 · 구역화물 8.0톤 30km · 급수호스 2"×10m · 잔디 0.3 …) — 우리 자재표가 이름 뒤에 「, 서울」 같은 지역 꼬리를 달거나 색 · 규격이 다른 형제를 같이 내놓아 「하나」가 못 됨 · 나머지 3 종(고무테이프 · 초류종자 · PVC D=100)만 진짜 없음 → 안: 지역 꼬리 뗀 뒤 이름 · 규격이 정확히 같은 줄이 있으면 그것으로 잇고 지역은 옵션(유가와 같은 꼴) · 정확 일치가 없을 때만 「사람 확인」 | 결정(요구 749) | 흠 10 |
|
||||
|
||||
## D. 가벼움 · 도구
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 조합 첫 줄 기계 칩 글 · 소계 검산 표지 · 찾기 없는 기계 줄 7 · 올린 직후 409 · 엑셀 블록 「원파일 확정」 글 · 전수 도우미 str() | 코드 한 번에 할 때 같이 | 결정(묶음) | 흠 11 ~ 16 |
|
||||
|
||||
## E. 수정 사항 정리(코드 한 번에 할 때의 목록 · 요구 749)
|
||||
|
||||
| # | 고칠 것 | 근거 |
|
||||
|---|---|---|
|
||||
| 1 | 올리기 = 파일 하나(엑셀 또는 확장자) · 「같은 공사 원파일 우선 붙이기」 걷음 | A1 |
|
||||
| 2 | 확장자 파일(.estx · .STC) = 산출근거 머리로 단가산출표 판 + 산출근거 · 일위대가 안 만듦 | A2 |
|
||||
| 3 | 연결 찾기 · 요소 잇기 때 엑셀 이름의 규격 · 단위 꼬리 뗀 이름으로 맞댐(보이는 이름 그대로) | A3 |
|
||||
| 4 | STmate X 코드 L · M · S 세 줄 · EST 「〇〇 노무비 · 재료비 · 경비」 세 줄 = 기계 한 줄로 묶음 → 조합이면 그 뒤 기계마다 그룹 | B1 · B2 |
|
||||
| 5 | 시간당 경비 계산식(GC000267~276)의 기계 목록에 원문에 있는데 빠진 기계 더함(자료에 따라 기계경비 식 / 손료 식) · 조합(굴착기 + 브레이커 · + 집게)은 우리 조합 식 | B3 |
|
||||
| 6 | 공구손료 율의 비목 차이 = 값(참고) · [바꾸기] 때 우리 비목 | C1 |
|
||||
| 7 | 「없음」 = 흠 아님 · 「품셈 밖」 표지 없음 · 못 이은 줄만 까닭 표시 · 조합 묶음은 안의 참조로 · 잣대 = 요소 이음률 · [우리 단가로] | C2 · 기준 0 |
|
||||
| 8 | 유가 = 전국평균 줄 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 | C4-유가 |
|
||||
| 9 | 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치면 잇기 + 지역 옵션 · 없을 때만 사람 확인 | C4-재료 |
|
||||
| 10 | 가벼움 묶음 — 조합 첫 줄 칩 글 · 소계 검산 표지 · 찾기 없는 기계 줄 7 · 올린 직후 409 · 엑셀 블록 「원파일 확정」 글 · 전수 도우미 str() | D1 |
|
||||
| 뒤 | 화면 속도(목록 70 초) · 후보 고르기(흠 6 · 7 · 8) | B4 보류 · C3 협의 중 |
|
||||
|
||||
## F. 예상 정합성(요구 749 · 어림 — 812-12 수치에 결정 사항을 얹어 셈)
|
||||
|
||||
| 잣대 | 지금 | 뒤(어림) | 셈 근거 |
|
||||
|---|---|---|---|
|
||||
| 금액 · 줄 읽기 · 차림 왕복 | 100 % | 100 % | 그대로 |
|
||||
| 좌측 차림 조합 엇갈림 | 83 블록 흠 | 0 | #4 |
|
||||
| 인력 · 형제 단가산출 이음 | 100 % | 100 % | 그대로 |
|
||||
| 기계 이음 | 90.8 %(5,699 / 6,279) | 96 ~ 99 % | #5 로 349 줄 + · 별칭 승인 65 · 후보 여럿 88 은 사용자 고름 · 표에 없음 71 남음 |
|
||||
| 재료 이음 | 72.0 %(1,466 / 2,036) | 86 ~ 98 % | #9 — EST 확인분 283 줄 확실(86 %) · 후보 여럿 556 줄 대부분 같은 꼴이면 98 % |
|
||||
| 유가 이음 | 0 %(0 / 29) | 100 % | #8 |
|
||||
| [우리 단가로] 블록 전부 우리 값(= 우리 정보로 만들어짐) | 원파일 71.9 % · 엑셀 74.6 % | 88 ~ 93 % | 0 원 줄 252 + 177 (#4 · #5) · 재료 · 유가(#8 · #9) 풀림 · 남는 것 = 우리 자재 값 0(88 줄) · 표에 없는 기계 71 · 별칭 대기 · 견적 운임 |
|
||||
| 연결 찾기 확정(보조) | 15.3 %(339) | 25 ~ 30 % | #4 로 「없음」 중 기계 세 줄 탓 24 %(어림) 풀림 · #6 공구손료 · #3 꼬리 18 · 「없음」 818 중 품셈 밖 38 % 는 그대로 |
|
||||
@@ -0,0 +1,185 @@
|
||||
# 812-12 단가산출 불러오기 정합성 — 전수 보고(요구 738-5)
|
||||
|
||||
- 대상 = 원파일 EST 6 · STC 7(산근 2,220 블록) + 실무 엑셀 11(1,021 블록) · 812-1 · 812-2 · 812-4 · 812-9 · 812-11 · 813-1 · 813-2 반영 판
|
||||
- 수치 = 전수로 잰 것 · 「어림」 = 무작위 쉰 · 서른을 사람 눈으로 가른 비율
|
||||
- 자세한 표 = `_목록_812-12_정합성.xlsx`(시트 금액 · 줄 읽기 · 요소 이음 · 우리 단가 · 차림 · 연결 판정 · 없음 쉰 · 근접 서른 · 흠)
|
||||
- 코드 · 마스터 · storage 고침 없음 · 화면에서 만든 SK 열셋 · 올린 파일 모두 지움
|
||||
|
||||
## 요약
|
||||
|
||||
| 항목 | 수 | 비율 | 판정 |
|
||||
|---|---|---|---|
|
||||
| 1 금액(엑셀 단가 그대로 셈 = 원본) | 원파일 2,220 / 2,220 · 엑셀 1,021 / 1,021 | 100 % · 100 % | 맞음 |
|
||||
| 2 줄 읽기 — 못 읽음 표지 | 0 / 78,308 줄 | 0 % | 맞음 |
|
||||
| 2 엑셀 ↔ 원파일 실제 짝(거창 · STC 다섯) | 블록 264 · 줄 10,744 다름 0 | 100 % | 맞음 |
|
||||
| 2 기계 비목 세 줄(노 · 재 · 경 따로) | STC 285 / 285 기계 블록 · 엑셀 277 · EST 155 | STC 100 % | **흠** |
|
||||
| 3 인력 이음 | 5,089 / 5,089 줄 | 100 % | 맞음 |
|
||||
| 3 기계 이음(이어짐 · 별칭 · 조합 가름) | 5,699 / 6,279 줄 | 90.8 % | 보통 |
|
||||
| 3 기계 — 찾았으나 시간당 경비 이음 없음 | 349 줄 · 26 종 | 5.6 % | **흠** |
|
||||
| 3 재료 이음 | 1,466 / 2,036 줄 | 72.0 % | 보통 |
|
||||
| 3 유가 이음 | 0 / 29 줄(모두 후보 여럿) | 0 % | 흠 |
|
||||
| 3 형제 단가산출 이음 | 402 / 402 줄 | 100 % | 맞음 |
|
||||
| 4 [우리 단가로] 모든 줄 우리 값 | 원파일 1,596 / 2,220 · 엑셀 762 / 1,021 | 71.9 % · 74.6 % | 보통 |
|
||||
| 4 [우리 단가로] 0 원 줄 있는 블록 | 원파일 236 · 엑셀 47 | 10.6 % · 4.6 % | **흠** |
|
||||
| 5 차림 왕복(문서 → 줄 → 문서) · 거울 | 3,241 / 3,241 | 100 % | 맞음 |
|
||||
| 5 조합 기계 항목 엇갈림(비목 세 줄 + 조합) | 83 블록 | 2.6 % | 흠 |
|
||||
| 6 연결 판정(원파일) 확정 · 근접 · 없음 · 기계 작업 | 339 · 865 · 818 · 198 | 15.3 · 39.0 · 36.8 · 8.9 % | — |
|
||||
| 6 연결 판정(엑셀) 확정 · 근접 · 없음 · 기계 작업 | 205 · 260 · 472 · 84 | 20.1 · 25.5 · 46.2 · 8.2 % | — |
|
||||
| 6 「없음」 — 우리 기초 있는데 못 찾음 | 쉰 중 31 | 62 %(어림) | **흠** |
|
||||
| 6 「근접」 — 연결 찾기가 더 붙일 수 있음 | 서른 중 15 | 50 %(어림) | 흠 |
|
||||
| 7 화면 블록 열 개 금액 = 원본 · 콘솔 오류 | 10 / 10 · 0 | 100 % | 맞음 |
|
||||
| 흠 | 심각 4 · 보통 7 · 가벼움 5 | 16 | — |
|
||||
|
||||
## 1 금액
|
||||
|
||||
| 원천 | 블록 | 셈 = 원본 | 다름 | 오류 |
|
||||
|---|---|---|---|---|
|
||||
| EST 원파일 6 | 1,849 | 1,849 | 0 | 0 |
|
||||
| STC 원파일 7 | 371 | 371 | 0 | 0 |
|
||||
| 실무 엑셀 11 | 1,021 | 1,021 | 0 | 0 |
|
||||
|
||||
- 검산 줄 틀림 3 줄 — 표지 있음 2(main 1308 · 원파일 자체 엇갈림) · 표지 없음 1(북부청임도 33 소계 · 블록 금액은 같음)
|
||||
|
||||
## 2 줄 읽기
|
||||
|
||||
| 줄 갈래 | EST | STC | 엑셀 |
|
||||
|---|---|---|---|
|
||||
| 제목 · 합계 | 19,107 | — | 5,846 |
|
||||
| 자원 | 7,609 | 1,918 | 3,485 |
|
||||
| 자원(형제 단가산출) | 356 | 9 | 37 |
|
||||
| 변수(값 · 공식) | 6,655 | 2,601 | 2,961 |
|
||||
| 율 | 1,125 | 114 | 511 |
|
||||
| 소계 | 1,795 | 1,072 | 1,296 |
|
||||
| 글 | 15,973 | 2,003 | 3,835 |
|
||||
| 표지 — 못 읽음 · 못 옮김 | 0 | 0 | 0 |
|
||||
| 표지 — 추정(품셈 조건 글) | 2,182 | 0 | 324 |
|
||||
| 표지 — 참고(조합 기계 · 당초 칸 · 후보 여럿 등) | 471 | 42 | 823 |
|
||||
|
||||
| 엑셀 ↔ 원파일 실제 짝 | 블록 | 줄 | 다름 |
|
||||
|---|---|---|---|
|
||||
| 거창 xlsx ↔ .estx | 61 | 1,467 | 0 |
|
||||
| STC 다섯(봉화 · 동부청영월 · 울진대흥 · 영덕계류 · 울진소광) | 203 | 9,277 | 0 |
|
||||
|
||||
- 울진대흥 엑셀 블록 33 = 원파일에 없는 블록(다른 판 파일) — 짝 대상 밖
|
||||
|
||||
| 기계 비목 세 줄 | EST | STC | 엑셀 |
|
||||
|---|---|---|---|
|
||||
| 블록 | 155 | 285 | 277 |
|
||||
| 줄 — 이름 = 「노 무 비 · 재 료 비 · 경 비」 | 0 | 1,391 | 1,109 |
|
||||
| 줄 — 「덤프트럭 노무비」 꼴 | 476 | 1 | 160 |
|
||||
|
||||
## 3 요소 이음(줄 수 · 이름 수)
|
||||
|
||||
| 갈래 | 쓰임 | 이어짐 | 별칭 | 조합 가름 | 찾음 · 시간당 이음 없음 | 후보 여럿 | 못 찾음(별칭 필요) | 우리 표에 없음 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 인력 | 5,089 | 5,089(47 이름) | — | — | — | 0 | 0 | 0 |
|
||||
| 기계 | 6,279 | 4,932(192) | 62(8) | 705(44) | 349(26 종) | 88 | 65(13) | 71(5) |
|
||||
| 재료 | 2,036 | 1,466(348) | — | — | — | 556(156) | 14(7) | — |
|
||||
| 유가 | 29 | 0 | — | — | — | 29(3) | — | — |
|
||||
| 환율 | 0 | — | — | — | — | — | — | — |
|
||||
| 형제 단가산출 | 402 | 402(97) | — | — | — | — | — | — |
|
||||
|
||||
- 기계 판정 없음 7 줄 · 고정 줄(갈래 밖) 59
|
||||
- 못 이은 것 많이 나오는 스무 이름 = 목록 「요소 이음」 시트(기계 = 대형브레이카 73 · 부착용집게 68 · 굴착기(타이어형) 56 · 진동파일해머 32 …)
|
||||
|
||||
## 4 [우리 단가로]
|
||||
|
||||
| 블록 판정 | 원파일 | 엑셀 |
|
||||
|---|---|---|
|
||||
| 모든 줄 우리 값 | 1,596 | 762 |
|
||||
| 이음 없는 줄 = 엑셀 단가 남음 | 388 | 212 |
|
||||
| 0 원 줄 있음 | 236 | 47 |
|
||||
| 셈 못 함 | 0 | 0 |
|
||||
|
||||
| 0 원 줄 까닭 | 줄 |
|
||||
|---|---|
|
||||
| 조합 다음 줄 · 시간당 경비 이음 없음(대형 브레이커 · 덤프트럭 자동덮개시설 등) | 252 |
|
||||
| 조합 다음 줄 · 기계 못 이음(부착용 집게 등) | 177 |
|
||||
| 재료 이음 · M01 값 0(공기주입기 · 시스템동바리 등) | 88 |
|
||||
| 기계 낱 줄 | 8 |
|
||||
|
||||
## 5 좌측 차림
|
||||
|
||||
| 항 | EST | STC | 엑셀 |
|
||||
|---|---|---|---|
|
||||
| 블록 | 1,849 | 371 | 1,021 |
|
||||
| 항목 · 제목 있는 항목 | 8,290 · 7,218 | 1,188 · 825 | 2,861 · 2,306 |
|
||||
| 가나다 소항목 | 1,725 | 28 | 298 |
|
||||
| 공식 묶음 · 글 줄 | 2,917 · 6,221 | 722 · 1,252 | 1,136 · 1,992 |
|
||||
| 조합 기계 · 그 항목 | 634 · 552 | 150 · 100 | 176 · 140 |
|
||||
| 제목 없는 블록 | 0 | 86 | 69 |
|
||||
| 왕복 같음 · TS = 파이썬 거울 | 1,849 · 1,849 | 371 · 371 | 1,021 · 1,021 |
|
||||
|
||||
- 조합 기계 수 > 항목 수 = 비목 세 줄 기계가 조합일 때 같은 제목 항목이 겹치고 줄이 엇갈림(83 블록 · 흠 2)
|
||||
|
||||
## 6 연결 찾기
|
||||
|
||||
| 원천 | 확정 | 근접 | 없음 | 기계 작업 | 합 |
|
||||
|---|---|---|---|---|---|
|
||||
| EST 원파일 | 316 | 771 | 636 | 126 | 1,849 |
|
||||
| STC 원파일 | 23 | 94 | 182 | 72 | 371 |
|
||||
| 실무 엑셀 | 205 | 260 | 472 | 84 | 1,021 |
|
||||
|
||||
- 파일별 수 = 목록 「연결 판정」 시트 · 원파일 「못 셈」 134 = 전수 도우미 흠 → 고친 사본으로 다시 잼(확정 2 · 근접 17 · 없음 115) · 엑셀 「못 셈」 17 = 같은 흠(다시 안 잼)
|
||||
- 확정 참고(블록) — 수량 114 · 단가 105 · 율 값 41 · 수량 끝자리 39 · 규격 37 · 옵션 확인 34 · 품 숫자 18 · 끝수 1 · 확정 다시 셈 다름 0
|
||||
- 근접 까닭(블록) — 줄 없음 536 · 기계 규격 296 · 율 줄 239 · 비목 223 · 이름 다름 186 · 율 대상 77 · 공구손료 유무 46 · 자원 25 · 단위 환산 4
|
||||
- 엑셀 ↔ 원파일 같은 블록 판정 — 264 중 246 같음(93.2 %)
|
||||
- 원파일 「없음」 818 구조 — 후보 0 = 294 · 가까운 후보 있으나 줄 짝 반 못 넘음 = 524
|
||||
|
||||
| 「없음」 무작위 쉰(어림) | 수 | 비율 |
|
||||
|---|---|---|
|
||||
| 우리 기초에 그 공종 없음(구역화물 운임 · 하차 요금 · 지자체 기준 · 자체 품 · 구조물 묶음 · 부품만) | 19 | 38 % |
|
||||
| 우리 기초 있는데 못 찾음 | 31 | 62 % |
|
||||
| └ 기계 비목 세 줄 탓 | 12 | 24 % |
|
||||
| └ 줄 구성 · 율 · 이름 다름(손율 재료 · 유리알 더함 · 이름 덧말) | 15 | 30 % |
|
||||
| └ 형제 묶음 · 못 셈 | 4 | 8 % |
|
||||
|
||||
| 「근접」 무작위 서른(어림) | 수 | 비율 |
|
||||
|---|---|---|
|
||||
| 같은 공종 · 차이는 표현(율 비목 · 비목 세 줄 · 조합 한 줄) — 더 붙일 수 있음 | 7 | 23 % |
|
||||
| 맞는 우리 기초가 따로 있음 — 후보 고르기 틀림 | 8 | 27 % |
|
||||
| 같은 공종 · 로직 실제로 다름 — 근접 맞음 | 4 | 13 % |
|
||||
| 우리 기초에 공종 없음 — 엉뚱한 근접 | 11 | 37 % |
|
||||
|
||||
## 7 화면(8005 · 5178 · 일반 계정 개인 층)
|
||||
|
||||
| 파일 | 블록 | 금액 = 원본 | 확인 |
|
||||
|---|---|---|---|
|
||||
| 거창 .xlsx | 1 측구터파기 | 5,323 | [연결 찾기] 확정 GF000099 → [바꾸기] 5,286 → [되돌리기] 5,323 |
|
||||
| 거창 .xlsx | 2 파쇄암 | 30,381 | 조합 가 · 나 · 다 항목 · 근접 GF000268 = 전수 · [우리 단가로] 23,747 = 전수(브레이커 0 원) |
|
||||
| 거창 .xlsx | 15 · 41 · 60 | 94,215 · 52,796 · 1,675 | 15 = 없음(후보 14 · 「못 셈」 아님) · 60 = 「기계 한 줄 단가산출」 띠 |
|
||||
| 거창 .estx | 25 장비운반 | 271,869 | 엑셀 HP 에 붙음 |
|
||||
| 봉화 .STC | 14 · 55 · 93 | 26,144 · 16,629 · 36,282 | 55 기계 = 「재 료 비 · 노 무 비 · 경 비」 세 줄 · 93 = 없음(후보 17) |
|
||||
| 북부청임도 .xls | 16 · 31 | 2,264 · 134,304 | 31 조합 나 · 다 항목(어두움) |
|
||||
|
||||
- 콘솔 오류 0(ResizeObserver 알림만) · 밝음 · 어두움 · 가로 넘침 0(1274 폭)
|
||||
|
||||
## 흠
|
||||
|
||||
| # | 심각 | 흠 | 예 | 고칠 길 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 심각 | STmate 기계 = 비목 세 줄(이름 「노 무 비」 · 단위 X) | 봉화 55 크로울러드릴 세 줄 · STC 285 블록 1,391 줄 · 엑셀 277 블록 | X 코드 L · M · S 세 줄 → 기계 한 묶음 줄 |
|
||||
| 2 | 심각 | EST 「덤프트럭 노무비 · 재료비 · 경비」 세 줄 · 조합이면 항목 엇갈림 | main 360 같은 제목 항목 셋 · 155 블록 · 엇갈림 83 블록 | 세 줄 묶은 뒤 조합 가름 |
|
||||
| 3 | 심각 | 단가산출 목록 열기 65~75 초 | SK 16 개 층 = 파일 읽기 4,137 번(`M02_Labor_Ref.users`) | 쓰는 곳 색인 한 번 |
|
||||
| 4 | 심각 | 찾은 기계 26 종 시간당 경비 이음 없음 → 우리 단가 0 원 · 엑셀 값 남음 | 거창 2 브레이커 0 원 · 349 줄 · 0 원 블록 283 | M01 시간당 짝 채움 · 못 이으면 엑셀 값 + 표지 |
|
||||
| 5 | 보통 | 원파일만 올리면 블록 목록 판 없음(STC 호표 0) | sub3임시_봉화.STC | 격자 없어도 목록 판 |
|
||||
| 6 | 보통 | 맞는 우리 기초 두고 다른 후보 근접 | 플륨관 설치 → 강관압입추진 · 서른 중 8 | 이름 · 절 근거 후보 먼저 |
|
||||
| 7 | 보통 | 엉뚱한 근접(인부 한 줄 짝) | 떼 하차비 → 텍코팅 · 서른 중 11 | 근접에 이름 근거 또는 핵심 줄 둘 |
|
||||
| 8 | 보통 | 이름 거의 같은 기초 두고 후보 0 | 비탈규준틀 ↔ GF000151 · 후보 0 = 294 | 덧말 뺀 이름 · 절 번호로 모음 |
|
||||
| 9 | 보통 | 엑셀 ↔ 원파일 같은 블록 판정 다름 | 동부청영월 2 · 264 중 18 | 이름 맞대기 전 규격 · 단위 꼬리 뗌 |
|
||||
| 10 | 보통 | 재료 · 유가 후보 여럿 → 이음 없음 | 재료 556 줄 · 유가 29 줄 | 규격 · 단위로 좁힘 · 유가 전국 기본 |
|
||||
| 11 | 보통 | 전수 도우미 형제 값 글자 → 「못 셈」(시험 도구만 · 화면 정상) | 원파일 134 · 엑셀 17 블록 | `helper_810_4_sweep` str() 뗌 |
|
||||
| 12 | 가벼움 | 조합 첫 줄 기계 칩 = 원래 조합 글 | 「굴삭기(무한궤도) 0.7+브레이카」 | 칩도 그 기계 이름 |
|
||||
| 13 | 가벼움 | 소계 줄 검산 틀림 · 표지 없음 | 북부청임도 33 · 478.8 ↔ 3,897.1 | 얼린 줄도 소계 식에 |
|
||||
| 14 | 가벼움 | 기계 줄 찾기 판정 없음 | 진동 로울러(핸드가이드식) · 7 줄 | 찾기 붙임 |
|
||||
| 15 | 가벼움 | 올린 직후 몇 초 [내 단가산출로] 409 | 거창 xlsx 블록 넷 | 올림 답 전 보관 끝냄 |
|
||||
| 16 | 가벼움 | 엑셀 블록에도 「원파일 확정」 글 | 거창 xlsx 2 | 갈래 맞는 글 |
|
||||
|
||||
## 사용자가 정할 것
|
||||
|
||||
| # | 정할 것 | 갈래 |
|
||||
|---|---|---|
|
||||
| 1 | 공구손료 율을 실무는 재료비 · 우리는 경비 — 이 비목 차이를 로직(근접)으로 볼지 · 값(참고 · 확정)으로 볼지 | 근접 서른 중 4 블록이 이것 하나로 근접(1 블록은 자재 이름도) |
|
||||
| 2 | 원파일(.estx · .STC)만 올린 경우에도 단가산출 블록 목록을 열지 | 지금 STC 단독은 화면 길 없음 |
|
||||
| 3 | 시간당 경비 자료 없는 기계 26 종 — [우리 단가로] 때 엑셀 값 그대로 · 0 원 + 표지 · M01 자료 채움 중 | 대형 브레이커 · 부착용 집게 · 굴착기(타이어형) 등 |
|
||||
| 4 | 구역화물 운임 · 하차 요금 · 지자체 기준 · 자체 품 블록(없음 쉰 중 38 % 어림)을 「품셈 밖」 표지로 연결 대상에서 뺄지 | 연결 「없음」 해석 |
|
||||
@@ -0,0 +1,79 @@
|
||||
# 814-1 불러오기 결정 반영 — 전후 전수 보고(요구 742 ~ 750 · 방향 장부 E 표 1 ~ 10)
|
||||
|
||||
- 대상 = 812-12 와 같은 자 — 원파일 EST 6 · STC 7(산근 2,220 블록) + 실무 엑셀 11(1,021 블록)
|
||||
- 전 = 812-12 보고 수 + 같은 자로 다시 잰 수(코드 바꾸기 전) · 후 = 814-1 반영 판(커밋 56a3e05 · 8914b21 · bed35e3)
|
||||
- 자세한 표 = `_목록_814-1_정합성.xlsx`(요약 전 · 후 · 연결 · 못 이은 줄 · 0 원 줄)
|
||||
- 화면에서 만든 SK 둘 · 올린 파일 셋 모두 지움 · 마스터 별칭 · 별칭 장부 · storage 층 사본 안 건드림
|
||||
|
||||
## 요약
|
||||
|
||||
| 잣대 | 전 | 후 | 판정 |
|
||||
|---|---|---|---|
|
||||
| 금액(엑셀 단가 그대로 셈 = 원본) | 원파일 2,220 · 엑셀 1,021 = 100 % | 원파일 2,220 · 엑셀 1,021 = 100 % | 그대로 |
|
||||
| 못 읽음 표지 | 0 | 0 | 그대로 |
|
||||
| 인력 · 형제 단가산출 이음 | 100 % · 100 % | 100 % · 100 % | 그대로 |
|
||||
| 기계 이음 | 90.8 %(5,699 / 6,279 줄) | **95.7 %**(4,601 / 4,807 줄 — 세 줄 묶은 뒤 줄 수) | 좋아짐 |
|
||||
| 기계 — 찾았는데 시간당 경비 없음 | 349 줄(26 종) | **0** | 풀림 |
|
||||
| 재료 이음 | 72.0 %(1,466 / 2,036) | **87.6 %**(1,784 / 2,036) | 좋아짐 |
|
||||
| 유가 · 전력 이음 | 0 %(0 / 29) | **100 %**(29 / 29) | 풀림 |
|
||||
| [우리 단가로] 블록 전부 우리 값(= 우리 정보로 만들어짐) | 원파일 71.9 %(1,596) · 엑셀 74.6 %(762) | 원파일 **85.5 %**(1,899) · 엑셀 **79.7 %**(814) | 좋아짐 |
|
||||
| [우리 단가로] 0 원 줄 있는 블록 | 원파일 236 · 엑셀 47 | 원파일 115 · 엑셀 4 | 줄어듦 |
|
||||
| STmate 기계 비목 세 줄(문서 줄 쌓기) | STC 285 블록 · 1,391 줄 | 남음 STC 2 블록 · 엑셀 6 블록(번지가 셋으로 갈린 꼴) | 풀림 |
|
||||
| 조합 기계 항목 겹침(같은 기계 항목이 여럿) | 83 블록 | **0** | 풀림 |
|
||||
| 차림 왕복 · TS 거울 | 100 % | 100 %(812-3 시험 · 7 통과) | 그대로 |
|
||||
| 1,513 M01 문서 금액 | — | 기본 1,513 같음 · 옵션 표본 18,797 중 다름 1(채택된 차이 그대로) | 그대로 |
|
||||
| 연결 판정(보조) 원파일 확정 · 근접 · 없음 · 기계 작업 | 339 · 865 · 818 · 198(15.3 %) | **463** · 784 · 761 · 212(20.9 %) · 못 셈 0 · 확정 다시 셈 다름 0 | 좋아짐 |
|
||||
| 연결 판정(보조) 엑셀 확정 · 근접 · 없음 · 기계 작업 | 205 · 260 · 472 · 84(20.1 %) | 211 · 271 · 447 · 92(20.7 %) · 못 셈 0 | 조금 |
|
||||
|
||||
## 항목별(E 표)
|
||||
|
||||
| # | 고친 것 | 화면 · 수 |
|
||||
|---|---|---|
|
||||
| 1 | 올리기 = 파일 하나 · 같은 공사 붙이기 걷음 | 엑셀이든 .estx · .STC 든 따로 새 작업 · 「같은 공사 엑셀에 붙음」 글 없음 |
|
||||
| 2 | 확장자 파일 = 단가산출표 판(코드 · 이름 · 규격 · 단위 · 재료비 · 노무비 · 경비 · 합계) + 산출근거 | 봉화 .STC 하나 → 65 블록 목록 · [내 단가산출로] = 산출근거 · 일위대가 안 만듦(EST 도 HeadMainDat 안 씀) |
|
||||
| 3 | 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름(「구조물터파기(인력) 경질토사 / M3」 → 「구조물터파기(인력)」) | 보이는 이름 그대로 · STmate 블록 규격도 근거 칸에 |
|
||||
| 4 | 기계 세 줄 = 기계 한 줄 · 조합 다음 줄 기계마다 한 항목 | 봉화 55 크롤러드릴 「노 무 비 · 재 료 비 · 경 비」 → 「크롤러드릴(탑승유압식) 110㎾」 한 줄 · main 360 덤프 자동덮개 시설 항목 셋 → 하나 · 금액 그대로 |
|
||||
| 5 | 시간당 경비 계산식 기계 목록에 빠진 기계 더함 | 384 대 가운데 351 대 더함(기계경비 92 · 조종원 직종 입력 18 · 조종원 없음 43 · 연료 없음 7 · 손료만 198) · 못 넣음 33 대(기계표 가격 · 계수 · 연료 종류 빔 · 잡재료율 「13∼18」 범위) · 조합(굴착기 + 대형 브레이커 · 부착용 집게) = 우리 조합 식 GC000271 · 270 한 줄 — 거창 2 [우리 단가로] 23,747 → 25,772(브레이커 0 원 풀림) |
|
||||
| 6 | 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고 | 거창 41 합판거푸집 = 근접 → 확정(참고 「공구손료 비목」) |
|
||||
| 7 | 못 이은 줄 까닭 · 「없음」 띠 | 이음 없는 자원 띠 = 「대형 브레이커 (조합 — 우리 조합 식 GC000271 에 듦)」 꼴 · 문서 줄 설명 = 「이음 없음 — 자재 없음 · 엑셀 단가 그대로」 꼴 · 연결 「없음」 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」 |
|
||||
| 8 | 유가 = 전국평균 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 | 경유 · 등유 → `MO:경유:{S_지역}`(기본 전국평균) · 전력 → 114.4 원(엑셀 114.8 = 검산 칸) |
|
||||
| 9 | 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치 | 유리알 1호 고휘도 · 합판(내수) 12t(서울) · 외송각재 · 철못 N 25 · 구역화물 8.0톤 30km 이음 |
|
||||
| 10 | 가벼움 여섯 | 조합 첫 줄 칩 = 「굴삭기(무한궤도) 0.7」 · 북부청임도 33 소계 검산 맞음 · 기계 찾기 판정 없는 줄 7 → 0 · 올린 직후 409 = HP 보이기 전 산근 보관 · 엑셀 블록 「엑셀 읽기 확정」 · 전수 도우미 str() 뗌 |
|
||||
|
||||
## 남은 못 이음 — 까닭별(후)
|
||||
|
||||
| 갈래 | 까닭 | 줄 |
|
||||
|---|---|---|
|
||||
| 기계 | 후보 여럿(사람 고름 · C3 협의 중) | 88 |
|
||||
| 기계 | 못 찾음 — 별칭 대기 | 65 |
|
||||
| 기계 | 우리 기계표에 없음 | 53 |
|
||||
| 재료 | 후보 여럿(정확 일치 없음 — 접착제 · 유로폼 패널 · 각재 외송 …) | 238 |
|
||||
| 재료 | 우리 자재표에 없음 | 14 |
|
||||
| 갈래 밖 | 견적 · 운임 · 수수료(구역화물자동차 · 도선료 · 치즐 견적 …) | 59 |
|
||||
|
||||
| [우리 단가로] 0 원 줄(후) | 줄 | 뜻 |
|
||||
|---|---|---|
|
||||
| 조합 다음 줄 이음 · 덤프트럭 자동덮개시설(노무비 · 재료비 칸) | 82 | 부착 장비 = 손료(경비)만 — 노무 · 재료 칸 0 이 맞음 |
|
||||
| 조합 다음 줄 못 이음(굴착기 후보 여럿 · 별칭) | 46 | 사람 고름 대기 |
|
||||
| 재료 이음 · 우리 값 0(급수호스 · 시험 수수료 · 공기주입기 · 시스템동바리 …) | 102 | 우리 자재표 값 없음 |
|
||||
| 기계 낱 줄(발전기 노무비 칸 등) | 11 | 그 기계에 그 비목 없음 |
|
||||
|
||||
- 연결 「확정」 참고(원파일) — 수량 269 · 공구손료 비목 154(6 번) · 단가 차이 97 · 옵션 확인 94 · 수량 끝자리 72 · 품 숫자 65
|
||||
- 「없음」 = 흠 아님(요구 745) — 띠 글 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 근접 까닭 1위 = 줄 없음 1,361 · 기계 규격 475(후보 고르기 C3 협의 몫)
|
||||
|
||||
## 남은 흠
|
||||
|
||||
| # | 흠 | 예 | 갈래 |
|
||||
|---|---|---|---|
|
||||
| 1 | EST 「〇〇 노무비 · 재료비 · 경비」 가운데 수량 식이 다른 세 줄은 못 묶음 | main 360 덤프 재료비 × To | 원파일 로직 그대로(값 묶음으로 못 접음) |
|
||||
| 2 | STmate 세 줄이 번지 셋(A01 · A02 · A03)으로 갈린 꼴은 안 묶음 | 울진대흥 6 크로울러드릴 | 묶으면 번지 율 식이 깨짐 · 8 블록 |
|
||||
| 3 | 재료 지역 옵션 화면 칸 없음 — 서울 줄 기본으로 이음 | 합판(내수) 서울 | 자재표 = 키마다 지역(우리 기초도 서울 고정 검색어) |
|
||||
| 4 | 원파일 단가산출 목록 줄 아래 글 「일위대가 밖」 | 봉화 .STC 목록 | 일위대가를 안 만드니 늘 그 글 |
|
||||
| 5 | 연결 판정 띠(「우리 기초 없음 — 우리 정보로 셈」)는 [연결 찾기] 다시 누른 SK 부터 | 옛 SK | 기록된 판정에 칸 없음 |
|
||||
|
||||
## 화면(8005 · 5178 · 일반 계정 개인 층 · 「sub3임시」)
|
||||
|
||||
- 봉화 .STC 하나만 올림 → 단가산출표 판 65 블록 · 55 암발파 = 크롤러드릴 한 줄 · 검산 같음 · [우리 단가로] 16,629 → 16,020
|
||||
- 거창 .xlsx 하나만 올림 → 2 측구터파기(연암) = 가 · 굴착기 / 나 · 대형 브레이커 / 다 · 치즐 항목 · 브레이커 띠 「조합 — 우리 조합 식 GC000271 에 듦」 · [우리 단가로] 30,381 → 25,772 · 「같은 공사」 글 없음
|
||||
- 밝음 · 어두움 · 1366×768 가로 넘침 0 · 콘솔 오류 0(로그인 전 세션 401 하나 뺌) · 사진 = tmp(셋)
|
||||
- 만든 SK 둘 · 올린 HP 셋 지움
|
||||
@@ -561,3 +561,246 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의
|
||||
## 요구 712 (11-09 · 사용자 · 매칭은 로직으로(돈만 보지 말 것) · 권장 순서 뒤 정합성 추정 · 엑셀 엔진 새로 짜는 게 나은지)
|
||||
|
||||
결국 전부 단가산출을 제대로 연결할 수 있어야한다인데. 결과적으로 단가산출은 로직과 값으로 되어있음. 경쟁사도 값은 주기적으로 업데이트 됨. 그렇다면 매칭해야하는 자료는 로직임. 단순히 총합계나 계산된 돈만 보면 판단이 어려워지니 조심할 것. 다음으로 권장 순서대로 했을때 정합성이 어느정도 될것같아? 그리고 현 엑셀엔진을 처음부터 코드를 만드는게 더 효율적이고 체계적일 것 같다는 생각이드는데 어떻게 생각함?
|
||||
|
||||
## 요구 713 (11-09 · 사용자 · 제안대로 진행(엑셀 읽기기 새로 + 연결 기준 = 로직) · 가져온 단가산출이 좌측에서 읽히게(나열만 X))
|
||||
|
||||
다음으로 중요한 점은 경쟁사의 단가산출을 가져왔다 가정하고 현재의 경쟁사 단가산출은 좌측영역에서 제대로 읽기가 힘든 그냥 노무비, 경비, 재료 1,2, 3 형태로 죽 나열만 되어있음. 분석도 중요하지만 보여지는것도 중요함. 너의 제안대로 잘 끝까지 진행해줘
|
||||
|
||||
## 요구 714 (11-09 · 사용자 · 판단 셋 답 — 1 보류 · 2-1 율 값 = 값(로직 아님) · 2-2 기계 이름 = 별칭 조사 · 3 1 원은 로직만 맞으면 됨)
|
||||
|
||||
1번은 생각할 시간이 필요.
|
||||
2-1번 율값 관련 내가 로직과 값을 구분하라고 했잖아. 로직은 어떤 계산을 할때 뭐를 곱하는지 뭐를 더할지 같은 것들이고 여기에 사용된 값은 결국 언제든 업데이트 될수 있다고 불러오기 기능들에서에서 초기에 검산을 위해서는 겂을 사용하겠지만 결국은 우리의 m01의 값을 사용할 수 있어야해. 그게 현재 m02의 단가산출에 있는 값 업데이트 기능이고 이해해?
|
||||
2-2번 기계 이름이 다를경우 별칭을 사용하는게 어떨까해. 우리는 품셈원문의 이름을 사용하고 있기도 하고 m01 단가산출에도 별칭으로 경쟁사와 정보가 다를경우 승인하고 별칭으로 등록하는게 있잖아. 조사해봐
|
||||
3. 1원 차이는 로직만 맞다면 문제 없음.
|
||||
|
||||
## 요구 715 (11-09 · 사용자 · 수량 추가 설명 요청 · 1번 계속 대기 · 기계 230 블록 뜻 물음)
|
||||
|
||||
수량 관련은 추가 설명 요청
|
||||
|
||||
1번 계속 대기
|
||||
2번 기계 230 블럭은 경쟁사 프로그램에서 만들어둔 기계의리스트라는 말임?
|
||||
|
||||
## 요구 716 (11-09 · 사용자 · 하나씩 — 기계 230 블록: 굴삭기 0.7 = 바스켓 크기 · 기계 옵션값 변경 또는 미리 만들어 두기 · 확인부터)
|
||||
|
||||
일단 하나씩 하자. 기계230블럭에서 굴삭기 0.7에서 0.7은 바스켓의 사이즈로 보이는데 기계의 옵션값 변경을 하면 되는거 아닌가? 아니면 미리 만들어 두던가 확인부터 해봐.
|
||||
|
||||
## 요구 717 (11-09 · 사용자 · 우리 단가산출에서 기계 · 옵션 변경이 안 되는 게 문제 — 기계가 들어오면 상단 옵션에 기계 선택 · 하위 옵션 · 그 뒤 연결 찾기 방향 · 확인부터)
|
||||
|
||||
1. 그것보다 중요한 건 우리 단가산출에서 기계를 변경하거나 옵션을 변경하는게 안되는게 문제아니야? 그렇다면 기계가 단가산출에 들어오면 상단 옵션에 기계선택이나 하위 옵션선택이 되어야하는거 아닌가?
|
||||
2. 그리고 나서 연결찾기의 방향이 결정될것같은데?
|
||||
3. 확인부터 해봐.
|
||||
|
||||
## 요구 718 (11-09 · 사용자 · 기계 줄 누르면 목록이 나옴 → 옵션 없어도 됨 · 문제는 연결을 못 찾는 것 · 조회어 「굴착기(무한궤도) 0.7㎥」 가 문제 아닌지 · 기계 못 연결하는 이유 다시 확인)
|
||||
|
||||
지금 확인했는데 좌측영역에서 기계 줄에서 지정된 기계를 눌렀더니 옵션 별로 항목 리스트가 나오는데 이러면 옵션이 없어도 되는거잖아. 그럼 문제는 옵션 여부가 아니라 연결을 제대로 찾지 못하는게 아닌가? 그리고 추가로 좌측영역 기계를 눌렀을때 굴착기(무한궤도) 0.7㎥ 이렇게 조회를 하는데 이게 문제 아니야? 기계를 못 연결하는 이유를 다시 확인해봐.
|
||||
|
||||
## 요구 719 (11-09 · 사용자 · 기계 예들 직접 찾아 보니 대형 브레이커 + 굴착기 빼고 다 찾을 수 있음 · 옛 이름은 별칭 사용 가능 · 조합 기계 = 설비 둘을 각각 가져오기)
|
||||
|
||||
너의 알려준 낱말을 모르는 예와 후보가 여럿인 예를 내가 직접찾아 봤는데 대형 브레이커 + 굴착기 만 제외하면 다 려었이 아니라 다찾을 수 있겠는데? 그리고 경쟁사 프로그램이 예전프로그램이라 찾기 힘들면 별칭을 사용해도됨. 다음으로 2개의 기계의 조합을 대현 브레이커, 굴착기로 표현한게 아닐까? 우리는 설비 2개를 각각 가져오면 맞을 것같다는 생각이 드네.
|
||||
|
||||
## 요구 720 (11-09 · 사용자 · 수정을 어떻게 요청했는지 물음)
|
||||
|
||||
뭐지 어떻게 수정을 요청한거야?
|
||||
|
||||
## 요구 721 (11-09 · 사용자 · 작업 이어서 · 기계 · 옵션 때문에 품셈 원문 제목과 다르게 표현된 단가산출 기초 조사 — 200 개쯤 예상 · 1,300 → 1,500 늘어난 까닭)
|
||||
|
||||
같은 기종인데 규격만 다른 경우은 지금의 수정으로 가능한거 아니야?
|
||||
|
||||
1. 작업은 이어서 진행, 문제는 없어보임.
|
||||
2. 우리의 단가산출은 기계나 특정 옵션에 의해 품셈 원문 제목과는 다르게 표현된 항목들이 거기에 해당 될것 같은데. 그것때문에 제대로 연결이 안되는게 아닐까? 어떤게 있는지 먼저 조사가 필요해봐. 그렇다 하더라도 품셈원문 제목에 품셈원문 제목이 있다면 모를까 하지만 제목에 기계가 들어가는 경우는 드물것같아. . 내가 예상하기로는 200개 정도 될것같은데 확인해봐. 이전에는 1300개정도에서 갑자기 1500개로 단가산출 기초가 늘었거든.
|
||||
|
||||
## 요구 722 (11-09 · 사용자 · 기초 이름 정리 방향 다섯 — 수정 말고 대답만 · 방향성부터)
|
||||
|
||||
1. 제목 + 옵션, 조건 덧말은 조심해야 원문제목과 대조할 필요가 있어보임
|
||||
2. 제목과 다르지만 분기가 필요해 셋으로 나눈건 괜찮음.
|
||||
3. 보조문서는 기계와 연계된 정보이니 단가산출이 아닌걸로 봐야하는게 아닌지. 자동으로 연결될 것같은데?
|
||||
4. 기계 이름이 품셈 제목에 없는데붙는 경우는 수정해야 될것같음.
|
||||
5. 위에처럼 정리하고 나면 이름으로 구분안되는 정보들이 나올텐데 이때는 삭제하는게 맞겠지?
|
||||
수정하지말고 대답해줘. 방향성 부터 봐야지.
|
||||
|
||||
## 요구 723 (11-09 · 사용자 · 방향 다섯 동의(3 은 둘 자리 협의) · 원문 패널 md 경로 삭제 · [주] 글자 크기 반대로 · 반영 · 다시 조사 · 작업 이어서 — sub3 과 충돌 없으면 다른 워크트리에)
|
||||
|
||||
1. 동의
|
||||
2. 동의
|
||||
3. 동의는 하나, 단가산출 기초 리스트에서 빼서 어디에 둘건지는 협의 필요
|
||||
4. 동의
|
||||
5. 동의
|
||||
추가로
|
||||
6. 지금 단가산출의 우측 공통 원문 패널보기 내용 중에 md파일의 경로를 알려주고 있음. 이부분은 다른 워크트리에게 경로를 볼필요없으니 삭제해달라고 해줘.
|
||||
7. 원문 패널의 내용 중 [주] 내용의 글자가 더크고 본문의 내용이 작음. 반대가 되는게 맞음.
|
||||
위 내용을 반영하고 다시 조사하고 작업을 이어서 하는데 너가 판단해보고 현재 작업중인 서브3과 충돌이 없다면 다른 워크트리에게 작업을 시키자.
|
||||
|
||||
## 요구 724 (11-09 · 사용자 · 보조 문서를 단가산출 목록에서 빼면 어디에 둘지 의견 물음)
|
||||
|
||||
단가산출 목록에서 빼면 어디다 두는게 좋을것같아?
|
||||
|
||||
## 요구 725 (11-09 · 사용자 · 보조 문서 자리 = 권장으로 해도 될 듯 · 조사해 맞는지 먼저 보고 판단 · 「정할 것」 설명 필요)
|
||||
|
||||
1. 권장으로 해도 될것같음. 하지만 조사해보고 맞는 먼저 보고 판단하자.
|
||||
2. 정할것에 대한 설명이 필요.
|
||||
|
||||
## 요구 726 (11-09 · 사용자 · 전용 작업량도 기계 항목으로 · 작업량당 기계경비 예시 요청 · 기계 무관 = 제안대로 · 토사절취(백호 0.7㎥) 대화창 출력 · 1~4 한 번에 진행 예정 — 일단 대기)
|
||||
|
||||
1. 전용 일지라도 결과적으로는 다른곳에서 사용할수도 있으니 기계라면 당연히 기계항목으로 들어가는게 맞아보임. 현재는 하나지만 나중에는 늘어날수도 있잖아.
|
||||
2. 작업량당 기계경비는 어떤걸말하는것임? 예를 들어 보여줘.
|
||||
3. 기계무관은 너의 제안대로
|
||||
4. 토사절취(백호 0.7㎥) 단가산출을 대화창에 출력해줘.
|
||||
5. 1~4번은 한번에 진행할 생각이야. 그러니 일단대기하고 4번을 대화창에 출력
|
||||
|
||||
## 요구 727 (11-09 · 사용자 · 토사절취 예시의 문제가 무엇인지 물음 — 시간당이 아니라 단위당으로 나오는 것이 문제냐)
|
||||
|
||||
이게 뭐가 문제야? 원래는 기계의 돈은 시간당으로 나와야하는데 단위면적당으로 나온다는 거야?
|
||||
|
||||
## 요구 728 (11-09 · 사용자 · 신규 단가산출에 기계 하나만 지정하면 작업량당 기계경비와 같은 것)
|
||||
|
||||
아니 그러니까 내가 만약에 신규 단가산출을 만들고 거기에 기계만 하나 지정하면 같은게 되는거잖아.
|
||||
|
||||
## 요구 729 (11-09 · 사용자 · 결국 표현 단위 차이 — 단위 환산 로직을 단가산출 기초에 두는 게 맞지 않나)
|
||||
|
||||
결국은 표현의 단위차이라는 말 아니야? 그렇다면 표현하는 단위 환산로직을 단가산출 기초에 넣어두는게 더 맍지 않아?
|
||||
|
||||
## 요구 730 (11-09 · 사용자 · 표로 다시 · 짧게)
|
||||
|
||||
테이블로 다시 표현해봐 . 이해하기 힘들어. 짧게 설명해줘.
|
||||
|
||||
## 요구 731 (11-09 · 사용자 · 나눔 맞는 것 같음 · 단가산출 기초 화면의 공식 틀 추가는 필요 없어진 것인지)
|
||||
|
||||
맞는것같네. 그럼 이제 단가산출 기초화면에서 공식 틀 추가는 필요없어진 것임?
|
||||
|
||||
## 요구 732 (11-09 · 사용자 · 「식」으로 변경 · 작업 진행 · 진행 뒤 엑셀 불러오기에서 더 결정할 것 알려 달라)
|
||||
|
||||
식으로 변경하자. 이제 작업 진행하자.
|
||||
진행 후 엑셀불러오기와 관련하여 뭐를 더 결정해야하는지 알랴줘.
|
||||
|
||||
## 요구 733 (11-09 · 사용자 · 결정 목록 여섯에 대한 답 · 물음 + 드래그 그룹 이동 확인 — 수정 아님 · 조사부터)
|
||||
|
||||
1. 이유 짧게 설명
|
||||
2. 규격이 다른 같으 기종의 경우 가져온 걸로 우리의 기계값을 바꾸면 되는거 아닌지.
|
||||
3. 반올림 자리만 다른 경우 계산에는 정밀도 높게 계산하는지 확인하고 계산에 문제 없다면 확정. 품숫자가 다른 4개라는건 뭔지 짧게 설명
|
||||
4. 작업량 당 기계경비의 경우에는 단가산출이 어떤식으로 제목이 나오는지 확인. 기계만 있는 단가산출도 필요는 해보이는데 문제는 우리의 단가산출은 자유롭게 기계를 변경할 수 있음. 그렇다면 만약 단가산출 기초 제목으로 M02에서 찾을텐데 이때 기계명이 있는 이름의 단가산출이 있다면 사용자는 해당 단가산출 정보만 있을것으로 오해할것이기 때문에 6개의 단가산출이 기계명으로만 구분되어 있는 단가산출이라면 그냥 통합도 가능할 것으로 예상되어 물어봄(수정하라는 말은 아님)
|
||||
5. 별칭 승인의 승인요청 시점이 사용자들이 저장하여 단가산출을 저장한 시점으로 보이는데 맞음? 그리고 별칭은 있어야 되는건 맞는데 혹시 경쟁사 프로그램의 출력물로 찾을 경우 이제 빈도가 줄었을 것으로 보임. 어느정도 수준인 확인
|
||||
6. 굴삭기 + 브레이카 는 사용자가 그룹 구분 없이 사용한다면 한 덩이로 그룹으로 구분한다면 자동으로 구분되겠지. 일반적으로는 구분되게 사용하는게 맞는것같음.
|
||||
7. 드래그로 그룹이동 되는지 확인
|
||||
바로 수정작업 아님. 조사부터 진행.
|
||||
|
||||
## 요구 734 (11-09 · 사용자 · 결정 답 — 소계는 우리 양식 · 수량 끝자리 뜻 물음 · 품 숫자 = 우리 값이 최신(가져오면 우리 값) · 작업량당 기계경비 단가산출 불필요 의견 물음 · 별칭 8 목록 요청(초기 별칭 직접 · 승인까지) · 6 · 7 문제 없음 · 1~5 한 번에 진행 예정 — 먼저 대답)
|
||||
|
||||
1. 소계는 결국 항목들의 합계형식일 뿐 우리양식 맞춰 표기해도 문제 없을것같다는 생각이 듬.
|
||||
2. 수량 끝자리라는게 뭐임?
|
||||
3. 품 숫자가 다르다는건 결국 품셈이나 요소들에 지정된 값이 다르다라는 의미지? 그렇다면 우리가 더 최신이 맞음. 그래서 가져오기 전 비교할때는 해당정보로 계산하겠지만 가져오면 우리값으로 업데이트 되는게 맞다고 대답한 적이 있음. 그리고 만약 그래도 사용자가 이전값으로 사용하겠다면 우리 단가산출 양식에는 값변경을 할수 있으니 OK
|
||||
4. 작업량당 기계경비는 결국 이걸 활용하여 다른 단가산출들에서 사용하고 있는거잖아. 기계로 넣어서 사용하고 있는거잖아. 그렇다면 작업량당 기계경비라는 단가산출을 만들어둘 필요가 없어보이네 너의 생각은? 어차피 경쟁사 데이터 불러오기 기능에도 우리 단가산출 표현에 문제도 없을거고 있으면 되려 혼란을 줄것같아서 말이야.
|
||||
5. 별칭 필요한 8개의 리스트를 보여주면 초기 별칭을 내가 넣어줄게. 승인처리까지 했으면 함.
|
||||
6. 문제 없음.
|
||||
7. 문제 없음.
|
||||
1 ~ 5번은 한번에 진행할거야. 가능한 경우에는 다른 워크트리를 사용해도됨. 그러니 먼저 대답부터
|
||||
|
||||
## 요구 735 (11-09 · 사용자 · 1~3 동의 · 작업량당 기계경비 = 필요하면 기계 하나 넣어 만들어 씀 · 별칭 아홉 답)
|
||||
|
||||
1. 동의
|
||||
2. 동의
|
||||
3. 동의
|
||||
4. 작업량당 기계경비는 본인이 필요한 경우 기계하나만 넣고 만들어 사용하면 됨.
|
||||
5. 별칭 확인
|
||||
1) 후보맞음.
|
||||
2) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
3) 후보맞음.
|
||||
4) 엔진(디젤) 이렇게 되어 있음. 가솔린은 엔진(가솔린)으로 되어 있음.
|
||||
5) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
6) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
7) 워터젯트
|
||||
8) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
9) 후보맞음
|
||||
|
||||
## 요구 736 (11-09 · 사용자 · 바이브레이터 규격이 하나면 엔진식)
|
||||
|
||||
바이브 레이터에 규격은 하나임? 하나라면 엔진식이 맞음.
|
||||
|
||||
## 요구 737 (11-09 · 사용자 · 남은 결정 사항 물음)
|
||||
|
||||
남은 결정사항은 뭐가 있어?
|
||||
|
||||
## 요구 738 (11-09 · 사용자 · 별칭 넷 = 단가산출 만들다 나오면 승인(할 일 없음) · 원문 패널 표 글자 16 · 제목 통합(그림 `요구738_원문패널_제목중복.png`) · 조합 기계 = 개별 그룹으로 · 공식 연결 분리 가능한지 조사 · 반영 뒤 단가산출 불러오기 정합성 확인)
|
||||
|
||||
지금 정할 수 있는 것.
|
||||
1. 지금 하겠다는게 아니라 단가산출을 만들면서 해당내용이 나오면 내가 승인하겠다는거야. 그러니 이제 너가 할 일은 없음.
|
||||
2 .표의 글자도 16으로 변경
|
||||
2-1. [Image #1]중복표기가 많음 제목을 통합해줘. 건설품셈 공통 7-1-1 메쌓기('12, '19, '26년 보완) 형태면 될것같음.
|
||||
위 내용도 같이 반영, 반영완료
|
||||
3. 조합기계는 우리 단가산출로 가져올때 개별 그룹으로 하나씩 가져오는게 좋을것같음.
|
||||
4. 공식을 연결해야 단가가 나올것같은데 분리할 수 없을것같은데 조사
|
||||
5. 위내용까지 반영 후 일단 단가산출에 대한 불러오기 정합성 확인
|
||||
|
||||
## 요구 739 (11-09 · 사용자 · 4 조사 결과 — 일단 현재 유지가 좋겠음 · 지금은 원문 기준으로 만든 품셈이 맞는지)
|
||||
|
||||
4 조가결과에서 일단 현재를 유지하는게 좋을것같은데 일단 현재는 원문을 기준으로 만들어진 품셈이잖아. 맞지?
|
||||
|
||||
## 요구 740 (11-09 · 사용자 · 현재 유지 범위 확인)
|
||||
|
||||
현재 유지는 지금의 범위가 맞음
|
||||
|
||||
## 요구 741 (11-10 · 사용자 · 정합성 · 연결 · 흠 · 정할 것을 아티팩트로)
|
||||
|
||||
불러오기 정합성, 우리기초와 연결, 찾음 흠, 정할것을 별도의 아티팩트로 저장.
|
||||
|
||||
## 요구 742 (11-10 · 사용자 · 엑셀 = 경쟁사 프로그램 결과물인데 왜 다른가 · 파일은 하나만 넣게 · STmate 단가산출표 = 산출근거 상단 제목 — 조사)
|
||||
|
||||
1.일단 하나만 물어보자. tool 폴더에는 경쟁사 복제프로그램을 만들어뒀어. 그렇다면 엑셀 파일은 이 경쟁사 프로그램에 의해 결정될텐데 서로 다르다라는게 이해가 되지않아서 말이야. 엑셀이라면 먼저 경쟁사 분석엔진을 돌고 그다음은 경쟁사 프로그램 확장자로 넣은 결과와 같다는 거지. 그럼 복수의 파일을 받는게 아니라 사용자는 엑셀이든 경쟁사 확장자 파일이든 하나만 넣게할거야.
|
||||
2. stmate 확장자인경우 단가산출표가 없다고 했는데 단가산출표는 산출근거의 상단제목만 뽑은게 단가산출표야.
|
||||
조사해봐
|
||||
|
||||
## 요구 743 (11-10 · 사용자 · 이제부터 불러오기는 방향 결정만 · 코드 수정은 한 번에 · 아티팩트 기준으로 하나씩 협의 · 1 동의 · 2 확장자 파일 = 단가산출표 + 산출근거만(일위대가 제외) · 3 이름 꼬리 = 연결용 분리면 문제없음)
|
||||
|
||||
이제부터 불러오기에 대한 방향성 결정만 하고 코드 수정은 한번에 진행할 생각임. 이전 대화에서 공유해준 아티팩트를 기준으로 하나하나 방향성을 협의하고 조사하고 방향성을 수립해두면 됨.
|
||||
1. 동의함.
|
||||
2. 경쟁사 확장자일 경우 단가산출표와 산출근거만 표현. 일위대가는 제외
|
||||
3. 엑셀이름 꼬리는 우리단가 연결을 위한 분리작업이라면 문제없음.
|
||||
|
||||
## 요구 744 (11-10 · 사용자 · B1 · B2 동의 · B3 동의 안 됨(우리 프로그램엔 있음 · 이름으로 따라가고 우리 단가산출화 때 조합으로) · C1 비목을 바꾸면 됨 · C2 품셈 밖이 아니라 품셈 기본으로 조합하고 이름만 넣은 자료 아닌지)
|
||||
|
||||
b1 b2 동의
|
||||
c3는 동의 안됨. 우리프로그램은 있음. 경쟁사 프로그램에 정보가 없다는말? 그렇다면 그냥 이름만 보고 따라간다면? 우리 단가산출화 했을때는 따로 조합으로 보려주면 될듯한데?
|
||||
c1 비못을 바꾸면 되지 않음?
|
||||
c2 품셈밖이 아니라 결국 품셈을 기본으로하여 조합하고 이름만 넣은 자료가 아닌지.
|
||||
|
||||
## 요구 745 (11-10 · 사용자 · B3 = 경쟁사 목록에 없다는 뜻인지(맞으면 제안 OK) · C2 = 목적은 우리 기초 만들기가 아니라 서로의 인력/자재/기계/다른 정보로 단가산출을 만드는 것 — 특정 기초와 맞추기보다 우리 정보로 만들어 낼 수 있는지)
|
||||
|
||||
b3 에서 경쟁사 목록에 해당내용이 없다는건지? 그게 맞다면 제안이 맞음
|
||||
c2 지금 우리가 하는 작업은 우리단가산출 기초를 만들기 위함이 아님. 우리의 단가산출 기초처럼 서로의 인력/자재/기계/다른 정보로 단가산출을 만들기 위함임. 그러니 우리의 특정단가산출 기초와 동일하게 맞추기보다는 우리의 정보로 만들어 낼수 있는지를 봐야함.
|
||||
|
||||
## 요구 746 (11-10 · 사용자 · B3 = 품셈 원문에 없어서 안 만든 것인지 · B2 맞는 방향)
|
||||
|
||||
b3 그럼 품셈 원문에 해당 내용이 없어서 아뉴만들었다는게 되는건데 맞아?
|
||||
b2 맞는 방향인것으로 판단됨
|
||||
|
||||
## 요구 747 (11-10 · 사용자 · B3 = 원문에 있는데 누락된 것만 추가하면 되는지 · 다음 = 유가가 왜 연결 안 되는지 조사)
|
||||
|
||||
b3 품셈 원문에 있는데 누락된 것만 추가하면 된다는거지?
|
||||
다음 유가 부분이 왜 연결안되는지 조사
|
||||
|
||||
## 요구 748 (11-10 · 사용자 · 유가 = 전국으로 · 전력 = 토목공사에 맞는 기본값으로 결정 · 이상하면 사용자가 나중에 바꿈)
|
||||
|
||||
유가는 경쟁사는 분리안되어있다는 말인것같고 일단 전국으로 해줘. 나중에 필요하면 변경하겠지. 전력도 어떤 항목이 토목공사에 더 작합한지 고민해보고 그냥 기본값으로 결정하면 됨. 이후 둘다 이상하면 사용자가 나중에 변걍할거야
|
||||
|
||||
## 요구 749 (11-10 · 사용자 · 재료 안 맞음 · 화면 속도는 나중에 한 번에 · 결정 사항 수정 정리 + 예상 정합성 조사)
|
||||
|
||||
일단은 맞는것같아.
|
||||
화면속도는 나중에 한번에 진행항 생각.
|
||||
이제까지 결정사항에 대한 수정사항을 정리해보고 어느정도 정합성이 나올건지 조사해줘
|
||||
꽤 많이 개선된걸로 보임
|
||||
|
||||
## 요구 750 (11-10 · 사용자 · 진행)
|
||||
|
||||
진행
|
||||
|
||||
## 요구 751 (11-10 · 사용자 · 다른 워크트리와 나눠서 작업하면 나을지 물음)
|
||||
|
||||
다른 워크트리와 나눠서 작업하면 좀 나을려나?
|
||||
|
||||
## 요구 752 (11-10 · 사용자 · 혼자(한 창)로 하는 게 좋겠음 · 기다림)
|
||||
|
||||
그냥 혼자하는게 좋겠네. 기다릴께
|
||||
|
||||
## 요구 753 (11-10 · 사용자 · 다음 협의 1 · 2 번 설명 요청 · 재확인 = 우리 기초와 맞추기가 아니라 우리 기능 · 요소로 단가산출을 만드는 것)
|
||||
|
||||
1번 2번 설명.
|
||||
잘 이해해야하는 부분은 우리의 일위대가 기초와 동일하게 맞추는게 아니라 우리가 가지고 있는 기능과 요소로 단가산출을 만드는 거야. 이해하지? 이전 대화에서 했던 말이야. 재확인차 물어봄.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -118,3 +118,14 @@
|
||||
- 끝수 결과 · 중간 `표시` → 식 안 `반올림` · 끝수 계 · 끝전 · 단계 · 조정 · 번지 · 합밖 · 자리 → 문서 칸 그대로.
|
||||
- 기호 = 수식 탭 기호(800-1 · 800-5) · 한 문서 안 겹침 = 아래첨자 끝 숫자.
|
||||
- 저장 = 대조(기본 옵션 + 옵션 표본)가 모두 같고 부르는 단가산출도 저장되는 문서만 · 다른 것 = `docs/raw/요구_1104/_목록_802-2_변환대조.xlsx`.
|
||||
|
||||
## 9. 부품 표지(813-2) — 단가산출이 아닌 보조 문서
|
||||
|
||||
- 머리 칸 `부품` = `{갈래, 기종?, 규격?}` · 옛 로직 줄 `부품` 을 변환이 그대로 옮김 · 셈 · 부르기 · 키로 열기는 그대로.
|
||||
- 단가산출 기초 목록 · 찾아 넣기 「다른 단가산출」 목록에 안 보임(`M01_MasterData_Parts.strip`).
|
||||
|
||||
| 갈래 | 뜻 | 칸 | 자리 |
|
||||
| --- | --- | --- | --- |
|
||||
| 기계경비식 | 기계 시간당 경비 셈 방식(기계 줄이 부름) | — | M01 「기계」 기종 아래 |
|
||||
| 작업량 틀 | 기종 공식(Q = 3,600 × q × K × f × E ÷ Cm 등) | `기종` = 기계표 상세구분 목록 · `규격` = 기계 규격 수를 받는 변수 열쇠 | M01 「기계」 기종 아래 |
|
||||
| 계수식 | 기계 무관 · 돈 아닌 식 | — | M01 「계수」 아래 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"다음": {"LB": 409, "EQ": 655, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 337, "CC": 528, "GC": 1195, "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"}},
|
||||
"폐기": {"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)"},
|
||||
"키": {
|
||||
"CC000001": {"원문번호": "공통 1-3-1 콘크리트 및 포장용 재료", "파일": "계수_건설품셈_01장_적용기준.json"},
|
||||
"CC000002": {"원문번호": "공통 1-3-1 노상 및 노반재료", "파일": "계수_건설품셈_01장_적용기준.json"},
|
||||
@@ -2490,28 +2490,22 @@
|
||||
"GC001142": {"원문번호": "공통 1-2-7 벽돌운반", "파일": "로직_건설품셈_01장_적용기준.json"},
|
||||
"GC001143": {"원문번호": "공통 8-1-4 기본식", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001144": {"원문번호": "공통 8-2-1 불도저", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001145": {"원문번호": "공통 8-2-1 불도저경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001146": {"원문번호": "공통 8-2-2 리퍼", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001147": {"원문번호": "공통 8-2-3 굴착기", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001148": {"원문번호": "공통 8-2-3 굴착기경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001149": {"원문번호": "공통 8-2-4 트랜처", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001150": {"원문번호": "공통 8-2-5 로더", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001151": {"원문번호": "공통 8-2-5 로더경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001152": {"원문번호": "공통 8-2-6 스크레이퍼", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001153": {"원문번호": "공통 8-2-7 그레이더면적", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001154": {"원문번호": "공통 8-2-7 그레이더토량", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001155": {"원문번호": "공통 8-2-8 덤프트럭", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001156": {"원문번호": "공통 8-2-8 덤프트럭경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001157": {"원문번호": "공통 8-2-9 롤러토량", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001158": {"원문번호": "공통 8-2-9 롤러면적", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001159": {"원문번호": "공통 8-2-9 롤러경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001160": {"원문번호": "공통 8-2-10 아스팔트플랜트", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001161": {"원문번호": "공통 8-2-11 스테이빌라이저", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001162": {"원문번호": "공통 8-2-12 죠", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001163": {"원문번호": "공통 8-2-12 롤", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001164": {"원문번호": "공통 8-2-12 이동식", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001165": {"원문번호": "공통 8-2-12 스크린", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001166": {"원문번호": "공통 8-2-13 브레이커경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001167": {"원문번호": "공통 8-2-14 압쇄기", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001168": {"원문번호": "공통 8-2-15 법면다짐기", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001169": {"원문번호": "공통 8-2-16 골재세척", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
@@ -2791,16 +2785,16 @@
|
||||
"GF000249": {"원문번호": "9-14-2 다짐", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000250": {"원문번호": "9-15-2 표토제거 답외구간", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000251": {"원문번호": "9-17-2 비다짐", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000252": {"원문번호": "9-4-1 암파쇄(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000253": {"원문번호": "9-5-2 발파암 깎기 90%(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000252": {"원문번호": "9-4-1 암파쇄", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000253": {"원문번호": "9-5-2 발파암 깎기(90%)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000254": {"원문번호": "9-5-1 육상 암석 절취(발파 10%)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000255": {"원문번호": "9-6-1 발파암 기계소할(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000256": {"원문번호": "9-7-1 무근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000257": {"원문번호": "9-7-2 무근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000258": {"원문번호": "9-8-1 철근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000259": {"원문번호": "9-8-2 철근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000260": {"원문번호": "9-10-1 기존포장 깨기 콘크리트 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000261": {"원문번호": "9-10-2 기존포장 깨기 아스팔트 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000255": {"원문번호": "9-6-1 발파암 기계소할(15%)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000256": {"원문번호": "9-7-1 무근콘크리트 깨기(T=30㎝ 미만)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000257": {"원문번호": "9-7-2 무근콘크리트 깨기(T=30㎝ 이상)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000258": {"원문번호": "9-8-1 철근콘크리트 깨기(T=30㎝ 미만)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000259": {"원문번호": "9-8-2 철근콘크리트 깨기(T=30㎝ 이상)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000260": {"원문번호": "9-10-1 기존포장 깨기 콘크리트", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000261": {"원문번호": "9-10-2 기존포장 깨기 아스팔트", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000262": {"원문번호": "9-11-1 포장절단 콘크리트(기계)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000263": {"원문번호": "9-12-2 측구터파기 암절취", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000264": {"원문번호": "9-12-3 측구터파기 발파암", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
@@ -2822,13 +2816,13 @@
|
||||
"GF000280": {"원문번호": "10-6-2 인력운반 돌(지게)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000281": {"원문번호": "10-6-3 인력운반 기타 임업자재", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000282": {"원문번호": "10-8-1 케이블 크레인 짐내리기", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000283": {"원문번호": "10-9-1 덤프트럭 자재 운반(1회 표준 운반량)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000284": {"원문번호": "10-12-1 덤프운반(굴착기 0.7㎥ 적재 · 덤프트럭 15ton)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000283": {"원문번호": "10-9-1 덤프트럭 운반(자재의 1회당 표준 운반량)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000284": {"원문번호": "10-12-1 덤프운반 토사", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000285": {"원문번호": "10-13-2 묘목 인력운반", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000286": {"원문번호": "4-1-1 수확베기 임업용 동력기계톱 벌목", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000287": {"원문번호": "4-2-1 단목베기(100본당)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000288": {"원문번호": "4-2-2 단목베기(1,000㎡당 · 굴착기 0.2㎥ + 부착용 집게)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000289": {"원문번호": "4-3 위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000288": {"원문번호": "4-2-2 단목베기(1,000㎡당)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000289": {"원문번호": "4-3 위험목 베기", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000290": {"원문번호": "5-1-1 관목굴취", "파일": "로직_산림품셈_05장_식재_및_녹화.json"},
|
||||
"GF000291": {"원문번호": "5-3-2 관목식재(단식)", "파일": "로직_산림품셈_05장_식재_및_녹화.json"},
|
||||
"GF000292": {"원문번호": "5-3-3 관목식재(군식)", "파일": "로직_산림품셈_05장_식재_및_녹화.json"},
|
||||
@@ -2860,7 +2854,7 @@
|
||||
"GF000318": {"원문번호": "8-1-2 나무주사 약제주입병", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000319": {"원문번호": "8-4 끈끈이 롤 트랩 설치·제거", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000320": {"원문번호": "8-6-2 드론방제(무인헬기 · 멀티콥터)", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000321": {"원문번호": "8-6-3 지상방제 차량살포(동력분무기)", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000321": {"원문번호": "8-6-3 지상방제 차량살포", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000322": {"원문번호": "8-10 그물망 피복", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000323": {"원문번호": "8-11 이동식 임목 파쇄(93.25KW)", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000324": {"원문번호": "13-4-6 밑돌 및 천단 돌쌓기", "파일": "로직_산림품셈_13장_구조물.json"},
|
||||
|
||||
@@ -26,6 +26,7 @@ def marine(C) -> list[dict]:
|
||||
)
|
||||
row.pop("결과")
|
||||
row.pop("시험입력")
|
||||
row["이름"] = "수중펌프 설치 및 해체" # 813-2 원문 제목 · 윗절
|
||||
row["호표"] = [
|
||||
{
|
||||
"종류": "인력",
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"""시간당 경비 계산식(GC000267 ~ GC000276) 「기계」 고르기 · 선택 목록에 원문에 있는데 빠진 기계 더함(PLAN 814-1 · 요구 744 ~ 747).
|
||||
|
||||
기계표(EQ) 가운데 어느 계산식 목록에도 없는 기계를 자료(기계표 값)에 따라 식 하나에 배정 — 지어 넣는 값 없음:
|
||||
연료(주연료 · 종류) · 조종원(인 · 직종) · 잡재료율 · 상각 · 정비 · 관리 계수 있음 = GC000268(기계경비)
|
||||
연료 · 조종원(인) 있음 · 직종 없음 = GC000275(조종원직종 입력)
|
||||
연료 있음 · 조종원 없음 = GC000272(조종원 없는 기종)
|
||||
연료 없음 · 조종원(인 · 직종) 있음 = GC000276(연료 없는 기종)
|
||||
연료 · 조종원(직종) 없음 · 가격 · 손료계수 있음 = GC000273(손료만 — 부착 장비 등)
|
||||
그 식이 부르는 기계표 값이 비거나 수 하나가 아니면(「13∼18」) 안 넣고 「못 넣음」 목록에 적음.
|
||||
8-1-7 손료 보정 식(GC000269 · GC000274)은 보정 기종 식에 그 기계 번호가 이미 있을 때만 같이 더함.
|
||||
목록 끝에 덧붙임(기본값 그대로 = 기존 문서 금액 같음). 로직 파일(`로직_…_건설기계.json` 입력 고르기)과
|
||||
수식 문서(`수식문서/수식_…_건설기계.json` 「기계」 선택 목록)를 같이 고침. 두 번 돌려도 같음.
|
||||
|
||||
쓰기: venv python resources/master_data/scripts/build_기계목록_빠진기계_814_1.py [--check]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
DATA = HERE.parent
|
||||
LOGIC = DATA / "로직_건설품셈_08장_건설기계.json"
|
||||
DOC = DATA / "수식문서" / "수식_건설품셈_08장_건설기계.json"
|
||||
FAMILY = [f"GC00027{i}" for i in range(6)] + ["GC000267", "GC000268", "GC000269"]
|
||||
FAMILY = sorted(set(FAMILY) | {"GC000276"})
|
||||
FIX = ("상각비계수", "정비비계수", "관리비계수")
|
||||
NEEDS = {
|
||||
"GC000268": ("가격", *FIX, "주연료종류", "주연료", "조종원직종", "조종원", "잡재료율"),
|
||||
"GC000275": ("가격", *FIX, "주연료종류", "주연료", "조종원", "잡재료율"),
|
||||
"GC000272": ("가격", *FIX, "주연료종류", "주연료", "잡재료율"),
|
||||
"GC000276": ("가격", *FIX, "조종원직종", "조종원"),
|
||||
"GC000273": ("가격", "손료계수"),
|
||||
}
|
||||
PAIR = {"GC000268": "GC000269", "GC000273": "GC000274"} # 8-1-7 보정 식
|
||||
|
||||
|
||||
TEXT = ("주연료종류", "조종원직종") # 글 칸 — 그 밖 = 수 하나(범위 「13∼18」 · 빈 값 = 없음)
|
||||
|
||||
|
||||
def _has(row: dict, field: str) -> bool:
|
||||
v = (row.get("값") or {}).get(field)
|
||||
if v in (None, ""):
|
||||
return False
|
||||
if field in TEXT:
|
||||
return bool(str(v).strip())
|
||||
try:
|
||||
return float(str(v).replace(",", "")) != 0
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def family_of(row: dict) -> tuple[str | None, list[str]]:
|
||||
"""기계표 한 줄 → (식 키 | None, 빈 값 이름들) — 연료 · 조종원 유무로 식 가족을 고름."""
|
||||
fuel, crew, job = _has(row, "주연료"), _has(row, "조종원"), _has(row, "조종원직종")
|
||||
if fuel and crew:
|
||||
key = "GC000268" if job else "GC000275"
|
||||
elif fuel:
|
||||
key = "GC000272"
|
||||
elif crew and job:
|
||||
key = "GC000276"
|
||||
else:
|
||||
key = "GC000273"
|
||||
miss = [f for f in NEEDS[key] if not _has(row, f)]
|
||||
return (None if miss else key), miss
|
||||
|
||||
|
||||
def _rows() -> list[dict]:
|
||||
data = json.loads((DATA / "기계.json").read_text("utf-8"))
|
||||
return [
|
||||
r for r in data.get("줄") or data.get("표") or [] if str(r.get("키", "")).startswith("EQ")
|
||||
]
|
||||
|
||||
|
||||
def _logic_lists(text: str) -> dict[str, list[str]]:
|
||||
data = json.loads(text)
|
||||
out = {}
|
||||
for row in data["줄"]:
|
||||
for i in row.get("입력") or []:
|
||||
if row.get("키") in FAMILY and i.get("이름") == "기계":
|
||||
out[row["키"]] = [str(c) for c in i.get("고르기") or []]
|
||||
return out
|
||||
|
||||
|
||||
def _span(text: str, key: str, start_pat: str) -> tuple[int, int]:
|
||||
"""그 키 블록 안 첫 `start_pat` 뒤 배열 [ … ] 의 (여는 괄호, 닫는 괄호) 자리."""
|
||||
at = text.index(f'"키": "{key}"')
|
||||
m = re.compile(start_pat).search(text, at)
|
||||
if not m:
|
||||
raise SystemExit(f"{key} 기계 목록 자리를 못 찾음")
|
||||
open_ = m.end() - 1
|
||||
close = text.index("]", open_)
|
||||
return open_, close
|
||||
|
||||
|
||||
def _append(text: str, key: str, codes: list[str], start_pat: str) -> str:
|
||||
if not codes:
|
||||
return text
|
||||
open_, close = _span(text, key, start_pat)
|
||||
body = text[open_ + 1 : close]
|
||||
if "\n" in body: # 여러 줄 꼴 — 마지막 줄 들여쓰기 그대로
|
||||
last = body.rstrip().rsplit("\n", 1)[-1]
|
||||
pad = last[: len(last) - len(last.lstrip())]
|
||||
add = "".join(f',\n{pad}"{c}"' for c in codes)
|
||||
cut = open_ + 1 + len(body.rstrip())
|
||||
return text[:cut] + add + text[cut:]
|
||||
add = "".join(f', "{c}"' for c in codes)
|
||||
return text[:close] + add + text[close:]
|
||||
|
||||
|
||||
LOGIC_PAT = r'"이름":\s*"기계",\s*"고르기":\s*\['
|
||||
DOC_PAT = r'"열쇠":\s*"기계"[^\n]*?"목록":\s*\['
|
||||
|
||||
|
||||
def plan() -> tuple[dict[str, list[str]], list[tuple[str, str, str, list[str]]]]:
|
||||
logic = LOGIC.read_text("utf-8")
|
||||
have = _logic_lists(logic)
|
||||
taken = {c for v in have.values() for c in v}
|
||||
fixes = {}
|
||||
for r in json.loads(logic)["줄"]:
|
||||
if r.get("키") in PAIR.values():
|
||||
fixes[r["키"]] = " ".join(str(x.get("식") or "") for x in r.get("중간") or [])
|
||||
add: dict[str, list[str]] = {}
|
||||
skipped = []
|
||||
for row in _rows():
|
||||
code = str(row.get("원문번호") or "")
|
||||
if not code or code in taken:
|
||||
continue
|
||||
key, miss = family_of(row)
|
||||
if key is None:
|
||||
skipped.append((code, str(row.get("이름")), str(row.get("규격") or ""), miss))
|
||||
continue
|
||||
if code in add.get(key, []):
|
||||
continue
|
||||
add.setdefault(key, []).append(code)
|
||||
if PAIR.get(key) and f"'{code}'" in fixes.get(PAIR[key], ""):
|
||||
add.setdefault(PAIR[key], []).append(code)
|
||||
return add, skipped
|
||||
|
||||
|
||||
def main(check: bool = False) -> None:
|
||||
add, skipped = plan()
|
||||
print("더함:", {k: len(v) for k, v in sorted(add.items())}, "· 못 넣음:", len(skipped))
|
||||
for code, name, spec, miss in skipped:
|
||||
print(f" 못 넣음 {code} {name} {spec} — 빈 값 {', '.join(miss)}")
|
||||
if check or not add:
|
||||
return
|
||||
logic, doc = LOGIC.read_text("utf-8"), DOC.read_text("utf-8")
|
||||
for key, codes in sorted(add.items()):
|
||||
logic = _append(logic, key, codes, LOGIC_PAT)
|
||||
doc = _append(doc, key, codes, DOC_PAT)
|
||||
json.loads(logic), json.loads(doc) # 꼴 확인
|
||||
LOGIC.write_text(logic, "utf-8", newline="")
|
||||
DOC.write_text(doc, "utf-8", newline="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main("--check" in sys.argv)
|
||||
@@ -30,8 +30,8 @@ REGIONS = [
|
||||
] # fmt: skip
|
||||
ROCK = ["암석작업(연암・보통암・경암)", "해당 없음", "전석섞인토사"]
|
||||
NAMES = (
|
||||
"시멘트운반(구역화물 운임 + 하차비)",
|
||||
"철근운반(구역화물 운임 + 하차비)",
|
||||
"시멘트운반",
|
||||
"철근운반",
|
||||
"깬잡석 · 야면석 · 고임돌 · 파쇄석 채집(기계)",
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ def haul(no: str, what: str, unit: str, per: str, old: str) -> dict:
|
||||
div = "1.1 * 적재량 * 포수" if sack else "1.1 * 적재량"
|
||||
return {
|
||||
"원문번호": no,
|
||||
"이름": f"{what}(구역화물 운임 + 하차비)",
|
||||
"이름": what,
|
||||
"결과단위": unit,
|
||||
"출처": f"산림품셈 {no}",
|
||||
"소유": "공용",
|
||||
|
||||
@@ -375,7 +375,7 @@ def logics(key: dict) -> list[dict]:
|
||||
"키": None,
|
||||
"열쇠": "공통 1-2-7 인력운반(지게)",
|
||||
**HEAD,
|
||||
"이름": "인력운반(지게) — 건설 공통 1-2-7",
|
||||
"이름": "지게운반",
|
||||
"결과단위": "원/자재 단위",
|
||||
"출처": f"{SRC} 2 · 3",
|
||||
"소유": "공용",
|
||||
@@ -461,7 +461,7 @@ def logics(key: dict) -> list[dict]:
|
||||
"키": None,
|
||||
"열쇠": "공통 1-2-7 인력운반(기계설비)",
|
||||
**HEAD,
|
||||
"이름": "인력운반(기계설비) — 건설 공통 1-2-7",
|
||||
"이름": "인력운반(기계설비)",
|
||||
"결과단위": "원/자재 단위",
|
||||
"출처": f"{SRC} 5",
|
||||
"소유": "공용",
|
||||
@@ -534,7 +534,7 @@ def logics(key: dict) -> list[dict]:
|
||||
"키": None,
|
||||
"열쇠": "공통 1-2-7 벽돌운반",
|
||||
**HEAD,
|
||||
"이름": "벽돌운반 — 건설 공통 1-2-7",
|
||||
"이름": "벽돌운반",
|
||||
"결과단위": "원/1,000매",
|
||||
"출처": f"{SRC} 4",
|
||||
"소유": "공용",
|
||||
|
||||
@@ -164,12 +164,7 @@ def main() -> None:
|
||||
C = chooser(made, old_coef)
|
||||
|
||||
rows = L1.part1(C) + L2.part2(C) + L3.part3(C)
|
||||
q_rows = {r["열쇠"]: r for r in rows}
|
||||
hourly = {r["키"]: r for r in _load(LOGIC)["줄"]}
|
||||
machines: dict[str, list] = {}
|
||||
for m in _load("기계.json")["줄"]:
|
||||
machines.setdefault(m.get("상세구분"), []).append(m["원문번호"])
|
||||
rows += LC.costs(q_rows, hourly, machines) + [LC.breaker(hourly)]
|
||||
# 작업량당 기계경비(LC.costs · LC.breaker) = 813-2 지움 — 단가산출에 기계 하나 넣어 만듦
|
||||
rows.sort(key=lambda r: _sec(r["원문번호"]))
|
||||
for r in rows:
|
||||
r["키"] = mk.issue(book, "GC", r["열쇠"], LOGIC)
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
· 계(돈 아닌 로직 = 결과)
|
||||
원 단위 — 기본 옵션 전부 + 옵션 표본(로직마다 고르기 입력의 다른 갈래 하나씩 · 나머지 기본).
|
||||
둘 다 못 셈 = 같음(따로 셈). 보정 없음 — 다르면 목록에 까닭.
|
||||
- 저장 = 같은 것만(요구서_664 1 장 3) — 모든 대조가 같고 부르는 단가산출도 저장되는 문서 ·
|
||||
- 채택된 차이(`ADOPTED`) = 사용자 요구 714-3 「1원 차이는 로직만 맞다면 문제 없음」 — 식 속 단가산출
|
||||
부름 값 = 끝수 뒤(새 계산기 그대로) · 목록에 남기고 저장은 막지 않음.
|
||||
- 저장 = 같은 것만(요구서_664 1 장 3) — 모든 대조가 같고(채택된 차이 빼고) 부르는 단가산출도 저장되는 문서 ·
|
||||
`resources/master_data/수식문서/수식_<옛 로직 파일 꼬리>.json`(로직 파일마다
|
||||
· 옛 로직 JSON 은 안 씀).
|
||||
- 목록 = `docs/raw/요구_1104/_목록_802-2_변환대조.xlsx`(로직 · 옵션 · 옛 · 새 · 차이 · 까닭 + 요약).
|
||||
@@ -44,6 +46,8 @@ RAW_SUB = (
|
||||
"식 속 단가산출 부름 = 끝수 뒤 값"
|
||||
"(옛 엔진 = 끝수 전 날 값 · `master_formula.run` 결과를 식에 씀)"
|
||||
)
|
||||
# 채택된 차이 — (로직, 옵션) · 요구 714-3
|
||||
ADOPTED = {("GC000100", "지역=경북")}
|
||||
|
||||
|
||||
def rows_of(files: dict) -> list[tuple[str, dict]]:
|
||||
@@ -108,7 +112,7 @@ def why(doc: dict, old, new) -> str:
|
||||
if isinstance(new, str) and not isinstance(old, str):
|
||||
return f"새 계산기 못 셈 — {new}"
|
||||
if _uses_raw_sub(doc):
|
||||
return RAW_SUB
|
||||
return RAW_SUB + " · 채택(요구 714-3)"
|
||||
return "까닭 모름"
|
||||
|
||||
|
||||
@@ -151,6 +155,7 @@ def build(log=print) -> dict:
|
||||
)
|
||||
for one in runs:
|
||||
if not one["같음"]:
|
||||
one["채택"] = (one["로직"], one["옵션"]) in ADOPTED
|
||||
one["까닭"] = why(docs[one["로직"]], one["옛"], one["새"])
|
||||
keep = _keep(docs, runs)
|
||||
for one in runs: # 같아도 못 넣는 것 — 부르는 단가산출이 빠짐
|
||||
@@ -165,6 +170,7 @@ def build(log=print) -> dict:
|
||||
"옵션 표본": sum(1 for x in runs if x["옵션"]),
|
||||
"옵션 같음": sum(1 for x in runs if x["옵션"] and x["같음"]),
|
||||
"옵션 다름": sum(1 for x in runs if x["옵션"] and not x["같음"]),
|
||||
"채택된 차이": sum(1 for x in runs if x.get("채택")),
|
||||
"둘 다 못 셈": both_out,
|
||||
"저장": len(keep),
|
||||
"변환 알림": sum(1 for v in notes.values() if v),
|
||||
@@ -192,7 +198,7 @@ def _calls(doc: dict) -> set[str]:
|
||||
def _keep(docs: dict, runs: list[dict]) -> set[str]:
|
||||
"""저장할 문서 — 모든 대조 같음 + 부르는 단가산출도 저장(닫힘).
|
||||
키 구멍 부름은 그 로직 대조로 판정."""
|
||||
keep = set(docs) - {x["로직"] for x in runs if not x["같음"]}
|
||||
keep = set(docs) - {x["로직"] for x in runs if not x["같음"] and not x.get("채택")}
|
||||
while True:
|
||||
drop = {k for k in keep if not _calls(docs[k]) <= keep}
|
||||
if not drop:
|
||||
|
||||
@@ -443,6 +443,7 @@ def _build(c: _Conv, row: dict, given: dict, lone_steps: dict) -> dict:
|
||||
"이름": row.get("이름") or "",
|
||||
"결과단위": row.get("결과단위") or "",
|
||||
"출처": row.get("출처") or "",
|
||||
**({"부품": row["부품"]} if row.get("부품") else {}), # 813-2 부품 표지
|
||||
}
|
||||
money: list[dict] = []
|
||||
if "결과" in row:
|
||||
|
||||
@@ -277,11 +277,6 @@
|
||||
"t": {"이름": "기어 변속시간", "근거": "건설품셈 공통 8-2-1 「t : 기어 변속시간(0.25분)」", "확신": "원문"},
|
||||
"cm": {"이름": "1회 싸이클시간", "근거": "건설품셈 공통 8-2-1 「cm : 1회 싸이클 시간」", "확신": "원문"}
|
||||
},
|
||||
"GC001145": {
|
||||
"운반거리": {"기호": "L", "근거": "건설품셈 공통 8-2-1 「L : 운반거리(m)」", "확신": "원문"},
|
||||
"f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-1 「f : 체적환산계수」", "확신": "원문"},
|
||||
"작업량": {"이름": "시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-1 「Q : 시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"}
|
||||
},
|
||||
"GC001146": {
|
||||
"작업거리": {"기호": "ℓ", "근거": "건설품셈 공통 8-2-2 「ℓ : 1회의 작업거리(m)」", "확신": "원문"},
|
||||
"f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-2 「f : 체적환산계수」", "확신": "원문"},
|
||||
@@ -296,11 +291,6 @@
|
||||
"E": {"이름": "작업효율", "근거": "건설품셈 공통 8-2-3 「E : 작업효율」", "확신": "원문"},
|
||||
"cm": {"이름": "1회 싸이클시간", "근거": "건설품셈 공통 8-2-3 「cm : 1회 싸이클 시간(초)」", "확신": "원문"}
|
||||
},
|
||||
"GC001148": {
|
||||
"버킷용량": {"기호": "q", "근거": "건설품셈 공통 8-2-3 「q : 버킷용량(㎥)」", "확신": "원문"},
|
||||
"f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-3 「f : 체적환산계수」", "확신": "원문"},
|
||||
"작업량": {"이름": "시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-3 「Q : 시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"}
|
||||
},
|
||||
"GC001149": {
|
||||
"작업거리": {"기호": "L", "근거": "건설품셈 공통 8-2-4 「L : 1열 실작업거리(편도m)」", "확신": "원문"},
|
||||
"전진거리": {"기호": "L", "아래": "1", "근거": "건설품셈 공통 8-2-4 「L_1 : 1열 전진거리(m)」", "확신": "원문"},
|
||||
@@ -320,12 +310,6 @@
|
||||
"cm": {"이름": "1회 싸이클시간", "근거": "건설품셈 공통 8-2-5 「cm : 1회 싸이클 시간(초)」", "확신": "원문"},
|
||||
"E": {"이름": "작업효율", "근거": "건설품셈 공통 8-2-5 「E : 작업효율」", "확신": "원문"}
|
||||
},
|
||||
"GC001151": {
|
||||
"버킷용량": {"기호": "q", "근거": "건설품셈 공통 8-2-5 「q : 버킷용량(㎥)」", "확신": "원문"},
|
||||
"주행거리": {"기호": "ℓ", "근거": "건설품셈 공통 8-2-5 「ℓ : 편도주행거리(표준을 8m로 한다)」", "확신": "원문"},
|
||||
"f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-5 「f : 체적환산계수」", "확신": "원문"},
|
||||
"작업량": {"이름": "운전시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-5 「Q : 운전시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"}
|
||||
},
|
||||
"GC001152": {
|
||||
"적재주행거리": {"기호": "L", "아래": "1", "근거": "건설품셈 공통 8-2-6 「L_1 : 적재시의 주행거리(m)」", "확신": "원문"},
|
||||
"공차주행거리": {"기호": "L", "아래": "2", "근거": "건설품셈 공통 8-2-6 「L_2 : 공차시의 주행거리(m)」", "확신": "원문"},
|
||||
@@ -375,16 +359,6 @@
|
||||
"t5": {"기호": "t", "아래": "5", "근거": "열쇠 꼴 ○○+숫자 = 첨자", "확신": "추정"},
|
||||
"t6": {"기호": "t", "아래": "6", "근거": "열쇠 꼴 ○○+숫자 = 첨자", "확신": "추정"}
|
||||
},
|
||||
"GC001156": {
|
||||
"적재용량": {"기호": "T", "근거": "건설품셈 공통 8-2-8 「T : 덤프트럭의 적재용량(ton)」", "확신": "원문"},
|
||||
"체적변화율": {"기호": "L", "근거": "건설품셈 공통 8-2-8 「L : 체적환산계수에서의 체적변화율」", "확신": "원문"},
|
||||
"f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-8 「f : 체적환산계수」", "확신": "원문"},
|
||||
"적재싸이클": {"기호": "cms", "근거": "건설품셈 공통 8-2-8 「cms : 적재기계의 1회 싸이클시간(초)」", "확신": "원문"},
|
||||
"적재버킷": {"기호": "q", "근거": "건설품셈 공통 8-2-8 「q : 흐트러진 상태의 덤프트럭 1회 적재량(㎥)」", "확신": "원문"},
|
||||
"버킷계수": {"기호": "k", "근거": "건설품셈 공통 8-2-8 「k : 리퍼 또는 버킷계수」", "확신": "원문"},
|
||||
"적재효율": {"기호": "Es", "근거": "건설품셈 공통 8-2-8 「Es : 적재기계의 작업효율」", "확신": "원문"},
|
||||
"작업량": {"이름": "1시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-8 「Q : 1시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"}
|
||||
},
|
||||
"GC001157": {
|
||||
"다짐횟수": {"기호": "N", "근거": "건설품셈 공통 8-2-9 「N : 소요다짐횟수」", "확신": "원문"},
|
||||
"두께": {"기호": "D", "근거": "건설품셈 공통 8-2-9 「D : 펴는 흙의 두께(m)」", "확신": "원문"},
|
||||
@@ -399,12 +373,6 @@
|
||||
"V": {"이름": "다짐속도", "근거": "건설품셈 공통 8-2-9 「V : 다짐속도(㎞/hr)」", "확신": "원문"},
|
||||
"E": {"이름": "작업효율", "근거": "건설품셈 공통 8-2-9 「E : 작업효율」", "확신": "원문"}
|
||||
},
|
||||
"GC001159": {
|
||||
"다짐횟수": {"기호": "N", "근거": "건설품셈 공통 8-2-9 「N : 소요다짐횟수」", "확신": "원문"},
|
||||
"두께": {"기호": "D", "근거": "건설품셈 공통 8-2-9 「D : 펴는 흙의 두께(m)」", "확신": "원문"},
|
||||
"f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-9 「f : 체적환산계수」", "확신": "원문"},
|
||||
"작업량": {"이름": "시간당 다짐토량", "기호": "Q", "근거": "건설품셈 공통 8-2-9 「Q : 시간당 다짐토량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"}
|
||||
},
|
||||
"GC001161": {
|
||||
"W": {"이름": "유효혼합폭", "근거": "건설품셈 공통 8-2-11 「W : 유효혼합폭(m)」", "확신": "원문"},
|
||||
"V": {"이름": "작업속도", "근거": "건설품셈 공통 8-2-11 「V : 작업속도(1,000m/hr)」", "확신": "원문"},
|
||||
|
||||
@@ -824,7 +824,7 @@
|
||||
"원문번호": "1-2-16",
|
||||
"구분": "건설품셈 유지관리",
|
||||
"상세구분": "01장 공통",
|
||||
"이름": "약제살포(동력분무기)",
|
||||
"이름": "약제살포(기계)",
|
||||
"결과단위": "원/ℓ",
|
||||
"출처": "건설품셈 유지관리 1-2-16",
|
||||
"개정": [2019, 2022],
|
||||
|
||||
@@ -860,7 +860,7 @@
|
||||
"원문번호": "1-6-5",
|
||||
"구분": "건설품셈 토목",
|
||||
"상세구분": "01장 도로포장공사",
|
||||
"이름": "콘크리트 페이버 조립 및 해체",
|
||||
"이름": "기계포설 장비조립 및 해체",
|
||||
"결과단위": "원/회",
|
||||
"출처": "건설품셈 토목 1-6-5",
|
||||
"개정": [2021],
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"원문번호": "1-2-7",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "01장 적용기준",
|
||||
"이름": "인력운반(지게) — 건설 공통 1-2-7",
|
||||
"이름": "지게운반",
|
||||
"결과단위": "원/자재 단위",
|
||||
"출처": "건설품셈 공통 1-2-7 2 · 3",
|
||||
"개정": [2008, 2010, 2016, 2022, 2023],
|
||||
@@ -131,7 +131,7 @@
|
||||
"원문번호": "1-2-7",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "01장 적용기준",
|
||||
"이름": "인력운반(기계설비) — 건설 공통 1-2-7",
|
||||
"이름": "인력운반(기계설비)",
|
||||
"결과단위": "원/자재 단위",
|
||||
"출처": "건설품셈 공통 1-2-7 5",
|
||||
"개정": [2008, 2010, 2016, 2022, 2023],
|
||||
@@ -258,7 +258,7 @@
|
||||
"원문번호": "1-2-7",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "01장 적용기준",
|
||||
"이름": "벽돌운반 — 건설 공통 1-2-7",
|
||||
"이름": "벽돌운반",
|
||||
"결과단위": "원/1,000매",
|
||||
"출처": "건설품셈 공통 1-2-7 4",
|
||||
"개정": [2008, 2010, 2016, 2022, 2023],
|
||||
@@ -282,6 +282,7 @@
|
||||
"이름": "건설폐기물 발생량 — 건설 공통 1-5-4",
|
||||
"결과단위": "ton",
|
||||
"출처": "건설품셈 공통 1-5-4 2",
|
||||
"부품": { "갈래": "계수식" },
|
||||
"개정": [2011, 2014, 2017, 2020],
|
||||
"소유": "공용",
|
||||
"비고": "발생량 = 면적 × 단위면적당 발생량(TON/㎡) × (1 + 증가율 ÷ 100) · 증가율 = 폐콘크리트류만(주③ 파일 시공 15% · 20% 이내 · 주④ 세륜 시설 개소당 3% 이내 — 설계자가 넣음 · 0 = 증가 없음) · 원문 칸 「-」 = 줄 없음(멈춤) · 건축물 해체는 설계도서 산출 우선(원문 2) · 주⑤ 현장 여건 조정",
|
||||
@@ -314,6 +315,7 @@
|
||||
"이름": "공사용수 — 건설 공통 1-5-6",
|
||||
"결과단위": "㎥",
|
||||
"출처": "건설품셈 공통 1-5-6 3",
|
||||
"부품": { "갈래": "계수식" },
|
||||
"소유": "공용",
|
||||
"비고": "용수량 = 작업량 × 단위당 용수(㎥) × (1 + 잡용수율 ÷ 100) · 양생 물 포함([주]) · 해석: 잡용수 = 원문 「사용량비의 40∼50%」 — 잡용수율 0 = 더하지 않음(설계자가 40∼50 넣음)",
|
||||
"시험입력": { "구분": "콘크리트혼합 및 양생", "작업량": 100, "잡용수율": 0 },
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"원문번호": "1-1-1",
|
||||
"구분": "건설품셈 건축",
|
||||
"상세구분": "01장 철골공사",
|
||||
"이름": "철골 공장가공조립(기본철골공수)",
|
||||
"이름": "철골 가공 조립 기본철골공수",
|
||||
"결과단위": "원/ton",
|
||||
"출처": "건설품셈 건축 1-1-1",
|
||||
"개정": [2008, 2013],
|
||||
@@ -80,7 +80,7 @@
|
||||
"원문번호": "1-1-3",
|
||||
"구분": "건설품셈 건축",
|
||||
"상세구분": "01장 철골공사",
|
||||
"이름": "철골 공장용접(기본용접공수)",
|
||||
"이름": "철골 가공 조립 기본용접공수",
|
||||
"결과단위": "원/ton",
|
||||
"출처": "건설품셈 건축 1-1-3",
|
||||
"소유": "공용",
|
||||
@@ -443,7 +443,7 @@
|
||||
"원문번호": "1-2-7",
|
||||
"구분": "건설품셈 건축",
|
||||
"상세구분": "01장 철골공사",
|
||||
"이름": "타워크레인 가설 · 해체 · 수직이동",
|
||||
"이름": "철골세우기용 장비의 가설 및 해체이동",
|
||||
"결과단위": "원/대",
|
||||
"출처": "건설품셈 건축 1-2-7",
|
||||
"소유": "공용",
|
||||
|
||||
@@ -477,7 +477,7 @@
|
||||
"원문번호": "5-1-7",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "05장 기초공사",
|
||||
"이름": "앵커 천공(크롤러형 보링장비)",
|
||||
"이름": "앵커 천공",
|
||||
"결과단위": "원/공",
|
||||
"출처": "건설품셈 공통 5-1-7",
|
||||
"개정": [2020, 2026],
|
||||
@@ -1501,7 +1501,7 @@
|
||||
"원문번호": "5-2-3",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "05장 기초공사",
|
||||
"이름": "PBD천공기 조립 및 해체",
|
||||
"이름": "PBD 장비조립 및 해체",
|
||||
"결과단위": "원/회",
|
||||
"출처": "건설품셈 공통 5-2-3",
|
||||
"개정": [2013, 2021, 2026],
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"원문번호": "6-1-2",
|
||||
"구분": "건설품셈 건축",
|
||||
"상세구분": "06장 방수공사",
|
||||
"이름": "방수프라이머 바름(롤러 1회)",
|
||||
"이름": "방수프라이머 바름",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 건축 6-1-2",
|
||||
"개정": [2018],
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"원문번호": "7-1-1",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "07장 돌공사",
|
||||
"이름": "메쌓기(굴착기+부착용 집게)",
|
||||
"이름": "메쌓기",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 공통 7-1-1",
|
||||
"개정": [2012, 2019, 2026],
|
||||
@@ -49,7 +49,7 @@
|
||||
"원문번호": "7-1-2",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "07장 돌공사",
|
||||
"이름": "찰쌓기(굴착기+부착용 집게)",
|
||||
"이름": "찰쌓기",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 공통 7-1-2",
|
||||
"개정": [2012, 2018, 2019, 2026],
|
||||
@@ -88,7 +88,7 @@
|
||||
"원문번호": "7-2-1",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "07장 돌공사",
|
||||
"이름": "메붙임(굴착기+부착용 집게)",
|
||||
"이름": "메붙임",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 공통 7-2-1",
|
||||
"개정": [2012, 2019, 2026],
|
||||
@@ -127,7 +127,7 @@
|
||||
"원문번호": "7-2-2",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "07장 돌공사",
|
||||
"이름": "찰붙임(굴착기+부착용 집게)",
|
||||
"이름": "찰붙임",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 공통 7-2-2",
|
||||
"개정": [2012, 2019, 2026],
|
||||
@@ -166,7 +166,7 @@
|
||||
"원문번호": "7-3-1",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "07장 돌공사",
|
||||
"이름": "전석쌓기(굴착기+부착용 집게)",
|
||||
"이름": "전석쌓기",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 공통 7-3-1",
|
||||
"개정": [1992, 2012, 2018, 2026],
|
||||
@@ -221,7 +221,7 @@
|
||||
"원문번호": "7-3-2",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "07장 돌공사",
|
||||
"이름": "전석깔기(굴착기+부착용 집게)",
|
||||
"이름": "전석깔기",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 공통 7-3-2",
|
||||
"개정": [2018, 2026],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,7 +85,7 @@
|
||||
"원문번호": "9-2-1",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "09장 기타공사",
|
||||
"이름": "바탕만들기(블라스트 · 공구 · 와이어브러시)",
|
||||
"이름": "바탕만들기",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "건설품셈 기계설비 9-2-1",
|
||||
"소유": "공용",
|
||||
@@ -270,7 +270,7 @@
|
||||
"원문번호": "9-4-1",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "09장 기타공사",
|
||||
"이름": "가스배관 기밀시험",
|
||||
"이름": "기밀시험",
|
||||
"결과단위": "원/회",
|
||||
"출처": "건설품셈 기계설비 9-4-1",
|
||||
"개정": [2015, 2019],
|
||||
|
||||
@@ -1317,7 +1317,7 @@
|
||||
"원문번호": "13-2-6",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "응력제거(Ring Burner · 전기저항)",
|
||||
"이름": "응력제거(Ring Burner, Electric, Resistence Heating)",
|
||||
"결과단위": "원/개소",
|
||||
"출처": "건설품셈 기계설비 13-2-6",
|
||||
"소유": "공용",
|
||||
@@ -1884,7 +1884,7 @@
|
||||
"원문번호": "13-5-1",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "화력 보일러 설치(공정별)",
|
||||
"이름": "화력 보일러 설치",
|
||||
"결과단위": "원/기",
|
||||
"출처": "건설품셈 기계설비 13-5-1",
|
||||
"소유": "공용",
|
||||
@@ -2059,7 +2059,7 @@
|
||||
"원문번호": "13-5-2",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "보일러 드럼 설치(공정별)",
|
||||
"이름": "보일러 드럼 설치",
|
||||
"결과단위": "원/대",
|
||||
"출처": "건설품셈 기계설비 13-5-2",
|
||||
"소유": "공용",
|
||||
@@ -2275,7 +2275,7 @@
|
||||
"원문번호": "13-5-4",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "덕트 설치(공정별)",
|
||||
"이름": "덕트 설치",
|
||||
"결과단위": "원/식",
|
||||
"출처": "건설품셈 기계설비 13-5-4",
|
||||
"소유": "공용",
|
||||
@@ -2360,7 +2360,7 @@
|
||||
"원문번호": "13-5-5",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "공기예열기(Preheater) 설치(공정별)",
|
||||
"이름": "공기예열기(Preheater) 설치",
|
||||
"결과단위": "원/식",
|
||||
"출처": "건설품셈 기계설비 13-5-5",
|
||||
"소유": "공용",
|
||||
@@ -2555,7 +2555,7 @@
|
||||
"원문번호": "13-5-8",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "터빈 설치(공정별)",
|
||||
"이름": "터빈 설치",
|
||||
"결과단위": "원/기",
|
||||
"출처": "건설품셈 기계설비 13-5-8",
|
||||
"소유": "공용",
|
||||
@@ -2716,7 +2716,7 @@
|
||||
"원문번호": "13-5-9",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "발전기 설치(공정별)",
|
||||
"이름": "발전기 설치",
|
||||
"결과단위": "원/기",
|
||||
"출처": "건설품셈 기계설비 13-5-9",
|
||||
"소유": "공용",
|
||||
@@ -2842,7 +2842,7 @@
|
||||
"원문번호": "13-5-10",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "복수기 설치(공정별)",
|
||||
"이름": "복수기 설치",
|
||||
"결과단위": "원/기",
|
||||
"출처": "건설품셈 기계설비 13-5-10",
|
||||
"소유": "공용",
|
||||
@@ -3350,7 +3350,7 @@
|
||||
"원문번호": "13-6-1",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "수차 설치(Kaplan · Francis · Propeller)",
|
||||
"이름": "수차 설치",
|
||||
"결과단위": "원/ton",
|
||||
"출처": "건설품셈 기계설비 13-6-1",
|
||||
"소유": "공용",
|
||||
@@ -6728,7 +6728,7 @@
|
||||
"원문번호": "13-7-19",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "Mill Line 기기류 조립설치(공정별)",
|
||||
"이름": "Mill Line 기기류 조립설치",
|
||||
"결과단위": "원/식",
|
||||
"출처": "건설품셈 기계설비 13-7-19",
|
||||
"소유": "공용",
|
||||
@@ -6772,7 +6772,7 @@
|
||||
"원문번호": "13-7-20",
|
||||
"구분": "건설품셈 기계설비",
|
||||
"상세구분": "13장 플랜트설비공사",
|
||||
"이름": "Roller Table 조립설치(공정별)",
|
||||
"이름": "Roller Table 조립설치",
|
||||
"결과단위": "원/식",
|
||||
"출처": "건설품셈 기계설비 13-7-20",
|
||||
"소유": "공용",
|
||||
@@ -8793,6 +8793,7 @@
|
||||
"이름": "아세틸렌량의 환산(ℓ → ㎏)",
|
||||
"결과단위": "㎏",
|
||||
"출처": "건설품셈 기계설비 13-2-7",
|
||||
"부품": { "갈래": "계수식" },
|
||||
"소유": "공용",
|
||||
"비고": "아세틸렌(㎏) = 아세틸렌(ℓ) × 26g/22.4ℓ ÷ 1,000 · 26g = 1mol당 분자량 · 22.4ℓ = 표준상태 1mol당량",
|
||||
"시험입력": { "아세틸렌부피": 1000 },
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"이름": "임산물 운반로 신설 굴착기 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 3-4-2 1",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "해석: 원문 단가산출 예시의 값(q 0.2 · K 0.7 · f 0.71 · E 0.45 · Cm 18)을 표준 값으로 씀",
|
||||
@@ -103,7 +104,7 @@
|
||||
"원문번호": "3-4-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "03장 작업장 관리",
|
||||
"이름": "임산물 운반로 및 작업로 신설(굴착기 0.2㎥)",
|
||||
"이름": "임산물 운반로 및 작업로 신설비 산정",
|
||||
"결과단위": "원/m",
|
||||
"출처": "산림품셈 3-4-2",
|
||||
"개정": [2026],
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
"원문번호": "4-2-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "04장 나무베기",
|
||||
"이름": "단목베기(1,000㎡당 · 굴착기 0.2㎥ + 부착용 집게)",
|
||||
"이름": "단목베기(1,000㎡당)",
|
||||
"결과단위": "원/1,000㎡",
|
||||
"출처": "산림품셈 4-2-2",
|
||||
"개정": [2026],
|
||||
@@ -199,7 +199,7 @@
|
||||
"원문번호": "4-3",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "04장 나무베기",
|
||||
"이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)",
|
||||
"이름": "위험목 베기",
|
||||
"결과단위": "원/본",
|
||||
"출처": "산림품셈 4-3",
|
||||
"개정": [2026],
|
||||
|
||||
@@ -1199,6 +1199,7 @@
|
||||
"이름": "복사이식 굴착기 시간당 작업량(Q1)",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 5-29 주④",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "해석: q = 주③ 굴착기 용량 1.0㎥ · 주⑤ Q=Q1×T 는 단위가 안 맞아(㎥/시간 × m) 넣지 않음",
|
||||
@@ -1219,7 +1220,7 @@
|
||||
"원문번호": "5-29",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "05장 식재 및 녹화",
|
||||
"이름": "복사이식(굴착기 1.0㎥)",
|
||||
"이름": "복사이식",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 5-29",
|
||||
"개정": [2026],
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
"원문번호": "8-6-3",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "08장 방제",
|
||||
"이름": "지상방제 차량살포(동력분무기)",
|
||||
"이름": "지상방제 차량살포",
|
||||
"결과단위": "원/일",
|
||||
"출처": "산림품셈 8-6-3",
|
||||
"개정": [2026],
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"이름": "토사깍기(기계 · 굴착기 0.7㎥) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-3-2",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "E 원문 「0.55∼0.45」 — 설계자 입력 · f 는 주④ 0.77",
|
||||
@@ -73,7 +74,7 @@
|
||||
"원문번호": "9-3-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "토사깍기(기계 · 굴착기 0.7㎥)",
|
||||
"이름": "토사깍기(기계)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-3-2",
|
||||
"개정": [2026],
|
||||
@@ -94,7 +95,7 @@
|
||||
"원문번호": "9-4-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "암파쇄(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "암파쇄",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-4-1",
|
||||
"개정": [2026],
|
||||
@@ -158,6 +159,7 @@
|
||||
"이름": "암절취 집토(굴착기 0.7㎥) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-4-2",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "f 원문 「1/1.40」 그대로 계산",
|
||||
@@ -178,7 +180,7 @@
|
||||
"원문번호": "9-4-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "암절취 집토(굴착기 0.7㎥)",
|
||||
"이름": "암절취 집토",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-4-2",
|
||||
"개정": [2026],
|
||||
@@ -235,7 +237,7 @@
|
||||
"원문번호": "9-5-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "발파암 깎기 90%(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "발파암 깎기(90%)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-5-2",
|
||||
"개정": [2026],
|
||||
@@ -299,6 +301,7 @@
|
||||
"이름": "발파암 집토(굴착기 0.7㎥) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-5-3",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "f 원문 「1/1.625」 그대로 계산",
|
||||
@@ -319,7 +322,7 @@
|
||||
"원문번호": "9-5-3",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "발파암 집토(굴착기 0.7㎥)",
|
||||
"이름": "발파암 집토",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-5-3",
|
||||
"개정": [2026],
|
||||
@@ -340,7 +343,7 @@
|
||||
"원문번호": "9-6-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "발파암 기계소할(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "발파암 기계소할(15%)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-6-1",
|
||||
"개정": [2026],
|
||||
@@ -388,6 +391,7 @@
|
||||
"이름": "콘크리트 깨기 집토(굴착기 0.7㎥) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-7-1 주⑥",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"입력": [],
|
||||
@@ -407,7 +411,7 @@
|
||||
"원문번호": "9-7-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "콘크리트 깨기 집토(굴착기 0.7㎥)",
|
||||
"이름": "무근콘크리트 깨기 집토",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-7-1 주⑥",
|
||||
"개정": [2026],
|
||||
@@ -428,7 +432,7 @@
|
||||
"원문번호": "9-7-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "무근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "무근콘크리트 깨기(T=30㎝ 미만)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-7-1",
|
||||
"개정": [2026],
|
||||
@@ -475,7 +479,7 @@
|
||||
"원문번호": "9-7-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "무근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "무근콘크리트 깨기(T=30㎝ 이상)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-7-2",
|
||||
"개정": [2026],
|
||||
@@ -579,7 +583,7 @@
|
||||
"원문번호": "9-8-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "철근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "철근콘크리트 깨기(T=30㎝ 미만)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-8-1",
|
||||
"개정": [2026],
|
||||
@@ -626,7 +630,7 @@
|
||||
"원문번호": "9-8-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "철근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "철근콘크리트 깨기(T=30㎝ 이상)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-8-2",
|
||||
"개정": [2026],
|
||||
@@ -691,7 +695,7 @@
|
||||
"원문번호": "9-10-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "기존포장 깨기 콘크리트 깨기(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "기존포장 깨기 콘크리트",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-10-1",
|
||||
"개정": [2026],
|
||||
@@ -738,7 +742,7 @@
|
||||
"원문번호": "9-10-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "기존포장 깨기 아스팔트 깨기(대형브레이커 · 굴착기 0.7㎥)",
|
||||
"이름": "기존포장 깨기 아스팔트",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-10-2",
|
||||
"개정": [2026],
|
||||
@@ -825,6 +829,7 @@
|
||||
"이름": "측구터파기 토사 굴착기 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-12-1",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"입력": [],
|
||||
@@ -2028,6 +2033,7 @@
|
||||
"이름": "되메우기 굴착기 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-14-1",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"입력": [],
|
||||
@@ -2080,6 +2086,7 @@
|
||||
"이름": "다짐(플레이트콤펙터) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-14-2",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["플레이트 콤팩터"] },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "돈 로직 GF000249",
|
||||
@@ -2101,7 +2108,7 @@
|
||||
"원문번호": "9-14-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "다짐(플레이트콤펙터 1.5ton)",
|
||||
"이름": "다짐(플레이트콤펙터)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-14-2",
|
||||
"개정": [2026],
|
||||
@@ -2121,6 +2128,7 @@
|
||||
"이름": "표토제거 답(畓)구간(굴착기 0.7㎥) 시간당 작업량",
|
||||
"결과단위": "㎡/hr",
|
||||
"출처": "산림품셈 9-15-1",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "T(표토두께) 설계자 입력 · Q=Q1/T · f 원문 「1/1.30」 그대로 계산",
|
||||
@@ -2141,7 +2149,7 @@
|
||||
"원문번호": "9-15-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "표토제거 답(畓)구간(굴착기 0.7㎥)",
|
||||
"이름": "표토제거 답(畓)구간",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "산림품셈 9-15-1",
|
||||
"개정": [2026],
|
||||
@@ -2165,6 +2173,7 @@
|
||||
"이름": "표토제거 답(畓)외구간(불도저 19ton) 시간당 작업량",
|
||||
"결과단위": "㎡/hr",
|
||||
"출처": "산림품셈 9-15-2",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["불도저"] },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "돈 로직 GF000250 · f 원문 「1/1.3」 그대로 계산",
|
||||
@@ -2201,7 +2210,7 @@
|
||||
"원문번호": "9-15-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "표토제거 답(畓)외구간(불도저 19ton)",
|
||||
"이름": "표토제거 답(畓)외구간",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "산림품셈 9-15-2",
|
||||
"개정": [2026],
|
||||
@@ -2224,6 +2233,7 @@
|
||||
"이름": "노체포설 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-16-1 주②",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"] },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"입력": [],
|
||||
@@ -2243,7 +2253,7 @@
|
||||
"원문번호": "9-16-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "노체포설(굴착기 0.7㎥ 무한궤도)",
|
||||
"이름": "노체포설",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-16-1",
|
||||
"개정": [2026],
|
||||
@@ -2270,6 +2280,7 @@
|
||||
"이름": "노체다짐 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-16-2 주②",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["머캐덤 롤러", "탠덤롤러", "진동롤러", "타이어 롤러", "양족식 롤러"] },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"입력": [],
|
||||
@@ -2341,6 +2352,7 @@
|
||||
"이름": "비다짐(정지 · 불도저 19ton) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-17-2",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["불도저"] },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "돈 로직 GF000251 · E 원문 「(0.55-0.1)」 · f 원문 「1/1.3」 그대로 계산",
|
||||
@@ -2372,7 +2384,7 @@
|
||||
"원문번호": "9-17-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "비다짐(정지 · 불도저 19ton)",
|
||||
"이름": "비다짐(정지)",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-17-2",
|
||||
"개정": [2026],
|
||||
@@ -2395,6 +2407,7 @@
|
||||
"이름": "층따기(굴착기 0.7㎥) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-18",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "f 원문 「1/1.3」 그대로 계산",
|
||||
@@ -2415,7 +2428,7 @@
|
||||
"원문번호": "9-18",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "층따기(굴착기 0.7㎥)",
|
||||
"이름": "층따기",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-18",
|
||||
"개정": [2026],
|
||||
@@ -2478,7 +2491,7 @@
|
||||
"원문번호": "9-19-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "토사면 성토면 고르기(기계시공 · 굴착기 0.6㎥)",
|
||||
"이름": "토사면 성토면 고르기(기계시공)",
|
||||
"결과단위": "원/10㎡",
|
||||
"출처": "산림품셈 9-19-1",
|
||||
"개정": [2026],
|
||||
@@ -2641,6 +2654,7 @@
|
||||
"이름": "섞기(굴착기 0.7㎥) 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 9-22",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "f 원문 「1/1.25」 그대로 계산 · 주③ Q=Q1*T 는 단위가 맞지 않아 Q1 까지만",
|
||||
@@ -2661,7 +2675,7 @@
|
||||
"원문번호": "9-22",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "09장 토공",
|
||||
"이름": "섞기(굴착기 0.7㎥)",
|
||||
"이름": "섞기",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 9-22",
|
||||
"개정": [2026],
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"이름": "골재운반 덤프트럭 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 10-3-1 주",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["덤프트럭"] },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "원문은 식만(Q=60×qt×f×E/㎝) — qt · f · E · ㎝ 값 없음 · 설계자 입력",
|
||||
@@ -26,7 +27,7 @@
|
||||
"원문번호": "10-3-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "골재운반 모래(덤프 15ton)",
|
||||
"이름": "골재운반 모래",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 10-3-1",
|
||||
"개정": [2026],
|
||||
@@ -61,7 +62,7 @@
|
||||
"원문번호": "10-3-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "골재운반 잡석(덤프 15ton)",
|
||||
"이름": "골재운반 잡석",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 10-3-2",
|
||||
"개정": [2026],
|
||||
@@ -96,7 +97,7 @@
|
||||
"원문번호": "10-3-3",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "골재운반 혼합골재(덤프 15ton)",
|
||||
"이름": "골재운반 혼합골재",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 10-3-3",
|
||||
"개정": [2026],
|
||||
@@ -131,7 +132,7 @@
|
||||
"원문번호": "10-4",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "중기운반 트레일러(20ton)",
|
||||
"이름": "중기운반",
|
||||
"결과단위": "원/회",
|
||||
"출처": "산림품셈 10-4",
|
||||
"개정": [2026],
|
||||
@@ -518,6 +519,7 @@
|
||||
"이름": "4륜구동 1톤 덤프트럭 시간당 기계경비",
|
||||
"결과단위": "원/시간",
|
||||
"출처": "산림품셈 10-9",
|
||||
"부품": { "갈래": "기계경비식" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "손료·운전경비는 2.5톤(0602-0025) · 주연료 2.5톤의 80% · 중기가격 조달청 등재가격은 요소 없음 · 작업량(건설품셈 덤프트럭 식)·10-9-1 운반량 쓰는 식 요소 없음 · 주② 제잡비 비율 원문에 없음",
|
||||
@@ -565,7 +567,7 @@
|
||||
"원문번호": "10-9-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "덤프트럭 자재 운반(1회 표준 운반량)",
|
||||
"이름": "덤프트럭 운반(자재의 1회당 표준 운반량)",
|
||||
"결과단위": "원/ton",
|
||||
"출처": "산림품셈 10-9-1",
|
||||
"개정": [2026],
|
||||
@@ -673,6 +675,7 @@
|
||||
"이름": "불도저 운반 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 10-11 주②",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["불도저"] },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "해석: 파쇄암·발파암의 E 는 「암석」 줄",
|
||||
@@ -729,7 +732,7 @@
|
||||
"원문번호": "10-12-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "덤프운반(굴착기 0.7㎥ 적재 · 덤프트럭 15ton)",
|
||||
"이름": "덤프운반 토사",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 10-12-1",
|
||||
"개정": [2026],
|
||||
@@ -861,7 +864,7 @@
|
||||
"원문번호": "10-1",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "시멘트운반(구역화물 운임 + 하차비)",
|
||||
"이름": "시멘트운반",
|
||||
"결과단위": "원/포",
|
||||
"출처": "산림품셈 10-1",
|
||||
"개정": [2026],
|
||||
@@ -964,7 +967,7 @@
|
||||
"원문번호": "10-2",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "10장 자재․장비 운반",
|
||||
"이름": "철근운반(구역화물 운임 + 하차비)",
|
||||
"이름": "철근운반",
|
||||
"결과단위": "원/ton",
|
||||
"출처": "산림품셈 10-2",
|
||||
"개정": [2026],
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
"이름": "쇄석·혼합석 부설 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 11-4 주①",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "해석: 원문 식 「3600×90×k×f×E/㎝」의 90 을 q(버킷용량)로 봄",
|
||||
@@ -113,7 +114,7 @@
|
||||
"원문번호": "11-4",
|
||||
"구분": "산림품셈",
|
||||
"상세구분": "11장 가설",
|
||||
"이름": "쇄석·혼합석 부설(굴착기 0.7㎥)",
|
||||
"이름": "쇄석·혼합석 부설",
|
||||
"결과단위": "원/㎥",
|
||||
"출처": "산림품셈 11-4",
|
||||
"개정": [2026],
|
||||
|
||||
@@ -1323,6 +1323,7 @@
|
||||
"이름": "토양개량제 부설 굴착기 시간당 작업량",
|
||||
"결과단위": "㎥/hr",
|
||||
"출처": "산림품셈 13-13-2",
|
||||
"부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" },
|
||||
"개정": [2026],
|
||||
"소유": "공용",
|
||||
"비고": "식은 9장 같은 꼴 표(9-12-1 등)와 같이 Q=3600×q×k×f×E/㎝",
|
||||
|
||||
@@ -704,7 +704,7 @@
|
||||
"꼴": "수식문서",
|
||||
"판": 1,
|
||||
"키": "GC001013",
|
||||
"이름": "약제살포(동력분무기)",
|
||||
"이름": "약제살포(기계)",
|
||||
"결과단위": "원/ℓ",
|
||||
"출처": "건설품셈 유지관리 1-2-16",
|
||||
"변수": [
|
||||
|
||||
@@ -889,7 +889,7 @@
|
||||
"꼴": "수식문서",
|
||||
"판": 1,
|
||||
"키": "GC000300",
|
||||
"이름": "콘크리트 페이버 조립 및 해체",
|
||||
"이름": "기계포설 장비조립 및 해체",
|
||||
"결과단위": "원/회",
|
||||
"출처": "건설품셈 토목 1-6-5",
|
||||
"변수": [
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"꼴": "수식문서",
|
||||
"판": 1,
|
||||
"키": "GC001140",
|
||||
"이름": "인력운반(지게) — 건설 공통 1-2-7",
|
||||
"이름": "지게운반",
|
||||
"결과단위": "원/자재 단위",
|
||||
"출처": "건설품셈 공통 1-2-7 2 · 3",
|
||||
"변수": [
|
||||
@@ -55,7 +55,7 @@
|
||||
"꼴": "수식문서",
|
||||
"판": 1,
|
||||
"키": "GC001141",
|
||||
"이름": "인력운반(기계설비) — 건설 공통 1-2-7",
|
||||
"이름": "인력운반(기계설비)",
|
||||
"결과단위": "원/자재 단위",
|
||||
"출처": "건설품셈 공통 1-2-7 5",
|
||||
"변수": [
|
||||
@@ -102,7 +102,7 @@
|
||||
"꼴": "수식문서",
|
||||
"판": 1,
|
||||
"키": "GC001142",
|
||||
"이름": "벽돌운반 — 건설 공통 1-2-7",
|
||||
"이름": "벽돌운반",
|
||||
"결과단위": "원/1,000매",
|
||||
"출처": "건설품셈 공통 1-2-7 4",
|
||||
"변수": [
|
||||
@@ -128,6 +128,9 @@
|
||||
"이름": "건설폐기물 발생량 — 건설 공통 1-5-4",
|
||||
"결과단위": "ton",
|
||||
"출처": "건설품셈 공통 1-5-4 2",
|
||||
"부품": {
|
||||
"갈래": "계수식"
|
||||
},
|
||||
"결과": "Q",
|
||||
"변수": [
|
||||
{"열쇠": "공사", "기호": "S", "아래": "공사", "위": "", "이름": "공사", "단위": "", "설명": "신축 · 해체", "출처": "건설품셈 공통 1-5-4 2", "연결": {"갈래": "선택", "목록": ["신축", "해체"], "값": "신축"}},
|
||||
@@ -157,6 +160,9 @@
|
||||
"이름": "공사용수 — 건설 공통 1-5-6",
|
||||
"결과단위": "㎥",
|
||||
"출처": "건설품셈 공통 1-5-6 3",
|
||||
"부품": {
|
||||
"갈래": "계수식"
|
||||
},
|
||||
"결과": "Q",
|
||||
"변수": [
|
||||
{"열쇠": "구분", "기호": "S", "아래": "구분", "위": "", "이름": "구분", "단위": "", "설명": "용수 구분", "출처": "건설품셈 공통 1-5-6 3", "연결": {"갈래": "선택", "목록": ["거푸집씻기", "콘크리트혼합 및 양생", "경량콘크리트혼합 및 양생", "보통벽돌쌓기", "돌쌓기모르타르", "돌씻기", "미장", "타일붙임모르타르", "타일씻기"], "값": "콘크리트혼합 및 양생"}},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user