@simplysm/sd-cli 14.0.94 → 14.0.96

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 (42) hide show
  1. package/dist/commands/init/generators/client.d.ts.map +1 -1
  2. package/dist/commands/init/generators/client.js +9 -0
  3. package/dist/commands/init/generators/client.js.map +1 -1
  4. package/dist/commands/init/generators/server.d.ts.map +1 -1
  5. package/dist/commands/init/generators/server.js +1 -0
  6. package/dist/commands/init/generators/server.js.map +1 -1
  7. package/package.json +5 -5
  8. package/src/commands/init/generators/client.ts +45 -0
  9. package/src/commands/init/generators/server.ts +5 -0
  10. package/src/commands/init/templates/client/src/app/home/home.view.ts.hbs +1 -1
  11. package/src/commands/init/templates/client/src/app/home/master/role-permission/role-permission.detail.ts.hbs +221 -0
  12. package/src/commands/init/templates/client/src/app/home/master/role-permission/role-permission.view.ts.hbs +106 -0
  13. package/src/commands/init/templates/client/src/app/home/master/role-permission/role.detail.ts.hbs +277 -0
  14. package/src/commands/init/templates/client/src/app/home/master/role-permission/role.list.ts.hbs +537 -0
  15. package/src/commands/init/templates/client/src/app/home/master/user.detail.ts.hbs +337 -0
  16. package/src/commands/init/templates/client/src/app/home/master/user.list.ts.hbs +540 -0
  17. package/src/commands/init/templates/client/src/app/home/my-info/my-info.detail.ts.hbs +4 -6
  18. package/src/commands/init/templates/client/src/app/home/system/data-log/data-log.list.ts.hbs +355 -0
  19. package/src/commands/init/templates/client/src/app/home/system/system-log/system-log.list.ts.hbs +382 -0
  20. package/src/commands/init/templates/client/src/app/login/login.view.ts.hbs +3 -4
  21. package/src/commands/init/templates/client/src/app.root.ts.hbs +9 -3
  22. package/src/commands/init/templates/client/src/index.html.hbs +1 -0
  23. package/src/commands/init/templates/client/src/main.ts.hbs +23 -18
  24. package/src/commands/init/templates/client/src/modals/text-view.modal.ts.hbs +30 -0
  25. package/src/commands/init/templates/client/src/routes.ts.hbs +22 -0
  26. package/src/commands/init/templates/client-common/src/index.ts.hbs +6 -4
  27. package/src/commands/init/templates/client-common/src/providers/app-auth.provider.ts.hbs +3 -3
  28. package/src/commands/init/templates/client-common/src/providers/app-orm.provider.ts.hbs +0 -11
  29. package/src/commands/init/templates/client-common/src/providers/app-service.provider.ts.hbs +24 -10
  30. package/src/commands/init/templates/client-common/src/providers/app-shared-data.provider.ts.hbs +2 -2
  31. package/src/commands/init/templates/common/package.json.hbs +2 -1
  32. package/src/commands/init/templates/common/src/app-structure.ts.hbs +7 -2
  33. package/src/commands/init/templates/common/src/auth-info-changed.event.ts.hbs +3 -1
  34. package/src/commands/init/templates/common/src/db/db-context.ts.hbs +2 -2
  35. package/src/commands/init/templates/common/src/db/tables/master/user.ts.hbs +2 -2
  36. package/src/commands/init/templates/common/src/db/tables/system/role.ts.hbs +1 -0
  37. package/src/commands/init/templates/common/src/index.ts.hbs +1 -1
  38. package/src/commands/init/templates/server/src/index.ts.hbs +3 -0
  39. package/src/commands/init/templates/server/src/main.ts.hbs +15 -1
  40. package/src/commands/init/templates/server/src/services/auth.service.ts.hbs +28 -22
  41. package/src/commands/init/templates/server/src/services/dev.service.ts.hbs +5 -5
  42. package/src/commands/init/templates/server/src/services/user.service.ts.hbs +191 -0
@@ -0,0 +1,337 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ inject,
7
+ input,
8
+ output,
9
+ signal,
10
+ untracked,
11
+ ViewEncapsulation,
12
+ } from "@angular/core";
13
+ import {
14
+ injectPermsSignal,
15
+ injectViewTypeSignal,
16
+ mark,
17
+ SdButton,
18
+ SdCrudDetail,
19
+ SdItemOfTemplate,
20
+ type SdModalContentDef,
21
+ SdSharedDataSelect,
22
+ SdTextfield,
23
+ SdToastProvider,
24
+ setupCanDeactivate,
25
+ } from "@simplysm/angular";
26
+ import { obj } from "@simplysm/core-common";
27
+ import { expr } from "@simplysm/orm-common";
28
+ import {
29
+ AppAuthProvider,
30
+ AppOrmProvider,
31
+ AppServiceProvider,
32
+ AppSharedDataProvider,
33
+ useSharedSignal,
34
+ } from "@{{workspaceName}}/client-common";
35
+
36
+ @Component({
37
+ selector: "app-{{userEntityKebab}}-detail",
38
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
+ encapsulation: ViewEncapsulation.None,
40
+ standalone: true,
41
+ imports: [SdCrudDetail, SdTextfield, SdSharedDataSelect, SdItemOfTemplate, SdButton],
42
+ template: `
43
+ <sd-crud-detail
44
+ [(ready)]="ready"
45
+ [initialized]="initialized()"
46
+ [(busyCount)]="busyCount"
47
+ [viewType]="viewType()"
48
+ [restricted]="!perms().includes('use')"
49
+ [readonly]="!perms().includes('edit')"
50
+ (submit)="onSubmit()"
51
+ >
52
+ @if ({{userEntityCamel}}Id() != null && !isSelf() && perms().includes("edit")) {
53
+ <ng-template #bottomCommandTpl>
54
+ <div class="flex-row gap-sm">
55
+ @if (data().isDeleted) {
56
+ <sd-button [size]="'sm'" [theme]="'warning'" (click)="onRestore()">
57
+ 복구
58
+ </sd-button>
59
+ } @else {
60
+ <sd-button [size]="'sm'" [theme]="'danger'" (click)="onDelete()">
61
+ 삭제
62
+ </sd-button>
63
+ }
64
+ </div>
65
+ </ng-template>
66
+ }
67
+
68
+ <ng-template #contentTpl>
69
+ <div class="p-default">
70
+ <table class="form-table">
71
+ <tbody>
72
+ <tr><th colspan="2" class="form-table-header">기본정보</th></tr>
73
+ <tr>
74
+ <th>이름</th>
75
+ <td>
76
+ <sd-textfield
77
+ [type]="'text'"
78
+ [(value)]="data().name"
79
+ (valueChange)="mark(data)"
80
+ [required]="true"
81
+ [disabled]="!perms().includes('edit')"
82
+ />
83
+ </td>
84
+ </tr>
85
+ </tbody>
86
+
87
+ <tbody>
88
+ <tr><th colspan="2" class="form-table-header">인증/권한</th></tr>
89
+ <tr>
90
+ <th>로그인ID</th>
91
+ <td>
92
+ <sd-textfield
93
+ [type]="'text'"
94
+ [(value)]="data().loginId"
95
+ (valueChange)="mark(data)"
96
+ [disabled]="!perms().includes('edit')"
97
+ />
98
+ </td>
99
+ </tr>
100
+ <tr>
101
+ <th>역할</th>
102
+ <td>
103
+ <sd-shared-data-select
104
+ [items]="sharedRoles.items()"
105
+ [(value)]="data().roleId"
106
+ (valueChange)="mark(data)"
107
+ [required]="!!data().loginId?.trim()"
108
+ [disabled]="!perms().includes('edit')"
109
+ >
110
+ <ng-template [itemOf]="sharedRoles.items()" let-item="item">
111
+ \{{ item.name }}
112
+ </ng-template>
113
+ </sd-shared-data-select>
114
+ </td>
115
+ </tr>
116
+ <tr>
117
+ <th>비밀번호</th>
118
+ <td>
119
+ <sd-textfield
120
+ [type]="'password'"
121
+ [placeholder]="passwordPlaceholder()"
122
+ [(value)]="data().password"
123
+ (valueChange)="mark(data)"
124
+ [required]="!!data().loginId?.trim() && {{userEntityCamel}}Id() == null"
125
+ [disabled]="!perms().includes('edit')"
126
+ />
127
+ </td>
128
+ </tr>
129
+ </tbody>
130
+ </table>
131
+ </div>
132
+ </ng-template>
133
+ </sd-crud-detail>
134
+ `,
135
+ })
136
+ export class {{userEntityPascal}}Detail implements SdModalContentDef<{ id: number } | undefined> {
137
+ private readonly _sdToast = inject(SdToastProvider);
138
+ private readonly _appOrm = inject(AppOrmProvider);
139
+ private readonly _appService = inject(AppServiceProvider);
140
+ private readonly _appSharedData = inject(AppSharedDataProvider);
141
+ private readonly _appAuth = inject(AppAuthProvider);
142
+
143
+ {{userEntityCamel}}Id = input<number>();
144
+ close = output<{ id: number } | undefined>();
145
+
146
+ perms = injectPermsSignal(["master.{{userEntityKebab}}"], ["use", "edit"]);
147
+ viewType = injectViewTypeSignal();
148
+
149
+ // 로그인한 본인 계정은 삭제 불가 — 삭제 버튼 숨김(아래 isSelf). 핸들러 가드는 두지 않음(목록 선택 차단 + 이 버튼 숨김이 모든 삭제 경로를 차단).
150
+ isSelf = computed(
151
+ () => this.{{userEntityCamel}}Id() != null && this.{{userEntityCamel}}Id() === this._appAuth.authInfo()?.{{userEntityCamel}}Id,
152
+ );
153
+
154
+ ready = signal(false);
155
+ initialized = signal(false);
156
+ busyCount = signal(0);
157
+
158
+ sharedRoles = useSharedSignal("역할");
159
+
160
+ passwordPlaceholder = (): string =>
161
+ this.{{userEntityCamel}}Id() == null ? "비밀번호를 입력하세요." : "변경하려면 입력하세요.";
162
+
163
+ data = signal<IData>({});
164
+ private _orgData?: IData;
165
+
166
+ constructor() {
167
+ effect(() => {
168
+ this.{{userEntityCamel}}Id();
169
+
170
+ if (!this.perms().includes("use") || !this.ready()) {
171
+ this.initialized.set(true);
172
+ return;
173
+ }
174
+
175
+ void untracked(async () => {
176
+ this.busyCount.update((v) => v + 1);
177
+ await this._sdToast.try(async () => {
178
+ await this._refresh();
179
+ });
180
+ this.busyCount.update((v) => v - 1);
181
+ this.initialized.set(true);
182
+ });
183
+ });
184
+
185
+ setupCanDeactivate(() => this._checkIgnoreChanges());
186
+ }
187
+
188
+ async onSubmit(): Promise<void> {
189
+ if (this.busyCount() > 0) return;
190
+ if (!this.perms().includes("edit")) return;
191
+
192
+ const data = this.data();
193
+ if (this._orgData != null && obj.equal(data, this._orgData)) {
194
+ this._sdToast.info("변경사항이 없습니다.");
195
+ return;
196
+ }
197
+
198
+ this.busyCount.update((v) => v + 1);
199
+ await this._sdToast.try(async () => {
200
+ const [{ id }] = await this._appService.{{userEntityCamel}}.save([
201
+ {
202
+ id: this.{{userEntityCamel}}Id(),
203
+ name: data.name!.trim(),
204
+ loginId: data.loginId?.trim(),
205
+ roleId: data.roleId,
206
+ password: data.password,
207
+ },
208
+ ]);
209
+
210
+ await this._appSharedData.emitAsync("{{userEntityLabel}}", [id]);
211
+ this._sdToast.success("저장되었습니다.");
212
+ this.close.emit({ id });
213
+ });
214
+ this.busyCount.update((v) => v - 1);
215
+ }
216
+
217
+ async onDelete(): Promise<void> {
218
+ if (this.busyCount() > 0) return;
219
+ if (!this.perms().includes("edit")) return;
220
+
221
+ const {{userEntityCamel}}Id = this.{{userEntityCamel}}Id();
222
+ if ({{userEntityCamel}}Id == null) return;
223
+
224
+ if (!confirm("삭제하시겠습니까?")) return;
225
+
226
+ this.busyCount.update((v) => v + 1);
227
+ await this._sdToast.try(async () => {
228
+ const performerId = this._appAuth.authInfo()?.{{userEntityCamel}}Id;
229
+ await this._appOrm.connectAsync(async (db) => {
230
+ await db
231
+ .{{userEntityCamel}}()
232
+ .where((c) => [expr.eq(c.id, {{userEntityCamel}}Id)])
233
+ .update(() => ({ isDeleted: true }));
234
+ await db
235
+ .{{userEntityCamel}}()
236
+ .insertDataLog({ action: "삭제", itemId: {{userEntityCamel}}Id, {{userEntityCamel}}Id: performerId });
237
+ });
238
+
239
+ await this._appSharedData.emitAsync("{{userEntityLabel}}", [{{userEntityCamel}}Id]);
240
+ this._sdToast.success("삭제되었습니다.");
241
+ this.close.emit({ id: {{userEntityCamel}}Id });
242
+ });
243
+ this.busyCount.update((v) => v - 1);
244
+ }
245
+
246
+ async onRestore(): Promise<void> {
247
+ if (this.busyCount() > 0) return;
248
+ if (!this.perms().includes("edit")) return;
249
+
250
+ const {{userEntityCamel}}Id = this.{{userEntityCamel}}Id();
251
+ if ({{userEntityCamel}}Id == null) return;
252
+
253
+ const {{userEntityCamel}}Name = this.data().name;
254
+
255
+ this.busyCount.update((v) => v + 1);
256
+ await this._sdToast.try(async () => {
257
+ const performerId = this._appAuth.authInfo()?.{{userEntityCamel}}Id;
258
+ await this._appOrm.connectAsync(async (db) => {
259
+ // 복구 전 이름 비삭제 유니크 재검증 (삭제된 동안 같은 이름의 활성 {{userEntityLabel}}이 생겼을 수 있음).
260
+ const isNameDuplicated = await db
261
+ .{{userEntityCamel}}()
262
+ .where((c) => [
263
+ expr.eq(c.name, {{userEntityCamel}}Name),
264
+ expr.eq(c.isDeleted, false),
265
+ expr.not(expr.eq(c.id, {{userEntityCamel}}Id)),
266
+ ])
267
+ .exists();
268
+ if (isNameDuplicated) {
269
+ throw new Error("같은 이름의 활성 {{userEntityLabel}}이 있어 복구할 수 없습니다.");
270
+ }
271
+
272
+ await db
273
+ .{{userEntityCamel}}()
274
+ .where((c) => [expr.eq(c.id, {{userEntityCamel}}Id)])
275
+ .update(() => ({ isDeleted: false }));
276
+ await db
277
+ .{{userEntityCamel}}()
278
+ .insertDataLog({ action: "복구", itemId: {{userEntityCamel}}Id, {{userEntityCamel}}Id: performerId });
279
+ });
280
+
281
+ await this._appSharedData.emitAsync("{{userEntityLabel}}", [{{userEntityCamel}}Id]);
282
+ this._sdToast.success("복구되었습니다.");
283
+ await this._refresh();
284
+ });
285
+ this.busyCount.update((v) => v - 1);
286
+ }
287
+
288
+ private async _refresh(): Promise<void> {
289
+ const {{userEntityCamel}}Id = this.{{userEntityCamel}}Id();
290
+ if ({{userEntityCamel}}Id == null) {
291
+ this.data.set({});
292
+ this._orgData = obj.clone(this.data());
293
+ return;
294
+ }
295
+
296
+ const loaded = await this._appOrm.connectAsync(async (db) => {
297
+ return db
298
+ .{{userEntityCamel}}()
299
+ .where((c) => [expr.eq(c.id, {{userEntityCamel}}Id)])
300
+ .select((c) => ({
301
+ name: c.name,
302
+ loginId: c.loginId,
303
+ roleId: c.roleId,
304
+ isDeleted: c.isDeleted,
305
+ }))
306
+ .single();
307
+ });
308
+
309
+ if (loaded == null) throw new Error("{{userEntityLabel}}을 찾을 수 없습니다.");
310
+
311
+ this.data.set({
312
+ name: loaded.name,
313
+ loginId: loaded.loginId,
314
+ roleId: loaded.roleId ?? undefined,
315
+ isDeleted: loaded.isDeleted,
316
+ });
317
+ this._orgData = obj.clone(this.data());
318
+ }
319
+
320
+ private _checkIgnoreChanges(): boolean {
321
+ return (
322
+ this._orgData == null ||
323
+ obj.equal(this.data(), this._orgData) ||
324
+ confirm("변경사항이 있습니다. 무시하고 진행하시겠습니까?")
325
+ );
326
+ }
327
+
328
+ protected readonly mark = mark;
329
+ }
330
+
331
+ interface IData {
332
+ name?: string;
333
+ loginId?: string;
334
+ roleId?: number;
335
+ password?: string;
336
+ isDeleted?: boolean;
337
+ }