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