feat(일위대가실무): 338-1 맞춤 핵심 서버 2 단계 — 줄 새 상태 · 체크 제안 · 대상 목록 · 품 비교 · 호표 통째 · 셈 · 만든 · guess 목록 + 골든 시험
- 새 Line — 상태 차례(% · 호표 · 노임 · 장부 뺌 · 별칭 · 맞춤셈 판정 · 이름 근거 후보) · 체크 제안(맞음 · 별칭 규칙 셋 · 확인된 하위 호표) · 대상(입력값 출처 별칭 > 블록 > 규격 · 환산 · 몫 · 배) · 품 비교 · 직접 줄 호표 통째 · guess(대상 목록 · 낱개도 받음 · 엔진 한 번) - Match — view(to = 만들 층 · 셈) · picks · guess 를 Line 으로 · 옛 층 기억 · confirm · 들어감 없앰 · b_order 후보에 근거 칸 - Verdict — 위 다섯 딱 맞음 먼저(조합 없는 줄) · 후보 근거 = 근거 맞춤 꼴 · 지문차례(조합 전) - Fit — 맞춤셈 캐시 이름에 셈 판(CALC_VERSION) - 골든 시험(느림 · 1 분 54 초) — 사람 126 1순위 118 · 다섯 안 122 · 맞음 57 틀림 0 · 다른 점 틀림 1 · 옵션까지 57 · 조합 맞음 62/83 · 새 파일 51 맞음 틀림 2 · 씨앗 끔 · 북부청 조합 전 통과 - 근거 시험(나 창) 세 곳 — 옛 match_line → b_order(새 API) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -54,6 +54,7 @@ COMBO = (0.85, 0.75, 0.6)
|
||||
COVER = 0.6
|
||||
SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9)
|
||||
EVERY = 20 # 블록 몇 개마다 캐시 덮음
|
||||
CALC_VERSION = 1 # 맞춤셈 꼴 · 셈이 바뀌면 올림 — 옛 캐시 안 씀
|
||||
COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"}
|
||||
HOUR = {"hr", "시간", "h"}
|
||||
NO_NUMBER = "숫자 판정 없음"
|
||||
@@ -296,7 +297,7 @@ def _hp_path(folder: Path, key: str) -> Path:
|
||||
|
||||
def cache_path(folder: Path, key: str) -> Path:
|
||||
hp = hashlib.sha256(_hp_path(folder, key).read_bytes()).hexdigest()[:16]
|
||||
return ours.folder() / f"맞춤_{hp}_{ours.version()}.json"
|
||||
return ours.folder() / f"맞춤_{hp}_{ours.version()}_{CALC_VERSION}.json"
|
||||
|
||||
|
||||
def units(doc: dict[str, Any]) -> list[tuple[str, dict | None, dict]]:
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
"""M02 일위대가 실무자료 줄 맞춤 응답(PLAN 338-1 · 설계 3-3 · 3-4) — 줄 상태 · 체크 제안 · 대상 목록
|
||||
·
|
||||
후보 · 품 비교 · 호표 통째 · 셈 · 만든 · 미리 셈(`guess`).
|
||||
|
||||
상태 차례 = ① % ② 호표 참조(만들 층에 확인된 사전조합이면 체크) ③ 노임(그릇 없음) ④ 장부 「우리에
|
||||
없음 ·
|
||||
안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(규격 다름 · 근거 다름 · 숫자 판정 맞음이 다른 로직이면 체크 안 함) ⑥
|
||||
맞춤셈
|
||||
판정(맞음 = 체크 · 다른 점 · 후보) ⑦ 셈 전 · 숫자 없음 → 이름 · 근거 후보(「셈 중」 · 「숫자 판정
|
||||
없음」).
|
||||
자재 · 별도계상 = 이름 · 규격 같음 하나면 맞음(체크) · 그 밖 후보.
|
||||
입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값.
|
||||
체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독.
|
||||
장부 = `M02_Hopyo_Ledger`(다 창) · 확인된 사전조합 = `M02_Hopyo_Build.confirmed`(다 창).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
SKIP = ("우리에 없음", "안 씀")
|
||||
|
||||
|
||||
class Ctx:
|
||||
"""보기 한 번 — 파일 · 장부 · 맞춤셈 · 확인된 사전조합을 한 번 읽어 줄마다 넘김."""
|
||||
|
||||
def __init__(self, folder: Path, key: str, to: Path | None = None) -> None:
|
||||
self.folder, self.key = Path(folder), key
|
||||
self.to = Path(to or folder)
|
||||
self.doc = hopyo_store.read(folder, key)
|
||||
self.got = hm.targets()
|
||||
self.units = {int(h["번"]): h for h in self.doc.get("호표") or []}
|
||||
self.books = ledger.books(self.to)
|
||||
self.calc = fit.ensure(folder, key)
|
||||
self.cache = (fit.result(folder, key) or {}).get("블록") or {}
|
||||
self.done = hopyo_build.confirmed(self.to, refs.where(self.folder))
|
||||
self.names = {x["로직"]: x for x in self.got["로직"]}
|
||||
self.mats = {x["자재"]: x for x in self.got["자재"]}
|
||||
|
||||
|
||||
def _target(code: str, line: dict, ctx: Ctx, found: dict | None, use: bool, **extra) -> dict:
|
||||
"""대상 하나(로직) — 입력값 · 못읽음 · 출처 · 환산 · 몫 · 배."""
|
||||
one = ctx.names.get(code) or {"이름": "", "단위": ""}
|
||||
vals, unread = hm.spec_inputs(line, hm._logic_specs(code))
|
||||
src = {k: "규격" for k in vals}
|
||||
for k, v in ((found or {}).get("입력") or {}).items() if use else ():
|
||||
if str(vals.get(k)) != str(v):
|
||||
vals[k], src[k] = v, "블록"
|
||||
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
|
||||
share = extra.pop("몫", None)
|
||||
times = conv["배"] * share if conv["배"] is not None and share else conv["배"]
|
||||
head = {"로직": code, "이름": one["이름"], "단위": one["단위"], "입력값": vals}
|
||||
head.update(못읽음=[u for u in unread if u not in vals], 출처=src, 환산=conv)
|
||||
return {**head, "몫": share, "배": times, **extra}
|
||||
|
||||
|
||||
def _pair(pair: dict, line: dict, ctx: Ctx) -> dict:
|
||||
"""장부 짝 → 대상(입력값 출처 = 별칭)."""
|
||||
vals = dict(pair.get("입력값") or {})
|
||||
if pair.get("로직"):
|
||||
one = ctx.names.get(pair["로직"]) or {"이름": "", "단위": ""}
|
||||
head = {"로직": pair["로직"]}
|
||||
elif pair.get("자재"):
|
||||
one, head = ctx.mats.get(pair["자재"]) or {"이름": "", "단위": ""}, {"자재": pair["자재"]}
|
||||
else:
|
||||
h = ctx.units.get(int(pair.get("호표") or 0)) or {}
|
||||
one, head = (
|
||||
{"이름": h.get("이름", ""), "단위": h.get("단위", "")},
|
||||
{"호표": pair.get("호표")},
|
||||
)
|
||||
conv = {"배": pair.get("배"), "갈래": "별칭"}
|
||||
return {**head, "이름": one["이름"], "단위": one["단위"], "입력값": vals, "못읽음": []} | {
|
||||
"출처": {k: "별칭" for k in vals},
|
||||
"환산": conv,
|
||||
"몫": pair.get("몫"),
|
||||
"배": pair.get("배"),
|
||||
}
|
||||
|
||||
|
||||
def _qty(entry: dict, found: dict | None) -> dict:
|
||||
"""품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용)."""
|
||||
ex = entry.get("품") or {"인력": {}, "장비": [], "자재": []}
|
||||
us = (found or {}).get("우리품") or option.ours_qty(None)
|
||||
same = {}
|
||||
for t, a in ex["인력"].items():
|
||||
b = us["인력"].get(t)
|
||||
same[t] = bool(b) and option.near(a, b, fit.TOL[0], option.ru(a))
|
||||
for m in ex["장비"]:
|
||||
mine = [x["시간"] for x in us["장비"] if set(x["갈래"]) & set(m["갈래"])]
|
||||
hit = any(option.near(m["시간"], q, fit.TOL[0], option.ru(m["시간"])) for q in mine)
|
||||
same["·".join(m["갈래"])] = hit or same.get("·".join(m["갈래"]), False)
|
||||
return {"엑셀": ex, "우리": us, "같음": same}
|
||||
|
||||
|
||||
def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict:
|
||||
"""⑥ 맞춤셈 판정 → 상태 · 대상 · 후보 · 품 · 글 · 꼬리표."""
|
||||
judged = entry["판정"]
|
||||
state = judged["상태"]
|
||||
top = (entry.get("차례") or [None])[0]
|
||||
found = (entry.get("옵션") or {}).get(top)
|
||||
use = state in ("맞음", "다른 점")
|
||||
combo = [c for c in entry.get("조합") or [] if c["체크"]]
|
||||
combo = combo if len(combo) > 1 else [] # 체크할 로직이 하나뿐이면 조합 아님(기계 몫은 꼬리표)
|
||||
if combo:
|
||||
targets = [
|
||||
_target(
|
||||
c["로직"], line, ctx, entry["옵션"].get(c["로직"]), use, 몫=c["몫"], 어디=c["어디"]
|
||||
)
|
||||
for c in combo
|
||||
]
|
||||
else:
|
||||
targets = [_target(top, line, ctx, found, use)] if top else []
|
||||
tags = list(judged.get("꼬리표") or [])
|
||||
tags += [f"조합 — {' + '.join(c['로직'] for c in combo)}"] if combo else []
|
||||
for c in entry.get("조합") or []:
|
||||
if not c["체크"]:
|
||||
tags.append(f"기계 몫 {c['로직']} — 시간 = 엑셀 값(우리 셈 아님)")
|
||||
if any("블록" in t["출처"].values() for t in targets):
|
||||
tags.append("옵션 — 블록에서")
|
||||
tags += [f"근거 {b} {n}" for b, n in (entry.get("지문") or {}).get("출처") or []]
|
||||
ok = all(t["배"] is not None and t["환산"]["갈래"] != "추정" for t in targets)
|
||||
out = {"상태": state, "체크": state == "맞음" and bool(targets) and ok, "대상": targets}
|
||||
out.update(후보=entry.get("후보") or [], 글=judged["글"], 꼬리표=tags, 품=_qty(entry, found))
|
||||
if found and found.get("갈리는칸"):
|
||||
out["갈리는칸"] = found["갈리는칸"]
|
||||
return out
|
||||
|
||||
|
||||
def _names_only(line: dict, ctx: Ctx, why: str, full: bool) -> dict:
|
||||
"""⑦ 숫자 판정 없음 · 셈 전 — 이름 · 근거 후보(차례만 · 대상 = 1위)."""
|
||||
if not full:
|
||||
return {"상태": "후보", "체크": False, "대상": [], "후보": [], "글": why, "꼬리표": [why]}
|
||||
order = hm.b_order(line, ctx.got)
|
||||
top = order["top"]
|
||||
cands = [{**c, "판정": "후보", "글": why} for c in order["후보"]]
|
||||
targets = [_target(top[0], line, ctx, None, False)] if top else []
|
||||
state = "후보" if top else "없음"
|
||||
return {
|
||||
"상태": state,
|
||||
"체크": False,
|
||||
"대상": targets,
|
||||
"후보": cands,
|
||||
"글": why,
|
||||
"꼬리표": [why],
|
||||
}
|
||||
|
||||
|
||||
def _material(line: dict, ctx: Ctx, full: bool) -> dict:
|
||||
found = hm._exact(line, "자재", ctx.got)
|
||||
if len(found) == 1:
|
||||
one = found[0]
|
||||
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
|
||||
target = {**hm._public(one), "입력값": {}, "못읽음": [], "출처": {}, "환산": conv}
|
||||
target.update(몫=None, 배=conv["배"])
|
||||
ok = conv["배"] is not None and conv["갈래"] != "추정"
|
||||
return {"상태": "맞음", "체크": ok, "대상": [target], "후보": [], "글": "이름 · 규격 같음"}
|
||||
cands = [{**c, "판정": "후보"} for c in hm.candidates(line, "자재", ctx.got, 5)] if full else []
|
||||
return {
|
||||
"상태": "후보" if cands or not full else "없음",
|
||||
"체크": False,
|
||||
"대상": [],
|
||||
"후보": cands,
|
||||
}
|
||||
|
||||
|
||||
def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]:
|
||||
"""줄 → 맞춤(설계 3-3) — `full` 아니면 상태 · 체크만(호표 목록 수)."""
|
||||
out: dict[str, Any] = {"열쇠": hm.line_key(line), "체크": False, "대상": [], "후보": []}
|
||||
kind = finger.kind_of(line)
|
||||
ref = line.get("참조") or {}
|
||||
if kind == "%":
|
||||
return {**out, "상태": "%", "글": "% 줄 — 우리 로직 덧줄이 셈"}
|
||||
if kind == "호표" or ref.get("종류") == "일위":
|
||||
no = int(ref.get("번") or 0)
|
||||
h = ctx.units.get(no) or {}
|
||||
conv = hm.factor(line.get("단위"), h.get("단위"), line.get("규격"))
|
||||
made = ctx.done.get(hopyo_build.mark(ctx.key, no))
|
||||
target = {"호표": no, "이름": h.get("이름", ""), "단위": h.get("단위"), "입력값": {}}
|
||||
target.update(못읽음=[], 출처={}, 환산=conv, 몫=None, 배=conv["배"])
|
||||
ok = bool(made) and conv["배"] is not None and conv["갈래"] != "추정"
|
||||
text = f"확인된 사전조합 {made}" if made else f"제{no}호표 먼저 확인"
|
||||
return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text}
|
||||
if kind == "노임":
|
||||
return {**out, "상태": "노임", "글": "노임 · 그릇 없음"}
|
||||
alias = ledger.recall(line, ctx.to, ctx.books)
|
||||
entry = ctx.cache.get(fit.unit_key(line)) if kind not in hm.MATERIAL_KINDS else None
|
||||
if alias and alias.get("갈래") in SKIP:
|
||||
return {**out, "상태": "뺌", "뺌": alias["갈래"], "별칭": _alias_info(alias, False)}
|
||||
if alias and alias.get("갈래") == "짝" and alias.get("짝"):
|
||||
return {**out, **_aliased(line, ctx, alias, entry)}
|
||||
if kind in hm.MATERIAL_KINDS:
|
||||
return {**out, **_material(line, ctx, full)}
|
||||
if entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"):
|
||||
return {**out, **(_numeric(line, ctx, entry) if full else _brief(entry))}
|
||||
why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER
|
||||
return {**out, **_names_only(line, ctx, why, full)}
|
||||
|
||||
|
||||
def _brief(entry: dict) -> dict:
|
||||
state = entry["판정"]["상태"]
|
||||
return {"상태": state, "체크": state == "맞음", "글": entry["판정"]["글"]}
|
||||
|
||||
|
||||
def _alias_info(alias: dict, basis_off: bool) -> dict:
|
||||
keep = ("층", "주인", "때", "누가", "처음파일", "규격다름")
|
||||
return {**{k: alias.get(k) for k in keep}, "근거다름": basis_off}
|
||||
|
||||
|
||||
def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict:
|
||||
"""⑤ 장부 짝 — 체크 = 규격 같음 · 근거 같음 · 숫자 판정 맞음이 다른 로직 아님."""
|
||||
targets = [_pair(p, line, ctx) for p in alias["짝"]]
|
||||
codes = {t["로직"] for t in targets if t.get("로직")}
|
||||
theirs = ((line.get("근거") or {}).get("출처")) or []
|
||||
table = hopyo_basis.ours()
|
||||
off = bool(theirs and codes) and all(
|
||||
hm.clause(theirs, table[c])[0] < 0.9 for c in codes if c in table
|
||||
)
|
||||
other = bool(entry and entry["판정"]["상태"] == "맞음" and entry["차례"][0] not in codes)
|
||||
check = not alias.get("규격다름") and not off and not other
|
||||
tags = ["별칭 — " + " · ".join(str(x) for x in (alias.get("층"), alias.get("때")) if x)]
|
||||
tags += ["규격 다름"] * bool(alias.get("규격다름")) + ["근거 다름"] * off
|
||||
tags += [f"숫자 판정 맞음 = {entry['차례'][0]}"] if other else []
|
||||
out = {"상태": "별칭", "체크": check, "대상": targets, "글": "별칭 장부 짝", "꼬리표": tags}
|
||||
out["별칭"] = _alias_info(alias, off)
|
||||
if entry and entry.get("품"):
|
||||
out["품"] = _qty(entry, (entry.get("옵션") or {}).get(next(iter(codes), None)))
|
||||
return out
|
||||
|
||||
|
||||
def whole(hopyo: dict, ctx: Ctx) -> dict | None:
|
||||
"""직접 줄 호표 통째 제안 — 장부 짝(호표 열쇠) 먼저 · 맞춤셈 「호표:번」."""
|
||||
head = {k: hopyo.get(k) for k in ("이름", "규격", "단위")}
|
||||
alias = ledger.recall(head, ctx.to, ctx.books, whole=True)
|
||||
if alias and alias.get("갈래") == "짝" and alias.get("짝"):
|
||||
return {
|
||||
"대상": [_pair(p, head, ctx) for p in alias["짝"]],
|
||||
"글": "별칭 장부 짝",
|
||||
"체크": True,
|
||||
}
|
||||
entry = ctx.cache.get(f"호표:{hopyo['번']}")
|
||||
if not entry or not entry.get("차례"):
|
||||
return None
|
||||
top = entry["차례"][0]
|
||||
state = entry["판정"]["상태"]
|
||||
target = _target(top, head, ctx, entry["옵션"].get(top), state in ("맞음", "다른 점"))
|
||||
return {"대상": [target], "글": f"통째 = {entry['판정']['글']}", "체크": state == "맞음"}
|
||||
|
||||
|
||||
def hopyo_view(hopyo: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]:
|
||||
"""호표 하나 — 줄마다 맞춤 · `맞춤` = [체크 제안 수, 줄 수] · 통째 · 셈 · 만든."""
|
||||
lines = [{**one, "맞춤": match_line(one, ctx, full)} for one in hopyo.get("줄") or []]
|
||||
check = sum(1 for one in lines if one["맞춤"]["체크"])
|
||||
out = {**hopyo, "줄": lines, "맞춤": [check, len(lines)]}
|
||||
if full:
|
||||
made = ctx.done.get(hopyo_build.mark(ctx.key, int(hopyo["번"])))
|
||||
out.update(통째=whole(hopyo, ctx), 셈=ctx.calc)
|
||||
out["만든"] = {"키": made, "확인": True} if made else None
|
||||
return out
|
||||
|
||||
|
||||
def guess(
|
||||
folder: Path, key: str, no: int, at: int, target: Any, to: Path | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""미리 셈(저장 안 함) — 고른 대상 · 옵션으로 엔진 한 번 → {대상, 품: {엑셀, 우리, 같음},
|
||||
글}."""
|
||||
ctx = Ctx(folder, key, to)
|
||||
hopyo = ctx.units.get(int(no))
|
||||
lines = (hopyo or {}).get("줄") or []
|
||||
if not 0 <= int(at) < len(lines):
|
||||
raise store.StoreError(404, f"없는 줄 「제{no}호표 {at}」")
|
||||
line = lines[int(at)]
|
||||
picked = target if isinstance(target, list) else [target]
|
||||
if not picked or not all(isinstance(t, dict) for t in picked):
|
||||
raise store.StoreError(400, "대상 꼴이 틀림")
|
||||
entry = ctx.cache.get(fit.unit_key(line))
|
||||
if entry is None:
|
||||
block = next(
|
||||
(
|
||||
b
|
||||
for b in ctx.doc.get("단산") or []
|
||||
if b.get("번") == (line.get("참조") or {}).get("번")
|
||||
),
|
||||
None,
|
||||
)
|
||||
entry = {"품": verdict.excel_qty(finger.excel(block, line))}
|
||||
targets, total = [], {"L": {}, "X": [], "M": {}, "S": {}}
|
||||
for t in picked:
|
||||
if t.get("로직"):
|
||||
if t["로직"] not in ctx.names:
|
||||
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
|
||||
given = t.get("입력값")
|
||||
one = _target(t["로직"], line, ctx, None, False, 몫=t.get("몫"))
|
||||
if isinstance(given, dict):
|
||||
one["입력값"] = {**one["입력값"], **given}
|
||||
one["출처"].update({k: "고름" for k in given})
|
||||
v = ours.run(t["로직"], one["입력값"]) or {"L": {}, "X": [], "M": {}, "S": {}}
|
||||
k = float(t.get("몫") or 1)
|
||||
for name, q in v["L"].items():
|
||||
total["L"][name] = total["L"].get(name, 0.0) + q * k
|
||||
total["X"] += [[a, b, q * k] for a, b, q in v["X"]]
|
||||
for name, q in v["M"].items():
|
||||
total["M"][name] = total["M"].get(name, 0.0) + q * k
|
||||
targets.append(one)
|
||||
elif t.get("자재") in ctx.mats:
|
||||
targets.append(_pair({"자재": t["자재"], "배": t.get("배")}, line, ctx))
|
||||
else:
|
||||
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
|
||||
qty = _qty(entry, {"우리품": option.ours_qty(total)})
|
||||
return {"대상": targets, "품": qty, "글": "우리 품 — 엔진 한 번(저장 안 함)"}
|
||||
@@ -1,18 +1,14 @@
|
||||
"""M02 일위대가 실무자료 맞춤(PLAN 324-2) — 엑셀 구성 줄 → 우리 돈 로직 · 자재 · 입력값 · 환산.
|
||||
"""M02 일위대가 실무자료 맞춤(PLAN 324-2 · 338) — 엑셀 구성 줄 → 우리 로직 · 자재 · 입력값 · 환산.
|
||||
|
||||
차례 = ① 층 기억(같은 층 → 위 층 · 확정분) ② 이름 같음(정규화) ·
|
||||
씨앗(일위대가조합.json 메모 · 읽기만)
|
||||
③ 정리 이름 같음(괄호 · 기계 낱말) ④ 후보 5(글자 두 개 묶음 점수 + 단위 맞음 + 산림품셈 덤 ·
|
||||
뜻 낱말이 하나도 안 겹치면 뺌 — 「잔토처리」 ↔ 「소금처리」) —
|
||||
+ 근거 후보(330-2 · `M02_Hopyo_Basis` · 단산 줄 출처 · 직종 · 이름 거름 안 거침 ·
|
||||
로직 키로 합쳐 점수 더함) ·
|
||||
③ ④ 는 사용자 확정(층 기억) 뒤에만 들어감.
|
||||
줄 갈래: 단가산출 · 기계 · 기타 = 로직 / 자재 · 별도계상 = 자재(M01 자재품목) / 호표 = 그 호표 /
|
||||
% = 안 들어감(로직 덧줄이 셈).
|
||||
이름 · 근거 차례(`b_order` · 338 설계 2-4 B) = 이름 같음(정규화) · 씨앗(일위대가조합.json 메모 ·
|
||||
읽기만)이 1위 · 단 엑셀 인용 조항이 다른 로직을 가리키면(근거 검사 · 옛 판 글이면 그대로) 후보 ·
|
||||
그 밖 = 이름 후보(글자 두 개 묶음 + 단위 + 산림 덤 · 뜻 낱말 안 겹치면 뺌 · 로직은 접지 않음) +
|
||||
근거 후보(`M02_Hopyo_Basis` · 조항 같음 · 윗마디 · 직종) 합침 — 숫자 맞춤(`M02_Hopyo_Fit`)의 덤.
|
||||
줄 · 호표 맞춤 응답(상태 · 체크 제안 · 대상 · 품 비교) = `M02_Hopyo_Line` · 별칭 장부 = 다 창.
|
||||
규격 → 입력값 = 고르기 글이 규격(품명 괄호 글 포함)에 있으면 그 값(기계 = 고르기이름) ·
|
||||
이름 붙은 수 D= H= H: B= T= L= L3= 는 입력 단위로 · 못 읽으면 빈 값(자동값) + 표시.
|
||||
환산(우리 수량 = 엑셀 수량 × 배): 같은 단위 1 · 같은 성격 = 단위표 · ㎡→㎥ × T · m→㎡ × B ·
|
||||
m→㎥ × B × T = 「추정」(확정 뒤에만) · 두 단위 로직 = 단위 입력으로 고르고 1 · 그 밖 = 사용자 입력.
|
||||
m→㎥ × B × T = 「추정」(체크 안 함) · 두 단위 로직 = 단위 입력으로 고르고 1 · 그 밖 = 사용자 입력.
|
||||
M01 은 읽기만(로직 별칭 안 씀).
|
||||
"""
|
||||
|
||||
@@ -21,7 +17,6 @@ from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
@@ -41,7 +36,6 @@ mm = mp.mm
|
||||
SEED_NAME = "일위대가조합.json"
|
||||
LOGIC_KINDS = ("단가산출", "기계", "기타")
|
||||
MATERIAL_KINDS = ("자재", "별도계상")
|
||||
READY = ("기억", "같음", "호표")
|
||||
MACH_WORDS = (
|
||||
("불도우저", "불도저"),
|
||||
("굴삭기", "굴착기"),
|
||||
@@ -377,41 +371,108 @@ def _strong_basis(line: dict[str, Any], cand: dict[str, Any]) -> bool:
|
||||
|
||||
|
||||
def b_order(line: dict[str, Any], got: dict[str, Any] | None = None, text: Any = ()) -> dict:
|
||||
"""이름 · 근거 차례 `{상태: 같음 | 후보 | 없음, top: [로직 5], 합침: [로직 15]}` — 이름 후보 +
|
||||
근거 후보 합침(접힘 없음) · 「같음」도 근거 검사(인용 조항이 다른 로직을 가리키면 후보) ·
|
||||
옛 판 글이면 같음 그대로 · 씨앗은 이름 같음 자리."""
|
||||
"""이름 · 근거 차례 `{상태: 같음 | 후보 | 없음, top: [로직 5], 합침: [로직 15], 후보: [5],
|
||||
근거: {로직: 근거}}` — 이름 후보 + 근거 후보 합침(접힘 없음) · 「같음」도 근거 검사(인용 조항이
|
||||
다른 로직을 가리키면 후보) · 옛 판 글이면 같음 그대로 · 씨앗은 이름 같음 자리."""
|
||||
got = got or targets()
|
||||
basis_ = dict(line.get("근거") or {})
|
||||
if basis_.get("출처"):
|
||||
line = {**line, "근거": {**basis_, "출처": fix_typo(basis_["출처"])}}
|
||||
basis = hopyo_basis.candidates(line, got)
|
||||
named = candidates(line, "로직", got, None)
|
||||
merged = [c.get("로직") for c in hopyo_basis.merge(basis, named) if c.get("로직")]
|
||||
merged_c = [c for c in hopyo_basis.merge(basis, named) if c.get("로직")]
|
||||
merged = [c["로직"] for c in merged_c]
|
||||
why = {c["로직"]: c["근거"] for c in merged_c if c.get("근거")}
|
||||
by = {c["로직"]: c for c in merged_c}
|
||||
pub = {x["로직"]: x for x in got["로직"]}
|
||||
|
||||
def done(state: str, top: list[str]) -> dict:
|
||||
cands = [by.get(k) or {**_public(pub[k]), "점수": None} for k in top[:5] if k in pub]
|
||||
return {"상태": state, "top": top[:5], "합침": merged[:15], "후보": cands, "근거": why}
|
||||
|
||||
found = _exact(line, "로직", got)
|
||||
if len(found) == 1:
|
||||
tgt = found[0]["로직"]
|
||||
theirs = (line.get("근거") or {}).get("출처") or []
|
||||
our = hopyo_basis.ours().get(tgt)
|
||||
if _OLD_TEXT.search(" ".join(str(t) for t in list(text)[:5])) or not theirs:
|
||||
return {"상태": "같음", "top": [tgt], "합침": merged[:15]}
|
||||
return done("같음", [tgt])
|
||||
if our and clause(theirs, our)[0] >= 0.9:
|
||||
return {"상태": "같음", "top": [tgt], "합침": merged[:15]}
|
||||
strong = [c for c in basis if c["근거"]["까닭"].startswith("조항 같음")]
|
||||
return done("같음", [tgt])
|
||||
strong = [c for c in basis if c["근거"]["까닭"].startswith(("조항 같음", "윗마디 같음"))]
|
||||
strong = [c for c in strong if _strong_basis(line, c)]
|
||||
if not strong:
|
||||
return {"상태": "같음", "top": [tgt], "합침": merged[:15]}
|
||||
return done("같음", [tgt])
|
||||
top = list(dict.fromkeys([strong[0]["로직"], tgt] + [c["로직"] for c in strong[1:]]))
|
||||
return {"상태": "후보", "top": top[:5], "합침": merged[:15]}
|
||||
return done("후보", top)
|
||||
top = [x["로직"] for x in found]
|
||||
rest = [c for c in hopyo_basis.merge(basis, named) if c.get("로직") not in top]
|
||||
rest = [c for c in merged_c if c["로직"] not in top]
|
||||
first = next((c.get("로직") for c in named if c.get("로직") not in top), None)
|
||||
top += [c["로직"] for c in hopyo_basis.pick(rest, first if basis else None, 5 - len(top))]
|
||||
return {"상태": "후보" if top else "없음", "top": top[:5], "합침": merged[:15]}
|
||||
return done("후보" if top else "없음", top)
|
||||
|
||||
|
||||
# ── 줄 · 호표 맞춤(338 · 설계 3-3 · 3-4 — 몸은 `M02_Hopyo_Line`) ─────────────────
|
||||
def _logic_specs(code: str) -> list[dict[str, Any]]:
|
||||
got = labor_inputs.logic_inputs([code])["로직"]
|
||||
return got[0]["입력"] if got else []
|
||||
|
||||
|
||||
def _line_mod():
|
||||
from M02_MasterTemplete import M02_Hopyo_Line as line_mod # 줄 맞춤이 이 파일을 부름
|
||||
|
||||
return line_mod
|
||||
|
||||
|
||||
def units_of(doc: dict[str, Any]) -> dict[int, str]:
|
||||
return {int(h["번"]): str(h.get("단위") or "") for h in doc.get("호표") or []}
|
||||
|
||||
|
||||
def view(folder: Path, key: str, no: int | None = None, to: Path | None = None) -> dict[str, Any]:
|
||||
"""실무자료 보기 — `no` 없으면 호표마다 [체크 제안 수, 줄 수] · 있으면 그 호표 줄마다 맞춤(3-3)
|
||||
·
|
||||
`to` = 만들 층(장부 · 확인된 사전조합 · 없으면 파일 층) · 맞춤셈이 없으면 뒤 셈을 넣음."""
|
||||
lines = _line_mod()
|
||||
ctx = lines.Ctx(folder, key, to)
|
||||
doc = ctx.doc
|
||||
if no is None:
|
||||
hopyos = [lines.hopyo_view(h, ctx, False) for h in doc.get("호표") or []]
|
||||
keep = ("번", "이름", "규격", "단위", "금액", "맞춤", "행")
|
||||
brief = [{k: h.get(k) for k in keep if k in h} for h in hopyos]
|
||||
ready = [sum(h["맞춤"][0] for h in hopyos), sum(h["맞춤"][1] for h in hopyos)]
|
||||
head = {**hopyo_store.brief(doc), "검산": doc.get("검산"), "셈": ctx.calc}
|
||||
return {**head, "호표": brief, "맞춤": ready}
|
||||
hopyo = ctx.units.get(int(no))
|
||||
if hopyo is None:
|
||||
raise store.StoreError(404, f"없는 호표 「제{no}호표」")
|
||||
return {**hopyo_store.brief(doc), "호표": lines.hopyo_view(hopyo, ctx)}
|
||||
|
||||
|
||||
def guess(
|
||||
folder: Path, key: str, no: int, at: int, target: Any, to: Path | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""미리 셈(3-4 · 저장 안 함) — 대상 목록(낱개도 받음) → {대상, 품, 글}."""
|
||||
return _line_mod().guess(folder, key, no, at, target, to)
|
||||
|
||||
|
||||
def picks(folder: Path) -> list[dict[str, Any]]:
|
||||
"""[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 실무자료 호표마다 맞춤 [체크 제안 수, 줄 수]
|
||||
(장부 · 확인된 사전조합 = 이 층)."""
|
||||
lines, out = _line_mod(), []
|
||||
for one in chain(folder):
|
||||
layer, owner = refs.where(one)
|
||||
for row in hopyo_store.rows(one):
|
||||
ctx = lines.Ctx(one, row["키"], folder)
|
||||
for hopyo in ctx.doc.get("호표") or []:
|
||||
ready = lines.hopyo_view(hopyo, ctx, False)["맞춤"]
|
||||
head = {"층": layer, "주인": owner, "키": row["키"], "파일": row["파일"]}
|
||||
keep = ("번", "이름", "규격", "단위")
|
||||
out.append({**head, **{k: hopyo.get(k) for k in keep}, "맞춤": ready})
|
||||
return out
|
||||
|
||||
|
||||
# ── 층 기억 ───────────────────────────────────────────────────────────
|
||||
def chain(folder: Path) -> list[Path]:
|
||||
"""기억을 볼 층 폴더 — 같은 층 → 위 층(개인 → 회사 → 마스터)."""
|
||||
"""실무자료를 볼 층 폴더 — 같은 층 → 위 층(개인 → 회사 → 마스터)."""
|
||||
layer, owner = refs.where(folder)
|
||||
out = [Path(folder)]
|
||||
if layer == "personal":
|
||||
@@ -419,196 +480,3 @@ def chain(folder: Path) -> list[Path]:
|
||||
if layer in ("personal", "company"):
|
||||
out.append(Path(store.FOLDER))
|
||||
return out
|
||||
|
||||
|
||||
def memories(folder: Path) -> list[dict[str, dict[str, Any]]]:
|
||||
return [hopyo_store.memory(one) for one in chain(folder)]
|
||||
|
||||
|
||||
def recall(line: dict[str, Any], memo: list[dict[str, dict[str, Any]]]) -> dict[str, Any] | None:
|
||||
key = line_key(line)
|
||||
return next((one[key] for one in memo if key in one), None)
|
||||
|
||||
|
||||
# ── 줄 하나 맞춤 ───────────────────────────────────────────────────────
|
||||
def _logic_specs(code: str) -> list[dict[str, Any]]:
|
||||
got = labor_inputs.logic_inputs([code])["로직"]
|
||||
return got[0]["입력"] if got else []
|
||||
|
||||
|
||||
def _target_of(remembered: dict[str, Any], got: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if remembered.get("로직"):
|
||||
return next((x for x in got["로직"] if x["로직"] == remembered["로직"]), None)
|
||||
if remembered.get("자재"):
|
||||
return next((x for x in got["자재"] if x["자재"] == remembered["자재"]), None)
|
||||
return None
|
||||
|
||||
|
||||
def match_line(
|
||||
line: dict[str, Any],
|
||||
memo: list[dict[str, dict[str, Any]]],
|
||||
units: dict[int, str],
|
||||
full: bool = True,
|
||||
got: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""줄 → `{열쇠, 상태, 대상, 입력값, 못읽음, 환산, 들어감, 후보?}` —
|
||||
`units` = {호표 번: 단위}(일위N 참조) · `full` = 입력값 · 후보까지(아니면 상태 · 환산만)."""
|
||||
out: dict[str, Any] = {"열쇠": line_key(line), "대상": None, "입력값": {}, "못읽음": []}
|
||||
kind = line.get("갈래")
|
||||
pool = "자재" if kind in MATERIAL_KINDS else "로직"
|
||||
remembered = recall(line, memo)
|
||||
got = got or targets()
|
||||
if kind == "%":
|
||||
return {**out, "상태": "%", "환산": None, "들어감": False}
|
||||
if remembered and remembered.get("빼기"):
|
||||
return {**out, "상태": "빼기", "환산": None, "들어감": False}
|
||||
if kind == "호표":
|
||||
no = int((line.get("참조") or {}).get("번") or 0)
|
||||
conv = factor(line.get("단위"), units.get(no), line.get("규격"))
|
||||
target = {"호표": no, "단위": units.get(no)}
|
||||
ready = no in units and conv["배"] is not None and conv["갈래"] != "추정"
|
||||
return {**out, "상태": "호표", "대상": target, "환산": conv, "들어감": ready}
|
||||
if remembered and (target := _target_of(remembered, got)):
|
||||
conv = {"배": remembered.get("환산"), "갈래": "기억"}
|
||||
state = {"상태": "기억", "대상": _public(target), "입력값": remembered.get("입력값") or {}}
|
||||
return {**out, **state, "환산": conv, "들어감": conv["배"] is not None}
|
||||
found = _exact(line, pool, got)
|
||||
if len(found) == 1:
|
||||
target = found[0]
|
||||
conv = factor(line.get("단위"), target["단위"], line.get("규격"))
|
||||
out.update(상태="같음", 대상=_public(target), 환산=conv)
|
||||
out["들어감"] = conv["배"] is not None and conv["갈래"] != "추정"
|
||||
if full and "로직" in target:
|
||||
out["입력값"], out["못읽음"] = spec_inputs(line, _logic_specs(target["로직"]))
|
||||
return out
|
||||
out.update(상태="후보" if found else "없음", 환산=None, 들어감=False)
|
||||
if not full:
|
||||
return out
|
||||
top = [{**_public(x), "점수": 9} for x in found]
|
||||
same = [_public(x) for x in found]
|
||||
basis = hopyo_basis.candidates(line, got) if pool == "로직" else []
|
||||
named = candidates(line, pool, got, None if basis else 5)
|
||||
rest = [c for c in hopyo_basis.merge(basis, named) if _public(c) not in same]
|
||||
first = next((c.get("로직") for c in named if _public(c) not in same), None)
|
||||
out["후보"] = top[:5] + hopyo_basis.pick(rest, first if basis else None, 5 - len(top))
|
||||
return {**out, "상태": "후보" if out["후보"] else "없음"}
|
||||
|
||||
|
||||
def guess(folder: Path, key: str, no: int, at: int, target: dict[str, Any]) -> dict[str, Any]:
|
||||
"""후보 · 고른 대상 하나 미리 채움 — `{대상, 입력값, 못읽음, 환산}`(확정 전 화면 보기)."""
|
||||
line = _line(folder, key, no, at)
|
||||
found = _target_of(target if isinstance(target, dict) else {}, targets())
|
||||
if found is None:
|
||||
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
|
||||
target = _public(found)
|
||||
conv = factor(line.get("단위"), target.get("단위"), line.get("규격"))
|
||||
if not target.get("로직"):
|
||||
return {"대상": target, "입력값": {}, "못읽음": [], "환산": conv}
|
||||
values, unread = spec_inputs(line, _logic_specs(str(target["로직"])))
|
||||
return {"대상": target, "입력값": values, "못읽음": unread, "환산": conv}
|
||||
|
||||
|
||||
def _line(folder: Path, key: str, no: int, at: int) -> dict[str, Any]:
|
||||
doc = hopyo_store.read(folder, key)
|
||||
hopyo = next((h for h in doc.get("호표") or [] if int(h["번"]) == int(no)), None)
|
||||
lines = (hopyo or {}).get("줄") or []
|
||||
if not 0 <= int(at) < len(lines):
|
||||
raise store.StoreError(404, f"없는 줄 「제{no}호표 {at}」")
|
||||
return lines[int(at)]
|
||||
|
||||
|
||||
def match_hopyo(
|
||||
hopyo: dict[str, Any],
|
||||
memo: list[dict[str, dict[str, Any]]],
|
||||
units: dict[int, str],
|
||||
full: bool = True,
|
||||
got: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""호표 하나 — 줄마다 맞춤 · `맞춤` = [들어감 수, 줄 수]."""
|
||||
got = got or targets()
|
||||
lines = [
|
||||
{**one, "맞춤": match_line(one, memo, units, full, got)} for one in hopyo.get("줄") or []
|
||||
]
|
||||
ready = sum(1 for one in lines if one["맞춤"]["들어감"])
|
||||
return {**hopyo, "줄": lines, "맞춤": [ready, len(lines)]}
|
||||
|
||||
|
||||
def units_of(doc: dict[str, Any]) -> dict[int, str]:
|
||||
return {int(h["번"]): str(h.get("단위") or "") for h in doc.get("호표") or []}
|
||||
|
||||
|
||||
def view(folder: Path, key: str, no: int | None = None) -> dict[str, Any]:
|
||||
"""실무자료 보기 — `no` 없으면 호표마다 맞춤 수만 · 있으면 그 호표 줄마다 맞춤 · 후보."""
|
||||
doc = hopyo_store.read(folder, key)
|
||||
memo, units = memories(folder), units_of(doc)
|
||||
if no is None:
|
||||
got = targets()
|
||||
hopyos = [match_hopyo(h, memo, units, False, got) for h in doc.get("호표") or []]
|
||||
keep = ("번", "이름", "규격", "단위", "금액", "맞춤")
|
||||
brief = [{k: h.get(k) for k in keep} for h in hopyos]
|
||||
ready = [sum(h["맞춤"][0] for h in hopyos), sum(h["맞춤"][1] for h in hopyos)]
|
||||
return {**hopyo_store.brief(doc), "검산": doc.get("검산"), "호표": brief, "맞춤": ready}
|
||||
hopyo = next((h for h in doc.get("호표") or [] if int(h["번"]) == int(no)), None)
|
||||
if hopyo is None:
|
||||
raise store.StoreError(404, f"없는 호표 「제{no}호표」")
|
||||
return {**hopyo_store.brief(doc), "호표": match_hopyo(hopyo, memo, units)}
|
||||
|
||||
|
||||
def picks(folder: Path) -> list[dict[str, Any]]:
|
||||
"""[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 실무자료 호표마다 맞춤 n/m
|
||||
(이 층 기억 · 만드는 층)."""
|
||||
memo, got, out = memories(folder), targets(), []
|
||||
for one in chain(folder):
|
||||
layer, owner = refs.where(one)
|
||||
for row in hopyo_store.rows(one):
|
||||
doc = hopyo_store.read(one, row["키"])
|
||||
units = units_of(doc)
|
||||
for hopyo in doc.get("호표") or []:
|
||||
ready = match_hopyo(hopyo, memo, units, False, got)["맞춤"]
|
||||
head = {"층": layer, "주인": owner, "키": row["키"], "파일": row["파일"]}
|
||||
keep = ("번", "이름", "규격", "단위")
|
||||
out.append({**head, **{k: hopyo.get(k) for k in keep}, "맞춤": ready})
|
||||
return out
|
||||
|
||||
|
||||
# ── 확정 저장 ─────────────────────────────────────────────────────────
|
||||
def confirm(folder: Path, entries: dict[str, Any]) -> dict[str, Any]:
|
||||
"""확정 · 빼기 · 지움 → 층 기억 — `{열쇠: {로직|자재|빼기, 입력값, 환산} | None}`.
|
||||
로직 = 돈 로직 · 자재 = M01 자재품목 · 환산 = 0 넘는 수(빼기는 안 봄)."""
|
||||
if not isinstance(entries, dict) or not entries:
|
||||
raise store.StoreError(400, "확정할 줄이 없음")
|
||||
got = targets()
|
||||
logics = {x["로직"] for x in got["로직"]}
|
||||
mats = {x["자재"] for x in got["자재"]}
|
||||
stamp = datetime.now().isoformat(timespec="seconds")
|
||||
clean: dict[str, Any] = {}
|
||||
for key, raw in entries.items():
|
||||
if raw is None:
|
||||
clean[str(key)] = None
|
||||
continue
|
||||
if not isinstance(raw, dict) or str(key).count("|") != 2:
|
||||
raise store.StoreError(400, f"확정 꼴이 틀림 「{key}」")
|
||||
if raw.get("빼기") is True:
|
||||
clean[str(key)] = {"빼기": True, "확정": stamp}
|
||||
continue
|
||||
code, mat = str(raw.get("로직") or ""), str(raw.get("자재") or "")
|
||||
if (code and code not in logics) or (mat and mat not in mats) or bool(code) == bool(mat):
|
||||
raise store.StoreError(400, f"돈 로직 · 우리 자재 하나만 「{key}」")
|
||||
times = raw.get("환산")
|
||||
number = None if isinstance(times, bool) else _number(times)
|
||||
if number is None or number <= 0:
|
||||
raise store.StoreError(400, f"환산은 0 넘는 수 「{key}」")
|
||||
values = raw.get("입력값") or {}
|
||||
if not isinstance(values, dict):
|
||||
raise store.StoreError(400, f"입력값이 묶음 아님 「{key}」")
|
||||
head = {"로직": code} if code else {"자재": mat}
|
||||
clean[str(key)] = {**head, "입력값": values, "환산": _json(number), "확정": stamp}
|
||||
return {"기억": hopyo_store.remember(folder, clean)}
|
||||
|
||||
|
||||
def _number(raw: Any) -> Decimal | None:
|
||||
try:
|
||||
got = Decimal(str(raw).replace(",", "").strip())
|
||||
except ArithmeticError:
|
||||
return None
|
||||
return got if got.is_finite() else None
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
블록 + 줄 → 엑셀 지문(`Finger`) → 이름 · 근거 차례(`Match.b_order`) → 지문 순위 섞기(`Fit`) ·
|
||||
같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve`) → 위 다섯 옵션
|
||||
거꾸로(`Option`) →
|
||||
위 다섯 가운데 딱 맞음(옵션까지)이 있는데 1위가 아니면 그것이 1위(조합 없는 줄) →
|
||||
판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표).
|
||||
글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음.
|
||||
"""
|
||||
@@ -117,6 +118,7 @@ def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dic
|
||||
top = [first] + [k for k in fused if k != first][:4]
|
||||
else:
|
||||
top = fused
|
||||
entry["지문차례"] = top[: fit.TOP_OPT] # 조합 · 딱 맞음 먼저 전(시험 지표)
|
||||
parts = efp.get("몫") or []
|
||||
if parts and btop and combine.is_mixed(parts, line): # 섞인 줄 — 이름 · 근거 차례로 몫 먼저
|
||||
top = btop[: fit.TOP_OPT]
|
||||
@@ -128,6 +130,11 @@ def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dic
|
||||
pfp = _part_fp(part, c["몫"], line) if part else efp
|
||||
opts[c["로직"]] = option.find(c["로직"], pfp, text) if pfp["인력"] else None
|
||||
theirs = efp["출처"]
|
||||
exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"]
|
||||
if (
|
||||
not combo and exact and exact[0] != top[0]
|
||||
): # 딱 맞음 먼저(K5) — 1위가 못 맞추면 위 다섯 딱 맞음
|
||||
top = [exact[0]] + [k for k in top if k != exact[0]]
|
||||
chans = {k: ch for _, k, ch, _ in pool}
|
||||
entry["순위"] = [
|
||||
{
|
||||
@@ -142,7 +149,7 @@ def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dic
|
||||
entry["옵션"] = opts
|
||||
entry["조합"] = combo or None
|
||||
entry["판정"] = judge(efp, top, opts, theirs, got)
|
||||
entry["후보"] = candidates(efp, top, opts, theirs, got, scores)
|
||||
entry["후보"] = candidates(efp, top, opts, theirs, got, scores, border.get("근거") or {})
|
||||
return entry
|
||||
|
||||
|
||||
@@ -170,7 +177,7 @@ def judge(efp: dict, top: list[str], opts: dict, theirs: list, got: dict) -> dic
|
||||
|
||||
|
||||
def candidates(
|
||||
efp: dict, top: list[str], opts: dict, theirs: list, got: dict, scores: dict
|
||||
efp: dict, top: list[str], opts: dict, theirs: list, got: dict, scores: dict, why: dict
|
||||
) -> list[dict[str, Any]]:
|
||||
"""후보 다섯 — 로직마다 옵션 판정 꼬리표 · 「맞음 둘」이면 둘째 맞음이 맨 앞."""
|
||||
by = {x["로직"]: x for x in got["로직"]}
|
||||
@@ -180,8 +187,10 @@ def candidates(
|
||||
found = opts.get(k) or {}
|
||||
state = _state(efp, k, opts.get(k), theirs)
|
||||
our = table.get(k)
|
||||
sc, clauses, why = hopyo_match.clause(theirs, our) if (our and theirs) else (0, [], "")
|
||||
sc, clauses, tag = hopyo_match.clause(theirs, our) if (our and theirs) else (0, [], "")
|
||||
one = by.get(k) or {"로직": k, "이름": "", "단위": ""}
|
||||
unit = hopyo_match.fits(efp["단위"], one["단위"])
|
||||
basis = why.get(k) or {"조항": clauses, "직종": [], "단위": unit, "점수": sc, "까닭": tag}
|
||||
text = state if state != "다른 점" else " · ".join(["다른 점", *_diffs(efp, found)[:1]])
|
||||
out.append(
|
||||
{
|
||||
@@ -191,7 +200,7 @@ def candidates(
|
||||
"점수": round(scores[k], 3) if k in scores else None,
|
||||
"판정": state,
|
||||
"글": text,
|
||||
"근거": {"조항": clauses, "까닭": why},
|
||||
"근거": basis,
|
||||
}
|
||||
)
|
||||
first = [c for c in out[1:] if c["판정"] == "맞음"][:1]
|
||||
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -24,6 +25,7 @@ from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # noqa: E402
|
||||
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
|
||||
DOCS = ROOT / "resources/knowledge/original/실무문서"
|
||||
FIXTURE = Path(__file__).resolve().parent / "fixtures/m02_hopyo_golden.json"
|
||||
FILES = {
|
||||
@@ -84,15 +86,24 @@ def read(tag: str) -> dict[str, Any] | None:
|
||||
return book
|
||||
|
||||
|
||||
def _key(line: dict[str, Any]) -> tuple:
|
||||
def _keys(line: dict[str, Any]) -> list[tuple]:
|
||||
"""줄 → 짝 열쇠 후보 — (이름 · 규격 · 단위 · 이음) · 이음 = 단산 번 · 줄 코드 · 없음."""
|
||||
ref = line.get("참조") or {}
|
||||
no = ref.get("번") if ref.get("종류") == "단산" else None
|
||||
code = str(ref.get("코드") or line.get("코드") or "")
|
||||
return line.get("이름"), line.get("규격") or "", line.get("단위") or "", no, code[:6]
|
||||
head = (line.get("이름"), line.get("규격") or "", line.get("단위") or "")
|
||||
code = str(ref.get("코드") or line.get("코드") or "")[:6]
|
||||
out = [(*head, ("단산", ref["번"]))] if ref.get("종류") == "단산" else []
|
||||
return out + ([(*head, ("코드", code))] if code else []) + [(*head, None)]
|
||||
|
||||
|
||||
def _lab_key(lab: dict[str, Any]) -> tuple:
|
||||
return lab["이름"], lab["규격"], lab["단위"], lab.get("단산"), str(lab.get("코드") or "")[:6]
|
||||
head = (lab["이름"], lab["규격"], lab["단위"])
|
||||
if lab.get("단산") is not None:
|
||||
return (*head, ("단산", lab["단산"]))
|
||||
return (*head, ("코드", str(lab["코드"])[:6]) if lab.get("코드") else None)
|
||||
|
||||
|
||||
def _find(want: dict[tuple, dict], line: dict[str, Any]) -> dict | None:
|
||||
return next((want[k] for k in _keys(line) if k in want), None)
|
||||
|
||||
|
||||
def line_of(lid: str) -> tuple[dict | None, dict[str, Any]]:
|
||||
@@ -102,9 +113,7 @@ def line_of(lid: str) -> tuple[dict | None, dict[str, Any]]:
|
||||
blocks = {int(b["번"]): b for b in book.get("단산") or []}
|
||||
for h in book["호표"]:
|
||||
for line in h["줄"]:
|
||||
if _key(line) == _lab_key(lab) or (
|
||||
not line.get("참조") and _key(line)[:3] == _lab_key(lab)[:3]
|
||||
):
|
||||
if _find({_lab_key(lab): lab}, line):
|
||||
ref = line.get("참조") or {}
|
||||
return (blocks.get(int(ref["번"])) if ref.get("종류") == "단산" else None), line
|
||||
raise KeyError(lid)
|
||||
@@ -123,7 +132,6 @@ def evaluate(tags: tuple[str, ...] | None = None) -> dict[str, Any]:
|
||||
continue
|
||||
out["읽음"].append(tag)
|
||||
want = {_lab_key(lab): lab for lab in labs if lab["파일"] == tag}
|
||||
loose = {k[:3]: lab for k, lab in want.items()}
|
||||
blocks = {int(b["번"]): b for b in book.get("단산") or []}
|
||||
t0 = time.time()
|
||||
for h in book["호표"]:
|
||||
@@ -132,7 +140,7 @@ def evaluate(tags: tuple[str, ...] | None = None) -> dict[str, Any]:
|
||||
ref = line.get("참조") or {}
|
||||
if ref.get("종류") == "일위" and ref.get("번"):
|
||||
refs.append(int(ref["번"]))
|
||||
lab = want.get(_key(line)) or (loose.get(_key(line)[:3]) if not ref else None)
|
||||
lab = _find(want, line)
|
||||
if lab is None:
|
||||
continue
|
||||
ids.append(lab["id"])
|
||||
@@ -141,6 +149,15 @@ def evaluate(tags: tuple[str, ...] | None = None) -> dict[str, Any]:
|
||||
block = blocks.get(int(ref["번"])) if ref.get("종류") == "단산" else None
|
||||
out["줄"][lab["id"]] = {"lab": lab, "셈": verdict.block(block, line, got)}
|
||||
out["호표"][f"{tag}|{h['번']}"] = (list(dict.fromkeys(ids)), refs)
|
||||
by_code = {str(b.get("코드") or "")[:6]: b for b in blocks.values() if b.get("코드")}
|
||||
for lab in want.values(): # 호표 줄이 안 부르는 블록(실험 C6 = 블록마다) — 블록 머리로 줄
|
||||
block = blocks.get(lab.get("단산") or -1) or by_code.get(str(lab.get("코드"))[:6])
|
||||
if lab["id"] in out["줄"] or block is None:
|
||||
continue
|
||||
basis = {"출처": block.get("출처") or [], "직종": block.get("직종") or []}
|
||||
line = {**{k: lab[k] for k in ("이름", "규격", "단위")}, "갈래": "단가산출"}
|
||||
line.update(참조={"종류": "단산", "번": block["번"]}, 근거=basis)
|
||||
out["줄"][lab["id"]] = {"lab": lab, "셈": verdict.block(block, line, got)}
|
||||
out["시간"][tag] = round(time.time() - t0, 1)
|
||||
return out
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ def test_출처_글():
|
||||
|
||||
|
||||
def _top(book: dict, no: int) -> list[dict]:
|
||||
got = hm.match_line(_line(book, no), [], hm.units_of(book))
|
||||
assert got["상태"] == "후보" and got["들어감"] is False # 자동 확정 없음
|
||||
got = hm.b_order(_line(book, no)) # 338 — 이름 · 근거 차례(숫자 판정의 덤)
|
||||
assert got["상태"] == "후보" # 자동 확정 없음
|
||||
return got["후보"]
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ def test_근거_후보(north):
|
||||
assert "GC000143" in [c["로직"] for c in _top(north, 64)[:2]]
|
||||
assert _top(north, 49)[0]["로직"] == "GC000085"
|
||||
soil = next(x for x in north["호표"][2]["줄"] if x["이름"].startswith("잔토처리"))
|
||||
names = [c["이름"] for c in hm.match_line(soil, [], {})["후보"]]
|
||||
names = [c["이름"] for c in hm.b_order(soil)["후보"]]
|
||||
assert not any("철골재" in n for n in names)
|
||||
assert master_hash() == before
|
||||
|
||||
@@ -106,7 +106,7 @@ def test_근거_없는_줄_그대로(north):
|
||||
assert bare
|
||||
for line in bare:
|
||||
plain = {k: v for k, v in line.items() if k != "근거"}
|
||||
assert hm.match_line(line, [], {}) == hm.match_line(plain, [], {})
|
||||
assert hm.b_order(line) == hm.b_order(plain)
|
||||
|
||||
|
||||
def test_길_응답_근거_칸(client):
|
||||
|
||||
@@ -46,7 +46,7 @@ def test_딱_맞음이_같음을_이김(got):
|
||||
def test_판정_셋(got):
|
||||
a = _block("N000", got)
|
||||
assert a["판정"]["상태"] == "맞음" and "소량감률 50" in a["판정"]["글"]
|
||||
assert a["후보"][0]["판정"] == "맞음" and a["후보"][0]["근거"]["까닭"] == "조항 같음"
|
||||
assert a["후보"][0]["판정"] == "맞음" and a["후보"][0]["근거"]["까닭"].startswith("조항 같음")
|
||||
b = _block("N008", got)
|
||||
assert b["판정"]["상태"] == "다른 점" and b["차례"][0] == "GC000083"
|
||||
assert "특별인부 엑셀 0.3542 · 우리 0.3704" in b["판정"]["꼬리표"]
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""338-1 골든(느림 · 2~4 분) — 정답 표(사람 136 · 새 파일 51)를 실물 엑셀 여덟 파일(읽기만)로 맞춰
|
||||
설계 7-3 기준 수치 · master_data 원본 그대로 · 셈 무게. 우리 지문 캐시가 없으면 처음 ≈ 40 초 더."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from resources.tester import helper_m02_hopyo_golden as golden
|
||||
from resources.tester.test_m02_hopyo_excel import master_hash
|
||||
|
||||
LABELED = ("북부청임도", "북부청사방", "동부청영월")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def run():
|
||||
before = master_hash()
|
||||
if not ours.ready():
|
||||
ours.build()
|
||||
t0 = time.time()
|
||||
res = golden.evaluate()
|
||||
return {"결과": res, "시간": time.time() - t0, "원본": before}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def no_seeds():
|
||||
keep = hm._seeds
|
||||
hm._seeds = lambda: {}
|
||||
hm._targets.cache_clear()
|
||||
try:
|
||||
yield golden.evaluate(LABELED)
|
||||
finally:
|
||||
hm._seeds = keep
|
||||
hm._targets.cache_clear()
|
||||
|
||||
|
||||
def test_모든_파일_모든_줄(run):
|
||||
res = run["결과"]
|
||||
assert not res["못읽음"] and len(res["읽음"]) == 8
|
||||
assert {lab["id"] for lab in golden.labels()} == set(res["줄"])
|
||||
|
||||
|
||||
def test_사람_126_순위(run):
|
||||
got = golden.metrics(run["결과"], {"사람"})
|
||||
assert got["줄"] == 126
|
||||
assert got["1순위"] >= 116 # 92%
|
||||
assert got["다섯 안"] >= 122 # 96%
|
||||
|
||||
|
||||
def test_북부청_92_조합_전(run):
|
||||
rows = [r for r in run["결과"]["줄"].values() if r["lab"]["파일"] in LABELED[:2]]
|
||||
have = [r for r in rows if r["lab"]["정답"]]
|
||||
ok = sum((r["셈"].get("지문차례") or [None])[0] in r["lab"]["정답"] for r in have)
|
||||
assert len(have) == 92 and ok >= 81 # 88%
|
||||
|
||||
|
||||
def test_판정_맞음_다른_점_옵션(run):
|
||||
got = golden.metrics(run["결과"], {"사람"})
|
||||
assert got["맞음"] >= 55 and got["맞음 틀림"] <= 2
|
||||
assert got["다른 점 틀림"] <= 2
|
||||
assert got["옵션까지"] >= 50
|
||||
|
||||
|
||||
def test_새_파일_맞음_틀림(run):
|
||||
got = golden.metrics(run["결과"], {"자동", "씨앗", "자동+씨앗"})
|
||||
assert got["줄"] == 51 and got["맞음 틀림"] <= 3
|
||||
|
||||
|
||||
def test_조합_맞음(run):
|
||||
got = golden.metrics(run["결과"], {"사람"})
|
||||
assert got["조합 호표"] == 83 and got["조합 맞음"] >= 60 # 72%
|
||||
|
||||
|
||||
def test_씨앗_끔_1순위(no_seeds):
|
||||
got = golden.metrics(no_seeds, {"사람"})
|
||||
assert got["줄"] == 126 and got["1순위"] >= 111 # 88%
|
||||
|
||||
|
||||
def test_원본_그대로_셈_무게(run):
|
||||
assert master_hash() == run["원본"]
|
||||
assert run["결과"]["시간"]["북부청임도"] <= 300 # 북부청 임도 맞춤 셈 ≤ 5 분
|
||||
@@ -2,9 +2,13 @@
|
||||
맞춤 차례 · 기억 우선 · 길(보기 · 미리 채움 · 확정) · master_data 원본 그대로. 층은 임시 폴더.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
# 올리기 길 시험의 `client` 마당을 같이 씀
|
||||
@@ -86,88 +90,124 @@ def test_규격에서_입력값():
|
||||
assert values == {} and unread == ["뒷길이"] # 고르기 밖 수 = 빈 값 + 표시
|
||||
|
||||
|
||||
# ── 맞춤 차례 ────────────────────────────────────────────────────────
|
||||
# ── 맞춤 응답(338 · 설계 3-3) ─────────────────────────────────────────
|
||||
@pytest.fixture(autouse=True)
|
||||
def calc(tmp_path, monkeypatch):
|
||||
"""맞춤셈 캐시 = 임시 폴더(우리 지문 사본) · 뒤 셈은 시험이 바로 돌림(`run_job`)."""
|
||||
if not ours.ready():
|
||||
ours.build()
|
||||
cache = tmp_path / "hopyo_cache"
|
||||
cache.mkdir()
|
||||
shutil.copy(ours.path_of(), cache / ours.path_of().name)
|
||||
monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache))
|
||||
monkeypatch.setattr(fit, "ensure", fit.status)
|
||||
|
||||
|
||||
def _hopyo(client, no, layer="personal"):
|
||||
res = client.get(f"{URL.format(layer)}/HP-000001/{no}")
|
||||
assert res.status_code == 200, res.text
|
||||
return {one["이름"]: one["맞춤"] for one in res.json()["호표"]["줄"]}
|
||||
|
||||
|
||||
def test_이름_같음_후보_호표_퍼센트(client):
|
||||
def _ready(client, layer="personal"):
|
||||
assert _post(client, layer).status_code == 200
|
||||
folder = layers.personal_dir(7, 42) if layer == "personal" else client.system
|
||||
before = _hopyo(client, 1, layer)["레미콘타설(장비사용)"]
|
||||
assert before["상태"] == "후보" and "셈 중" in before["꼬리표"] # 셈 전 = 이름 · 근거 후보
|
||||
fit.run_job(folder, "HP-000001")
|
||||
return folder
|
||||
|
||||
|
||||
def test_새_상태_체크_제안(client):
|
||||
before = master_hash()
|
||||
assert _post(client).status_code == 200
|
||||
_ready(client)
|
||||
first = _hopyo(client, 1)
|
||||
remicon = first["레미콘타설(장비사용)"]
|
||||
assert remicon["상태"] == "맞음" and remicon["체크"] is True and "들어감" not in remicon
|
||||
target = remicon["대상"][0]
|
||||
assert target["로직"] == "GC000198" and target["입력값"]["소량감률"] == 50
|
||||
assert target["출처"]["소량감률"] == "블록" and target["배"] == 1
|
||||
assert remicon["품"]["같음"]["콘크리트공"] and remicon["품"]["엑셀"]["인력"]["콘크리트공"]
|
||||
plywood = first["합판거푸집"]
|
||||
assert plywood["상태"] == "같음" and plywood["대상"]["로직"] == "GF000160"
|
||||
assert plywood["입력값"] == {"횟수": "4회사용시"} and plywood["들어감"] is True
|
||||
remicon = first["레미콘(25-21-120)"]
|
||||
assert remicon["상태"] == "후보" and remicon["들어감"] is False
|
||||
assert all(c["규격"] == "25-21-120" for c in remicon["후보"][:3]) # 규격 같은 자재 먼저
|
||||
assert plywood["상태"] == "맞음" and set(plywood["갈리는칸"]) == {"측압", "폼타이규격"}
|
||||
mat = first["레미콘(25-21-120)"]
|
||||
assert mat["상태"] == "후보" and mat["체크"] is False
|
||||
assert all(c["규격"] == "25-21-120" for c in mat["후보"][:3]) # 규격 같은 자재 먼저
|
||||
dig = _hopyo(client, 3)["구조물터파기(인력100%)"]
|
||||
assert dig["상태"] == "후보" and len(dig["후보"]) == 5
|
||||
assert dig["후보"][0]["이름"] == "굴착(인력/토사)" # 330-2 근거(건설 3-2-1 · 특별인부) 1순위
|
||||
assert any(c["이름"].startswith("구조물터파기") for c in dig["후보"]) # 이름 1순위 자리
|
||||
assert dig["상태"] == "다른 점" and dig["체크"] is False
|
||||
assert dig["대상"][0]["로직"] == "GC000083" and len(dig["후보"]) == 5
|
||||
assert "특별인부 엑셀 0.3542 · 우리 0.3704" in dig["꼬리표"]
|
||||
ref = _hopyo(client, 45)["고화매트재료비"]
|
||||
assert ref["상태"] == "호표" and ref["대상"] == {"호표": 46, "단위": "㎡"} and ref["들어감"]
|
||||
assert ref["상태"] == "호표" and ref["체크"] is False and ref["글"] == "제46호표 먼저 확인"
|
||||
assert ref["대상"][0]["호표"] == 46 and ref["대상"][0]["단위"] == "㎡"
|
||||
pct = _hopyo(client, 46)["잡재료비"]
|
||||
assert pct["상태"] == "%" and pct["들어감"] is False
|
||||
assert pct["상태"] == "%" and pct["체크"] is False
|
||||
whole = client.get(f"{URL.format('personal')}/HP-000001").json()
|
||||
assert whole["맞춤"][1] == 415 and 0 < whole["맞춤"][0] < 415
|
||||
assert whole["맞춤"][1] == 415 and 0 < whole["맞춤"][0] < 415 and whole["셈"]["끝"]
|
||||
assert next(h for h in whole["호표"] if h["번"] == 61)["맞춤"] == [0, 0]
|
||||
one = client.get(f"{URL.format('personal')}/HP-000001/1").json()["호표"]
|
||||
assert one["셈"]["끝"] and one["만든"] is None and one["맞춤"][0] >= 2
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_미리_채움_확정_기억_우선(client):
|
||||
assert _post(client).status_code == 200
|
||||
url = f"{URL.format('personal')}/HP-000001/3/guess"
|
||||
got = client.post(url, json={"차례": 1, "대상": {"로직": "GF000101"}}).json()
|
||||
assert got["환산"] == {"배": 1, "갈래": "같음"} and got["대상"]["단위"] == "㎥"
|
||||
assert client.post(url, json={"차례": 1, "대상": {"로직": "XX"}}).status_code == 400
|
||||
key = _hopyo(client, 3)["구조물터파기(인력100%)"]["열쇠"]
|
||||
assert key == "구조물터파기(인력100%)|토사|m3"
|
||||
body = {"줄": {key: {"로직": "GF000101", "입력값": {"지역": "서울"}, "환산": 1}}}
|
||||
assert client.post("/api/m02/layers/personal/hopyo-memory", json=body).status_code == 200
|
||||
dig = _hopyo(client, 3)["구조물터파기(인력100%)"]
|
||||
assert (dig["상태"], dig["대상"]["로직"], dig["입력값"], dig["들어감"]) == (
|
||||
"기억",
|
||||
"GF000101",
|
||||
{"지역": "서울"},
|
||||
True,
|
||||
def _ledger(client, rows, layer="personal"):
|
||||
res = client.put(f"/api/m02/layers/{layer}/hopyo-ledger", json={"줄": rows})
|
||||
assert res.status_code == 200, res.text
|
||||
|
||||
|
||||
def test_별칭_체크_규칙(client):
|
||||
_ready(client)
|
||||
dig = "구조물터파기(인력100%)|토사|m3"
|
||||
remicon = "레미콘타설(장비사용)|철근구조물-소량타설|m3"
|
||||
_ledger(
|
||||
client,
|
||||
{
|
||||
dig: {"갈래": "짝", "짝": [{"로직": "GC000083", "배": 1}]},
|
||||
remicon: {"갈래": "짝", "짝": [{"로직": "GC000199", "배": 1}]},
|
||||
"합판거푸집|6회|m2": {"갈래": "짝", "짝": [{"로직": "GC000211", "배": 1}]},
|
||||
"면목|구멍|개": {"갈래": "우리에 없음"},
|
||||
},
|
||||
)
|
||||
# 이름 같음보다 기억이 앞섬 · 빼기
|
||||
plywood = _hopyo(client, 1)["합판거푸집"]["열쇠"]
|
||||
out = {"줄": {plywood: {"빼기": True}}}
|
||||
assert client.post("/api/m02/layers/personal/hopyo-memory", json=out).status_code == 200
|
||||
assert _hopyo(client, 1)["합판거푸집"]["상태"] == "빼기"
|
||||
clear = {"줄": {plywood: None}}
|
||||
assert client.post("/api/m02/layers/personal/hopyo-memory", json=clear).status_code == 200
|
||||
assert _hopyo(client, 1)["합판거푸집"]["상태"] == "같음"
|
||||
got = _hopyo(client, 3)["구조물터파기(인력100%)"]
|
||||
assert got["상태"] == "별칭" and got["체크"] is True and got["대상"][0]["출처"] == {}
|
||||
assert got["별칭"]["층"] == "personal" and not got["별칭"]["근거다름"]
|
||||
first = _hopyo(client, 1)
|
||||
other = first["레미콘타설(장비사용)"] # 숫자 판정 맞음 = GC000198 → 체크 안 함
|
||||
assert other["상태"] == "별칭" and other["체크"] is False
|
||||
assert "숫자 판정 맞음 = GC000198" in other["꼬리표"]
|
||||
loose = first["합판거푸집"] # 규격 4회 ↔ 장부 6회 — 느슨 열쇠
|
||||
assert loose["상태"] == "별칭" and loose["별칭"]["규격다름"] and loose["체크"] is False
|
||||
_ledger(client, {dig: {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}})
|
||||
off = _hopyo(client, 3)["구조물터파기(인력100%)"] # 산림 로직 ↔ 엑셀 건설 3-2-1
|
||||
assert off["별칭"]["근거다름"] and off["체크"] is False and "근거 다름" in off["꼬리표"]
|
||||
|
||||
|
||||
def test_확정_검사와_위_층_기억(client):
|
||||
bad = [
|
||||
{"로직": "GC000422", "환산": 1}, # 돈 아닌 로직
|
||||
{"로직": "GF000101", "환산": 0},
|
||||
{"로직": "GF000101", "자재": "MT004258", "환산": 1},
|
||||
{"환산": 1},
|
||||
]
|
||||
for one in bad:
|
||||
res = client.post("/api/m02/layers/personal/hopyo-memory", json={"줄": {"a|b|c": one}})
|
||||
assert res.status_code == 400, one
|
||||
assert client.post("/api/m02/layers/system/hopyo-memory", json={"줄": {}}).status_code == 403
|
||||
def test_장부_뺌_위_층(client):
|
||||
client.who.update(role="SYSTEM_ADMIN")
|
||||
key = "구조물터파기(인력100%)|토사|m3"
|
||||
body = {"줄": {key: {"로직": "GF000103", "환산": 1}}}
|
||||
assert client.post("/api/m02/layers/system/hopyo-memory", json=body).status_code == 200
|
||||
_ledger(client, {key: {"갈래": "안 씀"}}, "system")
|
||||
client.who.update(role="USER")
|
||||
assert _post(client).status_code == 200
|
||||
assert _hopyo(client, 3)["구조물터파기(인력100%)"]["대상"]["로직"] == "GF000103" # 위 층
|
||||
mine = {"줄": {key: {"로직": "GF000101", "환산": 1}}}
|
||||
assert client.post("/api/m02/layers/personal/hopyo-memory", json=mine).status_code == 200
|
||||
assert _hopyo(client, 3)["구조물터파기(인력100%)"]["대상"]["로직"] == "GF000101" # 같은 층 먼저
|
||||
_ready(client)
|
||||
got = _hopyo(client, 3)["구조물터파기(인력100%)"]
|
||||
assert got["상태"] == "뺌" and got["뺌"] == "안 씀" and got["별칭"]["층"] == "system"
|
||||
assert hm.chain(layers.personal_dir(7, 42))[1] == layers.company_dir(7)
|
||||
|
||||
|
||||
def test_미리_셈_목록_낱개(client):
|
||||
folder = _ready(client)
|
||||
url = f"{URL.format('personal')}/HP-000001/1/guess"
|
||||
one = client.post(url, json={"차례": 1, "대상": {"로직": "GC000198"}})
|
||||
assert one.status_code == 200, one.text
|
||||
assert one.json()["대상"][0]["환산"] == {"배": 1, "갈래": "같음"}
|
||||
assert client.post(url, json={"차례": 1, "대상": {"로직": "XX"}}).status_code == 400
|
||||
body = [{"로직": "GC000198", "입력값": {"타설방법": "장비사용타설", "소량감률": 50}}]
|
||||
got = hm.guess(folder, "HP-000001", 1, 1, body)
|
||||
assert got["대상"][0]["출처"]["소량감률"] == "고름"
|
||||
assert got["품"]["같음"]["콘크리트공"] and got["품"]["엑셀"]["인력"]["콘크리트공"]
|
||||
plain = hm.guess(folder, "HP-000001", 1, 1, [{"로직": "GC000198", "입력값": {"소량감률": 0}}])
|
||||
assert not plain["품"]["같음"]["콘크리트공"] # 옵션을 바꾸면 우리 품이 바뀜
|
||||
|
||||
|
||||
def test_로직_추가_고르기_줄은_이_층과_위_층(client):
|
||||
client.who.update(role="SYSTEM_ADMIN")
|
||||
assert _post(client, "system").status_code == 200
|
||||
@@ -197,7 +237,7 @@ def test_뜻_낱말이_안_겹치면_후보_아님():
|
||||
assert hm.candidates(soil, "로직") == [] # 「처리」 두 글자만 겹침(소금처리 · 바탕처리)
|
||||
stone = {"이름": "임도표지석(받침,글새김포함)", "규격": "", "단위": "㎥", "갈래": "자재"}
|
||||
assert hm.candidates(stone, "자재") == [] # 속도표지판 · 도로표지용페인트
|
||||
got = hm.match_line(soil, [], {})
|
||||
assert got["상태"] == "없음" and got["후보"] == []
|
||||
got = hm.b_order(soil)
|
||||
assert got["상태"] == "없음" and got["top"] == []
|
||||
fill = {"이름": "구조물되메우기(인력100%)", "규격": "", "단위": "㎥", "갈래": "단가산출"}
|
||||
assert hm.candidates(fill, "로직")[0]["이름"] == "되메우기" # 한쪽 낱말에 듦 = 겹침
|
||||
|
||||
Reference in New Issue
Block a user