feat(B08): 타설 방식 단가 차이 표시 + 값 자리 식 깃발

타설 방식 — 「무엇을 정해야 하는지」만으로는 부족하고 「정하면 얼마나 달라지는지」가
보여야 사용자가 판단함. 세 방식 단가 차이가 6배라 조용히 기본값을 쓰면 총액이 갈림.
- 방식 이름을 늘 값 옆에 보임(「적용 중: 레디믹스트 (기본값 — 확인 필요)」).
  코드(12-01-01)만으로는 무엇을 쓰는지 모름.
- 참고 단가 세 줄을 안내로 띄움 — 레디믹스트 65,826 · 기계비빔 163,508 ·
  인력비빔 408,327 원/㎥. ⚠ **표시 전용**이며 B08 의 어떤 계산에도 안 들어감
  (금액은 B09 몫). 출처·산출일을 데이터에 함께 적음.

품셈 마스터 — 값 자리에 식이 적힌 칸에 깃발(`expression_cells`).
`0.2 × 30%`(기초잡석 소할)처럼 계산이 그대로 적힌 칸은 값이 숫자로 안 읽혀
그 성분이 조용히 빠짐. 형태 판정은 통과하고 배분율 딱지도 없어 아무 검사에도
안 걸리던 자리(서브 창이 실물에서 부딪힘 — 기초잡석이 부설다짐 0.6인만으로 섬).
⚠ 식을 계산하지 않고 드러내기만 함 — 뜻을 잘못 읽으면 조용히 틀림.
31표에서 잡힘(9-12·9-13 터파기 계열 · 12-24 뒷채움 · 12-25 기초잡석 등).

검증 — 품셈 34건 통과, 전체 588 passed, tsc 오류 0.
화면에서 방식 이름·기본값 표시·단가 세 줄 확인.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 01:21:44 +09:00
co-authored by Claude Opus 5
parent 24fc6ec090
commit 01578eaed6
7 changed files with 655 additions and 25 deletions
@@ -314,6 +314,38 @@ SHARE_TAG_RE = re.compile(
CAPACITY_SYMBOLS = {"K", "k", "f", "E", "Cm", "㎝(sec)", "q", "qo", "Q"}
# ⚠ **값 자리에 숫자가 아니라 식이 적힌 칸** (2026-09-07 서브 창 제보로 확인).
# `0.2 × 30%`(기초잡석 소할) · `(0.2+0.26)/2` · `1/1.3` 처럼 계산이 그대로 적혀 있다.
# 형태 판정은 통과하는데 **값이 숫자로 안 읽혀 그 성분이 조용히 빠진다** —
# `partial_ratio` 와 같은 병인데 배분율 딱지가 없어 아무 검사에도 안 걸렸다.
# ⚠ 여기서 **식을 계산하지 않는다.** 뜻을 잘못 읽으면 조용히 틀리므로 **드러내기만** 한다.
_PLAIN_NUMBER_RE = re.compile(r"^[\d,]+(?:\.\d+)?$")
_CALC_CELL_RE = re.compile(r"^[\d,.\s()×xX*/÷+\-%]+$")
_CALC_MAX_LEN = 22
def expression_cells(table: dict[str, Any]) -> list[str]:
"""값 자리에 식이 적힌 칸 목록. 없으면 빈 목록."""
found: list[str] = []
for row in table.get("rows", []):
for cell in row:
text = norm(cell)
if not text or len(text) > _CALC_MAX_LEN or _PLAIN_NUMBER_RE.match(text):
continue
if (
_CALC_CELL_RE.match(text)
and re.search(r"\d", text)
and re.search(r"[×xX*/÷+]", text)
):
found.append(text)
# 순서를 지키되 중복은 지운다 — 같은 식이 여러 줄에 반복된다.
seen: list[str] = []
for item in found:
if item not in seen:
seen.append(item)
return seen
def resource_shares(table: dict[str, Any]) -> dict[str, float]:
"""`{인력: 10.0, 장비: 90.0}` — 딱지에 붙은 몫. 없으면 빈 칸."""
shares: dict[str, float] = {}
@@ -421,6 +453,8 @@ def build() -> dict[str, Any]:
"basis_source": basis_source,
"resource_shares": shares,
"partial_ratio": partial,
# ⚠ 값 자리에 식이 적힌 칸 — 그 성분은 숫자로 안 읽히므로 **금액을 만들면 안 된다**.
"expression_cells": expression_cells(table),
# 공식 기호가 이 표에 직접 있는가 — 없으면 앞 표에서 물려받는 모양이다.
"capacity_formula_here": capacity_formula_pending(table),
"variant_key": variant_axis(table),
@@ -31,6 +31,7 @@ from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryInput
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table as build_summary_table
from B08_Quantity.B08_Quantity_Engine_EarthworkTable import StationArea, build_table
from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table
from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping
from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_preparation_table
from B08_Quantity.B08_Quantity_Engine_HaulSummary import summary_input_rows
from B08_Quantity.B08_Quantity_Engine_SlopeArea import build_table as build_slope_table
@@ -103,7 +104,14 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
)
method, method_is_default = concrete_placing_method(settings)
# ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다.
table["concrete_placing"] = {"method": method, "is_default": method_is_default}
table["concrete_placing"] = {
"method": method,
"is_default": method_is_default,
# ⚠ **표시 전용 참고값** — 「무엇을 정해야 하는지」만으로는 부족하고
# 「정하면 얼마나 달라지는지」가 보여야 사용자가 판단한다(2026-09-07 조율 창).
# B08 의 어떤 계산에도 안 들어간다.
"price_hint": (load_mapping().concrete_placing or {}).get("price_hint_krw_per_m3"),
}
table["settings"] = settings
table["project_root_known"] = project_root is not None
table["route_id"] = route_id
+37 -1
View File
@@ -134,6 +134,13 @@ function numberField(label: string, value: number, onInput: (value: number) => v
return row;
}
/** 타설 방식 표기 — 코드가 아니라 사람이 읽는 이름으로 보인다. */
const PLACING_LABELS: Record<string, string> = {
ready_mixed: "레디믹스트",
machine_mixed: "기계비빔",
hand_mixed: "인력비빔",
};
/** 고르는 칸. 첫 보기는 **「안 정함」**이고 그것이 기본이다 — 찍으면 값이 조용히 틀린다. */
function selectField(
label: string,
@@ -274,8 +281,37 @@ function buildQuantitySidePanel(
),
);
const placing = (
table as unknown as { concrete_placing?: { method: string; is_default: boolean } }
table as unknown as {
concrete_placing?: {
method: string;
is_default: boolean;
price_hint?: { basis?: string; values?: Record<string, number> };
};
}
).concrete_placing;
if (placing) {
// ⚠ 방식 이름을 **늘** 값 옆에 보인다 — 코드(`12-01-01`)만으로는 무엇을 쓰는지 모른다.
const label = PLACING_LABELS[placing.method] ?? placing.method;
panel.append(
field(
L("B08_Quantity_Placing_Current"),
placing.is_default ? `${label} (${L("B08_Quantity_Placing_Default_Tag")})` : label,
),
);
}
// ⚠ 「정하면 얼마나 달라지는지」까지 보여야 사용자가 판단한다. 이 값은 **참고 표시 전용**이고
// B08 의 어떤 계산에도 안 들어간다(금액은 B09 몫).
const hint = placing?.price_hint;
if (hint?.values) {
const line = document.createElement("p");
line.className = "b08-quantity__notice";
const parts = Object.entries(hint.values).map(
([key, value]) =>
`${PLACING_LABELS[key] ?? key} ${Math.round(value).toLocaleString("ko-KR")}`,
);
line.textContent = `${L("B08_Quantity_Placing_Hint")} ${hint.basis ?? ""}${parts.join(" · ")}`;
panel.append(line);
}
if (placing?.is_default) {
// 「확인 필요」만 있으면 무엇을 정해야 하는지 모른다 — **지금 무엇으로 돌고 있는지**를 함께 적는다.
const notice = document.createElement("p");
@@ -142,19 +142,12 @@
"items": [
{
"group": "지장목제거",
"candidates": [
"FP-04-01 수확베기",
"FP-04-02 단목베기",
"FP-04-03 위험목 베기"
],
"candidates": ["FP-04-01 수확베기", "FP-04-02 단목베기", "FP-04-03 위험목 베기"],
"why": "품셈 4장은 벌목을 목적별로 가르는데 임도 지장목이 어느 쪽인지 원본이 말하지 않음"
},
{
"group": "흙깎기/측구터파기 암",
"candidates": [
"FP-09-04 암절취(리핑)",
"FP-09-05 발파암"
],
"candidates": ["FP-09-04 암절취(리핑)", "FP-09-05 발파암"],
"why": "설계자가 넣는 암 갈래 이름(풍화암·연암·보통암·경암)이 리핑이냐 발파냐를 말하지 않음. 갈래마다 시공법을 지정하는 칸이 필요함"
}
]
@@ -173,10 +166,7 @@
"why": "품셈 12장에 「옹벽」 공종이 없음. 실무 내역은 「반중력식옹벽 H=2.0」 한 줄이고 그 일위대가가 위 공종을 묶음.",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김",
"placing_note": "타설 코드는 프로젝트 설정의 타설 방식으로 갈림(기본 레디믹스트). 철근구조물 판정은 원단위의 D13·D16 에서 자동으로 나옴.",
"not_ready": [
"FP-12-03",
"FP-12-25"
],
"not_ready": ["FP-12-03", "FP-12-25"],
"not_ready_why": "B09 일위대가가 아직 안 섬 — 지금 세우면 절반짜리가 됨(2026-09-07 조율 창)"
}
]
@@ -190,11 +180,18 @@
},
"default_method": "ready_mixed",
"default_is_provisional": true,
"structure_kinds": [
"무근구조물",
"철근구조물",
"소형구조물"
],
"kind_rule": "원단위 성분에 철근(이형철근·원형철근)이 있으면 철근구조물, 없으면 무근구조물. 소형구조물 판정 기준은 미확보."
"structure_kinds": ["무근구조물", "철근구조물", "소형구조물"],
"kind_rule": "원단위 성분에 철근(이형철근·원형철근)이 있으면 철근구조물, 없으면 무근구조물. 소형구조물 판정 기준은 미확보.",
"price_hint_krw_per_m3": {
"note": "⚠ **표시 전용.** 사용자가 타설 방식을 고를 때 「정하면 얼마나 달라지는지」를 보이려고 둔 값이며 B08 의 어떤 계산에도 들어가지 않는다(금액은 B09 몫 — 8-2 경계). 값은 B09 가 2026-09-07 에 낸 철근구조물 기준 단가이고, 요율·노임이 바뀌면 어긋난다 — 화면이 「참고」임을 함께 적는다.",
"basis": "철근구조물 콘크리트 타설 (원/㎥)",
"computed_by": "B09",
"computed_on": "2026-09-07",
"values": {
"ready_mixed": 65826,
"machine_mixed": 163508,
"hand_mixed": 408327
}
}
}
}
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-08T01:00:54+09:00",
"generated_at": "2026-09-08T01:18:06+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_forest",
@@ -12,8 +12,8 @@
"files": [
{
"file": "work_item_master_2026-01-01.json",
"sha256": "fe454c56c9dc01ad7dae04a8f90d776d08c5ce33badeadb2606b35234bfce7eb",
"size_bytes": 785034
"sha256": "086441e1380441259e860620d1e81b886019f11838dea601c0a3797f6f1a3d3e",
"size_bytes": 800211
},
{
"file": "form_undetermined_2026-01-01.json",
File diff suppressed because it is too large Load Diff
+3
View File
@@ -631,6 +631,9 @@ export const ui_locales_b2 = {
B08_Quantity_Side_Placing: ["콘크리트 타설", "Concrete Placing"],
B08_Quantity_Side_Placing_Label: ["타설 방식", "Method"],
B08_Quantity_Placing_Unset: ["안 정함(기본값 사용)", "Not set (default)"],
B08_Quantity_Placing_Current: ["적용 중", "In use"],
B08_Quantity_Placing_Default_Tag: ["기본값 — 확인 필요", "default — needs review"],
B08_Quantity_Placing_Hint: ["참고 단가(B09 산출):", "Reference unit price (from B09):"],
B08_Quantity_Placing_Ready: ["레디믹스트", "Ready-mixed"],
B08_Quantity_Placing_Machine: ["기계비빔", "Machine-mixed"],
B08_Quantity_Placing_Hand: ["인력비빔", "Hand-mixed"],