활용 사례

listingsapi-js 리소스를 조합하여 실제 연동을 구현하는 전체 과정을 TypeScript 예제로 설명합니다.

이 예제는 여러 리소스를 함께 사용하는 워크플로로, 단일 메서드 문서만으로는 충분하지 않을 때 사용하는 스크립트입니다. 각 예제는 2개 이상의 리소스를 조합해 실제로 배포할 수 있도록 작성한 완전한 실행 가능 TypeScript 프로그램입니다. 버전이 고정된 SDK(npm install listingsapi-js@0.3.2)를 설치하고, 대시보드에서 발급받은 키로 LISTINGSAPI_KEY을 환경 변수로 내보내면 npx tsx <file>.ts로 예제를 그대로 실행할 수 있습니다.

SDK를 처음 사용한다면 먼저 Node SDK 개요를 읽어 보시기 바랍니다. 이 개요는 아래 모든 예제의 전제 지식인 클라이언트 생성, 인증, 오류, 페이지네이션을 다룹니다.

1. 빠른 시작: 사업장 조회 및 출력

최소한의 연동 작업으로 인증을 수행하고, 사업장 1페이지를 조회한 다음 사업장 이름을 출력합니다.

01_quickstart.ts
import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); // reads LISTINGSAPI_KEY from env const page = await client.fetchAllLocations({ first: 5 });if (!Array.isArray(page)) {for (const loc of page.locations) {  console.log(loc.name + ' - ' + loc.city);}}

fetchAll이 없으면 fetchAllLocations은 locations, pageInfo, raw 페이로드가 포함된 객체를 반환합니다. 다음 페이지를 가져오려면 pageInfo.endCursor를 after로 전달합니다.

2. JSON으로 일괄 내보내기

자동 페이지네이션으로 계정 내 모든 사업장을 조회하고 결과를 디스크에 저장합니다.

02_bulk_export.ts
import { writeFileSync } from 'fs';import { ListingsAPI } from 'listingsapi-js';import type { Location } from 'listingsapi-js'; const client = new ListingsAPI(); const locations = (await client.fetchAllLocations({fetchAll: true,pageSize: 100,})) as Location[]; writeFileSync('locations.json', JSON.stringify(locations, null, 2));console.log('Exported ' + locations.length + ' locations to locations.json');

fetchAll: true은 커서 기반 페이지를 자동으로 순회하고 중첩되지 않은 Location[]을 반환합니다. 사업장이 10 000개 이상인 경우 개별 요청이 빠르게 처리되도록 pageSize를 기본값인 100으로 유지합니다.

3. 새 사업장 등록

fetchSubcategories으로 적절한 카테고리 ID를 조회한 다음 사업장을 생성합니다. createLocation가 subCategoryId으로 받는 값은 하위 카테고리의 databaseId입니다. API에는 200자 이상의 설명도 필요합니다.

03_onboarding.ts
import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const categories = await client.fetchSubcategories();const dentist = categories.find((c) => c.name === 'Dentist');if (!dentist) throw new Error('Category not found'); const description ='Acme Dental is a family-owned dental practice in downtown New York ' +'offering preventive care, cosmetic dentistry, orthodontics, and ' +'emergency appointments. Our team has served the neighborhood for over ' +'twenty years with same-week scheduling and weekend hours.'; const created = await client.createLocation({name:          'Acme Dental',storeId:       'ACME-NY-001',street:        '123 Jump Street',city:          'New York',stateIso:      'NY',postalCode:    '10013',countryIso:    'US',phone:         '6443859313',subCategoryId: dentist.databaseId,description:   description,}); console.log('Created location', created.location?.id);

유효성 검사에 실패하면(필드 누락, 짧은 설명, 잘못된 카테고리) err.errors에 플랫폼의 오류 항목이 포함된 ValidationError이 발생하므로, 확인할 success 플래그는 없습니다.

4. Google과 Facebook에 게시물 일괄 게시

bulkPublish은 단일 호출로 여러 사업장의 두 사이트 모두에 게시물 1개를 게시합니다. 클라이언트 측에서 페이로드의 유효성을 검사하고, 사이트별로 메시지를 구성하며, 숫자형 사업장 ID를 인코딩합니다.

04_bulk_publish.ts
import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const result = await client.bulkPublish({name:        'Holiday hours',locationIds: [16808, 16809, 16810],message:     'Open until 10pm through the holidays!',mediaUrl:    'https://cdn.example.com/holiday.jpg',ctaType:     'LEARN_MORE',ctaUrl:      'https://example.com/holiday-hours',}); console.log('Campaign:', result.socialPost?.id, result.socialPost?.status); // Publishing is asynchronous; poll the bulk campaign for per-location statusconst bulk = await client.fetchBulkPost(result.socialPost.id);console.log(bulk);

sites의 기본값은 ['GOOGLE', 'FACEBOOK']입니다. 사이트별로 문구가 달라야 한다면 사이트별 맵을 message({ GOOGLE: '...', FACEBOOK: '...' })로 전달합니다. 타입이 지정된 옵션을 사용하는 단일 캠페인에는 대신 createAnnouncement, createEvent 또는 createOffer을 사용합니다.

5. 리뷰 모니터링: 낮은 평점 표시

모든 사업장에서 별점 1점과 2점 리뷰를 찾아 주의가 필요한 리뷰를 로그에 기록합니다.

05_review_monitoring.ts
import { ListingsAPI, APIError } from 'listingsapi-js';import type { Interaction, Location } from 'listingsapi-js'; const client = new ListingsAPI(); const locations = (await client.fetchAllLocations({ fetchAll: true })) as Location[]; for (const loc of locations) {let reviews: Interaction[];try {  reviews = (await client.fetchInteractions(loc.id, {    fetchAll:      true,    startDate:     '2026-01-01',    ratingFilters: [1, 2],  })) as Interaction[];} catch (err) {  if (err instanceof APIError) {    console.error(loc.name + ': ' + err.statusCode);    continue;  }  throw err;} for (const review of reviews) {  const who = review.reviewerName ?? 'Anonymous';  console.log('NEEDS ATTENTION [' + loc.name + '] ' + who + ' (' + review.rating + ' stars)');}}

6. 답글이 없는 리뷰에 답글

아직 답글이 없는 모든 리뷰에 자동으로 답글을 작성합니다. 템플릿을 사용하거나 buildResponse을 LLM 호출로 대체합니다.

06_bulk_respond.ts
import { ListingsAPI } from 'listingsapi-js';import type { Interaction } from 'listingsapi-js'; const client = new ListingsAPI();const LOCATION_ID = 16808; function buildResponse(review: Interaction): string {const name = review.reviewerName ?? 'there';if ((review.rating ?? 5) <= 2) {  return 'Hi ' + name + ', we are sorry to hear about your experience. Our team will reach out shortly.';}return 'Hi ' + name + ', thank you for the kind words. We look forward to seeing you again!';} const reviews = (await client.fetchInteractions(LOCATION_ID, {fetchAll:  true,startDate: '2026-01-01',})) as Interaction[]; let responded = 0;for (const review of reviews) {if (review.responses && review.responses.length > 0) continue;await client.respondToReview(review.id, buildResponse(review));responded++;}console.log('Responded to ' + responded + ' reviews');

7. 분석 보고서: Google, Bing, Facebook 및 리뷰 통계

단일 사업장의 프로필 분석 데이터와 리뷰 통계를 병렬로 조회하고 1페이지 요약을 출력합니다.

07_analytics_report.ts
import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI();const LOCATION_ID = 16808;const range = { fromDate: '2026-01-01', toDate: '2026-06-30' }; const [google, bing, facebook, reviewOverview] = await Promise.all([client.fetchGoogleAnalytics(LOCATION_ID, range),client.fetchBingAnalytics(LOCATION_ID, range),client.fetchFacebookAnalytics(LOCATION_ID, range),client.fetchReviewAnalyticsOverview(LOCATION_ID, {  startDate: range.fromDate,  endDate:   range.toDate,}),]); console.log('Google insights:',  JSON.stringify(google));console.log('Bing insights:',    JSON.stringify(bing));console.log('Facebook insights:', JSON.stringify(facebook));console.log('Review overview:',  JSON.stringify(reviewOverview));

매개변수의 차이에 유의해야 합니다. 플랫폼 분석에는 fromDate/toDate을 사용하고, 리뷰 분석에는 startDate/endDate을 사용합니다.

8. 등록 정보 점검: 동기화되지 않은 프로필 및 중복 프로필 찾기

사업장 1곳에서 동기화되지 않은 프리미엄 등록 정보와 ListingsAPI가 발견한 중복 프로필을 찾아 중복으로 표시합니다.

08_listings_audit.ts
import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI();const LOCATION_ID = 16808; const [premium, voice, duplicates] = await Promise.all([client.fetchPremiumListings(LOCATION_ID),client.fetchVoiceListings(LOCATION_ID),client.fetchDuplicateListings(LOCATION_ID),]); for (const l of premium) {if (l.syncStatus !== 'SYNCED') {  console.log('Not synced: ' + l.site + ' - ' + l.syncStatus);}} console.log('Voice listings: ' + voice.length);console.log('Duplicates detected: ' + duplicates.length); const ids = duplicates.map((d) => d.id).filter((id): id is string => typeof id === 'string'); if (ids.length > 0) {await client.markListingsAsDuplicate(LOCATION_ID, ids);console.log('Marked ' + ids.length + ' listings as duplicate');}

계정 전체를 점검할 때 fetchAllDuplicateListings({ page: 1 })은 모든 사업장의 중복 항목을 집계한 결과를 반환합니다.

9. 연결된 계정: Google 연결 절차

Google 계정을 한 번 연결하고, 해당 계정의 등록 정보를 사업장과 매칭한 다음 제안된 매칭을 확인합니다.

09_connected_accounts.ts
import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); // 1. Get an OAuth URL and send the account owner thereconst connect = await client.connectGoogleAccount('https://example.com/connected','https://example.com/connect-error',);console.log('Send the owner to:', JSON.stringify(connect)); // 2. After the owner completes OAuth, list connected accountsconst accounts = await client.fetchConnectedAccounts({ page: 1, perPage: 25 });console.log(JSON.stringify(accounts)); // 3. Trigger matching between the account's listings and your locationsconst accountId = 'connected-account-uuid';await client.triggerConnectedAccountMatches([accountId]); const suggestions = await client.fetchConnectionSuggestions(accountId, {page: 1,perPage: 25,});console.log(JSON.stringify(suggestions)); // 4. Confirm the matches you agree withawait client.confirmConnectedAccountMatches(['match-record-id-1','match-record-id-2',]);console.log('Matches confirmed');

대신 단일 사업장을 연결하려면 getOauthConnectUrl(locationId, 'GOOGLE', successUrl, errorUrl)을 사용하고, 등록 정보 1개를 직접 연결하려면 connectListing(locationId, listingId, accountId)을 사용합니다.

10. 사업장 검색 및 필드 일괄 업데이트

검색 조건에 맞는 사업장을 찾은 다음 각 검색 결과의 필드를 업데이트합니다. 예를 들어 특정 도시의 사업장에 새 전화번호를 일괄 적용할 수 있습니다.

10_bulk_update.ts
import { ListingsAPI } from 'listingsapi-js';import type { Location } from 'listingsapi-js'; const client = new ListingsAPI(); const matches = (await client.searchLocations('Brooklyn', { fetchAll: true })) as Location[]; let updated = 0;for (const loc of matches) {await client.updateLocation({  id:    loc.id,  phone: '7185550100',});updated++;}console.log('Updated phone on ' + updated + ' Brooklyn locations');

다음에 읽을 문서

  • 사업장: 매개변수 표를 포함한 전체 메서드 참조
  • 리뷰: 분석, 설정, 문구 분석
  • 등록 정보: 프리미엄, 음성, 중복
  • 분석: Google, Bing, Facebook 인사이트
  • 오류 처리: APIError 하위 클래스 및 재시도 전략