[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>
190 lines
7.8 KiB
Python
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],
|
|
}
|