sh-ui-cli 0.76.0 → 0.77.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.
Files changed (89) hide show
  1. package/data/changelog/versions.json +26 -0
  2. package/data/registry/react/components/switch/index.tailwind.tsx +1 -1
  3. package/data/registry/react/components/switch/styles.css +6 -0
  4. package/data/registry/react/components/switch/styles.module.css +6 -0
  5. package/data/registry/react/tokens-used.json +3 -1
  6. package/package.json +1 -1
  7. package/src/create/architectures/index.js +2 -1
  8. package/src/create/architectures/mes.js +53 -0
  9. package/templates/monorepo/packages/eslint-config/mes.js +82 -0
  10. package/templates/monorepo/packages/eslint-config/package.json +2 -1
  11. package/templates/nextjs-app/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
  12. package/templates/nextjs-app/_arch/mes/app/layout.tsx +16 -0
  13. package/templates/nextjs-app/_arch/mes/app/sign-in/page.tsx +1 -0
  14. package/templates/nextjs-app/_arch/mes/eslint.config.js +10 -0
  15. package/templates/nextjs-app/_arch/mes/src/components/common/.gitkeep +0 -0
  16. package/templates/nextjs-app/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
  17. package/templates/nextjs-app/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
  18. package/templates/nextjs-app/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
  19. package/templates/nextjs-app/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
  20. package/templates/nextjs-app/_arch/mes/src/components/providers/index.tsx +1 -0
  21. package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  22. package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  23. package/templates/nextjs-app/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
  24. package/templates/nextjs-app/_arch/mes/src/hooks/.gitkeep +0 -0
  25. package/templates/nextjs-app/_arch/mes/src/hooks/useAppMutation.ts +59 -0
  26. package/templates/nextjs-app/_arch/mes/src/lib/api/.gitkeep +0 -0
  27. package/templates/nextjs-app/_arch/mes/src/lib/api/apiTypes.ts +21 -0
  28. package/templates/nextjs-app/_arch/mes/src/lib/api/clientFetch.ts +40 -0
  29. package/templates/nextjs-app/_arch/mes/src/lib/api/error.ts +12 -0
  30. package/templates/nextjs-app/_arch/mes/src/lib/api/errorMessages.ts +37 -0
  31. package/templates/nextjs-app/_arch/mes/src/lib/api/http.ts +13 -0
  32. package/templates/nextjs-app/_arch/mes/src/lib/api/observability.ts +20 -0
  33. package/templates/nextjs-app/_arch/mes/src/lib/api/queryClient.ts +30 -0
  34. package/templates/nextjs-app/_arch/mes/src/lib/api/serverFetch.ts +59 -0
  35. package/templates/nextjs-app/_arch/mes/src/lib/config/.gitkeep +0 -0
  36. package/templates/nextjs-app/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
  37. package/templates/nextjs-app/_arch/mes/src/lib/test/index.ts +2 -0
  38. package/templates/nextjs-app/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
  39. package/templates/nextjs-app/_arch/mes/src/lib/utils/.gitkeep +0 -0
  40. package/templates/nextjs-app/_arch/mes/src/lib/utils/formatDate.ts +26 -0
  41. package/templates/nextjs-app/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
  42. package/templates/nextjs-app/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
  43. package/templates/nextjs-app/_arch/mes/src/pages/sign-in/api.ts +3 -0
  44. package/templates/nextjs-app/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
  45. package/templates/nextjs-app/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
  46. package/templates/nextjs-app/_arch/mes/src/pages/sign-in/index.tsx +14 -0
  47. package/templates/nextjs-app/_arch/mes/src/pages/sign-in/schema.ts +2 -0
  48. package/templates/nextjs-app/_arch/mes/tsconfig.json +24 -0
  49. package/templates/nextjs-standalone/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
  50. package/templates/nextjs-standalone/_arch/mes/app/globals.css +49 -0
  51. package/templates/nextjs-standalone/_arch/mes/app/layout.tsx +16 -0
  52. package/templates/nextjs-standalone/_arch/mes/app/sign-in/page.tsx +1 -0
  53. package/templates/nextjs-standalone/_arch/mes/eslint.config.js +137 -0
  54. package/templates/nextjs-standalone/_arch/mes/sh-ui.config.json +22 -0
  55. package/templates/nextjs-standalone/_arch/mes/src/components/common/.gitkeep +0 -0
  56. package/templates/nextjs-standalone/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
  57. package/templates/nextjs-standalone/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
  58. package/templates/nextjs-standalone/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
  59. package/templates/nextjs-standalone/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
  60. package/templates/nextjs-standalone/_arch/mes/src/components/providers/index.tsx +1 -0
  61. package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
  62. package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  63. package/templates/nextjs-standalone/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
  64. package/templates/nextjs-standalone/_arch/mes/src/hooks/.gitkeep +0 -0
  65. package/templates/nextjs-standalone/_arch/mes/src/hooks/useAppMutation.ts +59 -0
  66. package/templates/nextjs-standalone/_arch/mes/src/lib/api/.gitkeep +0 -0
  67. package/templates/nextjs-standalone/_arch/mes/src/lib/api/apiTypes.ts +21 -0
  68. package/templates/nextjs-standalone/_arch/mes/src/lib/api/clientFetch.ts +40 -0
  69. package/templates/nextjs-standalone/_arch/mes/src/lib/api/error.ts +12 -0
  70. package/templates/nextjs-standalone/_arch/mes/src/lib/api/errorMessages.ts +37 -0
  71. package/templates/nextjs-standalone/_arch/mes/src/lib/api/http.ts +13 -0
  72. package/templates/nextjs-standalone/_arch/mes/src/lib/api/observability.ts +20 -0
  73. package/templates/nextjs-standalone/_arch/mes/src/lib/api/queryClient.ts +30 -0
  74. package/templates/nextjs-standalone/_arch/mes/src/lib/api/serverFetch.ts +59 -0
  75. package/templates/nextjs-standalone/_arch/mes/src/lib/config/.gitkeep +0 -0
  76. package/templates/nextjs-standalone/_arch/mes/src/lib/styles/tokens.css +170 -0
  77. package/templates/nextjs-standalone/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
  78. package/templates/nextjs-standalone/_arch/mes/src/lib/test/index.ts +2 -0
  79. package/templates/nextjs-standalone/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
  80. package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatDate.ts +26 -0
  81. package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
  82. package/templates/nextjs-standalone/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
  83. package/templates/nextjs-standalone/_arch/mes/src/lib/utils/utils.ts +6 -0
  84. package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/api.ts +3 -0
  85. package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
  86. package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
  87. package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/index.tsx +14 -0
  88. package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/schema.ts +2 -0
  89. package/templates/nextjs-standalone/_arch/mes/tsconfig.json +39 -0
@@ -0,0 +1,59 @@
1
+ import { cookies } from 'next/headers';
2
+
3
+ import type { ApiResponse } from './apiTypes';
4
+ import { ApiError } from './error';
5
+ import { captureApiError, logApiError } from './observability';
6
+
7
+ const API_URL = process.env.API_URL ?? 'http://localhost:8080/api';
8
+
9
+ const ACCESS_TOKEN_COOKIE = 'accessToken';
10
+ const LOCALE_COOKIE = 'NEXT_LOCALE';
11
+
12
+ export async function serverFetch<T>(
13
+ path: string,
14
+ init: RequestInit = {},
15
+ ): Promise<T> {
16
+ const jar = await cookies();
17
+ const accessToken = jar.get(ACCESS_TOKEN_COOKIE)?.value;
18
+ const locale = jar.get(LOCALE_COOKIE)?.value;
19
+
20
+ const url = `${API_URL}${path.startsWith('/') ? path : `/${path}`}`;
21
+ const method = (init.method ?? 'GET').toUpperCase();
22
+
23
+ const headers: Record<string, string> = {
24
+ 'Content-Type': 'application/json',
25
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
26
+ ...(locale && { 'Accept-Language': locale }),
27
+ ...(init.headers as Record<string, string> | undefined),
28
+ };
29
+
30
+ const res = await fetch(url, {
31
+ ...init,
32
+ headers,
33
+ cache: init.cache ?? 'no-store',
34
+ });
35
+
36
+ const body = (await res.json()) as ApiResponse<T>;
37
+
38
+ if (!res.ok || body.result === 'ERROR') {
39
+ logApiError('SERVER_FETCH', {
40
+ url,
41
+ method,
42
+ status: res.status,
43
+ requestBody: typeof init.body === 'string' ? init.body : undefined,
44
+ responseBody: body,
45
+ });
46
+
47
+ captureApiError({
48
+ url,
49
+ apiPath: path,
50
+ method,
51
+ status: res.status,
52
+ responseBody: body,
53
+ });
54
+
55
+ throw new ApiError(res.status, body.error?.code ?? '', body.error);
56
+ }
57
+
58
+ return body.data;
59
+ }
@@ -0,0 +1,170 @@
1
+ /* Generated by @sh-ui/tokens — do not edit directly */
2
+ /* base=neutral radius=md mode=light-dark */
3
+
4
+ /* sh-ui:theme-colors-start */
5
+ :root {
6
+ --background: #FFFFFF;
7
+ --background-subtle: #FAFAFA;
8
+ --background-muted: #F5F5F5;
9
+ --background-inverse: #0A0A0A;
10
+ --foreground: #0A0A0A;
11
+ --foreground-muted: #525252;
12
+ --foreground-subtle: #A3A3A3;
13
+ --foreground-inverse: #FFFFFF;
14
+ --border: #E5E5E5;
15
+ --border-strong: #D4D4D4;
16
+ --primary: #171717;
17
+ --primary-foreground: #FAFAFA;
18
+ --primary-hover: #262626;
19
+ --ring: color-mix(in srgb, var(--primary) 50%, transparent);
20
+ --danger: #DC2626;
21
+ --danger-hover: color-mix(in srgb, var(--danger) 90%, black);
22
+ --danger-foreground: #FFFFFF;
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ :root:not(.light):not(.dark) {
26
+ --background: #0A0A0A;
27
+ --background-subtle: #171717;
28
+ --background-muted: #262626;
29
+ --background-inverse: #FFFFFF;
30
+ --foreground: #FAFAFA;
31
+ --foreground-muted: #A3A3A3;
32
+ --foreground-subtle: #737373;
33
+ --foreground-inverse: #0A0A0A;
34
+ --border: #262626;
35
+ --border-strong: #404040;
36
+ --primary: #FAFAFA;
37
+ --primary-foreground: #171717;
38
+ --primary-hover: #E5E5E5;
39
+ --danger: #DC2626;
40
+ --danger-hover: color-mix(in srgb, var(--danger) 90%, black);
41
+ --danger-foreground: #FFFFFF;
42
+ }
43
+ }
44
+ .dark {
45
+ --background: #0A0A0A;
46
+ --background-subtle: #171717;
47
+ --background-muted: #262626;
48
+ --background-inverse: #FFFFFF;
49
+ --foreground: #FAFAFA;
50
+ --foreground-muted: #A3A3A3;
51
+ --foreground-subtle: #737373;
52
+ --foreground-inverse: #0A0A0A;
53
+ --border: #262626;
54
+ --border-strong: #404040;
55
+ --primary: #FAFAFA;
56
+ --primary-foreground: #171717;
57
+ --primary-hover: #E5E5E5;
58
+ --danger: #DC2626;
59
+ --danger-hover: color-mix(in srgb, var(--danger) 90%, black);
60
+ --danger-foreground: #FFFFFF;
61
+ }
62
+ /* sh-ui:theme-colors-end */
63
+
64
+ :root {
65
+ /* sh-ui:theme-radius-start */
66
+ --radius: 0.5rem;
67
+ /* sh-ui:theme-radius-end */
68
+ /* sh-ui:theme-space-start */
69
+ --space-0: 0;
70
+ --space-1: 0.25rem;
71
+ --space-2: 0.5rem;
72
+ --space-3: 0.75rem;
73
+ --space-4: 1rem;
74
+ --space-5: 1.25rem;
75
+ --space-6: 1.5rem;
76
+ --space-8: 2rem;
77
+ --space-10: 2.5rem;
78
+ --space-12: 3rem;
79
+ --space-16: 4rem;
80
+ /* sh-ui:theme-space-end */
81
+ /* sh-ui:theme-text-start */
82
+ --text-xs: 0.75rem;
83
+ --text-sm: 0.875rem;
84
+ --text-base: 1rem;
85
+ --text-lg: 1.125rem;
86
+ --text-xl: 1.25rem;
87
+ --text-2xl: 1.5rem;
88
+ --text-3xl: 1.875rem;
89
+ --text-4xl: 2.25rem;
90
+ /* sh-ui:theme-text-end */
91
+ /* sh-ui:theme-weight-start */
92
+ --weight-regular: 400;
93
+ --weight-medium: 500;
94
+ --weight-semibold: 600;
95
+ --weight-bold: 700;
96
+ /* sh-ui:theme-weight-end */
97
+ /* sh-ui:theme-shadow-start */
98
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08);
99
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);
100
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.15);
101
+ --shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.18);
102
+ --shadow-menu: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
103
+ /* sh-ui:theme-shadow-end */
104
+ /* sh-ui:theme-duration-start */
105
+ --duration-fast: 120ms;
106
+ --duration-base: 160ms;
107
+ --duration-slow: 200ms;
108
+ /* sh-ui:theme-duration-end */
109
+ /* sh-ui:theme-ease-start */
110
+ --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
111
+ --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
112
+ /* sh-ui:theme-ease-end */
113
+ /* sh-ui:theme-control-start */
114
+ --control-sm: 2rem;
115
+ --control-md: 2.5rem;
116
+ --control-lg: 3rem;
117
+ /* sh-ui:theme-control-end */
118
+ /* sh-ui:theme-border-width-start */
119
+ --border-width: 1px;
120
+ --border-width-strong: 2px;
121
+ /* sh-ui:theme-border-width-end */
122
+ /* sh-ui:theme-gradient-start */
123
+ --gradient-primary: linear-gradient(135deg, #171717 0%, #525252 100%);
124
+ --gradient-surface: linear-gradient(180deg, #FFFFFF 0%, #F5F5F5 100%);
125
+ --gradient-overlay: linear-gradient(180deg, #000000 0%, #1F1F1F 100%);
126
+ /* sh-ui:theme-gradient-end */
127
+ --opacity-disabled: 0.5;
128
+ --z-base: 0;
129
+ --z-sticky: 100;
130
+ --z-dropdown: 200;
131
+ --z-overlay: 300;
132
+ --z-modal: 400;
133
+ --z-popover: 500;
134
+ --z-toast: 600;
135
+ --z-tooltip: 700;
136
+ --bp-sm: 640px;
137
+ --bp-md: 768px;
138
+ --bp-lg: 1024px;
139
+ --bp-xl: 1280px;
140
+ }
141
+
142
+ /* WCAG 2.5.5 — 터치 타겟 최소 44×44px. 마우스/스타일러스 대신 손가락 입력 시 control 높이를 보정. */
143
+ @media (hover: none) and (pointer: coarse) {
144
+ :root {
145
+ --control-sm: 2.75rem;
146
+ --control-md: 2.75rem;
147
+ }
148
+ }
149
+
150
+ /* WCAG AA 보장 — 사용자가 OS/브라우저에서 "고대비" 접근성 옵션 켰을 때.
151
+ * foreground-subtle, border, border-strong 만 강화 (다른 토큰은 이미 AA 통과). */
152
+ @media (prefers-contrast: high) {
153
+ :root {
154
+ --foreground-subtle: #767676;
155
+ --border: #767676;
156
+ --border-strong: #595959;
157
+ }
158
+ .dark {
159
+ --foreground-subtle: #B3B3B3;
160
+ --border: #757575;
161
+ --border-strong: #999999;
162
+ }
163
+ @media (prefers-color-scheme: dark) {
164
+ :root:not(.light):not(.dark) {
165
+ --foreground-subtle: #B3B3B3;
166
+ --border: #757575;
167
+ --border-strong: #999999;
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,18 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+
3
+ /**
4
+ * 테스트 전용 QueryClient — retry/refetch 끄고 gcTime 0 으로 격리.
5
+ */
6
+ export const createTestQueryClient = (): QueryClient =>
7
+ new QueryClient({
8
+ defaultOptions: {
9
+ queries: {
10
+ retry: false,
11
+ refetchOnWindowFocus: false,
12
+ gcTime: 0,
13
+ },
14
+ mutations: {
15
+ retry: false,
16
+ },
17
+ },
18
+ });
@@ -0,0 +1,2 @@
1
+ export { createTestQueryClient } from './createTestQueryClient';
2
+ export { renderWithProviders } from './renderWithProviders';
@@ -0,0 +1,65 @@
1
+ import type { ComponentType, ReactElement, ReactNode } from 'react';
2
+
3
+ import {
4
+ render,
5
+ type RenderOptions,
6
+ type RenderResult,
7
+ } from '@testing-library/react';
8
+ import userEvent from '@testing-library/user-event';
9
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
10
+ import { ThemeProvider } from 'next-themes';
11
+
12
+ import { createTestQueryClient } from './createTestQueryClient';
13
+
14
+ type Options = Omit<RenderOptions, 'wrapper'> & {
15
+ queryClient?: QueryClient;
16
+ /**
17
+ * 외부 Provider 가 필요할 때 (예: next-intl 활성 프로젝트에서
18
+ * `NextIntlClientProvider`) 사용. ThemeProvider 안쪽 / QueryClientProvider 바깥에 wrap.
19
+ */
20
+ extraWrapper?: ComponentType<{ children: ReactNode }>;
21
+ };
22
+
23
+ type Result = RenderResult & {
24
+ user: ReturnType<typeof userEvent.setup>;
25
+ queryClient: QueryClient;
26
+ };
27
+
28
+ /**
29
+ * RTL render + ThemeProvider + QueryClientProvider + userEvent setup 한 번에.
30
+ *
31
+ * next-intl 프로젝트에서 `useTranslations` 사용 컴포넌트 테스트 시:
32
+ *
33
+ * const Intl = ({ children }) => (
34
+ * <NextIntlClientProvider locale='ko' messages={ko}>
35
+ * {children}
36
+ * </NextIntlClientProvider>
37
+ * );
38
+ * renderWithProviders(<MyComponent />, { extraWrapper: Intl });
39
+ */
40
+ export const renderWithProviders = (
41
+ ui: ReactElement,
42
+ options: Options = {},
43
+ ): Result => {
44
+ const {
45
+ queryClient = createTestQueryClient(),
46
+ extraWrapper: Extra,
47
+ ...rtlOptions
48
+ } = options;
49
+
50
+ const Wrapper = ({ children }: { children: ReactNode }) => {
51
+ const inner = (
52
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
53
+ );
54
+ return (
55
+ <ThemeProvider attribute='class' defaultTheme='light' enableSystem={false}>
56
+ {Extra ? <Extra>{inner}</Extra> : inner}
57
+ </ThemeProvider>
58
+ );
59
+ };
60
+
61
+ const result = render(ui, { wrapper: Wrapper, ...rtlOptions });
62
+ const user = userEvent.setup();
63
+
64
+ return { ...result, user, queryClient };
65
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Date → 로케일 기반 날짜 포맷 (시간 없음).
3
+ *
4
+ * 비-i18n 프로젝트면 default 'ko-KR' 사용. next-intl 활성 시엔 이 util 을 직접
5
+ * 부르지 말고 같은 모듈의 hook (`useFormatDate`) 을 사용하세요 — 현재 locale 을
6
+ * 자동으로 따릅니다 (next-intl 플러그인이 emit).
7
+ */
8
+ export const formatDate = (date: Date, locale = 'ko-KR'): string =>
9
+ new Intl.DateTimeFormat(locale, {
10
+ year: 'numeric',
11
+ month: '2-digit',
12
+ day: '2-digit',
13
+ }).format(date);
14
+
15
+ /**
16
+ * Date → 로케일 기반 날짜 + 시간 포맷 (24h).
17
+ */
18
+ export const formatDateTime = (date: Date, locale = 'ko-KR'): string =>
19
+ new Intl.DateTimeFormat(locale, {
20
+ year: 'numeric',
21
+ month: '2-digit',
22
+ day: '2-digit',
23
+ hour: '2-digit',
24
+ minute: '2-digit',
25
+ hour12: false,
26
+ }).format(date);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 숫자 → 통화 포맷. default ko-KR + KRW 이지만 두 인자 모두 override 가능.
3
+ * 예: formatPrice(12000) → "₩12,000"
4
+ * formatPrice(99.5, 'en-US', 'USD') → "$99.50"
5
+ *
6
+ * next-intl 활성 시엔 같은 모듈의 hook (`useFormatPrice`) 을 사용하면 현재
7
+ * locale 을 자동으로 따릅니다 (next-intl 플러그인이 emit).
8
+ */
9
+ export const formatPrice = (
10
+ amount: number,
11
+ locale = 'ko-KR',
12
+ currency = 'KRW',
13
+ ): string =>
14
+ new Intl.NumberFormat(locale, {
15
+ style: 'currency',
16
+ currency,
17
+ maximumFractionDigits: currency === 'KRW' ? 0 : 2,
18
+ }).format(amount);
@@ -0,0 +1,14 @@
1
+ import { isServer } from '@tanstack/react-query';
2
+
3
+ import {
4
+ getBrowserQueryClient,
5
+ getServerQueryClient,
6
+ } from '@/lib/api/queryClient';
7
+
8
+ /**
9
+ * RSC 와 클라이언트 양쪽에서 안전하게 호출 가능한 QueryClient 핸들.
10
+ * 서버에서는 React `cache()` 로 요청 단위 싱글턴, 브라우저에서는 모듈 싱글턴.
11
+ */
12
+ export function getQueryClient() {
13
+ return isServer ? getServerQueryClient() : getBrowserQueryClient();
14
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,3 @@
1
+ // 순수 fetch 레이어 — React 모름. hooks.ts 가 useQuery/useMutation 으로 감싼다.
2
+ // 공용 fetcher 는 `@/lib/api/clientFetch` 등 src/lib/api/ 에서 import.
3
+ export {};
@@ -0,0 +1,3 @@
1
+ // 페이지 전용 React 어댑터 — TanStack Query useQuery/useMutation 래퍼.
2
+ // query key 와 invalidation 을 이 파일에 모아 다른 mutation 추가 시 빠뜨리지 않게.
3
+ export {};
@@ -0,0 +1,14 @@
1
+ // MES 프리셋의 페이지 본체 슬롯 예시.
2
+ //
3
+ // 페이지 폴더 구조 (관용):
4
+ // index.tsx ─ 페이지 컴포넌트 (이 파일)
5
+ // components/ ─ 페이지 전용 UI (폼, 다이얼로그, 테이블 등)
6
+ // api.ts ─ 순수 fetch 레이어
7
+ // hooks.ts ─ TanStack Query 어댑터 (useQuery / useMutation)
8
+ // schema.ts ─ zod 스키마
9
+ // columns.ts ─ (옵션) 리스트 페이지의 테이블 컬럼 정의
10
+ //
11
+ // 새 페이지를 추가할 때 이 폴더를 복사해 이름만 바꾸세요.
12
+ export default function SignInPage() {
13
+ return null;
14
+ }
@@ -0,0 +1,2 @@
1
+ // zod 스키마 — entity 와 form 이 다르면 두 개 (예: createXxxSchema 는 id/createdAt 제외).
2
+ export {};
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "lib": ["es2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "moduleDetection": "force",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "esModuleInterop": true,
12
+ "isolatedModules": true,
13
+ "resolveJsonModule": true,
14
+ "skipLibCheck": true,
15
+ "strict": true,
16
+ "noUncheckedIndexedAccess": true,
17
+ "allowJs": true,
18
+ "jsx": "preserve",
19
+ "noEmit": true,
20
+ "incremental": true,
21
+ "baseUrl": ".",
22
+ "paths": {
23
+ "@/*": ["./src/*"]
24
+ },
25
+ "plugins": [
26
+ {
27
+ "name": "next"
28
+ }
29
+ ]
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "next.config.ts",
34
+ "**/*.ts",
35
+ "**/*.tsx",
36
+ ".next/types/**/*.ts"
37
+ ],
38
+ "exclude": ["node_modules"]
39
+ }