"""
자산 QR 태그 — 서버·장비에 QR 부착 → 스캔 → CMDB 조회
엔드포인트:
POST /api/asset-qr/generate/{server_id} — QR 코드 생성
GET /api/asset-qr/scan/{qr_token} — QR 스캔 → 자산 정보
POST /api/asset-qr/checkin/{qr_token} — 실사 체크인
GET /api/asset-qr/print/{server_id} — 인쇄용 QR 라벨 HTML
GET /api/asset-qr/batch-print — 다수 자산 일괄 인쇄
GET /api/asset-qr/list — QR 발행 목록
GET /api/asset-qr/audit-log/{server_id}— 스캔 이력
"""
from __future__ import annotations
import base64
import io
import uuid
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import User, Server, AssetQRToken, AssetQRScanLog
router = APIRouter(prefix="/api/asset-qr", tags=["자산 QR 태그"])
BASE_URL = "https://zioinfo.co.kr:8443"
def _gen_qr(url: str) -> str:
"""QR base64 PNG 생성."""
try:
import qrcode
qr = qrcode.QRCode(version=1, box_size=8, border=3)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="#003366", back_color="white")
buf = io.BytesIO()
img.save(buf, format='PNG')
return base64.b64encode(buf.getvalue()).decode()
except ImportError:
return ""
class CheckinRequest(BaseModel):
note: Optional[str] = None
location: Optional[str] = None
@router.post("/generate/{server_id}")
async def generate_qr(
server_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
srv_row = await db.execute(select(Server).where(Server.id == server_id))
server = srv_row.scalar_one_or_none()
if not server:
raise HTTPException(404, "서버 없음")
# 기존 QR 확인
existing = await db.execute(select(AssetQRToken).where(AssetQRToken.server_id == server_id))
token_obj = existing.scalar_one_or_none()
if not token_obj:
token = uuid.uuid4().hex
scan_url = f"{BASE_URL}/api/asset-qr/scan/{token}"
qr_b64 = _gen_qr(scan_url)
token_obj = AssetQRToken(
qr_token=token,
server_id=server_id,
qr_data=qr_b64,
scan_count=0,
created_at=datetime.utcnow(),
)
db.add(token_obj)
await db.commit()
await db.refresh(token_obj)
return {
"ok": True,
"qr_token": token_obj.qr_token,
"scan_url": f"{BASE_URL}/api/asset-qr/scan/{token_obj.qr_token}",
"print_url": f"{BASE_URL}/api/asset-qr/print/{server_id}",
"qr_image_b64": token_obj.qr_data,
}
@router.get("/scan/{qr_token}")
async def scan_qr(
qr_token: str,
request: Request,
db: AsyncSession = Depends(get_db),
):
"""QR 스캔 → 자산 정보 반환. 인증 불필요 (공개 기본 정보)."""
row = await db.execute(select(AssetQRToken).where(AssetQRToken.qr_token == qr_token))
token_obj = row.scalar_one_or_none()
if not token_obj:
raise HTTPException(404, "유효하지 않은 QR 코드")
srv_row = await db.execute(select(Server).where(Server.id == token_obj.server_id))
server = srv_row.scalar_one_or_none()
if not server:
raise HTTPException(404, "서버 정보 없음")
# 스캔 횟수 증가
token_obj.scan_count = (token_obj.scan_count or 0) + 1
token_obj.last_scan_at = datetime.utcnow()
# 스캔 로그
log = AssetQRScanLog(
qr_token=qr_token,
scan_type="VIEW",
user_agent=request.headers.get("User-Agent", "")[:200],
scanned_at=datetime.utcnow(),
)
db.add(log)
await db.commit()
return {
"server_id": server.id,
"hostname": server.server_name or "미설정",
"ip_addr": "***.***.***.**", # 공개 응답에서 IP 마스킹
"os_type": server.os_type or "미상",
"cpu_cores": server.cpu_cores,
"memory_gb": round((server.memory_mb or 0) / 1024, 1),
"scan_count": token_obj.scan_count,
"last_scan_at": token_obj.last_scan_at,
"checkin_url": f"{BASE_URL}/api/asset-qr/checkin/{qr_token}",
}
@router.post("/checkin/{qr_token}")
async def checkin(
qr_token: str,
req: CheckinRequest,
request: Request,
db: AsyncSession = Depends(get_db),
):
"""실사 체크인 — 장비 위치·상태 기록."""
row = await db.execute(select(AssetQRToken).where(AssetQRToken.qr_token == qr_token))
token_obj = row.scalar_one_or_none()
if not token_obj:
raise HTTPException(404)
log = AssetQRScanLog(
qr_token=qr_token,
scan_type="CHECKIN",
user_agent=request.headers.get("User-Agent", "")[:200],
location=req.location,
note=req.note,
scanned_at=datetime.utcnow(),
)
db.add(log)
await db.commit()
return {"ok": True, "message": "실사 체크인 완료", "checked_at": log.scanned_at}
@router.get("/print/{server_id}", response_class=HTMLResponse)
async def print_label(
server_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""인쇄용 QR 라벨 HTML (50×30mm 라벨 크기)."""
srv_row = await db.execute(select(Server).where(Server.id == server_id))
server = srv_row.scalar_one_or_none()
if not server:
raise HTTPException(404)
token_row = await db.execute(select(AssetQRToken).where(AssetQRToken.server_id == server_id))
token_obj = token_row.scalar_one_or_none()
if not token_obj:
raise HTTPException(400, "먼저 QR을 생성하세요. POST /api/asset-qr/generate/{server_id}")
qr_img = f'' if token_obj.qr_data else ''
html = f"""