Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

어드민 계정 분리 관련 페이지 작업 #63

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import NoticeDetail from 'pages/Services/Notice/NoticeDetail';
import NoticeWrite from 'pages/Services/Notice/NoticeWrite';
import ForceUpdate from 'pages/Update/ForceUpdate';
import UpdateList from 'pages/Update/UpdateList';
import History from 'pages/History';

function RequireAuth() {
const location = useLocation();
Expand Down Expand Up @@ -74,6 +75,7 @@ function App() {
<Route path="/notice/write" element={<NoticeWrite />} />
<Route path="/force-update" element={<ForceUpdate />} />
<Route path="/update-list" element={<UpdateList />} />
<Route path="/history" element={<History />} />
<Route path="*" element={<h1>404</h1>} />
</Route>
</Routes>
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
104 changes: 104 additions & 0 deletions src/components/common/SideNav/ChangePasswordModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import sha256 from 'sha256';
import { Button, Flex, message } from 'antd';
import { useChangePasswordMutation } from 'store/api/auth';
import useBooleanState from 'utils/hooks/useBoolean';
import CustomForm from 'components/common/CustomForm';
import { styled } from 'styled-components';

interface ChangePasswordFormProps {
closeModal: () => void,
}

interface PasswordFormData {
currentPassword: string,
newPassword: string,
checkPassword: string,
}

const ButtonContainer = styled(Flex)`
padding: 12px 0;
button {
border-radius: 5px;
}
`;

export default function ChangePasswordForm({
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
closeModal,
}: ChangePasswordFormProps) {
const {
value: isPasswordCorrect, setTrue: correctPassword, setFalse: incorrectPassword,
} = useBooleanState(true);
const {
value: isNewPasswordMatch, setTrue: matchPassword, setFalse: notMatchPassword,
} = useBooleanState(true);

const [changePassword] = useChangePasswordMutation();
const [changePasswordForm] = CustomForm.useForm();
const { required } = CustomForm.useValidate();

const handlePasswordMatch = () => {
if (changePasswordForm.getFieldValue('newPassword') === changePasswordForm.getFieldValue('checkPassword')) {
matchPassword();
} else {
notMatchPassword();
}
};

const onFinish = (formData: PasswordFormData) => {
const hashedCurrentPassword = sha256(formData.currentPassword);
const hashedNewPassword = sha256(formData.checkPassword);
if (formData.newPassword !== formData.checkPassword) {
message.error('새 비밀번호를 다시 확인해주세요.');
return;
}
changePassword({ old_password: hashedCurrentPassword, new_password: hashedNewPassword })
.unwrap()
.then(() => {
message.success('비밀번호 변경 완료');
correctPassword();
closeModal();
changePasswordForm.resetFields();
})
.catch(({ data }) => {
message.error(data.message);
incorrectPassword();
});
};

return (
<CustomForm form={changePasswordForm} onFinish={onFinish}>
<CustomForm.Input
label="현재 비밀번호"
name="currentPassword"
type="password"
rules={[required()]}
/>
{
!isPasswordCorrect
&& <div style={{ color: 'red' }}>현재 비밀번호가 일치하지 않습니다.</div>
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

antd를 살펴보니 다이나믹룰 이라고 하면서dependencies와 rule의 validation으로 비번 확인에 활용할 수 있는게 있는 것 같아요! 버전 등으로 못쓸 수도 있으니 확인 한번 해주시면 좋을 것 가탕요!
error 관련은 따로 쓰기보다는 antd의 form의 input을 쓰고 있으니 가능하다면 rule안에서 최대한 처리하는 방식을 찾아보는 것도 좋을 것 같습니다!

Copy link
Contributor Author

@Gwak-Seungju Gwak-Seungju Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dependencies라는 속성이 있었군요! 반영했습니다!! 81e2de8 , 67d92b1

<CustomForm.Input
label="새 비밀번호"
name="newPassword"
type="password"
onChange={handlePasswordMatch}
rules={[required()]}
/>
<CustomForm.Input
label="새 비밀번호 확인"
name="checkPassword"
type="password"
onChange={handlePasswordMatch}
rules={[required()]}
/>
{
!isNewPasswordMatch
&& <div style={{ color: 'red' }}>새 비밀번호와 일치하지 않습니다.</div>
}
<ButtonContainer justify="end">
<Button onClick={closeModal}>취소하기</Button>
<CustomForm.Button htmlType="submit">변경하기</CustomForm.Button>
</ButtonContainer>
</CustomForm>
);
}
50 changes: 47 additions & 3 deletions src/components/common/SideNav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import {
UsergroupDeleteOutlined, FolderOpenOutlined, ControlOutlined,
UserAddOutlined, BoldOutlined, ApartmentOutlined, SnippetsOutlined, GiftOutlined,
NotificationOutlined, IssuesCloseOutlined, FormOutlined, UnorderedListOutlined,
HistoryOutlined, FlagOutlined,
} from '@ant-design/icons';
import { Menu, MenuProps } from 'antd';
import {
Button, Menu, MenuProps,
} from 'antd';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import useBooleanState from 'utils/hooks/useBoolean';
import useLogout from 'utils/hooks/useLogout';
import CustomForm from 'components/common/CustomForm';
import ChangePasswordForm from './ChangePasswordModal';

type MenuItem = Required<MenuProps>['items'][number];

Expand Down Expand Up @@ -54,6 +61,9 @@ const items: MenuProps['items'] = [
getItem('업데이트 관리', '/force-update', <FormOutlined />),
getItem('목록 관리', '/update-list', <UnorderedListOutlined />),
]),
getItem('히스토리', 'history', <HistoryOutlined />, [
getItem('로그 히스토리', '/history', <FlagOutlined />),
]),
];

const SideNavConatiner = styled.nav`
Expand All @@ -76,14 +86,34 @@ const LogoImg = styled.img`
cursor: pointer;
`;

const ModalWrap = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin: 12px 0;

button {
border: none;
box-shadow: none;
padding: 0 7px;;
}

span.ant-btn-icon {
display: none;
}
`;

function SideNav() {
const navigate = useNavigate();
const { pathname } = useLocation();
const onClick: MenuProps['onClick'] = (e) => {
const onClickMenu: MenuProps['onClick'] = (e) => {
navigate(e.key);
};
const selectedKeys = pathname.startsWith('/notice') ? ['/notice'] : [pathname];

const { value: isModalOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false);
const logout = useLogout();

return (
<SideNavConatiner>
<Logo>
Expand All @@ -92,12 +122,26 @@ function SideNav() {
</Link>
</Logo>
<Menu
onClick={onClick}
onClick={onClickMenu}
selectedKeys={selectedKeys}
defaultOpenKeys={['service', 'service-store', 'user']}
mode="inline"
items={items}
/>
<ModalWrap>
<CustomForm.Modal
buttonText="비밀번호 변경"
title="비밀번호 변경"
footer={null}
open={isModalOpen}
onCancel={closeModal}
onClick={openModal}
>
<ChangePasswordForm closeModal={closeModal} />
</CustomForm.Modal>
<Button onClick={logout}>로그아웃</Button>
</ModalWrap>

</SideNavConatiner>
);
}
Expand Down
2 changes: 0 additions & 2 deletions src/constant/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export const API_PATH = process.env.REACT_APP_API_PATH;

export const SECOND_PASSWORD = process.env.REACT_APP_SECOND_PASSWORD;

export const KOIN_URL = process.env.REACT_APP_API_PATH?.includes('stage') ? 'https://stage.koreatech.in' : 'https://koreatech.in';

// 테이블 헤더 Title 매핑
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
5 changes: 5 additions & 0 deletions src/model/auth.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ export interface LoginRequest {
email: string
password: string
}

export interface ChangePasswordRequest {
old_password: string,
new_password: string,
}
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions src/pages/History/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function History() {
return (
<div>History</div>
);
}
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 3 additions & 7 deletions src/pages/Login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SyntheticEvent, useEffect, useRef } from 'react';
import { useLoginMutation } from 'store/api/auth';
import sha256 from 'sha256';
import { SECOND_PASSWORD } from 'constant';
import { setCredentials, useToken } from 'store/slice/auth';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
Expand All @@ -12,7 +11,6 @@ import * as S from './Login.style';
function useLogin() {
const idRef = useRef<InputRef>(null);
const passwordRef = useRef<InputRef>(null);
const secondPasswordRef = useRef<InputRef>(null);
const token = useToken();
const [loginMutation] = useLoginMutation();
const dispatch = useDispatch();
Expand All @@ -24,10 +22,9 @@ function useLogin() {

const login = async (e: SyntheticEvent) => {
e.preventDefault();
const refList = [idRef.current, passwordRef.current, secondPasswordRef.current];
const refList = [idRef.current, passwordRef.current];

if (refList.some((current) => (current?.input?.value === ''))) message.warning('필수 입력값을 입력해주세요.');
Comment on lines +25 to 27
Copy link

@SimYunSup SimYunSup Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

본인이 하지 않았지만, ChangePasswordFormLoginForm이 다른 방식으로 validate하는 이유가 있는지 살펴보고, 바꿀 부분이 있다면 고쳐주세요.

else if (refList[2]?.input?.value !== SECOND_PASSWORD) message.error('올바른 계정이 아닙니다.');
else {
const res = await loginMutation({
email: refList[0]?.input?.value!,
Expand All @@ -46,13 +43,13 @@ function useLogin() {
};

return {
idRef, passwordRef, secondPasswordRef, login,
idRef, passwordRef, login,
};
}

function Login() {
const {
idRef, passwordRef, secondPasswordRef, login,
idRef, passwordRef, login,
} = useLogin();

return (
Expand All @@ -63,7 +60,6 @@ function Login() {
<S.Divider />
<S.FormInput ref={idRef} autoComplete="id" placeholder="ID" />
<S.FormInput ref={passwordRef} type="password" autoComplete="current-password" placeholder="PASSWORD" />
<S.FormInput ref={secondPasswordRef} type="password" placeholder="SECOND PASSWORD" />
<S.SubmitButton type="primary" htmlType="submit">Login</S.SubmitButton>
</S.LoginForm>
</S.Container>
Expand Down
11 changes: 9 additions & 2 deletions src/store/api/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
BaseQueryFn, createApi, FetchArgs, fetchBaseQuery, FetchBaseQueryError,
} from '@reduxjs/toolkit/query/react';
import { LoginRequest, LoginResponse } from 'model/auth.model';
import { LoginRequest, LoginResponse, ChangePasswordRequest } from 'model/auth.model';
import { RootState } from 'store';
import { API_PATH } from 'constant';
import { setCredentials } from 'store/slice/auth';
Expand Down Expand Up @@ -78,7 +78,14 @@ export const authApi = createApi({
protected: builder.mutation<{ message: string }, void>({
query: () => 'protected',
}),
changePassword: builder.mutation<void, ChangePasswordRequest>({
query: ({ old_password, new_password }) => ({
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
url: '/password',
method: 'PUT',
body: { old_password, new_password },
}),
}),
}),
});

export const { useLoginMutation, useProtectedMutation } = authApi;
export const { useLoginMutation, useProtectedMutation, useChangePasswordMutation } = authApi;
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions src/utils/hooks/useLogout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { setCredentials } from 'store/slice/auth';

export default function useLogout() {
const logout = () => {
sessionStorage.removeItem('token');
localStorage.removeItem('refresh_token');
setCredentials({ token: '' });
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
window.location.href = '/login';
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. useLogout이 hook으로 분리될 정도로 재사용되나요?
  2. logout 함수만 반환하는데, 이를 훅으로 만들 가치가 있었나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. SideNav 한 곳에서만 로그아웃을 하기 때문에 재사용성은 없습니다.
  2. 한 곳에서만 쓰이고 단순히 logout만 하는 함수이기 때문에 SideNav에서 정의해도 될 것 같습니다.
    반영했습니다! 74127bb

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 토큰을 저장하는 용도로 redux를 사용하고 있는데, redux를 단순 저장용도로 사용하고 다른 사이드 이펙트를 React쪽에서 관리하는 이유라도 있을까요?

return logout;
}
Gwak-Seungju marked this conversation as resolved.
Show resolved Hide resolved
Loading