# -*- coding: utf-8 -*- """ 글벗 Light v2.0 2단계 API 변환 + 대화형 피드백 시스템 Flask + Claude API + Railway """ import os import json import anthropic from flask import Flask, render_template, request, jsonify, Response, session from datetime import datetime import io import re from api_config import API_KEYS 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-v2') # Claude API 클라이언트 client = anthropic.Anthropic(api_key=API_KEYS.get('CLAUDE_API_KEY', '')) # ============== 프롬프트 로드 ============== def load_prompt(filename): """프롬프트 파일 로드""" prompt_path = os.path.join(os.path.dirname(__file__), 'prompts', filename) try: with open(prompt_path, 'r', encoding='utf-8') as f: return f.read() except FileNotFoundError: return None def get_step1_prompt(): """1단계: 구조 추출 프롬프트""" prompt = load_prompt('step1_extract.txt') if prompt: return prompt # 기본 프롬프트 (파일 없을 경우) return """HTML 문서를 분석하여 JSON 구조로 추출하세요. 원본 텍스트를 그대로 보존하고, 구조만 정확히 파악하세요.""" def get_step2_prompt(): """2단계: HTML 생성 프롬프트""" prompt = load_prompt('step2_generate.txt') if prompt: return prompt # 기본 프롬프트 (파일 없을 경우) return """JSON 구조를 각인된 양식의 HTML로 변환하세요. Navy 색상 테마, A4 크기, Noto Sans KR 폰트를 사용하세요.""" def get_step1_5_prompt(): """1.5단계: 배치 계획 프롬프트""" prompt = load_prompt('step1_5_plan.txt') if prompt: return prompt return """JSON 구조를 분석하여 페이지 배치 계획을 수립하세요.""" def get_refine_prompt(): """피드백 반영 프롬프트""" return """당신은 HTML 보고서 수정 전문가입니다. 사용자의 피드백을 반영하여 현재 HTML을 수정합니다. ## 규칙 1. 피드백에서 언급된 부분만 정확히 수정 2. 나머지 구조와 스타일은 그대로 유지 3. 완전한 HTML 문서로 출력 ( ~ ) 4. 코드 블록(```) 없이 순수 HTML만 출력 ## 현재 HTML {current_html} ## 사용자 피드백 {feedback} 위 피드백을 반영하여 수정된 완전한 HTML을 출력하세요.""" # ============== API 호출 함수 ============== def call_claude(system_prompt, user_message, max_tokens=8000): """Claude API 호출""" response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=max_tokens, system=system_prompt, messages=[{"role": "user", "content": user_message}] ) return response.content[0].text def extract_json(text): """텍스트에서 JSON 추출""" # 코드 블록 제거 if '```json' in text: text = text.split('```json')[1].split('```')[0] elif '```' in text: text = text.split('```')[1].split('```')[0] text = text.strip() # JSON 파싱 시도 try: return json.loads(text) except json.JSONDecodeError: # JSON 부분만 추출 시도 match = re.search(r'\{[\s\S]*\}', text) if match: try: return json.loads(match.group()) except: pass return None def extract_html(text): """텍스트에서 HTML 추출""" # 코드 블록 제거 if '```html' in text: text = text.split('```html')[1].split('```')[0] elif '```' in text: parts = text.split('```') if len(parts) >= 2: text = parts[1] text = text.strip() # )', text, re.IGNORECASE) if match: text = match.group(1) return text def content_too_long(html, max_sections_per_page=4): """페이지당 콘텐츠 양 체크""" from bs4 import BeautifulSoup soup = BeautifulSoup(html, 'html.parser') sheets = soup.find_all('div', class_='sheet') for sheet in sheets: sections = sheet.find_all('div', class_='section') if len(sections) > max_sections_per_page: return True # 리스트 항목 체크 all_li = sheet.find_all('li') if len(all_li) > 12: return True # 프로세스 스텝 체크 steps = sheet.find_all('div', class_='process-step') if len(steps) > 6: return True return False # ============== 라우트 ============== @app.route('/') def index(): """메인 페이지""" return render_template('index.html') @app.route('/generate', methods=['POST']) def generate(): """보고서 생성 API (2단계 처리)""" 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') department = request.form.get('department', '총괄기획실') additional_prompt = request.form.get('additional_prompt', '') # ============== 1단계: 구조 추출 ============== step1_prompt = get_step1_prompt() step1_message = f"""다음 HTML 문서의 구조를 분석하여 JSON으로 추출해주세요. ## 원본 HTML {content} --- 위 문서를 분석하여 JSON 구조로 출력하세요. 설명 없이 JSON만 출력.""" step1_response = call_claude(step1_prompt, step1_message, max_tokens=4000) structure_json = extract_json(step1_response) if not structure_json: # JSON 추출 실패 시 원본 그대로 전달 structure_json = {"raw_content": content, "parse_failed": True} # ============== 1.5단계: 배치 계획 ============== step1_5_prompt = get_step1_5_prompt() step1_5_message = f"""다음 JSON 구조를 분석하여 페이지 배치 계획을 수립해주세요. ## 문서 구조 (JSON) {json.dumps(structure_json, ensure_ascii=False, indent=2)} ## 페이지 수 {page_option}페이지 --- 배치 계획 JSON만 출력하세요. 설명 없이 JSON만.""" step1_5_response = call_claude(step1_5_prompt, step1_5_message, max_tokens=4000) page_plan = extract_json(step1_5_response) if not page_plan: page_plan = {"page_plan": {}, "parse_failed": True} # ============== 2단계: HTML 생성 ============== page_instructions = { '1': '1페이지로 핵심 내용만 압축하여 작성하세요. 내용이 넘치면 텍스트를 줄이거나 줄간격을 조정하세요.', '2': '2페이지로 작성하세요. 1페이지는 본문(개요, 핵심 내용), 2페이지는 [첨부]로 시작하는 상세 내용입니다.', 'n': '여러 페이지로 작성하세요. 1페이지는 본문, 나머지는 [첨부 1], [첨부 2] 형태로 분할합니다.' } step2_prompt = get_step2_prompt() step2_message = f"""다음 배치 계획과 문서 구조를 기반으로 각인된 양식의 HTML 보고서를 생성해주세요. ## 배치 계획 {json.dumps(page_plan, ensure_ascii=False, indent=2)} ## 문서 구조 (JSON) {json.dumps(structure_json, ensure_ascii=False, indent=2)} ## 페이지 옵션 {page_instructions.get(page_option, page_instructions['1'])} ## 부서명 {department} ## 추가 요청사항 {additional_prompt if additional_prompt else '없음'} --- 위 JSON을 바탕으로 완전한 HTML 문서를 생성하세요. 코드 블록(```) 없이 부터 까지 순수 HTML만 출력.""" step2_response = call_claude(step2_prompt, step2_message, max_tokens=8000) html_content = extract_html(step2_response) # 후처리 검증: 콘텐츠가 너무 많으면 압축 재요청 if content_too_long(html_content): compress_message = f"""다음 HTML이 페이지당 콘텐츠가 너무 많습니다. 각 페이지당 섹션 3~4개, 리스트 항목 8개 이하로 압축해주세요. 텍스트를 줄이거나 덜 중요한 내용은 생략하세요. {html_content} 코드 블록 없이 압축된 완전한 HTML만 출력하세요.""" compress_response = call_claude(step2_prompt, compress_message, max_tokens=8000) html_content = extract_html(compress_response) # 세션에 저장 (피드백용) session['original_html'] = content session['current_html'] = html_content session['structure_json'] = json.dumps(structure_json, ensure_ascii=False) session['conversation'] = [] return jsonify({ 'success': True, 'html': html_content, 'structure': structure_json }) except anthropic.APIError as e: return jsonify({'error': f'Claude API 오류: {str(e)}'}), 500 except Exception as e: import traceback return jsonify({'error': f'서버 오류: {str(e)}', 'trace': traceback.format_exc()}), 500 @app.route('/refine', methods=['POST']) def refine(): """피드백 반영 API (대화형)""" try: feedback = request.json.get('feedback', '') current_html = request.json.get('current_html', '') or session.get('current_html', '') if not feedback.strip(): return jsonify({'error': '피드백 내용을 입력해주세요.'}), 400 if not current_html: return jsonify({'error': '수정할 HTML이 없습니다. 먼저 변환을 실행해주세요.'}), 400 # 원본 HTML도 컨텍스트에 포함 original_html = session.get('original_html', '') # 피드백 반영 프롬프트 refine_prompt = f"""당신은 HTML 보고서 수정 전문가입니다. 사용자의 피드백을 반영하여 현재 HTML을 수정합니다. ## 규칙 1. 피드백에서 언급된 부분만 정확히 수정 2. 나머지 구조와 스타일은 그대로 유지 3. 완전한 HTML 문서로 출력 ( ~ ) 4. 코드 블록(```) 없이 순수 HTML만 출력 5. 원본 문서의 텍스트를 참조하여 누락된 내용 복구 가능 ## 원본 HTML (참고용) {original_html[:3000] if original_html else '없음'}... ## 현재 HTML {current_html} ## 사용자 피드백 {feedback} --- 위 피드백을 반영하여 수정된 완전한 HTML을 출력하세요.""" response = call_claude("", refine_prompt, max_tokens=8000) new_html = extract_html(response) # 세션 업데이트 session['current_html'] = new_html # 대화 히스토리 저장 conversation = session.get('conversation', []) conversation.append({'role': 'user', 'content': feedback}) conversation.append({'role': 'assistant', 'content': '수정 완료'}) session['conversation'] = conversation return jsonify({ 'success': True, 'html': new_html }) 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 파일 다운로드""" try: from weasyprint import HTML html_content = request.form.get('html', '') if not html_content: return "No content", 400 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 다운로드 후 브라우저에서 인쇄하세요.'}), 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': '2.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)