aiteamutils 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|