@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.
Files changed (48) hide show
  1. package/README.ko.md +349 -0
  2. package/README.md +249 -419
  3. package/dist/index.d.ts +1 -11
  4. package/dist/index.es.js +106 -60
  5. package/dist/index.umd.cjs +1 -1
  6. package/dist/{components → src/components}/AddressSearchInput.d.ts +1 -0
  7. package/dist/src/components/AddressSearchInput.d.ts.map +1 -0
  8. package/dist/{components → src/components}/PostalCodeInput.d.ts +1 -0
  9. package/dist/src/components/PostalCodeInput.d.ts.map +1 -0
  10. package/dist/src/core/errors.d.ts +11 -0
  11. package/dist/src/core/errors.d.ts.map +1 -0
  12. package/dist/src/core/formatters.d.ts.map +1 -0
  13. package/dist/{core → src/core}/normalizers.d.ts +1 -1
  14. package/dist/src/core/normalizers.d.ts.map +1 -0
  15. package/dist/src/core/types.d.ts +261 -0
  16. package/dist/src/core/types.d.ts.map +1 -0
  17. package/dist/src/core/validators.d.ts.map +1 -0
  18. package/dist/src/index.d.ts +11 -0
  19. package/dist/src/index.d.ts.map +1 -0
  20. package/dist/src/react/toJapanAddressError.d.ts +8 -0
  21. package/dist/src/react/toJapanAddressError.d.ts.map +1 -0
  22. package/dist/{react → src/react}/useJapanAddress.d.ts +2 -1
  23. package/dist/src/react/useJapanAddress.d.ts.map +1 -0
  24. package/dist/src/react/useJapanAddressSearch.d.ts.map +1 -0
  25. package/dist/src/react/useJapanPostalCode.d.ts.map +1 -0
  26. package/dist/{react → src/react}/useLatestRequestState.d.ts +6 -0
  27. package/dist/src/react/useLatestRequestState.d.ts.map +1 -0
  28. package/package.json +5 -5
  29. package/dist/components/AddressSearchInput.d.ts.map +0 -1
  30. package/dist/components/PostalCodeInput.d.ts.map +0 -1
  31. package/dist/core/errors.d.ts +0 -10
  32. package/dist/core/errors.d.ts.map +0 -1
  33. package/dist/core/formatters.d.ts.map +0 -1
  34. package/dist/core/normalizers.d.ts.map +0 -1
  35. package/dist/core/types.d.ts +0 -182
  36. package/dist/core/types.d.ts.map +0 -1
  37. package/dist/core/validators.d.ts.map +0 -1
  38. package/dist/index.d.ts.map +0 -1
  39. package/dist/react/toJapanAddressError.d.ts +0 -7
  40. package/dist/react/toJapanAddressError.d.ts.map +0 -1
  41. package/dist/react/useJapanAddress.d.ts.map +0 -1
  42. package/dist/react/useJapanAddressSearch.d.ts.map +0 -1
  43. package/dist/react/useJapanPostalCode.d.ts.map +0 -1
  44. package/dist/react/useLatestRequestState.d.ts.map +0 -1
  45. /package/dist/{core → src/core}/formatters.d.ts +0 -0
  46. /package/dist/{core → src/core}/validators.d.ts +0 -0
  47. /package/dist/{react → src/react}/useJapanAddressSearch.d.ts +0 -0
  48. /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
- [English](#english) | [한국어](#한국어)
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 + TypeScript hooks and headless inputs for Japan postal-code and address
12
- lookup.
7
+ React hooks, headless input components, and utilities for Japan postal-code
8
+ and address lookup.
13
9
 
14
- This package guide covers the published library. Repository-level demo scripts
15
- such as `pnpm demo:full` currently target Linux/WSL-style shell environments
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
- - Supported React versions: React 18 and React 19
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 { useJapanPostalCode } from "@cp949/japanpost-react";
30
- import type { JapanAddressDataSource } from "@cp949/japanpost-react";
31
- import { createJapanAddressError } from "@cp949/japanpost-react";
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
- async lookupPostalCode(postalCode) {
37
- const res = await fetch(`/searchcode/${postalCode}`);
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
- async searchAddress(query) {
73
- const res = await fetch(`/addresszip?q=${encodeURIComponent(query)}`);
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 PostalForm() {
134
+ export function PostalCodeLookupExample() {
111
135
  const { loading, data, error, search } = useJapanPostalCode({ dataSource });
112
136
 
113
137
  return (
114
138
  <div>
115
- <button onClick={() => void search("100-0001")}>Search</button>
116
- {loading && <p>Loading...</p>}
117
- {error && (
118
- <p>
119
- {error.code}: {error.message}
120
- </p>
121
- )}
122
- {data?.addresses.map((addr) => (
123
- <p key={addr.postalCode + addr.address}>{addr.address}</p>
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
- ## Exports
131
-
132
- - `normalizeJapanPostalCode`
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
- ### useJapanAddressSearch
166
+ ## Core Contract
164
167
 
165
- Searches addresses by free-form keyword and supports debouncing.
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 JapanAddressRequestOptions = {
194
- signal?: AbortSignal;
171
+ type Page<T> = {
172
+ elements: T[];
173
+ totalElements: number;
174
+ pageNumber: number;
175
+ rowsPerPage: number;
195
176
  };
196
177
  ```
197
178
 
198
- Hooks pass `signal` so your data source can cancel superseded requests,
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
- Use this package with your own backend server. The official Japan Post flow
234
- uses token-based authentication, so browser apps should not hold upstream
235
- credentials directly. The supported integration model is a real server-backed
236
- flow.
237
-
238
- This repository includes `apps/minimal-api` as the reference local server. It
239
- wraps Japan Post API ver 2.0 and is intended for local development and
240
- integration testing. The demo's `/minimal-api` path is only a development-time
241
- route to that local server. When the upstream payload includes both structured
242
- address parts and a free-form `address` string, the reference server keeps the
243
- display address non-duplicated instead of concatenating both blindly.
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
- ## 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
- 그 외 길이에서는 하이픈을 추가하지 않고 숫자만 남긴 값을 그대로 반환합니다.
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 { loading, data, error, search, reset } = useJapanPostalCode({
405
- dataSource,
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
- 자유 형식 키워드로 주소를 검색하며 `debounceMs`를 지원합니다.
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 { loading, data, error, search, reset } = useJapanAddressSearch({
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 { loading, data, error, searchByPostalCode, searchByKeyword, reset } =
426
- useJapanAddress({ dataSource, debounceMs: 300 });
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
- `JapanAddressDataSource`의 메서드는 모두 `JapanAddress[]`를 직접
434
- 반환해야 합니다. 각 훅은 그 배열을 받아 `{ postalCode, addresses }`,
435
- `{ query, addresses }` 형태의 결과 객체를 조합합니다.
266
+ ## Headless Components
436
267
 
437
- 메서드는 선택적인 두 번째 인자도 받을 수 있습니다.
268
+ ### `PostalCodeInput`
438
269
 
439
- ```ts
440
- type JapanAddressRequestOptions = {
441
- signal?: AbortSignal;
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
- 훅은 superseded 요청, `reset()`, unmount 정리 상황에서 이전 요청을 취소할 수
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
- 저장소의 참고 demo 흐름에서는 예시 `dataSource`가 `400 /searchcode/...`를
462
- `invalid_postal_code`, `400 /addresszip?...`를 `invalid_query`, `404`를
463
- `not_found`, `504`를 `timeout`으로 매핑합니다.
283
+ - `inputProps` for the rendered `<input>`
284
+ - `buttonProps` for the rendered `<button>`
464
285
 
465
- ## Headless 컴포넌트
286
+ ## Data Source Integration
466
287
 
467
- `PostalCodeInput`, `AddressSearchInput`은 스타일 없이 동작과 DOM 구조만
468
- 제공하므로, 앱의 디자인 시스템에 맞게 직접 꾸밀 수 있습니다.
288
+ The package exports types for both sides of the integration:
469
289
 
470
- 컴포넌트는 네이티브 props 전달도 지원합니다.
290
+ - `JapanAddressDataSource`
291
+ - `JapanPostSearchcodeRequest`
292
+ - `JapanPostAddresszipRequest`
293
+ - `JapanPostalCodeSearchInput`
294
+ - `JapanAddressSearchInput`
295
+ - `JapanAddressRequestOptions`
471
296
 
472
- - `inputProps`: 실제 `<input />`에 전달
473
- - `buttonProps`: 실제 `<button />`에 전달
297
+ The optional second argument to each data-source method is:
474
298
 
475
- 따라서 `id`, `name`, `placeholder`, `aria-*`, `autoComplete`, `className`,
476
- 연동용 속성을 직접 넘길 수 있습니다. `PostalCodeInput`은 별도 override가
477
- 없으면 기본적으로 `inputMode="numeric"`를 사용합니다.
299
+ ```ts
300
+ type JapanAddressRequestOptions = {
301
+ signal?: AbortSignal;
302
+ };
303
+ ```
478
304
 
479
- ## Data Source와 서버 연동
305
+ The hooks pass `signal` so your data source can cancel superseded requests,
306
+ `cancel()` calls, `reset()` calls, and unmount cleanup.
480
307
 
481
- 패키지는 자체 백엔드 서버와 함께 사용하는 것을 권장합니다. Japan Post
482
- 공식 연동은 토큰 기반 인증을 사용하므로, 브라우저에서 업스트림 자격증명을
483
- 직접 보관하면 안 됩니다. 현재 지원 방식은 실제 서버 연동뿐입니다.
308
+ This repository's reference backend uses these routes:
484
309
 
485
- 저장소의 `apps/minimal-api`는 로컬 기준 참고 서버 구현입니다. Japan Post
486
- API ver 2.0을 감싸며, 로컬 개발과 통합 확인 용도로 쓰는 구성을 목표로
487
- 합니다. demo의 `/minimal-api` 경로는 개발 편의를 위한 로컬 경로 연결입니다.
488
- 업스트림 payload에 구조화된 주소 필드와 원본 전체 주소 문자열인 `address`가 함께
489
- 있더라도, 참고 서버는 둘을 그대로 이어붙이지 않고 중복 없는 표시 주소를
490
- 우선 사용합니다.
310
+ - `POST /q/japanpost/searchcode`
311
+ - `POST /q/japanpost/addresszip`
491
312
 
492
- timeout 메시지는 토큰 발급 단계와 실제 조회 단계 어느 쪽에서 timeout이
493
- 발생했는지에 따라 달라질 수 있지만, 두 경우 모두 `timeout` 코드로 다루면
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
- ## SSR
316
+ ## Constraints And Notes
497
317
 
498
- `dataSource` 구현에서는 서버 API를 사용하고, 토큰 교환과 업스트림 서명은
499
- 서버에서만 처리하세요. React 훅과 UI 컴포넌트는 클라이언트 컴포넌트에서
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.