검색 기능 만들기
#1
- SearchFeature Component 만들기
- Search 기능을 위한 UI 만들기
- onChange Function 만들기
- search Data를 부모 컴포넌트에 업데이트 하기
LandingPage/Sections/SearchFeature.js 파일을 만든 후에 LandingPage.js에 임포트 해오자.
https://ant.design/components/input#inputsearch
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을 작동시키기
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/
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/