2023. 1. 6. 18:56ㆍ기록/Projects

이번 리액트 쇼핑몰 프로젝트에서
내가 맡은 파트는 관리자 파트 !! 🐶🐶
참고한 사이트인 마켓컬리에서는 관리자 페이지를 직접 볼 수가 없었다. ㅠㅠㅠ
그래서 관리자 화면을 어떻게 구성해야 할 지 찾아보다가
구글에 "admin page" 라고 검색해서 나오는 이미지들을 보고 참고했다. (구글 짱...)
처음에는 관리자 페이지만 만들면 되는 줄 알았는데,,,,
관리자 여부를 확인해서 화면을 다르게 보여주고, 경로도 보호해야 했다.
그래서 처음에 셋팅하는 게 조금 오래 걸렸다. ㅠㅠㅠ
그래도 이렇게 처음부터 관리자 / 사용자 별로 화면을 어떻게 보여줄 지 정하고
권한 확인 부분을 만들어놓고 시작하니 팀원분들이 각자의 파트에 집중할 수 있게 된 것 같아서 다행이었다 !
중간중간 코드가 있긴 하지만, 일부만 가져왔기 때문에
자세한 코드를 보시려면 원본 저장소에 가서 보시는 것을 추천합니다 !
관리자 페이지 설정
App.js
function App() {
const dispatch = useDispatch();
const loading = useSelector((state) => state.loading.isLoading);
const isAdmin = useSelector((state) => state.user.isAdmin);
...
return (
<div>
{loading ? <Loading /> : null}
{isAdmin ? (
<div>
<AdminHeader />
<div className={style.adminWrap}>
<AdminNavbar />
<div className={style.adminOutletWrap}>
<div className={style.adminOutlet}>
<Outlet />
</div>
</div>
</div>
</div>
) : (
<div>
<Navbar isLogin={isLogin} />
<div className={style.outlet}>
<Outlet />
</div>
<div className={style.footer}>
<Footer />
</div>
</div>
)}
</div>
);
}
export default App;
loading 상태 값이 true 일 경우에만 보여주고
isAdmin 상태값을 이용해 관리자일 경우와 아닐 경우를 나누어서 보여주기 !
Loading Slice
import { createSlice } from '@reduxjs/toolkit';
let loading = createSlice({
name: 'loading',
initialState: { isLoading: false },
reducers: {
showLoading(state) {
state.isLoading = true;
},
hideLoading(state) {
state.isLoading = false;
},
},
});
export let { showLoading, hideLoading } = loading.actions;
export default loading;
개발 환경에서 redux toolkit 을 사용하기 때문에
showLoading, hideLoading 을 이용해 isLoading 의 상태값을 바꿔주었다.
App.js 에서는 이 값을 이용해 로딩 컴포넌트를 보여주거나 보여주지 않는다 !
index.js
const router = createBrowserRouter([
{
path: '/',
element: <App />,
errorElement: <NotFound />,
children: [
{ index: true, path: '/', element: <Home /> },
{
path: '/login',
element: <Login />,
},
{
path: '/signup',
element: <Signup />,
},
{
path: '/cart',
element: <Cart />,
},
{
path: '/payment',
element: (
<ProtectedRoute>
<Payment />
</ProtectedRoute>
),
},
{
path: '/admin',
element: (
<ProtectedRoute requireAdmin>
<Dashboard />
</ProtectedRoute>
),
},
...
],
},
]);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
);
- 권한이 필요없는 페이지 : 해당 컴포넌트만 작성함
- 사용자 권한이 필요한 페이지 : 해당 컴포넌트를 ProtectedRoute 로 감싸줌
- 관리자 권한이 필요한 페이지 : 해당 컴포넌트를 ProtectedRoute 로 감싸고 requireAdmin 속성을 줌
ProtectedRoute.jsx
import React from 'react';
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
export default function ProtectedRoute({ children, requireAdmin }) {
let user = useSelector((state) => state.user);
if (requireAdmin) {
if (user.isAdmin) return children;
else {
alert('접근 권한이 없습니다.\n메인 페이지로 이동합니다.');
return <Navigate to="/" replace />;
}
} else if (!user.email) {
alert('회원 전용 페이지입니다.\n로그인 페이지로 이동합니다.');
return <Navigate to="/login" replace />;
}
return children;
}
관리자 권한이 필요한 페이지인 경우, isAdmin 값이 true 일 경우에만 해당 페이지를 보여준다.
아닐 경우 메인 페이지로 이동!
사용자 권한이 필요한 페이지인 경우, 사용자의 email 값이 있을 때만 해당 페이지를 보여준다.
아닐 경우 로그인 페이지로 이동!
adminNavbar.jsx
export default function AdminNavbar() {
return (
<nav className={style.adminNavbar}>
<ul>
<NavbarItem page={'/admin'} title={'대시 보드'} />
<NavbarItem page={'/admin/products'} title={'상품 관리'} />
<NavbarItem page={'/admin/order'} title={'거래 내역 관리'} />
</ul>
</nav>
);
}
NavbarItem 컴포넌트에 page, title prop 을 전달함 !
NavbarItem.jsx
export default function NavbarItem({ page, title }) {
const location = useLocation();
return (
<li className={location.pathname.split('/')[2] === page.split('/')[2] ? style.active : ''}>
<Link to={page}>
<span>{title}</span>
<MdOutlineArrowForwardIos size='12' color='rgb(95, 0, 128)' />
</Link>
</li>
);
}
사용자 마이페이지 / 관리자 페이지에서 공통으로 사용하는 컴포넌트
page, title prop 을 전달받음
현재 페이지 경로의 값과 전달받은 page 값을 비교하여 스타일을 다르게 주었다.
두 값이 같을 경우, 기존 색상보다 진한 색으로 보여주어 현재 활성화된 메뉴인 것을 표시하였다.

상품 관리 메뉴를 선택했을 경우 이렇게 보여진다 !
메뉴가 세 개 밖에 없어서 약간 썰렁하긴 하지만... ㅎ
대시 보드

구글에서 여러 사진들을 보고 짬뽕해 놓은 디자인 !!!....
내가 사장이라면 어떤 정보를 먼저 확인하고 싶을까 생각해 봤다. (잠깐의 힐링이었음)
- 이번 달 거래 현황 : 거래 수 / 거래 취소 수 / 거래 확정 수 / 총 거래 금액
- 현재 상품 현황 : 총 상품 수, 품절 상품 수
- 거래 카테고리 통계 : 파이 차트를 이용해 카테고리 별 거래 현황을 보여줌
- 이번 주 거래 금액 통계 : 오늘 포함 7일간의 거래 금액을 보여줌
백엔드가 있었다면 쿼리문으로 필터된 정보를 받아서 바로 사용했겠지만
프론트엔드 만으로 이루어진 프로젝트이고, API 가 다양하지 않다보니
모든 정보를 가져온 후, 직접 filter 메서드를 이용해서 데이터를 추출해야 했다....ㅎ
백엔드의 필요성을 아주 뼈시리게 느껸 부분이었다.
차트 라이브러리는 echarts 를 사용해 보았는데,
사용법도 간단하고 차트 모양도 심플해서 사용하기에 좋은 것 같다.
[ 코드 일부 ]
useEffect(() => {
setMonthOrder(orders.filter((item) => item.timePaid.substr(5, 2) === month).length);
setMonthCancel(
orders.filter((item) => item.timePaid.substr(5, 2) === month && item.isCanceled === true)
.length
);
setMonthDone(
orders.filter((item) => item.timePaid.substr(5, 2) === month && item.done === true).length
);
setMonthAmount(
formatPrice(
orders
.filter((item) => item.timePaid.substr(5, 2) === month && item.isCanceled === false)
.reduce((acc, cur) => acc + Number(cur.product.price), 0)
)
);
const map = new Map();
orders
.map((item) => item.product.tags)
.forEach((item) => {
map.set(item, (map.get(item) || 0) + 1);
});
const arr = [];
for (let [key, val] of map) {
arr.push({ value: val, name: key });
}
setCategory(arr);
...
}, [orders]);
상품 관리

모든 상품의 데이터를 가져와서 페이징을 해주었다.
페이징 생각을 못하고 있었는데, 이 부분 만들다가 페이징이 필요해서
급하게 공통 컴포넌트로 만들어 놓았다. 다음에는 미리 만들어놓을 것 같다.. 미래의 나 화이팅..^^

카테고리, 품절여부, 상품명으로 검색이 가능하다. (이것도 백엔드의 필요성을 뼈시리게 느낌)
체크박스를 선택해서 선택 삭제가 가능하고, 등록 버튼 클릭 시 등록 페이지로 이동한다 !
상품 등록

상품의 정보를 작성한 후 등록 버튼을 누르면 등록이 완료된다.
취소 버튼 클릭 시 이전 페이지로 이동한다.
처음에는 가격 포맷팅을 안해놓았었는데,
콤마가 없으니까 가격처럼 안보여서 불편했다... 속세에 너무 찌들었나..ㅎ
아무튼 내가 불편하니 다른 사용자들도 불편할 것 같았다.
그래서 글자를 입력할 때 자동으로 콤마를 넣어 포맷팅 되도록 구현하였다. (편안해짐)
이미지 파일을 선택하면 이미지와 파일명을 미리 볼 수 있게 구현했다.
또 이미지 파일이 선택되었을 경우 파일 변경 버튼으로 바뀌도록 했다.

loading 컴포넌트가 보여지도록 한 후에
insert API를 호출하는 함수가 끝날 때 까지 await 로 기다린다.
(기다리는 동안 발자국 로딩 컴포넌트가 보여짐...귀엽다ㅜㅜ)
함수의 작동이 끝나면 상품 목록 페이지로 이동, 상품명으로 검색해서 확인 가능 !
[ 코드 일부 ]
if (window.confirm('상품을 등록하시겠습니까?')) {
try {
dispatch(showLoading());
await insertProduct(product);
dispatch(isProductsUpdate(true));
alert('상품 등록이 완료되었습니다.');
navigate('/admin/products');
} catch {
alert('상품 등록이 완료되지 못했습니다.');
} finally {
dispatch(hideLoading());
}
}
상품 상세

상품 목록에서 상품을 클릭하면 해당 상품 상세 페이지로 이동한다.
상품의 정보를 확인할 수 있고, 삭제도 가능하다.
수정 버튼 클릭 시 수정 페이지로 이동한다.
상품 수정

상품 등록 페이지와 같은 jsx를 사용하지만
경로를 이용해 다른 내용을 보여주도록 분기 처리 하였다.
등록 페이지와 마찬가지로 가격 포맷팅을 해주었고, 이미지 미리보기가 가능하다.

수정 버튼 클릭 시,
update API를 호출하는 함수를 기다리면서 loading 컴포넌트가 보여지고
수정이 완료되었다는 alert 창이 뜬다.
alert 창의 확인 버튼을 누르면 수정한 상품의 상세 페이지로 이동해서
수정한 내용을 바로 확인할 수 있다 !
상품 삭제

상품 상세 페이지에서 삭제버튼을 누르면 상품 단일 삭제가 가능하다.
삭제 후 상품 목록으로 이동한다.

맨 위의 체크박스 선택 시 전체 선택이 되고,
모든 체크박스 선택 시, 전체 선택 체크박스가 체크된다.
삭제버튼 클릭 시 다른 API 호출과 마찬가지로
delete API를 호출하고 기다리면서 loading 컴포넌트를 보여준다.
삭제가 완료되면 해당 상품이 삭제된 목록을 확인할 수 있다 !
[ 코드 일부 ]
const handleDelete = async () => {
if (checkId.length === 0) {
alert('선택된 상품이 없습니다.');
return;
}
if (window.confirm('선택한 상품을 삭제하시겠습니까?')) {
try {
dispatch(showLoading());
for (let id of checkId) {
await deleteProduct(id);
}
async function getData() {
const data = await getListProductAdmin();
setProducts(data);
setSearch(data);
dispatch(isProductsUpdate(true));
}
getData();
alert('삭제가 완료되었습니다.');
} catch {
alert('삭제가 완료되지 못했습니다.');
} finally {
dispatch(hideLoading());
}
}
};
거래 관리

상품 관리 메뉴의 목록과 마찬가지로 페이징을 적용해 주었고,
거래자명, 거래일자로 검색이 가능하도록 해주었다.
거래일자를 쉽게 선택하기 위해 react-datepicker 라이브러리를 사용하였는데,
input 컴포넌트를 커스텀할 수 있고, 사용법도 자세히 나와있어서 좋았다.
리셋 버튼을 누르면 각 검색 조건을 초기화하고 모든 데이터를 보여주도록 했다.
거래 상세

거래 목록에서 거래 내역을 선택하면 상세 페이지로 이동한다.
거래 내역 상세 페이지에서는 거래 취소/해제 및 거래 완료/해제가 가능하다.
상품 정보 클릭 시 해당 상품 상세 페이지로 이동하도록 해주었다.
이렇게 첫 리액트 팀 프로젝트가 끝났다 !!! 🐶🐶
코드리뷰를 받아 보니 아직 고칠 점이 많지만..
우리 팀원 모두 끝까지 포기하지 않고 완성해냈다는 것이 너무 감격스럽다 ㅠㅠㅠ
우리 팀원들 정말 최고!!! 수고 많으셨습니다 !!! 🥹
팀 프로젝트는 처음이었는데, 혼자 하는 프로젝트보다 더 해야할 일이 많은 것 같다.
그래도 다 같이 으쌰으쌰 하니까 더 할 맛이 나고 !!!?
서로 맡은 부분에 대해 이야기를 나누다 보니 얻어가는 것도 많은 것 같다 !
다음 번에는 TypeScript, Recoil, StyledComponent 도 함께 사용해 보고 싶다.
그럼 끝까지 봐 주셔서 감사합니다...ㅎㅎㅎㅎㅎ
다음에 더 재밌는 프로젝트를 들고 올게요 ❤️