# 앱 보고서 데이터 + 원본 서식(hwpx) → 완성된 한글 파일(.hwpx)
#   사용: python _build/make_hwpx.py <앱데이터(.json|.html)> <서식.hwpx> <출력.hwpx>
#
# 원칙: 원본 XML 을 파서로 다시 쓰지 않는다(재직렬화하면 서식이 틀어짐).
#       필요한 부분만 문자열로 도려내고, 문단·표·셀은 서식 원본의 것을 그대로 복제해
#       내용만 바꾼다 → paraPr/charPr/borderFill 등 서식 참조가 그대로 따라온다.
import zipfile, io, sys, re, json, base64, html as H, os
from PIL import Image

SRC, TPL, OUT = sys.argv[1], sys.argv[2], sys.argv[3]
PHOTO_MAX, PHOTO_Q = 1200, 82        # 사진 축소(원본 그대로면 1GB 폭증 — 스펙 photoBudget)
BMP_MIN = 400 * 1024                 # 서식에 박힌 대용량 무압축 BMP 재압축 기준

esc = lambda t: H.escape(t or '')

# ── 1) 앱 데이터 ──
doc = io.open(SRC, encoding='utf-8').read()
if SRC.lower().endswith('.json'):
    data = json.loads(doc)
else:
    m = re.search(r'<script id="ELEV_DATA"[^>]*>(.*?)</script>', doc, re.S)
    if not m: sys.exit('ELEV_DATA 없음 — 앱에서 만든 파일이 아닙니다.')
    data = json.loads(m.group(1).replace('\\u003c', '<'))
site   = data.get('site') or ''
units  = int(data.get('units') or 1)
photos = data.get('photos') or []
FIELDS = data.get('fields') or {}
print(f'현장="{site}" 호기={units} 사진={len(photos)}장 입력칸={len(FIELDS)}')

# ── 2) 사진 페이지 구성 (page = area × 호기군, block = 세부부위) ──
AREA_OF = {'machineroom':'기계실','traction':'기계실','controlpanel':'기계실',
           'shaft':'승강로','rope':'승강로','railL':'승강로','railR':'승강로',
           'carIn':'카','carTop':'카','carDoor':'카','pit':'피트','buffer':'피트'}
def area_of(p):
    a = AREA_OF.get(p.get('part'))
    if a: return a
    return '출입구' if str(p.get('part','')).startswith(('landing','jamb')) else '기타'
AREA_ORDER = ['기계실','승강로','카','출입구','승장','피트']

pages = []
for area in AREA_ORDER:
    inA = [p for p in photos if area_of(p) == area]
    if not inA: continue
    # 모든 부위 호기별로 분리 (기계실·피트 포함). 4대면 4장, 병합은 PC 한글에서.
    groups = [{'label': f'{u+1}호기', 'rows': [p for p in inA if p.get('unit') == u]} for u in range(units)]
    for g in groups:
        if not g['rows']: continue
        g['rows'].sort(key=lambda r: (r.get('detail') or '', r.get('ts') or ''))
        blocks = []
        for r in g['rows']:
            c = r.get('detail') or area
            if blocks and blocks[-1]['comp'] == c: blocks[-1]['photos'].append(r)
            else: blocks.append({'comp': c, 'photos': [r]})
        for i in range(0, len(blocks), 3):
            pages.append({'area': area, 'unit': g['label'], 'blocks': blocks[i:i+3]})
print('사진 페이지', len(pages), '쪽:', [f"{p['area']}({p['unit']})" for p in pages])

zin = zipfile.ZipFile(TPL)
S0  = zin.read('Contents/section0.xml').decode('utf-8')

# ── 3) 서식 원본에서 '표제 문단'·'사진표'·'셀'을 프로토타입으로 추출 ──
def para_bounds(xml, pos):
    """pos 를 포함하는 최상위 문단의 (시작, 끝). '<hp:p' 는 <hp:pic> 등에도 걸리므로 경계를 엄격히."""
    starts = [m.start() for m in re.finditer(r'<hp:p[ >]', xml)]
    i = max(s for s in starts if s <= pos)
    depth = 0
    for m in re.finditer(r'<hp:p[ >]|</hp:p>', xml[i:]):
        if m.group(0).startswith('</'):
            depth -= 1
            if depth == 0: return i, i + m.end()
        else: depth += 1
    return i, len(xml)

# PRE 서식만 '3-N)' 견본 사진쪽 원형을 가짐. SUP 등은 없음 → 조건부.
HAS_PHOTO_PROTO = '3-N)' in S0
if HAS_PHOTO_PROTO:
    _pos = S0.find('3-N)')
    T_START, T_END = para_bounds(S0, _pos)
    TITLE_PROTO = S0[T_START:T_END]                  # 표제 문단 (paraPr 74 / charPr 50)
    TB_START, TB_END = para_bounds(S0, S0.find('<hp:tbl', T_END))
    TABLE_PARA_PROTO = S0[TB_START:TB_END]           # 사진표를 감싼 문단
    TBL_PROTO = re.search(r'<hp:tbl\b.*?</hp:tbl>', TABLE_PARA_PROTO, re.S).group(0)
    CELLS = re.findall(r'<hp:tc\b.*?</hp:tc>', TBL_PROTO, re.S)
    C_HDR_ITEM, C_HDR_COMP, C_ITEM, C_WIDE, C_HALF = CELLS[0], CELLS[1], CELLS[2], CELLS[3], CELLS[4]

def set_text(cell, txt):
    """셀의 첫 <hp:t> 를 교체(없으면 첫 run 에 삽입). 나머지 <hp:t> 는 비운다."""
    ts = list(re.finditer(r'<hp:t>.*?</hp:t>', cell, re.S))
    if ts:
        cell = cell[:ts[0].start()] + f'<hp:t>{esc(txt)}</hp:t>' + cell[ts[0].end():]
        cell = re.sub(r'(?<!\A)<hp:t>.*?</hp:t>', '', cell, count=len(ts) - 1, flags=re.S) if len(ts) > 1 else cell
        return cell
    return _into_first_run(cell, f'<hp:t>{esc(txt)}</hp:t>')

def _into_first_run(cell, payload):
    """첫 run 안에 payload 삽입. 빈 셀의 self-closing run(<hp:run .../>)도 정식 run 으로 변환."""
    m = re.search(r'<hp:run\b[^>]*?/>', cell)       # self-closing run 우선 처리
    if m:
        open_tag = m.group(0)[:-2] + '>'            # '/>' → '>'
        return cell[:m.start()] + open_tag + payload + '</hp:run>' + cell[m.end():]
    return re.sub(r'(<hp:run\b[^>]*?>)', r'\1' + payload, cell, count=1)

def set_pic(cell, pic):
    """셀 안의 텍스트를 지우고 그림을 넣는다."""
    cell = re.sub(r'<hp:t>.*?</hp:t>', '', cell, flags=re.S)
    return _into_first_run(cell, pic)

def set_addr(cell, col, row, cs=None, rs=None):
    cell = re.sub(r'<hp:cellAddr colAddr="\d+" rowAddr="\d+"/>',
                  f'<hp:cellAddr colAddr="{col}" rowAddr="{row}"/>', cell)
    if cs is not None or rs is not None:
        def f(m):
            c = cs if cs is not None else m.group(1)
            r = rs if rs is not None else m.group(2)
            return f'<hp:cellSpan colSpan="{c}" rowSpan="{r}"/>'
        cell = re.sub(r'<hp:cellSpan colSpan="(\d+)" rowSpan="(\d+)"/>', f, cell)
    return cell

# ── 4) 사진 → JPEG, BinData 등록 ──
existing = [n for n in zin.namelist() if n.startswith('BinData/')]
next_id = max([int(re.search(r'image(\d+)', n).group(1)) for n in existing if re.search(r'image(\d+)', n)] + [0]) + 1
newbins = []
for r in photos:                      # 전체 사진 등록(PRE=pages, SUP=품목표 공용)
    try:
        im = Image.open(io.BytesIO(base64.b64decode(r['dataURL'].split(',', 1)[1])))
        if im.mode != 'RGB': im = im.convert('RGB')
        im.thumbnail((PHOTO_MAX, PHOTO_MAX))
        buf = io.BytesIO(); im.save(buf, 'JPEG', quality=PHOTO_Q)
    except Exception:
        continue
    nm = f'image{next_id}'; next_id += 1
    newbins.append((f'BinData/{nm}.jpg', buf.getvalue(), nm))
    r['_img'], r['_px'] = nm, im.size
print('삽입 이미지', len(newbins), '개')

# 원본 그림 XML 을 복제해 이미지 ID·크기만 교체 (렌더링 속성 그대로 유지)
PIC_PROTO = re.search(r'<hp:pic\b.*?</hp:pic>', S0, re.S).group(0)
def make_pic(imgid, px, boxw, fixed=None, wh=None, box=None):
    ratio = px[1] / max(px[0], 1)                # 원본 세로/가로 비율
    if box:                                      # 박스(maxW,maxH) 안에 원본비율로 맞춤(contain)
        mw, mh = box
        w = mw; h = int(w * ratio)
        if h > mh: h = mh; w = int(h / max(ratio, 1e-6))
    elif wh:
        w, h = wh
    elif fixed:
        w = h = fixed
    else:
        w = boxw - 800
        h = max(1, int(w * ratio))
    w = max(1, w); h = max(1, h)
    p = PIC_PROTO
    p = re.sub(r'binaryItemIDRef="[^"]*"', f'binaryItemIDRef="{imgid}"', p)
    p = re.sub(r'<hp:orgSz width="\d+" height="\d+"/>', f'<hp:orgSz width="{w}" height="{h}"/>', p)
    p = re.sub(r'<hp:sz width="\d+"', f'<hp:sz width="{w}"', p)
    p = re.sub(r'height="\d+" heightRelTo="ABSOLUTE"', f'height="{h}" heightRelTo="ABSOLUTE"', p)
    p = re.sub(r'<hp:imgRect>.*?</hp:imgRect>',
               f'<hp:imgRect><hc:pt0 x="0" y="0"/><hc:pt1 x="{w}" y="0"/>'
               f'<hc:pt2 x="{w}" y="{h}"/><hc:pt3 x="0" y="{h}"/></hp:imgRect>', p, flags=re.S)
    p = re.sub(r'<hp:imgClip[^/]*/>', f'<hp:imgClip left="0" right="{w}" top="0" bottom="{h}"/>', p)
    p = re.sub(r'<hp:imgDim[^/]*/>', f'<hp:imgDim dimwidth="{w}" dimheight="{h}"/>', p)
    return p

W_ITEM, W_WIDE, W_HALF = 2856, 42784, 21392

def build_table(pg):
    """가로 헤더=대분류(area), 세로 병합=세부항목(comp). 세부마다 사진N칸+지적 페어."""
    # 헤더: [항목][area cs2]
    rows = ['<hp:tr>' + set_addr(set_text(C_HDR_ITEM, '항목'), 0, 0)
            + set_addr(set_text(C_HDR_COMP, pg['area']), 1, 0, cs=2) + '</hp:tr>']
    r = 1
    for b in pg['blocks']:
        seg = []                                # 이 세부항목의 (kind,payload) 행들
        ph = b['photos']
        for i in range(0, len(ph), 2):
            pair = ph[i:i+2]
            if len(pair) == 2:
                seg.append(('pic2', pair)); seg.append(('cap2', pair))
            else:
                seg.append(('pic1', pair[0])); seg.append(('cap1', pair[0]))
        n = len(seg)
        for j, (kind, payload) in enumerate(seg):
            lead = (set_addr(set_text(C_ITEM, b['comp']), 0, r, cs=1, rs=n)
                    .replace('textDirection="HORIZONTAL"', 'textDirection="VERTICAL"')) if j == 0 else ''
            if kind == 'pic2':
                inner = ''.join(set_addr(set_pic(C_HALF, make_pic(p['_img'], p['_px'], W_HALF, box=(20372, 15279))), 1 + k, r)
                                for k, p in enumerate(payload))
            elif kind == 'cap2':
                inner = ''.join(set_addr(set_text(C_HALF, p.get('comment') or ''), 1 + k, r)
                                for k, p in enumerate(payload))
            elif kind == 'pic1':
                inner = set_addr(set_pic(C_WIDE, make_pic(payload['_img'], payload['_px'], W_WIDE, box=(41764, 18609))), 1, r, cs=2)
            else:
                inner = set_addr(set_text(C_WIDE, payload.get('comment') or ''), 1, r, cs=2)
            rows.append(f'<hp:tr>{lead}{inner}</hp:tr>')
            r += 1
    tbl = re.sub(r'<hp:tr\b.*</hp:tr>', ''.join(rows), TBL_PROTO, flags=re.S)
    tbl = re.sub(r'rowCnt="\d+"', f'rowCnt="{r}"', tbl)
    return tbl

def photo_pages_xml():
    out = []
    for i, pg in enumerate(pages):
        title = f'3-{i+1}) 엘리베이터 {pg["area"]} 사진 ({pg["unit"]})'
        out.append(re.sub(r'<hp:t>.*?</hp:t>', f'<hp:t>{esc(title)}</hp:t>', TITLE_PROTO, count=1, flags=re.S))
        out.append(TABLE_PARA_PROTO.replace(TBL_PROTO, build_table(pg)))
    return ''.join(out)

# ── SUP: 입고사진 표를 품목(=사진 detail)별로 원형 복제해 채움 ──
def build_sup_table(tbl_xml):
    cells = re.findall(r'<hp:tc\b.*?</hp:tc>', tbl_xml, re.S)
    if len(cells) < 9: return tbl_xml            # 예상 원형 아님 → 그대로
    HDR = cells[:4]
    C_ITEM, C_PHOTO, C_CONTENT, C_ACTION = cells[4], cells[5], cells[6], cells[7]
    photo_w = int((re.search(r'<hp:cellSz width="(\d+)"', C_PHOTO) or re.search(r'(18000)','x18000').re).group(1)) if re.search(r'<hp:cellSz width="(\d+)"', C_PHOTO) else 18000
    # 품목(=detail, 없으면 part) 순서대로 그룹
    groups = []
    for p in photos:
        key = p.get('detail') or p.get('part') or '품목'
        g = next((x for x in groups if x['k'] == key), None)
        if not g: g = {'k': key, 'ph': []}; groups.append(g)
        g['ph'].append(p)
    if not groups: return tbl_xml
    rows = ['<hp:tr>' + ''.join(set_addr(c, i, 0) for i, c in enumerate(HDR)) + '</hp:tr>']
    r = 1
    for g in groups:
        ph = g['ph']; n = len(ph)
        for j, p in enumerate(ph):
            row = []
            if j == 0:
                row.append(set_addr(set_text(C_ITEM, g['k']), 0, r, rs=n))
            img = make_pic(p['_img'], p['_px'], photo_w, box=(14261, 14261)) if p.get('_img') else ''   # 50.31mm 박스 안 원본비율
            row.append(set_addr(set_pic(C_PHOTO, img) if img else C_PHOTO, 1, r + j))
            if j == 0:
                row.append(set_addr(set_text(C_CONTENT, p.get('comment') or ''), 2, r, rs=n))
                row.append(set_addr(C_ACTION, 3, r, rs=n))
            rows.append('<hp:tr>' + ''.join(row) + '</hp:tr>')
        r += n
    tbl = re.sub(r'<hp:tr\b.*</hp:tr>', ''.join(rows), tbl_xml, flags=re.S)
    return re.sub(r'rowCnt="\d+"', f'rowCnt="{r}"', tbl)

# ── 5) 사양표를 호기 수에 맞게 (문자열 수술 — 문서 나머지는 그대로) ──
def row_cells(tr): return re.findall(r'<hp:tc\b.*?</hp:tc>', tr, re.S)
def cell_txt(c):   return ''.join(re.findall(r'<hp:t>(.*?)</hp:t>', c, re.S)).strip()
def set_w(c, w):
    c = re.sub(r'<hp:cellSz width="\d+"', f'<hp:cellSz width="{w}"', c)
    return re.sub(r'textWidth="\d+"', f'textWidth="{w}"', c)

def adapt_specs(xml):
    def fix(mt):
        tbl = mt.group(0)
        trs = re.findall(r'<hp:tr\b.*?</hp:tr>', tbl, re.S)
        if not trs: return tbl
        head = [cell_txt(c) for c in row_cells(trs[0])]
        # 진동 그래프(표6): 'N호기'+'용도' → 헤더 2칸(N호기|용도)만, 표 전체를 호기수만큼 복제
        if head[:1] == ['N호기'] and any('용도' in h for h in head):
            h0 = row_cells(trs[0])
            if len(h0) >= 3:                     # 헤더 3번째 칸 삭제 + 용도칸 cs2 로 폭 흡수
                w1 = int((re.search(r'<hp:cellSz width="(\d+)"', h0[1]) or re.search(r'(0)', '0')).group(1))
                w2 = int((re.search(r'<hp:cellSz width="(\d+)"', h0[2]) or re.search(r'(0)', '0')).group(1))
                new_h1 = set_w(set_addr(h0[1], 1, 0, cs=2), w1 + w2)
                new_hdr = '<hp:tr>' + h0[0] + new_h1 + '</hp:tr>'
                tbl = tbl.replace(trs[0], new_hdr, 1)
            return ''.join(re.sub(r'N호기', f'{u+1}호기', tbl, count=1) for u in range(units))
        # 진동 측정결과(표5): 헤더2행 + 'N호기' 5행블록 → 블록을 호기수만큼 세로복제
        if head[:1] == ['호기'] and 'X축' in tbl:
            hdr = trs[:2]                        # 호기/진동/소음/속도 + X/Y/Z (rowAddr 0,1)
            block = trs[2:]                      # N호기 5행 블록
            made = []
            for u in range(units):
                off = u * len(block)
                for tr in block:
                    def shift(c):
                        c = re.sub(r'rowAddr="(\d+)"', lambda m: f'rowAddr="{int(m.group(1)) + off}"', c)
                        if cell_txt(c) == 'N호기':
                            c = set_text(c, f'{u+1}호기')   # 호기 라벨은 볼드(cp63) 유지 — 완성본과 동일
                        return c
                    made.append('<hp:tr>' + ''.join(shift(c) for c in row_cells(tr)) + '</hp:tr>')
            tbl = re.sub(r'<hp:tr\b.*</hp:tr>', ''.join(hdr) + ''.join(made), tbl, flags=re.S)
            return re.sub(r'rowCnt="\d+"', f'rowCnt="{2 + units * len(block)}"', tbl)
        # 로프 인장력(표9): '구분'+'#1' → 'N호기' 데이터 행을 호기수만큼
        if head[:1] == ['구분'] and any(h == '#1' for h in head):
            out_rows, made_done = [], False
            for tr in trs:
                cells = row_cells(tr)
                if not made_done and cells and cell_txt(cells[0]) == 'N호기':
                    for u in range(units):
                        col, cc = 0, []
                        for c in cells:
                            sp = re.search(r'colSpan="(\d+)"', c); cspan = int(sp.group(1)) if sp else 1
                            cc.append(set_addr(set_text(c, f'{u+1}호기') if col == 0 else c, col, 1 + u)); col += cspan
                        out_rows.append('<hp:tr>' + ''.join(cc) + '</hp:tr>')
                    made_done = True
                elif made_done:                          # 허용편차 등 이후 행 좌표 +units-1
                    col, cc = 0, []
                    ra = 1 + units + (len([r for r in out_rows]) - (1 + units))  # 대략
                    for c in cells:
                        sp = re.search(r'colSpan="(\d+)"', c); cspan = int(sp.group(1)) if sp else 1
                        cc.append(set_addr(c, col, len(out_rows))); col += cspan
                    out_rows.append('<hp:tr>' + ''.join(cc) + '</hp:tr>')
                else:
                    out_rows.append(tr)
            tbl = re.sub(r'<hp:tr\b.*</hp:tr>', ''.join(out_rows), tbl, flags=re.S)
            return re.sub(r'rowCnt="\d+"', f'rowCnt="{len(out_rows)}"', tbl)
        if head[:1] == ['NO.']:                       # 1) 기본사양 — 열을 호기 수만큼
            c0 = row_cells(trs[0])
            w0 = int(re.search(r'<hp:cellSz width="(\d+)"', c0[0]).group(1))
            w1 = int(re.search(r'<hp:cellSz width="(\d+)"', c0[1]).group(1))
            tot = sum(int(re.search(r'<hp:cellSz width="(\d+)"', c).group(1)) for c in c0)
            each = (tot - w0 - w1) // max(units, 1)
            new_trs = []
            for ri, tr in enumerate(trs):
                cs = row_cells(tr)
                if len(cs) <= 2: new_trs.append(tr); continue
                keep, proto = cs[:2], cs[2]
                made = [set_w(set_addr(set_text(proto, f'{u+1}호기' if ri == 0 else ''), 2 + u, ri), each)
                        for u in range(units)]
                new_trs.append('<hp:tr>' + ''.join(keep + made) + '</hp:tr>')
            tbl = re.sub(r'<hp:tr\b.*</hp:tr>', ''.join(new_trs), tbl, flags=re.S)
            return re.sub(r'colCnt="\d+"', f'colCnt="{2 + units}"', tbl)
        if head[:1] == ['모델']:                      # 2) 호기별 사양 — 행을 호기 수만큼
            data_rows = [t for t in trs[1:] if '합' not in ''.join(cell_txt(c) for c in row_cells(t))]
            sum_rows  = [t for t in trs[1:] if t not in data_rows]
            if not data_rows: return tbl
            proto = data_rows[0]
            made = []
            for u in range(units):
                cs, col, out = row_cells(proto), 0, []
                for ci, c in enumerate(cs):
                    span = re.search(r'colSpan="(\d+)"', c)
                    cspan = int(span.group(1)) if span else 1
                    out.append(set_addr(set_text(c, str(u + 1) if ci == 1 else ''), col, u + 1))
                    col += cspan
                made.append('<hp:tr>' + ''.join(out) + '</hp:tr>')
            fixed_sum = []
            for t in sum_rows:                        # 합계행 좌표도 병합폭 고려해 재계산
                col, out = 0, []
                for c in row_cells(t):
                    span = re.search(r'colSpan="(\d+)"', c)
                    cspan = int(span.group(1)) if span else 1
                    out.append(set_addr(c, col, units + 1)); col += cspan
                fixed_sum.append('<hp:tr>' + ''.join(out) + '</hp:tr>')
            tbl = re.sub(r'<hp:tr\b.*</hp:tr>', trs[0] + ''.join(made + fixed_sum), tbl, flags=re.S)
            return re.sub(r'rowCnt="\d+"', f'rowCnt="{1 + units + len(fixed_sum)}"', tbl)
        if any('n호기' in h.replace(' ', '') for h in head):   # SUP 개요표 — 헤더 n호기 → 호기수 열
            cw = lambda c: int((re.search(r'<hp:cellSz width="(\d+)"', c) or re.search(r'(0)', c)).group(1))
            new_trs = []
            for ri, tr in enumerate(trs):
                cs = row_cells(tr)
                keep = cs[:1] if ri == 0 else cs[:2]         # 헤더=항목(cs2) 1칸 / 데이터=NO·항목명 2칸
                label_w = sum(cw(c) for c in keep)
                total = sum(cw(c) for c in cs)
                each = (total - label_w) // max(units, 1)
                proto = cs[len(keep)]                        # 호기 값칸 원형
                made = []
                col = 2                                       # 항목 영역(NO+항목명 / 항목 cs2) = 항상 2열
                for u in range(units):
                    v = f'{u+1}호기' if ri == 0 else ''
                    made.append(set_w(set_addr(set_text(proto, v), col + u, ri, cs=1), each))
                new_trs.append('<hp:tr>' + ''.join(keep + made) + '</hp:tr>')
            tbl = re.sub(r'<hp:tr\b.*</hp:tr>', ''.join(new_trs), tbl, flags=re.S)
            return re.sub(r'colCnt="\d+"', f'colCnt="{2 + units}"', tbl)
        return tbl
    return re.sub(r'<hp:tbl\b.*?</hp:tbl>', fix, xml, flags=re.S)

# ── 6) 미리보기 입력칸(t{표}.r{행}.c{열}) → 한글 같은 칸 ──
def apply_fields(xml, seq0):
    if not FIELDS: return xml, seq0
    seq = [seq0]
    def fix(mt):
        seq[0] += 1; s = seq[0]
        def rc(mc):
            c = mc.group(0)
            ma = re.search(r'<hp:cellAddr colAddr="(\d+)" rowAddr="(\d+)"/>', c)
            if not ma: return c
            k = f't{s}.r{ma.group(2)}.c{ma.group(1)}'
            return set_text(c, str(FIELDS[k])) if k in FIELDS else c
        return re.sub(r'<hp:tc\b.*?</hp:tc>', rc, mt.group(0), flags=re.S)
    out = re.sub(r'<hp:tbl\b.*?</hp:tbl>', fix, xml, flags=re.S)
    return out, seq[0]

# ── 7) 대용량 BMP 재압축 ──
recompress = {}
for it in zin.infolist():
    if not it.filename.startswith('BinData/') or not it.filename.upper().endswith('.BMP'): continue
    if it.file_size < BMP_MIN: continue
    try:
        im = Image.open(io.BytesIO(zin.read(it.filename)))
        if im.mode != 'RGB': im = im.convert('RGB')
        im.thumbnail((1600, 1600))
        buf = io.BytesIO(); im.save(buf, 'JPEG', quality=85)
        stem = it.filename.split('/')[-1].rsplit('.', 1)[0]
        recompress[it.filename] = (f'BinData/{stem}.jpg', buf.getvalue())
    except Exception:
        pass
if recompress:
    b0 = sum(zin.getinfo(k).file_size for k in recompress); b1 = sum(len(v[1]) for v in recompress.values())
    print(f'대용량 이미지 재압축 {len(recompress)}개: {b0/1024/1024:.1f}MB → {b1/1024/1024:.1f}MB')

# ── 8) 출력 ──
vals = {'{{현장명}}': site, '{{작성일}}': data.get('issueDate') or '',
        '{{설치연월}}': data.get('installYM') or '', '{{경과연수}}': data.get('ageY') or ''}
seq_state = 0
zout = zipfile.ZipFile(OUT, 'w', zipfile.ZIP_DEFLATED)
for it in zin.infolist():
    b = zin.read(it.filename)
    if it.filename.startswith('Contents/section'):
        t = b.decode('utf-8')
        for k, v in vals.items():
            if v: t = t.replace(k, esc(v))
        t = t.replace('2025.11.04.(화),11.07(금)', '')   # 진동 측정일 견본 → 공백
        # 머리말: ① 밑줄 제거(bf16 실선→bf1 무테두리) ② 현장명을 오른쪽 끝으로(가운데탭 22652/type2 → 오른쪽탭 45348/type1)
        hi = t.find('<hp:header')
        if hi >= 0:
            hj = t.find('</hp:header>', hi) + 12
            hseg = t[hi:hj].replace('borderFillIDRef="16"', 'borderFillIDRef="1"')
            if site:   # 탭 바로 뒤가 현장명일 때만 → 오른쪽정렬 탭으로
                hseg = hseg.replace(f'<hp:tab width="22652" leader="0" type="2"/>{esc(site)}',
                                    f'<hp:tab width="45348" leader="0" type="1"/>{esc(site)}')
            t = t[:hi] + hseg + t[hj:]
        if '3-N)' in t:                                   # PRE: 견본 사진쪽 → 앱 사진쪽
            a, _ = para_bounds(t, t.find('3-N)'))
            _, z2 = para_bounds(t, t.find('<hp:tbl', a))
            t = t[:a] + photo_pages_xml() + t[z2:]
        elif '입 고 사 진' in t and photos:               # SUP: 입고사진 표를 품목별로 채움
            def _sup(mt):
                tb = mt.group(0)
                return build_sup_table(tb) if '입 고 사 진' in tb else tb
            t = re.sub(r'<hp:tbl\b.*?</hp:tbl>', _sup, t, flags=re.S)
        t = adapt_specs(t)
        t, seq_state = apply_fields(t, seq_state)
        b = t.encode('utf-8')
    elif it.filename == 'Contents/content.hpf':
        t = b.decode('utf-8')
        for old, (new, _d) in recompress.items():
            t = t.replace(f'href="{old}" media-type="image/bmp"', f'href="{new}" media-type="image/jpeg"')
            t = t.replace(f'href="{old}"', f'href="{new}"')
        add = ''.join(f'<opf:item id="{i}" href="{n}" media-type="image/jpeg" isEmbeded="1"/>'
                      for n, _d, i in newbins)
        t = t.replace('</opf:manifest>', add + '</opf:manifest>')
        b = t.encode('utf-8')
    if it.filename in recompress:
        zout.writestr(recompress[it.filename][0], recompress[it.filename][1]); continue
    zout.writestr(it, b)
for n, d, _i in newbins:
    zout.writestr(n, d)
zout.close(); zin.close()

print(f'완료: {OUT}  {os.path.getsize(OUT)/1024/1024:.1f}MB')
zt = zipfile.ZipFile(OUT)
s0 = zt.read('Contents/section0.xml').decode('utf-8')
print('zip 무결성:', zt.testzip() is None,
      '| 현장명:', site in s0, '| 사진쪽:', len(re.findall(r'3-\d+\) 엘리베이터', s0)))
