@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
@@ -128,6 +128,140 @@ describe("server/src/main.ts.hbs", () => {
128
128
  expect(out).toMatchSnapshot();
129
129
  expect(out).toContain("OrmService");
130
130
  });
131
+
132
+ it("DB=Y 인증 OFF — AuthService 없음", async () => {
133
+ const data = buildData({
134
+ workspaceName: "demo",
135
+ description: "Demo",
136
+ hasServer: true,
137
+ hasDb: true,
138
+ dbDialect: "mysql",
139
+ hasAuth: false,
140
+ clients: [],
141
+ serverPort: 40080,
142
+ });
143
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/src/main.ts.hbs"), data);
144
+ expect(out).toContain(
145
+ 'services: [OrmService, ...(parseBoolEnv(env("DEV")) ? [DevService] : [])],',
146
+ );
147
+ expect(out).not.toContain("AuthService");
148
+ });
149
+
150
+ it("인증 ON — AuthService import + services 등록", async () => {
151
+ const data = buildData({
152
+ workspaceName: "demo",
153
+ description: "Demo",
154
+ hasServer: true,
155
+ hasDb: true,
156
+ dbDialect: "mysql",
157
+ hasAuth: true,
158
+ clients: [],
159
+ serverPort: 40080,
160
+ });
161
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/src/main.ts.hbs"), data);
162
+ expect(out).toContain('import { AuthService } from "./services/auth.service";');
163
+ expect(out).toContain('import { DevService } from "./services/dev.service";');
164
+ expect(out).toContain(
165
+ 'services: [OrmService, AuthService, ...(parseBoolEnv(env("DEV")) ? [DevService] : [])],',
166
+ );
167
+ });
168
+ });
169
+
170
+ describe("server 인증 템플릿", () => {
171
+ const authData = buildData({
172
+ workspaceName: "demo",
173
+ description: "Demo",
174
+ hasServer: true,
175
+ hasDb: true,
176
+ dbDialect: "mysql",
177
+ dbContextName: "main",
178
+ hasAuth: true,
179
+ userEntityName: "employee",
180
+ userEntityLabel: "직원",
181
+ clients: [],
182
+ serverPort: 40080,
183
+ });
184
+
185
+ it("orm.utils: dbContextClassName + 설정키(Upper) + workspace common import 반영", async () => {
186
+ const out = await renderTemplate(
187
+ path.join(TPL_ROOT, "server/src/utils/orm.utils.ts.hbs"),
188
+ authData,
189
+ );
190
+ expect(out).toContain('import { MainDbContext } from "@demo/common";');
191
+ expect(out).toContain("await ctx.getConfig<{ MAIN: DbConnConfig }>(\"orm\")");
192
+ expect(out).toContain("return createOrm(MainDbContext, ormConfig.MAIN);");
193
+ });
194
+
195
+ it("auth.service: 사용자 엔티티 네이밍이 IAuthData/쿼리/메시지에 반영", async () => {
196
+ const out = await renderTemplate(
197
+ path.join(TPL_ROOT, "server/src/services/auth.service.ts.hbs"),
198
+ authData,
199
+ );
200
+ expect(out).toContain('import { AuthInfoChangedEvent, EmployeeConfig, RolePermission } from "@demo/common";');
201
+ expect(out).toContain("export interface IEmployeeConfigMap {");
202
+ expect(out).toContain("employeeId: number;");
203
+ expect(out).toContain("name: string;");
204
+ expect(out).toContain("roleName: string;");
205
+ expect(out).toContain("configs: IEmployeeConfigMap;");
206
+ expect(out).toContain(".employee()");
207
+ expect(out).toContain("currentAuth.employeeId");
208
+ expect(out).toContain("roleName: employee.role!.name,");
209
+ expect(out).toContain(".include((item) => item.role)");
210
+ expect(out).toContain(".from(EmployeeConfig)");
211
+ expect(out).toContain("expr.eq(cfg.employeeId, e.id)");
212
+ expect(out).toContain("(configs as Record<string, unknown>)[cfg.code] = JSON.parse(cfg.valueJson);");
213
+ expect(out).toContain("유효하지 않은 직원입니다.");
214
+
215
+ // update(내정보수정) 메서드 + 인증정보 변경 이벤트 emit
216
+ expect(out).toContain("update: auth(");
217
+ expect(out).toContain("configs: IEmployeeConfigMap;");
218
+ expect(out).toContain(".employeeConfig()");
219
+ expect(out).toContain("expr.eq(c.employeeId, employeeId)");
220
+ expect(out).toContain('action: "수정(내정보수정)",');
221
+ expect(out).toContain("ctx.server.emitEvent(");
222
+ expect(out).toContain("(info) => info.employeeId === employeeId,");
223
+ });
224
+
225
+ it("auth.service: 기본 네이밍(user/사용자) 반영", async () => {
226
+ const data = buildData({
227
+ workspaceName: "demo",
228
+ description: "Demo",
229
+ hasServer: true,
230
+ hasDb: true,
231
+ dbDialect: "mysql",
232
+ hasAuth: true,
233
+ clients: [],
234
+ serverPort: 40080,
235
+ });
236
+ const out = await renderTemplate(
237
+ path.join(TPL_ROOT, "server/src/services/auth.service.ts.hbs"),
238
+ data,
239
+ );
240
+ expect(out).toContain("userId: number;");
241
+ expect(out).toContain(".user()");
242
+ expect(out).toContain("유효하지 않은 사용자입니다.");
243
+ });
244
+
245
+ it("server/package.json: 인증 ON 시 bcrypt + @types/bcrypt 포함", async () => {
246
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/package.json.hbs"), authData);
247
+ expect(out).toContain('"bcrypt": "^6.0.0"');
248
+ expect(out).toContain('"@types/bcrypt": "^6.0.0"');
249
+ });
250
+
251
+ it("server/package.json: 인증 OFF 시 bcrypt 없음", async () => {
252
+ const data = buildData({
253
+ workspaceName: "demo",
254
+ description: "Demo",
255
+ hasServer: true,
256
+ hasDb: true,
257
+ dbDialect: "mysql",
258
+ hasAuth: false,
259
+ clients: [],
260
+ serverPort: 40080,
261
+ });
262
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/package.json.hbs"), data);
263
+ expect(out).not.toContain("bcrypt");
264
+ });
131
265
  });
132
266
 
133
267
  describe("client/src/main.ts.hbs", () => {
@@ -163,7 +297,7 @@ describe("client/src/main.ts.hbs", () => {
163
297
  });
164
298
  });
165
299
 
166
- describe("client/src/AppPage.ts.hbs", () => {
300
+ describe("client/src/app.root.ts.hbs", () => {
167
301
  it("라우팅 Y → router-outlet", async () => {
168
302
  const data = buildData({
169
303
  workspaceName: "demo",
@@ -173,7 +307,7 @@ describe("client/src/AppPage.ts.hbs", () => {
173
307
  clients: [{ name: "admin", type: "web", hasRouter: true }],
174
308
  });
175
309
  const ctx = { ...data, client: data.clients[0] };
176
- const out = await renderTemplate(path.join(TPL_ROOT, "client/src/AppPage.ts.hbs"), ctx);
310
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app.root.ts.hbs"), ctx);
177
311
  expect(out).toContain("router-outlet");
178
312
  });
179
313
 
@@ -186,13 +320,146 @@ describe("client/src/AppPage.ts.hbs", () => {
186
320
  clients: [{ name: "x", type: "web", hasRouter: false }],
187
321
  });
188
322
  const ctx = { ...data, client: data.clients[0] };
189
- const out = await renderTemplate(path.join(TPL_ROOT, "client/src/AppPage.ts.hbs"), ctx);
323
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app.root.ts.hbs"), ctx);
190
324
  expect(out).not.toContain("router-outlet");
191
325
  expect(out).toContain("<div></div>");
192
326
  });
193
327
  });
194
328
 
195
- describe("client-common/src/providers/AppOrmProvider.ts.hbs", () => {
329
+ describe("client 인증 로그인 (routes / login.view / ng-icons)", () => {
330
+ function authClientCtx(hasAuth: boolean, hasRouter = true) {
331
+ const data = buildData({
332
+ workspaceName: "demo",
333
+ description: "Demo",
334
+ hasServer: true,
335
+ hasDb: true,
336
+ dbDialect: "mysql",
337
+ hasAuth,
338
+ clients: [{ name: "admin", type: "web", hasRouter }],
339
+ serverPort: 40080,
340
+ });
341
+ return { ...data, client: data.clients[0] };
342
+ }
343
+
344
+ it("routes.ts: 인증 ON → /login 리다이렉트 + lazy LoginView", async () => {
345
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/routes.ts.hbs"), authClientCtx(true));
346
+ expect(out).toContain('{ path: "", redirectTo: "/login", pathMatch: "full" }');
347
+ expect(out).toContain('import("./app/login/login.view").then((m) => m.LoginView)');
348
+ });
349
+
350
+ it("routes.ts: 인증 OFF → 빈 routes", async () => {
351
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/routes.ts.hbs"), authClientCtx(false));
352
+ expect(out).toContain("export const routes: Routes = [];");
353
+ expect(out).not.toContain("login");
354
+ });
355
+
356
+ it("login.view: client-common import + Angular 보간 복원 + configs 시작경로", async () => {
357
+ const out = await renderTemplate(
358
+ path.join(TPL_ROOT, "client/src/app/login/login.view.ts.hbs"),
359
+ authClientCtx(true),
360
+ );
361
+ expect(out).toContain('import { AppAuthProvider } from "@demo/client-common";');
362
+ expect(out).toContain('alt="demo"');
363
+ expect(out).toContain("v{{ VER }}.{{ DEV ? \"d\" : \"p\" }}");
364
+ expect(out).toContain('this._appAuth.authInfo()?.configs["first-router-link"] ?? "/home/main"');
365
+ expect(out).not.toContain("{{workspaceName}}");
366
+ });
367
+
368
+ it("client/package.json: 인증 ON + router → @ng-icons 포함", async () => {
369
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/package.json.hbs"), authClientCtx(true));
370
+ expect(out).toContain('"@ng-icons/core": "^33.2.3"');
371
+ expect(out).toContain('"@ng-icons/tabler-icons": "^33.2.3"');
372
+ });
373
+
374
+ it("client/package.json: 인증 OFF → @ng-icons 없음", async () => {
375
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/package.json.hbs"), authClientCtx(false));
376
+ expect(out).not.toContain("@ng-icons");
377
+ });
378
+ });
379
+
380
+ describe("관리자 셸 (home/main/app-structure/main.ts 와이어링)", () => {
381
+ function authClientCtx(hasAuth: boolean, hasRouter = true, userEntityName = "employee", userEntityLabel = "직원") {
382
+ const data = buildData({
383
+ workspaceName: "demo",
384
+ description: "Demo",
385
+ hasServer: true,
386
+ hasDb: true,
387
+ dbDialect: "mysql",
388
+ hasAuth,
389
+ userEntityName: hasAuth ? userEntityName : undefined,
390
+ userEntityLabel: hasAuth ? userEntityLabel : undefined,
391
+ clients: [{ name: "admin", type: "web", hasRouter }],
392
+ serverPort: 40080,
393
+ });
394
+ return { ...data, client: data.clients[0] };
395
+ }
396
+
397
+ it("common/app-structure: 사용자 엔티티 메뉴 항목에 선택 네이밍 반영", async () => {
398
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/src/app-structure.ts.hbs"), authClientCtx(true));
399
+ expect(out).toContain('import type { AppStructureItem } from "@simplysm/service-common";');
400
+ expect(out).toContain("export const appStructureItems: AppStructureItem[] = [");
401
+ expect(out).toContain('{ code: "employee", title: "직원", perms: ["use", "edit"] }');
402
+ expect(out).toContain('{ code: "role-permission", title: "역할/권한", perms: ["use", "edit"] }');
403
+ });
404
+
405
+ it("common/index: 인증 ON → app-structure re-export", async () => {
406
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/src/index.ts.hbs"), authClientCtx(true));
407
+ expect(out).toContain('export * from "./app-structure";');
408
+ });
409
+
410
+ it("common/index: 인증 OFF → app-structure 없음", async () => {
411
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/src/index.ts.hbs"), authClientCtx(false));
412
+ expect(out).not.toContain("app-structure");
413
+ });
414
+
415
+ it("common/package.json: 인증 ON → service-common 의존", async () => {
416
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/package.json.hbs"), authClientCtx(true));
417
+ expect(out).toContain('"@simplysm/service-common": "^14.0.0"');
418
+ });
419
+
420
+ it("common/package.json: 인증 OFF → service-common 없음", async () => {
421
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/package.json.hbs"), authClientCtx(false));
422
+ expect(out).not.toContain("service-common");
423
+ });
424
+
425
+ it("routes.ts: 인증 ON → home(+main children) 라우트 포함", async () => {
426
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/routes.ts.hbs"), authClientCtx(true));
427
+ expect(out).toContain('import("./app/home/home.view").then((m) => m.HomeView)');
428
+ expect(out).toContain('import("./app/home/main/main.view").then((m) => m.MainView)');
429
+ expect(out).toContain('{ path: "", redirectTo: "main", pathMatch: "full" }');
430
+ });
431
+
432
+ it("home.view: client-common import + Angular 보간 복원 + 사이드바", async () => {
433
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app/home/home.view.ts.hbs"), authClientCtx(true));
434
+ expect(out).toContain('import { AppAuthProvider } from "@demo/client-common";');
435
+ expect(out).toContain("export class HomeView {");
436
+ expect(out).toContain("{{ authInfo()?.name }}");
437
+ expect(out).toContain("{{ authInfo()?.roleName }}");
438
+ expect(out).toContain("this._sdAppStructure.usableMenus()");
439
+ expect(out).not.toContain("\\{{");
440
+ });
441
+
442
+ it("main.view: SdBaseContainer + viewType", async () => {
443
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app/home/main/main.view.ts.hbs"), authClientCtx(true));
444
+ expect(out).toContain("export class MainView {");
445
+ expect(out).toContain("viewType = injectViewTypeSignal();");
446
+ });
447
+
448
+ it("client/main.ts: 인증 ON + router → app-structure 초기화 와이어링", async () => {
449
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.ts.hbs"), authClientCtx(true));
450
+ expect(out).toContain("SdAppStructureProvider");
451
+ expect(out).toContain('import { appStructureItems } from "@demo/common";');
452
+ expect(out).toContain("inject(SdAppStructureProvider).initialize(appStructureItems);");
453
+ });
454
+
455
+ it("client/main.ts: 인증 OFF → app-structure 초기화 없음", async () => {
456
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/main.ts.hbs"), authClientCtx(false));
457
+ expect(out).not.toContain("SdAppStructureProvider");
458
+ expect(out).not.toContain("appStructureItems");
459
+ });
460
+ });
461
+
462
+ describe("client-common/src/providers/app-orm.provider.ts.hbs", () => {
196
463
  it("default dbContextName=main → MainDbContext + workspaceNameUpper database", async () => {
197
464
  const data = buildData({
198
465
  workspaceName: "demo2",
@@ -204,7 +471,7 @@ describe("client-common/src/providers/AppOrmProvider.ts.hbs", () => {
204
471
  serverPort: 40080,
205
472
  });
206
473
  const out = await renderTemplate(
207
- path.join(TPL_ROOT, "client-common/src/providers/AppOrmProvider.ts.hbs"),
474
+ path.join(TPL_ROOT, "client-common/src/providers/app-orm.provider.ts.hbs"),
208
475
  data,
209
476
  );
210
477
  expect(out).toContain('database: "DEMO2"');
@@ -226,7 +493,7 @@ describe("client-common/src/providers/AppOrmProvider.ts.hbs", () => {
226
493
  serverPort: 40080,
227
494
  });
228
495
  const out = await renderTemplate(
229
- path.join(TPL_ROOT, "client-common/src/providers/AppOrmProvider.ts.hbs"),
496
+ path.join(TPL_ROOT, "client-common/src/providers/app-orm.provider.ts.hbs"),
230
497
  data,
231
498
  );
232
499
  expect(out).toContain("import { OrderDbContext }");
@@ -235,6 +502,171 @@ describe("client-common/src/providers/AppOrmProvider.ts.hbs", () => {
235
502
  });
236
503
  });
237
504
 
505
+ describe("client-common/src/providers/app-service.provider.ts.hbs", () => {
506
+ const svcTpl = "client-common/src/providers/app-service.provider.ts.hbs";
507
+
508
+ it("인증 OFF (DB만) → auth getter / AuthServiceMethods 없음", async () => {
509
+ const data = buildData({
510
+ workspaceName: "demo",
511
+ description: "Demo",
512
+ hasServer: true,
513
+ hasDb: true,
514
+ dbDialect: "mysql",
515
+ hasAuth: false,
516
+ clients: [],
517
+ serverPort: 40080,
518
+ });
519
+ const out = await renderTemplate(path.join(TPL_ROOT, svcTpl), data);
520
+ expect(out).toContain("get orm()");
521
+ expect(out).not.toContain("AuthServiceMethods");
522
+ expect(out).not.toContain("get auth()");
523
+ });
524
+
525
+ it("인증 ON → AuthService 프록시 getter + server 타입 import", async () => {
526
+ const data = buildData({
527
+ workspaceName: "demo",
528
+ description: "Demo",
529
+ hasServer: true,
530
+ hasDb: true,
531
+ dbDialect: "mysql",
532
+ hasAuth: true,
533
+ clients: [],
534
+ serverPort: 40080,
535
+ });
536
+ const out = await renderTemplate(path.join(TPL_ROOT, svcTpl), data);
537
+ expect(out).toContain('import type { AuthServiceMethods } from "@demo/server";');
538
+ expect(out).toContain("type ServiceProxy,");
539
+ expect(out).toContain("private _auth?: ServiceProxy<AuthServiceMethods>;");
540
+ expect(out).toContain("get auth(): ServiceProxy<AuthServiceMethods> {");
541
+ expect(out).toContain(
542
+ 'this._auth ??= this.client.getService<AuthServiceMethods>("AuthService")',
543
+ );
544
+ });
545
+ });
546
+
547
+ describe("client-common/src/providers/app-auth.provider.ts.hbs", () => {
548
+ it("인증 ON → IAuthData(server) import + auth 흐름 메서드 포함", async () => {
549
+ const data = buildData({
550
+ workspaceName: "demo",
551
+ description: "Demo",
552
+ hasServer: true,
553
+ hasDb: true,
554
+ dbDialect: "mysql",
555
+ hasAuth: true,
556
+ clients: [],
557
+ serverPort: 40080,
558
+ });
559
+ const out = await renderTemplate(
560
+ path.join(TPL_ROOT, "client-common/src/providers/app-auth.provider.ts.hbs"),
561
+ data,
562
+ );
563
+ expect(out).toContain('import type { IAuthData } from "@demo/server";');
564
+ expect(out).toContain("export class AppAuthProvider {");
565
+ expect(out).toContain("authInfo = signal<IAuthData | undefined>(undefined);");
566
+ expect(out).toContain("inject(SdAppStructureProvider)");
567
+ expect(out).toContain("this._sdAppStructure.permRecord.set(this.authInfo()?.permissions);");
568
+ expect(out).toContain("async login(email: string, password: string): Promise<void>");
569
+ expect(out).toContain("async tryReloadAuth(): Promise<boolean>");
570
+ expect(out).toContain("await this._appService.client.auth(token);");
571
+ });
572
+ });
573
+
574
+ describe("client-common/src/index.ts.hbs auth export", () => {
575
+ it("인증 ON → AppAuthProvider export", async () => {
576
+ const data = buildData({
577
+ workspaceName: "demo",
578
+ description: "Demo",
579
+ hasServer: true,
580
+ hasDb: true,
581
+ dbDialect: "mysql",
582
+ hasAuth: true,
583
+ clients: [],
584
+ serverPort: 40080,
585
+ });
586
+ const out = await renderTemplate(path.join(TPL_ROOT, "client-common/src/index.ts.hbs"), data);
587
+ expect(out).toContain('export { AppAuthProvider } from "./providers/app-auth.provider";');
588
+ });
589
+
590
+ it("인증 OFF → AppAuthProvider export 없음", async () => {
591
+ const data = buildData({
592
+ workspaceName: "demo",
593
+ description: "Demo",
594
+ hasServer: true,
595
+ hasDb: true,
596
+ dbDialect: "mysql",
597
+ hasAuth: false,
598
+ clients: [],
599
+ serverPort: 40080,
600
+ });
601
+ const out = await renderTemplate(path.join(TPL_ROOT, "client-common/src/index.ts.hbs"), data);
602
+ expect(out).not.toContain("AppAuthProvider");
603
+ });
604
+ });
605
+
606
+ describe("server/src/index.ts.hbs", () => {
607
+ it("인증 ON → auth service 타입 re-export", async () => {
608
+ const data = buildData({
609
+ workspaceName: "demo",
610
+ description: "Demo",
611
+ hasServer: true,
612
+ hasDb: true,
613
+ dbDialect: "mysql",
614
+ hasAuth: true,
615
+ clients: [],
616
+ serverPort: 40080,
617
+ });
618
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/src/index.ts.hbs"), data);
619
+ expect(out).toContain(
620
+ 'export type { AuthServiceMethods, IAuthData, IAuthResult } from "./services/auth.service";',
621
+ );
622
+ });
623
+ });
624
+
625
+ describe("client-common/package.json.hbs server 의존", () => {
626
+ it("인증 ON → @workspace/server workspace 의존 추가", async () => {
627
+ const data = buildData({
628
+ workspaceName: "demo",
629
+ description: "Demo",
630
+ hasServer: true,
631
+ hasDb: true,
632
+ dbDialect: "mysql",
633
+ hasAuth: true,
634
+ clients: [],
635
+ serverPort: 40080,
636
+ });
637
+ const out = await renderTemplate(path.join(TPL_ROOT, "client-common/package.json.hbs"), data);
638
+ expect(out).toContain('"@demo/server": "workspace:*"');
639
+ });
640
+
641
+ it("DB OFF → @workspace/server 의존 없음", async () => {
642
+ const data = buildData({
643
+ workspaceName: "demo",
644
+ description: "Demo",
645
+ hasServer: true,
646
+ hasDb: false,
647
+ clients: [],
648
+ serverPort: 40080,
649
+ });
650
+ const out = await renderTemplate(path.join(TPL_ROOT, "client-common/package.json.hbs"), data);
651
+ expect(out).not.toContain("/server");
652
+ });
653
+
654
+ it("DB ON 인증 OFF → dev 서비스용 server 의존 유지", async () => {
655
+ const data = buildData({
656
+ workspaceName: "demo",
657
+ description: "Demo",
658
+ hasServer: true,
659
+ hasDb: true,
660
+ dbDialect: "mysql",
661
+ hasAuth: false,
662
+ clients: [],
663
+ serverPort: 40080,
664
+ });
665
+ const out = await renderTemplate(path.join(TPL_ROOT, "client-common/package.json.hbs"), data);
666
+ expect(out).toContain('"@demo/server": "workspace:*"');
667
+ });
668
+ });
669
+
238
670
  describe("sd.config.ts.hbs orm config 키", () => {
239
671
  it("DB=Y default → MAIN", async () => {
240
672
  const data = buildData({
@@ -273,7 +705,9 @@ describe("sd.config.ts.hbs orm config 키", () => {
273
705
  });
274
706
  });
275
707
 
276
- describe("common/src/DbContext.ts.hbs", () => {
708
+ describe("common/src/db/db-context.ts.hbs", () => {
709
+ const dbContextTpl = "common/src/db/db-context.ts.hbs";
710
+
277
711
  it("dbContextClassName 자리에 클래스명 들어감", async () => {
278
712
  const data = buildData({
279
713
  workspaceName: "demo",
@@ -285,24 +719,198 @@ describe("common/src/DbContext.ts.hbs", () => {
285
719
  clients: [],
286
720
  serverPort: 40080,
287
721
  });
288
- const out = await renderTemplate(path.join(TPL_ROOT, "common/src/DbContext.ts.hbs"), data);
722
+ const out = await renderTemplate(path.join(TPL_ROOT, dbContextTpl), data);
289
723
  expect(out).toContain("export class MainDbContext extends DbContext");
290
724
  });
725
+
726
+ it("인증 ON → 사용자/역할/권한 queryable 등록 (선택 네이밍 반영)", async () => {
727
+ const data = buildData({
728
+ workspaceName: "demo",
729
+ description: "Demo",
730
+ hasServer: true,
731
+ hasDb: true,
732
+ dbDialect: "mysql",
733
+ hasAuth: true,
734
+ userEntityName: "employee",
735
+ userEntityLabel: "직원",
736
+ clients: [],
737
+ serverPort: 40080,
738
+ });
739
+ const out = await renderTemplate(path.join(TPL_ROOT, dbContextTpl), data);
740
+ expect(out).toContain("employee = this.queryable(Employee);");
741
+ expect(out).toContain("employeeConfig = this.queryable(EmployeeConfig);");
742
+ expect(out).toContain("role = this.queryable(Role);");
743
+ expect(out).toContain("rolePermission = this.queryable(RolePermission);");
744
+ expect(out).toContain("dataLog = this.queryable(SystemDataLog);");
745
+ expect(out).toContain("systemLog = this.queryable(SystemLog);");
746
+ });
747
+
748
+ it("인증 OFF → 로그 queryable 만, 사용자/역할 없음", async () => {
749
+ const data = buildData({
750
+ workspaceName: "demo",
751
+ description: "Demo",
752
+ hasServer: true,
753
+ hasDb: true,
754
+ dbDialect: "mysql",
755
+ hasAuth: false,
756
+ clients: [],
757
+ serverPort: 40080,
758
+ });
759
+ const out = await renderTemplate(path.join(TPL_ROOT, dbContextTpl), data);
760
+ expect(out).toContain("dataLog = this.queryable(SystemDataLog);");
761
+ expect(out).toContain("systemLog = this.queryable(SystemLog);");
762
+ expect(out).not.toContain("queryable(Role)");
763
+ expect(out).not.toContain("Config = this.queryable");
764
+ });
765
+ });
766
+
767
+ describe("common/src/db/tables", () => {
768
+ const authData = buildData({
769
+ workspaceName: "demo",
770
+ description: "Demo",
771
+ hasServer: true,
772
+ hasDb: true,
773
+ dbDialect: "mysql",
774
+ hasAuth: true,
775
+ userEntityName: "employee",
776
+ userEntityLabel: "직원",
777
+ clients: [],
778
+ serverPort: 40080,
779
+ });
780
+
781
+ it("master 사용자 테이블: Pascal 테이블명 + 한글 라벨 + config 역참조 (camel 관계명)", async () => {
782
+ const out = await renderTemplate(
783
+ path.join(TPL_ROOT, "common/src/db/tables/master/user.ts.hbs"),
784
+ authData,
785
+ );
786
+ expect(out).toContain('export const Employee = Table("Employee")');
787
+ expect(out).toContain('.description("직원")');
788
+ expect(out).toContain("configs: r.foreignKeyTarget(() => EmployeeConfig, \"employee\")");
789
+ });
790
+
791
+ it("master 사용자-config 테이블: {camel}Id FK", async () => {
792
+ const out = await renderTemplate(
793
+ path.join(TPL_ROOT, "common/src/db/tables/master/user-config.ts.hbs"),
794
+ authData,
795
+ );
796
+ expect(out).toContain('export const EmployeeConfig = Table("EmployeeConfig")');
797
+ expect(out).toContain("employeeId: c.bigint(),");
798
+ expect(out).toContain('employee: r.foreignKey(["employeeId"], () => Employee)');
799
+ });
800
+
801
+ it("system-data-log 인증 ON → {camel}Id FK 포함", async () => {
802
+ const out = await renderTemplate(
803
+ path.join(TPL_ROOT, "common/src/db/tables/system/system-data-log.ts.hbs"),
804
+ authData,
805
+ );
806
+ expect(out).toContain("employeeId: c.bigint().nullable(),");
807
+ expect(out).toContain('employee: r.foreignKey(["employeeId"], () => Employee)');
808
+ });
809
+
810
+ it("system-data-log 인증 OFF → employee 컬럼/관계 없음, 문장 종료 정상", async () => {
811
+ const data = buildData({
812
+ workspaceName: "demo",
813
+ description: "Demo",
814
+ hasServer: true,
815
+ hasDb: true,
816
+ dbDialect: "mysql",
817
+ hasAuth: false,
818
+ clients: [],
819
+ serverPort: 40080,
820
+ });
821
+ const out = await renderTemplate(
822
+ path.join(TPL_ROOT, "common/src/db/tables/system/system-data-log.ts.hbs"),
823
+ data,
824
+ );
825
+ expect(out).not.toContain("employeeId");
826
+ expect(out).not.toContain(".relations(");
827
+ expect(out).not.toContain("master/");
828
+ expect(out.trimEnd().endsWith("]);")).toBe(true);
829
+ });
830
+ });
831
+
832
+ describe("common/src/db/system-data-log.ext.ts.hbs", () => {
833
+ it("인증 ON → {camel}Id/{camel}Name 조인 select 포함", async () => {
834
+ const data = buildData({
835
+ workspaceName: "demo",
836
+ description: "Demo",
837
+ hasServer: true,
838
+ hasDb: true,
839
+ dbDialect: "mysql",
840
+ hasAuth: true,
841
+ userEntityName: "employee",
842
+ userEntityLabel: "직원",
843
+ clients: [],
844
+ serverPort: 40080,
845
+ });
846
+ const out = await renderTemplate(
847
+ path.join(TPL_ROOT, "common/src/db/system-data-log.ext.ts.hbs"),
848
+ data,
849
+ );
850
+ expect(out).toContain("employeeId?: number;");
851
+ expect(out).toContain("employeeName?: string;");
852
+ expect(out).toContain(".include((dl) => dl.employee)");
853
+ expect(out).toContain("employeeName: dl.employee!.name,");
854
+ });
855
+
856
+ it("인증 OFF → employee 조인 없음 (action/dateTime 만)", async () => {
857
+ const data = buildData({
858
+ workspaceName: "demo",
859
+ description: "Demo",
860
+ hasServer: true,
861
+ hasDb: true,
862
+ dbDialect: "mysql",
863
+ hasAuth: false,
864
+ clients: [],
865
+ serverPort: 40080,
866
+ });
867
+ const out = await renderTemplate(
868
+ path.join(TPL_ROOT, "common/src/db/system-data-log.ext.ts.hbs"),
869
+ data,
870
+ );
871
+ expect(out).not.toContain("employee");
872
+ expect(out).not.toContain(".include(");
873
+ });
291
874
  });
292
875
 
293
876
  describe("common/src/index.ts.hbs", () => {
294
- it("DB=Y → MainDbContext re-export", async () => {
877
+ it("DB=Y 인증 ON db폴더 경로 + 사용자/역할 re-export", async () => {
878
+ const data = buildData({
879
+ workspaceName: "demo",
880
+ description: "Demo",
881
+ hasServer: true,
882
+ hasDb: true,
883
+ dbDialect: "mysql",
884
+ hasAuth: true,
885
+ userEntityName: "employee",
886
+ userEntityLabel: "직원",
887
+ clients: [],
888
+ serverPort: 40080,
889
+ });
890
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/src/index.ts.hbs"), data);
891
+ expect(out).toContain('export * from "./db-main/main.db-context"');
892
+ expect(out).toContain('export * from "./db-main/tables/master/employee"');
893
+ expect(out).toContain('export * from "./db-main/tables/master/employee-config"');
894
+ expect(out).toContain('export * from "./db-main/tables/system/role"');
895
+ expect(out).toContain('export * from "./db-main/system-data-log.ext"');
896
+ });
897
+
898
+ it("DB=Y 인증 OFF → 로그 테이블만 re-export, 사용자/역할 없음", async () => {
295
899
  const data = buildData({
296
900
  workspaceName: "demo",
297
901
  description: "Demo",
298
902
  hasServer: true,
299
903
  hasDb: true,
300
904
  dbDialect: "mysql",
905
+ hasAuth: false,
301
906
  clients: [],
302
907
  serverPort: 40080,
303
908
  });
304
909
  const out = await renderTemplate(path.join(TPL_ROOT, "common/src/index.ts.hbs"), data);
305
- expect(out).toContain('export * from "./MainDbContext"');
910
+ expect(out).toContain('export * from "./db-main/main.db-context"');
911
+ expect(out).toContain('export * from "./db-main/tables/system/system-data-log"');
912
+ expect(out).not.toContain("master/");
913
+ expect(out).not.toContain("tables/system/role");
306
914
  });
307
915
 
308
916
  it("DB=N → 빈 export", async () => {
@@ -355,3 +963,336 @@ describe("root/package.json.hbs", () => {
355
963
  expect(out).not.toContain("run-device");
356
964
  });
357
965
  });
966
+
967
+ describe("common/src/auth-info-changed.event.ts.hbs", () => {
968
+ it("인증 ON → AuthInfoChangedEvent 정의 (선택 네이밍 반영)", async () => {
969
+ const data = buildData({
970
+ workspaceName: "demo",
971
+ description: "Demo",
972
+ hasServer: true,
973
+ hasDb: true,
974
+ dbDialect: "mysql",
975
+ hasAuth: true,
976
+ userEntityName: "employee",
977
+ userEntityLabel: "직원",
978
+ clients: [],
979
+ serverPort: 40080,
980
+ });
981
+ const out = await renderTemplate(
982
+ path.join(TPL_ROOT, "common/src/auth-info-changed.event.ts.hbs"),
983
+ data,
984
+ );
985
+ expect(out).toContain('import { defineEvent } from "@simplysm/service-common";');
986
+ expect(out).toContain(
987
+ 'export const AuthInfoChangedEvent = defineEvent<{ employeeId: number }, void>("AuthInfoChanged");',
988
+ );
989
+ });
990
+ });
991
+
992
+ describe("common/src/index.ts.hbs auth-info-changed export", () => {
993
+ it("인증 ON → auth-info-changed.event re-export", async () => {
994
+ const data = buildData({
995
+ workspaceName: "demo",
996
+ description: "Demo",
997
+ hasServer: true,
998
+ hasDb: true,
999
+ dbDialect: "mysql",
1000
+ hasAuth: true,
1001
+ clients: [],
1002
+ serverPort: 40080,
1003
+ });
1004
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/src/index.ts.hbs"), data);
1005
+ expect(out).toContain('export * from "./auth-info-changed.event";');
1006
+ });
1007
+
1008
+ it("인증 OFF → auth-info-changed.event 없음", async () => {
1009
+ const data = buildData({
1010
+ workspaceName: "demo",
1011
+ description: "Demo",
1012
+ hasServer: true,
1013
+ hasDb: true,
1014
+ dbDialect: "mysql",
1015
+ hasAuth: false,
1016
+ clients: [],
1017
+ serverPort: 40080,
1018
+ });
1019
+ const out = await renderTemplate(path.join(TPL_ROOT, "common/src/index.ts.hbs"), data);
1020
+ expect(out).not.toContain("auth-info-changed");
1021
+ });
1022
+ });
1023
+
1024
+ describe("client-common 인증정보 변경 구독 배선", () => {
1025
+ const data = buildData({
1026
+ workspaceName: "demo",
1027
+ description: "Demo",
1028
+ hasServer: true,
1029
+ hasDb: true,
1030
+ dbDialect: "mysql",
1031
+ hasAuth: true,
1032
+ userEntityName: "employee",
1033
+ userEntityLabel: "직원",
1034
+ clients: [],
1035
+ serverPort: 40080,
1036
+ });
1037
+
1038
+ it("app-service: authInfoChangedEvent getter + ClientEventProxy/이벤트 import", async () => {
1039
+ const out = await renderTemplate(
1040
+ path.join(TPL_ROOT, "client-common/src/providers/app-service.provider.ts.hbs"),
1041
+ data,
1042
+ );
1043
+ expect(out).toContain("type ClientEventProxy,");
1044
+ expect(out).toContain('import { AuthInfoChangedEvent } from "@demo/common";');
1045
+ expect(out).toContain(
1046
+ "get authInfoChangedEvent(): ClientEventProxy<typeof AuthInfoChangedEvent> {",
1047
+ );
1048
+ expect(out).toContain(
1049
+ "this._authInfoChangedEvent ??= this.client.getEvent(AuthInfoChangedEvent)",
1050
+ );
1051
+ });
1052
+
1053
+ it("app-auth: 이벤트 구독 등록/해제 + 비동기 logout", async () => {
1054
+ const out = await renderTemplate(
1055
+ path.join(TPL_ROOT, "client-common/src/providers/app-auth.provider.ts.hbs"),
1056
+ data,
1057
+ );
1058
+ expect(out).toContain("async logout(): Promise<void> {");
1059
+ expect(out).toContain("await this._unregisterAuthEvent();");
1060
+ expect(out).toContain("await this._registerAuthEvent(authData.employeeId);");
1061
+ expect(out).toContain("private async _registerAuthEvent(employeeId: number): Promise<void> {");
1062
+ expect(out).toContain("this._appService.authInfoChangedEvent.addListener(");
1063
+ });
1064
+ });
1065
+
1066
+ describe("client/src/app/home/my-info/my-info.detail.ts.hbs", () => {
1067
+ const data = buildData({
1068
+ workspaceName: "demo",
1069
+ description: "Demo",
1070
+ hasServer: true,
1071
+ hasDb: true,
1072
+ dbDialect: "mysql",
1073
+ hasAuth: true,
1074
+ userEntityName: "employee",
1075
+ userEntityLabel: "직원",
1076
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
1077
+ serverPort: 40080,
1078
+ });
1079
+ const ctx = { ...data, client: data.clients[0] };
1080
+
1081
+ it("내정보수정 화면: 네이밍 반영 + Angular 보간 복원 + update 호출", async () => {
1082
+ const out = await renderTemplate(
1083
+ path.join(TPL_ROOT, "client/src/app/home/my-info/my-info.detail.ts.hbs"),
1084
+ ctx,
1085
+ );
1086
+ expect(out).toContain("export class MyInfoDetail {");
1087
+ expect(out).toContain(
1088
+ 'import { AppAuthProvider, AppOrmProvider, AppServiceProvider } from "@demo/client-common";',
1089
+ );
1090
+ expect(out).toContain('import { EmployeeConfig } from "@demo/common";');
1091
+ expect(out).toContain('import type { IEmployeeConfigMap } from "@demo/server";');
1092
+ expect(out).toContain(".employee()");
1093
+ expect(out).toContain(".from(EmployeeConfig)");
1094
+ expect(out).toContain("this._appAuth.authInfo()?.employeeId");
1095
+ expect(out).toContain("lastModifiedBy: item.lastDataLog?.employeeName,");
1096
+ expect(out).toContain("await this._appService.auth.update({");
1097
+ expect(out).toContain('{{ flatMenu.titleChain.join(" / ") }}');
1098
+ expect(out).not.toContain("\\{{");
1099
+ });
1100
+ });
1101
+
1102
+ describe("client/src/routes.ts.hbs my-info 라우트", () => {
1103
+ it("인증 ON → my-info lazy 라우트 포함", async () => {
1104
+ const data = buildData({
1105
+ workspaceName: "demo",
1106
+ description: "Demo",
1107
+ hasServer: true,
1108
+ hasDb: true,
1109
+ dbDialect: "mysql",
1110
+ hasAuth: true,
1111
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
1112
+ serverPort: 40080,
1113
+ });
1114
+ const ctx = { ...data, client: data.clients[0] };
1115
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/routes.ts.hbs"), ctx);
1116
+ expect(out).toContain(
1117
+ 'import("./app/home/my-info/my-info.detail").then((m) => m.MyInfoDetail)',
1118
+ );
1119
+ });
1120
+ });
1121
+
1122
+ describe("server/src/services/dev.service.ts.hbs", () => {
1123
+ it("인증 ON → 엑셀 시드(역할/직원/역할권한) + 사용자 네이밍 반영", async () => {
1124
+ const data = buildData({
1125
+ workspaceName: "demo",
1126
+ description: "Demo",
1127
+ hasServer: true,
1128
+ hasDb: true,
1129
+ dbDialect: "mysql",
1130
+ hasAuth: true,
1131
+ userEntityName: "employee",
1132
+ userEntityLabel: "직원",
1133
+ clients: [],
1134
+ serverPort: 40080,
1135
+ });
1136
+ const out = await renderTemplate(
1137
+ path.join(TPL_ROOT, "server/src/services/dev.service.ts.hbs"),
1138
+ data,
1139
+ );
1140
+ expect(out).toContain('export const DevService = defineService("DevService"');
1141
+ expect(out).toContain("await db.initialize({ force: true });");
1142
+ expect(out).toContain('import { ExcelWorkbook } from "@simplysm/excel";');
1143
+ expect(out).toContain('import { appStructureItems } from "@demo/common";');
1144
+ expect(out).toContain(".employee().insert(");
1145
+ expect(out).toContain("getFlatPermissions(appStructureItems, undefined)");
1146
+ expect(out).toContain("// TODO: 업무 테이블 초기 데이터 시드 추가");
1147
+ });
1148
+
1149
+ it("인증 OFF (DB만) → 시드 없이 db.initialize + TODO", async () => {
1150
+ const data = buildData({
1151
+ workspaceName: "demo",
1152
+ description: "Demo",
1153
+ hasServer: true,
1154
+ hasDb: true,
1155
+ dbDialect: "mysql",
1156
+ hasAuth: false,
1157
+ clients: [],
1158
+ serverPort: 40080,
1159
+ });
1160
+ const out = await renderTemplate(
1161
+ path.join(TPL_ROOT, "server/src/services/dev.service.ts.hbs"),
1162
+ data,
1163
+ );
1164
+ expect(out).toContain("await db.initialize({ force: true });");
1165
+ expect(out).toContain("// TODO: 초기 데이터 시드 추가");
1166
+ expect(out).not.toContain("ExcelWorkbook");
1167
+ expect(out).not.toContain("bcrypt");
1168
+ expect(out).not.toContain(".insert(");
1169
+ });
1170
+ });
1171
+
1172
+ describe("server/src/index.ts.hbs dev export", () => {
1173
+ it("DB ON 인증 OFF → DevServiceMethods 만 export", async () => {
1174
+ const data = buildData({
1175
+ workspaceName: "demo",
1176
+ description: "Demo",
1177
+ hasServer: true,
1178
+ hasDb: true,
1179
+ dbDialect: "mysql",
1180
+ hasAuth: false,
1181
+ clients: [],
1182
+ serverPort: 40080,
1183
+ });
1184
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/src/index.ts.hbs"), data);
1185
+ expect(out).toContain('export type { DevServiceMethods } from "./services/dev.service";');
1186
+ expect(out).not.toContain("auth.service");
1187
+ });
1188
+
1189
+ it("인증 ON → auth + dev export", async () => {
1190
+ const data = buildData({
1191
+ workspaceName: "demo",
1192
+ description: "Demo",
1193
+ hasServer: true,
1194
+ hasDb: true,
1195
+ dbDialect: "mysql",
1196
+ hasAuth: true,
1197
+ clients: [],
1198
+ serverPort: 40080,
1199
+ });
1200
+ const out = await renderTemplate(path.join(TPL_ROOT, "server/src/index.ts.hbs"), data);
1201
+ expect(out).toContain('from "./services/auth.service";');
1202
+ expect(out).toContain('export type { DevServiceMethods } from "./services/dev.service";');
1203
+ });
1204
+ });
1205
+
1206
+ describe("client-common app-service dev getter", () => {
1207
+ it("DB ON → dev getter + DevServiceMethods import", async () => {
1208
+ const data = buildData({
1209
+ workspaceName: "demo",
1210
+ description: "Demo",
1211
+ hasServer: true,
1212
+ hasDb: true,
1213
+ dbDialect: "mysql",
1214
+ hasAuth: false,
1215
+ clients: [],
1216
+ serverPort: 40080,
1217
+ });
1218
+ const out = await renderTemplate(
1219
+ path.join(TPL_ROOT, "client-common/src/providers/app-service.provider.ts.hbs"),
1220
+ data,
1221
+ );
1222
+ expect(out).toContain('import type { DevServiceMethods } from "@demo/server";');
1223
+ expect(out).toContain("type ServiceProxy,");
1224
+ expect(out).toContain("get dev(): ServiceProxy<DevServiceMethods> {");
1225
+ expect(out).toContain('this._dev ??= this.client.getService<DevServiceMethods>("DevService")');
1226
+ });
1227
+
1228
+ it("DB OFF → dev getter 없음", async () => {
1229
+ const data = buildData({
1230
+ workspaceName: "demo",
1231
+ description: "Demo",
1232
+ hasServer: true,
1233
+ hasDb: false,
1234
+ clients: [],
1235
+ serverPort: 40080,
1236
+ });
1237
+ const out = await renderTemplate(
1238
+ path.join(TPL_ROOT, "client-common/src/providers/app-service.provider.ts.hbs"),
1239
+ data,
1240
+ );
1241
+ expect(out).not.toContain("DevServiceMethods");
1242
+ expect(out).not.toContain("get dev()");
1243
+ });
1244
+ });
1245
+
1246
+ describe("client/src/app.root.ts.hbs dev 도구 배선", () => {
1247
+ function devCtx(hasDb: boolean, hasRouter = true) {
1248
+ const data = buildData({
1249
+ workspaceName: "demo",
1250
+ description: "Demo",
1251
+ hasServer: hasDb,
1252
+ hasDb,
1253
+ dbDialect: "mysql",
1254
+ clients: [{ name: "admin", type: "web", hasRouter }],
1255
+ serverPort: 40080,
1256
+ });
1257
+ return { ...data, client: data.clients[0] };
1258
+ }
1259
+
1260
+ it("DB ON → HostListener keydown + DevModal 호출", async () => {
1261
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app.root.ts.hbs"), devCtx(true));
1262
+ expect(out).toContain('@HostListener("document:keydown", ["$event"])');
1263
+ expect(out).toContain('import { DevModal } from "./modals/dev.modal";');
1264
+ expect(out).toContain("this._sdModal.showAsync({ type: DevModal");
1265
+ expect(out).toContain("export class AppRoot {");
1266
+ });
1267
+
1268
+ it("DB OFF → 빈 AppRoot (dev 배선 없음)", async () => {
1269
+ const out = await renderTemplate(path.join(TPL_ROOT, "client/src/app.root.ts.hbs"), devCtx(false));
1270
+ expect(out).toContain("export class AppRoot {}");
1271
+ expect(out).not.toContain("HostListener");
1272
+ expect(out).not.toContain("DevModal");
1273
+ });
1274
+ });
1275
+
1276
+ describe("client/src/modals/dev.modal.ts.hbs", () => {
1277
+ it("dev 모달: client-common import + initDb 호출 (아이콘 의존 없음)", async () => {
1278
+ const data = buildData({
1279
+ workspaceName: "demo",
1280
+ description: "Demo",
1281
+ hasServer: true,
1282
+ hasDb: true,
1283
+ dbDialect: "mysql",
1284
+ hasAuth: true,
1285
+ clients: [{ name: "admin", type: "web", hasRouter: true }],
1286
+ serverPort: 40080,
1287
+ });
1288
+ const ctx = { ...data, client: data.clients[0] };
1289
+ const out = await renderTemplate(
1290
+ path.join(TPL_ROOT, "client/src/modals/dev.modal.ts.hbs"),
1291
+ ctx,
1292
+ );
1293
+ expect(out).toContain("export class DevModal implements SdModalContentDef<void> {");
1294
+ expect(out).toContain('import { AppServiceProvider } from "@demo/client-common";');
1295
+ expect(out).toContain("await this._appService.dev.initDb();");
1296
+ expect(out).not.toContain("@ng-icons");
1297
+ });
1298
+ });