Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"@typescript-eslint/parser": "^8.9.0",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"chromatic": "^11.18.1",
"chromatic": "^11.20.0",
"dotenv-cli": "^7.4.3",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.15",
Expand Down
21 changes: 7 additions & 14 deletions src/app/(pages)/albaList/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useUser } from "@/hooks/queries/user/me/useUser";
import Link from "next/link";
import { IoAdd } from "react-icons/io5";
import { userRoles } from "@/constants/userRoles";
import FloatingBtn from "@/app/components/button/default/FloatingBtn";

const FORMS_PER_PAGE = 10;

Expand Down Expand Up @@ -130,15 +131,11 @@ export default function AlbaList() {
<div className="w-full pt-[132px]">
{/* 폼 만들기 버튼 - 고정 위치 */}
{isOwner && (
<div className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
<Link
href="/addform"
className="flex items-center gap-2 rounded-lg bg-[#FFB800] px-4 py-3 text-base font-semibold text-white shadow-lg transition-all hover:bg-[#FFA800] md:px-6 md:text-lg"
>
<IoAdd className="size-6" />
<span>폼 만들기</span>
</Link>
</div>
<Link href="/addform" className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
<FloatingBtn icon={<IoAdd className="size-6" />} variant="orange">
폼 만들기
</FloatingBtn>
</Link>
)}

{!data?.pages?.[0]?.data?.length ? (
Expand All @@ -152,11 +149,7 @@ export default function AlbaList() {
<React.Fragment key={page.nextCursor}>
{page.data.map((form) => (
<div key={form.id}>
<Link
href={isOwner ? `/albaFormDetail/owner/${form.id}` : `/albaFormDetail/applicant/${form.id}`}
>
<AlbaListItem {...form} />
</Link>
<AlbaListItem {...form} />
</div>
))}
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import React from "react";
import FilterDropdown from "@/app/components/button/dropdown/FilterDropdown";
import { formStatusOptions } from "@/constants/formOptions";
import { useRouter } from "next/navigation";

const APPLICANT_SORT_OPTIONS = [
{ label: "전체", value: "" },
{ label: "최신순", value: formStatusOptions.INTERVIEW_PENDING },
{ label: "시급높은순", value: formStatusOptions.INTERVIEW_COMPLETED },
{ label: "지원자 많은순", value: formStatusOptions.HIRED },
{ label: "스크랩 많은순", value: formStatusOptions.REJECTED },
];

interface ApplicantSortSectionProps {
pathname: string;
searchParams: URLSearchParams;
}

export default function ApplicantSortSection({ pathname, searchParams }: ApplicantSortSectionProps) {
const router = useRouter();
const currentOrderBy = searchParams.get("orderBy") || "";

const currentLabel =
APPLICANT_SORT_OPTIONS.find((opt) => opt.value === currentOrderBy)?.label || APPLICANT_SORT_OPTIONS[0].label;

const handleSortChange = (selected: string) => {
const option = APPLICANT_SORT_OPTIONS.find((opt) => opt.label === selected);
if (option) {
const params = new URLSearchParams(searchParams);
params.set("orderBy", option.value);
router.push(`${pathname}?${params.toString()}`);
}
};

return (
<FilterDropdown
options={APPLICANT_SORT_OPTIONS.map((option) => option.label)}
className="!w-28 md:!w-40"
initialValue={currentLabel}
onChange={handleSortChange}
/>
);
}
122 changes: 113 additions & 9 deletions src/app/(pages)/myAlbaform/(role)/applicant/page.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,141 @@
"use client";

import React from "react";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useInView } from "react-intersection-observer";
import { useUser } from "@/hooks/queries/user/me/useUser";
import { userRoles } from "@/constants/userRoles";
import ApplicantSortSection from "./components/ApplicantSortSection";
import SearchSection from "@/app/components/layout/forms/SearchSection";
import MyApplicationListItem from "@/app/components/card/cardList/MyApplicationListItem";
import { useMyApplications } from "@/hooks/queries/user/me/useMyApplications";

const APPLICATIONS_PER_PAGE = 10;

export default function ApplicantPage() {
const router = useRouter();
const { user, isLoading } = useUser();
const pathname = usePathname();
const searchParams = useSearchParams();
const { user, isLoading: isUserLoading } = useUser();

// 무한 스크롤을 위한 Intersection Observer 설정
const { ref, inView } = useInView({
threshold: 0.1,
triggerOnce: false,
rootMargin: "100px",
});

// 검색 및 정렬 상태 관리
const status = searchParams.get("status") || undefined;
const keyword = searchParams.get("keyword") || undefined;

const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: isLoadingData,
error,
} = useMyApplications({
limit: APPLICATIONS_PER_PAGE,
status,
keyword,
});

useEffect(() => {
if (!isLoading) {
if (!isUserLoading) {
if (!user) {
router.push("/login");
} else if (user.role === userRoles.OWNER) {
router.push("/myAlbaform/owner");
}
}
}, [user, isLoading, router]);
}, [user, isUserLoading, router]);

// 스크롤이 하단에 도달하면 다음 페이지 로드
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);

// 에러 상태 처리
if (error) {
return (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<p className="text-red-500">지원 내역을 불러오는데 실패했습니다.</p>
</div>
);
}

if (isLoading) {
// 로딩 상태 처리
if (isUserLoading || isLoadingData) {
return (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<div>로딩 중...</div>
</div>
);
}

// 지원자용 페이지 컨텐츠
return (
<div>
<h1>지원자 페이지</h1>
{/* 지원자용 컨텐츠 */}
<div className="flex min-h-screen flex-col items-center">
{/* 검색 섹션과 필터를 고정 위치로 설정 */}
<div className="fixed left-0 right-0 top-16 z-40 bg-white shadow-sm">
{/* 검색 섹션 */}
<div className="w-full border-b border-grayscale-100">
<div className="mx-auto flex max-w-screen-2xl flex-col gap-4 px-4 py-4 md:px-6 lg:px-8">
<SearchSection />
</div>
</div>

{/* 필터 섹션 */}
<div className="w-full border-b border-grayscale-100">
<div className="mx-auto flex max-w-screen-2xl items-center justify-between gap-2 px-4 py-4 md:px-6 lg:px-8">
<ApplicantSortSection pathname={pathname} searchParams={searchParams} />
</div>
</div>
</div>

{/* 메인 콘텐츠 영역 */}
<div className="w-full pt-[132px]">
{!data?.pages?.[0]?.data?.length ? (
<div className="flex h-[calc(100vh-200px)] flex-col items-center justify-center">
<p className="text-grayscale-500">지원 내역이 없습니다.</p>
</div>
) : (
<div className="mx-auto mt-4 w-full max-w-screen-xl px-3">
<div className="flex flex-wrap justify-start gap-6">
{data?.pages.map((page) => (
<React.Fragment key={page.nextCursor}>
{page.data.map((application) => (
<div key={application.id}>
<MyApplicationListItem
id={application.id}
createdAt={application.createdAt}
updatedAt={application.updatedAt}
status={application.status}
resumeId={application.resumeId}
resumeName={application.resumeName}
form={application.form}
/>
</div>
))}
</React.Fragment>
))}
</div>

{/* 무한 스크롤 트리거 영역 */}
<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>
</div>
);
}
15 changes: 6 additions & 9 deletions src/app/(pages)/myAlbaform/(role)/owner/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useUser } from "@/hooks/queries/user/me/useUser";
import Link from "next/link";
import { IoAdd } from "react-icons/io5";
import { userRoles } from "@/constants/userRoles";
import FloatingBtn from "@/app/components/button/default/FloatingBtn";

const FORMS_PER_PAGE = 10;

Expand Down Expand Up @@ -186,15 +187,11 @@ export default function AlbaList() {
<div className="w-full pt-[132px]">
{/* 폼 만들기 버튼 - 고정 위치 */}
{isOwner && (
<div className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
<Link
href="/addform"
className="flex items-center gap-2 rounded-lg bg-[#FFB800] px-4 py-3 text-base font-semibold text-white shadow-lg transition-all hover:bg-[#FFA800] md:px-6 md:text-lg"
>
<IoAdd className="size-6" />
<span>폼 만들기</span>
</Link>
</div>
<Link href="/addform" className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
<FloatingBtn icon={<IoAdd className="size-6" />} variant="orange">
폼 만들기
</FloatingBtn>
</Link>
)}

{!data?.pages?.[0]?.data?.length ? (
Expand Down
34 changes: 32 additions & 2 deletions src/app/components/card/cardList/AlbaListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,24 @@ const AlbaListItem = ({
title: "지원하기",
content: "정말로 지원하시겠습니까?",
Copy link
Collaborator

Choose a reason for hiding this comment

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

지원하시겠습니까? 또는 지원서를 작성하시겠습니까? 로 해도 좋을것같아요 ㅎㅎ
삭제같은 경우에는 정말로 삭제하시겠습니까? 로 하고요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

onConfirm: () => {
openModal("customForm", {
isOpen: false,
title: "",
content: "",
onConfirm: () => {},
onCancel: () => {},
});
router.push(`/apply/${id}`);
},
onCancel: () => {},
onCancel: () => {
openModal("customForm", {
isOpen: false,
title: "",
content: "",
onConfirm: () => {},
onCancel: () => {},
});
},
});
};

Expand All @@ -78,9 +93,24 @@ const AlbaListItem = ({
title: "스크랩 확인",
content: "이 공고를 스크랩하시겠습니까?",
onConfirm: () => {
openModal("customForm", {
isOpen: false,
title: "",
content: "",
onConfirm: () => {},
onCancel: () => {},
});
scrap();
},
onCancel: () => {},
onCancel: () => {
openModal("customForm", {
isOpen: false,
title: "",
content: "",
onConfirm: () => {},
onCancel: () => {},
});
},
});
};

Expand Down
Loading
Loading