토이프로젝트/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를 활용해 게시글 상세 정보를 카드 형식으로 보기 좋게 구성했습니다.
✅ 적용 화면
✍️ 마무리
이번 글에서는 게시물 목록을 가져와 메인 페이지에 보여주고, 게시글 클릭 시 해당 상세 페이지로 이동하는 동적 라우팅을 구현해보았습니다.