import { HttpResponse } from '@iyulab/http-client'; import { LocaleNamespace } from '@iyulab/components/dist/utilities/Locale.js'; export declare class ApiConfig { private static _baseUrl; private static _odataPrefix; private static _apiPrefix; private static _isDevelopment; /** * Initialize API configuration * @param options Configuration options */ static initialize(options?: ApiConfigOptions): void; /** * API Base URL (always relative path by default) */ static get baseUrl(): string; /** * Manually set Base URL (for testing or custom configurations) */ static setBaseUrl(url: string): void; /** * OData endpoint prefix */ static get odataPrefix(): string; /** * REST API endpoint prefix */ static get apiPrefix(): string; /** * Generate OData endpoint URL * @param entityName Entity name for OData endpoint */ static getODataUrl(entityName: string): string; /** * Generate REST API endpoint URL * @param endpoint API endpoint path */ static getApiUrl(endpoint: string): string; /** * Generate full URL with query parameters * @param endpoint Endpoint path * @param params Query parameters */ static getUrlWithParams(endpoint: string, params: Record): string; /** * Check if running in development environment * Tries to detect from various bundler environments */ static get isDevelopment(): boolean; /** * Check if running in production environment */ static get isProduction(): boolean; /** * Reset configuration to defaults */ static reset(): void; } /** * API configuration and environment-based endpoint management * * Supports both development (proxy) and production (same-origin) deployments */ export declare interface ApiConfigOptions { /** Base URL for all API requests (default: ''). A trailing `/` is ignored — `'/'` is the same as `''`. */ baseUrl?: string; /** OData endpoint prefix (default: '$data') */ odataPrefix?: string; /** REST API endpoint prefix (default: 'api') */ apiPrefix?: string; /** Force development mode detection */ isDevelopment?: boolean; } /** * API 호출 실패 에러 — HTTP status 를 실어 호출부가 상태별 분기(예: 404 도메인 문구)를 할 수 있게 한다. * `Error` 를 상속하므로 기존 `e instanceof Error`/`e.message` 소비처는 그대로 동작한다. */ export declare class ApiError extends Error { readonly status: number; /** * 서버가 정한 거절 코드 — OData v4 오류 봉투의 `error.code`(없으면 최상위 `code` → `Code`). 같은 상태 안에서 * 실패를 가른다(예: 403 중 «비밀번호 변경 필요»). 서버가 주지 않았으면 undefined. */ readonly code?: string; /** OData v4 오류 봉투의 `error.details`(필드별 검증 상세) — 서버 응답에 없거나 파싱 실패면 undefined. */ readonly details?: ApiErrorDetail[]; /** * 실패 응답의 본문 — JSON 이면 파싱한 값, 아니면 텍스트. 비었으면 undefined. 거절이 싣는 «충돌의 현재 상태» * (409 의 새 판정, 412 의 현재 표현 등)를 같은 요청을 다시 보내지 않고 읽는 자리다. 모양은 서버 계약이라 * `unknown` 이다 — 호출부가 좁힌다. `formatError` 가 받는 `body` 와 같은 값이다. * * 표준 `cause` 처럼 **열거되지 않는다** — 에러를 직렬화(로그 · 원격 보고)해도 본문(서버의 `innererror` 같은 디버깅 * 정보 포함)이 따라 나가지 않는다. 읽기는 `err.body` 로 그대로 된다. */ readonly body?: unknown; constructor(message: string, status: number, init?: ApiErrorInit); /** * 서비스가 이 실패를 `notify.error` 로 **이미 사용자에게 알렸는가** — 경계가 «알리지 않은 것만» 알리는 근거. * `notify.error` 를 설정하지 않은 서비스에서는 알린 것이 없으므로 항상 `false` 다. 읽기 전용이다. */ get notified(): boolean; } /** * OData v4 오류 봉투의 `error.details` 항목 — 필드별 검증 실패 상세. * * 규격(OData JSON Format v4.0)상 각 항목은 `code`/`message` 를 **반드시** 갖고 * `target`(오류가 난 속성 이름)은 **선택**이다 — 필수로 선언하면 target 을 생략한 * 서버 응답에서 타입이 거짓말을 하게 된다. */ export declare interface ApiErrorDetail { code: string; message: string; target?: string; } /** `ApiError` 의 선택 필드. */ export declare interface ApiErrorInit { /** 서버의 거절 코드 — `ApiError.code`. */ code?: string; /** 검증을 마친 `error.details` 항목 — `ApiError.details`. */ details?: ApiErrorDetail[]; /** 실패 응답의 본문 — `ApiError.body`. */ body?: unknown; /** 응답 없이 실패했을 때(네트워크·오프라인) 전송이 던진 예외 — 표준 `Error.cause` 로 실린다. 진단용이다. */ cause?: unknown; } /** 컨트롤에 내려간 항목 하나. */ export declare interface AppliedFieldError { detail: ApiErrorDetail; control: HTMLElement; /** 컨트롤에 실린 문장(`options.message` 를 거친 것). */ message: string; } /** * 서버가 거절한 필드마다 그 컨트롤에 메시지를 얹고 오류를 보인다 — 저장 실패가 «어느 칸» 의 일인지 칸 옆에 말한다. * * - `target` 과 같은 `name` 을 가진 컨트롤(`root` 안, 문서 순서로 첫 것)에 `setCustomValidity(message)` 하고, * `@iyulab/components` 컨트롤이면 `validate()` 로 오류를 보인다. 같은 `target` 이 여럿이면 문장을 잇는다. * - 사용자가 그 칸을 고치면(`input`·`change`) 그 메시지를 걷는다 — 고친 값을 다시 보내는 것을 막지 않는다. * - 다시 부르면 앞서 얹은 메시지를 먼저 걷는다. * - 컨트롤로 못 내려간 항목은 `formLevel` 로 돌려준다 — 요약에 싣는다. * - ⚠저장하는 동안 폼을 잠갔다면(`
`) **푼 뒤에** 부른다 — 비활성 컨트롤은 제약 검증에서 * 빠지므로(플랫폼 규칙) 메시지는 실려도 오류가 보이지 않는다. * * ```ts * try { await svc.odataPatch('Orders', id, draft) } * catch (e) { * const { applied, formLevel } = applyFieldErrors(form, e) * showSummary(applied, formLevel) // 칸 링크 + 폼 단위 줄 — 그리고 요약으로 포커스 * } * ``` * * @param source `ApiError`(그 `details`) 또는 상세 배열. 그 밖의 값(네트워크 오류 등)은 빈 결과다. */ export declare function applyFieldErrors(root: ParentNode, source: unknown, options?: ApplyFieldErrorsOptions): FieldErrorsResult; export declare interface ApplyFieldErrorsOptions { /** * 보일 문장 — 기본은 `detail.message`(서버가 사람에게 쓴 문장). `detail.code` 로 지역화할 때 쓴다 * (예: 값을 그 타입으로 읽지 못한 `Unconvertible` 만 앱의 언어로). */ message?: (detail: ApiErrorDetail) => string; } export declare interface AuthClient { /** 현재 세션 조회. 던지지 않는다 — 결과는 `SessionState` 의 셋 중 하나다. */ fetchMe(options?: AuthRequestOptions): Promise>; /** 로그인. 던지지 않는다. 성공 시 권한 store 자동 갱신(getPermissions 지정 시). */ login(credentials: TCredentials, options?: AuthRequestOptions): Promise>; /** 로그아웃. 던지지 않는다 — 결과는 `LogoutResult`. 성공 시에만 권한 store 자동 clear(getPermissions 지정 시). */ logout(options?: AuthRequestOptions): Promise; } export declare interface AuthClientConfig { /** 현재 세션 조회 URL (GET) */ meUrl: string; /** 로그인 URL (POST, 자격증명을 JSON 바디로) */ loginUrl: string; /** 로그아웃 URL (POST) */ logoutUrl: string; /** 상대 URL 앞에 붙일 오리진(기본 '' = same-origin). */ baseUrl?: string; /** fetch credentials 모드(기본 'same-origin' — 쿠키 세션). */ credentials?: RequestCredentials; /** 사용자 대면 문구. 지정한 키만 쓰고, 나머지는 패키지 로캘 레지스트리(`messages` — 영어 · 한국어 내장)를 따른다. */ messages?: Partial; /** * 서버 거절 문장을 보이는 규칙(기본 `'always'`) — 데이터 서비스의 같은 설정과 같은 뜻이다(`ServerMessages`). * `'domain-codes'` 면 상태 이름뿐인 코드(`Forbidden` 등)의 문장 대신 각 동작의 로캘 문구를 쓴다. * `extractLoginError` 를 주면 로그인 실패 문장은 그것이 정한다. */ serverMessages?: ServerMessages; /** * 로그인 실패(non-401) 응답 바디에서 사용자 대면 메시지 추출. 기본: OData 봉투 `error.message` → * `message` → `Message`. `error.code`·`error.details` 는 이 함수와 무관하게 `LoginResult.error` 에 실린다. */ extractLoginError?: (body: unknown) => string | undefined; /** * user 에서 권한 코드 배열을 추출. 지정하면 «인증됨» 에서 권한 store 를 갱신하고, «미인증»·성공한 `logout` 에서 * 비운다(실패한 로그아웃은 세션이 살아 있으므로 건드리지 않는다). «모름» 에는 건드리지 않는다(마지막으로 알던 권한이 남고, 처음이면 store 는 계속 «모름»). */ getPermissions?: (user: TUser) => string[]; /** 권한 자동 갱신 대상 store(기본: `defaultPermissionStore`). */ permissionStore?: PermissionStore; /** * 로그인에 성공한 뒤 세션 사용자를 어디서 얻는가. * - `'body'`(기본): 로그인 응답 본문이 곧 세션 사용자(`me` 와 같은 모양)인 서버. * - `'me'`: 로그인 뒤 `meUrl` 로 다시 묻는다 — 로그인 응답이 봉투(`{ Success, User, Token }`)처럼 세션 사용자 모양이 아닌 * 서버. 쿠키 세션에서 세션 사용자의 정본은 `me` 라 `LoginResult.user` 의 타입과 권한(`getPermissions`)이 그것을 따른다. * 그 조회가 «인증됨» 이 아니면 로그인은 실패로 돌아온다(쿠키가 서지 않은 것이다). */ loginUser?: 'body' | 'me'; } export declare interface AuthClientMessages { /** 401 로그인 실패 */ invalidCredentials: string; /** 기타 로그인 실패(non-401) 폴백 — 서버 메시지가 없을 때 */ loginFailed: string; /** 세션 조회가 2xx·401 이 아닌 답을 받았을 때의 폴백 — 서버 메시지가 없을 때 */ sessionCheckFailed: string; /** 로그아웃이 2xx·401 이 아닌 답을 받았을 때의 폴백 — 서버 메시지가 없을 때 */ logoutFailed: string; /** 응답을 받지 못함(네트워크·오프라인) */ networkError: string; /** 2xx 응답의 본문을 읽지 못함 */ invalidResponse: string; /** 호출자의 `signal` 이 요청을 끊음 */ canceled: string; } /** * 한 호출에만 적용되는 옵션. * - `signal`: 이 호출을 끊을 표준 신호(`fetch` 의 `signal` 과 같다) — 서버가 오래 답하지 않을 때 시간 예산을 걸거나, * 화면을 떠나며 지난 요청을 버리는 자리. 끊겨도 **던지지 않는다**: `fetchMe` 는 `{ status: 'unknown' }`, * `login`·`logout` 은 `{ ok: false }` 이고, `error.status` 는 `0`, `error.cause` 는 `signal.reason` 이다. * 끊긴 호출은 권한 store 를 건드리지 않는다. */ export declare interface AuthRequestOptions { signal?: AbortSignal; } /** * `source` 를 `view` 에 묶는다. 돌려주는 함수가 묶음을 푼다(구독 해지 · 리스너 제거). * * - **쓴다**: `data` · `totalCount` · `loading` · `error` · `sortCriteria` · `page` · `pageSize` — 요소가 가진 것만, 소스 값이 * 바뀐 때만. 그리고 요소에 `dataMode` 가 있으면 `'server'` 로 — 질의(정렬 · 검색 · 페이지)는 소스가 하므로 뷰가 받은 한 * 페이지를 다시 거르거나 나누면 안 된다. * - **듣는다**: `sort-change {criteria}` → `setSort` · `page-change {page, pageSize?}` → `setPage`(크기가 바뀌었으면 * `setPageSize` — 첫 장으로) · `search {query}` → `setSearch`(목록 검색 칸 — `u-input type="search"` 가 검색을 확정할 때 내는 모양). * * 선택(`selection-change`)과 행 열기(`row-activate`)는 묶지 않는다 — 소스의 것이 아니라 화면의 것이라, 뷰에서 직접 듣는다. * * ```ts * const orders = createODataSource('/api/orders', { pageSize: 20 }); * const unbind = [bindSource(orders, table), bindSource(orders, pager), bindSource(orders, searchBox)]; * ``` */ export declare function bindSource(source: ViewSource, view: EventTarget): () => void; /** * `root` 안에서 이 모듈이 얹은 서버 메시지를 전부 걷는다 — 다시 저장하기 전 상태로. `applyFieldErrors` 는 적용 * 전에 이것을 먼저 하므로 보통은 부를 일이 없다(폼을 닫거나 처음 값으로 되돌릴 때). */ export declare function clearFieldErrors(root: ParentNode): void; /** 기본 store 권한을 비운다. */ export declare const clearPermissions: () => void; export declare function createAuthClient>(config: AuthClientConfig): AuthClient; /** * OData v4 + custom REST 서비스를 생성한다. * 반환된 서비스는 상태를 공유하지 않는 순수 클로저이므로 여러 개를 만들어도 안전하다(테스트 격리에도 유리). */ export declare function createODataService(config: ODataServiceConfig): ODataService; /** * 독립적인 권한 store 를 생성한다(테스트 격리·다중 컨텍스트에 유리). `initial` 을 주면 처음부터 * «알려짐» 이고, 주지 않으면 «모름» 으로 시작한다. */ export declare function createPermissionStore(initial?: Iterable): PermissionStore; /** * Date manipulation and formatting utility class */ export declare class DateHelper { /** * Format date to YYYY-MM-DD string — the local calendar date * @param date - Date to format (Date object or ISO string; `YYYY-MM-DD` is read as a local date) */ static formatDate(date: Date | string | null | undefined): string; /** * Format date to localized string * @param date - Date to format * @param locale - Locale for formatting (default: the app locale — `Locale.get()` from `@iyulab/components`) * @param options - Intl.DateTimeFormat options */ static formatLocalDate(date: Date | string | null | undefined, locale?: string, options?: Intl.DateTimeFormatOptions): string; /** * Format datetime to YYYY-MM-DD HH:mm string — local date and time * @param date - Date to format */ static formatDateTime(date: Date | string | null | undefined): string; /** * Calculate days difference between two dates * @param startDate - Start date * @param endDate - End date * @returns Number of days (positive if endDate > startDate) */ static getDaysDifference(startDate: Date | string, endDate: Date | string): number; /** * Calculate days from now to target date * @param targetDate - Target date * @returns Days remaining (negative if overdue) */ static getDaysFromNow(targetDate: Date | string): number; /** * Check if date is today */ static isToday(date: Date | string): boolean; /** * Check if date is in the past */ static isPast(date: Date | string): boolean; /** * Check if date is in the future */ static isFuture(date: Date | string): boolean; /** * Add days to date * @param date - Base date * @param days - Number of days to add (can be negative) */ static addDays(date: Date | string, days: number): Date; /** * Get start of day (00:00:00) */ static startOfDay(date: Date | string): Date; /** * Get end of day (23:59:59.999) */ static endOfDay(date: Date | string): Date; } /** * 기본 싱글톤 권한 store. 앱 전역에서 `hasPermission(code)` 를 어디서나 호출하는 * 흔한 패턴을 위해 free 함수로 바인딩해 노출한다. 격리가 필요하면 `createPermissionStore()` 를 쓴다. */ export declare const defaultPermissionStore: PermissionStore; /** * Display string for an absent value (`null`/`undefined`/unparsable), shared by every * formatting helper in this package so "no value" renders identically everywhere and * never collides with a real `0`. */ export declare const EMPTY_VALUE_DISPLAY = "\u2014"; /** * `@iyulab/enterprise` 의 화면 문자열 — **영어 기본 + 로케일 레지스트리**. * * ## 왜 이 파일이 생겼나 * * 헬퍼들이 라벨을 **한국어 리터럴로** 들고 있었다(실측 14건). 이 리포는 이미 로케일 표준을 * 채택했지만(영어 기본 + 레지스트리) **강제하는 장치가 없어서** 채택 이후에 들어온 코드가 * 한국어 기본값을 갖고 있었다. * * ⚠**「의견 있음」이 정당화하는 것은 스타일 값의 의견이지 언어가 아니다**(2026-08-04 결정). * * ## 왜 자체 레지스트리를 만들지 않았나 * * `@iyulab/components` 의 `Locale.namespace()`(1.23.0~)를 쓴다. 자체 구현은 이 조직의 * **네 번째 레지스트리**가 됐을 것이고, 그러면 소비자가 앱 하나에서 로케일을 **두 번** * 전환해야 한다. `Locale.set()` 한 번으로 검증 메시지·이 라벨이 함께 따라온다. * * ## 동작 변화 * * ⚠기본 로케일은 브라우저 언어에서 감지된다 — **한국어 브라우저는 종전과 같은 문구**를 본다 * (`ko-KR` → `ko`). 그 밖의 환경은 한국어 대신 **영어**를 본다. 그것이 이 이주의 목적이다. */ export declare type EnterpriseMessageKey = 'progressNotStarted' | 'progressEarly' | 'progressInProgress' | 'progressPastMid' | 'progressAlmost' | 'progressDone' | 'urgencyOverdue' | 'urgencyCritical' | 'urgencyUrgent' | 'urgencySoon' | 'urgencyNormal' | 'daysOverdue' | 'daysToday' | 'daysRemaining' | 'saved' | 'updated' | 'deleted' | 'sessionExpired' | 'requestFailed' | 'http400' | 'http403' | 'http404' | 'http409' | 'http412' | 'http500' | 'authInvalidCredentials' | 'authLoginFailed' | 'authSessionCheckFailed' | 'authLogoutFailed' | 'authNetworkError' | 'authInvalidResponse' | 'authCanceled'; /** * 엔티티의 ETag(`@odata.etag`) — 조건부 쓰기(`ODataRequestOptions.ifMatch`)에 그대로 넘긴다. * 동시성 토큰을 선언한 엔티티만 갖는다. 없으면 `undefined`. * * ⚠값은 불투명하다 — 행 버전 필드(`RowVersion` 등)를 읽거나 비교하지 말고 이 값을 돌려보낸다. */ export declare function etagOf(entity: unknown): string | undefined; /** `applyFieldErrors` 의 결과 — 요약을 그리는 재료. */ export declare interface FieldErrorsResult { /** 컨트롤에 내려간 항목 — 요약에 칸으로 가는 링크로 싣는다(`control.focus()`). 문서 순서. */ applied: AppliedFieldError[]; /** * 폼 단위로 보여야 하는 항목 — `target` 이 없거나, 그 이름의 컨트롤이 `root` 안에 없다(서버 쪽 속성 경로 * `Lines(1)/Qty` 처럼 화면에 같은 이름이 없는 것 포함). 요약에 링크 없는 줄로 싣는다. */ formLevel: ApiErrorDetail[]; } /** 기본 store 의 현재 권한 집합. */ export declare const getPermissions: () => ReadonlySet; /** 기본 store 기준 모두 보유. */ export declare const hasAllPermissions: (codes: Iterable) => boolean; /** 기본 store 기준 하나라도 보유. */ export declare const hasAnyPermission: (codes: Iterable) => boolean; /** 기본 store 기준 단일 권한 보유 여부. */ export declare const hasPermission: (code: string) => boolean; /** * 로그인 결과 — 실패에도 `error`(`status`·`code`·`details`)가 실려, 401(자격 증명) · 429(시도 제한) · * 서버 거절 코드(예: 403 `password-change-required`)로 화면을 가를 수 있다. `message` 는 사용자 대면 문구다. * 응답이 없던 실패의 `error.status` 는 `0` 이다. */ export declare type LoginResult = /** `user` 는 세션 사용자(`loginUser` 가 정한 곳에서) · `body` 는 로그인 응답 본문 그대로(토큰 봉투 등 — 서버 계약이라 `unknown`). */ { ok: true; user: TUser; body: unknown; } | { ok: false; message: string; error: ApiError; }; /** * 로그아웃 결과 — 쿠키 세션에서 로그아웃의 효과는 **서버가 세션을 끊는 것** 이다. 그래서 «답을 못 받음» 은 실패다: * 그때 쿠키는 살아 있고, 화면이 «로그아웃됨» 으로 서면 공용 단말에서 다음 사람이 그 세션을 이어받는다. * - `ok: true`: 2xx, 또는 401(세션이 이미 없다는 서버의 답). * - `ok: false`: 그 밖의 답 · 네트워크 실패. 화면은 머무르고 `message` 를 알린다. `error.status` 는 응답이 없던 * 실패에서 `0` 이다. */ export declare type LogoutResult = { ok: true; } | { ok: false; message: string; error: ApiError; }; /** 이 패키지의 문자열 묶음. 소비자는 `messages.register(locale, table)` 로 덮거나 언어를 더한다. */ export declare const messages: LocaleNamespace; /** * `odataGet` 전용 옵션 — 요청 옵션에 **모을 행 수의 상한**을 더한다. * * 🔴**상한을 넘으면 던진다 — 자르지 않는다.** `odataGet` 의 계약은 «전량» 이라, 상한에서 조용히 멈추면 * 잘린 목록이 전량처럼 보이는 바로 그 실패로 돌아간다. 그 크기가 정상인 화면은 `odataGetPage` 로 * 페이지 단위로 읽는다. */ export declare interface ODataGetOptions extends ODataRequestOptions { /** * 모을 행 수의 상한(양의 정수). 서버 페이지를 따라가다 이 수를 **넘으면** 남은 페이지를 읽지 않고 * `RangeError` 를 던진다 — 정확히 이 수만큼 있는 컬렉션은 통과한다. 생략하면 상한이 없다. */ maxRows?: number; } /** `ODataServiceConfig.onMutated` 가 받는 정보 — 무엇이 어디에 쓰였는가. */ export declare interface ODataMutation { /** HTTP 메서드. */ method: 'POST' | 'PUT' | 'PATCH' | 'DELETE'; /** `odata*` 로 보낸 쓰기인가, `api*` 로 보낸 쓰기인가. `target` 의 뜻이 이것으로 갈린다. */ source: 'odata' | 'api'; /** `source: 'odata'` 면 엔티티 셋 이름, `'api'` 면 호출에 넘긴 경로 그대로. */ target: string; /** 키로 지정한 쓰기(`odataPatch`·`odataDelete`)의 키. */ id?: string; } /** * 서버가 자른 컬렉션의 한 페이지(OData v4 서버 주도 페이징). * * `nextLink` 는 서버가 준 **다음 페이지 URL** 이다 — 조립하지 말고 `odataGetNextPage` 에 그대로 넘긴다 * (`$skiptoken` 등 이어 읽기 상태가 그 안에 있다). 없으면 마지막 페이지다. */ export declare interface ODataPage { value: T[]; /** 응답의 `@odata.nextLink` — 서버가 페이지를 잘랐을 때만 있다. */ nextLink?: string; /** 응답의 `@odata.count` — `$count=true` 로 요청했을 때만 있다. */ count?: number; } /** * 한 호출에만 적용되는 요청 옵션 — 서비스 전역 정책(`ODataServiceConfig`)의 호출 단위 예외와, * 그 호출 하나에만 뜻이 있는 HTTP 표준 축(취소 신호 · 조건부 쓰기). * * 🔴**옵션을 생략한 호출은 이 타입이 생기기 전과 한 글자도 다르게 동작하지 않는다.** 기본값은 * 항상 «config 그대로» 이고, 요청에 아무것도 더하지 않는다. */ export declare interface ODataRequestOptions { /** * 이 호출을 취소할 표준 신호(`fetch` 의 `signal` 과 같다). 취소되면 **`signal.reason` 을 그대로** * 던진다(기본은 `AbortError` 인 `DOMException`) — `ApiError` 가 아니고, `notify.error` 도 * `onMutated` 도 부르지 않는다: 사용자가 버린 요청은 실패가 아니다. * 새 입력이 앞선 요청을 버리는 화면(연속 클릭 · 입력 중 조회)이 쓰는 자리다. */ signal?: AbortSignal; /** * 조건부 쓰기 — `If-Match` 헤더로 보낼 ETag. 읽을 때 받은 값(`etagOf(entity)`)을 그대로 넘긴다. * 그 사이 다른 사람이 레코드를 바꿨으면 서버가 412 로 거절하고, 서비스는 * `ApiError(…, 412)` 를 던진다(서버 문장이 없으면 `messages.http[412]`). 값을 계산하거나 비교하지 말 것 — 불투명하다. */ ifMatch?: string; /** * 이 호출에만 더할 요청 머리글 — 용도 표지(«이 조회는 내보내기다») · 추적 · 멱등 키처럼 호출 하나에만 뜻이 있는 것. * 서비스가 정하는 머리글(`Content-Type` 등)과 이름이 같으면 이것이 이긴다. `ifMatch` 를 함께 주면 `If-Match` 는 * `ifMatch` 가 정한다(같은 축을 두 자리에서 다르게 말하지 않게). 서버 주도 페이징을 따라가는 읽기는 페이지마다 싣는다. */ headers?: HeadersInit; /** * `false` → 이 호출의 401 을 «세션 만료» 로 취급하지 않는다: 전역 `onUnauthorized` 를 * 부르지 않고, `messages.sessionExpired` 로 덮어쓰지도 않으며, **서버가 준 메시지**로 * `ApiError(…, 401)` 을 던진다. 기본값(생략 시)은 전역 정책 그대로다. * * ⚠**로그인 자체가 이 옵션이 태어난 자리다** — 401 이 「세션이 끊겼다」가 아니라 * 「자격 증명이 틀렸다」를 뜻하는 유일한 호출이라, 전역 훅이 발화하면 로그인 화면에서 * 로그인 화면으로 리다이렉트되고 화면이 실패 사유를 **지어내야** 한다. * * ⚠**`authenticate()` 같은 이름 있는 프리미티브를 두지 않은 이유**: 그것은 엔드포인트 * 경로·바디 모양·토큰 처리 규약을 이 라이브러리가 안다고 가정하는 **도메인 이름**이다. * 그 규약은 앱마다 다르므로 adapter 에 남기고, 라이브러리는 범용 축만 연다. */ onUnauthorized?: false; } export declare interface ODataService { /** 설정된 prefix + baseUrl 로 OData 엔티티 URL 을 만든다(flex-table useODataSource 등에서 사용). */ odataUrl(entity: string): string; /** 설정된 prefix + baseUrl 로 REST 엔드포인트 URL 을 만든다. */ apiUrl(path: string): string; /** * OData GET(목록) — **전량**을 반환한다. 서버가 페이지를 자르면(`@odata.nextLink`) 끝까지 따라가 * 모은다. 화면이 페이지 단위로 읽어야 하면 `odataGetPage` 를 쓴다. * * ⚠`nextLink` 가 서비스의 오리진 밖을 가리키거나 이미 읽은 URL 로 되돌아오면 **던진다** — * 조용히 멈추면 잘린 목록이 전량처럼 보이고, 그것이 이 동작이 막으려는 실패다. * 모은 행이 `opts.maxRows` 를 넘어도 같은 이유로 던진다(`RangeError`). */ odataGet(entity: string, params?: Record, opts?: ODataGetOptions): Promise; /** OData GET(목록) 한 페이지 — `value` 와 함께 `nextLink`·`count` 를 준다. */ odataGetPage(entity: string, params?: Record, opts?: ODataRequestOptions): Promise>; /** `ODataPage.nextLink` 가 가리키는 다음 페이지. 오리진 규칙은 `odataGet` 과 같다. */ odataGetNextPage(nextLink: string, opts?: ODataRequestOptions): Promise>; /** OData GET(단건, key). */ odataGetById(entity: string, id: string, opts?: ODataRequestOptions): Promise; /** `$count=true&$top=0` — 데이터 없이 총 건수만. */ odataCount(entity: string, filter?: Record, opts?: ODataRequestOptions): Promise; /** OData POST(생성) — 토스트 없이 결과만(일괄 처리에서 토스트 폭주 방지). */ odataPostQuiet(entity: string, body: Partial, opts?: ODataRequestOptions): Promise; /** OData POST(생성) — 성공 시 `saved` 토스트. */ odataPost(entity: string, body: Partial, opts?: ODataRequestOptions): Promise; /** OData PATCH(수정) — 토스트 없이 결과만(자식 컬렉션 편집 후 부모의 파생 필드를 * 함께 동기화하는 등, 한 사용자 액션이 여러 mutation을 낼 때 토스트 폭주 방지). */ odataPatchQuiet(entity: string, id: string, body: Partial, opts?: ODataRequestOptions): Promise; /** OData PATCH(수정) — 성공 시 `updated` 토스트. */ odataPatch(entity: string, id: string, body: Partial, opts?: ODataRequestOptions): Promise; /** OData DELETE — 토스트 없이(`odataPatchQuiet`와 같은 이유). */ odataDeleteQuiet(entity: string, id: string, opts?: ODataRequestOptions): Promise; /** OData DELETE — 성공 시 `deleted` 토스트. */ odataDelete(entity: string, id: string, opts?: ODataRequestOptions): Promise; /** custom REST GET — 204 등 빈 바디를 안전 파싱. */ apiGet(path: string, opts?: ODataRequestOptions): Promise; /** custom REST POST — 실패 시 `error` 토스트 후 rethrow(401 제외). `body`가 `FormData` * 인스턴스면 그대로(직렬화 없이) 멀티파트로 전송된다 — `@iyulab/http-client`가 * Content-Type을 브라우저 자동 설정에 맡기고 JSON 직렬화 분기를 타지 않는다. * `apiPut`/`apiPatch`도 동일하게 동작한다. */ apiPost(path: string, body?: unknown, opts?: ODataRequestOptions): Promise; /** custom REST POST — 토스트 없이 결과만(`odataPostQuiet`와 같은 이유: 한 사용자 액션이 * 여러 요청을 내거나, 소비자가 자기 래퍼로 이미 통지할 때). */ apiPostQuiet(path: string, body?: unknown, opts?: ODataRequestOptions): Promise; /** custom REST PUT(리소스 전체 교체/생성) — 실패 시 `error` 토스트 후 rethrow(401 제외). * `body`의 `FormData` 처리는 `apiPost` 참조. */ apiPut(path: string, body?: unknown, opts?: ODataRequestOptions): Promise; /** custom REST PUT — 토스트 없이 결과만. */ apiPutQuiet(path: string, body?: unknown, opts?: ODataRequestOptions): Promise; /** custom REST PATCH — 실패 시 `error` 토스트 후 rethrow(401 제외). `body`의 `FormData` * 처리는 `apiPost` 참조. */ apiPatch(path: string, body?: unknown, opts?: ODataRequestOptions): Promise; /** custom REST PATCH — 토스트 없이 결과만. */ apiPatchQuiet(path: string, body?: unknown, opts?: ODataRequestOptions): Promise; /** custom REST DELETE — 실패 시 `error` 토스트 후 rethrow(401 제외). 대부분 204 No Content. */ apiDelete(path: string, opts?: ODataRequestOptions): Promise; /** custom REST DELETE — 토스트 없이 결과만. */ apiDeleteQuiet(path: string, opts?: ODataRequestOptions): Promise; /** * URL 을 직접 조립한 커스텀 조회(csv-export·연결 프로브 등)를 위해 응답을 **그대로** 반환한다. * * 🔴**비-2xx 에도 던지지 않고 `onUnauthorized` 도 부르지 않는다 — 판단은 호출부가 한다.** * 「raw」는 정책을 태우지 않는다는 뜻이고, 이 메서드가 존재하는 이유가 그것이다. * 상태에 따라 예외·토스트·세션 처리를 원하면 `apiGet` 을 쓴다. * * ⚠**0.15.0 이전에는 선언이 이렇게 적혀 있으면서 실제로는 비-2xx 에 던졌다**(`throwIfError` * 를 거쳤다). 그 어긋남 때문에 *"응답이 왔는가"* 만 묻는 연결 프로브가 401 을 「끊김」으로 * 세었다 — 서버가 살아서 거절한 것인데도. 선언이 옳고 구현이 틀렸던 자리라 구현을 고쳤다. * * `opts.signal` 로 끊으면 다른 메서드처럼 **`signal.reason` 을 그대로** 던진다 — 서버가 오래 답하지 않는 * 연결 확인에 시간 예산을 걸고 지난 요청을 끊는 자리다. */ fetchRaw(url: string, opts?: Pick): Promise; /** * flex-table `useODataSource` 에 주입할 공용 transport 옵션. * useODataSource 는 자체 fetcher 를 쓰므로 별도로 `onUnauthorized` 를 배선해야 401 처리가 걸린다. * * `onUnauthorized` 는 받은 응답이 **401 일 때만** `config.onUnauthorized(401)` 로 잇는다 — 서비스의 * 다른 경로(`throwIfError`)와 같은 의미다. 403(인증됐지만 권한 없음)은 세션 만료가 아니므로 * 통지하지 않고, 표가 자기 에러로 드러낸다. */ readonly sourceDefaults: { baseUrl: string; onUnauthorized: (response: Response) => void; }; /** `instanceof` 판정을 위해 재노출. (모듈 export `ApiError` 와 동일 클래스) */ readonly ApiError: typeof ApiError; } export declare interface ODataServiceConfig { /** 모든 요청의 베이스 URL (예: `window.location.origin`). 슬래시 없이 오리진만. */ baseUrl: string; /** OData 엔드포인트 prefix (기본 `$data`) */ odataPrefix?: string; /** custom REST 엔드포인트 prefix (기본 `api`) */ apiPrefix?: string; /** * 401 응답 시 호출된다(모든 메서드 공통). 세션 만료 리다이렉트는 앱이 결정한다. * 재진입 가드(한 번만 리다이렉트)도 앱 콜백 쪽에서 처리한다 — 라이브러리는 status만 통지. */ onUnauthorized?: (status: number) => void; /** 성공/실패 토스트 훅. 생략하면 토스트를 내지 않는다(순수 · 테스트 용이). */ notify?: { success?: (message: string) => void; error?: (message: string) => void; }; /** * 쓰기가 성공(2xx)으로 끝난 뒤 한 번 호출된다 — 목록·상세가 같은 데이터를 따로 들고 있는 * 앱이 캐시를 무효화하는 자리다. 조용한 변형(`*Quiet`)도 포함하고, 실패한 쓰기와 읽기에는 * 부르지 않는다. 이 콜백이 던져도 쓰기의 결과는 바뀌지 않는다(예외는 `reportError` 로 보고). */ onMutated?: (mutation: ODataMutation) => void; /** * 사용자 대면 문구. 지정한 키만 쓰고, 나머지는 패키지 로캘 레지스트리(`messages`)를 따른다 — `http` 도 상태별로 * 섞인다(`{ http: { 403: '…' } }` 는 403 만 바꾼다). */ messages?: Partial; /** 서버 거절 문장을 보이는 규칙(기본 `'always'`). `ServerMessages` 참조. */ serverMessages?: ServerMessages; /** * 에러 메시지 포매팅 오버라이드. 반환값이 있으면 그것을 에러 메시지로 사용한다. * (앱별 정책 — 예: 영문 raw OData 메시지를 로케일 친화 문구로 치환 — 을 여기에 둔다.) */ formatError?: (info: { status: number; statusText: string; rawMessage?: string; /** 서버의 거절 코드 — OData v4 `error.code`(없으면 최상위 `code` → `Code`). `ApiError.code` 와 같은 값. */ code?: string; /** OData v4 `error.details` — 검증을 마친 항목만 실린다(`ApiError.details` 와 같은 값). */ details?: ApiErrorDetail[]; body?: unknown; }) => string | undefined; } /** * 서비스가 토스트/에러에 쓰는 사용자 대면 문구. 설정에서 주지 않은 문구는 패키지 로캘 레지스트리(`messages` — * 영어 · 한국어 내장, `Locale.set()` 을 따른다)에서 그때그때 읽는다. */ export declare interface ODataServiceMessages { /** POST 성공 토스트 */ saved: string; /** PATCH 성공 토스트 */ updated: string; /** DELETE 성공 토스트 */ deleted: string; /** 401 세션 만료 시 throw 되는 에러 메시지 */ sessionExpired: string; /** status별 매핑이 없을 때의 폴백 */ requestFailed: string; /** 서버 문장을 보이지 않을 때(없음 · 200자 초과 · `serverMessages` 정책) status → 친화 메시지 */ http: Record; } /** 기본 store 의 권한이 알려졌는가(`PermissionStore.isKnown`). */ export declare const permissionsKnown: () => boolean; /** * 권한 스냅샷 store — 부팅 시점의 현재 사용자 권한 코드 집합을 보관하고 * `hasPermission`/`hasAny`/`hasAll` 판정을 제공한다. 프레임워크 무관(React·Lit 공통)이며 * 권한 코드는 **불투명 문자열**로만 다룬다(도메인 의미는 앱이 소유). * * 부팅 시점 스냅샷 모델 — 권한 변경은 다음 로그인(재-set)까지 반영되지 않는다. 단일 운영자 * 모델에 충분하며, 실시간 권한 회수/멀티테넌트가 필요하면 `subscribe` 로 반응형 확장 가능. * * **«알려짐» 축**: 아직 아무도 권한을 묻지 않았거나(부팅 직후) 물었지만 답을 못 받은 동안 store 는 * «모름» 이고, 그때의 빈 집합은 «권한 없음» 이 아니다. `set`·`clear` 가 store 를 «알려짐» 으로 만든다 * (`clear` = 알려진 빈 권한 — 로그아웃·미인증). 메뉴·버튼을 거르는 소비자는 `isKnown()` 이 거짓인 동안 * 숨기지도 막지도 말고 기다리면 된다. */ export declare interface PermissionStore { /** 현재 권한 집합을 통째로 교체한다(로그인/세션 조회 성공 시). */ set(codes: Iterable): void; /** 현재 권한 집합(읽기 전용). */ get(): ReadonlySet; /** 단일 권한 코드 보유 여부. */ has(code: string): boolean; /** 주어진 코드 중 하나라도 보유. 빈 목록은 `true`(제약 없음). */ hasAny(codes: Iterable): boolean; /** 주어진 코드를 모두 보유. 빈 목록은 `true`. */ hasAll(codes: Iterable): boolean; /** 권한을 비운다(로그아웃/세션 만료 시) — 알려진 «권한 없음» 이 된다. */ clear(): void; /** * 권한이 알려졌는가 — `set`·`clear` 뒤 참. 초기값 없이 만든 store 는 처음에 거짓이고, * 그동안의 빈 집합은 «권한 없음» 이 아니라 «아직 모름» 이다. */ isKnown(): boolean; /** 권한 또는 «알려짐» 변경 구독. 해제 함수를 반환한다. */ subscribe(listener: (codes: ReadonlySet) => void): () => void; } /** * Progress bar and percentage utility class */ export declare class ProgressHelper { /** * Get color based on progress percentage * @param progress - Progress value (0-100) * @param thresholds - Custom thresholds { high, medium } */ static getProgressColor(progress: number, thresholds?: { high?: number; medium?: number; }): string; /** * Validate and clamp progress to 0-100 range. * * Returns `null` for a missing/unparsable input rather than `0` — "no progress value yet" * and "0% progress" are different facts and must not render identically. * @param progress - Raw progress value * @param round - Whether to round to integer (default: true) */ static validateProgress(progress: number, round?: boolean): number; static validateProgress(progress: number | null | undefined, round?: boolean): number | null; /** * Convert decimal ratio to percentage. * * Returns `null` for a missing/unparsable input — see {@link validateProgress}. * @param ratio - Decimal ratio (0-1) * @param round - Whether to round to integer */ static ratioToPercent(ratio: number | null | undefined, round?: boolean): number | null; /** * Get progress label text * @param progress - Progress value (0-100) */ static getProgressLabel(progress: number): string; /** * Calculate progress from current and total values * @param current - Current value * @param total - Total value */ static calculateProgress(current: number, total: number): number; /** * Get CSS gradient for progress bar * @param progress - Progress value (0-100) */ static getProgressGradient(progress: number): string; } /** * 서버가 준 거절 문장(`error.message`)을 사용자에게 보이는 규칙. * - `'always'`(기본): 보인다 — OData JSON Format §21.2 에서 `code` 는 언어 독립 식별자, `message` 는 사람이 읽는 문장이다. * 범주 코드(`Forbidden`)에 구체 문장(«'maintenance.read' 권한이 필요합니다.»)을 싣는 서버가 정석이다. * - `'domain-codes'`: 거절 코드가 HTTP 상태 이름을 되풀이할 뿐이면(`Forbidden` · `NotFound` · `InternalServerError` …) * 그 문장은 프레임워크 상투어로 보고 로캘 문구(`messages.http`)를 쓴다. 도메인 코드(`KeyNotFound`)의 문장은 보인다. * 본문 없던 실패에 영어 기본 봉투를 싣는 서버를 그대로 쓰는 앱이 고른다. * * 어느 쪽이든 200자를 넘는 문장(내부 스택 등)은 보이지 않고, `code` · `body` 는 `ApiError` 에 실린다. */ export declare type ServerMessages = 'always' | 'domain-codes'; /** * 세션 조회 결과 — `status` 로 가른다. * - `authenticated`: 2xx 로 사용자를 받았다. * - `anonymous`: 서버가 401 로 «세션 없음» 이라 답했다. 로그인 화면으로 보낼 자리. * - `unknown`: 답을 못 받았다(401 이 아닌 비-2xx · 네트워크 실패 · 읽을 수 없는 본문). 세션이 있는지 * 모르므로 로그인 화면으로 보내지 말고 «닿지 못함» 을 알린 뒤 재시도할 자리. `error.status` 는 응답이 없던 * 실패에서 `0` 이다. */ export declare type SessionState = { status: 'authenticated'; user: TUser; } | { status: 'anonymous'; } | { status: 'unknown'; error: ApiError; }; /** 기본 store 의 권한을 교체한다(부팅 시 `setPermissions(user.Permissions)`). */ export declare const setPermissions: (codes: Iterable) => void; /** * Lit 어댑터 — 호스트가 그린 요소에 소스를 묶는다. 요소는 렌더 뒤에 생기므로 `targets` 를 함수로 받아 갱신마다 다시 찾고, * 새로 생긴 요소는 묶고 사라진 요소는 푼다. 호스트가 떨어지면 전부 푼다. * * ```ts * class OrdersPage extends LitElement { * private orders = createODataSource('/api/orders', { pageSize: 20 }); * private binding = new SourceBinding(this, this.orders, () => this.renderRoot.querySelectorAll('flex-table, u-pagination')); * render() { return html``; } * } * ``` */ export declare class SourceBinding { readonly source: ViewSource; private readonly targets; private bound; private connected; constructor(host: SourceBindingHost, source: ViewSource, targets: () => Iterable); hostConnected(): void; hostUpdated(): void; hostDisconnected(): void; private sync; } /** Lit `ReactiveControllerHost` 의 필요한 부분 — 구조적이라 이 패키지가 `lit` 에 의존하지 않는다. */ export declare interface SourceBindingHost { addController(controller: { hostConnected?(): void; hostDisconnected?(): void; hostUpdated?(): void; }): void; } /** * Urgency configuration */ export declare interface UrgencyConfig { /** Days threshold for critical urgency (default: 1) */ critical?: number; /** Days threshold for urgent level (default: 3) */ urgent?: number; /** Days threshold for soon level (default: 7) */ soon?: number; } /** * Urgency calculation and display utility class */ export declare class UrgencyHelper { private static defaultConfig; /** * Get urgency color based on days remaining * @param daysRemaining - Days until deadline (negative if overdue) * @param config - Custom urgency thresholds */ static getUrgencyColor(daysRemaining: number | null | undefined, config?: UrgencyConfig): string; /** * Get urgency level based on days remaining * @param daysRemaining - Days until deadline * @param config - Custom urgency thresholds */ static getUrgencyLevel(daysRemaining: number | null | undefined, config?: UrgencyConfig): UrgencyLevel; /** * Get urgency text label * @param daysRemaining - Days until deadline * @param config - Custom urgency thresholds * @param labels - Custom label text */ static getUrgencyText(daysRemaining: number | null | undefined, config?: UrgencyConfig, labels?: Partial>): string; /** * Get background and text color for urgency badge * @param daysRemaining - Days until deadline * @param config - Custom urgency thresholds */ static getUrgencyBadgeColors(daysRemaining: number | null | undefined, config?: UrgencyConfig): { bg: string; text: string; }; /** * Format days remaining as display text * @param daysRemaining - Days until deadline */ static formatDaysRemaining(daysRemaining: number | null | undefined): string; /** * Check if deadline is overdue */ static isOverdue(daysRemaining: number | null | undefined): boolean; /** * Check if deadline requires attention */ static needsAttention(daysRemaining: number | null | undefined, config?: UrgencyConfig): boolean; } /** * Urgency level types */ export declare type UrgencyLevel = 'overdue' | 'critical' | 'urgent' | 'soon' | 'normal'; /** * 뷰에 쓰는 속성 — 뷰 프로토콜의 정본(루트 `table-vocab-check` 가 같은 목록으로 두 표를 잰다). 요소가 그 속성을 갖고 쓸 수 * 있을 때만 쓴다. */ export declare const VIEW_PROPERTIES: readonly ["data", "totalCount", "loading", "error", "sortCriteria", "page", "pageSize"]; /** * `bindSource` — 프레임워크 중립 데이터 소스를 «뷰 어휘» 를 말하는 요소에 묶는다. * * 두 표(`flex-table` · `u-rich-table`) · 카드 뷰(`u-data-view`) · 페이저(`u-pagination`)가 같은 이름으로 상태를 받고 * 같은 모양으로 이벤트를 낸다(루트 `table-vocab-check` 가 그 어휘를 잰다). 그러므로 «목록 화면» 의 배선은 뷰마다 다르지 * 않다 — 소스 상태를 뷰의 속성에 쓰고, 뷰의 이벤트를 소스 조작으로 돌린다. 그 한 가지를 여기서 한다. * * 소스는 구조로만 안다(`ViewSource`) — `@iyulab/flex-table/odata` 의 `createODataSource`·`createArraySource` 가 그 모양이지만 * 이 패키지는 flex-table 에 의존하지 않는다. 뷰도 구조로만 안다 — 요소가 **가진** 속성만 쓰고(카드 뷰에는 정렬이 없다), * 이벤트를 내지 않는 요소에는 리스너가 그냥 아무 일도 하지 않는다. */ /** 정렬 기준 — 두 표와 소스가 주고받는 모양. */ export declare interface ViewSortCriterion { key: string; direction: 'asc' | 'desc'; } /** `bindSource` 가 읽고 조작하는 소스 — flex-table 의 두 소스가 이 모양이다. */ export declare interface ViewSource { getState(): ViewSourceState; subscribe(listener: () => void): () => void; setPage(page: number): void; setPageSize(size: number): void; setSort(criteria: ViewSortCriterion[]): void; setSearch(term: string): void; } /** 뷰에 쓰는 소스 상태 — `createODataSource(...).getState()` 와 같은 모양(구조적). */ export declare interface ViewSourceState { data: T[]; totalCount: number; loading: boolean; error: { message: string; } | null; page: number; pageSize: number; sortCriteria: ViewSortCriterion[]; search: string; } /** * 이 실패를 서비스가 `notify.error` 로 이미 사용자에게 알렸는가. * * 쓰기(`odataPost/Patch/Delete`·`apiPost/Put/Patch/Delete`) 실패는 알린 뒤 다시 던지고, 읽기·`*Quiet`·401 은 * 알리지 않는다 — 그 정책의 결과를 경계(전역 `unhandledrejection` 핸들러·error boundary)가 서비스 인스턴스 * 없이 읽는 자리다: `if (!wasNotified(err)) toast(message)`. `ApiError` 는 같은 사실을 `err.notified` 로도 준다. * 네트워크 실패처럼 `ApiError` 가 아닌 것도 쓰기 경로에서 알렸다면 `true` 다. */ export declare function wasNotified(error: unknown): boolean; export { } declare global { interface HTMLElementTagNameMap { 'u-list-page': import('@iyulab/enterprise/list-page').UListPage; } }