토이프로젝트/find side(가제)

Next.js 토이 프로젝트 (3) - 전체 게시물 조회 및 게시물 동적 라우팅

hoj0806 2025. 5. 7. 02:11

 

이번 포스트에서는 posts 테이블의 모든 데이터를 불러오고, 각 게시물을 클릭하면 해당 상세 페이지로 이동하는 동적 라우팅 기능을 구현해보겠습니다.

 

 

 

✅ 1. Supabase에서 전체 게시물 가져오기

우선 Supabase 클라이언트를 사용해 posts 테이블의 전체 데이터를 조회하는 함수를 만들어야 합니다. 아래는 app/actions.ts에 정의된 함수입니다

export async function getPosts() {
  const supabase = await createClient();

  const { data, error } = await supabase
    .from("posts")
    .select("*")
    .order("created_at", { ascending: false });

  if (error) {
    console.error("게시글 조회 실패:", error.message);
    return [];
  }

  return data;
}

 

📌 설명

  • createClient()는 Supabase 클라이언트를 생성합니다.
  • select("*")는 모든 컬럼을 선택합니다.
  • order("created_at", { ascending: false })는 최신 게시글이 가장 먼저 오도록 정렬합니다.

 

 

 

✅ 2. 메인 페이지에서 게시글 목록 보여주기

이제 app/page.tsx에서 getPosts 함수를 호출하여 불러온 데이터를 화면에 렌더링해보겠습니다.

// app/page.tsx

import Link from "next/link";
import { getPosts } from "./actions";

export default async function Home() {
  const data = await getPosts();

  return (
    <main className='max-w-7xl mx-auto p-6'>
      <h1 className='text-2xl font-bold mb-6'>모집 게시판</h1>

      <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6'>
        {data.map((post) => (
          <Link href={`/study/${post.id}`} key={post.id}>
            <div className='bg-white border border-gray-200 shadow-sm rounded-xl p-4 space-y-2'>
              <div className='text-sm text-gray-500'>{post.category}</div>
              <h2 className='text-lg font-semibold truncate text-black'>
                {post.title}
              </h2>
              <div className='text-sm text-gray-700 line-clamp-2'>
                {post.content}
              </div>
              <div className='text-xs text-gray-400'>
                모집 인원: {post.recruitment_count}
              </div>
              <div className='text-xs text-gray-400'>
                마감일: {post.deadline}
              </div>
            </div>
          </Link>
        ))}
      </div>
    </main>
  );
}

 

📌 설명:

  • Link 컴포넌트를 사용하여 각 게시글 카드를 클릭 시 /study/{postId} 경로로 이동할 수 있게 합니다.

 

 

✅ 3. 특정 게시글 불러오기 (getPostById)

상세 페이지에서 사용할 데이터를 가져오기 위해, id 값을 기반으로 단일 게시글을 조회하는 함수를 추가합니다.

export async function getPostById(id: string) {
  const supabase = await createClient();

  const { data, error } = await supabase
    .from("posts")
    .select("*")
    .eq("id", id)
    .single();

  if (error) {
    console.error("게시글 조회 실패:", error.message);
    return null;
  }

  return data;
}

 

📌 설명:

  • .eq("id", id)는 posts 테이블에서 id가 일치하는 행을 필터링합니다.
  • .single()을 사용하면 하나의 객체만 반환받을 수 있습니다.

 

 

 

 

✅ 4. 상세 페이지 동적 라우팅 구현

/study/[postId]/page.tsx 파일을 생성하여 게시글의 상세 정보를 표시할 페이지를 구성합니다.

// app/study/[posId]/page.tsx

import { getPostById } from "@/app/actions";

type PostDetailPageProps = {
  params: {
    postId: string;
  };
};

const PostDetailPage = async ({ params }: PostDetailPageProps) => {
  const post = await getPostById(params.postId);

  if (!post) {
    return (
      <div className='text-center mt-10 text-red-600'>
        ❌ 게시글을 찾을 수 없습니다.
      </div>
    );
  }

  return (
    <div className='max-w-3xl mx-auto p-6 mt-10 bg-white rounded-xl shadow-md space-y-6'>
      <h1 className='text-2xl font-bold'>{post.title}</h1>

      <div className='text-gray-700 whitespace-pre-wrap'>{post.content}</div>

      <div className='grid grid-cols-2 gap-4 text-sm text-gray-600'>
        <div>
          <strong>모집 구분:</strong> {post.category}
        </div>
        <div>
          <strong>모집 인원:</strong> {post.recruitment_count}
        </div>
        <div>
          <strong>진행 방식:</strong> {post.mode}
        </div>
        <div>
          <strong>진행 기간:</strong> {post.duration}
        </div>
        <div>
          <strong>마감일:</strong> {post.deadline}
        </div>
        <div>
          <strong>연락 방법:</strong> {post.contact_method}
        </div>
        <div className='col-span-2'>
          <strong>연락 링크:</strong>
          <a
            href={post.contact_link}
            className='text-blue-600 underline break-all'
            target='_blank'
          >
            {post.contact_link}
          </a>
        </div>
      </div>

      <div className='space-y-2'>
        <div>
          <strong>기술 스택:</strong>
          <span className='text-sm text-gray-800'>
            {post.tech_stack?.join(", ")}
          </span>
        </div>
        <div>
          <strong>모집 포지션:</strong>
          <span className='text-sm text-gray-800'>
            {post.positions?.join(", ")}
          </span>
        </div>
      </div>
    </div>
  );
};

export default PostDetailPage;

 

📌 설명:

  • params.postId는 동적 라우팅을 통해 URL에서 받은 게시물 ID입니다.
  • 조건부 렌더링을 통해 게시글이 없을 경우 안내 메시지를 출력합니다.
  • Tailwind CSS를 활용해 게시글 상세 정보를 카드 형식으로 보기 좋게 구성했습니다.

 

✅ 적용 화면

 

 

 

 

 

✍️ 마무리

이번 글에서는 게시물 목록을 가져와 메인 페이지에 보여주고, 게시글 클릭 시 해당 상세 페이지로 이동하는 동적 라우팅을 구현해보았습니다.