# -*- coding: utf-8 -*-
"""김제시 재검토: 게시판↔페이지 재분류 + 페이지 인페이지탭(basic_tab) 부모 병합.
- 판별: 본문에 게시판 리스트 클래스(bbs_list/news_list/photo_list/video_list/magazine_list/gallery_list/board_list) → 게시판, 없으면 페이지.
- 외부 리다이렉트/ERR 행(새창열림 외부시스템)은 손대지 않음.
- basic_tab 탭그룹: 페이지 탭만 부모(페이지)로 접고 M=페이지탭수. 게시판 탭은 별도 행 유지.
사용: python -X utf8 _김제_재분류.py dry | run
"""
import os, sys, re, io, hashlib, shutil, importlib.util, warnings, pickle
warnings.filterwarnings('ignore')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
import openpyxl
HERE=os.path.dirname(os.path.abspath(__file__))
spec=importlib.util.spec_from_file_location('dd', os.path.join(HERE,'_스크립트','_dedup_all.py'))
dd=importlib.util.module_from_spec(spec); spec.loader.exec_module(dd)
XP=r"작업파일\광역_사이트맵\전북특별자치도\3.김제시\전북특별자치도_김제시.xlsx"
CACHE=r"_cache_gimje"
LIST=re.compile(r'class="[^"]*(bbs_list|news_list|photo_list|video_list|magazine_list|gallery_list|board_list)[^"]*"')
TAB_DIV=re.compile(r'<div class="basic_tab[^"]*">(.*?)</div>', re.S)
EXTERNAL={69,249,440,441,148,195,277,318,390,400,134,368}  # 외부 리다이렉트/ERR/외부새창 (현행 유지)
TYPE_ORDER=['어문','이미지','영상','음악','소프트웨어','데이터','3D','기타']
def union_types(vals_list):
    seen=[]
    for s in vals_list:
        if not s: continue
        for t in str(s).split(','):
            t=t.strip()
            if t and t not in seen: seen.append(t)
    pri=[t for t in TYPE_ORDER if t in seen]+[t for t in seen if t not in TYPE_ORDER]
    return ','.join(pri) if pri else None

def mc_of(K):
    m=re.search(r'menuCd=(DOM_\w+)', str(K)); return m.group(1) if m else None
def cpath(url):
    return os.path.join(CACHE, hashlib.md5(str(url).encode()).hexdigest()+".html")
def html_of(K):
    p=cpath(K); return open(p,encoding='utf-8').read() if os.path.exists(p) else None
def tail(mc):
    m=re.search(r'(\d{18})$', mc); return m.group(1) if m else None
def parent_mc(mc):
    t=tail(mc)
    if not t: return None
    g=[t[i:i+3] for i in range(0,18,3)]
    for i in range(5,-1,-1):
        if g[i]!='000':
            g2=g[:]; g2[i]='000'; return mc[:mc.rindex(t)]+''.join(g2)
    return None

def main():
    mode=sys.argv[1] if len(sys.argv)>1 else 'dry'
    wb=openpyxl.load_workbook(XP); ws=wb.active
    rows=dd.load_flat(ws)   # src 기준
    by_src={r['src']:r for r in rows}
    # menuCd 매핑 (src 기준)
    src_mc={}; mc_src={}
    for r in rows:
        mc=mc_of(r['vals'].get(11))
        if mc: src_mc[r['src']]=mc; mc_src.setdefault(mc,r['src'])
    # 판별
    det={}
    for r in rows:
        s=r['src']; K=r['vals'].get(11)
        if s in EXTERNAL or s not in src_mc:
            det[s]=None; continue
        h=html_of(K)
        det[s]='게시판' if (h and LIST.search(h)) else '페이지'
    # 탭그룹 발굴 → parent별 distinct 탭 menuCd
    plan={}
    for r in rows:
        mc=src_mc.get(r['src']);
        if not mc: continue
        h=html_of(r['vals'].get(11))
        if not h: continue
        m=TAB_DIV.search(h)
        if not m: continue
        tabs=re.findall(r'menuCd=(DOM_\w+)', m.group(1))
        seen=set(); tabs=[t for t in tabs if not (t in seen or seen.add(t))]
        if len(tabs)<2: continue
        par=parent_mc(tabs[0])
        plan.setdefault(par,set()).update(tabs)
    # 병합 액션 산출
    fold_src=set()       # 흡수(삭제)될 src
    survivor_M={}        # src -> M(페이지탭수)
    survivor_N={}        # src -> 저작물유형 합집합
    survivor_clear_leaf=set()  # 라벨 leaf 컬럼 비울 survivor
    actions=[]
    warn_data=[]; warn_new=[]
    for par, tabset in plan.items():
        tab_srcs=[mc_src[t] for t in tabset if t in mc_src]
        if len(tab_srcs)<2: continue
        # 폴드 가능한 페이지 탭 = det 페이지 & 외부아님
        page_tabs=[s for s in tab_srcs if det.get(s)=='페이지' and s not in EXTERNAL]
        board_tabs=[s for s in tab_srcs if not (det.get(s)=='페이지' and s not in EXTERNAL)]
        if len(page_tabs)<1:
            continue
        par_src=mc_src.get(par)
        if par_src and det.get(par_src)=='페이지' and par_src not in tab_srcs:
            survivor=par_src; members=page_tabs
        else:
            survivor=min(page_tabs); members=[s for s in page_tabs if s!=survivor]
            survivor_clear_leaf.add(survivor)
        if len(members)<1:
            continue
        for s in members: fold_src.add(s)
        survivor_M[survivor]=len(page_tabs)
        survivor_N[survivor]=union_types([by_src[survivor]['vals'].get(14)]+[by_src[s]['vals'].get(14) for s in members])
        actions.append((survivor, members, board_tabs, len(page_tabs)))
        # 경고: 흡수행에 KOGL/저작물 데이터(N=14,O=15,..S=19) 있나
        for s in members:
            v=by_src[s]['vals']
            if any(v.get(c) not in (None,'') for c in (15,16,17,18)):  # O~R 공공누리
                warn_data.append((s, v.get(7) or v.get(6), {c:v.get(c) for c in (14,15,19)}))
            lab=' > '.join(str(v.get(c)) for c in range(4,11) if v.get(c))
            if '새창열림' in lab:
                warn_new.append((s,lab))
    # 새 행 구성
    out=[]
    reclass={'게시판→페이지':0,'페이지→게시판(미적용/외부)':0,'유지':0}
    for r in rows:
        s=r['src']
        if s in fold_src:
            continue
        v=r['vals']
        oldL=v.get(12)
        d=det.get(s)
        # 재분류 (외부/비menuCd 제외)
        if d in ('게시판','페이지'):
            if oldL!=d and not (oldL=='페이지' and d=='게시판'):
                # 페이지→게시판은 외부 제외했으므로 여기 오면 진짜 게시판
                pass
            newL=d
            if oldL=='페이지' and d=='게시판' and s in EXTERNAL:
                newL=oldL
            # 통계
            if oldL=='게시판' and newL=='페이지': reclass['게시판→페이지']+=1
            v[12]=newL
            # M 설정
            if newL=='페이지':
                v[13]=survivor_M.get(s, 1)
            # 게시판이면 기존 M 유지
        # survivor: 저작물유형 합집합 갱신
        if s in survivor_N and survivor_N[s]:
            v[14]=survivor_N[s]
        # survivor 라벨 정리(탭이 survivor가 된 경우 leaf 비움)
        if s in survivor_clear_leaf:
            lc=dd.leaf_depth(v)
            v[lc]=None
        out.append(r)
    print(f"=== 김제시 재분류 dry ===")
    print(f"현재행 {len(rows)} → 병합후 {len(out)}  (흡수 {len(fold_src)}행)")
    print(f"게시판→페이지 재분류: {reclass['게시판→페이지']}건")
    print(f"병합 그룹 수: {len(actions)}")
    print(f"\n[경고] 흡수행 중 공공누리(O~R) 데이터 보유: {len(warn_data)}건")
    for s,nm,d2 in warn_data: print(f"   r{s} {nm}: {d2}")
    print(f"\n[확인] 흡수 대상 '새창열림' 페이지: {len(warn_new)}건")
    for s,lab in warn_new[:20]: print(f"   r{s}: {lab}")
    # 병합 그룹 요약
    print("\n=== 병합 그룹 (survivor M=페이지탭수, 게시판탭은 유지) ===")
    def lab(s):
        v=by_src[s]['vals']; return ' > '.join(str(v.get(c)) for c in range(4,11) if v.get(c))
    for survivor,members,boards,m in sorted(actions):
        print(f" survivor r{survivor} M={m} [{lab(survivor)}]  흡수 {len(members)}행" + (f", 게시판유지 {len(boards)}행" if boards else ""))
    if mode=='run':
        bak=XP.replace('.xlsx','_backup_재분류전.xlsx')
        shutil.copy(XP,bak)
        dd.write_back(ws,out)
        try: wb.save(XP); print(f"\n저장완료. 백업:{os.path.basename(bak)}")
        except PermissionError: wb.save(XP.replace('.xlsx','_LP.xlsx')); print("\n!! 잠김→_LP")

if __name__=='__main__':
    main()
