mobile-react-infinite-calendar
Version:
A mobile-optimized infinite scroll calendar component for React
239 lines (238 loc) • 8.39 kB
JavaScript
/**
* 한국 공공데이터포털 공휴일 API 서비스
*/
import { API_CONSTANTS } from '../constants/calendar';
import { createCustomHolidayServiceKey } from '../utils/envUtils';
import { logger } from '../utils/logger';
class HolidayCache {
constructor() {
this.cache = new Map();
this.CACHE_DURATION = API_CONSTANTS.CACHE_DURATION;
}
set(key, holidays) {
this.cache.set(key, {
holidays,
timestamp: Date.now()
});
}
get(key) {
const data = this.cache.get(key);
if (!data)
return null;
// 캐시 유효성 검사
if (Date.now() - data.timestamp > this.CACHE_DURATION) {
this.cache.delete(key);
return null;
}
return data.holidays;
}
clear() {
this.cache.clear();
}
}
const holidayCache = new HolidayCache();
export class HolidayService {
constructor(serviceKey, locale = 'ko-KR') {
this.serviceKey = createCustomHolidayServiceKey(serviceKey || '');
this.locale = locale;
}
/**
* 특정 년월의 공휴일 데이터 조회
*/
async getHolidays(year, month) {
// 유효성 검사
if (!this.isKoreanLocale() || !this.serviceKey) {
logger.debug('공휴일 데이터 로드 스킵: 비한국 로케일 또는 서비스키 없음');
return [];
}
const monthCacheKey = `${year}-${month.toString().padStart(2, '0')}`;
const monthCached = holidayCache.get(monthCacheKey);
if (monthCached) {
logger.debug(`공휴일 캐시 히트: ${monthCacheKey}`, { count: monthCached.length });
return monthCached;
}
try {
const yearCacheKey = `year-${year}`;
let yearHolidays = holidayCache.get(yearCacheKey);
if (!yearHolidays) {
logger.info(`공휴일 API 호출 시작: ${year}년`);
yearHolidays = await this.fetchYearHolidays(year);
holidayCache.set(yearCacheKey, yearHolidays);
logger.info(`공휴일 API 호출 완료: ${year}년`, { count: yearHolidays.length });
}
else {
logger.debug(`공휴일 연도 캐시 히트: ${year}년`);
}
const monthHolidays = this.filterHolidaysByMonth(yearHolidays, month);
holidayCache.set(monthCacheKey, monthHolidays);
return monthHolidays;
}
catch (error) {
logger.error('공휴일 데이터 조회 실패:', error);
return [];
}
}
/**
* 한국 로케일 확인
*/
isKoreanLocale() {
return this.locale === 'ko-KR' || this.locale === 'ko';
}
/**
* 연도별 공휴일 데이터 가져오기
*/
async fetchYearHolidays(year) {
var _a, _b, _c, _d, _e;
const url = this.buildApiUrl(year);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await this.parseResponse(response);
// API 응답 검증
if (((_b = (_a = data.response) === null || _a === void 0 ? void 0 : _a.header) === null || _b === void 0 ? void 0 : _b.resultCode) !== '00') {
logger.error('API 응답 오류:', (_c = data.response) === null || _c === void 0 ? void 0 : _c.header);
throw new Error(`API Error: ${((_e = (_d = data.response) === null || _d === void 0 ? void 0 : _d.header) === null || _e === void 0 ? void 0 : _e.resultMsg) || 'Unknown error'}`);
}
return this.transformApiData(data);
}
/**
* API URL 생성
*/
buildApiUrl(year) {
// URL 인코딩하지 않고 직접 문자열로 구성
const baseUrl = 'https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo';
const params = [
`solYear=${year}`,
`ServiceKey=${this.serviceKey}`,
'_type=json',
`numOfRows=${API_CONSTANTS.MAX_ROWS_PER_REQUEST}`
];
return `${baseUrl}?${params.join('&')}`;
}
/**
* API 응답 파싱
*/
async parseResponse(response) {
const contentType = response.headers.get('content-type');
if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/json')) {
const data = await response.json();
return data;
}
// XML 또는 텍스트 응답 처리
const text = await response.text();
// XML 오류 확인
if (this.isXmlError(text)) {
this.logXmlError(text);
throw new Error('API returned XML error');
}
// JSON 파싱 시도
try {
const data = JSON.parse(text);
return data;
}
catch (error) {
throw new Error('Failed to parse API response');
}
}
/**
* XML 오류 응답 확인
*/
isXmlError(text) {
return text.includes('<OpenAPI_ServiceResponse>') && text.includes('<errMsg>');
}
/**
* XML 오류 로깅
*/
logXmlError(text) {
var _a, _b;
const errMsg = (_a = text.match(/<errMsg>(.*?)<\/errMsg>/)) === null || _a === void 0 ? void 0 : _a[1];
const authMsg = (_b = text.match(/<returnAuthMsg>(.*?)<\/returnAuthMsg>/)) === null || _b === void 0 ? void 0 : _b[1];
logger.error('공휴일 API 오류:', { errMsg, authMsg });
if (authMsg === 'SERVICE_KEY_IS_NOT_REGISTERED_ERROR') {
logger.error('서비스 키가 등록되지 않았습니다.');
}
}
/**
* 월별 공휴일 필터링
*/
filterHolidaysByMonth(holidays, month) {
return holidays.filter(holiday => {
const holidayDate = new Date(holiday.date);
return holidayDate.getMonth() + 1 === month;
});
}
/**
* 여러 개월의 공휴일 데이터 일괄 조회
*/
async getHolidaysForRange(startDate, endDate) {
// 한국 로케일이 아닌 경우 빈 배열 반환
const isKorean = this.locale === 'ko-KR' || this.locale === 'ko';
if (!isKorean) {
return [];
}
const promises = [];
const current = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
const end = new Date(endDate.getFullYear(), endDate.getMonth(), 1);
while (current <= end) {
promises.push(this.getHolidays(current.getFullYear(), current.getMonth() + 1));
current.setMonth(current.getMonth() + 1);
}
try {
const results = await Promise.all(promises);
return results.flat();
}
catch (error) {
logger.error('공휴일 범위 조회 실패:', error);
return [];
}
}
/**
* API 응답 데이터를 내부 Holiday 타입으로 변환
*/
transformApiData(data) {
var _a, _b, _c;
const items = (_c = (_b = (_a = data.response) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.items) === null || _c === void 0 ? void 0 : _c.item;
if (!items || !Array.isArray(items)) {
return [];
}
return items
.filter(this.isHolidayItem)
.map(this.convertToHoliday.bind(this));
}
/**
* 공휴일 여부 확인
*/
isHolidayItem(item) {
return item.isHoliday === 'Y';
}
/**
* API 아이템을 Holiday 타입으로 변환
*/
convertToHoliday(item) {
return {
id: `holiday-${item.locdate}`,
name: item.dateName,
date: this.formatDate(item.locdate),
color: 'red'
};
}
/**
* YYYYMMDD 형식을 YYYY-MM-DD 형식으로 변환
*/
formatDate(locdate) {
const dateStr = locdate.toString();
const year = dateStr.substring(0, 4);
const month = dateStr.substring(4, 6);
const day = dateStr.substring(6, 8);
return `${year}-${month}-${day}`;
}
/**
* 캐시 클리어
*/
clearCache() {
holidayCache.clear();
}
}
// 기본 인스턴스 export
export const holidayService = new HolidayService();