Files
TK-BOM-Project/backend/app/services/activity_logger.py
Hyungi Ahn 83b90ef05c
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 자재 관리 페이지 대규모 개선
- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산)
- UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인)
- 자재별 그룹핑 로직 개선:
  * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시
  * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등)
  * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시
  * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계)
  * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시
  * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑
- 전체 카테고리 버튼 제거 (표시 복잡도 감소)
- 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
2025-09-09 09:24:45 +09:00

363 lines
11 KiB
Python

"""
사용자 활동 로그 서비스
모든 업무 활동을 추적하고 기록하는 서비스
"""
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
)