Skip to content
Merged

Dev #62

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
89 changes: 85 additions & 4 deletions src/app/(afterLogin)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <LoadingSpinner />;
if (isError) return <span className="text-red-500">오류가 발생하였습니다. 잠시 후 시도해주세요.</span>;

const box_style = "w-60 border rounded-sm px-9 py-5 flex flex-col gap-4";
return (
<>
<ApplicationStatusSection />
<ScheduleSection />
<section className="relative top-10 flex flex-row gap-6">
<div
className={`${box_style} ${
applicationStatus.APPLIED
? "border-[#FF9016] font-semibold"
: "border-[#E0E0E0] text-[#696F8C]"
}`}
>
<span className="w-full text-left">지원 회사</span>
<span className="w-full text-right">
{applicationStatus.APPLIED}개
</span>
</div>
<div
className={`${box_style} ${
applicationStatus.DOCUMENT_PASSED
? "border-[#FF9016] font-semibold"
: "border-[#E0E0E0] text-[#696F8C]"
}`}
>
<span className="w-full text-left">서류 합격</span>
<span className="w-full text-right">
{applicationStatus.DOCUMENT_PASSED}개
</span>
</div>
<div
className={`${box_style} ${
applicationStatus.REJECTED
? "border-[#FF9016] font-semibold"
: "border-[#E0E0E0] text-[#696F8C]"
}`}
>
<span className="w-full text-left">불합격</span>
<span className="w-full text-right">
{applicationStatus.REJECTED}개
</span>
</div>
<div
className={`${box_style} ${
applicationStatus.FINAL_PASSED
? "border-[#FF9016] font-semibold"
: "border-[#E0E0E0] text-[#696F8C]"
}`}
>
<span className="w-full text-left">최종 합격</span>
<span className="w-full text-right">
{applicationStatus.FINAL_PASSED}개
</span>
</div>
</section>

<section className="relative top-28 flex flex-col">
<h2 className="text-3xl font-semibold mb-6">다가오는 일정</h2>

<div className="flex flex-col gap-4">
{scheduleData?.map((schedule: Schedule & { dday: string }) => (
<div
key={schedule.id}
className="w-full border border-[#E7E7E7] rounded-lg px-5 py-3.5 text-xl font-medium"
>
<span className="flex flex-row gap-2 items-center">
<CalendarIcon />
{schedule.title}
<div className="text-xs text-main bg-[#FFF2E3] px-2 rounded-full">
{schedule.dday}
</div>
</span>
</div>
))}
</div>
</section>
</>
);
}
114 changes: 114 additions & 0 deletions src/app/api/dashboard/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
2 changes: 2 additions & 0 deletions src/hooks/useApplications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const useUpdateApplication = () => {
mutationFn: updateApplication,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["applications"] });
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
queryClient.invalidateQueries({ queryKey: ["schedule"] });
},
});
Expand All @@ -64,6 +65,7 @@ export const useDeleteApplication = () => {
mutationFn: deleteApplication,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["applications"] });
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
},
});
};
10 changes: 10 additions & 0 deletions src/hooks/useDashboard.ts
Original file line number Diff line number Diff line change
@@ -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,
});
};
3 changes: 3 additions & 0 deletions src/hooks/useDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const useCreateDocument = () => {
onSuccess: () => {
// 성공 시 문서 목록 캐시 무효화 → 자동 갱신
queryClient.invalidateQueries({ queryKey: ["documents"] });
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
},
});
};
Expand All @@ -46,6 +47,7 @@ export const useUpdateDocument = () => {
uploadDocument(id, fileInfo),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["documents", variables.id] });
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
},
});
};
Expand All @@ -58,6 +60,7 @@ export const useDeleteDocument = () => {
onSuccess: () => {
// 삭제 후 목록 갱신
queryClient.invalidateQueries({ queryKey: ["documents"] });
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
},
});
};
11 changes: 11 additions & 0 deletions src/lib/dashboard.ts
Original file line number Diff line number Diff line change
@@ -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;
};