guardia-itsm/routers/si_report.py
DESKTOP-TKLFCPRython 1f8b926066 feat(itsm): PMS/준수성/JMeter/공공기관 기능 + Nifty UI + 로고 Copyright
[PMS 완성]
- core/si_report.py: 일/주/월 보고서 (Excel/HTML/PDF/DOCX/PPTX)
- routers/si_report.py: daily|weekly|monthly + 메신저 발송
- routers/deliverables.py: 산출물 CRUD + 제출/검토
- si_issues.py: 이슈→SR 자동 연결
- scheduler.py: 일일 18:00 + 주간 금 17:00 자동 보고서
- models.py: Deliverable 모델

[준수성 자동 점검]
- core/compliance_check.py: SC-8개/WA-7개/PI-6개 규칙
- routers/compliance.py: 스캔 + HTML/Excel 보고서

[JMeter 성능 테스트]
- routers/jmeter.py: JTL 업로드 + 내장 부하 테스트 + 보고서

[공공기관 필수 기능]
- routers/public_checklist.py: 행안부 기준 19개 항목

[UI/브랜드]
- 로고(ziologo.png) + Copyright 2026 All Rights Reserved
- Nifty 계층형 사이드바 (PMS 서브메뉴)
- X-Powered-By + X-Copyright 응답 헤더
- manual/15_UI_Nifty_가이드.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 22:50:29 +09:00

190 lines
7.8 KiB
Python

"""
SI 프로젝트 보고서 생성 API
엔드포인트:
GET /api/si/projects/{pid}/report/daily — 일일 보고서
GET /api/si/projects/{pid}/report/weekly — 주간 보고서
GET /api/si/projects/{pid}/report/monthly — 월간 보고서
GET /api/si/projects/{pid}/report/status — 현황 요약 (JSON)
POST /api/si/projects/{pid}/report/send — 메신저 자동 발송
쿼리 파라미터:
format: excel | html | pdf | docx | pptx (기본: html)
send_messenger: true — 발송 후 파일도 반환
"""
from __future__ import annotations
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response, HTMLResponse
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import SiProject, User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/si/projects", tags=["si_report"])
_MIME = {
"excel": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"html": "text/html; charset=utf-8",
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
}
_EXT = {"excel": "xlsx", "html": "html", "pdf": "pdf", "docx": "docx", "pptx": "pptx"}
class SendRequest(BaseModel):
room: str = "ops"
report_type: str = "weekly"
fmt: str = "excel"
async def _build_report(pid: int, report_type: str, fmt: str, db: AsyncSession):
"""보고서 생성 공통 헬퍼."""
from core.si_report import generate_report
try:
content, mime, filename = await generate_report(pid, report_type, fmt, db)
return content, mime, filename
except ValueError as e:
raise HTTPException(400, str(e))
except Exception as e:
logger.error("보고서 생성 오류: %s", e, exc_info=True)
raise HTTPException(500, f"보고서 생성 실패: {str(e)[:200]}")
# ── 일일 보고서 ───────────────────────────────────────────────────────────────
@router.get("/{pid}/report/daily")
async def daily_report(
pid: int,
format: str = Query("html", description="출력 형식: excel|html|pdf|docx|pptx"),
db: AsyncSession = Depends(get_db),
_u: User = Depends(get_current_user),
):
content, mime, filename = await _build_report(pid, "daily", format, db)
headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
if format == "html":
return HTMLResponse(content.decode("utf-8") if isinstance(content, bytes) else content)
return Response(content=content, media_type=mime, headers=headers)
# ── 주간 보고서 ───────────────────────────────────────────────────────────────
@router.get("/{pid}/report/weekly")
async def weekly_report(
pid: int,
format: str = Query("excel"),
db: AsyncSession = Depends(get_db),
_u: User = Depends(get_current_user),
):
content, mime, filename = await _build_report(pid, "weekly", format, db)
headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
if format == "html":
return HTMLResponse(content.decode("utf-8") if isinstance(content, bytes) else content)
return Response(content=content, media_type=mime, headers=headers)
# ── 월간 보고서 ───────────────────────────────────────────────────────────────
@router.get("/{pid}/report/monthly")
async def monthly_report(
pid: int,
format: str = Query("pptx"),
db: AsyncSession = Depends(get_db),
_u: User = Depends(get_current_user),
):
content, mime, filename = await _build_report(pid, "monthly", format, db)
headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
if format == "html":
return HTMLResponse(content.decode("utf-8") if isinstance(content, bytes) else content)
return Response(content=content, media_type=mime, headers=headers)
# ── 현황 요약 (JSON) ──────────────────────────────────────────────────────────
@router.get("/{pid}/report/status")
async def project_status(
pid: int,
db: AsyncSession = Depends(get_db),
_u: User = Depends(get_current_user),
):
"""프로젝트 현황 JSON 요약 (대시보드용)."""
from core.si_report import collect_project_data
data = await collect_project_data(pid, db)
proj = data["project"]
return {
"project_code": proj.project_code,
"project_name": proj.project_name,
"phase": proj.phase,
"health_status": proj.health_status,
"overall_progress": data["overall_progress"],
"budget_pct": data["budget_pct"],
"wbs_total": data["wbs_total"],
"wbs_done": data["wbs_done"],
"wbs_delayed": data["wbs_delayed"],
"issue_open": data["issue_open"],
"issue_closed": data["issue_closed"],
"high_risks": len(data["high_risks"]),
"overdue_deliverables": len(data["overdue_deliverables"]),
"upcoming_milestones": [
{"name": m.name, "target_date": str(m.target_date)}
for m in data["upcoming_milestones"]
],
}
# ── 메신저 자동 발송 ──────────────────────────────────────────────────────────
@router.post("/{pid}/report/send")
async def send_report(
pid: int,
body: SendRequest,
db: AsyncSession = Depends(get_db),
cu: User = Depends(get_current_user),
):
"""보고서를 생성하여 메신저 채널로 자동 발송."""
from core.si_report import collect_project_data, generate_excel, generate_html
import os, httpx
data = await collect_project_data(pid, db)
proj = data["project"]
rtype_name = {"daily": "일일", "weekly": "주간", "monthly": "월간"}.get(body.report_type, body.report_type)
# 텍스트 요약 생성
summary = (
f"[{rtype_name} 보고서] {proj.project_name}\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"단계: {proj.phase} | 건강상태: {proj.health_status}\n"
f"진척률: {data['overall_progress']}% | 예산소진: {data['budget_pct']}%\n"
f"WBS 완료: {data['wbs_done']}/{data['wbs_total']} | 지연: {data['wbs_delayed']}\n"
f"미결 이슈: {data['issue_open']}건 | 고위험: {len(data['high_risks'])}\n"
f"미제출 산출물: {len(data['overdue_deliverables'])}\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"상세: http://localhost:8001/si (SI 프로젝트 관리)"
)
# 메신저 발송
messenger_url = os.getenv("MESSENGER_BASE_URL", "http://localhost:8002")
try:
async with httpx.AsyncClient(timeout=5.0) as client:
await client.post(
f"{messenger_url}/api/webhook/itsm",
json={"room": body.room, "text": summary, "event": "project_report"},
)
except Exception:
pass # Fail-Safe
return {
"message": f"{rtype_name} 보고서 발송 완료",
"room": body.room,
"summary": summary[:200],
}