[React] 고객관리 사이트 (Client) redux로 구현하기
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();