QHSE report 자동 생성: Django와 OpenCode로 시작하기

QHSE report 자동 생성 프로세스 이미지

앞 편から Vendor + QhseRecord 모델과 간단한 대시보드까지 만들었으니,
이제 남은 건 “QHSE report 생성하기”입니다.

현장에서 진짜로 요구되는 건 화면보다 ファイル이죠.

  • 감사·심사 때 내야 하는 월간/분기별 리포트
  • 원전 사업자에게 제출하는 PDF 묶음
  • 내부 회의용 요약 보고서

이번 글에서는 다음 흐름을 한 번에 잡아봅니다.

  1. ジャンゴから PDF 리포트 뼈대 뽑기
  2. 간단한 테스트 코드로 리포트 생성 여부 검증
  3. 터미널에서 OpenCodeを利用して
    • 요약 문구 자동 생성
    • 템플릿 리팩터링
    • 테스트 보완

까지 정리해 볼게요.

1. QHSE report 구조를 먼저 정리하기

일단 리포트를 코드로 찍어내려면, “틀”이 필요합니다.
예제로 월간 리포트를 이렇게 잡아볼 수 있어요.

  • 표지
  • 1장: 개요 (기간, 대상 Vendor 수, 기록 수, 미종결 수)
  • 2장: Q/H/E 유형별 통계 (막대그래프 이미지 or 숫자 테이블)
  • 3장: 주요 이슈 Top N + 조치 현황
  • 부록: 상세 리스트(필요 시)

실제 프로젝트로 가면 이 구조를 고객사 포맷에 맞게 커스터마이징하면 됩니다.

2. Django에서 PDF 생성 뼈대 만들기

여기서는 설명을 위해 WeasyPrint 같은 HTML→PDF 방식이라고 가정하겠습니다
(실제 프로젝트에서는 ReportLab, xhtml2pdf 등 어떤 걸 써도 됩니다).

2.1 의존성 설치 (예시)

pip install weasyprint

2.2 리포트용 뷰 작성 (reports/views.py)

import datetime
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML

from vendors.models import Vendor, QhseRecord


def monthly_qhse_report(request, year: int, month: int):
    # 1. 기간 설정
    start_date = datetime.date(year, month, 1)
    if month == 12:
        end_date = datetime.date(year + 1, 1, 1)
    else:
        end_date = datetime.date(year, month + 1, 1)

    # 2. 데이터 조회
    records = QhseRecord.objects.select_related("vendor").filter(
        occurred_at__gte=start_date,
        occurred_at__lt=end_date,
    )

    vendors = Vendor.objects.all().distinct()

    total_records = records.count()
    open_records = records.filter(is_closed=False).count()

    # 유형별 집계
    by_type = {
        key: records.filter(record_type=key).count()
        for key, _label in QhseRecord.QHSE_TYPE_CHOICES
    }

    context = {
        "year": year,
        "month": month,
        "vendors": vendors,
        "total_records": total_records,
        "open_records": open_records,
        "by_type": by_type,
        "records": records,
    }

    # 3. HTML 렌더링
    html_string = render_to_string("reports/monthly_qhse_report.html", context)

    # 4. PDF 생성
    html = HTML(string=html_string)
    pdf_file = html.write_pdf()

    # 5. 응답 반환
    filename = f"qhse_report_{year}_{month:02d}.pdf"
    response = HttpResponse(pdf_file, content_type="application/pdf")
    response["Content-Disposition"] = f'attachment; filename="{filename}"'
    return response

2.3 URL 연결 (core/urls.py 일부)

from reports.views import monthly_qhse_report

urlpatterns += [
    path(
        "reports/monthly/<int:year>/<int:month>/",
        monthly_qhse_report,
        name="monthly_qhse_report",
    ),
]

こうすれば /reports/monthly/2025/11/ 같은 URL로 접근했을 때
월간 리포트 PDF를 바로 내려받을 수 있는 구조가 됩니다.

3. 리포트 HTML 템플릿 예시

templates/reports/monthly_qhse_report.html의 아주 단순한 예시는 이렇게 잡을 수 있어요:

<!doctype html>
<html lang="ko">
  <head>
    <meta charset="utf-8" />
    <title>Monthly QHSE Report</title>
    <style>
      body { font-family: sans-serif; font-size: 12px; }
      h1, h2 { margin-bottom: 4px; }
      table { width: 100%; border-collapse: collapse; margin-top: 12px; }
      th, td { border: 1px solid #ccc; padding: 4px 6px; }
      th { background: #f3f3f3; }
    </style>
  </head>
  <body>
    <h1>{{ year }}년 {{ month }}월 QHSE 리포트</h1>
    <p>총 협력업체 수: {{ vendors|length }}</p>
    <p>전체 기록 수: {{ total_records }} / 미종결: {{ open_records }}</p>

    <h2>유형별 통계</h2>
    <ul>
      <li>품질(Q): {{ by_type.Q }}</li>
      <li>안전/보건(H): {{ by_type.H }}</li>
      <li>환경(E): {{ by_type.E }}</li>
    </ul>

    <h2>상세 기록 (상위 일부)</h2>
    <table>
      <thead>
        <tr>
          <th>발생일</th>
          <th>협력업체</th>
          <th>タイプ</th>
          <th>タイトル</th>
          <th>종결 여부</th>
        </tr>
      </thead>
      <tbody>
        {% for r in records|slice:":20" %}
          <tr>
            <td>{{ r.occurred_at }}</td>
            <td>{{ r.vendor.name }}</td>
            <td>{{ r.get_record_type_display }}</td>
            <td>{{ r.title }}</td>
            <td>{% if r.is_closed %}완료{% else %}진행중{% endif %}</td>
          </tr>
        {% endfor %}
      </tbody>
    </table>
  </body>
</html>

이제 이 템플릿 자체를 AI에게 맡겨서 개선시키면 됩니다.

4. 테스트 코드로 “PDF가 잘 떨어지는지” 확인하기

리포트 자동화에서 중요한 건 “코드 수정 후에도 PDF가 계속 잘 생성되느냐”입니다.
간단한 Django TestCase 예시는 다음과 같습니다.

# reports/tests/test_monthly_report.py
import datetime
from django.test import TestCase
from django.urls import reverse
from vendors.models import Vendor, QhseRecord


class MonthlyQhseReportTests(TestCase):
    def setUp(self):
        self.vendor = Vendor.objects.create(
            name="테스트 협력사",
            business_id="123-45-67890",
        )
        QhseRecord.objects.create(
            vendor=self.vendor,
            record_type="Q",
            title="용접 비드 불량",
            description="시운전 중 비드 불량 발견",
            occurred_at=datetime.date(2025, 11, 15),
            is_closed=False,
        )

    def test_report_pdf_is_generated(self):
        url = reverse("monthly_qhse_report", kwargs={"year": 2025, "month": 11})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response["Content-Type"], "application/pdf")
        self.assertGreater(len(response.content), 1000)

이 테스트 하나만 있어도

  • URL이 정상 동작하는지
  • PDF로 응답하는지
  • 내용이 “빈 파일 수준”은 아닌지
    를 최소한 검증해 줍니다.

5. OpenCode에게 “요약 글 + 개선안” 맡기기

이제 진짜 재미있는 부분입니다.
터미널에서 프로젝트 루트에서 OpenCode를 켜고, 이렇게 말해볼 수 있어요.

5.1 리포트 요약 텍스트 자동 생성

/ask reports/views.py와 monthly_qhse_report.html을 기반으로,
리포트 맨 앞에 들어갈 한글 요약 문단을 5줄 이내로 작성해줘.
협력사 관리자 관점에서 읽기 쉽게 써줘.

→ 이걸 복사해서 템플릿 상단에 넣으면,
리포트가 “숫자 나열”이 아니라 “사람이 읽을 수 있는 보고서”가 됩니다.

5.2 테스트 코드 보완 요청

/ask test_monthly_report.py를 검토해서
- 엣지 케이스
- 데이터가 없을 때의 동작
- 잘못된 월/연도 요청
을 검증하는 테스트 함수를 2~3개 더 추가해줘.

AI가 테스트 보완안을 제안해주면,
테스트 신뢰성이 빠르게 올라갑니다.

5.3 리팩터링 + 스타일 개선

/ask monthly_qhse_report 뷰의 복잡도를 줄이기 위해
쿼리 로직과 통계 계산을 별도 service 함수로 분리하는 리팩터링 코드를 제안해줘.

이런 식으로

  • 뷰는 “입출력 중심”
  • 서비스 함수는 “비즈니스 로직”
    으로 분리하는 패턴을 AI와 함께 연습することができます。

6. QHSE dashboard 와 연결해서 “클릭 한 번으로 리포트 받기”

QHSE report 생성 이미지

앞 글에서 만든 대시보드에, 다음 정도 기능만 붙여도 현장 느낌이 확 살아납니다.

  • 대시보드 상단에 “이번 달 리포트 다운로드” 버튼
  • /reports/monthly/2025/11/ 같은 URL로 바로 연결

Next.js 쪽에서는

// 예: app/qhse/page.tsx 내 일부
import Link from "next/link";

<button className="rounded-lg border px-4 py-2 text-sm">
  <Link href="/api/proxy/reports/monthly/2025/11">
    이번 달 리포트 PDF 다운로드
  </Link>
</button>

이 버튼을 클릭하면
관리자 입장에서는 “대시보드에서 바로 공식 리포트로 연결되는 경험”을 줄 수 있죠.

나중에는 기간 선택(DatePicker) + 파라미터 기반으로 확장하면 됩니다.

よくある質問

Q1. QHSE report PDF 생성은 반드시 WeasyPrint를 써야 하나요?

아니요. レポートラボ, xhtml2pdf, wkhtmltopdf 등 어떤 PDF 라이브러리를 써도 됩니다.
중요한 건 “템플릿 + 데이터 + 테스트 코드” 구조를 유지하는 것이에요.

Q2. SME 환경에서는 보안 때문에 클라우드 AI를 못 쓰면 어떡하죠?

그 경우 로컬 LLM + 사내망 전용 OpenCode 환경을 고려할 수 있습니다.
프롬프트 구조와 아키텍처는 그대로 두고, 모델만 내부로 교체하는 식입니다.

Q3. QHSE dashboard 와 PDF 리포트의 지표가 서로 다르면 문제가 되지 않나요?

그래서 가능한 한 같은 쿼리/서비스 함수를 공유해야 합니다.
예를 들어 services/qhse_stats.py 같은 모듈을 만들고,
대시보드와 리포트 모두 그 모듈만 호출하도록 설계하는 게 좋습니다.

Q4. 테스트 코드에서 “len(response.content) > 1000” 같은 검사는 너무 러프한가요?

맞아요, 최소 레벨 검증일 뿐입니다.
나중에는 PDF 내부 텍스트 일부를 추출해 검사하거나,
생성 일자를 비교하는 등 더 정교한 테스트를 추가하는 게 좋습니다.

Q5. 리포트에 그래프나 차트 이미지를 넣으려면 어떻게 해야 하나요?

일반적으로는

  1. Matplotlib 등으로 이미지를 먼저 PNG로 생성하고
  2. 템플릿에 <img src="...">로 삽입
  3. PDF 렌더링 시 파일을 함께 읽어들이는 방식으로 구현합니다.

Q6. OpenCode를 CI/CD 파이프라인에도 연동할 수 있나요?

가능은 하지만, 처음에는 개발자 로컬 터미널에서만 사용하는 것을 추천합니다.
CI에 붙이려면 프롬프트·보안·비용 관리까지 고려해야 해서,
일단은 “코딩 어시스턴트 + 설계/문서화 도우미” 역할에 집중하는 게 안전합니다.

整理する

이번 편에서는 테스트 + PDF 생성이라는
실제 현장에서 바로 쓰이는 기능을 예제로,

  • Django에서 월간 리포트 PDF를 찍어내는 뷰와 템플릿
  • 최소한의 검증을 위한 테스트 코드
  • 터미널에서의 AI 보조 흐름(OpenCode 활용)

까지 한 번에 정리해 봤습니다.

오늘 바로 해볼 수 있는 액션 3가지를 정리하면

  1. 현재 프로젝트에 reports 앱 하나 만들기
    • 가장 단순한 HTML 템플릿 + PDF 출력 뷰부터 시도
  2. 테스트 코드 1개라도 추가해보기
    • “PDF가 200 OK로 리턴되고, 용량이 일정 이상인지”만 검사해도 큰 차이
  3. 터미널에서 AI에게 리포트 요약 문장과 리팩터링 제안 맡기기
    • 직접 쓰면 30분 걸릴 글을 3분 안에 얻어보고, 필요 부분만 손보는 경험 해보기

마지막으로, 스스로에게 한 번 물어보면 좋습니다.

“내가 지금 관리 중인 품질·안전·환경 데이터를
이런 식의 자동 리포트로 바꿔두면,
다음 감사·심사 때 얼마나 덜 힘들까?”

다음 편에서는
“리포트 자동 생성에 스케줄러(예: Celery + cron)와 메일 발송까지 붙이는 흐름”을
이어서 정리해 볼게요.

類似の投稿