📦 Initialize Geulbeot structure and merge Prompts & test projects

This commit is contained in:
2026-03-05 11:32:29 +09:00
commit 555a954458
687 changed files with 205247 additions and 0 deletions

View File

@@ -0,0 +1,538 @@
# -*- coding: utf-8 -*-
"""
글벗 Light v1.0
상시 업무용 HTML 보고서 자동 생성기
Flask + Claude API + Railway
"""
import os
import anthropic
from flask import Flask, render_template, request, jsonify, send_file, Response
from werkzeug.utils import secure_filename
from datetime import datetime
import tempfile
import io
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'geulbeot-light-secret-key')
# Claude API 클라이언트
client = anthropic.Anthropic(api_key='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')
# 시스템 프롬프트 로드
def load_system_prompt():
prompt_path = os.path.join(os.path.dirname(__file__), 'prompts', 'system_prompt.txt')
try:
with open(prompt_path, 'r', encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
return get_default_system_prompt()
def get_default_system_prompt():
"""기본 시스템 프롬프트 (파일이 없을 경우)"""
return """당신은 전문 비즈니스 보고서 작성 전문가입니다.
사용자가 제공하는 원본 문서의 내용을 분석하여, 각인된 HTML 양식에 맞게 A4 인쇄용 보고서를 생성합니다.
기본 규칙:
1. 반드시 완전한 HTML 문서로 출력
2. Noto Sans KR 폰트, Navy 계열 색상 사용
3. A4 크기 (210mm × 297mm), 여백 20mm
4. 코드 블록 없이 순수 HTML만 출력
"""
# 각인된 양식 CSS (전체)
IMPRINTED_CSS = '''
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
:root {
--primary-navy: #1a365d;
--secondary-navy: #2c5282;
--accent-navy: #3182ce;
--dark-gray: #2d3748;
--medium-gray: #4a5568;
--light-gray: #e2e8f0;
--bg-light: #f7fafc;
--text-black: #1a202c;
--border-color: #cbd5e0;
}
* {
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.55;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
gap: 20px;
word-break: keep-all;
}
.sheet {
background-color: white;
width: 210mm;
height: 297mm;
padding: 20mm;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
@media print {
body { background: none; padding: 0; gap: 0; }
.sheet { box-shadow: none; margin: 0; border: none; page-break-after: always; }
.sheet:last-child { page-break-after: auto; }
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
font-size: 9pt;
color: var(--medium-gray);
}
.header-title {
font-size: 23pt;
font-weight: 900;
margin-bottom: 8px;
letter-spacing: -1px;
color: var(--primary-navy);
line-height: 1.25;
text-align: center;
}
.title-divider {
height: 3px;
background: linear-gradient(90deg, var(--primary-navy) 0%, var(--secondary-navy) 100%);
width: 100%;
margin-bottom: 20px;
}
.lead-box {
background-color: var(--bg-light);
border-left: 4px solid var(--primary-navy);
padding: 14px 16px;
margin-bottom: 18px;
}
.lead-box div {
font-size: 11.5pt;
font-weight: 500;
color: var(--dark-gray);
line-height: 1.6;
}
.lead-box b {
color: var(--primary-navy);
font-weight: 700;
}
.body-content {
flex: 1;
display: flex;
flex-direction: column;
}
.section {
margin-bottom: 16px;
}
.section-title {
font-size: 12pt;
font-weight: 700;
display: flex;
align-items: center;
margin-bottom: 10px;
color: var(--primary-navy);
}
.section-title::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--secondary-navy);
margin-right: 10px;
}
ul {
list-style: none;
padding-left: 10px;
}
li {
font-size: 10.5pt;
position: relative;
margin-bottom: 6px;
padding-left: 14px;
color: var(--dark-gray);
line-height: 1.55;
}
li::before {
content: "";
position: absolute;
left: 0;
color: var(--secondary-navy);
font-size: 10pt;
}
.bottom-box {
border: 1.5px solid var(--border-color);
display: flex;
margin-top: auto;
margin-bottom: 10px;
}
.bottom-left {
width: 18%;
background-color: var(--primary-navy);
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-weight: 700;
font-size: 10.5pt;
color: #fff;
line-height: 1.4;
}
.bottom-right {
width: 82%;
background-color: var(--bg-light);
padding: 12px 18px;
font-size: 10.5pt;
line-height: 1.6;
display: flex;
flex-direction: column;
justify-content: center;
color: var(--dark-gray);
}
.page-footer {
position: absolute;
bottom: 10mm;
left: 20mm;
right: 20mm;
padding-top: 8px;
text-align: center;
font-size: 8.5pt;
color: var(--medium-gray);
border-top: 1px solid var(--light-gray);
}
b { font-weight: 700; color: var(--primary-navy); }
.keyword { font-weight: 600; color: var(--text-black); }
/* 표 스타일 */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 9.5pt;
border-top: 2px solid var(--primary-navy);
border-bottom: 1px solid var(--border-color);
margin-top: 6px;
}
.data-table th {
background-color: var(--primary-navy);
color: #fff;
font-weight: 600;
padding: 8px 6px;
border: 1px solid var(--secondary-navy);
text-align: center;
font-size: 9pt;
}
.data-table td {
border: 1px solid var(--border-color);
padding: 7px 10px;
vertical-align: middle;
color: var(--dark-gray);
line-height: 1.45;
text-align: center;
}
.data-table td:first-child {
background-color: var(--bg-light);
font-weight: 600;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-weight: 600;
font-size: 8.5pt;
letter-spacing: -0.3px;
}
.badge-safe {
background-color: #e6f4ea;
color: #1e6f3f;
border: 1px solid #a8d5b8;
}
.badge-caution {
background-color: #fef3e2;
color: #9a5b13;
border: 1px solid #f5d9a8;
}
.badge-risk {
background-color: #fce8e8;
color: #a12b2b;
border: 1px solid #f5b8b8;
}
/* 2x2 그리드 */
.strategy-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 8px;
}
.strategy-item {
background: var(--bg-light);
border: 1px solid var(--border-color);
padding: 10px 12px;
}
.strategy-title {
font-weight: 700;
color: var(--primary-navy);
font-size: 10pt;
margin-bottom: 4px;
border-bottom: 1px solid var(--light-gray);
padding-bottom: 4px;
}
.strategy-item p {
font-size: 9.5pt;
color: var(--dark-gray);
line-height: 1.5;
}
/* QA 박스 */
.qa-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 8px;
}
.qa-item {
background: var(--bg-light);
border-left: 3px solid var(--secondary-navy);
padding: 8px 12px;
font-size: 9.5pt;
}
.qa-item strong {
color: var(--primary-navy);
}
/* 프로세스 스타일 */
.process-container {
background: var(--bg-light);
padding: 14px 16px;
border: 1px solid var(--border-color);
margin-top: 8px;
}
.process-step {
display: flex;
align-items: flex-start;
margin-bottom: 5px;
}
.step-num {
background: var(--primary-navy);
color: #fff;
width: 22px;
height: 22px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 10pt;
margin-right: 10px;
flex-shrink: 0;
}
.step-content {
font-size: 11pt;
line-height: 1.55;
color: var(--dark-gray);
}
.step-content strong {
color: var(--primary-navy);
font-weight: 600;
}
'''
@app.route('/')
def index():
"""메인 페이지"""
return render_template('index.html')
@app.route('/generate', methods=['POST'])
def generate():
"""보고서 생성 API"""
try:
# 파일 또는 텍스트 입력 받기
content = ""
if 'file' in request.files and request.files['file'].filename:
file = request.files['file']
content = file.read().decode('utf-8')
elif 'content' in request.form:
content = request.form.get('content', '')
if not content.strip():
return jsonify({'error': '내용을 입력하거나 파일을 업로드해주세요.'}), 400
# 옵션 가져오기
page_option = request.form.get('page_option', '1')
additional_prompt = request.form.get('additional_prompt', '')
department = request.form.get('department', '총괄기획실')
# 페이지 옵션에 따른 지시사항
page_instructions = {
'1': '1페이지로 핵심 내용만 압축하여 작성하세요.',
'2': '2페이지로 작성하세요. 1페이지는 본문(개요, 핵심 내용), 2페이지는 [첨부]로 시작하는 상세 내용입니다.',
'n': f'여러 페이지로 작성하세요. 1페이지는 본문, 나머지는 [첨부 1], [첨부 2] 형태로 상세 내용을 분할합니다.'
}
# Claude API 호출
system_prompt = load_system_prompt()
user_message = f"""다음 원본 문서를 분석하여 각인된 양식의 HTML 보고서로 변환해주세요.
## 페이지 옵션
{page_instructions.get(page_option, page_instructions['1'])}
## 부서명
{department}
## 추가 요청사항
{additional_prompt if additional_prompt else '없음'}
## 원본 문서 내용
{content}
---
위 내용을 바탕으로 완전한 HTML 문서를 생성해주세요.
코드 블록(```) 없이 <!DOCTYPE html>부터 </html>까지 순수 HTML만 출력하세요."""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8000,
messages=[
{"role": "user", "content": user_message}
],
system=system_prompt
)
# 응답에서 HTML 추출
html_content = response.content[0].text
# 코드 블록 제거 (혹시 포함된 경우)
if '```html' in html_content:
html_content = html_content.split('```html')[1].split('```')[0]
elif '```' in html_content:
html_content = html_content.split('```')[1].split('```')[0]
html_content = html_content.strip()
return jsonify({
'success': True,
'html': html_content
})
except anthropic.APIError as e:
return jsonify({'error': f'Claude API 오류: {str(e)}'}), 500
except Exception as e:
return jsonify({'error': f'서버 오류: {str(e)}'}), 500
@app.route('/download/html', methods=['POST'])
def download_html():
"""HTML 파일 다운로드"""
html_content = request.form.get('html', '')
if not html_content:
return "No content", 400
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'report_{timestamp}.html'
return Response(
html_content,
mimetype='text/html',
headers={'Content-Disposition': f'attachment; filename={filename}'}
)
@app.route('/download/pdf', methods=['POST'])
def download_pdf():
"""PDF 파일 다운로드 (WeasyPrint 사용)"""
try:
from weasyprint import HTML, CSS
html_content = request.form.get('html', '')
if not html_content:
return "No content", 400
# PDF 생성
pdf_buffer = io.BytesIO()
HTML(string=html_content).write_pdf(pdf_buffer)
pdf_buffer.seek(0)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'report_{timestamp}.pdf'
return Response(
pdf_buffer.getvalue(),
mimetype='application/pdf',
headers={'Content-Disposition': f'attachment; filename={filename}'}
)
except ImportError:
return jsonify({'error': 'PDF 변환 기능이 설치되지 않았습니다. HTML로 다운로드 후 브라우저에서 PDF로 인쇄해주세요.'}), 501
except Exception as e:
return jsonify({'error': f'PDF 변환 오류: {str(e)}'}), 500
@app.route('/hwp-script')
def hwp_script():
"""HWP 변환 스크립트 다운로드 안내"""
return render_template('hwp_guide.html')
@app.route('/health')
def health():
"""헬스 체크"""
return jsonify({'status': 'healthy', 'version': '1.0.0'})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
app.run(host='0.0.0.0', port=port, debug=debug)