From 337e42314cd4f5e627a781e720939b8ec28edfc7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 17 Sep 2026 16:46:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(z01):=20=EC=97=B0=EA=B2=B0=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=91=9C=EA=B0=80=20=ED=99=95=EC=A0=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EC=9D=80=20=EC=A4=84=EC=97=90=20=E3=80=8C?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EB=90=A8=E3=80=8D=20=E2=80=94=20=EA=B0=90?= =?UTF-8?q?=EC=B6=94=EC=A7=80=20=EC=95=8A=EA=B3=A0=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=A7=8C(=EA=B8=B0=EB=B3=B8=20=EB=8B=A4=20=EB=B3=B4=EC=9E=84)?= =?UTF-8?q?=20=C2=B7=20=E3=80=8C=EC=97=B0=EA=B2=B0=EB=90=9C=20=EA=B2=83?= =?UTF-8?q?=EB=A7=8C=E3=80=8D=20=EA=B1=B0=EB=A5=B4=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=EC=9D=80=20work=5Fitem=5Flink=20=ED=91=9C(?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EB=AA=A9=EB=A1=9D=20=EC=97=86?= =?UTF-8?q?=EC=9D=8C=20=C2=B7=20=EB=AF=B8=ED=8C=90=EC=A0=95=20=EC=95=88=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=8C)=20=C2=B7=20=EA=B1=B4=EC=84=A4=2011=20=C2=B7?= =?UTF-8?q?=20=EC=82=B0=EB=A6=BC=205=20=C2=B7=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=ED=91=9C=20=EC=97=B4=20=E3=80=8C=EC=97=B0=EA=B2=B0=E3=80=8D=20?= =?UTF-8?q?=C2=B7=20=EA=B8=88=EC=95=A1=20=EB=B6=88=EB=B3=80=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=EC=97=90=20=EC=9D=B4=ED=96=89=20=EC=A0=84=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A1=9C=20=EB=9C=AC=20=EC=9B=90=EA=B0=80=EA=B3=84?= =?UTF-8?q?=EC=82=B0=EC=84=9C=20=EA=B8=88=EC=95=A1=20=EB=AA=BB=EB=B0=95?= =?UTF-8?q?=EC=9D=8C(=EB=82=B4=EC=97=AD=EC=84=9C=C2=B7=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=84=9C=20=EC=A7=80=EB=AC=B8=EC=9D=B4=20?= =?UTF-8?q?=EC=9D=B4=ED=96=89=20=EC=A0=84=ED=9B=84=20=ED=95=9C=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=20=EA=B0=99=EC=95=98=EC=9D=8C)(=EB=B8=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- Z01_MasterData/Z01_MasterData_WorkItems.py | 50 ++++++++++++++++--- .../data_master_labels/labels_2026-01-01.json | 16 +++++- resources/tester/test_work_item_key_gate.py | 24 +++++++++ resources/tester/test_z01_work_items.py | 44 ++++++++++++++-- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/Z01_MasterData/Z01_MasterData_WorkItems.py b/Z01_MasterData/Z01_MasterData_WorkItems.py index 0d955e3f..63487f1d 100644 --- a/Z01_MasterData/Z01_MasterData_WorkItems.py +++ b/Z01_MasterData/Z01_MasterData_WorkItems.py @@ -97,12 +97,36 @@ def base_rows(kind: str) -> list[dict[str, Any]]: "pum_tables": [t["pum_table_id"] for t in item["tables"]], "rule": parent.get("parent_mode"), "step_weight": weight, + # 「임도가 쓰는 것」 — 연결고리 표가 확정으로 이은 줄(판정은 그 표 · 감추지 않고 표시만) + "link": LINKED if _key(item) in linked_keys() else "", "effective_date": d.get("pum_edition") or d.get("effective_date"), } ) return rows +LINK_FOLDER = tables.RESOURCES / "data_work_item_link" +LINKED = "연결됨" + + +def linked_keys() -> frozenset[str]: + """연결고리 표(`work_item_link_*.json` 끝 판 · 코덱스)가 **확정**으로 이은 열쇠(산림·건설). + + ⚠ 목록을 코드에 안 둠 — 판정은 그 표 · 미판정은 안 이음(표의 `unconfirmed_action: do_not_link`). + """ + found = sorted(LINK_FOLDER.glob("work_item_link_*.json")) if LINK_FOLDER.is_dir() else [] + if not found: + return frozenset() + links = _read(str(found[-1]), found[-1].stat().st_mtime_ns).get("links") or [] + return frozenset( + key + for link in links + if link.get("status") == "확정" + for key in (link.get("forest_key"), link.get("const_key")) + if key + ) + + def rules(kind: str) -> list[dict[str, Any]]: """부모 마디 계산 규칙 — choose_one(아래 중 하나) · sum_steps(아래를 무게대로 더해야 한 단위).""" found = doc(kind) @@ -144,16 +168,24 @@ def notice(kind: str) -> list[str]: return lines -#: 거르기 — **자료에 있는 가름만**(건설 부문 · 줄 구실). 어느 줄을 거를지는 서버 한 곳 · 화면은 고른 값만 보냄. -#: ⚠ 「임도가 쓰는 것」 같은 쓰임 가름은 잣대가 아직 없어 안 냄(지어내지 않음). -FILTER_KEYS = ("division", "axis_role") +#: 거르기 — **자료에 있는 가름만**(건설 부문 · 줄 구실 · 연결고리 표). 어느 줄을 거를지는 서버 한 곳 · +#: 화면은 고른 값만 보냄 · 기본은 늘 「전부」(감추지 않음 — 사용자 「어디에 쓸지 모르니 건설 전체」 · 브레인). +FILTER_KEYS = ("link", "division", "axis_role") ALL = "" def _facets(kind: str) -> dict[str, dict[str, str]]: found = doc(kind) items = found[0].get("work_items") or [] if found else [] - return {_key(i): {key: str(i.get(key) or "") for key in FILTER_KEYS} for i in items} + linked = linked_keys() + return { + _key(i): { + "link": LINKED if _key(i) in linked else ALL, + "division": str(i.get("division") or ""), + "axis_role": str(i.get("axis_role") or ""), + } + for i in items + } def _role_names() -> dict[str, str]: @@ -166,9 +198,13 @@ def _role_names() -> dict[str, str]: def filters(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: - """거르기 상자 — 값이 둘 이상 갈리는 가름만 · 기본은 「전부」(안 거름).""" + """거르기 상자 — 줄이 둘 이상으로 갈리는 가름만 · 기본은 「전부」(안 거름).""" facets = _facets(kind) - labels = {"division": ("부문", {}), "axis_role": ("줄 구실", _role_names())} + labels = { + "link": ("연결", {LINKED: "연결된 것만"}), + "division": ("부문", {}), + "axis_role": ("줄 구실", _role_names()), + } out = [] for key in FILTER_KEYS: counts: dict[str, int] = {} @@ -176,7 +212,7 @@ def filters(kind: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: value = facets.get(row["@id"], {}).get(key, ALL) counts[value] = counts.get(value, 0) + 1 values = [v for v in counts if v != ALL] - if len(values) < 2: + if len(counts) < 2: # 다 같은 값이면 거를 것이 없음(연결은 「연결됨」 · 빈칸 둘로 갈림) continue label, names = labels[key] out.append( diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index 1a560ad0..9aae9769 100644 --- a/resources/data_master_labels/labels_2026-01-01.json +++ b/resources/data_master_labels/labels_2026-01-01.json @@ -1414,6 +1414,13 @@ "visible": true, "note": "`sum_steps` 일 때만 — 발파암 절취 0.1 · 깍기 0.9 · 집토 1." }, + { + "key": "link", + "name_ko": "연결", + "unit": "", + "visible": true, + "note": "「연결됨」 = 연결고리 표(`work_item_link`)가 확정으로 이은 줄 — 임도가 쓰는 공종. 판정은 그 표 · 감추지 않고 표시만." + }, { "key": "effective_date", "name_ko": "적용 시작일", @@ -1477,6 +1484,13 @@ "visible": true, "note": "`sum_steps` 일 때만 — 건설은 아직 합산형 선언 없음." }, + { + "key": "link", + "name_ko": "연결", + "unit": "", + "visible": true, + "note": "「연결됨」 = 연결고리 표(`work_item_link`)가 확정으로 이은 줄 — 임도가 쓰는 공종. 판정은 그 표 · 감추지 않고 표시만." + }, { "key": "effective_date", "name_ko": "적용 시작일", @@ -1930,7 +1944,7 @@ }, "counts": { "merged_tables": 17, - "merged_columns": 172, + "merged_columns": 174, "files": 41, "tables": 100, "columns": 532, diff --git a/resources/tester/test_work_item_key_gate.py b/resources/tester/test_work_item_key_gate.py index 7df9eeb0..700fad49 100644 --- a/resources/tester/test_work_item_key_gate.py +++ b/resources/tester/test_work_item_key_gate.py @@ -137,3 +137,27 @@ def test_금액_불변_열쇠가_있든_없든_내역서가_한_원도_안_다 ] priced = [row for row in new.rows if not row.is_group and row.amount_krw] assert len(priced) >= 3 and new.body_total_krw > 0 # 금액이 실제로 선 줄로 잼 + + +#: ⭐ 이행 **전** 코드(4bfbf98f^ · 길목 없음)로 위 `_handoff()` 를 세워 뜬 금액 — 2026-09-17 랩탑 메인. +#: 이행 뒤 코드도 내역서 줄 18·성분·원가계산서 지문까지 한 글자 같았음(옛 두 파일을 따로 얹어 맞댐). +#: ⚠ 단가 자료(노임·기계·자재·요율)가 바뀌면 이 수도 바뀜 — 그땐 길목 탓이 아닌지 먼저 볼 것 +#: (열쇠 있든 없든 같은지 = 위 시험) · 길목 탓이면 고칠 것은 코드, 아니면 다시 뜸. +PRE_MIGRATION_KRW = { + "material_cost": "1158206", + "labor_cost": "4400597", + "expense": "1991430", + "direct_construction_cost": "5983724", + "net_construction_cost": "7550233", + "total_cost": "9203637", + "grand_total": "10124000", +} + + +def test_금액_불변_이행_전_코드로_뜬_원가계산서와_한_원도_같음() -> None: + from B09_Estimation.B09_Estimation_BillOfQuantities import cost_input_from_bill + from B09_Estimation.B09_Estimation_Engine_Cost import calculate_cost + + bill = build_bill(copy.deepcopy(_handoff()), build=build_unit_prices()) + totals = calculate_cost(cost_input_from_bill(bill)).totals + assert {key: str(totals[key]) for key in PRE_MIGRATION_KRW} == PRE_MIGRATION_KRW diff --git a/resources/tester/test_z01_work_items.py b/resources/tester/test_z01_work_items.py index 9e8b30a8..c42dca97 100644 --- a/resources/tester/test_z01_work_items.py +++ b/resources/tester/test_z01_work_items.py @@ -117,10 +117,10 @@ def test_불변_열쇠가_오면_id_로_씀_자료_파일은_판_id_로_가림( def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestClient) -> None: """화면(서브 b890fba0)이 `filters` 를 받으면 상자를 세우고 고른 값만 보냄 — 판정은 서버 한 곳. - 건설 부문 다섯 · 줄 구실(이름은 axis_policy.json). ⚠ 「임도가 쓰는 것」은 잣대가 없어 안 냄.""" + 건설 부문 다섯 · 줄 구실(이름은 axis_policy.json) · 연결(연결고리 표).""" const = _get(client, "work_item_const", size=1) by_key = {f["key"]: f for f in const["filters"]} - assert set(by_key) == {"division", "axis_role"} + assert set(by_key) == {"link", "division", "axis_role"} division = by_key["division"] assert division["default"] == "" assert division["options"][0] == {"value": "", "label": "전부", "rows": const["total"]} @@ -147,5 +147,43 @@ def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestCl assert rules["total"] > 0 assert all(r["name"].startswith("공통부문 › 적용기준") for r in rules["rows"]) - assert [f["key"] for f in _get(client, "work_item_forest", size=1)["filters"]] == ["axis_role"] + assert [f["key"] for f in _get(client, "work_item_forest", size=1)["filters"]] == [ + "link", + "axis_role", + ] assert _get(client, "labor", size=1, division="토목부문")["total"] == 261 # 다른 표는 안 거름 + + +def test_연결고리_표가_이은_줄에_연결됨_감추지_않고_표시만(client: TestClient) -> None: + """브레인 2026-09-17 — 「임도가 쓰는 것」 = 연결고리 표(work_item_link)가 **확정**으로 이은 줄. + 기본은 다 보임 · 표시 「연결됨」 · 「연결된 것만」 상자 하나 · 판정은 그 표(코드에 목록 없음).""" + from Z01_MasterData import Z01_MasterData_WorkItems as work_items + + link_doc = json.loads( + sorted(work_items.LINK_FOLDER.glob("work_item_link_*.json"))[-1].read_text(encoding="utf-8") + ) + confirmed = { + key + for link in link_doc["links"] + if link["status"] == "확정" + for key in (link["forest_key"], link["const_key"]) + if key + } + unconfirmed = { + key + for link in link_doc["links"] + if link["status"] != "확정" + for key in (link["forest_key"], link["const_key"]) + if key + } - confirmed + for kind, prefix in (("work_item_const", "CW-"), ("work_item_forest", "FW-")): + every = _get(client, kind, size=500) + rows = every["rows"] + _get(client, kind, size=500, page=2)["rows"] + assert len(rows) == every["total"] # 기본은 다 보임 + marked = {r["@id"] for r in rows if r["link"] == "연결됨"} + assert marked == {k for k in confirmed if k.startswith(prefix)} + assert not marked & unconfirmed + only = _get(client, kind, size=500, link="연결됨") + assert {r["@id"] for r in only["rows"]} == marked and only["total"] == len(marked) + box = next(f for f in every["filters"] if f["key"] == "link") + assert box["default"] == "" and box["options"][1]["rows"] == len(marked)