From 4d8462651a8a8a17c6c3f6030ded8bb2908c6944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Fri, 3 Jul 2026 15:59:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=81=EC=A0=91=EC=9E=85=EB=A0=A5=EC=8B=9C?= =?UTF-8?q?=20=EC=97=85=EC=A2=85=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/home/api/routers/v1/home.py | 7 +++--- app/home/schemas/home_schema.py | 3 ++- app/utils/prompts/chatgpt_prompt.py | 24 ++++++++++++++----- .../migration_2026_07_03_add_image_match.sql | 12 ++++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 docs/database-schema/migration_2026_07_03_add_image_match.sql diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index 701a354..26e04ea 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -421,6 +421,7 @@ async def _crawling_logic( ## 요청 필드 - **customer_name**: 업체명 / 브랜드명 (필수) - **address**: 도로명 또는 지번 주소 (필수) +- **category**: 업종/카테고리 자유 입력 (선택, 예: 펜션, 카페). 비우면 업체명 기반 AI 분류 ## 반환 정보 - **processed_info**: 가공된 장소 정보 (customer_name, region, detail_region_info) @@ -445,9 +446,9 @@ async def manual_marketing( # Step 2: GPT API 호출 → 마케팅 분석 결과 생성 # place_id 없이 업체명+주소만으로 분석 (크롤링 없이 직접 입력된 경우) chatgpt_service = ChatgptService() - # 요청에 industry가 있으면 사용, 없으면 업체명 기반 AI 분류 - industry = request_body.industry or await _resolve_industry( - "", request_body.store_name + # 크롤링 경로와 동일하게 category를 우선 사용하고, 없으면 업체명 기반 AI 분류 + industry = await _resolve_industry( + request_body.category, request_body.store_name ) processed_info.industry = industry input_marketing_data = { diff --git a/app/home/schemas/home_schema.py b/app/home/schemas/home_schema.py index f7c014b..1fcf753 100644 --- a/app/home/schemas/home_schema.py +++ b/app/home/schemas/home_schema.py @@ -260,13 +260,14 @@ class ManualMarketingRequest(BaseModel): "example": { "store_name": "스테이 머뭄", "address": "전북특별자치도 군산시 절골길 18", + "category": "펜션", } } ) store_name: str = Field(..., description="업체명 / 브랜드명") address: str = Field(..., description="도로명 또는 지번 주소") - industry: str = Field(default="", description="업종 분류 (stay|restaurant|cafe|salon|clinic|fitness|academy|attraction). 비우면 업체명 기반 AI 분류") + category: str = Field(default="", description="업체 업종/카테고리 자유 입력 (예: 펜션, 카페). 크롤링 경로와 동일하게 AI가 8개 industry enum으로 자동 분류하는 데 사용. 비우면 업체명 기반 AI 분류") class ErrorResponse(BaseModel): diff --git a/app/utils/prompts/chatgpt_prompt.py b/app/utils/prompts/chatgpt_prompt.py index 6a3f898..9581ce4 100644 --- a/app/utils/prompts/chatgpt_prompt.py +++ b/app/utils/prompts/chatgpt_prompt.py @@ -1,6 +1,6 @@ import json import re -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from typing import List, Optional from openai import AsyncOpenAI @@ -131,11 +131,23 @@ class ChatgptService: }) last_error = None for attempt in range(self.max_retries + 1): - response = await self.client.beta.chat.completions.parse( - model=model, - messages=[{"role": "user", "content": content}], - response_format=output_format - ) + try: + response = await self.client.beta.chat.completions.parse( + model=model, + messages=[{"role": "user", "content": content}], + response_format=output_format + ) + except (ValidationError, json.JSONDecodeError) as e: + # 모델이 스키마에 맞지 않는 JSON을 반환한 경우 (예: trailing characters). + # 확률적 출력 문제일 수 있으므로 재시도 대상에 포함한다. + logger.warning( + f"[ChatgptService({self.model_type})] Structured output parse failed " + f"(attempt {attempt + 1}/{self.max_retries + 1}): {e}" + ) + last_error = ChatGPTResponseError("parse_error", type(e).__name__, str(e)) + if attempt < self.max_retries: + logger.info(f"[ChatgptService({self.model_type})] Retrying request...") + continue # Response 디버그 로깅 logger.debug(f"[ChatgptService({self.model_type})] attempt: {attempt}") logger.debug(f"[ChatgptService({self.model_type})] Response ID: {response.id}") diff --git a/docs/database-schema/migration_2026_07_03_add_image_match.sql b/docs/database-schema/migration_2026_07_03_add_image_match.sql new file mode 100644 index 0000000..c8204c0 --- /dev/null +++ b/docs/database-schema/migration_2026_07_03_add_image_match.sql @@ -0,0 +1,12 @@ +-- ============================================================ +-- Migration: marketing 테이블에 image_match 컬럼 추가 +-- Date: 2026-07-03 +-- Description: 이미지 슬롯 배정 결과물 저장 +-- - lyric 생성 사전 단계(get_image_tags_by_task_id → template_matching_taged_image)에서 +-- 산출된 슬롯별 이미지 배정 결과를 저장 +-- - 영상 생성 시점에는 저장된 결과를 읽어 음악 URL/주소만 보완하여 재사용 +-- ============================================================ + +ALTER TABLE marketing + ADD COLUMN image_match JSON NULL + COMMENT '이미지 슬롯 배정 결과물' AFTER subtitle;