aiteamutils 0.2.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- aiteamutils/__init__.py +60 -0
- aiteamutils/base_model.py +81 -0
- aiteamutils/base_repository.py +503 -0
- aiteamutils/base_service.py +668 -0
- aiteamutils/cache.py +48 -0
- aiteamutils/config.py +26 -0
- aiteamutils/database.py +823 -0
- aiteamutils/dependencies.py +158 -0
- aiteamutils/enums.py +23 -0
- aiteamutils/exceptions.py +333 -0
- aiteamutils/security.py +396 -0
- aiteamutils/validators.py +188 -0
- aiteamutils-0.2.0.dist-info/METADATA +72 -0
- aiteamutils-0.2.0.dist-info/RECORD +15 -0
- aiteamutils-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
from typing import Type, Dict, Tuple, Any
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
3
|
+
from fastapi import Depends, status
|
4
|
+
from fastapi.security import OAuth2PasswordBearer
|
5
|
+
from jose import JWTError, jwt
|
6
|
+
|
7
|
+
from .database import get_db, DatabaseService
|
8
|
+
from .exceptions import CustomException, ErrorCode
|
9
|
+
|
10
|
+
class Settings:
|
11
|
+
"""기본 설정 클래스"""
|
12
|
+
def __init__(self, jwt_secret: str, jwt_algorithm: str = "HS256"):
|
13
|
+
self.JWT_SECRET = jwt_secret
|
14
|
+
self.JWT_ALGORITHM = jwt_algorithm
|
15
|
+
|
16
|
+
_settings: Settings | None = None
|
17
|
+
|
18
|
+
def init_settings(jwt_secret: str, jwt_algorithm: str = "HS256"):
|
19
|
+
"""설정 초기화 함수
|
20
|
+
|
21
|
+
Args:
|
22
|
+
jwt_secret (str): JWT 시크릿 키
|
23
|
+
jwt_algorithm (str, optional): JWT 알고리즘. Defaults to "HS256".
|
24
|
+
"""
|
25
|
+
global _settings
|
26
|
+
_settings = Settings(jwt_secret, jwt_algorithm)
|
27
|
+
|
28
|
+
def get_settings() -> Settings:
|
29
|
+
"""현재 설정을 반환하는 함수
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
Settings: 설정 객체
|
33
|
+
|
34
|
+
Raises:
|
35
|
+
RuntimeError: 설정이 초기화되지 않은 경우
|
36
|
+
"""
|
37
|
+
if _settings is None:
|
38
|
+
raise RuntimeError("Settings not initialized. Call init_settings first.")
|
39
|
+
return _settings
|
40
|
+
|
41
|
+
class ServiceRegistry:
|
42
|
+
"""서비스 레지스트리를 관리하는 클래스"""
|
43
|
+
def __init__(self):
|
44
|
+
self._services: Dict[str, Tuple[Type, Type]] = {}
|
45
|
+
|
46
|
+
def clear(self):
|
47
|
+
"""등록된 모든 서비스를 초기화합니다."""
|
48
|
+
self._services.clear()
|
49
|
+
|
50
|
+
def register(self, name: str, repository_class: Type, service_class: Type):
|
51
|
+
"""서비스를 레지스트리에 등록
|
52
|
+
|
53
|
+
Args:
|
54
|
+
name (str): 서비스 이름
|
55
|
+
repository_class (Type): Repository 클래스
|
56
|
+
service_class (Type): Service 클래스
|
57
|
+
|
58
|
+
Raises:
|
59
|
+
ValueError: 이미 등록된 서비스인 경우
|
60
|
+
"""
|
61
|
+
if name in self._services:
|
62
|
+
raise ValueError(f"Service '{name}' is already registered.")
|
63
|
+
self._services[name] = (repository_class, service_class)
|
64
|
+
|
65
|
+
def get(self, name: str) -> Tuple[Type, Type]:
|
66
|
+
"""등록된 서비스를 조회
|
67
|
+
|
68
|
+
Args:
|
69
|
+
name (str): 서비스 이름
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
Tuple[Type, Type]: (Repository 클래스, Service 클래스) 튜플
|
73
|
+
|
74
|
+
Raises:
|
75
|
+
ValueError: 등록되지 않은 서비스인 경우
|
76
|
+
"""
|
77
|
+
if name not in self._services:
|
78
|
+
raise ValueError(f"Service '{name}' is not registered.")
|
79
|
+
return self._services[name]
|
80
|
+
|
81
|
+
# ServiceRegistry 초기화
|
82
|
+
service_registry = ServiceRegistry()
|
83
|
+
|
84
|
+
def get_database_service(db: AsyncSession = Depends(get_db)) -> DatabaseService:
|
85
|
+
"""DatabaseService 의존성
|
86
|
+
|
87
|
+
Args:
|
88
|
+
db (AsyncSession): 데이터베이스 세션
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
DatabaseService: DatabaseService 인스턴스
|
92
|
+
"""
|
93
|
+
return DatabaseService(db)
|
94
|
+
|
95
|
+
def get_service(name: str):
|
96
|
+
"""등록된 서비스를 가져오는 의존성 함수
|
97
|
+
|
98
|
+
Args:
|
99
|
+
name (str): 서비스 이름
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Callable: 서비스 인스턴스를 반환하는 의존성 함수
|
103
|
+
"""
|
104
|
+
def _get_service(db_service: DatabaseService = Depends(get_database_service)):
|
105
|
+
repository_class, service_class = service_registry.get(name)
|
106
|
+
repository = repository_class(db_service)
|
107
|
+
return service_class(repository, db_service)
|
108
|
+
return _get_service
|
109
|
+
|
110
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/token")
|
111
|
+
async def get_current_user(
|
112
|
+
token: str = Depends(oauth2_scheme),
|
113
|
+
db_service: DatabaseService = Depends(get_database_service)
|
114
|
+
):
|
115
|
+
"""현재 사용자를 가져오는 의존성 함수
|
116
|
+
|
117
|
+
Args:
|
118
|
+
token (str): OAuth2 토큰
|
119
|
+
db_service (DatabaseService): DatabaseService 객체
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
User: 현재 사용자
|
123
|
+
|
124
|
+
Raises:
|
125
|
+
CustomException: 인증 실패 시 예외
|
126
|
+
"""
|
127
|
+
try:
|
128
|
+
settings = get_settings()
|
129
|
+
payload = jwt.decode(
|
130
|
+
token,
|
131
|
+
settings.JWT_SECRET,
|
132
|
+
algorithms=[settings.JWT_ALGORITHM],
|
133
|
+
audience="ai-team"
|
134
|
+
)
|
135
|
+
user_ulid = payload.get("sub")
|
136
|
+
if not user_ulid:
|
137
|
+
raise CustomException(
|
138
|
+
ErrorCode.INVALID_TOKEN,
|
139
|
+
source_function="dependencies.py / get_current_user"
|
140
|
+
)
|
141
|
+
except JWTError:
|
142
|
+
raise CustomException(
|
143
|
+
ErrorCode.INVALID_TOKEN,
|
144
|
+
detail=token[:10] + "...",
|
145
|
+
source_function="dependencies.py / get_current_user"
|
146
|
+
)
|
147
|
+
|
148
|
+
from app.user.repository import UserRepository
|
149
|
+
user_repo = UserRepository(db_service)
|
150
|
+
user = await user_repo.get_user(user_ulid, by="ulid")
|
151
|
+
|
152
|
+
if not user:
|
153
|
+
raise CustomException(
|
154
|
+
ErrorCode.USER_NOT_FOUND,
|
155
|
+
source_function="dependencies.py / get_current_user"
|
156
|
+
)
|
157
|
+
|
158
|
+
return user
|
aiteamutils/enums.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
"""시스템 전체에서 사용되는 열거형 정의."""
|
2
|
+
from enum import Enum
|
3
|
+
|
4
|
+
class UserStatus(str, Enum):
|
5
|
+
"""사용자 상태."""
|
6
|
+
PENDING = "PENDING"
|
7
|
+
ACTIVE = "ACTIVE"
|
8
|
+
INACTIVE = "INACTIVE"
|
9
|
+
BLOCKED = "BLOCKED"
|
10
|
+
|
11
|
+
class ActivityType(str, Enum):
|
12
|
+
"""시스템 활동 유형."""
|
13
|
+
# 인증 관련
|
14
|
+
ACCESS_TOKEN_ISSUED = "ACCESS_TOKEN_ISSUED"
|
15
|
+
REFRESH_TOKEN_ISSUED = "REFRESH_TOKEN_ISSUED"
|
16
|
+
LOGIN = "LOGIN"
|
17
|
+
LOGOUT = "LOGOUT"
|
18
|
+
|
19
|
+
# 사용자 관련
|
20
|
+
USER_CREATED = "USER_CREATED"
|
21
|
+
USER_UPDATED = "USER_UPDATED"
|
22
|
+
USER_DELETED = "USER_DELETED"
|
23
|
+
PASSWORD_CHANGE = "PASSWORD_CHANGE"
|
@@ -0,0 +1,333 @@
|
|
1
|
+
"""예외 처리 모듈."""
|
2
|
+
import logging
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Dict, Any, Optional, Tuple, List
|
5
|
+
from fastapi import Request
|
6
|
+
from fastapi.responses import JSONResponse
|
7
|
+
from fastapi.exceptions import RequestValidationError
|
8
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, NoResultFound, MultipleResultsFound, ProgrammingError, OperationalError
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
class ErrorResponse:
|
13
|
+
def __init__(self, code: int, value: str, status_code: int, detail_template: Optional[str] = None):
|
14
|
+
self.code = code
|
15
|
+
self.value = value
|
16
|
+
self.status_code = status_code
|
17
|
+
self.detail_template = detail_template
|
18
|
+
|
19
|
+
class ErrorCode(Enum):
|
20
|
+
# Auth 관련 에러: 1000번대
|
21
|
+
INVALID_CREDENTIALS = ErrorResponse(1001, "AUTH_INVALID_CREDENTIALS", 401, "잘못된 인증 정보입니다")
|
22
|
+
TOKEN_EXPIRED = ErrorResponse(1002, "AUTH_TOKEN_EXPIRED", 401, "토큰이 만료되었습니다")
|
23
|
+
INVALID_TOKEN = ErrorResponse(1003, "AUTH_INVALID_TOKEN", 401, "유효하지 않은 토큰입니다")
|
24
|
+
UNAUTHORIZED = ErrorResponse(1004, "AUTH_UNAUTHORIZED", 401, "인증이 필요합니다")
|
25
|
+
FORBIDDEN = ErrorResponse(1005, "AUTH_FORBIDDEN", 403, "접근 권한이 없습니다")
|
26
|
+
RATE_LIMIT_EXCEEDED = ErrorResponse(1006, "AUTH_RATE_LIMIT_EXCEEDED", 429, "너무 많은 요청이 발생했습니다")
|
27
|
+
INVALID_PASSWORD = ErrorResponse(1007, "AUTH_INVALID_PASSWORD", 401, "잘못된 비밀번호입니다")
|
28
|
+
|
29
|
+
# User 관련 에러: 2000번대
|
30
|
+
USER_NOT_FOUND = ErrorResponse(2001, "USER_NOT_FOUND", 404, "사용자를 찾을 수 없습니다")
|
31
|
+
USER_ALREADY_EXISTS = ErrorResponse(2002, "USER_ALREADY_EXISTS", 409, "이미 존재하는 사용자입니다")
|
32
|
+
INVALID_USER_DATA = ErrorResponse(2003, "USER_INVALID_DATA", 400, "잘못된 사용자 데이터입니다")
|
33
|
+
FIELD_INVALID_USERNAME = ErrorResponse(2005, "FIELD_INVALID_USERNAME", 400, "잘못된 사용자명입니다")
|
34
|
+
FIELD_INVALID_EMAIL = ErrorResponse(2006, "FIELD_INVALID_EMAIL", 400, "잘못된 이메일 형식입니다")
|
35
|
+
FIELD_INVALID_PHONE = ErrorResponse(2007, "FIELD_INVALID_PHONE", 400, "잘못된 전화번호 형식입니다")
|
36
|
+
|
37
|
+
# Password 관련 에러: 2100번대
|
38
|
+
FIELD_INVALID_PASSWORD_LENGTH = ErrorResponse(2101, "FIELD_INVALID_PASSWORD_LENGTH", 400, "비밀번호는 최소 8자 이상이어야 합니다")
|
39
|
+
FIELD_INVALID_PASSWORD_UPPER = ErrorResponse(2102, "FIELD_INVALID_PASSWORD_UPPER", 400, "비밀번호는 최소 1개의 대문자를 포함해야 합니다")
|
40
|
+
FIELD_INVALID_PASSWORD_NUMBER = ErrorResponse(2103, "FIELD_INVALID_PASSWORD_NUMBER", 400, "비밀번호는 최소 1개의 숫자를 포함해야 합니다")
|
41
|
+
FIELD_INVALID_PASSWORD_SPECIAL = ErrorResponse(2104, "FIELD_INVALID_PASSWORD_SPECIAL", 400, "비밀번호는 최소 1개의 특수문자를 포함해야 합니다")
|
42
|
+
FIELD_INVALID_PASSWORD_MISMATCH = ErrorResponse(2105, "FIELD_INVALID_PASSWORD_MISMATCH", 400, "새 비밀번호와 확인 비밀번호가 일치하지 않습니다")
|
43
|
+
|
44
|
+
# ID 형식 관련 에러: 2200번대
|
45
|
+
FIELD_INVALID_ID_LENGTH = ErrorResponse(2201, "FIELD_INVALID_ID_LENGTH", 400, "ID는 최소 3자 이상이어야 합니다")
|
46
|
+
FIELD_INVALID_ID_CHARS = ErrorResponse(2202, "FIELD_INVALID_ID_CHARS", 400, "ID는 영문 소문자, 숫자, 언더스코어만 사용할 수 있습니다")
|
47
|
+
|
48
|
+
# Database 관련 에러: 3000번대 세분화
|
49
|
+
DB_CONNECTION_ERROR = ErrorResponse(3001, "DB_CONNECTION_ERROR", 500, "데이터베이스 연결 오류")
|
50
|
+
DB_QUERY_ERROR = ErrorResponse(3002, "DB_QUERY_ERROR", 500, "데이터베이스 쿼리 오류")
|
51
|
+
DUPLICATE_ERROR = ErrorResponse(3003, "DB_DUPLICATE_ERROR", 409, "중복된 데이터가 존재합니다")
|
52
|
+
FOREIGN_KEY_VIOLATION = ErrorResponse(3004, "DB_FOREIGN_KEY_VIOLATION", 400, "참조하는 데이터가 존재하지 않습니다")
|
53
|
+
TRANSACTION_ERROR = ErrorResponse(3005, "DB_TRANSACTION_ERROR", 500, "트랜잭션 처리 중 오류가 발생했습니다")
|
54
|
+
DB_READ_ERROR = ErrorResponse(3006, "DB_READ_ERROR", 500, "데이터베이스 읽기 오류가 발생했습니다")
|
55
|
+
DB_CREATE_ERROR = ErrorResponse(3007, "DB_CREATE_ERROR", 500, "데이터베이스 생성 오류가 발생했습니다")
|
56
|
+
DB_UPDATE_ERROR = ErrorResponse(3008, "DB_UPDATE_ERROR", 500, "데이터베이스 업데이트 오류가 발생했습니다")
|
57
|
+
DB_DELETE_ERROR = ErrorResponse(3009, "DB_DELETE_ERROR", 500, "데이터베이스 삭제 오류가 발생했습니다")
|
58
|
+
DB_MULTIPLE_RESULTS = ErrorResponse(3010, "DB_MULTIPLE_RESULTS", 500, "중복된 데이터가 조회되었습니다")
|
59
|
+
DB_NO_RESULT = ErrorResponse(3011, "DB_NO_RESULT", 404, "데이터를 찾을 수 없습니다")
|
60
|
+
DB_INVALID_QUERY = ErrorResponse(3012, "DB_INVALID_QUERY", 500, "잘못된 쿼리 구문입니다")
|
61
|
+
DB_OPERATIONAL_ERROR = ErrorResponse(3013, "DB_OPERATIONAL_ERROR", 500, "데이터베이스 작업 중 오류가 발생했습니다")
|
62
|
+
|
63
|
+
# Validation 관련 에러: 4000번대
|
64
|
+
VALIDATION_ERROR = ErrorResponse(4001, "VALIDATION_ERROR", 422, "유효성 검사 오류")
|
65
|
+
FIELD_INVALID_FORMAT = ErrorResponse(4002, "FIELD_INVALID_FORMAT", 400, "잘못된 형식입니다")
|
66
|
+
REQUIRED_FIELD_MISSING = ErrorResponse(4003, "VALIDATION_REQUIRED_FIELD_MISSING", 400, "필수 필드가 누락되었습니다")
|
67
|
+
|
68
|
+
# General 에러: 5000번대
|
69
|
+
NOT_FOUND = ErrorResponse(5001, "GENERAL_NOT_FOUND", 404, "리소스를 찾을 수 없습니다")
|
70
|
+
INTERNAL_ERROR = ErrorResponse(5002, "GENERAL_INTERNAL_ERROR", 500, "내부 서버 오류")
|
71
|
+
SERVICE_UNAVAILABLE = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
|
72
|
+
|
73
|
+
class CustomException(Exception):
|
74
|
+
"""사용자 정의 예외 클래스"""
|
75
|
+
|
76
|
+
def __init__(
|
77
|
+
self,
|
78
|
+
error_code: ErrorCode,
|
79
|
+
detail: str,
|
80
|
+
source_function: str,
|
81
|
+
original_error: Optional[Exception] = None,
|
82
|
+
parent_source_function: Optional[str] = None
|
83
|
+
):
|
84
|
+
self.error_code = error_code
|
85
|
+
self.detail = detail
|
86
|
+
self.source_function = source_function
|
87
|
+
self.original_error = original_error
|
88
|
+
|
89
|
+
# 상위 함수 경로 연결
|
90
|
+
if parent_source_function:
|
91
|
+
self.source_function = f"{parent_source_function}/{self.source_function}"
|
92
|
+
|
93
|
+
# 에러 체인 구성
|
94
|
+
self.error_chain = []
|
95
|
+
if isinstance(original_error, CustomException):
|
96
|
+
self.error_chain = original_error.error_chain.copy()
|
97
|
+
|
98
|
+
self.error_chain.append({
|
99
|
+
"error_code": str(self.error_code),
|
100
|
+
"detail": str(self.detail),
|
101
|
+
"source_function": self.source_function,
|
102
|
+
"original_error": str(self.original_error) if self.original_error else None
|
103
|
+
})
|
104
|
+
|
105
|
+
def get_error_chain(self) -> List[Dict[str, Any]]:
|
106
|
+
"""에러 발생 경로 상세 정보를 반환합니다."""
|
107
|
+
return self.error_chain
|
108
|
+
|
109
|
+
def get_original_error(self) -> Optional[Exception]:
|
110
|
+
"""원본 에러를 반환합니다."""
|
111
|
+
return self.original_error
|
112
|
+
|
113
|
+
def to_dict(self) -> Dict[str, Any]:
|
114
|
+
"""에러 정보를 딕셔너리로 반환합니다."""
|
115
|
+
return {
|
116
|
+
"error_code": self.error_code.name,
|
117
|
+
"detail": self.detail,
|
118
|
+
"source_function": self.source_function,
|
119
|
+
"error_chain": self.error_chain,
|
120
|
+
"original_error": str(self.original_error) if self.original_error else None
|
121
|
+
}
|
122
|
+
|
123
|
+
def get_error_details(request: Request, exc: CustomException) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
124
|
+
"""에러 상세 정보를 생성합니다.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
request (Request): FastAPI 요청 객체
|
128
|
+
exc (CustomException): 발생한 예외
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Tuple[Dict[str, Any], Dict[str, Any]]: (로그 데이터, 응답 데이터)
|
132
|
+
"""
|
133
|
+
# 공통 데이터
|
134
|
+
base_data = {
|
135
|
+
"error_code": exc.error_code.value.code,
|
136
|
+
"error_type": exc.error_code.name,
|
137
|
+
"status_code": exc.error_code.value.status_code,
|
138
|
+
"path": request.url.path,
|
139
|
+
"method": request.method,
|
140
|
+
"client_ip": request.client.host,
|
141
|
+
"user_agent": request.headers.get("user-agent"),
|
142
|
+
"detail": exc.detail,
|
143
|
+
}
|
144
|
+
|
145
|
+
# 로그 데이터에는 error_chain, source_function, detail 포함
|
146
|
+
log_data = {
|
147
|
+
**base_data,
|
148
|
+
"source_function": exc.source_function,
|
149
|
+
"error_chain": exc.error_chain,
|
150
|
+
"original_error": str(exc.original_error) if exc.original_error else None
|
151
|
+
}
|
152
|
+
|
153
|
+
# 응답 데이터에는 error_chain, source_function, detail 제외
|
154
|
+
response_data = base_data.copy()
|
155
|
+
|
156
|
+
return log_data, response_data
|
157
|
+
|
158
|
+
def log_error(log_data: Dict[str, Any]):
|
159
|
+
"""로그 데이터 기록 (다중 줄 포맷)"""
|
160
|
+
log_message = ["Request failed:"]
|
161
|
+
|
162
|
+
# 기본 에러 정보
|
163
|
+
basic_info = [
|
164
|
+
"error_code", "error_type", "status_code", "path",
|
165
|
+
"method", "client_ip", "user_agent"
|
166
|
+
]
|
167
|
+
for key in basic_info:
|
168
|
+
if key in log_data:
|
169
|
+
log_message.append(f" {key}: {log_data[key]}")
|
170
|
+
|
171
|
+
# 상세 에러 정보
|
172
|
+
if "detail" in log_data:
|
173
|
+
log_message.append(" detail:")
|
174
|
+
for line in str(log_data["detail"]).split("\n"):
|
175
|
+
log_message.append(f" {line}")
|
176
|
+
|
177
|
+
# 에러 체인 정보
|
178
|
+
if "error_chain" in log_data:
|
179
|
+
log_message.append(" error_chain:")
|
180
|
+
for error in log_data["error_chain"]:
|
181
|
+
log_message.append(" Error:")
|
182
|
+
for key, value in error.items():
|
183
|
+
if value:
|
184
|
+
if key == "detail":
|
185
|
+
log_message.append(" detail:")
|
186
|
+
for line in str(value).split("\n"):
|
187
|
+
log_message.append(f" {line}")
|
188
|
+
else:
|
189
|
+
log_message.append(f" {key}: {value}")
|
190
|
+
|
191
|
+
# 원본 에러 정보
|
192
|
+
if "original_error" in log_data and log_data["original_error"]:
|
193
|
+
log_message.append(" original_error:")
|
194
|
+
for line in str(log_data["original_error"]).split("\n"):
|
195
|
+
log_message.append(f" {line}")
|
196
|
+
|
197
|
+
logger.error("\n".join(log_message), extra=log_data)
|
198
|
+
|
199
|
+
async def custom_exception_handler(request: Request, exc: CustomException):
|
200
|
+
"""CustomException에 대한 기본 핸들러"""
|
201
|
+
# 로그 데이터와 응답 데이터 생성
|
202
|
+
log_data, response_data = get_error_details(
|
203
|
+
request=request,
|
204
|
+
exc=exc,
|
205
|
+
)
|
206
|
+
|
207
|
+
# 로그 기록
|
208
|
+
log_error(log_data)
|
209
|
+
|
210
|
+
# 클라이언트 응답 반환
|
211
|
+
return JSONResponse(
|
212
|
+
status_code=exc.error_code.value.status_code,
|
213
|
+
content=response_data,
|
214
|
+
)
|
215
|
+
|
216
|
+
async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
|
217
|
+
"""FastAPI의 RequestValidationError를 처리합니다."""
|
218
|
+
missing_fields = []
|
219
|
+
for error in exc.errors():
|
220
|
+
if error["type"] == "missing":
|
221
|
+
missing_fields.append(error["loc"][1])
|
222
|
+
|
223
|
+
error = CustomException(
|
224
|
+
ErrorCode.REQUIRED_FIELD_MISSING,
|
225
|
+
detail="|".join(missing_fields),
|
226
|
+
source_function="FastAPI.request_validation_handler",
|
227
|
+
original_error=exc
|
228
|
+
)
|
229
|
+
return await custom_exception_handler(request, error)
|
230
|
+
|
231
|
+
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
|
232
|
+
"""SQLAlchemy 관련 예외 처리"""
|
233
|
+
error_str = str(exc)
|
234
|
+
error_class = exc.__class__.__name__
|
235
|
+
error_module = exc.__class__.__module__
|
236
|
+
|
237
|
+
# 상세 에러 정보 구성
|
238
|
+
error_details = f"{error_module}.{error_class}: {error_str}"
|
239
|
+
|
240
|
+
# 스택 트레이스가 있다면 포함
|
241
|
+
if hasattr(exc, '__traceback__'):
|
242
|
+
import traceback
|
243
|
+
stack_trace = ''.join(traceback.format_tb(exc.__traceback__))
|
244
|
+
error_details = f"{error_details}\nStack trace:\n{stack_trace}"
|
245
|
+
|
246
|
+
# SQL 쿼리 정보 추가 (가능한 경우)
|
247
|
+
if hasattr(exc, 'statement'):
|
248
|
+
error_details = f"{error_details}\nFailed SQL Query: {exc.statement}"
|
249
|
+
if hasattr(exc, 'params'):
|
250
|
+
error_details = f"{error_details}\nQuery Parameters: {exc.params}"
|
251
|
+
|
252
|
+
error = None
|
253
|
+
|
254
|
+
# SQLAlchemy 예외 타입별 세분화된 처리
|
255
|
+
if isinstance(exc, IntegrityError):
|
256
|
+
if "violates foreign key constraint" in error_str:
|
257
|
+
error = CustomException(
|
258
|
+
ErrorCode.FOREIGN_KEY_VIOLATION,
|
259
|
+
detail=error_details,
|
260
|
+
source_function="DatabaseService.execute_operation",
|
261
|
+
original_error=exc
|
262
|
+
)
|
263
|
+
elif "duplicate key" in error_str:
|
264
|
+
error = CustomException(
|
265
|
+
ErrorCode.DUPLICATE_ERROR,
|
266
|
+
detail=error_details,
|
267
|
+
source_function="DatabaseService.execute_operation",
|
268
|
+
original_error=exc
|
269
|
+
)
|
270
|
+
else:
|
271
|
+
error = CustomException(
|
272
|
+
ErrorCode.DB_QUERY_ERROR,
|
273
|
+
detail=error_details,
|
274
|
+
source_function="DatabaseService.execute_operation",
|
275
|
+
original_error=exc
|
276
|
+
)
|
277
|
+
elif isinstance(exc, NoResultFound):
|
278
|
+
error = CustomException(
|
279
|
+
ErrorCode.DB_NO_RESULT,
|
280
|
+
detail=error_details,
|
281
|
+
source_function="DatabaseService.execute_operation",
|
282
|
+
original_error=exc
|
283
|
+
)
|
284
|
+
elif isinstance(exc, MultipleResultsFound):
|
285
|
+
error = CustomException(
|
286
|
+
ErrorCode.DB_MULTIPLE_RESULTS,
|
287
|
+
detail=error_details,
|
288
|
+
source_function="DatabaseService.execute_operation",
|
289
|
+
original_error=exc
|
290
|
+
)
|
291
|
+
elif isinstance(exc, ProgrammingError):
|
292
|
+
error = CustomException(
|
293
|
+
ErrorCode.DB_INVALID_QUERY,
|
294
|
+
detail=error_details,
|
295
|
+
source_function="DatabaseService.execute_operation",
|
296
|
+
original_error=exc
|
297
|
+
)
|
298
|
+
elif isinstance(exc, OperationalError):
|
299
|
+
error = CustomException(
|
300
|
+
ErrorCode.DB_OPERATIONAL_ERROR,
|
301
|
+
detail=error_details,
|
302
|
+
source_function="DatabaseService.execute_operation",
|
303
|
+
original_error=exc
|
304
|
+
)
|
305
|
+
else:
|
306
|
+
error = CustomException(
|
307
|
+
ErrorCode.DB_QUERY_ERROR,
|
308
|
+
detail=error_details,
|
309
|
+
source_function="DatabaseService.execute_operation",
|
310
|
+
original_error=exc
|
311
|
+
)
|
312
|
+
|
313
|
+
# 로그에 추가 정보 기록
|
314
|
+
logger.error(
|
315
|
+
"Database Error Details:\n"
|
316
|
+
f"Error Type: {error_class}\n"
|
317
|
+
f"Error Module: {error_module}\n"
|
318
|
+
f"Error Message: {error_str}\n"
|
319
|
+
f"Request Path: {request.url.path}\n"
|
320
|
+
f"Request Method: {request.method}\n"
|
321
|
+
f"Client IP: {request.client.host}"
|
322
|
+
)
|
323
|
+
|
324
|
+
return await custom_exception_handler(request, error)
|
325
|
+
|
326
|
+
async def generic_exception_handler(request: Request, exc: Exception):
|
327
|
+
"""처리되지 않은 예외를 위한 기본 핸들러"""
|
328
|
+
error = CustomException(
|
329
|
+
ErrorCode.INTERNAL_ERROR,
|
330
|
+
detail=str(exc),
|
331
|
+
source_function="GenericExceptionHandler.handle_exception"
|
332
|
+
)
|
333
|
+
return await custom_exception_handler(request, error)
|