@simplysm/sd-cli 13.0.71 → 13.0.72

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 (98) hide show
  1. package/dist/commands/init.d.ts +4 -5
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +26 -8
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/sd-cli-entry.d.ts.map +1 -1
  6. package/dist/sd-cli-entry.js +0 -20
  7. package/dist/sd-cli-entry.js.map +1 -1
  8. package/package.json +4 -4
  9. package/src/commands/init.ts +40 -21
  10. package/src/sd-cli-entry.ts +0 -24
  11. package/templates/init/{.prettierrc.yaml.hbs → .prettierrc.yaml} +1 -1
  12. package/templates/init/eslint.config.ts +15 -0
  13. package/templates/init/mise.toml +3 -0
  14. package/templates/init/package.json.hbs +8 -7
  15. package/templates/init/packages/client-admin/index.html.hbs +144 -0
  16. package/templates/init/packages/client-admin/package.json.hbs +26 -0
  17. package/templates/init/packages/client-admin/public/assets/logo-landscape.png +0 -0
  18. package/templates/init/packages/client-admin/public/assets/logo.png +0 -0
  19. package/templates/init/packages/client-admin/src/App.tsx +42 -0
  20. package/templates/init/packages/client-admin/src/dev/DevDialog.tsx +34 -0
  21. package/templates/{add-client/__CLIENT__/src/main.css.hbs → init/packages/client-admin/src/main.css} +1 -1
  22. package/templates/init/packages/client-admin/src/main.tsx.hbs +146 -0
  23. package/templates/init/packages/client-admin/src/providers/AppServiceProvider.tsx.hbs +103 -0
  24. package/templates/init/packages/client-admin/src/providers/AppStructureProvider.tsx +84 -0
  25. package/templates/init/packages/client-admin/src/providers/AuthProvider.tsx.hbs +71 -0
  26. package/templates/init/packages/client-admin/src/providers/configureSharedData.ts.hbs +67 -0
  27. package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +132 -0
  28. package/templates/init/packages/client-admin/src/views/home/HomeView.tsx +108 -0
  29. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeDetail.tsx.hbs +262 -0
  30. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +271 -0
  31. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleDetail.tsx.hbs +154 -0
  32. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionDetail.tsx.hbs +123 -0
  33. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionView.tsx +52 -0
  34. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +125 -0
  35. package/templates/init/packages/client-admin/src/views/home/main/MainView.tsx.hbs +13 -0
  36. package/templates/init/packages/client-admin/src/views/home/my-info/MyInfoDetail.tsx.hbs +248 -0
  37. package/templates/init/packages/client-admin/src/views/home/system/system-log/SystemLogSheet.tsx.hbs +169 -0
  38. package/templates/init/packages/client-admin/src/views/not-found/NotFoundView.tsx +15 -0
  39. package/templates/init/packages/client-admin/tailwind.config.ts +10 -0
  40. package/templates/init/packages/db-main/package.json.hbs +13 -0
  41. package/templates/init/packages/db-main/src/MainDbContext.ts +20 -0
  42. package/templates/init/packages/db-main/src/dataLogExt.ts +127 -0
  43. package/templates/init/packages/db-main/src/index.ts +10 -0
  44. package/templates/init/packages/db-main/src/tables/Employee.ts +24 -0
  45. package/templates/init/packages/db-main/src/tables/EmployeeConfig.ts +13 -0
  46. package/templates/init/packages/db-main/src/tables/Role.ts +9 -0
  47. package/templates/init/packages/db-main/src/tables/RolePermission.ts +13 -0
  48. package/templates/init/packages/db-main/src/tables/_DataLog.ts +19 -0
  49. package/templates/init/packages/db-main/src/tables/_Log.ts +16 -0
  50. package/templates/init/packages/server/package.json.hbs +20 -0
  51. package/templates/init/packages/server/public-dev/dev//354/264/210/352/270/260/355/231/224.xlsx +0 -0
  52. package/templates/init/packages/server/src/index.ts +4 -0
  53. package/templates/init/packages/server/src/main.ts.hbs +34 -0
  54. package/templates/init/packages/server/src/services/AuthService.ts.hbs +171 -0
  55. package/templates/init/packages/server/src/services/DevService.ts.hbs +94 -0
  56. package/templates/init/packages/server/src/services/EmployeeService.ts.hbs +122 -0
  57. package/templates/init/packages/server/src/services/RoleService.ts.hbs +59 -0
  58. package/templates/init/{pnpm-workspace.yaml.hbs → pnpm-workspace.yaml} +3 -1
  59. package/templates/init/sd.config.ts.hbs +30 -1
  60. package/templates/init/tests/e2e/package.json.hbs +16 -0
  61. package/templates/init/tests/e2e/src/e2e.spec.ts +36 -0
  62. package/templates/init/tests/e2e/src/employee-crud.ts +204 -0
  63. package/templates/init/tests/e2e/src/login.ts +61 -0
  64. package/templates/init/tests/e2e/vitest.setup.ts.hbs +220 -0
  65. package/templates/init/tsconfig.json.hbs +0 -11
  66. package/templates/init/{vitest.config.ts.hbs → vitest.config.ts} +16 -12
  67. package/dist/commands/add-client.d.ts +0 -18
  68. package/dist/commands/add-client.d.ts.map +0 -1
  69. package/dist/commands/add-client.js +0 -79
  70. package/dist/commands/add-client.js.map +0 -6
  71. package/dist/commands/add-server.d.ts +0 -18
  72. package/dist/commands/add-server.d.ts.map +0 -1
  73. package/dist/commands/add-server.js +0 -83
  74. package/dist/commands/add-server.js.map +0 -6
  75. package/dist/utils/config-editor.d.ts +0 -17
  76. package/dist/utils/config-editor.d.ts.map +0 -1
  77. package/dist/utils/config-editor.js +0 -79
  78. package/dist/utils/config-editor.js.map +0 -6
  79. package/src/commands/add-client.ts +0 -126
  80. package/src/commands/add-server.ts +0 -138
  81. package/src/utils/config-editor.ts +0 -141
  82. package/templates/add-client/__CLIENT__/index.html.hbs +0 -13
  83. package/templates/add-client/__CLIENT__/package.json.hbs +0 -16
  84. package/templates/add-client/__CLIENT__/src/App.tsx.hbs +0 -65
  85. package/templates/add-client/__CLIENT__/src/appStructure.ts.hbs +0 -20
  86. package/templates/add-client/__CLIENT__/src/main.tsx.hbs +0 -24
  87. package/templates/add-client/__CLIENT__/src/pages/HomePage.tsx.hbs +0 -9
  88. package/templates/add-client/__CLIENT__/tailwind.config.ts.hbs +0 -15
  89. package/templates/add-server/__SERVER__/package.json.hbs +0 -10
  90. package/templates/add-server/__SERVER__/src/main.ts.hbs +0 -14
  91. package/templates/init/.gitignore.hbs +0 -26
  92. package/templates/init/.npmrc.hbs +0 -1
  93. package/templates/init/eslint.config.ts.hbs +0 -5
  94. package/templates/init/mise.toml.hbs +0 -3
  95. package/tests/config-editor.spec.ts +0 -160
  96. /package/templates/init/{.prettierignore.hbs → .prettierignore} +0 -0
  97. /package/templates/{add-client/__CLIENT__ → init/packages/client-admin}/public/favicon.ico +0 -0
  98. /package/templates/init/{stylelint.config.ts.hbs → stylelint.config.ts} +0 -0
@@ -0,0 +1,108 @@
1
+ import { createSignal, onMount, Show, Suspense } from "solid-js";
2
+ import { type RouteSectionProps, useLocation, useNavigate } from "@solidjs/router";
3
+ import {
4
+ BusyContainer,
5
+ NotificationBell,
6
+ Sidebar,
7
+ ThemeToggle,
8
+ Topbar,
9
+ useNotification,
10
+ } from "@simplysm/solid";
11
+ import { env } from "@simplysm/core-common";
12
+ import { useAuth } from "../../providers/AuthProvider";
13
+ import { useAppService } from "../../providers/AppServiceProvider";
14
+ import { useAppStructure } from "../../providers/AppStructureProvider";
15
+
16
+ function HomeContent(props: RouteSectionProps) {
17
+ const auth = useAuth();
18
+ const appService = useAppService();
19
+ const navigate = useNavigate();
20
+ const location = useLocation();
21
+ const noti = useNotification();
22
+ const appStructure = useAppStructure();
23
+
24
+ const [ready, setReady] = createSignal(false);
25
+
26
+ const titleChain = () => {
27
+ const chain = appStructure.getTitleChainByHref(location.pathname);
28
+ // 최상위 그룹(홈) 제외
29
+ return chain.length > 1 ? chain.slice(1) : chain;
30
+ };
31
+
32
+ const employeeMenus = [
33
+ {
34
+ title: "설정",
35
+ onClick: () => navigate("/home/my-info"),
36
+ },
37
+ {
38
+ title: "로그아웃",
39
+ onClick: () => {
40
+ auth.logout();
41
+ navigate("/login", { replace: true });
42
+ },
43
+ },
44
+ ];
45
+
46
+ onMount(async () => {
47
+ try {
48
+ // 1. Check auth - reload token only if not already authenticated
49
+ if (!auth.authInfo()) {
50
+ const authOk = await auth.tryReloadAuth();
51
+ if (!authOk) {
52
+ navigate("/login", { replace: true });
53
+ return;
54
+ }
55
+ }
56
+
57
+ await appService.orm.connectWithoutTransaction(async (db) => {
58
+ await db.initialize();
59
+ });
60
+
61
+ setReady(true);
62
+ } catch (err) {
63
+ noti.error(err, "초기화 실패");
64
+ }
65
+ });
66
+
67
+ return (
68
+ <BusyContainer ready={ready()}>
69
+ <Sidebar.Container>
70
+ <Sidebar>
71
+ <div class="p-2 px-4">
72
+ <img src="assets/logo-landscape.png" alt="SIMPLYSM" class="h-9 w-auto" />
73
+ </div>
74
+ <Sidebar.User
75
+ name={auth.authInfo()?.employeeName ?? ""}
76
+ description={auth.authInfo()?.email ?? ""}
77
+ menus={employeeMenus}
78
+ />
79
+ <Sidebar.Menu menus={appStructure.usableMenus()} class="flex-1" />
80
+ <div class="pointer-events-none px-2 py-1 text-sm text-black/30 dark:text-white/30">
81
+ v{env.VER}
82
+ <Show when={env.DEV}>(dev)</Show>
83
+ </div>
84
+ </Sidebar>
85
+
86
+ <Topbar.Container>
87
+ <Topbar>
88
+ <span class="ml-2 mr-6 text-lg font-bold">{titleChain().join(" > ")}</span>
89
+ <Topbar.Actions />
90
+ <div class="flex-1" />
91
+ <NotificationBell />
92
+ <ThemeToggle size="sm" />
93
+ </Topbar>
94
+
95
+ <main class="flex-1 overflow-auto">
96
+ <Suspense fallback={<BusyContainer ready={false} />}>
97
+ {props.children}
98
+ </Suspense>
99
+ </main>
100
+ </Topbar.Container>
101
+ </Sidebar.Container>
102
+ </BusyContainer>
103
+ );
104
+ }
105
+
106
+ export function HomeView(props: RouteSectionProps) {
107
+ return <HomeContent {...props} />;
108
+ }
@@ -0,0 +1,262 @@
1
+ import { createMemo, Show } from "solid-js";
2
+ import { CrudDetail, DatePicker, FormTable, TextInput, useSharedData } from "@simplysm/solid";
3
+ import type { DateOnly } from "@simplysm/core-common";
4
+ import { expr, type IDataLogJoinResult } from "@{{projectName}}/db-main";
5
+ import { useAuth } from "../../../../providers/AuthProvider";
6
+ import { useAppService } from "../../../../providers/AppServiceProvider";
7
+ import type { AppSharedData } from "../../../../providers/configureSharedData";
8
+
9
+ type DetailData = {
10
+ name?: string;
11
+ email?: string;
12
+ phoneNumber?: string;
13
+ birthDate?: DateOnly;
14
+ enteringDate?: DateOnly;
15
+ leavingDate?: DateOnly;
16
+ socialSecurityNumber?: string;
17
+ payrollAccountBank?: string;
18
+ payrollAccountNumber?: string;
19
+ password?: string;
20
+ isDeleted: boolean;
21
+ };
22
+
23
+ export function EmployeeDetail(props: { itemId?: number }) {
24
+ const auth = useAuth();
25
+ const appService = useAppService();
26
+ const sharedData = useSharedData<AppSharedData>();
27
+
28
+ const perms = createMemo(() => {
29
+ const p = auth.authInfo()?.permissions ?? {};
30
+ return {
31
+ edit: Boolean(p["/home/base/employee/edit"]),
32
+ authUse: Boolean(p["/home/base/employee/auth/use"]),
33
+ authEdit: Boolean(p["/home/base/employee/auth/edit"]),
34
+ personalUse: Boolean(p["/home/base/employee/personal/use"]),
35
+ personalEdit: Boolean(p["/home/base/employee/personal/edit"]),
36
+ payrollUse: Boolean(p["/home/base/employee/payroll/use"]),
37
+ payrollEdit: Boolean(p["/home/base/employee/payroll/edit"]),
38
+ };
39
+ });
40
+
41
+ // toggleDelete에서 현재 데이터 접근용
42
+ let dataRef: DetailData = { isDeleted: false };
43
+
44
+ async function handleLoad() {
45
+ if (props.itemId == null) {
46
+ return {
47
+ data: { isDeleted: false } as DetailData,
48
+ info: { isNew: true, isDeleted: false },
49
+ };
50
+ }
51
+
52
+ return appService.orm.connect(async (db) => {
53
+ const emp = await db
54
+ .employee()
55
+ .joinLastDataLog()
56
+ // eslint-disable-next-line solid/reactivity -- 비동기 콜백 내부
57
+ .where((c) => [expr.eq(c.id, props.itemId)])
58
+ .select((c) => ({
59
+ name: c.name,
60
+ email: c.email,
61
+ phoneNumber: c.phoneNumber,
62
+ birthDate: c.birthDate,
63
+ enteringDate: c.enteringDate,
64
+ leavingDate: c.leavingDate,
65
+ socialSecurityNumber: c.socialSecurityNumber,
66
+ payrollAccountBank: c.payrollAccountBank,
67
+ payrollAccountNumber: c.payrollAccountNumber,
68
+ isDeleted: c.isDeleted,
69
+ lastDataLog: c.lastDataLog,
70
+ }))
71
+ .single();
72
+
73
+ if (!emp) throw new Error("직원를 찾을 수 없습니다.");
74
+
75
+ const lastDataLog = emp.lastDataLog as IDataLogJoinResult | undefined;
76
+
77
+ return {
78
+ data: {
79
+ name: emp.name,
80
+ email: emp.email,
81
+ phoneNumber: emp.phoneNumber,
82
+ birthDate: emp.birthDate,
83
+ enteringDate: emp.enteringDate,
84
+ leavingDate: emp.leavingDate,
85
+ socialSecurityNumber: emp.socialSecurityNumber,
86
+ payrollAccountBank: emp.payrollAccountBank,
87
+ payrollAccountNumber: emp.payrollAccountNumber,
88
+ isDeleted: emp.isDeleted,
89
+ } as DetailData,
90
+ info: {
91
+ isNew: false,
92
+ isDeleted: emp.isDeleted,
93
+ lastModifiedAt: lastDataLog?.dateTime,
94
+ lastModifiedBy: lastDataLog?.employeeName,
95
+ },
96
+ };
97
+ });
98
+ }
99
+
100
+ async function handleSubmit(data: DetailData) {
101
+ const ids = await appService.employee.save([{ id: props.itemId, ...data }]);
102
+ await sharedData.employee.emit(ids);
103
+ return true;
104
+ }
105
+
106
+ async function handleToggleDelete(del: boolean) {
107
+ const ids = await appService.employee.save([{ id: props.itemId!, ...dataRef, isDeleted: del }]);
108
+ await sharedData.employee.emit(ids);
109
+ return true;
110
+ }
111
+
112
+ return (
113
+ <CrudDetail<DetailData>
114
+ editable={perms().edit}
115
+ deletable={props.itemId != null && props.itemId !== auth.authInfo()?.employeeId}
116
+ load={handleLoad}
117
+ submit={handleSubmit}
118
+ toggleDelete={handleToggleDelete}
119
+ >
120
+ {(ctx) => {
121
+ dataRef = ctx.data;
122
+ return (
123
+ <div class="flex flex-col gap-8">
124
+ <section class={"flex flex-col gap-2"}>
125
+ <h3 class="font-bold text-base-400">직원정보</h3>
126
+ <FormTable>
127
+ <tbody>
128
+ <tr>
129
+ <th>이름</th>
130
+ <td>
131
+ <TextInput
132
+ required
133
+ disabled={!perms().edit}
134
+ value={ctx.data.name ?? ""}
135
+ onValueChange={(v) => ctx.setData("name", v)}
136
+ />
137
+ </td>
138
+ <th>이메일</th>
139
+ <td>
140
+ <TextInput
141
+ type="email"
142
+ disabled={!perms().edit}
143
+ value={ctx.data.email ?? ""}
144
+ onValueChange={(v) => ctx.setData("email", v)}
145
+ />
146
+ </td>
147
+ </tr>
148
+ <tr>
149
+ <th>전화번호</th>
150
+ <td>
151
+ <TextInput
152
+ disabled={!perms().edit}
153
+ value={ctx.data.phoneNumber ?? ""}
154
+ onValueChange={(v) => ctx.setData("phoneNumber", v)}
155
+ />
156
+ </td>
157
+ <th>생년월일</th>
158
+ <td>
159
+ <DatePicker
160
+ disabled={!perms().edit}
161
+ value={ctx.data.birthDate}
162
+ onValueChange={(v) => ctx.setData("birthDate", v)}
163
+ />
164
+ </td>
165
+ </tr>
166
+ <tr>
167
+ <th>입사일자</th>
168
+ <td>
169
+ <DatePicker
170
+ disabled={!perms().edit}
171
+ value={ctx.data.enteringDate}
172
+ onValueChange={(v) => ctx.setData("enteringDate", v)}
173
+ />
174
+ </td>
175
+ <th>퇴사일자</th>
176
+ <td>
177
+ <DatePicker
178
+ disabled={!perms().edit}
179
+ value={ctx.data.leavingDate}
180
+ onValueChange={(v) => ctx.setData("leavingDate", v)}
181
+ />
182
+ </td>
183
+ </tr>
184
+ </tbody>
185
+ </FormTable>
186
+ </section>
187
+
188
+ <Show when={perms().authUse}>
189
+ <section class={"flex flex-col gap-2"}>
190
+ <h3 class="font-bold text-base-400">인증정보</h3>
191
+ <FormTable>
192
+ <tbody>
193
+ <tr>
194
+ <th>비밀번호</th>
195
+ <td colSpan={3}>
196
+ <TextInput
197
+ type="password"
198
+ disabled={!perms().authEdit}
199
+ value={ctx.data.password ?? ""}
200
+ onValueChange={(v) => ctx.setData("password", v)}
201
+ />
202
+ </td>
203
+ </tr>
204
+ </tbody>
205
+ </FormTable>
206
+ </section>
207
+ </Show>
208
+
209
+ <Show when={perms().personalUse}>
210
+ <section class={"flex flex-col gap-2"}>
211
+ <h3 class="font-bold text-base-400">개인정보</h3>
212
+ <FormTable>
213
+ <tbody>
214
+ <tr>
215
+ <th>주민번호</th>
216
+ <td colSpan={3}>
217
+ <TextInput
218
+ format="XXXXXX-XXXXXXX"
219
+ disabled={!perms().personalEdit}
220
+ value={ctx.data.socialSecurityNumber ?? ""}
221
+ onValueChange={(v) => ctx.setData("socialSecurityNumber", v)}
222
+ />
223
+ </td>
224
+ </tr>
225
+ </tbody>
226
+ </FormTable>
227
+ </section>
228
+ </Show>
229
+
230
+ <Show when={perms().payrollUse}>
231
+ <section class={"flex flex-col gap-2"}>
232
+ <h3 class="font-bold text-base-400">급여정보</h3>
233
+ <FormTable>
234
+ <tbody>
235
+ <tr>
236
+ <th>계좌은행</th>
237
+ <td>
238
+ <TextInput
239
+ disabled={!perms().payrollEdit}
240
+ value={ctx.data.payrollAccountBank ?? ""}
241
+ onValueChange={(v) => ctx.setData("payrollAccountBank", v)}
242
+ />
243
+ </td>
244
+ <th>계좌번호</th>
245
+ <td>
246
+ <TextInput
247
+ disabled={!perms().payrollEdit}
248
+ value={ctx.data.payrollAccountNumber ?? ""}
249
+ onValueChange={(v) => ctx.setData("payrollAccountNumber", v)}
250
+ />
251
+ </td>
252
+ </tr>
253
+ </tbody>
254
+ </FormTable>
255
+ </section>
256
+ </Show>
257
+ </div>
258
+ );
259
+ }}
260
+ </CrudDetail>
261
+ );
262
+ }
@@ -0,0 +1,271 @@
1
+ import { Show } from "solid-js";
2
+ import {
3
+ Checkbox,
4
+ CrudSheet,
5
+ type ExcelConfig,
6
+ FormGroup,
7
+ type ModalEditConfig,
8
+ type SortingDef,
9
+ TextInput,
10
+ useDialog,
11
+ useSharedData,
12
+ } from "@simplysm/solid";
13
+ import { DateOnly, type DateTime, objGetChainValue, strIsNullOrEmpty } from "@simplysm/core-common";
14
+ import { expr } from "@{{projectName}}/db-main";
15
+ import { type ExprUnit } from "@simplysm/orm-common";
16
+ import { ExcelWrapper } from "@simplysm/excel";
17
+ import { downloadBlob } from "@simplysm/core-browser";
18
+ import { z } from "zod";
19
+ import { useAppService } from "../../../../providers/AppServiceProvider";
20
+ import { useAppStructure } from "../../../../providers/AppStructureProvider";
21
+ import { EmployeeDetail } from "./EmployeeDetail";
22
+ import type { AppSharedData } from "../../../../providers/configureSharedData";
23
+
24
+ type SheetItem = {
25
+ id?: number;
26
+ name?: string;
27
+ email?: string;
28
+ phoneNumber?: string;
29
+ birthDate?: DateOnly;
30
+ enteringDate?: DateOnly;
31
+ leavingDate?: DateOnly;
32
+ socialSecurityNumber?: string;
33
+ payrollAccountBank?: string;
34
+ payrollAccountNumber?: string;
35
+ isDeleted: boolean;
36
+ lastDataLog?: {
37
+ action: string;
38
+ dateTime: DateTime;
39
+ employeeName?: string;
40
+ };
41
+ };
42
+
43
+ type Filter = {
44
+ searchText?: string;
45
+ isIncludeLeft: boolean;
46
+ isIncludeDeleted: boolean;
47
+ };
48
+
49
+ const ITEMS_PER_PAGE = 50;
50
+
51
+ const excelWrapper = new ExcelWrapper(
52
+ z.object({
53
+ id: z.number().optional().describe("ID"),
54
+ name: z.string().describe("이름"),
55
+ email: z.string().optional().describe("이메일"),
56
+ phoneNumber: z.string().optional().describe("전화번호"),
57
+ birthDate: z.custom<DateOnly>().optional().describe("생년월일"),
58
+ enteringDate: z.custom<DateOnly>().optional().describe("입사일자"),
59
+ leavingDate: z.custom<DateOnly>().optional().describe("퇴사일자"),
60
+ isDeleted: z.boolean().describe("삭제여부"),
61
+ socialSecurityNumber: z.string().optional().describe("주민번호"),
62
+ payrollAccountBank: z.string().optional().describe("급여계좌은행"),
63
+ payrollAccountNumber: z.string().optional().describe("급여계좌번호"),
64
+ password: z.string().optional().describe("비밀번호"),
65
+ }),
66
+ );
67
+
68
+ export function EmployeeSheet() {
69
+ const appService = useAppService();
70
+ const dialog = useDialog();
71
+ const sharedData = useSharedData<AppSharedData>();
72
+
73
+ const { perms } = useAppStructure();
74
+ const employeePerms = perms.home.base.employee;
75
+
76
+ const modalEdit: ModalEditConfig<SheetItem> = {
77
+ editItem: (item) =>
78
+ dialog.show(() => <EmployeeDetail itemId={item?.id} />, {
79
+ header: item ? "직원 수정" : "직원 등록",
80
+ }),
81
+ };
82
+
83
+ const excel: ExcelConfig<SheetItem> = {
84
+ download: async (items) => {
85
+ await using wb = await excelWrapper.write("직원", items, {
86
+ excludes: [
87
+ ...(!employeePerms.personal.use ? (["socialSecurityNumber"] as const) : []),
88
+ ...(!employeePerms.payroll.use
89
+ ? (["payrollAccountBank", "payrollAccountNumber"] as const)
90
+ : []),
91
+ ...(!employeePerms.auth.edit ? (["password"] as const) : []),
92
+ ],
93
+ });
94
+ const blob = await wb.getBlob();
95
+ downloadBlob(blob, "직원.xlsx");
96
+ },
97
+ upload: async (file) => {
98
+ const excelItems = await excelWrapper.read(file);
99
+ const ids = await appService.employee.save(excelItems);
100
+ await sharedData.employee.emit(ids);
101
+ },
102
+ };
103
+
104
+ async function search(filter: Filter, page: number | undefined, sorts: SortingDef[]) {
105
+ return appService.orm.connect(async (db) => {
106
+ let qr = db.employee();
107
+
108
+ const searchText = filter.searchText?.trim();
109
+ if (!strIsNullOrEmpty(searchText)) {
110
+ qr = qr.search((c) => [c.name, c.email, c.phoneNumber], searchText);
111
+ }
112
+
113
+ if (!filter.isIncludeLeft) {
114
+ qr = qr.where((c) => [
115
+ expr.or([expr.null(c.leavingDate), expr.gt(c.leavingDate, new DateOnly())]),
116
+ ]);
117
+ }
118
+
119
+ if (!filter.isIncludeDeleted) {
120
+ qr = qr.where((c) => [expr.eq(c.isDeleted, false)]);
121
+ }
122
+
123
+ const pageCount = page != null ? Math.ceil((await qr.count()) / ITEMS_PER_PAGE) : undefined;
124
+
125
+ let qr2 = qr.joinLastDataLog().select((c) => ({
126
+ id: c.id,
127
+ name: c.name,
128
+ email: c.email,
129
+ phoneNumber: c.phoneNumber,
130
+ birthDate: c.birthDate,
131
+ enteringDate: c.enteringDate,
132
+ leavingDate: c.leavingDate,
133
+ ...(employeePerms.personal.use ? { socialSecurityNumber: c.socialSecurityNumber } : {}),
134
+ ...(employeePerms.payroll.use
135
+ ? {
136
+ payrollAccountBank: c.payrollAccountBank,
137
+ payrollAccountNumber: c.payrollAccountNumber,
138
+ }
139
+ : {}),
140
+ isDeleted: c.isDeleted,
141
+ lastDataLog: c.lastDataLog,
142
+ }));
143
+
144
+ for (const sort of sorts) {
145
+ qr2 = qr2.orderBy(
146
+ (c) => objGetChainValue(c, sort.key) as ExprUnit<any>,
147
+ sort.desc ? "DESC" : "ASC",
148
+ );
149
+ }
150
+ if (!sorts.some((s) => s.key === "id")) {
151
+ qr2 = qr2.orderBy((c) => c.id, "DESC");
152
+ }
153
+
154
+ if (page != null) {
155
+ qr2 = qr2.limit((page - 1) * ITEMS_PER_PAGE, ITEMS_PER_PAGE);
156
+ }
157
+
158
+ const items = (await qr2.result()) as SheetItem[];
159
+ return { items, pageCount };
160
+ });
161
+ }
162
+
163
+ return (
164
+ <CrudSheet<SheetItem, Filter>
165
+ search={search}
166
+ getItemKey={(item) => item.id}
167
+ itemDeleted={(item) => item.isDeleted}
168
+ isItemSelectable={(item) => item.id != null}
169
+ persistKey="employee-page"
170
+ editable={employeePerms.edit}
171
+ filterInitial=\{{ isIncludeLeft: false, isIncludeDeleted: false } as Filter}
172
+ modalEdit={modalEdit}
173
+ excel={excel}
174
+ lastModifiedAtProp="lastDataLog.dateTime"
175
+ lastModifiedByProp="lastDataLog.employeeName"
176
+ >
177
+ <CrudSheet.Filter<Filter>>
178
+ {(filter, setFilter) => (
179
+ <>
180
+ <FormGroup.Item label="검색어">
181
+ <TextInput
182
+ value={filter.searchText}
183
+ onValueChange={(v) => setFilter("searchText", v)}
184
+ />
185
+ </FormGroup.Item>
186
+ <FormGroup.Item>
187
+ <Checkbox
188
+ value={filter.isIncludeLeft}
189
+ onValueChange={(v) => setFilter("isIncludeLeft", v)}
190
+ >
191
+ 퇴사자 포함
192
+ </Checkbox>
193
+ </FormGroup.Item>
194
+ <FormGroup.Item>
195
+ <Checkbox
196
+ value={filter.isIncludeDeleted}
197
+ onValueChange={(v) => setFilter("isIncludeDeleted", v)}
198
+ >
199
+ 삭제항목 포함
200
+ </Checkbox>
201
+ </FormGroup.Item>
202
+ </>
203
+ )}
204
+ </CrudSheet.Filter>
205
+
206
+ <CrudSheet.Column<SheetItem> key="id" header="#" editTrigger>
207
+ {({ item }) => <div class="px-2 py-1 text-right">{item.id}</div>}
208
+ </CrudSheet.Column>
209
+
210
+ <CrudSheet.Column<SheetItem> key="name" header="이름">
211
+ {({ item }) => <div class="px-2 py-1">{item.name}</div>}
212
+ </CrudSheet.Column>
213
+
214
+ <CrudSheet.Column<SheetItem> key="email" header="이메일">
215
+ {({ item }) => <div class="px-2 py-1">{item.email}</div>}
216
+ </CrudSheet.Column>
217
+
218
+ <CrudSheet.Column<SheetItem> key="phoneNumber" header="전화번호">
219
+ {({ item }) => <div class="px-2 py-1">{item.phoneNumber}</div>}
220
+ </CrudSheet.Column>
221
+
222
+ <CrudSheet.Column<SheetItem> key="birthDate" header="생년월일">
223
+ {({ item }) => (
224
+ <div class="px-2 py-1">{item.birthDate?.toFormatString("yyyy-MM-dd") ?? ""}</div>
225
+ )}
226
+ </CrudSheet.Column>
227
+
228
+ <CrudSheet.Column<SheetItem> key="enteringDate" header="입사일자">
229
+ {({ item }) => (
230
+ <div class="px-2 py-1">{item.enteringDate?.toFormatString("yyyy-MM-dd") ?? ""}</div>
231
+ )}
232
+ </CrudSheet.Column>
233
+
234
+ <CrudSheet.Column<SheetItem> key="leavingDate" header="퇴사일자">
235
+ {({ item }) => (
236
+ <div class={`px-2 py-1${item.leavingDate != null ? " text-warning-600" : ""}`}>
237
+ {item.leavingDate?.toFormatString("yyyy-MM-dd") ?? ""}
238
+ </div>
239
+ )}
240
+ </CrudSheet.Column>
241
+
242
+ <Show when={employeePerms.auth.use}>
243
+ <CrudSheet.Column<SheetItem>
244
+ key="password"
245
+ header={["인증정보", "비밀번호"]}
246
+ sortable={false}
247
+ >
248
+ {() => <div class="px-2 py-1">****</div>}
249
+ </CrudSheet.Column>
250
+ </Show>
251
+
252
+ <Show when={employeePerms.personal.use}>
253
+ <CrudSheet.Column<SheetItem> key="socialSecurityNumber" header={["개인정보", "주민번호"]}>
254
+ {({ item }) => <div class="px-2 py-1">{item.socialSecurityNumber}</div>}
255
+ </CrudSheet.Column>
256
+ </Show>
257
+
258
+ <Show when={employeePerms.payroll.use}>
259
+ <CrudSheet.Column<SheetItem> key="payrollAccountBank" header={["급여정보", "계좌은행"]}>
260
+ {({ item }) => <div class="px-2 py-1">{item.payrollAccountBank}</div>}
261
+ </CrudSheet.Column>
262
+ </Show>
263
+
264
+ <Show when={employeePerms.payroll.use}>
265
+ <CrudSheet.Column<SheetItem> key="payrollAccountNumber" header={["급여정보", "계좌번호"]}>
266
+ {({ item }) => <div class="px-2 py-1">{item.payrollAccountNumber}</div>}
267
+ </CrudSheet.Column>
268
+ </Show>
269
+ </CrudSheet>
270
+ );
271
+ }