@simplysm/sd-cli 14.0.89 → 14.0.90

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 (82) hide show
  1. package/dist/commands/init/generators/client-common.d.ts.map +1 -1
  2. package/dist/commands/init/generators/client-common.js +6 -2
  3. package/dist/commands/init/generators/client-common.js.map +1 -1
  4. package/dist/commands/init/generators/client.d.ts.map +1 -1
  5. package/dist/commands/init/generators/client.js +13 -2
  6. package/dist/commands/init/generators/client.js.map +1 -1
  7. package/dist/commands/init/generators/common.d.ts.map +1 -1
  8. package/dist/commands/init/generators/common.js +16 -1
  9. package/dist/commands/init/generators/common.js.map +1 -1
  10. package/dist/commands/init/generators/server.d.ts.map +1 -1
  11. package/dist/commands/init/generators/server.js +9 -0
  12. package/dist/commands/init/generators/server.js.map +1 -1
  13. package/dist/commands/init/init.d.ts.map +1 -1
  14. package/dist/commands/init/init.js +11 -2
  15. package/dist/commands/init/init.js.map +1 -1
  16. package/dist/commands/init/normalize.d.ts.map +1 -1
  17. package/dist/commands/init/normalize.js +40 -4
  18. package/dist/commands/init/normalize.js.map +1 -1
  19. package/dist/commands/init/prompts.d.ts +1 -1
  20. package/dist/commands/init/prompts.d.ts.map +1 -1
  21. package/dist/commands/init/prompts.js +34 -3
  22. package/dist/commands/init/prompts.js.map +1 -1
  23. package/dist/commands/init/types.d.ts +16 -1
  24. package/dist/commands/init/types.d.ts.map +1 -1
  25. package/dist/commands/init/validate.d.ts.map +1 -1
  26. package/dist/commands/init/validate.js +3 -0
  27. package/dist/commands/init/validate.js.map +1 -1
  28. package/package.json +4 -4
  29. package/src/commands/init/generators/client-common.ts +18 -5
  30. package/src/commands/init/generators/client.ts +41 -2
  31. package/src/commands/init/generators/common.ts +56 -2
  32. package/src/commands/init/generators/server.ts +30 -0
  33. package/src/commands/init/init.ts +12 -2
  34. package/src/commands/init/normalize.ts +49 -4
  35. package/src/commands/init/prompts.ts +34 -3
  36. package/src/commands/init/templates/client/login-public/assets/logo-landscape.png +0 -0
  37. package/src/commands/init/templates/client/login-public/assets/logo.png +0 -0
  38. package/src/commands/init/templates/client/package.json.hbs +3 -2
  39. package/src/commands/init/templates/client/src/app/home/home.view.ts.hbs +137 -0
  40. package/src/commands/init/templates/client/src/app/home/main/main.view.ts.hbs +16 -0
  41. package/src/commands/init/templates/client/src/app/home/my-info/my-info.detail.ts.hbs +265 -0
  42. package/src/commands/init/templates/client/src/app/login/login.view.ts.hbs +144 -0
  43. package/src/commands/init/templates/client/src/app.root.ts.hbs +64 -0
  44. package/src/commands/init/templates/client/src/index.html.hbs +75 -1
  45. package/src/commands/init/templates/client/src/main.ts.hbs +147 -7
  46. package/src/commands/init/templates/client/src/modals/dev.modal.ts.hbs +63 -0
  47. package/src/commands/init/templates/client/src/routes.ts.hbs +29 -0
  48. package/src/commands/init/templates/client-common/package.json.hbs +1 -0
  49. package/src/commands/init/templates/client-common/src/index.ts.hbs +6 -2
  50. package/src/commands/init/templates/client-common/src/providers/app-auth.provider.ts.hbs +90 -0
  51. package/src/commands/init/templates/client-common/src/providers/{AppOrmProvider.ts.hbs → app-orm.provider.ts.hbs} +5 -11
  52. package/src/commands/init/templates/client-common/src/providers/app-service.provider.ts.hbs +68 -0
  53. package/src/commands/init/templates/client-common/src/providers/app-shared-data.provider.ts.hbs +100 -0
  54. package/src/commands/init/templates/common/package.json.hbs +2 -1
  55. package/src/commands/init/templates/common/src/app-structure.ts.hbs +26 -0
  56. package/src/commands/init/templates/common/src/auth-info-changed.event.ts.hbs +3 -0
  57. package/src/commands/init/templates/common/src/db/db-context.ts.hbs +20 -0
  58. package/src/commands/init/templates/common/src/db/system-data-log.ext.ts.hbs +138 -0
  59. package/src/commands/init/templates/common/src/db/tables/master/user-config.ts.hbs +15 -0
  60. package/src/commands/init/templates/common/src/db/tables/master/user.ts.hbs +20 -0
  61. package/src/commands/init/templates/common/src/db/tables/system/role-permission.ts.hbs +16 -0
  62. package/src/commands/init/templates/common/src/db/tables/system/role.ts.hbs +16 -0
  63. package/src/commands/init/templates/common/src/db/tables/system/system-data-log.ts.hbs +23 -0
  64. package/src/commands/init/templates/common/src/db/tables/system/system-log.ts.hbs +21 -0
  65. package/src/commands/init/templates/common/src/index.ts.hbs +14 -1
  66. package/src/commands/init/templates/server/package.json.hbs +7 -3
  67. package/src/commands/init/templates/server/public-dev//354/264/210/352/270/260/355/231/224.xlsx +0 -0
  68. package/src/commands/init/templates/server/src/index.ts.hbs +4 -0
  69. package/src/commands/init/templates/server/src/main.ts.hbs +11 -1
  70. package/src/commands/init/templates/server/src/services/auth.service.ts.hbs +284 -0
  71. package/src/commands/init/templates/server/src/services/dev.service.ts.hbs +112 -0
  72. package/src/commands/init/templates/server/src/utils/orm.utils.ts.hbs +8 -0
  73. package/src/commands/init/templates/workspace-root/sd.config.ts.hbs +2 -2
  74. package/src/commands/init/types.ts +16 -1
  75. package/src/commands/init/validate.ts +6 -0
  76. package/tests/init/__snapshots__/render.spec.ts.snap +36 -9
  77. package/tests/init/normalize.spec.ts +95 -1
  78. package/tests/init/render.spec.ts +951 -10
  79. package/src/commands/init/templates/client/src/AppPage.ts.hbs +0 -18
  80. package/src/commands/init/templates/client/src/routes.ts +0 -3
  81. package/src/commands/init/templates/client-common/src/providers/AppServiceProvider.ts +0 -27
  82. package/src/commands/init/templates/common/src/DbContext.ts.hbs +0 -4
@@ -0,0 +1,138 @@
1
+ import { DateTime } from "@simplysm/core-common";
2
+ import { type DataRecord, expr, Queryable, queryable, type TableBuilder } from "@simplysm/orm-common";
3
+ import { SystemDataLog } from "./tables/system/system-data-log";
4
+
5
+ // ── Type Declarations ──
6
+
7
+ export type IDataLogJoinOptions = {
8
+ includeActions?: string[];
9
+ excludeActions?: string[];
10
+ };
11
+
12
+ export type IDataLogJoinResult = {
13
+ action?: string;
14
+ dateTime?: DateTime;
15
+ {{#if hasAuth}}
16
+ {{userEntityCamel}}Id?: number;
17
+ {{userEntityCamel}}Name?: string;
18
+ {{/if}}
19
+ };
20
+
21
+ export type IInsertDataLogParam = {
22
+ action: string;
23
+ itemId?: number;
24
+ valueJson?: string;
25
+ {{#if hasAuth}}
26
+ {{userEntityCamel}}Id?: number;
27
+ {{/if}}
28
+ };
29
+
30
+ declare module "@simplysm/orm-common" {
31
+ interface Queryable<TData extends DataRecord, TFrom extends TableBuilder<any, any, any> | never> {
32
+ joinLastDataLog(
33
+ opts?: IDataLogJoinOptions,
34
+ ): Queryable<TData & { lastDataLog?: IDataLogJoinResult }, TFrom>;
35
+
36
+ joinFirstDataLog(
37
+ opts?: IDataLogJoinOptions,
38
+ ): Queryable<TData & { firstDataLog?: IDataLogJoinResult }, TFrom>;
39
+
40
+ insertDataLog(log: IInsertDataLogParam): Promise<void>;
41
+ }
42
+ }
43
+
44
+ // ── Runtime: insertDataLog ──
45
+
46
+ Queryable.prototype.insertDataLog = async function (
47
+ this: Queryable<any, any>,
48
+ log: IInsertDataLogParam,
49
+ ): Promise<void> {
50
+ const fromTable = this.meta.from as TableBuilder<any, any, any>;
51
+ const qr = queryable(this.meta.db, SystemDataLog);
52
+
53
+ await qr().insert([
54
+ {
55
+ tableName: fromTable.meta.name,
56
+ tableDescription: fromTable.meta.description,
57
+ action: log.action,
58
+ itemId: log.itemId,
59
+ valueJson: log.valueJson,
60
+ dateTime: new DateTime(),
61
+ {{#if hasAuth}}
62
+ {{userEntityCamel}}Id: log.{{userEntityCamel}}Id,
63
+ {{/if}}
64
+ },
65
+ ]);
66
+ };
67
+
68
+ // ── Runtime: joinLastDataLog ──
69
+
70
+ Queryable.prototype.joinLastDataLog = function (
71
+ this: Queryable<any, any>,
72
+ opts?: IDataLogJoinOptions,
73
+ ) {
74
+ const tableName = (this.meta.from as TableBuilder<any, any, any>).meta.name;
75
+
76
+ return this.joinSingle("lastDataLog", (qr, en) =>
77
+ qr
78
+ .from(SystemDataLog)
79
+ .where((dl) => [
80
+ expr.eq(dl.tableName, tableName),
81
+ expr.eq(dl.itemId, en["id"]),
82
+ ...(opts?.includeActions ? [expr.in(dl.action, opts.includeActions)] : []),
83
+ ...(opts?.excludeActions ? [expr.not(expr.in(dl.action, opts.excludeActions))] : []),
84
+ ])
85
+ .orderBy((dl) => dl.dateTime, "DESC")
86
+ .top(1)
87
+ {{#if hasAuth}}
88
+ .include((dl) => dl.{{userEntityCamel}})
89
+ .select((dl) => ({
90
+ action: dl.action,
91
+ dateTime: dl.dateTime,
92
+ {{userEntityCamel}}Id: dl.{{userEntityCamel}}Id,
93
+ {{userEntityCamel}}Name: dl.{{userEntityCamel}}!.name,
94
+ })),
95
+ {{else}}
96
+ .select((dl) => ({
97
+ action: dl.action,
98
+ dateTime: dl.dateTime,
99
+ })),
100
+ {{/if}}
101
+ );
102
+ };
103
+
104
+ // ── Runtime: joinFirstDataLog ──
105
+
106
+ Queryable.prototype.joinFirstDataLog = function (
107
+ this: Queryable<any, any>,
108
+ opts?: IDataLogJoinOptions,
109
+ ) {
110
+ const tableName = (this.meta.from as TableBuilder<any, any, any>).meta.name;
111
+
112
+ return this.joinSingle("firstDataLog", (qr, en) =>
113
+ qr
114
+ .from(SystemDataLog)
115
+ .where((dl) => [
116
+ expr.eq(dl.tableName, tableName),
117
+ expr.eq(dl.itemId, en["id"]),
118
+ ...(opts?.includeActions ? [expr.in(dl.action, opts.includeActions)] : []),
119
+ ...(opts?.excludeActions ? [expr.not(expr.in(dl.action, opts.excludeActions))] : []),
120
+ ])
121
+ .orderBy((dl) => dl.dateTime, "ASC")
122
+ .top(1)
123
+ {{#if hasAuth}}
124
+ .include((dl) => dl.{{userEntityCamel}})
125
+ .select((dl) => ({
126
+ action: dl.action,
127
+ dateTime: dl.dateTime,
128
+ {{userEntityCamel}}Id: dl.{{userEntityCamel}}Id,
129
+ {{userEntityCamel}}Name: dl.{{userEntityCamel}}!.name,
130
+ })),
131
+ {{else}}
132
+ .select((dl) => ({
133
+ action: dl.action,
134
+ dateTime: dl.dateTime,
135
+ })),
136
+ {{/if}}
137
+ );
138
+ };
@@ -0,0 +1,15 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ import { {{userEntityPascal}} } from "./{{userEntityKebab}}";
3
+
4
+ export const {{userEntityPascal}}Config = Table("{{userEntityPascal}}Config")
5
+ .columns((c) => ({
6
+ id: c.bigint().autoIncrement(),
7
+ {{userEntityCamel}}Id: c.bigint(),
8
+ code: c.varchar(200),
9
+ valueJson: c.text(),
10
+ }))
11
+ .primaryKey("id")
12
+ .indexes((i) => [i.index("{{userEntityCamel}}Id", "code").unique()])
13
+ .relations((r) => ({
14
+ {{userEntityCamel}}: r.foreignKey(["{{userEntityCamel}}Id"], () => {{userEntityPascal}}),
15
+ }));
@@ -0,0 +1,20 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ import { Role } from "../system/role";
3
+ import { {{userEntityPascal}}Config } from "./{{userEntityKebab}}-config";
4
+
5
+ export const {{userEntityPascal}} = Table("{{userEntityPascal}}")
6
+ .description("{{userEntityLabel}}")
7
+ .columns((c) => ({
8
+ id: c.bigint().autoIncrement(),
9
+ name: c.varchar(100),
10
+ loginId: c.varchar(50),
11
+ encryptedPassword: c.varchar(200).nullable(),
12
+ roleId: c.bigint().nullable(),
13
+ isDeleted: c.boolean(),
14
+ }))
15
+ .primaryKey("id")
16
+ .indexes((i) => [i.index("loginId").unique()])
17
+ .relations((r) => ({
18
+ role: r.foreignKey(["roleId"], () => Role),
19
+ configs: r.foreignKeyTarget(() => {{userEntityPascal}}Config, "{{userEntityCamel}}"),
20
+ }));
@@ -0,0 +1,16 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ import { Role } from "./role";
3
+
4
+ export const RolePermission = Table("RolePermission")
5
+ .description("역할 권한")
6
+ .columns((c) => ({
7
+ id: c.bigint().autoIncrement(),
8
+ roleId: c.bigint(),
9
+ code: c.varchar(200),
10
+ valueJson: c.text(),
11
+ }))
12
+ .primaryKey("id")
13
+ .indexes((i) => [i.index("roleId", "code").unique()])
14
+ .relations((r) => ({
15
+ role: r.foreignKey(["roleId"], () => Role),
16
+ }));
@@ -0,0 +1,16 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ import { {{userEntityPascal}} } from "../master/{{userEntityKebab}}";
3
+ import { RolePermission } from "./role-permission";
4
+
5
+ export const Role = Table("Role")
6
+ .description("역할")
7
+ .columns((c) => ({
8
+ id: c.bigint().autoIncrement(),
9
+ name: c.varchar(100),
10
+ isDeleted: c.boolean(),
11
+ }))
12
+ .primaryKey("id")
13
+ .relations((r) => ({
14
+ {{userEntityCamel}}s: r.foreignKeyTarget(() => {{userEntityPascal}}, "role"),
15
+ permissions: r.foreignKeyTarget(() => RolePermission, "role"),
16
+ }));
@@ -0,0 +1,23 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ {{#if hasAuth}}
3
+ import { {{userEntityPascal}} } from "../master/{{userEntityKebab}}";
4
+ {{/if}}
5
+
6
+ export const SystemDataLog = Table("SystemDataLog")
7
+ .columns((c) => ({
8
+ id: c.bigint().autoIncrement(),
9
+ tableName: c.varchar(200),
10
+ tableDescription: c.varchar(200).nullable(),
11
+ action: c.varchar(50),
12
+ itemId: c.bigint().nullable(),
13
+ valueJson: c.text().nullable(),
14
+ dateTime: c.datetime(),
15
+ {{#if hasAuth}}
16
+ {{userEntityCamel}}Id: c.bigint().nullable(),
17
+ {{/if}}
18
+ }))
19
+ .primaryKey("id")
20
+ .indexes((i) => [i.index("tableName", "itemId"), i.index("dateTime").orderBy("DESC")]){{#if hasAuth}}
21
+ .relations((r) => ({
22
+ {{userEntityCamel}}: r.foreignKey(["{{userEntityCamel}}Id"], () => {{userEntityPascal}}),
23
+ })){{/if}};
@@ -0,0 +1,21 @@
1
+ import { Table } from "@simplysm/orm-common";
2
+ {{#if hasAuth}}
3
+ import { {{userEntityPascal}} } from "../master/{{userEntityKebab}}";
4
+ {{/if}}
5
+
6
+ export const SystemLog = Table("SystemLog")
7
+ .columns((c) => ({
8
+ id: c.bigint().autoIncrement(),
9
+ dateTime: c.datetime(),
10
+ severity: c.varchar(50),
11
+ message: c.text(),
12
+ clientName: c.varchar(200),
13
+ {{#if hasAuth}}
14
+ {{userEntityCamel}}Id: c.bigint().nullable(),
15
+ {{/if}}
16
+ }))
17
+ .primaryKey("id")
18
+ .indexes((i) => [i.index("dateTime").orderBy("DESC")]){{#if hasAuth}}
19
+ .relations((r) => ({
20
+ {{userEntityCamel}}: r.foreignKey(["{{userEntityCamel}}Id"], () => {{userEntityPascal}}),
21
+ })){{/if}};
@@ -1,5 +1,18 @@
1
1
  {{#if hasDb}}
2
- export * from "./{{dbContextClassName}}";
2
+ export * from "./{{dbFolderName}}/{{dbContextFileName}}";
3
+ {{#if hasAuth}}
4
+ export * from "./{{dbFolderName}}/tables/master/{{userEntityKebab}}";
5
+ export * from "./{{dbFolderName}}/tables/master/{{userEntityKebab}}-config";
6
+ export * from "./{{dbFolderName}}/tables/system/role";
7
+ export * from "./{{dbFolderName}}/tables/system/role-permission";
8
+ {{/if}}
9
+ export * from "./{{dbFolderName}}/tables/system/system-data-log";
10
+ export * from "./{{dbFolderName}}/tables/system/system-log";
11
+ export * from "./{{dbFolderName}}/system-data-log.ext";
12
+ {{#if hasAuth}}
13
+ export * from "./app-structure";
14
+ export * from "./auth-info-changed.event";
15
+ {{/if}}
3
16
  {{else}}
4
17
  export {};
5
18
  {{/if}}
@@ -14,10 +14,14 @@
14
14
  "@simplysm/orm-common": "^14.0.0",
15
15
  "@simplysm/orm-node": "^14.0.0"{{#if isMysql}},
16
16
  "mysql2": "^3.22.0"{{/if}}{{#if isPostgres}},
17
- "pg": "^8.13.0"{{/if}}{{#if isMssql}},
18
- "mssql": "^11.0.0"{{/if}}{{/if}}
17
+ "pg": "^8.13.0",
18
+ "pg-copy-streams": "^7.0.0"{{/if}}{{#if isMssql}},
19
+ "mssql": "^11.0.0"{{/if}}{{/if}}{{#if hasAuth}},
20
+ "@simplysm/excel": "^14.0.0",
21
+ "bcrypt": "^6.0.0"{{/if}}
19
22
  },
20
23
  "devDependencies": {
21
- "@types/node": "^20.19.0"
24
+ "@types/node": "^20.19.0"{{#if hasAuth}},
25
+ "@types/bcrypt": "^6.0.0"{{/if}}
22
26
  }
23
27
  }
@@ -0,0 +1,4 @@
1
+ {{#if hasAuth}}
2
+ export type * from "./services/auth.service";
3
+ {{/if}}
4
+ export type * from "./services/dev.service";
@@ -2,6 +2,16 @@ import path from "node:path";
2
2
  import { env, num, parseBoolEnv } from "@simplysm/core-common";
3
3
  import { setupConsola } from "@simplysm/core-node";
4
4
  import { createServiceServer, getConfig{{#if hasDb}}, OrmService{{/if}} } from "@simplysm/service-server";
5
+ import { EventEmitter } from "node:events";
6
+ {{#if hasAuth}}
7
+ import { AuthService } from "./services/auth.service";
8
+ {{/if}}
9
+ {{#if hasDb}}
10
+ import { DevService } from "./services/dev.service";
11
+ {{/if}}
12
+
13
+ Error.stackTraceLimit = Infinity;
14
+ EventEmitter.defaultMaxListeners = 0;
5
15
 
6
16
  setupConsola();
7
17
 
@@ -15,7 +25,7 @@ if (authConfig?.jwtSecret == null) {
15
25
 
16
26
  export const server = createServiceServer({
17
27
  rootPath: import.meta.dirname,
18
- services: [{{#if hasDb}}OrmService{{/if}}],
28
+ services: [{{#if hasDb}}OrmService{{#if hasAuth}}, AuthService{{/if}}, ...(parseBoolEnv(env("DEV")) ? [DevService] : []){{/if}}],
19
29
  port: num.parseInt(env("SERVER_PORT"))!,
20
30
  auth: { jwtSecret: authConfig.jwtSecret },
21
31
  });
@@ -0,0 +1,284 @@
1
+ import { auth, defineService, type ServiceMethods } from "@simplysm/service-server";
2
+ import { expr } from "@simplysm/orm-common";
3
+ import { AuthInfoChangedEvent, {{userEntityPascal}}Config, RolePermission } from "@{{workspaceName}}/common";
4
+ import bcrypt from "bcrypt";
5
+ import { getOrm } from "../utils/orm.utils";
6
+
7
+ export interface I{{userEntityPascal}}ConfigMap {
8
+ "first-router-link"?: string;
9
+ }
10
+
11
+ export interface IAuthData {
12
+ {{userEntityCamel}}Id: number;
13
+ name: string;
14
+ roleName: string;
15
+ loginId: string;
16
+ permissions: Record<string, boolean>;
17
+ configs: I{{userEntityPascal}}ConfigMap;
18
+ }
19
+
20
+ export interface IAuthResult extends IAuthData {
21
+ token: string;
22
+ }
23
+
24
+ export const AuthService = defineService("AuthService", (ctx) => {
25
+ async function buildAuthResult({{userEntityCamel}}: {
26
+ id: number;
27
+ name: string;
28
+ loginId: string;
29
+ role?: { id: number; name: string };
30
+ permissions?: { code: string; valueJson: string }[];
31
+ configs?: { code: string; valueJson: string }[];
32
+ }): Promise<IAuthResult> {
33
+ const permissions: Record<string, boolean> = {};
34
+ for (const rp of {{userEntityCamel}}.permissions ?? []) {
35
+ permissions[rp.code] = JSON.parse(rp.valueJson) as boolean;
36
+ }
37
+
38
+ const configs: I{{userEntityPascal}}ConfigMap = {};
39
+ for (const cfg of {{userEntityCamel}}.configs ?? []) {
40
+ (configs as Record<string, unknown>)[cfg.code] = JSON.parse(cfg.valueJson);
41
+ }
42
+
43
+ const authData: IAuthData = {
44
+ {{userEntityCamel}}Id: {{userEntityCamel}}.id,
45
+ name: {{userEntityCamel}}.name,
46
+ roleName: {{userEntityCamel}}.role!.name,
47
+ loginId: {{userEntityCamel}}.loginId,
48
+ permissions,
49
+ configs,
50
+ };
51
+
52
+ const token = await ctx.server.signAuthToken({
53
+ roles: [],
54
+ data: authData,
55
+ });
56
+
57
+ return { token, ...authData };
58
+ }
59
+
60
+ return {
61
+ login: async (loginId: string, password: string): Promise<IAuthResult> => {
62
+ if (loginId.length === 0 || loginId.length > 50) {
63
+ throw new Error("아이디는 1~50자의 문자열이어야 합니다.");
64
+ }
65
+
66
+ if (password.length === 0 || password.length > 500) {
67
+ throw new Error("비밀번호는 1~500자의 문자열이어야 합니다.");
68
+ }
69
+
70
+ const orm = await getOrm(ctx);
71
+ return orm.connect(async (db) => {
72
+ const {{userEntityCamel}} = await db
73
+ .{{userEntityCamel}}()
74
+ .where((c) => [expr.eq(c.loginId, loginId)])
75
+ .join("permissions", (qr, e) =>
76
+ qr
77
+ .from(RolePermission)
78
+ .where((rp) => [expr.eq(rp.roleId, e.roleId)])
79
+ .select((rp) => ({ code: rp.code, valueJson: rp.valueJson })),
80
+ )
81
+ .join("configs", (qr, e) =>
82
+ qr
83
+ .from({{userEntityPascal}}Config)
84
+ .where((cfg) => [expr.eq(cfg.{{userEntityCamel}}Id, e.id)])
85
+ .select((cfg) => ({ code: cfg.code, valueJson: cfg.valueJson })),
86
+ )
87
+ .include((item) => item.role)
88
+ .select((item) => ({
89
+ id: item.id,
90
+ name: item.name,
91
+ loginId: item.loginId,
92
+ encryptedPassword: item.encryptedPassword,
93
+ role: item.role,
94
+ permissions: item.permissions?.map((it) => ({
95
+ code: it.code,
96
+ valueJson: it.valueJson,
97
+ })),
98
+ configs: item.configs?.map((it) => ({
99
+ code: it.code,
100
+ valueJson: it.valueJson,
101
+ })),
102
+ }))
103
+ .single();
104
+
105
+ if (!{{userEntityCamel}}) {
106
+ throw new Error("아이디 또는 비밀번호가 올바르지 않습니다.");
107
+ }
108
+
109
+ if ({{userEntityCamel}}.encryptedPassword == null || {{userEntityCamel}}.encryptedPassword === "") {
110
+ throw new Error("비밀번호가 설정되지 않은 계정입니다.");
111
+ }
112
+
113
+ if ({{userEntityCamel}}.role == null) {
114
+ throw new Error("권한이 부여되지 않은 계정입니다.");
115
+ }
116
+
117
+ const isValid = await bcrypt.compare(password, {{userEntityCamel}}.encryptedPassword);
118
+ if (!isValid) {
119
+ throw new Error("아이디 또는 비밀번호가 올바르지 않습니다.");
120
+ }
121
+
122
+ return buildAuthResult({{userEntityCamel}});
123
+ });
124
+ },
125
+
126
+ refresh: auth(async (): Promise<IAuthResult> => {
127
+ const currentAuth = ctx.authInfo as IAuthData | undefined;
128
+ if (!currentAuth) {
129
+ throw new Error("인증 정보가 없습니다.");
130
+ }
131
+
132
+ const orm = await getOrm(ctx);
133
+ return orm.connect(async (db) => {
134
+ const {{userEntityCamel}} = await db
135
+ .{{userEntityCamel}}()
136
+ .where((c) => [expr.eq(c.id, currentAuth.{{userEntityCamel}}Id)])
137
+ .join("permissions", (qr, e) =>
138
+ qr
139
+ .from(RolePermission)
140
+ .where((rp) => [expr.eq(rp.roleId, e.roleId)])
141
+ .select((rp) => ({ code: rp.code, valueJson: rp.valueJson })),
142
+ )
143
+ .join("configs", (qr, e) =>
144
+ qr
145
+ .from({{userEntityPascal}}Config)
146
+ .where((cfg) => [expr.eq(cfg.{{userEntityCamel}}Id, e.id)])
147
+ .select((cfg) => ({ code: cfg.code, valueJson: cfg.valueJson })),
148
+ )
149
+ .include((item) => item.role)
150
+ .select((item) => ({
151
+ id: item.id,
152
+ name: item.name,
153
+ loginId: item.loginId,
154
+ encryptedPassword: item.encryptedPassword,
155
+ role: item.role,
156
+ permissions: item.permissions?.map((it) => ({
157
+ code: it.code,
158
+ valueJson: it.valueJson,
159
+ })),
160
+ configs: item.configs?.map((it) => ({
161
+ code: it.code,
162
+ valueJson: it.valueJson,
163
+ })),
164
+ }))
165
+ .single();
166
+
167
+ if (!{{userEntityCamel}}) {
168
+ throw new Error("유효하지 않은 {{userEntityLabel}}입니다.");
169
+ }
170
+
171
+ if ({{userEntityCamel}}.encryptedPassword == null || {{userEntityCamel}}.encryptedPassword === "") {
172
+ throw new Error("비밀번호가 설정되지 않은 계정입니다.");
173
+ }
174
+
175
+ if ({{userEntityCamel}}.role == null) {
176
+ throw new Error("권한이 부여되지 않은 계정입니다.");
177
+ }
178
+
179
+ return buildAuthResult({{userEntityCamel}});
180
+ });
181
+ }),
182
+
183
+ update: auth(
184
+ async (input: {
185
+ configs: I{{userEntityPascal}}ConfigMap;
186
+ currentPassword?: string;
187
+ newPassword?: string;
188
+ }): Promise<void> => {
189
+ const currentAuth = ctx.authInfo as IAuthData | undefined;
190
+ if (!currentAuth) {
191
+ throw new Error("인증 정보가 없습니다.");
192
+ }
193
+ const {{userEntityCamel}}Id = currentAuth.{{userEntityCamel}}Id;
194
+
195
+ const isChangingPassword = !(input.newPassword == null || input.newPassword === "");
196
+ if (isChangingPassword) {
197
+ if (input.newPassword!.length > 500) {
198
+ throw new Error("비밀번호는 1~500자의 문자열이어야 합니다.");
199
+ }
200
+ if (input.currentPassword == null || input.currentPassword === "") {
201
+ throw new Error("기존 비밀번호를 입력하세요.");
202
+ }
203
+ }
204
+
205
+ const orm = await getOrm(ctx);
206
+ await orm.connect(async (db) => {
207
+ // 1) 비밀번호 변경(입력 시): 기존 비밀번호 검증 후 해시 갱신
208
+ if (isChangingPassword) {
209
+ const target = await db
210
+ .{{userEntityCamel}}()
211
+ .where((c) => [expr.eq(c.id, {{userEntityCamel}}Id)])
212
+ .select((item) => ({ encryptedPassword: item.encryptedPassword }))
213
+ .single();
214
+ if (!target) {
215
+ throw new Error("유효하지 않은 {{userEntityLabel}}입니다.");
216
+ }
217
+ if (target.encryptedPassword == null || target.encryptedPassword === "") {
218
+ throw new Error("비밀번호가 설정되지 않은 계정입니다.");
219
+ }
220
+
221
+ const isValid = await bcrypt.compare(input.currentPassword!, target.encryptedPassword);
222
+ if (!isValid) {
223
+ throw new Error("기존 비밀번호가 올바르지 않습니다.");
224
+ }
225
+
226
+ const newEncryptedPassword = await bcrypt.hash(input.newPassword!, 10);
227
+ await db
228
+ .{{userEntityCamel}}()
229
+ .where((c) => [expr.eq(c.id, {{userEntityCamel}}Id)])
230
+ .update(() => ({ encryptedPassword: newEncryptedPassword }));
231
+ }
232
+
233
+ // 2) 시스템설정 동기화: input.configs 에 없는 코드는 삭제, 있는 코드는 upsert
234
+ const codes = Object.keys(input.configs).filter(
235
+ (code) => (input.configs as Record<string, unknown>)[code] != null,
236
+ );
237
+ const existingConfigs = await db
238
+ .{{userEntityCamel}}Config()
239
+ .where((c) => [expr.eq(c.{{userEntityCamel}}Id, {{userEntityCamel}}Id)])
240
+ .select((c) => ({ code: c.code }))
241
+ .execute();
242
+ for (const existing of existingConfigs) {
243
+ if (!codes.includes(existing.code)) {
244
+ await db
245
+ .{{userEntityCamel}}Config()
246
+ .where((c) => [expr.eq(c.{{userEntityCamel}}Id, {{userEntityCamel}}Id), expr.eq(c.code, existing.code)])
247
+ .delete();
248
+ }
249
+ }
250
+ for (const code of codes) {
251
+ const valueJson = JSON.stringify((input.configs as Record<string, unknown>)[code]);
252
+ await db
253
+ .{{userEntityCamel}}Config()
254
+ .where((c) => [expr.eq(c.{{userEntityCamel}}Id, {{userEntityCamel}}Id), expr.eq(c.code, code)])
255
+ .upsert(
256
+ () => ({ valueJson }),
257
+ (up) => ({
258
+ ...up,
259
+ {{userEntityCamel}}Id,
260
+ code,
261
+ }),
262
+ );
263
+ }
264
+
265
+ // 3) 데이터 변경이력
266
+ await db.{{userEntityCamel}}().insertDataLog({
267
+ action: "수정(내정보수정)",
268
+ itemId: {{userEntityCamel}}Id,
269
+ {{userEntityCamel}}Id,
270
+ });
271
+ });
272
+
273
+ // 4) 인증정보 변경 이벤트 발생 → 본인 세션 포함 구독자가 authInfo 재로드
274
+ await ctx.server.emitEvent(
275
+ AuthInfoChangedEvent,
276
+ (info) => info.{{userEntityCamel}}Id === {{userEntityCamel}}Id,
277
+ undefined,
278
+ );
279
+ },
280
+ ),
281
+ };
282
+ });
283
+
284
+ export type AuthServiceMethods = ServiceMethods<typeof AuthService>;