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:
sinmb79
2026-03-24 21:39:05 +09:00
parent 2a4950d8a0
commit 0156d8ca4f
13 changed files with 990 additions and 3 deletions
+98
View File
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Noto Sans KR', sans-serif; font-size: 11pt; color: #111; padding: 20mm; }
h1 { text-align: center; font-size: 18pt; font-weight: 700; margin-bottom: 4mm; }
.subtitle { text-align: center; font-size: 11pt; margin-bottom: 8mm; color: #444; }
table { width: 100%; border-collapse: collapse; margin-bottom: 6mm; }
th, td { border: 1px solid #888; padding: 3mm 4mm; vertical-align: top; }
th { background: #f0f0f0; font-weight: 700; text-align: center; width: 28%; }
.section-title { font-size: 12pt; font-weight: 700; background: #e8e8e8; padding: 2mm 4mm; margin: 5mm 0 2mm; }
.workers-table th { width: auto; }
.footer { margin-top: 10mm; text-align: right; font-size: 10pt; color: #555; }
.badge { display: inline-block; padding: 1mm 3mm; border-radius: 3px; font-size: 9pt; font-weight: 700; }
.badge-draft { background: #fef3c7; color: #92400e; }
.badge-confirmed { background: #d1fae5; color: #065f46; }
</style>
</head>
<body>
<h1>작 업 일 보</h1>
<div class="subtitle">{{ project.name }}</div>
<table>
<tr><th>공사명</th><td colspan="3">{{ project.name }}</td></tr>
<tr>
<th>일자</th><td>{{ report.report_date }}</td>
<th>날씨</th><td>{{ report.weather_summary or '-' }}</td>
</tr>
<tr>
<th>기온 (최고/최저)</th>
<td>{{ report.temperature_high }}°C / {{ report.temperature_low }}°C</td>
<th>상태</th>
<td>
{% if report.status.value == 'confirmed' %}
<span class="badge badge-confirmed">확인완료</span>
{% else %}
<span class="badge badge-draft">초안</span>
{% endif %}
</td>
</tr>
</table>
<div class="section-title">▶ 투입 인원</div>
{% if report.workers_count %}
<table class="workers-table">
<tr>
{% for key in report.workers_count %}<th>{{ key }}</th>{% endfor %}
<th>합계</th>
</tr>
<tr>
{% set total = namespace(n=0) %}
{% for key, val in report.workers_count.items() %}
<td style="text-align:center">{{ val }}명</td>
{% set total.n = total.n + val %}
{% endfor %}
<td style="text-align:center;font-weight:700">{{ total.n }}명</td>
</tr>
</table>
{% else %}
<table><tr><td style="color:#888">투입 인원 정보 없음</td></tr></table>
{% endif %}
{% if report.equipment_list %}
<div class="section-title">▶ 투입 장비</div>
<table>
<tr><th>장비명</th><th>규격</th><th>수량</th><th>비고</th></tr>
{% for eq in report.equipment_list %}
<tr>
<td>{{ eq.get('type', '-') }}</td>
<td>{{ eq.get('spec', '-') }}</td>
<td style="text-align:center">{{ eq.get('count', 1) }}대</td>
<td>{{ eq.get('notes', '') }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
<div class="section-title">▶ 작업 내용</div>
<table>
<tr><td style="min-height:30mm; white-space:pre-wrap">{{ report.work_content or '-' }}</td></tr>
</table>
{% if report.issues %}
<div class="section-title">▶ 특이사항 / 문제점</div>
<table>
<tr><td style="white-space:pre-wrap">{{ report.issues }}</td></tr>
</table>
{% endif %}
<div class="footer">
작성일시: {{ now }}&nbsp;&nbsp;&nbsp;
{% if report.ai_generated %}AI 보조 작성{% endif %}
</div>
</body>
</html>
+91
View File
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Noto Sans KR', sans-serif; font-size: 11pt; color: #111; padding: 20mm; }
h1 { text-align: center; font-size: 18pt; font-weight: 700; margin-bottom: 8mm; }
table { width: 100%; border-collapse: collapse; margin-bottom: 6mm; }
th, td { border: 1px solid #888; padding: 3mm 4mm; vertical-align: middle; }
th { background: #f0f0f0; font-weight: 700; text-align: center; width: 28%; }
.section-title { font-size: 12pt; font-weight: 700; background: #e8e8e8; padding: 2mm 4mm; margin: 5mm 0 2mm; }
.checklist-item { display: flex; align-items: flex-start; padding: 2mm 0; border-bottom: 1px solid #ddd; }
.check-box { width: 6mm; height: 6mm; border: 1px solid #666; margin-right: 3mm; flex-shrink: 0; margin-top: 1mm; }
.check-num { color: #888; margin-right: 2mm; min-width: 8mm; }
.badge { display: inline-block; padding: 1mm 3mm; border-radius: 3px; font-size: 9pt; font-weight: 700; }
.badge-pass { background: #d1fae5; color: #065f46; }
.badge-fail { background: #fee2e2; color: #991b1b; }
.badge-conditional { background: #fef3c7; color: #92400e; }
.sign-area { display: flex; justify-content: flex-end; gap: 10mm; margin-top: 10mm; }
.sign-box { border: 1px solid #888; width: 35mm; text-align: center; }
.sign-box .title { background: #f0f0f0; padding: 2mm; border-bottom: 1px solid #888; font-size: 9pt; }
.sign-box .space { height: 15mm; }
.footer { margin-top: 8mm; text-align: right; font-size: 10pt; color: #555; }
</style>
</head>
<body>
<h1>검 측 요 청 서</h1>
<table>
<tr><th>공사명</th><td colspan="3">{{ project.name }}</td></tr>
<tr>
<th>검측 항목</th>
<td>{{ inspection_type_label }}</td>
<th>요청일</th>
<td>{{ inspection.requested_date }}</td>
</tr>
<tr>
<th>위치 / 부위</th>
<td>{{ inspection.location_detail or '-' }}</td>
<th>결과</th>
<td>
{% if inspection.result %}
{% if inspection.result.value == 'pass' %}<span class="badge badge-pass">합격</span>
{% elif inspection.result.value == 'fail' %}<span class="badge badge-fail">불합격</span>
{% else %}<span class="badge badge-conditional">조건부합격</span>{% endif %}
{% else %}-{% endif %}
</td>
</tr>
{% if inspection.inspector_name %}
<tr><th>검측자</th><td colspan="3">{{ inspection.inspector_name }}</td></tr>
{% endif %}
</table>
{% if inspection.checklist_items %}
<div class="section-title">▶ 검측 체크리스트</div>
<table>
<tr><th style="width:8%">No.</th><th style="width:50%">검측 항목</th><th style="width:22%">기준값</th><th style="width:20%">확인</th></tr>
{% for item in inspection.checklist_items %}
<tr>
<td style="text-align:center">{{ loop.index }}</td>
<td>{{ item.get('item', item) if item is mapping else item }}</td>
<td>{{ item.get('standard', '') if item is mapping else '' }}</td>
<td style="text-align:center"></td>
</tr>
{% endfor %}
</table>
{% endif %}
{% if inspection.notes %}
<div class="section-title">▶ 특이사항</div>
<table><tr><td style="white-space:pre-wrap">{{ inspection.notes }}</td></tr></table>
{% endif %}
<div class="sign-area">
<div class="sign-box">
<div class="title">현장대리인</div>
<div class="space"></div>
<div style="padding:2mm;font-size:9pt">(인)</div>
</div>
<div class="sign-box">
<div class="title">감독관</div>
<div class="space"></div>
<div style="padding:2mm;font-size:9pt">(인)</div>
</div>
</div>
<div class="footer">출력일시: {{ now }}{% if inspection.ai_generated %}&nbsp;&nbsp;AI 보조 작성{% endif %}</div>
</body>
</html>
+58
View File
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Noto Sans KR', sans-serif; font-size: 11pt; color: #111; padding: 20mm; }
h1 { text-align: center; font-size: 18pt; font-weight: 700; margin-bottom: 4mm; }
.subtitle { text-align: center; font-size: 11pt; color: #444; margin-bottom: 8mm; }
table { width: 100%; border-collapse: collapse; margin-bottom: 6mm; }
th, td { border: 1px solid #888; padding: 3mm 4mm; vertical-align: top; }
th { background: #f0f0f0; font-weight: 700; text-align: center; width: 25%; }
.section-title { font-size: 12pt; font-weight: 700; background: #e8e8e8; padding: 2mm 4mm; margin: 5mm 0 2mm; }
.content-block { border: 1px solid #ccc; padding: 4mm; min-height: 20mm; white-space: pre-wrap; line-height: 1.8; }
.footer { margin-top: 10mm; text-align: right; font-size: 10pt; color: #555; }
</style>
</head>
<body>
<h1>{{ report_type_label }} 공정보고서</h1>
<div class="subtitle">{{ project.name }} &nbsp;|&nbsp; {{ period_label }}</div>
<table>
<tr><th>공사명</th><td colspan="3">{{ project.name }}</td></tr>
<tr>
<th>보고 기간</th><td>{{ period_label }}</td>
<th>상태</th><td>{{ status_label }}</td>
</tr>
</table>
{% if content_json %}
{% if content_json.get('work_summary') %}
<div class="section-title">▶ 주요 작업 내용</div>
<div class="content-block">{{ content_json.work_summary }}</div>
{% endif %}
{% if content_json.get('overall_progress') is not none %}
<div class="section-title">▶ 공정률</div>
<table><tr><th>종합 공정률</th><td>{{ content_json.overall_progress }}%</td></tr></table>
{% endif %}
{% if content_json.get('issues') %}
<div class="section-title">▶ 문제점 및 조치사항</div>
<div class="content-block">{{ content_json.issues }}</div>
{% endif %}
{% if content_json.get('next_plan') %}
<div class="section-title">▶ 다음 기간 예정 작업</div>
<div class="content-block">{{ content_json.next_plan }}</div>
{% endif %}
{% elif ai_draft_text %}
<div class="section-title">▶ AI 작성 보고서 초안</div>
<div class="content-block">{{ ai_draft_text }}</div>
{% endif %}
<div class="footer">출력일시: {{ now }}</div>
</body>
</html>