Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,30 @@
| API 문서 | https://api.edenapi.org/docs | 운영 VPS |
| OpenAPI | https://api.edenapi.org/openapi.json | 운영 VPS |

## API 완성도 현황 (2026-09-17, API v0.3.0)

공개 엔드포인트 8개를 "설계한 데이터를 실제로 수집·조합·가공해서 내는가" 기준으로 점검한 결과입니다. 응답에 하드코딩된 값은 없습니다. 아래 "미제공"은 어떤 공개 원천도 그 값을 주지 않아 항상 `null`인 필드이며, 가용성 판정에서 제외되어 있습니다. 목록과 사유는 [api/README.md의 Fields That Are Not Provided](api/README.md#fields-that-are-not-provided), 보류한 결정은 [api/KNOWN_GAPS.md](api/KNOWN_GAPS.md)에 있습니다.

| 엔드포인트 | 상태 | 제공하는 것 | 미제공 / 제한 |
| --- | --- | --- | --- |
| `GET /v1/trends` | 완성 (수집 범위 안에서) | YouTube 검색 표본(5개 시장 × 3 키워드), KTO 관광자원 수요 지수(한국어 관광지명·지역 필터), NAVER 검색 트렌드(지역 여행 키워드 18개, 2026-09-17 활성화) | 사전 수집된 키워드만 응답. `destination_searches`, `sns_mentions`(Instagram·Facebook·Reddit은 외부 승인 필요) |
| `GET /v1/regions/{area_code}/insights` | 완성 (시도 단위) | 일별 방문자(내·외국인, 원천 발표 지연 약 30일), 관광 체류·소비 강도, 국적 다양성, `compare=previous_period`로 증감률 | 시군구 코드는 시도 자료로 대체. `avg_stay_nights`, `age_index`는 원천 없음 |
| `GET /v1/visitors/timeseries` | 완성 (시도 단위, 일·주·월) | 방문자 시계열과 요약 | `concentration_rate`는 원천 없음. `attraction_name`(관광지별)은 원천이 없어 항상 unavailable |
| `GET /v1/forecasts/visitors` | 완성 | 시군구: 관광지별 KTO 공식 집중률의 평균(`sample_count` 표시). 시도: 과거 동일 요일 참고 지수. 기상청 단기예보, 축제, 공휴일 | `expected_visitors`, `confidence`는 원천 없음 |
| `GET /v1/markets/inbound` | 완성 | 월별 방한객(발표 지연 약 2개월), 국가별 도착 운항편·여객 수(2026-09-17 추가), 7일 운항 일정, 환율, 관광수지, YouTube 관심도 | `social_interest.youtube.score`는 설계상 국가 신호로 쓰지 않음. Instagram·Facebook·Reddit 없음 |
| `GET /v1/markets/{country}/alerts` | 완성 (한국어) | K-ETA 공지, KTO 시장 동향, 5개 공관 안전 공지(원문·링크) | 번역·요약은 하지 않기로 결정. `language=en`은 한국어 원문을 `fallback=true`로 반환 |
| `GET /v1/places/{content_id}` | 완성, 수집 채우는 중 | 한국어 제목·분류·주소·좌표, 소개문, 영어·일본어·중국어(간체) 제목·주소, 중심 관광지 순위, 연관 관광지, 주변 상권 | 2026-09-17 배포 후 소개문(약 8일), 번역(약 6일), 허브·연관 매핑(이름·좌표가 일치하는 곳만), 주변 상권(약 1주)이 순환 수집으로 채워짐. 중국어 번체(TW)는 원천 없음 |
| `POST /v1/recommendations/destinations` | 완성 | 테마·지역 수요·계절 혼잡도 점수, 근거, 연관 관광지 | `estimated_budget_krw`, `budget_krw`, `days`, `party_size`, 접근성·이동시간 조건은 원천 없음(입력은 `unapplied_inputs`로 알림) |

수집 원천 36개 중 26개가 켜져 있습니다. 꺼진 10개(Instagram·Facebook·Reddit·X·TikTok·Weibo·Douyin·Xiaohongshu·LINE·관광지 입장객)는 어댑터가 없거나 외부 승인이 필요합니다.

## 디렉터리

```text
.agents/ 작업 기록과 로컬 자료, Git 제외
.ops/ run.sh와 deploy.sh, Git 제외
dashboard/ React + TypeScript + Vite
api/ FastAPI 코드, 마이그레이션, 테스트, Python 의존성
api/ FastAPI 코드, 마이그레이션, 테스트, Python 의존성 (README, CHANGELOG, KNOWN_GAPS 포함)
.env 공통 환경 설정 원본, Git 제외
.gitignore
README.md
Expand Down
11 changes: 10 additions & 1 deletion api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# 변경 기록

## 미배포
## 0.3.0 (2026-09-17)

운영 배포 release `20260917T014213Z`. 이 판의 목표는 공개 API 8개가 설계한 데이터를 실제로 수집·조합해 내도록 만드는 것이다. 엔드포인트별 현황은 저장소 루트 README의 "API 완성도 현황"에 있다.

- NAVER 검색 트렌드(SRC_NAVER_TREND)를 켰다. NAVER는 한국어 검색만 답하므로 시장별 외국어 키워드 대신 지역 여행 키워드 18개("한국 여행", "서울 여행", …)를 하루 5개씩 순환 수집하고, 국내 검색 관심은 특정 방한시장에 속하지 않으므로 country 없이 저장한다. 트렌드 응답은 social_sources를 지정하지 않은 요청에서 한국어 키워드의 `search_ratio`를 NAVER로 채운다. 운영 반영에는 `NAVER_STORAGE_POLICY_APPROVED=true` 설정이 필요하다.
- 방한시장의 `passengers`를 채운다. 인천공항 국가별 항공통계 서비스의 여객 오퍼레이션(getTotalNumberOfPassenger)을 운항편 오퍼레이션과 함께 수집하고, 같은 국가·월의 관측에 운항편 수와 여객 수를 합쳐 저장한다. 여객 통계를 수집하기 전 달은 null로 남는다.
- 시군구 방문 전망이 공식 예측 없이 unavailable로 나오던 것을 고쳤다. KTO 공식 예측은 관광지 단위이므로 지역 요청은 그 지역 관광지들의 공식 집중률 평균을 `official`로 내고, `sample_count`에 평균에 쓴 관광지 수와 `basis`에 근거를 표시한다. 관광지명을 지정한 요청과 시도 참고 전망은 그대로다.
- 트렌드가 YouTube 고정 키워드 15개 외에는 항상 unavailable이던 것을 보완했다. 이미 수집 중인 KTO 관광자원 수요 지수(관광지명·지역별 월간, 0~100)를 관측이 있을 때 응답에 포함해 한국어 관광지명 키워드와 `area_code` 필터가 동작한다. `area_code`가 있으면 meta.sources에도 이 원천을 표시한다.
- KTO 중심 관광지·연관 관광지가 TourAPI 관광지에 붙지 않아 관광지 상세의 `hub`, `related_places`와 추천의 `related_places`가 항상 비어 있던 문제를 고쳤다. 두 KTO 원천은 TATS 코드로, 상세 API는 TourAPI content id로 관광지를 식별하는데, 같은 시도 안에서 정규화한 한국어 이름이 유일하게 일치하고 좌표가 있으면 1km 안에서 일치할 때만 같은 관광지로 본다(좌표만으로는 합치지 않는다). 새로 수집되는 허브·연관 행은 TourAPI 관광지에 바로 붙고, 이미 만들어진 TATS 관광지 행은 스케줄러 잡(`eden:place:crosswalk`, 6시간마다 최대 3,000건)이 canonical로 이어 붙이며 읽기 경로는 이어 붙인 행의 관계를 함께 조회한다.
- 2026-09-11에 껐던 관광지 원천 5개(TourAPI 영어·일본어·중국어 간체, KTO 중심 관광지, KTO 연관 관광지)를 다시 켰다. 언어별 카탈로그는 한국어 카탈로그로 고른 essential 관광지의 번역만 붙이고 새 관광지를 만들지 않는다(시도 3개씩 순환, 새 관광지 한도 0). 레지스트리의 enabled는 코드의 원천 범위를 따르며 스케줄러가 시작할 때 맞춘다.
- 관광지 상세의 `overview`가 항상 null이던 문제를 고쳤다. TourAPI 목록(areaBasedList2)에는 소개문이 없으므로, essential 관광지 중 소개문이 없는 곳이 있으면 TourAPI 실행이 시도 목록 대신 상세(detailCommon2)를 한 실행에 60곳씩 수집해 한국어 소개문을 채운다. 원천에 소개문이 없는 관광지는 빈 값으로 표시해 다시 요청하지 않으며 API에서는 null로 낸다. 목록 갱신이 저장된 소개문을 지우지 않는다.
- 원천이 없어 영구 null인 필드(지역 인사이트의 avg_stay_nights·age_index, 방한시장의 passengers, 추천의 estimated_budget_krw) 때문에 세 엔드포인트가 항상 partial이던 판정을 바꿨다. 이 필드들은 "미제공"으로 문서화하고 가용성 판정에서 제외하며, 응답 구조와 값(null)은 그대로다. 미제공 필드 목록은 README의 "Fields That Are Not Provided"에 있다.
- 주변 상권 수집이 6시간마다 관광지 5곳만 돌아 essential 관광지 480곳 중 298곳에 1km 안 상권 행이 없고 한 바퀴에 약 24일이 걸리던 것을, 한 실행에 20곳(응답 약 25KB씩, 런당 요청 예산 22)으로 늘려 일주일 안에 채우도록 했다. 주기와 반경, 저장 방식은 그대로다.
- 시군구 방문 전망의 `holiday`가 항상 null이던 문제를 고쳤다. 공휴일 행은 시도에만 기록되므로 시군구 게시본이 날씨와 같은 방식으로 부모 시도의 공휴일 행을 상속한다.
Expand Down
23 changes: 16 additions & 7 deletions api/KNOWN_GAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,43 @@

## 팀 결정이 필요한 항목

- **관광지 원천 5개 재활성화** (`SRC_TOUR_EN`, `SRC_TOUR_JA`, `SRC_TOUR_ZH_CN`, `SRC_KTO_PLACE_HUB`, `SRC_KTO_PLACE_RELATED`): 2026-09-11 범위 결정으로 `DISABLED_SOURCES`에 있다. DB에는 언어별 번역 약 9천 건과 관계 32만 행이 남아 있어 있는 관광지는 그대로 조회된다. 허브·연관은 KTO_TATS 아이디 체계라 TourAPI(KTO_CONTENT) 관광지와 잇는 매핑 코드 없이는 켜도 `hub`, `related_places`가 채워지지 않는다.
- **관광지 소개문(`overview`)**: TourAPI 목록 API(areaBasedList2)만 호출하며 상세 API(detailCommon)는 호출하지 않는다. 채우려면 essential 관광지당 요청 1회가 추가된다.
- ~~**관광지 원천 5개 재활성화**~~: 2026-09-17 결정으로 다시 켰고, KTO_TATS↔TourAPI 매핑(`app/normalization/place_crosswalk.py`)을 구현했다. 이름이 같은 시도 안에서 유일하지 않거나 좌표가 1km 넘게 어긋나는 행은 매핑하지 않으므로 일부 관광지는 `hub`, `related_places`가 계속 비어 있을 수 있다. 매핑률은 배포 후 운영 DB에서 확인한다.
- ~~**관광지 소개문(`overview`)**~~: 2026-09-17 결정으로 구현했다. essential 관광지의 소개문을 detailCommon2로 한 실행에 60곳씩 수집한다(무료 쿼터 1,000/일 안).
- **공지 번역·요약**: `ALERT_ENRICHMENT_BATCH_SIZE=0`으로 유료 LLM 보강이 꺼져 있다. 켜면 시간당 2건, 하루 최대 48건 처리한다.
- **시군구 방문 전망**: 6055a3e 이후 지역 요청은 특정 관광지의 집중률을 지역 값으로 쓰지 않는다. KTO 예측 행은 모두 관광지 단위라 시군구 요청은 공식 예측 없이 `unavailable`이 되고, 시도 요청은 과거 동일 요일 참고값을 쓴다. 시군구 단위 공식 값을 내려면 관광지 집중률의 집계 규칙(예: 평균)을 제품으로 정해야 한다.
- **트렌드의 KTO 관광자원 수요**: `SRC_KTO_RESOURCE_DEMAND` 관측 7천여 행이 social_signal 게시본에 들어가지만 뷰가 선택하지 않는다(`ALWAYS_INCLUDED_SOURCES` 상수가 정의만 되고 미사용). 지역 필터(`area_code`)가 항상 unavailable인 이유의 절반이다.
- ~~**시군구 방문 전망**~~: 2026-09-17 결정. 지역 요청은 지역 내 관광지들의 공식 집중률 평균을 `official`로 내고 `sample_count`와 `basis`로 근거를 밝힌다.
- ~~**트렌드의 KTO 관광자원 수요**~~: 2026-09-17 결정. 관측이 있으면 응답에 포함되어 한국어 관광지명 키워드와 `area_code` 필터가 동작한다. YouTube 키워드 범위(15개 고정)는 그대로다.
- **대시보드 파라미터**: `compare=previous_period`(기간 대비 증감률)와 `constraints.avoid_crowds`(계절 반영)를 프런트가 보내지 않는다. 백엔드는 준비돼 있다.

## 외부 승인이나 원천 부재로 채울 수 없는 필드

| 엔드포인트 | 필드 | 상태 |
| --- | --- | --- |
| trends | `search_ratio` | NAVER 데이터랩 전용, 소스 비활성 + 저장 정책 미승인 |
| trends, inbound | instagram / facebook / reddit 블록, `sns_mentions` | 어댑터 없음, 외부 승인 필요 |
| trends | `destination_searches` | 어떤 원천도 연결되지 않음 |
| regions/insights | `demand.avg_stay_nights`, `diversity.age_index` | KTO 원천에 대응 지표 없음, 정규화가 None 고정 (insights는 이 때문에 항상 partial) |
| visitors/timeseries | `concentration_rate`, `attraction_name` 경로 | 작성 경로 없음, `SRC_TOURISM_ADMISSION`은 HTTP 전용이라 어댑터가 unavailable |
| forecasts/visitors | `expected_visitors`, `confidence`, `adjustment_factors` | 원천 없음, `formulas.adjusted_forecast`는 호출되지 않음 |
| markets/inbound | `passengers`, `social_interest.youtube.score` | 공항공사 월별 자료에 여객 수 없음(DB 245행 모두 null), YouTube는 설계상 국가 신호에서 제외 |
| markets/inbound | `social_interest.youtube.score` | YouTube는 설계상 국가 신호에서 제외. `passengers`는 2026-09-17 인천공항 국가별 여객 오퍼레이션(getTotalNumberOfPassenger)을 추가해 수집한다 |
| markets/{country}/alerts | `source_scope=local`, `status=inactive`, `source_type=foreign_affairs` | 현지 기관 수집기 없음, 비활성 문서는 삭제되므로 도달 불가 |
| recommendations | `estimated_budget_krw`, `budget_krw`, `days`, `party_size`, 접근성·이동시간 조건 | 검증 원천 없음 |

**결정(2026-09-16):** 위 필드는 "미제공"으로 문서화하고(README "Fields That Are Not Provided", OpenAPI 필드 설명) 가용성 판정에서 제외한다. 필드와 응답 구조는 그대로 두어 대시보드와 클라이언트는 영향을 받지 않는다. 그 결과 insights·inbound·recommendations는 원천이 있는 필드가 모두 채워지면 `available`로 응답한다. 원천이 생기면 해당 필드를 채우고 이 목록에서 빼면 된다.

## B 항목 원천 조사 결과 (2026-09-17)

- **passengers**: 인천공항 국가별 항공통계 서비스(B551177/AviationStatsByCountry)의 `getTotalNumberOfPassenger`가 국가별 월간 도착·출발 여객 수를 제공한다(2026-07 기준 57개국, 라이브 확인). 같은 서비스 키로 되며 수집을 추가했다.
- **avg_stay_nights, age_index**: 관광공사 데이터랩 공개 API(AreaTarDemDsService, AreaTarDivService)는 관광체류강도·관광소비강도·관광객 다양성·소비 다양성·국제적 다양성 지수만 준다. 숙박일수와 연령 구성은 데이터랩 웹에만 있고 오픈 API에는 없다. 원천 없음 유지.
- **estimated_budget_krw**: 관광지 단위 비용 원천은 없다. 지역 단위 관광소비강도 지수(이미 수집)만 있다. 원천 없음 유지.
- **concentration_rate(시계열), expected_visitors, confidence**: 공개 원천 없음.
- **search_ratio**: 2026-09-17 NAVER 검색 트렌드(NCP API Hub)를 켰다. 외국어 시장 키워드에는 데이터가 없어 한국어 지역 여행 키워드 18개("서울 여행" 등)를 하루 5개씩 순환 수집한다. 운영에서 켜지려면 루트 `.env`에 `NAVER_STORAGE_POLICY_APPROVED=true`가 있어야 한다(저장·재게시 권리 확인 플래그).
- **destination_searches**: 절대 검색 수는 어떤 원천도 주지 않는다.

## 데이터 품질로 격리된 항목

- 축제(`SRC_FESTIVAL`) 격리 50행: 종료일이 시작일보다 앞서거나 주소가 여러 지역에 걸치는 원천 데이터다. 코드가 추측하지 않고 제외한 것이며 버그가 아니다.
- 10개 비활성 SNS 원천이 주기마다 0건 실행 기록(`ingestion_run`)을 남긴다. 무해하지만 노이즈다.

## 운영 참고

- 6055a3e에서 soak 필수 서비스와 `eden-scheduler.service` 의존성에서 `mariadb.service`를 뺐다. 운영 VPS의 MariaDB는 같은 호스트에 있으므로 부팅 순서는 `Restart=always`가 흡수하지만, soak 증거는 더 이상 MariaDB 재시작을 잡지 않는다.
- 6055a3e에서 soak 필수 서비스와 `eden-scheduler.service` 의존성에서 `mariadb.service`를 뺐다. 2026-09-17에 옮긴 새 VPS도 MariaDB가 같은 호스트에 있다. 부팅 순서는 `Restart=always`가 흡수하지만 soak 증거는 더 이상 MariaDB 재시작을 잡지 않는다.
- 2026-09-17 배포(release `20260917T014213Z`) 직후 확인: 다시 켠 원천 5개 실행 성공, 허브·연관 매핑 첫 배치 870곳. NAVER·소개문·여객 수·대사관 공지는 각 원천의 다음 실행(같은 날 오후·저녁) 이후 채워진다.
Loading