aiteamutils 0.2.51__tar.gz → 0.2.53__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/PKG-INFO +1 -1
- aiteamutils-0.2.53/aiteamutils/base_repository.py +208 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/base_service.py +46 -43
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/database.py +24 -5
- aiteamutils-0.2.53/aiteamutils/dependencies.py +213 -0
- aiteamutils-0.2.53/aiteamutils/security.py +291 -0
- aiteamutils-0.2.53/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.53}/.cursorrules +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/.gitignore +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/README.md +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/__init__.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/base_model.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/cache.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/config.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/enums.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/exceptions.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/aiteamutils/validators.py +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/pyproject.toml +0 -0
- {aiteamutils-0.2.51 → aiteamutils-0.2.53}/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
|
+
)
|