@uniai-fe/uds-templates 0.5.22 → 0.5.24

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.22",
3
+ "version": "0.5.24",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -106,6 +106,24 @@ export default function AuthLoginFormField({
106
106
  const idLabel = idField?.label ?? texts?.idLabel ?? "아이디";
107
107
  const passwordLabel =
108
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
+ };
109
127
 
110
128
  // aria-busy는 외부 로그인 요청 진행 상태만 전달한다.
111
129
  // 실제 사용자 피드백 modal/문구는 서비스 앱의 Modal.Alert/Dialog 흐름에서 처리한다.
@@ -113,7 +131,7 @@ export default function AuthLoginFormField({
113
131
  <form
114
132
  className="auth-login-form"
115
133
  {...formAttr}
116
- aria-busy={Boolean(isSubmitting) || undefined}
134
+ aria-busy={isLoginSubmitting || undefined}
117
135
  onSubmit={handleFormSubmit}
118
136
  >
119
137
  <div className="auth-login-fields">
@@ -129,7 +147,7 @@ export default function AuthLoginFormField({
129
147
  ? `${idLabel}를 입력해 주세요`
130
148
  : "아이디를 입력해 주세요")
131
149
  }
132
- inputProps={idField?.inputProps}
150
+ inputProps={idInputProps}
133
151
  templateProps={idField?.templateProps}
134
152
  />
135
153
  {/* 비밀번호 필드: PasswordInput 특성만 다르고 나머지 구조는 동일하다. */}
@@ -142,7 +160,7 @@ export default function AuthLoginFormField({
142
160
  texts?.passwordPlaceholder ??
143
161
  "비밀번호를 입력해 주세요"
144
162
  }
145
- inputProps={passwordField?.inputProps}
163
+ inputProps={passwordInputProps}
146
164
  templateProps={passwordField?.templateProps}
147
165
  />
148
166
  <Button.Default
@@ -151,9 +169,13 @@ export default function AuthLoginFormField({
151
169
  size="xlarge"
152
170
  priority="primary"
153
171
  block
172
+ // 외부 pending 중에는 primitives Button의 loading 경로를 사용한다.
173
+ // Button.Default는 loading=true일 때 readonly 상태와 aria-busy를 적용하므로,
174
+ // "값은 채워졌지만 로그인 요청 처리 중"인 상태를 disabled-only 상태와 구분할 수 있다.
175
+ loading={isLoginSubmitting}
154
176
  disabled={disabled}
155
177
  >
156
- {texts?.submit ?? "로그인"}
178
+ {submitLabel}
157
179
  </Button.Default>
158
180
  </div>
159
181
  </form>
@@ -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
  /**
@@ -159,6 +159,7 @@ export async function getServerCompanyList({
159
159
  total_count: 0,
160
160
  },
161
161
  errors: [],
162
+ meta: { locale: "ko", request_id: "" },
162
163
  };
163
164
  const API_OPTION = {
164
165
  domain,
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useCallback } from "react";
3
4
  import { useAtom } from "jotai";
4
5
  import { serviceInquiryNetworkErrorsAtom } from "../jotai";
5
6
  import type {
@@ -21,22 +22,25 @@ export function useServiceInquiryNetworkError(): UseServiceInquiryNetworkErrorRe
21
22
  );
22
23
 
23
24
  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
- };
25
+ useCallback(
26
+ (nextError: ServiceInquiryNetworkError) => {
27
+ setNetworkErrors(currentErrors =>
28
+ [
29
+ {
30
+ ...nextError,
31
+ timestamp: nextError.timestamp ?? new Date().toISOString(),
32
+ },
33
+ ...currentErrors,
34
+ ].slice(0, 5),
35
+ );
36
+ },
37
+ [setNetworkErrors],
38
+ );
35
39
 
36
40
  const clearNetworkErrors: UseServiceInquiryNetworkErrorReturn["clearNetworkErrors"] =
37
- () => {
41
+ useCallback(() => {
38
42
  setNetworkErrors([]);
39
- };
43
+ }, [setNetworkErrors]);
40
44
 
41
45
  return {
42
46
  networkErrors,