fix(backend): 원본 좌표 오류 장소 제외 · 관광공사 사진 https · KOPIS 일정변경 중복 정리 #6

Merged
ttipo merged 3 commits from fix/place-data-cleanup into main 2026-11-05 15:34:16 +09:00
7 changed files with 58 additions and 8 deletions
Showing only changes of commit 898ef04266 - Show all commits
@@ -52,8 +52,9 @@ describe('festivalItemToEvent', () => {
ldongRegnCd: '27',
ldongSignguCd: '260',
phone: '053-666-4842',
imageUrl: 'http://tong.visitkorea.or.kr/f.jpg',
imageThumbUrl: 'http://tong.visitkorea.or.kr/f_s.jpg',
// 관광공사 사진 서버는 https 로 맞춘다
imageUrl: 'https://tong.visitkorea.or.kr/f.jpg',
imageThumbUrl: 'https://tong.visitkorea.or.kr/f_s.jpg',
imageLicense: 'Type3',
sourceModifiedAt: new Date('2026-10-20T01:15:00.000Z'),
});
@@ -1,5 +1,6 @@
import { isInKorea } from '@/shared/utils/geo';
import { extractUrl, htmlToText } from '@/shared/utils/html-text';
import { preferHttps } from '@/shared/utils/image-url';
import { parseKstTimestamp } from '@/shared/utils/kst';
import { IMAGE_LICENSES } from '../place.constants';
import type { EventKind, ImageLicense } from '../place.constants';
@@ -88,8 +89,8 @@ export function festivalItemToEvent(
ldongRegnCd: str(item, 'lDongRegnCd'),
ldongSignguCd: str(item, 'lDongSignguCd'),
phone: htmlToText(item.tel),
imageUrl: str(item, 'firstimage'),
imageThumbUrl: str(item, 'firstimage2'),
imageUrl: preferHttps(str(item, 'firstimage')),
imageThumbUrl: preferHttps(str(item, 'firstimage2')),
imageLicense: (IMAGE_LICENSES as readonly string[]).includes(license ?? '')
? (license as ImageLicense)
: null,
@@ -70,8 +70,9 @@ describe('listItemToPlace', () => {
ldongRegnCd: '27',
ldongSignguCd: '110',
phone: '053-661-2194',
imageUrl: 'http://tong.visitkorea.or.kr/a.jpg',
imageThumbUrl: 'http://tong.visitkorea.or.kr/a_s.jpg',
// 관광공사 사진 서버는 https 로 맞춘다(웹에서 혼합 콘텐츠로 막히지 않게)
imageUrl: 'https://tong.visitkorea.or.kr/a.jpg',
imageThumbUrl: 'https://tong.visitkorea.or.kr/a_s.jpg',
imageLicense: 'Type1',
sourceModifiedAt: new Date('2026-10-15T00:30:00.000Z'),
});
@@ -1,5 +1,6 @@
import { isInKorea, isInSido, sidoCodeFromAddress } from '@/shared/utils/geo';
import { extractUrl, htmlToText } from '@/shared/utils/html-text';
import { preferHttps } from '@/shared/utils/image-url';
import { parseKstTimestamp } from '@/shared/utils/kst';
import { parseOpeningHours } from '@/shared/utils/opening-hours';
import type { ParsedOpeningHours } from '@/shared/utils/opening-hours';
@@ -224,8 +225,8 @@ export function listItemToPlace(
ldongRegnCd: str(item, 'lDongRegnCd'),
ldongSignguCd: str(item, 'lDongSignguCd'),
phone: htmlToText(item.tel),
imageUrl: str(item, 'firstimage'),
imageThumbUrl: str(item, 'firstimage2'),
imageUrl: preferHttps(str(item, 'firstimage')),
imageThumbUrl: preferHttps(str(item, 'firstimage2')),
imageLicense: (IMAGE_LICENSES as readonly string[]).includes(license ?? '')
? (license as ImageLicense)
: null,
@@ -0,0 +1,26 @@
import { preferHttps } from './image-url';
describe('preferHttps', () => {
it('관광공사 사진 서버의 http 주소를 https 로 바꾼다', () => {
expect(
preferHttps('http://tong.visitkorea.or.kr/cms/resource/a_image2_1.jpg'),
).toBe('https://tong.visitkorea.or.kr/cms/resource/a_image2_1.jpg');
expect(preferHttps('HTTP://Tong.VisitKorea.or.kr/a.jpg')).toBe(
'https://Tong.VisitKorea.or.kr/a.jpg',
);
});
it('이미 https 이거나 확인하지 않은 서버·빈 값은 그대로 둔다', () => {
expect(preferHttps('https://tong.visitkorea.or.kr/a.jpg')).toBe(
'https://tong.visitkorea.or.kr/a.jpg',
);
expect(preferHttps('http://www.kopis.or.kr/a.jpg')).toBe(
'http://www.kopis.or.kr/a.jpg',
);
expect(preferHttps('http://tong.visitkorea.or.kr.evil.example/a.jpg')).toBe(
'http://tong.visitkorea.or.kr.evil.example/a.jpg',
);
expect(preferHttps(null)).toBeNull();
expect(preferHttps('')).toBe('');
});
});
+18
View File
@@ -0,0 +1,18 @@
/**
* 사진 주소 정리 (순수 함수).
*/
/**
* 같은 파일을 https 로도 주는 것을 확인한 사진 서버.
* 2026-11-04 대구 관광공사 장소 사진 1,059건 중 780건은 https, 279건은 http 였다(같은 서버).
* https 페이지(웹)에서는 http 사진이 혼합 콘텐츠로 막히므로 저장할 때 https 로 맞춘다.
*/
const HTTPS_IMAGE_HOSTS = new Set(['tong.visitkorea.or.kr']);
/** 확인한 서버의 http 사진 주소를 https 로 바꾼다. 다른 서버·형식은 그대로 둔다 */
export function preferHttps(url: string | null): string | null {
if (!url) return url;
const m = /^http:\/\/([^/?#:]+)/i.exec(url);
if (!m || !HTTPS_IMAGE_HOSTS.has(m[1].toLowerCase())) return url;
return `https://${url.slice('http://'.length)}`;
}
+2
View File
@@ -199,6 +199,8 @@ erDiagram
- 좌표가 주소(없으면 법정동 코드)의 시·도 밖인 항목과, 좌표가 실제 위치가 아닌 것으로 확인한 항목(`place.constants.ts` `TOUR_WRONG_LOCATION_IDS`)도 받지 않는다.
이미 있던 장소는 목록을 끝까지 받은 뒤 숨겨진다. 2026-11-04 회귀 점검: '오천서원'(주소 대구 수성구 · 좌표 포항),
대구올레 팔공산 3·4·5코스(주소·좌표 모두 운영 사무실이라 김광석길에서 '도보 7분'으로 보였다). 실제 좌표를 확인하면 큐레이션으로 되살린다.
- 관광공사 사진 서버(`tong.visitkorea.or.kr`)의 `http://` 주소는 `https://` 로 바꿔 저장한다(`image-url.ts` `preferHttps`).
2026-11-04 대구 장소 사진 1,059건 중 279건이 http 였다(같은 서버가 https 로도 준다). 웹(https 페이지)에서는 http 사진이 막힌다.
- 2026-11-02 대구 도심 쇼핑 135곳 대부분이 `SH040300` **사후면세점**(더현대 입점 브랜드 매장)이었다. 쇼핑은 큐레이션으로만 AI 후보가 된다.
- 분류 코드가 비면 관광타입(12 관광지·14 문화시설·28 레포츠·32 숙박·38 쇼핑·39 음식점)으로 대신한다.