- 공공기관2/3 작업본 + 오늘 제출 17곳 D~J 카테고리 셀병합 정상화 - 한국지역난방공사 옵션2(고아셀 F98 수정)+전행 높이17 - 제출_프리랜서2_2026-06-21.zip 생성(17개 xlsx, 2,468행) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
6.3 KiB
Python
141 lines
6.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""D~J 카테고리 셀병합 정상화(off-by-one/누락/고아셀) + 선택적 행높이17.
|
|
값/스타일/K하이퍼링크/행수/B/L/M/N 보존. 자체검증 실패시 롤백.
|
|
사용: python _remerge.py <xlsx> [--apply] [--height] """
|
|
import openpyxl, os, shutil, sys, io, json
|
|
HDR=3
|
|
def load(path):
|
|
return openpyxl.load_workbook(path)
|
|
def process(path, apply=False, height=False):
|
|
rep={"file":path,"ok":False}
|
|
wb=load(path); ws=wb.active; mx=ws.max_row
|
|
if mx<HDR:
|
|
rep["skip"]="empty"; return rep
|
|
# effective from existing merges (C..J)
|
|
eff={}
|
|
for r in range(HDR,mx+1):
|
|
for c in range(3,11): eff[(r,c)]=ws.cell(r,c).value
|
|
cat_merges=[]
|
|
for mr in list(ws.merged_cells.ranges):
|
|
if mr.min_row>=HDR and mr.min_col>=3 and mr.max_col<=10 and mr.min_col==mr.max_col:
|
|
v=ws.cell(mr.min_row,mr.min_col).value
|
|
for r in range(mr.min_row,mr.max_row+1): eff[(r,mr.min_col)]=v
|
|
if mr.min_col>=4: cat_merges.append(str(mr))
|
|
elif mr.min_row>=HDR and mr.min_col>=4 and mr.max_col<=10:
|
|
# multi-col merge in cat area (unusual) -> fill & remove
|
|
v=ws.cell(mr.min_row,mr.min_col).value
|
|
for r in range(mr.min_row,mr.max_row+1):
|
|
for c in range(mr.min_col,mr.max_col+1): eff[(r,c)]=v
|
|
cat_merges.append(str(mr))
|
|
def E(r,c):
|
|
v=eff.get((r,c)); return str(v).strip() if v not in(None,"") else ""
|
|
def path_of(r): return tuple(E(r,c) for c in range(4,11))
|
|
orig_path={r:path_of(r) for r in range(HDR,mx+1)}
|
|
orig_K=[(r,ws.cell(r,11).value) for r in range(HDR,mx+1)]
|
|
orig_LMN=[(r,ws.cell(r,12).value,ws.cell(r,13).value,ws.cell(r,14).value) for r in range(HDR,mx+1)]
|
|
orig_B=[(r,ws.cell(r,2).value) for r in range(HDR,mx+1)]
|
|
# orphan fill: blank cat cell with deeper content -> inherit from row above
|
|
orphans=[]
|
|
for r in range(HDR+1,mx+1):
|
|
for c in range(4,11):
|
|
if E(r,c)=="" and any(E(r,cc)!="" for cc in range(c+1,11)) and E(r-1,c)!="":
|
|
eff[(r,c)]=eff[(r-1,c)]; orphans.append((r,c))
|
|
def parent(r,c): return tuple(E(r,cc) for cc in range(3,c)) # C..c-1
|
|
target=[]
|
|
for c in range(4,11):
|
|
r=HDR
|
|
while r<=mx:
|
|
v=E(r,c)
|
|
if v=="": r+=1; continue
|
|
r2=r
|
|
while r2+1<=mx and E(r2+1,c)==v and parent(r2+1,c)==parent(r,c): r2+=1
|
|
if r2>r: target.append((c,r,r2))
|
|
r=r2+1
|
|
existing=set()
|
|
for s in cat_merges:
|
|
# parse "X1:Y2"
|
|
from openpyxl.utils.cell import range_boundaries
|
|
c1,r1,c2,r2=range_boundaries(s)
|
|
if c1==c2: existing.add((c1,r1,r2))
|
|
tgt=set(target)
|
|
rep["orphans"]=[(r,openpyxl.utils.get_column_letter(c)) for (r,c) in orphans]
|
|
rep["add"]=sorted([(openpyxl.utils.get_column_letter(c),r1,r2) for (c,r1,r2) in (tgt-existing)])
|
|
rep["removed"]=sorted([(openpyxl.utils.get_column_letter(c),r1,r2) for (c,r1,r2) in (existing-tgt)])
|
|
rep["n_target"]=len(tgt); rep["n_existing"]=len(existing); rep["n_change"]=len(tgt-existing)
|
|
if not apply:
|
|
rep["ok"]=True; rep["dry"]=True; return rep
|
|
if not (tgt-existing) and not orphans and not height:
|
|
rep["ok"]=True; rep["nochange"]=True; return rep
|
|
# ---- APPLY ----
|
|
bdir=os.path.join(os.path.dirname(path),"_backup")
|
|
os.makedirs(bdir,exist_ok=True)
|
|
bpath=os.path.join(bdir, os.path.splitext(os.path.basename(path))[0]+"_backup_셀병합전.xlsx")
|
|
shutil.copy2(path,bpath); rep["backup"]=bpath
|
|
for s in cat_merges:
|
|
try: ws.unmerge_cells(s)
|
|
except Exception: pass
|
|
# write eff values to all cat cells
|
|
for r in range(HDR,mx+1):
|
|
for c in range(4,11):
|
|
v=eff.get((r,c))
|
|
ws.cell(r,c).value=(v if v not in(None,"") else None)
|
|
# blank non-top cells of runs
|
|
for (c,r1,r2) in target:
|
|
for r in range(r1+1,r2+1): ws.cell(r,c).value=None
|
|
# re-merge
|
|
for (c,r1,r2) in target:
|
|
ws.merge_cells(start_row=r1,start_column=c,end_row=r2,end_column=c)
|
|
if height:
|
|
ws.sheet_format.defaultRowHeight=17; ws.sheet_format.customHeight=True
|
|
for r in range(1,mx+1): ws.row_dimensions[r].height=17
|
|
wb.save(path)
|
|
# ---- VERIFY (reload) ----
|
|
wb2=load(path); ws2=wb2.active
|
|
e2={}
|
|
for r in range(HDR,ws2.max_row+1):
|
|
for c in range(3,11): e2[(r,c)]=ws2.cell(r,c).value
|
|
for mr in ws2.merged_cells.ranges:
|
|
if mr.min_row>=HDR and mr.min_col>=3 and mr.max_col<=10:
|
|
v=ws2.cell(mr.min_row,mr.min_col).value
|
|
for r in range(mr.min_row,mr.max_row+1):
|
|
for c in range(mr.min_col,mr.max_col+1): e2[(r,c)]=v
|
|
def E2(r,c):
|
|
v=e2.get((r,c)); return str(v).strip() if v not in(None,"") else ""
|
|
fails=[]
|
|
if ws2.max_row!=mx: fails.append("rowcount %d!=%d"%(ws2.max_row,mx))
|
|
orphan_set=set(orphans)
|
|
for r in range(HDR,mx+1):
|
|
npath=tuple(E2(r,c) for c in range(4,11))
|
|
if npath!=orig_path[r]:
|
|
# allowed only if diff positions are orphan-filled
|
|
diffcols=[c for i,c in enumerate(range(4,11)) if npath[i]!=orig_path[r][i]]
|
|
if not all((r,c) in orphan_set for c in diffcols):
|
|
fails.append("path r%d %s->%s"%(r,orig_path[r],npath))
|
|
for (r,k) in orig_K:
|
|
if ws2.cell(r,11).value!=k: fails.append("K r%d"%r)
|
|
for (r,b) in orig_B:
|
|
if ws2.cell(r,2).value!=b: fails.append("B r%d"%r)
|
|
for (r,l,m,n) in orig_LMN:
|
|
if (ws2.cell(r,12).value,ws2.cell(r,13).value,ws2.cell(r,14).value)!=(l,m,n): fails.append("LMN r%d"%r)
|
|
# merge overlap / boundary check
|
|
ranges=[(mr.min_col,mr.min_row,mr.max_row) for mr in ws2.merged_cells.ranges if mr.min_col==mr.max_col and 4<=mr.min_col<=10]
|
|
seen={}
|
|
for (c,r1,r2) in ranges:
|
|
for r in range(r1,r2+1):
|
|
if (c,r) in seen: fails.append("overlap c%d r%d"%(c,r))
|
|
seen[(c,r)]=1
|
|
rep["fails"]=fails[:20]
|
|
if fails:
|
|
shutil.copy2(bpath,path) # rollback
|
|
rep["rolledback"]=True; rep["ok"]=False
|
|
else:
|
|
rep["ok"]=True; rep["n_merges_after"]=len(ranges)
|
|
return rep
|
|
|
|
if __name__=="__main__":
|
|
args=sys.argv[1:]
|
|
path=args[0]
|
|
apply="--apply" in args; height="--height" in args
|
|
r=process(path,apply=apply,height=height)
|
|
print(json.dumps(r,ensure_ascii=False,indent=1))
|