sh-ui-cli 0.59.8 → 0.61.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 +53 -0
- package/data/registry/flutter/widgets/sh_ui_input.dart +0 -79
- package/data/registry/react/components/input/index.module.tsx +0 -70
- package/data/registry/react/components/input/index.tailwind.tsx +0 -53
- package/data/registry/react/components/input/index.tsx +0 -70
- package/data/registry/react/components/input/index.vanilla-extract.tsx +0 -63
- package/data/summaries/react.json +1 -1
- package/package.json +2 -2
- package/src/create/architectures/flat.js +1 -1
- package/src/create/generator.js +717 -17
- package/src/create/index.mjs +1 -1
- package/src/create/plugins/authJwt.js +51 -1
- package/src/create/plugins/nextIntl.js +171 -17
- package/src/create/plugins/sentry.js +43 -23
- package/src/mcp.mjs +2 -2
- package/templates/flutter-standalone/sh-ui.config.json +0 -1
- package/templates/monorepo/README.md +14 -5
- package/templates/monorepo/packages/eslint-config/flat.js +71 -0
- package/templates/monorepo/packages/eslint-config/fsd.js +0 -21
- package/templates/monorepo/packages/eslint-config/package.json +2 -3
- package/templates/monorepo/packages/typescript-config/package.json +6 -1
- package/templates/monorepo/packages/ui/ui-core/tsconfig.json +1 -1
- package/templates/monorepo/pnpm-workspace.yaml +2 -1
- package/templates/nextjs-app/.env.example +3 -2
- package/templates/nextjs-app/Dockerfile +36 -5
- package/templates/nextjs-app/README.md +9 -7
- package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-app/_arch/flat/app/layout.tsx +2 -2
- package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-app/_arch/flat/components/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-app/_arch/flat/components/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-app/_arch/flat/eslint.config.js +10 -0
- package/templates/nextjs-app/_arch/flat/lib/api/clientFetch.ts +4 -4
- package/templates/nextjs-app/_arch/flat/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-app/_arch/flat/lib/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-app/_arch/flat/lib/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-app/_arch/flat/lib/utils/formatDate.ts +4 -0
- package/templates/nextjs-app/_arch/flat/lib/utils/formatPrice.ts +13 -5
- package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +1 -1
- package/templates/nextjs-app/_arch/flat/tsconfig.json +0 -1
- package/templates/nextjs-app/_arch/fsd/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-app/_arch/fsd/app/layout.tsx +2 -2
- package/templates/nextjs-app/_arch/fsd/src/app/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-app/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-app/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-app/_arch/fsd/src/shared/api/clientFetch.ts +4 -4
- package/templates/nextjs-app/_arch/fsd/src/shared/api/errorMessages.ts +37 -0
- package/templates/nextjs-app/_arch/fsd/src/shared/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-app/_arch/fsd/src/shared/lib/formatDate.ts +4 -0
- package/templates/nextjs-app/_arch/fsd/src/shared/lib/formatPrice.ts +13 -5
- package/templates/nextjs-app/_arch/fsd/src/shared/lib/getQueryClient.ts +1 -1
- package/templates/nextjs-app/_arch/fsd/src/shared/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-app/_arch/fsd/src/shared/ui/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-app/vitest.config.ts +4 -0
- package/templates/nextjs-standalone/.env.example +3 -2
- package/templates/nextjs-standalone/Dockerfile +35 -0
- package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +2 -2
- package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-standalone/_arch/flat/eslint.config.js +123 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +4 -4
- package/templates/nextjs-standalone/_arch/flat/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +4 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +13 -5
- package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +1 -1
- package/templates/nextjs-standalone/_arch/flat/tsconfig.json +1 -2
- package/templates/nextjs-standalone/_arch/fsd/app/api/proxy/[...path]/route.ts +1 -1
- package/templates/nextjs-standalone/_arch/fsd/app/layout.tsx +2 -2
- package/templates/nextjs-standalone/_arch/fsd/src/app/layouts/RootLayout.tsx +2 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +3 -3
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +12 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +4 -4
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/errorMessages.ts +37 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +14 -7
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +4 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +13 -5
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/getQueryClient.ts +1 -1
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +32 -7
- package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/PrefetchBoundary/index.tsx +1 -1
- package/templates/nextjs-standalone/eslint.config.js +0 -15
- package/templates/nextjs-standalone/package.json +0 -2
- package/templates/ui-app-template/package.json +2 -2
- package/templates/ui-app-template/postcss.config.mjs +1 -1
- package/templates/monorepo/.eslintrc.js +0 -8
- package/templates/nextjs-app/_arch/flat/components/providers/theme/ThemeProviders.tsx +0 -12
- package/templates/nextjs-app/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +0 -12
- package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +0 -12
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +0 -12
package/src/create/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
|
|
|
27
27
|
--arch <${ARCHES_LIST}> 프로젝트 아키텍처 — 폴더 구조/import alias 컨벤션. next 에서 사용 가능: ${NEXT_ARCHES}. 기본 fsd
|
|
28
28
|
--plugins <a,b> 플러그인 (${PLUGINS_LIST}). 미지정/"" → 없음
|
|
29
29
|
--theme <preset|base64> 프리셋 이름(${THEME_PRESETS_LIST}) 또는 playground base64. 선택
|
|
30
|
-
--css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS
|
|
30
|
+
--css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS 프레임워크. base 파일까지 분기 emit (tailwind/plain/css-modules)
|
|
31
31
|
--yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
|
|
32
32
|
--dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
|
|
33
33
|
-h, --help 이 도움말
|
|
@@ -33,6 +33,56 @@ export const authJwtPlugin = {
|
|
|
33
33
|
// BFF 와 withAuthRetry 가 자동 활용한다.
|
|
34
34
|
|
|
35
35
|
files: (arch) => ({
|
|
36
|
+
// placeholder sign-in page — proxy.ts 가 미인증 요청을 /sign-in 으로 redirect
|
|
37
|
+
// 하므로, 페이지가 없으면 사용자가 dev 띄우자마자 무한 404 루프에 빠진다.
|
|
38
|
+
// 템플릿 단계에선 dev 우회용 "fake 토큰 set" 버튼 + 안내 문구로 최소화.
|
|
39
|
+
// next-intl 활성 시 nextIntl 플러그인의 transforms 가 [locale]/sign-in 으로 옮긴다.
|
|
40
|
+
'app/sign-in/page.tsx': `'use client';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* auth-jwt 플러그인의 placeholder sign-in 페이지.
|
|
44
|
+
*
|
|
45
|
+
* 실제 프로덕션에선 백엔드 sign-in API 와 연동하는 폼으로 교체해야 한다.
|
|
46
|
+
* 지금은 dev 시 미인증 redirect 루프를 끊기 위한 최소 페이지.
|
|
47
|
+
*/
|
|
48
|
+
export default function SignInPage() {
|
|
49
|
+
const setDevToken = () => {
|
|
50
|
+
document.cookie = 'accessToken=dev-placeholder; path=/; max-age=86400';
|
|
51
|
+
document.cookie = 'refreshToken=dev-placeholder; path=/; max-age=86400';
|
|
52
|
+
window.location.href = '/';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 토큰 기반 색상 — 다크/라이트 자동 적응. inline hex 사용 시 다크에서 시인성 깨짐.
|
|
56
|
+
return (
|
|
57
|
+
<main style={{ display: 'flex', minHeight: '100vh', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
|
|
58
|
+
<div style={{ maxWidth: 420, width: '100%', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
59
|
+
<h1 style={{ fontSize: 24, margin: 0, color: 'var(--foreground)' }}>Sign in</h1>
|
|
60
|
+
<p style={{ color: 'var(--foreground-muted)', fontSize: 14, lineHeight: 1.6, margin: 0 }}>
|
|
61
|
+
auth-jwt 플러그인 placeholder 페이지입니다. 실제 인증 연동 전까지는
|
|
62
|
+
아래 버튼으로 dev 용 가짜 토큰을 설정해 가드를 우회할 수 있습니다.
|
|
63
|
+
</p>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={setDevToken}
|
|
67
|
+
style={{
|
|
68
|
+
padding: '10px 16px',
|
|
69
|
+
borderRadius: 8,
|
|
70
|
+
border: '1px solid var(--border-strong)',
|
|
71
|
+
background: 'var(--primary)',
|
|
72
|
+
color: 'var(--primary-foreground)',
|
|
73
|
+
cursor: 'pointer',
|
|
74
|
+
fontSize: 14,
|
|
75
|
+
fontWeight: 500,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
Continue (dev — set fake token)
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</main>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
|
|
36
86
|
'proxy.ts': `import { NextRequest, NextResponse } from 'next/server';
|
|
37
87
|
|
|
38
88
|
const AUTH_ROUTES = ['/sign-in', '/sign-up'];
|
|
@@ -245,7 +295,7 @@ const proxyRequest = async (
|
|
|
245
295
|
data: null,
|
|
246
296
|
error: {
|
|
247
297
|
code: 'NETWORK_ERROR',
|
|
248
|
-
message: '
|
|
298
|
+
message: 'Failed to reach upstream server.',
|
|
249
299
|
},
|
|
250
300
|
},
|
|
251
301
|
{ status: 502 },
|
|
@@ -37,18 +37,103 @@ export const nextIntlPlugin = {
|
|
|
37
37
|
|
|
38
38
|
turboEnvVars: [],
|
|
39
39
|
|
|
40
|
-
// ───
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
// ─── providers 합성 ───
|
|
41
|
+
//
|
|
42
|
+
// NextIntlClientProvider 는 GlobalProvider 가 아니라 RootLayout 에서 직접 wrap 한다.
|
|
43
|
+
// 이유: RootLayout 만이 검증된 `locale: string` 을 가지므로 `<NextIntlClientProvider
|
|
44
|
+
// locale={locale}>` 처럼 prop 을 명시할 수 있다. GlobalProvider 단에서 wrap 하면
|
|
45
|
+
// locale 을 prop drilling 해야 해서 깔끔하지 않다. 대신 RootLayout 의 content
|
|
46
|
+
// (아래 transforms 의 replace) 가 NextIntlClientProvider 를 직접 import + wrap.
|
|
46
47
|
|
|
47
48
|
// ─── 라우트 구조 변환 ───
|
|
48
49
|
|
|
49
50
|
transforms: (arch) => [
|
|
50
51
|
{ type: 'move', from: 'app/page.tsx', to: 'app/[locale]/page.tsx' },
|
|
51
52
|
{ type: 'move', from: 'app/error.tsx', to: 'app/[locale]/error.tsx' },
|
|
53
|
+
// sentry 가 emit 한 error.tsx 를 i18n-aware 버전으로 교체.
|
|
54
|
+
// sentry 비활성이면 위 move 가 no-op 이라 [locale]/error.tsx 가 없고 이 replace 도 no-op.
|
|
55
|
+
{
|
|
56
|
+
type: 'replace',
|
|
57
|
+
path: 'app/[locale]/error.tsx',
|
|
58
|
+
contentFn: () => `'use client';
|
|
59
|
+
|
|
60
|
+
import * as Sentry from '@sentry/nextjs';
|
|
61
|
+
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
|
|
62
|
+
import { useTranslations } from 'next-intl';
|
|
63
|
+
import { useEffect } from 'react';
|
|
64
|
+
|
|
65
|
+
import { Link } from '${arch.aliases.config}/i18n/navigation';
|
|
66
|
+
|
|
67
|
+
export default function Error({
|
|
68
|
+
error,
|
|
69
|
+
reset,
|
|
70
|
+
}: {
|
|
71
|
+
error: Error & { digest?: string };
|
|
72
|
+
reset: () => void;
|
|
73
|
+
}) {
|
|
74
|
+
const t = useTranslations('error');
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
Sentry.captureException(error);
|
|
78
|
+
}, [error]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className='flex min-h-screen items-center justify-center px-4'>
|
|
82
|
+
<div className='border-border bg-background w-full max-w-md rounded-lg border p-6 shadow-lg'>
|
|
83
|
+
<div className='mb-4 flex justify-center'>
|
|
84
|
+
<div className='bg-danger/10 flex h-16 w-16 items-center justify-center rounded-full'>
|
|
85
|
+
<AlertTriangle className='text-danger h-8 w-8' />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<h2 className='text-foreground mb-2 text-center text-2xl font-bold'>
|
|
90
|
+
{t('title')}
|
|
91
|
+
</h2>
|
|
92
|
+
<p className='text-foreground-muted mb-6 text-center text-sm'>
|
|
93
|
+
{t('description')}
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
<div className='border-danger/30 bg-danger/5 rounded-md border p-3'>
|
|
97
|
+
<p className='text-danger text-sm'>
|
|
98
|
+
{error.message || t('unexpectedError')}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className='mt-6 space-y-3'>
|
|
103
|
+
<button
|
|
104
|
+
onClick={reset}
|
|
105
|
+
className='bg-primary text-primary-foreground hover:bg-primary-hover flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium'
|
|
106
|
+
>
|
|
107
|
+
<RefreshCw className='h-4 w-4' />
|
|
108
|
+
{t('button.tryAgain')}
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
<Link
|
|
112
|
+
href='/'
|
|
113
|
+
className='border-border text-foreground hover:bg-background-muted flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium'
|
|
114
|
+
>
|
|
115
|
+
<Home className='h-4 w-4' />
|
|
116
|
+
{t('button.goHome')}
|
|
117
|
+
</Link>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{process.env.NODE_ENV === 'development' && error.digest && (
|
|
121
|
+
<div className='bg-background-subtle mt-4 rounded-md p-3'>
|
|
122
|
+
<p className='text-foreground-subtle text-xs'>
|
|
123
|
+
Error ID: {error.digest}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
},
|
|
133
|
+
// auth-jwt 플러그인이 함께 활성화돼 있으면 sign-in 도 locale prefix 안으로 이동.
|
|
134
|
+
// auth-jwt 단독일 때 (= 이 transform 이 없을 때) 는 app/sign-in 이 그대로 root 에 남는다.
|
|
135
|
+
// `move` 트랜스폼은 from 파일이 없으면 silently skip 이라 auth-jwt 미활성 시 안전한 no-op.
|
|
136
|
+
{ type: 'move', from: 'app/sign-in/page.tsx', to: 'app/[locale]/sign-in/page.tsx' },
|
|
52
137
|
{
|
|
53
138
|
// Next 16 부터 root layout (app/layout.tsx) 은 반드시 <html>/<body> 를 가져야 한다.
|
|
54
139
|
// next-intl 적용 시에는 [locale] 가 root 역할을 맡으므로, 기본 app/layout.tsx 를 그대로
|
|
@@ -64,29 +149,38 @@ export const nextIntlPlugin = {
|
|
|
64
149
|
// side-effect import (`import 'x';` 형태, binding 없음) 만 보존하고 나머지는 통째 교체.
|
|
65
150
|
// 이름 있는 import (예: `import { RootLayout } from ...`) 는 새 본체와 식별자 충돌 가능성이
|
|
66
151
|
// 있어 제외.
|
|
152
|
+
//
|
|
153
|
+
// 경로 보정: 파일이 `app/layout.tsx` → `app/[locale]/layout.tsx` 로 1단계 깊어졌으므로
|
|
154
|
+
// 보존된 side-effect import 의 상대 경로(`./x` / `../x`)는 `../` 한 번만큼 더 위로 끌어올린다.
|
|
155
|
+
// 절대 경로(`/x`)나 모듈명(`polyfills`)은 그대로. v0.59.8 까지는 보정이 빠져 standalone+next-intl
|
|
156
|
+
// 조합에서 `./globals.css` 가 깨졌고 prod 빌드가 실패했다.
|
|
67
157
|
type: 'replace',
|
|
68
158
|
path: 'app/[locale]/layout.tsx',
|
|
69
159
|
contentFn: (existing) => {
|
|
160
|
+
const adjustRelative = (line) =>
|
|
161
|
+
line.replace(/(['"])(\.\.?\/)/g, (_m, q, prefix) => `${q}../${prefix === './' ? '' : prefix}`);
|
|
70
162
|
const sideEffectImports = existing
|
|
71
163
|
.split('\n')
|
|
72
164
|
.filter((line) => /^\s*import\s+['"][^'"]+['"];?\s*$/.test(line))
|
|
165
|
+
.map(adjustRelative)
|
|
73
166
|
.join('\n');
|
|
74
167
|
const body = `import type { Metadata } from 'next';
|
|
75
168
|
import { RootLayout } from '${arch.aliases.layouts}/RootLayout';
|
|
76
169
|
|
|
77
170
|
export const metadata: Metadata = {
|
|
78
|
-
title: '
|
|
79
|
-
description: '
|
|
171
|
+
title: 'sh-ui app',
|
|
172
|
+
description: 'sh-ui 기반 앱 — metadata 를 변경하세요.',
|
|
80
173
|
};
|
|
81
174
|
|
|
82
|
-
export default function Layout({
|
|
175
|
+
export default async function Layout({
|
|
83
176
|
children,
|
|
84
177
|
params,
|
|
85
178
|
}: Readonly<{
|
|
86
179
|
children: React.ReactNode;
|
|
87
180
|
params: Promise<{ locale: string }>;
|
|
88
181
|
}>) {
|
|
89
|
-
|
|
182
|
+
const { locale } = await params;
|
|
183
|
+
return <RootLayout locale={locale}>{children}</RootLayout>;
|
|
90
184
|
}
|
|
91
185
|
`;
|
|
92
186
|
return sideEffectImports ? `${sideEffectImports}\n\n${body}` : body;
|
|
@@ -95,20 +189,26 @@ export default function Layout({
|
|
|
95
189
|
{
|
|
96
190
|
type: 'replace',
|
|
97
191
|
path: `${arch.paths.layouts}/RootLayout.tsx`,
|
|
98
|
-
content: `import { hasLocale } from 'next-intl';
|
|
192
|
+
content: `import { hasLocale, NextIntlClientProvider } from 'next-intl';
|
|
99
193
|
import { notFound } from 'next/navigation';
|
|
100
194
|
import { GlobalProvider } from '${arch.aliases.providers}';
|
|
101
195
|
import { routing } from '${arch.aliases.config}/i18n/routing';
|
|
102
196
|
|
|
103
|
-
|
|
197
|
+
/**
|
|
198
|
+
* 루트 셸 — html/body + 전역 Provider. 로케일 검증은 여기서 한 번만.
|
|
199
|
+
* 호출자([locale]/layout.tsx) 가 이미 \`await params\` 로 string 을 풀어 넘긴다.
|
|
200
|
+
*
|
|
201
|
+
* NextIntlClientProvider 는 GlobalProvider 바깥(html 안쪽) 에서 \`locale\` prop 과
|
|
202
|
+
* 함께 직접 wrap. 자동 detect 로 두면 client 컴포넌트에서 useLocale() 결과가
|
|
203
|
+
* RSC 컨텍스트와 어긋날 수 있어 명시적으로 전달한다.
|
|
204
|
+
*/
|
|
205
|
+
export function RootLayout({
|
|
104
206
|
children,
|
|
105
|
-
|
|
207
|
+
locale,
|
|
106
208
|
}: {
|
|
107
209
|
children: React.ReactNode;
|
|
108
|
-
|
|
210
|
+
locale: string;
|
|
109
211
|
}) {
|
|
110
|
-
const { locale } = await params;
|
|
111
|
-
|
|
112
212
|
if (!hasLocale(routing.locales, locale)) {
|
|
113
213
|
notFound();
|
|
114
214
|
}
|
|
@@ -116,7 +216,9 @@ export async function RootLayout({
|
|
|
116
216
|
return (
|
|
117
217
|
<html lang={locale} suppressHydrationWarning>
|
|
118
218
|
<body>
|
|
119
|
-
<
|
|
219
|
+
<NextIntlClientProvider locale={locale}>
|
|
220
|
+
<GlobalProvider>{children}</GlobalProvider>
|
|
221
|
+
</NextIntlClientProvider>
|
|
120
222
|
</body>
|
|
121
223
|
</html>
|
|
122
224
|
);
|
|
@@ -232,6 +334,58 @@ export default intl;
|
|
|
232
334
|
export const config = {
|
|
233
335
|
matcher: '/((?!api|trpc|_next|_vercel|monitoring|.*\\\\..*).*)',
|
|
234
336
|
};
|
|
337
|
+
`,
|
|
338
|
+
|
|
339
|
+
// ─── i18n-aware formatter hooks ───
|
|
340
|
+
//
|
|
341
|
+
// base 템플릿의 \`formatDate\` / \`formatPrice\` util 은 default 'ko-KR' / 'KRW'.
|
|
342
|
+
// next-intl 활성 시엔 hook 으로 현재 locale 자동 추적.
|
|
343
|
+
|
|
344
|
+
[`${arch.paths.hooks}/useFormatDate.ts`]: `'use client';
|
|
345
|
+
|
|
346
|
+
import { useCallback } from 'react';
|
|
347
|
+
import { useLocale } from 'next-intl';
|
|
348
|
+
|
|
349
|
+
import {
|
|
350
|
+
formatDate as formatDateUtil,
|
|
351
|
+
formatDateTime as formatDateTimeUtil,
|
|
352
|
+
} from '${arch.aliases.utils}/formatDate';
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 현재 locale 을 자동으로 따르는 날짜 포맷 hook.
|
|
356
|
+
* 'use client' 필요 — RSC 에선 \`getLocale()\` 로 직접 util 호출.
|
|
357
|
+
*/
|
|
358
|
+
export function useFormatDate() {
|
|
359
|
+
const locale = useLocale();
|
|
360
|
+
return useCallback((date: Date) => formatDateUtil(date, locale), [locale]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function useFormatDateTime() {
|
|
364
|
+
const locale = useLocale();
|
|
365
|
+
return useCallback((date: Date) => formatDateTimeUtil(date, locale), [locale]);
|
|
366
|
+
}
|
|
367
|
+
`,
|
|
368
|
+
|
|
369
|
+
[`${arch.paths.hooks}/useFormatPrice.ts`]: `'use client';
|
|
370
|
+
|
|
371
|
+
import { useCallback } from 'react';
|
|
372
|
+
import { useLocale } from 'next-intl';
|
|
373
|
+
|
|
374
|
+
import { formatPrice as formatPriceUtil } from '${arch.aliases.utils}/formatPrice';
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 현재 locale 을 자동으로 따르는 통화 포맷 hook.
|
|
378
|
+
* currency 는 비즈니스 의존이라 인자로 받음 — 사용처에서 명시.
|
|
379
|
+
*
|
|
380
|
+
* 예: const fp = useFormatPrice('USD'); fp(99.5) → "$99.50" (locale 'en-US' 시)
|
|
381
|
+
*/
|
|
382
|
+
export function useFormatPrice(currency = 'KRW') {
|
|
383
|
+
const locale = useLocale();
|
|
384
|
+
return useCallback(
|
|
385
|
+
(amount: number) => formatPriceUtil(amount, locale, currency),
|
|
386
|
+
[locale, currency],
|
|
387
|
+
);
|
|
388
|
+
}
|
|
235
389
|
`,
|
|
236
390
|
}),
|
|
237
391
|
};
|
|
@@ -65,6 +65,8 @@ export const sentryPlugin = {
|
|
|
65
65
|
files: (arch) => ({
|
|
66
66
|
'sentry.server.config.ts': `import * as Sentry from '@sentry/nextjs';
|
|
67
67
|
|
|
68
|
+
import { ApiError } from '${arch.aliases.api}/error';
|
|
69
|
+
|
|
68
70
|
Sentry.init({
|
|
69
71
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
70
72
|
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? 'dev',
|
|
@@ -77,10 +79,8 @@ Sentry.init({
|
|
|
77
79
|
|
|
78
80
|
const error = hint?.originalException;
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (status === 401) return null;
|
|
83
|
-
}
|
|
82
|
+
// 401 (인증 실패) 은 비즈니스 흐름 — Sentry 로 안 보냄.
|
|
83
|
+
if (error instanceof ApiError && error.status === 401) return null;
|
|
84
84
|
|
|
85
85
|
if (event.exception?.values?.[0]?.value?.includes('An error occurred in the Server Components render')) {
|
|
86
86
|
return null;
|
|
@@ -186,6 +186,8 @@ Sentry.init({
|
|
|
186
186
|
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
|
187
187
|
`,
|
|
188
188
|
|
|
189
|
+
// global-error.tsx — root layout 자체가 깨졌을 때의 fallback. Tailwind/CSS 가 로드 안 될 수
|
|
190
|
+
// 있어 inline style 위주. 한국어로 통일 ([locale]/error.tsx 와 동일 메시지 톤).
|
|
189
191
|
'app/global-error.tsx': `'use client';
|
|
190
192
|
|
|
191
193
|
import * as Sentry from '@sentry/nextjs';
|
|
@@ -201,13 +203,23 @@ export default function GlobalError({
|
|
|
201
203
|
}, [error]);
|
|
202
204
|
|
|
203
205
|
return (
|
|
204
|
-
<html>
|
|
205
|
-
<body
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
<html lang='ko'>
|
|
207
|
+
<body
|
|
208
|
+
style={{
|
|
209
|
+
margin: 0,
|
|
210
|
+
minHeight: '100vh',
|
|
211
|
+
display: 'flex',
|
|
212
|
+
alignItems: 'center',
|
|
213
|
+
justifyContent: 'center',
|
|
214
|
+
fontFamily:
|
|
215
|
+
'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
<div style={{ textAlign: 'center', padding: 24 }}>
|
|
219
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, margin: 0 }}>
|
|
220
|
+
오류가 발생했습니다
|
|
221
|
+
</h1>
|
|
222
|
+
<p style={{ marginTop: 8, color: '#6b7280' }}>{error.message}</p>
|
|
211
223
|
</div>
|
|
212
224
|
</body>
|
|
213
225
|
</html>
|
|
@@ -215,6 +227,10 @@ export default function GlobalError({
|
|
|
215
227
|
}
|
|
216
228
|
`,
|
|
217
229
|
|
|
230
|
+
// [locale] 안으로 이동 시 next-intl 플러그인이 i18n-aware 버전으로 replace 함.
|
|
231
|
+
// 여기 emit 되는 버전은 next-intl 미활성 케이스용 — 한국어 + 토큰 기반 색상.
|
|
232
|
+
// \`bg-accent\` 같은 미정의 토큰은 사용하지 않고 \`--background-muted\` / \`--danger\`
|
|
233
|
+
// 등 tokens.css 에 실재하는 변수만 사용.
|
|
218
234
|
'app/error.tsx': `'use client';
|
|
219
235
|
|
|
220
236
|
import * as Sentry from '@sentry/nextjs';
|
|
@@ -235,20 +251,22 @@ export default function Error({
|
|
|
235
251
|
|
|
236
252
|
return (
|
|
237
253
|
<div className='flex min-h-screen items-center justify-center px-4'>
|
|
238
|
-
<div className='w-full max-w-md rounded-lg border p-6 shadow-lg'>
|
|
254
|
+
<div className='border-border bg-background w-full max-w-md rounded-lg border p-6 shadow-lg'>
|
|
239
255
|
<div className='mb-4 flex justify-center'>
|
|
240
|
-
<div className='flex h-16 w-16 items-center justify-center rounded-full
|
|
241
|
-
<AlertTriangle className='h-8 w-8
|
|
256
|
+
<div className='bg-danger/10 flex h-16 w-16 items-center justify-center rounded-full'>
|
|
257
|
+
<AlertTriangle className='text-danger h-8 w-8' />
|
|
242
258
|
</div>
|
|
243
259
|
</div>
|
|
244
260
|
|
|
245
|
-
<h2 className='mb-2 text-center text-2xl font-bold'
|
|
246
|
-
|
|
261
|
+
<h2 className='text-foreground mb-2 text-center text-2xl font-bold'>
|
|
262
|
+
오류가 발생했습니다
|
|
263
|
+
</h2>
|
|
264
|
+
<p className='text-foreground-muted mb-6 text-center text-sm'>
|
|
247
265
|
예상치 못한 오류가 발생했습니다. 다시 시도해주세요.
|
|
248
266
|
</p>
|
|
249
267
|
|
|
250
|
-
<div className='
|
|
251
|
-
<p className='text-
|
|
268
|
+
<div className='border-danger/30 bg-danger/5 rounded-md border p-3'>
|
|
269
|
+
<p className='text-danger text-sm'>
|
|
252
270
|
{error.message || '알 수 없는 오류'}
|
|
253
271
|
</p>
|
|
254
272
|
</div>
|
|
@@ -256,7 +274,7 @@ export default function Error({
|
|
|
256
274
|
<div className='mt-6 space-y-3'>
|
|
257
275
|
<button
|
|
258
276
|
onClick={reset}
|
|
259
|
-
className='flex w-full items-center justify-center gap-2 rounded-md
|
|
277
|
+
className='bg-primary text-primary-foreground hover:bg-primary-hover flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium'
|
|
260
278
|
>
|
|
261
279
|
<RefreshCw className='h-4 w-4' />
|
|
262
280
|
다시 시도
|
|
@@ -264,7 +282,7 @@ export default function Error({
|
|
|
264
282
|
|
|
265
283
|
<Link
|
|
266
284
|
href='/'
|
|
267
|
-
className='flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium
|
|
285
|
+
className='border-border text-foreground hover:bg-background-muted flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium'
|
|
268
286
|
>
|
|
269
287
|
<Home className='h-4 w-4' />
|
|
270
288
|
홈으로 이동
|
|
@@ -272,8 +290,8 @@ export default function Error({
|
|
|
272
290
|
</div>
|
|
273
291
|
|
|
274
292
|
{process.env.NODE_ENV === 'development' && error.digest && (
|
|
275
|
-
<div className='mt-4 rounded-md
|
|
276
|
-
<p className='text-
|
|
293
|
+
<div className='bg-background-subtle mt-4 rounded-md p-3'>
|
|
294
|
+
<p className='text-foreground-subtle text-xs'>
|
|
277
295
|
Error ID: {error.digest}
|
|
278
296
|
</p>
|
|
279
297
|
</div>
|
|
@@ -436,7 +454,9 @@ export const logApiError = (prefix: string, params: ApiLogParams): void => {
|
|
|
436
454
|
console.error(\`- Status: \${status ?? 'N/A'}\`);
|
|
437
455
|
|
|
438
456
|
if (requestHeaders) {
|
|
439
|
-
|
|
457
|
+
// Authorization 토큰은 로그에서 가린다.
|
|
458
|
+
const { Authorization: _authorization, ...safeHeaders } = requestHeaders;
|
|
459
|
+
void _authorization;
|
|
440
460
|
console.error('- Request Headers:', safeHeaders);
|
|
441
461
|
}
|
|
442
462
|
if (requestBody) console.error('- Request Body:', requestBody);
|
package/src/mcp.mjs
CHANGED
|
@@ -239,7 +239,7 @@ export async function startMcpServer() {
|
|
|
239
239
|
theme: z.string().optional()
|
|
240
240
|
.describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 사용자가 톤을 직접 손본 결과를 영구 보관하려면 sh_ui_encode_theme 으로 base64 를 만들어 여기에 넘긴다.`),
|
|
241
241
|
cssFramework: z.enum(CSS_FRAMEWORKS).optional()
|
|
242
|
-
.describe(`CSS 프레임워크. 기본 plain.
|
|
242
|
+
.describe(`CSS 프레임워크. 기본 plain. base 파일까지 분기 emit (plain/tailwind/css-modules) + 컴포넌트 변종까지 결정. 변종 미보유 시 add 는 plain fallback`),
|
|
243
243
|
cwd: z.string().optional()
|
|
244
244
|
.describe("부모 디렉토리. 기본 process.cwd()"),
|
|
245
245
|
force: z.boolean().optional()
|
|
@@ -311,7 +311,7 @@ export async function startMcpServer() {
|
|
|
311
311
|
mode: z.enum(MODES).optional()
|
|
312
312
|
.describe("색 모드. 기본 light-dark"),
|
|
313
313
|
cssFramework: z.enum(CSS_FRAMEWORKS).optional()
|
|
314
|
-
.describe(`CSS 프레임워크. 기본 plain.
|
|
314
|
+
.describe(`CSS 프레임워크. 기본 plain. base 파일까지 분기 emit (plain/tailwind/css-modules) + 컴포넌트 변종까지 결정. 변종 미보유 시 add 는 plain fallback`),
|
|
315
315
|
cwd: z.string().optional()
|
|
316
316
|
.describe("작업 디렉토리. 기본 process.cwd()"),
|
|
317
317
|
force: z.boolean().optional()
|
|
@@ -8,8 +8,17 @@ Turborepo + pnpm workspace 기반 모노레포 템플릿 (sh-ui 기반).
|
|
|
8
8
|
- **pnpm 10** (워크스페이스 패키지 매니저)
|
|
9
9
|
- **TypeScript 5.9**
|
|
10
10
|
- **sh-ui** (앱별 독립 테마 — 각 `ui-{app}/` 패키지가 자체 `sh-ui.config.json` 보유)
|
|
11
|
-
- **ESLint 9** (flat config)
|
|
12
|
-
- **Prettier** (tailwind
|
|
11
|
+
- **ESLint 9** (flat config — fsd / flat arch 별 boundaries 규칙)
|
|
12
|
+
- **Prettier** (tailwind 사용 시에만 prettier-plugin-tailwindcss 활성)
|
|
13
|
+
|
|
14
|
+
## CSS 프레임워크 선택
|
|
15
|
+
|
|
16
|
+
`sh-ui-cli create --css <plain | tailwind | css-modules>` 옵션이 **base 파일까지** 분기합니다.
|
|
17
|
+
- `tailwind` (기본): Tailwind v4 + utility class 기반 페이지/스타일
|
|
18
|
+
- `plain`: Tailwind 의존 없음, inline style + 토큰 변수 사용
|
|
19
|
+
- `css-modules`: `.module.css` 파일 + 클래스 import 패턴
|
|
20
|
+
|
|
21
|
+
같은 옵션이 `npx sh-ui-cli add <component>` 로 추가되는 컴포넌트의 styling 변종도 결정합니다.
|
|
13
22
|
|
|
14
23
|
## 프로젝트 구조
|
|
15
24
|
|
|
@@ -37,7 +46,8 @@ Turborepo + pnpm workspace 기반 모노레포 템플릿 (sh-ui 기반).
|
|
|
37
46
|
│ │ ├── base.js # 기본 (TS + Turbo + Prettier)
|
|
38
47
|
│ │ ├── next.js # Next.js 앱용
|
|
39
48
|
│ │ ├── react-internal.js # React 라이브러리용
|
|
40
|
-
│ │
|
|
49
|
+
│ │ ├── fsd.js # FSD 레이어 규칙 (boundaries, 파일 네이밍)
|
|
50
|
+
│ │ └── flat.js # flat arch 규칙 (lib/components/app boundaries)
|
|
41
51
|
│ │
|
|
42
52
|
│ └── typescript-config/ # 공유 TypeScript 설정
|
|
43
53
|
│ ├── base.json # 기본 (strict, ES2022)
|
|
@@ -45,9 +55,8 @@ Turborepo + pnpm workspace 기반 모노레포 템플릿 (sh-ui 기반).
|
|
|
45
55
|
│ └── react-library.json # React 라이브러리용 (react-jsx)
|
|
46
56
|
│
|
|
47
57
|
├── turbo.json # Turbo 태스크 파이프라인
|
|
48
|
-
├── pnpm-workspace.yaml # apps/* + packages
|
|
58
|
+
├── pnpm-workspace.yaml # apps/* + packages 명시 매핑
|
|
49
59
|
├── .prettierrc
|
|
50
|
-
├── .eslintrc.js
|
|
51
60
|
├── .gitignore
|
|
52
61
|
└── .dockerignore
|
|
53
62
|
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import boundaries from "eslint-plugin-boundaries"
|
|
2
|
+
import checkFile from "eslint-plugin-check-file"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flat arch ESLint configuration.
|
|
6
|
+
*
|
|
7
|
+
* FSD 의 슬라이스 boundaries 대신 flat 구조 (`lib/`, `components/`, `app/`) 의
|
|
8
|
+
* 단순한 의존 방향을 강제한다:
|
|
9
|
+
*
|
|
10
|
+
* - `lib/` — 다른 lib 만 import (UI 모름)
|
|
11
|
+
* - `components/` — components / lib 만
|
|
12
|
+
* - `app/` — components / lib 만 (Next.js routes)
|
|
13
|
+
*
|
|
14
|
+
* @type {import("eslint").Linter.Config[]}
|
|
15
|
+
*/
|
|
16
|
+
export const flatConfig = [
|
|
17
|
+
// ── boundaries ──
|
|
18
|
+
{
|
|
19
|
+
plugins: { boundaries },
|
|
20
|
+
settings: {
|
|
21
|
+
"import/resolver": {
|
|
22
|
+
typescript: { alwaysTryTypes: true },
|
|
23
|
+
},
|
|
24
|
+
"boundaries/elements": [
|
|
25
|
+
{ type: "lib", pattern: ["lib/*"], mode: "folder" },
|
|
26
|
+
{ type: "components", pattern: ["components/*"], mode: "folder" },
|
|
27
|
+
{ type: "app", pattern: ["app"], mode: "folder" },
|
|
28
|
+
],
|
|
29
|
+
"boundaries/ignore": ["**/*.test.*", "**/*.spec.*"],
|
|
30
|
+
},
|
|
31
|
+
rules: {
|
|
32
|
+
"boundaries/element-types": [
|
|
33
|
+
"warn",
|
|
34
|
+
{
|
|
35
|
+
default: "disallow",
|
|
36
|
+
rules: [
|
|
37
|
+
{ from: "app", allow: ["components", "lib"] },
|
|
38
|
+
{ from: "components", allow: ["components", "lib"] },
|
|
39
|
+
{ from: "lib", allow: ["lib"] },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// ── check-file (flat 의 lib 는 CAMEL_CASE, components 는 PASCAL_CASE) ──
|
|
47
|
+
{
|
|
48
|
+
plugins: { "check-file": checkFile },
|
|
49
|
+
rules: {
|
|
50
|
+
"check-file/filename-naming-convention": [
|
|
51
|
+
"error",
|
|
52
|
+
{
|
|
53
|
+
"**/components/**/*.tsx": "PASCAL_CASE",
|
|
54
|
+
"**/lib/**/*.ts": "CAMEL_CASE",
|
|
55
|
+
},
|
|
56
|
+
{ ignoreMiddleExtensions: true },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
files: [
|
|
62
|
+
"**/index.tsx", "**/index.ts",
|
|
63
|
+
"**/layout.tsx", "**/page.tsx",
|
|
64
|
+
"**/error.tsx", "**/not-found.tsx",
|
|
65
|
+
"**/routing.ts", "**/navigation.ts", "**/request.ts",
|
|
66
|
+
],
|
|
67
|
+
rules: {
|
|
68
|
+
"check-file/filename-naming-convention": "off",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import boundaries from "eslint-plugin-boundaries"
|
|
2
|
-
import importX from "eslint-plugin-import-x"
|
|
3
2
|
import checkFile from "eslint-plugin-check-file"
|
|
4
|
-
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* FSD (Feature-Sliced Design) ESLint configuration.
|
|
8
6
|
* boundaries: layer import direction + public API enforcement
|
|
9
|
-
* import-x: import group ordering
|
|
10
7
|
* check-file: file/folder naming convention enforcement
|
|
11
8
|
*
|
|
12
9
|
* @type {import("eslint").Linter.Config[]}
|
|
@@ -98,22 +95,4 @@ export const fsdConfig = [
|
|
|
98
95
|
"check-file/filename-naming-convention": "off",
|
|
99
96
|
},
|
|
100
97
|
},
|
|
101
|
-
|
|
102
|
-
// ── import-x plugin ──
|
|
103
|
-
{
|
|
104
|
-
plugins: {
|
|
105
|
-
"import-x": importX,
|
|
106
|
-
},
|
|
107
|
-
settings: {
|
|
108
|
-
"import-x/resolver-next": [
|
|
109
|
-
createTypeScriptImportResolver({
|
|
110
|
-
alwaysTryTypes: true,
|
|
111
|
-
}),
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
rules: {
|
|
115
|
-
"import-x/order": "off",
|
|
116
|
-
"import-x/no-unresolved": "off",
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
98
|
]
|