feat(backend): 관광공사 장소·반려동물 동기화 명령 추가

`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>
This commit is contained in:
2026-11-02 16:46:43 +09:00
parent cd089f76fb
commit 9775d6de45
27 changed files with 1993 additions and 18 deletions
+44 -16
View File
@@ -1,7 +1,7 @@
# 장소 DB 설계 (v1.1, 2026-11-02)
# 장소 DB 설계 (v1.2, 2026-11-02)
WanderGo 의 장소·축제 데이터를 어떻게 저장하고, 갱신하고, 앱에 노출하는지 정한다.
1단계(엔티티·마이그레이션)를 구현했다 — `backend/src/modules/place/`, `backend/src/migrations/1793597729768-AddPlaceTables.ts`.
1단계(엔티티·마이그레이션)와 2단계(관광공사 국문·반려동물 동기화 명령)를 구현했다 — `backend/src/modules/place/`, `backend/src/commands/place-sync.ts`, `backend/src/migrations/`.
이 문서와 코드가 다르면 코드가 기준이다. 바꿀 때는 이 문서도 함께 고친다.
## 0. 전제 (이미 정한 것)
@@ -88,6 +88,7 @@ erDiagram
| `source_id` | text | contentid / bizesId. **`(source, source_id)` 유니크** — 동기화·큐레이션 가져오기의 연결 키 |
| `category` | text | 자체 분류(§2.6). 검색 탭·AI 슬롯에 쓴다 |
| `source_category` | text | 원본 코드(lclsSystm3 / indsSclsCd) — 분류 규칙을 바꿔도 다시 계산할 수 있게 |
| `source_type` | text | 관광공사 관광타입(contenttypeid). 소개정보(detailIntro2) 조회에 필요하다 |
| `name`, `branch_name` | text | 한국어 이름, 지점명(상가정보 brchNm) |
| `address_road`, `address_jibun` | text | |
| `lat`, `lng` | double precision | WGS84. §5 위치 검색 |
@@ -105,6 +106,7 @@ erDiagram
| `source_modified_at` | timestamptz | 관광공사 modifiedtime |
| `source_snapshot` | text | 상가정보 기준년월(stdrYm) |
| `last_seen_at` | timestamptz | 마지막 동기화에서 확인된 시각 |
| `detail_synced_at` | timestamptz | 상세(개요·영업시간)를 받은 시각. null 이면 아직 못 받았거나 원본이 바뀌어 다시 받아야 한다 — 하루 한도로 나뉜 적재를 이어서 받는 기준 |
| `created_at`, `updated_at` | timestamptz | |
- **지우지 않는다.** 사라진 장소는 `status=gone` 으로 숨긴다. 큐레이션과 코스가 그 장소를 가리키고 있을 수 있다.
@@ -172,19 +174,25 @@ erDiagram
검색 탭(관광지 · 축제 · 맛집·카페 · 숙소 · 산책·자연)과 AI 코스 슬롯을 이 값으로 고른다.
| category | 관광공사(lclsSystm1 기준) | 상가정보 | 검색 탭 |
| --- | --- | --- | --- |
| `nature` | NA | — | 산책·자연 |
| `attraction` | HS, VE(문화시설 제외) | — | 관광지 |
| `culture` | VE07(문화시설) | — | 관광지 |
| `leisure` | LS | — | 관광지 |
| `food` | FD(카페 제외) | 그 외 음식 업종(지금은 적재하지 않음) | 맛집·카페 |
| `cafe` | FD05 | I21201 카페 · I21001 빵/도넛 · I21008 아이스크림/빙수 · I21005 샌드위치/샐러드 | 맛집·카페 |
| `bar` | — | I21104 요리 주점 · I21103 생맥주 | 맛집·카페 |
| `shopping` | SH | — | 관광지 |
| `lodging` | AC | — | 숙소 |
관광공사 대응은 「신분류체계정보 관광타입정보 연계 정의서」(`docs/data-openapi/`, 소분류 240개)를 보고 확정했다.
**소분류 → 중분류 → 대분류 → 관광타입** 순서로 찾는다. 코드는 `backend/src/modules/place/sync/tour-mapper.ts` 가 기준이다.
- 위 대응은 2026-11-02 표본에서 본 코드로 만든 것이다. `lclsSystmCode2` 로 전체 코드표를 받아 확정한다.
| category | 관광공사 | 상가정보 | 검색 탭 | AI 2순위 |
| --- | --- | --- | --- | --- |
| `nature` | NA 자연관광 전체 · VE03 도시공원 · VE040300 둘레길 | — | 산책·자연 | O |
| `attraction` | HS 역사관광 · VE01 랜드마크 · VE02 테마공원 · VE04 골목길·문화거리·마을 · VE05 관광단지 | — | 관광지 | O |
| `culture` | VE06 공연장·영화관 · VE07 박물관·전시관·미술관 · VE12 서점·기타문화시설 | — | 관광지 | O |
| `leisure` | LS 레저스포츠 · EX 체험관광(공예·템플스테이·웰니스) · VE10 경기장·스포츠센터 | — | 관광지 | O |
| `food` | FD 음식(아래 제외) | 그 외 음식 업종(지금은 적재하지 않음) | 맛집·카페 | O |
| `cafe` | FD05 카페·찻집 · FD030100 제과 | I21201 카페 · I21001 빵/도넛 · I21008 아이스크림/빙수 · I21005 샌드위치/샐러드 | 맛집·카페 | O(관광공사만) |
| `bar` | FD04 바·펍·주점 | I21104 요리 주점 · I21103 생맥주 | 맛집·카페 | — |
| `shopping` | SH 쇼핑(백화점·쇼핑몰·시장·사후면세점…) | — | 관광지 | — |
| `lodging` | AC 숙박 | — | 숙소 | — |
| `other` | VE08 연회장 · VE09 문화원·도서관·학교 | — | (전체에만) | — |
- 저장하지 않는 것: EV 축제·공연·행사(→ `festival_event`), 관광타입 25 여행코스, 좌표가 없거나 한국 밖인 항목.
- 2026-11-02 대구 도심 쇼핑 135곳 대부분이 `SH040300` **사후면세점**(더현대 입점 브랜드 매장)이었다. 쇼핑은 큐레이션으로만 AI 후보가 된다.
- 분류 코드가 비면 관광타입(12 관광지·14 문화시설·28 레포츠·32 숙박·38 쇼핑·39 음식점)으로 대신한다.
## 3. "앱이 보는 값" — `place_listing` 뷰
@@ -230,6 +238,27 @@ erDiagram
- 비표출 레코드는 법정동이 비어 있으므로 비표출 확인은 지역 필터 없이 한다(매뉴얼 검토 노트).
- 동기화는 `place` 만 쓴다. `place_curation` 은 읽지도 쓰지도 않는다.
### 4.1 실행 방법 (구현됨: 관광공사 국문·반려동물)
```bash
# 개발 — 키가 있는 백엔드 컨테이너 안에서 (dc = docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development)
dc exec backend npm run migration:run # 스키마 먼저
dc exec backend npm run place:sync -- tour-kor # 대구(27) 목록 → 상세. 한도에 닿으면 멈춘다
dc exec backend npm run place:sync -- tour-pet # 반려동물 동반 정보(약 10회)
dc exec backend npm run place:sync -- status # 적재 현황·오늘 쓴 호출 수
# 다음 날 같은 tour-kor 명령을 다시 실행하면 남은 상세를 이어서 받는다
# 운영 — cron
node dist/commands/place-sync.js tour-kor
```
- 한도: `TOUR_API_DAILY_LIMIT`(비우면 1,000) − 여유분 20. 오늘(KST) `place_sync_run` 에 기록된 호출부터 센다.
같은 키를 점검 스크립트 등 다른 곳에서 쓴 호출은 기록에 없으므로, 서버가 한도 초과(코드 22)로 거부하면 그때 멈춘다.
- 상세는 nature → attraction → culture → cafe → food … 순서로 받는다(이틀에 나뉘어도 앱에 먼저 필요한 것부터).
- 목록을 끝까지 받았을 때만 사라진 장소를 숨기고, 목록이 기존 노출 장소의 80% 미만이면 원본 장애로 보고 숨기지 않는다.
- 원본 수정일(modifiedtime)이 바뀐 장소는 `detail_synced_at` 을 비워 상세를 다시 받는다.
- 2026-11-02 임시 DB + 가짜 API 로 확인한 흐름: 한도 3회로 멈춤 → 같은 날 재실행 시 추가 호출 없음 → 다음 날 이어받기 → 수정 1곳만 재조회·사라진 1곳 숨김 → 반려동물 반영.
## 5. 위치 검색
- `lat`·`lng` double precision + 인덱스. 조회는 "경계 사각형으로 1차 필터 → 하버사인 거리로 정렬".
@@ -265,7 +294,6 @@ erDiagram
| 항목 | 언제 |
| --- | --- |
| `lclsSystm` 전체 코드표로 §2.6 분류 확정 | 엔티티 작성 전 (`lclsSystmCode2` 1회 호출) |
| `areaBasedSyncList2` 의 modifiedtime 이 "그날"인지 "그 이후"인지 | 변경분 동기화 첫 실행 |
| 영문 ↔ 국문 매칭률 | 영문 첫 적재 |
| Type3(변경금지) 이미지를 카드에서 잘라 보여 주는 것이 '변경'인지 | 화면 구현 전 (공공누리 기준) |
@@ -275,7 +303,7 @@ erDiagram
## 10. 구현 순서 (제안)
1. ✅ 엔티티 + 마이그레이션: `place`, `place_curation`, `place_i18n`, `festival_event`, `place_sync_run`, `place_listing` 뷰 (2026-11-02)
2. 관광공사 국문 초기 적재 명령(대구) — 개발 계정으로 2일
2. ✅ 관광공사 국문 적재 명령(대구) + 반려동물 동기화 + 현황 명령 (2026-11-02, 실제 적재는 개발 계정으로 2일)
3. 상가정보 적재 명령(대상 행정동)
4. 큐레이션 CSV 가져오기 명령(미리보기 → 반영)
5. 조회 API: `GET /api/places`(검색·필터·커서 페이지네이션), `GET /api/places/{id}`, `GET /api/events`