From 54fc156f1870df90a363fd11561cddf5e8ae578d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 15:21:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(713-3):=20=EB=A1=9C=EC=A7=81=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=8D=A7=EC=B9=B8=20=EC=84=9C=EB=B2=84=20=E2=80=94?= =?UTF-8?q?=20/logics=20=EC=A4=84=EB=A7=88=EB=8B=A4=20=EB=AC=B6=EC=9D=8C(?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=A0=88)=20=C2=B7=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=EB=9F=89=20=EB=B6=80=EB=AA=A8=20=C2=B7=20=EB=B6=80=EB=A5=B4?= =?UTF-8?q?=EB=8A=94/=EB=B6=88=EB=A6=AC=EB=8A=94=20=EC=88=98=20=C2=B7=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8(=EB=B9=84=EA=B3=A0=20=EC=A0=88=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8)=20=C2=B7=20=EC=A0=95=EB=B3=B8=20=EA=B7=B8=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=C2=B7=20=EC=83=88=20=ED=8C=8C=EC=9D=BC=20Store=5FL?= =?UTF-8?q?ist=20=C2=B7=20=EC=9D=B8=EC=9E=AC=EA=B2=BD=20=EC=A4=84=20?= =?UTF-8?q?=EC=88=98=EB=8A=94=20712-1=20=EB=92=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- M01_MasterData/M01_MasterData_Router.py | 4 +- M01_MasterData/M01_MasterData_Store_List.py | 96 +++++++++++++++++++++ resources/tester/test_713_3_list.py | 31 +++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 M01_MasterData/M01_MasterData_Store_List.py create mode 100644 resources/tester/test_713_3_list.py diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py index d3d083af4..02b30265a 100644 --- a/M01_MasterData/M01_MasterData_Router.py +++ b/M01_MasterData/M01_MasterData_Router.py @@ -15,6 +15,7 @@ from pydantic import BaseModel from M01_MasterData import M01_MasterData_Store as store 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_List as store_list from M01_MasterData import M01_MasterData_Store_Make as make from M01_MasterData import M01_MasterData_Store_Revise as revise @@ -193,7 +194,8 @@ def get_sections( def get_logics( sub: str = "", detail: str = "", q: str = "", blocked: int | None = None, owner: str = "" ) -> dict: - return {"logics": _call(store.logics, sub, detail, q, blocked, owner)} + """줄마다 덧칸(묶음 · 작업량 · 부르는/불리는 수 · 관련 · 713-3) — 읽기만.""" + return {"logics": store_list.attach(_call(store.logics, sub, detail, q, blocked, owner))} @router.get("/logic") diff --git a/M01_MasterData/M01_MasterData_Store_List.py b/M01_MasterData/M01_MasterData_Store_List.py new file mode 100644 index 000000000..18a0ba796 --- /dev/null +++ b/M01_MasterData/M01_MasterData_Store_List.py @@ -0,0 +1,96 @@ +"""M01 로직 목록 덧칸 (PLAN 713-3 · 요구서 내역12 4장 7) — 정본은 그대로 · 목록에서만 묶음. + +`/logics` 줄마다 붙이는 읽기 칸(파일에 안 씀): +- `묶음` = 같은 파일 · 같은 원문번호 로직이 둘 넘으면 그 번호 · `묶음수` = 몇 개. +- `작업량` = 돈 아닌 로직(`결과` 칸) · `부모` = 그 작업량을 부르는 로직(목록에서 그 밑으로 접음). +- `부르는수` · `불리는수` = 로직끼리 부르기(기계경비 포함 · `Store_Cache.counted`). +- `관련` = 비고 글의 절 번호(「1-5-6(주③)」 · 「별도」 참조)가 가리키는 로직 — 계산 밖 링크. +""" + +from __future__ import annotations + +import re +from collections import Counter, defaultdict + +from M01_MasterData import M01_MasterData_Store as store +from M01_MasterData import M01_MasterData_Store_Cache as cache + +_REF = re.compile( + r"(?:(산림품셈|건설품셈)\s*(공통|토목|건축|기계설비|유지관리)?\s*)?(? tuple[str, str]: + return str(data.get("원문") or ""), str(data.get("부문") or "") + + +def _related(row: dict, book: tuple[str, str], by_section: dict) -> list[str]: + """비고 글 절 번호 → 같은 원문(책 이름이 앞에 있으면 그 책) 로직 키 · 자기 빼고 · 차례대로.""" + out: dict[str, None] = {} + clauses = [c for c in str(row.get("비고") or "").split(" · ") if not c.startswith("해석")] + for name, division, number in _REF.findall(" · ".join(clauses)): + where = (name, division or ("" if name == "산림품셈" else book[1])) if name else book + keys = by_section.get((*where, number), []) + if len(keys) > _WIDE: # 장 · 큰 절 통째(「1-1」)는 링크 아님 + continue + out |= dict.fromkeys(k for k in keys if k != str(row.get("키"))) + return list(out) + + +def extras() -> dict[str, dict]: + """로직 키 → 덧칸 — 목록 전체로 셈(거른 목록이어도 묶음 수 · 부르는 수는 전체 기준).""" + files, _ = store.loaded() + rows = [ + (name, data, row) + for name, data in files.items() + if data.get("그룹") == "로직" + for row in data.get("줄") or [] + ] + calls = {key: got[1] for key, got in cache.counted(store.FOLDER).items()} + called = Counter(k for got in calls.values() for k in got) + parents: dict[str, list[str]] = defaultdict(list) + for key, got in calls.items(): + for k in got: + parents[k].append(key) + groups = Counter((name, str(row.get("원문번호") or "")) for name, _, row in rows) + by_section: dict[tuple, list[str]] = defaultdict(list) + for _, data, row in rows: + number = str(row.get("원문번호") or "") + for at in range(1, number.count("-") + 1): # 「1-5-6」 은 「1-5」 로도 찾힘 + by_section[(*_book(data), "-".join(number.split("-")[: at + 1]))].append( + str(row.get("키")) + ) + out = {} + for name, data, row in rows: + key, number = str(row.get("키")), str(row.get("원문번호") or "") + many = groups[(name, number)] if number else 1 + work = "결과" in row + out[key] = { + "묶음": number if many > 1 else "", + "묶음수": many if many > 1 else 0, + "작업량": work, + "부모": sorted(parents.get(key, [])) if work else [], + "부르는수": len(calls.get(key, ())), + "불리는수": called.get(key, 0), + "관련": _related(row, _book(data), by_section), + } + return out + + +def attach(logics: list[dict]) -> list[dict]: + """`/logics` 줄에 덧칸을 붙임(없는 키 = 저장 안 한 초안 — 빈 칸).""" + more = extras() + blank = { + "묶음": "", + "묶음수": 0, + "작업량": False, + "부모": [], + "부르는수": 0, + "불리는수": 0, + "관련": [], + } + return [{**one, **more.get(str(one.get("키")), blank)} for one in logics] diff --git a/resources/tester/test_713_3_list.py b/resources/tester/test_713_3_list.py new file mode 100644 index 000000000..6631933f3 --- /dev/null +++ b/resources/tester/test_713_3_list.py @@ -0,0 +1,31 @@ +"""713-3 지킴 — 로직 목록 덧칸(묶음 · 작업량 부모 · 부르는/불리는 수 · 관련) · 정본 그대로 · 줄 수 그대로.""" + +from __future__ import annotations + +from M01_MasterData import M01_MasterData_Router as router +from M01_MasterData import M01_MasterData_Store_List as store_list + +COLS = {"묶음", "묶음수", "작업량", "부모", "부르는수", "불리는수", "관련"} + + +def test_목록_줄_수_그대로_덧칸_다_있음() -> None: + got = router.get_logics()["logics"] + assert len(got) == 1469 # 묶음은 화면 접기만 — 목록 줄은 그대로 + assert all(COLS <= set(x) for x in got) + + +def test_묶음_작업량_부르기() -> None: + more = store_list.extras() + assert sum(1 for v in more.values() if v["묶음"]) == 323 # 한 절에 로직 둘 넘음(조사 12-B ㉯) + work = {k for k, v in more.items() if v["작업량"]} + assert len(work) == 27 + assert more["GF000102"]["부모"] == ["GF000103"] # 구조물터파기 작업량 → 단가 로직 밑 + assert more["GC000268"]["불리는수"] >= 300 # 기계경비 로직 + assert sum(1 for v in more.values() if v["부르는수"]) == 506 + assert more["GC000294"]["관련"] == ["GC000292"] # 비고 「1-5-6(주③)」 + + +def test_관련은_해석_글과_넓은_절을_뺌() -> None: + more = store_list.extras() + assert all(len(v["관련"]) <= store_list._WIDE for v in more.values()) + assert not any(k in v["관련"] for k, v in more.items()) # 자기 자신 없음