""" 사용자 활동 로그 서비스 모든 업무 활동을 추적하고 기록하는 서비스 """ from sqlalchemy.orm import Session from sqlalchemy import text from typing import Optional, Dict, Any from fastapi import Request import json from datetime import datetime from ..utils.logger import get_logger logger = get_logger(__name__) class ActivityLogger: """사용자 활동 로그 관리 클래스""" def __init__(self, db: Session): self.db = db def log_activity( self, username: str, activity_type: str, activity_description: str, target_id: Optional[int] = None, target_type: Optional[str] = None, user_id: Optional[int] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None ) -> int: """ 사용자 활동 로그 기록 Args: username: 사용자명 (필수) activity_type: 활동 유형 (FILE_UPLOAD, PROJECT_CREATE 등) activity_description: 활동 설명 target_id: 대상 ID (파일, 프로젝트 등) target_type: 대상 유형 (FILE, PROJECT 등) user_id: 사용자 ID ip_address: IP 주소 user_agent: 브라우저 정보 metadata: 추가 메타데이터 Returns: int: 생성된 로그 ID """ try: insert_query = text(""" INSERT INTO user_activity_logs ( user_id, username, activity_type, activity_description, target_id, target_type, ip_address, user_agent, metadata ) VALUES ( :user_id, :username, :activity_type, :activity_description, :target_id, :target_type, :ip_address, :user_agent, :metadata ) RETURNING id """) result = self.db.execute(insert_query, { 'user_id': user_id, 'username': username, 'activity_type': activity_type, 'activity_description': activity_description, 'target_id': target_id, 'target_type': target_type, 'ip_address': ip_address, 'user_agent': user_agent, 'metadata': json.dumps(metadata) if metadata else None }) log_id = result.fetchone()[0] self.db.commit() logger.info(f"Activity logged: {username} - {activity_type} - {activity_description}") return log_id except Exception as e: logger.error(f"Failed to log activity: {str(e)}") self.db.rollback() raise def log_file_upload( self, username: str, file_id: int, filename: str, file_size: int, job_no: str, revision: str, user_id: Optional[int] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None ) -> int: """파일 업로드 활동 로그""" metadata = { 'filename': filename, 'file_size': file_size, 'job_no': job_no, 'revision': revision, 'upload_time': datetime.now().isoformat() } return self.log_activity( username=username, activity_type='FILE_UPLOAD', activity_description=f'BOM 파일 업로드: {filename} (Job: {job_no}, Rev: {revision})', target_id=file_id, target_type='FILE', user_id=user_id, ip_address=ip_address, user_agent=user_agent, metadata=metadata ) def log_project_create( self, username: str, project_id: int, project_name: str, job_no: str, user_id: Optional[int] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None ) -> int: """프로젝트 생성 활동 로그""" metadata = { 'project_name': project_name, 'job_no': job_no, 'create_time': datetime.now().isoformat() } return self.log_activity( username=username, activity_type='PROJECT_CREATE', activity_description=f'프로젝트 생성: {project_name} ({job_no})', target_id=project_id, target_type='PROJECT', user_id=user_id, ip_address=ip_address, user_agent=user_agent, metadata=metadata ) def log_material_classify( self, username: str, file_id: int, classified_count: int, job_no: str, revision: str, user_id: Optional[int] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None ) -> int: """자재 분류 활동 로그""" metadata = { 'classified_count': classified_count, 'job_no': job_no, 'revision': revision, 'classify_time': datetime.now().isoformat() } return self.log_activity( username=username, activity_type='MATERIAL_CLASSIFY', activity_description=f'자재 분류 완료: {classified_count}개 자재 (Job: {job_no}, Rev: {revision})', target_id=file_id, target_type='FILE', user_id=user_id, ip_address=ip_address, user_agent=user_agent, metadata=metadata ) def log_purchase_confirm( self, username: str, job_no: str, revision: str, confirmed_count: int, total_amount: Optional[float] = None, user_id: Optional[int] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None ) -> int: """구매 확정 활동 로그""" metadata = { 'job_no': job_no, 'revision': revision, 'confirmed_count': confirmed_count, 'total_amount': total_amount, 'confirm_time': datetime.now().isoformat() } return self.log_activity( username=username, activity_type='PURCHASE_CONFIRM', activity_description=f'구매 확정: {confirmed_count}개 품목 (Job: {job_no}, Rev: {revision})', target_id=None, # 구매는 특정 ID가 없음 target_type='PURCHASE', user_id=user_id, ip_address=ip_address, user_agent=user_agent, metadata=metadata ) def get_user_activities( self, username: str, activity_type: Optional[str] = None, limit: int = 50, offset: int = 0 ) -> list: """사용자 활동 이력 조회""" try: where_clause = "WHERE username = :username" params = {'username': username} if activity_type: where_clause += " AND activity_type = :activity_type" params['activity_type'] = activity_type query = text(f""" SELECT id, activity_type, activity_description, target_id, target_type, metadata, created_at FROM user_activity_logs {where_clause} ORDER BY created_at DESC LIMIT :limit OFFSET :offset """) params.update({'limit': limit, 'offset': offset}) result = self.db.execute(query, params) activities = [] for row in result.fetchall(): activity = { 'id': row[0], 'activity_type': row[1], 'activity_description': row[2], 'target_id': row[3], 'target_type': row[4], 'metadata': json.loads(row[5]) if row[5] else {}, 'created_at': row[6].isoformat() if row[6] else None } activities.append(activity) return activities except Exception as e: logger.error(f"Failed to get user activities: {str(e)}") return [] def get_recent_activities( self, days: int = 7, limit: int = 100 ) -> list: """최근 활동 조회 (전체 사용자)""" try: query = text(""" SELECT username, activity_type, activity_description, target_id, target_type, created_at FROM user_activity_logs WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '%s days' ORDER BY created_at DESC LIMIT :limit """ % days) result = self.db.execute(query, {'limit': limit}) activities = [] for row in result.fetchall(): activity = { 'username': row[0], 'activity_type': row[1], 'activity_description': row[2], 'target_id': row[3], 'target_type': row[4], 'created_at': row[5].isoformat() if row[5] else None } activities.append(activity) return activities except Exception as e: logger.error(f"Failed to get recent activities: {str(e)}") return [] def get_client_info(request: Request) -> tuple: """ 요청에서 클라이언트 정보 추출 Args: request: FastAPI Request 객체 Returns: tuple: (ip_address, user_agent) """ # IP 주소 추출 (프록시 고려) ip_address = ( request.headers.get('x-forwarded-for', '').split(',')[0].strip() or request.headers.get('x-real-ip', '') or request.client.host if request.client else 'unknown' ) # User-Agent 추출 user_agent = request.headers.get('user-agent', 'unknown') return ip_address, user_agent def log_activity_from_request( db: Session, request: Request, username: str, activity_type: str, activity_description: str, target_id: Optional[int] = None, target_type: Optional[str] = None, user_id: Optional[int] = None, metadata: Optional[Dict[str, Any]] = None ) -> int: """ 요청 정보를 포함한 활동 로그 기록 (편의 함수) Args: db: 데이터베이스 세션 request: FastAPI Request 객체 username: 사용자명 activity_type: 활동 유형 activity_description: 활동 설명 target_id: 대상 ID target_type: 대상 유형 user_id: 사용자 ID metadata: 추가 메타데이터 Returns: int: 생성된 로그 ID """ ip_address, user_agent = get_client_info(request) activity_logger = ActivityLogger(db) return activity_logger.log_activity( username=username, activity_type=activity_type, activity_description=activity_description, target_id=target_id, target_type=target_type, user_id=user_id, ip_address=ip_address, user_agent=user_agent, metadata=metadata )