diff --git a/backend/src/modules/place/sync/festival-mapper.spec.ts b/backend/src/modules/place/sync/festival-mapper.spec.ts index 6dc3ad9..ec7bb64 100644 --- a/backend/src/modules/place/sync/festival-mapper.spec.ts +++ b/backend/src/modules/place/sync/festival-mapper.spec.ts @@ -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'), }); diff --git a/backend/src/modules/place/sync/festival-mapper.ts b/backend/src/modules/place/sync/festival-mapper.ts index b4e6a7a..97a8d66 100644 --- a/backend/src/modules/place/sync/festival-mapper.ts +++ b/backend/src/modules/place/sync/festival-mapper.ts @@ -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, diff --git a/backend/src/modules/place/sync/tour-mapper.spec.ts b/backend/src/modules/place/sync/tour-mapper.spec.ts index fce29fd..7a91ee6 100644 --- a/backend/src/modules/place/sync/tour-mapper.spec.ts +++ b/backend/src/modules/place/sync/tour-mapper.spec.ts @@ -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'), }); diff --git a/backend/src/modules/place/sync/tour-mapper.ts b/backend/src/modules/place/sync/tour-mapper.ts index 2e4995c..2dfd638 100644 --- a/backend/src/modules/place/sync/tour-mapper.ts +++ b/backend/src/modules/place/sync/tour-mapper.ts @@ -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, diff --git a/backend/src/shared/utils/image-url.spec.ts b/backend/src/shared/utils/image-url.spec.ts new file mode 100644 index 0000000..c95b314 --- /dev/null +++ b/backend/src/shared/utils/image-url.spec.ts @@ -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(''); + }); +}); diff --git a/backend/src/shared/utils/image-url.ts b/backend/src/shared/utils/image-url.ts new file mode 100644 index 0000000..34a7df1 --- /dev/null +++ b/backend/src/shared/utils/image-url.ts @@ -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)}`; +} diff --git a/docs/design/place-db.md b/docs/design/place-db.md index bc8aa50..1ce1b7c 100644 --- a/docs/design/place-db.md +++ b/docs/design/place-db.md @@ -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 음식점)으로 대신한다.