aiteamutils 0.2.73__py3-none-any.whl → 0.2.76__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/base_model.py +12 -4
 - aiteamutils/base_repository.py +88 -5
 - aiteamutils/base_service.py +199 -20
 - aiteamutils/database.py +396 -53
 - aiteamutils/exceptions.py +6 -2
 - aiteamutils/security.py +32 -47
 - aiteamutils/version.py +1 -1
 - {aiteamutils-0.2.73.dist-info → aiteamutils-0.2.76.dist-info}/METADATA +1 -1
 - aiteamutils-0.2.76.dist-info/RECORD +15 -0
 - aiteamutils-0.2.73.dist-info/RECORD +0 -15
 - {aiteamutils-0.2.73.dist-info → aiteamutils-0.2.76.dist-info}/WHEEL +0 -0
 
    
        aiteamutils/base_model.py
    CHANGED
    
    | 
         @@ -1,4 +1,4 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            from datetime import datetime, timezone
         
     | 
| 
      
 1 
     | 
    
         
            +
            from datetime import datetime, timedelta, timezone
         
     | 
| 
       2 
2 
     | 
    
         
             
            from typing import Any, Dict, TypeVar, Generic, Optional
         
     | 
| 
       3 
3 
     | 
    
         
             
            from ulid import ULID
         
     | 
| 
       4 
4 
     | 
    
         
             
            from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
         
     | 
| 
         @@ -23,10 +23,18 @@ class BaseColumn(Base): 
     | 
|
| 
       23 
23 
     | 
    
         
             
                    doc="ULID",
         
     | 
| 
       24 
24 
     | 
    
         
             
                    nullable=False
         
     | 
| 
       25 
25 
     | 
    
         
             
                )
         
     | 
| 
       26 
     | 
    
         
            -
                created_at: Mapped[datetime] = mapped_column( 
     | 
| 
      
 26 
     | 
    
         
            +
                created_at: Mapped[datetime] = mapped_column(
         
     | 
| 
      
 27 
     | 
    
         
            +
                    default=datetime.now(timezone.utc),
         
     | 
| 
      
 28 
     | 
    
         
            +
                    index=True
         
     | 
| 
      
 29 
     | 
    
         
            +
                )
         
     | 
| 
       27 
30 
     | 
    
         
             
                updated_at: Mapped[datetime] = mapped_column(
         
     | 
| 
       28 
     | 
    
         
            -
                    default=datetime. 
     | 
| 
       29 
     | 
    
         
            -
                    onupdate=datetime. 
     | 
| 
      
 31 
     | 
    
         
            +
                    default=datetime.now(timezone.utc),
         
     | 
| 
      
 32 
     | 
    
         
            +
                    onupdate=datetime.now(timezone.utc),
         
     | 
| 
      
 33 
     | 
    
         
            +
                    index=True
         
     | 
| 
      
 34 
     | 
    
         
            +
                )
         
     | 
| 
      
 35 
     | 
    
         
            +
                deleted_at: Mapped[datetime] = mapped_column(
         
     | 
| 
      
 36 
     | 
    
         
            +
                    default=None,
         
     | 
| 
      
 37 
     | 
    
         
            +
                    nullable=True
         
     | 
| 
       30 
38 
     | 
    
         
             
                )
         
     | 
| 
       31 
39 
     | 
    
         
             
                is_deleted: Mapped[bool] = mapped_column(
         
     | 
| 
       32 
40 
     | 
    
         
             
                    default=False,
         
     | 
    
        aiteamutils/base_repository.py
    CHANGED
    
    | 
         @@ -7,7 +7,13 @@ from sqlalchemy import select 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            #패키지 라이브러리
         
     | 
| 
       9 
9 
     | 
    
         
             
            from .exceptions import ErrorCode, CustomException
         
     | 
| 
       10 
     | 
    
         
            -
            from .database import  
     | 
| 
      
 10 
     | 
    
         
            +
            from .database import (
         
     | 
| 
      
 11 
     | 
    
         
            +
                list_entities,
         
     | 
| 
      
 12 
     | 
    
         
            +
                get_entity,
         
     | 
| 
      
 13 
     | 
    
         
            +
                create_entity,
         
     | 
| 
      
 14 
     | 
    
         
            +
                update_entity,
         
     | 
| 
      
 15 
     | 
    
         
            +
                delete_entity
         
     | 
| 
      
 16 
     | 
    
         
            +
            )
         
     | 
| 
       11 
17 
     | 
    
         | 
| 
       12 
18 
     | 
    
         
             
            ModelType = TypeVar("ModelType", bound=DeclarativeBase)
         
     | 
| 
       13 
19 
     | 
    
         | 
| 
         @@ -29,13 +35,67 @@ class BaseRepository(Generic[ModelType]): 
     | 
|
| 
       29 
35 
     | 
    
         
             
                            source_function=f"{self.__class__.__name__}.session"
         
     | 
| 
       30 
36 
     | 
    
         
             
                        )
         
     | 
| 
       31 
37 
     | 
    
         
             
                    self._session = value
         
     | 
| 
      
 38 
     | 
    
         
            +
               
         
     | 
| 
      
 39 
     | 
    
         
            +
                #######################
         
     | 
| 
      
 40 
     | 
    
         
            +
                # 입력 및 수정, 삭제 #
         
     | 
| 
      
 41 
     | 
    
         
            +
                ####################### 
         
     | 
| 
      
 42 
     | 
    
         
            +
                async def create(
         
     | 
| 
      
 43 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 44 
     | 
    
         
            +
                    entity_data: Dict[str, Any],
         
     | 
| 
      
 45 
     | 
    
         
            +
                    exclude_entities: List[str] | None = None
         
     | 
| 
      
 46 
     | 
    
         
            +
                ) -> ModelType:
         
     | 
| 
      
 47 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 48 
     | 
    
         
            +
                        return await create_entity(
         
     | 
| 
      
 49 
     | 
    
         
            +
                            session=self.session,
         
     | 
| 
      
 50 
     | 
    
         
            +
                            model=self.model,
         
     | 
| 
      
 51 
     | 
    
         
            +
                            entity_data=entity_data,
         
     | 
| 
      
 52 
     | 
    
         
            +
                            exclude_entities=exclude_entities
         
     | 
| 
      
 53 
     | 
    
         
            +
                        )
         
     | 
| 
      
 54 
     | 
    
         
            +
                    except CustomException as e:
         
     | 
| 
      
 55 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 56 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 57 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 58 
     | 
    
         
            +
                            ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 59 
     | 
    
         
            +
                            detail=str(e),
         
     | 
| 
      
 60 
     | 
    
         
            +
                            source_function=f"{self.__class__.__name__}.create",
         
     | 
| 
      
 61 
     | 
    
         
            +
                            original_error=e
         
     | 
| 
      
 62 
     | 
    
         
            +
                        )
         
     | 
| 
      
 63 
     | 
    
         
            +
                    
         
     | 
| 
      
 64 
     | 
    
         
            +
                async def update(
         
     | 
| 
      
 65 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 66 
     | 
    
         
            +
                    update_data: Dict[str, Any],
         
     | 
| 
      
 67 
     | 
    
         
            +
                    conditions: Dict[str, Any]
         
     | 
| 
      
 68 
     | 
    
         
            +
                ) -> ModelType:
         
     | 
| 
      
 69 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 70 
     | 
    
         
            +
                        return await update_entity(
         
     | 
| 
      
 71 
     | 
    
         
            +
                            session=self.session,
         
     | 
| 
      
 72 
     | 
    
         
            +
                            model=self.model,
         
     | 
| 
      
 73 
     | 
    
         
            +
                            update_data=update_data,
         
     | 
| 
      
 74 
     | 
    
         
            +
                            conditions=conditions
         
     | 
| 
      
 75 
     | 
    
         
            +
                        )
         
     | 
| 
      
 76 
     | 
    
         
            +
                    except CustomException as e:
         
     | 
| 
      
 77 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 78 
     | 
    
         
            +
                    
         
     | 
| 
      
 79 
     | 
    
         
            +
                async def delete(
         
     | 
| 
      
 80 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 81 
     | 
    
         
            +
                    conditions: Dict[str, Any]
         
     | 
| 
      
 82 
     | 
    
         
            +
                ) -> None:
         
     | 
| 
      
 83 
     | 
    
         
            +
                    await delete_entity(
         
     | 
| 
      
 84 
     | 
    
         
            +
                        session=self.session,
         
     | 
| 
      
 85 
     | 
    
         
            +
                        model=self.model,
         
     | 
| 
      
 86 
     | 
    
         
            +
                        conditions=conditions
         
     | 
| 
      
 87 
     | 
    
         
            +
                    )
         
     | 
| 
       32 
88 
     | 
    
         | 
| 
      
 89 
     | 
    
         
            +
                #########################
         
     | 
| 
      
 90 
     | 
    
         
            +
                # 조회 및 검색 메서드 #
         
     | 
| 
      
 91 
     | 
    
         
            +
                #########################
         
     | 
| 
       33 
92 
     | 
    
         
             
                async def list(
         
     | 
| 
       34 
93 
     | 
    
         
             
                    self,
         
     | 
| 
       35 
94 
     | 
    
         
             
                    skip: int = 0,
         
     | 
| 
       36 
95 
     | 
    
         
             
                    limit: int = 100,
         
     | 
| 
       37 
96 
     | 
    
         
             
                    filters: Optional[Dict[str, Any]] = None,
         
     | 
| 
       38 
     | 
    
         
            -
                     
     | 
| 
      
 97 
     | 
    
         
            +
                    explicit_joins: Optional[List[Any]] = None,
         
     | 
| 
      
 98 
     | 
    
         
            +
                    loading_joins: Optional[List[Any]] = None
         
     | 
| 
       39 
99 
     | 
    
         
             
                ) -> List[ModelType]:
         
     | 
| 
       40 
100 
     | 
    
         
             
                    """
         
     | 
| 
       41 
101 
     | 
    
         
             
                    엔티티 목록 조회.
         
     | 
| 
         @@ -48,11 +108,10 @@ class BaseRepository(Generic[ModelType]): 
     | 
|
| 
       48 
108 
     | 
    
         
             
                            skip=skip,
         
     | 
| 
       49 
109 
     | 
    
         
             
                            limit=limit,
         
     | 
| 
       50 
110 
     | 
    
         
             
                            filters=filters,
         
     | 
| 
       51 
     | 
    
         
            -
                             
     | 
| 
      
 111 
     | 
    
         
            +
                            explicit_joins=explicit_joins,
         
     | 
| 
      
 112 
     | 
    
         
            +
                            loading_joins=loading_joins
         
     | 
| 
       52 
113 
     | 
    
         
             
                        )
         
     | 
| 
       53 
114 
     | 
    
         
             
                    except CustomException as e:
         
     | 
| 
       54 
     | 
    
         
            -
                        e.detail = f"Repository list error for {self.model.__tablename__}: {e.detail}"
         
     | 
| 
       55 
     | 
    
         
            -
                        e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
         
     | 
| 
       56 
115 
     | 
    
         
             
                        raise e
         
     | 
| 
       57 
116 
     | 
    
         
             
                    except Exception as e:
         
     | 
| 
       58 
117 
     | 
    
         
             
                        raise CustomException(
         
     | 
| 
         @@ -61,3 +120,27 @@ class BaseRepository(Generic[ModelType]): 
     | 
|
| 
       61 
120 
     | 
    
         
             
                            source_function=f"{self.__class__.__name__}.list",
         
     | 
| 
       62 
121 
     | 
    
         
             
                            original_error=e
         
     | 
| 
       63 
122 
     | 
    
         
             
                        )
         
     | 
| 
      
 123 
     | 
    
         
            +
                    
         
     | 
| 
      
 124 
     | 
    
         
            +
                async def get(
         
     | 
| 
      
 125 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 126 
     | 
    
         
            +
                    conditions: Dict[str, Any] | None = None,
         
     | 
| 
      
 127 
     | 
    
         
            +
                    explicit_joins: Optional[List[Any]] = None,
         
     | 
| 
      
 128 
     | 
    
         
            +
                    loading_joins: Optional[List[Any]] = None
         
     | 
| 
      
 129 
     | 
    
         
            +
                ) -> ModelType:
         
     | 
| 
      
 130 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 131 
     | 
    
         
            +
                        return await get_entity(
         
     | 
| 
      
 132 
     | 
    
         
            +
                            session=self.session,
         
     | 
| 
      
 133 
     | 
    
         
            +
                            model=self.model,
         
     | 
| 
      
 134 
     | 
    
         
            +
                            conditions=conditions,
         
     | 
| 
      
 135 
     | 
    
         
            +
                            explicit_joins=explicit_joins,
         
     | 
| 
      
 136 
     | 
    
         
            +
                            loading_joins=loading_joins
         
     | 
| 
      
 137 
     | 
    
         
            +
                        )
         
     | 
| 
      
 138 
     | 
    
         
            +
                    except CustomException as e:
         
     | 
| 
      
 139 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 140 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 141 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 142 
     | 
    
         
            +
                            ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 143 
     | 
    
         
            +
                            detail=str(e),
         
     | 
| 
      
 144 
     | 
    
         
            +
                            source_function=f"{self.__class__.__name__}.get",
         
     | 
| 
      
 145 
     | 
    
         
            +
                            original_error=e
         
     | 
| 
      
 146 
     | 
    
         
            +
                        )
         
     | 
    
        aiteamutils/base_service.py
    CHANGED
    
    | 
         @@ -1,18 +1,19 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            #기본 라이브러리
         
     | 
| 
       2 
2 
     | 
    
         
             
            from fastapi import Request
         
     | 
| 
       3 
     | 
    
         
            -
            from typing import TypeVar, Generic, Type, Dict, Any, Union, List
         
     | 
| 
      
 3 
     | 
    
         
            +
            from typing import TypeVar, Generic, Type, Dict, Any, Union, List, Optional
         
     | 
| 
       4 
4 
     | 
    
         
             
            from sqlalchemy.orm import DeclarativeBase
         
     | 
| 
       5 
5 
     | 
    
         
             
            from sqlalchemy.ext.asyncio import AsyncSession
         
     | 
| 
       6 
6 
     | 
    
         
             
            from datetime import datetime
         
     | 
| 
      
 7 
     | 
    
         
            +
            from ulid import ULID
         
     | 
| 
       7 
8 
     | 
    
         | 
| 
       8 
9 
     | 
    
         
             
            #패키지 라이브러리
         
     | 
| 
       9 
10 
     | 
    
         
             
            from .exceptions import ErrorCode, CustomException
         
     | 
| 
       10 
11 
     | 
    
         
             
            from .base_repository import BaseRepository
         
     | 
| 
       11 
12 
     | 
    
         
             
            from .database import (
         
     | 
| 
       12 
13 
     | 
    
         
             
                process_response,
         
     | 
| 
       13 
     | 
    
         
            -
                 
     | 
| 
      
 14 
     | 
    
         
            +
                validate_unique_fields
         
     | 
| 
       14 
15 
     | 
    
         
             
            )
         
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
      
 16 
     | 
    
         
            +
            from .security import hash_password
         
     | 
| 
       16 
17 
     | 
    
         
             
            ModelType = TypeVar("ModelType", bound=DeclarativeBase)
         
     | 
| 
       17 
18 
     | 
    
         | 
| 
       18 
19 
     | 
    
         
             
            class BaseService(Generic[ModelType]):
         
     | 
| 
         @@ -30,26 +31,149 @@ class BaseService(Generic[ModelType]): 
     | 
|
| 
       30 
31 
     | 
    
         
             
                    self.repository = repository
         
     | 
| 
       31 
32 
     | 
    
         
             
                    self.db_session = db_session
         
     | 
| 
       32 
33 
     | 
    
         
             
                    self.additional_models = additional_models or {},
         
     | 
| 
      
 34 
     | 
    
         
            +
                
         
     | 
| 
      
 35 
     | 
    
         
            +
                #######################
         
     | 
| 
      
 36 
     | 
    
         
            +
                # 입력 및 수정, 삭제 #
         
     | 
| 
      
 37 
     | 
    
         
            +
                #######################
         
     | 
| 
      
 38 
     | 
    
         
            +
                async def create(
         
     | 
| 
      
 39 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 40 
     | 
    
         
            +
                    entity_data: Dict[str, Any],
         
     | 
| 
      
 41 
     | 
    
         
            +
                    model_name: str | None = None,
         
     | 
| 
      
 42 
     | 
    
         
            +
                    response_model: Any = None,
         
     | 
| 
      
 43 
     | 
    
         
            +
                    exclude_entities: List[str] | None = None,
         
     | 
| 
      
 44 
     | 
    
         
            +
                    unique_check: List[Dict[str, Any]] | None = None,
         
     | 
| 
      
 45 
     | 
    
         
            +
                    fk_check: List[Dict[str, Any]] | None = None,
         
     | 
| 
      
 46 
     | 
    
         
            +
                ) -> ModelType:
         
     | 
| 
      
 47 
     | 
    
         
            +
                    
         
     | 
| 
      
 48 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 49 
     | 
    
         
            +
                        async with self.db_session.begin():
         
     | 
| 
      
 50 
     | 
    
         
            +
                            # 고유 검사 수행
         
     | 
| 
      
 51 
     | 
    
         
            +
                            if unique_check:
         
     | 
| 
      
 52 
     | 
    
         
            +
                                await validate_unique_fields(self.db_session, unique_check, find_value=True)
         
     | 
| 
      
 53 
     | 
    
         
            +
                            # 외래 키 검사 수행
         
     | 
| 
      
 54 
     | 
    
         
            +
                            if fk_check:
         
     | 
| 
      
 55 
     | 
    
         
            +
                                await validate_unique_fields(self.db_session, fk_check, find_value=False)
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                            # repository의 create 메서드를 트랜잭션 내에서 실행
         
     | 
| 
      
 58 
     | 
    
         
            +
                            return await self.repository.create(
         
     | 
| 
      
 59 
     | 
    
         
            +
                                entity_data=entity_data,
         
     | 
| 
      
 60 
     | 
    
         
            +
                                exclude_entities=exclude_entities
         
     | 
| 
      
 61 
     | 
    
         
            +
                            )
         
     | 
| 
      
 62 
     | 
    
         
            +
                    except CustomException as e:
         
     | 
| 
      
 63 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 64 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 65 
     | 
    
         
            +
                        # 다른 예외 처리
         
     | 
| 
      
 66 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 67 
     | 
    
         
            +
                            ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 68 
     | 
    
         
            +
                            detail=str(e),
         
     | 
| 
      
 69 
     | 
    
         
            +
                            source_function=f"{self.__class__.__name__}.create",
         
     | 
| 
      
 70 
     | 
    
         
            +
                            original_error=e
         
     | 
| 
      
 71 
     | 
    
         
            +
                        )
         
     | 
| 
      
 72 
     | 
    
         
            +
                    
         
     | 
| 
      
 73 
     | 
    
         
            +
                async def update(
         
     | 
| 
      
 74 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 75 
     | 
    
         
            +
                    ulid: str | None = None,
         
     | 
| 
      
 76 
     | 
    
         
            +
                    update_data: Dict[str, Any] | None = None,
         
     | 
| 
      
 77 
     | 
    
         
            +
                    conditions: Dict[str, Any] | None = None,
         
     | 
| 
      
 78 
     | 
    
         
            +
                    unique_check: List[Dict[str, Any]] | None = None,
         
     | 
| 
      
 79 
     | 
    
         
            +
                    exclude_entities: List[str] | None = None
         
     | 
| 
      
 80 
     | 
    
         
            +
                ) -> ModelType:
         
     | 
| 
      
 81 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 82 
     | 
    
         
            +
                        # 고유 검사 수행
         
     | 
| 
      
 83 
     | 
    
         
            +
                        if unique_check:
         
     | 
| 
      
 84 
     | 
    
         
            +
                            await validate_unique_fields(self.db_session, unique_check, find_value=True)
         
     | 
| 
      
 85 
     | 
    
         
            +
                        # 비밀번호가 있으면 해시 처리
         
     | 
| 
      
 86 
     | 
    
         
            +
                        if "password" in update_data:
         
     | 
| 
      
 87 
     | 
    
         
            +
                            update_data["password"] = hash_password(update_data["password"])
         
     | 
| 
      
 88 
     | 
    
         
            +
                        # 제외할 엔티티가 있으면 제외
         
     | 
| 
      
 89 
     | 
    
         
            +
                        if exclude_entities:
         
     | 
| 
      
 90 
     | 
    
         
            +
                            update_data = {k: v for k, v in update_data.items() if k not in exclude_entities}
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                        if not ulid and not conditions:
         
     | 
| 
      
 93 
     | 
    
         
            +
                            raise CustomException(
         
     | 
| 
      
 94 
     | 
    
         
            +
                                ErrorCode.INVALID_INPUT,
         
     | 
| 
      
 95 
     | 
    
         
            +
                                detail="Either 'ulid' or 'conditions' must be provided.",
         
     | 
| 
      
 96 
     | 
    
         
            +
                                source_function="database.update_entity"
         
     | 
| 
      
 97 
     | 
    
         
            +
                            )
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                        # ulid로 조건 생성
         
     | 
| 
      
 100 
     | 
    
         
            +
                        if ulid:
         
     | 
| 
      
 101 
     | 
    
         
            +
                            if not ULID.from_str(ulid):
         
     | 
| 
      
 102 
     | 
    
         
            +
                                raise CustomException(
         
     | 
| 
      
 103 
     | 
    
         
            +
                                    ErrorCode.VALIDATION_ERROR,
         
     | 
| 
      
 104 
     | 
    
         
            +
                                    detail=ulid,
         
     | 
| 
      
 105 
     | 
    
         
            +
                                    source_function=f"{self.__class__.__name__}.update"
         
     | 
| 
      
 106 
     | 
    
         
            +
                                )
         
     | 
| 
      
 107 
     | 
    
         
            +
                            
         
     | 
| 
      
 108 
     | 
    
         
            +
                            conditions = {"ulid": ulid}
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                        return await self.repository.update(
         
     | 
| 
      
 111 
     | 
    
         
            +
                            update_data=update_data,
         
     | 
| 
      
 112 
     | 
    
         
            +
                            conditions=conditions
         
     | 
| 
      
 113 
     | 
    
         
            +
                        )
         
     | 
| 
      
 114 
     | 
    
         
            +
                    except CustomException as e:
         
     | 
| 
      
 115 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 116 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 117 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 118 
     | 
    
         
            +
                            ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 119 
     | 
    
         
            +
                            detail=str(e),
         
     | 
| 
      
 120 
     | 
    
         
            +
                            source_function=f"{self.__class__.__name__}.update",
         
     | 
| 
      
 121 
     | 
    
         
            +
                            original_error=e
         
     | 
| 
      
 122 
     | 
    
         
            +
                        )
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                async def delete(
         
     | 
| 
      
 125 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 126 
     | 
    
         
            +
                    ulid: str | None = None,
         
     | 
| 
      
 127 
     | 
    
         
            +
                    conditions: Dict[str, Any] | None = None
         
     | 
| 
      
 128 
     | 
    
         
            +
                ) -> None:
         
     | 
| 
      
 129 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 130 
     | 
    
         
            +
                        if not ULID.from_str(ulid):
         
     | 
| 
      
 131 
     | 
    
         
            +
                            raise CustomException(
         
     | 
| 
      
 132 
     | 
    
         
            +
                                ErrorCode.VALIDATION_ERROR,
         
     | 
| 
      
 133 
     | 
    
         
            +
                                detail=ulid,
         
     | 
| 
      
 134 
     | 
    
         
            +
                                source_function=f"{self.__class__.__name__}.delete"
         
     | 
| 
      
 135 
     | 
    
         
            +
                            )
         
     | 
| 
      
 136 
     | 
    
         
            +
                        
         
     | 
| 
      
 137 
     | 
    
         
            +
                        if not ulid and not conditions:
         
     | 
| 
      
 138 
     | 
    
         
            +
                            raise CustomException(
         
     | 
| 
      
 139 
     | 
    
         
            +
                                ErrorCode.INVALID_INPUT,
         
     | 
| 
      
 140 
     | 
    
         
            +
                                detail="Either 'ulid' or 'conditions' must be provided.",
         
     | 
| 
      
 141 
     | 
    
         
            +
                                source_function="database.update_entity"
         
     | 
| 
      
 142 
     | 
    
         
            +
                            )
         
     | 
| 
       33 
143 
     | 
    
         | 
| 
      
 144 
     | 
    
         
            +
                        # ulid로 조건 생성
         
     | 
| 
      
 145 
     | 
    
         
            +
                        if ulid:
         
     | 
| 
      
 146 
     | 
    
         
            +
                            conditions = {"ulid": ulid}
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                        conditions["is_deleted"] = False
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                        return await self.repository.delete(
         
     | 
| 
      
 151 
     | 
    
         
            +
                            conditions=conditions
         
     | 
| 
      
 152 
     | 
    
         
            +
                        )
         
     | 
| 
      
 153 
     | 
    
         
            +
                    except CustomException as e:
         
     | 
| 
      
 154 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 155 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 156 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 157 
     | 
    
         
            +
                            ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 158 
     | 
    
         
            +
                            detail=str(e),
         
     | 
| 
      
 159 
     | 
    
         
            +
                            source_function=f"{self.__class__.__name__}.delete",
         
     | 
| 
      
 160 
     | 
    
         
            +
                            original_error=e
         
     | 
| 
      
 161 
     | 
    
         
            +
                        )
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
                #########################
         
     | 
| 
      
 164 
     | 
    
         
            +
                # 조회 및 검색 메서드 #
         
     | 
| 
      
 165 
     | 
    
         
            +
                #########################
         
     | 
| 
       34 
166 
     | 
    
         
             
                async def list(
         
     | 
| 
       35 
167 
     | 
    
         
             
                    self,
         
     | 
| 
       36 
168 
     | 
    
         
             
                    skip: int = 0,
         
     | 
| 
       37 
169 
     | 
    
         
             
                    limit: int = 100,
         
     | 
| 
       38 
     | 
    
         
            -
                    filters: Dict[str, Any] | None = None,
         
     | 
| 
       39 
     | 
    
         
            -
                    search_params: Dict[str, Any] | None = None,
         
     | 
| 
      
 170 
     | 
    
         
            +
                    filters: List[Dict[str, Any]] | None = None,
         
     | 
| 
       40 
171 
     | 
    
         
             
                    model_name: str | None = None,
         
     | 
| 
       41 
     | 
    
         
            -
                    response_model: Any = None
         
     | 
| 
      
 172 
     | 
    
         
            +
                    response_model: Any = None,
         
     | 
| 
      
 173 
     | 
    
         
            +
                    explicit_joins: Optional[List[Any]] = None,
         
     | 
| 
      
 174 
     | 
    
         
            +
                    loading_joins: Optional[List[Any]] = None
         
     | 
| 
       42 
175 
     | 
    
         
             
                ) -> List[Dict[str, Any]]:
         
     | 
| 
       43 
176 
     | 
    
         
             
                    try:
         
     | 
| 
       44 
     | 
    
         
            -
                        # 검색 조건 처리 및 필터 병합
         
     | 
| 
       45 
     | 
    
         
            -
                        if search_params:
         
     | 
| 
       46 
     | 
    
         
            -
                            search_filters = build_search_filters(search_params)
         
     | 
| 
       47 
     | 
    
         
            -
                            if filters:
         
     | 
| 
       48 
     | 
    
         
            -
                                # 기존 filters와 search_filters 병합 (search_filters가 우선 적용)
         
     | 
| 
       49 
     | 
    
         
            -
                                filters.update(search_filters)
         
     | 
| 
       50 
     | 
    
         
            -
                            else:
         
     | 
| 
       51 
     | 
    
         
            -
                                filters = search_filters
         
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
177 
     | 
    
         
             
                        # 모델 이름을 통한 동적 처리
         
     | 
| 
       54 
178 
     | 
    
         
             
                        if model_name:
         
     | 
| 
       55 
179 
     | 
    
         
             
                            if model_name not in self.additional_models:
         
     | 
| 
         @@ -59,19 +183,74 @@ class BaseService(Generic[ModelType]): 
     | 
|
| 
       59 
183 
     | 
    
         
             
                                    source_function=f"{self.__class__.__name__}.list"
         
     | 
| 
       60 
184 
     | 
    
         
             
                                )
         
     | 
| 
       61 
185 
     | 
    
         
             
                            model = self.additional_models[model_name]
         
     | 
| 
       62 
     | 
    
         
            -
                             
     | 
| 
      
 186 
     | 
    
         
            +
                            entities = await self.repository.list(
         
     | 
| 
      
 187 
     | 
    
         
            +
                                skip=skip,
         
     | 
| 
      
 188 
     | 
    
         
            +
                                limit=limit,
         
     | 
| 
      
 189 
     | 
    
         
            +
                                filters=filters,
         
     | 
| 
      
 190 
     | 
    
         
            +
                                model=model
         
     | 
| 
      
 191 
     | 
    
         
            +
                            )
         
     | 
| 
      
 192 
     | 
    
         
            +
                            return [process_response(entity, response_model) for entity in entities]
         
     | 
| 
       63 
193 
     | 
    
         | 
| 
       64 
     | 
    
         
            -
                         
     | 
| 
      
 194 
     | 
    
         
            +
                        entities = await self.repository.list(
         
     | 
| 
      
 195 
     | 
    
         
            +
                            skip=skip,
         
     | 
| 
      
 196 
     | 
    
         
            +
                            limit=limit,
         
     | 
| 
      
 197 
     | 
    
         
            +
                            filters=filters,
         
     | 
| 
      
 198 
     | 
    
         
            +
                            explicit_joins=explicit_joins,
         
     | 
| 
      
 199 
     | 
    
         
            +
                            loading_joins=loading_joins
         
     | 
| 
      
 200 
     | 
    
         
            +
                        )
         
     | 
| 
      
 201 
     | 
    
         
            +
                        return [process_response(entity, response_model) for entity in entities]
         
     | 
| 
      
 202 
     | 
    
         
            +
                    
         
     | 
| 
       65 
203 
     | 
    
         
             
                    except CustomException as e:
         
     | 
| 
       66 
     | 
    
         
            -
                        e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
         
     | 
| 
       67 
     | 
    
         
            -
                        e.source_function = f"{self.__class__.__name__}.list -> {e.source_function}"
         
     | 
| 
       68 
204 
     | 
    
         
             
                        raise e
         
     | 
| 
       69 
205 
     | 
    
         
             
                    except Exception as e:
         
     | 
| 
       70 
206 
     | 
    
         
             
                        raise CustomException(
         
     | 
| 
       71 
207 
     | 
    
         
             
                            ErrorCode.INTERNAL_ERROR,
         
     | 
| 
       72 
     | 
    
         
            -
                            detail=str(e),
         
     | 
| 
       73 
208 
     | 
    
         
             
                            source_function=f"{self.__class__.__name__}.list",
         
     | 
| 
       74 
209 
     | 
    
         
             
                            original_error=e
         
     | 
| 
       75 
210 
     | 
    
         
             
                        )
         
     | 
| 
      
 211 
     | 
    
         
            +
                    
         
     | 
| 
      
 212 
     | 
    
         
            +
                async def get(
         
     | 
| 
      
 213 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 214 
     | 
    
         
            +
                    ulid: str,
         
     | 
| 
      
 215 
     | 
    
         
            +
                    model_name: str | None = None,
         
     | 
| 
      
 216 
     | 
    
         
            +
                    response_model: Any = None,
         
     | 
| 
      
 217 
     | 
    
         
            +
                    conditions: Dict[str, Any] | None = None,
         
     | 
| 
      
 218 
     | 
    
         
            +
                    explicit_joins: Optional[List[Any]] = None,
         
     | 
| 
      
 219 
     | 
    
         
            +
                    loading_joins: Optional[List[Any]] = None
         
     | 
| 
      
 220 
     | 
    
         
            +
                ):
         
     | 
| 
      
 221 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 222 
     | 
    
         
            +
                        if not ulid and not conditions:
         
     | 
| 
      
 223 
     | 
    
         
            +
                            raise CustomException(
         
     | 
| 
      
 224 
     | 
    
         
            +
                                ErrorCode.INVALID_INPUT,
         
     | 
| 
      
 225 
     | 
    
         
            +
                                detail="Either 'ulid' or 'conditions' must be provided.",
         
     | 
| 
      
 226 
     | 
    
         
            +
                                source_function="database.update_entity"
         
     | 
| 
      
 227 
     | 
    
         
            +
                            )
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
                        # ulid로 조건 생성
         
     | 
| 
      
 230 
     | 
    
         
            +
                        if ulid:
         
     | 
| 
      
 231 
     | 
    
         
            +
                            if not ULID.from_str(ulid):
         
     | 
| 
      
 232 
     | 
    
         
            +
                                raise CustomException(
         
     | 
| 
      
 233 
     | 
    
         
            +
                                    ErrorCode.VALIDATION_ERROR,
         
     | 
| 
      
 234 
     | 
    
         
            +
                                    detail=ulid,
         
     | 
| 
      
 235 
     | 
    
         
            +
                                    source_function=f"{self.__class__.__name__}.update"
         
     | 
| 
      
 236 
     | 
    
         
            +
                                )
         
     | 
| 
      
 237 
     | 
    
         
            +
                            
         
     | 
| 
      
 238 
     | 
    
         
            +
                            conditions = {"ulid": ulid}
         
     | 
| 
      
 239 
     | 
    
         
            +
                        
         
     | 
| 
      
 240 
     | 
    
         
            +
                        entity = await self.repository.get(
         
     | 
| 
      
 241 
     | 
    
         
            +
                            conditions=conditions,
         
     | 
| 
      
 242 
     | 
    
         
            +
                            explicit_joins=explicit_joins,
         
     | 
| 
      
 243 
     | 
    
         
            +
                            loading_joins=loading_joins
         
     | 
| 
      
 244 
     | 
    
         
            +
                        )
         
     | 
| 
      
 245 
     | 
    
         
            +
                        return process_response(entity, response_model)
         
     | 
| 
       76 
246 
     | 
    
         | 
| 
      
 247 
     | 
    
         
            +
                    except CustomException as e:
         
     | 
| 
      
 248 
     | 
    
         
            +
                        raise e
         
     | 
| 
      
 249 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 250 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 251 
     | 
    
         
            +
                            ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 252 
     | 
    
         
            +
                            detail=str(e),
         
     | 
| 
      
 253 
     | 
    
         
            +
                            source_function=f"{self.__class__.__name__}.get",
         
     | 
| 
      
 254 
     | 
    
         
            +
                            original_error=e
         
     | 
| 
      
 255 
     | 
    
         
            +
                        )
         
     | 
| 
       77 
256 
     | 
    
         | 
    
        aiteamutils/database.py
    CHANGED
    
    | 
         @@ -1,22 +1,100 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            #기본 라이브러리
         
     | 
| 
       2 
     | 
    
         
            -
            from typing import  
     | 
| 
      
 2 
     | 
    
         
            +
            from typing import (
         
     | 
| 
      
 3 
     | 
    
         
            +
                TypeVar, 
         
     | 
| 
      
 4 
     | 
    
         
            +
                Generic, 
         
     | 
| 
      
 5 
     | 
    
         
            +
                Type, 
         
     | 
| 
      
 6 
     | 
    
         
            +
                Any, 
         
     | 
| 
      
 7 
     | 
    
         
            +
                Dict, 
         
     | 
| 
      
 8 
     | 
    
         
            +
                List, 
         
     | 
| 
      
 9 
     | 
    
         
            +
                Optional, 
         
     | 
| 
      
 10 
     | 
    
         
            +
                AsyncGenerator
         
     | 
| 
      
 11 
     | 
    
         
            +
            )
         
     | 
| 
       3 
12 
     | 
    
         
             
            from sqlalchemy.ext.asyncio import AsyncSession
         
     | 
| 
       4 
     | 
    
         
            -
            from sqlalchemy import select, and_
         
     | 
| 
       5 
     | 
    
         
            -
            from sqlalchemy.orm import DeclarativeBase
         
     | 
| 
      
 13 
     | 
    
         
            +
            from sqlalchemy import select, and_, or_
         
     | 
| 
      
 14 
     | 
    
         
            +
            from sqlalchemy.orm import DeclarativeBase, joinedload, selectinload
         
     | 
| 
       6 
15 
     | 
    
         
             
            from sqlalchemy.exc import SQLAlchemyError
         
     | 
| 
       7 
16 
     | 
    
         
             
            from datetime import datetime
         
     | 
| 
      
 17 
     | 
    
         
            +
            from contextlib import asynccontextmanager
         
     | 
| 
      
 18 
     | 
    
         
            +
            from sqlalchemy.ext.asyncio import AsyncSession
         
     | 
| 
      
 19 
     | 
    
         
            +
            from fastapi import Request
         
     | 
| 
      
 20 
     | 
    
         
            +
            from ulid import ULID
         
     | 
| 
      
 21 
     | 
    
         
            +
            from sqlalchemy import MetaData, Table, insert
         
     | 
| 
       8 
22 
     | 
    
         | 
| 
       9 
23 
     | 
    
         
             
            #패키지 라이브러리
         
     | 
| 
       10 
24 
     | 
    
         
             
            from .exceptions import ErrorCode, CustomException
         
     | 
| 
       11 
25 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
26 
     | 
    
         
             
            ModelType = TypeVar("ModelType", bound=DeclarativeBase)
         
     | 
| 
       14 
27 
     | 
    
         | 
| 
       15 
28 
     | 
    
         
             
            ##################
         
     | 
| 
       16 
29 
     | 
    
         
             
            # 전처리 #
         
     | 
| 
       17 
30 
     | 
    
         
             
            ##################
         
     | 
| 
      
 31 
     | 
    
         
            +
            def process_entity_data(
         
     | 
| 
      
 32 
     | 
    
         
            +
                model: Type[ModelType],
         
     | 
| 
      
 33 
     | 
    
         
            +
                entity_data: Dict[str, Any],
         
     | 
| 
      
 34 
     | 
    
         
            +
                existing_data: Dict[str, Any] = None,
         
     | 
| 
      
 35 
     | 
    
         
            +
                exclude_entities: List[str] | None = None
         
     | 
| 
      
 36 
     | 
    
         
            +
            ) -> Dict[str, Any]:
         
     | 
| 
      
 37 
     | 
    
         
            +
                from .security import hash_password
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                """
         
     | 
| 
      
 40 
     | 
    
         
            +
                엔티티 데이터를 전처리하고 모델 속성과 extra_data를 분리합니다.
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                이 함수는 다음과 같은 작업을 수행합니다:
         
     | 
| 
      
 43 
     | 
    
         
            +
                1. 모델의 기본 속성을 식별합니다.
         
     | 
| 
      
 44 
     | 
    
         
            +
                2. Swagger 자동 생성 속성을 제외합니다.
         
     | 
| 
      
 45 
     | 
    
         
            +
                3. 모델 속성에 해당하는 데이터는 model_data에 저장
         
     | 
| 
      
 46 
     | 
    
         
            +
                4. 모델 속성에 없는 데이터는 extra_data에 저장
         
     | 
| 
      
 47 
     | 
    
         
            +
                5. 기존 엔티티 데이터의 extra_data를 유지할 수 있습니다.
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 50 
     | 
    
         
            +
                    model (Type[ModelType]): 데이터 모델 클래스
         
     | 
| 
      
 51 
     | 
    
         
            +
                    entity_data (Dict[str, Any]): 처리할 엔티티 데이터
         
     | 
| 
      
 52 
     | 
    
         
            +
                    existing_entity_data (Dict[str, Any], optional): 기존 엔티티 데이터. Defaults to None.
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 55 
     | 
    
         
            +
                    Dict[str, Any]: 전처리된 모델 데이터 (extra_data 포함)
         
     | 
| 
      
 56 
     | 
    
         
            +
                """
         
     | 
| 
      
 57 
     | 
    
         
            +
                # 모델의 속성을 가져와서 전처리합니다.
         
     | 
| 
      
 58 
     | 
    
         
            +
                model_attr = {
         
     | 
| 
      
 59 
     | 
    
         
            +
                    attr for attr in dir(model)
         
     | 
| 
      
 60 
     | 
    
         
            +
                    if not attr.startswith('_') and not callable(getattr(model, attr))
         
     | 
| 
      
 61 
     | 
    
         
            +
                }
         
     | 
| 
      
 62 
     | 
    
         
            +
                model_data = {}
         
     | 
| 
      
 63 
     | 
    
         
            +
                extra_data = {}
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                # 기존 엔티티 데이터가 있으면 추가
         
     | 
| 
      
 66 
     | 
    
         
            +
                if existing_data and "extra_data" in existing_data:
         
     | 
| 
      
 67 
     | 
    
         
            +
                    extra_data = existing_data["extra_data"].copy()
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                # 제외할 엔티티가 있으면 제거
         
     | 
| 
      
 70 
     | 
    
         
            +
                if exclude_entities:
         
     | 
| 
      
 71 
     | 
    
         
            +
                    entity_data = {k: v for k, v in entity_data.items() if k not in exclude_entities}
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                # Swagger 자동 생성 속성 패턴
         
     | 
| 
      
 74 
     | 
    
         
            +
                swagger_patterns = {"additionalProp1", "additionalProp2", "additionalProp3"}
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                for key, value in entity_data.items():
         
     | 
| 
      
 77 
     | 
    
         
            +
                    # Swagger 자동 생성 속성 무시
         
     | 
| 
      
 78 
     | 
    
         
            +
                    if key in swagger_patterns:
         
     | 
| 
      
 79 
     | 
    
         
            +
                        continue
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                    # 패스워드 필드 처리
         
     | 
| 
      
 82 
     | 
    
         
            +
                    if key == "password":
         
     | 
| 
      
 83 
     | 
    
         
            +
                        value = hash_password(value)  # 비밀번호 암호화
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                    # 모델 속성에 있는 경우 model_data에 추가
         
     | 
| 
      
 86 
     | 
    
         
            +
                    if key in model_attr:
         
     | 
| 
      
 87 
     | 
    
         
            +
                        model_data[key] = value
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                    # 모델 속성에 없는 경우 extra_data에 추가
         
     | 
| 
      
 90 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 91 
     | 
    
         
            +
                        extra_data[key] = value
         
     | 
| 
       18 
92 
     | 
    
         | 
| 
      
 93 
     | 
    
         
            +
                # extra_data가 있고 모델에 extra_data 속성이 있는 경우 추가
         
     | 
| 
      
 94 
     | 
    
         
            +
                if extra_data and "extra_data" in model_attr:
         
     | 
| 
      
 95 
     | 
    
         
            +
                    model_data["extra_data"] = extra_data
         
     | 
| 
       19 
96 
     | 
    
         | 
| 
      
 97 
     | 
    
         
            +
                return model_data
         
     | 
| 
       20 
98 
     | 
    
         | 
| 
       21 
99 
     | 
    
         
             
            ##################
         
     | 
| 
       22 
100 
     | 
    
         
             
            # 응답 처리 #
         
     | 
| 
         @@ -115,79 +193,224 @@ def process_response( 
     | 
|
| 
       115 
193 
     | 
    
         | 
| 
       116 
194 
     | 
    
         
             
                return result
         
     | 
| 
       117 
195 
     | 
    
         | 
| 
       118 
     | 
    
         
            -
             
     | 
| 
       119 
196 
     | 
    
         
             
            ##################
         
     | 
| 
       120 
197 
     | 
    
         
             
            # 조건 처리 #
         
     | 
| 
       121 
198 
     | 
    
         
             
            ##################
         
     | 
| 
       122 
     | 
    
         
            -
            def build_search_filters(
         
     | 
| 
       123 
     | 
    
         
            -
                    search_params: Dict[str, Dict[str, Any]]
         
     | 
| 
       124 
     | 
    
         
            -
            ) -> Dict[str, Any]:
         
     | 
| 
       125 
     | 
    
         
            -
                """
         
     | 
| 
       126 
     | 
    
         
            -
                검색 파라미터를 기반으로 필터 조건을 생성합니다.
         
     | 
| 
       127 
     | 
    
         
            -
             
     | 
| 
       128 
     | 
    
         
            -
                Args:
         
     | 
| 
       129 
     | 
    
         
            -
                    search_params: 검색 조건 설정을 위한 파라미터.
         
     | 
| 
       130 
     | 
    
         
            -
             
     | 
| 
       131 
     | 
    
         
            -
                Returns:
         
     | 
| 
       132 
     | 
    
         
            -
                    filters: 필터 조건 딕셔너리.
         
     | 
| 
       133 
     | 
    
         
            -
                """
         
     | 
| 
       134 
     | 
    
         
            -
                filters = {}
         
     | 
| 
       135 
     | 
    
         
            -
                for key, param in search_params.items():
         
     | 
| 
       136 
     | 
    
         
            -
                    value = param.get("value")
         
     | 
| 
       137 
     | 
    
         
            -
                    if value is not None:
         
     | 
| 
       138 
     | 
    
         
            -
                        if param.get("like", False):
         
     | 
| 
       139 
     | 
    
         
            -
                            filters[key] = {"field": param["fields"][0], "operator": "like", "value": f"%{value}%"}
         
     | 
| 
       140 
     | 
    
         
            -
                        else:
         
     | 
| 
       141 
     | 
    
         
            -
                            filters[key] = {"field": param["fields"][0], "operator": "eq", "value": value}
         
     | 
| 
       142 
     | 
    
         
            -
                return filters
         
     | 
| 
       143 
     | 
    
         
            -
             
     | 
| 
       144 
199 
     | 
    
         
             
            def build_conditions(
         
     | 
| 
       145 
     | 
    
         
            -
             
     | 
| 
       146 
     | 
    
         
            -
             
     | 
| 
      
 200 
     | 
    
         
            +
                filters: List[Dict[str, Any]],
         
     | 
| 
      
 201 
     | 
    
         
            +
                model: Type[ModelType]
         
     | 
| 
       147 
202 
     | 
    
         
             
            ) -> List[Any]:
         
     | 
| 
       148 
203 
     | 
    
         
             
                """
         
     | 
| 
       149 
204 
     | 
    
         
             
                필터 조건을 기반으로 SQLAlchemy 조건 리스트를 생성합니다.
         
     | 
| 
       150 
205 
     | 
    
         | 
| 
       151 
206 
     | 
    
         
             
                Args:
         
     | 
| 
       152 
     | 
    
         
            -
                    filters: 필터 조건  
     | 
| 
      
 207 
     | 
    
         
            +
                    filters: 필터 조건 리스트.
         
     | 
| 
       153 
208 
     | 
    
         
             
                    model: SQLAlchemy 모델 클래스.
         
     | 
| 
       154 
209 
     | 
    
         | 
| 
       155 
210 
     | 
    
         
             
                Returns:
         
     | 
| 
       156 
211 
     | 
    
         
             
                    List[Any]: SQLAlchemy 조건 리스트.
         
     | 
| 
       157 
212 
     | 
    
         
             
                """
         
     | 
| 
       158 
213 
     | 
    
         
             
                conditions = []
         
     | 
| 
       159 
     | 
    
         
            -
                for filter_data in filters.values():
         
     | 
| 
       160 
     | 
    
         
            -
                    if "." in filter_data["field"]:
         
     | 
| 
       161 
     | 
    
         
            -
                        # 관계를 따라 암묵적으로 연결된 모델의 필드 가져오기
         
     | 
| 
       162 
     | 
    
         
            -
                        related_model_name, field_name = filter_data["field"].split(".")
         
     | 
| 
       163 
     | 
    
         
            -
                        relationship_property = getattr(model, related_model_name)
         
     | 
| 
       164 
     | 
    
         
            -
                        related_model = relationship_property.property.mapper.class_
         
     | 
| 
       165 
     | 
    
         
            -
                        field = getattr(related_model, field_name)
         
     | 
| 
       166 
     | 
    
         
            -
                    else:
         
     | 
| 
       167 
     | 
    
         
            -
                        # 현재 모델의 필드 가져오기
         
     | 
| 
       168 
     | 
    
         
            -
                        field = getattr(model, filter_data["field"])
         
     | 
| 
       169 
214 
     | 
    
         | 
| 
       170 
     | 
    
         
            -
             
     | 
| 
       171 
     | 
    
         
            -
                     
     | 
| 
       172 
     | 
    
         
            -
                    value  
     | 
| 
      
 215 
     | 
    
         
            +
                for filter_item in filters:
         
     | 
| 
      
 216 
     | 
    
         
            +
                    value = filter_item.get("value")
         
     | 
| 
      
 217 
     | 
    
         
            +
                    if not value:  # 값이 없으면 건너뜀
         
     | 
| 
      
 218 
     | 
    
         
            +
                        continue
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
                    operator = filter_item.get("operator", "eq")
         
     | 
| 
      
 221 
     | 
    
         
            +
                    or_conditions = []
         
     | 
| 
      
 222 
     | 
    
         
            +
             
     | 
| 
      
 223 
     | 
    
         
            +
                    for field_path in filter_item.get("fields", []):
         
     | 
| 
      
 224 
     | 
    
         
            +
                        current_model = model
         
     | 
| 
       173 
225 
     | 
    
         | 
| 
       174 
     | 
    
         
            -
             
     | 
| 
       175 
     | 
    
         
            -
                         
     | 
| 
       176 
     | 
    
         
            -
             
     | 
| 
       177 
     | 
    
         
            -
             
     | 
| 
      
 226 
     | 
    
         
            +
                        # 관계를 따라 필드 가져오기
         
     | 
| 
      
 227 
     | 
    
         
            +
                        for part in field_path.split(".")[:-1]:
         
     | 
| 
      
 228 
     | 
    
         
            +
                            relationship_property = getattr(current_model, part)
         
     | 
| 
      
 229 
     | 
    
         
            +
                            current_model = relationship_property.property.mapper.class_
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
                        field = getattr(current_model, field_path.split(".")[-1])
         
     | 
| 
      
 232 
     | 
    
         
            +
             
     | 
| 
      
 233 
     | 
    
         
            +
                        # 조건 생성
         
     | 
| 
      
 234 
     | 
    
         
            +
                        if operator == "like":
         
     | 
| 
      
 235 
     | 
    
         
            +
                            or_conditions.append(field.ilike(f"%{value}%"))
         
     | 
| 
      
 236 
     | 
    
         
            +
                        elif operator == "eq":
         
     | 
| 
      
 237 
     | 
    
         
            +
                            or_conditions.append(field == value)
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
                    if or_conditions:  # OR 조건이 있을 때만 추가
         
     | 
| 
      
 240 
     | 
    
         
            +
                        conditions.append(or_(*or_conditions))
         
     | 
| 
       178 
241 
     | 
    
         | 
| 
       179 
242 
     | 
    
         
             
                return conditions
         
     | 
| 
       180 
243 
     | 
    
         | 
| 
       181 
244 
     | 
    
         
             
            ##################
         
     | 
| 
       182 
245 
     | 
    
         
             
            # 쿼리 실행 #
         
     | 
| 
       183 
246 
     | 
    
         
             
            ##################
         
     | 
| 
      
 247 
     | 
    
         
            +
            async def create_entity(
         
     | 
| 
      
 248 
     | 
    
         
            +
                session: AsyncSession,
         
     | 
| 
      
 249 
     | 
    
         
            +
                model: Type[ModelType],
         
     | 
| 
      
 250 
     | 
    
         
            +
                entity_data: Dict[str, Any],
         
     | 
| 
      
 251 
     | 
    
         
            +
                exclude_entities: List[str] | None = None
         
     | 
| 
      
 252 
     | 
    
         
            +
            ) -> ModelType:
         
     | 
| 
      
 253 
     | 
    
         
            +
                """
         
     | 
| 
      
 254 
     | 
    
         
            +
                새로운 엔티티를 데이터베이스에 생성합니다.
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 257 
     | 
    
         
            +
                    session (AsyncSession): 데이터베이스 세션
         
     | 
| 
      
 258 
     | 
    
         
            +
                    model (Type[ModelType]): 생성할 모델 클래스
         
     | 
| 
      
 259 
     | 
    
         
            +
                    entity_data (Dict[str, Any]): 엔티티 생성에 필요한 데이터
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 262 
     | 
    
         
            +
                    ModelType: 생성된 엔티티
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
                Raises:
         
     | 
| 
      
 265 
     | 
    
         
            +
                    CustomException: 엔티티 생성 중 발생하는 데이터베이스 오류
         
     | 
| 
      
 266 
     | 
    
         
            +
                """
         
     | 
| 
      
 267 
     | 
    
         
            +
                try:
         
     | 
| 
      
 268 
     | 
    
         
            +
                    # 엔티티 데이터 전처리
         
     | 
| 
      
 269 
     | 
    
         
            +
                    processed_data = process_entity_data(
         
     | 
| 
      
 270 
     | 
    
         
            +
                        model=model,
         
     | 
| 
      
 271 
     | 
    
         
            +
                        entity_data=entity_data,
         
     | 
| 
      
 272 
     | 
    
         
            +
                        exclude_entities=exclude_entities
         
     | 
| 
      
 273 
     | 
    
         
            +
                    )
         
     | 
| 
      
 274 
     | 
    
         
            +
                    
         
     | 
| 
      
 275 
     | 
    
         
            +
                    # 새로운 엔티티 생성
         
     | 
| 
      
 276 
     | 
    
         
            +
                    entity = model(**processed_data)
         
     | 
| 
      
 277 
     | 
    
         
            +
                    
         
     | 
| 
      
 278 
     | 
    
         
            +
                    # 세션에 엔티티 추가
         
     | 
| 
      
 279 
     | 
    
         
            +
                    session.add(entity)
         
     | 
| 
      
 280 
     | 
    
         
            +
                    
         
     | 
| 
      
 281 
     | 
    
         
            +
                    # 데이터베이스에 커밋
         
     | 
| 
      
 282 
     | 
    
         
            +
                    await session.flush()
         
     | 
| 
      
 283 
     | 
    
         
            +
                    await session.refresh(entity)
         
     | 
| 
      
 284 
     | 
    
         
            +
                    
         
     | 
| 
      
 285 
     | 
    
         
            +
                    # 생성된 엔티티 반환
         
     | 
| 
      
 286 
     | 
    
         
            +
                    return entity
         
     | 
| 
      
 287 
     | 
    
         
            +
                
         
     | 
| 
      
 288 
     | 
    
         
            +
                except SQLAlchemyError as e:
         
     | 
| 
      
 289 
     | 
    
         
            +
                    # 데이터베이스 오류 발생 시 CustomException으로 변환
         
     | 
| 
      
 290 
     | 
    
         
            +
                    raise CustomException(
         
     | 
| 
      
 291 
     | 
    
         
            +
                        ErrorCode.DB_CREATE_ERROR,
         
     | 
| 
      
 292 
     | 
    
         
            +
                        detail=f"{model.__name__}|{str(e)}",
         
     | 
| 
      
 293 
     | 
    
         
            +
                        source_function="database.create_entity",
         
     | 
| 
      
 294 
     | 
    
         
            +
                        original_error=e
         
     | 
| 
      
 295 
     | 
    
         
            +
                    )
         
     | 
| 
      
 296 
     | 
    
         
            +
             
     | 
| 
      
 297 
     | 
    
         
            +
            async def update_entity(
         
     | 
| 
      
 298 
     | 
    
         
            +
                session: AsyncSession,
         
     | 
| 
      
 299 
     | 
    
         
            +
                model: Type[ModelType],
         
     | 
| 
      
 300 
     | 
    
         
            +
                conditions: Dict[str, Any],
         
     | 
| 
      
 301 
     | 
    
         
            +
                update_data: Dict[str, Any]
         
     | 
| 
      
 302 
     | 
    
         
            +
            ) -> ModelType:
         
     | 
| 
      
 303 
     | 
    
         
            +
                """
         
     | 
| 
      
 304 
     | 
    
         
            +
                조건을 기반으로 엔티티를 조회하고 업데이트합니다.
         
     | 
| 
      
 305 
     | 
    
         
            +
             
     | 
| 
      
 306 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 307 
     | 
    
         
            +
                    session (AsyncSession): 데이터베이스 세션
         
     | 
| 
      
 308 
     | 
    
         
            +
                    model (Type[ModelType]): 업데이트할 모델 클래스
         
     | 
| 
      
 309 
     | 
    
         
            +
                    conditions (Dict[str, Any]): 엔티티 조회 조건 
         
     | 
| 
      
 310 
     | 
    
         
            +
                        conditions = {"user_id": 1, "status": "active"}
         
     | 
| 
      
 311 
     | 
    
         
            +
                    update_data (Dict[str, Any]): 업데이트할 데이터
         
     | 
| 
      
 312 
     | 
    
         
            +
                        update_data = {"status": "inactive"}
         
     | 
| 
      
 313 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 314 
     | 
    
         
            +
                    ModelType: 업데이트된 엔티티
         
     | 
| 
      
 315 
     | 
    
         
            +
             
     | 
| 
      
 316 
     | 
    
         
            +
                Raises:
         
     | 
| 
      
 317 
     | 
    
         
            +
                    CustomException: 엔티티 조회 또는 업데이트 중 발생하는 데이터베이스 오류
         
     | 
| 
      
 318 
     | 
    
         
            +
                """
         
     | 
| 
      
 319 
     | 
    
         
            +
                try:
         
     | 
| 
      
 320 
     | 
    
         
            +
                    # 조건 기반 엔티티 조회
         
     | 
| 
      
 321 
     | 
    
         
            +
                    stmt = select(model)
         
     | 
| 
      
 322 
     | 
    
         
            +
                    for key, value in conditions.items():
         
     | 
| 
      
 323 
     | 
    
         
            +
                        stmt = stmt.where(getattr(model, key) == value)
         
     | 
| 
      
 324 
     | 
    
         
            +
             
     | 
| 
      
 325 
     | 
    
         
            +
                    result = await session.execute(stmt)
         
     | 
| 
      
 326 
     | 
    
         
            +
                    entity = result.scalar_one_or_none()
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
                    if not entity:
         
     | 
| 
      
 329 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 330 
     | 
    
         
            +
                            ErrorCode.NOT_FOUND,
         
     | 
| 
      
 331 
     | 
    
         
            +
                            detail=f"{model.__name__}|{conditions}.",
         
     | 
| 
      
 332 
     | 
    
         
            +
                            source_function="database.update_entity"
         
     | 
| 
      
 333 
     | 
    
         
            +
                        )
         
     | 
| 
      
 334 
     | 
    
         
            +
             
     | 
| 
      
 335 
     | 
    
         
            +
                    # 기존 데이터를 딕셔너리로 변환
         
     | 
| 
      
 336 
     | 
    
         
            +
                    existing_data = {
         
     | 
| 
      
 337 
     | 
    
         
            +
                        column.name: getattr(entity, column.name)
         
     | 
| 
      
 338 
     | 
    
         
            +
                        for column in entity.__table__.columns
         
     | 
| 
      
 339 
     | 
    
         
            +
                    }
         
     | 
| 
      
 340 
     | 
    
         
            +
             
     | 
| 
      
 341 
     | 
    
         
            +
                    # 데이터 병합 및 전처리
         
     | 
| 
      
 342 
     | 
    
         
            +
                    processed_data = process_entity_data(model, update_data, existing_data)
         
     | 
| 
      
 343 
     | 
    
         
            +
             
     | 
| 
      
 344 
     | 
    
         
            +
                    # 엔티티 데이터 업데이트
         
     | 
| 
      
 345 
     | 
    
         
            +
                    for key, value in processed_data.items():
         
     | 
| 
      
 346 
     | 
    
         
            +
                        if hasattr(entity, key):
         
     | 
| 
      
 347 
     | 
    
         
            +
                            setattr(entity, key, value)
         
     | 
| 
      
 348 
     | 
    
         
            +
             
     | 
| 
      
 349 
     | 
    
         
            +
                    # 변경 사항 커밋
         
     | 
| 
      
 350 
     | 
    
         
            +
                    await session.flush()
         
     | 
| 
      
 351 
     | 
    
         
            +
                    await session.refresh(entity)
         
     | 
| 
      
 352 
     | 
    
         
            +
             
     | 
| 
      
 353 
     | 
    
         
            +
                    return entity
         
     | 
| 
      
 354 
     | 
    
         
            +
             
     | 
| 
      
 355 
     | 
    
         
            +
                except SQLAlchemyError as e:
         
     | 
| 
      
 356 
     | 
    
         
            +
                    raise CustomException(
         
     | 
| 
      
 357 
     | 
    
         
            +
                        ErrorCode.DB_UPDATE_ERROR,
         
     | 
| 
      
 358 
     | 
    
         
            +
                        detail=f"{model.__name__}|{conditions}",
         
     | 
| 
      
 359 
     | 
    
         
            +
                        source_function="database.update_entity",
         
     | 
| 
      
 360 
     | 
    
         
            +
                        original_error=e
         
     | 
| 
      
 361 
     | 
    
         
            +
                    )
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
            async def delete_entity(
         
     | 
| 
      
 364 
     | 
    
         
            +
                session: AsyncSession,
         
     | 
| 
      
 365 
     | 
    
         
            +
                model: Type[ModelType],
         
     | 
| 
      
 366 
     | 
    
         
            +
                conditions: Dict[str, Any]
         
     | 
| 
      
 367 
     | 
    
         
            +
            ) -> None:
         
     | 
| 
      
 368 
     | 
    
         
            +
                try:
         
     | 
| 
      
 369 
     | 
    
         
            +
                    stmt = select(model)
         
     | 
| 
      
 370 
     | 
    
         
            +
                    for key, value in conditions.items():
         
     | 
| 
      
 371 
     | 
    
         
            +
                        stmt = stmt.where(getattr(model, key) == value)
         
     | 
| 
      
 372 
     | 
    
         
            +
             
     | 
| 
      
 373 
     | 
    
         
            +
                    result = await session.execute(stmt)
         
     | 
| 
      
 374 
     | 
    
         
            +
                    entity = result.scalar_one_or_none()
         
     | 
| 
      
 375 
     | 
    
         
            +
             
     | 
| 
      
 376 
     | 
    
         
            +
                    if not entity:
         
     | 
| 
      
 377 
     | 
    
         
            +
                        raise CustomException(
         
     | 
| 
      
 378 
     | 
    
         
            +
                            ErrorCode.NOT_FOUND,
         
     | 
| 
      
 379 
     | 
    
         
            +
                            detail=f"{model.__name__}|{conditions}.",
         
     | 
| 
      
 380 
     | 
    
         
            +
                            source_function="database.delete_entity"
         
     | 
| 
      
 381 
     | 
    
         
            +
                        )
         
     | 
| 
      
 382 
     | 
    
         
            +
             
     | 
| 
      
 383 
     | 
    
         
            +
                    entity.is_deleted = True
         
     | 
| 
      
 384 
     | 
    
         
            +
                    entity.deleted_at = datetime.now()
         
     | 
| 
      
 385 
     | 
    
         
            +
             
     | 
| 
      
 386 
     | 
    
         
            +
                    await session.flush()
         
     | 
| 
      
 387 
     | 
    
         
            +
                    await session.refresh(entity)
         
     | 
| 
      
 388 
     | 
    
         
            +
             
     | 
| 
      
 389 
     | 
    
         
            +
                except SQLAlchemyError as e:
         
     | 
| 
      
 390 
     | 
    
         
            +
                    raise CustomException(
         
     | 
| 
      
 391 
     | 
    
         
            +
                        ErrorCode.DB_DELETE_ERROR,
         
     | 
| 
      
 392 
     | 
    
         
            +
                        detail=f"{model.__name__}|{conditions}",
         
     | 
| 
      
 393 
     | 
    
         
            +
                        source_function="database.delete_entity",
         
     | 
| 
      
 394 
     | 
    
         
            +
                        original_error=e
         
     | 
| 
      
 395 
     | 
    
         
            +
                    )
         
     | 
| 
      
 396 
     | 
    
         
            +
             
     | 
| 
      
 397 
     | 
    
         
            +
            async def purge_entity(
         
     | 
| 
      
 398 
     | 
    
         
            +
                session: AsyncSession,
         
     | 
| 
      
 399 
     | 
    
         
            +
                model: Type[ModelType],
         
     | 
| 
      
 400 
     | 
    
         
            +
                entity: ModelType
         
     | 
| 
      
 401 
     | 
    
         
            +
            ) -> None:
         
     | 
| 
      
 402 
     | 
    
         
            +
                # 엔티티를 영구 삭제합니다.
         
     | 
| 
      
 403 
     | 
    
         
            +
                await session.delete(entity)
         
     | 
| 
      
 404 
     | 
    
         
            +
                await session.commit()
         
     | 
| 
      
 405 
     | 
    
         
            +
             
     | 
| 
       184 
406 
     | 
    
         
             
            async def list_entities(
         
     | 
| 
       185 
407 
     | 
    
         
             
                session: AsyncSession,
         
     | 
| 
       186 
408 
     | 
    
         
             
                model: Type[ModelType],
         
     | 
| 
       187 
409 
     | 
    
         
             
                skip: int = 0,
         
     | 
| 
       188 
410 
     | 
    
         
             
                limit: int = 100,
         
     | 
| 
       189 
411 
     | 
    
         
             
                filters: Optional[Dict[str, Any]] = None,
         
     | 
| 
       190 
     | 
    
         
            -
                 
     | 
| 
      
 412 
     | 
    
         
            +
                explicit_joins: Optional[List[Any]] = None,
         
     | 
| 
      
 413 
     | 
    
         
            +
                loading_joins: Optional[List[Any]] = None
         
     | 
| 
       191 
414 
     | 
    
         
             
            ) -> List[Dict[str, Any]]:
         
     | 
| 
       192 
415 
     | 
    
         
             
                """
         
     | 
| 
       193 
416 
     | 
    
         
             
                엔터티 리스트를 필터 및 조건에 따라 가져오는 함수.
         
     | 
| 
         @@ -218,20 +441,26 @@ async def list_entities( 
     | 
|
| 
       218 
441 
     | 
    
         
             
                try:
         
     | 
| 
       219 
442 
     | 
    
         
             
                    query = select(model)
         
     | 
| 
       220 
443 
     | 
    
         | 
| 
      
 444 
     | 
    
         
            +
                    # 명시적 조인 적용
         
     | 
| 
      
 445 
     | 
    
         
            +
                    if explicit_joins:
         
     | 
| 
      
 446 
     | 
    
         
            +
                        for join_target in explicit_joins:
         
     | 
| 
      
 447 
     | 
    
         
            +
                            query = query.join(join_target)  # 명시적으로 정의된 조인 추가
         
     | 
| 
      
 448 
     | 
    
         
            +
             
     | 
| 
      
 449 
     | 
    
         
            +
                    # 조인 로딩 적용
         
     | 
| 
      
 450 
     | 
    
         
            +
                    if loading_joins:
         
     | 
| 
      
 451 
     | 
    
         
            +
                        for join_option in loading_joins:
         
     | 
| 
      
 452 
     | 
    
         
            +
                            query = query.options(join_option)
         
     | 
| 
      
 453 
     | 
    
         
            +
             
     | 
| 
       221 
454 
     | 
    
         
             
                    # 필터 조건 적용
         
     | 
| 
       222 
455 
     | 
    
         
             
                    if filters:
         
     | 
| 
       223 
456 
     | 
    
         
             
                        conditions = build_conditions(filters, model)
         
     | 
| 
       224 
457 
     | 
    
         
             
                        query = query.where(and_(*conditions))
         
     | 
| 
       225 
458 
     | 
    
         | 
| 
       226 
     | 
    
         
            -
                    # 조인 로딩 적용
         
     | 
| 
       227 
     | 
    
         
            -
                    if joins:
         
     | 
| 
       228 
     | 
    
         
            -
                        for join_option in joins:
         
     | 
| 
       229 
     | 
    
         
            -
                            query = query.options(join_option)
         
     | 
| 
       230 
     | 
    
         
            -
             
     | 
| 
       231 
459 
     | 
    
         
             
                    # 페이지네이션 적용
         
     | 
| 
       232 
460 
     | 
    
         
             
                    query = query.limit(limit).offset(skip)
         
     | 
| 
       233 
461 
     | 
    
         | 
| 
       234 
462 
     | 
    
         
             
                    result = await session.execute(query)
         
     | 
| 
      
 463 
     | 
    
         
            +
                    
         
     | 
| 
       235 
464 
     | 
    
         
             
                    return result.scalars().unique().all()
         
     | 
| 
       236 
465 
     | 
    
         
             
                except SQLAlchemyError as e:
         
     | 
| 
       237 
466 
     | 
    
         
             
                    raise CustomException(
         
     | 
| 
         @@ -240,3 +469,117 @@ async def list_entities( 
     | 
|
| 
       240 
469 
     | 
    
         
             
                        source_function="database.list_entities",
         
     | 
| 
       241 
470 
     | 
    
         
             
                        original_error=e
         
     | 
| 
       242 
471 
     | 
    
         
             
                    )
         
     | 
| 
      
 472 
     | 
    
         
            +
             
     | 
| 
      
 473 
     | 
    
         
            +
            async def get_entity(
         
     | 
| 
      
 474 
     | 
    
         
            +
                session: AsyncSession,
         
     | 
| 
      
 475 
     | 
    
         
            +
                model: Type[ModelType],
         
     | 
| 
      
 476 
     | 
    
         
            +
                conditions: Dict[str, Any],
         
     | 
| 
      
 477 
     | 
    
         
            +
                explicit_joins: Optional[List[Any]] = None,
         
     | 
| 
      
 478 
     | 
    
         
            +
                loading_joins: Optional[List[Any]] = None
         
     | 
| 
      
 479 
     | 
    
         
            +
            ) -> ModelType:
         
     | 
| 
      
 480 
     | 
    
         
            +
                try:
         
     | 
| 
      
 481 
     | 
    
         
            +
                    query = select(model)
         
     | 
| 
      
 482 
     | 
    
         
            +
             
     | 
| 
      
 483 
     | 
    
         
            +
                    if explicit_joins:
         
     | 
| 
      
 484 
     | 
    
         
            +
                        for join_target in explicit_joins:
         
     | 
| 
      
 485 
     | 
    
         
            +
                            query = query.join(join_target)
         
     | 
| 
      
 486 
     | 
    
         
            +
             
     | 
| 
      
 487 
     | 
    
         
            +
                    if loading_joins:
         
     | 
| 
      
 488 
     | 
    
         
            +
                        for join_option in loading_joins:
         
     | 
| 
      
 489 
     | 
    
         
            +
                            query = query.options(join_option)
         
     | 
| 
      
 490 
     | 
    
         
            +
             
     | 
| 
      
 491 
     | 
    
         
            +
                    if conditions:
         
     | 
| 
      
 492 
     | 
    
         
            +
                        for key, value in conditions.items():
         
     | 
| 
      
 493 
     | 
    
         
            +
                            query = query.where(getattr(model, key) == value)
         
     | 
| 
      
 494 
     | 
    
         
            +
             
     | 
| 
      
 495 
     | 
    
         
            +
                    result = await session.execute(query)
         
     | 
| 
      
 496 
     | 
    
         
            +
                    return result.scalars().unique().one_or_none()
         
     | 
| 
      
 497 
     | 
    
         
            +
             
     | 
| 
      
 498 
     | 
    
         
            +
                except SQLAlchemyError as e:
         
     | 
| 
      
 499 
     | 
    
         
            +
                    raise CustomException(
         
     | 
| 
      
 500 
     | 
    
         
            +
                        ErrorCode.DB_READ_ERROR,
         
     | 
| 
      
 501 
     | 
    
         
            +
                        detail=str(e),
         
     | 
| 
      
 502 
     | 
    
         
            +
                        source_function="database.get_entity",
         
     | 
| 
      
 503 
     | 
    
         
            +
                        original_error=str(e)
         
     | 
| 
      
 504 
     | 
    
         
            +
                    )
         
     | 
| 
      
 505 
     | 
    
         
            +
                
         
     | 
| 
      
 506 
     | 
    
         
            +
            ##################
         
     | 
| 
      
 507 
     | 
    
         
            +
            # 로그 등 #
         
     | 
| 
      
 508 
     | 
    
         
            +
            ##################
         
     | 
| 
      
 509 
     | 
    
         
            +
            async def log_create(
         
     | 
| 
      
 510 
     | 
    
         
            +
                session: AsyncSession,
         
     | 
| 
      
 511 
     | 
    
         
            +
                model: str,
         
     | 
| 
      
 512 
     | 
    
         
            +
                log_data: Dict[str, Any],
         
     | 
| 
      
 513 
     | 
    
         
            +
                request: Optional[Request] = None
         
     | 
| 
      
 514 
     | 
    
         
            +
            ) -> None:
         
     | 
| 
      
 515 
     | 
    
         
            +
                try:
         
     | 
| 
      
 516 
     | 
    
         
            +
                    # 사용자 에이전트 및 IP 주소 추가
         
     | 
| 
      
 517 
     | 
    
         
            +
                    if request:
         
     | 
| 
      
 518 
     | 
    
         
            +
                        log_data["user_agent"] = request.headers.get("user-agent")
         
     | 
| 
      
 519 
     | 
    
         
            +
                        log_data["ip_address"] = request.headers.get("x-forwarded-for") or request.client.host
         
     | 
| 
      
 520 
     | 
    
         
            +
             
     | 
| 
      
 521 
     | 
    
         
            +
                    await create_entity(
         
     | 
| 
      
 522 
     | 
    
         
            +
                        session=session,
         
     | 
| 
      
 523 
     | 
    
         
            +
                        model=model,
         
     | 
| 
      
 524 
     | 
    
         
            +
                        entity_data=log_data
         
     | 
| 
      
 525 
     | 
    
         
            +
                    )
         
     | 
| 
      
 526 
     | 
    
         
            +
                except Exception as e:
         
     | 
| 
      
 527 
     | 
    
         
            +
                    raise CustomException(
         
     | 
| 
      
 528 
     | 
    
         
            +
                        ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 529 
     | 
    
         
            +
                        detail=f"{model}|{str(e)}",
         
     | 
| 
      
 530 
     | 
    
         
            +
                        source_function="database.log_create",
         
     | 
| 
      
 531 
     | 
    
         
            +
                        original_error=e
         
     | 
| 
      
 532 
     | 
    
         
            +
                    )
         
     | 
| 
      
 533 
     | 
    
         
            +
             
     | 
| 
      
 534 
     | 
    
         
            +
            ######################
         
     | 
| 
      
 535 
     | 
    
         
            +
            # 검증 #
         
     | 
| 
      
 536 
     | 
    
         
            +
            ######################
         
     | 
| 
      
 537 
     | 
    
         
            +
            async def validate_unique_fields(
         
     | 
| 
      
 538 
     | 
    
         
            +
                session: AsyncSession,
         
     | 
| 
      
 539 
     | 
    
         
            +
                unique_check: List[Dict[str, Any]] | None = None,
         
     | 
| 
      
 540 
     | 
    
         
            +
                find_value: bool = True  # True: 값이 있는지, False: 값이 없는지 확인
         
     | 
| 
      
 541 
     | 
    
         
            +
            ) -> None:
         
     | 
| 
      
 542 
     | 
    
         
            +
                try:
         
     | 
| 
      
 543 
     | 
    
         
            +
                    for check in unique_check:
         
     | 
| 
      
 544 
     | 
    
         
            +
                        value = check["value"]
         
     | 
| 
      
 545 
     | 
    
         
            +
                        model = check["model"]
         
     | 
| 
      
 546 
     | 
    
         
            +
                        fields = check["fields"]
         
     | 
| 
      
 547 
     | 
    
         
            +
             
     | 
| 
      
 548 
     | 
    
         
            +
                        # 여러 개의 컬럼이 있을 경우 모든 조건을 만족해야 한다
         
     | 
| 
      
 549 
     | 
    
         
            +
                        conditions = [getattr(model, column) == value for column in fields]
         
     | 
| 
      
 550 
     | 
    
         
            +
             
     | 
| 
      
 551 
     | 
    
         
            +
                        # 쿼리 실행
         
     | 
| 
      
 552 
     | 
    
         
            +
                        query = select(model).where(or_(*conditions))
         
     | 
| 
      
 553 
     | 
    
         
            +
                        result = await session.execute(query)
         
     | 
| 
      
 554 
     | 
    
         
            +
                        existing = result.scalar_one_or_none()
         
     | 
| 
      
 555 
     | 
    
         
            +
             
     | 
| 
      
 556 
     | 
    
         
            +
                        # 값이 있는지 확인 (find_value=True) 또는 값이 없는지 확인 (find_value=False)
         
     | 
| 
      
 557 
     | 
    
         
            +
                        if find_value and existing:  # 값이 존재하는 경우
         
     | 
| 
      
 558 
     | 
    
         
            +
                            raise CustomException(
         
     | 
| 
      
 559 
     | 
    
         
            +
                                ErrorCode.FIELD_INVALID_UNIQUE,
         
     | 
| 
      
 560 
     | 
    
         
            +
                                detail=f"{model.name}|{value}",
         
     | 
| 
      
 561 
     | 
    
         
            +
                                source_function="database.validate_unique_fields.existing"
         
     | 
| 
      
 562 
     | 
    
         
            +
                            )
         
     | 
| 
      
 563 
     | 
    
         
            +
                        elif not find_value and not existing:  # 값이 존재하지 않는 경우
         
     | 
| 
      
 564 
     | 
    
         
            +
                            raise CustomException(
         
     | 
| 
      
 565 
     | 
    
         
            +
                                ErrorCode.FIELD_INVALID_NOT_EXIST,
         
     | 
| 
      
 566 
     | 
    
         
            +
                                detail=f"{model.name}|{value}",
         
     | 
| 
      
 567 
     | 
    
         
            +
                                source_function="database.validate_unique_fields.not_existing"
         
     | 
| 
      
 568 
     | 
    
         
            +
                            )
         
     | 
| 
      
 569 
     | 
    
         
            +
             
     | 
| 
      
 570 
     | 
    
         
            +
                except CustomException as e:
         
     | 
| 
      
 571 
     | 
    
         
            +
                    # 특정 CustomException 처리
         
     | 
| 
      
 572 
     | 
    
         
            +
                    raise CustomException(
         
     | 
| 
      
 573 
     | 
    
         
            +
                        e.error_code,
         
     | 
| 
      
 574 
     | 
    
         
            +
                        detail=str(e),
         
     | 
| 
      
 575 
     | 
    
         
            +
                        source_function="database.validate_unique_fields.Exception",
         
     | 
| 
      
 576 
     | 
    
         
            +
                        original_error=e
         
     | 
| 
      
 577 
     | 
    
         
            +
                    )
         
     | 
| 
      
 578 
     | 
    
         
            +
                except Exception as e:
         
     | 
| 
      
 579 
     | 
    
         
            +
                    # 알 수 없는 예외 처리
         
     | 
| 
      
 580 
     | 
    
         
            +
                    raise CustomException(
         
     | 
| 
      
 581 
     | 
    
         
            +
                        ErrorCode.INTERNAL_ERROR,
         
     | 
| 
      
 582 
     | 
    
         
            +
                        detail=f"Unexpected error: {str(e)}",
         
     | 
| 
      
 583 
     | 
    
         
            +
                        source_function="database.validate_unique_fields.Exception",
         
     | 
| 
      
 584 
     | 
    
         
            +
                        original_error=e
         
     | 
| 
      
 585 
     | 
    
         
            +
                    )
         
     | 
    
        aiteamutils/exceptions.py
    CHANGED
    
    | 
         @@ -64,12 +64,16 @@ class ErrorCode(Enum): 
     | 
|
| 
       64 
64 
     | 
    
         
             
                VALIDATION_ERROR = ErrorResponse(4001, "VALIDATION_ERROR", 422, "유효성 검사 오류")
         
     | 
| 
       65 
65 
     | 
    
         
             
                FIELD_INVALID_FORMAT = ErrorResponse(4002, "FIELD_INVALID_FORMAT", 400, "잘못된 형식입니다")
         
     | 
| 
       66 
66 
     | 
    
         
             
                REQUIRED_FIELD_MISSING = ErrorResponse(4003, "VALIDATION_REQUIRED_FIELD_MISSING", 400, "필수 필드가 누락되었습니다")
         
     | 
| 
      
 67 
     | 
    
         
            +
                FIELD_INVALID_UNIQUE = ErrorResponse(4004, "VALIDATION_FIELD_INVALID_UNIQUE", 400, "중복된 값이 존재합니다")
         
     | 
| 
      
 68 
     | 
    
         
            +
                FIELD_INVALID_NOT_EXIST = ErrorResponse(4005, "VALIDATION_FIELD_INVALID_NOT_EXIST", 400, "존재하지 않는 값입니다")
         
     | 
| 
       67 
69 
     | 
    
         | 
| 
       68 
70 
     | 
    
         
             
                # General 에러: 5000번대
         
     | 
| 
       69 
71 
     | 
    
         
             
                NOT_FOUND = ErrorResponse(5001, "GENERAL_NOT_FOUND", 404, "리소스를 찾을 수 없습니다")
         
     | 
| 
       70 
72 
     | 
    
         
             
                INTERNAL_ERROR = ErrorResponse(5002, "GENERAL_INTERNAL_ERROR", 500, "내부 서버 오류")
         
     | 
| 
       71 
73 
     | 
    
         
             
                SERVICE_UNAVAILABLE = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
         
     | 
| 
       72 
74 
     | 
    
         
             
                SERVICE_NOT_REGISTERED = ErrorResponse(5003, "GENERAL_SERVICE_UNAVAILABLE", 503, "서비스를 사용할 수 없습니다")
         
     | 
| 
      
 75 
     | 
    
         
            +
                LOGIN_ERROR = ErrorResponse(5004, "LOGIN_ERROR", 401, "로그인 오류")
         
     | 
| 
      
 76 
     | 
    
         
            +
                TOKEN_ERROR = ErrorResponse(5005, "TOKEN_ERROR", 401, "토큰 오류")
         
     | 
| 
       73 
77 
     | 
    
         | 
| 
       74 
78 
     | 
    
         
             
            class CustomException(Exception):
         
     | 
| 
       75 
79 
     | 
    
         
             
                """사용자 정의 예외 클래스"""
         
     | 
| 
         @@ -98,7 +102,7 @@ class CustomException(Exception): 
     | 
|
| 
       98 
102 
     | 
    
         | 
| 
       99 
103 
     | 
    
         
             
                    self.error_chain.append({
         
     | 
| 
       100 
104 
     | 
    
         
             
                        "error_code": str(self.error_code),
         
     | 
| 
       101 
     | 
    
         
            -
                        "detail": str(self.detail),
         
     | 
| 
      
 105 
     | 
    
         
            +
                        "detail": str(self.detail) if self.detail else None,
         
     | 
| 
       102 
106 
     | 
    
         
             
                        "source_function": self.source_function,
         
     | 
| 
       103 
107 
     | 
    
         
             
                        "original_error": str(self.original_error) if self.original_error else None
         
     | 
| 
       104 
108 
     | 
    
         
             
                    })
         
     | 
| 
         @@ -115,7 +119,7 @@ class CustomException(Exception): 
     | 
|
| 
       115 
119 
     | 
    
         
             
                    """에러 정보를 딕셔너리로 반환합니다."""
         
     | 
| 
       116 
120 
     | 
    
         
             
                    return {
         
     | 
| 
       117 
121 
     | 
    
         
             
                        "error_code": self.error_code.name,
         
     | 
| 
       118 
     | 
    
         
            -
                        "detail": self.detail,
         
     | 
| 
      
 122 
     | 
    
         
            +
                        "detail": self.detail if self.detail else None,
         
     | 
| 
       119 
123 
     | 
    
         
             
                        "source_function": self.source_function,
         
     | 
| 
       120 
124 
     | 
    
         
             
                        "error_chain": self.error_chain,
         
     | 
| 
       121 
125 
     | 
    
         
             
                        "original_error": str(self.original_error) if self.original_error else None
         
     | 
    
        aiteamutils/security.py
    CHANGED
    
    | 
         @@ -1,15 +1,16 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            """보안 관련 유틸리티."""
         
     | 
| 
       2 
     | 
    
         
            -
            from datetime import datetime, timedelta,  
     | 
| 
      
 2 
     | 
    
         
            +
            from datetime import datetime, timedelta, timezone
         
     | 
| 
       3 
3 
     | 
    
         
             
            from typing import Dict, Any, Optional, Literal, Callable, TYPE_CHECKING
         
     | 
| 
       4 
4 
     | 
    
         
             
            from fastapi import Request, HTTPException, status
         
     | 
| 
       5 
5 
     | 
    
         
             
            from functools import wraps
         
     | 
| 
       6 
6 
     | 
    
         
             
            from jose import jwt, JWTError
         
     | 
| 
       7 
7 
     | 
    
         
             
            from passlib.context import CryptContext
         
     | 
| 
       8 
8 
     | 
    
         
             
            import logging
         
     | 
| 
      
 9 
     | 
    
         
            +
            from sqlalchemy.ext.asyncio import AsyncSession
         
     | 
| 
       9 
10 
     | 
    
         | 
| 
       10 
11 
     | 
    
         
             
            from .exceptions import CustomException, ErrorCode
         
     | 
| 
       11 
12 
     | 
    
         
             
            from .enums import ActivityType
         
     | 
| 
       12 
     | 
    
         
            -
            from . 
     | 
| 
      
 13 
     | 
    
         
            +
            from .database import log_create
         
     | 
| 
       13 
14 
     | 
    
         | 
| 
       14 
15 
     | 
    
         
             
            pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
         
     | 
| 
       15 
16 
     | 
    
         | 
| 
         @@ -84,7 +85,7 @@ def rate_limit( 
     | 
|
| 
       84 
85 
     | 
    
         
             
                            rate_limit_key = f"rate_limit:{client_ip}:{func.__name__}"
         
     | 
| 
       85 
86 
     | 
    
         | 
| 
       86 
87 
     | 
    
         
             
                        try:
         
     | 
| 
       87 
     | 
    
         
            -
                            now = datetime.now( 
     | 
| 
      
 88 
     | 
    
         
            +
                            now = datetime.now(timezone.utc)
         
     | 
| 
       88 
89 
     | 
    
         | 
| 
       89 
90 
     | 
    
         
             
                            # 현재 rate limit 정보 가져오기
         
     | 
| 
       90 
91 
     | 
    
         
             
                            rate_info = _rate_limits.get(rate_limit_key)
         
     | 
| 
         @@ -115,7 +116,6 @@ def rate_limit( 
     | 
|
| 
       115 
116 
     | 
    
         
             
                            except Exception as e:
         
     | 
| 
       116 
117 
     | 
    
         
             
                                raise CustomException(
         
     | 
| 
       117 
118 
     | 
    
         
             
                                    ErrorCode.INTERNAL_ERROR,
         
     | 
| 
       118 
     | 
    
         
            -
                                    detail=str(e),
         
     | 
| 
       119 
119 
     | 
    
         
             
                                    source_function=func.__name__,
         
     | 
| 
       120 
120 
     | 
    
         
             
                                    original_error=e
         
     | 
| 
       121 
121 
     | 
    
         
             
                                )
         
     | 
| 
         @@ -125,7 +125,6 @@ def rate_limit( 
     | 
|
| 
       125 
125 
     | 
    
         
             
                        except Exception as e:
         
     | 
| 
       126 
126 
     | 
    
         
             
                            raise CustomException(
         
     | 
| 
       127 
127 
     | 
    
         
             
                                ErrorCode.INTERNAL_ERROR,
         
     | 
| 
       128 
     | 
    
         
            -
                                detail=str(e),
         
     | 
| 
       129 
128 
     | 
    
         
             
                                source_function="rate_limit",
         
     | 
| 
       130 
129 
     | 
    
         
             
                                original_error=e
         
     | 
| 
       131 
130 
     | 
    
         
             
                            )
         
     | 
| 
         @@ -136,78 +135,63 @@ def rate_limit( 
     | 
|
| 
       136 
135 
     | 
    
         
             
            async def create_jwt_token(
         
     | 
| 
       137 
136 
     | 
    
         
             
                user_data: Dict[str, Any],
         
     | 
| 
       138 
137 
     | 
    
         
             
                token_type: Literal["access", "refresh"],
         
     | 
| 
       139 
     | 
    
         
            -
                 
     | 
| 
       140 
     | 
    
         
            -
                 
     | 
| 
      
 138 
     | 
    
         
            +
                db_session: AsyncSession,
         
     | 
| 
      
 139 
     | 
    
         
            +
                token_settings: Dict[str, Any],
         
     | 
| 
       141 
140 
     | 
    
         
             
            ) -> str:
         
     | 
| 
       142 
141 
     | 
    
         
             
                """JWT 토큰을 생성하고 로그를 기록합니다."""
         
     | 
| 
       143 
142 
     | 
    
         
             
                try:
         
     | 
| 
       144 
     | 
    
         
            -
                     
     | 
| 
       145 
     | 
    
         
            -
                    
         
     | 
| 
      
 143 
     | 
    
         
            +
                    # 토큰 데이터 구성
         
     | 
| 
       146 
144 
     | 
    
         
             
                    if token_type == "access":
         
     | 
| 
       147 
     | 
    
         
            -
                        expires_at = datetime.now( 
     | 
| 
      
 145 
     | 
    
         
            +
                        expires_at = datetime.now(timezone.utc) + timedelta(minutes=token_settings["ACCESS_TOKEN_EXPIRE_MINUTES"])
         
     | 
| 
      
 146 
     | 
    
         
            +
                        
         
     | 
| 
       148 
147 
     | 
    
         
             
                        token_data = {
         
     | 
| 
       149 
148 
     | 
    
         
             
                            # 등록 클레임
         
     | 
| 
       150 
     | 
    
         
            -
                            "iss":  
     | 
| 
       151 
     | 
    
         
            -
                            "sub": user_data 
     | 
| 
       152 
     | 
    
         
            -
                            "aud":  
     | 
| 
      
 149 
     | 
    
         
            +
                            "iss": token_settings["TOKEN_ISSUER"],
         
     | 
| 
      
 150 
     | 
    
         
            +
                            "sub": user_data.username,
         
     | 
| 
      
 151 
     | 
    
         
            +
                            "aud": token_settings["TOKEN_AUDIENCE"],
         
     | 
| 
       153 
152 
     | 
    
         
             
                            "exp": expires_at,
         
     | 
| 
       154 
153 
     | 
    
         | 
| 
       155 
154 
     | 
    
         
             
                            # 공개 클레임
         
     | 
| 
       156 
     | 
    
         
            -
                            "username": user_data 
     | 
| 
       157 
     | 
    
         
            -
                            "name": user_data. 
     | 
| 
      
 155 
     | 
    
         
            +
                            "username": user_data.username,
         
     | 
| 
      
 156 
     | 
    
         
            +
                            "name": user_data.name,
         
     | 
| 
       158 
157 
     | 
    
         | 
| 
       159 
158 
     | 
    
         
             
                            # 비공개 클레임
         
     | 
| 
       160 
     | 
    
         
            -
                            "user_ulid": user_data 
     | 
| 
       161 
     | 
    
         
            -
                            "role_ulid": user_data. 
     | 
| 
       162 
     | 
    
         
            -
                            "status": user_data. 
     | 
| 
       163 
     | 
    
         
            -
                            "last_login": datetime.now( 
     | 
| 
      
 159 
     | 
    
         
            +
                            "user_ulid": user_data.ulid,
         
     | 
| 
      
 160 
     | 
    
         
            +
                            "role_ulid": user_data.role_ulid,
         
     | 
| 
      
 161 
     | 
    
         
            +
                            "status": user_data.status,
         
     | 
| 
      
 162 
     | 
    
         
            +
                            "last_login": datetime.now(timezone.utc).isoformat(),
         
     | 
| 
       164 
163 
     | 
    
         
             
                            "token_type": token_type,
         
     | 
| 
       165 
164 
     | 
    
         | 
| 
       166 
165 
     | 
    
         
             
                            # 조직 관련 클레임
         
     | 
| 
       167 
     | 
    
         
            -
                            "organization_ulid": user_data. 
     | 
| 
       168 
     | 
    
         
            -
                            "organization_id": user_data. 
     | 
| 
       169 
     | 
    
         
            -
                            "organization_name": user_data. 
     | 
| 
       170 
     | 
    
         
            -
                            "company_name": user_data. 
     | 
| 
      
 166 
     | 
    
         
            +
                            "organization_ulid": user_data.role.organization.ulid,
         
     | 
| 
      
 167 
     | 
    
         
            +
                            "organization_id": user_data.role.organization.id,
         
     | 
| 
      
 168 
     | 
    
         
            +
                            "organization_name": user_data.role.organization.name,
         
     | 
| 
      
 169 
     | 
    
         
            +
                            "company_name": user_data.role.organization.company.name
         
     | 
| 
       171 
170 
     | 
    
         
             
                        }
         
     | 
| 
       172 
171 
     | 
    
         
             
                    else:  # refresh token
         
     | 
| 
       173 
     | 
    
         
            -
                        expires_at = datetime.now( 
     | 
| 
      
 172 
     | 
    
         
            +
                        expires_at = datetime.now(timezone.utc) + timedelta(days=14)
         
     | 
| 
       174 
173 
     | 
    
         
             
                        token_data = {
         
     | 
| 
       175 
     | 
    
         
            -
                            "iss":  
     | 
| 
       176 
     | 
    
         
            -
                            "sub": user_data 
     | 
| 
      
 174 
     | 
    
         
            +
                            "iss": token_settings["TOKEN_ISSUER"],
         
     | 
| 
      
 175 
     | 
    
         
            +
                            "sub": user_data.username,
         
     | 
| 
       177 
176 
     | 
    
         
             
                            "exp": expires_at,
         
     | 
| 
       178 
177 
     | 
    
         
             
                            "token_type": token_type,
         
     | 
| 
       179 
     | 
    
         
            -
                            "user_ulid": user_data 
     | 
| 
      
 178 
     | 
    
         
            +
                            "user_ulid": user_data.ulid
         
     | 
| 
       180 
179 
     | 
    
         
             
                        }
         
     | 
| 
       181 
180 
     | 
    
         | 
| 
      
 181 
     | 
    
         
            +
                    # JWT 토큰 생성
         
     | 
| 
       182 
182 
     | 
    
         
             
                    try:
         
     | 
| 
       183 
183 
     | 
    
         
             
                        token = jwt.encode(
         
     | 
| 
       184 
184 
     | 
    
         
             
                            token_data,
         
     | 
| 
       185 
     | 
    
         
            -
                             
     | 
| 
       186 
     | 
    
         
            -
                            algorithm= 
     | 
| 
      
 185 
     | 
    
         
            +
                            token_settings["JWT_SECRET"],
         
     | 
| 
      
 186 
     | 
    
         
            +
                            algorithm=token_settings["JWT_ALGORITHM"]
         
     | 
| 
       187 
187 
     | 
    
         
             
                        )
         
     | 
| 
       188 
188 
     | 
    
         
             
                    except Exception as e:
         
     | 
| 
       189 
189 
     | 
    
         
             
                        raise CustomException(
         
     | 
| 
       190 
     | 
    
         
            -
                            ErrorCode. 
     | 
| 
      
 190 
     | 
    
         
            +
                            ErrorCode.TOKEN_ERROR,
         
     | 
| 
       191 
191 
     | 
    
         
             
                            detail=f"token|{token_type}",
         
     | 
| 
       192 
192 
     | 
    
         
             
                            source_function="security.create_jwt_token",
         
     | 
| 
       193 
193 
     | 
    
         
             
                            original_error=e
         
     | 
| 
       194 
194 
     | 
    
         
             
                        )
         
     | 
| 
       195 
     | 
    
         
            -
                    
         
     | 
| 
       196 
     | 
    
         
            -
                    # 로그 생성
         
     | 
| 
       197 
     | 
    
         
            -
                    try:
         
     | 
| 
       198 
     | 
    
         
            -
                        activity_type = ActivityType.ACCESS_TOKEN_ISSUED if token_type == "access" else ActivityType.REFRESH_TOKEN_ISSUED
         
     | 
| 
       199 
     | 
    
         
            -
                        await db_service.create_log(
         
     | 
| 
       200 
     | 
    
         
            -
                            {
         
     | 
| 
       201 
     | 
    
         
            -
                                "type": activity_type,
         
     | 
| 
       202 
     | 
    
         
            -
                                "user_ulid": user_data["ulid"],
         
     | 
| 
       203 
     | 
    
         
            -
                                "token": token
         
     | 
| 
       204 
     | 
    
         
            -
                            },
         
     | 
| 
       205 
     | 
    
         
            -
                            request
         
     | 
| 
       206 
     | 
    
         
            -
                        )
         
     | 
| 
       207 
     | 
    
         
            -
                    except Exception as e:
         
     | 
| 
       208 
     | 
    
         
            -
                        # 로그 생성 실패는 토큰 생성에 영향을 주지 않음
         
     | 
| 
       209 
     | 
    
         
            -
                        logging.error(f"Failed to create token log: {str(e)}")
         
     | 
| 
       210 
     | 
    
         
            -
                    
         
     | 
| 
       211 
195 
     | 
    
         
             
                    return token
         
     | 
| 
       212 
196 
     | 
    
         | 
| 
       213 
197 
     | 
    
         
             
                except CustomException as e:
         
     | 
| 
         @@ -217,9 +201,10 @@ async def create_jwt_token( 
     | 
|
| 
       217 
201 
     | 
    
         
             
                        ErrorCode.INTERNAL_ERROR,
         
     | 
| 
       218 
202 
     | 
    
         
             
                        detail=str(e),
         
     | 
| 
       219 
203 
     | 
    
         
             
                        source_function="security.create_jwt_token",
         
     | 
| 
       220 
     | 
    
         
            -
                        original_error=e
         
     | 
| 
      
 204 
     | 
    
         
            +
                        original_error=str(e)
         
     | 
| 
       221 
205 
     | 
    
         
             
                    )
         
     | 
| 
       222 
206 
     | 
    
         | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
       223 
208 
     | 
    
         
             
            async def verify_jwt_token(
         
     | 
| 
       224 
209 
     | 
    
         
             
                token: str,
         
     | 
| 
       225 
210 
     | 
    
         
             
                expected_type: Optional[Literal["access", "refresh"]] = None
         
     | 
    
        aiteamutils/version.py
    CHANGED
    
    | 
         @@ -1,2 +1,2 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            """버전 정보"""
         
     | 
| 
       2 
     | 
    
         
            -
            __version__ = "0.2. 
     | 
| 
      
 2 
     | 
    
         
            +
            __version__ = "0.2.76"
         
     | 
| 
         @@ -0,0 +1,15 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
         
     | 
| 
      
 2 
     | 
    
         
            +
            aiteamutils/base_model.py,sha256=SWifZPzlUGyKAjmV7Uoe7PUhedm215efD7zOay8DhNc,2906
         
     | 
| 
      
 3 
     | 
    
         
            +
            aiteamutils/base_repository.py,sha256=VLCLLVkNnYLmZ6EMcr3Tu0RkMqDyMhtCO3M_2j6q-R8,4409
         
     | 
| 
      
 4 
     | 
    
         
            +
            aiteamutils/base_service.py,sha256=GmO_fqrSEbIs_Jc5BoRBTLfaJaS6CIxYPAs4B4TdtaU,9123
         
     | 
| 
      
 5 
     | 
    
         
            +
            aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
         
     | 
| 
      
 6 
     | 
    
         
            +
            aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
         
     | 
| 
      
 7 
     | 
    
         
            +
            aiteamutils/database.py,sha256=zf5Ab5ZFYQBrtvxW9te9RoVsWlMkOpTdmJ8rDwfvINs,19192
         
     | 
| 
      
 8 
     | 
    
         
            +
            aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
         
     | 
| 
      
 9 
     | 
    
         
            +
            aiteamutils/exceptions.py,sha256=3FUCIqXgYmMqonnMgUlh-J2xtApiiCgg4WM-2UV4vmQ,15823
         
     | 
| 
      
 10 
     | 
    
         
            +
            aiteamutils/security.py,sha256=xjuSTwn3_DeLHqUb17GwDU58nH7wDyk_7Wq8CPskXo8,9579
         
     | 
| 
      
 11 
     | 
    
         
            +
            aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
         
     | 
| 
      
 12 
     | 
    
         
            +
            aiteamutils/version.py,sha256=sAzd6RPEDU8drCBs0LCLhJkj0zQgL2FdPYnX-PVpUrs,42
         
     | 
| 
      
 13 
     | 
    
         
            +
            aiteamutils-0.2.76.dist-info/METADATA,sha256=EVxAPwXRwIYNWzHV8ekVUIKXKlyY_RogJn1wWK5fE58,1718
         
     | 
| 
      
 14 
     | 
    
         
            +
            aiteamutils-0.2.76.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
         
     | 
| 
      
 15 
     | 
    
         
            +
            aiteamutils-0.2.76.dist-info/RECORD,,
         
     | 
| 
         @@ -1,15 +0,0 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
         
     | 
| 
       2 
     | 
    
         
            -
            aiteamutils/base_model.py,sha256=ODEnjvUVoxQ1RPCfq8-uZTfTADIA4c7Z3E6G4EVsSX0,2708
         
     | 
| 
       3 
     | 
    
         
            -
            aiteamutils/base_repository.py,sha256=tG_xz4hHYAN3-wkrLvEPxyTucV4pzT6dihoKVJp2JIc,2079
         
     | 
| 
       4 
     | 
    
         
            -
            aiteamutils/base_service.py,sha256=M02lAcDmzTul97VBC1H8i5OtNKV8a9En2_sNCz84e-w,2852
         
     | 
| 
       5 
     | 
    
         
            -
            aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
         
     | 
| 
       6 
     | 
    
         
            -
            aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
         
     | 
| 
       7 
     | 
    
         
            -
            aiteamutils/database.py,sha256=4Z1baj7UISE8KsVC_KilKwtCR4Yp8qR-kbwB7CHpDtE,8032
         
     | 
| 
       8 
     | 
    
         
            -
            aiteamutils/enums.py,sha256=ipZi6k_QD5-3QV7Yzv7bnL0MjDz-vqfO9I5L77biMKs,632
         
     | 
| 
       9 
     | 
    
         
            -
            aiteamutils/exceptions.py,sha256=_lKWXq_ujNj41xN6LDE149PwsecAP7lgYWbOBbLOntg,15368
         
     | 
| 
       10 
     | 
    
         
            -
            aiteamutils/security.py,sha256=xFVrjttxwXB1TTjqgRQQgQJQohQBT28vuW8FVLjvi-M,10103
         
     | 
| 
       11 
     | 
    
         
            -
            aiteamutils/validators.py,sha256=PvI9hbMEAqTawgxPbiWRyx2r9yTUrpNBQs1AD3w4F2U,7726
         
     | 
| 
       12 
     | 
    
         
            -
            aiteamutils/version.py,sha256=p9Q6ycHvK4ulfUsk9t-OMhQk41NdIQmGrAm5soMo2OY,42
         
     | 
| 
       13 
     | 
    
         
            -
            aiteamutils-0.2.73.dist-info/METADATA,sha256=oGgO7P8aKOYwsEPaGzDSRRIxCXfs3PVqhDT9GCehssM,1718
         
     | 
| 
       14 
     | 
    
         
            -
            aiteamutils-0.2.73.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
         
     | 
| 
       15 
     | 
    
         
            -
            aiteamutils-0.2.73.dist-info/RECORD,,
         
     | 
| 
         
            File without changes
         
     |