from __future__ import annotations from datetime import date, datetime from typing import Optional, List import httpx from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import User, LegacySystem router = APIRouter(prefix="/api/legacy", tags=["Legacy Modernization"]) def _tenant(user: User) -> str: return user.inst_code or str(user.id) async def _ollama(prompt: str) -> str: try: async with httpx.AsyncClient(timeout=30) as c: r = await c.post( "http://localhost:11434/api/generate", json={"model": "llama3", "prompt": prompt, "stream": False}, ) return r.json().get("response", "분석 결과 없음") except Exception: return "AI 분석 불가 (Ollama 연결 실패)" def _calc_risk(system: LegacySystem) -> float: score = 0.0 if system.eol_date: days_to_eol = (system.eol_date - date.today()).days if days_to_eol < 0: score += 40.0 elif days_to_eol < 90: score += 30.0 elif days_to_eol < 365: score += 20.0 elif days_to_eol < 730: score += 10.0 if system.tech_debt_score: score += min(system.tech_debt_score * 0.3, 30.0) if system.migration_status == "NOT_STARTED": score += 10.0 return min(score, 100.0) # ── Pydantic 스키마 ──────────────────────────────────────────────────────────── class LegacySystemIn(BaseModel): name: str os_name: Optional[str] = None os_version: Optional[str] = None middleware: Optional[str] = None eol_date: Optional[date] = None migration_strategy: Optional[str] = None tech_debt_score: Optional[float] = 0.0 notes: Optional[str] = None class LegacySystemOut(BaseModel): model_config = {"from_attributes": True} id: int name: str os_name: Optional[str] os_version: Optional[str] middleware: Optional[str] eol_date: Optional[date] risk_score: Optional[float] migration_strategy: Optional[str] migration_status: Optional[str] tech_debt_score: Optional[float] notes: Optional[str] created_at: datetime class AssessmentIn(BaseModel): system_id: int constraints: Optional[str] = None class MigrationPlanIn(BaseModel): system_ids: List[int] timeline_months: int = 12 class StatusUpdateIn(BaseModel): migration_status: str # ── 엔드포인트 ───────────────────────────────────────────────────────────────── @router.get("/systems", summary="레거시 시스템 목록") async def list_systems( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) rows = (await db.execute( select(LegacySystem).where(LegacySystem.tenant_id == tid) .order_by(LegacySystem.risk_score.desc()) )).scalars().all() return [LegacySystemOut.model_validate(r) for r in rows] @router.post("/systems", summary="레거시 시스템 등록") async def create_system( body: LegacySystemIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) sys = LegacySystem(tenant_id=tid, **body.model_dump()) sys.risk_score = _calc_risk(sys) db.add(sys) await db.commit() await db.refresh(sys) return LegacySystemOut.model_validate(sys) @router.get("/systems/{system_id}/risk", summary="EOL 위험도 자동 평가") async def evaluate_risk( system_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) sys = (await db.execute( select(LegacySystem).where(LegacySystem.tenant_id == tid, LegacySystem.id == system_id) )).scalar_one_or_none() if not sys: raise HTTPException(404, "시스템을 찾을 수 없습니다") score = _calc_risk(sys) prompt = ( f"레거시 시스템 위험도 분석:\n" f"- OS: {sys.os_name} {sys.os_version}\n" f"- EOL: {sys.eol_date}\n" f"- 기술 부채 점수: {sys.tech_debt_score}\n" f"- 마이그레이션 상태: {sys.migration_status}\n" f"위험 요인과 권고 조치를 간략히 설명하시오." ) analysis = await _ollama(prompt) sys.risk_score = score await db.commit() level = "CRITICAL" if score >= 70 else "HIGH" if score >= 50 else "MEDIUM" if score >= 30 else "LOW" return { "system_id": system_id, "name": sys.name, "risk_score": round(score, 1), "risk_level": level, "ai_analysis": analysis, } @router.post("/migration-plan", summary="마이그레이션 로드맵 AI 자동 생성") async def create_migration_plan( body: MigrationPlanIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) systems = (await db.execute( select(LegacySystem).where( LegacySystem.tenant_id == tid, LegacySystem.id.in_(body.system_ids), ).order_by(LegacySystem.risk_score.desc()) )).scalars().all() if not systems: raise HTTPException(404, "해당 시스템을 찾을 수 없습니다") summary = "\n".join( f"- {s.name} (OS:{s.os_name} {s.os_version}, 전략:{s.migration_strategy}, 위험:{s.risk_score:.0f})" for s in systems ) prompt = ( f"레거시 시스템 마이그레이션 로드맵 ({body.timeline_months}개월):\n{summary}\n" f"단계별 마이그레이션 계획, 우선순위, 예상 공수를 JSON 형식으로 제시하시오." ) plan = await _ollama(prompt) return { "timeline_months": body.timeline_months, "systems": [{"id": s.id, "name": s.name, "risk_score": s.risk_score} for s in systems], "roadmap": plan, } @router.get("/tech-debt", summary="기술 부채 지표 조회") async def get_tech_debt( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) rows = (await db.execute( select(LegacySystem).where(LegacySystem.tenant_id == tid) .order_by(LegacySystem.tech_debt_score.desc()) )).scalars().all() total = sum(r.tech_debt_score or 0 for r in rows) return { "total_debt_score": round(total, 1), "system_count": len(rows), "systems": [ {"id": r.id, "name": r.name, "tech_debt_score": r.tech_debt_score} for r in rows ], } @router.post("/assessment", summary="현대화 준비도 평가") async def assess_system( body: AssessmentIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) sys = (await db.execute( select(LegacySystem).where(LegacySystem.tenant_id == tid, LegacySystem.id == body.system_id) )).scalar_one_or_none() if not sys: raise HTTPException(404, "시스템을 찾을 수 없습니다") prompt = ( f"현대화 준비도 평가:\n" f"- 시스템: {sys.name}\n" f"- OS: {sys.os_name} {sys.os_version}\n" f"- 미들웨어: {sys.middleware}\n" f"- 기술 부채: {sys.tech_debt_score}\n" f"- 제약 사항: {body.constraints or '없음'}\n" f"lift-and-shift / refactor / replace 중 최적 전략과 이유를 제시하시오." ) assessment = await _ollama(prompt) return { "system_id": body.system_id, "name": sys.name, "recommended_strategy": sys.migration_strategy or "미정", "assessment": assessment, } @router.get("/eol-alerts", summary="EOL 임박 시스템 알림 목록") async def eol_alerts( days_threshold: int = Query(365, ge=1, le=3650), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) rows = (await db.execute( select(LegacySystem).where(LegacySystem.tenant_id == tid) )).scalars().all() alerts = [] today = date.today() for r in rows: if r.eol_date: days_left = (r.eol_date - today).days if days_left <= days_threshold: urgency = "EXPIRED" if days_left < 0 else "CRITICAL" if days_left < 90 else "WARNING" alerts.append({ "id": r.id, "name": r.name, "eol_date": str(r.eol_date), "days_remaining": days_left, "urgency": urgency, }) alerts.sort(key=lambda x: x["days_remaining"]) return {"alert_count": len(alerts), "alerts": alerts} @router.get("/migration-report/{system_id}", summary="전후 비교 보고서") async def migration_report( system_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): tid = _tenant(current_user) sys = (await db.execute( select(LegacySystem).where(LegacySystem.tenant_id == tid, LegacySystem.id == system_id) )).scalar_one_or_none() if not sys: raise HTTPException(404, "시스템을 찾을 수 없습니다") prompt = ( f"마이그레이션 전후 비교 보고서 작성:\n" f"- 시스템: {sys.name}\n" f"- 현재 OS: {sys.os_name} {sys.os_version}\n" f"- 전략: {sys.migration_strategy}\n" f"- 상태: {sys.migration_status}\n" f"Before/After 기대 효과, 위험 감소율, 예상 비용절감을 보고서 형식으로 작성하시오." ) report = await _ollama(prompt) return { "system_id": system_id, "name": sys.name, "current_status": sys.migration_status, "risk_score": sys.risk_score, "report": report, } @router.put("/systems/{system_id}/status", summary="마이그레이션 단계 상태 업데이트") async def update_status( system_id: int, body: StatusUpdateIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): valid_statuses = {"NOT_STARTED", "PLANNING", "IN_PROGRESS", "TESTING", "COMPLETED", "CANCELLED"} if body.migration_status not in valid_statuses: raise HTTPException(400, f"유효한 상태: {valid_statuses}") tid = _tenant(current_user) sys = (await db.execute( select(LegacySystem).where(LegacySystem.tenant_id == tid, LegacySystem.id == system_id) )).scalar_one_or_none() if not sys: raise HTTPException(404, "시스템을 찾을 수 없습니다") sys.migration_status = body.migration_status await db.commit() return {"id": system_id, "migration_status": body.migration_status, "message": "상태 업데이트 완료"}