feat(backend): 장소·축제·공연·전시 데이터 기반(장소 DB·동기화·조회 API) #1

Merged
ttipo merged 30 commits from feat/place-db into main 2026-11-04 13:25:51 +09:00
Owner

무엇을 · 왜

MVP 의 데이터 기반이다. 카카오·네이버 장소 데이터는 저장할 수 없어서, 공공데이터·KOPIS·한국문화정보원 데이터를 자체 DB 에 받아 앱이 그 DB 를 조회하게 했다.
1차 지역은 대구이고, 축제·공연·전시는 근교인 경북까지 받는다.

  • 장소 DB: place · place_curation · place_i18n · place_sync_run 테이블과 place_listing 뷰(앱에 노출할 값·AI 후보 등급의 단일 기준), 행사·공연장 테이블, 마이그레이션 5개
  • 동기화 CLI npm run place:sync -- <작업>
    • 관광공사 국문·반려동물, 상가정보(데이트 업종), 관광공사 축제·행사, KOPIS 공연, 한국문화정보원 전시
    • 오퍼레이션별 하루 호출 한도 관리, 원본 장애 시 숨김·폐업 처리 건너뜀, 같은 가게 병합
  • 조회 API: GET /api/places · /api/places/{id} · /api/events · /api/events/{id} (거리순·커서 페이지네이션·출처 표기)
  • 영업시간 파서: 공공데이터 영업시간 문장 → 요일별 시간표
  • KOPIS 공연
    • 공연장 좌표를 주소의 시·도와 대조한다(틀린 좌표 1건 발견).
    • 포스터는 https 로 바꿔 저장한다.
    • 제목 끝의 지역 꼬리표([대구] 등)는 떼고 (앵콜) 같은 메모는 남긴다.
  • 한국문화정보원 전시
    • 전시만 받는다. 공연은 KOPIS 가 맡는다.
    • 이미지는 이용 허락을 확인하기 전이라 저장하지 않는다.
    • 다른 출처에 이미 있는 행사는 건너뛴다.
    • 같은 출처에 제목만 바꿔 두 번 등록된 전시는 하나로 합친다. 기준은 같은 장소, 같은 기간, 제목이 6글자 이상 겹치는 것이다.
  • 의존성 취약점 정리
    • 치명 4 · 높음 3 → 0. Nest 는 11.2.8 로 올리고, multer·js-yaml 은 overrides 로 올렸다.
    • 남은 36건(보통·낮음, 메이저 업그레이드로만 풀림)은 security.md 에 유예로 기록했다.
  • 그 밖에
    • e2e 스위트를 순서대로 실행(maxWorkers: 1) — 빈 DB 에서 여러 스위트가 동시에 마이그레이션하다 부딪히는 문제를 막는다. CI 도 빈 DB 를 쓴다.
    • 에러 코드 계약 테스트의 기존 타입 오류 수정
    • 착수 전 조사 문서와 공공데이터 점검·진단 도구(tools/data-check/)
    • 네이버 지도 시험 앱(spikes/)
    • .dockerignore, 개발 백엔드 메모리 2GB

설계: docs/design/place-db.md (v1.7)

확인 방법

# 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 -- status     # 적재 현황(키 불필요)
curl "http://localhost:${BACKEND_PORT}/api/events?from=2026-11-07&to=2026-11-08&lat=35.8694&lng=128.5953&radius=120000"
  • 로컬 확인
    • lint 통과(warning 0), tsc 오류 0
    • 단위 테스트 208 · e2e 42 통과. e2e 는 빈 Postgres 에서 두 번 연속 통과했다.
    • npm ci 가 npm 10 · 11.6 · 11.20 에서 모두 통과
  • 개발 DB 실제 적재
    • 관광공사 장소 1,174곳, 상가정보 3,776곳
    • 진행 중인 행사(2026-11-04 기준): KOPIS 공연 239 · 문화정보원 전시 14 · 관광공사 축제·행사 8
    • 이번 주말(11/7~8) 목록 43건: 공연 28 · 전시 9 · 축제 4 · 행사 2
  • 동기화 명령은 키가 필요하다(backend/.env.{env} 의 PUBLIC_DATA_SERVICE_KEY · KOPIS_SERVICE_KEY)

영향 범위

  • backend
  • frontend (.dockerignore 만)
  • flutter (앱 코드 변경 없음. spikes/naver_map_spike 는 별도 시험 앱)
  • docker / 인프라 (.dockerignore, 개발 백엔드 메모리 한도)
  • 규칙 문서(.claude/)
    • security.md: 유예 취약점 기록
    • nestjs 스킬: 잠금 파일 npm 버전 주의
    • testing.md: e2e 순서 실행 규칙

공유 계약 변경 없음

  • 에러 코드·응답 포맷·페이지네이션 규약은 그대로다.
  • 새 엔드포인트는 기존 {items, pageInfo} · VAL_001 · RES_001 을 따른다.

체크리스트

  • 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과
  • 주석 한국어, 매직 넘버·하드코딩 문자열 없음
  • 사용자 노출 실패는 모두 에러 코드와 함께 처리 (BusinessException / error.code 분기)
  • 에러 코드를 바꿨다면 npm run sync:error-codes 실행 후 생성 파일까지 커밋 — 해당 없음
  • 민감정보 미커밋 (*.example 값은 placeholder)
  • 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음 (API 키는 오류 메시지에서 가림)

🤖 Generated with Claude Code

## 무엇을 · 왜 MVP 의 데이터 기반이다. 카카오·네이버 장소 데이터는 저장할 수 없어서, 공공데이터·KOPIS·한국문화정보원 데이터를 자체 DB 에 받아 앱이 그 DB 를 조회하게 했다. 1차 지역은 대구이고, 축제·공연·전시는 근교인 경북까지 받는다. - **장소 DB**: `place` · `place_curation` · `place_i18n` · `place_sync_run` 테이블과 `place_listing` 뷰(앱에 노출할 값·AI 후보 등급의 단일 기준), 행사·공연장 테이블, 마이그레이션 5개 - **동기화 CLI** `npm run place:sync -- <작업>` - 관광공사 국문·반려동물, 상가정보(데이트 업종), 관광공사 축제·행사, KOPIS 공연, 한국문화정보원 전시 - 오퍼레이션별 하루 호출 한도 관리, 원본 장애 시 숨김·폐업 처리 건너뜀, 같은 가게 병합 - **조회 API**: `GET /api/places` · `/api/places/{id}` · `/api/events` · `/api/events/{id}` (거리순·커서 페이지네이션·출처 표기) - **영업시간 파서**: 공공데이터 영업시간 문장 → 요일별 시간표 - **KOPIS 공연** - 공연장 좌표를 주소의 시·도와 대조한다(틀린 좌표 1건 발견). - 포스터는 https 로 바꿔 저장한다. - 제목 끝의 지역 꼬리표(`[대구]` 등)는 떼고 `(앵콜)` 같은 메모는 남긴다. - **한국문화정보원 전시** - 전시만 받는다. 공연은 KOPIS 가 맡는다. - 이미지는 이용 허락을 확인하기 전이라 저장하지 않는다. - 다른 출처에 이미 있는 행사는 건너뛴다. - 같은 출처에 제목만 바꿔 두 번 등록된 전시는 하나로 합친다. 기준은 같은 장소, 같은 기간, 제목이 6글자 이상 겹치는 것이다. - **의존성 취약점 정리** - 치명 4 · 높음 3 → 0. Nest 는 11.2.8 로 올리고, multer·js-yaml 은 overrides 로 올렸다. - 남은 36건(보통·낮음, 메이저 업그레이드로만 풀림)은 `security.md` 에 유예로 기록했다. - 그 밖에 - e2e 스위트를 순서대로 실행(`maxWorkers: 1`) — 빈 DB 에서 여러 스위트가 동시에 마이그레이션하다 부딪히는 문제를 막는다. CI 도 빈 DB 를 쓴다. - 에러 코드 계약 테스트의 기존 타입 오류 수정 - 착수 전 조사 문서와 공공데이터 점검·진단 도구(`tools/data-check/`) - 네이버 지도 시험 앱(`spikes/`) - `.dockerignore`, 개발 백엔드 메모리 2GB 설계: `docs/design/place-db.md` (v1.7) ## 확인 방법 ```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 -- status # 적재 현황(키 불필요) curl "http://localhost:${BACKEND_PORT}/api/events?from=2026-11-07&to=2026-11-08&lat=35.8694&lng=128.5953&radius=120000" ``` - 로컬 확인 - lint 통과(warning 0), tsc 오류 0 - 단위 테스트 208 · e2e 42 통과. e2e 는 빈 Postgres 에서 두 번 연속 통과했다. - `npm ci` 가 npm 10 · 11.6 · 11.20 에서 모두 통과 - 개발 DB 실제 적재 - 관광공사 장소 1,174곳, 상가정보 3,776곳 - 진행 중인 행사(2026-11-04 기준): KOPIS 공연 239 · 문화정보원 전시 14 · 관광공사 축제·행사 8 - 이번 주말(11/7~8) 목록 43건: 공연 28 · 전시 9 · 축제 4 · 행사 2 - 동기화 명령은 키가 필요하다(`backend/.env.{env}` 의 `PUBLIC_DATA_SERVICE_KEY` · `KOPIS_SERVICE_KEY`) ## 영향 범위 - [x] backend - [ ] frontend (`.dockerignore` 만) - [ ] flutter (앱 코드 변경 없음. `spikes/naver_map_spike` 는 별도 시험 앱) - [x] docker / 인프라 (`.dockerignore`, 개발 백엔드 메모리 한도) - [x] 규칙 문서(.claude/) - `security.md`: 유예 취약점 기록 - nestjs 스킬: 잠금 파일 npm 버전 주의 - `testing.md`: e2e 순서 실행 규칙 > 공유 계약 변경 없음 > - 에러 코드·응답 포맷·페이지네이션 규약은 그대로다. > - 새 엔드포인트는 기존 `{items, pageInfo}` · `VAL_001` · `RES_001` 을 따른다. ## 체크리스트 - [x] 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과 - [x] 주석 한국어, 매직 넘버·하드코딩 문자열 없음 - [x] 사용자 노출 실패는 모두 에러 코드와 함께 처리 (`BusinessException` / `error.code` 분기) - [x] 에러 코드를 바꿨다면 `npm run sync:error-codes` 실행 후 생성 파일까지 커밋 — 해당 없음 - [x] 민감정보 미커밋 (`*.example` 값은 placeholder) - [x] 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음 (API 키는 오류 메시지에서 가림) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ttipo added 30 commits 2026-11-04 12:39:10 +09:00
장소 데이터 적재(공공데이터포털)·장소 검색(카카오)·AI 코스 생성(Claude)에
필요한 키 자리를 placeholder 로 추가한다. 카카오 응답의 저장·LLM 전송 금지
조건을 주석으로 남겨 키를 쓰는 사람이 바로 알 수 있게 한다.

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>
공공데이터 장소(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>
대구 1차 출시 지역의 공공데이터를 점검하고 큐레이션 작업표를 만드는
스크립트다. 키는 backend/.env.development 에서 읽고 출력·오류 문구에서 가린다.

- public_data_check(2).py · run_region.py: 관광공사·반려동물·축제·상가정보·
  단기예보 점검 (결과는 docs/research/data-check/)
- curation_candidates.py: 동네별 큐레이션 후보표(CSV). 관광공사는 위치 검색이
  반경 안 장소의 15–38%만 돌려줘서 시·도 지역 목록을 받아 반경을 직접 거른다
- curation_shortlist.py: 후보표에 우선순위(1~4·제외)를 매기고 시범표를 만든다
  (키 불필요)

결과 CSV 는 data/ 아래에 생기며 .gitignore 대상이다. 파이썬 캐시도 무시한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
개발 전에 확정해야 할 전제(데이터 저장 가능 범위·지도 SDK·계정 리드타임)와
공공데이터 실측 결과를 남긴다.

- pre-dev-check: 카카오 장소 데이터 저장·LLM 전송 금지 조건, 지도 SDK 비교
  (네이버 권장), 스토어·위치기반서비스 신고 등 계정·승인 체크리스트
- data-openapi-review: 관광공사 국문·영문·반려동물, 상가정보, 단기예보 매뉴얼 검토
- data-check: 서울·부산·대구 점검 요약과 원자료(JSON, 키 미포함)

10-29 점검의 관광공사·반려동물 개수는 위치 검색으로 센 과소 집계였다.
2026-11-02 재측정 결과를 각 요약 맨 위에 정정 메모로 달았다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
관광공사(국문·영문·반려동물·신청 방법·분류코드), 기상청 단기예보,
소상공인 상가정보의 공공데이터포털 배포 매뉴얼 원본이다.
docs/research/data-openapi-review/ 의 검토 노트가 이 파일들을 근거로 인용한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AI 코스 생성 품질을 프롬프트·모델을 바꿀 때마다 같은 기준으로 재기 위한
62개 사례(데이트·축제·반려동물·카카오 데이터 경계·날씨·예외 입력)와
사례별 확인 항목·기대 동작이다. 대구 사례는 아직 넣지 않았다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flutter_naver_map 은 권장안이지만 유지보수 1인·최근 릴리스 공백이 있어
채택 전에 하루 시험한다. 코스 지도(마커·경로선), 핀 1,000개 묶음,
지명 언어, 한 화면에 지도 두 개, 지도 화면 열고 닫기 20번의 5개 화면으로
우리 요구와 안정성을 확인한다. NCP Client ID 는 코드에 두지 않고
--dart-define=NAVER_MAP_CLIENT_ID 로만 받는다.

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>
`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>
`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>
검색·지도·상세 화면이 장소 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>
대구 관광공사 원문을 실제로 적재해 보니 형식이 제각각이라 시간표를 잘못 읽는 곳이 있었다.
예: 더현대 대구 "월요일~목요일 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>
"이번 목록이 기존의 80% 미만이면 원본 장애로 보고 숨기지 않는다"는 규칙은
항목이 적은 곳(축제 몇 건, 동네 빙수집 5곳)에서 정상적인 한 건 취소도 막았다.
사라진 수를 직접 세어, 5건을 넘고 기존의 20% 도 넘을 때 또는 기존이 전부
사라졌을 때(빈 응답)만 원본 장애로 본다. 관광공사·상가정보 동기화가 함께 쓴다.

상가정보는 업종이 바뀐 업소(카페 → 빵집)를 사라진 것으로 세지 않도록
묶음별 개수 대신 이번에 본 업소 id 전체와 비교한다.

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>
dev 는 파일 감시(폴링)와 타입 검사(nest start --watch)가 함께 돌아 운영보다 무겁다.
base 의 1GB 로는 여러 파일을 연달아 저장할 때 두 번(2026-11-02·03) 메모리 부족으로
종료됐다(OOMKilled). 운영 한도는 그대로 두고 dev 오버라이드에서만 올린다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
대구 축제·행사가 90일 동안 1건만 잡힌 이유가 데이터 부족인지 조회 조건 때문인지
가르려고 만들었다(지역+종료일 / 지역만 / 전국 비교, searchFestival2 5~6회).
2026-11-03 결과: 조건 문제는 없고(코드 빈 행사 0건) 등록된 대구 행사가 실제로 적었다.

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>
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건) 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·한국문화정보원·대구문화예술진흥원 이용 조건·필드·한도, 사용자 확인(KOPIS 저장·포스터 재사용 가능), 2026-11-03 진단 결과
- place-db.md v1.7: 여러 출처 스키마(§2.4), KOPIS 동기화 순서·좌표 검사·호출량·알려진 한계(§12.1), 다음 출처(§12.2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KOPIS(https·조회 기간·건수·시설 좌표·afterdate)와 한눈에보는문화정보(건수·분야·좌표 채움)를
구현 전에 실제로 불러 확인한다. 키는 backend env 에서 읽고 출력하지 않는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
순회 공연은 제목 끝에 "[대구]"·"[대구 북구]"·"[안동] (11월)" 같은 지역 표시가 붙어 온다
(대구·경북 244건 중 203건). 앱 목록에서는 군더더기라 저장할 때 뗀다.
꼬리표 안이 지역 이름과 괄호 메모로만 되어 있을 때만 떼고, "(앵콜)" 같은 메모는 남긴다.

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>
- security.md: 남은 36건(glob·minimatch 보통, deepmerge·jest 낮음)의 도달 가능성·유예 이유·다시 볼 조건,
  multer ^2.5.0 · swagger 아래 js-yaml override 근거
- nestjs 스킬: 컨테이너 npm 11.20 이 지운 선택 peer 항목 때문에 CI(npm 10)의 npm ci 가 깨질 수 있다 —
  npx npm@10 ci --dry-run 으로 확인하는 절차

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>
전시는 관광공사·KOPIS 어디에도 없다. 한국문화정보원 한눈에보는문화정보(대구 90일 전시 13건)를
place:sync -- kcisa 로 매일 받는다.

- 분야가 '전시'인 것만 받는다(공연은 KOPIS 와 겹친다)
- 관광공사·KOPIS 에 이름 비교 키가 같고 기간이 겹치는 행사가 있으면 넣지 않는다(event-name.ts)
- 상세는 새 전시와 7일 지난 전시만(원본에 수정일이 없다), 좌표는 목록의 gpsY(위도)·gpsX(경도)
- 이미지는 이용 조건(공공누리 1유형 여부)을 확인하기 전이라 저장하지 않는다
- htmlToText 가 따옴표·가운뎃점·번호 엔티티도 푼다 — 제목이 &amp;lsquo; 처럼 이중 인코딩되어 온다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
place-db.md §12.1 에 지역 꼬리표 제거 규칙, §12.2 에 전시 동기화 순서·중복 판단·이미지 보류를 적었다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
한국문화정보원에 같은 전시가 제목만 다르게 두 번 등록되어 목록에 2건으로 나왔다
("문준호 전 - 남겨진 것들을 위한 풍경" / "문준호 《남겨진 것들을 위한 풍경》", 같은 갤러리·같은 기간).

같은 장소 · 같은 시작·종료일 · 제목 비교 키가 연속 6글자 이상 겹치면 같은 전시로 보고
먼저 등록된 것만 남긴다. 장소·기간만으로 합치지 않는 이유는 여러 전시실에서 같은 날
여러 전시를 여는 곳이 있어서다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(backend): e2e 스위트를 순서대로 실행
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m48s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 11m39s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m33s
CI / Backend (lint · test · e2e · build) (push) Successful in 12m27s
CI / Frontend (lint · type-check · test) (push) Successful in 11m0s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m46s
170be20b6d
빈 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>
ttipo changed title from chore(backend): 공공데이터·카카오·Claude API 키 환경변수 예시 추가 to feat(backend): 장소·축제·공연·전시 데이터 기반(장소 DB·동기화·조회 API) 2026-11-04 13:22:45 +09:00
ttipo merged commit 170be20b6d into main 2026-11-04 13:25:51 +09:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ttipo/wandergo#1