fix(일위대가실무): 330 덧고침 — 후보 다섯에 이름 1순위 자리 · 낱말 안 겹치는 근거 후보 뒤로 · 위 층 격자 읽기 시험
- Basis.merge 같은 점수는 이름 겹침 큰 쪽 · 이름 · 낱말(괄호 글 포함 · 인력 · 기계 · 장비 뺌) 안 겹치는 근거 후보는 근거 맞음(조항 · 직종 · 단위)이 아니면 겹치는 근거 후보 뒤로 - Basis.pick 다섯 안에 이름 1순위 하나 늘 남김 · Match match_line 이 씀 - 시험 basis(제3호표 구조물터파기 다섯 = 굴착(인력/토사) 1위 · 육상토사 안 · 터널굴착 밖 · pick) · match(이름 1순위 자리) · grid(고르기 줄 층 · 주인 그대로 회사 · 시스템 격자 읽기) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -8,7 +8,10 @@
|
||||
직종 = 「이름 : 수」 줄의 빈칸 뺀 이름이 우리 인력(LB) 이름 · 옛이름일 때만(지금 이름으로).
|
||||
견줌(`candidates`) = 같은 책 조항 같음 1.0 · 앞 마디 같음 0.5(조항이 안 맞으면 후보 아님) ·
|
||||
건설 공통 · 토목 덤 0.1 · 직종 겹침(보통인부 뺌) 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 ·
|
||||
같은 점수는 글자 두 개 묶음(괄호 글 포함)이 가름. 자동 확정 없음 — 맞춤 차례는 `M02_Hopyo_Match`.
|
||||
같은 점수는 글자 두 개 묶음(괄호 글 포함)이 가름 · 이름 · 낱말(괄호 글 포함 · 인력 · 기계 뺌)이
|
||||
하나도 안 겹치는 근거 후보는(근거 맞음 = 조항 · 직종 · 단위 다 맞으면 빼고) 겹치는 근거 후보 뒤로 ·
|
||||
다섯 안에 이름 1순위 하나는 늘 남김(`pick`).
|
||||
자동 확정 없음 — 맞춤 차례는 `M02_Hopyo_Match`.
|
||||
M01 · 원본 로직은 읽기만.
|
||||
"""
|
||||
|
||||
@@ -29,6 +32,7 @@ _HEAD = re.compile(r"^(\d+)\.\s*(.*?)\s*\[([^\[\]]*)\]\s*$")
|
||||
_BOOK = re.compile(r"품[셈줌]")
|
||||
_TRADE = re.compile(r"^[\s*·\-]*([가-힣][가-힣\s]*?)\s*:\s*[\d,]")
|
||||
_TYPOS = (("깍기", "깎기"),)
|
||||
_PLAIN_WORDS = frozenset({"인력", "기계", "장비"}) # 방법 낱말 — 「터널굴착 1발파당 인력」
|
||||
_MONEY = {"합계": "계", "노무비": "노무비", "재료비": "재료비", "경비": "경비"}
|
||||
|
||||
|
||||
@@ -179,8 +183,14 @@ def _text(line: dict[str, Any]) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def _loose_words(text: str) -> frozenset[str]:
|
||||
"""괄호 글까지 뜻 낱말 — 방법 낱말 뺌."""
|
||||
return hopyo_match.words(text.replace("(", " ").replace(")", " ")) - _PLAIN_WORDS
|
||||
|
||||
|
||||
def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""근거 후보 — 줄 `근거.출처` 가 없으면 빈 것 · `{로직, 이름, 단위, 점수, 근거}` 점수 차례."""
|
||||
"""근거 후보 — 줄 `근거.출처` 가 없으면 빈 것 · `{로직, 이름, 단위, 근거, _겹침, _낱말}`
|
||||
점수 차례(`_` 칸은 `merge` 가 쓰고 뺌)."""
|
||||
basis = line.get("근거") or {}
|
||||
theirs = [list(one) for one in basis.get("출처") or []]
|
||||
if not theirs:
|
||||
@@ -189,6 +199,7 @@ def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any]
|
||||
text = _text(line)
|
||||
mine = hopyo_match.words(text) # 괄호 글 뺌 — 「잔토처리(인력)」 ↔ 「철골재 철거(인력)」
|
||||
grams = hopyo_match._grams(hopyo_match.mm.plain(text)) # 괄호 글 포함 — 같은 점수 가름
|
||||
loose = _loose_words(text)
|
||||
table = ours()
|
||||
scored = []
|
||||
for one in got["로직"]:
|
||||
@@ -214,18 +225,42 @@ def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any]
|
||||
tags.append("단위 맞음")
|
||||
dice = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"]))
|
||||
why = {"조항": clauses, "직종": shared, "단위": unit, "점수": round(score, 3)}
|
||||
scored.append(
|
||||
(score, dice, {**hopyo_match._public(one), "근거": {**why, "까닭": " · ".join(tags)}})
|
||||
)
|
||||
theirs_loose = _loose_words(one["이름"])
|
||||
overlap = any(a in b or b in a for a in loose for b in theirs_loose)
|
||||
full = tag == "조항 같음" and unit and (bool(shared) or not trades) # 근거 맞음
|
||||
why["까닭"] = " · ".join(tags)
|
||||
extra = {"_겹침": dice, "_낱말": overlap or full}
|
||||
scored.append((score, dice, {**hopyo_match._public(one), "근거": why, **extra}))
|
||||
scored.sort(key=lambda x: (-x[0], -x[1]))
|
||||
return [one for _, _, one in scored]
|
||||
|
||||
|
||||
def merge(basis: list[dict[str, Any]], named: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""근거 후보 + 이름 후보 → 로직 키로 합침(점수 = 이름 점수 + 근거 점수) · 점수 차례."""
|
||||
"""근거 후보 + 이름 후보 → 로직 키로 합침(점수 = 이름 점수 + 근거 점수) · 점수 차례 ·
|
||||
같으면 이름 겹침 큰 쪽 · 이름 · 낱말 안 겹치는 근거 후보는 겹치는 근거 후보 뒤로."""
|
||||
if not basis:
|
||||
return named
|
||||
by_name = {c.get("로직"): c["점수"] for c in named}
|
||||
out = [{**c, "점수": round(c["근거"]["점수"] + by_name.pop(c["로직"], 0), 3)} for c in basis]
|
||||
out = []
|
||||
for c in basis:
|
||||
named_too = c["로직"] in by_name
|
||||
score = round(c["근거"]["점수"] + by_name.pop(c["로직"], 0), 3)
|
||||
out.append({**c, "점수": score, "_낱말": c["_낱말"] or named_too})
|
||||
out += [c for c in named if c.get("로직") in by_name]
|
||||
return sorted(out, key=lambda c: -c["점수"])
|
||||
out.sort(key=lambda c: (-c["점수"], -c.get("_겹침", 0)))
|
||||
loose = [c for c in out if c.get("_낱말") is False]
|
||||
if loose and any(c.get("_낱말") for c in out):
|
||||
out = [c for c in out if c.get("_낱말") is not False]
|
||||
last = max(at for at, c in enumerate(out) if c.get("_낱말"))
|
||||
out[last + 1 : last + 1] = loose
|
||||
return [{k: v for k, v in c.items() if not k.startswith("_")} for c in out]
|
||||
|
||||
|
||||
def pick(ordered: list[dict[str, Any]], first: Any, room: int) -> list[dict[str, Any]]:
|
||||
"""후보 `room` 개 — 이름 1순위(`first` 로직 키) 하나는 늘 남기고 나머지는 차례대로."""
|
||||
if room <= 0:
|
||||
return []
|
||||
if first is None or all(c.get("로직") != first for c in ordered):
|
||||
return ordered[:room]
|
||||
others = [c for c in ordered if c.get("로직") != first][: room - 1]
|
||||
return [c for c in ordered if c.get("로직") == first or any(c is o for o in others)]
|
||||
|
||||
@@ -409,7 +409,8 @@ def match_line(
|
||||
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]
|
||||
out["후보"] = (top + rest)[:5]
|
||||
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 "없음"}
|
||||
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ def test_근거_후보(north):
|
||||
"점수": 1.75,
|
||||
"까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음",
|
||||
}
|
||||
codes = [c["로직"] for c in dig]
|
||||
assert "GF000101" in codes and "GC000346" not in codes # 이름 1순위 남김 · 낱말 안 겹침 뒤로
|
||||
assert all("_겹침" not in c and "_낱말" not in c for c in dig)
|
||||
assert _top(north, 21)[0]["로직"] == "GC000198"
|
||||
assert "GC000143" in [c["로직"] for c in _top(north, 64)[:2]]
|
||||
assert _top(north, 49)[0]["로직"] == "GC000085"
|
||||
@@ -83,6 +86,14 @@ def test_근거_후보(north):
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_이름_1순위_자리():
|
||||
ordered = [{"로직": k} for k in "ABCDEFG"]
|
||||
assert [c["로직"] for c in hb.pick(ordered, "G", 5)] == ["A", "B", "C", "D", "G"]
|
||||
assert [c["로직"] for c in hb.pick(ordered, "B", 5)] == ["A", "B", "C", "D", "E"]
|
||||
assert [c["로직"] for c in hb.pick(ordered, None, 2)] == ["A", "B"]
|
||||
assert hb.pick(ordered, "G", 0) == []
|
||||
|
||||
|
||||
def test_근거_없는_줄_그대로(north):
|
||||
bare = [
|
||||
x
|
||||
|
||||
@@ -110,6 +110,19 @@ def test_역할_읽기(client, role, layer, owner, want):
|
||||
assert client.get(GRID.format(layer, "HP-000001"), params=params).status_code == want
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layer", ["company", "system"])
|
||||
def test_위_층_격자_읽기(client, layer):
|
||||
"""아래 층 사용자가 고르기 줄(층 · 주인) 그대로 위 층 파일 격자를 읽음."""
|
||||
client.who.update({"role": "SYSTEM_ADMIN"})
|
||||
assert _post(client, layer).status_code == 200
|
||||
client.who.update({"role": "USER"})
|
||||
rows = client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"]
|
||||
row = next(r for r in rows if r["층"] == layer)
|
||||
params = {"owner": row["주인"]} if row["주인"] else {}
|
||||
res = client.get(GRID.format(row["층"], row["키"]), params=params)
|
||||
assert res.status_code == 200 and len(res.json()["호표"]) == 68
|
||||
|
||||
|
||||
def test_xlsx_값만():
|
||||
data = _xlsx()
|
||||
grid = hg.read("a.xlsx", data, hx.read_book("a.xlsx", data))
|
||||
|
||||
@@ -106,6 +106,7 @@ def test_이름_같음_후보_호표_퍼센트(client):
|
||||
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순위 자리
|
||||
ref = _hopyo(client, 45)["고화매트재료비"]
|
||||
assert ref["상태"] == "호표" and ref["대상"] == {"호표": 46, "단위": "㎡"} and ref["들어감"]
|
||||
pct = _hopyo(client, 46)["잡재료비"]
|
||||
|
||||
Reference in New Issue
Block a user