# -*- coding: utf-8 -*-
"""komipo Phase2~4 (throttled Playwright). L 이미 채워짐. 게시판 M·N·O/P/Q.
출력: L~Q + _nshots(이미지후보 몽타주). N은 후보만 표시→몽타주 후 시각판정 별도 적용."""
import re, json, io, os, time, warnings
import openpyxl, requests
from PIL import Image, ImageDraw, ImageFont
from playwright.sync_api import sync_playwright
warnings.filterwarnings('ignore'); requests.packages.urllib3.disable_warnings()
FN='한국중부발전.xlsx'; OUT='_nshots'; os.makedirs(OUT,exist_ok=True)
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36'
NAME_NOISE=re.compile(r'(^ico[_\-]|icon|logo|btn[_\-]|^bul|bullet|^bg[_\-]|banner|^sns|blank|spacer|loading|arrow|^dot|footer|^foot|^header|symbol|copyright|^qr|no_img|noimage|share|facebook|insta|twitter|naver|kakao|mark_|public|kogl|opentype|opencode)',re.I)
KOGL=re.compile(r'open(?:type|code)0*(\d)\.(?:png|jpe?g|gif)',re.I)
JS=r"""()=>{
  const c=document.querySelector('#contents,.contents,#content,.content,.sub_content,#container,.cont_wrap,.contents_wrap')||document.body;
  const imgs=[];
  for(const i of c.querySelectorAll('img')){imgs.push({src:i.currentSrc||i.src||'',alt:i.alt||'',nw:i.naturalWidth,nh:i.naturalHeight});}
  let vid=0;
  if(c.querySelector('video,source[src*=".mp4"]'))vid++;
  for(const f of c.querySelectorAll('iframe')){if(/youtube|youtu\.be|vimeo/i.test(f.src||''))vid++;}
  // 게시판 총건수/리스트
  let total=null;
  const m=(document.body.innerText||'').match(/(?:총|전체)\s*[:\-]?\s*([\d,]+)\s*건/);
  if(m) total=parseInt(m[1].replace(/,/g,''));
  let rows=document.querySelectorAll('table.board_list tbody tr, .board_list li, table tbody tr, .bbs_list tbody tr').length;
  // KOGL 마크
  let kogl=[];
  for(const i of document.querySelectorAll('img')){const mm=(i.src||'').match(/open(?:type|code)0*(\d)/i); if(mm)kogl.push(parseInt(mm[1]));}
  return {imgs,vid,total,rows,kogl:[...new Set(kogl)]};
}"""
def realimg(items):
    out=[]
    for it in items:
        s=it['src']; 
        if not s: continue
        base=s.split('?')[0].split('/')[-1]
        if NAME_NOISE.search(base): continue
        if it['nw']>=200 and it['nh']>=150 and it['nw']*it['nh']>=40000: out.append(it)
    return out
wb=openpyxl.load_workbook(FN); ws=wb.active
sess=requests.Session(); sess.headers['User-Agent']=UA
targets=[]
for r in range(3,ws.max_row+1):
    L=ws.cell(r,12).value; u=ws.cell(r,11).value
    if L=='사이트' or not(u and str(u).startswith('http')): continue
    targets.append((r,str(u),L))
print('대상',len(targets),flush=True)
plan=[]; dl=[]
with sync_playwright() as p:
    b=p.chromium.launch(); pg=b.new_page(user_agent=UA,viewport={'width':1280,'height':2000})
    for idx,(r,u,L) in enumerate(targets):
        d=None
        for attempt in range(3):
            try:
                pg.goto(u,timeout=25000,wait_until='domcontentloaded'); pg.wait_for_timeout(900)
                d=pg.evaluate(JS); break
            except Exception:
                time.sleep(1.5)
        rec={'row':r,'L':L,'url':u}
        if d:
            ri=realimg(d['imgs']); rec['nimg']=len(ri); rec['vid']=d['vid']; rec['kogl']=d['kogl']
            # M
            if L=='게시판':
                M=d['total'] if d['total'] is not None else (d['rows'] if d['rows'] else 0)
                ws.cell(r,13).value=M; rec['M']=M
            else:
                ws.cell(r,13).value=1
            # O/P/Q
            if d['kogl']:
                ws.cell(r,15).value=','.join(str(x) for x in sorted(set(d['kogl'])))
                ws.cell(r,16).value='게시물' if L=='게시판' else '게시물'
                ws.cell(r,17).value='Y'
            else:
                ws.cell(r,15).value='미부착'
            # N 후보 다운로드
            if ri:
                rec['auto']='candidate'
                for it in ri[:2]:
                    try:
                        rr=sess.get(it['src'],timeout=10,verify=False); im=Image.open(io.BytesIO(rr.content)).convert('RGB'); im.thumbnail((280,280))
                        lp=os.path.join(OUT,f'r{r}_{len(dl)}.jpg'); im.save(lp,quality=78)
                        lab=ws.cell(r,7).value or ws.cell(r,6).value or ws.cell(r,5).value or ''
                        dl.append((r,str(lab)[:14],lp))
                    except Exception: pass
            else:
                rec['auto']='어문' if (L!='게시판' or rec.get('M',1)) else '없음'
            # 임시 N: 후보면 어문,이미지 / 영상 / 빈게시판 없음 — 시각판정 전 자동
            if d['vid'] and not ri: ws.cell(r,14).value='어문,영상'
            elif ri: ws.cell(r,14).value='어문,이미지'
            elif L=='게시판' and rec.get('M',0)==0: ws.cell(r,14).value='없음'
            else: ws.cell(r,14).value='어문'
        else:
            rec['err']=1; ws.cell(r,13).value=ws.cell(r,13).value or (1 if L=='페이지' else 0); ws.cell(r,14).value='어문'; ws.cell(r,15).value='미부착'
        plan.append(rec)
        if (idx+1)%20==0:
            wb.save(FN); print(f'  {idx+1}/{len(targets)} (저장)',flush=True)
        time.sleep(0.4)
    b.close()
wb.save(FN)
# 몽타주
try: font=ImageFont.truetype('malgun.ttf',15)
except: font=ImageFont.load_default()
cols,cw,ch=6,200,230; per=cols*7; mps=[]
for gi in range(0,len(dl),per):
    ch_=dl[gi:gi+per]; rn=(len(ch_)+cols-1)//cols
    cv=Image.new('RGB',(cols*cw,rn*ch),'white'); dr=ImageDraw.Draw(cv)
    for j,(r,lab,lp) in enumerate(ch_):
        try: im=Image.open(lp).convert('RGB'); im.thumbnail((cw-8,ch-28))
        except: continue
        cx,cy=(j%cols)*cw,(j//cols)*ch; cv.paste(im,(cx+4,cy+24)); dr.rectangle([cx,cy,cx+cw-1,cy+ch-1],outline='gray'); dr.text((cx+4,cy+4),f'r{r}',fill='red',font=font)
    mp=os.path.join(OUT,f'montage_{gi//per+1}.png'); cv.save(mp); mps.append(mp)
json.dump(plan,open(os.path.join(OUT,'plan.json'),'w',encoding='utf-8'),ensure_ascii=False,indent=1)
print('완료. 후보',len(dl),'몽타주',len(mps),flush=True)
for mp in mps: print(' MONT:',mp,flush=True)
