From 1665d11d66a79a6609b134ba40fd1e9c9ce0bde3 Mon Sep 17 00:00:00 2001 From: Dohyun Lim Date: Tue, 27 Jan 2026 16:34:48 +0900 Subject: [PATCH] openapi exception, retry, timeout finished --- app/home/api/routers/v1/home.py | 13 ++- app/home/schemas/home_schema.py | 26 ++++- app/lyric/schemas/lyric.py | 68 +++++++++--- app/lyric/worker/lyric_task.py | 10 +- app/utils/chatgpt_prompt.py | 2 - error_plan.md | 178 ++++++++++++++++++++++++++++++++ 6 files changed, 277 insertions(+), 20 deletions(-) create mode 100644 error_plan.md diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index 9f4ceec..738ab88 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -24,7 +24,7 @@ from app.home.schemas.home_schema import ( ProcessedInfo, ) from app.utils.upload_blob_as_request import AzureBlobUploader -from app.utils.chatgpt_prompt import ChatgptService +from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError from app.utils.common import generate_task_id from app.utils.logger import get_logger from app.utils.nvMapScraper import NvMapScraper, GraphQLException @@ -293,6 +293,15 @@ async def _crawling_logic(url:str): f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)" ) + except ChatGPTResponseError as e: + step3_elapsed = (time.perf_counter() - step3_start) * 1000 + logger.error( + f"[crawling] Step 3 FAILED - ChatGPT Error: status={e.status}, " + f"code={e.error_code}, message={e.error_message} ({step3_elapsed:.1f}ms)" + ) + marketing_analysis = None + gpt_status = "failed" + except Exception as e: step3_elapsed = (time.perf_counter() - step3_start) * 1000 logger.error( @@ -301,6 +310,7 @@ async def _crawling_logic(url:str): logger.exception("[crawling] Step 3 상세 오류:") # GPT 실패 시에도 크롤링 결과는 반환 marketing_analysis = None + gpt_status = "failed" else: step2_elapsed = (time.perf_counter() - step2_start) * 1000 logger.warning( @@ -320,6 +330,7 @@ async def _crawling_logic(url:str): logger.info(f"[crawling] - GPT API 호출: {step3_3_elapsed:.1f}ms") return { + "status": gpt_status if 'gpt_status' in locals() else "completed", "image_list": scraper.image_link_list, "image_count": len(scraper.image_link_list) if scraper.image_link_list else 0, "processed_info": processed_info, diff --git a/app/home/schemas/home_schema.py b/app/home/schemas/home_schema.py index 069aa6b..79f73da 100644 --- a/app/home/schemas/home_schema.py +++ b/app/home/schemas/home_schema.py @@ -158,13 +158,37 @@ class MarketingAnalysis(BaseModel): class CrawlingResponse(BaseModel): """크롤링 응답 스키마""" + model_config = ConfigDict( + json_schema_extra={ + "example": { + "status": "completed", + "image_list": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"], + "image_count": 2, + "processed_info": { + "customer_name": "스테이 머뭄", + "region": "군산", + "detail_region_info": "전북특별자치도 군산시 절골길 18" + }, + "marketing_analysis": { + "report": "마케팅 분석 리포트...", + "tags": ["힐링", "감성숙소"], + "facilities": ["조식", "주차"] + } + } + } + ) + + status: str = Field( + default="completed", + description="처리 상태 (completed: 성공, failed: ChatGPT 분석 실패)" + ) image_list: Optional[list[str]] = Field(None, description="이미지 URL 목록") image_count: int = Field(..., description="이미지 개수") processed_info: Optional[ProcessedInfo] = Field( None, description="가공된 장소 정보 (customer_name, region, detail_region_info)" ) marketing_analysis: Optional[MarketingAnalysis] = Field( - None, description="마케팅 분석 결과 (report, tags, facilities)" + None, description="마케팅 분석 결과 (report, tags, facilities). 실패 시 null" ) diff --git a/app/lyric/schemas/lyric.py b/app/lyric/schemas/lyric.py index 37cb76a..718f724 100644 --- a/app/lyric/schemas/lyric.py +++ b/app/lyric/schemas/lyric.py @@ -108,15 +108,33 @@ class LyricStatusResponse(BaseModel): Usage: GET /lyric/status/{task_id} Returns the current processing status of a lyric generation task. + + Status Values: + - processing: 가사 생성 진행 중 + - completed: 가사 생성 완료 + - failed: ChatGPT API 오류 또는 생성 실패 """ model_config = ConfigDict( json_schema_extra={ - "example": { - "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", - "status": "completed", - "message": "가사 생성이 완료되었습니다.", - } + "examples": [ + { + "summary": "성공", + "value": { + "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", + "status": "completed", + "message": "가사 생성이 완료되었습니다.", + } + }, + { + "summary": "실패", + "value": { + "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", + "status": "failed", + "message": "가사 생성에 실패했습니다.", + } + } + ] } ) @@ -131,26 +149,46 @@ class LyricDetailResponse(BaseModel): Usage: GET /lyric/{task_id} Returns the generated lyric content for a specific task. + + Note: + - status가 "failed"인 경우 lyric_result에 에러 메시지가 저장됩니다. + - 에러 메시지 형식: "ChatGPT Error: {message}" 또는 "Error: {message}" """ model_config = ConfigDict( json_schema_extra={ - "example": { - "id": 1, - "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", - "project_id": 1, - "status": "completed", - "lyric_result": "인스타 감성의 스테이 머뭄\n군산 신흥동 말랭이 마을에서\n여유로운 하루를 보내며\n추억을 만들어가요", - "created_at": "2024-01-15T12:00:00", - } + "examples": [ + { + "summary": "성공", + "value": { + "id": 1, + "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", + "project_id": 1, + "status": "completed", + "lyric_result": "인스타 감성의 스테이 머뭄\n군산 신흥동 말랭이 마을에서\n여유로운 하루를 보내며\n추억을 만들어가요", + "created_at": "2024-01-15T12:00:00", + } + }, + { + "summary": "실패", + "value": { + "id": 1, + "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", + "project_id": 1, + "status": "failed", + "lyric_result": "ChatGPT Error: Response incomplete: max_output_tokens", + "created_at": "2024-01-15T12:00:00", + } + } + ] } ) id: int = Field(..., description="가사 ID") task_id: str = Field(..., description="작업 고유 식별자") project_id: int = Field(..., description="프로젝트 ID") - status: str = Field(..., description="처리 상태") - lyric_result: Optional[str] = Field(None, description="생성된 가사") + status: str = Field(..., description="처리 상태 (processing, completed, failed)") + lyric_result: Optional[str] = Field(None, description="생성된 가사 또는 에러 메시지 (실패 시)") created_at: Optional[datetime] = Field(None, description="생성 일시") diff --git a/app/lyric/worker/lyric_task.py b/app/lyric/worker/lyric_task.py index f956164..5be9a95 100644 --- a/app/lyric/worker/lyric_task.py +++ b/app/lyric/worker/lyric_task.py @@ -11,7 +11,7 @@ from sqlalchemy.exc import SQLAlchemyError from app.database.session import BackgroundSessionLocal from app.lyric.models import Lyric -from app.utils.chatgpt_prompt import ChatgptService +from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError from app.utils.prompts.prompts import Prompt from app.utils.logger import get_logger @@ -130,6 +130,14 @@ async def generate_lyric_background( logger.debug(f"[generate_lyric_background] - Step 2 (GPT API 호출): {step2_elapsed:.1f}ms") logger.debug(f"[generate_lyric_background] - Step 3 (DB 업데이트): {step3_elapsed:.1f}ms") + except ChatGPTResponseError as e: + elapsed = (time.perf_counter() - task_start) * 1000 + logger.error( + f"[generate_lyric_background] ChatGPT ERROR - task_id: {task_id}, " + f"status: {e.status}, code: {e.error_code}, message: {e.error_message} ({elapsed:.1f}ms)" + ) + await _update_lyric_status(task_id, "failed", f"ChatGPT Error: {e.error_message}") + except SQLAlchemyError as e: elapsed = (time.perf_counter() - task_start) * 1000 logger.error(f"[generate_lyric_background] DB ERROR - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)", exc_info=True) diff --git a/app/utils/chatgpt_prompt.py b/app/utils/chatgpt_prompt.py index 9a97c55..ad3719b 100644 --- a/app/utils/chatgpt_prompt.py +++ b/app/utils/chatgpt_prompt.py @@ -22,8 +22,6 @@ class ChatGPTResponseError(Exception): class ChatgptService: """ChatGPT API 서비스 클래스 - - GPT 5.0 모델을 사용하여 마케팅 가사 및 분석을 생성합니다. """ def __init__(self, timeout: float = None): diff --git a/error_plan.md b/error_plan.md new file mode 100644 index 0000000..26bb97e --- /dev/null +++ b/error_plan.md @@ -0,0 +1,178 @@ +# ChatGPT API 에러 처리 개선 계획서 + +## 1. 현황 분석 + +### 1.1 `generate_structured_output` 사용처 + +| 파일 | 용도 | DB 상태 업데이트 | 응답 상태 변수 | +|------|------|-----------------|----------------| +| `app/lyric/worker/lyric_task.py` | 가사 생성 | ✅ "failed" 저장 | - (백그라운드) | +| `app/home/api/routers/v1/home.py` | 크롤링 마케팅 분석 | ❌ 없음 | ❌ 없음 | + +### 1.2 응답 스키마 상태 변수 현황 + +| 스키마 | 위치 | 상태 변수 | 조치 | +|--------|------|----------|------| +| `CrawlingResponse` | home_schema.py:158 | ❌ 없음 | `status` 추가 | +| `GenerateLyricResponse` | lyric.py:72 | ✅ `success: bool` | `False`로 설정 | +| `LyricStatusResponse` | lyric.py:105 | ✅ `status: str` | `"failed"` 설정 | +| `LyricDetailResponse` | lyric.py:128 | ✅ `status: str` | `"failed"` 설정 | + +--- + +## 2. 개선 목표 + +1. **DB 상태 업데이트**: 에러 발생 시 DB에 `status = "failed"` 저장 +2. **클라이언트 응답**: 기존 상태 변수가 있으면 `"failed"` 설정, 없으면 변수 추가 후 설정 + +--- + +## 3. 상세 작업 계획 + +### 3.1 lyric_task.py - `ChatGPTResponseError` 명시적 처리 + +**파일**: `app/lyric/worker/lyric_task.py` + +**현재 코드 (Line 138-141)**: +```python +except Exception as e: + elapsed = (time.perf_counter() - task_start) * 1000 + logger.error(f"[generate_lyric_background] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)", exc_info=True) + await _update_lyric_status(task_id, "failed", f"Error: {str(e)}") +``` + +**변경 코드**: +```python +from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError + +# ... 기존 코드 ... + +except ChatGPTResponseError as e: + elapsed = (time.perf_counter() - task_start) * 1000 + logger.error( + f"[generate_lyric_background] ChatGPT ERROR - task_id: {task_id}, " + f"status: {e.status}, code: {e.error_code} ({elapsed:.1f}ms)" + ) + await _update_lyric_status(task_id, "failed", f"ChatGPT Error: {e.error_message}") + +except SQLAlchemyError as e: + # ... 기존 코드 유지 ... + +except Exception as e: + # ... 기존 코드 유지 ... +``` + +**결과**: DB `lyric.status` = `"failed"`, `lyric.lyric_result` = 에러 메시지 + +--- + +### 3.2 home.py - CrawlingResponse에 status 추가 + +**파일**: `app/home/schemas/home_schema.py` + +**현재 코드 (Line 158-168)**: +```python +class CrawlingResponse(BaseModel): + """크롤링 응답 스키마""" + image_list: Optional[list[str]] = Field(None, description="이미지 URL 목록") + image_count: int = Field(..., description="이미지 개수") + processed_info: Optional[ProcessedInfo] = Field(None, ...) + marketing_analysis: Optional[MarketingAnalysis] = Field(None, ...) +``` + +**변경 코드**: +```python +class CrawlingResponse(BaseModel): + """크롤링 응답 스키마""" + status: str = Field( + default="completed", + description="처리 상태 (completed, failed)" + ) + image_list: Optional[list[str]] = Field(None, description="이미지 URL 목록") + image_count: int = Field(..., description="이미지 개수") + processed_info: Optional[ProcessedInfo] = Field(None, ...) + marketing_analysis: Optional[MarketingAnalysis] = Field(None, ...) +``` + +--- + +### 3.3 home.py - 크롤링 엔드포인트 에러 처리 + +**파일**: `app/home/api/routers/v1/home.py` + +**현재 코드 (Line 296-303)**: +```python +except Exception as e: + step3_elapsed = (time.perf_counter() - step3_start) * 1000 + logger.error(...) + marketing_analysis = None +``` + +**변경 코드**: +```python +from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError + +# ... 기존 코드 ... + +except ChatGPTResponseError as e: + step3_elapsed = (time.perf_counter() - step3_start) * 1000 + logger.error( + f"[crawling] Step 3 FAILED - ChatGPT Error: {e.status}, {e.error_code} ({step3_elapsed:.1f}ms)" + ) + marketing_analysis = None + gpt_status = "failed" + +except Exception as e: + step3_elapsed = (time.perf_counter() - step3_start) * 1000 + logger.error(...) + marketing_analysis = None + gpt_status = "failed" + +# 응답 반환 부분 수정 +return { + "status": gpt_status if 'gpt_status' in locals() else "completed", + "image_list": scraper.image_link_list, + "image_count": len(scraper.image_link_list) if scraper.image_link_list else 0, + "processed_info": processed_info, + "marketing_analysis": marketing_analysis, +} +``` + +--- + +## 4. 파일 변경 요약 + +| 파일 | 변경 내용 | +|------|----------| +| `app/lyric/worker/lyric_task.py` | `ChatGPTResponseError` import 및 명시적 처리 추가 | +| `app/home/schemas/home_schema.py` | `CrawlingResponse`에 `status` 필드 추가 | +| `app/home/api/routers/v1/home.py` | `ChatGPTResponseError` 처리, 응답에 `status` 포함 | + +--- + +## 5. 변경 후 동작 + +### 5.1 lyric_task.py (가사 생성) + +| 상황 | DB status | DB lyric_result | +|------|-----------|-----------------| +| 성공 | `"completed"` | 생성된 가사 | +| ChatGPT 에러 | `"failed"` | `"ChatGPT Error: {message}"` | +| DB 에러 | `"failed"` | `"Database Error: {message}"` | +| 기타 에러 | `"failed"` | `"Error: {message}"` | + +### 5.2 home.py (크롤링) + +| 상황 | 응답 status | marketing_analysis | +|------|------------|-------------------| +| 성공 | `"completed"` | 분석 결과 | +| ChatGPT 에러 | `"failed"` | `null` | +| 기타 에러 | `"failed"` | `null` | + +--- + +## 6. 구현 순서 + +1. `app/home/schemas/home_schema.py` - `CrawlingResponse`에 `status` 필드 추가 +2. `app/lyric/worker/lyric_task.py` - `ChatGPTResponseError` 명시적 처리 +3. `app/home/api/routers/v1/home.py` - 에러 처리 및 응답 `status` 설정