diff --git a/AGENTS.md b/AGENTS.md index 7d059c6..c005fde 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,50 @@ # AGENTS.md -이 저장소는 `pykma`, `pyopinet`과 같은 작업 형태를 따른다. +## 목표 + +이 저장소(GitHub/PyPI 이름 `python-krex-api`, import 패키지 이름 `krex`)는 한국도로공사 OpenAPI +(`data.ex.co.kr`, `data.go.kr`)를 Python 환경에서 쉽게 사용할 수 있도록 제공하는 **OpenAPI +클라이언트 라이브러리**다. API 명세 변환, 데이터 정규화, 로컬 유효성 검사 및 에러 매핑을 +핵심 가치로 삼으며, Streamlit 등 시각화나 도메인 가시 레이어는 라이브러리 소비자 앱이 +담당한다. `pykma`, `pyopinet`과 같은 작업 형태를 따른다. + +## Think Before Coding + +- 새 endpoint를 추가하기 전 `endpoints.md`에 명세가 있는지, `API_COVERAGE.md`에 구현/미구현 + 상태가 이미 표시돼 있는지 먼저 확인한다. +- 응답 shape이 실제 fixture로 검증되지 않았다면 Pydantic 모델을 성급하게 고정하지 말고 + `Page[dict]`로 시작한다(`CONTRIBUTING.md`의 방침과 동일). +- `pykma`, `pyopinet` 같은 sibling 라이브러리에 이미 검증된 provider 연동이 있는지 먼저 + 찾아본다. + +## Simplicity First + +- 새 기능은 `codes.py` → `models.py` → `client.py` 순으로 필요한 계층만 추가한다. +- 검증된 provider endpoint 구현이 sibling library에 있으면 독립 wrapper를 새로 만들지 않고 + 기존 `KrexClient` namespace로 포팅한다. +- Streamlit 등 도메인 시각화·디버그 UI 로직은 이 라이브러리에 넣지 않고 소비자 앱에 맡긴다. + +## Surgical Changes + +- 한 patch는 관련된 module과 문서만 건드린다. 예를 들어 `src/krex/client.py`를 고치면 같은 + patch에서 `endpoints.md`/`API_COVERAGE.md`도 함께 갱신한다. +- `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.coverage`, `.venv` 같은 산출물은 diff에 + 포함하지 않는다. +- 기존 enum/모델 필드 이름을 바꿀 때는 하위 호환이 필요한지 확인하고 `CHANGELOG.md`에 남긴다. + +## Goal-Driven Execution + +- 지시 우선순위는 사용자 요청 > 이 `AGENTS.md` > `README.md`/기존 테스트 순이다. +- 완료 기준은 관련 pytest가 통과하고, 변경된 동작이 `README.md`/`endpoints.md`/ + `API_COVERAGE.md`에 반영된 상태다. +- 애매한 요구사항은 최소한의, 되돌릴 수 있는 가정으로 진행하고 그 가정을 커밋/PR에 남긴다. + +## Practical Bias + +- 실제 provider 응답(fixture)이 없는 필드는 무리해서 타입을 좁히지 않고 raw로 남긴다. +- Live 검증은 opt-in(`KEX_LIVE=1`)으로 유지하고, 기본 test suite는 항상 네트워크 없이 + 통과해야 한다. +- 문서와 코드가 어긋나면 코드를 기준으로 문서를 먼저 고친다. ## 문서 언어 정책 @@ -8,11 +52,7 @@ 설명 문장, 절제목, 표 column 헤더, 빠른 시작 가이드, 일지 항목은 한글로 적는다. 새 문서를 만들 때 영문 초안을 두지 않는다 — 처음부터 한글로 쓴다. -## 역할 - -이 저장소(GitHub/PyPI 이름 `python-krex-api`, import 패키지 이름 `krex`)는 한국도로공사 OpenAPI (`data.ex.co.kr`, `data.go.kr`)를 Python 환경에서 쉽게 사용할 수 있도록 제공하는 **OpenAPI 클라이언트 라이브러리**다. API 명세 변환, 데이터 정규화, 로컬 유효성 검사 및 에러 매핑을 핵심 가치로 삼으며, Streamlit 등 시각화나 도메인 가시 레이어는 라이브러리 소비자 앱이 담당한다. - -## 식별자 (혼동 방지) +## 식별자 표 | 항목 | 값 | |------|----| @@ -24,35 +64,19 @@ ## 개발 환경 정책 -PC 개발은 Windows 호스트에서 직접 진행한다. -- **Git 사용**: 이 저장소의 git 작업은 항상 Windows용 `git.exe` 기준으로 수행한다. WSL 셸 안에서도 필요하면 `"/mnt/c/Program Files/Git/cmd/git.exe"`처럼 Windows git을 직접 호출한다. - **Python 버전**: Python 3.11+ +- **Git 사용**: 이 저장소의 git 작업은 항상 Windows용 `git.exe` 기준으로 수행한다. WSL 셸 안에서도 필요하면 `"/mnt/c/Program Files/Git/cmd/git.exe"`처럼 Windows git을 직접 호출한다. - **Unit test**: 네트워크를 절대 직접 호출하지 않는다. Fake session 또는 JSON fixture를 사용한다. - **의존성 관리**: `pyproject.toml`을 기준으로 하며, 개발 및 디버그용 의존성은 명시된 optional-dependencies에 격리한다. -- **에이전트별 고정 worktree**: ChatGPT Codex는 `F:\dev\python-krex-api-codex`, Claude Code는 `F:\dev\python-krex-api-claude`, Google Antigravity는 `F:\dev\python-krex-api-antigravity`를 사용한다. 작업마다 브랜치만 새로 만들고, CodeGraph는 worktree마다 1회 `codegraph init -i` 후 `codegraph sync`로 유지한다. - -## 지시 우선순위 - -1. 사용자 요청 -2. 이 `AGENTS.md` -3. `SKILL.md` -4. `API_COVERAGE.md`, `endpoints.md` -5. `codes.md`, `error-codes.md` -6. 기존 코드와 테스트 -7. 최소한의, 되돌릴 수 있는 가정 ## 절대 하지 말 것 (DO NOT) 1. **`main` 직접 푸시 금지**: 반드시 feature 브랜치 + PR 머지 과정을 거친다. -2. **API key 평문 커밋 금지**: `.env`에 키를 관리하되 절대 커밋하지 않는다. `data.ex.co.kr`에는 `KEX_EX_API_KEY`, `data.go.kr`에는 `DATA_GO_KR_SERVICE_KEY` 환경 변수를 사용한다. `KrexClient` 인스턴스 표현식(`__repr__`)이나 실패 메시지, 문서 등에 API 키가 노출되지 않도록 처리한다. +2. **API key 평문 커밋 및 노출 금지**: `.env`에 키를 관리하되 절대 커밋하지 않는다. `data.ex.co.kr`에는 `KEX_EX_API_KEY`, `data.go.kr`에는 `DATA_GO_KR_SERVICE_KEY` 환경 변수를 사용한다. `KrexClient` 인스턴스 표현식(`__repr__`)이나 실패 메시지, 문서 등에 API 키가 노출되지 않도록 처리한다. 3. **Unit test에서 network 호출 금지**: 가짜 세션(Fake Session)이나 fixture 파일(JSON 등)을 사용한다. 4. **Stable 의미가 있는 반환값을 raw string으로 노출 금지**: raw string보다는 typed Pydantic model 또는 enum을 제공한다. -5. **모호한 raw 주소 데이터에서 법정동 코드 추측 금지**: 공개 주소 데이터는 provider 원문 문자열 그대로 보존한다. -6. **`routeNo`, `unitCode`, branch code, office code 등의 leading zero 제거 금지**: 0이 앞에 붙는 코드는 반드시 문자열 타입으로 보존한다. -7. **Debug UI 의존성을 이 library에 포함 금지**: Streamlit 등의 디버그 UI 도구는 배포 패키지 코어 의존성에서 철저히 제외한다. -8. **임시/캐시 파일 커밋 금지**: `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.coverage`, `.venv` 및 가상환경 관련 파일은 절대 커밋하지 않는다. -9. **독립된 wrapper 신규 작성 남발 금지**: sibling library에 검증된 provider endpoint 구현이 있으면 기존 `KrexClient` namespace로 포팅하여 재사용성을 극대화한다. -10. **`.codegraph/` 커밋 금지**: CodeGraph 인덱스는 worktree 로컬 산출물이다. 이미 초기화된 worktree에서는 `codegraph init` 재실행 대신 `codegraph sync`를 사용한다. +5. **주소·코드 원문 임의 가공 금지**: 모호한 raw 주소 데이터에서 법정동 코드를 추측하지 않고 provider 원문 문자열 그대로 보존한다. `routeNo`, `unitCode`, branch code, office code 등 앞에 0이 붙는 코드는 반드시 문자열 타입으로 보존하고 leading zero를 제거하지 않는다. +6. **Debug UI 의존성을 이 library에 포함 금지**: Streamlit 등의 디버그 UI 도구는 배포 패키지 코어 의존성에서 철저히 제외한다. ## Module ownership @@ -78,22 +102,17 @@ PC 개발은 Windows 호스트에서 직접 진행한다. - coverage/support 상태: `API_COVERAGE.md` - enum 변경: `codes.md` - exception/provider error mapping: `error-codes.md` +- 구조적 결정(뒤집힐 수 있는, 재론쟁될 만한 결정): `docs/decisions.md` - agent workflow/repeated mistakes: `SKILL.md`와 이 파일 - 문서 스타일 규칙: `CONTRIBUTING.md`, `SKILL.md`, 이 파일을 함께 갱신 -## 작업 후 체크리스트 - -- [ ] `python -m compileall src/krex tests` 통과 -- [ ] `python -m pytest` 통과 -- [ ] `python -m mypy src/krex` 통과 (설치되어 있는 경우) -- [ ] `ruff check .` 통과 (설치되어 있는 경우) -- [ ] `.pytest_cache`, `.mypy_cache`, `.ruff_cache` 등이 커밋 대상에서 제외되었는지 확인 - ## 검증 ```bash python -m compileall src/krex tests python -m pytest +python -m mypy src/krex +ruff check . ``` 실제 `data.ex.co.kr` 및 `data.go.kr` 검증은 의도적으로 할 때만 다음 명령어를 사용한다: diff --git a/README.md b/README.md index 3ba7ce9..ee43a51 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,27 @@ # python-krex-api +![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg) +![GPL-3.0-or-later 라이선스](https://img.shields.io/badge/License-GPL--3.0--or--later-blue.svg) +![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg) + 한국도로공사(Korea Expressway Corporation, KEX) 공공데이터 OpenAPI를 Python에서 편하게 쓰기 위한 비공식 클라이언트 라이브러리입니다. `python-krex-api`는 `data.ex.co.kr`와 `data.go.kr`에 흩어진 고속도로 교통량, 실시간 소통, 통행료, 영업소, 휴게소, 휴게소별 날씨, 기준정보 API를 한 인터페이스로 감싸고, 응답을 Pydantic 모델과 enum으로 변환합니다. -> 현재 저장소는 초기 구현 단계입니다. 세부 엔드포인트 명세는 [endpoints.md](endpoints.md), 지원 상태표는 [API_COVERAGE.md](API_COVERAGE.md), 코드표는 [codes.md](codes.md), 에러 매핑은 [error-codes.md](error-codes.md), 구현 규칙은 [SKILL.md](SKILL.md)와 [AGENTS.md](AGENTS.md)를 참고하세요. +현재 진행 중인 작업은 [CHANGELOG.md](CHANGELOG.md#unreleased)를 참고하세요. + +## 먼저 읽을 문서 + +| 필요 정보 | 문서 | +|---|---| +| 엔드포인트 상세 명세 | [endpoints.md](endpoints.md) | +| 지원/live 검증 상태 | [API_COVERAGE.md](API_COVERAGE.md) | +| 공통 코드표 · enum | [codes.md](codes.md) | +| 에러 코드 매핑 | [error-codes.md](error-codes.md) | +| 에이전트 구현 규칙 | [SKILL.md](SKILL.md), [AGENTS.md](AGENTS.md) | +| 구조적 의사결정 기록 | [docs/decisions.md](docs/decisions.md) | +| 기여 가이드 | [CONTRIBUTING.md](CONTRIBUTING.md) | +| 변경 이력 | [CHANGELOG.md](CHANGELOG.md) | --- @@ -65,47 +82,66 @@ pip install python-krex-api ### 3단계: 사용 +`KrexClient`는 동기 클라이언트가 기본이고, `KrexClient.aio()`는 같은 namespace/메서드를 그대로 `await`할 수 있는 비동기 진입점입니다(`python-krheritage-api`와 같은 스타일). asyncio 애플리케이션이라면 이 방식을 대표 예제로 사용하세요. + +```python +import asyncio + +from krex import CarType, KrexClient + + +async def main() -> None: + async with KrexClient.aio() as client: + # 실시간 소통 + flows = await client.traffic.flow(route_no="0010") + for item in flows.items[:5]: + print(item.route_name, item.conzone_name, item.speed, item.congestion_level) + + # 영업소간 통행료 + fees = await client.tollfee.between_tollgates( + start_unit_code="101", + end_unit_code="105", + car_type=CarType.LIGHT, + ) + print(fees.items[0].toll_fee) + + # 휴게소 표준데이터(data.go.kr) + areas = await client.restarea.list_all(route_name="경부고속도로") + print(areas.items[0].name, areas.items[0].has_ev_charger) + + # 노선별 휴게시설과 휴게소 주유소 가격(data.ex.co.kr) + facilities = await client.restarea.route_facilities(route_code="0010") + fuel_prices = await client.restarea.fuel_prices( + service_area_code=facilities.items[0].service_area_code + ) + print(facilities.items[0].service_area_name, fuel_prices.items[0].gasoline_price) + + # 휴게소별 날씨(data.ex.co.kr) + weather = await client.restarea.latest_weather(lookback_hours=72) + if weather.first: + print(weather.first.unit_name, weather.first.weather, weather.first.temperature) + + +asyncio.run(main()) +``` + +스크립트나 동기 코드베이스에서는 `from_env()`로 만든 동기 클라이언트를 그대로 사용할 수 있습니다. + ```python from krex import CarType, KrexClient client = KrexClient.from_env() -# 실시간 소통 flows = client.traffic.flow(route_no="0010") for item in flows.items[:5]: print(item.route_name, item.conzone_name, item.speed, item.congestion_level) -# 영업소간 통행료 fees = client.tollfee.between_tollgates( start_unit_code="101", end_unit_code="105", car_type=CarType.LIGHT, ) print(fees.items[0].toll_fee) - -# 휴게소 표준데이터(data.go.kr) -areas = client.restarea.list_all(route_name="경부고속도로") -print(areas.items[0].name, areas.items[0].has_ev_charger) - -# 노선별 휴게시설과 휴게소 주유소 가격(data.ex.co.kr) -facilities = client.restarea.route_facilities(route_code="0010") -fuel_prices = client.restarea.fuel_prices(service_area_code=facilities.items[0].service_area_code) -print(facilities.items[0].service_area_name, fuel_prices.items[0].gasoline_price) - -# 휴게소별 날씨(data.ex.co.kr) -weather = client.restarea.latest_weather(lookback_hours=72) -if weather.first: - print(weather.first.unit_name, weather.first.weather, weather.first.temperature) -``` - -비동기 코드에서는 `python-krheritage-api`와 같은 `aio()` 진입점을 사용할 수 있습니다. - -```python -from krex import KrexClient - -async with KrexClient.aio() as client: - flows = await client.traffic.flow(route_no="0010") - print(flows.first) ``` ### 키를 직접 넘기는 방식 @@ -198,22 +234,7 @@ python -m streamlit run examples/streamlit_debug_ui.py --server.port 8504 ## 구현 상태 -현재 구현은 “공통 호출 기반 + 핵심 모델 우선” 단계입니다. - -| 영역 | 상태 | 비고 | -|---|---|---| -| HTTP transport | 구현됨 | 5xx/네트워크 retry, 키 마스킹, JSON 파싱 | -| `data.ex.co.kr` envelope | 구현됨 | `SUCCESS`, `NO_DATA`, 인증/한도/서버 코드 매핑 | -| `data.go.kr` envelope | 구현됨 | `response.header.resultCode` 검사 | -| 교통 핵심 모델 | 부분 구현 | `TrafficByIc`, `TrafficFlow`, `Incident` | -| 통행료 핵심 모델 | 구현됨 | `TollFee`, `Tollgate` | -| 휴게소 핵심 모델 | 부분 구현 | 노선별 휴게시설, 표준 휴게소, 휴게소별 날씨, 주유소 가격, 음식가격 | -| 시설/행정 상세 모델 | 원시 dict 반환 | 경로 검증 후 Pydantic 모델 승격 예정 | -| 라이브 호출 테스트 | 미포함 | 기본 테스트는 네트워크를 쓰지 않음 | - -검증되지 않은 포털 경로나 데이터셋은 무리해서 모델로 고정하지 않고 `Page[dict]`로 반환합니다. 실제 응답 fixture가 쌓이면 Pydantic 모델로 승격합니다. - -전체 지원/미지원/실서버 검증 상태는 [API_COVERAGE.md](API_COVERAGE.md)를 기준으로 관리합니다. +영역별 구현/live 검증 상태는 [API_COVERAGE.md](API_COVERAGE.md)를 기준으로 관리합니다. 검증되지 않은 포털 경로나 데이터셋은 무리해서 모델로 고정하지 않고 `Page[dict]`로 반환하며, 실제 응답 fixture가 쌓이면 Pydantic 모델로 승격합니다. --- @@ -493,6 +514,6 @@ tests/ ## 라이선스 -GPL-3.0-or-later. 자세한 조건은 [LICENSE](LICENSE)를 참고하세요. +GPL-3.0-or-later. 자세한 조건은 [LICENSE](LICENSE)를 참고하세요. 이 라이선스는 이 저장소의 코드에만 적용됩니다. -원천 데이터의 저작권과 이용조건은 한국도로공사, 공공데이터포털, 각 데이터 제공기관 정책을 따릅니다. 이 프로젝트는 비공식 라이브러리이며 한국도로공사와 무관합니다. +원천 데이터의 저작권과 이용조건은 한국도로공사, 공공데이터포털, 각 데이터 제공기관 정책을 따릅니다. 이 프로젝트는 비공식 라이브러리이며 한국도로공사와 무관합니다. 실제 이용 전에는 각 제공기관의 최신 이용약관을 직접 확인하세요 — 본 안내는 법적 자문이 아니며 법적 효력을 보장하지 않습니다. diff --git a/WSL_KRADDR_GEO_DEBUG_UI.md b/WSL_KOR_TRAVEL_GEO_DEBUG_UI.md similarity index 50% rename from WSL_KRADDR_GEO_DEBUG_UI.md rename to WSL_KOR_TRAVEL_GEO_DEBUG_UI.md index feb86df..d4ecd5f 100644 --- a/WSL_KRADDR_GEO_DEBUG_UI.md +++ b/WSL_KOR_TRAVEL_GEO_DEBUG_UI.md @@ -1,4 +1,20 @@ -# WSL kraddr.geo 디버그 UI 메모 +# WSL kor-travel-geo 디버그 UI 메모 + +> **이름 변경**: 이 문서는 `python-kraddr-geo`/`kraddr_geo_api`/`pykraddr` 경로명으로 작성됐지만, +> 그 프로젝트는 `kor-travel-geo`(Python 패키지 `kortravelgeo`)로 리네임됐고 SpatiaLite/SQLite 기반 +> 구현도 PostgreSQL + PostGIS로 재구현됐습니다. 아래의 옛 경로·모듈명·명령은 더 이상 유효하지 +> 않습니다. 이 저장소(`python-krex-api`)는 `kor-travel-geo`를 import하지 않으므로 이 노트를 +> 정본으로 유지하지 않습니다. +> +> `kor-travel-geo`의 로컬 디버그 UI를 WSL에서 띄우는 현재 방법은 그 저장소 자신의 문서 +> (`docs/agent-guide.md`, `docs/adr/065-linux-only-development-environment.md` 등)를 확인하세요. +> 제공 표면은 README 기준으로 REST API `uvicorn kortravelgeo.api.app:app`, 관리 UI는 별도 Next.js +> 패키지 `kor-travel-geo-ui`입니다 — 정확한 포트/플래그는 그쪽 문서에서 확인해야 이 저장소가 다시 +> 오래된 정보를 복제하지 않습니다. + +--- + +아래는 리네임 이전에 작성된 원문(참고용, 유효하지 않음)입니다. 이 provider를 `python-kraddr-geo` 또는 `python-krtour-map`과 함께 검증할 때는 WSL에서 Linux 실행 파일을 사용하고 host binding을 명시해 로컬 지오코딩 디버그 스택을 실행한다. diff --git a/docs/decisions.md b/docs/decisions.md new file mode 100644 index 0000000..84ea94e --- /dev/null +++ b/docs/decisions.md @@ -0,0 +1,101 @@ +# decisions.md — 의사결정 기록 + +이 문서는 이 프로젝트의 구조적 결정을 결정 시점 순서로 누적한다. +결정이 뒤집힐 때는 새 항목을 추가하고, 옛 항목은 지우지 않은 채 +(supersedes: 위 항목)으로 표시한다. + +## D-001: `strict_no_data`로 `NO_DATA` 처리 방식을 선택하게 한다 + +- 상태: accepted +- 날짜: 2026-04-30 + +### 컨텍스트 + +`data.ex.co.kr`/`data.go.kr`는 조회 결과가 없을 때도 HTTP 200을 반환하고, envelope 안에서만 +`NO_DATA`류 코드를 알려준다. 이를 빈 결과로 조용히 넘기면 파라미터 오류나 실제 데이터 +없음을 구분하지 못하는 호출부가 생기고, 반대로 항상 예외로 던지면 "결과가 없을 수 있음"을 +전제로 순회하는 배치 코드가 매 호출을 `try/except`로 감싸야 한다. + +### 결정 + +`KrexClient(strict_no_data=True)`를 기본값으로 하여 `NO_DATA`를 `KrexNotFoundError`로 던진다. +`strict_no_data=False`로 생성한 클라이언트는 같은 상황에서 예외 대신 빈 `Page`를 반환한다. +이 분기는 `KrexClient._page_ex`/`_page_go`와 비동기 버전 `_apage_ex`/`_apage_go`에 동일하게 +구현되어 있다(`src/krex/client.py`). + +### 근거 + +기본값을 엄격하게 두면 새 endpoint를 추가할 때 파라미터 실수를 "빈 리스트"로 착각해 +넘어가는 실수를 막을 수 있다. 반면 최근 N일 조회처럼 데이터가 없는 구간이 정상적으로 +존재하는 호출부는 명시적으로 `strict_no_data=False`를 선택해 예외 처리 없이 순회할 수 +있다. 기본값과 옵트아웃을 분리하면 두 사용 패턴을 모두 깨끗하게 지원한다. + +### 결과 + +`AGENTS.md`의 DO-NOT 목록에 "`NO_DATA`는 기본적으로 `KrexNotFoundError`"라는 규칙으로 +반영되어 있다. 새 namespace를 추가할 때도 이 두 helper를 통해서만 페이지를 만들어야 +일관성이 유지된다. + +## D-002: `KrexClient`를 동기 우선으로 두고 `AsyncKrexClient`는 `asyncio.to_thread` 파사드로 감싼다 + +- 상태: accepted +- 날짜: 2026-05-19 + +### 컨텍스트 + +httpx는 동기/비동기 클라이언트를 모두 제공하지만, 두 구현을 처음부터 각각 손으로 +관리하면 namespace 메서드(`traffic.flow()` 등)가 늘어날 때마다 동기/비동기 버전을 매번 +같이 작성하고 테스트해야 한다. 반대로 비동기만 제공하면 스크립트/배치 코드에서 매번 +`asyncio.run()`을 강제하게 된다. + +### 결정 + +`KrexClient`(동기)를 유일한 실제 구현으로 유지하고, `KrexClient.aio()`가 반환하는 +`AsyncKrexClient`는 각 namespace를 `_AsyncServiceProxy`로 감싸 모든 호출을 +`asyncio.to_thread(sync_method, *args, **kwargs)`로 위임한다(`src/krex/client.py`). +HTTP 계층(`KrexHttp`)만 `get_ex`/`aget_ex`처럼 진짜 동기/비동기 구현을 각각 갖는다. + +### 근거 + +Namespace 메서드 자체(파라미터 정리, enum 변환, 모델 파싱)는 동기든 비동기든 로직이 +같으므로 이중 구현할 이유가 없다. `asyncio.to_thread` 파사드를 쓰면 새 endpoint를 +`KrexClient`에만 추가해도 `AsyncKrexClient`에서 자동으로 `await` 가능해지고, 두 진입점의 +동작이 갈라질 여지가 줄어든다. + +### 결과 + +README의 대표 사용 예제는 `KrexClient.aio()` 기반 `async def main()`이고, 동기 사용법은 +보조 예제로 제공한다. 새 endpoint를 추가할 때 별도의 async 메서드를 작성할 필요는 없다. + +## D-003: `traffic.incident()`를 존재하지 않는 경로에서 실시간 문자정보 API로 repoint한다 + +- 상태: accepted +- 날짜: 2026-06-11 + +### 컨텍스트 + +`traffic.incident()`는 원래 `/openapi/trafficapi/incident`를 호출했지만, live 검증 결과 +이 경로는 항상 404를 반환해 실제로 존재하지 않는 endpoint였다. 사고 정보 자체는 +`data.ex.co.kr`가 실시간 문자정보(apiId 0611) `/openapi/burstInfo/realTimeSms`로만 +제공하고 있었다. + +### 결정 + +`traffic.incident()`가 `/openapi/burstInfo/realTimeSms`를 호출하도록 repoint하고, +파라미터를 `acc_type_code` 선택값으로 교체했다. `Incident` 모델 필드도 live 응답 shape +(`accDate`, `accHour`, `accType`, `smsText`, `startEndTypeCode`, 위경도 등)에 맞춰 +재정렬했다. 이 포털의 응답에서 경도는 `lon`이 아니라 `altitude` 키로 오므로, 파싱 시 +이를 명시적으로 처리한다. 목록은 `realTimeSMSList` 키에서 추출한다. + +### 근거 + +존재하지 않는 경로를 계속 노출하면 모든 호출이 항상 실패하는 죽은 기능을 유지하는 +셈이다. 실측 가능한 대체 endpoint로 옮기는 편이 "미구현으로 명시"보다 사용자에게 +유용하고, `API_COVERAGE.md`의 live 검증 원칙(실제 provider 호출로 확인된 것만 "Live +검증됨"으로 표시)과도 맞는다. + +### 결과 + +`API_COVERAGE.md`에 2026-06-11 live 검증(count=190)으로 기록되어 있다. `altitude` 키 +매핑과 `realTimeSMSList` 추출은 회귀 테스트와 `tests/fixtures/`의 fixture로 고정되어 +있으므로, 포털이 필드명을 다시 바꾸지 않는 한 이 매핑을 그대로 유지한다.