diff --git a/src/app/(afterLogin)/_components/infoChangeScreen/infoChangeModal.tsx b/src/app/(afterLogin)/_components/infoChangeScreen/infoChangeModal.tsx index ffad936..5f5c146 100644 --- a/src/app/(afterLogin)/_components/infoChangeScreen/infoChangeModal.tsx +++ b/src/app/(afterLogin)/_components/infoChangeScreen/infoChangeModal.tsx @@ -1,14 +1,17 @@ +"use client"; + import CloseIcon from "@/assets/Close.svg"; import SelectSection from "./selectSection"; +import { useModalStore } from "@/store/modalStore"; -export default function InfoChangeModal({ - setIsModal, -}: { - isModal: boolean; - setIsModal: (value: boolean) => void; -}) { +export default function InfoChangeModal() { + const { isModal, setIsModal } = useModalStore(); return ( -
+
{ setIsModal(false); diff --git a/src/app/(afterLogin)/_components/sideNav.tsx b/src/app/(afterLogin)/_components/sideNav.tsx index 20604a5..90ea60b 100644 --- a/src/app/(afterLogin)/_components/sideNav.tsx +++ b/src/app/(afterLogin)/_components/sideNav.tsx @@ -7,12 +7,10 @@ import ClipboardText from "@/assets/ClipboardText.svg"; import House from "@/assets/House.svg"; import UserInfo from "./userInfo"; import { useSelectedLayoutSegments } from "next/navigation"; +import { useModalStore } from "@/store/modalStore"; -export default function SideNavigation({ - setIsModal, -}: { - setIsModal: (value: boolean) => void; -}) { +export default function SideNavigation() { + const { setIsModal } = useModalStore(); const segments = useSelectedLayoutSegments(); const pathname = segments[0]; diff --git a/src/app/(afterLogin)/dashboard/_components/applicationStatusSection.tsx b/src/app/(afterLogin)/dashboard/_components/applicationStatusSection.tsx new file mode 100644 index 0000000..6adce30 --- /dev/null +++ b/src/app/(afterLogin)/dashboard/_components/applicationStatusSection.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useFetchAllApplications } from "@/hooks/useApplications"; +import { useMemo } from "react"; +import { CompanyApplication } from "@/type/applicationType"; + +export default function ApplicationStatusSection() { + const { data: ApplicationsData, isLoading } = useFetchAllApplications(0, ""); + + const applicationStatus = useMemo(() => { + if (!ApplicationsData?.data.content) { + return { APPLIED: 0, DOCUMENT_PASSED: 0, FINAL_PASSED: 0, REJECTED: 0 }; + } + + const applications = ApplicationsData.data + .content as Array; + + return applications.reduce( + (counts, app) => { + if (counts.hasOwnProperty(app.status)) { + counts[app.status as keyof typeof counts]++; + } + return counts; + }, + { APPLIED: 0, DOCUMENT_PASSED: 0, FINAL_PASSED: 0, REJECTED: 0 } + ); + }, [ApplicationsData]); + + 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}개 + +
+
+ ); +} diff --git a/src/app/(afterLogin)/dashboard/_components/scheduleSection.tsx b/src/app/(afterLogin)/dashboard/_components/scheduleSection.tsx new file mode 100644 index 0000000..6c02c48 --- /dev/null +++ b/src/app/(afterLogin)/dashboard/_components/scheduleSection.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useSchedule } from "@/hooks/useSchedule"; +import { Schedule } from "@/type/applicationType"; +import CalendarIcon from "@/assets/CalendarCheck.svg"; +import { useMemo } from "react"; + +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}`; +} + +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 default function ScheduleSection() { + const { startDate, endDate } = useMemo(() => { + const now = new Date(); + const futureDate = new Date(now); + futureDate.setDate(now.getDate() + 7); + + return { + startDate: formatDateToApiString(now), + endDate: formatDateToApiString(futureDate), + }; + }, []); + + const { data: ScheduleData, isLoading: scheduleLoading } = useSchedule( + startDate, + endDate + ); + + return ( +
+

다가오는 일정

+ +
+ {ScheduleData?.data.content.map((schedule: Schedule) => ( +
+ + + {schedule.title} +
+ {getDDay(schedule.dateTime)} +
+
+
+ ))} +
+
+ ); +} diff --git a/src/app/(afterLogin)/dashboard/page.tsx b/src/app/(afterLogin)/dashboard/page.tsx index c59031d..327938d 100644 --- a/src/app/(afterLogin)/dashboard/page.tsx +++ b/src/app/(afterLogin)/dashboard/page.tsx @@ -1,172 +1,11 @@ -"use client"; - -import { useFetchAllApplications } from "@/hooks/useApplications"; -import { useEffect, useState } from "react"; -import { CompanyApplication, Schedule } from "@/type/applicationType"; -import { useSchedule } from "@/hooks/useSchedule"; -import CalendarIcon from "@/assets/CalendarCheck.svg"; -import LoadingSpinner from "@/app/_components/loadingSpinner"; +import ScheduleSection from "./_components/scheduleSection"; +import ApplicationStatusSection from "./_components/applicationStatusSection"; 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, - }); - - 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 ; - - 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?.data.content.map((schedule: Schedule) => ( -
- - - {schedule.title} -
- {getDDay(schedule.dateTime)} -
-
-
- ))} -
-
+ + ); } diff --git a/src/app/(afterLogin)/documents/_components/mainDocuments.tsx b/src/app/(afterLogin)/documents/_components/mainDocuments.tsx new file mode 100644 index 0000000..b41af0e --- /dev/null +++ b/src/app/(afterLogin)/documents/_components/mainDocuments.tsx @@ -0,0 +1,275 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { formatDate } from "date-fns"; +import { useCreateDocument, useDocuments } from "@/hooks/useDocuments"; +import VersionBadge from "./versionBadge"; +import CompanyNameBadge from "./companyNameBadge"; +import Divider from "./divider"; +import PlusIcon from "@/assets/Plus.svg"; +import LoadingSpinner from "@/app/_components/loadingSpinner"; +import { useRouter } from "next/navigation"; +import { useDocumentStore } from "@/store/documents/documentStore"; +import { DocumentTypeWithId } from "@/type/documentType"; +import UploadFileButton from "./uploadFileButton"; + +export default function MainDocuments() { + const [page, setPage] = useState(0); + const { data, error, isLoading } = useDocuments(page); + const documents = data?.data?.data.content ?? []; + const router = useRouter(); + + const [isAdding, setIsAdding] = useState(false); + const [newDocumentTitle, setNewDocumentTitle] = useState(""); + const inputRef = useRef(null); + + const [fileKey, setFileKey] = useState(null); + const [fileInfo, setFileInfo] = useState<{ + name: string; + type: string; + size: number; + } | null>(null); + + const { mutate, isPending } = useCreateDocument(); + + useEffect(() => { + if (isAdding) { + inputRef.current?.focus(); + } + }, [isAdding]); + + useEffect(() => { + if (!isLoading && documents.length === 0) { + setIsAdding(true); + } + }, [isLoading, documents]); + + const handleRoute = (doc: DocumentTypeWithId) => { + useDocumentStore.setState({ document: doc }); + router.push(`/documents/${doc.id}`); + }; + + const handleBlur = () => { + if (newDocumentTitle === "") { + if (documents.length > 0) { + setIsAdding(false); + setNewDocumentTitle(""); + } + } + }; + + const handleAddDocument = () => { + setIsAdding(true); + }; + + const handleSaveNewDocument = () => { + setIsAdding(false); + setNewDocumentTitle(""); + + mutate({ + title: newDocumentTitle, + fileName: fileInfo?.name || "", + fileKey: fileKey || "", + fileType: "RESUME", + fileSize: fileInfo?.size || 0, + }); + }; + + const handleCancelAdd = () => { + if (documents.length > 0) { + setIsAdding(false); + setNewDocumentTitle(""); + } + }; + + const th_style = "relative text-center p-4 font-medium"; + const td_stytle = "text-center p-4"; + return ( + <> + {isLoading && ( +
+ +
+ )} + {error && ( +
+ 에러가 발생하였습니다.
잠시 후 시도해주세요. +
+ )} + {data && !isLoading && ( + + + + + + + + + + + + + {documents.map((doc: DocumentTypeWithId) => { + return ( + + + + + + + + + + ); + })} + + {isAdding ? ( + + + + + + + + + ) : ( + + + + + + + + + )} + +
+ + + 문서명 + + Version + + 올리기 + + 회사명 + 최종수정날짜
+ + + {doc.latestVersion} + + + +
+ {doc.applicationForms.length === 0 ? ( + + 아직 연동된 회사가 없습니다. + + ) : ( + doc.applicationForms.map((history) => ( + + {history.companyName} + + )) + )} +
+
+ {formatDate(new Date(doc.lastModifiedDate), "yyyy.MM.dd")} +
+ setNewDocumentTitle(e.target.value)} + onBlur={handleBlur} + placeholder="새 문서명을 입력하세요" + className="w-full p-2 rounded-md text-center" + /> + + {1} + + + + + {documents.length > 0 && ( + + )} +
+ +
+ )} +
+ + + {Array.from({ length: data?.data.data.totalPages ?? 0 }).map( + (_, idx) => ( + + ) + )} + + +
+ + ); +} diff --git a/src/app/(afterLogin)/documents/page.tsx b/src/app/(afterLogin)/documents/page.tsx index 11a0ef4..7f93cce 100644 --- a/src/app/(afterLogin)/documents/page.tsx +++ b/src/app/(afterLogin)/documents/page.tsx @@ -1,18 +1,5 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import { formatDate } from "date-fns"; -import { useCreateDocument, useDocuments } from "@/hooks/useDocuments"; -import VersionBadge from "./_components/versionBadge"; -import CompanyNameBadge from "./_components/companyNameBadge"; -import Divider from "./_components/divider"; -import PlusIcon from "@/assets/Plus.svg"; -import LoadingSpinner from "@/app/_components/loadingSpinner"; import { Roboto } from "next/font/google"; -import { useRouter } from "next/navigation"; -import { useDocumentStore } from "@/store/documents/documentStore"; -import { DocumentTypeWithId } from "@/type/documentType"; -import UploadFileButton from "./_components/uploadFileButton"; +import MainDocuments from "./_components/mainDocuments"; const roboto = Roboto({ subsets: ["latin"], @@ -20,76 +7,6 @@ const roboto = Roboto({ }); export default function DocumentsPage() { - const [page, setPage] = useState(0); - const { data, error, isLoading } = useDocuments(page); - const documents = data?.data?.data.content ?? []; - const router = useRouter(); - - const [isAdding, setIsAdding] = useState(false); - const [newDocumentTitle, setNewDocumentTitle] = useState(""); - const inputRef = useRef(null); - - const [fileKey, setFileKey] = useState(null); - const [fileInfo, setFileInfo] = useState<{ - name: string; - type: string; - size: number; - } | null>(null); - - const { mutate, isPending } = useCreateDocument(); - - useEffect(() => { - if (isAdding) { - inputRef.current?.focus(); - } - }, [isAdding]); - - useEffect(() => { - if (!isLoading && documents.length === 0) { - setIsAdding(true); - } - }, [isLoading, documents]); - - const handleRoute = (doc: DocumentTypeWithId) => { - useDocumentStore.setState({ document: doc }); - router.push(`/documents/${doc.id}`); - }; - - const handleBlur = () => { - if (newDocumentTitle === "") { - if (documents.length > 0) { - setIsAdding(false); - setNewDocumentTitle(""); - } - } - }; - - const handleAddDocument = () => { - setIsAdding(true); - }; - - const handleSaveNewDocument = () => { - setIsAdding(false); - setNewDocumentTitle(""); - - mutate({ - title: newDocumentTitle, - fileName: fileInfo?.name || "", - fileKey: fileKey || "", - fileType: "RESUME", - fileSize: fileInfo?.size || 0, - }); - }; - - const handleCancelAdd = () => { - if (documents.length > 0) { - setIsAdding(false); - setNewDocumentTitle(""); - } - }; - - const th_style = "relative text-center p-4 font-medium"; - const td_stytle = "text-center p-4"; return ( <>

나의 문서 목록

- {isLoading && ( -
- -
- )} - {error && ( -
- 에러가 발생하였습니다.
잠시 후 시도해주세요. -
- )} - {data && !isLoading && ( - - - - - - - - - - - - - {documents.map((doc: DocumentTypeWithId) => { - return ( - - - - - - - - - - ); - })} - - {isAdding ? ( - - - - - - - - - ) : ( - - - - - - - - - )} - -
- - - 문서명 - - Version - - 올리기 - - 회사명 - 최종수정날짜
- - - {doc.latestVersion} - - - -
- {doc.applicationForms.length === 0 ? ( - - 아직 연동된 회사가 없습니다. - - ) : ( - doc.applicationForms.map((history) => ( - - {history.companyName} - - )) - )} -
-
- {formatDate(new Date(doc.lastModifiedDate), "yyyy.MM.dd")} -
- setNewDocumentTitle(e.target.value)} - onBlur={handleBlur} - placeholder="새 문서명을 입력하세요" - className="w-full p-2 rounded-md text-center" - /> - - {1} - - - - - {documents.length > 0 && ( - - )} -
- -
- )} - -
- - - {Array.from({ length: data?.data.data.totalPages ?? 0 }).map( - (_, idx) => ( - - ) - )} - - -
+ ); } diff --git a/src/app/(afterLogin)/layout.tsx b/src/app/(afterLogin)/layout.tsx index 9b20727..79c1e2b 100644 --- a/src/app/(afterLogin)/layout.tsx +++ b/src/app/(afterLogin)/layout.tsx @@ -1,6 +1,4 @@ -"use client"; - -import { useState, Suspense } from "react"; +import { Suspense } from "react"; import Header from "./_components/header"; import InfoChangeModal from "./_components/infoChangeScreen/infoChangeModal"; import PageTitle from "./_components/pageTitle"; @@ -12,17 +10,16 @@ export default function AfterLoginLayout({ }: { children: React.ReactNode; }) { - const [isModal, setIsModal] = useState(false); return ( <>
- - {isModal && ( - - )} + + + +
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 33968a2..c72f992 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,7 +7,10 @@ import Link from "next/link"; export default function Home() { return (
- + 시작하기
@@ -24,6 +27,7 @@ export default function Home() {
splash void; +} + +export const useModalStore = create()((set) => ({ + isModal: false, + setIsModal: (isModal: boolean) => set({ isModal }), +}));