✨ Update 02. Prompts with the latest refined prompts from D:\_Geulbeot
This commit is contained in:
727
02. Prompts/최종본/01-2. 단일 AI 활용 보고서 작성 프롬프트.md
Normal file
727
02. Prompts/최종본/01-2. 단일 AI 활용 보고서 작성 프롬프트.md
Normal file
@@ -0,0 +1,727 @@
|
||||
# 내부 자료 기반 분석 보고서 작성 가이드
|
||||
|
||||
> 이 프롬프트는 **(사전) 요청사항.md** 파일 및 첨부된 관련 자료와 함께 제공됩니다.
|
||||
> 프롬프트의 모든 지시는 해당 파일과 자료를 기반으로 수행하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 페르소나 설정
|
||||
|
||||
**(사전) 요청사항.md**의 분야·전문성 힌트와 첨부된 자료를 읽고, 아래를 수행하십시오.
|
||||
|
||||
1. 자료의 분야를 파악하십시오.
|
||||
2. 자료의 성격을 파악하십시오.
|
||||
3. 파악된 분야와 성격에 맞는 전문가 페르소나를 선언하십시오.
|
||||
4. (사전) 요청사항에 힌트가 있으면 이를 반영하고, 없으면 자료에서 자동 파악하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 보고서 성격
|
||||
|
||||
**(사전) 요청사항.md**에 기재된 목적·대상 독자·성격을 기준으로 보고서의 방향을 설정하십시오.
|
||||
|
||||
기재가 없는 경우 아래를 기본값으로 적용하십시오.
|
||||
|
||||
- 대상 : 사내 임직원
|
||||
- 성격 : 현황 문제점 분석, 주제 학습과 이해, 실무 인사이트 도출
|
||||
- 이 보고서는 특정 방향을 제시하기보다, 향후 전략 수립을 위한 전단계적 관찰과 다각적 검토의 성격을 지닙니다.
|
||||
|
||||
---
|
||||
|
||||
## 출처 및 자료 활용 원칙
|
||||
|
||||
제공된 자료의 내용을 **우선적으로** 활용하십시오.
|
||||
|
||||
**작성 과정에서의 내부 규칙 (작성자에게는 보이지 않는 처리):**
|
||||
|
||||
- 자료에 근거한 내용과 AI 자체 판단을 내부적으로 구분하며 작성하십시오.
|
||||
- 자료 간 내용이 상충할 경우 임의로 한쪽을 선택하지 말고, 양쪽 입장을 모두 정리하십시오.
|
||||
- 자료에 없는 내용을 사실인 것처럼 작성하지 마십시오.
|
||||
|
||||
**최종 보고서의 출력 규칙:**
|
||||
|
||||
- 최종 보고서 본문에는 `[자료-001]`, `[AI 판단]`, `[상충]` 같은 내부 태그를 표시하지 마십시오.
|
||||
- 출처가 필요한 경우, 본문 흐름 안에서 자연스럽게 녹이십시오.
|
||||
(예: "국토교통부 통계에 따르면 ~", "실무 현장에서는 ~로 알려져 있음")
|
||||
- 단, **STEP 1 자료 분석 보고** 단계에서는 작성자에게 출처와 쟁점을 명확히 보여주기 위해 태그를 사용하십시오.
|
||||
|
||||
**비정형 출처 기준:**
|
||||
|
||||
- SNS, 블로그, 언론자료 등은 사례 참고용으로만 사용하십시오.
|
||||
- 분석과 인사이트 도출은 신뢰할 수 있는 자료나 실무 기반 내용에서 하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 문체 기준
|
||||
|
||||
기본 문체는 **보고체(간결체)**입니다. 전체 보고서에 일관 적용하십시오.
|
||||
|
||||
| 항목 | 기준 | 예시 |
|
||||
|------|------|------|
|
||||
| 문장 종결 | 명사형 또는 동사 원형 종결 | ~으로 나타남, ~임, ~필요 |
|
||||
| 문장 길이 | 1문장 2줄 이내 | 길면 두 문장으로 분리 |
|
||||
| 주어 생략 | 주어 반복 시 생략 | "분석 결과, ~로 확인됨" |
|
||||
| 수동태 | 적극 사용 | ~로 분석됨, ~으로 판단됨 |
|
||||
| 경어·구어 | **절대 사용 금지** | ~합니다, ~입니다, ~것 같다 금지 |
|
||||
| 숫자 표기 | 아라비아 숫자 | 3개, 15% |
|
||||
|
||||
---
|
||||
|
||||
## 보고서 형식
|
||||
|
||||
**계층 구조**
|
||||
|
||||
| 계층 | 표기 |
|
||||
|------|------|
|
||||
| 장 (Chapter) | 1, 2, 3 ... |
|
||||
| 절 (Section) | 1.1, 1.2, 2.1 ... |
|
||||
| 항 (Clause) | 가, 나, 다 ... |
|
||||
| 항 하위 | ▢ → ㅇ → - |
|
||||
|
||||
**문단 및 표 처리**
|
||||
|
||||
- 1개 문단은 3~5문장으로 구성하십시오.
|
||||
- 수치가 3개 이상 나열될 경우 반드시 표로 정리하십시오.
|
||||
- 표에는 출처를 본문 흐름 안에서 자연스럽게 명기하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 작업 절차
|
||||
|
||||
아래 단계를 순서대로 수행하십시오.
|
||||
각 단계는 작성자의 확인 후 다음 단계로 진행합니다.
|
||||
|
||||
---
|
||||
|
||||
### [STEP 1] 자료 분석 및 페르소나 선언
|
||||
|
||||
제공된 자료와 (사전) 요청사항을 읽고 아래 형식으로 보고하십시오.
|
||||
|
||||
> 이 단계에서만 내부 태그([자료-001], [상충] 등)를 사용하여 작성자에게 명확히 보여주십시오.
|
||||
|
||||
```
|
||||
[자료 분석 완료]
|
||||
|
||||
▣ 페르소나 선언
|
||||
- 분야 :
|
||||
- 선언 : "나는 ~~ 분야의 ~~ 전문가입니다."
|
||||
|
||||
▣ 자료별 핵심 내용
|
||||
- [자료-001] : 핵심 내용
|
||||
- [자료-002] : 핵심 내용
|
||||
|
||||
▣ 식별된 문제점·쟁점
|
||||
- 문제점 1 : 내용 / 근거 자료
|
||||
- 문제점 2 : 내용 / 근거 자료
|
||||
|
||||
▣ 자료 간 상충 사항 (있는 경우만)
|
||||
- [상충] : [자료-001]의 입장 vs [자료-002]의 입장
|
||||
|
||||
▣ 보고서에 활용 가능한 수치·사례
|
||||
- 수치/사례 : 근거 자료
|
||||
```
|
||||
|
||||
보고 후 작성자의 확인을 기다리십시오.
|
||||
|
||||
---
|
||||
|
||||
### [STEP 2] 목차 설계
|
||||
|
||||
STEP 1의 분석 결과와 (사전) 요청사항의 포함 키워드·보고서 흐름을 바탕으로 목차를 설계하십시오.
|
||||
|
||||
```
|
||||
[목차 설계 기준]
|
||||
- 장(Chapter) → 절(Section) 구조로 구성하십시오.
|
||||
- 자료에서 충분히 뒷받침되는 항목만 목차에 포함하십시오.
|
||||
- (사전) 요청사항에 보고서 흐름이 지정되어 있으면 이를 반영하십시오.
|
||||
- 지정되어 있지 않으면 논리적 흐름(배경 → 현황 → 분석 → 시사점)을 기본으로 하되, 자료 성격에 따라 조정하십시오.
|
||||
```
|
||||
|
||||
목차를 제시하고 작성자의 확인을 기다리십시오.
|
||||
수정 요청 시 반영 후 재제시하십시오.
|
||||
|
||||
---
|
||||
|
||||
### [STEP 3] 절 단위 본문 작성
|
||||
|
||||
확정된 목차의 첫 번째 절부터 순서대로 본문을 작성하십시오.
|
||||
|
||||
```
|
||||
[작성 규칙]
|
||||
- 한 번에 1개 절씩만 작성하십시오.
|
||||
- 최종 보고서 본문에는 내부 태그를 표시하지 마십시오.
|
||||
- 출처는 본문 흐름 안에서 자연스럽게 녹이십시오.
|
||||
- 작성한 절을 제시하고 작성자의 확인을 기다리십시오.
|
||||
- 수정 요청 시 해당 절만 수정하십시오. 다른 절을 건드리지 마십시오.
|
||||
- 확인 완료 시 다음 절로 이동하십시오.
|
||||
```
|
||||
|
||||
모든 절 작성이 완료되면 완료를 보고하십시오.
|
||||
|
||||
---
|
||||
|
||||
### [STEP 4] 전체 검토 및 마무리
|
||||
|
||||
모든 절 작성이 완료되면 아래 항목을 검토하십시오.
|
||||
|
||||
```
|
||||
[전체 검토]
|
||||
|
||||
✅ 맥락 흐름 : 장 간 연결 자연스러움 / 어색한 부분 X건
|
||||
✅ 중복 내용 : 절 간 중복 없음 / 중복 X건
|
||||
✅ 문체 통일 : 보고체 준수 / 위반 X건
|
||||
✅ 수치 일관성 : 수치·용어 통일 / 불일치 X건
|
||||
✅ 자료 반영 : (사전) 요청사항의 키워드 전부 반영 / 미반영 X건
|
||||
|
||||
⚠️ 수정 필요 항목 (있는 경우만)
|
||||
- 해당 절 : 사유 및 수정 제안
|
||||
```
|
||||
|
||||
검토 결과를 보고하고, 작성자의 최종 확인을 기다리십시오.
|
||||
|
||||
---
|
||||
|
||||
### [STEP 5] 최종 출력 — A4 HTML 보고서
|
||||
|
||||
작성자의 최종 확인이 완료되면, 아래 HTML/CSS/JS 규격에 따라 A4 보고서 HTML 파일을 출력하십시오.
|
||||
|
||||
---
|
||||
|
||||
#### 5-A. A4 렌더링 엔진 핵심 원칙
|
||||
|
||||
출력 HTML에 포함된 JS 렌더링 엔진은 아래 원칙으로 동작합니다.
|
||||
본문 HTML은 이 원칙에 맞는 구조여야 합니다.
|
||||
|
||||
| 원칙 | 설명 |
|
||||
|------|------|
|
||||
| **Deep Sanitization** | 모든 class, style을 삭제하고 표준 스타일로 재적용. SVG 내부는 제외 |
|
||||
| **H1 Only Break** | 대목차(H1) 태그에서만 무조건 페이지를 나눔 |
|
||||
| **Orphan Control** | H2, H3가 페이지 하단에 홀로 남으면 다음 페이지로 넘김 |
|
||||
| **Smart Fit** | 표·그림이 15% 이내로 넘치면 85%까지 축소하여 현재 페이지에 배치 |
|
||||
| **Gap Filling** | 그림이 넘어가 빈 공간이 생기면 뒤 텍스트 문단을 당겨와 채움 |
|
||||
| **Visual Standard** | 여백 상하좌우 20mm, 모든 그림·표 캡션은 하단 중앙 정렬 |
|
||||
|
||||
---
|
||||
|
||||
#### 5-B. HTML 전체 구조
|
||||
|
||||
출력 HTML은 아래 구조를 정확히 따르십시오.
|
||||
`raw-container` 안의 4개 박스에 콘텐츠를 주입하면, JS가 이를 읽어 A4 페이지로 조립합니다.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>보고서 제목</title>
|
||||
<style>
|
||||
/* === 5-C의 CSS 전문을 여기에 삽입 === */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ① 원본 데이터 컨테이너 (화면에 표시되지 않음, JS가 읽는 원본) -->
|
||||
<div id="raw-container">
|
||||
<div id="box-cover">
|
||||
<h1>보고서 제목</h1>
|
||||
<h2>부제 (선택)</h2>
|
||||
<p>작성자 / 소속 / 작성일</p>
|
||||
</div>
|
||||
<div id="box-toc">
|
||||
<!-- 본문의 H1, H2, H3를 그대로 나열 → JS가 목차로 자동 변환 -->
|
||||
</div>
|
||||
<div id="box-summary">
|
||||
<!-- 요약 내용 (없으면 비워둠) -->
|
||||
</div>
|
||||
<div id="box-content">
|
||||
<!-- 본문 전체 (H1~H3, p, table, ul, ol 등) -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ② 페이지 템플릿 (JS가 복제하여 사용) -->
|
||||
<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>
|
||||
|
||||
<!-- ③ 렌더링 엔진 JS -->
|
||||
<script>
|
||||
/* === 5-E의 JS 전문을 여기에 삽입 === */
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5-C. CSS 전문 — A4 렌더링 스타일시트
|
||||
|
||||
아래 CSS를 `<style>` 태그 안에 그대로 포함하십시오.
|
||||
|
||||
```css
|
||||
@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; }
|
||||
|
||||
.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; }
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.body-content {
|
||||
position: absolute;
|
||||
top: 20mm; left: 20mm; right: 20mm;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
white-space: nowrap; overflow: hidden; word-break: keep-all; color: var(--primary);
|
||||
margin: 0; padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20pt; 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; }
|
||||
|
||||
.toc-group { margin-bottom: 12px; break-inside: avoid; page-break-inside: avoid; }
|
||||
.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 #eee; }
|
||||
.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; }
|
||||
|
||||
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; color: #333;
|
||||
}
|
||||
.highlight-box li, .highlight-box p { font-size: 11pt !important; line-height: 1.2; letter-spacing: -0.6px; 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; }
|
||||
|
||||
.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; }
|
||||
#box-summary p, #box-summary li { font-size: 10pt !important; line-height: 1.45 !important; 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; }
|
||||
.toc-squeeze .toc-lvl-1 { margin-top: 8px !important; margin-bottom: 3px !important; }
|
||||
.toc-squeeze .toc-item { line-height: 1.4 !important; padding: 1px 0 !important; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5-D. 본문 변환 규칙
|
||||
|
||||
STEP 3에서 작성한 본문을 `box-content`에 넣을 때 아래 규칙으로 변환하십시오.
|
||||
|
||||
| 보고서 요소 | HTML 변환 |
|
||||
|------------|-----------|
|
||||
| 장 제목 (1, 2, 3...) | `<h1>1. 제목</h1>` |
|
||||
| 절 제목 (1.1, 1.2...) | `<h2>1.1 제목</h2>` |
|
||||
| 항 제목 | `<h3>제목</h3>` |
|
||||
| 본문 문단 | `<p>내용</p>` |
|
||||
| 항목 나열 | `<ul><li>항목</li></ul>` |
|
||||
| 순서 나열 | `<ol><li>항목</li></ol>` |
|
||||
| 수치 정리 | `<table>` 구조로 변환 |
|
||||
|
||||
> **주의** : `box-content` 안에는 class, style 속성을 붙이지 마십시오.
|
||||
> JS의 `detox()` 함수가 모든 class/style을 제거하고 표준 스타일로 재적용합니다.
|
||||
> 단, `highlight-box` 클래스가 필요한 강조 박스는 예외입니다.
|
||||
|
||||
---
|
||||
|
||||
#### 5-E. JS 렌더링 엔진 전문 — A4 페이지네이션
|
||||
|
||||
아래 JavaScript를 `<script>` 태그 안에 그대로 포함하십시오.
|
||||
|
||||
```javascript
|
||||
window.addEventListener("load", async () => {
|
||||
await document.fonts.ready;
|
||||
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 detox(node) {
|
||||
if (node.nodeType !== 1) return;
|
||||
if (node.closest('svg')) return;
|
||||
let cls = "";
|
||||
if (node.hasAttribute('class')) cls = node.getAttribute('class');
|
||||
if ( (cls.includes('bg-') || cls.includes('border-') || cls.includes('box')) &&
|
||||
!cls.includes('title-box') && !cls.includes('toc-') &&
|
||||
!cls.includes('cover-') && !cls.includes('highlight-box') ) {
|
||||
node.setAttribute('class', 'highlight-box atomic-block');
|
||||
node.querySelectorAll('h3, h4, strong, b').forEach(head => { head.removeAttribute('style'); head.removeAttribute('class'); });
|
||||
node.removeAttribute('style');
|
||||
cls = 'highlight-box atomic-block';
|
||||
}
|
||||
if (node.hasAttribute('class')) {
|
||||
if (!cls.includes('toc-') && !cls.includes('cover-') && !cls.includes('highlight-') &&
|
||||
!cls.includes('title-box') && !cls.includes('atomic-block')) {
|
||||
node.removeAttribute('class');
|
||||
}
|
||||
}
|
||||
node.removeAttribute('style');
|
||||
if (node.tagName === 'TABLE') node.border = "1";
|
||||
if (node.tagName === 'FIGURE') {
|
||||
node.querySelectorAll('h3, h4, .chart-title').forEach(t => t.style.display = 'none');
|
||||
}
|
||||
}
|
||||
|
||||
function formatTOC(container) {
|
||||
const nodes = container.querySelectorAll("h1, h2, h3");
|
||||
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");
|
||||
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>";
|
||||
container.innerHTML = tocHTML;
|
||||
}
|
||||
|
||||
function getFlatNodes(element) {
|
||||
if(element.id === 'box-toc') {
|
||||
element.querySelectorAll('*').forEach(el => detox(el));
|
||||
formatTOC(element);
|
||||
const tocNodes = [];
|
||||
let title = element.querySelector('h1');
|
||||
if (!title) { title = document.createElement('h1'); title.innerText = "목차"; }
|
||||
tocNodes.push(title.cloneNode(true));
|
||||
const allLis = element.querySelectorAll('li');
|
||||
let currentGroup = null;
|
||||
allLis.forEach(li => {
|
||||
if (li.classList.contains('toc-lvl-1')) {
|
||||
if (currentGroup) tocNodes.push(currentGroup);
|
||||
currentGroup = document.createElement('div');
|
||||
currentGroup.className = 'toc-group atomic-block';
|
||||
const ul = document.createElement('ul');
|
||||
ul.style.margin = "0"; ul.style.padding = "0";
|
||||
currentGroup.appendChild(ul);
|
||||
}
|
||||
if (!currentGroup) {
|
||||
currentGroup = document.createElement('div');
|
||||
currentGroup.className = 'toc-group atomic-block';
|
||||
const ul = document.createElement('ul');
|
||||
ul.style.margin = "0"; ul.style.padding = "0";
|
||||
currentGroup.appendChild(ul);
|
||||
}
|
||||
currentGroup.querySelector('ul').appendChild(li.cloneNode(true));
|
||||
});
|
||||
if (currentGroup) tocNodes.push(currentGroup);
|
||||
return tocNodes;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
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');
|
||||
if (isH1 && clone.innerText.includes('-')) { clone.innerText = clone.innerText.split('-')[0].trim(); }
|
||||
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; }
|
||||
}
|
||||
if (isHeading) {
|
||||
const spaceLeft = CONFIG.maxHeight - body.scrollHeight;
|
||||
if (spaceLeft < 160) { page = createPage(sectionType, currentHeaderTitle); body = page.querySelector('.body-content'); }
|
||||
}
|
||||
body.appendChild(clone);
|
||||
if (isText && clone.innerText.length > 10) {
|
||||
const originalHeight = clone.offsetHeight;
|
||||
clone.style.letterSpacing = "-1.0px";
|
||||
if (clone.offsetHeight < originalHeight) { clone.style.letterSpacing = "-0.8px"; }
|
||||
else { clone.style.letterSpacing = ""; }
|
||||
}
|
||||
if (body.scrollHeight > CONFIG.maxHeight) {
|
||||
if (sectionType === 'toc' && !body.classList.contains('toc-squeeze') && body.children.length > 0) {
|
||||
body.classList.add('toc-squeeze');
|
||||
if (body.scrollHeight <= CONFIG.maxHeight) continue;
|
||||
else { body.classList.remove('toc-squeeze'); body.removeChild(clone); }
|
||||
}
|
||||
if (isText) {
|
||||
body.removeChild(clone);
|
||||
let textContent = node.innerText;
|
||||
let tempP = node.cloneNode(false); 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 prevText = currentText;
|
||||
currentText += (currentText ? " " : "") + words[i];
|
||||
tempP.innerText = currentText;
|
||||
if (body.scrollHeight > CONFIG.maxHeight) {
|
||||
tempP.innerText = prevText;
|
||||
tempP.style.textAlign = "justify"; tempP.style.textAlignLast = "justify";
|
||||
let remainingNode = node.cloneNode(false);
|
||||
remainingNode.innerText = words.slice(i).join(' ');
|
||||
queue.unshift(remainingNode);
|
||||
page = createPage(sectionType, currentHeaderTitle);
|
||||
body = page.querySelector('.body-content');
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body.removeChild(clone);
|
||||
let spaceLeft = CONFIG.maxHeight - body.scrollHeight;
|
||||
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);
|
||||
if(['P','LI'].includes(filler.tagName) && filler.innerText.length > 10) filler.style.letterSpacing = "-1.0px";
|
||||
body.appendChild(filler);
|
||||
if (body.scrollHeight <= CONFIG.maxHeight) {
|
||||
if(filler.style.letterSpacing === "-1.0px") filler.style.letterSpacing = "-0.8px";
|
||||
queue.shift();
|
||||
} else { body.removeChild(filler); break; }
|
||||
}
|
||||
}
|
||||
if (body.children.length > 0) { page = createPage(sectionType, currentHeaderTitle); body = page.querySelector('.body-content'); }
|
||||
body.appendChild(clone);
|
||||
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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
createPage('cover');
|
||||
if(raw.toc) renderFlow('toc', getFlatNodes(raw.toc));
|
||||
|
||||
const summaryNodes = getFlatNodes(raw.summary);
|
||||
const tempBox = document.createElement('div');
|
||||
tempBox.style.width = "210mm"; tempBox.style.position = "absolute"; tempBox.style.visibility = "hidden";
|
||||
tempBox.id = 'box-summary';
|
||||
document.body.appendChild(tempBox);
|
||||
summaryNodes.forEach(node => tempBox.appendChild(node.cloneNode(true)));
|
||||
const totalHeight = tempBox.scrollHeight;
|
||||
const pageHeight = CONFIG.maxHeight;
|
||||
const lastPart = totalHeight % pageHeight;
|
||||
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');
|
||||
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);
|
||||
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)+"%"; }
|
||||
});
|
||||
|
||||
const allTextNodes = document.querySelectorAll('.sheet .body-content p, .sheet .body-content li');
|
||||
allTextNodes.forEach(el => {
|
||||
if (el.closest('table') || el.closest('figure') || el.closest('.chart')) return;
|
||||
if (el.innerText.trim().length < 10) return;
|
||||
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');
|
||||
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');
|
||||
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();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5-F. 출력 규칙 요약
|
||||
|
||||
```
|
||||
[출력 규칙]
|
||||
- STEP 3에서 확정된 절들을 5-D 변환 규칙에 따라 HTML로 변환하여 box-content에 넣으십시오.
|
||||
- 수정 확인 과정에서 변경된 내용이 있으면 최종본을 반영하십시오.
|
||||
- 내부 태그([자료-001], [AI 판단], [상충])는 본문에 포함하지 마십시오.
|
||||
- 출처는 본문 흐름 안에서 자연스럽게 녹이십시오.
|
||||
- 보고서 말미에 "참고 자료" 절을 두어, 활용한 자료 목록을 정리하십시오.
|
||||
- CSS(5-C)와 JS(5-E)를 그대로 포함하십시오. 수정하지 마십시오.
|
||||
- box-content 안에는 class, style 속성을 붙이지 마십시오. (highlight-box 예외)
|
||||
- 이 HTML을 브라우저에서 열면 A4 보고서가 자동 렌더링됩니다.
|
||||
```
|
||||
Reference in New Issue
Block a user