feat(z01): 연결고리 표가 확정으로 이은 줄에 「연결됨」 — 감추지 않고 표시만(기본 다 보임) · 「연결된 것만」 거르기 · 판정은 work_item_link 표(코드에 목록 없음 · 미판정 안 이음) · 건설 11 · 산림 5 · 이름표 열 「연결」 · 금액 불변 시험에 이행 전 코드로 뜬 원가계산서 금액 못박음(내역서·원가계산서 지문이 이행 전후 한 글자 같았음)(브레인)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user