@farmzone/fz-template-react 1.0.0 → 1.0.1

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 (58) hide show
  1. package/bin/create.js +10 -4
  2. package/package.json +8 -2
  3. package/template/.env.example +5 -0
  4. package/template/eslint.config.js +4 -1
  5. package/template/index.css +15 -2
  6. package/template/index.html +1 -1
  7. package/template/package.json +55 -41
  8. package/template/public/favicon.ico +0 -0
  9. package/template/public/mockServiceWorker.js +349 -0
  10. package/template/src/app/App.tsx +2 -0
  11. package/template/src/app/api/api.ts +178 -0
  12. package/template/src/app/api/queries.ts +321 -0
  13. package/template/src/app/api/queryKey.ts +7 -0
  14. package/template/src/app/api/token.ts +7 -0
  15. package/template/src/app/layout/Layout.tsx +33 -16
  16. package/template/src/app/layout/ListContents.tsx +9 -0
  17. package/template/src/app/layout/ListHeader.tsx +41 -0
  18. package/template/src/app/layout/MultiTabNav.tsx +101 -0
  19. package/template/src/app/layout/Sidebar.tsx +33 -53
  20. package/template/src/app/layout/UserInfo.tsx +94 -0
  21. package/template/src/app/layout/menu.ts +46 -21
  22. package/template/src/app/layout/tabSwitchStore.ts +11 -0
  23. package/template/src/app/router/Router.tsx +54 -28
  24. package/template/src/app/store/index.ts +26 -0
  25. package/template/src/index.tsx +21 -12
  26. package/template/src/mocks/browser.ts +17 -0
  27. package/template/src/mocks/handlers.ts +43 -0
  28. package/template/src/mocks/scenarios.ts +57 -0
  29. package/template/src/pages/dashboard/index.tsx +541 -8
  30. package/template/src/pages/error/Error.tsx +29 -17
  31. package/template/src/pages/error/NotFound.tsx +27 -17
  32. package/template/src/pages/login/index.tsx +317 -0
  33. package/template/src/pages/post/PostFormModal.tsx +128 -0
  34. package/template/src/pages/post/detail/index.tsx +548 -0
  35. package/template/src/pages/post/index.tsx +267 -0
  36. package/template/src/pages/sample/SampleFormModal.tsx +77 -0
  37. package/template/src/pages/sample/detail/index.tsx +424 -0
  38. package/template/src/pages/sample/index.tsx +269 -0
  39. package/template/src/pages/system/log/index.tsx +173 -0
  40. package/template/src/pages/user/config/columns.tsx +109 -0
  41. package/template/src/pages/user/config/schema.ts +54 -0
  42. package/template/src/pages/user/index.tsx +641 -0
  43. package/template/src/shared/components/CommentInput.tsx +243 -0
  44. package/template/src/shared/components/FilePreviewCard.tsx +70 -0
  45. package/template/src/shared/config/text.ts +27 -0
  46. package/template/src/shared/config/type.ts +40 -0
  47. package/template/src/shared/utils/format.ts +11 -0
  48. package/template/src/types/auth.ts +10 -0
  49. package/template/src/types/comment.ts +33 -0
  50. package/template/src/types/common.ts +19 -0
  51. package/template/src/types/dashboard.ts +53 -0
  52. package/template/src/types/index.ts +16 -0
  53. package/template/src/types/log.ts +21 -0
  54. package/template/src/types/post.ts +32 -0
  55. package/template/src/types/sample.ts +28 -0
  56. package/template/src/types/user.ts +51 -0
  57. package/template/src/vite-env.d.ts +10 -0
  58. package/template/gitignore +0 -32
@@ -0,0 +1,173 @@
1
+ import { useState } from "react";
2
+ import { PageFilter, Select, Table } from "@farmzone/fz-react-ui";
3
+ import type { Column } from "@farmzone/fz-react-ui";
4
+
5
+ import { useGetLogs } from "@/app/api/queries";
6
+ import type { ActionLog } from "@/types";
7
+ import ListContents from "@/app/layout/ListContents";
8
+ import ListHeader from "@/app/layout/ListHeader";
9
+ import { formatDateTime } from "@/shared/utils/format";
10
+
11
+ const PAGE_SIZE_OPTIONS = [
12
+ { label: "15", value: "15" },
13
+ { label: "30", value: "30" },
14
+ { label: "60", value: "60" },
15
+ { label: "100", value: "100" },
16
+ ];
17
+
18
+ const ROLE_OPTIONS = [
19
+ { label: "관리자", value: "ADMIN" },
20
+ { label: "사용자", value: "USER" },
21
+ ];
22
+
23
+ const KEYWORD_TYPE_OPTIONS = [
24
+ { label: "아이디", value: "userId" },
25
+ { label: "이름", value: "userName" },
26
+ ];
27
+
28
+ interface LogFilterParams {
29
+ role: string;
30
+ pageOption: string;
31
+ keywordType: string;
32
+ keyword: string;
33
+ startDate: string;
34
+ endDate: string;
35
+ }
36
+
37
+ const EMPTY_FILTER: LogFilterParams = {
38
+ role: "",
39
+ pageOption: "",
40
+ keywordType: "userId",
41
+ keyword: "",
42
+ startDate: "",
43
+ endDate: "",
44
+ };
45
+
46
+ const columns: Array<Column<ActionLog>> = [
47
+ {
48
+ key: "id",
49
+ title: "No",
50
+ width: "40px",
51
+ align: "center",
52
+ render: (_, record) => record.id,
53
+ },
54
+ { key: "userId", title: "아이디", minWidth: 80 },
55
+ { key: "name", title: "이름", minWidth: 80 },
56
+ { key: "action", title: "액션", width: "40px" },
57
+ { key: "pageOption", title: "대상", width: "40px" },
58
+ { key: "content", title: "상세 내용", minWidth: 300 },
59
+ {
60
+ key: "createdAt",
61
+ title: "발생일시",
62
+ align: "center",
63
+ width: "80px",
64
+ render: (_, record) => formatDateTime(record.createdAt),
65
+ },
66
+ ];
67
+
68
+ export default function LogPage() {
69
+ const [page, setPage] = useState(0);
70
+ const [pageSize, setPageSize] = useState(15);
71
+ const [filterParams, setFilterParams] = useState<LogFilterParams>(EMPTY_FILTER);
72
+ const [submittedFilter, setSubmittedFilter] = useState<LogFilterParams>(EMPTY_FILTER);
73
+
74
+ const { data, isLoading } = useGetLogs({
75
+ page,
76
+ size: pageSize,
77
+ userRole: submittedFilter.role || undefined,
78
+ pageOption: submittedFilter.pageOption || undefined,
79
+ keywordType: submittedFilter.keyword ? submittedFilter.keywordType : undefined,
80
+ keyword: submittedFilter.keyword || undefined,
81
+ startDate: submittedFilter.startDate || undefined,
82
+ endDate: submittedFilter.endDate || undefined,
83
+ });
84
+
85
+ const totalElements = data?.totalElements ?? 0;
86
+ const totalPages = data?.totalPages ?? 0;
87
+ const content = data?.content ?? [];
88
+
89
+ return (
90
+ <div className="p-6">
91
+ <ListHeader title="로그 관리" />
92
+ <ListContents>
93
+ <PageFilter
94
+ values={filterParams}
95
+ onChange={(updates) => setFilterParams((prev) => ({ ...prev, ...updates }))}
96
+ onSubmit={() => {
97
+ setPage(0);
98
+ setSubmittedFilter(filterParams);
99
+ }}
100
+ onReset={() => {
101
+ setFilterParams(EMPTY_FILTER);
102
+ setSubmittedFilter(EMPTY_FILTER);
103
+ setPage(0);
104
+ setPageSize(15);
105
+ }}
106
+ rows={[
107
+ {
108
+ options: [
109
+ {
110
+ type: "select",
111
+ key: "role",
112
+ label: "구분",
113
+ placeholder: "전체",
114
+ options: ROLE_OPTIONS,
115
+ },
116
+ {
117
+ type: "date-range",
118
+ key: "startDate",
119
+ endKey: "endDate",
120
+ label: "기간",
121
+ },
122
+ ],
123
+ },
124
+ {
125
+ options: [
126
+ {
127
+ type: "select",
128
+ key: "keywordType",
129
+ label: "검색",
130
+ options: KEYWORD_TYPE_OPTIONS,
131
+ },
132
+ { type: "input", key: "keyword", placeholder: "검색어 입력" },
133
+ ],
134
+ },
135
+ ]}
136
+ />
137
+ <div className="[&_.table-item]:!cursor-default [&_tr]:!cursor-default">
138
+ <Table
139
+ columns={columns}
140
+ data={content}
141
+ isLoading={isLoading}
142
+ rowKey="id"
143
+ paginationInfo={{
144
+ page,
145
+ totalPages,
146
+ onPageChange: setPage,
147
+ }}
148
+ renderFooter={{
149
+ renderLeft: (
150
+ <p className="text-sm text-gray-600">
151
+ 총 <span className="font-semibold text-gray-900">{totalElements}</span>건
152
+ </p>
153
+ ),
154
+ renderRight: (
155
+ <div className="flex items-center gap-2.5 pr-5">
156
+ <p className="text-sm font-medium text-gray-700">페이지 개수</p>
157
+ <Select
158
+ value={String(pageSize)}
159
+ options={PAGE_SIZE_OPTIONS}
160
+ onChange={(val) => {
161
+ setPageSize(Number(val));
162
+ setPage(0);
163
+ }}
164
+ />
165
+ </div>
166
+ ),
167
+ }}
168
+ />
169
+ </div>
170
+ </ListContents>
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,109 @@
1
+ import { Badge, Button, type Column } from "@farmzone/fz-react-ui";
2
+ import { Pencil, Trash2 } from "lucide-react";
3
+
4
+ import type { User } from "@/types";
5
+ import { formatDateTime } from "@/shared/utils/format";
6
+
7
+ const ROLE_LABEL: Record<string, string> = {
8
+ ADMIN: "관리자",
9
+ USER: "일반",
10
+ };
11
+
12
+ interface ActionHandlers {
13
+ onEdit: (user: User) => void;
14
+ onDelete: (user: User) => void;
15
+ totalElements: number;
16
+ page: number;
17
+ pageSize: number;
18
+ }
19
+
20
+ export const getUserColumns = ({
21
+ onEdit,
22
+ onDelete,
23
+ totalElements,
24
+ page,
25
+ pageSize,
26
+ }: ActionHandlers): Array<Column<User>> => [
27
+ {
28
+ key: "index",
29
+ title: "No",
30
+ width: "30px",
31
+ align: "center",
32
+ render: (_, __, index) => totalElements - page * pageSize - index,
33
+ },
34
+ { key: "userId", title: "아이디", minWidth: 100 },
35
+ { key: "name", title: "이름", minWidth: 80 },
36
+ {
37
+ key: "role",
38
+ title: "권한",
39
+ align: "center",
40
+ width: "40px",
41
+ render: (_, record) => ROLE_LABEL[record.role] ?? record.role,
42
+ },
43
+ {
44
+ key: "active",
45
+ title: "사용여부",
46
+ align: "center",
47
+ width: "50px",
48
+ render: (_, record) => (
49
+ <Badge
50
+ text={record.active ? "O" : "X"}
51
+ className={`scale-90 ${
52
+ record.active
53
+ ? "bg-green-100 text-green-700 border-green-100"
54
+ : "bg-red-100 text-red-500 border-red-100"
55
+ }`}
56
+ />
57
+ ),
58
+ },
59
+ {
60
+ key: "createdAt",
61
+ title: "등록일시",
62
+ align: "center",
63
+ width: "80px",
64
+ render: (_, record) => formatDateTime(record.createdAt),
65
+ },
66
+ {
67
+ key: "lastLoginAt",
68
+ title: "최종 로그인",
69
+ align: "center",
70
+ width: "80px",
71
+ render: (_, record) => formatDateTime(record.lastLoginAt),
72
+ },
73
+ // {
74
+ // key: "actions",
75
+ // title: "관리",
76
+ // align: "center",
77
+ // width: "40px",
78
+ // render: (_, record) => (
79
+ // <div className="flex items-center justify-center gap-1">
80
+ // <Button
81
+ // type="button"
82
+ // variant="ghost"
83
+ // size="icon-sm"
84
+ // className="cursor-pointer rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-blue-500"
85
+ // onClick={(e) => {
86
+ // e.stopPropagation();
87
+ // onEdit(record);
88
+ // }}
89
+ // aria-label="수정"
90
+ // >
91
+ // <Pencil size={15} />
92
+ // </Button>
93
+ // <Button
94
+ // type="button"
95
+ // variant="ghost"
96
+ // size="icon-sm"
97
+ // className="cursor-pointer rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-red-500"
98
+ // onClick={(e) => {
99
+ // e.stopPropagation();
100
+ // onDelete(record);
101
+ // }}
102
+ // aria-label="삭제"
103
+ // >
104
+ // <Trash2 size={15} />
105
+ // </Button>
106
+ // </div>
107
+ // ),
108
+ // },
109
+ ];
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+
3
+ export const userCreateSchema = z
4
+ .object({
5
+ userId: z.string().min(1, "아이디를 입력해 주세요."),
6
+ name: z.string().min(1, "성명을 입력해 주세요."),
7
+ password: z.string().min(1, "비밀번호를 입력해 주세요."),
8
+ passwordCheck: z.string().min(1, "비밀번호 확인을 입력해 주세요."),
9
+ role: z.string(),
10
+ gender: z.enum(["M", "F", ""]).default(""),
11
+ birthday: z.string().default(""),
12
+ phone: z
13
+ .string()
14
+ .regex(/^[0-9\-]{0,20}$/, "숫자와 하이픈(-)만 입력 가능합니다.")
15
+ .default(""),
16
+ })
17
+ .superRefine((data, ctx) => {
18
+ if (data.password !== data.passwordCheck) {
19
+ ctx.addIssue({
20
+ code: z.ZodIssueCode.custom,
21
+ message: "비밀번호가 일치하지 않습니다.",
22
+ path: ["passwordCheck"],
23
+ });
24
+ }
25
+ });
26
+
27
+ export type UserCreateFormData = z.infer<typeof userCreateSchema>;
28
+
29
+ export const userEditSchema = z
30
+ .object({
31
+ userId: z.string().default(""),
32
+ name: z.string().min(1, "성명을 입력해 주세요."),
33
+ password: z.string().default(""),
34
+ passwordCheck: z.string().default(""),
35
+ active: z.boolean().default(true),
36
+ role: z.string(),
37
+ gender: z.enum(["M", "F", ""]).default(""),
38
+ birthday: z.string().default(""),
39
+ phone: z
40
+ .string()
41
+ .regex(/^[0-9\-]{0,20}$/, "숫자와 하이픈(-)만 입력 가능합니다.")
42
+ .default(""),
43
+ })
44
+ .superRefine((data, ctx) => {
45
+ if (data.password && data.password !== data.passwordCheck) {
46
+ ctx.addIssue({
47
+ code: z.ZodIssueCode.custom,
48
+ message: "비밀번호가 일치하지 않습니다.",
49
+ path: ["passwordCheck"],
50
+ });
51
+ }
52
+ });
53
+
54
+ export type UserEditFormData = z.infer<typeof userEditSchema>;