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:
@@ -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:
|
||||
# 한 층 아래에 일위대가가 있으면 **후보로 보여준다** — 임의로 고르지 않는다
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user