@iyulab/enterprise 0.14.0 → 0.16.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 +64 -0
- package/README.md +40 -0
- package/dist/index.d.ts +660 -11
- package/dist/index.js +45 -42
- package/dist/styles/preset.css +21 -4
- 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 -136
- 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,37 +741,42 @@ 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
782
|
/**
|
|
@@ -797,79 +802,77 @@ function createODataService(config) {
|
|
|
797
802
|
throw e;
|
|
798
803
|
}
|
|
799
804
|
}
|
|
800
|
-
async function odataPostQuiet(entity, body) {
|
|
805
|
+
async function odataPostQuiet(entity, body, opts) {
|
|
801
806
|
const res = await client.post(odataUrl(entity), normalizeBody(body));
|
|
802
|
-
await throwIfError(res);
|
|
807
|
+
await throwIfError(res, opts);
|
|
803
808
|
return res.json();
|
|
804
809
|
}
|
|
805
|
-
async function odataPost(entity, body) {
|
|
810
|
+
async function odataPost(entity, body, opts) {
|
|
806
811
|
return notifyingWrite(async () => {
|
|
807
|
-
const result = await odataPostQuiet(entity, body);
|
|
812
|
+
const result = await odataPostQuiet(entity, body, opts);
|
|
808
813
|
notifySuccess?.(messages.saved);
|
|
809
814
|
return result;
|
|
810
815
|
});
|
|
811
816
|
}
|
|
812
|
-
async function odataPatchQuiet(entity, id, body) {
|
|
813
|
-
await throwIfError(await client.patch(`${odataUrl(entity)}(${id})`, normalizeBody(body)));
|
|
817
|
+
async function odataPatchQuiet(entity, id, body, opts) {
|
|
818
|
+
await throwIfError(await client.patch(`${odataUrl(entity)}(${id})`, normalizeBody(body)), opts);
|
|
814
819
|
}
|
|
815
|
-
async function odataPatch(entity, id, body) {
|
|
820
|
+
async function odataPatch(entity, id, body, opts) {
|
|
816
821
|
return notifyingWrite(async () => {
|
|
817
|
-
await odataPatchQuiet(entity, id, body);
|
|
822
|
+
await odataPatchQuiet(entity, id, body, opts);
|
|
818
823
|
notifySuccess?.(messages.updated);
|
|
819
824
|
});
|
|
820
825
|
}
|
|
821
|
-
async function odataDeleteQuiet(entity, id) {
|
|
822
|
-
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);
|
|
823
828
|
}
|
|
824
|
-
async function odataDelete(entity, id) {
|
|
829
|
+
async function odataDelete(entity, id, opts) {
|
|
825
830
|
return notifyingWrite(async () => {
|
|
826
|
-
await odataDeleteQuiet(entity, id);
|
|
831
|
+
await odataDeleteQuiet(entity, id, opts);
|
|
827
832
|
notifySuccess?.(messages.deleted);
|
|
828
833
|
});
|
|
829
834
|
}
|
|
830
|
-
async function apiGet(path) {
|
|
835
|
+
async function apiGet(path, opts) {
|
|
831
836
|
const [p, ...q] = path.split("?");
|
|
832
837
|
const url = q.length ? `${apiUrl(p)}?${q.join("?")}` : apiUrl(p);
|
|
833
838
|
const res = await client.get(url);
|
|
834
|
-
await throwIfError(res);
|
|
839
|
+
await throwIfError(res, opts);
|
|
835
840
|
return parseJsonBody(res);
|
|
836
841
|
}
|
|
837
|
-
async function apiPostQuiet(path, body) {
|
|
842
|
+
async function apiPostQuiet(path, body, opts) {
|
|
838
843
|
const res = await client.post(apiUrl(path), body ?? {});
|
|
839
|
-
await throwIfError(res);
|
|
844
|
+
await throwIfError(res, opts);
|
|
840
845
|
return parseJsonBody(res);
|
|
841
846
|
}
|
|
842
|
-
async function apiPost(path, body) {
|
|
843
|
-
return notifyingWrite(() => apiPostQuiet(path, body));
|
|
847
|
+
async function apiPost(path, body, opts) {
|
|
848
|
+
return notifyingWrite(() => apiPostQuiet(path, body, opts));
|
|
844
849
|
}
|
|
845
|
-
async function apiPutQuiet(path, body) {
|
|
850
|
+
async function apiPutQuiet(path, body, opts) {
|
|
846
851
|
const res = await client.put(apiUrl(path), body ?? {});
|
|
847
|
-
await throwIfError(res);
|
|
852
|
+
await throwIfError(res, opts);
|
|
848
853
|
return parseJsonBody(res);
|
|
849
854
|
}
|
|
850
|
-
async function apiPut(path, body) {
|
|
851
|
-
return notifyingWrite(() => apiPutQuiet(path, body));
|
|
855
|
+
async function apiPut(path, body, opts) {
|
|
856
|
+
return notifyingWrite(() => apiPutQuiet(path, body, opts));
|
|
852
857
|
}
|
|
853
|
-
async function apiPatchQuiet(path, body) {
|
|
858
|
+
async function apiPatchQuiet(path, body, opts) {
|
|
854
859
|
const res = await client.patch(apiUrl(path), body ?? {});
|
|
855
|
-
await throwIfError(res);
|
|
860
|
+
await throwIfError(res, opts);
|
|
856
861
|
return parseJsonBody(res);
|
|
857
862
|
}
|
|
858
|
-
async function apiPatch(path, body) {
|
|
859
|
-
return notifyingWrite(() => apiPatchQuiet(path, body));
|
|
863
|
+
async function apiPatch(path, body, opts) {
|
|
864
|
+
return notifyingWrite(() => apiPatchQuiet(path, body, opts));
|
|
860
865
|
}
|
|
861
|
-
async function apiDeleteQuiet(path) {
|
|
866
|
+
async function apiDeleteQuiet(path, opts) {
|
|
862
867
|
const res = await client.delete(apiUrl(path));
|
|
863
|
-
await throwIfError(res);
|
|
868
|
+
await throwIfError(res, opts);
|
|
864
869
|
return parseJsonBody(res);
|
|
865
870
|
}
|
|
866
|
-
async function apiDelete(path) {
|
|
867
|
-
return notifyingWrite(() => apiDeleteQuiet(path));
|
|
871
|
+
async function apiDelete(path, opts) {
|
|
872
|
+
return notifyingWrite(() => apiDeleteQuiet(path, opts));
|
|
868
873
|
}
|
|
869
874
|
async function fetchRaw(url) {
|
|
870
|
-
|
|
871
|
-
await throwIfError(res);
|
|
872
|
-
return res;
|
|
875
|
+
return client.get(url);
|
|
873
876
|
}
|
|
874
877
|
return {
|
|
875
878
|
odataUrl,
|
package/dist/styles/preset.css
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* ⚠**`@iyulab/components` 1.21.0 이상이 필요하다.** 이 파일은 값만 정하고, 그 값을 **읽는
|
|
8
8
|
* 쪽은 컴포넌트**다. 1.20.0 이하에는 타입 스케일·반경 상단 축이 아예 없어서 여기 적은
|
|
9
9
|
* `--u-text-*` 는 **아무도 읽지 않는다** — 에러 없이 절반만 적용된 것처럼 보인다.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* ⚠**이 peer 는 optional 이 아니다**(종전 판이 그렇게 적고 있었으나 사실이 아니었다 —
|
|
11
|
+
* 이 패키지의 **런타임 코드가** `@iyulab/components/dist/utilities/` 의 `Locale`·`format`·`icons`
|
|
12
|
+
* 를 import 한다). 프리셋을 쓰지 않는 소비자에게도 필요하다.
|
|
13
|
+
* ⇒ 버전 불일치는 npm 이 설치 단계에서 말해 준다. 프리셋을 로드했는데 글자 크기가 안 바뀌면
|
|
14
|
+
* 먼저 이 버전을, 그다음 아래 ⑶의 **로드 순서**를 본다.
|
|
13
15
|
*
|
|
14
16
|
* `@iyulab/components` 가 여는 **토큰 축**에 이유랩의 값을 채운 한 장이다.
|
|
15
17
|
* 축(이름·단 수)은 `components` 소유이고, 이 파일은 **값만** 정한다.
|
|
@@ -24,10 +26,25 @@
|
|
|
24
26
|
* 소비자가 그 선택자와 특이도 싸움을 하게 되고, 그때부터 이 파일은 프리셋이 아니라
|
|
25
27
|
* 또 하나의 프레임워크가 된다. (`tests/preset-contract.test.ts` 가 지킨다.)
|
|
26
28
|
* ⑶ 🔴**층은 «로드 순서»로 선다.** 세 층이 전부 `:root`(0,1,0)이므로 나중에 로드된
|
|
27
|
-
* 것이
|
|
29
|
+
* 것이 이긴다.
|
|
28
30
|
*
|
|
29
31
|
* 기본값(components/styles/light.css) → 하우스(이 파일) → 소비자 브랜드
|
|
30
32
|
*
|
|
33
|
+
* ⚠**그런데 「기본값」 층은 «런타임에» 붙는다 — `@iyulab/components` 1.43.x 이하에서는
|
|
34
|
+
* 위 그림이 성립하지 않았다.** `Theme.init()` 이 내장 시트를 `document.head` **끝**에
|
|
35
|
+
* `appendChild` 했고, 정적 `import` 로 올라온 이 파일은 번들러가 **파싱 시점에** 넣으므로
|
|
36
|
+
* ***언제나 기본값이 이겼다.*** 즉 머리말이 안내한 `import` 한 줄이 **아무 효과가 없었다.**
|
|
37
|
+
* ★**1.44.0 부터 `Theme.init()` 이 내장 시트를 «첫 스타일 앞»에 넣는다** — 기본값 층은
|
|
38
|
+
* 이름 그대로 가장 낮은 층이므로 위치도 바닥이다. ⇒ **그 판부터 `import` 한 줄이 문서대로
|
|
39
|
+
* 동작한다**(`peerDependencies` 가 그 하한을 선언한다).
|
|
40
|
+
* ⚠**1.43.x 이하를 쓰는 소비자가 지금 할 일**: 이 파일을 `?inline` 등으로 문자열로 받아
|
|
41
|
+
* `await Theme.init(...)` **뒤에** 직접 `<style>` 로 붙인다. 그 우회는 components 를
|
|
42
|
+
* 1.44.0 이상으로 올리는 즉시 `import` 한 줄로 줄일 수 있다.
|
|
43
|
+
* 🔴**이 실패는 조용하다 — 그것이 이 문단이 길어진 이유다.** 오류도 경고도 없고, 두 시트의
|
|
44
|
+
* 값이 일부만 다르면(실측: 상위 세 단과 반경만 다르고 body·label·caption·overline 은 동일)
|
|
45
|
+
* **한 군데도 적용되지 않았는데 적용된 것처럼 보인다.** 프리셋을 로드했는데 글자 크기가
|
|
46
|
+
* 안 바뀌면 **버전 다음으로 이 순서를 본다.**
|
|
47
|
+
*
|
|
31
48
|
* ⚠**종전 판(0.7.0)은 이 자리를 `:where()` 로 감싸 특이도 0 으로 만들었고,
|
|
32
49
|
* 그래서 «한 줄도 적용되지 않았다».** 의도는 옳았다 — *"프리셋이 소비자 브랜드를
|
|
33
50
|
* 덮으면 안 된다"*. 그러나 특이도 0 은 **이겨야 할 상대(기본값)와 지지 말아야 할
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/enterprise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Enterprise utilities and components for iyulab framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"enterprise",
|
|
@@ -56,6 +56,7 @@
|
|
|
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",
|
|
@@ -65,6 +66,6 @@
|
|
|
65
66
|
},
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"react": ">=18",
|
|
68
|
-
"@iyulab/components": ">=1.
|
|
69
|
+
"@iyulab/components": ">=1.44.0"
|
|
69
70
|
}
|
|
70
71
|
}
|
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,136 +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 — 실패 시 `error` 토스트 후 rethrow(401 제외). `body`가 `FormData`
|
|
98
|
-
* 인스턴스면 그대로(직렬화 없이) 멀티파트로 전송된다 — `@iyulab/http-client`가
|
|
99
|
-
* Content-Type을 브라우저 자동 설정에 맡기고 JSON 직렬화 분기를 타지 않는다.
|
|
100
|
-
* `apiPut`/`apiPatch`도 동일하게 동작한다. */
|
|
101
|
-
apiPost<T>(path: string, body?: unknown): Promise<T>;
|
|
102
|
-
/** custom REST POST — 토스트 없이 결과만(`odataPostQuiet`와 같은 이유: 한 사용자 액션이
|
|
103
|
-
* 여러 요청을 내거나, 소비자가 자기 래퍼로 이미 통지할 때). */
|
|
104
|
-
apiPostQuiet<T>(path: string, body?: unknown): Promise<T>;
|
|
105
|
-
/** custom REST PUT(리소스 전체 교체/생성) — 실패 시 `error` 토스트 후 rethrow(401 제외).
|
|
106
|
-
* `body`의 `FormData` 처리는 `apiPost` 참조. */
|
|
107
|
-
apiPut<T>(path: string, body?: unknown): Promise<T>;
|
|
108
|
-
/** custom REST PUT — 토스트 없이 결과만. */
|
|
109
|
-
apiPutQuiet<T>(path: string, body?: unknown): Promise<T>;
|
|
110
|
-
/** custom REST PATCH — 실패 시 `error` 토스트 후 rethrow(401 제외). `body`의 `FormData`
|
|
111
|
-
* 처리는 `apiPost` 참조. */
|
|
112
|
-
apiPatch<T>(path: string, body?: unknown): Promise<T>;
|
|
113
|
-
/** custom REST PATCH — 토스트 없이 결과만. */
|
|
114
|
-
apiPatchQuiet<T>(path: string, body?: unknown): Promise<T>;
|
|
115
|
-
/** custom REST DELETE — 실패 시 `error` 토스트 후 rethrow(401 제외). 대부분 204 No Content. */
|
|
116
|
-
apiDelete<T = void>(path: string): Promise<T>;
|
|
117
|
-
/** custom REST DELETE — 토스트 없이 결과만. */
|
|
118
|
-
apiDeleteQuiet<T = void>(path: string): Promise<T>;
|
|
119
|
-
/** URL 을 직접 조립한 커스텀 조회(csv-export 등)를 위해 raw 응답을 반환. */
|
|
120
|
-
fetchRaw(url: string): Promise<HttpResponse>;
|
|
121
|
-
/**
|
|
122
|
-
* flex-table `useODataSource` 에 주입할 공용 transport 옵션.
|
|
123
|
-
* useODataSource 는 자체 fetcher 를 쓰므로 별도로 `onUnauthorized` 를 배선해야 401 처리가 걸린다.
|
|
124
|
-
*/
|
|
125
|
-
readonly sourceDefaults: {
|
|
126
|
-
baseUrl: string;
|
|
127
|
-
onUnauthorized: () => void;
|
|
128
|
-
};
|
|
129
|
-
/** `instanceof` 판정을 위해 재노출. (모듈 export `ApiError` 와 동일 클래스) */
|
|
130
|
-
readonly ApiError: typeof ApiError;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* OData v4 + custom REST 서비스를 생성한다.
|
|
134
|
-
* 반환된 서비스는 상태를 공유하지 않는 순수 클로저이므로 여러 개를 만들어도 안전하다(테스트 격리에도 유리).
|
|
135
|
-
*/
|
|
136
|
-
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
|
-
}
|