@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.ko.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# @cp949/japanpost-react
|
|
2
|
+
|
|
3
|
+
[English README](./README.md)
|
|
4
|
+
|
|
5
|
+
<!-- This file is generated by `pnpm readme:package`. Edit docs/README.en.md and docs/README.ko.md instead. -->
|
|
6
|
+
|
|
7
|
+
React + TypeScript 기반의 일본 우편번호/주소 검색 훅과 headless 입력
|
|
8
|
+
컴포넌트 라이브러리입니다.
|
|
9
|
+
|
|
10
|
+
이 문서는 배포 패키지 사용 가이드입니다. `pnpm demo:full` 같은 저장소 수준 보조
|
|
11
|
+
스크립트는 현재 Linux/WSL 계열 셸 환경을 전제로 하며, 자세한 내용은 루트 README에서 다룹니다.
|
|
12
|
+
|
|
13
|
+
## 설치
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @cp949/japanpost-react
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- 지원 React 버전: React 18, React 19
|
|
20
|
+
|
|
21
|
+
## 빠른 시작
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useJapanPostalCode } from "@cp949/japanpost-react";
|
|
25
|
+
import type {
|
|
26
|
+
JapanAddressDataSource,
|
|
27
|
+
JapanAddressRequestOptions,
|
|
28
|
+
JapanAddress,
|
|
29
|
+
Page,
|
|
30
|
+
} from "@cp949/japanpost-react";
|
|
31
|
+
import { createJapanAddressError } from "@cp949/japanpost-react";
|
|
32
|
+
|
|
33
|
+
// 현재 지원 방식은 실제 서버 연동뿐입니다.
|
|
34
|
+
// 앱의 백엔드 API 경로에 맞게 dataSource를 연결하세요.
|
|
35
|
+
// 현재 beta 호환 백엔드에서는 빈 addresszip 검색과 우편번호 miss가
|
|
36
|
+
// HTTP 200 + empty page로 올 수 있으며, 이런 경우는 정상 Page 결과로 유지합니다.
|
|
37
|
+
// 아래 status 매핑은 non-OK 응답에만 적용됩니다.
|
|
38
|
+
function isAbortError(error: unknown): boolean {
|
|
39
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveErrorCode(path: string, status: number) {
|
|
43
|
+
if (status === 404) {
|
|
44
|
+
return "not_found";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (status === 504) {
|
|
48
|
+
return "timeout";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (status === 400) {
|
|
52
|
+
return path === "/q/japanpost/searchcode"
|
|
53
|
+
? "invalid_postal_code"
|
|
54
|
+
: "invalid_query";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return "data_source_error";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readPage(
|
|
61
|
+
path: string,
|
|
62
|
+
request: unknown,
|
|
63
|
+
options?: JapanAddressRequestOptions,
|
|
64
|
+
): Promise<Page<JapanAddress>> {
|
|
65
|
+
let res: Response;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
res = await fetch(path, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"content-type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(request),
|
|
74
|
+
signal: options?.signal,
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw createJapanAddressError(
|
|
78
|
+
isAbortError(error) ? "timeout" : "network_error",
|
|
79
|
+
isAbortError(error) ? "Request timed out" : "Network request failed",
|
|
80
|
+
{
|
|
81
|
+
cause: error,
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const message = `Request failed with status ${res.status}`;
|
|
88
|
+
|
|
89
|
+
throw createJapanAddressError(resolveErrorCode(path, res.status), message, {
|
|
90
|
+
status: res.status,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let payload: unknown;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
payload = await res.json();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw createJapanAddressError(
|
|
100
|
+
"bad_response",
|
|
101
|
+
"Response payload was not valid JSON",
|
|
102
|
+
{
|
|
103
|
+
cause: error,
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
typeof payload !== "object" ||
|
|
110
|
+
payload === null ||
|
|
111
|
+
!Array.isArray((payload as { elements?: unknown }).elements) ||
|
|
112
|
+
typeof (payload as { totalElements?: unknown }).totalElements !== "number" ||
|
|
113
|
+
typeof (payload as { pageNumber?: unknown }).pageNumber !== "number" ||
|
|
114
|
+
typeof (payload as { rowsPerPage?: unknown }).rowsPerPage !== "number"
|
|
115
|
+
) {
|
|
116
|
+
throw createJapanAddressError(
|
|
117
|
+
"bad_response",
|
|
118
|
+
"Response payload must include a valid page payload",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return payload as Page<JapanAddress>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const dataSource: JapanAddressDataSource = {
|
|
126
|
+
async lookupPostalCode(request, options) {
|
|
127
|
+
return readPage(`/q/japanpost/searchcode`, request, options);
|
|
128
|
+
},
|
|
129
|
+
async searchAddress(request, options) {
|
|
130
|
+
return readPage(`/q/japanpost/addresszip`, request, options);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export function PostalForm() {
|
|
135
|
+
const { loading, data, error, search } = useJapanPostalCode({ dataSource });
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div>
|
|
139
|
+
<button onClick={() => void search("100-0001")}>조회</button>
|
|
140
|
+
{loading && <p>조회 중...</p>}
|
|
141
|
+
{error && (
|
|
142
|
+
<p>
|
|
143
|
+
{error.code}: {error.message}
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
<p>전체 결과 수: {data?.totalElements ?? 0}</p>
|
|
147
|
+
{data?.elements.map((addr) => (
|
|
148
|
+
<p key={addr.postalCode + addr.address}>{addr.address}</p>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
예시 `resolveErrorCode()` helper는 non-OK 응답만 분류합니다. 현재 beta 호환
|
|
156
|
+
계약에서는 빈 주소 검색 요청과 우편번호 miss가 `200 + empty page`로 정상
|
|
157
|
+
성공할 수 있고, `404 -> not_found`는 miss를 오류로 노출하는 백엔드에서만
|
|
158
|
+
선택적으로 쓰면 됩니다.
|
|
159
|
+
|
|
160
|
+
## Exports
|
|
161
|
+
|
|
162
|
+
- `normalizeJapanPostalCode`
|
|
163
|
+
- `formatJapanPostalCode`
|
|
164
|
+
- `normalizeJapanPostAddressRecord`
|
|
165
|
+
- `isValidJapanPostalCode`
|
|
166
|
+
- `createJapanAddressError`
|
|
167
|
+
- `useJapanPostalCode`
|
|
168
|
+
- `useJapanAddressSearch`
|
|
169
|
+
- `useJapanAddress`
|
|
170
|
+
- `PostalCodeInput`
|
|
171
|
+
- `AddressSearchInput`
|
|
172
|
+
- `JapanAddress`, `JapanAddressDataSource`, `JapanPostSearchcodeRequest`,
|
|
173
|
+
`JapanPostAddresszipRequest`, `Page`를 포함한 공개 타입
|
|
174
|
+
- 요청 옵션 타입: `JapanAddressRequestOptions`
|
|
175
|
+
|
|
176
|
+
## 유틸리티 메모
|
|
177
|
+
|
|
178
|
+
`formatJapanPostalCode()`는 정규화된 값이 정확히 7자리일 때만 하이픈을 넣습니다.
|
|
179
|
+
그 외 길이에서는 하이픈을 추가하지 않고 숫자만 남긴 값을 그대로 반환합니다.
|
|
180
|
+
|
|
181
|
+
## Hooks
|
|
182
|
+
|
|
183
|
+
### useJapanPostalCode
|
|
184
|
+
|
|
185
|
+
우편번호로 주소를 조회합니다. `3~7자리` 숫자 입력을 받아 `3~6자리`일 때는
|
|
186
|
+
prefix 검색으로 동작합니다.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
const { loading, data, error, search, reset } = useJapanPostalCode({
|
|
190
|
+
dataSource,
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### useJapanAddressSearch
|
|
195
|
+
|
|
196
|
+
자유 형식 키워드로 주소를 검색하며 `debounceMs`를 지원합니다.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
const { loading, data, error, search, reset } = useJapanAddressSearch({
|
|
200
|
+
dataSource,
|
|
201
|
+
debounceMs: 300,
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
이 훅은 빈 검색어에 대해 여전히 클라이언트 선제 검증을 수행하고, 요청을 보내기
|
|
206
|
+
전에 `invalid_query`를 반환합니다. 이 검증은 UX 보조 장치이며 서버 검증이나
|
|
207
|
+
서버 계약 처리를 대체하지 않습니다.
|
|
208
|
+
|
|
209
|
+
### useJapanAddress
|
|
210
|
+
|
|
211
|
+
우편번호 조회와 키워드 검색을 하나의 훅으로 합칩니다.
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
const { loading, data, error, searchByPostalCode, searchByKeyword, reset } =
|
|
215
|
+
useJapanAddress({ dataSource, debounceMs: 300 });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
모든 훅은 런타임에서 `dataSource`가 필요합니다.
|
|
219
|
+
|
|
220
|
+
훅의 public API는 계속 문자열 기반입니다.
|
|
221
|
+
|
|
222
|
+
- `useJapanPostalCode().search(value: string)`
|
|
223
|
+
- `useJapanAddressSearch().search(query: string)`
|
|
224
|
+
- `useJapanAddress().searchByPostalCode(value: string)`
|
|
225
|
+
- `useJapanAddress().searchByKeyword(query: string)`
|
|
226
|
+
|
|
227
|
+
대신 훅 내부에서 `dataSource` 호출 전에 request object를 조립합니다.
|
|
228
|
+
|
|
229
|
+
- 우편번호 조회: `{ value, pageNumber: 0, rowsPerPage: 100 }`
|
|
230
|
+
- 주소 검색: `{ freeword, pageNumber: 0, rowsPerPage: 100 }`
|
|
231
|
+
|
|
232
|
+
`includeCityDetails`, `includePrefectureDetails` 같은 optional flag는
|
|
233
|
+
기본적으로 넣지 않으며, 필요하면 사용자 data source 구현에서 직접
|
|
234
|
+
지정하면 됩니다.
|
|
235
|
+
|
|
236
|
+
## 에러 처리 메모
|
|
237
|
+
|
|
238
|
+
`JapanAddressDataSource`의 두 메서드는 모두 `Page<JapanAddress>`를 직접
|
|
239
|
+
반환해야 합니다. 훅은 그 page payload를 그대로 유지하므로
|
|
240
|
+
`data.elements`, `data.totalElements`, `data.pageNumber`,
|
|
241
|
+
`data.rowsPerPage`를 바로 읽을 수 있습니다.
|
|
242
|
+
|
|
243
|
+
두 메서드는 선택적인 두 번째 인자도 받을 수 있습니다.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
type JapanAddressRequestOptions = {
|
|
247
|
+
signal?: AbortSignal;
|
|
248
|
+
};
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
훅은 superseded 요청, `reset()`, unmount 정리 상황에서 이전 요청을 취소할 수
|
|
252
|
+
있도록 `signal`을 전달합니다. 백엔드 레이어가 abort를 지원하면 그대로 활용할 수
|
|
253
|
+
있습니다.
|
|
254
|
+
|
|
255
|
+
권장 에러 코드 매핑:
|
|
256
|
+
|
|
257
|
+
| 상황 | 권장 코드 |
|
|
258
|
+
| --- | --- |
|
|
259
|
+
| 잘못된 우편번호 입력 | `invalid_postal_code` |
|
|
260
|
+
| 훅 선제 검증 단계의 빈 주소 검색어 | `invalid_query` |
|
|
261
|
+
| 네트워크 실패 | `network_error` |
|
|
262
|
+
| 요청 중단 / 타임아웃 | `timeout` |
|
|
263
|
+
| miss를 오류로 표면화하는 백엔드에서의 검색 결과 없음 | `not_found` |
|
|
264
|
+
| 성공 응답 shape 이상 | `bad_response` |
|
|
265
|
+
| 그 외 백엔드 오류 | `data_source_error` |
|
|
266
|
+
|
|
267
|
+
이 저장소의 참고 demo 흐름에서는 예시 `dataSource`가 실패 요청을 오직 HTTP
|
|
268
|
+
status code로만 분류합니다. 현재 beta와 정렬된 흐름에서는 빈 `addresszip`
|
|
269
|
+
요청과 우편번호 miss가 모두 `200 + empty page`로 올 수 있고, 이런 경우는 정상
|
|
270
|
+
성공 page로 유지합니다. 그 외 `400` 응답은 `invalid_query`로 매핑할 수 있고,
|
|
271
|
+
miss를 오류로 노출하는 백엔드에서는 `404 -> not_found`, `504 -> timeout`
|
|
272
|
+
매핑을 유지할 수 있습니다.
|
|
273
|
+
|
|
274
|
+
## Headless 컴포넌트
|
|
275
|
+
|
|
276
|
+
`PostalCodeInput`, `AddressSearchInput`은 스타일 없이 동작과 DOM 구조만
|
|
277
|
+
제공하므로, 앱의 디자인 시스템에 맞게 직접 꾸밀 수 있습니다.
|
|
278
|
+
|
|
279
|
+
두 컴포넌트는 네이티브 props 전달도 지원합니다.
|
|
280
|
+
|
|
281
|
+
- `inputProps`: 실제 `<input />`에 전달
|
|
282
|
+
- `buttonProps`: 실제 `<button />`에 전달
|
|
283
|
+
|
|
284
|
+
따라서 `id`, `name`, `placeholder`, `aria-*`, `autoComplete`, `className`,
|
|
285
|
+
폼 연동용 속성을 직접 넘길 수 있습니다. `PostalCodeInput`은 별도 override가
|
|
286
|
+
없으면 기본적으로 `inputMode="numeric"`를 사용합니다.
|
|
287
|
+
|
|
288
|
+
## Data Source와 서버 연동
|
|
289
|
+
|
|
290
|
+
이 패키지는 자체 백엔드 서버와 함께 사용하는 것을 권장합니다. Japan Post
|
|
291
|
+
공식 연동은 토큰 기반 인증을 사용하므로, 브라우저에서 업스트림 자격증명을
|
|
292
|
+
직접 보관하면 안 됩니다. 현재 지원 방식은 실제 서버 연동뿐입니다.
|
|
293
|
+
|
|
294
|
+
이 저장소의 `apps/minimal-api`는 로컬 기준 참고 서버 구현입니다. Japan Post
|
|
295
|
+
API ver 2.0을 감싸며, 로컬 개발과 통합 확인 용도로 쓰는 구성을 목표로
|
|
296
|
+
합니다. demo의 `/minimal-api` 경로는 개발 편의를 위한 로컬 경로 연결입니다.
|
|
297
|
+
업스트림 payload에 구조화된 주소 필드와 원본 전체 주소 문자열인 `address`가 함께
|
|
298
|
+
있더라도, 참고 서버는 둘을 그대로 이어붙이지 않고 중복 없는 표시 주소를
|
|
299
|
+
우선 사용합니다.
|
|
300
|
+
|
|
301
|
+
timeout 메시지는 토큰 발급 단계와 실제 조회 단계 중 어느 쪽에서 timeout이
|
|
302
|
+
발생했는지에 따라 달라질 수 있지만, 두 경우 모두 `timeout` 코드로 다루면
|
|
303
|
+
됩니다.
|
|
304
|
+
|
|
305
|
+
## SSR
|
|
306
|
+
|
|
307
|
+
`dataSource` 구현에서는 서버 측 API를 사용하고, 토큰 교환과 업스트림 서명은
|
|
308
|
+
서버에서만 처리하세요. React 훅과 UI 컴포넌트는 클라이언트 컴포넌트에서
|
|
309
|
+
사용하는 것이 안전합니다.
|