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
@@ -0,0 +1,52 @@
1
+ import type { SSRRoute } from "./types";
2
+
3
+ const ssrRoutes: SSRRoute[] = [];
4
+
5
+ export function registerSSR(route: SSRRoute): void {
6
+ ssrRoutes.push(route);
7
+ }
8
+
9
+ export function getSSRRoutes(): SSRRoute[] {
10
+ return ssrRoutes;
11
+ }
12
+
13
+ export function clearSSRRoutes(): void {
14
+ ssrRoutes.length = 0;
15
+ }
16
+
17
+ export function matchSSRRoute(
18
+ url: string,
19
+ ): { route: SSRRoute; params: Record<string, string> } | null {
20
+ for (const route of ssrRoutes) {
21
+ const params = matchPath(route.path, url);
22
+ if (params !== null) {
23
+ return { route, params };
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+
29
+ // 간단한 path matching
30
+ export function matchPath(pattern: string, url: string): Record<string, string> | null {
31
+ const patternParts = pattern.split("/").filter(Boolean);
32
+ const urlParts = url.split("?")[0].split("/").filter(Boolean);
33
+
34
+ if (patternParts.length !== urlParts.length) {
35
+ return null;
36
+ }
37
+
38
+ const params: Record<string, string> = {};
39
+
40
+ for (let i = 0; i < patternParts.length; i++) {
41
+ const patternPart = patternParts[i];
42
+ const urlPart = urlParts[i];
43
+
44
+ if (patternPart.startsWith(":")) {
45
+ params[patternPart.slice(1)] = urlPart;
46
+ } else if (patternPart !== urlPart) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ return params;
52
+ }
@@ -0,0 +1,105 @@
1
+ import path from "node:path";
2
+ import type { FastifyReply, FastifyRequest } from "fastify";
3
+ import type { ViteDevServer } from "vite";
4
+ import type { SonamuFastifyConfig } from "../types/types";
5
+ import type { PreloadedData, SSRRoute } from "./types";
6
+
7
+ export async function renderSSR(
8
+ url: string,
9
+ route: SSRRoute,
10
+ params: Record<string, string>,
11
+ request: FastifyRequest,
12
+ reply: FastifyReply,
13
+ config: SonamuFastifyConfig,
14
+ vite?: ViteDevServer,
15
+ ): Promise<string> {
16
+ const { Sonamu } = await import("../api/sonamu");
17
+
18
+ // 1. preload 실행 → SSRQuery[] 획득 (dev/prod 공통)
19
+ const preloadConfig = route.preload ? route.preload(params) : [];
20
+ const preloadedData: PreloadedData[] = [];
21
+
22
+ for (const { modelName, methodName, params: apiParams, serviceKey } of preloadConfig) {
23
+ const api = Sonamu.syncer.apis.find(
24
+ (a) => a.modelName === modelName && a.methodName === methodName,
25
+ );
26
+
27
+ if (!api) {
28
+ console.warn(`API not found: ${modelName}.${methodName}`);
29
+ continue;
30
+ }
31
+
32
+ try {
33
+ const result = await Sonamu.invokeApiForSSR(api, apiParams, config, request, reply);
34
+ preloadedData.push({
35
+ queryKey: [...serviceKey, ...apiParams],
36
+ data: result,
37
+ });
38
+ } catch (e) {
39
+ console.error(`Failed to preload ${modelName}.${methodName}:`, e);
40
+ }
41
+ }
42
+
43
+ // 2. Dev/Prod 스크립트 추출
44
+ let viteScripts: string;
45
+ let render: (
46
+ url: string,
47
+ preloadedData: PreloadedData[],
48
+ ) => Promise<{ html: string; dehydratedState: unknown }>;
49
+
50
+ if (vite) {
51
+ // Dev: Vite Dev Server
52
+ const fs = await import("node:fs/promises");
53
+ const indexHtmlPath = path.join(vite.config.root, "index.html");
54
+ const originalHtml = await fs.readFile(indexHtmlPath, "utf-8");
55
+ const transformedHtml = await vite.transformIndexHtml(url, originalHtml);
56
+
57
+ // Vite가 주입한 스크립트 추출
58
+ viteScripts = extractScriptTags(transformedHtml);
59
+
60
+ const entryModule = await vite.ssrLoadModule("/src/entry-server.generated.tsx");
61
+ render = entryModule.render;
62
+ } else {
63
+ // Prod: 빌드된 파일
64
+ const fs = await import("node:fs");
65
+ const webDistPath = path.join(Sonamu.apiRootPath, "public", "web");
66
+ const ssrPath = path.join(Sonamu.apiRootPath, "dist", "ssr");
67
+
68
+ // 빌드된 index.html에서 스크립트 추출
69
+ const builtHtml = fs.readFileSync(path.join(webDistPath, "index.html"), "utf-8");
70
+ viteScripts = extractScriptTags(builtHtml);
71
+
72
+ const entryModule = await import(path.join(ssrPath, "entry-server.generated.js"));
73
+ render = entryModule.render;
74
+ }
75
+
76
+ // 3. RouterProvider 렌더링 (full document)
77
+ const { html: fullDocHtml, dehydratedState } = await render(url, preloadedData);
78
+
79
+ // 4. SSR 데이터 스크립트 생성
80
+ const ssrDataScript = dehydratedState
81
+ ? `<script>window.__SONAMU_SSR__ = ${JSON.stringify(dehydratedState).replace(/</g, "\\u003c")};</script>`
82
+ : "";
83
+
84
+ // 5. SSR Config 스크립트 생성 (disableHydrate)
85
+ const ssrConfigScript = route.disableHydrate
86
+ ? `<script>window.__SONAMU_SSR_CONFIG__ = ${JSON.stringify({ disableHydrate: true })};</script>`
87
+ : "";
88
+
89
+ // 6. Vite 스크립트와 SSR 데이터를 </body> 직전에 주입
90
+ const finalHtml = fullDocHtml.replace(
91
+ "</body>",
92
+ `${ssrConfigScript}\n${ssrDataScript}\n${viteScripts}\n</body>`,
93
+ );
94
+
95
+ return finalHtml;
96
+ }
97
+
98
+ /**
99
+ * HTML에서 <script>, <link> 태그를 추출
100
+ */
101
+ function extractScriptTags(html: string): string {
102
+ const scriptRegex = /<script[^>]*>[\s\S]*?<\/script>|<link[^>]*>/gi;
103
+ const matches = html.match(scriptRegex) || [];
104
+ return matches.join("\n");
105
+ }
@@ -0,0 +1,20 @@
1
+ // Branded type - 실수로 일반 객체 사용 방지
2
+ export type SSRQuery = {
3
+ modelName: string; // 'UserModel' - 서버 모델 호출용
4
+ methodName: string; // 'findById' - 서버 메서드 호출용
5
+ params: unknown[]; // [subset, id] - Context 제외한 실제 파라미터
6
+ serviceKey: [string, string]; // ['User', 'getUsers'] - React Query queryKey용
7
+ } & { __brand: "SSRQuery" };
8
+
9
+ export type PreloadConfig = SSRQuery[];
10
+
11
+ export type SSRRoute = {
12
+ path: string;
13
+ preload?: (params: Record<string, string>) => PreloadConfig;
14
+ disableHydrate?: boolean;
15
+ };
16
+
17
+ export type PreloadedData = {
18
+ queryKey: unknown[];
19
+ data: unknown;
20
+ };
@@ -12,6 +12,7 @@ import type { WorkflowMetadata } from "..";
12
12
  import { registeredApis } from "../api/decorators";
13
13
  import { Sonamu } from "../api/sonamu";
14
14
  import { EntityManager, type EntityNamesRecord } from "../entity/entity-manager";
15
+ import { AlreadyProcessedException } from "../exceptions/so-exceptions";
15
16
  import { Naite } from "../naite/naite";
16
17
  import { TemplateManager } from "../template/template-manager";
17
18
  import type { GenerateOptions, PathAndCode } from "../types/types";
@@ -63,6 +64,18 @@ export class Syncer {
63
64
  const changedFiles = await findChangedFilesUsingChecksums();
64
65
  if (changedFiles.length === 0) {
65
66
  console.log(chalk.black.bgGreen(centerText("All files are synced!")));
67
+
68
+ // 변경사항이 없어도 SSR 템플릿은 생성 (초기 설정 시, 이미 존재하면 스킵)
69
+ try {
70
+ await generateTemplate("queries", {}, { overwrite: false });
71
+ await generateTemplate("entry_server", {}, { overwrite: false });
72
+ } catch (e) {
73
+ // 파일이 이미 존재하면 무시
74
+ if (!(e instanceof AlreadyProcessedException)) {
75
+ console.error("Failed to generate SSR templates:", e);
76
+ }
77
+ }
78
+
66
79
  return;
67
80
  }
68
81
 
@@ -91,6 +104,18 @@ export class Syncer {
91
104
  return;
92
105
  }
93
106
 
107
+ // SSR 설정 파일 변경 감지
108
+ if (diffFilePath.includes("/src/ssr/")) {
109
+ console.log(chalk.bold.yellow("SSR config changed - reloading..."));
110
+ // SSR 파일도 invalidate 후 reload
111
+ if (!isTest()) {
112
+ await hot.invalidateFile(diffFilePath, event);
113
+ }
114
+ await this.autoloadSSRRoutes();
115
+ this.eventEmitter.emit("onHMRCompleted");
116
+ return;
117
+ }
118
+
94
119
  // 일단 변경된 파일과 dependent 파일들을 invalidate 합니다.
95
120
  // 한 번 이상 import된 친구들에 대해서만 실제 작업이 일어납니다.
96
121
  // 그러니 안심하고 invalidate 해도 됩니다.
@@ -229,6 +254,36 @@ export class Syncer {
229
254
  await Sonamu.workflows.synchronize(this.workflows);
230
255
  }
231
256
 
257
+ async autoloadSSRRoutes(): Promise<void> {
258
+ const ssrConfigPath = path.join(Sonamu.apiRootPath, "src/ssr");
259
+
260
+ // 기존 routes 초기화
261
+ const { clearSSRRoutes } = await import("../ssr");
262
+ clearSSRRoutes();
263
+
264
+ // ssr 폴더 없으면 스킵
265
+ if (!(await exists(ssrConfigPath))) {
266
+ return;
267
+ }
268
+
269
+ // ssr 폴더 안의 모든 .ts 파일 로드
270
+ const { globAsync } = await import("../utils/async-utils");
271
+ const { importMembers } = await import("../utils/esm-utils");
272
+ const { runtimePath } = await import("../utils/path-utils");
273
+
274
+ // runtimePath를 사용하여 개발/프로덕션 환경에 맞는 확장자 처리
275
+ const files = await globAsync(path.join(ssrConfigPath, runtimePath("**/*.ts")));
276
+
277
+ for (const file of files) {
278
+ try {
279
+ // importMembers를 사용하면 파일의 side effect(registerSSR 호출)가 실행됨
280
+ await importMembers(file);
281
+ } catch (e) {
282
+ console.error(`Failed to load SSR route: ${file}`, e);
283
+ }
284
+ }
285
+ }
286
+
232
287
  /**
233
288
  * 실제 싱크를 수행하는 본체입니다.
234
289
  * 변경된 파일들을 타입별로 분류하고 각 타입에 맞는 액션을 실행합니다.
@@ -371,6 +426,10 @@ export class Syncer {
371
426
 
372
427
  await this.actionGenerateServices(params);
373
428
  await this.actionGenerateHttps();
429
+
430
+ // queries.generated.ts 및 entry-server.generated.tsx 재생성
431
+ await generateTemplate("queries", {}, { overwrite: true });
432
+ await generateTemplate("entry_server", {}, { overwrite: true });
374
433
  }
375
434
 
376
435
  // web/.sonamu.env 에 현재 설정값 저장
@@ -45,12 +45,22 @@ export type DefineWorkflowOptions<
45
45
  // 이것들은 syncer에서 한번에 load한 다음, WorkflowManager에서 synchronize를 통해 등록됨.
46
46
  export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undefined = undefined>(
47
47
  options: DefineWorkflowOptions<Input, Output, TSchema>,
48
- ) {
49
- return (fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>) => {
48
+ ): (fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>) => WorkflowMetadata;
49
+ export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undefined = undefined>(
50
+ options: DefineWorkflowOptions<Input, Output, TSchema>,
51
+ fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>,
52
+ ): WorkflowMetadata;
53
+ export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undefined = undefined>(
54
+ options: DefineWorkflowOptions<Input, Output, TSchema>,
55
+ fn?: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>,
56
+ ):
57
+ | WorkflowMetadata
58
+ | ((fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>) => WorkflowMetadata) {
59
+ const decorated = (fn: WorkflowFunction<SchemaOutput<TSchema, Input>, Output>) => {
50
60
  const id = randomUUID();
51
61
  const workflowName = options.name ?? inflection.underscore(fn.name);
52
62
 
53
- const decorated: WorkflowMetadata = {
63
+ const metadata: WorkflowMetadata = {
54
64
  type: "workflow" as const,
55
65
  id,
56
66
  name: workflowName,
@@ -66,6 +76,12 @@ export function workflow<Input, Output, TSchema extends StandardSchemaV1 | undef
66
76
  fn: fn as WorkflowFunction<unknown, unknown>,
67
77
  };
68
78
 
69
- return decorated;
79
+ return metadata;
70
80
  };
81
+
82
+ if (!fn) {
83
+ return decorated;
84
+ }
85
+
86
+ return decorated(fn);
71
87
  }
@@ -70,11 +70,20 @@ export class StepWrapper {
70
70
  }
71
71
 
72
72
  return {
73
- run: ((stepApi: StepApi) => {
74
- return (...args: TArgs) => {
75
- return stepApi.run(config, () => fn(...args));
76
- };
77
- })(this.#stepApi),
73
+ run: ((stepApi: StepApi, ...args: TArgs) => {
74
+ return stepApi.run(config, () => fn(...args));
75
+ }).bind(null, this.#stepApi),
76
+ };
77
+ }
78
+
79
+ define<TArgs extends unknown[], TResult>(
80
+ config: { name: string },
81
+ fn: StepFunction<TArgs, TResult>,
82
+ ) {
83
+ return {
84
+ run: ((stepApi: StepApi, ...args: TArgs) => {
85
+ return stepApi.run(config, () => fn(...args));
86
+ }).bind(null, this.#stepApi),
78
87
  };
79
88
  }
80
89
 
@@ -1,3 +1,4 @@
1
+ import { getLogger, type Logger } from "@logtape/logtape";
1
2
  import { BackendPostgres, OpenWorkflow, type Worker } from "@sonamu-kit/tasks";
2
3
  import type {
3
4
  RunnableWorkflow,
@@ -14,6 +15,7 @@ import { schedule as cronSchedule, type ScheduledTask } from "node-cron";
14
15
  import type { ZodObject } from "zod";
15
16
  import type { Context } from "../api/context";
16
17
  import { Sonamu } from "../api/sonamu";
18
+ import { convertDomainToCategory } from "../logger/category";
17
19
  import { Naite } from "../naite/naite";
18
20
  import { createMockSSEFactory } from "../stream/sse";
19
21
  import type { Executable } from "../types/types";
@@ -34,6 +36,7 @@ export interface WorkflowOptions {
34
36
  // Workflow 함수의 타입, @sonamu-kit/tasks와 다른 점은 step을 한번 감싼 형태.
35
37
  export type WorkflowFunction<Input, Output> = (
36
38
  params: Readonly<{
39
+ logger: Logger;
37
40
  input: Input;
38
41
  step: StepWrapper;
39
42
  version: string | null;
@@ -266,7 +269,12 @@ export class WorkflowManager {
266
269
 
267
270
  const step = new StepWrapper(params.step);
268
271
  return Sonamu.asyncLocalStorage.run({ context }, () =>
269
- options.function({ input: params.input, step, version: params.version }),
272
+ options.function({
273
+ input: params.input,
274
+ step,
275
+ version: params.version,
276
+ logger: getLogger(convertDomainToCategory(options.name, "workflow")),
277
+ }),
270
278
  );
271
279
  };
272
280
 
@@ -0,0 +1,81 @@
1
+ import type { TemplateOptions } from "../../types/types";
2
+ import { Template } from "../template";
3
+
4
+ export class Template__entry_server extends Template {
5
+ constructor() {
6
+ super("entry_server");
7
+ }
8
+
9
+ getTargetAndPath() {
10
+ return {
11
+ target: "web/src",
12
+ path: `entry-server.generated.tsx`,
13
+ };
14
+ }
15
+
16
+ render({}: TemplateOptions["entry_server"]) {
17
+ const body = `
18
+ import { QueryClient, dehydrate } from '@tanstack/react-query';
19
+ import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
20
+ import { renderToString } from 'react-dom/server';
21
+ import { routeTree } from './routeTree.gen';
22
+ import { Suspense } from "react";
23
+
24
+ export type PreloadedData = {
25
+ queryKey: any[];
26
+ data: any;
27
+ };
28
+
29
+ export async function render(url: string, preloadedData: PreloadedData[] = []) {
30
+ // QueryClient 생성
31
+ const queryClient = new QueryClient({
32
+ defaultOptions: {
33
+ queries: {
34
+ staleTime: 5000,
35
+ retry: false,
36
+ },
37
+ },
38
+ });
39
+
40
+ // Preloaded 데이터를 queryClient에 직접 주입
41
+ for (const { queryKey, data } of preloadedData) {
42
+ queryClient.setQueryData(queryKey, data);
43
+ }
44
+
45
+ // Dehydrate
46
+ const dehydratedState = dehydrate(queryClient);
47
+
48
+ // SSR용 메모리 히스토리 생성
49
+ const memoryHistory = createMemoryHistory({
50
+ initialEntries: [url],
51
+ });
52
+
53
+ // Router 생성 (SSR 모드)
54
+ const router = createRouter({
55
+ routeTree,
56
+ context: { queryClient },
57
+ history: memoryHistory,
58
+ defaultPreload: 'intent',
59
+ });
60
+
61
+ // 라우터 초기화: SSR에서 반드시 await router.load() 호출 필요
62
+ await router.load();
63
+
64
+ // RouterProvider만 렌더링 (Suspense로 래핑 - hydration mismatch 방지)
65
+ const appHtml = renderToString(<Suspense fallback={null}><RouterProvider router={router} /></Suspense>);
66
+
67
+ return {
68
+ html: appHtml,
69
+ dehydratedState,
70
+ };
71
+ }
72
+ `.trim();
73
+
74
+ return {
75
+ ...this.getTargetAndPath(),
76
+ body,
77
+ importKeys: [],
78
+ customHeaders: [],
79
+ };
80
+ }
81
+ }
@@ -61,7 +61,9 @@ class ${entityId}ModelClass extends BaseModelClass<
61
61
  typeof ${names.camel}SubsetQueries,
62
62
  typeof ${names.camel}LoaderQueries
63
63
  > {
64
- modelName = "${entityId}";
64
+ constructor() {
65
+ super("${entityId}", ${names.camel}SubsetQueries, ${names.camel}LoaderQueries);
66
+ }
65
67
 
66
68
  @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "${entityId}" })
67
69
  async findById<T extends ${entityId}SubsetKey>(
@@ -188,7 +190,7 @@ class ${entityId}ModelClass extends BaseModelClass<
188
190
  }
189
191
  }
190
192
 
191
- export const ${entityId}Model = new ${entityId}ModelClass(${names.camel}SubsetQueries, ${names.camel}LoaderQueries);
193
+ export const ${entityId}Model = new ${entityId}ModelClass();
192
194
  `.trim(),
193
195
  importKeys: [],
194
196
  };
@@ -0,0 +1,111 @@
1
+ import inflection from "inflection";
2
+ import { diff, unique } from "radashi";
3
+ import { apiParamToTsCode, apiParamTypeToTsType } from "../../api/code-converters";
4
+ import type { ExtendedApi } from "../../api/decorators";
5
+ import { Sonamu } from "../../api/sonamu";
6
+ import type { TemplateOptions } from "../../types/types";
7
+ import { ApiParamType } from "../../types/types";
8
+ import { Template } from "../template";
9
+
10
+ export class Template__queries extends Template {
11
+ constructor() {
12
+ super("queries");
13
+ }
14
+
15
+ getTargetAndPath() {
16
+ const { dir } = Sonamu.config.api;
17
+ return {
18
+ target: `${dir}/src/application`,
19
+ path: `queries.generated.ts`,
20
+ };
21
+ }
22
+
23
+ render({}: TemplateOptions["queries"]) {
24
+ const { apis } = Sonamu.syncer;
25
+
26
+ // tanstack-query를 포함한 API만 필터링
27
+ const queryApis = apis.filter((api) => api.options.clients?.includes("tanstack-query"));
28
+
29
+ // 모델별로 그룹화
30
+ const apisByModel = new Map<string, ExtendedApi[]>();
31
+ for (const api of queryApis) {
32
+ const modelName = api.modelName.replace(/Model$/, "").replace(/Frame$/, "");
33
+ if (!apisByModel.has(modelName)) {
34
+ apisByModel.set(modelName, []);
35
+ }
36
+ apisByModel.get(modelName)?.push(api);
37
+ }
38
+
39
+ const namespaces: string[] = [];
40
+ const importKeys: string[] = [];
41
+ let typeParamNames: string[] = [];
42
+
43
+ for (const [modelName, modelApis] of apisByModel) {
44
+ const functions: string[] = [];
45
+
46
+ for (const api of modelApis) {
47
+ // Context 제외한 파라미터
48
+ const paramsWithoutContext = api.parameters.filter(
49
+ (param) =>
50
+ !ApiParamType.isContext(param.type) &&
51
+ !ApiParamType.isRefKnex(param.type) &&
52
+ !(param.optional === true && param.name.startsWith("_")),
53
+ );
54
+
55
+ // 타입 파라미터 이름 수집
56
+ typeParamNames = typeParamNames.concat(api.typeParameters.map((tp) => tp.id));
57
+
58
+ // 타입 파라미터 정의
59
+ const typeParametersAsTsType = api.typeParameters
60
+ .map((typeParam) => {
61
+ return apiParamTypeToTsType(typeParam, importKeys);
62
+ })
63
+ .join(", ");
64
+ const typeParamsDef = typeParametersAsTsType ? `<${typeParametersAsTsType}>` : "";
65
+
66
+ // 파라미터 정의
67
+ const paramsDef = apiParamToTsCode(paramsWithoutContext, importKeys);
68
+ const paramNames = paramsWithoutContext.map((p) => p.name).join(", ");
69
+
70
+ // serviceMethodName 계산 (services.template.ts와 동일한 로직)
71
+ const serviceMethodName = api.options.resourceName
72
+ ? `get${inflection.camelize(api.options.resourceName)}`
73
+ : api.methodName;
74
+
75
+ // SSRQuery 함수 생성 (함수명도 serviceMethodName 사용)
76
+ functions.push(
77
+ `
78
+ export const ${serviceMethodName} = ${typeParamsDef}(${paramsDef}): SSRQuery =>
79
+ createSSRQuery('${api.modelName}', '${api.methodName}', [${paramNames}], ['${modelName}', '${serviceMethodName}']);
80
+ `.trim(),
81
+ );
82
+ }
83
+
84
+ namespaces.push(
85
+ `
86
+ export namespace ${modelName}Service {
87
+ ${functions.join("\n\n")}
88
+ }
89
+ `.trim(),
90
+ );
91
+ }
92
+
93
+ return {
94
+ ...this.getTargetAndPath(),
95
+ body: namespaces.join("\n\n"),
96
+ importKeys: diff(unique(importKeys), typeParamNames),
97
+ customHeaders: [
98
+ "/** biome-ignore-all lint: generated는 무시 */",
99
+ "/** biome-ignore-all assist: generated는 무시 */",
100
+ "",
101
+ `import type { SSRQuery } from 'sonamu/ssr';`,
102
+ "",
103
+ `// SSRQuery 헬퍼 함수`,
104
+ `function createSSRQuery(modelName: string, methodName: string, params: any[], serviceKey: [string, string]): SSRQuery {`,
105
+ ` return { modelName, methodName, params, serviceKey, __brand: 'SSRQuery' } as SSRQuery;`,
106
+ `}`,
107
+ "",
108
+ ],
109
+ };
110
+ }
111
+ }
@@ -22,33 +22,47 @@ export class Template__view_enums_select extends Template {
22
22
  return {
23
23
  ...this.getTargetAndPath(names, enumId),
24
24
  body: `
25
- import React from 'react';
26
- import {
27
- Dropdown,
28
- DropdownProps,
29
- } from 'semantic-ui-react';
25
+
26
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@sonamu-kit/react-components/components';
30
27
 
31
28
  import { ${enumId}, ${enumId}Label } from '@/services/sonamu.generated';
32
29
 
33
30
  export type ${enumId}SelectProps = {
31
+ value?: string;
32
+ onValueChange?: (value: string | null | undefined) => void;
34
33
  placeholder?: string;
35
34
  textPrefix?: string;
36
- } & DropdownProps;
37
- export function ${enumId}Select({placeholder, textPrefix, ...props}: ${enumId}SelectProps) {
38
- const typeOptions = ${enumId}.options.map((key) => ({
39
- key,
40
- value: key,
41
- text: (textPrefix ?? '${label}: ') + ${enumId}Label[key],
42
- }));
35
+ clearable?: boolean;
36
+ disabled?: boolean;
37
+ className?: string;
38
+ };
39
+
40
+ export function ${enumId}Select({
41
+ value,
42
+ onValueChange,
43
+ placeholder,
44
+ textPrefix,
45
+ clearable,
46
+ disabled,
47
+ className,
48
+ }: ${enumId}SelectProps) {
49
+ // Filter out empty string from options (Radix UI doesn't allow empty string as SelectItem value)
50
+ const validOptions = ${enumId}.options.filter((key) => (key as string) !== "");
43
51
 
44
52
  return (
45
- <Dropdown
46
- placeholder={placeholder ?? "${label}"}
47
- selection
48
- options={typeOptions}
49
- selectOnBlur={false}
50
- {...props}
51
- />
53
+ <Select value={value ?? ""} onValueChange={onValueChange} disabled={disabled}>
54
+ <SelectTrigger className={className}>
55
+ <SelectValue placeholder={placeholder ?? "${label}"} />
56
+ </SelectTrigger>
57
+ <SelectContent>
58
+ {clearable && <SelectItem value="">전체</SelectItem>}
59
+ {validOptions.map((key) => (
60
+ <SelectItem key={key} value={key}>
61
+ {(textPrefix ?? "") + ${enumId}Label[key]}
62
+ </SelectItem>
63
+ ))}
64
+ </SelectContent>
65
+ </Select>
52
66
  );
53
67
  }
54
68
  `.trim(),