import os import random import re import requests import logging import tempfile from bs4 import BeautifulSoup from datetime import datetime from zoneinfo import ZoneInfo import html from fpdf import FPDF from PIL import Image from urllib.request import urlopen import markdown2 import gradio as gr # 로깅 설정 (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 값 # Pretendard 폰트 파일 경로 FONT_REGULAR_PATH = os.path.join("Pretendard-Regular.otf") FONT_BOLD_PATH = os.path.join("Pretendard-Bold.otf") # 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 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("") 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개) - 참고 자료 분석을 통해 발견한 제품의 핵심 특징과 장단점을 담은 소제목 - 제품 기본 정보 및 스펙 - 카테고리별 핵심 성능/특징 평가 (전자제품이면 성능, 화장품이면 효과, 의류면 착용감 등) - 실제 사용 경험과 체감 품질 - 비슷한 제품과의 비교 분석 - 가격 대비 가치 평가 - (위 항목들은 제품 카테고리에 따라 유연하게 조정) - 결론(1개) - 전체 평가를 요약하고 구매 추천도를 담은 제목 [핵심 지침] 1. 완전히 한국어로만 작성할 것 2. 소제목은 최대 30자 이내로 간결하게 작성 3. 독자의 구매 결정에 도움을 주는 실용적인 표현 사용 (예: "놀라운 배터리 성능", "예상 밖의 단점", "알아두면 좋은 활용법") 4. 제품의 카테고리에 맞는 핵심 평가 요소가 소제목에 반영되도록 구성 5. 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 전체 아웃라인은 도입부(1) + 본론(최대 5개) + 결론(1)으로 구성 7. 다양한 제품 카테고리에 유연하게 적용할 수 있도록 구성 [출력 형식] 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. 기능의 실질적 가치와 성능을 강조하는 표현 사용 (예: "놀라운 처리 속도의 비밀", "숨겨진 고급 기능 완전 분석", "실측 테스트로 본 성능 한계") 4. 기술적 정확성과 객관적 성능 평가가 소제목에 반영되도록 구성 5. 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 전체 아웃라인은 도입부(1) + 본론(최대 5개) + 결론(1)으로 구성 7. 다양한 제품 카테고리의 기능 평가에 유연하게 적용할 수 있도록 구성 [출력 형식] 1. 참고 자료 분석을 통해 제품의 핵심 기능과 성능 요소를 파악하여 자유롭게 아웃라인 구성 2. 하지만 반드시 다음 구조를 유지할 것:(각 항목당 1번 엔터를 적용하여 빈칸이 나오지 않도록하라.) 3. 도입부: 1개 (제품의 주요 기능적 특징과 리뷰 목적을 담은 제목) 4. 본론: 4-5개 (핵심 기능과 성능 평가 요소를 반영한 제목) 5. 결론: 1개 (기능 중심 평가 요약 및 활용 가치 제목) 6. 소제목은 제품의 기능적 특징과 성능에 맞게 자유롭게 구성 7. 키워드에 맞추지 말고, 참고 자료 분석을 통해 발견한 핵심 기능과 성능 데이터 기반으로 구성 8. 예시 형식 (참고용일 뿐, 내용은 제품 카테고리와 참고 자료에 따라 완전히 달라질 수 있음): - 도입부: [핵심 기능 특징과 리뷰 목적 소개 제목] - 본론1: [주요 기능 작동 원리/기술 소개 관련 제목] - 본론2: [성능 테스트 결과/데이터 분석 관련 제목] - 본론3: [실제 사용 시나리오 기능 효율성 관련 제목] - 본론4: [기능적 한계/개선점 관련 제목] - 본론5: [경쟁 제품과의 기능 비교 관련 제목] (필요시) - 결론: [기능 중심 종합 평가 및 활용 가치 제목] """, "고객반응형": """ [상품리뷰 소주제(Outline) 생성 규칙 - 고객반응형] [시스템 역할] 당신은 수년간의 경험을 가진 고객 반응 분석 전문 리뷰 블로거입니다. 실제 사용자들의 다양한 평가를 수집·분석하여 제품의 진정한 가치와 한계점을 객관적으로 전달함으로써 많은 독자들의 신뢰를 받고 있습니다. [분석 단계] 1. 참고 자료와 실제 고객 리뷰 데이터를 철저히 분석하여 제품에 대한 주요 평가 경향성 파악 2. 제품의 카테고리와 특성에 맞는 고객 반응 패턴 식별 (각 카테고리별 중요 평가 요소) 3. 리뷰의 핵심이 될 5가지 주요 요소 파악: - 고객들이 가장 많이 언급한 장점 - 반복적으로 지적되는 단점 - 예상을 뛰어넘는 특이점(긍정/부정) - 구매자 유형별 만족도 차이 - 경쟁 제품과 비교한 고객 인식 [아웃라인 구성 원칙] 1. 도입부(1개) - 고객 반응 전체 경향성과 리뷰 목적을 담은 제목으로 시작 2. 본론(4-5개) - 고객 리뷰 분석을 통해 발견한 핵심 평가 요소와 경향성을 담은 소제목 - 고객들이 가장 높이 평가하는 제품의 핵심 가치 - 사용자들이 공통적으로 지적하는 아쉬운 점 - 실제 사용 경험에서 나타난 예상 외 특이점 - 사용자 유형/목적에 따른 만족도 차이 분석 - 시간 경과에 따른 평가 변화 또는 내구성 이슈 - (위 항목들은 제품 카테고리와 고객 반응 패턴에 따라 유연하게 조정) 3. 결론(1개) - 고객 평가 경향성을 종합하고 구매 적합성을 제시하는 제목 [핵심 지침] 1. 완전히 한국어로만 작성할 것 2. 소제목은 최대 30자 이내로 간결하게 작성 3. 실제 고객 반응을 객관적으로 전달하는 표현 사용 (예: "90%의 사용자가 인정한", "의외로 많은 불만이 나온", "장기 사용자들이 발견한 문제점") 4. 고객 평가의 주요 경향성과 특이점이 소제목에 반영되도록 구성 5. 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 전체 아웃라인은 도입부(1) + 본론(최대 5개) + 결론(1)으로 구성 7. 다양한 제품 카테고리의 고객 반응 패턴에 유연하게 적용할 수 있도록 구성 [출력 형식] 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. 반드시 {TARGET_CHAR_LENGTH}자 이상 작성하라 4. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상 작성하라 5. 상품의 특장점, 각종 정보, 팁등을 자세히 설명하라 6. 반드시 내가 직접 사용한 경험과 관점에서 작성하라 [텍스트 작성 규칙] 1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라 2. 참고글의 내용을 토대로 새롭게 구성하되, 다음을 반드시 준수하라: - 참고글에 언급된 닉네임, 이름, 회사명, 브랜드명 등을 그대로 사용하지 말 것 - 쿠팡파트너스, 광고, 제품 협찬, 소정의 금액이나 사은품을 받았다는 내용 제외할 것 - 참고글 작성자의 경험이 아닌 나의 직접 경험으로 재구성할 것 3. 전체 맥락을 이해하고 문장의 일관성을 유지하라 4. 쉽게 읽힐 수 있도록 쉬운 어휘로 작성 5. 시각적(디자인, 외관 등)인 부분, 스펙, 기능, 성능, 사용경험, 장단점, 가격 대비 성능(가성비), 추가비용등을 고려 6. 다른 제품과의 비교가 가능하다면 반영하라(수치, 데이터 포함) 7. 구체적으로 상품이 주는 유익(일상생활, 업무 등에서)에 대한 분석, 평가, 전후 비교, 경험, 추천등을 포함 8. 상품의 유지 관리 방법, 사용 팁 등을 공유하라 9. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라 10. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라 11. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라 12. "참고글", "참고글에 따르면", "~에 의하면" 등의 표현을 사용하지 말라 13. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라 14. 과장된 표현이나 불필요한 반복을 피하라 15. 수익형 블로그나 제휴마케팅 관련 표현을 일체 배제하라 16. 항상 직접 사용한 것처럼 1인칭 관점에서 작성하라 """, "기능집중형": """ [상품리뷰 콘텐츠 생성 규칙 - 기능집중형] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 기술 전문가이자 상품의 기능적 측면에 특화된 콘텐츠 작가이다 3. 반드시 {TARGET_CHAR_LENGTH}자 이상 작성하라 4. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상 작성하라 5. 반드시 상품의 기술적 측면과 기능적 요소에만 집중하여 작성하라 6. 기능에 대한 심층적이고 객관적인 분석, 데이터, 벤치마크 결과를 포함하라 7. 항상 직접 사용한 것처럼 1인칭 관점에서 작성하지만, 객관적인 기술 분석에 중점을 두라 [텍스트 작성 규칙] 1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라 2. 참고글의 내용을 토대로 새롭게 구성하되, 다음을 반드시 준수하라: - 참고글에 언급된 닉네임, 이름, 회사명, 브랜드명 등을 그대로 사용하지 말 것 - 쿠팡파트너스, 광고, 제품 협찬, 소정의 금액이나 사은품을 받았다는 내용 제외할 것 - 참고글 작성자의 경험이 아닌 기술 검증 과정과 분석 결과로 재구성할 것 3. 전체 맥락을 이해하고 문장의 일관성을 유지하라 4. 다음 요소들을 반드시 포함시켜라: - 정확한 기술 사양 및 스펙 데이터 (CPU, GPU, 메모리, 저장 공간 등) - 상세한 기능 설명 및 작동 원리 (각 기능이 어떻게 작동하는지 기술적으로 설명) - 벤치마크 테스트 및 성능 측정 결과 (구체적인 수치로 제시) - 이전 모델 또는 경쟁 제품과의 성능 비교 (표나 그래프 형태로 설명) - 사용자 설정 및 최적화 방법 (기능을 최대로 활용하기 위한 설정 방법) - 고급 기능 및 숨겨진 설정에 대한 상세 설명 - 기술적 한계와 개선 가능성 5. 감성적인 표현이나 주관적인 의견보다는 기술적 사실과 객관적인 데이터에 중점을 두라 6. 기술 전문 용어를 적절히 사용하되, 필요시 간략한 설명을 덧붙여라 7. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라 8. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라 9. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라 10. "참고글", "참고글에 따르면", "~에 의하면" 등의 표현을 사용하지 말라 11. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라 12. 과장된 표현이나 불필요한 반복을 피하라 13. 수익형 블로그나 제휴마케팅 관련 표현을 일체 배제하라 """, "고객반응형": """ [상품리뷰 콘텐츠 생성 규칙 - 고객반응형] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 다양한 고객 의견을 종합하고 분석하는 전문 리뷰어이다 3. 반드시 {TARGET_CHAR_LENGTH}자 이상 작성하라 4. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상 작성하라 5. 반드시 다양한 사용자들의 실제 경험과 의견을 자연스럽게 종합하는 형태로 작성하라 6. 다수의 의견을 중심으로 작성하되, 소수의 중요한 의견도 적절히 포함하라 7. 모든 섹션을 동일한 형식이나 패턴으로 작성하지 말고, 다양한 표현 방식을 활용하라 [텍스트 작성 규칙] 1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라 2. 참고글의 내용을 토대로 새롭게 구성하되, 다음을 반드시 준수하라: - 참고글에 언급된 닉네임, 이름, 회사명, 브랜드명 등을 그대로 사용하지 말 것 - 쿠팡파트너스, 광고, 제품 협찬, 소정의 금액이나 사은품을 받았다는 내용 제외할 것 - 참고글 작성자의 경험을 포함해 다양한 사용자들의 경험을 종합하는 형태로 재구성할 것 3. 전체 맥락을 이해하고 문장의 일관성을 유지하라 4. 다음과 같은 다양한 표현 방식을 섞어서 사용하라: - 통계와 수치를 활용한 종합적 의견 제시: "사용자의 약 75%가 디자인에 만족했으며..." - 사용자 집단별 의견 비교: "디자인 전공자들은 미니멀한 디자인을 높이 평가한 반면, 일반 사용자들은..." - 시간 경과에 따른 경험 변화: "구매 직후에는 높은 만족도를 보였으나, 3개월 사용 후에는..." - 실제 사용 환경별 평가: "사무실 환경에서는 성능이 뛰어났지만, 야외 활동 중에는..." - 온라인 커뮤니티 트렌드: "SNS에서는 카메라 기능이 화제를 모았으며..." - 전문가 의견 인용: "IT 전문 매체들은 주로 프로세서 성능을 높게 평가했지만..." 5. 문제점이나 단점을 언급할 때는 다음과 같은 방식을 활용하라: - 일관된 문제점: "대부분의 사용자가 공통적으로 지적한 문제는..." - 소수 의견 속 중요한 지적: "소수의 의견이지만 주목할 만한 지적은..." - 상황별 단점: "특정 상황에서만 나타나는 문제점으로는..." - 개선 가능성 언급: "사용자들이 지적한 이 문제는 향후 소프트웨어 업데이트로 개선될 가능성이..." 6. 정형화된 "A vs B" 구조를 반복적으로 사용하지 말고, 자연스러운 문장 흐름 속에서 다양한 의견을 표현하라 7. 마크다운 형식은 사용하지 말고 일반 텍스트로 작성하라 8. "참고글", "참고글에 따르면", "~에 의하면" 등의 표현을 사용하지 말라 9. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라 10. 과장된 표현이나 불필요한 반복을 피하라 11. 수익형 블로그나 제휴마케팅 관련 표현을 일체 배제하라 12. 각 섹션마다 다른 관점이나 접근 방식을 활용하여 내용의 다양성을 확보하라 """ } return prompts.get(category, prompts["일반"]) 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. 글의 처음에 전체 글의 매력적인 제목을 반드시 추가하라. 16. 반드시 구체적인 상품 정보(기능, 사양, 가격, 사용법, 구매팁 등)를 포함하라. """ # 카테고리별 특별 지침 추가 if category == "기능집중형": blog_prompt += """ [기능집중형 특별 지침] 1. 반드시 기술 사양과 기능에 대한 객관적인 데이터와 수치를 제공하라. 2. 각 기능의 작동 원리와 실제 성능을 기술적으로 설명하라. 3. 기능별로 사용법, 설정 방법, 최적화 팁을 상세히 제공하라. 4. 경쟁 제품 또는 이전 모델과의 기능 비교를 구체적인 수치와 함께 제공하라. 5. 기능 사용 시 발생할 수 있는 한계점과 해결 방법을 제시하라. 6. 전문 용어를 사용할 때는 간략한 설명을 함께 제공하라. """ elif category == "고객반응형": blog_prompt += """ [고객반응형 특별 지침] 1. 반드시 다양한 사용자 집단(연령, 성별, 사용 목적 등)의 의견을 포함하라. 2. "일부 사용자들은...", "많은 사람들이...", "20대 사용자들은...반면 40대 사용자들은..." 등의 표현으로 다양한 의견을 대비하라. 3. 긍정적 피드백과 부정적 피드백을 균형 있게 포함하라. 4. 온라인 커뮤니티, SNS, 리뷰 사이트 등 다양한 출처의 의견을 인용하는 형식으로 작성하라. 5. 전문가 의견과 일반 사용자 의견을 비교하여 제시하라. 6. 실제 사례와 경험담을 통해 제품에 대한 다양한 시각을 보여주라. """ # 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, category) # HTML 변환하여 글자 수 체크 temp_html = format_blog_post(processed_content) soup = BeautifulSoup(temp_html, 'html.parser') char_count = len(soup.get_text()) 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. 각 섹션에 더 구체적인 정보, 예시, 사례, 통계 등을 추가하라. """ # 카테고리별 확장 지침 추가 if category == "기능집중형": expansion_prompt += """ 3. 특히 다음 요소들을 추가하여 기능적 측면을 강화하라: - 각 기능의 기술적 작동 원리에 대한 더 상세한 설명 - 벤치마크 테스트 결과와 구체적인 성능 수치 - 기능 최적화를 위한 단계별 설정 방법 - 경쟁 제품과의 기능별 상세 비교 - 고급 사용자를 위한 숨겨진 설정과 기능 """ elif category == "고객반응형": expansion_prompt += """ 3. 특히 다음 요소들을 추가하여 다양한 고객 반응을 강화하라: - 더 많은 사용자 그룹(학생, 직장인, 크리에이터 등)의 다양한 의견 - 구체적인 사용 시나리오별 사용자 경험담 - 상반된 의견들 간의 더 명확한 대비 - 인용 형식의 실제 사용자 피드백과 반응 - 시간 경과에 따른 사용자 만족도 변화 """ else: expansion_prompt += """ 3. 특히 다음 요소들을 추가하라: - 상품 스펙, 기능, 가격, 구매처, 사용법 등 실용적 정보 추가 - 구체적인 사용 시나리오, 활용 팁, 주의사항 등 추가 - 유사 제품과의 비교 정보, 사용자 피드백 정보 등 추가 """ expansion_prompt += f""" 4. 전체 글자 수를 최소 {TARGET_CHAR_LENGTH}자 이상 달성하라. 5. 스타일과 어조는 일관성을 유지하라. 6. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 7. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 8. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 9. "참고글" 관련 표현을 사용하지 말라. 10. 부자연스러운 반복이나 과장된 표현을 피하라. """ # 확장 시도 expanded_content = call_gemini_api(expansion_prompt, temperature=0.75) processed_content = post_process_blog(expanded_content, style, category) # 다시 글자 수 체크 temp_html = format_blog_post(processed_content) soup = BeautifulSoup(temp_html, 'html.parser') char_count = len(soup.get_text()) 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. 본론 부분을 중심으로 세부 내용을 크게 확장하라. """ # 카테고리별 추가 확장 지침 if category == "기능집중형": additional_expansion_prompt += """ 2. 기술적 측면에서 다음 내용을 더 추가하라: - 각 핵심 기능의 세부 작동 메커니즘에 대한 기술적 설명 - 다양한 사용 환경에서의 성능 측정 데이터 - 하드웨어와 소프트웨어 간의 최적화 방식 - 숨겨진 개발자 모드나 고급 설정 옵션 - 기능 업데이트 예정 사항과 향후 가능성 """ elif category == "고객반응형": additional_expansion_prompt += """ 2. 다양한 사용자 반응 측면에서 다음 내용을 더 추가하라: - 다양한 국가와 문화권의 사용자 반응 차이 - 구매 후 시간 경과에 따른 만족도 변화 추이 - 특수한 사용 환경(극한 기후, 특수 직업 등)에서의 사용자 경험 - 제품 커뮤니티와 포럼에서의 주요 논쟁점 - 공식 지원팀의 피드백 대응에 대한 사용자 평가 """ else: additional_expansion_prompt += """ 2. 다음 항목에 대한 더 깊은 설명과 실용적인 적용 방법을 추가하라: - 상품의 특수한 사용 시나리오나 활용 사례 - 전문가들이 말하는 숨겨진 기능이나 팁 - 상품 사용 시 주의사항이나 유지관리 방법 - 구매 시 고려해야 할 추가 요소나 비용 - 구체적인 상품 스펙, 성능 데이터, 가격 정보 """ additional_expansion_prompt += f""" 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, category) # 최종 HTML 변환 final_html = format_blog_post(processed_content) # 최종 글자 수 계산 soup = BeautifulSoup(final_html, 'html.parser') final_char_count = len(soup.get_text()) 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 post_process_blog(blog_content, style="친근한", category="일반"): """블로그 컨텐츠 후처리 함수 - 카테고리별 특화 처리 추가""" 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) # 카테고리별 특화 처리 if category == "기능집중형": # 기술 용어 강화 및 구체적 수치 강조 tech_terms = [ (r'성능', r'기술적 성능'), (r'속도', r'처리 속도'), (r'화면', r'디스플레이'), (r'카메라', r'이미지 센서 시스템'), (r'배터리', r'전력 관리 시스템'), (r'사용', r'운용'), (r'좋다', r'효율적이다'), (r'빠르다', r'고성능이다') ] for pattern, replacement in tech_terms: blog_content = re.sub(r'\b' + pattern + r'\b', replacement, blog_content) # 객관적 분석 강화 blog_content = re.sub(r'제 생각에는', r'분석 결과에 따르면', blog_content) blog_content = re.sub(r'제가 봤을 때', r'기술적 관점에서', blog_content) blog_content = re.sub(r'느낌이 들어요', r'측정됩니다', blog_content) # 숫자와 단위 사이에 공백 추가 blog_content = re.sub(r'(\d+)([가-힣]+)', r'\1 \2', blog_content) elif category == "고객반응형": # 1. 다양한 사용자 집단 표현 사용 (단순 "사용자들"보다 구체적인 집단으로) user_group_patterns = [ # 중복 패턴 방지를 위해 충분히 긴 문맥과 함께 패턴 정의 (r'(많은|대부분의|일부) 사용자들은 ([가-힣\s]+)했', r'\1 사용자들은 \2했'), # 기존 패턴 유지 (r'사용자들이 ([가-힣\s]+)고 있', r'20~30대 사용자들이 \1고 있'), (r'사용자들의 ([가-힣\s]+)가 높', r'전문가와 일반 사용자들의 \1가 높'), (r'사용자들에게 ([가-힣\s]+)로 인기', r'크리에이터와 미디어 제작자들에게 \1로 인기'), (r'사용자들이 ([가-힣\s]+)를 지적', r'장시간 사용한 유저들이 \1를 지적') ] # 2. 다양한 의견 표현 패턴 (반복되는 A vs B 패턴 탈피) opinion_patterns = [ # 직접적 경험 -> 종합적 의견으로 변환 (r'저는 ([가-힣\s]+)했어요', [ r'사용자 설문에 따르면 대다수가 \1했다고 응답했습니다', r'주요 리뷰들을 종합해보면 \1한 경우가 많았습니다', r'사용 경험을 공유한 대부분의 유저들이 \1했다고 합니다' ]), (r'제 경험상 ([가-힣\s]+)는 것 같아요', [ r'장기 사용자들의 피드백을 보면 \1는 경향이 있습니다', r'온라인 커뮤니티에서는 \1는 의견이 지배적입니다', r'유튜브와 인스타그램의 리뷰들을 살펴보면 \1는 평가가 많습니다' ]), # 단순 의견 표현 -> 다양한 관점 표현으로 변환 (r'([가-힣\s]+)하다는 장점이 있어요', [ r'\1하다는 점이 주요 장점으로 꼽힙니다', r'\1하다는 특징에 만족한다는 사용자가 70% 이상입니다', r'\1하다는 점은 SNS에서도 가장 많이 언급되는 특징입니다' ]), (r'([가-힣\s]+)하다는 단점이 있어요', [ r'\1하다는 점은 일부 사용자들이 개선을 요구하는 부분입니다', r'\1하다는 한계점은 특히 전문 사용자들 사이에서 지적되고 있습니다', r'\1하다는 점은 사용자 포럼에서 자주 논의되는 문제입니다' ]) ] # 3. 대비되는 의견 표현 다양화 contrast_patterns = [ (r'([가-힣\s]+)하지만 ([가-힣\s]+)합니다', [ r'\1하는 경향이 있으나, 특정 상황에서는 \2한다는 의견도 있습니다', r'대체로 \1하지만, 일부 사용자들은 \2한다고 평가합니다', r'\1한다는 것이 다수 의견이지만, 전문가들은 \2하다고 지적합니다' ]), (r'([가-힣\s]+)는 반면에 ([가-힣\s]+)는', [ r'\1는 것으로 나타났습니다. 흥미롭게도 \2는', r'\1는 것이 주요 피드백입니다. 한편 다른 사용 환경에서는 \2는', r'\1는 경향이 두드러집니다. 사용 기간이 길어질수록 \2는' ]) ] # 패턴 랜덤 적용을 위한 시드 설정 import random random.seed(hash(blog_content[:100])) # 내용 일부를 시드로 사용하여 일관성 유지 # 패턴 적용 for pattern, replacements in opinion_patterns: matches = re.findall(pattern, blog_content) for match in matches: replacement = random.choice(replacements) blog_content = re.sub(pattern.replace(r'([가-힣\s]+)', re.escape(match)), replacement.replace(r'\1', match), blog_content, count=1) for pattern, replacements in contrast_patterns: for i in range(3): # 최대 3번 적용 match = re.search(pattern, blog_content) if match: groups = match.groups() if len(groups) == 2: replacement = random.choice(replacements) replacement = replacement.replace(r'\1', groups[0]).replace(r'\2', groups[1]) blog_content = re.sub(re.escape(match.group(0)), replacement, blog_content, count=1) # 사용자 그룹 패턴 적용 (마지막에 적용하여 다른 패턴과 충돌 방지) for pattern, replacement in user_group_patterns: blog_content = re.sub(pattern, replacement, blog_content) # 4. 데이터와 통계 표현 추가 (저는 ~했어요 -> 설문조사에 따르면 ~% 사용자가...) stats_patterns = [ (r'([가-힣\s]+)가 좋아요', r'설문에 참여한 사용자의 약 65%가 \1가 좋다고 평가했습니다'), (r'([가-힣\s]+)가 편리해요', r'사용자 리뷰 분석 결과 약 70%가 \1가 편리하다고 언급했습니다'), (r'([가-힣\s]+)가 아쉬워요', r'약 40%의 사용자가 \1가 아쉽다고 피드백을 남겼습니다') ] for pattern, replacement in stats_patterns: # 최대 3번만 적용하여 과도한 통계 표현 방지 for i in range(3): match = re.search(pattern, blog_content) if match: blog_content = re.sub(pattern.replace(r'([가-힣\s]+)', re.escape(match.group(1))), replacement.replace(r'\1', match.group(1)), blog_content, count=1) # 5. "A vs B" 패턴 줄이기 vs_pattern = r'([가-힣\s]+) vs ([가-힣\s]+)' vs_count = len(re.findall(vs_pattern, blog_content)) if vs_count > 2: # 3개 이상이면 일부 패턴 변경 replacements = [ r'\1과 \2의 차이점', r'\1 또는 \2에 따른 평가 차이', r'\1을 선호하는 집단과 \2를 선호하는 집단' ] # vs 패턴 매칭 및 변경 matches = re.findall(vs_pattern, blog_content) for i, match in enumerate(matches[2:]): # 처음 2개는 유지하고 나머지만 변경 replacement = replacements[i % len(replacements)] blog_content = re.sub(re.escape(f"{match[0]} vs {match[1]}"), replacement.replace(r'\1', match[0]).replace(r'\2', match[1]), blog_content, count=1) # 과장된 표현 정리 (공통 부분) 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'좋은'), (r'최적의', r'좋은'), (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) # 상품 리뷰 관련 표현 정리 product_specific_adjustments = [ (r'사용해보세요', r'사용해 보세요'), (r'구매해보세요', r'구매해 보세요'), (r'비교해보세요', r'비교해 보세요'), (r'확인해보세요', r'확인해 보세요'), (r'선택해보세요', r'선택해 보세요'), (r'테스트해보세요', r'테스트해 보세요'), (r'소비자들에게 (.*?)을 강력 추천합니다', r'소비자들에게 \1을 추천해요'), (r'소비자들에게 (.*?)를 강력 추천합니다', r'소비자들에게 \1를 추천해요'), (r'소비자들에게 (.*?)을 추천합니다', r'소비자들에게 \1을 추천해요'), (r'소비자들에게 (.*?)를 추천합니다', r'소비자들에게 \1를 추천해요'), (r'소비자에게 (.*?)을 강력 추천합니다', r'소비자에게 \1을 추천해요'), (r'소비자에게 (.*?)를 강력 추천합니다', r'소비자에게 \1를 추천해요') ] for pattern, replacement in product_specific_adjustments: blog_content = re.sub(pattern, replacement, blog_content) return blog_content except Exception as e: logging.error(f"블로그 글 후처리 중 오류 발생: {str(e)}") return blog_content 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} {style_prompt} 아웃라인 생성 시 추가 지침: 1. 참고글의 핵심 주제와 가치 있는 정보를 정확히 파악하라. 2. 각 소주제는 20자 이내로 명확하고 매력적으로 작성하라. 3. 전체 아웃라인이 논리적 흐름과 일관성을 가지도록 구성하라. 4. 독자의 관심과 호기심을 유발하는 소주제를 설계하라. 5. 소비자의 실질적인 필요와 관심사를 반영한 소주제를 구성하라. 6. 소주제만 간결하게 출력하고 설명은 포함하지 말라. 7. 각 소주제는 참고글의 내용에 따라 자유롭게 구성하되, 도입부와 결론은 반드시 포함하라. 8. 본론의 주제는 참고글의 내용을 면밀히 분석하여 가장 적합한 주제로 구성하라. 9. 반드시 총 7개의 섹션(도입부 1개, 본론 5개, 결론 1개)을 구성하라. 10. 전체적으로 상품의 주요 특징, 기능, 장단점, 사용 경험 등을 포괄할 수 있는 구조를 만들어라. """ # 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) return outline_result except Exception as e: logging.error(f"아웃라인 생성 중 오류 발생: {str(e)}") return f"아웃라인 생성 중 오류 발생: {str(e)}" def call_gemini_api(prompt, temperature=TEMPERATURE, top_p=TOP_P): """Gemini API 호출 함수""" try: logging.info("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 ) ) logging.info("Gemini API 호출 완료") return response.text.strip() except Exception as e: logging.error(f"Gemini API 호출 중 오류 발생: {str(e)}") return f"API 호출 중 오류 발생: {str(e)}" # PDF 클래스 정의 class PDF(FPDF): def __init__(self): super().__init__() self.add_font("Pretendard", "", FONT_REGULAR_PATH, uni=True) self.add_font("Pretendard", "B", FONT_BOLD_PATH, uni=True) def header(self): self.set_font("Pretendard", "", 10) def footer(self): self.set_y(-15) self.set_font("Pretendard", "", 8) self.cell(0, 10, f'Page {self.page_no()}', align='C') def save_to_pdf(blog_post, user_topic=""): try: logging.info("PDF 생성 시작") pdf = PDF() pdf.add_page() soup = BeautifulSoup(blog_post, 'html.parser') title = soup.h1.text.strip() if soup.h1 else user_topic or "상품 리뷰" logging.info(f"PDF 제목 설정: {title}") page_width = pdf.w - 2 * pdf.l_margin image_width = page_width image_url1 = "https://finalendai.com/wp-content/uploads/2024/10/pdf-banner-top.png" image_url2 = "https://finalendai.com/wp-content/uploads/2024/10/pdf-banner-bottom.png" logging.info("상단 이미지 삽입 시도") try: with urlopen(image_url1) as response: with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_file: tmp_file.write(response.read()) tmp_file_path = tmp_file.name img_width, img_height = Image.open(tmp_file_path).size ratio = img_height / img_width image_height = image_width * ratio x = (pdf.w - image_width) / 2 y = pdf.get_y() pdf.link(x, y, image_width, image_height, "https://finalendai.com") pdf.image(tmp_file_path, x=x, y=y, w=image_width) pdf.ln(image_height + 10) os.unlink(tmp_file_path) logging.info("상단 이미지 삽입 완료") except Exception as e: logging.error(f"상단 이미지 삽입 중 오류: {str(e)}") pdf.set_font("Pretendard", "B", 16) pdf.multi_cell(0, 10, title, align='C') pdf.ln(10) logging.info("PDF 본문 내용 추가 시작") pdf.set_font("Pretendard", "", 12) for tag in soup.find_all(["h2", "h3", "p", "ul", "li"]): tag_text = tag.get_text().strip() logging.info(f"추가 중인 본문 내용 [{tag.name}]: {tag_text}") if tag.name == "h2": pdf.set_font("Pretendard", "B", 14) pdf.multi_cell(0, 8, tag_text) pdf.ln(4) elif tag.name == "h3": pdf.set_font("Pretendard", "B", 12) pdf.multi_cell(0, 6, tag_text) pdf.ln(3) elif tag.name == "p": pdf.set_font("Pretendard", "", 12) pdf.multi_cell(0, 8, tag_text) pdf.ln(4) elif tag.name in ["ul", "li"]: pdf.set_font("Pretendard", "", 12) pdf.multi_cell(0, 8, f"• {tag_text}") pdf.ln(4) logging.info("PDF 본문 내용 추가 완료") logging.info("하단 이미지 삽입 시도") try: with urlopen(image_url2) as response: with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_file: tmp_file.write(response.read()) tmp_file_path = tmp_file.name if pdf.get_y() + image_height > pdf.page_break_trigger: pdf.add_page() x = (pdf.w - image_width) / 2 y = pdf.get_y() pdf.link(x, y, image_width, image_height, "https://finalendai.com/story/") pdf.image(tmp_file_path, x=x, y=y, w=image_width) os.unlink(tmp_file_path) logging.info("하단 이미지 삽입 완료") except Exception as e: logging.error(f"하단 이미지 삽입 중 오류: {str(e)}") now = datetime.now(ZoneInfo("Asia/Seoul")) filename = f"{now.strftime('%y%m%d_%H%M')}_{format_filename(title)}.pdf" pdf.output(filename) logging.info(f"PDF 파일 저장 완료: {filename}") return filename except Exception as e: logging.error(f"PDF 생성 중 오류 발생: {str(e)}") return None def format_filename(text): text = re.sub(r'[^\w\s-]', '', text) return text[:50].strip() def save_content_to_pdf(blog_post, user_topic=""): return save_to_pdf(blog_post, user_topic) # ------------------------------- # Gradio 인터페이스 구성 # ------------------------------- with gr.Blocks() as demo: gr.Markdown("# 상품 리뷰 블로그 포스팅 생성기") gr.Markdown("### 1단계: 포스팅 카테고리를 지정해주세요", elem_id="step-title") category = gr.Radio(choices=["일반", "기능집중형", "고객반응형"], label="포스팅 카테고리", value="일반") gr.Markdown("---\n\n") gr.Markdown("### 2단계: 포스팅 스타일을 선택해주세요", elem_id="step-title") style = gr.Radio(choices=["친근한", "일반", "전문적인"], label="포스팅 스타일", value="친근한") gr.Markdown("---\n\n") gr.Markdown("### 3단계: 참고 글을 입력하세요", elem_id="step-title") references1 = gr.Textbox(label="참고 글 1", placeholder="참고할 글을 복사하여 붙여넣으세요", lines=10) references2 = gr.Textbox(label="참고 글 2", placeholder="참고할 글을 복사하여 붙여넣으세요", lines=10) references3 = gr.Textbox(label="참고 글 3", placeholder="참고할 글을 복사하여 붙여넣으세요", lines=10) gr.Markdown("---\n\n") gr.Markdown("### 4단계: 아웃라인을 작성해주세요", elem_id="step-title") gr.HTML("[참고 글의 내용에 따라 자동으로 아웃라인이 생성됩니다. 필요한 경우 직접 수정하세요]") outline_generate_btn = gr.Button("아웃라인 생성하기") outline_result = gr.Textbox(label="아웃라인 결과", lines=15) outline_input = gr.Textbox(label="작성할 아웃라인을 입력해주세요", placeholder="생성된 아웃라인 복사, 수정해서 사용하세요", lines=10) outline_generate_btn.click( fn=generate_outline, inputs=[category, style, references1, references2, references3], outputs=[outline_result] ) gr.Markdown("---\n\n") gr.Markdown("### 5단계: 글 생성하기", elem_id="step-title") gr.HTML("[아웃라인을 확인하고 필요한 경우 수정한 후 글을 생성하세요]") generate_btn = gr.Button("블로그 글 생성하기") output = gr.HTML(label="생성된 블로그 글") char_count = gr.Number(label="글자 수", precision=0) generate_btn.click( fn=generate_blog_post, inputs=[category, style, outline_input, references1, references2, references3], outputs=[output, char_count], show_progress=True ) save_pdf_btn = gr.Button("PDF로 저장하기") pdf_output = gr.File(label="생성된 PDF 파일") save_pdf_btn.click( fn=save_content_to_pdf, inputs=[output, category], outputs=[pdf_output], show_progress=True ) gr.HTML(""" """) if __name__ == "__main__": demo.launch()