v1:글벗 초기 기획안_20260121
This commit is contained in:
343
templates/hwp_guide.html
Normal file
343
templates/hwp_guide.html
Normal file
@@ -0,0 +1,343 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HWP 변환 가이드 - 글벗 Light</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Noto Sans KR', sans-serif; }
|
||||
.gradient-bg { background: linear-gradient(135deg, #1a365d 0%, #2c5282 100%); }
|
||||
pre { background: #1e293b; color: #e2e8f0; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }
|
||||
code { font-family: 'Consolas', 'Monaco', monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 헤더 -->
|
||||
<header class="gradient-bg text-white py-6 shadow-lg">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<a href="/" class="text-blue-200 hover:text-white text-sm">← 메인으로</a>
|
||||
<h1 class="text-2xl font-bold mt-2">HWP 변환 가이드</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<!-- 안내 -->
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-xl p-6 mb-8">
|
||||
<h2 class="font-bold text-yellow-800 mb-2">⚠️ HWP 변환 요구사항</h2>
|
||||
<ul class="text-yellow-700 text-sm space-y-1">
|
||||
<li>• Windows 운영체제</li>
|
||||
<li>• 한글 프로그램 (한컴오피스) 설치</li>
|
||||
<li>• Python 3.8 이상</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 설치 방법 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-4">1. 필요 라이브러리 설치</h2>
|
||||
<pre><code>pip install pyhwpx beautifulsoup4</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- 사용 방법 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-4">2. 사용 방법</h2>
|
||||
<ol class="list-decimal list-inside space-y-2 text-gray-700">
|
||||
<li>글벗 Light에서 HTML 파일을 다운로드합니다.</li>
|
||||
<li>아래 Python 스크립트를 다운로드합니다.</li>
|
||||
<li>스크립트 내 경로를 수정합니다.</li>
|
||||
<li>스크립트를 실행합니다.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-bold text-gray-800">3. HWP 변환 스크립트</h2>
|
||||
<button onclick="copyScript()" class="px-4 py-2 bg-blue-900 text-white rounded hover:bg-blue-800 text-sm">
|
||||
📋 복사
|
||||
</button>
|
||||
</div>
|
||||
<pre id="scriptCode"><code># -*- coding: utf-8 -*-
|
||||
"""
|
||||
글벗 Light - HTML → HWP 변환기
|
||||
Windows + 한글 프로그램 필요
|
||||
"""
|
||||
|
||||
from pyhwpx import Hwp
|
||||
from bs4 import BeautifulSoup
|
||||
import os
|
||||
|
||||
|
||||
class HtmlToHwpConverter:
|
||||
def __init__(self, visible=True):
|
||||
self.hwp = Hwp(visible=visible)
|
||||
self.colors = {}
|
||||
|
||||
def _init_colors(self):
|
||||
self.colors = {
|
||||
'primary-navy': self.hwp.RGBColor(26, 54, 93),
|
||||
'secondary-navy': self.hwp.RGBColor(44, 82, 130),
|
||||
'dark-gray': self.hwp.RGBColor(45, 55, 72),
|
||||
'medium-gray': self.hwp.RGBColor(74, 85, 104),
|
||||
'bg-light': self.hwp.RGBColor(247, 250, 252),
|
||||
'white': self.hwp.RGBColor(255, 255, 255),
|
||||
'black': self.hwp.RGBColor(0, 0, 0),
|
||||
}
|
||||
|
||||
def _mm(self, mm):
|
||||
return self.hwp.MiliToHwpUnit(mm)
|
||||
|
||||
def _font(self, size=10, color='black', bold=False):
|
||||
self.hwp.set_font(
|
||||
FaceName='맑은 고딕',
|
||||
Height=size,
|
||||
Bold=bold,
|
||||
TextColor=self.colors.get(color, self.colors['black'])
|
||||
)
|
||||
|
||||
def _align(self, align):
|
||||
actions = {'left': 'ParagraphShapeAlignLeft', 'center': 'ParagraphShapeAlignCenter', 'right': 'ParagraphShapeAlignRight'}
|
||||
if align in actions:
|
||||
self.hwp.HAction.Run(actions[align])
|
||||
|
||||
def _para(self, text='', size=10, color='black', bold=False, align='left'):
|
||||
self._align(align)
|
||||
self._font(size, color, bold)
|
||||
if text:
|
||||
self.hwp.insert_text(text)
|
||||
self.hwp.BreakPara()
|
||||
|
||||
def _exit_table(self):
|
||||
self.hwp.HAction.Run("Cancel")
|
||||
self.hwp.HAction.Run("CloseEx")
|
||||
self.hwp.HAction.Run("MoveDocEnd")
|
||||
self.hwp.BreakPara()
|
||||
|
||||
def _set_cell_bg(self, color_name):
|
||||
self.hwp.HAction.GetDefault("CellBorderFill", self.hwp.HParameterSet.HCellBorderFill.HSet)
|
||||
pset = self.hwp.HParameterSet.HCellBorderFill
|
||||
pset.FillAttr.type = self.hwp.BrushType("NullBrush|WinBrush")
|
||||
pset.FillAttr.WinBrushFaceStyle = self.hwp.HatchStyle("None")
|
||||
pset.FillAttr.WinBrushHatchColor = self.hwp.RGBColor(0, 0, 0)
|
||||
pset.FillAttr.WinBrushFaceColor = self.colors.get(color_name, self.colors['white'])
|
||||
pset.FillAttr.WindowsBrush = 1
|
||||
self.hwp.HAction.Execute("CellBorderFill", pset.HSet)
|
||||
|
||||
def _create_header(self, left_text, right_text):
|
||||
try:
|
||||
self.hwp.HAction.GetDefault("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
|
||||
self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterStyle", 0)
|
||||
self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterCtrlType", 0)
|
||||
self.hwp.HAction.Execute("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
|
||||
self._font(9, 'medium-gray')
|
||||
self.hwp.insert_text(left_text)
|
||||
self.hwp.insert_text("\t" * 12)
|
||||
self.hwp.insert_text(right_text)
|
||||
self.hwp.HAction.Run("CloseEx")
|
||||
except Exception as e:
|
||||
print(f"머리말 생성 실패: {e}")
|
||||
|
||||
def _create_footer(self, text):
|
||||
try:
|
||||
self.hwp.HAction.GetDefault("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
|
||||
self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterStyle", 0)
|
||||
self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterCtrlType", 1)
|
||||
self.hwp.HAction.Execute("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
|
||||
self._align('center')
|
||||
self._font(8.5, 'medium-gray')
|
||||
self.hwp.insert_text(text)
|
||||
self.hwp.HAction.Run("CloseEx")
|
||||
except Exception as e:
|
||||
print(f"꼬리말 생성 실패: {e}")
|
||||
|
||||
def _convert_lead_box(self, elem):
|
||||
content = elem.find("div")
|
||||
if not content:
|
||||
return
|
||||
text = ' '.join(content.get_text().split())
|
||||
self.hwp.create_table(1, 1, treat_as_char=True)
|
||||
self._set_cell_bg('bg-light')
|
||||
self._font(11.5, 'dark-gray', False)
|
||||
self.hwp.insert_text(text)
|
||||
self._exit_table()
|
||||
|
||||
def _convert_bottom_box(self, elem):
|
||||
left = elem.find(class_="bottom-left")
|
||||
right = elem.find(class_="bottom-right")
|
||||
if not left or not right:
|
||||
return
|
||||
left_text = ' '.join(left.get_text().split())
|
||||
right_text = right.get_text(strip=True)
|
||||
|
||||
self.hwp.create_table(1, 2, treat_as_char=True)
|
||||
self._set_cell_bg('primary-navy')
|
||||
self._font(10.5, 'white', True)
|
||||
self._align('center')
|
||||
self.hwp.insert_text(left_text)
|
||||
self.hwp.HAction.Run("MoveRight")
|
||||
self._set_cell_bg('bg-light')
|
||||
self._font(10.5, 'primary-navy', True)
|
||||
self._align('center')
|
||||
self.hwp.insert_text(right_text)
|
||||
self._exit_table()
|
||||
|
||||
def _convert_section(self, section):
|
||||
title = section.find(class_="section-title")
|
||||
if title:
|
||||
self._para("■ " + title.get_text(strip=True), 12, 'primary-navy', True)
|
||||
|
||||
ul = section.find("ul")
|
||||
if ul:
|
||||
for li in ul.find_all("li", recursive=False):
|
||||
keyword = li.find(class_="keyword")
|
||||
if keyword:
|
||||
kw_text = keyword.get_text(strip=True)
|
||||
full = li.get_text(strip=True)
|
||||
rest = full.replace(kw_text, '', 1).strip()
|
||||
self._font(10.5, 'primary-navy', True)
|
||||
self.hwp.insert_text(" • " + kw_text + " ")
|
||||
self._font(10.5, 'dark-gray', False)
|
||||
self.hwp.insert_text(rest)
|
||||
self.hwp.BreakPara()
|
||||
else:
|
||||
self._para(" • " + li.get_text(strip=True), 10.5, 'dark-gray')
|
||||
self._para()
|
||||
|
||||
def _convert_sheet(self, sheet, is_first_page=False):
|
||||
if is_first_page:
|
||||
header = sheet.find(class_="page-header")
|
||||
if header:
|
||||
left = header.find(class_="header-left")
|
||||
right = header.find(class_="header-right")
|
||||
left_text = left.get_text(strip=True) if left else ""
|
||||
right_text = right.get_text(strip=True) if right else ""
|
||||
if left_text or right_text:
|
||||
self._create_header(left_text, right_text)
|
||||
|
||||
footer = sheet.find(class_="page-footer")
|
||||
if footer:
|
||||
self._create_footer(footer.get_text(strip=True))
|
||||
|
||||
title = sheet.find(class_="header-title")
|
||||
if title:
|
||||
title_text = title.get_text(strip=True)
|
||||
if '[첨부]' in title_text:
|
||||
self._para(title_text, 15, 'primary-navy', True, 'left')
|
||||
else:
|
||||
self._para(title_text, 23, 'primary-navy', True, 'center')
|
||||
self._font(10, 'secondary-navy')
|
||||
self._align('center')
|
||||
self.hwp.insert_text("━" * 45)
|
||||
self.hwp.BreakPara()
|
||||
|
||||
self._para()
|
||||
|
||||
lead_box = sheet.find(class_="lead-box")
|
||||
if lead_box:
|
||||
self._convert_lead_box(lead_box)
|
||||
self._para()
|
||||
|
||||
for section in sheet.find_all(class_="section"):
|
||||
self._convert_section(section)
|
||||
|
||||
bottom_box = sheet.find(class_="bottom-box")
|
||||
if bottom_box:
|
||||
self._para()
|
||||
self._convert_bottom_box(bottom_box)
|
||||
|
||||
def convert(self, html_path, output_path):
|
||||
print(f"[입력] {html_path}")
|
||||
|
||||
with open(html_path, 'r', encoding='utf-8') as f:
|
||||
soup = BeautifulSoup(f.read(), 'html.parser')
|
||||
|
||||
self.hwp.FileNew()
|
||||
self._init_colors()
|
||||
|
||||
# 페이지 설정
|
||||
try:
|
||||
self.hwp.HAction.GetDefault("PageSetup", self.hwp.HParameterSet.HSecDef.HSet)
|
||||
sec = self.hwp.HParameterSet.HSecDef
|
||||
sec.PageDef.LeftMargin = self._mm(20)
|
||||
sec.PageDef.RightMargin = self._mm(20)
|
||||
sec.PageDef.TopMargin = self._mm(20)
|
||||
sec.PageDef.BottomMargin = self._mm(20)
|
||||
sec.PageDef.HeaderLen = self._mm(10)
|
||||
sec.PageDef.FooterLen = self._mm(10)
|
||||
self.hwp.HAction.Execute("PageSetup", sec.HSet)
|
||||
except Exception as e:
|
||||
print(f"페이지 설정 실패: {e}")
|
||||
|
||||
sheets = soup.find_all(class_="sheet")
|
||||
total = len(sheets)
|
||||
print(f"[변환] 총 {total} 페이지")
|
||||
|
||||
for i, sheet in enumerate(sheets, 1):
|
||||
print(f"[{i}/{total}] 페이지 처리 중...")
|
||||
self._convert_sheet(sheet, is_first_page=(i == 1))
|
||||
if i < total:
|
||||
self.hwp.HAction.Run("BreakPage")
|
||||
|
||||
self.hwp.SaveAs(output_path)
|
||||
print(f"✅ 저장 완료: {output_path}")
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.hwp.Quit()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
# ====================================
|
||||
# 경로 설정 (본인 환경에 맞게 수정)
|
||||
# ====================================
|
||||
html_path = r"C:\Users\User\Downloads\report.html"
|
||||
output_path = r"C:\Users\User\Downloads\report.hwp"
|
||||
|
||||
print("=" * 50)
|
||||
print("글벗 Light - HTML → HWP 변환기")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
converter = HtmlToHwpConverter(visible=True)
|
||||
converter.convert(html_path, output_path)
|
||||
print("\n✅ 변환 완료!")
|
||||
input("Enter를 누르면 HWP가 닫힙니다...")
|
||||
converter.close()
|
||||
except FileNotFoundError:
|
||||
print(f"\n[에러] 파일을 찾을 수 없습니다: {html_path}")
|
||||
except Exception as e:
|
||||
print(f"\n[에러] {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- 경로 수정 안내 -->
|
||||
<div class="bg-white rounded-xl shadow-md p-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-4">4. 경로 수정</h2>
|
||||
<p class="text-gray-700 mb-4">스크립트 하단의 <code class="bg-gray-100 px-2 py-1 rounded">main()</code> 함수에서 경로를 수정하세요:</p>
|
||||
<pre><code>html_path = r"C:\다운로드경로\report.html"
|
||||
output_path = r"C:\저장경로\report.hwp"</code></pre>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function copyScript() {
|
||||
const code = document.getElementById('scriptCode').innerText;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
alert('스크립트가 클립보드에 복사되었습니다!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
427
templates/index.html
Normal file
427
templates/index.html
Normal file
@@ -0,0 +1,427 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>글벗 Light - 상시 업무용 보고서 생성기</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Noto Sans KR', sans-serif; }
|
||||
.gradient-bg { background: linear-gradient(135deg, #1a365d 0%, #2c5282 100%); }
|
||||
.card-shadow { box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #1a365d 0%, #2c5282 100%);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(26, 54, 93, 0.4);
|
||||
}
|
||||
.preview-frame {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.loading-spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #1a365d;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.tab-active {
|
||||
border-bottom: 3px solid #1a365d;
|
||||
color: #1a365d;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 헤더 -->
|
||||
<header class="gradient-bg text-white py-6 shadow-lg">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">글벗 Light</h1>
|
||||
<p class="text-blue-200 text-sm mt-1">상시 업무용 보고서 자동 생성기 v2.0</p>
|
||||
</div>
|
||||
<div class="text-right text-sm text-blue-200">
|
||||
<p>각인된 양식 기반</p>
|
||||
<p>A4 인쇄 최적화</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- 입력 패널 -->
|
||||
<div class="bg-white rounded-xl card-shadow p-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-blue-900 text-white rounded flex items-center justify-center mr-3 text-sm">1</span>
|
||||
문서 입력
|
||||
</h2>
|
||||
|
||||
<!-- 입력 방식 탭 -->
|
||||
<div class="flex border-b mb-4">
|
||||
<button id="tab-file" class="px-4 py-2 tab-active" onclick="switchTab('file')">파일 업로드</button>
|
||||
<button id="tab-text" class="px-4 py-2 text-gray-500" onclick="switchTab('text')">직접 입력</button>
|
||||
</div>
|
||||
|
||||
<form id="generateForm">
|
||||
<!-- 파일 업로드 -->
|
||||
<div id="input-file" class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">HTML 파일 업로드</label>
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-blue-500 transition-colors">
|
||||
<input type="file" id="fileInput" name="file" accept=".html,.htm,.txt" class="hidden" onchange="handleFileSelect(this)">
|
||||
<label for="fileInput" class="cursor-pointer">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-600">클릭하여 파일 선택</p>
|
||||
<p class="text-xs text-gray-400">HTML, TXT 지원</p>
|
||||
</label>
|
||||
<p id="fileName" class="mt-2 text-sm text-blue-600 font-medium hidden"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 직접 입력 -->
|
||||
<div id="input-text" class="mb-4 hidden">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">내용 직접 입력</label>
|
||||
<textarea name="content" id="contentInput" rows="10"
|
||||
class="w-full border border-gray-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="변환할 문서 내용을 입력하세요..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 옵션 설정 -->
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<h3 class="font-medium text-gray-800 mb-3">옵션 설정</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<!-- 페이지 옵션 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">페이지 구성</label>
|
||||
<select name="page_option" class="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-blue-500">
|
||||
<option value="1">1페이지 (핵심 요약)</option>
|
||||
<option value="2">2페이지 (본문 + 첨부)</option>
|
||||
<option value="n">N페이지 (본문 + 다중 첨부)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 부서명 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">부서명</label>
|
||||
<input type="text" name="department" value="총괄기획실"
|
||||
class="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 추가 요청사항 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">추가 요청사항 (선택)</label>
|
||||
<textarea name="additional_prompt" rows="2"
|
||||
class="w-full border border-gray-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="예: 표를 더 상세하게 만들어주세요, 핵심 결론을 강조해주세요..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 생성 버튼 -->
|
||||
<button type="submit" id="generateBtn" class="w-full btn-primary text-white py-3 rounded-lg font-medium flex items-center justify-center">
|
||||
<span id="btnText">보고서 생성</span>
|
||||
<div id="btnSpinner" class="loading-spinner ml-2 hidden"></div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 미리보기 패널 -->
|
||||
<div class="bg-white rounded-xl card-shadow p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<span class="w-8 h-8 bg-blue-900 text-white rounded flex items-center justify-center mr-3 text-sm">2</span>
|
||||
미리보기 & 다운로드
|
||||
</h2>
|
||||
|
||||
<!-- 다운로드 버튼 그룹 -->
|
||||
<div id="downloadBtns" class="flex gap-2 hidden">
|
||||
<button onclick="downloadHTML()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded text-sm font-medium transition-colors">
|
||||
📄 HTML
|
||||
</button>
|
||||
<button onclick="downloadPDF()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded text-sm font-medium transition-colors">
|
||||
📑 PDF
|
||||
</button>
|
||||
<button onclick="printPreview()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded text-sm font-medium transition-colors">
|
||||
🖨️ 인쇄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 미리보기 영역 -->
|
||||
<div id="previewArea" class="preview-frame rounded-lg overflow-hidden" style="height: 600px;">
|
||||
<div id="emptyState" class="h-full flex items-center justify-center text-gray-400">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-16 w-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p>문서를 입력하고 생성 버튼을 누르세요</p>
|
||||
</div>
|
||||
</div>
|
||||
<iframe id="previewFrame" class="w-full h-full hidden"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- 에러 메시지 -->
|
||||
<div id="errorMessage" class="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 hidden"></div>
|
||||
|
||||
<!-- 피드백 채팅 UI -->
|
||||
<div id="refineSection" class="mt-6 hidden">
|
||||
<h3 class="font-bold text-gray-800 mb-3 flex items-center">
|
||||
<span class="w-6 h-6 bg-green-600 text-white rounded flex items-center justify-center mr-2 text-xs">✎</span>
|
||||
수정 요청
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="feedbackInput"
|
||||
class="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||
placeholder="수정할 내용을 입력하세요 (예: 테이블 4열로 맞춰줘, 결론 한 줄로 줄여줘)">
|
||||
<button onclick="submitFeedback()" id="refineBtn"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2">
|
||||
<span id="refineBtnText">수정</span>
|
||||
<div id="refineBtnSpinner" class="loading-spinner hidden"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div id="refineHistory" class="mt-3 space-y-2 max-h-32 overflow-y-auto text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HWP 변환 안내 -->
|
||||
<div class="mt-8 bg-blue-50 border border-blue-200 rounded-xl p-6">
|
||||
<h3 class="font-bold text-blue-900 mb-2">💡 HWP 파일이 필요하신가요?</h3>
|
||||
<p class="text-blue-800 text-sm mb-3">
|
||||
HWP 변환은 Windows + 한글 프로그램이 필요합니다. HTML 다운로드 후 제공되는 Python 스크립트로 로컬에서 변환할 수 있습니다.
|
||||
</p>
|
||||
<a href="/hwp-script" class="inline-block px-4 py-2 bg-blue-900 text-white rounded-lg text-sm hover:bg-blue-800 transition-colors">
|
||||
HWP 변환 스크립트 받기 →
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 푸터 -->
|
||||
<footer class="bg-gray-100 py-6 mt-12">
|
||||
<div class="container mx-auto px-4 text-center text-gray-500 text-sm">
|
||||
<p>글벗 Light v2.0 | 2단계 변환 + 대화형 피드백 시스템</p>
|
||||
<p class="mt-1">Powered by Claude API</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 생성된 HTML 저장용 -->
|
||||
<script>
|
||||
let generatedHTML = '';
|
||||
|
||||
function switchTab(tab) {
|
||||
const fileTab = document.getElementById('tab-file');
|
||||
const textTab = document.getElementById('tab-text');
|
||||
const fileInput = document.getElementById('input-file');
|
||||
const textInput = document.getElementById('input-text');
|
||||
|
||||
if (tab === 'file') {
|
||||
fileTab.classList.add('tab-active');
|
||||
textTab.classList.remove('tab-active');
|
||||
textTab.classList.add('text-gray-500');
|
||||
fileInput.classList.remove('hidden');
|
||||
textInput.classList.add('hidden');
|
||||
} else {
|
||||
textTab.classList.add('tab-active');
|
||||
fileTab.classList.remove('tab-active');
|
||||
fileTab.classList.add('text-gray-500');
|
||||
textInput.classList.remove('hidden');
|
||||
fileInput.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(input) {
|
||||
const fileName = document.getElementById('fileName');
|
||||
if (input.files && input.files[0]) {
|
||||
fileName.textContent = '📎 ' + input.files[0].name;
|
||||
fileName.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('generateForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('generateBtn');
|
||||
const btnText = document.getElementById('btnText');
|
||||
const btnSpinner = document.getElementById('btnSpinner');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
||||
// 로딩 상태
|
||||
btn.disabled = true;
|
||||
btnText.textContent = '생성 중...';
|
||||
btnSpinner.classList.remove('hidden');
|
||||
errorMessage.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const formData = new FormData(this);
|
||||
|
||||
// 파일 또는 텍스트 내용 확인
|
||||
const file = document.getElementById('fileInput').files[0];
|
||||
const content = document.getElementById('contentInput').value;
|
||||
|
||||
if (!file && !content.trim()) {
|
||||
throw new Error('파일을 업로드하거나 내용을 입력해주세요.');
|
||||
}
|
||||
|
||||
const response = await fetch('/generate', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (data.success && data.html) {
|
||||
generatedHTML = data.html;
|
||||
|
||||
// 미리보기 표시
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const previewFrame = document.getElementById('previewFrame');
|
||||
const downloadBtns = document.getElementById('downloadBtns');
|
||||
const refineSection = document.getElementById('refineSection');
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
previewFrame.classList.remove('hidden');
|
||||
downloadBtns.classList.remove('hidden');
|
||||
refineSection.classList.remove('hidden');
|
||||
|
||||
// iframe에 HTML 로드
|
||||
previewFrame.srcdoc = generatedHTML;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errorMessage.textContent = error.message;
|
||||
errorMessage.classList.remove('hidden');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btnText.textContent = '보고서 생성';
|
||||
btnSpinner.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
function downloadHTML() {
|
||||
if (!generatedHTML) return;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/download/html';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'html';
|
||||
input.value = generatedHTML;
|
||||
|
||||
form.appendChild(input);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
}
|
||||
|
||||
function downloadPDF() {
|
||||
const printWindow = window.open('', '_blank');
|
||||
printWindow.document.write(generatedHTML);
|
||||
printWindow.document.close();
|
||||
printWindow.onload = function() {
|
||||
printWindow.print();
|
||||
};
|
||||
}
|
||||
|
||||
function printPreview() {
|
||||
const previewFrame = document.getElementById('previewFrame');
|
||||
if (previewFrame.contentWindow) {
|
||||
previewFrame.contentWindow.print();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitFeedback() {
|
||||
const feedbackInput = document.getElementById('feedbackInput');
|
||||
const feedback = feedbackInput.value.trim();
|
||||
|
||||
if (!feedback) {
|
||||
alert('수정 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!generatedHTML) {
|
||||
alert('먼저 보고서를 생성해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('refineBtn');
|
||||
const btnText = document.getElementById('refineBtnText');
|
||||
const btnSpinner = document.getElementById('refineBtnSpinner');
|
||||
const history = document.getElementById('refineHistory');
|
||||
|
||||
// 로딩 상태
|
||||
btn.disabled = true;
|
||||
btnText.textContent = '수정 중...';
|
||||
btnSpinner.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch('/refine', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
feedback: feedback,
|
||||
current_html: generatedHTML
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (data.success && data.html) {
|
||||
generatedHTML = data.html;
|
||||
|
||||
// 미리보기 업데이트
|
||||
const previewFrame = document.getElementById('previewFrame');
|
||||
previewFrame.srcdoc = generatedHTML;
|
||||
|
||||
// 히스토리에 추가
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'p-2 bg-green-50 border border-green-200 rounded text-green-800';
|
||||
historyItem.textContent = '✓ ' + feedback;
|
||||
history.appendChild(historyItem);
|
||||
|
||||
// 입력창 초기화
|
||||
feedbackInput.value = '';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('수정 오류: ' + error.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btnText.textContent = '수정';
|
||||
btnSpinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Enter 키로 피드백 제출
|
||||
document.getElementById('feedbackInput')?.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
submitFeedback();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user