feat: Add logout, my posts and password change page
This commit is contained in:
parent
5f8eb4897f
commit
cd9aff3bf6
21
package-lock.json
generated
21
package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"@toss/error-boundary": "^1.4.4",
|
||||
"axios": "^1.5.0",
|
||||
"chart.js": "^4.4.0",
|
||||
"qs": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-cookie": "^6.1.1",
|
||||
@ -2688,7 +2689,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
@ -3571,7 +3571,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
@ -3722,7 +3721,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -3734,7 +3732,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -4448,7 +4445,6 @@
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@ -4735,6 +4731,20 @@
|
||||
"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": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@ -5159,7 +5169,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@toss/error-boundary": "^1.4.4",
|
||||
"axios": "^1.5.0",
|
||||
"chart.js": "^4.4.0",
|
||||
"qs": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-cookie": "^6.1.1",
|
||||
|
@ -29,6 +29,8 @@ export const URL = {
|
||||
login: '/user/login/',
|
||||
logout: '/user/logout/',
|
||||
myPage: '/user/mypage/',
|
||||
myPost: '/mypost/',
|
||||
password: '/user/password/',
|
||||
user: '/user/',
|
||||
brand: '/brand/',
|
||||
product: '/product/',
|
||||
|
@ -15,6 +15,8 @@ import WriteSteps from './pages/WriteSteps';
|
||||
import CurrencyInput from './components/CurrencyInput';
|
||||
import { useState } from 'react';
|
||||
import MyPage from './pages/MyPage';
|
||||
import PasswordChanger from './pages/PasswordChanger';
|
||||
import MyPost from './pages/MyPost';
|
||||
|
||||
const Router = () => (
|
||||
<BrowserRouter>
|
||||
@ -40,6 +42,14 @@ const Router = () => (
|
||||
path={URL.myPage}
|
||||
element={<MyPage />}
|
||||
/>
|
||||
<Route
|
||||
path={URL.myPost}
|
||||
element={<MyPost />}
|
||||
/>
|
||||
<Route
|
||||
path={URL.password}
|
||||
element={<PasswordChanger />}
|
||||
/>
|
||||
<Route
|
||||
path={`${URL.brand}/:id`}
|
||||
element={<Brand />}
|
||||
@ -69,6 +79,10 @@ const Router = () => (
|
||||
element={<WriteSteps />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={URL.logout}
|
||||
element={<Logout />}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { usePosts } from '../../hooks/network/post';
|
||||
import PostItem from '../PostItem';
|
||||
|
||||
const PostList = ({ page }) => {
|
||||
const { data } = usePosts({ page });
|
||||
|
||||
const PostList = ({ data, page }) => {
|
||||
return (
|
||||
<>
|
||||
{data?.results?.map((post) => (
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { API, URL } from '../../API';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
const getPosts = async ({ page }) => {
|
||||
const getPosts = async ({ my, 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;
|
||||
};
|
||||
|
||||
export const usePosts = ({ page }) => {
|
||||
const data = useQuery('posts', () => getPosts({ page }), {
|
||||
export const usePosts = ({ my, page }) => {
|
||||
const data = useQuery(['post', my, page], () => getPosts({ my, page }), {
|
||||
suspense: true,
|
||||
});
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { URL } from '../../API';
|
||||
import Title from '../../components/Title';
|
||||
import { SubTitle } from '../../styles/Common.styles';
|
||||
import * as S from '../../styles/MyPage.styles';
|
||||
@ -10,12 +11,12 @@ function MyPage() {
|
||||
<S.MyPage>
|
||||
<SubTitle>거래</SubTitle>
|
||||
<S.Section>
|
||||
<S.LinkItem to={'/'}>내가 쓴 판매글</S.LinkItem>
|
||||
<S.LinkItem to={URL.myPost}>내가 쓴 판매글</S.LinkItem>
|
||||
</S.Section>
|
||||
<SubTitle>계정</SubTitle>
|
||||
<S.Section>
|
||||
<S.LinkItem to={'/'}>내가 쓴 판매글</S.LinkItem>
|
||||
<S.LinkItem to={'/'}>비밀번호 바꾸기</S.LinkItem>
|
||||
<S.LinkItem to={URL.password}>비밀번호 바꾸기</S.LinkItem>
|
||||
<S.LinkItem to={URL.logout}>로그아웃</S.LinkItem>
|
||||
<S.LinkItem to={'/'}>회원 탈퇴</S.LinkItem>
|
||||
</S.Section>
|
||||
</S.MyPage>
|
||||
|
26
src/pages/MyPost/index.jsx
Normal file
26
src/pages/MyPost/index.jsx
Normal 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;
|
132
src/pages/PasswordChanger/index.jsx
Normal file
132
src/pages/PasswordChanger/index.jsx
Normal 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;
|
@ -1,13 +1,26 @@
|
||||
import * as S from '../../styles/Posts.styles';
|
||||
import QueryString from 'qs';
|
||||
// import { Title } from '../../styles/Common.styles';
|
||||
import PostList from '../../components/Post/PostList';
|
||||
import Title from '../../components/Title';
|
||||
import { usePosts } from '../../hooks/network/post';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const Posts = () => {
|
||||
const location = useLocation();
|
||||
const queryData = QueryString.parse(location.search, {
|
||||
ignoreQueryPrefix: true,
|
||||
});
|
||||
const page = queryData.page || 1;
|
||||
const { data } = usePosts({ page });
|
||||
|
||||
return (
|
||||
<S.Posts>
|
||||
<Title>최신글</Title>
|
||||
<PostList />
|
||||
<PostList
|
||||
data={data}
|
||||
page={page}
|
||||
/>
|
||||
</S.Posts>
|
||||
);
|
||||
};
|
||||
|
5
src/styles/MyPost.styles.js
Normal file
5
src/styles/MyPost.styles.js
Normal file
@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const MyPost = styled.div`
|
||||
margin: 0 auto;
|
||||
`;
|
20
src/styles/PasswordChanger.styles.js
Normal file
20
src/styles/PasswordChanger.styles.js
Normal 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;
|
||||
`;
|
@ -11,6 +11,7 @@ const colors = {
|
||||
gray700: '#4e5968',
|
||||
gray800: '#333d4b',
|
||||
gray900: '#191f28',
|
||||
error: '#e0245e',
|
||||
background: '#fff',
|
||||
grayBackground: '#f2f4f6',
|
||||
primary: '#2060ee',
|
||||
|
Loading…
Reference in New Issue
Block a user