Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function SortSection() {
};

return (
<div className="my-2 flex justify-end lg:my-4">
<div className="fixed right-6 top-[180px] z-10 md:right-28 md:top-[200px] lg:right-8">
<FilterDropdown
options={options.map((option) => option.label)}
className="!w-28 md:!w-40"
Expand Down
110 changes: 78 additions & 32 deletions src/app/(pages)/mypage/components/sections/ScrapsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ 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;

export default function ScrapsSection() {
// 정렬 상태 관리
const { orderBy } = useSortStore();
const { filterBy, setFilterBy } = useFilterStore();

// 무한 스크롤을 위한 Intersection Observer 설정
const { ref, inView } = useInView({
Expand All @@ -24,16 +28,43 @@ 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) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage, isFetchingNextPage]);

// 에러 상태 처리
// 에러 ��태 처리
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오잇

if (error) {
return (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
Expand All @@ -51,43 +82,58 @@ export default function ScrapsSection() {
);
}

// 데이터가 없는 경우 처리
if (!data?.pages[0]?.data?.length) {
return (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<p className="text-grayscale-500">스크랩한 공고가 없습니다.</p>
</div>
);
}

return (
<div className="space-y-4">
{/* 필터 드롭다운 섹션 */}
<div className="border-b border-grayscale-100">
<div className="flex items-center gap-2 py-4">
<FilterDropdown
options={filterPublicOptions.map((option) => option.label)}
initialValue={getInitialPublicValue(filterBy.isPublic)}
onChange={handlePublicFilter}
/>
<FilterDropdown
options={filterRecruitingOptions.map((option) => option.label)}
initialValue={getInitialRecruitingValue(filterBy.isRecruiting)}
onChange={handleRecruitingFilter}
/>
</div>
</div>

{/* 스크랩 목록 렌더링 */}
{data.pages.map((page, index) => (
<React.Fragment key={index}>
{page.data.map((scrap: FormListType) => (
<div key={scrap.id} className="rounded-lg border p-4 transition-all hover:border-primary-orange-200">
<h3 className="font-bold">{scrap.title}</h3>
<div className="mt-2 text-sm text-grayscale-500">
<span>지원자 {scrap.applyCount}명</span>
<span className="mx-2">•</span>
<span>스크랩 {scrap.scrapCount}명</span>
<span className="mx-2">•</span>
<span>마감 {new Date(scrap.recruitmentEndDate).toLocaleDateString()}</span>
</div>
</div>
{!data?.pages[0]?.data?.length ? (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<p className="text-grayscale-500">스크랩한 공고가 없습니다.</p>
</div>
) : (
<>
{data.pages.map((page, index) => (
<React.Fragment key={index}>
{page.data.map((scrap: FormListType) => (
<div key={scrap.id} className="rounded-lg border p-4 transition-all hover:border-primary-orange-200">
<h3 className="font-bold">{scrap.title}</h3>
<div className="mt-2 text-sm text-grayscale-500">
<span>지원자 {scrap.applyCount}명</span>
<span className="mx-2">•</span>
<span>스크랩 {scrap.scrapCount}명</span>
<span className="mx-2">•</span>
<span>마감 {new Date(scrap.recruitmentEndDate).toLocaleDateString()}</span>
</div>
</div>
))}
</React.Fragment>
))}
</React.Fragment>
))}

{/* 무한 스크롤 트리거 영역 */}
<div ref={ref} className="h-4 w-full">
{isFetchingNextPage && (
<div className="flex justify-center py-4">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary-orange-300 border-t-transparent" />
{/* 무한 스크롤 트리거 영역 */}
<div ref={ref} className="h-4 w-full">
{isFetchingNextPage && (
<div className="flex justify-center py-4">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary-orange-300 border-t-transparent" />
</div>
)}
</div>
)}
</div>
</>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion src/app/(pages)/mypage/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface MypageLayoutProps {
export default function MypageLayout({ children }: MypageLayoutProps) {
return (
<div className="flex min-h-screen flex-col">
<div className="mx-auto w-full min-w-[327px] px-6 md:min-w-[600px] lg:min-w-[1480px]">
<div className="mx-auto w-full min-w-[327px] px-6 md:max-w-[600px] lg:max-w-[1480px]">
<Suspense fallback={<div>로딩 중...</div>}>
<FilterBar />
{children}
Expand Down
22 changes: 16 additions & 6 deletions src/app/api/users/me/scrap/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | null> = {
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: {
Expand Down
11 changes: 11 additions & 0 deletions src/constants/filterOptions.ts
Original file line number Diff line number Diff line change
@@ -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;
25 changes: 25 additions & 0 deletions src/store/filterStore.ts
Original file line number Diff line number Diff line change
@@ -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<FilterState>((set) => ({
filterBy: {
isPublic: true,
isRecruiting: true,
},
setFilterBy: (filterType, value) =>
set((state) => ({
filterBy: {
...state.filterBy,
[filterType]: value,
},
})),
}));
Loading