sonamu 0.7.15 → 0.7.17

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 (96) hide show
  1. package/dist/ai/providers/rtzr/error.d.ts +1 -1
  2. package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
  3. package/dist/api/config.d.ts +1 -0
  4. package/dist/api/config.d.ts.map +1 -1
  5. package/dist/api/config.js +1 -1
  6. package/dist/api/decorators.d.ts +1 -1
  7. package/dist/api/decorators.d.ts.map +1 -1
  8. package/dist/api/decorators.js +1 -1
  9. package/dist/api/sonamu.d.ts +3 -1
  10. package/dist/api/sonamu.d.ts.map +1 -1
  11. package/dist/api/sonamu.js +51 -40
  12. package/dist/database/base-model.d.ts +16 -6
  13. package/dist/database/base-model.d.ts.map +1 -1
  14. package/dist/database/base-model.js +44 -3
  15. package/dist/database/base-model.types.d.ts +29 -48
  16. package/dist/database/base-model.types.d.ts.map +1 -1
  17. package/dist/database/base-model.types.js +12 -2
  18. package/dist/database/puri.d.ts +2 -1
  19. package/dist/database/puri.d.ts.map +1 -1
  20. package/dist/database/puri.js +2 -1
  21. package/dist/database/puri.types.d.ts +3 -3
  22. package/dist/database/puri.types.d.ts.map +1 -1
  23. package/dist/database/puri.types.js +1 -1
  24. package/dist/entity/entity-manager.d.ts +8 -4
  25. package/dist/entity/entity-manager.d.ts.map +1 -1
  26. package/dist/entity/entity.d.ts +10 -1
  27. package/dist/entity/entity.d.ts.map +1 -1
  28. package/dist/entity/entity.js +84 -39
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -1
  32. package/dist/syncer/checksum.d.ts +8 -3
  33. package/dist/syncer/checksum.d.ts.map +1 -1
  34. package/dist/syncer/checksum.js +17 -9
  35. package/dist/syncer/code-generator.js +7 -2
  36. package/dist/syncer/syncer.d.ts +6 -6
  37. package/dist/syncer/syncer.d.ts.map +1 -1
  38. package/dist/syncer/syncer.js +27 -13
  39. package/dist/tasks/workflow-manager.d.ts +3 -3
  40. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  41. package/dist/tasks/workflow-manager.js +15 -11
  42. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  43. package/dist/template/implementations/generated.template.js +8 -6
  44. package/dist/template/implementations/model.template.js +5 -5
  45. package/dist/template/implementations/services.template.d.ts +17 -0
  46. package/dist/template/implementations/services.template.d.ts.map +1 -0
  47. package/dist/template/implementations/services.template.js +159 -0
  48. package/dist/template/implementations/view_form.template.js +2 -2
  49. package/dist/template/implementations/view_id_async_select.template.js +2 -2
  50. package/dist/template/implementations/view_list.template.js +5 -5
  51. package/dist/types/types.d.ts +43 -25
  52. package/dist/types/types.d.ts.map +1 -1
  53. package/dist/types/types.js +29 -17
  54. package/dist/ui/ai-api.d.ts +2 -0
  55. package/dist/ui/ai-api.d.ts.map +1 -1
  56. package/dist/ui/ai-api.js +43 -49
  57. package/dist/ui/ai-client.d.ts +10 -0
  58. package/dist/ui/ai-client.d.ts.map +1 -1
  59. package/dist/ui/ai-client.js +457 -437
  60. package/dist/ui/api.d.ts.map +1 -1
  61. package/dist/ui/api.js +14 -3
  62. package/dist/ui-web/assets/{index-J9MCfjCd.js → index-DzqUrTB-.js} +56 -59
  63. package/dist/ui-web/index.html +1 -1
  64. package/package.json +12 -8
  65. package/src/api/config.ts +3 -0
  66. package/src/api/decorators.ts +6 -1
  67. package/src/api/sonamu.ts +71 -52
  68. package/src/database/base-model.ts +66 -11
  69. package/src/database/base-model.types.ts +79 -76
  70. package/src/database/puri.ts +5 -1
  71. package/src/database/puri.types.ts +3 -6
  72. package/src/entity/entity.ts +83 -34
  73. package/src/index.ts +1 -0
  74. package/src/shared/app.shared.ts.txt +1 -1
  75. package/src/shared/web.shared.ts.txt +0 -43
  76. package/src/syncer/checksum.ts +31 -9
  77. package/src/syncer/code-generator.ts +8 -1
  78. package/src/syncer/syncer.ts +38 -26
  79. package/src/tasks/workflow-manager.ts +16 -12
  80. package/src/template/implementations/generated.template.ts +17 -3
  81. package/src/template/implementations/model.template.ts +4 -4
  82. package/src/template/implementations/services.template.ts +226 -0
  83. package/src/template/implementations/view_form.template.ts +1 -1
  84. package/src/template/implementations/view_id_async_select.template.ts +1 -1
  85. package/src/template/implementations/view_list.template.ts +4 -4
  86. package/src/types/types.ts +33 -16
  87. package/src/ui/ai-api.ts +61 -60
  88. package/src/ui/ai-client.ts +535 -499
  89. package/src/ui/api.ts +14 -2
  90. package/src/ui/entity.instructions.md +536 -0
  91. package/dist/template/implementations/service.template.d.ts +0 -29
  92. package/dist/template/implementations/service.template.d.ts.map +0 -1
  93. package/dist/template/implementations/service.template.js +0 -202
  94. package/dist/ui-web/assets/provider-utils_false-BKJD46kk.js +0 -1
  95. package/dist/ui-web/assets/provider-utils_false-Bu5lmX18.js +0 -1
  96. package/src/template/implementations/service.template.ts +0 -328
@@ -0,0 +1,226 @@
1
+ import inflection from "inflection";
2
+ import { diff, unique } from "radashi";
3
+ import {
4
+ apiParamToTsCode,
5
+ apiParamTypeToTsType,
6
+ unwrapPromiseOnce,
7
+ } from "../../api/code-converters";
8
+ import type { ExtendedApi } from "../../api/decorators";
9
+ import { Sonamu } from "../../api/sonamu";
10
+ import type { TemplateOptions } from "../../types/types";
11
+ import { ApiParamType } from "../../types/types";
12
+ import { assertDefined } from "../../utils/utils";
13
+ import { Template } from "../template";
14
+
15
+ export class Template__services extends Template {
16
+ constructor() {
17
+ super("services");
18
+ }
19
+
20
+ getTargetAndPath() {
21
+ return {
22
+ target: ":target/src/services",
23
+ path: `services.generated.ts`,
24
+ };
25
+ }
26
+
27
+ render({}: TemplateOptions["services"]) {
28
+ const { apis } = Sonamu.syncer;
29
+
30
+ // 모델별로 그룹화
31
+ const apisByModel = new Map<string, ExtendedApi[]>();
32
+ for (const api of apis) {
33
+ const modelName = api.modelName.replace(/Model$/, "").replace(/Frame$/, "");
34
+ if (!apisByModel.has(modelName)) {
35
+ apisByModel.set(modelName, []);
36
+ }
37
+ apisByModel.get(modelName)?.push(api);
38
+ }
39
+
40
+ const importKeys: string[] = [];
41
+ const namespaces: string[] = [];
42
+ let typeParamNames: string[] = [];
43
+
44
+ for (const [modelName, modelApis] of apisByModel) {
45
+ const functions: string[] = [];
46
+
47
+ for (const api of modelApis) {
48
+ // Context 제외한 파라미터
49
+ const paramsWithoutContext = api.parameters.filter(
50
+ (param) =>
51
+ !ApiParamType.isContext(param.type) &&
52
+ !ApiParamType.isRefKnex(param.type) &&
53
+ !(param.optional === true && param.name.startsWith("_")),
54
+ );
55
+
56
+ // 타입 파라미터 정의
57
+ const typeParametersAsTsType = api.typeParameters
58
+ .map((typeParam) => apiParamTypeToTsType(typeParam, importKeys))
59
+ .join(", ");
60
+ const typeParamsDef = typeParametersAsTsType ? `<${typeParametersAsTsType}>` : "";
61
+ typeParamNames = typeParamNames.concat(api.typeParameters.map((tp) => tp.id));
62
+
63
+ // 파라미터 정의
64
+ const paramsDef = apiParamToTsCode(paramsWithoutContext, importKeys);
65
+ const paramNames = paramsWithoutContext.map((p) => p.name).join(", ");
66
+
67
+ // 리턴 타입 정의
68
+ const returnTypeDef = apiParamTypeToTsType(
69
+ assertDefined(unwrapPromiseOnce(api.returnType)),
70
+ importKeys,
71
+ );
72
+
73
+ // 기본 URL
74
+ const apiBaseUrl = `${Sonamu.config.api.route.prefix}${api.path}`;
75
+
76
+ const clients = api.options.clients || [];
77
+
78
+ // 1. axios 함수 생성
79
+ // resourceName이 있으면 get + resourceName 형태로 함수명 생성
80
+ const methodName = api.options.resourceName
81
+ ? `get${inflection.camelize(api.options.resourceName)}`
82
+ : api.methodName;
83
+
84
+ // axios-multipart 처리 (파일 업로드)
85
+ if (clients.includes("axios-multipart")) {
86
+ const isMultiple = api.uploadOptions?.mode === "multiple";
87
+ const fileParamName = isMultiple ? "files" : "file";
88
+ const fileParamType = isMultiple ? "File[]" : "File";
89
+
90
+ const formDataAppend = isMultiple
91
+ ? `${fileParamName}.forEach(f => { formData.append("${fileParamName}", f); });`
92
+ : `formData.append("${fileParamName}", ${fileParamName});`;
93
+
94
+ const otherParamsAppend = paramsWithoutContext
95
+ .map((param) => `formData.append('${param.name}', String(${param.name}));`)
96
+ .join("\n ");
97
+
98
+ const paramsDefComma = paramsDef !== "" ? ", " : "";
99
+ functions.push(
100
+ `
101
+ export async function ${methodName}${typeParamsDef}(
102
+ ${paramsDef}${paramsDefComma}
103
+ ${fileParamName}: ${fileParamType},
104
+ onUploadProgress?: (pe: AxiosProgressEvent) => void
105
+ ): Promise<${returnTypeDef}> {
106
+ const formData = new FormData();
107
+ ${formDataAppend}
108
+ ${otherParamsAppend}
109
+ return fetch({
110
+ method: 'POST',
111
+ url: \`${apiBaseUrl}\`,
112
+ headers: {
113
+ "Content-Type": "multipart/form-data",
114
+ },
115
+ onUploadProgress,
116
+ data: formData,
117
+ ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
118
+ });
119
+ }
120
+ `.trim(),
121
+ );
122
+ } else if (api.options.httpMethod === "GET") {
123
+ const hasParams = paramsWithoutContext.length > 0;
124
+ functions.push(
125
+ `
126
+ export async function ${methodName}${typeParamsDef}(${paramsDef}): Promise<${returnTypeDef}> {
127
+ return fetch({
128
+ method: "GET",
129
+ url: \`${apiBaseUrl}${hasParams ? `?\${qs.stringify({ ${paramNames} })}` : ""}\`,
130
+ ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
131
+ });
132
+ }
133
+ `.trim(),
134
+ );
135
+ } else {
136
+ const hasParams = paramsWithoutContext.length > 0;
137
+ functions.push(
138
+ `
139
+ export async function ${methodName}${typeParamsDef}(${paramsDef}): Promise<${returnTypeDef}> {
140
+ return fetch({
141
+ method: "${api.options.httpMethod}",
142
+ url: \`${apiBaseUrl}\`,
143
+ ${hasParams ? `data: { ${paramNames} },` : ""}
144
+ ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
145
+ });
146
+ }
147
+ `.trim(),
148
+ );
149
+ }
150
+
151
+ // 2. queryOptions + useQuery (tanstack-query)
152
+ if (clients.includes("tanstack-query")) {
153
+ const hookName = api.options.resourceName
154
+ ? inflection.camelize(api.options.resourceName, true)
155
+ : inflection.camelize(api.methodName, true);
156
+
157
+ // queryOptions
158
+ functions.push(
159
+ `
160
+ export const ${methodName}QueryOptions = ${typeParamsDef}(${paramsDef}) => queryOptions({
161
+ queryKey: ['${modelName}', '${methodName}'${paramNames ? `, ${paramNames}` : ""}],
162
+ queryFn: () => ${methodName}(${paramNames})
163
+ });
164
+ `.trim(),
165
+ );
166
+
167
+ // useQuery hook
168
+ functions.push(
169
+ `
170
+ export const use${inflection.camelize(hookName)} = ${typeParamsDef}(${paramsDef}${
171
+ paramsDef ? ", " : ""
172
+ }options?: { enabled?: boolean }) =>
173
+ useQuery({
174
+ ...${methodName}QueryOptions(${paramNames}),
175
+ ...options
176
+ });
177
+ `.trim(),
178
+ );
179
+ }
180
+
181
+ // 3. useMutation (tanstack-mutation)
182
+ if (clients.includes("tanstack-mutation")) {
183
+ const hookName = inflection.camelize(api.methodName);
184
+ const mutationParamType =
185
+ paramsWithoutContext.length > 0
186
+ ? `{ ${paramsWithoutContext
187
+ .map((p) => `${p.name}: ${apiParamTypeToTsType(p.type, [])}`)
188
+ .join(", ")} }`
189
+ : "void";
190
+ const mutationParamNames =
191
+ paramsWithoutContext.length > 0
192
+ ? paramsWithoutContext.map((p) => `params.${p.name}`).join(", ")
193
+ : "";
194
+
195
+ functions.push(
196
+ `
197
+ export const use${hookName}Mutation = ${typeParamsDef}() => useMutation({
198
+ mutationFn: (params: ${mutationParamType}) => ${methodName}(${mutationParamNames})
199
+ });
200
+ `.trim(),
201
+ );
202
+ }
203
+ }
204
+
205
+ namespaces.push(
206
+ `
207
+ export namespace ${modelName}Service {
208
+ ${functions.join("\n\n")}
209
+ }
210
+ `.trim(),
211
+ );
212
+ }
213
+
214
+ return {
215
+ ...this.getTargetAndPath(),
216
+ body: namespaces.join("\n\n"),
217
+ importKeys: diff(unique(importKeys), [...typeParamNames, "ListResult"]),
218
+ customHeaders: [
219
+ `import { queryOptions, useQuery, useMutation } from '@tanstack/react-query';`,
220
+ `import type { AxiosProgressEvent } from 'axios';`,
221
+ `import qs from 'qs';`,
222
+ `import { type ListResult, fetch } from './sonamu.shared';`,
223
+ ],
224
+ };
225
+ }
226
+ }
@@ -256,7 +256,7 @@ import { defaultCatch } from '@/services/sonamu.shared';
256
256
  // import { useCommonModal } from "@/admin-common/CommonModal";
257
257
 
258
258
  import { ${names.capital}SaveParams } from '@/services/${names.fs}/${names.fs}.types';
259
- import { ${names.capital}Service } from '@/services/${names.fs}/${names.fs}.service';
259
+ import { ${names.capital}Service } from '@/services/services.generated';
260
260
  import { ${names.capital}SubsetA } from '@/services/sonamu.generated';
261
261
  ${unique(
262
262
  columns
@@ -40,7 +40,7 @@ import { DropdownProps, DropdownItemProps, DropdownOnSearchChangeData, Dropdown
40
40
  import { ${names.capital}SubsetKey, ${
41
41
  names.capital
42
42
  }SubsetMapping } from "@/services/sonamu.generated";
43
- import { ${names.capital}Service } from "@/services/${names.fs}/${names.fs}.service";
43
+ import { ${names.capital}Service } from "@/services/services.generated";
44
44
  import { ${names.capital}ListParams } from "@/services/${names.fs}/${names.fs}.types";
45
45
 
46
46
  export function ${names.capital}IdAsyncSelect<T extends ${names.capital}SubsetKey>(
@@ -326,7 +326,7 @@ import { DateTime } from "luxon";
326
326
  import { DelButton, EditButton, AppBreadcrumbs, AddButton, useSelection, useListParams, SonamuCol, numF, formatDate, formatDateTime } from '@sonamu-kit/react-sui';
327
327
 
328
328
  import { ${names.capital}SubsetA } from "@/services/sonamu.generated";
329
- import { ${names.capital}Service } from '@/services/${names.fs}/${names.fs}.service';
329
+ import { ${names.capital}Service } from '@/services/services.generated';
330
330
  import { ${names.capital}ListParams } from '@/services/${names.fs}/${names.fs}.types';
331
331
  ${columnImports}
332
332
  ${filterColumns
@@ -346,7 +346,7 @@ export default function ${names.capital}List({}: ${names.capital}ListProps) {
346
346
  });
347
347
 
348
348
  // 리스트 쿼리
349
- const { data, mutate, error, isLoading } = ${names.capital}Service.use${
349
+ const { data, refetch, isLoading } = ${names.capital}Service.use${
350
350
  names.capitalPlural
351
351
  }('A', listParams);
352
352
  const { rows, total } = data ?? {};
@@ -359,7 +359,7 @@ export default function ${names.capital}List({}: ${names.capital}ListProps) {
359
359
  }
360
360
 
361
361
  ${names.capital}Service.del(ids).then(() => {
362
- mutate();
362
+ refetch();
363
363
  });
364
364
  };
365
365
 
@@ -371,7 +371,7 @@ export default function ${names.capital}List({}: ${names.capital}ListProps) {
371
371
  }
372
372
 
373
373
  ${names.capital}Service.del(selectedKeys).then(() => {
374
- mutate();
374
+ refetch();
375
375
  });
376
376
  };
377
377
 
@@ -104,6 +104,7 @@ export type UuidArrayProp = CommonProp & {
104
104
  export type VirtualProp = CommonProp & {
105
105
  type: "virtual";
106
106
  id: string;
107
+ virtualType?: "query" | "code"; // default: "code"
107
108
  }; // PG: none / TS: any(id) / JSON: any
108
109
  export type VectorProp = CommonProp & {
109
110
  type: "vector";
@@ -264,6 +265,17 @@ export type EntityIndex = {
264
265
  */
265
266
  lists?: number;
266
267
  };
268
+
269
+ // SubsetField 타입: string 또는 internal 옵션이 있는 객체
270
+ export type SubsetField = string | { field: string; internal?: boolean };
271
+
272
+ export function normalizeSubsetField(f: SubsetField): string {
273
+ return typeof f === "string" ? f : f.field;
274
+ }
275
+ export function isInternalSubsetField(f: SubsetField): boolean {
276
+ return typeof f !== "string" && f.internal === true;
277
+ }
278
+
267
279
  export type EntityJson = {
268
280
  id: string;
269
281
  parentId?: string;
@@ -272,7 +284,7 @@ export type EntityJson = {
272
284
  props: EntityProp[];
273
285
  indexes: EntityIndex[];
274
286
  subsets: {
275
- [subset: string]: string[];
287
+ [subset: string]: SubsetField[];
276
288
  };
277
289
  enums: {
278
290
  [enumId: string]: {
@@ -285,6 +297,9 @@ export type EntitySubsetRow = {
285
297
  has: {
286
298
  [key: string]: boolean;
287
299
  };
300
+ isInternal: {
301
+ [key: string]: boolean;
302
+ };
288
303
  children: EntitySubsetRow[];
289
304
  prefixes: string[];
290
305
  relationEntity?: string;
@@ -413,6 +428,14 @@ export function isJsonProp(p: unknown): p is JsonProp {
413
428
  export function isVirtualProp(p: unknown): p is VirtualProp {
414
429
  return (p as VirtualProp)?.type === "virtual";
415
430
  }
431
+ export function isVirtualCodeProp(p: unknown): p is VirtualProp {
432
+ if (!isVirtualProp(p)) return false;
433
+ return p.virtualType !== "query"; // undefined도 "code"로 취급
434
+ }
435
+ export function isVirtualQueryProp(p: unknown): p is VirtualProp {
436
+ if (!isVirtualProp(p)) return false;
437
+ return p.virtualType === "query";
438
+ }
416
439
  export function isVectorSingleProp(p: unknown): p is VectorProp {
417
440
  return (p as VectorProp)?.type === "vector";
418
441
  }
@@ -951,6 +974,7 @@ const VirtualPropSchema = z
951
974
  ...BasePropFields,
952
975
  type: z.literal("virtual"),
953
976
  id: z.string(),
977
+ virtualType: z.enum(["query", "code"]).optional(),
954
978
  })
955
979
  .strict();
956
980
 
@@ -1172,7 +1196,12 @@ export const EntityJsonSchema = z
1172
1196
  parentId: z.string().optional().describe("부모 Entity ID"),
1173
1197
  props: z.array(EntityPropSchema),
1174
1198
  indexes: z.array(EntityIndexSchema),
1175
- subsets: z.record(z.string(), z.array(z.string())),
1199
+ subsets: z.record(
1200
+ z.string(),
1201
+ z.array(
1202
+ z.union([z.string(), z.object({ field: z.string(), internal: z.boolean().optional() })]),
1203
+ ),
1204
+ ),
1176
1205
  enums: z.record(z.string(), z.record(z.string(), z.string())),
1177
1206
  })
1178
1207
  .strict();
@@ -1208,19 +1237,7 @@ export const TemplateOptions = z.object({
1208
1237
  bridge: z.object({
1209
1238
  entityId: z.string(),
1210
1239
  }),
1211
- service: z.object({
1212
- namesRecord: z.object({
1213
- fs: z.string(),
1214
- fsPlural: z.string(),
1215
- camel: z.string(),
1216
- camelPlural: z.string(),
1217
- capital: z.string(),
1218
- capitalPlural: z.string(),
1219
- upper: z.string(),
1220
- constant: z.string(),
1221
- }),
1222
- modelTsPath: z.string(),
1223
- }),
1240
+ services: z.object({}),
1224
1241
  view_list: z.object({
1225
1242
  entityId: z.string(),
1226
1243
  extra: z.unknown(),
@@ -1273,7 +1290,7 @@ export const TemplateKey = z.enum([
1273
1290
  "model",
1274
1291
  "model_test",
1275
1292
  "bridge",
1276
- "service",
1293
+ "services",
1277
1294
  "view_list",
1278
1295
  "view_list_columns",
1279
1296
  "view_search_input",
package/src/ui/ai-api.ts CHANGED
@@ -1,60 +1,61 @@
1
- // import { convertToModelMessages, type UIMessage } from "ai";
2
- // import type { FastifyInstance } from "fastify";
3
- // import { BadRequestException, type FixtureRecord } from "sonamu";
4
- // import { aiClient } from "./ai-client";
5
-
6
- // export async function setAiApi(server: FastifyInstance) {
7
- // await aiClient.init();
8
-
9
- // server.post("/api/ai/fixture/chat", async (request, reply) => {
10
- // const { messages, fixtureRecords } = request.body as {
11
- // messages: UIMessage[];
12
- // fixtureRecords?: FixtureRecord[];
13
- // };
14
-
15
- // if (!fixtureRecords || fixtureRecords.length === 0) {
16
- // throw new BadRequestException("픽스쳐 레코드가 없습니다. 픽스쳐 조회 후 시도하세요.");
17
- // }
18
-
19
- // const result = aiClient.handleFixture(convertToModelMessages(messages), fixtureRecords);
20
- // const response = result.toUIMessageStreamResponse();
21
-
22
- // reply.raw.writeHead(response.status, Object.fromEntries(response.headers.entries()));
23
-
24
- // if (response.body) {
25
- // const reader = response.body.getReader();
26
- // while (true) {
27
- // const { done, value } = await reader.read();
28
- // if (done) break;
29
- // reply.raw.write(value);
30
- // }
31
- // }
32
-
33
- // reply.raw.end();
34
- // return reply;
35
- // });
36
-
37
- // // Entity/Enum 생성용 AI Chat Stream
38
- // server.post("/api/ai/entity/chat", async (request, reply) => {
39
- // const { messages } = request.body as {
40
- // messages: UIMessage[];
41
- // };
42
-
43
- // const result = aiClient.handleEntity(convertToModelMessages(messages));
44
- // const response = result.toUIMessageStreamResponse();
45
-
46
- // reply.raw.writeHead(response.status, Object.fromEntries(response.headers.entries()));
47
-
48
- // if (response.body) {
49
- // const reader = response.body.getReader();
50
- // while (true) {
51
- // const { done, value } = await reader.read();
52
- // if (done) break;
53
- // reply.raw.write(value);
54
- // }
55
- // }
56
-
57
- // reply.raw.end();
58
- // return reply;
59
- // });
60
- // }
1
+ import { convertToModelMessages, type UIMessage } from "ai";
2
+ import type { FastifyInstance } from "fastify";
3
+ import { BadRequestException } from "../exceptions/so-exceptions";
4
+ import type { FixtureRecord } from "../types/types";
5
+ import { aiClient } from "./ai-client";
6
+
7
+ export async function setAiApi(server: FastifyInstance) {
8
+ await aiClient.init();
9
+
10
+ server.post("/api/ai/fixture/chat", async (request, reply) => {
11
+ const { messages, fixtureRecords } = request.body as {
12
+ messages: UIMessage[];
13
+ fixtureRecords?: FixtureRecord[];
14
+ };
15
+
16
+ if (!fixtureRecords || fixtureRecords.length === 0) {
17
+ throw new BadRequestException("픽스쳐 레코드가 없습니다. 픽스쳐 조회 후 시도하세요.");
18
+ }
19
+
20
+ const result = aiClient.handleFixture(await convertToModelMessages(messages), fixtureRecords);
21
+ const response = result.toUIMessageStreamResponse();
22
+
23
+ reply.raw.writeHead(response.status, Object.fromEntries(Object.entries(response.headers)));
24
+
25
+ if (response.body) {
26
+ const reader = response.body.getReader();
27
+ while (true) {
28
+ const { done, value } = await reader.read();
29
+ if (done) break;
30
+ reply.raw.write(value);
31
+ }
32
+ }
33
+
34
+ reply.raw.end();
35
+ return reply;
36
+ });
37
+
38
+ // Entity/Enum 생성용 AI Chat Stream
39
+ server.post("/api/ai/entity/chat", async (request, reply) => {
40
+ const { messages } = request.body as {
41
+ messages: UIMessage[];
42
+ };
43
+
44
+ const result = aiClient.handleEntity(await convertToModelMessages(messages));
45
+ const response = result.toUIMessageStreamResponse();
46
+
47
+ reply.raw.writeHead(response.status, Object.fromEntries(Object.entries(response.headers)));
48
+
49
+ if (response.body) {
50
+ const reader = response.body.getReader();
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (done) break;
54
+ reply.raw.write(value);
55
+ }
56
+ }
57
+
58
+ reply.raw.end();
59
+ return reply;
60
+ });
61
+ }