aiteamutils 0.2.51__tar.gz → 0.2.52__tar.gz
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-0.2.51 → aiteamutils-0.2.52}/PKG-INFO +1 -1
- aiteamutils-0.2.52/aiteamutils/base_repository.py +208 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/base_service.py +46 -43
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/database.py +24 -5
- aiteamutils-0.2.52/aiteamutils/dependencies.py +213 -0
- aiteamutils-0.2.52/aiteamutils/security.py +242 -0
- aiteamutils-0.2.52/aiteamutils/version.py +2 -0
- aiteamutils-0.2.51/aiteamutils/base_repository.py +0 -504
- aiteamutils-0.2.51/aiteamutils/dependencies.py +0 -135
- aiteamutils-0.2.51/aiteamutils/security.py +0 -479
- aiteamutils-0.2.51/aiteamutils/version.py +0 -2
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/.cursorrules +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/.gitignore +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/README.md +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/__init__.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/base_model.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/cache.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/config.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/enums.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/exceptions.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/aiteamutils/validators.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/pyproject.toml +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.52}/setup.py +0 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
"""기본 레포지토리 모듈."""
|
2
|
+
from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union
|
3
|
+
from sqlalchemy.orm import DeclarativeBase, Load
|
4
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
5
|
+
from sqlalchemy import select, or_, and_
|
6
|
+
from .exceptions import CustomException, ErrorCode
|
7
|
+
from sqlalchemy.orm import joinedload
|
8
|
+
from sqlalchemy.sql import Select
|
9
|
+
from fastapi import Request
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
11
|
+
|
12
|
+
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
13
|
+
|
14
|
+
class BaseRepository(Generic[ModelType]):
|
15
|
+
##################
|
16
|
+
# 1. 초기화 영역 #
|
17
|
+
##################
|
18
|
+
def __init__(self, session: AsyncSession, model: Type[ModelType]):
|
19
|
+
"""
|
20
|
+
Args:
|
21
|
+
session (AsyncSession): 데이터베이스 세션
|
22
|
+
model (Type[ModelType]): 모델 클래스
|
23
|
+
"""
|
24
|
+
self.session = session
|
25
|
+
self.model = model
|
26
|
+
|
27
|
+
@property
|
28
|
+
def session(self):
|
29
|
+
"""현재 세션을 반환합니다."""
|
30
|
+
if self._session is None:
|
31
|
+
raise CustomException(
|
32
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
33
|
+
detail="Database session is not set",
|
34
|
+
source_function=f"{self.__class__.__name__}.session"
|
35
|
+
)
|
36
|
+
return self._session
|
37
|
+
|
38
|
+
@session.setter
|
39
|
+
def session(self, value):
|
40
|
+
"""세션을 설정합니다."""
|
41
|
+
self._session = value
|
42
|
+
|
43
|
+
#######################
|
44
|
+
# 2. CRUD 작업 #
|
45
|
+
#######################
|
46
|
+
async def get(
|
47
|
+
self,
|
48
|
+
ulid: str
|
49
|
+
) -> Optional[Dict[str, Any]]:
|
50
|
+
"""ULID로 엔티티를 조회합니다."""
|
51
|
+
try:
|
52
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
53
|
+
result = await self.session.execute(stmt)
|
54
|
+
entity = result.scalars().unique().first()
|
55
|
+
|
56
|
+
if not entity:
|
57
|
+
raise CustomException(
|
58
|
+
ErrorCode.DB_NO_RESULT,
|
59
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
60
|
+
source_function=f"{self.__class__.__name__}.get"
|
61
|
+
)
|
62
|
+
return entity
|
63
|
+
except CustomException as e:
|
64
|
+
e.detail = f"Repository error for {self.model.__tablename__}: {e.detail}"
|
65
|
+
e.source_function = f"{self.__class__.__name__}.get -> {e.source_function}"
|
66
|
+
raise e
|
67
|
+
except SQLAlchemyError as e:
|
68
|
+
raise CustomException(
|
69
|
+
ErrorCode.DB_QUERY_ERROR,
|
70
|
+
detail=f"Database error in {self.model.__tablename__}: {str(e)}",
|
71
|
+
source_function=f"{self.__class__.__name__}.get",
|
72
|
+
original_error=e
|
73
|
+
)
|
74
|
+
|
75
|
+
async def list(
|
76
|
+
self,
|
77
|
+
skip: int = 0,
|
78
|
+
limit: int = 100,
|
79
|
+
filters: Dict[str, Any] | None = None,
|
80
|
+
search_params: Dict[str, Any] | None = None
|
81
|
+
) -> List[Any]:
|
82
|
+
"""엔티티 목록을 조회합니다."""
|
83
|
+
try:
|
84
|
+
stmt = select(self.model).where(self.model.is_deleted == False)
|
85
|
+
|
86
|
+
# 필터 적용
|
87
|
+
if filters:
|
88
|
+
stmt = self._apply_filters(stmt, filters)
|
89
|
+
|
90
|
+
# 검색 적용
|
91
|
+
if search_params:
|
92
|
+
stmt = self._apply_search_params(stmt, search_params)
|
93
|
+
|
94
|
+
# 페이지네이션 적용
|
95
|
+
stmt = stmt.limit(limit).offset(skip)
|
96
|
+
|
97
|
+
result = await self.session.execute(stmt)
|
98
|
+
return result.scalars().unique().all()
|
99
|
+
|
100
|
+
except SQLAlchemyError as e:
|
101
|
+
raise CustomException(
|
102
|
+
ErrorCode.DB_QUERY_ERROR,
|
103
|
+
detail=f"Unexpected repository list error in {self.model.__tablename__}: {str(e)}",
|
104
|
+
source_function=f"{self.__class__.__name__}.list",
|
105
|
+
original_error=e,
|
106
|
+
)
|
107
|
+
|
108
|
+
async def create(self, data: Dict[str, Any]) -> ModelType:
|
109
|
+
"""새로운 엔티티를 생성합니다."""
|
110
|
+
try:
|
111
|
+
entity = self.model(**data)
|
112
|
+
self.session.add(entity)
|
113
|
+
await self.session.flush()
|
114
|
+
await self.session.refresh(entity)
|
115
|
+
return entity
|
116
|
+
except IntegrityError as e:
|
117
|
+
await self.session.rollback()
|
118
|
+
self._handle_integrity_error(e, "create", data)
|
119
|
+
except SQLAlchemyError as e:
|
120
|
+
await self.session.rollback()
|
121
|
+
raise CustomException(
|
122
|
+
ErrorCode.DB_CREATE_ERROR,
|
123
|
+
detail=f"Database create error in {self.model.__tablename__}: {str(e)}",
|
124
|
+
source_function=f"{self.__class__.__name__}.create",
|
125
|
+
original_error=e
|
126
|
+
)
|
127
|
+
|
128
|
+
async def update(self, ulid: str, data: Dict[str, Any]) -> Optional[ModelType]:
|
129
|
+
"""기존 엔티티를 수정합니다."""
|
130
|
+
try:
|
131
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
132
|
+
result = await self.session.execute(stmt)
|
133
|
+
entity = result.scalars().first()
|
134
|
+
|
135
|
+
if not entity:
|
136
|
+
raise CustomException(
|
137
|
+
ErrorCode.DB_NO_RESULT,
|
138
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
139
|
+
source_function=f"{self.__class__.__name__}.update"
|
140
|
+
)
|
141
|
+
|
142
|
+
for key, value in data.items():
|
143
|
+
setattr(entity, key, value)
|
144
|
+
|
145
|
+
await self.session.flush()
|
146
|
+
await self.session.refresh(entity)
|
147
|
+
return entity
|
148
|
+
|
149
|
+
except IntegrityError as e:
|
150
|
+
await self.session.rollback()
|
151
|
+
self._handle_integrity_error(e, "update", data)
|
152
|
+
except SQLAlchemyError as e:
|
153
|
+
await self.session.rollback()
|
154
|
+
raise CustomException(
|
155
|
+
ErrorCode.DB_UPDATE_ERROR,
|
156
|
+
detail=f"Database update error in {self.model.__tablename__}: {str(e)}",
|
157
|
+
source_function=f"{self.__class__.__name__}.update",
|
158
|
+
original_error=e
|
159
|
+
)
|
160
|
+
|
161
|
+
async def delete(self, ulid: str) -> bool:
|
162
|
+
"""엔티티를 소프트 삭제합니다."""
|
163
|
+
try:
|
164
|
+
stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
|
165
|
+
result = await self.session.execute(stmt)
|
166
|
+
entity = result.scalars().first()
|
167
|
+
|
168
|
+
if not entity:
|
169
|
+
raise CustomException(
|
170
|
+
ErrorCode.DB_NO_RESULT,
|
171
|
+
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
172
|
+
source_function=f"{self.__class__.__name__}.delete"
|
173
|
+
)
|
174
|
+
|
175
|
+
entity.is_deleted = True
|
176
|
+
await self.session.flush()
|
177
|
+
return True
|
178
|
+
|
179
|
+
except SQLAlchemyError as e:
|
180
|
+
await self.session.rollback()
|
181
|
+
raise CustomException(
|
182
|
+
ErrorCode.DB_DELETE_ERROR,
|
183
|
+
detail=f"Database delete error in {self.model.__tablename__}: {str(e)}",
|
184
|
+
source_function=f"{self.__class__.__name__}.delete",
|
185
|
+
original_error=e
|
186
|
+
)
|
187
|
+
|
188
|
+
async def real_delete(self, ulid: str) -> bool:
|
189
|
+
"""엔티티를 실제로 삭제합니다."""
|
190
|
+
try:
|
191
|
+
stmt = select(self.model).filter_by(ulid=ulid)
|
192
|
+
result = await self.session.execute(stmt)
|
193
|
+
entity = result.scalars().first()
|
194
|
+
|
195
|
+
if entity:
|
196
|
+
await self.session.delete(entity)
|
197
|
+
await self.session.flush()
|
198
|
+
return True
|
199
|
+
return False
|
200
|
+
|
201
|
+
except SQLAlchemyError as e:
|
202
|
+
await self.session.rollback()
|
203
|
+
raise CustomException(
|
204
|
+
ErrorCode.DB_DELETE_ERROR,
|
205
|
+
detail=f"Database real delete error in {self.model.__tablename__}: {str(e)}",
|
206
|
+
source_function=f"{self.__class__.__name__}.real_delete",
|
207
|
+
original_error=e
|
208
|
+
)
|
@@ -9,6 +9,7 @@ from .base_repository import BaseRepository
|
|
9
9
|
from .security import hash_password
|
10
10
|
from fastapi import Request
|
11
11
|
from ulid import ULID
|
12
|
+
from sqlalchemy import select
|
12
13
|
|
13
14
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
14
15
|
|
@@ -30,11 +31,29 @@ class BaseService(Generic[ModelType]):
|
|
30
31
|
self.repository = repository
|
31
32
|
self.model = repository.model
|
32
33
|
self.additional_models = additional_models or {}
|
33
|
-
self.
|
34
|
+
self._session = None
|
34
35
|
self.searchable_fields = {
|
35
36
|
"name": {"type": "text", "description": "이름"},
|
36
37
|
"organization_ulid": {"type": "exact", "description": "조직 ID"}
|
37
38
|
}
|
39
|
+
|
40
|
+
@property
|
41
|
+
def session(self):
|
42
|
+
"""현재 세션을 반환합니다."""
|
43
|
+
if self._session is None:
|
44
|
+
raise CustomException(
|
45
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
46
|
+
detail="Database session is not set",
|
47
|
+
source_function=f"{self.__class__.__name__}.session"
|
48
|
+
)
|
49
|
+
return self._session
|
50
|
+
|
51
|
+
@session.setter
|
52
|
+
def session(self, value):
|
53
|
+
"""세션을 설정합니다."""
|
54
|
+
self._session = value
|
55
|
+
if hasattr(self.repository, 'session'):
|
56
|
+
self.repository.session = value
|
38
57
|
|
39
58
|
#########################
|
40
59
|
# 2. 이벤트 처리 메서드 #
|
@@ -448,18 +467,7 @@ class BaseService(Generic[ModelType]):
|
|
448
467
|
)
|
449
468
|
|
450
469
|
async def delete(self, ulid: str, model_name: str = None) -> bool:
|
451
|
-
"""엔티티를 소프트 삭제합니다 (is_deleted = True).
|
452
|
-
|
453
|
-
Args:
|
454
|
-
ulid (str): 삭제할 엔티티의 ULID
|
455
|
-
model_name (str, optional): 삭제할 모델 이름. Defaults to None.
|
456
|
-
|
457
|
-
Returns:
|
458
|
-
bool: 삭제 성공 여부
|
459
|
-
|
460
|
-
Raises:
|
461
|
-
CustomException: 데이터베이스 작업 중 오류 발생 시
|
462
|
-
"""
|
470
|
+
"""엔티티를 소프트 삭제합니다 (is_deleted = True)."""
|
463
471
|
try:
|
464
472
|
if model_name:
|
465
473
|
if model_name not in self.additional_models:
|
@@ -468,23 +476,24 @@ class BaseService(Generic[ModelType]):
|
|
468
476
|
detail=f"Model {model_name} not registered",
|
469
477
|
source_function=f"{self.__class__.__name__}.delete"
|
470
478
|
)
|
471
|
-
|
479
|
+
|
480
|
+
stmt = select(self.additional_models[model_name]).filter_by(ulid=ulid, is_deleted=False)
|
481
|
+
result = await self.session.execute(stmt)
|
482
|
+
entity = result.scalars().first()
|
483
|
+
|
472
484
|
if not entity:
|
473
485
|
raise CustomException(
|
474
486
|
ErrorCode.NOT_FOUND,
|
475
487
|
detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
|
476
488
|
source_function=f"{self.__class__.__name__}.delete"
|
477
489
|
)
|
490
|
+
|
491
|
+
entity.is_deleted = True
|
492
|
+
await self.session.flush()
|
478
493
|
return True
|
479
494
|
|
480
|
-
|
481
|
-
|
482
|
-
raise CustomException(
|
483
|
-
ErrorCode.NOT_FOUND,
|
484
|
-
detail=f"{self.model.__tablename__}|ulid|{ulid}",
|
485
|
-
source_function=f"{self.__class__.__name__}.delete"
|
486
|
-
)
|
487
|
-
return True
|
495
|
+
return await self.repository.delete(ulid)
|
496
|
+
|
488
497
|
except CustomException as e:
|
489
498
|
raise e
|
490
499
|
except Exception as e:
|
@@ -549,20 +558,7 @@ class BaseService(Generic[ModelType]):
|
|
549
558
|
request: Request | None = None,
|
550
559
|
response_model: Any = None
|
551
560
|
) -> List[Dict[str, Any]]:
|
552
|
-
"""엔티티 목록을 조회합니다.
|
553
|
-
|
554
|
-
Args:
|
555
|
-
skip (int, optional): 건너뛸 레코드 수. Defaults to 0.
|
556
|
-
limit (int, optional): 조회할 최대 레코드 수. Defaults to 100.
|
557
|
-
filters (Dict[str, Any] | None, optional): 필터링 조건. Defaults to None.
|
558
|
-
search_params (Dict[str, Any] | None, optional): 검색 파라미터. Defaults to None.
|
559
|
-
model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
|
560
|
-
request (Request | None, optional): 요청 객체. Defaults to None.
|
561
|
-
response_model (Any, optional): 응답 스키마. Defaults to None.
|
562
|
-
|
563
|
-
Returns:
|
564
|
-
List[Dict[str, Any]]: 엔티티 목록
|
565
|
-
"""
|
561
|
+
"""엔티티 목록을 조회합니다."""
|
566
562
|
try:
|
567
563
|
if model_name:
|
568
564
|
if model_name not in self.additional_models:
|
@@ -571,21 +567,28 @@ class BaseService(Generic[ModelType]):
|
|
571
567
|
detail=f"Model {model_name} not registered",
|
572
568
|
source_function=f"{self.__class__.__name__}.list"
|
573
569
|
)
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
limit=limit,
|
578
|
-
filters=filters
|
570
|
+
|
571
|
+
stmt = select(self.additional_models[model_name]).where(
|
572
|
+
self.additional_models[model_name].is_deleted == False
|
579
573
|
)
|
574
|
+
|
575
|
+
if filters:
|
576
|
+
for key, value in filters.items():
|
577
|
+
if value is not None:
|
578
|
+
stmt = stmt.where(getattr(self.additional_models[model_name], key) == value)
|
579
|
+
|
580
|
+
stmt = stmt.offset(skip).limit(limit)
|
581
|
+
result = await self.session.execute(stmt)
|
582
|
+
entities = result.scalars().all()
|
583
|
+
|
580
584
|
return [self._process_response(entity, response_model) for entity in entities]
|
581
585
|
|
582
|
-
|
586
|
+
return await self.repository.list(
|
583
587
|
skip=skip,
|
584
588
|
limit=limit,
|
585
589
|
filters=filters,
|
586
590
|
search_params=search_params
|
587
591
|
)
|
588
|
-
return [self._process_response(entity, response_model) for entity in entities]
|
589
592
|
|
590
593
|
except CustomException as e:
|
591
594
|
e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
|
@@ -764,11 +764,12 @@ class DatabaseService:
|
|
764
764
|
processed_data = self.preprocess_data(model, log_data)
|
765
765
|
entity = model(**processed_data)
|
766
766
|
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
767
|
+
async with self.get_session() as session:
|
768
|
+
# 로그 엔티티 저장
|
769
|
+
session.add(entity)
|
770
|
+
await session.flush()
|
771
|
+
await session.commit()
|
772
|
+
return entity
|
772
773
|
|
773
774
|
except Exception as e:
|
774
775
|
logging.error(f"Failed to create log: {str(e)}")
|
@@ -996,6 +997,24 @@ class DatabaseService:
|
|
996
997
|
original_error=e
|
997
998
|
)
|
998
999
|
|
1000
|
+
async def create_session(self) -> AsyncSession:
|
1001
|
+
"""새로운 데이터베이스 세션을 생성합니다.
|
1002
|
+
|
1003
|
+
Returns:
|
1004
|
+
AsyncSession: 생성된 세션
|
1005
|
+
|
1006
|
+
Raises:
|
1007
|
+
CustomException: 세션 생성 실패 시
|
1008
|
+
"""
|
1009
|
+
if self.session_factory is None:
|
1010
|
+
raise CustomException(
|
1011
|
+
ErrorCode.DB_CONNECTION_ERROR,
|
1012
|
+
detail="session_factory is not initialized",
|
1013
|
+
source_function="DatabaseService.create_session"
|
1014
|
+
)
|
1015
|
+
|
1016
|
+
return self.session_factory()
|
1017
|
+
|
999
1018
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
1000
1019
|
"""데이터베이스 세션 의존성
|
1001
1020
|
|
@@ -0,0 +1,213 @@
|
|
1
|
+
"""의존성 관리 모듈."""
|
2
|
+
from typing import Type, TypeVar, Dict, Any, Optional, Callable, List, AsyncGenerator
|
3
|
+
from fastapi import Request, Depends, HTTPException, status
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
5
|
+
from jose import jwt, JWTError
|
6
|
+
import logging
|
7
|
+
|
8
|
+
from .exceptions import CustomException, ErrorCode
|
9
|
+
from .config import get_settings
|
10
|
+
from .base_service import BaseService
|
11
|
+
from .base_repository import BaseRepository
|
12
|
+
from .database import db_manager
|
13
|
+
|
14
|
+
T = TypeVar("T", bound=BaseService)
|
15
|
+
R = TypeVar("R", bound=BaseRepository)
|
16
|
+
|
17
|
+
_service_registry: Dict[str, Dict[str, Any]] = {}
|
18
|
+
|
19
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
20
|
+
"""데이터베이스 세션을 반환합니다.
|
21
|
+
|
22
|
+
Yields:
|
23
|
+
AsyncSession: 데이터베이스 세션
|
24
|
+
|
25
|
+
Raises:
|
26
|
+
CustomException: 세션 생성 실패 시
|
27
|
+
"""
|
28
|
+
try:
|
29
|
+
async with db_manager.get_session() as session:
|
30
|
+
yield session
|
31
|
+
except Exception as e:
|
32
|
+
raise CustomException(
|
33
|
+
ErrorCode.DATABASE_ERROR,
|
34
|
+
detail=str(e),
|
35
|
+
source_function="dependencies.get_db",
|
36
|
+
original_error=e
|
37
|
+
)
|
38
|
+
|
39
|
+
def register_service(
|
40
|
+
service_class: Type[T],
|
41
|
+
repository_class: Optional[Type[R]] = None,
|
42
|
+
**kwargs
|
43
|
+
) -> None:
|
44
|
+
"""서비스를 등록합니다.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
service_class: 서비스 클래스
|
48
|
+
repository_class: 저장소 클래스 (선택)
|
49
|
+
**kwargs: 추가 의존성
|
50
|
+
"""
|
51
|
+
service_name = service_class.__name__
|
52
|
+
_service_registry[service_name] = {
|
53
|
+
"service_class": service_class,
|
54
|
+
"repository_class": repository_class,
|
55
|
+
"dependencies": kwargs
|
56
|
+
}
|
57
|
+
|
58
|
+
async def _get_service(
|
59
|
+
service_name: str,
|
60
|
+
session: AsyncSession,
|
61
|
+
request: Request
|
62
|
+
) -> BaseService:
|
63
|
+
"""서비스 인스턴스를 생성합니다.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
service_name: 서비스 이름
|
67
|
+
session: 데이터베이스 세션
|
68
|
+
request: FastAPI 요청 객체
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
BaseService: 서비스 인스턴스
|
72
|
+
|
73
|
+
Raises:
|
74
|
+
CustomException: 서비스 생성 실패 시
|
75
|
+
"""
|
76
|
+
try:
|
77
|
+
service_info = _service_registry.get(service_name)
|
78
|
+
if not service_info:
|
79
|
+
raise CustomException(
|
80
|
+
ErrorCode.SERVICE_NOT_FOUND,
|
81
|
+
detail=service_name,
|
82
|
+
source_function="dependencies._get_service"
|
83
|
+
)
|
84
|
+
|
85
|
+
service_class = service_info["service_class"]
|
86
|
+
repository_class = service_info["repository_class"]
|
87
|
+
dependencies = service_info["dependencies"]
|
88
|
+
|
89
|
+
# 저장소 인스턴스 생성
|
90
|
+
repository = None
|
91
|
+
if repository_class:
|
92
|
+
repository = repository_class(session=session)
|
93
|
+
|
94
|
+
# 서비스 인스턴스 생성
|
95
|
+
service = service_class(
|
96
|
+
repository=repository,
|
97
|
+
session=session,
|
98
|
+
request=request,
|
99
|
+
**dependencies
|
100
|
+
)
|
101
|
+
|
102
|
+
return service
|
103
|
+
|
104
|
+
except CustomException as e:
|
105
|
+
raise e
|
106
|
+
except Exception as e:
|
107
|
+
raise CustomException(
|
108
|
+
ErrorCode.INTERNAL_ERROR,
|
109
|
+
detail=str(e),
|
110
|
+
source_function="dependencies._get_service",
|
111
|
+
original_error=e
|
112
|
+
)
|
113
|
+
|
114
|
+
def get_service(service_name: str) -> Callable:
|
115
|
+
"""서비스 의존성을 반환합니다.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
service_name: 서비스 이름
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Callable: 서비스 의존성 함수
|
122
|
+
"""
|
123
|
+
async def _get_service_dependency(
|
124
|
+
request: Request,
|
125
|
+
session: AsyncSession = Depends(get_db)
|
126
|
+
) -> BaseService:
|
127
|
+
return await _get_service(service_name, session, request)
|
128
|
+
return _get_service_dependency
|
129
|
+
|
130
|
+
async def get_current_user(
|
131
|
+
request: Request,
|
132
|
+
session: AsyncSession = Depends(get_db),
|
133
|
+
auth_service: BaseService = Depends(get_service("AuthService"))
|
134
|
+
) -> Dict[str, Any]:
|
135
|
+
"""현재 사용자 정보를 반환합니다.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
request: FastAPI 요청 객체
|
139
|
+
session: 데이터베이스 세션
|
140
|
+
auth_service: 인증 서비스
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Dict[str, Any]: 사용자 정보
|
144
|
+
|
145
|
+
Raises:
|
146
|
+
HTTPException: 인증 실패 시
|
147
|
+
"""
|
148
|
+
settings = get_settings()
|
149
|
+
|
150
|
+
# Authorization 헤더 검증
|
151
|
+
authorization = request.headers.get("Authorization")
|
152
|
+
if not authorization or not authorization.startswith("Bearer "):
|
153
|
+
raise HTTPException(
|
154
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
155
|
+
detail="Not authenticated",
|
156
|
+
headers={"WWW-Authenticate": "Bearer"}
|
157
|
+
)
|
158
|
+
|
159
|
+
token = authorization.split(" ")[1]
|
160
|
+
|
161
|
+
try:
|
162
|
+
# JWT 토큰 디코딩
|
163
|
+
payload = jwt.decode(
|
164
|
+
token,
|
165
|
+
settings.jwt_secret,
|
166
|
+
algorithms=[settings.jwt_algorithm],
|
167
|
+
issuer=settings.token_issuer,
|
168
|
+
audience=settings.token_audience
|
169
|
+
)
|
170
|
+
|
171
|
+
# 토큰 타입 검증
|
172
|
+
token_type = payload.get("token_type")
|
173
|
+
if token_type != "access":
|
174
|
+
raise HTTPException(
|
175
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
176
|
+
detail="Invalid token type",
|
177
|
+
headers={"WWW-Authenticate": "Bearer"}
|
178
|
+
)
|
179
|
+
|
180
|
+
# 사용자 조회
|
181
|
+
user_ulid = payload.get("user_ulid")
|
182
|
+
if not user_ulid:
|
183
|
+
raise HTTPException(
|
184
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
185
|
+
detail="Invalid token payload",
|
186
|
+
headers={"WWW-Authenticate": "Bearer"}
|
187
|
+
)
|
188
|
+
|
189
|
+
user = await auth_service.get_by_ulid(user_ulid)
|
190
|
+
if not user:
|
191
|
+
raise HTTPException(
|
192
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
193
|
+
detail="User not found",
|
194
|
+
headers={"WWW-Authenticate": "Bearer"}
|
195
|
+
)
|
196
|
+
|
197
|
+
return user
|
198
|
+
|
199
|
+
except JWTError as e:
|
200
|
+
raise HTTPException(
|
201
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
202
|
+
detail=str(e),
|
203
|
+
headers={"WWW-Authenticate": "Bearer"}
|
204
|
+
)
|
205
|
+
except HTTPException as e:
|
206
|
+
raise e
|
207
|
+
except Exception as e:
|
208
|
+
logging.error(f"Error in get_current_user: {str(e)}")
|
209
|
+
raise HTTPException(
|
210
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
211
|
+
detail="Authentication failed",
|
212
|
+
headers={"WWW-Authenticate": "Bearer"}
|
213
|
+
)
|