diff --git a/package-lock.json b/package-lock.json index b5b2337..836fbc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@mui/material": "^5.15.17", "@mui/x-date-pickers": "^7.4.0", "@react-oauth/google": "^0.12.1", + "@tanstack/react-query": "^5.37.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -4263,6 +4264,30 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tanstack/query-core": { + "version": "5.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.36.1.tgz", + "integrity": "sha512-BteWYEPUcucEu3NBcDAgKuI4U25R9aPrHSP6YSf2NvaD2pSlIQTdqOfLRsxH9WdRYg7k0Uom35Uacb6nvbIMJg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.37.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.37.1.tgz", + "integrity": "sha512-EhtBNA8GL3XFeSx6VYUjXQ96n44xe3JGKZCzBINrCYlxbZP6UwBafv7ti4eSRWc2Fy+fybQre0w17gR6lMzULA==", + "dependencies": { + "@tanstack/query-core": "5.36.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz", @@ -22804,6 +22829,19 @@ "loader-utils": "^2.0.0" } }, + "@tanstack/query-core": { + "version": "5.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.36.1.tgz", + "integrity": "sha512-BteWYEPUcucEu3NBcDAgKuI4U25R9aPrHSP6YSf2NvaD2pSlIQTdqOfLRsxH9WdRYg7k0Uom35Uacb6nvbIMJg==" + }, + "@tanstack/react-query": { + "version": "5.37.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.37.1.tgz", + "integrity": "sha512-EhtBNA8GL3XFeSx6VYUjXQ96n44xe3JGKZCzBINrCYlxbZP6UwBafv7ti4eSRWc2Fy+fybQre0w17gR6lMzULA==", + "requires": { + "@tanstack/query-core": "5.36.1" + } + }, "@testing-library/dom": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz", diff --git a/package.json b/package.json index 1e7c588..da6509e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@mui/material": "^5.15.17", "@mui/x-date-pickers": "^7.4.0", "@react-oauth/google": "^0.12.1", + "@tanstack/react-query": "^5.37.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/src/components/Experience/KeywordTab.tsx b/src/components/Experience/KeywordTab.tsx index fa93bda..87fc25b 100644 --- a/src/components/Experience/KeywordTab.tsx +++ b/src/components/Experience/KeywordTab.tsx @@ -34,8 +34,6 @@ import ExpData from "../../services/JD/ExpData"; import editIcon from "../../assets/images/editIcon.png"; import { useNavigate } from "react-router-dom"; import { - deleteTag, - getPrimeTagSubTags, getPrimeTagYears, } from "../../services/Experience/tagApi"; import { getCookie } from "../../services/cookie"; @@ -47,6 +45,7 @@ import { } from "../../types/experience"; import { getExperienceList } from "../../services/Experience/experienceApi"; import { getKeywords } from "../../services/Experience/keywordApi"; +import { useSubTagsQuery } from "../hooks/useSubTagsQuery"; type TabType = "basic" | "my"; interface KeywordTabProp { @@ -59,9 +58,16 @@ const KeywordTab = ({ openDeleteModal }: KeywordTabProp) => { const navigate = useNavigate(); const [selectedYear, setSelectedYear] = useRecoilState(yearState); const [isDelete, setIsDelete] = useRecoilState(deleteState); - const [selectedDeleteTag, setSelectedDeleteTag] = useRecoilState(deleteTagState); + const [selectedDeleteTag, setSelectedDeleteTag] = + useRecoilState(deleteTagState); const [selectedPrimeTag, setSelectedPrimeTag] = useRecoilState(primeTagState); const [selectedSubTag, setSelectedSubTag] = useRecoilState(subTagState); + + const { data: subTagsData, isSuccess: isSubTagsSuccess } = useSubTagsQuery( + selectedYear, + selectedPrimeTag?.id, + user?.token + ); const [selectedQ, setSelectedQ] = React.useState(0); const [expanded, setExpanded] = React.useState(false); // 질문 아코디언 관리 const [keywordTabOption, setKeywordTabOption] = @@ -188,21 +194,21 @@ const KeywordTab = ({ openDeleteModal }: KeywordTabProp) => { // 상위태그 내 하위태그 목록 조회 (메뉴) React.useEffect(() => { - if (selectedYear && selectedPrimeTag && user?.token) { - getPrimeTagSubTags(selectedYear, selectedPrimeTag.id, user?.token).then( - (res) => { - const { totalExperienceCount, tagInfos } = res.data; - setTotalExpCount(totalExperienceCount); - setSubTagMenus(tagInfos); - setSelectedSubTag(null); - } - ); + if (isSubTagsSuccess && subTagsData) { + const { totalExperienceCount, tagInfos } = subTagsData; + setTotalExpCount(totalExperienceCount); + setSubTagMenus(tagInfos); + setSelectedSubTag(null); + // 하위 태그 목록이 없을 경우 사이드 탭 닫히는 로직 + if (tagInfos.length === 0) { + setSelectedPrimeTag(null); + } } - }, [selectedYear, selectedPrimeTag, user?.token]); + }, [isSubTagsSuccess, subTagsData]); // 상위태그 연도 리스트 조회 React.useEffect(() => { - if (selectedPrimeTag && user?.token) { + if (selectedPrimeTag && selectedPrimeTag.id !== "더보기" && user?.token) { getPrimeTagYears(selectedPrimeTag.id, user?.token).then((res) => setPrimeTagYears(res.data.years) ); diff --git a/src/components/Experience/YearList.tsx b/src/components/Experience/YearList.tsx index b36760c..edbe90f 100644 --- a/src/components/Experience/YearList.tsx +++ b/src/components/Experience/YearList.tsx @@ -8,9 +8,9 @@ import { primeTagState, yearState, } from "../../store/selectedStore"; -import { getExperienceYears } from "../../services/Experience/experienceApi"; import { getCookie } from "../../services/cookie"; import { TagType, YearData } from "../../types/experience"; +import { useExperienceYearsQuery } from "../hooks/useExperienceYearsQuery"; interface YearListProps { width: number; @@ -25,9 +25,21 @@ const YearList = ({ width, openDeleteModal }: YearListProps) => { const setSelectedPrimeTag = useSetRecoilState(primeTagState); const [isDelete, setIsDelete] = useRecoilState(deleteState); const [years, setYears] = React.useState([]); - const [allYearsData, setAllYearsData] = React.useState([]); - + const [allTagsData, setAllTagsData] = React.useState([]); const [hoveredYear, setHoveredYear] = useState(null); + const { data: allYearsData, isSuccess: isAllYearsDataSucces } = + useExperienceYearsQuery(user?.token); + + // 상위태그 목록이 없을 경우 year 원 닫히는 로직 + if (allYearsData) { + const { yearTagInfos } = allYearsData; + const selectedYearPrimeTags = yearTagInfos?.filter( + (item) => item.year === selectedYear + )?.[0]?.tags; + if (selectedYearPrimeTags?.length === 0) { + setSelectedYear(null); + } + } // 클릭한 year 객체로 스크롤 이동하기 위한 객체 참조 값 const yearRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}); @@ -95,15 +107,13 @@ const YearList = ({ width, openDeleteModal }: YearListProps) => { }; // 전체 연도 및 연도별 태그 목록 정보 - useEffect(() => { - if (user?.token) { - getExperienceYears(user?.token).then((res) => { - const { years, yearTagInfos } = res.data; - setYears(years); - setAllYearsData(yearTagInfos); - }); + React.useEffect(() => { + if (isAllYearsDataSucces && allYearsData) { + const { years, yearTagInfos } = allYearsData; + setYears(years); + setAllTagsData(yearTagInfos); } - }, [user?.token]); + }, [isAllYearsDataSucces, allYearsData]); // 클릭한 연도 원형 중심으로 스크롤 이동 useEffect(() => { @@ -119,7 +129,7 @@ const YearList = ({ width, openDeleteModal }: YearListProps) => { // return ( - {allYearsData?.map((data, index) => ( + {allTagsData?.map((data, index) => ( (yearRefs.current[data.year] = el)} key={data.year} diff --git a/src/components/common/Navbar.tsx b/src/components/common/Navbar.tsx index 9c92af4..51afb34 100644 --- a/src/components/common/Navbar.tsx +++ b/src/components/common/Navbar.tsx @@ -3,11 +3,13 @@ import styled from "styled-components"; import logo from "../../assets/images/logo.png"; import { useLocation, useNavigate } from "react-router-dom"; import { getCookie } from "../../services/cookie"; +import { useGetUserInfo } from "../hooks/useGetUserInfo"; const Navbar = () => { const navigate = useNavigate(); const location = useLocation(); const user = getCookie("user"); + const { data: userData } = useGetUserInfo(user?.token); const handleImgError = (e: React.SyntheticEvent) => { e.currentTarget.src = `${process.env.PUBLIC_URL}/assets/profile1.png`; @@ -39,6 +41,7 @@ const Navbar = () => { <> { + const queryKey = ["experienceYears", token]; + + const queryFn = async (): Promise => { + if (token) { + const response: AxiosResponse = + await getExperienceYears(token); + return response.data; + } + throw new Error("Missing parameters"); + }; + + const { isLoading, isError, data, error, isSuccess, refetch } = useQuery< + ExperienceYearsResType, + AxiosError + >({ + queryKey, + queryFn, + enabled: !!token, + }); + + return { isLoading, isError, data, error, isSuccess, refetch }; +}; diff --git a/src/components/hooks/useGetUserInfo.tsx b/src/components/hooks/useGetUserInfo.tsx new file mode 100644 index 0000000..2295d93 --- /dev/null +++ b/src/components/hooks/useGetUserInfo.tsx @@ -0,0 +1,27 @@ +import { useQuery } from "@tanstack/react-query"; +import { AxiosError, AxiosResponse } from "axios"; +import { UserDataType } from "../../types/user"; +import { getUserInfo } from "../../services/user"; + +export const useGetUserInfo = (token: string) => { + const queryKey = ["userInfo", token]; + + const queryFn = async (): Promise => { + if (token) { + const response: AxiosResponse = await getUserInfo(token); + return response.data; + } + throw new Error("Missing parameters"); + }; + + const { isLoading, isError, data, error, isSuccess, refetch } = useQuery< + UserDataType, + AxiosError + >({ + queryKey, + queryFn, + enabled: !!token, + }); + + return { isLoading, isError, data, error, isSuccess, refetch }; +}; diff --git a/src/components/hooks/useSubTagsQuery.tsx b/src/components/hooks/useSubTagsQuery.tsx new file mode 100644 index 0000000..eaac077 --- /dev/null +++ b/src/components/hooks/useSubTagsQuery.tsx @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query"; +import { getPrimeTagSubTags } from "../../services/Experience/tagApi"; +import { TagMenuType } from "../../types/experience"; +import { AxiosError, AxiosResponse } from "axios"; + +interface PrimeTagSubTagsResType { + totalExperienceCount: number; + tagInfos: TagMenuType[]; +} + +export const useSubTagsQuery = ( + year: number | null, + primeTagId: string | undefined, + token: string +) => { + const queryKey = ["primeTagSubTags", year, primeTagId, token]; + + const queryFn = async (): Promise => { + if (year && primeTagId && token) { + const response: AxiosResponse = + await getPrimeTagSubTags(year, primeTagId, token); + return response.data; + } + throw new Error("Missing parameters"); + }; + + const { isLoading, isError, data, error, isSuccess, refetch } = useQuery< + PrimeTagSubTagsResType, + AxiosError + >({ + queryKey, + queryFn, + enabled: !!(year && primeTagId && token), + }); + + return { isLoading, isError, data, error, isSuccess, refetch }; +}; diff --git a/src/index.tsx b/src/index.tsx index 18b7a32..2d49c38 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,20 +8,24 @@ import { BrowserRouter } from "react-router-dom"; import { GoogleOAuthProvider } from "@react-oauth/google"; import { ThemeProvider } from "styled-components"; import { theme } from "./styles/theme"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +const queryClient = new QueryClient(); const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); root.render( - - - - - - - + + + + + + + + + ); diff --git a/src/pages/ExperiencePage.tsx b/src/pages/ExperiencePage.tsx index 9979a5f..1e8eba2 100644 --- a/src/pages/ExperiencePage.tsx +++ b/src/pages/ExperiencePage.tsx @@ -20,6 +20,8 @@ import React from "react"; import warningImg from "../assets/images/warningIcon.png"; import { getCookie } from "../services/cookie"; import { deleteTag } from "../services/Experience/tagApi"; +import { useSubTagsQuery } from "../components/hooks/useSubTagsQuery"; +import { useExperienceYearsQuery } from "../components/hooks/useExperienceYearsQuery"; const ExperiencePage = () => { const user = getCookie("user"); @@ -29,6 +31,13 @@ const ExperiencePage = () => { const [selectedSubTag, setSelectedSubTag] = useRecoilState(subTagState); const [selectedDeleteTag, setSelectedDeleteTag] = useRecoilState(deleteTagState); + const { refetch: refetchSubTags } = useSubTagsQuery( + selectedYear, + selectedPrimeTag?.id, + user?.token + ); + const { data: allYearsData, refetch: refetchAllYears } = + useExperienceYearsQuery(user?.token); const navigate = useNavigate(); const [isModalOpen, setIsModalOpen] = React.useState(false); @@ -44,12 +53,32 @@ const ExperiencePage = () => { const handleDelete = () => { if (selectedDeleteTag && user?.token) { deleteTag(selectedDeleteTag.id, user?.token).then((res) => { - if (selectedDeleteTag.id === selectedPrimeTag?.id) { - setSelectedPrimeTag({ id: "더보기", name: "더보기" }); - } else if (selectedDeleteTag.id === selectedSubTag?.id) { - setSelectedSubTag(null); + if (allYearsData) { + const { yearTagInfos } = allYearsData; + const selectedYearPrimeTags = yearTagInfos.filter( + (item) => item.year === selectedYear + )[0].tags; + console.log("ggg", selectedYearPrimeTags); + // 선택된 상위태그가 삭제될 경우 + if (selectedDeleteTag.id === selectedPrimeTag?.id) { + // 상위태그 1개만 있는 경우 + if (selectedYearPrimeTags.length <= 1) { + setSelectedYear(null); + } + setSelectedPrimeTag(null); + // // 상위태그가 2개 이상 있는 경우 => 다음 상위 태그 선택 + // else { + // setSelectedPrimeTag(selectedYearPrimeTags[0]); + // } + } + // 선택된 하위태그가 삭제될 경우 + else if (selectedDeleteTag.id === selectedSubTag?.id) { + setSelectedSubTag(null); + } + refetchSubTags(); + refetchAllYears(); + closeDeleteModal(); } - closeDeleteModal(); }); } }; diff --git a/src/pages/ProfileEditPage.tsx b/src/pages/ProfileEditPage.tsx index 946f51b..3ae7307 100644 --- a/src/pages/ProfileEditPage.tsx +++ b/src/pages/ProfileEditPage.tsx @@ -1,4 +1,3 @@ -import React from "react"; import styled from "styled-components"; import Input from "../components/common/Input"; import Textarea from "../components/common/Textarea"; @@ -9,16 +8,38 @@ import profile2 from "../assets/images/profile2.png"; import profile3 from "../assets/images/profile3.png"; import profile4 from "../assets/images/profile4.png"; import profile5 from "../assets/images/profile5.png"; -import { Popper } from "@mui/material"; +import React from "react"; import { jobStateOptions } from "../assets/data/form"; +import { Popper } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { getUserInfo, patchUserInfo } from "../services/user"; +import { UserDataType } from "../types/user"; +import { getCookie } from "../services/cookie"; +import { useGetUserInfo } from "../components/hooks/useGetUserInfo"; + +const profileImgUrl = [ + "/assets/profile1.png", + "/assets/profile2.png", + "/assets/profile3.png", + "/assets/profile4.png", + "/assets/profile5.png", +]; + +type ProfileUserType = Omit; const ProfileEditPage = () => { - const [profile, setProfile] = React.useState(null); - const [nickname, setNickname] = React.useState(""); - const [jobState, setJobState] = React.useState(""); - const [jobs, setJobs] = React.useState(""); - const [abilities, setAbilities] = React.useState(""); - const [dreams, setDreams] = React.useState(""); + const navigate = useNavigate(); + const user = getCookie("user"); + + const [userData, setUserData] = React.useState({ + profileImgUrl: "", + nickName: "", + jobSearchStatus: "", + desiredJob: "", + goal: "", + dream: "", + }); + const { refetch } = useGetUserInfo(user?.token); const [anchorEl, setAnchorEl] = React.useState(null); // 팝업 위치 관리 const [popperWidth, setPopperWidth] = React.useState(0); @@ -31,14 +52,44 @@ const ProfileEditPage = () => { }; const handleOptionClick = (item: string) => { - setJobState(item); + setUserData({ ...userData, jobSearchStatus: item }); setAnchorEl(null); }; + const handleEditProfile = () => { + patchUserInfo(userData, user?.token).then(() => { + refetch(); + navigate(`/profile`); + }); + }; + + React.useEffect(() => { + if (user?.token) { + getUserInfo(user?.token).then((res) => { + const { + nickName, + profileImgUrl, + jobSearchStatus, + desiredJob, + goal, + dream, + } = res.data; + setUserData({ + nickName, + profileImgUrl, + jobSearchStatus, + desiredJob, + goal, + dream, + }); + }); + } + }, [user?.token]); + return ( <> - 프로필 수정 + 회원가입
@@ -46,53 +97,95 @@ const ProfileEditPage = () => {
setProfile(1)} + className={ + userData.profileImgUrl === profileImgUrl[0] ? "active" : "" + } + onClick={() => + setUserData({ + ...userData, + profileImgUrl: profileImgUrl[0], + }) + } > profile1
setProfile(2)} + className={ + userData.profileImgUrl === profileImgUrl[1] ? "active" : "" + } + onClick={() => + setUserData({ + ...userData, + profileImgUrl: profileImgUrl[1], + }) + } > profile2
setProfile(3)} + className={ + userData.profileImgUrl === profileImgUrl[2] ? "active" : "" + } + onClick={() => + setUserData({ + ...userData, + profileImgUrl: profileImgUrl[2], + }) + } > profile3
setProfile(4)} + className={ + userData.profileImgUrl === profileImgUrl[3] ? "active" : "" + } + onClick={() => + setUserData({ + ...userData, + profileImgUrl: profileImgUrl[3], + }) + } > profile4
setProfile(5)} + className={ + userData.profileImgUrl === profileImgUrl[4] ? "active" : "" + } + onClick={() => + setUserData({ + ...userData, + profileImgUrl: profileImgUrl[4], + }) + } > profile5
setNickname(e.target.value)} + onChange={(e) => + setUserData({ ...userData, nickName: e.target.value }) + } style={{ background: theme.colors.neutral100 }} /> setJobState(e.target.value)} + onChange={(e) => + setUserData({ + ...userData, + jobSearchStatus: e.target.value, + }) + } onClick={handleTagPopper} readOnly style={{ background: theme.colors.neutral100 }} @@ -120,34 +213,47 @@ const ProfileEditPage = () => { setJobs(e.target.value)} + onChange={(e) => + setUserData({ ...userData, desiredJob: e.target.value }) + } style={{ background: theme.colors.neutral100 }} />