@farmzone/fz-template-react 1.0.3 → 1.0.5

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 (51) hide show
  1. package/README.md +46 -50
  2. package/package.json +1 -1
  3. package/template/.env.example +5 -5
  4. package/template/package.json +55 -55
  5. package/template/pnpm-lock.yaml +4214 -4214
  6. package/template/public/mockServiceWorker.js +349 -349
  7. package/template/src/app/api/api.ts +178 -178
  8. package/template/src/app/api/queries.ts +326 -321
  9. package/template/src/app/api/queryKey.ts +7 -7
  10. package/template/src/app/api/token.ts +7 -7
  11. package/template/src/app/layout/Layout.tsx +33 -33
  12. package/template/src/app/layout/ListContents.tsx +9 -9
  13. package/template/src/app/layout/ListHeader.tsx +41 -41
  14. package/template/src/app/layout/MultiTabNav.tsx +101 -101
  15. package/template/src/app/layout/Sidebar.tsx +33 -33
  16. package/template/src/app/layout/UserInfo.tsx +94 -94
  17. package/template/src/app/layout/tabSwitchStore.ts +11 -11
  18. package/template/src/app/router/Router.tsx +56 -56
  19. package/template/src/app/store/index.ts +26 -26
  20. package/template/src/index.tsx +21 -21
  21. package/template/src/mocks/browser.ts +17 -17
  22. package/template/src/mocks/handlers.ts +43 -43
  23. package/template/src/mocks/scenarios.ts +57 -57
  24. package/template/src/pages/dashboard/index.tsx +541 -541
  25. package/template/src/pages/error/Error.tsx +29 -29
  26. package/template/src/pages/error/NotFound.tsx +27 -27
  27. package/template/src/pages/login/index.tsx +317 -317
  28. package/template/src/pages/post/PostFormModal.tsx +128 -128
  29. package/template/src/pages/post/detail/index.tsx +548 -548
  30. package/template/src/pages/post/index.tsx +266 -267
  31. package/template/src/pages/sample/SampleFormModal.tsx +115 -77
  32. package/template/src/pages/sample/detail/index.tsx +400 -424
  33. package/template/src/pages/sample/index.tsx +278 -269
  34. package/template/src/pages/sample/modal/index.tsx +300 -253
  35. package/template/src/pages/system/log/index.tsx +173 -173
  36. package/template/src/pages/user/config/columns.tsx +102 -109
  37. package/template/src/pages/user/config/schema.ts +54 -54
  38. package/template/src/pages/user/index.tsx +641 -641
  39. package/template/src/shared/components/CommentInput.tsx +243 -243
  40. package/template/src/shared/config/text.ts +27 -27
  41. package/template/src/shared/utils/format.ts +11 -11
  42. package/template/src/types/auth.ts +10 -10
  43. package/template/src/types/comment.ts +33 -33
  44. package/template/src/types/common.ts +19 -19
  45. package/template/src/types/dashboard.ts +53 -53
  46. package/template/src/types/index.ts +16 -16
  47. package/template/src/types/log.ts +21 -21
  48. package/template/src/types/post.ts +32 -32
  49. package/template/src/types/sample.ts +29 -28
  50. package/template/src/types/user.ts +51 -51
  51. package/template/src/vite-env.d.ts +10 -10
@@ -1,269 +1,278 @@
1
- import { useState } from "react";
2
- import { useNavigate } from "react-router";
3
- import { Badge, Button, confirmModal, PageFilter, Select, Table } from "@farmzone/fz-react-ui";
4
-
5
- import { useDeleteSamples, useGetSamples, usePostSample, usePutSample } from "@/app/api/queries";
6
- import type { Sample } from "@/types";
7
- import ListContents from "@/app/layout/ListContents";
8
- import ListHeader from "@/app/layout/ListHeader";
9
- import { formatDateTime } from "@/shared/utils/format";
10
- import { SampleFormModal, SAMPLE_FORM_DEFAULT_VALUES } from "./SampleFormModal";
11
- import type { SampleFormData } from "./SampleFormModal";
12
-
13
- const PAGE_SIZE_OPTIONS = [
14
- { label: "15", value: "15" },
15
- { label: "30", value: "30" },
16
- { label: "60", value: "60" },
17
- { label: "100", value: "100" },
18
- ];
19
-
20
- interface SampleFilterParams {
21
- name: string;
22
- category: string;
23
- }
24
-
25
- const FILTER_RESET: SampleFilterParams = { name: "", category: "" };
26
-
27
- const CATEGORY_FILTER_OPTIONS = [
28
- { label: "기본", value: "BASIC" },
29
- { label: "고급", value: "PREMIUM" },
30
- ];
31
-
32
- export default function SamplePage() {
33
- const navigate = useNavigate();
34
- const [page, setPage] = useState(0);
35
- const [pageSize, setPageSize] = useState(15);
36
- const [filterParams, setFilterParams] = useState<SampleFilterParams>(FILTER_RESET);
37
- const [submittedFilter, setSubmittedFilter] = useState<SampleFilterParams>(FILTER_RESET);
38
-
39
- const [modalMode, setModalMode] = useState<"create" | "edit" | null>(null);
40
- const [selectedSample, setSelectedSample] = useState<Sample | null>(null);
41
- const [editDefaultValues, setEditDefaultValues] = useState<SampleFormData | undefined>(undefined);
42
- const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);
43
- const [sortOption, setSortOption] = useState<{ sortKey: string; sortOrder: "asc" | "desc" } | undefined>(
44
- undefined,
45
- );
46
-
47
- const { data, isLoading } = useGetSamples({
48
- page,
49
- size: pageSize,
50
- name: submittedFilter.name || undefined,
51
- category: submittedFilter.category || undefined,
52
- sortKey: sortOption?.sortKey,
53
- sortOrder: sortOption?.sortOrder,
54
- });
55
-
56
- const { mutateAsync: postSample, isPending: isCreating } = usePostSample();
57
- const { mutateAsync: putSample, isPending: isUpdating } = usePutSample();
58
- const { mutateAsync: deleteSamples } = useDeleteSamples();
59
-
60
- const totalElements = data?.totalElements ?? 0;
61
- const totalPages = data?.totalPages ?? 0;
62
- const content = data?.content ?? [];
63
-
64
- const openCreate = () => {
65
- setEditDefaultValues(undefined);
66
- setSelectedSample(null);
67
- setModalMode("create");
68
- };
69
-
70
- const closeModal = () => {
71
- setModalMode(null);
72
- setSelectedSample(null);
73
- setEditDefaultValues(undefined);
74
- };
75
-
76
- const handleSubmit = async (data: SampleFormData) => {
77
- if (modalMode === "create") {
78
- await postSample({
79
- name: data.name,
80
- description: data.description,
81
- category: data.category as "BASIC" | "ADVANCED",
82
- active: data.active,
83
- });
84
- } else if (modalMode === "edit" && selectedSample) {
85
- await putSample({
86
- id: selectedSample.id,
87
- data: {
88
- name: data.name,
89
- description: data.description,
90
- category: data.category as "BASIC" | "ADVANCED",
91
- active: data.active,
92
- },
93
- });
94
- }
95
- closeModal();
96
- };
97
-
98
- const columns = [
99
- {
100
- key: "index",
101
- title: "No",
102
- width: "30px",
103
- align: "center" as const,
104
- render: (_: unknown, __: unknown, index: number) => totalElements - page * pageSize - index,
105
- },
106
- {
107
- key: "name",
108
- title: "이름",
109
- sortable: true,
110
- sort: sortOption?.sortKey === "name" ? sortOption.sortOrder : null,
111
- },
112
- { key: "description", title: "설명", minWidth: 200 },
113
- {
114
- key: "category",
115
- title: "카테고리",
116
- align: "center" as const,
117
- width: "40px",
118
- render: (_: unknown, record: Sample) => (
119
- <Badge
120
- text={record.category === "BASIC" ? "기본" : "고급"}
121
- className={`scale-90 ${record.category === "BASIC" ? "bg-blue-100 text-blue-700 border-blue-100" : "bg-purple-100 text-purple-700 border-purple-100"}`}
122
- />
123
- ),
124
- },
125
- {
126
- key: "priority",
127
- title: "우선순위",
128
- align: "center" as const,
129
- width: "30px",
130
- render: (_: unknown, record: Sample) => (
131
- <span className="text-sm font-medium text-gray-700">{record.priority}</span>
132
- ),
133
- },
134
- {
135
- key: "active",
136
- title: "사용여부",
137
- align: "center" as const,
138
- width: "30px",
139
- render: (_: unknown, record: Sample) => (
140
- <Badge
141
- text={record.active ? "O" : "X"}
142
- className={`scale-90 ${record.active ? "bg-green-100 text-green-700 border-green-100" : "bg-red-100 text-red-500 border-red-100"}`}
143
- />
144
- ),
145
- },
146
- {
147
- key: "createdAt",
148
- title: "등록일시",
149
- align: "center" as const,
150
- width: "80px",
151
- render: (_: unknown, record: Sample) => formatDateTime(record.createdAt),
152
- },
153
- ];
154
-
155
- return (
156
- <div className="p-6">
157
- <ListHeader
158
- title="샘플 관리"
159
- rightArea={
160
- <div className="flex items-center gap-2">
161
- <Button variant="save" onClick={openCreate}>
162
- 등록
163
- </Button>
164
- <Button
165
- variant="delete"
166
- disabled={selectedKeys.length === 0}
167
- onClick={() => {
168
- confirmModal({
169
- content: `선택한 ${selectedKeys.length}개 항목을 삭제하시겠습니까?`,
170
- onOk: async () => {
171
- await deleteSamples(selectedKeys.map(Number));
172
- setSelectedKeys([]);
173
- },
174
- onCancel: () => {},
175
- className: "max-w-100",
176
- });
177
- }}
178
- >
179
- 삭제
180
- </Button>
181
- </div>
182
- }
183
- />
184
- <ListContents>
185
- <PageFilter
186
- values={filterParams}
187
- onChange={(updates: Partial<SampleFilterParams>) =>
188
- setFilterParams((prev) => ({ ...prev, ...updates }))
189
- }
190
- onSubmit={() => {
191
- setPage(0);
192
- setSelectedKeys([]);
193
- setSubmittedFilter(filterParams);
194
- }}
195
- onReset={() => {
196
- setFilterParams(FILTER_RESET);
197
- setSubmittedFilter(FILTER_RESET);
198
- setPage(0);
199
- setPageSize(15);
200
- setSelectedKeys([]);
201
- }}
202
- rows={[
203
- {
204
- options: [
205
- { type: "select", key: "category", label: "카테고리", options: CATEGORY_FILTER_OPTIONS },
206
- { type: "input", key: "name", label: "이름", placeholder: "이름 검색" },
207
- ],
208
- },
209
- ]}
210
- />
211
- <Table
212
- rowKey="id"
213
- columns={columns}
214
- data={content}
215
- isLoading={isLoading}
216
- sortOption={sortOption}
217
- onSortChange={(sortKey: string, sortOrder: "asc" | "desc") => {
218
- setSortOption({ sortKey, sortOrder });
219
- setPage(0);
220
- }}
221
- onRowClick={(record: Sample) => navigate(`/sample/${record.id}`)}
222
- paginationInfo={{
223
- page,
224
- totalPages,
225
- onPageChange: (newPage: number) => {
226
- setPage(newPage);
227
- setSelectedKeys([]);
228
- },
229
- }}
230
- checkboxInfo={{
231
- selectedKeys,
232
- onSelectionChange: setSelectedKeys,
233
- }}
234
- renderFooter={{
235
- renderLeft: (
236
- <p className="text-sm text-gray-600">
237
- 총 <span className="font-semibold text-gray-900">{totalElements}</span>건
238
- </p>
239
- ),
240
- renderRight: (
241
- <div className="flex items-center gap-2.5 pr-5">
242
- <p className="text-sm font-medium text-gray-700">페이지 개수</p>
243
- <Select
244
- value={String(pageSize)}
245
- options={PAGE_SIZE_OPTIONS}
246
- onChange={(val: string) => {
247
- setPageSize(Number(val));
248
- setPage(0);
249
- setSelectedKeys([]);
250
- }}
251
- />
252
- </div>
253
- ),
254
- }}
255
- />
256
- </ListContents>
257
-
258
- <SampleFormModal
259
- mode={modalMode ?? "create"}
260
- isOpen={modalMode !== null}
261
- onClose={closeModal}
262
- defaultValues={editDefaultValues ?? SAMPLE_FORM_DEFAULT_VALUES}
263
- onSubmit={handleSubmit}
264
- isPending={isCreating || isUpdating}
265
- formKey={selectedSample?.id}
266
- />
267
- </div>
268
- );
269
- }
1
+ import { useState } from "react";
2
+ import { useNavigate } from "react-router";
3
+ import { Badge, Button, confirmModal, PageFilter, Select, Table } from "@farmzone/fz-react-ui";
4
+
5
+ import { checkSampleNameAvailable, useDeleteSamples, useGetSamples, usePostSample, usePutSample } from "@/app/api/queries";
6
+ import type { Sample } from "@/types";
7
+ import ListContents from "@/app/layout/ListContents";
8
+ import ListHeader from "@/app/layout/ListHeader";
9
+ import { formatDateTime } from "@/shared/utils/format";
10
+ import { SampleFormModal, SAMPLE_FORM_DEFAULT_VALUES } from "./SampleFormModal";
11
+ import type { SampleFormData } from "./SampleFormModal";
12
+
13
+ const PAGE_SIZE_OPTIONS = [
14
+ { label: "15", value: "15" },
15
+ { label: "30", value: "30" },
16
+ { label: "60", value: "60" },
17
+ { label: "100", value: "100" },
18
+ ];
19
+
20
+ interface SampleFilterParams {
21
+ name: string;
22
+ category: string;
23
+ }
24
+
25
+ const FILTER_RESET: SampleFilterParams = { name: "", category: "" };
26
+
27
+ const CATEGORY_FILTER_OPTIONS = [
28
+ { label: "기본", value: "BASIC" },
29
+ { label: "고급", value: "PREMIUM" },
30
+ ];
31
+
32
+ export default function SamplePage() {
33
+ const navigate = useNavigate();
34
+ const [page, setPage] = useState(0);
35
+ const [pageSize, setPageSize] = useState(15);
36
+ const [filterParams, setFilterParams] = useState<SampleFilterParams>(FILTER_RESET);
37
+ const [submittedFilter, setSubmittedFilter] = useState<SampleFilterParams>(FILTER_RESET);
38
+
39
+ const [modalMode, setModalMode] = useState<"create" | "edit" | null>(null);
40
+ const [selectedSample, setSelectedSample] = useState<Sample | null>(null);
41
+ const [editDefaultValues, setEditDefaultValues] = useState<SampleFormData | undefined>(undefined);
42
+ const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);
43
+ const [sortOption, setSortOption] = useState<{ sortKey: string; sortOrder: "asc" | "desc" } | undefined>(
44
+ undefined,
45
+ );
46
+
47
+ const { data, isLoading } = useGetSamples({
48
+ page,
49
+ size: pageSize,
50
+ name: submittedFilter.name || undefined,
51
+ category: submittedFilter.category || undefined,
52
+ sortKey: sortOption?.sortKey,
53
+ sortOrder: sortOption?.sortOrder,
54
+ });
55
+
56
+ const { mutateAsync: postSample, isPending: isCreating } = usePostSample();
57
+ const { mutateAsync: putSample, isPending: isUpdating } = usePutSample();
58
+ const { mutateAsync: deleteSamples } = useDeleteSamples();
59
+
60
+ const totalElements = data?.totalElements ?? 0;
61
+ const totalPages = data?.totalPages ?? 0;
62
+ const content = data?.content ?? [];
63
+
64
+ const openCreate = () => {
65
+ setEditDefaultValues(undefined);
66
+ setSelectedSample(null);
67
+ setModalMode("create");
68
+ };
69
+
70
+ const closeModal = () => {
71
+ setModalMode(null);
72
+ setSelectedSample(null);
73
+ setEditDefaultValues(undefined);
74
+ };
75
+
76
+ const handleSubmit = async (data: SampleFormData) => {
77
+ if (modalMode === "create") {
78
+ const available = await checkSampleNameAvailable(data.name);
79
+ if (!available) return;
80
+ await postSample({
81
+ name: data.name,
82
+ description: data.description,
83
+ category: data.category as "BASIC" | "ADVANCED",
84
+ priority: data.priority,
85
+ active: data.active,
86
+ });
87
+ } else if (modalMode === "edit" && selectedSample) {
88
+ if (data.name !== selectedSample.name) {
89
+ const available = await checkSampleNameAvailable(data.name);
90
+ if (!available) return;
91
+ }
92
+ await putSample({
93
+ id: selectedSample.id,
94
+ data: {
95
+ name: data.name,
96
+ description: data.description,
97
+ category: data.category as "BASIC" | "ADVANCED",
98
+ priority: data.priority,
99
+ active: data.active,
100
+ },
101
+ });
102
+ }
103
+ closeModal();
104
+ };
105
+
106
+ const columns = [
107
+ {
108
+ key: "index",
109
+ title: "No",
110
+ width: "30px",
111
+ align: "center" as const,
112
+ render: (_: unknown, __: unknown, index: number) => totalElements - page * pageSize - index,
113
+ },
114
+ {
115
+ key: "name",
116
+ title: "이름",
117
+ sortable: true,
118
+ sort: sortOption?.sortKey === "name" ? sortOption.sortOrder : null,
119
+ },
120
+ { key: "description", title: "설명", minWidth: 200 },
121
+ {
122
+ key: "category",
123
+ title: "카테고리",
124
+ align: "center" as const,
125
+ width: "40px",
126
+ render: (_: unknown, record: Sample) => (
127
+ <Badge
128
+ text={record.category === "BASIC" ? "기본" : "고급"}
129
+ className={`scale-90 ${record.category === "BASIC" ? "bg-blue-100 text-blue-700 border-blue-100" : "bg-purple-100 text-purple-700 border-purple-100"}`}
130
+ />
131
+ ),
132
+ },
133
+ {
134
+ key: "priority",
135
+ title: "우선순위",
136
+ align: "center" as const,
137
+ width: "30px",
138
+ render: (_: unknown, record: Sample) => (
139
+ <span className="text-sm font-medium text-gray-700">{record.priority}</span>
140
+ ),
141
+ },
142
+ {
143
+ key: "active",
144
+ title: "사용여부",
145
+ align: "center" as const,
146
+ width: "30px",
147
+ render: (_: unknown, record: Sample) => (
148
+ <Badge
149
+ text={record.active ? "O" : "X"}
150
+ className={`scale-90 ${record.active ? "bg-green-100 text-green-700 border-green-100" : "bg-red-100 text-red-500 border-red-100"}`}
151
+ />
152
+ ),
153
+ },
154
+ {
155
+ key: "createdAt",
156
+ title: "등록일시",
157
+ align: "center" as const,
158
+ width: "80px",
159
+ render: (_: unknown, record: Sample) => formatDateTime(record.createdAt),
160
+ },
161
+ ];
162
+
163
+ return (
164
+ <div className="p-6">
165
+ <ListHeader
166
+ title="샘플 관리"
167
+ rightArea={
168
+ <div className="flex items-center gap-2">
169
+ <Button variant="save" onClick={openCreate}>
170
+ 등록
171
+ </Button>
172
+ <Button
173
+ variant="delete"
174
+ disabled={selectedKeys.length === 0}
175
+ onClick={() => {
176
+ confirmModal({
177
+ content: `선택한 ${selectedKeys.length}개 항목을 삭제하시겠습니까?`,
178
+ onOk: async () => {
179
+ await deleteSamples(selectedKeys.map(Number));
180
+ setSelectedKeys([]);
181
+ },
182
+ onCancel: () => {},
183
+ className: "max-w-100",
184
+ });
185
+ }}
186
+ >
187
+ 삭제
188
+ </Button>
189
+ </div>
190
+ }
191
+ />
192
+ <ListContents>
193
+ <PageFilter
194
+ values={filterParams}
195
+ onChange={(updates: Partial<SampleFilterParams>) =>
196
+ setFilterParams((prev) => ({ ...prev, ...updates }))
197
+ }
198
+ onSubmit={() => {
199
+ setPage(0);
200
+ setSelectedKeys([]);
201
+ setSubmittedFilter(filterParams);
202
+ }}
203
+ onReset={() => {
204
+ setFilterParams(FILTER_RESET);
205
+ setSubmittedFilter(FILTER_RESET);
206
+ setPage(0);
207
+ setPageSize(15);
208
+ setSelectedKeys([]);
209
+ }}
210
+ rows={[
211
+ {
212
+ options: [
213
+ { type: "select", key: "category", label: "카테고리", options: CATEGORY_FILTER_OPTIONS },
214
+ { type: "input", key: "name", label: "이름", placeholder: "이름 검색" },
215
+ ],
216
+ },
217
+ ]}
218
+ />
219
+ <Table
220
+ rowKey="id"
221
+ columns={columns}
222
+ data={content}
223
+ isLoading={isLoading}
224
+ sortOption={sortOption}
225
+ onSortChange={(sortKey: string, sortOrder: "asc" | "desc") => {
226
+ setSortOption({ sortKey, sortOrder });
227
+ setPage(0);
228
+ }}
229
+ onRowClick={(record: Sample) => navigate(`/sample/${record.id}`)}
230
+ paginationInfo={{
231
+ page,
232
+ totalPages,
233
+ onPageChange: (newPage: number) => {
234
+ setPage(newPage);
235
+ setSelectedKeys([]);
236
+ },
237
+ }}
238
+ checkboxInfo={{
239
+ selectedKeys,
240
+ onSelectionChange: setSelectedKeys,
241
+ }}
242
+ renderFooter={{
243
+ renderLeft: (
244
+ <p className="text-sm text-gray-600">
245
+ 총 <span className="font-semibold text-gray-900">{totalElements}</span>건
246
+ </p>
247
+ ),
248
+ renderRight: (
249
+ <div className="flex items-center gap-2.5 pr-5">
250
+ <p className="text-sm font-medium text-gray-700">페이지 개수</p>
251
+ <Select
252
+ value={String(pageSize)}
253
+ options={PAGE_SIZE_OPTIONS}
254
+ onChange={(val: string) => {
255
+ setPageSize(Number(val));
256
+ setPage(0);
257
+ setSelectedKeys([]);
258
+ }}
259
+ />
260
+ </div>
261
+ ),
262
+ }}
263
+ />
264
+ </ListContents>
265
+
266
+ <SampleFormModal
267
+ mode={modalMode ?? "create"}
268
+ isOpen={modalMode !== null}
269
+ onClose={closeModal}
270
+ defaultValues={editDefaultValues ?? SAMPLE_FORM_DEFAULT_VALUES}
271
+ onSubmit={handleSubmit}
272
+ isPending={isCreating || isUpdating}
273
+ formKey={selectedSample?.id}
274
+ originalName={selectedSample?.name}
275
+ />
276
+ </div>
277
+ );
278
+ }