-
호텔 검색 앱 3 - 검색어 자동완성 기능 구현하기프로젝트/호텔 검색 앱 2021. 11. 28. 23:49728x90
* 자동완성을 실험하기 위한 가짜 데이터 저장하기
const queryData = { "query": "서울", "moresuggestions": 10, "suggestions": [ { "group": "CITY_GROUP", "entities": [ { "geoId": "3124", "destinationId": "759818", "landmarkCityDestinationId": null, "type": "CITY", "redirectPage": "DEFAULT_PAGE", "latitude": 37.565994, "longitude": 126.982577, "searchDetail": null, "caption": "<span class='highlighted'>서울</span>, 한국", "name": "서울" }, { "geoId": "6049712", "destinationId": "1665648", "landmarkCityDestinationId": null, "type": "NEIGHBORHOOD", "redirectPage": "DEFAULT_PAGE", "latitude": 37.49746981209052, "longitude": 127.06235814336002, "searchDetail": null, "caption": "강남, <span class='highlighted'>서울</span>, 한국", "name": "강남" }, { "geoId": "6156735", "destinationId": "1710405", "landmarkCityDestinationId": null, "type": "NEIGHBORHOOD", "redirectPage": "DEFAULT_PAGE", "latitude": 37.56372566629919, "longitude": 126.98414822919044, "searchDetail": null, "caption": "명동, <span class='highlighted'>서울</span>, 한국", "name": "명동" }, { "geoId": "179268", "destinationId": "1712713", "landmarkCityDestinationId": null, "type": "NEIGHBORHOOD", "redirectPage": "DEFAULT_PAGE", "latitude": 37.594880968864516, "longitude": 126.97734239320602, "searchDetail": null, "caption": "종로, <span class='highlighted'>서울</span>, 한국", "name": "종로" }, { "geoId": "6347337", "destinationId": "1812427", "landmarkCityDestinationId": null, "type": "NEIGHBORHOOD", "redirectPage": "DEFAULT_PAGE", "latitude": 37.505899761350655, "longitude": 127.08251899429683, "searchDetail": null, "caption": "잠실동, <span class='highlighted'>서울</span>, 한국", "name": "잠실동" }, { "geoId": "6222649", "destinationId": "1747961", "landmarkCityDestinationId": null, "type": "REGION", "redirectPage": "DEFAULT_PAGE", "latitude": 37.52507275820544, "longitude": 126.92598091344593, "searchDetail": null, "caption": "여의도, <span class='highlighted'>서울</span>, 한국", "name": "여의도" }, { "geoId": "6049713", "destinationId": "1665649", "landmarkCityDestinationId": null, "type": "NEIGHBORHOOD", "redirectPage": "DEFAULT_PAGE", "latitude": 37.543725187824165, "longitude": 126.99024687030192, "searchDetail": null, "caption": "이태원, <span class='highlighted'>서울</span>, 한국", "name": "이태원" }, { "geoId": "179262", "destinationId": "1639122", "landmarkCityDestinationId": null, "type": "NEIGHBORHOOD", "redirectPage": "DEFAULT_PAGE", "latitude": 37.53143365798815, "longitude": 126.98018807905429, "searchDetail": null, "caption": "용산구, <span class='highlighted'>서울</span>, 한국", "name": "용산구" } ] }, { "group": "HOTEL_GROUP", "entities": [ { "geoId": "15031", "destinationId": "141401", "landmarkCityDestinationId": null, "type": "HOTEL", "redirectPage": "DEFAULT_PAGE", "latitude": 37.55629, "longitude": 127.00449, "searchDetail": null, "caption": "<span class='highlighted'>서울</span> 신라호텔, 서울, 한국", "name": "서울 신라호텔" }, { "geoId": "21727", "destinationId": "106097", "landmarkCityDestinationId": null, "type": "HOTEL", "redirectPage": "DEFAULT_PAGE", "latitude": 37.54051, "longitude": 126.99683, "searchDetail": null, "caption": "그랜드 하얏트 <span class='highlighted'>서울</span>, 서울, 한국", "name": "그랜드 하얏트 서울" }, { "geoId": "24908", "destinationId": "105343", "landmarkCityDestinationId": null, "type": "HOTEL", "redirectPage": "DEFAULT_PAGE", "latitude": 37.56395, "longitude": 126.97953, "searchDetail": null, "caption": "웨스틴 조선 <span class='highlighted'>서울</span>, 서울, 한국", "name": "웨스틴 조선 서울" } ] }, { "group": "LANDMARK_GROUP", "entities": [ { "geoId": "6080001", "destinationId": "1655737", "landmarkCityDestinationId": "759818", "type": "LANDMARK", "redirectPage": "DEFAULT_PAGE", "latitude": 37.45998329568354, "longitude": 126.95299789150509, "searchDetail": null, "caption": "<span class='highlighted'>서울</span>대학교, 서울, 한국", "name": "서울대학교" }, { "geoId": "6159377", "destinationId": "1713589", "landmarkCityDestinationId": "759818", "type": "LANDMARK", "redirectPage": "DEFAULT_PAGE", "latitude": 37.575803, "longitude": 126.976832, "searchDetail": null, "caption": "광화문, <span class='highlighted'>서울</span>, 한국", "name": "광화문" }, { "geoId": "6228035", "destinationId": "1750805", "landmarkCityDestinationId": "759818", "type": "LANDMARK", "redirectPage": "DEFAULT_PAGE", "latitude": 37.566215, "longitude": 126.978505, "searchDetail": null, "caption": "<span class='highlighted'>서울</span>특별시청, 서울, 한국", "name": "서울특별시청" } ] }, { "group": "TRANSPORT_GROUP", "entities": [ { "geoId": "6164139", "destinationId": "1718570", "landmarkCityDestinationId": null, "type": "METRO_STATION", "redirectPage": "DEFAULT_PAGE", "latitude": 37.555813, "longitude": 126.972025, "searchDetail": null, "caption": "<span class='highlighted'>서울</span>역, 서울, 한국", "name": "서울역" } ] } ] } export default queryData
src 폴더에 위와 같이 queryData.js 파일을 생성하고 가짜 데이터를 저장한다. 해당 데이터는 hotels.com 에서 제공하는 실제 데이터를 다운로드 한 것이다.
* 서버에서 데이터를 가져오는 헬퍼 함수 구현하기
src 폴더에 lib 폴더를 생성하고 아래와 같은 파일들을 추가한다.
const fetchHotelsCom = async (url) => { try{ return await fetch(url, { "method": "GET", "headers": { "x-rapidapi-host": "hotels-com-provider.p.rapidapi.com", "x-rapidapi-key": "26fb175c66msh4ef16cba57a9a16p1df633jsnb7dfe2a4b6c7" } }).then( res => res.json()) }catch(e){ return e } } export default fetchHotelsCom
fetchHotelsCom.js 파일을 생성하고 위와 같이 작성하자! 위 코드는 hotels.com 서버에서 데이터를 가져올때 자주 사용하기 때문에 헬퍼 함수로 미리 정의해둔 다음에 사용한다.
const isArrayNull = (array) => { return array.length === 0 } export { isArrayNull }
helpers.js 파일을 생성하고 위와 같이 작성하자! 위 코드는 주어진 배열의 길이가 0 인지 여부를 검사하는 헬퍼 함수이다.
export { default as fetchHotelsCom } from './fetchHotelsCom' export { isArrayNull } from './helpers'
lib > index.js 파일을 생성하고 위와 같이 모듈을 내보낸다.
* 자동완성 메뉴를 위한 Caption, Button 컴포넌트 만들기
components 폴더 하위에 아래 파일들을 추가한다.
import React from 'react' import './Caption.css' const Caption = ({ id, destinationId, caption, setCaption, highlight }) => { return ( <div className={`Caption-container ${highlight === id ? 'highlight' : ''}`} id={id} data-destinationid={destinationId} onClick={setCaption} dangerouslySetInnerHTML={{ __html: caption }}></div> ) } export default Caption Caption.defaultProps = { highlight: 0 }
Caption.js 파일을 생성하고 위와 같이 작성하자!
.Caption-container{ border: 1px solid tan; box-sizing: border-box; padding: 10px; cursor: pointer; color: #a9a9a9; font-size: 1.2rem; font-weight: bold; font-family: Tahoma; } .Caption-container:hover{ background: linear-gradient(rgba(255,255,255,0.2), rgba(255,255,255,0.2)); } .highlight{ background: linear-gradient(rgba(255,255,255,0.2), rgba(255,255,255,0.2)); }
Caption.css 파일을 생성하고 위와 같이 작성하자!
import React from 'react' import './Button.css' const Button = ({ children, size, color, width, handleClick, disabled }) => { return <button className={`Button ${size} ${color} ${width}`} onClick={handleClick} disabled={disabled} >{children}</button> } export default Button; Button.defaultProps = { size: 'medium', color: 'tomato', disabled: false }
Button.js 파일을 생성하고 위와 같이 작성하자!
.Button { all: unset; color: white; cursor: pointer; border-radius: 5px; font-weight: bold; margin-left: 10px; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; } .Button:hover{ opacity: 0.7; } /* 버튼의 크기 설정 */ .long{ height: 60px; padding-left: 15px; padding-right: 15px; font-size: 1.2rem; } .medium{ height: 50px; padding-left: 10px; padding-right: 10px; font-size: 1rem; } .short{ height: 40px; padding-left: 5px; padding-right: 5px; font-size: 0.8rem; } /* 버튼의 배경색 설정 */ .blue{ background:blue; } .blue:hover{ background: skyblue; } .tomato{ background: tomato; } .tomato:hover{ background: lightsalmon; } .grey{ background: grey; } .grey:hover{ background: lightgray; } /* 전체 너비를 차지하는 버튼 */ .fullWidth{ width: 100%; margin-left: 0px; margin-top: 10px; margin-bottom: 10px; }
Button.css 파일을 생성하고 위와 같이 작성하자!
export { default as Input } from './Input' export { default as Button } from './Button' export { default as Caption } from './Caption'
components > index.js 파일을 위와 같이 수정하자!
* 자동완성 기능 구현하기
import React, { useState } from 'react' import { Input, Button, Caption } from 'components' import { fetchHotelsCom, isArrayNull } from 'lib' import { useNavigate } from 'react-router-dom' import queryData from '../queryData' import './Search.css' const Search = () => { const [destination, setDestination] = useState('') const [checkIn, setCheckIn] = useState('') const [checkOut, setCheckOut] = useState('') const [adultsNumber, setAdultsNumber] = useState(1) const [captions, setCaptions] = useState([]) // 자동완성 메뉴 const [open, setOpen] = useState('hide') // 자동완성 메뉴 온오프 const [index, setIndex] = useState(0) // 자동완성 메뉴 하이라이트 변경 const [destinationId, setDestinationId] = useState(0) const navigate = useNavigate() const handleChange = (e) => { const { name, value } = e.target console.log(name, value) switch(name){ case 'destination': value ? setOpen('show') : setOpen('hide') executeAutoCaption(value) setDestination(value) break; case 'check-in': setCheckIn(value) break; case 'check-out': setCheckOut(value) break; case 'adults-number': setAdultsNumber(value) break; } } // 자동완성 기능 구현 const executeAutoCaption = async (query) => { // const data = await getCaptions(query) // const { suggestions } = data const { suggestions } = queryData const captionsItems = [] if(!isArrayNull(suggestions)){ suggestions.map(suggestion => { const { entities } = suggestion captionsItems.push(...entities) }) } // console.log('captions: ',captionsItems) setCaptions(captionsItems) setHighlight() // 사용자 검색에 따라 하이라이트 변경 } const getCaptions = async (query) => { console.log('get captions ...') // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/destinations/search?query=${query}¤cy=KRW&locale=ko_KR`) // return data } const setCaption = (e) => { const target = e.target.closest('.Caption-container') console.log(target) // console.log(target.dataset.destinationid) setDestination(target.innerText) setDestinationId(target.dataset.destinationid) setOpen('hide') } const setHighlight = () => { captions.map( (captionItem, id) => { captionItem.caption.includes(destination) ? setIndex(id) : null }) } const changeCaptionHightlight = (e) => { // console.log(e.keyCode) const captionsLength = captions.length if(e.keyCode === 40){ index < captionsLength - 1 ? setIndex(index+1) : setIndex(0) }else if(e.keyCode === 38){ index > 0 ? setIndex(index-1) : setIndex(captionsLength - 1) }else if(e.keyCode === 13){ const target = document.getElementById(index) console.log(target) // console.log(target.dataset.destinationid) setDestination(target.innerText) setDestinationId(target.dataset.destinationid) setOpen('hide') } } const searchHotels = () => { console.log('search hotels ...') console.log(destinationId, checkIn, checkOut, adultsNumber) navigate('/hotels', { state : { destinationId, checkIn, checkOut, adultsNumber } }) } const Captions = ({captions}) => { let captionUI = null; // console.log('captionUI: ',captions) if(!isArrayNull(captions)){ captionUI = captions.map( (captionItem, id) => { return <Caption key={captionItem.destinationId} id={id} destinationId={captionItem.destinationId} caption={captionItem.caption} setCaption={setCaption} highlight={index}></Caption> }) } return <>{captionUI}</> } return ( <div className='Search-container'> <div className='Search-inputs'> <div className='destination-container'> <Input name='destination' type='text' placeholder='목적지를 입력하세요 ...' width='large' value={destination} onChange={handleChange} onKeyUp={changeCaptionHightlight}/> <div className={`captions ${open}`}>{<Captions captions={captions}/>}</div> </div> <Input name='check-in' type='date' placeholder='체크인' width='small' value={checkIn} onChange={handleChange}/> <Input name='check-out' type='date' placeholder='체크아웃' width='small' value={checkOut} onChange={handleChange}/> <Input name='adults-number' type='number' placeholder='인원수' width='middle' min={1} max={7} value={adultsNumber} onChange={handleChange}/> <Button handleClick={searchHotels} color='blue' size='long'>검색</Button> </div> </div> ) } export default Search
Search.js 파일을 위와 같이 수정하자!
.Search-container{ width: 100%; height: 100vh; text-align: center; background-image: url('../assets/images/search.jpg'); background-size: cover; } .Search-inputs{ padding-top: 100px; display: flex; justify-content: center; } .captions{ width: 500px; margin: 0 auto; } .show{ display: block; } .hide{ display: none; }
Search.css 파일을 위와 같이 수정하자!
728x90'프로젝트 > 호텔 검색 앱' 카테고리의 다른 글
호텔 검색 앱 5 - 호텔 상세 페이지 구현하기 (0) 2021.12.19 호텔 검색 앱 4 - 호텔 목록 페이지 구현하기 (0) 2021.12.05 호텔 검색 앱 - 지도 추가하기 (0) 2021.11.29 호텔 검색 앱 2 - 호텔 검색 페이지 구현하기 (0) 2021.11.28 호텔 검색 앱 1 - 프로젝트 셋팅 및 기본 라우터 설정하기 (0) 2021.11.28