ํด๋ผ์ด์ธํธ์ ์๋ฒ์ Dependencies ๋ค์ด๋ฐ๊ธฐ
npm installServer์ Root ๊ฒฝ๋ก, Client๋ clientํด๋ ๊ฒฝ๋กserver/config/dev.js ํ์ผ ์ค์ 
MongoDB ๋ก๊ทธ์ธdev.js ํ์ผ์ ๋ฃ๋๋ค.โญ// server/config/dev.js
module.exports = {
    mongoURI:
      'mongodb+srv://devPark:<password>@react-boiler-plate.ovbtd.mongodb.net/react-shop-app?retryWrites=true&w=majority'
  }

โญ server/models/Product.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const productSchema = mongoose.Schema(
  {
    writer: {
      type: Schema.Types.ObjectId,
      ref: 'User',
    },
    title: {
      type: String,
      maxlength: 50,
    },
    description: {
      type: String,
    },
    price: {
      type: Number,
      default: 0,
    },
    images: {
      type: Array,
      default: [],
    },
    sold: {
      type: Number,
      maxlength: 100,
      default: 0,
    },
    continents: {
      type: Number,
      default: 1,
    },
    views: {
      type: Number,
      default: 0,
    },
  },
  { timestamps: true }
)
const Product = mongoose.model('Product', productSchema)
module.exports = { Product }
Multer
$ npm install multer --save
multipart/form-data ๋ฅผ ๋ค๋ฃจ๊ธฐ ์ํ node.js ์ ๋ฏธ๋ค์จ์ด์ด๋ฉฐ ํจ์จ์ฑ์ ์ต๋ํ ํ๊ธฐ ์ํด busboy ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๊ณ  ์๋ค.โญ server/routes/product.js
const express = require('express')
const router = express.Router()
const multer = require('multer')
const { Product } = require('../models/Product')
var storage = multer.diskStorage({
  // ํ์ผ ์ ์ฅ ๊ฒฝ๋ก์ค์ 
  destination: function (req, file, cb) {
    cb(null, 'uploads/')
  },
  // ์ด๋ค ์ด๋ฆ์ผ๋ก ์ ์ฅ๋ ์ง ์ค์ 
  filename: function (req, file, cb) {
    cb(null, `${Date.now()}_${file.originalname}`)
  }
})
   
var upload = multer({ storage: storage }).single('file')
router.post('/image', (req, res) => {
    // ์ด๋ฏธ์ง๋ฅผ ์ ์ฅ
    upload(req, res, err => {
        if (err) {
            return req.json({ success: false, err })
        }
        // ํด๋ผ์ด์ธํธ๋ก ํ์ผ์ ๋ณด๋ฅผ ์ ๋ฌ
        return res.json({ success: true,
            filePath: res.req.file.path,
            fileName: res.req.file.filename})
    })
})
// ํด๋ผ์ด์ธํธ์์ ๋ณด๋ด์ง ์ ๋ณด๋ฅผ MongoDB์ ์ ์ฅ
router.post('/', (req, res) => {
  const product = new Product(req.body)
  product.save((err) => {
      if (err) return res.status(400).json({ success: false, err })
      return res.status(200).json({ success: true })
  })
})
react-dropzone
$npm install react-dropzone --save
โญ UploadProductPage.js
import React, { useState } from 'react'
import { Button, Form, Input } from 'antd'
import FileUpload from '../../utils/FileUpload'
import axios from 'axios'
const { TextArea } = Input
const Continents = [
    { key: 1, value: 'Africa' },
    { key: 2, value: 'Europe' },
    { key: 3, value: 'Asia' },
    { key: 4, value: 'North America' },
    { key: 5, value: 'South America' },
    { key: 6, value: 'Australia' },
    { key: 7, value: 'Antarctica' }
]
function UploadProductPage(props) {
    const [Title, setTitle] = useState('')
    const [Description, setDescription] = useState('')
    const [Price, setPrice] = useState(0)
    const [Continent, setContinent] = useState(1)
    const [Images, setImages] = useState([])
    const titleChangeHandler = (e) => {
        setTitle(e.currentTarget.value)
    }
    const descriptionChangeHandler = (e) => {
        setDescription(e.currentTarget.value)
    }
    const priceChangeHandler = (e) => {
        setPrice(e.currentTarget.value)
    }
    const continentChangeHandler = (e) => {
        setContinent(e.currentTarget.value)
    }
    // 2. ์์ ์ปดํฌ๋ํธ(FileUpload.js)์ ์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ 
    const updateImages = (newImages) => {
        setImages(newImages)
    }
    // 3. Form์ ์
๋ ฅํ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ๋ก ๋ณด๋ด๊ธฐ
    const submitHandler = (e) => {
        e.preventDefault()
        if (!Title || !Description || !Price || !Continent || Images.length === 0) {
            return alert('๋ชจ๋  ๊ฐ์ ๋ฃ์ด์ฃผ์
์ผ ํฉ๋๋ค.')
        }
        const body = {
            // UploadProductPage.js๋ auth.js์ ์์์ปดํฌ๋ํธ
            // let user = useSelector(state => state.user)
            // <SpecificComponent user={user} />
            // ๋ก๊ทธ์ธ ๋ ์ฌ๋์ ID
            writer: props.user.userData._id,
            title: Title,
            description: Description,
            price: Price,
            images: Images,
            continents: Continent
        }
        axios.post('/api/product', body)
            .then(res => {
                if (res.data.success) {
                    alert('์ํ ์
๋ก๋์ ์ฑ๊ณต ํ์ต๋๋ค.')
                    props.history.push('/')
                } else {
                    alert('์ํ ์
๋ก๋์ ์คํจ ํ์ต๋๋ค.')
                }
            })
    }
    return (
        <div style={{ maxWidth: '700px', margin: '2rem auto' }}>
            <div style={{ textAlign: 'center', marginBottom: '2rem' }}>
                <h2> ์ฌํ ์ํ ์
๋ก๋ </h2>
            </div>
            <Form onSubmit={submitHandler}>
                {/* DropZone : 
                1. ์์ ์ปดํฌ๋ํธ(FileUpload.js)์ ์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ๋ค. */}
                <FileUpload refreshFunction={updateImages}/>
                <br />
                <br />
                <label>์ด๋ฆ</label>
                <Input vluae={Title} onChange={titleChangeHandler}/>
                <br />
                <br />
                <label>์ค๋ช
</label>
                <TextArea value={Description} onChange={descriptionChangeHandler}/>
                <br />
                <br />
                <label>๊ฐ๊ฒฉ($)</label>
                <Input type='number' value={Price} onChalnge={priceChangeHandler}/>
                <br />
                <br />
                <select value={Continent} onChange={continentChangeHandler}>
                    {Continents.map(item => (
                        <option key={item.key} value={item.key}> {item.value} </option>
                    ))}
                </select>
                <br />
                <br />
                <Button type='submit' onClick={submitHandler}>
                    ํ์ธ
                </Button>
             </Form>
        </div>
    )
}
export default UploadProductPage
โญ components/utils/FileUpload.js
import React, { useState } from 'react'
import Dropzone from 'react-dropzone'
import { Icon , Input } from 'antd'
import axios from 'axios'
function FileUpload(props) {
    const [Images, setImages] = useState([])
    const dropHandler = (files) => {
        // ์ด๋ฏธ์ง๋ฅผ Ajax๋ก ์
๋ก๋ํ  ๊ฒฝ์ฐ Form ์ ์ก์ด ํ์
        let formData = new FormData()
        const config = {
            header: { 'content-type': 'multipart/form-data'}
        }
        // append๋ฅผ ํตํด ํค-๊ฐ ํ์์ผ๋ก ์ถ๊ฐ
        formData.append('file', files[0])
        axios.post('/api/product/image', formData, config)
            .then(res => {
                if (res.data.success) {
                    // ์๋ฒ์ ์ต์ข
 ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๊ธฐ ์ํด ์ ์ฅ
                    setImages([...Images, res.data.filePath])
                    // ๋ถ๋ชจ ์ปดํฌ๋ํธ(UploadProductPage.js)๋ก ๋ฐ์ดํฐ ์
๋ฐ์ดํธ
                    props.refreshFunction([...Images, res.data.filePath])
                } else {
                    alert('ํ์ผ์ ์ ์ฅํ๋๋ฐ ์คํจํ์ต๋๋ค.')
                }
            })
    }
    // ์ด๋ฏธ์ง๋ฅผ ์ง์ฐ๋ ๊ธฐ๋ฅ
    const deleteHandler = (image) => {
        const currentIndex = Images.indexOf(image)
        let newImages = [...Images]
        newImages.splice(currentIndex, 1)
        setImages(newImages)
        // ๋ถ๋ชจ ์ปดํฌ๋ํธ(UploadProductPage.js)๋ก ๋ฐ์ดํฐ ์
๋ฐ์ดํธ
        props.refreshFunction(newImages)
    }
    return (
        // ์ด๋ฏธ์ง ์
๋ก๋ Form
        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <Dropzone onDrop={dropHandler}>
                {({getRootProps, getInputProps}) => (
                    <div 
                        style={{
                            width: 300, height: 240, border: '1px solid lightgray',
                            display: 'flex', alignItems: 'center', justifyContent: 'center'
                        }}
                        {...getRootProps()}>
                        <Input {...getInputProps()} />
                        <Icon type='plus' style={{ fontSize: '3rem'}}/>
                    </div>
                )}
            </Dropzone>
            {/* ์
๋ก๋๋ ์ด๋ฏธ์ง๋ฅผ ๋ฃ์ Form */}
            <div style={{ display: 'flex', width: '350px', height: '240px', overflowX: 'scroll' }}>
                {Images.map((image, index) => (
                    <div onClick={() => deleteHandler(image)} key={index}>
                        <img style={{ minWidth: '300px', width: '300px', height: '240px' }}
                            src={`http://localhost:5000/${image}`}
                        />
                    </div>
               ))}
            </div>
        </div>
    )
}
export default FileUpload

LIMIT : ์ฒ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ๋์ ๋๋ณด๊ธฐ ๋ฒํผ์ ๋๋ฌ์ ๊ฐ์ ธ์ฌ๋SKIP : ์ด๋์๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ ์ค๋์ง์ ๋ํ ์์น2rd Skip = 0 + 6โญ LandingPage.js
import { Icon, Col, Card, Row} from 'antd'
import Meta from 'antd/lib/card/Meta'
import ImageSlider from '../../utils/ImageSlider'
function LandingPage() {
    
    // 1. ๋ ๋์นด๋, ์ด๋ฏธ์ง์ฌ๋ผ์ด๋
    const [Products, setProducts] = useState([])
    
    // 2. ๋๋ณด๊ธฐ ๊ธฐ๋ฅ
    const [Skip, setSkip] = useState(0)
    const [Limit, setLimit] = useState(8)
    const [PostSize, setPostSize] = useState(0)
    // 2. ๋๋ณด๊ธฐ ๊ธฐ๋ฅ
    useEffect(() => {
        // ์ํ 8๊ฐ๋ง ๊ฐ์ ธ์ค๊ธฐ
        let body = {
            skip: Skip,
            limit: Limit
        }
        getProducts(body)
    }, [])
    // 2. ๋๋ณด๊ธฐ ๊ธฐ๋ฅ
    const getProducts = (body) => {
      
        axios.post('/api/product/products', body)
            .then(response => {
                if (response.data.success) {
                    if (body.loadMore) {
                        setProducts([...Products, ...response.data.productInfo])
                    } else {
                        setProducts(response.data.productInfo)
                    }
                    setPostSize(response.data.postSize)
                } else {
                    alert(' ์ํ๋ค์ ๊ฐ์ ธ์ค๋๋ฐ ์คํจํ์ต๋๋ค. ')
                }
            })
      
    }
    
    // 2. ๋๋ณด๊ธฐ ๊ธฐ๋ฅ
    const loadMoreHandler = () => {
        // ๋๋ณด๊ธฐ ๋ฒํผ์ ๋๋ ์ ๋ ์ถ๊ฐ product๋ฅผ ๊ฐ์ ธ์ฌ๋๋ Skip ๋ถ๋ถ์ด ๋ฌ๋ผ์ง๋ค
        // Skip (0 -> 8) + Limit (8 -> 8)
        let skip = Skip + Limit
        let body = {
            skip: skip,
            limit: Limit,
            loadMore: true
        }
        getProducts(body)
        setSkip(skip)
    }
    // 1. ๋ ๋์นด๋, ์ด๋ฏธ์ง์ฌ๋ผ์ด๋
    const renderCards = Products.map((product, index) => {
        return <Col lg={6} md={8} xs={24} key={index}>
            <Card
                cover={<a href={`/product/${product._id}`}><ImageSlider images={product.images} /></a>}
            >
                <Meta
                    title={product.title}
                    description={`$${product.price}`}
                />
            </Card>
        </Col> 
    })
    return (
        <div style={{ width: '75%', margin: '3rem auto' }}>
           
            <div style={{ textAlign: 'center' }}>
                <h2>Let's Travel Anywhere<Icon type="rocket"/></h2>
            </div>
            {/* 1. ๋ ๋์นด๋/์ด๋ฏธ์ง์ฌ๋ผ์ด๋ */}
            <Row gutter={[16, 16]}>
                {renderCards}
            </Row>
            <br/>
            {/* 2. ๋๋ณด๊ธฐ ๊ธฐ๋ฅ */}
            {PostSize >= Limit &&
                <div style={{ display: 'flex', justifyContent: 'center' }}>
                    <button onClick={loadMoreHandler}>๋๋ณด๊ธฐ</button>
                </div>
            }
        </div>
    )
}
โญ components/utils/ImageSlider.js
import React from 'react'
import { Carousel } from 'antd'
function ImageSlider(props) {
    return (
        <div>
            <Carousel autoplay>
                {props.images.map((image, index) => (
                    <div key={index}>
                        <img style={{ width: '100%', maxHeight: '150px' }}
                            src={`http://localhost:5000/${image}`} />
                    </div>
                ))}
            </Carousel>
        </div>
    )
}
export default ImageSlider
โญ server/routes/product
router.post('/products', (req, res) => {
  // 2. ๋๋ณด๊ธฐ ๊ธฐ๋ฅ
  // limit๊ณผ skip์ ์ด์ฉํด ์ ํ๋ ์์ product ๊ฐ์ ธ์ค๊ธฐ
  let limit = req.body.limit ? parseInt(req.body.limit) : 20
  let skip = req.body.skip ? parseInt(req.body.skip) : 0
  	
  let findArgs = {}
  
  // 2. ๋๋ณด๊ธฐ ๊ธฐ๋ฅ
  Product.find(findArgs)
    .populate('writer')
    .skip(skip)
    .limit(limit)
    .exec((err, productInfo) => {
    if (err) return res.status(400).json({ success: false, err}) 
    return res.status(200).json({
      success: true, productInfo,
      postSize: productInfo.length
    })
  })
}  
โญ LandingPage/Sections/Datas.js
const continents = [
    {
        "_id": 1,
        "name": "Africa"
    },
    {
        "_id": 2,
        "name": "Europe"
    },
    {
        "_id": 3,
        "name": "Asia"
    },
    {
        "_id": 4,
        "name": "North America"
    },
    {
        "_id": 5,
        "name": "South America"
    },
    {
        "_id": 6,
        "name": "Australia"
    },
    {
        "_id": 7,
        "name": "Antarctica"
    }
]
const price = [
    {
        "_id": 0,
        "name": "Any",
        "array": []
    },
    {
        "_id": 1,
        "name": "$0 to $249",
        "array": [0, 249]
    },
    {
        "_id": 2,
        "name": "$250 to $499",
        "array": [250, 499]
    },
    {
        "_id": 3,
        "name": "$500 to $749",
        "array": [500, 749]
    },
    {
        "_id": 4,
        "name": "$750 to $999",
        "array": [750, 999]
    },
    {
        "_id": 5,
        "name": "More than $1000",
        "array": [1000, 1500000]
    }
]
export {
    continents,
    price
}
โญ LandingPage.js
import { continents, price } from './Sections/Datas'
import CheckBox from './Sections/CheckBox'
import Radiobox from './Sections/RadioBox'
function LandingPage() {
  
	// 3. ์ฒดํฌ๋ฐ์ค ํํฐ
    const [Filters, setFilters] = useState({
        continents: [],
        price: []
    })
    
    // 3. ์ฒดํฌ๋ฐ์ค ํํฐ
    const showFilteredResults = (filters) => {
      let body = {
        skip: 0,
        limit: Limit,
        filters: filters
      }
      getProducts(body)
      setSkip(0)
    }
        
	// 3. ์ฒดํฌ๋ฐ์ค ํํฐ
    const handleFilters = (filters, category) => {
        const newFilters = {...Filters}
        newFilters[category] = filters
        if (category === "price") {
            let priceValues = handlePrice(filters)
            newFilters[category] = priceValues
        }
        showFilteredResults(newFilters)
        setFilters(newFilters)
    }
    
  return (
    <div style={{ width: '75%', margin: '3rem auto' }}>
    
        <Row gutter={[16, 16]}>
       	    {/* 3. ์ฒดํฌ๋ฐ์ค ํํฐ */}
              <Col lg={12} xs={24}>
              <Checkbox list={continents}
          handleFilters={filters => handleFilters(filters, "continents")} />
              </Col>
            {/* 4. ๋ผ๋์ค๋ฐ์ค ํํฐ */}
            <Col lg={12} xs={24}>
              <Radiobox list={price}
              handleFilters={filters => handleFilters(filters, "price")} />
              </Col>
            </Row>
	</div>
  )
}
โญ LandingPage/Sections/CheckBox.js
import React, { useState } from 'react'
import { Collapse, Checkbox } from 'antd'
const { Panel } = Collapse
function CheckBox(props) {
    const [Checked, setChecked] = useState([])
    const handleToggle = (value) => {
        // ํด๋ฆญํ ์ฒดํฌ๋ฐ์ค์ index๋ฅผ ๊ตฌํ๊ณ 
        const currentIndex = Checked.indexOf(value)
        // ์ ์ฒด checked๋ state์์ ํ์ฌ ๋๋ฅธ Checkbox๊ฐ ์ด๋ฏธ ์๋ค๋ฉด
        const newChecked = [...Checked]
        // (value ๊ฐ์ด ์๋ค๋ฉด value๊ฐ์ ๋ฃ์ด์ค๋ค)
        if (currentIndex === -1) {
            newChecked.push(value)
        // ๋นผ์ฃผ๊ณ 
        } else {
            newChecked.splice(currentIndex, 1)
        }
        // state์ ๋ฃ์ด์ค๋ค.
        setChecked(newChecked)
        
        // ๋ถ๋ชจ ์ปดํฌ๋ํธ(LandingPage.js)์ ์
๋ฐ์ดํธ
        props.handleFilters(newChecked)
    }
    // ๋ถ๋ชจ ์ปดํฌ๋ํธ(LandingPage.js)์ ์
๋ฐ์ดํธ
    const renderCheckBoxLists = () => props.list && props.list.map((value, index) => (
        <React.Fragment key={index}>
            <Checkbox onChange={() => handleToggle(value._id)} checked={Checked.indexOf(value._id) === -1 ? false : true} />
                <span> {value.name} </span>
        </React.Fragment>
    ))
    return (
        <div>
            <Collapse defaultActiveKey={['0']}>
                <Panel header="Continents" key="1">
                    {renderCheckBoxLists()}
                    
                </Panel>
            </Collapse>
        </div>
    )
}
export default CheckBox
list.map((value) => <Radio key={value._id}></Radio>)list.map((value, index) => <Radio key={index}></Radio>)โญ LandingPage/Sections/RadioBox.js
import React, { useState } from 'react'
import { Collapse, Radio } from 'antd'
const { Panel } = Collapse
function RadioBox(props) {
    
    // Value : price._id (Datas.js)
    const [Value, setValue] = useState(0)
    const renderRadioBox = () => (
      // ๋ถ๋ชจ ์ปดํฌ๋ํธ๋ก(LandingPage.js) ์
๋ฐ์ดํธ
        props.list && props.list.map(value => (
            <Radio key={value._id} value={value._id}> {value.name} </Radio>
        ))
    )
    
    const handleChange = (event) => {
        setValue(event.target.value)
      // ๋ถ๋ชจ ์ปดํฌ๋ํธ๋ก(LandingPage.js) ์
๋ฐ์ดํธ
        props.handleFilters(event.target.value)
    }
    return (
        <div>
            <Collapse defaultActiveKey={['0']}>
                <Panel header="Price" key="1">
                    
                    <Radio.Group onChange={handleChange} value={Value}>
                        {renderRadioBox()}
                    </Radio.Group>
                </Panel>
            </Collapse>
        </div>
    )
}
export default RadioBox
โญ server/routes/product
router.post('/products', (req, res) => {
  // 4. ๋ผ๋์ค ๋ฐ์ค ํํฐ
  // req.body.filters -> continents: "[1, 2, 3..]" (LandingPage.js)
  // key -> "continents": [1, 2, 3..]
  for (let key in req.body.filters) {
    if (req.body.filters[key].length > 0) {
      if (key === 'price') {
        findArgs[key] = {
          //Greater than equal
          $gte: req.body.filters[key][0],
          //Less than equal
          $lte: req.body.filters[key][1]
        }
      } else {
        findArgs[key] = req.body.filters[key]
      }
    }
  }
})
โญ LandingPage.js
import SearchFeature from './Sections/SearchFeature'
function LandingPage() {
    
    // 5. ๊ฒ์ ๊ธฐ๋ฅ
    const [SearchTerm, setSearchTerm] = useState('')
    // 5. ๊ฒ์ ๊ธฐ๋ฅ
    const updateSearchTerm = (newSearchTerm) => {
        let body = {
            skip: 0,
            limit: Limit,
            filters: Filters,
            searchTerm: newSearchTerm
        }
        setSkip(0)
        setSearchTerm(newSearchTerm)
        getProducts(body)
    }
    return (
        <div style={{ width: '75%', margin: '3rem auto' }}>
      
            {/* 5. ๊ฒ์ ๊ธฐ๋ฅ */}
            <div style={{ display: 'flex', justifyContent: 'flex-end', margin: '1rem auto' }}>
                <SearchFeature
                    refreshFunction={updateSearchTerm}
                />
            </div>
        </div>
    )
}
export default LandingPage
โญ LandingPage/Sections/SearchFeature.js
import React, { useState } from 'react'
import { Input } from 'antd';
const { Search } = Input
function SearchFeature(props) {
    const [SearchTerm, setSearchTerm] = useState('')
    const searchHandler = (event) => {
        setSearchTerm(event.currentTarget.value)
        props.refreshFunction(event.currentTarget.value)
    }
    return (
        <div>
            <Search
                placeholder="input search text"
                onChange={searchHandler}
                style={{ width: 200 }}
                value={SearchTerm}
            />
        </div>
    )
}
export default SearchFeature
โญ server/models/Product.js
const productSchema = mongoose.Schema({
    ...
productSchema.index({
    title: 'text',
    description: 'text'
}, {
    weights:{
        title: 5,
        description: 1
    }
})
โญ server/routes/product.js
router.post('/products', (req, res) => {
  // 5. ๊ฒ์ ๊ธฐ๋ฅ
  let term = req.body.searchTerm // 'Mexico'
  let findArgs = {}
  // 5. ๊ฒ์ ๊ธฐ๋ฅ
  if (term) {
    Product.find(findArgs)
    .find({ $text: { $search: term } })
      .populate('writer')
      .skip(skip)
      .limit(limit)
      .exec((err, productInfo) => {
        if (err) return res.status(400).json({ success: false, err}) 
        return res.status(200).json({
          success: true, productInfo,
          postSize: productInfo.length
        })
      })
  }
})

โญ server/routes/product.js
router.get('/products_by_id', (req, res) => {
  // query๋ฅผ ์ด์ฉํด์ ํด๋ผ์ด์ธํธ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ๋๋ req.query
  let type = req.query.type
  let productIds = req.query.id
  if (type === "array") {
      let ids = req.query.id.split(',')
      productIds = ids.map(item => {
          return item
      })
  }
  Product.find({ _id: { $in: productIds } })
      .populate('writer')
      .exec((err, product) => {
          if (err) return res.status(400).send(err)
          return res.status(200).send(product)
      })
})
โญ App.js
import DetailProductPage from './views/DetailProductPage/DetailProductPage'
function App() {
  return (
        <Switch>
          <Route exact path="/product/:productId" component={Auth(DetailProductPage, null)} />
        </Switch>
  )
}
โญ DetailProductPage.js
import ProductImage from './Sections/ProductImage'
import ProductInfo from './Sections/ProductInfo'
import { Row, Col } from 'antd'
function DetailProductPage(props) {
  const [Product, setProduct] = useState({})
  useEffect(() => {
    axios
      .get(`/api/product/products_by_id?id=${productId}&type=single`)
      .then((response) => {
        if (response.data.success) {
          setProduct(response.data.product[0])
        } else {
          alert('์์ธ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ์คํจํ์ต๋๋ค.')
        }
      })
  }, [])
  return (
    <div style={{ width: '100%', padding: '3rem 4rem'}}>
    
        <div style= {{ display: 'flex', justifyContent: 'center' }}>
            <h1>{Product.title}</h1>
        </div>
        <br />
        <Row gutter={[16, 16]}>
            <Col lg={12} sm={24}>
                {/* ProductImage */}
                <ProductImage detail={Product}/>
            </Col>
            <Col lg={12} sm={24}>
                {/* ProductInfo */}
                <ProductInfo detail={Product}/>
            </Col>
        </Row>
    </div>
    )
  )
}
$ npm install react-image-gallery --save
๋ฉ์ธ์ด๋ฏธ์ง, ์ธ๋ค์ผ
react-image-gallery
gm -npm
โญ index.css
@import '~react-image-gallery/styles/css/image-gallery.css';
โญ DetailProductPage/Sections/ProductImage.js
import React, { useState, useEffect } from 'react'
import ImageGallery from 'react-image-gallery'
function ProductImage(props) {
    const [Images, setImages] = useState([])
    useEffect(() => {
        
        if (props.detail.images && props.detail.images.length > 0) {
            let images = []
            props.detail.images.map(item => {
                images.push({
                    original: `http://localhost:5000/${item}`,
                    thumbnail: `http://localhost:5000/${item}`
                })
            })
            setImages(images)
        }
    }, [props.detail])
    const images = [
        {
          original: 'https://picsum.photos/id/1018/1000/600/',
          thumbnail: 'https://picsum.photos/id/1018/250/150/',
        },
        {
          original: 'https://picsum.photos/id/1015/1000/600/',
          thumbnail: 'https://picsum.photos/id/1015/250/150/',
        },
        {
          original: 'https://picsum.photos/id/1019/1000/600/',
          thumbnail: 'https://picsum.photos/id/1019/250/150/',
        },
      ]
    return (
        <div>
            <ImageGallery items={Images} />
        </div>
    )
}
export default ProductImage
โญ DetailProductPage/Sections/ProductInfo.js
import React from 'react'
import { Button, Descriptions } from 'antd'
function ProductInfo(props) {
  
  const clickHandler = () => {}
  return (
    <div>
      <Descriptions title="Product Info">
        <Descriptions.Item label="Price">
          {props.detail.price}
        </Descriptions.Item>
        <Descriptions.Item label="Sold">{props.detail.sold}</Descriptions.Item>
        <Descriptions.Item label="View">{props.detail.views}</Descriptions.Item>
        <Descriptions.Item label="Description">
          {props.detail.description}
        </Descriptions.Item>
      </Descriptions>
      <br />
      <br />
      <br />
      <div style={{ display: 'flex', justifyContent: 'center' }}>
        <Button size="large" shape="round" type="danger" onClick={clickHandler}>
          Add to Cart
        </Button>
      </div>
    </div>
  )
}
export default ProductInfo
โญ server/models/User.js
const userSchema = mongoose.Schema({
    cart: {
        type: Array,
        default: []
    },
    history: {
        type: Array,
        default: []
    }
})
โญ server/routes/users.js
router.get('/auth', auth, (req, res) => {
    res.status(200).json({
        cart: req.user.cart,
        history: req.user.history
    })  
})
router.post('/addToCart', auth, (req, res) => {
  
  // ๋จผ์  User Collection์ ํด๋น ์ ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ
  // auth ๋ฏธ๋ค์จ์ด๋ฅผ ํต๊ณผํ๋ฉด์ req.user ์์ user ์ ๋ณด๊ฐ ๋ด๊ธด๋ค
  User.findOne({ _id: req.user._id }, (err, userInfo) => {
    
    // ๊ฐ์ ธ์จ ์ ๋ณด์์ ์นดํธ์๋ค ๋ฃ์ผ๋ ค ํ๋ ์ํ์ด ์ด๋ฏธ ๋ค์ด ์๋์ง ํ์ธ
    let duplicate = false
    userInfo.cart.forEach((item) => {
      if (item.id === req.body.productId) {
        duplicate = true
      }
    })
    // ์ํ์ด ์ด๋ฏธ ์์๋ -> ์ํ ๊ฐ์๋ฅผ 1๊ฐ ์ฌ๋ฆฌ๊ธฐ
    if (duplicate) {
      User.findOneAndUpdate(
        { _id: req.user._id, 'cart.id': req.body.productId },
        { $inc: { 'cart.$.quantity': 1 } },
        // ์
๋ฐ์ดํธ๋ ์ ๋ณด๋ฅผ ๋ฐ๊ธฐ ์ํด { new: true }๋ฅผ ์ฌ์ฉ
        { new: true },
        (err, userInfo) => {
          if (err) return res.status(200).json({ success: false, err })
          res.status(200).send(userInfo.cart)
        }
      )
    }
    // ์ํ์ด ์ด๋ฏธ ์์ง ์์๋ -> ํ์ํ ์ํ ์ ๋ณด ์ํ ID ๊ฐ์ 1, ๋ ์ง ์ ๋ ๋ค ๋ฃ์ด์ค์ผํจ
    else {
      User.findOneAndUpdate(
        { _id: req.user._id },
        {
          $push: {
            cart: {
              id: req.body.productId,
              quantity: 1,
              date: Date.now(),
            },
          },
        },
        { new: true },
        (err, userInfo) => {
          if (err) return res.status(400).json({ success: false, err })
          res.status(200).send(userInfo.cart)
        }
      )
    }
  })
})
โญ _actions/types.js
export const ADD_TO_CART = 'add_to_cart'
โญ _actions/user_actions.js
import {
    ADD_TO_CART
} from './types'
export function addToCart(id) {
    let body = {
        productId: id
    }
    const request = axios.post(`${USER_SERVER}/addToCart`, body)
        .then(response => response.data)
    return {
        type: ADD_TO_CART,
        payload: request
    }
}
โญ _reducers/user_reducer.js
import {
    ADD_TO_CART
} from '../_actions/types'
export default function(state={},action){
    switch(action.type){
        case ADD_TO_CART:
            return {
                ...state,
                userData: {
                    ...state.userData,
                    cart: action.payload
                }
            }
        default:
            return state
    }
}

์นดํธ ์์ ๋ค์ด๊ฐ ์๋ ์ํ๋ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ฐ์ ธ์ค๊ธฐ
User Collection, Product Collection
์ฐจ์ด์  : Quantity๊ฐ ์๋์ง ์๋์ง
๊ทธ๋์ : Product Collection๋ Quantity ์ ๋ณด๊ฐ ํ์
โญ server/routes/users.js
router.post("/addToCart", auth, (req, res) => {
    
    // ๋จผ์  User Collection์ ํด๋น ์ ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ
    // auth ๋ฏธ๋ค์จ์ด๋ฅผ ํต๊ณผํ๋ฉด์ req.user ์์ user ์ ๋ณด๊ฐ ๋ด๊ธด๋ค
    User.findOne({ _id: req.user._id },
        (err, userInfo) => {
            // ๊ฐ์ ธ์จ ์ ๋ณด์์ ์นดํธ์๋ค ๋ฃ์ผ๋ ค ํ๋ ์ํ์ด ์ด๋ฏธ ๋ค์ด ์๋์ง ํ์ธ
            let duplicate = false
            userInfo.cart.forEach((item) => {
                if (item.id === req.body.productId) {
                    duplicate = true
                }
            })
            // ์ํ์ด ์ด๋ฏธ ์์๋ -> ์ํ ๊ฐ์๋ฅผ 1๊ฐ ์ฌ๋ฆฌ๊ธฐ
            if (duplicate) {
                User.findOneAndUpdate(
                    { _id: req.user._id, "cart.id": req.body.productId },
                    { $inc: {"cart.$.quantity": 1} },
                    // ์
๋ฐ์ดํธ๋ ์ ๋ณด๋ฅผ ๋ฐ๊ธฐ ์ํด { new: true }๋ฅผ ์ฌ์ฉ
                    { new: true },
                    (err, userInfo) => {
                        if (err) return res.status(200).json({ success: false, err})
                        res.status(200).send(userInfo.cart)
                    }
                )
            }
            // ์ํ์ด ์ด๋ฏธ ์์ง ์์๋ -> ํ์ํ ์ํ ์ ๋ณด ์ํ ID ๊ฐ์ 1, ๋ ์ง ์ ๋ ๋ค ๋ฃ์ด์ค์ผํจ
            else {
                User.findOneAndUpdate(
                    { _id: req.user._id },
                    {
                        $push: {
                            cart: {
                                id: req.body.productId,
                                quantity: 1,
                                date: Date.now()
                            }
                        }
                    },
                    { new: true },
                    (err, userInfo) => {
                        if (err) return res.status(400).json( {success: false, err })
                        res.status(200).send(userInfo.cart)
                    }
                )
            }   
        })
})
router.get('/removeFromCart', auth, (req, res) => {
    // **๋จผ์  cart์์ ๋ด๊ฐ ์ง์ฐ๋ ค๊ณ  ํ ์ํ์ ์ง์์ฃผ๊ธฐ** 
    User.findOneAndUpdate(
        { _id: req.user._id },
        {
            "$pull":
                { "cart": { "id": req.query.id } }
        },
        { new: true },
        (err, userInfo) => {
            let cart = userInfo.cart;
            let array = cart.map(item => {
                return item.id
            })
            // **product collection์์  ํ์ฌ ๋จ์์๋ ์ํ๋ค์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ**
            // productIds = ['5e8961794be6d81ce2b94752(2๋ฒ์งธ)', '5e8960d721e2ca1cb3e30de4(3๋ฒ์งธ)'] ์ด๋ฐ์์ผ๋ก ๋ฐ๊ฟ์ฃผ๊ธฐ
            Product.find({ _id: { $in: array } })
                .populate('writer')
                .exec((err, productInfo) => {
                    return res.status(200).json({
                        productInfo,
                        cart
                    })
                })
        }
    )
})
// CartPage.js
import React, { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { getCartItems, removeCartItem, onSuccessBuy } from '../../../_actions/user_actions'
import UserCardBlock from './Sections/UserCardBlock'
import { Empty, Result } from 'antd'
import Paypal from '../../utils/Paypal'
function CartPage(props) {
    const dispatch = useDispatch()
    const [Total, setTotal] = useState(0)
    const [ShowTotal, setShowTotal] = useState(false)
    // Paypal ๊ฒฐ์  ์ฑ๊ณตํ ๋ฐ์ดํฐ
    const [ShowSuccess, setShowSuccess] = useState(false)
    useEffect(() => {
        let cartItems = []
        // ๋ฆฌ๋์ค User state์ cart ์์ ์ํ์ด ๋ค์ด์๋์ง ํ์ธ
        if (props.user.userData && props.user.userData.cart) {
            if (props.user.userData.cart.length > 0) {
                props.user.userData.cart.forEach(item => {
                    cartItems.push(item.id)
                })
                dispatch(getCartItems(cartItems, props.user.userData.cart))
                    .then(response => { calculateTotal(response.payload) })
            }
        }
    }, [props.user.userData])
    // ์นดํธ ์์ ์๋ ์ํ ์ด ๊ธ์ก ๊ณ์ฐ
    let calculateTotal = (cartDetail) => {
        let total = 0
        cartDetail.map(item => {
            total += parseInt(item.price, 10) * item.quantity
        })
        setTotal(total)
        setShowTotal(true)
    }
    // ์นดํธ์ ์๋ ์ํ ์ ๊ฑฐ ๊ธฐ๋ฅ
    let removeFromCart = (productId) => {
        dispatch(removeCartItem(productId))
            .then(response => {
                if (response.payload.productInfo.length <= 0) {
                    setShowTotal(false)
                }
            })
    }
    // Paypal ๊ฒฐ์  ์ฑ๊ณต ํ ๊ธฐ๋ฅ
    const transactionSuccess = (data) => {
        dispatch(onSuccessBuy({
            paymentData: data,
            cartDetail: props.user.cartDetail
        }))
            .then(response => {
                if (response.payload.success) {
                    setShowTotal(false)
                    setShowSuccess(true)
                }
            })
    }
    return (
        <div style={{ width: '85%', margin: '3rem auto' }}>
            <h1>My Cart</h1>
            <div>
                <UserCardBlock products={props.user.cartDetail} removeItem={removeFromCart} />
            </div>
            {ShowTotal ?
                <div style={{ marginTop: '3rem' }}>
                    <h2>Total Amount: ${Total}</h2>
                </div>
                : ShowSuccess ?
                    <Result
                        status="success"
                        title="Successfully Purchased Items"
                    />
                    :
                    <>
                        <br />
                        <Empty description={false} />
                    </>
            }
            {ShowTotal &&
                <Paypal 
                    total={Total}
                    onSuccess={transactionSuccess}
                />
            }
        </div>  
    )
}
export default CartPage
// CartPage/Sections/UserCardBlock.js
import React from 'react'
import './UserCardBlock.css'
function UserCardBlock(props) {
    const renderCartImage = (images) => {
        if (images.length > 0) {
            let image = images[0]
            return `http://localhost:5000/${image}`
        }
    }
    const renderItems = () => (
        props.products && props.products.map((product, index) => (
            <tr key={index}>
                <td>
                    <img style={{ width: '70px' }} alt="product"
                        src={renderCartImage(product.images)} />
                </td>
                <td>
                    {product.quantity} EA
                </td>
                <td>
                    $ {product.price}
                </td>
                <td>
                    <button onClick={() => props.removeItem(product._id)}>
                        Remove 
                    </button>
                </td>
            </tr>
        ))
    )
    return (
        <div>
            <table>
                <thead>
                    <tr>
                        <th>Product Image</th>
                        <th>Product Quantity</th>
                        <th>Product Price</th>
                        <th>Remove from Cart</th>
                    </tr>
                </thead>
                <tbody>
                    {renderItems()}
                </tbody>
            </table>
        </div>
    )
}
export default UserCardBlock
// CartPage/Sections/UserCardBlock.css
table {
  font-family: arial, sans-serif;
  border-collapse: collapse;
  width: 100%;
}
td,
th {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}
tr:nth-child(even) {
  background-color: #dddddd;
}
SandBox Paypal ํ์ ๊ฐ์
Paypal์ ์ํ test ID ๋ง๋ค๊ธฐ
Account name ์ค Default๊ฐ ์ ์จ์๋๊ฑธ๋ก ์ฌ์ฉ
View/Edit Account -> Password ๋ณ๊ฒฝ
Payment Model ๋ง๋ค๊ธฐ
user, data, product๊ฒฐ์  ์ฑ๊ณต ํ์ ํด์ผ ํ ์ผ์?
์นดํธ๋ฅผ ๋น์ฐ๊ธฐ
๊ฒฐ์  ์ ๋ณด ์ ์ฅํ๊ธฐ
Payment Collection (Detailed)User Collection (Simple)npm install async --save (Root ๊ฒฝ๋ก)โญ// server/models/Payment.js
const mongoose = require('mongoose')
const paymentSchema = mongoose.Schema(
  {
    user: {
      type: Array,
      default: [],
    },
    data: {
      type: Array,
      default: [],
    },
    product: {
      type: Array,
      default: [],
    },
  },
  { timestamps: true }
)
const Payment = mongoose.model('Payment', paymentSchema)
module.exports = { Payment }
Paypal Button ๋ง๋ค๊ธฐ
npm install react-paypal-express-checkout --save (Client ๊ฒฝ๋ก)Paypal๋ก ๊ฒฐ์ ํ๊ธฐ
Create app -> App Name -> Client ID -> Paypal.js์ sandbox
Paypal ๋ฒํผ ํด๋ฆญ ํ ๋ก๊ทธ์ธ ํ  ๋ ID๋ Sandbox Accounts์ Account Name
โญ// utils/Paypal.js
import React from 'react'
import PaypalExpressBtn from 'react-paypal-express-checkout'
export default class Paypal extends React.Component {
  render() {
    const onSuccess = (payment) => {
      console.log('The payment was succeeded!', payment)
      this.props.onSuccess(payment)
    }
    const onCancel = (data) => {
      console.log('The payment was cancelled!', data)
    }
    const onError = (err) => {
      console.log('Error!', err)
    }
    let env = 'sandbox'
    โญlet total = this.props.total
    const client = {
      โญsandbox:
        'YQLUoERa2zPTNGSFq3o9QBQPqw2pc3DKnWDn5RrchIixQUF9__bLP0cFpgfgLyh1EGt4S9NJk_H',
      production: 'YOUR-PRODUCTION-APP-ID',
    }
   
    return (
      <PaypalExpressBtn
        env={env}
        client={client}
        currency={currency}
        total={total}
        onError={onError}
        onSuccess={onSuccess}
        onCancel={onCancel}
        style={{
          size: 'large',
          color: 'blue',
          shape: 'rect',
          label: 'checkout',
        }}
      />
    )
  }
}
โญ// CartPage.js
import Paypal from '../../utils/Paypal'
function CartPage(props) {
    ...
    return (
      ...
      {ShowTotal &&
          <Paypal
            total={Total}
          />
      }
    )
}
โญ _actions_types.js
export const GET_CART_ITEMS = 'get_cart_items'
export const REMOVE_CART_ITEM = 'remove_cart_item'
export const ON_SUCCESS_BUY = 'on_success_buy'
โญ _actions_user_actions.js
import axios from 'axios';
import {
    GET_CART_ITEMS,
    REMOVE_CART_ITEM,
    ON_SUCCESS_BUY
} from './types'
import { USER_SERVER } from '../components/Config.js'
export function getCartItems(cartItems, userCart) {
    const request = axios.get(`/api/product/products_by_id?id=${cartItems}&type=array`)
        .then(response => {
            // CartItem๋ค์ ํด๋นํ๋ ์ ๋ณด๋ค์  
            // Product Collection์์ ๊ฐ์ ธ์จํ์ 
            // Quantity ์ ๋ณด๋ฅผ ๋ฃ์ด ์ค๋ค.
            userCart.forEach(cartItem => {
                response.data.forEach((productDetail, index) => {
                    if (cartItem.id === productDetail._id) {
                        response.data[index].quantity = cartItem.quantity
                    }
                })
            })
            return response.data
        })
    return {
        type: GET_CART_ITEMS,
        payload: request
    }
}
export function removeCartItem(productId) {
    const request = axios.get(`/api/users/removeFromCart?id=${productId}`)
        .then(response => {
           //productInfo, cart ์ ๋ณด๋ฅผ ์กฐํฉํด์ CartDetail์ ๋ง๋ ๋ค. 
           response.data.cart.forEach(item => {
                response.data.productInfo.forEach((product, index) => {
                    if (item.id === product._id) {
                        response.data.productInfo[index].quantity = item.quantity
                  }
                })
            })
            return response.data
        })
    return {
        type: REMOVE_CART_ITEM,
        payload: request
    }
}
export function onSuccessBuy(data) {
    const request = axios.post(`/api/users/successBuy`, data)
        .then(response => response.data)
    return {
        type: ON_SUCCESS_BUY,
        payload: request
    }
}
โญ _reducers/user_reducer.js
import {
    GET_CART_ITEMS,
    REMOVE_CART_ITEM,
    ON_SUCCESS_BUY
} from '../_actions/types'
 
export default function(state={},action){
    switch(action.type){
        case REMOVE_CART_ITEM:
            return {
                ...state, cartDetail: action.payload.productInfo,
                userData: {
                    ...state.userData,
                    cart: action.payload.cart
                }
            }
        case ON_SUCCESS_BUY:
            return {
                ...state, cartDetail: action.payload.cartDetail,
                userData: {
                    ...state.userData, cart: action.payload.cart
                }
            }
                default:
            return state
    }
}

โญ History.js
import React from 'react'
function HistoryPage(props) {
    return (
        <div style={{ width: '80%', margin: '3rem auto' }}>
            <div style={{ textAlign: 'center' }}>
                <h1>History</h1>
            </div>
            <br />
            <table>
                <thead>
                    <tr>
                        <th>Payment Id</th>
                        <th>Price</th>
                        <th>Quantity</th>
                        <th>Date of Purchase</th>
                    </tr>
                </thead>
                <tbody>
                    {props.user.userData && props.user.userData.history &&
                        props.user.userData.history.map(item => (
                            <tr key={item.id}>
                                <td>{item.id}</td>
                                <td>{item.price}</td>
                                <td>{item.quantity}</td>
                                <td>{item.dateOfPurchase}</td>
                            </tr>
                        ))}
                </tbody>
            </table>
        </div>
    )
}
export default HistoryPage