feat(backend): 상가정보 데이트 업종 적재 명령 추가

`npm run place:sync -- store [--signgu=...]` 로 소상공인 상가정보의 데이트 업종
6개(카페·빵/도넛·아이스크림/빙수·샌드위치/샐러드·요리 주점·생맥주)를 장소 DB 에
적재한다. 기본 범위는 1차 큐레이션 동네가 속한 대구 중구·남구·수성구다.

- 분기마다 다시 실행하면, 끝까지 받은 (시군구, 업종) 묶음에서 사라진 업소를
  gone(폐업 추정)으로 숨긴다. 지우지 않고, 결과가 기존의 80% 미만이면 건너뛴다
- 기준년월(stdrYm)을 source_snapshot 에 남긴다
- 공공데이터포털 공통 규칙(키 가림·간격·재시도·한도)을 PublicDataClient 로 묶어
  관광공사·상가정보 클라이언트가 함께 쓴다(TourApiFactory → PublicDataApiFactory)

2026-11-02 대구 실측: 기준 2026-09, 3,776곳(18회 호출). 세 동네 반경 안 업소 수는
후보표(반경 API)와 거의 같다(도심 1,295/1,316 · 수성못 425/425 · 앞산 262/263).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 17:00:13 +09:00
parent 9775d6de45
commit 71aaf0bcae
20 changed files with 953 additions and 226 deletions
+16
View File
@@ -0,0 +1,16 @@
import { isInKorea } from './geo';
describe('isInKorea', () => {
it('대구 좌표는 한국 안이다', () => {
expect(isInKorea(35.8677, 128.5867)).toBe(true);
});
it('위도·경도가 뒤바뀌었거나 0 이면 한국 밖으로 본다', () => {
expect(isInKorea(128.5867, 35.8677)).toBe(false);
expect(isInKorea(0, 0)).toBe(false);
});
it('숫자가 아니면 거른다', () => {
expect(isInKorea(Number.NaN, 128.5)).toBe(false);
});
});
+22
View File
@@ -0,0 +1,22 @@
/**
* 좌표 유틸 (순수 함수).
*/
/** 한국 영역 대략값(제주~백령도·독도 포함) */
const KOREA_BOUNDS = { latMin: 33, latMax: 39.5, lngMin: 124, lngMax: 132 };
/**
* 위도·경도가 한국 안의 정상 좌표인지 본다.
* 공공데이터에는 좌표가 0 이거나 위도·경도가 뒤바뀐 항목이 섞여 있다.
*/
export function isInKorea(lat: number, lng: number): boolean {
const b = KOREA_BOUNDS;
return (
Number.isFinite(lat) &&
Number.isFinite(lng) &&
lat >= b.latMin &&
lat <= b.latMax &&
lng >= b.lngMin &&
lng <= b.lngMax
);
}