326 lines
11 KiB
Python
326 lines
11 KiB
Python
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": "상태 업데이트 완료"}
|