์๊ฐ ์์ ์ฐจ๋จ์ Chromium CDP Fetch์ ImageยทFontยทMedia ํ์
์๋ง ์ ์ฉํ๋ค. HTTP ์บ์๋ฅผ
๋๋ Playwright ์์ฒญ ๋ผ์ฐํ
์ ์ ๊ฑฐํ๋ค. ๋ก์ปฌ HTTP ์๋ฒ์ ์ค์ Chromium์์ ์ด๊ธฐ GETยท
๋ฐ๋ณต POST์ JSยทCSS ์บ์ ์ฌ์ฌ์ฉ๊ณผ ๊ฐ๊ฒฉ fetchยท์คํฌ๋ฆฝํธ ์คํยท์คํ์ผ ์ ์ฉ์ ๊ฒ์ฆํ๋ค.
์ฒดํฌํฌ์ธํธ ์์ฑ์ ๊ฒฝํฉ์ OpinetCheckpointBusyError๋ก ๊ตฌ๋ถํ๋ค. ํธ์ถ์๋ ์งํ ์ค์ธ
๋ค๋ฅธ ์์ง๊ธฐ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ์ํ๋ก ์ฒ๋ฆฌํ ์ ์๋ค. SQLite I/Oยทํ์ผ ์ค๋ฅ๋ ๊ฒฝํฉ์ผ๋ก ๋ฐ๊พธ์ง ์๋๋ค.
2026-10-24 ์ถ๊ฐ: ๊ธฐ๋ณธ ๋ฌด์์ ์กฐ์ ๋๊ธฐ๋ฅผ 0.15~0.85์ด๋ก ๋จ์ถํ๊ณ ์๊ตฐ๊ตฌ ์ ํ์ ์ ์ ์๋ ์กฐํ ์๋ต์ ๊ฒ์ฆํด ์ค๋ณต ๋ฒํผ ์กฐํ๋ฅผ ์ค์ธ๋ค. ์๋ ์๋ต ๋ชฉ๋ก๊ณผ ์ค์ ์ต์ ์ ๋์กฐํ๋ฉฐ ๊ฐ๊ฒฉ ํ์ฑ์ ์ฐ์ง ์๋ ์๊ฐ ์์์ ์ ์ธํ๋ค. ๋ฌธ์ ์๋ฃยท์ง์ญยทํญ ๊ฒ์ฆ์ ์ ์งํด ๋์ ์ถ๊ฐ ํ์ ๋น ๋จ๋ฆฌ์ง ์๋๋ค. ์ด์ ์ ๊ตญ30๋ถ ๋ชฉํ๋ ์ค์ ์คํ์ผ๋ก ๋ณ๋ ํ์ธํ๋ค.
2026-10-24: ์ง์ญ๋ณ ์์ ์ฒดํฌํฌ์ธํธยท์๋ฃ ๊ฒฐ๊ณผ ์ฌ์ ๋ฌยท๋จ์ผ ์์ฑ์ ์ ๊ธยท์๊ตฐ๊ตฌ ํ์ ์ต์ ํ๋ฅผ ์ถ๊ฐํ๋ค. ๋ณต๊ตฌ ๊ณ์ฝ์ ๋ฐ๋ฅธ๋ค. ์ค์ ์ด์ ์๋ ๊ฐ์ ๋ฅ ์ ๋ฏธ์ธก์ ์ด๋ฉฐ ๋จ๊ณ๋ณ ๋์ ์๊ฐ ๋ก๊ทธ๋ก ์ง์ญ ์ ํยท์๋ตยท๊ฒฐ๊ณผ ์ค๋น ๋น์ฉ์ ๊ตฌ๋ถํ๋ค.
์ด ๋ฌธ์๋ ํ์ฌ ๊ตฌํ ์ํ, ๊ฒ์ฆ ๋ฐฉ๋ฒ, ๋ค์ ์์ ์๊ฐ ๋ฐ๋ณตํ์ง ๋ง์์ผ ํ ํ๋จ์ ํ๊ณณ์ ๋ชจ์๋๋ค.
OpinetClient๋ ์คํผ๋ท ๊ณต์ ์คํ API ํ์ด์ง์ ๋ฑ์ฌ๋ 5๊ฐ ์๋ํฌ์ธํธ๋ง ์์ ๊ตฌํ ๋์์ผ๋ก ๋ก๋๋ค.
์ด์ Chromium์์ ๊ฒ์ ํ๋ฉด์ ์๋ ์ด๋ ์งํ ์ด์ ์๋ต ๋ณธ๋ฌธ์ด ์์ด์ง๋ ์ค๋ฅ๋ฅผ
ํ์ธํ๋ค. ๊ฐ์ ์ถ์ฒยท๊ฒ์ ๋ฌธ์์ ํ์ฌ HTML๋ง ๋์ฒด ๊ฒ์ฌํ๋๋ก ๋ณด์ํ์ผ๋ฉฐ,
์ฐจ๋จ ํ๋ฉด๊ณผ ์ธ๋ถ ์ด๋, ๊ด๋ จ ์๋ ๋คํธ์ํฌ ์ค๋ฅ๋ ๊ณ์ ์คํจ๋ก ์ฒ๋ฆฌํ๋ค.
์ด์ ์์ง์์ ์ผ์์ ์ผ๋ก ์ง์ฐ๋ ํ๋ฉด ์๋ต์ ๊ธฐ๋ค๋ฆด ์ ์๋๋ก collector์ ๊ธฐ๋ณธ
timeout_ms๋ 60์ด๋ก ๋๋ค. ์ด ๋ณ๊ฒฝ์ ์์ฒญ ์ฌ์๋๋ ์ ๊ทผ ์ ํ ์ฐํ๋ฅผ ์ถ๊ฐํ์ง ์๋๋ค.
2026-10-12๋ถํฐ ์ง์ญ ์กฐํ ํ ๊ฑด์ '์กฐํ' ๊ฒ์ ์๋ต์ด ์๊ฐ ์ด๊ณผ๋๋ฉด ๊ฒ์ ํ๋ฉด์ ์๋ก ์ด๊ณ
ํญยท์ง์ญ์ ๋ค์ ๊ณ ๋ฅธ ๋ค ๊ทธ ์ง์ญ๋ง ํ ๋ฒ ๋ค์ ์กฐํํ๋ค(์ง์ญ ์ ํยท๊ฒฐ๊ณผ DOM ๋๊ธฐ ์๊ฐ ์ด๊ณผ๋ ์ฌ์๋ํ์ง
์๋๋ค). ์ฌ์๋๋ ํ๋ฉด ์กฐ์ ๊ฐ๊ฒฉ์ ์งํค๊ณ max_search_requests ์ํ์์ ์ฐจ๊ฐ๋๋ฉฐ,
์คํจ ์ง๋จ์๋ target=non-numeric ๋์ ์ ์ ยท์ ๋จํ ์ง์ญ ์๋ณ์์ ์๋ ํ์๊ฐ ๋จ๋๋ค.
๊ณต์ open API์ ๋ถ๋ฆฌ๋ opinet.experimental.OpinetBrowserCollector๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
Playwright๋ก ์ง์ญ๋ณ ๊ณต๊ฐ ํ๋ฉด์ ์๋ยท์๊ตฐ๊ตฌยท์๋ฉด๋ ์ ํ๊ฐ์ ์ฝ๊ณ , ํ๋ฉด ์กฐํ ์
๋ฐ์ํ๋ searRgSelect.do HTML ๋๋ searRgCircleAjax.do JSON ์๋ต์์
์ฃผ์ ์์ ์ถฉ์ ์ ๋ฐ์ดํฐ๋ฅผ typed dataclass๋ก ๋ณํํฉ๋๋ค. ๊ธฐ๋ณธ ์ฃผ์ ์ ์กฐํ ๋จ์๋ ์๊ตฐ๊ตฌ์ด๋ฉฐ, ํ์ํ ๋๋ง
query_level="dong"์ผ๋ก ์๋ฉด๋๋ณ ์กฐํ๋ฅผ ์ ํํฉ๋๋ค.
ํ๋ฉด ์กฐ์ ์ฌ์ด์ ์งง์ ๋ฌด์์ ์ง์ฐ๊ณผ ์ ์ฒด ์์ง ์ฌ์ด์ ๊ธฐ๋ณธ 8์๊ฐ ๊ฐ๊ฒฉ,
์ต๊ทผ 24์๊ฐ ์ต๋ 3ํ ์คํ ์ ํ์ ์์ฒญ ๋ถํ๋ฅผ ๋ถ์ฐํ๊ธฐ ์ํ ์ ์ฑ
์
๋๋ค. ์ง์ญ ์ ํ AJAX์ ๊ฒ์ ์๋ต ์๋ฃ,
HTTP ์ํยท์ ๊ทผ ๊ฑฐ๋ถ ํ์ด์ง๋ฅผ ํ์ธํ๋ฉฐ, ์๋ํ ํ์ง ์ฐํยท์ง๋ฌธ ์กฐ์ยทCAPTCHA
์ฐํ๋ ๊ตฌํํ์ง ์์ต๋๋ค. ์์ธ ์ฌ์ฉ๋ฒ๊ณผ ์ด์ฉ์ฝ๊ดยท์ฌ๋ฐฐํฌ ํ์ธ ์กฐ๊ฑด์
docs/browser-collection.md์ ๊ธฐ๋กํ์ต๋๋ค.
| Method | Endpoint | Status | Notes |
|---|---|---|---|
get_national_average_price() |
avgAllPrice.do |
Implemented | TRADE_DT, PRICE, DIFF๋ฅผ Python ํ์
์ผ๋ก ๋ณํ |
get_lowest_price_top20() |
lowTop10.do |
Implemented | cnt 1~20, area 2/4์๋ฆฌ ์ฌ์ ๊ฒ์ฆ |
search_stations_around() |
aroundAll.do |
Implemented | WGS84 ์ ๋ ฅ์ KATEC์ผ๋ก ๋ณํ ํ ์์ฒญ |
get_station_detail() |
detailById.do |
Implemented | LPG_YN์ StationType, KPETRO_YN์ is_kpetro |
get_area_codes() |
areaCode.do |
Implemented | ์ฝ๋๊ฐ์ ์ ํ 0 ๋ณด์กด์ ์ํด ํญ์ str |
PDF ๊ฐ์ด๋๋ถ์ ์ถ๊ฐ API๋ ์์ง ๊ณต์ ๋ช
์ธ ํ์ด์ง๊ฐ ์์ผ๋ฏ๋ก opinet.experimental์ ๋ก๋๋ค. ๊ฒ์ฆ ์ ์๋ ์์ API๋ก ์น๊ฒฉํ์ง ์์ต๋๋ค.
REST API ๋๋ฒ๊ทธ UI ์ค๊ณ์์ ๋ง์ถฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ณธ์ฒด์ Streamlit ๋น์์กด helper๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
| ์์ญ | ๊ตฌํ |
|---|---|
| API ์นดํ๋ก๊ทธ | src/opinet/catalog.py์ get_api_catalog(), get_api_catalog_item(), get_api_catalog_options() |
| Async/httpx | OpinetConfig, AsyncHttpxTransport, OpinetClient, AsyncTokenBucket |
| ๋ฐ์ดํฐ์ ํ์ | ApiCatalogItem.dataset_name์ผ๋ก ์ฌ๋์ด ์ฝ๊ธฐ ์ฌ์ด ๋ฐ์ดํฐ์
๋ช
์ ๊ณต |
| ์๋น์คํค ๋งํฌ | ๋ชจ๋ ApiCatalogItem.service_key_url์ด ์คํผ๋ท ์ธ์ฆํค ๋ฐ๊ธ ํ์ด์ง๋ฅผ ๊ฐ๋ฆฌํด |
| ์๋น์คํค ์ ๊ทํ | OpinetClient๊ฐ ๋ช
์ ์ธ์/ํ๊ฒฝ๋ณ์/.env์์ ํค๋ฅผ ์ฝ๊ณ ๊ณต๋ฐฑ ๋ฌธ์๋ฅผ ์ ๊ฑฐ |
| DebugRun | src/opinet/debug.py์ DebugRun์ด input, request, response, parsed, processed, trace, catalog item, error๋ฅผ ๋ณด๊ด |
| fixture ์ ์ฅ | save_fixture(), save_debug_fixture()๊ฐ tests/fixtures/{function}/{case}.json ํ์์ผ๋ก ์ ์ฅ |
| replay ํ ์คํธ | tests/test_generated_fixtures.py๊ฐ ํ์ ๋๋ ํฐ๋ฆฌ fixture๋ฅผ ๋คํธ์ํฌ ์์ด ์ฌํ์ฑ/๋น๊ต |
| ์์ UI | examples/streamlit_debug_ui.py๊ฐ ์นดํ๋ก๊ทธ ๊ธฐ๋ฐ ํ๋ผ๋ฏธํฐ ํผ, ์๋น์คํค ๋งํฌ, 6๊ฐ ๊ณ ์ ํญ(Raw Response/Pydantic Model/Processed Result/Validation Errors/Debug Trace/Fixture)์ ๊ฒ์ฆํ๋ ์ฐธ๊ณ ์ฑ |
๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฐํ์ ์์กด์ฑ์๋ Streamlit์ ์ถ๊ฐํ์ง ์์์ต๋๋ค. ํ๋ฉด ์ฑ์ pip install -e ".[debug-ui]"(pyproject.toml์ debug-ui extra)๋ก ์ ํ์ ์ผ๋ก ์ค์นํด examples/streamlit_debug_ui.py๋ฅผ ์คํํฉ๋๋ค.
- ์ธ์ฆ ํ๋ผ๋ฏธํฐ๋
certkey, ์ถ๋ ฅ ํฌ๋งท์out=json์ ๋๋ค. - ์๋น์คํค๋
OpinetClient(api_key=...),OPINET_API_KEY, ํ์ฌ ์์ ๋๋ ํฐ๋ฆฌ ๋๋ ๋ถ๋ชจ ๋๋ ํฐ๋ฆฌ์.env์์๋ก ์ฝ์ต๋๋ค. ๋ณต์ฌ/๋ถ์ฌ๋ฃ๊ธฐ ์ค ์์ธ ๊ณต๋ฐฑ ๋ฌธ์๋ ๊ฐ์ด ์๋๋ฏ๋ก ์ ๊ฑฐํฉ๋๋ค. - HTTP status/body error mapping lives in the httpx-based
src/opinet/_http.py. - ๊ณต๊ฐ HTTP/API๋ config + native async OpinetClient/AsyncHttpxTransport + ๊ณตํต AsyncTokenBucket์ผ๋ก ๊ตฌ์ฑํ๋ค.
- The normalized station detail contract consumed by
python-krtour-mapis kept stable aroundprovider_station_id,provider_station_name,lon/lat,katec_x/katec_y,prices, andraw. - ์๋ํฌ์ธํธ ํ๋ผ๋ฏธํฐ ์ค๋ฅ๋ HTTP ํธ์ถ ์ ์
OpinetInvalidParameterError๋ก ์คํจ์ํต๋๋ค. - API ์๋ต์ ๋ชจ๋ธ ์์ฑ ์ ์
date,time,float,bool,StrEnum์ผ๋ก ๋ณํํฉ๋๋ค. AREA_CD,SIGUNCD,UNI_ID, ์ ํ ์ฝ๋, ์ํ ์ฝ๋๋ ์ ์๋ก ๋ณํํ์ง ์์ต๋๋ค.- KATEC ๋ณํ์
src/opinet/coords.py์์pyproj๋ฅผ ์ง์ ์ฌ์ฉํฉ๋๋ค. - ์ฃผ์ ์ ์ขํ/์ง์ญ ๋งคํ ๊ด๋ จ ํ์ ๊ณผ ๋ณํ ๋ก์ง์ ์ด ์ ์ฅ์ ์์์ ์ง์ ์์ ํฉ๋๋ค. ์์ wrapper๋ mirror dataclass๋ ๋ง๋ค์ง ์์ต๋๋ค.
SIGUNCD๋ ์คํผ๋ท ์์ฒด 4์๋ฆฌ ์๊ตฐ๊ตฌ ์ฝ๋์ด๋ฉฐ ๋ฒ์ ๋ ์๊ตฐ๊ตฌ ์ฝ๋๋ 10์๋ฆฌ ๋ฒ์ ๋์ฝ๋๋ก ์ถ์ ๋ณํํ์ง ์์ต๋๋ค. ํ์ํ๋ฉดsrc/opinet/vworld.py์์vworld.VworldClient.search_district(..., category="L2")๊ฒฐ๊ณผ์ 5์๋ฆฌid์title์ ๋ช ์ ๋งค์นญํฉ๋๋ค.- ๋ฌธ์์์ ํ์ผ ์์น๋ ํ๋ก์ ํธ ๋ฃจํธ ๊ธฐ์ค ์๋ ๊ฒฝ๋ก๋ก ํ๊ธฐํฉ๋๋ค.
- Python ๋ด๋ถ ๋ฌธ์(docstring๊ณผ ์ ์ง๋ณด์์ฉ ์ฃผ์)๋ ํ๊ธ๋ก ์์ฑํฉ๋๋ค. ์ธ๋ถ API ๊ณ ์ ๋ช ์นญ๊ณผ ์ฝ๋ ์๋ณ์๋ ์๋ฌธ์ ์ ์งํฉ๋๋ค.
- ์ด Windows/PowerShell ํ๊ฒฝ์์๋
rg๊ฐ ์คํ ๊ถํ ๋ฌธ์ ๋ก ์คํจํ ์ ์์ผ๋ฏ๋ก, ์คํจ ์git ls-files,Get-ChildItem -Recurse -File,Select-String์ผ๋ก ํ์ผ ๋ชฉ๋ก๊ณผ ๊ฒ์์ ์ํํฉ๋๋ค. - ํ๊ธ ๋ฌธ์/์์ค ํ์ผ ํ์ธ ์
Get-Content -Encoding utf8๋๋Get-Content -Raw -Encoding utf8์ ์ฌ์ฉํด ๊นจ์ง ์ถ๋ ฅ์ผ๋ก ์ธํ ์คํ์ ํผํฉ๋๋ค.
๊ธฐ๋ณธ ํ ์คํธ๋ ๋คํธ์ํฌ ์์ด ์คํ๋ฉ๋๋ค.
| Area | Files | Coverage Intent |
|---|---|---|
| Type conversion | tests/test_convert.py |
๋ ์ง/์๊ฐ/์ซ์/๋ถ๋ฆฌ์ธ/๊ณต๋ฐฑ ์ฒ๋ฆฌ |
| Code mappings | tests/test_codes.py, tests/test_vworld_sigungu.py |
Opinet โ BJD ์๋ ๋งคํ, ์๋ฐ ์ํ ํ์ , VWorld ์๊ตฐ๊ตฌ ๋งคํ |
| Coordinates | tests/test_client_endpoints.py |
WGS84โKATEC ๋ณํ๊ณผ ์คํผ๋ท ์์ฒญ/์๋ต ๊ฒฝ๊ณ |
| HTTP errors | tests/test_http.py |
์ธ์ฆ/์ฟผํฐ/5xx/๋คํธ์ํฌ/JSON ์ค๋ฅ |
| Async client | tests/test_async_client.py |
OpinetClient, async context manager, TPS, ์ธ์
์์ ๊ถ |
| Endpoints | tests/test_client_endpoints.py |
๊ณต์ 5๊ฐ API์ ํ์ , ํ๋ผ๋ฏธํฐ, ๋น ๊ฒฐ๊ณผ, ๋จ์ผ dict ์๋ต |
| API catalog/debug | tests/test_catalog.py, tests/test_debug.py, tests/test_generated_fixtures.py |
์นดํ๋ก๊ทธ export, DebugRun trace, ๋ฏผ๊ฐ์ ๋ณด ๋ง์คํน, fixture replay |
| Experimental boundary | tests/test_experimental.py |
๋ฏธ๊ฒ์ฆ API๊ฐ ๋ช ์์ ์ผ๋ก unimplemented์์ ๊ณ ์ |
ํ์ ๊ฒ์ฆ ๋ช ๋ น:
python -m compileall src/opinet tests
python -m pytest
python -m pytest --cov=opinet --cov-fail-under=90
python -m mypy src/opinet์ค์ ์คํผ๋ท ํธ์ถ์ ๊ธฐ๋ณธ ํ ์คํธ์ ๋ฃ์ง ์์ต๋๋ค.
OPINET_API_KEY๊ฐ ์์ ๋๋ง ๋์ํ๊ฒ ํฉ๋๋ค.- ํ
์คํธ์๋
@pytest.mark.live๋ฅผ ๋ถ์ ๋๋ค. - ์บก์ฒํ fixture์๋ ํค, ๊ฐ์ธ ์๋ณ ์ ๋ณด, ํธ์ถ URL์ ์ธ์ฆ ํ๋ผ๋ฏธํฐ๋ฅผ ๋จ๊ธฐ์ง ์์ต๋๋ค.
- ๋ผ์ด๋ธ ํธ์ถ๋ก ํ์ธํ ์๋ต๋ fixture์ ์ ์ฅํ ๋ ์ซ์/๋ ์ง/์๊ฐ ํ๋๋ ๋ฌธ์์ด ๊ทธ๋๋ก ๋ก๋๋ค.
.env๋ ๋ก์ปฌ ์ ์ฉ์ด๋ฉฐ ์ปค๋ฐํ์ง ์์ต๋๋ค..env.example๋ง ์ถ์ ํฉ๋๋ค.areaCode.do์ฒ๋ผ ํญ์ ๋ฐ์ดํฐ๊ฐ ์์ด์ผ ํ ์๋ํฌ์ธํธ๊ฐ ๋นRESULT.OIL์ ๋ฐํํ๋ฉด ํค๊ฐ ๊ณต์ open API ๊ฒ์ดํธ์จ์ด์ provision๋์ง ์์ ์ํ๋ก ๊ฐ์ฃผํ๊ณ ํ์ฑ smoke ํ ์คํธ๋ฅผ skipํฉ๋๋ค.
๋ผ์ด๋ธ ํ ์คํธ ์คํ:
pytest -m live --run-live๋ก์ปฌ .env์ ํค๋ก ์ค์ ์๋ฒ์ ์ฐ๊ฒฐํ์ ๋ areaCode.do์ JSON envelope๋ ์ ์ ์์ ๋์์ต๋๋ค. ๋ค๋ง ๊ณต์ 5๊ฐ API๊ฐ ๋ชจ๋ HTTP 200๊ณผ ๋น RESULT.OIL์ ๋ฐํํ๋ ์ํ๊ฐ ๊ด์ฐฐ๋์์ต๋๋ค. ์ด ์๋ต์ ํ์ ์ค๋ฅ๊ฐ ์๋๋ผ ํค๊ฐ ๊ณต์ open API ๊ฒ์ดํธ์จ์ด์ ๋ฐ์ดํฐ ๋ฐํ ๊ถํ์ผ๋ก provision๋์ง ์์ ์ํ์ผ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค.
๊ทธ๋์ live ํ ์คํธ๋ ๋ ๋จ๊ณ๋ก ๋๋ฉ๋๋ค.
- ์๋ฒ ์ฐ๊ฒฐ๊ณผ
RESULT.OILenvelope ํ์ธ์ pass/fail๋ก ๊ฒ์ฆํฉ๋๋ค. - ์ค์ ๋ฐ์ดํฐ ํ์ฑ smoke๋
areaCode.do๊ฐ ๋น์ด ์์ผ๋ฉด skipํ๊ณ , ๋ฐ์ดํฐ๊ฐ ๋ฐํ๋๋ ํค์์๋ ๊ณต์ 5๊ฐ API ์ ์ฒด๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
์ด๋ฏธ ํ ๋ฒ ํ์ธํ ํผ๋ ์ง์ ์ ๋๋ค.
StationDetail.tel์ ์ฌ์ฉํฉ๋๋ค.phoneํ๋๋ฅผ ์๋ก ๋ง๋ค์ง ์์ต๋๋ค.OilPrice์๋product_name์ด ์์ต๋๋ค.OIL_PRICE์๋ต์๋PRODNM์ด ์ค์ง ์์ต๋๋ค.RESULT.OIL๊ณผOIL_PRICE๋ list๋ฟ ์๋๋ผ ๋จ์ผ dict๋ก ์ฌ ์ ์์ต๋๋ค.- ์ค์ ์๋ต์ ์ํ ํ๋๋
POLL_DIV_CO,GPOLL_DIV_CO๊ฐ ์ฐ์ ์ ๋๋ค.*_CD๋ fallback์ ๋๋ค. - ๊ณต๋ฐฑ 1์(
" ")๋ ์๋ฏธ ์๋ ๋ฌธ์์ด์ด ์๋๋ผNone์ ๋๋ค. - ์ขํ ๋ณํ ์ ๋ฐ๋์ ์คํผ๋ท ๋ชจ๋ธ ๊ฒฝ๊ณ๋
python-opinet-apiํ ์คํธ๊ฐ ์์ ํฉ๋๋ค. - Unit HTTP mocks replay httpx calls with
respx.
์ ์๋ํฌ์ธํธ๋ฅผ ์ถ๊ฐํ ๋:
opinet-api.md๋๋ ์ค์ ์๋ต์ผ๋ก ํ๋๋ฅผ ํ์ธํฉ๋๋ค.- ๊ณต์ 5๊ฐ์ ์ํ์ง ์์ผ๋ฉด
opinet.experimental์ ๋ก๋๋ค. - ๋ชจ๋ธ ํ๋๋ Python ๋ค์ดํฐ๋ธ ํ์ ์ผ๋ก ์ ์ํฉ๋๋ค.
- fixture๋ ์ค์ ์๋ต์ฒ๋ผ ๋ฌธ์์ด ๊ฐ์ ์ ์งํฉ๋๋ค.
- happy path, empty result, error response, ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ ํ ์คํธ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
- README์ ์ด ๋ฌธ์๋ฅผ ๊ฐฑ์ ํฉ๋๋ค.
2026-05-06 ๊ธฐ์ค์ผ๋ก ๊ธฐ์กด public API๋ฅผ ์ ์งํ๋ฉด์ optional/additive normalized layer๋ฅผ ์ถ๊ฐํ์ต๋๋ค. ์ด layer๋ OpiNet provider row๋ฅผ ์ฌ๋ฌ ํ๋ก์ ํธ์์ ๋ฐ๋ก ์ฌ์ฌ์ฉํ ์ ์๊ฒ ํ๋ ๊ณต์ฉ ํด์ ๊ณ์ธต์ด๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์ ๋ณ DB ์ ์ฅ, ETL cache, serving table, ์๋น์ค enum ์ ์ฑ ์ ํฌํจํ์ง ์์ต๋๋ค.
| ์์ญ | ๊ตฌํ |
|---|---|
| canonical ์ ์ข | FuelType ๋ฐ CanonicalFuelType alias |
| ์ ์ข mapping | product_code_to_fuel_type(), fuel_type_to_product_code() |
| ์ขํ ํ๋ | lon, lat, katec_x, katec_y, lon_lat, katec_xy |
| raw payload | AvgPrice.raw, Station.raw, StationDetail.raw, OilPrice.raw, AreaCode.raw |
| Station product context | Station.product_code, Station.product_name |
| Station trade context | Station.trade_date, Station.trade_time |
| Station normalized helpers | provider_station_id, provider_product_code, provider_product_name, fuel_type, brand_code, lon_lat, katec_xy |
| AvgPrice normalized helpers | provider_product_code, provider_product_name, fuel_type |
| OilPrice helper | provider_product_code, fuel_type |
| AreaCode helpers | code_level, parent_sido_code, bjd_sido_prefix |
- ๊ธฐ์กด dataclass ํ๋๋ ์์๋ฅผ ์ ์งํ์ต๋๋ค.
- ์ ์ ํ ํ๋๋ ๊ธฐ์กด ํ๋ ๋ค์ ์ถ๊ฐํ์ต๋๋ค.
- ๋ชจ๋ model์
raw๋ ๋ง์ง๋ง dataclass ํ๋์ ๋๋ค. raw๋ ์ฝ๊ธฐ ์ ์ฉ mapping์ด๋ฉฐ, nestedOIL_PRICE๋ tuple๊ณผ read-only mapping์ผ๋ก ๊ฐ๋ฅํ ํ ์ํ ๋ฌธ์์ด ๊ฐ์ ๋ณด์กดํฉ๋๋ค.certkey,api_key,authorization,x-api-key๊ฐ์ ์ธ์ฆ ๊ด๋ จ key๋ raw์์ ์ ์ธํฉ๋๋ค.
lowTop10.do์ aroundAll.do์ Station row์๋ PRODCD/PRODNM์ด ์๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
- row์
PRODCD๊ฐ ์์ผ๋ฉด ์๋ต ๊ฐ์Station.product_code๋ก ์ฌ์ฉํฉ๋๋ค. - row์
PRODCD๊ฐ ์์ผ๋ฉด ์์ฒญ์ ์ฌ์ฉํprodcd๋ฅผStation.product_code์ ์ฑ์๋๋ค. - row์
PRODNM์ด ์์ผ๋ฉดStation.product_name์ ๋ณด์กดํฉ๋๋ค. - row์
TRADE_DT/TRADE_TM์ด ์์ผ๋ฉดStation.trade_date/Station.trade_time์ผ๋ก ๋ณํํฉ๋๋ค. - ํ๋๊ฐ ์์ผ๋ฉด
product_name,trade_date,trade_time์None์ ๋๋ค.
tests/test_normalized_models.py์ fixture ๊ธฐ๋ฐ ํ๊ท ํ
์คํธ๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
| ์ผ์ด์ค | ๊ฒ์ฆ |
|---|---|
| ProductCode โ FuelType | gasoline, premium_gasoline, diesel, lpg, kerosene roundtrip |
| mapping ์คํจ | ์ ์ ์๋ product code, ์ ์ ์๋ fuel type, FuelType.UNKNOWN ์ญ๋ณํ ์คํจ |
| AreaCode | 01, 0113, 001, ๋ฏธํ์ธ ์๋ code |
| BJD prefix | AreaCode.bjd_sido_prefix๊ฐ opinet_sido_to_bjd ๊ธฐ๋ฐ์ผ๋ก ๋์ |
| ์ขํ value object | ๊ธฐ์กด float ํ๋ ์ ์ง ๋ฐ Station.coordinates ์ ๊ณต |
| Station request product | lowTop10.do, aroundAll.do ์์ฒญ prodcd๊ฐ Station์ ๋ณด์กด |
| Station response product | ์๋ต PRODCD๊ฐ ์์ผ๋ฉด ์์ฒญ ๊ฐ๋ณด๋ค ์ฐ์ |
| Station trade context | ์๋ต TRADE_DT/TRADE_TM ๋ฌธ์์ด์ date/time์ผ๋ก ๋ณํ |
| raw payload | ํ์ ๋ณํ ์ ๋ฌธ์์ด ๊ฐ ๋ณด์กด, top-level ๋ฐ nested raw read-only ํ์ธ |
| raw shape | mapping์ด ์๋ raw payload๋ ๋ช ์์ ์ผ๋ก ์คํจ |
import asyncio
from opinet import OpinetClient, ProductCode
async def main() -> None:
async with OpinetClient() as client:
station = (await client.search_stations_around(
lon=127.0276,
lat=37.4979,
prodcd=ProductCode.DIESEL,
))[0]
record = {
"provider_station_id": station.provider_station_id,
"provider_product_code": station.provider_product_code,
"fuel_type": station.fuel_type.value,
"brand_code": station.brand_code,
"price": station.price,
"trade_date": station.trade_date,
"trade_time": station.trade_time,
"lon": station.coordinates.wgs84.lon,
"lat": station.coordinates.wgs84.lat,
"raw_price": station.raw.get("PRICE"),
}
asyncio.run(main())์ด ์์๋ python-opinet-api์ normalized ๋ชจ๋ธ๋ง ์ฌ์ฉํฉ๋๋ค. ์ ์ฅ schema, cache ์ ๋ต, raw ๋ณด๊ด ์ฌ๋ถ๋ ํธ์ถ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ ์์ ๋๋ค.
2026-05-06์ opinet.normalized ๋ชจ๋์ ์ถ๊ฐํ์ต๋๋ค. ์ด ๋ชจ๋์ ๊ธฐ์กด ์๋ต ๋ชจ๋ธ์ ์์ฑ ์์ ์ฑ ์นํ์ ์ธ Pydantic DTO๋ฅผ ์น๋ ๊ณ์ธต์
๋๋ค. ๊ธฐ์กด public model๊ณผ client method๋ ๋ณ๊ฒฝํ์ง ์๊ณ , AvgPrice.to_normalized(), Station.to_normalized(), AreaCode.to_normalized()๋ก ๋ณํํฉ๋๋ค.
DTO๋ Pydantic v2 BaseModel ๊ธฐ๋ฐ์ด๋ฉฐ frozen=True, extra="forbid" ์ค์ ์ ์ฌ์ฉํฉ๋๋ค. ํธ์ถ ์ฑ์ model_dump() ๋๋ model_dump(mode="json")๋ก ์ ์ฅ ๊ณ์ธต์ ๋๊ธธ payload๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
| DTO | Source model | ๊ธฐ๋ณธ endpoint | ์ฃผ์ ํ๋ |
|---|---|---|---|
NormalizedFuelAverage |
AvgPrice |
avgAllPrice.do |
provider, provider_endpoint, provider_product_code, provider_product_name, fuel_type, trade_date, price, diff, raw |
NormalizedFuelStation |
Station |
ํธ์ถ์๊ฐ ์ง์ | provider_station_id, provider_station_name, provider_product_code, provider_product_name, fuel_type, brand_code, price, distance_m, ์ฃผ์, ์ขํ, ๊ฑฐ๋์๊ฐ, raw |
NormalizedFuelRegionCode |
AreaCode |
areaCode.do |
provider_region_code, provider_region_name, code_level, parent_sido_code, bjd_sido_prefix, raw |
๋ชจ๋ DTO์ provider ๊ฐ์ "opinet"์
๋๋ค. Station DTO์ provider_endpoint๋ lowTop10.do์ aroundAll.do ์ค ํธ์ถ ๋งฅ๋ฝ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ฏ๋ก ๋ณํ ์ ๋ช
์ํ๊ฒ ํ์ต๋๋ค.
NormalizedFuelAverage.price_datetime(tz="Asia/Seoul"):trade_date๋ฅผ ํด๋น timezone์ ์์ ์ผ๋ก ๋ณํํฉ๋๋ค.NormalizedFuelAverage.price_timestamp(tz="Asia/Seoul"): ์ datetime์ Unix timestamp๋ฅผ ๋ฐํํฉ๋๋ค.NormalizedFuelStation.trade_datetime(tz="Asia/Seoul"):trade_date์trade_time์ด ๋ชจ๋ ์์ ๋ timezone-aware datetime์ ๋ฐํํฉ๋๋ค. ๋ ์ค ํ๋๋ผ๋ ์์ผ๋ฉดNone์ ๋๋ค.- ๊ธฐ์กด
AvgPrice.price_datetime(),AvgPrice.price_timestamp(),Station.trade_datetime()๋ ๋์ผํ helper๋ก ์ฐ๊ฒฐํ์ต๋๋ค.
to_json_safe_raw()์ alias raw_to_json_safe()๋ read-only MappingProxyType, tuple๋ก ๋ณด์กด๋ nested OIL_PRICE, enum, date/time ๊ฐ์ JSON-safe plain dict/list ๊ตฌ์กฐ๋ก ๋ณํํฉ๋๋ค. ๊ธฐ๋ณธ fixture ๊ฒฝ๋ก์์๋ ์ซ์/๋ ์ง/์๊ฐ raw ๊ฐ์ด ์๋ณธ ๋ฌธ์์ด ๊ทธ๋๋ก ๋ณด์กด๋ฉ๋๋ค.
from opinet.normalized import to_json_safe_raw
raw = to_json_safe_raw(station.raw)
# json.dumps(raw, ensure_ascii=False) ๊ฐ๋ฅtests/test_normalized_records.py๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
| ์ผ์ด์ค | ๊ฒ์ฆ |
|---|---|
| normalized record ์์ฑ | ์ธ DTO์ provider/endpoint/provider code/name/fuel field ํ์ธ |
| AvgPrice KST timestamp | ๋ ์ง-only ํ๊ท ๊ฐ๊ฐ Asia/Seoul ์์ datetime/timestamp๋ก ๋ณํ๋จ |
| Station trade datetime | trade field ์์์ None, TRADE_DT/TRADE_TM ์์์ KST datetime |
| AreaCode normalized | code level, parent sido, BJD sido prefix ํฌํจ |
| raw JSON-safe | nested OIL_PRICE๊ฐ plain list/dict๋ก ๋ณํ๋๊ณ json.dumps ๊ฐ๋ฅ |
| PRODNM ์๋ Station | ์์ฒญ prodcd๋ product code๋ก ๋ณด์กด๋๊ณ provider product name์ None |
| Pydantic ๋์ | DTO๊ฐ BaseModel์ด๋ฉฐ frozen/extra forbid/model_dump ๋์ |
2026-05-06์ PEP 561 typed package ์ง์์ ์ถ๊ฐํ์ต๋๋ค.
src/opinet/py.typedmarker๋ฅผ ์ถ๊ฐํ์ต๋๋ค.pyproject.toml์ setuptools package data์py.typed๋ฅผ ๋ฑ๋กํ์ต๋๋ค.- dev extra์ packaging smoke์ฉ
build,wheel,setuptools>=68๋ฅผ ๋ช ์ํ์ต๋๋ค. tests/test_pep561_packaging.py์์ wheel/sdist๋ฅผ ๋น๋ํ๊ณ , ๊ฐ ์ฐ์ถ๋ฌผ์ ์์ venv์ ์ค์นํ ๋คimport opinet,import opinet.normalized, downstream mypy reveal smoke๋ฅผ ์คํํฉ๋๋ค.
์ด ํ ์คํธ๋ Pydantic DTO์ ๊ธฐ์กด public export๊ฐ ์ค์น ํ๊ฒฝ์์๋ typed package๋ก ๋ณด์ด๋์ง ํ์ธํฉ๋๋ค.
OpinetClient์ ์กฐํ์ ๋๋ฒ๊ทธ๋ await, iter_stations_in_bbox๋ async for,
์ข
๋ฃ๋ async with ๋๋ await client.aclose()๋ฅผ ์ฌ์ฉํ๋ค. Async ์ ๋์ฌ ํด๋ผ์ด์ธํธ,
aio ํฉํฐ๋ฆฌ์ ๋๊ธฐ transport๋ ์ ๊ฑฐํ๋ค. ์ฝ๋ํยท์ขํยท๋ชจ๋ธ ๋ณํยทfixture ์ ์ฅ ๊ฐ์
๋ก์ปฌ ์ ํธ๋ฆฌํฐ๋ ์ผ๋ฐ ํจ์๋ค. ๊ธฐ์กด ์๋ํฌ์ธํธ ์ธ์์ ๋ฐํ ๋ชจ๋ธ์ ์ ์งํ๋ค.
๊ธฐ๋ณธ max_rps=5.0์ด๋ค. AsyncTokenBucket(max_rps, capacity=...)๋ฅผ
rate_limiter=์ ์ฃผ์
ํ๋ฉด ์ฌ๋ฌ ํด๋ผ์ด์ธํธ๊ฐ ๊ฐ์ ์์ฒญ ์์ฐ์ ์ด๋ค. ์ฃผ์
๋ ๋ฒํท์ด
max_rps๋ณด๋ค ์ฐ์ ํ๋ค. ๊ธฐ๋ณธ capacity๋ max(1, max_rps)์ด๋ฉฐ ์ด๊ธฐ์๋ ๊ฐ๋ ์ฐจ
์์ผ๋ฏ๋ก burst๋ฅผ ํ์ฉํ๋ค. ์ผ์ ํ ๊ฐ๊ฒฉ์ capacity=1๋ก ์ค์ ํ๋ค.
๊ฐ ์์ฒญยท์ฌ์๋ยท๋ฆฌ๋ค์ด๋ ํธยท๋๋ฒ๊ทธยท๊ฒฉ์ ์
์ ๊ฐ์ ๋ฒํท์ ์ ์ฉํ๋ค.
์ธ์ ๊ฒ์ฆ ์คํจ์ ์บ์ ์ ์ค์ ์์ฒญ์ ๋ณด๋ด์ง ์๋๋ค. TPS๋ ์ผ์ผ ์ฟผํฐ๋ฅผ ๋์ ํ์ง ์๋๋ค.
๋ฒํท์ ํ ์ด๋ฒคํธ ๋ฃจํ์์ ์ฌ์ฉํ๋ค. ๋๊ธฐ ์ทจ์๋ ํ ํฐ์ ์๋นํ์ง ์๊ณ ๋ค์ ๋๊ธฐ์๋ฅผ ์งํ์ํจ๋ค. 401/403/429๋ ์ฆ์ ์คํจํ๋ฉฐ ๋คํธ์ํฌ ์ค๋ฅ์ 5xx๋ง ๊ธฐ์กด backoff๋ก ์ฌ์๋ํ๋ค. ์ฌ์ฉ์ ์ ์ ์ธ์ฆ/transport ๋ด๋ถ์์ ๋ฐ์ํ๋ ์ถ๊ฐ ์ ์ก์ ๊ณ์ธก ๋ฒ์ ๋ฐ์ด๋ค.
๋ด๋ถ HTTP ์ธ์ ์ ์ฒซ ์์ฒญ ์ ์์ฑํ๊ณ ์ข ๋ฃ ์ ๋ซ๋๋ค. session์ ์ฃผ์ ํ๋ ๋น๋๊ธฐ ์ธ์ ์ ํธ์ถ์๊ฐ ๋ซ๋๋ค. ์ข ๋ฃํ ํด๋ผ์ด์ธํธ์ ์ถ๊ฐ ์์ฒญ์ ์คํจํ๋ค. ๋๋ฒ๊ทธ ๊ธฐ๋ก์ ContextVar๋ก ํธ์ถ๋ณ ๊ฒฉ๋ฆฌํ๊ณ ์ฑ๊ณตยท์คํจยท์ทจ์ ๋ชจ๋์์ ๋ณต์ํ๋ค. ์๋ต ํ์ฑ์ ์๋ฌธ์ผ๋ก ์๋ฃํ ํ ์ง๋จ ๊ฒฐ๊ณผ์ ์๋ ค์ง ํค์ ์ธ์ฝ๋ฉ๋ ํค๋ฅผ ๋ง์คํนํ๋ค. VWorld ์ฐ๋์ ๋น๋๊ธฐ search_district๋ฅผ ์ฌ์ฉํ๋ฉฐ ์๋ฃ๋ ์ง์ญ ์ฝ๋๋ง ์บ์ํ๋ค.
์์ ๋ docs/async-tps.md๋ฅผ ์ฐธ๊ณ ํ๋ค.