@rscc/common-core 0.1.2 → 0.3.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/dist/index.d.ts CHANGED
@@ -247,6 +247,22 @@ interface ApiClientConfig {
247
247
  * - onUnauthorized 는 시도마다 발화될 수 있다 (기본 판정에선 401 비재시도라 1회).
248
248
  */
249
249
  retry?: ApiClientRetryOptions;
250
+ /**
251
+ * 지정 시 대상 메서드 요청에 멱등성 키 헤더를 자동 부착한다 (contracts/idempotency.md).
252
+ * 미지정 = 미부착(현행 동작).
253
+ *
254
+ * - 키는 {@link generateIdempotencyKey} 로 **논리 호출당 1회** 생성된다 —
255
+ * sentTraceId 와 같은 위치(재시도 클로저 밖)라 재시도 전 시도에 동일 키가
256
+ * 부착된다 ("같은 논리 작업 = 같은 키" 계약의 자동화).
257
+ * - 호출자가 이미 같은 이름의 헤더를 실었으면 덮어쓰지 않는다 (호출자 우선).
258
+ * - 새 논리 호출(새 request)은 새 키를 생성한다.
259
+ */
260
+ idempotency?: {
261
+ /** 멱등성 키 헤더명. 기본 "Idempotency-Key". */
262
+ header?: string;
263
+ /** 부착 대상 메서드(대문자). 기본 ["POST","PATCH"] — 비멱등 메서드만. */
264
+ methods?: string[];
265
+ };
250
266
  /**
251
267
  * true 면 성공(2xx) 응답의 본문이 유효한 JSON 이 아닐 때(본문 읽기 `text()` 실패 포함)
252
268
  * 조용히 null 을 반환하지 않고 code "INVALID_JSON" 의 ApiError 를 throw 한다
@@ -299,6 +315,11 @@ interface ApiClient {
299
315
  *
300
316
  * 동작 (contracts/common-response.schema.json, trace.md):
301
317
  * - 매 요청에 traceId 를 생성해 traceIdHeader(기본 X-Trace-Id)로 부착한다.
318
+ * - `Content-Type: application/json` 은 호출자가 Content-Type 을 지정하지 않았고 body 가
319
+ * **문자열**일 때만 기본 부착한다 (JSON.stringify 관용). body 없는 요청(GET 등)엔 붙이지
320
+ * 않아 교차 출처 GET 의 CORS preflight 를 유발하지 않으며, URLSearchParams/Blob/FormData/
321
+ * ArrayBuffer/ReadableStream 등은 fetch 기본값에 맡긴다. 문자열이지만 JSON 이 아닌 본문은
322
+ * Content-Type 을 직접 지정할 것.
302
323
  * - 응답이 CommonResponse 봉투면 success 검사 후 data 를 언랩한다
303
324
  * (data 부재 시 undefined). 비봉투 JSON 은 그대로 반환.
304
325
  * - 실패 봉투 / HTTP 에러는 code·message·traceId 를 담은 ApiError 를 throw.
@@ -559,9 +580,13 @@ declare function createTtlCache<K, V>(options: TtlCacheOptions): TtlCache<K, V>;
559
580
  * 경량 서킷 브레이커. 런타임 의존성 0 (native 만).
560
581
  *
561
582
  * Java `com.rscc.common.util.CircuitBreaker` / Python `rscc_common.circuit_breaker` 와
562
- * 시맨틱 동일 (3언어 공유 벡터 CB-01~09 — 각 언어 테스트에 수동 동기화. Python 은
583
+ * 시맨틱 동일 (3언어 공유 벡터 CB-01~13 — 각 언어 테스트에 수동 동기화. Python 은
563
584
  * 시간 단위가 초 — `초 × 1000 == ms`).
564
585
  *
586
+ * - **권장 사용법 — `execute(fn)`**: 게이트(allowRequest) → 실행 → 결과 보고를 한 번에
587
+ * 처리해 보고를 **정확히 1회** 보장한다. 차단 시 {@link CircuitOpenError} 로 reject
588
+ * (fn 미호출), 취소(`name === "AbortError"`)는 성공·실패 어느 쪽에도 집계하지 않는다
589
+ * (onIgnore — CB-12). 수동 게이트(allowRequest + 보고)는 execute 로 감쌀 수 없는 경우에만.
565
590
  * - **연속(consecutive) 실패 카운트**: `onFailure()` 가 failureThreshold 에 도달(`>=`)하면
566
591
  * OPEN. `onSuccess()` 는 카운터를 0 으로 리셋한다 (슬라이딩 윈도 비채택 — 단순성).
567
592
  * - **lazy 전이**: OPEN → HALF_OPEN 전이는 `allowRequest()` 호출 시점에 판정한다
@@ -570,38 +595,51 @@ declare function createTtlCache<K, V>(options: TtlCacheOptions): TtlCache<K, V>;
570
595
  * - **HALF_OPEN 프로브 1개**: 프로브가 진행 중이면 나머지 `allowRequest()` 는 false.
571
596
  * 프로브 성공 → CLOSED(카운터 0), 프로브 실패 → OPEN 재진입(openedAt 갱신 —
572
597
  * 전체 openDurationMs 재대기).
573
- * - **결과 보고 규율**: `allowRequest()==true` 를 받은 호출만 `onSuccess()`/`onFailure()`
574
- * 를 **정확히 1회** 호출한다 (try/finally 로 보고 보장 권장 — 보고 누락은 아래 프로브
575
- * 보류를 무기한 지속시킨다). OPEN 대기 중 늦게 도착한 보고는 상태·openedAt·카운터를
576
- * 바꾸지 않고(no-op — CB-08) 미보고 그랜트 잔량만 소진한다.
598
+ * - **결과 보고 규율**: `allowRequest()==true` 를 받은 호출만 `onSuccess()`/`onFailure()`/
599
+ * `onIgnore()` 중 하나를 **정확히 1회** 호출한다 (수동 게이트라면 try/catch 로 보고 보장 —
600
+ * 성공·실패로 판정할 수 없는 취소 등은 `onIgnore()` 로 그랜트만 반납). OPEN 대기 중 늦게
601
+ * 도착한 보고는 상태·openedAt·카운터를 바꾸지 않고(no-op — CB-08) 미보고 그랜트 잔량만
602
+ * 소진한다.
577
603
  * - **스테일 그랜트 격리**: CLOSED 시절 그랜트가 아직 보고되지 않은 동안에는
578
604
  * openDurationMs 가 경과해도 HALF_OPEN 전이(프로브)가 보류된다 — 스테일 보고가 프로브
579
- * 결과로 오인되는 것을 막아 HALF_OPEN 중 도착하는 보고는 항상 프로브의 것이다 (CB-09).
605
+ * 결과로 오인되는 것을 막아 HALF_OPEN 중 도착하는 보고는 (아래 용서 이후의 매우 늦은
606
+ * 보고를 제외하면) 항상 프로브의 것이다 (CB-09).
607
+ * - **유실 그랜트 용서 (2× 규칙)**: 보고가 끝내 오지 않는 그랜트(보고 누락·유실)가 위 보류를
608
+ * 무기한 지속시켜 서킷이 OPEN 에 영구 고착되는 것을 막는다 — OPEN 진입 후
609
+ * `2 × openDurationMs` 경과 시점에도 미보고 그랜트가 남아 있으면 이를 유실로 간주해 0 으로
610
+ * 용서하고 정상 HALF_OPEN 전이 + 프로브 획득을 수행한다 (CB-10). 용서 뒤 매우 늦은 스테일
611
+ * 보고가 HALF_OPEN 중 도착하면 프로브 결과로 취급되지만 무해하다 — 스테일 성공은 하류가
612
+ * 실제로 응답했다는 증거이고, 스테일 실패는 보수적인 OPEN 재진입일 뿐이다 (스테일
613
+ * onIgnore 는 프로브 슬롯을 조기 해제해 프로브가 1개 더 허가될 뿐 — 최초 보고가 판정).
614
+ * - **유실 프로브 용서**: HALF_OPEN 프로브가 openDurationMs(`>=`) 동안 보고되지 않으면 유실로
615
+ * 간주해 다음 allowRequest() 에 새 프로브를 넘긴다 — 보고 누락 프로브 1개로 HALF_OPEN 에 영구
616
+ * 고착되는 것을 막는다 (CB-13). 옛 프로브의 늦은 보고는 새 프로브 결과로 취급되며 위와 같은
617
+ * 이유로 무해하다.
618
+ *
619
+ * @example 권장 — execute 로 게이트·보고를 위임한다.
620
+ * ```ts
621
+ * const cb = createCircuitBreaker({ failureThreshold: 5, openDurationMs: 30_000 });
622
+ * const user = await cb.execute(() => api.request<User>("/users/1")); // OPEN 이면 CircuitOpenError
623
+ * ```
580
624
  *
581
- * @example 서킷 + 재시도 조합 — 시도 직전 allowRequest 게이트, 결과를 브레이커에 보고.
582
- * OPEN 차단 오류는 shouldRetry 에서 비재시도로 걸러 즉시 실패시킨다(빠른 실패 유지).
625
+ * @example 서킷 + 재시도 조합 — 시도마다 execute 로 게이트·보고. OPEN 차단
626
+ * ({@link CircuitOpenError})은 shouldRetry 에서 비재시도로 걸러 즉시 실패시킨다(빠른 실패 유지).
583
627
  * ```ts
584
628
  * const cb = createCircuitBreaker({ failureThreshold: 5, openDurationMs: 30_000 });
585
- * const circuitOpen = new Error("서킷 OPEN — 요청 차단");
586
- * await retry(
587
- * async () => {
588
- * if (!cb.allowRequest()) throw circuitOpen;
589
- * try {
590
- * const r = await call();
591
- * cb.onSuccess();
592
- * return r;
593
- * } catch (error) {
594
- * cb.onFailure();
595
- * throw error;
596
- * }
597
- * },
598
- * {
599
- * shouldRetry: (error) => error !== circuitOpen, // OPEN 차단은 즉시 실패
600
- * onRetry: (error, attempt, delayMs) => console.warn(`재시도 ${attempt} (${delayMs}ms)`),
601
- * }
602
- * );
629
+ * await retry(() => cb.execute(() => call()), {
630
+ * shouldRetry: (error) => !(error instanceof CircuitOpenError), // OPEN 차단은 즉시 실패
631
+ * onRetry: (error, attempt, delayMs) => console.warn(`재시도 ${attempt} (${delayMs}ms)`),
632
+ * });
603
633
  * ```
604
634
  */
635
+ /**
636
+ * 서킷 차단 거부 오류 — `execute()` 가 allowRequest()==false(OPEN 대기·프로브 보류·HALF_OPEN
637
+ * 프로브 경합)일 때 reject 하며, 이때 작업 함수는 **호출되지 않는다** (CB-12).
638
+ * Java `CircuitOpenException` / Python `CircuitOpenError` 패리티. 재시도 조합에서 비재시도 신호로 쓴다.
639
+ */
640
+ declare class CircuitOpenError extends Error {
641
+ constructor(message?: string);
642
+ }
605
643
  type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
606
644
  interface CircuitBreakerOptions {
607
645
  /** OPEN 전이 임계 — 연속 실패가 이 값에 도달(`>=`)하면 OPEN. 필수 양의 정수 (아니면 RangeError). */
@@ -615,14 +653,32 @@ interface CircuitBreaker {
615
653
  /**
616
654
  * 요청 통과 여부 — CLOSED 는 true, OPEN 대기 중 false, openDurationMs 경과 시
617
655
  * HALF_OPEN 전이 + 프로브 슬롯 획득(true) — 단 CLOSED 시절 미보고 그랜트가 남아 있으면
618
- * 전이를 보류하고 false (CB-09). HALF_OPEN 프로브 경합 시 나머지 false.
619
- * true 를 받은 호출만 결과를 onSuccess()/onFailure() 로 보고해야 한다.
656
+ * 전이를 보류하고 false (CB-09), 보류는 OPEN 진입 후 `2 × openDurationMs` 까지만 —
657
+ * 그 시점엔 미보고 그랜트를 유실로 용서하고 전이 + 프로브 획득(true) (CB-10).
658
+ * HALF_OPEN 프로브 경합 시 나머지 false — 단 프로브가 openDurationMs 경과(`>=`)까지
659
+ * 미보고면 유실로 간주해 이 호출에 새 프로브를 넘긴다(true) (CB-13).
660
+ * true 를 받은 호출만 결과를 onSuccess()/onFailure()/onIgnore() 중 하나로 보고해야 한다.
620
661
  */
621
662
  allowRequest(): boolean;
622
663
  /** 성공 보고 — CLOSED 카운터 리셋, HALF_OPEN 프로브 성공 시 CLOSED 복귀. OPEN 중엔 무시. */
623
664
  onSuccess(): void;
624
665
  /** 실패 보고 — CLOSED 연속 카운트 증가(임계 도달 시 OPEN), HALF_OPEN 프로브 실패 시 OPEN 재진입. OPEN 중엔 무시. */
625
666
  onFailure(): void;
667
+ /**
668
+ * 결과 없는 그랜트 반납 — 취소(abort) 등 성공·실패로 판정할 수 없는 호출이 보고 대신
669
+ * 호출한다 (CB-11). CLOSED: 미보고 그랜트만 반납(연속 실패 카운터 무변화). HALF_OPEN:
670
+ * 프로브 슬롯 해제 — HALF_OPEN 유지, 다음 allowRequest() 가 새 프로브를 획득한다.
671
+ * OPEN: 미보고 그랜트 잔량만 소진(그 외 무변화).
672
+ */
673
+ onIgnore(): void;
674
+ /**
675
+ * fn 래핑 (권장 사용법) — allowRequest() 게이트 → 실행 → 결과 보고를 **정확히 1회** 보장한다.
676
+ * 거부는 reject(CircuitOpenError) — 동기 throw 아님, 거부 시 fn 은 호출되지 않는다.
677
+ * resolve → onSuccess() 후 값 반환. throw/reject 는 `name === "AbortError"` 면 onIgnore(),
678
+ * 그 외(TimeoutError 포함)는 onFailure() 후 **같은 값을 래핑 없이** 다시 throw 한다 (CB-12).
679
+ * fn 의 동기 throw 도 동일하게 처리된다.
680
+ */
681
+ execute<T>(fn: () => T | PromiseLike<T>): Promise<T>;
626
682
  /** 현재 상태 조회 — lazy 전이 특성상 openDurationMs 경과 후에도 allowRequest() 전엔 OPEN 으로 보고된다. */
627
683
  state(): CircuitState;
628
684
  }
@@ -756,4 +812,539 @@ declare function isValidBusinessNumber(value: string | null | undefined): boolea
756
812
  */
757
813
  declare function isValidCorporateNumber(value: string | null | undefined): boolean;
758
814
 
759
- export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type Bulkhead, BulkheadFullError, type BulkheadOptions, type CircuitBreaker, type CircuitBreakerOptions, type CircuitState, type CommonResponse, type FieldErrorDetail, type PageResponse, ResultCode, type RetryOptions, type SseCallbacks, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type ValidationErrorData, createApiClient, createBulkhead, createCircuitBreaker, createTokenBucket, createTtlCache, decodeJwtPayload, getTokenExpiry, isChosungQuery, isRetryableStatus, isTokenExpired, isValidBusinessNumber, isValidCorporateNumber, isValidationErrorData, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, normalizeBusinessNumber, parseRetryAfterMs, parseSseFrame, parseWireDateTime, readSseStream, retry, sanitizeLogValue, stripZone, toChosung, toWireDate, toWireDateTime };
815
+ /**
816
+ * 멱등성 키 생성 — contracts/idempotency.md (수동 동기화).
817
+ *
818
+ * 비멱등 메서드(POST/PATCH)의 실수 재전송이 부수효과를 중복 실행하지 않도록,
819
+ * 클라이언트가 논리 요청당 고유 키를 `Idempotency-Key` 헤더로 보낸다.
820
+ * 키 형식: `^[A-Za-z0-9_-]{1,128}$` — UUIDv4(36자, 하이픈 포함) 권장.
821
+ *
822
+ * "같은 논리 작업 = 같은 키, 새 작업 = 새 키" 가 핵심이다 — 재시도 루프
823
+ * **바깥에서 1회 생성**해 전 시도에 동일 부착해야 한다. apiClient 의
824
+ * `idempotency` 옵션은 이를 자동화한다 (키가 재시도 클로저 밖에서 생성됨).
825
+ */
826
+ /**
827
+ * 멱등성 키 신규 생성 — `crypto.randomUUID()` 우선(UUIDv4), 부재 환경에서는
828
+ * Math.random 기반 8-4-4-4-12 hex 폴백 (generateTraceId 관용구 — 형식 동일).
829
+ * 두 경로 모두 계약 형식 `[A-Za-z0-9_-]{1,128}` 을 만족한다.
830
+ */
831
+ declare function generateIdempotencyKey(): string;
832
+
833
+ /**
834
+ * 웹훅 서명 — HMAC-SHA256. contracts/webhook-signature.md (수동 동기화).
835
+ *
836
+ * Java `com.rscc.common.crypto.WebhookSignature` / Python `rscc_common.webhook`
837
+ * 과 동일 규칙 (3언어 공유 골든 벡터 WS-01~07).
838
+ *
839
+ * - 서명 대상: `"{t}.{rawBody}"` — 본문은 **원문 바이트 그대로**(재직렬화 금지).
840
+ * - 서명: HMAC-SHA256 의 lowercase hex 64자. secret 은 UTF-8 바이트.
841
+ * - 헤더 값: `t=<unix초>,v1=<hex>[,v1=<hex>...]` — 다중 v1 = 무중단 시크릿
842
+ * 로테이션(어느 하나라도 일치하면 유효). 미지 key(v0/v2 등)는 무시.
843
+ *
844
+ * [주의] 언어별 시그니처 비대칭 — 계약 문서 인용: "JS 구현은 async 다 —
845
+ * WebCrypto(`crypto.subtle.sign`)가 Promise 기반이라 signWebhook/verifyWebhook 은
846
+ * Promise<string>/Promise<boolean> 을 반환한다. Java/Python 은 동기다.
847
+ * 시맨틱(입력→판정)은 3언어 동일하며 반환 형태만 다르다."
848
+ *
849
+ * Node 전용 API(`timingSafeEqual` 등) 미사용 — 브라우저/Node 공용
850
+ * (`globalThis.crypto.subtle`, Node 18+ / 모던 브라우저).
851
+ */
852
+ /** 웹훅 서명 헤더명 기본값 (contracts/webhook-signature.md). */
853
+ declare const WEBHOOK_SIGNATURE_HEADER = "X-Rscc-Signature";
854
+ /**
855
+ * 웹훅 서명 헤더 값을 생성한다 (발신측) — 반환값이 `X-Rscc-Signature` 헤더
856
+ * 값 전체(`t=<unix초>,v1=<hex>`)다.
857
+ *
858
+ * 시크릿 로테이션 기간에는 신·구 시크릿으로 각각 sign 한 뒤 `v1` 을 병기해
859
+ * 발송한다 (`t=...,v1=<신>,v1=<구>` — t 가 같으므로 두 번째 결과의 `v1=` 부분만
860
+ * 이어 붙이면 된다).
861
+ *
862
+ * @param secret 서명 시크릿 (UTF-8 인코딩 후 HMAC 키로 사용).
863
+ * @param payload 요청 본문 — **원문 그대로** (파싱·재직렬화 금지, 계약).
864
+ * @param timestampSeconds 발신 시각 unix 초.
865
+ */
866
+ declare function signWebhook(secret: string, payload: string | Uint8Array, timestampSeconds: number): Promise<string>;
867
+ /**
868
+ * 웹훅 서명을 검증한다 (수신측) — 형식 오류·스큐 초과·전 후보 불일치 모두
869
+ * 예외 없이 **false** 를 반환한다 (호출부 분기 단순화, 계약).
870
+ *
871
+ * 판정 절차: 헤더 파싱(t 정확히 1개·정수, v1 1개 이상) → 시계 스큐
872
+ * `|now - t| ≤ toleranceSeconds`(경계 포함) → 보유 시크릿 각각 × 헤더의 각 v1
873
+ * 상수시간 대조, 하나라도 일치 → true.
874
+ *
875
+ * @param opts.header 수신한 `X-Rscc-Signature` 헤더 값 (부재 시 null → false).
876
+ * @param opts.payload 수신한 요청 본문 — raw 바이트/원문 문자열.
877
+ * @param opts.secrets 보유 시크릿 (단일 또는 배열 — 로테이션 기간 신·구 병행).
878
+ * @param opts.toleranceSeconds 허용 스큐(초). 기본 300.
879
+ * @param opts.nowSeconds 현재 시각 unix 초 주입 (테스트용). 기본 시스템 시계.
880
+ */
881
+ declare function verifyWebhook(opts: {
882
+ header: string | null;
883
+ payload: string | Uint8Array;
884
+ secrets: string | string[];
885
+ toleranceSeconds?: number;
886
+ nowSeconds?: number;
887
+ }): Promise<boolean>;
888
+
889
+ /**
890
+ * 벌크/부분 실패 봉투 — BulkResult. contracts/bulk.md (수동 동기화).
891
+ *
892
+ * Java `com.rscc.common.response.BulkResult` / Python `rscc_common.bulk` 와
893
+ * 동일 와이어 계약 (3언어 공유 골든 벡터 BK-01~04).
894
+ *
895
+ * - 항상 `CommonResponse` 봉투의 `data` 자리에 실린다.
896
+ * - **항목 성공/실패 판정은 `code` 유무다** — code 있으면 실패, 없으면 성공.
897
+ * - 실패 항목은 items 에 전량 필수 기재, 성공 항목은 id 전달 필요 시만 기재.
898
+ * - 부분 실패여도 HTTP 200 + `success: true` (벌크 연산 자체는 수행됨).
899
+ * - `total == succeeded + failed` 불변식.
900
+ */
901
+ /**
902
+ * 항목별 처리 상세 — `index` 는 **요청 배열에서의 0-기점 인덱스**(필수,
903
+ * 원본 항목과 대응시키는 유일 키). `code` 존재 = 실패 항목.
904
+ */
905
+ interface BulkResultItem {
906
+ /** 요청 배열에서의 0-기점 인덱스 (필수). */
907
+ index: number;
908
+ /** 실패 사유 코드 — error-codes.yaml 코드 문자열 재사용. **존재 = 실패 항목**. */
909
+ code?: string;
910
+ /** 실패 사유 한국어 메시지 (실패 항목이면 기재). */
911
+ message?: string;
912
+ /** 성공 항목이 생성/영향을 준 리소스 식별자 (전달 필요 시만). */
913
+ id?: string;
914
+ }
915
+ /** 벌크 처리 결과 페이로드 — `total == succeeded + failed` 불변식. */
916
+ interface BulkResult {
917
+ /** 요청에 담긴 전체 항목 수. */
918
+ total: number;
919
+ /** 성공 항목 수. */
920
+ succeeded: number;
921
+ /** 실패 항목 수. */
922
+ failed: number;
923
+ /** 항목별 상세 — 실패 전량 필수, 성공은 선택. 전량 성공·id 없음이면 `[]`. */
924
+ items: BulkResultItem[];
925
+ }
926
+ /**
927
+ * 값이 BulkResult 와이어 형태인지 판별하는 타입 가드 — total/succeeded/failed 가
928
+ * number, items 가 배열이고 각 요소에 number `index` 가 있는지만 검사한다.
929
+ * 선택 키(code/message/id)·미지 키는 검사하지 않는다 (관용적 읽기 —
930
+ * isValidationErrorData 관용구, 새 선택 키 추가는 non-breaking).
931
+ */
932
+ declare function isBulkResult(value: unknown): value is BulkResult;
933
+ /** 실패 항목만 추출한다 — 판정 기준은 `code` 유무 (계약 규칙 1). */
934
+ declare function bulkFailures(result: BulkResult): BulkResultItem[];
935
+ /** BulkResult 빌더 — success/failure 를 순서대로 기록하고 build 로 확정한다. */
936
+ interface BulkResultBuilder {
937
+ /** 성공 항목 기록 — `id` 를 준 경우에만 items 에 기재된다 (응답 크기 억제). */
938
+ success(index: number, id?: string): BulkResultBuilder;
939
+ /** 실패 항목 기록 — items 에 반드시 기재된다 (code·message 필수, 계약 규칙 1·2). */
940
+ failure(index: number, code: string, message: string): BulkResultBuilder;
941
+ /** total/succeeded/failed 자동 계산으로 BulkResult 를 확정한다. */
942
+ build(): BulkResult;
943
+ }
944
+ /**
945
+ * BulkResult 빌더 생성 — 서버/BFF 측에서 항목별 처리 결과를 순서대로 기록한다.
946
+ *
947
+ * ```ts
948
+ * const result = createBulkResultBuilder()
949
+ * .success(0, "ord_001")
950
+ * .failure(1, "400", "수량은 1 이상이어야 합니다.")
951
+ * .success(2) // id 없음 — items 미기재
952
+ * .build(); // { total: 3, succeeded: 2, failed: 1, items: [...] }
953
+ * ```
954
+ */
955
+ declare function createBulkResultBuilder(): BulkResultBuilder;
956
+
957
+ /**
958
+ * 파일 업로드 검증 — 크기·확장자·매직바이트. contracts/file-upload.md (수동 동기화).
959
+ *
960
+ * Java `com.rscc.common.util.FileSniffer` / Python `rscc_common.upload` 와
961
+ * 동일 계약 (3언어 공유 골든 벡터 MB-01~11).
962
+ *
963
+ * **브라우저 프리검증이다** — 업로드 전에 사용자에게 즉시 피드백을 주는 용도이며,
964
+ * 프리검증 통과가 서버 검증을 대체하지 않는다 (보안 경계는 항상 서버).
965
+ *
966
+ * - 판정은 **컨테이너 수준**: docx/hwpx 는 `zip`, hwp(5.0)/doc 는 `cfbf` 로
967
+ * 판정된다 (내부 구조 열람은 비범위).
968
+ * - 판정 우선순위는 계약 테이블 위→아래, 첫 일치 kind 반환.
969
+ */
970
+ /** 매직바이트 판정 결과 kind — 3언어 공유 소문자 문자열 (계약 테이블 고정). */
971
+ type FileKind = "png" | "jpeg" | "gif" | "webp" | "pdf" | "zip" | "cfbf" | "hwp3";
972
+ /**
973
+ * 선두 바이트의 매직바이트를 대조해 파일 kind 를 판정한다 — 계약 테이블
974
+ * 위→아래 첫 일치, 전부 불일치·빈 입력·시그니처보다 짧은 head 는 null.
975
+ *
976
+ * @param head 파일 선두 바이트 (webp 판정에 최대 12바이트 필요 — File.slice
977
+ * 등으로 앞부분만 읽어 넘기면 된다).
978
+ */
979
+ declare function sniffFile(head: ArrayBuffer | Uint8Array): FileKind | null;
980
+ /**
981
+ * 확장자가 허용하는 kind 집합 — 계약 매핑표 고정. 미지 확장자는 빈 집합.
982
+ * 대소문자 무시, 선행 `.` 은 허용(제거 후 비교).
983
+ */
984
+ declare function kindsForExtension(ext: string): ReadonlySet<FileKind>;
985
+ /** validateUpload 결과 — ok:false 면 첫 위반 사유·계약 메시지를 담는다. */
986
+ type UploadValidationResult = {
987
+ ok: true;
988
+ } | {
989
+ ok: false;
990
+ reason: "size" | "extension" | "content-mismatch";
991
+ message: string;
992
+ };
993
+ /**
994
+ * 업로드 파일 3중 검증 (브라우저 프리검증) — 순서: (1) 크기 ≤ maxSizeBytes →
995
+ * (2) 확장자 ∈ allowedExtensions → (3) 매직바이트 kind ∈ 확장자 허용 kind.
996
+ * 첫 위반의 사유·메시지(계약 문구)를 결과 객체로 반환한다 (예외 없음).
997
+ *
998
+ * 계약 매핑표에 없는 확장자를 allowlist 에 넣으려면 `extraMappings` 로 확장자 →
999
+ * 허용 kind 배열을 주입해야 한다(주입 확장자는 기본 표를 대체 — Python
1000
+ * `extra_mappings` / Java `extraMappings` 대응). 매핑이 없는 허용 확장자는 콘텐츠
1001
+ * 대조가 불가능해 콘텐츠 불일치로 거부된다 (Python 과 동일 판정 — 조용한 통과 금지).
1002
+ *
1003
+ * @param input.fileName 파일명 (확장자는 마지막 `.` 뒤, 대소문자 무시).
1004
+ * @param input.size 파일 크기 바이트 (File.size).
1005
+ * @param input.head 파일 선두 바이트 (12바이트 이상 권장 — webp 최대 요구량).
1006
+ * @param input.maxSizeBytes 크기 상한 (엔드포인트 정책).
1007
+ * @param input.allowedExtensions 허용 확장자 allowlist (대소문자 무시).
1008
+ * @param input.extraMappings 확장자 → 허용 kind 확장 주입 (선택). 키는 소문자·
1009
+ * 선행 `.` 없는 확장자.
1010
+ */
1011
+ declare function validateUpload(input: {
1012
+ fileName: string;
1013
+ size: number;
1014
+ head: ArrayBuffer | Uint8Array;
1015
+ maxSizeBytes: number;
1016
+ allowedExtensions: readonly string[];
1017
+ extraMappings?: Readonly<Record<string, readonly FileKind[]>>;
1018
+ }): UploadValidationResult;
1019
+
1020
+ /**
1021
+ * 목록 조회 쿼리 빌더 — page/size/sort. contracts/query-params.md (수동 동기화).
1022
+ *
1023
+ * 요청 **생성측** 빌더다 — 서버 파서(Java `SortWhitelist`/`PageParams`,
1024
+ * Python `rscc_common.query`)와 왕복 일치하는 쿼리스트링을 만든다.
1025
+ *
1026
+ * - sort 문법: `sort=<field>[,<direction>]` — 같은 이름 반복으로 다중 정렬
1027
+ * (선언 순서 = 정렬 우선순위). direction 생략 시 서버 기본 asc.
1028
+ * - 예약 이름 `page`/`size`/`sort` 를 필터 이름으로 재사용하면 throw (계약).
1029
+ */
1030
+ /** 정렬 지정 1건 — direction 생략 시 값에 방향을 싣지 않는다 (서버 기본 asc). */
1031
+ interface SortParam {
1032
+ /** 정렬 필드 — 서버 화이트리스트와 정확 일치해야 한다 (대소문자 구분). */
1033
+ field: string;
1034
+ /** 정렬 방향. 생략 시 asc (서버 기본). */
1035
+ direction?: "asc" | "desc";
1036
+ }
1037
+ /** buildListQuery 입력 — 전 필드 선택 (지정한 것만 쿼리에 실린다). */
1038
+ interface ListQueryOptions {
1039
+ /** 페이지 번호 — 0-기점 (contracts/pagination.md 와 동일 기점). */
1040
+ page?: number;
1041
+ /** 페이지 크기. */
1042
+ size?: number;
1043
+ /** 다중 정렬 — 배열 순서 = 정렬 우선순위 (순서 보존). */
1044
+ sort?: readonly SortParam[];
1045
+ /**
1046
+ * 필터 파라미터 (엔드포인트 고유 스키마). 배열 값은 같은 이름으로 반복 기재,
1047
+ * null/undefined 값은 생략. 예약 이름 page/size/sort 는 금지 — RangeError.
1048
+ */
1049
+ filters?: Record<string, string | number | boolean | readonly (string | number | boolean)[] | null | undefined>;
1050
+ }
1051
+ /**
1052
+ * 목록 조회 쿼리스트링을 조립한다 — `URLSearchParams` 반환
1053
+ * (`toString()` 하면 서버 파서와 왕복 일치: `page=2&size=50&sort=name%2Cdesc`).
1054
+ *
1055
+ * @throws RangeError filters 키가 예약 이름(page/size/sort)인 경우.
1056
+ */
1057
+ declare function buildListQuery(options: ListQueryOptions): URLSearchParams;
1058
+
1059
+ /**
1060
+ * 피처 플래그 읽기 — boolean 파싱 통일. contracts/feature-flags.md (수동 동기화).
1061
+ *
1062
+ * Java `com.rscc.common.util.FeatureFlags` / Python `rscc_common.feature_flags`
1063
+ * 와 동일 파싱 규칙 (3언어 공유 골든 벡터 FF-01~06).
1064
+ *
1065
+ * - 참 집합은 **정확히 `{"true","1","on","yes"}`** — 대소문자 무시·trim.
1066
+ * 그 외 전부 false ("y"·"t"·"enabled" 불인정 — 오타는 기능 꺼짐, 안전한 쪽).
1067
+ * - **키 부재(→default)와 값 불인식(→false)은 다르다** — 잘못 쓴 설정 값이
1068
+ * default 뒤로 숨지 않게 한다.
1069
+ * - JS 는 주입된 소스 객체만 본다 — **env 폴백 없음** (브라우저엔 env 가 없다,
1070
+ * 계약 명시). 빌드타임 치환·서버 전달 설정 객체를 소스로 주입한다.
1071
+ */
1072
+ /**
1073
+ * 플래그 값을 boolean 으로 파싱한다 — 문자열은 trim·소문자화 후 참 집합
1074
+ * `{"true","1","on","yes"}` 대조, boolean 은 그대로, null/undefined 는 false.
1075
+ */
1076
+ declare function parseFlag(value: string | boolean | null | undefined): boolean;
1077
+ /** 피처 플래그 리더 — 주입된 소스 객체에서 boolean 플래그를 읽는다. */
1078
+ interface FeatureFlagReader {
1079
+ /**
1080
+ * 플래그 조회 — 키 부재 시 defaultValue(기본 false), 키 존재 시
1081
+ * {@link parseFlag} 판정 결과 (불인식 값 → false, default 미적용).
1082
+ */
1083
+ isEnabled(key: string, defaultValue?: boolean): boolean;
1084
+ }
1085
+ /**
1086
+ * 피처 플래그 리더 생성 — 소스 객체(빌드타임 치환·서버 전달 설정)를 주입한다.
1087
+ * 키는 논리 키 그대로 조회한다 (예: `"search.rerank"`).
1088
+ *
1089
+ * ```ts
1090
+ * const flags = createFeatureFlags({ "search.rerank": "on" });
1091
+ * flags.isEnabled("search.rerank"); // true
1092
+ * flags.isEnabled("upload.hwp-preview", true); // 키 부재 → default true
1093
+ * ```
1094
+ */
1095
+ declare function createFeatureFlags(source: Record<string, string | boolean | undefined>): FeatureFlagReader;
1096
+
1097
+ /**
1098
+ * 주민등록번호·외국인등록번호(13자리) 형식 검증.
1099
+ *
1100
+ * **[중요] 2020-10 이후 발급분은 뒷자리(성별코드 제외 6자리)가 임의번호라서
1101
+ * mod-11 체크섬이 성립하지 않는다** — 따라서 기본 검증({@link isValidRrn})은
1102
+ * 체크섬을 포함하지 않고, 레거시 체크섬은 {@link rrnChecksumOkLegacy} 로 분리한다.
1103
+ *
1104
+ * 원천: Java `RrnUtils` / Python `rrn.py` 와 동일 규칙 (수동 동기화).
1105
+ *
1106
+ * 검증 범위: 13자리 형식 + 성별코드(1~8) + 생년월일 실존(윤년 포함 실제 달력).
1107
+ * **미래 날짜 검증은 비범위** — 기준 시점이 없는 순수 함수이므로 하지 않는다.
1108
+ * 정규화는 하이픈·스페이스·탭만 제거(bizno 파리티), 숫자 판정은 ASCII `[0-9]` 만
1109
+ * (전각 숫자 무효).
1110
+ *
1111
+ * ⚠️ 수집 최소화: 주민등록번호는 법령상 처리 근거가 있을 때만 취급하고, 저장 시
1112
+ * 암호화 의무(개인정보보호법 제24조의2)가 있으며, 검증 목적 달성 즉시 파기를
1113
+ * 권장한다.
1114
+ */
1115
+ /**
1116
+ * 주민등록번호 표기에서 하이픈(`-`)·스페이스(` `)·탭(`\t`)을 위치 불문 제거한다.
1117
+ * 그 외 문자는 보존한다(검증 단계에서 무효 처리).
1118
+ */
1119
+ declare function normalizeRrn(value: string): string;
1120
+ /**
1121
+ * 기본 검증 = 13자리 형식 + 성별코드 1~8 + 생년월일 실존(윤년 포함 실제 달력).
1122
+ * **체크섬 미포함** — 2020-10 이후 발급분은 뒷자리가 임의번호라 mod-11 이 성립하지
1123
+ * 않는다. null/undefined·형식 위반은 false (예외 없음).
1124
+ */
1125
+ declare function isValidRrn(value: string | null | undefined): boolean;
1126
+ /**
1127
+ * 외국인등록번호 여부 — {@link isValidRrn} 을 통과하고 성별코드가 5~8 이면 true.
1128
+ * 무효한 값은 false.
1129
+ */
1130
+ declare function isForeignerRrn(value: string | null | undefined): boolean;
1131
+ /**
1132
+ * 레거시 mod-11 체크섬 검증 — **2020-10 이전 발급분 한정**. 이후 발급분은
1133
+ * 임의번호라서 유효한 번호도 false 가 나올 수 있으므로, 유효성 판정에 쓰지 말 것.
1134
+ *
1135
+ * 검증식: 가중치 `[2,3,4,5,6,7,8,9,2,3,4,5]`, `check = (11 − sum%11) % 10`,
1136
+ * **외국인(성별코드 5~8)은 `(check+2) % 10` 보정** 후 13번째 자리와 대조.
1137
+ * 기본 검증({@link isValidRrn}) 불통과 값은 false.
1138
+ */
1139
+ declare function rrnChecksumOkLegacy(value: string | null | undefined): boolean;
1140
+ /**
1141
+ * 생년월일을 `"YYYY-MM-DD"` 문자열로 추출한다(타임존 함정 차단 — `Date` 미사용).
1142
+ * 세기는 성별코드로 판정. 무효한 번호는 null.
1143
+ */
1144
+ declare function rrnBirthDate(value: string | null | undefined): string | null;
1145
+
1146
+ /**
1147
+ * 한국어 조사 자동 선택 — 받침 유무에 따라 은/는·이/가 등을 고른다.
1148
+ *
1149
+ * 원천: Java `JosaUtils` / Python `text/josa.py` 와 동일 규칙 (수동 동기화).
1150
+ *
1151
+ * 판정 규칙:
1152
+ * - 끝 공백(스페이스·탭)을 스킵한 마지막 문자로 판정
1153
+ * - 한글 음절 → `jong = (c − 0xAC00) % 28` (0 = 받침 없음)
1154
+ * - 숫자(ASCII 0~9) → 독음 음절(`영일이삼사오육칠팔구`)로 치환 후 동일 판정
1155
+ * - 그 외(영문·기호·빈 문자열) → 병기형 `은(는)/이(가)/을(를)/과(와)/(으)로/아(야)`
1156
+ * - (으)로 특례: 받침 없음 또는 ㄹ 받침(jong==8) → "로", 그 외 받침 → "으로"
1157
+ */
1158
+ /** 지원하는 조사 쌍 — 이 6종 외 값은 런타임에서도 RangeError. */
1159
+ type JosaPair = "은/는" | "이/가" | "을/를" | "과/와" | "(으)로" | "아/야";
1160
+ /**
1161
+ * 단어에 어울리는 조사를 고른다 (조사만 반환).
1162
+ * 판정 불가(영문·기호·빈 문자열)면 병기형(`은(는)` 등)을 반환한다.
1163
+ * 지원하지 않는 조사 쌍은 RangeError.
1164
+ */
1165
+ declare function pickJosa(word: string, josa: JosaPair): string;
1166
+ /**
1167
+ * 단어 뒤에 어울리는 조사를 붙여 반환한다 (`attachJosa("사과", "은/는")` → `"사과는"`).
1168
+ * 판정에서 스킵한 끝 공백(스페이스·탭)은 출력에서도 제거한다
1169
+ * (`"필드 "` → `"필드는"` — 벡터 JO-12).
1170
+ */
1171
+ declare function attachJosa(word: string, josa: JosaPair): string;
1172
+
1173
+ /**
1174
+ * 한국식 나이 계산 3종 — 만 나이·연 나이·보험 나이.
1175
+ *
1176
+ * 원천: Java `KoreanAgeUtils` / Python `age.py` 와 동일 규칙 (수동 동기화).
1177
+ *
1178
+ * 입력은 **`"YYYY-MM-DD"` 문자열 고정** (`Date` 객체 금지 — JS Date 의 타임존
1179
+ * 함정 원천 차단). 형식 위반·실존하지 않는 날짜·`기준일 < 생일` 은 RangeError.
1180
+ */
1181
+ /**
1182
+ * 만 나이 — 생일이 지나지 않았으면 1 을 뺀다 (민법 기준).
1183
+ * 2/29 생은 평년 3/1 에 증가한다(월·일 튜플 비교 — Java `Period` 시맨틱과 일치).
1184
+ */
1185
+ declare function ageMan(birth: string, on: string): number;
1186
+ /** 연 나이 — 연도 차이만 계산한다 (청소년 보호법 등의 연 나이). */
1187
+ declare function ageByYear(birth: string, on: string): number;
1188
+ /**
1189
+ * 보험 나이 — 만 개월수 `m`(당월 일 미도달 시 −1) 기준 `⌊(m+6)/12⌋`.
1190
+ * 생후 6개월이 지난 시점(상령일)마다 한 살씩 올라간다.
1191
+ */
1192
+ declare function ageInsurance(birth: string, on: string): number;
1193
+
1194
+ /**
1195
+ * 한국 전화번호 정규화·분류·포맷·E.164 변환.
1196
+ *
1197
+ * 원천: Java `PhoneNumberUtils` / Python `phone.py` 와 동일 규칙 (수동 동기화).
1198
+ *
1199
+ * 타입 문자열(`mobile|landline|voip|safe|m2m|unknown`)은 3언어 공유 소문자
1200
+ * 와이어 값이다.
1201
+ *
1202
+ * 로그·화면 노출 권장 경로: `formatPhoneNumber(normalizePhoneNumber(x))` →
1203
+ * `maskPhone`(masking.ts) 파이프라인. maskPhone 은 부분 문자열 단위로 매칭하므로
1204
+ * 형식이 보장되지 않은 원본을 직접 넣지 말고 이 경로로 정형화한 뒤 마스킹할 것.
1205
+ */
1206
+ /** 전화번호 분류 — 3언어 공유 소문자 타입 문자열. */
1207
+ type PhoneType = "mobile" | "landline" | "voip" | "safe" | "m2m" | "unknown";
1208
+ /**
1209
+ * 구분자(하이픈·스페이스·탭·괄호·점)를 제거하고, `+82` 접두는 `0` 을 보충해
1210
+ * 국내 표기로 역변환한다 (`"+82 10-1234-5678"` → `"01012345678"`).
1211
+ * 이미 `0` 으로 시작하는 `+82 010…` 표기는 `0` 을 중복 보충하지 않는다
1212
+ * (Java/Python 동일 규칙). 그 외 문자는 보존한다(분류 단계에서 unknown 처리).
1213
+ */
1214
+ declare function normalizePhoneNumber(value: string): string;
1215
+ /**
1216
+ * 정규화 후 번호를 분류한다. 표에 없는 패턴·비숫자 포함은 `"unknown"`.
1217
+ * - mobile: 010(11자리), 011/016/017/018/019(10~11자리)
1218
+ * - landline: 02(9~10자리), 지역번호 표(10~11자리)
1219
+ * - voip: 070(11자리) / safe: 050X(11~12자리) / m2m: 012(11~12자리)
1220
+ */
1221
+ declare function classifyPhoneNumber(value: string): PhoneType;
1222
+ /**
1223
+ * 정규화 후 표준 하이픈 그룹핑으로 포맷한다. 그룹핑 불가(unknown·비표준 길이)면
1224
+ * **정규화 문자열을 그대로 반환**한다 (예외 없음).
1225
+ * - 02: 2-3-4(9자리) / 2-4-4(10자리)
1226
+ * - 3자리 식별번호(이동·지역·070·012 11자리): 3-3-4(10자리) / 3-4-4(11자리)
1227
+ * - 050X: 4-3-4(11자리) / 4-4-4(12자리)
1228
+ */
1229
+ declare function formatPhoneNumber(value: string): string;
1230
+ /**
1231
+ * E.164(`+82…`) 표기로 변환한다 — 분류 성공 시 선행 `0` 을 떼고 `+82` 를 붙인다.
1232
+ * `"unknown"` 은 null. 역변환(`+82` → `0…`)은 {@link normalizePhoneNumber} 가 담당.
1233
+ */
1234
+ declare function toE164(value: string): string | null;
1235
+
1236
+ /**
1237
+ * 금액 한글 표기 3종 — 한글 수사·공문서 갖은자 표기·UI 축약.
1238
+ *
1239
+ * 원천: Java `KoreanAmountUtils` / Python `money.py` 와 동일 규칙 (수동 동기화).
1240
+ *
1241
+ * 지원 범위: `|amount| < 10^20` (만·억·조·경). 입력은 `number`(안전 정수) 또는
1242
+ * `bigint` — 비정수·비안전 정수·범위 초과는 RangeError.
1243
+ * 축약은 **내림**을 채택한다(금액 과대 표시 방지).
1244
+ */
1245
+ /**
1246
+ * 순수 한글 수사 표기 — `12345678` → `"천이백삼십사만오천육백칠십팔"`.
1247
+ * 그룹 내 1 은 십/백/천 앞에서 생략하고, 그룹값 1 은 "일만/일억" 으로 명시한다.
1248
+ * 0 은 `"영"`, 음수는 `"마이너스 "` 접두.
1249
+ */
1250
+ declare function toKoreanWords(amount: number | bigint): string;
1251
+ /**
1252
+ * 공문서 위조방지 표기 — `"금" + 전자리 명시 수사(일십·일백·일천 포함) + "원整"`.
1253
+ * `12345678` → `"금일천이백삼십사만오천육백칠십팔원整"`, 0 → `"금영원整"`.
1254
+ * 음수는 RangeError (공문서 금액에 음수 없음).
1255
+ */
1256
+ declare function toFormalNotation(amount: number | bigint): string;
1257
+ /**
1258
+ * UI 축약 표기 — 소수 1자리 **내림**(금액 과대 표시 방지), `.0` 생략, 정수부 콤마.
1259
+ * - ≥1조 → `"X.Y조"` / ≥1억 → `"X.Y억"` / ≥1만 → 만 단위 내림 콤마 `"N,NNN만"`
1260
+ * - <1만 → 콤마 숫자 그대로(단위·"원" 없음), 음수는 `-` 접두.
1261
+ */
1262
+ declare function abbreviateAmount(amount: number | bigint): string;
1263
+
1264
+ /**
1265
+ * 영업일 계산기 — 공휴일 데이터 주입형.
1266
+ *
1267
+ * 원천: Java `BusinessDays` / Python `business_days.py` 와 동일 규칙 (수동 동기화).
1268
+ *
1269
+ * **내장 공휴일 테이블은 없다** — 한국 공휴일은 대체공휴일·임시공휴일 등으로
1270
+ * 매년 변하므로 라이브러리에 박제하지 않고 소비자가 주입한다.
1271
+ *
1272
+ * 날짜는 **`"YYYY-MM-DD"` 문자열 고정** (`Date` 객체 금지 — 타임존 함정 차단).
1273
+ * 형식 위반·실존하지 않는 날짜는 RangeError. 내부 요일·가감 연산은 UTC 기준
1274
+ * epoch day 산술이라 실행 환경 타임존의 영향을 받지 않는다.
1275
+ */
1276
+ /** 영업일 계산기 — {@link createBusinessDays} 로 생성한다. */
1277
+ interface BusinessDays {
1278
+ /** 영업일 여부 — 주말(토·일) 또는 주입된 공휴일이면 false. */
1279
+ isBusinessDay(date: string): boolean;
1280
+ /**
1281
+ * 영업일 n일 가감(n 음수 허용). **n=0 은 입력 날짜 그대로 반환한다(스냅 없음)** —
1282
+ * 비영업일 입력도 그대로.
1283
+ */
1284
+ addBusinessDays(date: string, n: number): string;
1285
+ /** 다음 영업일 (엄격 초과 — 입력이 영업일이어도 그다음을 찾는다). */
1286
+ nextBusinessDay(date: string): string;
1287
+ /** 이전 영업일 (엄격 미만). */
1288
+ previousBusinessDay(date: string): string;
1289
+ /** 반개구간 `[start, endExclusive)` 의 영업일 수. `start > end` 는 RangeError. */
1290
+ countBusinessDays(start: string, endExclusive: string): number;
1291
+ }
1292
+ interface BusinessDaysOptions {
1293
+ /** 공휴일 목록 — `"YYYY-MM-DD"` 문자열 (불변 복사). */
1294
+ holidays: Iterable<string>;
1295
+ }
1296
+ /**
1297
+ * 공휴일을 주입해 영업일 계산기를 만든다.
1298
+ *
1299
+ * ```ts
1300
+ * const bd = createBusinessDays({ holidays: ["2026-01-01"] });
1301
+ * bd.addBusinessDays("2025-12-31", 1); // "2026-01-02" — 1/1 스킵
1302
+ * ```
1303
+ */
1304
+ declare function createBusinessDays(options: BusinessDaysOptions): BusinessDays;
1305
+
1306
+ /**
1307
+ * 한글 자모 분해·결합 + 혼합 질의 전방일치 매처.
1308
+ *
1309
+ * 원천: Java `HangulUtils` / Python `text/jamo.py` 와 동일 규칙 (수동 동기화).
1310
+ * 초성만 뽑는 기존 `chosung.ts`(무변경·하위호환)와 별개 모듈이다.
1311
+ *
1312
+ * 분해 정책(확정): **두벌식 키보드 자모열** — 초성·중성·종성을 호환 자모
1313
+ * (U+3131~U+3163)로 펼치되,
1314
+ * - 복합 모음 분해: ㅘ→ㅗㅏ, ㅙ→ㅗㅐ, ㅚ→ㅗㅣ, ㅝ→ㅜㅓ, ㅞ→ㅜㅔ, ㅟ→ㅜㅣ, ㅢ→ㅡㅣ
1315
+ * - 겹받침 분해: ㄳ→ㄱㅅ, ㄵ→ㄴㅈ, ㄶ→ㄴㅎ, ㄺ→ㄹㄱ, ㄻ→ㄹㅁ, ㄼ→ㄹㅂ,
1316
+ * ㄽ→ㄹㅅ, ㄾ→ㄹㅌ, ㄿ→ㄹㅍ, ㅀ→ㄹㅎ, ㅄ→ㅂㅅ
1317
+ * - **쌍자음(ㄲㄸㅃㅆㅉ·종성 ㄲㅆ)은 분해하지 않는다**(시프트 입력 = 단일 타건)
1318
+ * - 완성 음절이 아닌 문자(비한글·단독 자모)는 그대로 통과 — 단독 복합 자모
1319
+ * (ㅘ·ㄳ 등)도 분해하지 않는다 (Java/Python 동일 정책)
1320
+ *
1321
+ * 결합은 역변환(왼쪽부터 탐욕) — 완성 음절 입력 기준 `compose(decompose(s)) == s`
1322
+ * 왕복이 보장된다. 음절을 이루지 못하는 자모는 그대로 출력한다.
1323
+ */
1324
+ /**
1325
+ * 문자열을 두벌식 키보드 자모열로 분해한다 (`"값"` → `"ㄱㅏㅂㅅ"`).
1326
+ * 완성 음절은 초·중·종성으로, 복합 모음·겹받침은 구성 자모로 펼친다.
1327
+ * 쌍자음은 분해하지 않고, 완성 음절이 아닌 문자(비한글·단독 자모)는 그대로
1328
+ * 통과한다 (Java `HangulUtils.decompose` / Python `jamo.decompose` 동일 정책).
1329
+ */
1330
+ declare function decomposeHangul(s: string): string;
1331
+ /**
1332
+ * 자모열을 완성 음절로 결합한다 — {@link decomposeHangul} 의 역변환(왼쪽부터 탐욕).
1333
+ * 초성→중성(다음 모음과 복합 결합 시도)→종성(다음 문자가 모음이면 초성으로 양보,
1334
+ * 겹받침 결합 후에도 다음이 모음이면 둘째를 양보) 순으로 채운다.
1335
+ * 음절을 이루지 못하는 자모·비한글 문자는 그대로 출력한다.
1336
+ */
1337
+ declare function composeHangul(s: string): string;
1338
+ /**
1339
+ * 혼합 질의 전방일치 매처 — 완성 음절·단독 자음·진행 중 음절이 섞인 질의가
1340
+ * 대상의 접두인지 판정한다 (`matchesHangul("ㅎ길", "홍길동")` → true).
1341
+ *
1342
+ * - 마지막이 아닌 질의 문자: 완성 음절은 대상과 정확 일치, 단독 자음(ㄱ~ㅎ)은
1343
+ * 대상 음절의 초성 일치, 비한글은 소문자화 후 비교(toChosung 관례)
1344
+ * - 마지막 질의 문자: **자모열 전방일치** — `decompose(질의)` 가
1345
+ * `decompose(대상)` 의 접두면 매치 (`"셔"`→`"션"`, `"ㄷ"`→`"동"` 커버)
1346
+ * - 빈 질의는 true, 질의가 대상보다 길면 false
1347
+ */
1348
+ declare function matchesHangul(query: string, target: string): boolean;
1349
+
1350
+ export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, 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 FeatureFlagReader, type FieldErrorDetail, type FileKind, type JosaPair, type ListQueryOptions, type PageResponse, type PhoneType, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createTokenBucket, createTtlCache, decodeJwtPayload, decomposeHangul, formatPhoneNumber, generateIdempotencyKey, getTokenExpiry, isBulkResult, isChosungQuery, isForeignerRrn, isRetryableStatus, isTokenExpired, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseFlag, parseRetryAfterMs, parseSseFrame, parseWireDateTime, pickJosa, readSseStream, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };