aiteamutils 0.2.51__py3-none-any.whl → 0.2.53__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_repository.py +68 -364
- aiteamutils/base_service.py +46 -43
- aiteamutils/database.py +24 -5
- aiteamutils/dependencies.py +183 -105
- aiteamutils/security.py +93 -281
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.53.dist-info}/METADATA +1 -1
- aiteamutils-0.2.53.dist-info/RECORD +16 -0
- aiteamutils-0.2.51.dist-info/RECORD +0 -16
- {aiteamutils-0.2.51.dist-info → aiteamutils-0.2.53.dist-info}/WHEEL +0 -0
    
        aiteamutils/base_repository.py
    CHANGED
    
    | @@ -3,281 +3,54 @@ from typing import TypeVar, Generic, Dict, Any, List, Optional, Type, Union | |
| 3 3 | 
             
            from sqlalchemy.orm import DeclarativeBase, Load
         | 
| 4 4 | 
             
            from sqlalchemy.exc import IntegrityError, SQLAlchemyError
         | 
| 5 5 | 
             
            from sqlalchemy import select, or_, and_
         | 
| 6 | 
            -
            from .database import DatabaseService
         | 
| 7 6 | 
             
            from .exceptions import CustomException, ErrorCode
         | 
| 8 7 | 
             
            from sqlalchemy.orm import joinedload
         | 
| 9 8 | 
             
            from sqlalchemy.sql import Select
         | 
| 10 9 | 
             
            from fastapi import Request
         | 
| 10 | 
            +
            from sqlalchemy.ext.asyncio import AsyncSession
         | 
| 11 11 |  | 
| 12 12 | 
             
            ModelType = TypeVar("ModelType", bound=DeclarativeBase)
         | 
| 13 13 |  | 
| 14 14 | 
             
            class BaseRepository(Generic[ModelType]):
         | 
| 15 | 
            -
             | 
| 15 | 
            +
                ##################
         | 
| 16 16 | 
             
                # 1. 초기화 영역 #
         | 
| 17 17 | 
             
                ##################
         | 
| 18 | 
            -
                def __init__(self,  | 
| 18 | 
            +
                def __init__(self, session: AsyncSession, model: Type[ModelType]):
         | 
| 19 19 | 
             
                    """
         | 
| 20 20 | 
             
                    Args:
         | 
| 21 | 
            -
                         | 
| 21 | 
            +
                        session (AsyncSession): 데이터베이스 세션
         | 
| 22 22 | 
             
                        model (Type[ModelType]): 모델 클래스
         | 
| 23 23 | 
             
                    """
         | 
| 24 | 
            -
                    self. | 
| 24 | 
            +
                    self.session = session
         | 
| 25 25 | 
             
                    self.model = model
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                 | 
| 28 | 
            -
                 | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
                    """
         | 
| 36 | 
            -
                    return select(self.model)
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                #######################
         | 
| 39 | 
            -
                # 3. 전차리 영역    #
         | 
| 40 | 
            -
                #######################
         | 
| 41 | 
            -
                def _apply_exact_match(self, stmt: Select, field_name: str, value: Any) -> Select:
         | 
| 42 | 
            -
                    """정확한 값 매칭 조건을 적용합니다.
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                    Args:
         | 
| 45 | 
            -
                        stmt (Select): 쿼리문
         | 
| 46 | 
            -
                        field_name (str): 필드명
         | 
| 47 | 
            -
                        value (Any): 매칭할 값
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                    Returns:
         | 
| 50 | 
            -
                        Select: 조건이 적용된 쿼리
         | 
| 51 | 
            -
                    """
         | 
| 52 | 
            -
                    return stmt.where(getattr(self.model, field_name) == value)
         | 
| 53 | 
            -
             | 
| 54 | 
            -
                def _apply_like_match(self, stmt: Select, field_name: str, value: str) -> Select:
         | 
| 55 | 
            -
                    """LIKE 검색 조건을 적용합니다.
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                    Args:
         | 
| 58 | 
            -
                        stmt (Select): 쿼리문
         | 
| 59 | 
            -
                        field_name (str): 필드명
         | 
| 60 | 
            -
                        value (str): 검색할 값
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                    Returns:
         | 
| 63 | 
            -
                        Select: 조건이 적용된 쿼리
         | 
| 64 | 
            -
                    """
         | 
| 65 | 
            -
                    return stmt.where(getattr(self.model, field_name).ilike(f"%{value}%"))
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                def _apply_relation_match(self, stmt: Select, relations: List[str], field_name: str, operator: str, value: Any) -> Select:
         | 
| 68 | 
            -
                    """관계 테이블 검색 조건을 적용합니다."""
         | 
| 69 | 
            -
                    current = self.model
         | 
| 70 | 
            -
                    
         | 
| 71 | 
            -
                    # 관계 체인 따라가기
         | 
| 72 | 
            -
                    for i in range(len(relations)-1):
         | 
| 73 | 
            -
                        current = getattr(current, relations[i]).property.mapper.class_
         | 
| 74 | 
            -
                    
         | 
| 75 | 
            -
                    # 마지막 모델과 필드
         | 
| 76 | 
            -
                    final_model = getattr(current, relations[-1]).property.mapper.class_
         | 
| 77 | 
            -
                    
         | 
| 78 | 
            -
                    # 중첩된 EXISTS 절 생성
         | 
| 79 | 
            -
                    current = self.model
         | 
| 80 | 
            -
                    subq = select(1)
         | 
| 81 | 
            -
                    
         | 
| 82 | 
            -
                    # 첫 번째 관계
         | 
| 83 | 
            -
                    next_model = getattr(current, relations[0]).property.mapper.class_
         | 
| 84 | 
            -
                    subq = subq.where(getattr(next_model, 'ulid') == getattr(current, f"{relations[0]}_ulid"))
         | 
| 85 | 
            -
                    
         | 
| 86 | 
            -
                    # 중간 관계들
         | 
| 87 | 
            -
                    for i in range(1, len(relations)):
         | 
| 88 | 
            -
                        prev_model = next_model
         | 
| 89 | 
            -
                        next_model = getattr(prev_model, relations[i]).property.mapper.class_
         | 
| 90 | 
            -
                        subq = subq.where(getattr(next_model, 'ulid') == getattr(prev_model, f"{relations[i]}_ulid"))
         | 
| 91 | 
            -
                    
         | 
| 92 | 
            -
                    # 최종 검색 조건
         | 
| 93 | 
            -
                    subq = subq.where(getattr(final_model, field_name).__getattribute__(operator)(value))
         | 
| 94 | 
            -
                    
         | 
| 95 | 
            -
                    return stmt.where(subq.exists())
         | 
| 96 | 
            -
             | 
| 97 | 
            -
                def _apply_ordering(self, stmt: Select, order_by: List[str]) -> Select:
         | 
| 98 | 
            -
                    """정렬 조건을 적용합니다.
         | 
| 99 | 
            -
             | 
| 100 | 
            -
                    Args:
         | 
| 101 | 
            -
                        stmt (Select): 쿼리문
         | 
| 102 | 
            -
                        order_by (List[str]): 정렬 기준 필드 목록 (예: ["name", "-created_at"])
         | 
| 103 | 
            -
             | 
| 104 | 
            -
                    Returns:
         | 
| 105 | 
            -
                        Select: 정렬이 적용된 쿼리
         | 
| 106 | 
            -
                    """
         | 
| 107 | 
            -
                    for field in order_by:
         | 
| 108 | 
            -
                        if field.startswith("-"):
         | 
| 109 | 
            -
                            field_name = field[1:]
         | 
| 110 | 
            -
                            stmt = stmt.order_by(getattr(self.model, field_name).desc())
         | 
| 111 | 
            -
                        else:
         | 
| 112 | 
            -
                            stmt = stmt.order_by(getattr(self.model, field).asc())
         | 
| 113 | 
            -
                    return stmt
         | 
| 114 | 
            -
             | 
| 115 | 
            -
                def _apply_pagination(self, stmt: Select, skip: int = 0, limit: int = 100) -> Select:
         | 
| 116 | 
            -
                    """페이징을 적용합니다.
         | 
| 117 | 
            -
             | 
| 118 | 
            -
                    Args:
         | 
| 119 | 
            -
                        stmt (Select): 쿼리문
         | 
| 120 | 
            -
                        skip (int): 건너뛸 레코드 수
         | 
| 121 | 
            -
                        limit (int): 조회할 최대 레코드 수
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                    Returns:
         | 
| 124 | 
            -
                        Select: 페이징이 적용된 쿼리
         | 
| 125 | 
            -
                    """
         | 
| 126 | 
            -
                    return stmt.offset(skip).limit(limit)
         | 
| 127 | 
            -
             | 
| 128 | 
            -
                def _apply_joins(self, stmt: Select, joins: List[str]) -> Select:
         | 
| 129 | 
            -
                    """조인을 적용합니다.
         | 
| 130 | 
            -
             | 
| 131 | 
            -
                    Args:
         | 
| 132 | 
            -
                        stmt (Select): 쿼리문
         | 
| 133 | 
            -
                        joins (List[str]): 조인할 관계명 목록
         | 
| 134 | 
            -
             | 
| 135 | 
            -
                    Returns:
         | 
| 136 | 
            -
                        Select: 조인이 적용된 쿼리
         | 
| 137 | 
            -
                    """
         | 
| 138 | 
            -
                    for join in joins:
         | 
| 139 | 
            -
                        stmt = stmt.options(joinedload(getattr(self.model, join)))
         | 
| 140 | 
            -
                    return stmt
         | 
| 141 | 
            -
             | 
| 142 | 
            -
                def _build_jsonb_condition(self, model: Any, field_path: str, value: str) -> Any:
         | 
| 143 | 
            -
                    """JSONB 필드에 대한 검색 조건을 생성합니다.
         | 
| 144 | 
            -
             | 
| 145 | 
            -
                    Args:
         | 
| 146 | 
            -
                        model: 대상 모델
         | 
| 147 | 
            -
                        field_path (str): JSONB 키 경로 (예: "address", "name.first")
         | 
| 148 | 
            -
                        value (str): 검색할 값
         | 
| 149 | 
            -
             | 
| 150 | 
            -
                    Returns:
         | 
| 151 | 
            -
                        Any: SQLAlchemy 검색 조건
         | 
| 152 | 
            -
                    """
         | 
| 153 | 
            -
                    # JSONB 경로가 중첩된 경우 (예: "name.first")
         | 
| 154 | 
            -
                    if "." in field_path:
         | 
| 155 | 
            -
                        path_parts = field_path.split(".")
         | 
| 156 | 
            -
                        jsonb_path = "{" + ",".join(path_parts) + "}"
         | 
| 157 | 
            -
                        return model.extra_data[jsonb_path].astext.ilike(f"%{value}%")
         | 
| 158 | 
            -
                    # 단일 키인 경우
         | 
| 159 | 
            -
                    return model.extra_data[field_path].astext.ilike(f"%{value}%")
         | 
| 160 | 
            -
             | 
| 161 | 
            -
                def _apply_jsonb_match(self, stmt: Select, relations: List[str], json_key: str, value: str) -> Select:
         | 
| 162 | 
            -
                    """JSONB 필드 검색 조건을 적용합니다.
         | 
| 163 | 
            -
             | 
| 164 | 
            -
                    Args:
         | 
| 165 | 
            -
                        stmt (Select): 쿼리문
         | 
| 166 | 
            -
                        relations (List[str]): 관계 테이블 경로
         | 
| 167 | 
            -
                        json_key (str): JSONB 키 경로
         | 
| 168 | 
            -
                        value (str): 검색할 값
         | 
| 169 | 
            -
             | 
| 170 | 
            -
                    Returns:
         | 
| 171 | 
            -
                        Select: 조건이 적용된 쿼리
         | 
| 172 | 
            -
                    """
         | 
| 173 | 
            -
                    current = self.model
         | 
| 174 | 
            -
                    
         | 
| 175 | 
            -
                    # 단일 모델 검색
         | 
| 176 | 
            -
                    if not relations:
         | 
| 177 | 
            -
                        condition = self._build_jsonb_condition(current, json_key, value)
         | 
| 178 | 
            -
                        return stmt.where(condition)
         | 
| 179 | 
            -
                        
         | 
| 180 | 
            -
                    # 관계 모델 검색
         | 
| 181 | 
            -
                    for i in range(len(relations)-1):
         | 
| 182 | 
            -
                        current = getattr(current, relations[i]).property.mapper.class_
         | 
| 183 | 
            -
                        
         | 
| 184 | 
            -
                    final_model = getattr(current, relations[-1]).property.mapper.class_
         | 
| 185 | 
            -
                    
         | 
| 186 | 
            -
                    # 관계 체인 구성
         | 
| 187 | 
            -
                    if len(relations) == 1:
         | 
| 188 | 
            -
                        condition = getattr(self.model, relations[0]).has(
         | 
| 189 | 
            -
                            self._build_jsonb_condition(final_model, json_key, value)
         | 
| 190 | 
            -
                        )
         | 
| 191 | 
            -
                    else:
         | 
| 192 | 
            -
                        condition = getattr(self.model, relations[0]).has(
         | 
| 193 | 
            -
                            getattr(final_model, relations[-1]).has(
         | 
| 194 | 
            -
                                self._build_jsonb_condition(final_model, json_key, value)
         | 
| 195 | 
            -
                            )
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                @property
         | 
| 28 | 
            +
                def session(self):
         | 
| 29 | 
            +
                    """현재 세션을 반환합니다."""
         | 
| 30 | 
            +
                    if self._session is None:
         | 
| 31 | 
            +
                        raise CustomException(
         | 
| 32 | 
            +
                            ErrorCode.DB_CONNECTION_ERROR,
         | 
| 33 | 
            +
                            detail="Database session is not set",
         | 
| 34 | 
            +
                            source_function=f"{self.__class__.__name__}.session"
         | 
| 196 35 | 
             
                        )
         | 
| 197 | 
            -
             | 
| 198 | 
            -
             | 
| 199 | 
            -
             | 
| 200 | 
            -
                def  | 
| 201 | 
            -
                    """ | 
| 202 | 
            -
                     | 
| 203 | 
            -
                        return stmt
         | 
| 204 | 
            -
                        
         | 
| 205 | 
            -
                    for key, value in search_params.items():
         | 
| 206 | 
            -
                        if not value.get("value"):
         | 
| 207 | 
            -
                            continue
         | 
| 208 | 
            -
                            
         | 
| 209 | 
            -
                        conditions = []
         | 
| 210 | 
            -
                        for field in value.get("fields", []):
         | 
| 211 | 
            -
                            parts = field.split('.')
         | 
| 212 | 
            -
                            
         | 
| 213 | 
            -
                            if len(parts) == 1:
         | 
| 214 | 
            -
                                # 직접 필드 검색
         | 
| 215 | 
            -
                                condition = self._apply_like_match(stmt, parts[0], value["value"]).whereclause
         | 
| 216 | 
            -
                            elif 'extra_data' in parts:
         | 
| 217 | 
            -
                                # JSONB 필드 검색
         | 
| 218 | 
            -
                                extra_data_idx = parts.index('extra_data')
         | 
| 219 | 
            -
                                tables = parts[:extra_data_idx]
         | 
| 220 | 
            -
                                json_key = ".".join(parts[extra_data_idx + 1:])
         | 
| 221 | 
            -
                                condition = self._apply_jsonb_match(
         | 
| 222 | 
            -
                                    stmt,
         | 
| 223 | 
            -
                                    tables,
         | 
| 224 | 
            -
                                    json_key,
         | 
| 225 | 
            -
                                    value["value"]
         | 
| 226 | 
            -
                                ).whereclause
         | 
| 227 | 
            -
                            else:
         | 
| 228 | 
            -
                                # 관계 테이블 검색
         | 
| 229 | 
            -
                                condition = self._apply_relation_match(
         | 
| 230 | 
            -
                                    stmt,
         | 
| 231 | 
            -
                                    parts[:-1],
         | 
| 232 | 
            -
                                    parts[-1],
         | 
| 233 | 
            -
                                    "ilike",
         | 
| 234 | 
            -
                                    f"%{value['value']}%"
         | 
| 235 | 
            -
                                ).whereclause
         | 
| 236 | 
            -
                            
         | 
| 237 | 
            -
                            conditions.append(condition)
         | 
| 238 | 
            -
                                
         | 
| 239 | 
            -
                        if conditions:
         | 
| 240 | 
            -
                            stmt = stmt.where(or_(*conditions))
         | 
| 241 | 
            -
                                
         | 
| 242 | 
            -
                    return stmt
         | 
| 243 | 
            -
             | 
| 244 | 
            -
                def _apply_filters(self, stmt, filters: Dict[str, Any]):
         | 
| 245 | 
            -
                    """일반 필터를 적용합니다."""
         | 
| 246 | 
            -
                    for key, value in filters.items():
         | 
| 247 | 
            -
                        if value is None:
         | 
| 248 | 
            -
                            continue
         | 
| 249 | 
            -
                            
         | 
| 250 | 
            -
                        if "." in key:
         | 
| 251 | 
            -
                            # 관계 테이블 필터
         | 
| 252 | 
            -
                            relation, field = key.split(".")
         | 
| 253 | 
            -
                            stmt = self._apply_relation_match(stmt, relation, field, "__eq__", value)
         | 
| 254 | 
            -
                        else:
         | 
| 255 | 
            -
                            # 일반 필드 필터
         | 
| 256 | 
            -
                            stmt = stmt.where(getattr(self.model, key) == value)
         | 
| 257 | 
            -
                            
         | 
| 258 | 
            -
                    return stmt
         | 
| 36 | 
            +
                    return self._session
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                @session.setter
         | 
| 39 | 
            +
                def session(self, value):
         | 
| 40 | 
            +
                    """세션을 설정합니다."""
         | 
| 41 | 
            +
                    self._session = value
         | 
| 259 42 |  | 
| 260 43 | 
             
                #######################
         | 
| 261 | 
            -
                #  | 
| 44 | 
            +
                # 2. CRUD 작업     #
         | 
| 262 45 | 
             
                #######################
         | 
| 263 46 | 
             
                async def get(
         | 
| 264 47 | 
             
                    self,
         | 
| 265 48 | 
             
                    ulid: str
         | 
| 266 49 | 
             
                ) -> Optional[Dict[str, Any]]:
         | 
| 267 | 
            -
                    """ULID로 엔티티를 조회합니다.
         | 
| 268 | 
            -
             | 
| 269 | 
            -
                    Args:
         | 
| 270 | 
            -
                        ulid (str): 조회할 엔티티의 ULID
         | 
| 271 | 
            -
             | 
| 272 | 
            -
                    Returns:
         | 
| 273 | 
            -
                        Optional[Dict[str, Any]]: 조회된 엔티티, 없으면 None
         | 
| 274 | 
            -
             | 
| 275 | 
            -
                    Raises:
         | 
| 276 | 
            -
                        CustomException: 데이터베이스 작업 중 오류 발생 시
         | 
| 277 | 
            -
                    """
         | 
| 50 | 
            +
                    """ULID로 엔티티를 조회합니다."""
         | 
| 278 51 | 
             
                    try:
         | 
| 279 52 | 
             
                        stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
         | 
| 280 | 
            -
                        result = await self. | 
| 53 | 
            +
                        result = await self.session.execute(stmt)
         | 
| 281 54 | 
             
                        entity = result.scalars().unique().first()
         | 
| 282 55 |  | 
| 283 56 | 
             
                        if not entity:
         | 
| @@ -298,14 +71,7 @@ class BaseRepository(Generic[ModelType]): | |
| 298 71 | 
             
                            source_function=f"{self.__class__.__name__}.get",
         | 
| 299 72 | 
             
                            original_error=e
         | 
| 300 73 | 
             
                        )
         | 
| 301 | 
            -
             | 
| 302 | 
            -
                        raise CustomException(
         | 
| 303 | 
            -
                            ErrorCode.DB_QUERY_ERROR,
         | 
| 304 | 
            -
                            detail=f"Unexpected repository error in {self.model.__tablename__}: {str(e)}",
         | 
| 305 | 
            -
                            source_function=f"{self.__class__.__name__}.get",
         | 
| 306 | 
            -
                            original_error=e
         | 
| 307 | 
            -
                        )
         | 
| 308 | 
            -
                    
         | 
| 74 | 
            +
             | 
| 309 75 | 
             
                async def list(
         | 
| 310 76 | 
             
                    self,
         | 
| 311 77 | 
             
                    skip: int = 0,
         | 
| @@ -328,7 +94,7 @@ class BaseRepository(Generic[ModelType]): | |
| 328 94 | 
             
                        # 페이지네이션 적용
         | 
| 329 95 | 
             
                        stmt = stmt.limit(limit).offset(skip)
         | 
| 330 96 |  | 
| 331 | 
            -
                        result = await self. | 
| 97 | 
            +
                        result = await self.session.execute(stmt)
         | 
| 332 98 | 
             
                        return result.scalars().unique().all()
         | 
| 333 99 |  | 
| 334 100 | 
             
                    except SQLAlchemyError as e:
         | 
| @@ -340,165 +106,103 @@ class BaseRepository(Generic[ModelType]): | |
| 340 106 | 
             
                        )
         | 
| 341 107 |  | 
| 342 108 | 
             
                async def create(self, data: Dict[str, Any]) -> ModelType:
         | 
| 343 | 
            -
                    """새로운 엔티티를 생성합니다.
         | 
| 344 | 
            -
             | 
| 345 | 
            -
                    Args:
         | 
| 346 | 
            -
                        data (Dict[str, Any]): 생성할 엔티티 데이터
         | 
| 347 | 
            -
             | 
| 348 | 
            -
                    Returns:
         | 
| 349 | 
            -
                        ModelType: 생성된 엔티티
         | 
| 350 | 
            -
             | 
| 351 | 
            -
                    Raises:
         | 
| 352 | 
            -
                        CustomException: 데이터베이스 작업 중 오류 발생 시
         | 
| 353 | 
            -
                    """
         | 
| 109 | 
            +
                    """새로운 엔티티를 생성합니다."""
         | 
| 354 110 | 
             
                    try:
         | 
| 355 | 
            -
                         | 
| 356 | 
            -
             | 
| 357 | 
            -
                         | 
| 358 | 
            -
                         | 
| 359 | 
            -
                         | 
| 111 | 
            +
                        entity = self.model(**data)
         | 
| 112 | 
            +
                        self.session.add(entity)
         | 
| 113 | 
            +
                        await self.session.flush()
         | 
| 114 | 
            +
                        await self.session.refresh(entity)
         | 
| 115 | 
            +
                        return entity
         | 
| 360 116 | 
             
                    except IntegrityError as e:
         | 
| 117 | 
            +
                        await self.session.rollback()
         | 
| 361 118 | 
             
                        self._handle_integrity_error(e, "create", data)
         | 
| 362 119 | 
             
                    except SQLAlchemyError as e:
         | 
| 120 | 
            +
                        await self.session.rollback()
         | 
| 363 121 | 
             
                        raise CustomException(
         | 
| 364 122 | 
             
                            ErrorCode.DB_CREATE_ERROR,
         | 
| 365 123 | 
             
                            detail=f"Database create error in {self.model.__tablename__}: {str(e)}",
         | 
| 366 124 | 
             
                            source_function=f"{self.__class__.__name__}.create",
         | 
| 367 125 | 
             
                            original_error=e
         | 
| 368 126 | 
             
                        )
         | 
| 369 | 
            -
                    except Exception as e:
         | 
| 370 | 
            -
                        raise CustomException(
         | 
| 371 | 
            -
                            ErrorCode.DB_CREATE_ERROR,
         | 
| 372 | 
            -
                            detail=f"Unexpected repository create error in {self.model.__tablename__}: {str(e)}",
         | 
| 373 | 
            -
                            source_function=f"{self.__class__.__name__}.create",
         | 
| 374 | 
            -
                            original_error=e
         | 
| 375 | 
            -
                        )
         | 
| 376 127 |  | 
| 377 128 | 
             
                async def update(self, ulid: str, data: Dict[str, Any]) -> Optional[ModelType]:
         | 
| 378 | 
            -
                    """기존 엔티티를 수정합니다.
         | 
| 379 | 
            -
             | 
| 380 | 
            -
                    Args:
         | 
| 381 | 
            -
                        ulid (str): 수정할 엔티티의 ULID
         | 
| 382 | 
            -
                        data (Dict[str, Any]): 수정할 데이터
         | 
| 383 | 
            -
             | 
| 384 | 
            -
                    Returns:
         | 
| 385 | 
            -
                        Optional[ModelType]: 수정된 엔티티, 없으면 None
         | 
| 386 | 
            -
             | 
| 387 | 
            -
                    Raises:
         | 
| 388 | 
            -
                        CustomException: 데이터베이스 작업 중 오류 발생 시
         | 
| 389 | 
            -
                    """
         | 
| 129 | 
            +
                    """기존 엔티티를 수정합니다."""
         | 
| 390 130 | 
             
                    try:
         | 
| 391 | 
            -
                         | 
| 392 | 
            -
             | 
| 393 | 
            -
             | 
| 394 | 
            -
             | 
| 395 | 
            -
                        )
         | 
| 131 | 
            +
                        stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
         | 
| 132 | 
            +
                        result = await self.session.execute(stmt)
         | 
| 133 | 
            +
                        entity = result.scalars().first()
         | 
| 134 | 
            +
                        
         | 
| 396 135 | 
             
                        if not entity:
         | 
| 397 136 | 
             
                            raise CustomException(
         | 
| 398 137 | 
             
                                ErrorCode.DB_NO_RESULT,
         | 
| 399 138 | 
             
                                detail=f"{self.model.__tablename__}|ulid|{ulid}",
         | 
| 400 139 | 
             
                                source_function=f"{self.__class__.__name__}.update"
         | 
| 401 140 | 
             
                            )
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                        for key, value in data.items():
         | 
| 143 | 
            +
                            setattr(entity, key, value)
         | 
| 144 | 
            +
                        
         | 
| 145 | 
            +
                        await self.session.flush()
         | 
| 146 | 
            +
                        await self.session.refresh(entity)
         | 
| 402 147 | 
             
                        return entity
         | 
| 403 | 
            -
             | 
| 404 | 
            -
                        e.detail = f"Repository update error for {self.model.__tablename__}: {e.detail}"
         | 
| 405 | 
            -
                        e.source_function = f"{self.__class__.__name__}.update -> {e.source_function}"
         | 
| 406 | 
            -
                        raise e
         | 
| 148 | 
            +
                        
         | 
| 407 149 | 
             
                    except IntegrityError as e:
         | 
| 150 | 
            +
                        await self.session.rollback()
         | 
| 408 151 | 
             
                        self._handle_integrity_error(e, "update", data)
         | 
| 409 152 | 
             
                    except SQLAlchemyError as e:
         | 
| 153 | 
            +
                        await self.session.rollback()
         | 
| 410 154 | 
             
                        raise CustomException(
         | 
| 411 155 | 
             
                            ErrorCode.DB_UPDATE_ERROR,
         | 
| 412 156 | 
             
                            detail=f"Database update error in {self.model.__tablename__}: {str(e)}",
         | 
| 413 157 | 
             
                            source_function=f"{self.__class__.__name__}.update",
         | 
| 414 158 | 
             
                            original_error=e
         | 
| 415 159 | 
             
                        )
         | 
| 416 | 
            -
                    except Exception as e:
         | 
| 417 | 
            -
                        raise CustomException(
         | 
| 418 | 
            -
                            ErrorCode.DB_UPDATE_ERROR,
         | 
| 419 | 
            -
                            detail=f"Unexpected repository update error in {self.model.__tablename__}: {str(e)}",
         | 
| 420 | 
            -
                            source_function=f"{self.__class__.__name__}.update",
         | 
| 421 | 
            -
                            original_error=e
         | 
| 422 | 
            -
                        )
         | 
| 423 160 |  | 
| 424 161 | 
             
                async def delete(self, ulid: str) -> bool:
         | 
| 425 | 
            -
                    """엔티티를 소프트  | 
| 426 | 
            -
             | 
| 427 | 
            -
                    Args:
         | 
| 428 | 
            -
                        ulid (str): 삭제할 엔티티의 ULID
         | 
| 429 | 
            -
             | 
| 430 | 
            -
                    Returns:
         | 
| 431 | 
            -
                        bool: 삭제 성공 여부
         | 
| 432 | 
            -
             | 
| 433 | 
            -
                    Raises:
         | 
| 434 | 
            -
                        CustomException: 데이터베이스 작업 중 오류 발생 시
         | 
| 435 | 
            -
                    """
         | 
| 162 | 
            +
                    """엔티티를 소프트 삭제합니다."""
         | 
| 436 163 | 
             
                    try:
         | 
| 437 | 
            -
                         | 
| 164 | 
            +
                        stmt = select(self.model).filter_by(ulid=ulid, is_deleted=False)
         | 
| 165 | 
            +
                        result = await self.session.execute(stmt)
         | 
| 166 | 
            +
                        entity = result.scalars().first()
         | 
| 167 | 
            +
                        
         | 
| 438 168 | 
             
                        if not entity:
         | 
| 439 169 | 
             
                            raise CustomException(
         | 
| 440 170 | 
             
                                ErrorCode.DB_NO_RESULT,
         | 
| 441 171 | 
             
                                detail=f"{self.model.__tablename__}|ulid|{ulid}",
         | 
| 442 172 | 
             
                                source_function=f"{self.__class__.__name__}.delete"
         | 
| 443 173 | 
             
                            )
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                        entity.is_deleted = True
         | 
| 176 | 
            +
                        await self.session.flush()
         | 
| 444 177 | 
             
                        return True
         | 
| 445 | 
            -
             | 
| 446 | 
            -
                        e.detail = f"Repository delete error for {self.model.__tablename__}: {e.detail}"
         | 
| 447 | 
            -
                        e.source_function = f"{self.__class__.__name__}.delete -> {e.source_function}"
         | 
| 448 | 
            -
                        raise e
         | 
| 449 | 
            -
                    except IntegrityError as e:
         | 
| 450 | 
            -
                        self._handle_integrity_error(e, "delete")
         | 
| 178 | 
            +
                        
         | 
| 451 179 | 
             
                    except SQLAlchemyError as e:
         | 
| 180 | 
            +
                        await self.session.rollback()
         | 
| 452 181 | 
             
                        raise CustomException(
         | 
| 453 182 | 
             
                            ErrorCode.DB_DELETE_ERROR,
         | 
| 454 183 | 
             
                            detail=f"Database delete error in {self.model.__tablename__}: {str(e)}",
         | 
| 455 184 | 
             
                            source_function=f"{self.__class__.__name__}.delete",
         | 
| 456 185 | 
             
                            original_error=e
         | 
| 457 186 | 
             
                        )
         | 
| 458 | 
            -
                    except Exception as e:
         | 
| 459 | 
            -
                        raise CustomException(
         | 
| 460 | 
            -
                            ErrorCode.DB_DELETE_ERROR,
         | 
| 461 | 
            -
                            detail=f"Unexpected repository delete error in {self.model.__tablename__}: {str(e)}",
         | 
| 462 | 
            -
                            source_function=f"{self.__class__.__name__}.delete",
         | 
| 463 | 
            -
                            original_error=e
         | 
| 464 | 
            -
                        )
         | 
| 465 | 
            -
             | 
| 466 | 
            -
                async def real_row_delete(self, ulid: str) -> bool:
         | 
| 467 | 
            -
                    """엔티티를 실제로 삭제합니다.
         | 
| 468 | 
            -
             | 
| 469 | 
            -
                    Args:
         | 
| 470 | 
            -
                        ulid (str): 삭제할 엔티티의 ULID
         | 
| 471 187 |  | 
| 472 | 
            -
             | 
| 473 | 
            -
             | 
| 474 | 
            -
             | 
| 475 | 
            -
                    Raises:
         | 
| 476 | 
            -
                        CustomException: 데이터베이스 작업 중 오류 발생 시
         | 
| 477 | 
            -
                    """
         | 
| 188 | 
            +
                async def real_delete(self, ulid: str) -> bool:
         | 
| 189 | 
            +
                    """엔티티를 실제로 삭제합니다."""
         | 
| 478 190 | 
             
                    try:
         | 
| 479 | 
            -
                         | 
| 480 | 
            -
             | 
| 481 | 
            -
             | 
| 482 | 
            -
                         | 
| 191 | 
            +
                        stmt = select(self.model).filter_by(ulid=ulid)
         | 
| 192 | 
            +
                        result = await self.session.execute(stmt)
         | 
| 193 | 
            +
                        entity = result.scalars().first()
         | 
| 194 | 
            +
                        
         | 
| 483 195 | 
             
                        if entity:
         | 
| 484 | 
            -
                            await self. | 
| 196 | 
            +
                            await self.session.delete(entity)
         | 
| 197 | 
            +
                            await self.session.flush()
         | 
| 485 198 | 
             
                            return True
         | 
| 486 199 | 
             
                        return False
         | 
| 487 | 
            -
             | 
| 488 | 
            -
                        e.detail = f"Repository real delete error for {self.model.__tablename__}: {e.detail}"
         | 
| 489 | 
            -
                        e.source_function = f"{self.__class__.__name__}.real_row_delete -> {e.source_function}"
         | 
| 490 | 
            -
                        raise e
         | 
| 200 | 
            +
                        
         | 
| 491 201 | 
             
                    except SQLAlchemyError as e:
         | 
| 202 | 
            +
                        await self.session.rollback()
         | 
| 492 203 | 
             
                        raise CustomException(
         | 
| 493 204 | 
             
                            ErrorCode.DB_DELETE_ERROR,
         | 
| 494 205 | 
             
                            detail=f"Database real delete error in {self.model.__tablename__}: {str(e)}",
         | 
| 495 | 
            -
                            source_function=f"{self.__class__.__name__}. | 
| 496 | 
            -
                            original_error=e
         | 
| 497 | 
            -
                        )
         | 
| 498 | 
            -
                    except Exception as e:
         | 
| 499 | 
            -
                        raise CustomException(
         | 
| 500 | 
            -
                            ErrorCode.DB_DELETE_ERROR,
         | 
| 501 | 
            -
                            detail=f"Unexpected repository real delete error in {self.model.__tablename__}: {str(e)}",
         | 
| 502 | 
            -
                            source_function=f"{self.__class__.__name__}.real_row_delete",
         | 
| 206 | 
            +
                            source_function=f"{self.__class__.__name__}.real_delete",
         | 
| 503 207 | 
             
                            original_error=e
         | 
| 504 208 | 
             
                        ) 
         | 
    
        aiteamutils/base_service.py
    CHANGED
    
    | @@ -9,6 +9,7 @@ from .base_repository import BaseRepository | |
| 9 9 | 
             
            from .security import hash_password
         | 
| 10 10 | 
             
            from fastapi import Request
         | 
| 11 11 | 
             
            from ulid import ULID
         | 
| 12 | 
            +
            from sqlalchemy import select
         | 
| 12 13 |  | 
| 13 14 | 
             
            ModelType = TypeVar("ModelType", bound=DeclarativeBase)
         | 
| 14 15 |  | 
| @@ -30,11 +31,29 @@ class BaseService(Generic[ModelType]): | |
| 30 31 | 
             
                    self.repository = repository
         | 
| 31 32 | 
             
                    self.model = repository.model
         | 
| 32 33 | 
             
                    self.additional_models = additional_models or {}
         | 
| 33 | 
            -
                    self. | 
| 34 | 
            +
                    self._session = None
         | 
| 34 35 | 
             
                    self.searchable_fields = {
         | 
| 35 36 | 
             
                        "name": {"type": "text", "description": "이름"},
         | 
| 36 37 | 
             
                        "organization_ulid": {"type": "exact", "description": "조직 ID"}
         | 
| 37 38 | 
             
                    }
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                @property
         | 
| 41 | 
            +
                def session(self):
         | 
| 42 | 
            +
                    """현재 세션을 반환합니다."""
         | 
| 43 | 
            +
                    if self._session is None:
         | 
| 44 | 
            +
                        raise CustomException(
         | 
| 45 | 
            +
                            ErrorCode.DB_CONNECTION_ERROR,
         | 
| 46 | 
            +
                            detail="Database session is not set",
         | 
| 47 | 
            +
                            source_function=f"{self.__class__.__name__}.session"
         | 
| 48 | 
            +
                        )
         | 
| 49 | 
            +
                    return self._session
         | 
| 50 | 
            +
                
         | 
| 51 | 
            +
                @session.setter
         | 
| 52 | 
            +
                def session(self, value):
         | 
| 53 | 
            +
                    """세션을 설정합니다."""
         | 
| 54 | 
            +
                    self._session = value
         | 
| 55 | 
            +
                    if hasattr(self.repository, 'session'):
         | 
| 56 | 
            +
                        self.repository.session = value
         | 
| 38 57 |  | 
| 39 58 | 
             
                #########################
         | 
| 40 59 | 
             
                # 2. 이벤트 처리 메서드 #
         | 
| @@ -448,18 +467,7 @@ class BaseService(Generic[ModelType]): | |
| 448 467 | 
             
                        )
         | 
| 449 468 |  | 
| 450 469 | 
             
                async def delete(self, ulid: str, model_name: str = None) -> bool:
         | 
| 451 | 
            -
                    """엔티티를 소프트 삭제합니다 (is_deleted = True).
         | 
| 452 | 
            -
             | 
| 453 | 
            -
                    Args:
         | 
| 454 | 
            -
                        ulid (str): 삭제할 엔티티의 ULID
         | 
| 455 | 
            -
                        model_name (str, optional): 삭제할 모델 이름. Defaults to None.
         | 
| 456 | 
            -
             | 
| 457 | 
            -
                    Returns:
         | 
| 458 | 
            -
                        bool: 삭제 성공 여부
         | 
| 459 | 
            -
             | 
| 460 | 
            -
                    Raises:
         | 
| 461 | 
            -
                        CustomException: 데이터베이스 작업 중 오류 발생 시
         | 
| 462 | 
            -
                    """
         | 
| 470 | 
            +
                    """엔티티를 소프트 삭제합니다 (is_deleted = True)."""
         | 
| 463 471 | 
             
                    try:
         | 
| 464 472 | 
             
                        if model_name:
         | 
| 465 473 | 
             
                            if model_name not in self.additional_models:
         | 
| @@ -468,23 +476,24 @@ class BaseService(Generic[ModelType]): | |
| 468 476 | 
             
                                    detail=f"Model {model_name} not registered",
         | 
| 469 477 | 
             
                                    source_function=f"{self.__class__.__name__}.delete"
         | 
| 470 478 | 
             
                                )
         | 
| 471 | 
            -
                             | 
| 479 | 
            +
                            
         | 
| 480 | 
            +
                            stmt = select(self.additional_models[model_name]).filter_by(ulid=ulid, is_deleted=False)
         | 
| 481 | 
            +
                            result = await self.session.execute(stmt)
         | 
| 482 | 
            +
                            entity = result.scalars().first()
         | 
| 483 | 
            +
                            
         | 
| 472 484 | 
             
                            if not entity:
         | 
| 473 485 | 
             
                                raise CustomException(
         | 
| 474 486 | 
             
                                    ErrorCode.NOT_FOUND,
         | 
| 475 487 | 
             
                                    detail=f"{self.additional_models[model_name].__tablename__}|ulid|{ulid}",
         | 
| 476 488 | 
             
                                    source_function=f"{self.__class__.__name__}.delete"
         | 
| 477 489 | 
             
                                )
         | 
| 490 | 
            +
                            
         | 
| 491 | 
            +
                            entity.is_deleted = True
         | 
| 492 | 
            +
                            await self.session.flush()
         | 
| 478 493 | 
             
                            return True
         | 
| 479 494 |  | 
| 480 | 
            -
                         | 
| 481 | 
            -
                         | 
| 482 | 
            -
                            raise CustomException(
         | 
| 483 | 
            -
                                ErrorCode.NOT_FOUND,
         | 
| 484 | 
            -
                                detail=f"{self.model.__tablename__}|ulid|{ulid}",
         | 
| 485 | 
            -
                                source_function=f"{self.__class__.__name__}.delete"
         | 
| 486 | 
            -
                            )
         | 
| 487 | 
            -
                        return True
         | 
| 495 | 
            +
                        return await self.repository.delete(ulid)
         | 
| 496 | 
            +
                        
         | 
| 488 497 | 
             
                    except CustomException as e:
         | 
| 489 498 | 
             
                        raise e
         | 
| 490 499 | 
             
                    except Exception as e:
         | 
| @@ -549,20 +558,7 @@ class BaseService(Generic[ModelType]): | |
| 549 558 | 
             
                    request: Request | None = None,
         | 
| 550 559 | 
             
                    response_model: Any = None
         | 
| 551 560 | 
             
                ) -> List[Dict[str, Any]]:
         | 
| 552 | 
            -
                    """엔티티 목록을 조회합니다.
         | 
| 553 | 
            -
             | 
| 554 | 
            -
                    Args:
         | 
| 555 | 
            -
                        skip (int, optional): 건너뛸 레코드 수. Defaults to 0.
         | 
| 556 | 
            -
                        limit (int, optional): 조회할 최대 레코드 수. Defaults to 100.
         | 
| 557 | 
            -
                        filters (Dict[str, Any] | None, optional): 필터링 조건. Defaults to None.
         | 
| 558 | 
            -
                        search_params (Dict[str, Any] | None, optional): 검색 파라미터. Defaults to None.
         | 
| 559 | 
            -
                        model_name (str | None, optional): 조회할 모델 이름. Defaults to None.
         | 
| 560 | 
            -
                        request (Request | None, optional): 요청 객체. Defaults to None.
         | 
| 561 | 
            -
                        response_model (Any, optional): 응답 스키마. Defaults to None.
         | 
| 562 | 
            -
             | 
| 563 | 
            -
                    Returns:
         | 
| 564 | 
            -
                        List[Dict[str, Any]]: 엔티티 목록
         | 
| 565 | 
            -
                    """
         | 
| 561 | 
            +
                    """엔티티 목록을 조회합니다."""
         | 
| 566 562 | 
             
                    try:
         | 
| 567 563 | 
             
                        if model_name:
         | 
| 568 564 | 
             
                            if model_name not in self.additional_models:
         | 
| @@ -571,21 +567,28 @@ class BaseService(Generic[ModelType]): | |
| 571 567 | 
             
                                    detail=f"Model {model_name} not registered",
         | 
| 572 568 | 
             
                                    source_function=f"{self.__class__.__name__}.list"
         | 
| 573 569 | 
             
                                )
         | 
| 574 | 
            -
                             | 
| 575 | 
            -
             | 
| 576 | 
            -
                                 | 
| 577 | 
            -
                                limit=limit,
         | 
| 578 | 
            -
                                filters=filters
         | 
| 570 | 
            +
                            
         | 
| 571 | 
            +
                            stmt = select(self.additional_models[model_name]).where(
         | 
| 572 | 
            +
                                self.additional_models[model_name].is_deleted == False
         | 
| 579 573 | 
             
                            )
         | 
| 574 | 
            +
                            
         | 
| 575 | 
            +
                            if filters:
         | 
| 576 | 
            +
                                for key, value in filters.items():
         | 
| 577 | 
            +
                                    if value is not None:
         | 
| 578 | 
            +
                                        stmt = stmt.where(getattr(self.additional_models[model_name], key) == value)
         | 
| 579 | 
            +
                            
         | 
| 580 | 
            +
                            stmt = stmt.offset(skip).limit(limit)
         | 
| 581 | 
            +
                            result = await self.session.execute(stmt)
         | 
| 582 | 
            +
                            entities = result.scalars().all()
         | 
| 583 | 
            +
                            
         | 
| 580 584 | 
             
                            return [self._process_response(entity, response_model) for entity in entities]
         | 
| 581 585 |  | 
| 582 | 
            -
                         | 
| 586 | 
            +
                        return await self.repository.list(
         | 
| 583 587 | 
             
                            skip=skip,
         | 
| 584 588 | 
             
                            limit=limit,
         | 
| 585 589 | 
             
                            filters=filters,
         | 
| 586 590 | 
             
                            search_params=search_params
         | 
| 587 591 | 
             
                        )
         | 
| 588 | 
            -
                        return [self._process_response(entity, response_model) for entity in entities]
         | 
| 589 592 |  | 
| 590 593 | 
             
                    except CustomException as e:
         | 
| 591 594 | 
             
                        e.detail = f"Service list error for {self.repository.model.__tablename__}: {e.detail}"
         |