댓글 (0)
댓글 (0)
사용자 취향과 실시간 맥락을 반영한 AI 여행 플래너 에이전트인 TriPick을 개발하는 중, 여행 일정에 관광지 혼잡도를 반영하려다, 자동 반영 대신 "이 관광지에 사람이 많이 몰릴 수 있어요" 알림으로 방향을 틀었다. 그 결정과, 한국관광공사 관광지 집중률 API를 붙이며 지역코드 때문에 겪은 삽질을 정리한다.
처음 그림은 단순했다. 붐비는 관광지를 피해 일정을 짜준다. 혼잡할 것 같은 곳은 점수를 깎아 뒤로 미는 식이다.
그런데 막상 넣어보면 부작용이 있다. 여행 일정 추천은 "사용자가 좋아할 곳"을 최우선으로 골라야 한다. 여기에 혼잡도를 점수로 섞으면, 정작 취향에 맞는 장소가 "붐빈다"는 이유로 밀려난다. 붐비는 데는 대개 그럴 만해서 붐빈다. 인기 있는 곳을 혼잡하다고 빼버리면 추천의 질이 오히려 떨어진다.
그래서 혼잡도를 일정 자동 생성에서 빼고 알림으로 돌렸다. 날씨 예보 알림과 같은 방식이다. 비가 올 것 같으면 "일정 바꿀까요?" 하고 물어보듯, 혼잡이 예상되면 알려만 주고 실제로 바꿀지는 사용자가 정하게 둔다. 자동으로 일정을 갈아엎지 않는다.
혼잡도의 역할이 "일정을 자동으로 최적화하는 입력"에서 "사용자에게 판단을 넘기는 알림 신호"로 바뀐 셈이다.
한국관광공사가 공공데이터포털에 공개한 관광지 집중률(방문자 추이 예측) API를 쓴다. 오퍼레이션은 tatsCnctrRatedList. 시도·시군구를 주면 그 지역 관광지별로 향후 약 30일간 일자별 집중률을 돌려준다.
응답은 이렇게 생겼다.
{
"baseYmd": "20260720",
"areaCd": "51",
"areaNm": "강원특별자치도",
"signguCd": "51130",
"signguNm": "원주시",
"tAtsNm": "간현관광지",
"cnctrRate": "20.46"
}
baseYmd가 날짜, cnctrRate가 그날의 집중률(%)이다. 관광지 하나당 이런 행이 날짜별로 쌓인다. 요청에는 areaCd(시도)·signguCd(시군구)가 필수고 tAtsNm(관광지명)으로 특정 장소만 걸러 조회할 수 있다.
붙이기 전에 두 가지를 확인했다. 첫째, 이 데이터는 관광지만 있다. 음식점·카페는 없다. 그래서 알림은 관광지 일정에만 건다. 둘째, 없는 이름을 tAtsNm에 넣으면 totalCount: 0으로 조용히 빈 응답이 온다. 데이터가 없는 장소는 자연스럽게 걸러진다.
이 API의 진짜 난관은 지역코드였다. 한국관광공사 API는 지역코드 체계가 여러 개고 오퍼레이션마다 다른 걸 쓴다.
처음엔 이 API가 예전 관광정보 API에 쓰던 한국관광공사 고유 지역코드(areaCode2)를 쓸 거라고 생각했다. 그런데 스펙의 샘플값이 이 전제를 깼다. areaCd=51이 강원인데, 고유 지역코드에서 강원은 32다. 51은 아예 코드 범위(1-8, 31-39) 밖이다.
51은 법정동 코드에서 강원의 시도 코드다. 즉 이 API는 고유 지역코드가 아니라 법정동 코드 계열을 쓴다. 한국관광공사는 법정동 코드를 주는 별도 오퍼레이션(ldongCode2)을 제공하므로, 여기서 코드를 조달하기로 했다.
일정에는 장소의 좌표와 주소만 있고 지역코드는 없다. 그래서 주소에서 시도·시군구 이름을 뽑아 ldongCode2로 만든 이름→코드 색인으로 변환한다.
// "강원특별자치도 원주시 지정면 ..." → { areaCd: '51', signguCd: '51130' }
const sidoName = address.trim().split(/\s+/)[0]; // 강원특별자치도
const sigunguName = parseSigungu(address); // 원주시
const areaCd = sidoIndex.get(regionStem(sidoName)); // 51
여기서 한 번 더 걸렸다. ldongCode2로 강원(51)의 시군구 목록을 받으면, 원주시 코드가 51130이 아니라 130으로 온다. 뒤 3자리만 준다.
ldongCode2 (lDongRegnCd=51) → 110 춘천시 / 130 원주시 / 150 강릉시
집중률 API 가 요구하는 signguCd → 51130 (= 51 + 130)
집중률 API의 signguCd는 법정동 5자리 전체다. 그래서 시도 코드를 앞에 붙여야 한다.
const signguCd = `${areaCd}${String(code).padStart(3, '0')}`; // '51' + '130' → '51130'
이건 스펙 문서만 봐선 놓치기 쉬웠다. 실제로 API를 한 번 찔러보고 나서야 잡혔다. 강원특별자치도 원주시를 넣어 areaCd 51 / signguCd 51130이 나오고 간현관광지로 26일치 집중률이 돌아오는 걸 확인하고 코드를 확정했다.
실제 API 호출로 areaCd 51 / signguCd 51130, 간현관광지 집중률이 돌아온 터미널 결과
전체 흐름은 단순하다. 하루 한 번 예정된 여행을 훑어 관광지 일정의 예정일 집중률을 조회하고 붐빌 것 같으면 알림을 보낸다. 알림을 받은 사용자가 일정을 바꿀지 정한다.
판정 기준은 절대값이 아니라 상대값으로 잡았다. 집중률의 절대 스케일이 관광지마다 다르기 때문이다. 어떤 곳은 평소 5%, 어떤 곳은 평소 30%다. 절대값 하나로 자르면 늘 붐비는 곳만 계속 걸린다. 그래서 "그 관광지 자기 평균 대비 붐비는 날"을 본다.
혼잡으로 본다. 두 값은 실데이터를 보며 맞춰야 할 초기 임계값이라 상수로 빼뒀다.
여기서 조심할 게 하나 있었다. 지역코드 이름→코드 색인은 자주 변하지 않아 메모리에 한 번만 만들어 캐싱한다. 그런데 처음 구현은 실패한 조회 결과까지 캐싱했다. 첫 조회가 일시적 오류나 호출 한도를 만나 실패하면, 그 실패가 캐시에 박혀 프로세스를 재시작하기 전까지 지역 변환이 영영 안 된다. 캐시는 성공한 결과만 담고 실패하면 비워 다음번에 다시 시도하도록 고쳤다. 실패를 캐싱하지 않는다는 기본을 놓쳤던 셈이다.
관광지 혼잡도는 일정을 자동으로 최적화하는 입력이 아니라, 사용자에게 판단을 넘기는 알림으로 붙였다. 인기 있는 장소가 혼잡하다는 이유로 추천에서 밀려나는 걸 막기 위한 결정이다.
기술적으로 가장 시간을 쓴 건 지역코드였다. 한국관광공사 집중률 API는 예전 고유 코드가 아니라 법정동 코드를 쓰고 시군구 코드는 뒤 3자리만 와서 시도 코드를 붙여 5자리로 만들어야 했다. 공개 API를 붙일 때 스펙 문서만 믿지 말고 실제 응답을 한 번 받아보는 게 결국 제일 빨랐다.