Update step7_index.py
This commit is contained in:
@@ -1,31 +1,42 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from dotenv import load_dotenv
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
make_outline.py
|
make_outline.py
|
||||||
|
|
||||||
기능:
|
기능:
|
||||||
- output_context/context/domain_prompt.txt
|
- output_context/context/domain_prompt.txt
|
||||||
- output_context/context/corpus.txt
|
- output_context/context/corpus.txt
|
||||||
를 바탕으로 보고서 목차를 생성합니다.
|
을 기반으로 목차를 생성하고,
|
||||||
1) outline_issue_report.txt 생성
|
|
||||||
2) outline_issue_report.html 생성 (미리보기용)
|
1) outline_issue_report.txt 저장
|
||||||
|
2) outline_issue_report.html 저장 (테스트.html 레이아웃 기반 표 형태)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Dict, Any, Tuple
|
from typing import List, Dict, Any, Tuple
|
||||||
|
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
from api_config import API_KEYS
|
||||||
|
|
||||||
|
# ===== 경로 설정 =====
|
||||||
|
DATA_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out")
|
||||||
|
OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치
|
||||||
|
CONTEXT_DIR = OUTPUT_ROOT / "context"
|
||||||
|
LOG_DIR = OUTPUT_ROOT / "logs"
|
||||||
|
|
||||||
|
for d in [CONTEXT_DIR, LOG_DIR]:
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# ===== OpenAI 설정 (구조 유지) =====
|
# ===== OpenAI 설정 (구조 유지) =====
|
||||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
OPENAI_API_KEY = API_KEYS.get('GPT_API_KEY', '')
|
||||||
GPT_MODEL = "gpt-5-2025-08-07"
|
GPT_MODEL = "gpt-5-2025-08-07"
|
||||||
|
|
||||||
client = OpenAI(api_key=OPENAI_API_KEY)
|
client = OpenAI(api_key=OPENAI_API_KEY)
|
||||||
|
|
||||||
# ===== 목차 구성을 위한 정규식 =====
|
# ===== 목차 파싱용 정규식 보완 (5분할 대응) =====
|
||||||
RE_KEYWORDS = re.compile(r"(#\S+)")
|
RE_KEYWORDS = re.compile(r"(#\S+)")
|
||||||
RE_L1 = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$")
|
RE_L1 = re.compile(r"^\s*(\d+)\.\s+(.+?)\s*$")
|
||||||
RE_L2 = re.compile(r"^\s*(\d+\.\d+)\s+(.+?)\s*$")
|
RE_L2 = re.compile(r"^\s*(\d+\.\d+)\s+(.+?)\s*$")
|
||||||
@@ -39,126 +50,85 @@ def log(msg: str):
|
|||||||
def load_domain_prompt() -> str:
|
def load_domain_prompt() -> str:
|
||||||
p = CONTEXT_DIR / "domain_prompt.txt"
|
p = CONTEXT_DIR / "domain_prompt.txt"
|
||||||
if not p.exists():
|
if not p.exists():
|
||||||
log("domain_prompt.txt가 없습니다. 먼저 domain_prompt.py를 실행하십시오.")
|
log("domain_prompt.txt가 없습니다. 먼저 domain_prompt.py를 실행해야 합니다.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return p.read_text(encoding="utf-8", errors="ignore").strip()
|
return p.read_text(encoding="utf-8", errors="ignore").strip()
|
||||||
|
|
||||||
def load_corpus() -> str:
|
def load_corpus() -> str:
|
||||||
p = CONTEXT_DIR / "corpus.txt"
|
p = CONTEXT_DIR / "corpus.txt"
|
||||||
if not p.exists():
|
if not p.exists():
|
||||||
log("corpus.txt가 없습니다. 먼저 make_corpus.py를 실행하십시오.")
|
log("corpus.txt가 없습니다. 먼저 make_corpus.py를 실행해야 합니다.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return p.read_text(encoding="utf-8", errors="ignore").strip()
|
return p.read_text(encoding="utf-8", errors="ignore").strip()
|
||||||
|
|
||||||
|
|
||||||
# 기존 RE_L1, RE_L2와 일치하지 않는 가이드용 정규식 추가.
|
# 기존 RE_L1, RE_L2는 유지하고 아래 두 개를 추가/교체합니다.
|
||||||
RE_L3_HEAD = re.compile(r"^\s*(\d+\.\d+\.\d+)\s+(.+)$")
|
RE_L3_HEAD = re.compile(r"^\s*(\d+\.\d+\.\d+)\s+(.+)$")
|
||||||
RE_L3_TOPIC = re.compile(r"^\s*[\-\*]\s+(.+?)\s*\|\s*(.+?)\s*\|\s*(\[.+?\])\s*\|\s*(.+)$")
|
RE_L3_TOPIC = re.compile(r"^\s*[\-\*]\s+(.+?)\s*\|\s*(.+?)\s*\|\s*(\[.+?\])\s*\|\s*(.+)$")
|
||||||
|
|
||||||
def generate_outline(domain_prompt: str, corpus: str, rag_chunks: str = "", doc_type: str = 'report', attach_pages: int = 1) -> str:
|
def generate_outline(domain_prompt: str, corpus: str) -> str:
|
||||||
"""
|
|
||||||
GPT를 호출하여 전체 보고서의 목차를 구성합니다.
|
|
||||||
- doc_type: 'report' (일반 보고서) 또는 'briefing' (브리핑 자료)
|
|
||||||
- attach_pages: 브리핑 자료 시 첨부 페이지 수
|
|
||||||
"""
|
|
||||||
if doc_type == 'briefing':
|
|
||||||
sys_msg = {
|
sys_msg = {
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": (
|
"content": (
|
||||||
domain_prompt + "\n\n"
|
domain_prompt + "\n\n"
|
||||||
"당신은 측량 및 지리정보 분야의 보고서 기획 전문가입니다. "
|
"너는 건설/측량 DX 기술 보고서의 구조를 설계하는 시니어 기술사이다. "
|
||||||
"제공된 정보를 분석하여, A4 1~2매 분량의 '핵심 브리핑 자료' 목차를 작성하세요. "
|
"주어진 corpus를 분석하여, 실무자가 즉시 활용 가능한 고밀도 지침서 목차를 설계하라."
|
||||||
"본문은 '1. 개요 - 2. 현황 - 3. 문제점 및 대책 - 4. 결론' 형식을 기본으로 하되 내용에 맞춰 조정 가능합니다. "
|
|
||||||
"각 세부 주제별로 데이터 근거와 시각화 방안을 포함한 기획안을 도출하세요."
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
attach_str = ""
|
|
||||||
for i in range(1, attach_pages + 1):
|
user_msg = {
|
||||||
attach_str += f"""
|
"role": "user",
|
||||||
[첨부 {i}]
|
"content": f"""
|
||||||
- 제목: 본문 내용을 보완하는 상세 데이터/참고 자료 (본문과 제목은 다르게)
|
아래 [corpus]를 바탕으로 보고서 제목과 전략적 목차를 설계하라.
|
||||||
- 리드문: 본문에서 언급된 핵심 수치나 근거를 요약하여 제시
|
|
||||||
- 세부 항목: [- 주제 | #키워드 | [시각화방안] | 내용 가이드] (3~4개 구성)
|
[corpus]
|
||||||
|
{corpus}
|
||||||
|
|
||||||
|
요구 사항:
|
||||||
|
1) 첫 줄에 보고서 제목 1개를 작성하라.
|
||||||
|
2) 그 아래 목차를 번호 기반 계측 구조로 작성하라.
|
||||||
|
- 대목차: 1. / 2. / 3. ...
|
||||||
|
- 중목차: 1.1 / 1.2 / ...
|
||||||
|
- 소목차: 1.1.1 / 1.1.2 / ...
|
||||||
|
3) **수량 제약 (중요)**:
|
||||||
|
- 대목차(1.)는 5~8개로 구성하라.
|
||||||
|
- **중목차(1.1) 하나당 소목차(1.1.1, 1.1.2...)는 반드시 2개에서 4개 사이로 구성하라.** (절대 1개만 만들지 말 것)
|
||||||
|
- 소목차(1.1.1) 하나당 '핵심주제(꼭지)'는 반드시 2개에서 3개 사이로 구성하라.
|
||||||
|
|
||||||
|
[소목차 작성 형식]
|
||||||
|
1.1.1 소목차 제목
|
||||||
|
- 핵심주제 1 | #키워드 | [유형] | 집필가이드(데이터/표 구성 지침)
|
||||||
|
- 핵심주제 2 | #키워드 | [유형] | 집필가이드(데이터/표 구성 지침)
|
||||||
|
|
||||||
|
5) [유형] 분류 가이드:
|
||||||
|
- [비교형]: 기존 vs DX 방식의 비교표(Table)가 필수적인 경우
|
||||||
|
- [기술형]: RMSE, GSD, 중복도 등 정밀 수치와 사양 설명이 핵심인 경우
|
||||||
|
- [절차형]: 단계별 워크플로 및 체크리스트가 중심인 경우
|
||||||
|
- [인사이트형]: 한계점 분석 및 전문가 제언(☞)이 중심인 경우
|
||||||
|
6) 집필가이드는 50자 내외로, "어떤 데이터를 검색해서 어떤 표를 그려라"와 같이 구체적으로 지시하라.
|
||||||
|
7) 대목차는 최대 8개 이내로 구성하라.
|
||||||
"""
|
"""
|
||||||
user_msg = {
|
|
||||||
"role": "user",
|
|
||||||
"content": f"""
|
|
||||||
다음 정보를 바탕으로 브리핑 보고서 목차를 생성하세요.
|
|
||||||
|
|
||||||
[정보원(Corpus)]
|
|
||||||
{corpus[:8000]}
|
|
||||||
|
|
||||||
[작성 규칙]
|
|
||||||
1. 최상단에 [보고서 제목]을 작성 (전문적이고 명확하게)
|
|
||||||
2. 본문(1페이지 분량)과 첨부({attach_pages}페이지 분량)로 구분
|
|
||||||
3. 각 페이지별로 리드문(전체 내용을 관통하는 핵심 메시지) 포함
|
|
||||||
4. 세부 주제(Topic)는 다음 형식을 준수:
|
|
||||||
- 주제명 | #키워드 | [시각화방안] | 내용 가이드
|
|
||||||
- 시각화방안: 표, 그래프, 비교표, 다이어그램 등 구체적으로 명시
|
|
||||||
5. (중요) 코퍼스 내의 핵심 수치, 기준, 측량 기법 등을 세부 항목 가이드에 포함할 것.
|
|
||||||
|
|
||||||
{attach_str}
|
|
||||||
""",
|
|
||||||
}
|
}
|
||||||
else:
|
|
||||||
# 일반 보고서 모드
|
|
||||||
sys_msg = {
|
|
||||||
"role": "system",
|
|
||||||
"content": (
|
|
||||||
domain_prompt + "\n\n"
|
|
||||||
"당신은 건설/측량 DX 기술 전문가이자 보고서 기획자입니다. "
|
|
||||||
"제시된 코퍼스를 분석하여, 실무에 즉시 활용 가능한 고품질 기술 보고서 목차를 생성하세요. "
|
|
||||||
"목차는 대분류(1.), 중분류(1.1), 소분류(1.1.1)의 3단계 계층 구조를 따릅니다. "
|
|
||||||
"각 소분류(1.1.1) 하위에는 반드시 구체적인 집필 가이드를 포함해야 합니다."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
user_msg = {
|
|
||||||
"role": "user",
|
|
||||||
"content": f"""
|
|
||||||
다음 정보를 바탕으로 기술 보고서 목차를 생성하세요.
|
|
||||||
|
|
||||||
[정보원(Corpus)]
|
|
||||||
{corpus[:10000]}
|
|
||||||
|
|
||||||
[작성 규칙]
|
|
||||||
1. 최상단에 [보고서 제목] 1개를 작성
|
|
||||||
2. 목차는 1. / 1.1 / 1.1.1 형식의 3단계 구조
|
|
||||||
3. 소분류(1.1.1) 하단에는 해당 섹션에서 다룰 상세 주제들을 다음 형식으로 나열:
|
|
||||||
- 주제명 | #핵심키워드 | [구성형식] | 집필 가이드(수치나 핵심 기법 포함)
|
|
||||||
- 구성형식 예시: [비교표], [기술설명], [절차도], [성과분석] 등
|
|
||||||
4. 도메인 지식(측량 정확도 기준, 사용 장비 등)이 각 세부 항목 가이드에 녹아있어야 함.
|
|
||||||
|
|
||||||
출력은 목차 텍스트만 깔끔하게 출력하세요.
|
|
||||||
""",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = client.chat.completions.create(
|
resp = client.chat.completions.create(
|
||||||
model=GPT_MODEL,
|
model=GPT_MODEL,
|
||||||
messages=[sys_msg, user_msg],
|
messages=[sys_msg, user_msg],
|
||||||
temperature=0.3,
|
|
||||||
)
|
)
|
||||||
return (resp.choices[0].message.content or "").strip()
|
return (resp.choices[0].message.content or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def parse_outline(outline_text: str) -> Tuple[str, List[Dict[str, Any]]]:
|
def parse_outline(outline_text: str) -> Tuple[str, List[Dict[str, Any]]]:
|
||||||
"""
|
lines = [ln.rstrip() for ln in outline_text.splitlines() if ln.strip()]
|
||||||
생성된 목차 텍스트를 구조화된 데이터로 파싱합니다.
|
if not lines: return "", []
|
||||||
"""
|
|
||||||
lines = [l.strip() for l in outline_text.splitlines() if l.strip()]
|
|
||||||
if not lines:
|
|
||||||
return "제목 없음", []
|
|
||||||
|
|
||||||
# 1. 제목 추출
|
title = lines[0].strip() # 첫 줄은 보고서 제목
|
||||||
title_line = lines[0]
|
|
||||||
title = re.sub(r'^\[?보고서 제목\]?[:\s]*', '', title_line).strip()
|
|
||||||
|
|
||||||
# 2. 계층 구조 파싱
|
|
||||||
rows = []
|
rows = []
|
||||||
current_section = None
|
current_section = None # 현재 처리 중인 소목차(1.1.1)를 추적
|
||||||
|
|
||||||
for ln in lines[1:]:
|
for ln in lines[1:]:
|
||||||
raw = ln.strip()
|
raw = ln.strip()
|
||||||
|
|
||||||
# 소분류 (1.1.1)
|
# 1. 소목차 헤더(1.1.1 제목) 발견 시
|
||||||
m3_head = RE_L3_HEAD.match(raw)
|
m3_head = RE_L3_HEAD.match(raw)
|
||||||
if m3_head:
|
if m3_head:
|
||||||
num, s_title = m3_head.groups()
|
num, s_title = m3_head.groups()
|
||||||
@@ -166,16 +136,19 @@ def parse_outline(outline_text: str) -> Tuple[str, List[Dict[str, Any]]]:
|
|||||||
"depth": 3,
|
"depth": 3,
|
||||||
"num": num,
|
"num": num,
|
||||||
"title": s_title,
|
"title": s_title,
|
||||||
"sub_topics": []
|
"sub_topics": [] # 여기에 아래 줄의 꼭지들을 담을 예정
|
||||||
}
|
}
|
||||||
rows.append(current_section)
|
rows.append(current_section)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 상세 주제 (- 주제 | #키워드 | [형식] | 가이드)
|
# 2. 세부 꼭지(- 주제 | #키워드 | [유형] | 가이드) 발견 시
|
||||||
m_topic = RE_L3_TOPIC.match(raw)
|
m_topic = RE_L3_TOPIC.match(raw)
|
||||||
if m_topic and current_section:
|
if m_topic and current_section:
|
||||||
t_title, kws_raw, t_type, guide = m_topic.groups()
|
t_title, kws_raw, t_type, guide = m_topic.groups()
|
||||||
|
# 키워드 추출 (#키워드 형태)
|
||||||
kws = [k.lstrip("#").strip() for k in RE_KEYWORDS.findall(kws_raw)]
|
kws = [k.lstrip("#").strip() for k in RE_KEYWORDS.findall(kws_raw)]
|
||||||
|
|
||||||
|
# 현재 소목차(current_section)의 리스트에 추가
|
||||||
current_section["sub_topics"].append({
|
current_section["sub_topics"].append({
|
||||||
"topic_title": t_title,
|
"topic_title": t_title,
|
||||||
"keywords": kws,
|
"keywords": kws,
|
||||||
@@ -184,18 +157,18 @@ def parse_outline(outline_text: str) -> Tuple[str, List[Dict[str, Any]]]:
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 대분류 (1.)
|
# 3. 대목차(1.) 처리
|
||||||
m1 = RE_L1.match(raw)
|
m1 = RE_L1.match(raw)
|
||||||
if m1:
|
if m1:
|
||||||
rows.append({"depth": 1, "num": m1.group(1).strip(), "title": m1.group(2).strip()})
|
rows.append({"depth": 1, "num": m1.group(1).strip(), "title": m1.group(2).strip()})
|
||||||
current_section = None
|
current_section = None # 소목차 구간 종료
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 중분류 (1.1)
|
# 4. 중목차(1.1) 처리
|
||||||
m2 = RE_L2.match(raw)
|
m2 = RE_L2.match(raw)
|
||||||
if m2:
|
if m2:
|
||||||
rows.append({"depth": 2, "num": m2.group(1).strip(), "title": m2.group(2).strip()})
|
rows.append({"depth": 2, "num": m2.group(1).strip(), "title": m2.group(2).strip()})
|
||||||
current_section = None
|
current_section = None # 소목차 구간 종료
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return title, rows
|
return title, rows
|
||||||
@@ -208,16 +181,33 @@ def html_escape(s: str) -> str:
|
|||||||
.replace('"', """)
|
.replace('"', """)
|
||||||
.replace("'", "'"))
|
.replace("'", "'"))
|
||||||
|
|
||||||
|
def chunk_rows(rows: List[Dict[str, Any]], max_rows_per_page: int = 26) -> List[List[Dict[str, Any]]]:
|
||||||
|
"""
|
||||||
|
A4 1장에 표가 길어지면 넘치므로, 단순 행 개수로 페이지 분할한다.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
cur = []
|
||||||
|
for r in rows:
|
||||||
|
cur.append(r)
|
||||||
|
if len(cur) >= max_rows_per_page:
|
||||||
|
out.append(cur)
|
||||||
|
cur = []
|
||||||
|
if cur:
|
||||||
|
out.append(cur)
|
||||||
|
return out
|
||||||
|
|
||||||
def build_outline_table_html(rows: List[Dict[str, Any]]) -> str:
|
def build_outline_table_html(rows: List[Dict[str, Any]]) -> str:
|
||||||
"""목차 구조를 HTML 테이블로 변환"""
|
"""
|
||||||
|
테스트.html의 table 스타일을 그대로 쓰는 전제의 표 HTML
|
||||||
|
"""
|
||||||
head = """
|
head = """
|
||||||
<table border="1" style="width:100%; border-collapse: collapse;">
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background-color: #f2f2f2;">
|
<tr>
|
||||||
<th>분류</th>
|
<th>구분</th>
|
||||||
<th>번호</th>
|
<th>번호</th>
|
||||||
<th>항목명</th>
|
<th>제목</th>
|
||||||
<th>상세 가이드 / 키워드</th>
|
<th>키워드</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -228,85 +218,287 @@ def build_outline_table_html(rows: List[Dict[str, Any]]) -> str:
|
|||||||
depth = r["depth"]
|
depth = r["depth"]
|
||||||
num = html_escape(r["num"])
|
num = html_escape(r["num"])
|
||||||
title = html_escape(r["title"])
|
title = html_escape(r["title"])
|
||||||
|
kw = " ".join([f"#{k}" for k in r.get("keywords", []) if k])
|
||||||
|
kw = html_escape(kw)
|
||||||
|
|
||||||
if depth == 3:
|
if depth == 1:
|
||||||
kw_list = []
|
|
||||||
for st in r.get("sub_topics", []):
|
|
||||||
kw_list.append(f"<b>{html_escape(st['topic_title'])}</b>: {html_escape(st['guide'])}")
|
|
||||||
detail = "<br>".join(kw_list)
|
|
||||||
cls_name = "소분류"
|
|
||||||
elif depth == 2:
|
|
||||||
detail = ""
|
|
||||||
cls_name = "중분류"
|
|
||||||
else:
|
|
||||||
detail = ""
|
|
||||||
cls_name = "대분류"
|
|
||||||
|
|
||||||
body_parts.append(
|
body_parts.append(
|
||||||
f"""
|
f"""
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 8px; text-align: center;">{cls_name}</td>
|
<td class="group-cell">대목차</td>
|
||||||
<td style="padding: 8px; text-align: center;">{num}</td>
|
<td>{num}</td>
|
||||||
<td style="padding: 8px; font-weight: {'bold' if depth < 3 else 'normal'};">{title}</td>
|
<td>{title}</td>
|
||||||
<td style="padding: 8px; font-size: 0.9em;">{detail}</td>
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
elif depth == 2:
|
||||||
|
body_parts.append(
|
||||||
|
f"""
|
||||||
|
<tr>
|
||||||
|
<td class="group-cell">중목차</td>
|
||||||
|
<td>{num}</td>
|
||||||
|
<td>{title}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
body_parts.append(
|
||||||
|
f"""
|
||||||
|
<tr>
|
||||||
|
<td class="group-cell">소목차</td>
|
||||||
|
<td>{num}</td>
|
||||||
|
<td>{title}</td>
|
||||||
|
<td>{kw}</td>
|
||||||
</tr>
|
</tr>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
tail = "</tbody></table>"
|
tail = """
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
return head + "\n".join(body_parts) + tail
|
return head + "\n".join(body_parts) + tail
|
||||||
|
|
||||||
def build_outline_html(report_title: str, rows: List[Dict[str, Any]]) -> str:
|
def build_outline_html(report_title: str, rows: List[Dict[str, Any]]) -> str:
|
||||||
table_html = build_outline_table_html(rows)
|
"""
|
||||||
|
테스트.html 레이아웃 구조를 그대로 따라 A4 시트 형태로 HTML 생성
|
||||||
|
"""
|
||||||
|
css = r"""
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-blue: #3057B9;
|
||||||
|
--gray-light: #F2F2F2;
|
||||||
|
--gray-medium: #E6E6E6;
|
||||||
|
--gray-dark: #666666;
|
||||||
|
--border-light: #DDDDDD;
|
||||||
|
--text-black: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans KR', sans-serif;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: var(--text-black);
|
||||||
|
line-height: 1.35;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet {
|
||||||
|
background-color: white;
|
||||||
|
width: 210mm;
|
||||||
|
height: 297mm;
|
||||||
|
padding: 20mm 20mm;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body { background: none; padding: 0; }
|
||||||
|
.sheet { box-shadow: none; margin: 0; border: none; page-break-after: always; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--gray-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: 900;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: -1.5px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-divider {
|
||||||
|
height: 4px;
|
||||||
|
background-color: var(--primary-blue);
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-box {
|
||||||
|
background-color: var(--gray-light);
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-box div {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-blue);
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead-notes {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #777;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-left: 5px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-content { flex: 1; }
|
||||||
|
|
||||||
|
.section { margin-bottom: 22px; }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #999;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 9.5pt;
|
||||||
|
border-top: 1.5px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: var(--gray-medium);
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-cell {
|
||||||
|
background-color: #F9F9F9;
|
||||||
|
font-weight: 700;
|
||||||
|
width: 16%;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary-blue);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-footer {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--gray-dark);
|
||||||
|
border-top: 1px solid #EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-page { flex: 1; text-align: center; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
pages = chunk_rows(rows, max_rows_per_page=26)
|
||||||
|
|
||||||
|
html_pages = []
|
||||||
|
total_pages = len(pages) if pages else 1
|
||||||
|
for i, page_rows in enumerate(pages, start=1):
|
||||||
|
table_html = build_outline_table_html(page_rows)
|
||||||
|
|
||||||
|
html_pages.append(f"""
|
||||||
|
<div class="sheet">
|
||||||
|
<header class="page-header">
|
||||||
|
<div class="header-left">
|
||||||
|
보고서: 목차 자동 생성 결과
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
작성일자: {datetime.now().strftime("%Y. %m. %d.")}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="title-block">
|
||||||
|
<h1 class="header-title">{html_escape(report_title)}</h1>
|
||||||
|
<div class="title-divider"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body-content">
|
||||||
|
<div class="lead-box">
|
||||||
|
<div>확정 목차 표 형태 정리본</div>
|
||||||
|
</div>
|
||||||
|
<div class="lead-notes">목차는 outline_issue_report.txt를 기반으로 표로 재구성됨</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">목차</div>
|
||||||
|
{table_html}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div class="footer-slogan">Word Style v2 Outline</div>
|
||||||
|
<div class="footer-page">- {i} / {total_pages} -</div>
|
||||||
|
<div class="footer-info">outline_issue_report.html</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
|
||||||
return f"""<!DOCTYPE html>
|
return f"""<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>보고서 목차 구성안</title>
|
<title>{html_escape(report_title)} - Outline</title>
|
||||||
<style>
|
<style>{css}</style>
|
||||||
body {{ font-family: 'Malgun Gothic', sans-serif; padding: 40px; line-height: 1.6; color: #333; }}
|
|
||||||
h1 {{ color: #1a365d; border-bottom: 2px solid #1a365d; padding-bottom: 10px; }}
|
|
||||||
.info {{ background: #f7fafc; padding: 15px; border-left: 5px solid #1a365d; margin-bottom: 20px; }}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{html_escape(report_title)}</h1>
|
{''.join(html_pages)}
|
||||||
<div class="info">
|
|
||||||
본 목차는 제공된 코퍼스를 기반으로 AI가 설계한 보고서 구성안입니다.
|
|
||||||
</div>
|
|
||||||
{table_html}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def main(input_dir, output_dir, doc_type='report', attach_pages=1):
|
def main():
|
||||||
global DATA_ROOT, OUTPUT_ROOT, CONTEXT_DIR, LOG_DIR
|
|
||||||
DATA_ROOT = Path(input_dir)
|
|
||||||
OUTPUT_ROOT = Path(output_dir)
|
|
||||||
CONTEXT_DIR = OUTPUT_ROOT / "context"
|
|
||||||
LOG_DIR = OUTPUT_ROOT / "logs"
|
|
||||||
for d in [CONTEXT_DIR, LOG_DIR]:
|
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
|
||||||
log("=== 목차 생성 시작 ===")
|
log("=== 목차 생성 시작 ===")
|
||||||
domain_prompt = load_domain_prompt()
|
domain_prompt = load_domain_prompt()
|
||||||
corpus = load_corpus()
|
corpus = load_corpus()
|
||||||
|
|
||||||
# RAG에서 일부 정보를 가져올 수 있으면 가져옴 (선택 사항)
|
outline = generate_outline(domain_prompt, corpus)
|
||||||
rag_chunks = ""
|
|
||||||
outline = generate_outline(domain_prompt, corpus, rag_chunks, doc_type, attach_pages)
|
|
||||||
|
|
||||||
# TXT 저장
|
# TXT 저장 유지
|
||||||
out_txt = CONTEXT_DIR / "outline_issue_report.txt"
|
out_txt = CONTEXT_DIR / "outline_issue_report.txt"
|
||||||
out_txt.write_text(outline, encoding="utf-8")
|
out_txt.write_text(outline, encoding="utf-8")
|
||||||
log(f"목차 TXT 저장 완료: {out_txt}")
|
log(f"목차 TXT 저장 완료: {out_txt}")
|
||||||
|
|
||||||
# HTML 미리보기용 저장
|
# HTML 추가 저장
|
||||||
title, rows = parse_outline(outline)
|
title, rows = parse_outline(outline)
|
||||||
out_html = CONTEXT_DIR / "outline_issue_report.html"
|
out_html = CONTEXT_DIR / "outline_issue_report.html"
|
||||||
out_html.write_text(build_outline_html(title, rows), encoding="utf-8")
|
out_html.write_text(build_outline_html(title, rows), encoding="utf-8")
|
||||||
log(f"목차 HTML 미리보기 저장 완료: {out_html}")
|
log(f"목차 HTML 저장 완료: {out_html}")
|
||||||
|
|
||||||
log("=== 목차 생성 완료 ===")
|
log("=== 목차 생성 종료 ===")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user