@simplysm/sd-cli 13.0.70 → 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 (103) 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/tests/infra/WorkerManager.spec.ts +1 -1
  68. package/tests/replace-deps.spec.ts +2 -2
  69. package/tests/run-lint.spec.ts +6 -6
  70. package/tests/run-typecheck.spec.ts +3 -3
  71. package/tests/sd-cli.spec.ts +1 -1
  72. package/dist/commands/add-client.d.ts +0 -18
  73. package/dist/commands/add-client.d.ts.map +0 -1
  74. package/dist/commands/add-client.js +0 -79
  75. package/dist/commands/add-client.js.map +0 -6
  76. package/dist/commands/add-server.d.ts +0 -18
  77. package/dist/commands/add-server.d.ts.map +0 -1
  78. package/dist/commands/add-server.js +0 -83
  79. package/dist/commands/add-server.js.map +0 -6
  80. package/dist/utils/config-editor.d.ts +0 -17
  81. package/dist/utils/config-editor.d.ts.map +0 -1
  82. package/dist/utils/config-editor.js +0 -79
  83. package/dist/utils/config-editor.js.map +0 -6
  84. package/src/commands/add-client.ts +0 -126
  85. package/src/commands/add-server.ts +0 -138
  86. package/src/utils/config-editor.ts +0 -141
  87. package/templates/add-client/__CLIENT__/index.html.hbs +0 -13
  88. package/templates/add-client/__CLIENT__/package.json.hbs +0 -16
  89. package/templates/add-client/__CLIENT__/src/App.tsx.hbs +0 -65
  90. package/templates/add-client/__CLIENT__/src/appStructure.ts.hbs +0 -20
  91. package/templates/add-client/__CLIENT__/src/main.tsx.hbs +0 -24
  92. package/templates/add-client/__CLIENT__/src/pages/HomePage.tsx.hbs +0 -9
  93. package/templates/add-client/__CLIENT__/tailwind.config.ts.hbs +0 -15
  94. package/templates/add-server/__SERVER__/package.json.hbs +0 -10
  95. package/templates/add-server/__SERVER__/src/main.ts.hbs +0 -14
  96. package/templates/init/.gitignore.hbs +0 -26
  97. package/templates/init/.npmrc.hbs +0 -1
  98. package/templates/init/eslint.config.ts.hbs +0 -5
  99. package/templates/init/mise.toml.hbs +0 -3
  100. package/tests/config-editor.spec.ts +0 -160
  101. /package/templates/init/{.prettierignore.hbs → .prettierignore} +0 -0
  102. /package/templates/{add-client/__CLIENT__ → init/packages/client-admin}/public/favicon.ico +0 -0
  103. /package/templates/init/{stylelint.config.ts.hbs → stylelint.config.ts} +0 -0
@@ -0,0 +1,19 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ import { Employee } from "./Employee";
3
+
4
+ export const _DataLog = Table("_DataLog")
5
+ .columns((c) => ({
6
+ id: c.bigint().autoIncrement(),
7
+ tableName: c.varchar(200),
8
+ tableDescription: c.varchar(200).nullable(),
9
+ action: c.varchar(50),
10
+ itemId: c.bigint().nullable(),
11
+ valueJson: c.text().nullable(),
12
+ dateTime: c.datetime(),
13
+ employeeId: c.bigint().nullable(),
14
+ }))
15
+ .primaryKey("id")
16
+ .indexes((i) => [i.index("tableName", "itemId"), i.index("dateTime").orderBy("DESC")])
17
+ .relations((r) => ({
18
+ employee: r.foreignKey(["employeeId"], () => Employee),
19
+ }));
@@ -0,0 +1,16 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ import { Employee } from "./Employee";
3
+
4
+ export const _Log = Table("_Log")
5
+ .columns((c) => ({
6
+ id: c.bigint().autoIncrement(),
7
+ clientName: c.varchar(200),
8
+ dateTime: c.datetime(),
9
+ severity: c.varchar(50),
10
+ message: c.text(),
11
+ employeeId: c.bigint().nullable(),
12
+ }))
13
+ .primaryKey("id")
14
+ .relations((r) => ({
15
+ employee: r.foreignKey(["employeeId"], () => Employee),
16
+ }));
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@{{projectName}}/server",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "dependencies": {
7
+ "@{{projectName}}/db-main": "workspace:*",
8
+ "@simplysm/core-common": "~13.0.72",
9
+ "@simplysm/excel": "^13.0.71",
10
+ "@simplysm/orm-common": "~13.0.72",
11
+ "@simplysm/orm-node": "~13.0.72",
12
+ "@simplysm/service-server": "~13.0.72",
13
+ "bcrypt": "^6.0.0",
14
+ "pg": "^8.19.0",
15
+ "pg-copy-streams": "^7.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bcrypt": "^6.0.0"
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ export type { AuthServiceMethods } from "./services/AuthService";
2
+ export type { IAuthResult, IAuthData } from "./services/AuthService";
3
+ export type { DevServiceMethods } from "./services/DevService";
4
+ export type { EmployeeServiceMethods } from "./services/EmployeeService";
@@ -0,0 +1,34 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { env } from "@simplysm/core-common";
4
+ import { createServiceServer, getConfig, OrmService } from "@simplysm/service-server";
5
+ import { AuthService } from "./services/AuthService";
6
+ import { DevService } from "./services/DevService";
7
+ import { EmployeeService } from "./services/EmployeeService";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const config = await getConfig<Record<string, { jwtSecret: string }>>(
12
+ path.resolve(__dirname, ".config.json"),
13
+ );
14
+
15
+ if (config?.["auth"]?.jwtSecret == null || config["auth"].jwtSecret === "") {
16
+ throw new Error(
17
+ "Missing 'auth.jwtSecret' in .config.json. Server cannot start without JWT secret.",
18
+ );
19
+ }
20
+
21
+ export const server = createServiceServer({
22
+ rootPath: __dirname,
23
+ port: {{port}},
24
+ auth: config["auth"],
25
+ services: env.DEV
26
+ ? [OrmService, AuthService, DevService, EmployeeService]
27
+ : [OrmService, AuthService, EmployeeService],
28
+ });
29
+
30
+ // 프로덕션 모드: 정적 파일 서빙 포함하여 직접 listen
31
+ // Watch 모드 (env.DEV): Server Runtime Worker가 proxy 설정 후 listen 호출
32
+ if (!env.DEV) {
33
+ await server.listen();
34
+ }
@@ -0,0 +1,171 @@
1
+ import { auth, defineService, type ServiceMethods } from "@simplysm/service-server";
2
+ import { createOrm, type DbConnConfig } from "@simplysm/orm-node";
3
+ import { expr } from "@simplysm/orm-common";
4
+ import { MainDbContext, type MainDbContext as MainDbContextType } from "@{{projectName}}/db-main";
5
+ import bcrypt from "bcrypt";
6
+
7
+ export interface IAuthData {
8
+ employeeId: number;
9
+ employeeName: string;
10
+ email: string;
11
+ permissions: Record<string, boolean>;
12
+ }
13
+
14
+ export interface IAuthResult extends IAuthData {
15
+ token: string;
16
+ }
17
+
18
+ export const AuthService = defineService("AuthService", (ctx) => {
19
+ async function getOrm() {
20
+ const ormConfig = await ctx.getConfig<{ default: DbConnConfig }>("orm");
21
+
22
+ return createOrm(MainDbContext, ormConfig.default);
23
+ }
24
+
25
+ async function getPermissions(
26
+ db: MainDbContextType,
27
+ roleId: number | undefined,
28
+ ): Promise<Record<string, boolean>> {
29
+ const permissions: Record<string, boolean> = {};
30
+
31
+ if (roleId != null) {
32
+ const rolePermissions = await db
33
+ .rolePermission()
34
+ .where((c) => [expr.eq(c.roleId, roleId)])
35
+ .result();
36
+
37
+ for (const rp of rolePermissions) {
38
+ permissions[rp.code] = JSON.parse(rp.valueJson);
39
+ }
40
+ }
41
+
42
+ return permissions;
43
+ }
44
+
45
+ async function buildAuthResult(
46
+ employee: { id: number; name: string; email: string | undefined },
47
+ permissions: Record<string, boolean>,
48
+ ): Promise<IAuthResult> {
49
+ const authData: IAuthData = {
50
+ employeeId: employee.id,
51
+ employeeName: employee.name,
52
+ email: employee.email ?? "",
53
+ permissions,
54
+ };
55
+
56
+ const token = await ctx.server.generateAuthToken({
57
+ roles: [],
58
+ data: authData,
59
+ });
60
+
61
+ return { token, ...authData };
62
+ }
63
+
64
+ function validateLoginInput(email: string, password: string): void {
65
+ if (email.length === 0 || email.length > 200) {
66
+ throw new Error("이메일은 1~200자의 문자열이어야 합니다.");
67
+ }
68
+ if (password.length === 0 || password.length > 500) {
69
+ throw new Error("비밀번호는 1~500자의 문자열이어야 합니다.");
70
+ }
71
+ }
72
+
73
+ return {
74
+ login: async (email: string, password: string): Promise<IAuthResult> => {
75
+ const orm = await getOrm();
76
+ validateLoginInput(email, password);
77
+
78
+ return orm.connect(async (db) => {
79
+ const employee = await db
80
+ .employee()
81
+ .where((c) => [expr.eq(c.email, email), expr.eq(c.isDeleted, false)])
82
+ .single();
83
+
84
+ if (!employee) {
85
+ throw new Error("이메일 또는 비밀번호가 올바르지 않습니다.");
86
+ }
87
+
88
+ if (employee.encryptedPassword == null || employee.encryptedPassword === "") {
89
+ throw new Error("비밀번호가 설정되지 않은 계정입니다.");
90
+ }
91
+
92
+ const isValid = await bcrypt.compare(password, employee.encryptedPassword);
93
+ if (!isValid) {
94
+ throw new Error("이메일 또는 비밀번호가 올바르지 않습니다.");
95
+ }
96
+
97
+ const permissions = await getPermissions(db, employee.roleId);
98
+
99
+ return buildAuthResult(employee, permissions);
100
+ });
101
+ },
102
+
103
+ refresh: auth(async (): Promise<IAuthResult> => {
104
+ const currentAuth = ctx.authInfo as IAuthData | undefined;
105
+ if (!currentAuth) {
106
+ throw new Error("인증 정보가 없습니다.");
107
+ }
108
+
109
+ const orm = await getOrm();
110
+
111
+ return orm.connect(async (db) => {
112
+ const employee = await db
113
+ .employee()
114
+ .where((c) => [expr.eq(c.id, currentAuth.employeeId), expr.eq(c.isDeleted, false)])
115
+ .single();
116
+
117
+ if (!employee) {
118
+ throw new Error("유효하지 않은 직원입니다.");
119
+ }
120
+
121
+ const permissions = await getPermissions(db, employee.roleId);
122
+
123
+ return buildAuthResult(employee, permissions);
124
+ });
125
+ }),
126
+
127
+ changePassword: auth(async (currentPassword: string, newPassword: string): Promise<void> => {
128
+ const currentAuth = ctx.authInfo as IAuthData | undefined;
129
+ if (!currentAuth) {
130
+ throw new Error("인증 정보가 없습니다.");
131
+ }
132
+
133
+ validateLoginInput(currentAuth.email, currentPassword);
134
+ validateLoginInput(currentAuth.email, newPassword);
135
+
136
+ if (newPassword.length < 8) {
137
+ throw new Error("새 비밀번호는 최소 8자 이상이어야 합니다.");
138
+ }
139
+
140
+ const orm = await getOrm();
141
+
142
+ await orm.connect(async (db) => {
143
+ const employee = await db
144
+ .employee()
145
+ .where((c) => [expr.eq(c.id, currentAuth.employeeId), expr.eq(c.isDeleted, false)])
146
+ .single();
147
+
148
+ if (!employee) {
149
+ throw new Error("유효하지 않은 직원입니다.");
150
+ }
151
+
152
+ if (employee.encryptedPassword == null || employee.encryptedPassword === "") {
153
+ throw new Error("비밀번호가 설정되지 않은 계정입니다.");
154
+ }
155
+
156
+ const isCurrentValid = await bcrypt.compare(currentPassword, employee.encryptedPassword);
157
+ if (!isCurrentValid) {
158
+ throw new Error("현재 비밀번호가 올바르지 않습니다.");
159
+ }
160
+
161
+ const encryptedPassword = await bcrypt.hash(newPassword, 10);
162
+ await db
163
+ .employee()
164
+ .where((c) => [expr.eq(c.id, employee.id)])
165
+ .update(() => ({ encryptedPassword: expr.val("string", encryptedPassword) }));
166
+ });
167
+ }),
168
+ };
169
+ });
170
+
171
+ export type AuthServiceMethods = ServiceMethods<typeof AuthService>;
@@ -0,0 +1,94 @@
1
+ import { defineService, type ServiceMethods } from "@simplysm/service-server";
2
+ import { createOrm, type DbConnConfig } from "@simplysm/orm-node";
3
+ import { MainDbContext } from "@{{projectName}}/db-main";
4
+ import { ExcelWorkbook } from "@simplysm/excel";
5
+ import bcrypt from "bcrypt";
6
+ import fs from "fs";
7
+ import path from "path";
8
+
9
+ export const DevService = defineService("DevService", (ctx) => {
10
+ async function getOrm() {
11
+ const ormConfig = await ctx.getConfig<{ default: DbConnConfig }>("orm");
12
+ return createOrm(MainDbContext, ormConfig.default);
13
+ }
14
+
15
+ return {
16
+ initDb: async (permCodes: string[]) => {
17
+ const orm = await getOrm();
18
+
19
+ await orm.connectWithoutTransaction(async (db) => {
20
+ await db.initialize({ force: true });
21
+
22
+ const excelPath = path.resolve(ctx.server.options.rootPath, "dev/초기화.xlsx");
23
+ if (!fs.existsSync(excelPath)) {
24
+ throw new Error(`초기화 파일을 찾을 수 없습니다: ${excelPath}`);
25
+ }
26
+
27
+ const buffer = fs.readFileSync(excelPath);
28
+ const wb = new ExcelWorkbook(buffer);
29
+ const wsNames = await wb.getWorksheetNames();
30
+
31
+ // Role
32
+ if (wsNames.includes("권한그룹")) {
33
+ const ws = await wb.getWorksheet("권한그룹");
34
+ const rows = await ws.getDataTable();
35
+ const filtered = rows.filter((r: Record<string, any>) => r["ID"] != null);
36
+ if (filtered.length > 0) {
37
+ await db.role().insert(
38
+ filtered.map((r: Record<string, any>) => ({
39
+ id: Number(r["ID"]),
40
+ name: String(r["명칭"]),
41
+ })),
42
+ );
43
+ }
44
+ }
45
+
46
+ // RolePermission
47
+ if (wsNames.includes("권한그룹권한")) {
48
+ const ws = await wb.getWorksheet("권한그룹권한");
49
+ const rows = await ws.getDataTable();
50
+ const filtered = rows.filter((r: Record<string, any>) => r["권한그룹.ID"] != null);
51
+ if (filtered.length > 0) {
52
+ const permRows = filtered.flatMap((r: Record<string, any>) => {
53
+ const roleId = Number(r["권한그룹.ID"]);
54
+ const code = String(r["코드"]);
55
+ const valueJson = String(r["값"]);
56
+
57
+ if (code === "ALL") {
58
+ return permCodes.map((pc) => ({ roleId, code: pc, valueJson }));
59
+ }
60
+ return [{ roleId, code, valueJson }];
61
+ });
62
+
63
+ await db.rolePermission().insert(permRows);
64
+ }
65
+ }
66
+
67
+ // Employee (bcrypt hash passwords)
68
+ if (wsNames.includes("직원")) {
69
+ const ws = await wb.getWorksheet("직원");
70
+ const rows = await ws.getDataTable();
71
+ const filtered = rows.filter((r: Record<string, any>) => r["ID"] != null);
72
+ if (filtered.length > 0) {
73
+ const employees = [];
74
+ for (const r of filtered) {
75
+ const pw = r["비밀번호"];
76
+ employees.push({
77
+ id: Number(r["ID"]),
78
+ name: String(r["이름"]),
79
+ email: r["이메일"] != null ? String(r["이메일"]) : undefined,
80
+ encryptedPassword:
81
+ pw != null && String(pw) !== "" ? await bcrypt.hash(String(pw), 10) : undefined,
82
+ roleId: r["권한그룹.ID"] != null ? Number(r["권한그룹.ID"]) : undefined,
83
+ isDeleted: Boolean(r["삭제"]),
84
+ });
85
+ }
86
+ await db.employee().insert(employees);
87
+ }
88
+ }
89
+ });
90
+ },
91
+ };
92
+ });
93
+
94
+ export type DevServiceMethods = ServiceMethods<typeof DevService>;
@@ -0,0 +1,122 @@
1
+ import { auth, defineService, type ServiceMethods } from "@simplysm/service-server";
2
+ import { createOrm, type DbConnConfig } from "@simplysm/orm-node";
3
+ import { expr } from "@simplysm/orm-common";
4
+ import { MainDbContext } from "@{{projectName}}/db-main";
5
+ import { DateOnly, strIsNullOrEmpty } from "@simplysm/core-common";
6
+ import bcrypt from "bcrypt";
7
+ import type { IAuthData } from "./AuthService";
8
+
9
+ export const EmployeeService = defineService("EmployeeService", (ctx) => {
10
+ async function getOrm() {
11
+ const ormConfig = await ctx.getConfig<{ default: DbConnConfig }>("orm");
12
+ return createOrm(MainDbContext, ormConfig.default);
13
+ }
14
+
15
+ return {
16
+ save: auth(
17
+ async (
18
+ items: {
19
+ id?: number;
20
+ name?: string;
21
+ email?: string;
22
+ phoneNumber?: string;
23
+ birthDate?: DateOnly;
24
+ enteringDate?: DateOnly;
25
+ leavingDate?: DateOnly;
26
+ socialSecurityNumber?: string;
27
+ payrollAccountBank?: string;
28
+ payrollAccountNumber?: string;
29
+ password?: string;
30
+ isDeleted: boolean;
31
+ }[],
32
+ ): Promise<number[]> => {
33
+ const authData = ctx.authInfo as IAuthData | undefined;
34
+ if (authData?.permissions["/home/base/employee/edit"] !== true) {
35
+ throw new Error("저장 권한이 없습니다.");
36
+ }
37
+
38
+ const ids: number[] = [];
39
+ const orm = await getOrm();
40
+ await orm.connect(async (db) => {
41
+ for (const item of items) {
42
+ if (
43
+ !item.isDeleted &&
44
+ !strIsNullOrEmpty(item.name) &&
45
+ (await db
46
+ .employee()
47
+ .where((c) => [
48
+ expr.not(expr.eq(c.id, item.id)),
49
+ expr.eq(c.isDeleted, false),
50
+ expr.eq(c.name, item.name),
51
+ ])
52
+ .exists())
53
+ ) {
54
+ throw new Error(`동일한 이름이 이미 등록되어 있습니다: ${item.name}`);
55
+ }
56
+
57
+ if (
58
+ !item.isDeleted &&
59
+ !strIsNullOrEmpty(item.email) &&
60
+ (await db
61
+ .employee()
62
+ .where((c) => [
63
+ expr.not(expr.eq(c.id, item.id)),
64
+ expr.eq(c.isDeleted, false),
65
+ expr.eq(c.email, item.email),
66
+ ])
67
+ .exists())
68
+ ) {
69
+ throw new Error(`동일한 이메일이 이미 등록되어 있습니다: ${item.email}`);
70
+ }
71
+
72
+ const passwordObj =
73
+ authData.permissions["/home/base/employee/auth/edit"] &&
74
+ !strIsNullOrEmpty(item.password)
75
+ ? { encryptedPassword: await bcrypt.hash(item.password, 10) }
76
+ : {};
77
+
78
+ const upsertedId = (
79
+ await db
80
+ .employee()
81
+ .where((c) => [expr.eq(c.id, item.id)])
82
+ .upsert(
83
+ () => ({
84
+ name: item.name,
85
+ email: item.email,
86
+ phoneNumber: item.phoneNumber,
87
+ birthDate: item.birthDate,
88
+ enteringDate: item.enteringDate,
89
+ leavingDate: item.leavingDate,
90
+ isDeleted: item.isDeleted,
91
+ ...(authData.permissions["/home/base/employee/personal/edit"]
92
+ ? { socialSecurityNumber: item.socialSecurityNumber }
93
+ : {}),
94
+ ...(authData.permissions["/home/base/employee/payroll/edit"]
95
+ ? {
96
+ payrollAccountBank: item.payrollAccountBank,
97
+ payrollAccountNumber: item.payrollAccountNumber,
98
+ }
99
+ : {}),
100
+ ...passwordObj,
101
+ }),
102
+ ["id"],
103
+ )
104
+ ).single()?.id;
105
+
106
+ if (upsertedId != null) {
107
+ ids.push(upsertedId);
108
+ await db.employee().insertDataLog({
109
+ action: item.id == null ? "등록" : "수정",
110
+ itemId: upsertedId,
111
+ employeeId: authData.employeeId,
112
+ });
113
+ }
114
+ }
115
+ });
116
+ return ids;
117
+ },
118
+ ),
119
+ };
120
+ });
121
+
122
+ export type EmployeeServiceMethods = ServiceMethods<typeof EmployeeService>;
@@ -0,0 +1,59 @@
1
+ import { auth, defineService, type ServiceMethods } from "@simplysm/service-server";
2
+ import { createOrm, type DbConnConfig } from "@simplysm/orm-node";
3
+ import { expr } from "@simplysm/orm-common";
4
+ import { MainDbContext } from "@{{projectName}}/db-main";
5
+ import { strIsNullOrEmpty } from "@simplysm/core-common";
6
+ import type { IAuthData } from "./AuthService";
7
+
8
+ export const RoleService = defineService("RoleService", (ctx) => {
9
+ async function getOrm() {
10
+ const ormConfig = await ctx.getConfig<{ default: DbConnConfig }>("orm");
11
+ return createOrm(MainDbContext, ormConfig.default);
12
+ }
13
+
14
+ return {
15
+ save: auth(
16
+ async (
17
+ items: {
18
+ id?: number;
19
+ name?: string;
20
+ }[],
21
+ ): Promise<void> => {
22
+ const authData = ctx.authInfo as IAuthData | undefined;
23
+ if (authData?.permissions["/home/base/role-permission/edit"] !== true) {
24
+ throw new Error("저장 권한이 없습니다.");
25
+ }
26
+
27
+ const orm = await getOrm();
28
+ await orm.connect(async (db) => {
29
+ for (const item of items) {
30
+ if (
31
+ !strIsNullOrEmpty(item.name) &&
32
+ (await db
33
+ .role()
34
+ .where((c) => [
35
+ expr.not(expr.eq(c.id, item.id)),
36
+ expr.eq(c.name, item.name),
37
+ ])
38
+ .exists())
39
+ ) {
40
+ throw new Error(`이미 존재하는 권한그룹 이름입니다: ${item.name}`);
41
+ }
42
+
43
+ await db
44
+ .role()
45
+ .where((c) => [expr.eq(c.id, item.id)])
46
+ .upsert(
47
+ () => ({
48
+ name: item.name ?? "",
49
+ }),
50
+ ["id"],
51
+ );
52
+ }
53
+ });
54
+ },
55
+ ),
56
+ };
57
+ });
58
+
59
+ export type RoleServiceMethods = ServiceMethods<typeof RoleService>;
@@ -3,11 +3,13 @@ packages:
3
3
  - tests/*
4
4
 
5
5
  onlyBuiltDependencies:
6
- - '@simplysm/sd-claude'
6
+ - "@simplysm/sd-claude"
7
+ - bcrypt
7
8
  - bufferutil
8
9
  - core-js
9
10
  - cpu-features
10
11
  - esbuild
11
12
  - sharp
12
13
  - ssh2
14
+ - unrs-resolver
13
15
  - utf-8-validate
@@ -1,7 +1,36 @@
1
1
  import type { SdConfigFn } from "@simplysm/sd-cli";
2
2
 
3
+ const dbPort = Number(process.env["DB_PORT"] ?? 5432);
4
+ const dbDatabase = process.env["DB_DATABASE"] ?? "{{projectName}}";
5
+
3
6
  const config: SdConfigFn = () => ({
4
- packages: {},
7
+ packages: {
8
+ "db-main": { target: "neutral" },
9
+ "client-admin": {
10
+ target: "client",
11
+ server: "server",
12
+ },
13
+ "server": {
14
+ target: "server",
15
+ packageManager: "volta",
16
+ pm2: {},
17
+ configs: {
18
+ orm: {
19
+ default: {
20
+ dialect: "postgresql",
21
+ host: "localhost",
22
+ port: dbPort,
23
+ username: "postgres",
24
+ password: "1234",
25
+ database: dbDatabase,
26
+ },
27
+ },
28
+ auth: {
29
+ jwtSecret: "{{jwtSecret}}",
30
+ },
31
+ },
32
+ },
33
+ },
5
34
  });
6
35
 
7
36
  export default config;
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@{{projectName}}-test/e2e",
3
+ "version": "1.0.0",
4
+ "description": "{{projectName}} E2E tests",
5
+ "type": "module",
6
+ "private": true,
7
+ "dependencies": {
8
+ "@{{projectName}}/db-main": "workspace:*",
9
+ "@simplysm/orm-node": "~13.0.72",
10
+ "bcrypt": "^6.0.0",
11
+ "playwright": "^1.58.2"
12
+ },
13
+ "devDependencies": {
14
+ "@types/bcrypt": "^6.0.0"
15
+ }
16
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, inject, beforeAll, afterAll } from "vitest";
2
+ import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
3
+ import { loginTests } from "./login";
4
+ import { employeeCrudTests } from "./employee-crud";
5
+
6
+ const ctx = {} as { page: Page; baseUrl: string };
7
+
8
+ let browser: Browser;
9
+ let context: BrowserContext;
10
+
11
+ describe("E2E", () => {
12
+ beforeAll(async () => {
13
+ ctx.baseUrl = inject("baseUrl");
14
+ browser = await chromium.launch({ headless: true });
15
+ context = await browser.newContext();
16
+ ctx.page = await context.newPage();
17
+ ctx.page.setDefaultTimeout(500);
18
+
19
+ // 브라우저 콘솔 에러 → 테스트 콘솔에 출력
20
+ ctx.page.on("pageerror", (err) => {
21
+ console.error(`[PAGE_ERROR] ${err.message}`);
22
+ });
23
+ ctx.page.on("console", (msg) => {
24
+ if (msg.type() === "error") {
25
+ console.error(`[CONSOLE_ERROR] ${msg.text()}`);
26
+ }
27
+ });
28
+ });
29
+
30
+ afterAll(async () => {
31
+ await browser.close();
32
+ });
33
+
34
+ loginTests(ctx);
35
+ employeeCrudTests(ctx);
36
+ });