sh-ui-cli 0.75.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/bin/sh-ui.mjs +1 -1
- package/data/changelog/versions.json +39 -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 +3 -3
- package/src/create/architectures/index.js +2 -1
- package/src/create/architectures/mes.js +53 -0
- package/src/mcp-init.mjs +161 -20
- 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,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 function getQueryClient() {
|
|
13
|
+
return isServer ? getServerQueryClient() : getBrowserQueryClient();
|
|
14
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// MES 프리셋의 페이지 본체 슬롯 예시.
|
|
2
|
+
//
|
|
3
|
+
// 페이지 폴더 구조 (관용):
|
|
4
|
+
// index.tsx ─ 페이지 컴포넌트 (이 파일)
|
|
5
|
+
// components/ ─ 페이지 전용 UI (폼, 다이얼로그, 테이블 등)
|
|
6
|
+
// api.ts ─ 순수 fetch 레이어
|
|
7
|
+
// hooks.ts ─ TanStack Query 어댑터 (useQuery / useMutation)
|
|
8
|
+
// schema.ts ─ zod 스키마
|
|
9
|
+
// columns.ts ─ (옵션) 리스트 페이지의 테이블 컬럼 정의
|
|
10
|
+
//
|
|
11
|
+
// 새 페이지를 추가할 때 이 폴더를 복사해 이름만 바꾸세요.
|
|
12
|
+
export default function SignInPage() {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@workspace/typescript-config/nextjs.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/*": ["./src/*"],
|
|
7
|
+
"@workspace/ui-app-name/*": ["../../packages/ui/ui-apps/ui-app-name/src/*"],
|
|
8
|
+
"@workspace/ui-core/*": ["../../packages/ui/ui-core/src/*"]
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "next"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"next-env.d.ts",
|
|
18
|
+
"next.config.ts",
|
|
19
|
+
"**/*.ts",
|
|
20
|
+
"**/*.tsx",
|
|
21
|
+
".next/types/**/*.ts"
|
|
22
|
+
],
|
|
23
|
+
"exclude": ["node_modules"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { cookies } from 'next/headers';
|
|
2
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
captureApiError,
|
|
6
|
+
logApiError,
|
|
7
|
+
} from '@/lib/api/observability';
|
|
8
|
+
|
|
9
|
+
const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
|
|
10
|
+
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
11
|
+
const LOCALE_COOKIE = 'NEXT_LOCALE';
|
|
12
|
+
|
|
13
|
+
const proxyRequest = async (
|
|
14
|
+
request: NextRequest,
|
|
15
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
16
|
+
method: string,
|
|
17
|
+
) => {
|
|
18
|
+
const { path } = await ctx.params;
|
|
19
|
+
const apiPath = path.join('/');
|
|
20
|
+
const url = new URL(`${API_URL}/${apiPath}`);
|
|
21
|
+
|
|
22
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
23
|
+
url.searchParams.set(key, value);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const cookieStore = await cookies();
|
|
27
|
+
const accessToken = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
28
|
+
const locale =
|
|
29
|
+
cookieStore.get(LOCALE_COOKIE)?.value ??
|
|
30
|
+
request.headers.get('Accept-Language') ??
|
|
31
|
+
undefined;
|
|
32
|
+
|
|
33
|
+
const headers: Record<string, string> = {};
|
|
34
|
+
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
35
|
+
if (locale) headers['Accept-Language'] = locale;
|
|
36
|
+
|
|
37
|
+
let body: BodyInit | undefined;
|
|
38
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
39
|
+
const contentType = request.headers.get('Content-Type');
|
|
40
|
+
if (contentType?.includes('multipart/form-data')) {
|
|
41
|
+
body = await request.formData();
|
|
42
|
+
} else {
|
|
43
|
+
headers['Content-Type'] = 'application/json';
|
|
44
|
+
body = await request.text();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let response: Response;
|
|
49
|
+
try {
|
|
50
|
+
response = await fetch(url.toString(), { method, headers, body });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`[PROXY] ${method} ${url.toString()} —`, error);
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{
|
|
55
|
+
result: 'ERROR',
|
|
56
|
+
data: null,
|
|
57
|
+
error: {
|
|
58
|
+
code: 'NETWORK_ERROR',
|
|
59
|
+
message: 'Failed to reach upstream server.',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{ status: 502 },
|
|
63
|
+
);
|
|
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 });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const GET = (
|
|
90
|
+
req: NextRequest,
|
|
91
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
92
|
+
) => proxyRequest(req, ctx, 'GET');
|
|
93
|
+
|
|
94
|
+
export const POST = (
|
|
95
|
+
req: NextRequest,
|
|
96
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
97
|
+
) => proxyRequest(req, ctx, 'POST');
|
|
98
|
+
|
|
99
|
+
export const PUT = (
|
|
100
|
+
req: NextRequest,
|
|
101
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
102
|
+
) => proxyRequest(req, ctx, 'PUT');
|
|
103
|
+
|
|
104
|
+
export const PATCH = (
|
|
105
|
+
req: NextRequest,
|
|
106
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
107
|
+
) => proxyRequest(req, ctx, 'PATCH');
|
|
108
|
+
|
|
109
|
+
export const DELETE = (
|
|
110
|
+
req: NextRequest,
|
|
111
|
+
ctx: { params: Promise<{ path: string[] }> },
|
|
112
|
+
) => proxyRequest(req, ctx, 'DELETE');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import '../src/lib/styles/tokens.css';
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
/* Tailwind v4 — 토큰 CSS 변수를 utility 클래스로 노출.
|
|
7
|
+
* `bg-primary` / `text-foreground` 같은 클래스가 토큰을 따라가도록 매핑.
|
|
8
|
+
* 옵셔널 색(success/warning/info)은 토큰에 없으면 변수가 undefined 라 browsers 가 단순 무시 — 안전.
|
|
9
|
+
*
|
|
10
|
+
* mes overlay — 베이스(`app/globals.css`)는 fsd 경로로 토큰을 import 하므로
|
|
11
|
+
* mes arch 에서는 이 파일이 베이스를 덮어 `src/lib/styles/tokens.css` 로 보낸다.
|
|
12
|
+
* 베이스 globals.css 내용 변경 시 이 파일도 동일하게 동기화할 것.
|
|
13
|
+
*/
|
|
14
|
+
@theme inline {
|
|
15
|
+
--color-background: var(--background);
|
|
16
|
+
--color-background-subtle: var(--background-subtle);
|
|
17
|
+
--color-background-muted: var(--background-muted);
|
|
18
|
+
--color-background-inverse: var(--background-inverse);
|
|
19
|
+
--color-foreground: var(--foreground);
|
|
20
|
+
--color-foreground-muted: var(--foreground-muted);
|
|
21
|
+
--color-foreground-subtle: var(--foreground-subtle);
|
|
22
|
+
--color-foreground-inverse: var(--foreground-inverse);
|
|
23
|
+
--color-border: var(--border);
|
|
24
|
+
--color-border-strong: var(--border-strong);
|
|
25
|
+
--color-primary: var(--primary);
|
|
26
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
27
|
+
--color-primary-hover: var(--primary-hover);
|
|
28
|
+
--color-ring: var(--ring);
|
|
29
|
+
--color-danger: var(--danger);
|
|
30
|
+
--color-danger-hover: var(--danger-hover);
|
|
31
|
+
--color-danger-foreground: var(--danger-foreground);
|
|
32
|
+
--color-success: var(--success);
|
|
33
|
+
--color-success-foreground: var(--success-foreground);
|
|
34
|
+
--color-warning: var(--warning);
|
|
35
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
36
|
+
--color-info: var(--info);
|
|
37
|
+
--color-info-foreground: var(--info-foreground);
|
|
38
|
+
--radius-sm: calc(var(--radius) - 2px);
|
|
39
|
+
--radius-md: var(--radius);
|
|
40
|
+
--radius-lg: calc(var(--radius) + 2px);
|
|
41
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@layer base {
|
|
45
|
+
body {
|
|
46
|
+
background: var(--background);
|
|
47
|
+
color: var(--foreground);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -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
|