@rscc/common-core 0.5.0 → 0.6.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 +46 -1
- package/dist/index.cjs +182 -14
- package/dist/index.d.cts +77 -14
- package/dist/index.d.ts +77 -14
- package/dist/index.js +182 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ npm i @rscc/common-core
|
|
|
30
30
|
| sseEvents | `createSseEventParser` · `parseSseEvents` · `readSseEvents` · `formatSseEvent` · `formatSseComment` · `SseEvent` · `SseEventParser` · `SseEventParserOptions` · `ReadSseEventsOptions` · `ReadSseEventsResult` · `FormatSseEventOptions` | 범용 SSE — WHATWG 표준 파서(`event`/`id`/`retry`, CRLF·CR, BOM)·스트림 리더(중단·`Last-Event-ID` 이어받기)·정규 빌더/하트비트 |
|
|
31
31
|
| jwt | `decodeJwtPayload` · `getTokenExpiry` · `isTokenExpired` | base64url 디코드만 — **서명 미검증**, 만료 판단은 fail-closed |
|
|
32
32
|
| masking | `maskSecret` · `maskName` · `maskPhone` · `maskEmail` · `maskCardNumber` | 시크릿(앞4+뒤4)·이름·휴대폰·이메일·카드번호 마스킹 |
|
|
33
|
-
| datetime | `toWireDateTime` · `toWireDate` · `parseWireDateTime` · `stripZone` | 오프셋 없는 LocalDateTime 와이어 형식 — `toISOString()`(Z) 의 9시간 스큐
|
|
33
|
+
| datetime | `toWireDateTime` · `toWireDate` · `parseWireDateTime` · `stripZone` · `WireDateTimeFormatOptions` · `WireDateFormatOptions` · `WireDateTimeParseOptions` · `InboundOffsetPolicy` | 오프셋 없는 LocalDateTime 와이어 형식(기본) — `toISOString()`(Z) 의 9시간 스큐 차단. opt-in 기준 시간대(`timeZone`)·offset 프로필(`offset`)·수신 오프셋 정책(`inboundOffset`) |
|
|
34
34
|
| chosung | `toChosung` · `isChosungQuery` | 한글 초성 변환·초성 질의 판별 (자동완성) |
|
|
35
35
|
| retry | `retry` · `isRetryableStatus` · `parseRetryAfterMs` · `RetryOptions` | 지수 백오프 + full jitter, 시간 예산, `Retry-After` 파싱 |
|
|
36
36
|
| cache | `createTtlCache` · `TtlCache<K,V>` · `TtlCacheOptions` | TTL 캐시 + single-flight(동일 키 로더 1회 공유), lazy expiry |
|
|
@@ -268,6 +268,49 @@ formatSseComment("ping"); // ": ping\n\n" — 하트비트(이벤트 없음)
|
|
|
268
268
|
- 권장 응답 헤더: `Content-Type: text/event-stream`, `Cache-Control: no-cache, no-transform`, `X-Accel-Buffering: no`.
|
|
269
269
|
- React 에서는 `@rscc/common-react` 의 `useSseEvents`(재연결 시 `Last-Event-ID`·서버 `retry:` 존중)를 쓴다.
|
|
270
270
|
|
|
271
|
+
### 날짜·시각 — 기준 시간대 · offset 프로필 (opt-in)
|
|
272
|
+
|
|
273
|
+
**기본은 local 프로필** — 옵션을 주지 않으면 현행 그대로(실행 환경 로컬 게터, 오프셋 없는 `yyyy-MM-ddTHH:mm:ss`,
|
|
274
|
+
수신 오프셋은 버림). 서비스 단위로 기준 시간대와 offset 프로필을 켤 수 있다.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { toWireDateTime, toWireDate, parseWireDateTime } from "@rscc/common-core";
|
|
278
|
+
|
|
279
|
+
const at = new Date("2026-07-10T04:00:00Z");
|
|
280
|
+
toWireDateTime(at); // 로컬 게터 (기본·현행)
|
|
281
|
+
toWireDateTime(at, { timeZone: "Asia/Seoul" }); // "2026-07-10T13:00:00" (DT-01)
|
|
282
|
+
toWireDateTime(at, { timeZone: "Asia/Seoul", offset: true }); // "2026-07-10T13:00:00+09:00" (DT-02)
|
|
283
|
+
toWireDateTime(at, { timeZone: "UTC", offset: true }); // "2026-07-10T04:00:00Z" (DT-03 — 0 은 Z)
|
|
284
|
+
toWireDateTime(at, { offset: true }); // 호스트 오프셋(-getTimezoneOffset) 부착
|
|
285
|
+
toWireDate(at, { timeZone: "Asia/Seoul" }); // "2026-07-10"
|
|
286
|
+
|
|
287
|
+
// 수신 — 오프셋 없는 벽시계를 기준 시간대로 해석 (DST 갭은 갭 길이만큼 뒤로, 겹침은 이른 오프셋)
|
|
288
|
+
parseWireDateTime("2026-07-10T13:00:00", { timeZone: "Asia/Seoul" }); // 04:00Z (DT-06)
|
|
289
|
+
parseWireDateTime("2026-03-08T02:30:00", { timeZone: "America/New_York" }); // 07:30Z (DT-12 갭)
|
|
290
|
+
parseWireDateTime("2026-11-01T01:30:00", { timeZone: "America/New_York" }); // 05:30Z (DT-13 겹침)
|
|
291
|
+
|
|
292
|
+
// 수신 오프셋 정책 — 오프셋이 붙은 입력
|
|
293
|
+
parseWireDateTime("2026-07-10T04:00:00Z", { inboundOffset: "convert" }); // 정확한 순간 04:00Z (timeZone 무관)
|
|
294
|
+
parseWireDateTime("2026-07-10T04:00:00Z", { timeZone: "Asia/Seoul" }); // drop(기본) — 벽시계 04:00 KST (현행 스큐)
|
|
295
|
+
parseWireDateTime("2026-07-10T04:00:00Z", { inboundOffset: "reject" }); // RangeError
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| 옵션 | 함수 | 기본 | 설명 |
|
|
299
|
+
|---|---|---|---|
|
|
300
|
+
| `timeZone` | 세 함수 모두 | 실행 환경 로컬 | IANA 이름(`Asia/Seoul`·`UTC`·`America/New_York`). 잘못된 이름은 `Intl.DateTimeFormat` 의 `RangeError` |
|
|
301
|
+
| `offset` | `toWireDateTime` | `false` | offset 프로필 — 그 순간의 기준 시간대 오프셋(`Z` / `±HH:MM`) 부착 |
|
|
302
|
+
| `inboundOffset` | `parseWireDateTime` | `"drop"` | `drop` 오프셋 버리고 벽시계(현행) · `convert` 오프셋으로 정확한 순간 · `reject` `RangeError` |
|
|
303
|
+
|
|
304
|
+
- **프로필 전환 순서**: offset 프로필로 바꾸는 것은 그 서비스 API 에 breaking 이다(현행 Jackson `LocalDateTime` 리더는
|
|
305
|
+
`+09:00` 을 거부). **클라이언트가 먼저 `inboundOffset: "convert"` 를 켜고 배포**한 뒤 서버가 offset 프로필로 바꾼다.
|
|
306
|
+
클라이언트 발신에 `offset: true` 를 켜는 것도 서버 전환 이후다.
|
|
307
|
+
- `convert`·`reject` 는 RFC 3339 오프셋(`Z`·`±HH:MM`)만 인식한다 — `+0900` 같은 표기는 형식 오류(`drop` 은 현행대로 버림).
|
|
308
|
+
오프셋 없는 입력은 모든 정책에서 벽시계 그대로.
|
|
309
|
+
- 발신은 초 단위(밀리초 절단 — 현행), 수신 소수부는 밀리초까지 보존(`.123456789` → `.123`, DT-14).
|
|
310
|
+
- 시간대 계산은 `Intl.DateTimeFormat(...).formatToParts` 만 쓴다(의존성 0, 시간대별 포매터 캐시) — Intl 의 IANA 시간대
|
|
311
|
+
지원이 필요하다(Node 20+ 공식 빌드(full-ICU)·모던 브라우저는 기본 포함).
|
|
312
|
+
- 골든 벡터 DT-01~14(contracts/datetime.md)는 `timeZone` 을 명시해 실행 머신의 시간대와 무관하게 검증한다.
|
|
313
|
+
|
|
271
314
|
### 메시지 다국어 — 내장 ko(기본)·en
|
|
272
315
|
|
|
273
316
|
라이브러리가 내보내는 와이어 메시지(서버 봉투 메시지 22키 + 클라이언트 로컬 메시지 3키)를 **ko·en 으로 내장**한다.
|
|
@@ -350,6 +393,8 @@ const dec = decryptAesGcm(enc, key); // 키 상이·변조 시 CryptoError
|
|
|
350
393
|
- `isValidRrn` 은 체크섬을 포함하지 않는다(2020-10 이후 발급분은 체크섬 불성립) — `rrnChecksumOkLegacy` 는 레거시 정합 검사 전용.
|
|
351
394
|
- `signWebhook`/`verifyWebhook` 은 WebCrypto 기반이라 **async** 다 (Java/Python 은 동기).
|
|
352
395
|
- 서킷 브레이커·토큰버킷·Bulkhead·TTL 캐시는 **인스턴스(프로세스) 로컬** 상태다 — 분산 공유되지 않는다.
|
|
396
|
+
- datetime 의 `timeZone` 옵션은 `Intl.DateTimeFormat` 의 IANA 시간대 지원에 의존한다 — 지원하지 않는 런타임(시간대 데이터가
|
|
397
|
+
없는 경량 JS 엔진 등)에서는 `RangeError`. 옵션을 주지 않은 기본 경로는 `Intl` 을 쓰지 않는다.
|
|
353
398
|
- 내장 메시지 언어는 `ko`·`en` 뿐이다 — 그 외 언어는 `getMessage` 의 `overrides`(업로드는 `messages`)로 공급할 것.
|
|
354
399
|
- `@rscc/common-core/crypto` 는 `node:crypto` 를 쓰는 **Node 전용** 서브패스 — 브라우저 번들에 포함하지 말 것.
|
|
355
400
|
- 서브패스는 `exports` 맵으로만 노출된다 — TypeScript `moduleResolution` 이 레거시 `node`(`node10`)면
|
package/dist/index.cjs
CHANGED
|
@@ -1021,22 +1021,190 @@ function maskCardNumber(cardNumber) {
|
|
|
1021
1021
|
|
|
1022
1022
|
// src/datetime.ts
|
|
1023
1023
|
var pad = (n) => String(n).padStart(2, "0");
|
|
1024
|
-
|
|
1025
|
-
|
|
1024
|
+
var zoneFormatters = /* @__PURE__ */ new Map();
|
|
1025
|
+
function zoneFormatter(timeZone) {
|
|
1026
|
+
let formatter = zoneFormatters.get(timeZone);
|
|
1027
|
+
if (formatter === void 0) {
|
|
1028
|
+
formatter = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
|
|
1029
|
+
timeZone,
|
|
1030
|
+
hourCycle: "h23",
|
|
1031
|
+
year: "numeric",
|
|
1032
|
+
month: "2-digit",
|
|
1033
|
+
day: "2-digit",
|
|
1034
|
+
hour: "2-digit",
|
|
1035
|
+
minute: "2-digit",
|
|
1036
|
+
second: "2-digit"
|
|
1037
|
+
});
|
|
1038
|
+
zoneFormatters.set(timeZone, formatter);
|
|
1039
|
+
}
|
|
1040
|
+
return formatter;
|
|
1041
|
+
}
|
|
1042
|
+
function zonedFields(ms, timeZone) {
|
|
1043
|
+
const fields = {
|
|
1044
|
+
year: 0,
|
|
1045
|
+
month: 0,
|
|
1046
|
+
day: 0,
|
|
1047
|
+
hour: 0,
|
|
1048
|
+
minute: 0,
|
|
1049
|
+
second: 0,
|
|
1050
|
+
millisecond: (ms % 1e3 + 1e3) % 1e3
|
|
1051
|
+
};
|
|
1052
|
+
for (const part of zoneFormatter(timeZone).formatToParts(ms)) {
|
|
1053
|
+
switch (part.type) {
|
|
1054
|
+
case "year":
|
|
1055
|
+
fields.year = Number(part.value);
|
|
1056
|
+
break;
|
|
1057
|
+
case "month":
|
|
1058
|
+
fields.month = Number(part.value);
|
|
1059
|
+
break;
|
|
1060
|
+
case "day":
|
|
1061
|
+
fields.day = Number(part.value);
|
|
1062
|
+
break;
|
|
1063
|
+
case "hour":
|
|
1064
|
+
fields.hour = Number(part.value) % 24;
|
|
1065
|
+
break;
|
|
1066
|
+
case "minute":
|
|
1067
|
+
fields.minute = Number(part.value);
|
|
1068
|
+
break;
|
|
1069
|
+
case "second":
|
|
1070
|
+
fields.second = Number(part.value);
|
|
1071
|
+
break;
|
|
1072
|
+
default:
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return fields;
|
|
1077
|
+
}
|
|
1078
|
+
function wallAsUtcMs(f) {
|
|
1079
|
+
const d = /* @__PURE__ */ new Date(0);
|
|
1080
|
+
d.setUTCFullYear(f.year, f.month - 1, f.day);
|
|
1081
|
+
d.setUTCHours(f.hour, f.minute, f.second, f.millisecond);
|
|
1082
|
+
return d.getTime();
|
|
1083
|
+
}
|
|
1084
|
+
function offsetMinutes(ms, timeZone) {
|
|
1085
|
+
const wall = zonedFields(ms, timeZone);
|
|
1086
|
+
wall.millisecond = 0;
|
|
1087
|
+
return (wallAsUtcMs(wall) - Math.floor(ms / 1e3) * 1e3) / 6e4;
|
|
1088
|
+
}
|
|
1089
|
+
var DAY_MS = 864e5;
|
|
1090
|
+
function wallToEpoch(fields, timeZone) {
|
|
1091
|
+
const wall = wallAsUtcMs(fields);
|
|
1092
|
+
const toMs = (minutes) => Math.round(minutes * 6e4);
|
|
1093
|
+
const before = offsetMinutes(wall - DAY_MS, timeZone);
|
|
1094
|
+
const after = offsetMinutes(wall + DAY_MS, timeZone);
|
|
1095
|
+
const atBefore = wall - toMs(before);
|
|
1096
|
+
if (offsetMinutes(atBefore, timeZone) === before) return atBefore;
|
|
1097
|
+
const atAfter = wall - toMs(after);
|
|
1098
|
+
if (offsetMinutes(atAfter, timeZone) === after) return atAfter;
|
|
1099
|
+
return atBefore;
|
|
1100
|
+
}
|
|
1101
|
+
function formatOffset(minutes) {
|
|
1102
|
+
const totalSeconds = Math.round(minutes * 60);
|
|
1103
|
+
if (totalSeconds === 0) return "Z";
|
|
1104
|
+
const sign = totalSeconds < 0 ? "-" : "+";
|
|
1105
|
+
const abs = Math.abs(totalSeconds);
|
|
1106
|
+
const hh = pad(Math.floor(abs / 3600));
|
|
1107
|
+
const mm = pad(Math.floor(abs % 3600 / 60));
|
|
1108
|
+
const ss = abs % 60;
|
|
1109
|
+
return `${sign}${hh}:${mm}${ss === 0 ? "" : `:${pad(ss)}`}`;
|
|
1110
|
+
}
|
|
1111
|
+
var formatWallDate = (f) => `${f.year}-${pad(f.month)}-${pad(f.day)}`;
|
|
1112
|
+
var formatWallDateTime = (f) => `${formatWallDate(f)}T${pad(f.hour)}:${pad(f.minute)}:${pad(f.second)}`;
|
|
1113
|
+
function assertValidDate(date) {
|
|
1114
|
+
const ms = date.getTime();
|
|
1115
|
+
if (Number.isNaN(ms)) throw new RangeError("Invalid time value");
|
|
1116
|
+
return ms;
|
|
1117
|
+
}
|
|
1118
|
+
function toWireDateTime(date, options) {
|
|
1119
|
+
const timeZone = options?.timeZone;
|
|
1120
|
+
const withOffset = options?.offset === true;
|
|
1121
|
+
if (timeZone === void 0) {
|
|
1122
|
+
const wall2 = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1123
|
+
if (!withOffset) return wall2;
|
|
1124
|
+
assertValidDate(date);
|
|
1125
|
+
return wall2 + formatOffset(-date.getTimezoneOffset());
|
|
1126
|
+
}
|
|
1127
|
+
const ms = assertValidDate(date);
|
|
1128
|
+
const wall = formatWallDateTime(zonedFields(ms, timeZone));
|
|
1129
|
+
return withOffset ? wall + formatOffset(offsetMinutes(ms, timeZone)) : wall;
|
|
1130
|
+
}
|
|
1131
|
+
function toWireDate(date, options) {
|
|
1132
|
+
const timeZone = options?.timeZone;
|
|
1133
|
+
if (timeZone === void 0) {
|
|
1134
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1135
|
+
}
|
|
1136
|
+
return formatWallDate(zonedFields(assertValidDate(date), timeZone));
|
|
1137
|
+
}
|
|
1138
|
+
var WALL_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d+))?$/;
|
|
1139
|
+
function parseWallText(text, original) {
|
|
1140
|
+
const m = text.match(WALL_PATTERN);
|
|
1141
|
+
if (!m) throw new Error(`\uC62C\uBC14\uB978 datetime \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${original}`);
|
|
1142
|
+
const [, y, mo, day, h, mi, s = "0", frac = "0"] = m;
|
|
1143
|
+
return {
|
|
1144
|
+
year: Number(y),
|
|
1145
|
+
month: Number(mo),
|
|
1146
|
+
day: Number(day),
|
|
1147
|
+
hour: Number(h),
|
|
1148
|
+
minute: Number(mi),
|
|
1149
|
+
second: Number(s),
|
|
1150
|
+
millisecond: Number((frac + "000").slice(0, 3))
|
|
1151
|
+
// JS Date 는 ms 정밀도까지만
|
|
1152
|
+
};
|
|
1026
1153
|
}
|
|
1027
|
-
function
|
|
1028
|
-
|
|
1154
|
+
function assertValidWall(f, original) {
|
|
1155
|
+
const d = new Date(wallAsUtcMs(f));
|
|
1156
|
+
if (d.getUTCFullYear() !== f.year || d.getUTCMonth() !== f.month - 1 || d.getUTCDate() !== f.day || d.getUTCHours() !== f.hour || d.getUTCMinutes() !== f.minute || d.getUTCSeconds() !== f.second) {
|
|
1157
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 datetime \uAC12\uC785\uB2C8\uB2E4: ${original}`);
|
|
1158
|
+
}
|
|
1029
1159
|
}
|
|
1030
|
-
function
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
);
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1160
|
+
function matchRfc3339Offset(value) {
|
|
1161
|
+
const tIndex = value.indexOf("T");
|
|
1162
|
+
if (tIndex === -1) return null;
|
|
1163
|
+
const m = value.slice(tIndex).match(/(?:[Zz]|([+-])(\d{2}):(\d{2}))$/);
|
|
1164
|
+
if (!m || m.index === void 0) return null;
|
|
1165
|
+
const [token, sign, hh, mm] = m;
|
|
1166
|
+
const index = value.length - token.length;
|
|
1167
|
+
if (sign === void 0) return { index, minutes: 0 };
|
|
1168
|
+
const hours = Number(hh);
|
|
1169
|
+
const minutes = Number(mm);
|
|
1170
|
+
if (hours > 23 || minutes > 59) {
|
|
1171
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC624\uD504\uC14B\uC785\uB2C8\uB2E4: ${value}`);
|
|
1172
|
+
}
|
|
1173
|
+
return { index, minutes: (sign === "-" ? -1 : 1) * (hours * 60 + minutes) };
|
|
1174
|
+
}
|
|
1175
|
+
function parseWireDateTime(value, options) {
|
|
1176
|
+
const timeZone = options?.timeZone;
|
|
1177
|
+
const policy = options?.inboundOffset ?? "drop";
|
|
1178
|
+
if (policy !== "drop" && policy !== "convert" && policy !== "reject") {
|
|
1179
|
+
throw new RangeError(`\uC54C \uC218 \uC5C6\uB294 inboundOffset \uC815\uCC45\uC785\uB2C8\uB2E4: ${String(policy)}`);
|
|
1180
|
+
}
|
|
1181
|
+
if (timeZone !== void 0) zoneFormatter(timeZone);
|
|
1182
|
+
const trimmed = value.trim();
|
|
1183
|
+
let wallText = trimmed;
|
|
1184
|
+
let offset;
|
|
1185
|
+
if (policy === "drop") {
|
|
1186
|
+
wallText = stripZone(trimmed);
|
|
1187
|
+
} else {
|
|
1188
|
+
const found = matchRfc3339Offset(trimmed);
|
|
1189
|
+
if (found) {
|
|
1190
|
+
if (policy === "reject") {
|
|
1191
|
+
throw new RangeError(`\uC624\uD504\uC14B\uC774 \uBD99\uC740 datetime \uC740 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(inboundOffset: "reject"): ${value}`);
|
|
1192
|
+
}
|
|
1193
|
+
offset = found.minutes;
|
|
1194
|
+
wallText = trimmed.slice(0, found.index);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const f = parseWallText(wallText, value);
|
|
1198
|
+
if (offset !== void 0) {
|
|
1199
|
+
assertValidWall(f, value);
|
|
1200
|
+
return new Date(wallAsUtcMs(f) - offset * 6e4);
|
|
1201
|
+
}
|
|
1202
|
+
if (timeZone !== void 0) {
|
|
1203
|
+
assertValidWall(f, value);
|
|
1204
|
+
return new Date(wallToEpoch(f, timeZone));
|
|
1205
|
+
}
|
|
1206
|
+
const date = new Date(f.year, f.month - 1, f.day, f.hour, f.minute, f.second, f.millisecond);
|
|
1207
|
+
if (date.getFullYear() !== f.year || date.getMonth() !== f.month - 1 || date.getDate() !== f.day || date.getHours() !== f.hour || date.getMinutes() !== f.minute || date.getSeconds() !== f.second) {
|
|
1040
1208
|
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 datetime \uAC12\uC785\uB2C8\uB2E4: ${value}`);
|
|
1041
1209
|
}
|
|
1042
1210
|
return date;
|
package/dist/index.d.cts
CHANGED
|
@@ -823,30 +823,93 @@ declare function maskEmail(email: string): string;
|
|
|
823
823
|
declare function maskCardNumber(cardNumber: string): string;
|
|
824
824
|
|
|
825
825
|
/**
|
|
826
|
-
*
|
|
826
|
+
* LocalDateTime 와이어 직렬화 헬퍼 — contracts/datetime.md.
|
|
827
827
|
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
828
|
+
* 두 프로필:
|
|
829
|
+
* - **local 프로필(기본·현행)** — 와이어에 시간대/오프셋을 싣지 않는다(`yyyy-MM-ddTHH:mm:ss`).
|
|
830
|
+
* 값은 서버 기준(운영 전제: KST) 벽시계 시각으로 해석된다. 옵션을 주지 않으면 항상 이 동작이다.
|
|
831
|
+
* - **offset 프로필(opt-in)** — `yyyy-MM-ddTHH:mm:ss(Z|±HH:MM)`. 오프셋은 그 순간의 기준 시간대
|
|
832
|
+
* 오프셋이며 0 오프셋은 `Z`. `toWireDateTime(date, { offset: true })` 로 켠다.
|
|
833
|
+
*
|
|
834
|
+
* 기준 시간대(wire zone)는 기본이 **실행 환경의 로컬 시간대**(브라우저/Node 로컬 게터 — 현행)이고,
|
|
835
|
+
* `timeZone`(IANA 이름: `Asia/Seoul`, `UTC`, `America/New_York`)으로 고정할 수 있다. 시간대 계산은
|
|
836
|
+
* `Intl.DateTimeFormat(...).formatToParts` 만 쓴다(런타임 의존성 0 — Node full-ICU / 모던 브라우저 전제).
|
|
837
|
+
* 잘못된 `timeZone` 은 `Intl.DateTimeFormat` 이 던지는 `RangeError` 를 그대로 전파한다.
|
|
830
838
|
*
|
|
831
839
|
* ⚠️ Z 스큐 경고(datetime.md): `new Date(x).toISOString()` 은 UTC 로 변환해 끝에
|
|
832
840
|
* `Z` 를 붙인다. 서버(`LocalDateTime`)는 그 `Z` 를 조용히 버리고 나머지를 KST 벽시계로
|
|
833
|
-
* 재해석해 **9시간 스큐**를 만든다. 이 모듈의 직렬화 함수는 오프셋을
|
|
841
|
+
* 재해석해 **9시간 스큐**를 만든다. 이 모듈의 직렬화 함수는 기본적으로 오프셋을 만들지 않아
|
|
834
842
|
* 그 경로를 원천 차단한다. `<input type="datetime-local">` 값은 이미 오프셋 없는
|
|
835
843
|
* 형식이므로 그대로 보내면 되고, Date 객체를 보내야 할 땐 {@link toWireDateTime} 을 쓴다.
|
|
844
|
+
* offset 프로필(`offset: true`)은 **서버가 offset 프로필로 전환한 서비스에서만** 쓸 것.
|
|
836
845
|
*/
|
|
846
|
+
/** {@link toWireDateTime} 옵션 — 모두 opt-in, 생략 시 현행(로컬 게터·오프셋 없음). */
|
|
847
|
+
interface WireDateTimeFormatOptions {
|
|
848
|
+
/**
|
|
849
|
+
* 기준 시간대(IANA 이름, 예: `"Asia/Seoul"`, `"UTC"`). 지정하면 그 순간을 이 시간대의 벽시계로 직렬화한다.
|
|
850
|
+
* 생략 시 실행 환경의 로컬 시간대(현행). 잘못된 이름은 `RangeError`.
|
|
851
|
+
*/
|
|
852
|
+
timeZone?: string;
|
|
853
|
+
/**
|
|
854
|
+
* `true` 면 offset 프로필 — 그 순간의 기준 시간대 오프셋을 붙인다(0 은 `Z`, 그 외 `±HH:MM`).
|
|
855
|
+
* `timeZone` 없이 켜면 호스트 오프셋(`-date.getTimezoneOffset()`)을 쓴다. 기본 `false`(현행).
|
|
856
|
+
*/
|
|
857
|
+
offset?: boolean;
|
|
858
|
+
}
|
|
859
|
+
/** {@link toWireDate} 옵션 — 생략 시 현행(로컬 날짜). */
|
|
860
|
+
interface WireDateFormatOptions {
|
|
861
|
+
/** 기준 시간대(IANA 이름). 지정하면 그 순간의 이 시간대 날짜를 직렬화한다. 잘못된 이름은 `RangeError`. */
|
|
862
|
+
timeZone?: string;
|
|
863
|
+
}
|
|
837
864
|
/**
|
|
838
|
-
*
|
|
839
|
-
* `
|
|
865
|
+
* 수신 오프셋 정책 — 오프셋이 붙은 문자열을 받았을 때의 동작(contracts/datetime.md §수신 오프셋 정책).
|
|
866
|
+
* - `"drop"`(기본·현행): 오프셋(`Z`·`±HH:MM`·`±HHMM`)을 버리고 나머지를 벽시계로 해석 — Z 스큐 그대로.
|
|
867
|
+
* - `"convert"`: 오프셋으로 정확한 순간을 계산(`timeZone` 무관). RFC 3339 오프셋(`Z`·`±HH:MM`)만 인식.
|
|
868
|
+
* - `"reject"`: RFC 3339 오프셋이 붙어 있으면 `RangeError`.
|
|
840
869
|
*/
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
|
|
870
|
+
type InboundOffsetPolicy = "drop" | "convert" | "reject";
|
|
871
|
+
/** {@link parseWireDateTime} 옵션 — 모두 opt-in, 생략 시 현행(로컬 해석·오프셋 drop). */
|
|
872
|
+
interface WireDateTimeParseOptions {
|
|
873
|
+
/**
|
|
874
|
+
* 기준 시간대(IANA 이름). 오프셋 없는 벽시계를 이 시간대 기준으로 해석한다(DST 갭은 갭 길이만큼 뒤로,
|
|
875
|
+
* 겹침은 이른 오프셋). 생략 시 실행 환경의 로컬 시간대(현행). 잘못된 이름은 `RangeError`.
|
|
876
|
+
*/
|
|
877
|
+
timeZone?: string;
|
|
878
|
+
/** 수신 오프셋 정책. 기본 `"drop"`(현행). */
|
|
879
|
+
inboundOffset?: InboundOffsetPolicy;
|
|
880
|
+
}
|
|
844
881
|
/**
|
|
845
|
-
* 와이어 datetime
|
|
846
|
-
*
|
|
847
|
-
*
|
|
882
|
+
* Date 를 와이어 datetime(초 단위)으로 직렬화한다. `toISOString()`(UTC/Z) 대신 이 함수를 쓸 것.
|
|
883
|
+
*
|
|
884
|
+
* - 옵션 없음(기본·현행): **로컬** 필드를 `yyyy-MM-ddTHH:mm:ss`(오프셋 없음)로 — 스큐가 생기지 않는다.
|
|
885
|
+
* - `timeZone`: 그 순간을 해당 IANA 시간대의 벽시계로 직렬화.
|
|
886
|
+
* - `offset: true`: offset 프로필 — 그 순간의 기준 시간대 오프셋을 붙인다(`Z` / `±HH:MM`).
|
|
887
|
+
* `timeZone` 이 없으면 호스트 오프셋(`-date.getTimezoneOffset()`).
|
|
888
|
+
*
|
|
889
|
+
* @throws RangeError 잘못된 `timeZone`, 또는 시간대/오프셋 옵션과 함께 준 잘못된 Date(Invalid Date)
|
|
890
|
+
*/
|
|
891
|
+
declare function toWireDateTime(date: Date, options?: WireDateTimeFormatOptions): string;
|
|
892
|
+
/**
|
|
893
|
+
* Date 의 날짜를 `yyyy-MM-dd` 로 직렬화한다. 기본은 로컬 날짜(현행), `timeZone` 을 주면 그 순간의
|
|
894
|
+
* 해당 시간대 날짜.
|
|
895
|
+
*
|
|
896
|
+
* @throws RangeError 잘못된 `timeZone`, 또는 `timeZone` 과 함께 준 잘못된 Date
|
|
897
|
+
*/
|
|
898
|
+
declare function toWireDate(date: Date, options?: WireDateFormatOptions): string;
|
|
899
|
+
/**
|
|
900
|
+
* 와이어 datetime 문자열(`yyyy-MM-ddTHH:mm[:ss[.fff…]]`, 소수부는 밀리초까지 보존)을 Date 로 파싱한다.
|
|
901
|
+
*
|
|
902
|
+
* - 옵션 없음(기본·현행): **로컬 시간대**로 해석(사용자·서버가 모두 KST 라는 전제). 끝의 `Z`/오프셋은
|
|
903
|
+
* 무시하고 벽시계 숫자만 취한다(Java Jackson lenient 파리티). 로컬 DST 갭 시각은 throw(현행).
|
|
904
|
+
* - `timeZone`: 오프셋 없는 벽시계를 그 IANA 시간대로 해석한다 — DST 갭은 갭 길이만큼 뒤로, 겹침은 이른 오프셋.
|
|
905
|
+
* - `inboundOffset`: 오프셋이 붙은 입력의 정책 — `"drop"`(기본·현행) / `"convert"`(오프셋으로 정확한 순간,
|
|
906
|
+
* `timeZone` 무관) / `"reject"`(`RangeError`). `convert`·`reject` 는 RFC 3339 오프셋(`Z`·`±HH:MM`)만
|
|
907
|
+
* 인식하므로 `+0900` 같은 다른 표기는 형식 오류가 된다. 오프셋 없는 입력은 모든 정책에서 벽시계 그대로.
|
|
908
|
+
*
|
|
909
|
+
* @throws Error 형식 불일치·범위를 벗어난 값(25시·2월 30일 등)·잘못된 오프셋
|
|
910
|
+
* @throws RangeError 잘못된 `timeZone`·알 수 없는 `inboundOffset`, `"reject"` 정책에서 오프셋이 붙은 입력
|
|
848
911
|
*/
|
|
849
|
-
declare function parseWireDateTime(value: string): Date;
|
|
912
|
+
declare function parseWireDateTime(value: string, options?: WireDateTimeParseOptions): Date;
|
|
850
913
|
/**
|
|
851
914
|
* 문자열 끝의 `Z` 또는 `±HH:MM`/`±HHMM` 오프셋을 제거한다(발신 전 sanitize).
|
|
852
915
|
* 시각 자체는 변환하지 않고 벽시계 부분만 남긴다. 오프셋이 없으면 원본을 그대로 반환.
|
|
@@ -1792,4 +1855,4 @@ declare function composeHangul(s: string): string;
|
|
|
1792
1855
|
*/
|
|
1793
1856
|
declare function matchesHangul(query: string, target: string): boolean;
|
|
1794
1857
|
|
|
1795
|
-
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BuiltinLanguage, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, CircuitOpenError, type CircuitState, type CommonResponse, type CsrfOptions, DEFAULT_CSRF_COOKIE_NAME, DEFAULT_CSRF_HEADER_NAME, type ErrorCodeValue, FIXED_MESSAGE_KEYS, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type FormatSseEventOptions, type GetMessageOptions, type JosaPair, type ListQueryOptions, MESSAGES, type MessageKey, type MessageOverrides, type PageResponse, type PhoneType, type ReadSseEventsOptions, type ReadSseEventsResult, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseEvent, type SseEventParser, type SseEventParserOptions, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadMessageKey, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createSseEventParser, createTokenBucket, createTtlCache, csrfHeaderFor, decodeJwtPayload, decomposeHangul, formatMessage, formatPhoneNumber, formatSseComment, formatSseEvent, generateIdempotencyKey, getMessage, getTokenExpiry, isBulkResult, isChosungQuery, isCommonResultCode, isForeignerRrn, isRetryableStatus, isTokenExpired, isUnsafeMethod, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, negotiateLanguage, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseChatPayload, parseFlag, parseRetryAfterMs, parseSseEvents, parseSseFrame, parseWireDateTime, pickJosa, readCookie, readSseChatEvents, readSseEvents, readSseStream, resultCodeForStatus, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toCommonResultCode, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|
|
1858
|
+
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BuiltinLanguage, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, CircuitOpenError, type CircuitState, type CommonResponse, type CsrfOptions, DEFAULT_CSRF_COOKIE_NAME, DEFAULT_CSRF_HEADER_NAME, type ErrorCodeValue, FIXED_MESSAGE_KEYS, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type FormatSseEventOptions, type GetMessageOptions, type InboundOffsetPolicy, type JosaPair, type ListQueryOptions, MESSAGES, type MessageKey, type MessageOverrides, type PageResponse, type PhoneType, type ReadSseEventsOptions, type ReadSseEventsResult, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseEvent, type SseEventParser, type SseEventParserOptions, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadMessageKey, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, type WireDateFormatOptions, type WireDateTimeFormatOptions, type WireDateTimeParseOptions, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createSseEventParser, createTokenBucket, createTtlCache, csrfHeaderFor, decodeJwtPayload, decomposeHangul, formatMessage, formatPhoneNumber, formatSseComment, formatSseEvent, generateIdempotencyKey, getMessage, getTokenExpiry, isBulkResult, isChosungQuery, isCommonResultCode, isForeignerRrn, isRetryableStatus, isTokenExpired, isUnsafeMethod, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, negotiateLanguage, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseChatPayload, parseFlag, parseRetryAfterMs, parseSseEvents, parseSseFrame, parseWireDateTime, pickJosa, readCookie, readSseChatEvents, readSseEvents, readSseStream, resultCodeForStatus, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toCommonResultCode, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -823,30 +823,93 @@ declare function maskEmail(email: string): string;
|
|
|
823
823
|
declare function maskCardNumber(cardNumber: string): string;
|
|
824
824
|
|
|
825
825
|
/**
|
|
826
|
-
*
|
|
826
|
+
* LocalDateTime 와이어 직렬화 헬퍼 — contracts/datetime.md.
|
|
827
827
|
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
828
|
+
* 두 프로필:
|
|
829
|
+
* - **local 프로필(기본·현행)** — 와이어에 시간대/오프셋을 싣지 않는다(`yyyy-MM-ddTHH:mm:ss`).
|
|
830
|
+
* 값은 서버 기준(운영 전제: KST) 벽시계 시각으로 해석된다. 옵션을 주지 않으면 항상 이 동작이다.
|
|
831
|
+
* - **offset 프로필(opt-in)** — `yyyy-MM-ddTHH:mm:ss(Z|±HH:MM)`. 오프셋은 그 순간의 기준 시간대
|
|
832
|
+
* 오프셋이며 0 오프셋은 `Z`. `toWireDateTime(date, { offset: true })` 로 켠다.
|
|
833
|
+
*
|
|
834
|
+
* 기준 시간대(wire zone)는 기본이 **실행 환경의 로컬 시간대**(브라우저/Node 로컬 게터 — 현행)이고,
|
|
835
|
+
* `timeZone`(IANA 이름: `Asia/Seoul`, `UTC`, `America/New_York`)으로 고정할 수 있다. 시간대 계산은
|
|
836
|
+
* `Intl.DateTimeFormat(...).formatToParts` 만 쓴다(런타임 의존성 0 — Node full-ICU / 모던 브라우저 전제).
|
|
837
|
+
* 잘못된 `timeZone` 은 `Intl.DateTimeFormat` 이 던지는 `RangeError` 를 그대로 전파한다.
|
|
830
838
|
*
|
|
831
839
|
* ⚠️ Z 스큐 경고(datetime.md): `new Date(x).toISOString()` 은 UTC 로 변환해 끝에
|
|
832
840
|
* `Z` 를 붙인다. 서버(`LocalDateTime`)는 그 `Z` 를 조용히 버리고 나머지를 KST 벽시계로
|
|
833
|
-
* 재해석해 **9시간 스큐**를 만든다. 이 모듈의 직렬화 함수는 오프셋을
|
|
841
|
+
* 재해석해 **9시간 스큐**를 만든다. 이 모듈의 직렬화 함수는 기본적으로 오프셋을 만들지 않아
|
|
834
842
|
* 그 경로를 원천 차단한다. `<input type="datetime-local">` 값은 이미 오프셋 없는
|
|
835
843
|
* 형식이므로 그대로 보내면 되고, Date 객체를 보내야 할 땐 {@link toWireDateTime} 을 쓴다.
|
|
844
|
+
* offset 프로필(`offset: true`)은 **서버가 offset 프로필로 전환한 서비스에서만** 쓸 것.
|
|
836
845
|
*/
|
|
846
|
+
/** {@link toWireDateTime} 옵션 — 모두 opt-in, 생략 시 현행(로컬 게터·오프셋 없음). */
|
|
847
|
+
interface WireDateTimeFormatOptions {
|
|
848
|
+
/**
|
|
849
|
+
* 기준 시간대(IANA 이름, 예: `"Asia/Seoul"`, `"UTC"`). 지정하면 그 순간을 이 시간대의 벽시계로 직렬화한다.
|
|
850
|
+
* 생략 시 실행 환경의 로컬 시간대(현행). 잘못된 이름은 `RangeError`.
|
|
851
|
+
*/
|
|
852
|
+
timeZone?: string;
|
|
853
|
+
/**
|
|
854
|
+
* `true` 면 offset 프로필 — 그 순간의 기준 시간대 오프셋을 붙인다(0 은 `Z`, 그 외 `±HH:MM`).
|
|
855
|
+
* `timeZone` 없이 켜면 호스트 오프셋(`-date.getTimezoneOffset()`)을 쓴다. 기본 `false`(현행).
|
|
856
|
+
*/
|
|
857
|
+
offset?: boolean;
|
|
858
|
+
}
|
|
859
|
+
/** {@link toWireDate} 옵션 — 생략 시 현행(로컬 날짜). */
|
|
860
|
+
interface WireDateFormatOptions {
|
|
861
|
+
/** 기준 시간대(IANA 이름). 지정하면 그 순간의 이 시간대 날짜를 직렬화한다. 잘못된 이름은 `RangeError`. */
|
|
862
|
+
timeZone?: string;
|
|
863
|
+
}
|
|
837
864
|
/**
|
|
838
|
-
*
|
|
839
|
-
* `
|
|
865
|
+
* 수신 오프셋 정책 — 오프셋이 붙은 문자열을 받았을 때의 동작(contracts/datetime.md §수신 오프셋 정책).
|
|
866
|
+
* - `"drop"`(기본·현행): 오프셋(`Z`·`±HH:MM`·`±HHMM`)을 버리고 나머지를 벽시계로 해석 — Z 스큐 그대로.
|
|
867
|
+
* - `"convert"`: 오프셋으로 정확한 순간을 계산(`timeZone` 무관). RFC 3339 오프셋(`Z`·`±HH:MM`)만 인식.
|
|
868
|
+
* - `"reject"`: RFC 3339 오프셋이 붙어 있으면 `RangeError`.
|
|
840
869
|
*/
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
|
|
870
|
+
type InboundOffsetPolicy = "drop" | "convert" | "reject";
|
|
871
|
+
/** {@link parseWireDateTime} 옵션 — 모두 opt-in, 생략 시 현행(로컬 해석·오프셋 drop). */
|
|
872
|
+
interface WireDateTimeParseOptions {
|
|
873
|
+
/**
|
|
874
|
+
* 기준 시간대(IANA 이름). 오프셋 없는 벽시계를 이 시간대 기준으로 해석한다(DST 갭은 갭 길이만큼 뒤로,
|
|
875
|
+
* 겹침은 이른 오프셋). 생략 시 실행 환경의 로컬 시간대(현행). 잘못된 이름은 `RangeError`.
|
|
876
|
+
*/
|
|
877
|
+
timeZone?: string;
|
|
878
|
+
/** 수신 오프셋 정책. 기본 `"drop"`(현행). */
|
|
879
|
+
inboundOffset?: InboundOffsetPolicy;
|
|
880
|
+
}
|
|
844
881
|
/**
|
|
845
|
-
* 와이어 datetime
|
|
846
|
-
*
|
|
847
|
-
*
|
|
882
|
+
* Date 를 와이어 datetime(초 단위)으로 직렬화한다. `toISOString()`(UTC/Z) 대신 이 함수를 쓸 것.
|
|
883
|
+
*
|
|
884
|
+
* - 옵션 없음(기본·현행): **로컬** 필드를 `yyyy-MM-ddTHH:mm:ss`(오프셋 없음)로 — 스큐가 생기지 않는다.
|
|
885
|
+
* - `timeZone`: 그 순간을 해당 IANA 시간대의 벽시계로 직렬화.
|
|
886
|
+
* - `offset: true`: offset 프로필 — 그 순간의 기준 시간대 오프셋을 붙인다(`Z` / `±HH:MM`).
|
|
887
|
+
* `timeZone` 이 없으면 호스트 오프셋(`-date.getTimezoneOffset()`).
|
|
888
|
+
*
|
|
889
|
+
* @throws RangeError 잘못된 `timeZone`, 또는 시간대/오프셋 옵션과 함께 준 잘못된 Date(Invalid Date)
|
|
890
|
+
*/
|
|
891
|
+
declare function toWireDateTime(date: Date, options?: WireDateTimeFormatOptions): string;
|
|
892
|
+
/**
|
|
893
|
+
* Date 의 날짜를 `yyyy-MM-dd` 로 직렬화한다. 기본은 로컬 날짜(현행), `timeZone` 을 주면 그 순간의
|
|
894
|
+
* 해당 시간대 날짜.
|
|
895
|
+
*
|
|
896
|
+
* @throws RangeError 잘못된 `timeZone`, 또는 `timeZone` 과 함께 준 잘못된 Date
|
|
897
|
+
*/
|
|
898
|
+
declare function toWireDate(date: Date, options?: WireDateFormatOptions): string;
|
|
899
|
+
/**
|
|
900
|
+
* 와이어 datetime 문자열(`yyyy-MM-ddTHH:mm[:ss[.fff…]]`, 소수부는 밀리초까지 보존)을 Date 로 파싱한다.
|
|
901
|
+
*
|
|
902
|
+
* - 옵션 없음(기본·현행): **로컬 시간대**로 해석(사용자·서버가 모두 KST 라는 전제). 끝의 `Z`/오프셋은
|
|
903
|
+
* 무시하고 벽시계 숫자만 취한다(Java Jackson lenient 파리티). 로컬 DST 갭 시각은 throw(현행).
|
|
904
|
+
* - `timeZone`: 오프셋 없는 벽시계를 그 IANA 시간대로 해석한다 — DST 갭은 갭 길이만큼 뒤로, 겹침은 이른 오프셋.
|
|
905
|
+
* - `inboundOffset`: 오프셋이 붙은 입력의 정책 — `"drop"`(기본·현행) / `"convert"`(오프셋으로 정확한 순간,
|
|
906
|
+
* `timeZone` 무관) / `"reject"`(`RangeError`). `convert`·`reject` 는 RFC 3339 오프셋(`Z`·`±HH:MM`)만
|
|
907
|
+
* 인식하므로 `+0900` 같은 다른 표기는 형식 오류가 된다. 오프셋 없는 입력은 모든 정책에서 벽시계 그대로.
|
|
908
|
+
*
|
|
909
|
+
* @throws Error 형식 불일치·범위를 벗어난 값(25시·2월 30일 등)·잘못된 오프셋
|
|
910
|
+
* @throws RangeError 잘못된 `timeZone`·알 수 없는 `inboundOffset`, `"reject"` 정책에서 오프셋이 붙은 입력
|
|
848
911
|
*/
|
|
849
|
-
declare function parseWireDateTime(value: string): Date;
|
|
912
|
+
declare function parseWireDateTime(value: string, options?: WireDateTimeParseOptions): Date;
|
|
850
913
|
/**
|
|
851
914
|
* 문자열 끝의 `Z` 또는 `±HH:MM`/`±HHMM` 오프셋을 제거한다(발신 전 sanitize).
|
|
852
915
|
* 시각 자체는 변환하지 않고 벽시계 부분만 남긴다. 오프셋이 없으면 원본을 그대로 반환.
|
|
@@ -1792,4 +1855,4 @@ declare function composeHangul(s: string): string;
|
|
|
1792
1855
|
*/
|
|
1793
1856
|
declare function matchesHangul(query: string, target: string): boolean;
|
|
1794
1857
|
|
|
1795
|
-
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BuiltinLanguage, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, CircuitOpenError, type CircuitState, type CommonResponse, type CsrfOptions, DEFAULT_CSRF_COOKIE_NAME, DEFAULT_CSRF_HEADER_NAME, type ErrorCodeValue, FIXED_MESSAGE_KEYS, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type FormatSseEventOptions, type GetMessageOptions, type JosaPair, type ListQueryOptions, MESSAGES, type MessageKey, type MessageOverrides, type PageResponse, type PhoneType, type ReadSseEventsOptions, type ReadSseEventsResult, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseEvent, type SseEventParser, type SseEventParserOptions, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadMessageKey, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createSseEventParser, createTokenBucket, createTtlCache, csrfHeaderFor, decodeJwtPayload, decomposeHangul, formatMessage, formatPhoneNumber, formatSseComment, formatSseEvent, generateIdempotencyKey, getMessage, getTokenExpiry, isBulkResult, isChosungQuery, isCommonResultCode, isForeignerRrn, isRetryableStatus, isTokenExpired, isUnsafeMethod, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, negotiateLanguage, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseChatPayload, parseFlag, parseRetryAfterMs, parseSseEvents, parseSseFrame, parseWireDateTime, pickJosa, readCookie, readSseChatEvents, readSseEvents, readSseStream, resultCodeForStatus, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toCommonResultCode, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|
|
1858
|
+
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BuiltinLanguage, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, CircuitOpenError, type CircuitState, type CommonResponse, type CsrfOptions, DEFAULT_CSRF_COOKIE_NAME, DEFAULT_CSRF_HEADER_NAME, type ErrorCodeValue, FIXED_MESSAGE_KEYS, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type FormatSseEventOptions, type GetMessageOptions, type InboundOffsetPolicy, type JosaPair, type ListQueryOptions, MESSAGES, type MessageKey, type MessageOverrides, type PageResponse, type PhoneType, type ReadSseEventsOptions, type ReadSseEventsResult, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseEvent, type SseEventParser, type SseEventParserOptions, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadMessageKey, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, type WireDateFormatOptions, type WireDateTimeFormatOptions, type WireDateTimeParseOptions, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createSseEventParser, createTokenBucket, createTtlCache, csrfHeaderFor, decodeJwtPayload, decomposeHangul, formatMessage, formatPhoneNumber, formatSseComment, formatSseEvent, generateIdempotencyKey, getMessage, getTokenExpiry, isBulkResult, isChosungQuery, isCommonResultCode, isForeignerRrn, isRetryableStatus, isTokenExpired, isUnsafeMethod, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, negotiateLanguage, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseChatPayload, parseFlag, parseRetryAfterMs, parseSseEvents, parseSseFrame, parseWireDateTime, pickJosa, readCookie, readSseChatEvents, readSseEvents, readSseStream, resultCodeForStatus, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toCommonResultCode, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|
package/dist/index.js
CHANGED
|
@@ -909,22 +909,190 @@ function maskCardNumber(cardNumber) {
|
|
|
909
909
|
|
|
910
910
|
// src/datetime.ts
|
|
911
911
|
var pad = (n) => String(n).padStart(2, "0");
|
|
912
|
-
|
|
913
|
-
|
|
912
|
+
var zoneFormatters = /* @__PURE__ */ new Map();
|
|
913
|
+
function zoneFormatter(timeZone) {
|
|
914
|
+
let formatter = zoneFormatters.get(timeZone);
|
|
915
|
+
if (formatter === void 0) {
|
|
916
|
+
formatter = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
|
|
917
|
+
timeZone,
|
|
918
|
+
hourCycle: "h23",
|
|
919
|
+
year: "numeric",
|
|
920
|
+
month: "2-digit",
|
|
921
|
+
day: "2-digit",
|
|
922
|
+
hour: "2-digit",
|
|
923
|
+
minute: "2-digit",
|
|
924
|
+
second: "2-digit"
|
|
925
|
+
});
|
|
926
|
+
zoneFormatters.set(timeZone, formatter);
|
|
927
|
+
}
|
|
928
|
+
return formatter;
|
|
929
|
+
}
|
|
930
|
+
function zonedFields(ms, timeZone) {
|
|
931
|
+
const fields = {
|
|
932
|
+
year: 0,
|
|
933
|
+
month: 0,
|
|
934
|
+
day: 0,
|
|
935
|
+
hour: 0,
|
|
936
|
+
minute: 0,
|
|
937
|
+
second: 0,
|
|
938
|
+
millisecond: (ms % 1e3 + 1e3) % 1e3
|
|
939
|
+
};
|
|
940
|
+
for (const part of zoneFormatter(timeZone).formatToParts(ms)) {
|
|
941
|
+
switch (part.type) {
|
|
942
|
+
case "year":
|
|
943
|
+
fields.year = Number(part.value);
|
|
944
|
+
break;
|
|
945
|
+
case "month":
|
|
946
|
+
fields.month = Number(part.value);
|
|
947
|
+
break;
|
|
948
|
+
case "day":
|
|
949
|
+
fields.day = Number(part.value);
|
|
950
|
+
break;
|
|
951
|
+
case "hour":
|
|
952
|
+
fields.hour = Number(part.value) % 24;
|
|
953
|
+
break;
|
|
954
|
+
case "minute":
|
|
955
|
+
fields.minute = Number(part.value);
|
|
956
|
+
break;
|
|
957
|
+
case "second":
|
|
958
|
+
fields.second = Number(part.value);
|
|
959
|
+
break;
|
|
960
|
+
default:
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return fields;
|
|
965
|
+
}
|
|
966
|
+
function wallAsUtcMs(f) {
|
|
967
|
+
const d = /* @__PURE__ */ new Date(0);
|
|
968
|
+
d.setUTCFullYear(f.year, f.month - 1, f.day);
|
|
969
|
+
d.setUTCHours(f.hour, f.minute, f.second, f.millisecond);
|
|
970
|
+
return d.getTime();
|
|
971
|
+
}
|
|
972
|
+
function offsetMinutes(ms, timeZone) {
|
|
973
|
+
const wall = zonedFields(ms, timeZone);
|
|
974
|
+
wall.millisecond = 0;
|
|
975
|
+
return (wallAsUtcMs(wall) - Math.floor(ms / 1e3) * 1e3) / 6e4;
|
|
976
|
+
}
|
|
977
|
+
var DAY_MS = 864e5;
|
|
978
|
+
function wallToEpoch(fields, timeZone) {
|
|
979
|
+
const wall = wallAsUtcMs(fields);
|
|
980
|
+
const toMs = (minutes) => Math.round(minutes * 6e4);
|
|
981
|
+
const before = offsetMinutes(wall - DAY_MS, timeZone);
|
|
982
|
+
const after = offsetMinutes(wall + DAY_MS, timeZone);
|
|
983
|
+
const atBefore = wall - toMs(before);
|
|
984
|
+
if (offsetMinutes(atBefore, timeZone) === before) return atBefore;
|
|
985
|
+
const atAfter = wall - toMs(after);
|
|
986
|
+
if (offsetMinutes(atAfter, timeZone) === after) return atAfter;
|
|
987
|
+
return atBefore;
|
|
988
|
+
}
|
|
989
|
+
function formatOffset(minutes) {
|
|
990
|
+
const totalSeconds = Math.round(minutes * 60);
|
|
991
|
+
if (totalSeconds === 0) return "Z";
|
|
992
|
+
const sign = totalSeconds < 0 ? "-" : "+";
|
|
993
|
+
const abs = Math.abs(totalSeconds);
|
|
994
|
+
const hh = pad(Math.floor(abs / 3600));
|
|
995
|
+
const mm = pad(Math.floor(abs % 3600 / 60));
|
|
996
|
+
const ss = abs % 60;
|
|
997
|
+
return `${sign}${hh}:${mm}${ss === 0 ? "" : `:${pad(ss)}`}`;
|
|
998
|
+
}
|
|
999
|
+
var formatWallDate = (f) => `${f.year}-${pad(f.month)}-${pad(f.day)}`;
|
|
1000
|
+
var formatWallDateTime = (f) => `${formatWallDate(f)}T${pad(f.hour)}:${pad(f.minute)}:${pad(f.second)}`;
|
|
1001
|
+
function assertValidDate(date) {
|
|
1002
|
+
const ms = date.getTime();
|
|
1003
|
+
if (Number.isNaN(ms)) throw new RangeError("Invalid time value");
|
|
1004
|
+
return ms;
|
|
1005
|
+
}
|
|
1006
|
+
function toWireDateTime(date, options) {
|
|
1007
|
+
const timeZone = options?.timeZone;
|
|
1008
|
+
const withOffset = options?.offset === true;
|
|
1009
|
+
if (timeZone === void 0) {
|
|
1010
|
+
const wall2 = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1011
|
+
if (!withOffset) return wall2;
|
|
1012
|
+
assertValidDate(date);
|
|
1013
|
+
return wall2 + formatOffset(-date.getTimezoneOffset());
|
|
1014
|
+
}
|
|
1015
|
+
const ms = assertValidDate(date);
|
|
1016
|
+
const wall = formatWallDateTime(zonedFields(ms, timeZone));
|
|
1017
|
+
return withOffset ? wall + formatOffset(offsetMinutes(ms, timeZone)) : wall;
|
|
1018
|
+
}
|
|
1019
|
+
function toWireDate(date, options) {
|
|
1020
|
+
const timeZone = options?.timeZone;
|
|
1021
|
+
if (timeZone === void 0) {
|
|
1022
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1023
|
+
}
|
|
1024
|
+
return formatWallDate(zonedFields(assertValidDate(date), timeZone));
|
|
1025
|
+
}
|
|
1026
|
+
var WALL_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d+))?$/;
|
|
1027
|
+
function parseWallText(text, original) {
|
|
1028
|
+
const m = text.match(WALL_PATTERN);
|
|
1029
|
+
if (!m) throw new Error(`\uC62C\uBC14\uB978 datetime \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4: ${original}`);
|
|
1030
|
+
const [, y, mo, day, h, mi, s = "0", frac = "0"] = m;
|
|
1031
|
+
return {
|
|
1032
|
+
year: Number(y),
|
|
1033
|
+
month: Number(mo),
|
|
1034
|
+
day: Number(day),
|
|
1035
|
+
hour: Number(h),
|
|
1036
|
+
minute: Number(mi),
|
|
1037
|
+
second: Number(s),
|
|
1038
|
+
millisecond: Number((frac + "000").slice(0, 3))
|
|
1039
|
+
// JS Date 는 ms 정밀도까지만
|
|
1040
|
+
};
|
|
914
1041
|
}
|
|
915
|
-
function
|
|
916
|
-
|
|
1042
|
+
function assertValidWall(f, original) {
|
|
1043
|
+
const d = new Date(wallAsUtcMs(f));
|
|
1044
|
+
if (d.getUTCFullYear() !== f.year || d.getUTCMonth() !== f.month - 1 || d.getUTCDate() !== f.day || d.getUTCHours() !== f.hour || d.getUTCMinutes() !== f.minute || d.getUTCSeconds() !== f.second) {
|
|
1045
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 datetime \uAC12\uC785\uB2C8\uB2E4: ${original}`);
|
|
1046
|
+
}
|
|
917
1047
|
}
|
|
918
|
-
function
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
);
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
|
|
1048
|
+
function matchRfc3339Offset(value) {
|
|
1049
|
+
const tIndex = value.indexOf("T");
|
|
1050
|
+
if (tIndex === -1) return null;
|
|
1051
|
+
const m = value.slice(tIndex).match(/(?:[Zz]|([+-])(\d{2}):(\d{2}))$/);
|
|
1052
|
+
if (!m || m.index === void 0) return null;
|
|
1053
|
+
const [token, sign, hh, mm] = m;
|
|
1054
|
+
const index = value.length - token.length;
|
|
1055
|
+
if (sign === void 0) return { index, minutes: 0 };
|
|
1056
|
+
const hours = Number(hh);
|
|
1057
|
+
const minutes = Number(mm);
|
|
1058
|
+
if (hours > 23 || minutes > 59) {
|
|
1059
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC624\uD504\uC14B\uC785\uB2C8\uB2E4: ${value}`);
|
|
1060
|
+
}
|
|
1061
|
+
return { index, minutes: (sign === "-" ? -1 : 1) * (hours * 60 + minutes) };
|
|
1062
|
+
}
|
|
1063
|
+
function parseWireDateTime(value, options) {
|
|
1064
|
+
const timeZone = options?.timeZone;
|
|
1065
|
+
const policy = options?.inboundOffset ?? "drop";
|
|
1066
|
+
if (policy !== "drop" && policy !== "convert" && policy !== "reject") {
|
|
1067
|
+
throw new RangeError(`\uC54C \uC218 \uC5C6\uB294 inboundOffset \uC815\uCC45\uC785\uB2C8\uB2E4: ${String(policy)}`);
|
|
1068
|
+
}
|
|
1069
|
+
if (timeZone !== void 0) zoneFormatter(timeZone);
|
|
1070
|
+
const trimmed = value.trim();
|
|
1071
|
+
let wallText = trimmed;
|
|
1072
|
+
let offset;
|
|
1073
|
+
if (policy === "drop") {
|
|
1074
|
+
wallText = stripZone(trimmed);
|
|
1075
|
+
} else {
|
|
1076
|
+
const found = matchRfc3339Offset(trimmed);
|
|
1077
|
+
if (found) {
|
|
1078
|
+
if (policy === "reject") {
|
|
1079
|
+
throw new RangeError(`\uC624\uD504\uC14B\uC774 \uBD99\uC740 datetime \uC740 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(inboundOffset: "reject"): ${value}`);
|
|
1080
|
+
}
|
|
1081
|
+
offset = found.minutes;
|
|
1082
|
+
wallText = trimmed.slice(0, found.index);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const f = parseWallText(wallText, value);
|
|
1086
|
+
if (offset !== void 0) {
|
|
1087
|
+
assertValidWall(f, value);
|
|
1088
|
+
return new Date(wallAsUtcMs(f) - offset * 6e4);
|
|
1089
|
+
}
|
|
1090
|
+
if (timeZone !== void 0) {
|
|
1091
|
+
assertValidWall(f, value);
|
|
1092
|
+
return new Date(wallToEpoch(f, timeZone));
|
|
1093
|
+
}
|
|
1094
|
+
const date = new Date(f.year, f.month - 1, f.day, f.hour, f.minute, f.second, f.millisecond);
|
|
1095
|
+
if (date.getFullYear() !== f.year || date.getMonth() !== f.month - 1 || date.getDate() !== f.day || date.getHours() !== f.hour || date.getMinutes() !== f.minute || date.getSeconds() !== f.second) {
|
|
928
1096
|
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 datetime \uAC12\uC785\uB2C8\uB2E4: ${value}`);
|
|
929
1097
|
}
|
|
930
1098
|
return date;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rscc/common-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "RSCC 공통 코어 — CommonResponse 타입·apiClient(traceId·재시도·멱등성 키), SSE 파서, 회복탄력성(retry·서킷 브레이커·토큰버킷·Bulkhead·TTL 캐시), 보안(마스킹·로그 리댁션·웹훅 서명·JWT 디코드·AES-GCM 서브패스), 와이어 계약 헬퍼, 한국 도메인 유틸. 프레임워크 무관, 런타임 의존성 0.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rscc",
|