ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 호텔 검색 앱 4 - 호텔 목록 페이지 구현하기
    프로젝트/호텔 검색 앱 2021. 12. 5. 22:16
    728x90

     

    * 호텔 목록을 보여주기 위한 가짜 데이터 저장하기

    const hotelsData = {
        "header": "강남, 서울, 한국",
        "query": {
            "destination": {
                "id": "1665648",
                "value": "강남, 서울, 한국",
                "resolvedLocation": "NEIGHBORHOOD:1665648:UNKNOWN:UNKNOWN"
            }
        },
        "searchResults": {
            "totalCount": 2972,
            "results": [
                {
                    "id": 106707,
                    "name": "조선 팰리스 럭셔리 컬렉션 호텔, 서울 강남 (Josun Palace, a Luxury Collection Hotel, Seoul Gangnam)",
                    "starRating": 5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 테헤란로 231",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06142",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.5,
                        "rating": "4.5",
                        "total": 81,
                        "scale": 5,
                        "badge": "superb",
                        "badgeText": "매우 훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "0.7km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.0km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩495,000",
                            "exactCurrent": 495000,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩990,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": false,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "역삼",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "HRW",
                    "coordinate": {
                        "lat": 37.502642,
                        "lon": 127.041787
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 4871,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/1000000/10000/4900/4871/69fcb36b_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 116487,
                    "name": "그랜드 인터컨티넨탈 서울 파르나스 (Grand InterContinental Seoul Parnas, an IHG Hotel)",
                    "starRating": 5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 테헤란로 521",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-732",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.7,
                        "rating": "4.7",
                        "total": 685,
                        "scale": 5,
                        "badge": "exceptional",
                        "badgeText": "최고 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.2km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.2km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩255,000",
                            "exactCurrent": 255000,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩510,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "코엑스",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|TESCO",
                    "coordinate": {
                        "lat": 37.50846,
                        "lon": 127.061036
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 22529,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/1000000/30000/22600/22529/4a3dd0ec_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 182415,
                    "name": "인터컨티넨탈 서울 코엑스 (InterContinental Seoul COEX, an IHG Hotel)",
                    "starRating": 5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 봉은사로 524",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-975",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.6,
                        "rating": "4.6",
                        "total": 933,
                        "scale": 5,
                        "badge": "superb",
                        "badgeText": "매우 훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.2km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.8km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩160,000",
                            "exactCurrent": 160000,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩320,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "삼성동",
                    "deals": {},
                    "messaging": {
                        "scarcity": "저희 앱에서 1개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|TESCO",
                    "coordinate": {
                        "lat": 37.513481,
                        "lon": 127.057276
                    },
                    "roomsLeft": 1,
                    "providerType": "LOCAL",
                    "supplierHotelId": 424942,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/1000000/430000/425000/424942/29e80402_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 229056,
                    "name": "파크 하얏트 서울 (Park Hyatt Seoul)",
                    "starRating": 5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 테헤란로 606",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-502",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.5,
                        "rating": "4.5",
                        "total": 269,
                        "scale": 5,
                        "badge": "superb",
                        "badgeText": "매우 훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.4km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.0km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩466,500",
                            "exactCurrent": 466500,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩933,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "코엑스",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|TESCO",
                    "coordinate": {
                        "lat": 37.50845,
                        "lon": 127.06335
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 1200015,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/2000000/1210000/1200100/1200015/73dac5ab_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 1238009376,
                    "name": "안다즈 서울 강남 (Andaz Seoul Gangnam)",
                    "starRating": 5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 신사동 논현로 854",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06022",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.5,
                        "rating": "4.5",
                        "total": 276,
                        "scale": 5,
                        "badge": "superb",
                        "badgeText": "매우 훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "2.9km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "3.6km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩356,000",
                            "exactCurrent": 356000,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩712,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "강남",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|HRW|TESCO",
                    "coordinate": {
                        "lat": 37.525265,
                        "lon": 127.028627
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 38656543,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/39000000/38660000/38656600/38656543/2c89da6c_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 626710,
                    "name": "글래드 라이브 강남 (Glad Live Gangnam)",
                    "starRating": 4,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 봉은사로 223",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06109",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.3,
                        "rating": "4.3",
                        "total": 376,
                        "scale": 5,
                        "badge": "fabulous",
                        "badgeText": "훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.2km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.3km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩155,000",
                            "exactCurrent": 155000,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩310,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "논현동",
                    "deals": {},
                    "messaging": {
                        "scarcity": "저희 앱에서 2개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|HRW|TESCO",
                    "coordinate": {
                        "lat": 37.508184,
                        "lon": 127.036514
                    },
                    "roomsLeft": 2,
                    "providerType": "LOCAL",
                    "supplierHotelId": 16164397,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/17000000/16170000/16164400/16164397/d93a1f06_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 548967,
                    "name": "호텔 카푸치노 (Hotel Cappuccino)",
                    "starRating": 4,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 봉은사로 155",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06122",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.1,
                        "rating": "4.1",
                        "total": 483,
                        "scale": 5,
                        "badge": "very-good",
                        "badgeText": "매우 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.5km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.8km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩123,341",
                            "exactCurrent": 123341,
                            "old": "₩190,341",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩246,682</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "논현동",
                    "deals": {
                        "secretPrice": {
                            "dealText": "비밀 가격으로 추가 할인받기"
                        },
                        "priceReasoning": "DRR-445"
                    },
                    "messaging": {
                        "scarcity": "저희 앱에서 3개 남았어요!"
                    },
                    "badging": {
                        "hotelBadge": {
                            "type": "vipBasic",
                            "label": "VIP"
                        }
                    },
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.506641,
                        "lon": 127.031575
                    },
                    "roomsLeft": 3,
                    "providerType": "LOCAL",
                    "supplierHotelId": 12385513,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/13000000/12390000/12385600/12385513/4b7ae2b9_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 720758944,
                    "name": "포 포인츠 바이 쉐라톤 강남 (Four Points by Sheraton Gangnam)",
                    "starRating": 4,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 도산대로 203",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06026",
                        "region": "ICN",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.3,
                        "rating": "4.3",
                        "total": 372,
                        "scale": 5,
                        "badge": "fabulous",
                        "badgeText": "훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "2.5km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "3.3km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩130,000",
                            "exactCurrent": 130000,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩260,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "강남",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|TESCO",
                    "coordinate": {
                        "lat": 37.520077,
                        "lon": 127.028287
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 22492467,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/23000000/22500000/22492500/22492467/6b3493d7_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 668653,
                    "name": "유리앤 호텔 (Hotel Uri&)",
                    "starRating": 4,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 삼성로96길 20",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06169",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.4,
                        "rating": "4.4",
                        "total": 407,
                        "scale": 5,
                        "badge": "fabulous",
                        "badgeText": "훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "0.9km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.5km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩130,200",
                            "exactCurrent": 130200,
                            "old": "₩140,000",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩260,400</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "코엑스",
                    "deals": {
                        "secretPrice": {
                            "dealText": "비밀 가격으로 추가 할인받기"
                        },
                        "priceReasoning": "DRR-443"
                    },
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.508976,
                        "lon": 127.057428
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 17484868,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/18000000/17490000/17484900/17484868/d75eb27a_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 333203,
                    "name": "호텔 프리마 (Hotel Prima)",
                    "starRating": 4,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 도산대로 536",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-100",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 3.9,
                        "rating": "3.9",
                        "total": 234,
                        "scale": 5,
                        "badge": "good",
                        "badgeText": "좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "2.3km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.1km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩77,221",
                            "exactCurrent": 77221,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩154,442</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "청담동",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.52476,
                        "lon": 127.05227
                    },
                    "providerType": "MULTISOURCE",
                    "supplierHotelId": 3041752,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/4000000/3050000/3041800/3041752/a24691b4_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 235442,
                    "name": "베스트 웨스턴 프리미어 강남 (Best Western Premier Gangnam)",
                    "starRating": 4,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 봉은사로 139",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-010",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.3,
                        "rating": "4.3",
                        "total": 481,
                        "scale": 5,
                        "badge": "fabulous",
                        "badgeText": "훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.7km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.9km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩114,865",
                            "exactCurrent": 114865,
                            "old": "₩191,442",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩229,730</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "논현동",
                    "deals": {
                        "specialDeal": {
                            "dealText": "40% 할인"
                        },
                        "priceReasoning": "DRR-446"
                    },
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|priceRangeAU|TESCO",
                    "coordinate": {
                        "lat": 37.506127,
                        "lon": 127.029857
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 1168148,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/2000000/1170000/1168200/1168148/87da40f3_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 439998,
                    "name": "호텔 그라모스 (Hotel Grammos)",
                    "starRating": 3.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 테헤란로33길 6-5",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-915",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.2,
                        "rating": "4.2",
                        "total": 251,
                        "scale": 5,
                        "badge": "very-good",
                        "badgeText": "매우 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "0.8km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.1km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩184,250",
                            "exactCurrent": 184250,
                            "old": "₩335,000",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩368,500</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "역삼",
                    "deals": {
                        "secretPrice": {
                            "dealText": "비밀 가격으로 추가 할인받기"
                        },
                        "priceReasoning": "DRR-445"
                    },
                    "messaging": {
                        "scarcity": "저희 앱에서 4개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.502447,
                        "lon": 127.039935
                    },
                    "roomsLeft": 4,
                    "providerType": "LOCAL",
                    "supplierHotelId": 6532238,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/7000000/6540000/6532300/6532238/5570dab1_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 1069704832,
                    "name": "호텔 인 9 강남 (HOTEL in 9 Gangnam)",
                    "starRating": 3.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 영동대로 618",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06081",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.4,
                        "rating": "4.4",
                        "total": 333,
                        "scale": 5,
                        "badge": "fabulous",
                        "badgeText": "훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.5km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.8km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩147,491",
                            "exactCurrent": 147491,
                            "old": "₩189,091",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩294,982</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "삼성동",
                    "deals": {
                        "secretPrice": {
                            "dealText": "비밀 가격으로 추가 할인받기"
                        },
                        "priceReasoning": "DRR-445"
                    },
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.515396,
                        "lon": 127.059892
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 33397026,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/34000000/33400000/33397100/33397026/24573d1c_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 1474882912,
                    "name": "신라스테이 삼성 (Shilla Stay Samsung)",
                    "starRating": 3.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "영동대로 506",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06173",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.2,
                        "rating": "4.2",
                        "total": 370,
                        "scale": 5,
                        "badge": "very-good",
                        "badgeText": "매우 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.4km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.1km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩273,081",
                            "exactCurrent": 273080.5,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩546,161</strong>)"
                        },
                        "features": {
                            "freeCancellation": false,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "코엑스",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13",
                    "coordinate": {
                        "lat": 37.509578,
                        "lon": 127.063196
                    },
                    "providerType": "MULTISOURCE",
                    "supplierHotelId": 46058841,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/47000000/46060000/46058900/46058841/93be1b8a_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 692847744,
                    "name": "글래드 강남 코엑스센터 (GLAD Gangnam COEX Center)",
                    "starRating": 3.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "테헤란로 610",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06174",
                        "region": "강남",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.4,
                        "rating": "4.4",
                        "total": 949,
                        "scale": 5,
                        "badge": "fabulous",
                        "badgeText": "훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.4km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.1km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩180,000",
                            "exactCurrent": 180000,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩360,000</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "코엑스",
                    "deals": {},
                    "messaging": {
                        "scarcity": "저희 앱에서 1개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.509131,
                        "lon": 127.064137
                    },
                    "roomsLeft": 1,
                    "providerType": "LOCAL",
                    "supplierHotelId": 21620242,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/22000000/21630000/21620300/21620242/955ed55d_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 1288351008,
                    "name": "케이팝 코엑스 강남 스테이 (KPOP Coex Gangnam Stay)",
                    "starRating": 3,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 테헤란로88길 10",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06179",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.7,
                        "rating": "4.7",
                        "total": 24,
                        "scale": 5,
                        "badge": "exceptional",
                        "badgeText": "최고 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.0km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "0.3km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩80,909",
                            "exactCurrent": 80909,
                            "info": "유닛당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩161,818</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "코엑스",
                    "deals": {},
                    "messaging": {
                        "scarcity": "저희 앱에서 1개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13",
                    "coordinate": {
                        "lat": 37.50693,
                        "lon": 127.0598
                    },
                    "roomsLeft": 1,
                    "providerType": "LOCAL",
                    "supplierHotelId": 40229719,
                    "vrBadge": "아파트",
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/41000000/40230000/40229800/40229719/25fc57c3_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 550524,
                    "name": "호텔 더 디자이너스 LYJ 강남 프리미어 (Hotel The Designers LYJ Gangnam Premier)",
                    "starRating": 3,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 봉은사로 113",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06120",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 3.5,
                        "rating": "3.5",
                        "total": 177,
                        "scale": 5,
                        "badge": "good",
                        "badgeText": "좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "2.0km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "3.3km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩122,727",
                            "exactCurrent": 122727,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩245,454</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "논현동",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.50495,
                        "lon": 127.02591
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 12465289,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/13000000/12470000/12465300/12465289/294d5777_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 783213792,
                    "name": "라비타 호텔 (Lavita Hotel)",
                    "starRating": 3,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 영동대로 712",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06075",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 3.9,
                        "rating": "3.9",
                        "total": 71,
                        "scale": 5,
                        "badge": "good",
                        "badgeText": "좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "2.0km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "1.6km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩82,802",
                            "exactCurrent": 82801.5,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩165,603</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "청담동",
                    "deals": {},
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.52139,
                        "lon": 127.05688
                    },
                    "providerType": "MULTISOURCE",
                    "supplierHotelId": 24444181,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/25000000/24450000/24444200/24444181/b642e791_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 572035,
                    "name": "블랑 호텔 강남 (Blanc Hotel Gangnam)",
                    "starRating": 3,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 언주로94길 13",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 3.1,
                        "rating": "3.1",
                        "total": 27,
                        "scale": 5,
                        "badge": "good",
                        "badgeText": "좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "0.5km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "1.8km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩73,554",
                            "exactCurrent": 73554,
                            "old": "₩81,727",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩147,108</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "역삼",
                    "deals": {
                        "secretPrice": {
                            "dealText": "비밀 가격으로 추가 할인받기"
                        },
                        "priceReasoning": "DRR-443"
                    },
                    "messaging": {
                        "scarcity": "저희 앱에서 2개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.504308,
                        "lon": 127.043244
                    },
                    "roomsLeft": 2,
                    "providerType": "LOCAL",
                    "supplierHotelId": 13456652,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/14000000/13460000/13456700/13456652/2126d241_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 237409,
                    "name": "강남 패밀리 호텔 (Gangnam Family Hotel)",
                    "starRating": 3,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 봉은사로 143",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-010",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4,
                        "rating": "4.0",
                        "total": 149,
                        "scale": 5,
                        "badge": "very-good",
                        "badgeText": "매우 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.6km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.9km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩85,909",
                            "exactCurrent": 85909,
                            "old": "₩127,273",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩171,818</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "논현동",
                    "deals": {
                        "secretPrice": {
                            "dealText": "비밀 가격으로 추가 할인받기"
                        },
                        "priceReasoning": "DRR-445"
                    },
                    "messaging": {},
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.506345,
                        "lon": 127.030365
                    },
                    "providerType": "LOCAL",
                    "supplierHotelId": 1243595,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/2000000/1250000/1243600/1243595/bcef1f35_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 1275349600,
                    "name": "어반엑시트 (UrbanExit)",
                    "starRating": 2.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 역삼동 639-15 4층",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135-080",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.5km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "1.7km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩636,364",
                            "exactCurrent": 636364,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩1,272,728</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "삼성동",
                    "deals": {},
                    "messaging": {
                        "scarcity": "저희 앱에서 1개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13",
                    "coordinate": {
                        "lat": 37.517684,
                        "lon": 127.047561
                    },
                    "roomsLeft": 1,
                    "providerType": "LOCAL",
                    "supplierHotelId": 39823425,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/40000000/39830000/39823500/39823425/f3b740ea_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 505687,
                    "name": "프린세스 호텔 (Princess Hotel)",
                    "starRating": 2.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 압구정로46길 17",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "135896",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4,
                        "rating": "4.0",
                        "total": 82,
                        "scale": 5,
                        "badge": "very-good",
                        "badgeText": "매우 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "2.8km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "3.2km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩63,868",
                            "exactCurrent": 63868,
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩127,736</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "강남",
                    "deals": {},
                    "messaging": {
                        "scarcity": "저희 앱에서 2개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.527413,
                        "lon": 127.036476
                    },
                    "roomsLeft": 2,
                    "providerType": "LOCAL",
                    "supplierHotelId": 10281147,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/11000000/10290000/10281200/10281147/1be56555_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 407941,
                    "name": "베니키아 노블레스 호텔 (Benikea Hotel Noblesse)",
                    "starRating": 2.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 역삼동 642-2",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "",
                        "region": "서울특별시",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 3.9,
                        "rating": "3.9",
                        "total": 33,
                        "scale": 5,
                        "badge": "good",
                        "badgeText": "좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.2km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.5km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩101,157",
                            "exactCurrent": 101157,
                            "old": "₩112,397",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩202,314</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": true,
                            "noCCRequired": false
                        },
                        "type": "Dual"
                    },
                    "neighbourhood": "역삼",
                    "deals": {
                        "secretPrice": {
                            "dealText": "비밀 가격으로 추가 할인받기"
                        },
                        "priceReasoning": "DRR-443"
                    },
                    "messaging": {
                        "scarcity": "저희 앱에서 4개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.501162,
                        "lon": 127.035832
                    },
                    "roomsLeft": 4,
                    "providerType": "LOCAL",
                    "supplierHotelId": 4349551,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/5000000/4350000/4349600/4349551/22ec44b0_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 600484,
                    "name": "스테이호텔 강남 (Stay Hotel Gangnam)",
                    "starRating": 2.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "논현로87길 15-4",
                        "extendedAddress": "강남구",
                        "locality": "서울특별시",
                        "postalCode": "06236",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.2,
                        "rating": "4.2",
                        "total": 244,
                        "scale": 5,
                        "badge": "very-good",
                        "badgeText": "매우 좋음"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "1.3km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "2.6km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩111,410",
                            "exactCurrent": 111410,
                            "old": "₩146,581",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩222,820</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "역삼",
                    "deals": {
                        "specialDeal": {
                            "dealText": "24% 할인"
                        },
                        "priceReasoning": "DRR-446"
                    },
                    "messaging": {
                        "scarcity": "저희 앱에서 4개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.499438,
                        "lon": 127.035615
                    },
                    "roomsLeft": 4,
                    "providerType": "LOCAL",
                    "supplierHotelId": 15412296,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/16000000/15420000/15412300/15412296/8e0c61da_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                },
                {
                    "id": 592479,
                    "name": "류게스트하우스 (Ryu Guest House)",
                    "starRating": 2.5,
                    "urls": {},
                    "address": {
                        "streetAddress": "강남구 논현로159길 22",
                        "extendedAddress": "",
                        "locality": "서울특별시",
                        "postalCode": "06018",
                        "region": "",
                        "countryName": "대한민국",
                        "countryCode": "kr",
                        "obfuscate": false
                    },
                    "welcomeRewards": {
                        "collect": true
                    },
                    "guestReviews": {
                        "unformattedRating": 4.5,
                        "rating": "4.5",
                        "total": 29,
                        "scale": 5,
                        "badge": "superb",
                        "badgeText": "매우 훌륭함"
                    },
                    "landmarks": [
                        {
                            "label": "선릉역",
                            "distance": "2.9km"
                        },
                        {
                            "label": "삼성역",
                            "distance": "3.6km"
                        }
                    ],
                    "geoBullets": [],
                    "ratePlan": {
                        "price": {
                            "current": "₩116,722",
                            "exactCurrent": 116722,
                            "old": "₩129,691",
                            "info": "객실당 1박 요금",
                            "summary": "세금 및 수수료 불포함",
                            "additionalInfo": "이는 선택하신 날짜의 1박 평균 요금입니다.",
                            "totalPricePerStay": "(2박 요금: <strong>₩233,444</strong>)"
                        },
                        "features": {
                            "freeCancellation": true,
                            "paymentPreference": false,
                            "noCCRequired": false
                        },
                        "type": "EC"
                    },
                    "neighbourhood": "신사동",
                    "deals": {
                        "specialDeal": {
                            "dealText": "모바일 한정: 10% 할인"
                        },
                        "priceReasoning": "DRR-444"
                    },
                    "messaging": {
                        "scarcity": "저희 앱에서 1개 남았어요!"
                    },
                    "badging": {},
                    "pimmsAttributes": "DoubleStamps|D13|TESCO",
                    "coordinate": {
                        "lat": 37.52311,
                        "lon": 127.0262
                    },
                    "roomsLeft": 1,
                    "providerType": "LOCAL",
                    "supplierHotelId": 15157404,
                    "isAlternative": false,
                    "optimizedThumbUrls": {
                        "srpDesktop": "https://exp.cdn-hotels.com/hotels/16000000/15160000/15157500/15157404/8af1c158_z.jpg?impolicy=fcrop&w=250&h=140&q=high"
                    }
                }
            ],
            "pagination": {
                "currentPage": 1,
                "pageGroup": "EXPEDIA_IN_POLYGON",
                "nextPageStartIndex": 25,
                "nextPageNumber": 2,
                "nextPageGroup": "EXPEDIA_IN_POLYGON"
            }
        },
        "sortResults": {
            "options": [
                {
                    "label": "추천",
                    "itemMeta": "popular",
                    "choices": [
                        {
                            "label": "추천",
                            "value": "BEST_SELLER",
                            "selected": false
                        }
                    ],
                    "enhancedChoices": []
                },
                {
                    "label": "숙박 시설 등급",
                    "itemMeta": "star",
                    "selectedChoiceLabel": "숙박 시설 등급(높은 순)",
                    "choices": [
                        {
                            "label": "숙박 시설 등급(높은 순)",
                            "value": "STAR_RATING_HIGHEST_FIRST",
                            "selected": true
                        },
                        {
                            "label": "숙박 시설 등급(낮은 순)",
                            "value": "STAR_RATING_LOWEST_FIRST",
                            "selected": false
                        }
                    ],
                    "enhancedChoices": []
                },
                {
                    "label": "거리",
                    "itemMeta": "distance",
                    "choices": [
                        {
                            "label": "도심까지의 거리",
                            "value": "DISTANCE_FROM_LANDMARK",
                            "selected": false
                        }
                    ],
                    "enhancedChoices": [
                        {
                            "label": "랜드마크",
                            "itemMeta": "landmarks",
                            "choices": [
                                {
                                    "label": "LG 강남 타워",
                                    "id": 10894757
                                },
                                {
                                    "label": "LG 아트센터",
                                    "id": 10679018
                                },
                                {
                                    "label": "SMTOWN 코엑스아티움",
                                    "id": 11130093
                                },
                                {
                                    "label": "가로수길",
                                    "id": 1713591
                                },
                                {
                                    "label": "강남파이낸스센터",
                                    "id": 10660398
                                },
                                {
                                    "label": "교보 타워",
                                    "id": 10601132
                                },
                                {
                                    "label": "국기원",
                                    "id": 10678912
                                },
                                {
                                    "label": "도산공원",
                                    "id": 10645186
                                },
                                {
                                    "label": "봉은사",
                                    "id": 1655786
                                },
                                {
                                    "label": "비주얼아트센터 보다",
                                    "id": 10441808
                                },
                                {
                                    "label": "삼성타워팰리스",
                                    "id": 1655788
                                },
                                {
                                    "label": "선정릉",
                                    "id": 1713683
                                },
                                {
                                    "label": "세븐럭 카지노 강남점",
                                    "id": 10440506
                                },
                                {
                                    "label": "스타필드 코엑스몰",
                                    "id": 10894751
                                },
                                {
                                    "label": "압구정 로데오거리",
                                    "id": 1711618
                                },
                                {
                                    "label": "코엑스",
                                    "id": 1655777
                                },
                                {
                                    "label": "코엑스 아쿠아리움",
                                    "id": 1713592
                                },
                                {
                                    "label": "테헤란로",
                                    "id": 1713585
                                },
                                {
                                    "label": "한국자수박물관",
                                    "id": 10644767
                                },
                                {
                                    "label": "현대 백화점",
                                    "id": 10461231
                                }
                            ]
                        },
                        {
                            "label": "역",
                            "itemMeta": "stations",
                            "choices": [
                                {
                                    "label": "강남구청역",
                                    "id": 1719302
                                },
                                {
                                    "label": "개포동역",
                                    "id": 1719102
                                },
                                {
                                    "label": "구룡역",
                                    "id": 1719103
                                },
                                {
                                    "label": "대모산입구역",
                                    "id": 1719101
                                },
                                {
                                    "label": "대청역",
                                    "id": 1719079
                                },
                                {
                                    "label": "대치역",
                                    "id": 1719077
                                },
                                {
                                    "label": "도곡역",
                                    "id": 1719076
                                },
                                {
                                    "label": "매봉역",
                                    "id": 1721137
                                },
                                {
                                    "label": "봉은사역",
                                    "id": 12676800
                                },
                                {
                                    "label": "삼성역",
                                    "id": 1719422
                                },
                                {
                                    "label": "선릉역",
                                    "id": 1719421
                                },
                                {
                                    "label": "수서역",
                                    "id": 1719081
                                },
                                {
                                    "label": "신사역",
                                    "id": 1719067
                                },
                                {
                                    "label": "압구정역",
                                    "id": 1719066
                                },
                                {
                                    "label": "역삼역",
                                    "id": 1719419
                                },
                                {
                                    "label": "일원역",
                                    "id": 1719080
                                },
                                {
                                    "label": "청담역",
                                    "id": 1719299
                                },
                                {
                                    "label": "학동역",
                                    "id": 1719307
                                },
                                {
                                    "label": "학여울역",
                                    "id": 1719078
                                },
                                {
                                    "label": "한티역",
                                    "id": 1719104
                                }
                            ]
                        },
                        {
                            "label": "공항",
                            "itemMeta": "airports",
                            "choices": [
                                {
                                    "label": "서울 (GMP-김포국제공항)",
                                    "id": 1665709
                                }
                            ]
                        }
                    ]
                },
                {
                    "label": "고객 평점",
                    "itemMeta": "rating",
                    "choices": [
                        {
                            "label": "고객 평점",
                            "value": "GUEST_RATING",
                            "selected": false
                        }
                    ],
                    "enhancedChoices": []
                },
                {
                    "label": "가격",
                    "itemMeta": "price",
                    "choices": [
                        {
                            "label": "가격(높은 순)",
                            "value": "PRICE_HIGHEST_FIRST",
                            "selected": false
                        },
                        {
                            "label": "가격(낮은 순)",
                            "value": "PRICE",
                            "selected": false
                        }
                    ],
                    "enhancedChoices": []
                }
            ],
            "distanceOptionLandmarkId": 1665648
        },
        "filters": {
            "applied": true,
            "name": {
                "item": {
                    "value": ""
                },
                "autosuggest": {
                    "additionalUrlParams": {
                        "resolved-location": "NEIGHBORHOOD:1665648:UNKNOWN:UNKNOWN",
                        "q-destination": "강남, 서울, 한국",
                        "destination-id": "1665648"
                    }
                }
            },
            "starRating": {
                "applied": false,
                "items": [
                    {
                        "value": 5
                    },
                    {
                        "value": 4
                    },
                    {
                        "value": 3
                    },
                    {
                        "value": 2
                    },
                    {
                        "value": 1
                    }
                ]
            },
            "guestRating": {
                "range": {
                    "min": {
                        "defaultValue": 0
                    },
                    "max": {
                        "defaultValue": 5
                    }
                }
            },
            "landmarks": {
                "selectedOrder": [],
                "items": [
                    {
                        "label": "선릉역",
                        "value": "1719421"
                    },
                    {
                        "label": "삼성역",
                        "value": "1719422"
                    },
                    {
                        "label": "서울 (GMP-김포국제공항)",
                        "value": "1665709"
                    },
                    {
                        "label": "코엑스",
                        "value": "1655777"
                    },
                    {
                        "label": "가로수길",
                        "value": "1713591"
                    },
                    {
                        "label": "수서역",
                        "value": "1719081"
                    },
                    {
                        "label": "신사역",
                        "value": "1719067"
                    },
                    {
                        "label": "역삼역",
                        "value": "1719419"
                    },
                    {
                        "label": "강남구청역",
                        "value": "1719302"
                    },
                    {
                        "label": "압구정 로데오거리",
                        "value": "1711618"
                    },
                    {
                        "label": "압구정역",
                        "value": "1719066"
                    },
                    {
                        "label": "봉은사",
                        "value": "1655786"
                    },
                    {
                        "label": "선정릉",
                        "value": "1713683"
                    },
                    {
                        "label": "스타필드 코엑스몰",
                        "value": "10894751"
                    },
                    {
                        "label": "청담역",
                        "value": "1719299"
                    },
                    {
                        "label": "일원역",
                        "value": "1719080"
                    },
                    {
                        "label": "학동역",
                        "value": "1719307"
                    },
                    {
                        "label": "대치역",
                        "value": "1719077"
                    },
                    {
                        "label": "학여울역",
                        "value": "1719078"
                    },
                    {
                        "label": "테헤란로",
                        "value": "1713585"
                    }
                ],
                "distance": []
            },
            "neighbourhood": {
                "applied": false,
                "items": [
                    {
                        "label": "강남",
                        "value": "1665648"
                    },
                    {
                        "label": "코엑스",
                        "value": "1773305"
                    },
                    {
                        "label": "삼성동",
                        "value": "1713605"
                    },
                    {
                        "label": "역삼",
                        "value": "1772395"
                    },
                    {
                        "label": "청담동",
                        "value": "1805581"
                    },
                    {
                        "label": "압구정",
                        "value": "1711619"
                    },
                    {
                        "label": "신사동",
                        "value": "11129227"
                    },
                    {
                        "label": "논현동",
                        "value": "11114222"
                    },
                    {
                        "label": "도곡동",
                        "value": "10584556"
                    },
                    {
                        "label": "개포동",
                        "value": "12547821"
                    },
                    {
                        "label": "대치동",
                        "value": "12549439"
                    }
                ]
            },
            "accommodationType": {
                "applied": false,
                "items": [
                    {
                        "label": "게스트하우스",
                        "value": "30"
                    },
                    {
                        "label": "리조트",
                        "value": "3"
                    },
                    {
                        "label": "모텔",
                        "value": "7"
                    },
                    {
                        "label": "빌라",
                        "value": "14"
                    },
                    {
                        "label": "아파트",
                        "value": "15"
                    },
                    {
                        "label": "아파트식 호텔",
                        "value": "20"
                    },
                    {
                        "label": "인/여관",
                        "value": "8"
                    },
                    {
                        "label": "캐러밴 파크",
                        "value": "21"
                    },
                    {
                        "label": "캐빈 & 로지",
                        "value": "9"
                    },
                    {
                        "label": "캡슐 호텔",
                        "value": "43"
                    },
                    {
                        "label": "코티지",
                        "value": "11"
                    },
                    {
                        "label": "펜션",
                        "value": "25"
                    },
                    {
                        "label": "호스텔",
                        "value": "12"
                    },
                    {
                        "label": "호텔",
                        "value": "1"
                    },
                    {
                        "label": "휴가용 주택",
                        "value": "4"
                    }
                ]
            },
            "facilities": {
                "applied": false,
                "items": [
                    {
                        "label": "24시간 운영 프런트 데스크",
                        "value": "2063"
                    },
                    {
                        "label": "WiFi 포함",
                        "value": "527"
                    },
                    {
                        "label": "객실 내 욕조",
                        "value": "517"
                    },
                    {
                        "label": "객실 연결 가능",
                        "value": "523"
                    },
                    {
                        "label": "공항 교통편",
                        "value": "513"
                    },
                    {
                        "label": "금연",
                        "value": "529"
                    },
                    {
                        "label": "레스토랑",
                        "value": "256"
                    },
                    {
                        "label": "바",
                        "value": "515"
                    },
                    {
                        "label": "반려동물 동반 가능",
                        "value": "64"
                    },
                    {
                        "label": "비즈니스 시설",
                        "value": "519"
                    },
                    {
                        "label": "수영장",
                        "value": "128"
                    },
                    {
                        "label": "스키 보관 시설",
                        "value": "533"
                    },
                    {
                        "label": "스키 셔틀",
                        "value": "531"
                    },
                    {
                        "label": "스키 타고 출입 가능",
                        "value": "535"
                    },
                    {
                        "label": "스파",
                        "value": "539"
                    },
                    {
                        "label": "아침 식사 포함",
                        "value": "2048"
                    },
                    {
                        "label": "유아용 침대 제공",
                        "value": "525"
                    },
                    {
                        "label": "인터넷",
                        "value": "8"
                    },
                    {
                        "label": "전기차 충전",
                        "value": "1073743315"
                    },
                    {
                        "label": "주방",
                        "value": "32"
                    },
                    {
                        "label": "주차 가능",
                        "value": "16384"
                    },
                    {
                        "label": "주차 포함",
                        "value": "134234112"
                    },
                    {
                        "label": "카지노",
                        "value": "2112"
                    },
                    {
                        "label": "탁아 서비스",
                        "value": "521"
                    },
                    {
                        "label": "피트니스",
                        "value": "2"
                    },
                    {
                        "label": "회의 시설",
                        "value": "1"
                    },
                    {
                        "label": "흡연 가능",
                        "value": "537"
                    }
                ]
            },
            "accessibility": {
                "applied": false,
                "items": [
                    {
                        "label": "객실 내 장애인 편의 시설",
                        "value": "1048576"
                    },
                    {
                        "label": "롤인 샤워(휠체어 이용)",
                        "value": "262144"
                    },
                    {
                        "label": "배리어 프리",
                        "value": "65536"
                    },
                    {
                        "label": "장애인 지원 욕실",
                        "value": "131072"
                    },
                    {
                        "label": "장애인 지원 주차",
                        "value": "524288"
                    },
                    {
                        "label": "점자 지원",
                        "value": "4194304"
                    },
                    {
                        "label": "청각 장애인 지원 장비",
                        "value": "2097152"
                    },
                    {
                        "label": "휠체어 이용 가능 객실",
                        "value": "541"
                    }
                ]
            },
            "themesAndTypes": {
                "applied": false,
                "items": [
                    {
                        "label": "가족 여행",
                        "value": "25"
                    },
                    {
                        "label": "골프",
                        "value": "26"
                    },
                    {
                        "label": "럭셔리",
                        "value": "15"
                    },
                    {
                        "label": "로맨틱",
                        "value": "1"
                    },
                    {
                        "label": "부티크",
                        "value": "4"
                    },
                    {
                        "label": "비즈니스",
                        "value": "14"
                    },
                    {
                        "label": "성소수자 환영",
                        "value": "21"
                    },
                    {
                        "label": "쇼핑",
                        "value": "17"
                    },
                    {
                        "label": "스키",
                        "value": "28"
                    },
                    {
                        "label": "스파",
                        "value": "27"
                    },
                    {
                        "label": "어드벤처",
                        "value": "18"
                    },
                    {
                        "label": "온천",
                        "value": "22"
                    },
                    {
                        "label": "와이너리/포도밭",
                        "value": "19"
                    },
                    {
                        "label": "카지노",
                        "value": "8"
                    }
                ]
            },
            "price": {
                "label": "1박 요금",
                "range": {
                    "min": {
                        "defaultValue": 0,
                        "value": 0
                    },
                    "max": {
                        "defaultValue": 1000000
                    },
                    "increments": 10000
                },
                "multiplier": 1,
                "applied": true
            },
            "paymentPreference": {
                "items": [
                    {
                        "label": "무료 취소",
                        "value": "fc"
                    }
                ]
            },
            "welcomeRewards": {
                "label": "Hotels.com™ 호텔스닷컴 리워드",
                "items": [
                    {
                        "label": "스탬프 적립",
                        "value": "collect"
                    },
                    {
                        "label": "리워드* 숙박 사용",
                        "value": "redeem"
                    }
                ]
            }
        },
        "pointOfSale": {
            "currency": {
                "code": "KRW",
                "symbol": "₩",
                "separators": ",.",
                "format": "₩{0}"
            }
        }
    }
    
    
    export default hotelsData

    src 폴더에 위와 같이 hotelsData.js 파일을 생성하고 가짜 데이터를 저장한다. 해당 데이터는 hotels.com 에서 제공하는 실제 데이터를 다운로드 한 것이다. 

     

    * 목적지 ID 를 이용하여 API 서버에서 호텔 목록 가져오기

    import React from 'react'
    import { useLocation } from 'react-router-dom'
    
    import { fetchHotelsCom, isArrayNull } from 'lib'
    import hotelsData from '../hotelsData'
    
    const Hotels = () => {
        const location = useLocation()
        const { destinationId, checkIn, checkOut, adultsNumber } = location.state
        console.log(destinationId, checkIn, checkOut, adultsNumber)
    
        const getHotels = async () => {
            // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`)
            // console.log(data)
    
            const {searchResults: {results}} = hotelsData
            console.log(results)
            
            return results
        }
    
        return (
            <div>Hotels Page</div>
        )
    }
    
    export default Hotels

    Hotels.js 파일을 위와 같이 수정하자!

    import { fetchHotelsCom, isArrayNull } from 'lib'

    hotels.com API 서버에서 데이터를 가져오기 위하여 관련된 함수를 임포트한다. 

    import hotelsData from '../hotelsData'

    호텔 검색 결과에 대한 가짜 데이터를 임포트한다.

    const getHotels = async () => {
        // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`)
        // console.log(data)
    
        const {searchResults: {results}} = hotelsData
        console.log(results)
    
        return results
    }

    검색된 호텔 목록을 API 서버로부터 가져온다. 현재는 hotelsData 라는 가짜 데이터를 사용하여 해당 데이터로부터 호텔 목록에 대한 정보를 조회한 다음 반환한다. 

     

    * 호텔 목록 정보를 화면에 보여주기- 리액트 훅 사용하기

    import React, { useState, useEffect } from 'react'
    import { useLocation } from 'react-router-dom'
    
    import { fetchHotelsCom, isArrayNull } from 'lib'
    import hotelsData from '../hotelsData'
    
    const Hotels = () => {
        const location = useLocation()
        const { destinationId, checkIn, checkOut, adultsNumber } = location.state
        console.log(destinationId, checkIn, checkOut, adultsNumber)
    
        const [hotels, setHotels] = useState([])
    
        useEffect( async () => {
            const hotelsList = await getHotels()
            setHotels(hotelsList)
        }, [])
    
        const getHotels = async () => {
            // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`)
            // console.log(data)
    
            const {searchResults: {results}} = hotelsData
            console.log(results)
            
            return results
        }
    
        return (
            <div>Hotels Page</div>
        )
    }
    
    export default Hotels

    Hotels.js 파일을 위와 같이 수정하자!

    import React, { useState, useEffect } from 'react'

    리액트 훅을 사용하기 위하여 해당 함수를 임포트한다. 

    const [hotels, setHotels] = useState([])

    hotels.com API 서버로부터 가져온 호텔 목록 정보를 화면에 보여주기 위하여 hotels 라는 상태를 선언하다.

    useEffect( async () => {
        const hotelsList = await getHotels()
        setHotels(hotelsList)
    }, [])

    Effect Hook 을 사용하여 서버로부터 호텔 목록에 대한 데이터를 가져온 다음, hotels 상태를 해당 데이터로 업데이트한다. 

     

    * 호텔 목록 정보를 화면에 보여주기- HotelItem 컴포넌트 만들기

    const isArrayNull = (array) => {
        return array.length === 0
    }
    const handleNullObj = (obj) => {
        return obj || {}
    }
    export { isArrayNull, handleNullObj }

    lib > helpers.js 파일을 위와 같이 수정하자! 

    const handleNullObj = (obj) => {
        return obj || {}
    }

    handleNullObj 이라는 헬퍼함수를 추가한다. 해당 함수는 인자로 전달된 객체가 undefined 이면 빈 객체를 반환한다.

    export { default as fetchHotelsCom } from './fetchHotelsCom'
    export { isArrayNull, handleNullObj } from './helpers'

    lib > index.js 파일을 위와 같이 수정하자!

     

    components 폴더 하위에 아래와 같은 파일을 생성하자!

    import React from 'react'
    import { Link } from 'react-router-dom'
    
    import { isArrayNull, handleNullObj } from 'lib'
    import './HotelItem.css'
    
    const HotelItem = ({ hotel }) => {
        const { id, name,  optimizedThumbUrls, starRating, address, landmarks, guestReviews, ratePlan, neighbourhood } = handleNullObj(hotel)
        const { srpDesktop } = handleNullObj(optimizedThumbUrls)
        const { streetAddress, locality, postalCode, countryName} = handleNullObj(address)
        const { rating, badgeText} = handleNullObj(guestReviews)
        const { price } = handleNullObj(ratePlan)
        const { old, current, info, summary, totalPricePerStay} = handleNullObj(price)
        const totalPrice = totalPricePerStay? totalPricePerStay.split(/[<>()]/) : []
    
        return (<div className='HotelItem-container'>
                    <Link className='HotelItem-thumbnail' to='/hotelInfo'>
                        <img className='HotelItem-thumbnail-img' src={srpDesktop} alt={name}/>
                    </Link>
                    <div className='HotelItem-info'>
                        <div className='HotelItem-name'>{name} <span>{starRating}성급</span></div>
                        <div className='HotelItem-address'>{streetAddress}, {locality}, {countryName}</div>
                        <div className='HotelItem-neighbourhood'>{neighbourhood}</div>
                        <div className='HotelItem-landmarks'>
                            {!isArrayNull(landmarks) && landmarks.map( (landmark, index) => {
                                return <div key={index}>* {landmark.label}까지 {landmark.distance}</div>
                            })}
                        </div>
                        <div className='HotelItem-rating'>
                            <div className={`HotelItem-rating-badge ${parseInt(rating) < 8 ? 'HotelItem-rating-badge-gray' : ''}`}>{rating}</div>
                            <div className='HotelItem-rating-badgeText'> {badgeText}</div>
                        </div>
                    </div>
                    <div className='HotelItem-price'>
                        <div className='HotelItem-price-per-oneday'><span>{old}</span> {current}</div>
                        <div className='HotelItem-price-per-oneday-title'>{info}</div>
                        <div className='HotelItem-price-total'>{totalPrice[1]} {totalPrice[3]}</div>
                        <div className='HotelItem-price-summary'>{summary}</div>
                    </div>
                </div>)
    }
    export default HotelItem

    HotelItem.js 파일을 위와 같이 생성하자!

    import { Link } from 'react-router-dom'

    호텔 썸네일을 클릭하면 호텔에 대한 자세한 정보가 표시되는 페이지로 이동하기 위하여 Link 컴포넌트를 임포트한다.

    import { isArrayNull, handleNullObj } from 'lib'

    호텔 목록 정보에 대한 유효성 검증을 하기 위하여 lib 폴더로부터 헬퍼함수를 임포트한다. 

    const { id, name,  optimizedThumbUrls, starRating, address, landmarks, guestReviews, ratePlan, neighbourhood } = handleNullObj(hotel)

    특정 호텔 정보로부터 필요한 프로퍼티를 조회한다. 호텔의 ID, 이름, 썸네일, 등급, 주소, 랜드마크, 리뷰, 숙박가격, 가까운 동네 등을 조회한다.

    const { srpDesktop } = handleNullObj(optimizedThumbUrls)

    호텔 썸네일 링크를 조회한다.

    const { streetAddress, locality, postalCode, countryName} = handleNullObj(address)

    호텔 주소를 조회한다.

    const { rating, badgeText} = handleNullObj(guestReviews)

    호텔 리뷰를 조회한다.

    const { price } = handleNullObj(ratePlan)
    const { old, current, info, summary, totalPricePerStay} = handleNullObj(price)

    호텔의 가격을 조회한다. old 는 할인전 가격, current 는 할인후 가격을 의미한다.

    const totalPrice = totalPricePerStay? totalPricePerStay.split(/[<>()]/) : []

    호텔의 최종 숙박가격을 조회한다. totalPricePerStay 값은 원하지 않는 포맷으로 되어 있어서 split 함수와 정규표현식을 사용해서 파싱한 다음 화면에 보여준다. 

    <Link className='HotelItem-thumbnail' to='/hotelInfo'>
        <img className='HotelItem-thumbnail-img' src={srpDesktop} alt={name}/>
    </Link>

    호텔의 썸네일을 화면에 렌더링한다. Link 컴포넌트를 사용하여 썸네일을 클릭하면 해당 호텔에 대한 자세한 정보를 보여주는 페이지로 이동한다.

    <div className='HotelItem-info'>
        <div className='HotelItem-name'>{name} <span>{starRating}성급</span></div>
        <div className='HotelItem-address'>{streetAddress}, {locality}, {countryName}</div>
        <div className='HotelItem-neighbourhood'>{neighbourhood}</div>
        <div className='HotelItem-landmarks'>
            {!isArrayNull(landmarks) && landmarks.map( (landmark, index) => {
                return <div key={index}>* {landmark.label}까지 {landmark.distance}</div>
            })}
        </div>
        <div className='HotelItem-rating'>
            <div className={`HotelItem-rating-badge ${parseInt(rating) < 8 ? 'HotelItem-rating-badge-gray' : ''}`}>{rating}</div>
            <div className='HotelItem-rating-badgeText'> {badgeText}</div>
        </div>
    </div>

    호텔의 기본 정보를 화면에 렌더링한다. 호텔 이름, 등급, 주소, 가까운 동네, 리뷰 등을 화면에 보여준다. rating 값이 8 보다 작으면 회색 배지로 보여주고, 8보다 크면 녹색 배지로 보여준다.

    <div className='HotelItem-price'>
        <div className='HotelItem-price-per-oneday'><span>{old}</span> {current}</div>
        <div className='HotelItem-price-per-oneday-title'>{info}</div>
        <div className='HotelItem-price-total'>{totalPrice[1]} {totalPrice[3]}</div>
        <div className='HotelItem-price-summary'>{summary}</div>
    </div>

    호텔 숙박가격을 화면에 렌더링한다. 호텔의 할인전 가격, 할인후 가격, 총 지불비용 등을 화면에 보여준다. 

    .HotelItem-container{
        display: flex;
        border-bottom: 1px solid lightgray;
    }
    .HotelItem-thumbnail{
        width: 250px;
        height: 140px;
        overflow: hidden;
        border-radius: 5px;
        margin: 10px;
    }
    .HotelItem-thumbnail-img{
        width: 100%;
        height: 100%;
    }
    .HotelItem-info{
        box-sizing: border-box;
        padding: 10px;
        flex: 1;
    }
    .HotelItem-name{
        font-size: 1.3rem;
        font-weight: bold;
    }
    .HotelItem-name span{
        font-size: 0.9rem;
        color: red;
    }
    .HotelItem-address{
        color: #525252;
    }
    .HotelItem-neighbourhood{
        margin-top: 10px;
        margin-bottom: 5px;
        font-weight: bold;
        font-size: 1.1rem;
    }
    .HotelItem-landmarks{
        font-size: 0.9rem;
    }
    .HotelItem-rating{
        display: flex;
        align-items: flex-end;
        margin-top: 10px;
    }
    .HotelItem-rating-badge{
        background-color: #218242;
        color: white;
        padding-left: 5px;
        padding-right: 5px;
        padding-bottom: 2px;
        border-radius: 2px;
    }
    .HotelItem-rating-badgeText{
        font-size: 1rem;
        font-weight: bold;
        margin-left: 10px;
    }
    .HotelItem-price{
        box-sizing: border-box;
        padding: 10px;
        width: 250px;
    
        display: flex;
        flex-direction: column;
        justify-content: flex-start;
        align-items: flex-end;
    }
    .HotelItem-price-per-oneday{
        font-size: 1.5rem;
        font-weight: bold;
    }
    .HotelItem-price-per-oneday span{
        font-size: 0.8rem;
        text-decoration: line-through;
    }
    .HotelItem-price-per-oneday-title{
        font-size: 0.8rem;
        color: #525252;
    }
    .HotelItem-price-total{
        font-weight: bold;
        margin-top: 10px;
    }
    .HotelItem-price-summary{
        font-size: 0.8rem;
        color: #525252;
    }

    HotelItem.css 파일을 위와 같이 생성하자!

    export { default as Input } from './Input'
    export { default as Button } from './Button'
    export { default as Caption } from './Caption'
    export { default as HotelItem } from './HotelItem'

    components > index.js 파일에 HotelItem 컴포넌트도 추가로 내보내도록 한다. 

     

    import React, { useState, useEffect } from 'react'
    import { useLocation } from 'react-router-dom'
    
    import { fetchHotelsCom, isArrayNull } from 'lib'
    import hotelsData from '../hotelsData'
    import { HotelItem } from 'components'
    
    import './Hotels.css'
    
    const Hotels = () => {
        const location = useLocation()
        const { destinationId, checkIn, checkOut, adultsNumber } = location.state
        console.log(destinationId, checkIn, checkOut, adultsNumber)
    
        const [hotels, setHotels] = useState([])
    
        useEffect( async () => {
            const hotelsList = await getHotels()
            setHotels(hotelsList)
        }, [])
    
        const getHotels = async () => {
            // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`)
            // console.log(data)
    
            const {searchResults: {results}} = hotelsData
            console.log(results)
            
            return results
        }
    
        return (
            <div className='Hotels-container'>
                {!isArrayNull(hotels) && hotels.map( hotel => {
                    return (
                        <HotelItem hotel={hotel} key={hotel.id}/>
                    )
                })}
            </div>
        )
    }
    
    export default Hotels

    Hotel.js 파일을 위와 같이 수정하자!

    <div className='Hotels-container'>
        {!isArrayNull(hotels) && hotels.map( hotel => {
            return (
                <HotelItem hotel={hotel} key={hotel.id}/>
            )
        })}
    </div>

    hotels 상태는 전체 호텔 목록에 대한 정보를 담고 있다. hotels 배열에 요소가 하나 이상 있으면 호텔 목록 정보를 화면에 보여준다. HotelItem 컴포넌트로 hotel 이라는 props 를 전달한다. 

    .Hotels-container{
        width: 45%;
        margin: 0 auto;
    }

    pages > Hotels.css 파일을 위와 같이 생성하자! 아래와 같은 화면이 보이면 제대로 된 것이다. 

    호텔 목록 페이지 화면

     

     

    * 호텔 위치를 보여줄 지도 추가하기

     

    지도 라이브러리 참고문서

     

    Quick Start Guide - Leaflet - a JavaScript library for interactive maps

    ← Tutorials Leaflet Quick Start Guide This step-by-step guide will quickly get you started on Leaflet basics, including setting up a Leaflet map, working with markers, polylines and popups, and dealing with events. Preparing your page Before writing any

    leafletjs.com

     

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>호텔 검색 서비스</title>
        <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
       integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
       crossorigin=""/>
       <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
       integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
       crossorigin=""></script>
      </head>
      <body>
        <div id="app"></div>
      </body>
    </html>

     

     

    src > index.html 파일의 head 부분에 관련 라이브러리를 추가한다. 

    .Hotels-container{
        width: 45%;
        margin: 0 auto;
    }
    #map { height: 400px; }

    pages > Hotels.css 파일에 지도의 크기를 설정해준다.

    import React, { useState, useEffect } from 'react'
    import { useLocation } from 'react-router-dom'
    
    import { fetchHotelsCom, isArrayNull, handleNullObj } from 'lib'
    import hotelsData from '../hotelsData'
    import { HotelItem } from 'components'
    
    import './Hotels.css'
    
    const Hotels = () => {
        const location = useLocation()
        const { destinationId, checkIn, checkOut, adultsNumber } = location.state
        console.log(destinationId, checkIn, checkOut, adultsNumber)
    
        const [hotels, setHotels] = useState([])
        const [mapObj, setMapObj] = useState(null)
    
        useEffect( async () => {
            const hotelsList = await getHotels()
            setHotels(hotelsList)
            const m = L.map('map')
            setMapObj(m)
        }, [])
    
        const getHotels = async () => {
            // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`)
            // console.log(data)
    
            const {searchResults: {results}} = hotelsData
            console.log(results)
            
            return results
        }
    
        
        const displayLocation = (lat, lon, msg) => {
            console.log('inside:', mapObj)
    
            if(mapObj){
                const map = mapObj.setView([lat, lon], 13)
    
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                }).addTo(map)
    
                L.marker([lat, lon]).addTo(map)
                    .bindPopup(msg)
                    .openPopup()
            }
        }
    
        return (
            <div className='Hotels-container'>
                <div id="map"></div>
                {!isArrayNull(hotels) && hotels.map( hotel => {
                    const { name, address, coordinate } = handleNullObj(hotel)
                    const { streetAddress, locality, countryName } = handleNullObj(address)
                    const { lat, lon } = handleNullObj(coordinate)
                    const msg = `${name}<br/>${streetAddress}, ${locality}, ${countryName}`
                    displayLocation(lat, lon, msg)
                    return (
                        <HotelItem hotel={hotel} key={hotel.id}/>
                    )
                })}
            </div>
        )
    }
    
    export default Hotels

    Hotels.js 파일을 위와 같이 수정하자!

    import { fetchHotelsCom, isArrayNull, handleNullObj } from 'lib'

    데이터 유효성 검증을 위하여 handleNullObj 함수를 추가로 임포트한다. 해당 함수는 인자로 들어온 값이 undefined 이면 빈 객체를 반환한다. 

     const [mapObj, setMapObj] = useState(null)

    라이브러리에서 사용되는 지도 객체를 조회할 상태값(mapObj)을 선언한다. 

     useEffect( async () => {
         const hotelsList = await getHotels()
         setHotels(hotelsList)
            const m = L.map('map')
            setMapObj(m)
     }, [])
    const m = L.map('map')
    setMapObj(m)

    라이브러리의 지도 객체를 useEffect 함수 외부에서 사용하기 위하여 mapObj 상태에 저장한다.

    const displayLocation = (lat, lon, msg) => {
        console.log('inside:', mapObj)
    
        if(mapObj){
            const map = mapObj.setView([lat, lon], 13)
    
            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
            }).addTo(map)
    
            L.marker([lat, lon]).addTo(map)
                .bindPopup(msg)
                .openPopup()
        }
    }

    mapObj 는 상태값이므로 초기값이 null 로 설정되어 있다. 그러므로 null 이 아닌 실제 지도 객체가 저장되면 그때 지도를 보여준다. 

    const map = mapObj.setView([lat, lon], 13)

    setView 메서드의 첫번째 인자로 배열이 들어간다. 배열요소로 위도(latitude)와 경도(longitude) 값이 설정된다. 이렇게 하면 지도의 중앙에 설정한 위도와 경도가 표시된다. 13 이라는 숫자는 zoom level 을 의미한다. 말그대로 줌의 등급을 나타낸다. 자세하게 줌을 하거나 광범위하게 줌을 한다. 

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map)

    위 코드는 지도의 배경화면을 구성하는 종류와 크기를 설정한다.

    L.marker([lat, lon]).addTo(map)
                    .bindPopup(msg)
                    .openPopup()

    위 코드는 인자로 전달된 위도(lat)와 경도(lon)를 사용하여 마커(marker)로 지도에 위치를 표시한다. bindPopup, openPopup 메서드는 마커에서 보여줄 메세지를 표시한다. 

     <div id="map"></div>

    화면에 지도를 렌더링한다.

    const { name, address, coordinate } = handleNullObj(hotel)

    hotel 객체로부터 호텔의 이름(name), 주소(address), 위치(coordinate)를 조회한다. 

    const { streetAddress, locality, countryName } = handleNullObj(address)

    호텔의 주소 정보(address)로부터 호텔이 위치한 거리(streetAddress), 도시(locality), 국가(countryName) 정보를 추출한다. 

    const { lat, lon } = handleNullObj(coordinate)

    호텔의 위치 정보(coordinate)로부터 위도(lat)와 경도(lon)를 추출한다. 

    const msg = `${name}<br/>${streetAddress}, ${locality}, ${countryName}`

    지도 마커(marker)에 표시할 메세지를 생성한다. 

     displayLocation(lat, lon, msg)

    위 함수를 실행하여 지도에 해당 호텔의 위치를 추가한다.

    호텔의 위치를 지도에 표시한 모습

     

     

    * 호텔 목록 페이지에 필터 적용하기

    components 폴더에 아래 파일들을 생성한다.

    import React from 'react'
    import { isArrayNull } from 'lib'
    
    import { AccordionItem } from 'components'
    
    import './Accordion.css'
    
    const Accordion = ({ title, items, displayFilter }) => {
        return (
            <div className='Accordion-container'>
                <div className='Accordion-menu' onClick={displayFilter}>
                    <div className='Accordion-arrow'></div>
                    <div className='Accordion-title'>{title}</div>
                </div>
                <div className='Accordion-items'>
                    {!isArrayNull(items) && items.map( item => {
                        return (
                            <AccordionItem key={item.value}>{item.label}</AccordionItem>
                        )
                    })}
                </div>
            </div>
        )
    }
    
    export default Accordion

    Accordion.js 파일을 위와 같이 작성하자!

    import { isArrayNull } from 'lib'

    데이터 유효성 검증을 위하여 lib 폴더에서 isArrayNull 함수를 임포트한다.

    import { AccordionItem } from 'components'

    AccordionItem 컴포넌트를 사용하기 위하여 임포트한다. 해당 컴포넌트는 Accordion 컴포넌트(필터 카테고리) 안에 있는 하나의 필터이다. 

    { title, items, displayFilter }

    title 은 필터 카테고리의 이름이다. 예를 들면, 위치 및 주변지역, 랜드마크, 숙박 시설 유형과 같은 문자열 형태이다. items 는 필터 이름에 대한 배열이다. 예를 들면, 숙박 시설 유형 카테고리 안에 있는 게스트하우스, 호텔, 아파트 등과 같은 문자열 형태이다. displayFilter 함수는 사용자가 필터 카테고리를 클릭할때마다 실행되는 이벤트핸들러 함수이다. 

    <div className='Accordion-menu' onClick={displayFilter}>
        <div className='Accordion-arrow'></div>
        <div className='Accordion-title'>{title}</div>
    </div>

    Accordion-menu 요소에 onClick 이벤트를 등록하고 displayFilter 이벤트핸들러 함수를 연결한다. 사용자가 Accordion-menu 요소를 클릭하면 해당 카테고리 안의 필터 목록이 열리고 닫힌다. 

    <div className='Accordion-items'>
        {!isArrayNull(items) && items.map( item => {
            return (
                <AccordionItem key={item.value}>{item.label}</AccordionItem>
            )
        })}
    </div>

    AccordionItem 컴포넌트를 사용하여 필터목록을 화면에 렌더링한다. items 는 필터 목록이 저장된 배열이다. 

    .Accordion-container{
        border-top: 1px solid lightgray;
    }
    .Accordion-menu{
        cursor: pointer;
        display: flex;
        justify-content: flex-start;
        align-items: center;
        user-select: none;
    }
    .Accordion-arrow{
        width: 20px;
        height: 20px;
        margin-right: 10px;
        background-image: url('../assets/images/down-arrow.png');
        background-size: cover;
    }
    .change-arrow{
        background-image: url('../assets/images/up-arrow.png');
    }
    
    .Accordion-title{
        font-size: 1.2rem;
        font-weight: bold;
        margin-top: 20px;
        margin-bottom: 20px;
        color: #1a1c1b;
    }
    .Accordion-items{
        display: none;
    }
    .expand-filter{
        display: block;
    }

    Accordion.css 파일을 위와 같이 작성하자! 화살표 아이콘은 div 요소의 배경 이미지로 설정하고, change-arrow 라는 클래스명을 추가하거나 제거함으로써 화살표의 스타일을 변경한다. 

    필터 카테고리를 열고 닫을때 보여줄 화살표는 아래 사이트에서 다운로드 받았다. assets > images 폴더에 다운로드 받은 화살표 리소스를 추가한다. 아이콘 이름은 각각 down-arrow.png 와 up-arrow.png 로 수정하도록 한다. 

    화살표 아이콘 리소스 다운로드 사이트

     

    Arrow Icons – Free Vector Download, PNG, SVG, GIF

    Tell us about an icon you need, and we will draw it for free in one of the existing Icons8 styles or any other (but paid). Request icon

    icons8.com

     

    import React from 'react'
    import './AccordionItem.css'
    
    const AccordionItem = ({ children }) => {
        return (
            <div className='AccordionItem-container'>
                <div className='AccordionItem-checker'><input type='checkbox'/></div>
                <div className='AccordionItem-filter'>{children}</div>
            </div>
        )
    }
    
    export default AccordionItem

    AccordionItem.js 파일을 위와 같이 작성하자!

    chidren 은 필터 이름을 의미한다. 예를 들면, 숙박 시설 유형이라는 필터 카테고리 안에 있는 게스트 하우스, 호텔, 아파트 등과 같은 문자열이다. 체크박스를 함께 렌더링하여 사용자가 특정 필터를 클릭하면 해당 필터를 반영한 호텔 목록만 다시 검색해서 보여주려고 한다. 

    .AccordionItem-container{
        display: flex;
        justify-content: flex-start;
    
        margin-top: 15px;
        margin-bottom: 15px;
        user-select: none;
    }
    .AccordionItem-checker{
        margin-right: 10px;
    }
    .AccordionItem-checker input{
        width: 18px;
        height: 18px;
    }
    .AccordionItem-filter{
        color: #333333;
    }

    AccordionItem.css 파일을 위와 같이 작성하자!

    export { default as Input } from './Input'
    export { default as Button } from './Button'
    export { default as Caption } from './Caption'
    export { default as HotelItem } from './HotelItem'
    export { default as Accordion } from './Accordion'
    export { default as AccordionItem } from './AccordionItem'

    components > index.js 파일에 생성한 컴포넌트를 추가로 내보낸다.

    import React, { useState, useEffect } from 'react'
    import { useLocation } from 'react-router-dom'
    
    import { fetchHotelsCom, isArrayNull, handleNullObj } from 'lib'
    import hotelsData from '../hotelsData'
    import { HotelItem, Accordion } from 'components'
    
    import './Hotels.css'
    
    const Hotels = () => {
        const location = useLocation()
        const { destinationId, checkIn, checkOut, adultsNumber } = handleNullObj(location.state)
        console.log(destinationId, checkIn, checkOut, adultsNumber)
    
        const [hotels, setHotels] = useState([])
        const [mapObj, setMapObj] = useState(null)
        const [filters, setFilters] = useState(null)
    
        useEffect( async () => {
            const { results, filters } = await getHotels()
            setHotels(results)
            setFilters(filters)
    
            const m = L.map('map')
            setMapObj(m)
        }, [])
    
        const getHotels = async () => {
            // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`)
            // console.log(data)
    
            const {searchResults: {results}, filters } = hotelsData
            
            return { results, filters }
        }
    
        
        const displayLocation = (lat, lon, msg) => {
            // console.log('inside:', mapObj)
    
            if(mapObj){
                const map = mapObj.setView([lat, lon], 13)
    
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                }).addTo(map)
    
                L.marker([lat, lon]).addTo(map)
                    .bindPopup(msg)
                    .openPopup()
            }
        }
    
        const displayFilter = (e) => {
            const target = e.target.closest('.Accordion-container')
            const arrow = target.querySelector('.Accordion-arrow')
            const items = target.querySelector('.Accordion-items')
            console.log(target)
            console.log(arrow, items)
    
            arrow.classList.toggle('change-arrow')
            items.classList.toggle('expand-filter')
    
        }
    
        const AccordionList = () => {
            if(filters){
                const { neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility } = handleNullObj(filters)
                const filterTypes = [
                    {items: handleNullObj(neighbourhood).items, title: '위치 및 주변 지역'},
                    {items: handleNullObj(landmarks).items, title: '랜드마크'},
                    {items: handleNullObj(accommodationType).items, title: '숙박 시설 유형'},
                    {items: handleNullObj(facilities).items, title: '시설'},
                    {items: handleNullObj(themesAndTypes).items, title: '테마/유형'},
                    {items: handleNullObj(accessibility).items, title: '장애인 편의 시설'},
                ]
    
                return (
                    <div>{filterTypes.map( (filterType, id) => {
                        return (
                            <Accordion key={id} title={filterType.title} items={filterType.items} displayFilter={displayFilter}/>
                        )
                    })}</div>
                )
            }else{
                return <></>
            }
        }
    
        const getLocation = (hotel) => {
            const { name, address, coordinate } = handleNullObj(hotel)
            const { streetAddress, locality, countryName } = handleNullObj(address)
            const { lat, lon } = handleNullObj(coordinate)
            const msg = `${name}<br/>${streetAddress}, ${locality}, ${countryName}`
    
            return { lat, lon, msg }
        }
    
        return (
            <div className='Hotels-container'>
                <div className='Hotels-filtered'>
                    <AccordionList/>
                </div>
                <div className='Hotels-searched'>
                    <div id="map"></div>
                    {!isArrayNull(hotels) && hotels.map( hotel => {
                        const { lat, lon, msg } = getLocation(hotel)
                        displayLocation(lat, lon, msg)
                        return (
                            <HotelItem hotel={hotel} key={hotel.id}/>
                        )
                    })}
                </div>
            </div>
        )
    }
    
    export default Hotels

    Hotels.js 파일을 위와 같이 수정하자!

    import { HotelItem, Accordion } from 'components'

    필터 카테고리와 필터 목록을 보여주기 위하여 Accordion 컴포넌트를 추가로 임포트한다. 

    const { destinationId, checkIn, checkOut, adultsNumber } = handleNullObj(location.state)

    홈화면에서 전달받은 데이터가 존재하지 않을수도 있으므로 handleNullObj 함수로 location.state 가 undefined 인 경우를 대비하여 데이터 유효성 검증을 한다.

     const [filters, setFilters] = useState(null)

    hotelsCom API 서버로부터 전달받은 필터 카테고리와 필터 목록을 화면에 보여주기 위하여 State Hook 을 사용하여 filters 상태를 선언한다. 

    const getHotels = async () => {
        // const data = await fetchHotelsCom(`https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`)
        // console.log(data)
    
        const {searchResults: {results}, filters } = hotelsData
    
        return { results, filters }
    }

    API 서버로부터 데이터를 추출할때 filters 프로퍼티를 추가로 추출한다. 반환값으로는 filters 값이 추가된 객체 형태로 내보낸다. 

    const { results, filters } = await getHotels()
    setHotels(results)
    setFilters(filters)

    setFilters 함수를 사용하여 getHotels 함수로부터 반환된 filters 값으로 filters 상태값을 업데이트한다. 

    const displayFilter = (e) => {
        const target = e.target.closest('.Accordion-container') // 필터 카테고리 요소
        const arrow = target.querySelector('.Accordion-arrow')  // 화살표
        const items = target.querySelector('.Accordion-items')  // 필터 목록
        console.log(target)
        console.log(arrow, items)
    
        arrow.classList.toggle('change-arrow')  // 화살표 아이콘 변경
        items.classList.toggle('expand-filter') // 필터 목록 열고 닫기
    
    }

    사용자가 필터 카테고리를 선택하면 실행되는 이벤트핸들러 함수이다. 필터 카테고리를 선택하면 화살표 아이콘을 변경하고, 필터 목록을 열고 닫는다. 

    const AccordionList = () => {
        if(filters){
            const { neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility } = handleNullObj(filters)
            const filterTypes = [
                {items: handleNullObj(neighbourhood).items, title: '위치 및 주변 지역'},
                {items: handleNullObj(landmarks).items, title: '랜드마크'},
                {items: handleNullObj(accommodationType).items, title: '숙박 시설 유형'},
                {items: handleNullObj(facilities).items, title: '시설'},
                {items: handleNullObj(themesAndTypes).items, title: '테마/유형'},
                {items: handleNullObj(accessibility).items, title: '장애인 편의 시설'},
            ]
    
            return (
                <div>{filterTypes.map( (filterType, id) => {
                    return (
                        <Accordion key={id} title={filterType.title} items={filterType.items} displayFilter={displayFilter}/>
                    )
                })}</div>
            )
        }else{
            return <></>
        }
    }

    AccordionList 는 필터 카테고리 목록을 화면에 렌더링하기 위한 컴포넌트이다. filters 는 상태값이므로 초기상태는 null 이고, 서버로부터 데이터를 전달받으면 같이 존재한다. 그래서 조건문을 사용하여 값이 존재하는 경우에만 화면에 필터 카테고리와 필터 목록을 보여주도록 한다. 

    const { neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility } = handleNullObj(filters)

    filters 객체로부터 6개의 필터 카테고리를 추출한다. 

    const filterTypes = [
        {items: handleNullObj(neighbourhood).items, title: '위치 및 주변 지역'},
        {items: handleNullObj(landmarks).items, title: '랜드마크'},
        {items: handleNullObj(accommodationType).items, title: '숙박 시설 유형'},
        {items: handleNullObj(facilities).items, title: '시설'},
        {items: handleNullObj(themesAndTypes).items, title: '테마/유형'},
        {items: handleNullObj(accessibility).items, title: '장애인 편의 시설'},
    ]

    필터 카테고리 필드에서 items 프로퍼티만 사용할 것이므로 객체의 배열로 데이터를 재가공한다. 필터 카테고리 이름을 의미하는 title 프로퍼티를 추가로 설정한다. 필터 카테고리에 해당하는 neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility 값이 undefined 인 경우를 대비하여 handleNullObj 함수로 데이터 유효성 검증을 하도록 한다. 

    <div>{filterTypes.map( (filterType, id) => {
        return (
            <Accordion key={id} title={filterType.title} items={filterType.items} displayFilter={displayFilter}/>
        )
    })}</div>

    필터 카테고리 배열을 순회하면서 Accordion 컴포넌트를 사용하여 필터 카테고리와 필터 목록을 화면에 렌더링한다. 

    const getLocation = (hotel) => {
        const { name, address, coordinate } = handleNullObj(hotel)
        const { streetAddress, locality, countryName } = handleNullObj(address)
        const { lat, lon } = handleNullObj(coordinate)
        const msg = `${name}<br/>${streetAddress}, ${locality}, ${countryName}`
    
        return { lat, lon, msg }
    }

    호텔목록을 지도에 표시하기 위하여 위도, 경도, 호텔 주소 정보를 추출하는 코드를 getLocation 이라는 함수로 빼낸다.

    <div className='Hotels-filtered'>
        <AccordionList/>
    </div>

    필터 카테고리 목록을 화면에 렌더링한다.

    const { lat, lon, msg } = getLocation(hotel)
    displayLocation(lat, lon, msg)
    return (
        <HotelItem hotel={hotel} key={hotel.id}/>
    )

    호텔에 대한 위치 정보를 추출하기 위하여 getLocation 함수를 사용한다. 

    .Hotels-container{
        width: 80%;
        margin: 0 auto;
    
        display: flex;
        /* flex-wrap: wrap; */
        justify-content: center;
        align-items: flex-start;
    }
    .Hotels-filtered{
        width: 30%;
        margin-right: 1%;
        /* border: 1px solid red; */
    }
    .Hotels-searched{
        width: 69%;
        /* border: 1px solid blue; */
    }
    #map { height: 400px; }

    Hotels.css 파일을 위와 같이 위와 같이 수정하자!

    필터 카테고리가 추가된 화면
    필터 목록이 열린 화면

     

     

    이제 사용자가 선택한 필터로 호텔을 재검색하는 기능을 구현해보자!

    import React from 'react'
    import { isArrayNull } from 'lib'
    
    import { AccordionItem } from 'components'
    
    import './Accordion.css'
    
    const Accordion = ({ title, items, displayFilter, searchHotelsWithFilter, querystring }) => {
        return (
            <div className='Accordion-container'>
                <div className='Accordion-menu' onClick={displayFilter}>
                    <div className='Accordion-arrow'></div>
                    <div className='Accordion-title'>{title}</div>
                </div>
                <div className='Accordion-items'>
                    {!isArrayNull(items) && items.map( item => {
                        return (
                            <AccordionItem key={item.value} value={item.value} searchHotelsWithFilter={searchHotelsWithFilter} querystring={querystring}>{item.label}</AccordionItem>
                        )
                    })}
                </div>
            </div>
        )
    }
    
    export default Accordion

    Accordion.js 파일을 위와 같이 수정하자!

    { title, items, displayFilter, searchHotelsWithFilter, querystring }

    Accordion 컴포넌트의 props 에 searchHotelsWithFilter 함수와 querystring 이라는 문자열이 추가되었다. 모두 Accordion 컴포넌트에서 사용하는 것이 아니라 자식 컴포넌트인 AccordionItem 컴포넌트로 전달만 해준다.

     <AccordionItem key={item.value} value={item.value} searchHotelsWithFilter={searchHotelsWithFilter} querystring={querystring}>{item.label}</AccordionItem>

    AccordionItem 컴포넌트의 props에 searchHotelsWithFilter 함수와 querystring 이라는 문자열, 그리고 value 를 추가로 전달한다. 

    import React from 'react'
    import './AccordionItem.css'
    
    const AccordionItem = ({ children, searchHotelsWithFilter, value, querystring }) => {
        return (
            <div className='AccordionItem-container'>
                <div className='AccordionItem-checker' onClick={() => searchHotelsWithFilter(querystring, value)}><input type='checkbox'/></div>
                <div className='AccordionItem-filter'>{children}</div>
            </div>
        )
    }
    
    export default AccordionItem

    AccordionItem.js 파일을 위와 같이 수정하자!

    { children, searchHotelsWithFilter, value, querystring }

    AccordionItem 컴포넌트의 props에 searchHotelsWithFilter 함수와 querystring 이라는 문자열, 그리고 value 를 추가로 전달받는다. 

    <div className='AccordionItem-checker' onClick={() => searchHotelsWithFilter(querystring, value)}><input type='checkbox'/></div>

    사용자가 필터 카테고리에서 적용하고자 하는 필터를 선택할때마다 click 이벤트가 발생하면서 searchHotelsWithFilter 함수가 실행된다. 해당 함수의 인자로 querystring 과 value 값이 전달된다. querystring 은 hotels.com API 서버에서 데이터를 가져올때 URL 에 들어가는 쿼리 파라미터이고, value 는 쿼리 파라미터에 대한 값이다.

     

    아래와 같이 accommodation_ids, theme_ids 와 같은 문자열이 querystring 이다. 20%2C15%2C5%2C1 이 value 에 해당된다.

    쿼리 파라미터와 해당하는 값

     

    import React, { useState, useEffect } from 'react'
    import { useLocation } from 'react-router-dom'
    
    import { fetchHotelsCom, isArrayNull, handleNullObj } from 'lib'
    import hotelsData from '../hotelsData'
    import { HotelItem, Accordion, Button } from 'components'
    
    import './Hotels.css'
    
    const Hotels = () => {
        let query = {}
        const location = useLocation()
        const { destinationId, checkIn, checkOut, adultsNumber } = handleNullObj(location.state)
        console.log(destinationId, checkIn, checkOut, adultsNumber)
    
        const BASE_URL = `https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`
    
        const [hotels, setHotels] = useState([])
        const [mapObj, setMapObj] = useState(null)
        const [filters, setFilters] = useState(null)
        const [queryURL, setQueryURL] = useState(null)
    
        useEffect( async () => {
            console.log(BASE_URL)
            const { results, filters } = await getHotels(BASE_URL)
            setHotels(results)
            setFilters(filters)
    
            const m = L.map('map')
            setMapObj(m)
        }, [])
    
        useEffect( async () => {
            console.log('query: ', queryURL)
            console.log(BASE_URL)
            let url = BASE_URL
            
            for(let prop in queryURL){
                const queryvalue = encodeURIComponent(queryURL[prop].join(','))
                url += `&${prop}=${queryvalue}`
                console.log(prop, queryvalue)
            }
            console.log('total url: ', url)
    
            const { results } = await getHotels(url)
            setHotels(results)
        }, [queryURL])
    
        const getHotels = async (url) => {
            // const data = await fetchHotelsCom(url)
            // console.log(data)
            // const {searchResults: {results}, filters } = data
    
            const {searchResults: {results}, filters } = hotelsData
            
            return { results, filters }
        }
    
        
        const displayLocation = (lat, lon, msg) => {
            // console.log('inside:', mapObj)
    
            if(mapObj){
                const map = mapObj.setView([lat, lon], 13)
    
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                }).addTo(map)
    
                L.marker([lat, lon]).addTo(map)
                    .bindPopup(msg)
                    .openPopup()
            }
        }
    
        const displayFilter = (e) => {
            const target = e.target.closest('.Accordion-container')
            const arrow = target.querySelector('.Accordion-arrow')
            const items = target.querySelector('.Accordion-items')
            console.log(target)
            console.log(arrow, items)
    
            arrow.classList.toggle('change-arrow')
            items.classList.toggle('expand-filter')
    
        }
    
        const searchHotelsWithFilter = (querystring, value) => {
            console.log('search with filter', querystring, value)
            query = {...query, [querystring]: [...query[querystring] ?? [] , value]}
            console.log('query in filter function: ', query)
    
        }
    
        const searchHotels = () => {
            setQueryURL(query)
            console.log('seach ')
        }
    
        const AccordionList = () => {
            if(filters){
                const { neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility } = handleNullObj(filters)
                const filterTypes = [
                    {items: handleNullObj(neighbourhood).items, title: '위치 및 주변 지역', querystring: 'landmark_id'},
                    {items: handleNullObj(landmarks).items, title: '랜드마크', querystring: 'landmark_id'},
                    {items: handleNullObj(accommodationType).items, title: '숙박 시설 유형', querystring: 'accommodation_ids'},
                    {items: handleNullObj(facilities).items, title: '시설', querystring: 'amenity_ids'},
                    {items: handleNullObj(themesAndTypes).items, title: '테마/유형', querystring: 'theme_ids'},
                    {items: handleNullObj(accessibility).items, title: '장애인 편의 시설', querystring: 'amenity_ids'},
                ]
    
                return (
                    <div>{filterTypes.map( (filterType, id) => {
                        return (
                            <Accordion key={id} title={filterType.title} items={filterType.items} displayFilter={displayFilter} searchHotelsWithFilter={searchHotelsWithFilter} querystring={filterType.querystring}/>
                        )
                    })}</div>
                )
            }else{
                return <></>
            }
        }
    
        const getLocation = (hotel) => {
            const { name, address, coordinate } = handleNullObj(hotel)
            const { streetAddress, locality, countryName } = handleNullObj(address)
            const { lat, lon } = handleNullObj(coordinate)
            const msg = `${name}<br/>${streetAddress}, ${locality}, ${countryName}`
    
            return { lat, lon, msg }
        }
    
        return (
            <div className='Hotels-container'>
                <div className='Hotels-filtered'>
                    <AccordionList/>
                    <Button handleClick={searchHotels}>호텔 검색</Button>
                </div>
                <div className='Hotels-searched'>
                    <div id="map"></div>
                    {!isArrayNull(hotels) && hotels.map( hotel => {
                        const { lat, lon, msg } = getLocation(hotel)
                        displayLocation(lat, lon, msg)
                        return (
                            <HotelItem hotel={hotel} key={hotel.id}/>
                        )
                    })}
                </div>
            </div>
        )
    }
    
    export default Hotels

    Hotels.js 파일을 위와 같이 수정하자!

    import { HotelItem, Accordion, Button } from 'components'

    사용자가 선택한 필터 조건으로 호텔을 재검색하기 위하여 Button 컴포넌트를 추가로 임포트한다. 

    let query = {}

    사용자가 선택한 필터 조건들을 한꺼번에 기억하기 위한 변수이다.

     const BASE_URL = `https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`

    hotels.com API 서버에서 호텔 목록 데이터를 가져오기 위한 기본 URL 이다. 목적지 ID, 체크인과 체크아웃 날짜, 투숙인원이 기본적으로 들어간다. 

    const [queryURL, setQueryURL] = useState(null)

    queryURL 이라는 상태를 선언한다. queryURL 은 사용자가 선택한 모든 필터 조건들로 호텔을 재검색할때 기본 URL 에 덧붙여 새로운 URL 을 만드는데 사용된다. 

    const { results, filters } = await getHotels(BASE_URL)

    호텔 목록 페이지가 처음 보여질때 기본 URL 을 사용하여 검색한 호텔 목록 데이터를 가져온다. getHotels 함수에 데이터를 가져올 서버의 URL 인자를 전달하도록 변경하였다.

    useEffect( async () => {
        let url = BASE_URL
    
        for(let prop in queryURL){
            const queryvalue = encodeURIComponent(queryURL[prop].join(','))
            url += `&${prop}=${queryvalue}`
        }
        console.log('total url: ', url)
    
        const { results } = await getHotels(url)
        setHotels(results)
    }, [queryURL])

    또다른 Effect Hook 을 사용하였다. 해당 코드는 사용자가 필터조건을 적용하여 호텔을 재검색할때 실행되는 부분이다. Effect Hook 의 두번째 인자에 의하여 queryURL 상태가 변경된 경우에만 해당 코드가 실행된다. 

    let url = BASE_URL

    사용자가 선택한 필터 조건으로 새로운 URL 을 생성하기 위하여 기본 URL 을 참조한다.

    for(let prop in queryURL){
        const queryvalue = encodeURIComponent(queryURL[prop].join(','))
        url += `&${prop}=${queryvalue}`
    }

    반복문(for ~ in 문)을 사용하여 queryURL 객체로부터 모든 프로퍼티를 조회한다. prop 은 accommodation_ids, theme_ids, amenity_ids 와 같이 URL 에서 쿼리 파라미터를 의미한다.

    queryURL[prop] 은 배열이며, 특정 필터 카테고리에서 사용자가 선택한 필터 조건들에 대한 value 값들이 들어있다. 예를 들면, [25, 3, 30] 과 같다. 해당 배열의 value 값들은 URL 의 쿼리 파라미터 값으로 추가하기 위하여 문자열의 join 함수를 사용하여 콤마로 구분된 문자열로 변경해준다. 그러면 '25, 3, 30' 과 같다. 

    하지만 아래에서 살펴보면 쿼리 파라미터에 대한 값에 %2C 와 같은 문자열이 보인다. 이는 필터조건들이 20, 8, 15, 5, 1 과 같을때 서버로 보내기 전 URL 을 인코딩한 것이다. 즉, %2C 는 콤마(,) 를 인코딩한 것이다. 이렇게 URL 을 인코딩하기 위하여 자바스크립트에서 제공하는 encodeURIComponent 를 사용하였다.  

    url += `&${prop}=${queryvalue}`

    & 기호를 사용하여 기본 URL 에 필터가 적용된 새로운 URL 을 생성한다. 

    const { results } = await getHotels(url)
    setHotels(results)

    필터가 적용된 새로운 URL 을 이용하여 호텔 목록을 새로 검색한다. 그런 다음 서버로부터 가져온 결과인 results 값을 사용하여 hotels 상태를 업데이트한다.

    const getHotels = async (url) => {
        // const data = await fetchHotelsCom(url)
        // console.log(data)
        // const {searchResults: {results}, filters } = data
    
        const {searchResults: {results}, filters } = hotelsData
        return { results, filters }
    }

    getHotels 함수에 접속할 서버 URL 을 인자로 전달하도록 수정한다. 

    const searchHotelsWithFilter = (querystring, value) => {
        console.log('search with filter', querystring, value)
        query = {...query, [querystring]: [...query[querystring] ?? [] , value]}
        console.log('query in filter function: ', query)
    }

    사용자가 필터를 선택할때마다 실행되는 함수이다. 

    query = {...query, [querystring]: [...query[querystring] ?? [] , value]}

    query 는 사용자가 선택한 필터조건들에 대한 정보를 담고 있는 객체이다. 사용자가 필터를 추가할때마다 query 객체를 변경한다. 스프레드 연산자(...)을 이용하여 기존 객체에서 특정 프로퍼티 값만 변경한다. querystring 은 문자열이므로 프로퍼티를 설정할때 대괄호([])를 사용해야 한다. 

    querystring 은 앞에서도 언급했듯이 사용자가 선택한 필터조건에 대한 URL 쿼리 파라미터를 의미한다. 예를 들면, accommodation_ids, theme_ids, amenity_ids 와 같다. query[querystring] 은 필터 조건들에 대한 배열이다. 예를 들어 [12, 3, 30] 과 같은 필터조건들에 대한 value 값이다. 마찬가지로 스프레드 연산자(...)를 사용하여 기존 배열에 사용자가 선택한 새로운 필터조건에 대한 value 값을 추가한다.

    const searchHotels = () => {
        setQueryURL(query)
        console.log('seach ')
    }

    호텔 목록 페이지에서 [호텔 검색] 버튼을 클릭하면 실행되는 이벤트핸들러 함수이다. 사용자가 선택한 모든 필터조건들이 저장된 query 값으로 queryURL 상태를 업데이트한다. 

    const filterTypes = [
        {items: handleNullObj(neighbourhood).items, title: '위치 및 주변 지역', querystring: 'landmark_id'},
        {items: handleNullObj(landmarks).items, title: '랜드마크', querystring: 'landmark_id'},
        {items: handleNullObj(accommodationType).items, title: '숙박 시설 유형', querystring: 'accommodation_ids'},
        {items: handleNullObj(facilities).items, title: '시설', querystring: 'amenity_ids'},
        {items: handleNullObj(themesAndTypes).items, title: '테마/유형', querystring: 'theme_ids'},
        {items: handleNullObj(accessibility).items, title: '장애인 편의 시설', querystring: 'amenity_ids'},
    ]

    filterTypes 은 필터 카테고리와 필터조건들에 대한 정보를 담고 있는 배열이다. 해당 배열요소는 객체 형태이며, 객체의 프로퍼티에 querystring 이 추가되었다. 

    <Accordion key={id} 
    	   title={filterType.title} 
               items={filterType.items} 
               displayFilter={displayFilter} 
               searchHotelsWithFilter={searchHotelsWithFilter} 
               querystring={filterType.querystring}
    />

    Accordion 컴포넌트에 searchHotelsWithFilter 함수와 querystring 문자열이라는 props 를 추가하였다. 

    <Button handleClick={searchHotels}>호텔 검색</Button>

    사용자가 선택한 필터로 호텔을 다시 검색하기 위하여 검색 버튼을 추가한다. 

     

    hotels.com 페이지에서 게스트하우스 필터로 호텔을 검색한 화면

     

    hotels.com 클론 페이지에서 게스트하우스 필터로 호텔을 검색한 화면

     

    이제 숙박 시설 등급에 대한 필터 기능을 추가해보자!

    components 폴더에 아래 파일들을 추가한다.

    import React from 'react'
    import './StarRatingFilter.css'
    
    import { isArrayNull } from 'lib'
    
    const StarRatingFilter = ({ title, items, searchHotelsWithFilter, querystring }) => {
        const reversedItems = items.sort((a, b) => {
            return a.value - b.value;
          })
        
        return (
            <div className='StarRatingFilter-container'>
                <div className='StarRatingFilter-title'>{title}</div>
                <div className='StarRatingFilter-btns'>
                    {!isArrayNull(reversedItems) && reversedItems.map( (item, idx) => {
                        return (
                            <div className='StarRatingFilter-rating' key={item.value} onClick={() => searchHotelsWithFilter(querystring, item.value)}>{item.value}</div>
                        )
                    })}
                </div>
            </div>
        )
    }
    
    export default StarRatingFilter

    StarRatingFilter.js 파일을 위와 같이 생성하자! Accordion 컴포넌트와 형태는 거의 동일하다. 다만 숙박 등급에 대한 필터 목록 (items) 가 거꾸로 되어 있어서 아래와 같은 코드로 오름차순으로 정렬하였다. 

    const reversedItems = items.sort((a, b) => {
      return a.value - b.value;
    })

    숙박 시설 등급에 대한 필터 목록을 오름차순으로 정렬한다.

    .StarRatingFilter-container{
        border-top: 1px solid lightgray;
        margin-bottom: 15px;
    }
    .StarRatingFilter-title{
        font-size: 1.2rem;
        font-weight: bold;
        margin-top: 20px;
        margin-bottom: 20px;
        color: #1a1c1b;
    }
    .StarRatingFilter-btns{
        display: flex;
        justify-content: center;
        align-items: center;
    }
    .StarRatingFilter-rating{
        width: 100px;
        height: 40px;
        line-height: 40px;
        text-align: center;
        margin-right: 10px;
        font-size: 1.2rem;
    
        border-radius: 20px;
        cursor: pointer;
        user-select: none;
        box-sizing: border-box;
        color: #125ca5;
        border: 1px solid #125ca5;
    }

    StarRatingFilter.css 파일을 위와 같이 생성하자!

    export { default as Input } from './Input'
    export { default as Button } from './Button'
    export { default as Caption } from './Caption'
    export { default as HotelItem } from './HotelItem'
    export { default as Accordion } from './Accordion'
    export { default as AccordionItem } from './AccordionItem'
    export { default as StarRatingFilter } from './StarRatingFilter'

    components > index.js 파일에 StarRatingFilter 컴포넌트를 추가로 내보낸다. 

    import React, { useState, useEffect } from 'react'
    import { useLocation } from 'react-router-dom'
    
    import { fetchHotelsCom, isArrayNull, handleNullObj } from 'lib'
    import hotelsData from '../hotelsData'
    import { HotelItem, Accordion, Button, StarRatingFilter } from 'components'
    
    import './Hotels.css'
    
    const Hotels = () => {
        let query = {}
        const location = useLocation()
        const { destinationId, checkIn, checkOut, adultsNumber } = handleNullObj(location.state)
        console.log(destinationId, checkIn, checkOut, adultsNumber)
    
        const BASE_URL = `https://hotels-com-provider.p.rapidapi.com/v1/hotels/search?checkin_date=${checkIn}&checkout_date=${checkOut}&sort_order=STAR_RATING_HIGHEST_FIRST&destination_id=${destinationId}&adults_number=${adultsNumber}&locale=ko_KR&currency=KRW`
    
        const [hotels, setHotels] = useState([])
        const [mapObj, setMapObj] = useState(null)
        const [filters, setFilters] = useState(null)
        const [queryURL, setQueryURL] = useState(null)
    
        useEffect( async () => {
            console.log(BASE_URL)
            // 새로고침해도 필터값이 적용되려면 초기 렌더링시에는 BASE_URL 을 적용하고 queryURL 상태가 null 이 아니면 queryURL 을 적용하면 되지 않을까?
            const { results, filters } = await getHotels(BASE_URL)
            setHotels(results)
            setFilters(filters)
    
            const m = L.map('map')
            setMapObj(m)
        }, [])
    
        useEffect( async () => {
            console.log('query: ', queryURL)
            console.log(BASE_URL)
            let url = BASE_URL
            
            for(let prop in queryURL){
                const queryvalue = encodeURIComponent(queryURL[prop].join(','))
                url += `&${prop}=${queryvalue}`
                console.log(prop, queryvalue)
            }
            console.log('total url: ', url)
    
            const { results } = await getHotels(url)
            setHotels(results)
        }, [queryURL])
    
        const getHotels = async (url) => {
            // const data = await fetchHotelsCom(url)
            // console.log(data)
            // const {searchResults: {results}, filters } = data
    
            const {searchResults: {results}, filters } = hotelsData
            
            return { results, filters }
        }
    
        
        const displayLocation = (lat, lon, msg) => {
            // console.log('inside:', mapObj)
    
            if(mapObj){
                const map = mapObj.setView([lat, lon], 13)
    
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                }).addTo(map)
    
                L.marker([lat, lon]).addTo(map)
                    .bindPopup(msg)
                    .openPopup()
            }
        }
    
        const displayFilter = (e) => {
            const target = e.target.closest('.Accordion-container')
            const arrow = target.querySelector('.Accordion-arrow')
            const items = target.querySelector('.Accordion-items')
            console.log(target)
            console.log(arrow, items)
    
            arrow.classList.toggle('change-arrow')
            items.classList.toggle('expand-filter')
    
        }
    
        const searchHotelsWithFilter = (querystring, value) => {
            console.log('search with filter', querystring, value)
            query = {...query, [querystring]: [...query[querystring] ?? [] , value]}
            console.log('query in filter function: ', query)
    
        }
    
        const searchHotels = () => {
            setQueryURL(query)
            console.log('seach ')
        }
    
        const FilterList = () => {
            if(filters){
                const { neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility, starRating } = handleNullObj(filters)
                const starRatingTypes = {items: handleNullObj(starRating).items, title: '숙박 시설 등급', querystring: 'star_rating_ids'}
                const filterTypes = [
                    {items: handleNullObj(neighbourhood).items, title: '위치 및 주변 지역', querystring: 'landmark_id'},
                    {items: handleNullObj(landmarks).items, title: '랜드마크', querystring: 'landmark_id'},
                    {items: handleNullObj(accommodationType).items, title: '숙박 시설 유형', querystring: 'accommodation_ids'},
                    {items: handleNullObj(facilities).items, title: '시설', querystring: 'amenity_ids'},
                    {items: handleNullObj(themesAndTypes).items, title: '테마/유형', querystring: 'theme_ids'},
                    {items: handleNullObj(accessibility).items, title: '장애인 편의 시설', querystring: 'amenity_ids'},
                ]
    
                return (
                    <>
                        <StarRatingFilter title={starRatingTypes.title} items={starRatingTypes.items} searchHotelsWithFilter={searchHotelsWithFilter} querystring={starRatingTypes.querystring}/>
                        <div>{filterTypes.map( (filterType, id) => {
                            return (
                                <Accordion key={id} title={filterType.title} items={filterType.items} displayFilter={displayFilter} searchHotelsWithFilter={searchHotelsWithFilter} querystring={filterType.querystring}/>
                            )
                    })}</div>
                    </>
                )
            }else{
                return <></>
            }
        }
    
        const getLocation = (hotel) => {
            const { name, address, coordinate } = handleNullObj(hotel)
            const { streetAddress, locality, countryName } = handleNullObj(address)
            const { lat, lon } = handleNullObj(coordinate)
            const msg = `${name}<br/>${streetAddress}, ${locality}, ${countryName}`
    
            return { lat, lon, msg }
        }
    
        return (
            <div className='Hotels-container'>
                <div className='Hotels-filtered'>
                    <FilterList/>
                    <Button handleClick={searchHotels}>호텔 검색</Button>
                </div>
                <div className='Hotels-searched'>
                    <div id="map"></div>
                    {!isArrayNull(hotels) && hotels.map( hotel => {
                        const { lat, lon, msg } = getLocation(hotel)
                        displayLocation(lat, lon, msg)
                        return (
                            <HotelItem hotel={hotel} key={hotel.id}/>
                        )
                    })}
                </div>
            </div>
        )
    }
    
    export default Hotels

    Hotels.js 파일을 위와 같이 수정하자!

    import { HotelItem, Accordion, Button, StarRatingFilter } from 'components'

    숙박 시설 등급에 대한 필터 목록을 보여주기 위하여 StarRatingFilter 컴포넌트를 추가로 임포트한다. 

    const FilterList = () => {
            if(filters){
                const { neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility, starRating } = handleNullObj(filters)
                const starRatingTypes = {items: handleNullObj(starRating).items, title: '숙박 시설 등급', querystring: 'star_rating_ids'}
                const filterTypes = [
                    {items: handleNullObj(neighbourhood).items, title: '위치 및 주변 지역', querystring: 'landmark_id'},
                    {items: handleNullObj(landmarks).items, title: '랜드마크', querystring: 'landmark_id'},
                    {items: handleNullObj(accommodationType).items, title: '숙박 시설 유형', querystring: 'accommodation_ids'},
                    {items: handleNullObj(facilities).items, title: '시설', querystring: 'amenity_ids'},
                    {items: handleNullObj(themesAndTypes).items, title: '테마/유형', querystring: 'theme_ids'},
                    {items: handleNullObj(accessibility).items, title: '장애인 편의 시설', querystring: 'amenity_ids'},
                ]
    
                return (
                    <>
                        <StarRatingFilter title={starRatingTypes.title} items={starRatingTypes.items} searchHotelsWithFilter={searchHotelsWithFilter} querystring={starRatingTypes.querystring}/>
                        <div>{filterTypes.map( (filterType, id) => {
                            return (
                                <Accordion key={id} title={filterType.title} items={filterType.items} displayFilter={displayFilter} searchHotelsWithFilter={searchHotelsWithFilter} querystring={filterType.querystring}/>
                            )
                    })}</div>
                    </>
                )
            }else{
                return <></>
            }
        }

    AccordionList 컴포넌트를 FilterList 라는 이름으로 변경하였다. 

    const { neighbourhood, landmarks, accommodationType, facilities, themesAndTypes, accessibility, starRating } = handleNullObj(filters)
    const starRatingTypes = {items: handleNullObj(starRating).items, title: '숙박 시설 등급', querystring: 'star_rating_ids'}

    filters 데이터에서 starRating 프로퍼티를 추가로 조회한다. starRating 데이터를 이용하여 데이터를 재가공한다. starRatingTypes 객체는 해당 필터 카테고리에 대한 title 과 querystring 프로퍼티를 가지고 있다. 

    <StarRatingFilter title={starRatingTypes.title} items={starRatingTypes.items} searchHotelsWithFilter={searchHotelsWithFilter} querystring={starRatingTypes.querystring}/>

    숙박 시설 등급에 대한 필터 카테고리를 화면에 보여준다. 

    <FilterList/>

    AccordionList 컴포넌트 대신에 FilterList 컴포넌트를 렌더링한다. 

    숙박 시설 등급 필터가 적용된 모습

     

    728x90
Designed by Tistory.