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.
Files changed (200) hide show
  1. package/dist/ai/agents/agent.d.ts +6 -1
  2. package/dist/ai/agents/agent.d.ts.map +1 -1
  3. package/dist/ai/agents/agent.js +20 -5
  4. package/dist/api/base-frame.d.ts +4 -0
  5. package/dist/api/base-frame.d.ts.map +1 -1
  6. package/dist/api/base-frame.js +9 -1
  7. package/dist/api/caster.d.ts.map +1 -1
  8. package/dist/api/caster.js +2 -2
  9. package/dist/api/config.d.ts +35 -3
  10. package/dist/api/config.d.ts.map +1 -1
  11. package/dist/api/config.js +1 -1
  12. package/dist/api/decorators.d.ts +4 -4
  13. package/dist/api/decorators.d.ts.map +1 -1
  14. package/dist/api/decorators.js +80 -18
  15. package/dist/api/index.d.ts +1 -0
  16. package/dist/api/index.d.ts.map +1 -1
  17. package/dist/api/index.js +2 -1
  18. package/dist/api/secret.d.ts +7 -0
  19. package/dist/api/secret.d.ts.map +1 -0
  20. package/dist/api/secret.js +17 -0
  21. package/dist/api/sonamu.d.ts +17 -8
  22. package/dist/api/sonamu.d.ts.map +1 -1
  23. package/dist/api/sonamu.js +265 -47
  24. package/dist/cache/cache-manager.d.ts +11 -0
  25. package/dist/cache/cache-manager.d.ts.map +1 -0
  26. package/dist/cache/cache-manager.js +22 -0
  27. package/dist/cache/decorator.d.ts +31 -0
  28. package/dist/cache/decorator.d.ts.map +1 -0
  29. package/dist/cache/decorator.js +86 -0
  30. package/dist/cache/drivers.d.ts +33 -0
  31. package/dist/cache/drivers.d.ts.map +1 -0
  32. package/dist/cache/drivers.js +36 -0
  33. package/dist/cache/index.d.ts +4 -0
  34. package/dist/cache/index.d.ts.map +1 -0
  35. package/dist/cache/index.js +8 -0
  36. package/dist/cache/types.d.ts +28 -0
  37. package/dist/cache/types.d.ts.map +1 -0
  38. package/dist/cache/types.js +6 -0
  39. package/dist/database/base-model.d.ts +4 -2
  40. package/dist/database/base-model.d.ts.map +1 -1
  41. package/dist/database/base-model.js +9 -4
  42. package/dist/database/code-generator.d.ts +3 -1
  43. package/dist/database/code-generator.d.ts.map +1 -1
  44. package/dist/database/code-generator.js +3 -2
  45. package/dist/database/db.d.ts +1 -1
  46. package/dist/database/db.d.ts.map +1 -1
  47. package/dist/database/db.js +5 -5
  48. package/dist/database/knex.d.ts +3 -0
  49. package/dist/database/knex.d.ts.map +1 -0
  50. package/dist/database/knex.js +29 -0
  51. package/dist/database/puri.types.d.ts.map +1 -1
  52. package/dist/database/puri.types.js +1 -1
  53. package/dist/database/upsert-builder.d.ts.map +1 -1
  54. package/dist/database/upsert-builder.js +49 -5
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -1
  58. package/dist/logger/category.d.ts +4 -0
  59. package/dist/logger/category.d.ts.map +1 -0
  60. package/dist/logger/category.js +34 -0
  61. package/dist/logger/configure.d.ts +9 -0
  62. package/dist/logger/configure.d.ts.map +1 -0
  63. package/dist/logger/configure.js +115 -0
  64. package/dist/migration/code-generation.d.ts +5 -1
  65. package/dist/migration/code-generation.d.ts.map +1 -1
  66. package/dist/migration/code-generation.js +13 -7
  67. package/dist/migration/migrator.d.ts +1 -1
  68. package/dist/migration/migrator.d.ts.map +1 -1
  69. package/dist/migration/migrator.js +7 -7
  70. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  71. package/dist/migration/postgresql-schema-reader.js +5 -3
  72. package/dist/naite/naite.d.ts +0 -4
  73. package/dist/naite/naite.d.ts.map +1 -1
  74. package/dist/naite/naite.js +11 -19
  75. package/dist/ssr/index.d.ts +4 -0
  76. package/dist/ssr/index.d.ts.map +1 -0
  77. package/dist/ssr/index.js +4 -0
  78. package/dist/ssr/registry.d.ts +10 -0
  79. package/dist/ssr/registry.d.ts.map +1 -0
  80. package/dist/ssr/registry.js +43 -0
  81. package/dist/ssr/renderer.d.ts +6 -0
  82. package/dist/ssr/renderer.d.ts.map +1 -0
  83. package/dist/ssr/renderer.js +70 -0
  84. package/dist/ssr/types.d.ts +19 -0
  85. package/dist/ssr/types.d.ts.map +1 -0
  86. package/dist/ssr/types.js +4 -0
  87. package/dist/syncer/syncer.d.ts +1 -0
  88. package/dist/syncer/syncer.d.ts.map +1 -1
  89. package/dist/syncer/syncer.js +58 -1
  90. package/dist/tasks/decorator.d.ts +1 -0
  91. package/dist/tasks/decorator.d.ts.map +1 -1
  92. package/dist/tasks/decorator.js +9 -7
  93. package/dist/tasks/step-wrapper.d.ts +5 -0
  94. package/dist/tasks/step-wrapper.d.ts.map +1 -1
  95. package/dist/tasks/step-wrapper.js +11 -6
  96. package/dist/tasks/workflow-manager.d.ts +2 -0
  97. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  98. package/dist/tasks/workflow-manager.js +5 -2
  99. package/dist/template/implementations/entry-server.template.d.ts +17 -0
  100. package/dist/template/implementations/entry-server.template.d.ts.map +1 -0
  101. package/dist/template/implementations/entry-server.template.js +78 -0
  102. package/dist/template/implementations/model.template.d.ts.map +1 -1
  103. package/dist/template/implementations/model.template.js +5 -3
  104. package/dist/template/implementations/queries.template.d.ts +17 -0
  105. package/dist/template/implementations/queries.template.d.ts.map +1 -0
  106. package/dist/template/implementations/queries.template.js +83 -0
  107. package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -1
  108. package/dist/template/implementations/view_enums_select.template.js +34 -20
  109. package/dist/template/implementations/view_form.template.d.ts +2 -1
  110. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  111. package/dist/template/implementations/view_form.template.js +301 -129
  112. package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -1
  113. package/dist/template/implementations/view_id_async_select.template.js +136 -57
  114. package/dist/template/implementations/view_list.template.d.ts +2 -0
  115. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  116. package/dist/template/implementations/view_list.template.js +392 -227
  117. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  118. package/dist/template/implementations/view_search_input.template.js +46 -30
  119. package/dist/template/zod-converter.d.ts.map +1 -1
  120. package/dist/template/zod-converter.js +2 -2
  121. package/dist/testing/bootstrap.d.ts +28 -0
  122. package/dist/testing/bootstrap.d.ts.map +1 -0
  123. package/dist/testing/bootstrap.js +120 -0
  124. package/dist/testing/fixture-loader.d.ts +21 -0
  125. package/dist/testing/fixture-loader.d.ts.map +1 -0
  126. package/dist/testing/fixture-loader.js +28 -0
  127. package/dist/testing/fixture-manager.d.ts +1 -1
  128. package/dist/testing/fixture-manager.d.ts.map +1 -1
  129. package/dist/testing/fixture-manager.js +7 -7
  130. package/dist/testing/index.d.ts +4 -0
  131. package/dist/testing/index.d.ts.map +1 -0
  132. package/dist/testing/index.js +5 -0
  133. package/dist/testing/naite-vitest-reporter.d.ts +12 -0
  134. package/dist/testing/naite-vitest-reporter.d.ts.map +1 -0
  135. package/dist/testing/naite-vitest-reporter.js +17 -0
  136. package/dist/types/types.d.ts +5 -6
  137. package/dist/types/types.d.ts.map +1 -1
  138. package/dist/types/types.js +7 -8
  139. package/dist/ui/ai-client.d.ts +3 -1
  140. package/dist/ui/ai-client.d.ts.map +1 -1
  141. package/dist/ui/ai-client.js +27 -8
  142. package/dist/ui-web/assets/index-CTYv3qL6.js +92 -0
  143. package/dist/ui-web/index.html +1 -1
  144. package/package.json +43 -20
  145. package/src/ai/agents/agent.ts +38 -19
  146. package/src/api/base-frame.ts +8 -0
  147. package/src/api/caster.ts +6 -1
  148. package/src/api/config.ts +38 -4
  149. package/src/api/decorators.ts +106 -20
  150. package/src/api/index.ts +1 -0
  151. package/src/api/secret.ts +23 -0
  152. package/src/api/sonamu.ts +334 -61
  153. package/src/cache/cache-manager.ts +23 -0
  154. package/src/cache/decorator.ts +116 -0
  155. package/src/cache/drivers.ts +42 -0
  156. package/src/cache/index.ts +16 -0
  157. package/src/cache/types.ts +32 -0
  158. package/src/database/base-model.ts +7 -3
  159. package/src/database/code-generator.ts +3 -1
  160. package/src/database/db.ts +5 -5
  161. package/src/database/knex.ts +34 -0
  162. package/src/database/puri.types.ts +2 -3
  163. package/src/database/upsert-builder.ts +58 -4
  164. package/src/index.ts +4 -0
  165. package/src/logger/category.ts +42 -0
  166. package/src/logger/configure.ts +132 -0
  167. package/src/migration/code-generation.ts +19 -6
  168. package/src/migration/migrator.ts +7 -6
  169. package/src/migration/postgresql-schema-reader.ts +7 -2
  170. package/src/naite/naite.ts +10 -18
  171. package/src/shared/web.shared.ts.txt +1 -1
  172. package/src/ssr/index.ts +13 -0
  173. package/src/ssr/registry.ts +52 -0
  174. package/src/ssr/renderer.ts +105 -0
  175. package/src/ssr/types.ts +20 -0
  176. package/src/syncer/syncer.ts +59 -0
  177. package/src/tasks/decorator.ts +20 -4
  178. package/src/tasks/step-wrapper.ts +14 -5
  179. package/src/tasks/workflow-manager.ts +9 -1
  180. package/src/template/implementations/entry-server.template.ts +81 -0
  181. package/src/template/implementations/model.template.ts +4 -2
  182. package/src/template/implementations/queries.template.ts +111 -0
  183. package/src/template/implementations/view_enums_select.template.ts +33 -19
  184. package/src/template/implementations/view_form.template.ts +324 -145
  185. package/src/template/implementations/view_id_async_select.template.ts +145 -56
  186. package/src/template/implementations/view_list.template.ts +446 -236
  187. package/src/template/implementations/view_search_input.template.ts +45 -29
  188. package/src/template/zod-converter.ts +4 -1
  189. package/src/testing/bootstrap.ts +176 -0
  190. package/src/testing/fixture-loader.ts +28 -0
  191. package/src/testing/fixture-manager.ts +7 -6
  192. package/src/testing/index.ts +3 -0
  193. package/src/testing/naite-vitest-reporter.ts +18 -0
  194. package/src/types/types.ts +4 -5
  195. package/src/ui/ai-client.ts +82 -50
  196. package/dist/template/implementations/view_enums_dropdown.template.d.ts +0 -17
  197. package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +0 -1
  198. package/dist/template/implementations/view_enums_dropdown.template.js +0 -50
  199. package/dist/ui-web/assets/index-B87IyofX.js +0 -92
  200. 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, unique } from "radashi";
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/pages/admin",
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
- const colName = withoutName ? `${parentObj}` : `${parentObj}.${col.name}`;
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
- const relPropFk = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
47
- return `<>${relPropFk.with}#{${colName}}</>`;
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 `<>{${col.nullable ? `${colName} && ` : ""}<img src={${colName}} />}</>`;
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 className="text-tiny">{${colName} === null ? '-' : formatDateTime(${colName})}</span>`;
68
+ if (col.nullable || col.name.includes(".")) {
69
+ return `<span>{${colName} ? datetimeF(${colName}) : '-'}</span>`;
54
70
  } else {
55
- return `<span className="text-tiny">{formatDateTime(${colName})}</span>`;
71
+ return `<span>{datetimeF(${colName})}</span>`;
56
72
  }
57
73
  case "string-datetime":
58
- if (col.nullable) {
59
- return `<span className="text-tiny">{${colName} === null ? '-' : dateF(${colName})}</span>`;
74
+ if (col.nullable || col.name.includes(".")) {
75
+ return `<span>{${colName} ? dateF(${colName}) : '-'}</span>`;
60
76
  } else {
61
- return `<span className="text-tiny">{dateF(${colName})}</span>`;
77
+ return `<span>{dateF(${colName})}</span>`;
62
78
  }
63
79
  case "boolean":
64
- return `<>{${colName} ? <Label color='green' circular>O</Label> : <Label color='grey' circular>X</Label> }</>`;
80
+ return `<>{${colName} ? <Badge variant="default">O</Badge> : <Badge variant="secondary">X</Badge>}</>`;
65
81
  case "enums": {
66
- const { id: enumId } = getEnumInfoFromColName(entityId, col.name);
67
- return `<>{${col.nullable ? `${colName} && ` : ""}${enumId}Label[${colName}]}</>`;
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 `<>{ ${colName}.map(r => ${col.nullable ? `r && ` : ""}<img src={r} />) }</>`;
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 `<>{/* object ${colName} */}</>`;
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?.map((child) => {
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
- assert(col.element);
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: "id-desc",
198
- search: "title",
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.options[0].toString();
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.options[0].toString();
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
- .filter((col) => col.name !== "id")
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) => ${this.renderColumn(entityId, col, names)}`,
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 = "view_enums_dropdown";
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
- try {
259
- const { targetEntityNames, id } = getEnumInfoFromColName(entityId, col.name);
260
- targetEntityId = targetEntityNames.capital;
261
- enumId = id;
262
- } catch {
263
- continue;
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
- assert(columnsNode.children);
287
- const columnImports = unique(
288
- columnsNode.children
289
- .flatMap((col) => {
290
- return this.renderColumnImport(entityId, col, names);
291
- })
292
- .filter((col) => col !== null),
293
- ).join("\n");
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
- assert(preTemplates);
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 React from 'react';
311
- import { Link } from 'react-router-dom';
312
- import {
313
- Breadcrumb,
314
- Checkbox,
315
- Pagination,
316
- Segment,
317
- Table,
318
- TableRow,
319
- Message,
320
- Transition,
321
- Button,
322
- Label,
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 '@/services/services.generated';
330
- import { ${names.capital}ListParams } from '@/services/${names.fs}/${names.fs}.types';
331
- ${columnImports}
332
- ${filterColumns
333
- .map((col) => {
334
- return this.renderFilterImport(entityId, col, names);
335
- })
336
- .join("\n")}
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: 12,
450
+ num: 10,
343
451
  page: 1,
344
- orderBy: '${def.orderBy}',
345
- search: '${def.search}',
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 confirmDel = (ids: number[]) => {
356
- const answer = confirm('삭제하시겠습니까?');
357
- if (!answer) {
358
- return;
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
- ${names.capital}Service.del(ids).then(() => {
362
- refetch();
363
- });
530
+ const isAllSelected = () => {
531
+ return (rows?.length ?? 0) > 0 && rows!.every((row) => selectedItems.has(row.id));
364
532
  };
365
533
 
366
- // 일괄 삭제
367
- const confirmDelSelected = () => {
368
- const answer = confirm(\`\${selectedKeys.length}건을 일괄 삭제하시겠습니까?\`);
369
- if (!answer) {
370
- return;
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 PAGE = {
380
- route: '/admin/${names.fsPlural}',
381
- title: '${entity.title ?? names.capital}',
542
+ // 삭제 핸들러
543
+ const handleDeleteClick = (id: number, name?: string) => {
544
+ setItemToDelete({ id, name });
545
+ setDeleteDialogOpen(true);
382
546
  };
383
547
 
384
- // 선택
385
- const {
386
- getSelected,
387
- isAllSelected,
388
- selectedKeys,
389
- toggle,
390
- selectAll,
391
- deselectAll,
392
- handleCheckboxClick,
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="list ${names.fsPlural}-index">
408
- <div className="top-nav">
409
- <div className="header-row">
410
- <div className="header">{PAGE.title}</div>
411
- <AppBreadcrumbs>
412
- <Breadcrumb.Section active>{PAGE.title}</Breadcrumb.Section>
413
- </AppBreadcrumbs>
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("&nbsp;\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
- <Table
439
- celled
440
- compact
441
- selectable
442
- className={classNames({ hidden: total === undefined || total === 0 })}
443
- >
444
- <Table.Header>
445
- <TableRow>
446
- <Table.HeaderCell collapsing>
447
- <Checkbox
448
- label="ID"
449
- checked={isAllSelected}
450
- onChange={isAllSelected ? deselectAll : selectAll}
451
- />
452
- </Table.HeaderCell>
453
- {
454
- /* Header */
455
- columns.map((col, index) => col.th ?? <Table.HeaderCell key={index} collapsing={col.collapsing}>{ col.label }</Table.HeaderCell>)
456
- }
457
- <Table.HeaderCell>관리</Table.HeaderCell>
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
- </Table.Cell>
474
- {
475
- /* Body */
476
- columns.map((col, colIndex) => (
477
- <Table.Cell key={colIndex} collapsing={col.collapsing} className={col.className}>
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
- <DelButton onClick={() => confirmDel([row.id])} />
489
- </Table.Cell>
490
- </Table.Row>
491
- ))}
492
- </Table.Body>
493
- </Table>
494
- <div
495
- className={classNames('pagination-row', {
496
- hidden: (total ?? 0) === 0,
497
- })}
498
- >
499
- <Pagination
500
- totalPages={Math.ceil((total ?? 0) / (listParams.num ?? 24))}
501
- {...register('page')}
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
  }