@simplysm/sd-cli 13.0.71 → 13.0.74

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 (108) hide show
  1. package/README.md +62 -14
  2. package/dist/commands/init.d.ts +4 -5
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +26 -8
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/publish.js +1 -1
  7. package/dist/commands/publish.js.map +1 -1
  8. package/dist/sd-cli-entry.d.ts.map +1 -1
  9. package/dist/sd-cli-entry.js +0 -20
  10. package/dist/sd-cli-entry.js.map +1 -1
  11. package/package.json +4 -4
  12. package/src/commands/init.ts +40 -21
  13. package/src/commands/publish.ts +1 -1
  14. package/src/sd-cli-entry.ts +0 -24
  15. package/src/utils/replace-deps.ts +361 -361
  16. package/src/utils/sd-config.ts +44 -44
  17. package/src/utils/tailwind-config-deps.ts +98 -98
  18. package/src/utils/template.ts +56 -56
  19. package/src/utils/tsconfig.ts +127 -127
  20. package/src/utils/typecheck-serialization.ts +86 -86
  21. package/templates/init/{.prettierrc.yaml.hbs → .prettierrc.yaml} +1 -1
  22. package/templates/init/eslint.config.ts +15 -0
  23. package/templates/init/mise.toml +3 -0
  24. package/templates/init/package.json.hbs +8 -7
  25. package/templates/init/packages/client-admin/index.html.hbs +144 -0
  26. package/templates/init/packages/client-admin/package.json.hbs +26 -0
  27. package/templates/init/packages/client-admin/public/assets/logo-landscape.png +0 -0
  28. package/templates/init/packages/client-admin/public/assets/logo.png +0 -0
  29. package/templates/init/packages/client-admin/src/App.tsx +42 -0
  30. package/templates/init/packages/client-admin/src/dev/DevDialog.tsx +34 -0
  31. package/templates/{add-client/__CLIENT__/src/main.css.hbs → init/packages/client-admin/src/main.css} +1 -1
  32. package/templates/init/packages/client-admin/src/main.tsx.hbs +146 -0
  33. package/templates/init/packages/client-admin/src/providers/AppServiceProvider.tsx.hbs +103 -0
  34. package/templates/init/packages/client-admin/src/providers/AppStructureProvider.tsx +84 -0
  35. package/templates/init/packages/client-admin/src/providers/AuthProvider.tsx.hbs +71 -0
  36. package/templates/init/packages/client-admin/src/providers/configureSharedData.ts.hbs +67 -0
  37. package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +132 -0
  38. package/templates/init/packages/client-admin/src/views/home/HomeView.tsx +108 -0
  39. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeDetail.tsx.hbs +262 -0
  40. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +271 -0
  41. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleDetail.tsx.hbs +154 -0
  42. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionDetail.tsx.hbs +123 -0
  43. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionView.tsx +52 -0
  44. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +125 -0
  45. package/templates/init/packages/client-admin/src/views/home/main/MainView.tsx.hbs +13 -0
  46. package/templates/init/packages/client-admin/src/views/home/my-info/MyInfoDetail.tsx.hbs +248 -0
  47. package/templates/init/packages/client-admin/src/views/home/system/system-log/SystemLogSheet.tsx.hbs +169 -0
  48. package/templates/init/packages/client-admin/src/views/not-found/NotFoundView.tsx +15 -0
  49. package/templates/init/packages/client-admin/tailwind.config.ts +10 -0
  50. package/templates/init/packages/db-main/package.json.hbs +13 -0
  51. package/templates/init/packages/db-main/src/MainDbContext.ts +20 -0
  52. package/templates/init/packages/db-main/src/dataLogExt.ts +127 -0
  53. package/templates/init/packages/db-main/src/index.ts +10 -0
  54. package/templates/init/packages/db-main/src/tables/Employee.ts +24 -0
  55. package/templates/init/packages/db-main/src/tables/EmployeeConfig.ts +13 -0
  56. package/templates/init/packages/db-main/src/tables/Role.ts +9 -0
  57. package/templates/init/packages/db-main/src/tables/RolePermission.ts +13 -0
  58. package/templates/init/packages/db-main/src/tables/_DataLog.ts +19 -0
  59. package/templates/init/packages/db-main/src/tables/_Log.ts +16 -0
  60. package/templates/init/packages/server/package.json.hbs +20 -0
  61. package/templates/init/packages/server/public-dev/dev//354/264/210/352/270/260/355/231/224.xlsx +0 -0
  62. package/templates/init/packages/server/src/index.ts +4 -0
  63. package/templates/init/packages/server/src/main.ts.hbs +34 -0
  64. package/templates/init/packages/server/src/services/AuthService.ts.hbs +171 -0
  65. package/templates/init/packages/server/src/services/DevService.ts.hbs +94 -0
  66. package/templates/init/packages/server/src/services/EmployeeService.ts.hbs +122 -0
  67. package/templates/init/packages/server/src/services/RoleService.ts.hbs +59 -0
  68. package/templates/init/{pnpm-workspace.yaml.hbs → pnpm-workspace.yaml} +3 -1
  69. package/templates/init/sd.config.ts.hbs +30 -1
  70. package/templates/init/tests/e2e/package.json.hbs +16 -0
  71. package/templates/init/tests/e2e/src/e2e.spec.ts +36 -0
  72. package/templates/init/tests/e2e/src/employee-crud.ts +204 -0
  73. package/templates/init/tests/e2e/src/login.ts +61 -0
  74. package/templates/init/tests/e2e/vitest.setup.ts.hbs +220 -0
  75. package/templates/init/tsconfig.json.hbs +0 -11
  76. package/templates/init/{vitest.config.ts.hbs → vitest.config.ts} +16 -12
  77. package/dist/commands/add-client.d.ts +0 -18
  78. package/dist/commands/add-client.d.ts.map +0 -1
  79. package/dist/commands/add-client.js +0 -79
  80. package/dist/commands/add-client.js.map +0 -6
  81. package/dist/commands/add-server.d.ts +0 -18
  82. package/dist/commands/add-server.d.ts.map +0 -1
  83. package/dist/commands/add-server.js +0 -83
  84. package/dist/commands/add-server.js.map +0 -6
  85. package/dist/utils/config-editor.d.ts +0 -17
  86. package/dist/utils/config-editor.d.ts.map +0 -1
  87. package/dist/utils/config-editor.js +0 -79
  88. package/dist/utils/config-editor.js.map +0 -6
  89. package/src/commands/add-client.ts +0 -126
  90. package/src/commands/add-server.ts +0 -138
  91. package/src/utils/config-editor.ts +0 -141
  92. package/templates/add-client/__CLIENT__/index.html.hbs +0 -13
  93. package/templates/add-client/__CLIENT__/package.json.hbs +0 -16
  94. package/templates/add-client/__CLIENT__/src/App.tsx.hbs +0 -65
  95. package/templates/add-client/__CLIENT__/src/appStructure.ts.hbs +0 -20
  96. package/templates/add-client/__CLIENT__/src/main.tsx.hbs +0 -24
  97. package/templates/add-client/__CLIENT__/src/pages/HomePage.tsx.hbs +0 -9
  98. package/templates/add-client/__CLIENT__/tailwind.config.ts.hbs +0 -15
  99. package/templates/add-server/__SERVER__/package.json.hbs +0 -10
  100. package/templates/add-server/__SERVER__/src/main.ts.hbs +0 -14
  101. package/templates/init/.gitignore.hbs +0 -26
  102. package/templates/init/.npmrc.hbs +0 -1
  103. package/templates/init/eslint.config.ts.hbs +0 -5
  104. package/templates/init/mise.toml.hbs +0 -3
  105. package/tests/config-editor.spec.ts +0 -160
  106. /package/templates/init/{.prettierignore.hbs → .prettierignore} +0 -0
  107. /package/templates/{add-client/__CLIENT__ → init/packages/client-admin}/public/favicon.ico +0 -0
  108. /package/templates/init/{stylelint.config.ts.hbs → stylelint.config.ts} +0 -0
@@ -0,0 +1,123 @@
1
+ import { createMemo, createSignal } from "solid-js";
2
+ import { reconcile } from "solid-js/store";
3
+ import {
4
+ type AppPerm,
5
+ Button,
6
+ CrudDetail,
7
+ PermissionTable,
8
+ SharedDataSelect,
9
+ useSharedData,
10
+ } from "@simplysm/solid";
11
+ import { expr } from "@{{projectName}}/db-main";
12
+ import type { AppSharedData, RoleSharedItem } from "../../../../providers/configureSharedData";
13
+ import { useAuth } from "../../../../providers/AuthProvider";
14
+ import { useAppService } from "../../../../providers/AppServiceProvider";
15
+ import { useAppStructure } from "../../../../providers/AppStructureProvider";
16
+
17
+ export function RolePermissionDetail(props: { roleId: number }) {
18
+ const auth = useAuth();
19
+ const appService = useAppService();
20
+ const appStructure = useAppStructure();
21
+ const sharedData = useSharedData<AppSharedData>();
22
+
23
+ const perms = createMemo(() => {
24
+ const p = auth.authInfo()?.permissions ?? {};
25
+ return {
26
+ edit: Boolean(p["/home/base/role-permission/edit"]),
27
+ };
28
+ });
29
+
30
+ const [copySourceRoleId, setCopySourceRoleId] = createSignal<number>();
31
+
32
+ async function loadPermissionsByRoleId(roleId: number): Promise<Record<string, boolean>> {
33
+ return appService.orm.connectWithoutTransaction(async (db) => {
34
+ const rows = await db
35
+ .rolePermission()
36
+ .where((c) => [expr.eq(c.roleId, roleId)])
37
+ .select((c) => ({
38
+ code: c.code,
39
+ valueJson: c.valueJson,
40
+ }))
41
+ .result();
42
+
43
+ const record: Record<string, boolean> = {};
44
+ for (const row of rows) {
45
+ record[row.code] = JSON.parse(row.valueJson);
46
+ }
47
+ return record;
48
+ });
49
+ }
50
+
51
+ async function handleLoad() {
52
+ const data = await loadPermissionsByRoleId(props.roleId);
53
+ return {
54
+ data,
55
+ info: { isNew: false, isDeleted: false },
56
+ };
57
+ }
58
+
59
+ async function handleSubmit(data: Record<string, boolean>) {
60
+ // eslint-disable-next-line solid/reactivity -- 비동기 콜백 내부
61
+ await appService.orm.connect(async (db) => {
62
+ await db
63
+ .rolePermission()
64
+ // eslint-disable-next-line solid/reactivity -- 비동기 콜백 내부
65
+ .where((c) => [expr.eq(c.roleId, props.roleId)])
66
+ .delete();
67
+
68
+ const entries = Object.entries(data).filter(([, v]) => v);
69
+ if (entries.length > 0) {
70
+ await db.rolePermission().insert(
71
+ entries.map(([code, value]) => ({
72
+ roleId: props.roleId,
73
+ code,
74
+ valueJson: JSON.stringify(value),
75
+ })),
76
+ );
77
+ }
78
+ });
79
+ return true;
80
+ }
81
+
82
+ return (
83
+ <CrudDetail<Record<string, boolean>>
84
+ class={"p-2"}
85
+ editable={perms().edit}
86
+ load={handleLoad}
87
+ submit={handleSubmit}
88
+ >
89
+ {(ctx) => {
90
+ async function onImportClick() {
91
+ const sourceId = copySourceRoleId();
92
+ if (sourceId == null) return;
93
+ const data = await loadPermissionsByRoleId(sourceId);
94
+ ctx.setData(reconcile(data));
95
+ }
96
+
97
+ return (
98
+ <>
99
+ <CrudDetail.Tools>
100
+ <SharedDataSelect
101
+ data={sharedData.role}
102
+ value={copySourceRoleId()}
103
+ onValueChange={(v) => setCopySourceRoleId(v as number | undefined)}
104
+ >
105
+ {(item: RoleSharedItem) => <>{item.name}</>}
106
+ </SharedDataSelect>
107
+ <Button onClick={onImportClick} disabled={copySourceRoleId() == null}>
108
+ 가져오기
109
+ </Button>
110
+ </CrudDetail.Tools>
111
+
112
+ <PermissionTable
113
+ items={appStructure.usablePerms().flatMap((p) => p.children ?? []) as AppPerm<string>[]}
114
+ value={ctx.data}
115
+ onValueChange={(v) => ctx.setData(reconcile(v))}
116
+ disabled={!perms().edit}
117
+ />
118
+ </>
119
+ );
120
+ }}
121
+ </CrudDetail>
122
+ );
123
+ }
@@ -0,0 +1,52 @@
1
+ import { createSignal, Show } from "solid-js";
2
+ import { Button, Icon, SharedDataSelectList, useDialog, useSharedData } from "@simplysm/solid";
3
+ import type { AppSharedData, RoleSharedItem } from "../../../../providers/configureSharedData";
4
+ import { RolePermissionDetail } from "./RolePermissionDetail";
5
+ import { RoleSheet } from "./RoleSheet";
6
+ import { IconExternalLink } from "@tabler/icons-solidjs";
7
+
8
+ export function RolePermissionView() {
9
+ const dialog = useDialog();
10
+ const sharedData = useSharedData<AppSharedData>();
11
+ const [selectedRole, setSelectedRole] = createSignal<RoleSharedItem>();
12
+
13
+ return (
14
+ <div class="flex h-full flex-row">
15
+ <SharedDataSelectList
16
+ class={"border-r border-base-200 p-1.5 py-3"}
17
+ data={sharedData.role}
18
+ value={selectedRole()}
19
+ onValueChange={setSelectedRole}
20
+ header={
21
+ <div class={"flex flex-row p-1"}>
22
+ <div class={"flex-1 font-bold text-base-400"}>권한그룹</div>
23
+ <Button
24
+ variant={"ghost"}
25
+ theme={"primary"}
26
+ size={"xs"}
27
+ onClick={() => dialog.show(() => <RoleSheet />, { header: "권한그룹", closeOnBackdrop: true })}
28
+ >
29
+ <Icon icon={IconExternalLink} />
30
+ </Button>
31
+ </div>
32
+ }
33
+ required
34
+ >
35
+ <SharedDataSelectList.ItemTemplate<RoleSharedItem>>
36
+ {(item) => <>{item.name}</>}
37
+ </SharedDataSelectList.ItemTemplate>
38
+ </SharedDataSelectList>
39
+
40
+ <Show
41
+ when={selectedRole()}
42
+ fallback={
43
+ <div class="flex flex-1 items-center justify-center text-4xl leading-relaxed text-base-300">
44
+ 권한그룹을 선택하세요.
45
+ </div>
46
+ }
47
+ >
48
+ <RolePermissionDetail roleId={selectedRole()!.id} />
49
+ </Show>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ Checkbox,
3
+ CrudSheet,
4
+ type DialogEditConfig,
5
+ type SortingDef,
6
+ TextInput,
7
+ useDialog,
8
+ } from "@simplysm/solid";
9
+ import { FormGroup } from "@simplysm/solid";
10
+ import { expr } from "@{{projectName}}/db-main";
11
+ import { type ExprUnit } from "@simplysm/orm-common";
12
+ import { objGetChainValue } from "@simplysm/core-common";
13
+ import { useAppService } from "../../../../providers/AppServiceProvider";
14
+ import { useAppStructure } from "../../../../providers/AppStructureProvider";
15
+ import { RoleDetail } from "./RoleDetail";
16
+
17
+ type SheetItem = {
18
+ id?: number;
19
+ name?: string;
20
+ isDeleted: boolean;
21
+ };
22
+
23
+ type Filter = {
24
+ searchText?: string;
25
+ isIncludeDeleted: boolean;
26
+ };
27
+
28
+ const ITEMS_PER_PAGE = 50;
29
+
30
+ export function RoleSheet() {
31
+ const appService = useAppService();
32
+ const appStructure = useAppStructure();
33
+ const dialog = useDialog();
34
+
35
+ const rolePerms = appStructure.perms.home.base["role-permission"];
36
+
37
+ const dialogEdit: DialogEditConfig<SheetItem> = {
38
+ editItem: (item) =>
39
+ dialog.show(() => <RoleDetail itemId={item?.id} />, {
40
+ header: item ? "권한그룹 수정" : "권한그룹 등록",
41
+ }),
42
+ };
43
+
44
+ async function search(filter: Filter, page: number | undefined, sorts: SortingDef[]) {
45
+ return appService.orm.connect(async (db) => {
46
+ let qr = db.role();
47
+
48
+ const searchText = filter.searchText?.trim();
49
+ if (searchText != null && searchText.length > 0) {
50
+ qr = qr.search((c) => [c.name], searchText);
51
+ }
52
+
53
+ if (!filter.isIncludeDeleted) {
54
+ qr = qr.where((c) => [expr.eq(c.isDeleted, false)]);
55
+ }
56
+
57
+ const pageCount = page != null ? Math.ceil((await qr.count()) / ITEMS_PER_PAGE) : undefined;
58
+
59
+ let qr2 = qr.select((c) => ({
60
+ id: c.id,
61
+ name: c.name,
62
+ isDeleted: c.isDeleted,
63
+ }));
64
+
65
+ for (const sort of sorts) {
66
+ qr2 = qr2.orderBy(
67
+ (c) => objGetChainValue(c, sort.key) as ExprUnit<any>,
68
+ sort.desc ? "DESC" : "ASC",
69
+ );
70
+ }
71
+ if (!sorts.some((s) => s.key === "id")) {
72
+ qr2 = qr2.orderBy((c) => c.id, "DESC");
73
+ }
74
+
75
+ if (page != null) {
76
+ qr2 = qr2.limit((page - 1) * ITEMS_PER_PAGE, ITEMS_PER_PAGE);
77
+ }
78
+
79
+ const items = (await qr2.result()) as SheetItem[];
80
+ return { items, pageCount };
81
+ });
82
+ }
83
+
84
+ return (
85
+ <CrudSheet<SheetItem, Filter>
86
+ search={search}
87
+ getItemKey={(item) => item.id}
88
+ itemDeleted={(item) => item.isDeleted}
89
+ isItemSelectable={(item) => item.id != null}
90
+ persistKey="role-sheet-page"
91
+ editable={rolePerms.edit}
92
+ filterInitial=\{{ isIncludeDeleted: false } as Filter}
93
+ dialogEdit={dialogEdit}
94
+ >
95
+ <CrudSheet.Filter<Filter>>
96
+ {(filter, setFilter) => (
97
+ <>
98
+ <FormGroup.Item label="검색어">
99
+ <TextInput
100
+ value={filter.searchText}
101
+ onValueChange={(v) => setFilter("searchText", v)}
102
+ />
103
+ </FormGroup.Item>
104
+ <FormGroup.Item>
105
+ <Checkbox
106
+ value={filter.isIncludeDeleted}
107
+ onValueChange={(v) => setFilter("isIncludeDeleted", v)}
108
+ >
109
+ 삭제항목 포함
110
+ </Checkbox>
111
+ </FormGroup.Item>
112
+ </>
113
+ )}
114
+ </CrudSheet.Filter>
115
+
116
+ <CrudSheet.Column<SheetItem> key="id" header="#" editTrigger>
117
+ {({ item }) => <div class="px-2 py-1 text-right">{item.id}</div>}
118
+ </CrudSheet.Column>
119
+
120
+ <CrudSheet.Column<SheetItem> key="name" header="이름">
121
+ {({ item }) => <div class="px-2 py-1">{item.name}</div>}
122
+ </CrudSheet.Column>
123
+ </CrudSheet>
124
+ );
125
+ }
@@ -0,0 +1,13 @@
1
+ import { useAuth } from "../../../providers/AuthProvider";
2
+
3
+ export function MainView() {
4
+ const auth = useAuth();
5
+
6
+ return (
7
+ <div class="flex h-full items-center justify-center">
8
+ <h1 class="text-2xl font-bold text-base-700 dark:text-base-200">
9
+ {{projectName}} ({auth.authInfo()?.employeeName})
10
+ </h1>
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,248 @@
1
+ import { createMemo } from "solid-js";
2
+ import { CrudDetail, FormTable, Icon, Select, TextInput } from "@simplysm/solid";
3
+ import { useAuth } from "../../../providers/AuthProvider";
4
+ import { useAppService } from "../../../providers/AppServiceProvider";
5
+ import { useAppStructure } from "../../../providers/AppStructureProvider";
6
+ import { expr } from "@{{projectName}}/db-main";
7
+ import { IconX } from "@tabler/icons-solidjs";
8
+ import { jsonParse } from "@simplysm/core-common";
9
+
10
+ type DetailData = {
11
+ email: string;
12
+ currentPassword: string;
13
+ newPassword: string;
14
+ confirmPassword: string;
15
+ firstRouterLink?: string;
16
+ };
17
+
18
+ export function MyInfoDetail() {
19
+ const auth = useAuth();
20
+ const appService = useAppService();
21
+ const appStructure = useAppStructure();
22
+
23
+ const routeMap = createMemo(() => {
24
+ const menus = appStructure.usableMenus();
25
+ const map = new Map<string, string>();
26
+
27
+ const flatten = (items: typeof menus, prefix = ""): void => {
28
+ for (const item of items) {
29
+ const label = prefix !== "" ? `${prefix} > ${item.title}` : item.title;
30
+ if (item.href !== undefined) {
31
+ map.set(item.href, label);
32
+ }
33
+ if (item.children !== undefined) {
34
+ flatten(item.children, label);
35
+ }
36
+ }
37
+ };
38
+
39
+ flatten(menus);
40
+ return map;
41
+ });
42
+
43
+ const routeOptions = createMemo(() => Array.from(routeMap().keys()));
44
+
45
+ async function handleLoad() {
46
+ const authData = auth.authInfo();
47
+ if (!authData) throw new Error("직원 정보를 불러올 수 없습니다.");
48
+
49
+ return appService.orm.connect(async (db) => {
50
+ const employee = await db
51
+ .employee()
52
+ .where((c: any) => [expr.eq(c.id, authData.employeeId)])
53
+ .single();
54
+
55
+ const config = await db
56
+ .employeeConfig()
57
+ .where((c: any) => [
58
+ expr.eq(c.employeeId, authData.employeeId),
59
+ expr.eq(c.code, "firstRouterLink"),
60
+ ])
61
+ .first();
62
+
63
+ let firstRouterLink: string | undefined;
64
+ if (config?.valueJson !== undefined && config.valueJson !== "") {
65
+ try {
66
+ firstRouterLink = jsonParse(config.valueJson);
67
+ } catch {
68
+ // JSON 파싱 실패 시 기본값 유지
69
+ }
70
+ }
71
+
72
+ return {
73
+ data: {
74
+ email: employee?.email ?? "",
75
+ currentPassword: "",
76
+ newPassword: "",
77
+ confirmPassword: "",
78
+ firstRouterLink,
79
+ } as DetailData,
80
+ info: { isNew: false, isDeleted: false },
81
+ };
82
+ });
83
+ }
84
+
85
+ async function handleSubmit(data: DetailData) {
86
+ const authData = auth.authInfo();
87
+ if (!authData) throw new Error("직원 정보를 불러올 수 없습니다.");
88
+
89
+ const isPasswordChanging =
90
+ data.currentPassword.trim() !== "" ||
91
+ data.newPassword.trim() !== "" ||
92
+ data.confirmPassword.trim() !== "";
93
+
94
+ if (isPasswordChanging) {
95
+ await appService.auth.changePassword(data.currentPassword.trim(), data.newPassword.trim());
96
+ }
97
+
98
+ await appService.orm.connect(async (db) => {
99
+ await db
100
+ .employee()
101
+ .where((c: any) => [expr.eq(c.id, authData.employeeId)])
102
+ .update(() => ({
103
+ email: expr.val("string", data.email.trim()),
104
+ }));
105
+
106
+ const existingConfig = await db
107
+ .employeeConfig()
108
+ .where((c: any) => [
109
+ expr.eq(c.employeeId, authData.employeeId),
110
+ expr.eq(c.code, "firstRouterLink"),
111
+ ])
112
+ .first();
113
+
114
+ const valueJson =
115
+ data.firstRouterLink !== undefined
116
+ ? JSON.stringify(data.firstRouterLink)
117
+ : JSON.stringify(null);
118
+
119
+ if (existingConfig) {
120
+ await db
121
+ .employeeConfig()
122
+ .where((c: any) => [
123
+ expr.eq(c.employeeId, authData.employeeId),
124
+ expr.eq(c.code, "firstRouterLink"),
125
+ ])
126
+ .update(() => ({
127
+ valueJson: expr.val("string", valueJson),
128
+ }));
129
+ } else {
130
+ await db.employeeConfig().insert([
131
+ {
132
+ employeeId: authData.employeeId,
133
+ code: "firstRouterLink",
134
+ valueJson,
135
+ },
136
+ ]);
137
+ }
138
+ });
139
+
140
+ await auth.tryReloadAuth();
141
+ return true;
142
+ }
143
+
144
+ return (
145
+ <CrudDetail<DetailData> class={"px-2 py-4"} load={handleLoad} submit={handleSubmit}>
146
+ {(ctx) => (
147
+ <div class="flex flex-col gap-8">
148
+ <section>
149
+ <h3 class="mb-4 border-l-4 border-base-500 pl-3 font-bold text-base-500">인증정보</h3>
150
+ <FormTable>
151
+ <tr>
152
+ <th>이메일</th>
153
+ <td>
154
+ <TextInput
155
+ type="email"
156
+ required
157
+ value={ctx.data.email}
158
+ onValueChange={(v) => ctx.setData("email", v)}
159
+ autocomplete="off"
160
+ />
161
+ </td>
162
+ </tr>
163
+ <tr>
164
+ <th>현재 비밀번호</th>
165
+ <td>
166
+ <TextInput
167
+ required={Boolean(ctx.data.newPassword || ctx.data.confirmPassword)}
168
+ class="w-56"
169
+ type="password"
170
+ autocomplete="off"
171
+ value={ctx.data.currentPassword}
172
+ onValueChange={(v) => ctx.setData("currentPassword", v)}
173
+ placeholder="비밀번호 변경 시에만 입력"
174
+ />
175
+ </td>
176
+ </tr>
177
+ <tr>
178
+ <th>새 비밀번호</th>
179
+ <td>
180
+ <TextInput
181
+ required={Boolean(ctx.data.currentPassword || ctx.data.confirmPassword)}
182
+ minLength={8}
183
+ class="w-56"
184
+ type="password"
185
+ autocomplete="off"
186
+ value={ctx.data.newPassword}
187
+ onValueChange={(v) => ctx.setData("newPassword", v)}
188
+ placeholder="비밀번호 변경 시에만 입력"
189
+ />
190
+ </td>
191
+ </tr>
192
+ <tr>
193
+ <th>새 비밀번호 확인</th>
194
+ <td>
195
+ <TextInput
196
+ required={Boolean(ctx.data.currentPassword || ctx.data.newPassword)}
197
+ minLength={8}
198
+ validate={() =>
199
+ ctx.data.newPassword !== ctx.data.confirmPassword
200
+ ? "비밀번호가 일치하지 않습니다"
201
+ : ""
202
+ }
203
+ class="w-56"
204
+ type="password"
205
+ autocomplete="off"
206
+ value={ctx.data.confirmPassword}
207
+ onValueChange={(v) => ctx.setData("confirmPassword", v)}
208
+ placeholder="비밀번호 변경 시에만 입력"
209
+ />
210
+ </td>
211
+ </tr>
212
+ </FormTable>
213
+ </section>
214
+
215
+ <section>
216
+ <h3 class="mb-4 border-l-4 border-base-500 pl-3 font-bold text-base-500">시스템설정</h3>
217
+ <FormTable>
218
+ <tbody>
219
+ <tr>
220
+ <th>로그인 시 화면</th>
221
+ <td>
222
+ <Select
223
+ class="w-full"
224
+ value={ctx.data.firstRouterLink}
225
+ onValueChange={(v) => ctx.setData("firstRouterLink", v)}
226
+ items={routeOptions()}
227
+ renderValue={(v) => <>{routeMap().get(v) ?? v}</>}
228
+ placeholder="선택 안함"
229
+ >
230
+ <Select.ItemTemplate>
231
+ {(href: string) => <>{routeMap().get(href) ?? href}</>}
232
+ </Select.ItemTemplate>
233
+ <Select.Action
234
+ onClick={() => ctx.setData("firstRouterLink", undefined as any)}
235
+ >
236
+ <Icon icon={IconX} class={"text-danger-500"} />
237
+ </Select.Action>
238
+ </Select>
239
+ </td>
240
+ </tr>
241
+ </tbody>
242
+ </FormTable>
243
+ </section>
244
+ </div>
245
+ )}
246
+ </CrudDetail>
247
+ );
248
+ }