feat(B09): 막힘 사유를 갈래별로 화면에 가름

B08 이 `blocked_reason`(사용자 말 문구) + `blocked_kind` 를 실어 보내기 시작함

- 문구는 **B08 것을 그대로** 씀 — 두 벌로 짜면 한쪽만 고쳐지는 자리가 됨
- 화면 미확보 목록을 두 갈래로 가름
  · 「입력하면 풀리는 것 — 설계 화면에서 값을 고르면 금액이 섭니다」(`input_missing`)
  · 「우리가 만들어야 하는 것 — 원단위·전개식이 아직 없습니다」
- 한 목록에 섞이면 사용자가 「후보를 고르면 되나」로 잘못 읽음(돌쌓기가 그 자리였음)
- ⚠ 빈 값은 오류가 아니라 **「아직 안 고른 상태」** — 랩탑이 「— 선택 —」 빈 칸으로
  열어 둔 것과 짝임(첫 항목을 슬쩍 고르면 근거 없는 값이 단가로 흘러감)

계약 시험의 측점 까닭 보강 — 「지금은 안 씀 — 구간별 산출근거를 낼 때 쓸 것」

검증: pytest 195 통과, tsc 0건

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 04:43:28 +09:00
co-authored by Claude Opus 5
parent af3b7c6d83
commit 2e9e0d1f4a
3 changed files with 69 additions and 7 deletions
@@ -39,6 +39,13 @@ _ZERO = Decimal(0)
#: **관급자재대에도, 도급 재료비에도 넣지 않는다** — 어느 쪽에 넣어도 총액이 틀린다.
SUPPLY_UNKNOWN = "unknown"
#: 막힘 갈래를 사람 말로. **할 일이 다르므로 화면에서 갈라 보인다.**
_BLOCKED_LABELS = {
"input_missing": "입력이 필요합니다",
"unit_data_missing": "원단위가 없습니다(우리가 만들 것)",
"formula_missing": "전개식이 없습니다(우리가 만들 것)",
}
class BillError(ValueError):
"""내역서를 세울 수 없는 경우. 빈 표를 돌려주지 않고 멈춘다."""
@@ -72,6 +79,11 @@ class HandoffWorkItem:
#: 묶음인데 아직 못 채운 조각 — 「단가 없음」과 「물량 없음」을 갈라 적는다.
composite_not_ready: tuple = ()
structure_kind: str = ""
#: B08 이 적어 보낸 막힘 사유 — **문구는 B08 것을 그대로 쓴다**(두 벌로 짜지 않는다).
blocked_reason: str = ""
#: 막힘 갈래 — `input_missing`(사용자가 입력하면 풀림) /
#: `unit_data_missing`·`formula_missing`(우리가 만들어야 함). 할 일이 다르므로 가른다.
blocked_kind: str = ""
@property
def display_name(self) -> str:
@@ -209,6 +221,8 @@ def parse_handoff(payload: dict[str, Any]) -> tuple[list[HandoffWorkItem], list[
composite_parts=tuple(row.get("composite_parts") or ()),
composite_not_ready=tuple(row.get("composite_not_ready") or ()),
structure_kind=row.get("structure_kind") or "",
blocked_reason=row.get("blocked_reason") or "",
blocked_kind=row.get("blocked_kind") or "",
)
for row in payload["work_items"]
]
@@ -539,6 +553,23 @@ def _leaf_row(
result.excluded.append(row)
return row
if item.blocked_reason:
# B08 이 「왜 못 골랐는지」를 적어 보냈다 — **그 문구를 그대로** 보인다.
# 사용자가 입력하면 풀리는 것(`input_missing`)과 우리가 만들어야 하는 것을
# 가르지 않으면, 사용자가 「후보를 고르면 되나」로 잘못 읽는다.
row.note = f"{_BLOCKED_LABELS.get(item.blocked_kind, '막힘')}{item.blocked_reason}"
result.missing.append(
{
"name": row.name,
"code": node.code,
"unit": row.unit,
"quantity": str(item.quantity),
"reason": row.note,
"blocked_kind": item.blocked_kind,
}
)
return row
price_code = f"B-{node.code}"
if price_code not in unit_prices.book.titles:
# 한 층 아래에 일위대가가 있으면 **후보로 보여준다** — 임의로 고르지 않는다
+30 -7
View File
@@ -668,7 +668,13 @@ interface BillDto {
rows: number;
detail_rows: number;
body_total_krw: string;
missing: Array<{ name: string; reason: string; unit?: string; quantity?: string }>;
missing: Array<{
name: string;
reason: string;
unit?: string;
quantity?: string;
blocked_kind?: string;
}>;
notes: string[];
};
}
@@ -872,13 +878,30 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
note.className = "b09-hint";
note.textContent = `${L("B09_Estimation_Boq_Missing")} (${bill.summary.missing.length})`;
body.append(note);
const list = document.createElement("ul");
for (const item of bill.summary.missing) {
const li = document.createElement("li");
li.textContent = `${item.name}${item.reason}`;
list.append(li);
// ⚠ **할 일이 다르므로 갈라 보인다** — 「사용자가 입력하면 풀리는 것」과
// 「우리가 만들어야 하는 것」. 한 목록에 섞으면 사용자가 무엇을 해야 할지 못 읽는다.
const needsInput = bill.summary.missing.filter(
(item) => item.blocked_kind === "input_missing",
);
const rest = bill.summary.missing.filter((item) => item.blocked_kind !== "input_missing");
for (const [labelKey, group] of [
["B09_Estimation_Boq_NeedsInput", needsInput],
["B09_Estimation_Boq_NeedsWork", rest],
] as Array<[keyof typeof ui_locales, typeof bill.summary.missing]>) {
if (group.length === 0) continue;
const head = document.createElement("div");
head.className = "b09-hint";
head.textContent = `${L(labelKey)} (${group.length})`;
body.append(head);
const list = document.createElement("ul");
for (const item of group) {
const li = document.createElement("li");
li.textContent = `${item.name}${item.reason}`;
list.append(li);
}
body.append(list);
}
body.append(list);
}
if (bill.materials.length > 0) {
+8
View File
@@ -679,6 +679,14 @@ export const ui_locales_b2 = {
"검산용 줄 — 수량만 보이고 금액을 매기지 않습니다",
"Check rows — quantity only, never priced",
],
B09_Estimation_Boq_NeedsInput: [
"입력하면 풀리는 것 — 설계 화면에서 값을 고르면 금액이 섭니다",
"Waiting on input — pick the value on the design screen and the amount appears",
],
B09_Estimation_Boq_NeedsWork: [
"우리가 만들어야 하는 것 — 원단위·전개식이 아직 없습니다",
"Needs build — unit data or formula is missing",
],
B09_Estimation_Boq_Missing: [
"금액을 못 세운 줄 — 0 으로 채우지 않고 그대로 보입니다",
"Rows without an amount — shown as-is, not zero-filled",