From 576e6300aacf1655d73efbc96b937dd8f0ae86a1 Mon Sep 17 00:00:00 2001 From: strdeok Date: Wed, 22 Oct 2025 21:03:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20dashboard=20bff=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(afterLogin)/dashboard/page.tsx | 98 ++------------------ src/app/api/dashboard/route.ts | 114 ++++++++++++++++++++++++ src/hooks/useApplications.ts | 2 + src/hooks/useDashboard.ts | 10 +++ src/hooks/useDocuments.ts | 3 + src/lib/dashboard.ts | 11 +++ 6 files changed, 149 insertions(+), 89 deletions(-) create mode 100644 src/app/api/dashboard/route.ts create mode 100644 src/hooks/useDashboard.ts create mode 100644 src/lib/dashboard.ts diff --git a/src/app/(afterLogin)/dashboard/page.tsx b/src/app/(afterLogin)/dashboard/page.tsx index c59031d..5fb60e5 100644 --- a/src/app/(afterLogin)/dashboard/page.tsx +++ b/src/app/(afterLogin)/dashboard/page.tsx @@ -1,97 +1,17 @@ "use client"; -import { useFetchAllApplications } from "@/hooks/useApplications"; -import { useEffect, useState } from "react"; -import { CompanyApplication, Schedule } from "@/type/applicationType"; -import { useSchedule } from "@/hooks/useSchedule"; +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 [applicationStatus, setApplicationStatus] = useState<{ - APPLIED: number; - DOCUMENT_PASSED: number; - FINAL_PASSED: number; - REJECTED: number; - }>({ - APPLIED: 0, - DOCUMENT_PASSED: 0, - FINAL_PASSED: 0, - REJECTED: 0, - }); + const { data, isLoading, isError } = useDashboard(); + const applicationStatus = data?.applicationStatusCounts; + const scheduleData = data?.scheduleData; - function formatDateToApiString(date: Date) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes()).padStart(2, "0"); - const seconds = String(date.getSeconds()).padStart(2, "0"); - return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; - } - - const startDate = formatDateToApiString(new Date()); - const endDate = formatDateToApiString( - new Date(new Date().setDate(new Date().getDate() + 7)) - ); - - 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)}`; - } - - const { data, isLoading } = useFetchAllApplications(0, ""); - const { data: scheduleData, isLoading: scheduleLoading } = useSchedule( - startDate, - endDate - ); - - const applications = data?.data.content as Array; - - const statusCount = () => { - if (!applications) return; - - const counts = { - APPLIED: 0, - DOCUMENT_PASSED: 0, - FINAL_PASSED: 0, - REJECTED: 0, - }; - - applications.forEach((app) => { - 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; - }); - - setApplicationStatus(counts); - }; - - useEffect(() => { - statusCount(); - }, [applications]); - - if (isLoading || scheduleLoading) return ; + if (isLoading) return ; + if (isError) return 오류가 발생하였습니다. 잠시 후 시도해주세요.; const box_style = "w-60 border rounded-sm px-9 py-5 flex flex-col gap-4"; return ( @@ -151,7 +71,7 @@ export default function DashboardPage() {

다가오는 일정

- {scheduleData?.data.content.map((schedule: Schedule) => ( + {scheduleData?.map((schedule: Schedule & { dday: string }) => (
{schedule.title}
- {getDDay(schedule.dateTime)} + {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 7ec3dd1..73cdb95 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"] }); }, }); }; @@ -63,6 +64,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; +};