소형 건설업체(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>
145 lines
5.1 KiB
Python
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()
|