generated from ttipo/project-base
feat(backend): 장소 조회 API와 같은 가게 중복 병합 추가
검색·지도·상세 화면이 장소 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>
This commit is contained in:
+34
-4
@@ -1,7 +1,7 @@
|
||||
# 장소 DB 설계 (v1.3, 2026-11-02)
|
||||
# 장소 DB 설계 (v1.4, 2026-11-02)
|
||||
|
||||
WanderGo 의 장소·축제 데이터를 어떻게 저장하고, 갱신하고, 앱에 노출하는지 정한다.
|
||||
1단계(엔티티·마이그레이션)와 2·3단계(관광공사 국문·반려동물·상가정보 동기화 명령)를 구현했다 — `backend/src/modules/place/`, `backend/src/commands/place-sync.ts`, `backend/src/migrations/`.
|
||||
1단계(엔티티·마이그레이션)와 2·3단계(관광공사 국문·반려동물·상가정보 동기화 명령), 중복 병합, 조회 API(§11)를 구현했다 — `backend/src/modules/place/`, `backend/src/commands/place-sync.ts`, `backend/src/migrations/`.
|
||||
이 문서와 코드가 다르면 코드가 기준이다. 바꿀 때는 이 문서도 함께 고친다.
|
||||
|
||||
## 0. 전제 (이미 정한 것)
|
||||
@@ -106,6 +106,7 @@ erDiagram
|
||||
| `source_modified_at` | timestamptz | 관광공사 modifiedtime |
|
||||
| `source_snapshot` | text | 상가정보 기준년월(stdrYm) |
|
||||
| `last_seen_at` | timestamptz | 마지막 동기화에서 확인된 시각 |
|
||||
| `merged_into_id` | uuid | 같은 가게를 다른 출처가 이미 가지고 있으면 그 장소 id(상가정보 → 관광공사). 값이 있으면 검색·추천에 나오지 않는다 — §4.2 |
|
||||
| `detail_synced_at` | timestamptz | 상세(개요·영업시간)를 받은 시각. null 이면 아직 못 받았거나 원본이 바뀌어 다시 받아야 한다 — 하루 한도로 나뉜 적재를 이어서 받는 기준 |
|
||||
| `created_at`, `updated_at` | timestamptz | |
|
||||
|
||||
@@ -210,7 +211,8 @@ erDiagram
|
||||
|
||||
| 플래그 | 조건 | 쓰는 곳 |
|
||||
| --- | --- | --- |
|
||||
| `is_searchable` | `status = active` | 검색·지도·상세 |
|
||||
| `is_searchable` | `status = active` 이고 `merged_into_id` 없음 | 검색·지도·상세 |
|
||||
| `is_brand_store` | 사후면세점(`SH040300`)이고 백화점·쇼핑몰 자체가 아님 | 목록·필터에서 빼고 이름 검색(q)에만 낸다 |
|
||||
| `is_home_pick` | `verdict = recommend` **이고 `verified_at` 있음** | 홈 동네 추천 |
|
||||
| `ai_tier` | 1: `verdict = recommend` 이고 `verified_at` 있음 / 2: `source = tour` 이고 코스 분류(숙박·쇼핑 매장 제외)이며 `verdict` 가 `not_recommend` 가 아님 / null: 그 외(**상가정보 미확인 매장 포함**) | AI 후보 조회 |
|
||||
|
||||
@@ -246,6 +248,7 @@ 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 -- store # 상가정보 데이트 업종(대구 중구·남구·수성구, 30회 안팎). 분기마다
|
||||
dc exec backend npm run place:sync -- dedupe # 같은 가게 병합(키 불필요). tour-kor·store 가 끝날 때도 자동으로 돈다
|
||||
dc exec backend npm run place:sync -- status # 적재 현황·오늘 쓴 호출 수
|
||||
# 다음 날 같은 tour-kor 명령을 다시 실행하면 남은 상세를 이어서 받는다
|
||||
|
||||
@@ -264,6 +267,18 @@ node dist/commands/place-sync.js tour-kor
|
||||
- 2026-11-02 임시 DB + 가짜 상가정보 API 로 확인: 첫 적재 → 다음 분기 폐업 추정·업종 변경·신규 → 급감 시 폐업 처리 건너뜀 → 한도 0 이면 아무것도 바꾸지 않음.
|
||||
- 2026-11-02 임시 DB + 가짜 API 로 확인한 흐름: 한도 3회로 멈춤 → 같은 날 재실행 시 추가 호출 없음 → 다음 날 이어받기 → 수정 1곳만 재조회·사라진 1곳 숨김 → 반려동물 반영.
|
||||
|
||||
### 4.2 중복 병합 (같은 가게)
|
||||
|
||||
같은 가게를 관광공사·상가정보가 둘 다 가진 경우(2026-11-02 대구 약 21곳) 상가정보 쪽 `merged_into_id` 에 관광공사 장소 id 를 넣는다.
|
||||
관광공사 쪽에 영업시간·사진·소개가 있어서 그쪽을 남긴다. 규칙(`modules/place/shop-name.ts`, `sync/place-dedupe.ts`):
|
||||
|
||||
- 관광공사 쪽이 카페·식당·주점일 때만 — 박물관·공원·경기장 안의 카페("이디야커피 국립대구박물관점")를 그 시설과 합치지 않는다
|
||||
- 100m 안
|
||||
- 상호가 같거나(공백·괄호·기호 무시), 한쪽이 다른 쪽을 포함하고 짧은 쪽이 2글자 이상 — 한 글자 상호 '동'·'문' 오탐 방지
|
||||
- 후보가 여럿이면 가장 가까운 곳. 매번 전부 다시 계산한다(관광공사 장소가 숨겨지면 병합도 풀린다)
|
||||
- 상세 API 는 병합된 id 로 들어와도 병합 대상 장소를 보여 준다(공유 링크가 깨지지 않게)
|
||||
- ⚠️ 큐레이션 가져오기(4단계)는 병합된 상가정보 행을 가리키는 큐레이션을 병합 대상 장소로 옮겨야 한다
|
||||
|
||||
## 5. 위치 검색
|
||||
|
||||
- `lat`·`lng` double precision + 인덱스. 조회는 "경계 사각형으로 1차 필터 → 하버사인 거리로 정렬".
|
||||
@@ -311,5 +326,20 @@ node dist/commands/place-sync.js tour-kor
|
||||
2. ✅ 관광공사 국문 적재 명령(대구) + 반려동물 동기화 + 현황 명령 (2026-11-02, 실제 적재는 개발 계정으로 2일)
|
||||
3. ✅ 상가정보 적재 명령(대구 중구·남구·수성구 데이트 업종) (2026-11-02)
|
||||
4. 큐레이션 CSV 가져오기 명령(미리보기 → 반영)
|
||||
5. 조회 API: `GET /api/places`(검색·필터·커서 페이지네이션), `GET /api/places/{id}`, `GET /api/events`
|
||||
5. ✅ 조회 API: `GET /api/places`(검색·근처·필터·커서), `GET /api/places/{id}` + 중복 병합 (2026-11-02). `GET /api/events` 는 축제 동기화와 함께
|
||||
6. 일·주·분기 동기화 + 폐업 대조
|
||||
|
||||
## 11. 조회 API (구현됨)
|
||||
|
||||
비로그인으로 쓴다(둘러보기·검색 비로그인 허용 합의). 노출 규칙은 `place_listing` 뷰가 정하고 API 는 읽기만 한다.
|
||||
|
||||
| 엔드포인트 | 내용 |
|
||||
| --- | --- |
|
||||
| `GET /api/places` | `q`(이름) · `lat`+`lng`+`radius`(기본 1,500m, 100~20,000) · `category`(쉼표 구분) · `pet` · `indoor` · `cursor` · `limit`. 좌표가 있으면 거리순, 없으면 이름순. 키셋 커서(정렬값+id). 응답 `{items, pageInfo}` 에 `total` 포함 |
|
||||
| `GET /api/places/{id}` | 상세 — 영업시간(원문·해석 상태·출처), 반려동물(크기·조건), 이용 안내(요금·주차 등), 추천 이유(추천+확인한 곳만), 출처 표기. 병합된 id 면 대상 장소, 숨김·폐업 추정이면 RES_001 |
|
||||
|
||||
- 목록 항목의 `openNow` 는 한국 시간 기준 영업 중 여부다. 알 수 없으면 null → 화면은 "영업시간 미확인 · 방문 전 확인".
|
||||
- 사후면세점 브랜드 매장은 이름 검색일 때만 나온다(반려동물 필터 결과가 더현대 입점 매장으로 덮이던 문제).
|
||||
- 실패 코드: 검색 조건·커서·id 형식 오류 VAL_001(400), 없음 RES_001(404). 새 코드는 만들지 않았다.
|
||||
- 계약 테스트: `backend/test/place.e2e-spec.ts` — 시작할 때 마이그레이션을 적용하고 동해 좌표에 테스트 장소를 만들어 검증한다(16개).
|
||||
- 아직 없는 것: 영어 등 다국어 응답(`place_i18n`), 지도 화면용 사각형(bbox) 조회, 축제 API.
|
||||
|
||||
Reference in New Issue
Block a user