CASTAD-v0.1/server/services/tourApiClient.js

440 lines
11 KiB
JavaScript

/**
* TourAPI 4.0 클라이언트
* 한국관광공사 국문 관광정보 서비스 (KorService2)
*
* 문서: https://www.data.go.kr/data/15101578/openapi.do
* Base URL: https://apis.data.go.kr/B551011/KorService2
*/
const axios = require('axios');
// 관광타입 ID
const CONTENT_TYPE = {
TOURIST_SPOT: '12', // 관광지
CULTURAL: '14', // 문화시설
FESTIVAL: '15', // 행사/공연/축제
TRAVEL_COURSE: '25', // 여행코스
LEISURE: '28', // 레포츠
STAY: '32', // 숙박 (펜션)
SHOPPING: '38', // 쇼핑
RESTAURANT: '39', // 음식점
};
// 지역코드
const AREA_CODE = {
SEOUL: '1',
INCHEON: '2',
DAEJEON: '3',
DAEGU: '4',
GWANGJU: '5',
BUSAN: '6',
ULSAN: '7',
SEJONG: '8',
GYEONGGI: '31',
GANGWON: '32',
CHUNGBUK: '33',
CHUNGNAM: '34',
GYEONGBUK: '35',
GYEONGNAM: '36',
JEONBUK: '37',
JEONNAM: '38',
JEJU: '39',
};
// 지역코드 이름 매핑
const AREA_NAME = {
'1': '서울',
'2': '인천',
'3': '대전',
'4': '대구',
'5': '광주',
'6': '부산',
'7': '울산',
'8': '세종',
'31': '경기',
'32': '강원',
'33': '충북',
'34': '충남',
'35': '경북',
'36': '경남',
'37': '전북',
'38': '전남',
'39': '제주',
};
class TourApiClient {
constructor() {
this.baseUrl = process.env.TOURAPI_ENDPOINT || 'https://apis.data.go.kr/B551011/KorService2';
this.serviceKey = process.env.TOURAPI_KEY;
this.client = axios.create({
baseURL: this.baseUrl,
timeout: 30000,
});
}
/**
* API 요청 공통 메서드
*/
async request(operation, params = {}) {
try {
const response = await this.client.get(`/${operation}`, {
params: {
serviceKey: this.serviceKey,
MobileOS: 'ETC',
MobileApp: 'CastAD',
_type: 'json',
...params,
},
});
const data = response.data;
// 에러 체크
if (data.response?.header?.resultCode !== '0000') {
const errorMsg = data.response?.header?.resultMsg || 'Unknown error';
throw new Error(`TourAPI Error: ${errorMsg}`);
}
const body = data.response?.body;
return {
items: body?.items?.item || [],
totalCount: body?.totalCount || 0,
pageNo: body?.pageNo || 1,
numOfRows: body?.numOfRows || 10,
};
} catch (error) {
if (error.response?.status === 401) {
throw new Error('TourAPI 인증 실패: 서비스키를 확인하세요');
}
throw error;
}
}
// ============================================
// 지역/분류 코드 조회
// ============================================
/**
* 지역코드 조회
* @param {string} areaCode - 시도 코드 (없으면 전체 시도 조회)
*/
async getAreaCodes(areaCode = null) {
const params = { numOfRows: 100, pageNo: 1 };
if (areaCode) params.areaCode = areaCode;
return this.request('areaCode2', params);
}
/**
* 서비스 분류코드 조회
*/
async getCategoryCodes(contentTypeId = null, cat1 = null, cat2 = null) {
const params = { numOfRows: 100, pageNo: 1 };
if (contentTypeId) params.contentTypeId = contentTypeId;
if (cat1) params.cat1 = cat1;
if (cat2) params.cat2 = cat2;
return this.request('categoryCode2', params);
}
// ============================================
// 축제/행사 조회 (핵심!)
// ============================================
/**
* 행사정보 조회 (축제)
* @param {Object} options
* @param {string} options.eventStartDate - 행사 시작일 (YYYYMMDD) - 필수!
* @param {string} options.eventEndDate - 행사 종료일 (YYYYMMDD)
* @param {string} options.areaCode - 지역코드
* @param {string} options.sigunguCode - 시군구코드
* @param {number} options.numOfRows - 한 페이지 결과 수
* @param {number} options.pageNo - 페이지 번호
* @param {string} options.arrange - 정렬 (A=제목순, C=수정일순, D=생성일순)
*/
async searchFestival(options = {}) {
const {
eventStartDate = this.getTodayString(),
eventEndDate,
areaCode,
sigunguCode,
numOfRows = 100,
pageNo = 1,
arrange = 'A',
} = options;
const params = {
numOfRows,
pageNo,
arrange,
eventStartDate,
};
if (eventEndDate) params.eventEndDate = eventEndDate;
if (areaCode) params.areaCode = areaCode;
if (sigunguCode) params.sigunguCode = sigunguCode;
return this.request('searchFestival2', params);
}
/**
* 진행중인 축제 조회
*/
async getOngoingFestivals(areaCode = null) {
const today = this.getTodayString();
return this.searchFestival({
eventStartDate: '20240101', // 과거부터
eventEndDate: today, // 오늘까지 종료되지 않은
areaCode,
arrange: 'C', // 수정일순
});
}
/**
* 예정 축제 조회 (향후 6개월)
*/
async getUpcomingFestivals(areaCode = null, months = 6) {
const today = this.getTodayString();
const endDate = this.getDateAfterMonths(months);
return this.searchFestival({
eventStartDate: today,
eventEndDate: endDate,
areaCode,
arrange: 'A', // 제목순
});
}
// ============================================
// 숙박 조회 (펜션)
// ============================================
/**
* 숙박정보 조회
* @param {Object} options
* @param {string} options.areaCode - 지역코드
* @param {string} options.sigunguCode - 시군구코드
* @param {number} options.numOfRows - 한 페이지 결과 수
* @param {number} options.pageNo - 페이지 번호
* @param {string} options.arrange - 정렬
*/
async searchStay(options = {}) {
const {
areaCode,
sigunguCode,
numOfRows = 100,
pageNo = 1,
arrange = 'A',
} = options;
const params = { numOfRows, pageNo, arrange };
if (areaCode) params.areaCode = areaCode;
if (sigunguCode) params.sigunguCode = sigunguCode;
return this.request('searchStay2', params);
}
// ============================================
// 지역/위치 기반 조회
// ============================================
/**
* 지역기반 관광정보 조회
*/
async getAreaBasedList(options = {}) {
const {
contentTypeId,
areaCode,
sigunguCode,
cat1, cat2, cat3,
numOfRows = 100,
pageNo = 1,
arrange = 'C',
} = options;
const params = { numOfRows, pageNo, arrange };
if (contentTypeId) params.contentTypeId = contentTypeId;
if (areaCode) params.areaCode = areaCode;
if (sigunguCode) params.sigunguCode = sigunguCode;
if (cat1) params.cat1 = cat1;
if (cat2) params.cat2 = cat2;
if (cat3) params.cat3 = cat3;
return this.request('areaBasedList2', params);
}
/**
* 위치기반 관광정보 조회 (근처 검색)
* @param {number} mapX - 경도 (longitude)
* @param {number} mapY - 위도 (latitude)
* @param {number} radius - 반경 (미터, 최대 20000)
* @param {string} contentTypeId - 관광타입
*/
async getLocationBasedList(mapX, mapY, radius = 10000, contentTypeId = null) {
const params = {
mapX: mapX.toString(),
mapY: mapY.toString(),
radius: Math.min(radius, 20000), // 최대 20km
numOfRows: 100,
pageNo: 1,
arrange: 'E', // 거리순
};
if (contentTypeId) params.contentTypeId = contentTypeId;
return this.request('locationBasedList2', params);
}
/**
* 근처 축제 검색
*/
async getNearbyFestivals(mapX, mapY, radius = 20000) {
return this.getLocationBasedList(mapX, mapY, radius, CONTENT_TYPE.FESTIVAL);
}
/**
* 근처 숙박시설 검색
*/
async getNearbyStay(mapX, mapY, radius = 20000) {
return this.getLocationBasedList(mapX, mapY, radius, CONTENT_TYPE.STAY);
}
// ============================================
// 상세 정보 조회
// ============================================
/**
* 공통정보 조회 (상세정보1)
*/
async getDetailCommon(contentId) {
return this.request('detailCommon2', { contentId });
}
/**
* 소개정보 조회 (상세정보2) - 타입별 상세
*/
async getDetailIntro(contentId, contentTypeId) {
return this.request('detailIntro2', { contentId, contentTypeId });
}
/**
* 반복정보 조회 (상세정보3) - 객실정보 등
*/
async getDetailInfo(contentId, contentTypeId) {
return this.request('detailInfo2', { contentId, contentTypeId });
}
/**
* 이미지정보 조회 (상세정보4)
*/
async getDetailImage(contentId) {
return this.request('detailImage2', { contentId });
}
/**
* 축제 전체 상세정보 조회
*/
async getFestivalDetail(contentId) {
const [common, intro, images] = await Promise.all([
this.getDetailCommon(contentId),
this.getDetailIntro(contentId, CONTENT_TYPE.FESTIVAL),
this.getDetailImage(contentId),
]);
return {
common: common.items[0] || {},
intro: intro.items[0] || {},
images: images.items || [],
};
}
/**
* 숙박 전체 상세정보 조회
*/
async getStayDetail(contentId) {
const [common, intro, rooms, images] = await Promise.all([
this.getDetailCommon(contentId),
this.getDetailIntro(contentId, CONTENT_TYPE.STAY),
this.getDetailInfo(contentId, CONTENT_TYPE.STAY),
this.getDetailImage(contentId),
]);
return {
common: common.items[0] || {},
intro: intro.items[0] || {},
rooms: rooms.items || [],
images: images.items || [],
};
}
// ============================================
// 동기화
// ============================================
/**
* 관광정보 동기화 목록 조회 (변경된 데이터)
*/
async getSyncList(options = {}) {
const {
contentTypeId,
modifiedTime, // YYYYMMDD 형식
numOfRows = 100,
pageNo = 1,
} = options;
const params = { numOfRows, pageNo };
if (contentTypeId) params.contentTypeId = contentTypeId;
if (modifiedTime) params.modifiedtime = modifiedTime;
return this.request('areaBasedSyncList2', params);
}
// ============================================
// 유틸리티
// ============================================
/**
* 오늘 날짜 문자열 (YYYYMMDD)
*/
getTodayString() {
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
}
/**
* N개월 후 날짜 문자열
*/
getDateAfterMonths(months) {
const date = new Date();
date.setMonth(date.getMonth() + months);
return date.toISOString().slice(0, 10).replace(/-/g, '');
}
/**
* 날짜 파싱 (YYYYMMDD -> Date)
*/
parseDate(dateStr) {
if (!dateStr || dateStr.length < 8) return null;
const year = dateStr.slice(0, 4);
const month = dateStr.slice(4, 6);
const day = dateStr.slice(6, 8);
return new Date(`${year}-${month}-${day}`);
}
/**
* 날짜 포맷팅 (YYYYMMDD -> YYYY.MM.DD)
*/
formatDate(dateStr) {
if (!dateStr || dateStr.length < 8) return '';
return `${dateStr.slice(0, 4)}.${dateStr.slice(4, 6)}.${dateStr.slice(6, 8)}`;
}
}
// 싱글톤 인스턴스
const tourApiClient = new TourApiClient();
module.exports = {
tourApiClient,
TourApiClient,
CONTENT_TYPE,
AREA_CODE,
AREA_NAME,
};