@xfilecom/xframe 0.1.37 → 0.1.38
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/defaults.json +2 -2
- package/package.json +1 -1
- package/template/shared/endpoint/endpoint.ts +1 -0
- package/template/web/admin/src/App.tsx +5 -11
- package/template/web/client/src/App.tsx +3 -7
- package/template/web/shared/src/api/client.ts +3 -1
- package/template/web/shared/src/context/StoreProvider.tsx +39 -1
- package/template/web/shared/src/hooks/useAppStore.ts +1 -0
- package/template/web/shared/src/hooks/useHealthStatus.ts +11 -15
- package/template/web/shared/src/store/root-store.ts +43 -5
- package/template/web/shared/src/styles/app.css +23 -0
package/defaults.json
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Badge, Text } from '@xfilecom/front-core';
|
|
2
|
-
import { Shell, useHealthStatus } from '__WEB_SHARED_WORKSPACE__';
|
|
2
|
+
import { safeJsonStringify, Shell, useHealthStatus } from '__WEB_SHARED_WORKSPACE__';
|
|
3
3
|
|
|
4
4
|
const title = import.meta.env.VITE_APP_TITLE || '__PACKAGE_NAME__ (admin)';
|
|
5
5
|
|
|
6
6
|
export function App() {
|
|
7
|
-
const {
|
|
7
|
+
const { data } = useHealthStatus();
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
10
|
<Shell
|
|
@@ -18,15 +18,9 @@ export function App() {
|
|
|
18
18
|
</>
|
|
19
19
|
}
|
|
20
20
|
>
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
</Text>
|
|
25
|
-
) : (
|
|
26
|
-
<Text as="pre" variant="small" style={{ margin: 0, overflow: 'auto' }}>
|
|
27
|
-
{text}
|
|
28
|
-
</Text>
|
|
29
|
-
)}
|
|
21
|
+
<Text as="pre" variant="small" style={{ margin: 0, overflow: 'auto' }}>
|
|
22
|
+
{data != null ? safeJsonStringify(data, 2) : '—'}
|
|
23
|
+
</Text>
|
|
30
24
|
</Shell>
|
|
31
25
|
);
|
|
32
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { Badge, Button, Stack, Text } from '@xfilecom/front-core';
|
|
3
|
-
import { Shell, useHealthStatus } from '__WEB_SHARED_WORKSPACE__';
|
|
3
|
+
import { safeJsonStringify, Shell, useHealthStatus } from '__WEB_SHARED_WORKSPACE__';
|
|
4
4
|
import { FrontCoreShowcase } from './FrontCoreShowcase';
|
|
5
5
|
|
|
6
6
|
const title = import.meta.env.VITE_APP_TITLE || '__PACKAGE_NAME__';
|
|
@@ -9,7 +9,7 @@ type Tab = 'api' | 'atoms';
|
|
|
9
9
|
|
|
10
10
|
export function App() {
|
|
11
11
|
const [tab, setTab] = useState<Tab>('atoms');
|
|
12
|
-
const {
|
|
12
|
+
const { data } = useHealthStatus();
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
15
|
<Shell
|
|
@@ -44,13 +44,9 @@ export function App() {
|
|
|
44
44
|
|
|
45
45
|
{tab === 'atoms' ? (
|
|
46
46
|
<FrontCoreShowcase />
|
|
47
|
-
) : error ? (
|
|
48
|
-
<Text as="pre" variant="body" style={{ color: 'var(--xfc-warning)', margin: 0, whiteSpace: 'pre-wrap' }}>
|
|
49
|
-
{error}
|
|
50
|
-
</Text>
|
|
51
47
|
) : (
|
|
52
48
|
<Text as="pre" variant="small" style={{ margin: 0, overflow: 'auto' }}>
|
|
53
|
-
{
|
|
49
|
+
{data != null ? safeJsonStringify(data, 2) : '—'}
|
|
54
50
|
</Text>
|
|
55
51
|
)}
|
|
56
52
|
</Stack>
|
|
@@ -75,11 +75,13 @@ function logRequest(config: InternalAxiosRequestConfig) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function logResponseSuccess(response: AxiosResponse) {
|
|
78
|
+
const body = response.data;
|
|
79
|
+
const data = isCommonResponsePayload(body) ? body.data : body;
|
|
78
80
|
console.log(`${LOG_PREFIX} response`, {
|
|
79
81
|
status: response.status,
|
|
80
82
|
statusText: response.statusText,
|
|
81
83
|
url: response.config.url,
|
|
82
|
-
data
|
|
84
|
+
data,
|
|
83
85
|
});
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RootStore 컨텍스트 — useStore / useParamStore / useSendMessage 등
|
|
3
3
|
* (싱글톤 rootStore 이므로 Provider 없이도 rootStore 직접 import 가능)
|
|
4
|
+
*
|
|
5
|
+
* 최상위 레이어: HTTP 로딩 바(`indicator`) · `rootStore.toasts` → ToastList
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import React, { createContext, useCallback, useContext } from 'react';
|
|
9
|
+
import { ToastList } from '@xfilecom/front-core';
|
|
10
|
+
import { observer } from 'mobx-react-lite';
|
|
7
11
|
import type { EndpointDef } from '@shared/endpoint';
|
|
8
12
|
import type { SendMessageOptions } from '../types/ui';
|
|
9
13
|
import { rootStore } from '../store/root-store';
|
|
@@ -12,6 +16,34 @@ type RootStoreApi = typeof rootStore;
|
|
|
12
16
|
|
|
13
17
|
const StoreContext = createContext<RootStoreApi | null>(null);
|
|
14
18
|
|
|
19
|
+
const GlobalHttpIndicatorView = observer(function GlobalHttpIndicatorView() {
|
|
20
|
+
if (!rootStore.indicator.loading) return null;
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="xfc-global-http-indicator"
|
|
24
|
+
role="progressbar"
|
|
25
|
+
aria-busy="true"
|
|
26
|
+
aria-label="Loading"
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const GlobalToastStackView = observer(function GlobalToastStackView() {
|
|
32
|
+
const entries = rootStore.toasts.map((t) => ({
|
|
33
|
+
id: t.id,
|
|
34
|
+
severity: t.severity,
|
|
35
|
+
message: t.message,
|
|
36
|
+
}));
|
|
37
|
+
return (
|
|
38
|
+
<ToastList
|
|
39
|
+
toasts={entries}
|
|
40
|
+
onDismiss={(id) => {
|
|
41
|
+
rootStore.removeToast(id);
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
15
47
|
export function useStore(): RootStoreApi {
|
|
16
48
|
const ctx = useContext(StoreContext);
|
|
17
49
|
if (!ctx) throw new Error('StoreProvider is required');
|
|
@@ -19,7 +51,13 @@ export function useStore(): RootStoreApi {
|
|
|
19
51
|
}
|
|
20
52
|
|
|
21
53
|
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
22
|
-
return
|
|
54
|
+
return (
|
|
55
|
+
<StoreContext.Provider value={rootStore}>
|
|
56
|
+
<GlobalHttpIndicatorView />
|
|
57
|
+
<GlobalToastStackView />
|
|
58
|
+
{children}
|
|
59
|
+
</StoreContext.Provider>
|
|
60
|
+
);
|
|
23
61
|
}
|
|
24
62
|
|
|
25
63
|
export function useSendMessage() {
|
|
@@ -5,6 +5,7 @@ import { rootStore } from '../store/root-store';
|
|
|
5
5
|
/**
|
|
6
6
|
* Zustand 스타일 셀렉터 — RootStore(MobX) 구독
|
|
7
7
|
* 예: `useAppStore((s) => s.lastHealthAt)`, `useAppStore((s) => s.setLastHealthAt)`
|
|
8
|
+
* (프로토타입 메서드는 분리 시 this 가 깨지므로, 콜백으로 쓸 메서드는 스토어에서 화살표 필드로 두거나 `bind` 하세요.)
|
|
8
9
|
*/
|
|
9
10
|
export function useAppStore<T>(selector: (store: typeof rootStore) => T): T {
|
|
10
11
|
return useSyncExternalStore(
|
|
@@ -1,38 +1,34 @@
|
|
|
1
|
-
import { useEffect
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
2
|
import { configureApi } from '../api/client';
|
|
3
3
|
import { fetchHealth } from '../methods/health';
|
|
4
|
-
import {
|
|
4
|
+
import { rootStore } from '../store/root-store';
|
|
5
5
|
import { useAppStore } from './useAppStore';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @param apiBaseUrl 선택 — 넘기면 `configureApi` 로 baseURL 갱신. 앱 entry 에서 이미 `configureApi` 했다면 생략 가능.
|
|
9
|
+
* `data`·`error` 는 `rootStore` 에 저장·`runInAction`; 로딩 표시는 `StoreProvider` 전역 바 + `indicator`(axios).
|
|
9
10
|
*/
|
|
10
11
|
export function useHealthStatus(apiBaseUrl?: string) {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const data = useAppStore((s) => s.healthData);
|
|
13
|
+
const error = useAppStore((s) => s.healthError);
|
|
14
14
|
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
if (apiBaseUrl != null && apiBaseUrl !== '') {
|
|
17
17
|
configureApi({ baseURL: apiBaseUrl });
|
|
18
18
|
}
|
|
19
19
|
let cancelled = false;
|
|
20
|
-
|
|
21
|
-
setText('loading…');
|
|
20
|
+
rootStore.applyHealthFetchStart();
|
|
22
21
|
fetchHealth()
|
|
23
22
|
.then((body) => {
|
|
24
|
-
if (!cancelled)
|
|
25
|
-
setText(safeJsonStringify(body, 2));
|
|
26
|
-
setLastHealthAt(Date.now());
|
|
27
|
-
}
|
|
23
|
+
if (!cancelled) rootStore.applyHealthFetchSuccess(body);
|
|
28
24
|
})
|
|
29
|
-
.catch((e:
|
|
30
|
-
if (!cancelled)
|
|
25
|
+
.catch((e: unknown) => {
|
|
26
|
+
if (!cancelled) rootStore.applyHealthFetchFailure(e);
|
|
31
27
|
});
|
|
32
28
|
return () => {
|
|
33
29
|
cancelled = true;
|
|
34
30
|
};
|
|
35
|
-
}, [apiBaseUrl
|
|
31
|
+
}, [apiBaseUrl]);
|
|
36
32
|
|
|
37
|
-
return {
|
|
33
|
+
return { data, error };
|
|
38
34
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Root store — session, param, cache, indicator, execute/sendMessage (business-promotion 패턴 축소판)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import axios from 'axios';
|
|
5
6
|
import { makeAutoObservable, runInAction } from 'mobx';
|
|
6
7
|
import { setApiHooks } from '../api/client';
|
|
7
8
|
import {
|
|
@@ -28,6 +29,7 @@ import { sessionStore } from './session-store';
|
|
|
28
29
|
import { ParamStore } from '../params/param-store';
|
|
29
30
|
import type { ApiErrorItem, IndicatorState, SendMessageOptions, ToastItem } from '../types/ui';
|
|
30
31
|
import type { ToastSeverity } from '../types/ui';
|
|
32
|
+
import type { HealthData } from '../types/api';
|
|
31
33
|
|
|
32
34
|
function genId() {
|
|
33
35
|
return `id_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -88,6 +90,10 @@ export class RootStore {
|
|
|
88
90
|
/** 데모·useHealthStatus 용 타임스탬프 */
|
|
89
91
|
lastHealthAt: number | null = null;
|
|
90
92
|
|
|
93
|
+
/** `/health` 성공 본문 (axios 공통 래핑 제거 후) */
|
|
94
|
+
healthData: HealthData | null = null;
|
|
95
|
+
healthError: string | null = null;
|
|
96
|
+
|
|
91
97
|
constructor() {
|
|
92
98
|
makeAutoObservable(this);
|
|
93
99
|
this.sessionStore.registerLogoutCallback(() => {
|
|
@@ -104,7 +110,9 @@ export class RootStore {
|
|
|
104
110
|
if (this._multiEndpointBatch) return;
|
|
105
111
|
runInAction(() => this._endRequest());
|
|
106
112
|
},
|
|
107
|
-
onError: () => {
|
|
113
|
+
onError: (message) => {
|
|
114
|
+
runInAction(() => this.addToast(message, 'error'));
|
|
115
|
+
},
|
|
108
116
|
onCommonBusinessError: (message) => {
|
|
109
117
|
runInAction(() => this.addToast(message, 'error'));
|
|
110
118
|
},
|
|
@@ -123,9 +131,31 @@ export class RootStore {
|
|
|
123
131
|
return this._toasts;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
|
|
134
|
+
/** useAppStore 셀렉터로 넘길 때 this 유지 */
|
|
135
|
+
setLastHealthAt = (t: number) => {
|
|
127
136
|
this.lastHealthAt = t;
|
|
128
|
-
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** 로딩 표시는 `indicator`(axios 훅) — 여기선 에러만 초기화 */
|
|
140
|
+
applyHealthFetchStart = () => {
|
|
141
|
+
runInAction(() => {
|
|
142
|
+
this.healthError = null;
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
applyHealthFetchSuccess = (data: HealthData) => {
|
|
147
|
+
runInAction(() => {
|
|
148
|
+
this.healthError = null;
|
|
149
|
+
this.healthData = data;
|
|
150
|
+
this.lastHealthAt = Date.now();
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
applyHealthFetchFailure = (err: unknown) => {
|
|
155
|
+
runInAction(() => {
|
|
156
|
+
this.healthError = getErrorMessage(err);
|
|
157
|
+
});
|
|
158
|
+
};
|
|
129
159
|
|
|
130
160
|
private _startRequest() {
|
|
131
161
|
this._requestDepth++;
|
|
@@ -239,7 +269,11 @@ export class RootStore {
|
|
|
239
269
|
}
|
|
240
270
|
return result;
|
|
241
271
|
} catch (e) {
|
|
242
|
-
if (
|
|
272
|
+
if (
|
|
273
|
+
showErrorToast &&
|
|
274
|
+
!(e instanceof CommonResponseRejectedError) &&
|
|
275
|
+
!axios.isAxiosError(e)
|
|
276
|
+
) {
|
|
243
277
|
runInAction(() => this.addToast(getErrorMessage(e), 'error'));
|
|
244
278
|
}
|
|
245
279
|
throw e;
|
|
@@ -358,7 +392,11 @@ export class RootStore {
|
|
|
358
392
|
return data;
|
|
359
393
|
} catch (e) {
|
|
360
394
|
runInAction(() => {
|
|
361
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
showErrorToast &&
|
|
397
|
+
!(e instanceof CommonResponseRejectedError) &&
|
|
398
|
+
!axios.isAxiosError(e)
|
|
399
|
+
) {
|
|
362
400
|
this.addToast(getErrorMessage(e), 'error');
|
|
363
401
|
}
|
|
364
402
|
});
|
|
@@ -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이 마지막 로드) —— */
|