06 · Developer Guide · 레시피

신청 폼 (DB 연동 전 임시 동작)Signup Form

라디오 카드 + 입력 필드 + 긴 글 영역 + 동의 + 보내기 버튼으로 구성된 폼입니다.
시안 단계에서는 보내기 동작이 임시로 시뮬레이션되고, 실제 연동 시 보내기 핸들러 한 줄만 바꾸면 됩니다.

언제 쓰는가

  • 새가족 등록·상담 신청·중보기도 신청·문의하기 등 사용자 입력 폼
  • Supabase 또는 외부 API 연동 전, 시안 단계에서 동작 시뮬레이션
  • 라디오 카드로 카테고리 선택이 필요한 경우

① 임시 동작 골격 — 실 코드의 기본 구조

components/preview/CounselForm.tsxtsx
"use client";

import { useState, type FormEvent } from "react";
import { Check } from "lucide-react";

const TYPES = [
  { value: "personal", label: "개인 상담", desc: "삶의 어려움·신앙 점검" },
  { value: "youth", label: "아동·청소년 상담", desc: "학교·또래·가정 이슈" },
  // ...
];

export function CounselForm() {
  const [submitted, setSubmitted] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [type, setType] = useState<string>("personal");

  const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setSubmitting(true);

    // ─── 실 연동 시 이 부분만 교체 ───
    // const fd = new FormData(e.currentTarget);
    // await fetch("/api/counsel", { method: "POST", body: fd });
    await new Promise((r) => setTimeout(r, 600));
    // ───────────────────────────────

    setSubmitting(false);
    setSubmitted(true);
  };

  if (submitted) {
    return (
      <div className="rounded-2xl border bg-white p-10 text-center">
        <Check size={24} />
        <p className="mt-6 text-[20px] font-bold">신청이 접수되었습니다.</p>
        <button onClick={() => setSubmitted(false)}>새 신청 작성</button>
      </div>
    );
  }

  return (
    <form onSubmit={onSubmit} className="space-y-6 rounded-2xl border bg-white p-7">
      {/* 라디오 카드 그룹 */}
      <fieldset>
        <legend>상담 유형 *</legend>
        <ul className="mt-4 grid gap-3 sm:grid-cols-2">
          {TYPES.map((t) => (
            <li key={t.value}>
              <label className={type === t.value ? "border-accent" : "border-border"}>
                <input
                  type="radio"
                  name="type"
                  value={t.value}
                  checked={type === t.value}
                  onChange={() => setType(t.value)}
                  className="sr-only"
                />
                {t.label}
                <span>{t.desc}</span>
              </label>
            </li>
          ))}
        </ul>
      </fieldset>

      {/* 입력 필드 + textarea + 동의 + submit */}
    </form>
  );
}

② 실 연동 — Supabase로 교체

임시 동작 자리의 await new Promise(...) 한 줄만 실제 서버 호출로 바꾸면 끝납니다. 토큰 검증·요청 횟수 제한(rate limit)·자동 입력 차단(CAPTCHA)은 별도 서버 함수(예: Supabase Edge Function)에서 처리합니다.

tsx
// 실 연동 — Supabase 예시
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  setSubmitting(true);

  const fd = new FormData(e.currentTarget);
  const { error } = await supabase.from("counsel_requests").insert({
    type: fd.get("type"),
    name: fd.get("name"),
    phone: fd.get("phone"),
    email: fd.get("email") || null,
    memo: fd.get("memo"),
    submitted_at: new Date().toISOString(),
  });

  setSubmitting(false);
  if (error) {
    // 토스트로 오류 안내
    return;
  }
  setSubmitted(true);
};

실 사이트의 폼 3종

  • NewFamilyForm/site/community/new-family
  • CounselForm/site/community/counsel (라디오 카드 + 동의)
  • 중보기도 신청 /site/yard/prayer-request (익명 옵션 + 분류 select)
Need Help

도움이 필요하신가요?

주님의교회 PCL 디자인 시스템을 적용하시다가 막히는 부분이 있다면, 다음 경로로 직접 문의하실 수 있습니다.