728x90
* 자동완성을 실험하기 위한 가짜 데이터 저장하기
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 |