@uniai-fe/uds-templates 0.4.7 → 0.4.9

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.
package/README.md CHANGED
@@ -107,7 +107,7 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
107
107
  - ui-legacy 스택 기반 모달 Provider/Root/Container + 템플릿(`Modal.Alert`, `Modal.Dialog`)
108
108
  - Storybook(`apps/design-storybook/src/stories/templates/modal`)에서 Alert/Confirm 케이스를 검증한다.
109
109
  - `/service-inquiry/**`
110
- - 문의 입력 전용 form, 기본 원형 `?` open button, 커스텀 trigger용 open hook, 페이지 context 등록 hook, request context 조립 hook, modal preset factory를 제공한다.
110
+ - 문의 입력 전용 form, 기본 원형 `?` open button, 커스텀 trigger용 open hook, 페이지 context 등록 hook, request context 조립 hook, 네트워크 오류 수집 hook, modal preset factory를 제공한다.
111
111
  - submit transport, React Query mutation, Next.js route handler, 에러 피드백(`Modal.Alert`)은 서비스 앱이 소유한다.
112
112
  - 모듈 내부 Jotai registry를 사용하므로, layout 고정 버튼 1개와 페이지별 context 등록 hook 조합으로 ready-to-use 구성이 가능하다.
113
113
  - 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
@@ -124,10 +124,11 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
124
124
 
125
125
  1. 서비스 앱이 `react-hook-form`으로 `defaultValues`와 `onSubmit`을 준비한다.
126
126
  2. layout 고정 버튼은 `ServiceInquiry.useUserContext()`로 모듈 내부 registry 기반 `requestContext`를 읽는다.
127
- 3. 페이지/폼은 `ServiceInquiry.useProvideContext({ labels, userContext })`로 `ServiceInquiryProvidedContext`를 등록한다.
128
- 4. 기본 버튼은 `ServiceInquiry.OpenButton`, 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
129
- 5. modal footer confirm이 `ServiceInquiry.Form` submit 진입을 담당한다.
130
- 6. 실제 submit은 서비스 앱의 `useMutation + Next.js route handler + Modal.Alert` 조합으로 처리한다.
127
+ 3. 서비스 앱은 네트워크 오류가 발생했을 때 `ServiceInquiry.useNetworkError().reportNetworkError(...)`만 호출하고, 모듈이 이를 `user_context.network_errors`에 자동 병합한다.
128
+ 4. 페이지/폼은 `ServiceInquiry.useProvideContext({ labels, userContext })`로 `ServiceInquiryProvidedContext`를 등록한다.
129
+ 5. 기본 버튼은 `ServiceInquiry.OpenButton`, 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
130
+ 6. modal footer confirm이 `ServiceInquiry.Form` submit 진입을 담당한다.
131
+ 7. 실제 submit은 서비스 앱의 `useMutation + Next.js route handler + Modal.Alert` 조합으로 처리한다.
131
132
 
132
133
  `service-inquiry`는 구조와 request context까지만 제공하고, 네트워크 상태/재시도/성공·실패 피드백은 서비스 앱이 소유합니다.
133
134
 
package/dist/styles.css CHANGED
@@ -1764,5 +1764,5 @@
1764
1764
  left: var(--service-inquiry-button-pos-left);
1765
1765
  right: var(--service-inquiry-button-pos-right);
1766
1766
  bottom: var(--service-inquiry-button-pos-bottom);
1767
- z-index: 100;
1767
+ z-index: 700;
1768
1768
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -2,12 +2,14 @@
2
2
 
3
3
  import { Button } from "@uniai-fe/uds-primitives";
4
4
  import { useOpenServiceInquiry } from "../hooks";
5
- import type { UseOpenServiceInquiryOptions } from "../types";
5
+ import type { ServiceInquiryOpenButtonProps } from "../types";
6
+ import clsx from "clsx";
6
7
 
7
8
  /**
8
9
  * Service Inquiry Open Button; 문의 모달 trigger adapter
9
10
  * @component
10
11
  * @param {UseOpenServiceInquiryOptions} props 문의 모달 열기 props
12
+ * @param {string} [props.className]
11
13
  * @param {string} props.stackKey modal stack key
12
14
  * @param {ServiceInquiryFormProps} props.formProps 문의 form props
13
15
  * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [props.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
@@ -19,11 +21,12 @@ import type { UseOpenServiceInquiryOptions } from "../types";
19
21
  * />
20
22
  */
21
23
  const ServiceInquiryOpenButton = ({
24
+ className,
22
25
  stackKey,
23
26
  formProps,
24
27
  dialogOptions,
25
28
  onOpen,
26
- }: UseOpenServiceInquiryOptions) => {
29
+ }: ServiceInquiryOpenButtonProps) => {
27
30
  const { openServiceInquiry } = useOpenServiceInquiry({
28
31
  stackKey,
29
32
  formProps,
@@ -33,7 +36,7 @@ const ServiceInquiryOpenButton = ({
33
36
 
34
37
  return (
35
38
  <Button.Rounded
36
- className="service-inquiry-open-button"
39
+ className={clsx("service-inquiry-open-button", className)}
37
40
  priority="tertiary"
38
41
  size="large"
39
42
  // 변경 설명: 기본 제공 버튼은 고정 원형 `?` 버튼 사양으로 렌더링한다.
@@ -1,3 +1,4 @@
1
1
  export * from "./useOpen";
2
+ export * from "./useNetworkError";
2
3
  export * from "./useProvideContext";
3
4
  export * from "./useUserContext";
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { useAtom } from "jotai";
4
+ import { serviceInquiryNetworkErrorsAtom } from "../jotai";
5
+ import type {
6
+ ServiceInquiryNetworkError,
7
+ UseServiceInquiryNetworkErrorReturn,
8
+ } from "../types";
9
+
10
+ /**
11
+ * Service Inquiry Hook; 네트워크 오류 수집 Hook
12
+ * @hook
13
+ * @returns {UseServiceInquiryNetworkErrorReturn} 최근 오류 목록과 기록/초기화 함수
14
+ * @example
15
+ * const { reportNetworkError } = useServiceInquiryNetworkError();
16
+ * reportNetworkError({ route: "/api/v2/login", message: "timeout" });
17
+ */
18
+ export function useServiceInquiryNetworkError(): UseServiceInquiryNetworkErrorReturn {
19
+ const [networkErrors, setNetworkErrors] = useAtom(
20
+ serviceInquiryNetworkErrorsAtom,
21
+ );
22
+
23
+ const reportNetworkError: UseServiceInquiryNetworkErrorReturn["reportNetworkError"] =
24
+ (nextError: ServiceInquiryNetworkError) => {
25
+ setNetworkErrors(currentErrors =>
26
+ [
27
+ {
28
+ ...nextError,
29
+ timestamp: nextError.timestamp ?? new Date().toISOString(),
30
+ },
31
+ ...currentErrors,
32
+ ].slice(0, 5),
33
+ );
34
+ };
35
+
36
+ const clearNetworkErrors: UseServiceInquiryNetworkErrorReturn["clearNetworkErrors"] =
37
+ () => {
38
+ setNetworkErrors([]);
39
+ };
40
+
41
+ return {
42
+ networkErrors,
43
+ reportNetworkError,
44
+ clearNetworkErrors,
45
+ };
46
+ }
@@ -2,11 +2,16 @@ import "./index.scss";
2
2
  import ServiceInquiryForm from "./components/Form";
3
3
  import ServiceInquiryOpenButton from "./components/OpenButton";
4
4
  import {
5
+ useServiceInquiryNetworkError,
5
6
  useOpenServiceInquiry,
6
7
  useProvideServiceInquiryContext,
7
8
  useServiceInquiryUserContext,
8
9
  } from "./hooks";
9
10
  import { createServiceInquiryModal } from "./utils/modal-option";
11
+ import {
12
+ getServiceInquiryDebugHeaders,
13
+ parseResponseWithServiceInquiryDebug,
14
+ } from "./utils/network-debug";
10
15
 
11
16
  /**
12
17
  * Service Inquiry; 문의 form + modal option 엔트리
@@ -16,17 +21,23 @@ export const ServiceInquiry = {
16
21
  Form: ServiceInquiryForm,
17
22
  OpenButton: ServiceInquiryOpenButton,
18
23
  useOpen: useOpenServiceInquiry,
24
+ useNetworkError: useServiceInquiryNetworkError,
19
25
  useProvideContext: useProvideServiceInquiryContext,
20
26
  useUserContext: useServiceInquiryUserContext,
21
27
  createModal: createServiceInquiryModal,
28
+ getNetworkDebugHeaders: getServiceInquiryDebugHeaders,
29
+ parseResponseDebug: parseResponseWithServiceInquiryDebug,
22
30
  };
23
31
 
24
32
  export {
25
33
  ServiceInquiryForm,
26
34
  ServiceInquiryOpenButton,
35
+ useServiceInquiryNetworkError,
27
36
  useOpenServiceInquiry,
28
37
  useProvideServiceInquiryContext,
29
38
  useServiceInquiryUserContext,
30
39
  createServiceInquiryModal,
40
+ getServiceInquiryDebugHeaders,
41
+ parseResponseWithServiceInquiryDebug,
31
42
  };
32
43
  export type * from "./types";
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { atom } from "jotai";
4
4
  import type {
5
+ ServiceInquiryNetworkError,
5
6
  ServiceInquiryRequestContext,
6
7
  ServiceInquiryProvidedContext,
7
8
  ServiceInquiryUserContext,
@@ -22,6 +23,14 @@ export const serviceInquiryProvidedContextRegistryAtom = atom<
22
23
  export const serviceInquiryAdditionalUserContextAtom =
23
24
  atom<ServiceInquiryUserContext | null>(null);
24
25
 
26
+ /**
27
+ * Service Inquiry State; 최근 네트워크 오류 목록
28
+ * @state
29
+ */
30
+ export const serviceInquiryNetworkErrorsAtom = atom<
31
+ ServiceInquiryNetworkError[]
32
+ >([]);
33
+
25
34
  /**
26
35
  * Service Inquiry State; registry 병합 context
27
36
  * @state
@@ -64,16 +73,22 @@ export const serviceInquiryRequestContextAtom =
64
73
  atom<ServiceInquiryRequestContext>(get => {
65
74
  const providedContext = get(serviceInquiryProvidedContextAtom);
66
75
  const additionalUserContext = get(serviceInquiryAdditionalUserContextAtom);
76
+ const networkErrors = get(serviceInquiryNetworkErrorsAtom);
67
77
 
68
78
  return {
69
79
  labels: providedContext.labels ?? [],
70
80
  // 변경 설명: page_path는 서비스 입력값이 아니라 모듈이 현재 페이지 URL 전체를 자동 수집하는 시스템 필드로 고정한다.
71
81
  page_path: typeof window === "undefined" ? "" : window.location.href,
72
82
  user_context:
73
- providedContext.userContext || additionalUserContext
83
+ providedContext.userContext ||
84
+ additionalUserContext ||
85
+ networkErrors.length > 0
74
86
  ? {
75
87
  ...(providedContext.userContext ?? {}),
76
88
  ...(additionalUserContext ?? {}),
89
+ ...(networkErrors.length > 0
90
+ ? { network_errors: networkErrors }
91
+ : {}),
77
92
  }
78
93
  : null,
79
94
  };
@@ -7,5 +7,5 @@
7
7
  left: var(--service-inquiry-button-pos-left);
8
8
  right: var(--service-inquiry-button-pos-right);
9
9
  bottom: var(--service-inquiry-button-pos-bottom);
10
- z-index: 100;
10
+ z-index: 700;
11
11
  }
@@ -19,6 +19,94 @@ export interface ServiceInquiryProvidedContext {
19
19
  userContext?: ServiceInquiryUserContext | null;
20
20
  }
21
21
 
22
+ /**
23
+ * Service Inquiry Hook; 네트워크 오류 헤더 요약
24
+ * @property {string} [uniai_native_domain] 응답 헤더 `Uniai-Native-Domain`
25
+ * @property {string} [uniai_native_path] 응답 헤더 `Uniai-Native-Path`
26
+ * @property {string} [uniai_native_url] 응답 헤더 `Uniai-Native-URL`
27
+ * @property {string} [content_type] 응답 헤더 `content-type`
28
+ * @property {boolean} [has_authorization] 요청 헤더에 Authorization 존재 여부
29
+ */
30
+ export interface ServiceInquiryNetworkErrorHeaders {
31
+ /**
32
+ * 응답 헤더 `Uniai-Native-Domain`
33
+ */
34
+ uniai_native_domain?: string;
35
+ /**
36
+ * 응답 헤더 `Uniai-Native-Path`
37
+ */
38
+ uniai_native_path?: string;
39
+ /**
40
+ * 응답 헤더 `Uniai-Native-URL`
41
+ */
42
+ uniai_native_url?: string;
43
+ /**
44
+ * 응답 헤더 `content-type`
45
+ */
46
+ content_type?: string;
47
+ /**
48
+ * 요청 헤더에 Authorization 존재 여부
49
+ */
50
+ has_authorization?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Service Inquiry Hook; 네트워크 오류 기록
55
+ * @property {string} [route] 요청 route 또는 endpoint
56
+ * @property {number} [code] HTTP status code
57
+ * @property {string} [state] 서비스 레이어 상태 문자열
58
+ * @property {string} [message] 사용자/운영 확인용 오류 메시지
59
+ * @property {ServiceInquiryNetworkErrorHeaders} [headers] 선택 수집된 request/response 헤더 정보
60
+ * @property {string} [timestamp] 오류 기록 시각
61
+ */
62
+ export interface ServiceInquiryNetworkError {
63
+ /**
64
+ * 요청 route 또는 endpoint
65
+ */
66
+ route?: string;
67
+ /**
68
+ * HTTP status code
69
+ */
70
+ code?: number;
71
+ /**
72
+ * 서비스 레이어 상태 문자열
73
+ */
74
+ state?: string;
75
+ /**
76
+ * 사용자/운영 확인용 오류 메시지
77
+ */
78
+ message?: string;
79
+ /**
80
+ * 선택 수집된 request/response 헤더 정보
81
+ */
82
+ headers?: ServiceInquiryNetworkErrorHeaders;
83
+ /**
84
+ * 오류 기록 시각
85
+ */
86
+ timestamp?: string;
87
+ }
88
+
89
+ /**
90
+ * Service Inquiry Hook; 네트워크 오류 수집 훅 반환값
91
+ * @property {ServiceInquiryNetworkError[]} networkErrors 누적된 최근 네트워크 오류 목록
92
+ * @property {(nextError: ServiceInquiryNetworkError) => void} reportNetworkError 네트워크 오류 1건 기록
93
+ * @property {() => void} clearNetworkErrors 누적된 네트워크 오류 초기화
94
+ */
95
+ export interface UseServiceInquiryNetworkErrorReturn {
96
+ /**
97
+ * 누적된 최근 네트워크 오류 목록
98
+ */
99
+ networkErrors: ServiceInquiryNetworkError[];
100
+ /**
101
+ * 네트워크 오류 1건 기록
102
+ */
103
+ reportNetworkError: (nextError: ServiceInquiryNetworkError) => void;
104
+ /**
105
+ * 누적된 네트워크 오류 초기화
106
+ */
107
+ clearNetworkErrors: () => void;
108
+ }
109
+
22
110
  /**
23
111
  * Service Inquiry Hook; user_context 훅 반환값
24
112
  * @property {ServiceInquiryRequestContext} requestContext form submit에 바로 사용할 request context
@@ -134,6 +134,18 @@ export interface UseOpenServiceInquiryOptions extends ServiceInquiryCreateModalO
134
134
  onOpen?: () => void;
135
135
  }
136
136
 
137
+ /**
138
+ * Service Inquiry Button props; 버튼 옵션
139
+ * @property {string} [className]
140
+ * @property {string} stackKey modal stack key
141
+ * @property {ServiceInquiryFormProps} formProps 문의 form props
142
+ * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
143
+ * @property {() => void} [onOpen] 모달 open 직전 콜백
144
+ */
145
+ export interface ServiceInquiryOpenButtonProps extends UseOpenServiceInquiryOptions {
146
+ className?: string;
147
+ }
148
+
137
149
  /**
138
150
  * Service Inquiry Hook; 문의 모달 열기 훅 반환값
139
151
  * @property {() => void} openServiceInquiry 문의 모달 open 함수
@@ -0,0 +1,112 @@
1
+ import type { ServiceInquiryNetworkErrorHeaders } from "../types";
2
+
3
+ /**
4
+ * Service Inquiry Utility; Authorization 헤더 존재 여부 판별
5
+ * @param {HeadersInit} [requestHeaders] 요청 헤더
6
+ * @returns {boolean} Authorization 헤더 존재 여부
7
+ * @example
8
+ * const hasAuthorization = resolveServiceInquiryHasAuthorization({
9
+ * Authorization: "Bearer token",
10
+ * });
11
+ */
12
+ const resolveServiceInquiryHasAuthorization = (
13
+ requestHeaders?: HeadersInit,
14
+ ): boolean => {
15
+ if (!requestHeaders) {
16
+ return false;
17
+ }
18
+
19
+ return new Headers(requestHeaders).has("Authorization");
20
+ };
21
+
22
+ /**
23
+ * Service Inquiry Utility; 선택 디버그 헤더 추출
24
+ * @param {Headers} responseHeaders 응답 헤더
25
+ * @param {HeadersInit} [requestHeaders] 요청 헤더
26
+ * @returns {ServiceInquiryNetworkErrorHeaders | undefined} service-inquiry에 기록할 최소 헤더 정보
27
+ * @example
28
+ * const headers = pickServiceInquiryDebugHeaders(response.headers, {
29
+ * Authorization: "Bearer token",
30
+ * });
31
+ */
32
+ const pickServiceInquiryDebugHeaders = (
33
+ responseHeaders: Headers,
34
+ requestHeaders?: HeadersInit,
35
+ ): ServiceInquiryNetworkErrorHeaders | undefined => {
36
+ const headers: ServiceInquiryNetworkErrorHeaders = {
37
+ uniai_native_domain:
38
+ responseHeaders.get("Uniai-Native-Domain") ?? undefined,
39
+ uniai_native_path: responseHeaders.get("Uniai-Native-Path") ?? undefined,
40
+ uniai_native_url: responseHeaders.get("Uniai-Native-URL") ?? undefined,
41
+ content_type: responseHeaders.get("content-type") ?? undefined,
42
+ has_authorization: resolveServiceInquiryHasAuthorization(requestHeaders),
43
+ };
44
+
45
+ if (
46
+ !headers.uniai_native_domain &&
47
+ !headers.uniai_native_path &&
48
+ !headers.uniai_native_url &&
49
+ !headers.content_type &&
50
+ !headers.has_authorization
51
+ ) {
52
+ return undefined;
53
+ }
54
+
55
+ return headers;
56
+ };
57
+
58
+ /**
59
+ * Service Inquiry Utility; 응답 payload에 디버그 헤더 주입
60
+ * @param {Response} response fetch 응답 객체
61
+ * @param {HeadersInit} [requestHeaders] 요청 헤더
62
+ * @returns {Promise<ResponseData>} 선택 디버그 헤더가 포함된 응답 payload
63
+ * @example
64
+ * const payload = await parseResponseWithServiceInquiryDebug(response, {
65
+ * Authorization: "Bearer token",
66
+ * });
67
+ */
68
+ export async function parseResponseWithServiceInquiryDebug<
69
+ ResponseData extends object,
70
+ >(response: Response, requestHeaders?: HeadersInit): Promise<ResponseData> {
71
+ const payload = (await response.json()) as ResponseData;
72
+ const headers = pickServiceInquiryDebugHeaders(
73
+ response.headers,
74
+ requestHeaders,
75
+ );
76
+
77
+ if (!headers) {
78
+ return payload;
79
+ }
80
+
81
+ // 변경 설명: service-inquiry 네트워크 기록에 필요한 최소 header 정보만 응답 payload에 숨겨서 전달한다.
82
+ return Object.assign(payload, {
83
+ _service_inquiry_debug: {
84
+ headers,
85
+ },
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Service Inquiry Utility; 응답 payload에서 디버그 헤더 읽기
91
+ * @param {unknown} response service 응답 payload
92
+ * @returns {ServiceInquiryNetworkErrorHeaders | undefined} 숨겨서 실은 디버그 헤더
93
+ * @example
94
+ * const headers = getServiceInquiryDebugHeaders(response);
95
+ */
96
+ export function getServiceInquiryDebugHeaders(
97
+ response: unknown,
98
+ ): ServiceInquiryNetworkErrorHeaders | undefined {
99
+ if (!response || typeof response !== "object") {
100
+ return undefined;
101
+ }
102
+
103
+ const debugData = (
104
+ response as {
105
+ _service_inquiry_debug?: {
106
+ headers?: ServiceInquiryNetworkErrorHeaders;
107
+ };
108
+ }
109
+ )._service_inquiry_debug;
110
+
111
+ return debugData?.headers;
112
+ }