sh-ui-cli 0.76.0 → 0.77.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 +26 -0
- package/data/registry/react/components/switch/index.tailwind.tsx +1 -1
- package/data/registry/react/components/switch/styles.css +6 -0
- package/data/registry/react/components/switch/styles.module.css +6 -0
- package/data/registry/react/tokens-used.json +3 -1
- package/package.json +1 -1
- package/src/create/architectures/index.js +2 -1
- package/src/create/architectures/mes.js +53 -0
- package/templates/monorepo/packages/eslint-config/mes.js +82 -0
- package/templates/monorepo/packages/eslint-config/package.json +2 -1
- package/templates/nextjs-app/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-app/_arch/mes/app/layout.tsx +16 -0
- package/templates/nextjs-app/_arch/mes/app/sign-in/page.tsx +1 -0
- package/templates/nextjs-app/_arch/mes/eslint.config.js +10 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-app/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/index.tsx +1 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
- package/templates/nextjs-app/_arch/mes/src/hooks/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/hooks/useAppMutation.ts +59 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/error.ts +12 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/http.ts +13 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/observability.ts +20 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-app/_arch/mes/src/lib/config/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/index.ts +2 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/formatDate.ts +26 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/api.ts +3 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/index.tsx +14 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/schema.ts +2 -0
- package/templates/nextjs-app/_arch/mes/tsconfig.json +24 -0
- package/templates/nextjs-standalone/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-standalone/_arch/mes/app/globals.css +49 -0
- package/templates/nextjs-standalone/_arch/mes/app/layout.tsx +16 -0
- package/templates/nextjs-standalone/_arch/mes/app/sign-in/page.tsx +1 -0
- package/templates/nextjs-standalone/_arch/mes/eslint.config.js +137 -0
- package/templates/nextjs-standalone/_arch/mes/sh-ui.config.json +22 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
- package/templates/nextjs-standalone/_arch/mes/src/hooks/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/hooks/useAppMutation.ts +59 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/error.ts +12 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/http.ts +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/observability.ts +20 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/config/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/styles/tokens.css +170 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/index.ts +2 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatDate.ts +26 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/utils.ts +6 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/api.ts +3 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/index.tsx +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/schema.ts +2 -0
- package/templates/nextjs-standalone/_arch/mes/tsconfig.json +39 -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: 'sh-ui app',
|
|
7
|
+
description: 'sh-ui 기반 앱 — metadata 를 변경하세요.',
|
|
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 @@
|
|
|
1
|
+
export { default } from '@/pages/sign-in';
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import js from "@eslint/js"
|
|
2
|
+
import pluginNext from "@next/eslint-plugin-next"
|
|
3
|
+
import eslintConfigPrettier from "eslint-config-prettier"
|
|
4
|
+
import boundaries from "eslint-plugin-boundaries"
|
|
5
|
+
import checkFile from "eslint-plugin-check-file"
|
|
6
|
+
import onlyWarn from "eslint-plugin-only-warn"
|
|
7
|
+
import pluginReact from "eslint-plugin-react"
|
|
8
|
+
import pluginReactHooks from "eslint-plugin-react-hooks"
|
|
9
|
+
import globals from "globals"
|
|
10
|
+
import tseslint from "typescript-eslint"
|
|
11
|
+
|
|
12
|
+
export default [
|
|
13
|
+
{
|
|
14
|
+
ignores: [".next/**", "dist/**", "node_modules/**"],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// ── Base ──
|
|
18
|
+
js.configs.recommended,
|
|
19
|
+
eslintConfigPrettier,
|
|
20
|
+
...tseslint.configs.recommended,
|
|
21
|
+
{
|
|
22
|
+
plugins: { onlyWarn },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
rules: {
|
|
26
|
+
"@typescript-eslint/no-unused-vars": [
|
|
27
|
+
"warn",
|
|
28
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// ── React + Next.js ──
|
|
34
|
+
{
|
|
35
|
+
...pluginReact.configs.flat.recommended,
|
|
36
|
+
languageOptions: {
|
|
37
|
+
...pluginReact.configs.flat.recommended.languageOptions,
|
|
38
|
+
globals: {
|
|
39
|
+
...globals.serviceworker,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
plugins: {
|
|
45
|
+
"@next/next": pluginNext,
|
|
46
|
+
},
|
|
47
|
+
rules: {
|
|
48
|
+
...pluginNext.configs.recommended.rules,
|
|
49
|
+
...pluginNext.configs["core-web-vitals"].rules,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
plugins: {
|
|
54
|
+
"react-hooks": pluginReactHooks,
|
|
55
|
+
},
|
|
56
|
+
settings: { react: { version: "detect" } },
|
|
57
|
+
rules: {
|
|
58
|
+
...pluginReactHooks.configs.recommended.rules,
|
|
59
|
+
"react/react-in-jsx-scope": "off",
|
|
60
|
+
"react/prop-types": "off",
|
|
61
|
+
"react/function-component-definition": [
|
|
62
|
+
"warn",
|
|
63
|
+
{
|
|
64
|
+
namedComponents: "function-declaration",
|
|
65
|
+
unnamedComponents: "arrow-function",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// ── MES-arch boundaries ──
|
|
72
|
+
// 페이지 격리 + 단방향 의존:
|
|
73
|
+
// - 각 `src/pages/<name>/` 는 자기완결 — 다른 페이지 import 금지
|
|
74
|
+
// - 페이지/공용 컴포넌트는 hooks/lib 사용 OK, 반대 방향은 X
|
|
75
|
+
// - app 라우트는 페이지/공용 모두 import 가능 (한 줄 위임)
|
|
76
|
+
{
|
|
77
|
+
plugins: { boundaries },
|
|
78
|
+
settings: {
|
|
79
|
+
"boundaries/elements": [
|
|
80
|
+
{ type: "lib", pattern: ["src/lib/*"], mode: "folder" },
|
|
81
|
+
{ type: "hooks", pattern: ["src/hooks"], mode: "folder" },
|
|
82
|
+
{ type: "components", pattern: ["src/components/*"], mode: "folder" },
|
|
83
|
+
{ type: "pages", pattern: ["src/pages/*"], mode: "folder" },
|
|
84
|
+
{ type: "app", pattern: ["app"], mode: "folder" },
|
|
85
|
+
],
|
|
86
|
+
"boundaries/ignore": ["**/*.test.*", "**/*.spec.*"],
|
|
87
|
+
},
|
|
88
|
+
rules: {
|
|
89
|
+
"boundaries/element-types": [
|
|
90
|
+
"warn",
|
|
91
|
+
{
|
|
92
|
+
default: "disallow",
|
|
93
|
+
rules: [
|
|
94
|
+
{ from: "app", allow: ["pages", "components", "hooks", "lib"] },
|
|
95
|
+
// pages 끼리는 import 금지 — 페이지 격리 원칙
|
|
96
|
+
{ from: "pages", allow: ["components", "hooks", "lib"] },
|
|
97
|
+
{ from: "components", allow: ["components", "hooks", "lib"] },
|
|
98
|
+
{ from: "hooks", allow: ["hooks", "lib"] },
|
|
99
|
+
{ from: "lib", allow: ["lib"] },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// ── File naming ──
|
|
107
|
+
// .tsx = PASCAL_CASE (컴포넌트), .ts = CAMEL_CASE (유틸/스키마/훅)
|
|
108
|
+
{
|
|
109
|
+
plugins: { "check-file": checkFile },
|
|
110
|
+
rules: {
|
|
111
|
+
"check-file/filename-naming-convention": [
|
|
112
|
+
"error",
|
|
113
|
+
{
|
|
114
|
+
"**/components/**/*.tsx": "PASCAL_CASE",
|
|
115
|
+
"**/pages/**/components/**/*.tsx": "PASCAL_CASE",
|
|
116
|
+
"**/lib/**/*.ts": "CAMEL_CASE",
|
|
117
|
+
"**/hooks/**/*.ts": "CAMEL_CASE",
|
|
118
|
+
},
|
|
119
|
+
{ ignoreMiddleExtensions: true },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
files: [
|
|
125
|
+
"**/index.tsx", "**/index.ts",
|
|
126
|
+
"**/layout.tsx", "**/page.tsx",
|
|
127
|
+
"**/error.tsx", "**/not-found.tsx",
|
|
128
|
+
"**/routing.ts", "**/navigation.ts", "**/request.ts",
|
|
129
|
+
// MES 페이지 concern 파일들
|
|
130
|
+
"**/pages/**/api.ts", "**/pages/**/schema.ts",
|
|
131
|
+
"**/pages/**/columns.ts", "**/pages/**/hooks.ts",
|
|
132
|
+
],
|
|
133
|
+
rules: {
|
|
134
|
+
"check-file/filename-naming-convention": "off",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/sanghyeonKim0201/sh-ui/live/packages/cli/sh-ui.schema.json",
|
|
3
|
+
"platform": "react",
|
|
4
|
+
"cssFramework": "plain",
|
|
5
|
+
"theme": {
|
|
6
|
+
"base": "neutral",
|
|
7
|
+
"radius": "md",
|
|
8
|
+
"mode": "light-dark"
|
|
9
|
+
},
|
|
10
|
+
"paths": {
|
|
11
|
+
"tokens": "src/lib/styles/tokens.css",
|
|
12
|
+
"cssEntry": "app/globals.css",
|
|
13
|
+
"styles": "src/lib/styles",
|
|
14
|
+
"components": "src/components/common",
|
|
15
|
+
"utils": "src/lib/utils/utils.ts"
|
|
16
|
+
},
|
|
17
|
+
"aliases": {
|
|
18
|
+
"components": "@/components/common",
|
|
19
|
+
"utils": "@/lib/utils/utils",
|
|
20
|
+
"ui": "@/components/common"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
File without changes
|
package/templates/nextjs-standalone/_arch/mes/src/components/common/FallbackBoundary/index.tsx
ADDED
|
@@ -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
|
+
}
|
package/templates/nextjs-standalone/_arch/mes/src/components/common/PrefetchBoundary/index.tsx
ADDED
|
@@ -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,13 @@
|
|
|
1
|
+
import { GlobalProvider } from '@/components/providers';
|
|
2
|
+
|
|
3
|
+
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
// `lang` 은 앱의 주 언어. 영어 등 다른 언어 우선이면 'en' 으로 바꾸거나
|
|
5
|
+
// next-intl 플러그인을 활성화해 라우트 기반 자동 분기로 전환하세요.
|
|
6
|
+
return (
|
|
7
|
+
<html lang='ko' suppressHydrationWarning>
|
|
8
|
+
<body>
|
|
9
|
+
<GlobalProvider>{children}</GlobalProvider>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
12
|
+
);
|
|
13
|
+
}
|
package/templates/nextjs-standalone/_arch/mes/src/components/providers/GlobalProvider/index.tsx
ADDED
|
@@ -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 { ThemeProvider } from '../theme/ThemeProvider';
|
|
7
|
+
|
|
8
|
+
interface GlobalProviderProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function GlobalProvider({ children }: GlobalProviderProps) {
|
|
13
|
+
return (
|
|
14
|
+
<ThemeProvider>
|
|
15
|
+
<QueryClientProvider>
|
|
16
|
+
<TanstackDevtoolsProvider>
|
|
17
|
+
<Toaster />
|
|
18
|
+
{children}
|
|
19
|
+
</TanstackDevtoolsProvider>
|
|
20
|
+
</QueryClientProvider>
|
|
21
|
+
</ThemeProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GlobalProvider } from './GlobalProvider';
|
|
@@ -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
|
+
}
|
package/templates/nextjs-standalone/_arch/mes/src/components/providers/theme/ThemeProvider.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 다크/라이트 테마 — next-themes ThemeProvider 를 wrap.
|
|
8
|
+
*
|
|
9
|
+
* - `attribute='class'` — `<html class="dark">` 토글 (Tailwind dark variant 와 호환)
|
|
10
|
+
* - `defaultTheme='system'` + `enableSystem` — OS 설정에 자동 동기화. light/dark 만
|
|
11
|
+
* 노출하려면 `enableSystem` 을 false 로
|
|
12
|
+
* - `disableTransitionOnChange` — 토글 순간 transition 깜빡임 차단
|
|
13
|
+
*
|
|
14
|
+
* useTheme 는 next-themes 에서 직접 import: `import { useTheme } from 'next-themes'`
|
|
15
|
+
*/
|
|
16
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
17
|
+
return (
|
|
18
|
+
<NextThemesProvider
|
|
19
|
+
attribute='class'
|
|
20
|
+
defaultTheme='system'
|
|
21
|
+
enableSystem
|
|
22
|
+
disableTransitionOnChange
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</NextThemesProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
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 '@/lib/api/error';
|
|
9
|
+
import { resolveErrorMessage } from '@/lib/api/errorMessages';
|
|
10
|
+
|
|
11
|
+
type AppMutationOptions<
|
|
12
|
+
TData = unknown,
|
|
13
|
+
TError = DefaultError,
|
|
14
|
+
TVariables = void,
|
|
15
|
+
TContext = unknown,
|
|
16
|
+
> = UseMutationOptions<TData, TError, TVariables, TContext> & {
|
|
17
|
+
/** ApiError 가 아니거나 mapping 에 없을 때의 fallback 메시지. */
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
/** false 면 toast 띄우지 않음. */
|
|
20
|
+
showErrorToast?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* useMutation 래퍼 — 에러 발생 시 `resolveErrorMessage` 를 통해 안전한
|
|
25
|
+
* 사용자 facing 메시지를 toast 로 띄운다. backend 가 보낸 raw 메시지를 그대로
|
|
26
|
+
* 띄우지 않고 `errorMessages.ts` 의 mapping 을 우선 사용해 일관된 사용자
|
|
27
|
+
* 경험과 i18n 친화성을 확보.
|
|
28
|
+
*
|
|
29
|
+
* showErrorToast: false 로 자동 toast 끌 수 있고, errorMessage 로 fallback 지정.
|
|
30
|
+
*/
|
|
31
|
+
export const useAppMutation = <
|
|
32
|
+
TData = unknown,
|
|
33
|
+
TError = DefaultError,
|
|
34
|
+
TVariables = void,
|
|
35
|
+
TContext = unknown,
|
|
36
|
+
>(
|
|
37
|
+
options: AppMutationOptions<TData, TError, TVariables, TContext>,
|
|
38
|
+
) => {
|
|
39
|
+
const { errorMessage, showErrorToast = true, onError, ...rest } = options;
|
|
40
|
+
|
|
41
|
+
return useMutation({
|
|
42
|
+
...rest,
|
|
43
|
+
onError: (...args) => {
|
|
44
|
+
onError?.(...args);
|
|
45
|
+
|
|
46
|
+
if (!showErrorToast) return;
|
|
47
|
+
|
|
48
|
+
const [error] = args;
|
|
49
|
+
const message = resolveErrorMessage(error, errorMessage);
|
|
50
|
+
|
|
51
|
+
if (message) {
|
|
52
|
+
toast.error(message);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// re-export ApiError 타입을 쓰는 사용처 편의용 (선택).
|
|
59
|
+
export type { ApiError };
|
|
File without changes
|
|
@@ -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
|
+
// 이미 sign-in 페이지면 redirect 루프 방지.
|
|
27
|
+
// next-intl 활성 시 path 가 `/ko/sign-in` 형태가 되므로 regex 로 match.
|
|
28
|
+
if (typeof window !== 'undefined' && !/\/sign-in(\/|$)/.test(window.location.pathname)) {
|
|
29
|
+
// /sign-in 으로만 보내면 middleware (next-intl 활성 시) 가 default locale 을 prefix 추가.
|
|
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,37 @@
|
|
|
1
|
+
import { ApiError } from './error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 에러 코드 → 사용자 facing 메시지 mapping.
|
|
5
|
+
*
|
|
6
|
+
* 백엔드가 보내는 raw `error.message` 를 그대로 toast 로 띄우면 i18n 어긋나거나
|
|
7
|
+
* 내부 노출이 될 수 있어, frontend 가 정의한 안전한 메시지로 변환하는 게 권장.
|
|
8
|
+
* 코드별로 명시 안 된 경우 `error.data.message` (backend 메시지) → fallback.
|
|
9
|
+
*
|
|
10
|
+
* 사용자 프로젝트에서 자유롭게 추가/수정. next-intl 활성 시엔 `useTranslations`
|
|
11
|
+
* 와 결합해 hook 형태로 변환 가능 (이 파일은 RSC/CSR 양쪽 호환을 위해 module).
|
|
12
|
+
*/
|
|
13
|
+
export const ERROR_MESSAGES: Record<string, string> = {
|
|
14
|
+
UNAUTHORIZED: '로그인이 필요합니다.',
|
|
15
|
+
FORBIDDEN: '접근 권한이 없습니다.',
|
|
16
|
+
NOT_FOUND: '요청한 리소스를 찾을 수 없습니다.',
|
|
17
|
+
NETWORK_ERROR: '서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.',
|
|
18
|
+
// 사용자 정의 코드를 여기에 추가하세요.
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const DEFAULT_FALLBACK = '일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 에러 → 사용자 facing 메시지 결정.
|
|
25
|
+
* 1. ApiError + 코드가 ERROR_MESSAGES 에 있으면 그 메시지
|
|
26
|
+
* 2. ApiError 의 backend `data.message` (있으면)
|
|
27
|
+
* 3. 호출자의 fallback
|
|
28
|
+
* 4. 글로벌 DEFAULT_FALLBACK
|
|
29
|
+
*/
|
|
30
|
+
export function resolveErrorMessage(error: unknown, fallback?: string): string {
|
|
31
|
+
if (error instanceof ApiError) {
|
|
32
|
+
const mapped = ERROR_MESSAGES[error.code];
|
|
33
|
+
if (mapped) return mapped;
|
|
34
|
+
if (error.data?.message) return error.data.message;
|
|
35
|
+
}
|
|
36
|
+
return fallback ?? DEFAULT_FALLBACK;
|
|
37
|
+
}
|
|
@@ -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
|
+
}
|