sh-ui-cli 0.59.9 → 0.61.1

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.
Files changed (94) hide show
  1. package/data/changelog/versions.json +53 -0
  2. package/data/registry/flutter/widgets/sh_ui_input.dart +0 -79
  3. package/data/registry/react/components/input/index.module.tsx +0 -70
  4. package/data/registry/react/components/input/index.tailwind.tsx +0 -53
  5. package/data/registry/react/components/input/index.tsx +0 -70
  6. package/data/registry/react/components/input/index.vanilla-extract.tsx +0 -63
  7. package/data/summaries/react.json +1 -1
  8. package/package.json +2 -2
  9. package/src/create/architectures/flat.js +1 -1
  10. package/src/create/generator.js +717 -26
  11. package/src/create/index.mjs +1 -1
  12. package/src/create/plugins/authJwt.js +51 -1
  13. package/src/create/plugins/nextIntl.js +163 -17
  14. package/src/create/plugins/sentry.js +43 -23
  15. package/src/mcp.mjs +2 -2
  16. package/src/rename-app.mjs +3 -3
  17. package/templates/flutter-standalone/sh-ui.config.json +0 -1
  18. package/templates/monorepo/README.md +14 -5
  19. package/templates/monorepo/packages/eslint-config/flat.js +71 -0
  20. package/templates/monorepo/packages/eslint-config/fsd.js +0 -21
  21. package/templates/monorepo/packages/eslint-config/package.json +2 -3
  22. package/templates/monorepo/packages/typescript-config/package.json +6 -1
  23. package/templates/monorepo/packages/ui/ui-core/tsconfig.json +1 -1
  24. package/templates/monorepo/pnpm-workspace.yaml +2 -1
  25. package/templates/nextjs-app/.env.example +3 -2
  26. package/templates/nextjs-app/README.md +9 -9
  27. package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +1 -1
  28. package/templates/nextjs-app/_arch/flat/app/layout.tsx +2 -2
  29. package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +1 -1
  30. package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +2 -0
  31. package/templates/nextjs-app/_arch/flat/components/providers/GlobalProvider/index.tsx +3 -3
  32. package/templates/nextjs-app/_arch/flat/components/providers/theme/ThemeProvider.tsx +12 -0
  33. package/templates/nextjs-app/_arch/flat/eslint.config.js +10 -0
  34. package/templates/nextjs-app/_arch/flat/lib/api/clientFetch.ts +4 -4
  35. package/templates/nextjs-app/_arch/flat/lib/api/errorMessages.ts +37 -0
  36. package/templates/nextjs-app/_arch/flat/lib/hooks/useAppMutation.ts +14 -7
  37. package/templates/nextjs-app/_arch/flat/lib/test/renderWithProviders.tsx +32 -7
  38. package/templates/nextjs-app/_arch/flat/lib/utils/formatDate.ts +4 -0
  39. package/templates/nextjs-app/_arch/flat/lib/utils/formatPrice.ts +13 -5
  40. package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +1 -1
  41. package/templates/nextjs-app/_arch/flat/tsconfig.json +0 -1
  42. package/templates/nextjs-app/_arch/fsd/app/api/proxy/[...path]/route.ts +1 -1
  43. package/templates/nextjs-app/_arch/fsd/app/layout.tsx +2 -2
  44. package/templates/nextjs-app/_arch/fsd/src/app/layouts/RootLayout.tsx +2 -0
  45. package/templates/nextjs-app/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +3 -3
  46. package/templates/nextjs-app/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +12 -0
  47. package/templates/nextjs-app/_arch/fsd/src/shared/api/clientFetch.ts +4 -4
  48. package/templates/nextjs-app/_arch/fsd/src/shared/api/errorMessages.ts +37 -0
  49. package/templates/nextjs-app/_arch/fsd/src/shared/hooks/useAppMutation.ts +14 -7
  50. package/templates/nextjs-app/_arch/fsd/src/shared/lib/formatDate.ts +4 -0
  51. package/templates/nextjs-app/_arch/fsd/src/shared/lib/formatPrice.ts +13 -5
  52. package/templates/nextjs-app/_arch/fsd/src/shared/lib/getQueryClient.ts +1 -1
  53. package/templates/nextjs-app/_arch/fsd/src/shared/test/renderWithProviders.tsx +32 -7
  54. package/templates/nextjs-app/_arch/fsd/src/shared/ui/PrefetchBoundary/index.tsx +1 -1
  55. package/templates/nextjs-app/vitest.config.ts +4 -0
  56. package/templates/nextjs-standalone/.env.example +3 -2
  57. package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +1 -1
  58. package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +2 -2
  59. package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +1 -1
  60. package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +2 -0
  61. package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +3 -3
  62. package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProvider.tsx +12 -0
  63. package/templates/nextjs-standalone/_arch/flat/eslint.config.js +123 -0
  64. package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +4 -4
  65. package/templates/nextjs-standalone/_arch/flat/lib/api/errorMessages.ts +37 -0
  66. package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +14 -7
  67. package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +32 -7
  68. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +4 -0
  69. package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +13 -5
  70. package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +1 -1
  71. package/templates/nextjs-standalone/_arch/flat/tsconfig.json +1 -2
  72. package/templates/nextjs-standalone/_arch/fsd/app/api/proxy/[...path]/route.ts +1 -1
  73. package/templates/nextjs-standalone/_arch/fsd/app/layout.tsx +2 -2
  74. package/templates/nextjs-standalone/_arch/fsd/src/app/layouts/RootLayout.tsx +2 -0
  75. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +3 -3
  76. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +12 -0
  77. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +4 -4
  78. package/templates/nextjs-standalone/_arch/fsd/src/shared/api/errorMessages.ts +37 -0
  79. package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +14 -7
  80. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +4 -0
  81. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +13 -5
  82. package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/getQueryClient.ts +1 -1
  83. package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +32 -7
  84. package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/PrefetchBoundary/index.tsx +1 -1
  85. package/templates/nextjs-standalone/eslint.config.js +0 -15
  86. package/templates/nextjs-standalone/package.json +0 -2
  87. package/templates/ui-app-template/package.json +2 -2
  88. package/templates/ui-app-template/postcss.config.mjs +1 -1
  89. package/templates/monorepo/.eslintrc.js +0 -8
  90. package/templates/nextjs-app/Dockerfile +0 -11
  91. package/templates/nextjs-app/_arch/flat/components/providers/theme/ThemeProviders.tsx +0 -12
  92. package/templates/nextjs-app/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +0 -12
  93. package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +0 -12
  94. package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +0 -12
@@ -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 프레임워크 (현재 plain만 지원, 향후 tailwind 등 추가 예정)
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
- // ─── 공유 파일 조각 (providers 합성용) ───
41
-
42
- providerImports: [
43
- `import { NextIntlClientProvider } from 'next-intl';`,
44
- ],
45
- providerWrappers: ['NextIntlClientProvider'],
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 를 그대로
@@ -83,18 +168,19 @@ export const nextIntlPlugin = {
83
168
  import { RootLayout } from '${arch.aliases.layouts}/RootLayout';
84
169
 
85
170
  export const metadata: Metadata = {
86
- title: 'My App',
87
- description: 'My App Description',
171
+ title: 'sh-ui app',
172
+ description: 'sh-ui 기반 앱 — metadata 를 변경하세요.',
88
173
  };
89
174
 
90
- export default function Layout({
175
+ export default async function Layout({
91
176
  children,
92
177
  params,
93
178
  }: Readonly<{
94
179
  children: React.ReactNode;
95
180
  params: Promise<{ locale: string }>;
96
181
  }>) {
97
- return <RootLayout params={params}>{children}</RootLayout>;
182
+ const { locale } = await params;
183
+ return <RootLayout locale={locale}>{children}</RootLayout>;
98
184
  }
99
185
  `;
100
186
  return sideEffectImports ? `${sideEffectImports}\n\n${body}` : body;
@@ -103,20 +189,26 @@ export default function Layout({
103
189
  {
104
190
  type: 'replace',
105
191
  path: `${arch.paths.layouts}/RootLayout.tsx`,
106
- content: `import { hasLocale } from 'next-intl';
192
+ content: `import { hasLocale, NextIntlClientProvider } from 'next-intl';
107
193
  import { notFound } from 'next/navigation';
108
194
  import { GlobalProvider } from '${arch.aliases.providers}';
109
195
  import { routing } from '${arch.aliases.config}/i18n/routing';
110
196
 
111
- export async function RootLayout({
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({
112
206
  children,
113
- params,
207
+ locale,
114
208
  }: {
115
209
  children: React.ReactNode;
116
- params: Promise<{ locale: string }>;
210
+ locale: string;
117
211
  }) {
118
- const { locale } = await params;
119
-
120
212
  if (!hasLocale(routing.locales, locale)) {
121
213
  notFound();
122
214
  }
@@ -124,7 +216,9 @@ export async function RootLayout({
124
216
  return (
125
217
  <html lang={locale} suppressHydrationWarning>
126
218
  <body>
127
- <GlobalProvider>{children}</GlobalProvider>
219
+ <NextIntlClientProvider locale={locale}>
220
+ <GlobalProvider>{children}</GlobalProvider>
221
+ </NextIntlClientProvider>
128
222
  </body>
129
223
  </html>
130
224
  );
@@ -240,6 +334,58 @@ export default intl;
240
334
  export const config = {
241
335
  matcher: '/((?!api|trpc|_next|_vercel|monitoring|.*\\\\..*).*)',
242
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
+ }
243
389
  `,
244
390
  }),
245
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
- if (error instanceof Error && error.name === 'ApiError') {
81
- const status = (error as { status?: number }).status;
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
- <div className='flex min-h-screen items-center justify-center'>
207
- <div className='text-center'>
208
- <h1 className='text-2xl font-bold'>Something went wrong!</h1>
209
- <p className='mt-2 text-gray-600'>{error.message}</p>
210
- </div>
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 bg-red-50 dark:bg-red-950'>
241
- <AlertTriangle className='h-8 w-8 text-red-600 dark:text-red-400' />
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'>오류가 발생했습니다</h2>
246
- <p className='mb-6 text-center text-sm text-gray-500'>
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='rounded-md border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-950'>
251
- <p className='text-sm text-red-600 dark:text-red-400'>
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 bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90'
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 hover:bg-accent'
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 bg-gray-50 p-3 dark:bg-gray-900'>
276
- <p className='text-xs text-gray-600 dark:text-gray-400'>
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
- const { Authorization: _, ...safeHeaders } = requestHeaders;
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. 현재 ${CSS_FRAMEWORKS.join('/')} 지원 변종 미보유 컴포넌트는 add plain 으로 자동 fallback`),
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. 현재 ${CSS_FRAMEWORKS.join('/')} 지원 변종 미보유 컴포넌트는 add plain 으로 자동 fallback`),
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()
@@ -1,8 +1,8 @@
1
1
  // monorepo 의 앱 이름 (apps/<old>/ + packages/ui/ui-apps/ui-<old>/) 을 일괄 변경.
2
2
  //
3
- // 디렉토리 이동 + 정해진 6개 패턴 치환을 자동화. 사용자가 손으로
4
- // 6~10 군데 (package.json 이름, tsconfig paths, Dockerfile WORKDIR,
5
- // next.config transpilePackages, sh-ui.config aliases, README, ...) 를
3
+ // 디렉토리 이동 + 정해진 패턴 치환을 자동화. 사용자가 손으로
4
+ // 6~10 군데 (package.json 이름, tsconfig paths, next.config transpilePackages,
5
+ // sh-ui.config aliases, README, 필요 시 사용자가 추가한 Dockerfile WORKDIR ...) 를
6
6
  // 일일이 갈아엎지 않도록.
7
7
  //
8
8
  // false-positive 방지를 위해 bare 단어 (`web`) 는 절대 치환하지 않고,
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "platform": "flutter",
3
- "cssFramework": "plain",
4
3
  "theme": {
5
4
  "base": "neutral",
6
5
  "radius": "md",
@@ -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
- │ │ └── fsd.js # FSD 레이어 규칙 (boundaries, 파일 네이밍)
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
  ]
@@ -7,7 +7,8 @@
7
7
  "./base": "./base.js",
8
8
  "./next-js": "./next.js",
9
9
  "./react-internal": "./react-internal.js",
10
- "./fsd": "./fsd.js"
10
+ "./fsd": "./fsd.js",
11
+ "./flat": "./flat.js"
11
12
  },
12
13
  "devDependencies": {
13
14
  "@eslint/js": "^9.39.2",
@@ -16,10 +17,8 @@
16
17
  "@typescript-eslint/parser": "^8.54.0",
17
18
  "eslint": "^9.39.2",
18
19
  "eslint-config-prettier": "^10.1.8",
19
- "eslint-import-resolver-typescript": "^4.4.4",
20
20
  "eslint-plugin-boundaries": "^5.4.0",
21
21
  "eslint-plugin-check-file": "^3.3.1",
22
- "eslint-plugin-import-x": "^4.16.1",
23
22
  "eslint-plugin-only-warn": "^1.1.0",
24
23
  "eslint-plugin-react": "^7.37.5",
25
24
  "eslint-plugin-react-hooks": "^7.0.1",