k-skill/docs/features/zipcode-search.md
Jeffrey (Dongkyu) Kim ca490a5e6e Harden postcode lookup guidance for flaky shell/runtime conditions
The zipcode-search docs were directionally correct, but they left a few practical failure modes underexplained for real operator use. This update makes the retry order explicit, adds guidance for temp-file based parsing in wrapped shells, and documents the benign curl (23) case when downstream readers close early.

Constraint: Keep the change docs-first without adding runtime code or dependencies
Constraint: The official ePost endpoint remains intermittently flaky, so guidance must focus on stable operator behavior rather than pretending transport is deterministic
Rejected: Replace the docs example with a larger shell wrapper script | too heavy for a small skill-doc improvement
Rejected: Ignore curl (23) and here-doc issues as user error | repeated operator confusion is worth documenting
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If the zipcode-search example changes again, keep the retry order and shell-wrapper caveats aligned between the skill doc, feature doc, and regression test
Tested: node --check scripts/skill-docs.test.js
Tested: node --test scripts/skill-docs.test.js
Tested: npm run ci
Tested: npx --yes skills add . --list
Not-tested: Live ePost requests for this doc-only follow-up
2026-03-26 23:58:01 +09:00

3.8 KiB

우편번호 검색 가이드

이 기능으로 할 수 있는 일

  • 주소 키워드로 공식 우체국 우편번호 조회
  • 같은 도로명/건물명 후보가 여러 개일 때 상위 결과 비교
  • 검색 결과가 없을 때 바로 재검색 키워드 조정

먼저 필요한 것

  • 인터넷 연결
  • curl
  • 선택 사항: python3

입력값

  • 주소 키워드
    • 예: 세종대로 209
    • 예: 판교역로 235

기본 흐름

  1. 비공식 지도/블로그 검색으로 우회하지 말고 우체국 공식 검색 페이지를 먼저 조회합니다.
  2. 주소 키워드를 keyword 파라미터로 넘겨 HTML 결과를 받습니다.
  3. 결과에서 우편번호(sch_zipcode)와 표준 주소(sch_address1), 건물명(sch_bdNm)을 추출합니다.
  4. 후보가 여러 개면 상위 3~5개만 간단히 비교해 줍니다.
  5. 전송 timeout/reset이 나면 curl 재시도 옵션을 유지한 채 한 번 더 돌리고, 그래도 실패하면 세종대로 209 같은 짧은 도로명 + 건물번호 → 서울 종로구 세종대로 209 같은 시/군/구 포함 전체 주소 → 동/리 + 지번 순으로 재시도합니다.

예시

python3 - <<'PY'
import html
import re
import subprocess

query = "세종대로 209"
cmd = [
    "curl",
    "--http1.1",
    "--tls-max",
    "1.2",
    "--silent",
    "--show-error",
    "--location",
    "--retry",
    "3",
    "--retry-all-errors",
    "--retry-delay",
    "1",
    "--max-time",
    "20",
    "--get",
    "--data-urlencode",
    f"keyword={query}",
    "https://parcel.epost.go.kr/parcel/comm/zipcode/comm_newzipcd_list.jsp",
]
result = subprocess.run(
    cmd,
    check=True,
    capture_output=True,
    text=True,
    encoding="utf-8",
)
page = result.stdout

matches = re.findall(
    r'name="sch_zipcode"\s+value="([^"]+)".*?name="sch_address1"\s+value="([^"]+)".*?name="sch_bdNm"\s+value="([^"]*)"',
    page,
    re.S,
)

if not matches:
    raise SystemExit("검색 결과가 없습니다.")

for zip_code, address, building in matches[:5]:
    suffix = f" ({building})" if building else ""
    print(f"{zip_code}\t{html.unescape(address)}{suffix}")
PY

실전 운영 팁

  • 쉘 래퍼나 에이전트 환경에서는 here-doc + Python one-liner보다 mktemp 같은 임시 파일에 HTML을 저장한 뒤 파싱하는 쪽이 더 안전합니다.
  • 응답 일부만 빨리 보려고 curl ... | head 를 붙이면 다운스트림이 먼저 닫히면서 curl: (23) 이 보일 수 있습니다. 이때는 전체 응답을 임시 파일에 저장한 뒤 확인합니다.
  • 재시도 순서는 보통 세종대로 209 같은 짧은 도로명 + 건물번호 → 서울 종로구 세종대로 209 같은 전체 주소 → 동/리 + 지번 순이 가장 덜 헷갈립니다.

프로토콜/클라이언트 제약

  • 현재 ePost 엔드포인트는 로컬 기본 urllib 전송으로 붙으면 TLS/HTTP 협상 중 연결 reset이 날 수 있습니다.
  • 현재 ePost 엔드포인트는 같은 curl 플래그여도 간헐적인 timeout/reset이 있을 수 있으므로 문서 기본 예시는 --retry 3 --retry-all-errors --retry-delay 1을 포함합니다.
  • 문서 기본 예시는 curl --http1.1 --tls-max 1.2 전송을 사용하고, Python은 응답 파싱/정리에만 사용합니다.
  • 바깥쪽 Python timeout은 두지 않고 curl 자체 제한(--max-time + --retry)으로 전체 전송 시간을 제어합니다.
  • 다른 클라이언트를 쓰더라도 최소한 HTTP/1.1 + TLS 1.2 경로에서 실제 응답을 먼저 확인한 뒤 정규식 추출을 붙입니다.

주의할 점

  • 같은 번지라도 건물명에 따라 여러 행이 나올 수 있으니 첫 결과만 바로 확정하지 않습니다.
  • 결과가 너무 많으면 시/군/구를 포함해 검색어를 더 구체화합니다.
  • 조회형 스킬이므로 개인정보를 저장하지 않습니다.