16 Commits
Author SHA1 Message Date
eomsangdonandClaude Fable 5.1 af156a3c3a knowledge(요구720): 수정 요청 방식 물음
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 17:33:16 +09:00
eomsangdonandClaude Fable 5.1 fd30e71dba knowledge(요구719): 기계 잇기 — 다 찾을 수 있음 · 별칭 · 조합 기계 둘로
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 17:31:22 +09:00
eomsangdonandClaude Fable 5.1 b8cd50fd63 knowledge(요구718): 기계 못 잇는 이유 다시 확인 요청
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 17:19:17 +09:00
eomsangdonandClaude Fable 5.1 74faf9cf40 knowledge(요구717): 단가산출 기계 · 옵션 변경 가능 여부 확인 요청
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 17:08:32 +09:00
eomsangdonandClaude Fable 5.1 eaab09896b knowledge(요구716): 기계 230 블록 — 옵션값 · 미리 만든 기초 확인 요청
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 14:48:13 +09:00
eomsangdonandClaude Fable 5.1 0c27c0793a knowledge(요구715): 수량 설명 요청 · 1번 대기 · 기계 230 블록 뜻
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 14:16:00 +09:00
eomsangdon a83b923363 Merge branch 'sub_laptop_2' of C:/Aislo_wt/sub_laptop_2 into main_laptop_1 2026-11-09 13:37:48 +09:00
eomsangdonandClaude Sonnet 5.5 9e59cffbbe knowledge(812-4): 연결 판정 목록 — 전수 2,220 블록(확정 163 · 근접 577 · 없음 1,480) · 812-2 대비 전후 · 율 값 = 참고
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 13:36:56 +09:00
eomsangdon 4e7ebf7f87 feat(812-4): 연결 찾기 율 값 = 참고 — 율 줄 판정 = 구조(대상 · 비목)만 · 율 값 % 다름 = 확정 + 참고(바꾸면 우리 값) · 율 대상 · 비목 다름 = 다른 항 그대로
- M02_Doc_Match_Note.rate_pairs → (다른 항, 참고) · 「율 값」 다른 항 폐지(EXPLAINED 에서도 뺌) · 띠 참고 요약에 「율 값 다름 N 줄」
- 시험: test_812_2 율 값 = 참고로 고침 · test_812_4 새(리퍼 105 · 275 확정 + 참고 · 율 대상 다름 = 다른 항 · 바꾸기 뒤 M01 율 · 되돌리기)
- 전수 2,220: 확정 152 → 163 · 근접 588 → 577 · 없음 1,480 · 확정 다시 셈 다름 0
2026-11-09 13:24:28 +09:00
eomsangdon 043bc1ee78 Merge branch 'sub_laptop_3' of C:/Aislo_wt/sub_laptop_3 into main_laptop_1 2026-11-09 13:19:44 +09:00
eomsangdonandClaude Sonnet 5.5 2094436577 feat(802-2a): GC000100 암파쇄(유압식 할암공법) 수식 문서 넣음 — 단가산출 부름 값 = 끝수 뒤 채택(요구 714-3)
- 빌더 ADOPTED(채택된 차이) — GC000100 지역=경북 재료비 1 원 · 목록에 남기고 저장은 막지 않음 · 요약에 「채택된 차이」
- 수식 문서 1,518 → 1,519(건설품셈 03장 토공사 파일에 GC000100 49 줄 더함 · 다른 문서 그대로)
- test_802_2 기대 바꿈(채택 차이 · 저장 1,519) · test_802_3 「빈 문서」 길은 임시 사본에서 GC000100 문서를 지워 봄(낡은 기대: GC000100 문서 없음)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 13:19:17 +09:00
eomsangdonandClaude Fable 5.1 79769e6a47 knowledge(요구714): 판단 셋 답 — 율 값 = 값 · 기계 이름 별칭 조사 · 1 원 채택
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 12:45:16 +09:00
eomsangdon 22588ea7c4 Merge branch 'sub_laptop_2' of C:/Aislo_wt/sub_laptop_2 into main_laptop_1 2026-11-09 10:10:09 +09:00
eomsangdonandClaude Opus 5.5 fbaf2f1d28 knowledge(812-2): 연결 판정 목록 — 전수 2,220 블록(확정 152 · 근접 588 · 없음 1,480) · 다른 항 · 참고 · 810-4 전후
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:09:21 +09:00
eomsangdonandClaude Opus 5.5 f1abfc2fb1 fix(812-2): 연결 찾기 로직 대조 다듬음 — 원파일 기계경비 묶음 ↔ 우리 기계 = 이름 종류 짝 · 율 대상 = (자원 갈래, 비목) · 번지 합 · 비목 이름 몫
- 기계경비 숫자 묶음(코드 없음) ↔ 우리 기계 = 이름 머리로 같은 기계(로울러 = 롤러 · 물탱크 = 살수차 등) · 시간당 단가 · 구성 = 참고
- 로직 같은 짝을 먼저 지음(같은 수량 줄 엇갈림 막음) · 한쪽 단가 없음(0) 비목 = 값
- 율 = 번지 합이면 핵심 줄 전부 · 「노무비 × n %」 = 그 비목 몫(기계 운전 노무비 포함) · 대상 = (갈래, 비목) 묶음
- test_812_2 — 기계 이름 짝 · 율 대상 범위 시험 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:38:19 +09:00
eomsangdonandClaude Opus 5.5 1e4ce036c6 feat(812-2): 연결 찾기 확정 = 로직 — 단가 · 끝수 = 참고(확정을 안 막음) · 율 값 = 비율 · 노임 · 자재 종류 · 산출 단가 = 다른 항
- M02_Doc_Match_Note(새) — 율 = 율 줄 셈값 ÷ 대상 줄 금액 합(대상 비목) · 짝 줄 다른 항 대 참고 가르기
- M02_Doc_Match — 같은 줄 = 로직 같은 줄(코드 다름은 짝 대조로) · 연결후보 `참고` 칸 · 참고 적은 조합 · 후보 먼저
- 띠 글 「확정 — 로직 같음 · 참고: 단가 다름 n 줄 · 우리 단가로 셈 ○○ 원」
- test_810_4 기대 고침(단가 차이 · 끝수 = 참고로 옮김) · 전수 도우미 확정 다시 셈 = 우리 금액 · 확정 참고 셈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:03:34 +09:00
15 changed files with 709 additions and 54 deletions
+63 -32
View File
@@ -7,9 +7,10 @@
② 옵션 풀이 = 그 문서의 선택 · 입력 변수 값을 줄마다 좁힘(줄 수량 · 단가가 의존하는 변수만 돌림 ·
값이 가져온 줄 수량 · 단가 묶음에 있거나 0(꺼진 줄)인 값만 남김) → 남은 조합(상한 `CAP_ALL`)을
문서 셈 · 입력 변수 후보 = 기본값 · 0 · 가져온 식에 나온 수 · 선택은 가져온 글에 든 값부터.
③ 판정 = 줄 대조(수량 · 단가 · 비목 금액 · 율 줄) + 노 · 재 · 경 · 계 —
③ 판정 = 줄 대조 = 로직(812-2 · 요구 712 — `M02_Doc_Match_Note`): 다른 항 = 자원 · 기계 규격 · 수량 ·
비목 · 율 구조(대상 · 비목) · 줄 없음 · 이름 · 참고 = 단가 · 끝수 · 율 값 %(값 — 확정을 안 막음) —
다른 항 0 = 「확정」 · 같은 줄이 하나 넘고 핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」.
결과 `{판정, 키, 이름, 옵션, 다른, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`.
결과 `{판정, 키, 이름, 옵션, 다른, 참고, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`.
"""
from __future__ import annotations
@@ -22,6 +23,7 @@ from decimal import Decimal, InvalidOperation
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Match_Note as note
md, mc, ml = dc.md, dc.mc, dc.ml
if not hasattr(
@@ -133,6 +135,7 @@ def facts(doc: dict, got: dict) -> list[dict]:
"합밖": bool(row.get("합밖")),
}
)
note.rates(doc, got, out)
return out
@@ -405,7 +408,7 @@ def _ratio(a: Any, b: Any) -> str | None:
def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
"""가져온 줄(mine) ↔ M01 줄(ours) → {같음, 짝, 핵심, 다른 [{항, 글}]}."""
"""가져온 줄(mine) ↔ M01 줄(ours) → {같음(로직 같은 줄), 짝, 핵심, 다른 [{항, 글}], 참고 [{항, 글}]}."""
left, right = list(mine), list(ours)
same = paired = 0
diffs: list[dict] = []
@@ -426,22 +429,29 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
def cls(a: dict, b: dict) -> bool:
return a["갈래"] == b["갈래"] or {a["갈래"], b["갈래"]} <= {"기계", "산출"}
notes: list[dict] = []
exact = take(
lambda a, b: (
a["수량"] == b["수량"] and a["단가"] == b["단가"] and a["비목"] == b["비목"]
if core(a, b)
else a["갈래"] == b["갈래"] == "율" and a["비목"] == b["비목"]
core(a, b)
and a["수량"] == b["수량"]
and a["단가"] == b["단가"]
and a["비목"] == b["비목"]
and (a["코드"] == b["코드"] or not (a["코드"] and b["코드"])) # 코드 다름 = 짝 대조
)
)
same += sum(1 for a, _ in exact if a["갈래"] != "율")
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["수량"] == b["수량"]):
paired += 1
if a["갈래"] == "기계" and b["갈래"] == "기계" and a["코드"] != b["코드"]:
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
elif a["단가"] != b["단가"]:
diffs.append({"항": "단가 차이", "글": f"{a['이름']} 단가 {a['단가']} ↔ {b['단가']}"})
else:
diffs.append({"항": "비목", "글": f"{a['이름']} 비목 다름"})
same += len(exact)
def twin(a: dict, b: dict) -> bool:
return core(a, b) and cls(a, b) and a["수량"] == b["수량"]
for a, b in take(lambda a, b: twin(a, b) and not note.pair(a, b)[0]): # 로직 같은 짝 먼저
same += 1
notes += note.pair(a, b)[1]
for a, b in take(twin):
bad, ref = note.pair(a, b)
same, paired = (same + 1, paired) if not bad else (same, paired + 1)
diffs += bad
notes += ref
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["단가"] == b["단가"]):
paired += 1
how = _ratio(a["수량"], b["수량"])
@@ -452,10 +462,9 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
diffs.append(
{"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']} · 수량 다름"}
)
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율" and a["금액"] == b["금액"]):
diffs.append({"항": "비목", "글": f"율 줄 {a['이름']} 비목 다름"})
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율"):
diffs.append({"항": "율 값", "글": f"{a['이름']} {a['금액']} ↔ {b['이름']} {b['금액']}"})
rate_diffs, rate_notes = note.rate_pairs(left, right, take)
diffs += rate_diffs
notes += rate_notes
for side, rows in (("가져온 쪽만", left), ("우리 쪽만", right)):
for x in rows:
if x["갈래"] == "율":
@@ -464,7 +473,7 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
what = "줄 없음"
diffs.append({"항": what, "글": f"{side} — {x['이름']}"})
n_core = sum(1 for a in mine if a["갈래"] != "율")
return {"같음": same, "짝": same + paired, "핵심": n_core, "다른": diffs}
return {"같음": same, "짝": same + paired, "핵심": n_core, "다른": diffs, "참고": notes}
def _totals(got: dict) -> dict:
@@ -473,27 +482,30 @@ def _totals(got: dict) -> dict:
EXPLAINED = (
"이름 다름",
"단가 차이",
"자원",
"산출 단가",
"율 대상",
"단위 환산",
"기계 규격",
"비목",
"율 값",
"율 줄",
"공구손료 유무",
"끝수",
)
def verdict(
mine: list[dict], ours: list[dict], got_a: dict, got_b: dict, off: list[dict] = ()
) -> dict:
"""확정 = 다른 항 0(옵션 글 근거까지 맞음) · 근접 = 핵심 줄 반 넘게 짝 + (같은 줄 있음 또는
다른 항이 모두 까닭 있는 갈래 — 단가 · 단위 환산 · 기계 규격 · 비목 · 율) · 그 밖 없음."""
"""확정 = 다른 항 0(옵션 글 근거까지 맞음 · 참고는 있어도 됨) · 근접 = 핵심 줄 반 넘게 짝 + (같은 줄
있음 또는 다른 항이 모두 까닭 있는 갈래 — 자원 · 단위 환산 · 기계 규격 · 비목 · 율) · 그 밖 없음 ·
끝수 = 줄 로직 · 단가 모두 같은데 합만 다름(참고)."""
res = compare(mine, ours)
res["다른"] = [*off, *res["다른"]]
ta, tb = _totals(got_a), _totals(got_b)
if not res["다른"] and ta != tb:
res["다른"].append({"항": "끝수", "글": f"줄은 같고 합이 다름 {ta} ↔ {tb}"})
if not res["다른"] and not res["참고"] and ta != tb:
res["참고"].append(
{"항": "끝수", "글": f"줄은 같고 합이 다름 {_sum_txt(ta)} ↔ {_sum_txt(tb)}"}
)
if not res["다른"] and res["같음"]:
res["판정"] = SURE
elif res["짝"] * 2 >= res["핵심"] + 1 and (
@@ -505,6 +517,10 @@ def verdict(
return res
def _sum_txt(t: dict) -> str:
return " · ".join(f"{k} {note.txt(v)}" for k, v in t.items() if v is not None)
# ── 한 문서 ───────────────────────────────────────────────────────────
def _calc(doc: dict, links: Any, given: dict) -> dict | None:
try:
@@ -533,12 +549,26 @@ def try_doc(
off = [_off(base, k, pick[k], ev[k]) for k in ev if k in pick and pick[k] not in ev[k]]
off += named
res = verdict(mine, facts(base, got), got_imp, got, off)
rank = (res["판정"] == SURE, -len(off), res["같음"], res["짝"], -len(res["다른"]))
rank = (
res["판정"] == SURE,
-len(off),
res["같음"],
res["짝"],
-len(res["다른"]),
-len(res["참고"]),
)
if best is None or rank > best["순위"]:
best = {**res, "순위": rank, "옵션": pick, "금액": _totals(got)}
if res["판정"] == SURE:
if res["판정"] == SURE and not res["참고"]:
break
return best or {"판정": NONE, "같음": 0, "짝": 0, "다른": [], "순위": (False, -9, 0, 0, 0)}
return best or {
"판정": NONE,
"같음": 0,
"짝": 0,
"다른": [],
"참고": [],
"순위": (False, -9, 0, 0, 0, 0),
}
def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
@@ -549,7 +579,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)
got_imp = _calc(imported, links_imp, {})
if got_imp is None:
@@ -561,7 +591,7 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
res = try_doc(index.docs[key], links, imported, got_imp, mine, index.regions)
if best is None or res["순위"] > best["순위"]:
best, best_key = res, key
if res["판정"] == SURE:
if res["판정"] == SURE and not res["참고"]:
break
if best is None:
return {"판정": NONE, "후보수": 0}
@@ -575,6 +605,7 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
"이름": base.get("이름"),
"옵션": _shown(base, best["옵션"]),
"다른": best["다른"],
"참고": best["참고"],
"금액": {"가져온": _plain(_totals(got_imp)), "우리": _plain(best["금액"])},
"후보수": len(keys),
}
+228
View File
@@ -0,0 +1,228 @@
"""연결 찾기 — 로직 차이(다른 항) 대 값 차이(참고) 가르기(PLAN 812-2 · 요구 712) — `M02_Doc_Match` 가 부름.
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기계 규격 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
율 구조(있고 없음 · 대상 비목 — 율 값 % 는 참고) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함).
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 율 값(%) · 그로 인한 비목 금액 · 계.
율 = 율 줄 셈값 ÷ 대상 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 몫 · 번지 합 = 핵심 줄 전부 ·
대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님.
"""
from __future__ import annotations
import re
from decimal import Decimal, InvalidOperation
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
md = dc.md
PRICED = ("노임", "재료", "기계") # 시간당 · 단위 단가 = 값(참고)
# ── 율 구조 ───────────────────────────────────────────────────────────
def _names(text: Any) -> set[str]:
if text in (None, ""):
return set()
try:
return set(md.names(md.parse(text)))
except md.DocError:
return set()
def _reach(doc: dict) -> dict[str, set[str]]:
"""기호 → 바로 부르는 이름(변수 연결 · 식 줄 · 금액 줄 수량 · 단가)."""
direct: dict[str, set[str]] = {}
for v in doc.get("변수") or []:
direct[md.var_id(v)] = set(md.link_names(v.get("연결") or {}))
for line in doc.get("식") or []:
direct[str(line.get("기호"))] = _names(line.get("식"))
for line in doc.get("금액") or []:
if line.get("기호"):
direct[str(line["기호"])] = _names(line.get("수량")) | _names(line.get("단가"))
return direct
def _close(start: set[str], direct: dict[str, set[str]]) -> set[str]:
seen, todo = set(start), list(start)
while todo:
for n in direct.get(todo.pop(), ()):
if n not in seen:
seen.add(n)
todo.append(n)
return seen
def _short(r: Decimal) -> Decimal:
return Decimal(f"{r:.6g}").normalize()
def rates(doc: dict, got: dict, facts: list[dict]) -> None:
"""율 사실에 `율`(비율 · 못 셈 None) · `대상`({(자원 갈래, 비목)})을 채움(제자리)."""
money, rows = doc.get("금액") or [], got.get("줄") or []
direct = _reach(doc)
core = [f for f in facts if f["갈래"] != "율" and not f["합밖"]]
for f in facts:
if f["갈래"] != "율":
continue
line, row = money[f["번"]], rows[f["번"]]
reach = _close(_names(line.get("수량")) | _names(line.get("단가")), direct)
costs = reach & set(md.COSTS)
hits = [c for c in core if _hit(money[c["번"]], reach)]
if hits: # 줄 기호 · 줄 수량 · 단가 이름 = 그 줄 금액 전부
parts = [(c, tuple(c["비목"])) for c in hits]
elif costs: # 비목 이름(「노무비 × 4 %」) = 그 비목 몫만
parts = [(c, tuple(k for k in c["비목"] if k in costs)) for c in core]
else: # 번지 합 · 못 찾음 = 블록 핵심 줄 전부
parts = [(c, tuple(c["비목"])) for c in core]
parts = [(c, ks) for c, ks in parts if ks]
f["대상"] = frozenset((_cls(c["갈래"]), k) for c, ks in parts for k in ks)
f["율"] = None
amount = _mul(row.get("수량"), row.get("단가"))
if amount is None:
continue
bases = [sum((_num(c["비목"][k]) for c, ks in parts for k in ks), Decimal(0))]
if hits:
each = (_mul(rows[c["번"]].get("수량"), rows[c["번"]].get("단가")) for c in hits)
bases.append(sum((x or 0 for x in each), Decimal(0)))
found = [_short(amount / b) for b in bases if b]
if found:
f["율"] = min(found, key=lambda r: len(r.as_tuple().digits))
def _hit(line: dict, reach: set[str]) -> bool:
parts = _names(line.get("수량")) | _names(line.get("단가"))
return bool(line.get("기호") and str(line["기호"]) in reach) or bool(parts and parts <= reach)
def _cls(kind: str) -> str:
return "기계" if kind == "산출" else kind # 기계경비 묶음(코드 없음) = 기계
def _num(v: Any) -> Decimal:
if isinstance(v, dict):
return sum((_num(v.get(k)) for k in md.COSTS), Decimal(0))
try:
return Decimal(str(v)) if v is not None else Decimal(0)
except InvalidOperation:
return Decimal(0)
def _mul(q: Any, p: Any) -> Decimal | None:
if q is None or p is None:
return None
return _num(q) * _num(p)
def pct(r: Any) -> str:
"""비율 → 「9 %」."""
if not isinstance(r, Decimal):
return "?"
return f"{(r * 100).normalize():f} %"
# ── 짝 줄 ─────────────────────────────────────────────────────────────
def txt(v: Any) -> str:
"""단가 글 — 수 = 원 단위 쉼표 · 기계 묶음 = 노 · 재 · 경."""
if isinstance(v, tuple):
return " · ".join(f"{k[0]} {txt(x)}" for k, x in zip(md.COSTS, v) if x)
if isinstance(v, Decimal):
return f"{v.normalize():,f}"
return str(v)
_ALIAS = (("로울러", "롤러"), ("굴삭", "굴착"), ("브레이카", "브레이커"), ("로우더", "로더"))
_ALIAS += (("물탱크", "살수차"), ("아스팔트페이버", "아스팔트피니셔")) # 같은 기계 다른 이름
def _plain(text: str) -> str:
t = re.sub(r"\s", "", text)
for old, new in _ALIAS:
t = t.replace(old, new)
return t
def _head(text: str) -> str:
"""기계 이름 머리(괄호 · 규격 앞 한글) — 「머캐덤로울러(자주식)」 → 「머캐덤롤러」."""
m = re.match(r"[가-힣]+", _plain(text))
return m.group(0) if m else ""
def _same_machine(a: str, b: str) -> bool:
x, y = _head(a), _head(b)
return bool(x and y) and (x in y or y in x)
def _grams(text: str) -> set[str]:
t = _plain(text)
return {run[i : i + 2] for run in re.findall(r"[가-힣]+", t) for i in range(len(run) - 1)}
def pair(a: dict, b: dict) -> tuple[list[dict], list[dict]]:
"""수량 같은 짝 줄 → (다른 항, 참고) — 단가만 다르면 참고 · 자원 · 비목이 다르면 다른 항."""
diffs: list[dict] = []
kinds = {a["갈래"], b["갈래"]}
# 기계경비 숫자 묶음(코드 없음 · 원파일 값) ↔ 우리 기계 = 이름 종류로 같은 기계인지
loose = kinds == {"기계", "산출"} and not (a if a["갈래"] == "산출" else b)["코드"]
if a["갈래"] == b["갈래"] == "기계" and a["코드"] != b["코드"]:
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
elif loose and not _same_machine(a["이름"], b["이름"]):
diffs.append({"항": "기계 규격", "글": f"{a['이름']} ↔ {b['이름']} {b['코드']}"})
elif a["갈래"] == b["갈래"] == "노임" and a["코드"] != b["코드"]:
diffs.append({"항": "자원", "글": f"{a['이름']} {a['코드']} ↔ {b['이름']} {b['코드']}"})
elif a["갈래"] == b["갈래"] == "재료" and not _same_stuff(a, b):
diffs.append({"항": "자원", "글": f"{a['이름']} ↔ {b['이름']}"})
machine = a["갈래"] == b["갈래"] == "기계" or loose # 시간당 단가 구성(운전 노무비 등) = 값
priced = a["비목"] and b["비목"] # 한쪽 단가 없음(0) = 값
if priced and not machine and set(a["비목"]) != set(b["비목"]):
diffs.append(
{"항": "비목", "글": f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])}"}
)
if a["단가"] == b["단가"]:
return diffs, []
say = f"{a['이름']} 단가 {txt(a['단가'])} ↔ {txt(b['단가'])}"
if (a["갈래"] == b["갈래"] and a["갈래"] in PRICED) or loose:
return diffs, [{"항": "단가 차이", "글": say}]
return [*diffs, {"항": "산출 단가", "글": say}], []
def _same_stuff(a: dict, b: dict) -> bool:
"""자재 종류 같음 — 두 코드가 있으면 코드 · 없으면 이름 낱말(한글 두 글자) 겹침."""
if a["코드"] and b["코드"] and a["코드"] == b["코드"]:
return True
return bool(_grams(a["이름"]) & _grams(b["이름"]))
def rate_pairs(left: list[dict], right: list[dict], take: Any) -> tuple[list[dict], list[dict]]:
"""율 줄 짝 → (다른 항, 참고) — 구조 = 대상 · 비목 · 같으면 로직 같음(율 값 % 다름 = 참고 · 값은 업데이트됨)."""
diffs: list[dict] = []
notes: list[dict] = []
def is_rate(a: dict, b: dict) -> bool:
return a["갈래"] == b["갈래"] == "율"
def same(a: dict, b: dict, *keys: str) -> bool:
return is_rate(a, b) and all(
(set(a[k]) == set(b[k])) if k == "비목" else a.get(k) == b.get(k) for k in keys
)
def name(a: dict, b: dict) -> str:
return f"{a['이름']} {pct(a.get('율'))} ↔ {b['이름']} {pct(b.get('율'))}"
def target(x: dict) -> str:
return "·".join(f"{k} {c}" for k, c in sorted(x.get("대상") or ())) or "없음"
def value(a: dict, b: dict) -> None:
if a.get("율") != b.get("율"):
notes.append({"항": "율 값", "글": f"{name(a, b)} — 바꾸면 우리 값"})
take(lambda a, b: same(a, b, "율", "대상", "비목")) # 로직 · 값 같음
for a, b in take(lambda a, b: same(a, b, "대상", "비목")): # 로직 같음 · 율 값만 다름
value(a, b)
for a, b in take(lambda a, b: same(a, b, "대상")):
diffs.append({"항": "비목", "글": 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
+24 -4
View File
@@ -3,6 +3,7 @@
* 가져온 식 ↔ 우리 M01 기초 연결 판정 띠 자료(PLAN 810-4 · 요구 707 · 709 · 703) — 원문 패널 카드 위.
* SK `가져온.원본.연결후보`(서버 `M02_Doc_Match` 가 적음) → 띠 글 · 다른 항 · 단추 하나:
* 확정 = [우리 기초 사본으로 바꾸기] · 근접 · 없음 = 단추 없음(703) · 안 찾음 = [연결 찾기] ·
* 다른 항 = 로직 차이(확정을 막음) · 참고 = 단가 · 끝수 · 율 값 %(값 차이 — 「우리 단가로 셈 ○○ 원」 · 812-2 · 812-4) ·
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기]
* 화면 · DOM · fetch 없음
* ========================================================================== */
@@ -15,6 +16,8 @@ export interface MatchFound {
이름?: string;
옵션?: Record<string, string>;
다른?: { 항: string; 글: string }[];
/** 값 차이(단가 · 끝수 · 율 값) — 확정을 안 막음 */
참고?: { 항: string; 글: string }[];
금액?: { 가져온?: Record<string, number | null>; 우리?: Record<string, number | null> };
후보수?: number;
가까움?: { 키?: string; 이름?: string; 다른?: { 항: string; 글: string }[] };
@@ -44,6 +47,23 @@ const opts = (o?: Record<string, unknown>): string => {
const lines = (d?: { 항: string; 글: string }[]): string[] =>
(d ?? []).map((x) => `${x.항} — ${x.글}`);
const won = (n?: number | null): string =>
n == null ? "" : `${Math.round(n).toLocaleString("ko-KR")} 원`;
/** 참고 요약 — 「참고: 단가 다름 3 줄 · 율 값 다름 1 줄 · 우리 단가로 셈 53,218 원」 · 참고 없음 = "" */
const noteText = (f: MatchFound): string => {
const n = f.참고 ?? [];
if (!n.length) return "";
const price = n.filter((x) => x.항 === "단가 차이").length;
const rate = n.filter((x) => x.항 === "율 값").length;
const bits = [
price ? `단가 다름 ${price} 줄` : "",
rate ? `율 값 다름 ${rate} 줄` : "",
n.some((x) => x.항 === "끝수") ? "끝수 다름" : "",
].filter(Boolean);
const ours = won(f.금액?.우리?.계);
return ` · 참고: ${bits.join(" · ")}${ours ? ` · 우리 단가로 셈 ${ours}` : ""}`;
};
/** SK → 띠 자료 · 가져온 식도 · 바꾼 SK 도 아니면 null */
export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
const kept = sk?.가져온보관;
@@ -64,8 +84,8 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
if (f.판정 === "확정") {
return {
tone: "success",
text: `우리 기초 ${base} 와 확정 일치`,
details: [],
text: `확정 — 로직 같음 · 우리 기초 ${base}${noteText(f)}`,
details: lines(f.참고),
action: "바꾸기",
};
}
@@ -73,8 +93,8 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
const kinds = [...new Set((f.다른 ?? []).map((x) => x.항))].join(" · ");
return {
tone: "warning",
text: `근접 — ${base} · 다른 항: ${kinds}`,
details: lines(f.다른),
text: `근접 — ${base} · 다른 항: ${kinds}${noteText(f)}`,
details: [...lines(f.다른), ...lines(f.참고).map((x) => `참고 ${x}`)],
action: null,
};
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -565,3 +565,39 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의
## 요구 713 (11-09 · 사용자 · 제안대로 진행(엑셀 읽기기 새로 + 연결 기준 = 로직) · 가져온 단가산출이 좌측에서 읽히게(나열만 X))
다음으로 중요한 점은 경쟁사의 단가산출을 가져왔다 가정하고 현재의 경쟁사 단가산출은 좌측영역에서 제대로 읽기가 힘든 그냥 노무비, 경비, 재료 1,2, 3 형태로 죽 나열만 되어있음. 분석도 중요하지만 보여지는것도 중요함. 너의 제안대로 잘 끝까지 진행해줘
## 요구 714 (11-09 · 사용자 · 판단 셋 답 — 1 보류 · 2-1 율 값 = 값(로직 아님) · 2-2 기계 이름 = 별칭 조사 · 3 1 원은 로직만 맞으면 됨)
1번은 생각할 시간이 필요.
2-1번 율값 관련 내가 로직과 값을 구분하라고 했잖아. 로직은 어떤 계산을 할때 뭐를 곱하는지 뭐를 더할지 같은 것들이고 여기에 사용된 값은 결국 언제든 업데이트 될수 있다고 불러오기 기능들에서에서 초기에 검산을 위해서는 겂을 사용하겠지만 결국은 우리의 m01의 값을 사용할 수 있어야해. 그게 현재 m02의 단가산출에 있는 값 업데이트 기능이고 이해해?
2-2번 기계 이름이 다를경우 별칭을 사용하는게 어떨까해. 우리는 품셈원문의 이름을 사용하고 있기도 하고 m01 단가산출에도 별칭으로 경쟁사와 정보가 다를경우 승인하고 별칭으로 등록하는게 있잖아. 조사해봐
3. 1원 차이는 로직만 맞다면 문제 없음.
## 요구 715 (11-09 · 사용자 · 수량 추가 설명 요청 · 1번 계속 대기 · 기계 230 블록 뜻 물음)
수량 관련은 추가 설명 요청
1번 계속 대기
2번 기계 230 블럭은 경쟁사 프로그램에서 만들어둔 기계의리스트라는 말임?
## 요구 716 (11-09 · 사용자 · 하나씩 — 기계 230 블록: 굴삭기 0.7 = 바스켓 크기 · 기계 옵션값 변경 또는 미리 만들어 두기 · 확인부터)
일단 하나씩 하자. 기계230블럭에서 굴삭기 0.7에서 0.7은 바스켓의 사이즈로 보이는데 기계의 옵션값 변경을 하면 되는거 아닌가? 아니면 미리 만들어 두던가 확인부터 해봐.
## 요구 717 (11-09 · 사용자 · 우리 단가산출에서 기계 · 옵션 변경이 안 되는 게 문제 — 기계가 들어오면 상단 옵션에 기계 선택 · 하위 옵션 · 그 뒤 연결 찾기 방향 · 확인부터)
1. 그것보다 중요한 건 우리 단가산출에서 기계를 변경하거나 옵션을 변경하는게 안되는게 문제아니야? 그렇다면 기계가 단가산출에 들어오면 상단 옵션에 기계선택이나 하위 옵션선택이 되어야하는거 아닌가?
2. 그리고 나서 연결찾기의 방향이 결정될것같은데?
3. 확인부터 해봐.
## 요구 718 (11-09 · 사용자 · 기계 줄 누르면 목록이 나옴 → 옵션 없어도 됨 · 문제는 연결을 못 찾는 것 · 조회어 「굴착기(무한궤도) 0.7㎥」 가 문제 아닌지 · 기계 못 연결하는 이유 다시 확인)
지금 확인했는데 좌측영역에서 기계 줄에서 지정된 기계를 눌렀더니 옵션 별로 항목 리스트가 나오는데 이러면 옵션이 없어도 되는거잖아. 그럼 문제는 옵션 여부가 아니라 연결을 제대로 찾지 못하는게 아닌가? 그리고 추가로 좌측영역 기계를 눌렀을때 굴착기(무한궤도) 0.7㎥ 이렇게 조회를 하는데 이게 문제 아니야? 기계를 못 연결하는 이유를 다시 확인해봐.
## 요구 719 (11-09 · 사용자 · 기계 예들 직접 찾아 보니 대형 브레이커 + 굴착기 빼고 다 찾을 수 있음 · 옛 이름은 별칭 사용 가능 · 조합 기계 = 설비 둘을 각각 가져오기)
너의 알려준 낱말을 모르는 예와 후보가 여럿인 예를 내가 직접찾아 봤는데 대형 브레이커 + 굴착기 만 제외하면 다 려었이 아니라 다찾을 수 있겠는데? 그리고 경쟁사 프로그램이 예전프로그램이라 찾기 힘들면 별칭을 사용해도됨. 다음으로 2개의 기계의 조합을 대현 브레이커, 굴착기로 표현한게 아닐까? 우리는 설비 2개를 각각 가져오면 맞을 것같다는 생각이 드네.
## 요구 720 (11-09 · 사용자 · 수정을 어떻게 요청했는지 물음)
뭐지 어떻게 수정을 요청한거야?
@@ -12,7 +12,9 @@
· 계(돈 아닌 로직 = 결과)
원 단위 — 기본 옵션 전부 + 옵션 표본(로직마다 고르기 입력의 다른 갈래 하나씩 · 나머지 기본).
둘 다 못 셈 = 같음(따로 셈). 보정 없음 — 다르면 목록에 까닭.
- 저장 = 같은 것만(요구서_664 1 장 3) — 모든 대조가 같고 부르는 단가산출도 저장되는 문서 ·
- 채택된 차이(`ADOPTED`) = 사용자 요구 714-3 「1원 차이는 로직만 맞다면 문제 없음」 — 식 속 단가산출
부름 값 = 끝수 뒤(새 계산기 그대로) · 목록에 남기고 저장은 막지 않음.
- 저장 = 같은 것만(요구서_664 1 장 3) — 모든 대조가 같고(채택된 차이 빼고) 부르는 단가산출도 저장되는 문서 ·
`resources/master_data/수식문서/수식_<옛 로직 파일 꼬리>.json`(로직 파일마다
· 옛 로직 JSON 은 안 씀).
- 목록 = `docs/raw/요구_1104/_목록_802-2_변환대조.xlsx`(로직 · 옵션 · 옛 · 새 · 차이 · 까닭 + 요약).
@@ -44,6 +46,8 @@ RAW_SUB = (
"식 속 단가산출 부름 = 끝수 뒤 값"
"(옛 엔진 = 끝수 전 날 값 · `master_formula.run` 결과를 식에 씀)"
)
# 채택된 차이 — (로직, 옵션) · 요구 714-3
ADOPTED = {("GC000100", "지역=경북")}
def rows_of(files: dict) -> list[tuple[str, dict]]:
@@ -108,7 +112,7 @@ def why(doc: dict, old, new) -> str:
if isinstance(new, str) and not isinstance(old, str):
return f"새 계산기 못 셈 — {new}"
if _uses_raw_sub(doc):
return RAW_SUB
return RAW_SUB + " · 채택(요구 714-3)"
return "까닭 모름"
@@ -151,6 +155,7 @@ def build(log=print) -> dict:
)
for one in runs:
if not one["같음"]:
one["채택"] = (one["로직"], one["옵션"]) in ADOPTED
one["까닭"] = why(docs[one["로직"]], one["옛"], one["새"])
keep = _keep(docs, runs)
for one in runs: # 같아도 못 넣는 것 — 부르는 단가산출이 빠짐
@@ -165,6 +170,7 @@ def build(log=print) -> dict:
"옵션 표본": sum(1 for x in runs if x["옵션"]),
"옵션 같음": sum(1 for x in runs if x["옵션"] and x["같음"]),
"옵션 다름": sum(1 for x in runs if x["옵션"] and not x["같음"]),
"채택된 차이": sum(1 for x in runs if x.get("채택")),
"둘 다 못 셈": both_out,
"저장": len(keep),
"변환 알림": sum(1 for v in notes.values() if v),
@@ -192,7 +198,7 @@ def _calls(doc: dict) -> set[str]:
def _keep(docs: dict, runs: list[dict]) -> set[str]:
"""저장할 문서 — 모든 대조 같음 + 부르는 단가산출도 저장(닫힘).
키 구멍 부름은 그 로직 대조로 판정."""
keep = set(docs) - {x["로직"] for x in runs if not x["같음"]}
keep = set(docs) - {x["로직"] for x in runs if not x["같음"] and not x.get("채택")}
while True:
drop = {k for k in keep if not _calls(docs[k]) <= keep}
if not drop:
@@ -641,6 +641,55 @@
"끝전": "비목버림"
}
},
{
"꼴": "수식문서",
"판": 1,
"키": "GC000100",
"이름": "암파쇄(유압식 할암공법)",
"결과단위": "원/㎥",
"출처": "건설품셈 공통 3-3-8",
"변수": [
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
{"열쇠": "보정작업", "기호": "S", "아래": "보정작업", "위": "", "이름": "보정작업", "단위": "", "출처": "건설품셈 공통 8-1-7 1", "연결": {"갈래": "선택", "목록": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "값": "해당 없음"}},
{"열쇠": "드릴경비", "기호": "C", "아래": "드릴", "위": "", "이름": "드릴경비", "단위": "", "연결": {"갈래": "식"}},
{"열쇠": "브레이커경비", "기호": "C", "아래": "브레이커", "위": "", "이름": "브레이커경비", "단위": "", "연결": {"갈래": "식"}},
{"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'5405-0110'", "지역": "S_지역"}, "칸": "계"}},
{"열쇠": "Q_유압식크롤드^표", "기호": "Q", "아래": "유압식크롤드", "위": "표", "이름": "암파쇄(유압식 할암공법).유압식크롤러드릴", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "유압식크롤러드릴"}},
{"열쇠": "P_굴착기무한궤", "기호": "P", "아래": "굴착기무한궤", "위": "", "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000271", "입력": {"기계": "'0201-0100'", "브레이커": "'0230-0010'", "지역": "S_지역", "보정작업": "S_보정작업"}, "칸": "계"}},
{"열쇠": "Q_굴착기대형브^표", "기호": "Q", "아래": "굴착기대형브", "위": "표", "이름": "암파쇄(유압식 할암공법).굴착기_대형브레이커", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "굴착기_대형브레이커"}},
{"열쇠": "Q_기계설비공^표", "기호": "Q", "아래": "기계설비공", "위": "표", "이름": "암파쇄(유압식 할암공법).기계설비공", "단위": "인", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "기계설비공"}},
{"열쇠": "w_기계설비공", "기호": "w", "아래": "기계설비공", "위": "", "이름": "기계설비공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000051"}},
{"열쇠": "Q_특별인부^표", "기호": "Q", "아래": "특별인부", "위": "표", "이름": "암파쇄(유압식 할암공법).특별인부", "단위": "인", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "특별인부"}},
{"열쇠": "w_특별인부", "기호": "w", "아래": "특별인부", "위": "", "이름": "특별인부노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000003"}},
{"열쇠": "P_기계시간당2", "기호": "P", "아래": "기계시간당2", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'5405-0110'", "지역": "S_지역"}}},
{"열쇠": "Q_발전기^표", "기호": "Q", "아래": "발전기", "위": "표", "이름": "암파쇄(유압식 할암공법).발전기", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "발전기"}},
{"열쇠": "P_기계시간당3", "기호": "P", "아래": "기계시간당3", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'7505-0025'", "지역": "S_지역"}}},
{"열쇠": "Q_유압식할암기^표", "기호": "Q", "아래": "유압식할암기", "위": "표", "이름": "암파쇄(유압식 할암공법).유압식할암기", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "유압식할암기"}},
{"열쇠": "P_기계시간당4", "기호": "P", "아래": "기계시간당4", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'5501-0080'"}}},
{"열쇠": "P_굴착기무한2", "기호": "P", "아래": "굴착기무한2", "위": "", "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000271", "입력": {"기계": "'0201-0100'", "브레이커": "'0230-0010'", "지역": "S_지역", "보정작업": "S_보정작업"}}},
{"열쇠": "Q_기계경비의^표", "기호": "Q", "아래": "기계경비의", "위": "표", "이름": "소모자재 요율.기계경비의", "단위": "%", "연결": {"갈래": "표", "표": "QC000202", "조건": {"장비": "'유압식 크롤러드릴'"}, "칸": "기계경비의"}},
{"열쇠": "Q_기계경비의2^표", "기호": "Q", "아래": "기계경비의2", "위": "표", "이름": "소모자재 요율.기계경비의", "단위": "%", "연결": {"갈래": "표", "표": "QC000202", "조건": {"장비": "'굴착기+대형브레이커'"}, "칸": "기계경비의"}}
],
"식": [
{"기호": "C_드릴", "식": "P_기계시간당 × Q_유압식크롤드^표"},
{"기호": "C_브레이커", "식": "P_굴착기무한궤 × Q_굴착기대형브^표"}
],
"금액": [
{"이름": "기계설비공", "단위": "인", "비목": "노무비", "수량": "Q_기계설비공^표", "단가": "w_기계설비공", "묶음": "인력"},
{"이름": "특별인부", "단위": "인", "비목": "노무비", "수량": "Q_특별인부^표", "단가": "w_특별인부", "묶음": "인력"},
{"이름": "유압식 크롤러드릴 110㎾", "단위": "시간", "비목": "묶음", "수량": "Q_유압식크롤드^표", "단가": "P_기계시간당2", "묶음": "유압식 크롤러드릴 110㎾"},
{"이름": "발전기 25㎾", "단위": "시간", "비목": "묶음", "수량": "Q_발전기^표", "단가": "P_기계시간당3", "묶음": "발전기 25㎾"},
{"이름": "유압식할암기 ∅80", "단위": "시간", "비목": "묶음", "수량": "Q_유압식할암기^표", "단가": "P_기계시간당4", "묶음": "유압식할암기 ∅80"},
{"이름": "굴착기+대형브레이커 1.0㎥", "단위": "시간", "비목": "묶음", "수량": "Q_굴착기대형브^표", "단가": "P_굴착기무한2", "묶음": "굴착기+대형브레이커 1.0㎥"},
{"이름": "소모자재(크롤러드릴)", "단위": "식", "비목": "재료비", "단가": "C_드릴 × Q_기계경비의^표 ÷ 100", "출처": "건설품셈 공통 3-3-8 주④", "묶음": "소모자재(크롤러드릴) · 소모자재(대형브레이커)"},
{"이름": "소모자재(대형브레이커)", "단위": "식", "비목": "재료비", "단가": "C_브레이커 × Q_기계경비의2^표 ÷ 100", "출처": "건설품셈 공통 3-3-8 주④", "묶음": "소모자재(크롤러드릴) · 소모자재(대형브레이커)"}
],
"끝수": {
"줄": {"방법": "버림", "자리": 1},
"비목": {"방법": "버림", "자리": 0},
"끝전": "비목버림"
}
},
{
"꼴": "수식문서",
"판": 1,
+12 -3
View File
@@ -2,7 +2,7 @@
파일마다 프로세스 · 안쪽 산근(형제)은 같은 파일 블록을 우리 단가로 먼저 셈해 묶음 값으로 넣음(저장 층의
형제 SK 읽기와 같은 값) · 확정은 M01 문서를 찾은 옵션(입력값 꼴)으로 다시 셈해 금액 대조.
마지막 줄 = JSON {블록, 판정, 근접항, 오류, 확정다시셈다름} · `--rows <pkl>` = 줄 목록 저장(목록 xlsx 용).
마지막 줄 = JSON {블록, 판정, 근접항, 확정참고, 확정참고블록, 오류, 확정다시셈다름} · `--rows <pkl>` = 줄 목록 저장(목록 xlsx 용).
"""
from __future__ import annotations
@@ -96,9 +96,12 @@ def sweep(arg: tuple[str, str, int]) -> list[dict]:
row |= {"규격": (doc.get("가져온") or {}).get("규격"), **res}
if res.get("판정") == dm.SURE:
again = dm._calc(docs[res["키"]], dm.ml.Links(whole, docs), dict(res["옵션"]))
money = res["금액"] # 다시 셈 = 우리 · 참고 없으면 가져온 = 우리(812-2)
row["다시셈같음"] = (
again is not None and dm._plain(dm._totals(again)) == res["금액"]["가져온"]
) # noqa: E501
again is not None
and dm._plain(dm._totals(again)) == money["우리"]
and (bool(res.get("참고")) or money["우리"] == money["가져온"])
)
row["초"] = round(time.time() - start, 2)
rows.append(row)
return rows
@@ -121,6 +124,12 @@ def main() -> None:
"근접항": dict(
collections.Counter(d["항"] for r in real if r["판정"] == "근접" for d in r["다른"])
), # noqa: E501
"확정참고": dict(
collections.Counter(
d["항"] for r in real if r["판정"] == "확정" for d in r.get("참고") or []
)
),
"확정참고블록": sum(1 for r in real if r["판정"] == "확정" and r.get("참고")),
"오류": sum(1 for r in real if r["판정"] == "오류"),
"확정다시셈다름": sum(1 for r in real if r.get("다시셈같음") is False),
}
+16 -6
View File
@@ -4,7 +4,7 @@
① --check — 다시 돌린 결과 = 저장본(`resources/master_data/수식문서/`) · 목록 xlsx.
② 로직 1,519 전수 기본 옵션 = 옛 엔진 노 · 재 · 경 · 계(결과) 원 단위 같음.
③ 옵션 표본 — 로직마다 고르기 입력의 모든 갈래 한 번 이상 · 같음 수 · 다름 = 목록 · 까닭 모름 0.
④ 저장 = 같은 것만(부르는 단가산출까지) · 저장본을 읽어 다시 셈해도 같음.
④ 저장 = 같은 것만(부르는 단가산출까지 · 채택된 차이 GC000100 지역=경북 재료비 1 원 빼고) · 저장본을 읽어 다시 셈해도 같음.
⑤ 791 실무 손 옮김 견본 넷(번지 · 단계 · 합밖 · 줄 자리 · 끝전 · 조정 · 값묶음)도 변환 셈 = 엑셀 금액.
"""
@@ -56,7 +56,7 @@ def test_옵션_표본_모든_갈래_한_번_이상(result):
assert (row["키"], f"{spec['이름']}={opt}") in seen
info = result["요약"]
assert info["옵션 표본"] == want == info["옵션 같음"] + info["옵션 다름"]
assert info["옵션 같음"] >= 18700 and info["옵션 다름"] <= 1
assert info["옵션 같음"] >= 18700 and info["옵션 다름"] == info["채택된 차이"] == 1
def _whole():
@@ -69,15 +69,25 @@ def test_다름_목록_까닭(result):
assert sheet[0] == list(build.HEAD) and sheet[1:] == rows
diff = [x for x in result["대조"] if not x["같음"]]
assert all(x["까닭"] != "까닭 모름" for x in diff)
assert [(x["로직"], x["옵션"], x["까닭"]) for x in diff] == [
("GC000100", "지역=경북", build.RAW_SUB)
]
# 채택된 차이(요구 714-3) — 식 속 단가산출 부름 값 = 끝수 뒤 · 재료비 1 원 · 저장은 함
assert [(x["로직"], x["옵션"], x["채택"]) for x in diff] == [("GC000100", "지역=경북", True)]
assert diff[0]["까닭"].startswith(build.RAW_SUB)
assert diff[0]["옛"]["재료비"] != diff[0]["새"]["재료비"]
assert abs(diff[0]["새"]["재료비"] - diff[0]["옛"]["재료비"]) == 1
assert {k: diff[0]["옛"][k] for k in ("노무비", "경비")} == {
k: diff[0]["새"][k] for k in ("노무비", "경비")
}
def test_저장_같은_것만_다시_셈(result):
saved = ml.load_all()
assert set(saved) == result["저장"]
assert "GC000100" not in saved and len(saved) == result["요약"]["저장"] >= 1518
assert "GC000100" in saved and len(saved) == result["요약"]["저장"] >= 1519
base = next(x for x in result["대조"] if x["로직"] == "GC000100" and not x["옵션"])
assert (
base["같음"]
and build.new_answer(ml.Links(_whole(), saved), saved["GC000100"]) == base["옛"]
)
for key in ("GC000211", "GC001147", "GF000294", "GC000087"):
doc = saved[key]
got = build.new_answer(ml.Links(_whole(), saved), doc)
+5 -1
View File
@@ -67,7 +67,11 @@ def test_읽기_미리셈_연결기록(client):
def test_없는문서_빈문서_없는키_404(client):
fresh = _read(client, "GC000100") # 802-2a 로 문서 안 넣음
assert (
_read(client, "GC000100")["새"] is False
) # 802-2a 로 문서 넣음 — 임시 사본에서 지워 빈 문서 길을 봄
doc_screen.drop("GC000100")
fresh = _read(client, "GC000100")
assert fresh["새"] is True and fresh["문서"]["식"] == [] and fresh["문서"]["금액"] == []
assert fresh["셈"]["셈됨"] is False and fresh["셈"]["계"] is None
assert client.get("/api/m01/logic/doc", params={"key": "GX999999"}).status_code == 404
+8 -5
View File
@@ -5,7 +5,7 @@
근접(합판거푸집 4회 = GC000211 보통 · 4회 — 단가 차이 · 비목 · 율) · 요구 704 파형강관 매설 Φ800 =
GF000179 관경 800 근접 · 다른 항 = 단위 환산(× 6) · 기계 규격 · 율 줄 · 없음(후보 0)
(b) 옵션 풀이 — 줄 수량이 의존하는 변수만 돌려 좁힘 · 글 근거 값과 다르면 「옵션 글」 다른 항(확정 아님)
(c) 줄 대조 — 같음 · 단가 차이 · 단위 환산 · 율 줄 · 끝수(지어 맞추기 없음 — 1 원 달라도 확정 아님)
(c) 줄 대조 — 같음 · 단위 환산 · 율 줄 = 다른 항 · 단가 차이 · 끝수 = 참고(812-2 · 확정을 안 막음)
(d) 바꾸기 — 임시 층 개인: 확정 SK = 같은 키에 M01 사본(바탕 · 입력값) · 금액 = M01 문서 셈 · 원래 몸 보관 →
되돌리기 = 원래 금액 · 근접 SK 바꾸기 = 409 · M01 문서 파일 그대로
(e) 전수(1,518 문서 × 원파일 산근 2,220 블록 · 파일마다 프로세스) — 확정은 모두 다시 셈해도 금액 같음 ·
@@ -70,7 +70,8 @@ def test_near_formwork_lists_differences() -> None:
assert got["판정"] == dm.NEAR and got["키"] == "GC000211"
assert got["옵션"]["사용횟수"] == "4회" and got["옵션"]["유형"] == "보통"
kinds = {d["항"] for d in got["다른"]}
assert "단가 차이" in kinds and "비목" in kinds # 대전 합판 단가 · 공구손료 재료비 ↔ 경비
assert kinds == {"비목"} # 공구손료 1 % 재료비 ↔ 경비 = 로직(812-2)
assert [d["항"] for d in got["참고"]] == ["단가 차이"] # 대전 합판 단가 = 값(참고)
def test_req704_pipe_is_near_with_unit_rate_machine() -> None:
@@ -118,10 +119,11 @@ def test_compare_kinds_and_no_tolerance() -> None:
tot = {"노무비": 9, "재료비": 0, "경비": 0, "계": 9}
assert dm.verdict(a, list(a), tot, tot)["판정"] == dm.SURE
off = dm.verdict(a, list(a), tot, {**tot, "노무비": 10, "계": 10})
assert off["판정"] != dm.SURE and off["다른"][0]["항"] == "끝수" # 1 원도 확정 아님
assert off["판정"] == dm.SURE and off["참고"][0]["항"] == "끝수" # 끝수 = 참고(812-2)
b = [_line("인부", "노임", "0.24", "100"), _line("공", "노임", "0.5", "11")]
res = dm.verdict(a, b, tot, tot)
assert [d["항"] for d in res["다른"]] == ["단가 차이", "단위 환산"] and res["판정"] == dm.NEAR
assert [d["항"] for d in res["다른"]] == ["단위 환산"] and res["판정"] == dm.NEAR
assert [d["항"] for d in res["참고"]] == ["단가 차이"]
rate = {**_line("율", "율", "1", "3", "경비"), "수량": None, "단가": None}
res = dm.verdict([*a, rate], a, tot, tot)
assert res["다른"] == [{"항": "율 줄", "글": "가져온 쪽만 — 율"}] and res["판정"] == dm.NEAR
@@ -197,7 +199,8 @@ def test_every_block_table() -> None:
assert got["블록"] == 2220 and got["오류"] == 0 and got["확정다시셈다름"] == 0
assert set(got["판정"]) <= {dm.SURE, dm.NEAR, dm.NONE}
assert sum(got["판정"].values()) == 2220 and got["판정"].get(dm.SURE, 0) >= 1
assert collections.Counter(got["근접항"]).get("단가 차이", 0) >= 1
assert not {"단가 차이", "끝수"} & set(got["근접항"]) # 값 차이 = 참고(812-2)
assert collections.Counter(got["확정참고"]).get("단가 차이", 0) >= 1
def test_parent_reads_swapped_sibling(world) -> None:
+180
View File
@@ -0,0 +1,180 @@
"""812-2 지킴 — 연결 찾기 확정 = 로직(요구 712) · 값 차이 = 참고(`M02_Doc_Match_Note`).
(a) 단가만 다름 = 확정 + 참고 「단가 차이」(원파일 기계경비 묶음 ↔ 우리 기계 = 이름 종류 같을 때) ·
끝수만 다름 = 확정 + 참고 「끝수」
(b) 로직 차이 = 다른 항 — 율 대상 · 기계 규격 · 노임 종류(「자원」) · 산출 단가 · 비목
(c) 율 = 비율 — 거창 합판거푸집(블록 41): 가져온 「주자재소계 × 9 %」 = M01 소모자재 9 % 짝 · 율_2 1 % 재료비 ↔
공구손료 1 % 경비 = 「비목」 · 대전 합판 단가 = 참고
(d) 요구 704 배수관부설 D=800 ↔ GF000179 = 여전히 근접(단위 환산 × 6 · 기계 규격 · 율 줄)
(e) 전수(S8122_ALL=1) — 2,220 블록 · 확정 다시 셈 같음 · 근접 까닭에 단가 차이 · 끝수 없음 · 확정 참고 있음
원파일 없으면 (c) · (d) · (e) skip(다른 PC).
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from decimal import Decimal as D
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))
from M02_MasterTemplete import M02_Doc_Match as dm # noqa: E402
TOT = {"노무비": 9, "재료비": 0, "경비": 0, "계": 9}
def _line(name, kind, qty, price, cost="노무비", code=""):
q, p = D(qty), D(price)
return {"번": 0, "이름": name, "갈래": kind, "코드": code, "수량": dm._key(q),
"단가": dm._key(p), "비목": {cost: dm._key(q * p)}, "금액": dm._key(q * p), "합밖": False} # fmt: skip
def _rate(name, rate, target=(("노임", "노무비"),), cost="경비"):
return {"번": 0, "이름": name, "갈래": "율", "코드": "", "수량": None, "단가": None,
"비목": {cost: D(1)}, "금액": D(1), "합밖": False, "율": D(rate), "대상": frozenset(target)} # fmt: skip
A = [_line("인부", "노임", "0.04", "100", code="LB1"), _line("공", "노임", "0.5", "10", code="LB2")]
# ── (a) 값 차이 = 참고 ───────────────────────────────────────────────────
def test_price_only_is_sure_with_note() -> None:
b = [_line("인부", "노임", "0.04", "120", code="LB1"), A[1]]
res = dm.verdict(A, b, TOT, {**TOT, "노무비": 10, "계": 10})
assert res["판정"] == dm.SURE and not res["다른"] and res["같음"] == 2
assert [d["항"] for d in res["참고"]] == ["단가 차이"]
mach = [_line("굴삭기", "기계", "0.1", "500", "경비", "0201-0070")]
other = [_line("굴삭기", "기계", "0.1", "530", "경비", "0201-0070")]
assert dm.verdict(mach, other, TOT, TOT)["판정"] == dm.SURE # 기계 시간당 단가 = 참고
file = [
_line("머캐덤로울러(자주식)", "산출", "0.1", "530", "경비")
] # 원파일 기계경비 묶음(코드 없음)
ours = [_line("탠덤롤러", "기계", "0.1", "500", "경비", "1206-0008")]
ours.append(_line("머캐덤롤러 10∼12ton", "기계", "0.1", "500", "경비", "1106-0012"))
res = dm.verdict(file, ours, TOT, TOT) # 이름 종류 같은 기계와 짝 · 단가 = 참고
assert (
not [d for d in res["다른"] if d["항"] != "줄 없음"] and res["참고"][0]["항"] == "단가 차이"
)
run = [{**_line("드릴", "산출", "0.1", "530", "경비"), "비목": {"노무비": D(1), "경비": D(2)}}]
res = dm.verdict(run, [_line("드릴", "기계", "0.1", "500", "경비", "0901-0010")], TOT, TOT)
assert res["판정"] == dm.SURE # 기계경비 구성(운전 노무비 포함) 차이 = 시간당 단가 = 참고
none = [{**_line("모래", "재료", "1", "0", "재료비"), "비목": {}}]
assert (
dm.verdict([_line("모래", "재료", "1", "9", "재료비")], none, TOT, TOT)["판정"] == dm.SURE
)
res = dm.verdict(file, ours[:1], TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["기계 규격"]
def test_rounding_only_is_sure_with_note() -> None:
res = dm.verdict(A, list(A), TOT, {**TOT, "노무비": 10, "계": 10})
assert res["판정"] == dm.SURE and [d["항"] for d in res["참고"]] == ["끝수"]
# ── (b) 로직 차이 = 다른 항 ──────────────────────────────────────────────
def test_rate_value_is_note_structure_is_logic() -> None:
res = dm.verdict([*A, _rate("공구손료", "0.02")], [*A, _rate("공구손료", "0.03")], TOT, TOT)
assert not res["다른"] and res["판정"] == dm.SURE
assert [d["항"] for d in res["참고"]] == ["율 값"] # 율 값(%) 다름 = 참고(812-4) · 로직 같음
assert "2 % ↔" in res["참고"][0]["글"] and "3 %" in res["참고"][0]["글"]
same = dm.verdict([*A, _rate("율", "0.02")], [*A, _rate("공구손료", "0.02")], TOT, TOT)
assert same["판정"] == dm.SURE # 율 · 대상 · 비목 같음 = 이름 달라도 같은 율 줄
tgt = dm.verdict(
[*A, _rate("율", "0.02", (("재료", "재료비"),))], [*A, _rate("율", "0.02")], TOT, TOT
)
assert [d["항"] for d in tgt["다른"]] == ["율 대상"]
cost = dm.verdict([*A, _rate("율", "0.02", cost="재료비")], [*A, _rate("율", "0.02")], TOT, TOT)
assert [d["항"] for d in cost["다른"]] == ["비목"]
def test_machine_labor_kind_differs_blocks() -> None:
a = [*A, _line("굴삭기", "기계", "0.1", "500", "경비", "0201-0070")]
b = [*A, _line("굴삭기", "기계", "0.1", "500", "경비", "0201-0080")]
res = dm.verdict(a, b, TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["기계 규격"] and res["판정"] == dm.NEAR
c = [_line("특별인부", "노임", "0.04", "120", code="LB9"), A[1]]
res = dm.verdict(A, c, TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["자원"] and res["판정"] != dm.SURE
sub = [_line("모르타르", "산출", "0.1", "500", "재료비")]
res = dm.verdict(sub, [_line("모르타르", "산출", "0.1", "520", "재료비")], TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["산출 단가"] # 안쪽 산출 = 로직 결과
# ── (c) · (d) 원파일 블록 ────────────────────────────────────────────────
def _judge(no: int) -> dict:
t4 = pytest.importorskip("test_810_4_match")
try:
return t4.judge(no)
except FileNotFoundError:
pytest.skip("원파일 없음")
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["참고"]] == ["단가 차이"]
def test_req704_still_near() -> None:
got = _judge(19) # 배수관부설 D=800 ↔ GF000179
kinds = [d["항"] for d in got["다른"]]
assert got["판정"] == dm.NEAR and got["키"] == "GF000179"
assert kinds.count("단위 환산") == 2 and "기계 규격" in kinds and "율 줄" in kinds
# ── (e) 전수 ─────────────────────────────────────────────────────────────
@pytest.mark.skipif(os.environ.get("S8122_ALL") != "1", reason="전수 = S8122_ALL=1")
def test_every_block_logic() -> None:
out = subprocess.run(
[sys.executable, str(HERE / "helper_810_4_sweep.py")], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr[-2000:]
got = json.loads(out.stdout.strip().splitlines()[-1])
assert got["블록"] == 2220 and got["오류"] == 0 and got["확정다시셈다름"] == 0
assert not {"단가 차이", "끝수"} & set(got["근접항"])
assert got["확정참고블록"] >= 1 and got["판정"][dm.SURE] >= 98
def test_rate_target_scope() -> None:
"""율 대상 — 줄 기호 = 그 줄 · 비목 이름 = 그 비목 몫(기계 운전 노무비 포함) · 번지 합 = 핵심 줄 전부."""
from M02_MasterTemplete import M02_Doc_Match_Note as note
def run(rate_text: str) -> dict:
money = [
{"이름": "인부", "수량": "1", "단가": "100", "기호": "C_인부"},
{"이름": "굴삭기", "수량": "1", "단가": "300"},
{"이름": "율", "단가": rate_text},
]
rows = [
{"수량": D(1), "단가": D(100), "금액": D(100)},
{"수량": D(1), "단가": D(300), "금액": D(300)},
{"수량": D(1), "단가": D(0), "금액": D(0)},
]
facts = [
{"번": 0, "갈래": "노임", "비목": {"노무비": D(100)}, "합밖": False},
{"번": 1, "갈래": "산출", "비목": {"노무비": D(100), "경비": D(200)}, "합밖": False},
{"번": 2, "갈래": "율", "비목": {"경비": D(4)}, "합밖": False},
]
doc = {"변수": [], "식": [], "금액": money}
rows[2]["단가"] = {"C_인부 × 4%": D(4), "노무비 × 2%": D(4), "번지.A01.계 × 1%": D(4)}[
rate_text
]
note.rates(doc, {"줄": rows}, facts)
return facts[2]
one = run("C_인부 × 4%")
assert one["율"] == D("0.04") and one["대상"] == {("노임", "노무비")}
labor = run("노무비 × 2%") # 노무비 200(인부 100 + 굴삭기 운전 100)
assert labor["율"] == D("0.02") and labor["대상"] == {("노임", "노무비"), ("기계", "노무비")}
whole = run("번지.A01.계 × 1%") # 400 × 1 %
assert whole["율"] == D("0.01") and ("기계", "경비") in whole["대상"]
+79
View File
@@ -0,0 +1,79 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""812-4 지킴 — 율 값(%) 다름 = 참고(값은 M01 값으로 업데이트됨) · 율 대상 · 비목 다름 = 다른 항(로직).
(a) 원파일 main 블록 105(터파기 · 리퍼 2.42 % ↔ 우리 유압식 리퍼 2 %) · 275(절토면고르기 리퍼) =
확정 + 참고 「율 값」 (다른 항 0) · 글에 「바꾸면 우리 값」
(b) 확정 + 참고 블록 [바꾸기] → 금액 = M01 사본 셈(율도 M01 값) · [되돌리기] → 원래 금액 · M01 문서 파일 그대로
(c) 대상이 다른 율(교량받침 1.14 % ↔ 공구손료 3 % 노임 노무비) = 여전히 다른 항 「율 대상」
원파일 없으면 skip(다른 PC) — 합성 줄 시험은 test_812_2.
"""
from __future__ import annotations
import sys
from decimal import Decimal as D
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_810_2_native_doc as t2 # noqa: E402
import test_810_4_match as t4 # noqa: E402
from M02_MasterTemplete import M02_Doc_Match as dm # noqa: E402
from M02_MasterTemplete import M02_Doc_MatchSk as msk # noqa: E402
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
def _judge(no: int, tag: str = "main") -> dict:
whole, _, index, links = t4.ctx()
try:
out = t2.fn.emit(t2.native("EST", tag), no)
doc = t4.dim.derive(t2.sk_of(out), None, t2.whole())
except FileNotFoundError:
pytest.skip("원파일 없음")
return dm.match(doc, t4.dim.Links(whole, "우리"), index, links)
@pytest.mark.parametrize(("no", "key"), [(105, "GC000089"), (275, "GC000111")])
def test_rate_value_only_is_sure_with_note(no: int, key: str) -> None:
got = _judge(no)
assert got["판정"] == dm.SURE and got["키"] == key and not got["다른"]
rate = [d for d in got["참고"] if d["항"] == "율 값"]
assert len(rate) == 1 and "바꾸면 우리 값" in rate[0]["글"] and "2 %" in rate[0]["글"]
def test_rate_target_differs_stays_logic() -> None:
got = _judge(1533) # 교량받침설치 — 율 대상 기계 · 노임 ↔ 노임
assert "율 대상" in {d["항"] for d in got["다른"]} and got["판정"] != dm.SURE
def test_swap_rate_note_block_uses_m01_rate(world) -> None:
folder = world["personal"]
est = t2._file(t2.EST["main"])
before = {p.name: p.read_bytes() for p in dm.ml.FOLDER.glob("수식_*.json")}
with store.at(folder):
hp = fh.upload_native(folder, est.name, est.read_bytes(), t2.WHO)["키"]
name = fh.make(folder, hp, 105)["이름"]
old = store.read("sangeun", name)
money0 = t4._money(name)
found = msk.find(name, old["판"])
meta = found[dm.KEY]
assert meta["판정"] == dm.SURE and any(d["항"] == "율 값" for d in meta["참고"])
assert t4._money(name) == money0 # 찾기 = 기록만
msk.swap(name, found["판"])
doc = store.read("sangeun", name)["문서"]
want = dm._calc(dm.ml.load_all()[meta["키"]], dm.ml.Links(t2.whole()), dict(meta["옵션"]))
assert t4._money(name) == [want[k] for k in t4.COSTS] # 율도 M01 값
assert doc["바탕"] == {"로직": meta["키"]} and sum(t4._money(name)) != sum(money0)
msk.back(name, store.read("sangeun", name)["판"])
assert t4._money(name) == money0 # 되돌리기 = 원래 금액
assert D(sum(money0)) > 0
assert before == {p.name: p.read_bytes() for p in dm.ml.FOLDER.glob("수식_*.json")}