@rscc/common-core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -13
- package/dist/index.cjs +435 -22
- package/dist/index.d.cts +349 -6
- package/dist/index.d.ts +349 -6
- package/dist/index.js +420 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# @rscc/common-core
|
|
2
2
|
|
|
3
|
-
RSCC 공통 코어 — `CommonResponse` 봉투 타입과 API 클라이언트(traceId·재시도·멱등성 키·쿠키 인증/CSRF),
|
|
3
|
+
RSCC 공통 코어 — `CommonResponse` 봉투 타입과 API 클라이언트(traceId·재시도·멱등성 키·쿠키 인증/CSRF·도메인 에러 코드 폴백),
|
|
4
|
+
SSE(WHATWG 표준 파서·리더·빌더 + 채팅 프로필),
|
|
4
5
|
회복탄력성 유틸(retry·서킷 브레이커·토큰버킷·Bulkhead·TTL 캐시), 보안 유틸(마스킹·로그 리댁션·
|
|
5
|
-
웹훅 서명·JWT 디코드), 와이어 계약 헬퍼(날짜·목록 쿼리·벌크·업로드·피처 플래그),
|
|
6
|
+
웹훅 서명·JWT 디코드), 와이어 계약 헬퍼(날짜·목록 쿼리·벌크·업로드·피처 플래그), 메시지 다국어(내장 ko·en),
|
|
7
|
+
한국 도메인 유틸.
|
|
6
8
|
**프레임워크 무관, 런타임 의존성 0.** 같은 라이브러리의 Java·Python 구현과 동일한 와이어 계약·공유
|
|
7
9
|
테스트 벡터로 시맨틱을 맞춘다.
|
|
8
10
|
|
|
@@ -20,10 +22,12 @@ npm i @rscc/common-core
|
|
|
20
22
|
|
|
21
23
|
| 모듈 | 공개 API | 설명 |
|
|
22
24
|
|---|---|---|
|
|
23
|
-
| types | `CommonResponse<T>` · `PageResponse<T>` · `ResultCode` · `FieldErrorDetail` · `ValidationErrorData` · `isValidationErrorData` | 응답 봉투 · 페이지(page 0 시작) · 결과 코드(값은 문자열) · 검증 오류 봉투 타입 가드 |
|
|
24
|
-
|
|
|
25
|
+
| types | `CommonResponse<T>` · `PageResponse<T>` · `ResultCode` · `ErrorCodeValue` · `FieldErrorDetail` · `ValidationErrorData` · `isValidationErrorData` | 응답 봉투 · 페이지(page 0 시작) · 결과 코드(값은 문자열) · 봉투 code 타입(공통 코드 자동완성 + 도메인 코드 string) · 검증 오류 봉투 타입 가드 |
|
|
26
|
+
| errorCodes | `isCommonResultCode` · `resultCodeForStatus` · `toCommonResultCode` | 도메인 에러 코드 폴백 — 모르는 code 는 HTTP 상태로 공통 코드 유도 (정확 일치 → 4xx `"400"` → `"500"`) |
|
|
27
|
+
| apiClient | `createApiClient` · `ApiError`(`code` 원문 + `commonCode` 정규화) · `ApiClient` · `ApiClientConfig` · `ApiClientRetryOptions` · `ApiResult<T>` · `ApiRequestInfo` / `ApiResponseInfo` / `ApiErrorInfo` | 봉투 언랩 · 401 콜백 · `X-Trace-Id` 발신/에코 · opt-in 재시도/멱등성 키/`strictJson`/관측 훅 · 인증 모드(`getToken` bearer / `credentials`+`csrf` 쿠키) · 로컬 메시지 `locale`·opt-in `sendAcceptLanguage` |
|
|
25
28
|
| csrf | `csrfHeaderFor` · `readCookie` · `isUnsafeMethod` · `DEFAULT_CSRF_COOKIE_NAME` · `DEFAULT_CSRF_HEADER_NAME` · `CsrfOptions` | CSRF double-submit 헤더 계산 — 비안전 메서드 + 쿠키 존재 + 같은 출처/`allowedOrigins` 일 때만. SSR 안전 |
|
|
26
|
-
| sse | `parseSseFrame` · `readSseStream` · `SseFrameEvent` · `SseSource` · `SseCallbacks` |
|
|
29
|
+
| sse | `parseSseFrame` · `readSseStream` · `parseChatPayload` · `readSseChatEvents` · `SseFrameEvent` · `SseSource` · `SseCallbacks` | 채팅 프로필 SSE — 청크·멀티바이트 경계 안전, 모르는 키는 skip. `readSseChatEvents` 는 표준 파서 위 어댑터(CRLF·멀티라인 data 수용) |
|
|
30
|
+
| sseEvents | `createSseEventParser` · `parseSseEvents` · `readSseEvents` · `formatSseEvent` · `formatSseComment` · `SseEvent` · `SseEventParser` · `SseEventParserOptions` · `ReadSseEventsOptions` · `ReadSseEventsResult` · `FormatSseEventOptions` | 범용 SSE — WHATWG 표준 파서(`event`/`id`/`retry`, CRLF·CR, BOM)·스트림 리더(중단·`Last-Event-ID` 이어받기)·정규 빌더/하트비트 |
|
|
27
31
|
| jwt | `decodeJwtPayload` · `getTokenExpiry` · `isTokenExpired` | base64url 디코드만 — **서명 미검증**, 만료 판단은 fail-closed |
|
|
28
32
|
| masking | `maskSecret` · `maskName` · `maskPhone` · `maskEmail` · `maskCardNumber` | 시크릿(앞4+뒤4)·이름·휴대폰·이메일·카드번호 마스킹 |
|
|
29
33
|
| datetime | `toWireDateTime` · `toWireDate` · `parseWireDateTime` · `stripZone` | 오프셋 없는 LocalDateTime 와이어 형식 — `toISOString()`(Z) 의 9시간 스큐 차단 |
|
|
@@ -38,7 +42,8 @@ npm i @rscc/common-core
|
|
|
38
42
|
| idempotency | `generateIdempotencyKey` | 멱등성 키 생성 (apiClient `idempotency` 옵션으로 자동 부착 가능) |
|
|
39
43
|
| webhook | `signWebhook` · `verifyWebhook` · `WEBHOOK_SIGNATURE_HEADER` | HMAC-SHA256 웹훅 서명/검증 (WebCrypto — async), 시크릿 로테이션 |
|
|
40
44
|
| bulk | `isBulkResult` · `bulkFailures` · `createBulkResultBuilder` · `BulkResult` · `BulkResultItem` · `BulkResultBuilder` | 벌크/부분 실패 봉투 판별·생성 |
|
|
41
|
-
| upload | `sniffFile` · `kindsForExtension` · `validateUpload` · `FileKind` · `UploadValidationResult` | 업로드 프리검증 — 크기·확장자·매직바이트
|
|
45
|
+
| upload | `sniffFile` · `kindsForExtension` · `validateUpload` · `FileKind` · `UploadValidationResult` · `UploadMessageKey` | 업로드 프리검증 — 크기·확장자·매직바이트 대조, 메시지 `locale`·`messages` 오버라이드 |
|
|
46
|
+
| messages | `getMessage` · `formatMessage` · `negotiateLanguage` · `MESSAGES` · `FIXED_MESSAGE_KEYS` · `MessageKey` · `BuiltinLanguage` · `GetMessageOptions` · `MessageOverrides` | 와이어 메시지 다국어 — 내장 ko(기본)·en 카탈로그, 해석 순서·오버라이드, `{N}` 단일 패스 치환, `Accept-Language` 협상 |
|
|
42
47
|
| query | `buildListQuery` · `ListQueryOptions` · `SortParam` | 목록 조회 쿼리(page/size/sort/필터) 빌더 |
|
|
43
48
|
| featureFlags | `parseFlag` · `createFeatureFlags` · `FeatureFlagReader` | 피처 플래그 파싱 (참 집합 `true`/`1`/`on`/`yes`) |
|
|
44
49
|
| rrn | `normalizeRrn` · `isValidRrn` · `isForeignerRrn` · `rrnChecksumOkLegacy` · `rrnBirthDate` | 주민등록번호 형식·생년월일 검증 (기본 검증은 **체크섬 미포함**) |
|
|
@@ -70,7 +75,7 @@ const user = await api.request<UserInfo>("/api/v1/users/me");
|
|
|
70
75
|
// 문자열 body → Content-Type: application/json 자동 부착
|
|
71
76
|
await api.request("/api/v1/items", { method: "POST", body: JSON.stringify(item) });
|
|
72
77
|
|
|
73
|
-
// 실패 봉투/HTTP 에러 → ApiError { code, message, status, traceId, data, retryAfterMs }
|
|
78
|
+
// 실패 봉투/HTTP 에러 → ApiError { code, commonCode, message, status, traceId, data, retryAfterMs }
|
|
74
79
|
try {
|
|
75
80
|
await api.request("/api/v1/things/999");
|
|
76
81
|
} catch (e) {
|
|
@@ -96,6 +101,43 @@ const { data, traceId, response } = await api.requestWithMeta<UserInfo>("/api/v1
|
|
|
96
101
|
- `requestWithMeta` 의 `response` 는 **본문 미소비 원본** — 파싱은 `clone()` 사본으로 하므로 `text()`/`json()`
|
|
97
102
|
재호출이 가능하다.
|
|
98
103
|
|
|
104
|
+
### 에러 코드 — 공통 코드 + 서비스 도메인 코드 폴백
|
|
105
|
+
|
|
106
|
+
봉투의 `code` 는 공통 코드 10종(`"200"`·`"400"`·`"401"`·`"403"`·`"404"`·`"405"`·`"409"`·`"415"`·`"429"`·`"500"`)
|
|
107
|
+
또는 서비스가 얹은 **도메인 코드**(예: `ORDER_OUT_OF_STOCK` — 실패 전용, 정확히 3자리 숫자는 공통 예약)다.
|
|
108
|
+
모르는 코드를 받으면 오류를 내지 말고 **HTTP 상태로 공통 코드를 유도**한다 — 이 폴백 덕분에 서버가 도메인 코드를
|
|
109
|
+
추가해도 기존 소비자는 깨지지 않는다.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { ApiError, ResultCode, resultCodeForStatus, toCommonResultCode, isCommonResultCode } from "@rscc/common-core";
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await api.request("/api/v1/payments", { method: "POST", body: JSON.stringify(payment) });
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (!(e instanceof ApiError)) throw e;
|
|
118
|
+
// 도메인별 처리는 원 코드로
|
|
119
|
+
if (e.code === "PAYMENT_DECLINED") return showDeclined();
|
|
120
|
+
// 공통 분기는 정규화된 commonCode 로 — HTTP 402 + PAYMENT_DECLINED → "400"
|
|
121
|
+
switch (e.commonCode) {
|
|
122
|
+
case ResultCode.UNAUTHORIZED: return redirectToLogin();
|
|
123
|
+
case ResultCode.BAD_REQUEST: return showInputError(e.message);
|
|
124
|
+
default: return showGenericError(e.traceId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
resultCodeForStatus(404); // "404" — 정확 일치 공통 코드 (SUCCESS 제외)
|
|
129
|
+
resultCodeForStatus(422); // "400" — 그 외 4xx
|
|
130
|
+
resultCodeForStatus(503); // "500" — 그 외
|
|
131
|
+
toCommonResultCode("PAYMENT_DECLINED", 402); // "400" — 모르는 코드는 상태로 유도
|
|
132
|
+
toCommonResultCode("409", 400); // "409" — 공통 코드는 그대로
|
|
133
|
+
isCommonResultCode("ORDER_OUT_OF_STOCK"); // false (타입 가드: true 면 ResultCode)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `ApiError.code` 는 원 코드 보존(비봉투 에러는 상태 코드 문자열, `strictJson` 은 `"INVALID_JSON"`),
|
|
137
|
+
`ApiError.commonCode` 는 생성자에서 `toCommonResultCode(code, status)` 로 계산한 공통 코드다.
|
|
138
|
+
- `CommonResponse.code` / `ApiError.code` 의 타입 `ErrorCodeValue` 는 `ResultCode | (string & {})` —
|
|
139
|
+
`string` 과 상호 대입 가능하면서 공통 코드 리터럴을 자동완성한다.
|
|
140
|
+
|
|
99
141
|
### 인증 모드 — bearer vs 쿠키(jwt-cookie / session) + CSRF
|
|
100
142
|
|
|
101
143
|
서버가 인증 자격을 어디로 받는지(서버 설정의 인증 모드)에 맞춰 클라이언트 설정을 고른다.
|
|
@@ -166,21 +208,121 @@ await retry(() => cb.execute(() => api.request("/api/v1/downstream")), {
|
|
|
166
208
|
- 보고가 끝내 오지 않은 그랜트는 OPEN 진입 후 `2 × openDurationMs` 에 유실로 용서되어 OPEN 영구 고착을 막는다.
|
|
167
209
|
HALF_OPEN 프로브도 `openDurationMs` 동안 미보고면 유실로 간주해 새 프로브를 넘긴다(HALF_OPEN 고착 방지).
|
|
168
210
|
|
|
169
|
-
### SSE 스트림 소비
|
|
211
|
+
### SSE 스트림 소비 — 채팅 프로필
|
|
170
212
|
|
|
171
213
|
```ts
|
|
172
|
-
import { readSseStream } from "@rscc/common-core";
|
|
214
|
+
import { readSseStream, readSseChatEvents } from "@rscc/common-core";
|
|
173
215
|
|
|
174
216
|
const res = await fetch(streamUrl, { method: "POST", body, signal });
|
|
175
|
-
await readSseStream(res, {
|
|
217
|
+
await readSseStream(res, { // 레거시 리더 — LF(\n\n) 프레임 구분, 첫 data: 라인
|
|
176
218
|
onConversationId: (id) => { /* 첫 프레임 */ },
|
|
177
219
|
onSources: (sources) => { /* RAG 근거 */ },
|
|
178
220
|
onDelta: (chunk) => { /* 텍스트 증분 */ },
|
|
179
|
-
onError: (message) => { /* in-band 오류 */ },
|
|
221
|
+
onError: (message) => { /* in-band 오류 — 이후 delta 는 전달 안 됨 */ },
|
|
180
222
|
onDone: () => { /* data: [DONE] */ },
|
|
181
223
|
});
|
|
224
|
+
|
|
225
|
+
// 같은 콜백, 표준 파서 위의 어댑터 — CRLF·CR 줄 끝, 멀티라인 data, "[DONE] "(공백) 도 수용.
|
|
226
|
+
// EOF 에서 빈 줄 없이 끝난 잔여 프레임도 레거시처럼 처리한다.
|
|
227
|
+
await readSseChatEvents(res, callbacks);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### SSE 범용 계층 — 표준 파서·리더·빌더 (WHATWG event stream)
|
|
231
|
+
|
|
232
|
+
채팅 외의 SSE(알림·진행률·로그 등)와 `event`/`id`/`retry` 필드를 쓰는 스트림용. WHATWG HTML 표준의 이벤트 스트림
|
|
233
|
+
해석을 그대로 따른다 — BOM 1개 제거, CRLF·LF·CR 줄 끝(청크 경계의 CR+LF 도 1개), 값 앞 공백 1개만 제거,
|
|
234
|
+
NUL 포함 `id` 무시, `retry` 는 ASCII 숫자만, 데이터 없는 블록은 디스패치 안 함, **EOF 의 미완성 이벤트는 폐기**.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import {
|
|
238
|
+
readSseEvents, parseSseEvents, createSseEventParser, formatSseEvent, formatSseComment, type SseEvent,
|
|
239
|
+
} from "@rscc/common-core";
|
|
240
|
+
|
|
241
|
+
// 1) fetch 스트림 읽기 — onEvent 가 true 를 반환하면 즉시 멈추고 연결(reader)을 cancel
|
|
242
|
+
const res = await fetch("/api/v1/jobs/42/events", { signal });
|
|
243
|
+
const { lastEventId, retryMs, stopped } = await readSseEvents(res, {
|
|
244
|
+
onEvent: (e: SseEvent) => { // { event: "progress", data: "60", id: "17" }
|
|
245
|
+
render(e);
|
|
246
|
+
return e.event === "end"; // 종료 이벤트 → stopped: true
|
|
247
|
+
},
|
|
248
|
+
onRetry: (ms) => { /* 서버 재연결 대기 힌트 */ },
|
|
249
|
+
// lastEventId: prevId, // 재연결 시 직전 값 — id 없는 이벤트도 이어받음(EventSource 동일)
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// 2) 완결 텍스트 파싱 / 증분 파서 직접 구동
|
|
253
|
+
parseSseEvents('event: update\nid: 42\ndata: {"x":1}\n\n'); // [{ event: "update", data: '{"x":1}', id: "42" }]
|
|
254
|
+
const parser = createSseEventParser((e) => handle(e), (ms) => setRetry(ms));
|
|
255
|
+
parser.feed(chunk1);
|
|
256
|
+
parser.feed(chunk2);
|
|
257
|
+
parser.end(); // 미완성 이벤트 폐기 — end({ dispatchIncomplete: true }) 면 잔여까지 디스패치
|
|
258
|
+
|
|
259
|
+
// 3) 서버/프록시 측 정규 빌더 — 필드 순서 id → event → retry → data, 줄 끝 LF
|
|
260
|
+
formatSseEvent("a\nb", { id: "7", event: "update", retry: 3000 });
|
|
261
|
+
// "id: 7\nevent: update\nretry: 3000\ndata: a\ndata: b\n\n"
|
|
262
|
+
formatSseComment("ping"); // ": ping\n\n" — 하트비트(이벤트 없음)
|
|
182
263
|
```
|
|
183
264
|
|
|
265
|
+
- `readSseEvents` 는 body 가 없으면 throw 하고, `onEvent`/`onRetry` 가 throw 하면 reader 를 cancel 한 뒤 그 오류로 reject 한다.
|
|
266
|
+
- 빌더는 `id`/`event` 의 CR·LF, `id` 의 NUL, 음수·비정수 `retry` 를 오류로 거부한다. `id: ""` 는 `id: ` 를 써서
|
|
267
|
+
수신 측의 마지막 이벤트 ID 를 초기화한다. 빈 `event` 는 생략(수신 측 기본 `"message"` 와 동치).
|
|
268
|
+
- 권장 응답 헤더: `Content-Type: text/event-stream`, `Cache-Control: no-cache, no-transform`, `X-Accel-Buffering: no`.
|
|
269
|
+
- React 에서는 `@rscc/common-react` 의 `useSseEvents`(재연결 시 `Last-Event-ID`·서버 `retry:` 존중)를 쓴다.
|
|
270
|
+
|
|
271
|
+
### 메시지 다국어 — 내장 ko(기본)·en
|
|
272
|
+
|
|
273
|
+
라이브러리가 내보내는 와이어 메시지(서버 봉투 메시지 22키 + 클라이언트 로컬 메시지 3키)를 **ko·en 으로 내장**한다.
|
|
274
|
+
**기본 로케일은 `ko`** — 아무것도 지정하지 않으면 모든 문구는 이전(현행 한국어)과 바이트 동일하다. `code` 는
|
|
275
|
+
로케일과 무관하게 불변이고 `message` 만 바뀐다.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { getMessage, formatMessage, negotiateLanguage, MESSAGES, FIXED_MESSAGE_KEYS } from "@rscc/common-core";
|
|
279
|
+
|
|
280
|
+
getMessage("rscc.result.NOT_FOUND"); // "리소스를 찾을 수 없습니다." (기본 ko)
|
|
281
|
+
getMessage("rscc.result.NOT_FOUND", { locale: "en-GB" }); // "Resource not found." — BCP 47 주 서브태그 사용
|
|
282
|
+
getMessage("rscc.upload.extension", { locale: "en", args: ["exe"] }); // "File type is not allowed: exe"
|
|
283
|
+
getMessage("rscc.result.NOT_FOUND", { locale: "ja" }); // 내장 밖 언어 → 기본 언어(defaultLocale, 기본 ko) 표
|
|
284
|
+
getMessage("rscc.result.NOT_FOUND", {
|
|
285
|
+
locale: "ja",
|
|
286
|
+
overrides: (key, language) => (language === "ja" ? jaCatalog[key] : undefined), // 또는 { [key]: 템플릿 } 표
|
|
287
|
+
});
|
|
288
|
+
formatMessage("{0}-{1}", "{1}", "x"); // "{1}-x" — 단일 패스 치환(재치환 없음)
|
|
289
|
+
|
|
290
|
+
// Accept-Language 협상 — 서버와 같은 규칙 (q 내림차순 안정 정렬, q=0 거부, `*`·미지원·형식 오류 → 기본 언어)
|
|
291
|
+
negotiateLanguage("en-US,en;q=0.9,ko;q=0.8"); // "en"
|
|
292
|
+
negotiateLanguage("ja", ["ko", "en", "ja"]); // "ja" — 지원 언어 추가
|
|
293
|
+
negotiateLanguage(undefined, ["ko", "en"], "en"); // "en" — 헤더 없음 → 기본 언어
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- **해석 순서**: ① 고정 키 + 요청 언어 `ko`·`en` → 내장 → ② `overrides`(빈 값·키 자체와 같은 결과는 미해결) →
|
|
297
|
+
③ 내장[요청 언어] → ④ 내장[`defaultLocale`] → ⑤ 내장[`ko`] → ⑥ 키 문자열 (모르는 키도 예외 없음).
|
|
298
|
+
- **규약 고정 문구**(`FIXED_MESSAGE_KEYS` — 멱등성 키 3종·정렬 쿼리 2종·CSRF)는 클라이언트가 문구로 분기할 수 있어
|
|
299
|
+
`ko`·`en` 은 오버라이드로 바꿀 수 없다(그 외 언어는 허용). 서버가 영어를 켜면 이 문구도 `en` 값으로 오므로, 문구로
|
|
300
|
+
분기하는 클라이언트는 `MESSAGES.en[key]` 도 함께 인식할 것 (가능하면 `code` 로 분기).
|
|
301
|
+
- 자리표시자 `{0}`,`{1}`… 는 단일 패스 문자 치환(MessageFormat 아님) — 인자가 없는 `{N}` 은 그대로 남는다.
|
|
302
|
+
- **서버 봉투의 `message` 는 서버가 정한다** — 서비스가 i18n 을 켜면(기본 로케일 변경 또는 `Accept-Language` 요청별
|
|
303
|
+
협상) 서버가 현지화된 문구를 보낸다. 클라이언트의 `locale` 은 서버 문구를 바꾸지 않는다.
|
|
304
|
+
|
|
305
|
+
클라이언트가 직접 만드는 문구:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
// 업로드 프리검증 — locale 미지정 = 현행 ko 문구
|
|
309
|
+
validateUpload({ fileName, size, head, maxSizeBytes, allowedExtensions, locale: "en" });
|
|
310
|
+
// → { ok: false, reason: "extension", message: "File type is not allowed: exe" }
|
|
311
|
+
validateUpload({ ...input, messages: { "rscc.upload.size": "10MB 이하만 올릴 수 있어요." } }); // 템플릿 오버라이드
|
|
312
|
+
|
|
313
|
+
// apiClient — 로컬 ApiError 문구 + opt-in Accept-Language
|
|
314
|
+
const api = createApiClient({ baseUrl, locale: "en", sendAcceptLanguage: true });
|
|
315
|
+
// 비봉투 HTTP 에러 → "API error: 503 Service Unavailable" · strictJson → "The response body is not valid JSON."
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- `validateUpload` 의 `messages`: `rscc.upload.size` / `rscc.upload.extension`(`{0}` = 확장자) / `rscc.upload.contentMismatch`
|
|
319
|
+
템플릿 — 준 키는 로케일과 무관하게 내장 문구보다 우선.
|
|
320
|
+
- `ApiClientConfig.locale` 은 클라이언트가 만드는 3종(본문 읽기 실패·`INVALID_JSON`·비봉투 HTTP 에러)에만 적용 —
|
|
321
|
+
실패 봉투의 `message` 는 서버 문구 그대로.
|
|
322
|
+
- **`sendAcceptLanguage`** (기본 false): true 이고 `locale` 이 있으면 `Accept-Language: <locale>` 부착(호출자 헤더 우선).
|
|
323
|
+
**브라우저는 사용자 언어 설정으로 `Accept-Language` 를 스스로 보낸다** — 이 옵션은 그 헤더가 없는 Node·SSR 이나
|
|
324
|
+
언어를 강제할 때 쓴다. 값은 BCP 47 태그로 둘 것(CORS safelisted — preflight 미유발).
|
|
325
|
+
|
|
184
326
|
### AES-256/GCM — Node 전용 서브패스
|
|
185
327
|
|
|
186
328
|
```ts
|
|
@@ -192,7 +334,10 @@ const dec = decryptAesGcm(enc, key); // 키 상이·변조 시 CryptoError
|
|
|
192
334
|
|
|
193
335
|
## 알려진 제약
|
|
194
336
|
|
|
195
|
-
-
|
|
337
|
+
- **레거시 SSE 리더(`parseSseFrame`/`readSseStream`)는 프레임 구분자 LF(`\n\n`) 고정** — 개행을 CRLF 로 정규화하는
|
|
338
|
+
프록시 뒤에서는 프레임이 분리되지 않는다. 이 경우 `readSseChatEvents`(표준 파서 위 채팅 어댑터)를 쓸 것.
|
|
339
|
+
- 표준 SSE 리더(`readSseEvents`)는 스펙대로 **EOF 의 미완성 이벤트를 폐기**한다 — 서버는 이벤트를 빈 줄로 끝내야 한다
|
|
340
|
+
(레거시 호환이 필요하면 `dispatchIncompleteAtEof: true`).
|
|
196
341
|
- 2xx 응답인데 본문이 JSON 이 아니면 `data` 는 기본적으로 조용히 null 이 된다 — 명시 실패가 필요하면 `strictJson: true`.
|
|
197
342
|
- 문자열 body 는 `application/json` 으로 간주된다 — JSON 이 아닌 문자열 본문은 Content-Type 을 직접 지정할 것.
|
|
198
343
|
- 교차 출처에서 traceId 에코를 읽으려면 서버가 `Access-Control-Expose-Headers: X-Trace-Id` 를 내려야 한다
|
|
@@ -205,6 +350,7 @@ const dec = decryptAesGcm(enc, key); // 키 상이·변조 시 CryptoError
|
|
|
205
350
|
- `isValidRrn` 은 체크섬을 포함하지 않는다(2020-10 이후 발급분은 체크섬 불성립) — `rrnChecksumOkLegacy` 는 레거시 정합 검사 전용.
|
|
206
351
|
- `signWebhook`/`verifyWebhook` 은 WebCrypto 기반이라 **async** 다 (Java/Python 은 동기).
|
|
207
352
|
- 서킷 브레이커·토큰버킷·Bulkhead·TTL 캐시는 **인스턴스(프로세스) 로컬** 상태다 — 분산 공유되지 않는다.
|
|
353
|
+
- 내장 메시지 언어는 `ko`·`en` 뿐이다 — 그 외 언어는 `getMessage` 의 `overrides`(업로드는 `messages`)로 공급할 것.
|
|
208
354
|
- `@rscc/common-core/crypto` 는 `node:crypto` 를 쓰는 **Node 전용** 서브패스 — 브라우저 번들에 포함하지 말 것.
|
|
209
355
|
- 서브패스는 `exports` 맵으로만 노출된다 — TypeScript `moduleResolution` 이 레거시 `node`(`node10`)면
|
|
210
356
|
`@rscc/common-core/crypto` 의 타입을 찾지 못한다. `node16` / `nodenext` / `bundler` 를 사용할 것
|
|
@@ -212,5 +358,5 @@ const dec = decryptAesGcm(enc, key); // 키 상이·변조 시 CryptoError
|
|
|
212
358
|
|
|
213
359
|
## 관련 패키지
|
|
214
360
|
|
|
215
|
-
- React 훅(`useDebounce`, `useSse`): [@rscc/common-react](https://www.npmjs.com/package/@rscc/common-react)
|
|
361
|
+
- React 훅(`useDebounce`, `useSse`, `useSseEvents`): [@rscc/common-react](https://www.npmjs.com/package/@rscc/common-react)
|
|
216
362
|
- 라이선스: MIT
|