nnuer
강희태@gmlxo9917

사유를 골라 리뷰를 신고하는 모달 대화상자

기타 블록

리뷰 카드의 신고 버튼을 누르면 네이티브 dialog 모달이 열려 사유 4가지 중 하나를 고르고 설명을 적어 신고합니다. 사유가 없거나 기타인데 설명이 없으면 안내 문구가 나옵니다.

블록 신고

운영자가 확인한 뒤 필요하면 블록을 숨깁니다.

사유
Claude Code · claude-opus-5-5 · React (TSX)
결과 코드펼치기
import { useRef, useState } from "react";

const REASONS = [
  { id: "ad", label: "광고·홍보", hint: "가게나 상품을 홍보하는 글" },
  { id: "abuse", label: "욕설·비방", hint: "사람을 깎아내리거나 모욕하는 표현" },
  { id: "fake", label: "허위 정보", hint: "방문하지 않고 쓴 것 같은 리뷰" },
  { id: "other", label: "기타", hint: "위에 없는 이유" },
];

export default function ReviewReportDialog() {
  const dialog = useRef<HTMLDialogElement>(null);
  const [reason, setReason] = useState<string | null>(null);
  const [detail, setDetail] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [sent, setSent] = useState(false);

  function submit() {
    if (!reason) return setError("신고 사유를 선택해 주세요.");
    if (reason === "other" && !detail.trim()) return setError("기타 사유는 설명을 적어 주세요.");
    setSent(true);
    dialog.current?.close();
  }

  return (
    <section className="w-full bg-stone-100 px-4 py-14 text-stone-900 sm:px-8">
      <article className="mx-auto max-w-xl rounded-2xl border border-stone-200 bg-white p-5 shadow-sm">
        <div className="flex items-center gap-3">
          <span className="flex h-9 w-9 items-center justify-center rounded-full bg-orange-100 text-sm font-semibold text-orange-700" aria-hidden>
            맛
          </span>
          <div className="min-w-0 flex-1">
            <p className="text-sm font-semibold">맛집탐방러</p>
            <p className="text-xs text-stone-500">을지로 국숫집 · 3일 전</p>
          </div>
          <button
            type="button"
            disabled={sent}
            onClick={() => {
              setError(null);
              dialog.current?.showModal();
            }}
            className="inline-flex h-8 items-center gap-1 rounded-lg px-2.5 text-[13px] text-stone-500 transition hover:bg-stone-100 hover:text-stone-900 disabled:pointer-events-none disabled:opacity-60"
          >
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
              <path d="M3.5 14V2.5M3.5 3h8l-1.5 3 1.5 3h-8" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
            {sent ? "신고 접수됨" : "신고"}
          </button>
        </div>
        <p className="mt-4 text-[15px] leading-relaxed">
          ★★★★★ 여기 무조건 가세요!! 할인 쿠폰은 아래 링크에서 받으시면 됩니다 → bit.ly/xxxx
        </p>
      </article>

      <dialog
        ref={dialog}
        aria-labelledby="report-title"
        className="m-auto w-[min(28rem,calc(100%-2rem))] rounded-2xl border border-stone-200 bg-white p-0 text-stone-900 shadow-2xl backdrop:bg-black/50 backdrop:backdrop-blur-[2px]"
      >
        <form
          method="dialog"
          className="space-y-4 p-6"
          onSubmit={(e) => {
            e.preventDefault();
            submit();
          }}
        >
          <div>
            <h2 id="report-title" className="text-lg font-semibold">
              리뷰 신고
            </h2>
            <p className="mt-1 text-sm text-stone-500">운영팀이 확인한 뒤 규칙에 맞지 않으면 숨깁니다.</p>
          </div>
          <fieldset className="space-y-2">
            <legend className="sr-only">신고 사유</legend>
            {REASONS.map((r) => (
              <label
                key={r.id}
                className={`flex cursor-pointer items-start gap-3 rounded-xl border px-3.5 py-3 transition-colors ${
                  reason === r.id ? "border-orange-400 bg-orange-50" : "border-stone-200 hover:border-stone-300"
                }`}
              >
                <input
                  type="radio"
                  name="reason"
                  value={r.id}
                  checked={reason === r.id}
                  onChange={() => {
                    setReason(r.id);
                    setError(null);
                  }}
                  className="mt-0.5 h-4 w-4 accent-orange-500"
                />
                <span>
                  <span className="block text-sm font-medium">{r.label}</span>
                  <span className="block text-xs text-stone-500">{r.hint}</span>
                </span>
              </label>
            ))}
          </fieldset>
          <div>
            <label htmlFor="report-detail" className="mb-1.5 block text-[13px] font-medium text-stone-600">
              설명 {reason === "other" ? "(필수)" : "(선택)"}
            </label>
            <textarea
              id="report-detail"
              rows={3}
              maxLength={500}
              value={detail}
              onChange={(e) => setDetail(e.target.value)}
              placeholder="어떤 점이 문제인지 알려 주세요"
              className="w-full resize-none rounded-xl border border-stone-200 px-3 py-2 text-sm outline-none transition placeholder:text-stone-400 focus:border-orange-400 focus:ring-4 focus:ring-orange-100"
            />
          </div>
          {error && (
            <p role="alert" className="text-sm text-red-600">
              {error}
            </p>
          )}
          <div className="flex justify-end gap-2">
            <button type="button" onClick={() => dialog.current?.close()} className="h-10 rounded-xl px-4 text-sm font-medium text-stone-600 hover:bg-stone-100">
              취소
            </button>
            <button type="submit" className="h-10 rounded-xl bg-stone-900 px-4 text-sm font-medium text-white transition hover:bg-stone-700 active:scale-[0.98]">
              신고하기
            </button>
          </div>
        </form>
      </dialog>
    </section>
  );
}

적용 사이트 0

이 블록이 실제로 쓰인 사이트입니다.

로그인하고 내 사이트 추가

아직 등록된 사이트가 없습니다. 이 블록을 쓴 사이트가 있다면 알려 주세요.

로그인하고 재현 결과 올리기

재현 후기

v1 · 0개

아직 이 버전의 프롬프트로 직접 만들어 본 사람이 없습니다. 첫 번째로 재현해 보세요.