import os import random import re import logging import tempfile from datetime import datetime from zoneinfo import ZoneInfo import html from PIL import Image from urllib.request import urlopen import markdown2 # 로깅 설정 (INFO 레벨) logging.basicConfig(level=logging.INFO) # ------------------------------- # 상수 정의 (향후 조정 및 유지보수 용이하도록) # ------------------------------- TARGET_CHAR_LENGTH = 4000 # 정보성 블로그 최소 글자수 MIN_SECTION_LENGTH = 600 # 각 소제목 아래 최소 글자수 MAX_TOKENS = 15000 # Gemini API 최대 토큰 수 TEMPERATURE = 0.75 # Gemini API 온도 값 TOP_P = 0.95 # Gemini API top_p 값 # Gemini API 설정 gemini_api_key = os.getenv("GEMINI_API_KEY") # --- Google Gemini SDK 초기화 --- from google import genai from google.genai import types client = genai.Client(api_key=gemini_api_key) # ------------------------------- # 기본 도우미 함수들 # ------------------------------- def remove_unwanted_phrases(text): """불필요한 표현 제거 함수""" unwanted_phrases = [ '여러분', '최근', '마지막으로', '결론적으로', '결국', '종합적으로', '따라서', '마무리', '끝으로', '요약', '한 줄 요약', '정리하자면', '총정리', '글을 마치며', '이상으로', '추천드립니다', '참고하세요', '도움이 되셨길', '좋은 하루 되세요', '다음 글에서', '도움이 되었길', '즐거운 하루 되세요', '감사합니다' ] # 문단별로 나누어 처리 lines = text.split('\n') result_lines = [] for line in lines: if "다음 섹션에서는" in line: parts = line.split("다음 섹션에서는") if parts[0].strip(): result_lines.append(parts[0].strip()) else: # 불필요한 표현 제거 (구두점 포함) for phrase in unwanted_phrases: # 불필요한 표현 앞뒤의 구두점과 공백까지 포함하여 제거 pattern = rf'(\b{re.escape(phrase)}\b[\s,.!?]*)|([,.!?]*\b{re.escape(phrase)}\b)' line = re.sub(pattern, '', line) # 문장 내 잔여 공백 및 구두점 정리 line = re.sub(r'\s{2,}', ' ', line) # 연속 공백 제거 line = line.strip() # 앞뒤 공백 제거 result_lines.append(line) return '\n'.join(result_lines) def convert_to_html(text): """마크다운 형식을 HTML로 변환""" text = re.sub(r'^\s*[-*]\s+', '', text, flags=re.MULTILINE) text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE) text = re.sub(r'^\s*#{1,6}\s+', '', text, flags=re.MULTILINE) return markdown2.markdown(text) def extract_text_from_html(html_content): """HTML에서 순수 텍스트만 추출하는 함수""" if not html_content: return "" # HTML 태그 제거 text = re.sub(r'<[^>]+>', '', html_content) # HTML 엔티티 디코딩 text = html.unescape(text) # 연속된 공백 제거 text = re.sub(r'\s+', ' ', text) return text.strip() def format_blog_post(blog_post, query="", with_title=False): """블로그 포스트 포맷팅 함수 - 소제목 강화 버전""" blog_post = re.sub(r'^#+\s+', '', blog_post, flags=re.MULTILINE) blog_post = re.sub(r'^\d+\.\s+', '', blog_post, flags=re.MULTILINE) blog_post = re.sub(r'^[\*\-]\s+', '', blog_post, flags=re.MULTILINE) # 첫 줄(원본 제목)과 비슷한 패턴이 있다면 제거 lines = blog_post.split('\n') if lines and len(lines) > 0: first_line = lines[0].strip() # 첫 줄이 제목인 경우, 비슷한 내용의 라인을 모두 제거 if first_line and len(first_line) > 5: # 첫 줄과 유사한 내용을 가진 라인 찾아 제거 filtered_lines = [] for line in lines: # 첫 줄과 유사하면 제거 if line.strip() and (first_line in line or line in first_line): continue filtered_lines.append(line) lines = filtered_lines # 도입부, 결론 소제목 패턴 intro_pattern = r'(?i)도입부\s*[:]?\s*(.*?)$' conclusion_pattern = r'(?i)결론\s*[:]?\s*(.*?)$' # 도입부, 결론 소제목 제거 filtered_lines = [] for line in lines: if re.match(intro_pattern, line) or re.match(conclusion_pattern, line): continue filtered_lines.append(line) lines = filtered_lines # 본론 소제목 패턴 강화 section_patterns = [ r'^본론\d+\s*[:]?\s*(.*?)$', # 본론1: 내용 패턴 r'^.{5,50}의 [가-힣\s]+$', # ~의 ~ 패턴 r'^[가-힣\s]{5,30}(이란|이란\?|이란\s무엇인가|이란\s무엇일까)[\?\s]*$', # ~이란? 패턴 r'^[가-힣\s]{5,50}\s[-–]\s.{5,30}$', # 강조 표현 패턴 (예: 효과적인 방법 - 실천하기) r'^[가-힣A-Za-z\s]{10,50}[\.!\?]$', # 긴 문장으로 된 소제목 패턴 ] formatted_lines = [] in_paragraph = False # 본론 섹션 번호 추적 section_number = 1 for i, line in enumerate(lines): line = line.strip() if not line: if in_paragraph: formatted_lines.append("

") in_paragraph = False formatted_lines.append("
") continue # 다음 줄과 이전 줄이 비어있는지 확인 prev_empty = i == 0 or not lines[i-1].strip() next_empty = i == len(lines) - 1 or not lines[i+1].strip() # 소제목인지 체크 is_subtitle = False clean_subtitle = line # 패턴 매칭으로 소제목 확인 for pattern in section_patterns: if re.match(pattern, line): is_subtitle = True # 본론X: 패턴인 경우 정리 if "본론" in line: clean_subtitle = re.sub(r'^본론\d+\s*[:]?\s*', '', line) break # 패턴에 매칭되지 않았지만 다음 규칙으로 소제목으로 판단할 수 있는 경우 if not is_subtitle: # 앞뒤 줄이 비어있고, 길이가 짧은(5-50자) 문장은 소제목으로 처리 if prev_empty and next_empty and 5 <= len(line) <= 50: is_subtitle = True # 이전에 있던 소제목들과 비슷한 길이를 가진 경우도 소제목으로 간주 elif section_number > 1 and prev_empty and 5 <= len(line) <= 50: is_subtitle = True if is_subtitle and clean_subtitle.strip(): if in_paragraph: formatted_lines.append("

") in_paragraph = False # 소제목이 없거나 너무 짧은 경우 기본 소제목 사용 if not clean_subtitle.strip() or len(clean_subtitle.strip()) < 2: clean_subtitle = f"주요 포인트 {section_number}" # 소제목 형식 강화 - 항상 볼드처리와 스타일링 적용 formatted_lines.append( f'

{html.escape(clean_subtitle)}

' ) section_number += 1 else: # 일반 텍스트 처리 if not in_paragraph: formatted_lines.append("

") in_paragraph = True content = html.escape(line) bold_content = re.sub(r'\*\*(.*?)\*\*', r'\1', content) formatted_lines.append(bold_content) if in_paragraph: formatted_lines.append("

") return '\n'.join(formatted_lines) # ------------------------------- # 스타일 및 프롬프트 가이드 함수 # ------------------------------- def get_style_prompt(style="친근한"): """블로그 글의 스타일 프롬프트를 반환""" prompts = { "친근한": """ [친근한 정보성 블로그 스타일 가이드] 1. 톤과 어조 - 대화하듯 편안하고 친근한 말투 사용 - 주제에 대한 관심과 호기심을 담은 표현 사용 2. 문장 및 어투 - 반드시 '해요체'로 작성, 절대 '습니다'체를 사용하지 말 것 - '~요'로 끝나도록 작성, '~다'로 끝나지 않게 하라 - 구어체 표현 사용 (예: "~했어요", "~인 것 같아요") - 적절한 감정 표현과 공감대 형성 3. 용어 및 설명 방식 - 전문 용어는 쉬운 단어로 풀어서 설명 - 비유나 은유를 활용하여 복잡한 개념 설명 - 수사의문문 활용하여 독자와 소통하는 느낌 주기 (예: "어떻게 생각하세요?", "이런 경험 있으신가요?") - 구체적 사례와 예시를 통한 개념 설명 4. 정보 전달 방식 - 개인적인 관점에 녹여 자연스럽게 정보 전달 - 복잡한 개념을 단계적으로 설명 - 독자가 실제로 활용할 수 있는 실용적 정보 제공 5. 독자와의 상호작용 - 독자의 의견을 물어보는 질문 포함 - 실생활에 적용할 수 있는 팁이나 조언 제공 주의사항: 자연스러운 대화체를 유지하면서 정보의 질과 내용의 깊이를 잃지 않도록 한다 """, "일반": """ [일반적인 정보성 블로그 스타일 가이드] 1. 톤과 어조 - 중립적이고 객관적인 톤 유지 - 적절한 존댓말 사용 (예: "~합니다", "~입니다") - 정보 전달 중심의 명확한 어투 2. 내용 구조 및 전개 - 명확한 주제 소개로 시작 - 논리적인 순서로 정보 전개 (배경 → 주요 개념 → 분석 → 적용 등) - 핵심 포인트를 강조하는 소제목 활용 - 적절한 길이의 단락으로 구성 3. 용어 및 설명 방식 - 일반적으로 이해하기 쉬운 용어 선택 - 필요시 전문 용어에 간단한 설명 추가 - 객관적인 정보 제공에 중점 - 균형 잡힌 시각에서 다양한 관점 제시 4. 정보 전달 방식 - 주제의 기본 개념과 원리 명확하게 제공 - 구체적인 예시와 사례 포함 - 최신 연구나 동향 참고 5. 독자 상호작용 - 적절히 독자의 생각을 묻는 질문 포함 - 추가 정보를 찾을 수 있는 키워드 제시 - 실용적인 적용 방안 제공 주의사항: 객관적 정보 제공을 중심으로 하되, 독자의 이해와 활용을 도울 수 있는 맥락과 설명을 충분히 제공한다 """, "전문적인": """ [전문적인 정보성 블로그 스타일 가이드] 1. 톤과 구조 - 공식적이고 전문적인 톤 사용 - 객관적이고 분석적인 접근 유지 - 명확한 서론(개요), 본론(상세 분석), 결론(종합 평가) 구조 - 체계적인 정보 전개 - 세부 섹션을 위한 명확한 소제목 사용 2. 내용 구성 및 전개 - 주제의 역사적 배경, 이론적 기반, 현재 동향 등 심층적 정보 포함 - 논리적 연결을 위한 전환어 활용 - 전문 용어 적절히 활용 (필요시 간략한 설명 제공) - 심층적인 분석과 비판적 평가 제공 - 다양한 관점과 이론적 프레임워크 제시 3. 데이터 및 근거 활용 - 통계, 연구 결과, 사례 연구 등 객관적 데이터 활용 - 주제 분석을 위한 체계적인 프레임워크 제시 - 수치 데이터는 명확히 설명 (추세, 상관관계, 인과관계 등) - 학술적 근거와 현실 적용의 균형 4. 전문적 정보 제공 - 최신 연구 동향 및 발전 방향 분석 - 이론과 실제 적용 사이의 간극 분석 - 주제 관련 쟁점과 논쟁점 소개 - 체계적인 문제 해결 접근법 제시 주의사항: 전문성과 깊이를 유지하면서도 이해 가능한 용어와 설명을 통해 접근성을 높인다 """ } return prompts.get(style, prompts["친근한"]) def get_category_outline_prompt(category="일반"): """카테고리별 아웃라인 생성 프롬프트""" prompts = { "일반": """ [일반 주제 소주제(Outline) 생성 규칙] [시스템 역할] 당신은 수년간의 경험을 가진 전문 정보성 블로그 작가입니다. 다양한 주제에 대한 깊이 있는 분석과 유용한 정보를 제공하여 많은 독자들의 신뢰를 받고 있습니다. [분석 단계] 1. 참고 자료 3개를 철저히 분석하여 핵심 주제와 중요 정보 파악 2. 포스팅 주제의 유형과 특성 식별 (건강, 기술, 교육, 취미, 경제 등) 3. 포스팅의 핵심이 될 5가지 주요 요소 파악 (개념 설명, 주요 이점, 실용적 적용, 최신 트렌드, 전문가 견해 등) [아웃라인 구성 원칙] 1. 도입부(1개) - 독자의 관심을 끄는 매력적인 제목으로 시작 2. 본론(4-5개) - 참고 자료 분석을 통해 발견한 주제의 핵심 가치와 중요 정보를 담은 소제목 - 주제의 기본 개념과 배경 지식 - 독자들이 가장 유용하게 활용할 수 있는 정보 - 최신 트렌드나 연구 결과 - 실용적인 적용 방법이나 팁 - (위 항목들은 예시일 뿐, 참고 자료 분석을 통해 자유롭게 결정) 3. 결론(1개) - 전체 내용을 요약하고 독자에게 유용한 인사이트를 제공하는 제목 [핵심 지침] 1. 완전히 한국어로만 작성할 것 2. 소제목은 최대 30자 이내로 간결하게 작성 3. 독자의 호기심과 관심을 자극하는 표현 사용 (예: "반드시 알아야 할", "놀라운 사실", "전문가들이 추천하는") 4. 주제의 가장 가치 있는 정보가 소제목에 반영되도록 구성 5. 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 전체 아웃라인은 도입부(1) + 본론(최대 5개) + 결론(1)으로 구성 [출력 형식] 1. 참고 자료 분석을 통해 가장 핵심적인 주제와 정보를 파악하여 자유롭게 아웃라인 구성 2. 하지만 반드시 다음 구조를 유지할 것:(각 항목당 1번 엔터를 적용하여 빈칸이 나오지 않도록하라.) 3. 도입부: 1개 (독자의 관심을 끄는 매력적인 제목) 4. 본론: 4-5개 (주제의 가장 중요한 정보와 가치를 반영한 제목) 5. 결론: 1개 (전체 내용 요약 제목) 6. 소제목은 주제의 핵심 정보와 가치에 맞게 자유롭게 구성 7. 키워드에 맞추지 말고, 참고 자료 분석을 통해 발견한 핵심 가치와 정보 기반으로 구성 8. 예시 형식 (참고용일 뿐, 내용은 참고 자료에 따라 완전히 달라질 수 있음): - 도입부: [관심을 끄는 도입 제목] - 본론1: [핵심 개념/배경 관련 제목] - 본론2: [주요 이점/가치 관련 제목] - 본론3: [최신 트렌드/연구 관련 제목] - 본론4: [실용적 적용/팁 관련 제목] - 본론5: [전문가 견해/주요 사례 관련 제목] (필요시) - 결론: [전체 내용 요약 및 인사이트 제목] """, "생활건강": """ [생활건강 주제 소주제(Outline) 생성 규칙] [시스템 역할] 당신은 수년간의 경험을 가진 생활건강 전문 블로거입니다. 영양, 운동, 정신건강, 수면, 자연치유법 등 일상에서 실천할 수 있는 건강 정보로 많은 독자들의 신뢰를 받고 있습니다. [분석 단계] 1. 참고 자료 3개를 철저히 분석하여 핵심 건강 주제와 중요 정보 파악 2. 건강 주제의 유형과 특성 식별 (영양, 운동, 스트레스 관리, 수면, 면역력 등) 3. 포스팅의 핵심이 될 5가지 주요 요소 파악 (과학적 근거, 실천 방법, 기대 효과, 주의사항, 전문가 조언 등) [아웃라인 구성 원칙] 1. 도입부(1개) - 건강 문제나 관심사에 대한 공감대를 형성하는 제목으로 시작 2. 본론(4-5개) - 참고 자료 분석을 통해 발견한 건강 관련 핵심 정보와 실천 방법을 담은 소제목 - 건강 이슈의 원인과 과학적 배경] - 일상에서 쉽게 실천할 수 있는 개선 방법 - 건강 습관이 가져오는 구체적 이점 - 흔한 오해와 주의사항 - 전문가 조언이나 최신 연구 결과 - (위 항목들은 예시일 뿐, 참고 자료 분석을 통해 자유롭게 결정) 3. 결론(1개) - 전체 건강 정보를 요약하고 지속적인 실천을 독려하는 제목 [핵심 지침] 1. 완전히 한국어로만 작성할 것 2. 소제목은 최대 30자 이내로 간결하게 작성 3. 독자의 건강 개선 의지를 자극하는 표현 사용 (예: "하루 5분만 투자하면", "전문의가 권장하는", "잘못된 건강 상식") 4. 과학적 근거와 실생활 적용 방법이 소제목에 반영되도록 구성 5. 건강 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 전체 아웃라인은 도입부(1) + 본론(최대 5개) + 결론(1)으로 구성 [출력 형식] 1. 참고 자료 분석을 통해 가장 핵심적인 건강 주제와 정보를 파악하여 자유롭게 아웃라인 구성 2. 하지만 반드시 다음 구조를 유지할 것:(각 항목당 1번 엔터를 적용하여 빈칸이 나오지 않도록하라.) 3. 도입부: 1개 (건강 문제에 공감하고 관심을 끄는 제목) 4. 본론: 4-5개 (건강 개선을 위한 핵심 정보와 실천법을 반영한 제목) 5. 결론: 1개 (전체 건강 정보 요약 및 실천 독려 제목) 6. 소제목은 건강 주제의 과학적 근거와 실천 방법에 맞게 자유롭게 구성 7. 키워드에 맞추지 말고, 참고 자료 분석을 통해 발견한 핵심 건강 가치와 정보 기반으로 구성 8. 예시 형식 (참고용일 뿐, 내용은 참고 자료에 따라 완전히 달라질 수 있음): - 도입부: [건강 문제 공감 및 관심 유도 제목] - 본론1: [건강 이슈 원인/배경 관련 제목] - 본론2: [실천 가능한 개선 방법 관련 제목] - 본론3: [기대할 수 있는 건강 이점 관련 제목] - 본론4: [주의사항/흔한 오해 관련 제목] - 본론5: [전문가 조언/최신 연구 관련 제목] (필요시) - 결론: [건강 정보 요약 및 실천 독려 제목] """ } return prompts.get(category, prompts["일반"]) def get_category_blog_prompt(category="일반"): """카테고리별 블로그 글 생성 프롬프트""" prompts = { "일반": """ [일반 주제 블로그 작성 가이드] 1. 너는 최고의 블로그 작가이자 정보 전달 전문가이다. 2. 주어진 아웃라인과 참고글을 바탕으로 독자에게 가치 있는 정보를 제공하라. 3. 정보의 정확성과 깊이를 유지하면서도 이해하기 쉽게 설명하라. [콘텐츠 작성 규칙] 1. 객관적인 정보 제공에 중점을 두되, 독자의 관심과 참여를 유도하라. 2. 복잡한 개념은 비유와 예시를 통해 쉽게 설명하라. 3. 주제에 대한 다양한 관점과 측면을 균형 있게 다루어라. 4. 독자가 실생활에 적용할 수 있는 실용적인 정보와 팁을 제공하라. 5. 참고글의 정보를 재구성하되, 표현을 단순히 복사하지 말고 창의적으로 재구성하라. 6. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상의 충분한 내용으로 작성하라. 7. 글 전체 길이는 최소 {TARGET_CHAR_LENGTH}자가 되도록 작성하라. 8. 글 전체 길이는 {TARGET_CHAR_LENGTH}자에서 {TARGET_CHAR_LENGTH + 1000}자 사이가 되도록 작성하라. [중요 규칙] 1. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 2. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 3. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 4. "참고글", "참고글에 따르면" 등의 표현을 사용하지 말라. 5. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라. """, "생활건강": """ [생활건강 주제 블로그 작성 가이드] 1. 너는 생활건강 및 웰빙 전문 블로거이자 건강 정보 전달자이다. 2. 과학적 근거를 바탕으로 정확한 건강 정보를 제공하라. 3. 복잡한 건강 개념을 일상적 맥락에서 이해하기 쉽게 설명하라. [콘텐츠 작성 규칙] 1. 건강 정보의 정확성과 신뢰성을 최우선으로 하라. 2. 의학적 주장은 과학적 근거나 연구 결과를 바탕으로 서술하라. 3. 과도한 주장이나 과장된 효과를 약속하지 말라. 4. 일상생활에서 쉽게 실천할 수 있는 구체적인 건강 관리 방법을 제시하라. 5. 건강 문제의 원인, 증상, 예방, 관리 방법을 체계적으로 설명하라. 6. 전문적인 의학 용어는 쉬운 설명과 함께 제공하라. 7. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상의 충분한 내용으로 작성하라. 8. 글 전체 길이는 최소 {TARGET_CHAR_LENGTH}자가 되도록 작성하라. [중요 규칙] 1. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 2. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 3. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 4. "참고글", "참고글에 따르면" 등의 표현을 사용하지 말라. 5. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라. 6. 의학적 조언이 필요한 심각한 증상의 경우 전문의 상담을 권고하라. """ } return prompts.get(category, prompts["일반"]) # ------------------------------- # 글 생성 관련 주요 함수 # ------------------------------- def call_gemini_api(prompt, temperature=TEMPERATURE, top_p=TOP_P): """Gemini API 호출 함수""" response = client.models.generate_content( model="gemini-2.0-flash", contents=[prompt], config=types.GenerateContentConfig( max_output_tokens=MAX_TOKENS, temperature=temperature, top_p=top_p ) ) return response.text.strip() def generate_outline(category, style, references1, references2, references3): """아웃라인 생성 함수""" try: category_prompt = get_category_outline_prompt(category) style_prompt = get_style_prompt(style) # 참고글 정보 준비 references = [ references1.strip() if references1.strip() else "참고 자료 없음", references2.strip() if references2.strip() else "참고 자료 없음", references3.strip() if references3.strip() else "참고 자료 없음" ] # 의미 있는 참고글만 필터링 meaningful_refs = [ref for ref in references if ref != "참고 자료 없음"] if not meaningful_refs: return "참고 자료가 없습니다. 최소 하나 이상의 참고 자료를 입력해주세요." outline_prompt = f""" [블로그 아웃라인 생성 요청] 카테고리: {category} 포스팅 스타일: {style} 참고글: {references[0]} {references[1] if len(meaningful_refs) > 1 else ""} {references[2] if len(meaningful_refs) > 2 else ""} {category_prompt} 아웃라인 생성 시 추가 지침: 1. 참고글의 핵심 주제와 가치 있는 정보를 정확히 파악하라. 2. 각 소주제는 20자 이내로 명확하고 매력적으로 작성하라. 3. 전체 아웃라인이 논리적 흐름과 일관성을 가지도록 구성하라. 4. 독자의 관심과 호기심을 유발하는 소주제를 설계하라. 5. 소주제만 간결하게 출력하고 설명은 포함하지 말라. 6. 각 소주제가 약속된 섹션(도입부, 본론1~5, 결론)에 적합한지 확인하라. 7. 백틱(```)이나 코드 블록 표시를 사용하지 말라. 8. 정확히 7줄로 구성하여 '도입부', '본론1~5', '결론'만 출력하라. """ # Gemini API 호출 outline_result = call_gemini_api(outline_prompt, temperature=0.7) # 결과 후처리 (불필요한 형식 제거) outline_result = re.sub(r'^\s*[-*]\s+', '', outline_result, flags=re.MULTILINE) outline_result = re.sub(r'^\s*\d+\.\s+', '', outline_result, flags=re.MULTILINE) # 백틱 및 코드 블록 제거 outline_result = re.sub(r'```[a-zA-Z]*\n?', '', outline_result) outline_result = re.sub(r'```', '', outline_result) # 정확히 7줄 형식으로 정리 lines = outline_result.strip().split('\n') clean_lines = [] for line in lines: line = line.strip() if line and (line.startswith('도입부:') or line.startswith('본론') or line.startswith('결론:')): clean_lines.append(line) # 정확히 7줄이 나오도록 조정 if len(clean_lines) > 7: clean_lines = clean_lines[:7] elif len(clean_lines) < 7: sections = ['도입부:', '본론1:', '본론2:', '본론3:', '본론4:', '본론5:', '결론:'] while len(clean_lines) < 7: missing_section = sections[len(clean_lines)] clean_lines.append(f"{missing_section} 첨부된 내용이 없거나, 일시적인 오류가 발생하였습니다. '아웃라인 생성' 버튼을 다시 눌러주세요.") return '\n'.join(clean_lines) except Exception as e: logging.error(f"아웃라인 생성 중 오류 발생: {str(e)}") return f"아웃라인 생성 중 오류 발생: {str(e)}" def post_process_blog(blog_content, style="친근한"): """블로그 컨텐츠 후처리 함수""" try: # 번호 목록, 불릿, 헤딩 등 제거 blog_content = re.sub(r'^\d+\.\s+', '', blog_content, flags=re.MULTILINE) blog_content = re.sub(r'^[\*\-\•]\s+', '', blog_content, flags=re.MULTILINE) blog_content = re.sub(r'^#+\s+', '', blog_content, flags=re.MULTILINE) # 스타일에 따른 어투 조정 if style == "친근한": blog_content = re.sub(r'([가-힣]+)고요', r'\1구요', blog_content) blog_content = re.sub(r'답니다', '어요', blog_content) blog_content = re.sub(r'였답니다', '였어요', blog_content) blog_content = re.sub(r'했답니다', '했어요', blog_content) blog_content = re.sub(r'습니다', '요', blog_content) blog_content = re.sub(r'합니다', '해요', blog_content) blog_content = re.sub(r'됩니다', '돼요', blog_content) blog_content = re.sub(r'입니다', '이에요', blog_content) # 과장된 표현 정리 exaggerated_expressions = [ (r'필수적인', r'중요한'), (r'혁명적인', r'중요한'), (r'놀라운', r'주목할 만한'), (r'기적의', r'효과적인'), (r'최고의', r'좋은'), (r'세계적인', r'유명한'), (r'완벽한', r'우수한'), (r'극적인', r'상당한'), (r'무한한', r'많은'), (r'절대적인', r'상당한'), (r'혁신적인', r'새로운'), (r'환상적인', r'좋은'), (r'근본적인', r'기본적인'), (r'획기적인', r'중요한'), (r'전례없는', r'특별한'), (r'압도적인', r'주목할 만한'), (r'황홀한', r'좋은'), (r'천상의', r'우수한'), (r'기가 막힌', r'효과적인'), (r'끝판왕', r'최상위'), (r'그 자체', r''), (r'이 .{1,10} 그 자체였어요', r'이 \1였어요'), (r'가 .{1,10} 그 자체였어요', r'가 \1였어요'), (r'압도적인', r'중요한'), (r'천국', r'좋은 곳'), (r'황홀했어요', r'좋았어요'), (r'환상의', r'좋은') ] for pattern, replacement in exaggerated_expressions: blog_content = re.sub(pattern, replacement, blog_content, flags=re.IGNORECASE) blog_content = re.sub(r'참고글에 따르면', r'알려진 바로는', blog_content) blog_content = re.sub(r'참고글', r'관련 정보', blog_content) return blog_content except Exception as e: logging.error(f"블로그 글 후처리 중 오류 발생: {str(e)}") return blog_content def generate_blog_post(category, style, outline_input, references1, references2, references3): """한 번의 호출로 전체 블로그 글 생성 함수 (퇴고 및 확장 기능 포함)""" try: # 참고글 준비 references = [ references1.strip() if references1.strip() else "참고 자료 없음", references2.strip() if references2.strip() else "참고 자료 없음", references3.strip() if references3.strip() else "참고 자료 없음" ] # 의미 있는 참고글만 필터링 references = [ref for ref in references if ref != "참고 자료 없음"] if not references: return "

참고 자료가 없습니다. 최소 하나 이상의 참고 자료를 입력해주세요.

", 0 if not outline_input.strip(): return "

아웃라인이 없습니다. 아웃라인을 입력해주세요.

", 0 # 카테고리 및 스타일 프롬프트 가져오기 category_prompt = get_category_blog_prompt(category) style_prompt = get_style_prompt(style) # Phase 1: 초기 블로그 글 생성 blog_prompt = f""" [블로그 글 작성 요청] 카테고리: {category} 포스팅 스타일: {style} 아웃라인: {outline_input} 참고글: {references[0]} {references[1] if len(references) > 1 else ""} {references[2] if len(references) > 2 else ""} {category_prompt} {style_prompt} [소제목 작성 가이드] 1. 본론의 각 부분마다 명확한 소제목을 사용하세요. 2. 소제목은 10~20자 내외로 명확하고 간결하게 작성하세요. 3. 소제목은 독립된 줄에 위치하고 앞뒤에 빈 줄이 있어야 합니다. 4. 소제목 예시: '효과적인 운동 방법', '건강한 식습관의 중요성', '스트레스 관리란?' [중요 작성 규칙] 1. 반드시 위의 아웃라인 순서와 구조에 따라 작성하라. 2. 각 섹션은 명확히 구분되어야 하며, 섹션 제목을 포함하라. 3. 도입부는 독자의 관심을 끌고 주제를 소개하는 방식으로 작성하라. 4. 본론 각 부분은 주제의 서로 다른 측면을 다루며, 구체적인 정보와 예시를 포함하라. 5. 결론은 핵심 내용을 요약하고 최종 메시지나 통찰을 제공하라. 6. 전체 글의 길이는 최소 {TARGET_CHAR_LENGTH}자가 되도록 작성하라. 7. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상의 충분한 내용으로 작성하라. 8. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 9. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 10. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 11. "참고글", "참고글에 따르면" 등의 표현을 사용하지 말라. 12. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라. 13. 과장된 표현이나 불필요한 반복을 피하라. 14. 각 섹션 사이에 자연스러운 연결성을 유지하라. 15. 글의 처음에 전체 글의 매력적인 제목을 반드시 추가하라. """ # Gemini API 호출 (한 번의 호출로 전체 글 생성) logging.info("전체 블로그 글 생성 시작") blog_content = call_gemini_api(blog_prompt, temperature=0.7) logging.info(f"생성된 원본 글 길이: {len(blog_content)}") # 후처리 processed_content = post_process_blog(blog_content, style) # HTML 변환하여 글자 수 체크 temp_html = format_blog_post(processed_content) char_count = len(extract_text_from_html(temp_html)) logging.info(f"초기 블로그 글 글자 수: {char_count}") # Phase 2: 글자 수가 목표에 미달하면 퇴고 및 확장 if char_count < TARGET_CHAR_LENGTH * 0.8: # 목표의 80% 미만이면 확장 logging.info(f"글자 수 부족 ({char_count} < {TARGET_CHAR_LENGTH * 0.8}), 확장 시도") # 가장 긴 참고글 선택 longest_ref = max(references, key=len) expansion_prompt = f""" [블로그 글 확장 요청] 카테고리: {category} 포스팅 스타일: {style} 원본 글: {processed_content} 참고글: {longest_ref} 문제점: 이 글은 목표 글자수인 {TARGET_CHAR_LENGTH}자에 미치지 못합니다. 현재 글자수는 약 {char_count}자입니다. 내용이 부실하여 확장이 필요합니다. {style_prompt} [확장 요구사항] 1. 원본 글의 구조와 아웃라인을 유지하면서 각 섹션의 내용을 대폭 확장하라. 2. 각 섹션에 더 구체적인 정보, 예시, 사례, 통계 등을 추가하라. 3. 전체 글자 수를 최소 {TARGET_CHAR_LENGTH}자 이상 달성하라. 4. 스타일과 어조는 일관성을 유지하라. 5. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 6. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 7. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 8. "참고글" 관련 표현을 사용하지 말라. 9. 부자연스러운 반복이나 과장된 표현을 피하라. """ # 확장 시도 expanded_content = call_gemini_api(expansion_prompt, temperature=0.75) processed_content = post_process_blog(expanded_content, style) # 다시 글자 수 체크 temp_html = format_blog_post(processed_content) char_count = len(extract_text_from_html(temp_html)) logging.info(f"확장 후 블로그 글 글자 수: {char_count}") # Phase 3: 여전히 부족하면 추가 확장 시도 if char_count < TARGET_CHAR_LENGTH * 0.9: # 목표의 90% 미만이면 추가 확장 logging.info(f"여전히 글자 수 부족 ({char_count} < {TARGET_CHAR_LENGTH * 0.9}), 추가 확장 시도") additional_expansion_prompt = f""" [블로그 글 추가 확장 요청] 카테고리: {category} 포스팅 스타일: {style} 원본 글: {processed_content} 문제점: 이 글은 여전히 목표 글자수인 {TARGET_CHAR_LENGTH}자에 미치지 못합니다. 현재 글자수는 약 {char_count}자입니다. [추가 확장 요구사항] 1. 본론 부분을 중심으로 세부 내용을 크게 확장하라. 2. 각 개념에 대한 더 깊은 설명과 실용적인 적용 방법을 추가하라. 3. 독자에게 유용한 핵심 정보와 인사이트를 더 풍부하게 제공하라. 4. 전체 글자 수를 최소 {TARGET_CHAR_LENGTH}자 이상으로 확장하라. 5. 스타일과 어조의 일관성을 유지하라. 6. 반복되는 내용이나 중복은 피하라. """ # 추가 확장 시도 further_expanded_content = call_gemini_api(additional_expansion_prompt, temperature=0.8) processed_content = post_process_blog(further_expanded_content, style) # 최종 HTML 변환 final_html = format_blog_post(processed_content) # 최종 글자 수 계산 final_char_count = len(extract_text_from_html(final_html)) logging.info(f"최종 블로그 글 글자 수: {final_char_count}") return final_html, final_char_count except Exception as e: logging.error(f"블로그 글 생성 중 오류 발생: {str(e)}") return f"

블로그 글 생성 중 오류 발생: {str(e)}

", 0 def format_filename(text): text = re.sub(r'[^\w\s-]', '', text) return text[:50].strip() # API 함수들 (첫 번째 파일의 API 엔드포인트와 정확히 일치하도록 작성) def generate_outline_1(category, style, ref1, ref2, ref3): """API endpoint: /generate_outline_1""" return generate_outline(category, style, ref1, ref2, ref3) def generate_blog_post_1(category, style, ref1, ref2, ref3, outline): """API endpoint: /generate_blog_post_1""" result = generate_blog_post(category, style, outline, ref1, ref2, ref3) return result[0] # 튜플이므로 첫 번째 요소(HTML)만 반환