Files
conai/backend/app/services/completion_service.py
sinmb79 5a044a3882 feat: Phase 3 구현 — 완전 자동화, 준공도서, Vision L3, 발주처 포털
EVMS 완전 자동화:
- 공기 지연 AI 예측 (SPI 기반 준공일 예측)
- 기성청구 가능 금액 자동 산출
- 매일 자정 EVMS 스냅샷 자동 생성 (APScheduler)
- 매일 07:00 GONGSA 아침 브리핑 자동 생성

준공도서 패키지:
- 준공 요약 + 품질시험 목록 + 검측 이력 + 인허가 현황 → ZIP 번들
- 준공 준비 체크리스트 API
- 4종 HTML 템플릿 (WeasyPrint PDF 출력)

Vision AI Level 3:
- 설계 도면 vs 현장 사진 비교 보조 판독 (Claude Vision)
- 철근 배근, 거푸집 치수 1차 분석

설계도서 파싱:
- PDF 이미지/텍스트에서 공종·수량·규격 자동 추출
- Pandoc HWP 출력 지원

발주처 전용 포털:
- 토큰 기반 읽기 전용 API
- 공사 현황 대시보드, 공정률 추이 차트

에이전트 협업 고도화:
- 협업 시나리오 (concrete_pour, excavation, weekly_report)
- GONGSA→PUMJIL→ANJEON 순차 처리

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:02:29 +09:00

143 lines
5.0 KiB
Python

"""
준공도서 패키지 자동화 서비스
작업일보, 품질시험, 검측이력, 사진대장, 인허가 현황을 종합하여
준공도서 PDF 번들을 생성합니다.
"""
import io
import zipfile
from datetime import date
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.project import Project
from app.models.daily_report import DailyReport
from app.models.quality import QualityTest, QualityResult
from app.models.inspection import InspectionRequest, InspectionStatus
from app.models.permit import PermitItem, PermitStatus
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
_jinja = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)), autoescape=True)
def _html_to_pdf(html: str) -> bytes:
try:
from weasyprint import HTML
return HTML(string=html).write_pdf()
except ImportError:
raise RuntimeError("WeasyPrint가 설치되지 않았습니다. `pip install weasyprint`")
async def build_completion_package(
db: AsyncSession,
project_id,
) -> tuple[bytes, str]:
"""
준공도서 ZIP 패키지 생성.
반환: (zip_bytes, filename)
"""
import uuid
pid = uuid.UUID(str(project_id))
today = date.today()
# 프로젝트 정보
proj_r = await db.execute(select(Project).where(Project.id == pid))
project = proj_r.scalar_one_or_none()
if not project:
raise ValueError("프로젝트를 찾을 수 없습니다")
# 데이터 수집
dr_r = await db.execute(
select(DailyReport).where(DailyReport.project_id == pid).order_by(DailyReport.report_date)
)
daily_reports = dr_r.scalars().all()
qt_r = await db.execute(
select(QualityTest).where(QualityTest.project_id == pid).order_by(QualityTest.test_date)
)
quality_tests = qt_r.scalars().all()
insp_r = await db.execute(
select(InspectionRequest).where(InspectionRequest.project_id == pid).order_by(InspectionRequest.requested_date)
)
inspections = insp_r.scalars().all()
permit_r = await db.execute(
select(PermitItem).where(PermitItem.project_id == pid).order_by(PermitItem.sort_order)
)
permits = permit_r.scalars().all()
# 통계
total_dr = len(daily_reports)
total_qt = len(quality_tests)
pass_qt = sum(1 for q in quality_tests if q.result == QualityResult.PASS)
total_insp = len(inspections)
completed_insp = sum(1 for i in inspections if i.status == InspectionStatus.COMPLETED)
total_permits = len(permits)
approved_permits = sum(1 for p in permits if p.status == PermitStatus.APPROVED)
# 1. 준공 요약 PDF
summary_html = _jinja.get_template("completion_summary.html").render(
project=project,
today=today,
total_dr=total_dr,
total_qt=total_qt,
pass_qt=pass_qt,
pass_rate=round(pass_qt / total_qt * 100, 1) if total_qt else 0,
total_insp=total_insp,
completed_insp=completed_insp,
total_permits=total_permits,
approved_permits=approved_permits,
daily_reports=daily_reports[:5], # 최근 5개 미리보기
quality_tests=quality_tests[-10:], # 마지막 10개 미리보기
)
summary_pdf = _html_to_pdf(summary_html)
# 2. 품질시험 목록 PDF
qt_html = _jinja.get_template("quality_list.html").render(
project=project,
quality_tests=quality_tests,
today=today,
)
qt_pdf = _html_to_pdf(qt_html)
# 3. 검측 이력 PDF
insp_html = _jinja.get_template("inspection_list.html").render(
project=project,
inspections=inspections,
today=today,
)
insp_pdf = _html_to_pdf(insp_html)
# 4. 인허가 현황 PDF
permit_html = _jinja.get_template("permit_list.html").render(
project=project,
permits=permits,
today=today,
)
permit_pdf = _html_to_pdf(permit_html)
# ZIP 번들 생성
zip_buffer = io.BytesIO()
project_code = getattr(project, "code", str(pid)[:8])
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr(f"01_준공요약_{project_code}.pdf", summary_pdf)
zf.writestr(f"02_품질시험목록_{project_code}.pdf", qt_pdf)
zf.writestr(f"03_검측이력_{project_code}.pdf", insp_pdf)
zf.writestr(f"04_인허가현황_{project_code}.pdf", permit_pdf)
zf.writestr("README.txt", (
f"준공도서 패키지\n"
f"프로젝트: {project.name}\n"
f"생성일: {today}\n\n"
f"포함 문서:\n"
f" 01_준공요약 — 전체 공사 실적 요약\n"
f" 02_품질시험목록 — 전체 품질시험 기록 ({total_qt}건)\n"
f" 03_검측이력 — 검측 요청·완료 이력 ({total_insp}건)\n"
f" 04_인허가현황 — 인허가 취득 현황 ({total_permits}건)\n"
).encode("utf-8"))
zip_buffer.seek(0)
filename = f"completion_{project_code}_{today}.zip"
return zip_buffer.read(), filename