fix(일위대가실무): 330 덧고침 2 — 화면 근거 맞음 판정을 조항 같음 · 단위 · 직종(줄 직종이 보통인부뿐 · 없으면 안 봄)으로

- Hopyo_View basisOk(후보 근거, 줄 근거) — 앞 마디 같음은 근거 맞음 아님 · 엑셀 직종 없는 줄(기계)도 조항 같음 + 단위면 맞음 · basisPick · 고르기 창 꼬리표가 줄 근거를 넘김
- 시험 screen(앞 마디 · 보통인부뿐 · 직종 없음) · seam(북부청 실제 응답 1순위 근거 맞음 123 = 서버 기준 · 앞 마디 0)

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-15 12:57:25 +09:00
co-authored by Claude Opus 5.5
parent 46ab9d5b11
commit 1a029141f8
5 changed files with 41 additions and 14 deletions
@@ -117,9 +117,13 @@ export function memoryOf(
return null;
}
/** 근거 맞음 — 조항 · 직종 · 단위 모두 맞음(330 [근거 맞음 모두 확정]) */
export const basisOk = (b: HopyoBasisHit | undefined): boolean =>
!!b && b.조항.length > 0 && b.직종.length > 0 && b.단위;
/** 근거 맞음(330 [근거 맞음 모두 확정]) — 조항 같음(앞 마디 아님) · 단위 맞음 · 직종 겹침
* (줄 근거 직종이 보통인부뿐이거나 없으면 — 기계 등 — 직종은 안 봄) */
export const basisOk = (b: HopyoBasisHit | undefined, line?: HopyoLineBasis): boolean =>
!!b &&
b.까닭.startsWith("조항 같음") &&
b.단위 &&
(b.직종.length > 0 || !(line?.직종 ?? []).some((x) => x !== "보통인부"));
/** 후보 근거 꼬리표 — 「조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음」 · 근거 없으면 "" */
export const basisTag = (b: HopyoBasisHit | undefined): string =>
@@ -149,8 +153,8 @@ export const sourceText = (b: HopyoLineBasis | undefined): string =>
export function basisPick(line: HopyoLine): HopyoTarget | null {
const m = line.맞춤;
if (m.상태 !== "후보") return null;
if (m.대상 && basisOk(m.대상.근거)) return m.대상;
return (m.후보 ?? []).find((c) => basisOk(c.근거)) ?? null;
if (m.대상 && basisOk(m.대상.근거, line.근거)) return m.대상;
return (m.후보 ?? []).find((c) => basisOk(c.근거, line.근거)) ?? null;
}
export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement {
@@ -450,7 +454,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement
if (tag && c.근거)
button.append(
el("span", {
className: `m02-hopyo__basis${basisOk(c.근거) ? " is-ok" : ""}`,
className: `m02-hopyo__basis${basisOk(c.근거, line.근거) ? " is-ok" : ""}`,
text: tag,
attrs: { title: `${c.근거.까닭} · 점수 ${c.근거.점수}` },
}),
+9 -3
View File
@@ -94,14 +94,20 @@ function loadTs(file) {
직종: ["특별인부"],
단위: true,
점수: 1.65,
까닭: "조항",
까닭: "조항 같음 · 직종 특별인부 · 단위 맞음",
};
const half = { ...hit, 직종: [] };
const line = (상태, 대상, 후보) => ({ 맞춤: { 상태, 대상, 후보 } });
const trade = { 출처: [["건설품셈", "3-2-1"]], 직종: ["특별인부", "보통인부"], 행: [1, 1] };
const plain = { ...trade, 직종: ["보통인부"] };
const line = (상태, 대상, 후보) => ({ 근거: trade, 맞춤: { 상태, 대상, 후보 } });
const a = { 로직: "GF000101", 근거: half };
const b = { 로직: "GC000083", 근거: hit };
return {
ok: [hit, half, { ...hit, 단위: false }, undefined].map(view.basisOk),
ok: [hit, half, { ...hit, 단위: false }, { ...hit, 까닭: "앞 마디 같음" }, undefined].map(
(b) => view.basisOk(b, trade),
),
// 엑셀 직종이 보통인부뿐 · 없음(기계) = 직종 안 봄
plain: [view.basisOk(half, plain), view.basisOk(half, { ...trade, 직종: [] })],
tag: [hit, half, undefined].map(view.basisTag),
source: [
view.sourceText({ 출처: [["건설표준품셈", "3-2-1"]], 직종: ["특별인부"], 행: [93, 101] }),
+10 -2
View File
@@ -1,7 +1,8 @@
/* M02 일위대가 실무자료 이음매(329 · 330) 헬퍼 — 실제 서버 응답(격자 · 호표 맞춤)을
* `M02_MasterTemplete_Hopyo_Sheet.ts`(행 → 호표 · 영역 A1) · `M02_MasterTemplete_Hopyo_View.ts`
* (근거 꼬리표 · 엑셀 근거 줄 · 근거 맞음 대상)에 먹임 ·
* stdin = {grid, lines} · stdout = {at, span, rows, lines: [{이름, source, tag, ok, pick}]} JSON. */
* stdin = {grid, lines, all} · stdout = {at, span, rows, lines: [{이름, source, tag, ok, pick}],
* count: {top, pick, half}} JSON — count = 「후보」 줄 1순위 근거 맞음 · 근거 맞음 대상 · 그 가운데 앞 마디. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
@@ -47,9 +48,16 @@ const out = {
이름: line.이름,
source: view.sourceText(line.근거),
tag: view.basisTag(top?.근거),
ok: view.basisOk(top?.근거),
ok: view.basisOk(top?.근거, line.근거),
pick: view.basisPick(line)?.로직 ?? null,
};
}),
};
const open = (input.all ?? []).filter((line) => line.맞춤.상태 === "후보");
const picks = open.map((line) => view.basisPick(line)).filter(Boolean);
out.count = {
top: open.filter((line) => view.basisOk((line.맞춤.후보 ?? [])[0]?.근거, line.근거)).length,
pick: picks.length,
half: picks.filter((c) => !c.근거.까닭.startsWith("조항 같음")).length,
};
process.stdout.write(JSON.stringify(out));
+2 -1
View File
@@ -193,7 +193,8 @@ def test_엑셀_보기_창_여는_자리와_늦게_불러옴():
def test_고르기_창_근거_꼬리표와_근거_맞음():
basis = _run()["basis"]
assert basis["ok"] == [True, False, False, False] # 조항 · 직종 · 단위 모두
assert basis["ok"] == [True, False, False, False, False] # 조항 같음 · 직종 · 단위 모두
assert basis["plain"] == [True, True] # 줄 직종이 보통인부뿐 · 없음 = 직종 안 봄
assert basis["tag"] == [
"조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음",
"조항 건설 공통 3-2-1 · 단위 같음",
+10 -2
View File
@@ -2,7 +2,8 @@
화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet · Hopyo_View)에 먹임.
행 22 → 제3호표 · 25 빈 줄 · 26 → 제4호표 · 영역 B20:O24 · 단산93 행 4229:4244 ·
엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 대상.
엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 대상 · 북부청 「후보」 줄 1순위 근거 맞음 = 서버 기준 123 ·
앞 마디 같음 0(330 덧고침 2).
"""
import json
@@ -36,7 +37,13 @@ def test_실제_응답_화면_도우미(client):
for no in (3, 12, 38, 58):
res = client.get(f"{URL.format('personal')}/HP-000001/{no}").json()
lines += [x for x in res["호표"]["줄"] if x.get("근거")]
got = _node({"grid": grid, "lines": lines})
every = []
for one in grid["호표"]:
res = client.get(f"{URL.format('personal')}/HP-000001/{one['번']}").json()
every += [x for x in res["호표"]["줄"] if x["갈래"] == "단가산출"]
got = _node({"grid": grid, "lines": lines, "all": every})
assert got["count"]["top"] == 123 and got["count"]["half"] == 0
assert got["count"]["pick"] >= 123
assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄
assert got["span"] == "B20:O24" and got["rows"] == "4229:4244"
by = {x["이름"]: x for x in got["lines"]}
@@ -45,3 +52,4 @@ def test_실제_응답_화면_도우미(client):
assert dig["tag"] == "조항 건설품셈 공통 3-2-1 · 직종 특별인부 · 단위 같음"
assert dig["ok"] is True and dig["pick"] == "GC000083"
assert by["비탈면 보호공"]["pick"] == "GC000143"
assert by["흙깍기(토사)"]["ok"] is True # 엑셀 직종 없는 줄(기계) = 조항 같음 + 단위