@uniai-fe/uds-templates 0.5.21 → 0.5.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.
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -78,9 +106,34 @@ export default function AuthLoginFormField({
|
|
|
78
106
|
const idLabel = idField?.label ?? texts?.idLabel ?? "아이디";
|
|
79
107
|
const passwordLabel =
|
|
80
108
|
passwordField?.label ?? texts?.passwordLabel ?? "비밀번호";
|
|
109
|
+
const isLoginSubmitting = Boolean(isSubmitting);
|
|
110
|
+
// Button loading은 readonly/aria-busy 같은 상태 의미를 제공하지만, 라벨은 자동 변경하지 않는다.
|
|
111
|
+
// pending 중에는 눈에 보이는 문구도 바꿔 사용자가 "요청 처리 중"임을 즉시 알 수 있게 한다.
|
|
112
|
+
const submitLabel = isLoginSubmitting
|
|
113
|
+
? (texts?.submitting ?? "로그인 중...")
|
|
114
|
+
: (texts?.submit ?? "로그인");
|
|
115
|
+
const idInputProps = {
|
|
116
|
+
...(idField?.inputProps ?? {}),
|
|
117
|
+
// 별도 fieldset 계층을 만들지 않고 primitives input에 직접 pending 잠금을 전달한다.
|
|
118
|
+
// readOnly는 현재 입력값을 유지하면서 편집만 막기 때문에, API 지연 중 RHF 값 흐름을 건드리지 않는다.
|
|
119
|
+
readOnly: isLoginSubmitting || idField?.inputProps?.readOnly,
|
|
120
|
+
};
|
|
121
|
+
const passwordInputProps = {
|
|
122
|
+
...(passwordField?.inputProps ?? {}),
|
|
123
|
+
// 비밀번호 필드도 아이디와 같은 pending 잠금 정책을 따른다.
|
|
124
|
+
// disabled 대신 readOnly를 써서 제출 중 값 보존과 UI 잠금을 함께 맞춘다.
|
|
125
|
+
readOnly: isLoginSubmitting || passwordField?.inputProps?.readOnly,
|
|
126
|
+
};
|
|
81
127
|
|
|
128
|
+
// aria-busy는 외부 로그인 요청 진행 상태만 전달한다.
|
|
129
|
+
// 실제 사용자 피드백 modal/문구는 서비스 앱의 Modal.Alert/Dialog 흐름에서 처리한다.
|
|
82
130
|
return (
|
|
83
|
-
<form
|
|
131
|
+
<form
|
|
132
|
+
className="auth-login-form"
|
|
133
|
+
{...formAttr}
|
|
134
|
+
aria-busy={isLoginSubmitting || undefined}
|
|
135
|
+
onSubmit={handleFormSubmit}
|
|
136
|
+
>
|
|
84
137
|
<div className="auth-login-fields">
|
|
85
138
|
{/* 아이디 필드: placeholder/label/templateProps를 그대로 내려 투명한 조립 구조를 유지한다. */}
|
|
86
139
|
<AuthLoginFormFieldId
|
|
@@ -94,7 +147,7 @@ export default function AuthLoginFormField({
|
|
|
94
147
|
? `${idLabel}를 입력해 주세요`
|
|
95
148
|
: "아이디를 입력해 주세요")
|
|
96
149
|
}
|
|
97
|
-
inputProps={
|
|
150
|
+
inputProps={idInputProps}
|
|
98
151
|
templateProps={idField?.templateProps}
|
|
99
152
|
/>
|
|
100
153
|
{/* 비밀번호 필드: PasswordInput 특성만 다르고 나머지 구조는 동일하다. */}
|
|
@@ -107,7 +160,7 @@ export default function AuthLoginFormField({
|
|
|
107
160
|
texts?.passwordPlaceholder ??
|
|
108
161
|
"비밀번호를 입력해 주세요"
|
|
109
162
|
}
|
|
110
|
-
inputProps={
|
|
163
|
+
inputProps={passwordInputProps}
|
|
111
164
|
templateProps={passwordField?.templateProps}
|
|
112
165
|
/>
|
|
113
166
|
<Button.Default
|
|
@@ -116,9 +169,13 @@ export default function AuthLoginFormField({
|
|
|
116
169
|
size="xlarge"
|
|
117
170
|
priority="primary"
|
|
118
171
|
block
|
|
172
|
+
// 외부 pending 중에는 primitives Button의 loading 경로를 사용한다.
|
|
173
|
+
// Button.Default는 loading=true일 때 readonly 상태와 aria-busy를 적용하므로,
|
|
174
|
+
// "값은 채워졌지만 로그인 요청 처리 중"인 상태를 disabled-only 상태와 구분할 수 있다.
|
|
175
|
+
loading={isLoginSubmitting}
|
|
119
176
|
disabled={disabled}
|
|
120
177
|
>
|
|
121
|
-
{
|
|
178
|
+
{submitLabel}
|
|
122
179
|
</Button.Default>
|
|
123
180
|
</div>
|
|
124
181
|
</form>
|
|
@@ -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
|
/**
|
|
@@ -223,6 +223,7 @@ export interface AuthLoginPasswordFieldOptions extends AuthLoginFieldProps<Input
|
|
|
223
223
|
* @property {ReactNode} [passwordLabel] 비밀번호 필드 기본 라벨
|
|
224
224
|
* @property {string} [passwordPlaceholder] 비밀번호 필드 기본 placeholder
|
|
225
225
|
* @property {ReactNode} [submit] submit 버튼 라벨
|
|
226
|
+
* @property {ReactNode} [submitting] 로그인 요청 pending 중 submit 버튼 라벨
|
|
226
227
|
*/
|
|
227
228
|
export interface AuthLoginFieldTexts {
|
|
228
229
|
/**
|
|
@@ -245,6 +246,10 @@ export interface AuthLoginFieldTexts {
|
|
|
245
246
|
* submit 버튼 라벨
|
|
246
247
|
*/
|
|
247
248
|
submit?: ReactNode;
|
|
249
|
+
/**
|
|
250
|
+
* 로그인 요청 pending 중 submit 버튼 라벨
|
|
251
|
+
*/
|
|
252
|
+
submitting?: ReactNode;
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
/**
|
|
@@ -281,6 +286,7 @@ export interface AuthLoginTexts
|
|
|
281
286
|
* @property {AuthLoginIdFieldOptions} [idField] 로그인 아이디 필드 옵션
|
|
282
287
|
* @property {AuthLoginPasswordFieldOptions} [passwordField] 로그인 비밀번호 필드 옵션
|
|
283
288
|
* @property {AuthLoginFieldTexts} [texts] 로그인 form 기본 문구 옵션
|
|
289
|
+
* @property {boolean} [isSubmitting] 외부 로그인 요청 pending 여부
|
|
284
290
|
* @property {React.FormHTMLAttributes<HTMLFormElement>} [formAttr] <form /> attributes
|
|
285
291
|
* @property {SubmitHandler<Record<string, string>>} onLogin 로그인 콜백 이벤트
|
|
286
292
|
*/
|
|
@@ -297,6 +303,14 @@ export interface AuthLoginFieldOptions {
|
|
|
297
303
|
* 로그인 form 기본 문구 옵션
|
|
298
304
|
*/
|
|
299
305
|
texts?: AuthLoginFieldTexts;
|
|
306
|
+
/**
|
|
307
|
+
* 외부 로그인 요청 pending 여부
|
|
308
|
+
* @desc
|
|
309
|
+
* - 서비스 앱에서 React Query mutation pending 값을 주입한다.
|
|
310
|
+
* - RHF isSubmitting은 onLogin이 Promise를 반환할 때만 실제 요청 시간을 대표하므로,
|
|
311
|
+
* mutate()를 호출하고 즉시 반환하는 서비스 흐름에서는 이 값으로 중복 제출을 잠근다.
|
|
312
|
+
*/
|
|
313
|
+
isSubmitting?: boolean;
|
|
300
314
|
/**
|
|
301
315
|
* form attr
|
|
302
316
|
*/
|