sh-ui-cli 0.31.1 → 0.32.1
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/README.md +1 -1
- package/data/changelog/versions.json +26 -0
- package/package.json +1 -1
- package/src/create/cli-args.js +1 -1
- package/src/create/generator.js +55 -1
- package/src/create/index.mjs +2 -2
- package/src/create/plugins/authJwt.js +340 -0
- package/src/create/plugins/index.js +2 -1
- package/src/create/plugins/sentry.js +32 -280
- package/src/mcp.mjs +1 -1
- package/templates/nextjs-app/app/api/proxy/[...path]/route.ts +31 -4
- package/templates/nextjs-app/package.json +0 -1
- package/templates/nextjs-app/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
- package/templates/nextjs-app/src/shared/api/clientFetch.ts +40 -0
- package/templates/nextjs-app/src/shared/api/http.ts +13 -56
- package/templates/nextjs-app/src/shared/api/observability.ts +20 -0
- package/templates/nextjs-app/src/shared/api/queryClient.ts +30 -0
- package/templates/nextjs-app/src/shared/api/serverFetch.ts +59 -0
- package/templates/nextjs-app/src/shared/hooks/useAppMutation.ts +52 -0
- package/templates/nextjs-standalone/app/api/proxy/[...path]/route.ts +31 -4
- package/templates/nextjs-standalone/package.json +0 -1
- package/templates/nextjs-standalone/src/app/providers/tanstack/QueryClientProvider.tsx +5 -18
- package/templates/nextjs-standalone/src/shared/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/src/shared/api/http.ts +13 -56
- package/templates/nextjs-standalone/src/shared/api/observability.ts +20 -0
- package/templates/nextjs-standalone/src/shared/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/src/shared/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/src/shared/hooks/useAppMutation.ts +52 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
|
|
3
|
+
import type { ApiResponse } from './apiTypes';
|
|
4
|
+
import { ApiError } from './error';
|
|
5
|
+
import { captureApiError, logApiError } from './observability';
|
|
6
|
+
|
|
7
|
+
const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
|
|
8
|
+
|
|
9
|
+
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
10
|
+
const LOCALE_COOKIE = 'NEXT_LOCALE';
|
|
11
|
+
|
|
12
|
+
export async function serverFetch<T>(
|
|
13
|
+
path: string,
|
|
14
|
+
init: RequestInit = {},
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
const jar = await cookies();
|
|
17
|
+
const accessToken = jar.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
18
|
+
const locale = jar.get(LOCALE_COOKIE)?.value;
|
|
19
|
+
|
|
20
|
+
const url = `${API_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
|
21
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
22
|
+
|
|
23
|
+
const headers: Record<string, string> = {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
|
|
26
|
+
...(locale && { 'Accept-Language': locale }),
|
|
27
|
+
...(init.headers as Record<string, string> | undefined),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url, {
|
|
31
|
+
...init,
|
|
32
|
+
headers,
|
|
33
|
+
cache: init.cache ?? 'no-store',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const body = (await res.json()) as ApiResponse<T>;
|
|
37
|
+
|
|
38
|
+
if (!res.ok || body.result === 'ERROR') {
|
|
39
|
+
logApiError('SERVER_FETCH', {
|
|
40
|
+
url,
|
|
41
|
+
method,
|
|
42
|
+
status: res.status,
|
|
43
|
+
requestBody: typeof init.body === 'string' ? init.body : undefined,
|
|
44
|
+
responseBody: body,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
captureApiError({
|
|
48
|
+
url,
|
|
49
|
+
apiPath: path,
|
|
50
|
+
method,
|
|
51
|
+
status: res.status,
|
|
52
|
+
responseBody: body,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
throw new ApiError(res.status, body.error?.code ?? '', body.error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return body.data;
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMutation,
|
|
3
|
+
type UseMutationOptions,
|
|
4
|
+
type DefaultError,
|
|
5
|
+
} from '@tanstack/react-query';
|
|
6
|
+
import { toast } from 'sonner';
|
|
7
|
+
|
|
8
|
+
import { ApiError } from '../api/error';
|
|
9
|
+
|
|
10
|
+
type AppMutationOptions<
|
|
11
|
+
TData = unknown,
|
|
12
|
+
TError = DefaultError,
|
|
13
|
+
TVariables = void,
|
|
14
|
+
TContext = unknown,
|
|
15
|
+
> = UseMutationOptions<TData, TError, TVariables, TContext> & {
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
showErrorToast?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* useMutation 래퍼 — 에러 발생 시 ApiError.message 를 toast 로 띄운다.
|
|
22
|
+
* showErrorToast: false 로 끌 수 있고, errorMessage 로 fallback 메시지 지정.
|
|
23
|
+
*/
|
|
24
|
+
export const useAppMutation = <
|
|
25
|
+
TData = unknown,
|
|
26
|
+
TError = DefaultError,
|
|
27
|
+
TVariables = void,
|
|
28
|
+
TContext = unknown,
|
|
29
|
+
>(
|
|
30
|
+
options: AppMutationOptions<TData, TError, TVariables, TContext>,
|
|
31
|
+
) => {
|
|
32
|
+
const { errorMessage, showErrorToast = true, onError, ...rest } = options;
|
|
33
|
+
|
|
34
|
+
return useMutation({
|
|
35
|
+
...rest,
|
|
36
|
+
onError: (...args) => {
|
|
37
|
+
onError?.(...args);
|
|
38
|
+
|
|
39
|
+
if (!showErrorToast) return;
|
|
40
|
+
|
|
41
|
+
const [error] = args;
|
|
42
|
+
const message =
|
|
43
|
+
error instanceof ApiError
|
|
44
|
+
? (error.data?.message ?? errorMessage)
|
|
45
|
+
: errorMessage;
|
|
46
|
+
|
|
47
|
+
if (message) {
|
|
48
|
+
toast.error(message);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { cookies } from 'next/headers';
|
|
2
2
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
captureApiError,
|
|
6
|
+
logApiError,
|
|
7
|
+
} from '@/src/shared/api/observability';
|
|
8
|
+
|
|
4
9
|
const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
|
|
5
10
|
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
6
11
|
const LOCALE_COOKIE = 'NEXT_LOCALE';
|
|
@@ -11,7 +16,8 @@ const proxyRequest = async (
|
|
|
11
16
|
method: string,
|
|
12
17
|
) => {
|
|
13
18
|
const { path } = await ctx.params;
|
|
14
|
-
const
|
|
19
|
+
const apiPath = path.join('/');
|
|
20
|
+
const url = new URL(`${API_URL}/${apiPath}`);
|
|
15
21
|
|
|
16
22
|
request.nextUrl.searchParams.forEach((value, key) => {
|
|
17
23
|
url.searchParams.set(key, value);
|
|
@@ -39,10 +45,9 @@ const proxyRequest = async (
|
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
let response: Response;
|
|
42
49
|
try {
|
|
43
|
-
|
|
44
|
-
const data = await response.json();
|
|
45
|
-
return NextResponse.json(data, { status: response.status });
|
|
50
|
+
response = await fetch(url.toString(), { method, headers, body });
|
|
46
51
|
} catch (error) {
|
|
47
52
|
console.error(`[PROXY] ${method} ${url.toString()} —`, error);
|
|
48
53
|
return NextResponse.json(
|
|
@@ -57,6 +62,28 @@ const proxyRequest = async (
|
|
|
57
62
|
{ status: 502 },
|
|
58
63
|
);
|
|
59
64
|
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
logApiError('PROXY', {
|
|
70
|
+
url: url.toString(),
|
|
71
|
+
method,
|
|
72
|
+
status: response.status,
|
|
73
|
+
requestBody: typeof body === 'string' ? body : undefined,
|
|
74
|
+
responseBody: data,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
captureApiError({
|
|
78
|
+
url: url.toString(),
|
|
79
|
+
apiPath,
|
|
80
|
+
method,
|
|
81
|
+
status: response.status,
|
|
82
|
+
responseBody: data,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return NextResponse.json(data, { status: response.status });
|
|
60
87
|
};
|
|
61
88
|
|
|
62
89
|
export const GET = (
|
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
QueryClientProvider as TanstackQueryClientProvider,
|
|
6
|
-
} from '@tanstack/react-query';
|
|
7
|
-
import { useState, type ReactNode } from 'react';
|
|
3
|
+
import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
const [queryClient] = useState(
|
|
11
|
-
() =>
|
|
12
|
-
new QueryClient({
|
|
13
|
-
defaultOptions: {
|
|
14
|
-
queries: {
|
|
15
|
-
staleTime: 60 * 1000,
|
|
16
|
-
retry: 1,
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
}),
|
|
20
|
-
);
|
|
6
|
+
import { getBrowserQueryClient } from '@/src/shared/api/queryClient';
|
|
21
7
|
|
|
8
|
+
export function QueryClientProvider({ children }: { children: ReactNode }) {
|
|
22
9
|
return (
|
|
23
|
-
<TanstackQueryClientProvider client={
|
|
10
|
+
<TanstackQueryClientProvider client={getBrowserQueryClient()}>
|
|
24
11
|
{children}
|
|
25
12
|
</TanstackQueryClientProvider>
|
|
26
13
|
);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ApiResponse } from './apiTypes';
|
|
4
|
+
import { ApiError } from './error';
|
|
5
|
+
|
|
6
|
+
const PROXY_BASE = '/api/proxy';
|
|
7
|
+
|
|
8
|
+
export async function clientFetch<T>(
|
|
9
|
+
path: string,
|
|
10
|
+
init: RequestInit = {},
|
|
11
|
+
): Promise<T> {
|
|
12
|
+
const url = `${PROXY_BASE}${path.startsWith('/') ? path : `/${path}`}`;
|
|
13
|
+
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
...init,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
...(init.headers as Record<string, string> | undefined),
|
|
19
|
+
},
|
|
20
|
+
credentials: 'include',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const body = (await res.json()) as ApiResponse<T>;
|
|
24
|
+
|
|
25
|
+
if (res.status === 401) {
|
|
26
|
+
if (
|
|
27
|
+
typeof window !== 'undefined' &&
|
|
28
|
+
!window.location.pathname.startsWith('/sign-in')
|
|
29
|
+
) {
|
|
30
|
+
window.location.href = '/sign-in';
|
|
31
|
+
}
|
|
32
|
+
throw new ApiError(401, body.error?.code ?? 'UNAUTHORIZED', body.error);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!res.ok || body.result === 'ERROR') {
|
|
36
|
+
throw new ApiError(res.status, body.error?.code ?? '', body.error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return body.data;
|
|
40
|
+
}
|
|
@@ -1,56 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// 호스트만 프리픽스하고 그 외 분기는 두지 않는다.
|
|
15
|
-
http.interceptors.request.use(async (config) => {
|
|
16
|
-
if (typeof window !== 'undefined') return config;
|
|
17
|
-
|
|
18
|
-
const { headers: getHeaders } = await import('next/headers');
|
|
19
|
-
const hdrs = await getHeaders();
|
|
20
|
-
const host = hdrs.get('host') ?? 'localhost:3000';
|
|
21
|
-
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
|
22
|
-
config.baseURL = `${protocol}://${host}/api/proxy`;
|
|
23
|
-
return config;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
http.interceptors.response.use(
|
|
27
|
-
(response) => {
|
|
28
|
-
const body = response.data as ApiResponse;
|
|
29
|
-
if (body && typeof body === 'object' && 'result' in body) {
|
|
30
|
-
if (body.result === 'ERROR') {
|
|
31
|
-
return Promise.reject(
|
|
32
|
-
new ApiError(response.status, body.error?.code ?? '', body.error),
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
response.data = body.data;
|
|
36
|
-
}
|
|
37
|
-
return response;
|
|
38
|
-
},
|
|
39
|
-
(error) => {
|
|
40
|
-
if (axios.isAxiosError(error)) {
|
|
41
|
-
const { response } = error;
|
|
42
|
-
const body = response?.data as ApiResponse | undefined;
|
|
43
|
-
const errorBody = body?.error ?? null;
|
|
44
|
-
return Promise.reject(
|
|
45
|
-
new ApiError(
|
|
46
|
-
response?.status ?? 0,
|
|
47
|
-
errorBody?.code ?? '',
|
|
48
|
-
errorBody,
|
|
49
|
-
),
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
return Promise.reject(error);
|
|
53
|
-
},
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
export { http };
|
|
1
|
+
import { clientFetch } from './clientFetch';
|
|
2
|
+
import { serverFetch } from './serverFetch';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* isomorphic 진입점.
|
|
6
|
+
* RSC/서버에서는 백엔드로 직통, 브라우저에서는 /api/proxy 경유.
|
|
7
|
+
* API 함수는 한 번만 작성하고 환경 분기는 여기서 처리한다.
|
|
8
|
+
*/
|
|
9
|
+
export function http<T>(path: string, init?: RequestInit): Promise<T> {
|
|
10
|
+
return typeof window === 'undefined'
|
|
11
|
+
? serverFetch<T>(path, init)
|
|
12
|
+
: clientFetch<T>(path, init);
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type ApiCaptureParams = {
|
|
2
|
+
url: string;
|
|
3
|
+
apiPath: string;
|
|
4
|
+
method: string;
|
|
5
|
+
status: number | undefined;
|
|
6
|
+
responseBody?: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ApiLogParams = {
|
|
10
|
+
url: string;
|
|
11
|
+
method: string;
|
|
12
|
+
status: number | undefined;
|
|
13
|
+
requestHeaders?: Record<string, string | undefined>;
|
|
14
|
+
requestBody?: unknown;
|
|
15
|
+
responseBody?: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const captureApiError = (_params: ApiCaptureParams): void => {};
|
|
19
|
+
|
|
20
|
+
export const logApiError = (_prefix: string, _params: ApiLogParams): void => {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QueryClient,
|
|
3
|
+
defaultShouldDehydrateQuery,
|
|
4
|
+
isServer,
|
|
5
|
+
} from '@tanstack/react-query';
|
|
6
|
+
import { cache } from 'react';
|
|
7
|
+
|
|
8
|
+
function makeQueryClient(): QueryClient {
|
|
9
|
+
return new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
staleTime: 60 * 1000,
|
|
13
|
+
retry: 1,
|
|
14
|
+
},
|
|
15
|
+
dehydrate: {
|
|
16
|
+
shouldDehydrateQuery: (q) =>
|
|
17
|
+
defaultShouldDehydrateQuery(q) || q.state.status === 'pending',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getServerQueryClient = cache(makeQueryClient);
|
|
24
|
+
|
|
25
|
+
let browserQueryClient: QueryClient | undefined;
|
|
26
|
+
|
|
27
|
+
export function getBrowserQueryClient(): QueryClient {
|
|
28
|
+
if (isServer) return makeQueryClient();
|
|
29
|
+
return (browserQueryClient ??= makeQueryClient());
|
|
30
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
|
|
3
|
+
import type { ApiResponse } from './apiTypes';
|
|
4
|
+
import { ApiError } from './error';
|
|
5
|
+
import { captureApiError, logApiError } from './observability';
|
|
6
|
+
|
|
7
|
+
const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
|
|
8
|
+
|
|
9
|
+
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
10
|
+
const LOCALE_COOKIE = 'NEXT_LOCALE';
|
|
11
|
+
|
|
12
|
+
export async function serverFetch<T>(
|
|
13
|
+
path: string,
|
|
14
|
+
init: RequestInit = {},
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
const jar = await cookies();
|
|
17
|
+
const accessToken = jar.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
18
|
+
const locale = jar.get(LOCALE_COOKIE)?.value;
|
|
19
|
+
|
|
20
|
+
const url = `${API_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
|
21
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
22
|
+
|
|
23
|
+
const headers: Record<string, string> = {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
|
|
26
|
+
...(locale && { 'Accept-Language': locale }),
|
|
27
|
+
...(init.headers as Record<string, string> | undefined),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url, {
|
|
31
|
+
...init,
|
|
32
|
+
headers,
|
|
33
|
+
cache: init.cache ?? 'no-store',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const body = (await res.json()) as ApiResponse<T>;
|
|
37
|
+
|
|
38
|
+
if (!res.ok || body.result === 'ERROR') {
|
|
39
|
+
logApiError('SERVER_FETCH', {
|
|
40
|
+
url,
|
|
41
|
+
method,
|
|
42
|
+
status: res.status,
|
|
43
|
+
requestBody: typeof init.body === 'string' ? init.body : undefined,
|
|
44
|
+
responseBody: body,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
captureApiError({
|
|
48
|
+
url,
|
|
49
|
+
apiPath: path,
|
|
50
|
+
method,
|
|
51
|
+
status: res.status,
|
|
52
|
+
responseBody: body,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
throw new ApiError(res.status, body.error?.code ?? '', body.error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return body.data;
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMutation,
|
|
3
|
+
type UseMutationOptions,
|
|
4
|
+
type DefaultError,
|
|
5
|
+
} from '@tanstack/react-query';
|
|
6
|
+
import { toast } from 'sonner';
|
|
7
|
+
|
|
8
|
+
import { ApiError } from '../api/error';
|
|
9
|
+
|
|
10
|
+
type AppMutationOptions<
|
|
11
|
+
TData = unknown,
|
|
12
|
+
TError = DefaultError,
|
|
13
|
+
TVariables = void,
|
|
14
|
+
TContext = unknown,
|
|
15
|
+
> = UseMutationOptions<TData, TError, TVariables, TContext> & {
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
showErrorToast?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* useMutation 래퍼 — 에러 발생 시 ApiError.message 를 toast 로 띄운다.
|
|
22
|
+
* showErrorToast: false 로 끌 수 있고, errorMessage 로 fallback 메시지 지정.
|
|
23
|
+
*/
|
|
24
|
+
export const useAppMutation = <
|
|
25
|
+
TData = unknown,
|
|
26
|
+
TError = DefaultError,
|
|
27
|
+
TVariables = void,
|
|
28
|
+
TContext = unknown,
|
|
29
|
+
>(
|
|
30
|
+
options: AppMutationOptions<TData, TError, TVariables, TContext>,
|
|
31
|
+
) => {
|
|
32
|
+
const { errorMessage, showErrorToast = true, onError, ...rest } = options;
|
|
33
|
+
|
|
34
|
+
return useMutation({
|
|
35
|
+
...rest,
|
|
36
|
+
onError: (...args) => {
|
|
37
|
+
onError?.(...args);
|
|
38
|
+
|
|
39
|
+
if (!showErrorToast) return;
|
|
40
|
+
|
|
41
|
+
const [error] = args;
|
|
42
|
+
const message =
|
|
43
|
+
error instanceof ApiError
|
|
44
|
+
? (error.data?.message ?? errorMessage)
|
|
45
|
+
: errorMessage;
|
|
46
|
+
|
|
47
|
+
if (message) {
|
|
48
|
+
toast.error(message);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
};
|