Files
Aislo/resources/master_data/scripts/_토목_표.py
T
eomsangdonandClaude Sonnet 5 a09fbdfd78 feat(master_data): 건설 토목 1장 2장 6장 소요량 옮김
- 소요량_건설품셈_토목1장(116표) · 토목2장(9표) · 토목6장(73표) — md 표 73 · 8 · 50 개 모두 + 글 속 계수 표
- 변환 스크립트 build_소요량_건설품셈_토목.py + _토목_읽기 · _토목_표 · _토목_손 · _토목_계수
- check_master 틀 · 본문 · 로직 0건

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
2026-09-19 17:16:42 +09:00

323 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""토목 표 만들기 — 편성표(crew) · 납작표(flat) · 요율 · 매트릭스. (build_소요량_건설품셈_토목.py 가 씀)"""
import re
from _md표_읽기 import N, R, Raw, join
from _토목_읽기 import NUM, ROLE, UNITS, cell_val, fill_right, is_num, kind_of, nz, unit_split
def head_paths(rows, h, start, ncol):
"""머리 h 줄 → 열별 층 목록. 빈 칸은 같은 맨 윗 층 무리 안에서만 왼쪽 값으로 채움."""
top, paths = fill_right(rows[0][start:], 0), {c: [] for c in range(start, ncol)}
for r in range(h):
row = list(rows[r][start:])
for i in range(1, len(row)):
if not row[i] and r > 0 and top[i] == top[i - 1]:
row[i] = row[i - 1]
elif not row[i] and r == 0:
row[i] = row[i - 1]
for i, c in enumerate(range(start, ncol)):
v = jn(row[i]) if row[i] else ""
if v and (not paths[c] or paths[c][-1] != v):
paths[c].append(v)
return paths
def is_text(v):
return isinstance(v, str) and not isinstance(v, Raw)
def jn(cell):
"""칸 글 — 「∼」 둘레 줄바꿈은 붙임."""
if "" in cell:
return re.sub(r"\s*<br>\s*", "", cell).replace(" ", "").replace(" ", "")
return join(cell)
def is_unit(p):
return p in UNITS or bool(re.fullmatch(r"[㎡㎥㎜㎝㎏a-zA-Z/개소본일회]+", p))
def conds_out(names, rows):
"""조건 이름별 종류를 정해 줄의 글을 수로 바꿈 → 조건 사전."""
cond = {}
for nm in names:
vals = [r[nm] for r in rows if nm in r]
kd = kind_of(vals)
cond[nm] = kd
if kd == "수":
for r in rows:
if nm in r:
r[nm] = N(r[nm])
return cond
def bigo_items(text):
items = []
for p in (x.strip() for x in text.split("<br>")):
if p.startswith("- ") or not items:
items.append(p)
else:
items[-1] += " " + p
return ["비고 " + x for x in items]
def split_name(nm, spec):
"""이름 끝 괄호에 수가 들었으면(「…(폭 2.4m)」) 이름에서 떼어 규격 글로."""
m = re.search(r"\(([^()]*\d[^()]*)\)$", nm)
if m and " " not in nm[: m.start()].strip():
return nm[: m.start()].strip(), (m.group(1) + (" " + spec if spec else ""))
return nm, spec
# ── 편성표 ─────────────────────────────────────────────────────
def crew(t, cn=(), gname=None, prefix_group=False):
R0 = t.rows
head0 = R0[0]
uc = next(i for i, x in enumerate(head0) if join(x) == "단위")
sc = next((i for i, x in enumerate(head0) if join(x).startswith("규격")), None)
ncol = len(head0)
h = next(i for i in range(1, len(R0)) if R0[i][uc] != "")
namecols = [c for c in range(uc) if c != sc]
paths = head_paths(R0, h, uc + 1, ncol)
role, sig, cname_of = {}, {}, {}
for c, pth in paths.items():
ri = next((i for i, x in enumerate(pth) if ROLE.match(x)), None)
if ri is None: # 역할 글 없음(머리 = 조건) — 값은 모두 수량
role[c] = ("수량", "")
sig[c] = tuple(pth[1:] if len(pth) > 1 else pth)
cname_of[c] = (
[
re.sub(r"\(.*\)$", "", pth[0]).strip()
+ (
re.search(r"\(.*\)$", pth[0]).group(0)
if re.search(r"\(.*\)$", pth[0])
else ""
)
]
if len(pth) > 1
else []
)
continue
m = ROLE.match(pth[ri])
par = re.search(r"\((.*)\)", pth[ri])
p = par.group(1) if par else ""
role[c] = (m.group(1), p)
rest = [x for i, x in enumerate(pth) if i != ri]
sig[c] = tuple(rest)
cname_of[c] = [p] if (p and not is_unit(p) and rest) else []
prod = [c for c in role if role[c][0] != "수량"]
nc = namecols[-1]
for r in range(h, len(R0)): # 이름 칸이 비면(같은 이름 잇따름) 위 이름
if not R0[r][nc] and R0[r][uc] and R0[r - 1][nc]:
R0[r][nc] = R0[r - 1][nc]
# 블록
blocks, bigo, grp = [], [], ""
for r in range(h, len(R0)):
row = R0[r]
if join(row[0]) == "비고":
bigo += bigo_items(row[1])
continue
if len(namecols) > 1 and row[namecols[0]]:
grp = join(row[namecols[0]])
if (prod and any(row[c] for c in prod)) or not blocks:
blocks.append({"grp": grp, "rows": []})
blocks[-1]["rows"].append((grp, row))
cols, rows, condnames = {}, [], []
gcol = gname or (join(head0[namecols[0]]) if len(namecols) > 1 else "")
sigs = []
for c in sorted(role):
if sig[c] not in sigs:
sigs.append(sig[c])
nonempty = [s for s in sigs if s]
outsigs = nonempty if nonempty else [()]
for blk in blocks:
first = blk["rows"][0][1]
for sg in outsigs:
row = {}
if gcol and prod:
row[gcol] = blk["grp"]
names = list(cn) or next(
(cname_of[c] for c in role if sig[c] == sg and cname_of[c]), []
)
for lv, val in enumerate(sg):
nm = names[lv] if lv < len(names) else f"조건{lv + 1}"
if nm == "-":
continue
row[nm] = val[len(nm) + 1 :] if val.startswith(nm + " ") else val
vcols = [c for c in role if sig[c] in (sg, ())]
have_prod = False
for c in vcols:
if role[c][0] != "수량" and nz(first[c]):
nm = role[c][0]
pu = role[c][1] if is_unit(role[c][1]) else ""
cols[nm] = pu
row[nm] = N(first[c])
have_prod = True
if prod and not have_prod:
continue
for g, mrow in blk["rows"]:
nm = jn(mrow[namecols[-1]])
if prefix_group and g:
nm = f"{g} {nm}"
unit = mrow[uc]
for c in vcols:
if role[c][0] == "수량" and nz(mrow[c]):
assert is_num(mrow[c]), (t.ident, mrow[c])
spec = jn(mrow[sc]) if sc is not None and nz(mrow[sc]) else ""
nm, spec = split_name(nm, spec)
if nm in row: # 같은 이름 둘(규격만 다름) — 이름 뒤에 규격을 붙여 가름
nm = f"{nm} {spec}"
cols.setdefault(nm, unit)
row[nm] = N(mrow[c])
if spec and " " + spec not in nm:
cols[nm + "규격"] = "글"
row[nm + "규격"] = spec
rows.append(row)
cnames = []
for r in rows:
for k in r:
if k not in cnames and k not in cols:
cnames.append(k)
cond = conds_out(cnames, rows)
return cond, cols, rows, bigo
# ── 납작표 ─────────────────────────────────────────────────────
def flat(t, lead=1, levels=("V",), hdr=None, vname=None, halves=False, unit=None):
R0 = t.rows
bigo = []
body_rows = []
for row in R0:
if join(row[0]) == "비고":
bigo += bigo_items(row[1])
R0 = [r for r in R0 if join(r[0]) != "비고"]
if halves:
w = len(R0[0]) // 2
parts = [[r[:w] for r in R0], [r[w:] for r in R0]]
else:
parts = [R0]
cols, rows, condnames = {}, [], []
for P in parts:
h = hdr
if h is None:
h = next(i for i, row in enumerate(P) if any(is_num(c) for c in row[lead:]))
ncol = len(P[0])
lead_names = []
for c in range(lead):
hs = [jn(P[r][c]) for r in range(h) if P[r][c]]
lead_names.append(re.sub(r"\s+\(", "(", hs[-1]) if hs else f"조건{c + 1}")
paths = head_paths(P, h, lead, ncol)
info = {}
for c, pth in paths.items():
cn_, vn_ = {}, []
for i, el in enumerate(pth):
lv = levels[i] if i < len(levels) else "V"
if lv.startswith("C:"):
cn_[lv[2:]] = el
elif lv == "V":
vn_.append(el)
info[c] = (tuple(cn_.items()), " ".join(vn_))
prev = [""] * lead
for row in P[h:]:
if not any(row):
continue
lc = {}
for c in range(lead):
if row[c]:
prev[c] = jn(row[c])
for c2 in range(c + 1, lead):
prev[c2] = ""
lc[lead_names[c]] = prev[c]
sigs = []
for c in range(lead, ncol):
if info[c][0] not in sigs:
sigs.append(info[c][0])
for sg in sigs:
out = dict(lc)
for k, v in sg:
out[k] = v
got = False
for c in range(lead, ncol):
if info[c][0] != sg or not nz(row[c]):
continue
name, un = unit_split(info[c][1]) if info[c][1] else (vname, "")
if info[c][1] == "" or vname:
name = vname or name
un = unit or t.base
v, orig = cell_val(row[c])
cols[name] = "글" if is_text(v) or cols.get(name) == "글" else un
out[name] = v
if orig:
cols[name + "원문"] = "글"
out[name + "원문"] = orig
got = True
if got:
rows.append(out)
names = []
for r in rows:
for k in r:
if k not in names and k not in cols:
names.append(k)
cond = conds_out(names, rows)
return cond, cols, rows, bigo
def rate(t, cname=None, vname=None):
"""머리 = 조건, 둘째 줄 = 요율(%)."""
h, b = t.rows[0], t.rows[1]
cname = cname or jn(h[0])
vn = vname or re.sub(r"\(%\)$", "", jn(b[0]))
rows = [{cname: jn(h[c]), vn: R(b[c].rstrip("%"))} for c in range(1, len(h)) if nz(b[c])]
return {cname: "고르기"}, {vn: "%"}, rows, []
def matrix(t, rname, cname, vname, unit, hdr):
"""줄 머리 · 칸 머리 → 값 하나."""
P = t.rows
colh = [jn(x) for x in P[0]]
rows = []
for row in P[hdr:]:
for c in range(1, len(row)):
if nz(row[c]):
rows.append({rname: jn(row[0]), cname: colh[c], vname: N(row[c])})
cond = conds_out([rname, cname], rows)
return cond, {vname: unit}, rows, []
def tpose(t, cname, vname):
"""머리 = 조건, 둘째 줄 = 값(수 하나가 든 글은 수 + 원문)."""
h, b = t.rows[0], t.rows[1]
rows, cols = [], {}
for c in range(1, len(h)):
if not nz(b[c]):
continue
v, orig = cell_val(b[c])
d = {cname: jn(h[c]), vname: v}
cols[vname] = "글" if is_text(v) else ""
if orig:
d[vname + "원문"] = orig
cols[vname + "원문"] = "글"
rows.append(d)
return {cname: kind_of([r[cname] for r in rows])}, cols, rows, []
def members(rows, uc, sc, qc, nc=0):
"""몸 줄들 → (값칸, 값) — 수량 칸이 「-」 · 빈칸이면 뺌."""
cols, vals = {}, {}
for row in rows:
if not nz(row[qc]):
continue
nm = jn(row[nc])
spec = jn(row[sc]) if sc is not None and nz(row[sc]) else ""
nm, spec = split_name(nm, spec)
if nm in vals:
nm = f"{nm} {spec}"
cols.setdefault(nm, row[uc])
vals[nm] = N(row[qc])
if spec and " " + spec not in nm:
cols[nm + "규격"] = "글"
vals[nm + "규격"] = spec
return cols, vals