"""B09 원가계산 — **사람이 모양을 읽어 둔 표** (자원 축 보조 · 2026-09-14 ⑤ 브레인 판정 Ⓐ~Ⓔ). 9-19-1 토사면 고르기(고시 2025-82 원문 L5407~5436)는 한 절에 표가 둘이고 둘 다 일반 길에 안 맞음. F0288 1. 절토면 고르기 자원 이름이 **첫 자료 줄**(「보통인부 (인)」 …) · 규격은 [주]① 에만 · 「·」 = 그 토질엔 그 자원 없음 F0289 2. 성토면 고르기 병합 첫 칸(시공) 탓에 둘째 줄이 한 칸 앞당겨 옴 F0336 12-4 합판거푸집 기준수량(1회) × 사용횟수별 비율 — 재료 줄은 재료 %, 인력 줄은 노무비 % (원문 L6187 · 2026-09-14 브레인 ㉮ — 비율 셈이 없어 막아 뒀던 표) ⚠ 일반 보정으로 넓히지 않음 — 밀린 줄 고쳐 읽기는 목재틀흙막이 503만원 전례(`_Transposed` 머리말). **적어 둔 표만** 읽고, 칸이 판정과 다르면 한 줄도 안 세우고 막음. ⚠ 규격은 범위 별칭(`data_aliases` scope FP-09-19-01 · [주]①)이 코드로 이음 — 칸이 규격을 적었으면 별칭이 안 덮음(`scoped_alias_entry`). 성토면 굴착기 0.6㎥ 는 무한궤도·타이어 둘이라 규격 미정(Ⓑ). """ from __future__ import annotations import re from decimal import Decimal from typing import Any from B09_Estimation.B09_Estimation_ResourceAxis import ( AxisResult, ResourceCatalog, ResourceRow, UnmatchedRow, _resolve_cell, parse_amount, ) from B09_Estimation.B09_Estimation_ResourceAxis_Join import scoped_alias_entry, unmatched_reason #: 표 번호 → (공종 · 모양 · 갈래 앞말 · 판정 근거). 여기 없는 표는 이 길로 안 읽음. JUDGED_TABLES: dict[str, dict[str, Any]] = { "F0288": { "code": "FP-09-19-01", "shape": "header_row", "prefix": "절토면", "why": "원문 L5414 「1. 절토면 고르기 (단위: 10㎡당)」 — 자원 머리가 첫 자료 줄", }, "F0289": { "code": "FP-09-19-01", "shape": "merged_first", "prefix": "성토면", # 원문 L5430 표 — 「인력시공」 칸이 두 줄 병합이라 둘째 줄(모래 또는 사질토)이 앞당겨 옴. "merged_rows": (1,), # 기계시공 「굴착기 | 0.6㎥」 — 원문이 형식을 안 적음. 카탈로그 0.6 은 무한궤도·타이어 # 둘이라 형식마다 갈래 · 고르기는 B08 칸(제안 무한궤도 — 영월 「06M3 B/H」 · 브레인 ②). "form_split": {"굴착기": ("무한궤도", "타이어")}, "why": "원문 L5430 「2. 성토면 고르기 (단위: 10㎡당)」 — 시공 칸 병합으로 둘째 줄 앞당김", }, "F0336": { "code": "FP-12-04", "shape": "use_count", "prefix": "합판거푸집", "why": "원문 L6187 「기준수량(1회사용) · 사용횟수별기준수량에대한 비율(%) 재료별·노무비」", }, "F0353": { "code": "FP-12-15", "shape": "remark_labor", "prefix": "집수정", # 구체콘크리트는 바로 아래 다짐기 줄과 한 갈래 — 다짐기가 안 풀리면 갈래를 안 세움(⑴). "needs_machine": {"구체콘크리트": "다짐:봉상후렉시블(45mm)"}, "why": "원문 L6460 「구체·버림콘크리트 ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", }, # 12-15 와 같은 모양 셋 — 같은 봉상후렉시블 줄 하나가 셋을 막고 있었음(2026-09-14 브레인 · 672 다음). "F0350": { "code": "FP-12-12", "shape": "remark_labor", "prefix": "날개벽", "needs_machine": {"콘크리트": "다짐:봉상후렉시블(45mm)"}, "why": "원문 L6419 12-12 「콘크리트(레미콘) ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", }, "F0351": { "code": "FP-12-13", "shape": "remark_labor", "prefix": "면벽", "needs_machine": {"콘크리트": "다짐:봉상후렉시블(45mm)"}, "why": "원문 L6436 12-13 「콘크리트(레미콘) ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", }, "F0354": { "code": "FP-12-16", "shape": "remark_labor", "prefix": "맨홀", # 칸이 하나 밀려 비고가 끝 칸이 아님(「구체콘크리트 | 철근 | ㎥ | | 비고 | 」). "needs_machine": {"구체콘크리트": "봉상후렉시블(45mm)"}, "why": "원문 L6474 12-16 「구체·버림콘크리트 ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", }, # 12-34-1 — 머리 「(단위: 개소당)」 이나 인력 0.17·0.29 가 12-16 맨홀 ㎥당과 같고 기계가 Q ㎥/hr # ⇒ ㎥당으로 읽음 · 「봉상후렉시블 대 2」 는 엔진식 진동기(엔진+플렉시블 한 대)의 봉(2026-09-14 브레인). "F0385": { "code": "FP-12-34-01", "shape": "per_m3_rows", "prefix": "콘크리트 타설", # 엔진식 진동기(건설품셈 8-3 (4611) 엔진+플렉시블 한 대)의 봉 — 「진동기(3.5HP) 대 2」 로 셈. "same_machine": ("봉상후렉시블(45mm)",), "why": "원문 L6839 12-34-1 「인력 콘크리트공·보통인부 인 · 기계 대 (Q=5.4㎥/hr)」", }, # 시간 줄 표 — 장비·인력이 다 「h」(2026-09-15 브레인 ㉡-3). 굴착기 형식을 원문이 안 적어 두 갈래 · 제안 없음(①). "F0450": { "code": "FP-14-01", "shape": "hour_rows", "prefix": "입목뿌리 밑막이", "forms": (("무한궤도", "0201-0080"), ("타이어", "0211-0080")), # 원문 「우드그랩 부착」 ↔ 카탈로그 「부착용 집게 0.6∼0.8」 — 이름이 다름(②). "attachment": "7206-0070", "why": "원문 L7798 14-1 「0.8㎥ 굴착기(우드그랩 부착) h · 보통인부 h」(10본당)", }, "F0451": { "code": "FP-14-02", "shape": "hour_rows", "prefix": "근주이식", "forms": (("무한궤도", "0201-0080"), ("타이어", "0211-0080")), "why": "원문 L7809 14-2 「0.8㎥ 굴착기 h · 보통인부 h」(10본당)", }, # 도구 줄 표 — 「공법 | (갈래) | 사용도구 | 단위 | 소요인력 | 인력구분」 · 이름이 끝 칸(2026-09-15 브레인 ②). # `variants` 면 단위 칸 앞 마지막 칸이 갈래 · 「인/100본」 줄(묘목찾기)은 본수 칸이 들 때 붙음(`_Brushcutting`). "F0155": { "code": "FP-06-02-02", "shape": "tool_rows", "prefix": "줄베기", "variants": True, "why": "원문 6-2-2 「묘목찾기 낫 인/100본 · 줄베기 본수 구간 예취기 인/ha · 인력구분」", }, "F0156": { "code": "FP-06-02-03", "shape": "tool_rows", "prefix": "모두베기", "variants": True, "why": "원문 6-2-3 「묘목찾기 낫 인/100본 · 모두베기 조림 경과 예취기 인/ha · 인력구분」", }, "F0158": { "code": "FP-06-04-01", "shape": "tool_rows", "prefix": "덩굴걷기", "why": "원문 6-4-1 「기계작업 예취기 인/ha 3.30 특별인부」", }, "F0159": { "code": "FP-06-04-02", "shape": "tool_rows", "prefix": "덩굴 약제 살포처리", "why": "원문 6-4-2 「약제살포 배부식분무기 인/ha · 작업보조 인/ha · 인력구분」", }, } #: 도구 줄 표의 단위 칸 — 「인/ha」 는 공종 단위 ha 로 곧장 · 「인/100본」 은 본수 칸이 들어야 섬. _RE_TOOL_UNIT = re.compile(r"^인/(ha|100본)$") #: 총칙 L530 「본 품셈에서 제시된 품은 일일 작업시간 8시간을 기준」 — 인력 시간 ÷ 8 = 인. HOURS_PER_DAY = Decimal(8) _RE_SPEC_FIRST_MACHINE = re.compile(r"^\d+(?:\.\d+)?㎥굴착기") #: 줄 첫 칸이 분류 딱지인 표(12-34-1 「자재 | 콘크리트(레미콘)」) — 이름은 다음 칸. _ROW_CATEGORIES = ("자재", "인력", "기계") #: 자원이 아닌 머리 줄(12-04 「횟수별 | 재료별(%) | 노무비(%)」). _HEADER_ROWS = ("횟수별", "구분") UNREAD_REASON = "판정표가 안 읽은 줄 — 이 일위대가에 안 넣음(자동 · 2026-09-14)" def _loose(text: str) -> str: """겹침 비교용 — 빈칸·괄호·가운뎃점·쉼표를 뺌(「적사(굴착기 0.7㎥)」 ↔ 「적사 굴착기 0.7㎥」).""" return re.sub(r"[\s()·,:]", "", str(text)) def _unread_rows(code, table_id, judged, rows, staged) -> list: """읽힌 줄 밖의 줄을 「못 붙은 줄」 로 — 표를 넣을 때마다 손으로 사유를 안 달아도 안 샘. ㉠ 빼는 것: 읽힌 줄(`raw_row_index`) · 이미 못 맞춤으로 선 이름 · 다른 갈래가 쓴 기계 줄 (`needs_machine`·`same_machine`) · 자원 머리(`header_row` 첫 줄 · 「횟수별」) · 빈 줄 ㉡ 손 사유(`known_gap_note`)가 이미 적은 이름은 안 올림 — 같은 말이 두 번 안 뜨게 """ from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note read = {item.raw_row_index for item in staged if isinstance(item, ResourceRow)} taken = {_loose(item.cell) for item in staged if isinstance(item, UnmatchedRow)} taken |= {_loose(name) for name in judged.get("needs_machine", {}).values()} taken |= {_loose(name) for name in judged.get("same_machine", ())} hand = _loose(known_gap_note(code)) unread: list = [] for index, cells in enumerate(rows): cells = [c for c in cells] if index in read or not any(cells) or (judged["shape"] == "header_row" and index == 0): continue name = cells[1] if cells[0] in _ROW_CATEGORIES and len(cells) > 1 else cells[0] key = _loose(name) if not key or key in _HEADER_ROWS or key in taken or key in hand: continue if key == "비고": name = f"비고 — {' '.join(' '.join(cells[1:]).split())[:40]}…" unread.append(UnmatchedRow(code, table_id, " ".join(name.split()), UNREAD_REASON)) taken.add(key) return unread #: 비고 칸 인력 — 「콘크리트공0.24인/㎥, 보통인부 0.42인/㎥」. _RE_REMARK_LABOR = re.compile(r"([가-힣]+)\s*(\d+(?:\.\d+)?)\s*인/㎥") _RE_Q = re.compile(r"Q\s*=\s*(\d+(?:\.\d+)?)") #: 비율 줄 — 「1회사용시 2회사용시 …」 칸. _RE_USE_COUNT = re.compile(r"(\d+)회사용시") #: 값으로 안 읽는 줄 — 사용고재 평가기준(원문이 셈을 안 줌 · 사유는 `KNOWN_GAPS`) · 비고. _NOT_RESOURCE_ROWS = ("사용고재평가기준", "비고", "횟수별") #: 「보통인부 (인)」 의 단위 꼬리 — 규격이 아님. _UNIT_TAIL = re.compile(r"\s*[((](?:인|시간)[))]\s*$") _ABSENT = frozenset({"·", "ㆍ", "-", "-", "–"}) def _entry(catalog: ResourceCatalog, name_cell: str, code: str, side: tuple[str, ...] = ()): return scoped_alias_entry(catalog, name_cell, code, side) or _resolve_cell( catalog, name_cell, [name_cell, *side] ) def _row( code: str, table: dict[str, Any], entry, amount: Decimal, unit: str, index: int, variant: str ): return ResourceRow( work_item_code=code, pum_table_id=str(table.get("pum_table_id", "")), pum_form=str(table.get("pum_form", "")), resource_kind=entry.kind, resource_code=entry.code, resource_name=entry.name, resource_spec=entry.spec, amount=amount, amount_unit=unit, raw_row_index=index, variant=variant, ) def match_judged_table( node: dict[str, Any], table: dict[str, Any], catalog: ResourceCatalog, result: AxisResult, basis_quantity: Decimal | None, unit: str, ) -> bool: """적어 둔 표를 판정대로 읽음. 그 표가 아니면 `False`.""" code = str(node.get("work_item_code") or "") table_id = str(table.get("pum_table_id", "")) judged = JUDGED_TABLES.get(table_id) if judged is None or judged["code"] != code: return False rows = [[str(c).strip() for c in row] for row in (table.get("raw_row") or [])] def block(why: str) -> bool: result.unmatched.append(UnmatchedRow(code, table_id, judged["prefix"], why)) result.partial_items[code] = why return True # 비고·Q 가 ㎥당을 적는 모양은 표 머리 밑수를 안 씀(12-12 날개벽은 「개소당」 머리조차 없음). if basis_quantity in (None, 0) and judged["shape"] not in ( "remark_labor", "per_m3_rows", "tool_rows", # 줄마다 단위 칸(인/ha)이 밑수 ): return block("판정 표에 밑수가 없습니다") if judged["shape"] == "header_row": staged = _header_row(code, table, judged, rows, catalog, basis_quantity, unit) elif judged["shape"] == "use_count": staged = _use_count(code, table, rows, catalog, basis_quantity, unit) elif judged["shape"] == "remark_labor": staged = _remark_labor(code, table, judged, rows, catalog) elif judged["shape"] == "hour_rows": staged = _hour_rows(code, table, judged, rows, catalog, basis_quantity) elif judged["shape"] == "per_m3_rows": staged = _per_m3_rows(code, table, judged, rows, catalog) elif judged["shape"] == "tool_rows": staged = _tool_rows(code, table, judged, rows, catalog) else: staged = _merged_first(code, table, judged, rows, catalog, basis_quantity, unit) if isinstance(staged, str): return block(f"{staged} — 판정({judged['why']})과 칸이 달라 안 읽음") staged = [*staged, *_unread_rows(code, table_id, judged, rows, staged)] for item in staged: if isinstance(item, UnmatchedRow): result.unmatched.append(item) else: result.rows.append(item) return True def _header_row(code, table, judged, rows, catalog, basis, unit) -> list | str: """첫 줄 = 자원 머리 · 이후 줄 = [토질, 값 …]. 머리 자원을 하나라도 못 풀면 표째 막음.""" names = [_UNIT_TAIL.sub("", cell) for cell in (rows[0] if rows else []) if cell] entries = [_entry(catalog, name, code) for name in names] if len(names) < 2 or not all(entries): missing = [name for name, entry in zip(names, entries) if entry is None] return f"자원 머리 {missing or names} 을 못 풂" staged: list = [] for index, cells in enumerate(rows[1:], start=1): values, tail = cells[1 : 1 + len(names)], cells[1 + len(names) :] readable = all(v in _ABSENT or parse_amount(v) is not None for v in values) if not cells[0] or len(values) != len(names) or any(tail) or not readable: return f"{index}째 줄 「{cells[0] if cells else ''}」 값 칸" variant = f"{judged['prefix']} · {cells[0]}" for value, entry in zip(values, entries): if value in _ABSENT: continue # 그 토질엔 이 자원이 안 듦 staged.append( _row(code, table, entry, parse_amount(value) / basis, unit, index, variant) ) return staged def _merged_first(code, table, judged, rows, catalog, basis, unit) -> list | str: """[시공, 토질, 구분, 규격, 단위, 수량] — 병합 줄은 앞 줄 시공을 이어받아 한 칸 되돌림.""" width = len(table.get("condition_note") or []) if width != 6: return f"머리 {width} 칸" lines: list[list[str]] = [] for index, cells in enumerate(rows): if index in judged["merged_rows"]: if not lines or cells[-1]: return f"{index}째 줄 병합 칸" cells = [lines[-1][0], *cells[:-1]] lines.append(cells) methods = [cells[0] for cells in lines] staged: list = [] for index, cells in enumerate(lines): if len(cells) != width: return f"{index}째 줄 칸 {len(cells)}" method, soil, name, size, _unit_word, value = cells amount = parse_amount(value) if not method or not name or amount is None: return f"{index}째 줄 「{method}」 값 칸" short = method.removesuffix("시공") variant = " · ".join( [judged["prefix"], short, *([soil] if methods.count(method) > 1 else [])] ) side = (size,) if size else () forms = judged.get("form_split", {}).get(name) for form in forms or (None,): entry = _entry(catalog, f"{name}({form})" if form else name, code, side) if entry is None: # 그 갈래만 안 섬(자원이 하나뿐) — 사유는 못 맞춘 줄로. label = f"{name} {size}".strip() reason = unmatched_reason(catalog, name) staged.append(UnmatchedRow(code, str(table.get("pum_table_id", "")), label, reason)) continue named = f"{variant} · {form}" if form else variant staged.append(_row(code, table, entry, amount / basis, unit, index, named)) return staged def _use_count(code, table, rows, catalog, basis, unit) -> list | str: """[이름, 단위, 기준수량, …] + 비율 줄 하나 → 사용횟수마다 갈래(재료 % · 노무비 %).""" ratio_row = next((cells for cells in rows if _RE_USE_COUNT.search(" ".join(cells))), None) if ratio_row is None: return "사용횟수 비율 줄" counts = _RE_USE_COUNT.findall(" ".join(ratio_row)) number = re.compile(r"\d+(?:\.\d+)?") material = [Decimal(x) for x in number.findall(ratio_row[4] if len(ratio_row) > 4 else "")] labor = [Decimal(x) for x in number.findall(ratio_row[5] if len(ratio_row) > 5 else "")] if not counts or not (len(counts) == len(material) == len(labor)): return f"비율 칸 수(횟수 {len(counts)} · 재료 {len(material)} · 노무비 {len(labor)})" staged: list = [] for index, cells in enumerate(rows): name = "".join(cells[0].split()) if cells else "" base = parse_amount(cells[2]) if len(cells) > 2 else None if not name or name in _NOT_RESOURCE_ROWS or base is None: continue entry = _entry(catalog, cells[0], code) if entry is None: reason = unmatched_reason(catalog, cells[0]) staged.append(UnmatchedRow(code, str(table.get("pum_table_id", "")), cells[0], reason)) continue ratios = labor if entry.kind == "labor" else material for count, ratio in zip(counts, ratios): amount = base * ratio / Decimal(100) / basis staged.append(_row(code, table, entry, amount, unit, index, f"{count}회")) return staged def _remark_labor(code, table, judged, rows, catalog) -> list | str: """㎥ 줄 비고 칸의 인력(인/㎥)으로 갈래 — 다짐기가 딸린 갈래는 그 기계가 풀려야 세움.""" table_id = str(table.get("pum_table_id", "")) by_name = {"".join(cells[0].split()): cells for cells in rows if cells} staged: list = [] for index, cells in enumerate(rows): labors = _RE_REMARK_LABOR.findall(" ".join(cells[3:])) # 비고가 끝 칸이 아닌 표(12-16) if len(cells) < 3 or cells[2] != "㎥" or not labors: continue variant = cells[0].split("(")[0].strip() pieces = [] for name, amount in labors: entry = _entry(catalog, name, code) if entry is None: return f"{variant} 인력 「{name}」" pieces.append((entry, Decimal(amount))) machine_name = judged.get("needs_machine", {}).get(variant) if machine_name: machine_cells = by_name.get("".join(machine_name.split())) or [] found_q = _RE_Q.search(" ".join(machine_cells)) entry = _entry(catalog, machine_name, code) if machine_cells else None if entry is None or found_q is None: reason = unmatched_reason(catalog, machine_name) staged.append(UnmatchedRow(code, table_id, machine_name, reason)) why = ( f"다짐기 「{machine_name}」 가 안 풀려 갈래를 안 세움 — 인력만이면 조립 줄이" " 조용히 싸짐(2026-09-14 ㉯ ⑴)" ) staged.append(UnmatchedRow(code, table_id, variant, why)) continue pieces.append((entry, Decimal(1) / Decimal(found_q.group(1)))) for entry, amount in pieces: staged.append(_row(code, table, entry, amount, "㎥", index, variant)) return staged def _per_m3_rows(code, table, judged, rows, catalog) -> list | str: """「인」 칸 앞 이름 · 뒤 수(인/㎥) · 「대」 칸 앞 이름 · 뒤 대수 ÷ Q — 칸이 밀린 줄도 단위 칸으로 찾음.""" staged: list = [] for index, cells in enumerate(rows): unit = next((i for i, c in enumerate(cells) if c in ("인", "대") and i > 0), None) if unit is None: continue name = cells[unit - 1] if "".join(name.split()) in judged.get("same_machine", {}): continue # 같은 기계 두 번 안 셈 — 까닭은 공종 사유 한 줄(`KnownGaps`)이 화면에 보임 amount = next((parse_amount(c) for c in cells[unit + 1 :] if parse_amount(c)), None) entry = _entry(catalog, name, code) if amount is None or entry is None: return f"{name} 줄" if cells[unit] == "대": found_q = _RE_Q.search(" ".join(cells)) if found_q is None: return f"{name} Q" amount = amount / Decimal(found_q.group(1)) staged.append(_row(code, table, entry, amount, "㎥", index, "")) return staged def _hour_rows(code, table, judged, rows, catalog, basis_quantity) -> list | str: """장비·인력이 다 시간인 표 — 장비 h 그대로 · 인력 h ÷ 8 · ÷ 밑수 · 굴착기 형식마다 갈래.""" entries = {entry.code: entry for entry in catalog.entries} unit = str(table.get("basis_unit") or "") machine: tuple[int, Decimal] | None = None labor: list = [] for index, cells in enumerate(rows): if len(cells) < 3 or "".join(cells[1].split()) != "h": continue hours = parse_amount(cells[2]) if hours is None: return f"{cells[0]} 시간" if _RE_SPEC_FIRST_MACHINE.match("".join(cells[0].split())): machine = (index, hours) continue entry = _entry(catalog, cells[0], code) if entry is None: return f"{cells[0]} 줄" labor.append((index, entry, hours / HOURS_PER_DAY)) if machine is None: return "장비 줄" staged: list = [] for form, machine_code in judged["forms"]: codes = [machine_code, *([judged["attachment"]] if judged.get("attachment") else [])] if any(c not in entries for c in codes): return f"{form} 기종 코드" for machine_code_ in codes: staged.append( _row( code, table, entries[machine_code_], machine[1] / basis_quantity, unit, machine[0], form, ) ) for index, entry, amount in labor: staged.append(_row(code, table, entry, amount / basis_quantity, unit, index, form)) return staged #: 「인/100본」 줄 사유 — 본수 칸이 들면 `_Brushcutting` 이 걷고 붙임. PER_HUNDRED_REASON = "조림목 본수(본/ha) 칸이 들어야 붙음 — 단위가 인/100본" def _tool_rows(code, table, judged, rows, catalog) -> list | str: """단위 칸(인/ha · 인/100본)을 찾아 뒤 칸 = 값 · 그 뒤 = 인력 · `variants` 면 단위 앞 앞 칸이 갈래. ⚠ 「인/100본」 줄은 여기서 안 셈 — 본수가 설계 입력이라 자원 축(프로젝트 밖)에선 모름 → 못 붙은 줄로. """ staged: list = [] per_ha = 0 for index, cells in enumerate(rows): unit = next( (i for i, c in enumerate(cells) if _RE_TOOL_UNIT.match("".join(c.split()))), None ) if unit is None or unit < 1 or len(cells) < unit + 3: continue amount = parse_amount(cells[unit + 1]) entry = _entry(catalog, cells[unit + 2], code) if amount is None or entry is None: return f"{cells[0]} 줄" if "".join(cells[unit].split()) == "인/100본": staged.append( UnmatchedRow(code, str(table.get("pum_table_id", "")), cells[0], PER_HUNDRED_REASON) ) continue variant = "" if judged.get("variants"): if unit < 2: return f"{index}째 줄 갈래 칸" variant = cells[unit - 2] staged.append(_row(code, table, entry, amount, "ha", index, variant)) per_ha += 1 return staged if per_ha else "인/ha 줄"