diff --git a/.pnp.cjs b/.pnp.cjs index 213a580..ee1c144 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -30,7 +30,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["@next/font", "npm:13.5.2"],\ ["@tanstack/react-query", "virtual:ddccc941eb8b35cd4b898a64351d8bba4ecc85eb47e8f1b36dce7852d6c3635665e0fc5464861f723d175edb2248ce0fa54dfefb9b5e4d2fdaef4b2353c4aa82#npm:4.35.0"],\ - ["@team-return/design-system", "npm:1.1.8"],\ + ["@team-return/design-system", "npm:1.1.15"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@types/debug", "npm:4.1.9"],\ ["@types/eslint", "npm:8.44.3"],\ @@ -3178,14 +3178,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@team-return/design-system", [\ - ["npm:1.1.8", {\ - "packageLocation": "./.yarn/cache/@team-return-design-system-npm-1.1.8-8d1ab3ee26-d87866fa3a.zip/node_modules/@team-return/design-system/",\ + ["npm:1.1.15", {\ + "packageLocation": "./.yarn/cache/@team-return-design-system-npm-1.1.15-2a958b8446-de878fb3ac.zip/node_modules/@team-return/design-system/",\ "packageDependencies": [\ - ["@team-return/design-system", "npm:1.1.8"],\ + ["@team-return/design-system", "npm:1.1.15"],\ ["react", "npm:18.2.0"],\ - ["react-dom", "virtual:8d1ab3ee26f8c1a1d32b5ffe6ba6ba7c51738c535392bf31b823fcd1d816d89de4f1f786058da43264ae3129cc024b8728b7f1c4d1283ab36f85f5214a8999a5#npm:18.2.0"],\ + ["react-dom", "virtual:2a958b8446798cc43c7b13945ac95e5c1f15acea181b4b59b4352f85721fc5a9e112f1859bcfe6685b1f312192606bdcff918718df11e29e8a91c0329ede203b#npm:18.2.0"],\ ["uuid", "npm:9.0.1"],\ - ["zustand", "virtual:8d1ab3ee26f8c1a1d32b5ffe6ba6ba7c51738c535392bf31b823fcd1d816d89de4f1f786058da43264ae3129cc024b8728b7f1c4d1283ab36f85f5214a8999a5#npm:4.4.2"]\ + ["zustand", "virtual:2a958b8446798cc43c7b13945ac95e5c1f15acea181b4b59b4352f85721fc5a9e112f1859bcfe6685b1f312192606bdcff918718df11e29e8a91c0329ede203b#npm:4.4.2"]\ ],\ "linkType": "HARD"\ }]\ @@ -7225,7 +7225,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["my-app", "workspace:."],\ ["@next/font", "npm:13.5.2"],\ ["@tanstack/react-query", "virtual:ddccc941eb8b35cd4b898a64351d8bba4ecc85eb47e8f1b36dce7852d6c3635665e0fc5464861f723d175edb2248ce0fa54dfefb9b5e4d2fdaef4b2353c4aa82#npm:4.35.0"],\ - ["@team-return/design-system", "npm:1.1.8"],\ + ["@team-return/design-system", "npm:1.1.15"],\ ["@testing-library/jest-dom", "npm:5.17.0"],\ ["@types/debug", "npm:4.1.9"],\ ["@types/eslint", "npm:8.44.3"],\ @@ -8018,10 +8018,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ - ["virtual:8d1ab3ee26f8c1a1d32b5ffe6ba6ba7c51738c535392bf31b823fcd1d816d89de4f1f786058da43264ae3129cc024b8728b7f1c4d1283ab36f85f5214a8999a5#npm:18.2.0", {\ - "packageLocation": "./.yarn/__virtual__/react-dom-virtual-ef3a66c86e/0/cache/react-dom-npm-18.2.0-dd675bca1c-7d323310be.zip/node_modules/react-dom/",\ + ["virtual:2a958b8446798cc43c7b13945ac95e5c1f15acea181b4b59b4352f85721fc5a9e112f1859bcfe6685b1f312192606bdcff918718df11e29e8a91c0329ede203b#npm:18.2.0", {\ + "packageLocation": "./.yarn/__virtual__/react-dom-virtual-c8bcd2ef63/0/cache/react-dom-npm-18.2.0-dd675bca1c-7d323310be.zip/node_modules/react-dom/",\ "packageDependencies": [\ - ["react-dom", "virtual:8d1ab3ee26f8c1a1d32b5ffe6ba6ba7c51738c535392bf31b823fcd1d816d89de4f1f786058da43264ae3129cc024b8728b7f1c4d1283ab36f85f5214a8999a5#npm:18.2.0"],\ + ["react-dom", "virtual:2a958b8446798cc43c7b13945ac95e5c1f15acea181b4b59b4352f85721fc5a9e112f1859bcfe6685b1f312192606bdcff918718df11e29e8a91c0329ede203b#npm:18.2.0"],\ ["@types/react", null],\ ["loose-envify", "npm:1.4.0"],\ ["react", "npm:18.2.0"],\ @@ -9295,10 +9295,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["virtual:476cc08bb7fa24aa75189dbaa5ab2e156378b108b1129c8ce8230e6bebbf8da544ae85437a7b24bf59fffd9c8ae93d50fdd53c05317a9843ee5dffe4540333fd#npm:1.2.0", {\ - "packageLocation": "./.yarn/__virtual__/use-sync-external-store-virtual-93960c39bb/0/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip/node_modules/use-sync-external-store/",\ + ["virtual:5169122044150f707a1718f0152074815860ada1fb07c2fabb1f97098510f4c35243a5dd4c65498366f2928ba2174c4a4dfb6ce9be87a372840fb59f665a30a9#npm:1.2.0", {\ + "packageLocation": "./.yarn/__virtual__/use-sync-external-store-virtual-da94c0b4fe/0/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip/node_modules/use-sync-external-store/",\ "packageDependencies": [\ - ["use-sync-external-store", "virtual:476cc08bb7fa24aa75189dbaa5ab2e156378b108b1129c8ce8230e6bebbf8da544ae85437a7b24bf59fffd9c8ae93d50fdd53c05317a9843ee5dffe4540333fd#npm:1.2.0"],\ + ["use-sync-external-store", "virtual:5169122044150f707a1718f0152074815860ada1fb07c2fabb1f97098510f4c35243a5dd4c65498366f2928ba2174c4a4dfb6ce9be87a372840fb59f665a30a9#npm:1.2.0"],\ ["@types/react", null],\ ["react", "npm:18.2.0"]\ ],\ @@ -9511,15 +9511,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ - ["virtual:8d1ab3ee26f8c1a1d32b5ffe6ba6ba7c51738c535392bf31b823fcd1d816d89de4f1f786058da43264ae3129cc024b8728b7f1c4d1283ab36f85f5214a8999a5#npm:4.4.2", {\ - "packageLocation": "./.yarn/__virtual__/zustand-virtual-476cc08bb7/0/cache/zustand-npm-4.4.2-3931468256-ca10785e14.zip/node_modules/zustand/",\ + ["virtual:2a958b8446798cc43c7b13945ac95e5c1f15acea181b4b59b4352f85721fc5a9e112f1859bcfe6685b1f312192606bdcff918718df11e29e8a91c0329ede203b#npm:4.4.2", {\ + "packageLocation": "./.yarn/__virtual__/zustand-virtual-5169122044/0/cache/zustand-npm-4.4.2-3931468256-ca10785e14.zip/node_modules/zustand/",\ "packageDependencies": [\ - ["zustand", "virtual:8d1ab3ee26f8c1a1d32b5ffe6ba6ba7c51738c535392bf31b823fcd1d816d89de4f1f786058da43264ae3129cc024b8728b7f1c4d1283ab36f85f5214a8999a5#npm:4.4.2"],\ + ["zustand", "virtual:2a958b8446798cc43c7b13945ac95e5c1f15acea181b4b59b4352f85721fc5a9e112f1859bcfe6685b1f312192606bdcff918718df11e29e8a91c0329ede203b#npm:4.4.2"],\ ["@types/immer", null],\ ["@types/react", null],\ ["immer", null],\ ["react", "npm:18.2.0"],\ - ["use-sync-external-store", "virtual:476cc08bb7fa24aa75189dbaa5ab2e156378b108b1129c8ce8230e6bebbf8da544ae85437a7b24bf59fffd9c8ae93d50fdd53c05317a9843ee5dffe4540333fd#npm:1.2.0"]\ + ["use-sync-external-store", "virtual:5169122044150f707a1718f0152074815860ada1fb07c2fabb1f97098510f4c35243a5dd4c65498366f2928ba2174c4a4dfb6ce9be87a372840fb59f665a30a9#npm:1.2.0"]\ ],\ "packagePeers": [\ "@types/immer",\ diff --git a/package.json b/package.json index 4a45a4f..1380f24 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "@next/font": "^13.5.2", "@tanstack/react-query": "^4.33.0", - "@team-return/design-system": "^1.1.8", + "@team-return/design-system": "^1.1.15", "axios": "^1.4.0", "debug": "^4.3.4", "next": "13.4.7", diff --git a/public/BannerBackground.png b/public/BannerBackground.webp similarity index 100% rename from public/BannerBackground.png rename to public/BannerBackground.webp diff --git a/public/Clip.svg b/public/Clip.svg new file mode 100644 index 0000000..9d1d141 --- /dev/null +++ b/public/Clip.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/Docs.svg b/public/Docs.svg new file mode 100644 index 0000000..fe01f7f --- /dev/null +++ b/public/Docs.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/Loading.png b/public/Loading.png new file mode 100644 index 0000000..9205574 Binary files /dev/null and b/public/Loading.png differ diff --git a/public/PopularCompanyBanner.webp b/public/PopularCompanyBanner.webp new file mode 100644 index 0000000..e2d765a Binary files /dev/null and b/public/PopularCompanyBanner.webp differ diff --git a/public/WinterIntrenBanner.webp b/public/WinterIntrenBanner.webp new file mode 100644 index 0000000..57854f3 Binary files /dev/null and b/public/WinterIntrenBanner.webp differ diff --git a/public/test-banner-1.png b/public/test-banner-1.png deleted file mode 100644 index 82b3937..0000000 Binary files a/public/test-banner-1.png and /dev/null differ diff --git a/src/apis/applications/index.ts b/src/apis/applications/index.ts new file mode 100644 index 0000000..8031962 --- /dev/null +++ b/src/apis/applications/index.ts @@ -0,0 +1,63 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useToastStore } from "@team-return/design-system"; +import { AxiosError } from "axios"; +import { useRouter } from "next/navigation"; +import { instance } from "../axios"; +import { ApplicationsResponseType, ApplyRequestItmeType } from "./type"; + +const router = "/applications"; + +export default function useApplyToCompany(recruitmentId: string) { + const navigator = useRouter(); + const { append } = useToastStore(); + return useMutation( + async function (body: ApplyRequestItmeType[]) { + return await instance.post(`${router}/${recruitmentId}`, { + attachments: body, + }); + }, + { + onSuccess: () => { + navigator.push("/mypage"); + }, + onError: (error: AxiosError) => { + switch (error.response?.status) { + case 401: + append({ + title: "", + message: "3학년이 아닌 학생은 지원할 수 없습니다.", + type: "RED", + }); + break; + case 404: + append({ + title: "", + message: "모집의뢰서가 존재하지 않습니다.", + type: "RED", + }); + break; + case 409: + append({ + title: "", + message: + "이미 해당 모집의뢰에 지원했거나 승인된 지원요청이 존재합니다.", + type: "RED", + }); + + default: + break; + } + }, + } + ); +} + + +export function useGetApplications() { + return useQuery(["GetApplications"], async () => { + const { data } = await instance.get( + `${router}/students` + ); + return data; + }); +} diff --git a/src/apis/applications/type.ts b/src/apis/applications/type.ts new file mode 100644 index 0000000..9f37c1d --- /dev/null +++ b/src/apis/applications/type.ts @@ -0,0 +1,29 @@ +export type AttachmentsType = "FILE" | "URL"; + +export interface ApplyRequestItmeType { + url: string; + type: AttachmentsType; +} + +export type ApplicationsStatusType = + | "REQUESTED" + | "APPROVED" + | "FAILED" + | "PASS" + | "REJECTED"; + +export interface ApplicationItemType { + application_id: number; + company: string; + attachments: ApplyRequestItmeType[]; + application_status: ApplicationsStatusType; +} + +export interface ApplicationsResponseType { + applications: ApplicationItemType[]; + count: number +} + +export interface ApplyRequestBody { + attachments: ApplyRequestItmeType[]; +} \ No newline at end of file diff --git a/src/apis/auth/index.ts b/src/apis/auth/index.ts index b6d7433..66616e3 100644 --- a/src/apis/auth/index.ts +++ b/src/apis/auth/index.ts @@ -5,7 +5,7 @@ import { AuthCode, IAuthorizationResponse, SendAuthCodeType } from "./type"; const router = "/auth"; -export const ReissueToken = async (refresh_token: string) => { +export const useReissueToken = async (refresh_token: string) => { const response = await axios.put( `${process.env.NEXT_PUBLIC_BASE_URL}${router}/reissue`, null, @@ -18,7 +18,7 @@ export const ReissueToken = async (refresh_token: string) => { return response.data; }; -export const SendAuthCode = () => { +export const useSendAuthCode = () => { const { append } = useToastStore(); return useMutation( @@ -67,7 +67,7 @@ export const SendAuthCode = () => { ); }; -export const CheckAuthCode = ( +export const useCheckAuthCode = ( query_string: AuthCode, options?: Omit< UseMutationOptions, void, unknown>, diff --git a/src/apis/axios.ts b/src/apis/axios.ts index 171fda3..5fcee55 100644 --- a/src/apis/axios.ts +++ b/src/apis/axios.ts @@ -1,6 +1,6 @@ import axios, { AxiosError } from "axios"; import { Cookies } from "react-cookie"; -import { ReissueToken } from "./auth"; +import { useReissueToken } from "./auth"; export const instance = axios.create({ baseURL: process.env.NEXT_PUBLIC_BASE_URL, @@ -28,6 +28,7 @@ instance.interceptors.response.use( if (axios.isAxiosError(error) && error.response) { const { config } = error; const refreshToken = cookies.get("refresh_token"); + if ( (error.response.data.message === "Invalid Token" || error.response.data.message === "Token Expired" || @@ -37,7 +38,7 @@ instance.interceptors.response.use( if (!isRefreshing) { cookies.remove("access_token"); isRefreshing = true; - ReissueToken(refreshToken) + useReissueToken(refreshToken) .then((res) => { isRefreshing = false; cookies.remove("refresh_token"); @@ -61,7 +62,7 @@ instance.interceptors.response.use( window.location.href = "/account/login"; }); } - } else { + } else if (error.response.status === 403) { window.location.href = "/account/login"; } } diff --git a/src/apis/bookmarks/index.ts b/src/apis/bookmarks/index.ts new file mode 100644 index 0000000..62f3b85 --- /dev/null +++ b/src/apis/bookmarks/index.ts @@ -0,0 +1,26 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { instance } from "../axios"; +import { BookmarkResponseType } from "./type"; + +const router = "/bookmarks"; + +export const useGetBookmarks = () => { + return useQuery(["GetBookmarks"], async () => { + const { data } = await instance.get(`${router}`); + return data; + }); +}; + +export const useSetBookmarks = () => { + const queryClient = useQueryClient(); + return useMutation( + async (recruitmentId: number) => { + await instance.patch(`${router}/${recruitmentId}`); + }, + { + onSuccess: () => { + queryClient.invalidateQueries(["getRecruitmentsList", "GetBookmarks"]); + }, + } + ); +}; diff --git a/src/apis/bookmarks/type.ts b/src/apis/bookmarks/type.ts new file mode 100644 index 0000000..3841856 --- /dev/null +++ b/src/apis/bookmarks/type.ts @@ -0,0 +1,9 @@ +export interface BookmarkResponseType { + bookmarks: BookmarkItemsType[]; +} + +export interface BookmarkItemsType { + company_name: string; + recruitment_id: number; + created_at: string; +} diff --git a/src/apis/code/index.ts b/src/apis/code/index.ts index e53ce0f..a82299c 100644 --- a/src/apis/code/index.ts +++ b/src/apis/code/index.ts @@ -1,21 +1,20 @@ -import { CodeType } from "./type"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; +import { CodeType, GetCodeType } from "./type"; const router = "/codes"; -export const GetCode = ( +export const useGetCode = ( type: CodeType, keyword?: string, parent_code?: number ) => { - return useQuery( - ["GetCode", type, keyword, parent_code], - async () => - await axios.get( - `${process.env.NEXT_PUBLIC_BASE_URL}${router}?type=${type}&keyword=${ - keyword || "" - }&parent_code=${parent_code || ""}` - ) - ); + return useQuery(["GetCode", type, keyword, parent_code], async () => { + const { data } = await axios.get( + `${process.env.NEXT_PUBLIC_BASE_URL}${router}?type=${type}&keyword=${ + keyword || "" + }&parent_code=${parent_code || ""}` + ); + return data; + }); }; diff --git a/src/apis/code/type.ts b/src/apis/code/type.ts index 8310bb4..2c3de1d 100644 --- a/src/apis/code/type.ts +++ b/src/apis/code/type.ts @@ -1,10 +1,11 @@ export interface GetCodeType { - codes: CodeType[]; + codes: ResponesType[]; } export interface ResponesType { code: number; keyword: string; + job_type: string; } export type CodeType = "JOB" | "TECH" | "BUSINESS_AREA"; diff --git a/src/apis/companies/index.ts b/src/apis/companies/index.ts index f7060fb..293c0d1 100644 --- a/src/apis/companies/index.ts +++ b/src/apis/companies/index.ts @@ -6,14 +6,15 @@ import { CompaniesDetailsType, CompaniesListResponseType } from "./type"; const router = "/companies"; -export const GetCompaniesList = (queryString: string) => { +export const useGetCompaniesList = (queryString: string) => { const { append } = useToastStore(); return useQuery( ["companiesList", queryString], async () => { - return await instance.get( + const { data } = await instance.get( `${router}/student?${queryString}` ); + return data; }, { onError: () => { @@ -27,7 +28,7 @@ export const GetCompaniesList = (queryString: string) => { ); }; -export const GetCompaniesDetail = (id: string) => { +export const useGetCompaniesDetail = (id: string) => { const { append } = useToastStore(); return useQuery( ["companiesDetails"], @@ -45,7 +46,7 @@ export const GetCompaniesDetail = (id: string) => { ); }; -export const GetNumberOfCompaniesListPages = (queryString: string) => { +export const useGetNumberOfCompaniesListPages = (queryString: string) => { const { data } = useQuery( ["getNumberOfCompaniesListPages", queryString], async () => diff --git a/src/apis/file/index.ts b/src/apis/file/index.ts new file mode 100644 index 0000000..b212c00 --- /dev/null +++ b/src/apis/file/index.ts @@ -0,0 +1,34 @@ +import { useMutation } from "@tanstack/react-query"; +import { useToastStore } from "@team-return/design-system"; +import { AxiosError } from "axios"; +import { instance } from "../axios"; +import { UploadFileResponse } from "./type"; + +const router = "/files"; + +export const useFileUpload = () => { + const { append } = useToastStore(); + return useMutation( + async (files: File[]) => { + const formData = new FormData(); + files.map((file) => { + formData.append("file", file); + }); + const { data }: { data: UploadFileResponse } = await instance.post( + `${router}?type=EXTENSION_FILE`, + formData + ); + return data; + }, + { + onError: (error: AxiosError) => { + if (error.response) + append({ + title: "", + message: "파일 업로드에 실패했습니다.", + type: "RED", + }); + }, + } + ); +}; diff --git a/src/apis/file/type.ts b/src/apis/file/type.ts new file mode 100644 index 0000000..2000fe8 --- /dev/null +++ b/src/apis/file/type.ts @@ -0,0 +1,3 @@ +export interface UploadFileResponse { + urls: string[]; +} diff --git a/src/apis/recruitments/index.ts b/src/apis/recruitments/index.ts index 32fd095..feaf54b 100644 --- a/src/apis/recruitments/index.ts +++ b/src/apis/recruitments/index.ts @@ -4,19 +4,21 @@ import { instance } from "../axios"; import { GetNumberOfPagesType, RecruitmentsDetailType, - RecruitmentsListResponseType, + RecruitmentsListResponseType } from "./type"; const router = "/recruitments"; -export const GetRecruitmentsList = (queryString: string) => { +export const useGetRecruitmentsList = (queryString: string) => { const { append } = useToastStore(); return useQuery( ["getRecruitmentsList", queryString], - async () => - await instance.get( + async () => { + const { data } = await instance.get( `${router}/student?${queryString}` - ), + ); + return data; + }, { onError: () => { append({ @@ -29,11 +31,16 @@ export const GetRecruitmentsList = (queryString: string) => { ); }; -export const GetRecruitmentsDetail = (id: string) => { +export const useGetRecruitmentsDetail = (id: string) => { const { append } = useToastStore(); return useQuery( ["getRecruitmentsDetail", id], - async () => await instance.get(`${router}/${id}`), + async () => { + const { data } = await instance.get( + `${router}/${id}` + ); + return data; + }, { refetchOnWindowFocus: false, onError: () => { @@ -47,7 +54,7 @@ export const GetRecruitmentsDetail = (id: string) => { ); }; -export const GetNumberOfRecruitmentRequestListPages = (queryString: string) => { +export const useGetNumberOfRecruitmentRequestListPages = (queryString: string) => { const { data } = useQuery( ["getNumberOfRecruitmentRequestListPages", queryString], async () => diff --git a/src/apis/recruitments/type.ts b/src/apis/recruitments/type.ts index 6346279..c17c995 100644 --- a/src/apis/recruitments/type.ts +++ b/src/apis/recruitments/type.ts @@ -1,12 +1,12 @@ export interface RecruitmentsListResponseType { - recruitments: RecruitmentsListType[]; + recruitments: RecruitmentsListType[]; } export interface RecruitmentsListType { recruit_id: number; company_name: string; company_profile_url: string; - train_pay: string; + train_pay: number; military: boolean; total_hiring: number; job_code_list: string; diff --git a/src/apis/reviews/index.ts b/src/apis/reviews/index.ts index 21187dc..6635fdd 100644 --- a/src/apis/reviews/index.ts +++ b/src/apis/reviews/index.ts @@ -2,12 +2,12 @@ import { useQuery } from "@tanstack/react-query"; import { instance } from "../axios"; import { getReviewDetailResponseProps, - getReviewListResponseProps, + getReviewListResponseProps } from "./type"; const router = "/reviews"; -export const GetReviewList = (companiesId: string) => { +export const useGetReviewList = (companiesId: string) => { return useQuery(["getReviewList", companiesId], async () => { const { data } = await instance.get( `${router}/${companiesId}` @@ -16,7 +16,7 @@ export const GetReviewList = (companiesId: string) => { }); }; -export const getReviewDetails = (reviewId: string) => { +export const useGetReviewDetails = (reviewId: string) => { return useQuery(["getReviewDetails", reviewId], async () => { const { data } = await instance.get( `${router}/details/${reviewId}` diff --git a/src/apis/students/index.ts b/src/apis/students/index.ts index ddf9e86..a31d97e 100644 --- a/src/apis/students/index.ts +++ b/src/apis/students/index.ts @@ -9,14 +9,17 @@ import { MyProfileProps, RequestBody } from "./type"; const router = "/students"; -export const Signup = () => { +export const useSignup = () => { const [, setCookie] = useCookies(); const navigator = useRouter(); const { append } = useToastStore(); return useMutation( async (body: RequestBody) => { - const response = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}${router}`, body); + const response = await axios.post( + `${process.env.NEXT_PUBLIC_BASE_URL}${router}`, + body + ); return response.data; }, { @@ -69,10 +72,13 @@ export const Signup = () => { ); }; -export const MyProfile = () => { +export const useMyProfile = () => { return useQuery( ["myProfile"], - async () => await instance.get(`${router}/my`), + async () => { + const { data } = await instance.get(`${router}/my`); + return data; + }, { refetchOnWindowFocus: false, } diff --git a/src/apis/students/type.ts b/src/apis/students/type.ts index 3c11875..eae66df 100644 --- a/src/apis/students/type.ts +++ b/src/apis/students/type.ts @@ -11,6 +11,10 @@ export interface RequestBody { export interface MyProfileProps { student_name: string; student_gcn: string; - department: string; // 소개과 | 임베과 | 정보보안 | 인공지능 + department: + | "SOFTWARE_DEVELOP" + | "EMBEDDED_SOFTWARE" + | "INFORMATION_SECURITY" + | "AI_SOFTWARE"; profile_image_url: string; } diff --git a/src/apis/user/index.ts b/src/apis/user/index.ts index 405f891..3342bd0 100644 --- a/src/apis/user/index.ts +++ b/src/apis/user/index.ts @@ -1,13 +1,13 @@ -import { useToastStore, Toast } from "@team-return/design-system"; import { useMutation } from "@tanstack/react-query"; +import { useToastStore } from "@team-return/design-system"; +import axios, { AxiosError } from "axios"; import { useRouter } from "next/navigation"; import { useCookies } from "react-cookie"; import { RequestBody, ResponseBody } from "./type"; -import axios, { AxiosError } from "axios"; const router = "/users"; -export const Login = (body: RequestBody, checkBoxValue: boolean) => { +export const useLogin = (body: RequestBody, checkBoxValue: boolean) => { const [, setCookie, removeCookie] = useCookies(); const navigator = useRouter(); const { append } = useToastStore(); diff --git a/src/app/companies/detail/page.tsx b/src/app/companies/detail/page.tsx index 28f8e0a..df55cd4 100644 --- a/src/app/companies/detail/page.tsx +++ b/src/app/companies/detail/page.tsx @@ -4,7 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useToastStore } from "@team-return/design-system"; import CompanyTable from "@/components/company/CompanyTable"; import CompanyTitle from "@/components/company/CompanyTitle"; -import { GetCompaniesDetail } from "@/apis/companies"; +import { useGetCompaniesDetail } from "@/apis/companies"; import { business_number_regex } from "@/util/regex"; export default function CompanyDetailPage() { @@ -12,7 +12,7 @@ export default function CompanyDetailPage() { const params = useSearchParams(); const { append } = useToastStore(); - const { data } = GetCompaniesDetail(params.get("id")!); + const { data } = useGetCompaniesDetail(params.get("id")!); if (data) { const { diff --git a/src/app/companies/page.tsx b/src/app/companies/page.tsx index 04f92e3..06b81bf 100644 --- a/src/app/companies/page.tsx +++ b/src/app/companies/page.tsx @@ -1,31 +1,27 @@ "use client"; +import Pagination from "@/components/common/Pagination"; import TextFiled from "@/components/common/TextFiled"; +import CompanyCard from "@/components/company/CompanyCard"; import useForm from "@/hook/useForm"; -import { useEffect, useState } from "react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import CompanyCard from "@/components/CompanyCard"; -import Pagination from "@/components/common/Pagination"; +import { CompaniesQueryType } from "@/hook/useQueryString/type"; +import { useQueryString } from "@/hook/useQueryString/useQueryString"; export default function CompanyListPage() { - const getParams = useSearchParams(); - const [page, setPage] = useState(Number(getParams.get("page"))); - const navigator = useRouter(); - const pathname = usePathname(); + const { getQueryString, setQueryString } = useQueryString( + { + page: "1", + name: "", + } + ); const { state: searchState, onChange: onChangeSearch } = useForm<{ search: string | undefined; }>({ - search: getParams.get("name")?.toString(), + search: getQueryString("name"), }); - const onSearch = () => { - navigator.push(`${pathname}?page=${page}&name=${searchState.search}`); - }; - - useEffect(onSearch, [page]); - return ( -
+

🏢 기업체

@@ -37,7 +33,9 @@ export default function CompanyListPage() { onChange={onChangeSearch} name="search" customType="Search" - enterEvent={onSearch} + enterEvent={() => { + setQueryString({ name: searchState.search }); + }} />
diff --git a/src/app/globals.css b/src/app/globals.css index a56ff80..a901920 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,6 +2,24 @@ @tailwind components; @tailwind utilities; +@layer components { + .tagStyle { + @apply text-caption leading-caption text-lightBlue font-r border rounded-full border-[#135C9D] py-1 px-2; + } + .skeltonUi { + @apply bg-[#f7f7f7] relative; + } + .drag { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + } + .dropDownMenu { + @apply flex-1 text-b3 leading-b3 font-m text-[#7f7f7f] flex justify-start items-center px-[10px] cursor-pointer hover:text-[#333333]; + } +} + * { margin: 0; padding: 0; @@ -42,12 +60,6 @@ input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; } -.drag { - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} table { width: 100%; diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 2f63214..5e633aa 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,3 +1,11 @@ +import AppliedCompaniesList from "@/components/mypage/AppliedCompaniesList"; +import DetailProfile from "@/components/mypage/DetailProfile"; + export default function MyPage() { - return <>mypage; + return ( +
+ + +
+ ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 1b63291..caa8c19 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,11 +8,11 @@ export default function Home() { return (
-
+
- +
); diff --git a/src/app/recruitments/apply/page.tsx b/src/app/recruitments/apply/page.tsx new file mode 100644 index 0000000..dbe3905 --- /dev/null +++ b/src/app/recruitments/apply/page.tsx @@ -0,0 +1,225 @@ +"use client"; + +import useApplyToCompany from "@/apis/applications"; +import { + ApplyRequestItmeType, + AttachmentsType, +} from "@/apis/applications/type"; +import { useFileUpload } from "@/apis/file"; +import { useGetRecruitmentsDetail } from "@/apis/recruitments"; +import GhostBtn from "@/components/common/Button/GhostBtn"; +import Loading from "@/components/common/Loading"; +import Logo from "@/components/common/Logo"; +import FilePreview from "@/components/recruitments/apply/FilePreview"; +import FileUploader from "@/components/recruitments/apply/FileUploader"; +import Header_Contents from "@/components/recruitments/apply/Header_Contents"; +import UrlListComponent from "@/components/recruitments/apply/UrlListComponent"; +import ShadowBox from "@/components/recruitments/apply/ShadowBox"; +import TitleBox from "@/components/recruitments/apply/TitleBox"; +import useMoadl from "@/hook/useModal"; +import { Icon, useToastStore } from "@team-return/design-system"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import URLItem from "@/components/recruitments/apply/URLItem"; + +export default function Apply() { + const param = useSearchParams(); + const { Modal, openModal, closeModal } = useMoadl(); + const { mutate: onApplyToCompany, isLoading: applyIsLoading } = + useApplyToCompany(param.get("id")!); + const { + mutate: onUploadFile, + data: fileResponse, + isLoading, + isSuccess, + } = useFileUpload(); + const { append } = useToastStore(); + const [fileList, setFileList] = useState([]); + const [urlList, setUrlList] = useState([]); + const [applyRequest, setApplyRequest] = useState([]); + const [isClickedApplyBtn, setIsClickedApplyBtn] = useState(false); + + const [isApply, setIsApply] = useState(false); + + const addRequestFileList = (fileList: File[]) => { + setFileList((prev) => [...prev, ...fileList]); + }; + + const addRequestUrlList = (urlList: string[]) => { + setUrlList((prev) => [...prev, ...urlList]); + if (urlList.length) + setApplyRequest((prev) => [ + ...prev, + ...urlList.map((url) => ({ + url, + type: "URL" as AttachmentsType, + })), + ]); + }; + + useEffect(() => { + if (isClickedApplyBtn) { + if (fileList.length) { + openModal(); + } else { + append({ + title: "", + message: "파일이 존재하지 않습니다.", + type: "RED", + }); + setIsClickedApplyBtn(false); + setApplyRequest([]); + } + } + }, [fileList]); + + useEffect(() => { + if (isSuccess) { + setIsApply(true); + setApplyRequest((prev: ApplyRequestItmeType[]) => [ + ...prev, + ...fileResponse.urls.map((url) => ({ + url, + type: "FILE" as AttachmentsType, + })), + ]); + } + }, [fileResponse]); + + useEffect(() => { + if (isApply) { + onApplyToCompany(applyRequest); + //openModal(); + } + }, [applyRequest]); + + //============================== + // 모집의뢰서 + + const { data: recruitmentsDetial } = useGetRecruitmentsDetail(param.get("id")!); + + return ( + <> + {(isLoading || applyIsLoading) && ( +
{ + e.stopPropagation(); + }} + > +
+ +
+
+ )} +
+ +
+
+ +

+ {recruitmentsDetial?.company_name} +

+
+

+ 제출서류 : {recruitmentsDetial?.submit_document} +

+ + + + + + + + +
+
+ +
+ + + + + + + + +
+ { + setIsClickedApplyBtn(true); + }} + > + 지원하기 + +
+
+
+
+ +
+
+

첨부파일

+
+ {fileList.map((file, idx) => ( + + ))} +
+

URL

+
+ {urlList.length ? ( + urlList.map((url, idx) => ) + ) : ( +

+ 등록된 URL이 존재하지 않습니다. +

+ )} +
+
+
+ { + onUploadFile(fileList); + }} + > + 지원하기 + +
+ +
+
+ + ); +} diff --git a/src/app/recruitments/detail/page.tsx b/src/app/recruitments/detail/page.tsx index d29c528..5874036 100644 --- a/src/app/recruitments/detail/page.tsx +++ b/src/app/recruitments/detail/page.tsx @@ -1,17 +1,18 @@ "use client"; -import { GetRecruitmentsDetail } from "@/apis/recruitments"; +import { useGetRecruitmentsDetail } from "@/apis/recruitments"; import GhostBtn from "@/components/common/Button/GhostBtn"; import CompanyTitle from "@/components/company/CompanyTitle"; import RecruitmentsTable from "@/components/recruitments/RecruitmentsTable"; -import { useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; export default function RecruitmentsDetailPage() { const param = useSearchParams(); - const { data } = GetRecruitmentsDetail(param.get("id")!); - if (data) { + const navigator = useRouter(); + const { data: RecruitmentsDetial } = useGetRecruitmentsDetail(param.get("id")!); + if (RecruitmentsDetial) { const { company_id, company_name, company_profile_url, ...rest } = - data.data; + RecruitmentsDetial; return (
@@ -19,7 +20,13 @@ export default function RecruitmentsDetailPage() { company_name={company_name} company_profile_url={company_profile_url} > - 지원하기 + { + navigator.push(`/recruitments/apply/?id=${param.get("id")}`); + }} + > + 지원하기 +
diff --git a/src/components/BandBanner.tsx b/src/components/BandBanner.tsx index 0a1374e..e42ceb8 100644 --- a/src/components/BandBanner.tsx +++ b/src/components/BandBanner.tsx @@ -1,19 +1,27 @@ "use client"; -import { Icon } from "@team-return/design-system"; +import { Icon, useToastStore } from "@team-return/design-system"; import Image from "next/image"; -import BannerBackground from "@public/BannerBackground.png"; +import BannerBackground from "@public/BannerBackground.webp"; import Guy from "@public/Guy.webp"; import Link from "next/link"; export default function BandBanner() { + const { append } = useToastStore(); return ( { + append({ + title: "", + message: "개발 중인 기능입니다.", + type: "YELLOW", + }); + }} > -
-

+

+

우리학교 학생들은 얼마나 취업했을까?

diff --git a/src/components/BookmarkCard.tsx b/src/components/BookmarkCard.tsx new file mode 100644 index 0000000..db262a0 --- /dev/null +++ b/src/components/BookmarkCard.tsx @@ -0,0 +1,39 @@ +import { useGetBookmarks } from "@/apis/bookmarks"; +import { Icon } from "@team-return/design-system"; +import Link from "next/link"; +import HoverPrefetchLink from "./common/HoverPrefetchLink"; + +export default function BookmarkCard() { + const { data: bookmarks } = useGetBookmarks(); + + return ( +
+ {!bookmarks?.bookmarks.length && ( +
+

⚠ 저장한 모집의뢰서가 존재하지 않습니다.

+ + 모집의뢰서 보러가기 + + +
+ )} + {bookmarks?.bookmarks.map(({ company_name, recruitment_id }) => ( + +
+
+

+ {company_name} +

+ +
+
+
+ ))} +
+ ); +} diff --git a/src/components/CardList.tsx b/src/components/CardList.tsx index d4f1701..eccf58a 100644 --- a/src/components/CardList.tsx +++ b/src/components/CardList.tsx @@ -1,5 +1,5 @@ import React from "react"; -import CompanyCard from "./CompanyCard"; +import CompanyCard from "./company/CompanyCard"; import RecruitmentsCard from "./recruitments/RecruitmentsCard"; interface PropsType { diff --git a/src/components/Carousel.tsx b/src/components/Carousel.tsx index 8e3ac1e..80383ab 100644 --- a/src/components/Carousel.tsx +++ b/src/components/Carousel.tsx @@ -5,27 +5,17 @@ import React, { useEffect, useRef, useState } from "react"; import CircleBtn from "./CircleBtn"; //======================================================================================================= -import TeatBannerImg from "@public/test-banner-1.png"; -const test = [ +import PopularCompanyBanner from "@public/PopularCompanyBanner.webp"; +import WinterIntrenBanner from "@public/WinterIntrenBanner.webp"; +import { useRouter } from "next/navigation"; +const BannerList = [ { - img: TeatBannerImg, - url: "https://www.naver.com", + img: WinterIntrenBanner, + url: "/recruitments/?page=1&winter_intern=true", }, { - img: TeatBannerImg, - url: "https://www.naver.com", - }, - { - img: TeatBannerImg, - url: "https://www.naver.com", - }, - { - img: TeatBannerImg, - url: "https://www.naver.com", - }, - { - img: TeatBannerImg, - url: "https://www.naver.com", + img: PopularCompanyBanner, + url: "/companies/detail/?id=9", }, ]; //======================================================================================================= @@ -33,10 +23,11 @@ const test = [ export default function Banner() { const [selected, setSelected] = useState(0); const BannerRefs = useRef([]); + const navigator = useRouter(); const handleChangeNext = () => { setSelected((prev) => { - if (test.length - 1 === prev) { + if (BannerList.length - 1 === prev) { return 0; } return ++prev; @@ -45,7 +36,7 @@ export default function Banner() { const handleChangePrev = () => { setSelected((prev) => { if (prev === 0) { - return test.length - 1; + return BannerList.length - 1; } return --prev; }); @@ -62,15 +53,15 @@ export default function Banner() { return (
- {test.map((item, index) => ( + {BannerList.map((item, index) => (
(BannerRefs.current[index] = el)} onClick={() => { - window.open(item.url); + navigator.push(item.url); }} > @@ -79,7 +70,7 @@ export default function Banner() {
- {test.map((_, index: number) => ( + {BannerList.map((_, index: number) => (
diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 6664c0b..9962444 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -4,7 +4,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ToastContainer } from "@team-return/design-system"; import { CookiesProvider } from "react-cookie"; import SignupContextProvider from "./account/singup/ContextProvider"; -import ModalContainer from "./modal/ModalContainer"; import ModalContextProvider from "./modal/ModalContextProvider"; interface PropsType { @@ -19,7 +18,6 @@ export default function Provider({ children }: PropsType) { - {children} diff --git a/src/components/Suggestion.tsx b/src/components/Suggestion.tsx index c011b73..dbb6927 100644 --- a/src/components/Suggestion.tsx +++ b/src/components/Suggestion.tsx @@ -1,17 +1,31 @@ -import CompanyCard from "./CompanyCard"; +import { useEffect } from "react"; +import BookmarkCard from "./BookmarkCard"; +import CompanyCard from "./company/CompanyCard"; import RecruitmentsCard from "./recruitments/RecruitmentsCard"; import SuggestionHeader from "./SuggestionHeader"; interface PropsType { - listType: "Company" | "Recruitments" | "BookMark"; + listType: "Company" | "Recruitments" | "Bookmark"; } export default function Suggestion({ listType }: PropsType) { + function windowResize() { + console.log("resize"); + } + useEffect(() => { + document.addEventListener(`resize`, windowResize); + + return () => { + document.removeEventListener(`resize`, windowResize); + }; + }, []); + return (
- {listType === "Company" && } - {listType === "Recruitments" && } + {listType === "Company" && } + {listType === "Recruitments" && } + {listType === "Bookmark" && }
); } diff --git a/src/components/SuggestionHeader.tsx b/src/components/SuggestionHeader.tsx index 6f39bc7..5a677f4 100644 --- a/src/components/SuggestionHeader.tsx +++ b/src/components/SuggestionHeader.tsx @@ -1,37 +1,44 @@ "use client"; import { Icon } from "@team-return/design-system"; -import { MyProfile } from "@/apis/students"; +import { useMyProfile } from "@/apis/students"; +import Link from "next/link"; interface PropsType { - listType: "Company" | "Recruitments" | "BookMark"; + listType: "Company" | "Recruitments" | "Bookmark"; } export default function SuggestionHeader({ listType }: PropsType) { - const { data } = MyProfile(); - - const fix_data = { + const { data: profile } = useMyProfile(); + + const suggestionHeaderDummy = { Company: { title: "🏢 이런 기업은 어떠세요?", - router: "/company", + router: "/companies", }, Recruitments: { - title: `👩‍💻 ${data?.data.student_name || '사용자'}님의 관심 분야에요`, - router: "/recruitements", + title: `👩‍💻 ${profile?.student_name || "사용자"}님의 관심 분야에요`, + + router: "/recruitments", }, - BookMark: { + Bookmark: { title: "📌 내가 저장한 모집의뢰서", }, }; return (
-

{fix_data[listType].title}

+

{suggestionHeaderDummy[listType].title}

- + {listType !== "Bookmark" && ( + + 전체보기 + + + )}
); } diff --git a/src/components/account/login/loginStateManagement.tsx b/src/components/account/login/loginStateManagement.tsx index 5d37bd3..2b4ec4e 100644 --- a/src/components/account/login/loginStateManagement.tsx +++ b/src/components/account/login/loginStateManagement.tsx @@ -5,7 +5,7 @@ import { RequestBody } from "@/apis/user/type"; import LoginInputs from "@/components/account/login/loginInputsComponents"; import SubmitBtn from "@/components/account/login/submitBtn"; import { useCallback, useState } from "react"; -import { Login } from "@/apis/user"; +import { useLogin } from "@/apis/user"; import useForm from "@/hook/useForm"; export default function LoginStateMenagement() { @@ -14,7 +14,7 @@ export default function LoginStateMenagement() { account_id: "", password: "", }); - const { mutate: loginClick } = Login(inputStates, isChecked); + const { mutate: loginClick } = useLogin(inputStates, isChecked); const allIsInputState = useCallback( (): boolean => inputStates.account_id !== "" && inputStates.password !== "", [inputStates] diff --git a/src/components/account/singup/signupPages/FirstSignupPage.tsx b/src/components/account/singup/signupPages/FirstSignupPage.tsx index bdd6ee2..447a9de 100644 --- a/src/components/account/singup/signupPages/FirstSignupPage.tsx +++ b/src/components/account/singup/signupPages/FirstSignupPage.tsx @@ -1,6 +1,6 @@ "use client"; -import { CheckAuthCode, SendAuthCode } from "@/apis/auth"; +import { useCheckAuthCode, useSendAuthCode } from "@/apis/auth"; import LargeBtn from "@/components/common/Button/LargeBtn"; import TextFiled from "@/components/common/TextFiled"; import useSignUpContext from "@/hook/useSignupContext"; @@ -13,8 +13,8 @@ function FirstSignupPage() { const { email, auth_code, password, passwordCheck } = signupState; const { append } = useToastStore(); const navigator = useRouter(); - const { mutate: SandAuthCodeAPI } = SendAuthCode(); - const { mutate: CheckAuthCodeAPI } = CheckAuthCode( + const { mutate: SandAuthCodeAPI } = useSendAuthCode(); + const { mutate: CheckAuthCodeAPI } = useCheckAuthCode( { email, auth_code, diff --git a/src/components/account/singup/signupPages/SecondSignupPage.tsx b/src/components/account/singup/signupPages/SecondSignupPage.tsx index 8d24548..db7ada7 100644 --- a/src/components/account/singup/signupPages/SecondSignupPage.tsx +++ b/src/components/account/singup/signupPages/SecondSignupPage.tsx @@ -1,4 +1,4 @@ -import { Signup } from "@/apis/students"; +import { useSignup } from "@/apis/students"; import LargeBtn from "@/components/common/Button/LargeBtn"; import RadioBtn from "@/components/common/RadioBtn"; import TextFiled from "@/components/common/TextFiled"; @@ -16,7 +16,7 @@ function SecondSignupPage() { setSignupState((prev) => ({ ...prev, ["gender"]: select })); }, [select]); - const { mutate } = Signup(); + const { mutate } = useSignup(); const SignupAPI = () => { const RequestBody = { diff --git a/src/components/common/Button/FIleDownload.tsx b/src/components/common/Button/FIleDownload.tsx new file mode 100644 index 0000000..4f05a22 --- /dev/null +++ b/src/components/common/Button/FIleDownload.tsx @@ -0,0 +1,26 @@ +import Link from "next/link"; +import { file_name_regex } from "@/util/regex"; +import { Icon } from "@team-return/design-system"; + +interface PropsType { + fileURL: string; + onClick?: (props: any) => void; +} + +export default function FileDownload({ fileURL, onClick }: PropsType) { + return ( + +
+

+ {file_name_regex(fileURL)} +

+ +
+ + ); +} diff --git a/src/components/common/Button/GhostBtn.tsx b/src/components/common/Button/GhostBtn.tsx index dfb25ce..868b589 100644 --- a/src/components/common/Button/GhostBtn.tsx +++ b/src/components/common/Button/GhostBtn.tsx @@ -1,17 +1,35 @@ "use client"; -import React from "react"; +import React, { useEffect, useState } from "react"; interface PropsType { children: React.ReactNode; onClick?: () => void; + disabled?: boolean; } -function GhostBtn({ children, onClick }: PropsType) { +function GhostBtn({ children, onClick, disabled = false }: PropsType) { + const [isHover, setIsHover] = useState(false); + return ( diff --git a/src/components/common/Chips.tsx b/src/components/common/Chips.tsx index b1dc358..467fde4 100644 --- a/src/components/common/Chips.tsx +++ b/src/components/common/Chips.tsx @@ -1,6 +1,6 @@ "use client"; -import { TechCodeResponensType } from "@/util/type"; +import { TechCodeResponensType } from "@/util/type/type"; import React from "react"; interface PropsType { diff --git a/src/components/common/DropDown.tsx b/src/components/common/DropDown.tsx index 9e43fa0..cf0e038 100644 --- a/src/components/common/DropDown.tsx +++ b/src/components/common/DropDown.tsx @@ -4,22 +4,19 @@ import { useDropDown } from "@/hook/useDropDown"; import { Icon } from "@team-return/design-system"; import React from "react"; -type DropDownItemsType = { - code: number; - keyword: string; -}; +interface DropdownItemsType { + code: string; + label: string; +} interface PropsType { title: string; - items?: DropDownItemsType[]; - onItemClick: ( - itemCode: string | number, - name: "job_code" | "tech_code" - ) => void; + items: DropdownItemsType[]; + onClickItem: (itemId: string) => void; selected: number | string; } -function DropDown({ title, items, onItemClick, selected }: PropsType) { +function DropDown({ title, items, onClickItem, selected }: PropsType) { const { toggleDropdown, DropDownComponent, closeDropDown } = useDropDown(); return ( @@ -31,11 +28,11 @@ function DropDown({ title, items, onItemClick, selected }: PropsType) {
- {items?.map((item, index) => ( + {items?.map((item, idx) => (
{ - onItemClick(item.code, "job_code"); + onClickItem(item.code.toString()); closeDropDown(); }} className={`text-caption leading-caption ${ @@ -44,7 +41,7 @@ function DropDown({ title, items, onItemClick, selected }: PropsType) { : "text-[#7f7f7f] font-r" } hover:text-black py-2 w-full flex flex-col items-center`} > - {item.keyword} + {item.label}
))}
diff --git a/src/components/common/Dropdown/Dropdown.tsx b/src/components/common/Dropdown/Dropdown.tsx new file mode 100644 index 0000000..75d36bb --- /dev/null +++ b/src/components/common/Dropdown/Dropdown.tsx @@ -0,0 +1,7 @@ +export default function Dropdown() { + return ( + <> + <> + + ); +} diff --git a/src/components/common/Dropdown/KebabMenu.tsx b/src/components/common/Dropdown/KebabMenu.tsx new file mode 100644 index 0000000..6e5cf75 --- /dev/null +++ b/src/components/common/Dropdown/KebabMenu.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useDropDown } from "@/hook/useDropDown"; +import { KebabItemType } from "@/util/type/kebabMenu"; +import { Icon } from "@team-return/design-system"; + +interface PropsType { + items: KebabItemType[]; +} + +export default function KebabMenu({ items }: PropsType) { + const { DropDownComponent, toggleDropdown, closeDropDown } = useDropDown(); + return ( +
+ + +
+ {items.map(({ label, onClick }, idx) => ( +
{ + onClick(); + closeDropDown(); + }} + > + {label} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 0968003..7fae175 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -6,7 +6,7 @@ import Logo from "@public/Logo.png"; import { Icon } from "@team-return/design-system"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { MyProfile } from "@/apis/students"; +import { useMyProfile } from "@/apis/students"; function Header() { const pathname = usePathname(); @@ -14,7 +14,7 @@ function Header() { return null; } - const profile = MyProfile(); + const { data: profile } = useMyProfile(); return (
기업체 @@ -37,7 +37,7 @@ function Header() { className={`text-[#333333] text-b2 ${ pathname.indexOf("/recruitments") !== -1 && "font-b" }`} - href={"/recruitments?page=1&job_code=&tech_code=&name="} + href={"/recruitments?page=1"} > 모집의뢰서 @@ -56,25 +56,25 @@ function Header() { //알림모달 }} > -
+
프로필사진

- {profile.data?.data.student_name} + {profile?.student_name}

- + {/* */}
diff --git a/src/components/common/Loading.tsx b/src/components/common/Loading.tsx new file mode 100644 index 0000000..7fb8346 --- /dev/null +++ b/src/components/common/Loading.tsx @@ -0,0 +1,17 @@ +import LoadingImage from "@public/Loading.png"; +import Image from "next/image"; + +interface PropsType { + size?: string; +} + +export default function Loading({ size = "100px" }: PropsType) { + return ( +
+ 로딩 중... +
+ ); +} diff --git a/src/components/common/Logo.tsx b/src/components/common/Logo.tsx new file mode 100644 index 0000000..d735fc1 --- /dev/null +++ b/src/components/common/Logo.tsx @@ -0,0 +1,26 @@ +import Image from "next/image"; + +interface PropsType { + src: string; + width?: string; + height?: string; +} + +export default function Logo({ src, width, height }: PropsType) { + return ( +
+ CompaniesLogo +
+ ); +} diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx index aa3a282..21841d8 100644 --- a/src/components/common/Pagination.tsx +++ b/src/components/common/Pagination.tsx @@ -1,8 +1,9 @@ "use client"; -import { GetNumberOfCompaniesListPages } from "@/apis/companies"; -import { GetNumberOfRecruitmentRequestListPages } from "@/apis/recruitments"; -import { useQueryString } from "@/hook/useQueryString"; +import { useGetNumberOfCompaniesListPages } from "@/apis/companies"; +import { useGetNumberOfRecruitmentRequestListPages } from "@/apis/recruitments"; +import { RecruitmentsQueryType } from "@/hook/useQueryString/type"; +import { useQueryString } from "@/hook/useQueryString/useQueryString"; import { Icon, theme } from "@team-return/design-system"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; @@ -10,20 +11,30 @@ import { useEffect, useState } from "react"; export default function Pagination() { const pathname = usePathname(); - const { setQueryString, getQueryString, getQueryStringEntry } = useQueryString({ - page: "1", - job_code: "", - tech_code: "", - name: "", - }); + const { setQueryString, getQueryString, getQueryStringEntry } = + useQueryString({ + page: "1", + job_code: "", + tech_code: "", + name: "", + winter_intern: "", + }); const currentPageNumber = getQueryString("page"); + const recruitmentQuery = [ + getQueryStringEntry("job_code"), + getQueryStringEntry("tech_code"), + getQueryStringEntry("name"), + getQueryStringEntry("winter_intern"), + ] + .filter((item) => item) + .join("&"); + const numberOfPages = pathname === "/recruitments/" - ? GetNumberOfRecruitmentRequestListPages( - `${getQueryStringEntry("job_code")}&${getQueryStringEntry("tech_code")}&${getQueryStringEntry("name")}` - )?.data - : GetNumberOfCompaniesListPages(`${getQueryStringEntry("name")}`)?.data; + ? useGetNumberOfRecruitmentRequestListPages(recruitmentQuery)?.data + : useGetNumberOfCompaniesListPages(`${getQueryStringEntry("name")}`) + ?.data; const [pagesArray, setPagesArray] = useState([]); useEffect(() => { @@ -73,7 +84,11 @@ export default function Pagination() { icon="Chevron" direction="right" size={16} - color={getQueryString("page") === String(pagesArray.length) ? "gray50" : "gray90"} + color={ + getQueryString("page") === String(pagesArray.length) + ? "gray50" + : "gray90" + } />
diff --git a/src/components/common/SearchDropDown.tsx b/src/components/common/SearchDropDown.tsx index 008c7cb..ecab39a 100644 --- a/src/components/common/SearchDropDown.tsx +++ b/src/components/common/SearchDropDown.tsx @@ -1,15 +1,16 @@ "use client"; -import React, { useEffect, useState } from "react"; +import { useGetCode } from "@/apis/code"; import { useDropDown } from "@/hook/useDropDown"; -import { Icon } from "@team-return/design-system"; -import TextFiled from "./TextFiled"; import useForm from "@/hook/useForm"; -import Chips from "./Chips"; -import { GetCode } from "@/apis/code"; -import { TechCodeResponensType } from "@/util/type"; +import { RecruitmentsQueryType } from "@/hook/useQueryString/type"; +import { useQueryString } from "@/hook/useQueryString/useQueryString"; +import { TechCodeResponensType } from "@/util/type/type"; +import { Icon } from "@team-return/design-system"; +import React, { useEffect, useState } from "react"; import GhostBtn from "./Button/GhostBtn"; -import { setQueryStringType, useQueryString } from "@/hook/useQueryString"; +import Chips from "./Chips"; +import TextFiled from "./TextFiled"; interface PropsType { title: string; @@ -18,12 +19,14 @@ interface PropsType { function SearchDropDown({ title }: PropsType) { const { DropDownComponent, toggleDropdown, closeDropDown } = useDropDown(); - const { setQueryString, getQueryString } = useQueryString({ - page: "1", - job_code: "", - tech_code: "", - name: "", - }); + const { setQueryString, getQueryString } = + useQueryString({ + page: "1", + job_code: "", + tech_code: "", + name: "", + winter_intern: "", + }); return (
void; - setQueryString: (newValue: setQueryStringType) => void; - getQueryString: (key: keyof setQueryStringType) => string; + setQueryString: (newValue: Partial) => void; + getQueryString: (key: string) => string; }) { const [select, setSelect] = useState([]); const [searchKeyword, setSearch] = useState(""); @@ -60,7 +63,7 @@ function TechCodeDropDownComponent({ techCodeSearch: "", }); - const { data } = GetCode("TECH", searchKeyword); + const { data: codes } = useGetCode("TECH", searchKeyword); useEffect(() => { const techArray = getQueryString("tech_code") @@ -68,12 +71,12 @@ function TechCodeDropDownComponent({ .map((item) => Number(item)); setSelect(() => { return ( - data?.data.codes.filter((item: TechCodeResponensType) => + codes?.codes.filter((item: TechCodeResponensType) => techArray?.some((techItem) => item.code === techItem) ) || [] ); }); - }, [getQueryString("tech_code"), data?.data.codes]); + }, [getQueryString("tech_code"), codes?.codes]); return (
diff --git a/src/components/common/Skelton/CompanySkelton.tsx b/src/components/common/Skelton/CompanySkelton.tsx new file mode 100644 index 0000000..87e85f5 --- /dev/null +++ b/src/components/common/Skelton/CompanySkelton.tsx @@ -0,0 +1,21 @@ +import Shimmer from "./Shimmer"; + +export default function CompaniesSkelton() { + return ( + <> + {Array.from({ length: 9 }, () => ( +
+
+ +
+
+ +
+
+ +
+
+ ))} + + ); +} diff --git a/src/components/common/Skelton/Shimmer.tsx b/src/components/common/Skelton/Shimmer.tsx new file mode 100644 index 0000000..bd85cfd --- /dev/null +++ b/src/components/common/Skelton/Shimmer.tsx @@ -0,0 +1,7 @@ +export default function Shimmer() { + return ( +
+
+
+ ); +} diff --git a/src/components/common/Skelton/SkeltonElement.tsx b/src/components/common/Skelton/SkeltonElement.tsx new file mode 100644 index 0000000..388924a --- /dev/null +++ b/src/components/common/Skelton/SkeltonElement.tsx @@ -0,0 +1,24 @@ +import Shimmer from "./Shimmer"; + +export default function RecruitmentSkelton() { + return ( + <> + {Array.from({ length: 12 }, () => ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ))} + + ); +} diff --git a/src/components/CompanyCard.tsx b/src/components/company/CompanyCard.tsx similarity index 69% rename from src/components/CompanyCard.tsx rename to src/components/company/CompanyCard.tsx index 7c0bc60..b4f8785 100644 --- a/src/components/CompanyCard.tsx +++ b/src/components/company/CompanyCard.tsx @@ -1,31 +1,29 @@ "use client"; +import { useGetCompaniesList } from "@/apis/companies"; +import { Icon } from "@team-return/design-system"; import Image from "next/image"; import { useSearchParams } from "next/navigation"; -import { GetCompaniesList } from "@/apis/companies"; -import { useEffect, useState } from "react"; -import { CompaniesListType } from "@/apis/companies/type"; -import HoverPrefetchLink from "./common/HoverPrefetchLink"; -import { Icon } from "@team-return/design-system"; +import HoverPrefetchLink from "../common/HoverPrefetchLink"; +import CompaniesSkelton from "../common/Skelton/CompanySkelton"; -export default function CompanyCard() { - const getParams = useSearchParams(); - const [companyList, setCompanyList] = useState([]); +interface PropsType { + maxLength?: number; +} - const res = GetCompaniesList(getParams.toString()); +export default function CompanyCard({ maxLength = 12 }: PropsType) { + const getParams = useSearchParams(); - useEffect(() => { - if (res.data?.data.companies) { - (() => { - setCompanyList(res.data.data.companies); - })(); - } - }, [res]); + const { data: companyList, isLoading } = useGetCompaniesList( + getParams.toString() + ); return (
- {companyList.map( - ({ logo_url, name, take, has_recruitment, id }, index) => ( + {isLoading && } + {companyList?.companies + .filter((_, idx) => idx < maxLength) + .map(({ logo_url, name, take, has_recruitment, id }, index) => (
@@ -51,8 +49,7 @@ export default function CompanyCard() {
- ) - )} + ))}
); } diff --git a/src/components/company/CompanyTitle.tsx b/src/components/company/CompanyTitle.tsx index a72e8d6..1baf113 100644 --- a/src/components/company/CompanyTitle.tsx +++ b/src/components/company/CompanyTitle.tsx @@ -1,9 +1,9 @@ "use client"; +import { getCompanyKebabItems } from "@/util/object/kebabMenuItems"; +import { KebabItemType } from "@/util/type/kebabMenu"; import Image from "next/image"; -import useMoadl from "@/hook/useModal"; -import { Icon } from "@team-return/design-system"; -import { useDropDown } from "@/hook/useDropDown"; +import KebabMenu from "../common/Dropdown/KebabMenu"; interface PropsType { business_number?: string; @@ -22,11 +22,11 @@ export default function CompanyTitle({ onClickInterview, children, }: PropsType) { - const { Modal, toggleModal, closeModal } = useMoadl(); - const { DropDownComponent, toggleDropdown, closeDropDown } = useDropDown(); + const kebabItems: KebabItemType[] = getCompanyKebabItems( + onClickRecruitments, + onClickInterview + ); - const menuStyle = - "flex-1 text-b3 leading-b3 font-m text-[#7f7f7f] flex justify-start items-center px-[10px] cursor-pointer hover:text-[#333333]"; return (
@@ -49,34 +49,7 @@ export default function CompanyTitle({
{onClickInterview && onClickRecruitments && ( - + )} {children}
diff --git a/src/components/company/ReviewList.tsx b/src/components/company/ReviewList.tsx index ad35085..1f121df 100644 --- a/src/components/company/ReviewList.tsx +++ b/src/components/company/ReviewList.tsx @@ -1,13 +1,13 @@ "use client"; -import { getReviewDetails, GetReviewList } from "@/apis/reviews"; +import { useGetReviewDetails, useGetReviewList } from "@/apis/reviews"; import { getReviewListProps } from "@/apis/reviews/type"; import { useSearchParams } from "next/navigation"; import ReviewItem from "./ReviewItem"; export default function ReviewList() { const params = useSearchParams(); - const { data: reviewList, isLoading } = GetReviewList(params.get("id")!); + const { data: reviewList, isLoading } = useGetReviewList(params.get("id")!); if (isLoading) return ( @@ -33,7 +33,7 @@ export default function ReviewList() { } function ReviewContainers({ review_id, writer, date }: getReviewListProps) { - const { data: reviewDetails } = getReviewDetails(review_id); + const { data: reviewDetails } = useGetReviewDetails(review_id); return ( <> {reviewDetails?.qna_responses.map((item, idx) => ( diff --git a/src/components/modal/ModalContainer.tsx b/src/components/modal/ModalContainer.tsx deleted file mode 100644 index a2c2e35..0000000 --- a/src/components/modal/ModalContainer.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import useMoadl from "@/hook/useModal"; - -export default function ModalContainer() { - const { isOpen, closeModal } = useMoadl(); - return ( - <> - {isOpen && ( -
- )} - - ); -} \ No newline at end of file diff --git a/src/components/mypage/ApplicationStatus.tsx b/src/components/mypage/ApplicationStatus.tsx new file mode 100644 index 0000000..0a75151 --- /dev/null +++ b/src/components/mypage/ApplicationStatus.tsx @@ -0,0 +1,19 @@ +import { ApplicationsStatusType } from "@/apis/applications/type"; +import { applicationEnum, applicationStatusStyle } from "@/util/object/enum"; + +interface PropsType { + status: ApplicationsStatusType; +} + +export default function ApplicationStatus({ status }: PropsType) { + return ( +
+

+ {applicationEnum[status]} +

+
+ ); +} diff --git a/src/components/mypage/AppliedCompaniesList.tsx b/src/components/mypage/AppliedCompaniesList.tsx new file mode 100644 index 0000000..fe9d0d9 --- /dev/null +++ b/src/components/mypage/AppliedCompaniesList.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useGetApplications } from "@/apis/applications"; +import APpliedCompanyItem from "./AppliedICompanyItem"; +import Docs from "@public/Docs.svg"; +import Image from "next/image"; + +export default function AppliedCompaniesList() { + const { data: applications } = useGetApplications(); + + return ( +
+

내가 지원한 기업

+ {!applications && ( +
+ DOCS +

+ 아직 지원한 기업이 없어요 +

+
+ )} +
+ {applications?.applications.map((item) => ( + + ))} +
+
+ ); +} diff --git a/src/components/mypage/AppliedICompanyItem.tsx b/src/components/mypage/AppliedICompanyItem.tsx new file mode 100644 index 0000000..81fb0cb --- /dev/null +++ b/src/components/mypage/AppliedICompanyItem.tsx @@ -0,0 +1,103 @@ +import { ApplicationItemType } from "@/apis/applications/type"; +import { Icon } from "@team-return/design-system"; +import Link from "next/link"; +import { useCallback, useRef, useState } from "react"; +import FileDownload from "../common/Button/FIleDownload"; +import ApplicationStatus from "./ApplicationStatus"; + +export default function APpliedCompanyItem({ + company, + application_status, + attachments, +}: ApplicationItemType) { + const parentRef = useRef(null); + const childRef = useRef(null); + const [isCollapse, setIsCollapse] = useState(false); + + const handleButtonClick = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation(); + if (parentRef.current === null || childRef.current === null) { + return; + } + if (parentRef.current.clientHeight > 0) { + parentRef.current.style.height = "0"; + } else { + parentRef.current.style.height = `${childRef.current.clientHeight}px`; + } + setIsCollapse(!isCollapse); + }, + [isCollapse] + ); + + const parentRefHeight = parentRef.current?.style.height ?? "0px"; + + return ( +
+
+
+

{company}

+ {/*디자인에는 있지만 api에는 없어서 잠시 주석*/} + {/*

+ 2023.11.25 +

*/} +
+
+ +
+ +
+
+
+
+ {attachments.map((item, idx) => ( + <> + {item.type === "FILE" && ( + { + e.stopPropagation(); + }} + /> + )} + + ))} +
+
{ + e.stopPropagation(); + }} + > + {attachments.map((item, idx) => ( + <> + {item.type === "URL" && ( + +
  • + {item.url} +
  • + + )} + + ))} +
    +
    +
    +
    + ); +} diff --git a/src/components/mypage/DetailProfile.tsx b/src/components/mypage/DetailProfile.tsx new file mode 100644 index 0000000..af85b8f --- /dev/null +++ b/src/components/mypage/DetailProfile.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useMyProfile } from "@/apis/students"; +import { departmentEnum } from "@/util/object/enum"; +import { getMypageKebabItems } from "@/util/object/kebabMenuItems"; +import Image from "next/image"; +import KebabMenu from "../common/Dropdown/KebabMenu"; +import GhostTag from "./GhostTag"; + +export default function DetailProfile() { + const { data: profile } = useMyProfile(); + + return ( +
    +
    + 프로필 사진 +
    +
    +
    +

    {profile?.student_name}

    + {profile?.student_gcn} +
    +

    + {profile && departmentEnum[profile.department]} +

    +
    + +
    + ); +} diff --git a/src/components/mypage/GhostTag.tsx b/src/components/mypage/GhostTag.tsx new file mode 100644 index 0000000..c033111 --- /dev/null +++ b/src/components/mypage/GhostTag.tsx @@ -0,0 +1,13 @@ +interface PropsType { + children?: string | number; +} + +export default function GhostTag({ children }: PropsType) { + return ( +
    +

    + {children} +

    +
    + ); +} diff --git a/src/components/recruitments/RecruitmentsCard.tsx b/src/components/recruitments/RecruitmentsCard.tsx index a5bc9f8..6a36c48 100644 --- a/src/components/recruitments/RecruitmentsCard.tsx +++ b/src/components/recruitments/RecruitmentsCard.tsx @@ -1,81 +1,85 @@ "use client"; +import { useSetBookmarks } from "@/apis/bookmarks"; +import { useGetRecruitmentsList } from "@/apis/recruitments"; import { Icon } from "@team-return/design-system"; import Image from "next/image"; -import React, { useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; -import { RecruitmentsListType } from "@/apis/recruitments/type"; -import { GetRecruitmentsList } from "@/apis/recruitments"; +import React from "react"; import HoverPrefetchLink from "../common/HoverPrefetchLink"; +import RecruitmentSkelton from "../common/Skelton/SkeltonElement"; -export default function RecruitmentsCard() { +interface PropsType { + maxLength?: number; +} + +export default function RecruitmentsCard({ maxLength = 12 }: PropsType) { const getParams = useSearchParams(); - const [list, setList] = useState([]); - const res = GetRecruitmentsList(getParams.toString()); - useEffect(() => { - setList((prev) => res.data?.data.recruitments || prev); - }, [res]); + const { data: recruitmentsList, isLoading } = useGetRecruitmentsList(getParams.toString()); - const tagStyle = - "text-caption leading-caption text-lightBlue font-r border rounded-full border-[#135C9D] py-1 px-2"; + const { mutate: SetBookmarksMutate } = useSetBookmarks(); return (
    - {list.map( - ( - { - company_profile_url, - company_name, - train_pay, - job_code_list, - bookmarked, - recruit_id, - military, - }, - index - ) => ( - -
    -
    - -
    -
    -

    - {job_code_list} -

    -

    - {company_name} -

    -
    -
    실습수당 {train_pay}만원
    - {military &&
    병역특례
    } -
    - +
    +
    +

    + {job_code_list} +

    +

    + {company_name} +

    +
    +
    실습수당 {train_pay}만원
    + {military &&
    병역특례
    } +
    + +
    -
    - - ) - )} + + ) + )}
    ); } diff --git a/src/components/recruitments/RecruitmentsTable.tsx b/src/components/recruitments/RecruitmentsTable.tsx index 0b90de8..8c19639 100644 --- a/src/components/recruitments/RecruitmentsTable.tsx +++ b/src/components/recruitments/RecruitmentsTable.tsx @@ -1,10 +1,10 @@ "use client"; import { RecruitmentsDetailTable } from "@/apis/recruitments/type"; -import { hiringProgressEnum } from "@/util/enum"; +import { hiringProgressEnum } from "@/util/object/enum"; import { money_regex, time_parsing } from "@/util/regex"; import { Icon } from "@team-return/design-system"; -import React, { useState } from "react"; +import React, { useCallback, useRef, useState } from "react"; function RecruitmentsTable({ ...rest }: RecruitmentsDetailTable) { const { @@ -30,56 +30,79 @@ function RecruitmentsTable({ ...rest }: RecruitmentsDetailTable) { {areas.map((item, index) => { const [isOpen, setIsOpen] = useState(false); + const parentRef = useRef(null); + const childRef = useRef(null); + + const handleButtonClick = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation(); + if (parentRef.current === null || childRef.current === null) + return; + if (parentRef.current.clientHeight > 0) { + parentRef.current.style.height = "0px"; + } else { + parentRef.current.style.height = `${childRef.current.clientHeight}px`; + } + setIsOpen((prev) => !prev); + }, + [isOpen] + ); + return ( <> - - setIsOpen((prev) => !prev)} - > - 모집분야 {areas.length !== 1 && index + 1} - - - setIsOpen((prev) => !prev)} - className="cursor-pointer value" +
    + + setIsOpen((prev) => !prev)} + > + 모집분야 {areas.length !== 1 && index + 1} + + + setIsOpen((prev) => !prev)}*/ + className="cursor-pointer value" + > + {isOpen ? "닫기" : "펼쳐서 확인하기"} + + +
    - 펼쳐서 확인하기 - - - {isOpen && ( - <> - - 직무 - {item.job.join(", ")} - - - 기술스택 - - {item.tech.join("\n")} - - - - 채용인원 - {item.hiring} 명 - - - 수행업무 - {item.major_task} - - - 우대사항 - - {item.preferential_treatment || "-"} - - - - )} +
    + + 직무 + {item.job.join(", ")} + + + 기술스택 + + {item.tech.join("\n")} + + + + 채용인원 + {item.hiring} 명 + + + 수행업무 + {item.major_task} + + + 우대사항 + + {item.preferential_treatment || "-"} + + +
    +
    +
    ); })} diff --git a/src/components/recruitments/apply/FilePreview.tsx b/src/components/recruitments/apply/FilePreview.tsx new file mode 100644 index 0000000..bdbf106 --- /dev/null +++ b/src/components/recruitments/apply/FilePreview.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import Image from "next/image"; +import Clip from "@public/Clip.svg"; +import { Icon } from "@team-return/design-system"; + +interface PropsType { + fileName: string; + prepend?: (fileName: string) => void; +} + +function FilePreview({ fileName, prepend }: PropsType) { + return ( +
    + 첨부파일 +

    + {fileName} +

    + {prepend && ( +
    { + prepend(fileName); + }} + > + +
    + )} +
    + ); +} + +export default React.memo(FilePreview); diff --git a/src/components/recruitments/apply/FileUploader.tsx b/src/components/recruitments/apply/FileUploader.tsx new file mode 100644 index 0000000..2f3033d --- /dev/null +++ b/src/components/recruitments/apply/FileUploader.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { Icon } from "@team-return/design-system"; +import { useEffect, useRef, useState } from "react"; +import FilePreview from "./FilePreview"; + +interface PropsType { + multiple?: boolean; + addFilesToRequest: (fileList: File[]) => void; + isBtnClicked: boolean; +} + +export default function FileUploader({ + multiple = false, + addFilesToRequest, + isBtnClicked, +}: PropsType) { + const [fileList, setFileList] = useState([]); + const [isHover, setIsHover] = useState(false); + const fileRef = useRef(null); + + const addFileList = () => { + if (fileRef.current?.files?.length) { + const files = Array.from(fileRef.current?.files); + setFileList((prev) => [...prev, ...files]); + } + }; + + const prependFileItem = (fileName: string) => { + setFileList((prev) => prev.filter((file) => file.name !== fileName)); + }; + + useEffect(() => { + if (isBtnClicked) addFilesToRequest(fileList); + }, [isBtnClicked]); + + return ( +
    + +
    + {fileList.map((file, idx) => ( + + ))} +
    +
    + ); +} diff --git a/src/components/recruitments/apply/Header_Contents.tsx b/src/components/recruitments/apply/Header_Contents.tsx new file mode 100644 index 0000000..c36010c --- /dev/null +++ b/src/components/recruitments/apply/Header_Contents.tsx @@ -0,0 +1,15 @@ +interface PropsType { + title: string; + children: React.ReactNode; +} + +export default function Header_Contents({ title, children }: PropsType) { + return ( +
    +

    + {title} +

    + {children} +
    + ); +} diff --git a/src/components/recruitments/apply/ShadowBox.tsx b/src/components/recruitments/apply/ShadowBox.tsx new file mode 100644 index 0000000..fdef8b8 --- /dev/null +++ b/src/components/recruitments/apply/ShadowBox.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +interface PropsType { + children: React.ReactNode; + width?: string; + height?: string; +} + +export default function ShadowBox({ children, width, height }: PropsType) { + return ( +
    + {children} +
    + ); +} diff --git a/src/components/recruitments/apply/TitleBox.tsx b/src/components/recruitments/apply/TitleBox.tsx new file mode 100644 index 0000000..e1df206 --- /dev/null +++ b/src/components/recruitments/apply/TitleBox.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +interface PropsType { + title: string; + children: React.ReactNode; +} + +export default function TitleBox({ title, children }: PropsType) { + return ( +
    +

    {title}

    +
    +
    {children}
    +
    + ); +} diff --git a/src/components/recruitments/apply/URLItem.tsx b/src/components/recruitments/apply/URLItem.tsx new file mode 100644 index 0000000..0f5dd68 --- /dev/null +++ b/src/components/recruitments/apply/URLItem.tsx @@ -0,0 +1,34 @@ +import { Icon } from "@team-return/design-system"; +import { useState } from "react"; + +interface PropsType { + url: string; + prepend?: () => void; +} + +export default function URLItem({ url, prepend }: PropsType) { + const [isHover, setIsHover] = useState(false); + return ( +
    { + setIsHover(true); + }} + onMouseLeave={() => { + setIsHover(false); + }} + className="flex w-full gap-5 bg-[#f7f7f7] rounded-[8px] py-[10px] px-[14px] items-center mt-2" + > +

    + {url} +

    + {isHover && prepend && ( +
    + +
    + )} +
    + ); +} diff --git a/src/components/recruitments/apply/UrlListComponent.tsx b/src/components/recruitments/apply/UrlListComponent.tsx new file mode 100644 index 0000000..f6371be --- /dev/null +++ b/src/components/recruitments/apply/UrlListComponent.tsx @@ -0,0 +1,74 @@ +import useForm from "@/hook/useForm"; +import { Icon } from "@team-return/design-system"; +import { useEffect, useState } from "react"; +import URLItem from "./URLItem"; + +interface PropsType { + addUrlsToRequest: (urlList: string[]) => void; + isBtnClicked: boolean; +} + +export default function UrlListComponent({ + addUrlsToRequest, + isBtnClicked, +}: PropsType) { + const { + state: urlState, + onChange, + setState: setUrlState, + } = useForm<{ url: string }>({ + url: "", + }); + const [urlList, setUrlList] = useState([]); + + useEffect(() => { + if (isBtnClicked) addUrlsToRequest(urlList); + }, [isBtnClicked]); + + const onEnterEvent = (event: React.KeyboardEvent) => { + if (event.key === "Enter") appendURL(); + }; + + const appendURL = () => { + if (urlState.url.replaceAll(" ", "")) { + setUrlList((prev) => [...prev, urlState.url]); + setUrlState({ url: "" }); + } + }; + + const prependURL = (itemIdx: number) => { + setUrlList((prev) => prev.filter((_, idx) => idx !== itemIdx)); + }; + + return ( +
    +
    + +
    + +
    +
    + {urlList.map((item, idx) => ( + { + prependURL(idx); + }} + /> + ))} +
    + ); +} diff --git a/src/components/recruitments/filter.tsx b/src/components/recruitments/filter.tsx index 8fc41c4..af05fbc 100644 --- a/src/components/recruitments/filter.tsx +++ b/src/components/recruitments/filter.tsx @@ -1,25 +1,30 @@ "use client"; -import { GetCode } from "@/apis/code"; +import { useGetCode } from "@/apis/code"; import useForm from "@/hook/useForm"; -import { useQueryString } from "@/hook/useQueryString"; -import React, { useEffect, useState } from "react"; +import { RecruitmentsQueryType } from "@/hook/useQueryString/type"; +import { useQueryString } from "@/hook/useQueryString/useQueryString"; +import { internDropdownItems } from "@/util/object/jobCodeDorpdownItems"; +import { useEffect, useState } from "react"; import DropDown from "../common/DropDown"; import SearchDropDown from "../common/SearchDropDown"; import TextFiled from "../common/TextFiled"; function Filter() { - const { setQueryString, getQueryString } = useQueryString({ - page: "1", - job_code: "", - tech_code: "", - name: "", - }); + const { setQueryString, getQueryString } = + useQueryString({ + page: "1", + job_code: "", + tech_code: "", + name: "", + winter_intern: "", + }); const [filter, setFilter] = useState({ page: getQueryString("page"), job_code: getQueryString("job_code"), tech_code: getQueryString("tech_code"), + winter_intern: getQueryString("winter_intern"), }); const { state: searchState, onChange: onChangeSearch } = useForm<{ search: string | undefined; @@ -33,28 +38,44 @@ function Filter() { job_code: filter.job_code, tech_code: filter.tech_code, name: searchState.search, + winter_intern: filter.winter_intern, }); }; useEffect(onSearch, [filter]); const onItemClick = ( - itemCode: string | number, - name: "job_code" | "tech_code" + jobType: "job_code" | "winter_intern", + itemCode: string ) => { - if (filter[name] === itemCode) { - setFilter((prev) => ({ ...prev, [name]: "" })); - } else setFilter((prev) => ({ ...prev, [name]: itemCode })); + if (filter[jobType] === itemCode) { + setFilter((prev) => ({ ...prev, [jobType]: "" })); + } else setFilter((prev) => ({ ...prev, [jobType]: itemCode })); }; - const { data } = GetCode("JOB"); + const { data:codes } = useGetCode("JOB"); + + const jobCodeDropdownItems = codes?.codes.map((item) => ({ + code: item.code.toString(), + label: item.keyword, + })); return (
    + { + onItemClick("winter_intern", itemCode); + }} + selected={getQueryString("winter_intern") || ""} + /> { + onItemClick("job_code", itemCode); + }} selected={getQueryString("job_code") || ""} /> @@ -65,9 +86,8 @@ function Filter() { name="search" customType="Search" enterEvent={onSearch} - width="26vw" + width="20vw" /> -
    ); } diff --git a/src/hook/useModal.tsx b/src/hook/useModal.tsx index 41d552c..58fd9bc 100644 --- a/src/hook/useModal.tsx +++ b/src/hook/useModal.tsx @@ -1,9 +1,13 @@ -import { useContext } from "react"; +import { useContext, useEffect } from "react"; import { ModalContext } from "@/context/ModalContext"; export default function useMoadl() { const { isOpen, setIsOpen } = useContext(ModalContext); + const preventScroll = (e: Event) => { + e.preventDefault(); + }; + const openModal = () => { setIsOpen(true); }; @@ -14,22 +18,40 @@ export default function useMoadl() { setIsOpen((prev) => !prev); }; + useEffect(() => { + if (isOpen) + document.addEventListener("wheel", preventScroll, { passive: false }); + return () => { + document.removeEventListener("wheel", preventScroll); + }; + }, [isOpen]); + return { Modal: ({ children, className, + backgroundClose = true, }: { className?: string; children: React.ReactNode | React.ReactNode[]; + backgroundClose?: boolean; }) => ( <> {isOpen && (
    e.stopPropagation()} - style={{ zIndex: 6, cursor: "default" }} + onClick={() => { + backgroundClose && closeModal(); + }} + className="top-0 left-0 w-screen h-screen fixed z-[6] flex justify-center items-center bg-[rgba(0,0,0,0.3)]" > - {children} +
    e.stopPropagation()} + > + {children} +
    )} diff --git a/src/hook/useQueryString/type.ts b/src/hook/useQueryString/type.ts new file mode 100644 index 0000000..5ca214e --- /dev/null +++ b/src/hook/useQueryString/type.ts @@ -0,0 +1,16 @@ +export interface QueryStringType { + [key: string]: string; +} + +export interface RecruitmentsQueryType extends QueryStringType { + page: string; + job_code: string; + tech_code: string; + name: string; + winter_intern: string; +} + +export interface CompaniesQueryType extends QueryStringType { + page: string; + name: string; +} diff --git a/src/hook/useQueryString.ts b/src/hook/useQueryString/useQueryString.ts similarity index 69% rename from src/hook/useQueryString.ts rename to src/hook/useQueryString/useQueryString.ts index 6444367..748c02c 100644 --- a/src/hook/useQueryString.ts +++ b/src/hook/useQueryString/useQueryString.ts @@ -2,19 +2,9 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; +import { QueryStringType } from "./type"; -interface QueryStringType { - [key: string]: string; -} - -export interface setQueryStringType { - page?: string; - job_code?: string; - tech_code?: string; - name?: string; -} - -export const useQueryString = (initialState: QueryStringType) => { +export const useQueryString = (initialState: T) => { const pathname = usePathname(); const router = useRouter(); const params = useSearchParams(); @@ -26,13 +16,14 @@ export const useQueryString = (initialState: QueryStringType) => { }, {} as QueryStringType) ); - const setQueryString = (newValue: setQueryStringType) => { + const setQueryString = (newValue: Partial) => { setValue((prev) => ({ ...prev, ...newValue })); }; const refresh = () => { const queryString = Object.entries(value) - .map(([key, val]) => `${key}=${val}`) + .map(([key, val]) => val && `${key}=${val}`) + .filter((item) => item) .join("&"); router.push(`${pathname}?${queryString}`); }; @@ -43,13 +34,16 @@ export const useQueryString = (initialState: QueryStringType) => { .join("&"); }; - const getQueryString = (key: keyof setQueryStringType) => { + const getQueryString = (key: string) => { return value[key]; }; - const getQueryStringEntry = (key: keyof setQueryStringType) => { - return `${Object.keys(value).find((objectKey) => objectKey === key)}=${ - value[key] - }`; + const getQueryStringEntry = (key: string) => { + return ( + value[key] && + `${Object.keys(value).find((objectKey) => objectKey === key)}=${ + value[key] + }` + ); }; const resetQueryString = () => { diff --git a/src/util/enum.ts b/src/util/enum.ts deleted file mode 100644 index 5628a90..0000000 --- a/src/util/enum.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum hiringProgressEnum { - CULTURE_INTERVIEW = "컬쳐면접", - DOCUMENT = "서류전형", - TASK = "과제제출", - LIVE_CODING = "라이브코딩", - TECH_INTERVIEW = "기술면접", - FINAL_INTERVIEW = "최종면접", - PERSONALITY = "인적성 테스트", - AI = "AI면접", - CODING_TEST = "코딩테스트", -} diff --git a/src/util/object/enum.ts b/src/util/object/enum.ts new file mode 100644 index 0000000..e41a2cf --- /dev/null +++ b/src/util/object/enum.ts @@ -0,0 +1,34 @@ +export const hiringProgressEnum = { + CULTURE_INTERVIEW: "컬쳐면접", + DOCUMENT: "서류전형", + TASK: "과제제출", + LIVE_CODING: "라이브코딩", + TECH_INTERVIEW: "기술면접", + FINAL_INTERVIEW: "최종면접", + PERSONALITY: "인적성 테스트", + AI: "AI면접", + CODING_TEST: "코딩테스트", +}; + +export const departmentEnum = { + SOFTWARE_DEVELOP: "소프트웨어개발과", + EMBEDDED_SOFTWARE: "임베디드소프트웨어과", + INFORMATION_SECURITY: "정보보안과", + AI_SOFTWARE: "인공지능스프트웨어과", +}; + +export const applicationEnum = { + REQUESTED: "승인요청", + APPROVED: "승인됨", + FAILED: "불합격", + PASS: "합격", + REJECTED: "반려", +}; + +export const applicationStatusStyle = { + REQUESTED: { color: "#7F7F7F", backgroundColor: "#F7F7F7" }, + APPROVED: { color: "#2ECC71", backgroundColor: "#E6F8EE" }, + FAILED: { color: "#E74C3C", backgroundColor: "#FCE9E7" }, + PASS: { color: "#237BC9", backgroundColor: "#E4EFF8" }, + REJECTED: { color: "#F1C40F", backgroundColor: "#FDF8E2" }, +}; diff --git a/src/util/object/jobCodeDorpdownItems.ts b/src/util/object/jobCodeDorpdownItems.ts new file mode 100644 index 0000000..1becf60 --- /dev/null +++ b/src/util/object/jobCodeDorpdownItems.ts @@ -0,0 +1,10 @@ +export const internDropdownItems = [ + { + code: "true", + label: "채험형", + }, + { + code: "false", + label: "채용형", + }, +]; diff --git a/src/util/object/kebabMenuItems.ts b/src/util/object/kebabMenuItems.ts new file mode 100644 index 0000000..4b3fe6e --- /dev/null +++ b/src/util/object/kebabMenuItems.ts @@ -0,0 +1,71 @@ +import { useToastStore } from "@team-return/design-system"; +import { useRouter } from "next/navigation"; +import { Cookies } from "react-cookie"; +import { KebabItemType } from "../type/kebabMenu"; + +export const getMypageKebabItems = (): KebabItemType[] => { + const { append } = useToastStore(); + const navigator = useRouter(); + const cookies = new Cookies(); + return [ + { + label: "프로필 수정", + onClick: () => { + append({ + title: "", + message: "개발 중인 기능입니다.", + type: "YELLOW", + }); + }, + }, + { + label: "비밀변호 변경", + onClick: () => { + append({ + title: "", + message: "개발 중인 기능입니다.", + type: "YELLOW", + }); + }, + }, + { + label: "버그 제보하기", + onClick: () => { + append({ + title: "", + message: "개발 중인 기능입니다.", + type: "YELLOW", + }); + }, + }, + { + label: "로그아웃", + onClick: () => { + console.log("로그아웃"); + cookies.remove("access_token", { path: "/" }); + cookies.remove("refresh_token", { path: "/" }); + navigator.push("/account/login"); + }, + }, + ]; +}; + +export const getCompanyKebabItems = ( + onClickRecruitments?: () => void, + onClickInterview?: () => void +): KebabItemType[] => { + return [ + { + label: "모집의뢰서 조회", + onClick: () => { + onClickRecruitments && onClickRecruitments(); + }, + }, + { + label: "면접 후기 조회", + onClick: () => { + onClickInterview && onClickInterview(); + }, + }, + ]; +}; diff --git a/src/util/regex.ts b/src/util/regex.ts index cc0f208..91ca374 100644 --- a/src/util/regex.ts +++ b/src/util/regex.ts @@ -21,3 +21,7 @@ export const time_parsing = (time: string) => { export const money_regex = (money: number) => { return money.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }; + +export const file_name_regex = (url: string) => { + return url.replace(/(?:.*?-){5}(.*)/, "$1").replaceAll("+", " "); +}; diff --git a/src/util/type/kebabMenu.ts b/src/util/type/kebabMenu.ts new file mode 100644 index 0000000..1f0a2bd --- /dev/null +++ b/src/util/type/kebabMenu.ts @@ -0,0 +1,4 @@ +export interface KebabItemType { + label: string; + onClick: () => void; +} diff --git a/src/util/type.ts b/src/util/type/type.ts similarity index 99% rename from src/util/type.ts rename to src/util/type/type.ts index feeeb07..ebe552c 100644 --- a/src/util/type.ts +++ b/src/util/type/type.ts @@ -3,3 +3,4 @@ export interface TechCodeResponensType { keyword: string; job_type: string | null; } + diff --git a/tailwind.config.js b/tailwind.config.js index c56c1a8..428b0c6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -3,6 +3,29 @@ module.exports = { content: ["./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: { + keyframes: { + skeleton: { + "0%": { transform: "translateX(-150%)" }, + "50%": { transform: "translateX(-60%)" }, + "100%": { transform: "translateX(150%)" }, + }, + loading: { + "0%": { transform: "rotate(0deg)" }, + "12.5%": { transform: "rotate(15deg)" }, + "25%": { transform: "rotate(45deg)" }, + "37.5%": { transform: "rotate(90deg)" }, + "50%": { transform: "rotate(165deg)" }, + "62.5%": { transform: "rotate(255deg)" }, + "75%": { transform: "rotate(315deg)" }, + "87.5%": { transform: "rotate(345deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + }, + animation: { + skeleton: "skeleton 2.5s linear infinite", + loading: "loading 1s linear infinite", + }, + boxShadow: { elevaiton: "0px 4px 20px 0px rgba(112, 144, 176, 0.15);", }, diff --git a/yarn.lock b/yarn.lock index a4d3cdc..1f0cc24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1906,15 +1906,15 @@ __metadata: languageName: node linkType: hard -"@team-return/design-system@npm:^1.1.8": - version: 1.1.8 - resolution: "@team-return/design-system@npm:1.1.8" +"@team-return/design-system@npm:^1.1.15": + version: 1.1.15 + resolution: "@team-return/design-system@npm:1.1.15" dependencies: react: ^18.2.0 react-dom: ^18.2.0 uuid: ^9.0.0 zustand: ^4.3.7 - checksum: d87866fa3af2ef7a68269709e01923ab67a323ee887249b74323927dcf0df3e543dea8bc16125ed791b93613705c18f7e04d39805ee053aa1e59d6b0b0c0bce8 + checksum: de878fb3ac32cc7faf19b91fe4172bbb416a99fd16924d540924149f1359698adf167d6fd606ac70987e7814595d0a5f9acaf93e7d5fe4dc85e37f6018f6b2f0 languageName: node linkType: hard @@ -5309,7 +5309,7 @@ __metadata: dependencies: "@next/font": ^13.5.2 "@tanstack/react-query": ^4.33.0 - "@team-return/design-system": ^1.1.8 + "@team-return/design-system": ^1.1.15 "@testing-library/jest-dom": ^5.16.5 "@types/debug": ^4 "@types/eslint": ^8