feat: Add logout, my posts and password change page

This commit is contained in:
jhynsoo 2023-11-01 16:58:36 +09:00
parent 5f8eb4897f
commit cd9aff3bf6
13 changed files with 242 additions and 18 deletions

21
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@toss/error-boundary": "^1.4.4", "@toss/error-boundary": "^1.4.4",
"axios": "^1.5.0", "axios": "^1.5.0",
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"qs": "^6.11.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-cookie": "^6.1.1", "react-cookie": "^6.1.1",
@ -2688,7 +2689,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
@ -3571,7 +3571,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
@ -3722,7 +3721,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -3734,7 +3732,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -4448,7 +4445,6 @@
"version": "1.12.3", "version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"dev": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -4735,6 +4731,20 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -5159,7 +5169,6 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.0", "call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2", "get-intrinsic": "^1.0.2",

View File

@ -13,6 +13,7 @@
"@toss/error-boundary": "^1.4.4", "@toss/error-boundary": "^1.4.4",
"axios": "^1.5.0", "axios": "^1.5.0",
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"qs": "^6.11.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-cookie": "^6.1.1", "react-cookie": "^6.1.1",

View File

@ -29,6 +29,8 @@ export const URL = {
login: '/user/login/', login: '/user/login/',
logout: '/user/logout/', logout: '/user/logout/',
myPage: '/user/mypage/', myPage: '/user/mypage/',
myPost: '/mypost/',
password: '/user/password/',
user: '/user/', user: '/user/',
brand: '/brand/', brand: '/brand/',
product: '/product/', product: '/product/',

View File

@ -15,6 +15,8 @@ import WriteSteps from './pages/WriteSteps';
import CurrencyInput from './components/CurrencyInput'; import CurrencyInput from './components/CurrencyInput';
import { useState } from 'react'; import { useState } from 'react';
import MyPage from './pages/MyPage'; import MyPage from './pages/MyPage';
import PasswordChanger from './pages/PasswordChanger';
import MyPost from './pages/MyPost';
const Router = () => ( const Router = () => (
<BrowserRouter> <BrowserRouter>
@ -40,6 +42,14 @@ const Router = () => (
path={URL.myPage} path={URL.myPage}
element={<MyPage />} element={<MyPage />}
/> />
<Route
path={URL.myPost}
element={<MyPost />}
/>
<Route
path={URL.password}
element={<PasswordChanger />}
/>
<Route <Route
path={`${URL.brand}/:id`} path={`${URL.brand}/:id`}
element={<Brand />} element={<Brand />}
@ -69,6 +79,10 @@ const Router = () => (
element={<WriteSteps />} element={<WriteSteps />}
/> />
</Route> </Route>
<Route
path={URL.logout}
element={<Logout />}
/>
<Route <Route
path="*" path="*"
element={<NotFound />} element={<NotFound />}

View File

@ -1,9 +1,6 @@
import { usePosts } from '../../hooks/network/post';
import PostItem from '../PostItem'; import PostItem from '../PostItem';
const PostList = ({ page }) => { const PostList = ({ data, page }) => {
const { data } = usePosts({ page });
return ( return (
<> <>
{data?.results?.map((post) => ( {data?.results?.map((post) => (

View File

@ -1,14 +1,17 @@
import { API, URL } from '../../API'; import { API, URL } from '../../API';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
const getPosts = async ({ page }) => { const getPosts = async ({ my, page }) => {
const pageString = page ? `?page=${page}` : ''; const pageString = page ? `?page=${page}` : '';
const { data } = await API.get(`${URL.post}${pageString}`); const urlString = my
? `${URL.post}my${pageString}`
: `${URL.post}${pageString}`;
const { data } = await API.get(urlString);
return data; return data;
}; };
export const usePosts = ({ page }) => { export const usePosts = ({ my, page }) => {
const data = useQuery('posts', () => getPosts({ page }), { const data = useQuery(['post', my, page], () => getPosts({ my, page }), {
suspense: true, suspense: true,
}); });

View File

@ -1,3 +1,4 @@
import { URL } from '../../API';
import Title from '../../components/Title'; import Title from '../../components/Title';
import { SubTitle } from '../../styles/Common.styles'; import { SubTitle } from '../../styles/Common.styles';
import * as S from '../../styles/MyPage.styles'; import * as S from '../../styles/MyPage.styles';
@ -10,12 +11,12 @@ function MyPage() {
<S.MyPage> <S.MyPage>
<SubTitle>거래</SubTitle> <SubTitle>거래</SubTitle>
<S.Section> <S.Section>
<S.LinkItem to={'/'}>내가 판매글</S.LinkItem> <S.LinkItem to={URL.myPost}>내가 판매글</S.LinkItem>
</S.Section> </S.Section>
<SubTitle>계정</SubTitle> <SubTitle>계정</SubTitle>
<S.Section> <S.Section>
<S.LinkItem to={'/'}>내가 판매글</S.LinkItem> <S.LinkItem to={URL.password}>비밀번호 바꾸기</S.LinkItem>
<S.LinkItem to={'/'}>비밀번호 바꾸기</S.LinkItem> <S.LinkItem to={URL.logout}>로그아웃</S.LinkItem>
<S.LinkItem to={'/'}>회원 탈퇴</S.LinkItem> <S.LinkItem to={'/'}>회원 탈퇴</S.LinkItem>
</S.Section> </S.Section>
</S.MyPage> </S.MyPage>

View File

@ -0,0 +1,26 @@
import { useLocation } from 'react-router-dom';
import Title from '../../components/Title';
import * as S from '../../styles/MyPost.styles';
import { usePosts } from '../../hooks/network/post';
import QueryString from 'qs';
import PostList from '../../components/Post/PostList';
function MyPost() {
const location = useLocation();
const queryData = QueryString.parse(location.search, {
ignoreQueryPrefix: true,
});
const page = queryData.page || 1;
const { data } = usePosts({ my: true, page });
return (
<S.MyPost>
<Title>내가 판매글</Title>
<PostList
data={data}
page={page}
/>
</S.MyPost>
);
}
export default MyPost;

View File

@ -0,0 +1,132 @@
import { Fragment, useState } from 'react';
import Title from '../../components/Title';
import * as S from '../../styles/PasswordChanger.styles';
import ButtonArea from '../../components/ButtonArea';
import Button from '../../components/Button';
import { Input, SubTitle } from '../../styles/Common.styles';
import { API, URL } from '../../API';
function PasswordChanger() {
const inputField = [
{
id: 'oldPassword',
name: '현재 비밀번호',
placeholder: '현재 비밀번호를 입력하세요',
},
{
id: 'newPassword',
name: '새 비밀번호',
placeholder: '비밀번호는 8자리 이상 영문, 숫자를 조합해서 입력하세요',
},
{
id: 'newPasswordConfirm',
name: '새 비밀번호 확인',
placeholder: '새 비밀번호를 한번 더 입력하세요',
},
];
const [passwordData, setPasswordData] = useState({
oldPassword: { value: '' },
newPassword: { value: '' },
newPasswordConfirm: { value: '' },
error: {
id: null,
msg: null,
},
});
const handleInputChange = (fieldName) => (event) => {
const newValue = event.target.value;
setPasswordData((prevData) => ({
...prevData,
[fieldName]: { ...prevData[fieldName], value: newValue },
}));
};
const setError = (id, msg) => {
setPasswordData((prevData) => ({
...prevData,
error: {
id,
msg,
},
}));
};
const handleSubmit = (event) => {
if (passwordData.oldPassword.value === '') {
setError('oldPassword', '현재 비밀번호를 입력해주세요');
return;
}
if (passwordData.newPassword.value === '') {
setError('newPassword', '새 비밀번호를 입력해주세요');
return;
}
if (passwordData.oldPassword.value === passwordData.newPassword.value) {
setError('newPassword', '현재 비밀번호와 다른 비밀번호를 입력해주세요');
return;
}
if (
passwordData.newPassword.value !== passwordData.newPasswordConfirm.value
) {
setError('newPasswordConfirm', '비밀번호가 일치하지 않습니다');
return;
}
if (
passwordData.newPassword.value.length < 8 ||
!passwordData.newPassword.value.match(
/^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/
)
) {
setError(
'newPassword',
'비밀번호는 영문과 숫자를 포함하고 8자리 이상이어야 합니다'
);
return;
}
API.patch(URL.password, {
oldPassword: passwordData.oldPassword.value,
newPassword: passwordData.newPassword.value,
})
.then((response) => {
if (response.status === 204) {
alert('비밀번호가 변경되었습니다\n다시 로그인해주세요');
window.location.href = '/';
return;
}
alert('에러');
})
.catch(({ response }) => {
if (response.status === 400) {
setError('newPassword', response.data.msg);
return;
}
});
};
return (
<>
<Title>비밀번호 바꾸기</Title>
<S.PasswordChanger>
{inputField.map(({ id, name, placeholder }, index) => (
<Fragment key={id}>
<SubTitle>{name}</SubTitle>
<S.PasswordInput key={name}>
<Input
type="password"
placeholder={placeholder}
value={passwordData[id].value}
onChange={handleInputChange(id)}
autoFocus={index === 0}
/>
</S.PasswordInput>
{passwordData.error.id === id && (
<S.ErrorText>{passwordData.error.msg}</S.ErrorText>
)}
</Fragment>
))}
<ButtonArea>
<Button onClick={handleSubmit}>비밀번호 바꾸기</Button>
</ButtonArea>
</S.PasswordChanger>
</>
);
}
export default PasswordChanger;

View File

@ -1,13 +1,26 @@
import * as S from '../../styles/Posts.styles'; import * as S from '../../styles/Posts.styles';
import QueryString from 'qs';
// import { Title } from '../../styles/Common.styles'; // import { Title } from '../../styles/Common.styles';
import PostList from '../../components/Post/PostList'; import PostList from '../../components/Post/PostList';
import Title from '../../components/Title'; import Title from '../../components/Title';
import { usePosts } from '../../hooks/network/post';
import { useLocation } from 'react-router-dom';
const Posts = () => { const Posts = () => {
const location = useLocation();
const queryData = QueryString.parse(location.search, {
ignoreQueryPrefix: true,
});
const page = queryData.page || 1;
const { data } = usePosts({ page });
return ( return (
<S.Posts> <S.Posts>
<Title>최신글</Title> <Title>최신글</Title>
<PostList /> <PostList
data={data}
page={page}
/>
</S.Posts> </S.Posts>
); );
}; };

View File

@ -0,0 +1,5 @@
import styled from 'styled-components';
export const MyPost = styled.div`
margin: 0 auto;
`;

View File

@ -0,0 +1,20 @@
import styled from 'styled-components';
export const PasswordChanger = styled.div`
display: flex;
flex-direction: column;
`;
export const PasswordInput = styled.div`
${({ theme }) => theme.boxShadow};
${({ theme }) => theme.hoverBorder};
${({ theme }) => theme.activeTransform};
padding: 0;
`;
export const ErrorText = styled.p`
color: ${({ theme }) => theme.colors.error};
font-size: 14px;
font-weight: 600;
margin: 14px 0 14px 20px;
`;

View File

@ -11,6 +11,7 @@ const colors = {
gray700: '#4e5968', gray700: '#4e5968',
gray800: '#333d4b', gray800: '#333d4b',
gray900: '#191f28', gray900: '#191f28',
error: '#e0245e',
background: '#fff', background: '#fff',
grayBackground: '#f2f4f6', grayBackground: '#f2f4f6',
primary: '#2060ee', primary: '#2060ee',