@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,266 +1,266 @@
1
- import { useState } from "react";
2
- import { useNavigate } from "react-router";
3
- import { useQueryClient } from "@tanstack/react-query";
4
- import { Badge, Button, confirmModal, PageFilter, Select, Table, toast } from "@farmzone/fz-react-ui";
5
-
6
- import { useGetPosts, useDeletePosts } from "@/app/api/queries";
7
- import type { Post } from "@/types";
8
- import { useUserStore } from "@/app/store";
9
- import { apiFormDataInstance } from "@/app/api/api";
10
- import { POST_QUERY_KEY } from "@/app/api/queryKey";
11
- import ListContents from "@/app/layout/ListContents";
12
- import ListHeader from "@/app/layout/ListHeader";
13
- import { COMMON_MESSAGES } from "@/shared/config/text";
14
- import { formatDateTime } from "@/shared/utils/format";
15
- import { PostFormModal, POST_FORM_DEFAULT_VALUES } from "./PostFormModal";
16
- import type { PostFormData } from "./PostFormModal";
17
-
18
- const PAGE_SIZE_OPTIONS = [
19
- { label: "15", value: "15" },
20
- { label: "30", value: "30" },
21
- { label: "60", value: "60" },
22
- { label: "100", value: "100" },
23
- ];
24
-
25
- interface PostFilterParams {
26
- title: string;
27
- noticeCategory: string;
28
- }
29
-
30
- const EMPTY_FILTER: PostFilterParams = { title: "", noticeCategory: "" };
31
-
32
- const FILTER_CATEGORY_OPTIONS = [
33
- { label: "일반", value: "BASIC" },
34
- { label: "공지", value: "NOTICE" },
35
- ];
36
-
37
- export default function PostPage() {
38
- const navigate = useNavigate();
39
- const queryClient = useQueryClient();
40
- const currentUser = useUserStore((s) => s.user);
41
- const [page, setPage] = useState(0);
42
- const [pageSize, setPageSize] = useState(15);
43
- const [filterParams, setFilterParams] = useState<PostFilterParams>(EMPTY_FILTER);
44
- const [submittedFilter, setSubmittedFilter] = useState<PostFilterParams>(EMPTY_FILTER);
45
- const [isCreateOpen, setIsCreateOpen] = useState(false);
46
- const [pendingFiles, setPendingFiles] = useState<Array<File>>([]);
47
- const [isUploading, setIsUploading] = useState(false);
48
- const [sortOption, setSortOption] = useState<{ sortKey: string; sortOrder: "asc" | "desc" } | undefined>(
49
- undefined,
50
- );
51
-
52
- const { data, isLoading } = useGetPosts({
53
- page,
54
- size: pageSize,
55
- title: submittedFilter.title || undefined,
56
- noticeCategory: submittedFilter.noticeCategory || undefined,
57
- sortKey: sortOption?.sortKey,
58
- sortOrder: sortOption?.sortOrder,
59
- });
60
-
61
- const columns = [
62
- {
63
- key: "id",
64
- title: "No",
65
- width: "30px",
66
- align: "center" as const,
67
- render: (_: unknown, record: Post) => record.id,
68
- },
69
- {
70
- key: "title",
71
- title: "제목",
72
- minWidth: 200,
73
- sortable: true,
74
- sort: (sortOption?.sortKey === "title" ? sortOption.sortOrder : null) as "asc" | "desc" | null,
75
- },
76
- {
77
- key: "visible",
78
- title: "노출 여부",
79
- align: "center" as const,
80
- width: "40px",
81
- render: (_: unknown, record: Post) => (
82
- <Badge
83
- text={record.visible ? "O" : "X"}
84
- className={`scale-90 ${record.visible ? "bg-green-100 text-green-700 border-green-100" : "bg-red-100 text-red-500 border-red-100"}`}
85
- />
86
- ),
87
- },
88
- {
89
- key: "viewCount",
90
- title: "조회수",
91
- align: "center" as const,
92
- width: "40px",
93
- },
94
- {
95
- key: "createdAt",
96
- title: "등록일시",
97
- align: "center" as const,
98
- width: "80px",
99
- render: (_: unknown, record: Post) => formatDateTime(record.createdAt),
100
- },
101
- {
102
- key: "updatedAt",
103
- title: "수정일시",
104
- align: "center" as const,
105
- width: "80px",
106
- render: (_: unknown, record: Post) => formatDateTime(record.updatedAt),
107
- },
108
- ];
109
-
110
- const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);
111
-
112
- const { mutateAsync: deletePosts } = useDeletePosts();
113
-
114
- const totalElements = data?.totalElements ?? 0;
115
- const totalPages = data?.totalPages ?? 0;
116
- const content = data?.content ?? [];
117
-
118
- const closeCreateModal = () => {
119
- setIsCreateOpen(false);
120
- setPendingFiles([]);
121
- };
122
-
123
- const handleCreateFormSubmit = async (data: PostFormData) => {
124
- setIsUploading(true);
125
- try {
126
- const request = {
127
- title: data.title,
128
- content: data.content,
129
- noticeCategory: data.noticeCategory,
130
- visible: data.visible,
131
- writer: currentUser?.userId ?? "",
132
- };
133
- const formData = new FormData();
134
- formData.append("request", JSON.stringify(request));
135
- pendingFiles.forEach((f) => formData.append("files", f));
136
- await apiFormDataInstance.post("/posts", formData);
137
- toast.success(COMMON_MESSAGES.SAVE_SUCCESS);
138
- void queryClient.invalidateQueries({ queryKey: [POST_QUERY_KEY] });
139
- closeCreateModal();
140
- } finally {
141
- setIsUploading(false);
142
- }
143
- };
144
-
145
- return (
146
- <div className="p-6">
147
- <ListHeader
148
- title="게시글 관리"
149
- rightArea={
150
- <div className="flex items-center gap-2">
151
- <Button variant="save" onClick={() => setIsCreateOpen(true)}>
152
- 등록
153
- </Button>
154
- <Button
155
- variant="delete"
156
- disabled={selectedKeys.length === 0}
157
- onClick={() => {
158
- confirmModal({
159
- content: `선택한 ${selectedKeys.length}개 항목을 삭제하시겠습니까?`,
160
- onOk: async () => {
161
- await deletePosts(selectedKeys.map(Number));
162
- setSelectedKeys([]);
163
- },
164
- onCancel: () => {},
165
- className: "max-w-100",
166
- });
167
- }}
168
- >
169
- 삭제
170
- </Button>
171
- </div>
172
- }
173
- />
174
- <ListContents>
175
- <PageFilter
176
- values={filterParams}
177
- onChange={(updates: Partial<PostFilterParams>) =>
178
- setFilterParams((prev) => ({ ...prev, ...updates }))
179
- }
180
- onSubmit={() => {
181
- setPage(0);
182
- setSelectedKeys([]);
183
- setSubmittedFilter(filterParams);
184
- }}
185
- onReset={() => {
186
- setFilterParams(EMPTY_FILTER);
187
- setSubmittedFilter(EMPTY_FILTER);
188
- setPage(0);
189
- setPageSize(15);
190
- setSelectedKeys([]);
191
- }}
192
- rows={[
193
- {
194
- options: [
195
- {
196
- type: "select",
197
- key: "noticeCategory",
198
- label: "카테고리",
199
- placeholder: "전체",
200
- options: FILTER_CATEGORY_OPTIONS,
201
- },
202
- { type: "input", key: "title", label: "제목", placeholder: "제목 검색" },
203
- ],
204
- },
205
- ]}
206
- />
207
- <Table
208
- columns={columns}
209
- data={content}
210
- isLoading={isLoading}
211
- rowKey="id"
212
- sortOption={sortOption}
213
- onSortChange={(sortKey: string, sortOrder: "asc" | "desc") => {
214
- setSortOption({ sortKey, sortOrder });
215
- setPage(0);
216
- }}
217
- onRowClick={(post: Post) => navigate(`/post/${post.id}`)}
218
- paginationInfo={{
219
- page,
220
- totalPages,
221
- onPageChange: (newPage: number) => {
222
- setPage(newPage);
223
- setSelectedKeys([]);
224
- },
225
- }}
226
- checkboxInfo={{
227
- selectedKeys,
228
- onSelectionChange: setSelectedKeys,
229
- }}
230
- renderFooter={{
231
- renderLeft: (
232
- <p className="text-sm text-gray-600">
233
- 총 <span className="font-semibold text-gray-900">{totalElements}</span>건
234
- </p>
235
- ),
236
- renderRight: (
237
- <div className="flex items-center gap-2.5 pr-5">
238
- <p className="text-sm font-medium text-gray-700">페이지 개수</p>
239
- <Select
240
- value={String(pageSize)}
241
- options={PAGE_SIZE_OPTIONS}
242
- onChange={(val: string) => {
243
- setPageSize(Number(val));
244
- setPage(0);
245
- setSelectedKeys([]);
246
- }}
247
- />
248
- </div>
249
- ),
250
- }}
251
- />
252
- </ListContents>
253
-
254
- <PostFormModal
255
- mode="create"
256
- isOpen={isCreateOpen}
257
- onClose={closeCreateModal}
258
- defaultValues={POST_FORM_DEFAULT_VALUES}
259
- onSubmit={handleCreateFormSubmit}
260
- isPending={isUploading}
261
- pendingFiles={pendingFiles}
262
- onPendingFilesChange={setPendingFiles}
263
- />
264
- </div>
265
- );
266
- }
1
+ import { useState } from "react";
2
+ import { useNavigate } from "react-router";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { Badge, Button, confirmModal, PageFilter, Select, Table, toast } from "@farmzone/fz-react-ui";
5
+
6
+ import { useGetPosts, useDeletePosts } from "@/app/api/queries";
7
+ import type { Post } from "@/types";
8
+ import { useUserStore } from "@/app/store";
9
+ import { apiFormDataInstance } from "@/app/api/api";
10
+ import { POST_QUERY_KEY } from "@/app/api/queryKey";
11
+ import ListContents from "@/app/layout/ListContents";
12
+ import ListHeader from "@/app/layout/ListHeader";
13
+ import { COMMON_MESSAGES } from "@/shared/config/text";
14
+ import { formatDateTime } from "@/shared/utils/format";
15
+ import { PostFormModal, POST_FORM_DEFAULT_VALUES } from "./PostFormModal";
16
+ import type { PostFormData } from "./PostFormModal";
17
+
18
+ const PAGE_SIZE_OPTIONS = [
19
+ { label: "15", value: "15" },
20
+ { label: "30", value: "30" },
21
+ { label: "60", value: "60" },
22
+ { label: "100", value: "100" },
23
+ ];
24
+
25
+ interface PostFilterParams {
26
+ title: string;
27
+ noticeCategory: string;
28
+ }
29
+
30
+ const EMPTY_FILTER: PostFilterParams = { title: "", noticeCategory: "" };
31
+
32
+ const FILTER_CATEGORY_OPTIONS = [
33
+ { label: "일반", value: "BASIC" },
34
+ { label: "공지", value: "NOTICE" },
35
+ ];
36
+
37
+ export default function PostPage() {
38
+ const navigate = useNavigate();
39
+ const queryClient = useQueryClient();
40
+ const currentUser = useUserStore((s) => s.user);
41
+ const [page, setPage] = useState(0);
42
+ const [pageSize, setPageSize] = useState(15);
43
+ const [filterParams, setFilterParams] = useState<PostFilterParams>(EMPTY_FILTER);
44
+ const [submittedFilter, setSubmittedFilter] = useState<PostFilterParams>(EMPTY_FILTER);
45
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
46
+ const [pendingFiles, setPendingFiles] = useState<Array<File>>([]);
47
+ const [isUploading, setIsUploading] = useState(false);
48
+ const [sortOption, setSortOption] = useState<{ sortKey: string; sortOrder: "asc" | "desc" } | undefined>(
49
+ undefined,
50
+ );
51
+
52
+ const { data, isLoading } = useGetPosts({
53
+ page,
54
+ size: pageSize,
55
+ title: submittedFilter.title || undefined,
56
+ noticeCategory: submittedFilter.noticeCategory || undefined,
57
+ sortKey: sortOption?.sortKey,
58
+ sortOrder: sortOption?.sortOrder,
59
+ });
60
+
61
+ const columns = [
62
+ {
63
+ key: "id",
64
+ title: "No",
65
+ width: "30px",
66
+ align: "center" as const,
67
+ render: (_: unknown, record: Post) => record.id,
68
+ },
69
+ {
70
+ key: "title",
71
+ title: "제목",
72
+ minWidth: 200,
73
+ sortable: true,
74
+ sort: (sortOption?.sortKey === "title" ? sortOption.sortOrder : null) as "asc" | "desc" | null,
75
+ },
76
+ {
77
+ key: "visible",
78
+ title: "노출 여부",
79
+ align: "center" as const,
80
+ width: "40px",
81
+ render: (_: unknown, record: Post) => (
82
+ <Badge
83
+ text={record.visible ? "O" : "X"}
84
+ className={`scale-90 ${record.visible ? "bg-green-100 text-green-700 border-green-100" : "bg-red-100 text-red-500 border-red-100"}`}
85
+ />
86
+ ),
87
+ },
88
+ {
89
+ key: "viewCount",
90
+ title: "조회수",
91
+ align: "center" as const,
92
+ width: "40px",
93
+ },
94
+ {
95
+ key: "createdAt",
96
+ title: "등록일시",
97
+ align: "center" as const,
98
+ width: "80px",
99
+ render: (_: unknown, record: Post) => formatDateTime(record.createdAt),
100
+ },
101
+ {
102
+ key: "updatedAt",
103
+ title: "수정일시",
104
+ align: "center" as const,
105
+ width: "80px",
106
+ render: (_: unknown, record: Post) => formatDateTime(record.updatedAt),
107
+ },
108
+ ];
109
+
110
+ const [selectedKeys, setSelectedKeys] = useState<Array<string | number>>([]);
111
+
112
+ const { mutateAsync: deletePosts } = useDeletePosts();
113
+
114
+ const totalElements = data?.totalElements ?? 0;
115
+ const totalPages = data?.totalPages ?? 0;
116
+ const content = data?.content ?? [];
117
+
118
+ const closeCreateModal = () => {
119
+ setIsCreateOpen(false);
120
+ setPendingFiles([]);
121
+ };
122
+
123
+ const handleCreateFormSubmit = async (data: PostFormData) => {
124
+ setIsUploading(true);
125
+ try {
126
+ const request = {
127
+ title: data.title,
128
+ content: data.content,
129
+ noticeCategory: data.noticeCategory,
130
+ visible: data.visible,
131
+ writer: currentUser?.userId ?? "",
132
+ };
133
+ const formData = new FormData();
134
+ formData.append("request", JSON.stringify(request));
135
+ pendingFiles.forEach((f) => formData.append("files", f));
136
+ await apiFormDataInstance.post("/posts", formData);
137
+ toast.success(COMMON_MESSAGES.SAVE_SUCCESS);
138
+ void queryClient.invalidateQueries({ queryKey: [POST_QUERY_KEY] });
139
+ closeCreateModal();
140
+ } finally {
141
+ setIsUploading(false);
142
+ }
143
+ };
144
+
145
+ return (
146
+ <div className="p-6">
147
+ <ListHeader
148
+ title="게시글 관리"
149
+ rightArea={
150
+ <div className="flex items-center gap-2">
151
+ <Button variant="save" onClick={() => setIsCreateOpen(true)}>
152
+ 등록
153
+ </Button>
154
+ <Button
155
+ variant="delete"
156
+ disabled={selectedKeys.length === 0}
157
+ onClick={() => {
158
+ confirmModal({
159
+ content: `선택한 ${selectedKeys.length}개 항목을 삭제하시겠습니까?`,
160
+ onOk: async () => {
161
+ await deletePosts(selectedKeys.map(Number));
162
+ setSelectedKeys([]);
163
+ },
164
+ onCancel: () => {},
165
+ className: "max-w-100",
166
+ });
167
+ }}
168
+ >
169
+ 삭제
170
+ </Button>
171
+ </div>
172
+ }
173
+ />
174
+ <ListContents>
175
+ <PageFilter
176
+ values={filterParams}
177
+ onChange={(updates: Partial<PostFilterParams>) =>
178
+ setFilterParams((prev) => ({ ...prev, ...updates }))
179
+ }
180
+ onSubmit={() => {
181
+ setPage(0);
182
+ setSelectedKeys([]);
183
+ setSubmittedFilter(filterParams);
184
+ }}
185
+ onReset={() => {
186
+ setFilterParams(EMPTY_FILTER);
187
+ setSubmittedFilter(EMPTY_FILTER);
188
+ setPage(0);
189
+ setPageSize(15);
190
+ setSelectedKeys([]);
191
+ }}
192
+ rows={[
193
+ {
194
+ options: [
195
+ {
196
+ type: "select",
197
+ key: "noticeCategory",
198
+ label: "카테고리",
199
+ placeholder: "전체",
200
+ options: FILTER_CATEGORY_OPTIONS,
201
+ },
202
+ { type: "input", key: "title", label: "제목", placeholder: "제목 검색" },
203
+ ],
204
+ },
205
+ ]}
206
+ />
207
+ <Table
208
+ columns={columns}
209
+ data={content}
210
+ isLoading={isLoading}
211
+ rowKey="id"
212
+ sortOption={sortOption}
213
+ onSortChange={(sortKey: string, sortOrder: "asc" | "desc") => {
214
+ setSortOption({ sortKey, sortOrder });
215
+ setPage(0);
216
+ }}
217
+ onRowClick={(post: Post) => navigate(`/post/${post.id}`)}
218
+ paginationInfo={{
219
+ page,
220
+ totalPages,
221
+ onPageChange: (newPage: number) => {
222
+ setPage(newPage);
223
+ setSelectedKeys([]);
224
+ },
225
+ }}
226
+ checkboxInfo={{
227
+ selectedKeys,
228
+ onSelectionChange: setSelectedKeys,
229
+ }}
230
+ renderFooter={{
231
+ renderLeft: (
232
+ <p className="text-sm text-gray-600">
233
+ 총 <span className="font-semibold text-gray-900">{totalElements}</span>건
234
+ </p>
235
+ ),
236
+ renderRight: (
237
+ <div className="flex items-center gap-2.5 pr-5">
238
+ <p className="text-sm font-medium text-gray-700">페이지 개수</p>
239
+ <Select
240
+ value={String(pageSize)}
241
+ options={PAGE_SIZE_OPTIONS}
242
+ onChange={(val: string) => {
243
+ setPageSize(Number(val));
244
+ setPage(0);
245
+ setSelectedKeys([]);
246
+ }}
247
+ />
248
+ </div>
249
+ ),
250
+ }}
251
+ />
252
+ </ListContents>
253
+
254
+ <PostFormModal
255
+ mode="create"
256
+ isOpen={isCreateOpen}
257
+ onClose={closeCreateModal}
258
+ defaultValues={POST_FORM_DEFAULT_VALUES}
259
+ onSubmit={handleCreateFormSubmit}
260
+ isPending={isUploading}
261
+ pendingFiles={pendingFiles}
262
+ onPendingFilesChange={setPendingFiles}
263
+ />
264
+ </div>
265
+ );
266
+ }