react-shop-web/front & back

검색 기능 만들기

tjdvyzl 2023. 2. 4. 02:33

#1

  • SearchFeature Component 만들기
  • Search 기능을 위한 UI 만들기
  • onChange Function 만들기
  • search Data를 부모 컴포넌트에 업데이트 하기 

 

LandingPage/Sections/SearchFeature.js 파일을 만든 후에 LandingPage.js에 임포트 해오자.

 

 

https://ant.design/components/input#inputsearch

 

Input - Ant Design

When Input is used in a Form.Item context, if the Form.Item has the id and options props defined then value, defaultValue, and id props of Input are automatically set. Supports all props of Input. When Input dynamic add or remove prefix/suffix/showCount wi

ant.design

 

 

더보기
import axios from 'axios';
import React, { useEffect, useState } from 'react'
import {Icon, Col, Row, Card, Carousel} from 'antd'
import Meta from 'antd/lib/card/Meta';
import ImageSlider from '../../utils/ImageSlider'
import CheckBox from '../LandingPage/Sections/CheckBox';
import RadioBox from './Sections/RadioBox';
import SearchFeature from './Sections/SearchFeature';
import { continents, price } from '../LandingPage/Sections/Datas';


function LandingPage() {

    const [Products, setProducts] = useState([])
    const [Skip, setSkip] = useState(0)
    const [Limit, SetLimit] = useState(8)
    const [PostSize, setPostSize] = useState(0)
    const [Filters, setFilters] = useState({
        continents:[],
        price:[]
    })
    const [SearchTerm, setSearchTerm] = useState("")

    useEffect(() => {
        
        let body = {
            skip:Skip,
            limit:Limit
        }

        getProducts(body)

    },[])

    const loadMoreHandler = () => {

        let new_skip = Skip + Limit

        let body = {
            skip:new_skip,
            limit: Limit,
            // 더보기 버튼을 눌렀을 때 가는 request라는 정보를 넣어줌
            loadMore:true
        }

        getProducts(body)
        setSkip(new_skip)

    }

    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('상품들을 갖고오는데 실패')
                            }
            })
    }

    const renderCards = Products.map((product, index) => {
        return (
            <Col lg={6} md={8} xs={24} key={index}>
                <Card
                    cover={<ImageSlider images={product.images} />}>
                    <Meta
                        title={product.title} 
                        description={`${product.price}`}
                    />
                </Card>
            </Col>
        )
    })


    // 체크박스에서 체크를 눌렀을 때 필터링 된 아이템들을 보여줘야 하므로 
    // 누를때 마다 getProducts 함수를 호출한다.
    const showFilteredResults = (filters) => {
        
        let body = {
            // 누를때 마다 새로 필터링을 하여 DB에서 갖고오기 때문에 skip 또한 0으로 해줘야한다.
            skip: 0,
            limit: Limit,
            filters: filters
        }

        getProducts(body)
        setSkip(0)

    }

    const handlePrice = (value) => {
        const data = price;
        let array = [];

        // 현재 data는 datas.js에서 갖고온 price 데이터들이 들어감 
        // 여기서 key는 0, 1, 2 ... 이런식으로 들어감 
        for (let key in data) {
            // 매개변수로 받은 value는 price 필터의 filters 이다.
            // 그리고 price 필터의 filters 데이터는 각 price 컴포넌트들의 id값들 즉, 0, 1, 2 ... 이다.
            // parseInt로 감싸준 이유는 혹시라도 string이 들어오면 숫자로 바꿔주기 위해서이다.
            if (data[key]._id === parseInt(value, 10)) {
                array = data[key].array;
            }
        }

        return array;
    }

    const handleFilters = (filters, category) => {
        
        const newFilters = { ...Filters }
        
        newFilters[category] = filters

        if (category === 'price') {
            let priceValues = handlePrice(filters)
            newFilters[category] = priceValues
        }

        showFilteredResults(newFilters)
        setFilters(newFilters)
    }

    const updateSearchTerm = (newSearchTerm) => {
        setSearchTerm(newSearchTerm)
    }

    return (
        <div style={{ width: '75%', margin: '3rem auto' }}>
            <div style={{textAlign: 'center'}}>
                <h2>Let's Travel Anywhere <Icon type="rocket"/></h2>
            </div>

            {/* Filter */}
            <Row gutter={[16,16]}>

                <Col lg={12} xs={24}>
                    <CheckBox list={continents} handleFilters={filters => handleFilters(filters, 'continents')} />
                </Col>

                <Col lg={12} xs={24}>
                    <RadioBox list={price} handleFilters={filters => handleFilters(filters, 'price')}/>
                </Col>
            </Row>
            
            {/* Search */}
            <div style={{display:'flex', justifyContent:'flex-end', margin: '1rem auto'}}>
                <SearchFeature
                    refreshFunction={updateSearchTerm}
                />
            </div>
        
            <Row gutter={[16, 16]}>
                {renderCards}
            </Row>
            {PostSize >= Limit &&
                <div style={{ display: 'flex', justifyContent: 'center' }}>
                  <button onClick={loadMoreHandler}>더보기</button>
                </div>
            }
            
        </div>
    )
}

export default LandingPage

LandingPage.js

 

 

더보기
import React, { useState } from 'react'
import { Input } from 'antd';
const { Search } = Input;

function SearchFeature(props) {

    const [SearchTerm, setSearchTerm] = useState("")

    const searchHandler = (e) => {
        setSearchTerm(e.currentTarget.value)
        props.refreshFunction(e.currentTarget.value)
    }

  return (
      <div>
          <Search
            placeholder="input search text"
            onChange={searchHandler}
            style={{ width: 200 }}
            value={SearchTerm}
          />
      </div>
  )
}

export default SearchFeature

LandingPage/Sections/SearchFeature.js

 

지금까지 코드들을 보면 반복적인 구조가 계속 나온다. 

자식 컴포넌트에 props로 refresh 함수를 넣어주고, 

자식 컴포넌트에서는 부모 컴포넌트로 부터 받은 refresh함수를 호출하여 매개변수로 onChange된 SearchTerm 값을 보내주고,

그 후 부모 컴포넌트에선 자식 컴포넌트로 부터 올려받은 SearchTerm 값을 State 값으로 저장하는 구조다.

 


#2

검색 값을 이용한 getProduct Function을 작동시키기


LandingPage.js

 


Search 기능을 위해서 getProduct Route 수정하기


더보기
const express = require('express');
const router = express.Router();
const multer = require('multer')
const {Product} = require('../models/Product')

//=================================
//             Product
//=================================

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        // 밑에처럼 설정해주면 모든 파일이 uploads 폴더에 저장된다.
    cb(null, 'uploads/')
  },
  filename: function (req, file, cb) {
    cb(null, `${Date.now()}_${file.originalname}`) 
  }
})

const upload = multer({ storage: storage }).single("file")

router.post('/image', (req, res) => {

    // 가져온 이미지를 저장 해주는 부분 
    upload(req, res, err => {
        if(err){
            return res.json({success: false, err})
        }
        // 백엔드에서 파일 저장과 파일 저장 정보를 전달해주는데
        // 파일을 어디다가 저장했는지, 파일 이름은 무엇으로 정했는지 이 두가지를 client에 전달한다. 
        return res.json({success:true, filePath:res.req.file.path, fileName:res.req.filename})
    })
})


// UploadProductPage.js에서 api endPoint를 /api/product 로 요청을 보냈었다.
// index.js에서 product라는 라우트를 따로 만들어줬기 때문에 여기에서 end point는 '/'로 주면 된다.
router.post('/', (req, res) => {
    
    // 받아온 정보들을 DB에 넣어준다.

    // 넣어주기 전에 Product 모델을 갖고와야 한다.
    const product = new Product(req.body)

    product.save((err) => {
        if (err) return res.status(400).json({ success: false })
        return res.status(200).json({success:true})
    })
    
})

router.post('/products', (req, res) => {
    
    // product collection에 들어있는 모든 상품 정보를 갖고오기 

    let limit = req.body.limit ? parseInt(req.body.limit) : 20;
    let skip = req.body.skip ? parseInt(req.body.skip) : 0;
    // 검색 창에 입력된 값이 들어옴
    let term = req.body.searchTerm

    let findArgs = {};

    for (let key in req.body.filters) {
        if (req.body.filters[key].length > 0) {
            if (key === "price") {
                findArgs[key] = {
                    $gte: req.body.filters[key][0],
                    $lte: req.body.filters[key][1]
                }
            } else {
                findArgs[key] = req.body.filters[key];
            }
        }
    }

    /*
        지금까지 보면 showFilteredResults 함수나 loadMoreHandler 함수 등등 여러 군데에서 getProducts함수를 호출하여
        이 라우트를 타고 온다. 
        이럴 때 마다 밑의 코드에서 새로운 부분인 .find 부분이 없이 가능했지만 
        검색을 통해 이 라우트를 타고 온 경우엔 추가해줘야 하므로 if문으로 나눠주자.
    */

    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 })
            })
    } else {
        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 })
            })
    }
})

module.exports = router;

product.js

 

 

위 $text 부분의 설명은 밑에 링크에 나와있다.

 

https://www.mongodb.com/docs/manual/reference/operator/query/text/

 

$text — MongoDB Manual

Docs Home → MongoDB Manual This page describes text search capabilities for self-managed (non-Atlas) deployments. For data hosted on MongoDB Atlas, MongoDB offers an improved full-text search solution, Atlas Search.This page describes $text operator for

www.mongodb.com

 


Search 기능을 가능하게 하기 위해서 Product Model에 코드 추가해주기


더보기
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})



productSchema.index({ 
    title:'text',
    description: 'text',
}, {
    weights: {
        title: 5,
        description: 1,
    }
})


const Product = mongoose.model('Product', productSchema);

module.exports = { Product }

server/models/Product.js

 

index 코드 부분을 추가해줬다.

설명을 간략하게 하자면, 검색을 하면 검색한 값이 어떤 부분에 걸려서 검색이 되도록 설정을 해줘야한다.

product 모델을 검색할 때 검색한 값이 title과 description 중에 검색하고 싶다면 그때 위 코드를 넣어주면 된다. 

그리고 weights 부분은 가중치를 부여하는 것인데, title: 5, description: 1은 description에 1만큼 가중치를 부여하고

title에는 description보다 5배 만큼 더 가중치를 부여함으로써 검색할 때 우선순위를 설정해줄 수 있다.

자세한 내용은 밑에 링크에 나와있다.

 

 

https://www.mongodb.com/docs/manual/tutorial/control-results-of-text-search/

 

Control Search Results with Weights — MongoDB Manual

Docs Home → MongoDB Manual Text search assigns a score to each document that contains the search term in the indexed fields. The score determines the relevance of a document to a given search query.For a text index, the weight of an indexed field denotes

www.mongodb.com

 

 

검색기능을 하는데 아무리해도 검색이 안돼서..  혹시 몰라서 product model 구현할 때 이름 변수를 name으로 지었는데 title로 바꿔서 처음부터 다시 해보니까 된다.. 강사님한테 물어보고 답변 달아주시면 그때 글 수정하자.