296 lines
15 KiB
Python
296 lines
15 KiB
Python
# app/controllers/mission_controller.py
|
|
"""
|
|
HealthSync AI 미션 관련 컨트롤러 (다층 특성 기반 AI 이모지 자동 매핑)
|
|
"""
|
|
from fastapi import APIRouter, status, HTTPException, Query
|
|
from app.controllers.base_controller import BaseController
|
|
from app.services.mission_service import MissionService
|
|
from app.dto.request.mission_request import MissionRecommendRequest
|
|
from app.dto.response.mission_response import MissionRecommendationResponse
|
|
from app.dto.request.celebration_request import CelebrationRequest
|
|
from app.dto.response.celebration_response import CelebrationResponse
|
|
from app.dto.response.similar_mission_news_response import SimilarMissionNewsResponse
|
|
from app.exceptions import (
|
|
UserNotFoundException,
|
|
HealthDataNotFoundException,
|
|
DatabaseException,
|
|
ClaudeAPIException
|
|
)
|
|
|
|
|
|
class MissionController(BaseController):
|
|
"""미션 관련 컨트롤러 (다층 특성 기반 AI 이모지 자동 매핑)"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.mission_service = MissionService()
|
|
self.router = APIRouter()
|
|
self._setup_routes()
|
|
|
|
def _setup_routes(self):
|
|
"""라우트 설정"""
|
|
|
|
@self.router.post("/recommend",
|
|
response_model=MissionRecommendationResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="🎯 AI 건강 미션 추천",
|
|
description="""
|
|
사용자의 건강검진 데이터를 기반으로 AI가 맞춤형 건강 미션을 추천합니다.
|
|
|
|
**처리 과정:**
|
|
1. 사용자 기본 정보 조회 (직업, 나이 등)
|
|
2. 최신 건강검진 데이터 조회
|
|
3. Claude AI를 통한 개인화된 미션 생성
|
|
4. 미션별 추천 이유와 함께 반환
|
|
|
|
**추천 미션 특징:**
|
|
- 일일 목표 횟수 1-5회 범위
|
|
- 사용자 건강 상태에 맞춤화
|
|
- 일상에서 실행 가능한 건강 행동
|
|
- 각 미션별 상세한 추천 이유 제공
|
|
""")
|
|
async def recommend_missions(request: MissionRecommendRequest) -> MissionRecommendationResponse:
|
|
"""AI 기반 건강 미션 추천"""
|
|
try:
|
|
self.log_request("recommend_missions", user_id=request.user_id)
|
|
|
|
# 미션 추천 서비스 호출
|
|
response = await self.mission_service.recommend_missions(request.user_id)
|
|
|
|
self.logger.info(f"미션 추천 성공 - user_id: {request.user_id}, "
|
|
f"추천 미션 수: {len(response.missions)}")
|
|
|
|
return response
|
|
|
|
except ValueError as e:
|
|
self.logger.warning(f"잘못된 요청 - user_id: {request.user_id}, error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"잘못된 요청입니다: {str(e)}"
|
|
)
|
|
except Exception as e:
|
|
self.handle_service_error(e, "recommend_missions")
|
|
|
|
@self.router.post("/celebrate",
|
|
response_model=CelebrationResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="🎉 미션 달성 축하 메시지",
|
|
description="""
|
|
사용자가 달성한 미션에 대해 AI가 개인화된 축하 메시지를 생성합니다.
|
|
|
|
**처리 과정:**
|
|
1. 미션 ID(숫자)로 데이터베이스에서 미션 정보 조회 (미션명, 설명, 목표 등)
|
|
2. 조회된 미션 정보를 기반으로 Claude AI 호출
|
|
3. 미션 내용을 반영한 맞춤형 축하 메시지 생성
|
|
4. 생성된 축하 메시지를 Chat DB에 "celebration" 타입으로 저장
|
|
5. 이모지와 함께 1줄 축하 메시지 반환
|
|
|
|
**축하 메시지 특징:**
|
|
- DB에서 조회한 실제 미션 정보 반영
|
|
- 간결하고 따뜻한 1줄 메시지 (50자 내외)
|
|
- 다양한 이모지로 시각적 효과
|
|
- 미션별 맞춤형 축하 내용
|
|
- 지속적인 동기부여 유도
|
|
""")
|
|
async def celebrate_mission(request: CelebrationRequest) -> CelebrationResponse:
|
|
"""미션 달성 축하 메시지 생성 및 Chat DB 저장"""
|
|
try:
|
|
self.log_request("celebrate_mission",
|
|
user_id=request.userId, mission_id=request.missionId)
|
|
|
|
# 축하 메시지 서비스 호출 (Chat DB 저장 포함)
|
|
response = await self.mission_service.generate_celebration_message(
|
|
user_id=request.userId,
|
|
mission_id=request.missionId
|
|
)
|
|
|
|
self.logger.info(f"미션 축하 성공 (Chat DB 저장 완료) - user_id: {request.userId}, "
|
|
f"mission_id: {request.missionId}, "
|
|
f"메시지 길이: {len(response.congratsMessage)}")
|
|
|
|
return response
|
|
|
|
except HealthDataNotFoundException as e:
|
|
self.logger.warning(f"미션을 찾을 수 없음 - mission_id: {request.missionId}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"미션 ID {request.missionId}를 찾을 수 없습니다."
|
|
)
|
|
except DatabaseException as e:
|
|
self.logger.error(f"데이터베이스 오류 - error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="데이터베이스 연결에 문제가 있습니다. 잠시 후 다시 시도해 주세요."
|
|
)
|
|
except ClaudeAPIException as e:
|
|
self.logger.error(f"Claude API 오류 - error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail="AI 서비스에 일시적인 문제가 있습니다. 잠시 후 다시 시도해 주세요."
|
|
)
|
|
except Exception as e:
|
|
self.handle_service_error(e, "celebrate_mission")
|
|
|
|
@self.router.get("/similar-news",
|
|
response_model=SimilarMissionNewsResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="🔔 유사 사용자 미션 완료 소식 (5가지 기준별 선별)",
|
|
description="""
|
|
**5가지 유사도 기준별로 각각 1명씩 총 5개의 미션 완료 소식을 조회합니다.**
|
|
|
|
### 🎯 5가지 유사도 기준:
|
|
1. **직업 유사도**: 동일 직업 또는 유사 직업군 (테크/케어/서비스)
|
|
2. **나이 유사도**: 연령대가 비슷한 사용자 (10세 차이 내)
|
|
3. **BMI/체형 유사도**: 체형이 비슷한 사용자 (마른/표준/통통)
|
|
4. **혈압 유사도**: 혈압 수치가 비슷한 사용자 (정상/주의/높음)
|
|
5. **혈당 유사도**: 혈당 수치가 비슷한 사용자 (정상/주의/높음)
|
|
|
|
### 📋 다양한 메시지 형태 (각 기준별 1개씩):
|
|
1. **직업 기준**: "IT직군 김OO님이 물마시기 미션을 완료했어요! 💧"
|
|
2. **나이 기준**: "23세 홍OO님이 산책을 완료했어요! 🚶♀️"
|
|
3. **체형 기준**: "통통한 박OO님이 스트레칭을 완료했어요! 🧘♂️"
|
|
4. **혈압 기준**: "혈압높은 이OO님이 명상을 완료했어요! 🧘♀️"
|
|
5. **혈당 기준**: "혈당주의 최OO님이 계단오르기를 완료했어요! 🏃♂️"
|
|
|
|
### ⚡ 성능 최적화:
|
|
- **빠른 이모지 매핑**: AI 대신 키워드 기반 매핑으로 빠른 응답
|
|
- **중복 제거**: 같은 사용자가 여러 기준에서 선정되는 것 방지
|
|
- **다양성 보장**: 5가지 다른 기준으로 다양한 유사성 표현
|
|
- **최소 유사도 임계값**: 0.3 이상의 유사도를 가진 사용자만 선별
|
|
|
|
### 🔄 선별 프로세스:
|
|
1. **후보 수집**: 벡터 유사도 기준 상위 30명 조회
|
|
2. **기준별 계산**: 각 후보에 대해 5가지 기준별 유사도 점수 계산
|
|
3. **최적 선별**: 각 기준별로 가장 높은 점수의 사용자 1명씩 선택
|
|
4. **중복 제거**: 동일 사용자가 여러 기준에서 선정될 경우 가장 높은 점수 기준만 적용
|
|
5. **결과 반환**: 최대 5개의 다양한 유사 사용자 소식 반환
|
|
|
|
### 💡 유사 직업군 분류:
|
|
- **테크 그룹**: 사무직(OFF001), IT직군(ENG001)
|
|
- **케어 그룹**: 의료진(MED001), 교육직(EDU001)
|
|
- **서비스 그룹**: 서비스직(SRV001)
|
|
|
|
### 🏥 건강 특성 분류:
|
|
- **BMI**: 마른(<18.5), 표준(18.5-25), 통통(≥25)
|
|
- **혈압**: 정상(<130), 주의(130-139), 높음(≥140)
|
|
- **혈당**: 정상(<100), 주의(100-125), 높음(≥126)
|
|
|
|
### 🔒 개인정보 보호:
|
|
- 이름 마스킹 처리 (김OO 형식)
|
|
- 구체적인 수치 노출 없이 특성만 표시
|
|
- 다양한 기준으로 분산하여 개인 식별 위험 최소화
|
|
|
|
**이제 5가지 다른 기준으로 선별된 다양한 유사 사용자들의 미션 완료 소식을 빠르게 확인할 수 있습니다!**
|
|
""")
|
|
async def get_similar_mission_news(
|
|
user_id: int = Query(..., gt=0, description="사용자 ID")) -> SimilarMissionNewsResponse:
|
|
"""유사 사용자 미션 완료 소식 조회 (다층 특성 기반 AI 이모지)"""
|
|
try:
|
|
self.log_request("get_similar_mission_news", user_id=user_id)
|
|
response = await self.mission_service.get_similar_mission_news(user_id)
|
|
return response
|
|
except Exception as e:
|
|
self.handle_service_error(e, "get_similar_mission_news")
|
|
|
|
@self.router.post("/upsert-vector",
|
|
status_code=status.HTTP_200_OK,
|
|
summary="📊 모든 사용자 벡터 일괄 저장/업데이트",
|
|
description="""
|
|
**모든 사용자의 건강 중심 벡터를 Pinecone DB에 일괄 저장/업데이트합니다.**
|
|
|
|
### 🔄 처리 과정:
|
|
1. **전체 사용자 조회**: PostgreSQL에서 모든 사용자 목록 조회
|
|
2. **기존 벡터 확인**: Pinecone에서 이미 저장된 벡터 ID 목록 조회
|
|
3. **신규 사용자 필터링**: 벡터가 없는 사용자만 필터링
|
|
4. **건강 데이터 조회**: 각 사용자별 최신 건강검진 데이터 조회
|
|
5. **건강 중심 벡터 생성**: 1024차원 건강 특성 벡터 생성
|
|
6. **벡터 저장**: Pinecone에 저장
|
|
7. **진행상황 로깅**: 실시간 처리 현황 로그 출력
|
|
|
|
### 🎯 건강 중심 벡터 구성 (1024차원):
|
|
- **나이 특성** (50차원): 연령대별 유사도 강화
|
|
- **건강 위험도** (300차원): BMI, 혈압, 혈당, 콜레스테롤 등 주요 지표
|
|
- **상세 건강 특성** (500차원): 세부 건강 데이터 정교한 벡터화
|
|
- **직업 특성** (100차원): 직업별 건강 위험 프로필
|
|
- **생활습관** (74차원): 흡연, 음주 상태
|
|
|
|
### ⚡ 성능 최적화:
|
|
- **스킵 로직**: 이미 벡터가 있는 사용자는 건너뛰기
|
|
- **배치 처리**: 여러 사용자를 한 번에 처리
|
|
- **에러 핸들링**: 개별 사용자 실패 시에도 전체 프로세스 계속 진행
|
|
- **진행률 표시**: 전체 진행 상황 실시간 확인
|
|
|
|
### 📊 응답 정보:
|
|
- **총 사용자 수**: 전체 사용자 수
|
|
- **기존 벡터 수**: 이미 저장된 벡터 수
|
|
- **신규 처리 수**: 새로 저장된 벡터 수
|
|
- **성공/실패 수**: 처리 결과 통계
|
|
- **소요 시간**: 전체 처리 시간
|
|
|
|
### 🔧 사용 시점:
|
|
- 초기 시스템 구축 시 모든 사용자 벡터 생성
|
|
- 정기적인 벡터 데이터 동기화
|
|
- 새로운 사용자 대량 등록 후 벡터 일괄 생성
|
|
- 벡터 알고리즘 업데이트 후 재생성
|
|
|
|
### ⚠️ 주의사항:
|
|
- 대량 데이터 처리로 시간이 오래 걸릴 수 있음
|
|
- Pinecone API 요청 한도 고려 필요
|
|
- 처리 중 중단되어도 부분적으로 완료된 데이터는 유지됨
|
|
""")
|
|
async def upsert_all_user_vectors():
|
|
"""모든 사용자 건강 중심 벡터 일괄 저장/업데이트 (기존 벡터 스킵)"""
|
|
try:
|
|
self.log_request("upsert_all_user_vectors")
|
|
|
|
# 모든 사용자 벡터 일괄 처리 서비스 호출
|
|
result = await self.mission_service.upsert_all_user_vectors()
|
|
|
|
if result["success"]:
|
|
self.logger.info(f"✅ 모든 사용자 건강 중심 벡터 일괄 처리 완료 - "
|
|
f"총 사용자: {result['total_users']}, "
|
|
f"기존 벡터: {result['existing_vectors']}, "
|
|
f"신규 저장: {result['new_vectors']}, "
|
|
f"실패: {result['failed']}")
|
|
|
|
return self.create_success_response(
|
|
data=result,
|
|
message=f"건강 중심 벡터 일괄 처리 완료! 신규 {result['new_vectors']}개 저장, "
|
|
f"기존 {result['existing_vectors']}개 스킵"
|
|
)
|
|
else:
|
|
self.logger.warning(f"⚠️ 벡터 일괄 처리 부분 실패 - "
|
|
f"성공: {result['new_vectors']}, 실패: {result['failed']}")
|
|
|
|
return self.create_success_response(
|
|
data=result,
|
|
message=f"벡터 일괄 처리 부분 완료. 일부 사용자 처리 실패: {result['failed']}개"
|
|
)
|
|
|
|
except DatabaseException as e:
|
|
self.logger.error(f"❌ 데이터베이스 오류 - error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="데이터베이스 연결에 문제가 있습니다. 잠시 후 다시 시도해 주세요."
|
|
)
|
|
except Exception as e:
|
|
self.logger.error(f"❌ 벡터 일괄 처리 중 예상치 못한 오류: {str(e)}", exc_info=True)
|
|
|
|
# 일괄 처리 실패해도 서비스는 계속 동작 가능하도록 처리
|
|
return self.create_success_response(
|
|
data={
|
|
"success": False,
|
|
"total_users": 0,
|
|
"existing_vectors": 0,
|
|
"new_vectors": 0,
|
|
"failed": 0,
|
|
"error_message": "벡터 일괄 처리에 실패했지만 개별 기능은 정상 이용 가능합니다.",
|
|
"error_type": type(e).__name__
|
|
},
|
|
message="벡터 일괄 처리에 실패했지만 서비스는 계속 이용 가능합니다."
|
|
)
|
|
|
|
|
|
# 컨트롤러 인스턴스 생성
|
|
mission_controller = MissionController()
|