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
@@ -6,7 +6,6 @@ import type { RenderingNode, TemplateKey, TemplateOptions } from "../../types/ty
6
6
  import { getEnumInfoFromColName, getRelationPropFromColName } from "../helpers";
7
7
  import type { RenderedTemplate } from "../template";
8
8
  import { Template } from "../template";
9
- import { getZodTypeById, zodTypeToRenderingNode } from "../zod-converter";
10
9
 
11
10
  export class Template__view_form extends Template {
12
11
  constructor() {
@@ -15,20 +14,20 @@ export class Template__view_form extends Template {
15
14
 
16
15
  getTargetAndPath(names: EntityNamesRecord) {
17
16
  return {
18
- target: "web/src/pages/admin",
17
+ target: "web/src/routes/admin",
19
18
  path: `${names.fsPlural}/form.tsx`,
20
19
  };
21
20
  }
22
21
 
23
22
  wrapFC(body: string, label?: string): string {
24
23
  return [
25
- `<Form.Field>${label ? `\n <label>${label}</label>` : ""}`,
26
- body,
27
- `</Form.Field>`,
24
+ `<div className="space-y-2">${label ? `\n <Label>${label}</Label>` : ""}`,
25
+ ` ${body}`,
26
+ `</div>`,
28
27
  ].join("\n");
29
28
  }
30
29
  wrapFG(body: string, label?: string): string {
31
- return [`<Form.Group widths="equal">`, this.wrapFC(body, label), `</Form.Group>`].join("\n");
30
+ return this.wrapFC(body, label);
32
31
  }
33
32
 
34
33
  renderColumnImport(entityId: string, col: RenderingNode) {
@@ -50,7 +49,7 @@ export class Template__view_form extends Template {
50
49
  }
51
50
  }
52
51
 
53
- renderColumn(
52
+ renderColumnOld(
54
53
  entityId: string,
55
54
  col: RenderingNode,
56
55
  names: EntityNamesRecord,
@@ -64,24 +63,30 @@ export class Template__view_form extends Template {
64
63
  if (col.zodType instanceof z.ZodString && (col.zodType.maxLength ?? 0) <= 512) {
65
64
  return `<Input placeholder="${col.label}" ${regExpr} />`;
66
65
  } else {
67
- return `<TextArea rows={8} placeholder="${col.label}" ${regExpr} />`;
66
+ return `<Textarea rows={8} placeholder="${col.label}" ${regExpr} />`;
68
67
  }
69
- case "datetime":
70
- return `<Input type="datetime-local" ${regExpr} />`;
71
68
  case "string-datetime":
72
- return `<SQLDateTimeInput ${regExpr} />`;
69
+ return `<DatePicker ${regExpr} />`;
73
70
  case "string-date":
74
- return `<SQLDateInput ${regExpr} />`;
71
+ return `<DatePicker ${regExpr} />`;
75
72
  case "number-id":
76
73
  return `<input type="hidden" ${regExpr} />`;
77
74
  case "number-plain":
78
- return `<NumberInput placeholder="${col.label}" ${regExpr} />`;
75
+ return `<Input type="number" placeholder="${col.label}" ${regExpr} />`;
79
76
  case "boolean":
80
- return `<BooleanToggle ${regExpr} />`;
77
+ return `<Switch ${regExpr} />`;
81
78
  case "string-image":
82
- return `<ImageUploader multiple={false} ${regExpr} />`;
79
+ return `<ImageUploader
80
+ ${regExpr}
81
+ uploader={async (file: File) => {
82
+ const { file: uploadedFile } = await FileService.upload(file);
83
+ return uploadedFile.url;
84
+ }}
85
+ previewSize="md"
86
+ />`;
83
87
  case "array-images":
84
- return `<ImageUploader multiple={true} ${regExpr} maxSize={5} />`;
88
+ return `{/* TODO: Implement multiple image uploader */}
89
+ <Input placeholder="${col.label}" ${regExpr} />`;
85
90
  case "enums":
86
91
  try {
87
92
  let enumId: string;
@@ -91,11 +96,9 @@ export class Template__view_form extends Template {
91
96
  const { id } = getEnumInfoFromColName(entityId, col.name);
92
97
  enumId = `${id}Select`;
93
98
  }
94
- return `<${enumId} ${regExpr} ${
95
- col.optional || col.nullable ? "clearable" : ""
96
- } textPrefix="" />`;
99
+ return `<${enumId} ${regExpr} ${col.optional || col.nullable ? "clearable" : ""} />`;
97
100
  } catch {
98
- return `<>찾을 수 없는 Enum ${col.name}</>`;
101
+ return `<span className="text-destructive">찾을 수 없는 Enum ${col.name}</span>`;
99
102
  }
100
103
  case "number-fk_id":
101
104
  try {
@@ -108,17 +111,100 @@ export class Template__view_form extends Template {
108
111
  return `<Input ${regExpr} />`;
109
112
  }
110
113
  case "array":
111
- return `<>${col.name} array</>`;
114
+ return `<span className="text-muted-foreground">${col.name} array</span>`;
112
115
  case "object":
113
- return `<>${col.name} object</>`;
114
- case "vector":
115
- // vector 타입은 일반적으로 API를 통해 생성되므로 읽기 전용으로 표시
116
- return `<div className="p-8px text-gray-500">[Vector: ${col.name}] - 임베딩 데이터는 API를 통해 자동 생성됩니다.</div>`;
116
+ return `<span className="text-muted-foreground">${col.name} object</span>`;
117
117
  default:
118
118
  throw new Error(`대응 불가능한 렌더 타입 ${col.renderType} on ${col.name}`);
119
119
  }
120
120
  }
121
121
 
122
+ // New style rendering for feed-sites style form
123
+ renderColumn(entityId: string, col: RenderingNode, names: EntityNamesRecord): string {
124
+ const regExpr = `{...register("${col.name}")}`;
125
+
126
+ switch (col.renderType) {
127
+ case "string-plain":
128
+ if (col.zodType instanceof z.ZodString && (col.zodType.maxLength ?? 0) <= 256) {
129
+ return `<Input className="h-8 text-xs bg-white" placeholder="${col.label}" ${regExpr} />`;
130
+ } else {
131
+ return `<Textarea className="text-xs bg-white" rows={4} placeholder="${col.label}" ${regExpr} />`;
132
+ }
133
+ case "string-datetime":
134
+ return `<DateInput
135
+ className="h-8 text-xs bg-white"
136
+ value={form.${col.name} ? new Date(form.${col.name}) : null}
137
+ onValueChange={(value) => setForm({ ...form, ${col.name}: value })}
138
+ />`;
139
+ case "string-date":
140
+ return `<DateInput
141
+ mode="date"
142
+ className="h-8 text-xs bg-white"
143
+ value={form.${col.name} ? new Date(form.${col.name}) : null}
144
+ onValueChange={(value) => setForm({ ...form, ${col.name}: value })}
145
+ />`;
146
+ case "datetime":
147
+ return `<DateInput
148
+ className="h-8 text-xs bg-white"
149
+ ${regExpr}
150
+ />`;
151
+ case "number-id":
152
+ return `<input type="hidden" ${regExpr} />`;
153
+ case "number-plain":
154
+ return `<Input type="number" className="h-8 text-xs bg-white" placeholder="${col.label}" ${regExpr} />`;
155
+ case "boolean":
156
+ return `<Switch ${regExpr} />`;
157
+ case "string-image":
158
+ return `<ImageUploader
159
+ ${regExpr}
160
+ uploader={async (file: File) => {
161
+ const { file: uploadedFile } = await FileService.upload(file);
162
+ return uploadedFile.url;
163
+ }}
164
+ previewSize="md"
165
+ />`;
166
+ case "array-images":
167
+ return `<MultiImageUploader
168
+ value={Array.isArray(form.${col.name}) ? form.${col.name} : []}
169
+ onValueChange={(urls) => setForm({ ...form, ${col.name}: urls })}
170
+ uploader={async (file: File) => {
171
+ const { file: uploadedFile } = await FileService.upload(file);
172
+ return uploadedFile.url;
173
+ }}
174
+ previewSize="md"
175
+ placeholder="${col.label}"
176
+ />`;
177
+ case "enums":
178
+ try {
179
+ let enumId: string;
180
+ if (col.name === "orderBy") {
181
+ enumId = `${names.capital}${inflection.camelize(col.name)}Select`;
182
+ } else {
183
+ const { id } = getEnumInfoFromColName(entityId, col.name);
184
+ enumId = `${id}Select`;
185
+ }
186
+ return `<${enumId} ${regExpr} ${col.optional || col.nullable ? "clearable" : ""} />`;
187
+ } catch {
188
+ return `<Input className="h-8 text-xs bg-white" ${regExpr} />`;
189
+ }
190
+ case "number-fk_id":
191
+ try {
192
+ const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
193
+ const fkId = `${relProp.with}IdAsyncSelect`;
194
+ return `<${fkId} subset="A" ${regExpr} ${
195
+ col.optional || col.nullable ? "clearable" : ""
196
+ } className="h-8 text-xs" />`;
197
+ } catch {
198
+ return `<Input type="number" className="h-8 text-xs bg-white" placeholder="${col.label}" ${regExpr} />`;
199
+ }
200
+ case "array":
201
+ case "object":
202
+ return `<Input className="h-8 text-xs bg-white" placeholder="${col.name}" ${regExpr} />`;
203
+ default:
204
+ return `<Input className="h-8 text-xs bg-white" ${regExpr} />`;
205
+ }
206
+ }
207
+
122
208
  resolveDefaultValue(columns: RenderingNode[]): object {
123
209
  return columns.reduce(
124
210
  (result, col) => {
@@ -132,11 +218,9 @@ export class Template__view_form extends Template {
132
218
  } else if (col.zodType instanceof z.ZodNumber) {
133
219
  value = 0;
134
220
  } else if (col.zodType instanceof z.ZodEnum) {
135
- value = Object.keys(col.zodType.options)[0];
221
+ value = Object.keys(col.zodType.enum)[0];
136
222
  } else if (col.zodType instanceof z.ZodBoolean) {
137
223
  value = false;
138
- } else if (col.zodType instanceof z.ZodDate) {
139
- value = new Date();
140
224
  } else if (col.zodType instanceof z.ZodString) {
141
225
  if (col.renderType === "string-datetime") {
142
226
  value = "now()";
@@ -157,12 +241,23 @@ export class Template__view_form extends Template {
157
241
  }
158
242
 
159
243
  async render({ entityId }: TemplateOptions["view_form"]) {
160
- const saveParamsZodType = await getZodTypeById(`${entityId}SaveParams`);
161
- const saveParamsNode = zodTypeToRenderingNode(saveParamsZodType);
162
-
163
244
  const entity = EntityManager.get(entityId);
164
245
  const names = EntityManager.getNamesFromId(entityId);
165
- const columns = (saveParamsNode.children as RenderingNode[])
246
+
247
+ // SaveParams 타입을 로드하여 saveParamsNode 생성
248
+ const { loadTypes } = await import("../../syncer/module-loader");
249
+ const loadedTypes = await loadTypes();
250
+ const SaveParamsZodType = loadedTypes[`${entityId}SaveParams`];
251
+
252
+ if (!SaveParamsZodType) {
253
+ throw new Error(`SaveParams for ${entityId} not found. Did you run 'sonamu sync'?`);
254
+ }
255
+
256
+ // Zod 타입을 RenderingNode로 변환
257
+ const { zodTypeToRenderingNode } = await import("../zod-converter");
258
+ const saveParamsNode = zodTypeToRenderingNode(SaveParamsZodType);
259
+
260
+ const columns = ((saveParamsNode?.children ?? []) as RenderingNode[])
166
261
  .filter((col) => col.name !== "id")
167
262
  .map((col) => {
168
263
  const propCandidate = entity.props.find((prop) => prop.name === col.name);
@@ -236,28 +331,46 @@ export class Template__view_form extends Template {
236
331
  return {
237
332
  ...this.getTargetAndPath(names),
238
333
  body: `
239
- import React, { useEffect, useState, Dispatch, SetStateAction, forwardRef, Ref, useImperativeHandle, useCallback } from 'react';
240
- import { useSearchParams } from 'react-router-dom';
241
334
  import {
242
335
  Button,
243
- Checkbox,
244
- Form,
245
- Header,
246
- Input,
247
- Segment,
248
- TextArea,
249
- Label,
250
- } from 'semantic-ui-react';
251
- import { DateTime } from "luxon";
252
-
253
- import { BackLink, LinkInput, NumberInput, BooleanToggle, SQLDateTimeInput, SQLDateInput, useTypeForm, useGoBack, formatDateTime } from "@sonamu-kit/react-sui";
254
- import { defaultCatch } from '@/services/sonamu.shared';
255
- // import { ImageUploader } from '@/admin-common/ImageUploader';
256
- // import { useCommonModal } from "@/admin-common/CommonModal";
257
-
258
- import { ${names.capital}SaveParams } from '@/services/${names.fs}/${names.fs}.types';
259
- import { ${names.capital}Service } from '@/services/services.generated';
260
- import { ${names.capital}SubsetA } from '@/services/sonamu.generated';
336
+ Card,
337
+ CardContent,
338
+ CardHeader,
339
+ CardTitle,
340
+ Input,${columns.some((col) => col.renderType === "string-plain" && col.zodType instanceof z.ZodString && (col.zodType.maxLength ?? 0) > 256) ? "\n Textarea," : ""}${columns.some((col) => col.renderType === "enums") ? "\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue," : ""}${columns.some((col) => col.renderType === "boolean") ? "\n Switch," : ""}${columns.some((col) => col.renderType === "string-image") ? "\n ImageUploader," : ""}${columns.some((col) => col.renderType === "array-images") ? "\n MultiImageUploader," : ""}${columns.some((col) => ["string-datetime", "string-date", "datetime"].includes(col.renderType)) ? "\n DateInput," : ""}
341
+ } from "@sonamu-kit/react-components/components";
342
+ import { useTypeForm } from "@sonamu-kit/react-components/lib";
343
+ import { useQueryClient } from "@tanstack/react-query";
344
+ import { createFileRoute, useRouter } from "@tanstack/react-router";
345
+ import { useEffect } from "react";
346
+ import { z } from "zod";
347
+ import { ${names.capital}Service${
348
+ columns.some((col) => ["string-image", "array-images"].includes(col.renderType))
349
+ ? ", FileService"
350
+ : ""
351
+ } } from "@/services/services.generated";
352
+ import type { ${names.capital}SubsetA } from "@/services/sonamu.generated";${
353
+ columns.filter((col) => col.renderType === "enums").length > 0
354
+ ? "\nimport { " +
355
+ unique(
356
+ columns
357
+ .filter((col) => col.renderType === "enums")
358
+ .map((col) => {
359
+ try {
360
+ const { id } = getEnumInfoFromColName(entityId, col.name);
361
+ return `${id}, ${id}Label`;
362
+ } catch {
363
+ return "";
364
+ }
365
+ }),
366
+ )
367
+ .filter(Boolean)
368
+ .join(", ") +
369
+ ' } from "@/services/sonamu.generated";'
370
+ : ""
371
+ }
372
+ import { defaultCatch } from "@/services/sonamu.shared";
373
+ import { ${names.capital}SaveParams } from "@/services/${names.fs}/${names.fs}.types";
261
374
  ${unique(
262
375
  columns
263
376
  .filter((col) => ["number-fk_id", "enums"].includes(col.renderType))
@@ -266,119 +379,185 @@ ${unique(
266
379
  }),
267
380
  ).join("\n")}
268
381
 
269
- export default function ${names.capitalPlural}FormPage() {
270
- // 라우팅 searchParams
271
- const [searchParams] = useSearchParams();
272
- const query = {
273
- id: searchParams.get('id') ?? undefined,
274
- };
382
+ import ArrowLeftIcon from "~icons/lucide/arrow-left";
383
+ import SaveIcon from "~icons/lucide/save";
384
+ import FormIcon from "~icons/mdi/form-select";
385
+
386
+ const formSearchSchema = z.object({
387
+ id: z.number().optional(),
388
+ });
275
389
 
276
- return <${names.capitalPlural}Form id={query?.id ? Number(query.id) : undefined} />;
390
+ export const Route = createFileRoute("/admin/${names.fsPlural}/form")({
391
+ validateSearch: formSearchSchema,
392
+ component: ${names.capitalPlural}FormPage,
393
+ });
394
+
395
+ function ${names.capitalPlural}FormPage() {
396
+ const { id } = Route.useSearch();
397
+ return <${names.capitalPlural}Form id={id} />;
277
398
  }
399
+
278
400
  type ${names.capitalPlural}FormProps = {
279
401
  id?: number;
280
- mode?: 'page' | 'modal';
402
+ mode?: "page" | "modal";
281
403
  };
404
+
282
405
  export function ${names.capitalPlural}Form({ id, mode }: ${names.capitalPlural}FormProps) {
283
- // 편집시 기존 row
284
- const [row, setRow] = useState<${names.capital}SubsetA | undefined>();
406
+ const router = useRouter();
407
+ const queryClient = useQueryClient();
408
+
409
+ const { form, setForm, register } = useTypeForm(${names.capital}SaveParams, ${JSON.stringify(defaultValue).replace(/"now\(\)"/g, '""')});
410
+ ${(() => {
411
+ const hasDatetime = columns.some((col) => col.renderType === "string-datetime");
412
+ const hasDate = columns.some((col) => col.renderType === "string-date");
413
+ if (!hasDatetime && !hasDate) return "";
414
+
415
+ let helpers = "\n";
416
+ if (hasDatetime) {
417
+ helpers += ` // datetime-local 형식으로 변환 (YYYY-MM-DDTHH:MM)
418
+ const toDatetimeLocalString = (date: Date | string | null | undefined): string => {
419
+ if (!date) return "";
420
+ const d = typeof date === "string" ? new Date(date) : date;
421
+ return d.toISOString().slice(0, 16);
422
+ };
285
423
 
286
- // ${names.capital}SaveParams
287
- const { form, setForm, register } = useTypeForm(${names.capital}SaveParams, ${JSON.stringify(
288
- defaultValue,
289
- ).replace(/"now\(\)"/g, "DateTime.local().toSQL()!.slice(0, 19)")});
424
+ // datetime-local 문자열을 Date로 변환
425
+ const fromDatetimeLocalString = (value: string): Date | null => {
426
+ if (!value) return null;
427
+ return new Date(value);
428
+ };
429
+ `;
430
+ }
431
+ if (hasDate) {
432
+ helpers += ` // date 형식으로 변환 (YYYY-MM-DD)
433
+ const toDateString = (date: Date | string | null | undefined): string => {
434
+ if (!date) return "";
435
+ const d = typeof date === "string" ? new Date(date) : date;
436
+ return d.toISOString().split("T")[0];
437
+ };
290
438
 
291
- // 수정일 기존 row 콜
439
+ // date 문자열을 Date로 변환
440
+ const fromDateString = (value: string): Date | null => {
441
+ if (!value) return null;
442
+ return new Date(value);
443
+ };
444
+ `;
445
+ }
446
+ return helpers;
447
+ })()}
292
448
  useEffect(() => {
293
449
  if (id) {
294
- ${names.capital}Service.get${names.capital}('A', id).then((row) => {
295
- setRow(row);
296
- setForm({
450
+ ${names.capital}Service.get${names.capital}("A", id).then((row) => {
451
+ setForm((prevForm) => ({
452
+ ...prevForm,
297
453
  ...row,
298
- ${columns
299
- .filter((col) => col.renderType === "number-fk_id")
300
- .map((col) => {
301
- if (col.nullable) {
302
- return `${col.name}: row.${col.name.replace("_id", "?.id")} ?? null`;
303
- } else {
304
- return `${col.name}: row.${col.name.replace("_id", ".id")}`;
305
- }
306
- })
307
- .join(",\n")}
308
- });
454
+ }));
309
455
  });
310
456
  }
311
- }, [id]);
312
-
313
- // CommonModal
314
- // const { doneModal, closeModal } = useCommonModal();
315
-
316
- // 저장
317
- const { goBack } = useGoBack();
318
- const handleSubmit = useCallback(() => {
319
- ${names.capital}Service.save([form]).then(([id]) => {
320
- if( mode === 'modal' ) {
321
- // doneModal();
322
- } else {
323
- goBack('/admin/${names.fsPlural}');
324
- }
325
- }).catch(defaultCatch);
326
- }, [ form, mode, id ]);
457
+ }, [id, setForm]);
458
+
459
+ const saveMutation = ${names.capital}Service.useSaveMutation();
460
+ const handleSubmit = () => {
461
+ saveMutation.mutate(
462
+ { spa: [form] },
463
+ {
464
+ onSuccess: () => {
465
+ queryClient.invalidateQueries({
466
+ queryKey: ["${names.capital}"],
467
+ });
468
+
469
+ if (mode === "modal") {
470
+ // modal mode
471
+ } else {
472
+ router.navigate({ to: "/admin/${names.fsPlural}" });
473
+ }
474
+ },
475
+ onError: defaultCatch,
476
+ },
477
+ );
478
+ };
327
479
 
328
- // 페이지
329
480
  const PAGE = {
330
- title: \`${entity.title ?? names.capital}\${id ? \`#\${id} 수정\` : ' 등록'}\`,
331
- }
481
+ title: \`${entity.title ?? names.capital}\${id ? \` #\${id} Edit\` : " Create"}\`,
482
+ };
332
483
 
333
484
  return (
334
- <div className="form">
335
- <Segment padded basic>
336
- <Segment padded color="grey">
337
- <div className="header-row">
338
- <Header>
339
- {PAGE.title}
340
- </Header>
341
- { mode !== 'modal' && <div className="buttons">
342
- <BackLink primary size="tiny" to="/admin/${
343
- names.fsPlural
344
- }" content="목록" icon="list" />
345
- </div>}
485
+ <div className="flex-1 overflow-auto">
486
+ <div className="max-w-[1800px] mx-auto p-8">
487
+ <div className="space-y-6 mb-8">
488
+ {/* Header */}
489
+ <div className="flex items-center justify-between">
490
+ <div className="flex items-center gap-2">
491
+ <FormIcon className="h-5 w-5" />
492
+ <span className="text-lg font-semibold h-5">{PAGE.title}</span>
493
+ </div>
494
+ {mode !== "modal" && (
495
+ <Button
496
+ variant="outline"
497
+ onClick={() => router.navigate({ to: "/admin/${names.fsPlural}" })}
498
+ icon={<ArrowLeftIcon />}
499
+ >
500
+ Back To List
501
+ </Button>
502
+ )}
346
503
  </div>
347
- <Form>
348
- ${columns
349
- .map((col) => {
350
- if (col.name === "created_at") {
351
- return `{form.id && (${this.wrapFG(
352
- `<div className="p-8px">{formatDateTime(form.${col.name})}</div>`,
353
- "등록일시",
354
- )})}`;
355
- } else {
356
- return this.wrapFG(
357
- this.renderColumn(entityId, col, names),
358
- (() => {
359
- if (col.label.endsWith("Id")) {
360
- try {
361
- const entity = EntityManager.get(col.label.replace("Id", ""));
362
- return entity.title ?? col.label;
363
- } catch {
364
- return col.label;
365
- }
366
- }
367
- return col.label;
368
- })(),
369
- );
370
- }
371
- })
372
- .join("\n")}
373
- <Segment basic textAlign="center">
374
- <Button type="submit" primary onClick={handleSubmit} content="저장" icon="save" />
375
- </Segment>
376
- </Form>
377
- </Segment>
378
- </Segment>
504
+
505
+ {/* Form Card */}
506
+ <Card className="border-border/40 bg-gray-50 shadow-sm">
507
+ <CardHeader className="px-4 border-b border-gray-200 flex items-center">
508
+ <CardTitle className="text-sm font-medium leading-none m-0">
509
+ {PAGE.title}
510
+ </CardTitle>
511
+ </CardHeader>
512
+ <CardContent className="p-6">
513
+ <div className="space-y-6">
514
+ ${columns
515
+ .filter((col) => col.name !== "created_at")
516
+ .map((col) => {
517
+ const label = (() => {
518
+ if (col.label.endsWith("Id")) {
519
+ try {
520
+ const entity = EntityManager.get(col.label.replace("Id", ""));
521
+ return entity.title ?? col.label;
522
+ } catch {
523
+ return col.label;
524
+ }
525
+ }
526
+ return col.label;
527
+ })();
528
+ return ` {/* ${label} */}
529
+ <div className="space-y-2">
530
+ <label className="block text-xs mb-1 text-gray-600">${label}</label>
531
+ ${this.renderColumn(entityId, col, names)}
532
+ </div>`;
533
+ })
534
+ .join("\n\n")}
535
+
536
+ {/* Save Button */}
537
+ <div className="flex items-center justify-between pt-4">
538
+ {form.id && form.created_at && (
539
+ <div className="flex items-center">
540
+ <label className="mr-2 text-xs text-gray-600">Created At:</label>
541
+ <span className="text-xs text-gray-600">
542
+ {String(form.created_at)}
543
+ </span>
544
+ </div>
545
+ )}
546
+ <Button
547
+ onClick={handleSubmit}
548
+ icon={<SaveIcon />}
549
+ >
550
+ Save
551
+ </Button>
552
+ </div>
553
+ </div>
554
+ </CardContent>
555
+ </Card>
556
+ </div>
557
+ </div>
379
558
  </div>
380
559
  );
381
- };
560
+ }
382
561
  `.trim(),
383
562
  importKeys: [],
384
563
  preTemplates,