Files
conai/backend/app/api/reports.py
sinmb79 2a4950d8a0 feat: CONAI Phase 1 MVP 초기 구현
소형 건설업체(100억 미만)를 위한 AI 기반 토목공사 통합관리 플랫폼

Backend (FastAPI):
- SQLAlchemy 모델 13개 (users, projects, wbs, tasks, daily_reports, reports, inspections, quality, weather, permits, rag, settings)
- API 라우터 11개 (auth, projects, tasks, daily_reports, reports, inspections, weather, rag, kakao, permits, settings)
- Services: Claude AI 래퍼, CPM Gantt 계산, 기상청 API, RAG(pgvector), 카카오 Skill API
- Alembic 마이그레이션 (pgvector 포함)
- pytest 테스트 (CPM, 날씨 경보)

Frontend (Next.js 15):
- 11개 페이지 (대시보드, 프로젝트, Gantt, 일보, 검측, 품질, 날씨, 인허가, RAG, 설정)
- TanStack Query + Zustand + Tailwind CSS

인프라:
- Docker Compose (PostgreSQL pgvector + backend + frontend)
- 한국어 README 및 설치 가이드

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:06:36 +09:00

145 lines
5.1 KiB
Python

import uuid
from datetime import date
from fastapi import APIRouter, HTTPException, status
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
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.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()