느려도 한걸음씩

Next.js 토이 프로젝트 (11) - 게시물 일정 마감 기능 & 마감 기능 필터링 본문

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

Next.js 토이 프로젝트 (11) - 게시물 일정 마감 기능 & 마감 기능 필터링

hoj0806 2025. 5. 10. 01:13

이번 포스트에서는 게시물 작성자가 본인의 게시글을 마감하거나 다시 모집 상태로 되돌릴 수 있는 기능과, 마감되지 않은(모집 중인) 게시글만 골라서 보여주는 필터링 기능을 함께 구현해 보겠습니다.

 

예를 들어, 스터디나 프로젝트 팀원을 모집하는 게시글을 작성한 사용자가 더 이상 팀원이 필요 없을 경우 '마감' 버튼을 눌러 해당 게시글을 마감 처리하고, 사용자들이 모집 중인 게시글만 볼 수 있도록 필터링할 수 있게 하는 것이 목표입니다.

 

 

✅ 1. posts 테이블에 expired 필드 추가

먼저 Supabase의 posts 테이블에 expired라는 새로운 컬럼을 추가합니다. 이 필드는 게시글의 마감 여부를 저장하는 Boolean 타입이며, 기본값은 false로 설정합니다. 즉, 게시글이 처음 생성되면 기본적으로 "모집 중" 상태입니다.

 

 

✅ 2. 게시글 소유자만 수정 가능하도록 RLS 설정

이제 게시글의 expired 값을 업데이트할 수 있도록 Row Level Security(RLS)를 설정합니다. 단, 해당 게시글의 작성자만 수정이 가능하도록 제약을 걸었습니다.

CREATE POLICY "Users can update their own posts"
ON posts
FOR UPDATE
USING (
  auth.uid() = user_id
);

이렇게 하면 인증된 사용자가 자신이 작성한 게시글에 대해서만 업데이트 권한을 가지게 됩니다.

 

 

 

✅ 3. 게시글 마감/마감 해제 서버 액션

클라이언트에서 버튼을 클릭했을 때 작동할 서버 액션을 작성합니다.

🔹 게시글 마감 (expired: true)

export async function expirePost(formData: FormData) {
  // 유저 인증 및 게시글 작성자 확인
  ...
  const { error } = await supabase
    .from("posts")
    .update({ expired: true })
    .eq("id", postId);
  ...
}

 

🔹 게시글 마감 취소 (expired: false)

export async function unexpirePost(formData: FormData) {
  // 유저 인증 및 게시글 작성자 확인
  ...
  const { error } = await supabase
    .from("posts")
    .update({ expired: false })
    .eq("id", postId);
  ...
}

 

두 함수 모두:

  • 로그인 여부 확인
  • 게시글 작성자가 맞는지 검증
  • 조건을 통과하면 posts 테이블의 expired 값을 업데이트

업데이트 후에는 revalidatePath()를 호출해 해당 페이지가 새롭게 갱신되도록 합니다.

 

 

✅ 4. 마감/마감 취소 버튼 UI 구현

이제 게시글 상세 페이지에서 작성자일 경우에만 마감 상태를 변경할 수 있는 버튼을 보여줍니다.

{isAuthor && (
  <form action={!post.expired ? expirePost : unexpirePost}>
    <input type='hidden' name='post_id' value={postId} />
    <button
      type='submit'
      className='text-sm bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600'
    >
      {!post.expired ? "마감" : "마감 취소"}
    </button>
  </form>
)}

 

  • post.expired 값에 따라 버튼 텍스트와 서버 액션이 달라지며,
  • 숨겨진 <input>을 통해 post_id를 전달합니다.

🔒 참고: 버튼은 오직 게시글의 작성자에게만 보이도록 조건(isAuthor)을 걸어 UI 접근 제어도 함께 처리했습니다.

 

 

다음으로는 모집중인 게시물 필터링 기능을 구현해보겠습니다

 

📦 모집중인 게시물 필터링 기능 구현

1. Supabase 쿼리 수정

우선, getPosts 함수에서 showAll 파라미터를 추가하여 모집 중인 게시물만 불러올 수 있도록 조건을 추가합니다.

변경된 getPosts 함수

export async function getPosts(
  categoryParams?: string,
  modeParams?: string,
  positionParams?: string,
  searchQuery?: string,
  page: number = 1,
  pageSize: number = 1,
  showAll?: string // ✅ 추가된 파라미터
) {
  const supabase = await createClient();
  let query = supabase
    .from("posts")
    .select("*", { count: "exact" })
    .order("created_at", { ascending: false });

  // 필터링 조건
  if (categoryParams) query = query.eq("category", categoryParams);
  if (modeParams) query = query.eq("mode", modeParams);
  if (positionParams) query = query.overlaps("positions", [positionParams]);
  if (searchQuery) {
    query = query.or(
      `title.ilike.%${searchQuery}%,content.ilike.%${searchQuery}%`
    );
  }

  // 모집 중인 게시물만 필터링 (showAll이 "true"가 아닐 경우)
  if (showAll !== "true") {
    query = query.eq("expired", false); // 모집이 끝난 게시물은 제외
  }

  const from = (page - 1) * pageSize;
  const to = from + pageSize - 1;

  const { data, error, count } = await query.range(from, to);

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

  return { data, total: count ?? 0 };
}

 

 

 

2. showAll 토글 버튼 구현

사용자가 모집 중인 게시물만 볼지 아니면 모든 게시물을 볼지 선택할 수 있게 토글 버튼을 추가합니다. 이 버튼을 클릭하면 URL의 showAll 파라미터가 변경되고, 페이지가 리셋되어 새로고침됩니다.

ShowAllToggleButton 컴포넌트

"use client";

import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useCallback } from "react";

const ShowAllToggleButton = () => {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const router = useRouter();

  const showAll = searchParams.get("showAll");

  const createQueryString = useCallback(
    (value: string | null) => {
      const params = new URLSearchParams(searchParams.toString());

      if (value === null) {
        // 모집중만 보기 (기본값)
        params.delete("showAll");
      } else {
        // 전체 보기
        params.set("showAll", value);
      }

      // 페이지를 1로 리셋
      params.set("page", "1");

      return params.toString();
    },
    [searchParams]
  );

  const handleToggle = () => {
    const newValue = showAll === "true" ? null : "true";
    const query = createQueryString(newValue);
    router.push(`${pathname}?${query}`);
  };

  const isActive = showAll !== "true"; // 기본: 모집중만 보기 (활성화 상태)

  return (
    <button
      onClick={handleToggle}
      className={`px-4 py-2 text-sm rounded border border-gray-300 shadow-sm transition-colors
        ${isActive ? "bg-purple-100 text-purple-700 hover:bg-purple-200" : "bg-white text-gray-600 hover:bg-gray-100"}`}
    >
      모집중만 보기
    </button>
  );
};

export default ShowAllToggleButton;

 

설명:

  • createQueryString: 쿼리 스트링을 생성하는 함수로, showAll 파라미터 값을 변경하여 URL을 업데이트합니다.
  • handleToggle: 버튼 클릭 시 showAll 값을 변경하고, 페이지를 1로 리셋합니다.
  • isActive: showAll 값이 "true"일 때, 버튼의 스타일을 변경하여 활성화된 상태를 나타냅니다.

3. getPosts 호출 시 showAll 파라미터 전달

페이지 컴포넌트에서는 getPosts 함수 호출 시 showAll 파라미터를 전달하여 모집 중인 게시물만 필터링하도록 합니다.

게시물 호출 코드

const [posts, likes] = await Promise.all([
  getPosts(
    searchParams.category,
    searchParams.mode,
    searchParams.position,
    searchParams.search,
    page,
    pageSize,
    searchParams.showAll // showAll 파라미터 전달
  ),
  getMyLikes(),
]);

 

 

 

✅ 마무리

이번에는 모집중인 게시물 필터링 기능을 구현했습니다. 이 기능을 통해 사용자는 모집이 종료된 게시물을 제외하고, 현재 모집 중인 게시물만 볼 수 있게 되었습니다. showAll 파라미터를 사용하여 필터링할 수 있으며, 토글 버튼을 통해 상태를 쉽게 전환할 수 있습니다.