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)