@cp949/japanpost-react 1.0.0 → 1.0.2
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 +349 -0
- package/README.md +249 -419
- package/dist/index.d.ts +1 -11
- package/dist/index.es.js +106 -60
- package/dist/index.umd.cjs +1 -1
- package/dist/{components → src/components}/AddressSearchInput.d.ts +1 -0
- package/dist/src/components/AddressSearchInput.d.ts.map +1 -0
- package/dist/{components → src/components}/PostalCodeInput.d.ts +1 -0
- package/dist/src/components/PostalCodeInput.d.ts.map +1 -0
- package/dist/src/core/errors.d.ts +11 -0
- package/dist/src/core/errors.d.ts.map +1 -0
- package/dist/src/core/formatters.d.ts.map +1 -0
- package/dist/{core → src/core}/normalizers.d.ts +1 -1
- package/dist/src/core/normalizers.d.ts.map +1 -0
- package/dist/src/core/types.d.ts +261 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/validators.d.ts.map +1 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/react/toJapanAddressError.d.ts +8 -0
- package/dist/src/react/toJapanAddressError.d.ts.map +1 -0
- package/dist/{react → src/react}/useJapanAddress.d.ts +2 -1
- package/dist/src/react/useJapanAddress.d.ts.map +1 -0
- package/dist/src/react/useJapanAddressSearch.d.ts.map +1 -0
- package/dist/src/react/useJapanPostalCode.d.ts.map +1 -0
- package/dist/{react → src/react}/useLatestRequestState.d.ts +6 -0
- package/dist/src/react/useLatestRequestState.d.ts.map +1 -0
- package/package.json +5 -5
- package/dist/components/AddressSearchInput.d.ts.map +0 -1
- package/dist/components/PostalCodeInput.d.ts.map +0 -1
- package/dist/core/errors.d.ts +0 -10
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/formatters.d.ts.map +0 -1
- package/dist/core/normalizers.d.ts.map +0 -1
- package/dist/core/types.d.ts +0 -182
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/validators.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/react/toJapanAddressError.d.ts +0 -7
- package/dist/react/toJapanAddressError.d.ts.map +0 -1
- package/dist/react/useJapanAddress.d.ts.map +0 -1
- package/dist/react/useJapanAddressSearch.d.ts.map +0 -1
- package/dist/react/useJapanPostalCode.d.ts.map +0 -1
- package/dist/react/useLatestRequestState.d.ts.map +0 -1
- /package/dist/{core → src/core}/formatters.d.ts +0 -0
- /package/dist/{core → src/core}/validators.d.ts +0 -0
- /package/dist/{react → src/react}/useJapanAddressSearch.d.ts +0 -0
- /package/dist/{react → src/react}/useJapanPostalCode.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
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
|
-
React
|
|
12
|
-
lookup.
|
|
7
|
+
React hooks, headless input components, and utilities for Japan postal-code
|
|
8
|
+
and address lookup.
|
|
13
9
|
|
|
14
|
-
This package
|
|
15
|
-
|
|
16
|
-
and are documented in the root README.
|
|
10
|
+
This package does not call Japan Post directly. You provide a
|
|
11
|
+
`JapanAddressDataSource` that talks to your own backend API.
|
|
17
12
|
|
|
18
13
|
## Install
|
|
19
14
|
|
|
@@ -21,480 +16,315 @@ and are documented in the root README.
|
|
|
21
16
|
pnpm add @cp949/japanpost-react
|
|
22
17
|
```
|
|
23
18
|
|
|
24
|
-
-
|
|
19
|
+
- Peer dependencies: React 18 or React 19
|
|
20
|
+
- Package source in this repository:
|
|
21
|
+
`packages/japanpost-react`
|
|
22
|
+
- Demo app in this repository: `apps/demo`
|
|
23
|
+
|
|
24
|
+
## What The Package Provides
|
|
25
|
+
|
|
26
|
+
- Hooks for postal-code lookup and address search:
|
|
27
|
+
`useJapanPostalCode`, `useJapanAddressSearch`, `useJapanAddress`
|
|
28
|
+
- Headless form components:
|
|
29
|
+
`PostalCodeInput`, `AddressSearchInput`
|
|
30
|
+
- Utilities:
|
|
31
|
+
`normalizeJapanPostalCode`, `formatJapanPostalCode`,
|
|
32
|
+
`isValidJapanPostalCode`, `normalizeJapanPostAddressRecord`,
|
|
33
|
+
`createJapanAddressError`
|
|
34
|
+
- Public types for the request, response, error, and data-source contracts
|
|
25
35
|
|
|
26
36
|
## Quick Start
|
|
27
37
|
|
|
38
|
+
The package expects a `JapanAddressDataSource` with two methods:
|
|
39
|
+
|
|
40
|
+
- `lookupPostalCode(request, options?)`
|
|
41
|
+
- `searchAddress(request, options?)`
|
|
42
|
+
|
|
43
|
+
Both methods return `Promise<Page<JapanAddress>>`.
|
|
44
|
+
|
|
28
45
|
```tsx
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
import {
|
|
47
|
+
PostalCodeInput,
|
|
48
|
+
createJapanAddressError,
|
|
49
|
+
useJapanPostalCode,
|
|
50
|
+
type JapanAddress,
|
|
51
|
+
type JapanAddressDataSource,
|
|
52
|
+
type JapanAddressRequestOptions,
|
|
53
|
+
type Page,
|
|
54
|
+
} from "@cp949/japanpost-react";
|
|
55
|
+
|
|
56
|
+
function isAbortError(error: unknown): boolean {
|
|
57
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isPagePayload(payload: unknown): payload is Page<JapanAddress> {
|
|
61
|
+
return (
|
|
62
|
+
typeof payload === "object" &&
|
|
63
|
+
payload !== null &&
|
|
64
|
+
Array.isArray((payload as { elements?: unknown }).elements) &&
|
|
65
|
+
typeof (payload as { totalElements?: unknown }).totalElements === "number" &&
|
|
66
|
+
typeof (payload as { pageNumber?: unknown }).pageNumber === "number" &&
|
|
67
|
+
typeof (payload as { rowsPerPage?: unknown }).rowsPerPage === "number"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readPage(
|
|
72
|
+
path: string,
|
|
73
|
+
request: unknown,
|
|
74
|
+
options?: JapanAddressRequestOptions,
|
|
75
|
+
): Promise<Page<JapanAddress>> {
|
|
76
|
+
let response: Response;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
response = await fetch(path, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
"content-type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(request),
|
|
85
|
+
signal: options?.signal,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw createJapanAddressError(
|
|
89
|
+
isAbortError(error) ? "timeout" : "network_error",
|
|
90
|
+
isAbortError(error) ? "Request timed out" : "Network request failed",
|
|
91
|
+
{ cause: error },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw createJapanAddressError(
|
|
97
|
+
"data_source_error",
|
|
98
|
+
`Request failed with status ${response.status}`,
|
|
99
|
+
{ status: response.status },
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let payload: unknown;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
payload = await response.json();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw createJapanAddressError(
|
|
109
|
+
"bad_response",
|
|
110
|
+
"Response payload was not valid JSON",
|
|
111
|
+
{ cause: error },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isPagePayload(payload)) {
|
|
116
|
+
throw createJapanAddressError(
|
|
117
|
+
"bad_response",
|
|
118
|
+
"Response payload must include a valid page payload",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return payload;
|
|
123
|
+
}
|
|
32
124
|
|
|
33
|
-
// The only supported integration model is a real server-backed flow.
|
|
34
|
-
// Point the data source at your own backend API.
|
|
35
125
|
const dataSource: JapanAddressDataSource = {
|
|
36
|
-
|
|
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;
|
|
126
|
+
lookupPostalCode(request, options) {
|
|
127
|
+
return readPage("/q/japanpost/searchcode", request, options);
|
|
71
128
|
},
|
|
72
|
-
|
|
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;
|
|
129
|
+
searchAddress(request, options) {
|
|
130
|
+
return readPage("/q/japanpost/addresszip", request, options);
|
|
107
131
|
},
|
|
108
132
|
};
|
|
109
133
|
|
|
110
|
-
export function
|
|
134
|
+
export function PostalCodeLookupExample() {
|
|
111
135
|
const { loading, data, error, search } = useJapanPostalCode({ dataSource });
|
|
112
136
|
|
|
113
137
|
return (
|
|
114
138
|
<div>
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
<PostalCodeInput
|
|
140
|
+
buttonLabel="Search"
|
|
141
|
+
label="Postal code"
|
|
142
|
+
onSearch={(postalCode) => {
|
|
143
|
+
void search({ postalCode });
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
{loading ? <p>Loading...</p> : null}
|
|
148
|
+
{error ? <p>{error.message}</p> : null}
|
|
149
|
+
|
|
150
|
+
<ul>
|
|
151
|
+
{(data?.elements ?? []).map((address) => (
|
|
152
|
+
<li key={`${address.postalCode}-${address.address}`}>
|
|
153
|
+
{address.address}
|
|
154
|
+
</li>
|
|
155
|
+
))}
|
|
156
|
+
</ul>
|
|
125
157
|
</div>
|
|
126
158
|
);
|
|
127
159
|
}
|
|
128
160
|
```
|
|
129
161
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
- `formatJapanPostalCode`
|
|
134
|
-
- `normalizeJapanPostAddressRecord`
|
|
135
|
-
- `isValidJapanPostalCode`
|
|
136
|
-
- `createJapanAddressError`
|
|
137
|
-
- `useJapanPostalCode`
|
|
138
|
-
- `useJapanAddressSearch`
|
|
139
|
-
- `useJapanAddress`
|
|
140
|
-
- `PostalCodeInput`
|
|
141
|
-
- `AddressSearchInput`
|
|
142
|
-
- Public types including `JapanAddress` and `JapanAddressDataSource`
|
|
143
|
-
- Request options type: `JapanAddressRequestOptions`
|
|
144
|
-
|
|
145
|
-
## Utility Notes
|
|
146
|
-
|
|
147
|
-
`formatJapanPostalCode()` inserts a hyphen only when the normalized value is
|
|
148
|
-
exactly 7 digits. For any other length, it returns the normalized digits
|
|
149
|
-
without inserting a hyphen.
|
|
150
|
-
|
|
151
|
-
## Hooks
|
|
152
|
-
|
|
153
|
-
### useJapanPostalCode
|
|
154
|
-
|
|
155
|
-
Looks up addresses by postal code.
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
const { loading, data, error, search, reset } = useJapanPostalCode({
|
|
159
|
-
dataSource,
|
|
160
|
-
});
|
|
161
|
-
```
|
|
162
|
+
The example paths above match this repository's reference backend. In your own
|
|
163
|
+
app, the backend routes can be different as long as your `dataSource`
|
|
164
|
+
implementation returns the same public types.
|
|
162
165
|
|
|
163
|
-
|
|
166
|
+
## Core Contract
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```tsx
|
|
168
|
-
const { loading, data, error, search, reset } = useJapanAddressSearch({
|
|
169
|
-
dataSource,
|
|
170
|
-
debounceMs: 300,
|
|
171
|
-
});
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### useJapanAddress
|
|
175
|
-
|
|
176
|
-
Combines postal-code lookup and keyword search into one hook.
|
|
177
|
-
|
|
178
|
-
```tsx
|
|
179
|
-
const { loading, data, error, searchByPostalCode, searchByKeyword, reset } =
|
|
180
|
-
useJapanAddress({ dataSource, debounceMs: 300 });
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
All hooks require `dataSource` at runtime.
|
|
184
|
-
|
|
185
|
-
## Error Handling Notes
|
|
186
|
-
|
|
187
|
-
`JapanAddressDataSource` should return `JapanAddress[]` directly from both
|
|
188
|
-
methods. The hooks wrap those arrays into lookup/search result objects.
|
|
189
|
-
|
|
190
|
-
Both methods may also receive an optional second argument:
|
|
168
|
+
`Page<T>` is the result shape shared by the hooks and the reference backend:
|
|
191
169
|
|
|
192
170
|
```ts
|
|
193
|
-
type
|
|
194
|
-
|
|
171
|
+
type Page<T> = {
|
|
172
|
+
elements: T[];
|
|
173
|
+
totalElements: number;
|
|
174
|
+
pageNumber: number;
|
|
175
|
+
rowsPerPage: number;
|
|
195
176
|
};
|
|
196
177
|
```
|
|
197
178
|
|
|
198
|
-
|
|
199
|
-
`reset()` calls, and unmount cleanup when your backend layer supports aborts.
|
|
200
|
-
|
|
201
|
-
Recommended error-code mapping:
|
|
202
|
-
|
|
203
|
-
| Situation | Recommended code |
|
|
204
|
-
| --- | --- |
|
|
205
|
-
| Invalid postal code input | `invalid_postal_code` |
|
|
206
|
-
| Blank keyword input | `invalid_query` |
|
|
207
|
-
| Network failure | `network_error` |
|
|
208
|
-
| Request aborted / timeout | `timeout` |
|
|
209
|
-
| No matching addresses | `not_found` |
|
|
210
|
-
| Malformed success payload | `bad_response` |
|
|
211
|
-
| Other backend failures | `data_source_error` |
|
|
212
|
-
|
|
213
|
-
In this repository's reference demo flow, the sample `dataSource` maps `400
|
|
214
|
-
/searchcode/...` to `invalid_postal_code`, `400 /addresszip?...` to
|
|
215
|
-
`invalid_query`, `404` to `not_found`, and `504` to `timeout`.
|
|
216
|
-
|
|
217
|
-
## Headless Components
|
|
218
|
-
|
|
219
|
-
`PostalCodeInput` and `AddressSearchInput` provide behavior and DOM structure
|
|
220
|
-
without bundled styles, so you can plug them into your own design system.
|
|
221
|
-
|
|
222
|
-
Both components also support native prop passthrough:
|
|
223
|
-
|
|
224
|
-
- `inputProps`: forwarded to the rendered `<input />`
|
|
225
|
-
- `buttonProps`: forwarded to the rendered `<button />`
|
|
226
|
-
|
|
227
|
-
Use these for `id`, `name`, `placeholder`, `aria-*`, `autoComplete`,
|
|
228
|
-
`className`, and form integration. `PostalCodeInput` defaults to
|
|
229
|
-
`inputMode="numeric"` unless you override it through `inputProps`.
|
|
230
|
-
|
|
231
|
-
## Data Source and Server Integration
|
|
179
|
+
`JapanAddress` is the normalized address shape returned by the package:
|
|
232
180
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
address
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
Timeout messages can differ depending on whether the token exchange timed out or
|
|
246
|
-
the upstream lookup request timed out. Both cases still map cleanly to the
|
|
247
|
-
`timeout` error code.
|
|
248
|
-
|
|
249
|
-
## SSR
|
|
250
|
-
|
|
251
|
-
Use your server-side API from the `dataSource` implementation, and keep token
|
|
252
|
-
exchange plus upstream signing on the server. React hooks and UI components
|
|
253
|
-
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
|
-
},
|
|
181
|
+
```ts
|
|
182
|
+
type JapanAddress = {
|
|
183
|
+
postalCode: string;
|
|
184
|
+
prefecture: string;
|
|
185
|
+
prefectureKana?: string;
|
|
186
|
+
city: string;
|
|
187
|
+
cityKana?: string;
|
|
188
|
+
town: string;
|
|
189
|
+
townKana?: string;
|
|
190
|
+
address: string;
|
|
191
|
+
provider: "japan-post";
|
|
355
192
|
};
|
|
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
193
|
```
|
|
376
194
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
그 외 길이에서는 하이픈을 추가하지 않고 숫자만 남긴 값을 그대로 반환합니다.
|
|
195
|
+
The hooks keep this page payload as-is, so consumers read
|
|
196
|
+
`data?.elements`, `data?.totalElements`, `data?.pageNumber`, and
|
|
197
|
+
`data?.rowsPerPage` directly.
|
|
396
198
|
|
|
397
199
|
## Hooks
|
|
398
200
|
|
|
399
|
-
### useJapanPostalCode
|
|
201
|
+
### `useJapanPostalCode`
|
|
400
202
|
|
|
401
|
-
|
|
203
|
+
- Accepts `string` or `JapanPostalCodeSearchInput`
|
|
204
|
+
- Normalizes the input to digits before calling the data source
|
|
205
|
+
- Allows `3-7` digits, so prefix lookup is possible
|
|
206
|
+
- Builds `{ postalCode, pageNumber: 0, rowsPerPage: 100 }` by default
|
|
207
|
+
- Exposes `loading`, `data`, `error`, `search`, `cancel`, and `reset`
|
|
402
208
|
|
|
403
209
|
```tsx
|
|
404
|
-
const
|
|
405
|
-
|
|
210
|
+
const postalCode = useJapanPostalCode({ dataSource });
|
|
211
|
+
|
|
212
|
+
void postalCode.search("100-0001");
|
|
213
|
+
void postalCode.search({
|
|
214
|
+
postalCode: "1000001",
|
|
215
|
+
pageNumber: 1,
|
|
216
|
+
rowsPerPage: 10,
|
|
217
|
+
includeParenthesesTown: true,
|
|
406
218
|
});
|
|
407
219
|
```
|
|
408
220
|
|
|
409
|
-
### useJapanAddressSearch
|
|
221
|
+
### `useJapanAddressSearch`
|
|
410
222
|
|
|
411
|
-
|
|
223
|
+
- Accepts `string` or `JapanAddressSearchInput`
|
|
224
|
+
- Supports free-form search and structured fields in the same request type
|
|
225
|
+
- Rejects a fully blank query before calling the data source
|
|
226
|
+
- Omits `includeCityDetails` and `includePrefectureDetails` unless you set them
|
|
227
|
+
- Supports `debounceMs`
|
|
228
|
+
- Exposes `loading`, `data`, `error`, `search`, `cancel`, and `reset`
|
|
412
229
|
|
|
413
230
|
```tsx
|
|
414
|
-
const
|
|
231
|
+
const addressSearch = useJapanAddressSearch({
|
|
415
232
|
dataSource,
|
|
416
233
|
debounceMs: 300,
|
|
417
234
|
});
|
|
235
|
+
|
|
236
|
+
void addressSearch.search("千代田");
|
|
237
|
+
void addressSearch.search({
|
|
238
|
+
prefName: "東京都",
|
|
239
|
+
cityName: "千代田区",
|
|
240
|
+
pageNumber: 0,
|
|
241
|
+
rowsPerPage: 10,
|
|
242
|
+
});
|
|
418
243
|
```
|
|
419
244
|
|
|
420
|
-
### useJapanAddress
|
|
245
|
+
### `useJapanAddress`
|
|
421
246
|
|
|
422
|
-
|
|
247
|
+
- Combines postal-code lookup and address search in one hook
|
|
248
|
+
- Reuses the same `dataSource`
|
|
249
|
+
- Exposes `searchByPostalCode`, `searchByAddressQuery`, and `reset`
|
|
250
|
+
- Returns `data` and `error` for the currently active search mode only
|
|
423
251
|
|
|
424
252
|
```tsx
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
모든 훅은 런타임에서 `dataSource`가 필요합니다.
|
|
253
|
+
const address = useJapanAddress({
|
|
254
|
+
dataSource,
|
|
255
|
+
debounceMs: 300,
|
|
256
|
+
});
|
|
430
257
|
|
|
431
|
-
|
|
258
|
+
void address.searchByPostalCode("1000001");
|
|
259
|
+
void address.searchByAddressQuery({
|
|
260
|
+
addressQuery: "千代田",
|
|
261
|
+
pageNumber: 0,
|
|
262
|
+
rowsPerPage: 10,
|
|
263
|
+
});
|
|
264
|
+
```
|
|
432
265
|
|
|
433
|
-
|
|
434
|
-
반환해야 합니다. 각 훅은 그 배열을 받아 `{ postalCode, addresses }`,
|
|
435
|
-
`{ query, addresses }` 형태의 결과 객체를 조합합니다.
|
|
266
|
+
## Headless Components
|
|
436
267
|
|
|
437
|
-
|
|
268
|
+
### `PostalCodeInput`
|
|
438
269
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
```
|
|
270
|
+
- Renders a `<form>` with `<label>`, `<input>`, and `<button>`
|
|
271
|
+
- Supports controlled and uncontrolled usage
|
|
272
|
+
- Calls `onSearch` with a normalized digits-only postal code
|
|
273
|
+
- Defaults `inputMode="numeric"` unless overridden with `inputProps`
|
|
444
274
|
|
|
445
|
-
|
|
446
|
-
있도록 `signal`을 전달합니다. 백엔드 레이어가 abort를 지원하면 그대로 활용할 수
|
|
447
|
-
있습니다.
|
|
275
|
+
### `AddressSearchInput`
|
|
448
276
|
|
|
449
|
-
|
|
277
|
+
- Renders the same minimal form structure
|
|
278
|
+
- Supports controlled and uncontrolled usage
|
|
279
|
+
- Calls `onSearch` with a trimmed query string
|
|
450
280
|
|
|
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` |
|
|
281
|
+
Both components accept:
|
|
460
282
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
`not_found`, `504`를 `timeout`으로 매핑합니다.
|
|
283
|
+
- `inputProps` for the rendered `<input>`
|
|
284
|
+
- `buttonProps` for the rendered `<button>`
|
|
464
285
|
|
|
465
|
-
##
|
|
286
|
+
## Data Source Integration
|
|
466
287
|
|
|
467
|
-
|
|
468
|
-
제공하므로, 앱의 디자인 시스템에 맞게 직접 꾸밀 수 있습니다.
|
|
288
|
+
The package exports types for both sides of the integration:
|
|
469
289
|
|
|
470
|
-
|
|
290
|
+
- `JapanAddressDataSource`
|
|
291
|
+
- `JapanPostSearchcodeRequest`
|
|
292
|
+
- `JapanPostAddresszipRequest`
|
|
293
|
+
- `JapanPostalCodeSearchInput`
|
|
294
|
+
- `JapanAddressSearchInput`
|
|
295
|
+
- `JapanAddressRequestOptions`
|
|
471
296
|
|
|
472
|
-
|
|
473
|
-
- `buttonProps`: 실제 `<button />`에 전달
|
|
297
|
+
The optional second argument to each data-source method is:
|
|
474
298
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
299
|
+
```ts
|
|
300
|
+
type JapanAddressRequestOptions = {
|
|
301
|
+
signal?: AbortSignal;
|
|
302
|
+
};
|
|
303
|
+
```
|
|
478
304
|
|
|
479
|
-
|
|
305
|
+
The hooks pass `signal` so your data source can cancel superseded requests,
|
|
306
|
+
`cancel()` calls, `reset()` calls, and unmount cleanup.
|
|
480
307
|
|
|
481
|
-
|
|
482
|
-
공식 연동은 토큰 기반 인증을 사용하므로, 브라우저에서 업스트림 자격증명을
|
|
483
|
-
직접 보관하면 안 됩니다. 현재 지원 방식은 실제 서버 연동뿐입니다.
|
|
308
|
+
This repository's reference backend uses these routes:
|
|
484
309
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
합니다. demo의 `/minimal-api` 경로는 개발 편의를 위한 로컬 경로 연결입니다.
|
|
488
|
-
업스트림 payload에 구조화된 주소 필드와 원본 전체 주소 문자열인 `address`가 함께
|
|
489
|
-
있더라도, 참고 서버는 둘을 그대로 이어붙이지 않고 중복 없는 표시 주소를
|
|
490
|
-
우선 사용합니다.
|
|
310
|
+
- `POST /q/japanpost/searchcode`
|
|
311
|
+
- `POST /q/japanpost/addresszip`
|
|
491
312
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
됩니다.
|
|
313
|
+
But those route names are not part of the package API. They are just the
|
|
314
|
+
example used by `apps/demo` and `apps/minimal-api`.
|
|
495
315
|
|
|
496
|
-
##
|
|
316
|
+
## Constraints And Notes
|
|
497
317
|
|
|
498
|
-
`dataSource`
|
|
499
|
-
|
|
500
|
-
|
|
318
|
+
- `dataSource` is required at runtime for all hooks.
|
|
319
|
+
- `isValidJapanPostalCode()` checks for an exact 7-digit postal code after
|
|
320
|
+
normalization. `useJapanPostalCode()` is less strict and accepts `3-7`
|
|
321
|
+
digits for prefix lookup.
|
|
322
|
+
- `formatJapanPostalCode()` inserts a hyphen only when the normalized value is
|
|
323
|
+
exactly 7 digits.
|
|
324
|
+
- `cancel()` on `useJapanPostalCode()` and `useJapanAddressSearch()` aborts the
|
|
325
|
+
in-flight request but keeps the latest settled `data` and `error`.
|
|
326
|
+
- `reset()` clears both `data` and `error`.
|
|
327
|
+
- The package does not require a backend to return `404` for misses. Returning
|
|
328
|
+
`200` with an empty page is also compatible with the hook contract.
|
|
329
|
+
- Use your own server-side API in the `dataSource` implementation. Keep Japan
|
|
330
|
+
Post credentials and token exchange on the server side.
|