@xfilecom/xframe 0.1.35 → 0.1.37
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 +12 -10
- package/defaults.json +2 -2
- package/package.json +1 -1
- package/template/apps/api/src/main.ts +53 -13
- package/template/shared/endpoint/endpoint.ts +1 -1
- package/template/web/admin/tsconfig.json +1 -2
- package/template/web/admin/vite.config.ts +0 -7
- package/template/web/client/tsconfig.json +1 -2
- package/template/web/client/vite.config.ts +0 -7
- package/template/web/shared/README.md +4 -5
- package/template/web/shared/src/api/client.ts +39 -11
- package/template/web/shared/src/api/commonResponse.ts +80 -0
- package/template/web/shared/src/api/methods/health.method.ts +2 -2
- package/template/web/shared/src/index.ts +10 -1
- package/template/web/shared/src/methods/health.ts +3 -3
- package/template/web/shared/src/store/root-store.ts +20 -4
package/bin/xframe.js
CHANGED
|
@@ -442,10 +442,9 @@ function executeDbPull(cwd) {
|
|
|
442
442
|
return false;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
/**
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
/**
|
|
446
|
+
* 구 스캐폴드 tsconfig 의 paths.@xfilecom/front-core 제거 → 설치된 패키지(node_modules) 기준 해석.
|
|
447
|
+
*/
|
|
449
448
|
function patchWebTsconfigFrontCorePath(targetRoot) {
|
|
450
449
|
for (const sub of ['client', 'admin']) {
|
|
451
450
|
const p = path.join(targetRoot, 'web', sub, 'tsconfig.json');
|
|
@@ -456,17 +455,20 @@ function patchWebTsconfigFrontCorePath(targetRoot) {
|
|
|
456
455
|
} catch {
|
|
457
456
|
continue;
|
|
458
457
|
}
|
|
459
|
-
const
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
458
|
+
const paths = j?.compilerOptions?.paths;
|
|
459
|
+
if (!paths || !Object.prototype.hasOwnProperty.call(paths, '@xfilecom/front-core')) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
delete paths['@xfilecom/front-core'];
|
|
463
|
+
if (Object.keys(paths).length === 0) {
|
|
464
|
+
delete j.compilerOptions.paths;
|
|
463
465
|
}
|
|
466
|
+
fs.writeFileSync(p, `${JSON.stringify(j, null, 2)}\n`, 'utf8');
|
|
464
467
|
}
|
|
465
468
|
}
|
|
466
469
|
|
|
467
470
|
/**
|
|
468
|
-
* Vite
|
|
469
|
-
* `tokens.css` 같은 서브패스가 exports 를 타지 못해 깨짐 → alias 제거 후 npm 기본 해석.
|
|
471
|
+
* 구 템플릿 Vite 의 @xfilecom/front-core → 소스 디렉터리 alias 제거(npm 패키지·exports 사용).
|
|
470
472
|
*/
|
|
471
473
|
function patchWebViteFrontCoreAlias(targetRoot) {
|
|
472
474
|
for (const sub of ['client', 'admin']) {
|
package/defaults.json
CHANGED
package/package.json
CHANGED
|
@@ -5,7 +5,21 @@ import type { INestApplication } from '@nestjs/common';
|
|
|
5
5
|
import { AppModule } from './app.module';
|
|
6
6
|
import { appConfig } from './config.loader';
|
|
7
7
|
|
|
8
|
-
function
|
|
8
|
+
function parsePositivePort(v: string | number | undefined): number | undefined {
|
|
9
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
10
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `PORT`(syncEnvKeys) → `server.port`(YAML) → 기본값 */
|
|
14
|
+
function resolveListenPort(): number {
|
|
15
|
+
return (
|
|
16
|
+
parsePositivePort(process.env.PORT) ??
|
|
17
|
+
parsePositivePort(appConfig.server?.port) ??
|
|
18
|
+
3000
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function applyCorsFromConfig(app: INestApplication): void {
|
|
9
23
|
const cors = appConfig.cors;
|
|
10
24
|
if (cors?.enabled === false) {
|
|
11
25
|
return;
|
|
@@ -19,26 +33,52 @@ function applyCors(app: INestApplication): void {
|
|
|
19
33
|
app.enableCors({ origin: origin === true, credentials });
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
function
|
|
36
|
+
function applyHttpFromConfig(app: INestApplication): void {
|
|
23
37
|
const prefix = appConfig.http?.globalPrefix;
|
|
24
38
|
if (prefix != null && String(prefix).trim() !== '') {
|
|
25
39
|
app.setGlobalPrefix(String(prefix).replace(/^\/+|\/+$/g, ''));
|
|
26
40
|
}
|
|
27
41
|
}
|
|
28
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Nest 인스턴스에 직접 걸 수 있는 설정만 반영합니다.
|
|
45
|
+
* - DB 연결·JWT·인터셉터 등: `config.loader`의 syncEnvKeys + `AppModule`(CoreModule)
|
|
46
|
+
*/
|
|
47
|
+
function applyNestAppFromConfig(app: INestApplication): void {
|
|
48
|
+
applyCorsFromConfig(app);
|
|
49
|
+
applyHttpFromConfig(app);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function logStartupFromConfig(port: number): void {
|
|
53
|
+
const base = appConfig.http?.publicBaseUrl?.replace(/\/$/, '');
|
|
54
|
+
const pfx = appConfig.http?.globalPrefix
|
|
55
|
+
? `/${String(appConfig.http.globalPrefix).replace(/^\/+|\/+$/g, '')}`
|
|
56
|
+
: '';
|
|
57
|
+
const env =
|
|
58
|
+
process.env.APP_ENV ??
|
|
59
|
+
appConfig.app?.env ??
|
|
60
|
+
process.env.NODE_ENV ??
|
|
61
|
+
'development';
|
|
62
|
+
const dbOn = appConfig.core?.database?.auto === true;
|
|
63
|
+
const dbHost = appConfig.database?.host;
|
|
64
|
+
const dbName = appConfig.database?.name;
|
|
65
|
+
const jwtGuard = appConfig.core?.guards?.jwt === true;
|
|
66
|
+
const sqlEp = appConfig.core?.sqlEndpoint?.enabled === true;
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log(
|
|
69
|
+
`[api] listen :${port} env=${env}` +
|
|
70
|
+
(base ? ` public=${base}${pfx}` : '') +
|
|
71
|
+
` cors=${appConfig.cors?.enabled !== false ? 'on' : 'off'}` +
|
|
72
|
+
` db=${dbOn ? 'auto' : 'off'}${dbHost ? `@${dbHost}` : ''}${dbName ? `/${dbName}` : ''}` +
|
|
73
|
+
` jwtGuard=${jwtGuard} sqlEndpoint=${sqlEp}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
29
77
|
async function bootstrap() {
|
|
30
78
|
const app = await NestFactory.create(AppModule);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const port = Number(process.env.PORT) || 3000;
|
|
34
|
-
const base = appConfig.http?.publicBaseUrl?.replace(/\/$/, '');
|
|
79
|
+
applyNestAppFromConfig(app);
|
|
80
|
+
const port = resolveListenPort();
|
|
35
81
|
await app.listen(port);
|
|
36
|
-
|
|
37
|
-
const pfx = appConfig.http?.globalPrefix
|
|
38
|
-
? `/${String(appConfig.http.globalPrefix).replace(/^\/+|\/+$/g, '')}`
|
|
39
|
-
: '';
|
|
40
|
-
// eslint-disable-next-line no-console
|
|
41
|
-
console.log(`Listening ${base}${pfx} (port ${port})`);
|
|
42
|
-
}
|
|
82
|
+
logStartupFromConfig(port);
|
|
43
83
|
}
|
|
44
84
|
bootstrap();
|
|
@@ -49,7 +49,7 @@ export const healthEndpoint: EndpointDef = {
|
|
|
49
49
|
export const appMetaEndpoint: EndpointDef = {
|
|
50
50
|
method: 'GET',
|
|
51
51
|
path: '/app-meta',
|
|
52
|
-
|
|
52
|
+
/** 공통 `{ code, data, error }` 는 axios 인터셉터에서 `data` 로 치환 */
|
|
53
53
|
showIndicator: false,
|
|
54
54
|
timeoutMs: 10_000,
|
|
55
55
|
};
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
"paths": {
|
|
11
11
|
"__WEB_SHARED_WORKSPACE__": ["../shared/src/index.ts"],
|
|
12
12
|
"__WEB_SHARED_WORKSPACE__/*": ["../shared/src/*"],
|
|
13
|
-
"@shared/*": ["../../shared/*"]
|
|
14
|
-
"@xfilecom/front-core": ["../../../../front-core/src/index.ts"]
|
|
13
|
+
"@shared/*": ["../../shared/*"]
|
|
15
14
|
},
|
|
16
15
|
"allowImportingTsExtensions": true,
|
|
17
16
|
"resolveJsonModule": true,
|
|
@@ -7,12 +7,6 @@ import react from '@vitejs/plugin-react';
|
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* xframe 레포 안 템플릿: …/packages/xframe/packages/front-core/src
|
|
12
|
-
* (`npx @xfilecom/xframe <dir>` 생성 시 CLI 가 front-core alias·const 를 제거 → npm exports 로 해석)
|
|
13
|
-
*/
|
|
14
|
-
const frontCoreSrc = path.resolve(__dirname, '../../../..', 'front-core', 'src');
|
|
15
|
-
|
|
16
10
|
const webSharedSrc = path.resolve(__dirname, '../shared/src');
|
|
17
11
|
const rootSharedSrc = path.resolve(__dirname, '../../shared');
|
|
18
12
|
|
|
@@ -70,7 +64,6 @@ export default defineConfig(({ mode }) => {
|
|
|
70
64
|
plugins: [react()],
|
|
71
65
|
resolve: {
|
|
72
66
|
alias: {
|
|
73
|
-
'@xfilecom/front-core': frontCoreSrc,
|
|
74
67
|
'@shared': rootSharedSrc,
|
|
75
68
|
'__WEB_SHARED_WORKSPACE__': webSharedSrc,
|
|
76
69
|
},
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
"paths": {
|
|
11
11
|
"__WEB_SHARED_WORKSPACE__": ["../shared/src/index.ts"],
|
|
12
12
|
"__WEB_SHARED_WORKSPACE__/*": ["../shared/src/*"],
|
|
13
|
-
"@shared/*": ["../../shared/*"]
|
|
14
|
-
"@xfilecom/front-core": ["../../../../front-core/src/index.ts"]
|
|
13
|
+
"@shared/*": ["../../shared/*"]
|
|
15
14
|
},
|
|
16
15
|
"allowImportingTsExtensions": true,
|
|
17
16
|
"resolveJsonModule": true,
|
|
@@ -7,12 +7,6 @@ import react from '@vitejs/plugin-react';
|
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* xframe 레포 안 템플릿: …/packages/xframe/packages/front-core/src
|
|
12
|
-
* (`npx @xfilecom/xframe <dir>` 생성 시 CLI 가 front-core alias·const 를 제거 → npm exports 로 해석)
|
|
13
|
-
*/
|
|
14
|
-
const frontCoreSrc = path.resolve(__dirname, '../../../..', 'front-core', 'src');
|
|
15
|
-
|
|
16
10
|
/** file:../shared 복사본이 아닌 워크스페이스 web/shared/src (플레이스홀더 패키지 이름과 동일 키) */
|
|
17
11
|
const webSharedSrc = path.resolve(__dirname, '../shared/src');
|
|
18
12
|
|
|
@@ -73,7 +67,6 @@ export default defineConfig(({ mode }) => {
|
|
|
73
67
|
plugins: [react()],
|
|
74
68
|
resolve: {
|
|
75
69
|
alias: {
|
|
76
|
-
'@xfilecom/front-core': frontCoreSrc,
|
|
77
70
|
'@shared': rootSharedSrc,
|
|
78
71
|
'__WEB_SHARED_WORKSPACE__': webSharedSrc,
|
|
79
72
|
},
|
|
@@ -24,16 +24,15 @@ import { Shell } from '__WEB_SHARED_WORKSPACE__';
|
|
|
24
24
|
|
|
25
25
|
새 파일을 앱에서 가져오려면 `package.json`의 **`exports`**에 서브패스를 추가하세요.
|
|
26
26
|
|
|
27
|
-
## `@xfilecom/front-core
|
|
27
|
+
## `@xfilecom/front-core`
|
|
28
28
|
|
|
29
|
-
`web/client`·`web/admin
|
|
29
|
+
`web/client`·`web/admin`·`web/shared`는 **`package.json` 의존성 + `node_modules`** 로 해석합니다(`tsconfig`·Vite에 `paths`/alias로 소스 경로를 강제하지 않음). 서브패스(`tokens.css` 등)는 패키지 `exports`를 따릅니다.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
로컬에서 코어 소스를 쓰려면 **`npm link @xfilecom/front-core`** 또는 `package.json`의 `file:../front-core` / `workspace:` 등으로 **설치 위치**를 바꾸는 방식을 권장합니다.
|
|
32
32
|
|
|
33
33
|
### `web/shared/tsconfig.json`에는 `paths`로 front-core를 붙이지 마세요
|
|
34
34
|
|
|
35
|
-
`rootDir
|
|
36
|
-
`web/shared` 패키지 타입체크는 **`node_modules`의 `@xfilecom/front-core`**에 맡기고, 로컬 소스 번들은 위 **Vite alias**(client/admin)만 사용하면 됩니다.
|
|
35
|
+
`rootDir` 밖 소스를 끌어오면 `tsc`·에디터가 깨지기 쉽습니다. 타입은 **`node_modules/@xfilecom/front-core`**에 맡깁니다.
|
|
37
36
|
|
|
38
37
|
### `web/shared` CSS·TS가 수정해도 반영되지 않을 때
|
|
39
38
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Axios — baseURL, 로깅 인터셉터, 전역 훅(onRequestStart/End), Bearer
|
|
3
|
+
* HTTP 200/201 본문이 `{ code, data, error }` 이면 code 검사 후 data 로 치환 또는 토스트+reject
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import axios, {
|
|
@@ -8,25 +9,43 @@ import axios, {
|
|
|
8
9
|
type InternalAxiosRequestConfig,
|
|
9
10
|
} from 'axios';
|
|
10
11
|
import { sessionStore } from '../store/session-store';
|
|
12
|
+
import {
|
|
13
|
+
applyCommonResponseEnvelope,
|
|
14
|
+
CommonResponseRejectedError,
|
|
15
|
+
formatCommonErrorField,
|
|
16
|
+
isCommonResponsePayload,
|
|
17
|
+
} from './commonResponse';
|
|
11
18
|
|
|
12
19
|
const LOG_PREFIX = '[xframe/http]';
|
|
13
20
|
|
|
14
21
|
export type OnRequestStart = () => void;
|
|
15
22
|
export type OnRequestEnd = () => void;
|
|
16
23
|
export type OnError = (message: string, code?: string | number) => void;
|
|
24
|
+
export type OnCommonBusinessError = (message: string, appCode: number) => void;
|
|
17
25
|
|
|
18
26
|
let onRequestStart: OnRequestStart = () => {};
|
|
19
27
|
let onRequestEnd: OnRequestEnd = () => {};
|
|
20
28
|
let onError: OnError = () => {};
|
|
29
|
+
let onCommonBusinessError: OnCommonBusinessError = () => {};
|
|
30
|
+
|
|
31
|
+
export { CommonResponseRejectedError } from './commonResponse';
|
|
32
|
+
export {
|
|
33
|
+
COMMON_RESPONSE_SUCCESS_CODE,
|
|
34
|
+
formatCommonErrorField,
|
|
35
|
+
isCommonResponsePayload,
|
|
36
|
+
} from './commonResponse';
|
|
21
37
|
|
|
22
38
|
export function setApiHooks(hooks: {
|
|
23
39
|
onRequestStart?: OnRequestStart;
|
|
24
40
|
onRequestEnd?: OnRequestEnd;
|
|
25
41
|
onError?: OnError;
|
|
42
|
+
/** HTTP 정상(2xx)인데 body.code !== 0 일 때 (토스트 등) */
|
|
43
|
+
onCommonBusinessError?: OnCommonBusinessError;
|
|
26
44
|
}) {
|
|
27
45
|
if (hooks.onRequestStart) onRequestStart = hooks.onRequestStart;
|
|
28
46
|
if (hooks.onRequestEnd) onRequestEnd = hooks.onRequestEnd;
|
|
29
47
|
if (hooks.onError) onError = hooks.onError;
|
|
48
|
+
if (hooks.onCommonBusinessError) onCommonBusinessError = hooks.onCommonBusinessError;
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
export const apiClient = axios.create({
|
|
@@ -104,28 +123,37 @@ apiClient.interceptors.response.use(
|
|
|
104
123
|
const c = response.config as ExtConfig;
|
|
105
124
|
if (!c.skipGlobalIndicator) onRequestEnd();
|
|
106
125
|
logResponseSuccess(response);
|
|
126
|
+
try {
|
|
127
|
+
applyCommonResponseEnvelope(response, onCommonBusinessError);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return Promise.reject(e);
|
|
130
|
+
}
|
|
107
131
|
return response;
|
|
108
132
|
},
|
|
109
133
|
(err: AxiosError) => {
|
|
110
134
|
const c = err.config as ExtConfig | undefined;
|
|
111
135
|
if (!c?.skipGlobalIndicator) onRequestEnd();
|
|
112
136
|
logResponseError(err);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
const resData = err.response?.data;
|
|
138
|
+
let raw: unknown =
|
|
139
|
+
isCommonResponsePayload(resData) ? resData.error : undefined;
|
|
140
|
+
if (raw === undefined) {
|
|
141
|
+
raw =
|
|
142
|
+
(err.response?.data as { message?: unknown; error?: unknown } | undefined)?.message ??
|
|
143
|
+
(err.response?.data as { message?: unknown; error?: unknown } | undefined)?.error ??
|
|
144
|
+
(err as Error)?.message ??
|
|
145
|
+
'Request failed';
|
|
146
|
+
}
|
|
117
147
|
const message =
|
|
118
148
|
typeof raw === 'string'
|
|
119
149
|
? raw
|
|
120
150
|
: Array.isArray(raw)
|
|
121
151
|
? raw.map((x) => (typeof x === 'string' ? x : String(x))).join(', ')
|
|
122
|
-
:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
: JSON.stringify(raw);
|
|
128
|
-
const code = res?.status ?? (res?.data?.code as string | number | undefined);
|
|
152
|
+
: formatCommonErrorField(raw);
|
|
153
|
+
const code =
|
|
154
|
+
err.response?.status ??
|
|
155
|
+
(isCommonResponsePayload(resData) ? resData.code : undefined) ??
|
|
156
|
+
(err.response?.data as { code?: string | number } | undefined)?.code;
|
|
129
157
|
onError(message, code);
|
|
130
158
|
return Promise.reject(err);
|
|
131
159
|
},
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서버 공통 본문: HTTP 200/201 + body `{ code, data, error }`
|
|
3
|
+
* — 비즈니스 실패는 code !== 0 (HTTP는 정상).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AxiosResponse } from 'axios';
|
|
7
|
+
|
|
8
|
+
/** 백엔드와 동일한 “성공” 코드 */
|
|
9
|
+
export const COMMON_RESPONSE_SUCCESS_CODE = 0 as const;
|
|
10
|
+
|
|
11
|
+
export class CommonResponseRejectedError extends Error {
|
|
12
|
+
readonly appCode: number;
|
|
13
|
+
readonly payload: unknown;
|
|
14
|
+
|
|
15
|
+
constructor(message: string, appCode: number, payload: unknown) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'CommonResponseRejectedError';
|
|
18
|
+
this.appCode = appCode;
|
|
19
|
+
this.payload = payload;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isCommonResponsePayload(
|
|
24
|
+
body: unknown,
|
|
25
|
+
): body is { code: number; data: unknown; error?: unknown } {
|
|
26
|
+
if (body === null || typeof body !== 'object') return false;
|
|
27
|
+
const b = body as Record<string, unknown>;
|
|
28
|
+
return typeof b.code === 'number' && 'data' in b;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractMessageString(value: unknown): string | null {
|
|
32
|
+
if (value == null) return null;
|
|
33
|
+
if (typeof value === 'string') return value;
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
const first = value.find((x) => typeof x === 'string');
|
|
36
|
+
return first ?? (value.map((x) => (typeof x === 'string' ? x : String(x))).join(', ') || null);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'object' && 'message' in value) {
|
|
39
|
+
const inner = (value as { message: unknown }).message;
|
|
40
|
+
return extractMessageString(inner);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `error` 필드를 사용자용 문구로 */
|
|
46
|
+
export function formatCommonErrorField(error: unknown): string {
|
|
47
|
+
const extracted = extractMessageString(error);
|
|
48
|
+
if (extracted) return extracted;
|
|
49
|
+
if (typeof error === 'string') return error;
|
|
50
|
+
if (error != null && typeof error === 'object') {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.stringify(error);
|
|
53
|
+
} catch {
|
|
54
|
+
return 'Request failed';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 'Request failed';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type OnCommonBusinessError = (message: string, appCode: number) => void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 성공 인터셉터에서 호출: 공통 래핑이면 code 검사 후 data 로 치환하거나 reject.
|
|
64
|
+
*/
|
|
65
|
+
export function applyCommonResponseEnvelope(
|
|
66
|
+
response: AxiosResponse,
|
|
67
|
+
onBusinessError: OnCommonBusinessError,
|
|
68
|
+
): unknown {
|
|
69
|
+
const body = response.data;
|
|
70
|
+
if (!isCommonResponsePayload(body)) {
|
|
71
|
+
return response;
|
|
72
|
+
}
|
|
73
|
+
if (body.code !== COMMON_RESPONSE_SUCCESS_CODE) {
|
|
74
|
+
const msg = formatCommonErrorField(body.error);
|
|
75
|
+
onBusinessError(msg, body.code);
|
|
76
|
+
throw new CommonResponseRejectedError(msg, body.code, body);
|
|
77
|
+
}
|
|
78
|
+
response.data = body.data;
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HealthData } from '../../types/api';
|
|
2
2
|
import type { ApiMethodConfig, ApiMethodStore } from '../types';
|
|
3
3
|
import { healthEndpoint } from '@shared/endpoint';
|
|
4
4
|
|
|
5
5
|
/** GET /health — params·body 없음, 스토어는 validate/post 에서 사용 가능 */
|
|
6
|
-
export const healthMethod: ApiMethodConfig<void,
|
|
6
|
+
export const healthMethod: ApiMethodConfig<void, HealthData, ApiMethodStore> = {
|
|
7
7
|
schema: {},
|
|
8
8
|
endpoint: healthEndpoint,
|
|
9
9
|
ui: {
|
|
@@ -10,7 +10,16 @@ export {
|
|
|
10
10
|
useSessionStore,
|
|
11
11
|
} from './context/StoreProvider';
|
|
12
12
|
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
configureApi,
|
|
15
|
+
getApiBaseUrl,
|
|
16
|
+
setApiHooks,
|
|
17
|
+
apiClient,
|
|
18
|
+
CommonResponseRejectedError,
|
|
19
|
+
COMMON_RESPONSE_SUCCESS_CODE,
|
|
20
|
+
formatCommonErrorField,
|
|
21
|
+
isCommonResponsePayload,
|
|
22
|
+
} from './api/client';
|
|
14
23
|
export { sendRequest, type RequestTransportOptions } from './api/request';
|
|
15
24
|
export type { EndpointDef, HttpMethod } from '@shared/endpoint';
|
|
16
25
|
export { healthEndpoint, appMetaEndpoint, unwrapResponseData } from '@shared/endpoint';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { sendRequest } from '../api/request';
|
|
2
2
|
import { healthEndpoint } from '@shared/endpoint';
|
|
3
|
-
import type {
|
|
3
|
+
import type { HealthData } from '../types/api';
|
|
4
4
|
|
|
5
5
|
/** 직접 호출용 (토스트 없음) — UI 훅은 보통 이 함수 사용 */
|
|
6
|
-
export async function fetchHealth(): Promise<
|
|
7
|
-
return sendRequest<unknown,
|
|
6
|
+
export async function fetchHealth(): Promise<HealthData> {
|
|
7
|
+
return sendRequest<unknown, HealthData>(healthEndpoint, {});
|
|
8
8
|
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { makeAutoObservable, runInAction } from 'mobx';
|
|
6
6
|
import { setApiHooks } from '../api/client';
|
|
7
|
+
import {
|
|
8
|
+
CommonResponseRejectedError,
|
|
9
|
+
formatCommonErrorField,
|
|
10
|
+
isCommonResponsePayload,
|
|
11
|
+
} from '../api/commonResponse';
|
|
7
12
|
import { sendRequest } from '../api/request';
|
|
8
13
|
import type {
|
|
9
14
|
ApiMethodConfig,
|
|
@@ -53,8 +58,14 @@ function extractMessageString(value: unknown): string | null {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function getErrorMessage(e: unknown): string {
|
|
56
|
-
|
|
57
|
-
const
|
|
61
|
+
if (e instanceof CommonResponseRejectedError) return e.message;
|
|
62
|
+
const err = e as { response?: { data?: unknown }; message?: string };
|
|
63
|
+
const resData = err?.response?.data;
|
|
64
|
+
if (isCommonResponsePayload(resData)) {
|
|
65
|
+
return formatCommonErrorField(resData.error);
|
|
66
|
+
}
|
|
67
|
+
const d = resData as { message?: unknown; error?: unknown } | undefined;
|
|
68
|
+
const raw = d?.message ?? d?.error ?? err?.message;
|
|
58
69
|
const extracted = extractMessageString(raw);
|
|
59
70
|
if (extracted) return extracted;
|
|
60
71
|
if (raw != null && typeof raw === 'string') return raw;
|
|
@@ -94,6 +105,9 @@ export class RootStore {
|
|
|
94
105
|
runInAction(() => this._endRequest());
|
|
95
106
|
},
|
|
96
107
|
onError: () => {},
|
|
108
|
+
onCommonBusinessError: (message) => {
|
|
109
|
+
runInAction(() => this.addToast(message, 'error'));
|
|
110
|
+
},
|
|
97
111
|
});
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -225,7 +239,7 @@ export class RootStore {
|
|
|
225
239
|
}
|
|
226
240
|
return result;
|
|
227
241
|
} catch (e) {
|
|
228
|
-
if (showErrorToast) {
|
|
242
|
+
if (showErrorToast && !(e instanceof CommonResponseRejectedError)) {
|
|
229
243
|
runInAction(() => this.addToast(getErrorMessage(e), 'error'));
|
|
230
244
|
}
|
|
231
245
|
throw e;
|
|
@@ -344,7 +358,9 @@ export class RootStore {
|
|
|
344
358
|
return data;
|
|
345
359
|
} catch (e) {
|
|
346
360
|
runInAction(() => {
|
|
347
|
-
if (showErrorToast
|
|
361
|
+
if (showErrorToast && !(e instanceof CommonResponseRejectedError)) {
|
|
362
|
+
this.addToast(getErrorMessage(e), 'error');
|
|
363
|
+
}
|
|
348
364
|
});
|
|
349
365
|
throw e;
|
|
350
366
|
} finally {
|