chore(M01): 로직 1,354줄 입력 목록 · 요약(PLAN 28-1)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYNJBegcapQYMwkKAf8D7S
This commit is contained in:
2026-09-30 13:02:48 +09:00
co-authored by Claude Fable 5.1
parent 50abcb894d
commit 8bed79a277
4 changed files with 7535 additions and 0 deletions
+427
View File
@@ -0,0 +1,427 @@
# -*- coding: utf-8 -*-
"""M01 로직마다 「받아야 할 입력」 목록 뽑기(PLAN 28-1) — 점검만 · 자료를 안 고침.
실행: ./venv/Scripts/python.exe tmp/M01_입력목록/build.py → 입력목록.md · 입력목록.csv · 요약.md
"""
import csv
import re
import sys
from collections import Counter, defaultdict
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts"))
import master_auto as mau # noqa: E402
import master_formula as mf # noqa: E402
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
OUT = Path(__file__).parent
LOGIC_KEY = re.compile(r"^G[FCX]\d{6}$")
REGION_NAMES = {"지역", "자재지역", "전력 계약종별"}
HOLE = re.compile(r"\{([^{}]+)\}")
def plain(value):
if isinstance(value, Decimal):
return str(int(value)) if value == value.to_integral_value() else format(value.normalize(), "f")
return "" if value is None else str(value)
# ── 자료 읽기 ───────────────────────────────────────────────────────────
files, whole = m01.loaded()
logics = [] # (파일 이름, 머리, 줄)
for name, doc in sorted(files.items()):
if name.startswith("로직_"):
for row in doc["줄"]:
logics.append((name, doc, row))
by_key = {row["키"]: row for _, _, row in logics}
combo_rows = m01.read("일위대가조합.json")[0]["줄"]
in_combo = defaultdict(set) # 로직 키 → 일위대가 키
for combo in combo_rows:
for one in combo.get("담은로직") or []:
in_combo[str(one.get("로직"))].add(combo["키"])
def place(name: str, doc: dict, row: dict) -> dict:
"""부문 · 장 · 절 · 임도 묶음."""
m = re.match(r"로직_(산림품셈|건설품셈)_(\d+)장_(.+)\.json", name)
source, chapter, title = m.group(1), int(m.group(2)), m.group(3)
part = "산림" if source == "산림품셈" else str(doc.get("부문"))
forest = part == "산림" or (part == "토목" and 1 <= chapter <= 3) or (part == "공통" and 5 <= chapter <= 7)
return {
"부문": part,
"장": f"{chapter:02d}장 {title.replace('_', ' ')}",
"절": str(row.get("원문번호") or ""),
"임도": forest,
}
def texts(row: dict):
"""(식 글자, 어디) — 중간 · 결과 · 호표 수량 · 요소 · 덧줄."""
for step in row.get("중간") or []:
yield step["식"], f"중간 {step['이름']}"
if "결과" in row:
yield row["결과"]["식"], "결과"
for item in row.get("호표") or []:
yield item["수량"], f"호표 {item.get('이름', '')} 수량"
if isinstance(item.get("요소"), str):
yield item["요소"], f"호표 {item.get('이름', '')} 요소"
for extra in row.get("덧줄") or []:
yield extra["식"], f"덧줄 {extra['이름']}"
# ── 부르는 관계 · 넘겨 주는 값 ───────────────────────────────────────────
callers = defaultdict(list) # 부른 로직 키 → [(부르는 키, {입력 이름: 식 마디})]
for _, _, row in logics:
for text, _ in texts(row):
try:
node = mf.parse(text)
except mf.FormulaError:
continue
for part in mf.walk(node):
if part[0] == "logic" and LOGIC_KEY.match(part[1]):
callers[part[1]].append((row["키"], dict(part[2])))
def how_passed(target: dict, name: str):
"""부르는 쪽이 이 입력을 넘기나 → (넘기는 부름 수, 전체 부름 수, 상수만인가)."""
calls = callers.get(target["키"], [])
given = [args[name] for _, args in calls if name in args]
constant = bool(given) and all(node[0] in ("num", "str") for node in given)
return len(given), len(calls), constant, [plain(node[1]) for node in given if node[0] in ("num", "str")]
# ── 식에서 입력이 쓰인 수 · 정의 빠짐 ─────────────────────────────────────
def usage(row: dict) -> Counter:
used = Counter()
for text, _ in texts(row):
try:
node = mf.parse(text)
except mf.FormulaError:
continue
for part in mf.walk(node):
if part[0] == "name":
used[part[1]] += 1
if part[0] in ("ref", "find", "logic"):
for hole in HOLE.findall(part[1]):
used[hole] += 1
for item in row.get("호표") or []:
if isinstance(item.get("요소"), dict):
for value in item["요소"].values():
for hole in HOLE.findall(value) if isinstance(value, str) else []:
used[hole] += 1
return used
def missing_of(row: dict) -> dict[str, str]:
"""입력 정의 빠짐 — 엔진 검사(`check_logic`)가 「모르는 이름」 · 「{이름} 입력·중간 아님」 · 「입력 필요」 로 알리는 것."""
reasons, _ = mf.check_logic(whole, row)
found: dict[str, str] = {}
for reason in reasons:
m = re.search(r"모르는 이름 「(.+?)」", reason)
if m:
found.setdefault(m.group(1), "식에 쓰였으나 입력 · 중간에 없음")
m = re.search(r"원문번호 \{(.+?)\} 가 입력·중간 아님", reason) or re.search(
r"고르기 조건 \{(.+?)\} 가 입력·중간 아님", reason
)
if m:
found.setdefault(m.group(1), "원문번호 · 고르기 조건 {…} 가 입력 · 중간에 없음")
m = re.search(r"는 입력 「(.+?)」 필요", reason)
if m:
found.setdefault(m.group(1), "지역 · 계약종별 고르기 조건이 이 입력을 필요로 함")
return found
# ── 한 줄씩 ────────────────────────────────────────────────────────────
HEAD = [
"로직 키", "로직 이름", "결과단위", "부문", "장", "절", "임도", "일위대가", "입력 이름", "단위",
"어디서(갈래)", "세부", "선택지 · 범위", "기본값(견본)", "기본값 근거", "식 쓰임", "정의 불완전", "상태",
]
table = []
stats = {
"logics": len(logics), "inputs": 0, "missing_rows": 0, "missing_logics": set(), "no_input_logics": 0,
}
input_facts = [] # 요약용 한 입력마다
def choices_text(spec: dict) -> str:
if spec.get("고르기"):
items = [plain(v) for v in spec["고르기"]]
shown = " / ".join(items[:4]) + (f" … 외 {len(items) - 4}" if len(items) > 4 else "")
return f"고르기 {len(items)}: {shown}"
if spec.get("범위"):
return f"범위 {plain(spec['범위'][0])} ~ {plain(spec['범위'][1])}"
return ""
def default_rule(row: dict, spec: dict, finds: dict, divs: set) -> str:
name = spec["이름"]
if spec.get("고르기"):
return "첫 선택지"
if spec.get("범위"):
return "범위 아래 끝"
if finds.get(name):
return "찾기 표의 가장 낮은 값"
return "비움(가격)" if "원" in str(spec.get("단위") or "") else ("1(나누는 자리)" if name in divs else "0")
for name, doc, row in logics:
where = place(name, doc, row)
auto = mau.values(whole, row)
finds, divs = mau._uses(row)
used = usage(row)
gone = missing_of(row)
combos = sorted(in_combo.get(row["키"], []))
base = [row["키"], row.get("이름") or "", row.get("결과단위") or "", where["부문"], where["장"], where["절"],
"●" if where["임도"] else "", " ".join(combos)]
specs = row.get("입력") or []
if not specs and not gone:
stats["no_input_logics"] += 1
table.append([*base, "(입력 없음)", "", "—", "", "", "", "", "", "", "입력 없음"])
for spec in specs:
stats["inputs"] += 1
given, total, constant, literals = how_passed(row, spec["이름"])
top = bool(combos) # 조합이 직접 담은 로직 = 25장 선택 입력 카드가 읽음
if given and given == total and not top:
kind = "상수" if constant else "다른 로직"
detail = "상위 로직이 고정값 " + "·".join(sorted(set(literals))[:3]) + " 로 넘김" if constant else "상위 로직이 값을 넘김(맨 위 로직 입력으로 안 보임)"
elif spec["이름"] in REGION_NAMES:
kind, detail = "사용자 선택", "프로젝트 공통 후보(지역 · 계약종별) — 항목마다 고를 값이 아님"
elif spec.get("고르기"):
if len(spec["고르기"]) == 1:
kind, detail = "상수", "선택지가 하나뿐"
else:
kind, detail = "사용자 선택", "목록에서 고름"
elif spec.get("범위"):
kind, detail = "설계값", "원문 범위 안의 수를 넣음"
else:
kind, detail = "설계값", "설계자가 수를 넣음"
if given and (given < total or top) and given > 0:
detail += f" · 부르는 로직 {total} 중 {given} 곳이 넘김"
rule = default_rule(row, spec, finds, divs)
state = "" if used[spec["이름"]] else "식에서 안 쓰임"
# 정의 불완전 — 수 입력(고르기 아님)에 단위가 없으면 · 설명이 없으면(도움말 자리)
weak = []
if not spec.get("고르기") and not spec.get("단위"):
weak.append("단위 없음")
if not spec.get("설명"):
weak.append("설명 없음")
table.append([
*base, spec["이름"], spec.get("단위") or "", kind, detail, choices_text(spec),
plain(auto.get(spec["이름"])), rule, str(used[spec["이름"]]), " · ".join(weak), state,
])
input_facts.append({
"name": spec["이름"], "unit": spec.get("단위") or "", "kind": kind, "part": where["부문"], "forest": where["임도"],
"top": top, "logic": row["키"], "choices": bool(spec.get("고르기")), "range": bool(spec.get("범위")),
"used": used[spec["이름"]], "desc": bool(spec.get("설명")), "region": spec["이름"] in REGION_NAMES,
"nounit": (not spec.get("고르기") and not spec.get("단위")),
})
for miss, why in gone.items():
if miss in {s["이름"] for s in specs}:
continue
stats["missing_rows"] += 1
stats["missing_logics"].add(row["키"])
table.append([*base, miss, "", "—(정의 없음)", why, "", "", "", str(used[miss]), "", "입력 정의 빠짐"])
input_facts.append({
"name": miss, "unit": "", "kind": "빠짐", "part": where["부문"], "forest": where["임도"], "top": bool(combos),
"logic": row["키"], "choices": False, "range": False, "used": used[miss], "desc": False, "region": miss in REGION_NAMES,
})
# ── 파일 쓰기 ──────────────────────────────────────────────────────────
def cell(text) -> str:
return str(text).replace("|", "\\|").replace("\n", " ")
with (OUT / "입력목록.csv").open("w", encoding="utf-8-sig", newline="") as fh:
writer = csv.writer(fh)
writer.writerow(HEAD)
writer.writerows(table)
with (OUT / "입력목록.md").open("w", encoding="utf-8") as fh:
fh.write("# M01 로직마다 받아야 할 입력 목록\n\n")
fh.write(
"PLAN 28-1 · 점검만(자료를 안 고침) · 로직 줄 %d · 표 줄 %d(입력 %d · 정의 빠짐 %d · 입력 없는 로직 %d).\n\n"
% (stats["logics"], len(table), stats["inputs"], stats["missing_rows"], stats["no_input_logics"])
)
fh.write(
"- **어디서(갈래)** — 설계값 = 설계자가 수를 넣음(범위 있으면 그 안) · 사용자 선택 = 목록(고르기)에서 고름 · "
"상수 = 선택지가 하나이거나 부르는 로직이 늘 같은 값을 넘김 · 다른 로직 = 부르는 로직이 값을 넘김.\n"
)
fh.write(
"- **일위대가** = 이 로직을 담은 조합 키(25장 선택 입력 카드는 조합이 직접 담은 로직의 입력만 읽음) · "
"**임도** ● = 산림품셈 전부 · 건설품셈 토목 1~3장 · 공통 5~7장.\n"
)
fh.write(
"- **기본값(견본)** = `master_auto` 보기용 자동값 — 프로젝트 기본값이 아님 · **입력 정의 빠짐** = 식이 쓰는데 "
"입력·중간에 없는 이름(엔진 `check_logic` 「모르는 이름」 · 「{…} 입력·중간 아님」 · 「입력 필요」).\n\n"
)
fh.write("| " + " | ".join(HEAD) + " |\n")
fh.write("|" + "---|" * len(HEAD) + "\n")
for line in table:
fh.write("| " + " | ".join(cell(v) for v in line) + " |\n")
# ── 요약 ───────────────────────────────────────────────────────────────
parts = ["산림", "공통", "토목", "건축", "기계설비", "유지관리"]
logic_by_part = Counter(place(n, d, r)["부문"] for n, d, r in logics)
inp_by_part = Counter(f["part"] for f in input_facts if f["kind"] != "빠짐")
miss_by_part = Counter(f["part"] for f in input_facts if f["kind"] == "빠짐")
missl_by_part = defaultdict(set)
for f in input_facts:
if f["kind"] == "빠짐":
missl_by_part[f["part"]].add(f["logic"])
kind_by_part = defaultdict(Counter)
for f in input_facts:
if f["kind"] != "빠짐":
kind_by_part[f["part"]][f["kind"]] += 1
forest_logics = sum(1 for n, d, r in logics if place(n, d, r)["임도"])
real = [f for f in input_facts if f["kind"] != "빠짐"]
names = Counter(f["name"] for f in real)
top20 = names.most_common(20)
def mix(name):
c = Counter(f["kind"] for f in real if f["name"] == name)
return " · ".join(f"{k} {v}" for k, v in c.most_common())
def units(name):
c = Counter(f["unit"] or "(없음)" for f in real if f["name"] == name)
return " · ".join(f"{k} {v}" for k, v in c.most_common(3))
def sections(lines):
return "\n".join(lines) + "\n"
rows_md = []
rows_md.append("# M01 입력 목록 요약(PLAN 28-1)\n")
rows_md.append(
f"로직 {stats['logics']}줄 · 입력 {stats['inputs']}개(이름 {len(names)}종) · 입력 정의 빠짐 {stats['missing_rows']}건"
f"(로직 {len(stats['missing_logics'])}줄) · 입력 없는 로직 {stats['no_input_logics']}줄 · 임도 묶음 로직 {forest_logics}줄.\n"
)
rows_md.append("## 1. 부문별 입력 수 · 빠짐 수\n")
rows_md.append("| 부문 | 로직 | 입력 | 설계값 | 사용자 선택 | 상수 | 다른 로직 | 정의 빠짐(건) | 빠짐 있는 로직 |")
rows_md.append("|---|---|---|---|---|---|---|---|---|")
for p in parts:
k = kind_by_part[p]
rows_md.append(
f"| {p} | {logic_by_part[p]} | {inp_by_part[p]} | {k['설계값']} | {k['사용자 선택']} | {k['상수']} | {k['다른 로직']} | {miss_by_part[p]} | {len(missl_by_part[p])} |"
)
allk = Counter(f["kind"] for f in real)
rows_md.append(
f"| **합계** | {stats['logics']} | {stats['inputs']} | {allk['설계값']} | {allk['사용자 선택']} | {allk['상수']} | {allk['다른 로직']} | {stats['missing_rows']} | {len(stats['missing_logics'])} |\n"
)
forest = [f for f in real if f["forest"]]
fk = Counter(f["kind"] for f in forest)
fmiss = [f for f in input_facts if f["kind"] == "빠짐" and f["forest"]]
rows_md.append(
f"**임도 묶음**(산림 · 토목 1~3장 · 공통 5~7장): 로직 {forest_logics}줄 · 입력 {len(forest)}개 "
f"(설계값 {fk['설계값']} · 사용자 선택 {fk['사용자 선택']} · 상수 {fk['상수']} · 다른 로직 {fk['다른 로직']}) · "
f"정의 빠짐 {len(fmiss)}건(로직 {len({f['logic'] for f in fmiss})}줄).\n"
)
rows_md.append("## 2. 입력 정의 빠짐 — 실측\n")
miss_names = Counter(f["name"] for f in input_facts if f["kind"] == "빠짐")
# 견본 자동값으로 돌려 봄 — 못 도는 로직 = 자동값으로는 받을 수 없는 입력이 있음
ran = failed = 0
fail_ex = []
for _, _, row in logics:
try:
mf.run(whole, row["키"], mau.values(whole, row))
ran += 1
except Exception as error: # noqa: BLE001 — 어떤 오류든 세어 알림
failed += 1
fail_ex.append(f"{row['키']}: {str(error)[:60]}")
# 찾기가 표 조건을 안 준 곳 — 그 조건 열에 값이 둘 이상이면 입력이 필요할 수 있는 자리
omit_all = omit_multi = 0
omit_logics = set()
multi_logics = set()
for _, _, row in logics:
for text, _ in texts(row):
try:
node = mf.parse(text)
except mf.FormulaError:
continue
for part in mf.walk(node):
if part[0] != "find":
continue
try:
tab = whole.get(part[1])
except mf.FormulaError:
continue
lack = set(tab.get("조건", {})) - {c for c, _ in part[2]}
if not lack:
continue
omit_all += 1
omit_logics.add(row["키"])
for cond in lack:
values = {str(r.get(cond)) for r in tab["줄"] if r.get(cond) is not None}
if len(values) >= 2:
omit_multi += 1
multi_logics.add(row["키"])
break
gone_logic = {"산림": 0, "공통": 0, "토목": 0, "건축": 0, "기계설비": 0, "유지관리": 0}
flaw_file = ROOT / "docs" / "raw" / "M01_점검" / "종합" / "종합_흠목록.md"
if flaw_file.is_file():
for line in flaw_file.read_text(encoding="utf-8").splitlines():
cols = [c.strip() for c in line.split("|")]
if len(cols) >= 5 and cols[4] == "로직 없음(절 누락)" and cols[0] in gone_logic:
gone_logic[cols[0]] += 1
nounit = sum(1 for f in real if f.get("nounit"))
rows_md.append(f"- **이름 빠짐(식이 쓰는데 입력 · 중간에 없음)**: **{stats['missing_rows']}건**(로직 {len(stats['missing_logics'])}줄) — 엔진 검사(`check_logic` 「모르는 이름」 · 「{{…}} 입력·중간 아님」 · 「입력 필요」)와 같은 기준 · 1,354줄 전부 통과.")
rows_md.append(f"- **견본 자동값으로 계산**: {ran}줄 통과 · {failed}줄 실패 — 자동값(고르기 첫째 · 범위 아래 끝 · 표 최저 · 0/1)만으로도 모든 로직이 돎 → 정의가 모자라 못 도는 로직은 없음. 시험입력 78줄은 일괄 시험용 견본." + (" 실패: " + " · ".join(fail_ex[:5]) if fail_ex else ""))
rows_md.append(f"- **로직 자체가 없어 입력을 못 뽑은 절**(종합_흠목록 「로직 없음」): " + " · ".join(f"{k} {v}" for k, v in gone_logic.items()) + f" = 합 {sum(gone_logic.values())}절(28-2a) — 이 절들은 입력 정의 자체가 없음.")
rows_md.append(f"- **정의 불완전(참고)**: 수 입력(고르기 아님)인데 단위가 없는 것 {nounit}개 · 설명(도움말) 없는 것 {sum(1 for f in real if not f['desc'])}개 — 표 「정의 불완전」 칸에 표시.")
rows_md.append(f"- **찾기가 표 조건을 안 준 곳**(참고): {omit_all}곳(로직 {len(omit_logics)}줄) 가운데 그 조건 열에 값이 둘 이상 있는 곳 {omit_multi}곳(로직 {len(multi_logics)}줄) — 지금 견본으로 계산은 되지만 값을 바꾸면 「줄이 여럿」 이 날 수 있는 자리(입력을 더해야 할 후보 · 28-2d 계산식 어긋남과 겹침).")
rows_md.append("")
rows_md.append("## 3. 자주 쓰이는 입력 20\n")
rows_md.append("| 순위 | 입력 이름 | 로직 수 | 단위(많은 순) | 갈래 |")
rows_md.append("|---|---|---|---|---|")
for i, (n, c) in enumerate(top20, 1):
rows_md.append(f"| {i} | {n} | {c} | {units(n)} | {mix(n)} |")
rows_md.append("")
# 25장
combo_logic = {k for k in in_combo}
top_inputs = [f for f in real if f["top"]]
tk = Counter(f["kind"] for f in top_inputs)
hidden = [f for f in real if not f["top"] and f["kind"] in ("다른 로직", "상수")]
region_inputs = [f for f in real if f["region"]]
no_unit = sum(1 for f in real if not f["unit"])
no_desc = sum(1 for f in real if not f["desc"])
unused = sum(1 for f in real if f["used"] == 0)
unit_design = Counter(f["unit"] or "(단위 없음)" for f in real if f["kind"] == "설계값")
choice_names = Counter(f["name"] for f in real if f["kind"] == "사용자 선택")
rows_md.append("## 4. 25장 선택 입력(PLAN 25 · 26)과 이어질 자리\n")
rows_md.append(
"25장 카드는 항목(구조물 집계표 열) → 일위대가 조합 → 담은 로직마다 **그 로직 줄의 `입력` 그대로**(`M02_MasterTemplete_Inputs.inputs`)를 보임. "
"하위 로직으로 넘겨지는 입력은 맨 위 로직 입력으로만 나오고, 값은 프로젝트 복사본 `입력값.json`(`{로직키: {입력이름: {값, 확인}}}`)에 「확인」 표시와 함께 받음.\n"
)
rows_md.append(f"- 조합이 직접 담은 로직: {len(combo_logic)}줄(일위대가 조합 {len(combo_rows)}개) · 그 입력 {len(top_inputs)}개 = 25장 카드가 실제로 읽는 자리(설계값 {tk['설계값']} · 사용자 선택 {tk['사용자 선택']} · 상수 {tk['상수']} · 다른 로직 {tk['다른 로직']}).")
rows_md.append(f"- 카드에 안 보이는 입력 {len(hidden)}개 = 조합이 담지 않고 다른 로직만 부르는 로직의 입력 가운데 부르는 쪽이 값을 넘기는 것(상수 {sum(1 for f in hidden if f['kind']=='상수')} · 다른 로직 {sum(1 for f in hidden if f['kind']=='다른 로직')}) — 사용자가 못 고침(맨 위 로직 입력으로 이미 받은 값이 내려감).")
rows_md.append(f"- **프로젝트 공통 후보**(지역 · 자재지역 · 전력 계약종별) 입력 {len(region_inputs)}개(이름 {len({f['name'] for f in region_inputs})}종) — PLAN 25 판단 2 그대로 항목마다 고를 값이 아니라 프로젝트에서 한 번 받고 로직마다 채워 넣을 자리.")
rows_md.append(f"- 「설계값」 {allk['설계값']}개 = 형상 · 물량(면적산출 알약 후보) — 단위 분포: " + " · ".join(f"{u} {c}" for u, c in unit_design.most_common(10)) + " …")
rows_md.append(f"- 「사용자 선택」 {allk['사용자 선택']}개(이름 {len(choice_names)}종) = 25장 「선택」 자리(고르기 목록 · 첫 선택지가 견본값) — 많이 나오는 이름: " + " · ".join(f"{n} {c}" for n, c in choice_names.most_common(8)) + ".")
rows_md.append(f"- 「상수」 {allk['상수']}개 = 선택지 하나 · 부르는 로직이 고정값을 넘김 — 사용자에게 안 물어도 되는 자리(카드에서 숨기거나 「확인함」 으로 채울 후보).")
rows_md.append(f"- 정의 품질: 단위 없음 {no_unit}개({no_unit * 100 // max(1, len(real))}%) · 설명(도움말) 없음 {no_desc}개({no_desc * 100 // max(1, len(real))}%) · 식에서 안 쓰이는 입력 {unused}개 — 카드가 이름 옆에 단위 · 설명을 보이는 자리라 채울수록 받기 쉬움.")
rows_md.append("- 자동값(견본) 규칙: 고르기 = 첫 선택지 · 범위 = 아래 끝 · 찾기 조건 값 = 그 표 가장 낮은 줄 · 나누는 자리 = 1 · 그 밖 0 · 가격(원) = 비움 — 25장은 이 값을 **흐리게 미리 보이고 「확인 안 함」** 표시(저장값 아님).")
per_combo = []
for combo in combo_rows:
total = sum(len(by_key[str(one.get("로직"))].get("입력") or []) for one in combo.get("담은로직") or [] if str(one.get("로직")) in by_key)
per_combo.append(total)
if per_combo:
rows_md.append(f"- 조합 한 개가 담은 로직들의 입력을 다 더하면 평균 {sum(per_combo) / len(per_combo):.1f}개 · 최대 {max(per_combo)}개 · 0개인 조합 {sum(1 for c in per_combo if c == 0)}개 — 25장 카드 한 장에 로직마다 묶어 보이는 분량(같은 이름도 로직마다 따로).")
rows_md.append("- 같은 이름의 단위 표기가 갈림(예: 규격 ㎜ · mm) · 이름만 같고 뜻이 다른 입력(구분 · 규격 · 작업)이 많아 25장은 합치지 않고 로직마다 보임(PLAN 25-1 그대로).")
rows_md.append("- 잇는 순서 제안: ① 입력 이름 빠짐은 0이라 목록은 그대로 읽을 수 있음 — 다만 로직 없는 127절(28-2a)이 채워지면 목록이 늘어남 ② 상수 · 다른 로직은 카드에서 숨김 ③ 사용자 선택 → 카드 「선택」 · 지역류(505)는 프로젝트 공통 한 칸 ④ 설계값 → 시트 알약(면적산출) 또는 카드 값 칸(범위 있으면 범위 검사) ⑤ 단위 · 설명 채우기(단위 없음 2,327 · 설명 없음 3,110)를 25장 카드 앞에 두면 받기 쉬움.")
(OUT / "요약.md").write_text(sections(rows_md), encoding="utf-8")
print(
"표 줄", len(table), "| 로직", stats["logics"], "| 입력", stats["inputs"], "| 빠짐", stats["missing_rows"],
"| 빠짐 로직", len(stats["missing_logics"]), "| 입력없음 로직", stats["no_input_logics"], "| 임도 로직", forest_logics,
)
+66
View File
@@ -0,0 +1,66 @@
# M01 입력 목록 요약(PLAN 28-1)
로직 1354줄 · 입력 3238개(이름 869종) · 입력 정의 빠짐 0건(로직 0줄) · 입력 없는 로직 273줄 · 임도 묶음 로직 430줄.
## 1. 부문별 입력 수 · 빠짐 수
| 부문 | 로직 | 입력 | 설계값 | 사용자 선택 | 상수 | 다른 로직 | 정의 빠짐(건) | 빠짐 있는 로직 |
|---|---|---|---|---|---|---|---|---|
| 산림 | 251 | 581 | 167 | 390 | 4 | 20 | 0 | 0 |
| 공통 | 276 | 691 | 78 | 584 | 6 | 23 | 0 | 0 |
| 토목 | 282 | 695 | 166 | 526 | 3 | 0 | 0 | 0 |
| 건축 | 163 | 245 | 67 | 175 | 3 | 0 | 0 | 0 |
| 기계설비 | 272 | 758 | 313 | 426 | 19 | 0 | 0 | 0 |
| 유지관리 | 110 | 268 | 14 | 254 | 0 | 0 | 0 | 0 |
| **합계** | 1354 | 3238 | 805 | 2355 | 35 | 43 | 0 | 0 |
**임도 묶음**(산림 · 토목 1~3장 · 공통 5~7장): 로직 430줄 · 입력 1112개 (설계값 250 · 사용자 선택 836 · 상수 6 · 다른 로직 20) · 정의 빠짐 0건(로직 0줄).
## 2. 입력 정의 빠짐 — 실측
- **이름 빠짐(식이 쓰는데 입력 · 중간에 없음)**: **0건**(로직 0줄) — 엔진 검사(`check_logic` 「모르는 이름」 · 「{…} 입력·중간 아님」 · 「입력 필요」)와 같은 기준 · 1,354줄 전부 통과.
- **견본 자동값으로 계산**: 1354줄 통과 · 0줄 실패 — 자동값(고르기 첫째 · 범위 아래 끝 · 표 최저 · 0/1)만으로도 모든 로직이 돎 → 정의가 모자라 못 도는 로직은 없음. 시험입력 78줄은 일괄 시험용 견본.
- **로직 자체가 없어 입력을 못 뽑은 절**(종합_흠목록 「로직 없음」): 산림 84 · 공통 3 · 토목 3 · 건축 0 · 기계설비 0 · 유지관리 37 = 합 127절(28-2a) — 이 절들은 입력 정의 자체가 없음.
- **정의 불완전(참고)**: 수 입력(고르기 아님)인데 단위가 없는 것 39개 · 설명(도움말) 없는 것 3110개 — 표 「정의 불완전」 칸에 표시.
- **찾기가 표 조건을 안 준 곳**(참고): 1331곳(로직 256줄) 가운데 그 조건 열에 값이 둘 이상 있는 곳 1058곳(로직 179줄) — 지금 견본으로 계산은 되지만 값을 바꾸면 「줄이 여럿」 이 날 수 있는 자리(입력을 더해야 할 후보 · 28-2d 계산식 어긋남과 겹침).
## 3. 자주 쓰이는 입력 20
| 순위 | 입력 이름 | 로직 수 | 단위(많은 순) | 갈래 |
|---|---|---|---|---|
| 1 | 지역 | 471 | (없음) 471 | 사용자 선택 461 · 다른 로직 10 |
| 2 | 보정작업 | 135 | (없음) 135 | 사용자 선택 130 · 다른 로직 5 |
| 3 | 구분 | 110 | (없음) 110 | 사용자 선택 93 · 상수 17 |
| 4 | 규격 | 100 | (없음) 65 · ㎜ 23 · mm 9 | 사용자 선택 69 · 설계값 27 · 상수 4 |
| 5 | 크레인 | 93 | (없음) 93 | 사용자 선택 93 |
| 6 | 조종원직종 | 52 | (없음) 52 | 사용자 선택 51 · 다른 로직 1 |
| 7 | 관경 | 47 | (없음) 38 · ㎜ 7 · mm 2 | 사용자 선택 45 · 설계값 2 |
| 8 | 작업 | 37 | (없음) 37 | 사용자 선택 37 |
| 9 | 지형 | 36 | (없음) 36 | 사용자 선택 36 |
| 10 | 시공조건 | 35 | (없음) 35 | 사용자 선택 35 |
| 11 | 두께 | 27 | ㎝ 9 · (없음) 8 · ㎜ 7 | 사용자 선택 18 · 설계값 9 |
| 12 | 시공면 | 26 | (없음) 26 | 사용자 선택 26 |
| 13 | 자재지역 | 26 | (없음) 26 | 사용자 선택 25 · 다른 로직 1 |
| 14 | 면적 | 25 | ㎢ 13 · ㎡ 9 · 만㎡ 2 | 설계값 24 · 사용자 선택 1 |
| 15 | 장비일수 | 25 | 일 25 | 설계값 25 |
| 16 | 기술관리 | 21 | 일 21 | 설계값 21 |
| 17 | 시공방법 | 20 | (없음) 20 | 사용자 선택 20 |
| 18 | 고소작업차 | 19 | (없음) 19 | 사용자 선택 19 |
| 19 | Type | 19 | (없음) 19 | 사용자 선택 19 |
| 20 | 기계 | 18 | (없음) 18 | 사용자 선택 9 · 다른 로직 8 · 상수 1 |
## 4. 25장 선택 입력(PLAN 25 · 26)과 이어질 자리
25장 카드는 항목(구조물 집계표 열) → 일위대가 조합 → 담은 로직마다 **그 로직 줄의 `입력` 그대로**(`M02_MasterTemplete_Inputs.inputs`)를 보임. 하위 로직으로 넘겨지는 입력은 맨 위 로직 입력으로만 나오고, 값은 프로젝트 복사본 `입력값.json`(`{로직키: {입력이름: {값, 확인}}}`)에 「확인」 표시와 함께 받음.
- 조합이 직접 담은 로직: 43줄(일위대가 조합 37개) · 그 입력 138개 = 25장 카드가 실제로 읽는 자리(설계값 26 · 사용자 선택 112 · 상수 0 · 다른 로직 0).
- 카드에 안 보이는 입력 78개 = 조합이 담지 않고 다른 로직만 부르는 로직의 입력 가운데 부르는 쪽이 값을 넘기는 것(상수 35 · 다른 로직 43) — 사용자가 못 고침(맨 위 로직 입력으로 이미 받은 값이 내려감).
- **프로젝트 공통 후보**(지역 · 자재지역 · 전력 계약종별) 입력 505개(이름 3종) — PLAN 25 판단 2 그대로 항목마다 고를 값이 아니라 프로젝트에서 한 번 받고 로직마다 채워 넣을 자리.
- 「설계값」 805개 = 형상 · 물량(면적산출 알약 후보) — 단위 분포: % 125 · m 86 · ton 74 · ㎡ 72 · 일 48 · ㎜ 45 · ㎥ 30 · 개 25 · 대 24 · 점 23 …
- 「사용자 선택」 2355개(이름 535종) = 25장 「선택」 자리(고르기 목록 · 첫 선택지가 견본값) — 많이 나오는 이름: 지역 461 · 보정작업 130 · 크레인 93 · 구분 93 · 규격 69 · 조종원직종 51 · 관경 45 · 작업 37.
- 「상수」 35개 = 선택지 하나 · 부르는 로직이 고정값을 넘김 — 사용자에게 안 물어도 되는 자리(카드에서 숨기거나 「확인함」 으로 채울 후보).
- 정의 품질: 단위 없음 2327개(71%) · 설명(도움말) 없음 3110개(96%) · 식에서 안 쓰이는 입력 33개 — 카드가 이름 옆에 단위 · 설명을 보이는 자리라 채울수록 받기 쉬움.
- 자동값(견본) 규칙: 고르기 = 첫 선택지 · 범위 = 아래 끝 · 찾기 조건 값 = 그 표 가장 낮은 줄 · 나누는 자리 = 1 · 그 밖 0 · 가격(원) = 비움 — 25장은 이 값을 **흐리게 미리 보이고 「확인 안 함」** 표시(저장값 아님).
- 조합 한 개가 담은 로직들의 입력을 다 더하면 평균 18.2개 · 최대 65개 · 0개인 조합 0개 — 25장 카드 한 장에 로직마다 묶어 보이는 분량(같은 이름도 로직마다 따로).
- 같은 이름의 단위 표기가 갈림(예: 규격 ㎜ · mm) · 이름만 같고 뜻이 다른 입력(구분 · 규격 · 작업)이 많아 25장은 합치지 않고 로직마다 보임(PLAN 25-1 그대로).
- 잇는 순서 제안: ① 입력 이름 빠짐은 0이라 목록은 그대로 읽을 수 있음 — 다만 로직 없는 127절(28-2a)이 채워지면 목록이 늘어남 ② 상수 · 다른 로직은 카드에서 숨김 ③ 사용자 선택 → 카드 「선택」 · 지역류(505)는 프로젝트 공통 한 칸 ④ 설계값 → 시트 알약(면적산출) 또는 카드 값 칸(범위 있으면 범위 검사) ⑤ 단위 · 설명 채우기(단위 없음 2,327 · 설명 없음 3,110)를 25장 카드 앞에 두면 받기 쉬움.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff