OpenCode 실전 예시: Django·Next.js 코드와 프롬프트 세트

opencode django next.js project image
(OpenCode Project QHSE System)

앞선 글들에서 개념·설정·워크플로우를 충분히 살펴봤으니,
이번 편부터는 정말 “바로 붙여 넣고 돌려볼 수 있는 수준”으로 내려가 보겠습니다.

오늘 목표는 단순합니다.

  1. Django 백엔드
    • QHSE용 핵심 모델 2개
    • REST API 뼈대
  2. Next.js 프론트엔드
    • 매우 단순한 QHSE 대시보드 페이지
  3. 이 둘을 터미널에서 AI와 같이 만드는 프롬프트 세트까지 제공

그러니까 이 글은

“코드 + 프롬프트를 그대로 가져가서 내 프로젝트 이름만 바꿔서 돌려보는”
실습 스텝이라고 보시면 됩니다 😊

1) OpenCode-Django: QHSE 핵심 모델 + API 뼈대

1.1 예시 도메인 정리

협력사(SME) 입장에서 최소 단위 데이터를 두 개만 잡겠습니다.

  1. Vendor – 협력업체(자기 회사 혹은 하도급사)
  2. QhseRecord – 품질/안전/환경 관련 이벤트·조치 기록

실제로는 훨씬 더 쪼개야 하지만, MVP 학습용 예시로는 충분합니다.

1.2 Django 모델 예시 (vendors/models.py)

from django.db import models


class Vendor(models.Model):
    """
    원전 공급망에 참여하는 협력업체 정보
    """
    name = models.CharField(max_length=255)
    business_id = models.CharField(
        max_length=50,
        unique=True,
        help_text="사업자등록번호 또는 내부 코드"
    )
    iso_19443_certified = models.BooleanField(default=False)
    iso_9001_certified = models.BooleanField(default=True)
    country = models.CharField(max_length=100, default="Korea")
    contact_person = models.CharField(max_length=100, blank=True)
    contact_email = models.EmailField(blank=True)
    note = models.TextField(blank=True)

    def __str__(self):
        return f"{self.name} ({self.business_id})"


class QhseRecord(models.Model):
    """
    품질/보건/안전/환경 관련 이벤트·조치 기록
    """
    QHSE_TYPE_CHOICES = [
        ("Q", "Quality"),
        ("H", "Health & Safety"),
        ("E", "Environment"),
    ]

    vendor = models.ForeignKey(
        Vendor, on_delete=models.CASCADE,
        related_name="qhse_records"
    )
    record_type = models.CharField(max_length=1, choices=QHSE_TYPE_CHOICES)
    title = models.CharField(max_length=255)
    description = models.TextField()
    occurred_at = models.DateField()
    corrective_action = models.TextField(blank=True)
    is_closed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-occurred_at", "-created_at"]

    def __str__(self):
        return f"[{self.get_record_type_display()}] {self.title}"

1.3 DRF Serializer & View 예시 (vendors/api.py)

from rest_framework import serializers, viewsets
from .models import Vendor, QhseRecord


class VendorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Vendor
        fields = "__all__"


class QhseRecordSerializer(serializers.ModelSerializer):
    vendor_name = serializers.CharField(
        source="vendor.name", read_only=True
    )

    class Meta:
        model = QhseRecord
        fields = "__all__"


class VendorViewSet(viewsets.ModelViewSet):
    queryset = Vendor.objects.all()
    serializer_class = VendorSerializer


class QhseRecordViewSet(viewsets.ModelViewSet):
    queryset = QhseRecord.objects.select_related("vendor")
    serializer_class = QhseRecordSerializer

2.4 URL 라우팅 예시 (core/urls.py)

from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from vendors.api import VendorViewSet, QhseRecordViewSet

router = DefaultRouter()
router.register(r"vendors", VendorViewSet)
router.register(r"qhse-records", QhseRecordViewSet)

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include(router.urls)),
]

여기까지가 백엔드 최소 뼈대입니다.
이제 프론트에서 이 API를 써서 간단한 대시보드를 만들어 볼게요.

2) Next.js: QHSE 대시보드 페이지 뼈대

기준: app router, TypeScript 사용, 아주 심플한 UI

2.1 간단한 fetch 유틸 (lib/api.ts)

// lib/api.ts
export async function fetchVendors() {
  const res = await fetch("http://localhost:8000/api/vendors/");
  if (!res.ok) {
    throw new Error("Failed to fetch vendors");
  }
  return res.json();
}

export async function fetchQhseRecords() {
  const res = await fetch("http://localhost:8000/api/qhse-records/");
  if (!res.ok) {
    throw new Error("Failed to fetch QHSE records");
  }
  return res.json();
}

2.2 QHSE 대시보드 페이지 (app/qhse/page.tsx)

// app/qhse/page.tsx
import { fetchVendors, fetchQhseRecords } from "@/lib/api";

export default async function QhseDashboardPage() {
  const [vendors, records] = await Promise.all([
    fetchVendors(),
    fetchQhseRecords(),
  ]);

  return (
    <main className="px-6 py-8 space-y-8">
      <section>
        <h1 className="text-2xl font-bold">QHSE Dashboard</h1>
        <p className="text-sm text-gray-600 mt-2">
          원전 협력업체의 품질·안전·환경 이슈 현황을 한눈에 볼 수 있는 간단한 대시보드입니다.
        </p>
      </section>

      <section className="grid gap-4 md:grid-cols-3">
        <div className="rounded-xl border p-4">
          <h2 className="text-sm font-semibold mb-2">총 협력업체 수</h2>
          <p className="text-3xl font-bold">{vendors.length}</p>
        </div>
        <div className="rounded-xl border p-4">
          <h2 className="text-sm font-semibold mb-2">QHSE 기록 수</h2>
          <p className="text-3xl font-bold">{records.length}</p>
        </div>
        <div className="rounded-xl border p-4">
          <h2 className="text-sm font-semibold mb-2">미종결 이슈 수</h2>
          <p className="text-3xl font-bold">
            {records.filter((r: any) => !r.is_closed).length}
          </p>
        </div>
      </section>

      <section className="space-y-3">
        <h2 className="text-lg font-semibold">최근 QHSE 기록</h2>
        <div className="space-y-2">
          {records.slice(0, 5).map((r: any) => (
            <div
              key={r.id}
              className="flex flex-col md:flex-row md:items-center justify-between rounded-lg border p-3"
            >
              <div>
                <p className="text-sm font-semibold">
                  [{r.record_type}] {r.title}
                </p>
                <p className="text-xs text-gray-600 mt-1">
                  {r.vendor_name} · {r.occurred_at}
                </p>
              </div>
              <span
                className={`mt-2 md:mt-0 inline-flex items-center rounded-full px-3 py-1 text-xs font-medium ${
                  r.is_closed ? "bg-green-100 text-green-700" : "bg-amber-100 text-amber-700"
                }`}
              >
                {r.is_closed ? "조치 완료" : "진행 중"}
              </span>
            </div>
          ))}
        </div>
      </section>
    </main>
  );
}

이 정도면
“QHSE 시스템 대시보드의 아주 작은 프로토타입”을
빠르게 만들어 보는 데 충분한 뼈대가 됩니다.

3) 터미널에서 이걸 같이 만드는 프롬프트 세트

OpenCode Project QHSE System 개발 이미지

이제 진짜 핵심인 “어떻게 말하면서 같이 만들 것인가”를 정리해 보겠습니다.
아래 프롬프트들은 그대로 복붙해서 써도 되고,
당신 상황에 맞게 조금만 고쳐서 쓰면 됩니다.

3.1 설계 단계 프롬프트

/ask 중소기업을 위한 Nuclear QHSE & DocHub 시스템을 만들고 싶어.
모델은 Vendor와 QhseRecord 두 개로 시작하려고 하는데,
각 필드에 어떤 값이 들어가는지 예시와 함께 설명해줘.
Django 모델 관점에서 설계해줘.

3.2 Django 모델/시리얼라이저 생성 프롬프트

/ask 방금 설명한 내용을 기반으로
vendors/models.py 안에 Vendor와 QhseRecord 모델을 만들어줘.
ISO 19443 인증 여부를 나타내는 필드도 포함해줘.
그리고 DRF Serializer와 ViewSet 코드 초안도 같이 작성해줘.

3.3 URL 라우팅 생성 프롬프트

/ask DefaultRouter를 사용하는 Django REST Framework 라우팅 예시를
core/urls.py 기준으로 작성해줘.
api/vendors/, api/qhse-records/ 두 엔드포인트를 노출하고 싶어.

3.4 Next.js fetch 유틸 생성 프롬프트

/ask Django API에서 vendors와 qhse-records를 가져오는
Next.js용 fetch 유틸 함수를 만들어줘.
TypeScript로 작성하고, lib/api.ts에 들어갈 예시로 만들어줘.

3.5 QHSE 대시보드 페이지 생성 프롬프트

@frontend-ui-ux-engineer
간단한 QHSE Dashboard 페이지를 만들고 싶어.
app/qhse/page.tsx 파일을 기준으로,
총 협력업체 수, QHSE 기록 수, 미종결 이슈 수를 보여주는
심플한 카드 UI와 최근 5개 기록 리스트를 포함해줘.
Tailwind를 사용해서 깔끔하게 작성해줘.

3.6 리팩터링 & 개선 요청 프롬프트

/ask 지금 작성된 QHSE 대시보드 코드 전체를 리뷰해줘.
초급 개발자가 알아두면 좋은 리팩터링 포인트를 3개만 뽑아서 설명해주고,
그 중 1번 항목을 실제 코드로 반영한 버전을 제안해줘.

3.7 문서화 프롬프트

@document
Nuclear QHSE & DocHub의 첫 버전을 위한 README.md 초안을 작성해줘.
- 프로젝트 소개
- 사용 기술 스택
- Django/Next.js 실행 방법
- QHSE 대시보드 기능 설명
위 4가지를 포함해줘.

정리하기

이번 포스트은 말 그대로 “코드+프롬프트 패키지”였습니다.

  • Django로 Vendor / QhseRecord 모델과 REST API를 만들고
  • Next.js로 QHSE 대시보드 페이지 뼈대를 구성한 뒤
  • 터미널에서 AI와 함께 전체를 설계·리팩터링·문서화하는 대화 패턴까지 붙여보았죠.

이제 여기서부터는

  • 모델을 더 세분화하고 (교육, 인증서, 프로젝트, 감사 등)
  • 대시보드를 더 풍성하게 만들고
  • 리포트(PDF/Zip) 생성 기능까지 확장해 나가면

실제로 SME들이 “돈 내고 쓸 법한” 시스템으로도 충분히 성장시킬 수 있습니다.

원하시면 12편에서는 “QHSE 리포트 자동 생성”을 주제로 계속 이어가 볼게요.