From 4455309e5d0c8310b6a09997c4a7801e3b788bd0 Mon Sep 17 00:00:00 2001 From: strdeok Date: Sun, 7 Sep 2025 00:57:47 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20css=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AF=B8=EC=99=84=20=EB=B6=80=EB=B6=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(afterLogin)/_components/pageTitle.tsx | 4 +- src/app/(afterLogin)/applications/page.tsx | 194 ++++++++++++++---- .../_components/documentDescription.tsx | 71 +++---- src/app/(afterLogin)/documents/page.tsx | 65 +++--- src/app/_components/loadingSpinner.tsx | 2 +- src/app/join/_components/JoinForm.tsx | 61 +++--- .../login/_components/animatedCardStack.tsx | 160 +++++++++++++++ src/app/login/_components/loginForm.tsx | 6 +- src/app/login/page.tsx | 9 +- src/assets/PencilSimple.svg | 3 + src/assets/TrashSimple.svg | 3 + src/type/documentType.ts | 1 + 12 files changed, 434 insertions(+), 145 deletions(-) create mode 100644 src/app/login/_components/animatedCardStack.tsx create mode 100644 src/assets/PencilSimple.svg create mode 100644 src/assets/TrashSimple.svg diff --git a/src/app/(afterLogin)/_components/pageTitle.tsx b/src/app/(afterLogin)/_components/pageTitle.tsx index 89021b8..8eab46d 100644 --- a/src/app/(afterLogin)/_components/pageTitle.tsx +++ b/src/app/(afterLogin)/_components/pageTitle.tsx @@ -35,11 +35,11 @@ export default function PageTitle() { const message = () => { switch (currentSegment) { case "applications": - return "지원 현황"; + return "회사별 지원 진행 상황과 제출 문서를 정리해보세요"; case "documents": return "문서를 업로드하고, 버전별로 체계적으로 관리하세요"; case "schedule": - return "일정"; + return "지원일정을 한눈에 관리하세요"; default: return ""; } diff --git a/src/app/(afterLogin)/applications/page.tsx b/src/app/(afterLogin)/applications/page.tsx index 283facb..b8a5319 100644 --- a/src/app/(afterLogin)/applications/page.tsx +++ b/src/app/(afterLogin)/applications/page.tsx @@ -1,17 +1,20 @@ "use client"; import Link from "next/link"; -import { useState, useMemo } from "react"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; import { useFetchAllApplications, useUpdateApplication, + useDeleteApplication, } from "@/hooks/useApplications"; import Divider from "../documents/_components/divider"; import LoadingSpinner from "@/app/_components/loadingSpinner"; import SearchIcon from "@/assets/Search.svg"; import PlusIcon from "@/assets/Plus.svg"; import { CompanyApplicationWithId } from "@/type/applicationType"; -import DropDownButton from "./_components/dropDonwButton"; +import PencilSimpleIcon from "@/assets/PencilSimple.svg"; +import TrashSimpleIcon from "@/assets/TrashSimple.svg"; const formatDate = (dateString?: string | null) => { if (!dateString) return "-"; @@ -23,33 +26,88 @@ const formatDate = (dateString?: string | null) => { }; export default function ApplicationsPage() { + const router = useRouter(); const [page, setPage] = useState(0); const [searchQuery, setSearchQuery] = useState(""); + const [selectedIds, setSelectedIds] = useState([]); - const { data, isLoading, isError } = useFetchAllApplications( + const { data, isLoading, isError, refetch } = useFetchAllApplications( page, searchQuery ); - const { mutate } = useUpdateApplication(); - - const applications = data?.data.content; + const { mutate: updateMutate } = useUpdateApplication(); + const { mutateAsync: deleteMutateAsync } = useDeleteApplication(); + const applications = data?.data.content ?? []; const pageInfo = data?.data; + const filteredApplications = applications; + + useEffect(() => { + setSelectedIds([]); + }, [page, searchQuery]); - const filteredApplications = useMemo(() => { - if (!searchQuery) { - return applications; + const handleSelectAll = (e: React.ChangeEvent) => { + if (e.target.checked) { + const allIds = filteredApplications.map((app: CompanyApplicationWithId) => + app.id.toString() + ); + setSelectedIds(allIds); + } else { + setSelectedIds([]); } - return applications.filter((app: CompanyApplicationWithId) => - app.companyName.toLowerCase().includes(searchQuery.toLowerCase()) - ); - }, [searchQuery, applications]); + }; + const handleSelectOne = ( + e: React.ChangeEvent, + id: string + ) => { + if (e.target.checked) { + setSelectedIds((prev) => [...prev, id]); + } else { + setSelectedIds((prev) => prev.filter((selectedId) => selectedId !== id)); + } + }; + + const handleEdit = () => { + if (selectedIds.length !== 1) { + alert("수정할 항목을 하나만 선택해주세요."); + return; + } + router.push(`/applications/edit/${selectedIds[0]}`); + }; + + const handleDelete = async () => { + if (selectedIds.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + if ( + window.confirm( + `선택된 ${selectedIds.length}개의 항목을 삭제하시겠습니까?` + ) + ) { + const deletePromises = selectedIds.map((id) => + deleteMutateAsync(Number(id)) + ); + + try { + await Promise.all(deletePromises); + + alert("선택된 항목이 모두 삭제되었습니다."); + setSelectedIds([]); + refetch(); + } catch (error) { + alert("일부 항목 삭제에 실패했습니다. 페이지를 새로고침합니다."); + refetch(); + } + } + }; const handlePrevPage = () => setPage((prev) => Math.max(prev - 1, 0)); const handleNextPage = () => - setPage((prev) => Math.min(prev + 1, pageInfo.totalPages - 1)); + setPage((prev) => + pageInfo ? Math.min(prev + 1, pageInfo.totalPages - 1) : prev + ); - // 특정 페이지 클릭 const handlePageClick = (i: number) => setPage(i); const th_style = "relative text-center p-4 text-sm font-medium"; @@ -60,7 +118,7 @@ export default function ApplicationsPage() { status: string ) => { const changedApp = { ...app, status }; - mutate( + updateMutate( { applicationId: app.id, changedApplication: changedApp }, { onError: () => { @@ -80,7 +138,7 @@ export default function ApplicationsPage() { return ( <> -
+
setSearchQuery(e.target.value)} - onKeyDown={(e) => e.key === "Enter"} />
- - 지원내역 등록 - + +
+ + + + + +
{isLoading && ( @@ -119,6 +199,17 @@ export default function ApplicationsPage() { + @@ -126,10 +217,10 @@ export default function ApplicationsPage() { 지역 - + {filteredApplications.length > 0 ? ( filteredApplications.map((app: CompanyApplicationWithId) => { - const resume = app.documents?.find( - (doc) => doc.type === "RESUME" - ); - const portfolio = app.documents?.find( - (doc) => doc.type === "PORTFOLIO" - ); const deadline = app.schedules?.find( (s: { title: string }) => s.title === "마감일" @@ -157,24 +242,51 @@ export default function ApplicationsPage() { return ( + ); }) ) : ( -
+ 0 && + selectedIds.length === filteredApplications.length + } + /> + + 회사명 - 이력서 + 연동 문서 - 포트폴리오 + 직무 마감일 @@ -137,18 +228,12 @@ export default function ApplicationsPage() { 상태 지원 날짜
+ + handleSelectOne(e, app.id.toString()) + } + /> + {app.companyName} {app.companyAddress || "-"} - {resume ? resume.title : "-"} +
+ {app.documents && app.documents.length > 0 ? ( + app.documents.map((doc) => ( +
+ {doc.title} +
+ )) + ) : ( +
-
+ )} +
- {portfolio ? portfolio.title : "-"} + {app.position ? app.position : "-"} {formatDate(deadline)} @@ -193,14 +305,16 @@ export default function ApplicationsPage() { - + {app.schedules + ? formatDate(app.schedules[0]?.dateTime) + : "-"}
+ {searchQuery ? "검색 결과가 없습니다." : "등록된 지원내역이 없습니다."} @@ -224,7 +338,7 @@ export default function ApplicationsPage() { key={i} onClick={() => handlePageClick(i)} className={`px-3 py-1 border rounded-md ${ - page === i ? "bg-main text-white" : "" + page === i ? "bg-orange-500 text-white" : "" }`} > {i + 1} diff --git a/src/app/(afterLogin)/documents/[documentId]/_components/documentDescription.tsx b/src/app/(afterLogin)/documents/[documentId]/_components/documentDescription.tsx index a392120..f0c1bd8 100644 --- a/src/app/(afterLogin)/documents/[documentId]/_components/documentDescription.tsx +++ b/src/app/(afterLogin)/documents/[documentId]/_components/documentDescription.tsx @@ -135,36 +135,41 @@ export default function DocumentDescription({

{activeDocument.title}

- {companies?.map((company) => { - const statusText = getStatusText(company.status as ApplicationStatus); - return ( -
-
-
-
-

- {company.companyName} -

- - {statusText} - + {companies.length === 0 ? ( + + 아직 연동된 회사가 없습니다. + + ) : ( + companies?.map((company) => { + const statusText = getStatusText( + company.status as ApplicationStatus + ); + return ( +
+
+
+
+

+ {company.companyName} +

+ + {statusText} + +
+ 주소: {company.companyAddress}
- 주소: {company.companyAddress} -
- -
-
- ); - })} - + +
+ + ); + }) + )}
@@ -209,7 +214,7 @@ export default function DocumentDescription({
- {/* {new Date(documentVersion.lastModifiedDate).toLocaleDateString()} */} + {new Date(documentVersion.createdDate).toLocaleDateString()}
- - - - Upload -
); diff --git a/src/app/(afterLogin)/documents/page.tsx b/src/app/(afterLogin)/documents/page.tsx index 52583f0..6e4adfa 100644 --- a/src/app/(afterLogin)/documents/page.tsx +++ b/src/app/(afterLogin)/documents/page.tsx @@ -43,6 +43,12 @@ export default function DocumentsPage() { } }, [isAdding]); + useEffect(() => { + if (!isLoading && documents.length === 0) { + setIsAdding(true); + } + }, [isLoading, documents]); + const handleRoute = (doc: DocumentTypeWithId) => { useDocumentStore.setState({ document: doc }); router.push(`/documents/${doc.id}`); @@ -50,8 +56,10 @@ export default function DocumentsPage() { const handleBlur = () => { if (newDocumentTitle === "") { - setIsAdding(false); - setNewDocumentTitle(""); + if (documents.length > 0) { + setIsAdding(false); + setNewDocumentTitle(""); + } } }; @@ -73,8 +81,10 @@ export default function DocumentsPage() { }; const handleCancelAdd = () => { - setIsAdding(false); - setNewDocumentTitle(""); + if (documents.length > 0) { + setIsAdding(false); + setNewDocumentTitle(""); + } }; const th_style = "relative text-center p-4 font-medium"; @@ -96,7 +106,7 @@ export default function DocumentsPage() { 에러가 발생하였습니다.
잠시 후 시도해주세요. )} - {data && ( + {data && !isLoading && ( @@ -119,19 +129,10 @@ export default function DocumentsPage() { - {documents.map((doc: DocumentTypeWithId, index: number) => { + {documents.map((doc: DocumentTypeWithId) => { return ( - + + + ) : ( + + + + + + + )} diff --git a/src/app/_components/loadingSpinner.tsx b/src/app/_components/loadingSpinner.tsx index 6c9b36d..899d0ee 100644 --- a/src/app/_components/loadingSpinner.tsx +++ b/src/app/_components/loadingSpinner.tsx @@ -1,6 +1,6 @@ export default function LoadingSpinner() { return ( -
+
{/* 전체동의 */} -
+
-
{/* (필수) 서비스 이용약관 동의 */}
-
- - -
+ + +
- {index === documents.length - 1 && !isAdding && ( - - )} -
@@ -211,16 +212,34 @@ export default function DocumentsPage() { > 저장 + {documents.length > 0 && ( + + )} +
- {new Date(documentVersion.createdDate).toLocaleDateString()} + {new Date(documentVersion.createdDate!).toLocaleDateString()}