Stack/React

[React] 고객관리 사이트 (Client) redux로 구현하기

7ingout 2022. 7. 21. 09:15

2022.07.07 - [Coding/React] - [React] 고객관리 사이트 구현 (Client)

 

[React] 고객관리 사이트 구현 (Client)

green_customer_client Header Footer CustomerList Customer DetailCustomer - 라이브러리 설치 axios react-dom react-router-dom react-daum-postcode material-ui https://mui.com/material-ui/getting-start..

7ingout.tistory.com

 

* 리덕스 모듈

초기값, 액션 타입, 액션 생성 
함수, 리듀서

modules -> customers.js

 

customers: {

data, error, loading

},

addCustomer: {

                c_name: ""

                ...

}

 

middleware_green_customer_client

npm install redux
npm install react-redux
npm install redux-thunk
yarn add redux-devtools-extension

npm / yarn 어느것으로 설치해도 상관없지만 npm이 좀 더 빠른 느낌이라 npm을 쓰게된다 ,,

 


 

App.js

import './App.css';
import CustomerList from './components/CustomerList';
import DetailCustomer from './components/DetailCustomer';
import Footer from './components/Footer';
import Header from './components/Header';
import { Route, Routes } from "react-router-dom";
import CreateCustomer from './components/CreateCustomer';
import EditCustomer from './components/EditCustomer';
import CustomerContainer from './components/CustomerContainer';

import CreateCustomerContainer from './components/CreateCustomerContainer';
const customers = [
  {
    no: 1,
    name: "고객",
    phone: "01012345678",
    birth: "19920206",
    gender: "여성",
    add: "울산시 남구"
  },
  {
    no: 2,
    name: "그린",
    phone: "01012345678",
    birth: "19920206",
    gender: "남성",
    add: "울산시 동구"
  },
  {
    no: 3,
    name: "kh",
    phone: "01012345678",
    birth: "19920206",
    gender: "여성",
    add: "울산시 남구"
  }
]
function App() {
  return (
    <div className="App">
      <Header />
      <Routes>
        {/* props로 위에 {customers}를 CustomerList로 전달 */}
        {/* <Route path="/" element={<CustomerList customers={customers}/>} /> */}
        <Route path="/" element={<CustomerContainer/>} />
        <Route path="/detailview/:no" element={<DetailCustomer/>} />
        <Route path="/edit/:no" element={<EditCustomer/>} />
        {/* <Route path="/write" element={<CreateCustomer/>} /> */}
        <Route path="/write" element={<CreateCustomerContainer/>} />
      </Routes>
      <Footer/>
    </div>
  );
}

export default App;

 

components/CreateCustomer2.js

import React, { useState } from 'react';
import { Table, TableBody, TableRow, TableCell } from '@mui/material';
import PopupDom from "./PopupDom"
import PopupPostCode from "./PopupPostCode"
// import { useNavigate } from 'react-router-dom';

const CreateCustomer2 = ({ onChange, onSubmit, addCustomer, onHome }) => {
    // 우편번호 관리하기
    const onAddData = (data) => {
        console.log(data);
        const postAdd = data.address;
        onChange({
            target: {
                name: "c_add",
                value: postAdd
            }
        })
    }
    // 팝업창 상태 관리
    const [ isPopupOpen, setIsPopupOpen ] = useState(false);
    // 팝업창 상태 true로 변경
    const openPostCode = ()=> {
        setIsPopupOpen(true);
    }
    // 팝업창 상태 false로 변경
    const closePostCode = () => {
        setIsPopupOpen(false);
    }


    // 폼 submit 이벤트
    // const navigate = useNavigate();
    const onSumbitch = (e) => {
        // form에 원래 연결된 이벤트를 제거
        e.preventDefault();

        // 전화번호가 숫자인지 체크하기
        if(isNaN(addCustomer.c_phone)){
            alert('전화번호는 숫자만 입력해주세요');
        } 
        // input에 값이 있는지 체크하고
        // 입력이 다되어있으면 post전송
        if(addCustomer.c_name !== "" && addCustomer.c_phone !== "" && 
        addCustomer.c_birth !== "" && addCustomer.c_gender !=="" &&
        addCustomer.c_add !== "" && addCustomer.c_adddetail !== "" ){
            onSubmit();
            // navigate("/");
            onHome();
        } 
    }
 
    return (
        <div>
            <h2>신규 고객 등록하기</h2>
            <form onSubmit={onSumbitch}>
                <Table>
                    <TableBody>
                        <TableRow>
                            <TableCell>이름</TableCell>
                            <TableCell>
                                <input name="c_name" type="text"
                                value={addCustomer.c_name}
                                onChange={onChange}/>
                            </TableCell>
                        </TableRow>
                        <TableRow>
                            <TableCell>연락처</TableCell>
                            <TableCell>
                                <input name="c_phone" type="text"
                                value={addCustomer.c_phone}
                                onChange={onChange}/>
                            </TableCell>
                        </TableRow>
                        <TableRow>
                            <TableCell>생년월일</TableCell>
                            <TableCell>
                                <input name="c_birth" type="date"
                                value={addCustomer.c_birth}
                                onChange={onChange}/>
                            </TableCell>
                        </TableRow>
                        <TableRow>
                            <TableCell>성별</TableCell>
                            <TableCell>
                                여성<input name="c_gender" type="radio"
                                value="여성"
                                onChange={onChange}/>
                                남성<input name="c_gender" type="radio"
                                value="남성"
                                onChange={onChange}/>
                            </TableCell>
                        </TableRow>
                        <TableRow>
                            <TableCell>주소</TableCell>
                            <TableCell>
                                <input name="c_add" type="text"
                                value={addCustomer.c_add}
                                onChange={onChange}/>
                                <input name="c_adddetail" type="text"
                                value={addCustomer.c_adddetail}
                                onChange={onChange}/>
                                <button type="button" onClick={openPostCode}>우편번호 검색</button>
                                <div id="popupDom">
                                    {isPopupOpen && (
                                        <PopupDom>
                                            <PopupPostCode onClose={closePostCode}
                                            onAddData={onAddData}
                                            />
                                        </PopupDom>
                                    )}
                                </div>
                            </TableCell>
                        </TableRow>
                        <TableRow>
                            <TableCell colSpan={2}>
                               <button type="submit">등록</button>
                               <button type="reset">취소</button>
                            </TableCell>
                        </TableRow>
                    </TableBody>
                </Table>
            </form>
        </div>
    );
};

export default CreateCustomer2;

 

components/CreateCustomerContainer.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setInput, setSubmit, goHome } from '../modules/customers'
import CreateCustomer2 from './CreateCustomer2';
import { useNavigate } from 'react-router-dom';

const CreateCustomerContainer = () => {
    const addCustomer = useSelector(state=> state.customers.addCustomer)
    const dispatch = useDispatch();
    const navigate = useNavigate();
    const onHome = () => {
        dispatch(goHome(navigate))
    }
    const onChange = (e) => {
        dispatch(setInput(e));
    }
    const onSubmit= () => {
        dispatch(setSubmit());
    }
    return (
        <CreateCustomer2 onHome={onHome} onChange={onChange} onSubmit={onSubmit} addCustomer={addCustomer}/>
    );
};

export default CreateCustomerContainer;

 

components/Customer.js

import React from 'react';
import { TableRow, TableCell } from '@mui/material';
import { Link } from "react-router-dom";

const Customer = ( {customer} ) => {
    return (
        <TableRow>
                <TableCell>{customer.no}</TableCell>
                <TableCell><Link to ={`/detailview/${customer.no}`}>{customer.name}</Link></TableCell>
                <TableCell>{customer.phone}</TableCell>
                <TableCell>{customer.birth}</TableCell>
                <TableCell>{customer.gender}</TableCell>
                <TableCell>{customer.add1}<br/>{customer.add2}</TableCell>
        </TableRow>
    );
};

export default Customer;

 

comonents/CustomerContainer.js

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getCustomers } from '../modules/customers';
import CustomerUi from './CustomerUi';

const CustomerContainer = () => {
    const { data, loading, error } = useSelector(state => state.customers.customers);
    const dispatch = useDispatch();
    // 컴포넌트 마운트 후 고객 목록 요청
    useEffect(()=>{
        dispatch(getCustomers());
    }, [dispatch])
    if(loading) return <div>로딩중입니다..</div>
    if(error) return <div>에러가 발생했습니다.</div>
    if(!data) return null
    return (
        <CustomerUi customers={data} />
    );
};

export default CustomerContainer;

 

components/CustomerUi.js

import React from 'react';
import { Table, TableBody, TableHead, TableCell, TableRow } from '@mui/material';
import Customer from './Customer'

const CustomerUi = ({ customers }) => {
    return (
        <div>
        <h2>고객리스트</h2>
        <Table>
            <TableHead>
                <TableRow>
                    <TableCell>번호</TableCell>
                    <TableCell>이름</TableCell>
                    <TableCell>연락처</TableCell>
                    <TableCell>생년월일</TableCell>
                    <TableCell>성별</TableCell>
                    <TableCell>주소</TableCell>
                </TableRow>
            </TableHead>
            <TableBody>
                {customers.map(customer=>(
                    <Customer key={customer.no} customer={customer}/>
                ))}
            </TableBody>
        </Table>
    </div>
    );
};

export default CustomerUi;

 

components/DetailCustomer.js

import React from 'react';
import { Table, TableBody, TableCell, TableRow} from '@mui/material';
import axios from 'axios';
import { useParams, useNavigate, Link } from 'react-router-dom';
import useAsync from '../customHook/useAsync';
import { API_URL } from '../config/contansts';

async function getCustomer(no) {
    const response = await axios.get(`${API_URL}/detailview/${no}`);
    return response.data;
}

const DetailCustomer = ( ) => {
    const navigate = useNavigate();
    const { no } = useParams();
    const [state] = useAsync(()=>getCustomer(no), [no]);
    const { loading, data: customer, error} = state;
    
    // 삭제하기
    const onDelete = () => {
        axios.delete(`${API_URL}/detailview/${no}`)
        .then(result=> {
            console.log('삭제되었습니다.')
            navigate('/');
        })
        .catch(err=> {
            console.log(err);
        })
    }

    if(loading) return <div>로딩중입니다...</div>
    if(error) return <div>에러가 발생했습니다.</div>
    if(!customer) return null;
    return (
        <div>
            <h2>고객 상세정보</h2>
            <Table>
                <TableBody>
                    <TableRow>
                        <TableCell>고객명</TableCell>
                        <TableCell>{customer.name}</TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>연락처</TableCell>
                        <TableCell>{customer.phone}</TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>생년월일</TableCell>
                        <TableCell>{customer.birth}</TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>성별</TableCell>
                        <TableCell>{customer.gender}</TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>주소</TableCell>
                        <TableCell>{customer.add1}{customer.add2}</TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell colSpan={2}>
                            <button onClick={onDelete}>삭제</button>
                            <button><Link to={`/edit/${no}`}>수정</Link></button>
                        </TableCell>
                    </TableRow>
                </TableBody>
            </Table>
        </div>
    );
};

export default DetailCustomer;

 

components/EditCustomer.js

import React, { useState, useEffect } from 'react';
import { Table, TableBody, TableRow, TableCell } from '@mui/material';
import PopupDom from "./PopupDom"
import PopupPostCode from "./PopupPostCode"
import axios from "axios";
import useAsync from '../customHook/useAsync';
import { API_URL } from '../config/contansts';
import { useParams, useNavigate } from 'react-router-dom';

async function getCustomer(no) {
    const response = await axios.get(`${API_URL}/detailview/${no}`);
    return response.data;
}

const EditCustomer = () => {
    const { no } = useParams();
    const [ formData, setFormData ] = useState({
        c_name: "",
        c_phone: "",
        c_birth: "",
        c_gender: "",
        c_add: "",
        c_adddetail: ""
    })  
  
    // const [state] = useAsync(()=>getCustomer(no), [no]);
    const [state] = useAsync(()=>getCustomer(no), [no]);
    const { loading, data: customer, error} = state;
    useEffect(()=>{
        setFormData({
            // 앞에는 인풋의 네임값(c_add), 뒤에는 db 컬럼값(customer.add1)
            c_name: customer? customer.name : "",
            c_phone: customer? customer.phone : "", 
            c_birth: customer? customer.birth : "", 
            c_gender: customer? customer.gender : "", 
            c_add: customer? customer.add1 : "", 
            c_adddetail: customer? customer.add2 : "",  
        })
    }, [customer]);
    const navigate = useNavigate(); 
    // console.log(customer);

    // 우편번호 관리하기
    const onAddData = (data) => {
        console.log(data);
        setFormData({
            ...formData,
            c_add: data.address
        })
    }
    // 팝업창 상태 관리
    const [ isPopupOpen, setIsPopupOpen ] = useState(false);
    // 팝업창 상태 true로 변경
    const openPostCode = ()=> {
        setIsPopupOpen(true);
    }
    // 팝업창 상태 false로 변경
    const closePostCode = () => {
        setIsPopupOpen(false);
    }

    const onChange = (e) => {
        const { name, value } = e.target;
        setFormData({
            ...formData,
            [name]: value
        })
    }
    // 폼 submit 이벤트
    const onSumbit = (e) => {
        // form에 원래 연결된 이벤트를 제거
        e.preventDefault();
        // console.log(formData);
        // 전화번호가 숫자인지 체크하기
        if(isNaN(formData.c_phone)){
            alert('전화번호는 숫자만 입력해주세요');
            setFormData({
                ...formData,
                c_phone: ""
            })
        } 
        // input에 값이 있는지 체크하고
        // 입력이 다되어있으면 post전송
        else if( formData.c_name.value !== "" && formData.c_phone.value !== "" && 
        formData.c_birth.value !== "" && formData.c_gender.value !=="" &&
        formData.c_add.value !== "" && formData.c_adddetail.value !== "" ){
            updateCustomer();
        } else {
            alert('모든 항목을 기입해주세요');
        }
    }
    // 수정하기
    function updateCustomer() {
        axios.put(`${API_URL}/edit/${no}`, formData)
        .then(result=> {
            console.log('수정되었습니다.')
            navigate('/');
        })
        .catch(err=> {
            console.log(err);
        })
    }

    if(loading) return <div>로딩중입니다...</div>
    if(error) return <div>에러가 발생했습니다.</div>
    if(!customer) return null;
    // console.log(customer.name);
return (
    <div>
        <h2>고객 정보 수정하기</h2>
        <form onSubmit={onSumbit}>
            <Table>
                <TableBody>
                    <TableRow>
                        <TableCell>이름</TableCell>
                        <TableCell>
                            <input name="c_name" type="text"
                             value={formData.c_name}
                            onChange={onChange}/>
                        </TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>연락처</TableCell>
                        <TableCell>
                            <input name="c_phone" type="text"
                                value={formData.c_phone}
                            onChange={onChange}/>
                        </TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>생년월일</TableCell>
                        <TableCell>
                            <input name="c_birth" type="date"
                              value={formData.c_birth}
                            onChange={onChange}/>
                        </TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>성별</TableCell>
                        <TableCell>
                            여성<input name="c_gender" type="radio"
                            value="여성"
                            onChange={onChange}
                            checked={formData.c_gender === "여성" ? true : false }/>
                            남성<input name="c_gender" type="radio"
                            value="남성"
                            onChange={onChange}
                            checked={formData.c_gender === "남성" ? true : false }/>
                        </TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell>주소</TableCell>
                        <TableCell>
                            <input name="c_add" type="text"
                            value={formData.c_add}
                            onChange={onChange}/>
                            <input name="c_adddetail" type="text"
                            value={formData.c_adddetail}
                            onChange={onChange}/>
                            <button type="button" onClick={openPostCode}>우편번호 검색</button>
                            <div id="popupDom">
                                {isPopupOpen && (
                                    <PopupDom>
                                        <PopupPostCode onClose={closePostCode}
                                        onAddData={onAddData}
                                        />
                                    </PopupDom>
                                )}
                            </div>
                        </TableCell>
                    </TableRow>
                    <TableRow>
                        <TableCell colSpan={2}>
                           <button type="submit">등록</button>
                           <button type="reset">취소</button>
                        </TableCell>
                    </TableRow>
                </TableBody>
            </Table>
        </form>
    </div>
);
};

export default EditCustomer;

 

components/Footer.js

import React from 'react';

const Footer = () => {
    return (
        <div id="footer">
            <h1>그린 고객 관리</h1>
            <p>
                대표자: 김그린 | 사업자 등록번호 214-86-12345<br/>
                통신판매업신고: 강남 13711호 | 학원등록번호: 강남 제 1104호<br/>
                주소: 서울시 강남구 역삼동 강남빌딩 5층<br/>
                copyright (c) 2022 gitaAcademy
            </p>
        </div>
    );
};

export default Footer;

 

components/Header.js

import React from 'react';
import { Link } from 'react-router-dom';

const Header = () => {
    return (
        <div id="header">
            <h1>그린고객센터</h1>
            <ul>
                <li><Link to="/">고객리스트보기</Link></li>
                <li><Link to="/write">신규 고객 등록하기</Link></li>
                <li>고객 검색</li>
            </ul>
        </div>
    );
};

export default Header;

 

components/PopupDom.js

import  ReactDOM  from "react-dom";

const PopupDom = ({children}) => {
    const el = document.getElementById('popupDom');
    return ReactDOM.createPortal(children, el);
};

export default PopupDom;

 

components/PopupPostCode.js

import React from 'react';
import DaumPostcode from "react-daum-postcode";
 
const PopupPostCode = (props) => {
	// 우편번호 검색 후 주소 클릭 시 실행될 함수, data callback 용
    const handlePostCode = (data) => {
        let fullAddress = data.address;
        let extraAddress = ''; 
        
        if (data.addressType === 'R') {
          if (data.bname !== '') {
            extraAddress += data.bname;
          }
          if (data.buildingName !== '') {
            extraAddress += (extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName);
          }
          fullAddress += (extraAddress !== '' ? ` (${extraAddress})` : '');
        }
        console.log(data)
        console.log(fullAddress)
        console.log(data.zonecode)
        props.onAddData(data);
        
    }

    const postCodeStyle = {
        display: "block",
        position: "absolute",
        top: '50%',
        left: '50%',
        transform:'translate(-50%,-50%)',
        width: "600px",
        height: "600px",
        padding: "7px",
        border: "2px solid #666"
      };
 
    return(
        <div>
            <DaumPostcode style={postCodeStyle} onComplete={handlePostCode} /> 
            <button type='button' onClick={() => {props.onClose()}} className='postCode_btn'>입력</button>
        </div>
    )
}
 
export default PopupPostCode;

 

config/contatnts.js

export const API_URL = "http://localhost:3001";

 

customHook/useAsync.js

import { useReducer, useEffect } from 'react'
const initialState = {
    loading: false,
    data: null,
    error: null
}

function reducer(state, action) {
    switch(action.type) {
        case "LOADING":
            return {
                loading: true,
                data: null,
                error: null
            };
        case "SUCCESS":
            return {
                loading: false,
                data: action.data,
                error: null
            }
        case "ERROR":
            return {
                loading: false,
                data: null,
                error: action.error
            }
        default:
            return state;
    }
}
function useAsync(callback, deps=[]) {
    const [state, dispatch] = useReducer(reducer, initialState);
    const fetchDate = async () => {
        dispatch({type: "LOADING"});
        try {
            const data = await callback();
            dispatch({
                type: "SUCCESS",
                data: data
            })
        }
        catch(e) {
            dispatch({
                type: "ERROR",
                error: e
            })
        }
    }
    useEffect(()=>{
        fetchDate();
    //eslint-disable-next-line
    }, deps);
    return [state, fetchDate];
}

export default useAsync

 

modules/customer.js

import axios from 'axios'
import { API_URL } from '../config/contansts'

// 리덕스 액션타입, 초기값, 액션 생성함수, 리듀서
const GET_CUSTOMERS = "GET_CUSTOMERS";
const GET_CUSTOMERS_ERROR = "GET_CUSTOMERS_ERROR";
const GET_CUSTOMERS_SUCCESS = "GET_CUSTOMERS_SUCCESS";
const SET_INPUT = "SET_INPUT";
const SET_RESET = "SET_RESET";

// 초기값 설정
const initialState = {
    customers: {
        loading: false,
        data: null,
        error: null
    },
    addCustomer: {
        c_name: "",
        c_phone: "",
        c_birth: "",
        c_gender: "",
        c_add: "",
        c_adddetail: "",
    }
}

// 액션 생성함수
export const setInput = (e) => {
    const { name, value } = e.target;
    return {
        type: SET_INPUT,
        name,
        value
    }
}

// 홈으로 이동 함수
export const goHome = (navigate) => () => {
    navigate('/');
}

// thunk 함수를 사용해서 액션객체 디스패치하기
export const getCustomers = () => async dispatch => {
    dispatch({ type: GET_CUSTOMERS }) // 요청시작
    try{
        const response = await axios.get(`${API_URL}/customers`)
        const customers = response.data;
        dispatch({ type: GET_CUSTOMERS_SUCCESS, customers })
    }
    catch(e) {
        dispatch({ type: GET_CUSTOMERS_ERROR, error: e})
    }
}

export const setSubmit = () => async (dispatch, getState) => {
    const formdata = getState().customers.addCustomer;
    try {
        const response = await axios.post(`${API_URL}/addCustomer`, formdata)
        dispatch({ type: SET_RESET })
    }
    catch(e) {
        dispatch({ type: SET_RESET })
    }
}

// 리듀서 만들기
export default function customers(state = initialState, action) {
    switch(action.type) {
        case GET_CUSTOMERS:
            return {
                ...state,
                customers: {
                    loading: true,
                    data: null,
                    error: null
                }
            }
        case GET_CUSTOMERS_SUCCESS:
            return {
                ...state,
                customers: {
                    loading: false,
                    data: action.customers,
                    error: null
                }
            }
        case GET_CUSTOMERS_ERROR:
            return {
                ...state,
                customers: {
                    loading: false,
                    data: null,
                    error: action.error
                }
            }
        case SET_INPUT:
            return {
                ...state,
                addCustomer: {
                    ...state.addCustomer,
                    [action.name]: action.value
                }
            }
        case SET_RESET:
            return {
                ...state,
                addCustomer: {
                    ...state.addCustomer,
                    c_name: "",
                    c_phone: "",
                    c_birth: "",
                    c_gender: "",
                    c_add: "",
                    c_adddetail: "",
                }
            }
        default:
            return state;
    }
}

 

modules/index.js

import { combineReducers } from "redux";
import customers from './customers';

const rootReducer = combineReducers({ customers });
export default rootReducer;

 

App.css

li { list-style: none; }
a { text-decoration: none; color: inherit; }
.App {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
}
#header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid #ccc;
  margin-bottom: 50px;
}
#header ul {
  display: flex;
}
#header li {
  padding: 0 20px;
}
h1 {
  font-size: 24px;
}
#footer {
  padding-top: 50px;
}

 

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));
console.log(store.getState());
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.StrictMode>
      <BrowserRouter>
      <Provider store={store}>
        <App />
        </Provider>
      </BrowserRouter>
  </React.StrictMode>

);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();