Files
conai/backend/app/api/reports.py
sinmb79 0156d8ca4f feat: Phase 1 잔여 기능 구현 완료
- 품질시험 API: schemas/quality.py + api/quality.py (CRUD, 합격률 요약, 자동 합불 판정)
- PDF 생성: WeasyPrint + Jinja2 (작업일보/검측요청서/보고서 템플릿 + /pdf 다운로드 엔드포인트)
- RAG 시드 스크립트: scripts/seed_rag.py (PDF/TXT 청킹, 배치 임베딩, CLI)
- APScheduler: 날씨 3시간 주기 자동 수집 + 경보 평가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:39:05 +09:00

161 lines
5.9 KiB
Python

import uuid
from datetime import date
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import Response
from sqlalchemy import select, func
from app.deps import CurrentUser, DB
from app.models.report import Report, ReportType
from app.models.daily_report import DailyReport
from app.models.weather import WeatherAlert
from app.models.project import Project
from app.schemas.report import ReportGenerateRequest, ReportResponse
from app.services.report_gen import generate_weekly_report, generate_monthly_report
from app.services.pdf_service import generate_report_pdf
router = APIRouter(prefix="/projects/{project_id}/reports", tags=["공정보고서"])
# Report schemas (inline for simplicity)
from pydantic import BaseModel
from app.models.report import ReportType, ReportStatus
class ReportGenerateRequest(BaseModel):
report_type: ReportType
period_start: date
period_end: date
class ReportResponse(BaseModel):
id: uuid.UUID
project_id: uuid.UUID
report_type: ReportType
period_start: date
period_end: date
ai_draft_text: str | None
status: ReportStatus
pdf_s3_key: str | None
model_config = {"from_attributes": True}
async def _get_project_or_404(project_id: uuid.UUID, db: DB) -> Project:
result = await db.execute(select(Project).where(Project.id == project_id))
p = result.scalar_one_or_none()
if not p:
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
return p
def _compute_overall_progress(tasks) -> float:
if not tasks:
return 0.0
total = sum(t.progress_pct for t in tasks)
return total / len(tasks)
@router.get("", response_model=list[ReportResponse])
async def list_reports(project_id: uuid.UUID, db: DB, current_user: CurrentUser):
result = await db.execute(
select(Report)
.where(Report.project_id == project_id)
.order_by(Report.period_start.desc())
)
return result.scalars().all()
@router.post("/generate", response_model=ReportResponse, status_code=status.HTTP_201_CREATED)
async def generate_report(project_id: uuid.UUID, data: ReportGenerateRequest, db: DB, current_user: CurrentUser):
"""AI-generate weekly or monthly report draft."""
project = await _get_project_or_404(project_id, db)
# Get daily reports in period
daily_result = await db.execute(
select(DailyReport).where(
DailyReport.project_id == project_id,
DailyReport.report_date >= data.period_start,
DailyReport.report_date <= data.period_end,
).order_by(DailyReport.report_date)
)
daily_reports = daily_result.scalars().all()
# Get tasks for progress
from app.models.task import Task
tasks_result = await db.execute(select(Task).where(Task.project_id == project_id))
tasks = tasks_result.scalars().all()
overall_progress = _compute_overall_progress(tasks)
if data.report_type == ReportType.WEEKLY:
# Get weather alerts in period
alerts_result = await db.execute(
select(WeatherAlert).where(
WeatherAlert.project_id == project_id,
WeatherAlert.alert_date >= data.period_start,
WeatherAlert.alert_date <= data.period_end,
)
)
weather_alerts = alerts_result.scalars().all()
ai_text, content_json = await generate_weekly_report(
project_name=project.name,
period_start=str(data.period_start),
period_end=str(data.period_end),
daily_reports=daily_reports,
overall_progress_pct=overall_progress,
weather_alerts=weather_alerts,
)
else:
ai_text, content_json = await generate_monthly_report(
project_name=project.name,
period_start=str(data.period_start),
period_end=str(data.period_end),
daily_reports=daily_reports,
overall_progress_pct=overall_progress,
)
report = Report(
project_id=project_id,
report_type=data.report_type,
period_start=data.period_start,
period_end=data.period_end,
content_json=content_json,
ai_draft_text=ai_text,
)
db.add(report)
await db.commit()
await db.refresh(report)
return report
@router.get("/{report_id}", response_model=ReportResponse)
async def get_report(project_id: uuid.UUID, report_id: uuid.UUID, db: DB, current_user: CurrentUser):
result = await db.execute(select(Report).where(Report.id == report_id, Report.project_id == project_id))
report = result.scalar_one_or_none()
if not report:
raise HTTPException(status_code=404, detail="보고서를 찾을 수 없습니다")
return report
@router.get("/{report_id}/pdf")
async def download_report_pdf(project_id: uuid.UUID, report_id: uuid.UUID, db: DB, current_user: CurrentUser):
"""공정보고서 PDF 다운로드"""
r = await db.execute(select(Report).where(Report.id == report_id, Report.project_id == project_id))
report = r.scalar_one_or_none()
if not report:
raise HTTPException(status_code=404, detail="보고서를 찾을 수 없습니다")
project = await _get_project_or_404(project_id, db)
pdf_bytes = generate_report_pdf(report, project)
filename = f"report_{report.report_type.value}_{report.period_start}.pdf"
return Response(content=pdf_bytes, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"})
@router.delete("/{report_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_report(project_id: uuid.UUID, report_id: uuid.UUID, db: DB, current_user: CurrentUser):
result = await db.execute(select(Report).where(Report.id == report_id, Report.project_id == project_id))
report = result.scalar_one_or_none()
if not report:
raise HTTPException(status_code=404, detail="보고서를 찾을 수 없습니다")
await db.delete(report)
await db.commit()