Files
wandergo/docs/design/place-db.md
T
ttipo df48ed78eb 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>
2026-11-03 18:57:58 +09:00

25 KiB
Raw Blame History

장소 DB 설계 (v1.4, 2026-11-02)

WanderGo 의 장소·축제 데이터를 어떻게 저장하고, 갱신하고, 앱에 노출하는지 정한다. 1단계(엔티티·마이그레이션)와 2·3단계(관광공사 국문·반려동물·상가정보 동기화 명령), 중복 병합, 조회 API(§11)를 구현했다 — backend/src/modules/place/, backend/src/commands/place-sync.ts, backend/src/migrations/. 이 문서와 코드가 다르면 코드가 기준이다. 바꿀 때는 이 문서도 함께 고친다.

0. 전제 (이미 정한 것)

전제 근거
장소 데이터는 공공데이터(자동 갱신) + 운영자 큐레이션만 저장한다 카카오는 ID·place_url·사용자가 입력한 글자 외 저장 금지 (docs/research/pre-dev-check/kakao_place_id.md)
카카오 응답은 LLM 에 보내지 않는다 → AI 후보는 자체 장소 DB 에서만 같은 문서
AI 추천은 큐레이션 O 장소 우선 + 관광공사 장소(보조). 확인 안 된 상가정보 매장은 추천하지 않는다 2026-11-02 합의
확인 안 된 공공데이터 장소도 검색·지도에는 노출, "영업시간 미확인 · 방문 전 확인" 2026-11-02 합의
관광공사 적재는 지역 목록·동기화 API 로 한다. 위치 검색은 반경 안 장소의 15–38%만 돌려준다 docs/research/data-check/ 정정 메모, 2026-11-02 실측
1차 지역은 대구(도심·수성못·앞산), 한국어·영어 출시, 4개 언어 전제 MVP 결정
영업시간 문장은 backend/src/shared/utils/opening-hours.ts 로 해석한다 구현·테스트 완료

1. 전체 구조

 [공공데이터 API]                         [운영자]
  관광공사 국문·영문·반려동물              큐레이션 표(CSV)
  상가정보 · 축제(searchFestival2)              │
        │  동기화 명령(일·주·분기)              │  가져오기 명령
        ▼                                       ▼
  ┌───────────┐  1:0..1  ┌────────────────┐
  │  place    │──────────│ place_curation │   ← 동기화가 절대 건드리지 않는다
  └───────────┘          └────────────────┘
     │   │ 1:N                   │
     │   └── place_i18n(영·일·중) │
     ▼                           ▼
  place_listing (뷰) — 공공 값과 큐레이션 값을 합친 "앱이 보는 값" + 노출 플래그
     │
     ▼
  검색·지도·상세 API / AI 후보 조회 / 홈 동네 추천

  festival_event — 축제·공연·행사 (홈·장소 상세의 축제 칸)
  place_sync_run — 동기화 기록

핵심은 두 가지다.

  1. 공공 원본(place)과 사람이 확인한 값(place_curation)을 테이블로 나눈다. 동기화가 아무리 덮어써도 큐레이션은 남는다.
  2. "앱이 보는 값"은 뷰 하나에서만 정한다. 큐레이션 값 > 공공 값 > 미확인 순서를 여기서만 적용한다(규칙을 서비스마다 따로 적지 않는다).
erDiagram
  place ||--o| place_curation : "확인 정보"
  place ||--o{ place_i18n : "언어별 텍스트"
  place ||--o{ festival_event : "개최 장소(선택)"
  place {
    uuid id PK
    text source "tour | store"
    text source_id "UK(source, source_id)"
    text category
    text name
    float lat
    float lng
    text status "active | hidden | gone"
  }
  place_curation {
    uuid place_id PK
    text verdict "recommend | not_recommend | pending"
    date verified_at
  }
  place_i18n {
    uuid place_id PK
    text lang PK
    text name
  }
  festival_event {
    uuid id PK
    text source_id UK
    date start_date
    date end_date
  }

2. 테이블

2.1 place — 공공데이터 장소 (출처 1건 = 1행)

컬럼 타입 설명
id uuid PK 앱·공유 링크에 노출하는 ID
source text tour(관광공사) · store(상가정보)
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 위치 검색
ldong_regn_cd, ldong_signgu_cd, adong_cd text 법정동 시도(2)·시군구(3), 행정동(상가 8자리). 지역 필터·동기화 범위
phone, homepage text 관광공사만
overview text 소개(detailCommon2)
public_hours_text, public_rest_text text 영업시간·쉬는날 원문 (유형별 필드 매핑은 tools/data-check/curation_candidates.py 의 INTRO_FIELDS)
public_hours jsonb 원문을 파서로 해석한 결과(ParsedOpeningHours). 동기화 때 다시 계산
public_pet jsonb 관광공사 반려동물 동반 정보 원문(acmpyTypeCd·acmpyPsblCpam·acmpyNeedMtr 등). 없으면 null = 미확인(불가 아님)
public_pet_policy text 위 원문을 동기화 때 요약한 값: allowed(전구역) · partial(일부구역) · null. 뷰는 이 값만 본다
public_extra jsonb 유형별 부가 정보(입장료·주차 등). 필터로 승격할 항목은 §8
image_url, image_thumb_url text firstimage / firstimage2
image_license text Type1 · Type3 · null. 화면에 출처 표시 필수
status text active · hidden(관광공사 비표출 showflag=0) · gone(다음 스냅샷에서 사라짐·폐업 추정)
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
  • 지우지 않는다. 사라진 장소는 status=gone 으로 숨긴다. 큐레이션과 코스가 그 장소를 가리키고 있을 수 있다.
  • 숙박도 저장한다(검색의 '숙소' 탭). AI 코스·큐레이션 대상에서는 뺀다.

2.2 place_curation — 운영자가 확인한 정보 (place 1 : 0..1)

큐레이션 표의 오른쪽 12칸이 그대로 옮겨 온다.

컬럼 타입 큐레이션 표 칸 설명
place_id uuid PK, FK 출처+출처ID 로 찾음
verdict text 데이트추천(O/X) recommend · not_recommend · pending
reason_ko text 추천이유 큐레이터가 직접 쓴 문장만 (리뷰 문장 복사 금지)
reason_i18n jsonb — {en: …}. 자체 작성 글이라 기계 번역 가능
hours_text, rest_text text 영업시간(확인)·휴무(확인)
hours jsonb — 위 문장을 파서로 해석한 결과
pet_policy text 반려동물(가능/일부/불가/미확인) allowed · partial · not_allowed · unknown
pet_sizes text[] 반려동물 조건 small · medium · large (디자인의 소형·중형·대형견)
pet_conditions text 반려동물 조건 "테라스석만", "켄넬 필수" 등
setting text 실내/실외 indoor · outdoor · both — 비 오는 날 실내 우선
official_url text 공식채널URL 업체 본인 채널만
verified_method text 확인방법 phone · official_channel · visit
verified_at date 확인일 화면에 "11월 2일 확인"으로 노출
verified_by text 확인자 운영자 식별자
recheck_due_at date — verified_at + 6개월(제안). 폐업 의심 시 앞당김
memo text 메모 내부용, API 로 내보내지 않는다
updated_at timestamptz
  • 카카오·네이버에서 본 정보는 어떤 칸에도 옮기지 않는다. 확인 근거는 업체 본인 채널·전화·방문뿐이다.

2.3 place_i18n — 언어별 표시 텍스트

컬럼 설명
place_id, lang PK. lang = en · ja · zh-Hans · zh-Hant
name, address, overview
origin tour_en(관광공사 영문) · machine(기계 번역) · curator
origin_id 영문 contentid (국문과 다르다)
updated_at
  • 한국어는 place 본체에 둔다.
  • 관광공사 영문은 국문과 연결 필드가 없어 좌표(50m 안) + 분류 코드로 맞춘다. 맞지 않은 영문 항목은 기록만 하고 붙이지 않는다(첫 적재 때 매칭률을 잰다).

2.4 festival_event — 축제·공연·행사

컬럼 설명
id uuid PK, source_id 유니크 관광공사 contentid
kind festival(EV01) · performance(EV02) · event(EV03)
name, start_date, end_date, lat, lng, address
place_id 개최 장소가 place 에 있으면 연결(선택)
fee_text, playtime_text, overview, image_url, image_license usetimefestival 은 요금이다(시간 아님)
i18n jsonb {en: {name, overview}}
status, last_seen_at
  • "60일 넘는 상시 프로그램 제외"는 조회할 때 적용한다(저장은 그대로).

2.5 place_sync_run — 동기화 기록

id, job(tour_kor·tour_eng·tour_pet·store·event), scope(예: 27), started_at, finished_at, fetched, inserted, updated, hidden, gone, calls(오퍼레이션별 호출 수 jsonb), errors(앞 20건), snapshot. 문의가 오면 "언제 받은 데이터인가"를 여기서 답한다.

2.6 자체 분류 (category)

검색 탭(관광지 · 축제 · 맛집·카페 · 숙소 · 산책·자연)과 AI 코스 슬롯을 이 값으로 고른다.

관광공사 대응은 「신분류체계정보 관광타입정보 연계 정의서」(docs/data-openapi/, 소분류 240개)를 보고 확정했다. 소분류 → 중분류 → 대분류 → 관광타입 순서로 찾는다. 코드는 backend/src/modules/place/sync/tour-mapper.ts 가 기준이다.

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 뷰

place LEFT JOIN place_curation 으로 만든다. API·AI 는 이 뷰만 읽는다.

값의 우선순위

항목 규칙 화면 표기
영업시간 큐레이션 hours → 공공 public_hours(해석 성공 시) → 미확인 "11월 2일 확인" / "출처 한국관광공사" / "영업시간 미확인 · 방문 전 확인"
반려동물 큐레이션 pet_policy → 공공 public_pet 있으면 해당 값 → unknown 같은 방식
실내/실외 큐레이션 setting → null

노출 플래그 (2026-11-02 합의를 그대로 옮긴 것)

플래그 조건 쓰는 곳
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 후보 조회
  • not_recommend 는 추천에서만 빠지고 검색에는 남는다.
  • 추천(O)만 표시하고 아직 확인하지 않은 상가정보 매장은 AI·홈 어디에도 쓰지 않는다(O/X 표시와 확인은 큐레이션의 서로 다른 단계다). 관광공사 장소는 확인 전이라도 2순위로 남는다.
  • 큐레이션 행이 있어도 verified_at 이 비었으면 영업시간·반려동물 값을 쓰지 않는다(확인 안 된 값을 확인된 것처럼 보이지 않게).
  • 2026-11-02 임시 DB 에서 8가지 경우(관광공사·상가 × 큐레이션 없음/추천+확인/추천만/X/사라짐, 숙소, 점포별 상이)로 규칙을 확인했다. 조회 API 를 만들 때 같은 경우를 DB 를 쓰는 e2e 테스트로 고정한다.

4. 동기화

모두 Nest 애플리케이션 컨텍스트로 도는 CLI 명령으로 만든다. API 서버 프로세스 안에서 돌리지 않는다. 운영 서버의 cron 이 부른다. 키는 backend env 의 PUBLIC_DATA_SERVICE_KEY(이미 example 에 있음). 호출 수는 place_sync_run.calls 에 남기고, 오퍼레이션별 하루 상한(개발 계정 1,000)에 닿으면 멈춘 뒤 다음 날 이어서 한다.

작업 주기 방법 대구 규모(2026-11-02)
관광공사 국문 초기 적재 1회 areaBasedList2(lDongRegnCd=27) 전체 → 항목마다 detailCommon2·detailIntro2 목록 1,193건 · 상세 오퍼레이션별 약 1,200회 → 개발 계정으로 2일
관광공사 국문 변경분 매일 areaBasedSyncList2(modifiedtime=어제) → 바뀐 것만 상세 재조회, showflag=0 → hidden 하루 수십 건 예상
반려동물 매주 detailPetTour2 contentId 없이 전체(약 9,700건, 10회) → contentid 로 붙임 10회
관광공사 영문 매주 영문 areaBasedList2 → 좌표+분류로 매칭 → place_i18n(en)
상가정보 분기 대상 행정동 storeListInDong(divId=adongCd) × 데이트 업종 → bizesId 로 upsert. 새 스냅샷에 없는 ID → gone 도심·수성못·앞산 행정동
축제·행사 매일 searchFestival2(오늘−60일 ~ 오늘+60일)
폐업 대조 분기 행안부 인허가(휴게음식점·제과점·일반음식점) 영업상태와 주소·상호로 대조 → gone 후보 활용신청 후
  • 비표출 레코드는 법정동이 비어 있으므로 비표출 확인은 지역 필터 없이 한다(매뉴얼 검토 노트).
  • 동기화는 place 만 쓴다. place_curation 은 읽지도 쓰지도 않는다.

4.1 실행 방법 (구현됨: 관광공사 국문·반려동물·상가정보)

# 개발 — 키가 있는 백엔드 컨테이너 안에서 (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 -- store                 # 상가정보 데이트 업종(대구 중구·남구·수성구, 30회 안팎). 분기마다
dc exec backend npm run place:sync -- dedupe                # 같은 가게 병합(키 불필요). tour-kor·store 가 끝날 때도 자동으로 돈다
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 을 비워 상세를 다시 받는다.
  • 상가정보는 storeListInDong(divId=signguCd, 소분류 업종 필터)로 시군구 × 데이트 업종 6개를 받는다(--signgu= 로 범위 변경). 끝까지 받은 (시군구, 업종) 묶음에서만 이번에 보이지 않은 업소를 gone(폐업 추정)으로 바꾸고, 결과가 기존의 80% 미만이면 건너뛴다. 업종이 바뀐 업소(예: 카페 → 빵집)는 같은 bizesId 로 갱신되어 그대로 노출된다. 기준년월(stdrYm)은 source_snapshot 에 남긴다.
  • 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차 필터 → 하버사인 거리로 정렬".
  • 대구 규모(수천 건)에서는 충분하다. PostGIS 는 지금 도입하지 않는다. 전국 확장으로 수십만 건이 되거나 다각형(동네 경계) 검색이 필요해지면 DB_IMAGE 를 PostGIS 이미지로 바꾸고 geography 컬럼 + GiST 인덱스를 추가한다.

6. 큐레이션 가져오기

  • MVP 는 CSV 가져오기 명령으로 시작한다. 운영자 웹 화면은 2단계.
  • 입력: data/curation/<지역>/*.csv (정렬표·시범표 형식 그대로).
  • (출처, 출처ID) → place(source, source_id) 로 찾는다. 장소가 없으면(아직 적재 전·사라짐) 건너뛰고 보고한다.
  • 값 검증: O/X, 가능/일부/불가/미확인, 실내/실외, 전화/공식채널/방문, 날짜 형식. 틀린 행은 반영하지 않고 행 번호와 함께 보고한다.
  • 먼저 미리보기(dry-run) 로 바뀔 내용을 보여 주고, 확인 후 반영한다.
  • 큐레이션 표는 운영 자산이다. 저장소에 커밋할지(공공데이터 + 우리가 확인한 사실만 담김), 구글 시트로 옮길지는 따로 정한다.

7. 다른 테이블과의 연결 (코스 설계 때 확정)

코스의 장소 한 칸(course_stop)은 둘 중 하나만 가진다.

  • place_id — 자체 장소 DB 의 장소
  • kakao_place_id + kakao_place_url + user_label(사용자가 직접 입력한 이름) — 카카오 검색으로 직접 추가한 장소

카카오 장소는 이름·주소·좌표를 저장할 수 없다. 지도에 어떻게 표시할지는 코스 설계 때 정한다(예: 사용자가 지도에서 직접 찍은 위치).

8. MVP 에서 하지 않는 것

  • 카카오·네이버 데이터 저장, UGC 리뷰
  • 운영자 웹 관리 화면(CSV 가져오기로 대신)
  • 상가정보 전국 적재(대상 행정동만)
  • 입장료·주차 필터의 정규화(public_extra 에 원문으로 두고, 디자인의 '무료 입장'·'주차' 필터는 큐레이션 칸 추가 또는 원문 해석으로 2차에 결정)

9. 확인이 필요한 것

항목 언제
areaBasedSyncList2 의 modifiedtime 이 "그날"인지 "그 이후"인지 변경분 동기화 첫 실행
영문 ↔ 국문 매칭률 영문 첫 적재
Type3(변경금지) 이미지를 카드에서 잘라 보여 주는 것이 '변경'인지 화면 구현 전 (공공누리 기준)
상가정보 반경·행정동 API 가 빠뜨리는 업소가 있는지 상가정보 첫 적재 (동 단위 결과와 반경 결과 비교)
큐레이션 표 보관 위치(저장소 / 구글 시트) 큐레이션 본격 시작 전

10. 구현 순서 (제안)

  1. ✅ 엔티티 + 마이그레이션: place, place_curation, place_i18n, festival_event, place_sync_run, place_listing 뷰 (2026-11-02)
  2. ✅ 관광공사 국문 적재 명령(대구) + 반려동물 동기화 + 현황 명령 (2026-11-02, 실제 적재는 개발 계정으로 2일)
  3. ✅ 상가정보 적재 명령(대구 중구·남구·수성구 데이트 업종) (2026-11-02)
  4. 큐레이션 CSV 가져오기 명령(미리보기 → 반영)
  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.