zioinfo-mail/workspace/guardia-itsm/routers/si_report.py
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +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],
}