From abd0d4b126226035dcdbdc89b07d973e7c2ecdcf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 25 Sep 2026 12:00:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=91=9C=20=EB=AC=B6=EC=9D=8C=20=EC=97=B4?= =?UTF-8?q?=EC=9D=80=20=EA=B0=92=EB=A7=88=EB=8B=A4=20=ED=95=9C=EB=8D=B0=20?= =?UTF-8?q?=C2=B7=20=EB=B9=88=20=EC=84=A4=EA=B3=84=EA=B0=92=20=EB=A8=B8?= =?UTF-8?q?=EB=A6=AC=EB=8A=94=20=E3=80=8C=EB=86=92=EC=9D=B4=20=EB=AF=B8?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=E3=80=8D=20=EA=BC=B4=20=C2=B7=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EC=97=B4=20=EC=84=A4=EA=B3=84=EA=B0=92=20=EB=94=B1?= =?UTF-8?q?=EC=A7=80=20=EB=97=8C=20(=EB=B8=8C=EB=A0=88=EC=9D=B8=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20=ED=99=95=EC=9D=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관공 등 묶음 열 차례 = 관종·관경마다 관매설 · 커플링 짝 — 머리 합치기가 값마다 묶임 - 머리틀은 맨 자리표 + 값꼴(H={} · T={}cm · Φ{}) · 빈 값은 빈값 글 · 열 id 는 그대로(빈 값 -) - 포장 A · 수축줄눈 · 커플링 · 큰돌쌓기 면적 = 바인딩 대신 펼침틀(묶음 · 머리틀 · 식틀) - 식틀 자리 값이 비면 식을 빼고 설명에 적음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015LuapLYqN1GGFD8Y1PStD5 --- M02_MasterTemplete/M02_Table_Fill.py | 135 ++++++++++++------ .../master_template/table/구조물집계표.json | 52 +++---- resources/tester/test_m02_table_fill.py | 29 +++- 3 files changed, 143 insertions(+), 73 deletions(-) diff --git a/M02_MasterTemplete/M02_Table_Fill.py b/M02_MasterTemplete/M02_Table_Fill.py index d5f32951..44dd9804 100644 --- a/M02_MasterTemplete/M02_Table_Fill.py +++ b/M02_MasterTemplete/M02_Table_Fill.py @@ -6,7 +6,8 @@ B06 횡단 설계 `design.pipe_length_m` 관 연장(부르는 쪽이 DB 에서 읽어 넘김) - 펼침 열 — 마스터 한 칸 → 설계에 쓰인 값마다 열(choices 차례) · 쓰인 값이 없으면 열을 뺌. - 열 id = `마스터 id|펼친 값|…`(머리 글자가 바뀌어도 id 는 그대로). + 열 id = `마스터 id|펼친 값|…`(머리 글자가 바뀌어도 id 는 그대로 · 빈 값은 `-`). + 한 묶음 열은 값마다 한데 모음 · 계산 열은 `펼침틀` 로 묶음을 따라 나뉨(바인딩 없음). - 줄 — 측점마다 한 줄(같은 측점 구조물은 한 줄) · 점 `NO.x+y` · 구간 `NO.a~NO.b` · 같은 측점에 관이 둘이면 줄을 나눔 · 줄 id 는 측점에서 지어 손 값이 다시 채워도 붙어 있음. - 손 열 값 · 전구간 줄 · 사용자가 더한 줄(`손`)은 작업본 것을 그대로 지킴. @@ -35,7 +36,10 @@ FACILITY_TYPES = { "ford_bridge": "ford_bridge", "revetment": "revetment", } -EMPTY_LABEL = "-" +EMPTY_LABEL = "-" # 열 id 안 빈 값(저장본과 맞춤) — 머리 글은 `빈값` +EMPTY_TEXT = "미입력" +#: 계산 열이 설계값 열 묶음을 따라 나뉠 때 쓰는 틀(바인딩 아님 — 표 부품이 설계값으로 안 봄). +FRAME_KEY = "펼침틀" ORPHAN_NOTE = "설계에서 측점 없어짐" #: 채운 표에 실어 보내는 펼침 전 열 — 저장 때 빠진 규칙 열을 되살리는 데만 씀. RULE_COLUMNS_KEY = "양식열" @@ -271,12 +275,31 @@ def _blank_vars(formula: str, variables: dict[str, Any]) -> list[str]: return [name for name in names if variables.get(name) in (None, "")] +def _frame(column: dict[str, Any]) -> dict[str, Any]: + """펼침 틀 — 설계값 열은 `바인딩` · 따라 나뉘는 계산 열은 `펼침틀`(설계값 딱지 없음).""" + return column.get("바인딩") or column.get(FRAME_KEY) or {} + + +def _group(column: dict[str, Any]) -> str: + return _frame(column).get("묶음") or column["id"] + + +def _head_part(frame: dict[str, Any], index: int, value: str) -> str: + """펼친 값 한 자리 머리 글 — `값꼴`(`H={}`) · 빈 값은 `빈값`(`높이 미입력`).""" + if value == EMPTY_LABEL: + empties = frame.get("빈값") or [] + return (empties[index] if index < len(empties) else "") or EMPTY_TEXT + shapes = frame.get("값꼴") or [] + shape = (shapes[index] if index < len(shapes) else "") or "{}" + return shape.replace("{}", str((frame.get("이름") or {}).get(value, value))) + + def _unexpand(columns: list[dict[str, Any]]) -> list[dict[str, Any]]: """채운 표가 작업본으로 저장됐으면 펼친 열을 마스터 칸(`원열`)으로 되돌림 — 두 번 안 펼침.""" out: list[dict[str, Any]] = [] seen: set[str] = set() for column in columns: - origin = (column.get("바인딩") or {}).get("원열") + origin = _frame(column).get("원열") if origin is None: out.append(column) elif origin["id"] not in seen: @@ -291,62 +314,82 @@ def _expand_columns( variables: dict[str, Any], choices: dict[str, list[str]], ) -> tuple[list[dict[str, Any]], dict[str, list[tuple[str, str, dict[str, Any]]]]]: - """펼침 열 → 값마다 열. 돌려주는 둘째 = `{master id: [(열 id, suffix, 펼친 열)]}`.""" + """펼침 열 → 값마다 열 · 묶음은 값마다 한데(관 Φ800 관매설 · 커플링 → Φ1000 …). + + 돌려주는 둘째 = `{master id: [(열 id, suffix, 펼친 열)]}`. + """ columns = _unexpand(columns) groups: dict[str, set[tuple[str, ...]]] = {} orders_of: dict[str, list[list[str]]] = {} + members: dict[str, list[dict[str, Any]]] = {} for column in columns: - binding = column.get("바인딩") or {} - if not column.get("펼침") or not binding: + if not column.get("펼침") or not _frame(column): continue - group = binding.get("묶음") or column["id"] - orders_of.setdefault(group, _position_orders(binding, choices)) - found = groups.setdefault(group, set()) + members.setdefault(_group(column), []).append(column) + binding = column.get("바인딩") + if not binding: + continue # 계산 열 — 설계값 열이 연 값을 따름 + orders_of.setdefault(_group(column), _position_orders(binding, choices)) + found = groups.setdefault(_group(column), set()) for source in _sources(binding): # 값이 빈 구조물도 열은 세움 — 놓였는데 수량이 빈칸인 것이 보여야 함 found.update(_expansion(source, item) for item in items if _matches(source, item)) - members: dict[str, set[str]] = {} - for column in columns: - binding = column.get("바인딩") or {} - if column.get("펼침") and binding: - members.setdefault(binding.get("묶음") or column["id"], set()).add(column["id"]) - out: list[dict[str, Any]] = [] made: dict[str, list[tuple[str, str, dict[str, Any]]]] = {} for column in columns: - binding = column.get("바인딩") or {} - if not column.get("펼침") or not binding: + if not column.get("펼침") or not _frame(column): out.append(copy.deepcopy(column)) continue - group = binding.get("묶음") or column["id"] - ordered = sorted(groups.get(group, set()), key=lambda v: _order_key(v, orders_of[group])) - names = binding.get("이름") or {} - for values in ordered: - shown = [names.get(value, value) for value in values] - suffix = "|".join(values) - new = copy.deepcopy(column) - new["id"] = f"{column['id']}|{suffix}" - new["펼침"] = False - new["머리"] = [ - None if part is None else part.format(*shown) - for part in (binding.get("머리틀") or column["머리"]) - ] - new["바인딩"] = {**binding, "펼친값": list(values), "원열": copy.deepcopy(column)} - formula = binding.get("식틀") - formula = formula.format(*values) if formula else column.get("식") - if formula: - formula = _rewrite(formula, members.get(group, set()), suffix) - if _blank_vars(formula, variables): - new.pop("식", None) - new["설명"] = f"{column.get('설명', '')} · 변수가 빈칸이라 계산 안 함".strip() - else: - new["식"] = formula - out.append(new) - made.setdefault(column["id"], []).append((new["id"], suffix, new)) + group = _group(column) + if members[group][0] is not column: + continue # 묶음 첫 열 자리에서 값마다 묶음 열을 한데 냄 + orders = orders_of.get(group, []) + sibling_ids = {member["id"] for member in members[group]} + for values in sorted(groups.get(group, set()), key=lambda v: _order_key(v, orders)): + for member in members[group]: + new = _expand_one(member, values, sibling_ids, variables) + out.append(new) + made.setdefault(member["id"], []).append((new["id"], "|".join(values), new)) return out, made +def _expand_one( + column: dict[str, Any], + values: tuple[str, ...], + siblings: set[str], + variables: dict[str, Any], +) -> dict[str, Any]: + frame = _frame(column) + suffix = "|".join(values) + shown = [_head_part(frame, index, value) for index, value in enumerate(values)] + new = copy.deepcopy(column) + new["id"] = f"{column['id']}|{suffix}" + new["펼침"] = False + new["머리"] = [ + None if part is None else part.format(*shown) + for part in (frame.get("머리틀") or column["머리"]) + ] + key = "바인딩" if column.get("바인딩") else FRAME_KEY + new[key] = {**frame, "펼친값": list(values), "원열": copy.deepcopy(column)} + template = frame.get("식틀") + used = [int(i) for i in re.findall(r"\{(\d+)\}", template or "")] + blank = "" + if any(values[i] == EMPTY_LABEL for i in used if i < len(values)): + formula, blank = None, "설계값이" + else: + formula = template.format(*values) if template else column.get("식") + if formula: + formula = _rewrite(formula, siblings, suffix) + blank = "변수가" if _blank_vars(formula, variables) else "" + if blank: + new.pop("식", None) + new["설명"] = f"{column.get('설명', '')} · {blank} 빈칸이라 계산 안 함".strip() + elif formula: + new["식"] = formula + return new + + def fill_document( document: dict[str, Any], items: list[dict[str, Any]], @@ -400,8 +443,8 @@ def fill_document( spacing = _number(item["options"].get("joint_spacing_m")) if item["type_id"] != "pavement_concrete" or spacing in (None, base_spacing): continue - for new_id, _, new in made.get("pv_jt", []): - if list(_expansion({"펼침": new["바인딩"]["펼침"]}, item)) == new["바인딩"]["펼친값"]: + for new_id, suffix, new in made.get("pv_jt", []): + if "|".join(_expansion({"펼침": ["thickness_cm"]}, item)) == suffix: text = new.get("식", "").replace("[$수축줄눈_간격_m]", _label("", spacing)) slots[slot].setdefault("식", {})[new_id] = text @@ -452,11 +495,11 @@ def fill_document( def _hand_ids(columns: list[dict[str, Any]]) -> set[str]: - """사람이 적는 열 — 바인딩 없는 열(손 열 · 비고 · 사용자가 더한 열) · 차례 · 측점 빼고.""" + """사람이 적는 열 — 설계값 · 계산 열 말고(손 열 · 비고 · 더한 열) · 차례 · 측점 빼고.""" return { column["id"] for column in columns - if not column.get("바인딩") and column["id"] not in ("no", "sta") + if not _frame(column) and not column.get("식") and column["id"] not in ("no", "sta") } diff --git a/resources/master_template/table/구조물집계표.json b/resources/master_template/table/구조물집계표.json index 0417e27f..6b91bff4 100644 --- a/resources/master_template/table/구조물집계표.json +++ b/resources/master_template/table/구조물집계표.json @@ -3,7 +3,7 @@ "종류": "표", "판": 1, "설명": "측점마다 구조물 수량 한 줄 · 머리 4층(종류 · 공법 · 규격 · 단위) · 설계값 열은 프로젝트 설계에서 채움(화면에서 못 고침 — 설계를 고침) · 손 열은 사용자가 적음 · 계산 열은 식. 종류 정본 = B05_Profile/B05_Profile_Structure_Types.json.", - "바인딩규칙": "종류 = type_id · 펼침 = 값마다 열을 나눌 칸 키(앞에 = 이면 고정 글) · 값 = 칸 키 | 개소(1건 1) | 길이(length_m, 없으면 끝-시작) | 관연장(B06 횡단 pipe_length_m) · 묶음 = 같은 펼침으로 함께 나뉘는 열 · 머리틀 = 펼친 머리({0}{1}… = 펼침 값 차례) · 이름 = 설계 값 → 머리 글 · 조건 = {칸: 값}(맞는 것만) · 더함 = 같은 열에 더하는 다른 출처 · 식틀 = 펼친 열의 식({0}… 펼침 값)", + "바인딩규칙": "종류 = type_id · 펼침 = 값마다 열을 나눌 칸 키(앞에 = 이면 고정 글) · 값 = 칸 키 | 개소(1건 1) | 길이(length_m, 없으면 끝-시작) | 관연장(B06 횡단 pipe_length_m) · 묶음 = 같은 펼침으로 함께 나뉘는 열 · 머리틀 = 펼친 머리({0}{1}… = 펼침 값 차례) · 이름 = 설계 값 → 머리 글 · 조건 = {칸: 값}(맞는 것만) · 더함 = 같은 열에 더하는 다른 출처 · 식틀 = 펼친 열의 식({0}… 펼침 값) · 값꼴 = 자리마다 머리 글 꼴({} = 값) · 빈값 = 설계값이 빈 자리 머리 글 · 펼침틀 = 계산 열이 같은 묶음으로 따라 나뉨(묶음 · 머리틀 · 값꼴 · 빈값 · 식틀 — 설계값을 받지 않아 바인딩 없음)", "층": ["종류", "공법", "규격"], "변수": { "파형강관_1본_m": 8, @@ -23,7 +23,7 @@ "일위대가": "UA000004", "바인딩": { "종류": "revetment", "펼침": ["form", "height_m"], "값": "length_m", "묶음": "rv", - "머리틀": ["돌기슭막이", "{0}", "H={1}"], + "머리틀": ["돌기슭막이", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["형태 미입력", "높이 미입력"], "이름": { "돌쌓기(찰)": "찰쌓기", "돌쌓기(메)": "메쌓기" }, "더함": [ { "종류": "pipe", "펼침": ["inlet_revet_form", "inlet_revet_height_m"], "값": "inlet_revet_length_m", "조건": { "inlet_type": "기슭막이" } }, @@ -35,18 +35,18 @@ { "id": "pv_b", "머리": ["콘크리트포장", "T=(두께마다)", "B"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 포장 폭", - "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "width_m", "묶음": "pv", "머리틀": ["콘크리트포장", "T={0}cm", "B"] } + "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "width_m", "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "B"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { "id": "pv_l", "머리": ["콘크리트포장", "T=(두께마다)", "L"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 포장 길이", "일위대가": "UA000007", - "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "길이", "묶음": "pv", "머리틀": ["콘크리트포장", "T={0}cm", "L"] } + "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "길이", "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "L"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { "id": "pv_w", "머리": ["콘크리트포장", "T=(두께마다)", "확폭"], "단위": "㎡", "꼴": "수", "펼침": true, "설명": "설계값 — 확폭 면적", - "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "widening_area_m2", "묶음": "pv", "머리틀": ["콘크리트포장", "T={0}cm", "확폭"] } + "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "widening_area_m2", "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "확폭"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { "id": "pv_a", "머리": ["콘크리트포장", "T=(두께마다)", "A"], "단위": "㎡", "꼴": "수", "펼침": true, @@ -54,14 +54,14 @@ "끝수": { "자리": 2, "방법": "반올림" }, "설명": "계산 — B × L + 확폭", "일위대가": "UA000006", - "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "식", "묶음": "pv", "머리틀": ["콘크리트포장", "T={0}cm", "A"] } + "펼침틀": { "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "A"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { "id": "pv_jt", "머리": ["콘크리트포장", "T=(두께마다)", "수축줄눈"], "단위": "m", "꼴": "수", "펼침": true, "식": "IF([pv_l]>0,INT([pv_l]/[$수축줄눈_간격_m])*[pv_b],\"\")", "끝수": { "자리": 2, "방법": "반올림" }, "설명": "계산 — 내림(L ÷ 줄눈 간격) × B · 줄눈 간격 = 설계값(기본 6m · 다르면 그 줄 식에 박음)", - "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "식", "묶음": "pv", "머리틀": ["콘크리트포장", "T={0}cm", "수축줄눈"] } + "펼침틀": { "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "수축줄눈"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { @@ -69,14 +69,14 @@ "설명": "설계값 — 횡단 관 연장(B06) · 같은 측점에 관이 둘이면 줄을 나눔", "일위대가": "UA000036", "일위대가후보": ["UA000036", "UA000027"], - "바인딩": { "종류": "pipe", "펼침": ["pipe_kind", "pipe_diameter_mm"], "값": "관연장", "묶음": "pp", "머리틀": ["관공", "{0} Φ{1}", "관매설"] } + "바인딩": { "종류": "pipe", "펼침": ["pipe_kind", "pipe_diameter_mm"], "값": "관연장", "묶음": "pp", "머리틀": ["관공", "{0} {1}", "관매설"], "값꼴": ["{}", "Φ{}"], "빈값": ["관종 미입력", "관경 미입력"] } }, { "id": "pp_cp", "머리": ["관공", "(관종·관경마다)", "커플링밴드"], "단위": "개소", "꼴": "수", "펼침": true, "식": "IF([pp_len]>0,ROUNDUP([pp_len]/[$파형강관_1본_m],0)-1,\"\")", "설명": "계산 — 올림(L ÷ 1본 길이) − 1 · 관마다(줄마다) 계산 뒤 합 · 1본 길이 = 변수(관종마다 · 빈칸이면 계산 안 함)", - "바인딩": { - "종류": "pipe", "펼침": ["pipe_kind", "pipe_diameter_mm"], "값": "식", "묶음": "pp", "머리틀": ["관공", "{0} Φ{1}", "커플링밴드"], + "펼침틀": { + "묶음": "pp", "머리틀": ["관공", "{0} {1}", "커플링밴드"], "값꼴": ["{}", "Φ{}"], "빈값": ["관종 미입력", "관경 미입력"], "식틀": "IF([pp_len]>0,ROUNDUP([pp_len]/[${0}_1본_m],0)-1,\"\")" } }, @@ -87,7 +87,7 @@ "일위대가": "UA000008", "일위대가후보": ["UA000008", "UA000025", "UA000029"], "바인딩": { - "종류": "pipe", "펼침": ["wing_wall_type"], "값": "개소", "묶음": "pg", "머리틀": ["관보호공", "{0}", "유입"], + "종류": "pipe", "펼침": ["wing_wall_type"], "값": "개소", "묶음": "pg", "머리틀": ["관보호공", "{0}", "유입"], "빈값": ["형식 미입력"], "이름": { "A-TYPE": "날개벽 A형", "C-TYPE": "날개벽 C형", "A-TYPE+집수정": "날개벽 A형+집수정" }, "더함": [ { "종류": "pipe", "펼침": ["inlet_basin_form"], "값": "개소", "조건": { "inlet_type": "집수정" } } ] } @@ -97,14 +97,14 @@ "id": "bx_len", "머리": ["BOX암거", "(폭×높이마다)", "연장"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — BOX 연장(계류 방향)", "일위대가": "UA000014", - "바인딩": { "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "length_m", "묶음": "bx", "머리틀": ["BOX암거", "{0}×{1}", "연장"] } + "바인딩": { "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "length_m", "묶음": "bx", "머리틀": ["BOX암거", "{0}×{1}", "연장"], "빈값": ["폭 미입력", "높이 미입력"] } }, { "id": "bx_wing", "머리": ["BOX암거", "(폭×높이마다)", "날개벽"], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 날개벽 유입·유출 있음 수", "일위대가": "UA000025", "바인딩": { - "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "개소", "묶음": "bx", "머리틀": ["BOX암거", "{0}×{1}", "날개벽"], + "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "개소", "묶음": "bx", "머리틀": ["BOX암거", "{0}×{1}", "날개벽"], "빈값": ["폭 미입력", "높이 미입력"], "조건": { "wing_in": "있음" }, "더함": [ { "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "개소", "조건": { "wing_out": "있음" } } ] } @@ -113,32 +113,32 @@ { "id": "fb", "머리": ["세월교", "(관경 × 련수마다)", null], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 세월교 개소", - "바인딩": { "종류": "ford_bridge", "펼침": ["pipe_diameter_mm", "pipe_count"], "값": "개소", "묶음": "fb", "머리틀": ["세월교", "Φ{0}×{1}련", null] } + "바인딩": { "종류": "ford_bridge", "펼침": ["pipe_diameter_mm", "pipe_count"], "값": "개소", "묶음": "fb", "머리틀": ["세월교", "{0}×{1}", null], "값꼴": ["Φ{}", "{}련"], "빈값": ["관경 미입력", "련수 미입력"] } }, { "id": "fp_w", "머리": ["물넘이포장", "T=(두께마다)", "폭"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 월류 폭", - "바인딩": { "종류": "ford_pavement", "펼침": ["thickness_cm"], "값": "ford_width_m", "묶음": "fp", "머리틀": ["물넘이포장", "T={0}cm", "폭"] } + "바인딩": { "종류": "ford_pavement", "펼침": ["thickness_cm"], "값": "ford_width_m", "묶음": "fp", "머리틀": ["물넘이포장", "{0}", "폭"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { "id": "fp_l", "머리": ["물넘이포장", "T=(두께마다)", "길이"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 포장 길이(노폭 방향)", - "바인딩": { "종류": "ford_pavement", "펼침": ["thickness_cm"], "값": "length_m", "묶음": "fp", "머리틀": ["물넘이포장", "T={0}cm", "길이"] } + "바인딩": { "종류": "ford_pavement", "펼침": ["thickness_cm"], "값": "length_m", "묶음": "fp", "머리틀": ["물넘이포장", "{0}", "길이"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { "id": "od_len", "머리": ["횡단개거", "(규격마다)", "길이"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 개거 연장", "일위대가": "UA000026", - "바인딩": { "종류": "open_ditch", "펼침": ["ditch_spec"], "값": "length_m", "묶음": "od", "머리틀": ["횡단개거", "{0}", "길이"] } + "바인딩": { "종류": "open_ditch", "펼침": ["ditch_spec"], "값": "length_m", "묶음": "od", "머리틀": ["횡단개거", "{0}", "길이"], "빈값": ["규격 미입력"] } }, { "id": "od_cnt", "머리": ["횡단개거", "(규격마다)", "개소"], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 개거 · 노출형 횡단수로 개소", "일위대가": "UA000003", "바인딩": { - "종류": "open_ditch", "펼침": ["ditch_spec"], "값": "개소", "묶음": "od", "머리틀": ["횡단개거", "{0}", "개소"], + "종류": "open_ditch", "펼침": ["ditch_spec"], "값": "개소", "묶음": "od", "머리틀": ["횡단개거", "{0}", "개소"], "빈값": ["규격 미입력"], "더함": [ { "종류": "cross_drain_exposed", "펼침": ["=노출형"], "값": "개소" } ] } }, @@ -146,13 +146,13 @@ { "id": "rw", "머리": ["옹벽", "(형식마다)", "H=(높이마다)"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 옹벽 길이", - "바인딩": { "종류": "retaining_wall", "펼침": ["form", "height_m"], "값": "길이", "묶음": "rw", "머리틀": ["옹벽", "{0}", "H={1}"] } + "바인딩": { "종류": "retaining_wall", "펼침": ["form", "height_m"], "값": "길이", "묶음": "rw", "머리틀": ["옹벽", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["형식 미입력", "높이 미입력"] } }, { "id": "ms", "머리": ["돌쌓기", "(찰·메마다)", "H=(높이마다)"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 돌쌓기 길이", "바인딩": { - "종류": "masonry_wet", "펼침": ["=찰쌓기", "height_m"], "값": "길이", "묶음": "ms", "머리틀": ["돌쌓기", "{0}", "H={1}"], + "종류": "masonry_wet", "펼침": ["=찰쌓기", "height_m"], "값": "길이", "묶음": "ms", "머리틀": ["돌쌓기", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["", "높이 미입력"], "더함": [ { "종류": "masonry_dry", "펼침": ["=메쌓기", "height_m"], "값": "길이" } ] } }, @@ -160,19 +160,19 @@ "id": "sg", "머리": ["흙막이", "(형태마다)", "H=(높이마다)"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 흙막이 길이 · 일위대가는 큰돌만", "일위대가": "UA000032", - "바인딩": { "종류": "soil_guard", "펼침": ["form", "height_m"], "값": "길이", "묶음": "sg", "머리틀": ["흙막이", "{0}", "H={1}"] } + "바인딩": { "종류": "soil_guard", "펼침": ["form", "height_m"], "값": "길이", "묶음": "sg", "머리틀": ["흙막이", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["형식 미입력", "높이 미입력"] } }, { "id": "bm_len", "머리": ["큰돌쌓기", "(찰·메 · 돌 크기 · 높이마다)", "길이"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 큰돌쌓기 길이", - "바인딩": { "종류": "boulder_masonry", "펼침": ["bond", "stone_cm", "height_m"], "값": "길이", "묶음": "bm", "머리틀": ["큰돌쌓기", "{0} {1} H={2}", "길이"] } + "바인딩": { "종류": "boulder_masonry", "펼침": ["bond", "stone_cm", "height_m"], "값": "길이", "묶음": "bm", "머리틀": ["큰돌쌓기", "{0} {1} {2}", "길이"], "값꼴": ["{}", "{}", "H={}"], "빈값": ["쌓기 방식 미입력", "돌규격 미입력", "높이 미입력"] } }, { "id": "bm_area", "머리": ["큰돌쌓기", "(찰·메 · 돌 크기 · 높이마다)", "면적"], "단위": "㎡", "꼴": "수", "펼침": true, "설명": "계산 — L × H (H = 펼친 높이)", "일위대가": "UA000031", - "바인딩": { - "종류": "boulder_masonry", "펼침": ["bond", "stone_cm", "height_m"], "값": "식", "묶음": "bm", "머리틀": ["큰돌쌓기", "{0} {1} H={2}", "면적"], + "펼침틀": { + "묶음": "bm", "머리틀": ["큰돌쌓기", "{0} {1} {2}", "면적"], "값꼴": ["{}", "{}", "H={}"], "빈값": ["쌓기 방식 미입력", "돌규격 미입력", "높이 미입력"], "식틀": "IF([bm_len]>0,[bm_len]*{2},\"\")" } }, @@ -180,7 +180,7 @@ { "id": "ec", "머리": ["골막이", "(형태마다)", null], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 골막이 개소", - "바인딩": { "종류": "erosion_check", "펼침": ["form"], "값": "개소", "묶음": "ec", "머리틀": ["골막이", "{0}", null] } + "바인딩": { "종류": "erosion_check", "펼침": ["form"], "값": "개소", "묶음": "ec", "머리틀": ["골막이", "{0}", null], "빈값": ["형식 미입력"] } }, { "id": "be", "머리": ["소단", null, null], "단위": "m", "꼴": "수", @@ -200,7 +200,7 @@ { "id": "etc", "머리": ["기타", "(이름마다)", null], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 기타 구조물 개소", - "바인딩": { "종류": "etc", "펼침": ["name"], "값": "개소", "묶음": "etc", "머리틀": ["기타", "{0}", null] } + "바인딩": { "종류": "etc", "펼침": ["name"], "값": "개소", "묶음": "etc", "머리틀": ["기타", "{0}", null], "빈값": ["이름 미입력"] } }, { "id": "h_intake", "머리": ["취수정", null, null], "단위": "개소", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000005" }, diff --git a/resources/tester/test_m02_table_fill.py b/resources/tester/test_m02_table_fill.py index bffc7425..a3086885 100644 --- a/resources/tester/test_m02_table_fill.py +++ b/resources/tester/test_m02_table_fill.py @@ -92,10 +92,37 @@ def test_펼침_열은_설계에_쓰인_값만_차례대로(project: Path) -> No assert "rv" not in ids and "ps" in ids # 펼침 마스터 칸은 빠지고 고정 열은 남음 # 관 유입·유출 기슭막이는 레지스트리 기본값(찰 · 메 · 높이 없음)으로 열이 섬 · id 는 설계 값 assert "rv|돌쌓기(찰)|-" in ids and "rv|돌쌓기(메)|-" in ids - assert _col(doc, "rv|돌쌓기(찰)|-")["머리"] == ["돌기슭막이", "찰쌓기", "H=-"] + assert _col(doc, "rv|돌쌓기(찰)|-")["머리"] == ["돌기슭막이", "찰쌓기", "높이 미입력"] + assert _col(doc, "fb|1000|-")["머리"] == ["세월교", "Φ1000×련수 미입력", None] assert not any(i.startswith("pg_in|") for i in ids) # 날개벽·집수정 형식 없음 → 관보호공 없음 +def test_묶음_열은_값마다_한데(project: Path) -> None: + ids = [column["id"] for column in _filled(project)["열"]] + assert [i for i in ids if i.startswith("pp_")] == [ + "pp_len|흄관|1000", + "pp_cp|흄관|1000", + "pp_len|파형강관|1000", + "pp_cp|파형강관|1000", + ] + assert [i for i in ids if i.startswith("pv_")] == [ + f"pv_{k}|20" for k in ("b", "l", "w", "a", "jt") + ] + + +def test_계산_열은_설계값_딱지_없음(project: Path) -> None: + master = _master() + assert not [c["id"] for c in master["열"] if c.get("식") and c.get("바인딩")] + doc = _filled(project) + for col_id in ("pv_a|20", "pv_jt|20", "pp_cp|파형강관|1000", "bm_area|메쌓기|40~60|2.5"): + column = _col(doc, col_id) + assert "바인딩" not in column and column["펼침틀"]["원열"]["id"] == col_id.split("|")[0] + assert _col(doc, "pp_cp|파형강관|1000")["머리"] == ["관공", "파형강관 Φ1000", "커플링밴드"] + # 다시 저장 · 채워도 같은 열(계산 열도 원열로 되돌림) + again = fill.fill_table(project, fill.strip_design(doc), {85.0: 14.0}) + assert [c["id"] for c in again["열"]] == [c["id"] for c in doc["열"]] + + def test_계산_열_식은_같은_묶음_열로_바뀌고_빈_변수면_안_셈(project: Path) -> None: doc = _filled(project) assert _col(doc, "pv_a|20")["식"] == 'IF([pv_l|20]>0,[pv_b|20]*[pv_l|20]+[pv_w|20],"")'