📦 Initialize Geulbeot structure and merge Prompts & test projects
This commit is contained in:
538
03. Code/geulbeot_1st/app.py
Normal file
538
03. Code/geulbeot_1st/app.py
Normal 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)
|
||||
Reference in New Issue
Block a user