GitHub
📌 초기 셋팅
👉 .env
# REST API SERVER CONFIG
REACT_APP_RESTAPI_SERVER_IP=localhost
REACT_APP_RESTAPI_SERVER_PORT=8001
👉 src/modules/index.js
const rootReducer = combineReducers({
productReducer, memberReducer, purchaseReducer, reviewReducer
});
export default rootReducer;
👉 src/Store.js
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(ReduxThunk, ReduxLogger))
);
export default store;
👉 src/index.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
👀 화면 배치
👉 src/App.js
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={ <Layout/> }>
<Route index element={ <Main/> }/>
<Route path="products/categories/:categoryCode" element={ <Main/> }/>
<Route path="search" element={ <Main/> }/>
<Route path="products/:productCode" element={ <ProductDetail/> }/>
<Route path="products/:productCode/purchase" element={<ProtectedRoute loginCheck={ true }><Purchase/></ProtectedRoute> }/>
{}
<Route path="mypage" element={ <MyPageLayout/> }>
<Route index element={ <Navigate to="/mypage/profile" replace/> }/> {}
<Route path="profile" element={ <ProtectedRoute loginCheck={ true }><Profile/></ProtectedRoute> }/>{}
<Route path="payment" element={ <ProtectedRoute loginCheck={ true }><Payment/></ProtectedRoute> }></Route>
</Route>
<Route path="products-management" element={ <ProtectedRoute authCheck={ true }><ProductManagement/></ProtectedRoute> }/>
<Route path="products-registration" element={ <ProtectedRoute authCheck={ true }><ProductRegistration/></ProtectedRoute> }/>
<Route path="products-update/:productCode" element={ <ProtectedRoute authCheck={ true }><ProductUpdate/></ProtectedRoute> }/>
<Route path="review/:productCode" element={ <Review /> }/>
<Route path="reviewDetail/:reviewCode" element={<ReviewDetail />}/>
</Route>
{}
<Route path="/login" element={ <ProtectedRoute loginCheck={ false }><Login/></ProtectedRoute> }/>
<Route path="/register" element={ <ProtectedRoute loginCheck={ false }><Register/></ProtectedRoute> }/>
{}
<Route path="*" element={ <Error/>}/>
</Routes>
</BrowserRouter>
);
}
export default App;
👉 src/layouts/Layout.js
function Layout () {
const navigate = useNavigate();
const onClickLogoutHandler = () => {
window.localStorage.removeItem('accessToken');
alert('로그아웃 후 메인으로 이동🥳');
navigate("/", { replace : true });
}
return (
<>
<Header onClickLogoutHandler = { onClickLogoutHandler }/>
<Navbar/>
<main className={ LayoutCSS.main }>
<Outlet/>
</main>
<Footer/>
</>
);
}
export default Layout;
👀 Pages
👉 src/pages/products/Main.js
function Main() {
const dispatch = useDispatch();
const products = useSelector(state => state.productReducer);
console.log('products : ', products);
const productList = products.data<;
const pageInfo = products.pageInfo;
const [currentPage, setCurrentPage] = useState(1);
const params = useParams();
const categoryCode = params.categoryCode;
console.log("categoryCode : ", categoryCode);
const [searchParams] = useSearchParams();
const search = searchParams.get('value');
console.log('search : ', search);
useEffect(
() => {
if(categoryCode) {
dispatch(callProductCategoriesListAPI({ categoryCode, currentPage }));
} else if(search) {
dispatch(callProductSearchListAPI({ search, currentPage }));
} else {
dispatch(callProductListAPI({ currentPage }));
}
},[currentPage, categoryCode, search]
);
return (
<>
<div>
{ productList && <ProductList productList={ productList }/> }
</div>
<div>
{}
{ pageInfo && <PagingBar pageInfo={ pageInfo } setCurrentPage={ setCurrentPage }/> }
</div>
</>
);
}
export default Main;
👀 Components
function Header({ onClickLogoutHandler }) {
const navigate = useNavigate();
const [search, setSearch] = useState('');
const onClickLogoHandler = () => {
navigate("/");
}
const onSearchChangeHandler = (e) => {
setSearch(e.target.value);
}
const onEnterKeyHandler = (e) => {
if(e.key = 'Enter'){
console.log('Enter key : ', search);
navigate(`/search?value=${search}`);
}
}
function BeforeLogin() {
return (
<div>
<NavLink to="/login" className={ HeaderCSS.font }>로그인</NavLink> | <NavLink to="/register" className={ HeaderCSS.font }>회원가입</NavLink>
</div>
);
}
function AfterLogin() {
const onClickMypageHandler = () => {
navigate("/mypage");
}
return (
<div >
<button
className={ HeaderCSS.HeaderBtn }
onClick={ onClickMypageHandler }
>
마이페이지
</button> |
<button
className={ HeaderCSS.HeaderBtn }
onClick={ onClickLogoutHandler }
>
로그아웃
</button>
</div>
);
}
return (
<>
<div className={ HeaderCSS.HeaderDiv }>
<button
className={ HeaderCSS.LogoBtn }
onClick={ onClickLogoHandler }
>
GREEDY
</button>
<input
className={ HeaderCSS.InputStyle }
type="text" placeholder="검색"
onChange={ onSearchChangeHandler }
onKeyUp={ onEnterKeyHandler }
/>
{ isLogin() ? <AfterLogin/> : <BeforeLogin/> } {}
</div>
</>
);
}
export default Header;
👉 src/components/common/Navbar.js
function Navbar() {
const style = { textDecoration : 'none', color : 'salmon', fontWeight : 'bold' };
const activeStyle = ({ isActive }) => isActive ? style : undefined;
return (
<div className={ NavbarCSS.NavbarDiv }>
<div className={ NavbarCSS.NavlistUl }>
<li><NavLink to="/" style={ activeStyle }>모든 음식</NavLink></li>
<li><NavLink to="/products/categories/1" style={ activeStyle }>식사</NavLink></li>
<li><NavLink to="/products/categories/2" style={ activeStyle }>디저트</NavLink></li>
<li><NavLink to="/products/categories/3" style={ activeStyle }>음료</NavLink></li>
{ isAdmin() && <li><NavLink to="/products-management" style={ activeStyle }>상품등록</NavLink></li> }
</div>
</div>
);
}
export default Navbar;
function Footer() {
return (
<div className={ FooterCSS.footerDiv }>
<h3 style={ { width: '100%', textAlign: 'center'} }>
Copyright 2023. Greedy All rights reserved.
</h3>
</div>
);
}
export default Footer;
👉 src/components/common/PagingBar.js
function PagingBar({ pageInfo, setCurrentPage }) {
const pageNumber = [];
if(pageInfo) {
for(let i = pageInfo.startPage; i <= pageInfo.endPage; i++) {
pageNumber.push(i);
}
}
return (
<div style={{ listStyleType: 'none', display: 'flex', justifyContent: 'center' }}>
<button
className={ PagingBarCSS.pagingBtn }
onClick={ () => setCurrentPage(pageInfo.currentPage - 1) }
disabled={ pageInfo.currentPage <= 1 }
>
◀
</button>
{ pageNumber.map(num => (
<li key={num} onClick={ () => setCurrentPage(num) }>
<button
className={ PagingBarCSS.pagingBtn }
style={ pageInfo.currentPage === num ? { backgroundColor : 'salmon'} : null }
>
{num}
</button>
</li>
))
}
<button
className={ PagingBarCSS.pagingBtn }
onClick={ () => setCurrentPage(pageInfo.currentPage + 1) }
disabled={ pageInfo.currentPage >= pageInfo.maxPage }
>
▶
</button>
</div>
);
}
export default PagingBar;
👉 src/components/items/ProductItem.js
function ProductItem ({ product : { productCode, productImageUrl, productName, productPrice }}) {
const navigate = useNavigate();
const onClickProductHandler = (productCode) => {
navigate(`/products/${productCode}`);
}
return (
<div
className={ ProductCSS.productDiv }
onClick={ () => onClickProductHandler(productCode) }
>
<img src={ productImageUrl } alt={ productName }/>
<h5>{ productName }</h5>
<h5>{ productPrice }</h5>
</div>
);
}
export default ProductItem;
👉 src/components/lists/ProductList.js
function ProductList ({ productList }) {
return (
<div className={ ProductListCSS.productDiv }>
{
Array.isArray(productList)
&& productList.map(product => <ProductItem key={ product.productCode } product={ product }/>)
}
</div>
);
}
export default ProductList;
👀 APIs
👉 src/apis/ProductAPICalls.js
const SERVER_IP = `${ process.env.REACT_APP_RESTAPI_SERVER_IP }`;
const SERVER_PORT = `${ process.env.REACT_APP_RESTAPI_SERVER_PORT }`;
const PRE_URL = `http://${SERVER_IP}:${SERVER_PORT}/api/v1`;
export const callProductListAPI = ({ currentPage = 1 }) => {
const requestURL = `${PRE_URL}/products?page=${currentPage}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL).then(response => response.json());
if(result.status == 200) {
console.log('[ProductAPICalls] : callProductListAPI result', result);
dispatch(getProducts(result));
}
}
}
export const callProductCategoriesListAPI = ({ categoryCode, currentPage = 1 }) => {
const requestURL = `${PRE_URL}/products/categories/${categoryCode}?page=${currentPage}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL).then(response => response.json());
if(result.status == 200) {
console.log('[ProductAPICalls] : callProductCategoriesListAPI result', result);
dispatch(getProducts(result));
}
}
}
export const callProductSearchListAPI = ({ search, currentPage = 1 }) => {
const requestURL = `${PRE_URL}/products/search?productName=${search}&page=${currentPage}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL).then(response => response.json());
if(result.status == 200) {
console.log('[ProductAPICalls] : callProductSearchListAPI result', result);
dispatch(getProducts(result));
}
}
}
export const callProductDetailAPI = ({ productCode }) => {
const requestURL = `${PRE_URL}/products/${productCode}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL).then(response => response.json());
if(result.status == 200) {
console.log('[ProductAPICalls] : callProductDetailAPI result', result);
dispatch(getProductDetail(result));
}
}
}
export const callProductListForAdminAPI = ({ currentPage = 1 }) => {
const requestURL = `${PRE_URL}/products-management?page=${currentPage}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method : 'GET',
headers : {
"Content-Type" : "application/json",
"Authorization" : "Bearer " + window.localStorage.getItem('accessToken')
}
})
.then(response => response.json());
if(result.status == 200) {
console.log('[ProductAPICalls] : callProductListForAdminAPI result', result);
dispatch(getProducts(result));
}
}
}
export const callProductRegistAPI = ( formData ) => {
const requestURL = `${PRE_URL}/products`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method : 'POST',
headers : {
"Authorization" : "Bearer " + window.localStorage.getItem('accessToken')
},
body : formData
})
.then(response => response.json());
if(result.status == 200) {
console.log('[ProductAPICalls] : callProductRegistAPI result', result);
dispatch(postProduct(result));
}
}
}
export const callProductDetailForAdminAPI = ({ productCode }) => {
const requestURL = `${PRE_URL}/products-management/${productCode}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method : 'GET',
headers : {
"Authorization" : "Bearer " + window.localStorage.getItem('accessToken')
}
}).
then(response => response.json());
if(result.status == 200) {
console.log('[ProductAPICalls] : callProductDetailForAdminAPI result', result);
dispatch(getProductDetail(result));
}
}
}
export const callProductUpdateAPI = (formData) => {
const requestURL = `${PRE_URL}/products`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method : 'PUT',
headers : {
"Authorization" : "Bearer " + window.localStorage.getItem('accessToken')
},
body : formData
}).then(response => response.json())
if(result.status === 200) {
console.log('[ProductAPICalls] callProductUpdateAPI result :', result);
dispatch(putProduct(result));
}
}
}
👀 Modules
👉 src/modules/ProductModule.js
const initalState = [];
const GET_PRODUCTS = "product/GET_PRODUCTS";
const GET_PRODUCT_DETAIL = "product/GET_PRODUCT_DETAIL";
const POST_PRODUCT = 'product/POST_PRODUCT';
const PUT_PRODUCT = 'product/PUT_PRODUCT'
export const { product : { getProducts, getProductDetail, postProduct, putProduct } } = createActions({
[GET_PRODUCTS] : (res) => res.data,
[GET_PRODUCT_DETAIL] : (res) => res.data,
[POST_PRODUCT] : (res) => res,
[PUT_PRODUCT] : (res) => res
});
const productReducer = handleActions(
{
[GET_PRODUCTS] : (state, { payload }) => payload,
[GET_PRODUCT_DETAIL] : (state, { payload }) => payload,
[POST_PRODUCT] : (state, { payload }) => ({ regist : payload }),
[PUT_PRODUCT] : (state, { payload }) => ({ modify : payload })
}
, initalState);
export default productReducer;