feat(z01): 마스터 화면에 공종 축 산림·건설 상자 — base-prices 목록·한 표가 공종 축 둘을 포함 · 거르기 인자 · WorkItems 읽기 모듈 신설
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
@@ -20,9 +21,10 @@ from Z01_MasterData import Z01_MasterData_BasePrices_Machine as machine
|
||||
from Z01_MasterData import Z01_MasterData_BasePrices_Tables as base_tables
|
||||
from Z01_MasterData import Z01_MasterData_Overrides as overrides
|
||||
from Z01_MasterData import Z01_MasterData_Tables as tables
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
|
||||
#: 기초단가 다섯 + 기초데이터 아홉(품셈 근거 8 · 돌쌓기 갈래 1) — 이름은 자료 이름 그대로
|
||||
KINDS = ("labor", "machine", "material", "oil", "rate", *base_tables.SPEC)
|
||||
#: 기초단가 다섯 + 기초데이터 열 + 공종 축 둘(산림·건설) — 이름은 자료 이름 그대로
|
||||
KINDS = ("labor", "machine", "material", "oil", "rate", *base_tables.SPEC, *work_items.KINDS)
|
||||
SOURCES_DIR = (
|
||||
tables.RESOURCES / "data_master_sources"
|
||||
) # 출처표(데스크탑 서브) — 어디서 받고 최신이 무엇인지
|
||||
@@ -37,8 +39,11 @@ _RATE_EDITABLE = (
|
||||
"manager_thresholds.default_estimated_amount_krw",
|
||||
"manager_thresholds.civil_main_work_estimated_amount_krw",
|
||||
)
|
||||
#: ⚠ **지금 상태를 먼저** — 덮개를 읽는 곳이 Z01 밖에 0(2026-09-17 서브 훑기 · 브레인 「없으면 없다고」).
|
||||
#: 뒤 문장은 프로젝트 복사(요구 14)가 붙으면 참이 되는 약속이라 지우지 않고 「그 길이 붙으면」으로 둠.
|
||||
_SCOPE_NOTICE = (
|
||||
"여기서 고친 값은 **새로 만드는 프로젝트부터** 쓰임 — 이미 만든 프로젝트 금액은 안 바뀜"
|
||||
"⚠ **아직 어디에도 안 쓰임** — 고친 값은 마스터에만 쌓임 · 프로젝트로 옮기는 길(프로젝트 복사)이 아직 없음."
|
||||
" 그 길이 붙으면: 여기서 고친 값은 **새로 만드는 프로젝트부터** 쓰임 — 이미 만든 프로젝트 금액은 안 바뀜"
|
||||
"(프로젝트는 만들 때 뜬 사본을 봄)"
|
||||
)
|
||||
_RATE_NOTICE = "법이 정한 값 — 고치면 원본 갱신 때 구간 이름이 바뀌어 고친 값이 주인을 잃기 쉬움(주인 없음으로 남음)"
|
||||
@@ -276,6 +281,8 @@ def base_rows(kind: str) -> list[dict[str, Any]]:
|
||||
if kind == "machine":
|
||||
doc, name = _doc("mach_base")
|
||||
return machine.base_rows(name, edition_of(doc))
|
||||
if kind in work_items.KINDS:
|
||||
return work_items.base_rows(kind)
|
||||
spec_of = base_tables.SPEC.get(kind)
|
||||
if spec_of:
|
||||
doc, name = _doc(spec_of["file"])
|
||||
@@ -323,15 +330,21 @@ class SortError(ValueError):
|
||||
"""모르는 열로 세우라 함 — 조용히 원본 차례로 돌아가지 않음(브레인 ④)."""
|
||||
|
||||
|
||||
#: 품셈 번호 꼴(「9-3-2」·「12-17」) — 글자로 세우면 「10-1」이 「2-1」 앞에 섬(서브 훑기 · 브레인).
|
||||
_NUMBER_PATH = re.compile(r"^\d+(?:-\d+)+$")
|
||||
|
||||
|
||||
def _order_key(value: Any) -> tuple:
|
||||
"""섞인 칸도 세움 — 수 먼저, 그 뒤 글자."""
|
||||
"""섞인 칸도 세움 — 수 먼저, 품셈 번호 꼴은 **마디마다 수로**, 그 뒤 글자."""
|
||||
if isinstance(value, bool):
|
||||
return (1, str(value))
|
||||
return (2, str(value))
|
||||
if isinstance(value, (int, float)):
|
||||
return (0, float(value))
|
||||
if isinstance(value, (dict, list)):
|
||||
return (1, json.dumps(value, ensure_ascii=False, default=str))
|
||||
return (1, str(value))
|
||||
return (2, json.dumps(value, ensure_ascii=False, default=str))
|
||||
if isinstance(value, str) and _NUMBER_PATH.match(value):
|
||||
return (1, tuple(int(part) for part in value.split("-")))
|
||||
return (2, str(value))
|
||||
|
||||
|
||||
def _sorted(rows: list[dict[str, Any]], column: str, desc: bool) -> list[dict[str, Any]]:
|
||||
@@ -343,7 +356,10 @@ def _sorted(rows: list[dict[str, Any]], column: str, desc: bool) -> list[dict[st
|
||||
|
||||
|
||||
#: 돌쌓기 표준경사는 B06 횡단설계가 같은 값으로 기울기를 그림 — 아홉 중 **그림에도 닿는** 하나(브레인).
|
||||
_SLOPE_NOTICE = "고치면 횡단 도면 모양도 함께 바뀜 — B06 횡단설계가 같은 값으로 벽 기울기를 그림"
|
||||
_SLOPE_NOTICE = (
|
||||
"⚠ **B06 횡단설계가 아직 여기서 고친 값을 안 읽음**(원본 값으로 벽 기울기를 그림) — 지금은 고쳐도 횡단 도면이 안 바뀜."
|
||||
" 읽게 되면: 고치면 횡단 도면 모양도 함께 바뀜"
|
||||
)
|
||||
#: 같은 「1:0.3」 이 두 자리에 있음(코덱스 검증 2) — 값은 같으나 **축과 근거가 다름**.
|
||||
#: masonry_slope = 품셈 13-4-4 [주]⑪ 표준경사(직고 · 메/찰 · 성토/절토 축 · 원문표)
|
||||
#: masonry_class.face_slope = 교본 7-3 돌흙막이(구조물 **형식**별 기본값 · 판정이 안 되는 자리의 종전값)
|
||||
@@ -360,6 +376,8 @@ _EXTRA_NOTICE = {
|
||||
|
||||
def notice(kind: str) -> list[str]:
|
||||
"""표 수준 알림 — 화면이 따로 가지면 문구가 두 벌이 됨(브레인 ③)."""
|
||||
if kind in work_items.KINDS:
|
||||
return work_items.notice(kind)
|
||||
return [_SCOPE_NOTICE] + ([_EXTRA_NOTICE[kind]] if kind in _EXTRA_NOTICE else [])
|
||||
|
||||
|
||||
@@ -367,6 +385,8 @@ def spec(kind: str, columns: list[str]) -> dict[str, Any]:
|
||||
"""표 수준 — editable · locked · formula."""
|
||||
if kind == "machine":
|
||||
return machine.spec(columns)
|
||||
if kind in work_items.KINDS:
|
||||
return work_items.spec(columns)
|
||||
if kind in base_tables.SPEC:
|
||||
spec_of = base_tables.SPEC[kind]
|
||||
editable = [c for c in spec_of["editable"] if c in columns]
|
||||
@@ -408,6 +428,8 @@ BASE_PRICE_GROUP, PUMSEM_GROUP = "base_price", "pumsem_basis"
|
||||
|
||||
|
||||
def kind_group(kind: str) -> str:
|
||||
if kind in work_items.KINDS:
|
||||
return work_items.GROUP
|
||||
return PUMSEM_GROUP if kind in base_tables.SPEC else BASE_PRICE_GROUP
|
||||
|
||||
|
||||
@@ -418,14 +440,21 @@ def kind_label(kind: str) -> str:
|
||||
return named.get("name_ko") or kind
|
||||
|
||||
|
||||
def group_label(group: str) -> str:
|
||||
"""상자 이름 — 이름표 `base_price_groups[갈래].name_ko` 에서만(없으면 영문 그대로 · 화면은 `group_label` 을 읽음)."""
|
||||
named = (tables.load_labels().get("base_price_groups") or {}).get(group) or {}
|
||||
return named.get("name_ko") or group
|
||||
|
||||
|
||||
def kinds() -> list[dict[str, Any]]:
|
||||
"""화면이 상자를 세울 목록 — **서버가 냄**(화면이 제 코드에 들면 새 kind 가 조용히 안 뜸).
|
||||
|
||||
갈래 · kind · 한글 이름(이름표) · 줄 수.
|
||||
갈래 · 갈래 한글 이름 · kind · 한글 이름(이름표) · 줄 수.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"group": kind_group(kind),
|
||||
"group_label": group_label(kind_group(kind)),
|
||||
"kind": kind,
|
||||
"label": kind_label(kind),
|
||||
"rows": len(rows(kind)),
|
||||
@@ -439,6 +468,8 @@ def row_label(kind: str, row: dict[str, Any] | None) -> str:
|
||||
return ""
|
||||
if kind == "labor":
|
||||
return str(row.get("occupation_name") or "")
|
||||
if kind in work_items.KINDS:
|
||||
return str(row.get("name") or "")
|
||||
if kind == "machine":
|
||||
return f"{row.get('machine_name') or ''} {row.get('specification') or ''}".strip()
|
||||
if kind == "material":
|
||||
@@ -493,6 +524,18 @@ def public(row: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in row.items() if k not in ("@source", "@axis")}
|
||||
|
||||
|
||||
#: 찾기만 같게 보는 글자 — 원문이 물결표·가운뎃점을 여러 벌로 씀(∼ 26 · ~ 8 · ․ · ㆍ) · 자간 공백(「굴 삭 기」).
|
||||
#: ⚠ **찾기에만** 씀 — 열쇠·칸 값은 원문 그대로(손질하면 갈래 열쇠가 갈림 · 브레인 2026-09-17).
|
||||
_SEARCH_SAME = str.maketrans(
|
||||
{"∼": "~", "~": "~", "〜": "~", "․": "·", "ㆍ": "·", "・": "·", "‧": "·"}
|
||||
)
|
||||
|
||||
|
||||
def fold(text: str) -> str:
|
||||
"""찾기 비교용 — 소문자 · 물결표·가운뎃점 한 벌 · 공백 없앰."""
|
||||
return "".join(str(text or "").lower().translate(_SEARCH_SAME).split())
|
||||
|
||||
|
||||
def table(
|
||||
kind: str,
|
||||
page: int = 1,
|
||||
@@ -500,19 +543,22 @@ def table(
|
||||
q: str = "",
|
||||
sort: str = "",
|
||||
desc: bool = False,
|
||||
filters: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
all_rows = rows(kind)
|
||||
columns = _columns(kind, all_rows)
|
||||
needle = q.strip().lower()
|
||||
needle = fold(q)
|
||||
hits = (
|
||||
[
|
||||
r
|
||||
for r in all_rows
|
||||
if needle in json.dumps(public(r), ensure_ascii=False, default=str).lower()
|
||||
if needle in fold(json.dumps(public(r), ensure_ascii=False, default=str))
|
||||
]
|
||||
if needle
|
||||
else all_rows
|
||||
)
|
||||
if kind in work_items.KINDS: # 서버가 준 거르기(부문·줄 구실) — 고른 값만 옴
|
||||
hits = work_items.apply_filters(kind, hits, filters or {})
|
||||
if sort: # ⚠ 거른 뒤에 세움 — 거꾸로 하면 차례가 깨짐(브레인 ③)
|
||||
if sort not in columns:
|
||||
raise SortError(f"없는 열로 세울 수 없음: {sort}")
|
||||
@@ -528,6 +574,7 @@ def table(
|
||||
**spec(kind, columns),
|
||||
"notice": notice(kind),
|
||||
"source": sources(kind),
|
||||
**(work_items.extra(kind) if kind in work_items.KINDS else {}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user