@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.
- package/dist/commands/init.d.ts +4 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/sd-cli-entry.d.ts.map +1 -1
- package/dist/sd-cli-entry.js +0 -20
- package/dist/sd-cli-entry.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/init.ts +40 -21
- package/src/sd-cli-entry.ts +0 -24
- package/templates/init/{.prettierrc.yaml.hbs → .prettierrc.yaml} +1 -1
- package/templates/init/eslint.config.ts +15 -0
- package/templates/init/mise.toml +3 -0
- package/templates/init/package.json.hbs +8 -7
- package/templates/init/packages/client-admin/index.html.hbs +144 -0
- package/templates/init/packages/client-admin/package.json.hbs +26 -0
- package/templates/init/packages/client-admin/public/assets/logo-landscape.png +0 -0
- package/templates/init/packages/client-admin/public/assets/logo.png +0 -0
- package/templates/init/packages/client-admin/src/App.tsx +42 -0
- package/templates/init/packages/client-admin/src/dev/DevDialog.tsx +34 -0
- package/templates/{add-client/__CLIENT__/src/main.css.hbs → init/packages/client-admin/src/main.css} +1 -1
- package/templates/init/packages/client-admin/src/main.tsx.hbs +146 -0
- package/templates/init/packages/client-admin/src/providers/AppServiceProvider.tsx.hbs +103 -0
- package/templates/init/packages/client-admin/src/providers/AppStructureProvider.tsx +84 -0
- package/templates/init/packages/client-admin/src/providers/AuthProvider.tsx.hbs +71 -0
- package/templates/init/packages/client-admin/src/providers/configureSharedData.ts.hbs +67 -0
- package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +132 -0
- package/templates/init/packages/client-admin/src/views/home/HomeView.tsx +108 -0
- package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeDetail.tsx.hbs +262 -0
- package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +271 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleDetail.tsx.hbs +154 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionDetail.tsx.hbs +123 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionView.tsx +52 -0
- package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +125 -0
- package/templates/init/packages/client-admin/src/views/home/main/MainView.tsx.hbs +13 -0
- package/templates/init/packages/client-admin/src/views/home/my-info/MyInfoDetail.tsx.hbs +248 -0
- package/templates/init/packages/client-admin/src/views/home/system/system-log/SystemLogSheet.tsx.hbs +169 -0
- package/templates/init/packages/client-admin/src/views/not-found/NotFoundView.tsx +15 -0
- package/templates/init/packages/client-admin/tailwind.config.ts +10 -0
- package/templates/init/packages/db-main/package.json.hbs +13 -0
- package/templates/init/packages/db-main/src/MainDbContext.ts +20 -0
- package/templates/init/packages/db-main/src/dataLogExt.ts +127 -0
- package/templates/init/packages/db-main/src/index.ts +10 -0
- package/templates/init/packages/db-main/src/tables/Employee.ts +24 -0
- package/templates/init/packages/db-main/src/tables/EmployeeConfig.ts +13 -0
- package/templates/init/packages/db-main/src/tables/Role.ts +9 -0
- package/templates/init/packages/db-main/src/tables/RolePermission.ts +13 -0
- package/templates/init/packages/db-main/src/tables/_DataLog.ts +19 -0
- package/templates/init/packages/db-main/src/tables/_Log.ts +16 -0
- package/templates/init/packages/server/package.json.hbs +20 -0
- package/templates/init/packages/server/public-dev/dev//354/264/210/352/270/260/355/231/224.xlsx +0 -0
- package/templates/init/packages/server/src/index.ts +4 -0
- package/templates/init/packages/server/src/main.ts.hbs +34 -0
- package/templates/init/packages/server/src/services/AuthService.ts.hbs +171 -0
- package/templates/init/packages/server/src/services/DevService.ts.hbs +94 -0
- package/templates/init/packages/server/src/services/EmployeeService.ts.hbs +122 -0
- package/templates/init/packages/server/src/services/RoleService.ts.hbs +59 -0
- package/templates/init/{pnpm-workspace.yaml.hbs → pnpm-workspace.yaml} +3 -1
- package/templates/init/sd.config.ts.hbs +30 -1
- package/templates/init/tests/e2e/package.json.hbs +16 -0
- package/templates/init/tests/e2e/src/e2e.spec.ts +36 -0
- package/templates/init/tests/e2e/src/employee-crud.ts +204 -0
- package/templates/init/tests/e2e/src/login.ts +61 -0
- package/templates/init/tests/e2e/vitest.setup.ts.hbs +220 -0
- package/templates/init/tsconfig.json.hbs +0 -11
- package/templates/init/{vitest.config.ts.hbs → vitest.config.ts} +16 -12
- package/dist/commands/add-client.d.ts +0 -18
- package/dist/commands/add-client.d.ts.map +0 -1
- package/dist/commands/add-client.js +0 -79
- package/dist/commands/add-client.js.map +0 -6
- package/dist/commands/add-server.d.ts +0 -18
- package/dist/commands/add-server.d.ts.map +0 -1
- package/dist/commands/add-server.js +0 -83
- package/dist/commands/add-server.js.map +0 -6
- package/dist/utils/config-editor.d.ts +0 -17
- package/dist/utils/config-editor.d.ts.map +0 -1
- package/dist/utils/config-editor.js +0 -79
- package/dist/utils/config-editor.js.map +0 -6
- package/src/commands/add-client.ts +0 -126
- package/src/commands/add-server.ts +0 -138
- package/src/utils/config-editor.ts +0 -141
- package/templates/add-client/__CLIENT__/index.html.hbs +0 -13
- package/templates/add-client/__CLIENT__/package.json.hbs +0 -16
- package/templates/add-client/__CLIENT__/src/App.tsx.hbs +0 -65
- package/templates/add-client/__CLIENT__/src/appStructure.ts.hbs +0 -20
- package/templates/add-client/__CLIENT__/src/main.tsx.hbs +0 -24
- package/templates/add-client/__CLIENT__/src/pages/HomePage.tsx.hbs +0 -9
- package/templates/add-client/__CLIENT__/tailwind.config.ts.hbs +0 -15
- package/templates/add-server/__SERVER__/package.json.hbs +0 -10
- package/templates/add-server/__SERVER__/src/main.ts.hbs +0 -14
- package/templates/init/.gitignore.hbs +0 -26
- package/templates/init/.npmrc.hbs +0 -1
- package/templates/init/eslint.config.ts.hbs +0 -5
- package/templates/init/mise.toml.hbs +0 -3
- package/tests/config-editor.spec.ts +0 -160
- /package/templates/init/{.prettierignore.hbs → .prettierignore} +0 -0
- /package/templates/{add-client/__CLIENT__ → init/packages/client-admin}/public/favicon.ico +0 -0
- /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
|
+
}
|
package/templates/init/packages/server/public-dev/dev//354/264/210/352/270/260/355/231/224.xlsx
ADDED
|
Binary file
|
|
@@ -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>;
|
|
@@ -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
|
+
});
|