feat(M02 실무자료): 358-1 후보 묶음 — 이름후보(옛 324 다섯) · 뜻겹침 · 출처 같음 · 책 표시 · 고르기 창 묶음 · 검색 칸 = 품명 낱말 · 일괄 적용(N)

- 서버 줄 맞춤에 이름후보(옛 데스크탑 판과 같음 · 600줄 대조 0 다름) · 후보마다 뜻겹침 · 출처같음(조항 ≥ 0.9) · 책 — 차례 · 무게 · 상태 그대로
- 고르기 창 = 1위 / 출처 같음 / 이름 닮은 로직 / 숫자만 닮음(접힘 · 뜻 안 겹치는 후보 회색 끝) · 같은 이름 책 표시
- 버튼 명사형 — 일괄 적용(N) · 엑셀 보기 · 다른 로직 검색 · 재료비 있는 줄은 다른 자재 검색도
- 시험: match 이름후보 다섯 · 되메우기 · 다웰바 · golden 다섯 안 ∪ 이름후보 · screen 묶음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-16 20:55:48 +09:00
co-authored by Claude Opus 5.5
parent ec02165954
commit 8c562812da
9 changed files with 263 additions and 34 deletions
@@ -159,6 +159,42 @@ let out0 = null;
word: [choose.builtWord({ 다시씀: false }), choose.builtWord({ 다시씀: true })],
qty: choose.qtyText(lines[1].맞춤.품.엑셀),
target: choose.targetText({ 호표: 46, 단위: "㎡" }),
// 358-1 후보 묶음 — 1위 / 출처 같음 / 이름 닮은 로직 / 숫자만 닮음(약한 후보 끝) · 같은 이름 책
groups: (() => {
const m = {
후보: [
{ 로직: "A", 이름: "되메우기", 판정: "맞음", 뜻겹침: true, 책: "산림품셈" },
{ 로직: "B", 이름: "야면석 채집", 판정: "후보", 뜻겹침: false },
{
로직: "C",
이름: "되메우기",
판정: "후보",
뜻겹침: true,
출처같음: true,
책: "건설품셈",
},
{ 로직: "D", 이름: "표토펴기", 판정: "다른 점", 뜻겹침: false },
],
이름후보: [
{ 로직: "A", 이름: "되메우기" },
{ 로직: "E", 이름: "되메우기(노상재)" },
],
};
const g = choose.candGroups(m);
const twins = choose.sameNames(g);
return {
groups: g.map((x) => [x.제목, x.접힘, x.후보.map((c) => c.로직)]),
text: g.flatMap((x) => x.후보).map((c) => choose.candText(c, twins)),
material: choose
.candGroups({ 후보: [{ 자재: "M1" }, { 자재: "M2" }] }, true)
.map((x) => x.제목),
pick: [
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: 10 } }),
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: null } }),
choose.canPickMaterial({ 갈래: "별도계상", 단가: {} }),
],
};
})(),
},
ledger: (() => {
const rows = input.ledger;
+15
View File
@@ -80,3 +80,18 @@ def test_씨앗_끔_1순위(no_seeds):
def test_원본_그대로_셈_무게(run):
assert master_hash() == run["원본"]
assert run["결과"]["시간"]["북부청임도"] <= 300 # 북부청 임도 맞춤 셈 ≤ 5 분
def test_이름후보_묶음_덧(run):
"""358-1 — 이름후보(옛 324 다섯) = 묶음 덧 · 숫자 차례 그대로 → 다섯 안 ∪ 이름후보 ≥ 다섯 안."""
have = [
r for r in run["결과"]["줄"].values() if r["lab"]["무리"] == "사람" and r["lab"]["정답"]
]
got = hm.targets()
five = [set(r["셈"]["차례"][:5]) for r in have]
named = [{c["로직"] for c in hm.name_cands(r["lab"], got)} for r in have]
near = sum(bool(f & set(r["lab"]["정답"])) for f, r in zip(five, have, strict=True))
both = sum(
bool((f | n) & set(r["lab"]["정답"])) for f, n, r in zip(five, named, have, strict=True)
)
assert both >= near >= 122
+23
View File
@@ -245,3 +245,26 @@ def test_뜻_낱말이_안_겹치면_후보_아님():
assert got["상태"] == "없음" and got["top"] == []
fill = {"이름": "구조물되메우기(인력100%)", "규격": "", "단위": "㎥", "갈래": "단가산출"}
assert hm.candidates(fill, "로직")[0]["이름"] == "되메우기" # 한쪽 낱말에 듦 = 겹침
# ── 이름 닮은 로직(358-1 · 옛 324 데스크탑 다섯 그대로) ─────────────────────
def test_이름후보_옛_데스크탑_다섯():
fill = {"이름": "구조물되메우기(인력100%)", "규격": "", "단위": "㎥", "갈래": "단가산출"}
got = [c["로직"] for c in hm.name_cands(fill)]
assert got == ["GF000113", "GF000194", "GC000108", "GC000109", "GC000451"] # 되메우기 다섯
dowel = {"이름": "다웰바설치", "규격": "Φ32×1000", "단위": "EA", "갈래": "단가산출"}
assert [c["로직"] for c in hm.name_cands(dowel)][:3] == ["GC000226", "GF000201", "GF000202"]
soil = {"이름": "잔토처리(인력100%)", "규격": "", "단위": "㎥", "갈래": "단가산출"}
assert hm.name_cands(soil) == [] # 뜻 낱말 안 겹치면 후보 아님 그대로
same = {"이름": "되메우기", "규격": "", "단위": "㎥", "갈래": "단가산출"}
assert hm.name_cands(same)[0] == {**hm.name_cands(same)[0], "로직": "GF000113", "점수": 9}
def test_이름후보_뜻겹침_출처_표시(client):
_ready(client)
dig = _hopyo(client, 3)["구조물터파기(인력100%)"]
assert dig["이름후보"] and all({"뜻겹침", "출처같음", "책"} <= set(c) for c in dig["이름후보"])
assert dig["후보"][0]["출처같음"] is True and dig["후보"][0]["책"] == "건설품셈" # 3-2-1
assert [c["로직"] for c in dig["후보"]][0] == "GC000083" # 차례 · 판정 그대로(표시만)
mat = _hopyo(client, 1)["레미콘(25-21-120)"]
assert "이름후보" not in mat # 자재 줄은 로직 이름후보 없음
+10 -1
View File
@@ -136,7 +136,16 @@ def test_확인_창_체크한_줄만_몸():
def test_고르기_창_도우미():
got = _check()["choose"]
assert got["hint"] == ["6-1-1", "고임돌"] # 인용 조항 번호 → 없으면 품명 첫 뜻 낱말
assert got["hint"] == ["레미콘타설", "고임돌"] # 품명 첫 뜻 낱말(358-1 · 조항 번호 아님)
assert got["groups"]["groups"] == [
["1위", False, ["A"]],
["출처 같음", False, ["C"]],
["이름 닮은 로직", False, ["E"]],
["숫자만 닮음", True, ["D", "B"]], # 뜻 낱말 안 겹치는 「후보」 = 끝
]
assert got["groups"]["text"][:2] == ["되메우기 · A · 산림", "되메우기 · C · 건설"]
assert got["groups"]["material"] == ["1위", "그 밖 후보"]
assert got["groups"]["pick"] == [True, False, True] # 재료비 있는 단가산출 줄도 자재 검색
assert got["picked"] == [
{"로직": "GC000206", "입력값": {}, "배": 1, "몫": None},
{"로직": "GF000211", "입력값": {}, "배": 0.1, "몫": 0.1},