@uniai-fe/util-functions 0.0.1

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.
@@ -0,0 +1,515 @@
1
+ import { dateFormat } from "./format";
2
+ import { convertObjectToSearchParams } from "./convert";
3
+ import type { CommonPostResponseType } from "@uniai/types";
4
+
5
+ /**
6
+ * API; 인프라에 따른 도메인 추출
7
+ * @desc
8
+ * - 환경변수 값을 반환
9
+ * - PUBLIC_NEXT가 붙지 않았으므로 서버에서만 사용 가능
10
+ * - 별도의 도메인 지정시, URL을 그대로 입력
11
+ */
12
+ const infraDomain = (infra: "ai" | "db" | "uniai" | string) => {
13
+ switch (infra) {
14
+ case "ai":
15
+ return process.env.AI_API_BASE;
16
+ case "db":
17
+ return process.env.DB_API_BASE;
18
+ case "uniai":
19
+ return process.env.UNIAI_API_BASE;
20
+ default:
21
+ return infra;
22
+ }
23
+ };
24
+
25
+ /**
26
+ * API; 쿼리스트링 생성
27
+ * @util
28
+ * @param {unknown} [searchParams] 쿼리스트링으로 변환할 객체
29
+ * @return {string} 쿼리스트링
30
+ */
31
+ export const getQueryString = (searchParams?: unknown): string =>
32
+ typeof searchParams !== "undefined" &&
33
+ convertObjectToSearchParams(searchParams).toString()
34
+ ? `?${convertObjectToSearchParams(searchParams).toString()}`
35
+ : "";
36
+
37
+ /**
38
+ * API; POST/DELETE method option
39
+ */
40
+ const getFetchOptions = ({
41
+ method,
42
+ headers,
43
+ body,
44
+ }: {
45
+ method: string;
46
+ } & Partial<{
47
+ headers: HeadersInit;
48
+ body: BodyInit | null;
49
+ }>): RequestInit => {
50
+ const option: RequestInit = { method };
51
+ // API fetch Headers
52
+ if (typeof headers !== "undefined") Object.assign(option, { headers });
53
+ // API fetch Body
54
+ if (typeof body !== "undefined") Object.assign(option, { body });
55
+ return option;
56
+ };
57
+
58
+ /**
59
+ * API 요청 url 생성; GET 타입
60
+ * @util
61
+ * @param {object} props
62
+ * @param {"ai" | "db" | string} props.infra API 인프라
63
+ * @param {string} props.routeUrl Next.js /app/api 라우트 주소
64
+ * @param {string} props.queryUrl 백엔드 API url
65
+ * @param {URLSearchParams} [props.searchParams] 쿼리 스트링 추출
66
+ * @param {object} [props.log] 디버깅용 서버 로그 정보
67
+ * @param {boolean} [props.logDisabled] 로그 비활성화
68
+ * @return {string} GET API 요청 full url
69
+ */
70
+ export const generateBackendQueryUrl_GET = ({
71
+ infra,
72
+ routeUrl,
73
+ queryUrl,
74
+ searchParams, // 가공이 완료된 파라미터
75
+ log,
76
+ logDisabled,
77
+ }: {
78
+ /**
79
+ * API 인프라
80
+ * - ai: AI API
81
+ * - db: DB API
82
+ * - uniai: UNIAI API
83
+ * - string: 직접 입력한 도메인
84
+ */
85
+ infra: "ai" | "db" | "uniai" | string;
86
+ /**
87
+ * Next.js /app/api 라우트 주소
88
+ */
89
+ routeUrl: string;
90
+ /**
91
+ * 백엔드 API url
92
+ */
93
+ queryUrl: string;
94
+ } & Partial<{
95
+ /**
96
+ * 쿼리 스트링
97
+ * @desc
98
+ * - URLSearchParams 객체로 가공된 파라미터
99
+ */
100
+ searchParams: URLSearchParams | object;
101
+ /**
102
+ * 디버깅용 서버 로그 정보
103
+ */
104
+ log: object;
105
+ /**
106
+ * 로그 비활성화
107
+ * @default false
108
+ */
109
+ logDisabled: boolean;
110
+ }>): string => {
111
+ // API 인프라에 따른 요청 도메인 추출
112
+ const DOMAIN = infraDomain(infra);
113
+
114
+ // url 생성
115
+ const url = `${DOMAIN}${queryUrl}${getQueryString(searchParams)}`;
116
+
117
+ // 서버 로그 출력
118
+ if (!logDisabled)
119
+ nextAPILog("get", routeUrl, url, {
120
+ ...log,
121
+ ...(searchParams ? searchParams : {}),
122
+ });
123
+
124
+ return url;
125
+ };
126
+
127
+ /**
128
+ * API fetch 요청; POST/DELETE 타입
129
+ * @util
130
+ * @param {object} props
131
+ * @param {"ai" | "db" | string} props.infra API 인프라
132
+ * @param {"POST" | "DELETE"} props.method POST, DELETE
133
+ * @param {string} props.routeUrl Next.js /app/api 라우트 주소
134
+ * @param {string} props.queryUrl 백엔드 API url
135
+ * @param {HeadersInit} [props.headers] fetch Headers
136
+ * @param {BodyInit | null} [props.body] post/delete body (이대로 바로 전송됨)
137
+ * @param {object} [props.bodyData] body로 전송하기 위해 가공이 필요한 데이터 객체
138
+ * @param {object} [props.queryStringData] url에 쿼리스트링으로 요청하는 경우에 대한 데이터 객체
139
+ * @param {object} [props.log] 디버깅용 서버 로그 정보
140
+ * @param {boolean} [props.logDisabled] 로그 비활성화
141
+ * @param {boolean} [props.fetchDisabled] fetch 실행 비활성화
142
+ * @return {Promise<MutateAPICommonResponseType>} POST, DELETE 응답
143
+ */
144
+ export const fetchBackendQuery = async <
145
+ FetchRequestType extends object,
146
+ FetchResponseType extends CommonPostResponseType,
147
+ >({
148
+ infra,
149
+ method,
150
+ routeUrl,
151
+ queryUrl,
152
+ headers,
153
+ body,
154
+ bodyOriginData,
155
+ queryStringData,
156
+ log,
157
+ logDisabled,
158
+ fetchDisabled,
159
+ }: {
160
+ /**
161
+ * API 인프라
162
+ * - ai: AI API
163
+ * - db: DB API
164
+ * - uniai: UNIAI API
165
+ * - string: 직접 입력한 도메인
166
+ */
167
+ infra: "ai" | "db" | "uniai" | string;
168
+ /**
169
+ * 요청 방식
170
+ * POST, DELETE
171
+ */
172
+ method: "POST" | "DELETE";
173
+ /**
174
+ * 프론트 API URL
175
+ */
176
+ routeUrl: string;
177
+ /**
178
+ * 백엔드 API 요청 URL
179
+ */
180
+ queryUrl: string;
181
+ } & Partial<{
182
+ /**
183
+ * fetch Headers
184
+ */
185
+ headers: HeadersInit;
186
+ /**
187
+ * fetch Body
188
+ */
189
+ body: BodyInit | null;
190
+ /**
191
+ * fetch Body 를 SearchParams 전환할 데이터
192
+ */
193
+ bodyOriginData: FetchRequestType;
194
+ /**
195
+ * URL 쿼리 스트링
196
+ */
197
+ queryStringData: FetchRequestType;
198
+ /**
199
+ * 디버그용 로그 객체
200
+ */
201
+ log: object;
202
+ /**
203
+ * 로그 비활성화
204
+ */
205
+ logDisabled: boolean;
206
+ /**
207
+ * fetch 비활성화
208
+ */
209
+ fetchDisabled: boolean;
210
+ }>): Promise<FetchResponseType> => {
211
+ // API 인프라에 따른 요청 도메인 추출
212
+ const DOMAIN = infraDomain(infra);
213
+
214
+ // 쿼리 url 생성
215
+ const url = `${DOMAIN}${queryUrl}${getQueryString(queryStringData)}`;
216
+
217
+ // 에러 응답
218
+ const errRes: FetchResponseType = {
219
+ is_ok: false,
220
+ idx: "",
221
+ error: "",
222
+ } as FetchResponseType;
223
+
224
+ const bodyData = convertObjectToSearchParams(bodyOriginData);
225
+
226
+ // bodyOriginData가 undefined가 아닌데도, bodyOriginData의 데이터가 유효하지 않은 경우
227
+ if (typeof bodyOriginData !== "undefined" && bodyData.toString() === "") {
228
+ if (!logDisabled) {
229
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
230
+ state: "ERROR (BODY - ORIGIN DATA)",
231
+ error: "데이터가 유효하지 않습니다.",
232
+ bodyOriginData,
233
+ ...log,
234
+ });
235
+ }
236
+ errRes.error = "데이터가 유효하지 않습니다.";
237
+ return errRes;
238
+ }
239
+
240
+ // 쿼리 옵션 생성
241
+ const option: RequestInit = getFetchOptions({
242
+ method,
243
+ headers,
244
+ body,
245
+ });
246
+ if (!body && typeof bodyOriginData !== "undefined")
247
+ Object.assign(option, { body: bodyData });
248
+
249
+ // 로그 옵션
250
+ if (!logDisabled) {
251
+ // 쿼리 전 로그 출력
252
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
253
+ state: "READY",
254
+ ...option,
255
+ ...log,
256
+ });
257
+ }
258
+
259
+ // 디버깅을 위한 fetch 요청 제한
260
+ if (fetchDisabled) {
261
+ errRes.error = "API 요청제한 활성화됨.";
262
+ return errRes;
263
+ }
264
+
265
+ try {
266
+ const res: FetchResponseType = await (await fetch(url, option)).json();
267
+
268
+ if (!logDisabled) {
269
+ // API 응답 성공 여부
270
+ const isSuccess =
271
+ "is_ok" in res && typeof res?.is_ok === "boolean" && res.is_ok === true;
272
+
273
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
274
+ state: isSuccess ? "SUCCESS" : "FAIL",
275
+ ...res,
276
+ });
277
+ }
278
+
279
+ return res;
280
+ } catch (error: unknown) {
281
+ errRes.error =
282
+ error instanceof Error
283
+ ? error.message || "알 수 없는 오류가 발생하였습니다."
284
+ : String(error);
285
+
286
+ if (!logDisabled) {
287
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
288
+ state: "ERROR (RESPONSE)",
289
+ error,
290
+ ...log,
291
+ });
292
+ }
293
+
294
+ return errRes;
295
+ }
296
+ };
297
+
298
+ /**
299
+ * 콘솔로그; Next.js API Route 디버깅용
300
+ * @util
301
+ * @param {string} method GET, POST, DELETE
302
+ * @param {string} routeUrl Next.js route API route URL
303
+ * @param {string} [queryUrl] DB API route URL
304
+ * @param {unknown[]} [messages] 로그 데이터
305
+ */
306
+ export const nextAPILog = (
307
+ method: string,
308
+ routeUrl: string,
309
+ queryUrl?: string,
310
+ ...messages: unknown[]
311
+ ) => {
312
+ if (process.env.NODE_ENV === "production") return;
313
+
314
+ const DATE = dateFormat(new Date());
315
+ const TIME =
316
+ new Intl.DateTimeFormat("en-US", {
317
+ hour: "numeric",
318
+ minute: "numeric",
319
+ second: "numeric",
320
+ hour12: false,
321
+ }).format(new Date()) +
322
+ "." +
323
+ new Date().getMilliseconds();
324
+
325
+ const REQUEST_METHOD = method.split("-")[0]?.toUpperCase() || "-";
326
+ const RESPONSE_METHOD = method.split("-")[1]
327
+ ? method.split("-")[1]?.toUpperCase() || "-"
328
+ : REQUEST_METHOD;
329
+
330
+ return console.log(
331
+ // `⚡️`,
332
+ `\n`,
333
+ `[Next Server API] ⏱ ${DATE} ( ${TIME} )`,
334
+ `\n λ [${REQUEST_METHOD}] FE route`,
335
+ `\n ${routeUrl}`,
336
+ ...(queryUrl
337
+ ? [`\n ═⏵ Ω [${RESPONSE_METHOD}] BE url`, `\n ${queryUrl}`, `\n`]
338
+ : []),
339
+ ...(messages?.[0] ? [` ◇-`, ...messages] : []),
340
+ `\n`,
341
+ );
342
+ };
343
+
344
+ /**
345
+ * API; POST/DELETE method fetch
346
+ * @util
347
+ * @param {object} props
348
+ * @param {"ai" | "db" | string} props.infra API 인프라
349
+ * @param {"POST" | "DELETE"} props.method POST, DELETE
350
+ * @param {string} props.routeUrl Next.js /app/api 라우트 주소
351
+ * @param {string} props.queryUrl 백엔드 API url
352
+ * @param {HeadersInit} [props.headers] fetch Headers
353
+ * @param {BodyInit | null} [props.body] post/delete body (이대로 바로 전송됨)
354
+ * @param {object} [props.bodyData] body로 전송하기 위해 가공이 필요한 데이터 객체
355
+ * @param {object} [props.log] 디버깅용 서버 로그 정보
356
+ * @param {boolean} [props.logDisabled] 로그 비활성화
357
+ * @param {boolean} [props.fetchDisabled] fetch 실행 비활성화
358
+ * @param {object} [props.alternateResponse] fetch 실패 시 대체 응답
359
+ * @return {Promise<ResponseType>} POST, DELETE 응답
360
+ */
361
+ export const fetchWithBody = async <
362
+ BodyDataType = object,
363
+ ResponseType extends object | Response = Response,
364
+ >({
365
+ infra,
366
+ method,
367
+ routeUrl,
368
+ queryUrl,
369
+ searchParams,
370
+ headers,
371
+ body,
372
+ bodyData,
373
+ isRawResponse,
374
+ alternateResponse,
375
+ debug,
376
+ disabled,
377
+ disabledLog,
378
+ }: {
379
+ /**
380
+ * API 인프라
381
+ * - ai: AI API
382
+ * - db: DB API
383
+ * - string: 직접 입력한 도메인
384
+ */
385
+ infra: "ai" | "db" | "uniai" | string;
386
+ /**
387
+ * 요청 방식
388
+ * POST, DELETE
389
+ */
390
+ method: string;
391
+ /**
392
+ * 프론트 API URL
393
+ */
394
+ routeUrl: string;
395
+ /**
396
+ * 백엔드 API 요청 URL
397
+ */
398
+ queryUrl: string;
399
+ /**
400
+ * fetch 실패 시 대체 응답
401
+ */
402
+ alternateResponse: ResponseType;
403
+ } & Partial<{
404
+ searchParams: URLSearchParams | object;
405
+ /**
406
+ * fetch Headers
407
+ */
408
+ headers: HeadersInit;
409
+ /**
410
+ * fetch Body
411
+ */
412
+ body: BodyInit | null;
413
+ /**
414
+ * fetch Body Data
415
+ */
416
+ bodyData: BodyDataType;
417
+ /**
418
+ * 응답값 원본으로 return
419
+ * const responseRaw = await fetch(api);
420
+ */
421
+ isRawResponse: boolean;
422
+ /**
423
+ * 디버그용 로그 객체
424
+ */
425
+ debug: object;
426
+ /**
427
+ * fetch 비활성화
428
+ */
429
+ disabled: boolean;
430
+ /**
431
+ * 로그 비활성화
432
+ */
433
+ disabledLog: boolean;
434
+ }>): Promise<ResponseType> => {
435
+ // ------------------------------------- API 요청 URL 생성
436
+ // API 인프라에 따른 요청 도메인 추출
437
+ const DOMAIN = infraDomain(infra);
438
+
439
+ // url 생성
440
+ const url = `${DOMAIN}${queryUrl}${getQueryString(searchParams)}`;
441
+
442
+ // ------------------------------------- 쿼리 옵션 생성
443
+ const option: RequestInit = getFetchOptions({
444
+ method,
445
+ headers,
446
+ body,
447
+ });
448
+
449
+ // ------------------------------------- body data 적용
450
+ // 완성된 body가 없고, body 데이터는 있는 경우
451
+ if (!body && typeof bodyData !== "undefined") {
452
+ // bodyData가 undefined가 아닌데도, bodyData의 데이터가 유효하지 않은 경우
453
+ if (typeof bodyData !== "undefined" && String(bodyData) === "") {
454
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
455
+ state: "ERROR (BODY - DATA)",
456
+ error: "데이터가 유효하지 않습니다.",
457
+ bodyData,
458
+ ...debug,
459
+ });
460
+
461
+ return alternateResponse;
462
+ }
463
+
464
+ // option에 bodyData 적용
465
+ Object.assign(option, { body: bodyData });
466
+ }
467
+
468
+ // option 준비가 완료되면, READY 상태
469
+ if (!disabledLog)
470
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
471
+ state: "READY",
472
+ ...option,
473
+ ...debug,
474
+ });
475
+
476
+ // ------------------------------------- fetch 요청
477
+ if (disabled) {
478
+ // fetch 요청 비활성화
479
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
480
+ state: "FETCH DISABLED",
481
+ message: "API 요청제한 활성화됨.",
482
+ });
483
+ return alternateResponse;
484
+ }
485
+
486
+ try {
487
+ const responseRaw = await fetch(url, option);
488
+
489
+ // API 응답 상태 코드
490
+ const resCode = responseRaw.status;
491
+
492
+ // API 응답 성공 여부
493
+ if (!disabledLog) {
494
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
495
+ state: resCode === 200 ? "SUCCESS" : "FAIL",
496
+ code: resCode,
497
+ });
498
+ }
499
+
500
+ if (isRawResponse) return responseRaw as ResponseType;
501
+
502
+ const res = await responseRaw.json();
503
+ return res;
504
+ } catch (error: unknown) {
505
+ // 에러 응답
506
+ if (!disabledLog)
507
+ nextAPILog(method.toLowerCase(), routeUrl, url, {
508
+ state: "ERROR (RESPONSE)",
509
+ error,
510
+ ...debug,
511
+ });
512
+
513
+ return alternateResponse;
514
+ }
515
+ };