DB_JOB/작업파일/공공기관2/13.한국청소년상담복지개발원/_tab_expand.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

216 lines
10 KiB
Python

# -*- coding: utf-8 -*-
import openpyxl, copy, sys, shutil, os, datetime
from openpyxl.utils import get_column_letter
sys.stdout.reconfigure(encoding='utf-8')
FN = '한국청소년상담복지개발원.xlsx'
BASE = 'https://www.kyci.or.kr'
BLUE = 'FFBDD7EE'
wb = openpyxl.load_workbook(FN)
ws = wb.active
MAXR = ws.max_row
NCOL = 27 # up to AA
# ---- resolve merged effective values ----
merged = list(ws.merged_cells.ranges)
def eff(r, c):
v = ws.cell(r, c).value
if v is not None:
return v
for rng in merged:
if rng.min_row <= r <= rng.max_row and rng.min_col <= c <= rng.max_col:
return ws.cell(rng.min_row, rng.min_col).value
return None
# capture style snapshots for original rows
def snap(r):
return [copy.copy(ws.cell(r, c)._style) for c in range(1, NCOL+1)]
# template data-row style = row 3
TPL = snap(3)
# ---- build flat records for rows 3..MAXR ----
# record = dict: vals[1..27] (1-indexed col -> value), style list, blue set (cols to paint blue), is_new
def base_record(r):
vals = {}
for c in range(1, NCOL+1):
if 4 <= c <= 10: # D..J use effective (merge-resolved)
vals[c] = eff(r, c)
else:
vals[c] = ws.cell(r, c).value
return {'vals': vals, 'style': snap(r), 'blue': set(), 'new': False}
def new_record(path_vals, leafcol, label, url, L, M, N, O, ext=False):
vals = {c: None for c in range(1, NCOL+1)}
vals[3] = '한국청소년상담복지개발원' # C
for c in range(4, 11):
vals[c] = path_vals.get(c) # inherit D..J path
vals[leafcol] = label
vals[11] = url # K
vals[12] = L
vals[13] = M
vals[14] = N
vals[15] = O
rec = {'vals': vals, 'style': [copy.copy(s) for s in TPL], 'blue': set(), 'new': True}
rec['blue'] = set(range(2, 16)) # B..O painted blue for new rows
return rec
# ---- expansion definitions (keyed by original sheet row) ----
def u(p):
if p.startswith('http'): return p
return BASE + p
# children: (label, url, L, M, N, O)
EXP = {
14: ('E', '사업소개', [('사업개요','/userSite/sub02_1_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_1_cont.asp','페이지',1,'어문','미부착')]),
15: ('E', '사업소개', [('사업개요','/userSite/sub02_14_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_14_cont.asp','페이지',1,'어문','미부착')]),
16: ('E', '사업소개', [('사업개요','/userSite/sub02_2_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_2_cont.asp','페이지',1,'어문','미부착'),
('홍보자료','/userSite/sub02_2_gallery.asp','페이지',1,'어문,이미지','미부착')]),
17: ('E', '사업소개', [('사업개요','/userSite/sub02_3_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_3_cont.asp','페이지',1,'어문','미부착'),
('홍보자료','/userSite/sub02_3_gallery.asp','페이지',1,'어문,이미지','미부착'),
('자료실','/userSite/sub02_3_file.asp','페이지',1,'어문','미부착')]),
18: ('E', '사업소개', [('사업개요','/userSite/sub02_4_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_4_cont.asp','페이지',1,'어문','미부착'),
('자료실','/userSite/sub02_4_file.asp','페이지',1,'어문','미부착')]),
19: ('E', '사업소개', [('사업개요','/userSite/sub02_6_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_6_cont.asp','페이지',1,'어문','미부착'),
('심리검사','/userSite/sub02_6_test.asp','페이지',1,'어문','미부착'),
('홍보자료','/userSite/sub02_6_gallery.asp','페이지',1,'어문,이미지','미부착')]),
20: ('E', '사업소개', [('사업개요','/userSite/sub02_7_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_7_cont.asp','페이지',1,'어문','미부착')]),
21: ('E', '사업소개', [('사업개요','/userSite/sub02_8_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_8_cont.asp','페이지',1,'어문','미부착')]),
22: ('E', '사업소개', [('사업개요','/userSite/sub02_10_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_10_cont.asp','페이지',1,'어문,이미지','미부착')]),
23: ('E', '사업소개', [('사업개요','/userSite/sub02_11_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_11_cont.asp','페이지',1,'어문','미부착'),
('권리교육 관련 자료','https://edu.kyci.or.kr/community/edu_files.asp','사이트',None,None,None)]),
24: ('E', '사업소개', [('사업개요','/userSite/sub02_13_info.asp','페이지',1,'어문','미부착'),
('주요내용','/userSite/sub02_13_cont.asp','페이지',1,'어문','미부착'),
('홍보자료','/userSite/sub02_13_gallery.asp','페이지',1,'어문,이미지','미부착')]),
45: ('E', '정보공개 제도란?', [
('제도의 필요성','/userSite/sub06_1_2.asp','페이지',1,'어문','미부착'),
('정보공개제도 도입배경','/userSite/sub06_1_3.asp','페이지',1,'어문','미부착'),
('정보공개 청구권자','/userSite/sub06_1_4.asp','페이지',1,'어문','미부착'),
('공공기관과 청구인의 의무','/userSite/sub06_1_5.asp','페이지',1,'어문','미부착'),
('정보공개 처리절차','/userSite/sub06_1_6.asp','페이지',1,'어문','미부착'),
('정보공개 방법','/userSite/sub06_1_7.asp','페이지',1,'어문','미부착'),
('정보공개 수수료','/userSite/sub06_1_8.asp','페이지',1,'어문','미부착'),
('불복구제 절차','/userSite/sub06_1_9.asp','페이지',1,'어문','미부착'),
('정보공개 대상 정보와 비공개 정보','/userSite/sub06_1_10.asp','페이지',1,'어문','미부착'),
('관련 법령','/boardManagement/Board.asp?bid=bid_35','게시판',3,'어문','미부착'),
('청구 서식','/boardManagement/Board.asp?bid=bid_36','게시판',6,'어문','미부착')]),
52: ('E', '입찰정보', [('계약현황','/boardManagement/board.asp?bid=bid_6&menuCategory=6','게시판',580,'어문','미부착')]),
77: ('F', '상담직', [('행정직','/userSite/sub060304-2.asp','페이지',1,'어문','미부착'),
('운전직','/userSite/sub060304-3.asp','페이지',1,'어문','미부착')]),
}
COLIDX = {'D':4,'E':5,'F':6,'G':7,'H':8,'I':9,'J':10}
# self-row L/M corrections (입찰정보 is a board, existing was 페이지/1)
SELF_FIX = {52: {12:'게시판', 13:175}}
records = []
for r in range(3, MAXR+1):
rec = base_record(r)
if r in EXP:
leafletter, self_label, children = EXP[r]
leafcol = COLIDX[leafletter]
childcol = leafcol + 1
# set self label on the existing row at leaf+1
rec['vals'][childcol] = self_label
rec['blue'].add(childcol)
if r in SELF_FIX:
for c, v in SELF_FIX[r].items():
rec['vals'][c] = v
rec['blue'].add(c)
path = {c: rec['vals'][c] for c in range(4, 11)} # D..J of parent
path[childcol] = None # children carry parent path up to leafcol only
records.append(rec)
for (label, url, L, M, N, O) in children:
pv = {c: path.get(c) for c in range(4, 11)}
records.append(new_record(pv, childcol, label, u(url), L, M, N, O))
else:
records.append(rec)
print('original data rows:', MAXR-2, '-> new total:', len(records))
# ---- write to fresh sheet ----
title = ws.title
# copy header rows 1..2 first
hdr = {}
for r in (1,2):
for c in range(1, NCOL+1):
cell = ws.cell(r, c)
hdr[(r,c)] = (cell.value, copy.copy(cell._style))
hdr_merges = [str(rng) for rng in merged if rng.min_row <= 2]
colwidths = {k: v.width for k, v in ws.column_dimensions.items()}
del wb[title]
ns = wb.create_sheet(title)
wb.move_sheet(title, -(len(wb.sheetnames)-1))
# header
for (r,c),(v,st) in hdr.items():
cc = ns.cell(r, c); cc.value = v; cc._style = st
for m in hdr_merges:
ns.merge_cells(m)
for col, w in colwidths.items():
if w: ns.column_dimensions[col].width = w
# data
from openpyxl.styles import PatternFill, Font
bluefill = PatternFill(fill_type='solid', fgColor=BLUE, bgColor=BLUE)
linkfont = Font(name='맑은 고딕', size=10, color='0000FF', underline='single')
for i, rec in enumerate(records):
R = 3 + i
for c in range(1, NCOL+1):
cell = ns.cell(R, c)
cell._style = rec['style'][c-1]
cell.value = rec['vals'][c]
# B renumber continuous
ns.cell(R, 2).value = i + 1
# K hyperlink
kv = rec['vals'][11]
if kv and isinstance(kv, str) and kv.startswith('http'):
kc = ns.cell(R, 11)
kc.hyperlink = kv
kc.font = linkfont
# blue paint
for c in rec['blue']:
ns.cell(R, c).fill = bluefill
# ---- re-merge D..J by boundary-aware equal runs ----
DATA0 = 3
DATAN = 3 + len(records) - 1
def val(R, c):
return ns.cell(R, c).value
for c in range(4, 11): # D..J
R = DATA0
while R <= DATAN:
v = val(R, c)
if v is None or v == '':
R += 1; continue
r2 = R
while r2+1 <= DATAN and val(r2+1, c) == v and all(val(r2+1, pc) == val(R, pc) for pc in range(4, c)):
r2 += 1
if r2 > R:
for rr in range(R+1, r2+1):
ns.cell(rr, c).value = None
ns.merge_cells(start_row=R, start_column=c, end_row=r2, end_column=c)
R = r2 + 1
# freeze panes
ns.freeze_panes = 'A3'
# ---- save with backup ----
bdir = '_backup'
os.makedirs(bdir, exist_ok=True)
ts = datetime.datetime.fromtimestamp(os.path.getmtime(FN)).strftime('%Y%m%d_%H%M%S')
shutil.copy2(FN, os.path.join(bdir, f'_탭전개전_{ts}.xlsx'))
wb.save(FN)
print('SAVED', FN, 'rows', DATAN)