nnuer
강희태@gmlxo9917

클래스 게시판 목록 (상태 분류 필터 + 검색)

블로그 블록

모집 중/진행 중/마감 분류 필터와 제목 검색이 있는 클래스 목록 표입니다. 네이비 표 머리와 상태 배지를 쓰고, 모바일에서는 번호·조회 열을 숨깁니다.

#board#table#filter#search#badge2026. 10. 16. 업데이트

블록 신고

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

사유
Claude Code · claude-opus-5-5 · React (TSX)

목록 데이터는 파일 안 상수입니다. 실제 게시판과 연결할 때 POSTS 배열을 게시판 데이터로 바꾸면 됩니다.

결과 코드펼치기
import { useMemo, useState, type FormEvent } from "react";

type State = "모집 중" | "진행 중" | "마감";
type Post = { id: number; title: string; state: State; date: string; views: number };

const CATEGORIES: State[] = ["모집 중", "진행 중", "마감"];

const POSTS: Post[] = [
  { id: 204, title: "나만의 사진집 만들기 (8주)", state: "모집 중", date: "2026.09.01", views: 318 },
  { id: 203, title: "라이트룸 보정 마스터 (4주)", state: "모집 중", date: "2026.09.01", views: 264 },
  { id: 103, title: "처음 만나는 카메라: 노출과 구도 (4주)", state: "모집 중", date: "2026.09.01", views: 402 },
  { id: 102, title: "스마트폰으로 찍는 일상 사진 (원데이)", state: "모집 중", date: "2026.08.28", views: 287 },
  { id: 202, title: "풍경 사진 장노출 실습 (5주)", state: "진행 중", date: "2026.07.15", views: 231 },
  { id: 101, title: "필름 카메라 입문과 현상 체험 (3주)", state: "진행 중", date: "2026.07.15", views: 199 },
  { id: 201, title: "인물 사진 라이팅 (6주)", state: "마감", date: "2026.05.02", views: 356 },
];

const FONT = '"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif';

function StateBadge({ state }: { state: State }) {
  const off = state === "마감";
  return (
    <span className={`inline-flex h-[26px] shrink-0 items-center whitespace-nowrap rounded-full px-2.5 text-xs font-bold leading-none ${off ? "bg-[#f4f6f9] text-[#8a93a3]" : "bg-[#e9effa] text-[#3D64B1]"}`}>
      {state}
    </span>
  );
}

export default function CourseBoard() {
  const [category, setCategory] = useState<State | "">("");
  const [draft, setDraft] = useState("");
  const [keyword, setKeyword] = useState("");

  const posts = useMemo(
    () =>
      POSTS.filter((p) => (category ? p.state === category : true)).filter((p) =>
        keyword ? p.title.toLowerCase().includes(keyword.toLowerCase()) : true,
      ),
    [category, keyword],
  );

  const onSearch = (e: FormEvent) => {
    e.preventDefault();
    setKeyword(draft.trim());
  };

  return (
    <section className="w-full bg-white" style={{ fontFamily: FONT }}>
      <div className="mx-auto w-full max-w-[1200px] px-4 py-12 sm:px-6 lg:py-16">
        <nav aria-label="분류" className="mb-6 flex flex-wrap gap-1.5">
          {(["", ...CATEGORIES] as const).map((c) => (
            <button
              key={c || "all"}
              type="button"
              onClick={() => setCategory(c)}
              aria-pressed={category === c}
              className={`rounded-full border-[1.5px] px-[18px] py-2 text-[15px] font-semibold transition-colors ${
                category === c ? "border-[#1B263B] bg-[#1B263B] text-white" : "border-[#e4e7ed] text-[#4a5466] hover:border-[#172031]"
              }`}
            >
              {c || "전체"}
            </button>
          ))}
        </nav>

        <div className="mb-5 flex flex-wrap items-center justify-between gap-4">
          <p className="text-[15px] text-[#8a93a3]">
            전체 <strong className="text-[#1B263B]">{posts.length}</strong>건
          </p>
          <form onSubmit={onSearch} role="search" className="flex w-full gap-1.5 rounded-full bg-[#f4f6f9] p-[5px] sm:w-auto">
            <label htmlFor="course-keyword" className="sr-only">
              검색어
            </label>
            <input
              id="course-keyword"
              type="search"
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              placeholder="제목 검색"
              className="h-[42px] min-w-0 flex-1 rounded-full bg-transparent px-4 text-[15px] outline-none focus:bg-white sm:w-60 sm:flex-none"
            />
            <button type="submit" className="h-[42px] rounded-full bg-[#1B263B] px-[22px] font-bold text-white">
              검색
            </button>
          </form>
        </div>

        {posts.length === 0 ? (
          <p className="rounded-2xl bg-[#f4f6f9] py-20 text-center text-[#8a93a3]">등록된 글이 없습니다.</p>
        ) : (
          <table className="w-full border-collapse tabular-nums">
            <caption className="sr-only">클래스 목록</caption>
            <thead>
              <tr className="text-sm font-bold text-white">
                <th scope="col" className="hidden w-20 rounded-l-[10px] bg-[#1B263B] p-3.5 sm:table-cell">번호</th>
                <th scope="col" className="rounded-l-[10px] bg-[#1B263B] p-3.5 sm:rounded-none">제목</th>
                <th scope="col" className="w-[92px] rounded-r-[10px] bg-[#1B263B] p-3.5 sm:w-[130px] sm:rounded-none">작성일</th>
                <th scope="col" className="hidden w-[90px] rounded-r-[10px] bg-[#1B263B] p-3.5 sm:table-cell">조회</th>
              </tr>
            </thead>
            <tbody>
              {posts.map((p, i) => (
                <tr key={p.id} className="border-b border-[#e4e7ed] text-center text-[15px] text-[#8a93a3]">
                  <td className="hidden px-3.5 py-5 sm:table-cell">{posts.length - i}</td>
                  <td className="px-2 py-4 text-left sm:px-3.5 sm:py-5">
                    <a href={`/classes/${p.id}`} className="group flex flex-col items-start gap-1.5 sm:flex-row sm:items-center sm:gap-2.5">
                      <StateBadge state={p.state} />
                      <span className="text-base font-semibold text-[#172031] group-hover:text-[#3D64B1]" style={{ wordBreak: "keep-all" }}>
                        {p.title}
                      </span>
                    </a>
                  </td>
                  <td className="px-2 py-4 text-[13px] sm:px-3.5 sm:py-5 sm:text-[15px]">{p.date}</td>
                  <td className="hidden px-3.5 py-5 sm:table-cell">{p.views}</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </section>
  );
}

적용 사이트 0

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

로그인하고 내 사이트 추가

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

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

재현 후기

v1 · 0개

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