slick-address-kr 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/index.tsx","../../src/constants.ts","../../src/errors.ts","../../src/api-client.ts","../../src/address-finder.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { KoreanAddressFinder } from '../address-finder';\nimport type {\n AddressFinderOptions,\n AddressResult,\n AddressSelectMeta,\n} from '../types';\n\nexport interface UseKoreanAddressResult {\n /** 검색 UI가 그려질 요소에 연결한다 */\n containerRef: (node: HTMLElement | null) => void;\n /** 현재 선택된 주소. 상세주소 입력도 반영된다 */\n address: AddressResult | null;\n /** 필요할 때 직접 조작하기 위한 인스턴스 */\n finder: KoreanAddressFinder | null;\n}\n\n/**\n * KoreanAddressFinder를 React 생명주기에 맞춰 붙인다.\n * 로직은 코어에 그대로 두고 마운트·언마운트와 상태 노출만 담당한다.\n */\nexport function useKoreanAddress(\n options: AddressFinderOptions = {}\n): UseKoreanAddressResult {\n const [address, setAddress] = useState<AddressResult | null>(null);\n const [finder, setFinder] = useState<KoreanAddressFinder | null>(null);\n\n // 옵션이 매 렌더 새 객체로 와도 재마운트하지 않도록 최신 값만 참조한다\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // finder 상태에만 의존하면 ref가 null로 분리될 때(요소는 사라지고\n // 컴포넌트는 유지되는 흔한 조건부 렌더 패턴) state 갱신 타이밍에\n // 기대지 않고는 정리할 방법이 없다. 그래서 살아있는 인스턴스를\n // ref로도 따로 들고, 콜백 ref 안에서 직접 파괴한다.\n const instanceRef = useRef<KoreanAddressFinder | null>(null);\n\n const containerRef = useCallback((node: HTMLElement | null) => {\n // 이전 인스턴스를 먼저 정리한다. ref가 null로 분리될 때도 여기로 들어온다.\n if (instanceRef.current) {\n instanceRef.current.destroy();\n instanceRef.current = null;\n setFinder(null);\n }\n\n if (!node) return;\n\n const instance = new KoreanAddressFinder({\n ...optionsRef.current,\n onSelect: (selected: AddressResult, meta: AddressSelectMeta) => {\n setAddress(selected);\n optionsRef.current.onSelect?.(selected, meta);\n },\n });\n instance.init(node);\n instanceRef.current = instance;\n setFinder(instance);\n }, []);\n\n // 컴포넌트 언마운트 시의 마지막 방어선.\n useEffect(() => {\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n }, []);\n\n return { containerRef, address, finder };\n}\n\nexport interface KoreanAddressFinderViewProps extends AddressFinderOptions {\n className?: string;\n}\n\n/**\n * 훅을 직접 다루지 않아도 되는 기본 컴포넌트.\n */\nexport function KoreanAddressFinderView({\n className,\n ...options\n}: KoreanAddressFinderViewProps) {\n const { containerRef } = useKoreanAddress(options);\n return <div className={className} ref={containerRef} />;\n}\n\nexport type { AddressResult, AddressSelectMeta, AddressFinderOptions };\n","/** 행정안전부 도로명주소 검색 API 기본 엔드포인트 */\nexport const DEFAULT_ENDPOINT =\n 'https://business.juso.go.kr/addrlink/addrLinkApi.do';\n\n/**\n * 승인키 발급 페이지 (2026-09 확인).\n * 구 경로 /addrlink/openApi/searchApi.do 는 사이트 개편으로 404다.\n */\nexport const API_KEY_ISSUE_URL =\n 'https://business.juso.go.kr/jst/jstAddressApiApplicationWrite';\n","import { API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 행안부 주소 API가 errorCode로 돌려주는 실패.\n * code는 원본을 보존하고, userMessage는 화면에 그대로 띄울 수 있는 안내다.\n */\nexport class JusoApiError extends Error {\n readonly code: string;\n readonly userMessage: string;\n\n constructor(code: string, apiMessage: string) {\n const userMessage = describeJusoError(code, apiMessage);\n super(`[${code}] ${apiMessage}`);\n this.name = 'JusoApiError';\n this.code = code;\n this.userMessage = userMessage;\n\n // ES5 타깃으로 다운레벨되는 소비자에서도 instanceof가 동작하도록\n Object.setPrototypeOf(this, JusoApiError.prototype);\n }\n}\n\n/**\n * errorCode를 행동 가능한 한국어 안내로 바꾼다.\n *\n * 코드 목록은 행안부 검색 API 문서를 따른다. 여기 담긴 것은 실제로\n * 자주 마주치는 코드이며, 모르는 코드는 API가 준 메시지를 살려서 보여준다.\n * 문서를 다시 확인할 일이 생기면:\n * https://business.juso.go.kr/jst/jstRoadNmAddrApiSearch\n */\nexport function describeJusoError(code: string, apiMessage: string): string {\n // 행안부 문서로 실제 확인된 코드만 여기 둔다. 확인되지 않은 코드를\n // 추측으로 채워 넣지 말 것 — 자신 있게 틀린 안내보다는 API가 준\n // 원본 메시지를 그대로 보여주는 편이 낫다. 확인되지 않은 코드는\n // default 분기로 흘려보내 apiMessage를 그대로 살린다.\n switch (code) {\n case 'E0005':\n case 'E0006':\n return (\n '승인키가 이 도메인에서 허용되지 않았습니다. ' +\n '발급 시 등록한 도메인과 현재 실행 중인 도메인이 같은지 확인하세요. ' +\n 'localhost에서 개발 중이라면 도메인에 localhost를 넣은 개발용 키가 따로 필요합니다. ' +\n `발급/수정: ${API_KEY_ISSUE_URL}`\n );\n default:\n return `주소 검색에 실패했습니다. (${code}) ${apiMessage}`.trim();\n }\n}\n","import type {\n AddressApiClientOptions,\n AddressCodes,\n AddressResult,\n AddressSearchOptions,\n AddressSearchResult,\n JusoApiResponse,\n} from './types';\nimport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\nimport { JusoApiError } from './errors';\n\nexport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 주소 검색 API 클라이언트.\n * endpoint를 바꾸면 자체 프록시 서버로 호출을 돌릴 수 있고,\n * fetchFn을 주입하면 네트워크 없이 동작을 검증할 수 있다.\n */\nexport class AddressApiClient {\n private readonly apiKey: string;\n private readonly endpoint: string;\n private readonly usesDefaultEndpoint: boolean;\n private readonly fetchFn: typeof fetch;\n\n constructor(options: AddressApiClientOptions = {}) {\n this.apiKey = options.apiKey ?? '';\n this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.usesDefaultEndpoint = this.endpoint === DEFAULT_ENDPOINT;\n this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);\n }\n\n async search(\n keyword: string,\n options: AddressSearchOptions = {}\n ): Promise<AddressSearchResult> {\n const trimmed = keyword?.trim() ?? '';\n if (trimmed.length === 0) {\n return { results: [], totalCount: 0 };\n }\n\n if (this.usesDefaultEndpoint && !this.apiKey) {\n throw new Error(\n 'API 키가 설정되지 않았습니다. KoreanAddressFinder 생성 시 apiKey 옵션을 전달하거나, ' +\n 'endpoint 옵션으로 자체 프록시 서버를 지정하세요. ' +\n `키 발급: ${API_KEY_ISSUE_URL}`\n );\n }\n\n const params = new URLSearchParams({\n currentPage: String(options.currentPage ?? 1),\n countPerPage: String(options.countPerPage ?? 10),\n keyword: trimmed,\n resultType: 'json',\n });\n\n // 프록시를 쓰는 경우 승인키는 서버가 붙인다.\n if (this.usesDefaultEndpoint) {\n params.set('confmKey', this.apiKey);\n }\n\n const response = await this.fetchFn(`${this.endpoint}?${params.toString()}`, {\n method: 'GET',\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`주소 검색 요청이 실패했습니다 (HTTP ${response.status})`);\n }\n\n const data = (await response.json()) as JusoApiResponse;\n return this.toSearchResult(data);\n }\n\n private toSearchResult(data: JusoApiResponse): AddressSearchResult {\n const { errorCode, errorMessage } = data.results.common;\n if (errorCode !== '0') {\n throw new JusoApiError(errorCode, errorMessage);\n }\n\n const juso = data.results.juso ?? [];\n const results: AddressResult[] = juso.map((item) => ({\n roadAddress: item.roadAddr,\n jibunAddress: item.jibunAddr,\n zipCode: item.zipNo,\n buildingName: item.bdNm || '',\n sido: item.siNm,\n sigungu: item.sggNm,\n bname: item.emdNm,\n extraAddress: buildExtraAddress(item),\n codes: toCodes(item),\n }));\n\n return {\n results,\n totalCount: Number.parseInt(data.results.common.totalCount, 10) || 0,\n };\n }\n}\n\ntype JusoItem = JusoApiResponse['results']['juso'][number];\n\nfunction toCodes(item: JusoItem): AddressCodes {\n return {\n admCd: item.admCd,\n rnMgtSn: item.rnMgtSn,\n udrtYn: item.udrtYn,\n buldMnnm: item.buldMnnm,\n buldSlno: item.buldSlno,\n };\n}\n\nfunction buildExtraAddress(item: JusoItem): string {\n const extras: string[] = [];\n if (item.bdNm) extras.push(item.bdNm);\n if (item.detBdNmList) extras.push(item.detBdNmList);\n return extras.join(', ');\n}\n","import { AddressApiClient } from './api-client';\nimport type { AddressFinderOptions, AddressResult, AddressSelectMeta } from './types';\nimport { JusoApiError } from './errors';\n\nlet instanceCounter = 0;\n\n/**\n * 주소 검색 컴포넌트\n * 팝업 없이 인라인으로 주소를 검색하고 선택할 수 있는 UI를 제공합니다.\n */\nexport class KoreanAddressFinder {\n // instanceCounter는 모듈 단위(=번들 단위)로 격리된다. 한 페이지에 ESM과\n // IIFE 번들이 동시에 로드되면 카운터가 각각 0부터 시작해 id가 겹칠 수\n // 있으므로, 짧은 무작위 접미사를 더해 번들 경계를 넘는 충돌을 막는다.\n private readonly instanceId = `kaf-${++instanceCounter}-${Math.random().toString(36).slice(2, 8)}`;\n private activeIndex = -1;\n private apiClient: AddressApiClient;\n // fetchFn은 값을 넣지 않은 상태(undefined)를 진짜 옵션값으로 다루고 싶어서\n // Required로 뭉뚱그리지 않는다. AddressApiClient는 undefined를 받아도\n // 스스로 전역 fetch로 대체하므로, 여기서 캐스팅으로 거짓 보장을 만들 필요가 없다.\n private options: Required<Omit<AddressFinderOptions, 'fetchFn'>> &\n Pick<AddressFinderOptions, 'fetchFn'>;\n private container: HTMLElement | null = null;\n private searchInput: HTMLInputElement | null = null;\n private resultsContainer: HTMLElement | null = null;\n private detailInput: HTMLInputElement | null = null;\n private isOpen = false;\n private debounceTimer: number | null = null;\n private abortController: AbortController | null = null;\n private selected: AddressResult | null = null;\n private results: AddressResult[] = [];\n private listeners: Array<() => void> = [];\n private destroyed = false;\n\n constructor(options: AddressFinderOptions = {}) {\n this.apiClient = new AddressApiClient({\n apiKey: options.apiKey,\n endpoint: options.endpoint,\n fetchFn: options.fetchFn,\n });\n\n // 기본 옵션 설정\n this.options = {\n apiKey: options.apiKey || '',\n countPerPage: options.countPerPage || 10,\n currentPage: options.currentPage || 1,\n containerId: options.containerId || 'address-finder-container',\n onSelect: options.onSelect || (() => {}),\n showDetailInput: options.showDetailInput !== false,\n customClass: options.customClass || '',\n autocompleteMode: options.autocompleteMode || false,\n endpoint: options.endpoint ?? '',\n fetchFn: options.fetchFn,\n };\n }\n\n /**\n * 주소 검색 UI 초기화\n */\n public init(targetElement?: HTMLElement): void {\n // 이전에 init()된 상태(destroy() 없이 다시 불렸거나, destroy() 뒤 재사용되는\n // 경우)라면 남아있는 리스너와 상태를 먼저 정리한다. 그러지 않으면\n // destroy() 없이 두 번 init()할 때 document 리스너가 중복 등록된다.\n if (this.container) {\n this.teardown();\n }\n // destroy()가 남긴 가드를 푼다 — 그러지 않으면 재사용된 인스턴스가\n // 렌더링과 검색은 정상 동작하면서 selectAddress()만 조용히 무시해\n // onSelect가 영원히 불리지 않는다.\n this.destroyed = false;\n\n if (targetElement) {\n this.container = targetElement;\n } else {\n this.container = document.getElementById(this.options.containerId);\n }\n\n if (!this.container) {\n throw new Error(`Container element not found: ${this.options.containerId}`);\n }\n\n this.render();\n this.attachEventListeners();\n }\n\n /**\n * UI 렌더링\n */\n private render(): void {\n if (!this.container) return;\n\n const className = this.options.customClass\n ? ` ${this.escapeHtml(this.options.customClass)}`\n : '';\n const listId = `${this.instanceId}-listbox`;\n\n this.container.innerHTML = `\n <div class=\"kaf-wrapper${className}\">\n <div class=\"kaf-search-box\">\n <input\n type=\"text\"\n class=\"kaf-search-input\"\n placeholder=\"주소를 입력하세요 (예: 판교역로 235)\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"false\"\n aria-controls=\"${listId}\"\n />\n <button type=\"button\" class=\"kaf-search-btn\">검색</button>\n </div>\n <div class=\"kaf-results\" style=\"display: none;\">\n <div class=\"kaf-results-header\">\n <span class=\"kaf-results-count\" aria-live=\"polite\">검색 결과</span>\n <button type=\"button\" class=\"kaf-close-btn\" aria-label=\"검색 결과 닫기\">×</button>\n </div>\n <div class=\"kaf-results-list\" id=\"${listId}\" role=\"listbox\" aria-label=\"주소 검색 결과\"></div>\n <div class=\"kaf-results-message\"></div>\n </div>\n ${this.options.showDetailInput ? `\n <div class=\"kaf-detail-box\" style=\"display: none;\">\n <input\n type=\"text\"\n class=\"kaf-detail-input\"\n placeholder=\"상세주소를 입력하세요\"\n aria-label=\"상세주소\"\n />\n </div>\n ` : ''}\n <div class=\"kaf-selected-address\" style=\"display: none;\"></div>\n </div>\n `;\n\n this.searchInput = this.container.querySelector('.kaf-search-input');\n this.resultsContainer = this.container.querySelector('.kaf-results');\n this.detailInput = this.container.querySelector('.kaf-detail-input');\n }\n\n /**\n * 이벤트를 등록하면서 해제 함수를 모아둔다.\n * destroy()가 이 목록만 비우면 리스너가 남지 않는다.\n */\n private on<T extends EventTarget>(\n target: T,\n type: string,\n handler: EventListenerOrEventListenerObject\n ): void {\n target.addEventListener(type, handler);\n this.listeners.push(() => target.removeEventListener(type, handler));\n }\n\n /**\n * 이벤트 리스너 등록\n */\n private attachEventListeners(): void {\n if (!this.container) return;\n\n const searchBtn = this.container.querySelector('.kaf-search-btn');\n if (searchBtn) {\n this.on(searchBtn, 'click', () => void this.search());\n }\n\n if (this.searchInput) {\n this.on(this.searchInput, 'keydown', (e) =>\n this.handleKeydown(e as KeyboardEvent)\n );\n\n if (this.options.autocompleteMode) {\n this.on(this.searchInput, 'input', () => this.handleAutocomplete());\n }\n }\n\n const closeBtn = this.container.querySelector('.kaf-close-btn');\n if (closeBtn) {\n this.on(closeBtn, 'click', () => this.closeResults());\n }\n\n if (this.detailInput) {\n this.on(this.detailInput, 'input', () => this.emitSelect('detail-change'));\n }\n\n this.on(document, 'click', (e) => {\n if (this.isOpen && !this.container?.contains(e.target as Node)) {\n this.closeResults();\n }\n });\n }\n\n private handleKeydown(event: KeyboardEvent): void {\n const count = this.results.length;\n\n switch (event.key) {\n case 'ArrowDown':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex >= count - 1 ? 0 : this.activeIndex + 1);\n return;\n\n case 'ArrowUp':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex <= 0 ? count - 1 : this.activeIndex - 1);\n return;\n\n case 'Enter':\n event.preventDefault();\n // 활성 항목이 있으면 선택, 없으면 검색\n if (this.isOpen && this.activeIndex >= 0 && this.results[this.activeIndex]) {\n this.selectAddress(this.results[this.activeIndex]);\n } else {\n void this.search();\n }\n return;\n\n case 'Escape':\n if (!this.isOpen) return;\n event.preventDefault();\n this.closeResults();\n return;\n\n default:\n }\n }\n\n /**\n * 활성 항목을 옮기고 ARIA 속성과 하이라이트를 함께 갱신한다.\n * -1은 활성 항목 없음을 뜻한다.\n */\n private setActiveIndex(index: number): void {\n this.activeIndex = index;\n\n const options = this.container?.querySelectorAll<HTMLElement>('.kaf-result-item');\n options?.forEach((option, i) => {\n const active = i === index;\n // APG 권고대로 활성 항목에만 aria-selected를 붙인다. 모든 항목에\n // \"false\"를 넣으면 일부 스크린리더가 화살표로 넘길 때마다\n // \"선택 안 됨\"을 매번 읽어준다.\n if (active) {\n option.setAttribute('aria-selected', 'true');\n } else {\n option.removeAttribute('aria-selected');\n }\n option.classList.toggle('kaf-result-item--active', active);\n if (active) {\n // jsdom에는 scrollIntoView가 없다. 선택적 호출이라야 테스트가 산다.\n option.scrollIntoView?.({ block: 'nearest' });\n }\n });\n\n if (this.searchInput) {\n const active = index >= 0 ? options?.[index] : undefined;\n if (active) {\n this.searchInput.setAttribute('aria-activedescendant', active.id);\n } else {\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n }\n }\n\n /**\n * 자동완성 처리 (디바운스 적용)\n */\n private handleAutocomplete(): void {\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer);\n }\n\n this.debounceTimer = window.setTimeout(() => {\n this.search();\n }, 300);\n }\n\n /**\n * 현재 입력값으로 주소를 검색한다.\n * 진행 중이던 이전 요청은 취소되며, 취소된 응답은 무시된다.\n */\n public async search(): Promise<void> {\n const keyword = this.searchInput?.value.trim();\n if (!keyword) return;\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n const { results, totalCount } = await this.apiClient.search(keyword, {\n currentPage: this.options.currentPage,\n countPerPage: this.options.countPerPage,\n signal: controller.signal,\n });\n\n // 기다리는 사이 더 새로운 검색이 시작됐다면 결과를 버린다\n if (this.abortController !== controller) return;\n\n this.displayResults(results, totalCount);\n } catch (error) {\n if (this.abortController !== controller) return;\n if (error instanceof DOMException && error.name === 'AbortError') return;\n\n const message =\n error instanceof JusoApiError\n ? error.userMessage\n : error instanceof Error\n ? error.message\n : '주소 검색 중 오류가 발생했습니다.';\n this.showError(message);\n }\n }\n\n /**\n * 검색 결과 표시\n */\n private displayResults(results: AddressResult[], totalCount: number): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n this.results = results;\n\n if (results.length === 0) {\n // listbox는 option/group만 자식으로 허용한다. 안내 문구는 listbox\n // 밖의 별도 영역에 둔다 — 그래야 스크린리더가 무시하지 않는다.\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = '<div class=\"kaf-no-results\">검색 결과가 없습니다.</div>';\n } else {\n resultsMessage.innerHTML = '';\n\n resultsList.innerHTML = results\n .map(\n (result, index) => `\n <div\n class=\"kaf-result-item\"\n id=\"${this.instanceId}-option-${index}\"\n role=\"option\"\n data-index=\"${index}\"\n >\n <div class=\"kaf-result-main\">\n <span class=\"kaf-result-type\">도로명</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.roadAddress)}</span>\n </div>\n <div class=\"kaf-result-sub\">\n <span class=\"kaf-result-type\">지번</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.jibunAddress)}</span>\n </div>\n <div class=\"kaf-result-zipcode\">우편번호: ${this.escapeHtml(result.zipCode)}</div>\n ${result.buildingName ? `<div class=\"kaf-result-building\">${this.escapeHtml(result.buildingName)}</div>` : ''}\n </div>\n `\n )\n .join('');\n\n // 결과 항목 클릭 이벤트\n resultsList.querySelectorAll('.kaf-result-item').forEach((item, index) => {\n item.addEventListener('click', () => this.selectAddress(results[index]));\n });\n }\n\n // aria-live 영역은 .kaf-results가 화면에 보인 뒤에 채워야 감시된다.\n // display:none인 상태에서 넣은 텍스트는 나중에 보여줘도 다시 읽히지 않는다.\n this.openResults();\n\n // 같은 건수가 연속으로 나오면 텍스트가 그대로라 재알림이 발생하지\n // 않는다. 비웠다가 다시 채워 값이 바뀌도록 만든다.\n resultsCount.textContent = '';\n resultsCount.textContent =\n results.length === 0 ? '검색 결과가 없습니다.' : `검색 결과 (${totalCount}건)`;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * 주소 선택 처리\n */\n private selectAddress(address: AddressResult): void {\n // 파괴된 인스턴스가 들고 있던 DOM 노드(detached result-item)가 클릭\n // 이벤트를 낼 수 있다. destroy() 이후에는 onSelect가 절대 불려서는\n // 안 된다.\n if (this.destroyed) return;\n\n this.selected = address;\n const selectedContainer = this.container?.querySelector('.kaf-selected-address');\n\n if (selectedContainer) {\n selectedContainer.innerHTML = `\n <div class=\"kaf-selected-content\">\n <div class=\"kaf-selected-main\">\n <strong>도로명:</strong> ${this.escapeHtml(address.roadAddress)}\n </div>\n <div class=\"kaf-selected-sub\">\n <strong>우편번호:</strong> ${this.escapeHtml(address.zipCode)}\n </div>\n </div>\n `;\n (selectedContainer as HTMLElement).style.display = 'block';\n }\n\n // 상세주소 입력 표시\n if (this.options.showDetailInput && this.detailInput) {\n const detailBox = this.container?.querySelector('.kaf-detail-box');\n if (detailBox) {\n (detailBox as HTMLElement).style.display = 'block';\n this.detailInput.focus();\n }\n }\n\n this.closeResults();\n\n this.emitSelect('select');\n }\n\n /**\n * getSelectedAddress()로 최신 상세주소를 채운 뒤 onSelect를 호출한다.\n * 선택된 주소가 없으면 아무 일도 하지 않는다.\n */\n private emitSelect(reason: AddressSelectMeta['reason']): void {\n const address = this.getSelectedAddress();\n if (!address) return;\n this.options.onSelect(address, { reason });\n }\n\n /**\n * 검색 결과 열기\n */\n private openResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'block';\n this.isOpen = true;\n this.searchInput?.setAttribute('aria-expanded', 'true');\n }\n }\n\n /**\n * 검색 결과 닫기\n */\n private closeResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'none';\n this.isOpen = false;\n this.searchInput?.setAttribute('aria-expanded', 'false');\n this.setActiveIndex(-1);\n }\n }\n\n /**\n * 에러 표시\n */\n private showError(message: string): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n // this.results를 비우지 않으면, 직전 검색의 결과가 여전히 배열에\n // 남아있는 채로 listbox만 지워진 상태가 된다. 그 상태에서 Enter를\n // 누르면 화면에도 스크린리더에도 없는 주소가 선택되어 버린다.\n this.results = [];\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = `<div class=\"kaf-error\">${this.escapeHtml(message)}</div>`;\n\n this.openResults();\n\n resultsCount.textContent = '';\n resultsCount.textContent = message;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * HTML 특수문자 이스케이프 (XSS 방지)\n */\n private escapeHtml(value: string): string {\n return String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * destroy()와 init()의 재마운트 양쪽이 공유하는 정리 로직이다.\n */\n private teardown(): void {\n for (const off of this.listeners) off();\n this.listeners = [];\n\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n\n this.abortController?.abort();\n this.abortController = null;\n\n if (this.container) {\n this.container.innerHTML = '';\n }\n\n this.selected = null;\n this.results = [];\n this.isOpen = false;\n this.activeIndex = -1;\n this.searchInput = null;\n this.resultsContainer = null;\n this.detailInput = null;\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * 여러 번 호출해도 안전하다. init()으로 다시 초기화하면 완전히\n * 재사용할 수 있다 — destroyed 가드는 초기화되지 않은 인스턴스가 아니라\n * \"이 destroy() 호출 이후, 다음 init() 전까지\"만 selectAddress()를 막는다.\n */\n public destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.teardown();\n }\n\n /**\n * 선택된 주소를 돌려준다. 선택 전이면 null.\n * detailAddress는 상세주소 입력란의 현재 값이 반영된다.\n */\n public getSelectedAddress(): AddressResult | null {\n if (!this.selected) return null;\n return {\n ...this.selected,\n detailAddress: this.detailInput?.value ?? '',\n };\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/react/index.tsx","../../src/constants.ts","../../src/errors.ts","../../src/api-client.ts","../../src/address-finder.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { KoreanAddressFinder } from '../address-finder';\nimport type {\n AddressFinderOptions,\n AddressResult,\n AddressSelectMeta,\n} from '../types';\n\nexport interface UseKoreanAddressResult {\n /** 검색 UI가 그려질 요소에 연결한다 */\n containerRef: (node: HTMLElement | null) => void;\n /** 현재 선택된 주소. 상세주소 입력도 반영된다 */\n address: AddressResult | null;\n /** 필요할 때 직접 조작하기 위한 인스턴스 */\n finder: KoreanAddressFinder | null;\n}\n\n/**\n * KoreanAddressFinder를 React 생명주기에 맞춰 붙인다.\n * 로직은 코어에 그대로 두고 마운트·언마운트와 상태 노출만 담당한다.\n */\nexport function useKoreanAddress(\n options: AddressFinderOptions = {}\n): UseKoreanAddressResult {\n const [address, setAddress] = useState<AddressResult | null>(null);\n const [finder, setFinder] = useState<KoreanAddressFinder | null>(null);\n\n // 옵션이 매 렌더 새 객체로 와도 재마운트하지 않도록 최신 값만 참조한다\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // finder 상태에만 의존하면 ref가 null로 분리될 때(요소는 사라지고\n // 컴포넌트는 유지되는 흔한 조건부 렌더 패턴) state 갱신 타이밍에\n // 기대지 않고는 정리할 방법이 없다. 그래서 살아있는 인스턴스를\n // ref로도 따로 들고, 콜백 ref 안에서 직접 파괴한다.\n const instanceRef = useRef<KoreanAddressFinder | null>(null);\n\n const containerRef = useCallback((node: HTMLElement | null) => {\n // 이전 인스턴스를 먼저 정리한다. ref가 null로 분리될 때도 여기로 들어온다.\n if (instanceRef.current) {\n instanceRef.current.destroy();\n instanceRef.current = null;\n setFinder(null);\n }\n\n if (!node) return;\n\n const instance = new KoreanAddressFinder({\n ...optionsRef.current,\n onSelect: (selected: AddressResult, meta: AddressSelectMeta) => {\n setAddress(selected);\n optionsRef.current.onSelect?.(selected, meta);\n },\n });\n instance.init(node);\n instanceRef.current = instance;\n setFinder(instance);\n }, []);\n\n // 컴포넌트 언마운트 시의 마지막 방어선.\n useEffect(() => {\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n }, []);\n\n return { containerRef, address, finder };\n}\n\nexport interface KoreanAddressFinderViewProps extends AddressFinderOptions {\n className?: string;\n}\n\n/**\n * 훅을 직접 다루지 않아도 되는 기본 컴포넌트.\n */\nexport function KoreanAddressFinderView({\n className,\n ...options\n}: KoreanAddressFinderViewProps) {\n const { containerRef } = useKoreanAddress(options);\n return <div className={className} ref={containerRef} />;\n}\n\nexport type { AddressResult, AddressSelectMeta, AddressFinderOptions };\n","/** 행정안전부 도로명주소 검색 API 기본 엔드포인트 */\nexport const DEFAULT_ENDPOINT =\n 'https://business.juso.go.kr/addrlink/addrLinkApi.do';\n\n/**\n * 승인키 발급 페이지 (2026-09 확인).\n * 구 경로 /addrlink/openApi/searchApi.do 는 사이트 개편으로 404다.\n */\nexport const API_KEY_ISSUE_URL =\n 'https://business.juso.go.kr/jst/jstAddressApiApplicationWrite';\n","import { API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 행안부 주소 API가 errorCode로 돌려주는 실패.\n * code는 원본을 보존하고, userMessage는 화면에 그대로 띄울 수 있는 안내다.\n */\nexport class JusoApiError extends Error {\n readonly code: string;\n readonly userMessage: string;\n /** 검색어를 고치면 풀리는 실패인지. 오류가 아니라 입력 안내로 보여준다. */\n readonly isInputError: boolean;\n\n constructor(code: string, apiMessage: string) {\n const userMessage = describeJusoError(code, apiMessage);\n super(`[${code}] ${apiMessage}`);\n this.name = 'JusoApiError';\n this.code = code;\n this.userMessage = userMessage;\n this.isInputError = isInputErrorCode(code);\n\n // ES5 타깃으로 다운레벨되는 소비자에서도 instanceof가 동작하도록\n Object.setPrototypeOf(this, JusoApiError.prototype);\n }\n}\n\n/**\n * 검색어 때문에 거절된 코드와, 사용자에게 보여줄 안내.\n *\n * 2026-10-06 실제 검색 API를 호출해 코드와 원본 메시지를 확인했다.\n * 사용자가 검색어를 고치면 풀리는 실패라서 코드 번호 없이 부드럽게 안내한다.\n */\nconst INPUT_ERROR_MESSAGES: Record<string, string> = {\n // 검색어가 입력되지 않았습니다.\n E0005: '검색어를 입력해 주세요.',\n // 주소를 상세히 입력해 주시기 바랍니다. ('서울', '서울시'처럼 시도명만 넣은 경우)\n E0006: '주소를 조금 더 자세히 입력해 주세요. (예: 판교역로 235)',\n // 검색어는 두글자 이상 입력되어야 합니다.\n E0008: '두 글자 이상 입력해 주세요.',\n // 검색어는 문자와 숫자 같이 입력되어야 합니다. (숫자만 입력한 경우)\n E0009: '도로명이나 건물명을 함께 입력해 주세요.',\n // 특수문자+숫자만으로는 검색이 불가능 합니다.\n E0012: '도로명이나 건물명을 함께 입력해 주세요.',\n // SQL 예약어 또는 특수문자( %,=,>,<,[,] )는 검색이 불가능 합니다.\n E0013: '특수문자(%, =, <, > 등)를 빼고 다시 입력해 주세요.',\n};\n\n/** 검색어를 고치면 풀리는 코드인지 */\nexport function isInputErrorCode(code: string): boolean {\n return Object.prototype.hasOwnProperty.call(INPUT_ERROR_MESSAGES, code);\n}\n\n/**\n * errorCode를 행동 가능한 한국어 안내로 바꾼다.\n *\n * 코드 목록은 행안부 검색 API 문서를 따른다. 여기 담긴 것은 실제로\n * 자주 마주치는 코드이며, 모르는 코드는 API가 준 메시지를 살려서 보여준다.\n * 문서를 다시 확인할 일이 생기면:\n * https://business.juso.go.kr/jst/jstRoadNmAddrApiSearch\n */\nexport function describeJusoError(code: string, apiMessage: string): string {\n // 실제 API로 확인된 코드만 여기 둔다. 확인되지 않은 코드를\n // 추측으로 채워 넣지 말 것 — 자신 있게 틀린 안내보다는 API가 준\n // 원본 메시지를 그대로 보여주는 편이 낫다. 확인되지 않은 코드는\n // default 분기로 흘려보내 apiMessage를 그대로 살린다.\n // (2.0.0까지는 E0005·E0006을 승인키 문제로 잘못 안내했다. 실제 승인키\n // 거절은 E0001이고, E0005·E0006은 검색어 문제다.)\n if (isInputErrorCode(code)) return INPUT_ERROR_MESSAGES[code];\n\n switch (code) {\n // 승인되지 않은 KEY 입니다. (키가 틀렸거나 비어 있는 경우)\n case 'E0001':\n return (\n '승인키가 올바르지 않거나 이 도메인에서 허용되지 않았습니다. ' +\n '발급받은 키를 그대로 넣었는지, 발급 시 등록한 도메인과 현재 실행 중인 도메인이 같은지 확인하세요. ' +\n `발급/수정: ${API_KEY_ISSUE_URL}`\n );\n default:\n return `주소 검색에 실패했습니다. (${code}) ${apiMessage}`.trim();\n }\n}\n","import type {\n AddressApiClientOptions,\n AddressCodes,\n AddressResult,\n AddressSearchOptions,\n AddressSearchResult,\n JusoApiResponse,\n} from './types';\nimport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\nimport { JusoApiError } from './errors';\n\nexport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 주소 검색 API 클라이언트.\n * endpoint를 바꾸면 자체 프록시 서버로 호출을 돌릴 수 있고,\n * fetchFn을 주입하면 네트워크 없이 동작을 검증할 수 있다.\n */\nexport class AddressApiClient {\n private readonly apiKey: string;\n private readonly endpoint: string;\n private readonly usesDefaultEndpoint: boolean;\n private readonly fetchFn: typeof fetch;\n\n constructor(options: AddressApiClientOptions = {}) {\n this.apiKey = options.apiKey ?? '';\n this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.usesDefaultEndpoint = this.endpoint === DEFAULT_ENDPOINT;\n this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);\n }\n\n async search(\n keyword: string,\n options: AddressSearchOptions = {}\n ): Promise<AddressSearchResult> {\n const trimmed = keyword?.trim() ?? '';\n if (trimmed.length === 0) {\n return { results: [], totalCount: 0 };\n }\n\n if (this.usesDefaultEndpoint && !this.apiKey) {\n throw new Error(\n 'API 키가 설정되지 않았습니다. KoreanAddressFinder 생성 시 apiKey 옵션을 전달하거나, ' +\n 'endpoint 옵션으로 자체 프록시 서버를 지정하세요. ' +\n `키 발급: ${API_KEY_ISSUE_URL}`\n );\n }\n\n const params = new URLSearchParams({\n currentPage: String(options.currentPage ?? 1),\n countPerPage: String(options.countPerPage ?? 10),\n keyword: trimmed,\n resultType: 'json',\n });\n\n // 프록시를 쓰는 경우 승인키는 서버가 붙인다.\n if (this.usesDefaultEndpoint) {\n params.set('confmKey', this.apiKey);\n }\n\n const response = await this.fetchFn(`${this.endpoint}?${params.toString()}`, {\n method: 'GET',\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`주소 검색 요청이 실패했습니다 (HTTP ${response.status})`);\n }\n\n const data = (await response.json()) as JusoApiResponse;\n return this.toSearchResult(data);\n }\n\n private toSearchResult(data: JusoApiResponse): AddressSearchResult {\n const { errorCode, errorMessage } = data.results.common;\n if (errorCode !== '0') {\n throw new JusoApiError(errorCode, errorMessage);\n }\n\n const juso = data.results.juso ?? [];\n const results: AddressResult[] = juso.map((item) => ({\n roadAddress: item.roadAddr,\n jibunAddress: item.jibunAddr,\n zipCode: item.zipNo,\n buildingName: item.bdNm || '',\n sido: item.siNm,\n sigungu: item.sggNm,\n bname: item.emdNm,\n extraAddress: buildExtraAddress(item),\n codes: toCodes(item),\n }));\n\n return {\n results,\n totalCount: Number.parseInt(data.results.common.totalCount, 10) || 0,\n };\n }\n}\n\ntype JusoItem = JusoApiResponse['results']['juso'][number];\n\nfunction toCodes(item: JusoItem): AddressCodes {\n return {\n admCd: item.admCd,\n rnMgtSn: item.rnMgtSn,\n udrtYn: item.udrtYn,\n buldMnnm: item.buldMnnm,\n buldSlno: item.buldSlno,\n };\n}\n\nfunction buildExtraAddress(item: JusoItem): string {\n const extras: string[] = [];\n if (item.bdNm) extras.push(item.bdNm);\n if (item.detBdNmList) extras.push(item.detBdNmList);\n return extras.join(', ');\n}\n","import { AddressApiClient } from './api-client';\nimport type { AddressFinderOptions, AddressResult, AddressSelectMeta } from './types';\nimport { JusoApiError } from './errors';\n\n/** 자동완성이 검색을 시작하는 최소 글자 수. 행안부 API가 한 글자를 거절한다(E0008). */\nconst MIN_AUTOCOMPLETE_LENGTH = 2;\n\nlet instanceCounter = 0;\n\n/**\n * 주소 검색 컴포넌트\n * 팝업 없이 인라인으로 주소를 검색하고 선택할 수 있는 UI를 제공합니다.\n */\nexport class KoreanAddressFinder {\n // instanceCounter는 모듈 단위(=번들 단위)로 격리된다. 한 페이지에 ESM과\n // IIFE 번들이 동시에 로드되면 카운터가 각각 0부터 시작해 id가 겹칠 수\n // 있으므로, 짧은 무작위 접미사를 더해 번들 경계를 넘는 충돌을 막는다.\n private readonly instanceId = `kaf-${++instanceCounter}-${Math.random().toString(36).slice(2, 8)}`;\n private activeIndex = -1;\n private apiClient: AddressApiClient;\n // fetchFn은 값을 넣지 않은 상태(undefined)를 진짜 옵션값으로 다루고 싶어서\n // Required로 뭉뚱그리지 않는다. AddressApiClient는 undefined를 받아도\n // 스스로 전역 fetch로 대체하므로, 여기서 캐스팅으로 거짓 보장을 만들 필요가 없다.\n private options: Required<Omit<AddressFinderOptions, 'fetchFn'>> &\n Pick<AddressFinderOptions, 'fetchFn'>;\n private container: HTMLElement | null = null;\n private searchInput: HTMLInputElement | null = null;\n private resultsContainer: HTMLElement | null = null;\n private detailInput: HTMLInputElement | null = null;\n private isOpen = false;\n private debounceTimer: number | null = null;\n private abortController: AbortController | null = null;\n private selected: AddressResult | null = null;\n private results: AddressResult[] = [];\n private listeners: Array<() => void> = [];\n private destroyed = false;\n\n constructor(options: AddressFinderOptions = {}) {\n this.apiClient = new AddressApiClient({\n apiKey: options.apiKey,\n endpoint: options.endpoint,\n fetchFn: options.fetchFn,\n });\n\n // 기본 옵션 설정\n this.options = {\n apiKey: options.apiKey || '',\n countPerPage: options.countPerPage || 10,\n currentPage: options.currentPage || 1,\n containerId: options.containerId || 'address-finder-container',\n onSelect: options.onSelect || (() => {}),\n showDetailInput: options.showDetailInput !== false,\n customClass: options.customClass || '',\n autocompleteMode: options.autocompleteMode || false,\n endpoint: options.endpoint ?? '',\n fetchFn: options.fetchFn,\n };\n }\n\n /**\n * 주소 검색 UI 초기화\n */\n public init(targetElement?: HTMLElement): void {\n // 이전에 init()된 상태(destroy() 없이 다시 불렸거나, destroy() 뒤 재사용되는\n // 경우)라면 남아있는 리스너와 상태를 먼저 정리한다. 그러지 않으면\n // destroy() 없이 두 번 init()할 때 document 리스너가 중복 등록된다.\n if (this.container) {\n this.teardown();\n }\n // destroy()가 남긴 가드를 푼다 — 그러지 않으면 재사용된 인스턴스가\n // 렌더링과 검색은 정상 동작하면서 selectAddress()만 조용히 무시해\n // onSelect가 영원히 불리지 않는다.\n this.destroyed = false;\n\n if (targetElement) {\n this.container = targetElement;\n } else {\n this.container = document.getElementById(this.options.containerId);\n }\n\n if (!this.container) {\n throw new Error(`Container element not found: ${this.options.containerId}`);\n }\n\n this.render();\n this.attachEventListeners();\n }\n\n /**\n * UI 렌더링\n */\n private render(): void {\n if (!this.container) return;\n\n const className = this.options.customClass\n ? ` ${this.escapeHtml(this.options.customClass)}`\n : '';\n const listId = `${this.instanceId}-listbox`;\n\n this.container.innerHTML = `\n <div class=\"kaf-wrapper${className}\">\n <div class=\"kaf-search-box\">\n <input\n type=\"text\"\n class=\"kaf-search-input\"\n placeholder=\"주소를 입력하세요 (예: 판교역로 235)\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"false\"\n aria-controls=\"${listId}\"\n />\n <button type=\"button\" class=\"kaf-search-btn\">검색</button>\n </div>\n <div class=\"kaf-results\" style=\"display: none;\">\n <div class=\"kaf-results-header\">\n <span class=\"kaf-results-count\" aria-live=\"polite\">검색 결과</span>\n <button type=\"button\" class=\"kaf-close-btn\" aria-label=\"검색 결과 닫기\">×</button>\n </div>\n <div class=\"kaf-results-list\" id=\"${listId}\" role=\"listbox\" aria-label=\"주소 검색 결과\"></div>\n <div class=\"kaf-results-message\"></div>\n </div>\n ${this.options.showDetailInput ? `\n <div class=\"kaf-detail-box\" style=\"display: none;\">\n <input\n type=\"text\"\n class=\"kaf-detail-input\"\n placeholder=\"상세주소를 입력하세요\"\n aria-label=\"상세주소\"\n />\n </div>\n ` : ''}\n <div class=\"kaf-selected-address\" style=\"display: none;\"></div>\n </div>\n `;\n\n this.searchInput = this.container.querySelector('.kaf-search-input');\n this.resultsContainer = this.container.querySelector('.kaf-results');\n this.detailInput = this.container.querySelector('.kaf-detail-input');\n }\n\n /**\n * 이벤트를 등록하면서 해제 함수를 모아둔다.\n * destroy()가 이 목록만 비우면 리스너가 남지 않는다.\n */\n private on<T extends EventTarget>(\n target: T,\n type: string,\n handler: EventListenerOrEventListenerObject\n ): void {\n target.addEventListener(type, handler);\n this.listeners.push(() => target.removeEventListener(type, handler));\n }\n\n /**\n * 이벤트 리스너 등록\n */\n private attachEventListeners(): void {\n if (!this.container) return;\n\n const searchBtn = this.container.querySelector('.kaf-search-btn');\n if (searchBtn) {\n this.on(searchBtn, 'click', () => void this.search());\n }\n\n if (this.searchInput) {\n this.on(this.searchInput, 'keydown', (e) =>\n this.handleKeydown(e as KeyboardEvent)\n );\n\n if (this.options.autocompleteMode) {\n this.on(this.searchInput, 'input', () => this.handleAutocomplete());\n }\n }\n\n const closeBtn = this.container.querySelector('.kaf-close-btn');\n if (closeBtn) {\n this.on(closeBtn, 'click', () => this.closeResults());\n }\n\n if (this.detailInput) {\n this.on(this.detailInput, 'input', () => this.emitSelect('detail-change'));\n }\n\n this.on(document, 'click', (e) => {\n if (this.isOpen && !this.container?.contains(e.target as Node)) {\n this.closeResults();\n }\n });\n }\n\n private handleKeydown(event: KeyboardEvent): void {\n const count = this.results.length;\n\n switch (event.key) {\n case 'ArrowDown':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex >= count - 1 ? 0 : this.activeIndex + 1);\n return;\n\n case 'ArrowUp':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex <= 0 ? count - 1 : this.activeIndex - 1);\n return;\n\n case 'Enter':\n event.preventDefault();\n // 활성 항목이 있으면 선택, 없으면 검색\n if (this.isOpen && this.activeIndex >= 0 && this.results[this.activeIndex]) {\n this.selectAddress(this.results[this.activeIndex]);\n } else {\n void this.search();\n }\n return;\n\n case 'Escape':\n if (!this.isOpen) return;\n event.preventDefault();\n this.closeResults();\n return;\n\n default:\n }\n }\n\n /**\n * 활성 항목을 옮기고 ARIA 속성과 하이라이트를 함께 갱신한다.\n * -1은 활성 항목 없음을 뜻한다.\n */\n private setActiveIndex(index: number): void {\n this.activeIndex = index;\n\n const options = this.container?.querySelectorAll<HTMLElement>('.kaf-result-item');\n options?.forEach((option, i) => {\n const active = i === index;\n // APG 권고대로 활성 항목에만 aria-selected를 붙인다. 모든 항목에\n // \"false\"를 넣으면 일부 스크린리더가 화살표로 넘길 때마다\n // \"선택 안 됨\"을 매번 읽어준다.\n if (active) {\n option.setAttribute('aria-selected', 'true');\n } else {\n option.removeAttribute('aria-selected');\n }\n option.classList.toggle('kaf-result-item--active', active);\n if (active) {\n // jsdom에는 scrollIntoView가 없다. 선택적 호출이라야 테스트가 산다.\n option.scrollIntoView?.({ block: 'nearest' });\n }\n });\n\n if (this.searchInput) {\n const active = index >= 0 ? options?.[index] : undefined;\n if (active) {\n this.searchInput.setAttribute('aria-activedescendant', active.id);\n } else {\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n }\n }\n\n /**\n * 자동완성 처리 (디바운스 적용)\n */\n private handleAutocomplete(): void {\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer);\n }\n\n // 한 글자는 API가 E0008로 거절한다. 타이핑 중인 사람에게 매번 안내를\n // 띄우지 않도록, 두 글자가 될 때까지 기다린다. 이미 떠 있던 결과는\n // 지금 검색어와 맞지 않으므로 닫는다.\n const keyword = this.searchInput?.value.trim() ?? '';\n if (keyword.length < MIN_AUTOCOMPLETE_LENGTH) {\n this.debounceTimer = null;\n this.abortController?.abort();\n this.abortController = null;\n this.closeResults();\n return;\n }\n\n this.debounceTimer = window.setTimeout(() => {\n this.search();\n }, 300);\n }\n\n /**\n * 현재 입력값으로 주소를 검색한다.\n * 진행 중이던 이전 요청은 취소되며, 취소된 응답은 무시된다.\n */\n public async search(): Promise<void> {\n const keyword = this.searchInput?.value.trim();\n if (!keyword) return;\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n const { results, totalCount } = await this.apiClient.search(keyword, {\n currentPage: this.options.currentPage,\n countPerPage: this.options.countPerPage,\n signal: controller.signal,\n });\n\n // 기다리는 사이 더 새로운 검색이 시작됐다면 결과를 버린다\n if (this.abortController !== controller) return;\n\n this.displayResults(results, totalCount);\n } catch (error) {\n if (this.abortController !== controller) return;\n if (error instanceof DOMException && error.name === 'AbortError') return;\n\n if (error instanceof JusoApiError && error.isInputError) {\n this.showHint(error.userMessage);\n return;\n }\n\n const message =\n error instanceof JusoApiError\n ? error.userMessage\n : error instanceof Error\n ? error.message\n : '주소 검색 중 오류가 발생했습니다.';\n this.showError(message);\n }\n }\n\n /**\n * 검색 결과 표시\n */\n private displayResults(results: AddressResult[], totalCount: number): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n this.results = results;\n\n if (results.length === 0) {\n // listbox는 option/group만 자식으로 허용한다. 안내 문구는 listbox\n // 밖의 별도 영역에 둔다 — 그래야 스크린리더가 무시하지 않는다.\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = '<div class=\"kaf-no-results\">검색 결과가 없습니다.</div>';\n } else {\n resultsMessage.innerHTML = '';\n\n resultsList.innerHTML = results\n .map(\n (result, index) => `\n <div\n class=\"kaf-result-item\"\n id=\"${this.instanceId}-option-${index}\"\n role=\"option\"\n data-index=\"${index}\"\n >\n <div class=\"kaf-result-main\">\n <span class=\"kaf-result-type\">도로명</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.roadAddress)}</span>\n </div>\n <div class=\"kaf-result-sub\">\n <span class=\"kaf-result-type\">지번</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.jibunAddress)}</span>\n </div>\n <div class=\"kaf-result-zipcode\">우편번호: ${this.escapeHtml(result.zipCode)}</div>\n ${result.buildingName ? `<div class=\"kaf-result-building\">${this.escapeHtml(result.buildingName)}</div>` : ''}\n </div>\n `\n )\n .join('');\n\n // 결과 항목 클릭 이벤트\n resultsList.querySelectorAll('.kaf-result-item').forEach((item, index) => {\n item.addEventListener('click', () => this.selectAddress(results[index]));\n });\n }\n\n // aria-live 영역은 .kaf-results가 화면에 보인 뒤에 채워야 감시된다.\n // display:none인 상태에서 넣은 텍스트는 나중에 보여줘도 다시 읽히지 않는다.\n this.openResults();\n\n // 같은 건수가 연속으로 나오면 텍스트가 그대로라 재알림이 발생하지\n // 않는다. 비웠다가 다시 채워 값이 바뀌도록 만든다.\n resultsCount.textContent = '';\n resultsCount.textContent =\n results.length === 0 ? '검색 결과가 없습니다.' : `검색 결과 (${totalCount}건)`;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * 주소 선택 처리\n */\n private selectAddress(address: AddressResult): void {\n // 파괴된 인스턴스가 들고 있던 DOM 노드(detached result-item)가 클릭\n // 이벤트를 낼 수 있다. destroy() 이후에는 onSelect가 절대 불려서는\n // 안 된다.\n if (this.destroyed) return;\n\n this.selected = address;\n const selectedContainer = this.container?.querySelector('.kaf-selected-address');\n\n if (selectedContainer) {\n selectedContainer.innerHTML = `\n <div class=\"kaf-selected-content\">\n <div class=\"kaf-selected-main\">\n <strong>도로명:</strong> ${this.escapeHtml(address.roadAddress)}\n </div>\n <div class=\"kaf-selected-sub\">\n <strong>우편번호:</strong> ${this.escapeHtml(address.zipCode)}\n </div>\n </div>\n `;\n (selectedContainer as HTMLElement).style.display = 'block';\n }\n\n // 상세주소 입력 표시\n if (this.options.showDetailInput && this.detailInput) {\n const detailBox = this.container?.querySelector('.kaf-detail-box');\n if (detailBox) {\n (detailBox as HTMLElement).style.display = 'block';\n this.detailInput.focus();\n }\n }\n\n this.closeResults();\n\n this.emitSelect('select');\n }\n\n /**\n * getSelectedAddress()로 최신 상세주소를 채운 뒤 onSelect를 호출한다.\n * 선택된 주소가 없으면 아무 일도 하지 않는다.\n */\n private emitSelect(reason: AddressSelectMeta['reason']): void {\n const address = this.getSelectedAddress();\n if (!address) return;\n this.options.onSelect(address, { reason });\n }\n\n /**\n * 검색 결과 열기\n */\n private openResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'block';\n this.isOpen = true;\n this.searchInput?.setAttribute('aria-expanded', 'true');\n }\n }\n\n /**\n * 검색 결과 닫기\n */\n private closeResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'none';\n this.isOpen = false;\n this.searchInput?.setAttribute('aria-expanded', 'false');\n this.setActiveIndex(-1);\n }\n }\n\n /**\n * 검색어 안내 표시 (두 글자 이상 등)\n *\n * 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만\n * 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.\n */\n private showHint(message: string): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n // showError와 같은 이유로 직전 결과를 비운다.\n this.results = [];\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = '';\n\n this.openResults();\n\n resultsCount.textContent = '';\n resultsCount.textContent = message;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * 에러 표시\n */\n private showError(message: string): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n // this.results를 비우지 않으면, 직전 검색의 결과가 여전히 배열에\n // 남아있는 채로 listbox만 지워진 상태가 된다. 그 상태에서 Enter를\n // 누르면 화면에도 스크린리더에도 없는 주소가 선택되어 버린다.\n this.results = [];\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = `<div class=\"kaf-error\">${this.escapeHtml(message)}</div>`;\n\n this.openResults();\n\n resultsCount.textContent = '';\n resultsCount.textContent = message;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * HTML 특수문자 이스케이프 (XSS 방지)\n */\n private escapeHtml(value: string): string {\n return String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * destroy()와 init()의 재마운트 양쪽이 공유하는 정리 로직이다.\n */\n private teardown(): void {\n for (const off of this.listeners) off();\n this.listeners = [];\n\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n\n this.abortController?.abort();\n this.abortController = null;\n\n if (this.container) {\n this.container.innerHTML = '';\n }\n\n this.selected = null;\n this.results = [];\n this.isOpen = false;\n this.activeIndex = -1;\n this.searchInput = null;\n this.resultsContainer = null;\n this.detailInput = null;\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * 여러 번 호출해도 안전하다. init()으로 다시 초기화하면 완전히\n * 재사용할 수 있다 — destroyed 가드는 초기화되지 않은 인스턴스가 아니라\n * \"이 destroy() 호출 이후, 다음 init() 전까지\"만 selectAddress()를 막는다.\n */\n public destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.teardown();\n }\n\n /**\n * 선택된 주소를 돌려준다. 선택 전이면 null.\n * detailAddress는 상세주소 입력란의 현재 값이 반영된다.\n */\n public getSelectedAddress(): AddressResult | null {\n if (!this.selected) return null;\n return {\n ...this.selected,\n detailAddress: this.detailInput?.value ?? '',\n };\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { d as AddressFinderOptions, e as AddressResult, K as KoreanAddressFinder } from '../address-finder-DIU0lay6.cjs';
3
- export { f as AddressSelectMeta } from '../address-finder-DIU0lay6.cjs';
2
+ import { d as AddressFinderOptions, e as AddressResult, K as KoreanAddressFinder } from '../address-finder-sdLnl5hT.cjs';
3
+ export { f as AddressSelectMeta } from '../address-finder-sdLnl5hT.cjs';
4
4
 
5
5
  interface UseKoreanAddressResult {
6
6
  /** 검색 UI가 그려질 요소에 연결한다 */
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
- import { d as AddressFinderOptions, e as AddressResult, K as KoreanAddressFinder } from '../address-finder-DIU0lay6.js';
3
- export { f as AddressSelectMeta } from '../address-finder-DIU0lay6.js';
2
+ import { d as AddressFinderOptions, e as AddressResult, K as KoreanAddressFinder } from '../address-finder-sdLnl5hT.js';
3
+ export { f as AddressSelectMeta } from '../address-finder-sdLnl5hT.js';
4
4
 
5
5
  interface UseKoreanAddressResult {
6
6
  /** 검색 UI가 그려질 요소에 연결한다 */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KoreanAddressFinder
3
- } from "../chunk-2OLEGOFJ.js";
3
+ } from "../chunk-H7AKAKT3.js";
4
4
 
5
5
  // src/react/index.tsx
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -37,14 +37,33 @@ var JusoApiError = class _JusoApiError extends Error {
37
37
  this.name = "JusoApiError";
38
38
  this.code = code;
39
39
  this.userMessage = userMessage;
40
+ this.isInputError = isInputErrorCode(code);
40
41
  Object.setPrototypeOf(this, _JusoApiError.prototype);
41
42
  }
42
43
  };
44
+ var INPUT_ERROR_MESSAGES = {
45
+ // 검색어가 입력되지 않았습니다.
46
+ E0005: "\uAC80\uC0C9\uC5B4\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
47
+ // 주소를 상세히 입력해 주시기 바랍니다. ('서울', '서울시'처럼 시도명만 넣은 경우)
48
+ E0006: "\uC8FC\uC18C\uB97C \uC870\uAE08 \uB354 \uC790\uC138\uD788 \uC785\uB825\uD574 \uC8FC\uC138\uC694. (\uC608: \uD310\uAD50\uC5ED\uB85C 235)",
49
+ // 검색어는 두글자 이상 입력되어야 합니다.
50
+ E0008: "\uB450 \uAE00\uC790 \uC774\uC0C1 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
51
+ // 검색어는 문자와 숫자 같이 입력되어야 합니다. (숫자만 입력한 경우)
52
+ E0009: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
53
+ // 특수문자+숫자만으로는 검색이 불가능 합니다.
54
+ E0012: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
55
+ // SQL 예약어 또는 특수문자( %,=,>,<,[,] )는 검색이 불가능 합니다.
56
+ E0013: "\uD2B9\uC218\uBB38\uC790(%, =, <, > \uB4F1)\uB97C \uBE7C\uACE0 \uB2E4\uC2DC \uC785\uB825\uD574 \uC8FC\uC138\uC694."
57
+ };
58
+ function isInputErrorCode(code) {
59
+ return Object.prototype.hasOwnProperty.call(INPUT_ERROR_MESSAGES, code);
60
+ }
43
61
  function describeJusoError(code, apiMessage) {
62
+ if (isInputErrorCode(code)) return INPUT_ERROR_MESSAGES[code];
44
63
  switch (code) {
45
- case "E0005":
46
- case "E0006":
47
- return `\uC2B9\uC778\uD0A4\uAC00 \uC774 \uB3C4\uBA54\uC778\uC5D0\uC11C \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uBC1C\uAE09 \uC2DC \uB4F1\uB85D\uD55C \uB3C4\uBA54\uC778\uACFC \uD604\uC7AC \uC2E4\uD589 \uC911\uC778 \uB3C4\uBA54\uC778\uC774 \uAC19\uC740\uC9C0 \uD655\uC778\uD558\uC138\uC694. localhost\uC5D0\uC11C \uAC1C\uBC1C \uC911\uC774\uB77C\uBA74 \uB3C4\uBA54\uC778\uC5D0 localhost\uB97C \uB123\uC740 \uAC1C\uBC1C\uC6A9 \uD0A4\uAC00 \uB530\uB85C \uD544\uC694\uD569\uB2C8\uB2E4. \uBC1C\uAE09/\uC218\uC815: ${API_KEY_ISSUE_URL}`;
64
+ // 승인되지 않은 KEY 입니다. (키가 틀렸거나 비어 있는 경우)
65
+ case "E0001":
66
+ return `\uC2B9\uC778\uD0A4\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uAC70\uB098 \uC774 \uB3C4\uBA54\uC778\uC5D0\uC11C \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uBC1C\uAE09\uBC1B\uC740 \uD0A4\uB97C \uADF8\uB300\uB85C \uB123\uC5C8\uB294\uC9C0, \uBC1C\uAE09 \uC2DC \uB4F1\uB85D\uD55C \uB3C4\uBA54\uC778\uACFC \uD604\uC7AC \uC2E4\uD589 \uC911\uC778 \uB3C4\uBA54\uC778\uC774 \uAC19\uC740\uC9C0 \uD655\uC778\uD558\uC138\uC694. \uBC1C\uAE09/\uC218\uC815: ${API_KEY_ISSUE_URL}`;
48
67
  default:
49
68
  return `\uC8FC\uC18C \uAC80\uC0C9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. (${code}) ${apiMessage}`.trim();
50
69
  }
@@ -127,6 +146,7 @@ function buildExtraAddress(item) {
127
146
  }
128
147
 
129
148
  // src/address-finder.ts
149
+ var MIN_AUTOCOMPLETE_LENGTH = 2;
130
150
  var instanceCounter = 0;
131
151
  var KoreanAddressFinder = class {
132
152
  constructor(options = {}) {
@@ -335,6 +355,14 @@ var KoreanAddressFinder = class {
335
355
  if (this.debounceTimer) {
336
356
  clearTimeout(this.debounceTimer);
337
357
  }
358
+ const keyword = this.searchInput?.value.trim() ?? "";
359
+ if (keyword.length < MIN_AUTOCOMPLETE_LENGTH) {
360
+ this.debounceTimer = null;
361
+ this.abortController?.abort();
362
+ this.abortController = null;
363
+ this.closeResults();
364
+ return;
365
+ }
338
366
  this.debounceTimer = window.setTimeout(() => {
339
367
  this.search();
340
368
  }, 300);
@@ -360,6 +388,10 @@ var KoreanAddressFinder = class {
360
388
  } catch (error) {
361
389
  if (this.abortController !== controller) return;
362
390
  if (error instanceof DOMException && error.name === "AbortError") return;
391
+ if (error instanceof JusoApiError && error.isInputError) {
392
+ this.showHint(error.userMessage);
393
+ return;
394
+ }
363
395
  const message = error instanceof JusoApiError ? error.userMessage : error instanceof Error ? error.message : "\uC8FC\uC18C \uAC80\uC0C9 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
364
396
  this.showError(message);
365
397
  }
@@ -468,6 +500,25 @@ var KoreanAddressFinder = class {
468
500
  this.setActiveIndex(-1);
469
501
  }
470
502
  }
503
+ /**
504
+ * 검색어 안내 표시 (두 글자 이상 등)
505
+ *
506
+ * 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만
507
+ * 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.
508
+ */
509
+ showHint(message) {
510
+ const resultsList = this.container?.querySelector(".kaf-results-list");
511
+ const resultsMessage = this.container?.querySelector(".kaf-results-message");
512
+ const resultsCount = this.container?.querySelector(".kaf-results-count");
513
+ if (!resultsList || !resultsMessage || !resultsCount) return;
514
+ this.results = [];
515
+ resultsList.innerHTML = "";
516
+ resultsMessage.innerHTML = "";
517
+ this.openResults();
518
+ resultsCount.textContent = "";
519
+ resultsCount.textContent = message;
520
+ this.setActiveIndex(-1);
521
+ }
471
522
  /**
472
523
  * 에러 표시
473
524
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vue/index.ts","../../src/constants.ts","../../src/errors.ts","../../src/api-client.ts","../../src/address-finder.ts"],"sourcesContent":["import { onBeforeUnmount, ref, shallowRef, watch, type Ref, type ShallowRef } from 'vue';\nimport { KoreanAddressFinder } from '../address-finder';\nimport type { AddressFinderOptions, AddressResult, AddressSelectMeta } from '../types';\n\nexport interface UseKoreanAddressResult {\n /** 검색 UI를 그릴 요소에 ref로 연결한다 */\n containerRef: Ref<HTMLElement | null>;\n /** 현재 선택된 주소. 상세주소 입력도 반영된다 */\n address: Ref<AddressResult | null>;\n /** 필요할 때 직접 조작하기 위한 인스턴스 */\n finder: ShallowRef<KoreanAddressFinder | null>;\n}\n\n/**\n * KoreanAddressFinder를 Vue 생명주기에 맞춰 붙인다.\n * 로직은 코어에 그대로 두고 마운트·언마운트와 상태 노출만 담당한다.\n *\n * 주의: `options`는 `setup()` 호출 시점의 값을 한 번만 캡처한다(React 훅과 달리\n * 매 렌더마다 다시 읽지 않는다). 이후 `onSelect` 등을 동적으로 바꿔야 한다면\n * 반환된 `finder` 인스턴스를 직접 조작하라.\n */\nexport function useKoreanAddress(\n options: AddressFinderOptions = {}\n): UseKoreanAddressResult {\n const containerRef = ref<HTMLElement | null>(null);\n const address = ref<AddressResult | null>(null);\n const finder = shallowRef<KoreanAddressFinder | null>(null);\n\n // ref로 붙은 노드가 나타나고 사라질 때마다 마운트·파괴한다. onMounted 한 번에만\n // 기대면, v-if로 컨테이너만 조건부 렌더하는 흔한 패턴에서 두 가지가 깨진다:\n // 노드가 사라져도 onBeforeUnmount는 불리지 않아 인스턴스가 새고, 노드가\n // 다시 나타나도 onMounted는 다시 불리지 않아 위젯이 영영 죽어 있다.\n // flush: 'post'로 DOM 패치 이후에 실행해야 init()이 문서에 붙은 노드를 받는다.\n watch(\n containerRef,\n (node) => {\n // 이전 인스턴스를 먼저 정리한다. 노드가 사라질 때도 여기로 들어온다.\n if (finder.value) {\n finder.value.destroy();\n finder.value = null;\n }\n\n if (!node) return;\n\n const instance = new KoreanAddressFinder({\n ...options,\n onSelect: (selected: AddressResult, meta: AddressSelectMeta) => {\n address.value = selected;\n options.onSelect?.(selected, meta);\n },\n });\n instance.init(node);\n finder.value = instance;\n },\n { immediate: true, flush: 'post' }\n );\n\n // 컴포넌트 언마운트 시의 마지막 방어선. destroy()는 멱등이라 이중 호출은 무해하다.\n onBeforeUnmount(() => {\n finder.value?.destroy();\n finder.value = null;\n });\n\n return { containerRef, address, finder };\n}\n\nexport type { AddressResult, AddressSelectMeta, AddressFinderOptions };\n","/** 행정안전부 도로명주소 검색 API 기본 엔드포인트 */\nexport const DEFAULT_ENDPOINT =\n 'https://business.juso.go.kr/addrlink/addrLinkApi.do';\n\n/**\n * 승인키 발급 페이지 (2026-09 확인).\n * 구 경로 /addrlink/openApi/searchApi.do 는 사이트 개편으로 404다.\n */\nexport const API_KEY_ISSUE_URL =\n 'https://business.juso.go.kr/jst/jstAddressApiApplicationWrite';\n","import { API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 행안부 주소 API가 errorCode로 돌려주는 실패.\n * code는 원본을 보존하고, userMessage는 화면에 그대로 띄울 수 있는 안내다.\n */\nexport class JusoApiError extends Error {\n readonly code: string;\n readonly userMessage: string;\n\n constructor(code: string, apiMessage: string) {\n const userMessage = describeJusoError(code, apiMessage);\n super(`[${code}] ${apiMessage}`);\n this.name = 'JusoApiError';\n this.code = code;\n this.userMessage = userMessage;\n\n // ES5 타깃으로 다운레벨되는 소비자에서도 instanceof가 동작하도록\n Object.setPrototypeOf(this, JusoApiError.prototype);\n }\n}\n\n/**\n * errorCode를 행동 가능한 한국어 안내로 바꾼다.\n *\n * 코드 목록은 행안부 검색 API 문서를 따른다. 여기 담긴 것은 실제로\n * 자주 마주치는 코드이며, 모르는 코드는 API가 준 메시지를 살려서 보여준다.\n * 문서를 다시 확인할 일이 생기면:\n * https://business.juso.go.kr/jst/jstRoadNmAddrApiSearch\n */\nexport function describeJusoError(code: string, apiMessage: string): string {\n // 행안부 문서로 실제 확인된 코드만 여기 둔다. 확인되지 않은 코드를\n // 추측으로 채워 넣지 말 것 — 자신 있게 틀린 안내보다는 API가 준\n // 원본 메시지를 그대로 보여주는 편이 낫다. 확인되지 않은 코드는\n // default 분기로 흘려보내 apiMessage를 그대로 살린다.\n switch (code) {\n case 'E0005':\n case 'E0006':\n return (\n '승인키가 이 도메인에서 허용되지 않았습니다. ' +\n '발급 시 등록한 도메인과 현재 실행 중인 도메인이 같은지 확인하세요. ' +\n 'localhost에서 개발 중이라면 도메인에 localhost를 넣은 개발용 키가 따로 필요합니다. ' +\n `발급/수정: ${API_KEY_ISSUE_URL}`\n );\n default:\n return `주소 검색에 실패했습니다. (${code}) ${apiMessage}`.trim();\n }\n}\n","import type {\n AddressApiClientOptions,\n AddressCodes,\n AddressResult,\n AddressSearchOptions,\n AddressSearchResult,\n JusoApiResponse,\n} from './types';\nimport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\nimport { JusoApiError } from './errors';\n\nexport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 주소 검색 API 클라이언트.\n * endpoint를 바꾸면 자체 프록시 서버로 호출을 돌릴 수 있고,\n * fetchFn을 주입하면 네트워크 없이 동작을 검증할 수 있다.\n */\nexport class AddressApiClient {\n private readonly apiKey: string;\n private readonly endpoint: string;\n private readonly usesDefaultEndpoint: boolean;\n private readonly fetchFn: typeof fetch;\n\n constructor(options: AddressApiClientOptions = {}) {\n this.apiKey = options.apiKey ?? '';\n this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.usesDefaultEndpoint = this.endpoint === DEFAULT_ENDPOINT;\n this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);\n }\n\n async search(\n keyword: string,\n options: AddressSearchOptions = {}\n ): Promise<AddressSearchResult> {\n const trimmed = keyword?.trim() ?? '';\n if (trimmed.length === 0) {\n return { results: [], totalCount: 0 };\n }\n\n if (this.usesDefaultEndpoint && !this.apiKey) {\n throw new Error(\n 'API 키가 설정되지 않았습니다. KoreanAddressFinder 생성 시 apiKey 옵션을 전달하거나, ' +\n 'endpoint 옵션으로 자체 프록시 서버를 지정하세요. ' +\n `키 발급: ${API_KEY_ISSUE_URL}`\n );\n }\n\n const params = new URLSearchParams({\n currentPage: String(options.currentPage ?? 1),\n countPerPage: String(options.countPerPage ?? 10),\n keyword: trimmed,\n resultType: 'json',\n });\n\n // 프록시를 쓰는 경우 승인키는 서버가 붙인다.\n if (this.usesDefaultEndpoint) {\n params.set('confmKey', this.apiKey);\n }\n\n const response = await this.fetchFn(`${this.endpoint}?${params.toString()}`, {\n method: 'GET',\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`주소 검색 요청이 실패했습니다 (HTTP ${response.status})`);\n }\n\n const data = (await response.json()) as JusoApiResponse;\n return this.toSearchResult(data);\n }\n\n private toSearchResult(data: JusoApiResponse): AddressSearchResult {\n const { errorCode, errorMessage } = data.results.common;\n if (errorCode !== '0') {\n throw new JusoApiError(errorCode, errorMessage);\n }\n\n const juso = data.results.juso ?? [];\n const results: AddressResult[] = juso.map((item) => ({\n roadAddress: item.roadAddr,\n jibunAddress: item.jibunAddr,\n zipCode: item.zipNo,\n buildingName: item.bdNm || '',\n sido: item.siNm,\n sigungu: item.sggNm,\n bname: item.emdNm,\n extraAddress: buildExtraAddress(item),\n codes: toCodes(item),\n }));\n\n return {\n results,\n totalCount: Number.parseInt(data.results.common.totalCount, 10) || 0,\n };\n }\n}\n\ntype JusoItem = JusoApiResponse['results']['juso'][number];\n\nfunction toCodes(item: JusoItem): AddressCodes {\n return {\n admCd: item.admCd,\n rnMgtSn: item.rnMgtSn,\n udrtYn: item.udrtYn,\n buldMnnm: item.buldMnnm,\n buldSlno: item.buldSlno,\n };\n}\n\nfunction buildExtraAddress(item: JusoItem): string {\n const extras: string[] = [];\n if (item.bdNm) extras.push(item.bdNm);\n if (item.detBdNmList) extras.push(item.detBdNmList);\n return extras.join(', ');\n}\n","import { AddressApiClient } from './api-client';\nimport type { AddressFinderOptions, AddressResult, AddressSelectMeta } from './types';\nimport { JusoApiError } from './errors';\n\nlet instanceCounter = 0;\n\n/**\n * 주소 검색 컴포넌트\n * 팝업 없이 인라인으로 주소를 검색하고 선택할 수 있는 UI를 제공합니다.\n */\nexport class KoreanAddressFinder {\n // instanceCounter는 모듈 단위(=번들 단위)로 격리된다. 한 페이지에 ESM과\n // IIFE 번들이 동시에 로드되면 카운터가 각각 0부터 시작해 id가 겹칠 수\n // 있으므로, 짧은 무작위 접미사를 더해 번들 경계를 넘는 충돌을 막는다.\n private readonly instanceId = `kaf-${++instanceCounter}-${Math.random().toString(36).slice(2, 8)}`;\n private activeIndex = -1;\n private apiClient: AddressApiClient;\n // fetchFn은 값을 넣지 않은 상태(undefined)를 진짜 옵션값으로 다루고 싶어서\n // Required로 뭉뚱그리지 않는다. AddressApiClient는 undefined를 받아도\n // 스스로 전역 fetch로 대체하므로, 여기서 캐스팅으로 거짓 보장을 만들 필요가 없다.\n private options: Required<Omit<AddressFinderOptions, 'fetchFn'>> &\n Pick<AddressFinderOptions, 'fetchFn'>;\n private container: HTMLElement | null = null;\n private searchInput: HTMLInputElement | null = null;\n private resultsContainer: HTMLElement | null = null;\n private detailInput: HTMLInputElement | null = null;\n private isOpen = false;\n private debounceTimer: number | null = null;\n private abortController: AbortController | null = null;\n private selected: AddressResult | null = null;\n private results: AddressResult[] = [];\n private listeners: Array<() => void> = [];\n private destroyed = false;\n\n constructor(options: AddressFinderOptions = {}) {\n this.apiClient = new AddressApiClient({\n apiKey: options.apiKey,\n endpoint: options.endpoint,\n fetchFn: options.fetchFn,\n });\n\n // 기본 옵션 설정\n this.options = {\n apiKey: options.apiKey || '',\n countPerPage: options.countPerPage || 10,\n currentPage: options.currentPage || 1,\n containerId: options.containerId || 'address-finder-container',\n onSelect: options.onSelect || (() => {}),\n showDetailInput: options.showDetailInput !== false,\n customClass: options.customClass || '',\n autocompleteMode: options.autocompleteMode || false,\n endpoint: options.endpoint ?? '',\n fetchFn: options.fetchFn,\n };\n }\n\n /**\n * 주소 검색 UI 초기화\n */\n public init(targetElement?: HTMLElement): void {\n // 이전에 init()된 상태(destroy() 없이 다시 불렸거나, destroy() 뒤 재사용되는\n // 경우)라면 남아있는 리스너와 상태를 먼저 정리한다. 그러지 않으면\n // destroy() 없이 두 번 init()할 때 document 리스너가 중복 등록된다.\n if (this.container) {\n this.teardown();\n }\n // destroy()가 남긴 가드를 푼다 — 그러지 않으면 재사용된 인스턴스가\n // 렌더링과 검색은 정상 동작하면서 selectAddress()만 조용히 무시해\n // onSelect가 영원히 불리지 않는다.\n this.destroyed = false;\n\n if (targetElement) {\n this.container = targetElement;\n } else {\n this.container = document.getElementById(this.options.containerId);\n }\n\n if (!this.container) {\n throw new Error(`Container element not found: ${this.options.containerId}`);\n }\n\n this.render();\n this.attachEventListeners();\n }\n\n /**\n * UI 렌더링\n */\n private render(): void {\n if (!this.container) return;\n\n const className = this.options.customClass\n ? ` ${this.escapeHtml(this.options.customClass)}`\n : '';\n const listId = `${this.instanceId}-listbox`;\n\n this.container.innerHTML = `\n <div class=\"kaf-wrapper${className}\">\n <div class=\"kaf-search-box\">\n <input\n type=\"text\"\n class=\"kaf-search-input\"\n placeholder=\"주소를 입력하세요 (예: 판교역로 235)\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"false\"\n aria-controls=\"${listId}\"\n />\n <button type=\"button\" class=\"kaf-search-btn\">검색</button>\n </div>\n <div class=\"kaf-results\" style=\"display: none;\">\n <div class=\"kaf-results-header\">\n <span class=\"kaf-results-count\" aria-live=\"polite\">검색 결과</span>\n <button type=\"button\" class=\"kaf-close-btn\" aria-label=\"검색 결과 닫기\">×</button>\n </div>\n <div class=\"kaf-results-list\" id=\"${listId}\" role=\"listbox\" aria-label=\"주소 검색 결과\"></div>\n <div class=\"kaf-results-message\"></div>\n </div>\n ${this.options.showDetailInput ? `\n <div class=\"kaf-detail-box\" style=\"display: none;\">\n <input\n type=\"text\"\n class=\"kaf-detail-input\"\n placeholder=\"상세주소를 입력하세요\"\n aria-label=\"상세주소\"\n />\n </div>\n ` : ''}\n <div class=\"kaf-selected-address\" style=\"display: none;\"></div>\n </div>\n `;\n\n this.searchInput = this.container.querySelector('.kaf-search-input');\n this.resultsContainer = this.container.querySelector('.kaf-results');\n this.detailInput = this.container.querySelector('.kaf-detail-input');\n }\n\n /**\n * 이벤트를 등록하면서 해제 함수를 모아둔다.\n * destroy()가 이 목록만 비우면 리스너가 남지 않는다.\n */\n private on<T extends EventTarget>(\n target: T,\n type: string,\n handler: EventListenerOrEventListenerObject\n ): void {\n target.addEventListener(type, handler);\n this.listeners.push(() => target.removeEventListener(type, handler));\n }\n\n /**\n * 이벤트 리스너 등록\n */\n private attachEventListeners(): void {\n if (!this.container) return;\n\n const searchBtn = this.container.querySelector('.kaf-search-btn');\n if (searchBtn) {\n this.on(searchBtn, 'click', () => void this.search());\n }\n\n if (this.searchInput) {\n this.on(this.searchInput, 'keydown', (e) =>\n this.handleKeydown(e as KeyboardEvent)\n );\n\n if (this.options.autocompleteMode) {\n this.on(this.searchInput, 'input', () => this.handleAutocomplete());\n }\n }\n\n const closeBtn = this.container.querySelector('.kaf-close-btn');\n if (closeBtn) {\n this.on(closeBtn, 'click', () => this.closeResults());\n }\n\n if (this.detailInput) {\n this.on(this.detailInput, 'input', () => this.emitSelect('detail-change'));\n }\n\n this.on(document, 'click', (e) => {\n if (this.isOpen && !this.container?.contains(e.target as Node)) {\n this.closeResults();\n }\n });\n }\n\n private handleKeydown(event: KeyboardEvent): void {\n const count = this.results.length;\n\n switch (event.key) {\n case 'ArrowDown':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex >= count - 1 ? 0 : this.activeIndex + 1);\n return;\n\n case 'ArrowUp':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex <= 0 ? count - 1 : this.activeIndex - 1);\n return;\n\n case 'Enter':\n event.preventDefault();\n // 활성 항목이 있으면 선택, 없으면 검색\n if (this.isOpen && this.activeIndex >= 0 && this.results[this.activeIndex]) {\n this.selectAddress(this.results[this.activeIndex]);\n } else {\n void this.search();\n }\n return;\n\n case 'Escape':\n if (!this.isOpen) return;\n event.preventDefault();\n this.closeResults();\n return;\n\n default:\n }\n }\n\n /**\n * 활성 항목을 옮기고 ARIA 속성과 하이라이트를 함께 갱신한다.\n * -1은 활성 항목 없음을 뜻한다.\n */\n private setActiveIndex(index: number): void {\n this.activeIndex = index;\n\n const options = this.container?.querySelectorAll<HTMLElement>('.kaf-result-item');\n options?.forEach((option, i) => {\n const active = i === index;\n // APG 권고대로 활성 항목에만 aria-selected를 붙인다. 모든 항목에\n // \"false\"를 넣으면 일부 스크린리더가 화살표로 넘길 때마다\n // \"선택 안 됨\"을 매번 읽어준다.\n if (active) {\n option.setAttribute('aria-selected', 'true');\n } else {\n option.removeAttribute('aria-selected');\n }\n option.classList.toggle('kaf-result-item--active', active);\n if (active) {\n // jsdom에는 scrollIntoView가 없다. 선택적 호출이라야 테스트가 산다.\n option.scrollIntoView?.({ block: 'nearest' });\n }\n });\n\n if (this.searchInput) {\n const active = index >= 0 ? options?.[index] : undefined;\n if (active) {\n this.searchInput.setAttribute('aria-activedescendant', active.id);\n } else {\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n }\n }\n\n /**\n * 자동완성 처리 (디바운스 적용)\n */\n private handleAutocomplete(): void {\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer);\n }\n\n this.debounceTimer = window.setTimeout(() => {\n this.search();\n }, 300);\n }\n\n /**\n * 현재 입력값으로 주소를 검색한다.\n * 진행 중이던 이전 요청은 취소되며, 취소된 응답은 무시된다.\n */\n public async search(): Promise<void> {\n const keyword = this.searchInput?.value.trim();\n if (!keyword) return;\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n const { results, totalCount } = await this.apiClient.search(keyword, {\n currentPage: this.options.currentPage,\n countPerPage: this.options.countPerPage,\n signal: controller.signal,\n });\n\n // 기다리는 사이 더 새로운 검색이 시작됐다면 결과를 버린다\n if (this.abortController !== controller) return;\n\n this.displayResults(results, totalCount);\n } catch (error) {\n if (this.abortController !== controller) return;\n if (error instanceof DOMException && error.name === 'AbortError') return;\n\n const message =\n error instanceof JusoApiError\n ? error.userMessage\n : error instanceof Error\n ? error.message\n : '주소 검색 중 오류가 발생했습니다.';\n this.showError(message);\n }\n }\n\n /**\n * 검색 결과 표시\n */\n private displayResults(results: AddressResult[], totalCount: number): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n this.results = results;\n\n if (results.length === 0) {\n // listbox는 option/group만 자식으로 허용한다. 안내 문구는 listbox\n // 밖의 별도 영역에 둔다 — 그래야 스크린리더가 무시하지 않는다.\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = '<div class=\"kaf-no-results\">검색 결과가 없습니다.</div>';\n } else {\n resultsMessage.innerHTML = '';\n\n resultsList.innerHTML = results\n .map(\n (result, index) => `\n <div\n class=\"kaf-result-item\"\n id=\"${this.instanceId}-option-${index}\"\n role=\"option\"\n data-index=\"${index}\"\n >\n <div class=\"kaf-result-main\">\n <span class=\"kaf-result-type\">도로명</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.roadAddress)}</span>\n </div>\n <div class=\"kaf-result-sub\">\n <span class=\"kaf-result-type\">지번</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.jibunAddress)}</span>\n </div>\n <div class=\"kaf-result-zipcode\">우편번호: ${this.escapeHtml(result.zipCode)}</div>\n ${result.buildingName ? `<div class=\"kaf-result-building\">${this.escapeHtml(result.buildingName)}</div>` : ''}\n </div>\n `\n )\n .join('');\n\n // 결과 항목 클릭 이벤트\n resultsList.querySelectorAll('.kaf-result-item').forEach((item, index) => {\n item.addEventListener('click', () => this.selectAddress(results[index]));\n });\n }\n\n // aria-live 영역은 .kaf-results가 화면에 보인 뒤에 채워야 감시된다.\n // display:none인 상태에서 넣은 텍스트는 나중에 보여줘도 다시 읽히지 않는다.\n this.openResults();\n\n // 같은 건수가 연속으로 나오면 텍스트가 그대로라 재알림이 발생하지\n // 않는다. 비웠다가 다시 채워 값이 바뀌도록 만든다.\n resultsCount.textContent = '';\n resultsCount.textContent =\n results.length === 0 ? '검색 결과가 없습니다.' : `검색 결과 (${totalCount}건)`;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * 주소 선택 처리\n */\n private selectAddress(address: AddressResult): void {\n // 파괴된 인스턴스가 들고 있던 DOM 노드(detached result-item)가 클릭\n // 이벤트를 낼 수 있다. destroy() 이후에는 onSelect가 절대 불려서는\n // 안 된다.\n if (this.destroyed) return;\n\n this.selected = address;\n const selectedContainer = this.container?.querySelector('.kaf-selected-address');\n\n if (selectedContainer) {\n selectedContainer.innerHTML = `\n <div class=\"kaf-selected-content\">\n <div class=\"kaf-selected-main\">\n <strong>도로명:</strong> ${this.escapeHtml(address.roadAddress)}\n </div>\n <div class=\"kaf-selected-sub\">\n <strong>우편번호:</strong> ${this.escapeHtml(address.zipCode)}\n </div>\n </div>\n `;\n (selectedContainer as HTMLElement).style.display = 'block';\n }\n\n // 상세주소 입력 표시\n if (this.options.showDetailInput && this.detailInput) {\n const detailBox = this.container?.querySelector('.kaf-detail-box');\n if (detailBox) {\n (detailBox as HTMLElement).style.display = 'block';\n this.detailInput.focus();\n }\n }\n\n this.closeResults();\n\n this.emitSelect('select');\n }\n\n /**\n * getSelectedAddress()로 최신 상세주소를 채운 뒤 onSelect를 호출한다.\n * 선택된 주소가 없으면 아무 일도 하지 않는다.\n */\n private emitSelect(reason: AddressSelectMeta['reason']): void {\n const address = this.getSelectedAddress();\n if (!address) return;\n this.options.onSelect(address, { reason });\n }\n\n /**\n * 검색 결과 열기\n */\n private openResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'block';\n this.isOpen = true;\n this.searchInput?.setAttribute('aria-expanded', 'true');\n }\n }\n\n /**\n * 검색 결과 닫기\n */\n private closeResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'none';\n this.isOpen = false;\n this.searchInput?.setAttribute('aria-expanded', 'false');\n this.setActiveIndex(-1);\n }\n }\n\n /**\n * 에러 표시\n */\n private showError(message: string): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n // this.results를 비우지 않으면, 직전 검색의 결과가 여전히 배열에\n // 남아있는 채로 listbox만 지워진 상태가 된다. 그 상태에서 Enter를\n // 누르면 화면에도 스크린리더에도 없는 주소가 선택되어 버린다.\n this.results = [];\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = `<div class=\"kaf-error\">${this.escapeHtml(message)}</div>`;\n\n this.openResults();\n\n resultsCount.textContent = '';\n resultsCount.textContent = message;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * HTML 특수문자 이스케이프 (XSS 방지)\n */\n private escapeHtml(value: string): string {\n return String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * destroy()와 init()의 재마운트 양쪽이 공유하는 정리 로직이다.\n */\n private teardown(): void {\n for (const off of this.listeners) off();\n this.listeners = [];\n\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n\n this.abortController?.abort();\n this.abortController = null;\n\n if (this.container) {\n this.container.innerHTML = '';\n }\n\n this.selected = null;\n this.results = [];\n this.isOpen = false;\n this.activeIndex = -1;\n this.searchInput = null;\n this.resultsContainer = null;\n this.detailInput = null;\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * 여러 번 호출해도 안전하다. init()으로 다시 초기화하면 완전히\n * 재사용할 수 있다 — destroyed 가드는 초기화되지 않은 인스턴스가 아니라\n * \"이 destroy() 호출 이후, 다음 init() 전까지\"만 selectAddress()를 막는다.\n */\n public destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.teardown();\n }\n\n /**\n * 선택된 주소를 돌려준다. 선택 전이면 null.\n * detailAddress는 상세주소 입력란의 현재 값이 반영된다.\n */\n public getSelectedAddress(): AddressResult | null {\n if (!this.selected) return null;\n return {\n ...this.selected,\n detailAddress: this.detailInput?.value ?? '',\n };\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/vue/index.ts","../../src/constants.ts","../../src/errors.ts","../../src/api-client.ts","../../src/address-finder.ts"],"sourcesContent":["import { onBeforeUnmount, ref, shallowRef, watch, type Ref, type ShallowRef } from 'vue';\nimport { KoreanAddressFinder } from '../address-finder';\nimport type { AddressFinderOptions, AddressResult, AddressSelectMeta } from '../types';\n\nexport interface UseKoreanAddressResult {\n /** 검색 UI를 그릴 요소에 ref로 연결한다 */\n containerRef: Ref<HTMLElement | null>;\n /** 현재 선택된 주소. 상세주소 입력도 반영된다 */\n address: Ref<AddressResult | null>;\n /** 필요할 때 직접 조작하기 위한 인스턴스 */\n finder: ShallowRef<KoreanAddressFinder | null>;\n}\n\n/**\n * KoreanAddressFinder를 Vue 생명주기에 맞춰 붙인다.\n * 로직은 코어에 그대로 두고 마운트·언마운트와 상태 노출만 담당한다.\n *\n * 주의: `options`는 `setup()` 호출 시점의 값을 한 번만 캡처한다(React 훅과 달리\n * 매 렌더마다 다시 읽지 않는다). 이후 `onSelect` 등을 동적으로 바꿔야 한다면\n * 반환된 `finder` 인스턴스를 직접 조작하라.\n */\nexport function useKoreanAddress(\n options: AddressFinderOptions = {}\n): UseKoreanAddressResult {\n const containerRef = ref<HTMLElement | null>(null);\n const address = ref<AddressResult | null>(null);\n const finder = shallowRef<KoreanAddressFinder | null>(null);\n\n // ref로 붙은 노드가 나타나고 사라질 때마다 마운트·파괴한다. onMounted 한 번에만\n // 기대면, v-if로 컨테이너만 조건부 렌더하는 흔한 패턴에서 두 가지가 깨진다:\n // 노드가 사라져도 onBeforeUnmount는 불리지 않아 인스턴스가 새고, 노드가\n // 다시 나타나도 onMounted는 다시 불리지 않아 위젯이 영영 죽어 있다.\n // flush: 'post'로 DOM 패치 이후에 실행해야 init()이 문서에 붙은 노드를 받는다.\n watch(\n containerRef,\n (node) => {\n // 이전 인스턴스를 먼저 정리한다. 노드가 사라질 때도 여기로 들어온다.\n if (finder.value) {\n finder.value.destroy();\n finder.value = null;\n }\n\n if (!node) return;\n\n const instance = new KoreanAddressFinder({\n ...options,\n onSelect: (selected: AddressResult, meta: AddressSelectMeta) => {\n address.value = selected;\n options.onSelect?.(selected, meta);\n },\n });\n instance.init(node);\n finder.value = instance;\n },\n { immediate: true, flush: 'post' }\n );\n\n // 컴포넌트 언마운트 시의 마지막 방어선. destroy()는 멱등이라 이중 호출은 무해하다.\n onBeforeUnmount(() => {\n finder.value?.destroy();\n finder.value = null;\n });\n\n return { containerRef, address, finder };\n}\n\nexport type { AddressResult, AddressSelectMeta, AddressFinderOptions };\n","/** 행정안전부 도로명주소 검색 API 기본 엔드포인트 */\nexport const DEFAULT_ENDPOINT =\n 'https://business.juso.go.kr/addrlink/addrLinkApi.do';\n\n/**\n * 승인키 발급 페이지 (2026-09 확인).\n * 구 경로 /addrlink/openApi/searchApi.do 는 사이트 개편으로 404다.\n */\nexport const API_KEY_ISSUE_URL =\n 'https://business.juso.go.kr/jst/jstAddressApiApplicationWrite';\n","import { API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 행안부 주소 API가 errorCode로 돌려주는 실패.\n * code는 원본을 보존하고, userMessage는 화면에 그대로 띄울 수 있는 안내다.\n */\nexport class JusoApiError extends Error {\n readonly code: string;\n readonly userMessage: string;\n /** 검색어를 고치면 풀리는 실패인지. 오류가 아니라 입력 안내로 보여준다. */\n readonly isInputError: boolean;\n\n constructor(code: string, apiMessage: string) {\n const userMessage = describeJusoError(code, apiMessage);\n super(`[${code}] ${apiMessage}`);\n this.name = 'JusoApiError';\n this.code = code;\n this.userMessage = userMessage;\n this.isInputError = isInputErrorCode(code);\n\n // ES5 타깃으로 다운레벨되는 소비자에서도 instanceof가 동작하도록\n Object.setPrototypeOf(this, JusoApiError.prototype);\n }\n}\n\n/**\n * 검색어 때문에 거절된 코드와, 사용자에게 보여줄 안내.\n *\n * 2026-10-06 실제 검색 API를 호출해 코드와 원본 메시지를 확인했다.\n * 사용자가 검색어를 고치면 풀리는 실패라서 코드 번호 없이 부드럽게 안내한다.\n */\nconst INPUT_ERROR_MESSAGES: Record<string, string> = {\n // 검색어가 입력되지 않았습니다.\n E0005: '검색어를 입력해 주세요.',\n // 주소를 상세히 입력해 주시기 바랍니다. ('서울', '서울시'처럼 시도명만 넣은 경우)\n E0006: '주소를 조금 더 자세히 입력해 주세요. (예: 판교역로 235)',\n // 검색어는 두글자 이상 입력되어야 합니다.\n E0008: '두 글자 이상 입력해 주세요.',\n // 검색어는 문자와 숫자 같이 입력되어야 합니다. (숫자만 입력한 경우)\n E0009: '도로명이나 건물명을 함께 입력해 주세요.',\n // 특수문자+숫자만으로는 검색이 불가능 합니다.\n E0012: '도로명이나 건물명을 함께 입력해 주세요.',\n // SQL 예약어 또는 특수문자( %,=,>,<,[,] )는 검색이 불가능 합니다.\n E0013: '특수문자(%, =, <, > 등)를 빼고 다시 입력해 주세요.',\n};\n\n/** 검색어를 고치면 풀리는 코드인지 */\nexport function isInputErrorCode(code: string): boolean {\n return Object.prototype.hasOwnProperty.call(INPUT_ERROR_MESSAGES, code);\n}\n\n/**\n * errorCode를 행동 가능한 한국어 안내로 바꾼다.\n *\n * 코드 목록은 행안부 검색 API 문서를 따른다. 여기 담긴 것은 실제로\n * 자주 마주치는 코드이며, 모르는 코드는 API가 준 메시지를 살려서 보여준다.\n * 문서를 다시 확인할 일이 생기면:\n * https://business.juso.go.kr/jst/jstRoadNmAddrApiSearch\n */\nexport function describeJusoError(code: string, apiMessage: string): string {\n // 실제 API로 확인된 코드만 여기 둔다. 확인되지 않은 코드를\n // 추측으로 채워 넣지 말 것 — 자신 있게 틀린 안내보다는 API가 준\n // 원본 메시지를 그대로 보여주는 편이 낫다. 확인되지 않은 코드는\n // default 분기로 흘려보내 apiMessage를 그대로 살린다.\n // (2.0.0까지는 E0005·E0006을 승인키 문제로 잘못 안내했다. 실제 승인키\n // 거절은 E0001이고, E0005·E0006은 검색어 문제다.)\n if (isInputErrorCode(code)) return INPUT_ERROR_MESSAGES[code];\n\n switch (code) {\n // 승인되지 않은 KEY 입니다. (키가 틀렸거나 비어 있는 경우)\n case 'E0001':\n return (\n '승인키가 올바르지 않거나 이 도메인에서 허용되지 않았습니다. ' +\n '발급받은 키를 그대로 넣었는지, 발급 시 등록한 도메인과 현재 실행 중인 도메인이 같은지 확인하세요. ' +\n `발급/수정: ${API_KEY_ISSUE_URL}`\n );\n default:\n return `주소 검색에 실패했습니다. (${code}) ${apiMessage}`.trim();\n }\n}\n","import type {\n AddressApiClientOptions,\n AddressCodes,\n AddressResult,\n AddressSearchOptions,\n AddressSearchResult,\n JusoApiResponse,\n} from './types';\nimport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\nimport { JusoApiError } from './errors';\n\nexport { DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './constants';\n\n/**\n * 주소 검색 API 클라이언트.\n * endpoint를 바꾸면 자체 프록시 서버로 호출을 돌릴 수 있고,\n * fetchFn을 주입하면 네트워크 없이 동작을 검증할 수 있다.\n */\nexport class AddressApiClient {\n private readonly apiKey: string;\n private readonly endpoint: string;\n private readonly usesDefaultEndpoint: boolean;\n private readonly fetchFn: typeof fetch;\n\n constructor(options: AddressApiClientOptions = {}) {\n this.apiKey = options.apiKey ?? '';\n this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n this.usesDefaultEndpoint = this.endpoint === DEFAULT_ENDPOINT;\n this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);\n }\n\n async search(\n keyword: string,\n options: AddressSearchOptions = {}\n ): Promise<AddressSearchResult> {\n const trimmed = keyword?.trim() ?? '';\n if (trimmed.length === 0) {\n return { results: [], totalCount: 0 };\n }\n\n if (this.usesDefaultEndpoint && !this.apiKey) {\n throw new Error(\n 'API 키가 설정되지 않았습니다. KoreanAddressFinder 생성 시 apiKey 옵션을 전달하거나, ' +\n 'endpoint 옵션으로 자체 프록시 서버를 지정하세요. ' +\n `키 발급: ${API_KEY_ISSUE_URL}`\n );\n }\n\n const params = new URLSearchParams({\n currentPage: String(options.currentPage ?? 1),\n countPerPage: String(options.countPerPage ?? 10),\n keyword: trimmed,\n resultType: 'json',\n });\n\n // 프록시를 쓰는 경우 승인키는 서버가 붙인다.\n if (this.usesDefaultEndpoint) {\n params.set('confmKey', this.apiKey);\n }\n\n const response = await this.fetchFn(`${this.endpoint}?${params.toString()}`, {\n method: 'GET',\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new Error(`주소 검색 요청이 실패했습니다 (HTTP ${response.status})`);\n }\n\n const data = (await response.json()) as JusoApiResponse;\n return this.toSearchResult(data);\n }\n\n private toSearchResult(data: JusoApiResponse): AddressSearchResult {\n const { errorCode, errorMessage } = data.results.common;\n if (errorCode !== '0') {\n throw new JusoApiError(errorCode, errorMessage);\n }\n\n const juso = data.results.juso ?? [];\n const results: AddressResult[] = juso.map((item) => ({\n roadAddress: item.roadAddr,\n jibunAddress: item.jibunAddr,\n zipCode: item.zipNo,\n buildingName: item.bdNm || '',\n sido: item.siNm,\n sigungu: item.sggNm,\n bname: item.emdNm,\n extraAddress: buildExtraAddress(item),\n codes: toCodes(item),\n }));\n\n return {\n results,\n totalCount: Number.parseInt(data.results.common.totalCount, 10) || 0,\n };\n }\n}\n\ntype JusoItem = JusoApiResponse['results']['juso'][number];\n\nfunction toCodes(item: JusoItem): AddressCodes {\n return {\n admCd: item.admCd,\n rnMgtSn: item.rnMgtSn,\n udrtYn: item.udrtYn,\n buldMnnm: item.buldMnnm,\n buldSlno: item.buldSlno,\n };\n}\n\nfunction buildExtraAddress(item: JusoItem): string {\n const extras: string[] = [];\n if (item.bdNm) extras.push(item.bdNm);\n if (item.detBdNmList) extras.push(item.detBdNmList);\n return extras.join(', ');\n}\n","import { AddressApiClient } from './api-client';\nimport type { AddressFinderOptions, AddressResult, AddressSelectMeta } from './types';\nimport { JusoApiError } from './errors';\n\n/** 자동완성이 검색을 시작하는 최소 글자 수. 행안부 API가 한 글자를 거절한다(E0008). */\nconst MIN_AUTOCOMPLETE_LENGTH = 2;\n\nlet instanceCounter = 0;\n\n/**\n * 주소 검색 컴포넌트\n * 팝업 없이 인라인으로 주소를 검색하고 선택할 수 있는 UI를 제공합니다.\n */\nexport class KoreanAddressFinder {\n // instanceCounter는 모듈 단위(=번들 단위)로 격리된다. 한 페이지에 ESM과\n // IIFE 번들이 동시에 로드되면 카운터가 각각 0부터 시작해 id가 겹칠 수\n // 있으므로, 짧은 무작위 접미사를 더해 번들 경계를 넘는 충돌을 막는다.\n private readonly instanceId = `kaf-${++instanceCounter}-${Math.random().toString(36).slice(2, 8)}`;\n private activeIndex = -1;\n private apiClient: AddressApiClient;\n // fetchFn은 값을 넣지 않은 상태(undefined)를 진짜 옵션값으로 다루고 싶어서\n // Required로 뭉뚱그리지 않는다. AddressApiClient는 undefined를 받아도\n // 스스로 전역 fetch로 대체하므로, 여기서 캐스팅으로 거짓 보장을 만들 필요가 없다.\n private options: Required<Omit<AddressFinderOptions, 'fetchFn'>> &\n Pick<AddressFinderOptions, 'fetchFn'>;\n private container: HTMLElement | null = null;\n private searchInput: HTMLInputElement | null = null;\n private resultsContainer: HTMLElement | null = null;\n private detailInput: HTMLInputElement | null = null;\n private isOpen = false;\n private debounceTimer: number | null = null;\n private abortController: AbortController | null = null;\n private selected: AddressResult | null = null;\n private results: AddressResult[] = [];\n private listeners: Array<() => void> = [];\n private destroyed = false;\n\n constructor(options: AddressFinderOptions = {}) {\n this.apiClient = new AddressApiClient({\n apiKey: options.apiKey,\n endpoint: options.endpoint,\n fetchFn: options.fetchFn,\n });\n\n // 기본 옵션 설정\n this.options = {\n apiKey: options.apiKey || '',\n countPerPage: options.countPerPage || 10,\n currentPage: options.currentPage || 1,\n containerId: options.containerId || 'address-finder-container',\n onSelect: options.onSelect || (() => {}),\n showDetailInput: options.showDetailInput !== false,\n customClass: options.customClass || '',\n autocompleteMode: options.autocompleteMode || false,\n endpoint: options.endpoint ?? '',\n fetchFn: options.fetchFn,\n };\n }\n\n /**\n * 주소 검색 UI 초기화\n */\n public init(targetElement?: HTMLElement): void {\n // 이전에 init()된 상태(destroy() 없이 다시 불렸거나, destroy() 뒤 재사용되는\n // 경우)라면 남아있는 리스너와 상태를 먼저 정리한다. 그러지 않으면\n // destroy() 없이 두 번 init()할 때 document 리스너가 중복 등록된다.\n if (this.container) {\n this.teardown();\n }\n // destroy()가 남긴 가드를 푼다 — 그러지 않으면 재사용된 인스턴스가\n // 렌더링과 검색은 정상 동작하면서 selectAddress()만 조용히 무시해\n // onSelect가 영원히 불리지 않는다.\n this.destroyed = false;\n\n if (targetElement) {\n this.container = targetElement;\n } else {\n this.container = document.getElementById(this.options.containerId);\n }\n\n if (!this.container) {\n throw new Error(`Container element not found: ${this.options.containerId}`);\n }\n\n this.render();\n this.attachEventListeners();\n }\n\n /**\n * UI 렌더링\n */\n private render(): void {\n if (!this.container) return;\n\n const className = this.options.customClass\n ? ` ${this.escapeHtml(this.options.customClass)}`\n : '';\n const listId = `${this.instanceId}-listbox`;\n\n this.container.innerHTML = `\n <div class=\"kaf-wrapper${className}\">\n <div class=\"kaf-search-box\">\n <input\n type=\"text\"\n class=\"kaf-search-input\"\n placeholder=\"주소를 입력하세요 (예: 판교역로 235)\"\n autocomplete=\"off\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded=\"false\"\n aria-controls=\"${listId}\"\n />\n <button type=\"button\" class=\"kaf-search-btn\">검색</button>\n </div>\n <div class=\"kaf-results\" style=\"display: none;\">\n <div class=\"kaf-results-header\">\n <span class=\"kaf-results-count\" aria-live=\"polite\">검색 결과</span>\n <button type=\"button\" class=\"kaf-close-btn\" aria-label=\"검색 결과 닫기\">×</button>\n </div>\n <div class=\"kaf-results-list\" id=\"${listId}\" role=\"listbox\" aria-label=\"주소 검색 결과\"></div>\n <div class=\"kaf-results-message\"></div>\n </div>\n ${this.options.showDetailInput ? `\n <div class=\"kaf-detail-box\" style=\"display: none;\">\n <input\n type=\"text\"\n class=\"kaf-detail-input\"\n placeholder=\"상세주소를 입력하세요\"\n aria-label=\"상세주소\"\n />\n </div>\n ` : ''}\n <div class=\"kaf-selected-address\" style=\"display: none;\"></div>\n </div>\n `;\n\n this.searchInput = this.container.querySelector('.kaf-search-input');\n this.resultsContainer = this.container.querySelector('.kaf-results');\n this.detailInput = this.container.querySelector('.kaf-detail-input');\n }\n\n /**\n * 이벤트를 등록하면서 해제 함수를 모아둔다.\n * destroy()가 이 목록만 비우면 리스너가 남지 않는다.\n */\n private on<T extends EventTarget>(\n target: T,\n type: string,\n handler: EventListenerOrEventListenerObject\n ): void {\n target.addEventListener(type, handler);\n this.listeners.push(() => target.removeEventListener(type, handler));\n }\n\n /**\n * 이벤트 리스너 등록\n */\n private attachEventListeners(): void {\n if (!this.container) return;\n\n const searchBtn = this.container.querySelector('.kaf-search-btn');\n if (searchBtn) {\n this.on(searchBtn, 'click', () => void this.search());\n }\n\n if (this.searchInput) {\n this.on(this.searchInput, 'keydown', (e) =>\n this.handleKeydown(e as KeyboardEvent)\n );\n\n if (this.options.autocompleteMode) {\n this.on(this.searchInput, 'input', () => this.handleAutocomplete());\n }\n }\n\n const closeBtn = this.container.querySelector('.kaf-close-btn');\n if (closeBtn) {\n this.on(closeBtn, 'click', () => this.closeResults());\n }\n\n if (this.detailInput) {\n this.on(this.detailInput, 'input', () => this.emitSelect('detail-change'));\n }\n\n this.on(document, 'click', (e) => {\n if (this.isOpen && !this.container?.contains(e.target as Node)) {\n this.closeResults();\n }\n });\n }\n\n private handleKeydown(event: KeyboardEvent): void {\n const count = this.results.length;\n\n switch (event.key) {\n case 'ArrowDown':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex >= count - 1 ? 0 : this.activeIndex + 1);\n return;\n\n case 'ArrowUp':\n if (!this.isOpen || count === 0) return;\n event.preventDefault();\n this.setActiveIndex(this.activeIndex <= 0 ? count - 1 : this.activeIndex - 1);\n return;\n\n case 'Enter':\n event.preventDefault();\n // 활성 항목이 있으면 선택, 없으면 검색\n if (this.isOpen && this.activeIndex >= 0 && this.results[this.activeIndex]) {\n this.selectAddress(this.results[this.activeIndex]);\n } else {\n void this.search();\n }\n return;\n\n case 'Escape':\n if (!this.isOpen) return;\n event.preventDefault();\n this.closeResults();\n return;\n\n default:\n }\n }\n\n /**\n * 활성 항목을 옮기고 ARIA 속성과 하이라이트를 함께 갱신한다.\n * -1은 활성 항목 없음을 뜻한다.\n */\n private setActiveIndex(index: number): void {\n this.activeIndex = index;\n\n const options = this.container?.querySelectorAll<HTMLElement>('.kaf-result-item');\n options?.forEach((option, i) => {\n const active = i === index;\n // APG 권고대로 활성 항목에만 aria-selected를 붙인다. 모든 항목에\n // \"false\"를 넣으면 일부 스크린리더가 화살표로 넘길 때마다\n // \"선택 안 됨\"을 매번 읽어준다.\n if (active) {\n option.setAttribute('aria-selected', 'true');\n } else {\n option.removeAttribute('aria-selected');\n }\n option.classList.toggle('kaf-result-item--active', active);\n if (active) {\n // jsdom에는 scrollIntoView가 없다. 선택적 호출이라야 테스트가 산다.\n option.scrollIntoView?.({ block: 'nearest' });\n }\n });\n\n if (this.searchInput) {\n const active = index >= 0 ? options?.[index] : undefined;\n if (active) {\n this.searchInput.setAttribute('aria-activedescendant', active.id);\n } else {\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n }\n }\n\n /**\n * 자동완성 처리 (디바운스 적용)\n */\n private handleAutocomplete(): void {\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer);\n }\n\n // 한 글자는 API가 E0008로 거절한다. 타이핑 중인 사람에게 매번 안내를\n // 띄우지 않도록, 두 글자가 될 때까지 기다린다. 이미 떠 있던 결과는\n // 지금 검색어와 맞지 않으므로 닫는다.\n const keyword = this.searchInput?.value.trim() ?? '';\n if (keyword.length < MIN_AUTOCOMPLETE_LENGTH) {\n this.debounceTimer = null;\n this.abortController?.abort();\n this.abortController = null;\n this.closeResults();\n return;\n }\n\n this.debounceTimer = window.setTimeout(() => {\n this.search();\n }, 300);\n }\n\n /**\n * 현재 입력값으로 주소를 검색한다.\n * 진행 중이던 이전 요청은 취소되며, 취소된 응답은 무시된다.\n */\n public async search(): Promise<void> {\n const keyword = this.searchInput?.value.trim();\n if (!keyword) return;\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n try {\n const { results, totalCount } = await this.apiClient.search(keyword, {\n currentPage: this.options.currentPage,\n countPerPage: this.options.countPerPage,\n signal: controller.signal,\n });\n\n // 기다리는 사이 더 새로운 검색이 시작됐다면 결과를 버린다\n if (this.abortController !== controller) return;\n\n this.displayResults(results, totalCount);\n } catch (error) {\n if (this.abortController !== controller) return;\n if (error instanceof DOMException && error.name === 'AbortError') return;\n\n if (error instanceof JusoApiError && error.isInputError) {\n this.showHint(error.userMessage);\n return;\n }\n\n const message =\n error instanceof JusoApiError\n ? error.userMessage\n : error instanceof Error\n ? error.message\n : '주소 검색 중 오류가 발생했습니다.';\n this.showError(message);\n }\n }\n\n /**\n * 검색 결과 표시\n */\n private displayResults(results: AddressResult[], totalCount: number): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n this.results = results;\n\n if (results.length === 0) {\n // listbox는 option/group만 자식으로 허용한다. 안내 문구는 listbox\n // 밖의 별도 영역에 둔다 — 그래야 스크린리더가 무시하지 않는다.\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = '<div class=\"kaf-no-results\">검색 결과가 없습니다.</div>';\n } else {\n resultsMessage.innerHTML = '';\n\n resultsList.innerHTML = results\n .map(\n (result, index) => `\n <div\n class=\"kaf-result-item\"\n id=\"${this.instanceId}-option-${index}\"\n role=\"option\"\n data-index=\"${index}\"\n >\n <div class=\"kaf-result-main\">\n <span class=\"kaf-result-type\">도로명</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.roadAddress)}</span>\n </div>\n <div class=\"kaf-result-sub\">\n <span class=\"kaf-result-type\">지번</span>\n <span class=\"kaf-result-address\">${this.escapeHtml(result.jibunAddress)}</span>\n </div>\n <div class=\"kaf-result-zipcode\">우편번호: ${this.escapeHtml(result.zipCode)}</div>\n ${result.buildingName ? `<div class=\"kaf-result-building\">${this.escapeHtml(result.buildingName)}</div>` : ''}\n </div>\n `\n )\n .join('');\n\n // 결과 항목 클릭 이벤트\n resultsList.querySelectorAll('.kaf-result-item').forEach((item, index) => {\n item.addEventListener('click', () => this.selectAddress(results[index]));\n });\n }\n\n // aria-live 영역은 .kaf-results가 화면에 보인 뒤에 채워야 감시된다.\n // display:none인 상태에서 넣은 텍스트는 나중에 보여줘도 다시 읽히지 않는다.\n this.openResults();\n\n // 같은 건수가 연속으로 나오면 텍스트가 그대로라 재알림이 발생하지\n // 않는다. 비웠다가 다시 채워 값이 바뀌도록 만든다.\n resultsCount.textContent = '';\n resultsCount.textContent =\n results.length === 0 ? '검색 결과가 없습니다.' : `검색 결과 (${totalCount}건)`;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * 주소 선택 처리\n */\n private selectAddress(address: AddressResult): void {\n // 파괴된 인스턴스가 들고 있던 DOM 노드(detached result-item)가 클릭\n // 이벤트를 낼 수 있다. destroy() 이후에는 onSelect가 절대 불려서는\n // 안 된다.\n if (this.destroyed) return;\n\n this.selected = address;\n const selectedContainer = this.container?.querySelector('.kaf-selected-address');\n\n if (selectedContainer) {\n selectedContainer.innerHTML = `\n <div class=\"kaf-selected-content\">\n <div class=\"kaf-selected-main\">\n <strong>도로명:</strong> ${this.escapeHtml(address.roadAddress)}\n </div>\n <div class=\"kaf-selected-sub\">\n <strong>우편번호:</strong> ${this.escapeHtml(address.zipCode)}\n </div>\n </div>\n `;\n (selectedContainer as HTMLElement).style.display = 'block';\n }\n\n // 상세주소 입력 표시\n if (this.options.showDetailInput && this.detailInput) {\n const detailBox = this.container?.querySelector('.kaf-detail-box');\n if (detailBox) {\n (detailBox as HTMLElement).style.display = 'block';\n this.detailInput.focus();\n }\n }\n\n this.closeResults();\n\n this.emitSelect('select');\n }\n\n /**\n * getSelectedAddress()로 최신 상세주소를 채운 뒤 onSelect를 호출한다.\n * 선택된 주소가 없으면 아무 일도 하지 않는다.\n */\n private emitSelect(reason: AddressSelectMeta['reason']): void {\n const address = this.getSelectedAddress();\n if (!address) return;\n this.options.onSelect(address, { reason });\n }\n\n /**\n * 검색 결과 열기\n */\n private openResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'block';\n this.isOpen = true;\n this.searchInput?.setAttribute('aria-expanded', 'true');\n }\n }\n\n /**\n * 검색 결과 닫기\n */\n private closeResults(): void {\n if (this.resultsContainer) {\n this.resultsContainer.style.display = 'none';\n this.isOpen = false;\n this.searchInput?.setAttribute('aria-expanded', 'false');\n this.setActiveIndex(-1);\n }\n }\n\n /**\n * 검색어 안내 표시 (두 글자 이상 등)\n *\n * 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만\n * 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.\n */\n private showHint(message: string): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n // showError와 같은 이유로 직전 결과를 비운다.\n this.results = [];\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = '';\n\n this.openResults();\n\n resultsCount.textContent = '';\n resultsCount.textContent = message;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * 에러 표시\n */\n private showError(message: string): void {\n const resultsList = this.container?.querySelector('.kaf-results-list');\n const resultsMessage = this.container?.querySelector('.kaf-results-message');\n const resultsCount = this.container?.querySelector('.kaf-results-count');\n\n if (!resultsList || !resultsMessage || !resultsCount) return;\n\n // this.results를 비우지 않으면, 직전 검색의 결과가 여전히 배열에\n // 남아있는 채로 listbox만 지워진 상태가 된다. 그 상태에서 Enter를\n // 누르면 화면에도 스크린리더에도 없는 주소가 선택되어 버린다.\n this.results = [];\n resultsList.innerHTML = '';\n resultsMessage.innerHTML = `<div class=\"kaf-error\">${this.escapeHtml(message)}</div>`;\n\n this.openResults();\n\n resultsCount.textContent = '';\n resultsCount.textContent = message;\n\n this.setActiveIndex(-1);\n }\n\n /**\n * HTML 특수문자 이스케이프 (XSS 방지)\n */\n private escapeHtml(value: string): string {\n return String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * destroy()와 init()의 재마운트 양쪽이 공유하는 정리 로직이다.\n */\n private teardown(): void {\n for (const off of this.listeners) off();\n this.listeners = [];\n\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n\n this.abortController?.abort();\n this.abortController = null;\n\n if (this.container) {\n this.container.innerHTML = '';\n }\n\n this.selected = null;\n this.results = [];\n this.isOpen = false;\n this.activeIndex = -1;\n this.searchInput = null;\n this.resultsContainer = null;\n this.detailInput = null;\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * 여러 번 호출해도 안전하다. init()으로 다시 초기화하면 완전히\n * 재사용할 수 있다 — destroyed 가드는 초기화되지 않은 인스턴스가 아니라\n * \"이 destroy() 호출 이후, 다음 init() 전까지\"만 selectAddress()를 막는다.\n */\n public destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.teardown();\n }\n\n /**\n * 선택된 주소를 돌려준다. 선택 전이면 null.\n * detailAddress는 상세주소 입력란의 현재 값이 반영된다.\n */\n public getSelectedAddress(): AddressResult | null {\n if (!this.selected) return null;\n return {\n ...this.selected,\n detailAddress: this.detailInput?.value ?? '',\n };\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { Ref, ShallowRef } from 'vue';
2
- import { e as AddressResult, K as KoreanAddressFinder, d as AddressFinderOptions } from '../address-finder-DIU0lay6.cjs';
3
- export { f as AddressSelectMeta } from '../address-finder-DIU0lay6.cjs';
2
+ import { e as AddressResult, K as KoreanAddressFinder, d as AddressFinderOptions } from '../address-finder-sdLnl5hT.cjs';
3
+ export { f as AddressSelectMeta } from '../address-finder-sdLnl5hT.cjs';
4
4
 
5
5
  interface UseKoreanAddressResult {
6
6
  /** 검색 UI를 그릴 요소에 ref로 연결한다 */
@@ -1,6 +1,6 @@
1
1
  import { Ref, ShallowRef } from 'vue';
2
- import { e as AddressResult, K as KoreanAddressFinder, d as AddressFinderOptions } from '../address-finder-DIU0lay6.js';
3
- export { f as AddressSelectMeta } from '../address-finder-DIU0lay6.js';
2
+ import { e as AddressResult, K as KoreanAddressFinder, d as AddressFinderOptions } from '../address-finder-sdLnl5hT.js';
3
+ export { f as AddressSelectMeta } from '../address-finder-sdLnl5hT.js';
4
4
 
5
5
  interface UseKoreanAddressResult {
6
6
  /** 검색 UI를 그릴 요소에 ref로 연결한다 */
package/dist/vue/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KoreanAddressFinder
3
- } from "../chunk-2OLEGOFJ.js";
3
+ } from "../chunk-H7AKAKT3.js";
4
4
 
5
5
  // src/vue/index.ts
6
6
  import { onBeforeUnmount, ref, shallowRef, watch } from "vue";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slick-address-kr",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "행정안전부 도로명주소 API 기반 팝업 없는 한국 주소 검색 컴포넌트 (자동완성 지원) - Korean address search with autocomplete using official road address API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",