diff --git a/src/app/(afterLogin)/dashboard/page.tsx b/src/app/(afterLogin)/dashboard/page.tsx index 327938d..5fb60e5 100644 --- a/src/app/(afterLogin)/dashboard/page.tsx +++ b/src/app/(afterLogin)/dashboard/page.tsx @@ -1,11 +1,92 @@ -import ScheduleSection from "./_components/scheduleSection"; -import ApplicationStatusSection from "./_components/applicationStatusSection"; +"use client"; + +import { Schedule } from "@/type/applicationType"; +import CalendarIcon from "@/assets/CalendarCheck.svg"; +import LoadingSpinner from "@/app/_components/loadingSpinner"; +import { useDashboard } from "@/hooks/useDashboard"; export default function DashboardPage() { + const { data, isLoading, isError } = useDashboard(); + const applicationStatus = data?.applicationStatusCounts; + const scheduleData = data?.scheduleData; + + if (isLoading) return ; + if (isError) return 오류가 발생하였습니다. 잠시 후 시도해주세요.; + + const box_style = "w-60 border rounded-sm px-9 py-5 flex flex-col gap-4"; return ( <> - - + + + 지원 회사 + + {applicationStatus.APPLIED}개 + + + + 서류 합격 + + {applicationStatus.DOCUMENT_PASSED}개 + + + + 불합격 + + {applicationStatus.REJECTED}개 + + + + 최종 합격 + + {applicationStatus.FINAL_PASSED}개 + + + + + + 다가오는 일정 + + + {scheduleData?.map((schedule: Schedule & { dday: string }) => ( + + + + {schedule.title} + + {schedule.dday} + + + + ))} + + > ); } diff --git a/src/app/api/dashboard/route.ts b/src/app/api/dashboard/route.ts new file mode 100644 index 0000000..0e3d1c2 --- /dev/null +++ b/src/app/api/dashboard/route.ts @@ -0,0 +1,114 @@ +import { Schedule } from "@/type/applicationType"; +import { NextResponse } from "next/server"; + +interface CompanyApplication { + status: "APPLIED" | "DOCUMENT_PASSED" | "FINAL_PASSED" | "REJECTED" | string; +} + +function getDDay(dateTime: string) { + const today = new Date(); + const targetDate = new Date(dateTime); + + const todayOnly = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + ); + const targetOnly = new Date( + targetDate.getFullYear(), + targetDate.getMonth(), + targetDate.getDate() + ); + + const diffTime = targetOnly.getTime() - todayOnly.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays > 0) return `D-${diffDays}`; + else if (diffDays === 0) return `D-Day`; + else return `D+${Math.abs(diffDays)}`; +} + +export async function GET(req: Request) { + try { + const token = req.headers.get("Authorization"); + const startDate = new Date().toISOString(); + const endDate = new Date( + new Date().setDate(new Date().getDate() + 7) + ).toISOString(); + + const apiUrl = new URL( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/application-forms` + ); + + const [applicationResponse, scheduleResponse] = await Promise.all([ + fetch(apiUrl.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: token || "", + }, + }), + fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/schedules?startDate=${startDate}&endDate=${endDate}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: token || "", + }, + } + ), + ]); + + if (!applicationResponse.ok) { + throw new Error( + `Failed to fetch applications: ${applicationResponse.statusText}` + ); + } + if (!scheduleResponse.ok) { + throw new Error( + `Failed to fetch schedules: ${scheduleResponse.statusText}` + ); + } + + const applicationData = await applicationResponse.json(); + const scheduleData = await scheduleResponse.json(); + + const applications: CompanyApplication[] = applicationData.data.content; + const schedules = scheduleData.data.content; + const refinedSchedules = schedules.map( + (schedule: Schedule) => ({ + id: schedule.id, + title: schedule.title, + dateTime: schedule.dateTime, + dday: getDDay(schedule.dateTime), + }) + ); + + const counts = { + APPLIED: 0, + DOCUMENT_PASSED: 0, + FINAL_PASSED: 0, + REJECTED: 0, + }; + + if (Array.isArray(applications)) { + applications.forEach((app: CompanyApplication) => { + if (app.status === "APPLIED") counts.APPLIED += 1; + else if (app.status === "DOCUMENT_PASSED") counts.DOCUMENT_PASSED += 1; + else if (app.status === "FINAL_PASSED") counts.FINAL_PASSED += 1; + else if (app.status === "REJECTED") counts.REJECTED += 1; + }); + } + + return NextResponse.json({ + applicationStatusCounts: counts, + scheduleData: refinedSchedules, + }); + } catch (error) { + console.error("API Route Error:", error); + const message = + error instanceof Error ? error.message : "An unknown error occurred"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/hooks/useApplications.ts b/src/hooks/useApplications.ts index 14bd54c..59493b5 100644 --- a/src/hooks/useApplications.ts +++ b/src/hooks/useApplications.ts @@ -52,6 +52,7 @@ export const useUpdateApplication = () => { mutationFn: updateApplication, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["applications"] }); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); queryClient.invalidateQueries({ queryKey: ["schedule"] }); }, }); @@ -64,6 +65,7 @@ export const useDeleteApplication = () => { mutationFn: deleteApplication, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["applications"] }); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); }, }); }; diff --git a/src/hooks/useDashboard.ts b/src/hooks/useDashboard.ts new file mode 100644 index 0000000..1e0bd0a --- /dev/null +++ b/src/hooks/useDashboard.ts @@ -0,0 +1,10 @@ +import { fetchDashboard } from "@/lib/dashboard"; +import { useQuery } from "@tanstack/react-query"; + +export const useDashboard = () => { + return useQuery({ + queryKey: ["dashboard"], + queryFn: () => fetchDashboard(), + staleTime: 1000 * 60 * 60, + }); +}; diff --git a/src/hooks/useDocuments.ts b/src/hooks/useDocuments.ts index 2b25ee0..427e79a 100644 --- a/src/hooks/useDocuments.ts +++ b/src/hooks/useDocuments.ts @@ -34,6 +34,7 @@ export const useCreateDocument = () => { onSuccess: () => { // 성공 시 문서 목록 캐시 무효화 → 자동 갱신 queryClient.invalidateQueries({ queryKey: ["documents"] }); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); }, }); }; @@ -46,6 +47,7 @@ export const useUpdateDocument = () => { uploadDocument(id, fileInfo), onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: ["documents", variables.id] }); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); }, }); }; @@ -58,6 +60,7 @@ export const useDeleteDocument = () => { onSuccess: () => { // 삭제 후 목록 갱신 queryClient.invalidateQueries({ queryKey: ["documents"] }); + queryClient.invalidateQueries({ queryKey: ["dashboard"] }); }, }); }; diff --git a/src/lib/dashboard.ts b/src/lib/dashboard.ts new file mode 100644 index 0000000..e99fd53 --- /dev/null +++ b/src/lib/dashboard.ts @@ -0,0 +1,11 @@ +import { useAuthStore } from "@/store/auth/authStore"; +import axios from "axios"; + +export const fetchDashboard = async () => { + const token = useAuthStore.getState().token; + const res = await axios.get(`/api/dashboard`, { + withCredentials: true, + headers: { Authorization: `Bearer ${token}` }, + }); + return res.data; +};