318 lines
9.7 KiB
Python
318 lines
9.7 KiB
Python
"""
|
|
접속 로그 기록을 위한 유틸리티 함수들
|
|
"""
|
|
from django.contrib.auth.models import User
|
|
from .models import AccessLog, Person
|
|
|
|
|
|
def get_client_ip(request):
|
|
"""
|
|
클라이언트의 실제 IP 주소를 추출
|
|
프록시, 로드밸런서, CDN 등을 고려하여 실제 IP를 찾음
|
|
"""
|
|
# 프록시나 로드밸런서를 통한 실제 클라이언트 IP 확인
|
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
if x_forwarded_for:
|
|
# 첫 번째 IP가 실제 클라이언트 IP (쉼표로 구분된 경우)
|
|
ip = x_forwarded_for.split(',')[0].strip()
|
|
if ip and ip != '127.0.0.1':
|
|
return ip
|
|
|
|
# Cloudflare 등 CDN에서 사용하는 헤더
|
|
cf_connecting_ip = request.META.get('HTTP_CF_CONNECTING_IP')
|
|
if cf_connecting_ip and cf_connecting_ip != '127.0.0.1':
|
|
return cf_connecting_ip
|
|
|
|
# Nginx 등 리버스 프록시에서 사용
|
|
x_real_ip = request.META.get('HTTP_X_REAL_IP')
|
|
if x_real_ip and x_real_ip != '127.0.0.1':
|
|
return x_real_ip
|
|
|
|
# 기본 REMOTE_ADDR
|
|
remote_addr = request.META.get('REMOTE_ADDR', '알 수 없음')
|
|
|
|
# 로컬 환경 표시
|
|
if remote_addr == '127.0.0.1':
|
|
return f"{remote_addr} (로컬 개발환경)"
|
|
elif remote_addr.startswith('192.168.') or remote_addr.startswith('10.') or remote_addr.startswith('172.'):
|
|
return f"{remote_addr} (내부 네트워크)"
|
|
|
|
return remote_addr
|
|
|
|
|
|
def get_user_agent(request):
|
|
"""사용자 에이전트 가져오기"""
|
|
return request.META.get('HTTP_USER_AGENT', '')
|
|
|
|
|
|
def get_device_info(user_agent):
|
|
"""User-Agent에서 기기 정보 추출"""
|
|
if not user_agent:
|
|
return "알 수 없음"
|
|
|
|
# 모바일 기기 체크
|
|
if any(mobile in user_agent for mobile in ['iPhone', 'iPad', 'Android', 'Mobile']):
|
|
if 'iPhone' in user_agent:
|
|
return "iPhone"
|
|
elif 'iPad' in user_agent:
|
|
return "iPad"
|
|
elif 'Android' in user_agent:
|
|
if 'Mobile' in user_agent:
|
|
return "Android 폰"
|
|
else:
|
|
return "Android 태블릿"
|
|
else:
|
|
return "모바일"
|
|
|
|
# 데스크톱 OS 체크
|
|
if 'Windows NT' in user_agent:
|
|
if 'Windows NT 10.0' in user_agent:
|
|
return "Windows 10/11"
|
|
elif 'Windows NT 6.3' in user_agent:
|
|
return "Windows 8.1"
|
|
elif 'Windows NT 6.1' in user_agent:
|
|
return "Windows 7"
|
|
else:
|
|
return "Windows"
|
|
elif 'Macintosh' in user_agent or 'Mac OS X' in user_agent:
|
|
return "Mac"
|
|
elif 'Linux' in user_agent and 'Android' not in user_agent:
|
|
return "Linux"
|
|
|
|
return "알 수 없음"
|
|
|
|
|
|
def get_browser_info(user_agent):
|
|
"""User-Agent에서 브라우저 정보 추출"""
|
|
if not user_agent:
|
|
return "알 수 없음"
|
|
|
|
# 브라우저 체크 (순서 중요 - Chrome이 Safari 문자열도 포함하므로)
|
|
if 'Edg/' in user_agent:
|
|
return "Microsoft Edge"
|
|
elif 'Chrome/' in user_agent and 'Safari/' in user_agent:
|
|
return "Chrome"
|
|
elif 'Firefox/' in user_agent:
|
|
return "Firefox"
|
|
elif 'Safari/' in user_agent and 'Chrome' not in user_agent:
|
|
return "Safari"
|
|
elif 'Opera' in user_agent or 'OPR/' in user_agent:
|
|
return "Opera"
|
|
|
|
return "알 수 없음"
|
|
|
|
|
|
def log_user_activity(request, action, description=None, user=None, metadata=None):
|
|
"""
|
|
사용자 활동 로그 기록
|
|
|
|
Args:
|
|
request: Django request 객체
|
|
action: 활동 유형 (AccessLog.ACTION_CHOICES 중 하나)
|
|
description: 상세 설명 (선택사항)
|
|
user: 사용자 객체 (선택사항, 없으면 request.user 사용)
|
|
metadata: 추가 정보 딕셔너리 (선택사항)
|
|
"""
|
|
try:
|
|
# 사용자 정보 가져오기
|
|
if user is None:
|
|
user = request.user if request.user.is_authenticated else None
|
|
|
|
# Person 객체 가져오기
|
|
person = None
|
|
if user:
|
|
try:
|
|
person = Person.objects.get(user=user)
|
|
except Person.DoesNotExist:
|
|
pass
|
|
|
|
# 메타데이터 기본값 설정
|
|
if metadata is None:
|
|
metadata = {}
|
|
|
|
# 요청 정보 추가
|
|
user_agent = get_user_agent(request)
|
|
metadata.update({
|
|
'path': request.path,
|
|
'method': request.method,
|
|
'referer': request.META.get('HTTP_REFERER', ''),
|
|
'device_info': get_device_info(user_agent),
|
|
'browser_info': get_browser_info(user_agent),
|
|
})
|
|
|
|
# 로그 생성
|
|
AccessLog.objects.create(
|
|
user=user,
|
|
person=person,
|
|
action=action,
|
|
description=description,
|
|
ip_address=get_client_ip(request),
|
|
user_agent=get_user_agent(request),
|
|
session_key=request.session.session_key,
|
|
metadata=metadata
|
|
)
|
|
|
|
print(f"[ACCESS_LOG] {action}: {user.username if user else 'Anonymous'} - {description}")
|
|
|
|
except Exception as e:
|
|
print(f"[ACCESS_LOG_ERROR] 로그 기록 실패: {e}")
|
|
|
|
|
|
def log_login(request, user):
|
|
"""로그인 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='LOGIN',
|
|
description=f'사용자 로그인: {user.username}',
|
|
user=user
|
|
)
|
|
|
|
|
|
def log_logout(request, user):
|
|
"""로그아웃 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='LOGOUT',
|
|
description=f'사용자 로그아웃: {user.username}',
|
|
user=user
|
|
)
|
|
|
|
|
|
def log_signup(request, user):
|
|
"""회원가입 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='SIGNUP',
|
|
description=f'새 회원가입: {user.username}',
|
|
user=user
|
|
)
|
|
|
|
|
|
def log_profile_update(request, user, updated_fields=None, field_changes=None):
|
|
"""프로필 수정 로그 기록"""
|
|
description = f'프로필 수정: {user.username}'
|
|
if updated_fields:
|
|
description += f' (수정된 필드: {", ".join(updated_fields)})'
|
|
|
|
metadata = {}
|
|
if updated_fields:
|
|
metadata['updated_fields'] = updated_fields
|
|
|
|
# 필드별 수정 전/후 값 기록
|
|
if field_changes:
|
|
metadata['field_changes'] = field_changes
|
|
# 상세 설명에 변경사항 추가
|
|
change_details = []
|
|
for field_name, changes in field_changes.items():
|
|
old_value = changes.get('old', '')
|
|
new_value = changes.get('new', '')
|
|
# 값이 너무 길면 자르기
|
|
if len(str(old_value)) > 50:
|
|
old_value = str(old_value)[:50] + '...'
|
|
if len(str(new_value)) > 50:
|
|
new_value = str(new_value)[:50] + '...'
|
|
change_details.append(f"{field_name}: '{old_value}' → '{new_value}'")
|
|
|
|
if change_details:
|
|
description += f' | 변경사항: {" | ".join(change_details)}'
|
|
|
|
log_user_activity(
|
|
request=request,
|
|
action='PROFILE_UPDATE',
|
|
description=description,
|
|
user=user,
|
|
metadata=metadata
|
|
)
|
|
|
|
|
|
def log_password_change(request, user):
|
|
"""비밀번호 변경 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='PASSWORD_CHANGE',
|
|
description=f'비밀번호 변경: {user.username}',
|
|
user=user
|
|
)
|
|
|
|
|
|
def log_phone_verification(request, phone_number, user=None):
|
|
"""전화번호 인증 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='PHONE_VERIFICATION',
|
|
description=f'전화번호 인증: {phone_number}',
|
|
user=user,
|
|
metadata={'phone_number': phone_number}
|
|
)
|
|
|
|
|
|
def log_search(request, query, result_count=None):
|
|
"""검색 로그 기록"""
|
|
description = f'검색 쿼리: {query}'
|
|
if result_count is not None:
|
|
description += f' (결과: {result_count}개)'
|
|
|
|
metadata = {'query': query}
|
|
if result_count is not None:
|
|
metadata['result_count'] = result_count
|
|
|
|
log_user_activity(
|
|
request=request,
|
|
action='SEARCH',
|
|
description=description,
|
|
metadata=metadata
|
|
)
|
|
|
|
|
|
def log_main_access(request):
|
|
"""메인페이지 접속 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='MAIN_ACCESS',
|
|
description='메인페이지 접속'
|
|
)
|
|
|
|
|
|
def log_error(request, error_message, error_type=None):
|
|
"""에러 로그 기록"""
|
|
metadata = {'error_message': error_message}
|
|
if error_type:
|
|
metadata['error_type'] = error_type
|
|
|
|
log_user_activity(
|
|
request=request,
|
|
action='ERROR',
|
|
description=f'에러 발생: {error_message}',
|
|
metadata=metadata
|
|
)
|
|
|
|
|
|
def log_withdrawal_request(request, user, withdrawal_request_id):
|
|
"""회원탈퇴 요청 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='OTHER',
|
|
description=f'회원탈퇴 요청 제출: {user.username}',
|
|
user=user,
|
|
metadata={
|
|
'withdrawal_request_id': withdrawal_request_id,
|
|
'action_type': 'withdrawal_request'
|
|
}
|
|
)
|
|
|
|
|
|
def log_withdrawal_approval(request, approved_by, withdrawn_user, withdrawn_person, withdrawal_request_id):
|
|
"""회원탈퇴 승인 로그 기록"""
|
|
log_user_activity(
|
|
request=request,
|
|
action='OTHER',
|
|
description=f'회원탈퇴 승인 처리: {withdrawn_user} ({withdrawn_person})',
|
|
user=approved_by,
|
|
metadata={
|
|
'withdrawal_request_id': withdrawal_request_id,
|
|
'withdrawn_user': withdrawn_user,
|
|
'withdrawn_person': withdrawn_person,
|
|
'action_type': 'withdrawal_approval'
|
|
}
|
|
)
|