sh-ui-cli 0.76.0 → 0.78.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 +58 -0
- package/data/registry/react/components/code-editor/index.module.tsx +7 -1
- package/data/registry/react/components/code-editor/index.tailwind.tsx +5 -2
- package/data/registry/react/components/code-editor/index.tsx +7 -1
- package/data/registry/react/components/markdown-editor/index.module.tsx +1 -1
- package/data/registry/react/components/markdown-editor/index.tailwind.tsx +1 -1
- package/data/registry/react/components/markdown-editor/index.tsx +1 -1
- package/data/registry/react/components/select/index.module.tsx +20 -9
- package/data/registry/react/components/select/index.tailwind.tsx +21 -8
- package/data/registry/react/components/select/index.tsx +28 -9
- package/data/registry/react/components/sidebar/index.module.tsx +10 -6
- package/data/registry/react/components/sidebar/index.tailwind.tsx +10 -6
- package/data/registry/react/components/sidebar/index.tsx +20 -4
- 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/registry.json +2 -1
- package/data/registry/react/tokens-used.json +3 -1
- package/data/tokens/src/semantic.json +16 -2
- package/package.json +1 -1
- package/src/create/architectures/index.js +2 -1
- package/src/create/architectures/mes.js +53 -0
- package/src/create/generator.js +61 -8
- package/src/create/plugins/authJwt.js +10 -0
- package/src/create/plugins/nextIntl.js +36 -2
- package/src/mcp.mjs +66 -1
- package/templates/monorepo/packages/eslint-config/mes.js +82 -0
- package/templates/monorepo/packages/eslint-config/package.json +2 -1
- package/templates/monorepo/packages/ui/ui-core/package.json +1 -1
- package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +6 -0
- package/templates/nextjs-app/_arch/fsd/src/app/layouts/RootLayout.tsx +11 -0
- 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 +19 -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/flat/app/globals.css +5 -0
- package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +6 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/layouts/RootLayout.tsx +6 -0
- package/templates/nextjs-standalone/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-standalone/_arch/mes/app/globals.css +54 -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 +19 -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
- package/templates/nextjs-standalone/app/globals.css +5 -0
- package/templates/ui-app-template/src/styles/globals.css +5 -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
|
+
}
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
--color-warning-foreground: var(--warning-foreground);
|
|
36
36
|
--color-info: var(--info);
|
|
37
37
|
--color-info-foreground: var(--info-foreground);
|
|
38
|
+
--color-sidebar-bg: var(--sidebar-bg);
|
|
39
|
+
--color-sidebar-fg: var(--sidebar-fg);
|
|
40
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
41
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
42
|
+
--color-sidebar-accent-fg: var(--sidebar-accent-fg);
|
|
38
43
|
--radius-sm: calc(var(--radius) - 2px);
|
|
39
44
|
--radius-md: var(--radius);
|
|
40
45
|
--radius-lg: calc(var(--radius) + 2px);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { GlobalProvider } from '@/components/providers';
|
|
2
2
|
|
|
3
|
+
/** FOUC 차단 — next-themes mount 전에 첫 paint 에 dark/light class 박기. */
|
|
4
|
+
const themeInitScript = `try{var t=localStorage.getItem('theme');var c=document.documentElement.classList;if(t==='dark'||(!t&&matchMedia('(prefers-color-scheme:dark)').matches)){c.add('dark');}else if(t==='light'){c.add('light');}}catch(e){}`;
|
|
5
|
+
|
|
3
6
|
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
4
7
|
// `lang` 은 앱의 주 언어. 영어 등 다른 언어 우선이면 'en' 으로 바꾸거나
|
|
5
8
|
// next-intl 플러그인을 활성화해 라우트 기반 자동 분기로 전환하세요.
|
|
6
9
|
return (
|
|
7
10
|
<html lang='ko' suppressHydrationWarning>
|
|
11
|
+
<head>
|
|
12
|
+
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
|
13
|
+
</head>
|
|
8
14
|
<body>
|
|
9
15
|
<GlobalProvider>{children}</GlobalProvider>
|
|
10
16
|
</body>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { GlobalProvider } from '@/src/app/providers';
|
|
2
2
|
|
|
3
|
+
/** FOUC 차단 — next-themes mount 전에 첫 paint 에 dark/light class 박기. */
|
|
4
|
+
const themeInitScript = `try{var t=localStorage.getItem('theme');var c=document.documentElement.classList;if(t==='dark'||(!t&&matchMedia('(prefers-color-scheme:dark)').matches)){c.add('dark');}else if(t==='light'){c.add('light');}}catch(e){}`;
|
|
5
|
+
|
|
3
6
|
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
4
7
|
// `lang` 은 앱의 주 언어. 영어 등 다른 언어 우선이면 'en' 으로 바꾸거나
|
|
5
8
|
// next-intl 플러그인을 활성화해 라우트 기반 자동 분기로 전환하세요.
|
|
6
9
|
return (
|
|
7
10
|
<html lang='ko' suppressHydrationWarning>
|
|
11
|
+
<head>
|
|
12
|
+
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
|
13
|
+
</head>
|
|
8
14
|
<body>
|
|
9
15
|
<GlobalProvider>{children}</GlobalProvider>
|
|
10
16
|
</body>
|
|
@@ -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,54 @@
|
|
|
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
|
+
--color-sidebar-bg: var(--sidebar-bg);
|
|
39
|
+
--color-sidebar-fg: var(--sidebar-fg);
|
|
40
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
41
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
42
|
+
--color-sidebar-accent-fg: var(--sidebar-accent-fg);
|
|
43
|
+
--radius-sm: calc(var(--radius) - 2px);
|
|
44
|
+
--radius-md: var(--radius);
|
|
45
|
+
--radius-lg: calc(var(--radius) + 2px);
|
|
46
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@layer base {
|
|
50
|
+
body {
|
|
51
|
+
background: var(--background);
|
|
52
|
+
color: var(--foreground);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -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,19 @@
|
|
|
1
|
+
import { GlobalProvider } from '@/components/providers';
|
|
2
|
+
|
|
3
|
+
/** FOUC 차단 — next-themes mount 전에 첫 paint 에 dark/light class 박기. */
|
|
4
|
+
const themeInitScript = `try{var t=localStorage.getItem('theme');var c=document.documentElement.classList;if(t==='dark'||(!t&&matchMedia('(prefers-color-scheme:dark)').matches)){c.add('dark');}else if(t==='light'){c.add('light');}}catch(e){}`;
|
|
5
|
+
|
|
6
|
+
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
7
|
+
// `lang` 은 앱의 주 언어. 영어 등 다른 언어 우선이면 'en' 으로 바꾸거나
|
|
8
|
+
// next-intl 플러그인을 활성화해 라우트 기반 자동 분기로 전환하세요.
|
|
9
|
+
return (
|
|
10
|
+
<html lang='ko' suppressHydrationWarning>
|
|
11
|
+
<head>
|
|
12
|
+
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<GlobalProvider>{children}</GlobalProvider>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
);
|
|
19
|
+
}
|
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
|
+
}
|