관광공사 장소 사진 1,059건 중 279건이 http 였다. 같은 서버(tong.visitkorea.or.kr)가 https 로도 주므로
저장할 때 https 로 바꾼다(장소·행사 모두). 앱은 http 사진도 받지만 웹(https 페이지)에서는 혼합 콘텐츠로 막힌다.
확인하지 않은 다른 서버의 주소는 그대로 둔다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
관광공사 원본 좌표가 실제 위치가 아닌 장소가 거리·지도에 엉뚱하게 나왔다(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>
장소 상세 '함께 가기 좋은 곳'은 확인 안 된 상가정보 매장을 빼야 한다(2026-11-02 합의).
앱이 가까운 30곳을 받아 거르면 도심처럼 상가가 빽빽한 곳은 받은 곳이 모두 상가라 칸이 비었다.
관광공사 장소 상세 860곳 중 30곳은 칸이 사라지고 186곳은 덜 보였다(봉산서원은 1.5km 안에
관광공사 장소가 53곳인데 0곳).
trusted=true 면 관광공사 장소와 운영자가 확인한 장소만 주고 '추천 안 함' 판정도 뺀다.
선택 조건이라 기존 호출은 그대로다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
from=2030-13-01 처럼 없는 시작일에 to 를 생략하면 끝 날짜를 계산하다 RangeError 가 나서
VAL_001(400) 대신 SYS_001(500)이 나갔다. from=9999-12-31 은 DB 가 읽지 못하는 끝 날짜를 만들었다.
날짜 계산을 shared/utils/iso-date.ts 순수 함수로 옮기고, 없는 날짜와 네 자리 연도를 넘는 결과는
null 로 알려 VAL_001 로 응답한다. 앱은 항상 두 날짜를 보내므로 사용자에게는 보이지 않았다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
관광공사 원문 107곳이 "- 11:00~21:00- 준비시간 16:00~17:00- 마지막 주문 20:20" 처럼
줄바꿈 없이 붙어 온다. 맨 앞 글머리표만 지우던 파서는 이를 한 구간으로 보고 영업시간까지
준비시간·마지막 주문으로 읽었다. 그래서 39곳이 영업 중인 낮에 '영업 종료'로 보였고
41곳은 요일 시간이 통째로 비었다(2026-11-04 회귀 점검).
- 시각·닫는 괄호 뒤의 '- '(다음이 숫자가 아닌 말)와 대괄호 머리말 앞에서 구간을 나눈다
- 시설 머리말("[칠성시장] … [칠성야시장] …")마다 시간표를 따로 만들고 요일별로 합친다.
나누기만 하면 뒤 시설의 요일 시간이 앞 시설 시간을 지워 일부 시간대가 빠졌다
- '휴게시간'은 쉬는 시간, '점심시간'은 2시간 이하일 때만 쉬는 시간으로 읽는다(식당의 점심 영업과 구분)
- 요일 사이의 '/' 는 나열로 읽고, 0분 구간("13:00~13:00")은 24시간으로 읽지 않고 버린다
개발 DB 는 reparse-hours 로 다시 해석했다(107곳 바뀜, 공공데이터 API 재호출 없음).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
빈 DB 에서 여러 e2e 스위트가 동시에 마이그레이션하면 같은 테이블을 만들다
pg_class_relname_nsp_index 중복으로 무작위 실패했다(스위트 5개에서 재현, CI 도 빈 DB 라 같은 위험).
스위트들은 한 DB 를 함께 쓰므로 maxWorkers 1 로 순서대로 돌린다. 빈 DB 두 번 연속 통과(약 30초).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
한국문화정보원에 같은 전시가 제목만 다르게 두 번 등록되어 목록에 2건으로 나왔다
("문준호 전 - 남겨진 것들을 위한 풍경" / "문준호 《남겨진 것들을 위한 풍경》", 같은 갤러리·같은 기간).
같은 장소 · 같은 시작·종료일 · 제목 비교 키가 연속 6글자 이상 겹치면 같은 전시로 보고
먼저 등록된 것만 남긴다. 장소·기간만으로 합치지 않는 이유는 여러 전시실에서 같은 날
여러 전시를 여는 곳이 있어서다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
전시는 관광공사·KOPIS 어디에도 없다. 한국문화정보원 한눈에보는문화정보(대구 90일 전시 13건)를
place:sync -- kcisa 로 매일 받는다.
- 분야가 '전시'인 것만 받는다(공연은 KOPIS 와 겹친다)
- 관광공사·KOPIS 에 이름 비교 키가 같고 기간이 겹치는 행사가 있으면 넣지 않는다(event-name.ts)
- 상세는 새 전시와 7일 지난 전시만(원본에 수정일이 없다), 좌표는 목록의 gpsY(위도)·gpsX(경도)
- 이미지는 이용 조건(공공누리 1유형 여부)을 확인하기 전이라 저장하지 않는다
- htmlToText 가 따옴표·가운뎃점·번호 엔티티도 푼다 — 제목이 &lsquo; 처럼 이중 인코딩되어 온다
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ERROR_CODES 는 코드마다 리터럴 타입이 달라 선택 속성 clientOnly 를 바로 읽으면 tsc 가 실패했다
(jest 는 타입 검사를 하지 않아 통과하고 있었다). 공통 정의 타입으로 넓혀 읽는다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm audit 47건(치명 4 · 높음 3) 중 범위 안에서 올릴 수 있는 것을 모두 올렸다.
- 컨테이너 안에서 npm audit fix: proxy-addr·piscina·handlebars·decompress·body-parser·qs 등
- Nest 11 최신판(11.2.8)으로 업데이트: 낡은 path-to-regexp 8.2.0(높음) 고정 해소
- overrides: multer ^2.5.0(2.4.0 에 DoS 취약점, platform-express 12.3.2 가 2.5.0 선언),
@nestjs/swagger 아래 js-yaml ^5.4.3(5.3.0 고정, swagger 12.1.0 이 5.5.0 선언)
컨테이너 npm 11.20 이 선택 peer 의존성 항목(chokidar 4.0.3)을 잠금 파일에서 지워
CI 와 같은 npm 10 의 npm ci 가 실패해서, 그 항목만 이전 잠금 파일에서 되살렸다.
npm 10 · 11.6 · 11.20 모두 npm ci 통과, lint · 단위 185 · e2e 39 · 빌드 · 서버 기동(/api-docs-yaml 포함) 확인.
남은 36건(보통 33 · 낮음 3)은 메이저 업그레이드로만 풀려 security.md 에 유예로 기록한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
순회 공연은 제목 끝에 "[대구]"·"[대구 북구]"·"[안동] (11월)" 같은 지역 표시가 붙어 온다
(대구·경북 244건 중 203건). 앱 목록에서는 군더더기라 저장할 때 뗀다.
꼬리표 안이 지역 이름과 괄호 메모로만 되어 있을 때만 떼고, "(앵콜)" 같은 메모는 남긴다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
대구 공연은 관광공사에 거의 없고(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>
KOPIS 공연·한국문화정보원 전시를 관광공사 행사와 같은 테이블에 받으려면
관광공사 contentid 단독 유니크로는 출처가 섞인다. (source, source_id) 로 바꾸고,
KOPIS 공연은 좌표가 공연장 상세에만 있어 event_venue 에 따로 둔다.
- 전시는 몇 달씩 하는 것이 보통이라 상시 프로그램 기간 상한을 종류별로 나눔(전시 183일)
- 관광공사 동기화는 관광공사 행사만 숨기도록 범위를 좁힘
- 조회 API 에 장르·예매 링크·출처별 표기 추가, 없는 날짜(2030-13-01)가 500 이 되던 것을 VAL_001 로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
관광공사에 등록된 앞으로 90일 행사가 대구 1건·경북 7건뿐이라(조회 조건 문제 아님)
홈 '이번 주말 축제·행사'가 대부분 비게 된다. 근교 행사를 거리와 함께 보여 준다.
- place:sync -- event 가 기본으로 대구(27)·경북(47)을 받는다(--regions= 로 변경,
시·도마다 실행 기록을 따로 남긴다)
- GET /api/events 반경 상한 50km → 150km(포항 약 85km·청송 약 100km). 기본 5km 는 그대로
- 설계 문서에 화면 규칙: 홈은 동네 좌표 120km 거리순(대구 먼저), 이번 주말이 비면
같은 반경 30일로 '다가오는 축제·행사'. 장소 상세 '근처'는 5km, 없으면 칸 숨김
2026-11-03 실측: 이번 주말 대구 도심 120km 안 5건(수성국제비엔날레 5km 가 맨 앞).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
홈 '이번 주말 축제·행사'와 장소 상세 '근처 축제·행사'에 쓸 데이터를 채운다.
- place:sync -- event: 관광공사 searchFestival2 를 오늘~90일 기간으로 받아
festival_event 에 upsert(서버는 기간이 겹치는 행사를 준다). 지역 조건을 보내고
응답에서도 다른 지역 행사는 거른다. 상세(소개·요금·공연 시간·장소)는 새로 생기거나
수정된 행사만 받는다. 기간 안에서 목록에 없는 행사는 숨긴다
- GET /api/events: 기간(기본 오늘~30일, 최대 92일)·근처(좌표+반경)·분류·커서.
60일 넘는 상시 프로그램과 숨김 행사는 뺀다. 항목에 오늘 진행 중 여부
- GET /api/events/{id}: 소개·공연 시간·요금·장소·홈페이지·출처
- 마이그레이션: festival_event 에 분류 코드·지역 코드·장소명·전화·홈페이지·
작은 사진·원본 수정일·상세 수신 시각 추가
e2e(test/festival.e2e-spec.ts)는 2030년 날짜·동해 좌표 픽스처라 실행 날짜와 무관하다.
2026-11-03 실측: 앞으로 90일 관광공사 행사는 전국 106건, 대구 1건, 경북 7건.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"이번 목록이 기존의 80% 미만이면 원본 장애로 보고 숨기지 않는다"는 규칙은
항목이 적은 곳(축제 몇 건, 동네 빙수집 5곳)에서 정상적인 한 건 취소도 막았다.
사라진 수를 직접 세어, 5건을 넘고 기존의 20% 도 넘을 때 또는 기존이 전부
사라졌을 때(빈 응답)만 원본 장애로 본다. 관광공사·상가정보 동기화가 함께 쓴다.
상가정보는 업종이 바뀐 업소(카페 → 빵집)를 사라진 것으로 세지 않도록
묶음별 개수 대신 이번에 본 업소 id 전체와 비교한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
대구 관광공사 원문을 실제로 적재해 보니 형식이 제각각이라 시간표를 잘못 읽는 곳이 있었다.
예: 더현대 대구 "월요일~목요일 10:30~20:00금요일~일요일 10:30~20:30" 을
월~목에 두 시간대, 금~일은 비어 있음으로 읽어 월요일 20:15 에 "영업 중"으로 나갔다.
- 줄바꿈을 구간 구분으로 본다(동기화가 <br> 을 줄바꿈으로 바꿔 저장하는데 공백으로 합쳐지고 있었다)
- 시각 바로 뒤에 요일·평일·주말·'[' 가 붙으면 구간을 나눈다(하이픈 시간 범위는 그대로)
- 괄호·대괄호 안의 '/' 로 구간을 자르지 않고, 괄호 안 브레이크타임·라스트오더를 각각 읽는다
- "[토요일]" 같은 대괄호 요일 머리말을 다음 줄들의 시간에 적용하고, "[1월]" 의 '월'은
월요일로 읽지 않는다(달마다 다르면 계절별처럼 시각 판단을 하지 않는다)
- 주중·평일 줄의 브레이크타임은 평일 전용으로 본다
- htmlToText 가 잘못 쓴 </br> 도 줄바꿈으로 바꾼다
저장된 원문을 API 재호출 없이 다시 해석하는 `place:sync -- reparse-hours` 를 추가했다
(jsonb 키 순서와 무관하게 비교). 2026-11-03 대구 1,174곳 중 124곳의 시간표를 바로잡았다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
검색·지도·상세 화면이 장소 DB 를 쓰도록 조회 API 를 연다(비로그인 허용).
- GET /api/places: 이름 검색, 좌표+반경 거리순, 분류·반려동물·실내 필터,
키셋 커서 페이지네이션(정렬값+id), 전체 개수, 항목별 "지금 영업 중"(KST)
- GET /api/places/{id}: 영업시간 원문·해석 상태·출처, 반려동물 크기·조건,
이용 안내(요금·주차 등), 추천 이유(추천+확인한 곳만), 출처 표기.
병합된 id 로 들어오면 병합 대상 장소를, 숨김·폐업 추정이면 RES_001 을 준다
- 같은 가게 병합: 관광공사 쪽이 카페·식당·주점이고 100m 안이며 상호가 같거나
포함(짧은 쪽 2글자 이상)이면 상가정보 쪽을 merged_into_id 로 관광공사 쪽에 합친다.
tour-kor·store 가 끝날 때 자동으로, place:sync -- dedupe 로 따로도 돈다
- 사후면세점 브랜드 매장(SH040300)은 목록·필터에서 빼고 이름 검색에만 낸다
(반려동물 필터 결과가 더현대 입점 매장으로 덮이던 문제)
- 공공 반려동물 조건 원문의 글머리표·줄바꿈 정리
e2e(test/place.e2e-spec.ts)는 시작할 때 마이그레이션을 적용하고 동해 좌표에
테스트 장소를 만들어 노출 규칙·필터·커서·상세·오류 코드를 검증한다.
2026-11-02 대구 실측: 같은 가게 21곳 병합, 도심 반려동물 필터 7곳.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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>
`npm run place:sync -- tour-kor|tour-pet|status` 로 관광공사 국문 장소와
반려동물 동반 정보를 장소 DB 에 적재한다. API 서버가 아니라 CLI(운영은 cron)로 돈다.
- tour-kor: 시·도 지역 목록 전체 → place upsert → 상세(개요·영업시간·요금·주차).
개발 계정 한도(오퍼레이션별 하루 1,000건)에 닿으면 멈추고 detail_synced_at 으로
다음 실행이 이어 받는다. 서버의 한도 초과 응답(코드 22)도 정상 멈춤으로 본다.
수정일이 바뀐 장소만 상세를 다시 받고, 목록에서 사라진 장소는 숨긴다
- tour-pet: detailPetTour2 전체(약 10회)를 받아 전구역/일부구역으로 요약
- 분류 대응을 「신분류체계 연계 정의서」 소분류 240개로 확정하고 'other' 분류 추가
- 키는 어떤 오류 메시지에도 남기지 않는다(원문·URL 인코딩 모두 가림)
CHECK 제약 변경은 migration:generate 가 감지하지 못해 마이그레이션에 직접 적었다.
2026-11-02 대구 실측: 목록 1,193건 → 장소 1,174곳, 상세 535곳(영업시간 해석 92%),
반려동물 정보 288곳(그중 쇼핑 매장 275곳).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
없으면 Dockerfile 의 `COPY . .` 가 폴더를 통째로 이미지에 넣는다.
Windows 호스트에서 백엔드 빌드 컨텍스트가 263MB(호스트 node_modules 포함)로
4분 넘게 걸렸고, 두 가지 문제가 있었다.
- 호스트(Windows) node_modules 가 컨테이너의 Linux 모듈을 덮어써 swc 등
네이티브 모듈이 깨질 수 있다
- backend/.env.{환경} 의 키가 이미지 레이어에 남는다(env 는 compose 가
실행할 때 넣으므로 이미지에 필요 없다)
node_modules·dist·coverage·.env*·uploads 등을 제외해 컨텍스트가 약 1MB 가 됐다.
프론트엔드는 VITE_API_BASE_URL 을 build arg 로 받으므로 결과가 같다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
공공데이터 장소(place)와 운영자 큐레이션(place_curation)을 테이블로 나눠
동기화가 사람이 확인한 값을 덮어쓰지 않게 한다. 앱이 보는 값(큐레이션 >
공공 > 미확인)과 노출 규칙은 place_listing 뷰 한 곳에서만 정한다.
- 노출 규칙(2026-11-02 합의): 노출 중인 장소는 모두 검색, 홈 추천은
추천(O)+확인일, AI 후보는 1순위 큐레이션 추천·2순위 관광공사 장소.
확인 안 된 상가정보 매장은 AI 후보가 아니다
- 축제·공연·행사(festival_event), 언어별 텍스트(place_i18n),
동기화 기록(place_sync_run) 포함
- 코드값은 place.constants.ts 하나로 CHECK 제약·뷰가 공유
- 전역 SnakeNamingStrategy(DB snake_case ↔ TS camelCase), uuid 는 pgcrypto
임시 Postgres 15 에서 생성 → 적용 → 재생성 무차이 → revert 를 확인했고,
뷰 규칙은 8가지 경우로 검증했다. 설계: docs/design/place-db.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
관광공사 영업시간·쉬는날은 "11:00~21:00 (준비 시간 15:30~17:00)",
"화~일 10:00~18:00 (월요일 휴관)", "점포별 상이"처럼 자유 문장이라
코스 영업시간 경고와 AI 시간 검증에 바로 쓸 수 없다.
요일별 시간표·준비시간·마지막 주문·정기 휴무·명절 휴무로 해석하고,
정할 수 없는 경우(점포별 상이·계절별·값 없음)는 지어내지 않고 상태로 남긴다.
테스트는 2026-10-29 공공데이터 점검에서 받은 실제 문장으로 작성했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
장소 데이터 적재(공공데이터포털)·장소 검색(카카오)·AI 코스 생성(Claude)에
필요한 키 자리를 placeholder 로 추가한다. 카카오 응답의 저장·LLM 전송 금지
조건을 주석으로 남겨 키를 쓰는 사람이 바로 알 수 있게 한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>