@iyulab/enterprise 0.13.0 → 0.15.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/CHANGELOG.md +57 -0
- package/README.md +60 -1
- package/dist/index.d.ts +660 -11
- package/dist/index.js +82 -49
- package/package.json +3 -2
- package/dist/ApiConfig.d.ts +0 -71
- package/dist/FormRow.d.ts +0 -33
- package/dist/FormSection.d.ts +0 -26
- package/dist/auth/AuthClient.d.ts +0 -46
- package/dist/auth/permissions.d.ts +0 -43
- package/dist/data/ODataService.d.ts +0 -124
- package/dist/helpers/CurrencyHelper.d.ts +0 -33
- package/dist/helpers/DateHelper.d.ts +0 -61
- package/dist/helpers/ProgressHelper.d.ts +0 -48
- package/dist/helpers/UrgencyHelper.d.ts +0 -62
- package/dist/helpers/constants.d.ts +0 -6
- package/dist/helpers/index.d.ts +0 -10
- package/dist/helpers/messages.d.ts +0 -26
package/dist/index.js
CHANGED
|
@@ -741,109 +741,138 @@ function createODataService(config) {
|
|
|
741
741
|
details
|
|
742
742
|
};
|
|
743
743
|
}
|
|
744
|
-
/**
|
|
745
|
-
|
|
744
|
+
/**
|
|
745
|
+
* 에러 확인 후 throw. 401 은 기본적으로 `onUnauthorized` 통지 후 세션 만료 에러로 단락한다.
|
|
746
|
+
*
|
|
747
|
+
* ⚠`opts.onUnauthorized === false` 면 그 단락을 건너뛰고 **다른 상태 코드와 똑같이** 다룬다 —
|
|
748
|
+
* 훅도 부르지 않고 메시지도 덮어쓰지 않는다(`ODataRequestOptions.onUnauthorized` 참조).
|
|
749
|
+
*/
|
|
750
|
+
async function throwIfError(res, opts) {
|
|
746
751
|
if (res.ok) return;
|
|
747
|
-
if (res.status === 401) {
|
|
752
|
+
if (res.status === 401 && opts?.onUnauthorized !== false) {
|
|
748
753
|
config.onUnauthorized?.(401);
|
|
749
754
|
throw new ApiError(messages.sessionExpired, 401);
|
|
750
755
|
}
|
|
751
756
|
const { message, details } = await extractErrorInfo(res);
|
|
752
757
|
throw new ApiError(message, res.status, details);
|
|
753
758
|
}
|
|
754
|
-
async function odataGet(entity, params) {
|
|
759
|
+
async function odataGet(entity, params, opts) {
|
|
755
760
|
const u = new URL(odataUrl(entity));
|
|
756
761
|
if (params) for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
|
|
757
762
|
const res = await client.get(u.toString());
|
|
758
|
-
await throwIfError(res);
|
|
763
|
+
await throwIfError(res, opts);
|
|
759
764
|
const json = await res.json();
|
|
760
765
|
return json.value ?? json;
|
|
761
766
|
}
|
|
762
|
-
async function odataGetById(entity, id) {
|
|
767
|
+
async function odataGetById(entity, id, opts) {
|
|
763
768
|
const res = await client.get(`${odataUrl(entity)}(${id})`);
|
|
764
|
-
await throwIfError(res);
|
|
769
|
+
await throwIfError(res, opts);
|
|
765
770
|
return res.json();
|
|
766
771
|
}
|
|
767
|
-
async function odataCount(entity, filter) {
|
|
772
|
+
async function odataCount(entity, filter, opts) {
|
|
768
773
|
const qs = buildQuery({
|
|
769
774
|
filter,
|
|
770
775
|
top: 0,
|
|
771
776
|
count: true
|
|
772
777
|
});
|
|
773
778
|
const res = await client.get(`${odataUrl(entity)}${qs}`);
|
|
774
|
-
await throwIfError(res);
|
|
779
|
+
await throwIfError(res, opts);
|
|
775
780
|
return (await res.json())["@odata.count"] ?? 0;
|
|
776
781
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
782
|
+
/**
|
|
783
|
+
* 쓰기 실패를 사용자에게 알린 뒤 그대로 다시 던진다.
|
|
784
|
+
*
|
|
785
|
+
* 🔴**이 서비스의 통지 축은 «쓰기 ↔ 읽기» 이지 «odata ↔ api» 가 아니다.** 조회
|
|
786
|
+
* (`odataGet`·`odataGetById`·`odataCount`·`apiGet`·`fetchRaw`)는 어느 쪽도 통지하지
|
|
787
|
+
* 않는다 — 화면이 비어 있는 것 자체가 신호이고, 목록 조회마다 토스트를 띄우면 읽을 수
|
|
788
|
+
* 없다. 쓰기는 **결과가 화면에 안 보일 수 있으므로** 통지한다: 서버가 409 와 사유를
|
|
789
|
+
* 돌려줘도 아무 일도 일어나지 않은 화면과 구별되지 않는다.
|
|
790
|
+
*
|
|
791
|
+
* ⚠**401 은 제외한다** — `onUnauthorized` 가 이미 안내하므로 토스트가 겹친다.
|
|
792
|
+
*
|
|
793
|
+
* ⚠성공 토스트는 **`odata*` 쓰기에만** 있다. `api*` 는 임의의 RPC(상태 전이·발행·업로드)를
|
|
794
|
+
* 태우는 경로라 «저장되었습니다» 같은 문구를 우리가 지어낼 수 없다 — 실패 메시지는 서버가
|
|
795
|
+
* 주므로 어느 엔드포인트에서나 뜻이 통하지만, 성공 문구는 그렇지 않다.
|
|
796
|
+
*/
|
|
797
|
+
async function notifyingWrite(run) {
|
|
783
798
|
try {
|
|
784
|
-
|
|
785
|
-
notifySuccess?.(messages.saved);
|
|
786
|
-
return result;
|
|
799
|
+
return await run();
|
|
787
800
|
} catch (e) {
|
|
788
801
|
if (!(e instanceof ApiError && e.status === 401)) notifyError?.(e instanceof Error ? e.message : messages.requestFailed);
|
|
789
802
|
throw e;
|
|
790
803
|
}
|
|
791
804
|
}
|
|
792
|
-
async function
|
|
793
|
-
|
|
805
|
+
async function odataPostQuiet(entity, body, opts) {
|
|
806
|
+
const res = await client.post(odataUrl(entity), normalizeBody(body));
|
|
807
|
+
await throwIfError(res, opts);
|
|
808
|
+
return res.json();
|
|
794
809
|
}
|
|
795
|
-
async function
|
|
796
|
-
|
|
797
|
-
await
|
|
810
|
+
async function odataPost(entity, body, opts) {
|
|
811
|
+
return notifyingWrite(async () => {
|
|
812
|
+
const result = await odataPostQuiet(entity, body, opts);
|
|
813
|
+
notifySuccess?.(messages.saved);
|
|
814
|
+
return result;
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
async function odataPatchQuiet(entity, id, body, opts) {
|
|
818
|
+
await throwIfError(await client.patch(`${odataUrl(entity)}(${id})`, normalizeBody(body)), opts);
|
|
819
|
+
}
|
|
820
|
+
async function odataPatch(entity, id, body, opts) {
|
|
821
|
+
return notifyingWrite(async () => {
|
|
822
|
+
await odataPatchQuiet(entity, id, body, opts);
|
|
798
823
|
notifySuccess?.(messages.updated);
|
|
799
|
-
}
|
|
800
|
-
if (!(e instanceof ApiError && e.status === 401)) notifyError?.(e instanceof Error ? e.message : messages.requestFailed);
|
|
801
|
-
throw e;
|
|
802
|
-
}
|
|
824
|
+
});
|
|
803
825
|
}
|
|
804
|
-
async function odataDeleteQuiet(entity, id) {
|
|
805
|
-
await throwIfError(await client.delete(`${odataUrl(entity)}(${id})`));
|
|
826
|
+
async function odataDeleteQuiet(entity, id, opts) {
|
|
827
|
+
await throwIfError(await client.delete(`${odataUrl(entity)}(${id})`), opts);
|
|
806
828
|
}
|
|
807
|
-
async function odataDelete(entity, id) {
|
|
808
|
-
|
|
809
|
-
await odataDeleteQuiet(entity, id);
|
|
829
|
+
async function odataDelete(entity, id, opts) {
|
|
830
|
+
return notifyingWrite(async () => {
|
|
831
|
+
await odataDeleteQuiet(entity, id, opts);
|
|
810
832
|
notifySuccess?.(messages.deleted);
|
|
811
|
-
}
|
|
812
|
-
if (!(e instanceof ApiError && e.status === 401)) notifyError?.(e instanceof Error ? e.message : messages.requestFailed);
|
|
813
|
-
throw e;
|
|
814
|
-
}
|
|
833
|
+
});
|
|
815
834
|
}
|
|
816
|
-
async function apiGet(path) {
|
|
835
|
+
async function apiGet(path, opts) {
|
|
817
836
|
const [p, ...q] = path.split("?");
|
|
818
837
|
const url = q.length ? `${apiUrl(p)}?${q.join("?")}` : apiUrl(p);
|
|
819
838
|
const res = await client.get(url);
|
|
820
|
-
await throwIfError(res);
|
|
839
|
+
await throwIfError(res, opts);
|
|
821
840
|
return parseJsonBody(res);
|
|
822
841
|
}
|
|
823
|
-
async function
|
|
842
|
+
async function apiPostQuiet(path, body, opts) {
|
|
824
843
|
const res = await client.post(apiUrl(path), body ?? {});
|
|
825
|
-
await throwIfError(res);
|
|
844
|
+
await throwIfError(res, opts);
|
|
826
845
|
return parseJsonBody(res);
|
|
827
846
|
}
|
|
828
|
-
async function
|
|
847
|
+
async function apiPost(path, body, opts) {
|
|
848
|
+
return notifyingWrite(() => apiPostQuiet(path, body, opts));
|
|
849
|
+
}
|
|
850
|
+
async function apiPutQuiet(path, body, opts) {
|
|
829
851
|
const res = await client.put(apiUrl(path), body ?? {});
|
|
830
|
-
await throwIfError(res);
|
|
852
|
+
await throwIfError(res, opts);
|
|
831
853
|
return parseJsonBody(res);
|
|
832
854
|
}
|
|
833
|
-
async function
|
|
855
|
+
async function apiPut(path, body, opts) {
|
|
856
|
+
return notifyingWrite(() => apiPutQuiet(path, body, opts));
|
|
857
|
+
}
|
|
858
|
+
async function apiPatchQuiet(path, body, opts) {
|
|
834
859
|
const res = await client.patch(apiUrl(path), body ?? {});
|
|
835
|
-
await throwIfError(res);
|
|
860
|
+
await throwIfError(res, opts);
|
|
836
861
|
return parseJsonBody(res);
|
|
837
862
|
}
|
|
838
|
-
async function
|
|
863
|
+
async function apiPatch(path, body, opts) {
|
|
864
|
+
return notifyingWrite(() => apiPatchQuiet(path, body, opts));
|
|
865
|
+
}
|
|
866
|
+
async function apiDeleteQuiet(path, opts) {
|
|
839
867
|
const res = await client.delete(apiUrl(path));
|
|
840
|
-
await throwIfError(res);
|
|
868
|
+
await throwIfError(res, opts);
|
|
841
869
|
return parseJsonBody(res);
|
|
842
870
|
}
|
|
871
|
+
async function apiDelete(path, opts) {
|
|
872
|
+
return notifyingWrite(() => apiDeleteQuiet(path, opts));
|
|
873
|
+
}
|
|
843
874
|
async function fetchRaw(url) {
|
|
844
|
-
|
|
845
|
-
await throwIfError(res);
|
|
846
|
-
return res;
|
|
875
|
+
return client.get(url);
|
|
847
876
|
}
|
|
848
877
|
return {
|
|
849
878
|
odataUrl,
|
|
@@ -859,9 +888,13 @@ function createODataService(config) {
|
|
|
859
888
|
odataDelete,
|
|
860
889
|
apiGet,
|
|
861
890
|
apiPost,
|
|
891
|
+
apiPostQuiet,
|
|
862
892
|
apiPut,
|
|
893
|
+
apiPutQuiet,
|
|
863
894
|
apiPatch,
|
|
895
|
+
apiPatchQuiet,
|
|
864
896
|
apiDelete,
|
|
897
|
+
apiDeleteQuiet,
|
|
865
898
|
fetchRaw,
|
|
866
899
|
sourceDefaults: {
|
|
867
900
|
baseUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/enterprise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Enterprise utilities and components for iyulab framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"enterprise",
|
|
@@ -56,12 +56,13 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@iyulab/components": "^1.27.0",
|
|
59
|
+
"@microsoft/api-extractor": "^7.58.8",
|
|
59
60
|
"@types/node": "^26.1.1",
|
|
60
61
|
"@types/react": "^19.2.14",
|
|
61
62
|
"typescript": "^6.0.2",
|
|
62
63
|
"vite": "^8.1.4",
|
|
63
64
|
"vite-plugin-dts": "^5.0.3",
|
|
64
|
-
"vitest": "^
|
|
65
|
+
"vitest": "^5.0.0"
|
|
65
66
|
},
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"react": ">=18",
|
package/dist/ApiConfig.d.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API configuration and environment-based endpoint management
|
|
3
|
-
*
|
|
4
|
-
* Supports both development (proxy) and production (same-origin) deployments
|
|
5
|
-
*/
|
|
6
|
-
export interface ApiConfigOptions {
|
|
7
|
-
/** Base URL for all API requests (default: '') */
|
|
8
|
-
baseUrl?: string;
|
|
9
|
-
/** OData endpoint prefix (default: '$data') */
|
|
10
|
-
odataPrefix?: string;
|
|
11
|
-
/** REST API endpoint prefix (default: 'api') */
|
|
12
|
-
apiPrefix?: string;
|
|
13
|
-
/** Force development mode detection */
|
|
14
|
-
isDevelopment?: boolean;
|
|
15
|
-
}
|
|
16
|
-
export declare class ApiConfig {
|
|
17
|
-
private static _baseUrl;
|
|
18
|
-
private static _odataPrefix;
|
|
19
|
-
private static _apiPrefix;
|
|
20
|
-
private static _isDevelopment;
|
|
21
|
-
/**
|
|
22
|
-
* Initialize API configuration
|
|
23
|
-
* @param options Configuration options
|
|
24
|
-
*/
|
|
25
|
-
static initialize(options?: ApiConfigOptions): void;
|
|
26
|
-
/**
|
|
27
|
-
* API Base URL (always relative path by default)
|
|
28
|
-
*/
|
|
29
|
-
static get baseUrl(): string;
|
|
30
|
-
/**
|
|
31
|
-
* Manually set Base URL (for testing or custom configurations)
|
|
32
|
-
*/
|
|
33
|
-
static setBaseUrl(url: string): void;
|
|
34
|
-
/**
|
|
35
|
-
* OData endpoint prefix
|
|
36
|
-
*/
|
|
37
|
-
static get odataPrefix(): string;
|
|
38
|
-
/**
|
|
39
|
-
* REST API endpoint prefix
|
|
40
|
-
*/
|
|
41
|
-
static get apiPrefix(): string;
|
|
42
|
-
/**
|
|
43
|
-
* Generate OData endpoint URL
|
|
44
|
-
* @param entityName Entity name for OData endpoint
|
|
45
|
-
*/
|
|
46
|
-
static getODataUrl(entityName: string): string;
|
|
47
|
-
/**
|
|
48
|
-
* Generate REST API endpoint URL
|
|
49
|
-
* @param endpoint API endpoint path
|
|
50
|
-
*/
|
|
51
|
-
static getApiUrl(endpoint: string): string;
|
|
52
|
-
/**
|
|
53
|
-
* Generate full URL with query parameters
|
|
54
|
-
* @param endpoint Endpoint path
|
|
55
|
-
* @param params Query parameters
|
|
56
|
-
*/
|
|
57
|
-
static getUrlWithParams(endpoint: string, params: Record<string, string | number | boolean | undefined>): string;
|
|
58
|
-
/**
|
|
59
|
-
* Check if running in development environment
|
|
60
|
-
* Tries to detect from various bundler environments
|
|
61
|
-
*/
|
|
62
|
-
static get isDevelopment(): boolean;
|
|
63
|
-
/**
|
|
64
|
-
* Check if running in production environment
|
|
65
|
-
*/
|
|
66
|
-
static get isProduction(): boolean;
|
|
67
|
-
/**
|
|
68
|
-
* Reset configuration to defaults
|
|
69
|
-
*/
|
|
70
|
-
static reset(): void;
|
|
71
|
-
}
|
package/dist/FormRow.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* L2 폼 행 — 기본 2열, `full` 이면 1열.
|
|
4
|
-
*
|
|
5
|
-
* 오버라이드 계약(docs/lob-layers.md §2)에 따라 `className`·`style` 을 받아 병합한다.
|
|
6
|
-
* `columns` 는 2열 고정이 맞지 않는 소비자가 **컴포넌트를 복제하지 않고** 조정하는 경로다
|
|
7
|
-
* — 복제가 시작되면 이 계층은 쓰기 전보다 나쁜 상태를 만든다.
|
|
8
|
-
*
|
|
9
|
-
* 🔴**트랙은 `minmax(0, 1fr)` 이지 `1fr` 이 아니다 — 순수 `1fr` 은 «균등» 을 약속하고**
|
|
10
|
-
* **지키지 않는다.** 그리드 아이템의 기본 `min-width: auto` 는 내용의 min-content 아래로
|
|
11
|
-
* 줄어들기를 거부하므로, 한 칸에 긴 내용이 들어오면 그 칸만 부풀고 나머지가 찌그러진다.
|
|
12
|
-
* 실측(400px 컨테이너 · 한 칸에 긴 불가분 문자열): 2열이 **425/8**, 3열이 **425/8/8** 이었고
|
|
13
|
-
* 행 자체가 컨테이너를 넘쳤다. `minmax(0, 1fr)` 은 트랙의 최소를 0 으로 만들어 **자식을**
|
|
14
|
-
* **건드리지 않고** 이를 고친다 — 같은 실측에서 **196/196** · **128/128/128**.
|
|
15
|
-
*
|
|
16
|
-
* ⚠**긴 불가분 내용 자체의 넘침은 별개 축이고 이 컴포넌트의 몫이 아니다.** 칸이 균등해져도
|
|
17
|
-
* 그 안의 긴 문자열은 여전히 자기 칸을 넘는다 — 그것은 셀의 `overflow-wrap` 이 답이다
|
|
18
|
-
* (`@iyulab/components` 의 엘리먼트는 이미 그 값을 갖는다). 같은 실측에서 `overflow-wrap` 을
|
|
19
|
-
* 주면 행의 넘침이 0 이 됐다.
|
|
20
|
-
*
|
|
21
|
-
* 계약은 `tests/form-layout-contract.test.ts` 가 고정한다 — 다만 그 파일은 **«선언» 을 재지**
|
|
22
|
-
* **«배치» 를 재지 않는다**(이 패키지엔 레이아웃을 계산하는 테스트 자리가 없다). 위 수치는
|
|
23
|
-
* 형제 패키지의 브라우저 프로젝트를 계측기로 빌린 일회성 탐침으로 쟀다.
|
|
24
|
-
*/
|
|
25
|
-
export declare function FormRow({ children, full, columns, className, style, }: {
|
|
26
|
-
children: ReactNode;
|
|
27
|
-
/** 한 행 전체를 한 칸으로 쓴다. `columns` 보다 우선한다. */
|
|
28
|
-
full?: boolean;
|
|
29
|
-
/** 열 수. 기본 2. */
|
|
30
|
-
columns?: number;
|
|
31
|
-
className?: string;
|
|
32
|
-
style?: CSSProperties;
|
|
33
|
-
}): import("react").JSX.Element;
|
package/dist/FormSection.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* L2 폼 블록 — 제목이 붙은 필드 묶음.
|
|
4
|
-
*
|
|
5
|
-
* 🔴**제목에 `text-transform: uppercase` 와 `letter-spacing` 을 걸지 않는다**(0.7.0 에서 제거).
|
|
6
|
-
* ⑴ 한글에는 대문자가 없어 `uppercase` 가 **아무 효과가 없고**, 제목에 영문이 섞이면
|
|
7
|
-
* 그것만 커져 오히려 어수선해진다.
|
|
8
|
-
* ⑵ 양수 `letter-spacing` 은 **한글 가독성을 떨어뜨린다** — 라틴 소문자 조판 관례를
|
|
9
|
-
* 그대로 옮기면 안 되는 자리다.
|
|
10
|
-
* ⇒ 위계는 **크기·굵기·색** 세 신호로만 만든다(타입 스케일의 `label` 단).
|
|
11
|
-
* 영문 전용 UI 에서 눈썹 텍스트 느낌이 필요하면 `titleStyle` 로 `--u-text-overline-*`
|
|
12
|
-
* 단을 직접 지정한다 — 그 단은 영문·숫자 라벨을 전제하고 양수 자간을 갖는다.
|
|
13
|
-
*
|
|
14
|
-
* 오버라이드 계약(docs/lob-layers.md §2): 구조는 두고 **children 치환 + prop** 으로 바꾼다.
|
|
15
|
-
* ⚠기본 스타일은 병합 가능한 형태로만 둔다 — 호출자의 `style` 이 뒤에 오므로 필요한
|
|
16
|
-
* 항목만 골라 덮을 수 있다. 인라인으로 굳혀 두면 소비자 CSS 가 이길 수 없어(`!important`
|
|
17
|
-
* 외) 계약이 원천적으로 무효가 된다.
|
|
18
|
-
*/
|
|
19
|
-
export declare function FormSection({ title, children, className, style, titleStyle, }: {
|
|
20
|
-
title: ReactNode;
|
|
21
|
-
children: ReactNode;
|
|
22
|
-
className?: string;
|
|
23
|
-
style?: CSSProperties;
|
|
24
|
-
/** 제목 줄만 따로 조정할 때. 블록을 통째로 갈아엎지 않기 위한 훅이다. */
|
|
25
|
-
titleStyle?: CSSProperties;
|
|
26
|
-
}): import("react").JSX.Element;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { PermissionStore } from './permissions';
|
|
2
|
-
export interface AuthClientMessages {
|
|
3
|
-
/** 401 로그인 실패 */
|
|
4
|
-
invalidCredentials: string;
|
|
5
|
-
/** 기타 로그인 실패(non-401) 폴백 */
|
|
6
|
-
loginFailed: string;
|
|
7
|
-
/** 네트워크/예외 */
|
|
8
|
-
networkError: string;
|
|
9
|
-
}
|
|
10
|
-
export interface LoginResult<TUser> {
|
|
11
|
-
ok: boolean;
|
|
12
|
-
user?: TUser;
|
|
13
|
-
message?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface AuthClientConfig<TUser> {
|
|
16
|
-
/** 현재 세션 조회 URL (GET) */
|
|
17
|
-
meUrl: string;
|
|
18
|
-
/** 로그인 URL (POST, 자격증명을 JSON 바디로) */
|
|
19
|
-
loginUrl: string;
|
|
20
|
-
/** 로그아웃 URL (POST) */
|
|
21
|
-
logoutUrl: string;
|
|
22
|
-
/** 상대 URL 앞에 붙일 오리진(기본 '' = same-origin). */
|
|
23
|
-
baseUrl?: string;
|
|
24
|
-
/** fetch credentials 모드(기본 'same-origin' — 쿠키 세션). */
|
|
25
|
-
credentials?: RequestCredentials;
|
|
26
|
-
/** 사용자 대면 문구(로케일). 기본 영어. */
|
|
27
|
-
messages?: Partial<AuthClientMessages>;
|
|
28
|
-
/** 로그인 실패(non-401) 응답 바디에서 서버 메시지 추출. 기본: `body.Message ?? body.message`. */
|
|
29
|
-
extractLoginError?: (body: unknown) => string | undefined;
|
|
30
|
-
/**
|
|
31
|
-
* user 에서 권한 코드 배열을 추출. 지정하면 `fetchMe`/`login` 성공 시 권한 store 를 자동 갱신,
|
|
32
|
-
* `fetchMe`→null / `logout` 시 자동 clear.
|
|
33
|
-
*/
|
|
34
|
-
getPermissions?: (user: TUser) => string[];
|
|
35
|
-
/** 권한 자동 갱신 대상 store(기본: `defaultPermissionStore`). */
|
|
36
|
-
permissionStore?: PermissionStore;
|
|
37
|
-
}
|
|
38
|
-
export interface AuthClient<TUser, TCredentials> {
|
|
39
|
-
/** 현재 세션 조회. 미인증/세션 만료 시 `null`. 성공 시 권한 store 자동 갱신(getPermissions 지정 시). */
|
|
40
|
-
fetchMe(): Promise<TUser | null>;
|
|
41
|
-
/** 로그인. 성공 시 권한 store 자동 갱신(getPermissions 지정 시). */
|
|
42
|
-
login(credentials: TCredentials): Promise<LoginResult<TUser>>;
|
|
43
|
-
/** 로그아웃. 권한 store 자동 clear(getPermissions 지정 시). */
|
|
44
|
-
logout(): Promise<void>;
|
|
45
|
-
}
|
|
46
|
-
export declare function createAuthClient<TUser, TCredentials = Record<string, unknown>>(config: AuthClientConfig<TUser>): AuthClient<TUser, TCredentials>;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 권한 스냅샷 store — 부팅 시점의 현재 사용자 권한 코드 집합을 보관하고
|
|
3
|
-
* `hasPermission`/`hasAny`/`hasAll` 판정을 제공한다. 프레임워크 무관(React·Lit 공통)이며
|
|
4
|
-
* 권한 코드는 **불투명 문자열**로만 다룬다(도메인 의미는 앱이 소유).
|
|
5
|
-
*
|
|
6
|
-
* 부팅 시점 스냅샷 모델 — 권한 변경은 다음 로그인(재-set)까지 반영되지 않는다. 단일 운영자
|
|
7
|
-
* 모델에 충분하며, 실시간 권한 회수/멀티테넌트가 필요하면 `subscribe` 로 반응형 확장 가능.
|
|
8
|
-
*/
|
|
9
|
-
export interface PermissionStore {
|
|
10
|
-
/** 현재 권한 집합을 통째로 교체한다(로그인/세션 조회 성공 시). */
|
|
11
|
-
set(codes: Iterable<string>): void;
|
|
12
|
-
/** 현재 권한 집합(읽기 전용). */
|
|
13
|
-
get(): ReadonlySet<string>;
|
|
14
|
-
/** 단일 권한 코드 보유 여부. */
|
|
15
|
-
has(code: string): boolean;
|
|
16
|
-
/** 주어진 코드 중 하나라도 보유. 빈 목록은 `true`(제약 없음). */
|
|
17
|
-
hasAny(codes: Iterable<string>): boolean;
|
|
18
|
-
/** 주어진 코드를 모두 보유. 빈 목록은 `true`. */
|
|
19
|
-
hasAll(codes: Iterable<string>): boolean;
|
|
20
|
-
/** 권한을 비운다(로그아웃/세션 만료 시). */
|
|
21
|
-
clear(): void;
|
|
22
|
-
/** 권한 변경 구독. 해제 함수를 반환한다. */
|
|
23
|
-
subscribe(listener: (codes: ReadonlySet<string>) => void): () => void;
|
|
24
|
-
}
|
|
25
|
-
/** 독립적인 권한 store 를 생성한다(테스트 격리·다중 컨텍스트에 유리). */
|
|
26
|
-
export declare function createPermissionStore(initial?: Iterable<string>): PermissionStore;
|
|
27
|
-
/**
|
|
28
|
-
* 기본 싱글톤 권한 store. 앱 전역에서 `hasPermission(code)` 를 어디서나 호출하는
|
|
29
|
-
* 흔한 패턴을 위해 free 함수로 바인딩해 노출한다. 격리가 필요하면 `createPermissionStore()` 를 쓴다.
|
|
30
|
-
*/
|
|
31
|
-
export declare const defaultPermissionStore: PermissionStore;
|
|
32
|
-
/** 기본 store 의 권한을 교체한다(부팅 시 `setPermissions(user.Permissions)`). */
|
|
33
|
-
export declare const setPermissions: (codes: Iterable<string>) => void;
|
|
34
|
-
/** 기본 store 의 현재 권한 집합. */
|
|
35
|
-
export declare const getPermissions: () => ReadonlySet<string>;
|
|
36
|
-
/** 기본 store 기준 단일 권한 보유 여부. */
|
|
37
|
-
export declare const hasPermission: (code: string) => boolean;
|
|
38
|
-
/** 기본 store 기준 하나라도 보유. */
|
|
39
|
-
export declare const hasAnyPermission: (codes: Iterable<string>) => boolean;
|
|
40
|
-
/** 기본 store 기준 모두 보유. */
|
|
41
|
-
export declare const hasAllPermissions: (codes: Iterable<string>) => boolean;
|
|
42
|
-
/** 기본 store 권한을 비운다. */
|
|
43
|
-
export declare const clearPermissions: () => void;
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { HttpResponse } from '@iyulab/http-client';
|
|
2
|
-
/**
|
|
3
|
-
* OData v4 오류 봉투의 `error.details` 항목 — 필드별 검증 실패 상세.
|
|
4
|
-
*
|
|
5
|
-
* 규격(OData JSON Format v4.0)상 각 항목은 `code`/`message` 를 **반드시** 갖고
|
|
6
|
-
* `target`(오류가 난 속성 이름)은 **선택**이다 — 필수로 선언하면 target 을 생략한
|
|
7
|
-
* 서버 응답에서 타입이 거짓말을 하게 된다.
|
|
8
|
-
*/
|
|
9
|
-
export interface ApiErrorDetail {
|
|
10
|
-
code: string;
|
|
11
|
-
message: string;
|
|
12
|
-
target?: string;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* API 호출 실패 에러 — HTTP status 를 실어 호출부가 상태별 분기(예: 404 도메인 문구)를 할 수 있게 한다.
|
|
16
|
-
* `Error` 를 상속하므로 기존 `e instanceof Error`/`e.message` 소비처는 그대로 동작한다.
|
|
17
|
-
*/
|
|
18
|
-
export declare class ApiError extends Error {
|
|
19
|
-
readonly status: number;
|
|
20
|
-
/** OData v4 오류 봉투의 `error.details`(필드별 검증 상세) — 서버 응답에 없거나 파싱 실패면 undefined. */
|
|
21
|
-
readonly details?: ApiErrorDetail[];
|
|
22
|
-
constructor(message: string, status: number, details?: ApiErrorDetail[]);
|
|
23
|
-
}
|
|
24
|
-
/** 서비스가 토스트/에러에 쓰는 사용자 대면 문구. 기본값은 영어 — 앱이 로케일별로 오버라이드한다. */
|
|
25
|
-
export interface ODataServiceMessages {
|
|
26
|
-
/** POST 성공 토스트 */
|
|
27
|
-
saved: string;
|
|
28
|
-
/** PATCH 성공 토스트 */
|
|
29
|
-
updated: string;
|
|
30
|
-
/** DELETE 성공 토스트 */
|
|
31
|
-
deleted: string;
|
|
32
|
-
/** 401 세션 만료 시 throw 되는 에러 메시지 */
|
|
33
|
-
sessionExpired: string;
|
|
34
|
-
/** status별 매핑이 없을 때의 폴백 */
|
|
35
|
-
requestFailed: string;
|
|
36
|
-
/** 서버 raw 메시지가 없거나 너무 길 때 status → 친화 메시지 */
|
|
37
|
-
http: Record<number, string>;
|
|
38
|
-
}
|
|
39
|
-
export interface ODataServiceConfig {
|
|
40
|
-
/** 모든 요청의 베이스 URL (예: `window.location.origin`). 슬래시 없이 오리진만. */
|
|
41
|
-
baseUrl: string;
|
|
42
|
-
/** OData 엔드포인트 prefix (기본 `$data`) */
|
|
43
|
-
odataPrefix?: string;
|
|
44
|
-
/** custom REST 엔드포인트 prefix (기본 `api`) */
|
|
45
|
-
apiPrefix?: string;
|
|
46
|
-
/**
|
|
47
|
-
* 401 응답 시 호출된다(모든 메서드 공통). 세션 만료 리다이렉트는 앱이 결정한다.
|
|
48
|
-
* 재진입 가드(한 번만 리다이렉트)도 앱 콜백 쪽에서 처리한다 — 라이브러리는 status만 통지.
|
|
49
|
-
*/
|
|
50
|
-
onUnauthorized?: (status: number) => void;
|
|
51
|
-
/** 성공/실패 토스트 훅. 생략하면 토스트를 내지 않는다(순수 · 테스트 용이). */
|
|
52
|
-
notify?: {
|
|
53
|
-
success?: (message: string) => void;
|
|
54
|
-
error?: (message: string) => void;
|
|
55
|
-
};
|
|
56
|
-
/** 사용자 대면 문구 오버라이드(로케일). 지정한 키만 기본값을 대체한다. */
|
|
57
|
-
messages?: Partial<ODataServiceMessages>;
|
|
58
|
-
/**
|
|
59
|
-
* 에러 메시지 포매팅 오버라이드. 반환값이 있으면 그것을 에러 메시지로 사용한다.
|
|
60
|
-
* (앱별 정책 — 예: 영문 raw OData 메시지를 로케일 친화 문구로 치환 — 을 여기에 둔다.)
|
|
61
|
-
*/
|
|
62
|
-
formatError?: (info: {
|
|
63
|
-
status: number;
|
|
64
|
-
statusText: string;
|
|
65
|
-
rawMessage?: string;
|
|
66
|
-
/** OData v4 `error.details` — 검증을 마친 항목만 실린다(`ApiError.details` 와 같은 값). */
|
|
67
|
-
details?: ApiErrorDetail[];
|
|
68
|
-
body?: unknown;
|
|
69
|
-
}) => string | undefined;
|
|
70
|
-
}
|
|
71
|
-
export interface ODataService {
|
|
72
|
-
/** 설정된 prefix + baseUrl 로 OData 엔티티 URL 을 만든다(flex-table useODataSource 등에서 사용). */
|
|
73
|
-
odataUrl(entity: string): string;
|
|
74
|
-
/** 설정된 prefix + baseUrl 로 REST 엔드포인트 URL 을 만든다. */
|
|
75
|
-
apiUrl(path: string): string;
|
|
76
|
-
/** OData GET(목록). `value` 배열을 벗겨 반환한다. */
|
|
77
|
-
odataGet<T>(entity: string, params?: Record<string, string>): Promise<T[]>;
|
|
78
|
-
/** OData GET(단건, key). */
|
|
79
|
-
odataGetById<T>(entity: string, id: string): Promise<T>;
|
|
80
|
-
/** `$count=true&$top=0` — 데이터 없이 총 건수만. */
|
|
81
|
-
odataCount(entity: string, filter?: Record<string, unknown>): Promise<number>;
|
|
82
|
-
/** OData POST(생성) — 토스트 없이 결과만(일괄 처리에서 토스트 폭주 방지). */
|
|
83
|
-
odataPostQuiet<T>(entity: string, body: Partial<T>): Promise<T>;
|
|
84
|
-
/** OData POST(생성) — 성공 시 `saved` 토스트. */
|
|
85
|
-
odataPost<T>(entity: string, body: Partial<T>): Promise<T>;
|
|
86
|
-
/** OData PATCH(수정) — 토스트 없이 결과만(자식 컬렉션 편집 후 부모의 파생 필드를
|
|
87
|
-
* 함께 동기화하는 등, 한 사용자 액션이 여러 mutation을 낼 때 토스트 폭주 방지). */
|
|
88
|
-
odataPatchQuiet<T>(entity: string, id: string, body: Partial<T>): Promise<void>;
|
|
89
|
-
/** OData PATCH(수정) — 성공 시 `updated` 토스트. */
|
|
90
|
-
odataPatch<T>(entity: string, id: string, body: Partial<T>): Promise<void>;
|
|
91
|
-
/** OData DELETE — 토스트 없이(`odataPatchQuiet`와 같은 이유). */
|
|
92
|
-
odataDeleteQuiet(entity: string, id: string): Promise<void>;
|
|
93
|
-
/** OData DELETE — 성공 시 `deleted` 토스트. */
|
|
94
|
-
odataDelete(entity: string, id: string): Promise<void>;
|
|
95
|
-
/** custom REST GET — 204 등 빈 바디를 안전 파싱. */
|
|
96
|
-
apiGet<T>(path: string): Promise<T>;
|
|
97
|
-
/** custom REST POST. `body`가 `FormData` 인스턴스면 그대로(직렬화 없이) 멀티파트로
|
|
98
|
-
* 전송된다 — `@iyulab/http-client`가 Content-Type을 브라우저 자동 설정에 맡기고
|
|
99
|
-
* JSON 직렬화 분기를 타지 않는다. `apiPut`/`apiPatch`도 동일하게 동작한다. */
|
|
100
|
-
apiPost<T>(path: string, body?: unknown): Promise<T>;
|
|
101
|
-
/** custom REST PUT(리소스 전체 교체/생성). `body`의 `FormData` 처리는 `apiPost` 참조. */
|
|
102
|
-
apiPut<T>(path: string, body?: unknown): Promise<T>;
|
|
103
|
-
/** custom REST PATCH. `body`의 `FormData` 처리는 `apiPost` 참조. */
|
|
104
|
-
apiPatch<T>(path: string, body?: unknown): Promise<T>;
|
|
105
|
-
/** custom REST DELETE — 대부분 204 No Content. */
|
|
106
|
-
apiDelete<T = void>(path: string): Promise<T>;
|
|
107
|
-
/** URL 을 직접 조립한 커스텀 조회(csv-export 등)를 위해 raw 응답을 반환. */
|
|
108
|
-
fetchRaw(url: string): Promise<HttpResponse>;
|
|
109
|
-
/**
|
|
110
|
-
* flex-table `useODataSource` 에 주입할 공용 transport 옵션.
|
|
111
|
-
* useODataSource 는 자체 fetcher 를 쓰므로 별도로 `onUnauthorized` 를 배선해야 401 처리가 걸린다.
|
|
112
|
-
*/
|
|
113
|
-
readonly sourceDefaults: {
|
|
114
|
-
baseUrl: string;
|
|
115
|
-
onUnauthorized: () => void;
|
|
116
|
-
};
|
|
117
|
-
/** `instanceof` 판정을 위해 재노출. (모듈 export `ApiError` 와 동일 클래스) */
|
|
118
|
-
readonly ApiError: typeof ApiError;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* OData v4 + custom REST 서비스를 생성한다.
|
|
122
|
-
* 반환된 서비스는 상태를 공유하지 않는 순수 클로저이므로 여러 개를 만들어도 안전하다(테스트 격리에도 유리).
|
|
123
|
-
*/
|
|
124
|
-
export declare function createODataService(config: ODataServiceConfig): ODataService;
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Currency formatting utility class.
|
|
3
|
-
*
|
|
4
|
-
* @deprecated The formatting logic now lives in `@iyulab/components`' `formatCurrency`
|
|
5
|
-
* (framework-neutral, lower in the dependency stack so `@iyulab/modern-app` can consume it
|
|
6
|
-
* too). This class is kept as a thin, behavior-preserving wrapper for existing callers —
|
|
7
|
-
* new code should import `formatCurrency`/`formatNumber`/`formatDate` from
|
|
8
|
-
* `@iyulab/components` directly.
|
|
9
|
-
*/
|
|
10
|
-
export declare class CurrencyHelper {
|
|
11
|
-
/**
|
|
12
|
-
* Format amount as currency string.
|
|
13
|
-
* @param amount - Amount to format
|
|
14
|
-
* @param currency - Currency code (default: 'KRW')
|
|
15
|
-
* @param locale - Locale for formatting (default: 'ko-KR')
|
|
16
|
-
*/
|
|
17
|
-
static formatCurrency(amount: number | null | undefined, currency?: string, locale?: string): string;
|
|
18
|
-
/** Format amount as Korean Won (KRW) */
|
|
19
|
-
static formatKRW(amount: number | null | undefined): string;
|
|
20
|
-
/** Format amount as US Dollar (USD) */
|
|
21
|
-
static formatUSD(amount: number | null | undefined): string;
|
|
22
|
-
/** Format amount as Euro (EUR) */
|
|
23
|
-
static formatEUR(amount: number | null | undefined): string;
|
|
24
|
-
/** Format amount as Japanese Yen (JPY) */
|
|
25
|
-
static formatJPY(amount: number | null | undefined): string;
|
|
26
|
-
/** Format amount as Chinese Yuan (CNY) */
|
|
27
|
-
static formatCNY(amount: number | null | undefined): string;
|
|
28
|
-
/**
|
|
29
|
-
* Parse currency string to number.
|
|
30
|
-
* @param value - Currency string to parse
|
|
31
|
-
*/
|
|
32
|
-
static parseCurrency(value: string): number;
|
|
33
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Date manipulation and formatting utility class
|
|
3
|
-
*/
|
|
4
|
-
export declare class DateHelper {
|
|
5
|
-
/**
|
|
6
|
-
* Format date to YYYY-MM-DD string
|
|
7
|
-
* @param date - Date to format (Date object or ISO string)
|
|
8
|
-
*/
|
|
9
|
-
static formatDate(date: Date | string | null | undefined): string;
|
|
10
|
-
/**
|
|
11
|
-
* Format date to localized string
|
|
12
|
-
* @param date - Date to format
|
|
13
|
-
* @param locale - Locale for formatting (default: 'ko-KR')
|
|
14
|
-
* @param options - Intl.DateTimeFormat options
|
|
15
|
-
*/
|
|
16
|
-
static formatLocalDate(date: Date | string | null | undefined, locale?: string, options?: Intl.DateTimeFormatOptions): string;
|
|
17
|
-
/**
|
|
18
|
-
* Format datetime to YYYY-MM-DD HH:mm string
|
|
19
|
-
* @param date - Date to format
|
|
20
|
-
*/
|
|
21
|
-
static formatDateTime(date: Date | string | null | undefined): string;
|
|
22
|
-
/**
|
|
23
|
-
* Calculate days difference between two dates
|
|
24
|
-
* @param startDate - Start date
|
|
25
|
-
* @param endDate - End date
|
|
26
|
-
* @returns Number of days (positive if endDate > startDate)
|
|
27
|
-
*/
|
|
28
|
-
static getDaysDifference(startDate: Date | string, endDate: Date | string): number;
|
|
29
|
-
/**
|
|
30
|
-
* Calculate days from now to target date
|
|
31
|
-
* @param targetDate - Target date
|
|
32
|
-
* @returns Days remaining (negative if overdue)
|
|
33
|
-
*/
|
|
34
|
-
static getDaysFromNow(targetDate: Date | string): number;
|
|
35
|
-
/**
|
|
36
|
-
* Check if date is today
|
|
37
|
-
*/
|
|
38
|
-
static isToday(date: Date | string): boolean;
|
|
39
|
-
/**
|
|
40
|
-
* Check if date is in the past
|
|
41
|
-
*/
|
|
42
|
-
static isPast(date: Date | string): boolean;
|
|
43
|
-
/**
|
|
44
|
-
* Check if date is in the future
|
|
45
|
-
*/
|
|
46
|
-
static isFuture(date: Date | string): boolean;
|
|
47
|
-
/**
|
|
48
|
-
* Add days to date
|
|
49
|
-
* @param date - Base date
|
|
50
|
-
* @param days - Number of days to add (can be negative)
|
|
51
|
-
*/
|
|
52
|
-
static addDays(date: Date | string, days: number): Date;
|
|
53
|
-
/**
|
|
54
|
-
* Get start of day (00:00:00)
|
|
55
|
-
*/
|
|
56
|
-
static startOfDay(date: Date | string): Date;
|
|
57
|
-
/**
|
|
58
|
-
* Get end of day (23:59:59.999)
|
|
59
|
-
*/
|
|
60
|
-
static endOfDay(date: Date | string): Date;
|
|
61
|
-
}
|