""" 유튜브 SEO 서비스 영상 제목/설명/해시태그를 생성하고 video 테이블에 저장합니다. """ import json import logging from fastapi import HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.home.models import MarketingIntel, Project from app.social.schemas import YoutubeDescriptionResponse from app.social.services.sns_metadata import apply_sns_metadata, has_stored_sns_metadata from app.user.models import User from app.video.models import Video logger = logging.getLogger(__name__) class SeoService: """유튜브 SEO 비즈니스 로직 서비스""" async def get_youtube_seo_description( self, video_id: int, current_user: User, session: AsyncSession, ) -> YoutubeDescriptionResponse: """ 저장된 SNS 메타데이터를 반환하거나, 없으면 생성 후 video에 저장합니다. """ logger.info( f"[SEO_SERVICE] Load metadata - user: {current_user.user_uuid} / video_id: {video_id}" ) video = await self._get_owned_video(video_id, current_user.user_uuid, session) if video is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"video_id '{video_id}'에 해당하는 영상을 찾을 수 없습니다.", ) if has_stored_sns_metadata(video): return self._response_from_video(video) result = await self.generate_and_save_for_video(video.id, session) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"video_id '{video_id}'에 해당하는 영상을 찾을 수 없습니다.", ) await session.commit() return result async def generate_and_save_for_video( self, video_id: int, session: AsyncSession, ) -> YoutubeDescriptionResponse | None: """GPT로 SEO를 생성해 지정한 video 행에 저장합니다. 워커/온디맨드 공용.""" video_result = await session.execute(select(Video).where(Video.id == video_id)) video = video_result.scalar_one_or_none() if video is None: logger.warning(f"[SEO_SERVICE] Video NOT FOUND - video_id: {video_id}") return None if has_stored_sns_metadata(video): return self._response_from_video(video) result = await self._generate_seo_description(video.task_id, session) apply_sns_metadata(video, result.title, result.description, result.keywords) await session.flush() logger.info(f"[SEO_SERVICE] Saved metadata - video_id: {video_id}") return result async def _get_owned_video( self, video_id: int, user_uuid: str, session: AsyncSession, ) -> Video | None: result = await session.execute( select(Video) .join(Project, Project.id == Video.project_id) .where( Video.id == video_id, Project.user_uuid == user_uuid, Video.is_deleted.is_(False), ) ) return result.scalar_one_or_none() async def _generate_seo_description( self, task_id: str, session: AsyncSession, ) -> YoutubeDescriptionResponse: """GPT를 사용하여 SEO description 생성""" from app.utils.prompts.chatgpt_prompt import ChatgptService from app.utils.prompts.prompts import yt_upload_prompt logger.info(f"[SEO_SERVICE] Generating SEO - task_id: {task_id}") try: project_result = await session.execute( select(Project) .where(Project.task_id == task_id) .order_by(Project.created_at.desc()) .limit(1) ) project = project_result.scalar_one_or_none() if project is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.", ) marketing_result = await session.execute( select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) ) marketing_intelligence = marketing_result.scalar_one_or_none() if marketing_intelligence is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="마케팅 인텔리전스를 찾을 수 없습니다.", ) hashtags = marketing_intelligence.intel_result["target_keywords"] yt_seo_input_data = { "customer_name": project.store_name, "detail_region_info": project.detail_region_info, "marketing_intelligence_summary": json.dumps( marketing_intelligence.intel_result, ensure_ascii=False ), "language": project.language, "target_keywords": hashtags, "industry": project.industry or "", } chatgpt = ChatgptService(timeout=180) yt_seo_output = await chatgpt.generate_structured_output( yt_upload_prompt, yt_seo_input_data ) return YoutubeDescriptionResponse( title=yt_seo_output.title, description=yt_seo_output.description, keywords=hashtags, ) except HTTPException: raise except Exception as e: logger.error(f"[SEO_SERVICE] EXCEPTION - error: {e}") raise HTTPException( status_code=500, detail=f"유튜브 SEO 생성에 실패했습니다. : {str(e)}", ) def _response_from_video(self, video: Video) -> YoutubeDescriptionResponse: return YoutubeDescriptionResponse( title=video.title or "", description=video.description or "", keywords=list(video.hashtags or []), ) seo_service = SeoService()