#!/usr/bin/env node
/**
 * finalize_layout.mjs — 본문 HWPX(엔진 산출)에 경북연구원 조판 레이어를 입힌다.
 *  · 겉/속표지·연구진·비매품 (양식 조각 splice + 플레이스홀더 치환)
 *  · 요약(Summary, 로마자 쪽번호) · 목차 · 표목차 · 그림목차 (본문 제목/캡션에서 자동 생성)
 *  · 장별 간지 · 절 투명표 + 새 페이지 · 항(1.) 새 페이지(절 바로 아래 첫 1.은 제외)
 *  · 머리말 쪽번호(홀수=장 제목, 짝수=보고서명; 본문 아라비아 / 요약 로마자)
 *  · 뒷표지
 *
 * 사용:
 *   node finalize_layout.mjs --in <body.hwpx> --summary <90_summary.md> --config <cfg.json> --out <final.hwpx>
 *   (--summary/--config 생략 가능; config 없으면 내장 기본값 사용)
 */
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import { splitSectionShell, splitTopLevelParas, paraAttrs, paraLeadText, esc } from './hwpx_util.mjs';
import { loadFragments } from './fragments.mjs';

const require = createRequire(import.meta.url);
const JSZip = require('../engine/jszip.min.js');
const HERE = path.dirname(fileURLToPath(import.meta.url));

// ── arg 파싱 ──
function parseArgs(argv) {
  const a = {};
  for (let i = 0; i < argv.length; i++) {
    const t = argv[i];
    if (t === '--in') a.in = argv[++i];
    else if (t === '--summary') a.summary = argv[++i];
    else if (t === '--config') a.config = argv[++i];
    else if (t === '--out') a.out = argv[++i];
    else throw new Error('알 수 없는 옵션: ' + t);
  }
  return a;
}

// 범용 기본값 — 프로젝트별 값은 --config <json> 으로 덮어쓴다(표지 플레이스홀더 원칙).
const DEFAULT_CFG = {
  titleLines: ['[보고서 제목 1행]', '[보고서 제목 2행]'],
  reportNo: '연구 2026 - 00',
  enTitle: ['[영문 제목 확인요망]', ''],
  authors: '[연구진 확인요망]',
  reportSeries: '경북연구원 정책과제 수립연구',
};

// ── 메인 ──
async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (!args.in || !args.out) throw new Error('--in, --out 필수');
  const cfg = { ...DEFAULT_CFG, ...(args.config ? JSON.parse(fs.readFileSync(args.config, 'utf8')) : {}) };
  const frag = loadFragments();
  FRAG_JANGBOX = frag.jang_box_tbl || '';
  FRAG_REFSBOX = frag.refs_box_tbl || '';

  const zip = await JSZip.loadAsync(fs.readFileSync(path.resolve(args.in)));
  const sec = await zip.file('Contents/section0.xml').async('string');

  // 참고문헌 박스용 신규 borderFill(상·하 SOLID, 좌우 NONE) 을 header.xml 에 추가
  const hdrXml = await zip.file('Contents/header.xml').async('string');
  const bfIds = [...hdrXml.matchAll(/<hh:borderFill id="(\d+)"/g)].map((m) => +m[1]);
  REFS_BF = String(Math.max(...bfIds) + 1);
  const newBF = `<hh:borderFill id="${REFS_BF}" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0"><hh:slash type="NONE" Crooked="0" isCounter="0"/><hh:backSlash type="NONE" Crooked="0" isCounter="0"/><hh:leftBorder type="NONE" width="0.12 mm" color="#000000"/><hh:rightBorder type="NONE" width="0.12 mm" color="#000000"/><hh:topBorder type="SOLID" width="0.2 mm" color="#000000"/><hh:bottomBorder type="SOLID" width="0.2 mm" color="#000000"/><hh:diagonal type="SOLID" width="0.1 mm" color="#000000"/></hh:borderFill>`;
  let newHdr = hdrXml.replace(/(<hh:borderFills itemCnt=")(\d+)(")/, (mm, a, n, c) => a + (Number(n) + 1) + c);
  newHdr = newHdr.replace('</hh:borderFills>', newBF + '</hh:borderFills>');
  zip.file('Contents/header.xml', newHdr);
  const { head, tail, paras } = splitSectionShell(sec);

  // 페이지는 양식 원본 크기(190×260mm = 53858×73700) 사용 — 표지 secPr(native)을 그대로 유지.
  // 본문(report_그림)은 A4 본문폭(42520)으로 만들어졌으므로 양식 본문폭(35150)으로 축소한다.

  // 본문 시작 = 첫 s4 "제N장"
  const bodyStart = paras.findIndex((p) => paraAttrs(p.xml).styleIDRef === '4' && /^제\d+장/.test(paraLeadText(p.xml)));
  if (bodyStart < 0) throw new Error('본문(제1장) 시작을 찾지 못함');
  const bodyParas = paras.slice(bodyStart).map((p) => rescaleBodyToNative(p.xml));

  // ── 본문에서 목차/표목차/그림목차 자료 수집 + 장→절 트리 ──
  const chapters = []; // {num,title,jeols:[{num,title,hangs:[{num,title}]}]}
  const tables = [], figures = [];
  let curCh = null, curJeol = null;
  for (const px of bodyParas) {
    const st = paraAttrs(px).styleIDRef;
    const tx = paraLeadText(px);
    let m;
    if (st === '4' && (m = tx.match(/^제(\d+)장\s+(.+)$/))) {
      curCh = { num: +m[1], title: m[2].trim(), jeols: [] }; chapters.push(curCh); curJeol = null;
    } else if (st === '4' && /참고문헌/.test(tx)) {
      curCh = null; curJeol = null; // 참고문헌은 목차 트리에서 제외
    } else if (st === '5' && curCh && (m = tx.match(/^제(\d+)절\s+(.+)$/))) {
      curJeol = { num: +m[1], title: m[2].trim(), hangs: [] }; curCh.jeols.push(curJeol);
    } else if (st === '6' && curJeol && (m = tx.match(/^(\d+)\.\s+(.+)$/))) {
      curJeol.hangs.push({ num: +m[1], title: m[2].trim() });
    } else if (st === '10' && (m = tx.match(/^(\[표\s*\d+-\d+\][^]*?)(?:\s*\(단위.*)?$/))) {
      tables.push(tx.replace(/\s*\(단위[^)]*\)\s*$/, '').trim());
    } else if (st === '14' && /^\[그림\s*\d+-\d+\]/.test(tx)) {
      figures.push(tx.trim());
    }
  }

  // ── 프런트매터 조립 ──
  const front = [];
  front.push(buildCover(frag.cover, cfg));
  front.push(buildSummary(frag, args.summary, cfg));
  front.push(buildToc(frag.toc_heading, chapters));
  front.push(buildCaptionToc(frag.tbl_toc_heading, tables, '표'));
  front.push(buildCaptionToc(frag.fig_toc_heading, figures, '그림'));

  // ── 본문 변환 ──
  const body = transformBody(bodyParas, chapters, frag, cfg);

  // ── 뒷표지 ──
  const back = buildBackCover(cfg, frag);

  const newSec = head + front.join('') + body + back + tail;

  // 검증: 태그 균형
  const bal = (re) => (newSec.match(re) || []).length;
  if (bal(/<hp:p\b/g) !== bal(/<\/hp:p>/g)) throw new Error('hp:p 불균형: ' + bal(/<hp:p\b/g) + '/' + bal(/<\/hp:p>/g));
  if (bal(/<hp:tbl\b/g) !== bal(/<\/hp:tbl>/g)) throw new Error('hp:tbl 불균형');

  zip.file('Contents/section0.xml', newSec);
  const outBuf = Buffer.from(await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }));
  fs.mkdirSync(path.dirname(path.resolve(args.out)), { recursive: true });
  fs.writeFileSync(path.resolve(args.out), outBuf);
  console.log(`✅ finalize: ${path.relative(process.cwd(), args.out)} (${outBuf.length.toLocaleString()} bytes)`);
  console.log(`   장 ${chapters.length} · 표 ${tables.length} · 그림 ${figures.length} · 본문문단 ${bodyParas.length}`);
}

// ── 본문(A4 42520폭)을 양식 본문폭(35150)으로 축소 — 표/그림만, 텍스트는 자동 reflow ──
function rescaleBodyToNative(xml) {
  const F = 35150 / 42520;
  const sc = (v) => Math.round(+v * F);
  // 표: 표 sz 너비 + 셀 너비 축소(높이는 콘텐츠 기반이라 유지)
  xml = xml.replace(/<hp:cellSz width="(\d+)" height="(\d+)"\/>/g, (m, w, h) => `<hp:cellSz width="${sc(w)}" height="${h}"/>`);
  xml = xml.replace(/(<hp:tbl\b[^>]*>\s*<hp:sz width=")(\d+)(")/g, (m, a, w, c) => a + sc(w) + c);
  // 그림(figimg): orgSz/curSz/sz/imgRect 모두 동일비율 축소(종횡비 유지)
  xml = xml.replace(/<hp:pic\b[\s\S]*?<\/hp:pic>/g, (pic) => {
    if (!/binaryItemIDRef="figimg/.test(pic)) return pic;
    pic = pic.replace(/<hp:(orgSz|curSz) width="(\d+)" height="(\d+)"\/>/g, (m, t, w, h) => `<hp:${t} width="${sc(w)}" height="${sc(h)}"/>`);
    pic = pic.replace(/<hp:sz width="(\d+)" widthRelTo="ABSOLUTE" height="(\d+)" heightRelTo="ABSOLUTE" protect="0"\/>/g, (m, w, h) => `<hp:sz width="${sc(w)}" widthRelTo="ABSOLUTE" height="${sc(h)}" heightRelTo="ABSOLUTE" protect="0"/>`);
    pic = pic.replace(/<hp:imgRect>[\s\S]*?<\/hp:imgRect>/, (r) => r.replace(/(x|y)="(\d+)"/g, (mm, ax, v) => `${ax}="${sc(v)}"`));
    return pic;
  });
  return xml;
}

// ── 표지(겉/속/연구진/비매품) — 양식 native(190×260mm) 그대로 ──
function buildCover(cover, cfg) {
  let s = cover;
  // 표지 secPr(양식 native 53858×73700)을 그대로 사용 → 페이지 전체가 native.
  // 레거시 "대구경북연구원" 로고(image1)는 양식 native 페이지(73700)에선 vertOff 76851로 페이지 밖이라 자동 비표시.
  // 붉은 가이드문구(※ 보고서 겉표지 / ※ 내지 표지)만 제거
  s = s.replace(/<hp:run charPrIDRef="78"><hp:t>[^<]*표지<\/hp:t><\/hp:run>/g, '');

  // 제목 (겉 charPr68 / 속 charPr80)
  for (const cp of ['68', '80']) {
    s = s.replace(`<hp:run charPrIDRef="${cp}"><hp:t>경북연구원</hp:t>`, `<hp:run charPrIDRef="${cp}"><hp:t>${esc(cfg.titleLines[0])}</hp:t>`);
    s = s.replace(`<hp:run charPrIDRef="${cp}"><hp:t>정책과제 수립 연구</hp:t>`, `<hp:run charPrIDRef="${cp}"><hp:t>${esc(cfg.titleLines[1] || '')}</hp:t>`);
  }
  // 연구번호 (2회)
  s = s.split('<hp:t>연구 2024 - 00</hp:t>').join(`<hp:t>${esc(cfg.reportNo)}</hp:t>`);
  // 영문 제목 (2회씩)
  s = s.split('<hp:t>Suggestions for Changing Administrative Districts to Improve</hp:t>').join(`<hp:t>${esc(cfg.enTitle[0] || '')}</hp:t>`);
  s = s.split('<hp:t>Conveniences of Resident Life in Daegu Metropolitan City</hp:t>').join(`<hp:t>${esc(cfg.enTitle[1] || '')}</hp:t>`);
  // 저자·연구진명은 샘플 표기 그대로 유지(겉/속 '김 연 구‧이 연 구', 연구진 페이지 '김연구 …연구위원·연구원')
  // 연구진 페이지: 두 번째 'ㅣ연구 책임ㅣ' 블록 → 'ㅣ공동 연구ㅣ'
  { let c = 0; s = s.replace(/연구 책임/g, (mm) => (++c === 2 ? '공동 연구' : mm)); }
  return s;
}

// ── 요약 ──
function buildSummary(frag, summaryPath, cfg) {
  const reportTitle = cfg.titleLines.join(' ');
  const oddRoman = a4FixHeader(frag.hdr_odd_roman);
  const evenRoman = a4FixHeader(frag.hdr_even_roman).replace('경북연구원 정책과제 수립연구', esc(reportTitle));
  // 양식 구조: "Summary 요약" 제목(머리말 없음) → 빈 스페이서 문단(로마자 머리말 운반 + 1줄 간격) → 1. 과제의 필요성
  // (머리말을 제목 run 에 직접 주입하면 앵커가 pageBreak 제목줄이 되어 '요약'이 표 상단선 위로 보이므로 스페이서로 분리)
  const sumSpacer = `<hp:p id="0" paraPrIDRef="10" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="30">${oddRoman}${evenRoman}<hp:t/></hp:run><hp:linesegarray><hp:lineseg textpos="0" vertpos="3520" vertsize="2000" textheight="2000" baseline="1700" spacing="1200" horzpos="0" horzsize="35148" flags="393216"/></hp:linesegarray></hp:p>`;
  let out = frag.summary_title + sumSpacer;
  if (summaryPath && fs.existsSync(summaryPath)) {
    const md = fs.readFileSync(summaryPath, 'utf8');
    const blocks = md.split('\n## ').slice(1); // 각 "## n. 제목\n\n본문..."
    for (const b of blocks) {
      const lines = b.split('\n');
      const heading = lines[0].replace(/^#*\s*/, '').trim();
      const bodyTxt = lines.slice(1).join('\n').trim();
      out += sumHead(heading);
      for (const para of bodyTxt.split(/\n{2,}/).map((x) => x.trim()).filter(Boolean)) out += sumBody(para);
      out += sumHead(''); // 절 간 빈 줄
    }
  }
  return out;
}
const sumHead = (t) => `<hp:p id="0" paraPrIDRef="16" styleIDRef="21" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="2">${t ? `<hp:t>${esc(t)}</hp:t>` : ''}</hp:run></hp:p>`;
const sumBody = (t) => `<hp:p id="0" paraPrIDRef="17" styleIDRef="22" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="45"><hp:t>${esc(t)}</hp:t></hp:run></hp:p>`;

// ── 목차 — 양식 verbatim 템플릿 (장=2열 투명박스, 절=실선리더, 항=리더없음) ──
const TOC_SPACER = `<hp:p id="0" paraPrIDRef="69" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="22"/></hp:p>`;
const TOC_JANG = `<hp:p id="0" paraPrIDRef="54" styleIDRef="25" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="22"><hp:tbl id="0" zOrder="35" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL" repeatHeader="1" rowCnt="1" colCnt="2" cellSpacing="0" borderFillIDRef="2" noAdjust="0"><hp:sz width="36399" widthRelTo="ABSOLUTE" height="1350" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="283" right="283" top="283" bottom="283"/><hp:inMargin left="0" right="566" top="0" bottom="0"/><hp:tr><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="4"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="54" styleIDRef="25" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="54"/></hp:p></hp:subList><hp:cellAddr colAddr="0" rowAddr="0"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="849" height="282"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc><hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="4"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"><hp:p id="0" paraPrIDRef="54" styleIDRef="28" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="51"><hp:t> {{TITLE}}</hp:t></hp:run></hp:p></hp:subList><hp:cellAddr colAddr="1" rowAddr="0"/><hp:cellSpan colSpan="1" rowSpan="1"/><hp:cellSz width="35550" height="282"/><hp:cellMargin left="510" right="510" top="141" bottom="141"/></hp:tc></hp:tr></hp:tbl></hp:run><hp:run charPrIDRef="54"><hp:t/></hp:run></hp:p>`;
const TOC_JEOL = `<hp:p id="0" paraPrIDRef="51" styleIDRef="25" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="52"><hp:t>{{TITLE}}<hp:tab width="12292" leader="1" type="1"/> {{PAGE}}</hp:t></hp:run><hp:linesegarray><hp:lineseg textpos="0" vertpos="0" vertsize="1100" textheight="1100" baseline="935" spacing="220" horzpos="5000" horzsize="30148" flags="393216"/></hp:linesegarray></hp:p>`;
// 항 제목은 길어(예: '1_디지털 지급결제 …의 부상') 좁은 줄에서 우측탭+쪽번호가 깨지므로 인라인 표기(제목 뒤 000).
// 실제 쪽번호·우측정렬은 한컴 [차례 만들기] 재생성 시 처리됨.
const TOC_HANG = `<hp:p id="0" paraPrIDRef="50" styleIDRef="29" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="55"><hp:t>{{TITLE}}　{{PAGE}}</hp:t></hp:run></hp:p>`;

function buildToc(heading, chapters) {
  let out = heading + TOC_SPACER.repeat(5); // 목 차 헤딩과 제1장 사이 빈줄 5개(양식 실측)
  for (const ch of chapters) {
    out += TOC_JANG.replace('{{TITLE}}', `제${ch.num}장 ${esc(ch.title)}`);
    for (const j of ch.jeols) {
      out += TOC_JEOL.replace('{{TITLE}}', `제${j.num}절 ${esc(j.title)}`).replace('{{PAGE}}', '000');
      for (const h of j.hangs) {
        out += TOC_HANG.replace('{{TITLE}}', `${h.num}_${esc(h.title)}`).replace('{{PAGE}}', '000');
      }
    }
    out += TOC_SPACER; // 장 간 빈 줄
  }
  return out;
}

// ── 표/그림 목차 ──
function buildCaptionToc(heading, items, kind) {
  // heading: 빨간 가이드문구 런(cp10 "표번호 …양식으로") 제거 + 새 페이지 시작
  let head = heading
    .replace(/<hp:run[^>]*>(?:(?!<\/hp:run>)[\s\S])*?양식으로(?:(?!<\/hp:run>)[\s\S])*?<\/hp:run>/g, '')
    .replace(/(<hp:p\b[^>]*?)pageBreak="\d+"/, '$1pageBreak="1"');
  let out = head;
  // 샘플처럼 헤딩 뒤 7줄 띄우고 시작
  const spacer = '<hp:p id="0" paraPrIDRef="69" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="22"/></hp:p>';
  out += spacer.repeat(7);
  for (const it of items) {
    out += `<hp:p id="0" paraPrIDRef="49" styleIDRef="31" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="53"><hp:t>${esc(it)}<hp:tab width="0" leader="1" type="1"/>000</hp:t></hp:run></hp:p>`;
  }
  return out;
}

// ── 본문 변환 ──
function transformBody(bodyParas, chapters, frag, cfg) {
  const reportTitle = cfg.titleLines.join(' ');
  let out = '';
  let chIdx = 0;
  let lastWasJeol = false;
  let firstChapterDone = false;
  let refsMode = false, inMihwakin = false;
  for (let i = 0; i < bodyParas.length; i++) {
    let px = bodyParas[i];
    const st = paraAttrs(px).styleIDRef;
    const tx = paraLeadText(px);
    let m;
    if (st === '4' && (m = tx.match(/^제(\d+)장\s+(.+)$/))) {
      // 장 → 간지로 대체
      const ch = chapters.find((c) => c.num === +m[1]);
      out += buildGanji(frag.ganji, ch);
      lastWasJeol = false;
      continue;
    }
    if (st === '4' && /참고문헌/.test(tx)) {
      // 참고문헌: 새 페이지 + 장 박스(제목="참고문헌") + 머리말 "참고문헌" 갱신, 이후 항목은 style 20
      const refHdr = a4FixHeader(frag.hdr_odd_arabic
        .replace('<hp:t>제1장 연구 개요</hp:t>', '<hp:t>참고문헌</hp:t>')
        .replace('<hp:t> (장 제목)</hp:t>', ''));
      out += buildRefsBox(esc('참고문헌'), refHdr);
      refsMode = true; inMihwakin = false;
      lastWasJeol = false;
      continue;
    }
    if (refsMode) {
      // 참고문헌 본문: 표/미확인 항목은 그대로, 그 외는 '참고문헌'(style 20)으로 평문 나열
      if (/<hp:tbl/.test(px)) { out += px; continue; }
      if (/미확인/.test(tx)) { inMihwakin = true; out += px; continue; }
      if (inMihwakin) { out += px; continue; }
      if (!tx) { continue; } // 빈 줄 제거
      if (st === '0' && /^본 참고문헌은/.test(tx)) continue; // 안내문 제거
      if (st === '5' && /^\d+\.\s/.test(tx)) continue;       // 번호 분류 헤더 제거(평문 나열)
      out += px.replace(/styleIDRef="\d+"/, 'styleIDRef="20"').replace(/paraPrIDRef="\d+"/, 'paraPrIDRef="9"').replace(/charPrIDRef="\d+"/g, 'charPrIDRef="46"');
      continue;
    }
    if (st === '5' && (m = tx.match(/^제(\d+)절/))) {
      // 절 → 투명표. 첫 절이면 장 박스(머리말+쪽번호 리셋 주입, 새 페이지) 후 같은 페이지에 절
      const isFirst = +m[1] === 1;
      if (isFirst) {
        const ch = chapters[chIdx] || chapters[0];
        const odd = a4FixHeader(frag.hdr_odd_arabic
          .replace('<hp:t>제1장 연구 개요</hp:t>', `<hp:t>제${ch.num}장 ${esc(ch.title)}</hp:t>`)
          .replace('<hp:t> (장 제목)</hp:t>', ''));
        const even = a4FixHeader(frag.hdr_even_arabic
          .replace('<hp:t>경북경구원 수립 연구</hp:t>', `<hp:t>${esc(reportTitle)}</hp:t>`)
          .replace('<hp:t> (책 제목)</hp:t>', ''));
        const newNum = firstChapterDone ? '' : '<hp:ctrl><hp:newNum num="1" numType="PAGE"/></hp:ctrl>';
        firstChapterDone = true; chIdx++;
        out += buildJangBox(ch, odd + even + newNum); // 장 박스(새 페이지)
        out += buildJeol(frag.jeol_tbl, tx, '', false); // 절(같은 페이지)
      } else {
        out += buildJeol(frag.jeol_tbl, tx, '', true); // 절(새 페이지)
      }
      lastWasJeol = true;
      continue;
    }
    if (st === '6' && /^\d+\.\s/.test(tx)) {
      // 항(1.) → 새 페이지 (단, 절 바로 아래 첫 항은 제외)
      out += setPageBreak(px, !lastWasJeol);
      lastWasJeol = false;
      continue;
    }
    // 그 외 본문: 그대로 (단 lastWasJeol 은 의미있는 콘텐츠에서만 리셋)
    out += px;
    if (tx) lastWasJeol = false;
  }
  return out;
}

// pageBreak 속성 강제 설정
function setPageBreak(px, on) {
  return px.replace(/(<hp:p\b[^>]*?)pageBreak="\d+"/, `$1pageBreak="${on ? 1 : 0}"`);
}

// ── 간지 생성 ──
function buildGanji(ganji, ch) {
  let g = ganji;
  // 밴드(첫 문단) 페이지에 머리말/쪽번호 숨김
  g = g.replace('<hp:run charPrIDRef="15">', '<hp:run charPrIDRef="15"><hp:ctrl><hp:pageHiding hideHeader="1" hideFooter="1" hideMasterPage="0" hideBorder="0" hideFill="0" hidePageNum="1"/></hp:ctrl>');
  // 간지는 양식 native 좌표 그대로 — 좌측 띠(17888×73584, 균등 31.95%)가 native 페이지(73700)를 꽉 채우고,
  // 장제목틀·중간선·절틀도 native 위치(23431/23939/23771)에서 정상. (A4 보정·우측이동 제거)
  // 제N장 / 장 제목
  g = g.replace('<hp:run charPrIDRef="37"><hp:t>제1장</hp:t></hp:run>', `<hp:run charPrIDRef="37"><hp:t>제${ch.num}장</hp:t></hp:run>`);
  g = g.replace('<hp:run charPrIDRef="38"><hp:t>연구 개요</hp:t></hp:run>', `<hp:run charPrIDRef="38"><hp:t>${esc(ch.title)}</hp:t></hp:run>`);
  // 절 목록 — 템플릿 절-라인(제1절/제2절) 블록을 실제 절 목록으로 치환
  const jeolRe = /<hp:p\b[^>]*paraPrIDRef="34"[^>]*>(?:(?!<\/hp:p>)[\s\S])*?<\/hp:p>/g;
  const matches = [...g.matchAll(jeolRe)].filter((mm) => /charPrIDRef="26"/.test(mm[0]));
  if (matches.length) {
    const tmpl = matches[0][0];
    const lines = ch.jeols.map((j) => tmpl.replace(/<hp:run charPrIDRef="26"><hp:t>[^<]*<\/hp:t><\/hp:run>/, `<hp:run charPrIDRef="26"><hp:t>제${j.num}절 ${esc(j.title)} </hp:t></hp:run>`)).join('');
    const start = matches[0].index;
    const last = matches[matches.length - 1];
    const end = last.index + last[0].length;
    g = g.slice(0, start) + lines + g.slice(end);
  }
  // 간지 뒤 빈 페이지 1장(머리말·쪽번호 숨김)
  g += `<hp:p id="0" paraPrIDRef="65" styleIDRef="4" pageBreak="1" columnBreak="0" merged="0"><hp:run charPrIDRef="44"><hp:ctrl><hp:pageHiding hideHeader="1" hideFooter="1" hideMasterPage="0" hideBorder="0" hideFill="0" hidePageNum="1"/></hp:ctrl></hp:run></hp:p>`;
  return g;
}

// ── 장 박스(제N장 제목, 상단 실선) — 새 페이지 시작 + 머리말 주입 ──
function buildJangBox(ch, inject) {
  const titleText = ch.num ? `제${ch.num}장 ${esc(ch.title)}` : esc(ch.title);
  let tbl = (FRAG_JANGBOX || '') // native 폭(34875) 그대로
    .replace(/<hp:t>제1장 연구개요<\/hp:t>/, `<hp:t>${titleText}</hp:t>`);
  return `<hp:p id="0" paraPrIDRef="74" styleIDRef="4" pageBreak="1" columnBreak="0" merged="0"><hp:run charPrIDRef="44">${inject}${tbl}</hp:run></hp:p>`;
}
let FRAG_JANGBOX = '';
let FRAG_REFSBOX = '';
let REFS_BF = '2';

// ── 참고문헌 전용 박스(위·아래 실선 + 중앙정렬) ──
//   양식 셀에는 '참고문헌' 위에만 빈 문단(상단 간격)이 있어, 아래에도 동일한 빈 문단을 추가해 상하 간격 균등
const REFS_GAP_PARA = `<hp:p id="0" paraPrIDRef="82" styleIDRef="4" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="84"/><hp:linesegarray><hp:lineseg textpos="0" vertpos="0" vertsize="1700" textheight="1700" baseline="1445" spacing="0" horzpos="0" horzsize="33852" flags="393216"/></hp:linesegarray></hp:p>`;
function buildRefsBox(titleText, inject) {
  let tbl = (FRAG_REFSBOX || '') // native 폭(34875) 그대로
    .replace(/__REFS_BF__/g, REFS_BF)
    .replace(/<hp:t>제1장 연구개요<\/hp:t>/, `<hp:t>${titleText}</hp:t>`)
    .replace('</hp:subList>', REFS_GAP_PARA + '</hp:subList>'); // 참고문헌 아래 간격
  return `<hp:p id="0" paraPrIDRef="74" styleIDRef="4" pageBreak="1" columnBreak="0" merged="0"><hp:run charPrIDRef="44">${inject}${tbl}</hp:run></hp:p>`;
}

// ── 머리말: 양식 원본(native) 좌표를 그대로 사용 ──
//   양식은 쪽번호+세로제목 표(RIGHT 정렬)·좌측 띠를 절대좌표로 두고, 짝수쪽은 양식의 좌철(gutter LEFT_ONLY)
//   미러 레이아웃으로 RIGHT 표가 좌측 여백에 렌더된다. A4로 임의 보정하면 오히려 어긋나므로
//   사용자가 제시한 한컴 실측값(=양식 원본값)을 그대로 둔다.
//   홀수 표 RIGHT −26.37mm(−7474)/위15.87mm(4499), 짝수 표 RIGHT 146.04mm(41396)/위15.88mm(4502),
//   홀수 띠 145.00mm(41103), 짝수 띠 −33.00mm(−9354), 띠 위 −17.00mm(−4818), 상단칸 5.01×45.93mm(1419×13020).
function a4FixHeader(h) {
  return h; // native 좌표 유지 (양식 = 사용자 실측값)
}

// ── 절 투명표 (pageBreak: 새 페이지 여부) ──
function buildJeol(jeolTbl, jeolText, inject, pageBreak = true) {
  let tbl = jeolTbl // native 폭(34875) 그대로
    .replace(/<hp:run charPrIDRef="35"><hp:t>[^<]*<\/hp:t><\/hp:run>/, `<hp:run charPrIDRef="35"><hp:t> ${esc(jeolText)} </hp:t></hp:run>`)
    .replace(/<hp:run charPrIDRef="77"><hp:t>\(필요시\)<\/hp:t><\/hp:run>/, '');
  return `<hp:p id="0" paraPrIDRef="83" styleIDRef="6" pageBreak="${pageBreak ? 1 : 0}" columnBreak="0" merged="0"><hp:run charPrIDRef="82">${inject}${tbl}</hp:run></hp:p>`;
}

// ── 뒷표지: 샘플 발행정보 표 그대로 + 제목 교체(나머지 발행정보 유지) ──
function buildBackCover(cfg, frag) {
  const reportTitle = cfg.titleLines.join(' ');
  const blank = () => '<hp:p id="0" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="0"></hp:run></hp:p>';
  // 새 페이지 + 머리말/쪽번호 숨김
  let s = '<hp:p id="0" paraPrIDRef="0" styleIDRef="0" pageBreak="1" columnBreak="0" merged="0"><hp:run charPrIDRef="0"><hp:ctrl><hp:pageHiding hideHeader="1" hideFooter="1" hideMasterPage="0" hideBorder="0" hideFill="0" hidePageNum="1"/></hp:ctrl></hp:run></hp:p>';
  for (let k = 0; k < 16; k++) s += blank(); // 표를 하단으로 밀어줌
  let bc = (frag.backcover_tbl || '')
    .replace('<hp:t>경북연구원 정책과제 수립 연구</hp:t>', `<hp:t>${esc(reportTitle)}</hp:t>`)
    .replace(/<hp:t>\s*\(보고서 제목\)<\/hp:t>/, '<hp:t/>');
  return s + bc;
}

main().catch((e) => { console.error('❌ finalize 오류:', e.stack || e.message); process.exitCode = 1; });
