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 +20 -0
- package/README.md +11 -2
- package/dist/{address-finder-DIU0lay6.d.cts → address-finder-sdLnl5hT.d.cts} +7 -0
- package/dist/{address-finder-DIU0lay6.d.ts → address-finder-sdLnl5hT.d.ts} +7 -0
- package/dist/browser.global.js +4 -4
- package/dist/browser.global.js.map +1 -1
- package/dist/{chunk-2OLEGOFJ.js → chunk-H7AKAKT3.js} +56 -4
- package/dist/chunk-H7AKAKT3.js.map +1 -0
- package/dist/index.cjs +58 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -3
- package/dist/react/index.cjs +54 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/dist/vue/index.cjs +54 -3
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.d.cts +2 -2
- package/dist/vue/index.d.ts +2 -2
- package/dist/vue/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-2OLEGOFJ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -26,7 +26,8 @@ __export(src_exports, {
|
|
|
26
26
|
JusoApiError: () => JusoApiError,
|
|
27
27
|
KoreanAddressFinder: () => KoreanAddressFinder,
|
|
28
28
|
default: () => KoreanAddressFinder,
|
|
29
|
-
describeJusoError: () => describeJusoError
|
|
29
|
+
describeJusoError: () => describeJusoError,
|
|
30
|
+
isInputErrorCode: () => isInputErrorCode
|
|
30
31
|
});
|
|
31
32
|
module.exports = __toCommonJS(src_exports);
|
|
32
33
|
|
|
@@ -42,14 +43,33 @@ var JusoApiError = class _JusoApiError extends Error {
|
|
|
42
43
|
this.name = "JusoApiError";
|
|
43
44
|
this.code = code;
|
|
44
45
|
this.userMessage = userMessage;
|
|
46
|
+
this.isInputError = isInputErrorCode(code);
|
|
45
47
|
Object.setPrototypeOf(this, _JusoApiError.prototype);
|
|
46
48
|
}
|
|
47
49
|
};
|
|
50
|
+
var INPUT_ERROR_MESSAGES = {
|
|
51
|
+
// 검색어가 입력되지 않았습니다.
|
|
52
|
+
E0005: "\uAC80\uC0C9\uC5B4\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
53
|
+
// 주소를 상세히 입력해 주시기 바랍니다. ('서울', '서울시'처럼 시도명만 넣은 경우)
|
|
54
|
+
E0006: "\uC8FC\uC18C\uB97C \uC870\uAE08 \uB354 \uC790\uC138\uD788 \uC785\uB825\uD574 \uC8FC\uC138\uC694. (\uC608: \uD310\uAD50\uC5ED\uB85C 235)",
|
|
55
|
+
// 검색어는 두글자 이상 입력되어야 합니다.
|
|
56
|
+
E0008: "\uB450 \uAE00\uC790 \uC774\uC0C1 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
57
|
+
// 검색어는 문자와 숫자 같이 입력되어야 합니다. (숫자만 입력한 경우)
|
|
58
|
+
E0009: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
59
|
+
// 특수문자+숫자만으로는 검색이 불가능 합니다.
|
|
60
|
+
E0012: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
61
|
+
// SQL 예약어 또는 특수문자( %,=,>,<,[,] )는 검색이 불가능 합니다.
|
|
62
|
+
E0013: "\uD2B9\uC218\uBB38\uC790(%, =, <, > \uB4F1)\uB97C \uBE7C\uACE0 \uB2E4\uC2DC \uC785\uB825\uD574 \uC8FC\uC138\uC694."
|
|
63
|
+
};
|
|
64
|
+
function isInputErrorCode(code) {
|
|
65
|
+
return Object.prototype.hasOwnProperty.call(INPUT_ERROR_MESSAGES, code);
|
|
66
|
+
}
|
|
48
67
|
function describeJusoError(code, apiMessage) {
|
|
68
|
+
if (isInputErrorCode(code)) return INPUT_ERROR_MESSAGES[code];
|
|
49
69
|
switch (code) {
|
|
50
|
-
|
|
51
|
-
case "
|
|
52
|
-
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.
|
|
70
|
+
// 승인되지 않은 KEY 입니다. (키가 틀렸거나 비어 있는 경우)
|
|
71
|
+
case "E0001":
|
|
72
|
+
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}`;
|
|
53
73
|
default:
|
|
54
74
|
return `\uC8FC\uC18C \uAC80\uC0C9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. (${code}) ${apiMessage}`.trim();
|
|
55
75
|
}
|
|
@@ -132,6 +152,7 @@ function buildExtraAddress(item) {
|
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
// src/address-finder.ts
|
|
155
|
+
var MIN_AUTOCOMPLETE_LENGTH = 2;
|
|
135
156
|
var instanceCounter = 0;
|
|
136
157
|
var KoreanAddressFinder = class {
|
|
137
158
|
constructor(options = {}) {
|
|
@@ -340,6 +361,14 @@ var KoreanAddressFinder = class {
|
|
|
340
361
|
if (this.debounceTimer) {
|
|
341
362
|
clearTimeout(this.debounceTimer);
|
|
342
363
|
}
|
|
364
|
+
const keyword = this.searchInput?.value.trim() ?? "";
|
|
365
|
+
if (keyword.length < MIN_AUTOCOMPLETE_LENGTH) {
|
|
366
|
+
this.debounceTimer = null;
|
|
367
|
+
this.abortController?.abort();
|
|
368
|
+
this.abortController = null;
|
|
369
|
+
this.closeResults();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
343
372
|
this.debounceTimer = window.setTimeout(() => {
|
|
344
373
|
this.search();
|
|
345
374
|
}, 300);
|
|
@@ -365,6 +394,10 @@ var KoreanAddressFinder = class {
|
|
|
365
394
|
} catch (error) {
|
|
366
395
|
if (this.abortController !== controller) return;
|
|
367
396
|
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
397
|
+
if (error instanceof JusoApiError && error.isInputError) {
|
|
398
|
+
this.showHint(error.userMessage);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
368
401
|
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.";
|
|
369
402
|
this.showError(message);
|
|
370
403
|
}
|
|
@@ -473,6 +506,25 @@ var KoreanAddressFinder = class {
|
|
|
473
506
|
this.setActiveIndex(-1);
|
|
474
507
|
}
|
|
475
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* 검색어 안내 표시 (두 글자 이상 등)
|
|
511
|
+
*
|
|
512
|
+
* 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만
|
|
513
|
+
* 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.
|
|
514
|
+
*/
|
|
515
|
+
showHint(message) {
|
|
516
|
+
const resultsList = this.container?.querySelector(".kaf-results-list");
|
|
517
|
+
const resultsMessage = this.container?.querySelector(".kaf-results-message");
|
|
518
|
+
const resultsCount = this.container?.querySelector(".kaf-results-count");
|
|
519
|
+
if (!resultsList || !resultsMessage || !resultsCount) return;
|
|
520
|
+
this.results = [];
|
|
521
|
+
resultsList.innerHTML = "";
|
|
522
|
+
resultsMessage.innerHTML = "";
|
|
523
|
+
this.openResults();
|
|
524
|
+
resultsCount.textContent = "";
|
|
525
|
+
resultsCount.textContent = message;
|
|
526
|
+
this.setActiveIndex(-1);
|
|
527
|
+
}
|
|
476
528
|
/**
|
|
477
529
|
* 에러 표시
|
|
478
530
|
*/
|
|
@@ -549,6 +601,7 @@ var KoreanAddressFinder = class {
|
|
|
549
601
|
DEFAULT_ENDPOINT,
|
|
550
602
|
JusoApiError,
|
|
551
603
|
KoreanAddressFinder,
|
|
552
|
-
describeJusoError
|
|
604
|
+
describeJusoError,
|
|
605
|
+
isInputErrorCode
|
|
553
606
|
});
|
|
554
607
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * destroy()와 init()의 재마운트 양쪽이 공유하는 정리 로직이다.\n */\n private teardown(): void {\n for (const off of this.listeners) off();\n this.listeners = [];\n\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n\n this.abortController?.abort();\n this.abortController = null;\n\n if (this.container) {\n this.container.innerHTML = '';\n }\n\n this.selected = null;\n this.results = [];\n this.isOpen = false;\n this.activeIndex = -1;\n this.searchInput = null;\n this.resultsContainer = null;\n this.detailInput = null;\n }\n\n /**\n * 리스너와 진행 중 요청을 정리하고 DOM을 비운다.\n * 여러 번 호출해도 안전하다. init()으로 다시 초기화하면 완전히\n * 재사용할 수 있다 — destroyed 가드는 초기화되지 않은 인스턴스가 아니라\n * \"이 destroy() 호출 이후, 다음 init() 전까지\"만 selectAddress()를 막는다.\n */\n public destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.teardown();\n }\n\n /**\n * 선택된 주소를 돌려준다. 선택 전이면 null.\n * detailAddress는 상세주소 입력란의 현재 값이 반영된다.\n */\n public getSelectedAddress(): AddressResult | null {\n if (!this.selected) return null;\n return {\n ...this.selected,\n detailAddress: this.detailInput?.value ?? '',\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AddressApiClientOptions, a as AddressSearchOptions, b as AddressSearchResult } from './address-finder-
|
|
2
|
-
export { c as AddressCodes, d as AddressFinderOptions, e as AddressResult, f as AddressSelectMeta, J as JusoApiResponse, K as KoreanAddressFinder, K as default } from './address-finder-
|
|
1
|
+
import { A as AddressApiClientOptions, a as AddressSearchOptions, b as AddressSearchResult } from './address-finder-sdLnl5hT.cjs';
|
|
2
|
+
export { c as AddressCodes, d as AddressFinderOptions, e as AddressResult, f as AddressSelectMeta, J as JusoApiResponse, K as KoreanAddressFinder, K as default } from './address-finder-sdLnl5hT.cjs';
|
|
3
3
|
|
|
4
4
|
/** 행정안전부 도로명주소 검색 API 기본 엔드포인트 */
|
|
5
5
|
declare const DEFAULT_ENDPOINT = "https://business.juso.go.kr/addrlink/addrLinkApi.do";
|
|
@@ -31,8 +31,12 @@ declare class AddressApiClient {
|
|
|
31
31
|
declare class JusoApiError extends Error {
|
|
32
32
|
readonly code: string;
|
|
33
33
|
readonly userMessage: string;
|
|
34
|
+
/** 검색어를 고치면 풀리는 실패인지. 오류가 아니라 입력 안내로 보여준다. */
|
|
35
|
+
readonly isInputError: boolean;
|
|
34
36
|
constructor(code: string, apiMessage: string);
|
|
35
37
|
}
|
|
38
|
+
/** 검색어를 고치면 풀리는 코드인지 */
|
|
39
|
+
declare function isInputErrorCode(code: string): boolean;
|
|
36
40
|
/**
|
|
37
41
|
* errorCode를 행동 가능한 한국어 안내로 바꾼다.
|
|
38
42
|
*
|
|
@@ -43,4 +47,4 @@ declare class JusoApiError extends Error {
|
|
|
43
47
|
*/
|
|
44
48
|
declare function describeJusoError(code: string, apiMessage: string): string;
|
|
45
49
|
|
|
46
|
-
export { API_KEY_ISSUE_URL, AddressApiClient, AddressApiClientOptions, AddressSearchOptions, AddressSearchResult, DEFAULT_ENDPOINT, JusoApiError, describeJusoError };
|
|
50
|
+
export { API_KEY_ISSUE_URL, AddressApiClient, AddressApiClientOptions, AddressSearchOptions, AddressSearchResult, DEFAULT_ENDPOINT, JusoApiError, describeJusoError, isInputErrorCode };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AddressApiClientOptions, a as AddressSearchOptions, b as AddressSearchResult } from './address-finder-
|
|
2
|
-
export { c as AddressCodes, d as AddressFinderOptions, e as AddressResult, f as AddressSelectMeta, J as JusoApiResponse, K as KoreanAddressFinder, K as default } from './address-finder-
|
|
1
|
+
import { A as AddressApiClientOptions, a as AddressSearchOptions, b as AddressSearchResult } from './address-finder-sdLnl5hT.js';
|
|
2
|
+
export { c as AddressCodes, d as AddressFinderOptions, e as AddressResult, f as AddressSelectMeta, J as JusoApiResponse, K as KoreanAddressFinder, K as default } from './address-finder-sdLnl5hT.js';
|
|
3
3
|
|
|
4
4
|
/** 행정안전부 도로명주소 검색 API 기본 엔드포인트 */
|
|
5
5
|
declare const DEFAULT_ENDPOINT = "https://business.juso.go.kr/addrlink/addrLinkApi.do";
|
|
@@ -31,8 +31,12 @@ declare class AddressApiClient {
|
|
|
31
31
|
declare class JusoApiError extends Error {
|
|
32
32
|
readonly code: string;
|
|
33
33
|
readonly userMessage: string;
|
|
34
|
+
/** 검색어를 고치면 풀리는 실패인지. 오류가 아니라 입력 안내로 보여준다. */
|
|
35
|
+
readonly isInputError: boolean;
|
|
34
36
|
constructor(code: string, apiMessage: string);
|
|
35
37
|
}
|
|
38
|
+
/** 검색어를 고치면 풀리는 코드인지 */
|
|
39
|
+
declare function isInputErrorCode(code: string): boolean;
|
|
36
40
|
/**
|
|
37
41
|
* errorCode를 행동 가능한 한국어 안내로 바꾼다.
|
|
38
42
|
*
|
|
@@ -43,4 +47,4 @@ declare class JusoApiError extends Error {
|
|
|
43
47
|
*/
|
|
44
48
|
declare function describeJusoError(code: string, apiMessage: string): string;
|
|
45
49
|
|
|
46
|
-
export { API_KEY_ISSUE_URL, AddressApiClient, AddressApiClientOptions, AddressSearchOptions, AddressSearchResult, DEFAULT_ENDPOINT, JusoApiError, describeJusoError };
|
|
50
|
+
export { API_KEY_ISSUE_URL, AddressApiClient, AddressApiClientOptions, AddressSearchOptions, AddressSearchResult, DEFAULT_ENDPOINT, JusoApiError, describeJusoError, isInputErrorCode };
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
DEFAULT_ENDPOINT,
|
|
5
5
|
JusoApiError,
|
|
6
6
|
KoreanAddressFinder,
|
|
7
|
-
describeJusoError
|
|
8
|
-
|
|
7
|
+
describeJusoError,
|
|
8
|
+
isInputErrorCode
|
|
9
|
+
} from "./chunk-H7AKAKT3.js";
|
|
9
10
|
export {
|
|
10
11
|
API_KEY_ISSUE_URL,
|
|
11
12
|
AddressApiClient,
|
|
@@ -13,6 +14,7 @@ export {
|
|
|
13
14
|
JusoApiError,
|
|
14
15
|
KoreanAddressFinder,
|
|
15
16
|
KoreanAddressFinder as default,
|
|
16
|
-
describeJusoError
|
|
17
|
+
describeJusoError,
|
|
18
|
+
isInputErrorCode
|
|
17
19
|
};
|
|
18
20
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.cjs
CHANGED
|
@@ -38,14 +38,33 @@ var JusoApiError = class _JusoApiError extends Error {
|
|
|
38
38
|
this.name = "JusoApiError";
|
|
39
39
|
this.code = code;
|
|
40
40
|
this.userMessage = userMessage;
|
|
41
|
+
this.isInputError = isInputErrorCode(code);
|
|
41
42
|
Object.setPrototypeOf(this, _JusoApiError.prototype);
|
|
42
43
|
}
|
|
43
44
|
};
|
|
45
|
+
var INPUT_ERROR_MESSAGES = {
|
|
46
|
+
// 검색어가 입력되지 않았습니다.
|
|
47
|
+
E0005: "\uAC80\uC0C9\uC5B4\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
48
|
+
// 주소를 상세히 입력해 주시기 바랍니다. ('서울', '서울시'처럼 시도명만 넣은 경우)
|
|
49
|
+
E0006: "\uC8FC\uC18C\uB97C \uC870\uAE08 \uB354 \uC790\uC138\uD788 \uC785\uB825\uD574 \uC8FC\uC138\uC694. (\uC608: \uD310\uAD50\uC5ED\uB85C 235)",
|
|
50
|
+
// 검색어는 두글자 이상 입력되어야 합니다.
|
|
51
|
+
E0008: "\uB450 \uAE00\uC790 \uC774\uC0C1 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
52
|
+
// 검색어는 문자와 숫자 같이 입력되어야 합니다. (숫자만 입력한 경우)
|
|
53
|
+
E0009: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
54
|
+
// 특수문자+숫자만으로는 검색이 불가능 합니다.
|
|
55
|
+
E0012: "\uB3C4\uB85C\uBA85\uC774\uB098 \uAC74\uBB3C\uBA85\uC744 \uD568\uAED8 \uC785\uB825\uD574 \uC8FC\uC138\uC694.",
|
|
56
|
+
// SQL 예약어 또는 특수문자( %,=,>,<,[,] )는 검색이 불가능 합니다.
|
|
57
|
+
E0013: "\uD2B9\uC218\uBB38\uC790(%, =, <, > \uB4F1)\uB97C \uBE7C\uACE0 \uB2E4\uC2DC \uC785\uB825\uD574 \uC8FC\uC138\uC694."
|
|
58
|
+
};
|
|
59
|
+
function isInputErrorCode(code) {
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(INPUT_ERROR_MESSAGES, code);
|
|
61
|
+
}
|
|
44
62
|
function describeJusoError(code, apiMessage) {
|
|
63
|
+
if (isInputErrorCode(code)) return INPUT_ERROR_MESSAGES[code];
|
|
45
64
|
switch (code) {
|
|
46
|
-
|
|
47
|
-
case "
|
|
48
|
-
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.
|
|
65
|
+
// 승인되지 않은 KEY 입니다. (키가 틀렸거나 비어 있는 경우)
|
|
66
|
+
case "E0001":
|
|
67
|
+
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}`;
|
|
49
68
|
default:
|
|
50
69
|
return `\uC8FC\uC18C \uAC80\uC0C9\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. (${code}) ${apiMessage}`.trim();
|
|
51
70
|
}
|
|
@@ -128,6 +147,7 @@ function buildExtraAddress(item) {
|
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
// src/address-finder.ts
|
|
150
|
+
var MIN_AUTOCOMPLETE_LENGTH = 2;
|
|
131
151
|
var instanceCounter = 0;
|
|
132
152
|
var KoreanAddressFinder = class {
|
|
133
153
|
constructor(options = {}) {
|
|
@@ -336,6 +356,14 @@ var KoreanAddressFinder = class {
|
|
|
336
356
|
if (this.debounceTimer) {
|
|
337
357
|
clearTimeout(this.debounceTimer);
|
|
338
358
|
}
|
|
359
|
+
const keyword = this.searchInput?.value.trim() ?? "";
|
|
360
|
+
if (keyword.length < MIN_AUTOCOMPLETE_LENGTH) {
|
|
361
|
+
this.debounceTimer = null;
|
|
362
|
+
this.abortController?.abort();
|
|
363
|
+
this.abortController = null;
|
|
364
|
+
this.closeResults();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
339
367
|
this.debounceTimer = window.setTimeout(() => {
|
|
340
368
|
this.search();
|
|
341
369
|
}, 300);
|
|
@@ -361,6 +389,10 @@ var KoreanAddressFinder = class {
|
|
|
361
389
|
} catch (error) {
|
|
362
390
|
if (this.abortController !== controller) return;
|
|
363
391
|
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
392
|
+
if (error instanceof JusoApiError && error.isInputError) {
|
|
393
|
+
this.showHint(error.userMessage);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
364
396
|
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.";
|
|
365
397
|
this.showError(message);
|
|
366
398
|
}
|
|
@@ -469,6 +501,25 @@ var KoreanAddressFinder = class {
|
|
|
469
501
|
this.setActiveIndex(-1);
|
|
470
502
|
}
|
|
471
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* 검색어 안내 표시 (두 글자 이상 등)
|
|
506
|
+
*
|
|
507
|
+
* 오류가 아니라 고쳐 쓰면 되는 안내라서, 빨간 본문 없이 머리줄 한 줄로만
|
|
508
|
+
* 보여준다. 머리줄은 aria-live라 스크린리더에도 그대로 읽힌다.
|
|
509
|
+
*/
|
|
510
|
+
showHint(message) {
|
|
511
|
+
const resultsList = this.container?.querySelector(".kaf-results-list");
|
|
512
|
+
const resultsMessage = this.container?.querySelector(".kaf-results-message");
|
|
513
|
+
const resultsCount = this.container?.querySelector(".kaf-results-count");
|
|
514
|
+
if (!resultsList || !resultsMessage || !resultsCount) return;
|
|
515
|
+
this.results = [];
|
|
516
|
+
resultsList.innerHTML = "";
|
|
517
|
+
resultsMessage.innerHTML = "";
|
|
518
|
+
this.openResults();
|
|
519
|
+
resultsCount.textContent = "";
|
|
520
|
+
resultsCount.textContent = message;
|
|
521
|
+
this.setActiveIndex(-1);
|
|
522
|
+
}
|
|
472
523
|
/**
|
|
473
524
|
* 에러 표시
|
|
474
525
|
*/
|