feat: 멀티테넌트 1단계 — 사용자별 KIS 계좌·포트폴리오·수동매매 (MULTI_TENANT_ENABLED 기본 OFF)

설계: docs/multi_tenant_design.md / 검수: docs/multi_tenant_phase1_review.md (High 0, MT=False 배포 가능)

데이터·인프라
- user_kis_credentials(Fernet 암호화, PBKDF2 키 파생, key_version), stock_bans_v2 신설,
  portfolio/trade_history/active_trades/trade_journals/trading_sessions 등 user_id nullable 추가
- alembic 0009~0011(idempotent, legacy owner 백필). main.py 기동 시 portfolio.user_id 스키마 검사(H1)
- Jenkinsfile: DB Migration 을 Deploy 앞으로, alembic 이력 없으면 stamp 0008 후 upgrade(N1)
- bootstrap: users 비어 있으면 BOOTSTRAP_ADMIN_* 로 관리자 시드 + .env KIS 자격증명 이관 + 백필(SAVEPOINT)

백엔드
- core/crypto.py, core/tenant_auth.py(get_current_user — 비활성 사용자 401, API 키→legacy owner,
  deny_non_owner_session_when_mt), core/dt_utils.py
- services/tenant: context, kis_pool, registry(UserContextRegistry, 락 최소화·복호화 to_thread), risk_factory
- KoreaInvestmentAPI.for_user(사용자별 인스턴스, 토큰캐시 users/{id}/, 시세는 공용 위임)
- trade_executor.safe_place_order_v2(사용자별 락/세마포어, legacy ctx 는 기존 키)
- mdd_manager/loss_tracker _v2 + ban_until tz-aware/naive 경계 정규화(레거시 경로 버그 포함 수정)
- db_worker: payload user_id 옵션 필터, upsert_stock_ban_v2, legacy ActiveTrade 조회 LIMIT 1
- API: /account/credentials(등록·마스킹조회·삭제·연결테스트·자동매매), /auth/admin/tenant/*,
  /portfolio/v2(조회·요약·sync·buy·sell), /trades/v2(+pending). PortfolioResponse Optional 수정

프론트
- /account 내 계좌 설정, useTenantMode(세션=v2 / API키=legacy, 미확정 시 쿼리·버튼 비활성),
  계좌 미등록 인라인 안내, v2 매수/매도/거래이력 분기

테스트: tests/integration(격리 Postgres, KIS 전면 mock) 20 passed / 1 xfailed(0012 이전 제약)
2단계(스케줄러 사용자화·legacy 라우터 격리) 전까지 MULTI_TENANT_ENABLED=True 전환 금지

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-21 10:26:51 +09:00
parent dc128ae4cf
commit b4a1fc79ab
52 changed files with 5614 additions and 96 deletions

View File

@@ -87,6 +87,27 @@ async def lifespan(app: FastAPI):
await conn.run_sync(Base.metadata.create_all)
logger.info("✅ Database tables created successfully")
# ── H1: alembic 마이그레이션 누락 조기 감지 ──────────────────────────
# `create_all()`은 신규 테이블만 만들고 기존 테이블에 컬럼을 추가하지 않는다.
# 멀티테넌트 1단계는 portfolio 등 기존 테이블에 `user_id` 컬럼을 추가했으므로,
# 배포 시 `alembic upgrade head`를 건너뛰면 이 컬럼이 없는 채로 기동돼
# SQLAlchemy가 매핑된 모든 컬럼을 SELECT/INSERT하다 UndefinedColumnError로
# 실패한다(자동매매 기록 전체 유실). 여기서 명시적으로 기동을 중단시킨다.
from sqlalchemy import text as _sa_text_check
async with engine.connect() as _check_conn:
_col_exists = (await _check_conn.execute(_sa_text_check(
"SELECT 1 FROM information_schema.columns "
"WHERE table_name='portfolio' AND column_name='user_id'"
))).fetchone()
if _col_exists is None:
logger.critical(
"❌ [H1] portfolio.user_id 컬럼이 없습니다 — alembic 마이그레이션이 적용되지 않았습니다. "
"배포 절차 위반: `cd backend && alembic upgrade head` 를 먼저 실행한 뒤 다시 기동하세요."
)
raise RuntimeError(
"Migration required: portfolio.user_id column missing. Run `alembic upgrade head` before starting the app."
)
try:
from app.services.loss_tracker import loss_tracker
async with AsyncSessionLocal() as session:
@@ -159,6 +180,13 @@ async def lifespan(app: FastAPI):
# CLI(`alembic upgrade head`)로 수행한다. 따라서 여기서는 호출하지 않는다.
logger.info("ℹ️ Alembic auto-migration skipped (schema via create_all; migrations run via CLI)")
# ── 멀티테넌트 부트스트랩: 관리자 계정 시드 + legacy KIS 자격증명 이관 + user_id 백필 ──
try:
from app.services.tenant.bootstrap import ensure_bootstrap_admin
await ensure_bootstrap_admin()
except Exception as e:
logger.warning(f"⚠️ 멀티테넌트 부트스트랩 실패 (non-critical): {e}")
# ── KRX 일봉 데이터 초기 수집 (DB 비어있을 때만 백그라운드 실행) ──────
try:
from app.models.models import StockDailyPrice