@farmzone/fz-template-react 1.0.5 → 1.0.7

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 (64) hide show
  1. package/README.md +102 -102
  2. package/bin/create.js +108 -108
  3. package/package.json +24 -24
  4. package/template/.env.example +5 -5
  5. package/template/.prettierrc +9 -9
  6. package/template/eslint.config.js +26 -26
  7. package/template/index.css +32 -32
  8. package/template/index.html +19 -19
  9. package/template/package.json +54 -55
  10. package/template/pnpm-lock.yaml +4214 -4214
  11. package/template/public/mockServiceWorker.js +349 -349
  12. package/template/src/app/App.tsx +26 -26
  13. package/template/src/app/api/api.ts +178 -178
  14. package/template/src/app/api/queries.ts +335 -326
  15. package/template/src/app/api/queryKey.ts +7 -7
  16. package/template/src/app/api/token.ts +8 -7
  17. package/template/src/app/layout/Layout.tsx +33 -33
  18. package/template/src/app/layout/ListContents.tsx +9 -9
  19. package/template/src/app/layout/ListHeader.tsx +41 -41
  20. package/template/src/app/layout/MultiTabNav.tsx +106 -101
  21. package/template/src/app/layout/Sidebar.tsx +33 -33
  22. package/template/src/app/layout/UserInfo.tsx +95 -94
  23. package/template/src/app/layout/menu.ts +79 -55
  24. package/template/src/app/layout/tabSwitchStore.ts +11 -11
  25. package/template/src/app/router/Router.tsx +56 -56
  26. package/template/src/app/store/index.ts +26 -26
  27. package/template/src/index.tsx +21 -21
  28. package/template/src/mocks/browser.ts +17 -17
  29. package/template/src/mocks/handlers.ts +43 -43
  30. package/template/src/mocks/scenarios.ts +57 -57
  31. package/template/src/pages/dashboard/index.tsx +541 -541
  32. package/template/src/pages/error/Error.tsx +29 -29
  33. package/template/src/pages/error/NotFound.tsx +27 -27
  34. package/template/src/pages/login/index.tsx +317 -317
  35. package/template/src/pages/post/PostFormModal.tsx +128 -128
  36. package/template/src/pages/post/detail/index.tsx +545 -548
  37. package/template/src/pages/post/index.tsx +266 -266
  38. package/template/src/pages/sample/SampleFormModal.tsx +188 -115
  39. package/template/src/pages/sample/detail/index.tsx +551 -400
  40. package/template/src/pages/sample/index.tsx +298 -278
  41. package/template/src/pages/sample/modal/index.tsx +308 -300
  42. package/template/src/pages/system/log/index.tsx +173 -173
  43. package/template/src/pages/user/config/columns.tsx +102 -102
  44. package/template/src/pages/user/config/schema.ts +54 -54
  45. package/template/src/pages/user/index.tsx +704 -641
  46. package/template/src/shared/components/CommentInput.tsx +243 -243
  47. package/template/src/shared/components/FilePreviewCard.tsx +71 -70
  48. package/template/src/shared/config/text.ts +27 -27
  49. package/template/src/shared/config/type.ts +40 -40
  50. package/template/src/shared/utils/format.ts +11 -11
  51. package/template/src/types/auth.ts +10 -10
  52. package/template/src/types/comment.ts +33 -33
  53. package/template/src/types/common.ts +19 -19
  54. package/template/src/types/dashboard.ts +53 -53
  55. package/template/src/types/index.ts +16 -16
  56. package/template/src/types/log.ts +21 -21
  57. package/template/src/types/post.ts +32 -32
  58. package/template/src/types/sample.ts +33 -29
  59. package/template/src/types/user.ts +51 -51
  60. package/template/src/vite-env.d.ts +10 -10
  61. package/template/tsconfig.app.json +32 -32
  62. package/template/tsconfig.json +7 -7
  63. package/template/tsconfig.node.json +26 -26
  64. package/template/vite.config.ts +13 -13
@@ -1,26 +1,26 @@
1
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
- import { ToastProvider } from "@farmzone/fz-react-ui";
3
-
4
- import Router from "@/app/router/Router";
5
-
6
- const queryClient = new QueryClient({
7
- defaultOptions: {
8
- queries: {
9
- staleTime: 0,
10
- gcTime: 5 * 60 * 1000,
11
- refetchOnMount: true,
12
- refetchOnReconnect: false,
13
- refetchOnWindowFocus: false,
14
- retry: 1,
15
- },
16
- },
17
- });
18
-
19
- export default function App() {
20
- return (
21
- <QueryClientProvider client={queryClient}>
22
- <ToastProvider />
23
- <Router />
24
- </QueryClientProvider>
25
- );
26
- }
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import { ToastProvider } from "@farmzone/fz-react-ui";
3
+
4
+ import Router from "@/app/router/Router";
5
+
6
+ const queryClient = new QueryClient({
7
+ defaultOptions: {
8
+ queries: {
9
+ staleTime: 0,
10
+ gcTime: 5 * 60 * 1000,
11
+ refetchOnMount: true,
12
+ refetchOnReconnect: false,
13
+ refetchOnWindowFocus: false,
14
+ retry: 1,
15
+ },
16
+ },
17
+ });
18
+
19
+ export default function App() {
20
+ return (
21
+ <QueryClientProvider client={queryClient}>
22
+ <ToastProvider />
23
+ <Router />
24
+ </QueryClientProvider>
25
+ );
26
+ }
@@ -1,178 +1,178 @@
1
- import axios, { type AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from "axios";
2
- import dayjs from "dayjs";
3
- import utc from "dayjs/plugin/utc";
4
- import Cookies from "js-cookie";
5
- import { v4 as uuidv4 } from "uuid";
6
-
7
- import { clearUserToken } from "@/app/api/token";
8
- import { COMMON_API_MSG_TO_KR, SERVER_ERROR_MESSAGES } from "@/shared/config/text";
9
- import { toast } from "@farmzone/fz-react-ui";
10
-
11
- dayjs.extend(utc);
12
-
13
- declare module "axios" {
14
- interface InternalAxiosRequestConfig {
15
- _retried?: boolean;
16
- }
17
- }
18
-
19
- const RETRYABLE_STATUSES = new Set([408, 503]);
20
- const RETRY_DELAY_MS = 1_000;
21
-
22
- export const isRetryable = (error: AxiosError): boolean => {
23
- if (!error.response) return true;
24
- return RETRYABLE_STATUSES.has(error.response.status);
25
- };
26
-
27
- interface RefreshTokenResponse {
28
- accessToken: string;
29
- refreshToken: string;
30
- accessTokenExpiresAt: string;
31
- refreshTokenExpiresAt: string;
32
- }
33
-
34
- export const basePath = `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`;
35
-
36
- export const apiInstance = axios.create({
37
- baseURL: `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`,
38
- });
39
-
40
- export const refreshTokenInstance = axios.create({
41
- baseURL: `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`,
42
- });
43
-
44
- export const apiFormDataInstance = axios.create({
45
- headers: {
46
- Accept: "application/json",
47
- "Content-Type": "multipart/form-data",
48
- },
49
- transformRequest: (formData) => formData,
50
- baseURL: `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`,
51
- });
52
-
53
- export const apiFileInstance = axios.create({
54
- baseURL: `${import.meta.env.VITE_APP_API_HOST}`,
55
- });
56
-
57
- // --- Request interceptors ---
58
-
59
- const attachAccessToken = (config: InternalAxiosRequestConfig) => {
60
- const token = Cookies.get("AccessToken");
61
- if (token) {
62
- config.headers.Authorization = `Bearer ${token}`;
63
- }
64
- return config;
65
- };
66
-
67
- const attachTraceId = (config: InternalAxiosRequestConfig) => {
68
- config.headers["X-Trace-Id"] = uuidv4();
69
- return config;
70
- };
71
-
72
- apiInstance.interceptors.request.use(attachAccessToken);
73
- apiInstance.interceptors.request.use(attachTraceId);
74
- apiFormDataInstance.interceptors.request.use(attachAccessToken);
75
- apiFormDataInstance.interceptors.request.use(attachTraceId);
76
- apiFileInstance.interceptors.request.use(attachAccessToken);
77
- apiFileInstance.interceptors.request.use(attachTraceId);
78
- refreshTokenInstance.interceptors.request.use(attachTraceId);
79
-
80
- refreshTokenInstance.interceptors.request.use((config) => {
81
- const token = Cookies.get("RefreshToken");
82
-
83
- if (!token) {
84
- clearUserToken();
85
- window.location.href = "/login";
86
- return Promise.reject(new Error("No refresh token"));
87
- }
88
-
89
- config.headers.Authorization = `Bearer ${token}`;
90
- return config;
91
- });
92
-
93
- // --- 401 token refresh ---
94
-
95
- let refreshPromise: Promise<RefreshTokenResponse> | null = null;
96
-
97
- const handleTokenRefresh = async (instance: AxiosInstance, error: AxiosError) => {
98
- if (refreshPromise) {
99
- await refreshPromise;
100
- return instance(error.config!);
101
- }
102
-
103
- refreshPromise = refreshTokenInstance
104
- .post<RefreshTokenResponse>("/auth/refresh")
105
- .then((res) => {
106
- const accessExpiry = dayjs.utc(res.data.accessTokenExpiresAt, "YYYY-MM-DD HH:mm:ss");
107
- const refreshExpiry = dayjs.utc(res.data.refreshTokenExpiresAt, "YYYY-MM-DD HH:mm:ss");
108
-
109
- Cookies.set("AccessToken", res.data.accessToken, { expires: accessExpiry.toDate() });
110
- Cookies.set("RefreshToken", res.data.refreshToken, { expires: refreshExpiry.toDate() });
111
-
112
- return res.data;
113
- })
114
- .catch((err) => {
115
- console.error("ERROR:: refresh token 갱신 실패", err);
116
- clearUserToken();
117
- window.location.href = "/login";
118
- throw err;
119
- })
120
- .finally(() => {
121
- refreshPromise = null;
122
- });
123
-
124
- await refreshPromise;
125
- return instance(error.config!);
126
- };
127
-
128
- // --- Response error handler ---
129
-
130
- const createErrorHandler = (instance: AxiosInstance) => async (err: AxiosError) => {
131
- const code = (err.response?.data as { code?: string } | undefined)?.code;
132
- const status = err.response?.status;
133
- const rawMessage = (err.response?.data as { message?: string } | undefined)?.message ?? err?.message;
134
- const message = (rawMessage && COMMON_API_MSG_TO_KR[rawMessage]) ?? rawMessage;
135
-
136
- const isAuthEndpoint = err.config?.url?.includes("/auth/login");
137
- if (status === 401 && code !== "INVALID_LOGIN_CREDENTIAL" && !isAuthEndpoint) {
138
- return handleTokenRefresh(instance, err);
139
- }
140
-
141
- if (isRetryable(err) && !err.config?._retried) {
142
- err.config!._retried = true;
143
- await new Promise<void>((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
144
- return instance(err.config!);
145
- }
146
-
147
- if (isRetryable(err)) {
148
- toast.error(
149
- status === 408
150
- ? (message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_NETWORK.replace("\n", " "))
151
- : SERVER_ERROR_MESSAGES.SERVER_ERROR_NETWORK.replace("\n", " "),
152
- { id: "server-error" },
153
- );
154
- } else if (status === 500) {
155
- toast.error(SERVER_ERROR_MESSAGES.SERVER_ERROR_TEMP.replace("\n", " "), {
156
- id: "server-error",
157
- });
158
- } else if (status === 400) {
159
- toast.error(message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_INVALID, { id: "server-error" });
160
- } else if (
161
- (status === 401 && code === "INVALID_LOGIN_CREDENTIAL") ||
162
- (status === 403 && code === "ACCOUNT_DISABLED")
163
- ) {
164
- toast.error(message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_INVALID, { id: "server-error" });
165
- } else if (status === 403 || status === 404) {
166
- // window.location.href = "/error";
167
- } else if (status === 409 || status === 422) {
168
- toast.error(message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_INVALID, { id: "server-error" });
169
- }
170
-
171
- return Promise.reject(err);
172
- };
173
-
174
- // --- Response interceptors ---
175
-
176
- apiInstance.interceptors.response.use((res) => res, createErrorHandler(apiInstance));
177
- apiFormDataInstance.interceptors.response.use((res) => res, createErrorHandler(apiFormDataInstance));
178
- apiFileInstance.interceptors.response.use((res) => res, createErrorHandler(apiFileInstance));
1
+ import axios, { type AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from "axios";
2
+ import dayjs from "dayjs";
3
+ import utc from "dayjs/plugin/utc";
4
+ import Cookies from "js-cookie";
5
+ import { v4 as uuidv4 } from "uuid";
6
+
7
+ import { clearUserToken } from "@/app/api/token";
8
+ import { COMMON_API_MSG_TO_KR, SERVER_ERROR_MESSAGES } from "@/shared/config/text";
9
+ import { toast } from "@farmzone/fz-react-ui";
10
+
11
+ dayjs.extend(utc);
12
+
13
+ declare module "axios" {
14
+ interface InternalAxiosRequestConfig {
15
+ _retried?: boolean;
16
+ }
17
+ }
18
+
19
+ const RETRYABLE_STATUSES = new Set([408, 503]);
20
+ const RETRY_DELAY_MS = 1_000;
21
+
22
+ export const isRetryable = (error: AxiosError): boolean => {
23
+ if (!error.response) return true;
24
+ return RETRYABLE_STATUSES.has(error.response.status);
25
+ };
26
+
27
+ interface RefreshTokenResponse {
28
+ accessToken: string;
29
+ refreshToken: string;
30
+ accessTokenExpiresAt: string;
31
+ refreshTokenExpiresAt: string;
32
+ }
33
+
34
+ export const basePath = `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`;
35
+
36
+ export const apiInstance = axios.create({
37
+ baseURL: `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`,
38
+ });
39
+
40
+ export const refreshTokenInstance = axios.create({
41
+ baseURL: `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`,
42
+ });
43
+
44
+ export const apiFormDataInstance = axios.create({
45
+ headers: {
46
+ Accept: "application/json",
47
+ "Content-Type": "multipart/form-data",
48
+ },
49
+ transformRequest: (formData) => formData,
50
+ baseURL: `${import.meta.env.VITE_APP_API_HOST}/api/${import.meta.env.VITE_APP_API_VERSION}`,
51
+ });
52
+
53
+ export const apiFileInstance = axios.create({
54
+ baseURL: `${import.meta.env.VITE_APP_API_HOST}`,
55
+ });
56
+
57
+ // --- Request interceptors ---
58
+
59
+ const attachAccessToken = (config: InternalAxiosRequestConfig) => {
60
+ const token = Cookies.get("AccessToken");
61
+ if (token) {
62
+ config.headers.Authorization = `Bearer ${token}`;
63
+ }
64
+ return config;
65
+ };
66
+
67
+ const attachTraceId = (config: InternalAxiosRequestConfig) => {
68
+ config.headers["X-Trace-Id"] = uuidv4();
69
+ return config;
70
+ };
71
+
72
+ apiInstance.interceptors.request.use(attachAccessToken);
73
+ apiInstance.interceptors.request.use(attachTraceId);
74
+ apiFormDataInstance.interceptors.request.use(attachAccessToken);
75
+ apiFormDataInstance.interceptors.request.use(attachTraceId);
76
+ apiFileInstance.interceptors.request.use(attachAccessToken);
77
+ apiFileInstance.interceptors.request.use(attachTraceId);
78
+ refreshTokenInstance.interceptors.request.use(attachTraceId);
79
+
80
+ refreshTokenInstance.interceptors.request.use((config) => {
81
+ const token = Cookies.get("RefreshToken");
82
+
83
+ if (!token) {
84
+ clearUserToken();
85
+ window.location.href = "/login";
86
+ return Promise.reject(new Error("No refresh token"));
87
+ }
88
+
89
+ config.headers.Authorization = `Bearer ${token}`;
90
+ return config;
91
+ });
92
+
93
+ // --- 401 token refresh ---
94
+
95
+ let refreshPromise: Promise<RefreshTokenResponse> | null = null;
96
+
97
+ const handleTokenRefresh = async (instance: AxiosInstance, error: AxiosError) => {
98
+ if (refreshPromise) {
99
+ await refreshPromise;
100
+ return instance(error.config!);
101
+ }
102
+
103
+ refreshPromise = refreshTokenInstance
104
+ .post<RefreshTokenResponse>("/auth/refresh")
105
+ .then((res) => {
106
+ const accessExpiry = dayjs.utc(res.data.accessTokenExpiresAt, "YYYY-MM-DD HH:mm:ss");
107
+ const refreshExpiry = dayjs.utc(res.data.refreshTokenExpiresAt, "YYYY-MM-DD HH:mm:ss");
108
+
109
+ Cookies.set("AccessToken", res.data.accessToken, { expires: accessExpiry.toDate() });
110
+ Cookies.set("RefreshToken", res.data.refreshToken, { expires: refreshExpiry.toDate() });
111
+
112
+ return res.data;
113
+ })
114
+ .catch((err) => {
115
+ console.error("ERROR:: refresh token 갱신 실패", err);
116
+ clearUserToken();
117
+ window.location.href = "/login";
118
+ throw err;
119
+ })
120
+ .finally(() => {
121
+ refreshPromise = null;
122
+ });
123
+
124
+ await refreshPromise;
125
+ return instance(error.config!);
126
+ };
127
+
128
+ // --- Response error handler ---
129
+
130
+ const createErrorHandler = (instance: AxiosInstance) => async (err: AxiosError) => {
131
+ const code = (err.response?.data as { code?: string } | undefined)?.code;
132
+ const status = err.response?.status;
133
+ const rawMessage = (err.response?.data as { message?: string } | undefined)?.message ?? err?.message;
134
+ const message = (rawMessage && COMMON_API_MSG_TO_KR[rawMessage]) ?? rawMessage;
135
+
136
+ const isAuthEndpoint = err.config?.url?.includes("/auth/login");
137
+ if (status === 401 && code !== "INVALID_LOGIN_CREDENTIAL" && !isAuthEndpoint) {
138
+ return handleTokenRefresh(instance, err);
139
+ }
140
+
141
+ if (isRetryable(err) && !err.config?._retried) {
142
+ err.config!._retried = true;
143
+ await new Promise<void>((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
144
+ return instance(err.config!);
145
+ }
146
+
147
+ if (isRetryable(err)) {
148
+ toast.error(
149
+ status === 408
150
+ ? (message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_NETWORK.replace("\n", " "))
151
+ : SERVER_ERROR_MESSAGES.SERVER_ERROR_NETWORK.replace("\n", " "),
152
+ { id: "server-error" },
153
+ );
154
+ } else if (status === 500) {
155
+ toast.error(SERVER_ERROR_MESSAGES.SERVER_ERROR_TEMP.replace("\n", " "), {
156
+ id: "server-error",
157
+ });
158
+ } else if (status === 400) {
159
+ toast.error(message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_INVALID, { id: "server-error" });
160
+ } else if (
161
+ (status === 401 && code === "INVALID_LOGIN_CREDENTIAL") ||
162
+ (status === 403 && code === "ACCOUNT_DISABLED")
163
+ ) {
164
+ toast.error(message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_INVALID, { id: "server-error" });
165
+ } else if (status === 403 || status === 404) {
166
+ // window.location.href = "/error";
167
+ } else if (status === 409 || status === 422) {
168
+ toast.error(message ?? SERVER_ERROR_MESSAGES.SERVER_ERROR_INVALID, { id: "server-error" });
169
+ }
170
+
171
+ return Promise.reject(err);
172
+ };
173
+
174
+ // --- Response interceptors ---
175
+
176
+ apiInstance.interceptors.response.use((res) => res, createErrorHandler(apiInstance));
177
+ apiFormDataInstance.interceptors.response.use((res) => res, createErrorHandler(apiFormDataInstance));
178
+ apiFileInstance.interceptors.response.use((res) => res, createErrorHandler(apiFileInstance));