@xfilecom/xframe 0.1.37 → 0.1.39
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/bin/xframe.js +7 -1
- package/defaults.json +2 -2
- package/package.json +1 -1
- package/template/apps/api/src/main.ts +36 -1
- package/template/docs/SCAFFOLD_CHECKLIST.md +13 -0
- package/template/shared/README.md +1 -1
- package/template/shared/endpoint/endpoint.ts +23 -5
- package/template/web/admin/src/App.tsx +6 -12
- package/template/web/admin/src/main.tsx +10 -1
- package/template/web/admin/src/vite-env.d.ts +3 -2
- package/template/web/admin/vite.config.ts +2 -2
- package/template/web/client/src/App.tsx +4 -8
- package/template/web/client/src/FrontCoreShowcase.tsx +44 -1
- package/template/web/client/src/main.tsx +11 -1
- package/template/web/client/src/vite-env.d.ts +3 -2
- package/template/web/client/vite.config.ts +3 -2
- package/template/web/shared/README.md +5 -0
- package/template/web/shared/src/api/client.ts +69 -8
- package/template/web/shared/src/api/commonResponse.ts +53 -5
- package/template/web/shared/src/api/methods/health.method.ts +20 -1
- package/template/web/shared/src/api/methods/index.ts +8 -1
- package/template/web/shared/src/api/request.ts +33 -1
- package/template/web/shared/src/api/types.ts +10 -1
- package/template/web/shared/src/context/StoreProvider.tsx +82 -3
- package/template/web/shared/src/hooks/useAppStore.ts +28 -2
- package/template/web/shared/src/hooks/useHealthStatus.ts +35 -16
- package/template/web/shared/src/import-meta.d.ts +13 -0
- package/template/web/shared/src/methods/health.ts +16 -1
- package/template/web/shared/src/store/api-cache-store.ts +19 -0
- package/template/web/shared/src/store/root-store.ts +183 -11
- package/template/web/shared/src/store/session-store.ts +21 -2
- package/template/web/shared/src/styles/app.css +23 -0
- package/template/web/shared/src/types/ui.ts +7 -1
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* RootStore — 프론트 “앱 오케스트레이션” 허브 (MobX)
|
|
4
|
+
* =============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 포함 관계
|
|
7
|
+
* ---------
|
|
8
|
+
* - **sessionStore** (싱글톤): JWT 등 — `api/client`가 Authorization 헤더에 반영.
|
|
9
|
+
* - **apiCacheStore** (싱글톤): `getCachedOrExecute`용 짧은 TTL 캐시.
|
|
10
|
+
* - **paramStore** (인스턴스): 화면별 URL/폼 파라미터 스키마. `getStore: () => this`로
|
|
11
|
+
* 검증 시 RootStore에 접근 가능.
|
|
12
|
+
*
|
|
13
|
+
* HTTP / UI 연동 (가장 중요)
|
|
14
|
+
* ---------------------------
|
|
15
|
+
* 생성자에서 `setApiHooks`로 axios와 연결한다:
|
|
16
|
+
*
|
|
17
|
+
* | 훅 | 호출 시점 | 템플릿 기본 동작 |
|
|
18
|
+
* |----|-----------|------------------|
|
|
19
|
+
* | onRequestStart/End | 각 apiClient 요청 (skip 아닐 때) | `_requestDepth` 증감 → `indicator.loading` |
|
|
20
|
+
* | onError | HTTP 실패 응답·네트워크 등 | `addToast(..., 'error')` |
|
|
21
|
+
* | onCommonBusinessError | HTTP 2xx + 본문 `code !== 0` | 동일 |
|
|
22
|
+
*
|
|
23
|
+
* 따라서 **대부분의 API 에러 알림은 페이지가 아니라 여기서 토스트**로 처리된다.
|
|
24
|
+
* `sendMessage` / `execute`의 catch에서는 `axios.isAxiosError`·`CommonResponseRejectedError`일 때
|
|
25
|
+
* **중복 토스트를 피하도록** 이미 인터셉터에서 처리된 경우를 건너뛴다.
|
|
26
|
+
*
|
|
27
|
+
* 토스트 UX (템플릿 기본)
|
|
28
|
+
* ----------------------
|
|
29
|
+
* - **error**: 자동 제거 없음 — 사용자가 닫기만 가능 (`StoreProvider`에서 `dismissible` true).
|
|
30
|
+
* - **info / success / warn**: `TOAST_AUTO_DISMISS_MS` 후 자동 제거, 닫기 버튼 없음.
|
|
31
|
+
*
|
|
32
|
+
* execute / sendMessage
|
|
33
|
+
* ----------------------
|
|
34
|
+
* - **sendMessage**: 단일 `EndpointDef` + 옵션. `showIndicator`에 따라 (1) 로컬 `setIndicator`와
|
|
35
|
+
* (2) axios `skipGlobalIndicator`를 맞춘다. 전역 바만 쓰려면 엔드포인트/`showIndicator` 정책을 통일할 것.
|
|
36
|
+
* - **execute**: `ApiMethodConfig` 기반 워크플로 — 검증, `sendMessage` 또는 다중 `sendRequest`,
|
|
37
|
+
* `post` 훅, `redirectPath`, 토스트 옵션 등.
|
|
38
|
+
* - **다중 엔드포인트 배치** (`config.endpoints`): `_multiEndpointBatch`로 axios 전역 카운터를
|
|
39
|
+
* 잠시 억제하는 패턴이 있음(배치 중 개별 요청이 바를 깜빡이지 않게).
|
|
40
|
+
*
|
|
41
|
+
* Health 데모 필드
|
|
42
|
+
* ----------------
|
|
43
|
+
* `healthData`, `healthError`, `lastHealthAt` — `useHealthStatus` + 샘플 UI용.
|
|
44
|
+
* 프로덕션 앱에서는 도메인 스토어로 옮기거나 제거해도 된다.
|
|
3
45
|
*/
|
|
4
46
|
|
|
47
|
+
import axios from 'axios';
|
|
5
48
|
import { makeAutoObservable, runInAction } from 'mobx';
|
|
6
49
|
import { setApiHooks } from '../api/client';
|
|
7
50
|
import {
|
|
@@ -28,11 +71,19 @@ import { sessionStore } from './session-store';
|
|
|
28
71
|
import { ParamStore } from '../params/param-store';
|
|
29
72
|
import type { ApiErrorItem, IndicatorState, SendMessageOptions, ToastItem } from '../types/ui';
|
|
30
73
|
import type { ToastSeverity } from '../types/ui';
|
|
74
|
+
import type { HealthData } from '../types/api';
|
|
31
75
|
|
|
76
|
+
/** 토스트·에러 항목 등에 쓰는 짧은 고유 id (타임스탬프 + 랜덤) */
|
|
32
77
|
function genId() {
|
|
33
78
|
return `id_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
34
79
|
}
|
|
35
80
|
|
|
81
|
+
/**
|
|
82
|
+
* 코어 SDK 스타일 `{ data: T | T[] }` 래핑을 한 겹 벗긴다.
|
|
83
|
+
* 배열이면 첫 요소를 대표값으로 쓰는 레거시 규칙이 있음 (`unwrapDataArray`).
|
|
84
|
+
* @param res - API 응답 본문(또는 이미 펼친 T)
|
|
85
|
+
* @param unwrapDataArray - false면 `res`를 그대로 T로 취급
|
|
86
|
+
*/
|
|
36
87
|
function unwrapCoreSdk<T>(res: T | CoreSdkWrapped<T>, unwrapDataArray: boolean): T | null {
|
|
37
88
|
if (!unwrapDataArray) return res as T;
|
|
38
89
|
const r = res as CoreSdkWrapped<T>;
|
|
@@ -43,6 +94,9 @@ function unwrapCoreSdk<T>(res: T | CoreSdkWrapped<T>, unwrapDataArray: boolean):
|
|
|
43
94
|
return res as T;
|
|
44
95
|
}
|
|
45
96
|
|
|
97
|
+
/**
|
|
98
|
+
* API 에러 본문 등에서 사람이 읽을 문자열 후보를 재귀적으로 추출한다.
|
|
99
|
+
*/
|
|
46
100
|
function extractMessageString(value: unknown): string | null {
|
|
47
101
|
if (value == null) return null;
|
|
48
102
|
if (typeof value === 'string') return value;
|
|
@@ -57,6 +111,10 @@ function extractMessageString(value: unknown): string | null {
|
|
|
57
111
|
return null;
|
|
58
112
|
}
|
|
59
113
|
|
|
114
|
+
/**
|
|
115
|
+
* `sendMessage`/`execute` catch 등에서 토스트에 넣을 문구를 만든다.
|
|
116
|
+
* @param e - AxiosError, CommonResponseRejectedError, 일반 Error 등
|
|
117
|
+
*/
|
|
60
118
|
function getErrorMessage(e: unknown): string {
|
|
61
119
|
if (e instanceof CommonResponseRejectedError) return e.message;
|
|
62
120
|
const err = e as { response?: { data?: unknown }; message?: string };
|
|
@@ -79,15 +137,29 @@ export class RootStore {
|
|
|
79
137
|
getStore: () => this,
|
|
80
138
|
});
|
|
81
139
|
|
|
140
|
+
/** 전역 로딩 + (선택) 메시지, 현재 실행 중 method 이름 등 */
|
|
82
141
|
private _indicator: IndicatorState = { loading: false, methodLoading: null };
|
|
142
|
+
/** 패널용 에러 리스트 — 토스트와 별도 */
|
|
83
143
|
private _errors: ApiErrorItem[] = [];
|
|
144
|
+
/** StoreProvider 전역 토스트 스택 데이터 소스 */
|
|
84
145
|
private _toasts: ToastItem[] = [];
|
|
146
|
+
/** info/success/warn 자동 제거용 — `removeToast`·타이머 콜백에서 정리 */
|
|
147
|
+
private _toastAutoDismissTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
148
|
+
/** axios onRequestStart가 중첩 호출될 때를 위한 깊이 카운터 */
|
|
85
149
|
private _requestDepth = 0;
|
|
150
|
+
/** execute 다중 엔드포인트 루프 중 전역 인디케이터 억제 */
|
|
86
151
|
private _multiEndpointBatch = false;
|
|
87
152
|
|
|
88
153
|
/** 데모·useHealthStatus 용 타임스탬프 */
|
|
89
154
|
lastHealthAt: number | null = null;
|
|
90
155
|
|
|
156
|
+
/** `/health` 성공 본문 (axios 공통 래핑 제거 후) */
|
|
157
|
+
healthData: HealthData | null = null;
|
|
158
|
+
healthError: string | null = null;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* MobX 관찰 가능 필드로 등록하고, 세션 로그아웃 시 캐시 비우기·axios 훅을 `rootStore`에 연결한다.
|
|
162
|
+
*/
|
|
91
163
|
constructor() {
|
|
92
164
|
makeAutoObservable(this);
|
|
93
165
|
this.sessionStore.registerLogoutCallback(() => {
|
|
@@ -104,29 +176,67 @@ export class RootStore {
|
|
|
104
176
|
if (this._multiEndpointBatch) return;
|
|
105
177
|
runInAction(() => this._endRequest());
|
|
106
178
|
},
|
|
107
|
-
onError: () => {
|
|
179
|
+
onError: (message) => {
|
|
180
|
+
runInAction(() => this.addToast(message, 'error'));
|
|
181
|
+
},
|
|
108
182
|
onCommonBusinessError: (message) => {
|
|
109
183
|
runInAction(() => this.addToast(message, 'error'));
|
|
110
184
|
},
|
|
111
185
|
});
|
|
112
186
|
}
|
|
113
187
|
|
|
188
|
+
/** 전역 로딩·옵션 메시지·현재 method 로딩 키 */
|
|
114
189
|
get indicator(): IndicatorState {
|
|
115
190
|
return this._indicator;
|
|
116
191
|
}
|
|
117
192
|
|
|
193
|
+
/** 패널/리스트용 에러 히스토리 (토스트와 별도) */
|
|
118
194
|
get errors(): ApiErrorItem[] {
|
|
119
195
|
return this._errors;
|
|
120
196
|
}
|
|
121
197
|
|
|
198
|
+
/** `GlobalToastStackView`가 구독하는 스택 */
|
|
122
199
|
get toasts(): ToastItem[] {
|
|
123
200
|
return this._toasts;
|
|
124
201
|
}
|
|
125
202
|
|
|
126
|
-
|
|
203
|
+
/**
|
|
204
|
+
* 마지막 health 성공 시각(밀리초). 화살표 필드라 `useAppStore(s => s.setLastHealthAt)`로 꺼내 호출해도 안전.
|
|
205
|
+
* @param t - `Date.now()` 등
|
|
206
|
+
*/
|
|
207
|
+
setLastHealthAt = (t: number) => {
|
|
127
208
|
this.lastHealthAt = t;
|
|
128
|
-
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** `/health` 요청 직전: `healthError`만 초기화 (진행 표시는 axios `indicator`) */
|
|
212
|
+
applyHealthFetchStart = () => {
|
|
213
|
+
runInAction(() => {
|
|
214
|
+
this.healthError = null;
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* `/health` 성공 시 스토어에 페이로드 반영.
|
|
220
|
+
* @param data - 인터셉터에서 봉투 제거된 뒤의 본문
|
|
221
|
+
*/
|
|
222
|
+
applyHealthFetchSuccess = (data: HealthData) => {
|
|
223
|
+
runInAction(() => {
|
|
224
|
+
this.healthError = null;
|
|
225
|
+
this.healthData = data;
|
|
226
|
+
this.lastHealthAt = Date.now();
|
|
227
|
+
});
|
|
228
|
+
};
|
|
129
229
|
|
|
230
|
+
/**
|
|
231
|
+
* `/health` 실패 시 `healthError`에 정규화된 메시지 저장 (토스트는 인터셉터가 담당할 수 있음).
|
|
232
|
+
*/
|
|
233
|
+
applyHealthFetchFailure = (err: unknown) => {
|
|
234
|
+
runInAction(() => {
|
|
235
|
+
this.healthError = getErrorMessage(err);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/** axios `onRequestStart`에서 호출: 중첩 요청 시 깊이만 증가, 첫 요청에서만 `loading: true` */
|
|
130
240
|
private _startRequest() {
|
|
131
241
|
this._requestDepth++;
|
|
132
242
|
if (this._requestDepth === 1) {
|
|
@@ -134,6 +244,7 @@ export class RootStore {
|
|
|
134
244
|
}
|
|
135
245
|
}
|
|
136
246
|
|
|
247
|
+
/** axios `onRequestEnd`에서 호출: 깊이 0이 되면 `loading: false` */
|
|
137
248
|
private _endRequest() {
|
|
138
249
|
this._requestDepth = Math.max(0, this._requestDepth - 1);
|
|
139
250
|
if (this._requestDepth === 0) {
|
|
@@ -141,41 +252,80 @@ export class RootStore {
|
|
|
141
252
|
}
|
|
142
253
|
}
|
|
143
254
|
|
|
255
|
+
/**
|
|
256
|
+
* `sendMessage` 등에서 쓰는 로컬 인디케이터(전역 바와 별개로 메시지 동반 가능).
|
|
257
|
+
*/
|
|
144
258
|
setIndicator(loading: boolean, message?: string) {
|
|
145
259
|
this._indicator = { ...this._indicator, loading, message };
|
|
146
260
|
}
|
|
147
261
|
|
|
262
|
+
/**
|
|
263
|
+
* `executeMethod`가 어떤 키를 실행 중인지 UI에 표시할 때 사용.
|
|
264
|
+
*/
|
|
148
265
|
setMethodLoading(methodName: string | null) {
|
|
149
266
|
this._indicator = { ...this._indicator, methodLoading: methodName };
|
|
150
267
|
}
|
|
151
268
|
|
|
269
|
+
/**
|
|
270
|
+
* 토스트가 아닌 “에러 패널”용 항목 추가 (최대 50건 유지).
|
|
271
|
+
*/
|
|
152
272
|
addError(message: string, code?: string | number) {
|
|
153
273
|
this._errors = [{ id: genId(), message, code, createdAt: Date.now() }, ...this._errors].slice(0, 50);
|
|
154
274
|
}
|
|
155
275
|
|
|
276
|
+
/** `addError`로 넣은 한 건 제거 */
|
|
156
277
|
clearError(id: string) {
|
|
157
278
|
this._errors = this._errors.filter((e) => e.id !== id);
|
|
158
279
|
}
|
|
159
280
|
|
|
281
|
+
/** 에러 패널 전체 비우기 */
|
|
160
282
|
clearAllErrors() {
|
|
161
283
|
this._errors = [];
|
|
162
284
|
}
|
|
163
285
|
|
|
164
286
|
private static readonly TOAST_AUTO_DISMISS_MS = 2000;
|
|
165
287
|
|
|
288
|
+
/**
|
|
289
|
+
* 동일 메시지+심각도 연속 스팸 방지.
|
|
290
|
+
* - **error**: 타이머 없음 — 닫기만 제거 (`GlobalToastStackView`에서 error만 `dismissible`).
|
|
291
|
+
* - **그 외**: `TOAST_AUTO_DISMISS_MS` 후 자동 제거.
|
|
292
|
+
* @param message - 토스트 본문
|
|
293
|
+
* @param severity - `info` | `success` | `warn` | `error`
|
|
294
|
+
*/
|
|
166
295
|
addToast(message: string, severity: ToastSeverity = 'info') {
|
|
167
296
|
if (this._toasts.some((t) => t.message === message && t.severity === severity)) return;
|
|
168
297
|
const id = genId();
|
|
169
298
|
this._toasts = [...this._toasts, { id, message, severity, createdAt: Date.now() }].slice(-20);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
299
|
+
if (severity !== 'error') {
|
|
300
|
+
const tid = setTimeout(() => {
|
|
301
|
+
runInAction(() => {
|
|
302
|
+
this._toastAutoDismissTimers.delete(id);
|
|
303
|
+
this.removeToast(id);
|
|
304
|
+
});
|
|
305
|
+
}, RootStore.TOAST_AUTO_DISMISS_MS);
|
|
306
|
+
this._toastAutoDismissTimers.set(id, tid);
|
|
307
|
+
}
|
|
173
308
|
}
|
|
174
309
|
|
|
310
|
+
/** 닫기 클릭 또는 비-error 자동 제거 타이머에서 호출 */
|
|
175
311
|
removeToast(id: string) {
|
|
312
|
+
const pending = this._toastAutoDismissTimers.get(id);
|
|
313
|
+
if (pending !== undefined) {
|
|
314
|
+
clearTimeout(pending);
|
|
315
|
+
this._toastAutoDismissTimers.delete(id);
|
|
316
|
+
}
|
|
176
317
|
this._toasts = this._toasts.filter((t) => t.id !== id);
|
|
177
318
|
}
|
|
178
319
|
|
|
320
|
+
/**
|
|
321
|
+
* 등록된 `ApiMethodConfig` 워크플로 실행.
|
|
322
|
+
* - `endpoints` 배열이 있으면 순차 `sendRequest` (다중 배치 플래그 사용).
|
|
323
|
+
* - 아니면 단일 `sendMessage` 후 unwrap/post/redirect.
|
|
324
|
+
*
|
|
325
|
+
* @param config - 엔드포인트·UI 옵션·validate·post·redirectPath 등
|
|
326
|
+
* @param payload - 메서드 인자(바디/쿼리 빌더에서 사용)
|
|
327
|
+
* @param ctx - 라우터 `navigate` 등 실행 컨텍스트
|
|
328
|
+
*/
|
|
179
329
|
async execute<TPayload = unknown, TResult = unknown>(
|
|
180
330
|
config: ApiMethodConfig<TPayload, TResult>,
|
|
181
331
|
payload?: TPayload,
|
|
@@ -239,7 +389,11 @@ export class RootStore {
|
|
|
239
389
|
}
|
|
240
390
|
return result;
|
|
241
391
|
} catch (e) {
|
|
242
|
-
if (
|
|
392
|
+
if (
|
|
393
|
+
showErrorToast &&
|
|
394
|
+
!(e instanceof CommonResponseRejectedError) &&
|
|
395
|
+
!axios.isAxiosError(e)
|
|
396
|
+
) {
|
|
243
397
|
runInAction(() => this.addToast(getErrorMessage(e), 'error'));
|
|
244
398
|
}
|
|
245
399
|
throw e;
|
|
@@ -288,6 +442,10 @@ export class RootStore {
|
|
|
288
442
|
return finalResult;
|
|
289
443
|
}
|
|
290
444
|
|
|
445
|
+
/**
|
|
446
|
+
* `methods` 레지스트리에 등록된 키로 `execute` 호출. `indicator.methodLoading` 설정.
|
|
447
|
+
* @param methodName - 예: `'health'`
|
|
448
|
+
*/
|
|
291
449
|
async executeMethod<K extends MethodName>(
|
|
292
450
|
methodName: K,
|
|
293
451
|
payload?: (typeof methods)[K] extends ApiMethodConfig<infer P, unknown> ? P : never,
|
|
@@ -309,6 +467,11 @@ export class RootStore {
|
|
|
309
467
|
}
|
|
310
468
|
}
|
|
311
469
|
|
|
470
|
+
/**
|
|
471
|
+
* 캐시 히트 시 네트워크 생략, 미스 시 `executeMethod` 후 저장.
|
|
472
|
+
* @param cacheKey - 호출자 정의 문자열(화면+파라미터 해시 등)
|
|
473
|
+
* @param ttlMs - 만료(ms); 생략 시 만료 없음
|
|
474
|
+
*/
|
|
312
475
|
async getCachedOrExecute<K extends MethodName>(
|
|
313
476
|
cacheKey: string,
|
|
314
477
|
methodName: K,
|
|
@@ -326,8 +489,13 @@ export class RootStore {
|
|
|
326
489
|
}
|
|
327
490
|
|
|
328
491
|
/**
|
|
329
|
-
*
|
|
330
|
-
* `showIndicator
|
|
492
|
+
* 단일 엔드포인트 호출 래퍼.
|
|
493
|
+
* - `showIndicator`: 옵션 → `endpoint.showIndicator` → 기본 true.
|
|
494
|
+
* - `skipGlobalIndicator: !showIndicator` 로 axios 전역 바와 로컬 setIndicator를 정렬.
|
|
495
|
+
* - 에러 시: Axios/봉투 비즈니스 에러는 인터셉터가 이미 토스트 → catch에서는 중복 방지.
|
|
496
|
+
*
|
|
497
|
+
* @param endpoint - shared `EndpointDef`
|
|
498
|
+
* @param options - 쿼리·바디·토스트·인디케이터 옵션
|
|
331
499
|
*/
|
|
332
500
|
async sendMessage<T = unknown, R = unknown>(endpoint: EndpointDef, options: SendMessageOptions<T> = {}): Promise<R> {
|
|
333
501
|
const {
|
|
@@ -358,7 +526,11 @@ export class RootStore {
|
|
|
358
526
|
return data;
|
|
359
527
|
} catch (e) {
|
|
360
528
|
runInAction(() => {
|
|
361
|
-
if (
|
|
529
|
+
if (
|
|
530
|
+
showErrorToast &&
|
|
531
|
+
!(e instanceof CommonResponseRejectedError) &&
|
|
532
|
+
!axios.isAxiosError(e)
|
|
533
|
+
) {
|
|
362
534
|
this.addToast(getErrorMessage(e), 'error');
|
|
363
535
|
}
|
|
364
536
|
});
|
|
@@ -1,25 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* SessionStore — 인증 세션 (싱글톤)
|
|
4
|
+
* =============================================================================
|
|
5
|
+
*
|
|
6
|
+
* - `token`이 설정되면 `api/client` 요청 인터셉터가 `Authorization: Bearer …`를 붙인다.
|
|
7
|
+
* - `logout()` / `setToken(null)` 시 `registerLogoutCallback`으로 등록된 루틴 실행
|
|
8
|
+
* (템플릿에서는 RootStore가 `apiCacheStore.clear()` 연동).
|
|
9
|
+
*
|
|
10
|
+
* 앱별로 refresh 토큰·저장소(localStorage) 동기화는 소비자 앱에서 확장한다.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
import { makeAutoObservable } from 'mobx';
|
|
2
14
|
|
|
3
|
-
/** Bearer 토큰 등 — api/client 가 Authorization 헤더에 반영 */
|
|
4
15
|
export class SessionStore {
|
|
5
16
|
token: string | null = null;
|
|
6
17
|
private _onLogout?: () => void;
|
|
7
18
|
|
|
19
|
+
/** MobX 관찰 필드로 등록 */
|
|
8
20
|
constructor() {
|
|
9
21
|
makeAutoObservable(this);
|
|
10
22
|
}
|
|
11
23
|
|
|
12
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* 로그아웃 시 한 번 호출할 콜백 등록 (중복 등록 시 마지막 것이 유효).
|
|
26
|
+
* @param cb - 예: API 캐시 클리어
|
|
27
|
+
*/
|
|
13
28
|
registerLogoutCallback(cb: () => void) {
|
|
14
29
|
this._onLogout = cb;
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 액세스 토큰 설정. `null`로 두면 Bearer 제거; 이전에 토큰이 있었는데 `null`이면 `_onLogout` 실행.
|
|
34
|
+
*/
|
|
17
35
|
setToken(token: string | null) {
|
|
18
36
|
const had = this.token != null;
|
|
19
37
|
this.token = token;
|
|
20
38
|
if (had && token == null) this._onLogout?.();
|
|
21
39
|
}
|
|
22
40
|
|
|
41
|
+
/** `setToken(null)`과 동일 */
|
|
23
42
|
logout() {
|
|
24
43
|
this.setToken(null);
|
|
25
44
|
}
|
|
@@ -67,4 +67,27 @@
|
|
|
67
67
|
border-bottom: 1px solid var(--xfc-border-header);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/* apiClient 진행 중 — StoreProvider 가 `rootStore.indicator.loading` 과 연동 */
|
|
71
|
+
.xfc-global-http-indicator {
|
|
72
|
+
position: fixed;
|
|
73
|
+
top: 0;
|
|
74
|
+
left: 0;
|
|
75
|
+
right: 0;
|
|
76
|
+
height: 3px;
|
|
77
|
+
z-index: 99999;
|
|
78
|
+
background: var(--xfc-accent, var(--xfc-border));
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
animation: xfc-global-http-pulse 0.9s ease-in-out infinite;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes xfc-global-http-pulse {
|
|
84
|
+
0%,
|
|
85
|
+
100% {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
}
|
|
88
|
+
50% {
|
|
89
|
+
opacity: 0.45;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
/* —— front-core atoms 추가 오버라이드는 xfc-theme.css 또는 여기 (app이 마지막 로드) —— */
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* UI 상태 타입 — RootStore·StoreProvider·sendMessage 옵션과 공유
|
|
4
|
+
* =============================================================================
|
|
5
|
+
*
|
|
6
|
+
* - **ToastItem**: MobX 배열 → `GlobalToastStackView`에서 front-core `Toast`로 매핑 (error만 닫기 버튼).
|
|
7
|
+
* - **IndicatorState**: 전역 로딩 바(`loading`) + (선택) 문구, 현재 method 이름 표시용 슬롯.
|
|
8
|
+
* - **SendMessageOptions**: `sendRequest`에 그대로 안 넘어가고 RootStore가 해석 후 일부만 전달.
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
11
|
export type ToastSeverity = 'info' | 'success' | 'warn' | 'error';
|