generated from ttipo/project-base
fix(backend): 좌표가 주소와 맞지 않거나 실제 위치가 아닌 관광공사 장소를 받지 않음
관광공사 원본 좌표가 실제 위치가 아닌 장소가 거리·지도에 엉뚱하게 나왔다(2026-11-04 회귀 점검). - 오천서원: 주소는 대구 수성구인데 좌표는 포항이라 대구 목록에 70km 떨어진 곳으로 나왔다 - 대구올레 팔공산 3·4·5코스: 주소·좌표 모두 운영 사무실(수성구)이라 김광석길에서 '도보 7분'으로 보였다 좌표가 주소(없으면 법정동 코드)의 시·도 밖이면 받지 않는다(geo.ts isInSido 재사용). 시·도 검사로 못 거르는 곳은 확인한 contentid 목록(TOUR_WRONG_LOCATION_IDS)으로 받지 않는다. 이미 있던 장소는 목록을 끝까지 받은 뒤 숨겨진다. 관광공사 장소 1,174곳에 적용해 이 4곳만 걸리는 것을 확인했고, 개발 DB 는 tour-kor --skip-details 로 반영했다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -145,6 +145,17 @@ export const MALL_NAME_PREFIXES = [
|
||||
'동아백화점',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 원본 좌표가 실제 위치가 아닌 관광공사 장소(contentid → 사유) — 원본이 고쳐질 때까지 받지 않는다(목록에서 숨겨진다).
|
||||
* 주소의 시·도와 맞는지만 보는 검사로는 못 거르는 곳만 둔다. 실제 좌표를 확인하면 큐레이션으로 되살린다.
|
||||
*/
|
||||
export const TOUR_WRONG_LOCATION_IDS: Readonly<Record<string, string>> = {
|
||||
// 2026-11-04 회귀 점검: 주소·좌표 모두 운영 사무실(수성구 신천동로86안길 14-6)이라 김광석길에서 '도보 7분'으로 보였다. 코스는 팔공산(동구)
|
||||
'1008532': '[대구올레 팔공산 3코스] 부인사 도보길',
|
||||
'1008588': '[대구올레 팔공산 4코스] 평광동 왕건길',
|
||||
'1008614': '[대구올레 팔공산 5코스] 구암마을 가는 길',
|
||||
};
|
||||
|
||||
/**
|
||||
* 같은 가게를 관광공사·상가정보가 둘 다 가지고 있을 때(2026-11-02 대구 약 21곳) 상가정보 쪽을 관광공사 쪽에 병합한다.
|
||||
* 관광공사 쪽이 이 분류일 때만 비교한다 — 박물관·공원 안의 카페("이디야커피 국립대구박물관점")를 박물관과 합치지 않게.
|
||||
|
||||
@@ -89,6 +89,39 @@ describe('listItemToPlace', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('좌표가 주소의 시·도 밖이면 건너뛴다(오천서원 — 주소 대구 수성구 · 좌표 포항)', () => {
|
||||
expect(
|
||||
listItemToPlace(
|
||||
item({
|
||||
contentid: '1955582',
|
||||
title: '오천서원',
|
||||
addr1: '대구광역시 수성구 파동로2안길 20',
|
||||
mapx: '129.3976973279',
|
||||
mapy: '35.9819418183',
|
||||
lDongSignguCd: '260',
|
||||
}),
|
||||
),
|
||||
).toEqual({ skip: 'coords_mismatch' });
|
||||
// 주소가 비면 법정동 시·도 코드로 확인한다
|
||||
expect(
|
||||
listItemToPlace(item({ addr1: '', mapx: '129.39', mapy: '35.98' })),
|
||||
).toEqual({ skip: 'coords_mismatch' });
|
||||
});
|
||||
|
||||
it('좌표가 실제 위치가 아닌 것으로 확인한 장소는 건너뛴다(대구올레 팔공산 3코스)', () => {
|
||||
expect(
|
||||
listItemToPlace(
|
||||
item({
|
||||
contentid: '1008532',
|
||||
title: '[대구올레 팔공산 3코스] 부인사 도보길',
|
||||
addr1: '대구광역시 수성구 신천동로86안길 14-6',
|
||||
mapx: '128.61234',
|
||||
mapy: '35.86191',
|
||||
}),
|
||||
),
|
||||
).toEqual({ skip: 'wrong_location' });
|
||||
});
|
||||
|
||||
it('알 수 없는 저작권 유형은 비우고, 주소 두 줄은 이어 붙인다', () => {
|
||||
const p = listItemToPlace(
|
||||
item({ cpyrhtDivCd: 'Type9', addr2: '(남산동)' }),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isInKorea } from '@/shared/utils/geo';
|
||||
import { isInKorea, isInSido, sidoCodeFromAddress } from '@/shared/utils/geo';
|
||||
import { extractUrl, htmlToText } from '@/shared/utils/html-text';
|
||||
import { parseKstTimestamp } from '@/shared/utils/kst';
|
||||
import { parseOpeningHours } from '@/shared/utils/opening-hours';
|
||||
import type { ParsedOpeningHours } from '@/shared/utils/opening-hours';
|
||||
import { IMAGE_LICENSES } from '../place.constants';
|
||||
import { IMAGE_LICENSES, TOUR_WRONG_LOCATION_IDS } from '../place.constants';
|
||||
import type {
|
||||
ImageLicense,
|
||||
PlaceCategory,
|
||||
@@ -167,7 +167,19 @@ export interface TourListPlace {
|
||||
sourceModifiedAt: Date | null;
|
||||
}
|
||||
|
||||
export type SkipReason = 'not_place' | 'no_category' | 'bad_coords' | 'no_id';
|
||||
/**
|
||||
* - bad_coords: 좌표가 없거나 한국 밖
|
||||
* - coords_mismatch: 좌표가 주소의 시·도와 다른 곳(2026-11-04 '오천서원' — 주소 대구 수성구 · 좌표 포항)
|
||||
* - wrong_location: 좌표가 실제 위치가 아닌 것으로 확인한 장소(TOUR_WRONG_LOCATION_IDS)
|
||||
* 건너뛴 장소는 저장하지 않으므로, 이미 있던 장소는 목록을 끝까지 받은 뒤 숨겨진다.
|
||||
*/
|
||||
export type SkipReason =
|
||||
| 'not_place'
|
||||
| 'no_category'
|
||||
| 'bad_coords'
|
||||
| 'coords_mismatch'
|
||||
| 'wrong_location'
|
||||
| 'no_id';
|
||||
|
||||
/** 목록(areaBasedList2) 항목 1건 → place 기본 컬럼. 장소가 아니거나 쓸 수 없으면 사유를 돌려준다 */
|
||||
export function listItemToPlace(
|
||||
@@ -190,11 +202,15 @@ export function listItemToPlace(
|
||||
if (!isInKorea(lat, lng)) {
|
||||
return { skip: 'bad_coords' };
|
||||
}
|
||||
if (TOUR_WRONG_LOCATION_IDS[sourceId]) return { skip: 'wrong_location' };
|
||||
|
||||
const license = str(item, 'cpyrhtDivCd');
|
||||
const address = [str(item, 'addr1'), str(item, 'addr2')]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
// 거리·지도가 엉뚱한 곳을 가리키지 않게, 주소(없으면 법정동 코드)의 시·도 밖 좌표는 받지 않는다
|
||||
const sido = sidoCodeFromAddress(address) ?? str(item, 'lDongRegnCd');
|
||||
if (sido && !isInSido(sido, lat, lng)) return { skip: 'coords_mismatch' };
|
||||
return {
|
||||
source: 'tour',
|
||||
sourceId,
|
||||
|
||||
Reference in New Issue
Block a user