sh-ui-cli 0.57.0 → 0.58.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 +17 -0
- package/package.json +1 -1
- package/src/api.d.ts +34 -0
- package/src/api.js +7 -0
- package/src/create/architectures/archSchema.js +69 -0
- package/src/create/architectures/flat.js +51 -0
- package/src/create/architectures/fsd.js +42 -0
- package/src/create/architectures/index.js +55 -0
- package/src/create/cli-args.js +8 -1
- package/src/create/generator.js +101 -32
- package/src/create/index.mjs +7 -0
- package/src/create/plugins/authJwt.js +14 -8
- package/src/create/plugins/nextIntl.js +25 -17
- package/src/create/plugins/pluginSchema.js +26 -10
- package/src/create/plugins/sentry.js +9 -5
- package/src/mcp.mjs +10 -0
- package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-app/_arch/flat/app/layout.tsx +16 -0
- package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +11 -0
- package/templates/nextjs-app/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-app/_arch/flat/tsconfig.json +25 -0
- package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +16 -0
- package/templates/nextjs-standalone/_arch/flat/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +11 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +12 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/error.ts +12 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/http.ts +13 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/observability.ts +20 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +52 -0
- package/templates/nextjs-standalone/_arch/flat/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/_arch/flat/lib/test/index.ts +2 -0
- package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +40 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +22 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +10 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-standalone/_arch/flat/sh-ui.config.json +19 -0
- package/templates/nextjs-standalone/_arch/flat/tsconfig.json +41 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +12 -0
- package/templates/nextjs-standalone/_arch/fsd/src/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/features/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/apiTypes.ts +21 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/error.ts +12 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/http.ts +13 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/observability.ts +20 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/config/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +52 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +22 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +10 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/utils.ts +6 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/model/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +135 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/index.ts +2 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +40 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/_arch/fsd/src/views/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/widgets/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/entities → _arch/flat/components/common}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/ui → _arch/flat/components/common}/FallbackBoundary/index.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/GlobalProvider/index.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/index.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/theme/ThemeProviders.tsx +0 -0
- /package/templates/nextjs-app/{src/features → _arch/flat/lib/api}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/apiTypes.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/clientFetch.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/error.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/http.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/observability.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/queryClient.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/serverFetch.ts +0 -0
- /package/templates/nextjs-app/{src/shared/api → _arch/flat/lib/config}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/config → _arch/flat/lib/hooks}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/hooks/useAppMutation.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/createTestQueryClient.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/index.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/renderWithProviders.tsx +0 -0
- /package/templates/nextjs-app/{src/shared/hooks → _arch/flat/lib/utils}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatDate.ts +0 -0
- /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatPrice.ts +0 -0
- /package/templates/nextjs-app/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
- /package/templates/nextjs-app/{app → _arch/fsd/app}/layout.tsx +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/GlobalProvider/index.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/index.tsx +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/theme/ThemeProviders.tsx +0 -0
- /package/templates/nextjs-app/{src/shared/lib → _arch/fsd/src/entities}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/model → _arch/fsd/src/features}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/ui → _arch/fsd/src/shared/api}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/apiTypes.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/clientFetch.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/error.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/http.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/observability.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/queryClient.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/serverFetch.ts +0 -0
- /package/templates/nextjs-app/{src/views → _arch/fsd/src/shared/config}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/widgets → _arch/fsd/src/shared/hooks}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/hooks/useAppMutation.ts +0 -0
- /package/templates/{nextjs-standalone/src/entities → nextjs-app/_arch/fsd/src/shared/lib}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatDate.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatPrice.ts +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
- /package/templates/{nextjs-standalone/src/features → nextjs-app/_arch/fsd/src/shared/model}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/createTestQueryClient.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/index.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/renderWithProviders.tsx +0 -0
- /package/templates/{nextjs-standalone/src/shared/api → nextjs-app/_arch/fsd/src/shared/ui}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/ui/FallbackBoundary/index.tsx +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
- /package/templates/{nextjs-standalone/src/shared/config → nextjs-app/_arch/fsd/src/views}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone/src/shared/hooks → nextjs-app/_arch/fsd/src/widgets}/.gitkeep +0 -0
- /package/templates/nextjs-app/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
- /package/templates/nextjs-standalone/{src/shared/model → _arch/flat/components/common}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/shared/ui → _arch/flat/lib/api}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/views → _arch/flat/lib/config}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/widgets → _arch/flat/lib/hooks}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/shared → _arch/flat/lib}/styles/tokens.css +0 -0
- /package/templates/nextjs-standalone/{src/shared/lib → _arch/flat/lib/utils}/utils.ts +0 -0
- /package/templates/nextjs-standalone/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
- /package/templates/nextjs-standalone/{app → _arch/fsd/app}/layout.tsx +0 -0
- /package/templates/nextjs-standalone/{sh-ui.config.json → _arch/fsd/sh-ui.config.json} +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
- /package/templates/nextjs-standalone/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { RootLayout } from '@/components/layouts/RootLayout';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: 'My App',
|
|
7
|
+
description: 'My App Description',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function Layout({
|
|
11
|
+
children,
|
|
12
|
+
}: Readonly<{
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}>) {
|
|
15
|
+
return <RootLayout>{children}</RootLayout>;
|
|
16
|
+
}
|
|
@@ -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 '@/lib/utils/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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { GlobalProvider } from '@/components/providers';
|
|
2
|
+
|
|
3
|
+
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<html lang='ko' suppressHydrationWarning>
|
|
6
|
+
<body>
|
|
7
|
+
<GlobalProvider>{children}</GlobalProvider>
|
|
8
|
+
</body>
|
|
9
|
+
</html>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Toaster } from 'sonner';
|
|
3
|
+
|
|
4
|
+
import { QueryClientProvider } from '../tanstack/QueryClientProvider';
|
|
5
|
+
import { TanstackDevtoolsProvider } from '../tanstack/TanstackDevtoolsProvider';
|
|
6
|
+
import { ThemeProviders } from '../theme/ThemeProviders';
|
|
7
|
+
|
|
8
|
+
interface GlobalProviderProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function GlobalProvider({ children }: GlobalProviderProps) {
|
|
13
|
+
return (
|
|
14
|
+
<ThemeProviders>
|
|
15
|
+
<QueryClientProvider>
|
|
16
|
+
<TanstackDevtoolsProvider>
|
|
17
|
+
<Toaster />
|
|
18
|
+
{children}
|
|
19
|
+
</TanstackDevtoolsProvider>
|
|
20
|
+
</QueryClientProvider>
|
|
21
|
+
</ThemeProviders>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GlobalProvider } from './GlobalProvider';
|
package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import { getBrowserQueryClient } from '@/lib/api/queryClient';
|
|
7
|
+
|
|
8
|
+
export function QueryClientProvider({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<TanstackQueryClientProvider client={getBrowserQueryClient()}>
|
|
11
|
+
{children}
|
|
12
|
+
</TanstackQueryClientProvider>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export function TanstackDevtoolsProvider({ children }: { children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
{children}
|
|
10
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider } from 'next-themes';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export function ThemeProviders({ children }: { children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
|
|
9
|
+
{children}
|
|
10
|
+
</ThemeProvider>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** 공통 API 에러 형식 */
|
|
2
|
+
export type ApiErrorBody = {
|
|
3
|
+
message: string;
|
|
4
|
+
code: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** 백엔드 공통 응답 래퍼 */
|
|
8
|
+
export type ApiResponse<TData = unknown, TError = ApiErrorBody> = {
|
|
9
|
+
result: 'SUCCESS' | 'ERROR';
|
|
10
|
+
data: TData;
|
|
11
|
+
error: TError | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** 페이지네이션 응답 래퍼 */
|
|
15
|
+
export type PaginatedData<T> = {
|
|
16
|
+
content: T[];
|
|
17
|
+
totalItems: number;
|
|
18
|
+
offset: number;
|
|
19
|
+
limit: number;
|
|
20
|
+
hasNext: boolean;
|
|
21
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ApiErrorBody } from './apiTypes';
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
public readonly status: number,
|
|
6
|
+
public readonly code: string,
|
|
7
|
+
public readonly data: ApiErrorBody | null,
|
|
8
|
+
) {
|
|
9
|
+
super(data?.message ?? `API 요청 실패 (${status})`);
|
|
10
|
+
this.name = 'ApiError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
};
|
|
@@ -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,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 '@/lib/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,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"platform": "react",
|
|
3
|
+
"cssFramework": "plain",
|
|
4
|
+
"theme": {
|
|
5
|
+
"base": "neutral",
|
|
6
|
+
"radius": "md",
|
|
7
|
+
"mode": "light-dark"
|
|
8
|
+
},
|
|
9
|
+
"paths": {
|
|
10
|
+
"tokens": "lib/styles/tokens.css",
|
|
11
|
+
"components": "components/common",
|
|
12
|
+
"utils": "lib/utils/utils.ts"
|
|
13
|
+
},
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components/common",
|
|
16
|
+
"utils": "@/lib/utils/utils",
|
|
17
|
+
"ui": "@/components/common"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["es2022", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"allowJs": true,
|
|
18
|
+
"jsx": "preserve",
|
|
19
|
+
"noEmit": true,
|
|
20
|
+
"incremental": true,
|
|
21
|
+
"baseUrl": ".",
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/lib/*": ["./lib/*"],
|
|
24
|
+
"@/components/*": ["./components/*"],
|
|
25
|
+
"@/app/*": ["./app/*"]
|
|
26
|
+
},
|
|
27
|
+
"plugins": [
|
|
28
|
+
{
|
|
29
|
+
"name": "next"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"next-env.d.ts",
|
|
35
|
+
"next.config.ts",
|
|
36
|
+
"**/*.ts",
|
|
37
|
+
"**/*.tsx",
|
|
38
|
+
".next/types/**/*.ts"
|
|
39
|
+
],
|
|
40
|
+
"exclude": ["node_modules"]
|
|
41
|
+
}
|