slick-address-kr 1.0.0

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.
@@ -0,0 +1,262 @@
1
+ import { AddressApiClient } from './api-client';
2
+ /**
3
+ * 주소 검색 컴포넌트
4
+ * 팝업 없이 인라인으로 주소를 검색하고 선택할 수 있는 UI를 제공합니다.
5
+ */
6
+ export class KoreanAddressFinder {
7
+ constructor(options = {}) {
8
+ this.container = null;
9
+ this.searchInput = null;
10
+ this.resultsContainer = null;
11
+ this.detailInput = null;
12
+ this.isOpen = false;
13
+ this.debounceTimer = null;
14
+ this.apiClient = new AddressApiClient(options.apiKey);
15
+ // 기본 옵션 설정
16
+ this.options = {
17
+ apiKey: options.apiKey || '',
18
+ countPerPage: options.countPerPage || 10,
19
+ currentPage: options.currentPage || 1,
20
+ containerId: options.containerId || 'address-finder-container',
21
+ onSelect: options.onSelect || (() => { }),
22
+ showDetailInput: options.showDetailInput !== false,
23
+ customClass: options.customClass || '',
24
+ autocompleteMode: options.autocompleteMode || false,
25
+ };
26
+ }
27
+ /**
28
+ * 주소 검색 UI 초기화
29
+ */
30
+ init(targetElement) {
31
+ if (targetElement) {
32
+ this.container = targetElement;
33
+ }
34
+ else {
35
+ this.container = document.getElementById(this.options.containerId);
36
+ }
37
+ if (!this.container) {
38
+ throw new Error(`Container element not found: ${this.options.containerId}`);
39
+ }
40
+ this.render();
41
+ this.attachEventListeners();
42
+ }
43
+ /**
44
+ * UI 렌더링
45
+ */
46
+ render() {
47
+ if (!this.container)
48
+ return;
49
+ const className = this.options.customClass ? ` ${this.options.customClass}` : '';
50
+ this.container.innerHTML = `
51
+ <div class="kaf-wrapper${className}">
52
+ <div class="kaf-search-box">
53
+ <input
54
+ type="text"
55
+ class="kaf-search-input"
56
+ placeholder="주소를 입력하세요 (예: 판교역로 235)"
57
+ autocomplete="off"
58
+ />
59
+ <button type="button" class="kaf-search-btn">검색</button>
60
+ </div>
61
+ <div class="kaf-results" style="display: none;">
62
+ <div class="kaf-results-header">
63
+ <span class="kaf-results-count">검색 결과</span>
64
+ <button type="button" class="kaf-close-btn">×</button>
65
+ </div>
66
+ <div class="kaf-results-list"></div>
67
+ </div>
68
+ ${this.options.showDetailInput ? `
69
+ <div class="kaf-detail-box" style="display: none;">
70
+ <input
71
+ type="text"
72
+ class="kaf-detail-input"
73
+ placeholder="상세주소를 입력하세요"
74
+ />
75
+ </div>
76
+ ` : ''}
77
+ <div class="kaf-selected-address" style="display: none;"></div>
78
+ </div>
79
+ `;
80
+ this.searchInput = this.container.querySelector('.kaf-search-input');
81
+ this.resultsContainer = this.container.querySelector('.kaf-results');
82
+ this.detailInput = this.container.querySelector('.kaf-detail-input');
83
+ }
84
+ /**
85
+ * 이벤트 리스너 등록
86
+ */
87
+ attachEventListeners() {
88
+ if (!this.container)
89
+ return;
90
+ // 검색 버튼 클릭
91
+ const searchBtn = this.container.querySelector('.kaf-search-btn');
92
+ searchBtn?.addEventListener('click', () => this.handleSearch());
93
+ // 검색 입력 엔터키
94
+ this.searchInput?.addEventListener('keypress', (e) => {
95
+ if (e.key === 'Enter') {
96
+ this.handleSearch();
97
+ }
98
+ });
99
+ // 자동완성 모드
100
+ if (this.options.autocompleteMode) {
101
+ this.searchInput?.addEventListener('input', () => {
102
+ this.handleAutocomplete();
103
+ });
104
+ }
105
+ // 닫기 버튼
106
+ const closeBtn = this.container.querySelector('.kaf-close-btn');
107
+ closeBtn?.addEventListener('click', () => this.closeResults());
108
+ // 외부 클릭 시 닫기
109
+ document.addEventListener('click', (e) => {
110
+ if (this.isOpen && !this.container?.contains(e.target)) {
111
+ this.closeResults();
112
+ }
113
+ });
114
+ }
115
+ /**
116
+ * 자동완성 처리 (디바운스 적용)
117
+ */
118
+ handleAutocomplete() {
119
+ if (this.debounceTimer) {
120
+ clearTimeout(this.debounceTimer);
121
+ }
122
+ this.debounceTimer = window.setTimeout(() => {
123
+ this.handleSearch();
124
+ }, 300);
125
+ }
126
+ /**
127
+ * 주소 검색 실행
128
+ */
129
+ async handleSearch() {
130
+ const keyword = this.searchInput?.value.trim();
131
+ if (!keyword)
132
+ return;
133
+ try {
134
+ const { results, totalCount } = await this.apiClient.search(keyword, this.options.currentPage, this.options.countPerPage);
135
+ this.displayResults(results, totalCount);
136
+ }
137
+ catch (error) {
138
+ console.error('Search error:', error);
139
+ this.showError('주소 검색 중 오류가 발생했습니다.');
140
+ }
141
+ }
142
+ /**
143
+ * 검색 결과 표시
144
+ */
145
+ displayResults(results, totalCount) {
146
+ const resultsList = this.container?.querySelector('.kaf-results-list');
147
+ const resultsCount = this.container?.querySelector('.kaf-results-count');
148
+ if (!resultsList || !resultsCount)
149
+ return;
150
+ if (results.length === 0) {
151
+ resultsList.innerHTML = '<div class="kaf-no-results">검색 결과가 없습니다.</div>';
152
+ }
153
+ else {
154
+ resultsCount.textContent = `검색 결과 (${totalCount}건)`;
155
+ resultsList.innerHTML = results.map((result, index) => `
156
+ <div class="kaf-result-item" data-index="${index}">
157
+ <div class="kaf-result-main">
158
+ <span class="kaf-result-type">도로명</span>
159
+ <span class="kaf-result-address">${result.roadAddress}</span>
160
+ </div>
161
+ <div class="kaf-result-sub">
162
+ <span class="kaf-result-type">지번</span>
163
+ <span class="kaf-result-address">${result.jibunAddress}</span>
164
+ </div>
165
+ <div class="kaf-result-zipcode">우편번호: ${result.zipCode}</div>
166
+ ${result.buildingName ? `<div class="kaf-result-building">${result.buildingName}</div>` : ''}
167
+ </div>
168
+ `).join('');
169
+ // 결과 항목 클릭 이벤트
170
+ resultsList.querySelectorAll('.kaf-result-item').forEach((item, index) => {
171
+ item.addEventListener('click', () => this.selectAddress(results[index]));
172
+ });
173
+ }
174
+ this.openResults();
175
+ }
176
+ /**
177
+ * 주소 선택 처리
178
+ */
179
+ selectAddress(address) {
180
+ const selectedContainer = this.container?.querySelector('.kaf-selected-address');
181
+ if (selectedContainer) {
182
+ selectedContainer.innerHTML = `
183
+ <div class="kaf-selected-content">
184
+ <div class="kaf-selected-main">
185
+ <strong>도로명:</strong> ${address.roadAddress}
186
+ </div>
187
+ <div class="kaf-selected-sub">
188
+ <strong>우편번호:</strong> ${address.zipCode}
189
+ </div>
190
+ </div>
191
+ `;
192
+ selectedContainer.style.display = 'block';
193
+ }
194
+ // 상세주소 입력 표시
195
+ if (this.options.showDetailInput && this.detailInput) {
196
+ const detailBox = this.container?.querySelector('.kaf-detail-box');
197
+ if (detailBox) {
198
+ detailBox.style.display = 'block';
199
+ this.detailInput.focus();
200
+ }
201
+ }
202
+ this.closeResults();
203
+ // 콜백 실행
204
+ const detailAddress = this.detailInput?.value || '';
205
+ this.options.onSelect({
206
+ ...address,
207
+ detailAddress,
208
+ });
209
+ }
210
+ /**
211
+ * 검색 결과 열기
212
+ */
213
+ openResults() {
214
+ if (this.resultsContainer) {
215
+ this.resultsContainer.style.display = 'block';
216
+ this.isOpen = true;
217
+ }
218
+ }
219
+ /**
220
+ * 검색 결과 닫기
221
+ */
222
+ closeResults() {
223
+ if (this.resultsContainer) {
224
+ this.resultsContainer.style.display = 'none';
225
+ this.isOpen = false;
226
+ }
227
+ }
228
+ /**
229
+ * 에러 표시
230
+ */
231
+ showError(message) {
232
+ const resultsList = this.container?.querySelector('.kaf-results-list');
233
+ if (resultsList) {
234
+ resultsList.innerHTML = `<div class="kaf-error">${message}</div>`;
235
+ this.openResults();
236
+ }
237
+ }
238
+ /**
239
+ * 컴포넌트 제거
240
+ */
241
+ destroy() {
242
+ if (this.container) {
243
+ this.container.innerHTML = '';
244
+ }
245
+ if (this.debounceTimer) {
246
+ clearTimeout(this.debounceTimer);
247
+ }
248
+ }
249
+ /**
250
+ * 선택된 주소 가져오기
251
+ */
252
+ getSelectedAddress() {
253
+ const selectedContainer = this.container?.querySelector('.kaf-selected-address');
254
+ if (!selectedContainer || selectedContainer.innerHTML === '') {
255
+ return null;
256
+ }
257
+ const roadAddress = selectedContainer.querySelector('.kaf-selected-main')?.textContent?.replace('도로명:', '').trim() || '';
258
+ const zipCode = selectedContainer.querySelector('.kaf-selected-sub')?.textContent?.replace('우편번호:', '').trim() || '';
259
+ const detailAddress = this.detailInput?.value || '';
260
+ return { roadAddress, zipCode, detailAddress };
261
+ }
262
+ }
@@ -0,0 +1,25 @@
1
+ import { AddressResult } from './types';
2
+ /**
3
+ * 주소 검색 API 클라이언트
4
+ * 행정안전부 주소 API를 사용하여 주소를 검색합니다.
5
+ */
6
+ export declare class AddressApiClient {
7
+ private apiUrl;
8
+ private confmKey;
9
+ constructor(apiKey?: string);
10
+ /**
11
+ * 주소 검색
12
+ * @param keyword 검색 키워드
13
+ * @param currentPage 현재 페이지 번호
14
+ * @param countPerPage 페이지당 결과 수
15
+ * @returns 주소 검색 결과
16
+ */
17
+ search(keyword: string, currentPage?: number, countPerPage?: number): Promise<{
18
+ results: AddressResult[];
19
+ totalCount: number;
20
+ }>;
21
+ /**
22
+ * 참고항목 생성
23
+ */
24
+ private buildExtraAddress;
25
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 주소 검색 API 클라이언트
3
+ * 행정안전부 주소 API를 사용하여 주소를 검색합니다.
4
+ */
5
+ export class AddressApiClient {
6
+ constructor(apiKey) {
7
+ this.apiUrl = 'https://business.juso.go.kr/addrlink/addrLinkApi.do';
8
+ // API 키가 없는 경우 공개 테스트 키 사용 (실제 서비스에서는 발급받아야 함)
9
+ this.confmKey = apiKey || 'devU01TX0FVVEgyMDI1MDExMDE1MTY0MTExNTM2MjY=';
10
+ }
11
+ /**
12
+ * 주소 검색
13
+ * @param keyword 검색 키워드
14
+ * @param currentPage 현재 페이지 번호
15
+ * @param countPerPage 페이지당 결과 수
16
+ * @returns 주소 검색 결과
17
+ */
18
+ async search(keyword, currentPage = 1, countPerPage = 10) {
19
+ if (!keyword || keyword.trim().length === 0) {
20
+ return { results: [], totalCount: 0 };
21
+ }
22
+ try {
23
+ const params = new URLSearchParams({
24
+ confmKey: this.confmKey,
25
+ currentPage: currentPage.toString(),
26
+ countPerPage: countPerPage.toString(),
27
+ keyword: keyword.trim(),
28
+ resultType: 'json',
29
+ });
30
+ const response = await fetch(`${this.apiUrl}?${params.toString()}`, {
31
+ method: 'GET',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ });
36
+ if (!response.ok) {
37
+ throw new Error(`HTTP error! status: ${response.status}`);
38
+ }
39
+ const data = await response.json();
40
+ // 에러 체크
41
+ if (data.results.common.errorCode !== '0') {
42
+ console.error('API Error:', data.results.common.errorMessage);
43
+ return { results: [], totalCount: 0 };
44
+ }
45
+ // 결과 변환
46
+ const results = data.results.juso.map((item) => ({
47
+ roadAddress: item.roadAddr,
48
+ jibunAddress: item.jibunAddr,
49
+ zipCode: item.zipNo,
50
+ buildingName: item.bdNm || '',
51
+ sido: item.siNm,
52
+ sigungu: item.sggNm,
53
+ bname: item.emdNm,
54
+ extraAddress: this.buildExtraAddress(item),
55
+ }));
56
+ return {
57
+ results,
58
+ totalCount: parseInt(data.results.common.totalCount, 10),
59
+ };
60
+ }
61
+ catch (error) {
62
+ console.error('Address search error:', error);
63
+ throw error;
64
+ }
65
+ }
66
+ /**
67
+ * 참고항목 생성
68
+ */
69
+ buildExtraAddress(item) {
70
+ const extras = [];
71
+ if (item.bdNm) {
72
+ extras.push(item.bdNm);
73
+ }
74
+ if (item.detBdNmList) {
75
+ extras.push(item.detBdNmList);
76
+ }
77
+ return extras.join(', ');
78
+ }
79
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Korean Address Finder
3
+ * 팝업 없는 한국 주소 검색 라이브러리
4
+ */
5
+ export { KoreanAddressFinder } from './address-finder';
6
+ export { AddressApiClient } from './api-client';
7
+ export type { AddressResult, AddressFinderOptions, JusoApiResponse } from './types';
8
+ export { KoreanAddressFinder as default } from './address-finder';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Korean Address Finder
3
+ * 팝업 없는 한국 주소 검색 라이브러리
4
+ */
5
+ export { KoreanAddressFinder } from './address-finder';
6
+ export { AddressApiClient } from './api-client';
7
+ // 기본 export
8
+ export { KoreanAddressFinder as default } from './address-finder';
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Korean Address Finder Styles
3
+ * 팝업 없는 한국 주소 검색 컴포넌트 스타일
4
+ */
5
+
6
+ .kaf-wrapper {
7
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
8
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
9
+ sans-serif;
10
+ -webkit-font-smoothing: antialiased;
11
+ -moz-osx-font-smoothing: grayscale;
12
+ width: 100%;
13
+ max-width: 600px;
14
+ margin: 0 auto;
15
+ position: relative;
16
+ }
17
+
18
+ /* 검색 박스 */
19
+ .kaf-search-box {
20
+ display: flex;
21
+ gap: 8px;
22
+ margin-bottom: 12px;
23
+ }
24
+
25
+ .kaf-search-input {
26
+ flex: 1;
27
+ padding: 12px 16px;
28
+ font-size: 14px;
29
+ border: 2px solid #e0e0e0;
30
+ border-radius: 8px;
31
+ outline: none;
32
+ transition: border-color 0.2s;
33
+ }
34
+
35
+ .kaf-search-input:focus {
36
+ border-color: #4a90e2;
37
+ }
38
+
39
+ .kaf-search-input::placeholder {
40
+ color: #999;
41
+ }
42
+
43
+ .kaf-search-btn {
44
+ padding: 12px 24px;
45
+ font-size: 14px;
46
+ font-weight: 600;
47
+ color: white;
48
+ background-color: #4a90e2;
49
+ border: none;
50
+ border-radius: 8px;
51
+ cursor: pointer;
52
+ transition: background-color 0.2s;
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .kaf-search-btn:hover {
57
+ background-color: #357abd;
58
+ }
59
+
60
+ .kaf-search-btn:active {
61
+ background-color: #2a6299;
62
+ }
63
+
64
+ /* 검색 결과 컨테이너 */
65
+ .kaf-results {
66
+ position: absolute;
67
+ top: 100%;
68
+ left: 0;
69
+ right: 0;
70
+ background: white;
71
+ border: 1px solid #e0e0e0;
72
+ border-radius: 8px;
73
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
74
+ z-index: 1000;
75
+ max-height: 400px;
76
+ overflow: hidden;
77
+ display: flex;
78
+ flex-direction: column;
79
+ }
80
+
81
+ .kaf-results-header {
82
+ display: flex;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ padding: 12px 16px;
86
+ border-bottom: 1px solid #e0e0e0;
87
+ background-color: #f8f9fa;
88
+ }
89
+
90
+ .kaf-results-count {
91
+ font-size: 14px;
92
+ font-weight: 600;
93
+ color: #333;
94
+ }
95
+
96
+ .kaf-close-btn {
97
+ background: none;
98
+ border: none;
99
+ font-size: 24px;
100
+ color: #666;
101
+ cursor: pointer;
102
+ padding: 0;
103
+ width: 28px;
104
+ height: 28px;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ border-radius: 4px;
109
+ transition: background-color 0.2s;
110
+ }
111
+
112
+ .kaf-close-btn:hover {
113
+ background-color: #e0e0e0;
114
+ }
115
+
116
+ .kaf-results-list {
117
+ overflow-y: auto;
118
+ max-height: 340px;
119
+ }
120
+
121
+ /* 검색 결과 항목 */
122
+ .kaf-result-item {
123
+ padding: 16px;
124
+ border-bottom: 1px solid #f0f0f0;
125
+ cursor: pointer;
126
+ transition: background-color 0.2s;
127
+ }
128
+
129
+ .kaf-result-item:hover {
130
+ background-color: #f8f9fa;
131
+ }
132
+
133
+ .kaf-result-item:last-child {
134
+ border-bottom: none;
135
+ }
136
+
137
+ .kaf-result-main,
138
+ .kaf-result-sub {
139
+ display: flex;
140
+ gap: 8px;
141
+ margin-bottom: 4px;
142
+ }
143
+
144
+ .kaf-result-type {
145
+ display: inline-block;
146
+ padding: 2px 8px;
147
+ font-size: 11px;
148
+ font-weight: 600;
149
+ color: white;
150
+ border-radius: 4px;
151
+ flex-shrink: 0;
152
+ }
153
+
154
+ .kaf-result-main .kaf-result-type {
155
+ background-color: #4a90e2;
156
+ }
157
+
158
+ .kaf-result-sub .kaf-result-type {
159
+ background-color: #999;
160
+ }
161
+
162
+ .kaf-result-address {
163
+ font-size: 14px;
164
+ color: #333;
165
+ line-height: 1.5;
166
+ }
167
+
168
+ .kaf-result-zipcode {
169
+ font-size: 12px;
170
+ color: #666;
171
+ margin-top: 4px;
172
+ }
173
+
174
+ .kaf-result-building {
175
+ font-size: 12px;
176
+ color: #4a90e2;
177
+ margin-top: 4px;
178
+ font-weight: 500;
179
+ }
180
+
181
+ /* 결과 없음 / 에러 */
182
+ .kaf-no-results,
183
+ .kaf-error {
184
+ padding: 32px 16px;
185
+ text-align: center;
186
+ color: #999;
187
+ font-size: 14px;
188
+ }
189
+
190
+ .kaf-error {
191
+ color: #e74c3c;
192
+ }
193
+
194
+ /* 상세주소 입력 */
195
+ .kaf-detail-box {
196
+ margin-top: 12px;
197
+ }
198
+
199
+ .kaf-detail-input {
200
+ width: 100%;
201
+ padding: 12px 16px;
202
+ font-size: 14px;
203
+ border: 2px solid #e0e0e0;
204
+ border-radius: 8px;
205
+ outline: none;
206
+ transition: border-color 0.2s;
207
+ box-sizing: border-box;
208
+ }
209
+
210
+ .kaf-detail-input:focus {
211
+ border-color: #4a90e2;
212
+ }
213
+
214
+ /* 선택된 주소 */
215
+ .kaf-selected-address {
216
+ margin-top: 12px;
217
+ padding: 16px;
218
+ background-color: #f0f8ff;
219
+ border: 1px solid #4a90e2;
220
+ border-radius: 8px;
221
+ }
222
+
223
+ .kaf-selected-content {
224
+ font-size: 14px;
225
+ }
226
+
227
+ .kaf-selected-main,
228
+ .kaf-selected-sub {
229
+ margin-bottom: 4px;
230
+ }
231
+
232
+ .kaf-selected-main strong,
233
+ .kaf-selected-sub strong {
234
+ color: #4a90e2;
235
+ margin-right: 8px;
236
+ }
237
+
238
+ /* 스크롤바 스타일 */
239
+ .kaf-results-list::-webkit-scrollbar {
240
+ width: 8px;
241
+ }
242
+
243
+ .kaf-results-list::-webkit-scrollbar-track {
244
+ background: #f1f1f1;
245
+ }
246
+
247
+ .kaf-results-list::-webkit-scrollbar-thumb {
248
+ background: #888;
249
+ border-radius: 4px;
250
+ }
251
+
252
+ .kaf-results-list::-webkit-scrollbar-thumb:hover {
253
+ background: #555;
254
+ }
255
+
256
+ /* 반응형 */
257
+ @media (max-width: 640px) {
258
+ .kaf-wrapper {
259
+ max-width: 100%;
260
+ }
261
+
262
+ .kaf-search-box {
263
+ flex-direction: column;
264
+ }
265
+
266
+ .kaf-search-btn {
267
+ width: 100%;
268
+ }
269
+
270
+ .kaf-results {
271
+ position: fixed;
272
+ top: auto;
273
+ bottom: 0;
274
+ left: 0;
275
+ right: 0;
276
+ border-radius: 16px 16px 0 0;
277
+ max-height: 70vh;
278
+ }
279
+ }