@farmzone/fz-template-react 1.0.6 → 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 -54
  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 -335
  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 -545
  37. package/template/src/pages/post/index.tsx +266 -266
  38. package/template/src/pages/sample/SampleFormModal.tsx +188 -188
  39. package/template/src/pages/sample/detail/index.tsx +551 -517
  40. package/template/src/pages/sample/index.tsx +298 -298
  41. package/template/src/pages/sample/modal/index.tsx +308 -308
  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 -650
  46. package/template/src/shared/components/CommentInput.tsx +243 -243
  47. package/template/src/shared/components/FilePreviewCard.tsx +71 -71
  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 -33
  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,188 +1,188 @@
1
- import { useEffect, useRef } from "react";
2
- import { useFormContext } from "react-hook-form";
3
- import {
4
- Button,
5
- FileUploader,
6
- Modal,
7
- ModalBody,
8
- ModalFooter,
9
- ModalIconHeader,
10
- SubmitForm,
11
- } from "@farmzone/fz-react-ui";
12
- import { Paperclip, X } from "lucide-react";
13
- import { z } from "zod";
14
-
15
- import { checkSampleNameAvailable } from "@/app/api/queries";
16
- import type { FileResponse } from "@/types";
17
-
18
- export const sampleFormSchema = z.object({
19
- name: z.string().min(1, "이름을 입력해 주세요."),
20
- description: z.string().min(1, "설명을 입력해 주세요."),
21
- category: z.string(),
22
- priority: z.coerce.number().min(1).max(10),
23
- active: z.boolean(),
24
- });
25
-
26
- export type SampleFormData = z.infer<typeof sampleFormSchema>;
27
-
28
- export const SAMPLE_FORM_DEFAULT_VALUES: SampleFormData = {
29
- name: "",
30
- description: "",
31
- category: "BASIC",
32
- priority: 1,
33
- active: true,
34
- };
35
-
36
- const CATEGORY_OPTIONS = [
37
- { label: "기본", value: "BASIC" },
38
- { label: "고급", value: "ADVANCED" },
39
- ];
40
-
41
- const PRIORITY_OPTIONS = Array.from({ length: 10 }, (_, i) => ({
42
- label: String(i + 1),
43
- value: String(i + 1),
44
- }));
45
-
46
- function SampleNameChecker({ originalName }: { originalName?: string }) {
47
- const { watch, setError, clearErrors, formState } = useFormContext();
48
- const name = watch("name") as string;
49
- const isDuplicateRef = useRef(false);
50
-
51
- useEffect(() => {
52
- if (!name || name === originalName) {
53
- isDuplicateRef.current = false;
54
- clearErrors("name");
55
- return;
56
- }
57
- const timer = setTimeout(async () => {
58
- const isDuplicate = await checkSampleNameAvailable(name);
59
- isDuplicateRef.current = isDuplicate;
60
- if (isDuplicate) {
61
- setError("name", { type: "manual", message: "이미 사용 중인 샘플명입니다." });
62
- } else {
63
- clearErrors("name");
64
- }
65
- }, 500);
66
- return () => clearTimeout(timer);
67
- }, [name, originalName, setError, clearErrors]);
68
-
69
- useEffect(() => {
70
- if (isDuplicateRef.current && !formState.errors.name) {
71
- setError("name", { type: "manual", message: "이미 사용 중인 샘플명입니다." });
72
- }
73
- }, [formState.errors.name, setError]);
74
-
75
- return null;
76
- }
77
-
78
- interface SampleFormModalProps {
79
- mode: "create" | "edit";
80
- isOpen: boolean;
81
- onClose: () => void;
82
- defaultValues?: SampleFormData;
83
- onSubmit: (data: SampleFormData) => Promise<void>;
84
- isPending: boolean;
85
- formKey?: string | number;
86
- originalName?: string;
87
- pendingFiles: Array<File>;
88
- onPendingFilesChange: (files: Array<File>) => void;
89
- existingFiles?: Array<FileResponse>;
90
- onDeleteExistingFile?: (id: number) => void;
91
- }
92
-
93
- export function SampleFormModal({
94
- mode,
95
- isOpen,
96
- onClose,
97
- defaultValues,
98
- onSubmit,
99
- isPending,
100
- formKey,
101
- originalName,
102
- pendingFiles,
103
- onPendingFilesChange,
104
- existingFiles,
105
- onDeleteExistingFile,
106
- }: SampleFormModalProps) {
107
- const formId = mode === "create" ? "sample-create-form" : "sample-edit-form";
108
- const title = mode === "create" ? "샘플 등록" : "샘플 수정";
109
- const submitLabel = mode === "create" ? "등록" : "수정";
110
-
111
- return (
112
- <Modal isOpen={isOpen} onClose={onClose} contentClassName="max-w-3xl rounded-xl bg-white">
113
- <ModalIconHeader type={mode} title={title} onClose={onClose} />
114
- <ModalBody className="px-6 py-5">
115
- <div className="space-y-4">
116
- <div className="overflow-hidden rounded-lg border border-gray-200">
117
- <SubmitForm
118
- key={formKey}
119
- formId={formId}
120
- schema={sampleFormSchema}
121
- defaultValues={defaultValues ?? SAMPLE_FORM_DEFAULT_VALUES}
122
- onSubmit={onSubmit}
123
- >
124
- <SampleNameChecker originalName={originalName} />
125
- <SubmitForm.Row formKey="name" label="이름" required maxLength={100} />
126
- <SubmitForm.Row
127
- formKey="description"
128
- formType="textarea"
129
- label="설명"
130
- required
131
- maxLength={500}
132
- />
133
- <SubmitForm.Row
134
- formKey="category"
135
- formType="radio"
136
- label="카테고리"
137
- options={CATEGORY_OPTIONS}
138
- />
139
- <SubmitForm.Row
140
- formKey="priority"
141
- formType="select"
142
- label="우선순위"
143
- options={PRIORITY_OPTIONS}
144
- />
145
- <SubmitForm.Row formKey="active" formType="switch" label="사용 여부" />
146
- </SubmitForm>
147
- </div>
148
-
149
- <div>
150
- <p className="mb-1.5 text-sm font-medium text-gray-700">첨부파일</p>
151
- {existingFiles && existingFiles.length > 0 && (
152
- <ul className="mb-3 space-y-1.5">
153
- {existingFiles.map((file) => (
154
- <li
155
- key={file.id}
156
- className="flex items-center justify-between rounded-md border border-gray-200 bg-gray-50 px-3 py-2"
157
- >
158
- <span className="flex items-center gap-1.5 truncate text-sm text-gray-700">
159
- <Paperclip size={13} className="shrink-0 text-gray-400" />
160
- {file.fileName}
161
- </span>
162
- <button
163
- type="button"
164
- onClick={() => onDeleteExistingFile?.(file.id)}
165
- className="ml-2 shrink-0 rounded p-0.5 text-gray-400 hover:bg-red-50 hover:text-red-500"
166
- aria-label="파일 삭제"
167
- >
168
- <X size={14} />
169
- </button>
170
- </li>
171
- ))}
172
- </ul>
173
- )}
174
- <FileUploader files={pendingFiles} onChange={onPendingFilesChange} multiple />
175
- </div>
176
- </div>
177
- </ModalBody>
178
- <ModalFooter className="flex justify-end gap-2 border-t border-gray-200 bg-neutral-50 px-5 py-3">
179
- <Button type="submit" form={formId} variant="save" disabled={isPending}>
180
- {isPending ? `${submitLabel} 중...` : submitLabel}
181
- </Button>
182
- <Button variant="outline" onClick={onClose}>
183
- 취소
184
- </Button>
185
- </ModalFooter>
186
- </Modal>
187
- );
188
- }
1
+ import { useEffect, useRef } from "react";
2
+ import { useFormContext } from "react-hook-form";
3
+ import {
4
+ Button,
5
+ FileUploader,
6
+ Modal,
7
+ ModalBody,
8
+ ModalFooter,
9
+ ModalIconHeader,
10
+ SubmitForm,
11
+ } from "@farmzone/fz-react-ui";
12
+ import { Paperclip, X } from "lucide-react";
13
+ import { z } from "zod";
14
+
15
+ import { checkSampleNameAvailable } from "@/app/api/queries";
16
+ import type { FileResponse } from "@/types";
17
+
18
+ export const sampleFormSchema = z.object({
19
+ name: z.string().min(1, "이름을 입력해 주세요."),
20
+ description: z.string().min(1, "설명을 입력해 주세요."),
21
+ category: z.string(),
22
+ priority: z.coerce.number().min(1).max(10),
23
+ active: z.boolean(),
24
+ });
25
+
26
+ export type SampleFormData = z.infer<typeof sampleFormSchema>;
27
+
28
+ export const SAMPLE_FORM_DEFAULT_VALUES: SampleFormData = {
29
+ name: "",
30
+ description: "",
31
+ category: "BASIC",
32
+ priority: 1,
33
+ active: true,
34
+ };
35
+
36
+ const CATEGORY_OPTIONS = [
37
+ { label: "기본", value: "BASIC" },
38
+ { label: "고급", value: "ADVANCED" },
39
+ ];
40
+
41
+ const PRIORITY_OPTIONS = Array.from({ length: 10 }, (_, i) => ({
42
+ label: String(i + 1),
43
+ value: String(i + 1),
44
+ }));
45
+
46
+ function SampleNameChecker({ originalName }: { originalName?: string }) {
47
+ const { watch, setError, clearErrors, formState } = useFormContext();
48
+ const name = watch("name") as string;
49
+ const isDuplicateRef = useRef(false);
50
+
51
+ useEffect(() => {
52
+ if (!name || name === originalName) {
53
+ isDuplicateRef.current = false;
54
+ clearErrors("name");
55
+ return;
56
+ }
57
+ const timer = setTimeout(async () => {
58
+ const isDuplicate = await checkSampleNameAvailable(name);
59
+ isDuplicateRef.current = isDuplicate;
60
+ if (isDuplicate) {
61
+ setError("name", { type: "manual", message: "이미 사용 중인 샘플명입니다." });
62
+ } else {
63
+ clearErrors("name");
64
+ }
65
+ }, 500);
66
+ return () => clearTimeout(timer);
67
+ }, [name, originalName, setError, clearErrors]);
68
+
69
+ useEffect(() => {
70
+ if (isDuplicateRef.current && !formState.errors.name) {
71
+ setError("name", { type: "manual", message: "이미 사용 중인 샘플명입니다." });
72
+ }
73
+ }, [formState.errors.name, setError]);
74
+
75
+ return null;
76
+ }
77
+
78
+ interface SampleFormModalProps {
79
+ mode: "create" | "edit";
80
+ isOpen: boolean;
81
+ onClose: () => void;
82
+ defaultValues?: SampleFormData;
83
+ onSubmit: (data: SampleFormData) => Promise<void>;
84
+ isPending: boolean;
85
+ formKey?: string | number;
86
+ originalName?: string;
87
+ pendingFiles: Array<File>;
88
+ onPendingFilesChange: (files: Array<File>) => void;
89
+ existingFiles?: Array<FileResponse>;
90
+ onDeleteExistingFile?: (id: number) => void;
91
+ }
92
+
93
+ export function SampleFormModal({
94
+ mode,
95
+ isOpen,
96
+ onClose,
97
+ defaultValues,
98
+ onSubmit,
99
+ isPending,
100
+ formKey,
101
+ originalName,
102
+ pendingFiles,
103
+ onPendingFilesChange,
104
+ existingFiles,
105
+ onDeleteExistingFile,
106
+ }: SampleFormModalProps) {
107
+ const formId = mode === "create" ? "sample-create-form" : "sample-edit-form";
108
+ const title = mode === "create" ? "샘플 등록" : "샘플 수정";
109
+ const submitLabel = mode === "create" ? "등록" : "수정";
110
+
111
+ return (
112
+ <Modal isOpen={isOpen} onClose={onClose} contentClassName="max-w-3xl rounded-xl bg-white">
113
+ <ModalIconHeader type={mode} title={title} onClose={onClose} />
114
+ <ModalBody className="px-6 py-5">
115
+ <div className="space-y-4">
116
+ <div className="overflow-hidden rounded-lg border border-gray-200">
117
+ <SubmitForm
118
+ key={formKey}
119
+ formId={formId}
120
+ schema={sampleFormSchema}
121
+ defaultValues={defaultValues ?? SAMPLE_FORM_DEFAULT_VALUES}
122
+ onSubmit={onSubmit}
123
+ >
124
+ <SampleNameChecker originalName={originalName} />
125
+ <SubmitForm.Row formKey="name" label="이름" required maxLength={100} />
126
+ <SubmitForm.Row
127
+ formKey="description"
128
+ formType="textarea"
129
+ label="설명"
130
+ required
131
+ maxLength={500}
132
+ />
133
+ <SubmitForm.Row
134
+ formKey="category"
135
+ formType="radio"
136
+ label="카테고리"
137
+ options={CATEGORY_OPTIONS}
138
+ />
139
+ <SubmitForm.Row
140
+ formKey="priority"
141
+ formType="select"
142
+ label="우선순위"
143
+ options={PRIORITY_OPTIONS}
144
+ />
145
+ <SubmitForm.Row formKey="active" formType="switch" label="사용 여부" />
146
+ </SubmitForm>
147
+ </div>
148
+
149
+ <div>
150
+ <p className="mb-1.5 text-sm font-medium text-gray-700">첨부파일</p>
151
+ {existingFiles && existingFiles.length > 0 && (
152
+ <ul className="mb-3 space-y-1.5">
153
+ {existingFiles.map((file) => (
154
+ <li
155
+ key={file.id}
156
+ className="flex items-center justify-between rounded-md border border-gray-200 bg-gray-50 px-3 py-2"
157
+ >
158
+ <span className="flex items-center gap-1.5 truncate text-sm text-gray-700">
159
+ <Paperclip size={13} className="shrink-0 text-gray-400" />
160
+ {file.fileName}
161
+ </span>
162
+ <button
163
+ type="button"
164
+ onClick={() => onDeleteExistingFile?.(file.id)}
165
+ className="ml-2 shrink-0 rounded p-0.5 text-gray-400 hover:bg-red-50 hover:text-red-500"
166
+ aria-label="파일 삭제"
167
+ >
168
+ <X size={14} />
169
+ </button>
170
+ </li>
171
+ ))}
172
+ </ul>
173
+ )}
174
+ <FileUploader files={pendingFiles} onChange={onPendingFilesChange} multiple />
175
+ </div>
176
+ </div>
177
+ </ModalBody>
178
+ <ModalFooter className="flex justify-end gap-2 border-t border-gray-200 bg-neutral-50 px-5 py-3">
179
+ <Button type="submit" form={formId} variant="save" disabled={isPending}>
180
+ {isPending ? `${submitLabel} 중...` : submitLabel}
181
+ </Button>
182
+ <Button variant="outline" onClick={onClose}>
183
+ 취소
184
+ </Button>
185
+ </ModalFooter>
186
+ </Modal>
187
+ );
188
+ }