diff --git a/smarketing-ai/.gitignore b/smarketing-ai/.gitignore
index 3bf780b..0ee64c1 100644
--- a/smarketing-ai/.gitignore
+++ b/smarketing-ai/.gitignore
@@ -1,2 +1,23 @@
-.idea
-.env
\ No newline at end of file
+# Python 가상환경
+venv/
+env/
+ENV/
+.venv/
+.env/
+
+# Python 캐시
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+
+# 환경 변수 파일
+.env
+.env.local
+.env.*.local
+
+# IDE 설정
+.vscode/
+.idea/
+*.swp
+*.swo
\ No newline at end of file
diff --git a/smarketing-ai/Dockerfile b/smarketing-ai/Dockerfile
index 897d2e0..8ad3c3f 100644
--- a/smarketing-ai/Dockerfile
+++ b/smarketing-ai/Dockerfile
@@ -1,4 +1,3 @@
-# Dockerfile
FROM python:3.11-slim
WORKDIR /app
diff --git a/smarketing-ai/app.py b/smarketing-ai/app.py
index abdb87b..a9e55d8 100644
--- a/smarketing-ai/app.py
+++ b/smarketing-ai/app.py
@@ -9,24 +9,29 @@ import os
from datetime import datetime
import traceback
from config.config import Config
-from services.content_service import ContentService
+# from services.content_service import ContentService
from services.poster_service import PosterService
-from models.request_models import ContentRequest, PosterRequest
+from services.sns_content_service import SnsContentService
+# from services.poster_generation_service import PosterGenerationService
+from models.request_models import ContentRequest, PosterRequest, SnsContentGetRequest, PosterContentGetRequest
def create_app():
"""Flask 애플리케이션 팩토리"""
app = Flask(__name__)
app.config.from_object(Config)
+
# CORS 설정
CORS(app)
+
# 업로드 폴더 생성
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'temp'), exist_ok=True)
os.makedirs('templates/poster_templates', exist_ok=True)
+
# 서비스 인스턴스 생성
- content_service = ContentService()
poster_service = PosterService()
+ sns_content_service = SnsContentService()
@app.route('/health', methods=['GET'])
def health_check():
@@ -37,16 +42,119 @@ def create_app():
'service': 'AI Marketing Service'
})
+ # ===== 새로운 API 엔드포인트 =====
+
+ @app.route('/api/ai/sns', methods=['POST'])
+ def generate_sns_content():
+ """
+ SNS 게시물 생성 API (새로운 요구사항)
+ Java 서버에서 JSON 형태로 요청받아 HTML 형식의 게시물 반환
+ """
+ try:
+ # JSON 요청 데이터 검증
+ if not request.is_json:
+ return jsonify({'error': 'Content-Type은 application/json이어야 합니다.'}), 400
+
+ data = request.get_json()
+ if not data:
+ return jsonify({'error': '요청 데이터가 없습니다.'}), 400
+
+ # 필수 필드 검증
+ required_fields = ['title', 'category', 'contentType', 'platform', 'images']
+ for field in required_fields:
+ if field not in data:
+ return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400
+
+ # 요청 모델 생성
+ sns_request = SnsContentGetRequest(
+ title=data.get('title'),
+ category=data.get('category'),
+ contentType=data.get('contentType'),
+ platform=data.get('platform'),
+ images=data.get('images', []),
+ requirement=data.get('requirement'),
+ toneAndManner=data.get('toneAndManner'),
+ emotionIntensity=data.get('emotionIntensity'),
+ eventName=data.get('eventName'),
+ startDate=data.get('startDate'),
+ endDate=data.get('endDate')
+ )
+
+ # SNS 콘텐츠 생성
+ result = sns_content_service.generate_sns_content(sns_request)
+
+ if result['success']:
+ return jsonify({'content': result['content']})
+ else:
+ return jsonify({'error': result['error']}), 500
+
+ except Exception as e:
+ app.logger.error(f"SNS 콘텐츠 생성 중 오류 발생: {str(e)}")
+ app.logger.error(traceback.format_exc())
+ return jsonify({'error': f'SNS 콘텐츠 생성 중 오류가 발생했습니다: {str(e)}'}), 500
+
+ @app.route('/api/ai/poster', methods=['POST'])
+ def generate_poster_content():
+ """
+ 홍보 포스터 생성 API (새로운 요구사항)
+ Java 서버에서 JSON 형태로 요청받아 OpenAI 이미지 URL 반환
+ """
+ try:
+ # JSON 요청 데이터 검증
+ if not request.is_json:
+ return jsonify({'error': 'Content-Type은 application/json이어야 합니다.'}), 400
+
+ data = request.get_json()
+ if not data:
+ return jsonify({'error': '요청 데이터가 없습니다.'}), 400
+
+ # 필수 필드 검증
+ required_fields = ['title', 'category', 'contentType', 'images']
+ for field in required_fields:
+ if field not in data:
+ return jsonify({'error': f'필수 필드가 누락되었습니다: {field}'}), 400
+
+ # 요청 모델 생성
+ poster_request = PosterContentGetRequest(
+ title=data.get('title'),
+ category=data.get('category'),
+ contentType=data.get('contentType'),
+ images=data.get('images', []),
+ photoStyle=data.get('photoStyle'),
+ requirement=data.get('requirement'),
+ toneAndManner=data.get('toneAndManner'),
+ emotionIntensity=data.get('emotionIntensity'),
+ eventName=data.get('eventName'),
+ startDate=data.get('startDate'),
+ endDate=data.get('endDate')
+ )
+
+ # 포스터 생성
+ result = poster_service.generate_poster(poster_request)
+
+ if result['success']:
+ return jsonify({'content': result['content']})
+ else:
+ return jsonify({'error': result['error']}), 500
+
+ except Exception as e:
+ app.logger.error(f"포스터 생성 중 오류 발생: {str(e)}")
+ app.logger.error(traceback.format_exc())
+ return jsonify({'error': f'포스터 생성 중 오류가 발생했습니다: {str(e)}'}), 500
+
+ # ===== 기존 API 엔드포인트 (하위 호환성) =====
+
@app.route('/api/content/generate', methods=['POST'])
def generate_content():
"""
- 마케팅 콘텐츠 생성 API
+ 마케팅 콘텐츠 생성 API (기존)
점주가 입력한 정보를 바탕으로 플랫폼별 맞춤 게시글 생성
"""
try:
# 요청 데이터 검증
if not request.form:
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
+
# 파일 업로드 처리
uploaded_files = []
if 'images' in request.files:
@@ -59,6 +167,7 @@ def create_app():
file_path = os.path.join(app.config['UPLOAD_FOLDER'], 'temp', unique_filename)
file.save(file_path)
uploaded_files.append(file_path)
+
# 요청 모델 생성
content_request = ContentRequest(
category=request.form.get('category', '음식'),
@@ -69,15 +178,19 @@ def create_app():
store_name=request.form.get('store_name', ''),
additional_info=request.form.get('additional_info', '')
)
+
# 콘텐츠 생성
- result = content_service.generate_content(content_request)
+ result = sns_content_service.generate_content(content_request)
+
# 임시 파일 정리
for file_path in uploaded_files:
try:
os.remove(file_path)
except OSError:
pass
+
return jsonify(result)
+
except Exception as e:
# 에러 발생 시 임시 파일 정리
for file_path in uploaded_files:
@@ -92,13 +205,14 @@ def create_app():
@app.route('/api/poster/generate', methods=['POST'])
def generate_poster():
"""
- 홍보 포스터 생성 API
+ 홍보 포스터 생성 API (기존)
점주가 입력한 정보를 바탕으로 시각적 홍보 포스터 생성
"""
try:
# 요청 데이터 검증
if not request.form:
return jsonify({'error': '요청 데이터가 없습니다.'}), 400
+
# 파일 업로드 처리
uploaded_files = []
if 'images' in request.files:
@@ -111,6 +225,7 @@ def create_app():
file_path = os.path.join(app.config['UPLOAD_FOLDER'], 'temp', unique_filename)
file.save(file_path)
uploaded_files.append(file_path)
+
# 요청 모델 생성
poster_request = PosterRequest(
category=request.form.get('category', '음식'),
@@ -122,15 +237,19 @@ def create_app():
discount_info=request.form.get('discount_info', ''),
additional_info=request.form.get('additional_info', '')
)
+
# 포스터 생성
result = poster_service.generate_poster(poster_request)
+
# 임시 파일 정리
for file_path in uploaded_files:
try:
os.remove(file_path)
except OSError:
pass
+
return jsonify(result)
+
except Exception as e:
# 에러 발생 시 임시 파일 정리
for file_path in uploaded_files:
diff --git a/smarketing-ai/models/request_models.py b/smarketing-ai/models/request_models.py
index 5abacf6..8816533 100644
--- a/smarketing-ai/models/request_models.py
+++ b/smarketing-ai/models/request_models.py
@@ -4,24 +4,61 @@ API 요청 데이터 구조를 정의
"""
from dataclasses import dataclass
from typing import List, Optional
+
+
+@dataclass
+class SnsContentGetRequest:
+ """SNS 게시물 생성 요청 모델"""
+ title: str
+ category: str
+ contentType: str
+ platform: str
+ images: List[str] # 이미지 URL 리스트
+ requirement: Optional[str] = None
+ toneAndManner: Optional[str] = None
+ emotionIntensity: Optional[str] = None
+ eventName: Optional[str] = None
+ startDate: Optional[str] = None
+ endDate: Optional[str] = None
+
+
+@dataclass
+class PosterContentGetRequest:
+ """홍보 포스터 생성 요청 모델"""
+ title: str
+ category: str
+ contentType: str
+ images: List[str] # 이미지 URL 리스트
+ photoStyle: Optional[str] = None
+ requirement: Optional[str] = None
+ toneAndManner: Optional[str] = None
+ emotionIntensity: Optional[str] = None
+ eventName: Optional[str] = None
+ startDate: Optional[str] = None
+ endDate: Optional[str] = None
+
+
+# 기존 모델들은 유지
@dataclass
class ContentRequest:
- """마케팅 콘텐츠 생성 요청 모델"""
- category: str # 음식, 매장, 이벤트
- platform: str # 네이버 블로그, 인스타그램
- image_paths: List[str] # 업로드된 이미지 파일 경로들
- start_time: Optional[str] = None # 이벤트 시작 시간
- end_time: Optional[str] = None # 이벤트 종료 시간
- store_name: Optional[str] = None # 매장명
- additional_info: Optional[str] = None # 추가 정보
+ """마케팅 콘텐츠 생성 요청 모델 (기존)"""
+ category: str
+ platform: str
+ image_paths: List[str]
+ start_time: Optional[str] = None
+ end_time: Optional[str] = None
+ store_name: Optional[str] = None
+ additional_info: Optional[str] = None
+
+
@dataclass
class PosterRequest:
- """홍보 포스터 생성 요청 모델"""
- category: str # 음식, 매장, 이벤트
- image_paths: List[str] # 업로드된 이미지 파일 경로들
- start_time: Optional[str] = None # 이벤트 시작 시간
- end_time: Optional[str] = None # 이벤트 종료 시간
- store_name: Optional[str] = None # 매장명
- event_title: Optional[str] = None # 이벤트 제목
- discount_info: Optional[str] = None # 할인 정보
- additional_info: Optional[str] = None # 추가 정보
\ No newline at end of file
+ """홍보 포스터 생성 요청 모델 (기존)"""
+ category: str
+ image_paths: List[str]
+ start_time: Optional[str] = None
+ end_time: Optional[str] = None
+ store_name: Optional[str] = None
+ event_title: Optional[str] = None
+ discount_info: Optional[str] = None
+ additional_info: Optional[str] = None
diff --git a/smarketing-ai/services/content_service.py b/smarketing-ai/services/content_service.py
deleted file mode 100644
index 34dc36b..0000000
--- a/smarketing-ai/services/content_service.py
+++ /dev/null
@@ -1,208 +0,0 @@
-"""
-마케팅 콘텐츠 생성 서비스
-AI를 활용하여 플랫폼별 맞춤 게시글 생성
-"""
-import os
-from typing import Dict, Any
-from datetime import datetime
-from utils.ai_client import AIClient
-from utils.image_processor import ImageProcessor
-from models.request_models import ContentRequest
-
-
-class ContentService:
- """마케팅 콘텐츠 생성 서비스 클래스"""
-
- def __init__(self):
- """서비스 초기화"""
- self.ai_client = AIClient()
- self.image_processor = ImageProcessor()
- # 플랫폼별 콘텐츠 특성 정의
- self.platform_specs = {
- '인스타그램': {
- 'max_length': 2200,
- 'hashtag_count': 15,
- 'style': '감성적이고 시각적',
- 'format': '짧은 문장, 해시태그 활용'
- },
- '네이버 블로그': {
- 'max_length': 3000,
- 'hashtag_count': 10,
- 'style': '정보성과 친근함',
- 'format': '구조화된 내용, 상세 설명'
- }
- }
- # 카테고리별 키워드 정의
- self.category_keywords = {
- '음식': ['맛집', '신메뉴', '추천', '맛있는', '특별한', '인기'],
- '매장': ['분위기', '인테리어', '편안한', '아늑한', '특별한', '방문'],
- '이벤트': ['할인', '이벤트', '특가', '한정', '기간한정', '혜택']
- }
-
- def generate_content(self, request: ContentRequest) -> Dict[str, Any]:
- """
- 마케팅 콘텐츠 생성
- Args:
- request: 콘텐츠 생성 요청 데이터
- Returns:
- 생성된 콘텐츠 정보
- """
- try:
- # 이미지 분석
- image_analysis = self._analyze_images(request.image_paths)
- # AI 프롬프트 생성
- prompt = self._create_content_prompt(request, image_analysis)
- # AI로 콘텐츠 생성
- generated_content = self.ai_client.generate_text(prompt)
- # 해시태그 생성
- hashtags = self._generate_hashtags(request)
- # 최종 콘텐츠 포맷팅
- formatted_content = self._format_content(
- generated_content,
- hashtags,
- request.platform
- )
- return {
- 'success': True,
- 'content': formatted_content,
- 'platform': request.platform,
- 'category': request.category,
- 'generated_at': datetime.now().isoformat(),
- 'image_count': len(request.image_paths),
- 'image_analysis': image_analysis
- }
- except Exception as e:
- return {
- 'success': False,
- 'error': str(e),
- 'generated_at': datetime.now().isoformat()
- }
-
- def _analyze_images(self, image_paths: list) -> Dict[str, Any]:
- """
- 업로드된 이미지들 분석
- Args:
- image_paths: 이미지 파일 경로 리스트
- Returns:
- 이미지 분석 결과
- """
- analysis_results = []
- for image_path in image_paths:
- try:
- # 이미지 기본 정보 추출
- image_info = self.image_processor.get_image_info(image_path)
- # AI를 통한 이미지 내용 분석
- image_description = self.ai_client.analyze_image(image_path)
- analysis_results.append({
- 'path': image_path,
- 'info': image_info,
- 'description': image_description
- })
- except Exception as e:
- analysis_results.append({
- 'path': image_path,
- 'error': str(e)
- })
- return {
- 'total_images': len(image_paths),
- 'results': analysis_results
- }
-
- def _create_content_prompt(self, request: ContentRequest, image_analysis: Dict[str, Any]) -> str:
- """
- AI 콘텐츠 생성을 위한 프롬프트 생성
- Args:
- request: 콘텐츠 생성 요청
- image_analysis: 이미지 분석 결과
- Returns:
- AI 프롬프트 문자열
- """
- platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램'])
- category_keywords = self.category_keywords.get(request.category, [])
- # 이미지 설명 추출
- image_descriptions = []
- for result in image_analysis.get('results', []):
- if 'description' in result:
- image_descriptions.append(result['description'])
- prompt = f"""
-당신은 소상공인을 위한 마케팅 콘텐츠 전문가입니다.
-다음 정보를 바탕으로 {request.platform}에 적합한 {request.category} 카테고리의 게시글을 작성해주세요.
-**매장 정보:**
-- 매장명: {request.store_name or '우리 가게'}
-- 카테고리: {request.category}
-- 추가 정보: {request.additional_info or '없음'}
-**이벤트 정보:**
-- 시작 시간: {request.start_time or '상시'}
-- 종료 시간: {request.end_time or '상시'}
-**이미지 분석 결과:**
-{chr(10).join(image_descriptions) if image_descriptions else '이미지 없음'}
-**플랫폼 특성:**
-- 최대 길이: {platform_spec['max_length']}자
-- 스타일: {platform_spec['style']}
-- 형식: {platform_spec['format']}
-**요구사항:**
-1. {request.platform}의 특성에 맞는 톤앤매너 사용
-2. {request.category} 카테고리에 적합한 내용 구성
-3. 고객의 관심을 끌 수 있는 매력적인 문구 사용
-4. 이미지와 연관된 내용으로 작성
-5. 자연스럽고 친근한 어조 사용
-해시태그는 별도로 생성하므로 본문에는 포함하지 마세요.
-"""
- return prompt
-
- def _generate_hashtags(self, request: ContentRequest) -> list:
- """
- 카테고리와 플랫폼에 맞는 해시태그 생성
- Args:
- request: 콘텐츠 생성 요청
- Returns:
- 해시태그 리스트
- """
- platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램'])
- category_keywords = self.category_keywords.get(request.category, [])
- hashtags = []
- # 기본 해시태그
- if request.store_name:
- hashtags.append(f"#{request.store_name.replace(' ', '')}")
- # 카테고리별 해시태그
- hashtags.extend([f"#{keyword}" for keyword in category_keywords[:5]])
- # 공통 해시태그
- common_tags = ['#맛집', '#소상공인', '#로컬맛집', '#일상', '#소통']
- hashtags.extend(common_tags)
- # 플랫폼별 인기 해시태그
- if request.platform == '인스타그램':
- hashtags.extend(['#인스타푸드', '#데일리', '#오늘뭐먹지', '#맛스타그램'])
- elif request.platform == '네이버 블로그':
- hashtags.extend(['#블로그', '#후기', '#추천', '#정보'])
- # 최대 개수 제한
- max_count = platform_spec['hashtag_count']
- return hashtags[:max_count]
-
- def _format_content(self, content: str, hashtags: list, platform: str) -> str:
- """
- 플랫폼에 맞게 콘텐츠 포맷팅
- Args:
- content: 생성된 콘텐츠
- hashtags: 해시태그 리스트
- platform: 플랫폼명
- Returns:
- 포맷팅된 최종 콘텐츠
- """
- platform_spec = self.platform_specs.get(platform, self.platform_specs['인스타그램'])
- # 길이 제한 적용
- if len(content) > platform_spec['max_length'] - 100: # 해시태그 공간 확보
- content = content[:platform_spec['max_length'] - 100] + '...'
- # 플랫폼별 포맷팅
- if platform == '인스타그램':
- # 인스타그램: 본문 + 해시태그
- hashtag_string = ' '.join(hashtags)
- formatted = f"{content}\n\n{hashtag_string}"
- elif platform == '네이버 블로그':
- # 네이버 블로그: 구조화된 형태
- hashtag_string = ' '.join(hashtags)
- formatted = f"{content}\n\n---\n{hashtag_string}"
- else:
- # 기본 형태
- hashtag_string = ' '.join(hashtags)
- formatted = f"{content}\n\n{hashtag_string}"
- return formatted
diff --git a/smarketing-ai/services/poster_service.py b/smarketing-ai/services/poster_service.py
index fc27adb..9ebbcb2 100644
--- a/smarketing-ai/services/poster_service.py
+++ b/smarketing-ai/services/poster_service.py
@@ -1,312 +1,190 @@
"""
-홍보 포스터 생성 서비스
-AI와 이미지 처리를 활용한 시각적 마케팅 자료 생성
+포스터 생성 서비스
+OpenAI를 사용한 이미지 생성 (한글 프롬프트)
"""
import os
-import base64
from typing import Dict, Any
from datetime import datetime
-from PIL import Image, ImageDraw, ImageFont
from utils.ai_client import AIClient
from utils.image_processor import ImageProcessor
-from models.request_models import PosterRequest
+from models.request_models import PosterContentGetRequest
class PosterService:
- """홍보 포스터 생성 서비스 클래스"""
+ """포스터 생성 서비스 클래스"""
def __init__(self):
"""서비스 초기화"""
self.ai_client = AIClient()
self.image_processor = ImageProcessor()
- # 포스터 기본 설정
- self.poster_config = {
- 'width': 1080,
- 'height': 1350, # 인스타그램 세로 비율
- 'background_color': (255, 255, 255),
- 'text_color': (50, 50, 50),
- 'accent_color': (255, 107, 107)
- }
- # 카테고리별 색상 테마
- self.category_themes = {
- '음식': {
- 'primary': (255, 107, 107), # 빨강
- 'secondary': (255, 206, 84), # 노랑
- 'background': (255, 248, 240) # 크림
- },
- '매장': {
- 'primary': (74, 144, 226), # 파랑
- 'secondary': (120, 198, 121), # 초록
- 'background': (248, 251, 255) # 연한 파랑
- },
- '이벤트': {
- 'primary': (156, 39, 176), # 보라
- 'secondary': (255, 193, 7), # 금색
- 'background': (252, 248, 255) # 연한 보라
- }
+
+ # 포토 스타일별 프롬프트
+ self.photo_styles = {
+ '미니멀': '미니멀하고 깔끔한 디자인, 단순함, 여백 활용',
+ '모던': '현대적이고 세련된 디자인, 깔끔한 레이아웃',
+ '빈티지': '빈티지 느낌, 레트로 스타일, 클래식한 색감',
+ '컬러풀': '다채로운 색상, 밝고 생동감 있는 컬러',
+ '우아한': '우아하고 고급스러운 느낌, 세련된 분위기',
+ '캐주얼': '친근하고 편안한 느낌, 접근하기 쉬운 디자인'
}
- def generate_poster(self, request: PosterRequest) -> Dict[str, Any]:
+ # 카테고리별 이미지 스타일
+ self.category_styles = {
+ '음식': '음식 사진, 먹음직스러운, 맛있어 보이는',
+ '매장': '레스토랑 인테리어, 아늑한 분위기',
+ '이벤트': '홍보용 디자인, 눈길을 끄는',
+ '메뉴': '메뉴 디자인, 정리된 레이아웃',
+ '할인': '세일 포스터, 할인 디자인'
+ }
+
+ # 톤앤매너별 디자인 스타일
+ self.tone_styles = {
+ '친근한': '따뜻하고 친근한 색감, 부드러운 느낌',
+ '정중한': '격식 있고 신뢰감 있는 디자인',
+ '재미있는': '밝고 유쾌한 분위기, 활기찬 색상',
+ '전문적인': '전문적이고 신뢰할 수 있는 디자인'
+ }
+
+ # 감정 강도별 디자인
+ self.emotion_designs = {
+ '약함': '은은하고 차분한 색감, 절제된 표현',
+ '보통': '적당히 활기찬 색상, 균형잡힌 디자인',
+ '강함': '강렬하고 임팩트 있는 색상, 역동적인 디자인'
+ }
+
+ def generate_poster(self, request: PosterContentGetRequest) -> Dict[str, Any]:
"""
- 홍보 포스터 생성
- Args:
- request: 포스터 생성 요청 데이터
- Returns:
- 생성된 포스터 정보
+ 포스터 생성 (OpenAI 이미지 URL 반환)
"""
try:
- # 포스터 텍스트 내용 생성
- poster_text = self._generate_poster_text(request)
- # 이미지 전처리
- processed_images = self._process_images(request.image_paths)
- # 포스터 이미지 생성
- poster_image = self._create_poster_image(request, poster_text, processed_images)
- # 이미지를 base64로 인코딩
- poster_base64 = self._encode_image_to_base64(poster_image)
+ # 참조 이미지 분석 (있는 경우)
+ image_analysis = self._analyze_reference_images(request.images)
+
+ # 포스터 생성 프롬프트 생성
+ prompt = self._create_poster_prompt(request, image_analysis)
+
+ # OpenAI로 이미지 생성
+ image_url = self.ai_client.generate_image_with_openai(prompt, "1024x1024")
+
return {
'success': True,
- 'poster_data': poster_base64,
- 'poster_text': poster_text,
- 'category': request.category,
- 'generated_at': datetime.now().isoformat(),
- 'image_count': len(request.image_paths),
- 'format': 'base64'
+ 'content': image_url
}
+
except Exception as e:
return {
'success': False,
- 'error': str(e),
- 'generated_at': datetime.now().isoformat()
+ 'error': str(e)
}
- def _generate_poster_text(self, request: PosterRequest) -> Dict[str, str]:
+ def _analyze_reference_images(self, image_urls: list) -> Dict[str, Any]:
"""
- 포스터에 들어갈 텍스트 내용 생성
- Args:
- request: 포스터 생성 요청
- Returns:
- 포스터 텍스트 구성 요소들
+ 참조 이미지들 분석
"""
- # 이미지 분석
- image_descriptions = []
- for image_path in request.image_paths:
- try:
- description = self.ai_client.analyze_image(image_path)
- image_descriptions.append(description)
- except:
- continue
- # AI 프롬프트 생성
- prompt = f"""
-당신은 소상공인을 위한 포스터 카피라이터입니다.
-다음 정보를 바탕으로 매력적인 포스터 문구를 작성해주세요.
-**매장 정보:**
-- 매장명: {request.store_name or '우리 가게'}
-- 카테고리: {request.category}
-- 추가 정보: {request.additional_info or '없음'}
-**이벤트 정보:**
-- 이벤트 제목: {request.event_title or '특별 이벤트'}
-- 할인 정보: {request.discount_info or '특가 진행'}
-- 시작 시간: {request.start_time or '상시'}
-- 종료 시간: {request.end_time or '상시'}
-**이미지 설명:**
-{chr(10).join(image_descriptions) if image_descriptions else '이미지 없음'}
-다음 형식으로 응답해주세요:
-1. 메인 헤드라인 (10글자 이내, 임팩트 있게)
-2. 서브 헤드라인 (20글자 이내, 구체적 혜택)
-3. 설명 문구 (30글자 이내, 친근하고 매력적으로)
-4. 행동 유도 문구 (15글자 이내, 액션 유도)
-각 항목은 줄바꿈으로 구분해서 작성해주세요.
-"""
- # AI로 텍스트 생성
- generated_text = self.ai_client.generate_text(prompt)
- # 생성된 텍스트 파싱
- lines = generated_text.strip().split('\n')
- return {
- 'main_headline': lines[0] if len(lines) > 0 else request.event_title or '특별 이벤트',
- 'sub_headline': lines[1] if len(lines) > 1 else request.discount_info or '지금 바로!',
- 'description': lines[2] if len(lines) > 2 else '특별한 혜택을 놓치지 마세요',
- 'call_to_action': lines[3] if len(lines) > 3 else '지금 방문하세요!'
- }
+ if not image_urls:
+ return {'total_images': 0, 'results': []}
- def _process_images(self, image_paths: list) -> list:
- """
- 포스터에 사용할 이미지들 전처리
- Args:
- image_paths: 원본 이미지 경로 리스트
- Returns:
- 전처리된 이미지 객체 리스트
- """
- processed_images = []
- for image_path in image_paths:
- try:
- # 이미지 로드 및 리사이즈
- image = Image.open(image_path)
- # RGBA로 변환 (투명도 처리)
- if image.mode != 'RGBA':
- image = image.convert('RGBA')
- # 포스터에 맞게 리사이즈 (최대 400x400)
- image.thumbnail((400, 400), Image.Resampling.LANCZOS)
- processed_images.append(image)
- except Exception as e:
- print(f"이미지 처리 오류 {image_path}: {e}")
- continue
- return processed_images
+ analysis_results = []
+ temp_files = []
- def _create_poster_image(self, request: PosterRequest, poster_text: Dict[str, str], images: list) -> Image.Image:
- """
- 실제 포스터 이미지 생성
- Args:
- request: 포스터 생성 요청
- poster_text: 포스터 텍스트
- images: 전처리된 이미지 리스트
- Returns:
- 생성된 포스터 이미지
- """
- # 카테고리별 테마 적용
- theme = self.category_themes.get(request.category, self.category_themes['음식'])
- # 캔버스 생성
- poster = Image.new('RGBA',
- (self.poster_config['width'], self.poster_config['height']),
- theme['background'])
- draw = ImageDraw.Draw(poster)
- # 폰트 설정 (시스템 기본 폰트 사용)
try:
- # 다양한 폰트 시도
- title_font = ImageFont.truetype("arial.ttf", 60)
- subtitle_font = ImageFont.truetype("arial.ttf", 40)
- text_font = ImageFont.truetype("arial.ttf", 30)
- small_font = ImageFont.truetype("arial.ttf", 24)
- except:
- # 기본 폰트 사용
- title_font = ImageFont.load_default()
- subtitle_font = ImageFont.load_default()
- text_font = ImageFont.load_default()
- small_font = ImageFont.load_default()
- # 레이아웃 계산
- y_pos = 80
- # 1. 메인 헤드라인
- main_headline = poster_text['main_headline']
- bbox = draw.textbbox((0, 0), main_headline, font=title_font)
- text_width = bbox[2] - bbox[0]
- x_pos = (self.poster_config['width'] - text_width) // 2
- draw.text((x_pos, y_pos), main_headline,
- fill=theme['primary'], font=title_font)
- y_pos += 100
- # 2. 서브 헤드라인
- sub_headline = poster_text['sub_headline']
- bbox = draw.textbbox((0, 0), sub_headline, font=subtitle_font)
- text_width = bbox[2] - bbox[0]
- x_pos = (self.poster_config['width'] - text_width) // 2
- draw.text((x_pos, y_pos), sub_headline,
- fill=theme['secondary'], font=subtitle_font)
- y_pos += 80
- # 3. 이미지 배치 (있는 경우)
- if images:
- image_y = y_pos + 30
- if len(images) == 1:
- # 단일 이미지: 중앙 배치
- img = images[0]
- img_x = (self.poster_config['width'] - img.width) // 2
- poster.paste(img, (img_x, image_y), img)
- y_pos = image_y + img.height + 50
- elif len(images) == 2:
- # 두 개 이미지: 나란히 배치
- total_width = sum(img.width for img in images) + 20
- start_x = (self.poster_config['width'] - total_width) // 2
- for i, img in enumerate(images):
- img_x = start_x + (i * (img.width + 20))
- poster.paste(img, (img_x, image_y), img)
- y_pos = image_y + max(img.height for img in images) + 50
- else:
- # 여러 이미지: 그리드 형태
- cols = 2
- rows = (len(images) + cols - 1) // cols
- img_spacing = 20
- for i, img in enumerate(images[:4]): # 최대 4개
- row = i // cols
- col = i % cols
- img_x = (self.poster_config['width'] // cols) * col + \
- (self.poster_config['width'] // cols - img.width) // 2
- img_y = image_y + row * (200 + img_spacing)
- poster.paste(img, (img_x, img_y), img)
- y_pos = image_y + rows * (200 + img_spacing) + 30
- # 4. 설명 문구
- description = poster_text['description']
- # 긴 텍스트는 줄바꿈 처리
- words = description.split()
- lines = []
- current_line = []
- for word in words:
- test_line = ' '.join(current_line + [word])
- bbox = draw.textbbox((0, 0), test_line, font=text_font)
- if bbox[2] - bbox[0] < self.poster_config['width'] - 100:
- current_line.append(word)
- else:
- if current_line:
- lines.append(' '.join(current_line))
- current_line = [word]
- if current_line:
- lines.append(' '.join(current_line))
- for line in lines:
- bbox = draw.textbbox((0, 0), line, font=text_font)
- text_width = bbox[2] - bbox[0]
- x_pos = (self.poster_config['width'] - text_width) // 2
- draw.text((x_pos, y_pos), line, fill=(80, 80, 80), font=text_font)
- y_pos += 40
- y_pos += 30
- # 5. 기간 정보 (있는 경우)
- if request.start_time and request.end_time:
- period_text = f"기간: {request.start_time} ~ {request.end_time}"
- bbox = draw.textbbox((0, 0), period_text, font=small_font)
- text_width = bbox[2] - bbox[0]
- x_pos = (self.poster_config['width'] - text_width) // 2
- draw.text((x_pos, y_pos), period_text, fill=(120, 120, 120), font=small_font)
- y_pos += 50
- # 6. 행동 유도 문구 (버튼 스타일)
- cta_text = poster_text['call_to_action']
- bbox = draw.textbbox((0, 0), cta_text, font=subtitle_font)
- text_width = bbox[2] - bbox[0]
- text_height = bbox[3] - bbox[1]
- # 버튼 배경
- button_width = text_width + 60
- button_height = text_height + 30
- button_x = (self.poster_config['width'] - button_width) // 2
- button_y = self.poster_config['height'] - 150
- draw.rounded_rectangle([button_x, button_y, button_x + button_width, button_y + button_height],
- radius=25, fill=theme['primary'])
- # 버튼 텍스트
- text_x = button_x + (button_width - text_width) // 2
- text_y = button_y + (button_height - text_height) // 2
- draw.text((text_x, text_y), cta_text, fill=(255, 255, 255), font=subtitle_font)
- # 7. 매장명 (하단)
- if request.store_name:
- store_text = request.store_name
- bbox = draw.textbbox((0, 0), store_text, font=text_font)
- text_width = bbox[2] - bbox[0]
- x_pos = (self.poster_config['width'] - text_width) // 2
- y_pos = self.poster_config['height'] - 50
- draw.text((x_pos, y_pos), store_text, fill=(100, 100, 100), font=text_font)
- return poster
+ for image_url in image_urls:
+ # 이미지 다운로드
+ temp_path = self.ai_client.download_image_from_url(image_url)
+ if temp_path:
+ temp_files.append(temp_path)
- def _encode_image_to_base64(self, image: Image.Image) -> str:
+ try:
+ # 이미지 분석
+ image_description = self.ai_client.analyze_image(temp_path)
+ # 색상 분석
+ colors = self.image_processor.analyze_colors(temp_path, 3)
+
+ analysis_results.append({
+ 'url': image_url,
+ 'description': image_description,
+ 'dominant_colors': colors
+ })
+ except Exception as e:
+ analysis_results.append({
+ 'url': image_url,
+ 'error': str(e)
+ })
+
+ return {
+ 'total_images': len(image_urls),
+ 'results': analysis_results
+ }
+
+ finally:
+ # 임시 파일 정리
+ for temp_file in temp_files:
+ try:
+ os.remove(temp_file)
+ except:
+ pass
+
+ def _create_poster_prompt(self, request: PosterContentGetRequest, image_analysis: Dict[str, Any]) -> str:
"""
- PIL 이미지를 base64 문자열로 인코딩
- Args:
- image: PIL 이미지 객체
- Returns:
- base64 인코딩된 이미지 문자열
+ 포스터 생성을 위한 AI 프롬프트 생성 (한글)
"""
- import io
- # RGB로 변환 (JPEG 저장을 위해)
- if image.mode == 'RGBA':
- # 흰색 배경과 합성
- background = Image.new('RGB', image.size, (255, 255, 255))
- background.paste(image, mask=image.split()[-1])
- image = background
- # 바이트 스트림으로 변환
- img_buffer = io.BytesIO()
- image.save(img_buffer, format='JPEG', quality=90)
- img_buffer.seek(0)
- # base64 인코딩
- img_base64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
- return f"data:image/jpeg;base64,{img_base64}"
+ # 기본 스타일 설정
+ photo_style = self.photo_styles.get(request.photoStyle, '현대적이고 깔끔한 디자인')
+ category_style = self.category_styles.get(request.category, '홍보용 디자인')
+ tone_style = self.tone_styles.get(request.toneAndManner, '친근하고 따뜻한 느낌')
+ emotion_design = self.emotion_designs.get(request.emotionIntensity, '적당히 활기찬 디자인')
+
+ # 참조 이미지 설명
+ reference_descriptions = []
+ for result in image_analysis.get('results', []):
+ if 'description' in result:
+ reference_descriptions.append(result['description'])
+
+ # 색상 정보
+ color_info = ""
+ if image_analysis.get('results'):
+ colors = image_analysis['results'][0].get('dominant_colors', [])
+ if colors:
+ color_info = f"참조 색상 팔레트: {colors[:3]}을 활용한 조화로운 색감"
+
+ prompt = f"""
+한국의 음식점/카페를 위한 전문적인 홍보 포스터를 디자인해주세요.
+
+**메인 콘텐츠:**
+- 제목: "{request.title}"
+- 카테고리: {request.category}
+- 콘텐츠 타입: {request.contentType}
+
+**디자인 스타일 요구사항:**
+- 포토 스타일: {photo_style}
+- 카테고리 스타일: {category_style}
+- 톤앤매너: {tone_style}
+- 감정 강도: {emotion_design}
+
+**이벤트 정보:**
+- 이벤트명: {request.eventName or '특별 프로모션'}
+- 시작일: {request.startDate or '지금'}
+- 종료일: {request.endDate or '한정 기간'}
+
+**특별 요구사항:**
+{request.requirement or '눈길을 끄는 전문적인 디자인'}
+
+**참조 이미지 설명:**
+{chr(10).join(reference_descriptions) if reference_descriptions else '참조 이미지 없음'}
+
+{color_info}
+
+**디자인 가이드라인:**
+- 한국 음식점/카페에 적합한 깔끔하고 현대적인 레이아웃
+- 한글 텍스트 요소를 자연스럽게 포함
+- 가독성이 좋은 전문적인 타이포그래피
+- 명확한 대비로 읽기 쉽게 구성
+- 소셜미디어 공유에 적합한 크기
+- 저작권이 없는 오리지널 디자인
+- 음식점에 어울리는 맛있어 보이는 색상 조합
+- 고객의 시선을 끄는 매력적인 비주얼
+
+고객들이 음식점을 방문하고 싶게 만드는 시각적으로 매력적인 포스터를 만들어주세요.
+텍스트는 한글로, 전체적인 분위기는 한국적 감성에 맞게 디자인해주세요.
+"""
+ return prompt
diff --git a/smarketing-ai/services/sns_content_service.py b/smarketing-ai/services/sns_content_service.py
new file mode 100644
index 0000000..e5090e0
--- /dev/null
+++ b/smarketing-ai/services/sns_content_service.py
@@ -0,0 +1,218 @@
+"""
+SNS 콘텐츠 생성 서비스
+"""
+import os
+from typing import Dict, Any
+from datetime import datetime
+from utils.ai_client import AIClient
+from utils.image_processor import ImageProcessor
+from models.request_models import SnsContentGetRequest
+
+
+class SnsContentService:
+
+ def __init__(self):
+ """서비스 초기화"""
+ self.ai_client = AIClient()
+ self.image_processor = ImageProcessor()
+
+ # 플랫폼별 콘텐츠 특성 정의
+ self.platform_specs = {
+ '인스타그램': {
+ 'max_length': 2200,
+ 'hashtag_count': 15,
+ 'style': '감성적이고 시각적',
+ 'format': '짧은 문장, 해시태그 활용'
+ },
+ '네이버 블로그': {
+ 'max_length': 3000,
+ 'hashtag_count': 10,
+ 'style': '정보성과 친근함',
+ 'format': '구조화된 내용, 상세 설명'
+ }
+ }
+
+ # 톤앤매너별 스타일
+ self.tone_styles = {
+ '친근한': '반말, 이모티콘 활용, 편안한 어조',
+ '정중한': '존댓말, 격식 있는 표현, 신뢰감 있는 어조',
+ '재미있는': '유머 섞인 표현, 트렌디한 말투, 참신한 비유',
+ '전문적인': '전문 용어 활용, 체계적 설명, 신뢰성 강조'
+ }
+
+ # 감정 강도별 표현
+ self.emotion_levels = {
+ '약함': '은은하고 차분한 표현',
+ '보통': '적당히 활기찬 표현',
+ '강함': '매우 열정적이고 강렬한 표현'
+ }
+
+ def generate_sns_content(self, request: SnsContentGetRequest) -> Dict[str, Any]:
+ """
+ SNS 콘텐츠 생성 (HTML 형식 반환)
+ """
+ try:
+ # 이미지 다운로드 및 분석
+ image_analysis = self._analyze_images_from_urls(request.images)
+
+ # AI 프롬프트 생성
+ prompt = self._create_sns_prompt(request, image_analysis)
+
+ # AI로 콘텐츠 생성
+ generated_content = self.ai_client.generate_text(prompt)
+
+ # HTML 형식으로 포맷팅
+ html_content = self._format_to_html(generated_content, request)
+
+ return {
+ 'success': True,
+ 'content': html_content
+ }
+
+ except Exception as e:
+ return {
+ 'success': False,
+ 'error': str(e)
+ }
+
+ def _analyze_images_from_urls(self, image_urls: list) -> Dict[str, Any]:
+ """
+ URL에서 이미지를 다운로드하고 분석
+ """
+ analysis_results = []
+ temp_files = []
+
+ try:
+ for image_url in image_urls:
+ # 이미지 다운로드
+ temp_path = self.ai_client.download_image_from_url(image_url)
+ if temp_path:
+ temp_files.append(temp_path)
+
+ # 이미지 분석
+ try:
+ image_info = self.image_processor.get_image_info(temp_path)
+ image_description = self.ai_client.analyze_image(temp_path)
+
+ analysis_results.append({
+ 'url': image_url,
+ 'info': image_info,
+ 'description': image_description
+ })
+ except Exception as e:
+ analysis_results.append({
+ 'url': image_url,
+ 'error': str(e)
+ })
+
+ return {
+ 'total_images': len(image_urls),
+ 'results': analysis_results
+ }
+
+ finally:
+ # 임시 파일 정리
+ for temp_file in temp_files:
+ try:
+ os.remove(temp_file)
+ except:
+ pass
+
+ def _create_sns_prompt(self, request: SnsContentGetRequest, image_analysis: Dict[str, Any]) -> str:
+ """
+ SNS 콘텐츠 생성을 위한 AI 프롬프트 생성
+ """
+ platform_spec = self.platform_specs.get(request.platform, self.platform_specs['인스타그램'])
+ tone_style = self.tone_styles.get(request.toneAndManner, '친근한 어조')
+ emotion_level = self.emotion_levels.get(request.emotionIntensity, '적당한 강도')
+
+ # 이미지 설명 추출
+ image_descriptions = []
+ for result in image_analysis.get('results', []):
+ if 'description' in result:
+ image_descriptions.append(result['description'])
+
+ prompt = f"""
+당신은 소상공인을 위한 SNS 마케팅 콘텐츠 전문가입니다.
+다음 정보를 바탕으로 {request.platform}에 적합한 게시글을 작성해주세요.
+
+**게시물 정보:**
+- 제목: {request.title}
+- 카테고리: {request.category}
+- 콘텐츠 타입: {request.contentType}
+
+**스타일 요구사항:**
+- 톤앤매너: {request.toneAndManner} ({tone_style})
+- 감정 강도: {request.emotionIntensity} ({emotion_level})
+- 특별 요구사항: {request.requirement or '없음'}
+
+**이벤트 정보:**
+- 이벤트명: {request.eventName or '없음'}
+- 시작일: {request.startDate or '없음'}
+- 종료일: {request.endDate or '없음'}
+
+**이미지 분석 결과:**
+{chr(10).join(image_descriptions) if image_descriptions else '이미지 없음'}
+
+**플랫폼 특성:**
+- 최대 길이: {platform_spec['max_length']}자
+- 스타일: {platform_spec['style']}
+- 형식: {platform_spec['format']}
+
+**요구사항:**
+1. {request.platform}의 특성에 맞는 톤앤매너 사용
+2. {request.category} 카테고리에 적합한 내용 구성
+3. 고객의 관심을 끌 수 있는 매력적인 문구 사용
+4. 이미지와 연관된 내용으로 작성
+5. 지정된 톤앤매너와 감정 강도에 맞게 작성
+
+본문과 해시태그를 모두 포함하여 완성된 게시글을 작성해주세요.
+"""
+ return prompt
+
+ def _format_to_html(self, content: str, request: SnsContentGetRequest) -> str:
+ """
+ 생성된 콘텐츠를 HTML 형식으로 포맷팅
+ """
+ # 줄바꿈을
태그로 변환
+ content = content.replace('\n', '
')
+
+ # 해시태그를 파란색으로 스타일링
+ import re
+ content = re.sub(r'(#[\w가-힣]+)', r'\1', content)
+
+ # 이모티콘은 그대로 유지
+
+ # 전체 HTML 구조
+ html_content = f"""
+