@farmzone/fz-template-react 1.0.6 → 1.0.8

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,173 +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
- }
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
+ }
@@ -1,102 +1,102 @@
1
- import { Badge, type Column } from "@farmzone/fz-react-ui";
2
-
3
- import type { User } from "@/types";
4
- import { formatDateTime } from "@/shared/utils/format";
5
-
6
- const ROLE_LABEL: Record<string, string> = {
7
- ADMIN: "관리자",
8
- USER: "일반",
9
- };
10
-
11
- interface ActionHandlers {
12
- onEdit: (user: User) => void;
13
- onDelete: (user: User) => void;
14
- totalElements: number;
15
- page: number;
16
- pageSize: number;
17
- }
18
-
19
- export const getUserColumns = ({ totalElements, page, pageSize }: ActionHandlers): Array<Column<User>> => [
20
- {
21
- key: "index",
22
- title: "No",
23
- width: "30px",
24
- align: "center",
25
- render: (_, __, index) => totalElements - page * pageSize - index,
26
- },
27
- { key: "userId", title: "아이디", minWidth: 100 },
28
- { key: "name", title: "이름", minWidth: 80 },
29
- {
30
- key: "role",
31
- title: "권한",
32
- align: "center",
33
- width: "40px",
34
- render: (_, record) => ROLE_LABEL[record.role] ?? record.role,
35
- },
36
- {
37
- key: "active",
38
- title: "사용여부",
39
- align: "center",
40
- width: "50px",
41
- render: (_, record) => (
42
- <Badge
43
- text={record.active ? "O" : "X"}
44
- className={`scale-90 ${
45
- record.active
46
- ? "bg-green-100 text-green-700 border-green-100"
47
- : "bg-red-100 text-red-500 border-red-100"
48
- }`}
49
- />
50
- ),
51
- },
52
- {
53
- key: "createdAt",
54
- title: "등록일시",
55
- align: "center",
56
- width: "80px",
57
- render: (_, record) => formatDateTime(record.createdAt),
58
- },
59
- {
60
- key: "lastLoginAt",
61
- title: "최종 로그인",
62
- align: "center",
63
- width: "80px",
64
- render: (_, record) => formatDateTime(record.lastLoginAt),
65
- },
66
- // {
67
- // key: "actions",
68
- // title: "관리",
69
- // align: "center",
70
- // width: "40px",
71
- // render: (_, record) => (
72
- // <div className="flex items-center justify-center gap-1">
73
- // <Button
74
- // type="button"
75
- // variant="ghost"
76
- // size="icon-sm"
77
- // className="cursor-pointer rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-blue-500"
78
- // onClick={(e) => {
79
- // e.stopPropagation();
80
- // onEdit(record);
81
- // }}
82
- // aria-label="수정"
83
- // >
84
- // <Pencil size={15} />
85
- // </Button>
86
- // <Button
87
- // type="button"
88
- // variant="ghost"
89
- // size="icon-sm"
90
- // className="cursor-pointer rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-red-500"
91
- // onClick={(e) => {
92
- // e.stopPropagation();
93
- // onDelete(record);
94
- // }}
95
- // aria-label="삭제"
96
- // >
97
- // <Trash2 size={15} />
98
- // </Button>
99
- // </div>
100
- // ),
101
- // },
102
- ];
1
+ import { Badge, type Column } from "@farmzone/fz-react-ui";
2
+
3
+ import type { User } from "@/types";
4
+ import { formatDateTime } from "@/shared/utils/format";
5
+
6
+ const ROLE_LABEL: Record<string, string> = {
7
+ ADMIN: "관리자",
8
+ USER: "일반",
9
+ };
10
+
11
+ interface ActionHandlers {
12
+ onEdit: (user: User) => void;
13
+ onDelete: (user: User) => void;
14
+ totalElements: number;
15
+ page: number;
16
+ pageSize: number;
17
+ }
18
+
19
+ export const getUserColumns = ({ totalElements, page, pageSize }: ActionHandlers): Array<Column<User>> => [
20
+ {
21
+ key: "index",
22
+ title: "No",
23
+ width: "30px",
24
+ align: "center",
25
+ render: (_, __, index) => totalElements - page * pageSize - index,
26
+ },
27
+ { key: "userId", title: "아이디", minWidth: 100 },
28
+ { key: "name", title: "이름", minWidth: 80 },
29
+ {
30
+ key: "role",
31
+ title: "권한",
32
+ align: "center",
33
+ width: "40px",
34
+ render: (_, record) => ROLE_LABEL[record.role] ?? record.role,
35
+ },
36
+ {
37
+ key: "active",
38
+ title: "사용여부",
39
+ align: "center",
40
+ width: "50px",
41
+ render: (_, record) => (
42
+ <Badge
43
+ text={record.active ? "O" : "X"}
44
+ className={`scale-90 ${
45
+ record.active
46
+ ? "bg-green-100 text-green-700 border-green-100"
47
+ : "bg-red-100 text-red-500 border-red-100"
48
+ }`}
49
+ />
50
+ ),
51
+ },
52
+ {
53
+ key: "createdAt",
54
+ title: "등록일시",
55
+ align: "center",
56
+ width: "80px",
57
+ render: (_, record) => formatDateTime(record.createdAt),
58
+ },
59
+ {
60
+ key: "lastLoginAt",
61
+ title: "최종 로그인",
62
+ align: "center",
63
+ width: "80px",
64
+ render: (_, record) => formatDateTime(record.lastLoginAt),
65
+ },
66
+ // {
67
+ // key: "actions",
68
+ // title: "관리",
69
+ // align: "center",
70
+ // width: "40px",
71
+ // render: (_, record) => (
72
+ // <div className="flex items-center justify-center gap-1">
73
+ // <Button
74
+ // type="button"
75
+ // variant="ghost"
76
+ // size="icon-sm"
77
+ // className="cursor-pointer rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-blue-500"
78
+ // onClick={(e) => {
79
+ // e.stopPropagation();
80
+ // onEdit(record);
81
+ // }}
82
+ // aria-label="수정"
83
+ // >
84
+ // <Pencil size={15} />
85
+ // </Button>
86
+ // <Button
87
+ // type="button"
88
+ // variant="ghost"
89
+ // size="icon-sm"
90
+ // className="cursor-pointer rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-red-500"
91
+ // onClick={(e) => {
92
+ // e.stopPropagation();
93
+ // onDelete(record);
94
+ // }}
95
+ // aria-label="삭제"
96
+ // >
97
+ // <Trash2 size={15} />
98
+ // </Button>
99
+ // </div>
100
+ // ),
101
+ // },
102
+ ];
@@ -1,54 +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>;
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>;