From 4f8ab4ac267e99dcec45ead4489b4c5cc0f07bde Mon Sep 17 00:00:00 2001 From: jpark0506 <45231740+jpark0506@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:15:25 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix=20:=20test=20api=20response=20null=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/dto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dto.py b/app/dto.py index b22c505..b9b6775 100644 --- a/app/dto.py +++ b/app/dto.py @@ -167,7 +167,7 @@ class VoiceAnalyzePreviewResponse(BaseModel): sad_bps: int neutral_bps: int angry_bps: int - anxiety_bps: int # fear -> anxiety (출력용) + anxiety_bps: Optional[int] = 0 # fear -> anxiety (출력용) surprise_bps: int top_emotion: Optional[str] = None top_confidence_bps: Optional[int] = None From a6fce73fffad8ea8f5f3847ae9936a472eba95bd Mon Sep 17 00:00:00 2001 From: jpark0506 <45231740+jpark0506@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:15:57 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=EC=9E=84=EC=9D=98=EC=9D=98=20test?= =?UTF-8?q?=20user=20=EC=B6=94=EA=B0=80=20python?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- create_test_user.py | 95 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100755 create_test_user.py diff --git a/create_test_user.py b/create_test_user.py new file mode 100755 index 0000000..06c4a8c --- /dev/null +++ b/create_test_user.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""테스트용 사용자 생성 스크립트""" +import sys +import os +sys.path.insert(0, os.path.dirname(__file__)) + +from app.database import SessionLocal +from app.auth_service import get_auth_service +from datetime import datetime +import secrets +import string + +def generate_random_string(length: int = 8) -> str: + """랜덤 문자열 생성""" + characters = string.ascii_lowercase + string.digits + return ''.join(secrets.choice(characters) for _ in range(length)) + +def create_test_user( + name: str = None, + username: str = None, + password: str = "test1234", + role: str = "USER", + birthdate: str = None +): + """테스트용 사용자 생성""" + db = SessionLocal() + try: + auth_service = get_auth_service(db) + + # 기본값 설정 + if not name: + name = f"테스트사용자_{generate_random_string(4)}" + if not username: + username = f"test_user_{generate_random_string(8)}" + if not birthdate: + # 기본 생년월일: 1990.01.01 + birthdate = "1990.01.01" + + print(f"사용자 생성 중...") + print(f" 이름: {name}") + print(f" 아이디: {username}") + print(f" 비밀번호: {password}") + print(f" 역할: {role}") + print(f" 생년월일: {birthdate}") + + result = auth_service.signup( + name=name, + birthdate=birthdate, + username=username, + password=password, + role=role, + connecting_user_code=None + ) + + if result["success"]: + print(f"\n✅ 사용자 생성 성공!") + print(f" user_code: {result['user_code']}") + print(f" username: {result['username']}") + print(f" name: {result['name']}") + print(f" role: {result['role']}") + print(f"\n📝 사용 예시:") + print(f" user_id (username): {result['username']}") + return result + else: + print(f"\n❌ 사용자 생성 실패: {result.get('error')}") + return None + + except Exception as e: + print(f"\n❌ 오류 발생: {str(e)}") + import traceback + traceback.print_exc() + return None + finally: + db.close() + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="테스트용 사용자 생성") + parser.add_argument("--name", type=str, help="사용자 이름") + parser.add_argument("--username", type=str, help="아이디 (username)") + parser.add_argument("--password", type=str, default="test1234", help="비밀번호 (기본값: test1234)") + parser.add_argument("--role", type=str, choices=["USER", "CARE"], default="USER", help="역할 (기본값: USER)") + parser.add_argument("--birthdate", type=str, help="생년월일 (YYYY.MM.DD 형식)") + + args = parser.parse_args() + + create_test_user( + name=args.name, + username=args.username, + password=args.password, + role=args.role, + birthdate=args.birthdate + ) + From bb545194ba3e62664d098a575e16d7e051f653b8 Mon Sep 17 00:00:00 2001 From: jpark0506 <45231740+jpark0506@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:16:52 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat=20:=20=EC=B1=84=ED=8C=85=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 31 +++ app/services/analyze_chat_service.py | 273 +++++++++++++++++++++++++++ check_user.py | 53 ++++++ list_care_users.py | 38 ++++ requirements.txt | 1 + update_emotion.py | 57 ++++++ 6 files changed, 453 insertions(+) create mode 100644 app/services/analyze_chat_service.py create mode 100644 check_user.py create mode 100644 list_care_users.py create mode 100644 update_emotion.py diff --git a/app/main.py b/app/main.py index 04b8f91..96f21d0 100644 --- a/app/main.py +++ b/app/main.py @@ -260,6 +260,7 @@ async def general_exception_handler(request, exc: Exception): nlp_router = APIRouter(prefix="/nlp", tags=["nlp"]) test_router = APIRouter(prefix="/test", tags=["test"]) questions_router = APIRouter(prefix="/questions", tags=["questions"]) +analyze_router = APIRouter(prefix="/analyze", tags=["analyze"]) # Health @app.get("/health") @@ -972,6 +973,35 @@ async def test_fcm_send( result = svc.send_notification_to_tokens([token], title, body) return {"success": True, "result": result} +def get_analyze_chat_service_dep(db: Session = Depends(get_db)): + """AnalyzeChatService 의존성 함수""" + from .services.analyze_chat_service import get_analyze_chat_service + return get_analyze_chat_service(db) + + +@analyze_router.post("/analyze/chat") +async def analyze_chat( + session_id: str = Form(...), + user_id: str = Form(...), + question: str = Form(...), + file: UploadFile = File(...), + analyze_chat_service: "AnalyzeChatService" = Depends(get_analyze_chat_service_dep) +): + """ + 음성 파일을 받아 STT, 감정 분석 후 외부 chatbot API로 전송 + """ + try: + return await analyze_chat_service.analyze_and_send( + file=file, + session_id=session_id, + user_id=user_id, + question=question + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + # ---------------- router 등록 ---------------- app.include_router(users_router) app.include_router(care_router) @@ -980,3 +1010,4 @@ async def test_fcm_send( app.include_router(test_router) app.include_router(questions_router) app.include_router(composite_router.router) +app.include_router(analyze_router) \ No newline at end of file diff --git a/app/services/analyze_chat_service.py b/app/services/analyze_chat_service.py new file mode 100644 index 0000000..ac3feb4 --- /dev/null +++ b/app/services/analyze_chat_service.py @@ -0,0 +1,273 @@ +import os +import asyncio +import uuid +from typing import Dict, Any +from io import BytesIO +from datetime import datetime +from sqlalchemy.orm import Session +import httpx +from fastapi import UploadFile, HTTPException + +from ..services.va_fusion import fuse_VA +from ..nlp_service import analyze_text_sentiment +from ..emotion_service import analyze_voice_emotion +from ..stt_service import transcribe_voice +from ..s3_service import upload_fileobj +from ..auth_service import get_auth_service + +class AnalyzeChatService: + """음성 파일 분석 및 chatbot API 전송 서비스""" + + def __init__(self, db: Session): + self.db = db + + async def analyze_and_send( + self, + file: UploadFile, + session_id: str, + user_id: str, + question: str + ) -> Dict[str, Any]: + """ + 음성 파일을 분석하고 외부 chatbot API로 전송 + + Args: + file: 업로드된 음성 파일 + session_id: 세션 ID + user_id: 사용자 ID + question: 질문 내용 + + Returns: + Dict: 외부 API 응답 + """ + # 파일 내용을 메모리에 저장 (여러 번 사용하기 위해) + file_content = await file.read() + # 파일명을 현재 시간과 UUID를 포함한 형식으로 생성 (중복 방지) + current_time = datetime.now().strftime("%Y%m%d_%H%M%S") + unique_id = str(uuid.uuid4())[:8] # UUID 앞 8자리만 사용 + if file.filename: + # 원본 파일명이 있으면 확장자 추출 후 고유한 이름 생성 + import os as os_module + name, ext = os_module.path.splitext(file.filename) + filename = f"{name}_{current_time}_{unique_id}{ext}" + else: + filename = f"audio_{current_time}_{unique_id}.wav" + + # 1. S3 파일 업로드 (별도 스레드에서 실행) + s3_key = await asyncio.to_thread( + self._upload_to_s3, + file_content, + filename, + session_id, + user_id, + file.content_type + ) + + # 2. STT (음성 → 텍스트) - 별도 스레드에서 실행 + content = await asyncio.to_thread( + self._transcribe_audio, + file_content, + filename, + file.content_type + ) + + # 3. 음성 감정 분석 - 별도 스레드에서 실행 + emotion_data = await asyncio.to_thread( + self._analyze_emotion, + file_content, + filename, + file.content_type, + content + ) + + # 4. 사용자 정보 조회 (DB 세션은 스레드 안전하지 않으므로 메인 스레드에서 실행) + user_name = self._get_user_name(user_id) + + # 5. 외부 API 호출 + return await self._send_to_chatbot( + content=content, + emotion=emotion_data, + question=question, + user_id=user_id, + user_name=user_name + ) + + def _upload_to_s3( + self, + file_content: bytes, + filename: str, + session_id: str, + user_id: str, + content_type: str + ) -> str: + """S3에 파일 업로드""" + bucket = os.getenv("S3_BUCKET_NAME") + if not bucket: + raise HTTPException(status_code=500, detail="S3_BUCKET_NAME not configured") + + # S3 키 생성: {session_id}/{user_id}/{filename} + s3_key = f"{session_id}/{user_id}/{filename}" if session_id and user_id else f"chat/{filename}" + + # S3 업로드 + file_obj = BytesIO(file_content) + upload_fileobj(bucket=bucket, key=s3_key, fileobj=file_obj, content_type=content_type) + + return s3_key + + def _transcribe_audio( + self, + file_content: bytes, + filename: str, + content_type: str + ) -> str: + """STT로 음성을 텍스트로 변환""" + # 파일 내용을 BytesIO로 래핑하여 UploadFile처럼 사용 + class FileWrapper: + def __init__(self, content: bytes, filename: str, content_type: str): + self.file = BytesIO(content) + self.filename = filename + self.content_type = content_type + + wrapped_file = FileWrapper(file_content, filename, content_type) + stt_result = transcribe_voice(wrapped_file) + + if stt_result.get("error"): + raise HTTPException(status_code=400, detail=f"STT failed: {stt_result.get('error')}") + + content = stt_result.get("transcript", "") + if not content: + raise HTTPException(status_code=400, detail="STT result is empty") + + return content + + def _analyze_emotion( + self, + file_content: bytes, + filename: str, + content_type: str, + text_content: str + ) -> Dict[str, Any]: + """음성 및 텍스트 감정 분석""" + + class FileWrapper: + def __init__(self, content: bytes, filename: str, content_type: str): + self.file = BytesIO(content) + self.filename = filename + self.content_type = content_type + + wrapped_file = FileWrapper(file_content, filename, content_type) + + # 3-1. Audio 감정 분석 + wrapped_file.file.seek(0) + emotion_result = analyze_voice_emotion(wrapped_file) + if emotion_result.get("error"): + raise HTTPException(status_code=400, detail=f"Emotion analysis failed: {emotion_result.get('error')}") + + audio_probs = emotion_result.get("emotion_scores", {}) + + # 3-2. 텍스트 감정 분석 + text_sentiment = analyze_text_sentiment(text_content, language_code="ko") + if text_sentiment.get("error"): + raise HTTPException(status_code=400, detail=f"Text sentiment analysis failed: {text_sentiment.get('error')}") + + sentiment_data = text_sentiment.get("sentiment", {}) + text_score = sentiment_data.get("score", 0.0) # [-1, 1] + text_magnitude = sentiment_data.get("magnitude", 0.0) # [0, +inf) + + # 3-3. VA Fusion으로 arousal, valence 계산 + va_result = fuse_VA(audio_probs, text_score, text_magnitude) + + # arousal, valence를 [0, 1] 범위로 변환 ([-1, 1] -> [0, 1]) + valence = (va_result.get("V_final", 0.0) + 1.0) / 2.0 # [-1, 1] -> [0, 1] + arousal = (va_result.get("A_final", 0.0) + 1.0) / 2.0 # [-1, 1] -> [0, 1] + + # emotion details 구성 (bps를 [0, 1] 범위로 변환) + per_emotion_bps = va_result.get("per_emotion_bps", {}) + details = { + "angry": per_emotion_bps.get("angry", 0) / 10000.0, + "anxiety": per_emotion_bps.get("fear", 0) / 10000.0, # fear -> anxiety + "happy": per_emotion_bps.get("happy", 0) / 10000.0, + "neutral": per_emotion_bps.get("neutral", 0) / 10000.0, + "sad": per_emotion_bps.get("sad", 0) / 10000.0, + "surprise": per_emotion_bps.get("surprise", 0) / 10000.0, + } + + top_emotion = va_result.get("top_emotion", "neutral") + # fear -> anxiety 변환 + if top_emotion == "fear": + top_emotion = "anxiety" + + top_confidence_bps = va_result.get("top_confidence_bps", 0) + confidence = top_confidence_bps / 10000.0 # [0, 1] + + return { + "arousal": round(arousal, 2), + "confidence": round(confidence, 2), + "details": {k: round(v, 2) for k, v in details.items()}, + "top_emotion": top_emotion, + "valence": round(valence, 2) + } + + def _get_user_name(self, user_id: str) -> str: + """사용자 이름 조회""" + if not user_id: + raise HTTPException(status_code=400, detail="user_id is required") + + auth_service = get_auth_service(self.db) + user = auth_service.get_user_by_username(user_id) + if not user: + raise HTTPException(status_code=404, detail=f"User not found: {user_id}") + + return user.name + + async def _send_to_chatbot( + self, + content: str, + emotion: Dict[str, Any], + question: str, + user_id: str, + user_name: str + ) -> Dict[str, Any]: + """외부 chatbot API로 전송""" + recorded_at = datetime.now().isoformat() + + request_payload = { + "content": content, + "emotion": emotion, + "question": question, + "recorded_at": recorded_at, + "user_id": user_id, + "user_name": user_name + } + + chatbot_url = os.getenv( + "CHATBOT_API_URL" + ) + + if not chatbot_url: + raise HTTPException(status_code=500, detail="CHATBOT_API_URL not configured") + + try: + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + chatbot_url, + json=request_payload, + headers={"accept": "application/json", "Content-Type": "application/json"} + ) + response.raise_for_status() + return response.json() + except httpx.HTTPStatusError as e: + # HTTP 에러 응답을 그대로 반환 + try: + error_response = e.response.json() + raise HTTPException(status_code=e.response.status_code, detail=error_response) + except: + raise HTTPException(status_code=e.response.status_code, detail=f"External API error: {e.response.text}") + except httpx.RequestError as e: + raise HTTPException(status_code=500, detail=f"External API request failed: {str(e)}") + + +def get_analyze_chat_service(db: Session) -> AnalyzeChatService: + """AnalyzeChatService 인스턴스 생성""" + return AnalyzeChatService(db) + diff --git a/check_user.py b/check_user.py new file mode 100644 index 0000000..7bfc473 --- /dev/null +++ b/check_user.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""사용자 정보 확인 스크립트""" +import sys +import os +sys.path.insert(0, os.path.dirname(__file__)) + +from app.database import SessionLocal, get_db +from app.models import User + +def check_user(username: str): + """사용자 정보 확인""" + db = next(get_db()) + try: + user = db.query(User).filter(User.username == username).first() + + if not user: + print(f"❌ 사용자 '{username}'를 찾을 수 없습니다.") + return + + print(f"✅ 사용자 정보:") + print(f" - user_id: {user.user_id}") + print(f" - username: {user.username}") + print(f" - user_code: {user.user_code}") + print(f" - name: {user.name}") + print(f" - role: {user.role}") + print(f" - connecting_user_code: {user.connecting_user_code}") + + if user.role != 'CARE': + print(f"\n⚠️ 경고: 사용자 역할이 'CARE'가 아닙니다. (현재: '{user.role}')") + + if not user.connecting_user_code: + print(f"\n⚠️ 경고: 연결된 피보호자 코드가 설정되어 있지 않습니다.") + else: + # 연결된 사용자 확인 + connected_user = db.query(User).filter(User.username == user.connecting_user_code).first() + if connected_user: + print(f"\n✅ 연결된 피보호자:") + print(f" - username: {connected_user.username}") + print(f" - name: {connected_user.name}") + print(f" - role: {connected_user.role}") + else: + print(f"\n❌ 연결된 피보호자 '{user.connecting_user_code}'를 찾을 수 없습니다.") + + finally: + db.close() + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("사용법: python check_user.py ") + sys.exit(1) + + check_user(sys.argv[1]) + diff --git a/list_care_users.py b/list_care_users.py new file mode 100644 index 0000000..46a42bc --- /dev/null +++ b/list_care_users.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""CARE 역할 사용자 목록 확인""" +import sys +import os +sys.path.insert(0, os.path.dirname(__file__)) + +from app.database import get_db +from app.models import User + +def list_care_users(): + """CARE 역할 사용자 목록 출력""" + db = next(get_db()) + try: + care_users = db.query(User).filter(User.role == 'CARE').all() + + if not care_users: + print("❌ CARE 역할 사용자가 없습니다.") + return + + print(f"✅ CARE 역할 사용자 목록 ({len(care_users)}명):") + print("-" * 80) + for user in care_users: + print(f" - username: {user.username}") + print(f" name: {user.name}") + print(f" connecting_user_code: {user.connecting_user_code}") + if user.connecting_user_code: + connected = db.query(User).filter(User.username == user.connecting_user_code).first() + if connected: + print(f" → 연결된 피보호자: {connected.name} ({connected.username})") + else: + print(f" → ⚠️ 연결된 피보호자를 찾을 수 없음") + print() + finally: + db.close() + +if __name__ == "__main__": + list_care_users() + diff --git a/requirements.txt b/requirements.txt index 84604b3..aabe3cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ alembic>=1.12.0 psutil>=5.9.0 firebase-admin>=6.0.0 openai>=1.40.0 +httpx>=0.25.0 diff --git a/update_emotion.py b/update_emotion.py new file mode 100644 index 0000000..1003a1e --- /dev/null +++ b/update_emotion.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""voice_analyze 테이블의 top_emotion 업데이트 스크립트""" +import sys +import os +sys.path.insert(0, os.path.dirname(__file__)) + +from app.database import get_db +from app.models import VoiceAnalyze + +def update_emotion(voice_analyze_id: int, old_emotion: str, new_emotion: str): + """감정 업데이트""" + db = next(get_db()) + try: + voice_analyze = db.query(VoiceAnalyze).filter( + VoiceAnalyze.voice_analyze_id == voice_analyze_id + ).first() + + if not voice_analyze: + print(f"❌ voice_analyze_id={voice_analyze_id}를 찾을 수 없습니다.") + return False + + if voice_analyze.top_emotion != old_emotion: + print(f"⚠️ 현재 감정이 '{voice_analyze.top_emotion}'입니다. (예상: '{old_emotion}')") + print(f" 그대로 '{new_emotion}'로 변경하시겠습니까?") + + print(f"업데이트 전:") + print(f" - voice_analyze_id: {voice_analyze.voice_analyze_id}") + print(f" - voice_id: {voice_analyze.voice_id}") + print(f" - top_emotion: {voice_analyze.top_emotion}") + + voice_analyze.top_emotion = new_emotion + db.commit() + db.refresh(voice_analyze) + + print(f"\n✅ 업데이트 완료:") + print(f" - top_emotion: {voice_analyze.top_emotion}") + + return True + + except Exception as e: + db.rollback() + print(f"❌ 업데이트 실패: {str(e)}") + return False + finally: + db.close() + +if __name__ == "__main__": + # voice_analyze_id=1, anxiety -> fear + update_emotion(voice_analyze_id=1, old_emotion="anxiety", new_emotion="fear") + + + + + + + + From de6479089b9107d6c142b59aaccd91360537f397 Mon Sep 17 00:00:00 2001 From: jpark0506 <45231740+jpark0506@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:19:13 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refac=20:=20AI=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9D=BC=EB=B6=80=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 2 +- app/services/analyze_chat_service.py | 4 +--- check_user.py | 2 +- create_test_user.py | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index 96f21d0..0de73d3 100644 --- a/app/main.py +++ b/app/main.py @@ -979,7 +979,7 @@ def get_analyze_chat_service_dep(db: Session = Depends(get_db)): return get_analyze_chat_service(db) -@analyze_router.post("/analyze/chat") +@analyze_router.post("/chat") async def analyze_chat( session_id: str = Form(...), user_id: str = Form(...), diff --git a/app/services/analyze_chat_service.py b/app/services/analyze_chat_service.py index ac3feb4..4920f3d 100644 --- a/app/services/analyze_chat_service.py +++ b/app/services/analyze_chat_service.py @@ -46,15 +46,13 @@ async def analyze_and_send( current_time = datetime.now().strftime("%Y%m%d_%H%M%S") unique_id = str(uuid.uuid4())[:8] # UUID 앞 8자리만 사용 if file.filename: - # 원본 파일명이 있으면 확장자 추출 후 고유한 이름 생성 - import os as os_module name, ext = os_module.path.splitext(file.filename) filename = f"{name}_{current_time}_{unique_id}{ext}" else: filename = f"audio_{current_time}_{unique_id}.wav" # 1. S3 파일 업로드 (별도 스레드에서 실행) - s3_key = await asyncio.to_thread( + await asyncio.to_thread( self._upload_to_s3, file_content, filename, diff --git a/check_user.py b/check_user.py index 7bfc473..3aa1a04 100644 --- a/check_user.py +++ b/check_user.py @@ -4,7 +4,7 @@ import os sys.path.insert(0, os.path.dirname(__file__)) -from app.database import SessionLocal, get_db +from app.database import get_db from app.models import User def check_user(username: str): diff --git a/create_test_user.py b/create_test_user.py index 06c4a8c..9460deb 100755 --- a/create_test_user.py +++ b/create_test_user.py @@ -6,7 +6,6 @@ from app.database import SessionLocal from app.auth_service import get_auth_service -from datetime import datetime import secrets import string From 74d7c898982b7e668a4854736211b7cc85b7e3df Mon Sep 17 00:00:00 2001 From: jpark0506 <45231740+jpark0506@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:30:56 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refac=20:=20general=20exception=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/exceptions.py | 12 ++++++ app/main.py | 8 ++-- app/services/analyze_chat_service.py | 56 +++++++++++++++++----------- 3 files changed, 50 insertions(+), 26 deletions(-) diff --git a/app/exceptions.py b/app/exceptions.py index 155398d..c240318 100644 --- a/app/exceptions.py +++ b/app/exceptions.py @@ -39,3 +39,15 @@ class InternalServerException(AppException): def __init__(self, message: str): super().__init__(status_code=500, message=message) + +class NotFoundException(AppException): + """리소스를 찾을 수 없음 (404)""" + def __init__(self, message: str): + super().__init__(status_code=404, message=message) + + +class ExternalAPIException(AppException): + """외부 API 호출 오류 (가변 상태 코드)""" + def __init__(self, status_code: int, message: str): + super().__init__(status_code=status_code, message=message) + diff --git a/app/main.py b/app/main.py index 0de73d3..7b5b82a 100644 --- a/app/main.py +++ b/app/main.py @@ -997,10 +997,10 @@ async def analyze_chat( user_id=user_id, question=question ) - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + except AppException as exc: + raise exc + except Exception: + raise InternalServerException("Internal server error while analyzing chat") # ---------------- router 등록 ---------------- app.include_router(users_router) diff --git a/app/services/analyze_chat_service.py b/app/services/analyze_chat_service.py index 4920f3d..5297d4d 100644 --- a/app/services/analyze_chat_service.py +++ b/app/services/analyze_chat_service.py @@ -6,7 +6,7 @@ from datetime import datetime from sqlalchemy.orm import Session import httpx -from fastapi import UploadFile, HTTPException +from fastapi import UploadFile from ..services.va_fusion import fuse_VA from ..nlp_service import analyze_text_sentiment @@ -14,6 +14,13 @@ from ..stt_service import transcribe_voice from ..s3_service import upload_fileobj from ..auth_service import get_auth_service +from ..exceptions import ( + ValidationException, + InternalServerException, + RuntimeException, + NotFoundException, + ExternalAPIException, +) class AnalyzeChatService: """음성 파일 분석 및 chatbot API 전송 서비스""" @@ -46,7 +53,7 @@ async def analyze_and_send( current_time = datetime.now().strftime("%Y%m%d_%H%M%S") unique_id = str(uuid.uuid4())[:8] # UUID 앞 8자리만 사용 if file.filename: - name, ext = os_module.path.splitext(file.filename) + name, ext = os.path.splitext(file.filename) filename = f"{name}_{current_time}_{unique_id}{ext}" else: filename = f"audio_{current_time}_{unique_id}.wav" @@ -101,7 +108,7 @@ def _upload_to_s3( """S3에 파일 업로드""" bucket = os.getenv("S3_BUCKET_NAME") if not bucket: - raise HTTPException(status_code=500, detail="S3_BUCKET_NAME not configured") + raise InternalServerException("S3_BUCKET_NAME not configured") # S3 키 생성: {session_id}/{user_id}/{filename} s3_key = f"{session_id}/{user_id}/{filename}" if session_id and user_id else f"chat/{filename}" @@ -130,11 +137,11 @@ def __init__(self, content: bytes, filename: str, content_type: str): stt_result = transcribe_voice(wrapped_file) if stt_result.get("error"): - raise HTTPException(status_code=400, detail=f"STT failed: {stt_result.get('error')}") + raise RuntimeException(f"STT failed: {stt_result.get('error')}") content = stt_result.get("transcript", "") if not content: - raise HTTPException(status_code=400, detail="STT result is empty") + raise ValidationException("STT result is empty") return content @@ -159,14 +166,14 @@ def __init__(self, content: bytes, filename: str, content_type: str): wrapped_file.file.seek(0) emotion_result = analyze_voice_emotion(wrapped_file) if emotion_result.get("error"): - raise HTTPException(status_code=400, detail=f"Emotion analysis failed: {emotion_result.get('error')}") + raise RuntimeException(f"Emotion analysis failed: {emotion_result.get('error')}") audio_probs = emotion_result.get("emotion_scores", {}) # 3-2. 텍스트 감정 분석 text_sentiment = analyze_text_sentiment(text_content, language_code="ko") if text_sentiment.get("error"): - raise HTTPException(status_code=400, detail=f"Text sentiment analysis failed: {text_sentiment.get('error')}") + raise RuntimeException(f"Text sentiment analysis failed: {text_sentiment.get('error')}") sentiment_data = text_sentiment.get("sentiment", {}) text_score = sentiment_data.get("score", 0.0) # [-1, 1] @@ -209,12 +216,12 @@ def __init__(self, content: bytes, filename: str, content_type: str): def _get_user_name(self, user_id: str) -> str: """사용자 이름 조회""" if not user_id: - raise HTTPException(status_code=400, detail="user_id is required") + raise ValidationException("user_id is required") auth_service = get_auth_service(self.db) user = auth_service.get_user_by_username(user_id) if not user: - raise HTTPException(status_code=404, detail=f"User not found: {user_id}") + raise NotFoundException(f"User not found: {user_id}") return user.name @@ -238,12 +245,10 @@ async def _send_to_chatbot( "user_name": user_name } - chatbot_url = os.getenv( - "CHATBOT_API_URL" - ) + chatbot_url = os.getenv("CHATBOT_API_URL") if not chatbot_url: - raise HTTPException(status_code=500, detail="CHATBOT_API_URL not configured") + raise InternalServerException("CHATBOT_API_URL not configured") try: async with httpx.AsyncClient(timeout=30.0) as client: @@ -254,15 +259,22 @@ async def _send_to_chatbot( ) response.raise_for_status() return response.json() - except httpx.HTTPStatusError as e: - # HTTP 에러 응답을 그대로 반환 - try: - error_response = e.response.json() - raise HTTPException(status_code=e.response.status_code, detail=error_response) - except: - raise HTTPException(status_code=e.response.status_code, detail=f"External API error: {e.response.text}") - except httpx.RequestError as e: - raise HTTPException(status_code=500, detail=f"External API request failed: {str(e)}") + except httpx.HTTPStatusError as exc: + error_message = self._extract_external_error(exc) + raise ExternalAPIException(status_code=exc.response.status_code, message=error_message) + except httpx.RequestError as exc: + raise ExternalAPIException(status_code=500, message=f"External API request failed: {str(exc)}") + + @staticmethod + def _extract_external_error(exc: httpx.HTTPStatusError) -> str: + """외부 API 예외 메시지를 문자열로 변환""" + try: + json_body = exc.response.json() + if isinstance(json_body, dict) and "message" in json_body: + return str(json_body["message"]) + return str(json_body) + except Exception: + return f"External API error: {exc.response.text}" def get_analyze_chat_service(db: Session) -> AnalyzeChatService: From 7c8e6eebdf3040d5e14f03089e860f162a5f09d2 Mon Sep 17 00:00:00 2001 From: jpark0506 <45231740+jpark0506@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:39:11 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refac=20:=20raise=20exception=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 7b5b82a..b630054 100644 --- a/app/main.py +++ b/app/main.py @@ -997,10 +997,12 @@ async def analyze_chat( user_id=user_id, question=question ) - except AppException as exc: - raise exc - except Exception: - raise InternalServerException("Internal server error while analyzing chat") + except AppException: + raise + except Exception as exc: + raise InternalServerException( + "Internal server error while analyzing chat" + ) from exc # ---------------- router 등록 ---------------- app.include_router(users_router) From c7633a4f5f3189c61fc8a795429fbb048f99d0d0 Mon Sep 17 00:00:00 2001 From: jpark0506 <45231740+jpark0506@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:42:09 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refac=20:=20=EB=93=A4=EC=97=AC=EC=93=B0?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index b630054..a849397 100644 --- a/app/main.py +++ b/app/main.py @@ -997,8 +997,8 @@ async def analyze_chat( user_id=user_id, question=question ) - except AppException: - raise + except AppException: + raise except Exception as exc: raise InternalServerException( "Internal server error while analyzing chat"