Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_7
This commit is contained in:
@@ -17,6 +17,8 @@ from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M01_MasterData import M01_MasterData_Store_Flow as flow
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
|
||||
import master_basis as mbs # noqa: E402 (`store` 가 scripts 를 길에 둠)
|
||||
|
||||
router = APIRouter(prefix="/api/m01", tags=["M01 MasterData"])
|
||||
|
||||
if "pytest" not in sys.modules: # 시험은 사본 폴더를 써서 정본을 미리 읽을 까닭이 없음
|
||||
@@ -122,7 +124,9 @@ def get_groups() -> dict:
|
||||
|
||||
@router.get("/groups/{group}/files")
|
||||
def get_files(group: str) -> dict:
|
||||
return {"files": _call(store.files_of, group)}
|
||||
"""`판정규` = 머리 판의 해(「2026-01-01」 · 「2026」 → 「2026」) · 읽을 때 파생(파일 안 씀)."""
|
||||
files = _call(store.files_of, group)
|
||||
return {"files": [{**f, "판정규": mbs.edition(f["edition"])} for f in files]}
|
||||
|
||||
|
||||
@router.get("/subs")
|
||||
@@ -156,12 +160,17 @@ def get_tables(
|
||||
usage: str = "",
|
||||
section: str = "",
|
||||
) -> dict:
|
||||
return _call(store.tables, file, group, sub, detail, q, page, size, usage, section)
|
||||
out = _call(store.tables, file, group, sub, detail, q, page, size, usage, section)
|
||||
for t in out["tables"]: # 기준배 · 기준단위 — 읽을 때 파생(정본 표에 안 씀 · 712-3)
|
||||
t["기준파생"] = _num(mbs.basis(t))
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/table")
|
||||
def get_table(file: str, key: str) -> dict:
|
||||
return _call(store.table, file, key)
|
||||
"""`기준파생` 은 표 밖 칸 — 표 줄째 되돌려 저장해도 정본에 안 섞임."""
|
||||
out = _call(store.table, file, key)
|
||||
return {**out, "기준파생": _num(mbs.basis(out["table"]))}
|
||||
|
||||
|
||||
@router.get("/table/options")
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(2) 본문(`check_master_body.py`) — 표형 요소의 출처 절을 본문 md 에서 찾아 수를 맞댐 · 결손(본문 표에 있고 요소에 없음)·허구(요소에 있고 본문 절에 없음)
|
||||
(3) 로직 — 변수가 가리키는 요소·표·칸 · 찾기 조건 이름 · 로직 입력 · 재료 고르기 조건 · 돌고 도는 참조 · 끊긴 키
|
||||
(4) 조합 — 일위대가 조합이 담은 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음 (`master_combo.py`)
|
||||
(배) 로직 결과단위 배 ↔ 부르는 표 기준배(10㎡당 표를 원/㎡ 로직이 「/ 10」 없이 씀) (`master_basis.py`)
|
||||
(5) 계산 — 로직 키와 입력값으로 호표 줄별 수량·단가·금액 · 비목 합 (`master_formula.py`)
|
||||
수 뽑기는 `resources/knowledge/original/_pipeline/pum_md_tool.py` 의 것을 씀.
|
||||
"""
|
||||
@@ -27,6 +28,7 @@ COST = ROOT / "resources/knowledge/original/원가계산"
|
||||
sys.path.insert(0, str(HERE))
|
||||
sys.path.insert(0, str(ROOT / "resources/knowledge/original/_pipeline"))
|
||||
|
||||
import master_basis as mbs # noqa: E402
|
||||
import master_combo as mcb # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
import master_keys as mk # noqa: E402
|
||||
@@ -556,6 +558,8 @@ def main(argv: list[str]) -> int:
|
||||
report["조합"] = mcb.check_all(files, whole)
|
||||
if mode in ("단위", "전부"):
|
||||
report["단위"] = check_units(files, whole)
|
||||
if mode in ("배", "전부"): # 결과단위 배 ↔ 부르는 표 기준배(읽을 때 파생 · `master_basis`)
|
||||
report["배"] = mbs.check_times(files)
|
||||
for title, found in report.items():
|
||||
print(f"({title}) {len(found)}건")
|
||||
for line in found:
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""표 기준 → 「기준배 · 기준단위」 — **읽을 때 파생**(정본 표 파일에 쓰지 않음 · PLAN 712-3).
|
||||
|
||||
- 표 `기준` 글(「10㎡당」 · 「인/100본당」 · 「ton 당」)을 배 · 단위로 읽음.
|
||||
- `기준` 이 비었으면 원문 절 머리(「(단위: ㎥당)」 · 「(일당)」)에서
|
||||
— 절과 아래 절 머리가 한 꼴일 때만.
|
||||
- 단위가 아닌 기준(「인력품」 · 「할증」 · 「정의」)은 `갈래` 「비단위」 · 못 읽음은 「모름」.
|
||||
- 배 대조(`check_times`) = 로직 결과단위 배(원/10㎡ → 10) ↔ 그 로직이 부르는 표 기준배
|
||||
(`check_master` 「배」).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
|
||||
from check_master_body import section_of
|
||||
|
||||
# ponytail: 단위 별칭은 여기 짧은 표 — 712-2 단위 모듈이 서면 그쪽 별칭으로 갈아 끼움
|
||||
_ALIAS = {"ea": "개", "톤": "ton", "t": "ton", "㎞": "km", "hr": "시간", "min": "분", "㎏": "kg"}
|
||||
_UNITS = sorted(
|
||||
(
|
||||
"㎡ ㎥ ㎢ ㎞ km ㎜ mm ㎝ cm m ha ton 톤 t kg ㎏ 개소 개 본 대 회 조 기 틀 "
|
||||
"일 시간 hr 분 min 초 매 주 도엽 도곽 필지 점 발파 인 노선 공 경간 련 식 "
|
||||
"개월 월 년 가닥 면 장 측점 세트 set 호 층 단계 ea"
|
||||
).split(),
|
||||
key=len,
|
||||
reverse=True,
|
||||
)
|
||||
_UNIT_AT_END = re.compile(
|
||||
rf"(?<![\d:,.A-Za-z])(\d[\d,]*(?:\.\d+)?)?\s*({'|'.join(map(re.escape, _UNITS))})$"
|
||||
)
|
||||
_HANGUL = re.compile(r"[가-힣]")
|
||||
_HEAD = re.compile(r"^\(\s*(?:단위\s*:\s*)?([^()]*당[^()]*)\)$") # 원문 머리 줄 「(단위: ㎥당)」
|
||||
_RESULT = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*)?\s*([^\s·(\d,.][^\s·(]*)") # 「원/10㎡」
|
||||
|
||||
|
||||
def unit(text: str) -> str:
|
||||
return _ALIAS.get(text, text)
|
||||
|
||||
|
||||
def _times(text: str | None) -> Decimal:
|
||||
return Decimal(text.replace(",", "")) if text else Decimal(1)
|
||||
|
||||
|
||||
def parse(text) -> dict | None:
|
||||
"""기준 글 → `{배, 단위}` · 단위 아닌 글이면 None."""
|
||||
core = re.split(r"\s+[—-]\s+", str(text or "").strip())[0]
|
||||
core = re.sub(r"\([^()]*\)", "", core).strip()
|
||||
if not core or core.startswith("단위"): # 「단위: 인」 = 값 칸 단위(기준 아님)
|
||||
return None
|
||||
if re.search(
|
||||
r",\s|[∼~]", core
|
||||
): # 「구하는점 5점, 주어진점 3점」 · 「400필지∼450필지」 = 조건 글
|
||||
return None
|
||||
core = core.rsplit("/", 1)[-1].strip() # 「인/100본당」 → 「100본당」(자원/기준)
|
||||
if "당" in core: # 「1필지당 1,500㎡」 · 「1대당 높이 2m 기준」 → 「당」 앞까지
|
||||
core = core[: core.rfind("당")].strip()
|
||||
m = _UNIT_AT_END.search(core)
|
||||
if not m:
|
||||
return None
|
||||
one, at = m.group(2), m.start(2)
|
||||
if len(one) == 1 and _HANGUL.match(one) and _HANGUL.match(core[at - 1 : at] or " "):
|
||||
return None # 「참조」 의 「조」 — 한 글자 한글 단위가 낱말 끝이면 단위 아님
|
||||
return {"배": _times(m.group(1)), "단위": unit(m.group(2))}
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _heads(source: str) -> tuple[str, ...]:
|
||||
"""원문 절(과 아래 절) 머리 줄 글 — 차례대로 · 겹침 뺌."""
|
||||
_, _, text = section_of(source)
|
||||
found = (_HEAD.match(line.strip()) for line in text.split("\n"))
|
||||
return tuple(dict.fromkeys(m.group(1).strip() for m in found if m))
|
||||
|
||||
|
||||
def basis(table: dict) -> dict:
|
||||
"""표 하나의 파생 칸 — `{기준배, 기준단위, 기준근거, 갈래}`(갈래 = 단위 · 비단위 · 모름)."""
|
||||
said = str(table.get("기준") or "").strip()
|
||||
if said:
|
||||
got = parse(said)
|
||||
if got:
|
||||
return {"기준배": got["배"], "기준단위": got["단위"], "기준근거": "표", "갈래": "단위"}
|
||||
return {"기준배": None, "기준단위": None, "기준근거": "표", "갈래": "비단위"}
|
||||
heads = [h for h in _heads(str(table.get("출처") or "")) if parse(h)]
|
||||
picks = {(parse(h)["배"], parse(h)["단위"]) for h in heads}
|
||||
if len(picks) == 1: # 절 머리가 한 꼴일 때만 — 둘 넘으면 손 확인
|
||||
times, one = picks.pop()
|
||||
return {
|
||||
"기준배": times,
|
||||
"기준단위": one,
|
||||
"기준근거": f"원문 「{heads[0]}」",
|
||||
"갈래": "단위",
|
||||
}
|
||||
return {"기준배": None, "기준단위": None, "기준근거": None, "갈래": "모름"}
|
||||
|
||||
|
||||
def result_per(result_unit) -> list[tuple[Decimal, str]]:
|
||||
"""로직 결과단위 분모 — 「원/10㎡」 → [(10, ㎡)] · 여럿이면 여럿."""
|
||||
return [(_times(n), unit(u)) for n, u in _RESULT.findall(str(result_unit or ""))]
|
||||
|
||||
|
||||
def _formula_text(row: dict) -> str:
|
||||
return str([row.get(k) for k in ("중간", "호표", "덧줄")])
|
||||
|
||||
|
||||
def _divides(text: str, times: Decimal) -> bool:
|
||||
"""식이 그 배로 나누거나 곱하는가 — 「/ 10」 · 「÷10」 · 「* 0.1」."""
|
||||
n = format(times.normalize(), "f")
|
||||
inverse = format((1 / times).normalize(), "f")
|
||||
pattern = rf"(?:[/÷]\s*{re.escape(n)}|\*\s*{re.escape(inverse)})(?![\d.])"
|
||||
return re.search(pattern, text) is not None
|
||||
|
||||
|
||||
def check_times(files: dict[str, dict]) -> list[str]:
|
||||
"""배 어긋남 — 로직이 부르는 표(`용도.로직키`)의 기준배 ≠ 1 인데 결과단위가 같은 배가 아니고
|
||||
식에서도 그 배로 나누지 않는 짝(시스템 관리자 원문 확인 거리)."""
|
||||
logic_rows = {
|
||||
str(r.get("키")): r
|
||||
for d in files.values()
|
||||
if d.get("그룹") == "로직"
|
||||
for r in d.get("줄") or []
|
||||
}
|
||||
out = []
|
||||
for name, data in files.items():
|
||||
for table in data.get("표") or []:
|
||||
got = basis(table)
|
||||
if got["갈래"] != "단위" or got["기준배"] == 1:
|
||||
continue
|
||||
for key in (table.get("용도") or {}).get("로직키") or []:
|
||||
row = logic_rows.get(str(key))
|
||||
if row is None:
|
||||
continue
|
||||
per = result_per(row.get("결과단위"))
|
||||
shown = format(got["기준배"].normalize(), "f")
|
||||
said = str(row.get("결과단위") or "") # 「원/보양길이 100m」
|
||||
if (
|
||||
(got["기준배"], got["기준단위"]) in per
|
||||
or f"{shown}{got['기준단위']}" in said
|
||||
or _divides(_formula_text(row), got["기준배"])
|
||||
):
|
||||
continue
|
||||
out.append(
|
||||
f"{key} · 결과단위 「{row.get('결과단위')}」 ↔ 표 {table.get('키')}"
|
||||
f" 기준 「{shown}{got['기준단위']}」 — 배 어긋남"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def edition(text) -> str:
|
||||
"""파일 머리 판 → 해(「2026-01-01」 · 「2026」 → 「2026」) · 해 없으면 그대로(파일 안 씀)."""
|
||||
m = re.match(r"\s*(\d{4})(?:\D|$)", str(text or ""))
|
||||
return m.group(1) if m else str(text or "")
|
||||
@@ -0,0 +1,79 @@
|
||||
"""712-3 지킴 — 표 기준 → 기준배 · 기준단위 읽을 때 파생 · 배 대조 · 정본 안 씀(`master_basis`)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import check_master as cm # noqa: E402
|
||||
import master_basis as mbs # noqa: E402
|
||||
|
||||
|
||||
def _p(text):
|
||||
got = mbs.parse(text)
|
||||
return None if got is None else (got["배"], got["단위"])
|
||||
|
||||
|
||||
def test_parse_forms():
|
||||
assert _p("10㎡당") == (Decimal(10), "㎡")
|
||||
assert _p("인/100본당") == (Decimal(100), "본")
|
||||
assert _p("ton 당") == _p("톤당") == _p("1t") == (Decimal(1), "ton")
|
||||
assert _p("1,000본당") == (Decimal(1000), "본")
|
||||
assert _p("1필지당 1,500㎡") == (Decimal(1), "필지")
|
||||
assert _p("일당 — 편성 1조 기준") == (Decimal(1), "일")
|
||||
assert _p("세면기 개당") == (Decimal(1), "개")
|
||||
assert _p("바닥면적㎡당") == (Decimal(1), "㎡")
|
||||
for text in (
|
||||
"인력품",
|
||||
"정의",
|
||||
"할증",
|
||||
"참조",
|
||||
"Joint당",
|
||||
"단위: 인",
|
||||
"구하는점 5점, 주어진점 3점",
|
||||
):
|
||||
assert _p(text) is None, text
|
||||
|
||||
|
||||
def test_fill_ratio_and_no_write():
|
||||
files = cm.load()
|
||||
before = json.dumps(files, default=str, ensure_ascii=False)
|
||||
called = [
|
||||
mbs.basis(t)
|
||||
for d in files.values()
|
||||
for t in d.get("표") or []
|
||||
if (t.get("용도") or {}).get("로직키")
|
||||
]
|
||||
unit = sum(1 for b in called if b["갈래"] == "단위")
|
||||
from_body = sum(1 for b in called if str(b["기준근거"] or "").startswith("원문"))
|
||||
assert unit / len(called) >= 0.78 # 로직이 부르는 표 2,023 중 1,601(79 %)
|
||||
assert from_body >= 280 # 빈 기준 579 중 원문 머리로 채움 285
|
||||
assert json.dumps(files, default=str, ensure_ascii=False) == before # 읽기만
|
||||
|
||||
|
||||
def test_times_mismatch_list():
|
||||
"""배 어긋남 = 시스템 관리자 원문 확인 거리 — 늘거나 줄면 PLAN 712-3 목록도 고침."""
|
||||
found = {line.split(" · ")[0] for line in mbs.check_times(cm.load())}
|
||||
known = {"GC000482", "GC000483", "GC000484", "GC000485", "GF000081"}
|
||||
assert found == known | {"GC000522", "GC000524"}
|
||||
|
||||
|
||||
def test_edition():
|
||||
assert mbs.edition("2026-01-01") == mbs.edition("2026") == "2026"
|
||||
assert mbs.edition(None) == ""
|
||||
|
||||
|
||||
def test_router_basis_outside_table():
|
||||
from M01_MasterData import M01_MasterData_Router as router
|
||||
|
||||
got = router.get_table("소요량_산림품셈_13장_구조물.json", "QF000443")
|
||||
assert got["기준파생"]["기준배"] == 10 and got["기준파생"]["기준단위"] == "㎡"
|
||||
assert "기준배" not in got["table"] and "기준파생" not in got["table"]
|
||||
listed = router.get_tables(file="소요량_산림품셈_13장_구조물.json", q="QF000443")
|
||||
assert listed["tables"][0]["기준파생"]["기준배"] == 10
|
||||
Reference in New Issue
Block a user