diff --git a/arcsolve/services/ev_charger/README.md b/arcsolve/services/ev_charger/README.md
index 6e62c59..785a1d6 100644
--- a/arcsolve/services/ev_charger/README.md
+++ b/arcsolve/services/ev_charger/README.md
@@ -53,8 +53,8 @@ GET·**XML**. **서비스키 필수**(공공데이터포털 발급), 키는 **
## 도구
| 도구 | 설명 |
|------|------|
-| `evcharger_status(zcode?, zscode?, period?, numOfRows?, pageNo?)` ⭐ | 충전기 실시간 상태(충전중/대기/통신이상/운영중지/점검중/상태미확인 + 상태갱신일시). ⚠️ 약 5분 지연 캐시 |
-| `evcharger_info(zcode?, zscode?, numOfRows?, pageNo?)` | 충전소 정보(충전기 타입 코드·주소·위경도·운영기관·이용가능시간) |
+| `ev_charger_status(zcode?, zscode?, period?, numOfRows?, pageNo?)` ⭐ | 충전기 실시간 상태(충전중/대기/통신이상/운영중지/점검중/상태미확인 + 상태갱신일시). ⚠️ 약 5분 지연 캐시 |
+| `ev_charger_info(zcode?, zscode?, numOfRows?, pageNo?)` | 충전소 정보(충전기 타입 코드·주소·위경도·운영기관·이용가능시간) |
> `zcode`(시도)/`zscode`(시군구)는 **행정구역 지역코드**(zcode=행정구역코드 앞 2자리, 예: 11=서울). 둘 다 생략 시 전국. `period`(분)는 상태갱신 조회범위(기본 5·최소 1·최대 10).
diff --git a/arcsolve/services/ev_charger/tools.py b/arcsolve/services/ev_charger/tools.py
index 1141ccb..e3bf4de 100644
--- a/arcsolve/services/ev_charger/tools.py
+++ b/arcsolve/services/ev_charger/tools.py
@@ -155,7 +155,7 @@ def register(mcp: FastMCP) -> None:
"""이 서비스의 도구를 서버에 등록한다."""
@mcp.tool
- async def evcharger_status(
+ async def ev_charger_status(
zcode: str | None = None,
zscode: str | None = None,
period: int = c.DEFAULT_PERIOD,
@@ -209,7 +209,7 @@ async def evcharger_status(
return "\n".join(lines)
@mcp.tool
- async def evcharger_info(
+ async def ev_charger_info(
zcode: str | None = None,
zscode: str | None = None,
numOfRows: int = c.DEFAULT_NUM_OF_ROWS, # noqa: N803 (공식 파라미터명)
diff --git a/changelog.d/ev_charger.md b/changelog.d/ev_charger.md
index 392d8b2..68f6eaa 100644
--- a/changelog.d/ev_charger.md
+++ b/changelog.d/ev_charger.md
@@ -1 +1 @@
-- **ev_charger**: 전기차 충전소(한국환경공단 EvCharger) 정보·실시간 상태 읽기 서비스 추가 — 충전기 실시간 상태·충전소 정보 2개 GET 도구(`evcharger_status`/`evcharger_info`, 폴더 `ev_charger`·prefix `evcharger_`), data.go.kr 서비스키는 쿼리 파라미터 `serviceKey`(필수, **Decoding 키** — 이중 인코딩 방지), airkorea·egen과 같은 기관(B552584), 응답은 **XML**이라 `get_text`+`xml.etree`로 파싱(egen 패턴), 봉투 `resultCode != "00"`/게이트웨이 `cmmMsgHeader` 에러 매핑(서비스키/트래픽), 지역코드 `zcode`(시도)·`zscode`(시군구) 선택 필터, `stat` 상태 코드(1통신이상·2충전대기·3충전중·4운영중지·5점검중·9상태미확인) 한글 표시, `chgerType` 타입 코드는 가이드 코드표로 라벨링(미상 코드는 원본 보존), `numOfRows`[10,9999]·`period`[1,10] 클램프, 좌표·상태·플래그는 문자열·결측 보존. ⚠️ 상태는 약 5분 주기 갱신(캐시 스냅샷) — 출력에 지연 명시
+- **ev_charger**: 전기차 충전소(한국환경공단 EvCharger) 정보·실시간 상태 읽기 서비스 추가 — 충전기 실시간 상태·충전소 정보 2개 GET 도구(`ev_charger_status`/`ev_charger_info`, 폴더 `ev_charger`·prefix `ev_charger_`), data.go.kr 서비스키는 쿼리 파라미터 `serviceKey`(필수, **Decoding 키** — 이중 인코딩 방지), airkorea·egen과 같은 기관(B552584), 응답은 **XML**이라 `get_text`+`xml.etree`로 파싱(egen 패턴), 봉투 `resultCode != "00"`/게이트웨이 `cmmMsgHeader` 에러 매핑(서비스키/트래픽), 지역코드 `zcode`(시도)·`zscode`(시군구) 선택 필터, `stat` 상태 코드(1통신이상·2충전대기·3충전중·4운영중지·5점검중·9상태미확인) 한글 표시, `chgerType` 타입 코드는 가이드 코드표로 라벨링(미상 코드는 원본 보존), `numOfRows`[10,9999]·`period`[1,10] 클램프, 좌표·상태·플래그는 문자열·결측 보존. ⚠️ 상태는 약 5분 주기 갱신(캐시 스냅샷) — 출력에 지연 명시
diff --git a/docs/providers.md b/docs/providers.md
index 5992bae..bababa2 100644
--- a/docs/providers.md
+++ b/docs/providers.md
@@ -333,8 +333,8 @@
- 한국환경공단_전기자동차 충전소 정보 OpenAPI(EvCharger) 상세(base·오퍼레이션·serviceKey/pageNo/numOfRows/period/zcode·getChargerStatus 응답 필드·stat 코드·실시간 5분 갱신): https://www.data.go.kr/data/15076352/openapi.do
- 전국전기차충전소표준데이터(정보 필드 한글 라벨 교차참조): https://www.data.go.kr/data/15013115/standard.do
- 도구(MVP, 전부 GET·읽기):
- - `evcharger_status(zcode?, zscode?, period?, numOfRows?, pageNo?)` ⭐ — `/getChargerStatus` 충전기 실시간 상태(충전중/충전대기/통신이상/운영중지/점검중/상태미확인 + 상태갱신일시). 지역코드 필터.
- - `evcharger_info(zcode?, zscode?, numOfRows?, pageNo?)` — `/getChargerInfo` 충전소 정보(충전기 타입 코드·주소·위경도·운영기관·이용가능시간). 지역코드 필터.
+ - `ev_charger_status(zcode?, zscode?, period?, numOfRows?, pageNo?)` ⭐ — `/getChargerStatus` 충전기 실시간 상태(충전중/충전대기/통신이상/운영중지/점검중/상태미확인 + 상태갱신일시). 지역코드 필터.
+ - `ev_charger_info(zcode?, zscode?, numOfRows?, pageNo?)` — `/getChargerInfo` 충전소 정보(충전기 타입 코드·주소·위경도·운영기관·이용가능시간). 지역코드 필터.
- 응답: 봉투 XML ` …` — 본문 페이지네이션. **`resultCode != "00"`이면 에러**(HTTP 200이라도 봉투로 옴; 게이트웨이 키 차단은 `` 대신 `cmmMsgHeader/returnReasonCode`로 옴 → 30 등 매핑). 상태(`stat`)·타입(`chgerType`)·위경도·플래그(`*Yn`)는 **문자열**·결측 빈 값 → 캐스팅 금지. `stat` 코드: 1통신이상·2충전대기·3충전중·4운영중지·5점검중·9상태미확인(한글 표시).
- 제약(공식): `zcode`(시도)·`zscode`(시군구)는 **행정구역 지역코드**(zcode=앞 2자리, 선택). `period`(분, status 전용) 기본 5·[1,10] 클램프. `numOfRows` 기본 100·**[10,9999]** 클램프·`pageNo` 기본 1.
- ⚠️ 실시간 지연: `getChargerStatus`는 "실시간"이지만 상류가 **약 5분 주기** 갱신 → 결과는 수 분 지연된 캐시 스냅샷(`statUpdDt`로 갱신시각 확인). 도구 출력 헤더에 "약 5분 지연(캐시 스냅샷)" 명시.
diff --git a/docs/services.md b/docs/services.md
index 296b044..ce1efa6 100644
--- a/docs/services.md
+++ b/docs/services.md
@@ -65,8 +65,8 @@
| 도구 | 설명 |
|------|------|
-| `evcharger_info` | 충전소 정보를 조회한다(GET /getChargerInfo). |
-| `evcharger_status` | 충전기 실시간 상태를 조회한다(GET /getChargerStatus). |
+| `ev_charger_info` | 충전소 정보를 조회한다(GET /getChargerInfo). |
+| `ev_charger_status` | 충전기 실시간 상태를 조회한다(GET /getChargerStatus). |
## feeds — RSS/Atom/RDF 피드 읽기(임의 피드 URL → 메타·최근 항목)
공식 문서: https://www.rssboard.org/rss-specification
diff --git a/tests/test_ev_charger_tools.py b/tests/test_ev_charger_tools.py
index bfa61e1..a61f807 100644
--- a/tests/test_ev_charger_tools.py
+++ b/tests/test_ev_charger_tools.py
@@ -67,7 +67,7 @@ async def test_status_request_and_output(tools, monkeypatch, recording_http):
http = recording_http(ret=STATUS_XML)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"](zcode="11", zscode="11680")
+ out = await tools["ev_charger_status"](zcode="11", zscode="11680")
assert http.last["url"] == "http://apis.data.go.kr/B552584/EvCharger/getChargerStatus"
# 서비스키는 쿼리 파라미터(헤더 아님), Decoding 키 원문 그대로(이중 인코딩 방지).
assert http.last["params"]["serviceKey"] == "DECODED_KEY"
@@ -86,7 +86,7 @@ async def test_status_request_and_output(tools, monkeypatch, recording_http):
async def test_status_omits_region_when_none(tools, monkeypatch, recording_http):
http = recording_http(ret=STATUS_XML)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"]()
+ out = await tools["ev_charger_status"]()
assert "zcode" not in http.last["params"]
assert "zscode" not in http.last["params"]
assert "전국" in out # 지역 미지정 → 전국
@@ -97,7 +97,7 @@ async def test_status_missing_key_no_network(monkeypatch, load_tools, recording_
tools = load_tools(register)
http = recording_http(ret=STATUS_XML)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"](zcode="11")
+ out = await tools["ev_charger_status"](zcode="11")
assert "EV_CHARGER_SERVICE_KEY" in out
assert "Decoding" in out # 이중 인코딩 함정 안내
assert not http.calls # HTTP 전에 막힘
@@ -106,14 +106,14 @@ async def test_status_missing_key_no_network(monkeypatch, load_tools, recording_
async def test_status_empty_items(tools, monkeypatch, recording_http):
http = recording_http(ret=_envelope("", total=0))
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"](zcode="50")
+ out = await tools["ev_charger_status"](zcode="50")
assert "데이터 없음" in out
async def test_status_period_clamped(tools, monkeypatch, recording_http):
http = recording_http(ret=STATUS_XML)
monkeypatch.setattr(f"{MOD}.get_text", http)
- await tools["evcharger_status"](period=99)
+ await tools["ev_charger_status"](period=99)
assert http.last["params"]["period"] == 10 # 최대 10으로 클램프
@@ -124,7 +124,7 @@ async def test_info_request_and_output(tools, monkeypatch, recording_http):
http = recording_http(ret=INFO_XML)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_info"](zcode="28")
+ out = await tools["ev_charger_info"](zcode="28")
assert http.last["url"].endswith("/getChargerInfo")
assert http.last["params"]["zcode"] == "28"
assert "period" not in http.last["params"] # period는 status 전용
@@ -145,7 +145,7 @@ async def test_info_unknown_chger_type_preserves_code(tools, monkeypatch, record
)
http = recording_http(ret=xml)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_info"](zcode="11")
+ out = await tools["ev_charger_info"](zcode="11")
assert "타입 99" in out
assert "99(" not in out # 미상 코드엔 괄호 라벨 없음
@@ -159,7 +159,7 @@ async def test_result_code_30_unregistered_key(tools, monkeypatch, recording_htt
ret=_envelope("", result_code="30", result_msg="SERVICE_KEY_IS_NOT_REGISTERED_ERROR")
)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"](zcode="11")
+ out = await tools["ev_charger_status"](zcode="11")
assert "등록되지 않은 서비스키" in out
assert "Decoding" in out # 이중 인코딩 힌트
@@ -168,7 +168,7 @@ async def test_gateway_cmmmsgheader_error(tools, monkeypatch, recording_http):
# 게이트웨이 차단은 없이 cmmMsgHeader로 온다(HTTP 200) — 30으로 매핑.
http = recording_http(ret=GATEWAY_ERROR_XML)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_info"](zcode="11")
+ out = await tools["ev_charger_info"](zcode="11")
assert "등록되지 않은 서비스키" in out
@@ -177,21 +177,21 @@ async def test_result_code_22_traffic_limit(tools, monkeypatch, recording_http):
ret=_envelope("", result_code="22", result_msg="LIMITED_NUMBER_OF_SERVICE_REQUESTS")
)
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"]()
+ out = await tools["ev_charger_status"]()
assert "요청 제한" in out
async def test_result_code_03_no_data(tools, monkeypatch, recording_http):
http = recording_http(ret=_envelope("", result_code="03", result_msg="NODATA_ERROR"))
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_info"](zcode="99")
+ out = await tools["ev_charger_info"](zcode="99")
assert "데이터 없음" in out
async def test_unknown_result_code(tools, monkeypatch, recording_http):
http = recording_http(ret=_envelope("", result_code="77", result_msg="WEIRD"))
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"](zcode="11")
+ out = await tools["ev_charger_status"](zcode="11")
assert "resultCode=77" in out and "WEIRD" in out
@@ -201,14 +201,14 @@ async def test_unknown_result_code(tools, monkeypatch, recording_http):
async def test_maps_http_401(tools, monkeypatch, recording_http):
http = recording_http(exc=UpstreamError(401, {"returnAuthMsg": "SERVICE ACCESS DENIED"}))
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"](zcode="11")
+ out = await tools["ev_charger_status"](zcode="11")
assert "401" in out and "EV_CHARGER_SERVICE_KEY" in out
async def test_mapped_http_error_does_not_leak_non_dict_detail(tools, monkeypatch, recording_http):
http = recording_http(exc=UpstreamError(403, "403 Forbidden"))
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_info"](zcode="11")
+ out = await tools["ev_charger_info"](zcode="11")
assert "403" in out
assert "" not in out and "" not in out
@@ -216,12 +216,12 @@ async def test_mapped_http_error_does_not_leak_non_dict_detail(tools, monkeypatc
async def test_unmapped_http_error_500(tools, monkeypatch, recording_http):
http = recording_http(exc=UpstreamError(500, {"resultMsg": "INTERNAL"}))
monkeypatch.setattr(f"{MOD}.get_text", http)
- out = await tools["evcharger_status"]()
+ out = await tools["ev_charger_status"]()
assert "500" in out
async def test_maps_xml_parse_error(tools, monkeypatch, recording_http):
http = recording_http(ret="