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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy import select
|
||||
from app.deps import CurrentUser, DB
|
||||
from app.models.daily_report import DailyReport, InputSource
|
||||
@@ -9,6 +10,7 @@ from app.schemas.daily_report import (
|
||||
DailyReportCreate, DailyReportUpdate, DailyReportGenerateRequest, DailyReportResponse
|
||||
)
|
||||
from app.services.daily_report_gen import generate_work_content
|
||||
from app.services.pdf_service import generate_daily_report_pdf
|
||||
|
||||
router = APIRouter(prefix="/projects/{project_id}/daily-reports", tags=["작업일보"])
|
||||
|
||||
@@ -97,6 +99,20 @@ async def update_report(project_id: uuid.UUID, report_id: uuid.UUID, data: Daily
|
||||
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(DailyReport).where(DailyReport.id == report_id, DailyReport.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_daily_report_pdf(report, project)
|
||||
filename = f"daily_report_{report.report_date}.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(DailyReport).where(DailyReport.id == report_id, DailyReport.project_id == project_id))
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import uuid
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy import select
|
||||
from app.deps import CurrentUser, DB
|
||||
from app.models.inspection import InspectionRequest
|
||||
from app.models.project import Project, WBSItem
|
||||
from app.schemas.inspection import InspectionCreate, InspectionUpdate, InspectionGenerateRequest, InspectionResponse
|
||||
from app.services.inspection_gen import generate_checklist
|
||||
from app.services.pdf_service import generate_inspection_pdf
|
||||
|
||||
router = APIRouter(prefix="/projects/{project_id}/inspections", tags=["검측요청서"])
|
||||
|
||||
@@ -96,6 +98,20 @@ async def update_inspection(project_id: uuid.UUID, inspection_id: uuid.UUID, dat
|
||||
return insp
|
||||
|
||||
|
||||
@router.get("/{inspection_id}/pdf")
|
||||
async def download_inspection_pdf(project_id: uuid.UUID, inspection_id: uuid.UUID, db: DB, current_user: CurrentUser):
|
||||
"""검측요청서 PDF 다운로드"""
|
||||
r = await db.execute(select(InspectionRequest).where(InspectionRequest.id == inspection_id, InspectionRequest.project_id == project_id))
|
||||
insp = r.scalar_one_or_none()
|
||||
if not insp:
|
||||
raise HTTPException(status_code=404, detail="검측요청서를 찾을 수 없습니다")
|
||||
project = await _get_project_or_404(project_id, db)
|
||||
pdf_bytes = generate_inspection_pdf(insp, project)
|
||||
filename = f"inspection_{insp.requested_date}_{insp.inspection_type}.pdf"
|
||||
return Response(content=pdf_bytes, media_type="application/pdf",
|
||||
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"})
|
||||
|
||||
|
||||
@router.delete("/{inspection_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_inspection(project_id: uuid.UUID, inspection_id: uuid.UUID, db: DB, current_user: CurrentUser):
|
||||
result = await db.execute(select(InspectionRequest).where(InspectionRequest.id == inspection_id, InspectionRequest.project_id == project_id))
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import uuid
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlalchemy import select, func
|
||||
from app.deps import CurrentUser, DB
|
||||
from app.models.quality import QualityTest, QualityResult
|
||||
from app.models.project import Project
|
||||
from app.schemas.quality import QualityTestCreate, QualityTestUpdate, QualityTestResponse
|
||||
|
||||
router = APIRouter(prefix="/projects/{project_id}/quality", tags=["품질시험"])
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@router.get("", response_model=list[QualityTestResponse])
|
||||
async def list_quality_tests(
|
||||
project_id: uuid.UUID,
|
||||
db: DB,
|
||||
current_user: CurrentUser,
|
||||
test_type: str | None = None,
|
||||
result: QualityResult | None = None,
|
||||
):
|
||||
query = select(QualityTest).where(QualityTest.project_id == project_id)
|
||||
if test_type:
|
||||
query = query.where(QualityTest.test_type == test_type)
|
||||
if result:
|
||||
query = query.where(QualityTest.result == result)
|
||||
query = query.order_by(QualityTest.test_date.desc())
|
||||
rows = await db.execute(query)
|
||||
return rows.scalars().all()
|
||||
|
||||
|
||||
@router.post("", response_model=QualityTestResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_quality_test(
|
||||
project_id: uuid.UUID,
|
||||
data: QualityTestCreate,
|
||||
db: DB,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
await _get_project_or_404(project_id, db)
|
||||
test = QualityTest(**data.model_dump(), project_id=project_id)
|
||||
db.add(test)
|
||||
await db.commit()
|
||||
await db.refresh(test)
|
||||
return test
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def quality_summary(project_id: uuid.UUID, db: DB, current_user: CurrentUser):
|
||||
"""프로젝트 품질시험 합격률 요약"""
|
||||
total_q = await db.execute(
|
||||
select(func.count()).where(QualityTest.project_id == project_id)
|
||||
)
|
||||
total = total_q.scalar() or 0
|
||||
|
||||
pass_q = await db.execute(
|
||||
select(func.count()).where(
|
||||
QualityTest.project_id == project_id,
|
||||
QualityTest.result == QualityResult.PASS,
|
||||
)
|
||||
)
|
||||
passed = pass_q.scalar() or 0
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"passed": passed,
|
||||
"failed": total - passed,
|
||||
"pass_rate": round(passed / total * 100, 1) if total > 0 else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{test_id}", response_model=QualityTestResponse)
|
||||
async def get_quality_test(
|
||||
project_id: uuid.UUID, test_id: uuid.UUID, db: DB, current_user: CurrentUser
|
||||
):
|
||||
result = await db.execute(
|
||||
select(QualityTest).where(
|
||||
QualityTest.id == test_id, QualityTest.project_id == project_id
|
||||
)
|
||||
)
|
||||
test = result.scalar_one_or_none()
|
||||
if not test:
|
||||
raise HTTPException(status_code=404, detail="품질시험 기록을 찾을 수 없습니다")
|
||||
return test
|
||||
|
||||
|
||||
@router.put("/{test_id}", response_model=QualityTestResponse)
|
||||
async def update_quality_test(
|
||||
project_id: uuid.UUID,
|
||||
test_id: uuid.UUID,
|
||||
data: QualityTestUpdate,
|
||||
db: DB,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
result = await db.execute(
|
||||
select(QualityTest).where(
|
||||
QualityTest.id == test_id, QualityTest.project_id == project_id
|
||||
)
|
||||
)
|
||||
test = result.scalar_one_or_none()
|
||||
if not test:
|
||||
raise HTTPException(status_code=404, detail="품질시험 기록을 찾을 수 없습니다")
|
||||
|
||||
update_data = data.model_dump(exclude_none=True)
|
||||
|
||||
# 측정값/기준값 변경 시 합격 여부 재계산
|
||||
new_measured = update_data.get("measured_value", test.measured_value)
|
||||
new_design = update_data.get("design_value", test.design_value)
|
||||
if "result" not in update_data and new_design is not None:
|
||||
update_data["result"] = QualityResult.PASS if new_measured >= new_design else QualityResult.FAIL
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(test, field, value)
|
||||
await db.commit()
|
||||
await db.refresh(test)
|
||||
return test
|
||||
|
||||
|
||||
@router.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_quality_test(
|
||||
project_id: uuid.UUID, test_id: uuid.UUID, db: DB, current_user: CurrentUser
|
||||
):
|
||||
result = await db.execute(
|
||||
select(QualityTest).where(
|
||||
QualityTest.id == test_id, QualityTest.project_id == project_id
|
||||
)
|
||||
)
|
||||
test = result.scalar_one_or_none()
|
||||
if not test:
|
||||
raise HTTPException(status_code=404, detail="품질시험 기록을 찾을 수 없습니다")
|
||||
await db.delete(test)
|
||||
await db.commit()
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -9,6 +10,7 @@ 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=["공정보고서"])
|
||||
|
||||
@@ -134,6 +136,20 @@ async def get_report(project_id: uuid.UUID, report_id: uuid.UUID, db: DB, curren
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user