diff --git a/src/app/(pages)/mypage/components/FilterBar/SortSection.tsx b/src/app/(pages)/mypage/components/FilterBar/SortSection.tsx index 105d1f12..af99a553 100644 --- a/src/app/(pages)/mypage/components/FilterBar/SortSection.tsx +++ b/src/app/(pages)/mypage/components/FilterBar/SortSection.tsx @@ -27,7 +27,7 @@ export default function SortSection() { }; return ( -
+
option.label)} className="!w-28 md:!w-40" diff --git a/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx b/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx index 645bf98f..6b4575e7 100644 --- a/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx +++ b/src/app/(pages)/mypage/components/sections/ScrapsSection.tsx @@ -4,7 +4,10 @@ import React, { useEffect } from "react"; import { useInView } from "react-intersection-observer"; import { useMyScraps } from "@/hooks/queries/user/me/useMyScraps"; import { useSortStore } from "@/store/sortStore"; +import { useFilterStore } from "@/store/filterStore"; import type { FormListType } from "@/types/response/form"; +import FilterDropdown from "@/app/components/button/dropdown/FilterDropdown"; +import { filterPublicOptions, filterRecruitingOptions } from "@/constants/filterOptions"; // 한 페이지당 스크랩 수 const SCRAPS_PER_PAGE = 10; @@ -12,6 +15,7 @@ const SCRAPS_PER_PAGE = 10; export default function ScrapsSection() { // 정렬 상태 관리 const { orderBy } = useSortStore(); + const { filterBy, setFilterBy } = useFilterStore(); // 무한 스크롤을 위한 Intersection Observer 설정 const { ref, inView } = useInView({ @@ -24,8 +28,35 @@ export default function ScrapsSection() { const { data, isLoading, error, hasNextPage, fetchNextPage, isFetchingNextPage } = useMyScraps({ limit: SCRAPS_PER_PAGE, orderBy: orderBy.scrap, + isPublic: filterBy.isPublic, + isRecruiting: filterBy.isRecruiting, }); + const handlePublicFilter = (selected: string) => { + const option = filterPublicOptions.find((opt) => opt.label === selected); + if (option) { + setFilterBy("isPublic", String(option.value)); + } + }; + + const handleRecruitingFilter = (selected: string) => { + const option = filterRecruitingOptions.find((opt) => opt.label === selected); + if (option) { + setFilterBy("isRecruiting", String(option.value)); + } + }; + + // 현재 필터 상태에 따른 초기값 설정을 위한 함수들 + const getInitialPublicValue = (isPublic: boolean) => { + const option = filterPublicOptions.find((opt) => opt.value === isPublic); + return option?.label || "전체"; + }; + + const getInitialRecruitingValue = (isRecruiting: boolean) => { + const option = filterRecruitingOptions.find((opt) => opt.value === isRecruiting); + return option?.label || "전체"; + }; + // 스크롤이 하단에 도달하면 다음 페이지 로드 useEffect(() => { if (inView && hasNextPage && !isFetchingNextPage) { @@ -33,7 +64,7 @@ export default function ScrapsSection() { } }, [inView, hasNextPage, fetchNextPage, isFetchingNextPage]); - // 에러 상태 처리 + // 에러 ��태 처리 if (error) { return (
@@ -51,43 +82,58 @@ export default function ScrapsSection() { ); } - // 데이터가 없는 경우 처리 - if (!data?.pages[0]?.data?.length) { - return ( -
-

스크랩한 공고가 없습니다.

-
- ); - } - return (
+ {/* 필터 드롭다운 섹션 */} +
+
+ option.label)} + initialValue={getInitialPublicValue(filterBy.isPublic)} + onChange={handlePublicFilter} + /> + option.label)} + initialValue={getInitialRecruitingValue(filterBy.isRecruiting)} + onChange={handleRecruitingFilter} + /> +
+
+ {/* 스크랩 목록 렌더링 */} - {data.pages.map((page, index) => ( - - {page.data.map((scrap: FormListType) => ( -
-

{scrap.title}

-
- 지원자 {scrap.applyCount}명 - - 스크랩 {scrap.scrapCount}명 - - 마감 {new Date(scrap.recruitmentEndDate).toLocaleDateString()} -
-
+ {!data?.pages[0]?.data?.length ? ( +
+

스크랩한 공고가 없습니다.

+
+ ) : ( + <> + {data.pages.map((page, index) => ( + + {page.data.map((scrap: FormListType) => ( +
+

{scrap.title}

+
+ 지원자 {scrap.applyCount}명 + + 스크랩 {scrap.scrapCount}명 + + 마감 {new Date(scrap.recruitmentEndDate).toLocaleDateString()} +
+
+ ))} +
))} -
- ))} - {/* 무한 스크롤 트리거 영역 */} -
- {isFetchingNextPage && ( -
-
+ {/* 무한 스크롤 트리거 영역 */} +
+ {isFetchingNextPage && ( +
+
+
+ )}
- )} -
+ + )}
); } diff --git a/src/app/(pages)/mypage/layout.tsx b/src/app/(pages)/mypage/layout.tsx index 3c5db867..c493c6b1 100644 --- a/src/app/(pages)/mypage/layout.tsx +++ b/src/app/(pages)/mypage/layout.tsx @@ -8,7 +8,7 @@ interface MypageLayoutProps { export default function MypageLayout({ children }: MypageLayoutProps) { return (
-
+
로딩 중...
}> {children} diff --git a/src/app/api/users/me/scrap/route.ts b/src/app/api/users/me/scrap/route.ts index 275c60cd..1879b26a 100644 --- a/src/app/api/users/me/scrap/route.ts +++ b/src/app/api/users/me/scrap/route.ts @@ -15,14 +15,24 @@ export async function GET(request: Request) { // URL 쿼리 파라미터 파싱 const { searchParams } = new URL(request.url); - const params = { - cursor: searchParams.get("cursor"), // 페이지네이션 커서 - limit: searchParams.get("limit"), // 한 페이지당 항목 수 - orderBy: searchParams.get("orderBy"), // 정렬 기준 - isPublic: searchParams.get("isPublic"), // 공개 여부 - isRecruiting: searchParams.get("isRecruiting"), // 모집 중 여부 + const params: Record = { + cursor: searchParams.get("cursor"), + limit: searchParams.get("limit"), + orderBy: searchParams.get("orderBy"), }; + // isPublic과 isRecruiting은 값이 있을 때만 추가 + const isPublic = searchParams.get("isPublic"); + const isRecruiting = searchParams.get("isRecruiting"); + + if (isPublic !== null && isPublic !== "null") { + params.isPublic = isPublic; + } + + if (isRecruiting !== null && isRecruiting !== "null") { + params.isRecruiting = isRecruiting; + } + // 스크랩 목록 조회 요청 const response = await apiClient.get("/users/me/scrap", { headers: { diff --git a/src/constants/filterOptions.ts b/src/constants/filterOptions.ts new file mode 100644 index 00000000..911cd296 --- /dev/null +++ b/src/constants/filterOptions.ts @@ -0,0 +1,11 @@ +export const filterPublicOptions = [ + { label: "전체", value: null }, + { label: "공개", value: true }, + { label: "비공개", value: false }, +] as const; + +export const filterRecruitingOptions = [ + { label: "전체", value: null }, + { label: "모집중", value: true }, + { label: "모집마감", value: false }, +] as const; diff --git a/src/store/filterStore.ts b/src/store/filterStore.ts new file mode 100644 index 00000000..051f5cde --- /dev/null +++ b/src/store/filterStore.ts @@ -0,0 +1,25 @@ +import { create } from "zustand"; + +type FilterType = "isPublic" | "isRecruiting"; + +interface FilterState { + filterBy: { + isPublic: boolean; + isRecruiting: boolean; + }; + setFilterBy: (filterType: FilterType, value: string) => void; +} + +export const useFilterStore = create((set) => ({ + filterBy: { + isPublic: true, + isRecruiting: true, + }, + setFilterBy: (filterType, value) => + set((state) => ({ + filterBy: { + ...state.filterBy, + [filterType]: value, + }, + })), +}));