""" 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], }