일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- 토이프로젝트
- 프론트엔드
- 스택
- 코딩테스트
- 리액트
- 코테
- 리액트 패턴
- 동적계획법
- react router dom
- TypeScript
- 타입스크립트
- revalidatepath
- react pattern
- tailwind
- 리덕스
- Form
- styled component
- React Query
- Supabase
- 코어자바스크립트
- tanstack query
- 그리디
- JavaScript
- React
- 프로그래머스
- reduxtoolkit
- Next.js
- 토이 프로젝트
- 리액트 라우터 돔
- 자바스크립트
- Today
- Total
느려도 한걸음씩
Next.js 토이 프로젝트 (11) - 게시물 일정 마감 기능 & 마감 기능 필터링 본문
이번 포스트에서는 게시물 작성자가 본인의 게시글을 마감하거나 다시 모집 상태로 되돌릴 수 있는 기능과, 마감되지 않은(모집 중인) 게시글만 골라서 보여주는 필터링 기능을 함께 구현해 보겠습니다.
예를 들어, 스터디나 프로젝트 팀원을 모집하는 게시글을 작성한 사용자가 더 이상 팀원이 필요 없을 경우 '마감' 버튼을 눌러 해당 게시글을 마감 처리하고, 사용자들이 모집 중인 게시글만 볼 수 있도록 필터링할 수 있게 하는 것이 목표입니다.
✅ 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 파라미터를 사용하여 필터링할 수 있으며, 토글 버튼을 통해 상태를 쉽게 전환할 수 있습니다.
'토이프로젝트 > find side(가제)' 카테고리의 다른 글
Next.js 토이 프로젝트 (10) - 게시물 페이지 네이션 기능 (0) | 2025.05.09 |
---|---|
Next.js 토이 프로젝트 (9) - 게시물 필터링 기능 (0) | 2025.05.09 |
Next.js 토이 프로젝트 (8) - 게시물 삭제 기능 구현 (0) | 2025.05.08 |
Next.js 토이 프로젝트 (7) - 내 게시물 페이지 구현하기 (0) | 2025.05.08 |
Next.js 토이 프로젝트 (6) - 내 관심글 목록 페이지 구현하기 (0) | 2025.05.08 |