sonamu 0.7.21 → 0.7.23
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/ai/agents/agent.d.ts +6 -1
- package/dist/ai/agents/agent.d.ts.map +1 -1
- package/dist/ai/agents/agent.js +20 -5
- package/dist/api/base-frame.d.ts +4 -0
- package/dist/api/base-frame.d.ts.map +1 -1
- package/dist/api/base-frame.js +9 -1
- package/dist/api/caster.d.ts.map +1 -1
- package/dist/api/caster.js +2 -2
- package/dist/api/config.d.ts +35 -3
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/decorators.d.ts +4 -4
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +80 -18
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -1
- package/dist/api/secret.d.ts +7 -0
- package/dist/api/secret.d.ts.map +1 -0
- package/dist/api/secret.js +17 -0
- package/dist/api/sonamu.d.ts +17 -8
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +265 -47
- package/dist/cache/cache-manager.d.ts +11 -0
- package/dist/cache/cache-manager.d.ts.map +1 -0
- package/dist/cache/cache-manager.js +22 -0
- package/dist/cache/decorator.d.ts +31 -0
- package/dist/cache/decorator.d.ts.map +1 -0
- package/dist/cache/decorator.js +86 -0
- package/dist/cache/drivers.d.ts +33 -0
- package/dist/cache/drivers.d.ts.map +1 -0
- package/dist/cache/drivers.js +36 -0
- package/dist/cache/index.d.ts +4 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/types.d.ts +28 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +6 -0
- package/dist/database/base-model.d.ts +4 -2
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +9 -4
- package/dist/database/code-generator.d.ts +3 -1
- package/dist/database/code-generator.d.ts.map +1 -1
- package/dist/database/code-generator.js +3 -2
- package/dist/database/db.d.ts +1 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +5 -5
- package/dist/database/knex.d.ts +3 -0
- package/dist/database/knex.d.ts.map +1 -0
- package/dist/database/knex.js +29 -0
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +1 -1
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +49 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/logger/category.d.ts +4 -0
- package/dist/logger/category.d.ts.map +1 -0
- package/dist/logger/category.js +34 -0
- package/dist/logger/configure.d.ts +9 -0
- package/dist/logger/configure.d.ts.map +1 -0
- package/dist/logger/configure.js +115 -0
- package/dist/migration/code-generation.d.ts +5 -1
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +13 -7
- package/dist/migration/migrator.d.ts +1 -1
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +7 -7
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +5 -3
- package/dist/naite/naite.d.ts +0 -4
- package/dist/naite/naite.d.ts.map +1 -1
- package/dist/naite/naite.js +11 -19
- package/dist/ssr/index.d.ts +4 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/index.js +4 -0
- package/dist/ssr/registry.d.ts +10 -0
- package/dist/ssr/registry.d.ts.map +1 -0
- package/dist/ssr/registry.js +43 -0
- package/dist/ssr/renderer.d.ts +6 -0
- package/dist/ssr/renderer.d.ts.map +1 -0
- package/dist/ssr/renderer.js +70 -0
- package/dist/ssr/types.d.ts +19 -0
- package/dist/ssr/types.d.ts.map +1 -0
- package/dist/ssr/types.js +4 -0
- package/dist/syncer/syncer.d.ts +1 -0
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +58 -1
- package/dist/tasks/decorator.d.ts +1 -0
- package/dist/tasks/decorator.d.ts.map +1 -1
- package/dist/tasks/decorator.js +9 -7
- package/dist/tasks/step-wrapper.d.ts +5 -0
- package/dist/tasks/step-wrapper.d.ts.map +1 -1
- package/dist/tasks/step-wrapper.js +11 -6
- package/dist/tasks/workflow-manager.d.ts +2 -0
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.js +5 -2
- package/dist/template/implementations/entry-server.template.d.ts +17 -0
- package/dist/template/implementations/entry-server.template.d.ts.map +1 -0
- package/dist/template/implementations/entry-server.template.js +78 -0
- package/dist/template/implementations/model.template.d.ts.map +1 -1
- package/dist/template/implementations/model.template.js +5 -3
- package/dist/template/implementations/queries.template.d.ts +17 -0
- package/dist/template/implementations/queries.template.d.ts.map +1 -0
- package/dist/template/implementations/queries.template.js +83 -0
- package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -1
- package/dist/template/implementations/view_enums_select.template.js +34 -20
- package/dist/template/implementations/view_form.template.d.ts +2 -1
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.js +301 -129
- package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -1
- package/dist/template/implementations/view_id_async_select.template.js +136 -57
- package/dist/template/implementations/view_list.template.d.ts +2 -0
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +392 -227
- package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
- package/dist/template/implementations/view_search_input.template.js +46 -30
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +2 -2
- package/dist/testing/bootstrap.d.ts +28 -0
- package/dist/testing/bootstrap.d.ts.map +1 -0
- package/dist/testing/bootstrap.js +120 -0
- package/dist/testing/fixture-loader.d.ts +21 -0
- package/dist/testing/fixture-loader.d.ts.map +1 -0
- package/dist/testing/fixture-loader.js +28 -0
- package/dist/testing/fixture-manager.d.ts +1 -1
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +7 -7
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/naite-vitest-reporter.d.ts +12 -0
- package/dist/testing/naite-vitest-reporter.d.ts.map +1 -0
- package/dist/testing/naite-vitest-reporter.js +17 -0
- package/dist/types/types.d.ts +5 -6
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +7 -8
- package/dist/ui/ai-client.d.ts +3 -1
- package/dist/ui/ai-client.d.ts.map +1 -1
- package/dist/ui/ai-client.js +27 -8
- package/dist/ui-web/assets/index-CTYv3qL6.js +92 -0
- package/dist/ui-web/index.html +1 -1
- package/package.json +43 -20
- package/src/ai/agents/agent.ts +38 -19
- package/src/api/base-frame.ts +8 -0
- package/src/api/caster.ts +6 -1
- package/src/api/config.ts +38 -4
- package/src/api/decorators.ts +106 -20
- package/src/api/index.ts +1 -0
- package/src/api/secret.ts +23 -0
- package/src/api/sonamu.ts +334 -61
- package/src/cache/cache-manager.ts +23 -0
- package/src/cache/decorator.ts +116 -0
- package/src/cache/drivers.ts +42 -0
- package/src/cache/index.ts +16 -0
- package/src/cache/types.ts +32 -0
- package/src/database/base-model.ts +7 -3
- package/src/database/code-generator.ts +3 -1
- package/src/database/db.ts +5 -5
- package/src/database/knex.ts +34 -0
- package/src/database/puri.types.ts +2 -3
- package/src/database/upsert-builder.ts +58 -4
- package/src/index.ts +4 -0
- package/src/logger/category.ts +42 -0
- package/src/logger/configure.ts +132 -0
- package/src/migration/code-generation.ts +19 -6
- package/src/migration/migrator.ts +7 -6
- package/src/migration/postgresql-schema-reader.ts +7 -2
- package/src/naite/naite.ts +10 -18
- package/src/shared/web.shared.ts.txt +1 -1
- package/src/ssr/index.ts +13 -0
- package/src/ssr/registry.ts +52 -0
- package/src/ssr/renderer.ts +105 -0
- package/src/ssr/types.ts +20 -0
- package/src/syncer/syncer.ts +59 -0
- package/src/tasks/decorator.ts +20 -4
- package/src/tasks/step-wrapper.ts +14 -5
- package/src/tasks/workflow-manager.ts +9 -1
- package/src/template/implementations/entry-server.template.ts +81 -0
- package/src/template/implementations/model.template.ts +4 -2
- package/src/template/implementations/queries.template.ts +111 -0
- package/src/template/implementations/view_enums_select.template.ts +33 -19
- package/src/template/implementations/view_form.template.ts +324 -145
- package/src/template/implementations/view_id_async_select.template.ts +145 -56
- package/src/template/implementations/view_list.template.ts +446 -236
- package/src/template/implementations/view_search_input.template.ts +45 -29
- package/src/template/zod-converter.ts +4 -1
- package/src/testing/bootstrap.ts +176 -0
- package/src/testing/fixture-loader.ts +28 -0
- package/src/testing/fixture-manager.ts +7 -6
- package/src/testing/index.ts +3 -0
- package/src/testing/naite-vitest-reporter.ts +18 -0
- package/src/types/types.ts +4 -5
- package/src/ui/ai-client.ts +82 -50
- package/dist/template/implementations/view_enums_dropdown.template.d.ts +0 -17
- package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +0 -1
- package/dist/template/implementations/view_enums_dropdown.template.js +0 -50
- package/dist/ui-web/assets/index-B87IyofX.js +0 -92
- package/src/template/implementations/view_enums_dropdown.template.ts +0 -53
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import assert from "assert";
|
|
2
1
|
import inflection from "inflection";
|
|
3
|
-
import { flat
|
|
2
|
+
import { flat } from "radashi";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { EntityManager, type EntityNamesRecord } from "../../entity/entity-manager";
|
|
6
5
|
import type { RenderingNode, TemplateKey, TemplateOptions } from "../../types/types";
|
|
7
|
-
import { getColumnsNode } from "../entity-converter";
|
|
8
6
|
import { getEnumInfoFromColName, getRelationPropFromColName } from "../helpers";
|
|
9
7
|
import type { RenderedTemplate } from "../template";
|
|
10
8
|
import { Template } from "../template";
|
|
11
|
-
import { getZodTypeById, zodTypeToRenderingNode } from "../zod-converter";
|
|
12
9
|
|
|
13
10
|
export class Template__view_list extends Template {
|
|
14
11
|
constructor() {
|
|
@@ -17,7 +14,7 @@ export class Template__view_list extends Template {
|
|
|
17
14
|
|
|
18
15
|
getTargetAndPath(names: EntityNamesRecord) {
|
|
19
16
|
return {
|
|
20
|
-
target: "web/src/
|
|
17
|
+
target: "web/src/routes/admin",
|
|
21
18
|
path: `${names.fsPlural}/index.tsx`,
|
|
22
19
|
};
|
|
23
20
|
}
|
|
@@ -35,7 +32,17 @@ export class Template__view_list extends Template {
|
|
|
35
32
|
parentObj: string = "row",
|
|
36
33
|
withoutName: boolean = false,
|
|
37
34
|
): string {
|
|
38
|
-
|
|
35
|
+
// 중첩 경로 처리 (예: "user.name" -> "row.user?.name")
|
|
36
|
+
let colName: string;
|
|
37
|
+
if (withoutName) {
|
|
38
|
+
colName = parentObj;
|
|
39
|
+
} else if (col.name.includes(".")) {
|
|
40
|
+
// 중첩 경로는 optional chaining으로 변환
|
|
41
|
+
const parts = col.name.split(".");
|
|
42
|
+
colName = `${parentObj}.${parts.join("?.")}`;
|
|
43
|
+
} else {
|
|
44
|
+
colName = `${parentObj}.${col.name}`;
|
|
45
|
+
}
|
|
39
46
|
|
|
40
47
|
switch (col.renderType) {
|
|
41
48
|
case "string-plain":
|
|
@@ -43,35 +50,50 @@ export class Template__view_list extends Template {
|
|
|
43
50
|
case "number-id":
|
|
44
51
|
return `<>{${colName}}</>`;
|
|
45
52
|
case "number-fk_id": {
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
try {
|
|
54
|
+
const baseName = col.name.includes(".")
|
|
55
|
+
? (col.name.split(".").pop() ?? col.name).replace("_id", "")
|
|
56
|
+
: col.name.replace("_id", "");
|
|
57
|
+
const relPropFk = getRelationPropFromColName(entityId, baseName);
|
|
58
|
+
return `<>${relPropFk.with}#{${colName}}</>`;
|
|
59
|
+
} catch {
|
|
60
|
+
return `<>{${colName}}</>`;
|
|
61
|
+
}
|
|
48
62
|
}
|
|
49
63
|
case "string-image":
|
|
50
|
-
return `<>{${
|
|
64
|
+
return `<>{${
|
|
65
|
+
col.nullable ? `${colName} && ` : ""
|
|
66
|
+
}<img src={${colName}} alt="${col.label ?? col.name}" className="h-8 w-8 object-cover rounded" />}</>`;
|
|
51
67
|
case "datetime":
|
|
52
|
-
if (col.nullable) {
|
|
53
|
-
return `<span
|
|
68
|
+
if (col.nullable || col.name.includes(".")) {
|
|
69
|
+
return `<span>{${colName} ? datetimeF(${colName}) : '-'}</span>`;
|
|
54
70
|
} else {
|
|
55
|
-
return `<span
|
|
71
|
+
return `<span>{datetimeF(${colName})}</span>`;
|
|
56
72
|
}
|
|
57
73
|
case "string-datetime":
|
|
58
|
-
if (col.nullable) {
|
|
59
|
-
return `<span
|
|
74
|
+
if (col.nullable || col.name.includes(".")) {
|
|
75
|
+
return `<span>{${colName} ? dateF(${colName}) : '-'}</span>`;
|
|
60
76
|
} else {
|
|
61
|
-
return `<span
|
|
77
|
+
return `<span>{dateF(${colName})}</span>`;
|
|
62
78
|
}
|
|
63
79
|
case "boolean":
|
|
64
|
-
return `<>{${colName} ? <
|
|
80
|
+
return `<>{${colName} ? <Badge variant="default">O</Badge> : <Badge variant="secondary">X</Badge>}</>`;
|
|
65
81
|
case "enums": {
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
try {
|
|
83
|
+
const { id: enumId } = getEnumInfoFromColName(entityId, col.name);
|
|
84
|
+
return `<>{${col.nullable ? `${colName} && ` : ""}${enumId}Label[${colName}]}</>`;
|
|
85
|
+
} catch {
|
|
86
|
+
return `<>{${colName}}</>`;
|
|
87
|
+
}
|
|
68
88
|
}
|
|
69
89
|
case "array-images":
|
|
70
|
-
return
|
|
90
|
+
return `<div className="flex gap-1">{ ${colName}?.map((r, i) => ${
|
|
91
|
+
col.nullable ? `r && ` : ""
|
|
92
|
+
}<img key={i} src={r} alt={\`${col.label ?? col.name} \${i + 1}\`} className="h-8 w-8 object-cover rounded" />) }</div>`;
|
|
71
93
|
case "number-plain":
|
|
72
|
-
return `<>{${col.nullable ? `${colName} && ` : ""}numF(${colName})}</>`;
|
|
94
|
+
return `<>{${col.nullable || col.name.includes(".") ? `${colName} && ` : ""}numF(${colName})}</>`;
|
|
73
95
|
case "object":
|
|
74
|
-
return
|
|
96
|
+
return `<span className="text-xs">{${col.nullable ? `${colName} ? ` : ""}JSON.stringify(${colName})${col.nullable ? ` : '-'` : ""}}</span>`;
|
|
75
97
|
case "object-pick": {
|
|
76
98
|
const pickedChild = col.children?.find((child) => child.name === col.config?.picked);
|
|
77
99
|
if (!pickedChild) {
|
|
@@ -105,17 +127,17 @@ export class Template__view_list extends Template {
|
|
|
105
127
|
} else if (col.renderType === "object") {
|
|
106
128
|
try {
|
|
107
129
|
const relProp = getRelationPropFromColName(entityId, col.name);
|
|
108
|
-
const result = col.children
|
|
130
|
+
const result = (col.children ?? []).map((child) => {
|
|
109
131
|
entityId = relProp.with;
|
|
110
132
|
names = EntityManager.getNamesFromId(relProp.with);
|
|
111
133
|
return this.renderColumnImport(entityId, child, names);
|
|
112
134
|
});
|
|
113
|
-
return flat(result
|
|
135
|
+
return flat(result);
|
|
114
136
|
} catch {
|
|
115
137
|
return [null];
|
|
116
138
|
}
|
|
117
139
|
} else if (col.renderType === "array") {
|
|
118
|
-
|
|
140
|
+
if (!col.element) return [null];
|
|
119
141
|
return this.renderColumnImport(entityId, col.element, names);
|
|
120
142
|
}
|
|
121
143
|
|
|
@@ -192,23 +214,32 @@ export class Template__view_list extends Template {
|
|
|
192
214
|
getDefault(columns: RenderingNode[]): {
|
|
193
215
|
orderBy: string;
|
|
194
216
|
search: string;
|
|
217
|
+
hasSearch: boolean;
|
|
218
|
+
hasOrderBy: boolean;
|
|
195
219
|
} {
|
|
196
220
|
const def = {
|
|
197
|
-
orderBy: "
|
|
198
|
-
search: "
|
|
221
|
+
orderBy: "",
|
|
222
|
+
search: "",
|
|
223
|
+
hasSearch: false,
|
|
224
|
+
hasOrderBy: false,
|
|
199
225
|
};
|
|
200
226
|
const orderByZodType = columns.find((col) => col.name === "orderBy")?.zodType;
|
|
201
227
|
if (orderByZodType && orderByZodType instanceof z.ZodEnum) {
|
|
202
|
-
def.orderBy = orderByZodType.
|
|
228
|
+
def.orderBy = Object.keys(orderByZodType.enum)[0];
|
|
229
|
+
def.hasOrderBy = true;
|
|
203
230
|
}
|
|
204
231
|
const searchZodType = columns.find((col) => col.name === "search")?.zodType;
|
|
205
232
|
if (searchZodType && searchZodType instanceof z.ZodEnum) {
|
|
206
|
-
def.search = searchZodType.
|
|
233
|
+
def.search = Object.keys(searchZodType.enum)[0];
|
|
234
|
+
def.hasSearch = true;
|
|
207
235
|
}
|
|
208
236
|
return def;
|
|
209
237
|
}
|
|
210
238
|
|
|
211
239
|
async render({ entityId }: TemplateOptions["view_list"]) {
|
|
240
|
+
const { getColumnsNode } = await import("../entity-converter");
|
|
241
|
+
const { getZodTypeById, zodTypeToRenderingNode } = await import("../zod-converter");
|
|
242
|
+
|
|
212
243
|
const columnsNode = await getColumnsNode(entityId, "A");
|
|
213
244
|
const listParamsZodType = await getZodTypeById(`${entityId}ListParams`);
|
|
214
245
|
const listParamsNode = zodTypeToRenderingNode(listParamsZodType);
|
|
@@ -218,13 +249,20 @@ export class Template__view_list extends Template {
|
|
|
218
249
|
|
|
219
250
|
// 실제 리스트 컬럼
|
|
220
251
|
const columns = (columnsNode.children as RenderingNode[])
|
|
221
|
-
|
|
252
|
+
|
|
253
|
+
.sort((a, b) => (a.name === "id" ? -1 : b.name === "id" ? 1 : 0))
|
|
222
254
|
.map((col) => {
|
|
223
255
|
const propCandidate = entity.props.find((p) => p.name === col.name);
|
|
256
|
+
const rendered = this.renderColumn(entityId, col, names);
|
|
224
257
|
return {
|
|
225
258
|
name: col.name,
|
|
226
|
-
label: propCandidate?.desc ?? col.label,
|
|
227
|
-
tc: `(row) => ${
|
|
259
|
+
label: col.name === "id" ? "ID" : (propCandidate?.desc ?? col.label),
|
|
260
|
+
tc: `(row) => ${rendered}`,
|
|
261
|
+
fit:
|
|
262
|
+
col.renderType === "number-id" ||
|
|
263
|
+
col.renderType === "datetime" ||
|
|
264
|
+
col.renderType === "string-datetime",
|
|
265
|
+
align: col.renderType === "number-id" ? "center" : undefined,
|
|
228
266
|
};
|
|
229
267
|
});
|
|
230
268
|
|
|
@@ -250,17 +288,23 @@ export class Template__view_list extends Template {
|
|
|
250
288
|
|
|
251
289
|
if (col.renderType === "enums") {
|
|
252
290
|
if (col.name === "search") {
|
|
253
|
-
key = "
|
|
291
|
+
key = "view_enums_select";
|
|
254
292
|
enumId = `${names.capital}SearchField`;
|
|
255
293
|
targetEntityId = names.capital;
|
|
256
294
|
} else {
|
|
257
295
|
key = "view_enums_select";
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
296
|
+
// config.enumId 우선 사용
|
|
297
|
+
if (col.config && "enumId" in col.config) {
|
|
298
|
+
enumId = (col.config as { enumId: string }).enumId;
|
|
299
|
+
targetEntityId = entityId;
|
|
300
|
+
} else {
|
|
301
|
+
try {
|
|
302
|
+
const { targetEntityNames, id } = getEnumInfoFromColName(entityId, col.name);
|
|
303
|
+
targetEntityId = targetEntityNames.capital;
|
|
304
|
+
enumId = id;
|
|
305
|
+
} catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
264
308
|
}
|
|
265
309
|
}
|
|
266
310
|
} else {
|
|
@@ -282,19 +326,19 @@ export class Template__view_list extends Template {
|
|
|
282
326
|
});
|
|
283
327
|
}
|
|
284
328
|
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
329
|
+
// 컬럼에서 사용하는 enum들 수집
|
|
330
|
+
const columnEnums: string[] = [];
|
|
331
|
+
(columnsNode.children ?? []).forEach((col) => {
|
|
332
|
+
if (col.renderType === "enums") {
|
|
333
|
+
try {
|
|
334
|
+
const { id: enumId } = getEnumInfoFromColName(entityId, col.name);
|
|
335
|
+
columnEnums.push(enumId);
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
294
339
|
|
|
295
340
|
// SearchInput
|
|
296
|
-
|
|
297
|
-
preTemplates.push({
|
|
341
|
+
preTemplates?.push({
|
|
298
342
|
key: "view_search_input",
|
|
299
343
|
options: {
|
|
300
344
|
entityId,
|
|
@@ -302,224 +346,390 @@ export class Template__view_list extends Template {
|
|
|
302
346
|
});
|
|
303
347
|
|
|
304
348
|
// 디폴트 파라미터
|
|
305
|
-
const def = this.getDefault(filterColumns);
|
|
349
|
+
// const def = this.getDefault(filterColumns);
|
|
306
350
|
|
|
307
351
|
return {
|
|
308
352
|
...this.getTargetAndPath(names),
|
|
309
353
|
body: `
|
|
310
|
-
import
|
|
311
|
-
import {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
} from 'semantic-ui-react';
|
|
324
|
-
import classNames from 'classnames';
|
|
325
|
-
import { DateTime } from "luxon";
|
|
326
|
-
import { DelButton, EditButton, AppBreadcrumbs, AddButton, useSelection, useListParams, SonamuCol, numF, formatDate, formatDateTime } from '@sonamu-kit/react-sui';
|
|
327
|
-
|
|
354
|
+
import { useState, Fragment } from "react";
|
|
355
|
+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
356
|
+
|
|
357
|
+
import { Card, CardContent, CardHeader } from "@sonamu-kit/react-components/components";
|
|
358
|
+
import { Badge } from "@sonamu-kit/react-components/components";
|
|
359
|
+
import { Button } from "@sonamu-kit/react-components/components";
|
|
360
|
+
import { Pagination, Table, TableBody, TableCell, type TableCol, TableHead, TableHeader, TableRow } from "@sonamu-kit/react-components/components";
|
|
361
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@sonamu-kit/react-components/components";
|
|
362
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@sonamu-kit/react-components/components";
|
|
363
|
+
import { Input } from "@sonamu-kit/react-components/components";
|
|
364
|
+
import { Checkbox } from "@sonamu-kit/react-components/components";
|
|
365
|
+
|
|
366
|
+
import { useListParams, numF, dateF, datetimeF } from "@sonamu-kit/react-components/lib";
|
|
328
367
|
import { ${names.capital}SubsetA } from "@/services/sonamu.generated";
|
|
329
|
-
import { ${names.capital}Service } from
|
|
330
|
-
import { ${names.capital}ListParams } from
|
|
331
|
-
${
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
368
|
+
import { ${names.capital}Service } from "@/services/services.generated";
|
|
369
|
+
import { ${names.capital}ListParams } from "@/services/${names.fs}/${names.fs}.types";
|
|
370
|
+
import { ${(() => {
|
|
371
|
+
// 기본 enum 수집 (filterColumns에 있는 것만)
|
|
372
|
+
const baseEnums: string[] = [];
|
|
373
|
+
if (filterColumns.some((col) => col.name === "orderBy")) {
|
|
374
|
+
baseEnums.push(`${names.capital}OrderBy`, `${names.capital}OrderByLabel`);
|
|
375
|
+
}
|
|
376
|
+
if (filterColumns.some((col) => col.name === "search")) {
|
|
377
|
+
baseEnums.push(`${names.capital}SearchField`, `${names.capital}SearchFieldLabel`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 필터 enum 수집 (config.enumId 우선, 없으면 getEnumInfoFromColName)
|
|
381
|
+
const filterEnumIds = filterColumns
|
|
382
|
+
.filter(
|
|
383
|
+
(col) => col.renderType === "enums" && col.name !== "search" && col.name !== "orderBy",
|
|
384
|
+
)
|
|
385
|
+
.map((col) => {
|
|
386
|
+
if (col.config && "enumId" in col.config) {
|
|
387
|
+
return (col.config as { enumId: string }).enumId;
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const { id: enumId } = getEnumInfoFromColName(entityId, col.name);
|
|
391
|
+
return enumId;
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
.filter(Boolean) as string[];
|
|
397
|
+
|
|
398
|
+
// 모든 enum 합치고 중복 제거
|
|
399
|
+
const allEnums = [...new Set([...filterEnumIds, ...columnEnums])];
|
|
400
|
+
const enumImports = allEnums.flatMap((enumId) => [`${enumId}`, `${enumId}Label`]);
|
|
401
|
+
|
|
402
|
+
return [...baseEnums, ...enumImports].join(", ");
|
|
403
|
+
})()} } from "@/services/sonamu.generated";
|
|
404
|
+
${(() => {
|
|
405
|
+
// FK 필드의 AsyncSelect 컴포넌트 import
|
|
406
|
+
const fkColumns = filterColumns.filter((col) => col.name.endsWith("_id") && col.name !== "id");
|
|
407
|
+
return fkColumns
|
|
408
|
+
.map((col) => {
|
|
409
|
+
try {
|
|
410
|
+
const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
|
|
411
|
+
const targetNames = EntityManager.getNamesFromId(relProp.with);
|
|
412
|
+
return `import { ${relProp.with}IdAsyncSelect } from "@/components/${targetNames.fs}/${relProp.with}IdAsyncSelect";`;
|
|
413
|
+
} catch {
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
.filter(Boolean)
|
|
418
|
+
.join("\n");
|
|
419
|
+
})()}
|
|
420
|
+
${
|
|
421
|
+
filterColumns.some((col) => col.name === "search")
|
|
422
|
+
? `
|
|
423
|
+
import { ${names.capital}SearchFieldSelect } from "@/components/${names.fs}/${names.capital}SearchFieldSelect";`
|
|
424
|
+
: ""
|
|
425
|
+
}
|
|
426
|
+
${
|
|
427
|
+
filterColumns.some((col) => col.name === "orderBy")
|
|
428
|
+
? `
|
|
429
|
+
import { ${names.capital}OrderBySelect } from "@/components/${names.fs}/${names.capital}OrderBySelect";`
|
|
430
|
+
: ""
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
import EditIcon from "~icons/lucide/square-pen";
|
|
434
|
+
import TrashIcon from "~icons/lucide/trash-2";
|
|
435
|
+
import ListIcon from "~icons/mdi/format-list-bulleted";
|
|
436
|
+
import SearchIcon from "~icons/mdi/magnify";
|
|
437
|
+
|
|
438
|
+
export const Route = createFileRoute("/admin/${names.fsPlural}/")({\n head: () => ({\n meta: [\n { title: "${entity.title ?? names.capital} List" },\n { name: "description", content: "${entity.title ?? names.capital} 목록 관리" },\n ],\n }),\n component: ${names.capital}List,\n});\n\ntype ${names.capital}ListProps = {};
|
|
439
|
+
|
|
440
|
+
function ${names.capital}List({}: ${names.capital}ListProps) {
|
|
441
|
+
const navigate = useNavigate();
|
|
442
|
+
|
|
443
|
+
// 상태 관리
|
|
444
|
+
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
|
445
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
446
|
+
const [itemToDelete, setItemToDelete] = useState<{ id: number; name?: string } | null>(null);
|
|
337
447
|
|
|
338
|
-
type ${names.capital}ListProps = {};
|
|
339
|
-
export default function ${names.capital}List({}: ${names.capital}ListProps) {
|
|
340
448
|
// 리스트 필터
|
|
341
449
|
const { listParams, register } = useListParams(${names.capital}ListParams, {
|
|
342
|
-
num:
|
|
450
|
+
num: 10,
|
|
343
451
|
page: 1,
|
|
344
|
-
|
|
345
|
-
|
|
452
|
+
keyword: "",${
|
|
453
|
+
filterColumns.some((col) => col.name === "search")
|
|
454
|
+
? `
|
|
455
|
+
search: ${names.capital}SearchField.options[0],`
|
|
456
|
+
: ""
|
|
457
|
+
}${
|
|
458
|
+
filterColumns.some((col) => col.name === "orderBy")
|
|
459
|
+
? `
|
|
460
|
+
orderBy: ${names.capital}OrderBy.options[0],`
|
|
461
|
+
: ""
|
|
462
|
+
}
|
|
346
463
|
});
|
|
347
464
|
|
|
348
465
|
// 리스트 쿼리
|
|
349
|
-
const { data, refetch, isLoading } = ${names.capital}Service.use${
|
|
350
|
-
names.capitalPlural
|
|
351
|
-
}('A', listParams);
|
|
466
|
+
const { data, refetch, isLoading } = ${names.capital}Service.use${names.capitalPlural}("A", listParams);
|
|
352
467
|
const { rows, total } = data ?? {};
|
|
353
468
|
|
|
354
|
-
//
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
469
|
+
// 현재 경로와 타이틀
|
|
470
|
+
const PAGE = {
|
|
471
|
+
route: "/admin/${names.fsPlural}",
|
|
472
|
+
title: "${entity.title ?? names.capital}",
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// 컬럼 정의
|
|
476
|
+
type ${names.capital}Row = NonNullable<typeof rows>[number];
|
|
477
|
+
const columns: TableCol<${names.capital}Row>[] = [
|
|
478
|
+
${columns
|
|
479
|
+
.map(
|
|
480
|
+
(col) => ` {
|
|
481
|
+
label: "${col.label}",
|
|
482
|
+
tc: ${col.tc},${
|
|
483
|
+
col.fit
|
|
484
|
+
? `
|
|
485
|
+
fit: true,`
|
|
486
|
+
: ""
|
|
487
|
+
}${
|
|
488
|
+
col.align
|
|
489
|
+
? `
|
|
490
|
+
align: "${col.align}",`
|
|
491
|
+
: ""
|
|
492
|
+
}
|
|
493
|
+
}`,
|
|
494
|
+
)
|
|
495
|
+
.join(",\n")},
|
|
496
|
+
{
|
|
497
|
+
label: "Manage",
|
|
498
|
+
fit: true,
|
|
499
|
+
align: "center",
|
|
500
|
+
tc: (row) => (
|
|
501
|
+
<div className="flex items-center justify-center gap-1">
|
|
502
|
+
<Button
|
|
503
|
+
variant="yellow"
|
|
504
|
+
size="xs"
|
|
505
|
+
icon={<EditIcon />}
|
|
506
|
+
onClick={() => navigate({ to: \`\${PAGE.route}/form\`, search: { id: row.id } })}
|
|
507
|
+
/>
|
|
508
|
+
<Button
|
|
509
|
+
variant="red"
|
|
510
|
+
size="xs"
|
|
511
|
+
icon={<TrashIcon />}
|
|
512
|
+
onClick={() => handleDeleteClick(row.id)}
|
|
513
|
+
/>
|
|
514
|
+
</div>
|
|
515
|
+
),
|
|
516
|
+
},
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
// 선택 핸들러
|
|
520
|
+
const handleToggleItem = (id: number) => {
|
|
521
|
+
const newSelection = new Set(selectedItems);
|
|
522
|
+
if (newSelection.has(id)) {
|
|
523
|
+
newSelection.delete(id);
|
|
524
|
+
} else {
|
|
525
|
+
newSelection.add(id);
|
|
359
526
|
}
|
|
527
|
+
setSelectedItems(newSelection);
|
|
528
|
+
};
|
|
360
529
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
});
|
|
530
|
+
const isAllSelected = () => {
|
|
531
|
+
return (rows?.length ?? 0) > 0 && rows!.every((row) => selectedItems.has(row.id));
|
|
364
532
|
};
|
|
365
533
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
534
|
+
const handleSelectAll = (checked: boolean) => {
|
|
535
|
+
if (checked) {
|
|
536
|
+
setSelectedItems(new Set(rows?.map((row) => row.id) ?? []));
|
|
537
|
+
} else {
|
|
538
|
+
setSelectedItems(new Set());
|
|
371
539
|
}
|
|
372
|
-
|
|
373
|
-
${names.capital}Service.del(selectedKeys).then(() => {
|
|
374
|
-
refetch();
|
|
375
|
-
});
|
|
376
540
|
};
|
|
377
541
|
|
|
378
|
-
//
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
542
|
+
// 삭제 핸들러
|
|
543
|
+
const handleDeleteClick = (id: number, name?: string) => {
|
|
544
|
+
setItemToDelete({ id, name });
|
|
545
|
+
setDeleteDialogOpen(true);
|
|
382
546
|
};
|
|
383
547
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
} = useSelection((rows ?? []).map((row) => row.id));
|
|
394
|
-
|
|
395
|
-
// 컬럼
|
|
396
|
-
const columns:SonamuCol<${names.capital}SubsetA>[] = [${columns
|
|
397
|
-
.map((col) => {
|
|
398
|
-
return [
|
|
399
|
-
`{ label: "${col.label}",`,
|
|
400
|
-
`tc: ${col.tc}, `,
|
|
401
|
-
`collapsing: ${["Title", "Name"].includes(col.label) === false}, }`,
|
|
402
|
-
].join("\n");
|
|
403
|
-
})
|
|
404
|
-
.join(",\n")}];
|
|
548
|
+
const handleConfirmDelete = () => {
|
|
549
|
+
if (itemToDelete) {
|
|
550
|
+
${names.capital}Service.del([itemToDelete.id]).then(() => {
|
|
551
|
+
refetch();
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
setDeleteDialogOpen(false);
|
|
555
|
+
setItemToDelete(null);
|
|
556
|
+
};
|
|
405
557
|
|
|
406
558
|
return (
|
|
407
|
-
<div className="
|
|
408
|
-
<div className="
|
|
409
|
-
<div className="
|
|
410
|
-
|
|
411
|
-
<
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
<${names.capital}SearchInput
|
|
415
|
-
input={register('keyword')}
|
|
416
|
-
dropdown={register('search')}
|
|
417
|
-
/>
|
|
418
|
-
</div>
|
|
419
|
-
<div className="filters-row">
|
|
420
|
-
${filterColumns
|
|
421
|
-
.map((col) => {
|
|
422
|
-
return this.renderFilter(entityId, col, names);
|
|
423
|
-
})
|
|
424
|
-
.join(" \n")}
|
|
425
|
-
</div>
|
|
426
|
-
</div>
|
|
427
|
-
|
|
428
|
-
<Segment basic padded className="contents-segment" loading={isLoading}>
|
|
429
|
-
<div className="buttons-row">
|
|
430
|
-
<div className={classNames('count', { hidden: isLoading })}>
|
|
431
|
-
{total} 건
|
|
559
|
+
<div className="flex-1 overflow-auto">
|
|
560
|
+
<div className="max-w-[1800px] mx-auto p-8">
|
|
561
|
+
<div className="space-y-6 mb-8">
|
|
562
|
+
{/* Header */}
|
|
563
|
+
<div className="flex items-center gap-2">
|
|
564
|
+
<ListIcon className="h-5 w-5" />
|
|
565
|
+
<span className="text-lg font-semibold h-5">{PAGE.title}</span>
|
|
432
566
|
</div>
|
|
433
|
-
<div className="buttons">
|
|
434
|
-
<AddButton currentRoute={PAGE.route} icon="write" label="추가" />
|
|
435
|
-
</div>
|
|
436
|
-
</div>
|
|
437
567
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
</TableRow>
|
|
459
|
-
</Table.Header>
|
|
460
|
-
<Table.Body>
|
|
461
|
-
{rows &&
|
|
462
|
-
rows.map((row, rowIndex) => (
|
|
463
|
-
<Table.Row key={row.id}>
|
|
464
|
-
<Table.Cell>
|
|
465
|
-
<Checkbox
|
|
466
|
-
label={row.id}
|
|
467
|
-
checked={getSelected(row.id)}
|
|
468
|
-
onChange={() => toggle(row.id)}
|
|
469
|
-
onClick={(e) =>
|
|
470
|
-
handleCheckboxClick(e, rowIndex)
|
|
471
|
-
}
|
|
568
|
+
<Card className="shadow-sm border-border/40 overflow-hidden">
|
|
569
|
+
<CardHeader className="pb-0 px-0 pt-0">
|
|
570
|
+
{/* Filters */}
|
|
571
|
+
<div className="bg-gray-100 px-6 py-4 space-y-3">
|
|
572
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
573
|
+
${
|
|
574
|
+
filterColumns.some((col) => col.name === "search")
|
|
575
|
+
? ` <${names.capital}SearchFieldSelect
|
|
576
|
+
{...register("search")}
|
|
577
|
+
placeholder="Search Type"
|
|
578
|
+
className="w-[200px] h-8 bg-white border-gray-300 text-xs"
|
|
579
|
+
/>`
|
|
580
|
+
: ""
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
<div className="relative flex-1 max-w-xs">
|
|
584
|
+
<Input
|
|
585
|
+
{...register("keyword")}
|
|
586
|
+
placeholder="Search..."
|
|
587
|
+
className="h-8 pr-8 text-xs bg-white border-gray-300"
|
|
472
588
|
/>
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
{col.tc(row, rowIndex)}
|
|
479
|
-
</Table.Cell>
|
|
480
|
-
))
|
|
481
|
-
}
|
|
482
|
-
<Table.Cell collapsing>
|
|
483
|
-
<EditButton
|
|
484
|
-
as={Link}
|
|
485
|
-
to={\`\${PAGE.route}/form?id=\${row.id}\`}
|
|
486
|
-
state={{ from: PAGE.route }}
|
|
589
|
+
<Button
|
|
590
|
+
variant="ghost"
|
|
591
|
+
size="sm"
|
|
592
|
+
icon={<SearchIcon />}
|
|
593
|
+
className="absolute right-0 top-0 h-8 w-8 hover:bg-transparent"
|
|
487
594
|
/>
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
<div className="ml-auto">
|
|
598
|
+
<Button
|
|
599
|
+
className="h-8 px-4 bg-primary hover:bg-primary/90 text-white"
|
|
600
|
+
onClick={() => navigate({ to: \`\${PAGE.route}/form\` })}
|
|
601
|
+
>
|
|
602
|
+
<span className="text-xs">Create</span>
|
|
603
|
+
</Button>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
608
|
+
${filterColumns
|
|
609
|
+
.filter((col) => col.name !== "search" && col.name !== "orderBy")
|
|
610
|
+
.map((col) => {
|
|
611
|
+
if (col.renderType === "enums") {
|
|
612
|
+
try {
|
|
613
|
+
// config.enumId가 있으면 우선 사용, 없으면 getEnumInfoFromColName 시도
|
|
614
|
+
const enumId =
|
|
615
|
+
col.config && "enumId" in col.config
|
|
616
|
+
? (col.config as { enumId: string }).enumId
|
|
617
|
+
: getEnumInfoFromColName(entityId, col.name).id;
|
|
618
|
+
return ` <Select key={\`${col.name}-\${listParams.${col.name}}\`} {...register("${col.name}")} clearable>
|
|
619
|
+
<SelectTrigger className="w-[200px] h-8 bg-white border-gray-300 text-xs">
|
|
620
|
+
<SelectValue placeholder="${col.label}" className="truncate" />
|
|
621
|
+
</SelectTrigger>
|
|
622
|
+
<SelectContent>
|
|
623
|
+
{${enumId}.options.map((key) => (
|
|
624
|
+
<SelectItem key={key} value={key}>
|
|
625
|
+
{${enumId}Label[key]}
|
|
626
|
+
</SelectItem>
|
|
627
|
+
))}
|
|
628
|
+
</SelectContent>
|
|
629
|
+
</Select>`;
|
|
630
|
+
} catch {
|
|
631
|
+
return "";
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// FK 필드 (AsyncSelect)
|
|
635
|
+
if (col.name.endsWith("_id") && col.name !== "id") {
|
|
636
|
+
try {
|
|
637
|
+
const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
|
|
638
|
+
return ` <${relProp.with}IdAsyncSelect
|
|
639
|
+
subset="A"
|
|
640
|
+
{...register("${col.name}")}
|
|
641
|
+
placeholder="${col.label ?? relProp.with}"
|
|
642
|
+
clearable
|
|
643
|
+
className="w-[200px] h-8 text-xs"
|
|
644
|
+
/>`;
|
|
645
|
+
} catch {
|
|
646
|
+
return "";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return "";
|
|
650
|
+
})
|
|
651
|
+
.filter(Boolean)
|
|
652
|
+
.join("\n")}
|
|
653
|
+
${
|
|
654
|
+
filterColumns.some((col) => col.name === "orderBy")
|
|
655
|
+
? ` <${names.capital}OrderBySelect
|
|
656
|
+
{...register("orderBy")}
|
|
657
|
+
placeholder="Sort"
|
|
658
|
+
textPrefix="Sort: "
|
|
659
|
+
className="w-[200px] h-8 bg-white border-gray-300 text-xs"
|
|
660
|
+
/>`
|
|
661
|
+
: ""
|
|
662
|
+
}
|
|
663
|
+
<span className="text-xs text-muted-foreground">{total ?? 0} results</span>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
</CardHeader>
|
|
667
|
+
|
|
668
|
+
<CardContent className="px-6 pb-6 pt-6 bg-white">
|
|
669
|
+
{/* Table */}
|
|
670
|
+
<Table>
|
|
671
|
+
<TableHeader>
|
|
672
|
+
<TableRow className="hover:bg-transparent bg-gray-100">
|
|
673
|
+
<TableHead className="h-9 text-xs w-[40px]">
|
|
674
|
+
<Checkbox
|
|
675
|
+
checked={isAllSelected()}
|
|
676
|
+
onValueChange={handleSelectAll}
|
|
677
|
+
/>
|
|
678
|
+
</TableHead>
|
|
679
|
+
{columns.map((col, idx) => (
|
|
680
|
+
<TableHead key={idx} fit={col.fit} align={col.align}>
|
|
681
|
+
{col.label}
|
|
682
|
+
</TableHead>
|
|
683
|
+
))}
|
|
684
|
+
</TableRow>
|
|
685
|
+
</TableHeader>
|
|
686
|
+
<TableBody>
|
|
687
|
+
{!isLoading && rows && rows.map((row) => (
|
|
688
|
+
<Fragment key={row.id}>
|
|
689
|
+
<TableRow>
|
|
690
|
+
<TableCell className="py-3">
|
|
691
|
+
<Checkbox
|
|
692
|
+
checked={selectedItems.has(row.id)}
|
|
693
|
+
onValueChange={() => handleToggleItem(row.id)}
|
|
694
|
+
/>
|
|
695
|
+
</TableCell>
|
|
696
|
+
{columns.map((col, idx) => (
|
|
697
|
+
<TableCell key={idx} fit={col.fit} align={col.align} className="py-3">
|
|
698
|
+
{col.tc(row)}
|
|
699
|
+
</TableCell>
|
|
700
|
+
))}
|
|
701
|
+
</TableRow>
|
|
702
|
+
</Fragment>
|
|
703
|
+
))}
|
|
704
|
+
</TableBody>
|
|
705
|
+
</Table>
|
|
706
|
+
|
|
707
|
+
{/* Pagination */}
|
|
708
|
+
<Pagination
|
|
709
|
+
{...register("page")}
|
|
710
|
+
total={total ?? 0}
|
|
711
|
+
itemsPerPage={listParams.num ?? 10}
|
|
712
|
+
/>
|
|
713
|
+
</CardContent>
|
|
714
|
+
</Card>
|
|
503
715
|
</div>
|
|
504
|
-
</Segment>
|
|
505
|
-
|
|
506
|
-
<div className="fixed-menu">
|
|
507
|
-
<Transition
|
|
508
|
-
visible={selectedKeys.length > 0}
|
|
509
|
-
animation="slide left"
|
|
510
|
-
duration={500}
|
|
511
|
-
>
|
|
512
|
-
<Message size="small" color="violet" className="text-center">
|
|
513
|
-
<span className="px-4">{selectedKeys.length}개 선택됨</span>
|
|
514
|
-
<Button size="tiny" color="violet" onClick={() => deselectAll()}>
|
|
515
|
-
선택 해제
|
|
516
|
-
</Button>
|
|
517
|
-
<Button size="tiny" color="red" onClick={confirmDelSelected}>
|
|
518
|
-
일괄 삭제
|
|
519
|
-
</Button>
|
|
520
|
-
</Message>
|
|
521
|
-
</Transition>
|
|
522
716
|
</div>
|
|
717
|
+
|
|
718
|
+
{/* Delete Dialog */}
|
|
719
|
+
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
720
|
+
<AlertDialogContent>
|
|
721
|
+
<AlertDialogHeader>
|
|
722
|
+
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
723
|
+
<AlertDialogDescription>
|
|
724
|
+
This action cannot be undone. This will permanently delete this item.
|
|
725
|
+
</AlertDialogDescription>
|
|
726
|
+
</AlertDialogHeader>
|
|
727
|
+
<AlertDialogFooter>
|
|
728
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
729
|
+
<AlertDialogAction onClick={handleConfirmDelete}>Delete</AlertDialogAction>
|
|
730
|
+
</AlertDialogFooter>
|
|
731
|
+
</AlertDialogContent>
|
|
732
|
+
</AlertDialog>
|
|
523
733
|
</div>
|
|
524
734
|
);
|
|
525
735
|
}
|