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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.1] - 2026-10-06
9
+
10
+ ### Fixed
11
+ - **틀린 에러 코드 매핑** — `E0005`·`E0006`을 "승인키가 이 도메인에서 허용되지
12
+ 않았습니다"로 안내했지만, 실제 API에서 `E0005`는 빈 검색어, `E0006`은
13
+ "서울"처럼 시도명만 넣은 검색어입니다. 승인키 거절은 `E0001`입니다.
14
+ 주소를 짧게 쳤을 뿐인 사용자에게 승인키 안내가 뜨던 문제를 고쳤습니다.
15
+ - **검색어 문제를 오류처럼 보여주던 문제** — 한 글자 입력(`E0008`) 등에
16
+ "주소 검색에 실패했습니다. (E0008) …"가 빨간 글씨로, 머리줄과 본문에 두 번
17
+ 떴습니다. 이제 코드 번호 없이 "두 글자 이상 입력해 주세요." 같은 안내를
18
+ 머리줄 한 줄로만 보여줍니다.
19
+
20
+ ### Changed
21
+ - **자동완성은 두 글자부터 검색** — 한 글자는 API가 거절하므로 요청하지 않고,
22
+ 떠 있던 결과는 닫습니다. 검색 버튼·Enter로 직접 검색하면 위 안내가 뜹니다.
23
+
24
+ ### Added
25
+ - `JusoApiError.isInputError`, `isInputErrorCode()` — 검색어를 고치면 풀리는
26
+ 실패인지 구분합니다.
27
+
8
28
  ## [2.0.0] - 2026-09-02
9
29
 
10
30
  패키지가 실제로 동작하도록 고치는 릴리스입니다. 자세한 이관 방법은
package/README.md CHANGED
@@ -328,8 +328,17 @@ try {
328
328
  }
329
329
  ```
330
330
 
331
- `error.userMessage`는 확인된 코드(승인되지 않은 키·도메인 미등록)에만 상세
332
- 설명을 붙입니다. 그 외 코드는 API가 준 원본 메시지를 그대로 담습니다.
331
+ `error.userMessage`는 실제 API로 확인된 코드에만 따로 쓴 안내를 담습니다.
332
+
333
+ - 승인키 문제(`E0001`): 키·도메인 확인 안내와 발급 링크
334
+ - 검색어 문제(`E0005` 빈 검색어, `E0006` 시도명만 입력, `E0008` 한 글자,
335
+ `E0009`·`E0012` 숫자·특수문자만, `E0013` 금지 특수문자): 코드 번호 없는
336
+ 부드러운 안내. `error.isInputError`가 `true`이고, 컴포넌트는 빨간 오류 대신
337
+ 결과 머리줄 한 줄로 보여줍니다.
338
+
339
+ 그 외 코드는 API가 준 원본 메시지를 그대로 담습니다.
340
+
341
+ 자동완성 모드는 검색어가 두 글자가 될 때까지 검색하지 않습니다.
333
342
 
334
343
  ## 사용 예제
335
344
 
@@ -206,6 +206,13 @@ declare class KoreanAddressFinder {
206
206
  * 검색 결과 닫기
207
207
  */
208
208
  private closeResults;
209
+ /**
210
+ * 검색어 안내 표시 (두 글자 이상 등)
211
+ *
212
+ * 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만
213
+ * 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.
214
+ */
215
+ private showHint;
209
216
  /**
210
217
  * 에러 표시
211
218
  */
@@ -206,6 +206,13 @@ declare class KoreanAddressFinder {
206
206
  * 검색 결과 닫기
207
207
  */
208
208
  private closeResults;
209
+ /**
210
+ * 검색어 안내 표시 (두 글자 이상 등)
211
+ *
212
+ * 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만
213
+ * 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.
214
+ */
215
+ private showHint;
209
216
  /**
210
217
  * 에러 표시
211
218
  */
@@ -1,4 +1,4 @@
1
- "use strict";var SlickAddressKr=(()=>{var p=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var b=(i,e)=>{for(var t in e)p(i,t,{get:e[t],enumerable:!0})},y=(i,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of m(e))!A.call(i,r)&&r!==t&&p(i,r,{get:()=>e[r],enumerable:!(s=v(e,r))||s.enumerable});return i};var g=i=>y(p({},"__esModule",{value:!0}),i);var S={};b(S,{API_KEY_ISSUE_URL:()=>l,AddressApiClient:()=>c,DEFAULT_ENDPOINT:()=>d,JusoApiError:()=>o,KoreanAddressFinder:()=>u,default:()=>u,describeJusoError:()=>f});var d="https://business.juso.go.kr/addrlink/addrLinkApi.do",l="https://business.juso.go.kr/jst/jstAddressApiApplicationWrite";var o=class i extends Error{constructor(e,t){let s=f(e,t);super(`[${e}] ${t}`),this.name="JusoApiError",this.code=e,this.userMessage=s,Object.setPrototypeOf(this,i.prototype)}};function f(i,e){switch(i){case"E0005":case"E0006":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: ${l}`;default:return`\uC8FC\uC18C \uAC80\uC0C9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. (${i}) ${e}`.trim()}}var c=class{constructor(e={}){this.apiKey=e.apiKey??"",this.endpoint=e.endpoint??d,this.usesDefaultEndpoint=this.endpoint===d,this.fetchFn=e.fetchFn??globalThis.fetch.bind(globalThis)}async search(e,t={}){let s=e?.trim()??"";if(s.length===0)return{results:[],totalCount:0};if(this.usesDefaultEndpoint&&!this.apiKey)throw new Error(`API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. KoreanAddressFinder \uC0DD\uC131 \uC2DC apiKey \uC635\uC158\uC744 \uC804\uB2EC\uD558\uAC70\uB098, endpoint \uC635\uC158\uC73C\uB85C \uC790\uCCB4 \uD504\uB85D\uC2DC \uC11C\uBC84\uB97C \uC9C0\uC815\uD558\uC138\uC694. \uD0A4 \uBC1C\uAE09: ${l}`);let r=new URLSearchParams({currentPage:String(t.currentPage??1),countPerPage:String(t.countPerPage??10),keyword:s,resultType:"json"});this.usesDefaultEndpoint&&r.set("confmKey",this.apiKey);let a=await this.fetchFn(`${this.endpoint}?${r.toString()}`,{method:"GET",signal:t.signal});if(!a.ok)throw new Error(`\uC8FC\uC18C \uAC80\uC0C9 \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (HTTP ${a.status})`);let n=await a.json();return this.toSearchResult(n)}toSearchResult(e){let{errorCode:t,errorMessage:s}=e.results.common;if(t!=="0")throw new o(t,s);return{results:(e.results.juso??[]).map(n=>({roadAddress:n.roadAddr,jibunAddress:n.jibunAddr,zipCode:n.zipNo,buildingName:n.bdNm||"",sido:n.siNm,sigungu:n.sggNm,bname:n.emdNm,extraAddress:k(n),codes:I(n)})),totalCount:Number.parseInt(e.results.common.totalCount,10)||0}}};function I(i){return{admCd:i.admCd,rnMgtSn:i.rnMgtSn,udrtYn:i.udrtYn,buldMnnm:i.buldMnnm,buldSlno:i.buldSlno}}function k(i){let e=[];return i.bdNm&&e.push(i.bdNm),i.detBdNmList&&e.push(i.detBdNmList),e.join(", ")}var E=0,u=class{constructor(e={}){this.instanceId=`kaf-${++E}-${Math.random().toString(36).slice(2,8)}`;this.activeIndex=-1;this.container=null;this.searchInput=null;this.resultsContainer=null;this.detailInput=null;this.isOpen=!1;this.debounceTimer=null;this.abortController=null;this.selected=null;this.results=[];this.listeners=[];this.destroyed=!1;this.apiClient=new c({apiKey:e.apiKey,endpoint:e.endpoint,fetchFn:e.fetchFn}),this.options={apiKey:e.apiKey||"",countPerPage:e.countPerPage||10,currentPage:e.currentPage||1,containerId:e.containerId||"address-finder-container",onSelect:e.onSelect||(()=>{}),showDetailInput:e.showDetailInput!==!1,customClass:e.customClass||"",autocompleteMode:e.autocompleteMode||!1,endpoint:e.endpoint??"",fetchFn:e.fetchFn}}init(e){if(this.container&&this.teardown(),this.destroyed=!1,e?this.container=e:this.container=document.getElementById(this.options.containerId),!this.container)throw new Error(`Container element not found: ${this.options.containerId}`);this.render(),this.attachEventListeners()}render(){if(!this.container)return;let e=this.options.customClass?` ${this.escapeHtml(this.options.customClass)}`:"",t=`${this.instanceId}-listbox`;this.container.innerHTML=`
1
+ "use strict";var SlickAddressKr=(()=>{var f=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var g=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},I=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of b(e))!y.call(r,i)&&i!==t&&f(r,i,{get:()=>e[i],enumerable:!(s=A(e,i))||s.enumerable});return r};var E=r=>I(f({},"__esModule",{value:!0}),r);var T={};g(T,{API_KEY_ISSUE_URL:()=>l,AddressApiClient:()=>c,DEFAULT_ENDPOINT:()=>d,JusoApiError:()=>a,KoreanAddressFinder:()=>u,default:()=>u,describeJusoError:()=>v,isInputErrorCode:()=>p});var d="https://business.juso.go.kr/addrlink/addrLinkApi.do",l="https://business.juso.go.kr/jst/jstAddressApiApplicationWrite";var a=class r extends Error{constructor(e,t){let s=v(e,t);super(`[${e}] ${t}`),this.name="JusoApiError",this.code=e,this.userMessage=s,this.isInputError=p(e),Object.setPrototypeOf(this,r.prototype)}},m={E0005:"\uAC80\uC0C9\uC5B4\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694.",E0006:"\uC8FC\uC18C\uB97C \uC870\uAE08 \uB354 \uC790\uC138\uD788 \uC785\uB825\uD574 \uC8FC\uC138\uC694. (\uC608: \uD310\uAD50\uC5ED\uB85C 235)",E0008:"\uB450 \uAE00\uC790 \uC774\uC0C1 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",E0009:"\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",E0012:"\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",E0013:"\uD2B9\uC218\uBB38\uC790(%, =, <, > \uB4F1)\uB97C \uBE7C\uACE0 \uB2E4\uC2DC \uC785\uB825\uD574 \uC8FC\uC138\uC694."};function p(r){return Object.prototype.hasOwnProperty.call(m,r)}function v(r,e){return p(r)?m[r]:r==="E0001"?`\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: ${l}`:`\uC8FC\uC18C \uAC80\uC0C9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. (${r}) ${e}`.trim()}var c=class{constructor(e={}){this.apiKey=e.apiKey??"",this.endpoint=e.endpoint??d,this.usesDefaultEndpoint=this.endpoint===d,this.fetchFn=e.fetchFn??globalThis.fetch.bind(globalThis)}async search(e,t={}){let s=e?.trim()??"";if(s.length===0)return{results:[],totalCount:0};if(this.usesDefaultEndpoint&&!this.apiKey)throw new Error(`API \uD0A4\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. KoreanAddressFinder \uC0DD\uC131 \uC2DC apiKey \uC635\uC158\uC744 \uC804\uB2EC\uD558\uAC70\uB098, endpoint \uC635\uC158\uC73C\uB85C \uC790\uCCB4 \uD504\uB85D\uC2DC \uC11C\uBC84\uB97C \uC9C0\uC815\uD558\uC138\uC694. \uD0A4 \uBC1C\uAE09: ${l}`);let i=new URLSearchParams({currentPage:String(t.currentPage??1),countPerPage:String(t.countPerPage??10),keyword:s,resultType:"json"});this.usesDefaultEndpoint&&i.set("confmKey",this.apiKey);let o=await this.fetchFn(`${this.endpoint}?${i.toString()}`,{method:"GET",signal:t.signal});if(!o.ok)throw new Error(`\uC8FC\uC18C \uAC80\uC0C9 \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (HTTP ${o.status})`);let n=await o.json();return this.toSearchResult(n)}toSearchResult(e){let{errorCode:t,errorMessage:s}=e.results.common;if(t!=="0")throw new a(t,s);return{results:(e.results.juso??[]).map(n=>({roadAddress:n.roadAddr,jibunAddress:n.jibunAddr,zipCode:n.zipNo,buildingName:n.bdNm||"",sido:n.siNm,sigungu:n.sggNm,bname:n.emdNm,extraAddress:S(n),codes:k(n)})),totalCount:Number.parseInt(e.results.common.totalCount,10)||0}}};function k(r){return{admCd:r.admCd,rnMgtSn:r.rnMgtSn,udrtYn:r.udrtYn,buldMnnm:r.buldMnnm,buldSlno:r.buldSlno}}function S(r){let e=[];return r.bdNm&&e.push(r.bdNm),r.detBdNmList&&e.push(r.detBdNmList),e.join(", ")}var C=2,x=0,u=class{constructor(e={}){this.instanceId=`kaf-${++x}-${Math.random().toString(36).slice(2,8)}`;this.activeIndex=-1;this.container=null;this.searchInput=null;this.resultsContainer=null;this.detailInput=null;this.isOpen=!1;this.debounceTimer=null;this.abortController=null;this.selected=null;this.results=[];this.listeners=[];this.destroyed=!1;this.apiClient=new c({apiKey:e.apiKey,endpoint:e.endpoint,fetchFn:e.fetchFn}),this.options={apiKey:e.apiKey||"",countPerPage:e.countPerPage||10,currentPage:e.currentPage||1,containerId:e.containerId||"address-finder-container",onSelect:e.onSelect||(()=>{}),showDetailInput:e.showDetailInput!==!1,customClass:e.customClass||"",autocompleteMode:e.autocompleteMode||!1,endpoint:e.endpoint??"",fetchFn:e.fetchFn}}init(e){if(this.container&&this.teardown(),this.destroyed=!1,e?this.container=e:this.container=document.getElementById(this.options.containerId),!this.container)throw new Error(`Container element not found: ${this.options.containerId}`);this.render(),this.attachEventListeners()}render(){if(!this.container)return;let e=this.options.customClass?` ${this.escapeHtml(this.options.customClass)}`:"",t=`${this.instanceId}-listbox`;this.container.innerHTML=`
2
2
  <div class="kaf-wrapper${e}">
3
3
  <div class="kaf-search-box">
4
4
  <input
@@ -33,7 +33,7 @@
33
33
  `:""}
34
34
  <div class="kaf-selected-address" style="display: none;"></div>
35
35
  </div>
36
- `,this.searchInput=this.container.querySelector(".kaf-search-input"),this.resultsContainer=this.container.querySelector(".kaf-results"),this.detailInput=this.container.querySelector(".kaf-detail-input")}on(e,t,s){e.addEventListener(t,s),this.listeners.push(()=>e.removeEventListener(t,s))}attachEventListeners(){if(!this.container)return;let e=this.container.querySelector(".kaf-search-btn");e&&this.on(e,"click",()=>{this.search()}),this.searchInput&&(this.on(this.searchInput,"keydown",s=>this.handleKeydown(s)),this.options.autocompleteMode&&this.on(this.searchInput,"input",()=>this.handleAutocomplete()));let t=this.container.querySelector(".kaf-close-btn");t&&this.on(t,"click",()=>this.closeResults()),this.detailInput&&this.on(this.detailInput,"input",()=>this.emitSelect("detail-change")),this.on(document,"click",s=>{this.isOpen&&!this.container?.contains(s.target)&&this.closeResults()})}handleKeydown(e){let t=this.results.length;switch(e.key){case"ArrowDown":if(!this.isOpen||t===0)return;e.preventDefault(),this.setActiveIndex(this.activeIndex>=t-1?0:this.activeIndex+1);return;case"ArrowUp":if(!this.isOpen||t===0)return;e.preventDefault(),this.setActiveIndex(this.activeIndex<=0?t-1:this.activeIndex-1);return;case"Enter":e.preventDefault(),this.isOpen&&this.activeIndex>=0&&this.results[this.activeIndex]?this.selectAddress(this.results[this.activeIndex]):this.search();return;case"Escape":if(!this.isOpen)return;e.preventDefault(),this.closeResults();return;default:}}setActiveIndex(e){this.activeIndex=e;let t=this.container?.querySelectorAll(".kaf-result-item");if(t?.forEach((s,r)=>{let a=r===e;a?s.setAttribute("aria-selected","true"):s.removeAttribute("aria-selected"),s.classList.toggle("kaf-result-item--active",a),a&&s.scrollIntoView?.({block:"nearest"})}),this.searchInput){let s=e>=0?t?.[e]:void 0;s?this.searchInput.setAttribute("aria-activedescendant",s.id):this.searchInput.removeAttribute("aria-activedescendant")}}handleAutocomplete(){this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=window.setTimeout(()=>{this.search()},300)}async search(){let e=this.searchInput?.value.trim();if(!e)return;this.abortController?.abort();let t=new AbortController;this.abortController=t;try{let{results:s,totalCount:r}=await this.apiClient.search(e,{currentPage:this.options.currentPage,countPerPage:this.options.countPerPage,signal:t.signal});if(this.abortController!==t)return;this.displayResults(s,r)}catch(s){if(this.abortController!==t||s instanceof DOMException&&s.name==="AbortError")return;let r=s instanceof o?s.userMessage:s instanceof Error?s.message:"\uC8FC\uC18C \uAC80\uC0C9 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";this.showError(r)}}displayResults(e,t){let s=this.container?.querySelector(".kaf-results-list"),r=this.container?.querySelector(".kaf-results-message"),a=this.container?.querySelector(".kaf-results-count");!s||!r||!a||(this.results=e,e.length===0?(s.innerHTML="",r.innerHTML='<div class="kaf-no-results">\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</div>'):(r.innerHTML="",s.innerHTML=e.map((n,h)=>`
36
+ `,this.searchInput=this.container.querySelector(".kaf-search-input"),this.resultsContainer=this.container.querySelector(".kaf-results"),this.detailInput=this.container.querySelector(".kaf-detail-input")}on(e,t,s){e.addEventListener(t,s),this.listeners.push(()=>e.removeEventListener(t,s))}attachEventListeners(){if(!this.container)return;let e=this.container.querySelector(".kaf-search-btn");e&&this.on(e,"click",()=>{this.search()}),this.searchInput&&(this.on(this.searchInput,"keydown",s=>this.handleKeydown(s)),this.options.autocompleteMode&&this.on(this.searchInput,"input",()=>this.handleAutocomplete()));let t=this.container.querySelector(".kaf-close-btn");t&&this.on(t,"click",()=>this.closeResults()),this.detailInput&&this.on(this.detailInput,"input",()=>this.emitSelect("detail-change")),this.on(document,"click",s=>{this.isOpen&&!this.container?.contains(s.target)&&this.closeResults()})}handleKeydown(e){let t=this.results.length;switch(e.key){case"ArrowDown":if(!this.isOpen||t===0)return;e.preventDefault(),this.setActiveIndex(this.activeIndex>=t-1?0:this.activeIndex+1);return;case"ArrowUp":if(!this.isOpen||t===0)return;e.preventDefault(),this.setActiveIndex(this.activeIndex<=0?t-1:this.activeIndex-1);return;case"Enter":e.preventDefault(),this.isOpen&&this.activeIndex>=0&&this.results[this.activeIndex]?this.selectAddress(this.results[this.activeIndex]):this.search();return;case"Escape":if(!this.isOpen)return;e.preventDefault(),this.closeResults();return;default:}}setActiveIndex(e){this.activeIndex=e;let t=this.container?.querySelectorAll(".kaf-result-item");if(t?.forEach((s,i)=>{let o=i===e;o?s.setAttribute("aria-selected","true"):s.removeAttribute("aria-selected"),s.classList.toggle("kaf-result-item--active",o),o&&s.scrollIntoView?.({block:"nearest"})}),this.searchInput){let s=e>=0?t?.[e]:void 0;s?this.searchInput.setAttribute("aria-activedescendant",s.id):this.searchInput.removeAttribute("aria-activedescendant")}}handleAutocomplete(){if(this.debounceTimer&&clearTimeout(this.debounceTimer),(this.searchInput?.value.trim()??"").length<C){this.debounceTimer=null,this.abortController?.abort(),this.abortController=null,this.closeResults();return}this.debounceTimer=window.setTimeout(()=>{this.search()},300)}async search(){let e=this.searchInput?.value.trim();if(!e)return;this.abortController?.abort();let t=new AbortController;this.abortController=t;try{let{results:s,totalCount:i}=await this.apiClient.search(e,{currentPage:this.options.currentPage,countPerPage:this.options.countPerPage,signal:t.signal});if(this.abortController!==t)return;this.displayResults(s,i)}catch(s){if(this.abortController!==t||s instanceof DOMException&&s.name==="AbortError")return;if(s instanceof a&&s.isInputError){this.showHint(s.userMessage);return}let i=s instanceof a?s.userMessage:s instanceof Error?s.message:"\uC8FC\uC18C \uAC80\uC0C9 \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";this.showError(i)}}displayResults(e,t){let s=this.container?.querySelector(".kaf-results-list"),i=this.container?.querySelector(".kaf-results-message"),o=this.container?.querySelector(".kaf-results-count");!s||!i||!o||(this.results=e,e.length===0?(s.innerHTML="",i.innerHTML='<div class="kaf-no-results">\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</div>'):(i.innerHTML="",s.innerHTML=e.map((n,h)=>`
37
37
  <div
38
38
  class="kaf-result-item"
39
39
  id="${this.instanceId}-option-${h}"
@@ -51,7 +51,7 @@
51
51
  <div class="kaf-result-zipcode">\uC6B0\uD3B8\uBC88\uD638: ${this.escapeHtml(n.zipCode)}</div>
52
52
  ${n.buildingName?`<div class="kaf-result-building">${this.escapeHtml(n.buildingName)}</div>`:""}
53
53
  </div>
54
- `).join(""),s.querySelectorAll(".kaf-result-item").forEach((n,h)=>{n.addEventListener("click",()=>this.selectAddress(e[h]))})),this.openResults(),a.textContent="",a.textContent=e.length===0?"\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.":`\uAC80\uC0C9 \uACB0\uACFC (${t}\uAC74)`,this.setActiveIndex(-1))}selectAddress(e){if(this.destroyed)return;this.selected=e;let t=this.container?.querySelector(".kaf-selected-address");if(t&&(t.innerHTML=`
54
+ `).join(""),s.querySelectorAll(".kaf-result-item").forEach((n,h)=>{n.addEventListener("click",()=>this.selectAddress(e[h]))})),this.openResults(),o.textContent="",o.textContent=e.length===0?"\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.":`\uAC80\uC0C9 \uACB0\uACFC (${t}\uAC74)`,this.setActiveIndex(-1))}selectAddress(e){if(this.destroyed)return;this.selected=e;let t=this.container?.querySelector(".kaf-selected-address");if(t&&(t.innerHTML=`
55
55
  <div class="kaf-selected-content">
56
56
  <div class="kaf-selected-main">
57
57
  <strong>\uB3C4\uB85C\uBA85:</strong> ${this.escapeHtml(e.roadAddress)}
@@ -60,6 +60,6 @@
60
60
  <strong>\uC6B0\uD3B8\uBC88\uD638:</strong> ${this.escapeHtml(e.zipCode)}
61
61
  </div>
62
62
  </div>
63
- `,t.style.display="block"),this.options.showDetailInput&&this.detailInput){let s=this.container?.querySelector(".kaf-detail-box");s&&(s.style.display="block",this.detailInput.focus())}this.closeResults(),this.emitSelect("select")}emitSelect(e){let t=this.getSelectedAddress();t&&this.options.onSelect(t,{reason:e})}openResults(){this.resultsContainer&&(this.resultsContainer.style.display="block",this.isOpen=!0,this.searchInput?.setAttribute("aria-expanded","true"))}closeResults(){this.resultsContainer&&(this.resultsContainer.style.display="none",this.isOpen=!1,this.searchInput?.setAttribute("aria-expanded","false"),this.setActiveIndex(-1))}showError(e){let t=this.container?.querySelector(".kaf-results-list"),s=this.container?.querySelector(".kaf-results-message"),r=this.container?.querySelector(".kaf-results-count");!t||!s||!r||(this.results=[],t.innerHTML="",s.innerHTML=`<div class="kaf-error">${this.escapeHtml(e)}</div>`,this.openResults(),r.textContent="",r.textContent=e,this.setActiveIndex(-1))}escapeHtml(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}teardown(){for(let e of this.listeners)e();this.listeners=[],this.debounceTimer!==null&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.abortController?.abort(),this.abortController=null,this.container&&(this.container.innerHTML=""),this.selected=null,this.results=[],this.isOpen=!1,this.activeIndex=-1,this.searchInput=null,this.resultsContainer=null,this.detailInput=null}destroy(){this.destroyed||(this.destroyed=!0,this.teardown())}getSelectedAddress(){return this.selected?{...this.selected,detailAddress:this.detailInput?.value??""}:null}};return g(S);})();
63
+ `,t.style.display="block"),this.options.showDetailInput&&this.detailInput){let s=this.container?.querySelector(".kaf-detail-box");s&&(s.style.display="block",this.detailInput.focus())}this.closeResults(),this.emitSelect("select")}emitSelect(e){let t=this.getSelectedAddress();t&&this.options.onSelect(t,{reason:e})}openResults(){this.resultsContainer&&(this.resultsContainer.style.display="block",this.isOpen=!0,this.searchInput?.setAttribute("aria-expanded","true"))}closeResults(){this.resultsContainer&&(this.resultsContainer.style.display="none",this.isOpen=!1,this.searchInput?.setAttribute("aria-expanded","false"),this.setActiveIndex(-1))}showHint(e){let t=this.container?.querySelector(".kaf-results-list"),s=this.container?.querySelector(".kaf-results-message"),i=this.container?.querySelector(".kaf-results-count");!t||!s||!i||(this.results=[],t.innerHTML="",s.innerHTML="",this.openResults(),i.textContent="",i.textContent=e,this.setActiveIndex(-1))}showError(e){let t=this.container?.querySelector(".kaf-results-list"),s=this.container?.querySelector(".kaf-results-message"),i=this.container?.querySelector(".kaf-results-count");!t||!s||!i||(this.results=[],t.innerHTML="",s.innerHTML=`<div class="kaf-error">${this.escapeHtml(e)}</div>`,this.openResults(),i.textContent="",i.textContent=e,this.setActiveIndex(-1))}escapeHtml(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}teardown(){for(let e of this.listeners)e();this.listeners=[],this.debounceTimer!==null&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.abortController?.abort(),this.abortController=null,this.container&&(this.container.innerHTML=""),this.selected=null,this.results=[],this.isOpen=!1,this.activeIndex=-1,this.searchInput=null,this.resultsContainer=null,this.detailInput=null}destroy(){this.destroyed||(this.destroyed=!0,this.teardown())}getSelectedAddress(){return this.selected?{...this.selected,detailAddress:this.detailInput?.value??""}:null}};return E(T);})();
64
64
  window.SlickAddressKr = SlickAddressKr;window.KoreanAddressFinder = SlickAddressKr.KoreanAddressFinder;
65
65
  //# sourceMappingURL=browser.global.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/errors.ts","../src/api-client.ts","../src/address-finder.ts"],"sourcesContent":["/**\n * Korean Address Finder\n * 팝업 없는 한국 주소 검색 라이브러리\n */\n\nexport { KoreanAddressFinder } from './address-finder';\nexport { AddressApiClient, DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './api-client';\nexport { JusoApiError, describeJusoError } from './errors';\nexport type {\n AddressResult,\n AddressCodes,\n AddressSelectMeta,\n AddressFinderOptions,\n AddressApiClientOptions,\n AddressSearchOptions,\n AddressSearchResult,\n JusoApiResponse,\n} from './types';\n\n// 기본 export\nexport { KoreanAddressFinder as default } from './address-finder';\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":["src_exports","__export","API_KEY_ISSUE_URL","AddressApiClient","DEFAULT_ENDPOINT","JusoApiError","KoreanAddressFinder","describeJusoError","DEFAULT_ENDPOINT","API_KEY_ISSUE_URL","JusoApiError","_JusoApiError","code","apiMessage","userMessage","describeJusoError","API_KEY_ISSUE_URL","AddressApiClient","options","DEFAULT_ENDPOINT","keyword","trimmed","API_KEY_ISSUE_URL","params","response","data","errorCode","errorMessage","JusoApiError","item","buildExtraAddress","toCodes","extras","instanceCounter","KoreanAddressFinder","options","AddressApiClient","targetElement","className","listId","target","type","handler","searchBtn","e","closeBtn","event","count","index","option","i","active","keyword","controller","results","totalCount","error","message","JusoApiError","resultsList","resultsMessage","resultsCount","result","item","address","selectedContainer","detailBox","reason","value","off"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/errors.ts","../src/api-client.ts","../src/address-finder.ts"],"sourcesContent":["/**\n * Korean Address Finder\n * 팝업 없는 한국 주소 검색 라이브러리\n */\n\nexport { KoreanAddressFinder } from './address-finder';\nexport { AddressApiClient, DEFAULT_ENDPOINT, API_KEY_ISSUE_URL } from './api-client';\nexport { JusoApiError, describeJusoError, isInputErrorCode } from './errors';\nexport type {\n AddressResult,\n AddressCodes,\n AddressSelectMeta,\n AddressFinderOptions,\n AddressApiClientOptions,\n AddressSearchOptions,\n AddressSearchResult,\n JusoApiResponse,\n} from './types';\n\n// 기본 export\nexport { KoreanAddressFinder as default } from './address-finder';\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":["src_exports","__export","API_KEY_ISSUE_URL","AddressApiClient","DEFAULT_ENDPOINT","JusoApiError","KoreanAddressFinder","describeJusoError","isInputErrorCode","DEFAULT_ENDPOINT","API_KEY_ISSUE_URL","JusoApiError","_JusoApiError","code","apiMessage","userMessage","describeJusoError","isInputErrorCode","INPUT_ERROR_MESSAGES","API_KEY_ISSUE_URL","AddressApiClient","options","DEFAULT_ENDPOINT","keyword","trimmed","API_KEY_ISSUE_URL","params","response","data","errorCode","errorMessage","JusoApiError","item","buildExtraAddress","toCodes","extras","MIN_AUTOCOMPLETE_LENGTH","instanceCounter","KoreanAddressFinder","options","AddressApiClient","targetElement","className","listId","target","type","handler","searchBtn","e","closeBtn","event","count","index","option","active","keyword","controller","results","totalCount","error","JusoApiError","message","resultsList","resultsMessage","resultsCount","result","item","address","selectedContainer","detailBox","reason","value","off"]}
@@ -10,14 +10,33 @@ var JusoApiError = class _JusoApiError extends Error {
10
10
  this.name = "JusoApiError";
11
11
  this.code = code;
12
12
  this.userMessage = userMessage;
13
+ this.isInputError = isInputErrorCode(code);
13
14
  Object.setPrototypeOf(this, _JusoApiError.prototype);
14
15
  }
15
16
  };
17
+ var INPUT_ERROR_MESSAGES = {
18
+ // 검색어가 입력되지 않았습니다.
19
+ E0005: "\uAC80\uC0C9\uC5B4\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
20
+ // 주소를 상세히 입력해 주시기 바랍니다. ('서울', '서울시'처럼 시도명만 넣은 경우)
21
+ E0006: "\uC8FC\uC18C\uB97C \uC870\uAE08 \uB354 \uC790\uC138\uD788 \uC785\uB825\uD574 \uC8FC\uC138\uC694. (\uC608: \uD310\uAD50\uC5ED\uB85C 235)",
22
+ // 검색어는 두글자 이상 입력되어야 합니다.
23
+ E0008: "\uB450 \uAE00\uC790 \uC774\uC0C1 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
24
+ // 검색어는 문자와 숫자 같이 입력되어야 합니다. (숫자만 입력한 경우)
25
+ E0009: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
26
+ // 특수문자+숫자만으로는 검색이 불가능 합니다.
27
+ E0012: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
28
+ // SQL 예약어 또는 특수문자( %,=,>,<,[,] )는 검색이 불가능 합니다.
29
+ E0013: "\uD2B9\uC218\uBB38\uC790(%, =, <, > \uB4F1)\uB97C \uBE7C\uACE0 \uB2E4\uC2DC \uC785\uB825\uD574 \uC8FC\uC138\uC694."
30
+ };
31
+ function isInputErrorCode(code) {
32
+ return Object.prototype.hasOwnProperty.call(INPUT_ERROR_MESSAGES, code);
33
+ }
16
34
  function describeJusoError(code, apiMessage) {
35
+ if (isInputErrorCode(code)) return INPUT_ERROR_MESSAGES[code];
17
36
  switch (code) {
18
- case "E0005":
19
- case "E0006":
20
- 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}`;
37
+ // 승인되지 않은 KEY 입니다. (키가 틀렸거나 비어 있는 경우)
38
+ case "E0001":
39
+ 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}`;
21
40
  default:
22
41
  return `\uC8FC\uC18C \uAC80\uC0C9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. (${code}) ${apiMessage}`.trim();
23
42
  }
@@ -100,6 +119,7 @@ function buildExtraAddress(item) {
100
119
  }
101
120
 
102
121
  // src/address-finder.ts
122
+ var MIN_AUTOCOMPLETE_LENGTH = 2;
103
123
  var instanceCounter = 0;
104
124
  var KoreanAddressFinder = class {
105
125
  constructor(options = {}) {
@@ -308,6 +328,14 @@ var KoreanAddressFinder = class {
308
328
  if (this.debounceTimer) {
309
329
  clearTimeout(this.debounceTimer);
310
330
  }
331
+ const keyword = this.searchInput?.value.trim() ?? "";
332
+ if (keyword.length < MIN_AUTOCOMPLETE_LENGTH) {
333
+ this.debounceTimer = null;
334
+ this.abortController?.abort();
335
+ this.abortController = null;
336
+ this.closeResults();
337
+ return;
338
+ }
311
339
  this.debounceTimer = window.setTimeout(() => {
312
340
  this.search();
313
341
  }, 300);
@@ -333,6 +361,10 @@ var KoreanAddressFinder = class {
333
361
  } catch (error) {
334
362
  if (this.abortController !== controller) return;
335
363
  if (error instanceof DOMException && error.name === "AbortError") return;
364
+ if (error instanceof JusoApiError && error.isInputError) {
365
+ this.showHint(error.userMessage);
366
+ return;
367
+ }
336
368
  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.";
337
369
  this.showError(message);
338
370
  }
@@ -441,6 +473,25 @@ var KoreanAddressFinder = class {
441
473
  this.setActiveIndex(-1);
442
474
  }
443
475
  }
476
+ /**
477
+ * 검색어 안내 표시 (두 글자 이상 등)
478
+ *
479
+ * 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만
480
+ * 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.
481
+ */
482
+ showHint(message) {
483
+ const resultsList = this.container?.querySelector(".kaf-results-list");
484
+ const resultsMessage = this.container?.querySelector(".kaf-results-message");
485
+ const resultsCount = this.container?.querySelector(".kaf-results-count");
486
+ if (!resultsList || !resultsMessage || !resultsCount) return;
487
+ this.results = [];
488
+ resultsList.innerHTML = "";
489
+ resultsMessage.innerHTML = "";
490
+ this.openResults();
491
+ resultsCount.textContent = "";
492
+ resultsCount.textContent = message;
493
+ this.setActiveIndex(-1);
494
+ }
444
495
  /**
445
496
  * 에러 표시
446
497
  */
@@ -515,8 +566,9 @@ export {
515
566
  DEFAULT_ENDPOINT,
516
567
  API_KEY_ISSUE_URL,
517
568
  JusoApiError,
569
+ isInputErrorCode,
518
570
  describeJusoError,
519
571
  AddressApiClient,
520
572
  KoreanAddressFinder
521
573
  };
522
- //# sourceMappingURL=chunk-2OLEGOFJ.js.map
574
+ //# sourceMappingURL=chunk-H7AKAKT3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/errors.ts","../src/api-client.ts","../src/address-finder.ts"],"sourcesContent":["/** 행정안전부 도로명주소 검색 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":[]}