diff --git a/web/package-lock.json b/web/package-lock.json index 680eb4e..04ef3b1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^3.9.0", - "@lukemorales/query-key-factory": "^1.3.4", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-tabs": "^1.1.0", "@reduxjs/toolkit": "^2.2.6", @@ -3008,19 +3007,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lukemorales/query-key-factory": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@lukemorales/query-key-factory/-/query-key-factory-1.3.4.tgz", - "integrity": "sha512-A3frRDdkmaNNQi6mxIshsDk4chRXWoXa05US8fBo4kci/H+lVmujS6QrwQLLGIkNIRFGjMqp2uKjC4XsLdydRw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@tanstack/query-core": ">= 4.0.0", - "@tanstack/react-query": ">= 4.0.0" - } - }, "node_modules/@mdx-js/react": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz", diff --git a/web/package.json b/web/package.json index d78e5ce..e36e682 100644 --- a/web/package.json +++ b/web/package.json @@ -19,7 +19,6 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.0", - "@lukemorales/query-key-factory": "^1.3.4", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-tabs": "^1.1.0", "@reduxjs/toolkit": "^2.2.6", diff --git a/web/src/App.tsx b/web/src/App.tsx index c5c2fe4..8f41f61 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -30,6 +30,15 @@ const router = createBrowserRouter([ }; }, }, + { + path: 'mypage', + lazy: async () => { + const { default: MyPage } = await import('@/pages/my-page'); + return { + Component: MyPage, + }; + }, + }, { path: 'myevent', lazy: async () => { @@ -86,7 +95,7 @@ export default function App() { - + diff --git a/web/src/assets/step1.png b/web/src/assets/step1.png deleted file mode 100644 index a996ee5..0000000 Binary files a/web/src/assets/step1.png and /dev/null differ diff --git a/web/src/assets/step2.png b/web/src/assets/step2.png deleted file mode 100644 index e766aa7..0000000 Binary files a/web/src/assets/step2.png and /dev/null differ diff --git a/web/src/assets/step3.png b/web/src/assets/step3.png deleted file mode 100644 index cd4a36a..0000000 Binary files a/web/src/assets/step3.png and /dev/null differ diff --git a/web/src/components/auth/sign-in.tsx b/web/src/components/auth/sign-in.tsx index 466f002..86dd9d9 100644 --- a/web/src/components/auth/sign-in.tsx +++ b/web/src/components/auth/sign-in.tsx @@ -78,9 +78,9 @@ export const SignIn = () => {
- 비밀번호 찾기 +

아이디 찾기

- 아이디 찾기 +

비밀번호 찾기

회원가입 diff --git a/web/src/components/auth/sign-up1.tsx b/web/src/components/auth/sign-up1.tsx index b1ee465..2b562d7 100644 --- a/web/src/components/auth/sign-up1.tsx +++ b/web/src/components/auth/sign-up1.tsx @@ -1,16 +1,16 @@ +import { AppDispatch, RootState } from '@/store'; +import { toggleCheck } from '@/store/signup/terms'; +import { body1Style, head1Style } from '@/styles/global-styles'; +import { COLORS } from '@/theme'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { Subtitle } from '../common/Subtitle'; -import { body1Style, head1Style } from '@/styles/global-styles'; -import { COLORS } from '@/theme'; -import { toggleCheck } from '@/store/signup/terms'; -import { AppDispatch, RootState } from '@/store'; +import { useNavigate } from 'react-router-dom'; import checkRound from '../../assets/checkRound.png'; -import uncheckRound from '../../assets/uncheckRound.png'; import rightArrow from '../../assets/rigth.png'; -import { useNavigate } from 'react-router-dom'; +import uncheckRound from '../../assets/uncheckRound.png'; export const SignUp1: React.FC = () => { const dispatch = useDispatch(); @@ -98,7 +98,7 @@ export const SignUp1: React.FC = () => { - + 다음으로 @@ -183,17 +183,17 @@ const Total = styled.div` `; interface NextButtonProps { - enabled: boolean; + $enabled: boolean; } const NextButton = styled.button` width: 100%; padding: 10px; - background-color: ${props => (props.enabled ? COLORS.Main : COLORS.Gray3)}; + background-color: ${props => (props.$enabled ? COLORS.Main : COLORS.Gray3)}; color: #fff; border: none; border-radius: 10px; - cursor: ${props => (props.enabled ? 'pointer' : 'not-allowed')}; + cursor: ${props => (props.$enabled ? 'pointer' : 'not-allowed')}; font-size: 16px; font-weight: bold; margin-top: 200px; diff --git a/web/src/components/auth/sign-up2.tsx b/web/src/components/auth/sign-up2.tsx index af4ace2..cf23294 100644 --- a/web/src/components/auth/sign-up2.tsx +++ b/web/src/components/auth/sign-up2.tsx @@ -20,11 +20,11 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import styled from 'styled-components'; import { Subtitle } from '../common/Subtitle'; import { Label } from '../common/label'; -import { useNavigate } from 'react-router-dom'; export const SignUp2 = () => { const dispatch = useDispatch(); @@ -171,7 +171,7 @@ export const SignUp2 = () => {
- + 다음으로 @@ -251,17 +251,17 @@ const Confirm1 = styled.button` `; interface NextButtonProps { - enabled: boolean; + $enabled: boolean; } const NextButton = styled.button` width: 100%; padding: 10px; - background-color: ${props => (props.enabled ? COLORS.Main : COLORS.Gray3)}; + background-color: ${props => (props.$enabled ? COLORS.Main : COLORS.Gray3)}; color: #fff; border: none; border-radius: 10px; - cursor: ${props => (props.enabled ? 'pointer' : 'not-allowed')}; + cursor: ${props => (props.$enabled ? 'pointer' : 'not-allowed')}; font-size: 16px; font-weight: bold; margin-top: 80px; diff --git a/web/src/components/common/header/header.styles.ts b/web/src/components/common/header/header.styles.ts index 6879145..b23d9dd 100644 --- a/web/src/components/common/header/header.styles.ts +++ b/web/src/components/common/header/header.styles.ts @@ -6,6 +6,7 @@ export const HeaderWrapper = styled.header` padding: 0 1.25rem; display: flex; align-items: center; + justify-content: space-between; h1 { font-weight: bold; diff --git a/web/src/components/common/header/index.tsx b/web/src/components/common/header/index.tsx index bb13b31..c88c2b6 100644 --- a/web/src/components/common/header/index.tsx +++ b/web/src/components/common/header/index.tsx @@ -1,8 +1,37 @@ -import { ComponentProps } from 'react'; +import { ComponentProps, useEffect, useState } from 'react'; import { HeaderWrapper } from './header.styles'; +import styled from 'styled-components'; +import btnMypage from '@/assets/btn_mypage.png'; +import { useNavigate } from 'react-router-dom'; type HeaderProps = ComponentProps<'header'>; export const Header = ({ children }: HeaderProps) => { - return {children}; + const [isLoggedIn, setIsLoggedIn] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + const token = localStorage.getItem('token'); + if (token) { + setIsLoggedIn(true); + } + }, []); + + const moveMyPage = () => { + navigate('/mypage'); + }; + + return ( + + {children} + {isLoggedIn && ( + + )} + + ); }; + +const MypageIcon = styled.img` + width: 38px; + cursor: pointer; +`; diff --git a/web/src/components/mypage/DeleteModal.tsx b/web/src/components/mypage/DeleteModal.tsx new file mode 100644 index 0000000..592a52f --- /dev/null +++ b/web/src/components/mypage/DeleteModal.tsx @@ -0,0 +1,15 @@ +import { useDeleteUser } from '@/services/queries/user.mutation'; + +export const DeleteModal = () => { + const { deleteUserAccount, isDeleting } = useDeleteUser(); + + const handleDelete = () => { + deleteUserAccount().then(() => onClose()); + }; + + return ( + <> +
탈퇴
+ + ); +}; diff --git a/web/src/components/mypage/user-info.styles.ts b/web/src/components/mypage/user-info.styles.ts new file mode 100644 index 0000000..bdb98cc --- /dev/null +++ b/web/src/components/mypage/user-info.styles.ts @@ -0,0 +1,28 @@ +import { body1Style } from '@/styles/global-styles'; +import { COLORS } from '@/theme'; +import styled from 'styled-components'; + +export const Information = styled.div` + ${body1Style} + color: ${COLORS.Gray1}; + border-bottom: 1px solid ${COLORS.Gray5}; + padding: 30px 0px 30px 0px; +`; + +export const BasicInfo = styled.div` + margin-bottom: 20px; +`; + +export const InfoContainer = styled.div` + display: flex; + gap: 27px; +`; +export const Info = styled.p` + ${body1Style} + color: ${COLORS.Gray2}; +`; + +export const Id = styled.a` + color: ${COLORS.Gray2}; + text-decoration: none; +`; diff --git a/web/src/components/mypage/user-info.tsx b/web/src/components/mypage/user-info.tsx new file mode 100644 index 0000000..7c97570 --- /dev/null +++ b/web/src/components/mypage/user-info.tsx @@ -0,0 +1,28 @@ +import { UserInfoType } from '@/types/user'; +import { + BasicInfo, + Id, + Info, + InfoContainer, + Information, +} from './user-info.styles'; + +export const UserInfo = ({ user }: { user: UserInfoType }) => { + return ( + + 기본 정보 + + 아이디 + {user.userId} + + + 휴대폰 + {user.phoneNumber} + + + 이메일 + {user.email} + + + ); +}; diff --git a/web/src/lib/query-keys.ts b/web/src/lib/query-keys.ts index e89648d..6b36e3f 100644 --- a/web/src/lib/query-keys.ts +++ b/web/src/lib/query-keys.ts @@ -1,7 +1,7 @@ -import { createQueryKeyStore } from '@lukemorales/query-key-factory'; +const USER = 'user'; -export const queries = createQueryKeyStore({ +export const queries = { user: { - all: null, + DEFAULT: [USER], }, -}); +}; diff --git a/web/src/lib/schema/auth.schema.ts b/web/src/lib/schema/auth.schema.ts index 115f23f..a418079 100644 --- a/web/src/lib/schema/auth.schema.ts +++ b/web/src/lib/schema/auth.schema.ts @@ -61,3 +61,11 @@ export const signinSchema = z.object({ export type SignupOneStepType = z.infer; export type SignupTwoStepType = z.infer; export type SigninType = z.infer; +export type SignupPayload = { + userId: string; + name: string; + password: string; + email: string; + phone: string; + verificationCode: string; +}; diff --git a/web/src/pages/my-page.tsx b/web/src/pages/my-page.tsx index cda6860..dc0cd1d 100644 --- a/web/src/pages/my-page.tsx +++ b/web/src/pages/my-page.tsx @@ -1,40 +1,54 @@ +import backImg from '@/assets/btn_back_black.png'; import { Subtitle } from '@/components/common/Subtitle'; +import { UserInfo } from '@/components/mypage/user-info'; +import { useDeleteUser } from '@/services/queries/user.mutation'; +import { useUserInfo } from '@/services/queries/user.queries'; import { body1Style, head2Style } from '@/styles/global-styles'; import { COLORS } from '@/theme'; +import { JSX } from 'react'; import styled from 'styled-components'; -import btn_mypage from '../assets/btn_mypage.png'; - const MyPage = () => { + const { userInfo } = useUserInfo(); + + const { deleteUserAccount, isDeleting } = useDeleteUser(); + + const handleDelete = () => { + if (window.confirm('정말로 탈퇴하시겠습니까?')) { + deleteUserAccount(); + } + }; + + // TODO: Loading 스켈레톤 추가 + let content: JSX.Element; + if (userInfo.isPending) { + content =
Loading,,,
; + } else if (userInfo.isFetching) { + content =
Loading,,,
; + } else if (userInfo.isError) { + content =
Error,,,
; + } else { + content = ; + } return (
- + Profile - 이현우 + {userInfo.data && userInfo.data.name} 인플루언서 - - 기본 정보 - - 아이디 - Suppin2024 - - - 휴대폰 - 010-1234-5678 - - - 이메일 - suppin2024@naver.com - - + {/* TODO: 스켈레톤 컴포넌트 추가 */} + {content} 비밀번호 변경하기 - 회원 탈퇴하기 + + {isDeleting ? '탈퇴 중...' : '회원 탈퇴하기'} + + {/* */} 버전 정보 @@ -54,7 +68,6 @@ const Container1 = styled.div` const SubContainer = styled.div` display: flex; - /* justify-content: flex-start; */ align-items: flex-start; flex-direction: column; margin-left: 15px; @@ -79,45 +92,24 @@ const Type = styled.div` const Container2 = styled.div` padding: 0px 20px; `; -const Information = styled.div` - ${body1Style} - color: ${COLORS.Gray1}; - border-bottom: 1px solid ${COLORS.Gray5}; - padding: 30px 0px 30px 0px; -`; - -const BasicInfo = styled.div` - margin-bottom: 20px; -`; - -const InfoContainer = styled.div` - display: flex; - gap: 27px; - - a { - text-decoration: none; /* 밑줄 제거 */ - } -`; -const Info = styled.p` - ${body1Style} - color: ${COLORS.Gray2}; -`; -const Id = styled.a` - color: ${COLORS.Gray2}; - text-decoration: none; -`; const Change = styled.div` padding: 20px 0px; border-bottom: 1px solid ${COLORS.Gray5}; ${body1Style} color: ${COLORS.Gray1}; `; -const Leave = styled.div` +const Leave = styled.button` padding: 20px 0px; - /* border-bottom: 1px solid ${COLORS.Gray5}; */ + border: none; + background: none; + cursor: pointer; ${body1Style} color: ${COLORS.Gray1}; + &:disabled { + color: ${COLORS.Gray4}; + cursor: not-allowed; + } `; const VersionContainer = styled.div` border-bottom: 4px solid ${COLORS.Gray6}; diff --git a/web/src/services/apis/user.service.ts b/web/src/services/apis/user.service.ts index 1dc1a1e..2b413e3 100644 --- a/web/src/services/apis/user.service.ts +++ b/web/src/services/apis/user.service.ts @@ -1,7 +1,8 @@ -import { SigninType, SignupType } from '@/lib/schema/auth.schema'; +import { SigninType, SignupPayload } from '@/lib/schema/auth.schema'; import { axiosInstance } from '@/services/axios-instance'; +import { UserResponse } from '@/types/user'; -export const signup = async (payload: SignupType) => { +export const signup = async (payload: SignupPayload) => { const { data } = await axiosInstance.post('/members/join', payload); return data; }; @@ -33,9 +34,26 @@ export const signin = async (payload: SigninType) => { return data; }; +// 아이디 중복 확인 (sign-up3.tsx) export const checkUserId = async (userId: string) => { const { data } = await axiosInstance.get('/members/checkUserId', { params: { userId }, }); return data; }; + +// 회원정보 상세 조회 +export const getUserInfo = async (): Promise => { + const { data } = await axiosInstance.get('/members/me'); + return data; +}; + +// 회원탈퇴 +export const deleteUser = async () => { + const { data } = await axiosInstance.delete('/members/delete', { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }); + return data; +}; diff --git a/web/src/services/axios-instance.ts b/web/src/services/axios-instance.ts index 8cf8c5b..6c7f5e9 100644 --- a/web/src/services/axios-instance.ts +++ b/web/src/services/axios-instance.ts @@ -7,6 +7,7 @@ export const axiosInstance = axios.create({ axiosInstance.interceptors.request.use( config => { + config.headers.Authorization = `Bearer ${window.localStorage.getItem('token')}`; return config; }, error => { @@ -17,6 +18,9 @@ axiosInstance.interceptors.request.use( axiosInstance.interceptors.response.use( response => { + if (response.config.url === '/members/login') { + window.localStorage.setItem('token', response.data.data.token); + } return response; }, error => { diff --git a/web/src/services/queries/user.mutation.ts b/web/src/services/queries/user.mutation.ts index 3e5c508..63d3b45 100644 --- a/web/src/services/queries/user.mutation.ts +++ b/web/src/services/queries/user.mutation.ts @@ -1,6 +1,7 @@ import { SigninType, SignupType } from '@/lib/schema/auth.schema'; -import { signin, signup } from '@/services/apis/user.service'; +import { deleteUser, signin, signup } from '@/services/apis/user.service'; import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; export const useSignup = () => { @@ -37,9 +38,14 @@ export const useSignin = () => { } toast.error('로그인 중 오류가 발생했습니다, 다시 시도해주세요'); }, - // onSuccess: () => { - // console.log('로그인 성공'); - // }, + // 로그인 성공 시 token 저장 및 콘솔 출력 + onSuccess: data => { + console.log(data); // 로그인 성공 시 응답 콘솔 출력 + if (data && data.data && data.data.token) { + localStorage.setItem('token', data.data.token); // token을 로컬스토리지에 저장 + } + // toast.success('로그인이 정상처리 되었습니다.'); + }, }); return { @@ -47,3 +53,27 @@ export const useSignin = () => { isSigninLoading, }; }; + +export const useDeleteUser = () => { + const navigate = useNavigate(); + + const { mutateAsync: deleteUserAccount, isPending: isDeleting } = useMutation( + { + mutationFn: async () => await deleteUser(), + onError: error => { + console.error('회원 탈퇴 에러:', error); + toast.error('회원 탈퇴 중 오류가 발생했습니다, 다시 시도해주세요'); + }, + onSuccess: () => { + toast.success('회원 탈퇴가 완료되었습니다.'); + localStorage.removeItem('token'); + navigate('/auth?authType=in'); + }, + } + ); + + return { + deleteUserAccount, + isDeleting, + }; +}; diff --git a/web/src/services/queries/user.queries.ts b/web/src/services/queries/user.queries.ts index e69de29..a8afb74 100644 --- a/web/src/services/queries/user.queries.ts +++ b/web/src/services/queries/user.queries.ts @@ -0,0 +1,15 @@ +import { queries } from '@/lib/query-keys'; +import { getUserInfo } from '@/services/apis/user.service'; +import { useQuery } from '@tanstack/react-query'; + +export const useUserInfo = () => { + const userInfo = useQuery({ + queryKey: queries.user.DEFAULT, + queryFn: getUserInfo, + select: data => data.data, + }); + + return { + userInfo, + }; +}; diff --git a/web/src/types/user.ts b/web/src/types/user.ts index e69de29..9f9d3d0 100644 --- a/web/src/types/user.ts +++ b/web/src/types/user.ts @@ -0,0 +1,19 @@ +export type UserResponse = { + code: string; + message: string; + data: { + userId: string; + name: string; + email: string; + phoneNumber: string; + createdAt: string; + }; +}; + +export type UserInfoType = { + userId: string; + name: string; + email: string; + phoneNumber: string; + createdAt: string; +};