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>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
설계도서 파싱 서비스 (Phase 3)
|
||||
PDF 설계도서에서 공종·수량·규격을 AI로 자동 추출합니다.
|
||||
HWP 출력은 Pandoc을 통해 변환합니다.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from anthropic import AsyncAnthropic
|
||||
from app.config import settings
|
||||
|
||||
_client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
|
||||
|
||||
_PARSE_SYSTEM = """당신은 건설 설계도서 분석 전문가입니다.
|
||||
제공된 설계도서 내용에서 다음 정보를 추출하세요:
|
||||
1. 공종 목록 (work_types): 주요 공종과 세부 공종
|
||||
2. 수량 목록 (quantities): 각 공종별 수량과 단위
|
||||
3. 규격 (specifications): 재료 규격, 강도, 등급
|
||||
4. 특기 사항 (notes): 시공 시 주의사항, 특수 조건
|
||||
|
||||
JSON 형식으로만 반환하세요."""
|
||||
|
||||
|
||||
async def parse_design_document_text(text: str) -> dict:
|
||||
"""
|
||||
설계도서 텍스트에서 공종/수량/규격 추출 (Claude API).
|
||||
RAG 시드 스크립트로 읽은 텍스트를 직접 전달하는 용도.
|
||||
"""
|
||||
prompt = f"""다음 설계도서 내용을 분석해주세요:
|
||||
|
||||
{text[:8000]}
|
||||
|
||||
JSON 형식으로만 반환하세요:
|
||||
{{
|
||||
"work_types": ["터파기", "철근콘크리트", ...],
|
||||
"quantities": [
|
||||
{{"work_type": "터파기", "quantity": 500, "unit": "m³"}},
|
||||
...
|
||||
],
|
||||
"specifications": [
|
||||
{{"item": "콘크리트", "spec": "fck=24MPa", "notes": ""}},
|
||||
...
|
||||
],
|
||||
"notes": ["주의사항1", ...]
|
||||
}}"""
|
||||
|
||||
response = await _client.messages.create(
|
||||
model=settings.CLAUDE_MODEL,
|
||||
max_tokens=2048,
|
||||
system=_PARSE_SYSTEM,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
|
||||
raw = response.content[0].text.strip()
|
||||
match = re.search(r"\{.*\}", raw, re.DOTALL)
|
||||
if match:
|
||||
return json.loads(match.group())
|
||||
return {"error": "파싱 실패", "raw": raw[:500]}
|
||||
|
||||
|
||||
async def parse_design_document_image(image_data: bytes, media_type: str = "image/jpeg") -> dict:
|
||||
"""
|
||||
설계 도면 이미지에서 공종/수량/규격 추출 (Claude Vision).
|
||||
도면 스캔 이미지나 PDF 페이지를 직접 분석합니다.
|
||||
"""
|
||||
image_b64 = base64.standard_b64encode(image_data).decode()
|
||||
|
||||
response = await _client.messages.create(
|
||||
model=settings.CLAUDE_MODEL,
|
||||
max_tokens=2048,
|
||||
system=_PARSE_SYSTEM,
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "image", "source": {"type": "base64", "media_type": media_type, "data": image_b64}},
|
||||
{"type": "text", "text": "이 설계 도면/도서에서 공종, 수량, 규격을 추출해주세요. JSON으로만 반환하세요."},
|
||||
],
|
||||
}],
|
||||
)
|
||||
|
||||
raw = response.content[0].text.strip()
|
||||
match = re.search(r"\{.*\}", raw, re.DOTALL)
|
||||
if match:
|
||||
return json.loads(match.group())
|
||||
return {"error": "파싱 실패", "raw": raw[:500]}
|
||||
|
||||
|
||||
def convert_to_hwp(html_content: str, output_path: str | None = None) -> bytes | str:
|
||||
"""
|
||||
HTML → HWP 변환 (Pandoc 필요).
|
||||
output_path 미지정 시 바이트 반환.
|
||||
|
||||
사전 요구사항: pandoc 설치 필요
|
||||
설치: https://pandoc.org/installing.html
|
||||
"""
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
html_path = f.name
|
||||
|
||||
if output_path:
|
||||
out = output_path
|
||||
else:
|
||||
out = html_path.replace(".html", ".hwp")
|
||||
|
||||
result = subprocess.run(
|
||||
["pandoc", html_path, "-o", out, "--from=html"],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
|
||||
os.unlink(html_path)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Pandoc 변환 실패: {result.stderr}")
|
||||
|
||||
if output_path:
|
||||
return output_path
|
||||
else:
|
||||
with open(out, "rb") as f:
|
||||
data = f.read()
|
||||
os.unlink(out)
|
||||
return data
|
||||
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(
|
||||
"Pandoc이 설치되지 않았습니다.\n"
|
||||
"설치: https://pandoc.org/installing.html\n"
|
||||
"Windows: winget install JohnMacFarlane.Pandoc"
|
||||
)
|
||||
Reference in New Issue
Block a user