diff --git a/package-lock.json b/package-lock.json index 845de5a..d68b081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react-kakao-maps-sdk": "^1.1.27", "react-loading-skeleton": "^3.5.0", "react-router-dom": "^6.28.0", + "react-toastify": "^10.0.6", "sockjs-client": "^1.6.1", "styled-components": "^6.1.13", "styled-normalize": "^8.1.1", @@ -2809,6 +2810,15 @@ "node": ">= 16" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5694,6 +5704,19 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", + "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", diff --git a/package.json b/package.json index c467469..690114b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-kakao-maps-sdk": "^1.1.27", "react-loading-skeleton": "^3.5.0", "react-router-dom": "^6.28.0", + "react-toastify": "^10.0.6", "sockjs-client": "^1.6.1", "styled-components": "^6.1.13", "styled-normalize": "^8.1.1", diff --git a/src/apis/loginService.ts b/src/apis/loginService.ts index e8228e0..8d125fb 100644 --- a/src/apis/loginService.ts +++ b/src/apis/loginService.ts @@ -1,11 +1,16 @@ import fetchApi from '@apis/ky' +import { useUserStore } from '@store/useUserStore' // 로그인 API 응답 타입 정의 interface LoginResponseData { - memberId: number - grantType: string accessToken: string + age: number + gender: string + grantType: string + memberId: number + nickname: string refreshToken: string + teamId: number } interface LoginResponse { @@ -16,6 +21,9 @@ interface LoginResponse { code: number } +const { setAge, setGender, setMemberId, setNickname, setTeamId } = + useUserStore.getState() + // 로그인 요청 함수 export const loginPost = async (email: string): Promise => { try { @@ -37,6 +45,15 @@ export const loginPost = async (email: string): Promise => { // localStorage.setItem('refreshToken', refreshToken); console.log('로그인 성공:', response.data) + const { age, gender, memberId, nickname, teamId } = response.data + + // 상태 업데이트 + setAge(age) + setGender(gender) + setMemberId(memberId) + setNickname(nickname) + setTeamId(teamId) + return response.data // `data` 객체를 반환 } catch (error) { console.error('로그인 실패:', error) diff --git a/src/hooks/useEditMyInfo.ts b/src/hooks/useEditMyInfo.ts index 7fb698d..1ef2010 100644 --- a/src/hooks/useEditMyInfo.ts +++ b/src/hooks/useEditMyInfo.ts @@ -1,12 +1,16 @@ import queryClient, { QUERY_KEY } from '@apis/queryClient' import userService from '@apis/userService' +import { ROUTE_PATH } from '@constants/ROUTE_PATH' import { useMutation } from '@tanstack/react-query' +import { useNavigate } from 'react-router-dom' +import { toast } from 'react-toastify' const useEditMyInfo = () => { - const { mutate, isPending, isError, error } = useMutation({ + const { mutate, isPending, isError, error, isSuccess } = useMutation({ mutationFn: (formData: FormData) => userService.editMyInfo(formData), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY.MY_INFO, 1] }) + toast('정보 수정이 완료되었습니다.') }, onSettled: (data, error) => { console.log(data, error) @@ -16,7 +20,7 @@ const useEditMyInfo = () => { }, }) - return { mutateMyInfo: mutate, isPending, isError, error } + return { mutateMyInfo: mutate, isPending, isError, error, isSuccess } } export default useEditMyInfo diff --git a/src/layouts/GlobalNav/index.tsx b/src/layouts/GlobalNav/index.tsx index 1e71ec8..26e80f5 100644 --- a/src/layouts/GlobalNav/index.tsx +++ b/src/layouts/GlobalNav/index.tsx @@ -12,9 +12,18 @@ import UserIcon from '@assets/icon/nav_user.svg?react' import UserIconFill from '@assets/icon/nav_user_fill.svg?react' import { Link, useLocation } from 'react-router-dom' import { ROUTE_PATH } from '@constants/ROUTE_PATH' +import { useEffect, useState } from 'react' +import { useUserStore } from '@store/useUserStore' const GlobalNav = () => { const { pathname } = useLocation() + const [isLogin, setIsLogin] = useState(false) + const { memberId } = useUserStore().userInfo + + useEffect(() => { + const token = localStorage.getItem('token') + setIsLogin(!!token) + }, [pathname]) return ( @@ -56,10 +65,25 @@ const GlobalNav = () => { - - {pathname === ROUTE_PATH.PROFILE ? : } - 마이 - + {isLogin ? ( + + {pathname === ROUTE_PATH.PROFILE ? ( + + ) : ( + + )} + 마이 + + ) : ( + + {pathname === ROUTE_PATH.PROFILE ? ( + + ) : ( + + )} + 로그인 + + )} diff --git a/src/main.tsx b/src/main.tsx index b6109bd..cd234b0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,8 +5,10 @@ import { GlobalStyle } from '@styles/globalStyle' import { ThemeProvider } from 'styled-components' import { theme } from '@styles/theme' import queryClient from '@apis/queryClient' -import App from './App' import { SkeletonTheme } from 'react-loading-skeleton' +import { ToastContainer } from 'react-toastify' +import App from './App' +import 'react-toastify/ReactToastify.css' createRoot(document.getElementById('root')!).render( @@ -16,6 +18,11 @@ createRoot(document.getElementById('root')!).render( baseColor='#313131' highlightColor='#525252' > + diff --git a/src/pages/GoodsRecordPage/index.tsx b/src/pages/GoodsRecordPage/index.tsx index 4226462..a8acf4a 100644 --- a/src/pages/GoodsRecordPage/index.tsx +++ b/src/pages/GoodsRecordPage/index.tsx @@ -1,8 +1,9 @@ import SubHeader from '@layouts/SubHeader' import GoodsRecordBox from './GoodsRecordBox' import { GoodsSection, NoGoodsList } from './style' -import { useEffect, useRef, useState } from 'react' -import { useLocation } from 'react-router-dom' + +import { useEffect, useState } from 'react' +import { useLocation, useParams } from 'react-router-dom' import { useInfiniteQuery } from '@tanstack/react-query' import { QUERY_KEY } from '@apis/queryClient' import userService from '@apis/userService' @@ -26,6 +27,7 @@ interface GoodsRecord { const GoodsRecordPage = () => { const location = useLocation() + const { id } = useParams() const [memberId, setMemberId] = useState(1) const [pageType, setPageType] = useState<('sold' | 'bought') | null>( location.state.type, @@ -39,7 +41,7 @@ const GoodsRecordPage = () => { queryFn: ({ pageParam }) => pageType && - userService.getGoodsRecordList(memberId, pageType, pageParam), + userService.getGoodsRecordList(Number(id), pageType, pageParam), initialPageParam: 0, diff --git a/src/pages/ProfilePage/ProfileEdit.tsx b/src/pages/ProfilePage/ProfileEdit.tsx index 7d11b53..dcfb99f 100644 --- a/src/pages/ProfilePage/ProfileEdit.tsx +++ b/src/pages/ProfilePage/ProfileEdit.tsx @@ -2,6 +2,7 @@ import { ProfileEditInputWrap, ProfileImageEdit, ProfileImageEditWrap, + ProfileSpinnerWrap, } from './style' import { FormEvent, useEffect, useState } from 'react' import { handleImageUpload } from './methods' @@ -13,15 +14,23 @@ import useTeamDialog from '@hooks/useTeamDialog' import BottomModalOption from './BottomModalOption' import SubHeader from '@layouts/SubHeader' import useEditMyInfo from '@hooks/useEditMyInfo' -import { useLocation } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import { UserInfo } from '@typings/userForm' import { kboTeamInfo } from '@constants/kboInfo' +import { toast } from 'react-toastify' +import { ROUTE_PATH } from '@constants/ROUTE_PATH' +import Spinner from '@components/Spinner' +import { useUserStore } from '@store/useUserStore' // 소개글 글자제한 const MAX_LENGTH = 500 const ProfileEdit = () => { const location = useLocation() + const navigate = useNavigate() + + const { memberId } = useUserStore().userInfo + const [isUpload, setIsUpload] = useState(false) const [currentTeamId, setCurrentTeamId] = useState(null) const [profileImg, setProfileImg] = useState(undefined) @@ -30,7 +39,7 @@ const ProfileEdit = () => { undefined, ) - const { mutateMyInfo, error, isError, isPending } = useEditMyInfo() + const { mutateMyInfo, error, isError, isPending, isSuccess } = useEditMyInfo() // 프로파일 수정 사항 서브밋 const onProfileEditSubmit = (e: FormEvent) => { @@ -51,7 +60,10 @@ const ProfileEdit = () => { ) profileImg && formData.append('image', profileImg) - mutateMyInfo(formData) + try { + mutateMyInfo(formData) + navigate(`${ROUTE_PATH.PROFILE}/${memberId}`) + } catch (err) {} } // 소개글 글자제한 @@ -95,6 +107,11 @@ const ProfileEdit = () => { <> {/* 폼 (내용) */}
+ {isPending ? ( + + + + ) : null} {/* 서브헤더 */} { }} >

- {userInfo?.aboutMe.length}/{MAX_LENGTH} + {userInfo?.aboutMe ? userInfo?.aboutMe.length : '0'}/{MAX_LENGTH}

diff --git a/src/pages/ProfilePage/ProfileMain.tsx b/src/pages/ProfilePage/ProfileMain.tsx index ca78f32..6db19f5 100644 --- a/src/pages/ProfilePage/ProfileMain.tsx +++ b/src/pages/ProfilePage/ProfileMain.tsx @@ -21,12 +21,14 @@ import SubHeader from '@layouts/SubHeader' import { ROUTE_PATH } from '@constants/ROUTE_PATH' import useGetUserInfo from '@hooks/useGetUserInfo' -import { Link, useNavigate } from 'react-router-dom' +import { Link, useNavigate, useParams } from 'react-router-dom' import { useEffect, useState, useRef } from 'react' import Skeleton from 'react-loading-skeleton' import 'react-loading-skeleton/dist/skeleton.css' import useGetMyInfo from '@hooks/useGetMyInfo' import { UserInfo } from '@typings/userForm' +import { toast } from 'react-toastify' +import { useUserStore } from '@store/useUserStore' import Alert from '@components/Alert' import ALERT_MESSAGE from '@constants/alertMessage' @@ -35,13 +37,18 @@ import { logoutPost } from '@apis/logoutService' const ProfileMain = () => { const alertRef = useRef(null) const navigate = useNavigate() - const [userId, setUserId] = useState(0) - const [isMyProfile, setIsMyProfile] = useState(null) + const { id } = useParams() + + const { memberId: loginMemberId } = useUserStore().userInfo + const [userId, setUserId] = useState(id) + const [isMyProfile, setIsMyProfile] = useState(null) const [userInfo, setUserInfo] = useState(null) - const myInfoResult = useGetMyInfo(1) - const userInfoResult = useGetUserInfo(2) + const myInfoResult = useGetMyInfo(loginMemberId) + const userInfoResult = useGetUserInfo( + typeof id === 'string' ? Number(id) : null, + ) const handleLogoutClick = () => { if (alertRef.current) { @@ -64,8 +71,7 @@ const ProfileMain = () => { } useEffect(() => { - const currentUserId = localStorage.getItem('userId') - if (Number(currentUserId) === userId) { + if (loginMemberId === Number(userId)) { setIsMyProfile(true) } else { setIsMyProfile(false) @@ -106,20 +112,12 @@ const ProfileMain = () => { {/* 프로필 상단 섹션 */} - {myInfoResult.isPending || userInfoResult.isPending ? ( - - ) : ( - - )} + {(userInfo && userInfo.nickname) || } @@ -134,23 +132,11 @@ const ProfileMain = () => {

팔로우

-

- {myInfoResult.isPending || userInfoResult.isPending ? ( - - ) : ( - userInfo?.followingCount - )} -

+

{userInfo?.followingCount}

팔로워

-

- {myInfoResult.isPending || userInfoResult.isPending ? ( - - ) : ( - userInfo?.followerCount - )} -

+

{userInfo?.followerCount}

@@ -158,7 +144,7 @@ const ProfileMain = () => { {/* 프로필 소개 섹션 */} -

{(userInfo && userInfo.aboutMe) || }

+

{userInfo?.aboutMe}

{/* 프로필 상단 버튼 본인 프로필 유무 */} @@ -215,45 +201,29 @@ const ProfileMain = () => { {/* 프로필 하단 이동 섹션 */} - - {(userInfo && ( - <> - 후기 모아보기 {userInfo.reviewsCount}개 - - - )) || } + + 후기 모아보기 {userInfo?.reviewsCount}개 + - {(userInfo && ( - <> - 굿즈 판매기록 {userInfo.goodsSoldCount}개 - - - )) || } + 굿즈 판매기록 {userInfo?.goodsSoldCount}개 + {isMyProfile ? ( <> - {(userInfo && ( - <> - 굿즈 구매기록 {userInfo.goodsBoughtCount}개 - - - )) || } + 굿즈 구매기록 {userInfo?.goodsBoughtCount}개 + - {(userInfo && ( - <> - 직관 보관기록 {userInfo.visitsCount}개 - - - )) || } + 직관 보관기록 {userInfo?.visitsCount}개 + ) : null} diff --git a/src/pages/ProfilePage/ReviewBoxComponent/index.tsx b/src/pages/ProfilePage/ReviewBoxComponent/index.tsx index a704f5a..ffb0079 100644 --- a/src/pages/ProfilePage/ReviewBoxComponent/index.tsx +++ b/src/pages/ProfilePage/ReviewBoxComponent/index.tsx @@ -21,7 +21,7 @@ const ReviewBoxComponent = ({ }) => { return ( - {reviewList ? ( + {reviewList && reviewList.length !== 0 ? ( reviewList.map((data, index) => { return ( @@ -46,7 +46,7 @@ const ReviewBoxComponent = ({ ) }) ) : ( -
데이터가 비어있습니다
+
아직 받은 후기가 없습니다
)}
) diff --git a/src/pages/ProfilePage/ReviewPage.tsx b/src/pages/ProfilePage/ReviewPage.tsx index e3f825b..182cdb4 100644 --- a/src/pages/ProfilePage/ReviewPage.tsx +++ b/src/pages/ProfilePage/ReviewPage.tsx @@ -8,6 +8,8 @@ import { QUERY_KEY } from '@apis/queryClient' import reviewService from '@apis/reviewService' import ReviewBoxComponent from './ReviewBoxComponent' import { useInView } from 'react-intersection-observer' + +import { useParams } from 'react-router-dom' import { RefContainer } from '@styles/globalStyle' import Spinner from '@components/Spinner' @@ -15,7 +17,7 @@ const GOODS_REVIEW = '1' const MATE_REVIEW = '2' const ReviewPage = () => { - const [myId, setMyId] = useState(0) + const { id: pageId } = useParams() const [selectedReview, setSelectedReview] = useState(GOODS_REVIEW) const { ref, inView } = useInView({ threshold: 0.5, @@ -35,7 +37,7 @@ const ReviewPage = () => { queryFn: ({ pageParam }) => reviewService.getReviewList( decideReviewType(selectedReview), - myId + 1, + Number(pageId), pageParam, ), initialPageParam: 0, diff --git a/src/pages/ProfilePage/style.ts b/src/pages/ProfilePage/style.ts index 2d933fe..1a9d06f 100644 --- a/src/pages/ProfilePage/style.ts +++ b/src/pages/ProfilePage/style.ts @@ -360,3 +360,11 @@ export const ReviewLinkBox = styled.div` } } ` + +export const ProfileSpinnerWrap = styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +` diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 4e0a43c..c8e55f3 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -56,9 +56,9 @@ const AppRoutes = () => { path={ROUTE_PATH.LOGIN} element={} /> - } + element={} /> { /> } /> { element={} /> } /> { element={} /> } /> { element={} /> } /> diff --git a/src/store/useUserStore.ts b/src/store/useUserStore.ts new file mode 100644 index 0000000..389c99f --- /dev/null +++ b/src/store/useUserStore.ts @@ -0,0 +1,43 @@ +import { create } from 'zustand' + +interface UserInfo { + age: number | null + gender: string + memberId: number | null + nickname: string + teamId: number | null +} + +interface UserStore { + userInfo: UserInfo + + setAge: (age: number) => void + setGender: (gender: string) => void + setMemberId: (memberId: number) => void + setNickname: (nickname: string) => void + setTeamId: (teamId: number) => void +} + +const initialState = { + userInfo: { + age: null, + gender: '', + memberId: null, + nickname: '', + teamId: null, + }, +} + +export const useUserStore = create((set) => ({ + ...initialState, + + setAge: (age) => set((state) => ({ userInfo: { ...state.userInfo, age } })), + setGender: (gender) => + set((state) => ({ userInfo: { ...state.userInfo, gender } })), + setMemberId: (memberId) => + set((state) => ({ userInfo: { ...state.userInfo, memberId } })), + setNickname: (nickname) => + set((state) => ({ userInfo: { ...state.userInfo, nickname } })), + setTeamId: (teamId) => + set((state) => ({ userInfo: { ...state.userInfo, teamId } })), +}))