From 56a3e0538f6d1c7b6f2b115fe57bdbe38817064f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 10 Nov 2026 14:14:21 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(814-1=201=C2=B72):=20=EC=98=AC?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=20=ED=8C=8C=EC=9D=BC=20=ED=95=98=EB=82=98=20?= =?UTF-8?q?=C2=B7=20=ED=99=95=EC=9E=A5=EC=9E=90=20=ED=8C=8C=EC=9D=BC=20=3D?= =?UTF-8?q?=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=ED=91=9C=20=ED=8C=90=20?= =?UTF-8?q?+=20=EC=82=B0=EC=B6=9C=EA=B7=BC=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 같은 공사 엑셀 · 원파일 붙이기 걷음(keep · upload_native · same 지움) — 엑셀이든 .estx · .STC 든 하나만 - 원파일 = 새 HP · 산출근거 머리(코드 · 이름 · 규격 · 단위 · 저장 금액) 단가산출표 격자 · 일위대가 안 만듦(HeadMainDat 안 씀) - 화면 = 일위대가 없으면 단가산출 목록 먼저 · 올림 안내 글 하나로 - 시험 810-2: 붙음 · EST 호표 → SC 기대 지움(요구 742-1 · 743-2 로 걷은 길) → 파일 하나 · 단가산출표 판 기대 Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Doc_FromNative.py | 114 ++++++------------ .../M02_MasterTemplete_Hopyo_Sheet.ts | 3 +- .../M02_MasterTemplete_Hopyo_Side.ts | 14 +-- M02_MasterTemplete/M02_Sangeun_FromHopyo.py | 37 +----- resources/tester/test_810_2_native_doc.py | 56 +++------ 5 files changed, 69 insertions(+), 155 deletions(-) diff --git a/M02_MasterTemplete/M02_Doc_FromNative.py b/M02_MasterTemplete/M02_Doc_FromNative.py index 5a7e819e1..207a061c3 100644 --- a/M02_MasterTemplete/M02_Doc_FromNative.py +++ b/M02_MasterTemplete/M02_Doc_FromNative.py @@ -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 @@ -154,89 +153,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: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index cee5e0942..8e0f9632e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -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) => { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index 7c62c78a3..4ac6a7a91 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -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); diff --git a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py index fa86b982b..6ea2aedd0 100644 --- a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py +++ b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py @@ -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,15 +94,7 @@ 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) + row = hopyo_store.save(folder, name, fn.hopyo(native), who, fn.grid(native), mark) path = _path(folder, row["키"]) path.parent.mkdir(parents=True, exist_ok=True) atomic_write_json(path, {"파일": name, "원파일": native}) diff --git a/resources/tester/test_810_2_native_doc.py b/resources/tester/test_810_2_native_doc.py index dd4be6446..a8005b3f8 100644 --- a/resources/tester/test_810_2_native_doc.py +++ b/resources/tester/test_810_2_native_doc.py @@ -6,8 +6,8 @@ (b) 꼴 — 소계 = 합 밖 · 비목 칸 비움(문서 셈 금액만) · 기계 세 줄 = 묶음 한 줄 · 변수 = 이름(상수 아님) · 분수 그림 공식 = 변수 식 · 수 하나 변수 = 숫자 연결 + 출처 「원파일」 · 품셈 조건 글 = 「추정」 표지 (c) 문서 셈 거울 — 비목 없는 합 밖 줄 파이썬 = TS -(d) 올림 길 — 원파일 = 같은 공사 엑셀 HP 에 붙음 · 가져온 식 = 원파일 · 원파일만 = 새 HP(EST 호표 · - STmate 호표 없음) · 원파일 HP 호표 → 일위대가(SC) 금액 = 원파일 · 엑셀만 = 엑셀 길 그대로 +(d) 올림 길(814-1 파일 하나) — 원파일 = 새 HP(단가산출표 판 · 일위대가 없음 · 엑셀 HP 에 안 붙음) · + 가져온 식 = 원파일 · 엑셀 HP = 엑셀 길 그대로 원파일 · 실무 엑셀 없으면 그 시험만 skip(다른 PC). """ @@ -33,7 +33,6 @@ from test_810_1_stc import FILES as STC # noqa: E402 from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402 from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402 -from M02_MasterTemplete import M02_Hopyo_Build as hb # noqa: E402 from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402 from M02_MasterTemplete import M02_Hopyo_Store as hs # noqa: E402 from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402 @@ -200,53 +199,34 @@ def _excel_hp(folder: Path, tag: str) -> str: return row["키"] -def test_upload_native_joins_excel_and_wins(world) -> None: +def test_upload_native_one_file_list_only(world) -> None: + """814-1 — 파일 하나: 원파일은 같은 공사 엑셀 HP 에 안 붙고 새 HP(단가산출표 판 · 일위대가 없음).""" folder = world["personal"] - est = _file(EST["거창"]) + est, stc = _file(EST["거창"]), _file(STC["봉화"]) with store.at(folder): hp = _excel_hp(folder, "거창") - before = fh.emit(folder, hp, 15) - assert not before["원본"].get("원파일") # 엑셀만 = 엑셀 길 got = fh.upload_native(folder, est.name, est.read_bytes(), WHO) - assert got["붙음"] == [hp] and got["키"] == hp and got["원파일"] + assert got["원파일"] and got["키"] != hp and "붙음" not in got and got["호표수"] == 0 + assert not fh.emit(folder, hp, 15)["원본"].get("원파일") # 엑셀 HP = 엑셀 길 그대로 again = fh.upload_native(folder, est.name, est.read_bytes(), WHO) - assert again["붙음"] == [hp] # 다시 올려도 새 HP 없음 - assert len(hs.rows(folder)) == 1 - made = fh.make(folder, hp, 15) # 전석 메쌓기 — 안쪽 산근 둘 + 굴삭기 + assert again["이어서"] and again["키"] == got["키"] and len(hs.rows(folder)) == 2 + grid = hs.grid(folder, got["키"]) + assert grid["보기"] == {"단가산출표": "s1"} and not grid["호표"] + assert len(grid["목록"]["단산"]) == len(fn.numbers(native("EST", "거창"))) + made = fh.make(folder, got["키"], 15) # 전석 메쌓기 — 안쪽 산근 둘 + 굴삭기 sk = store.read("sangeun", made["이름"])["문서"] assert sk["가져온"]["원본"]["원파일"] and made["검산"]["같음"] - refs = [x for x in sk["가져온"]["로직"]["호표"] if x.get("참조")] - assert len(refs) == 2 # 산근 속 산근 = 형제 SK 참조(797-1) - doc = dim.of(sk) - assert doc["가져온"]["원파일"] is True - ans = dim.run(doc, sk) + assert len([x for x in sk["가져온"]["로직"]["호표"] if x.get("참조")]) == 2 + ans = dim.run(dim.of(sk), sk) assert (ans["노무비"], ans["재료비"], ans["경비"]) == (74880, 7029, 12306) - # 엑셀 먼저 · 원파일 나중의 반대 — 원파일 HP 가 있으면 엑셀 올림에 붙음 - other = world["company"] - with store.at(other): - fh.upload_native(other, est.name, est.read_bytes(), WHO) - hp2 = _excel_hp(other, "거창") - assert fh.emit(other, hp2, 1)["원본"].get("원파일") - - -def test_upload_native_alone_hopyo_to_sc(world) -> None: - folder = world["personal"] - est, stc = _file(EST["장수"]), _file(STC["봉화"]) - with store.at(folder): - got = fh.upload_native(folder, est.name, est.read_bytes(), WHO) - assert got["원파일"] and not got.get("붙음") and got["호표수"] == 3 - out = hb.build_all(folder, folder, got["키"], None, True, 1) # 제1호표 = SC + 부르는 SK - assert [x["번"] for x in out["만듦"]] == [1] and out["같음"] == 1 and not out["실패"] - assert out["단가산출"] and all( - store.read("sangeun", k)["문서"]["가져온"]["원본"].get("원파일") - for k in out["단가산출"] - ) st = fh.upload_native(folder, stc.name, stc.read_bytes(), WHO) - assert st["원파일"] and st["호표수"] == 0 # STmate 일위대가 구성 못 읽음 — 산근만 + assert st["원파일"] and st["호표수"] == 0 + head = hs.grid(folder, st["키"])["책"]["시트"][0]["칸"] + assert head["A1"]["값"] == "코드" and str(head["A2"]["값"]).startswith("D") made = fh.make(folder, st["키"], next(iter(fn.numbers(native("STC", "봉화"))))) assert made["검산"]["같음"] with pytest.raises(store.StoreError): - fh.upload_native(folder, "깨진.estx", b"PK\x03\x04garbage", WHO) + fh.upload_native(folder, "깨진.estx", b"PKgarbage", WHO) def test_router_branch_by_extension() -> None: From 8914b21bad7eaa2aa7eebd6528664124cda2f99e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 10 Nov 2026 14:46:53 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(814-1=205):=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EB=8B=B9=20=EA=B2=BD=EB=B9=84=20=EA=B3=84=EC=82=B0=EC=8B=9D=20?= =?UTF-8?q?=EA=B8=B0=EA=B3=84=20=EB=AA=A9=EB=A1=9D=20=E2=80=94=20=EC=9B=90?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20=EC=9E=88=EB=8A=94=EB=8D=B0=20=EB=B9=A0?= =?UTF-8?q?=EC=A7=84=20=EA=B8=B0=EA=B3=84=20=EB=8D=94=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기계표 630 대 가운데 어느 계산식(GC000267 ~ 276)에도 없던 384 대를 자료대로 배정(빌더 build_기계목록_빠진기계_814_1) · 연료 · 조종원 = GC000268 92 · 조종원 직종 없음 = GC000275 18 · 조종원 없음 = GC000272 43 · 연료 없음 = GC000276 7 · 손료만 = GC000273 198 · 못 넣음 50 줄(33 대) = 기계표 값 빔 · 범위(「13∼18」) — 지어 넣는 값 없음 - 로직 · 수식 문서 「기계」 목록 끝에 덧붙임(기본값 그대로) — 1,513 문서 기본 · 옵션 표본 18,797 금액 전후 같음 · 시간당 경비 595 대 셈 됨 - 조합(굴착기 + 브레이커 · 집게) = 우리 조합 식 GC000270 · 271 부름 글(`M02_Machine_Hourly.combo`) - 802-2 대조 목록 다시 지음(옵션 표본 수만 늚) Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Machine_Hourly.py | 21 + docs/raw/요구_1104/_목록_802-2_변환대조.xlsx | Bin 13009 -> 13010 bytes .../scripts/build_기계목록_빠진기계_814_1.py | 161 ++++++++ .../로직_건설품셈_08장_건설기계.json | 361 +++++++++++++++++- .../수식문서/수식_건설품셈_08장_건설기계.json | 10 +- 5 files changed, 543 insertions(+), 10 deletions(-) create mode 100644 resources/master_data/scripts/build_기계목록_빠진기계_814_1.py diff --git a/M02_MasterTemplete/M02_Machine_Hourly.py b/M02_MasterTemplete/M02_Machine_Hourly.py index 50b2c94da..3febcb3b0 100644 --- a/M02_MasterTemplete/M02_Machine_Hourly.py +++ b/M02_MasterTemplete/M02_Machine_Hourly.py @@ -83,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 diff --git a/docs/raw/요구_1104/_목록_802-2_변환대조.xlsx b/docs/raw/요구_1104/_목록_802-2_변환대조.xlsx index be806a05eb9a0e01eafc6338bdc0b709a11e073a..0c2ed113921c66a687ee0a218c95c4ad2f37b471 100644 GIT binary patch delta 1201 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 diff --git a/resources/master_data/scripts/build_기계목록_빠진기계_814_1.py b/resources/master_data/scripts/build_기계목록_빠진기계_814_1.py new file mode 100644 index 000000000..f822bba99 --- /dev/null +++ b/resources/master_data/scripts/build_기계목록_빠진기계_814_1.py @@ -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) diff --git a/resources/master_data/로직_건설품셈_08장_건설기계.json b/resources/master_data/로직_건설품셈_08장_건설기계.json index cf980a4ba..929d896f8 100644 --- a/resources/master_data/로직_건설품셈_08장_건설기계.json +++ b/resources/master_data/로직_건설품셈_08장_건설기계.json @@ -192,7 +192,99 @@ "2502-0075", "5205-0035", "2702-0030", - "2702-0060" + "2702-0060", + "0211-0060", + "0211-0080", + "0211-0100", + "0221-0040", + "0221-0070", + "0302-0025", + "0302-0172", + "0302-0229", + "0302-0287", + "0302-0350", + "0302-0500", + "0406-0054", + "0406-0115", + "0406-0161", + "0406-0206", + "0503-0036", + "0602-0200", + "0602-0240", + "0602-0320", + "1106-0015", + "1206-0010", + "1206-0014", + "1209-0001", + "1209-0002", + "1209-0004", + "1209-0006", + "1209-0007", + "1209-0008", + "1209-0013", + "1306-0044", + "1306-0060", + "1406-0025", + "1506-0011", + "1506-0012", + "1506-0015", + "1506-0019", + "1506-0025", + "1506-0030", + "1506-0037", + "2602-0015", + "2602-0025", + "2602-0035", + "2602-0045", + "2702-0040", + "3530-0015", + "3530-0036", + "3601-0204", + "3601-0302", + "3701-0200", + "3805-0120", + "4504-0021", + "4504-0028", + "5405-0150", + "5701-0010", + "7106-0035", + "7505-0125", + "7505-0200", + "7505-0500", + "7505-0700", + "9010-0003", + "9010-0006", + "9010-0010", + "9010-0012", + "9010-0020", + "9010-0022", + "9010-0033", + "9010-0040", + "9010-0044", + "9010-0060", + "9010-0080", + "9020-0010", + "9020-0015", + "9020-0016", + "9020-0022", + "9020-0035", + "9020-0050", + "9020-0072", + "9020-0160", + "9020-0180", + "9020-0200", + "9050-0075", + "9050-0150", + "9050-0450", + "9050-0750", + "9050-0850", + "9090-0800", + "9090-1000", + "9090-1200", + "9090-1300", + "9090-1400", + "9090-1500", + "9090-1600" ] }, { @@ -595,7 +687,50 @@ "6540-0131", "3302-0038", "4430-0400", - "7505-0100" + "7505-0100", + "6630-0003", + "6630-0005", + "6630-0007", + "6630-0010", + "6630-0013", + "7811-0025", + "7811-0030", + "7811-0040", + "7811-0045", + "7811-0070", + "7811-0120", + "7812-0005", + "7812-0007", + "7812-0009", + "7812-0018", + "7812-0020", + "7812-0035", + "7812-0070", + "7812-0100", + "7812-0150", + "7812-0200", + "9030-0016", + "9030-0018", + "9030-0025", + "9030-0035", + "9030-0045", + "9030-0050", + "9030-0080", + "9030-0100", + "9030-0240", + "9040-0010", + "9040-0030", + "9040-0050", + "9040-0060", + "9040-0100", + "9040-0120", + "9040-0200", + "9040-0250", + "9040-0300", + "9040-0380", + "9040-0680", + "9070-0015", + "9070-0020" ] }, { @@ -755,7 +890,205 @@ "7614-0300", "0250-0100", "5210-0010", - "5210-0019" + "5210-0019", + "0103-0016", + "0103-0019", + "0103-0023", + "0103-0027", + "0103-0032", + "0230-0004", + "0230-0006", + "0230-0007", + "0230-0008", + "0230-0010", + "0240-0007", + "0250-0080", + "0407-0054", + "0407-0092", + "0407-0107", + "0407-0161", + "0407-0206", + "0610-0150", + "0610-0200", + "0610-0240", + "2115-0024", + "2115-0031", + "2115-0036", + "2116-0031", + "2116-0036", + "2117-0022", + "2117-0027", + "2118-0010", + "2210-0145", + "3430-0300", + "3450-0642", + "4115-0100", + "4115-0150", + "4115-0200", + "4115-0300", + "4505-0015", + "4505-0026", + "4611-0075", + "5105-0050", + "5105-0100", + "5105-0150", + "5105-0200", + "5111-0040", + "5111-0050", + "5111-0060", + "5111-0076", + "5111-0091", + "5112-0001", + "5112-0002", + "5112-0003", + "5112-0004", + "5112-0005", + "5113-0001", + "5113-0002", + "5113-0003", + "5113-0004", + "5113-0005", + "5113-0006", + "5113-0007", + "5113-0008", + "5113-0009", + "5113-0010", + "5113-0011", + "5114-0001", + "5114-0002", + "5114-0003", + "5114-0004", + "5114-0005", + "5114-0006", + "5114-0007", + "5114-0008", + "5115-0030", + "5115-0055", + "5115-0075", + "5115-0095", + "5116-0001", + "5116-0002", + "5116-0003", + "5116-0004", + "5116-0005", + "5116-0006", + "5116-0007", + "5116-0008", + "5117-0001", + "5117-0002", + "5117-0003", + "5117-0004", + "5117-0005", + "5117-0006", + "5117-0007", + "5117-0008", + "5118-0001", + "5118-0002", + "5118-0003", + "5118-0004", + "5118-0005", + "5118-0006", + "5118-0007", + "5119-0625", + "5203-1800", + "5203-2200", + "5203-2700", + "5203-3500", + "5203-4500", + "5330-0074", + "5805-0002", + "5805-0003", + "5901-0006", + "5901-0010", + "5901-0016", + "6202-0200", + "6408-0015", + "6408-0020", + "6408-0030", + "6408-0040", + "6408-0050", + "6408-0085", + "6408-0100", + "6515-0090", + "6530-0030", + "6530-0040", + "6530-0045", + "6530-0060", + "6532-0220", + "6550-0130", + "7103-0010", + "7104-0010", + "7202-0008", + "7202-0010", + "7205-0125", + "7206-0020", + "7206-0070", + "7206-0100", + "7430-1100", + "7430-1300", + "7430-1500", + "7430-2300", + "7430-2500", + "7612-0300", + "7612-0400", + "7613-0075", + "7613-0150", + "7613-0300", + "7613-0400", + "7613-0600", + "7613-0900", + "7620-0002", + "7730-0080", + "7730-0100", + "7730-0125", + "7740-0100", + "7770-0004", + "7930-0001", + "7930-0002", + "7930-0003", + "7930-0005", + "7930-0007", + "7930-0010", + "7930-0015", + "7930-0020", + "7930-0025", + "7930-0030", + "7930-0040", + "7930-0050", + "7930-0075", + "7930-0100", + "7935-0180", + "7991-0050", + "7991-0100", + "7991-0500", + "7993-0020", + "8201-0100", + "8202-0100", + "8203-0019", + "8204-0100", + "8205-0100", + "9060-0060", + "9060-0100", + "9060-0200", + "9060-0300", + "9060-0500", + "9060-0600", + "9080-0050", + "9080-0080", + "9080-0100", + "9080-0120", + "9080-0150", + "9080-0200", + "9080-0300", + "9080-0500", + "9080-0700", + "9080-1000", + "9080-1100", + "9080-1400", + "9080-1500", + "9080-1750", + "9080-2000", + "9080-3000" ] } ], @@ -963,7 +1296,25 @@ "7110-0025", "7110-0013", "7120-0746", - "7101-0450" + "7101-0450", + "0260-0355", + "2107-0005", + "2330-0005", + "2330-0007", + "2402-0001", + "3302-0030", + "3302-0047", + "3302-0057", + "3801-0120", + "4304-0060", + "4304-0061", + "6330-0015", + "6330-0022", + "6330-0032", + "6330-0040", + "7205-0475", + "7205-0540", + "7360-0055" ] }, { @@ -1019,7 +1370,7 @@ "소유": "공용", "비고": "원문 운전경비 표에 주연료가 빈 기종(공기식 크롤러드릴 등 — 동력은 공기압축기) — 손료 + 조종원", "입력": [ - { "이름": "기계", "고르기": ["5401-0017"] }, + { "이름": "기계", "고르기": ["5401-0017", "4108-0060", "4108-0090", "4108-0120", "4108-0150", "4108-0180", "4108-0210", "5401-0015"] }, { "이름": "지역", "고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], diff --git a/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json b/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json index 5462af5b2..d637e805c 100644 --- a/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json +++ b/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json @@ -62,7 +62,7 @@ "갈래": "기계경비식" }, "변수": [ - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "0602-0060", "0602-0105", "1630-0080", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060"], "값": "1730-0015"}}, + {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "0602-0060", "0602-0105", "1630-0080", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060", "0211-0060", "0211-0080", "0211-0100", "0221-0040", "0221-0070", "0302-0025", "0302-0172", "0302-0229", "0302-0287", "0302-0350", "0302-0500", "0406-0054", "0406-0115", "0406-0161", "0406-0206", "0503-0036", "0602-0200", "0602-0240", "0602-0320", "1106-0015", "1206-0010", "1206-0014", "1209-0001", "1209-0002", "1209-0004", "1209-0006", "1209-0007", "1209-0008", "1209-0013", "1306-0044", "1306-0060", "1406-0025", "1506-0011", "1506-0012", "1506-0015", "1506-0019", "1506-0025", "1506-0030", "1506-0037", "2602-0015", "2602-0025", "2602-0035", "2602-0045", "2702-0040", "3530-0015", "3530-0036", "3601-0204", "3601-0302", "3701-0200", "3805-0120", "4504-0021", "4504-0028", "5405-0150", "5701-0010", "7106-0035", "7505-0125", "7505-0200", "7505-0500", "7505-0700", "9010-0003", "9010-0006", "9010-0010", "9010-0012", "9010-0020", "9010-0022", "9010-0033", "9010-0040", "9010-0044", "9010-0060", "9010-0080", "9020-0010", "9020-0015", "9020-0016", "9020-0022", "9020-0035", "9020-0050", "9020-0072", "9020-0160", "9020-0180", "9020-0200", "9050-0075", "9050-0150", "9050-0450", "9050-0750", "9050-0850", "9090-0800", "9090-1000", "9090-1200", "9090-1300", "9090-1400", "9090-1500", "9090-1600"], "값": "1730-0015"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, {"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}}, {"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}}, @@ -272,7 +272,7 @@ "갈래": "기계경비식" }, "변수": [ - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["4611-0350", "7812-0015", "4506-0200", "4506-0400", "1730-0015", "1630-0080", "1305-0007", "2106-0002", "2106-0003", "2106-0005", "7210-0485", "7360-0090", "6540-0131", "3302-0038", "4430-0400", "7505-0100"], "값": "4611-0350"}}, + {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["4611-0350", "7812-0015", "4506-0200", "4506-0400", "1730-0015", "1630-0080", "1305-0007", "2106-0002", "2106-0003", "2106-0005", "7210-0485", "7360-0090", "6540-0131", "3302-0038", "4430-0400", "7505-0100", "6630-0003", "6630-0005", "6630-0007", "6630-0010", "6630-0013", "7811-0025", "7811-0030", "7811-0040", "7811-0045", "7811-0070", "7811-0120", "7812-0005", "7812-0007", "7812-0009", "7812-0018", "7812-0020", "7812-0035", "7812-0070", "7812-0100", "7812-0150", "7812-0200", "9030-0016", "9030-0018", "9030-0025", "9030-0035", "9030-0045", "9030-0050", "9030-0080", "9030-0100", "9030-0240", "9040-0010", "9040-0030", "9040-0050", "9040-0060", "9040-0100", "9040-0120", "9040-0200", "9040-0250", "9040-0300", "9040-0380", "9040-0680", "9070-0015", "9070-0020"], "값": "4611-0350"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, {"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}}, {"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}}, @@ -306,7 +306,7 @@ "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", "변수": [ - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["7620-0003", "7621-0100", "7995-0050", "5210-0013", "5210-0027", "5220-0015", "7730-0150", "6516-0060", "6516-0120", "6516-0250", "6516-0300", "6105-0190", "6202-0060", "0230-0002", "6410-0080", "6410-0100", "6410-0120", "6410-0150", "6410-0200", "6901-0010", "6530-0090", "6530-0120", "6517-0100", "6517-0150", "6517-0200", "6517-0250", "6517-0300", "6510-0100", "6510-0150", "6510-0200", "6510-0250", "6510-0300", "6518-0100", "6518-0150", "6518-0200", "6518-0250", "6518-0300", "5501-0080", "7750-0016", "7750-0025", "7730-0050", "5401-0017", "5202-0127", "5202-0240", "5202-0300", "7611-0200", "7612-0200", "5204-0200", "5204-0300", "5204-0400", "5204-0500", "5204-0600", "5702-0095", "3430-0400", "7431-1300", "7740-0080", "7740-0150", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "2208-5008", "2208-5010", "2208-5012", "2208-5016", "2208-5020", "2105-0002", "2105-0003", "2105-0005", "2105-0010", "2105-0015", "2105-0018", "0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200", "6105-0390", "6202-0125", "7830-0081", "7950-0149", "7951-0066", "7952-0246", "7953-3400", "7954-1119", "7611-0300", "7431-1100", "7431-2300", "7431-2500", "7611-0400", "7611-0500", "7614-0300", "0250-0100", "5210-0010", "5210-0019"], "값": "7620-0003"}}, + {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["7620-0003", "7621-0100", "7995-0050", "5210-0013", "5210-0027", "5220-0015", "7730-0150", "6516-0060", "6516-0120", "6516-0250", "6516-0300", "6105-0190", "6202-0060", "0230-0002", "6410-0080", "6410-0100", "6410-0120", "6410-0150", "6410-0200", "6901-0010", "6530-0090", "6530-0120", "6517-0100", "6517-0150", "6517-0200", "6517-0250", "6517-0300", "6510-0100", "6510-0150", "6510-0200", "6510-0250", "6510-0300", "6518-0100", "6518-0150", "6518-0200", "6518-0250", "6518-0300", "5501-0080", "7750-0016", "7750-0025", "7730-0050", "5401-0017", "5202-0127", "5202-0240", "5202-0300", "7611-0200", "7612-0200", "5204-0200", "5204-0300", "5204-0400", "5204-0500", "5204-0600", "5702-0095", "3430-0400", "7431-1300", "7740-0080", "7740-0150", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "2208-5008", "2208-5010", "2208-5012", "2208-5016", "2208-5020", "2105-0002", "2105-0003", "2105-0005", "2105-0010", "2105-0015", "2105-0018", "0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200", "6105-0390", "6202-0125", "7830-0081", "7950-0149", "7951-0066", "7952-0246", "7953-3400", "7954-1119", "7611-0300", "7431-1100", "7431-2300", "7431-2500", "7611-0400", "7611-0500", "7614-0300", "0250-0100", "5210-0010", "5210-0019", "0103-0016", "0103-0019", "0103-0023", "0103-0027", "0103-0032", "0230-0004", "0230-0006", "0230-0007", "0230-0008", "0230-0010", "0240-0007", "0250-0080", "0407-0054", "0407-0092", "0407-0107", "0407-0161", "0407-0206", "0610-0150", "0610-0200", "0610-0240", "2115-0024", "2115-0031", "2115-0036", "2116-0031", "2116-0036", "2117-0022", "2117-0027", "2118-0010", "2210-0145", "3430-0300", "3450-0642", "4115-0100", "4115-0150", "4115-0200", "4115-0300", "4505-0015", "4505-0026", "4611-0075", "5105-0050", "5105-0100", "5105-0150", "5105-0200", "5111-0040", "5111-0050", "5111-0060", "5111-0076", "5111-0091", "5112-0001", "5112-0002", "5112-0003", "5112-0004", "5112-0005", "5113-0001", "5113-0002", "5113-0003", "5113-0004", "5113-0005", "5113-0006", "5113-0007", "5113-0008", "5113-0009", "5113-0010", "5113-0011", "5114-0001", "5114-0002", "5114-0003", "5114-0004", "5114-0005", "5114-0006", "5114-0007", "5114-0008", "5115-0030", "5115-0055", "5115-0075", "5115-0095", "5116-0001", "5116-0002", "5116-0003", "5116-0004", "5116-0005", "5116-0006", "5116-0007", "5116-0008", "5117-0001", "5117-0002", "5117-0003", "5117-0004", "5117-0005", "5117-0006", "5117-0007", "5117-0008", "5118-0001", "5118-0002", "5118-0003", "5118-0004", "5118-0005", "5118-0006", "5118-0007", "5119-0625", "5203-1800", "5203-2200", "5203-2700", "5203-3500", "5203-4500", "5330-0074", "5805-0002", "5805-0003", "5901-0006", "5901-0010", "5901-0016", "6202-0200", "6408-0015", "6408-0020", "6408-0030", "6408-0040", "6408-0050", "6408-0085", "6408-0100", "6515-0090", "6530-0030", "6530-0040", "6530-0045", "6530-0060", "6532-0220", "6550-0130", "7103-0010", "7104-0010", "7202-0008", "7202-0010", "7205-0125", "7206-0020", "7206-0070", "7206-0100", "7430-1100", "7430-1300", "7430-1500", "7430-2300", "7430-2500", "7612-0300", "7612-0400", "7613-0075", "7613-0150", "7613-0300", "7613-0400", "7613-0600", "7613-0900", "7620-0002", "7730-0080", "7730-0100", "7730-0125", "7740-0100", "7770-0004", "7930-0001", "7930-0002", "7930-0003", "7930-0005", "7930-0007", "7930-0010", "7930-0015", "7930-0020", "7930-0025", "7930-0030", "7930-0040", "7930-0050", "7930-0075", "7930-0100", "7935-0180", "7991-0050", "7991-0100", "7991-0500", "7993-0020", "8201-0100", "8202-0100", "8203-0019", "8204-0100", "8205-0100", "9060-0060", "9060-0100", "9060-0200", "9060-0300", "9060-0500", "9060-0600", "9080-0050", "9080-0080", "9080-0100", "9080-0120", "9080-0150", "9080-0200", "9080-0300", "9080-0500", "9080-0700", "9080-1000", "9080-1100", "9080-1400", "9080-1500", "9080-1750", "9080-2000", "9080-3000"], "값": "7620-0003"}}, {"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}}, {"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}}, {"열쇠": "k_기계손료", "기호": "k", "아래": "기계손료", "위": "", "이름": "EQ:{기계}.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "손료계수"}} @@ -367,7 +367,7 @@ "갈래": "기계경비식" }, "변수": [ - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["2106-0002", "2106-0003", "2106-0005", "6802-0040", "6802-0060", "6802-0100", "6802-0120", "6802-0135", "6802-0160", "6801-0010", "6803-0100", "6803-0120", "4430-0400", "3302-0038", "3801-0795", "7330-0010", "7954-1119", "7110-0025", "7110-0013", "7120-0746", "7101-0450"], "값": "2106-0002"}}, + {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["2106-0002", "2106-0003", "2106-0005", "6802-0040", "6802-0060", "6802-0100", "6802-0120", "6802-0135", "6802-0160", "6801-0010", "6803-0100", "6803-0120", "4430-0400", "3302-0038", "3801-0795", "7330-0010", "7954-1119", "7110-0025", "7110-0013", "7120-0746", "7101-0450", "0260-0355", "2107-0005", "2330-0005", "2330-0007", "2402-0001", "3302-0030", "3302-0047", "3302-0057", "3801-0120", "4304-0060", "4304-0061", "6330-0015", "6330-0022", "6330-0032", "6330-0040", "7205-0475", "7205-0540", "7360-0055"], "값": "2106-0002"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, {"열쇠": "조종원직종", "기호": "S", "아래": "조종원직종", "위": "", "이름": "조종원직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "선택", "목록": ["1048", "1049", "1050"], "값": "1048"}}, {"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}}, @@ -412,7 +412,7 @@ "갈래": "기계경비식" }, "변수": [ - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["5401-0017"], "값": "5401-0017"}}, + {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["5401-0017", "4108-0060", "4108-0090", "4108-0120", "4108-0150", "4108-0180", "4108-0210", "5401-0015"], "값": "5401-0017"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, {"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}}, {"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}}, From bed35e3bc36f7729c2b176c628ab78696993ee3d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 10 Nov 2026 14:47:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(814-1=203=C2=B74=C2=B76=C2=B77=C2=B78?= =?UTF-8?q?=C2=B79=C2=B710):=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=20=EB=B0=98=EC=98=81=20=E2=80=94=20=EC=84=B8?= =?UTF-8?q?=20=EC=A4=84=20=EB=AC=B6=EA=B8=B0=20=C2=B7=20=EA=BC=AC=EB=A6=AC?= =?UTF-8?q?=20=EB=97=80=20=EB=A7=9E=EB=8C=80=EA=B8=B0=20=C2=B7=20=EC=9C=A0?= =?UTF-8?q?=EA=B0=80=20=C2=B7=20=EC=9E=AC=EB=A3=8C=20=C2=B7=20=EA=B9=8C?= =?UTF-8?q?=EB=8B=AD=20=C2=B7=20=EA=B0=80=EB=B2=BC=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름으로 맞댐(`M02_Doc_Match_Note.bare` · 보이는 이름 그대로) · STmate 블록 규격도 가져온 칸에 - 4 기계 세 줄 = 기계 한 줄 — STmate X 코드 노 · 재 · 경 세 참조 줄(같은 수량 · 번지) 묶음(묶어서 셈 못 하는 세 줄만 그대로) · 문서 단계 묶기 넓힘(차례 아무렇게 · 시간당 경비 이음 없으면 엑셀 값 묶음 · 번지 같을 때) · 조합 다음 줄 = 세 줄 뒤 · 같은 기계 한 항목 - 6 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고(`M02_Doc_Match_Value.tool_cost`) — 확정 안 막음 - 7 못 이은 줄 까닭(`M02_San_Link.why` — 자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 없음 · 조합) = 문서 설명 · 이음 없는 자원 띠 · 연결 「없음」 띠 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」 - 8 유가 = 전국평균 줄 + 지역 옵션(`MO:경유:{S_지역}`) · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 · 엑셀 값 = 검산 칸 - 9 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치면 이음(지역 여럿 = 줄 지역 › 서울) - 10 조합 첫 줄 규격 = 그 기계 · 얼린 줄도 소계 식에(북부청임도 33) · 기계 줄 찾기 판정 늘 붙음 · 올린 직후 409(HP 보이기 전 산근 보관) · 엑셀 블록 「엑셀 읽기 확정」 글 · 전수 도우미 str() 뗌 · 줄 쌓기 이름에 규격이 들면 한 번만 - M02_San_Link 새 파일(San_Emit 700 줄 막음 — 요소 잇기 · 시간당 · 조합 · 까닭) - 시험: 새 test_814_1 · 812-2 · 810-4 거푸집 41 = 근접 → 확정(공구손료 비목 = 값 · 요구 744 C1) · 바꾸기 거절 견본 = 19 · 812-9 조합 첫 줄 규격 「0.7+브레이카」 → 「0.7」(칩 글 · 요구 738 흠 12) Co-Authored-By: Claude Opus 5.5 --- A00_Common/line_stack_out.ts | 6 +- M02_MasterTemplete/M02_Doc_FromNative.py | 3 + M02_MasterTemplete/M02_Doc_Import.py | 57 ++++ M02_MasterTemplete/M02_Doc_Import_Shape.py | 100 +++++-- M02_MasterTemplete/M02_Doc_Match.py | 1 + M02_MasterTemplete/M02_Doc_MatchSk.py | 4 +- M02_MasterTemplete/M02_Doc_Match_Note.py | 19 +- M02_MasterTemplete/M02_Doc_Match_Value.py | 13 + M02_MasterTemplete/M02_Doc_NativeEst.py | 29 +- M02_MasterTemplete/M02_Doc_NativeLayout.py | 6 +- M02_MasterTemplete/M02_Hopyo_Router.py | 10 +- M02_MasterTemplete/M02_Hopyo_Store.py | 11 +- .../M02_MasterTemplete_Hopyo_Imported.ts | 2 +- .../M02_MasterTemplete_Hopyo_ImportedPanel.ts | 5 +- .../M02_MasterTemplete_Sangeun_Side.ts | 2 +- M02_MasterTemplete/M02_San_Emit.py | 154 +++++++---- M02_MasterTemplete/M02_San_Link.py | 167 +++++++++++ M02_MasterTemplete/M02_Sangeun_FromHopyo.py | 11 +- M02_MasterTemplete/M02_Sangeun_Imported.py | 6 +- M02_MasterTemplete/logic/M02_Sangeun_Match.ts | 8 +- .../logic/M02_Sangeun_Origin.ts | 3 + resources/tester/helper_810_4_sweep.py | 5 +- resources/tester/test_810_4_match.py | 18 +- resources/tester/test_812_2_match_logic.py | 8 +- resources/tester/test_812_9_machine_link.py | 5 +- .../tester/test_814_1_import_decisions.py | 260 ++++++++++++++++++ 26 files changed, 786 insertions(+), 127 deletions(-) create mode 100644 M02_MasterTemplete/M02_San_Link.py create mode 100644 resources/tester/test_814_1_import_decisions.py diff --git a/A00_Common/line_stack_out.ts b/A00_Common/line_stack_out.ts index 46d0fceb0..86b8c9b0b 100644 --- a/A00_Common/line_stack_out.ts +++ b/A00_Common/line_stack_out.ts @@ -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)[] => diff --git a/M02_MasterTemplete/M02_Doc_FromNative.py b/M02_MasterTemplete/M02_Doc_FromNative.py index 207a061c3..cd8fdad89 100644 --- a/M02_MasterTemplete/M02_Doc_FromNative.py +++ b/M02_MasterTemplete/M02_Doc_FromNative.py @@ -121,6 +121,9 @@ def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]: marks = sum(1 for one in out["원본"]["글줄"] if one.get("표지")) sure = len(out["로직"]["호표"]) + len(out["로직"].get("덧줄") or []) + len(out["로직"]["중간"]) out["원본"] |= {"원파일": True, "갈래": "STmate", "확정": {"확정": sure, "표지": marks}} + out["원본"] |= ( + {"규격": blk["규격"]} if blk.get("규격") else {} + ) # 연결 찾기 근거 · 이름 꼬리(814-1) return _excel(saved, out, blk) diff --git a/M02_MasterTemplete/M02_Doc_Import.py b/M02_MasterTemplete/M02_Doc_Import.py index 9950f112c..3c9145ee7 100644 --- a/M02_MasterTemplete/M02_Doc_Import.py +++ b/M02_MasterTemplete/M02_Doc_Import.py @@ -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,59 @@ 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` 가 머리 `가져온.기계` 로 옮김 @@ -280,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["참조비목"] diff --git a/M02_MasterTemplete/M02_Doc_Import_Shape.py b/M02_MasterTemplete/M02_Doc_Import_Shape.py index 063a16d14..84e980981 100644 --- a/M02_MasterTemplete/M02_Doc_Import_Shape.py +++ b/M02_MasterTemplete/M02_Doc_Import_Shape.py @@ -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 @@ -166,11 +213,18 @@ def shape(doc: dict, meta: dict) -> dict: 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 diff --git a/M02_MasterTemplete/M02_Doc_Match.py b/M02_MasterTemplete/M02_Doc_Match.py index 0ef8c1f75..e33fae82e 100644 --- a/M02_MasterTemplete/M02_Doc_Match.py +++ b/M02_MasterTemplete/M02_Doc_Match.py @@ -618,6 +618,7 @@ 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} diff --git a/M02_MasterTemplete/M02_Doc_MatchSk.py b/M02_MasterTemplete/M02_Doc_MatchSk.py index 67bb3280e..fa5cf795a 100644 --- a/M02_MasterTemplete/M02_Doc_MatchSk.py +++ b/M02_MasterTemplete/M02_Doc_MatchSk.py @@ -57,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]: diff --git a/M02_MasterTemplete/M02_Doc_Match_Note.py b/M02_MasterTemplete/M02_Doc_Match_Note.py index 9b9ee23d7..158b6add2 100644 --- a/M02_MasterTemplete/M02_Doc_Match_Note.py +++ b/M02_MasterTemplete/M02_Doc_Match_Note.py @@ -230,10 +230,27 @@ def rate_pairs(left: list[dict], right: list[dict], take: Any) -> tuple[list[dic 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, "대상")): - diffs.append({"항": "비목", "글": f"율 줄 {name(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} diff --git a/M02_MasterTemplete/M02_Doc_Match_Value.py b/M02_MasterTemplete/M02_Doc_Match_Value.py index 3b8487e9e..c902b2151 100644 --- a/M02_MasterTemplete/M02_Doc_Match_Value.py +++ b/M02_MasterTemplete/M02_Doc_Match_Value.py @@ -27,6 +27,19 @@ 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.""" diff --git a/M02_MasterTemplete/M02_Doc_NativeEst.py b/M02_MasterTemplete/M02_Doc_NativeEst.py index 7034bedb1..a5dbaa797 100644 --- a/M02_MasterTemplete/M02_Doc_NativeEst.py +++ b/M02_MasterTemplete/M02_Doc_NativeEst.py @@ -299,7 +299,7 @@ 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] @@ -307,8 +307,8 @@ class Block: 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} @@ -325,8 +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} - link = em.hourly_text(parts[0]["키"]) if parts else None - one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": link, "자원": 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: # 노 · 재 · 경 글 = 펼침(맞댈 값은 한 줄 계 — 소계 글 · 없으면 첫 줄) @@ -362,6 +364,7 @@ class Block: 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) @@ -548,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, "결과 없는 세 줄") diff --git a/M02_MasterTemplete/M02_Doc_NativeLayout.py b/M02_MasterTemplete/M02_Doc_NativeLayout.py index b1d94b1f3..e33f940f8 100644 --- a/M02_MasterTemplete/M02_Doc_NativeLayout.py +++ b/M02_MasterTemplete/M02_Doc_NativeLayout.py @@ -85,6 +85,9 @@ def apply(doc: dict, meta: dict) -> None: 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] @@ -193,8 +196,9 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic cut = False else: one = mach.get(price_sym(money[val]) or "") if kind == "금액" else None - if one and one.get("조합"): + 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() diff --git a/M02_MasterTemplete/M02_Hopyo_Router.py b/M02_MasterTemplete/M02_Hopyo_Router.py index d29041de6..db2191f58 100644 --- a/M02_MasterTemplete/M02_Hopyo_Router.py +++ b/M02_MasterTemplete/M02_Hopyo_Router.py @@ -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") diff --git a/M02_MasterTemplete/M02_Hopyo_Store.py b/M02_MasterTemplete/M02_Hopyo_Store.py index 261347eb0..4d7a9856d 100644 --- a/M02_MasterTemplete/M02_Hopyo_Store.py +++ b/M02_MasterTemplete/M02_Hopyo_Store.py @@ -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]: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts index 443a3770b..b5d0d5a65 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts @@ -61,7 +61,7 @@ interface EditAnswer { 문서: ImportedDoc; 전?: Money4 | null; 후?: Money4 | null; - 이음없음?: { 자리: string; 이름: string }[]; + 이음없음?: { 자리: string; 이름: string; 까닭?: string }[]; /** qty 답(797-2) */ 줄?: QtyRow[]; 집계?: QtyTally; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts index 6b4409c90..48a17ca3e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts @@ -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; }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts index b492db6cb..66de8e82a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts @@ -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") }), diff --git a/M02_MasterTemplete/M02_San_Emit.py b/M02_MasterTemplete/M02_San_Emit.py index c462f8f08..ecf9ba6c5 100644 --- a/M02_MasterTemplete/M02_San_Emit.py +++ b/M02_MasterTemplete/M02_San_Emit.py @@ -28,6 +28,8 @@ 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()} @@ -175,61 +177,6 @@ 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, 표지 · 유가 · - 환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(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["규격"]) - 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: - return keys.pop(), None - if len(keys) > 1 and kind in ("유가", "환율"): - return None, f"{NOTE}{kind} 후보 여럿({len(keys)}) · 사람 확인" - return None, None - - -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())})" - - # ── 기계 부분(812-9 · 조합 기계 = 줄 둘) ───────────────────────────────── def machine_parts(src: dict) -> list[dict]: """기계 자원 → 부분 찾기(`M02_Machine_Find.find_all` · 조합 = 둘 이상) · 기계 아님 = [].""" @@ -240,6 +187,13 @@ def machine_parts(src: dict) -> list[dict]: 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} @@ -248,8 +202,9 @@ def part_line(main: dict, part: dict, names: "Names") -> dict: title = part["후보"][0]["이름"] if part["키"] else part["이름"] one = {"이름": names.item(str(title)), "규격": part["규격"], "단위": main.get("단위") or ""} one |= {"수량": main["수량"], "자원": res} - if main.get("종류") == "로직": - one |= {"종류": "로직", "값묶음": bundle(res), "이음": hourly_text(part["키"])} + if main.get("종류") == "로직": # 우리 조합 식(814-1 `조합식`)에 든 다음 줄 = 이음 없음 · 값 0 + link = None if res.get("조합식") else hourly_text(part["키"]) + one |= {"종류": "로직", "값묶음": bundle(res), "이음": link} else: one |= {"종류": main["종류"], "비목": main["비목"], "단가": Decimal(0), "이음": part["키"]} return one | ({"자리": main["자리"]} if main.get("자리") else {}) @@ -439,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%": @@ -463,11 +420,13 @@ class _Stm: res |= {"조합첫": True} if len(parts) > 1 else {} else: link, many = _m01(res["갈래"], res) if self.keys else (None, None) - one = resource(o.names.item(name), pr.get("단위"), cf, res, link) + one = first_spec(resource(o.names.item(name), pr.get("단위"), cf, res, link), parts) if parts and one["종류"] == "로직": - one["이음"] = hourly_text(link) + one["이음"] = combo_link(one, parts) or hourly_text(link) self.line(one, node, True, src) - bare = {k: v for k, v in node.items() if k != "번지"} + 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("규격")) @@ -552,11 +511,84 @@ 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["코드"]) diff --git a/M02_MasterTemplete/M02_San_Link.py b/M02_MasterTemplete/M02_San_Link.py new file mode 100644 index 000000000..843b27ced --- /dev/null +++ b/M02_MasterTemplete/M02_San_Link.py @@ -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)} diff --git a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py index 6ea2aedd0..9aed1d97d 100644 --- a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py +++ b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py @@ -94,10 +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 - row = hopyo_store.save(folder, name, fn.hopyo(native), who, fn.grid(native), 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} diff --git a/M02_MasterTemplete/M02_Sangeun_Imported.py b/M02_MasterTemplete/M02_Sangeun_Imported.py index 0ecc5c8e3..cda813d77 100644 --- a/M02_MasterTemplete/M02_Sangeun_Imported.py +++ b/M02_MasterTemplete/M02_Sangeun_Imported.py @@ -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("참조") ] diff --git a/M02_MasterTemplete/logic/M02_Sangeun_Match.ts b/M02_MasterTemplete/logic/M02_Sangeun_Match.ts index f655d15a2..c1f61d34b 100644 --- a/M02_MasterTemplete/logic/M02_Sangeun_Match.ts +++ b/M02_MasterTemplete/logic/M02_Sangeun_Match.ts @@ -22,6 +22,8 @@ export interface MatchFound { 금액?: { 가져온?: Record; 우리?: Record }; 후보수?: number; 가까움?: { 키?: string; 이름?: string; 다른?: { 항: string; 글: string }[] }; + /** 우리 정보로 셈(814-1) — 자원 줄 이음 수 · 형제 단가산출 부름뿐 = 조합 */ + 우리정보?: { 됨?: number; 줄?: number; 조합?: boolean }; } export type MatchAction = "찾기" | "바꾸기" | "되돌리기" | null; @@ -114,9 +116,13 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | 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, }; diff --git a/M02_MasterTemplete/logic/M02_Sangeun_Origin.ts b/M02_MasterTemplete/logic/M02_Sangeun_Origin.ts index 7e0995116..aaefade58 100644 --- a/M02_MasterTemplete/logic/M02_Sangeun_Origin.ts +++ b/M02_MasterTemplete/logic/M02_Sangeun_Origin.ts @@ -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.원파일 ? "원파일 확정" : "엑셀 읽기 확정", } : {}), }; diff --git a/resources/tester/helper_810_4_sweep.py b/resources/tester/helper_810_4_sweep.py index 69b3dcdb5..e11dff597 100644 --- a/resources/tester/helper_810_4_sweep.py +++ b/resources/tester/helper_810_4_sweep.py @@ -63,7 +63,10 @@ def file_docs(t, dim, dm, whole, kind: str, tag: str) -> dict[int, dict]: continue for v in doc["변수"]: if dm.md.var_id(v) == line["단가"] and (v.get("연결") or {}).get("갈래") == "숫자": - v["연결"] = {"갈래": "숫자", "값": {k: str(got[k]) for k in dm.md.COSTS}} + v["연결"] = { + "갈래": "숫자", + "값": {k: got[k] for k in dm.md.COSTS}, + } # 수 그대로(814-1) made[no] = doc ours[no] = dm._calc(doc, links, {}) diff --git a/resources/tester/test_810_4_match.py b/resources/tester/test_810_4_match.py index a50d6f3f6..90dc120fb 100644 --- a/resources/tester/test_810_4_match.py +++ b/resources/tester/test_810_4_match.py @@ -65,13 +65,17 @@ def test_sure_one_block() -> None: assert got["금액"]["가져온"] == got["금액"]["우리"] -def test_near_formwork_lists_differences() -> None: - got = judge(41) # 합판거푸집 4회 - assert got["판정"] == dm.NEAR and got["키"] == "GC000211" +def test_sure_formwork_tool_cost_is_value() -> None: + got = judge( + 41 + ) # 합판거푸집 4회 — 공구손료 재료비 ↔ 경비 = 값(814-1 · 요구 744 C1 · 옛 812-2 = 로직) + assert got["판정"] == dm.SURE and got["키"] == "GC000211" assert got["옵션"]["사용횟수"] == "4회" and got["옵션"]["유형"] == "보통" - kinds = {d["항"] for d in got["다른"]} - assert kinds == {"비목"} # 공구손료 1 % 재료비 ↔ 경비 = 로직(812-2) - assert [d["항"] for d in got["참고"]] == ["단가 차이"] # 대전 합판 단가 = 값(참고) + assert not got["다른"] + assert [d["항"] for d in got["참고"]] == [ + "단가 차이", + "공구손료 비목", + ] # 대전 합판 단가 · 비목 = 값 def test_req704_pipe_is_near_with_unit_rate_machine() -> None: @@ -155,7 +159,7 @@ def test_swap_only_when_sure_and_back(world) -> None: with store.at(folder): hp = fh.upload_native(folder, est.name, est.read_bytes(), t2.WHO)["키"] sure = fh.make(folder, hp, 33)["이름"] - near = fh.make(folder, hp, 41)["이름"] + near = fh.make(folder, hp, 19)["이름"] # 파형강관(근접 — 41 은 814-1 로 확정) old = store.read("sangeun", sure) money0 = _money(sure) found = msk.find(sure, old["판"]) diff --git a/resources/tester/test_812_2_match_logic.py b/resources/tester/test_812_2_match_logic.py index 36501fbec..e10095f46 100644 --- a/resources/tester/test_812_2_match_logic.py +++ b/resources/tester/test_812_2_match_logic.py @@ -122,10 +122,10 @@ def _judge(no: int) -> dict: def test_formwork_rate_ratio_pairs() -> None: - got = _judge(41) # 합판거푸집 4회 ↔ GC000211 - assert got["판정"] == dm.NEAR and got["키"] == "GC000211" - assert [d["항"] for d in got["다른"]] == ["비목"] and "1 %" in got["다른"][0]["글"] - assert [d["항"] for d in got["참고"]] == ["단가 차이"] + got = _judge(41) # 합판거푸집 4회 ↔ GC000211 — 공구손료 비목 차이 = 값(814-1 · 요구 744 C1) + assert got["판정"] == dm.SURE and got["키"] == "GC000211" and not got["다른"] + assert [d["항"] for d in got["참고"]] == ["단가 차이", "공구손료 비목"] + assert "1 %" not in got["참고"][1]["글"] and "재료비" in got["참고"][1]["글"] def test_req704_still_near() -> None: diff --git a/resources/tester/test_812_9_machine_link.py b/resources/tester/test_812_9_machine_link.py index 7e3940e75..854e71477 100644 --- a/resources/tester/test_812_9_machine_link.py +++ b/resources/tester/test_812_9_machine_link.py @@ -198,7 +198,10 @@ def test_combo_money_same() -> None: blk = fn.numbers(saved)[2] sk, doc, money = doc_and_money(fn.emit(saved, 2)) names = [(m["이름"], m.get("규격")) for m in doc["금액"]] - assert ("굴삭기(무한궤도)", "0.7+브레이카") in names and ("대형 브레이커", "0.7㎥") in names + assert ("굴삭기(무한궤도)", "0.7") in names and ( + "대형 브레이커", + "0.7㎥", + ) in names # 첫 줄 규격 = 그 기계(814-1) assert float(money["계"]) == pytest.approx(float(blk["저장"]["T"]), abs=1) found = {x["원래"]["이름"]: x for x in doc["가져온"]["기계"]} assert found["브레이카"]["조합"] is True and found["브레이카"]["찾기어"] == "대형 브레이커" diff --git a/resources/tester/test_814_1_import_decisions.py b/resources/tester/test_814_1_import_decisions.py new file mode 100644 index 000000000..b4911e398 --- /dev/null +++ b/resources/tester/test_814_1_import_decisions.py @@ -0,0 +1,260 @@ +# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 +"""814-1 지킴 — 불러오기 결정 열 가지(방향 장부 `_방향_불러오기.md` E 표 · 요구 742 ~ 750). + +① 파일 하나(같은 공사 붙이기 걷음) ② 확장자 파일 = 단가산출표 판(산출근거 머리) · 일위대가 없음 · +③ 엑셀 이름 규격 · 단위 꼬리 뗀 맞대기 ④ 기계 세 줄 = 한 줄(STC · EST · 엑셀) · 조합 다음 줄 기계마다 한 항목 · +⑤ 계산식 기계 목록(빠진 기계 더함 · 기본값 그대로) · 조합 = 우리 조합 식 ⑥ 공구손료 비목 = 참고 · +⑦ 못 이은 줄 까닭 · 「없음」 띠 ⑧ 유가 = 전국평균 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 · +⑨ 재료 지역 꼬리 뗀 정확 일치 ⑩ 가벼움(조합 첫 줄 규격 · 소계 검산 · 기계 찾기 판정 · 409 · 확정 글 · 전수 도우미). +원파일 · 실무 엑셀 없으면 그 시험만 skip(다른 PC). +""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +for p in (ROOT, HERE): + if str(p) not in sys.path: + sys.path.insert(0, str(p)) + +import test_797_1a_import_flags as practice # noqa: E402 +import test_810_2_native_doc as t # noqa: E402 + +from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402 +from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402 +from M02_MasterTemplete import M02_Doc_MatchSk as msk # noqa: E402 +from M02_MasterTemplete import M02_Doc_Match_Note as note # noqa: E402 +from M02_MasterTemplete import M02_Doc_NativeLayout as lay # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Store as hs # noqa: E402 +from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402 +from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402 +from M02_MasterTemplete import M02_San_Emit as em # noqa: E402 +from M02_MasterTemplete import M02_San_Link as link # noqa: E402 +from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402 +from M02_MasterTemplete import M02_Sangeun_Imported as imp # noqa: E402 +from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층 + +SCRIPT = ROOT / "resources/master_data/scripts/build_기계목록_빠진기계_814_1.py" +FAMILY = [f"GC0002{n}" for n in range(67, 77)] + + +def excel(tag: str) -> dict: + path = practice.FILES.get(tag) + if not path or not path.exists(): + pytest.skip(f"실무 엑셀 없음 {tag}") + from M02_MasterTemplete import M02_San_Xlsx as sx + + return fn.pack(sx.read(path.name, path.read_bytes()), path.name) + + +def doc_of(saved: dict, no: int) -> tuple[dict, dict, dict]: + out = fn.emit(saved, no) + sk, doc, got = t.doc_and_money(out) + return out, doc, got + + +def same_money(saved: dict, no: int, got: dict) -> bool: + want = fn.numbers(saved)[no].get("저장") or {} + return all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST[:3]) + + +# ── ① ② 파일 하나 · 단가산출표 판 ────────────────────────────────────────── +def test_one_file_and_list_sheet(world) -> None: + assert not hasattr(fn, "same") and not hasattr(fh, "_natives") # 붙이기 길 걷음 + stc = t._file(t.STC["봉화"]) + folder = world["personal"] + with store.at(folder): + got = fh.upload_native(folder, stc.name, stc.read_bytes(), t.WHO) + assert got["원파일"] and got["호표수"] == 0 and "붙음" not in got + grid = hs.grid(folder, got["키"]) + blocks = fn.numbers(t.native("STC", "봉화")) + assert grid["보기"] == {"단가산출표": "s1"} and len(grid["단산"]) == len(blocks) + head = grid["책"]["시트"][0]["칸"] + assert [head[f"{c}1"]["값"] for c in "ABCDEFGH"] == list(fn._LIST_HEAD) + first = next(iter(blocks.values())) + assert head["A2"]["값"] == first["코드"] and head["H2"]["값"] == first["저장"]["T"] + est = fn.hopyo(t.native("EST", "거창")) + assert est["호표"] == [] and len(est["단산"]) == len(fn.numbers(t.native("EST", "거창"))) + + +# ── ③ 꼬리 뗀 맞대기 ───────────────────────────────────────────────────── +def test_bare_name_tail() -> None: + doc = { + "이름": "구조물터파기(인력) 경질토사 / M3", + "결과단위": "원/M3", + "가져온": {"규격": "경질토사"}, + } + assert note.bare(doc)["이름"] == "구조물터파기(인력)" and doc["이름"].endswith("/ M3") + plain = {"이름": "측구터파기", "결과단위": "원/㎥", "가져온": {"규격": "토사"}} + assert note.bare(plain) is plain + + +# ── ④ 세 줄 묶기 · 조합 그룹 ───────────────────────────────────────────── +def _machine_split(doc: dict, out: dict) -> int: + """문서에 남은 기계 비목 낱 줄(같은 기계 · 같은 수량 · 비목 셋이 따로) 묶음 수.""" + names = {h["이름"]: h for h in out["로직"]["호표"]} + seen: dict[tuple, set] = {} + for m in doc["금액"]: + res = (names.get(m.get("이름")) or {}).get("자원") or {} + if res.get("갈래") == "기계" and m.get("비목") in ("노무비", "재료비", "경비"): + seen.setdefault((res.get("코드"), str(m.get("수량"))), set()).add(m["비목"]) + return sum(1 for v in seen.values() if len(v) == 3) + + +def test_three_lines_one_machine_stc() -> None: + saved = t.native("STC", "봉화") + out, doc, got = doc_of(saved, 55) + assert same_money(saved, 55, got) and _machine_split(doc, out) == 0 + crawl = next(m for m in doc["금액"] if str(m["이름"]).startswith("크롤러드릴")) + assert crawl["비목"] == "묶음" # 「노 무 비 · 재 료 비 · 경 비」 → 기계 한 줄 + assert not any(str(h["이름"]).replace(" ", "") == "노무비" for h in out["로직"]["호표"]) + + +def test_three_lines_excel_stmate_and_est_combo_group() -> None: + saved = excel("봉화") + out, doc, got = doc_of(saved, 55) + assert same_money(saved, 55, got) and _machine_split(doc, out) == 0 + est = t.native("EST", "main") + out, doc, got = doc_of(est, 360) + assert same_money(est, 360, got) + titles = [it["제목"] for it in lay.stack_of(doc)] + assert titles.count("덤프트럭 자동덮개시설 15톤용") == 1 # 다음 줄 셋 = 한 항목(엇갈림 없음) + + +# ── ⑤ 계산식 기계 목록 · 조합 식 ───────────────────────────────────────── +def _build(): + spec = importlib.util.spec_from_file_location("build_814_1", SCRIPT) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def test_machine_lists_complete_and_defaults_kept() -> None: + build = _build() + add, skipped = build.plan() + assert add == {} # 빠진 기계 모두 더함(다시 돌려도 같음) + assert all(miss for *_, miss in skipped) # 못 넣음 = 기계표 빈 값(지어 넣지 않음) + docs = json.loads(build.DOC.read_text("utf-8"))["줄"] + for d in docs: + if d["키"] in FAMILY: + var = next(v for v in d["변수"] if v["열쇠"] == "기계") + assert var["연결"]["값"] == var["연결"]["목록"][0] # 기본값 = 첫 기계 그대로 + whole = imp._whole() + eq = {r.get("원문번호"): k for k, r in whole.index["EQ"].items()} + for code in ("0230-0007", "0211-0060", "6630-0005", "0213-0060"): # 브레이커 · 굴착기(타이어) … + if code in eq: + assert mh.hourly(eq[code])["ok"], code + + +def test_combo_uses_our_combo_formula() -> None: + saved = excel("거창") + out, doc, got = doc_of(saved, 2) + assert same_money(saved, 2, got) + main = next(h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("조합첫")) + part = next(h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("조합")) + assert str(main["이음"]).startswith("로직(GC000271") and part["이음"] is None + assert "조합" in link.why(part) + assert main["규격"] != (main["자원"] or {}).get("규격") # 칩 글 = 그 기계 규격(조합 글 아님) + + +# ── ⑥ 공구손료 비목 = 참고 ─────────────────────────────────────────────── +def test_tool_rate_cost_is_value() -> None: + a = { + "갈래": "율", + "이름": "공구손료", + "율": "3", + "대상": {("인력", "노무비")}, + "비목": ["재료비"], + } + b = {**a, "비목": ["경비"]} + left, right = [a], [b] + + def take(test): + hits = [(x, y) for x in list(left) for y in list(right) if test(x, y)][:1] + for x, y in hits: + left.remove(x), right.remove(y) + return hits + + diffs, notes = note.rate_pairs(left, right, take) + assert not diffs and notes[0]["항"] == "공구손료 비목" + + +# ── ⑧ ⑨ 유가 · 전력 · 재료 ─────────────────────────────────────────────── +def test_oil_power_material_links() -> None: + oil, _ = link.m01("재료", {"이름": "경 유", "규격": "", "단위": "ℓ"}) + power, _ = link.m01("재료", {"이름": "전 력", "규격": "", "단위": "KWH"}) + whole = imp._whole() + assert whole.get(oil)["상세구분"] == "전국평균" + assert whole.get(power)["상세구분"] == "일반용(갑)Ⅰ 저압" + got = dim._oil(oil) + assert got["키"] == "MO:경유:{S_지역}" and got[dim.OPTION]["연결"]["값"] == "전국평균" + got = dim._oil(power) + assert got["계약종별"] == "S_전력계약종별" + assert got[dim.OPTION]["연결"]["값"] == "일반용(갑)Ⅰ 저압" + ply, _ = link.m01("재료", {"이름": "합판(내수)", "규격": "12t×1220×2440mm", "단위": "㎡"}) + nail, _ = link.m01("재료", {"이름": "철못", "규격": "N 25", "단위": "kg"}) + assert whole.get(ply)["이름"] == "합판(내수), 서울" and whole.get(nail)["규격"] == "N 25" + assert link.m01("재료", {"이름": "접착제", "규격": "", "단위": "kg"}) == (None, None) + + +def test_oil_doc_region_option() -> None: + saved = excel("2016사방") + out, doc, got = doc_of(saved, 326) + assert same_money(saved, 326, got) + region = next(v for v in doc["변수"] if v["열쇠"] == "지역") + assert region["연결"]["값"] == "전국평균" + oil = next(v for v in doc["변수"] if str((v.get("연결") or {}).get("키", "")).startswith("MO:")) + assert "엑셀" in oil["연결"] # 엑셀 값 = 검산용 + + +# ── ⑦ 까닭 · 띠 ────────────────────────────────────────────────────────── +def test_unlinked_reasons_and_tally(monkeypatch) -> None: + from M02_MasterTemplete import M02_Hopyo_Row as hrow + + monkeypatch.setattr(hrow, "m01_picks", lambda kind, line: []) # 우리 자재표에 후보 없음 + assert link.why({"자원": {"갈래": "재료", "이름": "없는자재글자", "규격": ""}}).startswith( + "자재 없음" + ) + assert ( + link.why({"자원": {"갈래": "재료", "이름": "구역화물 운임", "규격": ""}}) == "운임표 없음" + ) + gone = {"자원": {"갈래": "기계", "찾기": {"판정": "못 찾음", "까닭": "기종 모름"}}} + assert link.why(gone) == "별칭 대기" + sib = {"호표": [{"이름": "a", "참조": {"키": "SK-1"}, "자원": {"갈래": "단가산출"}}]} + assert link.tally(sib) == {"됨": 1, "줄": 1, "조합": True} + src = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Match.ts").read_text("utf-8") + assert "우리 기초 없음" in src and "우리 정보로 셈" in src + assert "우리정보" in Path(msk.__file__).read_text("utf-8") + + +# ── ⑩ 가벼움 ───────────────────────────────────────────────────────────── +def test_light_fixes(world) -> None: + saved = excel("북부청임도") + out = fn.emit(saved, 33) + chk = imp.check(t.sk_of(out), t.whole()) + assert chk["같음"] and all(x["같음"] for x in chk["줄"]) # 얼린 줄도 소계 식에 + src = {"이름": "진동 로울러(핸드가이드식)", "규격": "0.7Ton", "단위": "hr"} + link.m01("기계", src) + assert src["찾기"]["판정"] and src["찾기"]["이름"] # 기계 찾기 판정 붙음 + seen = [] + folder = world["personal"] + with store.at(folder): + + def first(key: str) -> None: # HP 를 목록에 보이기 전에 산근 보관(409 막음) + seen.append(any(r["키"] == key for r in hs.rows(folder))) + + hs.save(folder, "가.xlsx", {"호표": [], "단산": []}, t.WHO, None, "지문814", first) + assert seen == [False] + origin = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Origin.ts").read_text("utf-8") + assert "엑셀 읽기 확정" in origin + helper = (HERE / "helper_810_4_sweep.py").read_text("utf-8") + assert "str(got[k])" not in helper + one = em.first_spec({"규격": "0.7+브레이카"}, [{"규격": "0.7㎥"}, {"규격": "0.7"}]) + assert one["규격"] == "0.7㎥" From 0c2a0b197196b1fe89d6e256aee68d62bfc5e0a6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 10 Nov 2026 15:54:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?knowledge(814-1):=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20=EA=B2=B0=EC=A0=95=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EC=A0=84=ED=9B=84=20=EC=A0=84=EC=88=98=20=EB=B3=B4=EA=B3=A0=20?= =?UTF-8?q?=E2=80=94=20=EA=B8=B0=EA=B3=84=20=EC=9D=B4=EC=9D=8C=2095.7=20%?= =?UTF-8?q?=20=C2=B7=20=EC=9E=AC=EB=A3=8C=2087.6=20%=20=C2=B7=20=EC=9C=A0?= =?UTF-8?q?=EA=B0=80=20100=20%=20=C2=B7=20=EC=9A=B0=EB=A6=AC=20=EA=B0=92?= =?UTF-8?q?=2085.5=20%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 금액 원파일 2,220 · 엑셀 1,021 = 100 % 그대로 · 못 읽음 0 · 시간당 경비 없는 기계 349 줄 → 0 · 조합 항목 겹침 83 → 0 - [우리 단가로] 블록 전부 우리 값 원파일 71.9 → 85.5 % · 엑셀 74.6 → 79.7 % · 0 원 블록 236 · 47 → 115 · 4 - 연결 확정(보조) 원파일 339 → 463 · 엑셀 205 → 211 · 남은 못 이음 까닭별 · 남은 흠 다섯 - 목록 _목록_814-1_정합성.xlsx(요약 전후 · 연결 · 못 이은 줄 · 0 원 줄) Co-Authored-By: Claude Opus 5.5 --- docs/raw/요구_1104/_목록_814-1_정합성.xlsx | Bin 0 -> 43376 bytes .../raw/요구_1104/_보고_814-1_불러오기반영.md | 79 ++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docs/raw/요구_1104/_목록_814-1_정합성.xlsx create mode 100644 docs/raw/요구_1104/_보고_814-1_불러오기반영.md diff --git a/docs/raw/요구_1104/_목록_814-1_정합성.xlsx b/docs/raw/요구_1104/_목록_814-1_정합성.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..212f446936ac542eb4c71d93658ce2e0b1682183 GIT binary patch literal 43376 zcmZ^Jb95!)vu!f5C$?=(FtKeI9H4!t>LgCJ+mjnlUoUyQL@VmEiZ|pkH7E@F^`k2N5sT*6!K&dbC zs5nS7oLIl`d^xr_IgW(}fxwhUBRawlQ4-HQQ4PvR?t!hgf- zxvFVuC+(85H~b}05YEw*BZ7lbPoaLjH|x)$JBZs5dhpS^KTF5uNVtA|zUE=f?O?CB zh-cB!Qo$Ov$9!;BTd9&O-;*!`OuQJpMY*BjG=F60PNJ&fan{CdRMzmKON zTqMZ0XYzp4oxDwniXQ_wJ?<_U3;`iwNw@C$tBmVQ9W9JoJL)WC$bwARrM1_8VT)yS zn=~yz`Z;|@QBnFg0wSTM-O{7)h5-8`n40f-J1_vBZt_eRS!}1!hzfHSP3> z8V?CspV7xvqD=}xQPl-0+QJ%dU#th;*S7dfrcb!=S^RC8a5aehrtC@& zi2s9l0P)~P?Z-oE$7m6&XSNU5lvR?SMI~tIMMvgz;=2>zAh$GeSypJuzLO5-=8aU# zAe7RU8`Y5u&rHaB`tdQon4D$$>IL>pj>n8pEu~Ipdio$I%8|5V^HIa+QC{4Zbh-R* z1Icod7@2792z~njGG~({0t+fXbaqIZr2^i!A{geB{E+9yp(^AZRG*e^=#lN{u%}A9 z3|{qo4av2j=kH=Tds$9LPP5i;P{m@ikE)#;g{d0ops85fKPuSh2AABE|K3z*@!+GP zOeVoW3WlVTQJ|Fn^p%xB{iH2k`^=gbxK3gudzFfhwP+wMb*7jEu;TQJ)#AE@n$Pig zenPxtvBBcS623%=iV<6{KdFTM_z$uZ(~N7UHIjfbY=L9hM_ys8gXP zzzg(ic8#O2@;Tt|Pd^5q>Ef%ld*1xH77kJ$v)=hgtt~NIt%0{1=C$P?3{*zyj*}z9 zDz%B!D?fApo`0D0<=kAnu95}SX8fWj#fX#kZxAGFeUBWwrWt?%7;BNkjH}F>a73Ww z2;thEC_hLgFCW&nQMK}D9h}tWJM+;^zwqcCH%`K_3b!~)pEf6TH&Jj(-Pcdp%Mpyj zbDQxb;5WPF;j_Ix>bM?|7BeZQ3b*12b%WWoXK!$E=A^~9!Okni+4aAi47%g&{agia z^GQ>;IbNBaeLL<|tSh|YNx>fuyDcET%nwPM?iUXA@X|{&OCD1Afo5Y}MMfvqqTjlF zLx;9wtWTc@650T207yN5zp2xsUbkb%bktA2;`3Z46L1JLC94K5S#6hBh zP9}|tTz?BNF2Vbq_6pZ^3*SbKK2^>4(@&HxFhovwjvNIoq7EK{7}Ih8>MGIFEo%9d z&fg|Tg~fOgb4)nAmzUL1ctwT$u4at_E_i)UWwHaOn?hf@-_tgmp7Lv5cY(JNlUMAn zFnx!hA=D|;v$apO>PusZ;Du%Tr~NwD7MSi4DxGkVZ?8{8#BbzE!|wK4`4{pm?nbX0 zf1=FyrS+o6%^i5}g-Xq0V_IZ>X((^P1W2POt@|H(v1o?XCH2)qYs~N-zcudQ^G!Lo zLq#)iSQjI!en!LE2`&m8@~9~_WlnEqFFcTg<=ObWLa6H-h}pe}>;e7n z1%%!X+Vl`uKq65dN2jBFW7BSQf>y#YCSBWPcWP+8xBQac1XyTu2U&v@4- zBKH%Q!`^^ghoorm>(UJUSb+}*)VDhL6;FzNN%G|IO6nxF638~`zQJ)t zieAp{SlYYvW;2n%w(^Pw=bMKEqG9FSq#%}jMRexpEAhjK1PKM%olzu&V(oFNWNSa$ zim!x0q|41HlO$TpJuz>kOzZ;^ybP5R!xoe^7Z>2_hn%?JcypJAfr}$ic}FE1h>w$& zhcv+jN3gO_4Z9eY0%?hNZXs;?3Ejhq2W`Fsvk4>f;4+6$6dI2TCk-}mkcV>w%0TKmKQ$Mx}O-y;?_Bk z&TRACLlUHQolVy`VEW21iK91Hn%&_tUvymOhR`h>m=JG^=pAba_P|_MN;dNIRii{sfI2v8>qK z_My&GyG*T4*asSyXQoVZ)=e9#mmx20Rv&C`kfhRi25e23uXdzYdt6(5;6Q&3jX zOfmnKbMm|hBcXb8xs7JizKU*+ss6o{FEF$YwO~q zO}x_9SbT82$$~nE+O-_0!9EOvM#Li>*(w9si%&4PAF*1C&@GA$ZBadQ&F5lK40IS6HM?6dF7X5q0mA?)bBk9kzTAKf=X4^IiirL*Bx3|_2eKocE9wE{#br2xE^ zNWGx920MrIogl&$L3}oz?s^}RUoEaEnM6j0<^DKBfJ117ycQzt2aowf%Zd%k9|?L` z4`p_je(@=|BPDbi5E-z0t1y94UaHJhgq7bK?^IN`m_o#S>F)}ZRWoU*hU(S}v5FXR z*9PqeAq;|-&?6!0*8zIYGZtwJB9mqwd!b&XyWxWZh|a$^+fOFmVQLoQCZ@OY;1i1d zM`wGuwSMF0iRPA1v36<&6Bsb0(qG;@_JfgwhkhfXc%eg?5UNIs`TdJ-hYi2A5aH@J z)Rg_W-%V~r3RD}lsU`E9c9|W=AjLDn-x0NFx~s)#a;Dz_fB*WLZ1sY-=ac?=A`~#> zqY>EIsgB*yAqj@9+_n=7VI1)edNSB2Z}ZCar+Z8i>+erB?`Lgc-ys1cj?_MB3Oy{V z{_y}O)8-Y9_l1Q_;^QChM4?-jKKmhRAKKU)Bi$ITEm|PL;Q<$A4RJPO49aG2bTNSx zzQd0^U>f!A7dyMbWj%o95o>?L_Tb>sA>DfAc`ba9?f^%oo|aiBnz0(x7rbE972*B- zoOP6AW^wG1Ss8Cb`#}GT-L~$~-RroR_v{(37s-0o2+K0BcAl5ZMqv6+f&2TPK?){% zS1$r5?#%BrB&864a;;pLh!>LmDg%)V1IVbra-mfP7)8o#BlH`?iDsqkMa=6pcxyZc zVu=tVtf)g{)p!1a@CYRHl!YxkUSIlvLqUDcvMwJq@f0Iak1A0<*sJWkrPmsi*5)@O z9s$M;N4PF}FZZ6pKjr`}>K{x9U0#jz3SxKqKOr{zsM21(K?^_{5IxNXEBY(;+Bvmx zokaeYt#D;u&2NNyCN>k;dv>q&Vzb_^G?*q%MTECqE>9I%#vO=+)tzReL~<|8?+-*v zzF39`Qm2hG4!TmonU@Hf66g8G8qrmzfr0`H@7Q7`s(Sf29dKZ}P)Zr(|H9$eZde??$zQJap&qj52H`G6?SL`yZ_m-@7C~B6% zbnnSHTUO}o&_OE8<7a6JH^sCzeSfgltim+<4CE17gczNixYGopjG+l2cA@IF%q^)S zS>=~Ioxcsps?INvd1WaYCQ4yiAa;+Cw8fSnL^8ZpA zS^f=J*&15%xZ-G^w1pRuI>nFYo<73A7mBLqLK_wHCWXfW!QeF9yB4;RRH#tmAZB;K z=XdnO5K3JBij=JVp2taURJv2XJxc#T;$^j3!t9jOgsFBr+URkbn)n5I+_)g)sj2Af ztS3u0FWlEyGmZHCadKlJ8eQzOR+RugR+OS-#6_p9{CRIJ6)tN}%(k1#g!r zJ#L1n#e#WWbKX0>bEF@KU%#>%p}lj4n#xep8q>OF9h~q!oOx#e%l;;<^F0w``?sxC zp#T!5C&U>=#2Z!$e*d4yADgR1wCTE!8)P6GhS^EF_P+tjiI+InI0NxiU_}}YpoUhE zriHfba?x_SKLh$NIvt@S@t!wnUfAYGX?F$6ven_mjiK8b?1fd4>d(N3{q8s3UabE_PWCFaoh4P>j?-&&hh)W@Pxy96`e#3kCpsZM zpSK0SgZSf3sw#+ZU9`6uXYj9UrE)Mv1QC8AaR3Tg0fHIhvrPs4V|#jBkS#pFqj0X< zQS$1f@AD+!l0j!F4ML~IQZG$MU)eq0wfgA<23!wjO}&4;!sCrB_uBmBr-@;eC$*hR zu$B&O*N1bO5i&BmKt~8KEk+n2;%31ERLKNm{X!CfW70S~)0zvV4?Z7XikRiY-=@RW#+3d>7f?~4*j zV(IvkL~dPgo7b;0xLXr;$}QNV_A#$8IYqC13kVFZ!c_p=?-ulfs?)`tD51Nae+!Hu zU(9Hl;DTX-P!?46=^z(-L9Ik2{k2vi77t-3xC*hFH z5)Fu4WcueH_;QiPW+}O|^Nb6Uzf5zo?rNk~zZ_f5`wtxK8$iKYRnWO$-i#OdSqBG) z*5oNi_($nL)QWO;tJMq5*Z{C1)3^6TaIk%oB`KMzI>lPUOOF)gIqRz8ldy&lr}5WO z5i8VWwx}5HivsC=(DeUt8Xx~i#+f3 zdoDyfIi7!&J3az@@X(3NCzkC?Yj58?sqG~xZzZ~S0LywFyDUCW#x3Ph_!-U0_95vW zw5d>_0O&GyB&{@bM^ktFnjAS#68*&TY{ibGM^L>>cnKWbGq>uXY%B6}F0%Twx@DU^ z3I~gCKEH@@S0la9DGXiN#0nJ6HbfLY;*3R7XeMtU8rq;pX9CUBb)*YM?CuX$2W&NH z)lb;!9_vt%4}DF)>6ji6h`sh=I_yNY%9iDA_^>!8X4vJ7TN4mV*QU97|8SrX>yT@~a0Z?6ZV#s=9@@U*ztg=EC= z4Cl{6GxTHOV81CWm+M58vjwVF6t0#X-%#cGDL>AE2utJpwXZr7oo14!MiPjDnz;|V z5TR5AQ7;_Wk>0JtGrKnwQ@vLX!}4->mS~hQG7uyvs;G17xA|3Fz#84xNLS@ad&%vx z1vz_u5f<1CC7Y}p#`*l_3+2#t&wrl?;e^E@%D#bsq=EmhWsmh=*>h3Zl%H2X`%<@k zwFW@F3Cyt}N6kW^ep4irT)k`#X?APIEF~t(3!JD66(pOe+lvayGn~6dHtW+{*!n=q znYS^)aX;lf&e z=%W<@n>5Twy@{-%;~yi<8e@Q@Z7-{H^N`JWF&x3oGT!k0)}&(QB3uGZG-{+;@mOMz zHGr)_D3!`j5ey{AaZLFS-svlbQ@Hpz#7R6CcPV*T(O$9W+2=rpJ}Yfhwc&M@N*vm~ zh>hlfghAgiLUw6$w9L-ILzqfCy6C}yr_2}G3E_El8eg{ABW>kCC4_|-3ArZwvUT^! zsr*S<>3Q|wFa-y~rjg5@K|_e{0ECoK{^4F+N-LBHTv97D=QH}(bImv&kG>c~%y*|J z;sX1xZAa_@B~g0Sh0ET^ex`Ql)ImkB8O!h;QxE%vR_QOk_x2y=O`=x<8RTaPoEyl3 za49cYufHW`uW)DA`|T+L4fvxNa7FO0!uxGTEGr;adyfN&c@i_XB#e+B3&*$O8dt-6 zya!%4gjZtSrVH#7!>(n()#H59#81c10I%BTjr(@=C|?$TzZ*zT7M$z%NQ&H*a@q>h zq{@1`6vEM^iB$o#BQFpIH}hhHpiwqizZDMRh`{s4xzmDVJm2Ga3EiccyDC_cUSNLi z&OE`~a}k7bWy^qD5XVj$Q-~$tj2)mh?a=Vq#JN9#^J!F4^$&@*fbwzg`Gh5f`#aRG zB71GDp%FIQYbkyaMgbo8oU(qghb|7=JfBI1n)6-yMf56l67aL&3L$Ue;p+Mu{QX7Wwo4o3bVN2z(}}mx|80elb!Z->R{s?X+wXtucQ|Wy zh9(2m?eo8@`^SH&`$BqM0P9OV#+#rd{3-#m#K@WuvcUR{s=lkamuy*Q%0(DFTb1iP zlqS)2r9CFRbyDyef_p_TN&735ce+W8<6imlY2)r)mjSp}I;7r*QXJ5E*F`j8NlrY* z+D>pJwcyr=tX2Ygj)wJw4;qL0P&ZMYs4m=HnwdTkbjfX-x0+Hh>9lTG+2;htH7(hC z`iAJoFU_kqY8m3HtYIHXo)6n@lblVWQ?zG-4GD5eEx+|(@x7gnC=27ynvs#|k_u|Y zezG*}ZZ=yl(|A-hk$cXxCHB1tZw9Kw?%#d-?82N-ptdLDR65WHM?21NUrTBDSF~J( zxQ08IZFQ6WtR;!vHttk;8v3i|%o>#3Xhq87?XF;$q4zOY*&N#YC(HmK-6bLegf}6` z4W5vE$@#mr+Ch*sW}W)OS3@cG%)ho>@qyZARsBJjmO4qr7n@@2ByVd+_kdLS6wd#n zv*h6nVU>5rbR+{q?<;3fac?#@@y~vuq^Q+~ss*fabgx_MOc%BKH^PFo$EL_Ri(yF5MnzS#s$b)s{-K`O| zQwMUSFKU#dS2TFk*5%n}uHG>q^rK738@9-37xy!(_wG`n36qFr6qIIE{(pX3(Sx5w zKsEHOD={20lS%HIip3W@rDv%~{_%oGI&|D)cRB52N{W4?28I2L3a#89aa=}>#QeVS z)n?q0nY9ZVg(DnO)J@#NzKD!8CD~jVS3}M(G7{aLxK(;!CH}%3{qV*R<9oceXZF&! zE6M>KE)Hi8!9tp7-}bVTgY;Y9hu!>quDiCoz4oH@qJ<;v2%`e>oa}F+Ikr7c zLiw@2I@Gs6c9G;h&&pf2y^jYfubQ{smIntpwmzN+bKTz#t`NJ>y6^Y&-up9qJ}1j> zdN`l^3Ev;1^>W=W2Fm^3ZIG!xZ+3LE^}5>J_p+b1cwXerHc`JE0mX#D+x!_JT@I$5 zlV^50&zoLjxmt(2?{}k#cFAAYZarN0N(#-V1M8H&9u;)AVmB>*+&8!3E!zxM=r%8U z9VU7im(4O2e!hyIl>72|<~#g7H(TMU^4%Yi9DXjR9HF{2ICqt(U$;X&-9D#;a-Wxg z@XbQ0Qd#fAXl1+iYpAQ7cb&7%TYJCfK)n<{Dz{(O&qv&c!}n|DZjpXpruC)D{1f~= zS^Sz`y}in!xMO}!*Qp)hgvy4Eomt!N6_TAY;pejnrg!T0ch$+~O9--iOysU1(yNnf zvU`;7D;(?%9bmG1(3kt`582{KKf(*&1$Cx4)GEURA<}rcW(l&PrSN<&S`iU!baODc zm=Z6>S&z1Tu|9Toddktt)E@1fj@L_H8k%OIkQzFW#0l*#thcvUaZXzmq7Cn|huh^} zDAU~kq$3dm+?;}hdWDWV0E`yGEx6%2ri`yiZyA+DI$o4z`$S!V_}nb#*=%Epl}#B{v?)0GdAGtkBT^q zWv@zCL@*733C9jj!zRdLY;cbKbvhKqPH9>`Mwf0NHmIHU-;YlbG{oStu4SdUEa)}p zhhU>CTSPPPPS|Z3n5qP(8weSS5u1iU9gkOa3|F1~$B|C*>bZy5;O(N|=gr0JZ3F~s zMehU?F>~Q}*8addlcjZ|B_|^w=!P6mxVcfUA&^e0-O0+H;mWI_NOnqHpI~*j$99RqlFr*o;Q&#CkYE@Xj9@Gb@=k-` z-uv*M_Wi-@sI{Aa8R}MX#ppQ5)$nEk@2CMG5NSB#WY%ehwpce%rDceoz@6{-<5||CkWc?|1Cb&zYJZ$HG&@UW2ox@^?;8Z=PbSi?BD^a*C(KiT?U1_1MFqJDx1*v$x46yxkQ+0cj{@PszkZ0kf z%qZ6HJdV_=@2Yo16_}RavGO3a!(qmRL2o9*iM=Ds6-xumb?m)FR%(gPr$|KZl*Flc zG26MZV#3}x=;6gWUqqD=P=8utZ4l_iK%x6~tv>Ao42tx6KVBZ7UJ`1g`wZx)I_aNo zS<(y>ux==SKCr|WruJUM8*32%tgNah_`;IH+7v|)7m2zofF%9IDlMRJyr|mS&=N&w zGq^w?Ttf9jB~KvORDXZ1Z5_vndQQw5%NS8y+If^2M860YI@O6FE^GhowTN=vqq|Tyl%W&tCW_4F&V3J5yVG(Gmy3_(#iI!|?|ZY-7o2_b4*ZIW(rvej-=GV08^3 zlz>M$VRGE4Yz)Ng0b@j=JIrTy)fjet(_q(Tl(2MYTgQ$GYu&hjCyeJ{F!>#S22FZQ zEQZcKU#-JG`zx%K2NL5Yn$EF>$y;QrwFHC0wUMcncdtStZR?zrXDQL44Ht83c5(d0 zEVFucP3`l>hMuLF;o~|rQqvf1wENwNmhjW>CHio0*elM?ZxN>&@%F3%NmNx+c^8En z7ITBZLN!R*?$ICr483s-w3+af@3lV-#oXfsT?U47dE-Nh=Yh7qP!D*oq@Jqp6|wIn zoSMX{H9%#qwq5LbFyJDJA9LKs<{n_$p7Z!By_Y`5>b|YUHpZT4C=i9)Jm31d+FIR@ z-kOVY7hngT_?H!VTytDm`rm%?$aR&QhvSRF*~Zd!pr89#MD1r7TRq4mw3tC*^E1x^ zS!+Ox=6@Cy0$E*?y`DH>FVWT3`!V3NI(^rl(cga!5Q$_B4ngfL^peFO0s$+;*BNQw2lG@AaSWmSy5mguzIAcHhE6e(X=J?Rv~o<^*DjucfoP5yu)%Cyc)I0mTqvt$b<`k;sI`(Pb%;4@|!BBfD+&`f;Sqbxco!#{IZN` zFA%Fb*}Zh~KUud);ZLFivo^wJ9?PN#+eE++^Zs5qk8;)cQg|)M^PpK?xZHPv>_9>) zwm1*WX!}1IbuDJI`;qaLCI5=kDHU|hf%=)O_{DaPiGi*#J?ndOS$)~~LV6>}+oWG! z2+@Cm>>@7qv0SIGp(4O54mZ@%(lMV?iXdIePUq5S(8^nIsiv@e#L-f(P;K3spwl`( zc1eE$&uF#V{>B)UgvGDN;F`IqInBQ;?;^MAo{!t}07w7VqrJxYv|UMjIe4A)?-<@h z6LX-()#9=qLo32vG22NeXVlC4QU5toJ7Pb7ylY!at*h_81-mw_T(!^JU9YFFk2G9@ zRF@+ae8Aw~_h? zeP7+DlsHu@5M-_tJJJD$C2dhzfhyP%y+ccmY)rgiOLhQ@Ax?7yap?fG-XOOlhnx zz)_9EkXJnEatVf1ED0@1K@*9?31}dTFTNZEG#D#uV5+VcVN}7}EIr#tda!WV&#%fp_KfE5PpTsr*%;1sDS<5CH;=RT0$0cO!*I2fZp*u<2Rmknbq+@MvsWIUcWC5kvqg&va(J7bv_*@EvC>Pc(ZD| z_#Umf!_b+-OzCvli+~?@U!#k?_sUL656^Z2*9}`!!=qmh0T5?9YcChHFQ-lCnSu^* z+4Q+f{4}jkOnYGx2}<8a+om7q(_w)wfB&n}K$lbJE4p+5={cQ2dl9lx0!YazpT6A7 zg&t@858yf)39Saih2YT}kVDo+z(@mN+eIn~GJ!be+m&~#&})x*1$HY5i|HBM4KxUF zuL;hW%y`I>e}V-QoO16o{l@0CoOfY7 zy3?07X{@LuyXLX0X|)dT$@@X|c6@?fXDmGa@;9QG+fVCq*+uDP#V%k^K+mpF7+|}w zfdDUdUSw25r=a*owBCV?u=gy z%w|CejEx2$Ho4iQKM;e1NFZ$4%CBXqyg%pi1GwZXovD;h;$YGN4RkvH8N@*Sm%$jx zeobW*HzVs7+ke9IVBiiTGO8`uhhZx9f2=BHwx`q-S^@dgtd**6=Ho&47NkeCE^EWZ zh+0wX_ehS{oR6X3ELT6_@&xv48)5itZXEw| zpRx`5pU9itSTUUcFF1rv38{sQ_V}&;hyC^ckB!rgT(kr{RBBsv1rW3MDp;y*kN-T+ z#Gz~D_`wdG3iY-pLe{1{vCOnPha+3WX#-(s@TuWL;GBOAgzc}yelA4b<97im8K}@L z;y-(<&m(7cBkuG1fsCpaNn*I-G!VbC5PpxJ{T~y#B7Uprdi`#$5&mu)vLgmzCPOj` zIxNwFYXDxjtdGR@Y$%Yws#D?DhD&6)qgZy=ZXcR`#}*;M&#`phEe1X>n`FV{xZS%uiubZf#&8JnR0hmk&qw=L?xn2`)LFhH19DIT zIeEhX%Bo)3YkQR#(#~?Q6YhVb(yl6=gi_`oXt>iIYUOpnOIzjlD6AJ4T02yJyvY3# zJ4@l$7_e1{%76T>09?>9m(zS^8lqf1|o0&tV#+CDtD>o6BcP^NhQlmix8v^M~e z)+#1p(`|9np5H5VSb~3tVjb_r1VjX{1vd_)P0YXcd{=W z6b=c-;XFyTr_z$cLQtYOM3pJ zXtaWHYqH)7%_Z8C*?QnDj@IYNg!&Bs6s#`M z|HV@R#N#5VQY8(r)L|lGmNqu!@u*~D)ol(Hu! zKD&{kFBvLLDNWGr?AU{GBQDECp)q$WE~`kEUrEyYTcm_Eu@?wvWrGP@urSSU!97JI zld!2I(6LV;o+Q5#t@khBJ<udc*RGL?K?m0%H5Kwj zAFTE_+8I4~>HP;b=f0bXink=-RGNt>|12C(XBLLf)$2yYwNV@uV~E0~!l?^D<40+K zMTVCEypnL8cN>IG2}fTum^h;)&P=K%ibPO^YqP;Xf;-1oNSbnwI}TE}&z|BG^tb+w zaicEiPy3C?>if?{+kadLA>(sFa61!hD!Zq$a79Q0j@EvXD}hTL&?o=_M2a}C7gO38%nNh=rkH#AENsf^gOK)=;Go2Bv?2wXaXHy98q`^vktxb9EOLUU{lg z&O-3!^NezmR8T|AcitpO_Iny5ztdFJdy-1p!`9sB;`jBpi~Xl29!+xb5W}JO($5swkFN?OoUBaaYcwE|a z2}KrUITa!JRZ|V7^D%fuJx->3Kt*5rL$tR`lQ!AzrKzv0*S-Iizu~8Q`0b&QB*#pl zHJ67J7Ig$g->Nh;`8onz5-&FSDLhmXL6;^ct5~{TYrXW{{`w&brk1_3wCa#Rzi(aH za&cM%R7F@OG$lK6-Z>B5?+QrDn(asZi;G^sxsuiK`osI<$!zevXVS)Dh~llL5FuK< z8h1}~a&8YqPr6VG2A#DmwHlsmRAT2ha`+X62(WmR<>ve#3xAZWn;t2y!zyl3O{DS^ zRU!i&FNuiga0fZ{fB};JZ%U%$3tO{>z}>`Ka~l`4R?Q8B6mD!l$Ta>@XV9o=uBXJa zBo6~O-z5*hftgFhep#KoRZv87xI=)t(1=j~Hv`e>1ziY38?7J$AFZm#a`CX-AY_Am zIr@4-GedRmKv3GCpzTn;$c1$)m!=1B7kjvsS5kCxpvDpsOpQz#RWgYz zg^YIq-YklYdUZ-YRVu%w4ICbqJ59rZslGC>_cwZfOgJ$Pnsf*OWC*KRNRstc^f6dS zG`>s~XWGhQum0Z%gXMwmHeuw#NMQZIi|U9`?Im4^!~sX;8Pyj@OlK`GrASv_X$bUG zUSTMC#=%6zofQ5{ER`VTqm?7&58RU*0skziQ*No~$_t@gEM))K0aLZLdpTSBsaa#E z(tG6lE0{Z3QH284d36bk;e~#_FrYH0rTuI0oi6QX!Q)zZ6Or!68^PuN&&% zJXuW+BpCaT#J>WlmFx2ZDBol!RwSA18lWHO$ja`SqG;caq^(VMya5;3 z1!^F#9)=3Z&k^F(z=iW})zo#@(e*0ueO8Qb#r%o~=jfOb!!H1iZt*3P${Ygj!w4$& zWR{(sC*05-MZF(*dZQ$|%|$k(Iw2xlqgPB#>o8p&27m5_I;O6kJ2t=*Krn%(SA6@^ zoaecK_asi)yFr&91yrXZ5pvt-&z7feQ}X-aswnTy&>p4YfczefFOZ2=mMv=N(-t9f zk)<3APB(&W#@)Km!OnpNpyVT3IbNY-V7(fKN;z`}7GL!L!secO&#jhww7K3K=uROx zot#_-yM39vu=+KwGQ_w!f5K#C6TA*@{C+fl@nvs?rz9Wb7QAjTKm!oUGMON$APa<7 zg7g7dtASThV7?UK)&5Snkv@65lT&RRSPY}Yt2ovIm$6Oz*(1NRrh!Qw!62pWf(%*J zTG38bEqf8vlzx`@Uh*h;U8f+kV`=@{yFf{sPZ9TNL=ufOsTGG9UIX-ETtM;Y8RUUU z*xNpZlIGAxZehrlN_9iw^fZJ?zn@KYQ=ycd7;P!XnQ0q4B{2F5HF<9HP!6vL@lNLoTu89Dm zrc+KQKSlLQA&7xyWxpD;G&t13CX8jH5rrzQ;erNRJrXap5-6LyD=pcsH-5?Pxu4Z- zx!c1oYCUXAwlgQZMTBz!m*M1{!b;3Ryflka@e5#c1P#|Lu9csxavHrc z_or_9T7`)LWkTu=#D>AT6bD(-I4X*mkkm0jge23i=nuCltf(tm8x@}(Eeu66#d)~y zt#Cvlw2d-C%2m1fSzbBa`+J~~kySL&!1#36`Wd8IZOBT^&NR2Ch4QyQq3ZKuetG(Q?x2i3=~GP1{aU_K-N3(J+m~-7n}+zqoy(j|@}Mx&L^O|D9?tVrBNO^008{ zuJV~~TchR@RYt-tJdMi^(GBr9vkqrEoW^Vct|qSqK;Zb$yNtHZtK>eja(^lt;A;HGK2y+m0*y^Ne)ySzOfUk+#2PCp&72KKAZW2V0F z4_O|u&=KYx7MZZ(m`Wg{0CL()p3qoR&{)i@fy{5W{z3Ki1t(MyCHocTOhg9fSWE?X z_PqdNb5188a{k&}|H2f-d+enKv%;{nn9499(h?HG)A0j?$-IvsNso$ zEJcbaTDeBUQedaj!NKK+d#PHcfYRASC4&?Cv({pmE28?ObqxnzLWLHr@icKqfAYQ0 z^HJZ%T5>|4_lW)|3bBfSnxPb*u4oW5f0KUZQVzQ8%5F;h3POFB9berh5ERDNghx%q zt0k6g(F4Tf55YW+(@+i_;^-d=?q!(Rj);kq_AxwoXycA#m{=<_`jjk>xfzzhy6ZRd zmHg+VuwRj-B3rc=SLW)>oI2`(Kz}+^g~GrmG*kU+`eN>!2Orn{#Dr-;xsN&P6azU| zsYp6mX2{iWbUrCOZzx^mSia3{Uq%W2N$t=F_AIkqS%9H%$={Z?_=iGAwAxu`5!gec zRA`wiO#E{$u6tT>I)cZsQzfe z{I22TmeYFS-%U7ZASZRe^W++o`1RyHrv;!R(554R8S~Bew2yw9a7KrlcWDAD=I;ai zjzS1luSb;Q-bA&5gSb|f?I2h?0Ur7KOBa%Bjy6NisR6}eVOG;{$#Zf|rC#svplZ9n zL-Su5bsqPTyHOxAX+~IQzraqYHH(fbM45;{PE<>(V5~Mt>y8-WfQ|I7l&kvFj8|I! zw?s&8p1xiFO?t+_D{;8d&s>5C46d(uI=}<8TAg?qAU>y(066madYIiw6`g54+(s^3 zW=mY~f??1<7&u(rT%Q0=53{TDbMl7+6ui;atzI_AFZA;}u+FadEENy$_x|uf{wZw* zy20)JJyE`9LSFTP%+S$3H&u%m*>jOOQBZ4kSpN;p^v~gZVbh|umB*Yg)ArUxEx)-e z^y%!4)Y|41Ut1yn=21J9ihMjBuli^JD7HltlyuAEx0G>)>cj1-qZX*uIVJ9|{g%Q* zr&kV04MD)*xR=dbBxdDUmwOv?OqaV?gzrh)DJJ}gH*J?r-z%Po20OPCJ>kYk2-28= zwqCAPs<=l-Rud$Z7Z%pK&zXVJvw0F^?_Fh2SOC$AP1ENmD(%Xb8qHSm@hNbu1Cy;3 zIHROH&BD%G4StiE@9$c<)R0f?L(kYaDAexFDKXUdtbv3CW0{nCN|kW(G+b@ETP+rJ zy&E(4@YsMby*;ZarLG!`?x{3S%JNh9V7B|z8SH<2)lqGfnN}Uw7l5pcNGB`9R;tR_ zd@FM|Wk9UbnVNgp&Es@>Jk&!1d=M_h&%lm{a~Bj#p{%vJ=AKaRl^oO9G7%N3F3PrS z4F^e=bi+NMWj7Tr#huavH0UO!7V73e$bazw8x z#>nW=Ha83hdcRdJm9>g?@OcGi6J#R>hF- z$6EyaX~!55eAtgSf0;z!15-n#yl`x!42?(&!-3e6z2MgxU1%T;V`#$;AH1Zgs-941 z4xaa!yYNrvM^P)pghopo4L+hs4>}LuA&s@;VK7tJ_HF@u{n((GPYLl{TSgNEsRh2T zHK$NuEHx`ZaN{@*9?liJ@NTWOCTQys59jGrz^$VSD<&obB{}LFz^^i>LaxlBN2x$s zElIT~IC=Oq2VcX#Qhlgkgmu%X!g>zuKI&$Hpl}rxt&)~&A++2)03+mZdP`ihW)(@p z$k2;HHD*qqe+v~{bMi^ZnOPOnY*e=JFznnW;e!arAyrIKzCIiRB?^ zN&mEt=-sGi_i1J?!+4>I*&+MC0UBhecjzoaN<<;p-hH)ZC8>fnX0haIb)1@*h#LBe zi_$i1aQT0Fm7EfCQ91W*%iAzCEn}WFPN^i&3Sy>0z%WEm)2YIseU`nwXSmAZmQVT6+k`piCoUW^qpcZx|Ci zJak(l3^64(vDq{7pA2t-Q`(J^3AjzW;}=w+@T44H`#Z5NTXG6c_1k1q2q9?k)*Q2|>D3 zS~{dtx?|~(W|vw)X{5WGrQ-DHaC%0?0zrtc6Hy|ht|;G7v~)Ctp7{Th)vQ) z)UxvuH|3a~MkGf(I%uS4&j?ORwejWX%}Y*_;P4?V1A}Mih)XpBs>#G-4@*kK^h(c9 zy!OH;`MD|t{5fS!U0*2-rF%nNK6xKNJ_jMb|GGtJe>yC;r~b;{f~@nT`s=)t%A`>z zzg23$Wn?}^GGk5%uWUXD=M zm`^qF%jZwW^-n|_bs{QTI$f$ZXrN~c5v}uB!;GzeJ$P&s?-LfArqlKWTFWxa_qe8# z$X6|M|Mdfe@eOd?Zw}iVC{t*w%WW&NAC7J+e#xJX@1ICA>ckb~^WVU7PY;wT>rTSu zZTJD5RS5WTJcj&Z61SpBqg@q%!fQEECV`x>YH~hbzguZ~u5um=Jg(&jn^gF+hn@E2 zh?uYG9Va+}DWas9(Cj5EJ_wEud0{F`(K>b1EIf7aCeFl)Mq54*g?C*k(Q$wNX)^et z((2ue__`51iDy07iNbAL^20GCuI#YuYpUavL~3jQ>-;_HU@lYn)RXS!$kwym@|3*( z2}FGN8hu z%(g8jd=8|-bA}B*neJc;&_P$ zvN%Zd!KG!6wqGDG2%pr}?^s5GS8hB{P86GC*G0LksbqRW(YO;)T0^eoClk@J<{^Ds(DdWz_dLI1pc8Oq>zmA^lm9Ls47pw!f>zhgi+LF$; z1r@7EVBzlJ1~4LYL}c0S=LtgsW2?ri`XC1Ej@+c1$tMNX`?tFKUDu7gs=aC}EJ=X& z(=?_wN;_CPN1qYdVC|dcHV91;!m9Vt^ne>ku}Ku6wsjB+?;pU0eeFmhW_9y{t@+!L zPz1&1;)im&+u;-Ao7-WAa0xkmdO~!By=It(VNC$5kW%bd7rLLPy6X|FvhJILXzZ$# zYAZXjTt@IR!Tjs-OR-O5K@2Ix2U%RJ+&Ng2D_DwLE65~1RRy(`&+%MF3~A)wJe385 zWW%;78DDnmqW)<;nH?r%$hl~D(_)RSXnA6;0tIMa ztt!ckveA>*N*6Na_G%IUS}wN`+kj}}Gd?x*ekC9;%bqv=q1<9b@{eFx*(9(={r=V6Z=&y+@GgDzmg#+BzI0ggIk_AO^m|MAOI&hj;e@QeXZ;N^$NmqL>Tmy$ z@ebQV_w7)@7Z)Kdd|N4xDn(}@twfjBqwSXMa}8BU zN!bp-(7`1u=lUO3w*BJS=CIpct5!RR3bx*^Yq`;#X*x>)qYH^JIhS}vWSaanvW!O# zgfTz7PxMU}xK1W?Pqw^dohH&mQ{G{5e^jkT*kp^3u~qs31|_Ml;jvESJP5dAqbCn} z-r2L9Ft{Z01JhcrtnTw&adX`^b

2*-#2!+qKBc;u!85A_XXKNm(Yavw!FdLd*`^ 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 QA_u;+fwjD)`1sZT0}H6*qW}N^ literal 0 HcmV?d00001 diff --git a/docs/raw/요구_1104/_보고_814-1_불러오기반영.md b/docs/raw/요구_1104/_보고_814-1_불러오기반영.md new file mode 100644 index 000000000..ebe23fd31 --- /dev/null +++ b/docs/raw/요구_1104/_보고_814-1_불러오기반영.md @@ -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 셋 지움