@simplysm/sd-cli 14.0.95 → 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.
- package/dist/commands/init/generators/client.d.ts.map +1 -1
- package/dist/commands/init/generators/client.js +9 -0
- package/dist/commands/init/generators/client.js.map +1 -1
- package/dist/commands/init/generators/server.d.ts.map +1 -1
- package/dist/commands/init/generators/server.js +1 -0
- package/dist/commands/init/generators/server.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/init/generators/client.ts +45 -0
- package/src/commands/init/generators/server.ts +5 -0
- package/src/commands/init/templates/client/src/app/home/home.view.ts.hbs +1 -1
- package/src/commands/init/templates/client/src/app/home/master/role-permission/role-permission.detail.ts.hbs +221 -0
- package/src/commands/init/templates/client/src/app/home/master/role-permission/role-permission.view.ts.hbs +106 -0
- package/src/commands/init/templates/client/src/app/home/master/role-permission/role.detail.ts.hbs +277 -0
- package/src/commands/init/templates/client/src/app/home/master/role-permission/role.list.ts.hbs +537 -0
- package/src/commands/init/templates/client/src/app/home/master/user.detail.ts.hbs +337 -0
- package/src/commands/init/templates/client/src/app/home/master/user.list.ts.hbs +540 -0
- package/src/commands/init/templates/client/src/app/home/my-info/my-info.detail.ts.hbs +4 -6
- package/src/commands/init/templates/client/src/app/home/system/data-log/data-log.list.ts.hbs +355 -0
- package/src/commands/init/templates/client/src/app/home/system/system-log/system-log.list.ts.hbs +382 -0
- package/src/commands/init/templates/client/src/app/login/login.view.ts.hbs +3 -4
- package/src/commands/init/templates/client/src/app.root.ts.hbs +9 -3
- package/src/commands/init/templates/client/src/index.html.hbs +1 -0
- package/src/commands/init/templates/client/src/main.ts.hbs +23 -18
- package/src/commands/init/templates/client/src/modals/text-view.modal.ts.hbs +30 -0
- package/src/commands/init/templates/client/src/routes.ts.hbs +22 -0
- package/src/commands/init/templates/client-common/src/index.ts.hbs +6 -4
- package/src/commands/init/templates/client-common/src/providers/app-auth.provider.ts.hbs +3 -3
- package/src/commands/init/templates/client-common/src/providers/app-orm.provider.ts.hbs +0 -11
- package/src/commands/init/templates/client-common/src/providers/app-service.provider.ts.hbs +24 -10
- package/src/commands/init/templates/client-common/src/providers/app-shared-data.provider.ts.hbs +2 -2
- package/src/commands/init/templates/common/package.json.hbs +2 -1
- package/src/commands/init/templates/common/src/app-structure.ts.hbs +7 -2
- package/src/commands/init/templates/common/src/auth-info-changed.event.ts.hbs +3 -1
- package/src/commands/init/templates/common/src/db/db-context.ts.hbs +2 -2
- package/src/commands/init/templates/common/src/db/tables/master/user.ts.hbs +2 -2
- package/src/commands/init/templates/common/src/db/tables/system/role.ts.hbs +1 -0
- package/src/commands/init/templates/common/src/index.ts.hbs +1 -1
- package/src/commands/init/templates/server/src/index.ts.hbs +3 -0
- package/src/commands/init/templates/server/src/main.ts.hbs +15 -1
- package/src/commands/init/templates/server/src/services/auth.service.ts.hbs +28 -22
- package/src/commands/init/templates/server/src/services/dev.service.ts.hbs +5 -5
- package/src/commands/init/templates/server/src/services/user.service.ts.hbs +191 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
effect,
|
|
6
|
+
inject,
|
|
7
|
+
signal,
|
|
8
|
+
untracked,
|
|
9
|
+
ViewEncapsulation,
|
|
10
|
+
} from "@angular/core";
|
|
11
|
+
import {
|
|
12
|
+
FormatPipe,
|
|
13
|
+
injectPermsSignal,
|
|
14
|
+
injectViewTitleSignal,
|
|
15
|
+
injectViewTypeSignal,
|
|
16
|
+
mark,
|
|
17
|
+
SdAnchor,
|
|
18
|
+
SdButton,
|
|
19
|
+
SdCheckbox,
|
|
20
|
+
SdCrudList,
|
|
21
|
+
SdItemOfTemplate,
|
|
22
|
+
SdModalProvider,
|
|
23
|
+
SdSharedDataSelect,
|
|
24
|
+
SdSheetColumn,
|
|
25
|
+
SdSheetColumnCellTemplate,
|
|
26
|
+
SdTextfield,
|
|
27
|
+
SdToastProvider,
|
|
28
|
+
type SortingDef,
|
|
29
|
+
} from "@simplysm/angular";
|
|
30
|
+
import { expr } from "@simplysm/orm-common";
|
|
31
|
+
import { DateTime, obj } from "@simplysm/core-common";
|
|
32
|
+
import { downloadBlob, openFileDialog } from "@simplysm/core-browser";
|
|
33
|
+
import { ExcelWrapper } from "@simplysm/excel";
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
import {
|
|
36
|
+
AppAuthProvider,
|
|
37
|
+
AppOrmProvider,
|
|
38
|
+
AppServiceProvider,
|
|
39
|
+
AppSharedDataProvider,
|
|
40
|
+
useSharedSignal,
|
|
41
|
+
} from "@{{workspaceName}}/client-common";
|
|
42
|
+
import { tablerDownload, tablerEdit, tablerUpload } from "@ng-icons/tabler-icons";
|
|
43
|
+
import { NgIcon } from "@ng-icons/core";
|
|
44
|
+
import { {{userEntityPascal}}Detail } from "./{{userEntityKebab}}.detail";
|
|
45
|
+
|
|
46
|
+
const ITEMS_PER_PAGE = 50;
|
|
47
|
+
|
|
48
|
+
@Component({
|
|
49
|
+
selector: "app-{{userEntityKebab}}-list",
|
|
50
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
51
|
+
encapsulation: ViewEncapsulation.None,
|
|
52
|
+
standalone: true,
|
|
53
|
+
imports: [
|
|
54
|
+
SdCrudList,
|
|
55
|
+
SdSheetColumn,
|
|
56
|
+
SdSheetColumnCellTemplate,
|
|
57
|
+
SdTextfield,
|
|
58
|
+
SdCheckbox,
|
|
59
|
+
SdAnchor,
|
|
60
|
+
SdButton,
|
|
61
|
+
SdSharedDataSelect,
|
|
62
|
+
SdItemOfTemplate,
|
|
63
|
+
NgIcon,
|
|
64
|
+
FormatPipe,
|
|
65
|
+
],
|
|
66
|
+
template: `
|
|
67
|
+
<sd-crud-list
|
|
68
|
+
[key]="'{{userEntityKebab}}-list'"
|
|
69
|
+
[(ready)]="ready"
|
|
70
|
+
[initialized]="initialized()"
|
|
71
|
+
[(busyCount)]="busyCount"
|
|
72
|
+
[viewType]="viewType()"
|
|
73
|
+
[restricted]="!perms().includes('use')"
|
|
74
|
+
[readonly]="!perms().includes('edit')"
|
|
75
|
+
[inlineEdit]="false"
|
|
76
|
+
[items]="items()"
|
|
77
|
+
[currDeletedItems]="deletedItems()"
|
|
78
|
+
[trackByFn]="trackByFn"
|
|
79
|
+
[getItemSelectableFn]="getItemSelectableFn"
|
|
80
|
+
[(selectedKeys)]="selectedKeys"
|
|
81
|
+
[(currentPage)]="page"
|
|
82
|
+
[totalPageCount]="pageLength()"
|
|
83
|
+
[(sorts)]="sortingDefs"
|
|
84
|
+
(filterSubmit)="onFilterSubmit()"
|
|
85
|
+
(create)="onCreate()"
|
|
86
|
+
(delete)="onDelete($event)"
|
|
87
|
+
(restore)="onRestore($event)"
|
|
88
|
+
>
|
|
89
|
+
<ng-template #filterTpl>
|
|
90
|
+
<div class="form-box-inline">
|
|
91
|
+
<div>
|
|
92
|
+
<label>검색어</label>
|
|
93
|
+
<sd-textfield
|
|
94
|
+
[type]="'text'"
|
|
95
|
+
[(value)]="filter().searchText"
|
|
96
|
+
(valueChange)="mark(filter)"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
<div>
|
|
100
|
+
<label>역할</label>
|
|
101
|
+
<sd-shared-data-select
|
|
102
|
+
[selectMode]="'multi'"
|
|
103
|
+
[items]="sharedRoles.items()"
|
|
104
|
+
[(value)]="filter().roleIds"
|
|
105
|
+
(valueChange)="mark(filter)"
|
|
106
|
+
>
|
|
107
|
+
<ng-template [itemOf]="sharedRoles.items()" let-item="item">
|
|
108
|
+
\{{ item.name }}
|
|
109
|
+
</ng-template>
|
|
110
|
+
</sd-shared-data-select>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="form-box-item">
|
|
113
|
+
<sd-checkbox [(value)]="filter().isIncludeDeleted" (valueChange)="mark(filter)">
|
|
114
|
+
삭제항목 포함
|
|
115
|
+
</sd-checkbox>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</ng-template>
|
|
119
|
+
|
|
120
|
+
<ng-template #toolTpl>
|
|
121
|
+
@if (perms().includes("edit")) {
|
|
122
|
+
<sd-button [size]="'sm'" [theme]="'link-success'" (click)="onUploadExcelButtonClick()">
|
|
123
|
+
<ng-icon [svg]="tablerUpload" />
|
|
124
|
+
엑셀 업로드
|
|
125
|
+
</sd-button>
|
|
126
|
+
}
|
|
127
|
+
<sd-button [size]="'sm'" [theme]="'link-success'" (click)="onDownloadExcelButtonClick()">
|
|
128
|
+
<ng-icon [svg]="tablerDownload" />
|
|
129
|
+
엑셀 다운로드
|
|
130
|
+
</sd-button>
|
|
131
|
+
</ng-template>
|
|
132
|
+
|
|
133
|
+
<sd-sheet-column [key]="'id'" [header]="'#'">
|
|
134
|
+
<ng-template [cell]="items()" let-item="item">
|
|
135
|
+
@if (perms().includes("edit")) {
|
|
136
|
+
<sd-anchor class="flex-row gap-sm p-xs-sm" (click)="onEdit(item, $event)">
|
|
137
|
+
<div><ng-icon [svg]="tablerEdit" /></div>
|
|
138
|
+
<div class="flex-fill tx-right">\{{ item.id }}</div>
|
|
139
|
+
</sd-anchor>
|
|
140
|
+
} @else {
|
|
141
|
+
<div class="p-xs-sm tx-right">\{{ item.id }}</div>
|
|
142
|
+
}
|
|
143
|
+
</ng-template>
|
|
144
|
+
</sd-sheet-column>
|
|
145
|
+
|
|
146
|
+
<sd-sheet-column [key]="'name'" [header]="'이름'">
|
|
147
|
+
<ng-template [cell]="items()" let-item="item">
|
|
148
|
+
<div class="p-xs-sm">\{{ item.name }}</div>
|
|
149
|
+
</ng-template>
|
|
150
|
+
</sd-sheet-column>
|
|
151
|
+
|
|
152
|
+
<sd-sheet-column [key]="'loginId'" [header]="'로그인ID'">
|
|
153
|
+
<ng-template [cell]="items()" let-item="item">
|
|
154
|
+
<div class="p-xs-sm">\{{ item.loginId }}</div>
|
|
155
|
+
</ng-template>
|
|
156
|
+
</sd-sheet-column>
|
|
157
|
+
|
|
158
|
+
<sd-sheet-column [key]="'roleName'" [header]="'역할'">
|
|
159
|
+
<ng-template [cell]="items()" let-item="item">
|
|
160
|
+
<div class="p-xs-sm">\{{ item.roleName ?? " " }}</div>
|
|
161
|
+
</ng-template>
|
|
162
|
+
</sd-sheet-column>
|
|
163
|
+
|
|
164
|
+
<sd-sheet-column [key]="'lastModifiedAt'" [header]="'수정일시'" [hidden]="true">
|
|
165
|
+
<ng-template [cell]="items()" let-item="item">
|
|
166
|
+
<div class="p-xs-sm">\{{ item.lastModifiedAt | format: "yyyy-MM-dd HH:mm" }}</div>
|
|
167
|
+
</ng-template>
|
|
168
|
+
</sd-sheet-column>
|
|
169
|
+
|
|
170
|
+
<sd-sheet-column [key]="'lastModifiedBy'" [header]="'수정자'" [hidden]="true">
|
|
171
|
+
<ng-template [cell]="items()" let-item="item">
|
|
172
|
+
<div class="p-xs-sm">\{{ item.lastModifiedBy ?? " " }}</div>
|
|
173
|
+
</ng-template>
|
|
174
|
+
</sd-sheet-column>
|
|
175
|
+
</sd-crud-list>
|
|
176
|
+
`,
|
|
177
|
+
})
|
|
178
|
+
export class {{userEntityPascal}}List {
|
|
179
|
+
private readonly _sdToast = inject(SdToastProvider);
|
|
180
|
+
private readonly _sdModal = inject(SdModalProvider);
|
|
181
|
+
private readonly _appOrm = inject(AppOrmProvider);
|
|
182
|
+
private readonly _appService = inject(AppServiceProvider);
|
|
183
|
+
private readonly _appSharedData = inject(AppSharedDataProvider);
|
|
184
|
+
private readonly _appAuth = inject(AppAuthProvider);
|
|
185
|
+
|
|
186
|
+
perms = injectPermsSignal(["master.{{userEntityKebab}}"], ["use", "edit"]);
|
|
187
|
+
viewType = injectViewTypeSignal();
|
|
188
|
+
viewTitle = injectViewTitleSignal();
|
|
189
|
+
|
|
190
|
+
ready = signal(false);
|
|
191
|
+
initialized = signal(false);
|
|
192
|
+
busyCount = signal(0);
|
|
193
|
+
|
|
194
|
+
selectedKeys = signal<number[]>([]);
|
|
195
|
+
|
|
196
|
+
sharedRoles = useSharedSignal("역할");
|
|
197
|
+
|
|
198
|
+
items = signal<IItem[]>([]);
|
|
199
|
+
deletedItems = computed(() => this.items().filter((i) => i.isDeleted));
|
|
200
|
+
page = signal(0);
|
|
201
|
+
pageLength = signal(0);
|
|
202
|
+
sortingDefs = signal<SortingDef[]>([]);
|
|
203
|
+
|
|
204
|
+
filter = signal<IFilter>({ isIncludeDeleted: false, roleIds: [] });
|
|
205
|
+
lastFilter = signal<IFilter>({ isIncludeDeleted: false, roleIds: [] });
|
|
206
|
+
|
|
207
|
+
trackByFn = (item: IItem): number => item.id;
|
|
208
|
+
|
|
209
|
+
// 로그인한 본인 계정은 선택(→삭제) 불가 — 해당 행 체크박스 비활성화.
|
|
210
|
+
getItemSelectableFn = (item: IItem): boolean | string =>
|
|
211
|
+
item.id === this._appAuth.authInfo()?.{{userEntityCamel}}Id ? "본인 계정은 삭제할 수 없습니다." : true;
|
|
212
|
+
|
|
213
|
+
private readonly _excelWrapper = new ExcelWrapper(
|
|
214
|
+
z.object({
|
|
215
|
+
id: z.number().optional().describe("ID"),
|
|
216
|
+
name: z.string().describe("이름"),
|
|
217
|
+
loginId: z.string().optional().describe("로그인ID"),
|
|
218
|
+
roleName: z.string().optional().describe("역할"),
|
|
219
|
+
password: z.string().optional().describe("비밀번호"),
|
|
220
|
+
isDeleted: z.boolean().describe("삭제"),
|
|
221
|
+
lastModifiedAt: z.custom<DateTime>().optional().describe("수정일시"),
|
|
222
|
+
lastModifiedBy: z.string().optional().describe("수정자"),
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
constructor() {
|
|
227
|
+
effect(() => {
|
|
228
|
+
if (!this.perms().includes("use") || !this.ready()) {
|
|
229
|
+
this.initialized.set(true);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.lastFilter();
|
|
234
|
+
this.page();
|
|
235
|
+
this.sortingDefs();
|
|
236
|
+
|
|
237
|
+
void untracked(async () => {
|
|
238
|
+
this.busyCount.update((v) => v + 1);
|
|
239
|
+
await this._sdToast.try(async () => {
|
|
240
|
+
await this._refresh();
|
|
241
|
+
});
|
|
242
|
+
this.busyCount.update((v) => v - 1);
|
|
243
|
+
this.initialized.set(true);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
onFilterSubmit(): void {
|
|
249
|
+
this.page.set(0);
|
|
250
|
+
this.lastFilter.set({ ...this.filter() });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async onDownloadExcelButtonClick(): Promise<void> {
|
|
254
|
+
if (this.busyCount() > 0) return;
|
|
255
|
+
|
|
256
|
+
this.busyCount.update((v) => v + 1);
|
|
257
|
+
await this._sdToast.try(async () => {
|
|
258
|
+
const r = await this._search(false);
|
|
259
|
+
const wb = await this._excelWrapper.write(this.viewTitle(), r.items);
|
|
260
|
+
try {
|
|
261
|
+
downloadBlob(
|
|
262
|
+
await wb.toBlob(),
|
|
263
|
+
`${this.viewTitle()}_${new DateTime().toFormatString("yyMMdd")}.xlsx`,
|
|
264
|
+
);
|
|
265
|
+
} finally {
|
|
266
|
+
await wb.close();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
this.busyCount.update((v) => v - 1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async onUploadExcelButtonClick(): Promise<void> {
|
|
273
|
+
if (this.busyCount() > 0) return;
|
|
274
|
+
if (!this.perms().includes("edit")) return;
|
|
275
|
+
|
|
276
|
+
const files = await openFileDialog({ accept: ".xlsx" });
|
|
277
|
+
if (files == null) return;
|
|
278
|
+
|
|
279
|
+
this.busyCount.update((v) => v + 1);
|
|
280
|
+
await this._sdToast.try(async () => {
|
|
281
|
+
// 파생 컬럼(수정일시·수정자)은 업로드에서 제외하고 파싱. 삭제 여부는 읽어서 반영.
|
|
282
|
+
const records = await this._excelWrapper.read(files[0], 0, {
|
|
283
|
+
excludes: ["lastModifiedAt", "lastModifiedBy"],
|
|
284
|
+
});
|
|
285
|
+
if (records.length === 0) {
|
|
286
|
+
throw new Error("업로드할 데이터가 없습니다.");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 다건일 때만 에러에 이름을 표기(단건이면 평문).
|
|
290
|
+
const label = ({{userEntityCamel}}Name: string): string =>
|
|
291
|
+
records.length > 1 ? `{{userEntityLabel}} '${ {{~userEntityCamel}}Name}': ` : "";
|
|
292
|
+
|
|
293
|
+
// 역할명 → 역할ID (활성 역할 기준). 없는 역할명은 오류.
|
|
294
|
+
const roleIdByName = new Map(this.sharedRoles.items().map((r) => [r.name, r.id] as const));
|
|
295
|
+
const inputs = records.map((rec) => {
|
|
296
|
+
const roleName = rec.roleName?.trim();
|
|
297
|
+
let roleId: number | undefined;
|
|
298
|
+
if (roleName != null && roleName !== "") {
|
|
299
|
+
roleId = roleIdByName.get(roleName);
|
|
300
|
+
if (roleId == null) {
|
|
301
|
+
throw new Error(`${label(rec.name)}존재하지 않는 역할입니다. (${roleName})`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
id: rec.id,
|
|
306
|
+
name: rec.name,
|
|
307
|
+
loginId: rec.loginId,
|
|
308
|
+
roleId,
|
|
309
|
+
password: rec.password,
|
|
310
|
+
isDeleted: rec.isDeleted,
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ID↔이름 정합성 — 전체(삭제 포함) {{userEntityLabel}}의 (id, name) 와 대조. ID 없으면 신규.
|
|
315
|
+
const ids = inputs.flatMap((x) => (x.id != null ? [x.id] : []));
|
|
316
|
+
const dbRows =
|
|
317
|
+
ids.length === 0
|
|
318
|
+
? []
|
|
319
|
+
: await this._appOrm.connectAsync(async (db) =>
|
|
320
|
+
db
|
|
321
|
+
.{{userEntityCamel}}()
|
|
322
|
+
.where((c) => [expr.in(c.id, ids)])
|
|
323
|
+
.select((c) => ({ id: c.id, name: c.name }))
|
|
324
|
+
.execute(),
|
|
325
|
+
);
|
|
326
|
+
const dbNameById = new Map(dbRows.map((r) => [r.id, r.name] as const));
|
|
327
|
+
|
|
328
|
+
let nameChangedCount = 0;
|
|
329
|
+
const changedSamples: string[] = [];
|
|
330
|
+
for (const x of inputs) {
|
|
331
|
+
if (x.id == null) continue;
|
|
332
|
+
const dbName = dbNameById.get(x.id);
|
|
333
|
+
if (dbName == null) {
|
|
334
|
+
throw new Error(`${label(x.name)}ID ${x.id} 에 해당하는 {{userEntityLabel}}이 없습니다.`);
|
|
335
|
+
}
|
|
336
|
+
if (dbName !== x.name.trim()) {
|
|
337
|
+
nameChangedCount++;
|
|
338
|
+
if (changedSamples.length < 2) {
|
|
339
|
+
changedSamples.push(`${dbName} → ${x.name.trim()}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 기존 이름(비즈니스키)이 바뀌는 건이 있으면 행 어긋남(정렬 사고)일 수 있어, 건수 입력으로 확인.
|
|
345
|
+
if (nameChangedCount >= 1) {
|
|
346
|
+
const sample =
|
|
347
|
+
changedSamples.join(", ") + (nameChangedCount > changedSamples.length ? " 등" : "");
|
|
348
|
+
let confirmed = false;
|
|
349
|
+
while (!confirmed) {
|
|
350
|
+
const answer = prompt(
|
|
351
|
+
`기존 항목 ${nameChangedCount}건의 이름이 변경됩니다. (예: ${sample})\n` +
|
|
352
|
+
`계속하려면 ${nameChangedCount} 을(를) 입력하세요.`,
|
|
353
|
+
);
|
|
354
|
+
if (answer == null) return;
|
|
355
|
+
confirmed = answer.trim() === String(nameChangedCount);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const results = await this._appService.{{userEntityCamel}}.save(inputs);
|
|
360
|
+
await this._appSharedData.emitAsync(
|
|
361
|
+
"{{userEntityLabel}}",
|
|
362
|
+
results.map((r) => r.id),
|
|
363
|
+
);
|
|
364
|
+
this._sdToast.success(`${results.length}건이 반영되었습니다.`);
|
|
365
|
+
await this._refresh();
|
|
366
|
+
});
|
|
367
|
+
this.busyCount.update((v) => v - 1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async onCreate(): Promise<void> {
|
|
371
|
+
await this._openDetail(undefined);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async onEdit(item: IItem, event: Event): Promise<void> {
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
event.stopPropagation();
|
|
377
|
+
|
|
378
|
+
await this._openDetail(item.id);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private async _openDetail({{userEntityCamel}}Id: number | undefined): Promise<void> {
|
|
382
|
+
if (this.busyCount() > 0) return;
|
|
383
|
+
|
|
384
|
+
const result = await this._sdModal.showAsync({
|
|
385
|
+
type: {{userEntityPascal}}Detail,
|
|
386
|
+
title: {{userEntityCamel}}Id == null ? "{{userEntityLabel}} 등록" : "{{userEntityLabel}} 수정",
|
|
387
|
+
inputs: { {{userEntityCamel}}Id },
|
|
388
|
+
});
|
|
389
|
+
if (result == null) return;
|
|
390
|
+
|
|
391
|
+
this.busyCount.update((v) => v + 1);
|
|
392
|
+
await this._sdToast.try(async () => {
|
|
393
|
+
await this._refresh();
|
|
394
|
+
});
|
|
395
|
+
this.busyCount.update((v) => v - 1);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async onDelete(targets: IItem[]): Promise<void> {
|
|
399
|
+
if (this.busyCount() > 0) return;
|
|
400
|
+
if (!this.perms().includes("edit")) return;
|
|
401
|
+
if (targets.length === 0) return;
|
|
402
|
+
if (!confirm("삭제하시겠습니까?")) return;
|
|
403
|
+
|
|
404
|
+
this.busyCount.update((v) => v + 1);
|
|
405
|
+
await this._sdToast.try(async () => {
|
|
406
|
+
const ids = targets.map((t) => t.id);
|
|
407
|
+
const {{userEntityCamel}}Id = this._appAuth.authInfo()?.{{userEntityCamel}}Id;
|
|
408
|
+
await this._appOrm.connectAsync(async (db) => {
|
|
409
|
+
await db
|
|
410
|
+
.{{userEntityCamel}}()
|
|
411
|
+
.where((c) => [expr.in(c.id, ids)])
|
|
412
|
+
.update(() => ({ isDeleted: true }));
|
|
413
|
+
for (const id of ids) {
|
|
414
|
+
await db.{{userEntityCamel}}().insertDataLog({ action: "삭제", itemId: id, {{userEntityCamel}}Id });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
await this._appSharedData.emitAsync("{{userEntityLabel}}", ids);
|
|
418
|
+
this._sdToast.success("삭제되었습니다.");
|
|
419
|
+
await this._refresh();
|
|
420
|
+
});
|
|
421
|
+
this.busyCount.update((v) => v - 1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async onRestore(targets: IItem[]): Promise<void> {
|
|
425
|
+
if (this.busyCount() > 0) return;
|
|
426
|
+
if (!this.perms().includes("edit")) return;
|
|
427
|
+
if (targets.length === 0) return;
|
|
428
|
+
|
|
429
|
+
this.busyCount.update((v) => v + 1);
|
|
430
|
+
await this._sdToast.try(async () => {
|
|
431
|
+
const ids = targets.map((t) => t.id);
|
|
432
|
+
const names = targets.map((t) => t.name);
|
|
433
|
+
const {{userEntityCamel}}Id = this._appAuth.authInfo()?.{{userEntityCamel}}Id;
|
|
434
|
+
await this._appOrm.connectAsync(async (db) => {
|
|
435
|
+
await db
|
|
436
|
+
.{{userEntityCamel}}()
|
|
437
|
+
.where((c) => [expr.in(c.id, ids)])
|
|
438
|
+
.update(() => ({ isDeleted: false }));
|
|
439
|
+
|
|
440
|
+
// 복구 후 이름 비삭제 유니크 재검증 — 복구로 활성 중복이 생겼으면 throw(트랜잭션 롤백).
|
|
441
|
+
// 대상끼리 충돌·기존 활성과의 충돌을 복구한 이름으로 한정해 한 번에 검사.
|
|
442
|
+
const conflicts = await db
|
|
443
|
+
.{{userEntityCamel}}()
|
|
444
|
+
.where((c) => [expr.in(c.name, names), expr.eq(c.isDeleted, false)])
|
|
445
|
+
.groupBy((c) => [c.name])
|
|
446
|
+
.having(() => [expr.gt(expr.count(), 1)])
|
|
447
|
+
.select((c) => ({ name: c.name }))
|
|
448
|
+
.execute();
|
|
449
|
+
if (conflicts.length > 0) {
|
|
450
|
+
const conflictNames = conflicts.map((x) => `'${x.name}'`).join(", ");
|
|
451
|
+
throw new Error(`같은 이름(${conflictNames})의 활성 {{userEntityLabel}}이 있어 복구할 수 없습니다.`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const id of ids) {
|
|
455
|
+
await db.{{userEntityCamel}}().insertDataLog({ action: "복구", itemId: id, {{userEntityCamel}}Id });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
await this._appSharedData.emitAsync("{{userEntityLabel}}", ids);
|
|
459
|
+
this._sdToast.success("복구되었습니다.");
|
|
460
|
+
await this._refresh();
|
|
461
|
+
});
|
|
462
|
+
this.busyCount.update((v) => v - 1);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private async _refresh(): Promise<void> {
|
|
466
|
+
const r = await this._search(true);
|
|
467
|
+
this.items.set(r.items);
|
|
468
|
+
this.pageLength.set(r.pageLength);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private async _search(usePagination: boolean): Promise<{ items: IItem[]; pageLength: number }> {
|
|
472
|
+
return this._appOrm.connectAsync(async (db) => {
|
|
473
|
+
let qr = db.{{userEntityCamel}}();
|
|
474
|
+
|
|
475
|
+
const searchText = this.lastFilter().searchText?.trim();
|
|
476
|
+
if (searchText != null && searchText.length > 0) {
|
|
477
|
+
qr = qr.search((c) => [c.name, c.loginId], searchText);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const roleIds = this.lastFilter().roleIds;
|
|
481
|
+
if (roleIds != null && roleIds.length > 0) {
|
|
482
|
+
qr = qr.where((c) => [expr.in(c.roleId, roleIds)]);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!this.lastFilter().isIncludeDeleted) {
|
|
486
|
+
qr = qr.where((c) => [expr.eq(c.isDeleted, false)]);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const pageLength = usePagination ? Math.ceil((await qr.count()) / ITEMS_PER_PAGE) : 0;
|
|
490
|
+
|
|
491
|
+
let qr2 = qr
|
|
492
|
+
.joinLastDataLog()
|
|
493
|
+
.include((c) => c.role)
|
|
494
|
+
.select((c) => ({
|
|
495
|
+
id: c.id,
|
|
496
|
+
name: c.name,
|
|
497
|
+
loginId: c.loginId,
|
|
498
|
+
roleName: c.role?.name,
|
|
499
|
+
isDeleted: c.isDeleted,
|
|
500
|
+
lastModifiedAt: c.lastDataLog?.dateTime,
|
|
501
|
+
lastModifiedBy: c.lastDataLog?.{{userEntityCamel}}Name,
|
|
502
|
+
}));
|
|
503
|
+
|
|
504
|
+
for (const sort of this.sortingDefs()) {
|
|
505
|
+
qr2 = qr2.orderBy((c) => obj.getChainValue(c, sort.key) as any, sort.desc ? "DESC" : "ASC");
|
|
506
|
+
}
|
|
507
|
+
if (!this.sortingDefs().some((s) => s.key === "id")) {
|
|
508
|
+
qr2 = qr2.orderBy((c) => c.id, "DESC");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (usePagination) {
|
|
512
|
+
qr2 = qr2.limit(this.page() * ITEMS_PER_PAGE, ITEMS_PER_PAGE);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const items = (await qr2.execute()) as IItem[];
|
|
516
|
+
return { items, pageLength };
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
protected readonly mark = mark;
|
|
521
|
+
protected readonly tablerEdit = tablerEdit;
|
|
522
|
+
protected readonly tablerDownload = tablerDownload;
|
|
523
|
+
protected readonly tablerUpload = tablerUpload;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
interface IItem {
|
|
527
|
+
id: number;
|
|
528
|
+
name: string;
|
|
529
|
+
loginId?: string;
|
|
530
|
+
roleName?: string;
|
|
531
|
+
isDeleted: boolean;
|
|
532
|
+
lastModifiedAt?: DateTime;
|
|
533
|
+
lastModifiedBy?: string;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
interface IFilter {
|
|
537
|
+
searchText?: string;
|
|
538
|
+
roleIds?: number[];
|
|
539
|
+
isIncludeDeleted: boolean;
|
|
540
|
+
}
|
|
@@ -150,7 +150,7 @@ export class MyInfoDetail {
|
|
|
150
150
|
|
|
151
151
|
flatMenus = computed(() => this._sdAppStructure.usableFlatMenus());
|
|
152
152
|
|
|
153
|
-
data = signal<IData>({
|
|
153
|
+
data = signal<IData>({ configRecord: {} });
|
|
154
154
|
private _orgData?: IData;
|
|
155
155
|
|
|
156
156
|
constructor() {
|
|
@@ -223,9 +223,7 @@ export class MyInfoDetail {
|
|
|
223
223
|
.single();
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
-
if (loaded == null) {
|
|
227
|
-
throw new Error("{{userEntityLabel}} 정보를 찾을 수 없습니다.");
|
|
228
|
-
}
|
|
226
|
+
if (loaded == null) throw new Error("{{userEntityLabel}} 정보를 찾을 수 없습니다.");
|
|
229
227
|
|
|
230
228
|
const configRecord = (loaded.configs ?? []).toObject(
|
|
231
229
|
(c) => c.code,
|
|
@@ -254,8 +252,8 @@ export class MyInfoDetail {
|
|
|
254
252
|
}
|
|
255
253
|
|
|
256
254
|
interface IData {
|
|
257
|
-
name
|
|
258
|
-
loginId
|
|
255
|
+
name?: string;
|
|
256
|
+
loginId?: string;
|
|
259
257
|
configRecord: I{{userEntityPascal}}ConfigMap;
|
|
260
258
|
currentPassword?: string;
|
|
261
259
|
newPassword?: string;
|