# 01. 인프라 - 설정, DB, 마이그레이션, 의존성 주입

## 환경 설정 (`app/config.py`)

```python
class Settings(BaseSettings):
    DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/ootd_poc"
    LLM_PROVIDER: str = "gemini"          # "gemini" 또는 "openai"
    MANNEQUIN_MODE: str = "edit"          # "edit" 또는 "generate"
    GEMINI_API_KEY: str = ""
    OPENAI_API_KEY: str = ""
    FACE_MATCH_THRESHOLD: float = 0.5     # 얼굴 매칭 유사도 임계값
    UPLOAD_DIR: str = "storage"           # 이미지 저장 경로
```

- `pydantic-settings`의 `BaseSettings`를 사용하여 `.env` 파일이나 환경변수에서 자동으로 값을 로드합니다.
- `settings = Settings()`로 싱글톤 인스턴스를 생성하고, 전체 앱에서 `from app.config import settings`로 임포트합니다.

### 핵심 설정값 설명

| 변수 | 설명 |
|------|------|
| `DATABASE_URL` | PostgreSQL 접속 URL. `asyncpg` 드라이버 사용 |
| `LLM_PROVIDER` | `"gemini"` → Gemini API, `"openai"` → OpenAI API |
| `MANNEQUIN_MODE` | 현재 `"edit"`만 사용 (원본 이미지에서 옷 추출) |
| `FACE_MATCH_THRESHOLD` | 0~1 사이. 높을수록 엄격한 매칭. 0.5는 "같은 사람일 확률 50% 이상" |
| `UPLOAD_DIR` | 이미지 파일이 저장되는 루트 디렉토리 |

---

## 데이터베이스 연결 (`app/dependencies.py` 상단)

```python
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_factory() as session:
        yield session
```

### 동작 방식
1. `create_async_engine` — asyncpg를 사용하는 비동기 DB 엔진 생성
2. `async_sessionmaker` — 세션 팩토리. `expire_on_commit=False`로 커밋 후에도 객체 속성 접근 가능
3. `get_db()` — FastAPI의 `Depends()`를 통해 각 요청마다 독립적인 DB 세션 제공
4. `async with`로 감싸져 있어 요청 종료 시 자동으로 세션이 닫힘

---

## 의존성 주입 (`app/dependencies.py`)

FastAPI의 `Depends()` 시스템을 활용하여 서비스 인스턴스를 라우터에 주입합니다.

### 패턴: 무거운 객체는 싱글톤, 서비스는 매번 생성

```python
@lru_cache(maxsize=1)
def get_face_analyzer():
    """FaceAnalyzer: 모델 로딩이 무거워서 한 번만 생성하고 캐싱"""
    return FaceAnalyzer()

def get_face_service():
    """FaceService: 가벼운 객체, 매번 생성하되 내부의 analyzer는 싱글톤"""
    return FaceService(analyzer=get_face_analyzer())
```

### 의존성 그래프

```
Router
  ├── get_db()                → AsyncSession (매 요청마다)
  ├── get_face_service()      → FaceService
  │     └── get_face_analyzer()  → FaceAnalyzer (싱글톤, @lru_cache)
  ├── get_garment_service()   → GarmentService
  │     └── get_llm_provider()   → GeminiProvider or OpenAIProvider (싱글톤, @lru_cache)
  └── get_wardrobe_service()  → WardrobeService
```

### LLM Provider 선택 로직

```python
@lru_cache(maxsize=1)
def get_llm_provider():
    provider_name = settings.LLM_PROVIDER.lower()
    if provider_name == "openai":
        return OpenAIProvider()
    else:
        return GeminiProvider()   # 기본값
```

- `.env`에서 `LLM_PROVIDER=openai` 또는 `LLM_PROVIDER=gemini`로 전환
- 서버 시작 시 한 번만 결정되고, 런타임 중 변경 불가 (`@lru_cache`)

---

## DB 마이그레이션 (Alembic)

### 설정 구조

- `alembic.ini` — Alembic 기본 설정 (스크립트 위치 등)
- `alembic/env.py` — 마이그레이션 실행 환경 설정

### `alembic/env.py` 핵심 포인트

```python
from app.config import settings
from app.models.base import Base
from app.models.user import User          # 모델 import 필수!
from app.models.garment import Garment
from app.models.face import FaceEmbedding

config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
target_metadata = Base.metadata
```

- **모든 모델을 명시적으로 import**해야 `Base.metadata`에 테이블 정보가 등록됩니다
- `settings.DATABASE_URL`을 사용하여 `.env`의 DB URL과 동기화
- `run_async_migrations()` — 비동기 마이그레이션 지원 (`asyncio.run`)

### 마이그레이션 파일

| 파일 | 내용 |
|------|------|
| `21a606c2592a_initial_tables.py` | users, garments, face_embeddings 테이블 생성 |
| `2527bdb25fb7_face_embedding_vector_512.py` | face_embeddings에 VECTOR(512) 컬럼 추가 |
| `99cce4e312a2_drop_is_global_rename_global_to_unknown.py` | is_global 제거, global→unknown user 전환 |

### 자주 쓰는 명령어

```bash
# 마이그레이션 적용
alembic upgrade head

# 새 마이그레이션 생성 (모델 변경 후)
alembic revision --autogenerate -m "설명"

# 현재 상태 확인
alembic current

# 마이그레이션 되돌리기
alembic downgrade -1
```

---

## 미들웨어 (`app/middleware.py`)

```python
class ErrorHandlingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        request_id = uuid.uuid4().hex[:8]   # 8자리 고유 ID
        start = time.time()
        try:
            response = await call_next(request)
            # 성공 로깅: "POST /api/v1/garments/analyze -> 200 (1.234s) [a1b2c3d4]"
            response.headers["X-Request-ID"] = request_id
            return response
        except Exception:
            # 실패 시 500 JSON 응답 + 에러 로깅
            return JSONResponse(status_code=500, content={...})
```

### 역할
1. **요청 ID 생성** — 모든 요청에 `X-Request-ID` 헤더 부여
2. **요청 로깅** — 메서드, 경로, 상태코드, 응답 시간 기록
3. **전역 에러 핸들링** — 처리되지 않은 예외를 500 JSON 응답으로 변환

---

## 앱 진입점 (`app/main.py`)

```python
@asynccontextmanager
async def lifespan(app: FastAPI):
    setup_logging()                          # 로깅 설정
    # 이미지 저장 디렉토리 생성
    for sub in ("faces", "originals", "ghost_mannequin"):
        (upload_dir / sub).mkdir(parents=True, exist_ok=True)
    yield                                    # 앱 실행
    # (셧다운 로직)

app = FastAPI(title="OOTD POC Server", version="0.2.0", lifespan=lifespan)
app.add_middleware(ErrorHandlingMiddleware)
app.include_router(health.router)
app.include_router(user.router)
app.include_router(garment.router)
app.include_router(face.router)
app.include_router(admin.router)
app.mount("/storage", StaticFiles(directory=settings.UPLOAD_DIR), name="storage")
```

### 시작 순서
1. `lifespan` 실행 → 로깅 설정, 디렉토리 생성
2. 미들웨어 등록 → 에러 핸들링
3. 라우터 등록 → API 엔드포인트
4. `/storage` 마운트 → 이미지 파일 정적 서빙

### 정적 파일 서빙
- `storage/` 디렉토리가 `/storage` URL로 마운트됩니다
- 예: `storage/ghost_mannequin/abc/def.png` → `http://localhost:8000/storage/ghost_mannequin/abc/def.png`
- DB에는 `ghost_mannequin/abc/def.png` (상대 경로)만 저장하고, API 응답에서 `/storage/` 접두사를 붙입니다
