From af62f527666ad52ec57781d2e2618c7e4fa349c5 Mon Sep 17 00:00:00 2001 From: thkim7 Date: Tue, 16 Sep 2025 12:37:28 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=98=88=EC=8B=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=A1=9C=20GPT=ED=95=9C=ED=85=8C=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EC=83=9D=EC=84=B1=20=ED=9B=84=20blogger?= =?UTF-8?q?=EC=97=90=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/service/keyword_service.py | 1 - .../service/product_blog_posting_service.py | 405 ++++++++++++++++++ apps/pre-processing-service/pyproject.toml | 1 + 3 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 apps/pre-processing-service/app/service/product_blog_posting_service.py 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..6f728277 --- /dev/null +++ b/apps/pre-processing-service/app/service/product_blog_posting_service.py @@ -0,0 +1,405 @@ +# product_blog_posting_service.py +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 + +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') + + +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 파일에 설정되지 않았습니다.") + + openai.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 = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + 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. 구매 결정에 도움이 되는 정보 +6. 자연스러운 마무리 + +HTML 태그를 사용해서 구조화된 콘텐츠로 작성해주세요. +(예:

,

,

,