fix(B09): 표의 일부만 붙은 단가가 조용히 서던 것 — 본표가 말하게 함
일위대가가 선 141 공종 중 **70 공종**이 「품셈 표에 있는데 못 붙은 줄」을 갖고 있었음. 막지 않는 것은 맞으나(막으면 정상 공종이 무더기로 멈춤) 화면이 말하지 않아 **조용히 싼 단가**가 내역서에 그대로 들 수 있었음. 실례 — 초류종자살포(FP-05-24-01, 수량 17,873.80㎡) 품셈 5-24-1 은 자재 5(종자·비료·피복제·침식안정제·색소) + 장비 3(종자살포기·트럭· 물탱크) + 인력 2 인데, 우리 단가는 **인력 둘만**으로 233.4원/㎡ 였음. - `UnitPriceBuild.unattached` 신설 — 공종별로 못 붙은 줄 이름을 들고 있음 - 본표(`detail_of`)가 `unattached`·`unattached_note` 를 함께 냄 - 일위대가 상세 화면이 그 문구를 굵게 띄움. 다 붙은 단가는 아무 말도 안 함 (늘 경고가 뜨면 아무도 안 읽음) - 품셈이 자간을 벌려 적은 이름(「종 자」)은 눌러서 보임 ⚠ 지역 이름 충돌 하나 잡음 — `names` 가 공종 이름표라 같은 이름을 쓰니 조립이 통째로 터졌음. 주석으로 못 박음. 검증: pytest 282 통과(신규 3) · tsc 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,6 +108,9 @@ interface UnitPriceDetailDto {
|
|||||||
expense: string;
|
expense: string;
|
||||||
total: string;
|
total: string;
|
||||||
sum_matches: boolean;
|
sum_matches: boolean;
|
||||||
|
/** 품셈 표에 있는데 아직 안 붙은 줄 — 있으면 이 단가는 **붙은 줄만의 값**이다. */
|
||||||
|
unattached: string[];
|
||||||
|
unattached_note: string;
|
||||||
rows: UnitPriceDetailRow[];
|
rows: UnitPriceDetailRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +358,17 @@ function buildUnitPriceDetail(
|
|||||||
` · ${detail.sum_matches ? L("B09_Estimation_UP_SumOk") : L("B09_Estimation_UP_SumBad")}`;
|
` · ${detail.sum_matches ? L("B09_Estimation_UP_SumOk") : L("B09_Estimation_UP_SumBad")}`;
|
||||||
wrap.append(caption);
|
wrap.append(caption);
|
||||||
|
|
||||||
|
// ⚠ **일부만 선 단가는 반드시 말한다.** 안 말하면 조용히 싼 값이 내역서에 그대로 든다
|
||||||
|
// (2026-09-09 실측: 일위대가가 선 141 공종 중 70 공종이 이 자리 — 초류종자살포는
|
||||||
|
// 자재 다섯·장비 셋이 빠진 채 인력 둘만으로 서 있었다).
|
||||||
|
if (detail.unattached_note) {
|
||||||
|
const gap = document.createElement("div");
|
||||||
|
gap.className = "b09-hint";
|
||||||
|
gap.style.fontWeight = "600";
|
||||||
|
gap.textContent = detail.unattached_note;
|
||||||
|
wrap.append(gap);
|
||||||
|
}
|
||||||
|
|
||||||
// 행별로 0.1원 미만을 버리므로 전정밀 합과 끝자리가 어긋난다 — **정상이다.**
|
// 행별로 0.1원 미만을 버리므로 전정밀 합과 끝자리가 어긋난다 — **정상이다.**
|
||||||
// 숨기면 나중에 「합계가 안 맞는다」며 계산을 고치려 든다.
|
// 숨기면 나중에 「합계가 안 맞는다」며 계산을 고치려 든다.
|
||||||
if (detail.precise_total !== detail.total) {
|
if (detail.precise_total !== detail.total) {
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ class UnitPriceBuild:
|
|||||||
#: ⚠ 일위대가가 **아예 안 선** 경우에도 남는다 — 「일위대가 없음」과 「성분이 빠져
|
#: ⚠ 일위대가가 **아예 안 선** 경우에도 남는다 — 「일위대가 없음」과 「성분이 빠져
|
||||||
#: 못 세움」은 할 일이 다르므로 화면에서 갈라 보여야 한다(2026-09-08 산마루측구).
|
#: 못 세움」은 할 일이 다르므로 화면에서 갈라 보여야 한다(2026-09-08 산마루측구).
|
||||||
component_gaps: dict[str, str] = field(default_factory=dict)
|
component_gaps: dict[str, str] = field(default_factory=dict)
|
||||||
|
#: 공종코드 → **표에 있는데 못 붙은 줄 이름들**.
|
||||||
|
#: ⚠ 값이 있다는 것은 그 단가가 **표의 일부만으로 서 있다**는 뜻이다 — 대개 자재·기계
|
||||||
|
#: 카탈로그가 없어서다. 막지는 않지만(막으면 정상 공종이 무더기로 멈춘다) **화면이
|
||||||
|
#: 반드시 말해야 한다.** 안 말하면 조용히 싼 단가가 내역서에 그대로 든다
|
||||||
|
#: (2026-09-09 실측: 일위대가가 선 141 공종 중 **70 공종**이 이 자리였다).
|
||||||
|
unattached: dict[str, list[str]] = field(default_factory=dict)
|
||||||
#: 배분율 표인데 일부 몫만 붙은 공종 — 「단가가 일부만 섬」. 값은 붙은 몫(%).
|
#: 배분율 표인데 일부 몫만 붙은 공종 — 「단가가 일부만 섬」. 값은 붙은 몫(%).
|
||||||
partial_ratio: dict[str, Decimal] = field(default_factory=dict)
|
partial_ratio: dict[str, Decimal] = field(default_factory=dict)
|
||||||
#: 시공능력 공식으로 장비 몫을 세운 공종 — 산출근거를 화면에 그대로 보인다.
|
#: 시공능력 공식으로 장비 몫을 세운 공종 — 산출근거를 화면에 그대로 보인다.
|
||||||
@@ -354,6 +360,13 @@ def build_unit_prices(
|
|||||||
|
|
||||||
build = UnitPriceBuild()
|
build = UnitPriceBuild()
|
||||||
build.component_gaps = dict(axis.partial_items)
|
build.component_gaps = dict(axis.partial_items)
|
||||||
|
for unmatched_row in axis.unmatched:
|
||||||
|
# ⚠ 지역 이름을 조심할 것 — 바로 위 `names` 는 **공종 이름표**다. 같은 이름을 쓰면
|
||||||
|
# 그 표가 리스트로 덮여 조립이 통째로 터진다(2026-09-09 실측).
|
||||||
|
labels = build.unattached.setdefault(unmatched_row.work_item_code, [])
|
||||||
|
label = " ".join(str(unmatched_row.cell).split())
|
||||||
|
if label and label not in labels:
|
||||||
|
labels.append(label)
|
||||||
missing_basis = load_basis_missing()
|
missing_basis = load_basis_missing()
|
||||||
wages = load_operator_wages()
|
wages = load_operator_wages()
|
||||||
_add_labor_titles(build.book, wages)
|
_add_labor_titles(build.book, wages)
|
||||||
|
|||||||
@@ -165,6 +165,9 @@ def detail_of(build: UnitPriceBuild, code: str) -> dict:
|
|||||||
"""본표 — 「그것이 무엇으로 이루어졌나」. 줄마다 원천과 파고들기 여부를 함께 낸다."""
|
"""본표 — 「그것이 무엇으로 이루어졌나」. 줄마다 원천과 파고들기 여부를 함께 낸다."""
|
||||||
title = build.book.title(code)
|
title = build.book.title(code)
|
||||||
money = build.book.resolve(code)
|
money = build.book.resolve(code)
|
||||||
|
# 코드에서 공종을 도로 뽑는다 — 「B-FP-09-11-01#갈래」의 갈래는 떼고 본다.
|
||||||
|
work_item_code = code[2:].split("#")[0] if code.startswith("B-") else ""
|
||||||
|
unattached = list(build.unattached.get(work_item_code, []))
|
||||||
rows: list[dict] = []
|
rows: list[dict] = []
|
||||||
for detail in build.book.details.get(code, []):
|
for detail in build.book.details.get(code, []):
|
||||||
if detail.percent_of_labor is not None:
|
if detail.percent_of_labor is not None:
|
||||||
@@ -255,4 +258,16 @@ def detail_of(build: UnitPriceBuild, code: str) -> dict:
|
|||||||
# 전정밀 합과의 차이 — 행별 절사 탓에 끝자리가 어긋나는 것은 **정상**이다.
|
# 전정밀 합과의 차이 — 행별 절사 탓에 끝자리가 어긋나는 것은 **정상**이다.
|
||||||
"precise_total": _money_text(money.total),
|
"precise_total": _money_text(money.total),
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
|
# ⚠ **표에 있는데 못 붙은 줄** — 이 단가가 일부만으로 섰다는 뜻이다.
|
||||||
|
# 안 보이면 조용히 싼 단가가 내역서에 그대로 든다.
|
||||||
|
"unattached": unattached,
|
||||||
|
"unattached_note": (
|
||||||
|
f"⚠ 품셈 표에 있는 {len(unattached)}줄이 아직 안 붙었습니다 — "
|
||||||
|
f"{', '.join(unattached[:4])}"
|
||||||
|
+ (" 등" if len(unattached) > 4 else "")
|
||||||
|
+ ". 자재·기계 카탈로그가 서면 채워집니다. 그때까지 이 단가는 "
|
||||||
|
"**붙은 줄만의 값**입니다."
|
||||||
|
)
|
||||||
|
if unattached
|
||||||
|
else "",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user