@iyulab/enterprise 0.15.0 → 0.17.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 +46 -0
- package/README.md +21 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +28 -2
- package/dist/styles/preset.css +21 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.0] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`ApiError.notified` and `wasNotified(err)` — whether the service already told the user about a
|
|
8
|
+
failure.** A failed write (`odataPost/Patch/Delete`, `apiPost/Put/Patch/Delete`) is reported
|
|
9
|
+
through `notify.error` and then rethrown, so the caller's flow stops. When that error reaches a
|
|
10
|
+
global `unhandledrejection` handler or an error boundary, the boundary could not tell it from a
|
|
11
|
+
failed read, which the service does not report: toasting again showed the message twice, not
|
|
12
|
+
toasting left read failures silent. The error now carries the answer. `notified` is `true` only
|
|
13
|
+
when the service actually called `notify.error` — reads, `*Quiet` writes, 401s and services
|
|
14
|
+
without `notify.error` give `false` — and it is read-only: only the service can set it.
|
|
15
|
+
`wasNotified(err)` answers the same for failures that are not `ApiError`, such as a network
|
|
16
|
+
error on a write.
|
|
17
|
+
|
|
18
|
+
## [0.16.0] - 2026-09-22
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- 🔴 **`styles/preset.css` now says why its documented usage used to do nothing.** The header's
|
|
23
|
+
third clause said layer order is decided by load order and is therefore already under the
|
|
24
|
+
consumer's control. It was not: the defaults layer is injected at runtime by
|
|
25
|
+
`@iyulab/components`' `Theme.init()`, so until that package's **1.44.0** it landed *after* a
|
|
26
|
+
statically imported sheet and won. The one-line `import` this file documents had **no effect at
|
|
27
|
+
all**, silently — and because the two sheets agree on `body`/`label`/`caption`/`overline` and
|
|
28
|
+
differ only on the upper type steps and the radii, it still looked applied. The clause now
|
|
29
|
+
states that, names the version where the documented usage starts working, and says what to do
|
|
30
|
+
on an older one.
|
|
31
|
+
- **`docs/lob-layers.md` carried the same third clause, and the same false statement with it.**
|
|
32
|
+
A rule that lives in two published documents goes stale in two places; that one now names the
|
|
33
|
+
premise this package cannot hold on its own -- load order is decided by whoever injects the
|
|
34
|
+
defaults layer -- and says which version makes the documented arrangement true.
|
|
35
|
+
- **The header also called the `@iyulab/components` peer optional. It is not** — this package's
|
|
36
|
+
runtime code imports `Locale`, `format` and `icons` from it, so it is required whether or not
|
|
37
|
+
you load the preset. The prose was the wrong half of that mismatch and has been corrected.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- 🔴 **The `@iyulab/components` peer range is now `>=1.44.0`** (was `>=1.27.0`). Every version
|
|
42
|
+
below that places the defaults layer last, which is exactly the arrangement in which this
|
|
43
|
+
package's documented preset usage is silently void. A range that admits those versions lets a
|
|
44
|
+
consumer install a combination where the preset does nothing and nothing says so — declaring
|
|
45
|
+
the floor is what turns that from a silent failure into an install-time answer.
|
|
46
|
+
|
|
47
|
+
⚠ **This is the only reason for the minor.** No API changed.
|
|
48
|
+
|
|
3
49
|
## [0.15.0] - 2026-09-22
|
|
4
50
|
|
|
5
51
|
### Fixed
|
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ npm install @iyulab/enterprise
|
|
|
19
19
|
| `ApiConfig` | class | baseUrl/OData·API prefix·dev 판별 중앙 설정 |
|
|
20
20
|
| `createODataService` | factory | OData v4 + custom REST CRUD 서비스(401·토스트·에러파싱) |
|
|
21
21
|
| `ApiError` | class | HTTP status + OData `error.details`(필드별 검증 상세)를 실은 API 호출 실패 에러 |
|
|
22
|
+
| `wasNotified` | function | 이 실패를 서비스가 `notify.error` 로 이미 알렸는가 — 경계의 이중 토스트 방지 |
|
|
22
23
|
| `createAuthClient` | factory | 쿠키 세션 인증(fetchMe/login/logout) — 제네릭 user/자격증명 |
|
|
23
24
|
| `createPermissionStore` · `hasPermission` 외 | store | 권한 스냅샷 store + 판정 free 함수 |
|
|
24
25
|
| `CurrencyHelper` | class | 통화 포맷(`formatKRW` 등) |
|
|
@@ -208,6 +209,26 @@ try {
|
|
|
208
209
|
| `message` | 사용자 대면 메시지 (`formatError` → 서버 raw → status 폴백 순으로 결정) |
|
|
209
210
|
| `status` | HTTP status |
|
|
210
211
|
| `details` | `error.details` 항목 배열 — 규격상 `code`/`message` 는 필수, `target`(속성 이름)은 선택. 상세가 없거나 규격 형태가 아니면 `undefined` |
|
|
212
|
+
| `notified` | 서비스가 이 실패를 `notify.error` 로 **이미 사용자에게 알렸는가**(읽기 전용). 쓰기 실패를 알렸으면 `true`, 읽기·`*Quiet`·401 이나 `notify.error` 미설정이면 `false` |
|
|
213
|
+
|
|
214
|
+
#### 경계에서 «알리지 않은 것만» 알리기 — `notified` · `wasNotified`
|
|
215
|
+
|
|
216
|
+
쓰기 실패는 서비스가 알린 **뒤** 같은 에러를 다시 던진다(호출부 흐름을 멈추게 하려고). 그 에러가 전역
|
|
217
|
+
`unhandledrejection` 핸들러나 error boundary 까지 올라오면, 거기서 다시 토스트를 띄우면 이중 통지가 되고
|
|
218
|
+
안 띄우면 읽기 실패가 조용해진다. 어느 쪽인지는 에러가 들고 온다:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { ApiError, wasNotified } from '@iyulab/enterprise'
|
|
222
|
+
|
|
223
|
+
window.addEventListener('unhandledrejection', (ev) => {
|
|
224
|
+
const err = ev.reason
|
|
225
|
+
if (err instanceof ApiError || wasNotified(err)) ev.preventDefault()
|
|
226
|
+
if (err instanceof ApiError && !err.notified) app.error(err.message) // 읽기 실패 등
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`wasNotified(err)` 는 `ApiError` 가 아닌 실패(네트워크 오류 등)에도 같은 답을 준다 — 쓰기 경로에서 알렸다면
|
|
231
|
+
`true`. 표식은 서비스만 세울 수 있다(에러 객체의 필드가 아니다).
|
|
211
232
|
|
|
212
233
|
|
|
213
234
|
### 인증 + 권한 (`createAuthClient` · 권한 store)
|
package/dist/index.d.ts
CHANGED
|
@@ -86,6 +86,11 @@ export declare class ApiError extends Error {
|
|
|
86
86
|
/** OData v4 오류 봉투의 `error.details`(필드별 검증 상세) — 서버 응답에 없거나 파싱 실패면 undefined. */
|
|
87
87
|
readonly details?: ApiErrorDetail[];
|
|
88
88
|
constructor(message: string, status: number, details?: ApiErrorDetail[]);
|
|
89
|
+
/**
|
|
90
|
+
* 서비스가 이 실패를 `notify.error` 로 **이미 사용자에게 알렸는가** — 경계가 «알리지 않은 것만» 알리는 근거.
|
|
91
|
+
* `notify.error` 를 설정하지 않은 서비스에서는 알린 것이 없으므로 항상 `false` 다. 읽기 전용이다.
|
|
92
|
+
*/
|
|
93
|
+
get notified(): boolean;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
|
@@ -657,4 +662,14 @@ export declare class UrgencyHelper {
|
|
|
657
662
|
*/
|
|
658
663
|
export declare type UrgencyLevel = 'overdue' | 'critical' | 'urgent' | 'soon' | 'normal';
|
|
659
664
|
|
|
665
|
+
/**
|
|
666
|
+
* 이 실패를 서비스가 `notify.error` 로 이미 사용자에게 알렸는가.
|
|
667
|
+
*
|
|
668
|
+
* 쓰기(`odataPost/Patch/Delete`·`apiPost/Put/Patch/Delete`) 실패는 알린 뒤 다시 던지고, 읽기·`*Quiet`·401 은
|
|
669
|
+
* 알리지 않는다 — 그 정책의 결과를 경계(전역 `unhandledrejection` 핸들러·error boundary)가 서비스 인스턴스
|
|
670
|
+
* 없이 읽는 자리다: `if (!wasNotified(err)) toast(message)`. `ApiError` 는 같은 사실을 `err.notified` 로도 준다.
|
|
671
|
+
* 네트워크 실패처럼 `ApiError` 가 아닌 것도 쓰기 경로에서 알렸다면 `true` 다.
|
|
672
|
+
*/
|
|
673
|
+
export declare function wasNotified(error: unknown): boolean;
|
|
674
|
+
|
|
660
675
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -645,6 +645,22 @@ function extractErrorDetails(raw) {
|
|
|
645
645
|
return details.length > 0 ? details : void 0;
|
|
646
646
|
}
|
|
647
647
|
/**
|
|
648
|
+
* 서비스가 `notify.error` 로 **사용자에게 이미 알린** 실패들 — 이 모듈만 채운다.
|
|
649
|
+
* ⚠에러 객체의 필드로 두지 않는다: 소비자가 세우거나 지울 수 있으면 표식이 «알렸다» 를 뜻하지 않게 된다.
|
|
650
|
+
*/
|
|
651
|
+
var notifiedFailures = /* @__PURE__ */ new WeakSet();
|
|
652
|
+
/**
|
|
653
|
+
* 이 실패를 서비스가 `notify.error` 로 이미 사용자에게 알렸는가.
|
|
654
|
+
*
|
|
655
|
+
* 쓰기(`odataPost/Patch/Delete`·`apiPost/Put/Patch/Delete`) 실패는 알린 뒤 다시 던지고, 읽기·`*Quiet`·401 은
|
|
656
|
+
* 알리지 않는다 — 그 정책의 결과를 경계(전역 `unhandledrejection` 핸들러·error boundary)가 서비스 인스턴스
|
|
657
|
+
* 없이 읽는 자리다: `if (!wasNotified(err)) toast(message)`. `ApiError` 는 같은 사실을 `err.notified` 로도 준다.
|
|
658
|
+
* 네트워크 실패처럼 `ApiError` 가 아닌 것도 쓰기 경로에서 알렸다면 `true` 다.
|
|
659
|
+
*/
|
|
660
|
+
function wasNotified(error) {
|
|
661
|
+
return typeof error === "object" && error !== null && notifiedFailures.has(error);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
648
664
|
* API 호출 실패 에러 — HTTP status 를 실어 호출부가 상태별 분기(예: 404 도메인 문구)를 할 수 있게 한다.
|
|
649
665
|
* `Error` 를 상속하므로 기존 `e instanceof Error`/`e.message` 소비처는 그대로 동작한다.
|
|
650
666
|
*/
|
|
@@ -655,6 +671,13 @@ var ApiError = class extends Error {
|
|
|
655
671
|
this.status = status;
|
|
656
672
|
this.details = details;
|
|
657
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* 서비스가 이 실패를 `notify.error` 로 **이미 사용자에게 알렸는가** — 경계가 «알리지 않은 것만» 알리는 근거.
|
|
676
|
+
* `notify.error` 를 설정하지 않은 서비스에서는 알린 것이 없으므로 항상 `false` 다. 읽기 전용이다.
|
|
677
|
+
*/
|
|
678
|
+
get notified() {
|
|
679
|
+
return notifiedFailures.has(this);
|
|
680
|
+
}
|
|
658
681
|
};
|
|
659
682
|
var DEFAULT_MESSAGES = {
|
|
660
683
|
saved: "Saved",
|
|
@@ -798,7 +821,10 @@ function createODataService(config) {
|
|
|
798
821
|
try {
|
|
799
822
|
return await run();
|
|
800
823
|
} catch (e) {
|
|
801
|
-
if (!(e instanceof ApiError && e.status === 401))
|
|
824
|
+
if (notifyError && !(e instanceof ApiError && e.status === 401)) {
|
|
825
|
+
notifyError(e instanceof Error ? e.message : messages.requestFailed);
|
|
826
|
+
if (typeof e === "object" && e !== null) notifiedFailures.add(e);
|
|
827
|
+
}
|
|
802
828
|
throw e;
|
|
803
829
|
}
|
|
804
830
|
}
|
|
@@ -1074,4 +1100,4 @@ function createAuthClient(config) {
|
|
|
1074
1100
|
};
|
|
1075
1101
|
}
|
|
1076
1102
|
//#endregion
|
|
1077
|
-
export { ApiConfig, ApiError, CurrencyHelper, DateHelper, EMPTY_VALUE_DISPLAY, FormRow, FormSection, ProgressHelper, UrgencyHelper, clearPermissions, createAuthClient, createODataService, createPermissionStore, defaultPermissionStore, getPermissions, hasAllPermissions, hasAnyPermission, hasPermission, messages, setPermissions };
|
|
1103
|
+
export { ApiConfig, ApiError, CurrencyHelper, DateHelper, EMPTY_VALUE_DISPLAY, FormRow, FormSection, ProgressHelper, UrgencyHelper, clearPermissions, createAuthClient, createODataService, createPermissionStore, defaultPermissionStore, getPermissions, hasAllPermissions, hasAnyPermission, hasPermission, messages, setPermissions, wasNotified };
|
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.17.0",
|
|
4
4
|
"description": "Enterprise utilities and components for iyulab framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"enterprise",
|
|
@@ -66,6 +66,6 @@
|
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"react": ">=18",
|
|
69
|
-
"@iyulab/components": ">=1.
|
|
69
|
+
"@iyulab/components": ">=1.44.0"
|
|
70
70
|
}
|
|
71
71
|
}
|