diff --git a/apps/pre-processing-service/app/core/config.py b/apps/pre-processing-service/app/core/config.py index ed54cc69..69e29d35 100644 --- a/apps/pre-processing-service/app/core/config.py +++ b/apps/pre-processing-service/app/core/config.py @@ -80,6 +80,9 @@ class BaseSettingsConfig(BaseSettings): # MeCab 사전 경로 (자동 감지) mecab_path: Optional[str] = None + # 테스트/추가용 필드 + openai_api_key: Optional[str] = None # << 이 부분 추가 + def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/apps/pre-processing-service/app/service/keyword_service.py b/apps/pre-processing-service/app/service/keyword_service.py index 575767ee..f8065fa3 100644 --- a/apps/pre-processing-service/app/service/keyword_service.py +++ b/apps/pre-processing-service/app/service/keyword_service.py @@ -1,4 +1,3 @@ -# Pydantic 모델을 가져오기 위해 schemas 파일 import import json import random diff --git a/apps/pre-processing-service/app/service/product_blog_posting_service.py b/apps/pre-processing-service/app/service/product_blog_posting_service.py new file mode 100644 index 00000000..129c4666 --- /dev/null +++ b/apps/pre-processing-service/app/service/product_blog_posting_service.py @@ -0,0 +1,387 @@ +import json +import logging +import os +from datetime import datetime +from typing import Dict, List, Optional, Any +from dataclasses import dataclass +from enum import Enum + +from openai import OpenAI +from dotenv import load_dotenv + +from app.service.blog.blogger_blog_post_adapter import BloggerBlogPostAdapter +from app.errors.BlogPostingException import * + +# 환경변수 로드 +load_dotenv(".env.dev") + +client = OpenAI() + + +class PostingStatus(Enum): + PENDING = "pending" + PROCESSING = "processing" + SUCCESS = "success" + FAILED = "failed" + RETRY = "retry" + + +@dataclass +class ProductData: + """크롤링된 상품 데이터 모델""" + + tag: str + product_url: str + title: str + price: int + rating: float + options: List[Dict[str, Any]] + material_info: Dict[str, str] + product_images: List[str] + crawled_at: str + + @classmethod + def from_dict(cls, data: Dict) -> "ProductData": + """딕셔너리에서 ProductData 객체 생성""" + product_detail = data.get("product_detail", {}) + return cls( + tag=data.get("tag", ""), + product_url=product_detail.get("url", ""), + title=product_detail.get("title", ""), + price=product_detail.get("price", 0), + rating=product_detail.get("rating", 0.0), + options=product_detail.get("options", []), + material_info=product_detail.get("material_info", {}), + product_images=product_detail.get("product_images", []), + crawled_at=data.get("crawled_at", ""), + ) + + +@dataclass +class BlogPostContent: + """생성된 블로그 포스트 콘텐츠""" + + title: str + content: str + tags: List[str] + + +@dataclass +class BlogContentRequest: + """블로그 콘텐츠 생성 요청""" + + content_style: str = "informative" # "informative", "promotional", "review" + target_keywords: List[str] = None + include_pricing: bool = True + include_specifications: bool = True + content_length: str = "medium" # "short", "medium", "long" + + +class ProductContentGenerator: + """GPT를 활용한 상품 블로그 콘텐츠 생성""" + + def __init__(self): + # 환경변수에서 OpenAI API 키 로드 + self.openai_api_key = os.getenv("OPENAI_API_KEY") + if not self.openai_api_key: + raise ValueError("OPENAI_API_KEY가 .env.dev 파일에 설정되지 않았습니다.") + + client.api_key = self.openai_api_key + + def generate_blog_content( + self, product_data: ProductData, request: BlogContentRequest + ) -> BlogPostContent: + """상품 데이터를 기반으로 블로그 콘텐츠 생성""" + + # 1. 상품 정보 정리 + product_info = self._format_product_info(product_data, request) + + # 2. 프롬프트 생성 + prompt = self._create_blog_prompt(product_info, request) + + # 3. GPT를 통한 콘텐츠 생성 + try: + + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + { + "role": "system", + "content": "당신은 전문적인 블로그 콘텐츠 작성자입니다. 상품 리뷰와 정보성 콘텐츠를 매력적이고 SEO 친화적으로 작성합니다.", + }, + {"role": "user", "content": prompt}, + ], + temperature=0.7, + max_tokens=2000, + ) + + generated_content = response.choices[0].message.content + + # 4. 콘텐츠 파싱 및 구조화 + return self._parse_generated_content( + generated_content, product_data, request + ) + + except Exception as e: + logging.error(f"콘텐츠 생성 실패: {e}") + return self._create_fallback_content(product_data, request) + + def _format_product_info( + self, product_data: ProductData, request: BlogContentRequest + ) -> str: + """상품 정보를 텍스트로 포맷팅""" + info_parts = [ + f"상품명: {product_data.title}", + ] + + # 가격 정보 추가 + if request.include_pricing and product_data.price: + info_parts.append(f"가격: {product_data.price:,}원") + + # 평점 정보 추가 + if product_data.rating: + info_parts.append(f"평점: {product_data.rating}/5.0") + + # 사양 정보 추가 + if request.include_specifications and product_data.material_info: + info_parts.append("\n상품 사양:") + for key, value in product_data.material_info.items(): + info_parts.append(f"- {key}: {value}") + + # 옵션 정보 추가 + if product_data.options: + info_parts.append(f"\n구매 옵션 ({len(product_data.options)}개):") + for i, option in enumerate(product_data.options[:5], 1): # 처음 5개만 + info_parts.append(f"{i}. {option.get('name', 'N/A')}") + + # 구매 링크 + if product_data.product_url: + info_parts.append(f"\n구매 링크: {product_data.product_url}") + + return "\n".join(info_parts) + + def _create_blog_prompt( + self, product_info: str, request: BlogContentRequest + ) -> str: + """블로그 작성용 프롬프트 생성""" + + # 스타일별 가이드라인 + style_guidelines = { + "informative": "객관적이고 상세한 정보 제공 중심으로, 독자가 제품을 이해할 수 있도록 전문적으로 작성", + "promotional": "제품의 장점과 매력을 강조하며, 구매 의욕을 자극할 수 있도록 매력적으로 작성", + "review": "실제 사용 경험을 바탕으로 한 솔직한 평가와 추천 중심으로 작성", + } + + # 길이별 가이드라인 + length_guidelines = { + "short": "800자 내외의 간결한 내용", + "medium": "1200자 내외의 적당한 길이", + "long": "1500자 이상의 상세한 내용", + } + + style_guide = style_guidelines.get( + request.content_style, style_guidelines["informative"] + ) + length_guide = length_guidelines.get( + request.content_length, length_guidelines["medium"] + ) + + # 키워드 정보 + keywords_text = "" + if request.target_keywords: + keywords_text = f"\n포함할 키워드: {', '.join(request.target_keywords)}" + + prompt = f""" +다음 상품 정보를 바탕으로 매력적인 블로그 포스트를 작성해주세요. + +상품 정보: +{product_info} + +작성 가이드라인: +- 스타일: {style_guide} +- 길이: {length_guide} +- 톤: 친근하면서도 신뢰할 수 있는, 정보 제공 중심{keywords_text} + +작성 요구사항: +1. SEO 친화적이고 클릭하고 싶은 매력적인 제목 +2. 독자의 관심을 끄는 도입부 +3. 상품의 핵심 특징과 장점을 구체적으로 설명 +4. 실제 사용 시나리오나 활용 팁 +5. 구매 결정에 도움이 되는 정보 + +⚠️ 주의: +- 절대로 마지막에 '이 HTML 구조는…' 같은 자기 평가 문장을 추가하지 마세요. +- 출력 시 ```나 ```html 같은 코드 블록 구문을 포함하지 마세요. +- 오직 HTML 태그만 사용하여 구조화된 콘텐츠를 작성해주세요. +(예:

,

,

,