@cp949/japanpost-react 1.0.0 → 1.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/README.ko.md +309 -0
- package/README.md +148 -334
- package/dist/components/AddressSearchInput.d.ts +1 -0
- package/dist/components/AddressSearchInput.d.ts.map +1 -1
- package/dist/components/PostalCodeInput.d.ts +1 -0
- package/dist/components/PostalCodeInput.d.ts.map +1 -1
- package/dist/core/errors.d.ts +3 -2
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/normalizers.d.ts +1 -1
- package/dist/core/normalizers.d.ts.map +1 -1
- package/dist/core/types.d.ts +79 -20
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +11 -9
- package/dist/index.umd.cjs +1 -1
- package/dist/react/toJapanAddressError.d.ts +2 -1
- package/dist/react/toJapanAddressError.d.ts.map +1 -1
- package/dist/react/useJapanAddress.d.ts +2 -1
- package/dist/react/useJapanAddress.d.ts.map +1 -1
- package/dist/react/useJapanAddressSearch.d.ts.map +1 -1
- package/dist/react/useJapanPostalCode.d.ts.map +1 -1
- package/dist/react/useLatestRequestState.d.ts +5 -0
- package/dist/react/useLatestRequestState.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
<!-- This file is generated by `pnpm readme:package`. Edit docs/README.en.md and docs/README.ko.md instead. -->
|
|
2
|
-
|
|
3
1
|
# @cp949/japanpost-react
|
|
4
2
|
|
|
5
|
-
[
|
|
3
|
+
[한국어 README](./README.ko.md)
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## English
|
|
5
|
+
<!-- This file is generated by `pnpm readme:package`. Edit docs/README.en.md and docs/README.ko.md instead. -->
|
|
10
6
|
|
|
11
7
|
React + TypeScript hooks and headless inputs for Japan postal-code and address
|
|
12
8
|
lookup.
|
|
@@ -27,83 +23,112 @@ pnpm add @cp949/japanpost-react
|
|
|
27
23
|
|
|
28
24
|
```tsx
|
|
29
25
|
import { useJapanPostalCode } from "@cp949/japanpost-react";
|
|
30
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
JapanAddressDataSource,
|
|
28
|
+
JapanAddressRequestOptions,
|
|
29
|
+
JapanAddress,
|
|
30
|
+
Page,
|
|
31
|
+
} from "@cp949/japanpost-react";
|
|
31
32
|
import { createJapanAddressError } from "@cp949/japanpost-react";
|
|
32
33
|
|
|
33
34
|
// The only supported integration model is a real server-backed flow.
|
|
34
35
|
// Point the data source at your own backend API.
|
|
36
|
+
// On beta-compatible backends, blank addresszip searches and postal-code misses
|
|
37
|
+
// may return HTTP 200 with an empty page. Keep those as successful Page results.
|
|
38
|
+
// The status mapping below applies only to non-OK responses.
|
|
39
|
+
function isAbortError(error: unknown): boolean {
|
|
40
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveErrorCode(path: string, status: number) {
|
|
44
|
+
if (status === 404) {
|
|
45
|
+
return "not_found";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (status === 504) {
|
|
49
|
+
return "timeout";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (status === 400) {
|
|
53
|
+
return path === "/q/japanpost/searchcode"
|
|
54
|
+
? "invalid_postal_code"
|
|
55
|
+
: "invalid_query";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return "data_source_error";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readPage(
|
|
62
|
+
path: string,
|
|
63
|
+
request: unknown,
|
|
64
|
+
options?: JapanAddressRequestOptions,
|
|
65
|
+
): Promise<Page<JapanAddress>> {
|
|
66
|
+
let res: Response;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
res = await fetch(path, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"content-type": "application/json",
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(request),
|
|
75
|
+
signal: options?.signal,
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw createJapanAddressError(
|
|
79
|
+
isAbortError(error) ? "timeout" : "network_error",
|
|
80
|
+
isAbortError(error) ? "Request timed out" : "Network request failed",
|
|
81
|
+
{
|
|
82
|
+
cause: error,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const message = `Request failed with status ${res.status}`;
|
|
89
|
+
|
|
90
|
+
throw createJapanAddressError(resolveErrorCode(path, res.status), message, {
|
|
91
|
+
status: res.status,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let payload: unknown;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
payload = await res.json();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw createJapanAddressError(
|
|
101
|
+
"bad_response",
|
|
102
|
+
"Response payload was not valid JSON",
|
|
103
|
+
{
|
|
104
|
+
cause: error,
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
typeof payload !== "object" ||
|
|
111
|
+
payload === null ||
|
|
112
|
+
!Array.isArray((payload as { elements?: unknown }).elements) ||
|
|
113
|
+
typeof (payload as { totalElements?: unknown }).totalElements !== "number" ||
|
|
114
|
+
typeof (payload as { pageNumber?: unknown }).pageNumber !== "number" ||
|
|
115
|
+
typeof (payload as { rowsPerPage?: unknown }).rowsPerPage !== "number"
|
|
116
|
+
) {
|
|
117
|
+
throw createJapanAddressError(
|
|
118
|
+
"bad_response",
|
|
119
|
+
"Response payload must include a valid page payload",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return payload as Page<JapanAddress>;
|
|
124
|
+
}
|
|
125
|
+
|
|
35
126
|
const dataSource: JapanAddressDataSource = {
|
|
36
|
-
async lookupPostalCode(
|
|
37
|
-
|
|
38
|
-
if (!res.ok) {
|
|
39
|
-
const message = `Postal code lookup failed with status ${res.status}`;
|
|
40
|
-
|
|
41
|
-
if (res.status === 400) {
|
|
42
|
-
throw createJapanAddressError("invalid_postal_code", message, {
|
|
43
|
-
status: res.status,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (res.status === 404) {
|
|
48
|
-
throw createJapanAddressError("not_found", message, {
|
|
49
|
-
status: res.status,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (res.status === 504) {
|
|
54
|
-
throw createJapanAddressError("timeout", message, {
|
|
55
|
-
status: res.status,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
throw createJapanAddressError("data_source_error", message, {
|
|
60
|
-
status: res.status,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
const payload = await res.json();
|
|
64
|
-
if (!Array.isArray(payload.addresses)) {
|
|
65
|
-
throw createJapanAddressError(
|
|
66
|
-
"bad_response",
|
|
67
|
-
"Postal code lookup returned an invalid payload",
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
return payload.addresses;
|
|
127
|
+
async lookupPostalCode(request, options) {
|
|
128
|
+
return readPage(`/q/japanpost/searchcode`, request, options);
|
|
71
129
|
},
|
|
72
|
-
async searchAddress(
|
|
73
|
-
|
|
74
|
-
if (!res.ok) {
|
|
75
|
-
const message = `Address search failed with status ${res.status}`;
|
|
76
|
-
|
|
77
|
-
if (res.status === 400) {
|
|
78
|
-
throw createJapanAddressError("invalid_query", message, {
|
|
79
|
-
status: res.status,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (res.status === 404) {
|
|
84
|
-
throw createJapanAddressError("not_found", message, {
|
|
85
|
-
status: res.status,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (res.status === 504) {
|
|
90
|
-
throw createJapanAddressError("timeout", message, {
|
|
91
|
-
status: res.status,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
throw createJapanAddressError("data_source_error", message, {
|
|
96
|
-
status: res.status,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
const payload = await res.json();
|
|
100
|
-
if (!Array.isArray(payload.addresses)) {
|
|
101
|
-
throw createJapanAddressError(
|
|
102
|
-
"bad_response",
|
|
103
|
-
"Address search returned an invalid payload",
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
return payload.addresses;
|
|
130
|
+
async searchAddress(request, options) {
|
|
131
|
+
return readPage(`/q/japanpost/addresszip`, request, options);
|
|
107
132
|
},
|
|
108
133
|
};
|
|
109
134
|
|
|
@@ -119,7 +144,8 @@ export function PostalForm() {
|
|
|
119
144
|
{error.code}: {error.message}
|
|
120
145
|
</p>
|
|
121
146
|
)}
|
|
122
|
-
{data?.
|
|
147
|
+
<p>Total results: {data?.totalElements ?? 0}</p>
|
|
148
|
+
{data?.elements.map((addr) => (
|
|
123
149
|
<p key={addr.postalCode + addr.address}>{addr.address}</p>
|
|
124
150
|
))}
|
|
125
151
|
</div>
|
|
@@ -127,6 +153,12 @@ export function PostalForm() {
|
|
|
127
153
|
}
|
|
128
154
|
```
|
|
129
155
|
|
|
156
|
+
The sample `resolveErrorCode()` helper only classifies non-OK responses. In the
|
|
157
|
+
current beta-compatible contract, blank address-search requests and postal-code
|
|
158
|
+
misses may still succeed with `200` plus an empty page, while `404 -> not_found`
|
|
159
|
+
remains a backend-specific choice for servers that intentionally surface misses
|
|
160
|
+
as errors.
|
|
161
|
+
|
|
130
162
|
## Exports
|
|
131
163
|
|
|
132
164
|
- `normalizeJapanPostalCode`
|
|
@@ -139,7 +171,8 @@ export function PostalForm() {
|
|
|
139
171
|
- `useJapanAddress`
|
|
140
172
|
- `PostalCodeInput`
|
|
141
173
|
- `AddressSearchInput`
|
|
142
|
-
- Public types including `JapanAddress
|
|
174
|
+
- Public types including `JapanAddress`, `JapanAddressDataSource`,
|
|
175
|
+
`JapanPostSearchcodeRequest`, `JapanPostAddresszipRequest`, and `Page`
|
|
143
176
|
- Request options type: `JapanAddressRequestOptions`
|
|
144
177
|
|
|
145
178
|
## Utility Notes
|
|
@@ -152,7 +185,8 @@ without inserting a hyphen.
|
|
|
152
185
|
|
|
153
186
|
### useJapanPostalCode
|
|
154
187
|
|
|
155
|
-
Looks up addresses by postal code.
|
|
188
|
+
Looks up addresses by postal code. The hook accepts `3-7` digits and uses
|
|
189
|
+
prefix search when the input has `3-6` digits.
|
|
156
190
|
|
|
157
191
|
```tsx
|
|
158
192
|
const { loading, data, error, search, reset } = useJapanPostalCode({
|
|
@@ -171,6 +205,11 @@ const { loading, data, error, search, reset } = useJapanAddressSearch({
|
|
|
171
205
|
});
|
|
172
206
|
```
|
|
173
207
|
|
|
208
|
+
The hook still performs client-side pre-validation for blank queries and
|
|
209
|
+
returns `invalid_query` before sending a request. That validation is a UX
|
|
210
|
+
helper only and does not replace server-side validation or server-side
|
|
211
|
+
contract handling.
|
|
212
|
+
|
|
174
213
|
### useJapanAddress
|
|
175
214
|
|
|
176
215
|
Combines postal-code lookup and keyword search into one hook.
|
|
@@ -182,10 +221,28 @@ const { loading, data, error, searchByPostalCode, searchByKeyword, reset } =
|
|
|
182
221
|
|
|
183
222
|
All hooks require `dataSource` at runtime.
|
|
184
223
|
|
|
224
|
+
The hook public APIs stay string-based:
|
|
225
|
+
|
|
226
|
+
- `useJapanPostalCode().search(value: string)`
|
|
227
|
+
- `useJapanAddressSearch().search(query: string)`
|
|
228
|
+
- `useJapanAddress().searchByPostalCode(value: string)`
|
|
229
|
+
- `useJapanAddress().searchByKeyword(query: string)`
|
|
230
|
+
|
|
231
|
+
Internally, the hooks build request objects before calling the data source:
|
|
232
|
+
|
|
233
|
+
- postal-code lookup: `{ value, pageNumber: 0, rowsPerPage: 100 }`
|
|
234
|
+
- address search: `{ freeword, pageNumber: 0, rowsPerPage: 100 }`
|
|
235
|
+
|
|
236
|
+
Optional request flags such as `includeCityDetails` and
|
|
237
|
+
`includePrefectureDetails` are omitted unless your own data source
|
|
238
|
+
implementation sets them explicitly.
|
|
239
|
+
|
|
185
240
|
## Error Handling Notes
|
|
186
241
|
|
|
187
|
-
`JapanAddressDataSource` should return `JapanAddress
|
|
188
|
-
methods.
|
|
242
|
+
`JapanAddressDataSource` should return `Page<JapanAddress>` directly from both
|
|
243
|
+
methods. Hooks preserve that page payload as-is, so consumers can read
|
|
244
|
+
`data.elements`, `data.totalElements`, `data.pageNumber`, and
|
|
245
|
+
`data.rowsPerPage` directly.
|
|
189
246
|
|
|
190
247
|
Both methods may also receive an optional second argument:
|
|
191
248
|
|
|
@@ -203,16 +260,20 @@ Recommended error-code mapping:
|
|
|
203
260
|
| Situation | Recommended code |
|
|
204
261
|
| --- | --- |
|
|
205
262
|
| Invalid postal code input | `invalid_postal_code` |
|
|
206
|
-
| Blank keyword input | `invalid_query` |
|
|
263
|
+
| Blank keyword input in hook-side pre-validation | `invalid_query` |
|
|
207
264
|
| Network failure | `network_error` |
|
|
208
265
|
| Request aborted / timeout | `timeout` |
|
|
209
|
-
| No matching addresses | `not_found` |
|
|
266
|
+
| No matching addresses on backends that surface misses as errors | `not_found` |
|
|
210
267
|
| Malformed success payload | `bad_response` |
|
|
211
268
|
| Other backend failures | `data_source_error` |
|
|
212
269
|
|
|
213
|
-
In this repository's reference demo flow, the sample `dataSource`
|
|
214
|
-
|
|
215
|
-
|
|
270
|
+
In this repository's reference demo flow, the sample `dataSource` classifies
|
|
271
|
+
failed requests by HTTP status code only. Current beta-compatible flows may
|
|
272
|
+
return `200` with an empty page for both blank `addresszip` requests and
|
|
273
|
+
postal-code misses, and those should stay successful page results. Other
|
|
274
|
+
`400` responses can still map to `invalid_query`, `404` remains useful for
|
|
275
|
+
backends that intentionally surface misses as errors, and `504` maps to
|
|
276
|
+
`timeout`.
|
|
216
277
|
|
|
217
278
|
## Headless Components
|
|
218
279
|
|
|
@@ -251,250 +312,3 @@ the upstream lookup request timed out. Both cases still map cleanly to the
|
|
|
251
312
|
Use your server-side API from the `dataSource` implementation, and keep token
|
|
252
313
|
exchange plus upstream signing on the server. React hooks and UI components
|
|
253
314
|
should stay in client components.
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## 한국어
|
|
258
|
-
|
|
259
|
-
React + TypeScript 기반의 일본 우편번호/주소 검색 훅과 headless 입력
|
|
260
|
-
컴포넌트 라이브러리입니다.
|
|
261
|
-
|
|
262
|
-
이 문서는 배포 패키지 사용 가이드입니다. `pnpm demo:full` 같은 저장소 수준 보조
|
|
263
|
-
스크립트는 현재 Linux/WSL 계열 셸 환경을 전제로 하며, 자세한 내용은 루트 README에서 다룹니다.
|
|
264
|
-
|
|
265
|
-
## 설치
|
|
266
|
-
|
|
267
|
-
```bash
|
|
268
|
-
pnpm add @cp949/japanpost-react
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
- 지원 React 버전: React 18, React 19
|
|
272
|
-
|
|
273
|
-
## 빠른 시작
|
|
274
|
-
|
|
275
|
-
```tsx
|
|
276
|
-
import { useJapanPostalCode } from "@cp949/japanpost-react";
|
|
277
|
-
import type { JapanAddressDataSource } from "@cp949/japanpost-react";
|
|
278
|
-
import { createJapanAddressError } from "@cp949/japanpost-react";
|
|
279
|
-
|
|
280
|
-
// 현재 지원 방식은 실제 서버 연동뿐입니다.
|
|
281
|
-
// 앱의 백엔드 API 경로에 맞게 dataSource를 연결하세요.
|
|
282
|
-
const dataSource: JapanAddressDataSource = {
|
|
283
|
-
async lookupPostalCode(postalCode) {
|
|
284
|
-
const res = await fetch(`/searchcode/${postalCode}`);
|
|
285
|
-
if (!res.ok) {
|
|
286
|
-
const message = `Postal code lookup failed with status ${res.status}`;
|
|
287
|
-
|
|
288
|
-
if (res.status === 400) {
|
|
289
|
-
throw createJapanAddressError("invalid_postal_code", message, {
|
|
290
|
-
status: res.status,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (res.status === 404) {
|
|
295
|
-
throw createJapanAddressError("not_found", message, {
|
|
296
|
-
status: res.status,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (res.status === 504) {
|
|
301
|
-
throw createJapanAddressError("timeout", message, {
|
|
302
|
-
status: res.status,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
throw createJapanAddressError("data_source_error", message, {
|
|
307
|
-
status: res.status,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
const payload = await res.json();
|
|
311
|
-
if (!Array.isArray(payload.addresses)) {
|
|
312
|
-
throw createJapanAddressError(
|
|
313
|
-
"bad_response",
|
|
314
|
-
"Postal code lookup returned an invalid payload",
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
return payload.addresses;
|
|
318
|
-
},
|
|
319
|
-
async searchAddress(query) {
|
|
320
|
-
const res = await fetch(`/addresszip?q=${encodeURIComponent(query)}`);
|
|
321
|
-
if (!res.ok) {
|
|
322
|
-
const message = `Address search failed with status ${res.status}`;
|
|
323
|
-
|
|
324
|
-
if (res.status === 400) {
|
|
325
|
-
throw createJapanAddressError("invalid_query", message, {
|
|
326
|
-
status: res.status,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (res.status === 404) {
|
|
331
|
-
throw createJapanAddressError("not_found", message, {
|
|
332
|
-
status: res.status,
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (res.status === 504) {
|
|
337
|
-
throw createJapanAddressError("timeout", message, {
|
|
338
|
-
status: res.status,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
throw createJapanAddressError("data_source_error", message, {
|
|
343
|
-
status: res.status,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
const payload = await res.json();
|
|
347
|
-
if (!Array.isArray(payload.addresses)) {
|
|
348
|
-
throw createJapanAddressError(
|
|
349
|
-
"bad_response",
|
|
350
|
-
"Address search returned an invalid payload",
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
return payload.addresses;
|
|
354
|
-
},
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
export function PostalForm() {
|
|
358
|
-
const { loading, data, error, search } = useJapanPostalCode({ dataSource });
|
|
359
|
-
|
|
360
|
-
return (
|
|
361
|
-
<div>
|
|
362
|
-
<button onClick={() => void search("100-0001")}>조회</button>
|
|
363
|
-
{loading && <p>조회 중...</p>}
|
|
364
|
-
{error && (
|
|
365
|
-
<p>
|
|
366
|
-
{error.code}: {error.message}
|
|
367
|
-
</p>
|
|
368
|
-
)}
|
|
369
|
-
{data?.addresses.map((addr) => (
|
|
370
|
-
<p key={addr.postalCode + addr.address}>{addr.address}</p>
|
|
371
|
-
))}
|
|
372
|
-
</div>
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
## Exports
|
|
378
|
-
|
|
379
|
-
- `normalizeJapanPostalCode`
|
|
380
|
-
- `formatJapanPostalCode`
|
|
381
|
-
- `normalizeJapanPostAddressRecord`
|
|
382
|
-
- `isValidJapanPostalCode`
|
|
383
|
-
- `createJapanAddressError`
|
|
384
|
-
- `useJapanPostalCode`
|
|
385
|
-
- `useJapanAddressSearch`
|
|
386
|
-
- `useJapanAddress`
|
|
387
|
-
- `PostalCodeInput`
|
|
388
|
-
- `AddressSearchInput`
|
|
389
|
-
- `JapanAddress`, `JapanAddressDataSource`를 포함한 공개 타입
|
|
390
|
-
- 요청 옵션 타입: `JapanAddressRequestOptions`
|
|
391
|
-
|
|
392
|
-
## 유틸리티 메모
|
|
393
|
-
|
|
394
|
-
`formatJapanPostalCode()`는 정규화된 값이 정확히 7자리일 때만 하이픈을 넣습니다.
|
|
395
|
-
그 외 길이에서는 하이픈을 추가하지 않고 숫자만 남긴 값을 그대로 반환합니다.
|
|
396
|
-
|
|
397
|
-
## Hooks
|
|
398
|
-
|
|
399
|
-
### useJapanPostalCode
|
|
400
|
-
|
|
401
|
-
우편번호로 주소를 조회합니다.
|
|
402
|
-
|
|
403
|
-
```tsx
|
|
404
|
-
const { loading, data, error, search, reset } = useJapanPostalCode({
|
|
405
|
-
dataSource,
|
|
406
|
-
});
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### useJapanAddressSearch
|
|
410
|
-
|
|
411
|
-
자유 형식 키워드로 주소를 검색하며 `debounceMs`를 지원합니다.
|
|
412
|
-
|
|
413
|
-
```tsx
|
|
414
|
-
const { loading, data, error, search, reset } = useJapanAddressSearch({
|
|
415
|
-
dataSource,
|
|
416
|
-
debounceMs: 300,
|
|
417
|
-
});
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
### useJapanAddress
|
|
421
|
-
|
|
422
|
-
우편번호 조회와 키워드 검색을 하나의 훅으로 합칩니다.
|
|
423
|
-
|
|
424
|
-
```tsx
|
|
425
|
-
const { loading, data, error, searchByPostalCode, searchByKeyword, reset } =
|
|
426
|
-
useJapanAddress({ dataSource, debounceMs: 300 });
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
모든 훅은 런타임에서 `dataSource`가 필요합니다.
|
|
430
|
-
|
|
431
|
-
## 에러 처리 메모
|
|
432
|
-
|
|
433
|
-
`JapanAddressDataSource`의 두 메서드는 모두 `JapanAddress[]`를 직접
|
|
434
|
-
반환해야 합니다. 각 훅은 그 배열을 받아 `{ postalCode, addresses }`,
|
|
435
|
-
`{ query, addresses }` 형태의 결과 객체를 조합합니다.
|
|
436
|
-
|
|
437
|
-
두 메서드는 선택적인 두 번째 인자도 받을 수 있습니다.
|
|
438
|
-
|
|
439
|
-
```ts
|
|
440
|
-
type JapanAddressRequestOptions = {
|
|
441
|
-
signal?: AbortSignal;
|
|
442
|
-
};
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
훅은 superseded 요청, `reset()`, unmount 정리 상황에서 이전 요청을 취소할 수
|
|
446
|
-
있도록 `signal`을 전달합니다. 백엔드 레이어가 abort를 지원하면 그대로 활용할 수
|
|
447
|
-
있습니다.
|
|
448
|
-
|
|
449
|
-
권장 에러 코드 매핑:
|
|
450
|
-
|
|
451
|
-
| 상황 | 권장 코드 |
|
|
452
|
-
| --- | --- |
|
|
453
|
-
| 잘못된 우편번호 입력 | `invalid_postal_code` |
|
|
454
|
-
| 빈 주소 검색어 | `invalid_query` |
|
|
455
|
-
| 네트워크 실패 | `network_error` |
|
|
456
|
-
| 요청 중단 / 타임아웃 | `timeout` |
|
|
457
|
-
| 검색 결과 없음 | `not_found` |
|
|
458
|
-
| 성공 응답 shape 이상 | `bad_response` |
|
|
459
|
-
| 그 외 백엔드 오류 | `data_source_error` |
|
|
460
|
-
|
|
461
|
-
이 저장소의 참고 demo 흐름에서는 예시 `dataSource`가 `400 /searchcode/...`를
|
|
462
|
-
`invalid_postal_code`, `400 /addresszip?...`를 `invalid_query`, `404`를
|
|
463
|
-
`not_found`, `504`를 `timeout`으로 매핑합니다.
|
|
464
|
-
|
|
465
|
-
## Headless 컴포넌트
|
|
466
|
-
|
|
467
|
-
`PostalCodeInput`, `AddressSearchInput`은 스타일 없이 동작과 DOM 구조만
|
|
468
|
-
제공하므로, 앱의 디자인 시스템에 맞게 직접 꾸밀 수 있습니다.
|
|
469
|
-
|
|
470
|
-
두 컴포넌트는 네이티브 props 전달도 지원합니다.
|
|
471
|
-
|
|
472
|
-
- `inputProps`: 실제 `<input />`에 전달
|
|
473
|
-
- `buttonProps`: 실제 `<button />`에 전달
|
|
474
|
-
|
|
475
|
-
따라서 `id`, `name`, `placeholder`, `aria-*`, `autoComplete`, `className`,
|
|
476
|
-
폼 연동용 속성을 직접 넘길 수 있습니다. `PostalCodeInput`은 별도 override가
|
|
477
|
-
없으면 기본적으로 `inputMode="numeric"`를 사용합니다.
|
|
478
|
-
|
|
479
|
-
## Data Source와 서버 연동
|
|
480
|
-
|
|
481
|
-
이 패키지는 자체 백엔드 서버와 함께 사용하는 것을 권장합니다. Japan Post
|
|
482
|
-
공식 연동은 토큰 기반 인증을 사용하므로, 브라우저에서 업스트림 자격증명을
|
|
483
|
-
직접 보관하면 안 됩니다. 현재 지원 방식은 실제 서버 연동뿐입니다.
|
|
484
|
-
|
|
485
|
-
이 저장소의 `apps/minimal-api`는 로컬 기준 참고 서버 구현입니다. Japan Post
|
|
486
|
-
API ver 2.0을 감싸며, 로컬 개발과 통합 확인 용도로 쓰는 구성을 목표로
|
|
487
|
-
합니다. demo의 `/minimal-api` 경로는 개발 편의를 위한 로컬 경로 연결입니다.
|
|
488
|
-
업스트림 payload에 구조화된 주소 필드와 원본 전체 주소 문자열인 `address`가 함께
|
|
489
|
-
있더라도, 참고 서버는 둘을 그대로 이어붙이지 않고 중복 없는 표시 주소를
|
|
490
|
-
우선 사용합니다.
|
|
491
|
-
|
|
492
|
-
timeout 메시지는 토큰 발급 단계와 실제 조회 단계 중 어느 쪽에서 timeout이
|
|
493
|
-
발생했는지에 따라 달라질 수 있지만, 두 경우 모두 `timeout` 코드로 다루면
|
|
494
|
-
됩니다.
|
|
495
|
-
|
|
496
|
-
## SSR
|
|
497
|
-
|
|
498
|
-
`dataSource` 구현에서는 서버 측 API를 사용하고, 토큰 교환과 업스트림 서명은
|
|
499
|
-
서버에서만 처리하세요. React 훅과 UI 컴포넌트는 클라이언트 컴포넌트에서
|
|
500
|
-
사용하는 것이 안전합니다.
|
|
@@ -2,6 +2,7 @@ import { AddressSearchInputProps } from '../core/types';
|
|
|
2
2
|
/**
|
|
3
3
|
* 스타일 의존성이 없는 최소한의 주소 키워드 검색 입력 컴포넌트.
|
|
4
4
|
* value를 전달하면 제어 모드, 전달하지 않으면 비제어 모드로 동작한다.
|
|
5
|
+
* 검색 시 trim 처리를 내부에서 수행해 공백만 다른 입력이 별도 쿼리로 번지지 않게 한다.
|
|
5
6
|
*/
|
|
6
7
|
export declare function AddressSearchInput({ defaultValue, value, disabled, label, buttonLabel, inputProps, buttonProps, onChange, onSearch, }: AddressSearchInputProps): import("react/jsx-runtime").JSX.Element;
|
|
7
8
|
//# sourceMappingURL=AddressSearchInput.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddressSearchInput.d.ts","sourceRoot":"","sources":["../../src/components/AddressSearchInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D
|
|
1
|
+
{"version":3,"file":"AddressSearchInput.d.ts","sourceRoot":"","sources":["../../src/components/AddressSearchInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,YAAiB,EACjB,KAAK,EACL,QAAQ,EACR,KAAyB,EACzB,WAAsB,EACtB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,QAAQ,GACT,EAAE,uBAAuB,2CA4CzB"}
|
|
@@ -2,6 +2,7 @@ import { PostalCodeInputProps } from '../core/types';
|
|
|
2
2
|
/**
|
|
3
3
|
* 스타일 의존성이 없는 최소한의 우편번호 입력 컴포넌트.
|
|
4
4
|
* value를 전달하면 제어 모드, 전달하지 않으면 비제어 모드로 동작한다.
|
|
5
|
+
* 제출 시에는 표시 형식이 아니라 정규화된 숫자 문자열을 콜백에 넘기는 것이 핵심 계약이다.
|
|
5
6
|
*/
|
|
6
7
|
export declare function PostalCodeInput({ defaultValue, value, disabled, label, buttonLabel, inputProps, buttonProps, onChange, onSearch, }: PostalCodeInputProps): import("react/jsx-runtime").JSX.Element;
|
|
7
8
|
//# sourceMappingURL=PostalCodeInput.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostalCodeInput.d.ts","sourceRoot":"","sources":["../../src/components/PostalCodeInput.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE1D
|
|
1
|
+
{"version":3,"file":"PostalCodeInput.d.ts","sourceRoot":"","sources":["../../src/components/PostalCodeInput.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC9B,YAAiB,EACjB,KAAK,EACL,QAAQ,EACR,KAAqB,EACrB,WAAsB,EACtB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,2CA8CtB"}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { JapanAddressError, JapanAddressErrorCode } from './types';
|
|
2
2
|
/**
|
|
3
|
-
* 라이브러리
|
|
4
|
-
*
|
|
3
|
+
* 라이브러리 전반에서 공통으로 쓰는 오류 객체 생성기다.
|
|
4
|
+
* 브라우저 fetch 오류, validation 오류, data source 오류를 모두 같은 표면으로 맞춰
|
|
5
|
+
* 소비자가 code/status만으로 분기할 수 있게 한다.
|
|
5
6
|
*/
|
|
6
7
|
export declare function createJapanAddressError(code: JapanAddressErrorCode, message: string, options?: {
|
|
7
8
|
cause?: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAExE
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAExE;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,qBAAqB,EAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,iBAAiB,CAQnB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { JapanAddress, NormalizedJapanAddressRecord } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* 정규화된 내부 주소 레코드를 라이브러리 공개 JapanAddress 형태로 변환한다.
|
|
4
|
-
* address는
|
|
4
|
+
* address는 표시용 convenience 필드이므로, 구조화된 필드와 같은 순서를 유지해 예측 가능성을 보장한다.
|
|
5
5
|
*/
|
|
6
6
|
export declare function normalizeJapanPostAddressRecord(record: NormalizedJapanAddressRecord): JapanAddress;
|
|
7
7
|
//# sourceMappingURL=normalizers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalizers.d.ts","sourceRoot":"","sources":["../../src/core/normalizers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"normalizers.d.ts","sourceRoot":"","sources":["../../src/core/normalizers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AAe1E;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,4BAA4B,GACnC,YAAY,CAoBd"}
|