sh-ui-cli 0.63.0 → 0.64.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.
@@ -2,6 +2,28 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
4
4
  "versions": [
5
+ {
6
+ "version": "0.64.0",
7
+ "date": "2026-05-08",
8
+ "title": "minor — proxy.ts 에 `HOME_REDIRECT` 자리 + input strict-indexing 회귀 fix",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**templates proxy.ts 에 `HOME_REDIRECT` 상수 추가** — `/` 진입 시 redirect 할 path 를 사용자가 한 줄로 지정. 빈 문자열이 기본이라 동작 변화 없음(= `app/page.tsx` 그대로 노출). `auth-jwt` 단독 / `next-intl` 단독 / `auth-jwt + next-intl` 합성 세 케이스 모두 동일 패턴. `/[locale]/page.tsx` 에 redirect RSC 를 따로 만들 필요 없이 미들웨어 한 곳에서 라우팅 게이트 처리.",
12
+ "**fix(input)**: `isValidBRN` 의 배열 인덱싱 4곳에 non-null assertion 추가 — `noUncheckedIndexedAccess: true` 인 사용자 프로젝트에서 `parseInt(d[i], 10)` 가 `string | undefined` 로 접혀 typecheck 실패하던 회귀. 직전 길이 가드(`d.length !== 10`) 로 모든 접근이 안전한 구간이라 런타임 동작 변경 없음. plain / tailwind / vanilla-extract / css-modules + `apps/docs` 듀얼카피본 동기화."
13
+ ],
14
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.64.0"
15
+ },
16
+ {
17
+ "version": "0.63.1",
18
+ "date": "2026-05-08",
19
+ "title": "patch — templates ThemeProvider 옵션 정비",
20
+ "type": "patch",
21
+ "highlights": [
22
+ "**templates 의 `ThemeProvider` 4곳에 `disableTransitionOnChange` 추가** — 토글 순간 transition 깜빡임 차단. nextjs-app / nextjs-standalone × flat / fsd 변종 모두 동일.",
23
+ "**옵션 의도 코멘트 보강** — attribute='class', defaultTheme/enableSystem, disableTransitionOnChange 가 왜 들어 있는지 + useTheme 는 next-themes 에서 직접 import 한다는 점을 파일 상단에 명시. 사용자가 RootLayout 작업 시 어디를 만져야 하는지 한눈에."
24
+ ],
25
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.63.1"
26
+ },
5
27
  {
6
28
  "version": "0.63.0",
7
29
  "date": "2026-05-08",
@@ -352,10 +352,10 @@ export function isValidBRN(digits: string): boolean {
352
352
  if (d.length !== 10) return false;
353
353
  const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
354
354
  let sum = 0;
355
- for (let i = 0; i < 9; i++) sum += parseInt(d[i], 10) * w[i];
356
- sum += Math.floor((parseInt(d[8], 10) * 5) / 10);
355
+ for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
356
+ sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
357
357
  const check = (10 - (sum % 10)) % 10;
358
- return check === parseInt(d[9], 10);
358
+ return check === parseInt(d[9]!, 10);
359
359
  }
360
360
 
361
361
  export interface BusinessNumberInputProps
@@ -298,10 +298,10 @@ export function isValidBRN(digits: string): boolean {
298
298
  if (d.length !== 10) return false;
299
299
  const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
300
300
  let sum = 0;
301
- for (let i = 0; i < 9; i++) sum += parseInt(d[i], 10) * w[i];
302
- sum += Math.floor((parseInt(d[8], 10) * 5) / 10);
301
+ for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
302
+ sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
303
303
  const check = (10 - (sum % 10)) % 10;
304
- return check === parseInt(d[9], 10);
304
+ return check === parseInt(d[9]!, 10);
305
305
  }
306
306
 
307
307
  export interface BusinessNumberInputProps
@@ -377,10 +377,10 @@ export function isValidBRN(digits: string): boolean {
377
377
  if (d.length !== 10) return false;
378
378
  const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
379
379
  let sum = 0;
380
- for (let i = 0; i < 9; i++) sum += parseInt(d[i], 10) * w[i];
381
- sum += Math.floor((parseInt(d[8], 10) * 5) / 10);
380
+ for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
381
+ sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
382
382
  const check = (10 - (sum % 10)) % 10;
383
- return check === parseInt(d[9], 10);
383
+ return check === parseInt(d[9]!, 10);
384
384
  }
385
385
 
386
386
  export interface BusinessNumberInputProps
@@ -307,10 +307,10 @@ export function isValidBRN(digits: string): boolean {
307
307
  if (d.length !== 10) return false;
308
308
  const w = [1, 3, 7, 1, 3, 7, 1, 3, 5];
309
309
  let sum = 0;
310
- for (let i = 0; i < 9; i++) sum += parseInt(d[i], 10) * w[i];
311
- sum += Math.floor((parseInt(d[8], 10) * 5) / 10);
310
+ for (let i = 0; i < 9; i++) sum += parseInt(d[i]!, 10) * w[i]!;
311
+ sum += Math.floor((parseInt(d[8]!, 10) * 5) / 10);
312
312
  const check = (10 - (sum % 10)) % 10;
313
- return check === parseInt(d[9], 10);
313
+ return check === parseInt(d[9]!, 10);
314
314
  }
315
315
 
316
316
  export interface BusinessNumberInputProps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1348,6 +1348,13 @@ import { routing } from '${configAlias}/i18n/routing';
1348
1348
 
1349
1349
  const AUTH_ROUTES = ['/sign-in', '/sign-up'];
1350
1350
 
1351
+ /**
1352
+ * 홈(\`/\`, \`/{locale}\`) 진입 시 redirect 할 path. 빈 문자열이면
1353
+ * \`app/[locale]/page.tsx\` 가 그대로 노출. 예: '/dashboard', '/projects'.
1354
+ * 인증 가드 위에서 동작하므로 미인증이면 그대로 \`/sign-in\` 으로 빠진다.
1355
+ */
1356
+ const HOME_REDIRECT = '';
1357
+
1351
1358
  const intl = createIntlMiddleware(routing);
1352
1359
 
1353
1360
  /**
@@ -1368,7 +1375,8 @@ const stripLocalePrefix = (pathname: string): string => {
1368
1375
  * Next 16+ proxy.ts (구 middleware.ts).
1369
1376
  * next-intl 라우팅 + auth-jwt 토큰 존재 체크 합성 버전.
1370
1377
  *
1371
- * - intl 먼저 로케일 prefix 처리 + NEXT_LOCALE 쿠키 set
1378
+ * - \`/\` + HOME_REDIRECT 설정 해당 경로로 리다이렉트 (인증 가드보다 먼저)
1379
+ * - intl 이 로케일 prefix 처리 + NEXT_LOCALE 쿠키 set
1372
1380
  * - 그 위에 인증 가드 — 토큰 없고 인증 라우트도 아니면 /sign-in 으로 redirect
1373
1381
  * - AT 만료 검사나 refresh 는 하지 않는다 (BFF 가 처리)
1374
1382
  */
@@ -1378,6 +1386,10 @@ export default function proxy(req: NextRequest) {
1378
1386
  const hasToken = !!req.cookies.get('accessToken')?.value;
1379
1387
  const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
1380
1388
 
1389
+ if (pathname === '/' && HOME_REDIRECT) {
1390
+ return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
1391
+ }
1392
+
1381
1393
  if (isAuthRoute) return intlRes;
1382
1394
  if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
1383
1395
 
@@ -87,10 +87,18 @@ export default function SignInPage() {
87
87
 
88
88
  const AUTH_ROUTES = ['/sign-in', '/sign-up'];
89
89
 
90
+ /**
91
+ * 홈(\`/\`) 진입 시 redirect 할 path. 빈 문자열이면 \`app/page.tsx\` 가 그대로 노출.
92
+ * 예: '/dashboard', '/projects'. 인증 가드 위에서 동작하므로 미인증이면
93
+ * 그대로 \`/sign-in\` 으로 빠진다.
94
+ */
95
+ const HOME_REDIRECT = '';
96
+
90
97
  /**
91
98
  * Next 16+ 의 proxy.ts (구 middleware.ts).
92
99
  * 토큰 존재 여부만 검사한다 — 만료 검사나 refresh 는 하지 않는다.
93
100
  *
101
+ * - \`/\` + HOME_REDIRECT 설정 → 해당 경로로 리다이렉트
94
102
  * - AT 쿠키 없음 + 인증 라우트 아님 → /sign-in 으로 리다이렉트
95
103
  * - AT 쿠키 있음 또는 인증 라우트 → 통과
96
104
  *
@@ -102,6 +110,10 @@ export default function proxy(req: NextRequest) {
102
110
  const hasToken = !!req.cookies.get('accessToken')?.value;
103
111
  const isAuthRoute = AUTH_ROUTES.some((r) => pathname.startsWith(r));
104
112
 
113
+ if (pathname === '/' && HOME_REDIRECT) {
114
+ return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
115
+ }
116
+
105
117
  if (isAuthRoute) return NextResponse.next();
106
118
  if (!hasToken) return NextResponse.redirect(new URL('/sign-in', req.url));
107
119
 
@@ -325,11 +325,38 @@ export const { Link, redirect, usePathname, useRouter, getPathname } =
325
325
  `,
326
326
 
327
327
  'proxy.ts': `import createIntlMiddleware from 'next-intl/middleware';
328
+ import { NextRequest, NextResponse } from 'next/server';
329
+
328
330
  import { routing } from '${arch.aliases.config}/i18n/routing';
329
331
 
332
+ /**
333
+ * 홈(\`/\`, \`/{locale}\`) 진입 시 redirect 할 path. 빈 문자열이면
334
+ * \`app/[locale]/page.tsx\` 가 그대로 노출. 예: '/dashboard', '/projects'.
335
+ */
336
+ const HOME_REDIRECT = '';
337
+
330
338
  const intl = createIntlMiddleware(routing);
331
339
 
332
- export default intl;
340
+ /**
341
+ * 로케일 prefix (/ko, /en) 를 벗겨 홈 매칭 (\`/\`) 에 사용한다.
342
+ * 예: /ko → /, /ko/posts → /posts.
343
+ */
344
+ const stripLocalePrefix = (pathname: string): string => {
345
+ const locales = routing.locales as readonly string[];
346
+ const segments = pathname.split('/').filter(Boolean);
347
+ if (segments[0] && locales.includes(segments[0])) {
348
+ const rest = segments.slice(1).join('/');
349
+ return \`/\${rest}\`.replace(/\\/$/, '') || '/';
350
+ }
351
+ return pathname;
352
+ };
353
+
354
+ export default function proxy(req: NextRequest) {
355
+ if (HOME_REDIRECT && stripLocalePrefix(req.nextUrl.pathname) === '/') {
356
+ return NextResponse.redirect(new URL(HOME_REDIRECT, req.url));
357
+ }
358
+ return intl(req);
359
+ }
333
360
 
334
361
  export const config = {
335
362
  matcher: '/((?!api|trpc|_next|_vercel|monitoring|.*\\\\..*).*)',
@@ -3,9 +3,24 @@
3
3
  import { ThemeProvider as NextThemesProvider } from 'next-themes';
4
4
  import type { ReactNode } from 'react';
5
5
 
6
+ /**
7
+ * 다크/라이트 테마 — next-themes ThemeProvider 를 wrap.
8
+ *
9
+ * - `attribute='class'` — `<html class="dark">` 토글 (Tailwind dark variant 와 호환)
10
+ * - `defaultTheme='system'` + `enableSystem` — OS 설정에 자동 동기화. light/dark 만
11
+ * 노출하려면 `enableSystem` 을 false 로
12
+ * - `disableTransitionOnChange` — 토글 순간 transition 깜빡임 차단
13
+ *
14
+ * useTheme 는 next-themes 에서 직접 import: `import { useTheme } from 'next-themes'`
15
+ */
6
16
  export function ThemeProvider({ children }: { children: ReactNode }) {
7
17
  return (
8
- <NextThemesProvider attribute='class' defaultTheme='system' enableSystem>
18
+ <NextThemesProvider
19
+ attribute='class'
20
+ defaultTheme='system'
21
+ enableSystem
22
+ disableTransitionOnChange
23
+ >
9
24
  {children}
10
25
  </NextThemesProvider>
11
26
  );
@@ -3,9 +3,24 @@
3
3
  import { ThemeProvider as NextThemesProvider } from 'next-themes';
4
4
  import type { ReactNode } from 'react';
5
5
 
6
+ /**
7
+ * 다크/라이트 테마 — next-themes ThemeProvider 를 wrap.
8
+ *
9
+ * - `attribute='class'` — `<html class="dark">` 토글 (Tailwind dark variant 와 호환)
10
+ * - `defaultTheme='system'` + `enableSystem` — OS 설정에 자동 동기화. light/dark 만
11
+ * 노출하려면 `enableSystem` 을 false 로
12
+ * - `disableTransitionOnChange` — 토글 순간 transition 깜빡임 차단
13
+ *
14
+ * useTheme 는 next-themes 에서 직접 import: `import { useTheme } from 'next-themes'`
15
+ */
6
16
  export function ThemeProvider({ children }: { children: ReactNode }) {
7
17
  return (
8
- <NextThemesProvider attribute='class' defaultTheme='system' enableSystem>
18
+ <NextThemesProvider
19
+ attribute='class'
20
+ defaultTheme='system'
21
+ enableSystem
22
+ disableTransitionOnChange
23
+ >
9
24
  {children}
10
25
  </NextThemesProvider>
11
26
  );
@@ -3,9 +3,24 @@
3
3
  import { ThemeProvider as NextThemesProvider } from 'next-themes';
4
4
  import type { ReactNode } from 'react';
5
5
 
6
+ /**
7
+ * 다크/라이트 테마 — next-themes ThemeProvider 를 wrap.
8
+ *
9
+ * - `attribute='class'` — `<html class="dark">` 토글 (Tailwind dark variant 와 호환)
10
+ * - `defaultTheme='system'` + `enableSystem` — OS 설정에 자동 동기화. light/dark 만
11
+ * 노출하려면 `enableSystem` 을 false 로
12
+ * - `disableTransitionOnChange` — 토글 순간 transition 깜빡임 차단
13
+ *
14
+ * useTheme 는 next-themes 에서 직접 import: `import { useTheme } from 'next-themes'`
15
+ */
6
16
  export function ThemeProvider({ children }: { children: ReactNode }) {
7
17
  return (
8
- <NextThemesProvider attribute='class' defaultTheme='system' enableSystem>
18
+ <NextThemesProvider
19
+ attribute='class'
20
+ defaultTheme='system'
21
+ enableSystem
22
+ disableTransitionOnChange
23
+ >
9
24
  {children}
10
25
  </NextThemesProvider>
11
26
  );
@@ -3,9 +3,24 @@
3
3
  import { ThemeProvider as NextThemesProvider } from 'next-themes';
4
4
  import type { ReactNode } from 'react';
5
5
 
6
+ /**
7
+ * 다크/라이트 테마 — next-themes ThemeProvider 를 wrap.
8
+ *
9
+ * - `attribute='class'` — `<html class="dark">` 토글 (Tailwind dark variant 와 호환)
10
+ * - `defaultTheme='system'` + `enableSystem` — OS 설정에 자동 동기화. light/dark 만
11
+ * 노출하려면 `enableSystem` 을 false 로
12
+ * - `disableTransitionOnChange` — 토글 순간 transition 깜빡임 차단
13
+ *
14
+ * useTheme 는 next-themes 에서 직접 import: `import { useTheme } from 'next-themes'`
15
+ */
6
16
  export function ThemeProvider({ children }: { children: ReactNode }) {
7
17
  return (
8
- <NextThemesProvider attribute='class' defaultTheme='system' enableSystem>
18
+ <NextThemesProvider
19
+ attribute='class'
20
+ defaultTheme='system'
21
+ enableSystem
22
+ disableTransitionOnChange
23
+ >
9
24
  {children}
10
25
  </NextThemesProvider>
11
26
  );