generated from ttipo/project-base
feat(backend): KOPIS 공연 동기화 추가
대구 공연은 관광공사에 거의 없고(90일 1건) KOPIS 에는 153건이 있다. place:sync -- kopis 로 대구·경북 공연을 매일 받는다. - 목록 → 새·바뀐(afterdate)·7일 지난 공연만 상세 → 새 공연장만 시설 상세 → 시설 좌표를 공연에 복사 - 시설 좌표가 주소의 시·도 영역 밖이면 비운다(대구 중구 주소에 경주 좌표인 시설이 있었다) - 포스터 http 주소를 https://kopis.or.kr 로 바꿔 저장, 0.6초 간격 호출(초당 10회 차단) - 공공데이터 클라이언트에 키 파라미터 이름·호출 간격·경로를 나눠 받게 함 - XML 응답 해석에 fast-xml-parser 추가. 컨테이너 npm 11.20 이 만든 잠금 파일은 선택 peer·@emnapi 항목을 지워 CI(npm 10)·호스트의 npm ci 가 실패해서, 새 패키지 항목만 기존 잠금에 더했다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { isInKorea } from './geo';
|
||||
import { isInKorea, isInSido, sidoCodeFromAddress } from './geo';
|
||||
|
||||
describe('isInKorea', () => {
|
||||
it('대구 좌표는 한국 안이다', () => {
|
||||
@@ -14,3 +14,33 @@ describe('isInKorea', () => {
|
||||
expect(isInKorea(Number.NaN, 128.5)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidoCodeFromAddress', () => {
|
||||
it('주소 앞머리로 대구·경북 코드를 찾는다', () => {
|
||||
expect(sidoCodeFromAddress('대구광역시 중구 태평로 141')).toBe('27');
|
||||
expect(sidoCodeFromAddress('경상북도 경주시 알천북로 1')).toBe('47');
|
||||
expect(sidoCodeFromAddress(' 경북 포항시 북구')).toBe('47');
|
||||
});
|
||||
|
||||
it('모르는 지역이거나 비어 있으면 null', () => {
|
||||
expect(sidoCodeFromAddress('부산광역시 해운대구')).toBeNull();
|
||||
expect(sidoCodeFromAddress(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInSido', () => {
|
||||
it('대구 도심·군위 좌표는 대구 안이다', () => {
|
||||
expect(isInSido('27', 35.8761, 128.5938)).toBe(true);
|
||||
expect(isInSido('27', 36.2428, 128.5728)).toBe(true);
|
||||
});
|
||||
|
||||
it('대구 주소에 경주 좌표(2026-11-03 KOPIS 실제 사례)는 대구 밖이다', () => {
|
||||
expect(isInSido('27', 35.847189, 129.212306)).toBe(false);
|
||||
expect(isInSido('47', 35.847189, 129.212306)).toBe(true);
|
||||
});
|
||||
|
||||
it('영역을 모르는 시·도는 한국 안인지만 본다', () => {
|
||||
expect(isInSido('26', 35.1587, 129.1604)).toBe(true);
|
||||
expect(isInSido('26', 0, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,3 +20,44 @@ export function isInKorea(lat: number, lng: number): boolean {
|
||||
lng <= b.lngMax
|
||||
);
|
||||
}
|
||||
|
||||
interface Bounds {
|
||||
latMin: number;
|
||||
latMax: number;
|
||||
lngMin: number;
|
||||
lngMax: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시·도 영역 대략값(법정동 시·도 코드 2자리) — 원본 좌표가 주소와 다른 지역을 가리키는지 본다.
|
||||
* 2026-11-03 KOPIS 공연시설 중 대구 중구 주소인데 경주 좌표(경도 129.21)인 곳이 있었다.
|
||||
* 동기화 범위(대구·경북)만 둔다. 없는 코드는 한국 영역만 확인한다.
|
||||
*/
|
||||
const SIDO_BOUNDS: Record<string, Bounds> = {
|
||||
// 대구 — 2023년 편입한 군위군 포함
|
||||
'27': { latMin: 35.55, latMax: 36.36, lngMin: 128.3, lngMax: 128.9 },
|
||||
// 경북 — 울릉도·독도 포함
|
||||
'47': { latMin: 35.55, latMax: 37.6, lngMin: 127.75, lngMax: 131.95 },
|
||||
};
|
||||
|
||||
/** 주소 앞머리 → 시·도 코드 (동기화 범위만) */
|
||||
const SIDO_ADDRESS_PREFIXES: Array<[RegExp, string]> = [
|
||||
[/^대구/, '27'],
|
||||
[/^(경상북도|경북)/, '47'],
|
||||
];
|
||||
|
||||
/** 주소('대구광역시 중구 …')에서 시·도 코드를 찾는다. 모르는 지역이면 null */
|
||||
export function sidoCodeFromAddress(address: string | null): string | null {
|
||||
const text = address?.trim() ?? '';
|
||||
return SIDO_ADDRESS_PREFIXES.find(([re]) => re.test(text))?.[1] ?? null;
|
||||
}
|
||||
|
||||
/** 좌표가 그 시·도 안인지 본다. 영역을 모르는 코드면 한국 안인지만 본다 */
|
||||
export function isInSido(sidoCode: string, lat: number, lng: number): boolean {
|
||||
if (!isInKorea(lat, lng)) return false;
|
||||
const b = SIDO_BOUNDS[sidoCode];
|
||||
if (!b) return true;
|
||||
return (
|
||||
lat >= b.latMin && lat <= b.latMax && lng >= b.lngMin && lng <= b.lngMax
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user