fix(일위대가실무): 339 덧고침 맞춤 셋 — 규격 다른 별칭 꼬리표만 · 같은 장비 여러 줄 품 비교 합침 · 근거 안 닿은 덧 로직 후보로

- 규격 다른 별칭(품명|단위 느슨 열쇠) = 꼬리표 「별칭 · 규격 다름」 · 짝은 후보 끝 덤 · 상태 · 옵션은 줄 숫자 맞춤
- 품 비교 같은 갈래 장비 여러 줄은 합쳐 견줌(서버 _qty · 화면 compareText)
- 조합 덧 로직 = 단위 맞음 + 조항 · 직종 · 이름 하나 닿을 때만 · 아니면 덧 후보 · 섞인 줄 그대로 · 맞춤셈 판 2
- 골든 사람 126 1순위 118 · 다섯 안 122 · 맞음 57 틀림 0 · 조합 62/83 그대로 · 338 대상 시험 142 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-16 01:32:53 +09:00
co-authored by Claude Opus 5.5
parent 933c4c5d71
commit 912575d9bb
9 changed files with 136 additions and 38 deletions
+37 -12
View File
@@ -6,6 +6,8 @@
+0.1.
덧 로직 = 1순위가 못 덮는(< COVER) 절은 절 1위(≥ 0.75) · 아니면 몫마다(≥ 0.6) · 장비만 든 몫 =
「기계 시간당 기계경비」(체크 안 함 · 시간 = 엑셀 값).
덧은 근거가 닿을 때만(`reached` — 단위 맞음 + 조항 · 직종(보통인부 뺌) · 이름 하나) · 아니면 덧
안 붙임(후보로만) · 섞인 줄(몫 먼저)은 그대로.
순위 덤 = 앞 차례 0.3 · 인용 조항 같음 0.3 · 같은 책 0.1 · 후보 풀 = 앞 차례 15 + 드문 직종 · 장비
색인
(40 개 이하) + 절 제목 이름 후보 8. 우리 벡터 = `M02_Hopyo_Ours` 지문 변형 L · X(따로 안 셈).
@@ -51,7 +53,8 @@ def _load() -> dict[str, Any]:
if var[o["로직"]]:
for d in dims(var[o["로직"]][0][1]):
idx.setdefault(d, set()).add(o["로직"])
_STATE.update(판=(ver, id(table)), 변형=var, 색인=idx)
trades = {o["로직"]: set(o.get("직종") or ()) for o in table}
_STATE.update(판=(ver, id(table)), 변형=var, 색인=idx, 직종=trades)
return _STATE
@@ -217,11 +220,28 @@ def machine_of(E: dict[str, Any]) -> str:
return MACHINE["굴착기"] if kinds == {frozenset({"굴착기"})} else MACHINE_ANY
def reached(code: str, line: dict, where: Any, E: dict, prior: Prior, got: dict) -> bool:
"""덧 근거 닿음 — 단위 맞음 + (인용 조항 같음 · 직종 겹침(보통인부 뺌) · 이름 낱말 겹침)."""
one = next((x for x in got["로직"] if x["로직"] == code), None)
if one is None or not fit.unit_fit(line.get("단위"), one["단위"]):
return False
our = prior.table.get(code)
theirs = prior.theirs + hopyo_basis.sources(str(where or ""))
if our and theirs and hopyo_match.clause(theirs, our)[0] >= 0.9:
return True
if (set(E.get("노임") or {}) - {COMMON}) & _load()["직종"].get(code, set()):
return True
loose = lambda t: hopyo_basis._loose_words(t) - hopyo_match._GENERIC # noqa: E731
mine, theirs_w = loose(f"{where or ''} {line.get('이름') or ''}"), loose(one["이름"])
return any(a in b or b in a for a in mine for b in theirs_w)
# ── 풀이 ───────────────────────────────────────────────────────────────
def combine(
primary: str | None, parts: list[dict], line: dict, got: dict, base: list[str]
) -> list[dict[str, Any]]:
"""[{로직, 어디, 맞음, 몫, 단계, 입력}] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직."""
"""[{로직, 어디, 맞음, 몫, 단계, 입력}] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직 · 근거 안 닿은
덧은 단계 「후보」(조합에 안 듦)."""
one = lambda c, w, f, x, step, g=None: { # noqa: E731
"로직": c,
"어디": w,
@@ -257,11 +277,12 @@ def combine(
pool = pool_for(E, base, title_cands(title, unit, got))
cand = ranked(E, pool, prior, share, _hint(title))
if cand and cand[0][1] >= fit.COMBO[1] and cand[0][3] not in have:
chosen.append(
one(cand[0][3], title, round(cand[0][1], 3), cand[0][2], "절", cand[0][4])
)
ok = mixed or reached(cand[0][3], line, title, E, prior, got)
f, x, c, g = round(cand[0][1], 3), cand[0][2], cand[0][3], cand[0][4]
chosen.append(one(c, title, f, x, "절" if ok else "후보", g))
have.add(cand[0][3])
continue
if ok:
continue
for p in ps:
if primary and cover(p, primary) >= fit.COVER:
continue
@@ -273,17 +294,20 @@ def combine(
cand = ranked(p, pool, prior, hint is not None or share, hint)
if cand and cand[0][1] >= fit.COMBO[2] and cand[0][3] not in have:
f, x, c, g = round(cand[0][1], 3), cand[0][2], cand[0][3], cand[0][4]
chosen.append(one(c, p.get("제목"), f, x, "몫", g))
ok = mixed or reached(c, line, p.get("제목"), p, prior, got)
chosen.append(one(c, p.get("제목"), f, x, "몫" if ok else "후보", g))
have.add(c)
return chosen
def solve(
top: list[str], parts: list[dict], line: dict, got: dict, base: list[str]
) -> tuple[list[str], list[dict[str, Any]]]:
"""(새 차례, 조합 대상) — 섞인 줄이고 1순위가 블록 합을 MIXED_CUT 못 덮으면 1순위를 인력 몫
로직으로 바꿈(몫 먼저) · 조합이 하나뿐이면 대상 빈 것."""
) -> tuple[list[str], list[dict[str, Any]], list[str]]:
"""(새 차례, 조합 대상, 덧 후보) — 섞인 줄이고 1순위가 블록 합을 MIXED_CUT 못 덮으면 1순위를
인력 몫 로직으로 바꿈(몫 먼저) · 조합이 하나뿐이면 대상 빈 것 · 덧 후보 = 근거 안 닿은 덧."""
chosen = combine((top or [None])[0], parts, line, got, base)
spare = [c["로직"] for c in chosen if c["단계"] == "후보"]
chosen = [c for c in chosen if c["단계"] != "후보"]
codes = list(dict.fromkeys(c["로직"] for c in chosen if c["로직"]))
if top and len(codes) > 1 and parts and is_mixed(parts, line):
if cover(_sum(parts), top[0]) < fit.MIXED_CUT:
@@ -296,5 +320,6 @@ def solve(
c for c in chosen if not (c["로직"] in seen or seen.add(c["로직"]))
] # 같은 로직은 처음 것
if len(chosen) < 2:
return top, []
return top, [{**c, "체크": c["로직"] not in MACHINE_SET or c["로직"] == top[0]} for c in chosen]
return top, [], spare
combo = [{**c, "체크": c["로직"] not in MACHINE_SET or c["로직"] == top[0]} for c in chosen]
return top, combo, spare
+1 -1
View File
@@ -54,7 +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 # 맞춤셈 꼴 · 셈이 바뀌면 올림 — 옛 캐시 안 씀
CALC_VERSION = 2 # 맞춤셈 꼴 · 셈이 바뀌면 올림 — 옛 캐시 안 씀
COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"}
HOUR = {"hr", "시간", "h"}
NO_NUMBER = "숫자 판정 없음"
+45 -15
View File
@@ -4,10 +4,12 @@
상태 차례 = ① % ② 호표 참조(만들 층에 확인된 사전조합이면 체크) ③ 노임(그릇 없음) ④ 장부 「우리에
없음 ·
안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(규격 다름 · 근거 다름 · 숫자 판정 맞음이 다른 로직이면 체크 안 함) ⑥
안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(근거 다름 · 숫자 판정 맞음이 다른 로직이면 체크 안 함) ⑥
맞춤셈
판정(맞음 = 체크 · 다른 점 · 후보) ⑦ 셈 전 · 숫자 없음 → 이름 · 근거 후보(「셈 중」 · 「숫자 판정
없음」).
장부 짝이 규격 다름(품명|단위 느슨 열쇠)이면 ⑤ 아님 — 꼬리표 「별칭 · 규격 다름」 · 짝은 후보
끝 덤만.
자재 · 별도계상 = 이름 · 규격 같음 하나면 맞음(체크) · 그 밖 후보.
입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값.
체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독.
@@ -92,18 +94,24 @@ def _pair(pair: dict, line: dict, ctx: Ctx) -> dict:
def _qty(entry: dict, found: dict | None) -> dict:
"""품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용)."""
"""품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 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))
sums: dict[str, list[float]] = {} # 열쇠 → [엑셀 합, 반올림 폭 합, 우리 합]
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)
name = "+".join(m["갈래"]) # 화면 품 비교 열쇠와 같음(갈래 + 로 묶음)
same[name] = hit or same.get(name, False)
one = sums.setdefault("+".join(sorted(m["갈래"])), [0.0, 0.0, 0.0])
one[0] += m["시간"]
one[1] += option.ru(m["시간"])
for x in us["장비"]:
if (one := sums.get("+".join(sorted(x["갈래"])))) is not None:
one[2] += x["시간"]
for name, (a, r, b) in sums.items():
same[name] = option.near(a, b, fit.TOL[0], r)
return {"엑셀": ex, "우리": us, "같음": same}
@@ -201,17 +209,39 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A
if kind == "노임":
return {**out, "상태": "노임", "글": "노임 · 그릇 없음"}
alias = ledger.recall(line, ctx.to, ctx.books)
loose, alias = (alias, None) if alias and alias.get("규격다름") else (None, alias)
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)}
got = _material(line, ctx, full)
elif entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"):
got = _numeric(line, ctx, entry) if full else _brief(entry)
else:
why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER
got = _names_only(line, ctx, why, full)
return {**out, **(_loose(got, loose, line, ctx) if loose and full else got)}
def _loose(got: dict, alias: dict, line: dict, ctx: Ctx) -> dict:
"""규격 다른 별칭 — 꼬리표 「별칭 · 규격 다름」 + 짝 로직 · 자재가 후보에 없으면 끝에 덤(상태 ·
대상 · 옵션은 이 줄 숫자 그대로)."""
tags = [*(got.get("꼬리표") or []), "별칭 · 규격 다름"]
have = {c.get("로직") or c.get("자재") for c in got.get("후보") or []}
have |= {t.get("로직") or t.get("자재") for t in got.get("대상") or []}
extra = []
for p in alias.get("짝") or []:
code = p.get("로직") or p.get("자재")
if code and code not in have:
one = {
k: v
for k, v in _pair(p, line, ctx).items()
if k in ("로직", "자재", "이름", "단위")
}
extra.append({**one, "점수": None, "판정": "후보", "글": "별칭 · 규격 다름"})
return {**got, "꼬리표": tags, "후보": [*(got.get("후보") or []), *extra]}
def _brief(entry: dict) -> dict:
@@ -225,7 +255,7 @@ def _alias_info(alias: dict, basis_off: bool) -> dict:
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 []
@@ -234,9 +264,9 @@ def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict:
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
check = not off and not other
tags = ["별칭 — " + " · ".join(str(x) for x in (alias.get("층"), alias.get("때")) if x)]
tags += ["규격 다름"] * bool(alias.get("규격다름")) + ["근거 다름"] * off
tags += ["근거 다름"] * off
tags += [f"숫자 판정 맞음 = {entry['차례'][0]}"] if other else []
out = {"상태": "별칭", "체크": check, "대상": targets, "글": "별칭 장부 짝", "꼬리표": tags}
out["별칭"] = _alias_info(alias, off)
@@ -249,7 +279,7 @@ 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("짝"):
if alias and alias.get("갈래") == "짝" and alias.get("짝") and not alias.get("규격다름"):
return {
"대상": [_pair(p, head, ctx) for p in alias["짝"]],
"글": "별칭 장부 짝",
+7 -3
View File
@@ -1,8 +1,8 @@
"""M02 일위대가 맞춤 — 블록 하나 셈 · 판정(PLAN 338-1 · 설계 2-1 · 2-6 · 3-2).
블록 + 줄 → 엑셀 지문(`Finger`) → 이름 · 근거 차례(`Match.b_order`) → 지문 순위 섞기(`Fit`) ·
같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve`) → 위 다섯 옵션
거꾸로(`Option`) →
같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve` · 근거 안 닿은 덧 = 후보 끝)
→ 위 다섯 옵션 거꾸로(`Option`) →
위 다섯 가운데 딱 맞음(옵션까지)이 있는데 1위가 아니면 그것이 1위(조합 없는 줄) →
판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표).
글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음.
@@ -122,7 +122,7 @@ def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dic
parts = efp.get("몫") or []
if parts and btop and combine.is_mixed(parts, line): # 섞인 줄 — 이름 · 근거 차례로 몫 먼저
top = btop[: fit.TOP_OPT]
top, combo = combine.solve(top, parts, line, got, border["합침"])
top, combo, spare = combine.solve(top, parts, line, got, border["합침"])
opts = {k: option.find(k, efp, text) for k in top[: fit.TOP_OPT]}
for c in combo:
if c["로직"] not in opts and c["체크"]:
@@ -150,6 +150,10 @@ def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dic
entry["조합"] = combo or None
entry["판정"] = judge(efp, top, opts, theirs, got)
entry["후보"] = candidates(efp, top, opts, theirs, got, scores, border.get("근거") or {})
names = {x["로직"]: x for x in got["로직"]}
for k in (k for k in spare if k not in top[: fit.TOP_OPT] and k in names): # 근거 안 닿은 덧
one = {key: names[k][key] for key in ("로직", "이름", "단위")}
entry["후보"].append({**one, "점수": None, "판정": "후보", "글": "덧 후보 — 근거 안 닿음"})
return entry
@@ -149,10 +149,14 @@ export function compareText(q: HopyoMatch["품"] | undefined): string {
const out = [...new Set([...Object.keys(ex), ...Object.keys(ours)])].map(
(k) => `${k} ${n4(ex[k])} → ${n4(ours[k])} ${mark(k)}`,
);
const name = (g: string | string[]): string => [g].flat().join("+");
for (const x of q.엑셀.장비 ?? []) {
const o = (q.우리.장비 ?? []).find((y) => name(y.갈래) === name(x.갈래));
out.push(`${name(x.갈래)} ${n4(x.시간)} → ${n4(o?.시간)} ${mark(name(x.갈래))}`);
// 같은 갈래 여러 줄은 합쳐 한 줄(서버 `_qty` 와 같은 열쇠 — 갈래 가나다 차례 + 로 묶음)
const name = (g: string | string[]): string => [g].flat().sort().join("+");
const sum = (xs: { 갈래: string | string[]; 시간: number }[], k: string): number | undefined =>
xs
.filter((y) => name(y.갈래) === k)
.reduce<number | undefined>((s, y) => (s ?? 0) + y.시간, undefined);
for (const k of new Set((q.엑셀.장비 ?? []).map((x) => name(x.갈래)))) {
out.push(`${k} ${n4(sum(q.엑셀.장비 ?? [], k))} → ${n4(sum(q.우리.장비 ?? [], k))} ${mark(k)}`);
}
return out.join(" · ");
}
@@ -132,6 +132,7 @@ let out0 = null;
checkable: lines.map((l) => check.checkable(l, fresh)),
sure: check.sureLines(lines, fresh).map((l) => l.차례),
compare: check.compareText(lines[1].맞춤.품),
compareMany: check.compareText(lines[3].맞춤.품),
toTarget: [
check.toTarget({ 호표: 46, 이름: "고화매트", 입력값: {}, 배: 2.3 }),
check.toTarget({ 자재: "MT1", 이름: "레미콘", 입력값: {}, 배: 1, 몫: null }),
+8
View File
@@ -77,6 +77,14 @@ def test_조합_고임돌_인력_몫_먼저(got):
assert all(not c["체크"] for c in r["조합"] if c["로직"] in ("GC000267", "GC000268"))
def test_조합_근거_안_닿은_덧은_후보(got):
r = _block("N009", got) # 구조물되메우기(인력100%) — 훈증더미 인력 제거(개)는 덧 아님
assert r["차례"][0] == "GF000113" and not r["조합"]
assert any(c["로직"] == "GF000083" and c["글"].startswith("덧 후보") for c in r["후보"])
r = _block("N017", got) # 다웰바(EA) — PSC빔 쉬즈관(m) 덧 아님
assert r["차례"][0] == "GC000226" and not r["조합"]
def test_숫자_없음_옛_HP(got):
block, line = line_of("N000")
r = verdict.block({**block, "글": []}, line, got)
+21 -2
View File
@@ -160,13 +160,32 @@ def test_별칭_체크_규칙(client):
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
loose = first["합판거푸집"] # 규격 4회 ↔ 장부 6회 — 느슨 열쇠 = 꼬리표 · 후보 덤만(229-4 ①)
assert loose["상태"] == "맞음" and "별칭" not in loose and "별칭 · 규격 다름" in loose["꼬리표"]
assert all("별칭" not in t["출처"].values() for t in loose["대상"])
assert "GC000211" in {c["로직"] for c in loose["후보"]} | {t["로직"] for t in loose["대상"]}
_ledger(
client, {remicon: {"갈래": "짝", "짝": [{"로직": "GC000198", "입력값": {"소량감률": 50}}]}}
)
plain = _hopyo(client, 7)["레미콘타설(장비사용)"] # 무근구조물 ↔ 장부 철근구조물-소량타설
assert plain["상태"] == "맞음" and plain["체크"] is True and "별칭" not in plain
assert plain["대상"][0]["로직"] == "GC000198" and plain["대상"][0]["입력값"]["소량감률"] == 0
assert "별칭 · 규격 다름" in plain["꼬리표"]
_ledger(client, {dig: {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}})
off = _hopyo(client, 3)["구조물터파기(인력100%)"] # 산림 로직 ↔ 엑셀 건설 3-2-1
assert off["별칭"]["근거다름"] and off["체크"] is False and "근거 다름" in off["꼬리표"]
def test_품_비교_같은_장비_여러_줄():
from M02_MasterTemplete import M02_Hopyo_Line as lines
ex = {"인력": {}, "장비": [{"갈래": ["굴착기"], "시간": t} for t in (0.0334, 0.0371, 0.0889)]}
one = {"인력": {}, "장비": [{"갈래": ["굴착기"], "시간": 0.0889}], "자재": []}
assert not lines._qty({"품": ex}, {"우리품": one})["같음"]["굴착기"] # 값이 달라도 ✓ 이던 흠
many = {**one, "장비": ex["장비"]}
assert lines._qty({"품": ex}, {"우리품": many})["같음"]["굴착기"]
def test_장부_뺌_위_층(client):
client.who.update(role="SYSTEM_ADMIN")
key = "구조물터파기(인력100%)|토사|m3"
+8 -1
View File
@@ -48,6 +48,12 @@ QTY = {
"같음": {"콘크리트공": True, "굴착기": False},
}
QTY_MANY = { # 같은 장비 여러 줄(229-4 ②) — 합쳐 한 줄로 견줌
"엑셀": {"장비": [{"갈래": "굴착기", "시간": t} for t in (0.0334, 0.0371, 0.0889)]},
"우리": {"장비": [{"갈래": "굴착기", "시간": 0.0889}]},
"같음": {"굴착기": False},
}
def _text(name: str) -> str:
return (M02 / name).read_text(encoding="utf-8")
@@ -77,7 +83,7 @@ def _check() -> dict:
_line(0, "후보", False, [{"자재": "MT004258", "입력값": {}, "배": 1}]),
_line(1, "맞음", True, [SURE], 품=QTY),
_line(2, "별칭", True, [{"로직": "GF000101", "입력값": {}, "배": 1}]),
_line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}]),
_line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}], 품=QTY_MANY),
_line(4, "후보", False, []),
_line(5, "%", False, []),
_line(6, "호표", True, [{"호표": 46, "입력값": {}, "배": 2.3}]),
@@ -122,6 +128,7 @@ def test_확인_창_체크한_줄만_몸():
assert got["checkable"] == [True, True, True, True, False, False, True, False]
assert got["sure"] == [1, 2] # [맞음 · 별칭 모두 체크]
assert got["compare"] == "콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠"
assert got["compareMany"] == "굴착기 0.1594 → 0.0889 ≠" # 같은 장비 여러 줄 = 합쳐 한 줄
assert got["toTarget"] == [{"호표": 46, "배": 2.3}, {"자재": "MT1", "배": 1}]
assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로
assert got["dropped"] is False # 만들기 성공 = 캐시 지움