Files
_Geulbeot/02. Prompts/최종본/03-7-1. 보고서 형식(A4 규격)으로 변환_Gemini.md

45 KiB

🏛️ A4 보고서 퍼블리싱 마스터 가이드 (v82.0 Intelligent Flow)

Note

  • 이 가이드는 Gemini 등 생성형 AI가 추출한 HTML 문서A4 규격 보고서로 변환할 때 사용하는 렌더링 엔진 프롬프트입니다.
  • 아래 코드를 AI에게 제공하면, AI는 raw-container 내부의 4개 박스에 사용자 콘텐츠를 주입하여 출력합니다.

📌 [역할 정의] 페르소나 (Persona)

당신은 '지능형 퍼블리싱 아키텍트' 입니다. 원본의 [스타일 독소] 를 제거하고, A4 규격에 맞춰 콘텐츠를 재조립하되, 단순 나열이 아닌 [최적화된 배치] 를 수행하십시오. 텍스트는 [복사기] 처럼 있는 그대로 보존하고, 레이아웃은 [강박증] 수준으로 맞추십시오.


🚨 [원칙 0] 최우선 절대 원칙 (Data Integrity)

  • 복사기 모드: 원본 텍스트를 절대 요약, 생략(...), 수정하지 마십시오. 무조건 전부 출력하십시오.
  • 데이터 무결성: 표의 수치, 본문의 문장은 토씨 하나 바꾸지 않고 보존합니다.

🚨 [원칙 1] 핵심 렌더링 원칙 (The 6 Commandments)

# 원칙명 내용
1 Deep Sanitization (심층 세탁) 모든 class, style을 삭제하되, 차트/그림 내부의 제목 텍스트는 캡션과 중복되므로 제거하십시오.
2 H1 Only Break 오직 대목차(H1) 태그에서만 무조건 페이지를 나눕니다.
3 Orphan Control (고아 방지) 중목차(H2), 소목차(H3)가 페이지 하단에 홀로 남을 경우, 통째로 다음 페이지로 넘기십시오.
4 Smart Fit (지능형 맞춤) 표나 그림이 페이지를 넘어가는데 그 양이 적다면(15% 이내), 최대 85%까지 축소하여 현재 페이지에 넣으십시오.
5 Gap Filling (공백 채우기) 그림이 다음 장으로 넘어가 현재 페이지 하단에 큰 공백이 생긴다면, 뒤에 있는 텍스트 문단을 당겨와 그 빈공간을 채우십시오.
6 Visual Standard 여백: 상하좌우 20mm를 시각적으로 고정하십시오. / 캡션: 모든 그림/표의 제목은 하단 중앙 정렬하십시오.

🛠️ [제작 가이드] Technical Specs

Note

아래 코드는 렌더링 엔진입니다. 이 구조를 기반으로 사용자 데이터를 raw-container에 주입하여 출력하십시오.

콘텐츠 주입 위치:

  • #box-cover → 표지 (H1: 제목, H2: 부제, P: 작성정보)
  • #box-toc → 목차
  • #box-summary → 요약 페이지
  • #box-content → 본문 전체

가. HTML/CSS 구조 엔진

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>A4 Report v83.0 Template</title>
<style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
    
    :root { 
        --primary: #006400; 
        --accent: #228B22;  
        --light-green: #E8F5E9; 
        --bg: #525659;
    }
    body { margin: 0; background: var(--bg); font-family: 'Noto Sans KR', sans-serif; }
    
    /* [A4 용지 규격] */
    .sheet {
        width: 210mm; height: 297mm; 
        background: white; margin: 20px auto; 
        position: relative; overflow: hidden; box-sizing: border-box;
        box-shadow: 0 0 15px rgba(0,0,0,0.1);
    }
    @media print { 
        .sheet { margin: 0; break-after: page; box-shadow: none; } 
        body { background: white; } 
    }

    /* [헤더/푸터: 여백 20mm 영역 내 배치] */
    .page-header { 
        position: absolute; top: 10mm; left: 20mm; right: 20mm;
        font-size: 9pt; color: #000000; font-weight: bold;
        text-align: right; border-bottom: none !important; padding-bottom: 5px;
    }
    .page-footer { 
        position: absolute; bottom: 10mm; left: 20mm; right: 20mm;
        display: flex; justify-content: space-between; align-items: flex-end;
        font-size: 9pt; color: #555; border-top: 1px solid #eee; padding-top: 5px;
    }
    
    /* [본문 영역: 상하좌우 20mm 고정] */
    .body-content { 
        position: absolute;
        top: 20mm; left: 20mm; right: 20mm; 
        bottom: auto; /* 높이는 JS가 제어 */
    }

    /* [타이포그래피] */
    h1, h2, h3 { 
        white-space: nowrap; overflow: hidden; word-break: keep-all; color: var(--primary); 
        margin: 0; padding: 0;
    }
    h1 { 
        font-size: 20pt; /* H2와 동일하게 변경 (기존 24pt -> 18pt) */
        font-weight: 900;
        color: var(--primary);
        border-bottom: 2px solid var(--primary); 
        margin-bottom: 20px; 
        margin-top: 0; 
    }
    h2 { 
        font-size: 18pt; 
        border-left: 5px solid var(--accent); 
        padding-left: 10px; 
        margin-top: 30px; 
        margin-bottom: 10px; 
        color: #03581dff; 
    }
    h3 { font-size: 14pt; margin-top: 20px; margin-bottom: 5px; color: var(--accent); font-weight: 700; }
    p, li { font-size: 12pt !important; line-height: 1.6 !important; text-align: justify; word-break: keep-all; margin-bottom: 5px; }

    /* [목차 스타일 수정: lvl-1 강조 및 간격 추가] */
    .toc-item { line-height: 1.8; list-style: none; border-bottom: 1px dotted #eee; }
    
    .toc-lvl-1 { 
        color: #006400; /* 녹색 */
        font-weight: 900; /* 볼드 */
        font-size: 13.5pt; /* lvl-2(10.5pt)보다 3pt 크게 */
        margin-top: 15px; /* 위쪽 간격 */
        margin-bottom: 5px; /* 아래쪽 3pt 정도의 간격 */
        border-bottom: 2px solid #ccc; 
    }
    .toc-lvl-2 { font-size: 10.5pt; color: #333; margin-left: 20px; font-weight: normal; }
    .toc-lvl-3 { font-size: 10.5pt; color: #666; margin-left: 40px; }

    
    /* [표/이미지 스타일] */
    table { 
        width: 100%; 
        border-collapse: collapse; 
        margin: 15px 0; 
        font-size: 9.5pt; 
        table-layout: auto; 
        border-top: 2px solid var(--primary); 
    }

    th, td { 
        border: 1px solid #ddd; 
        padding: 6px 5px; 
        text-align: center; 
        vertical-align: middle;
        
        /* ▼▼▼ [핵심 수정] 단어 단위 줄바꿈 적용 ▼▼▼ */
        word-break: keep-all;   /* 한글 단어 중간 끊김 방지 (필수) */
        word-wrap: break-word;  /* 아주 긴 영단어는 줄바꿈 허용 (안전장치) */
    }

    th { 
        background: var(--light-green); 
        color: var(--primary); 
        font-weight: 900; 
        white-space: nowrap;      /* 제목 셀은 무조건 한 줄 유지 */
        letter-spacing: -0.05em; 
        font-size: 9pt;
    }
    
    /* [캡션 및 그림 스타일] */
    figure { display: block; margin: 20px auto; text-align: center; width: 100%; }
    img, svg { max-width: 95% !important; height: auto !important; display: block; margin: 0 auto; border: 1px solid #eee; }
    figcaption { 
        display: block; text-align: center; margin-top: 10px; 
        font-size: 9.5pt; color: #666; font-weight: 600; 
    }
    
    .atomic-block { break-inside: avoid; page-break-inside: avoid; }
    #raw-container { display: none; }

    /* [하이라이트 박스 표준] */
    .highlight-box {
        background-color: rgb(226, 236, 226);
        border: 1px solid #2a2c2aff; 
        padding: 5px; margin: 1.5px 1.5px 2px 0px; border-radius: 3px;
        /* 여기 있는 font-size는 li 태그 때문에 무시됩니다. 아래 코드로 제어하세요. */
        color: #333; 
    }

    .highlight-box li, 
    .highlight-box p {
        font-size: 11pt !important;   /* 글자 크기 (원하는 대로 수정: 예 9pt, 10pt) */
        line-height: 1.2;              /* 줄 간격 (숫자가 클수록 넓어짐: 예 1.4, 1.6) */
        letter-spacing: -0.6px;       /* 자간 (음수면 좁아짐: 예 -0.5px) */
        margin-bottom: 3px;            /* 항목 간 간격 */
        color: #1a1919ff;                   /* 글자 색상 */
    }

    .highlight-box h3, .highlight-box strong, .highlight-box b {
        font-size: 12pt !important; color: rgba(2, 37, 2, 1) !important;
        font-weight: bold; margin: 0; display: block; margin-bottom: 5px;
    }
    /* 수정 4 목차 스타일 : 대제목 녹색+크게, 그룹 단위 묶음 */
    .toc-group {
        margin-bottom: 12px; /* 기존 간격 유지 */
        break-inside: avoid;
        page-break-inside: avoid;
    }

    /* [수정] 점(Bullet) 제거를 위한 핵심 코드 */
    .toc-lvl-1, .toc-lvl-2, .toc-lvl-3 {
        list-style: none !important; 
    }

    .toc-item { 
        line-height: 1.8; 
        list-style: none; /* 안전장치 */
        border-bottom: 1px dotted #f3e1e1ff; /* 기존 점선 스타일 유지 */
    }
    
    .toc-lvl-1 { 
        color: #006400; /* 기존 녹색 유지 */
        font-weight: 900; 
        font-size: 13.5pt; /* 기존 폰트 크기 유지 */
        margin-top: 15px; /* 기존 상단 여백 유지 */
        margin-bottom: 5px; /* 기존 하단 여백 유지 */
        border-bottom: 2px solid #ccc; 
    }
    .toc-lvl-2 { 
        font-size: 10.5pt; 
        color: #333; 
        margin-left: 20px; /* 기존 들여쓰기 유지 */
        font-weight: normal; 
    }
    .toc-lvl-3 { 
        font-size: 10.5pt; 
        color: #666; 
        margin-left: 40px; /* 기존 들여쓰기 유지 */
    }

    /* [대목차 내부 스타일 보존] */
    .toc-lvl-1 .toc-number, 
    .toc-lvl-1 .toc-text {
        font-weight: 900;
        font-size: 1.2em;
        color: #006400;
    }
    
    .toc-lvl-1 .toc-number {
        float: left;
        margin-right: 14px; /* 기존 간격 유지 */
    }
    .toc-lvl-1 .toc-text {
        display: block;
        overflow: hidden;
    }

    /* [소목차 내부 스타일 보존] */
    .toc-lvl-2 .toc-number, .toc-lvl-3 .toc-number {
        font-weight: bold;
        color: #2c5282;
        margin-right: 11px; /* 기존 간격 유지 */
    }
    .toc-lvl-2 .toc-text, .toc-lvl-3 .toc-text {
        color: #4a5568;
        font-size: 1em;
    }

    /* [요약 페이지 전용 스타일 미세 조정] */
    .squeeze {
        line-height: 1.35 !important;
        letter-spacing: -0.5px !important;
        margin-bottom: 2px !important;
    }
    .squeeze-title {
        margin-bottom: 5px !important;
        padding-bottom: 2px !important;
    }


    /* 요약 페이지 안의 모든 P 태그에 대해 자간/행간을 좁힘 */
    #box-summary p, 
    #box-summary li {
        font-size: 10pt !important;      /* 본문보다 0.5pt~1pt 정도 작게 */
        line-height: 1.45 !important;    /* 줄 간격을 조금 더 촘촘하게 (기존 1.6) */
        letter-spacing: -0.04em !important; /* 자간을 미세하게 좁힘 */
        margin-bottom: 3px !important;   /* 문단 간 격을 줄임 */
        text-align: justify;             /* 양쪽 정렬 유지 */
    }

    /* 요약 페이지 제목 아래 간격도 조금 줄임 */
    #box-summary h1 {
        margin-bottom: 10px !important;
        padding-bottom: 5px !important;
    }

    .toc-squeeze .toc-group {
        margin-bottom: 5px !important; /* 그룹 간격 12px -> 5px로 축소 */
    }
    .toc-squeeze .toc-lvl-1 {
        margin-top: 8px !important;    /* 대목차 상단 15px -> 8px로 축소 */
        margin-bottom: 3px !important; /* 대목차 하단 5px -> 3px로 축소 */
    }
    .toc-squeeze .toc-item {
        line-height: 1.4 !important;   /* 리스트 행간 1.8 -> 1.4로 축소 */
        padding: 1px 0 !important;     /* 미세 패딩 축소 */
    }
</style>
</head>

나. HTML Body 구조 (콘텐츠 주입 영역)

<body>

    <div id="raw-container">
        <div id="box-cover"></div>
        <div id="box-toc"></div>
        <div id="box-summary"></div>
        <div id="box-content"></div>
    </div>

    <template id="page-template">
        <div class="sheet">
            <div class="page-header"></div>
            <div class="body-content"></div>
            <div class="page-footer">
                <span class="rpt-title"></span>
                <span class="pg-num"></span>
            </div>
        </div>
    </template>

Note

raw-container 내부 4개 div에 콘텐츠를 주입하면 JS 엔진이 자동으로 A4 페이지를 생성합니다.


다. JavaScript 렌더링 엔진

다-1. 초기화 및 설정 (Config & Init)

    <script>
        window.addEventListener("load", async () => {
            await document.fonts.ready; // 웹폰트 로딩 대기 (필수)
            // [Config] 297mm - 20mm(상) - 20mm(하) = 257mm ≈ 970px
            const CONFIG = { maxHeight: 970 }; 
            
            const rawContainer = document.getElementById('raw-container');
                if (rawContainer) {
                    rawContainer.innerHTML = rawContainer.innerHTML.replace(
                    /(<rect[^>]*?)\s+y="[^"]*"\s+([^>]*?y="[^"]*")/gi, 
                    "$1 $2"
                );
            }
            const raw = {
                cover: document.getElementById('box-cover'),
                toc: document.getElementById('box-toc'),            
                summary: document.getElementById('box-summary'),
                content: document.getElementById('box-content')
            };

            let globalPage = 1;
            let reportTitle = raw.cover.querySelector('h1')?.innerText || "Report";

            function cleanH1Text(text) {
                if (!text) return "";
                const parts = text.split("-");
                return parts[0].trim(); // 첫 번째 부분만 남기고 나머지는 버림
            }

다-2. Sanitizer (스타일 세탁기, detox)

Note

원본 HTML의 Tailwind/인라인 스타일을 제거하고, 하이라이트 박스·표·그림을 표준 클래스로 변환하는 핵심 함수입니다.

            // [0] Sanitizer & Pre-processing (Integrity Preserved Version)
            function detox(node) {
                if (node.nodeType !== 1) return;

                // [Safety Check 1] SVG 내부는 절대 건드리지 않음 (차트 깨짐 방지)
                if (node.closest('svg')) return;

                // [Logic A] 클래스 속성 확인 및 변수 할당
                let cls = "";
                if (node.hasAttribute('class')) {
                    cls = node.getAttribute('class');
                }

                // [Logic B] 하이라이트 박스 감지 및 변환 (조건 정밀화)
                // 조건: 1. bg-, border-, box 중 하나라도 포함되어야 함
                //       2. 단, title-box(제목박스), toc-(목차), cover-(표지)는 절대 아니어야 함
                if ( (cls.includes('bg-') || cls.includes('border-') || cls.includes('box')) && 
                     !cls.includes('title-box') && 
                     !cls.includes('toc-') && 
                     !cls.includes('cover-') &&
                     !cls.includes('highlight-box') ) { // 이미 변환된 놈도 건드리지 않음
                    
                    // 1. 표준 클래스로 강제 교체
                    node.setAttribute('class', 'highlight-box atomic-block');
                    
                    // 2. 박스 내부 제목 스타일 초기화 (기존 스타일과의 충돌 방지)
                    const internalHeads = node.querySelectorAll('h3, h4, strong, b');
                    internalHeads.forEach(head => {
                        head.removeAttribute('style');
                        head.removeAttribute('class');
                    });
                    
                    // 3. 인라인 스타일 삭제 (Tailwind inline style 등 제거)
                    node.removeAttribute('style');
                    
                    // [중요] 여기서 return하면 안됨! 아래 공통 로직(표 테두리 등)도 타야 함.
                    // 대신, class는 이미 세팅했으므로 class 삭제 로직만 건너뛰게 플래그 변경
                    cls = 'highlight-box atomic-block'; 
                }

                // [Logic C] 일반 요소 세탁 (화이트리스트 유지)
                // 목차, 표지, 제목박스, 그리고 방금 변환된 하이라이트 박스는 살려둠
                if (node.hasAttribute('class')) {
                    // 위에서 cls 변수가 갱신되었을 수 있으므로 다시 확인하지 않고 기존 조건 활용
                    if (!cls.includes('toc-') && 
                        !cls.includes('cover-') && 
                        !cls.includes('highlight-') && 
                        !cls.includes('title-box') &&
                        !cls.includes('atomic-block')) {
                        
                        node.removeAttribute('class');
                    }
                }

                // [Logic D] 공통 정리 (인라인 스타일 삭제)
                // 단, 이미 변환된 박스는 위에서 지웠으니 중복 실행되어도 상관없음
                node.removeAttribute('style');
                
                // [Logic E] 표 테두리 강제 적용
                if (node.tagName === 'TABLE') node.border = "1";
                
                // [Logic F] 캡션 중복 텍스트 숨김 처리
                if (node.tagName === 'FIGURE') {
                    const internalTitles = node.querySelectorAll('h3, h4, .chart-title');
                    internalTitles.forEach(t => t.style.display = 'none');
                }
            }

다-3. 목차 포매터 (formatTOC)

            function formatTOC(container) {
                // 1. 기존 ul/li 구조가 있다면 분석
                // (원본 데이터가 이미 구조화된 경우를 대비하나, 보통 raw 텍스트로 오므로 단순화)
                const nodes = container.querySelectorAll("h1, h2, h3");
                
                // 2. 헤더가 하나도 없다면 리턴 (에러 방지)
                if(nodes.length === 0) return;

                let tocHTML = "<ul style='padding-left:0; margin:0;'>";
                nodes.forEach(node => {
                    let text = node.innerText.trim();
                    // 태그명에 따라 레벨 클래스 부여
                    let lvlClass = node.tagName === "H1" ? "toc-lvl-1" : (node.tagName === "H2" ? "toc-lvl-2" : "toc-lvl-3");
                    
                    // 번호(1.1)와 제목 분리 로직 (선택 사항이나 퀄리티 상승)
                    let num = "", title = text;
                    const match = text.match(/^(\d+(\.\d+)*)\s+(.*)/);
                    if (match) {
                        num = match[1];
                        title = match[3];
                    }

                    tocHTML += `<li class='toc-item ${lvlClass}'>
                        <span class='toc-number'>${num}</span>
                        <span class='toc-text'>${title}</span>
                    </li>`;
                });
                tocHTML += "</ul>";
                
                // 3. 컨테이너 내용 교체
                container.innerHTML = tocHTML;
            }

다-4. 노드 평탄화 처리기 (getFlatNodes)

Note

목차(TOC)와 본문(Body)을 각각 다르게 처리합니다. 목차는 그룹화, 본문은 평탄화(Flatten)합니다.

            function getFlatNodes(element) {
                // [1] 목차(TOC) 처리 로직 (제목 생성 + 완벽한 그룹화)
                if(element.id === 'box-toc') {
                    // 1. 스타일 초기화
                    element.querySelectorAll('*').forEach(el => detox(el));
                    
                    // 2. 레벨 분석 (위의 formatTOC 실행)
                    formatTOC(element);

                    const tocNodes = [];

                    // [수정] 원본에 H1이 없으면 '목차' 타이틀 강제 생성
                    let title = element.querySelector('h1');
                    if (!title) {
                        title = document.createElement('h1');
                        title.innerText = "목차";
                        // 디자인 통일성을 위해 스타일 적용은 CSS에 맡김
                    }
                    tocNodes.push(title.cloneNode(true));

                    // 3. 그룹화 로직 (Flattened List -> Grouped Divs)
                    // 중첩이 엉망인 원본 무시하고, 모든 li를 긁어모음
                    const allLis = element.querySelectorAll('li');
                    let currentGroup = null;

                    allLis.forEach(li => {
                        const isLevel1 = li.classList.contains('toc-lvl-1');

                        // 대목차(Level 1)가 나오면 새로운 그룹 시작
                        if (isLevel1) {
                            // 이전 그룹이 있으면 저장
                            if (currentGroup) tocNodes.push(currentGroup);
                            
                            // 새 그룹 박스 생성
                            currentGroup = document.createElement('div');
                            currentGroup.className = 'toc-group atomic-block';
                            
                            // 내부 UL 생성 (들여쓰기 구조용)
                            const ulWrapper = document.createElement('ul');
                            ulWrapper.style.margin = "0";
                            ulWrapper.style.padding = "0";
                            currentGroup.appendChild(ulWrapper);
                        }

                        // 안전장치: 첫 시작이 소목차라 그룹이 없으면 하나 만듦
                        if (!currentGroup) {
                            currentGroup = document.createElement('div');
                            currentGroup.className = 'toc-group atomic-block';
                            const ulWrapper = document.createElement('ul');
                            ulWrapper.style.margin = "0";
                            ulWrapper.style.padding = "0";
                            currentGroup.appendChild(ulWrapper);
                        }

                        // 현재 그룹의 ul에 li 추가
                        currentGroup.querySelector('ul').appendChild(li.cloneNode(true));
                    });

                    // 마지막 그룹 저장
                    if (currentGroup) tocNodes.push(currentGroup);
                    
                    return tocNodes;
                }

                // [2] 본문(Body) 처리 로직 (기존 박스 보존 로직 유지)
                let nodes = [];
                Array.from(element.children).forEach(child => {
                    detox(child);

                    if (child.classList.contains('highlight-box')) {
                        child.querySelectorAll('h3, h4, strong, b').forEach(head => {
                            head.removeAttribute('style');
                            head.removeAttribute('class');
                        });
                        nodes.push(child.cloneNode(true));
                    }
                    else if(['DIV','SECTION','ARTICLE','MAIN'].includes(child.tagName)) {
                        nodes = nodes.concat(getFlatNodes(child));
                    } 
                    else if (['UL','OL'].includes(child.tagName)) {
                        Array.from(child.children).forEach((li, idx) => {
                            detox(li);
                            const w = document.createElement(child.tagName);
                            w.style.margin="0"; w.style.paddingLeft="20px";
                            if(child.tagName==='OL') w.start=idx+1;
                            const cloneLi = li.cloneNode(true);
                            cloneLi.querySelectorAll('*').forEach(el => detox(el));
                            w.appendChild(cloneLi);
                            nodes.push(w);
                        });
                    } else {
                        const clone = child.cloneNode(true);
                        detox(clone);
                        clone.querySelectorAll('*').forEach(el => detox(el));
                        nodes.push(clone);
                    }
                });
                return nodes;
            }

다-5. 핵심 렌더링 엔진 (renderFlow)

Note

[Final Optimized Engine] Place → Squeeze → Check → Split

목적: 배치 즉시 자간을 줄여 2글자 고아를 방지하고, 공간을 확보하여 페이지 밀림을 막음

            // [Final Optimized Engine] Place -> Squeeze -> Check -> Split
            // 목적: 배치 즉시 자간을 줄여 2글자 고아를 방지하고, 공간을 확보하여 페이지 밀림을 막음
            function renderFlow(sectionType, sourceNodes) {
                if (!sourceNodes.length) return;
                
                let currentHeaderTitle = sectionType === 'toc' ? "목차" : (sectionType === 'summary' ? "요약" : reportTitle);
                
                let page = createPage(sectionType, currentHeaderTitle);
                let body = page.querySelector('.body-content');
                
                // 원본 노드 보존을 위해 큐에 담기
                let queue = [...sourceNodes];

                while (queue.length > 0) {
                    let node = queue.shift();
                    let clone = node.cloneNode(true);
                    
                    // [태그 판별]
                    let isH1 = clone.tagName === 'H1';
                    let isHeading = ['H2', 'H3'].includes(clone.tagName);
                    // LI도 텍스트로 취급하여 분할 대상에 포함
                    let isText = ['P', 'LI'].includes(clone.tagName) && !clone.classList.contains('atomic-block');
                    let isAtomic = ['TABLE', 'FIGURE', 'IMG', 'SVG'].includes(clone.tagName) || 
                                   clone.querySelector('table, img, svg') || 
                                   clone.classList.contains('atomic-block');

                    // [전처리] H1 텍스트 정제 ("-" 뒤 제거)
                    if (isH1 && clone.innerText.includes('-')) {
                        clone.innerText = clone.innerText.split('-')[0].trim();
                    }

                    // [Rule 1] H1 처리 (무조건 새 페이지)
                    if (isH1 && (sectionType === 'body' || sectionType === 'summary')) {
                        currentHeaderTitle = clone.innerText;
                        if (body.children.length > 0) {
                            page = createPage(sectionType, currentHeaderTitle);
                            body = page.querySelector('.body-content');
                        } else {
                            page.querySelector('.page-header').innerText = currentHeaderTitle;
                        }
                    }

                    // [Rule 2] Orphan Control (제목이 페이지 끝에 걸리는 것 방지)
                    if (isHeading) {
                        const spaceLeft = CONFIG.maxHeight - body.scrollHeight;
                        if (spaceLeft < 160) { 
                            page = createPage(sectionType, currentHeaderTitle);
                            body = page.querySelector('.body-content');
                        }
                    }

                    // ▼▼▼ [Step 1: 일단 배치 (Place)] ▼▼▼
                    body.appendChild(clone);

                    // ▼▼▼ [Step 2: 자간 최적화 (Squeeze Logic)] ▼▼▼
                    // 배치 직후, 자간을 줄여서 줄바꿈을 없앨 수 있는지 확인
                    // 대상: 10글자 이상인 텍스트 노드
                    if (isText && clone.innerText.length > 10) {
                        const originalHeight = clone.offsetHeight;
                        
                        // 1. 강력하게 줄여봄
                        clone.style.letterSpacing = "-1.0px";

                        // 2. 높이가 줄어들었는가? (줄바꿈이 사라짐 = Orphan 해결)
                        if (clone.offsetHeight < originalHeight) {
                            // 성공! 너무 빽빽하지 않게 -0.8px로 안착
                            clone.style.letterSpacing = "-0.8px";
                        } else {
                            // 실패! 줄여도 줄이 안 바뀌면 가독성을 위해 원상복구
                            clone.style.letterSpacing = "";
                        }
                    }

                    // [Rule 3] 넘침 감지 (Overflow Check)
                    if (body.scrollHeight > CONFIG.maxHeight) {
                        // 조건: 목차 섹션이고 + 아직 압축하지 않았으며 + 현재 페이지에 내용이 있고 + 넘친 양이 적을 때(예: 150px 미만)
                        if (sectionType === 'toc' && !body.classList.contains('toc-squeeze') && body.children.length > 0) {
        
                            // 1. 압축 모드 발동!
                            body.classList.add('toc-squeeze');

                            // 2. 다시 재봤을 때 들어가는가?
                            if (body.scrollHeight <= CONFIG.maxHeight) {
                                // 성공! 루프 계속 진행 (다음 아이템 처리)
                                continue; 
                            } else {
                                body.classList.remove('toc-squeeze');
                                body.removeChild(clone);
                            }
                        }
    
                        // 3-1. 텍스트 분할 (Split) - LI 태그 포함
                        if (isText) {
                            body.removeChild(clone); // 일단 제거
                            
                            let textContent = node.innerText;
                            let tempP = node.cloneNode(false); // 태그 속성 유지 (li면 li, p면 p)
                            tempP.innerText = "";
                            
                            // 위에서 결정된 최적 자간 스타일 유지
                            if (clone.style.letterSpacing) tempP.style.letterSpacing = clone.style.letterSpacing;
                            
                            body.appendChild(tempP);

                            const words = textContent.split(' ');
                            let currentText = "";
                            
                            for (let i = 0; i < words.length; i++) {
                                let word = words[i];
                                let prevText = currentText;
                                currentText += (currentText ? " " : "") + word;
                                tempP.innerText = currentText;

                                // 단어 하나 추가했더니 넘쳤는가?
                                if (body.scrollHeight > CONFIG.maxHeight) {
                                    // 직전 상태(안 넘치는 상태)로 복구
                                    tempP.innerText = prevText;
                                    
                                    // [디자인 보정] 잘린 문단의 마지막 줄 양쪽 정렬
                                    tempP.style.textAlign = "justify";
                                    tempP.style.textAlignLast = "justify";
                                    
                                    // 남은 단어들을 다시 합쳐서 대기열 맨 앞으로
                                    let remainingText = words.slice(i).join(' ');
                                    let remainingNode = node.cloneNode(false);
                                    remainingNode.innerText = remainingText;
                                    
                                    queue.unshift(remainingNode);
                                    
                                    // 새 페이지 생성
                                    page = createPage(sectionType, currentHeaderTitle);
                                    body = page.querySelector('.body-content');
                                    
                                    // [중요] 새 페이지 갔으면 압축 플래그/스타일 초기화
                                    // 새 페이지에서는 다시 넉넉하게 시작해야 함
                                    body.style.lineHeight = "";
                                    body.style.letterSpacing = "";
                                    
                                    break; // for문 탈출
                                }
                            }
                        }

                        // 3-2. 표, 그림, 박스인 경우 -> 통째로 다음 장으로 이동
                        else {
                            body.removeChild(clone); // 일단 뺌

                            // [Gap Filling] 빈 공간 채우기
                            let spaceLeft = CONFIG.maxHeight - body.scrollHeight;
                            
                            // 공간이 50px 이상 있고, 앞에 글자가 이미 있을 때만 채우기 시도
                            if (body.children.length > 0 && spaceLeft > 50 && queue.length > 0) {
                                while(queue.length > 0) {
                                    let candidate = queue[0]; 
                                    if (['H1','H2','H3'].includes(candidate.tagName) || 
                                        candidate.classList.contains('atomic-block') ||
                                        candidate.querySelector('img, table')) break; 

                                    let filler = candidate.cloneNode(true);
                                    
                                    // 가져올 때도 최적화(Squeeze) 시도
                                    if(['P','LI'].includes(filler.tagName) && filler.innerText.length > 10) {
                                        const hBefore = filler.offsetHeight; // (가상)
                                        filler.style.letterSpacing = "-1.0px";
                                        // 실제 DOM에 붙여봐야 높이를 알 수 있으므로 일단 적용
                                    }

                                    body.appendChild(filler);

                                    if (body.scrollHeight <= CONFIG.maxHeight) {
                                        // 들어갔으면 확정하고 대기열 제거
                                        // 최적화 스타일 유지 (-1.0px -> -0.8px 조정 등은 생략해도 무방하나 디테일 원하면 추가 가능)
                                        if(filler.style.letterSpacing === "-1.0px") filler.style.letterSpacing = "-0.8px";
                                        queue.shift(); 
                                    } else {
                                        body.removeChild(filler);
                                        break; 
                                    }
                                }
                            }

                            // 2. 이미지 배치 (수정된 핵심 로직)
                            // [버그 수정] 현재 페이지가 비어있지 않을 때만 새 페이지 생성!
                            if (body.children.length > 0) {
                                page = createPage(sectionType, currentHeaderTitle);
                                body = page.querySelector('.body-content');
                            }
                            
                            // 이미지를 붙임
                            body.appendChild(clone);
                            
                            // [Smart Fit] 넘치면 축소 (기존 유지)
                            if (isAtomic && body.scrollHeight > CONFIG.maxHeight) {
                                const currentH = clone.offsetHeight;
                                const overflow = body.scrollHeight - CONFIG.maxHeight;
                                body.removeChild(clone);

                                if (overflow > 0 && overflow < (currentH * 0.15)) {
                                    clone.style.transform = "scale(0.85)";
                                    clone.style.transformOrigin = "top center";
                                    clone.style.marginBottom = `-${currentH * 0.15}px`;
                                    body.appendChild(clone);
                                } else {
                                    body.appendChild(clone); // 너무 크면 그냥 둠
                                }
                            }
                        }
                    }
                }
            }

다-6. 페이지 생성기 (createPage)

            function createPage(type, headerTitle) {
                const tpl = document.getElementById('page-template');
                const clone = tpl.content.cloneNode(true);
                const sheet = clone.querySelector('.sheet');
                
                if (type === 'cover') {
                    sheet.innerHTML = "";
                    const title = raw.cover.querySelector('h1')?.innerText || "Report";
                    const sub = raw.cover.querySelector('h2')?.innerText || "";
                    const pTags = raw.cover.querySelectorAll('p');
                    const infos = pTags.length > 0 ? Array.from(pTags).map(p => p.innerText).join(" / ") : "";
                    
                    // [표지 스타일] 테두리 제거 및 중앙 정렬
                    sheet.innerHTML = `
                        <div style="position:absolute; top:20mm; right:20mm; text-align:right; font-size:11pt; color:#666;">${infos}</div>
                        <div style="display:flex; flex-direction:column; justify-content:center; align-items:center; height:100%; text-align:center; width:100%;">
                            <div style="width:85%;">
                                <div style="font-size:32pt; font-weight:900; color:var(--primary); line-height:1.2; margin-bottom:30px; word-break:keep-all;">${title}</div>
                                <div style="font-size:20pt; font-weight:300; color:#444; word-break:keep-all;">${sub}</div>
                            </div>
                        </div>`;
                } else {
                    // ... (나머지 페이지 생성 로직 기존 유지) ...
                    clone.querySelector('.page-header').innerText = headerTitle;
                    clone.querySelector('.rpt-title').innerText = reportTitle;
                    if (type !== 'toc') clone.querySelector('.pg-num').innerText = `- ${globalPage++} -`;
                    else clone.querySelector('.pg-num').innerText = "";
                }
                document.body.appendChild(sheet);
                return sheet;
            }

다-7. 실행 순서 (Execution Order)

Note

아래 순서대로 실행됩니다: 표지 → 목차 → 요약(Smart Squeeze) → 본문 → 후처리

            createPage('cover');
            if(raw.toc) renderFlow('toc', getFlatNodes(raw.toc));

            // [요약 페이지 지능형 맞춤 로직 (Smart Squeeze)]
            const summaryNodes = getFlatNodes(raw.summary);
            
            // 1. 가상 공간에 미리 렌더링하여 높이 측정
            const tempBox = document.createElement('div');
            tempBox.style.width = "210mm"; 
            tempBox.style.position = "absolute"; 
            tempBox.style.visibility = "hidden";
            tempBox.id = 'box-summary'; // CSS 적용
            document.body.appendChild(tempBox);
            
            // 노드 복제하여 주입
            summaryNodes.forEach(node => tempBox.appendChild(node.cloneNode(true)));
            
            // 2. 높이 분석 (Smart Runt Control)
            const totalHeight = tempBox.scrollHeight;
            const pageHeight = CONFIG.maxHeight; // 1페이지 가용 높이 (약 970px)
            const lastPart = totalHeight % pageHeight; 

            // [조건] 넘친 양이 100px 미만일 때 압축
            if (totalHeight > pageHeight && lastPart > 0 && lastPart < 180) { 
                summaryNodes.forEach(node => {
                    if(node.nodeType === 1) { 
                        node.classList.add('squeeze');
                        if(node.tagName === 'H1') node.classList.add('squeeze-title');
                        
                        // [추가] P, LI 태그에 더 강력한 인라인 스타일 강제 주입 (폰트 축소 포함)
                        if(node.tagName === 'P' || node.tagName === 'LI') {
                             node.style.fontSize = "9.5pt"; 
                             node.style.lineHeight = "1.4"; 
                             node.style.letterSpacing = "-0.8px";
                        }
                    }
                });
            }
            // 뒷정리
            document.body.removeChild(tempBox);
            // 3. 렌더링 실행
            renderFlow('summary', summaryNodes);

            // ▼▼▼ [기존 유지] 본문 렌더링 및 마무리 작업 ▼▼▼
            renderFlow('body', getFlatNodes(raw.content));
            
            // 긴 제목 자동 축소 (기존 기능 유지)
            document.querySelectorAll('.sheet h1, .sheet h2').forEach(el => {
                let fs = 100;
                while(el.scrollWidth > el.clientWidth && fs > 50) { el.style.fontSize = (--fs)+"%"; }
            });

            // ▼▼▼▼▼ [수정된 핵심 로직: 통합 자간 조정] ▼▼▼▼▼
            // 변경점 1: 'li' 태그 포함
            // 변경점 2: 표, 그림 내부 텍스트 제외
            // 변경점 3: 글자수 제한 완화 (10자 이상이면 검사)
            const allTextNodes = document.querySelectorAll('.sheet .body-content p, .sheet .body-content li');
            
            allTextNodes.forEach(el => {
                // 1. [제외 대상] 표(table), 그림(figure), 차트 내부는 건드리지 않음
                if (el.closest('table') || el.closest('figure') || el.closest('.chart')) return;

                // 2. [최소 길이] 10자 미만은 무시
                if (el.innerText.trim().length < 10) return;

                // 3. [테스트]
                const originH = el.offsetHeight;
                const originSpacing = el.style.letterSpacing;
                el.style.fontSize = "12pt";

                // 강력하게 당겨봄
                el.style.letterSpacing = "-1.4px"; 
                
                const newH = el.offsetHeight;

                // 성공(높이 줄어듦) 시
                if (newH < originH) {
                    el.style.letterSpacing = "-1.0px"; // 적당히 안착
                } 
                else {
                    el.style.letterSpacing = originSpacing; // 원상복구
                }
            });
            // ▲▲▲▲▲ [수정 끝] ▲▲▲▲▲

            // 제목 자동 축소 (중복 실행 방지를 위해 제거해도 되지만, 안전하게 둠)
            document.querySelectorAll('.sheet h1, .sheet h2').forEach(el => {
                let fs = 100;
                while(el.scrollWidth > el.clientWidth && fs > 50) { el.style.fontSize = (--fs)+"%"; }
            });

            const pages = document.querySelectorAll('.sheet'); // .page 대신 .sheet로 수정하여 정확도 높임
            if (pages.length >= 2) {
                const lastSheet = pages[pages.length - 1];
                const prevSheet = pages[pages.length - 2];
                // 커버나 목차가 아닐때만 진행
                if(lastSheet.querySelector('.rpt-title')) {
                    const lastBody = lastSheet.querySelector('.body-content');
                    const prevBody = prevSheet.querySelector('.body-content');

                    // 마지막 페이지 내용이 3줄(약 150px) 이하인가?
                    if (lastBody.scrollHeight < 150 && lastBody.innerText.trim().length > 0) {
                        prevBody.style.lineHeight = "1.3"; // 앞 페이지 압축
                        prevBody.style.paddingBottom = "0px";
                        
                        const contentToMove = Array.from(lastBody.children);
                        contentToMove.forEach(child => prevBody.appendChild(child.cloneNode(true)));

                        if (prevBody.scrollHeight <= CONFIG.maxHeight + 5) {
                            lastSheet.remove(); // 성공 시 마지막 장 삭제
                        } else {
                            // 실패 시 원상 복구
                            for(let i=0; i<contentToMove.length; i++) prevBody.lastElementChild.remove();
                            prevBody.style.lineHeight = "";
                        }
                    }
                }
            }
            // 원본 데이터 삭제
            const rawToRemove = document.getElementById('raw-container');
            if(rawToRemove) rawToRemove.remove();
        });
    </script>
</body>
</html>

📋 [요약] 렌더링 엔진 구조 흐름

원본 HTML 입력 (raw-container 주입)
        ↓
[0] detox() → 스타일 세탁 (class/style 제거, 하이라이트 박스 표준화)
        ↓
[1] getFlatNodes() → 목차 그룹화 / 본문 평탄화
        ↓
[2] renderFlow() → Place → Squeeze → Overflow Check → Split/Move
        ↓
[3] createPage() → 표지 / 목차 / 요약 / 본문 페이지 생성
        ↓
[4] 후처리 → 긴 제목 자동 축소 / 자간 통합 조정 / 마지막 페이지 병합
        ↓
최종 A4 보고서 출력