Skip to content
Merged

Dk #9

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
Binary file not shown.
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ SEOUL_DATA_API_KEY=
NAVER_API_KEY=

# --- 캐시 (선택) ---
REDIS_URL=

# --- API 운영 보호 ---
ENVIRONMENT=development
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ chroma_db/
*.db
*.sqlite3

# 지식그래프 빌드 산출물 (scripts/build_knowledge_graph.py로 재생성 가능)
# 사용하지 않는 과거 지식그래프 빌드 산출물
data/graph/

# IDE
Expand Down Expand Up @@ -61,3 +61,7 @@ gun/data/_checkpoint.json
gun/data/_qa/
gun/data/~$*
gun/data/*.tmp

# graphify 로컬 분석 캐시 — 재생성 가능하며 소스 그래프 JSON만 추적
graphify-out/.graphify_analysis.json
graphify-out/cache/ast/
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ EXPOSE 8000

# 컨테이너 헬스체크 — 기동 후 /health 200 확인
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health', timeout=4).status==200 else 1)"

CMD ["uvicorn", "src.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- updated: 2026-09-12 | hash: d14dcb5d | summary: POI 34,441건 반영·카카오 키워드 검색 폴백·추정 영업시간 강등·구현 현황 ⑰~⑲ 추가 -->
<!-- updated: 2026-09-17 | hash: 3320278f | summary: 웹 배포 환경변수와 컨테이너 readiness 프로브를 반영한 프로젝트 안내 -->

# 관광 일정 QA 엔진 — Explainable Travel Plan Validator

Expand All @@ -20,6 +20,18 @@ cp .env.example .env # 실제 API 키 입력
docker compose up --build
```

### 웹 배포

컨테이너를 지원하는 호스팅 서비스에서는 이 저장소의 `Dockerfile`을 그대로 사용한다. 운영 환경변수는 반드시 다음처럼 설정한다.

```text
ENVIRONMENT=production
API_AUTH_TOKEN=<강한 랜덤 토큰>
CORS_ALLOWED_ORIGINS=https://<실제-웹-도메인>
```

`API_AUTH_TOKEN`이 production에서 빠지면 API는 503으로 fail-closed하고, `CORS_ALLOWED_ORIGINS`에는 `*`를 사용하지 않는다. 컨테이너 프로브는 `/health`를 사용하며 필수 데이터가 없으면 503을 반환한다.

### 방법 B — 로컬 (Python 3.11+ 필요)

```bash
Expand All @@ -43,6 +55,8 @@ make run # 개발 서버
> `/health` 는 시크릿 노출 없이 어떤 외부 API가 연동됐는지(bool)와
> POI·혼잡도 데이터 적재 건수를 반환한다 — 연동 검증용.

> 운영 프로브는 로컬 필수 데이터 준비 상태를 검사하며, 외부 API의 실제 연결성까지 보장하는 probe는 아니다.

### 활용 공공데이터

| 데이터 | 출처 | 용도 |
Expand Down Expand Up @@ -402,7 +416,7 @@ ValidatorPipeline.run() — src/explain/pipeline.py

## 향후 발전 방향

1. **경량화** — 1회 호출 200ms 이하, Redis 캐싱 전략 적용으로 여행 앱 임베딩 가능 형태로 최적화
1. **경량화** — Kakao Mobility 결과를 로컬 JSON 캐시로 재사용해 여행 앱 임베딩 가능 형태로 최적화
2. **B2B 배포** — AI 생성 일정 자동 QA 게이트 + 검증 점수 배지 표시
3. **대중교통 통합** — 자차 단일 가정에서 지하철·버스 환승 시간 복합 이동 모델 확장
4. **체류시간 자동 보정** — EXIF 위치 메타데이터로 실제 체류 패턴 학습, 장소별 자동 보정
Expand Down
1 change: 1 addition & 0 deletions data/route_cache.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"126.5125,33.5148|126.4993,33.5190": 204, "126.5125,33.5148|126.5267,33.5175": 486, "126.5125,33.5148|126.4876,33.5183": 360, "126.5125,33.5148|126.5300,33.5170": 575, "126.5125,33.5148|126.2352,33.3835": 2880, "126.5125,33.5148|126.5092,33.5064": 411, "126.5125,33.5148|126.5042,33.5160": 150, "126.5125,33.5148|126.5270,33.5158": 350, "126.4993,33.5190|126.5300,33.5170": 764, "126.4993,33.5190|126.4876,33.5183": 157, "126.4993,33.5190|126.5092,33.5064": 290, "126.4993,33.5190|126.5042,33.5160": 101, "126.4993,33.5190|126.5267,33.5175": 744, "126.5300,33.5170|126.5267,33.5175": 201, "126.4993,33.5190|126.5270,33.5158": 664, "126.4993,33.5190|126.2352,33.3835": 2876, "126.5300,33.5170|126.4876,33.5183": 814, "126.5300,33.5170|126.5270,33.5158": 77, "126.5300,33.5170|126.5092,33.5064": 588, "126.5300,33.5170|126.5042,33.5160": 614, "126.5300,33.5170|126.2352,33.3835": 3150, "126.2352,33.3835|126.5300,33.5170": 3278, "126.2352,33.3835|126.5092,33.5064": 2832, "126.2352,33.3835|126.4876,33.5183": 2729, "126.2352,33.3835|126.5267,33.5175": 3263, "126.2352,33.3835|126.5042,33.5160": 2928, "126.5267,33.5175|126.4876,33.5183": 792, "126.2352,33.3835|126.5270,33.5158": 3200, "126.5267,33.5175|126.5042,33.5160": 593, "126.5267,33.5175|126.5092,33.5064": 565, "126.5267,33.5175|126.2352,33.3835": 3126, "126.5267,33.5175|126.5270,33.5158": 185, "126.4876,33.5183|126.5092,33.5064": 438, "126.4876,33.5183|126.5042,33.5160": 254, "126.4876,33.5183|126.5270,33.5158": 807, "126.5092,33.5064|126.5042,33.5160": 335, "126.5042,33.5160|126.5092,33.5064": 307, "126.5092,33.5064|126.4876,33.5183": 498, "126.5092,33.5064|126.5270,33.5158": 445, "126.5042,33.5160|126.5270,33.5158": 628, "126.5270,33.5158|126.5092,33.5064": 500, "126.5270,33.5158|126.5042,33.5160": 497, "126.4993,33.5190|126.4876,33.5184": 141, "126.5125,33.5148|126.4876,33.5184": 335, "126.5267,33.5175|126.4876,33.5184": 764, "126.5300,33.5170|126.4876,33.5184": 777, "126.2352,33.3835|126.4876,33.5184": 2586, "126.4876,33.5184|126.5125,33.5148": 361, "126.4876,33.5184|126.5300,33.5170": 837, "126.4876,33.5184|126.5092,33.5064": 399, "126.4876,33.5184|126.5042,33.5160": 224, "126.4876,33.5184|126.5270,33.5158": 755, "126.4876,33.5184|126.2352,33.3835": 2580, "126.5042,33.5160|126.4876,33.5184": 231, "126.5092,33.5064|126.4876,33.5184": 466, "126.5270,33.5158|126.4876,33.5184": 691}
6 changes: 4 additions & 2 deletions data_construct.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- updated: 2026-09-12 | hash: 528a7ecf | summary: NEAR_BY 캡을 반경 300m+Top-20으로 재확정(실측 밀집도 검증) -->
<!-- updated: 2026-09-16 | hash: 36027d5d | summary: 과거 그래프 DB 장소 스키마와 현재 로컬 리트리버 전환을 구분한 설계 기록 -->

# 제주 Place 지식그래프 스키마 (Head - Relation - Tail)
# 제주 Place 지식그래프 스키마 (과거 설계 기록)

> 이 문서는 외부 그래프 DB를 사용하던 과거 데이터 모델의 설계 기록이다. 현재 런타임은 `src/data/evidence_retriever.py`의 `LocalEvidenceRetriever`가 동일한 장소 CSV를 로컬에서 조회하며, 아래 그래프 DB 스키마를 직접 로드하지 않는다.

## 0. 개요

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
- .env
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"]
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health', timeout=4).status==200 else 1)"]
interval: 30s
timeout: 5s
start_period: 20s
Expand Down
48 changes: 24 additions & 24 deletions docs/ADR.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,71 @@
<!-- updated: 2026-09-12 | hash: 9b1cb461 | summary: ADR-005 Neo4j 제거 확정(Python M3/M4 대체), 제거 기술 표 갱신 -->
<!-- updated: 2026-09-16 | hash: 2427fbaf | summary: Neo4j 제거와 배포 의존성 manifest 기준을 기록한 ADR -->
# Architecture Decision Records

## 철학
MVP 속도 최우선. 로컬 실행 가능하고 별도 인프라를 최소화하는 도구 우선.
MVP 속도 최우선. 로컬 실행 가능하고 별도 인프라를 최소화하는 도구 우선.
외부 API 의존성은 명확히 격리하고, 테스트에서 mock으로 대체 가능한 구조를 선택한다.

---

### ADR-001: POI 정규화 — Kakao Local API 선택
**결정**: Kakao Local API (`/v2/local/search/keyword.json`)
**이유**: 장소명 → 정규화 좌표 변환에 가장 정확한 국내 서비스. TourAPI의 위치 데이터보다 정밀하고, 주소 기반 POI 검색 지원.
**결정**: Kakao Local API (`/v2/local/search/keyword.json`)
**이유**: 장소명 → 정규화 좌표 변환에 가장 정확한 국내 서비스. TourAPI의 위치 데이터보다 정밀하고, 주소 기반 POI 검색 지원.
**트레이드오프**: Google Places 대비 영어 지원 약함. MVP는 한국어 전용이므로 문제없음.

---

### ADR-002: 이동시간 계산 — Kakao Mobility API + 직선거리 폴백
**결정**: Kakao Mobility API (B2B) 우선, 키 없을 시 직선거리 × 속도계수 폴백
**이유**: Kakao Mobility는 실제 도로 경로/대중교통 시간을 제공하는 국내 최고 정확도 서비스. 단, B2B 신청 필요.
**결정**: Kakao Mobility API (B2B) 우선, 키 없을 시 직선거리 × 속도계수 폴백
**이유**: Kakao Mobility는 실제 도로 경로/대중교통 시간을 제공하는 국내 최고 정확도 서비스. 단, B2B 신청 필요.
**폴백 공식**: `travel_min = (직선거리_km / 속도_kmh) × 60 × 경로계수`
- transit: 속도 20km/h, 경로계수 1.3
- car: 속도 40km/h, 경로계수 1.1
- walk: 속도 4km/h, 경로계수 1.0
- car: 속도 40km/h, 경로계수 1.1
- walk: 속도 4km/h, 경로계수 1.0
**트레이드오프**: 폴백 사용 시 이동시간 정확도↓ → Confidence Level을 Medium-Low로 하향.

---

### ADR-003: 검증 방식 — 제약 기반 규칙 엔진 (vs ML 기반)
**결정**: 규칙 기반 Hard Fail/Warning 탐지
**이유**: 운영시간 충돌·이동 불가 같은 명확한 제약은 규칙으로 표현이 완전하다. ML 모델은 레이블 데이터가 없고, "왜 실패하는지" 설명이 불가능하다. Explainability가 핵심 가치이므로 규칙 기반이 필수.
**결정**: 규칙 기반 Hard Fail/Warning 탐지
**이유**: 운영시간 충돌·이동 불가 같은 명확한 제약은 규칙으로 표현이 완전하다. ML 모델은 레이블 데이터가 없고, "왜 실패하는지" 설명이 불가능하다. Explainability가 핵심 가치이므로 규칙 기반이 필수.
**트레이드오프**: 복잡한 맥락적 판단(예: "이 일정은 여행자에게 피로할 것인가")은 규칙보다 LLM에게 위임.

---

### ADR-004: LLM — Claude claude-sonnet-4-6 선택 (Explanation 전용)
**결정**: Anthropic `claude-sonnet-4-6`, prompt caching 적용
**이유**: 4단계 Evidence-based 설명 생성과 Repair 제안 문장화에 최적. 200K 토큰 컨텍스트로 전체 일정 맥락 제공 가능. `cache_control`로 시스템 프롬프트 캐싱.
**결정**: Anthropic `claude-sonnet-4-6`, prompt caching 적용
**이유**: 4단계 Evidence-based 설명 생성과 Repair 제안 문장화에 최적. 200K 토큰 컨텍스트로 전체 일정 맥락 제공 가능. `cache_control`로 시스템 프롬프트 캐싱.
**트레이드오프**: API 의존성. Validation 자체(Hard Fail/Warning/Score)는 LLM 없이 동작하므로 LLM 실패 시 부분 응답 가능.

---

### ADR-005: Graph DB — Neo4j 제거, Python 순수 구현으로 대체 (2026-04-30)
**결정**: Neo4j 제거 확정. 백트래킹·구역 재진입 탐지를 Python 규칙 기반 M3/M4 메트릭으로 완전 대체.
**이유**: `scripts/neo4j_coverage_analysis.py` 커버리지 검증 결과, Neo4j가 담당하던 7개 역할을 Python 로직이 100% 대체함을 확인.
- M3 `count_area_backtracks()`: 시군구 코드 기반 비연속 재진입, O(n) — Cypher 쿼리 완전 대체
- M4 `count_geo_cluster_backtracks()`: DBSCAN(eps=2km), Neo4j가 잡지 못하던 대형 시군구 내부 분산까지 추가 탐지
- 이동 행렬·시간 슬롯 저장 역할은 VRPTW 엔진 내부 Python 자료구조로 처리
**결정**: Neo4j 제거 확정. 백트래킹·구역 재진입 탐지를 Python 규칙 기반 M3/M4 메트릭으로 완전 대체.
**이유**: 당시 커버리지 검증 결과, Neo4j가 담당하던 7개 역할을 Python 로직이 100% 대체함을 확인.
- M3 `count_area_backtracks()`: 시군구 코드 기반 비연속 재진입, O(n) — Cypher 쿼리 완전 대체
- M4 `count_geo_cluster_backtracks()`: DBSCAN(eps=2km), Neo4j가 잡지 못하던 대형 시군구 내부 분산까지 추가 탐지
- 이동 행렬·시간 슬롯 저장 역할은 VRPTW 엔진 내부 Python 자료구조로 처리
**트레이드오프**: 그래프 시각화·복잡 관계 쿼리는 불가하나, MVP 검증 범위 내에서는 불필요.

---

### ADR-006: API 프레임워크 — FastAPI 선택
**결정**: `FastAPI` + `uvicorn`
**이유**: async 지원 (TourAPI/Kakao API 병렬 호출 필수). Pydantic 통합. Swagger UI 자동 생성. 타입 안전성.
**결정**: `FastAPI` + `uvicorn`
**이유**: async 지원 (TourAPI/Kakao API 병렬 호출 필수). Pydantic 통합. Swagger UI 자동 생성. 타입 안전성.
**트레이드오프**: Flask 대비 복잡도 높지만 async + 자동 문서화가 더 중요.

---

### ADR-007: HTTP 클라이언트 — httpx 선택
**결정**: `httpx` (async)
**이유**: TourAPI + Kakao API를 `asyncio.gather`로 병렬 호출. FastAPI lifespan에서 클라이언트 재사용. `httpx.AsyncClient`는 연결 풀 자동 관리.
**결정**: `httpx` (async)
**이유**: TourAPI + Kakao API를 `asyncio.gather`로 병렬 호출. FastAPI lifespan에서 클라이언트 재사용. `httpx.AsyncClient`는 연결 풀 자동 관리.
**트레이드오프**: requests 대비 학습 곡선 있음. async I/O 이점이 압도적으로 크므로 선택.

---

### ADR-008: 의존성 관리 — pyproject.toml
**결정**: `pyproject.toml` + `setuptools` (editable install `pip install -e ".[dev]"`)
**이유**: 표준 Python 패키지 구조. `src/` 레이아웃으로 import 충돌 방지.
### ADR-008: 의존성 관리 — pyproject.toml + requirements.txt
**결정**: `pyproject.toml`을 프로젝트 선언의 기준으로 삼고, `requirements.txt`를 Docker·CI 배포용 미러로 유지한다. 개발 설치는 `pip install -e ".[dev]"`를 사용한다.
**이유**: 표준 Python 패키지 구조. `src/` 레이아웃으로 import 충돌을 방지하고, 배포 환경은 별도 빌드 메타데이터 없이 requirements 파일을 설치한다. 두 manifest의 런타임 패키지 집합은 테스트로 동기화한다.
**트레이드오프**: Poetry/uv 대비 설정 약간 번거롭지만 추가 도구 설치 없이 동작.

---
Expand Down
2 changes: 1 addition & 1 deletion graphify-out/cache/last_query_stamp
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1789203191.2654688
1789637903.455872
2 changes: 1 addition & 1 deletion graphify-out/cache/stat-index.json

Large diffs are not rendered by default.

Loading
Loading