sh-ui-cli 0.33.0 → 0.34.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/data/changelog/versions.json +14 -0
- package/package.json +1 -1
- package/templates/nextjs-app/package.json +2 -0
- package/templates/nextjs-app/src/shared/lib/formatDate.ts +22 -0
- package/templates/nextjs-app/src/shared/lib/formatPrice.ts +10 -0
- package/templates/nextjs-app/src/shared/lib/getQueryClient.ts +14 -0
- package/templates/nextjs-app/src/shared/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-app/src/shared/test/index.ts +2 -0
- package/templates/nextjs-app/src/shared/test/renderWithProviders.tsx +40 -0
- package/templates/nextjs-app/src/shared/ui/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-app/src/shared/ui/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-standalone/package.json +2 -0
- package/templates/nextjs-standalone/src/shared/lib/formatDate.ts +22 -0
- package/templates/nextjs-standalone/src/shared/lib/formatPrice.ts +10 -0
- package/templates/nextjs-standalone/src/shared/lib/getQueryClient.ts +14 -0
- package/templates/nextjs-standalone/src/shared/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/src/shared/test/index.ts +2 -0
- package/templates/nextjs-standalone/src/shared/test/renderWithProviders.tsx +40 -0
- package/templates/nextjs-standalone/src/shared/ui/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/src/shared/ui/PrefetchBoundary/index.tsx +35 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.34.0",
|
|
7
|
+
"date": "2026-04-29",
|
|
8
|
+
"title": "Next 템플릿 — RSC prefetch / 에러 바운더리 / 테스트 헬퍼 + KRW·날짜 포맷 유틸 기본 탑재",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"shared/ui/PrefetchBoundary + shared/lib/getQueryClient — RSC 에서 prefetch 후 dehydrate → 클라이언트 hydrate. 단일/배열 fetchOptions 모두 지원, 이미 깔린 TanStack Query 인프라와 즉시 호환",
|
|
12
|
+
"shared/ui/FallbackBoundary — Suspense + ErrorBoundary 합성, QueryErrorResetBoundary 와 연동돼 errorFallback 의 resetErrorBoundary 가 쿼리까지 함께 리셋. onError 콜백으로 외부 옵저버빌리티 (Sentry 등) 훅인 자유",
|
|
13
|
+
"shared/test/renderWithProviders + createTestQueryClient — RTL render + QueryClientProvider + userEvent 한 번에. 기존 vitest 인프라에 컴포넌트 테스트 진입장벽 제거",
|
|
14
|
+
"shared/lib/formatDate / formatPrice — 로케일 기반 Intl 포맷 유틸. formatPrice 는 한국 원화(KRW, 소수점 없음) 기본",
|
|
15
|
+
"@testing-library/react + user-event devDeps 추가 (양쪽 nextjs 템플릿)"
|
|
16
|
+
],
|
|
17
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.34.0"
|
|
18
|
+
},
|
|
5
19
|
{
|
|
6
20
|
"version": "0.33.0",
|
|
7
21
|
"date": "2026-04-29",
|
package/package.json
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"@tailwindcss/postcss": "^4.1.18",
|
|
31
31
|
"@tanstack/react-query-devtools": "^5.91.3",
|
|
32
32
|
"@testing-library/jest-dom": "^6.9.1",
|
|
33
|
+
"@testing-library/react": "^16",
|
|
34
|
+
"@testing-library/user-event": "^14",
|
|
33
35
|
"@types/node": "^25.1.0",
|
|
34
36
|
"@types/react": "^19.2.10",
|
|
35
37
|
"@types/react-dom": "^19.2.3",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date → 로케일 기반 날짜 포맷 (시간 없음).
|
|
3
|
+
*/
|
|
4
|
+
export const formatDate = (date: Date, locale = 'ko-KR'): string =>
|
|
5
|
+
new Intl.DateTimeFormat(locale, {
|
|
6
|
+
year: 'numeric',
|
|
7
|
+
month: '2-digit',
|
|
8
|
+
day: '2-digit',
|
|
9
|
+
}).format(date);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Date → 로케일 기반 날짜 + 시간 포맷 (24h).
|
|
13
|
+
*/
|
|
14
|
+
export const formatDateTime = (date: Date, locale = 'ko-KR'): string =>
|
|
15
|
+
new Intl.DateTimeFormat(locale, {
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
month: '2-digit',
|
|
18
|
+
day: '2-digit',
|
|
19
|
+
hour: '2-digit',
|
|
20
|
+
minute: '2-digit',
|
|
21
|
+
hour12: false,
|
|
22
|
+
}).format(date);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 숫자 → 한국 원화(KRW) 포맷. 소수점 없음.
|
|
3
|
+
* 예: 12000 → "₩12,000"
|
|
4
|
+
*/
|
|
5
|
+
export const formatPrice = (amount: number, locale = 'ko-KR'): string =>
|
|
6
|
+
new Intl.NumberFormat(locale, {
|
|
7
|
+
style: 'currency',
|
|
8
|
+
currency: 'KRW',
|
|
9
|
+
maximumFractionDigits: 0,
|
|
10
|
+
}).format(amount);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isServer } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getBrowserQueryClient,
|
|
5
|
+
getServerQueryClient,
|
|
6
|
+
} from '@/src/shared/api/queryClient';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* RSC 와 클라이언트 양쪽에서 안전하게 호출 가능한 QueryClient 핸들.
|
|
10
|
+
* 서버에서는 React `cache()` 로 요청 단위 싱글턴, 브라우저에서는 모듈 싱글턴.
|
|
11
|
+
*/
|
|
12
|
+
export default function getQueryClient() {
|
|
13
|
+
return isServer ? getServerQueryClient() : getBrowserQueryClient();
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 테스트 전용 QueryClient — retry/refetch 끄고 gcTime 0 으로 격리.
|
|
5
|
+
*/
|
|
6
|
+
export const createTestQueryClient = (): QueryClient =>
|
|
7
|
+
new QueryClient({
|
|
8
|
+
defaultOptions: {
|
|
9
|
+
queries: {
|
|
10
|
+
retry: false,
|
|
11
|
+
refetchOnWindowFocus: false,
|
|
12
|
+
gcTime: 0,
|
|
13
|
+
},
|
|
14
|
+
mutations: {
|
|
15
|
+
retry: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
type RenderOptions,
|
|
6
|
+
type RenderResult,
|
|
7
|
+
} from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
10
|
+
|
|
11
|
+
import { createTestQueryClient } from './createTestQueryClient';
|
|
12
|
+
|
|
13
|
+
type Options = Omit<RenderOptions, 'wrapper'> & {
|
|
14
|
+
queryClient?: QueryClient;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Result = RenderResult & {
|
|
18
|
+
user: ReturnType<typeof userEvent.setup>;
|
|
19
|
+
queryClient: QueryClient;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* RTL render + QueryClientProvider + userEvent setup 한 번에.
|
|
24
|
+
* 추가 Provider 가 필요하면 이 파일을 직접 수정하거나 wrapper 옵션을 쓴다.
|
|
25
|
+
*/
|
|
26
|
+
export const renderWithProviders = (
|
|
27
|
+
ui: ReactElement,
|
|
28
|
+
options: Options = {},
|
|
29
|
+
): Result => {
|
|
30
|
+
const { queryClient = createTestQueryClient(), ...rtlOptions } = options;
|
|
31
|
+
|
|
32
|
+
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
33
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const result = render(ui, { wrapper: Wrapper, ...rtlOptions });
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
|
|
39
|
+
return { ...result, user, queryClient };
|
|
40
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Component,
|
|
5
|
+
type ComponentType,
|
|
6
|
+
type ErrorInfo,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
Suspense,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { QueryErrorResetBoundary } from '@tanstack/react-query';
|
|
11
|
+
|
|
12
|
+
export type ErrorFallbackProps = {
|
|
13
|
+
error: Error | null;
|
|
14
|
+
resetErrorBoundary: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ErrorBoundaryProps = {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
fallback?: ComponentType<ErrorFallbackProps>;
|
|
20
|
+
onReset: () => void;
|
|
21
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ErrorBoundaryState = {
|
|
25
|
+
hasError: boolean;
|
|
26
|
+
error: Error | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
30
|
+
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
31
|
+
|
|
32
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
33
|
+
return { hasError: true, error };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
37
|
+
this.props.onError?.(error, info);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
resetErrorBoundary = () => {
|
|
41
|
+
this.props.onReset();
|
|
42
|
+
this.setState({ hasError: false, error: null });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
render() {
|
|
46
|
+
const { hasError, error } = this.state;
|
|
47
|
+
const { children, fallback: Fallback } = this.props;
|
|
48
|
+
|
|
49
|
+
if (hasError && Fallback) {
|
|
50
|
+
return (
|
|
51
|
+
<Fallback error={error} resetErrorBoundary={this.resetErrorBoundary} />
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return children;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type FallbackBoundaryProps = {
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
errorFallback?: ComponentType<ErrorFallbackProps>;
|
|
62
|
+
suspenseFallback?: ReactNode;
|
|
63
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Suspense + ErrorBoundary 합성. React Query 의 reset 신호에 맞춰
|
|
68
|
+
* `errorFallback` 의 `resetErrorBoundary` 가 쿼리까지 함께 리셋한다.
|
|
69
|
+
*/
|
|
70
|
+
export function FallbackBoundary({
|
|
71
|
+
children,
|
|
72
|
+
errorFallback,
|
|
73
|
+
suspenseFallback,
|
|
74
|
+
onError,
|
|
75
|
+
}: FallbackBoundaryProps) {
|
|
76
|
+
return (
|
|
77
|
+
<QueryErrorResetBoundary>
|
|
78
|
+
{({ reset }) => (
|
|
79
|
+
<ErrorBoundary
|
|
80
|
+
onReset={reset}
|
|
81
|
+
fallback={errorFallback}
|
|
82
|
+
onError={onError}
|
|
83
|
+
>
|
|
84
|
+
<Suspense fallback={suspenseFallback}>{children}</Suspense>
|
|
85
|
+
</ErrorBoundary>
|
|
86
|
+
)}
|
|
87
|
+
</QueryErrorResetBoundary>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
dehydrate,
|
|
5
|
+
type FetchQueryOptions,
|
|
6
|
+
HydrationBoundary,
|
|
7
|
+
} from '@tanstack/react-query';
|
|
8
|
+
|
|
9
|
+
import getQueryClient from '@/src/shared/lib/getQueryClient';
|
|
10
|
+
|
|
11
|
+
export type FetchOptions = Pick<FetchQueryOptions, 'queryKey' | 'queryFn'>;
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
fetchOptions?: FetchOptions[] | FetchOptions | null;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RSC 에서 prefetch 를 끝낸 뒤 dehydrated state 로 클라이언트에 hydrate.
|
|
20
|
+
* 단일/배열 둘 다 받는다.
|
|
21
|
+
*/
|
|
22
|
+
export async function PrefetchBoundary({ fetchOptions, children }: Props) {
|
|
23
|
+
const queryClient = getQueryClient();
|
|
24
|
+
|
|
25
|
+
if (fetchOptions) {
|
|
26
|
+
const list = Array.isArray(fetchOptions) ? fetchOptions : [fetchOptions];
|
|
27
|
+
await Promise.all(list.map((opt) => queryClient.prefetchQuery(opt)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
32
|
+
{children}
|
|
33
|
+
</HydrationBoundary>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"@tailwindcss/postcss": "^4.1.18",
|
|
36
36
|
"@tanstack/react-query-devtools": "^5.91.3",
|
|
37
37
|
"@testing-library/jest-dom": "^6.9.1",
|
|
38
|
+
"@testing-library/react": "^16",
|
|
39
|
+
"@testing-library/user-event": "^14",
|
|
38
40
|
"@types/node": "^25.1.0",
|
|
39
41
|
"@types/react": "^19.2.10",
|
|
40
42
|
"@types/react-dom": "^19.2.3",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date → 로케일 기반 날짜 포맷 (시간 없음).
|
|
3
|
+
*/
|
|
4
|
+
export const formatDate = (date: Date, locale = 'ko-KR'): string =>
|
|
5
|
+
new Intl.DateTimeFormat(locale, {
|
|
6
|
+
year: 'numeric',
|
|
7
|
+
month: '2-digit',
|
|
8
|
+
day: '2-digit',
|
|
9
|
+
}).format(date);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Date → 로케일 기반 날짜 + 시간 포맷 (24h).
|
|
13
|
+
*/
|
|
14
|
+
export const formatDateTime = (date: Date, locale = 'ko-KR'): string =>
|
|
15
|
+
new Intl.DateTimeFormat(locale, {
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
month: '2-digit',
|
|
18
|
+
day: '2-digit',
|
|
19
|
+
hour: '2-digit',
|
|
20
|
+
minute: '2-digit',
|
|
21
|
+
hour12: false,
|
|
22
|
+
}).format(date);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 숫자 → 한국 원화(KRW) 포맷. 소수점 없음.
|
|
3
|
+
* 예: 12000 → "₩12,000"
|
|
4
|
+
*/
|
|
5
|
+
export const formatPrice = (amount: number, locale = 'ko-KR'): string =>
|
|
6
|
+
new Intl.NumberFormat(locale, {
|
|
7
|
+
style: 'currency',
|
|
8
|
+
currency: 'KRW',
|
|
9
|
+
maximumFractionDigits: 0,
|
|
10
|
+
}).format(amount);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isServer } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getBrowserQueryClient,
|
|
5
|
+
getServerQueryClient,
|
|
6
|
+
} from '@/src/shared/api/queryClient';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* RSC 와 클라이언트 양쪽에서 안전하게 호출 가능한 QueryClient 핸들.
|
|
10
|
+
* 서버에서는 React `cache()` 로 요청 단위 싱글턴, 브라우저에서는 모듈 싱글턴.
|
|
11
|
+
*/
|
|
12
|
+
export default function getQueryClient() {
|
|
13
|
+
return isServer ? getServerQueryClient() : getBrowserQueryClient();
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 테스트 전용 QueryClient — retry/refetch 끄고 gcTime 0 으로 격리.
|
|
5
|
+
*/
|
|
6
|
+
export const createTestQueryClient = (): QueryClient =>
|
|
7
|
+
new QueryClient({
|
|
8
|
+
defaultOptions: {
|
|
9
|
+
queries: {
|
|
10
|
+
retry: false,
|
|
11
|
+
refetchOnWindowFocus: false,
|
|
12
|
+
gcTime: 0,
|
|
13
|
+
},
|
|
14
|
+
mutations: {
|
|
15
|
+
retry: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
type RenderOptions,
|
|
6
|
+
type RenderResult,
|
|
7
|
+
} from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
10
|
+
|
|
11
|
+
import { createTestQueryClient } from './createTestQueryClient';
|
|
12
|
+
|
|
13
|
+
type Options = Omit<RenderOptions, 'wrapper'> & {
|
|
14
|
+
queryClient?: QueryClient;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Result = RenderResult & {
|
|
18
|
+
user: ReturnType<typeof userEvent.setup>;
|
|
19
|
+
queryClient: QueryClient;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* RTL render + QueryClientProvider + userEvent setup 한 번에.
|
|
24
|
+
* 추가 Provider 가 필요하면 이 파일을 직접 수정하거나 wrapper 옵션을 쓴다.
|
|
25
|
+
*/
|
|
26
|
+
export const renderWithProviders = (
|
|
27
|
+
ui: ReactElement,
|
|
28
|
+
options: Options = {},
|
|
29
|
+
): Result => {
|
|
30
|
+
const { queryClient = createTestQueryClient(), ...rtlOptions } = options;
|
|
31
|
+
|
|
32
|
+
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
33
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const result = render(ui, { wrapper: Wrapper, ...rtlOptions });
|
|
37
|
+
const user = userEvent.setup();
|
|
38
|
+
|
|
39
|
+
return { ...result, user, queryClient };
|
|
40
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Component,
|
|
5
|
+
type ComponentType,
|
|
6
|
+
type ErrorInfo,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
Suspense,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { QueryErrorResetBoundary } from '@tanstack/react-query';
|
|
11
|
+
|
|
12
|
+
export type ErrorFallbackProps = {
|
|
13
|
+
error: Error | null;
|
|
14
|
+
resetErrorBoundary: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ErrorBoundaryProps = {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
fallback?: ComponentType<ErrorFallbackProps>;
|
|
20
|
+
onReset: () => void;
|
|
21
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ErrorBoundaryState = {
|
|
25
|
+
hasError: boolean;
|
|
26
|
+
error: Error | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
30
|
+
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
31
|
+
|
|
32
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
33
|
+
return { hasError: true, error };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
37
|
+
this.props.onError?.(error, info);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
resetErrorBoundary = () => {
|
|
41
|
+
this.props.onReset();
|
|
42
|
+
this.setState({ hasError: false, error: null });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
render() {
|
|
46
|
+
const { hasError, error } = this.state;
|
|
47
|
+
const { children, fallback: Fallback } = this.props;
|
|
48
|
+
|
|
49
|
+
if (hasError && Fallback) {
|
|
50
|
+
return (
|
|
51
|
+
<Fallback error={error} resetErrorBoundary={this.resetErrorBoundary} />
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return children;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type FallbackBoundaryProps = {
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
errorFallback?: ComponentType<ErrorFallbackProps>;
|
|
62
|
+
suspenseFallback?: ReactNode;
|
|
63
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Suspense + ErrorBoundary 합성. React Query 의 reset 신호에 맞춰
|
|
68
|
+
* `errorFallback` 의 `resetErrorBoundary` 가 쿼리까지 함께 리셋한다.
|
|
69
|
+
*/
|
|
70
|
+
export function FallbackBoundary({
|
|
71
|
+
children,
|
|
72
|
+
errorFallback,
|
|
73
|
+
suspenseFallback,
|
|
74
|
+
onError,
|
|
75
|
+
}: FallbackBoundaryProps) {
|
|
76
|
+
return (
|
|
77
|
+
<QueryErrorResetBoundary>
|
|
78
|
+
{({ reset }) => (
|
|
79
|
+
<ErrorBoundary
|
|
80
|
+
onReset={reset}
|
|
81
|
+
fallback={errorFallback}
|
|
82
|
+
onError={onError}
|
|
83
|
+
>
|
|
84
|
+
<Suspense fallback={suspenseFallback}>{children}</Suspense>
|
|
85
|
+
</ErrorBoundary>
|
|
86
|
+
)}
|
|
87
|
+
</QueryErrorResetBoundary>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
dehydrate,
|
|
5
|
+
type FetchQueryOptions,
|
|
6
|
+
HydrationBoundary,
|
|
7
|
+
} from '@tanstack/react-query';
|
|
8
|
+
|
|
9
|
+
import getQueryClient from '@/src/shared/lib/getQueryClient';
|
|
10
|
+
|
|
11
|
+
export type FetchOptions = Pick<FetchQueryOptions, 'queryKey' | 'queryFn'>;
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
fetchOptions?: FetchOptions[] | FetchOptions | null;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RSC 에서 prefetch 를 끝낸 뒤 dehydrated state 로 클라이언트에 hydrate.
|
|
20
|
+
* 단일/배열 둘 다 받는다.
|
|
21
|
+
*/
|
|
22
|
+
export async function PrefetchBoundary({ fetchOptions, children }: Props) {
|
|
23
|
+
const queryClient = getQueryClient();
|
|
24
|
+
|
|
25
|
+
if (fetchOptions) {
|
|
26
|
+
const list = Array.isArray(fetchOptions) ? fetchOptions : [fetchOptions];
|
|
27
|
+
await Promise.all(list.map((opt) => queryClient.prefetchQuery(opt)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
32
|
+
{children}
|
|
33
|
+
</HydrationBoundary>
|
|
34
|
+
);
|
|
35
|
+
}
|