sonamu 0.7.53 → 0.8.0

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 (271) hide show
  1. package/dist/api/config.d.ts +9 -1
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/sonamu.d.ts +21 -1
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +159 -65
  7. package/dist/auth/plugins/entity-definitions/anonymous.d.ts +10 -0
  8. package/dist/auth/plugins/entity-definitions/anonymous.d.ts.map +1 -0
  9. package/dist/auth/plugins/entity-definitions/anonymous.js +23 -0
  10. package/dist/auth/plugins/entity-definitions/api-key.d.ts +9 -0
  11. package/dist/auth/plugins/entity-definitions/api-key.d.ts.map +1 -0
  12. package/dist/auth/plugins/entity-definitions/api-key.js +199 -0
  13. package/dist/auth/plugins/entity-definitions/index.d.ts +6 -0
  14. package/dist/auth/plugins/entity-definitions/index.d.ts.map +1 -1
  15. package/dist/auth/plugins/entity-definitions/index.js +20 -2
  16. package/dist/auth/plugins/entity-definitions/jwt.d.ts +9 -0
  17. package/dist/auth/plugins/entity-definitions/jwt.d.ts.map +1 -0
  18. package/dist/auth/plugins/entity-definitions/jwt.js +67 -0
  19. package/dist/auth/plugins/entity-definitions/organization.d.ts +9 -0
  20. package/dist/auth/plugins/entity-definitions/organization.d.ts.map +1 -0
  21. package/dist/auth/plugins/entity-definitions/organization.js +424 -0
  22. package/dist/auth/plugins/entity-definitions/passkey.d.ts +10 -0
  23. package/dist/auth/plugins/entity-definitions/passkey.d.ts.map +1 -0
  24. package/dist/auth/plugins/entity-definitions/passkey.js +129 -0
  25. package/dist/auth/plugins/entity-definitions/sso.d.ts +10 -0
  26. package/dist/auth/plugins/entity-definitions/sso.d.ts.map +1 -0
  27. package/dist/auth/plugins/entity-definitions/sso.js +110 -0
  28. package/dist/auth/plugins/entity-definitions/types.d.ts +1 -1
  29. package/dist/auth/plugins/entity-definitions/types.d.ts.map +1 -1
  30. package/dist/auth/plugins/entity-definitions/types.js +1 -1
  31. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  32. package/dist/auth/plugins/wrappers/admin.js +2 -4
  33. package/dist/auth/plugins/wrappers/anonymous.d.ts +18 -0
  34. package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -0
  35. package/dist/auth/plugins/wrappers/anonymous.js +26 -0
  36. package/dist/auth/plugins/wrappers/api-key.d.ts +18 -0
  37. package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -0
  38. package/dist/auth/plugins/wrappers/api-key.js +38 -0
  39. package/dist/auth/plugins/wrappers/index.d.ts +6 -0
  40. package/dist/auth/plugins/wrappers/index.d.ts.map +1 -1
  41. package/dist/auth/plugins/wrappers/index.js +7 -1
  42. package/dist/auth/plugins/wrappers/jwt.d.ts +18 -0
  43. package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -0
  44. package/dist/auth/plugins/wrappers/jwt.js +30 -0
  45. package/dist/auth/plugins/wrappers/organization.d.ts +18 -0
  46. package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -0
  47. package/dist/auth/plugins/wrappers/organization.js +67 -0
  48. package/dist/auth/plugins/wrappers/passkey.d.ts +18 -0
  49. package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -0
  50. package/dist/auth/plugins/wrappers/passkey.js +32 -0
  51. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  52. package/dist/auth/plugins/wrappers/phone-number.js +2 -4
  53. package/dist/auth/plugins/wrappers/sso.d.ts +853 -0
  54. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -0
  55. package/dist/auth/plugins/wrappers/sso.js +36 -0
  56. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  57. package/dist/auth/plugins/wrappers/two-factor.js +2 -4
  58. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  59. package/dist/auth/plugins/wrappers/username.js +2 -4
  60. package/dist/bin/build-config.d.ts +2 -2
  61. package/dist/bin/build-config.js +6 -7
  62. package/dist/bin/cli.js +417 -32
  63. package/dist/bin/fixture.d.ts +27 -0
  64. package/dist/bin/fixture.d.ts.map +1 -0
  65. package/dist/bin/fixture.js +245 -0
  66. package/dist/cache/decorator.d.ts +4 -3
  67. package/dist/cache/decorator.d.ts.map +1 -1
  68. package/dist/cache/decorator.js +5 -4
  69. package/dist/cone/cone-generator.d.ts +33 -0
  70. package/dist/cone/cone-generator.d.ts.map +1 -0
  71. package/dist/cone/cone-generator.js +286 -0
  72. package/dist/database/_batch_update.d.ts.map +1 -1
  73. package/dist/database/_batch_update.js +16 -2
  74. package/dist/database/puri-subset.test-d.js +1 -1
  75. package/dist/database/puri-subset.types.d.ts +1 -1
  76. package/dist/database/puri-subset.types.d.ts.map +1 -1
  77. package/dist/database/puri-subset.types.js +1 -1
  78. package/dist/database/puri.d.ts +4 -0
  79. package/dist/database/puri.d.ts.map +1 -1
  80. package/dist/database/puri.js +20 -2
  81. package/dist/database/upsert-builder.d.ts.map +1 -1
  82. package/dist/database/upsert-builder.js +19 -3
  83. package/dist/dict/en.d.ts +15 -0
  84. package/dist/dict/en.d.ts.map +1 -1
  85. package/dist/dict/en.js +2 -1
  86. package/dist/dict/ko.d.ts +15 -0
  87. package/dist/dict/ko.d.ts.map +1 -1
  88. package/dist/dict/ko.js +2 -1
  89. package/dist/dict/rc-keys.d.ts +28 -0
  90. package/dist/dict/rc-keys.d.ts.map +1 -1
  91. package/dist/dict/rc-keys.js +31 -1
  92. package/dist/dict/sd.d.ts.map +1 -1
  93. package/dist/dict/sd.js +20 -4
  94. package/dist/entity/entity-manager.d.ts +298 -2
  95. package/dist/entity/entity-manager.d.ts.map +1 -1
  96. package/dist/entity/entity-manager.js +4 -1
  97. package/dist/entity/entity-template-cone.d.ts +14 -0
  98. package/dist/entity/entity-template-cone.d.ts.map +1 -0
  99. package/dist/entity/entity-template-cone.js +222 -0
  100. package/dist/entity/entity.d.ts +47 -2
  101. package/dist/entity/entity.d.ts.map +1 -1
  102. package/dist/entity/entity.js +161 -14
  103. package/dist/ssr/renderer.js +3 -3
  104. package/dist/syncer/api-parser.js +12 -1
  105. package/dist/syncer/checksum.d.ts +0 -14
  106. package/dist/syncer/checksum.d.ts.map +1 -1
  107. package/dist/syncer/checksum.js +1 -23
  108. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  109. package/dist/syncer/syncer-actions.js +8 -2
  110. package/dist/syncer/syncer.d.ts +1 -1
  111. package/dist/syncer/syncer.d.ts.map +1 -1
  112. package/dist/syncer/syncer.js +17 -10
  113. package/dist/tasks/workflow-manager.d.ts +13 -1
  114. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  115. package/dist/tasks/workflow-manager.js +18 -1
  116. package/dist/template/entity-converter.js +4 -4
  117. package/dist/template/helpers.d.ts +10 -0
  118. package/dist/template/helpers.d.ts.map +1 -1
  119. package/dist/template/helpers.js +48 -1
  120. package/dist/template/implementations/entry-server.template.d.ts +1 -1
  121. package/dist/template/implementations/entry-server.template.js +7 -2
  122. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  123. package/dist/template/implementations/generated.template.js +5 -1
  124. package/dist/template/implementations/generated_http.template.d.ts +1 -0
  125. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  126. package/dist/template/implementations/generated_http.template.js +6 -2
  127. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  128. package/dist/template/implementations/generated_sso.template.js +29 -8
  129. package/dist/template/implementations/queries.template.d.ts.map +1 -1
  130. package/dist/template/implementations/queries.template.js +9 -1
  131. package/dist/template/implementations/sd.template.d.ts +1 -1
  132. package/dist/template/implementations/sd.template.d.ts.map +1 -1
  133. package/dist/template/implementations/sd.template.js +28 -4
  134. package/dist/template/implementations/services.template.d.ts.map +1 -1
  135. package/dist/template/implementations/services.template.js +12 -12
  136. package/dist/template/implementations/view_form.template.d.ts +11 -7
  137. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  138. package/dist/template/implementations/view_form.template.js +97 -87
  139. package/dist/template/implementations/view_list.template.d.ts +3 -3
  140. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  141. package/dist/template/implementations/view_list.template.js +115 -109
  142. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  143. package/dist/template/implementations/view_search_input.template.js +18 -14
  144. package/dist/template/zod-converter.d.ts.map +1 -1
  145. package/dist/template/zod-converter.js +95 -7
  146. package/dist/testing/_relation-graph.js +1 -1
  147. package/dist/testing/data-explorer.d.ts +61 -0
  148. package/dist/testing/data-explorer.d.ts.map +1 -0
  149. package/dist/testing/data-explorer.js +274 -0
  150. package/dist/testing/faker-mappings.d.ts +20 -0
  151. package/dist/testing/faker-mappings.d.ts.map +1 -0
  152. package/dist/testing/faker-mappings.js +421 -0
  153. package/dist/testing/fixture-generator.d.ts +161 -0
  154. package/dist/testing/fixture-generator.d.ts.map +1 -0
  155. package/dist/testing/fixture-generator.js +954 -0
  156. package/dist/testing/fixture-manager.d.ts +6 -1
  157. package/dist/testing/fixture-manager.d.ts.map +1 -1
  158. package/dist/testing/fixture-manager.js +72 -4
  159. package/dist/testing/index.d.ts +3 -0
  160. package/dist/testing/index.d.ts.map +1 -1
  161. package/dist/testing/index.js +4 -1
  162. package/dist/types/types.d.ts +1520 -26
  163. package/dist/types/types.d.ts.map +1 -1
  164. package/dist/types/types.js +136 -22
  165. package/dist/ui/ai-client.d.ts.map +1 -1
  166. package/dist/ui/ai-client.js +9 -4
  167. package/dist/ui/api.d.ts.map +1 -1
  168. package/dist/ui/api.js +303 -24
  169. package/dist/ui-web/assets/index-CsUr-_pV.js +254 -0
  170. package/dist/ui-web/assets/index-T42zzs1K.css +1 -0
  171. package/dist/ui-web/index.html +2 -2
  172. package/dist/utils/fs-utils.d.ts +2 -1
  173. package/dist/utils/fs-utils.d.ts.map +1 -1
  174. package/dist/utils/fs-utils.js +14 -3
  175. package/package.json +19 -11
  176. package/src/api/config.ts +12 -1
  177. package/src/api/sonamu.ts +179 -65
  178. package/src/auth/plugins/entity-definitions/anonymous.ts +17 -0
  179. package/src/auth/plugins/entity-definitions/api-key.ts +93 -0
  180. package/src/auth/plugins/entity-definitions/index.ts +18 -0
  181. package/src/auth/plugins/entity-definitions/jwt.ts +35 -0
  182. package/src/auth/plugins/entity-definitions/organization.ts +215 -0
  183. package/src/auth/plugins/entity-definitions/passkey.ts +64 -0
  184. package/src/auth/plugins/entity-definitions/sso.ts +62 -0
  185. package/src/auth/plugins/entity-definitions/types.ts +11 -1
  186. package/src/auth/plugins/wrappers/admin.ts +1 -3
  187. package/src/auth/plugins/wrappers/anonymous.ts +30 -0
  188. package/src/auth/plugins/wrappers/api-key.ts +42 -0
  189. package/src/auth/plugins/wrappers/index.ts +6 -0
  190. package/src/auth/plugins/wrappers/jwt.ts +34 -0
  191. package/src/auth/plugins/wrappers/organization.ts +73 -0
  192. package/src/auth/plugins/wrappers/passkey.ts +36 -0
  193. package/src/auth/plugins/wrappers/phone-number.ts +1 -3
  194. package/src/auth/plugins/wrappers/sso.ts +40 -0
  195. package/src/auth/plugins/wrappers/two-factor.ts +1 -3
  196. package/src/auth/plugins/wrappers/username.ts +1 -3
  197. package/src/bin/build-config.ts +6 -6
  198. package/src/bin/cli.ts +452 -31
  199. package/src/bin/fixture.ts +302 -0
  200. package/src/cache/decorator.ts +4 -3
  201. package/src/cone/cone-generator.ts +363 -0
  202. package/src/database/_batch_update.ts +11 -0
  203. package/src/database/puri-subset.test-d.ts +13 -13
  204. package/src/database/puri-subset.types.ts +1 -1
  205. package/src/database/puri.ts +43 -1
  206. package/src/database/upsert-builder.ts +16 -2
  207. package/src/dict/en.ts +1 -0
  208. package/src/dict/ko.ts +1 -0
  209. package/src/dict/rc-keys.ts +32 -0
  210. package/src/dict/sd.ts +23 -3
  211. package/src/entity/entity-manager.ts +4 -0
  212. package/src/entity/entity-template-cone.ts +298 -0
  213. package/src/entity/entity.ts +189 -13
  214. package/src/shared/app.shared.ts.txt +5 -0
  215. package/src/shared/web.shared.ts.txt +9 -5
  216. package/src/skills/project/README.md +21 -0
  217. package/src/skills/project/architecture.md +373 -0
  218. package/src/skills/project/business-logic.md +270 -0
  219. package/src/skills/project/requirements.md +160 -0
  220. package/src/skills/sonamu/SKILL.md +168 -3
  221. package/src/skills/sonamu/api.md +102 -0
  222. package/src/skills/sonamu/database.md +220 -1
  223. package/src/skills/sonamu/entity-relations.md +89 -1
  224. package/src/skills/sonamu/fixture-cli.md +501 -0
  225. package/src/skills/sonamu/frontend.md +214 -0
  226. package/src/skills/sonamu/i18n.md +95 -0
  227. package/src/skills/sonamu/model.md +153 -0
  228. package/src/skills/sonamu/project-init.md +178 -8
  229. package/src/skills/sonamu/scaffolding.md +112 -0
  230. package/src/skills/sonamu/subset.md +9 -3
  231. package/src/skills/sonamu/testing.md +287 -2
  232. package/src/skills/sonamu/workflow.md +70 -5
  233. package/src/ssr/renderer.ts +2 -2
  234. package/src/syncer/api-parser.ts +12 -0
  235. package/src/syncer/checksum.ts +0 -38
  236. package/src/syncer/syncer-actions.ts +7 -1
  237. package/src/syncer/syncer.ts +16 -5
  238. package/src/tasks/workflow-manager.ts +29 -8
  239. package/src/template/entity-converter.ts +3 -3
  240. package/src/template/helpers.ts +49 -0
  241. package/src/template/implementations/entry-server.template.ts +1 -1
  242. package/src/template/implementations/generated.template.ts +4 -0
  243. package/src/template/implementations/generated_http.template.ts +1 -0
  244. package/src/template/implementations/generated_sso.template.ts +40 -11
  245. package/src/template/implementations/queries.template.ts +8 -0
  246. package/src/template/implementations/sd.template.ts +22 -3
  247. package/src/template/implementations/services.template.ts +11 -10
  248. package/src/template/implementations/view_form.template.ts +111 -101
  249. package/src/template/implementations/view_list.template.ts +120 -119
  250. package/src/template/implementations/view_search_input.template.ts +17 -13
  251. package/src/template/zod-converter.ts +103 -6
  252. package/src/testing/_relation-graph.ts +1 -1
  253. package/src/testing/data-explorer.ts +427 -0
  254. package/src/testing/faker-mappings.ts +434 -0
  255. package/src/testing/fixture-generator.ts +1166 -0
  256. package/src/testing/fixture-manager.ts +91 -6
  257. package/src/testing/index.ts +3 -0
  258. package/src/types/types.ts +222 -26
  259. package/src/ui/ai-client.ts +9 -1
  260. package/src/ui/api.ts +429 -23
  261. package/src/utils/fs-utils.ts +14 -1
  262. package/dist/template/implementations/view_enums_select.template.d.ts +0 -17
  263. package/dist/template/implementations/view_enums_select.template.d.ts.map +0 -1
  264. package/dist/template/implementations/view_enums_select.template.js +0 -62
  265. package/dist/template/implementations/view_id_async_select.template.d.ts +0 -17
  266. package/dist/template/implementations/view_id_async_select.template.d.ts.map +0 -1
  267. package/dist/template/implementations/view_id_async_select.template.js +0 -125
  268. package/dist/ui-web/assets/index-Bd_2AkLb.css +0 -1
  269. package/dist/ui-web/assets/index-BpSbhQWo.js +0 -225
  270. package/src/template/implementations/view_enums_select.template.ts +0 -65
  271. package/src/template/implementations/view_id_async_select.template.ts +0 -139
@@ -2,8 +2,12 @@ import inflection from "inflection";
2
2
  import { flat } from "radashi";
3
3
  import { z } from "zod";
4
4
  import { EntityManager, type EntityNamesRecord } from "../../entity/entity-manager";
5
- import type { RenderingNode, TemplateKey, TemplateOptions } from "../../types/types";
6
- import { getEnumInfoFromColName, getRelationPropFromColName } from "../helpers";
5
+ import type { RenderingNode, TemplateOptions } from "../../types/types";
6
+ import {
7
+ getEnumInfoFromColName,
8
+ getRelationNameFromColumnName,
9
+ getRelationPropFromColName,
10
+ } from "../helpers";
7
11
  import type { RenderedTemplate } from "../template";
8
12
  import { Template } from "../template";
9
13
 
@@ -52,8 +56,10 @@ export class Template__view_list extends Template {
52
56
  case "string-plain":
53
57
  case "string-date":
54
58
  case "number-id":
59
+ case "string-id":
55
60
  return `<>{${colName}}</>`;
56
- case "number-fk_id": {
61
+ case "number-fk_id":
62
+ case "string-fk_id": {
57
63
  try {
58
64
  const baseName = col.name.includes(".")
59
65
  ? (col.name.split(".").pop() ?? col.name).replace("_id", "")
@@ -169,7 +175,7 @@ export class Template__view_list extends Template {
169
175
  return "";
170
176
  }
171
177
  }
172
- } else if (col.renderType === "number-fk_id") {
178
+ } else if (col.renderType === "number-fk_id" || col.renderType === "string-fk_id") {
173
179
  try {
174
180
  const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
175
181
  const targetNames = EntityManager.getNamesFromId(relProp.with);
@@ -202,7 +208,7 @@ export class Template__view_list extends Template {
202
208
  }
203
209
  }
204
210
  return `<${componentId} {...register('${col.name}')} ${isClearable ? "clearable" : ""} />`;
205
- } else if (col.renderType === "number-fk_id") {
211
+ } else if (col.renderType === "number-fk_id" || col.renderType === "string-fk_id") {
206
212
  try {
207
213
  const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
208
214
  componentId = `${relProp.with}IdAsyncSelect`;
@@ -262,17 +268,31 @@ export class Template__view_list extends Template {
262
268
 
263
269
  .sort((a, b) => (a.name === "id" ? -1 : b.name === "id" ? 1 : 0))
264
270
  .map((col) => {
265
- const propCandidate = entity.props.find((p) => p.name === col.name);
266
271
  const rendered = this.renderColumn(entityId, col, names);
272
+
273
+ // 라벨 생성: common 필드(created_at)는 SD("common.{field}"), entity 필드는 SD("entity.{Entity}.{field}")
274
+ let label: string;
275
+ if (col.name === "id") {
276
+ label = '"ID"';
277
+ } else if (["created_at"].includes(col.name)) {
278
+ // camelCase로 변환 (created_at -> createdAt)
279
+ const camelName = col.name.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
280
+ label = `SD("common.${camelName}")`;
281
+ } else {
282
+ const labelName = getRelationNameFromColumnName(entityId, col.name);
283
+ label = `SD("entity.${names.capital}.${labelName}")`;
284
+ }
285
+
267
286
  return {
268
287
  name: col.name,
269
- label: col.name === "id" ? "ID" : (propCandidate?.desc ?? col.label),
288
+ label,
270
289
  tc: `(row) => ${rendered}`,
271
290
  fit:
291
+ col.name === "id" ||
272
292
  col.renderType === "number-id" ||
273
293
  col.renderType === "datetime" ||
274
294
  col.renderType === "string-datetime",
275
- align: col.renderType === "number-id" ? "center" : undefined,
295
+ align: col.name === "id" || col.renderType === "number-id" ? "center" : undefined,
276
296
  };
277
297
  });
278
298
 
@@ -282,7 +302,7 @@ export class Template__view_list extends Template {
282
302
  (col) =>
283
303
  col.name !== "id" &&
284
304
  col.name !== "queryMode" &&
285
- (["enums", "number-id"].includes(col.renderType) || col.name.endsWith("_id")),
305
+ ["enums", "number-id", "number-fk_id", "string-fk_id"].includes(col.renderType),
286
306
  )
287
307
  // orderBy가 가장 뒤로 오게 순서 조정
288
308
  .sort((a) => {
@@ -291,50 +311,6 @@ export class Template__view_list extends Template {
291
311
 
292
312
  // 필터 컬럼을 프리 템플릿으로 설정
293
313
  const preTemplates: RenderedTemplate["preTemplates"] = [];
294
- for (const col of filterColumns) {
295
- let key: TemplateKey;
296
- let targetEntityId = entityId;
297
- let enumId: string | undefined;
298
-
299
- if (col.renderType === "enums") {
300
- if (col.name === "search") {
301
- key = "view_enums_select";
302
- enumId = `${names.capital}SearchField`;
303
- targetEntityId = names.capital;
304
- } else {
305
- key = "view_enums_select";
306
- // config.enumId 우선 사용
307
- if (col.config && "enumId" in col.config) {
308
- enumId = (col.config as { enumId: string }).enumId;
309
- targetEntityId = entityId;
310
- } else {
311
- try {
312
- const { targetEntityNames, id } = getEnumInfoFromColName(entityId, col.name);
313
- targetEntityId = targetEntityNames.capital;
314
- enumId = id;
315
- } catch {
316
- continue;
317
- }
318
- }
319
- }
320
- } else {
321
- key = "view_id_async_select";
322
- try {
323
- const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
324
- targetEntityId = relProp.with;
325
- } catch {
326
- continue;
327
- }
328
- }
329
-
330
- preTemplates.push({
331
- key,
332
- options: {
333
- entityId: targetEntityId,
334
- enumId,
335
- },
336
- });
337
- }
338
314
 
339
315
  // 컬럼에서 사용하는 enum들 수집
340
316
  const columnEnums: string[] = [];
@@ -347,14 +323,6 @@ export class Template__view_list extends Template {
347
323
  }
348
324
  });
349
325
 
350
- // SearchInput
351
- preTemplates?.push({
352
- key: "view_search_input",
353
- options: {
354
- entityId,
355
- },
356
- });
357
-
358
326
  // 디폴트 파라미터
359
327
  // const def = this.getDefault(filterColumns);
360
328
 
@@ -368,13 +336,14 @@ import { Card, CardContent, CardHeader } from "@sonamu-kit/react-components/comp
368
336
  import { Badge } from "@sonamu-kit/react-components/components";
369
337
  import { Button } from "@sonamu-kit/react-components/components";
370
338
  import { Pagination, Table, TableBody, TableCell, type TableCol, TableHead, TableHeader, TableRow } from "@sonamu-kit/react-components/components";
371
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@sonamu-kit/react-components/components";
339
+ import { EnumSelect } from "@sonamu-kit/react-components/components";
372
340
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@sonamu-kit/react-components/components";
373
341
  import { Input } from "@sonamu-kit/react-components/components";
374
342
  import { Checkbox } from "@sonamu-kit/react-components/components";
343
+ import { SonamuFilterModal, SonamuFilterPopover, extractFieldMetaFromSchema, type Rule } from "@sonamu-kit/react-components/components";
375
344
 
376
345
  import { useListParams, numF, dateF, datetimeF } from "@sonamu-kit/react-components/lib";
377
- import { ${names.capital}SubsetA } from "@/services/sonamu.generated";
346
+ import { ${names.capital}SubsetA, ${names.capital}BaseSchema } from "@/services/sonamu.generated";
378
347
  import { ${names.capital}Service } from "@/services/services.generated";
379
348
  import { ${names.capital}ListParams } from "@/services/${names.fs}/${names.fs}.types";
380
349
  import { ${(() => {
@@ -411,37 +380,30 @@ import { ${(() => {
411
380
 
412
381
  return [...baseEnums, ...enumImports].join(", ");
413
382
  })()} } from "@/services/sonamu.generated";
383
+ import { IdAsyncSelect } from "@sonamu-kit/react-components/components";
414
384
  ${(() => {
415
- // FK 필드의 AsyncSelect 컴포넌트 import
416
- const fkColumns = filterColumns.filter((col) => col.name.endsWith("_id") && col.name !== "id");
417
- return fkColumns
385
+ // FK 필드의 AsyncIdConfig import
386
+ const fkColumns = filterColumns.filter(
387
+ (col) => col.renderType === "number-fk_id" || col.renderType === "string-fk_id",
388
+ );
389
+ const configNames = fkColumns
418
390
  .map((col) => {
419
391
  try {
420
392
  const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
421
- const targetNames = EntityManager.getNamesFromId(relProp.with);
422
- return `import { ${relProp.with}IdAsyncSelect } from "@/components/${targetNames.fs}/${relProp.with}IdAsyncSelect";`;
393
+ return `${relProp.with}AsyncIdConfig`;
423
394
  } catch {
424
395
  return "";
425
396
  }
426
397
  })
427
- .filter(Boolean)
428
- .join("\n");
398
+ .filter(Boolean);
399
+ return configNames.length > 0
400
+ ? `import { ${configNames.join(", ")} } from "@/services/services.generated";`
401
+ : "";
429
402
  })()}
430
- ${
431
- filterColumns.some((col) => col.name === "search")
432
- ? `
433
- import { ${names.capital}SearchFieldSelect } from "@/components/${names.fs}/${names.capital}SearchFieldSelect";`
434
- : ""
435
- }
436
- ${
437
- filterColumns.some((col) => col.name === "orderBy")
438
- ? `
439
- import { ${names.capital}OrderBySelect } from "@/components/${names.fs}/${names.capital}OrderBySelect";`
440
- : ""
441
- }
442
403
 
443
404
  import EditIcon from "~icons/lucide/square-pen";
444
405
  import TrashIcon from "~icons/lucide/trash-2";
406
+ import FilterIcon from "~icons/mdi/filter-variant";
445
407
  import ListIcon from "~icons/mdi/format-list-bulleted";
446
408
  import SearchIcon from "~icons/mdi/magnify";
447
409
  import { SD } from "@/i18n/sd.generated";
@@ -455,9 +417,11 @@ function ${names.capital}List({}: ${names.capital}ListProps) {
455
417
  const [selectedItems, setSelectedItems] = useState<Set<${idTsType}>>(new Set());
456
418
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
457
419
  const [itemToDelete, setItemToDelete] = useState<{ id: ${idTsType}; name?: string } | null>(null);
420
+ const [filterModalOpen, setFilterModalOpen] = useState(false);
421
+ const [appliedRules, setAppliedRules] = useState<Rule[]>([]);
458
422
 
459
423
  // 리스트 필터
460
- const { listParams, register } = useListParams(${names.capital}ListParams, {
424
+ const { listParams, register, setListParams } = useListParams(${names.capital}ListParams, {
461
425
  num: 10,
462
426
  page: 1,
463
427
  keyword: "",${
@@ -471,6 +435,7 @@ function ${names.capital}List({}: ${names.capital}ListProps) {
471
435
  orderBy: ${names.capital}OrderBy.options[0],`
472
436
  : ""
473
437
  }
438
+ sonamuFilter: {},
474
439
  });
475
440
 
476
441
  // 리스트 쿼리
@@ -480,7 +445,7 @@ function ${names.capital}List({}: ${names.capital}ListProps) {
480
445
  // 현재 경로와 타이틀
481
446
  const PAGE = {
482
447
  route: "/admin/${names.fsPlural}",
483
- title: "${entity.title ?? names.capital}",
448
+ title: SD("entity.list")(SD("entity.${names.capital}")),
484
449
  };
485
450
 
486
451
  // 컬럼 정의
@@ -489,7 +454,7 @@ function ${names.capital}List({}: ${names.capital}ListProps) {
489
454
  ${columns
490
455
  .map(
491
456
  (col) => ` {
492
- label: "${col.label}",
457
+ label: ${col.label},
493
458
  tc: ${col.tc},${
494
459
  col.fit
495
460
  ? `
@@ -505,7 +470,7 @@ ${columns
505
470
  )
506
471
  .join(",\n")},
507
472
  {
508
- label: "Manage",
473
+ label: SD("common.manage"),
509
474
  fit: true,
510
475
  align: "center",
511
476
  tc: (row) => (
@@ -583,10 +548,12 @@ ${columns
583
548
  <div className="flex items-center gap-3 flex-wrap">
584
549
  ${
585
550
  filterColumns.some((col) => col.name === "search")
586
- ? ` <${names.capital}SearchFieldSelect
551
+ ? ` <EnumSelect
552
+ enum={${names.capital}SearchField}
553
+ labels={${names.capital}SearchFieldLabel}
587
554
  {...register("search")}
588
- placeholder="Search Type"
589
- className="w-[200px] h-8 bg-white border-gray-300 text-xs"
555
+ placeholder={SD("common.searchType")}
556
+ className="w-50 h-8 bg-white border-gray-300 text-xs"
590
557
  />`
591
558
  : ""
592
559
  }
@@ -594,7 +561,7 @@ ${
594
561
  <div className="relative flex-1 max-w-xs">
595
562
  <Input
596
563
  {...register("keyword")}
597
- placeholder="Search..."
564
+ placeholder={SD("common.search")}
598
565
  className="h-8 pr-8 text-xs bg-white border-gray-300"
599
566
  />
600
567
  <Button
@@ -605,13 +572,35 @@ ${
605
572
  />
606
573
  </div>
607
574
 
608
- <div className="ml-auto">
575
+ <div className="ml-auto flex items-center gap-2">
609
576
  <Button
610
577
  className="h-8 px-4 bg-primary hover:bg-primary/90 text-white"
611
578
  onClick={() => navigate({ to: \`\${PAGE.route}/form\` })}
612
579
  >
613
- <span className="text-xs">Create</span>
580
+ <span className="text-xs">{SD("common.create")}</span>
614
581
  </Button>
582
+ <SonamuFilterPopover
583
+ rules={appliedRules}
584
+ fieldMeta={extractFieldMetaFromSchema(
585
+ ${names.capital}BaseSchema,
586
+ SD as (key: string) => string,
587
+ )}
588
+ >
589
+ <Button
590
+ variant="outline"
591
+ size="sm"
592
+ icon={<FilterIcon />}
593
+ onClick={() => setFilterModalOpen(true)}
594
+ className="h-8"
595
+ >
596
+ <span className="text-xs">{SD("rc.sonamuFilter.title")}</span>
597
+ {appliedRules.length > 0 && (
598
+ <Badge variant="secondary" className="ml-1">
599
+ {appliedRules.length}
600
+ </Badge>
601
+ )}
602
+ </Button>
603
+ </SonamuFilterPopover>
615
604
  </div>
616
605
  </div>
617
606
 
@@ -626,32 +615,30 @@ ${filterColumns
626
615
  col.config && "enumId" in col.config
627
616
  ? (col.config as { enumId: string }).enumId
628
617
  : getEnumInfoFromColName(entityId, col.name).id;
629
- return ` <Select key={\`${col.name}-\${listParams.${col.name}}\`} {...register("${col.name}")} clearable>
630
- <SelectTrigger className="w-[200px] h-8 bg-white border-gray-300 text-xs">
631
- <SelectValue placeholder="${col.label}" className="truncate" />
632
- </SelectTrigger>
633
- <SelectContent>
634
- {${enumId}.options.map((key) => (
635
- <SelectItem key={key} value={key}>
636
- {${enumId}Label[key]}
637
- </SelectItem>
638
- ))}
639
- </SelectContent>
640
- </Select>`;
618
+ return ` <EnumSelect
619
+ key={\`${col.name}-\${listParams.${col.name}}\`}
620
+ enum={${enumId}}
621
+ labels={${enumId}Label}
622
+ {...register("${col.name}")}
623
+ placeholder="${col.label}"
624
+ clearable
625
+ className="w-50 h-8 bg-white border-gray-300 text-xs"
626
+ />`;
641
627
  } catch {
642
628
  return "";
643
629
  }
644
630
  }
645
- // FK 필드 (AsyncSelect)
646
- if (col.name.endsWith("_id") && col.name !== "id") {
631
+ // FK 필드 (IdAsyncSelect)
632
+ if (col.renderType === "number-fk_id" || col.renderType === "string-fk_id") {
647
633
  try {
648
634
  const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
649
- return ` <${relProp.with}IdAsyncSelect
635
+ return ` <IdAsyncSelect
636
+ config={${relProp.with}AsyncIdConfig}
650
637
  subset="A"
651
638
  {...register("${col.name}")}
652
639
  placeholder="${col.label ?? relProp.with}"
653
640
  clearable
654
- className="w-[200px] h-8 text-xs"
641
+ className="w-50 h-8 text-xs"
655
642
  />`;
656
643
  } catch {
657
644
  return "";
@@ -663,15 +650,17 @@ ${filterColumns
663
650
  .join("\n")}
664
651
  ${
665
652
  filterColumns.some((col) => col.name === "orderBy")
666
- ? ` <${names.capital}OrderBySelect
653
+ ? ` <EnumSelect
654
+ enum={${names.capital}OrderBy}
655
+ labels={${names.capital}OrderByLabel}
667
656
  {...register("orderBy")}
668
- placeholder="Sort"
669
- textPrefix="Sort: "
670
- className="w-[200px] h-8 bg-white border-gray-300 text-xs"
657
+ placeholder={SD("common.sort")}
658
+ textPrefix={\`\${SD("common.sort")}: \`}
659
+ className="w-50 h-8 bg-white border-gray-300 text-xs"
671
660
  />`
672
661
  : ""
673
662
  }
674
- <span className="text-xs text-muted-foreground">{total ?? 0} results</span>
663
+ <span className="text-xs text-muted-foreground">{SD("common.results")(total ?? 0)}</span>
675
664
  </div>
676
665
  </div>
677
666
  </CardHeader>
@@ -730,17 +719,29 @@ ${
730
719
  <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
731
720
  <AlertDialogContent>
732
721
  <AlertDialogHeader>
733
- <AlertDialogTitle>Are you sure?</AlertDialogTitle>
734
- <AlertDialogDescription>
735
- This action cannot be undone. This will permanently delete this item.
736
- </AlertDialogDescription>
722
+ <AlertDialogTitle>{SD("delete.confirm.title")}</AlertDialogTitle>
723
+ <AlertDialogDescription>{SD("delete.confirm.description")}</AlertDialogDescription>
737
724
  </AlertDialogHeader>
738
725
  <AlertDialogFooter>
739
- <AlertDialogCancel>Cancel</AlertDialogCancel>
740
- <AlertDialogAction onClick={handleConfirmDelete}>Delete</AlertDialogAction>
726
+ <AlertDialogCancel>{SD("common.cancel")}</AlertDialogCancel>
727
+ <AlertDialogAction onClick={handleConfirmDelete}>
728
+ {SD("common.delete")}
729
+ </AlertDialogAction>
741
730
  </AlertDialogFooter>
742
731
  </AlertDialogContent>
743
732
  </AlertDialog>
733
+
734
+ {/* Sonamu Filter Modal */}
735
+ <SonamuFilterModal
736
+ baseSchema={${names.capital}BaseSchema}
737
+ open={filterModalOpen}
738
+ onOpenChange={setFilterModalOpen}
739
+ initialRules={appliedRules}
740
+ onApply={(filters, rules) => {
741
+ setListParams({ ...listParams, sonamuFilter: filters, page: 1 });
742
+ setAppliedRules(rules);
743
+ }}
744
+ />
744
745
  </div>
745
746
  );
746
747
  }
@@ -24,37 +24,32 @@ export class Template__view_search_input extends Template {
24
24
  return {
25
25
  ...this.getTargetAndPath(names),
26
26
  body: `
27
- import { Button, Input } from "@sonamu-kit/react-components/components";
27
+ import { Button, EnumSelect, Input } from "@sonamu-kit/react-components/components";
28
+ import { useSonamuBaseContext } from "@sonamu-kit/react-components/contexts";
28
29
  import type React from "react";
29
30
  import { useState } from "react";
30
- import { ${names.capital}SearchFieldSelect } from "@/components/${names.fs}/${names.capital}SearchFieldSelect";
31
+ import { ${names.capital}SearchField, ${names.capital}SearchFieldLabel } from "@/services/sonamu.generated";
31
32
  import SearchIcon from "~icons/lucide/search";
32
33
  import { SD } from "@/i18n/sd.generated";
33
34
  export type ${names.capital}SearchInputProps = {
34
35
  input: {
35
36
  value?: string;
36
- onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
37
+ onValueChange?: (value: string | null | undefined) => void;
37
38
  };
38
39
  dropdown: {
39
40
  value?: string;
40
- onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
41
+ onValueChange?: (value: string | null | undefined) => void;
41
42
  };
42
43
  };
43
44
 
44
45
  export function ${names.capital}SearchInput({
45
- input: { value: inputValue, onChange: inputOnChange },
46
+ input: { value: inputValue, onValueChange: inputOnValueChange },
46
47
  dropdown: dropdownProps,
47
48
  }: ${names.capital}SearchInputProps) {
48
49
  const [keyword, setKeyword] = useState<string>(inputValue ?? "");
49
50
 
50
51
  const handleSearch = () => {
51
- if (inputOnChange) {
52
- const syntheticEvent = {
53
- target: { value: keyword },
54
- currentTarget: { value: keyword },
55
- } as React.ChangeEvent<HTMLInputElement>;
56
- inputOnChange(syntheticEvent);
57
- }
52
+ inputOnValueChange?.(keyword || undefined);
58
53
  };
59
54
 
60
55
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -65,7 +60,16 @@ export function ${names.capital}SearchInput({
65
60
 
66
61
  return (
67
62
  <div className="flex items-center gap-1">
68
- <${names.capital}SearchFieldSelect {...dropdownProps} />
63
+ <EnumSelect
64
+ enum={${names.capital}SearchField}
65
+ labels={${names.capital}SearchFieldLabel}
66
+ value={dropdownProps.value}
67
+ onValueChange={(value) => {
68
+ dropdownProps.onValueChange?.(value as string | null | undefined);
69
+ }}
70
+ placeholder={SD("common.search")}
71
+ className="w-[150px] h-8"
72
+ />
69
73
  <div className="relative flex items-center">
70
74
  <Input
71
75
  type="text"
@@ -57,6 +57,7 @@ import {
57
57
  type RenderingNode,
58
58
  SonamuFileArraySchema,
59
59
  SonamuFileSchema,
60
+ type ZodStringFormat,
60
61
  } from "../types/types";
61
62
  import { createImportUrl } from "../utils/esm-utils";
62
63
  import { runtimePath } from "../utils/path-utils";
@@ -87,6 +88,72 @@ export const BUILT_IN_TYPES = {
87
88
  },
88
89
  } as const;
89
90
 
91
+ /**
92
+ * zodFormat을 Zod 4 코드 문자열로 변환합니다.
93
+ * Zod 4에서는 z.email(), z.uuid() 등 독립적인 함수 형태를 사용합니다.
94
+ */
95
+ function zodFormatToCode(format: ZodStringFormat): string {
96
+ // ISO 포맷은 z.iso.xxx() 형태
97
+ const isoFormats: Record<string, string> = {
98
+ isoDate: "z.iso.date()",
99
+ isoTime: "z.iso.time()",
100
+ isoDatetime: "z.iso.datetime()",
101
+ isoDuration: "z.iso.duration()",
102
+ };
103
+
104
+ // hash 포맷은 z.hash("algorithm") 형태
105
+ const hashFormats: Record<string, string> = {
106
+ hashMd5: 'z.hash("md5")',
107
+ hashSha1: 'z.hash("sha1")',
108
+ hashSha256: 'z.hash("sha256")',
109
+ hashSha384: 'z.hash("sha384")',
110
+ hashSha512: 'z.hash("sha512")',
111
+ };
112
+
113
+ if (format in isoFormats) {
114
+ return isoFormats[format];
115
+ }
116
+
117
+ if (format in hashFormats) {
118
+ return hashFormats[format];
119
+ }
120
+
121
+ // 기본 포맷은 z.xxx() 형태 (Zod 4)
122
+ return `z.${format}()`;
123
+ }
124
+
125
+ /**
126
+ * zodFormat을 Zod 4 타입으로 변환합니다.
127
+ * Zod 4에서는 z.email(), z.uuid() 등 독립적인 함수 형태를 사용합니다.
128
+ */
129
+ function zodFormatToType(format: ZodStringFormat): z.ZodType {
130
+ // ISO 포맷은 z.iso.xxx() 형태
131
+ switch (format) {
132
+ case "isoDate":
133
+ return z.iso.date();
134
+ case "isoTime":
135
+ return z.iso.time();
136
+ case "isoDatetime":
137
+ return z.iso.datetime();
138
+ case "isoDuration":
139
+ return z.iso.duration();
140
+ // hash 포맷은 z.hash("algorithm") 형태
141
+ case "hashMd5":
142
+ return z.hash("md5");
143
+ case "hashSha1":
144
+ return z.hash("sha1");
145
+ case "hashSha256":
146
+ return z.hash("sha256");
147
+ case "hashSha384":
148
+ return z.hash("sha384");
149
+ case "hashSha512":
150
+ return z.hash("sha512");
151
+ // 기본 포맷은 z.xxx() 형태 (Zod 4)
152
+ default:
153
+ return (z as unknown as Record<string, () => z.ZodType>)[format]();
154
+ }
155
+ }
156
+
90
157
  /**
91
158
  * Zod 타입 ID로부터 동적으로 Zod 스키마를 로드합니다.
92
159
  * 내장 타입(BUILT_IN_TYPE_IDS)은 바로 반환하고,
@@ -136,17 +203,29 @@ export async function propToZodType(prop: EntityProp): Promise<z.ZodTypeAny> {
136
203
  } else if (isEnumArrayProp(prop)) {
137
204
  zodType = (await getZodTypeById(prop.id)).array();
138
205
  } else if (isStringSingleProp(prop)) {
139
- if (prop.length) {
206
+ if (prop.zodFormat) {
207
+ zodType = zodFormatToType(prop.zodFormat);
208
+ if (prop.length && "max" in zodType) {
209
+ zodType = (zodType as z.ZodString).max(prop.length);
210
+ }
211
+ } else if (prop.length) {
140
212
  zodType = z.string().max(prop.length);
141
213
  } else {
142
214
  zodType = z.string();
143
215
  }
144
216
  } else if (isStringArrayProp(prop)) {
145
- if (prop.length) {
146
- zodType = z.string().max(prop.length).array();
217
+ let elementType: z.ZodType;
218
+ if (prop.zodFormat) {
219
+ elementType = zodFormatToType(prop.zodFormat);
220
+ if (prop.length && "max" in elementType) {
221
+ elementType = (elementType as z.ZodString).max(prop.length);
222
+ }
223
+ } else if (prop.length) {
224
+ elementType = z.string().max(prop.length);
147
225
  } else {
148
- zodType = z.string().array();
226
+ elementType = z.string();
149
227
  }
228
+ zodType = elementType.array();
150
229
  } else if (isNumberSingleProp(prop)) {
151
230
  zodType = z.number();
152
231
  } else if (isNumberArrayProp(prop)) {
@@ -220,13 +299,27 @@ export function propToZodTypeDef(prop: EntityProp, injectImportKeys: string[]):
220
299
  stmt = `${prop.name}: ${prop.id}.array()`;
221
300
  injectImportKeys.push(prop.id);
222
301
  } else if (isStringSingleProp(prop)) {
223
- if (prop.length) {
302
+ if (prop.zodFormat) {
303
+ const base = zodFormatToCode(prop.zodFormat);
304
+ if (prop.length) {
305
+ stmt = `${prop.name}: ${base}.max(${prop.length})`;
306
+ } else {
307
+ stmt = `${prop.name}: ${base}`;
308
+ }
309
+ } else if (prop.length) {
224
310
  stmt = `${prop.name}: z.string().max(${prop.length})`;
225
311
  } else {
226
312
  stmt = `${prop.name}: z.string()`;
227
313
  }
228
314
  } else if (isStringArrayProp(prop)) {
229
- if (prop.length) {
315
+ if (prop.zodFormat) {
316
+ const base = zodFormatToCode(prop.zodFormat);
317
+ if (prop.length) {
318
+ stmt = `${prop.name}: ${base}.max(${prop.length}).array()`;
319
+ } else {
320
+ stmt = `${prop.name}: ${base}.array()`;
321
+ }
322
+ } else if (prop.length) {
230
323
  stmt = `${prop.name}: z.string().max(${prop.length}).array()`;
231
324
  } else {
232
325
  stmt = `${prop.name}: z.string().array()`;
@@ -653,6 +746,10 @@ function resolveRenderType(key: string, zodType: z.ZodTypeAny): RenderingNode["r
653
746
  return "string-datetime";
654
747
  } else if (key.endsWith("date")) {
655
748
  return "string-date";
749
+ } else if (key === "id") {
750
+ return "string-id";
751
+ } else if (key.endsWith("_id")) {
752
+ return "string-fk_id";
656
753
  } else {
657
754
  return "string-plain";
658
755
  }
@@ -44,7 +44,7 @@ export class RelationGraph {
44
44
  }
45
45
  } else if (isManyToManyRelationProp(prop)) {
46
46
  // ManyToMany 관계의 경우 양방향 의존성 추가
47
- const relatedIds = column.value as unknown as number[];
47
+ const relatedIds = column.value as unknown as (number | string)[];
48
48
  for (const relatedId of relatedIds) {
49
49
  const relatedFixtureId = `${prop.with}#${relatedId}`;
50
50
  if (this.graph.has(relatedFixtureId)) {