DB_JOB/작업파일/_스크립트/_remerge.py
hehihoho3 b0acd1e705 백업: 셀병합 정상화(D~J)+KDHC 옵션2/행높이17, 오늘(06-21) 제출 17곳 검수·압축
- 공공기관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>
2026-06-21 13:21:26 +09:00

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))