sh-ui-cli 0.57.0 → 0.58.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.
- package/data/changelog/versions.json +27 -0
- package/data/registry/react/components/sidebar/index.tsx +3 -3
- package/package.json +1 -1
- package/src/api.d.ts +34 -0
- package/src/api.js +7 -0
- package/src/create/architectures/archSchema.js +69 -0
- package/src/create/architectures/flat.js +51 -0
- package/src/create/architectures/fsd.js +42 -0
- package/src/create/architectures/index.js +55 -0
- package/src/create/cli-args.js +8 -1
- package/src/create/generator.js +101 -32
- package/src/create/index.mjs +7 -0
- package/src/create/plugins/authJwt.js +14 -8
- package/src/create/plugins/nextIntl.js +25 -17
- package/src/create/plugins/pluginSchema.js +26 -10
- package/src/create/plugins/sentry.js +9 -5
- package/src/mcp.mjs +10 -0
- package/templates/nextjs-app/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-app/_arch/flat/app/layout.tsx +16 -0
- package/templates/nextjs-app/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-app/_arch/flat/components/layouts/RootLayout.tsx +11 -0
- package/templates/nextjs-app/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-app/_arch/flat/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-app/_arch/flat/tsconfig.json +25 -0
- package/templates/nextjs-standalone/_arch/flat/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-standalone/_arch/flat/app/layout.tsx +16 -0
- package/templates/nextjs-standalone/_arch/flat/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/_arch/flat/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-standalone/_arch/flat/components/layouts/RootLayout.tsx +11 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/_arch/flat/components/providers/theme/ThemeProviders.tsx +12 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/error.ts +12 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/http.ts +13 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/observability.ts +20 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/_arch/flat/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/_arch/flat/lib/hooks/useAppMutation.ts +52 -0
- package/templates/nextjs-standalone/_arch/flat/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/_arch/flat/lib/test/index.ts +2 -0
- package/templates/nextjs-standalone/_arch/flat/lib/test/renderWithProviders.tsx +40 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatDate.ts +22 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/formatPrice.ts +10 -0
- package/templates/nextjs-standalone/_arch/flat/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-standalone/_arch/flat/sh-ui.config.json +19 -0
- package/templates/nextjs-standalone/_arch/flat/tsconfig.json +41 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/_arch/fsd/src/app/providers/theme/ThemeProviders.tsx +12 -0
- package/templates/nextjs-standalone/_arch/fsd/src/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/features/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/apiTypes.ts +21 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/error.ts +12 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/http.ts +13 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/observability.ts +20 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/config/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/hooks/useAppMutation.ts +52 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatDate.ts +22 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/formatPrice.ts +10 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/lib/utils.ts +6 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/model/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +135 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/index.ts +2 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/test/renderWithProviders.tsx +40 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/shared/ui/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/_arch/fsd/src/views/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/fsd/src/widgets/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/entities → _arch/flat/components/common}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/ui → _arch/flat/components/common}/FallbackBoundary/index.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/GlobalProvider/index.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/index.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
- /package/templates/nextjs-app/{src/app → _arch/flat/components}/providers/theme/ThemeProviders.tsx +0 -0
- /package/templates/nextjs-app/{src/features → _arch/flat/lib/api}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/apiTypes.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/clientFetch.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/error.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/http.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/observability.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/queryClient.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/api/serverFetch.ts +0 -0
- /package/templates/nextjs-app/{src/shared/api → _arch/flat/lib/config}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/config → _arch/flat/lib/hooks}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/hooks/useAppMutation.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/createTestQueryClient.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/index.ts +0 -0
- /package/templates/nextjs-app/{src/shared → _arch/flat/lib}/test/renderWithProviders.tsx +0 -0
- /package/templates/nextjs-app/{src/shared/hooks → _arch/flat/lib/utils}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatDate.ts +0 -0
- /package/templates/nextjs-app/{src/shared/lib → _arch/flat/lib/utils}/formatPrice.ts +0 -0
- /package/templates/nextjs-app/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
- /package/templates/nextjs-app/{app → _arch/fsd/app}/layout.tsx +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/GlobalProvider/index.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/index.tsx +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/app/providers/theme/ThemeProviders.tsx +0 -0
- /package/templates/nextjs-app/{src/shared/lib → _arch/fsd/src/entities}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/model → _arch/fsd/src/features}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/shared/ui → _arch/fsd/src/shared/api}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/apiTypes.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/clientFetch.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/error.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/http.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/observability.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/queryClient.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/api/serverFetch.ts +0 -0
- /package/templates/nextjs-app/{src/views → _arch/fsd/src/shared/config}/.gitkeep +0 -0
- /package/templates/nextjs-app/{src/widgets → _arch/fsd/src/shared/hooks}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/hooks/useAppMutation.ts +0 -0
- /package/templates/{nextjs-standalone/src/entities → nextjs-app/_arch/fsd/src/shared/lib}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatDate.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/lib/formatPrice.ts +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
- /package/templates/{nextjs-standalone/src/features → nextjs-app/_arch/fsd/src/shared/model}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/createTestQueryClient.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/index.ts +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/test/renderWithProviders.tsx +0 -0
- /package/templates/{nextjs-standalone/src/shared/api → nextjs-app/_arch/fsd/src/shared/ui}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone → nextjs-app/_arch/fsd}/src/shared/ui/FallbackBoundary/index.tsx +0 -0
- /package/templates/nextjs-app/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
- /package/templates/{nextjs-standalone/src/shared/config → nextjs-app/_arch/fsd/src/views}/.gitkeep +0 -0
- /package/templates/{nextjs-standalone/src/shared/hooks → nextjs-app/_arch/fsd/src/widgets}/.gitkeep +0 -0
- /package/templates/nextjs-app/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
- /package/templates/nextjs-standalone/{src/shared/model → _arch/flat/components/common}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/shared/ui → _arch/flat/lib/api}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/views → _arch/flat/lib/config}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/widgets → _arch/flat/lib/hooks}/.gitkeep +0 -0
- /package/templates/nextjs-standalone/{src/shared → _arch/flat/lib}/styles/tokens.css +0 -0
- /package/templates/nextjs-standalone/{src/shared/lib → _arch/flat/lib/utils}/utils.ts +0 -0
- /package/templates/nextjs-standalone/{app → _arch/fsd/app}/api/proxy/[...path]/route.ts +0 -0
- /package/templates/nextjs-standalone/{app → _arch/fsd/app}/layout.tsx +0 -0
- /package/templates/nextjs-standalone/{sh-ui.config.json → _arch/fsd/sh-ui.config.json} +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/layouts/RootLayout.tsx +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/app/providers/tanstack/QueryClientProvider.tsx +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/lib/getQueryClient.ts +0 -0
- /package/templates/nextjs-standalone/{src → _arch/fsd/src}/shared/ui/PrefetchBoundary/index.tsx +0 -0
- /package/templates/nextjs-standalone/{tsconfig.json → _arch/fsd/tsconfig.json} +0 -0
package/src/create/index.mjs
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
import { parseArgs } from './cli-args.js';
|
|
4
4
|
import { createProject, addApp, addComponent } from './generator.js';
|
|
5
5
|
import { allPlugins } from './plugins/index.js';
|
|
6
|
+
import { allArchitectures, getArchesForPlatform } from './architectures/index.js';
|
|
6
7
|
import { CREATE_PLATFORMS, CREATE_STRUCTURES, CSS_FRAMEWORKS_SUPPORTED } from '../constants.js';
|
|
7
8
|
import { THEME_PRESET_NAMES } from './theme/presets.js';
|
|
8
9
|
|
|
9
10
|
const PLUGIN_NAMES = allPlugins.map((p) => p.name);
|
|
10
11
|
const PLUGINS_LIST = PLUGIN_NAMES.join(', ');
|
|
12
|
+
const ARCH_NAMES = allArchitectures.map((a) => a.name);
|
|
13
|
+
const ARCHES_LIST = ARCH_NAMES.join('|');
|
|
14
|
+
const NEXT_ARCHES = getArchesForPlatform('next').map((a) => a.name).join(', ');
|
|
11
15
|
const THEME_PRESETS_LIST = THEME_PRESET_NAMES.join('|');
|
|
12
16
|
|
|
13
17
|
export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next.js / Flutter)
|
|
@@ -20,6 +24,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
|
|
|
20
24
|
옵션:
|
|
21
25
|
--platform <${CREATE_PLATFORMS.join('|')}> 타겟 플랫폼
|
|
22
26
|
--structure <${CREATE_STRUCTURES.join('|')}> Next.js 프로젝트 구조 (next 일 때)
|
|
27
|
+
--arch <${ARCHES_LIST}> 프로젝트 아키텍처 — 폴더 구조/import alias 컨벤션. next 에서 사용 가능: ${NEXT_ARCHES}. 기본 fsd
|
|
23
28
|
--plugins <a,b> 플러그인 (${PLUGINS_LIST}). 미지정/"" → 없음
|
|
24
29
|
--theme <preset|base64> 프리셋 이름(${THEME_PRESETS_LIST}) 또는 playground base64. 선택
|
|
25
30
|
--css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS 프레임워크 (현재 plain만 지원, 향후 tailwind 등 추가 예정)
|
|
@@ -33,6 +38,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
|
|
|
33
38
|
예 (비대화형 / 에이전트 / CI):
|
|
34
39
|
sh-ui create my-app --platform next --structure standalone --yes
|
|
35
40
|
sh-ui create my-app --platform next --structure monorepo --plugins ${PLUGIN_NAMES.slice(0, 3).join(',')} --yes
|
|
41
|
+
sh-ui create my-app --platform next --structure standalone --arch flat --yes
|
|
36
42
|
sh-ui create my-app --platform next --structure standalone --theme rose --yes
|
|
37
43
|
sh-ui create my-app --platform flutter --yes
|
|
38
44
|
|
|
@@ -70,6 +76,7 @@ export async function runCreate(rest) {
|
|
|
70
76
|
platform: flags.platform,
|
|
71
77
|
structure: flags.structure,
|
|
72
78
|
plugins: flags.plugins,
|
|
79
|
+
arch: flags.arch,
|
|
73
80
|
theme: flags.theme,
|
|
74
81
|
css: flags.css,
|
|
75
82
|
yes: flags.yes,
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth-jwt 플러그인 — Layer 2 부터 arch-aware.
|
|
3
|
+
*
|
|
4
|
+
* fs 경로 / import alias 가 arch.paths.api / arch.aliases.api 에서 파생.
|
|
5
|
+
* FSD 기준 v0.57 까지의 하드코딩과 1:1 일치 (회귀 가드는 smoke).
|
|
6
|
+
*/
|
|
1
7
|
export const authJwtPlugin = {
|
|
2
8
|
name: 'auth-jwt',
|
|
3
9
|
label: '쿠키 기반 JWT 인증 (refresh 자리표시자 포함)',
|
|
@@ -26,7 +32,7 @@ export const authJwtPlugin = {
|
|
|
26
32
|
// refreshSession.ts 는 v1 placeholder — 백엔드 명세 확정 후 본문만 채우면
|
|
27
33
|
// BFF 와 withAuthRetry 가 자동 활용한다.
|
|
28
34
|
|
|
29
|
-
files: {
|
|
35
|
+
files: (arch) => ({
|
|
30
36
|
'proxy.ts': `import { NextRequest, NextResponse } from 'next/server';
|
|
31
37
|
|
|
32
38
|
const AUTH_ROUTES = ['/sign-in', '/sign-up'];
|
|
@@ -57,7 +63,7 @@ export const config = {
|
|
|
57
63
|
};
|
|
58
64
|
`,
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
[`${arch.paths.api}/refreshSession.ts`]: `type RefreshResult =
|
|
61
67
|
| { ok: true; accessToken: string; refreshToken: string }
|
|
62
68
|
| { ok: false };
|
|
63
69
|
|
|
@@ -109,7 +115,7 @@ export async function refreshSession(
|
|
|
109
115
|
}
|
|
110
116
|
`,
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
[`${arch.paths.api}/withAuthRetry.ts`]: `import { cookies } from 'next/headers';
|
|
113
119
|
|
|
114
120
|
import { ApiError } from './error';
|
|
115
121
|
import { refreshSession } from './refreshSession';
|
|
@@ -135,8 +141,8 @@ const COOKIE = {
|
|
|
135
141
|
* 사용 예 (Server Action):
|
|
136
142
|
*
|
|
137
143
|
* 'use server';
|
|
138
|
-
* import { serverFetch } from '
|
|
139
|
-
* import { withAuthRetry } from '
|
|
144
|
+
* import { serverFetch } from '${arch.aliases.api}/serverFetch';
|
|
145
|
+
* import { withAuthRetry } from '${arch.aliases.api}/withAuthRetry';
|
|
140
146
|
*
|
|
141
147
|
* export async function toggleFavoriteAction(id: number) {
|
|
142
148
|
* return withAuthRetry(() =>
|
|
@@ -171,8 +177,8 @@ import { NextResponse, type NextRequest } from 'next/server';
|
|
|
171
177
|
import {
|
|
172
178
|
captureApiError,
|
|
173
179
|
logApiError,
|
|
174
|
-
} from '
|
|
175
|
-
import { refreshSession } from '
|
|
180
|
+
} from '${arch.aliases.api}/observability';
|
|
181
|
+
import { refreshSession } from '${arch.aliases.api}/refreshSession';
|
|
176
182
|
|
|
177
183
|
const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
|
|
178
184
|
const ACCESS_TOKEN_COOKIE = 'accessToken';
|
|
@@ -338,5 +344,5 @@ export const DELETE = (
|
|
|
338
344
|
ctx: { params: Promise<{ path: string[] }> },
|
|
339
345
|
) => proxyRequest(req, ctx, 'DELETE');
|
|
340
346
|
`,
|
|
341
|
-
},
|
|
347
|
+
}),
|
|
342
348
|
};
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next-intl 플러그인 — Layer 2 부터 arch-aware.
|
|
3
|
+
*
|
|
4
|
+
* 모든 fs 경로 / import alias 가 arch 디스크립터의 논리 키에서 파생된다:
|
|
5
|
+
* - i18n 설정 (request/routing/navigation/messages) → arch.paths.config + '/i18n'
|
|
6
|
+
* - 내부 import (RootLayout, GlobalProvider) → arch.aliases.layouts / providers
|
|
7
|
+
*
|
|
8
|
+
* FSD 에서는 v0.57 까지의 하드코딩과 1:1 일치 (회귀 가드는 smoke 시나리오 3).
|
|
9
|
+
* flat 에서는 자동으로 lib/config/i18n + components/layouts/RootLayout 로 emit.
|
|
10
|
+
*/
|
|
1
11
|
export const nextIntlPlugin = {
|
|
2
12
|
name: 'next-intl',
|
|
3
13
|
label: 'next-intl (다국어 지원)',
|
|
@@ -15,8 +25,8 @@ export const nextIntlPlugin = {
|
|
|
15
25
|
`import createNextIntlPlugin from 'next-intl/plugin';`,
|
|
16
26
|
],
|
|
17
27
|
|
|
18
|
-
preExport: [
|
|
19
|
-
`const withNextIntl = createNextIntlPlugin('
|
|
28
|
+
preExport: (arch) => [
|
|
29
|
+
`const withNextIntl = createNextIntlPlugin('./${arch.paths.config}/i18n/request.ts');`,
|
|
20
30
|
],
|
|
21
31
|
|
|
22
32
|
wrapExport(expr) {
|
|
@@ -36,7 +46,7 @@ export const nextIntlPlugin = {
|
|
|
36
46
|
|
|
37
47
|
// ─── 라우트 구조 변환 ───
|
|
38
48
|
|
|
39
|
-
transforms: [
|
|
49
|
+
transforms: (arch) => [
|
|
40
50
|
{ type: 'move', from: 'app/page.tsx', to: 'app/[locale]/page.tsx' },
|
|
41
51
|
{ type: 'move', from: 'app/error.tsx', to: 'app/[locale]/error.tsx' },
|
|
42
52
|
{
|
|
@@ -62,7 +72,7 @@ export const nextIntlPlugin = {
|
|
|
62
72
|
.filter((line) => /^\s*import\s+['"][^'"]+['"];?\s*$/.test(line))
|
|
63
73
|
.join('\n');
|
|
64
74
|
const body = `import type { Metadata } from 'next';
|
|
65
|
-
import { RootLayout } from '
|
|
75
|
+
import { RootLayout } from '${arch.aliases.layouts}/RootLayout';
|
|
66
76
|
|
|
67
77
|
export const metadata: Metadata = {
|
|
68
78
|
title: 'My App',
|
|
@@ -84,11 +94,11 @@ export default function Layout({
|
|
|
84
94
|
},
|
|
85
95
|
{
|
|
86
96
|
type: 'replace',
|
|
87
|
-
path:
|
|
97
|
+
path: `${arch.paths.layouts}/RootLayout.tsx`,
|
|
88
98
|
content: `import { hasLocale } from 'next-intl';
|
|
89
99
|
import { notFound } from 'next/navigation';
|
|
90
|
-
import { GlobalProvider } from '
|
|
91
|
-
import { routing } from '
|
|
100
|
+
import { GlobalProvider } from '${arch.aliases.providers}';
|
|
101
|
+
import { routing } from '${arch.aliases.config}/i18n/routing';
|
|
92
102
|
|
|
93
103
|
export async function RootLayout({
|
|
94
104
|
children,
|
|
@@ -117,8 +127,8 @@ export async function RootLayout({
|
|
|
117
127
|
|
|
118
128
|
// ─── 독립 파일 ───
|
|
119
129
|
|
|
120
|
-
files: {
|
|
121
|
-
|
|
130
|
+
files: (arch) => ({
|
|
131
|
+
[`${arch.paths.config}/i18n/routing.ts`]: `import { defineRouting } from 'next-intl/routing';
|
|
122
132
|
|
|
123
133
|
export const routing = defineRouting({
|
|
124
134
|
locales: ['ko', 'en'],
|
|
@@ -126,7 +136,7 @@ export const routing = defineRouting({
|
|
|
126
136
|
});
|
|
127
137
|
`,
|
|
128
138
|
|
|
129
|
-
|
|
139
|
+
[`${arch.paths.config}/i18n/request.ts`]: `import { getRequestConfig } from 'next-intl/server';
|
|
130
140
|
import { hasLocale } from 'next-intl';
|
|
131
141
|
import { routing } from './routing';
|
|
132
142
|
|
|
@@ -143,14 +153,14 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|
|
143
153
|
});
|
|
144
154
|
`,
|
|
145
155
|
|
|
146
|
-
|
|
156
|
+
[`${arch.paths.config}/i18n/navigation.ts`]: `import { createNavigation } from 'next-intl/navigation';
|
|
147
157
|
import { routing } from './routing';
|
|
148
158
|
|
|
149
159
|
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
|
150
160
|
createNavigation(routing);
|
|
151
161
|
`,
|
|
152
162
|
|
|
153
|
-
|
|
163
|
+
[`${arch.paths.config}/i18n/messages/ko.json`]: `{
|
|
154
164
|
"common": {
|
|
155
165
|
"loading": "로딩 중...",
|
|
156
166
|
"error": "오류가 발생했습니다",
|
|
@@ -181,7 +191,7 @@ export const { Link, redirect, usePathname, useRouter, getPathname } =
|
|
|
181
191
|
}
|
|
182
192
|
`,
|
|
183
193
|
|
|
184
|
-
|
|
194
|
+
[`${arch.paths.config}/i18n/messages/en.json`]: `{
|
|
185
195
|
"common": {
|
|
186
196
|
"loading": "Loading...",
|
|
187
197
|
"error": "An error occurred",
|
|
@@ -212,10 +222,8 @@ export const { Link, redirect, usePathname, useRouter, getPathname } =
|
|
|
212
222
|
}
|
|
213
223
|
`,
|
|
214
224
|
|
|
215
|
-
// app/[locale]/layout.tsx 는 transforms 에서 생성된다 (위 move + replace 참고)
|
|
216
|
-
|
|
217
225
|
'proxy.ts': `import createIntlMiddleware from 'next-intl/middleware';
|
|
218
|
-
import { routing } from '
|
|
226
|
+
import { routing } from '${arch.aliases.config}/i18n/routing';
|
|
219
227
|
|
|
220
228
|
const intl = createIntlMiddleware(routing);
|
|
221
229
|
|
|
@@ -225,5 +233,5 @@ export const config = {
|
|
|
225
233
|
matcher: '/((?!api|trpc|_next|_vercel|monitoring|.*\\\\..*).*)',
|
|
226
234
|
};
|
|
227
235
|
`,
|
|
228
|
-
},
|
|
236
|
+
}),
|
|
229
237
|
};
|
|
@@ -31,6 +31,13 @@ const wrapperFn = z
|
|
|
31
31
|
})
|
|
32
32
|
.optional();
|
|
33
33
|
|
|
34
|
+
// arch-aware 필드. 정적 값 OR `(arch) => 정적 값` 함수 모두 허용.
|
|
35
|
+
// 플러그인이 arch 디스크립터의 paths/aliases 를 조회해 자기 산출물의 fs 경로/import
|
|
36
|
+
// alias 를 결정할 때 사용. Layer 2 이전엔 모든 플러그인이 정적, Layer 2 이후엔 함수형.
|
|
37
|
+
const archAwareFn = z.custom((val) => typeof val === "function");
|
|
38
|
+
const archAwareRecord = z.union([z.record(filePath, z.string()), archAwareFn]);
|
|
39
|
+
const archAwareArray = z.union([z.array(z.any()), archAwareFn]);
|
|
40
|
+
|
|
34
41
|
export const PluginSchema = z.object({
|
|
35
42
|
name: z.string().regex(/^[a-z][a-z0-9-]*$/, {
|
|
36
43
|
message: 'Plugin name must be lowercase kebab-case (e.g., "auth-jwt")',
|
|
@@ -43,26 +50,35 @@ export const PluginSchema = z.object({
|
|
|
43
50
|
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
44
51
|
|
|
45
52
|
imports: z.array(z.string()).optional(),
|
|
53
|
+
// preExport 은 arch-aware (next.config.ts 의 export 직전 emit 되는 라인들 —
|
|
54
|
+
// 예: next-intl 의 `createNextIntlPlugin('./src/shared/config/i18n/request.ts')` 같이
|
|
55
|
+
// arch.paths.config 가 박혀야 하는 경로 포함).
|
|
56
|
+
preExport: z.union([z.array(z.string()), archAwareFn]).optional(),
|
|
46
57
|
wrapExport: wrapperFn,
|
|
47
58
|
|
|
48
59
|
envVars: z.array(z.string()).optional(),
|
|
49
60
|
turboEnvVars: z.array(z.string()).optional(),
|
|
50
61
|
|
|
51
|
-
providerImports
|
|
62
|
+
// providerImports/providerWrappers/files/transforms 는 arch-aware —
|
|
63
|
+
// 정적 값 또는 (arch) => 정적 값 함수 형태 둘 다 허용.
|
|
64
|
+
providerImports: z.union([z.array(z.string()), archAwareFn]).optional(),
|
|
52
65
|
providerWrappers: z
|
|
53
|
-
.
|
|
54
|
-
z.
|
|
55
|
-
z.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
.union([
|
|
67
|
+
z.array(
|
|
68
|
+
z.union([
|
|
69
|
+
z.object({ open: z.string(), close: z.string() }),
|
|
70
|
+
z.string(),
|
|
71
|
+
]),
|
|
72
|
+
),
|
|
73
|
+
archAwareFn,
|
|
74
|
+
])
|
|
59
75
|
.optional(),
|
|
60
76
|
|
|
61
|
-
files:
|
|
77
|
+
files: archAwareRecord.optional(),
|
|
62
78
|
|
|
63
79
|
// 향후 확장 — moves, transforms, etc 는 nextIntl.js 에서 사용하므로 허용
|
|
64
|
-
moves:
|
|
65
|
-
transforms:
|
|
80
|
+
moves: archAwareArray.optional(),
|
|
81
|
+
transforms: archAwareArray.optional(),
|
|
66
82
|
});
|
|
67
83
|
|
|
68
84
|
/**
|
|
@@ -57,8 +57,12 @@ export const sentryPlugin = {
|
|
|
57
57
|
// HTTP/proxy 인프라(http.ts, apiTypes.ts, error.ts, app/api/proxy 등)는
|
|
58
58
|
// 베이스 템플릿이 소유한다. Sentry 는 베이스의 observability.ts 를
|
|
59
59
|
// Sentry-aware 버전으로 덮어써서 캡처/로그를 활성화한다.
|
|
60
|
+
//
|
|
61
|
+
// arch 의존: FallbackBoundary 는 arch.paths.ui 에, observability 는 arch.paths.api 에
|
|
62
|
+
// 떨어진다. FallbackBoundary 안의 ApiError import 는 arch.aliases.api 로 fully-qualified —
|
|
63
|
+
// 상대 경로 (`../../api/error`) 는 FSD 에서만 동작하므로 alias 로 통일.
|
|
60
64
|
|
|
61
|
-
files: {
|
|
65
|
+
files: (arch) => ({
|
|
62
66
|
'sentry.server.config.ts': `import * as Sentry from '@sentry/nextjs';
|
|
63
67
|
|
|
64
68
|
Sentry.init({
|
|
@@ -280,7 +284,7 @@ export default function Error({
|
|
|
280
284
|
}
|
|
281
285
|
`,
|
|
282
286
|
|
|
283
|
-
|
|
287
|
+
[`${arch.paths.ui}/FallbackBoundary/index.tsx`]: `import React, {
|
|
284
288
|
Component,
|
|
285
289
|
ComponentType,
|
|
286
290
|
ErrorInfo,
|
|
@@ -290,7 +294,7 @@ export default function Error({
|
|
|
290
294
|
import * as Sentry from '@sentry/nextjs';
|
|
291
295
|
import { QueryErrorResetBoundary } from '@tanstack/react-query';
|
|
292
296
|
|
|
293
|
-
import { ApiError } from '
|
|
297
|
+
import { ApiError } from '${arch.aliases.api}/error';
|
|
294
298
|
|
|
295
299
|
interface ErrorFallbackProps {
|
|
296
300
|
error: Error | null;
|
|
@@ -386,7 +390,7 @@ export function FallbackBoundary({
|
|
|
386
390
|
// http.ts / serverFetch.ts / proxy/route.ts 가 이 모듈을 import 하므로,
|
|
387
391
|
// Sentry 플러그인이 켜지면 자동으로 캡처가 활성화된다.
|
|
388
392
|
|
|
389
|
-
|
|
393
|
+
[`${arch.paths.api}/observability.ts`]: `import * as Sentry from '@sentry/nextjs';
|
|
390
394
|
|
|
391
395
|
type ApiCaptureParams = {
|
|
392
396
|
url: string;
|
|
@@ -439,5 +443,5 @@ export const logApiError = (prefix: string, params: ApiLogParams): void => {
|
|
|
439
443
|
if (responseBody) console.error('- Response Body:', responseBody);
|
|
440
444
|
};
|
|
441
445
|
`,
|
|
442
|
-
},
|
|
446
|
+
}),
|
|
443
447
|
};
|
package/src/mcp.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
CSS_FRAMEWORKS_SUPPORTED,
|
|
46
46
|
} from "./constants.js";
|
|
47
47
|
import { allPlugins } from "./create/plugins/index.js";
|
|
48
|
+
import { allArchitectures } from "./create/architectures/index.js";
|
|
48
49
|
import { THEME_PRESET_NAMES } from "./create/theme/presets.js";
|
|
49
50
|
import { decodeTheme } from "./create/theme/decode.js";
|
|
50
51
|
import { encodeTheme } from "./create/theme/encode.js";
|
|
@@ -55,6 +56,7 @@ const RADII = THEME_RADII;
|
|
|
55
56
|
const MODES = THEME_MODES;
|
|
56
57
|
const CSS_FRAMEWORKS = CSS_FRAMEWORKS_SUPPORTED;
|
|
57
58
|
const PLUGIN_NAMES = allPlugins.map((p) => p.name);
|
|
59
|
+
const ARCH_NAMES = allArchitectures.map((a) => a.name);
|
|
58
60
|
const THEME_PRESETS_LIST = THEME_PRESET_NAMES.join(", ");
|
|
59
61
|
|
|
60
62
|
const INIT_DESCRIPTIONS = {
|
|
@@ -227,6 +229,13 @@ export async function startMcpServer() {
|
|
|
227
229
|
.describe("Next.js 구조 — platform=next 일 때 필수. standalone(단독) | monorepo(Turborepo)"),
|
|
228
230
|
plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
|
|
229
231
|
.describe(`Next.js 플러그인 (${PLUGIN_NAMES.join(', ')}). 미지정시 빈 배열`),
|
|
232
|
+
arch: z.enum(ARCH_NAMES).optional()
|
|
233
|
+
.describe(
|
|
234
|
+
`프로젝트 아키텍처 — 플랫폼별로 사용 가능한 값이 다름. ` +
|
|
235
|
+
`현재 next 에서 사용 가능: ${allArchitectures.filter((a) => a.platforms.includes('next')).map((a) => a.name).join(', ')} (기본 fsd). ` +
|
|
236
|
+
`flutter 는 현재 arch 디스크립터 없음 (미지정 또는 host 자체 default). ` +
|
|
237
|
+
`arch 와 플러그인은 별개 — arch 는 폴더 구조/import alias 컨벤션, 플러그인은 기능.`,
|
|
238
|
+
),
|
|
230
239
|
theme: z.string().optional()
|
|
231
240
|
.describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 사용자가 톤을 직접 손본 결과를 영구 보관하려면 sh_ui_encode_theme 으로 base64 를 만들어 여기에 넘긴다.`),
|
|
232
241
|
cssFramework: z.enum(CSS_FRAMEWORKS).optional()
|
|
@@ -271,6 +280,7 @@ export async function startMcpServer() {
|
|
|
271
280
|
platform: input.platform,
|
|
272
281
|
structure: input.structure,
|
|
273
282
|
plugins: input.plugins,
|
|
283
|
+
arch: input.arch,
|
|
274
284
|
theme: input.theme,
|
|
275
285
|
css: input.cssFramework,
|
|
276
286
|
yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
|
|
@@ -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: '서버에 연결할 수 없습니다.',
|
|
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,16 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import '@workspace/ui-app-name/globals.css';
|
|
3
|
+
import { RootLayout } from '@/components/layouts/RootLayout';
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: 'App Name',
|
|
7
|
+
description: 'App Description',
|
|
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,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,11 @@
|
|
|
1
|
+
import { GlobalProvider } from '@/components/providers';
|
|
2
|
+
|
|
3
|
+
export function RootLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<html lang='ko' suppressHydrationWarning>
|
|
6
|
+
<body>
|
|
7
|
+
<GlobalProvider>{children}</GlobalProvider>
|
|
8
|
+
</body>
|
|
9
|
+
</html>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -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,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 default function getQueryClient() {
|
|
13
|
+
return isServer ? getServerQueryClient() : getBrowserQueryClient();
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@workspace/typescript-config/nextjs.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/lib/*": ["./lib/*"],
|
|
7
|
+
"@/components/*": ["./components/*"],
|
|
8
|
+
"@/app/*": ["./app/*"],
|
|
9
|
+
"@workspace/ui-app-name/*": ["../../packages/ui/ui-apps/ui-app-name/src/*"]
|
|
10
|
+
},
|
|
11
|
+
"plugins": [
|
|
12
|
+
{
|
|
13
|
+
"name": "next"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"next-env.d.ts",
|
|
19
|
+
"next.config.ts",
|
|
20
|
+
"**/*.ts",
|
|
21
|
+
"**/*.tsx",
|
|
22
|
+
".next/types/**/*.ts"
|
|
23
|
+
],
|
|
24
|
+
"exclude": ["node_modules"]
|
|
25
|
+
}
|