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.
- package/data/changelog/versions.json +26 -0
- package/data/registry/react/components/switch/index.tailwind.tsx +1 -1
- package/data/registry/react/components/switch/styles.css +6 -0
- package/data/registry/react/components/switch/styles.module.css +6 -0
- package/data/registry/react/tokens-used.json +3 -1
- package/package.json +1 -1
- package/src/create/architectures/index.js +2 -1
- package/src/create/architectures/mes.js +53 -0
- package/templates/monorepo/packages/eslint-config/mes.js +82 -0
- package/templates/monorepo/packages/eslint-config/package.json +2 -1
- package/templates/nextjs-app/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-app/_arch/mes/app/layout.tsx +16 -0
- package/templates/nextjs-app/_arch/mes/app/sign-in/page.tsx +1 -0
- package/templates/nextjs-app/_arch/mes/eslint.config.js +10 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-app/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-app/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/index.tsx +1 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-app/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
- package/templates/nextjs-app/_arch/mes/src/hooks/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/hooks/useAppMutation.ts +59 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/error.ts +12 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/http.ts +13 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/observability.ts +20 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-app/_arch/mes/src/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-app/_arch/mes/src/lib/config/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/index.ts +2 -0
- package/templates/nextjs-app/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/formatDate.ts +26 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
- package/templates/nextjs-app/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/api.ts +3 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/index.tsx +14 -0
- package/templates/nextjs-app/_arch/mes/src/pages/sign-in/schema.ts +2 -0
- package/templates/nextjs-app/_arch/mes/tsconfig.json +24 -0
- package/templates/nextjs-standalone/_arch/mes/app/api/proxy/[...path]/route.ts +112 -0
- package/templates/nextjs-standalone/_arch/mes/app/globals.css +49 -0
- package/templates/nextjs-standalone/_arch/mes/app/layout.tsx +16 -0
- package/templates/nextjs-standalone/_arch/mes/app/sign-in/page.tsx +1 -0
- package/templates/nextjs-standalone/_arch/mes/eslint.config.js +137 -0
- package/templates/nextjs-standalone/_arch/mes/sh-ui.config.json +22 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/FallbackBoundary/index.tsx +89 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/common/PrefetchBoundary/index.tsx +35 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/layouts/RootLayout.tsx +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/GlobalProvider/index.tsx +23 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/index.tsx +1 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/QueryClientProvider.tsx +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/components/providers/theme/ThemeProvider.tsx +27 -0
- package/templates/nextjs-standalone/_arch/mes/src/hooks/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/hooks/useAppMutation.ts +59 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/apiTypes.ts +21 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/clientFetch.ts +40 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/error.ts +12 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/errorMessages.ts +37 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/http.ts +13 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/observability.ts +20 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/queryClient.ts +30 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/api/serverFetch.ts +59 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/config/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/styles/tokens.css +170 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/createTestQueryClient.ts +18 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/index.ts +2 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/test/renderWithProviders.tsx +65 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatDate.ts +26 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/formatPrice.ts +18 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/getQueryClient.ts +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/lib/utils/utils.ts +6 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/api.ts +3 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/components/.gitkeep +0 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/hooks.ts +3 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/index.tsx +14 -0
- package/templates/nextjs-standalone/_arch/mes/src/pages/sign-in/schema.ts +2 -0
- 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
|
+
}
|
|
File without changes
|
|
@@ -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,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
|
+
}
|
|
File without changes
|
|
@@ -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,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
|
+
}
|