1. 시작하기 전에
이 Codelab에서는 Google Maps Platform 장소 UI 키트를 사용하여 완전한 대화형 지역 검색 애플리케이션을 빌드하는 방법을 알아봅니다.
기본 요건
- 필요한 API와 사용자 인증 정보가 구성된 Google Cloud 프로젝트
- HTML 및 CSS에 대한 기본 지식
- 최신 JavaScript에 대한 이해
- 최신 버전의 Chrome과 같은 최신 웹브라우저
- 원하는 텍스트 편집기
실습할 내용
- JavaScript 클래스를 사용하여 매핑 애플리케이션을 구조화합니다.
- 웹 구성요소를 사용하여 지도 표시
- 장소 검색 요소를 사용하여 텍스트 검색을 실행하고 결과를 표시합니다.
- 프로그래매틱 방식으로 맞춤
AdvancedMarkerElement
지도 마커를 만들고 관리합니다. - 사용자가 위치를 선택하면 장소 세부정보 요소를 표시합니다.
- Geocoding API를 사용하여 동적이고 사용자 친화적인 인터페이스를 만드세요.
필요한 항목
- 결제가 사용 설정된 Google Cloud 프로젝트
- Google Maps Platform API 키
- 지도 ID
- 다음 API가 사용 설정됩니다.
- Maps JavaScript API
- 장소 UI 키트
- Geocoding API
2. 설정
다음 사용 설정 단계에서는 Maps JavaScript API, Places UI 키트, Geocoding API를 사용 설정해야 합니다.
Google Maps Platform 설정하기
Google Cloud Platform 계정 및 결제가 사용 설정된 프로젝트가 없는 경우 Google Maps Platform 시작하기 가이드를 참고하여 결제 계정 및 프로젝트를 만듭니다.
- Cloud Console에서 프로젝트 드롭다운 메뉴를 클릭하고 이 Codelab에 사용할 프로젝트를 선택합니다.
3. 애플리케이션 셸 및 기능 지도
이 첫 번째 단계에서는 애플리케이션의 전체 시각적 레이아웃을 만들고 JavaScript의 깔끔한 클래스 기반 구조를 설정합니다. 이를 통해 탄탄한 기반을 구축할 수 있습니다. 이 섹션이 끝나면 대화형 지도를 표시하는 스타일이 지정된 페이지가 완성됩니다.
HTML 파일 만들기
먼저 index.html
이라는 파일을 만듭니다. 이 파일에는 헤더, 검색 필터, 사이드바, 지도 컨테이너, 필요한 웹 구성요소를 비롯한 애플리케이션의 전체 구조가 포함됩니다.
다음 코드를 index.html
에 복사합니다. YOUR_API_KEY_HERE
를 자체 Google Maps Platform API 키로, DEMO_MAP_ID
를 자체 Google Maps Platform 지도 ID로 바꿔야 합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Local Search App</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Google Fonts: Roboto -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<!-- GMP Bootstrap Loader -->
<script>
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "YOUR_API_KEY_HERE",
v: "weekly",
libraries: "places,maps,marker,geocoding"
});
</script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<!-- Header for search controls -->
<header class="top-header">
<div class="logo">
<svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"></path></svg>
<span>PlaceFinder</span>
</div>
<div class="search-container">
<input
type="text"
id="query-input"
placeholder="e.g., burger in New York"
value="burger"
/>
<button id="search-button" aria-label="Search">Search</button>
</div>
<div class="filter-container">
<label class="open-now-label">
<input type="checkbox" id="open-now-filter"> Open Now
</label>
<select id="rating-filter" aria-label="Minimum rating">
<option value="0" selected>Any rating</option>
<option value="1">1+ ★</option>
<option value="2">2+ ★★</option>
<option value="3">3+ ★★★</option>
<option value="4">4+ ★★★★</option>
<option value="5">5 ★★★★★</option>
</select>
<select id="price-filter" aria-label="Price level">
<option value="0" selected>Any Price</option>
<option value="1">$</option>
<option value="2">$$</option>
<option value="3">$$$</option>
<option value="4">$$$$</option>
</select>
</div>
</header>
<!-- Main content area -->
<div class="app-container">
<!-- Left Panel: Results -->
<div class="sidebar">
<div class="results-header">
<h2 id="results-header-text">Results</h2>
</div>
<div class="results-container">
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
<div id="placeholder-message" class="placeholder">
<p>Your search results will appear here.</p>
</div>
<div id="loading-spinner" class="spinner-overlay">
<div class="spinner"></div>
</div>
</div>
</div>
<!-- Right Panel: Map -->
<div class="map-container">
<gmp-map
center="40.758896,-73.985130"
zoom="13"
map-id="DEMO_MAP_ID"
>
</gmp-map>
<div id="details-container">
<gmp-place-details-compact>
<gmp-place-details-place-request></gmp-place-details-place-request>
<gmp-place-all-content></gmp-place-all-content>
</gmp-place-details-compact>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
CSS 파일 만들기
그런 다음 style.css
파일을 만듭니다. 이제 처음부터 깔끔하고 현대적인 디자인을 만들기 위해 필요한 스타일을 모두 추가하겠습니다. 이 CSS는 전반적인 레이아웃, 색상, 글꼴, 모든 UI 요소의 모양을 처리합니다.
다음 코드를 style.css
에 복사합니다.
/* style.css */
:root {
--primary-color: #1a73e8;
--text-color: #202124;
--text-color-light: #5f6368;
--background-color: #f8f9fa;
--panel-background: #ffffff;
--border-color: #dadce0;
--shadow-color: rgba(0, 0, 0, 0.1);
}
body {
font-family: 'Roboto', sans-serif;
margin: 0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: var(--background-color);
color: var(--text-color);
}
.hidden {
display: none !important;
}
.top-header {
display: flex;
align-items: center;
padding: 12px 24px;
border-bottom: 1px solid var(--border-color);
background-color: var(--panel-background);
gap: 24px;
flex-shrink: 0;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 22px;
font-weight: 700;
color: var(--primary-color);
}
.search-container {
display: flex;
flex-grow: 1;
max-width: 720px;
}
.search-container input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 8px 0 0 8px;
font-size: 16px;
transition: box-shadow 0.2s ease;
}
.search-container input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}
.search-container button {
padding: 0 20px;
border: 1px solid var(--primary-color);
border-radius: 0 8px 8px 0;
background-color: var(--primary-color);
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.2s ease;
}
.search-container button:hover {
background-color: #185abc;
}
.filter-container {
display: flex;
gap: 12px;
align-items: center;
}
.filter-container select, .open-now-label {
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--panel-background);
font-size: 14px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.filter-container select:hover, .open-now-label:hover {
border-color: #c0c2c5;
}
.open-now-label {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.app-container {
display: flex;
flex-grow: 1;
overflow: hidden;
}
.sidebar {
width: 35%;
min-width: 380px;
max-width: 480px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
background-color: var(--panel-background);
overflow: hidden;
}
.results-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.results-header h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.results-container {
flex-grow: 1;
position: relative;
overflow-y: auto;
overflow-x: hidden;
}
.placeholder {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem;
box-sizing: border-box;
}
.placeholder p {
color: var(--text-color-light);
font-size: 1.1rem;
}
gmp-place-search {
width: 100%;
}
.map-container {
flex-grow: 1;
position: relative;
}
gmp-map {
width: 100%;
height: 100%;
}
.spinner-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.spinner-overlay.visible {
opacity: 1;
visibility: visible;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #e0e0e0;
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
gmp-place-details-compact {
width: 350px;
display: none;
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
gmp-place-details-compact::after {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 12px;
background-color: var(--panel-background);
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
JavaScript 애플리케이션 클래스 만들기
마지막으로 script.js
이라는 파일을 만듭니다. PlaceFinderApp
이라는 JavaScript 클래스 내에 애플리케이션을 구성합니다. 이렇게 하면 코드가 체계적으로 유지되고 상태가 깔끔하게 관리됩니다.
이 초기 코드는 클래스를 정의하고, constructor
에서 모든 HTML 요소를 찾고, Google Maps Platform 라이브러리를 로드하는 init()
메서드를 만듭니다.
다음 코드를 script.js
에 복사합니다.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// We will add more initialization logic here in later steps.
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
API 키 제한사항
이 Codelab이 작동하려면 API 키에 새 제한사항을 추가해야 할 수 있습니다. 자세한 내용과 방법을 알아보려면 API 키 제한을 참고하세요.
학습 내용 확인
웹브라우저에서 index.html
파일을 엽니다. 검색창과 필터가 포함된 헤더, '검색 결과가 여기에 표시됩니다'라는 메시지가 표시된 사이드바, 뉴욕시를 중심으로 한 대형 지도가 표시된 페이지가 표시됩니다. 이 단계에서는 검색 컨트롤이 아직 작동하지 않습니다.
4. 검색 기능 구현
이 섹션에서는 핵심 검색 기능을 구현하여 애플리케이션을 활성화합니다. 사용자가 '검색' 버튼을 클릭할 때 실행되는 코드를 작성합니다. Google에서는 사용자 상호작용을 원활하게 처리하고 경합 상태와 같은 일반적인 버그를 방지하기 위해 처음부터 권장사항을 적용하여 이 함수를 빌드할 예정입니다.
이 단계를 완료하면 검색 버튼을 클릭할 수 있으며 애플리케이션이 백그라운드에서 데이터를 가져오는 동안 로딩 스피너가 표시됩니다.
검색 메서드 만들기
먼저 PlaceFinderApp
클래스 내에서 performSearch
메서드를 정의합니다. 이 함수는 검색 로직의 핵심이 됩니다. '게이트키퍼' 역할을 하는 인스턴스 변수 isSearchInProgress
도 도입합니다. 이렇게 하면 사용자가 검색이 진행 중인 동안 새 검색을 시작할 수 없으므로 오류가 발생하지 않습니다.
performSearch
내부의 로직이 복잡해 보일 수 있으므로 이를 분석해 보겠습니다.
- 먼저 검색이 이미 진행 중인지 확인합니다. 그렇다면 아무 작업도 실행되지 않습니다.
isSearchInProgress
플래그를true
로 설정하여 함수를 '잠급니다'.- 로딩 스피너를 표시하고 새 결과를 위해 UI를 준비합니다.
- 검색 요청의
textQuery
속성을null
로 설정합니다. 이는 웹 구성요소가 새 요청이 들어오고 있음을 인식하도록 하는 중요한 단계입니다. 0
지연이 있는setTimeout
를 사용합니다. 이 표준 JavaScript 기법은 구성요소가null
값을 먼저 처리하도록 하여 나머지 코드가 다음 브라우저 작업에서 실행되도록 예약합니다. 사용자가 동일한 내용을 두 번 검색하더라도 항상 새로운 검색이 트리거됩니다.
이벤트 리스너 추가
다음으로 사용자가 앱과 상호작용할 때 performSearch
메서드를 호출해야 합니다. 모든 이벤트 처리 코드를 한곳에 유지하기 위해 새 메서드 attachEventListeners
를 만듭니다. 지금은 검색 버튼의 click
이벤트에 대한 리스너만 추가합니다. 다음 단계에서 사용할 다른 이벤트 gmp-load
의 자리표시자도 추가합니다.
JavaScript 파일 업데이트
다음 코드를 사용하여 script.js
파일을 업데이트합니다. 새로 추가되거나 변경된 섹션은 attachEventListeners
메서드와 performSearch
메서드입니다.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// Call the new method to set up listeners
this.attachEventListeners();
}
// NEW: Method to set up all event listeners
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// We will add the gmp-load listener in the next step
}
// NEW: Core search method
async performSearch() {
// Exit if a search is already in progress
if (this.isSearchInProgress) {
return;
}
// Set the lock
this.isSearchInProgress = true;
// Show the placeholder and spinner
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// Force a state change by clearing the query first.
this.searchRequest.textQuery = null;
// Defer setting the real properties to the next event loop cycle.
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
// If the query is empty, release the lock and hide the spinner
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// For now, we just set the textQuery. We'll add filters later.
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
// NEW: Helper method to show/hide the spinner
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
학습 내용 확인
script.js
파일을 저장하고 브라우저에서 index.html
을 새로고침합니다. 페이지는 이전과 동일하게 표시됩니다. 이제 헤더에서 '검색' 버튼을 클릭합니다.
다음 두 가지가 표시됩니다.
- '검색 결과가 여기에 표시됩니다'라는 자리표시자 메시지가 사라집니다.
- 로딩 스피너가 표시되고 계속 회전합니다.
언제 중지해야 하는지 아직 알려주지 않았으므로 스피너가 계속 회전합니다. 결과를 표시할 때 다음 섹션에서 이 작업을 수행합니다. 이렇게 하면 검색 기능이 올바르게 트리거되었는지 확인할 수 있습니다.
5. 결과 표시 및 마커 추가
이제 검색 트리거가 작동하므로 다음 작업은 화면에 결과를 표시하는 것입니다. 이 섹션의 코드는 검색 로직을 UI에 연결합니다. 장소 검색 요소가 데이터 로드를 완료하면 검색 '잠금'이 해제되고 로드 스피너가 숨겨지며 각 결과에 대한 마커가 지도에 표시됩니다.
검색 완료 리스너
장소 검색 요소는 데이터를 성공적으로 가져오면 gmp-load
이벤트를 발생시킵니다. 결과를 처리하기에 완벽한 신호입니다.
먼저 attachEventListeners
메서드에 이 이벤트의 이벤트 리스너를 추가합니다.
마커 처리 메서드 만들기
다음으로 clearMarkers
및 addMarkers
이라는 두 개의 새로운 도우미 메서드를 만듭니다.
clearMarkers()
는 이전 검색의 마커를 삭제합니다.addMarkers()
는gmp-load
리스너에 의해 호출됩니다. 검색에서 반환된 장소 목록을 반복하고 각 장소에 대해 새AdvancedMarkerElement
를 만듭니다. 여기에서 로드 스피너를 숨기고isSearchInProgress
잠금을 해제하여 검색 주기를 완료합니다.
지역 ID를 키로 사용하여 객체 (this.markers
)에 마커를 저장합니다. 이렇게 하면 마커를 관리할 수 있으며 나중에 특정 마커를 찾을 수 있습니다.
마지막으로, 새 검색이 시작될 때마다 clearMarkers()
을 호출해야 합니다. 이 코드는 performSearch
내부에 배치하는 것이 가장 좋습니다.
JavaScript 파일 업데이트
새 메서드와 attachEventListeners
및 performSearch
변경사항으로 script.js
파일을 업데이트합니다.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
attachEventListeners() {
this.searchButton.addEventListener('click', this.performSearch.bind(this));
// NEW: Listen for when the search component has loaded results
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
}
// NEW: Method to clear markers from a previous search
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// NEW: Method to add markers for new search results
addMarkers() {
// Release the lock and hide the spinner
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
// Create a new marker for each place result
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Store marker by its place ID for access later
this.markers[place.id] = marker;
}
}
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
// NEW: Clear old markers before starting a new search
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
학습 내용 확인
파일을 저장하고 브라우저에서 페이지를 새로고침합니다. '검색' 버튼을 클릭합니다.
이제 로딩 스피너가 잠시 표시되었다가 사라집니다. 사이드바에 검색어와 관련된 장소 목록이 표시되고 지도에 해당 마커가 표시됩니다. 아직 마커를 클릭해도 아무 작업도 실행되지 않습니다. 다음 섹션에서 상호작용을 추가할 예정입니다.
6. 검색 필터 및 목록 상호작용 활성화
이제 애플리케이션에서 검색 결과를 표시할 수 있지만 아직 대화형은 아닙니다. 이 섹션에서는 모든 사용자 컨트롤을 구현합니다. 필터가 활성화되고, 'Enter' 키로 검색이 가능해지며, 결과 목록의 항목이 지도상의 해당 위치에 연결됩니다.
이 단계를 마치면 애플리케이션이 사용자 입력에 완전히 반응하는 것처럼 느껴질 것입니다.
검색 필터 활성화
먼저 performSearch
메서드가 헤더의 모든 필터 컨트롤에서 값을 읽도록 업데이트됩니다. 각 필터 (가격, 평점, '지금 영업 중')의 경우 검색이 실행되기 전에 searchRequest
객체에 해당 속성이 설정됩니다.
모든 컨트롤에 이벤트 리스너 추가
다음으로 attachEventListeners
메서드를 확장합니다. 각 필터 컨트롤에 change
이벤트 리스너를 추가하고 검색 입력에 keydown
리스너를 추가하여 사용자가 'Enter' 키를 누르는 시점을 감지합니다. 이러한 새 리스너는 모두 performSearch
메서드를 호출합니다.
결과 목록을 지도에 연결
원활한 환경을 만들려면 사이드바 목록에서 항목을 클릭하면 해당 위치에 지도가 포커스되어야 합니다.
새 메서드인 handleResultClick
는 항목을 클릭할 때 장소 검색 요소에서 발생하는 gmp-select
이벤트를 수신 대기합니다. 이 함수는 연결된 장소의 위치를 찾아 지도를 해당 위치로 부드럽게 이동합니다.
이 기능이 작동하려면 index.html
의 gmp-place-search
구성요소에 selectable
속성이 있어야 합니다.
<gmp-place-search id="place-search-list" class="hidden" selectable>
<gmp-place-all-content></gmp-place-all-content>
<gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>
JavaScript 파일 업데이트
다음 전체 코드를 사용하여 script.js
파일을 업데이트합니다. 이 버전에는 새로운 handleResultClick
메서드와 attachEventListeners
및 performSearch
의 업데이트된 로직이 포함되어 있습니다.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.attachEventListeners();
}
// UPDATED: All event listeners are now attached
attachEventListeners() {
// Listen for the 'Enter' key press in the search input
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
// Listen for a sidebar result click
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
this.markers[place.id] = marker;
}
}
// NEW: Function to handle clicks on the results list
handleResultClick(event) {
const place = event.place;
if (!place || !place.location) return;
// Pan the map to the selected place
this.map.panTo(place.location);
}
// UPDATED: Search function now includes all filters
async performSearch() {
if (this.isSearchInProgress) {
return;
}
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
// Add filter values to the request
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
학습 내용 확인
script.js
파일을 저장하고 페이지를 새로고침합니다. 이제 애플리케이션이 매우 상호작용적이어야 합니다.
다음을 확인합니다.
- 검색창에서 'Enter' 키를 눌러 검색하는 것은 작동합니다.
- 필터 (가격, 평점, 영업 중)를 변경하면 새 검색이 트리거되고 결과가 업데이트됩니다.
- 이제 사이드바 목록에서 항목을 클릭하면 지도가 해당 항목의 위치로 부드럽게 이동합니다.
다음 섹션에서는 마커를 클릭할 때 표시되는 세부정보 카드를 구현합니다.
7. 장소 세부정보 요소 구현
이제 애플리케이션이 완전히 대화형이지만 선택한 장소에 관한 자세한 정보를 볼 수 있는 기능이 누락되어 있습니다. 이 섹션에서는 사용자가 지도에서 마커를 클릭하거나 장소 검색 요소에서 항목을 선택할 때 표시되는 장소 세부정보 요소를 구현합니다.
재사용 가능한 세부정보 카드 컨테이너 만들기
지도에 장소 세부정보를 표시하는 가장 효율적인 방법은 재사용 가능한 단일 컨테이너를 만드는 것입니다. 이 컨테이너로는 AdvancedMarkerElement
를 사용합니다. 콘텐츠는 index.html
에 이미 있는 숨겨진 gmp-place-details-compact
위젯입니다.
새 메서드인 initDetailsPopup
가 이 재사용 가능한 마커의 생성을 처리합니다. 애플리케이션이 로드될 때 한 번 생성되며 숨겨진 상태로 시작됩니다. 또한 이 메서드에서 기본 지도에 리스너를 추가하여 지도의 아무 곳이나 클릭하면 세부정보 카드가 숨겨지도록 합니다.
마커 클릭 동작 업데이트
다음으로 사용자가 장소 마커를 클릭할 때 발생하는 결과를 업데이트해야 합니다. 이제 addMarkers
메서드 내의 'click'
리스너가 세부정보 카드를 표시합니다.
마커를 클릭하면 리스너는 다음 작업을 실행합니다.
- 지도를 마커 위치로 이동합니다.
- 세부정보 카드를 해당 특정 장소의 정보로 업데이트합니다.
- 세부정보 카드를 마커의 위치에 배치하고 표시합니다.
목록 클릭을 마커 클릭에 연결
마지막으로 handleResultClick
메서드를 업데이트합니다. 이제 지도를 이동하는 대신 해당 마커에서 click
이벤트가 프로그래매틱 방식으로 트리거됩니다. 이는 두 상호작용 모두에 정확히 동일한 로직을 재사용하여 코드를 깔끔하고 유지관리 가능하게 유지할 수 있는 강력한 패턴입니다.
JavaScript 파일 업데이트
다음 코드를 사용하여 script.js
파일을 업데이트합니다. 새로 추가되거나 변경된 섹션은 initDetailsPopup
메서드와 업데이트된 addMarkers
및 handleResultClick
메서드입니다.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
// NEW: Call the method to initialize the details card
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
// NEW: Method to set up the reusable details card
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
clearMarkers() {
for (const marker of Object.values(this.markers)) {
marker.map = null;
}
this.markers = {};
}
// UPDATED: The marker's click listener now shows the details card
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
// Add the click listener to show the details card
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
// UPDATED: This now triggers the marker's click event
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
// Programmatically trigger the marker's click event
marker.click();
}
}
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
// Hide the details card when a new search starts
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
this.searchRequest.textQuery = rawQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
학습 내용 확인
script.js
파일을 저장하고 페이지를 새로고침합니다. 이제 애플리케이션에서 요청 시 세부정보를 표시해야 합니다.
다음을 확인합니다.
- 이제 지도에서 마커를 클릭하면 지도가 중앙에 배치되고 마커 위에 스타일이 지정된 세부정보 카드가 열립니다.
- 사이드바 결과 목록에서 항목을 클릭해도 마찬가지입니다.
- 카드가 아닌 지도를 클릭하면 카드가 닫힙니다.
- 새 검색을 시작하면 열려 있는 세부정보 카드도 닫힙니다.
8. 마무리 작업 추가
이제 애플리케이션이 완전히 작동하지만 사용자 환경을 더욱 개선하기 위해 몇 가지 최종 터치를 추가할 수 있습니다. 이 마지막 섹션에서는 검색 결과에 대한 더 나은 컨텍스트를 제공하는 동적 헤더와 사용자의 검색어에 대한 자동 서식 지정이라는 두 가지 주요 기능을 구현합니다.
동적 결과 헤더 만들기
현재 사이드바 헤더에는 항상 '결과'라고 표시됩니다. 현재 검색을 반영하도록 업데이트하면 더 많은 정보를 제공할 수 있습니다. 예: '뉴욕 근처 햄버거'
이를 위해 Geocoding API를 사용하여 지도의 중심 좌표를 도시 이름과 같은 사람이 읽을 수 있는 위치로 변환합니다. 새로운 async
메서드인 updateResultsHeader
가 이 로직을 처리합니다. 검색이 실행될 때마다 호출됩니다.
사용자의 검색어 형식 지정
UI가 깔끔하고 일관성 있게 표시되도록 사용자의 검색어가 자동으로 '제목 케이스' (예: '버거 레스토랑'이 'Burger Restaurant'으로 바뀝니다. 도우미 함수 toTitleCase
가 이 변환을 처리합니다. 검색을 실행하고 헤더를 업데이트하기 전에 사용자의 입력에 이 함수를 사용하도록 performSearch
메서드가 업데이트됩니다.
JavaScript 파일 업데이트
최종 버전의 코드로 script.js
파일을 업데이트합니다. 여기에는 새로운 toTitleCase
및 updateResultsHeader
메서드와 이를 통합하는 업데이트된 performSearch
메서드가 포함됩니다.
// script.js
class PlaceFinderApp {
constructor() {
// Get all DOM element references
this.queryInput = document.getElementById('query-input');
this.priceFilter = document.getElementById('price-filter');
this.ratingFilter = document.getElementById('rating-filter');
this.openNowFilter = document.getElementById('open-now-filter');
this.searchButton = document.getElementById('search-button');
this.placeSearch = document.getElementById('place-search-list');
this.gMap = document.querySelector('gmp-map');
this.loadingSpinner = document.getElementById('loading-spinner');
this.resultsHeaderText = document.getElementById('results-header-text');
this.placeholderMessage = document.getElementById('placeholder-message');
this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');
// Initialize instance variables
this.map = null;
this.geocoder = null;
this.markers = {};
this.detailsPopup = null;
this.PriceLevel = null;
this.isSearchInProgress = false;
// Start the application
this.init();
}
async init() {
// Import libraries
await google.maps.importLibrary("maps");
const { Place, PriceLevel } = await google.maps.importLibrary("places");
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
const { Geocoder } = await google.maps.importLibrary("geocoding");
// Make classes available to the instance
this.PriceLevel = PriceLevel;
this.AdvancedMarkerElement = AdvancedMarkerElement;
this.map = this.gMap.innerMap;
this.geocoder = new Geocoder();
this.initDetailsPopup();
this.attachEventListeners();
}
attachEventListeners() {
this.queryInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.performSearch();
}
});
this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
this.searchButton.addEventListener('click', this.performSearch.bind(this));
this.priceFilter.addEventListener('change', this.performSearch.bind(this));
this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
}
initDetailsPopup() {
this.detailsPopup = new this.AdvancedMarkerElement({
content: this.placeDetailsWidget,
map: null,
zIndex: 100
});
this.map.addListener('click', () => { this.detailsPopup.map = null; });
}
// NEW: Helper function to format text to Title Case
toTitleCase(str) {
if (!str) return '';
return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
showLoading(visible) {
this.loadingSpinner.classList.toggle('visible', visible);
}
clearMarkers() {
for (const marker of Object.values(this.markers)) { marker.map = null; }
this.markers = {};
}
addMarkers() {
this.isSearchInProgress = false;
this.showLoading(false);
const places = this.placeSearch.places;
if (!places || places.length === 0) return;
for (const place of places) {
if (!place.location || !place.id) continue;
const marker = new this.AdvancedMarkerElement({
map: this.map,
position: place.location,
title: place.displayName,
});
marker.addListener('click', (event) => {
event.stop();
this.map.panTo(place.location);
this.placeDetailsRequest.place = place;
this.placeDetailsWidget.style.display = 'block';
this.detailsPopup.position = place.location;
this.detailsPopup.map = this.map;
});
this.markers[place.id] = marker;
}
}
handleResultClick(event) {
const place = event.place;
if (!place || !place.id) return;
const marker = this.markers[place.id];
if (marker) {
marker.click();
}
}
// UPDATED: Now integrates formatting and the dynamic header
async performSearch() {
if (this.isSearchInProgress) return;
this.isSearchInProgress = true;
this.placeholderMessage.classList.add('hidden');
this.placeSearch.classList.remove('hidden');
this.showLoading(true);
this.clearMarkers();
if (this.detailsPopup) this.detailsPopup.map = null;
this.searchRequest.textQuery = null;
setTimeout(async () => {
const rawQuery = this.queryInput.value.trim();
if (!rawQuery) {
this.showLoading(false);
this.isSearchInProgress = false;
return;
};
// Format the query and update the input box value
const formattedQuery = this.toTitleCase(rawQuery);
this.queryInput.value = formattedQuery;
// Update the header with the new query and location
await this.updateResultsHeader(formattedQuery);
// Pass the formatted query to the search request
this.searchRequest.textQuery = formattedQuery;
this.searchRequest.locationRestriction = this.map.getBounds();
const selectedPrice = this.priceFilter.value;
let priceLevels = [];
switch (selectedPrice) {
case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
default: priceLevels = null; break;
}
this.searchRequest.priceLevels = priceLevels;
const selectedRating = parseFloat(this.ratingFilter.value);
this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
}, 0);
}
// NEW: Method to update the sidebar header with geocoded location
async updateResultsHeader(query) {
try {
const response = await this.geocoder.geocode({ location: this.map.getCenter() });
if (response.results && response.results.length > 0) {
const cityResult = response.results.find(r => r.types.includes('locality')) || response.results[0];
const city = cityResult.address_components[0].long_name;
this.resultsHeaderText.textContent = `${query} near ${city}`;
} else {
this.resultsHeaderText.textContent = `${query} near current map area`;
}
} catch (error) {
console.error("Geocoding failed:", error);
this.resultsHeaderText.textContent = `Results for ${query}`;
}
}
}
window.addEventListener('DOMContentLoaded', () => {
new PlaceFinderApp();
});
학습 내용 확인
script.js
파일을 저장하고 페이지를 새로고침합니다.
기능을 확인합니다.
- 검색창에
pizza
(모두 소문자)를 입력하고 검색을 클릭합니다. 상자의 텍스트가 '피자'로 변경되고 사이드바의 헤더가 '뉴욕 근처 피자'로 업데이트됩니다. - 지도를 보스턴과 같은 다른 도시로 이동한 후 다시 검색합니다. 헤더가 '보스턴 근처 피자'로 업데이트됩니다.
9. 축하합니다
Places UI 키트의 간편함과 핵심 Google Maps Platform JavaScript API의 강력한 기능을 결합한 완전한 대화형 지역 검색 애플리케이션을 성공적으로 빌드했습니다.
학습한 내용
- JavaScript 클래스를 사용하여 상태와 로직을 관리하는 매핑 애플리케이션을 구조화하는 방법
- Google Maps JavaScript API와 함께 Places UI 키트를 사용하여 UI를 빠르게 개발하는 방법
- 프로그래매틱 방식으로 고급 마커를 추가하고 관리하여 지도에 맞춤 관심 장소를 표시하는 방법
- Geocoding Service를 사용하여 좌표를 사람이 읽을 수 있는 주소로 변환하여 사용자 환경을 개선하는 방법
- 상태 플래그를 사용하고 구성요소 속성이 올바르게 업데이트되었는지 확인하여 대화형 애플리케이션에서 일반적인 경합 상태를 식별하고 수정하는 방법
다음 단계
- 색상, 크기를 변경하거나 맞춤 HTML을 사용하여 고급 마커를 맞춤설정하는 방법을 자세히 알아보세요.
- 클라우드 기반 지도 스타일 지정을 살펴보고 브랜드에 맞게 지도의 디자인과 분위기를 맞춤설정하세요.
- 그리기 라이브러리를 추가하여 사용자가 지도에 도형을 그려 검색 영역을 정의할 수 있도록 해 보세요.
- 다음 설문조사에 답하여 Google에서 가장 유용한 콘텐츠를 만들 수 있도록 도와주세요.
다른 Codelab에서 어떤 내용을 다뤘으면 하시나요?
가장 관심 있는 Codelab을 찾을 수 없나요? 여기에서 새로운 문제로 요청하세요.