feat : initial commit

This commit is contained in:
hehe 2025-06-20 05:51:38 +00:00
commit 00eae37abc
26 changed files with 924 additions and 0 deletions

28
.env.example Normal file
View File

@ -0,0 +1,28 @@
# .env.example
# HealthSync Motivator Batch 환경설정 샘플
# 애플리케이션 설정
APP_NAME=HealthSync Motivator Batch
APP_VERSION=1.0.0
DEBUG=True
# PostgreSQL 데이터베이스 설정
DB_HOST=localhost
DB_PORT=5432
DB_NAME=healthsync_ai
DB_USERNAME=postgres
DB_PASSWORD=your_database_password_here
# Claude AI API 설정
CLAUDE_API_KEY=your_claude_api_key_here
CLAUDE_MODEL=claude-3-5-sonnet-20241022
CLAUDE_MAX_TOKENS=150
CLAUDE_TEMPERATURE=0.7
CLAUDE_TIMEOUT=30
# 배치 설정
BATCH_SIZE=100
MAX_RETRIES=3
# 로깅 설정
LOG_LEVEL=INFO

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
.venv/
# Environment variables
.env
.env.local
.env.production
# IDE
.vscode/
.idea/
*.swp
*.swo
# Logs
*.log
logs/
# Database
*.db
*.sqlite3
healthsync_ai.db
# OS
.DS_Store
Thumbs.db
# HealthSync AI specific
temp/
uploads/
cache/

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
FROM python:3.11-slim
# 작업 디렉토리 설정
WORKDIR /app
# 시스템 패키지 업데이트 및 필수 패키지 설치
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Python 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 애플리케이션 코드 복사
COPY app/ ./app/
# 실행 권한 부여
RUN chmod +x app/batch_runner.py
# 환경변수 설정
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
# 기본 명령어 (배치 실행)
CMD ["python", "app/batch_runner.py"]

40
README.md Normal file
View File

@ -0,0 +1,40 @@
# HealthSync Motivator Batch Service
AI 기반 미션 독려 메시지 생성 배치 서비스
## 🎯 서비스 개요
사용자가 완료하지 않은 건강 미션을 조회하여 Claude AI를 통해 개인화된 독려 메시지를 생성하고, 채팅 DB에 저장하는 배치 서비스입니다.
## 🏗️ 아키텍처
```
📁 motivator-batch-service/
├── app/
│ ├── batch_runner.py # 메인 실행파일 (크론탭용)
│ ├── config/ # 환경설정
│ ├── models/ # 데이터 모델
│ ├── services/ # 비즈니스 로직
│ ├── repositories/ # 데이터베이스 쿼리
│ └── utils/ # 유틸리티
```
## 🔄 배치 처리 플로우
1. **활성 사용자 조회**: 최근 30일 내 로그인한 사용자
2. **미완료 미션 조회**: 오늘 완료되지 않은 미션 목록
3. **독려 메시지 생성**: Claude AI를 통한 개인화된 메시지 생성
4. **채팅 DB 저장**: `intelligence_service.chat_message` 테이블에 저장
## 🚀 실행 방법
### 로컬 실행
```bash
# 환경설정 파일 생성
cp .env.example .env
# .env 파일 수정 (API 키, DB 정보 등)
# 의존성 설치
pip install -r requirements.txt
# 배치 실행
python app/batch_runner.py

7
app/__init__.py Normal file
View File

@ -0,0 +1,7 @@
"""
HealthSync AI Motivator Batch Service
독려 메시지 생성 배치 서비스
"""
__version__ = "1.0.0"
__title__ = "HealthSync Motivator Batch"
__description__ = "AI 기반 미션 독려 메시지 생성 배치 서비스"

71
app/batch_runner.py Normal file
View File

@ -0,0 +1,71 @@
# app/batch_runner.py
"""
HealthSync Motivator Batch 메인 실행파일 (크론탭용)
"""
import asyncio
import sys
from datetime import datetime
from app.config.settings import settings
from app.utils.database_utils import db
from app.utils.logger import batch_logger
from app.services.motivation_service import motivation_service
async def main():
"""메인 배치 실행 함수"""
start_time = datetime.now()
batch_logger.info(f"🚀 {settings.app_name} 배치 시작 - {start_time}")
try:
# 1. 데이터베이스 연결 테스트
batch_logger.info("🔍 데이터베이스 연결 테스트...")
connection_ok = await db.test_connection()
if not connection_ok:
raise Exception("데이터베이스 연결 실패")
batch_logger.info("✅ 데이터베이스 연결 성공")
# 2. 독려 메시지 배치 처리 실행
batch_logger.info("💌 독려 메시지 배치 처리 시작...")
results = await motivation_service.process_all_users()
# 3. 처리 결과 요약
end_time = datetime.now()
duration = end_time - start_time
batch_logger.info(f"🎉 배치 처리 완료!")
batch_logger.info(f"📊 처리 결과:")
batch_logger.info(f" - 전체 사용자: {results['total_users']}")
batch_logger.info(f" - 처리 완료: {results['processed_users']}")
batch_logger.info(f" - 메시지 발송: {results['sent_messages']}")
batch_logger.info(f" - 건너뛴 사용자: {results['skipped_users']}")
batch_logger.info(f" - 실패한 사용자: {results['failed_users']}")
batch_logger.info(f"⏱️ 총 소요시간: {duration}")
# 4. 성공 종료
return 0
except Exception as e:
batch_logger.error(f"❌ 배치 처리 실패: {str(e)}")
return 1
finally:
# 5. 데이터베이스 연결 해제
try:
await db.disconnect()
batch_logger.info("🔌 데이터베이스 연결 해제 완료")
except Exception as e:
batch_logger.error(f"❌ 데이터베이스 연결 해제 실패: {str(e)}")
if __name__ == "__main__":
"""크론탭에서 실행되는 엔트리포인트"""
try:
# 비동기 메인 함수 실행
exit_code = asyncio.run(main())
sys.exit(exit_code)
except KeyboardInterrupt:
batch_logger.info("⚠️ 사용자에 의한 배치 중단")
sys.exit(1)
except Exception as e:
batch_logger.error(f"❌ 배치 실행 중 예상치 못한 오류: {str(e)}")
sys.exit(1)

0
app/config/__init__.py Normal file
View File

34
app/config/prompts.py Normal file
View File

@ -0,0 +1,34 @@
# app/config/prompts.py
"""
HealthSync Motivator Batch 프롬프트 설정
"""
class PromptConfig:
"""독려 메시지 프롬프트 템플릿"""
ENCOURAGEMENT_PROMPT = """
당신은 건강 미션을 격려하는 따뜻한 AI 코치입니다.
**독려 메시지 작성 원칙:**
- 정확히 1줄로 작성 (50 내외)
- 따뜻하고 격려적인
- 적절한 이모지 2-3 사용
- 미션의 구체적인 내용을 반영
- 지속적인 동기부여 메시지 포함
**사용자 정보:**
- 직업: {occupation}
- 미완료 미션들: {incomplete_missions}
정보를 바탕으로 사용자가 미션을 완료하도록 격려하는 메시지 1줄을 작성해주세요.
메시지만 작성하고 다른 말은 하지 마세요.
예시:
💪 오늘도 건강한 하루를 위해 미션을 완료해보세요! 작은 실천이 변화를 만들어요!
🌟 바쁜 하루 중에도 잠깐의 시간을 내어 건강을 챙겨보세요! 당신의 몸이 고마워할 거예요! 💚
🚶 오늘의 미션이 기다리고 있어요! 건강한 습관으로 나은 내일을 만들어가요! 🌈
"""
def get_encouragement_prompt() -> str:
"""독려 메시지 프롬프트 템플릿 반환"""
return PromptConfig.ENCOURAGEMENT_PROMPT

52
app/config/settings.py Normal file
View File

@ -0,0 +1,52 @@
# app/config/settings.py
"""
HealthSync Motivator Batch 환경설정
"""
from pydantic_settings import BaseSettings
from typing import Optional
import os
from dotenv import load_dotenv
# .env 파일 로드
load_dotenv()
class Settings(BaseSettings):
"""Motivator Batch 애플리케이션 설정 클래스"""
# 기본 앱 설정
app_name: str = "HealthSync Motivator Batch"
app_version: str = "1.0.0"
debug: bool = True
# PostgreSQL 설정
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "healthsync_ai"
db_username: str = "postgres"
db_password: str = "password"
# Claude AI 설정
claude_api_key: str = ""
claude_model: str = "claude-3-5-sonnet-20241022"
claude_max_tokens: int = 150
claude_temperature: float = 0.7
claude_timeout: int = 30
# 배치 설정
batch_size: int = 100
max_retries: int = 3
# 로깅 설정
log_level: str = "INFO"
@property
def database_url(self) -> str:
"""데이터베이스 URL 생성"""
return f"postgresql://{self.db_username}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
class Config:
env_file = ".env"
case_sensitive = False
# 전역 설정 인스턴스
settings = Settings()

9
app/models/__init__.py Normal file
View File

@ -0,0 +1,9 @@
# app/models/__init__.py
"""
HealthSync Motivator Batch 모델 패키지
"""
from .user import User
from .goal import UserMissionGoal, MissionCompletionHistory
from .chat_message import ChatMessage
__all__ = ["User", "UserMissionGoal", "MissionCompletionHistory", "ChatMessage"]

View File

@ -0,0 +1,28 @@
# app/models/chat_message.py
"""
HealthSync AI 채팅 메시지 모델
"""
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional
from enum import Enum
class MessageType(str, Enum):
"""메시지 타입"""
CONSULTATION = "consultation" # 상담
CELEBRATION = "celebration" # 축하
ENCOURAGEMENT = "encouragement" # 독려
class ChatMessage(BaseModel):
"""채팅 메시지 모델"""
message_id: Optional[int] = Field(None, description="메시지 ID")
member_serial_number: int = Field(..., description="회원 일련번호")
message_type: str = Field(..., max_length=20, description="메시지 타입")
message_content: Optional[str] = Field(None, description="메시지 내용")
response_content: Optional[str] = Field(None, description="AI 응답 내용")
created_at: datetime = Field(default_factory=datetime.now, description="생성일시")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}

40
app/models/goal.py Normal file
View File

@ -0,0 +1,40 @@
# app/models/goal.py
"""
HealthSync Motivator Batch 목표 관련 모델 (goal_service)
"""
from pydantic import BaseModel, Field
from datetime import datetime, date
from typing import Optional
class UserMissionGoal(BaseModel):
"""사용자 미션 목표 (goal_service.user_mission_goal 테이블)"""
mission_id: int = Field(..., description="미션 ID")
member_serial_number: int = Field(..., description="회원 일련번호")
performance_date: date = Field(..., description="수행 날짜")
mission_name: str = Field(..., max_length=100, description="미션명")
mission_description: Optional[str] = Field(None, max_length=200, description="미션 설명")
daily_target_count: int = Field(..., description="일일 목표 횟수")
is_active: bool = Field(..., description="활성 상태")
created_at: datetime = Field(..., description="생성일시")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat(),
date: lambda v: v.isoformat()
}
class MissionCompletionHistory(BaseModel):
"""미션 완료 이력 (goal_service.mission_completion_history 테이블)"""
completion_id: int = Field(..., description="완료 ID")
mission_id: int = Field(..., description="미션 ID")
member_serial_number: int = Field(..., description="회원 일련번호")
completion_date: date = Field(..., description="완료 날짜")
daily_target_count: int = Field(..., description="일일 목표 횟수")
daily_completed_count: int = Field(..., description="일일 완료 횟수")
created_at: datetime = Field(..., description="생성일시")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat(),
date: lambda v: v.isoformat()
}

24
app/models/user.py Normal file
View File

@ -0,0 +1,24 @@
# app/models/user.py
"""
HealthSync Motivator Batch 사용자 모델 (user_service.user)
"""
from pydantic import BaseModel, Field
from datetime import datetime, date
from typing import Optional
class User(BaseModel):
"""사용자 모델 (user_service.user 테이블)"""
member_serial_number: int = Field(..., description="회원 일련번호")
google_id: str = Field(..., max_length=255, description="구글 ID")
name: str = Field(..., max_length=100, description="사용자 이름")
birth_date: date = Field(..., description="생년월일")
occupation: Optional[str] = Field(None, max_length=50, description="직업")
created_at: datetime = Field(..., description="생성일시")
updated_at: datetime = Field(..., description="수정일시")
last_login_at: Optional[datetime] = Field(None, description="마지막 로그인")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat(),
date: lambda v: v.isoformat()
}

View File

@ -0,0 +1,4 @@
# app/repositories/__init__.py
"""
HealthSync Motivator Batch 리포지토리 패키지
"""

View File

@ -0,0 +1,10 @@
# app/repositories/queries/__init__.py
"""
HealthSync Motivator Batch 쿼리 패키지
"""
from .base_queries import BaseQueries
from .user_queries import UserQueries
from .goal_queries import GoalQueries
from .chat_queries import ChatQueries
__all__ = ["BaseQueries", "UserQueries", "GoalQueries", "ChatQueries"]

View File

@ -0,0 +1,13 @@
# app/repositories/queries/base_queries.py
"""
HealthSync Motivator Batch 기본 쿼리
"""
class BaseQueries:
"""기본 시스템 쿼리"""
# 데이터베이스 연결 테스트
CONNECTION_TEST = "SELECT 1"
# 현재 시간 조회
CURRENT_TIMESTAMP = "SELECT NOW()"

View File

@ -0,0 +1,23 @@
# app/repositories/queries/chat_queries.py
"""
HealthSync Motivator Batch 채팅 메시지 관련 쿼리
"""
class ChatQueries:
"""채팅 메시지 관련 쿼리"""
# 독려 메시지 저장 (수정된 쿼리)
INSERT_ENCOURAGEMENT_MESSAGE = """
INSERT INTO intelligence_service.chat_message
(member_serial_number, message_type, message_content, response_content, created_at)
VALUES (:member_serial_number, :message_type, :message_content, :response_content, :created_at)
"""
# 오늘 이미 독려 메시지를 받았는지 확인
CHECK_TODAY_ENCOURAGEMENT = """
SELECT COUNT(*) as count
FROM intelligence_service.chat_message cm
WHERE cm.member_serial_number = :user_id
AND cm.message_type = 'encouragement'
AND DATE(cm.created_at) = CURRENT_DATE
"""

View File

@ -0,0 +1,54 @@
# app/repositories/queries/goal_queries.py
"""
HealthSync Motivator Batch 목표/미션 관련 쿼리
"""
class GoalQueries:
"""목표/미션 관련 쿼리"""
# 사용자별 활성 미션 조회
GET_ACTIVE_MISSIONS_BY_USER = """
SELECT
umg.mission_id,
umg.member_serial_number,
umg.mission_name,
umg.mission_description,
umg.daily_target_count,
umg.performance_date
FROM goal_service.user_mission_goal umg
WHERE umg.member_serial_number = :user_id
AND umg.is_active = true
AND umg.performance_date = CURRENT_DATE
"""
# 오늘 완료된 미션 조회
GET_TODAY_COMPLETED_MISSIONS = """
SELECT
mch.mission_id,
mch.member_serial_number,
mch.daily_target_count,
mch.daily_completed_count
FROM goal_service.mission_completion_history mch
WHERE mch.member_serial_number = :user_id
AND mch.completion_date = CURRENT_DATE
AND mch.daily_completed_count >= mch.daily_target_count
"""
# 미완료 미션 조회 (오늘 활성 미션 중 완료되지 않은 것들)
GET_INCOMPLETE_MISSIONS = """
SELECT
umg.mission_id,
umg.mission_name,
umg.mission_description,
mch.daily_completed_count,
umg.daily_target_count,
mch.completion_date
FROM goal_service.user_mission_goal umg
LEFT JOIN goal_service.mission_completion_history mch
ON umg.mission_id = mch.mission_id
AND umg.member_serial_number = mch.member_serial_number
AND mch.completion_date = CURRENT_DATE
WHERE umg.member_serial_number = :user_id
AND ((mch.completion_date = CURRENT_DATE AND mch.daily_completed_count < mch.daily_target_count)
OR mch.completion_date IS NULL);
"""

View File

@ -0,0 +1,28 @@
# app/repositories/queries/user_queries.py
"""
HealthSync Motivator Batch 사용자 관련 쿼리
"""
class UserQueries:
"""사용자 관련 쿼리"""
# 활성 사용자 목록 조회
GET_ACTIVE_USERS = """
SELECT
u.member_serial_number,
u.name,
u.occupation
FROM user_service.user u
WHERE u.last_login_at >= NOW() - INTERVAL '30 days'
ORDER BY u.member_serial_number
"""
# 특정 사용자 정보 조회
GET_USER_BY_ID = """
SELECT
u.member_serial_number,
u.name,
u.occupation
FROM user_service.user u
WHERE u.member_serial_number = :user_id
"""

4
app/services/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# app/services/__init__.py
"""
HealthSync Motivator Batch 서비스 패키지
"""

View File

@ -0,0 +1,165 @@
# app/services/motivation_service.py
"""
HealthSync Motivator Batch 독려 메시지 생성 서비스
"""
import asyncio
from typing import Dict, Any, List
from datetime import datetime
from app.utils.database_utils import db
from app.utils.claude_client import claude_client
from app.utils.logger import batch_logger
from app.config.prompts import get_encouragement_prompt
from app.repositories.queries import UserQueries, GoalQueries, ChatQueries
from app.models.chat_message import MessageType
class MotivationService:
"""독려 메시지 생성 비즈니스 로직 서비스"""
def __init__(self):
self.logger = batch_logger
async def process_all_users(self) -> Dict[str, int]:
"""모든 활성 사용자에 대해 독려 메시지 처리"""
try:
self.logger.info("🚀 독려 메시지 배치 처리 시작")
# 1. 활성 사용자 목록 조회
active_users = await db.execute_query(UserQueries.GET_ACTIVE_USERS)
self.logger.info(f"📊 활성 사용자 수: {len(active_users)}")
# 처리 결과 카운터
results = {
"total_users": len(active_users),
"processed_users": 0,
"sent_messages": 0,
"skipped_users": 0,
"failed_users": 0
}
# 2. 각 사용자별 독려 메시지 처리
for user in active_users:
try:
user_id = user["member_serial_number"]
self.logger.info(f"👤 사용자 처리 시작: {user['name']} (ID: {user_id})")
# 사용자별 독려 메시지 처리
message_sent = await self._process_user_encouragement(user)
results["processed_users"] += 1
if message_sent:
results["sent_messages"] += 1
else:
results["skipped_users"] += 1
except Exception as e:
self.logger.error(f"❌ 사용자 처리 실패 - user_id: {user.get('member_serial_number')}, error: {str(e)}")
results["failed_users"] += 1
self.logger.info(f"✅ 독려 메시지 배치 처리 완료: {results}")
return results
except Exception as e:
self.logger.error(f"❌ 배치 처리 전체 실패: {str(e)}")
raise Exception(f"배치 처리 실패: {str(e)}")
async def _process_user_encouragement(self, user: Dict[str, Any]) -> bool:
"""개별 사용자 독려 메시지 처리"""
try:
user_id = user["member_serial_number"]
user_name = user["name"]
occupation = user.get("occupation", "정보 없음")
# # 1. 오늘 이미 독려 메시지를 받았는지 확인
# today_encouragement = await db.execute_query(
# ChatQueries.CHECK_TODAY_ENCOURAGEMENT,
# {"user_id": user_id}
# )
#
# if today_encouragement and today_encouragement[0]["count"] > 0:
# self.logger.info(f"⏭️ 이미 독려 메시지 발송됨 - user: {user_name}")
# return False
# 2. 미완료 미션 조회
incomplete_missions = await db.execute_query(
GoalQueries.GET_INCOMPLETE_MISSIONS,
{"user_id": user_id}
)
if not incomplete_missions:
self.logger.info(f"✅ 미완료 미션 없음 - user: {user_name}")
return False
# 3. 독려 메시지 생성
encouragement_message = await self._generate_encouragement_message(
occupation, incomplete_missions
)
# 4. 채팅 DB에 저장
await self._save_encouragement_message(user_id, encouragement_message)
self.logger.info(f"💌 독려 메시지 발송 완료 - user: {user_name}, "
f"미완료 미션: {len(incomplete_missions)}")
return True
except Exception as e:
self.logger.error(f"❌ 사용자 독려 메시지 처리 실패 - user_id: {user_id}, error: {str(e)}")
raise Exception(f"사용자 독려 메시지 처리 실패: {str(e)}")
async def _generate_encouragement_message(self, occupation: str, incomplete_missions: List[Dict[str, Any]]) -> str:
"""Claude API를 통한 독려 메시지 생성"""
try:
# 미완료 미션 목록 문자열 생성
mission_list = []
for mission in incomplete_missions:
mission_list.append(f"- {mission['mission_name']} (목표: {mission['daily_target_count']}회)")
missions_text = "\n".join(mission_list)
# 프롬프트 생성
prompt_template = get_encouragement_prompt()
formatted_prompt = prompt_template.format(
occupation=occupation,
incomplete_missions=missions_text
)
# Claude API 호출
claude_response = await claude_client.call_claude_api(formatted_prompt)
# 응답 정제 (앞뒤 공백 제거, 따옴표 제거)
encouragement_message = claude_response.strip().strip('"').strip("'")
self.logger.info(f"💭 독려 메시지 생성 완료 - 길이: {len(encouragement_message)}")
return encouragement_message
except Exception as e:
self.logger.error(f"❌ 독려 메시지 생성 실패: {str(e)}")
raise Exception(f"독려 메시지 생성 실패: {str(e)}")
async def _save_encouragement_message(self, user_id: int, message: str) -> None:
"""독려 메시지를 채팅 DB에 저장 (수정된 메서드)"""
try:
# ChatMessage 모델에 맞춰 데이터 구성
message_data = {
"member_serial_number": user_id,
"message_type": MessageType.ENCOURAGEMENT.value, # "encouragement"
"message_content": None, # 사용자 입력이 없으므로 None
"response_content": message, # AI가 생성한 독려 메시지
"created_at": datetime.now()
}
await db.execute_insert(
ChatQueries.INSERT_ENCOURAGEMENT_MESSAGE,
message_data
)
self.logger.info(f"💾 독려 메시지 DB 저장 완료 - user_id: {user_id}, "
f"message_type: {MessageType.ENCOURAGEMENT.value}")
except Exception as e:
self.logger.error(f"❌ 독려 메시지 DB 저장 실패 - user_id: {user_id}, error: {str(e)}")
raise Exception(f"독려 메시지 DB 저장 실패: {str(e)}")
# 전역 서비스 인스턴스
motivation_service = MotivationService()

4
app/utils/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# app/utils/__init__.py
"""
HealthSync Motivator Batch 유틸리티 패키지
"""

View File

@ -0,0 +1,72 @@
# app/utils/claude_client.py
"""
HealthSync Motivator Batch Claude API 클라이언트
"""
import logging
import asyncio
import anthropic
from app.config.settings import settings
logger = logging.getLogger(__name__)
class ClaudeClient:
"""Claude API 호출 클라이언트"""
def __init__(self):
self.api_key = settings.claude_api_key
# API 키 검증
if not self.api_key or self.api_key == "" or self.api_key == "your_claude_api_key_here":
raise ValueError("Claude API 키가 설정되지 않았습니다. .env 파일에 CLAUDE_API_KEY를 설정해주세요.")
# 동기 클라이언트 초기화
self.client = anthropic.Anthropic(api_key=self.api_key)
logger.info(f"✅ Claude API 클라이언트 초기화 완료")
async def call_claude_api(self, prompt: str) -> str:
"""Claude API 호출 (비동기 래퍼)"""
try:
logger.info(f"🚀 Claude API 호출 시작 (모델: {settings.claude_model})")
# 동기 함수를 비동기로 실행
def sync_call():
return self.client.messages.create(
model=settings.claude_model,
max_tokens=settings.claude_max_tokens,
temperature=settings.claude_temperature,
messages=[
{
"role": "user",
"content": prompt
}
]
)
# 스레드 풀에서 동기 함수 실행
message = await asyncio.get_event_loop().run_in_executor(None, sync_call)
logger.info("✅ Claude API 호출 성공")
return message.content[0].text
except anthropic.AuthenticationError as e:
logger.error("❌ Claude API 인증 실패 - API 키 확인 필요")
raise Exception(f"Claude API 인증 실패: {str(e)}")
except anthropic.NotFoundError as e:
logger.error("❌ Claude API 엔드포인트 또는 모델을 찾을 수 없음")
raise Exception(f"Claude API 모델 또는 엔드포인트 오류: {str(e)}")
except anthropic.RateLimitError as e:
logger.error("❌ Claude API 요청 한도 초과")
raise Exception(f"Claude API 요청 한도 초과: {str(e)}")
except anthropic.APITimeoutError as e:
logger.error("⏰ Claude API 타임아웃")
raise Exception(f"Claude API 타임아웃: {str(e)}")
except Exception as e:
logger.error(f"❌ Claude API 호출 중 예상치 못한 오류: {str(e)}")
raise Exception(f"Claude API 호출 실패: {str(e)}")
# 전역 클라이언트 인스턴스
claude_client = ClaudeClient()

View File

@ -0,0 +1,86 @@
# app/utils/database_utils.py
"""
HealthSync Motivator Batch 데이터베이스 유틸리티
"""
import databases
import logging
from typing import Dict, Any, List, Optional
from app.config.settings import settings
from app.repositories.queries import BaseQueries
logger = logging.getLogger(__name__)
class SimpleDatabase:
"""PostgreSQL 데이터베이스 연결 클래스"""
def __init__(self):
self.database = databases.Database(settings.database_url)
self._connected = False
async def connect(self):
"""데이터베이스 연결"""
if not self._connected:
try:
await self.database.connect()
self._connected = True
logger.info("데이터베이스 연결 성공")
except Exception as e:
logger.error(f"데이터베이스 연결 실패: {str(e)}")
raise
async def disconnect(self):
"""데이터베이스 연결 해제"""
if self._connected:
try:
await self.database.disconnect()
self._connected = False
logger.info("데이터베이스 연결 해제")
except Exception as e:
logger.error(f"데이터베이스 연결 해제 실패: {str(e)}")
async def test_connection(self) -> bool:
"""데이터베이스 연결 테스트"""
try:
if not self._connected:
await self.connect()
result = await self.database.fetch_val(BaseQueries.CONNECTION_TEST)
return result == 1
except Exception as e:
logger.error(f"데이터베이스 연결 테스트 실패: {str(e)}")
return False
async def execute_query(self, query: str, values: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
"""쿼리 실행 (SELECT)"""
try:
if not self._connected:
await self.connect()
rows = await self.database.fetch_all(query, values or {})
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"쿼리 실행 실패: {str(e)}")
logger.error(f"쿼리: {query}")
logger.error(f"파라미터: {values}")
raise Exception(f"쿼리 실행 실패: {str(e)}")
async def execute_insert(self, query: str, values: Optional[Dict[str, Any]] = None) -> int:
"""INSERT 쿼리 실행"""
try:
if not self._connected:
await self.connect()
result = await self.database.execute(query, values or {})
return result
except Exception as e:
logger.error(f"INSERT 실행 실패: {str(e)}")
logger.error(f"쿼리: {query}")
logger.error(f"파라미터: {values}")
raise Exception(f"INSERT 실행 실패: {str(e)}")
# 전역 데이터베이스 인스턴스
db = SimpleDatabase()

37
app/utils/logger.py Normal file
View File

@ -0,0 +1,37 @@
# app/utils/logger.py
"""
HealthSync Motivator Batch 로깅 설정
"""
import logging
import sys
from datetime import datetime
from app.config.settings import settings
def setup_logger(name: str = "motivator_batch") -> logging.Logger:
"""로거 설정"""
logger = logging.getLogger(name)
# 이미 핸들러가 있으면 중복 추가 방지
if logger.handlers:
return logger
logger.setLevel(getattr(logging, settings.log_level.upper()))
# 콘솔 핸들러
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(getattr(logging, settings.log_level.upper()))
# 포맷터
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
return logger
# 전역 로거
batch_logger = setup_logger()

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic>=2.8.0
pydantic-settings>=2.4.0
python-dotenv==1.0.0
databases[postgresql]==0.8.0
anthropic>=0.40.0
asyncio-mqtt==0.13.0