@uniai-fe/uds-templates 0.5.21 → 0.5.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -21,6 +21,7 @@ import type {
21
21
  export function useAuthLoginForm({
22
22
  fieldNames,
23
23
  form,
24
+ isSubmitting,
24
25
  onLogin,
25
26
  }: UseAuthLoginFormOptions): UseAuthLoginFormReturn {
26
27
  /** 1) form init — useForm은 FormField에서 생성 후 주입된다. */
@@ -67,7 +68,11 @@ export function useAuthLoginForm({
67
68
  return typeof value === "string" ? value.trim().length > 0 : false;
68
69
  });
69
70
 
70
- const disabled = form.formState.isSubmitting || !trimmedFilled;
71
+ // RHF isSubmitting handleSubmit 콜백이 반환한 Promise 동안만 유지된다.
72
+ // 서비스 앱 로그인 컨테이너는 React Query mutate()를 호출하고 즉시 반환하므로,
73
+ // 실제 API pending 동안의 중복 클릭/Enter submit 차단은 외부 isSubmitting을 함께 봐야 한다.
74
+ const disabled =
75
+ form.formState.isSubmitting || Boolean(isSubmitting) || !trimmedFilled;
71
76
 
72
77
  /** 4) submit — onLogin을 handleSubmit과 결합한다. */
73
78
  const onSubmit = form.handleSubmit(onLogin);
@@ -3,6 +3,7 @@
3
3
  import { useMemo } from "react";
4
4
  import { useForm } from "react-hook-form";
5
5
  import { Button } from "@uniai-fe/uds-primitives";
6
+ import type { FormEventHandler, SubmitEvent } from "react";
6
7
  import type { AuthLoginFieldOptions, AuthLoginFormValues } from "../types";
7
8
  import { useAuthLoginForm } from "../hooks";
8
9
  import AuthLoginFormFieldId from "./UserId";
@@ -15,6 +16,7 @@ import AuthLoginFormFieldPassword from "./Password";
15
16
  * @property {AuthLoginIdFieldOptions} [props.idField] 아이디 필드 옵션
16
17
  * @property {AuthLoginPasswordFieldOptions} [props.passwordField] 비밀번호 필드 옵션
17
18
  * @property {AuthLoginFieldTexts} [props.texts] 로그인 form 기본 문구 옵션
19
+ * @property {boolean} [props.isSubmitting] 외부 로그인 요청 pending 여부
18
20
  * @property {React.FormHTMLAttributes<HTMLFormElement>} [props.formAttr] <form> attr
19
21
  * @property {SubmitHandler<AuthLoginFormValues>} props.onLogin 로그인 콜백
20
22
  */
@@ -22,6 +24,7 @@ export default function AuthLoginFormField({
22
24
  idField,
23
25
  passwordField,
24
26
  texts,
27
+ isSubmitting,
25
28
  formAttr,
26
29
  onLogin,
27
30
  }: AuthLoginFieldOptions) {
@@ -55,9 +58,34 @@ export default function AuthLoginFormField({
55
58
  password: passwordFieldName,
56
59
  },
57
60
  form,
61
+ isSubmitting,
58
62
  onLogin,
59
63
  });
60
64
 
65
+ const handleFormSubmit: FormEventHandler<HTMLFormElement> = event => {
66
+ const nativeEvent =
67
+ event.nativeEvent as unknown as SubmitEvent<HTMLFormElement>;
68
+
69
+ // 소비 앱이 formAttr.onSubmit에서 analytics, validation bridge, custom preventDefault 등을
70
+ // 처리할 수 있으므로 템플릿 guard보다 먼저 실행한다.
71
+ formAttr?.onSubmit?.(nativeEvent);
72
+ if (event.defaultPrevented || nativeEvent.defaultPrevented) {
73
+ event.preventDefault();
74
+ return;
75
+ }
76
+
77
+ // disabled는 버튼 상태뿐 아니라 form submit 경계의 단일 잠금 조건이다.
78
+ // 버튼 클릭이 아닌 Enter submit도 같은 경로에서 막아 로그인 지연 중 중복 API 요청을 차단한다.
79
+ if (disabled) {
80
+ event.preventDefault();
81
+ return;
82
+ }
83
+
84
+ // 잠금 조건을 통과한 경우에만 RHF handleSubmit으로 넘긴다.
85
+ // 이 시점 이후의 API 호출, 오류 분류, Modal.Alert/Dialog 표시는 서비스 앱 책임이다.
86
+ void onSubmit(event);
87
+ };
88
+
61
89
  // helper 텍스트는 RHF 에러 > 필드 옵션 helper 순으로 병합하며, state가 없으면 undefined 처리한다.
62
90
  const idHelperNode =
63
91
  helpers.id.text !== undefined ? helpers.id.text : idField?.helper;
@@ -79,8 +107,15 @@ export default function AuthLoginFormField({
79
107
  const passwordLabel =
80
108
  passwordField?.label ?? texts?.passwordLabel ?? "비밀번호";
81
109
 
110
+ // aria-busy는 외부 로그인 요청 진행 상태만 전달한다.
111
+ // 실제 사용자 피드백 modal/문구는 서비스 앱의 Modal.Alert/Dialog 흐름에서 처리한다.
82
112
  return (
83
- <form className="auth-login-form" {...formAttr} onSubmit={onSubmit}>
113
+ <form
114
+ className="auth-login-form"
115
+ {...formAttr}
116
+ aria-busy={Boolean(isSubmitting) || undefined}
117
+ onSubmit={handleFormSubmit}
118
+ >
84
119
  <div className="auth-login-fields">
85
120
  {/* 아이디 필드: placeholder/label/templateProps를 그대로 내려 투명한 조립 구조를 유지한다. */}
86
121
  <AuthLoginFormFieldId
@@ -39,6 +39,7 @@ export interface AuthLoginFieldNames {
39
39
  * @property {UseFormReturn<AuthLoginFormValues>} form RHF useForm 반환값
40
40
  * @property {SubmitHandler<AuthLoginFormValues>} onLogin 제출 핸들러
41
41
  * @property {AuthLoginFieldNames} fieldNames RHF field name 매핑
42
+ * @property {boolean} [isSubmitting] 외부 로그인 요청 pending 여부
42
43
  */
43
44
  export interface UseAuthLoginFormOptions {
44
45
  /**
@@ -53,6 +54,13 @@ export interface UseAuthLoginFormOptions {
53
54
  * field name 매핑
54
55
  */
55
56
  fieldNames: AuthLoginFieldNames;
57
+ /**
58
+ * 외부 로그인 요청 pending 여부
59
+ * @desc
60
+ * - onLogin이 mutate()처럼 동기 반환되는 경우 RHF isSubmitting만으로는 API pending 구간을 잠글 수 없다.
61
+ * - 서비스 앱의 pending 상태를 병합해 버튼 클릭과 Enter submit의 중복 요청을 차단한다.
62
+ */
63
+ isSubmitting?: boolean;
56
64
  }
57
65
 
58
66
  /**
@@ -281,6 +281,7 @@ export interface AuthLoginTexts
281
281
  * @property {AuthLoginIdFieldOptions} [idField] 로그인 아이디 필드 옵션
282
282
  * @property {AuthLoginPasswordFieldOptions} [passwordField] 로그인 비밀번호 필드 옵션
283
283
  * @property {AuthLoginFieldTexts} [texts] 로그인 form 기본 문구 옵션
284
+ * @property {boolean} [isSubmitting] 외부 로그인 요청 pending 여부
284
285
  * @property {React.FormHTMLAttributes<HTMLFormElement>} [formAttr] <form /> attributes
285
286
  * @property {SubmitHandler<Record<string, string>>} onLogin 로그인 콜백 이벤트
286
287
  */
@@ -297,6 +298,14 @@ export interface AuthLoginFieldOptions {
297
298
  * 로그인 form 기본 문구 옵션
298
299
  */
299
300
  texts?: AuthLoginFieldTexts;
301
+ /**
302
+ * 외부 로그인 요청 pending 여부
303
+ * @desc
304
+ * - 서비스 앱에서 React Query mutation pending 값을 주입한다.
305
+ * - RHF isSubmitting은 onLogin이 Promise를 반환할 때만 실제 요청 시간을 대표하므로,
306
+ * mutate()를 호출하고 즉시 반환하는 서비스 흐름에서는 이 값으로 중복 제출을 잠근다.
307
+ */
308
+ isSubmitting?: boolean;
300
309
  /**
301
310
  * form attr
302
311
  */