feat(B08): 묶음 조각 부위별 수량 + 철근 갈래 원문 자동 판정
옹벽 네 조각이 B09 에 다 서서 이제 물량을 실어 보냄. - `composite_parts` 에 조각마다 수량·단위·근거(`basis_kind`)를 실음. 코드만 보내면 받는 쪽이 상세 줄을 못 세움. 조각은 **원단위 성분 이름으로** 찾고 못 찾으면 0 이 아니라 사유와 함께 `not_ready` 로 남김. - ⚠ 철근은 ㎏ → ton 환산. 단가가 원/ton 인데 원단위는 ㎏ 이라 안 맞추면 1000배 틀림. D13 13.45 + D16 30.42 = 43.87㎏/m × 10m = 0.4387ton. - 철근 갈래는 **품셈 12-3 [주]① 원문**이 정함 — 「간단: 중력식 옹벽 / 보통: 반중력식 옹벽 / 복잡: 부벽식 옹벽」. 거푸집 사용횟수(1-7-1)와 같은 자리라 사람이 고를 칸을 만들지 않음. 캔틸레버식은 예시에 없어 「보통」으로 때우지 않고 미확보로 드러냄. - 전개 결과에 저장 제원(`options`)을 실어 뒤 단계가 형식을 읽게 함. 치수를 다시 쓰라는 뜻이 아니라 읽으라는 것 — 치수 정본은 여전히 하나. - 화면(구조물 원단위 탭)에 묶음 조각과 갈래를 보임. ⚠ 옛 시험 하나를 계약 변경에 맞춰 옮김 — `composite_not_ready` 가 「일위대가가 아직 안 선 공종」에서 「물량을 못 채운 조각」으로 뜻이 바뀜. 검증 — 인계 54건 통과, 전체 594 passed, tsc 오류 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,11 @@ from common_util.common_util_quantity_spread import spread_by_unit
|
||||
DATASET_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_work_item_mapping"
|
||||
DATASET_PREFIX = "work_item_mapping_"
|
||||
|
||||
#: 철근 갈래표 — **품셈 12-3 [주]① 원문**이 구조물 예시로 갈라 둔 것이라 사람이 고르는 값이
|
||||
#: 아니다(거푸집 사용횟수 1-7-1 과 같은 자리). 원문 예시에 안 걸리면 지어내지 않는다.
|
||||
REBAR_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_rebar"
|
||||
REBAR_PREFIX = "rebar_complexity_"
|
||||
|
||||
#: 줄이 어디서 왔나 — 되짚을 때 쓴다.
|
||||
ORIGIN_EARTHWORK = "earthwork"
|
||||
ORIGIN_STRUCTURE = "structure"
|
||||
@@ -92,6 +97,7 @@ class WorkItemMapping:
|
||||
pending_user: dict[str, Any] = field(default_factory=dict)
|
||||
composite: dict[str, Any] = field(default_factory=dict)
|
||||
concrete_placing: dict[str, Any] = field(default_factory=dict)
|
||||
unit_conversion: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def for_earthwork(self, group: str, ground: str | None) -> dict[str, Any] | None: # noqa: D401
|
||||
"""공종+지반유형으로 찾는다. 지반이 갈리지 않는 공종은 `ground` 없는 줄이 받는다."""
|
||||
@@ -143,9 +149,75 @@ def load_mapping(path: Path | None = None) -> WorkItemMapping:
|
||||
pending_user=payload.get("pending_user") or {},
|
||||
composite=payload.get("composite") or {},
|
||||
concrete_placing=payload.get("concrete_placing") or {},
|
||||
unit_conversion=(payload.get("composite") or {}).get("unit_conversion") or {},
|
||||
)
|
||||
|
||||
|
||||
def composite_quantities(
|
||||
structure: dict[str, Any],
|
||||
composite: dict[str, Any],
|
||||
mapping: WorkItemMapping,
|
||||
) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
"""묶음 조각마다 **부위별 수량**을 채운다. (조각 목록, 못 채운 사유).
|
||||
|
||||
⚠ 조각은 **원단위 성분 이름으로** 찾는다. 이름이 어긋나면 물량이 조용히 0 이 되므로
|
||||
못 찾으면 그 조각을 `not_ready` 로 남기고 사유를 적는다 — 0 을 적지 않는다.
|
||||
⚠ **단위를 반드시 맞춘다.** 철근 단가는 `원/ton` 인데 원단위는 `㎏` 이다.
|
||||
안 맞추면 **1000배 틀린다** — 밑수에서 겪은 것과 같은 자리다.
|
||||
"""
|
||||
amounts: dict[str, tuple[float, str]] = {}
|
||||
for component in structure.get("components") or []:
|
||||
name = str(component.get("name") or "").strip()
|
||||
amounts[name] = (float(component.get("amount") or 0.0), str(component.get("unit") or ""))
|
||||
kg_to_ton = float((mapping.unit_conversion or {}).get("kg_to_ton") or 0.001)
|
||||
|
||||
parts: list[dict[str, Any]] = []
|
||||
missing: list[str] = []
|
||||
for spec in composite.get("parts") or []:
|
||||
if not isinstance(spec, dict): # 옛 모양(코드 문자열)은 그대로 흘린다
|
||||
parts.append({"code": str(spec)})
|
||||
continue
|
||||
sources = list(spec.get("from_components") or [])
|
||||
found = [name for name in sources if name in amounts]
|
||||
total = sum(amounts[name][0] for name in found)
|
||||
if spec.get("unit_from") == "kg" and spec.get("unit") == "ton":
|
||||
total *= kg_to_ton
|
||||
kinds = {
|
||||
component.get("basis_kind")
|
||||
for component in structure.get("components") or []
|
||||
if str(component.get("name") or "").strip() in found
|
||||
}
|
||||
suffix = spec.get("kind_suffix")
|
||||
entry: dict[str, Any] = {
|
||||
"code": spec.get("code"),
|
||||
"name": spec.get("name"),
|
||||
"unit": spec.get("unit"),
|
||||
"quantity": total if found else None,
|
||||
# 조각마다 근거를 단다 — 치수 전개와 관측값이 한 묶음에 섞인다.
|
||||
"basis_kind": next(iter(kinds)) if len(kinds) == 1 else (sorted(kinds) or None),
|
||||
"from_components": sources,
|
||||
}
|
||||
if suffix == "rebar_complexity":
|
||||
# 갈래는 원문이 정한다 — 화면·인계에 이름과 근거를 함께 실어 사람이 검증하게 한다.
|
||||
complexity, why = rebar_complexity(
|
||||
str(structure.get("type_id") or ""), structure.get("options") or {}
|
||||
)
|
||||
entry["kind"] = complexity
|
||||
entry["kind_basis"] = why
|
||||
if complexity:
|
||||
entry["code"] = f"{spec.get('code')}#{complexity}"
|
||||
else:
|
||||
entry["not_ready"] = True
|
||||
entry["why"] = why
|
||||
missing.append(f"{spec.get('name')}: {why}")
|
||||
if spec.get("not_ready") or not found:
|
||||
entry["not_ready"] = True
|
||||
entry["why"] = str(spec.get("why") or "원단위에 해당 성분이 없음")
|
||||
missing.append(f"{spec.get('name') or spec.get('code')}: {entry['why']}")
|
||||
parts.append(entry)
|
||||
return parts, missing
|
||||
|
||||
|
||||
def structure_kind(structure: dict[str, Any]) -> str:
|
||||
"""콘크리트 구조물 종류 — **원단위에 철근이 있나 없나로 판정한다.**
|
||||
|
||||
@@ -169,6 +241,44 @@ def placing_code(mapping: WorkItemMapping, method: str | None) -> tuple[str | No
|
||||
return codes.get(default), True
|
||||
|
||||
|
||||
def load_rebar_table(path: Path | None = None) -> dict[str, Any]:
|
||||
"""철근 갈래표를 읽는다. 파일이 없으면 **빈 표** — 전부 「갈래 미확보」로 드러난다."""
|
||||
target = path
|
||||
if target is None:
|
||||
files = sorted(REBAR_DIR.glob(REBAR_PREFIX + "*.json")) if REBAR_DIR.is_dir() else []
|
||||
target = files[-1] if files else None
|
||||
if target is None or not target.is_file():
|
||||
return {}
|
||||
return json.loads(target.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def rebar_complexity(
|
||||
type_id: str, options: dict[str, Any], table: dict[str, Any] | None = None
|
||||
) -> tuple[str | None, str]:
|
||||
"""(철근 갈래, 근거). **원문 예시에 걸리는 것만** 정하고 안 걸리면 `(None, 사유)`.
|
||||
|
||||
품셈 12-3 [주]① — 「간단: 측구·간단한 기초·**중력식 옹벽** / 보통: 수문·**반중력식 옹벽**·
|
||||
교대 / 복잡: 교량 슬래브·암거·우물통·**부벽식 옹벽** / 매우복잡: 구주식 교대·교각…」.
|
||||
사람에게 묻지 않는다 — **판정할 수 있는 것을 물으면 그것이 곧 미결이 된다.**
|
||||
"""
|
||||
found = table if table is not None else load_rebar_table()
|
||||
form = options.get("form")
|
||||
fallback: dict[str, Any] | None = None
|
||||
for row in found.get("form_map") or []:
|
||||
if row.get("type_id") != type_id:
|
||||
continue
|
||||
if "form" not in row:
|
||||
fallback = row
|
||||
continue
|
||||
if row.get("form") == form:
|
||||
if row.get("class"):
|
||||
return str(row["class"]), f"품셈 12-3 [주]① 「{row.get('matched')}」"
|
||||
return None, str(row.get("why") or "원문 예시에 없음")
|
||||
if fallback and fallback.get("class"):
|
||||
return str(fallback["class"]), f"품셈 12-3 [주]① 「{fallback.get('matched')}」"
|
||||
return None, f"품셈 12-3 [주]① 예시에 없는 구조({type_id} {form}) — 임의로 고르지 않음"
|
||||
|
||||
|
||||
def _spec_detail(structure: dict[str, Any]) -> str:
|
||||
"""규격 표기 — 저장된 제원에서 만든다. 없는 값은 적지 않는다."""
|
||||
parts: list[str] = []
|
||||
@@ -307,6 +417,10 @@ def _structure_rows(
|
||||
code = entry.get("work_item_code")
|
||||
composite = mapping.composite_for(type_id) if code is None else None
|
||||
kind = structure_kind(structure) if composite else None
|
||||
parts: list[dict[str, Any]] | None = None
|
||||
parts_missing: list[str] = []
|
||||
if composite:
|
||||
parts, parts_missing = composite_quantities(structure, composite, mapping)
|
||||
if code is None and composite is None:
|
||||
unmatched.append(f"구조물({type_id})")
|
||||
length = float(structure.get("length_m") or 0.0)
|
||||
@@ -328,11 +442,11 @@ def _structure_rows(
|
||||
"station_to": structure.get("end_m"),
|
||||
"spec_detail": _spec_detail(structure),
|
||||
# 품셈에 그 이름의 공종이 없어 여러 공종을 묶는 자리 — 빈 코드와 구별한다.
|
||||
"composite_parts": (composite or {}).get("parts"),
|
||||
"composite_parts": parts,
|
||||
# 철근이 있나 없나로 자동 판정 — 사람이 고르는 값이 아니다.
|
||||
"structure_kind": kind,
|
||||
# ⚠ 아직 일위대가가 안 선 공종 — 지금 세우면 절반짜리가 된다.
|
||||
"composite_not_ready": (composite or {}).get("not_ready"),
|
||||
# ⚠ 물량을 못 채운 조각 — 0 으로 적지 않고 사유와 함께 드러낸다.
|
||||
"composite_not_ready": parts_missing or None,
|
||||
"in_bill": True,
|
||||
"in_bill_reason": (composite or {}).get("why", ""),
|
||||
"origin": ORIGIN_STRUCTURE,
|
||||
|
||||
@@ -127,6 +127,9 @@ class StructureQuantity:
|
||||
# 측점 — 내역 줄에 「어디부터 어디까지」를 적으려면 여기서 따라가야 한다(B09 인계).
|
||||
start_m: float | None = None
|
||||
end_m: float | None = None
|
||||
# 저장된 제원 — **형식(반중력식…)처럼 뒤 단계가 봐야 하는 값**이 여기 있다.
|
||||
# 치수를 다시 쓰라는 뜻이 아니라 **읽으라고** 실어 나른다(치수 정본은 여전히 하나).
|
||||
options: dict[str, Any] = field(default_factory=dict)
|
||||
components: list[Component] = field(default_factory=list)
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
@@ -353,6 +356,7 @@ def expand(
|
||||
height_m=height,
|
||||
start_m=start if structure.get("start_m") is not None else None,
|
||||
end_m=end if structure.get("end_m") is not None else None,
|
||||
options=dict(options),
|
||||
)
|
||||
expander = EXPANDERS.get(type_id)
|
||||
if expander is None:
|
||||
@@ -420,6 +424,8 @@ def build_table(
|
||||
"height_m": item.height_m,
|
||||
"start_m": item.start_m,
|
||||
"end_m": item.end_m,
|
||||
# 저장된 제원 — 형식(반중력식…)처럼 **뒤 단계가 읽어야 하는** 값이 여기 있다.
|
||||
"options": item.options,
|
||||
"notes": item.notes,
|
||||
"components": [
|
||||
{
|
||||
|
||||
@@ -100,10 +100,22 @@ async def get_material_summary(project_id: UUID) -> JSONResponse:
|
||||
unit_table,
|
||||
supply_map=settings.get("material_supply") or {},
|
||||
)
|
||||
# 묶음으로 서는 구조물의 조각을 화면에도 보인다 — 코드만으로는 사람이 검증 못 한다.
|
||||
handoff = build_handoff(unit_quantity_table=unit_table)
|
||||
composite = [
|
||||
{
|
||||
"name": row["name"],
|
||||
"parts": row["composite_parts"],
|
||||
"not_ready": row.get("composite_not_ready"),
|
||||
}
|
||||
for row in handoff["work_items"]
|
||||
if row.get("composite_parts")
|
||||
]
|
||||
return JSONResponse(
|
||||
content={
|
||||
"unit_quantity": unit_table,
|
||||
"material": material_table,
|
||||
"composite": composite,
|
||||
"skipped_structures": skipped,
|
||||
"structure_count": len(structures),
|
||||
}
|
||||
|
||||
@@ -61,6 +61,20 @@ export interface UnitQuantityStructure {
|
||||
}[];
|
||||
}
|
||||
|
||||
/** 묶음으로 서는 구조물의 조각. 품셈에 그 이름의 공종이 없어 여러 공종으로 나뉜다. */
|
||||
export interface CompositePart {
|
||||
code: string | null;
|
||||
name?: string;
|
||||
unit?: string;
|
||||
quantity: number | null;
|
||||
basis_kind?: string | string[] | null;
|
||||
/** 철근 갈래(간단/보통/복잡/매우복잡) — 품셈 원문이 정한다. */
|
||||
kind?: string | null;
|
||||
kind_basis?: string;
|
||||
not_ready?: boolean;
|
||||
why?: string;
|
||||
}
|
||||
|
||||
export interface MaterialResponse {
|
||||
unit_quantity: {
|
||||
structures: UnitQuantityStructure[];
|
||||
@@ -72,6 +86,12 @@ export interface MaterialResponse {
|
||||
material: MaterialTable;
|
||||
skipped_structures: string[];
|
||||
structure_count: number;
|
||||
/** 인계에서 온 묶음 조각 — 화면이 「무엇으로 나뉘어 서는지」를 보인다. */
|
||||
composite?: {
|
||||
name: string;
|
||||
parts: CompositePart[];
|
||||
not_ready?: string[] | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** 거푸집·동바리 안내에 쓰는 값. */
|
||||
@@ -321,6 +341,25 @@ export function renderUnitQuantityGrid(response: MaterialResponse): HTMLElement
|
||||
wrap.append(line);
|
||||
}
|
||||
|
||||
// ⚠ 품셈에 그 이름의 공종이 없어 **여러 공종으로 나뉘어 서는** 구조물 — 무엇으로
|
||||
// 나뉘는지와 각 조각의 물량·갈래를 보인다. 코드만으로는 사람이 검증할 수 없다.
|
||||
for (const group of response.composite ?? []) {
|
||||
const box = document.createElement("p");
|
||||
box.className = "b08-quantity__notice";
|
||||
const parts = group.parts.map((part) => {
|
||||
const kind = part.kind ? `#${part.kind}` : "";
|
||||
const amount =
|
||||
part.quantity === null || part.quantity === undefined
|
||||
? "-"
|
||||
: `${num(part.quantity, 3)}${part.unit ?? ""}`;
|
||||
return `${part.name ?? part.code}${kind} ${amount}${part.not_ready ? " ⚠" : ""}`;
|
||||
});
|
||||
box.textContent = `${group.name}: 묶음 공종 — ${parts.join(" · ")}`;
|
||||
wrap.append(box);
|
||||
const blocked = warning("⚠ 물량을 못 채운 조각", group.not_ready ?? []);
|
||||
if (blocked) wrap.append(blocked);
|
||||
}
|
||||
|
||||
if (!unit.structures.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "b08-quantity__message";
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "rebar_complexity",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "철근 가공·조립 갈래(간단/보통/복잡/매우복잡) — **품셈 12-3 [주]① 원문**이 구조물을 예시로 갈라 둔다. 사람이 고르는 값이 아니라 법으로 정해지는 값이다(거푸집 사용횟수 1-7-1 과 같은 자리).",
|
||||
"source": {
|
||||
"doc": "산림사업 표준품셈 12-3 철근 현장가공 및 조립 [주] ①",
|
||||
"table_id": "F0335",
|
||||
"quote": "간단한 것이란 측구, 간단한 기초 및 중력식 옹벽 등을 말하며, 보통의 것이란 수문, 반중력식 옹벽 및 교대 등을 말하고, 복잡한 것이란 교량의 슬래브, 암거, 우물통 부벽식 옹벽 등을 말하며, 매우 복잡한 것이란 구주식(기둥형) 교대, 교각, 지하철, 터널"
|
||||
},
|
||||
"policy": {
|
||||
"auto_decided": true,
|
||||
"unlisted_is_flagged": true,
|
||||
"note": "원문 예시에 걸리는 것만 정한다. 안 걸리면 「갈래 미확보」로 드러내고 임의로 고르지 않는다."
|
||||
},
|
||||
"classes": [
|
||||
{ "key": "간단", "examples": ["측구", "간단한 기초", "중력식 옹벽"] },
|
||||
{ "key": "보통", "examples": ["수문", "반중력식 옹벽", "교대"] },
|
||||
{ "key": "복잡", "examples": ["교량 슬래브", "암거", "우물통", "부벽식 옹벽"] },
|
||||
{ "key": "매우복잡", "examples": ["구주식(기둥형) 교대", "교각", "지하철", "터널"] }
|
||||
],
|
||||
"form_map": [
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"form": "중력식",
|
||||
"class": "간단",
|
||||
"matched": "중력식 옹벽"
|
||||
},
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"form": "반중력식",
|
||||
"class": "보통",
|
||||
"matched": "반중력식 옹벽"
|
||||
},
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"form": "부벽식",
|
||||
"class": "복잡",
|
||||
"matched": "부벽식 옹벽"
|
||||
},
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"form": "캔틸레버식",
|
||||
"class": null,
|
||||
"why": "원문 [주]① 예시에 캔틸레버식 옹벽이 없음 — 임의로 고르지 않고 미확보로 드러냄"
|
||||
},
|
||||
{
|
||||
"type_id": "box_culvert",
|
||||
"class": "복잡",
|
||||
"matched": "암거"
|
||||
},
|
||||
{
|
||||
"type_id": "pipe_inlet_basin",
|
||||
"class": "간단",
|
||||
"matched": "간단한 기초",
|
||||
"note": "관보호공 집수정 — 원문의 「간단한 기초」에 해당. ⚠ 벽체까지 간단으로 볼지는 확인 필요"
|
||||
}
|
||||
],
|
||||
"price_hint_krw_per_ton": {
|
||||
"note": "⚠ **표시 전용.** 갈래를 자동으로 정하더라도 화면에 갈래 이름과 차이를 보여야 사람이 검증할 수 있다. B08 의 어떤 계산에도 안 들어간다(금액은 B09 몫).",
|
||||
"computed_by": "B09",
|
||||
"computed_on": "2026-09-07",
|
||||
"values": {
|
||||
"간단": 919146.7,
|
||||
"보통": 1032497.4,
|
||||
"복잡": 1143569.7,
|
||||
"매우복잡": 1278375.3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,12 +142,19 @@
|
||||
"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": "설계자가 넣는 암 갈래 이름(풍화암·연암·보통암·경암)이 리핑이냐 발파냐를 말하지 않음. 갈래마다 시공법을 지정하는 칸이 필요함"
|
||||
}
|
||||
]
|
||||
@@ -158,18 +165,62 @@
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"parts": [
|
||||
"FP-12-01-01#철근구조물",
|
||||
"FP-12-04 합판거푸집",
|
||||
"FP-12-03 철근 현장가공 및 조림",
|
||||
"FP-12-25 기초잡석"
|
||||
{
|
||||
"code": "FP-12-01-01",
|
||||
"name": "콘크리트 타설",
|
||||
"unit": "㎥",
|
||||
"from_components": [
|
||||
"콘크리트",
|
||||
"버림콘크리트"
|
||||
],
|
||||
"kind_suffix": "concrete_placing"
|
||||
},
|
||||
{
|
||||
"code": "FP-12-04",
|
||||
"name": "합판거푸집",
|
||||
"unit": "㎡",
|
||||
"from_components": [
|
||||
"합판거푸집"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "FP-12-38",
|
||||
"name": "유로폼",
|
||||
"unit": "㎡",
|
||||
"from_components": [
|
||||
"유로폼"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "FP-12-03",
|
||||
"name": "철근 현장가공 및 조립",
|
||||
"unit": "ton",
|
||||
"from_components": [
|
||||
"이형철근 D13",
|
||||
"이형철근 D16"
|
||||
],
|
||||
"unit_from": "kg",
|
||||
"kind_suffix": "rebar_complexity"
|
||||
},
|
||||
{
|
||||
"code": "FP-12-25",
|
||||
"name": "기초잡석",
|
||||
"unit": "㎥",
|
||||
"from_components": [],
|
||||
"not_ready": true,
|
||||
"why": "관측 원단위에 기초잡석 물량이 없음 — 울진 라이브러리 반중력식 H=2.0 항목에 그 줄이 없다"
|
||||
}
|
||||
],
|
||||
"why": "품셈 12장에 「옹벽」 공종이 없음. 실무 내역은 「반중력식옹벽 H=2.0」 한 줄이고 그 일위대가가 위 공종을 묶음.",
|
||||
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김",
|
||||
"placing_note": "타설 코드는 프로젝트 설정의 타설 방식으로 갈림(기본 레디믹스트). 철근구조물 판정은 원단위의 D13·D16 에서 자동으로 나옴.",
|
||||
"not_ready": ["FP-12-03", "FP-12-25"],
|
||||
"not_ready_why": "B09 일위대가가 아직 안 섬 — 지금 세우면 절반짜리가 됨(2026-09-07 조율 창)"
|
||||
"parts_note": "조각마다 **어느 성분에서 나오는지**(`from_components`)를 적는다. 이름을 바꾸면 물량이 조용히 0 이 되므로 원단위 성분 이름과 정확히 같아야 하고, 못 찾으면 드러낸다. `unit_from` 이 있으면 그 단위에서 목표 단위로 환산한다 — **철근은 ㎏ → ton (÷1000)**. ⚠ 단위를 안 맞추면 1000배 틀린다."
|
||||
}
|
||||
]
|
||||
],
|
||||
"unit_conversion": {
|
||||
"note": "조각 단위 환산. 단가의 단위(원/ton)와 원단위의 단위(㎏)가 달라 반드시 맞춰야 한다.",
|
||||
"kg_to_ton": 0.001
|
||||
}
|
||||
},
|
||||
"concrete_placing": {
|
||||
"note": "콘크리트 타설은 **타설 방식 × 구조물 종류**로 갈린다. 방식은 설계 판단이라 프로젝트 설정(`quantity.concrete_placing_method`)이 고르고, 종류는 **원단위에 철근이 있나 없나로 자동 판정**한다 — 사람이 고르는 값이 아니다(2026-09-07 3자 확정).",
|
||||
@@ -180,7 +231,11 @@
|
||||
},
|
||||
"default_method": "ready_mixed",
|
||||
"default_is_provisional": true,
|
||||
"structure_kinds": ["무근구조물", "철근구조물", "소형구조물"],
|
||||
"structure_kinds": [
|
||||
"무근구조물",
|
||||
"철근구조물",
|
||||
"소형구조물"
|
||||
],
|
||||
"kind_rule": "원단위 성분에 철근(이형철근·원형철근)이 있으면 철근구조물, 없으면 무근구조물. 소형구조물 판정 기준은 미확보.",
|
||||
"price_hint_krw_per_m3": {
|
||||
"note": "⚠ **표시 전용.** 사용자가 타설 방식을 고를 때 「정하면 얼마나 달라지는지」를 보이려고 둔 값이며 B08 의 어떤 계산에도 들어가지 않는다(금액은 B09 몫 — 8-2 경계). 값은 B09 가 2026-09-07 에 낸 철근구조물 기준 단가이고, 요율·노임이 바뀌면 어긋난다 — 화면이 「참고」임을 함께 적는다.",
|
||||
|
||||
Reference in New Issue
Block a user