@uniai-fe/uds-templates 0.0.11 → 0.0.13
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 +11 -0
- package/dist/styles.css +916 -1074
- package/package.json +3 -3
- package/src/auth/common/complete/Template.tsx +47 -0
- package/src/auth/common/complete/img/circle-check-complete.svg +4 -0
- package/src/auth/common/complete/index.scss +38 -0
- package/src/auth/common/complete/types.ts +15 -0
- package/src/auth/common/container/header/StageHeader.tsx +61 -0
- package/src/auth/common/container/header/index.tsx +5 -0
- package/src/auth/common/container/header/stage-header.scss +50 -0
- package/src/{components/auth → auth/common}/container/index.tsx +2 -0
- package/src/auth/common/find/hooks/useFindAccountForm.ts +79 -0
- package/src/auth/common/find/markup/CodeStep.tsx +166 -0
- package/src/auth/common/find/markup/Header.tsx +46 -0
- package/src/auth/common/find/markup/InfoStep.tsx +109 -0
- package/src/auth/common/find/styles/email.scss +55 -0
- package/src/auth/common/find/styles/find-account.scss +4 -0
- package/src/auth/common/find/styles/layout.scss +19 -0
- package/src/auth/common/find/styles/password.scss +39 -0
- package/src/auth/common/find/styles/result.scss +78 -0
- package/src/auth/common/find/types/forms.ts +30 -0
- package/src/auth/common/find/types/index.ts +121 -0
- package/src/auth/common/find/utils/composeFieldProps.ts +45 -0
- package/src/auth/common/password/constants.ts +19 -0
- package/src/auth/common/password/hooks/useCheckPassword.ts +133 -0
- package/src/auth/common/password/img/check-password.svg +3 -0
- package/src/auth/common/password/markup/PasswordSetField.tsx +250 -0
- package/src/auth/common/password/styles/password-set-field.scss +49 -0
- package/src/auth/common/password/types.ts +142 -0
- package/src/auth/common/password/utils/composePasswordFieldProps.ts +44 -0
- package/src/auth/find-account.ts +28 -0
- package/src/auth/find-id/hooks/index.ts +1 -0
- package/src/auth/find-id/index.scss +1 -0
- package/src/auth/find-id/index.ts +23 -0
- package/src/auth/find-id/markup/StepComplete.tsx +58 -0
- package/src/auth/find-id/markup/StepIdentify.tsx +46 -0
- package/src/auth/find-id/markup/StepVerifyCode.tsx +48 -0
- package/src/auth/find-id/types/index.ts +66 -0
- package/src/auth/find-password/index.scss +1 -0
- package/src/auth/find-password/index.ts +30 -0
- package/src/auth/find-password/markup/StepComplete.tsx +30 -0
- package/src/auth/find-password/markup/StepIdentify.tsx +45 -0
- package/src/auth/find-password/markup/StepResetPassword.tsx +150 -0
- package/src/auth/find-password/markup/StepVerifyCode.tsx +48 -0
- package/src/auth/index.tsx +41 -0
- package/src/{components/auth → auth}/login/markup/Container.tsx +1 -1
- package/src/{components/auth → auth}/login/types/props.ts +1 -1
- package/src/{components/auth → auth}/signup/hooks/useSignupAccountForm.ts +26 -2
- package/src/{components/auth → auth}/signup/hooks/useSignupUserInfoForm.ts +10 -3
- package/src/auth/signup/img/check-agree.svg +3 -0
- package/src/auth/signup/img/chevron-open-detail.svg +3 -0
- package/src/{components/auth → auth}/signup/index.ts +3 -0
- package/src/auth/signup/markup/AccountForm.tsx +113 -0
- package/src/auth/signup/markup/Complete.tsx +59 -0
- package/src/auth/signup/markup/Template.tsx +110 -0
- package/src/{components/auth → auth}/signup/markup/UserInfoForm.tsx +23 -13
- package/src/auth/signup/markup/VerificationForm.tsx +285 -0
- package/src/{components/auth → auth}/signup/markup/index.ts +1 -0
- package/src/auth/signup/styles/signup.scss +187 -0
- package/src/{components/auth → auth}/signup/types/hooks.ts +1 -0
- package/src/{components/auth → auth}/signup/types/props.ts +49 -9
- package/src/auth/signup/utils/getSignupFieldDefaultValue.ts +40 -0
- package/src/index.scss +5 -4
- package/src/index.tsx +3 -3
- package/src/page-frame/mobile/header/PageFrameMobileHeader.tsx +52 -0
- package/src/page-frame/mobile/header/index.ts +4 -0
- package/src/page-frame/mobile/header/page-frame-mobile-header.scss +48 -0
- package/src/page-frame/mobile/img/chevron-backward.svg +3 -0
- package/src/components/auth/index.tsx +0 -20
- package/src/components/auth/signup/markup/AccountForm.tsx +0 -124
- package/src/components/auth/signup/markup/Complete.tsx +0 -61
- package/src/components/auth/signup/markup/VerificationForm.tsx +0 -155
- package/src/components/auth/signup/styles/signup.scss +0 -135
- /package/src/{components/auth → auth/common}/container/AuthContainer.tsx +0 -0
- /package/src/{components/auth → auth/common}/container/index.scss +0 -0
- /package/src/{components/auth → auth/common}/container/types.ts +0 -0
- /package/src/{components/auth → auth}/login/data/valid-options.ts +0 -0
- /package/src/{components/auth → auth}/login/hooks/index.ts +0 -0
- /package/src/{components/auth → auth}/login/hooks/useAuthLoginForm.ts +0 -0
- /package/src/{components/auth → auth}/login/index.scss +0 -0
- /package/src/{components/auth → auth}/login/index.tsx +0 -0
- /package/src/{components/auth → auth}/login/markup/FormField.tsx +0 -0
- /package/src/{components/auth → auth}/login/markup/LinkButtons.tsx +0 -0
- /package/src/{components/auth → auth}/login/styles/login.scss +0 -0
- /package/src/{components/auth → auth}/login/types/form.ts +0 -0
- /package/src/{components/auth → auth}/login/types/hooks.ts +0 -0
- /package/src/{components/auth → auth}/login/types.ts +0 -0
- /package/src/{components/auth → auth}/signup/hooks/index.ts +0 -0
- /package/src/{components/auth → auth}/signup/hooks/useSignupVerificationForm.ts +0 -0
- /package/src/{components/auth → auth}/signup/types/index.ts +0 -0
- /package/src/{components/auth → auth}/signup/utils/composeFieldProps.ts +0 -0
- /package/src/{components/modal → modal}/core/components/Container.tsx +0 -0
- /package/src/{components/modal → modal}/core/components/FooterButtons.tsx +0 -0
- /package/src/{components/modal → modal}/core/components/Provider.tsx +0 -0
- /package/src/{components/modal → modal}/core/components/Root.tsx +0 -0
- /package/src/{components/modal → modal}/core/hooks/useModal.ts +0 -0
- /package/src/{components/modal → modal}/core/jotai/atoms.ts +0 -0
- /package/src/{components/modal → modal}/index.scss +0 -0
- /package/src/{components/modal → modal}/index.tsx +0 -0
- /package/src/{components/modal → modal}/styles/animations.scss +0 -0
- /package/src/{components/modal → modal}/styles/base.scss +0 -0
- /package/src/{components/modal → modal}/styles/container.scss +0 -0
- /package/src/{components/modal → modal}/styles/dimmer.scss +0 -0
- /package/src/{components/modal → modal}/templates/Alert.tsx +0 -0
- /package/src/{components/modal → modal}/templates/Dialog.tsx +0 -0
- /package/src/{components/modal → modal}/types/footer.ts +0 -0
- /package/src/{components/modal → modal}/types/index.ts +0 -0
- /package/src/{components/modal → modal}/types/options.ts +0 -0
- /package/src/{components/modal → modal}/types/state.ts +0 -0
- /package/src/{components/modal → modal}/types/templates.ts +0 -0
- /package/src/{components/page-frame → page-frame}/container/PageFrameContainer.tsx +0 -0
- /package/src/{components/page-frame → page-frame}/container/index.scss +0 -0
- /package/src/{components/page-frame → page-frame}/container/index.tsx +0 -0
- /package/src/{components/page-frame → page-frame}/container/types.ts +0 -0
- /package/src/{components/page-frame → page-frame}/index.tsx +0 -0
- /package/src/{components/page-frame → page-frame}/mobile/PageFrameMobile.tsx +0 -0
- /package/src/{components/page-frame → page-frame}/mobile/index.scss +0 -0
- /package/src/{components/page-frame → page-frame}/mobile/index.tsx +0 -0
- /package/src/{components/page-frame → page-frame}/mobile/types.ts +0 -0
- /package/src/{components/page-frame → page-frame}/navigation/PageFrameNavigation.tsx +0 -0
- /package/src/{components/page-frame → page-frame}/navigation/index.scss +0 -0
- /package/src/{components/page-frame → page-frame}/navigation/index.tsx +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import "../styles/password-set-field.scss";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import {
|
|
6
|
+
useFormContext,
|
|
7
|
+
type FieldPath,
|
|
8
|
+
type FieldValues,
|
|
9
|
+
} from "react-hook-form";
|
|
10
|
+
import {
|
|
11
|
+
PasswordInput,
|
|
12
|
+
type InputPasswordProps,
|
|
13
|
+
} from "@uniai-fe/uds-primitives";
|
|
14
|
+
import CheckPasswordIcon from "../img/check-password.svg";
|
|
15
|
+
import type {
|
|
16
|
+
AuthPasswordHelperItem,
|
|
17
|
+
AuthPasswordHelperState,
|
|
18
|
+
AuthPasswordRule,
|
|
19
|
+
AuthPasswordSetFieldProps,
|
|
20
|
+
} from "../types";
|
|
21
|
+
import { useCheckPassword } from "../hooks/useCheckPassword";
|
|
22
|
+
import { composePasswordFieldProps } from "../utils/composePasswordFieldProps";
|
|
23
|
+
|
|
24
|
+
const PASSWORD_MATCHED_HELPER_ID = "confirm-complete";
|
|
25
|
+
const PASSWORD_MISMATCH_HELPER_ID = "confirm-error";
|
|
26
|
+
const PASSWORD_MATCHED_TEXT = "비밀번호 일치";
|
|
27
|
+
const RHF_CONTEXT_ERROR =
|
|
28
|
+
"AuthPasswordSetField requires react-hook-form context. Wrap it with FormProvider or pass a form prop.";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* RHF 컨텍스트 optional getter
|
|
32
|
+
* @hook
|
|
33
|
+
*/
|
|
34
|
+
function useOptionalFormContext<TValues extends FieldValues>() {
|
|
35
|
+
try {
|
|
36
|
+
return useFormContext<TValues>();
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Helper 아이템 리스트 렌더러
|
|
44
|
+
* @param {{ items: AuthPasswordHelperItem[] }} props helper 컬렉션
|
|
45
|
+
*/
|
|
46
|
+
const HelperList = ({ items }: { items: AuthPasswordHelperItem[] }) =>
|
|
47
|
+
items.length ? (
|
|
48
|
+
<ul className="auth-password-set-helpers" aria-live="polite">
|
|
49
|
+
{items.map(item => (
|
|
50
|
+
<li
|
|
51
|
+
key={item.id}
|
|
52
|
+
className="auth-password-set-helper"
|
|
53
|
+
data-state={item.state}
|
|
54
|
+
>
|
|
55
|
+
<figure className="auth-password-set-helper-icon" aria-hidden="true">
|
|
56
|
+
<CheckPasswordIcon />
|
|
57
|
+
</figure>
|
|
58
|
+
<span>{item.text}</span>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
) : null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Password Input Set; 비밀번호 + 재확인 필드 묶음
|
|
66
|
+
* @component
|
|
67
|
+
* @param {AuthPasswordSetFieldProps} props PasswordSetField 구성 요소
|
|
68
|
+
* @param {UseFormReturn} props.form react-hook-form 객체
|
|
69
|
+
* @param {AuthPasswordFieldProps<InputPasswordProps>} props.passwordField 비밀번호 입력 필드
|
|
70
|
+
* @param {AuthPasswordFieldProps<InputPasswordProps>} props.confirmPasswordField 비밀번호 재확인 필드
|
|
71
|
+
* @param {AuthPasswordRule[]} [props.passwordRules] 비밀번호 규칙 또는 predicate 목록
|
|
72
|
+
* @param {AuthPasswordSetMessages} [props.messages] 검증 메시지
|
|
73
|
+
*/
|
|
74
|
+
export function AuthPasswordSetField<
|
|
75
|
+
TValues extends FieldValues = Record<string, string>,
|
|
76
|
+
>({
|
|
77
|
+
form,
|
|
78
|
+
passwordField,
|
|
79
|
+
confirmPasswordField,
|
|
80
|
+
passwordRules,
|
|
81
|
+
passwordFieldName,
|
|
82
|
+
confirmPasswordFieldName,
|
|
83
|
+
messages,
|
|
84
|
+
}: AuthPasswordSetFieldProps<TValues>) {
|
|
85
|
+
const contextForm = useOptionalFormContext<TValues>();
|
|
86
|
+
const resolvedForm = form ?? contextForm;
|
|
87
|
+
|
|
88
|
+
if (!resolvedForm) {
|
|
89
|
+
throw new Error(RHF_CONTEXT_ERROR);
|
|
90
|
+
}
|
|
91
|
+
const resolvedPasswordFieldName: FieldPath<TValues> =
|
|
92
|
+
passwordFieldName ??
|
|
93
|
+
(passwordField.attr?.name as FieldPath<TValues> | undefined) ??
|
|
94
|
+
("password" as FieldPath<TValues>);
|
|
95
|
+
const resolvedConfirmFieldName: FieldPath<TValues> =
|
|
96
|
+
confirmPasswordFieldName ??
|
|
97
|
+
(confirmPasswordField.attr?.name as FieldPath<TValues> | undefined) ??
|
|
98
|
+
("confirmPassword" as FieldPath<TValues>);
|
|
99
|
+
|
|
100
|
+
const {
|
|
101
|
+
passwordValue = "",
|
|
102
|
+
confirmPasswordValue = "",
|
|
103
|
+
validator: confirmValidator,
|
|
104
|
+
helper: confirmState,
|
|
105
|
+
isMatched,
|
|
106
|
+
} = useCheckPassword<TValues>({
|
|
107
|
+
form: resolvedForm,
|
|
108
|
+
passwordField: resolvedPasswordFieldName,
|
|
109
|
+
confirmField: resolvedConfirmFieldName,
|
|
110
|
+
helperText: confirmPasswordField.helper,
|
|
111
|
+
messages,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const passwordHelperBase: AuthPasswordHelperState = useMemo(
|
|
115
|
+
() => ({
|
|
116
|
+
text:
|
|
117
|
+
(resolvedForm.getFieldState(resolvedPasswordFieldName).error
|
|
118
|
+
?.message as ReactNode | undefined) ?? passwordField.helper,
|
|
119
|
+
state: resolvedForm.getFieldState(resolvedPasswordFieldName).invalid
|
|
120
|
+
? "error"
|
|
121
|
+
: undefined,
|
|
122
|
+
}),
|
|
123
|
+
[passwordField.helper, resolvedForm, resolvedPasswordFieldName],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const evaluatedRules = useMemo(() => {
|
|
127
|
+
if (!passwordRules || passwordRules.length === 0) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return passwordRules.map((rule: AuthPasswordRule) => {
|
|
131
|
+
const nextFulfilled =
|
|
132
|
+
typeof rule.fulfilled === "boolean"
|
|
133
|
+
? rule.fulfilled
|
|
134
|
+
: typeof rule.predicate === "function"
|
|
135
|
+
? rule.predicate(passwordValue ?? "")
|
|
136
|
+
: false;
|
|
137
|
+
return {
|
|
138
|
+
...rule,
|
|
139
|
+
fulfilled: nextFulfilled,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}, [passwordRules, passwordValue]);
|
|
143
|
+
|
|
144
|
+
const hasPasswordValue =
|
|
145
|
+
typeof passwordValue === "string" && passwordValue.trim().length > 0;
|
|
146
|
+
const showRules =
|
|
147
|
+
Boolean(evaluatedRules?.length) &&
|
|
148
|
+
hasPasswordValue &&
|
|
149
|
+
passwordHelperBase.state !== "error";
|
|
150
|
+
|
|
151
|
+
const allRulesFulfilled =
|
|
152
|
+
evaluatedRules && evaluatedRules.length > 0
|
|
153
|
+
? evaluatedRules.every(rule => rule.fulfilled)
|
|
154
|
+
: true;
|
|
155
|
+
const canConfirmPassword =
|
|
156
|
+
hasPasswordValue &&
|
|
157
|
+
passwordHelperBase.state !== "error" &&
|
|
158
|
+
allRulesFulfilled;
|
|
159
|
+
|
|
160
|
+
const passwordRulesItems: AuthPasswordHelperItem[] =
|
|
161
|
+
showRules && evaluatedRules
|
|
162
|
+
? evaluatedRules.map((rule: AuthPasswordRule) => ({
|
|
163
|
+
id: rule.id,
|
|
164
|
+
text: rule.label,
|
|
165
|
+
state: rule.fulfilled ? "complete" : undefined,
|
|
166
|
+
}))
|
|
167
|
+
: [];
|
|
168
|
+
|
|
169
|
+
const confirmHelperItems: AuthPasswordHelperItem[] = [];
|
|
170
|
+
const blockedByPassword =
|
|
171
|
+
!canConfirmPassword && Boolean(confirmPasswordValue?.length);
|
|
172
|
+
|
|
173
|
+
if (blockedByPassword) {
|
|
174
|
+
confirmHelperItems.push({
|
|
175
|
+
id: `${resolvedConfirmFieldName}-blocked`,
|
|
176
|
+
text: messages?.missing ?? "비밀번호를 먼저 입력해 주세요.",
|
|
177
|
+
state: "error",
|
|
178
|
+
});
|
|
179
|
+
} else if (confirmState.state === "error" && confirmState.text) {
|
|
180
|
+
confirmHelperItems.push({
|
|
181
|
+
id: PASSWORD_MISMATCH_HELPER_ID,
|
|
182
|
+
text: confirmState.text,
|
|
183
|
+
state: "error",
|
|
184
|
+
});
|
|
185
|
+
} else if (
|
|
186
|
+
isMatched &&
|
|
187
|
+
passwordHelperBase.state !== "error" &&
|
|
188
|
+
allRulesFulfilled
|
|
189
|
+
) {
|
|
190
|
+
confirmHelperItems.push({
|
|
191
|
+
id: PASSWORD_MATCHED_HELPER_ID,
|
|
192
|
+
text: PASSWORD_MATCHED_TEXT,
|
|
193
|
+
state: "complete",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const passwordHelperNode: AuthPasswordHelperState = passwordRulesItems.length
|
|
198
|
+
? {
|
|
199
|
+
...passwordHelperBase,
|
|
200
|
+
text: <HelperList items={passwordRulesItems} />,
|
|
201
|
+
state: undefined,
|
|
202
|
+
}
|
|
203
|
+
: passwordHelperBase;
|
|
204
|
+
|
|
205
|
+
const confirmHelperProps: AuthPasswordHelperState = confirmHelperItems.length
|
|
206
|
+
? {
|
|
207
|
+
...confirmState,
|
|
208
|
+
text: <HelperList items={confirmHelperItems} />,
|
|
209
|
+
state: confirmState.state === "error" ? "error" : undefined,
|
|
210
|
+
}
|
|
211
|
+
: { ...confirmState, text: undefined, state: undefined };
|
|
212
|
+
|
|
213
|
+
const passwordRegister = resolvedForm.register(resolvedPasswordFieldName);
|
|
214
|
+
const confirmPasswordRegister = resolvedForm.register(
|
|
215
|
+
resolvedConfirmFieldName,
|
|
216
|
+
{
|
|
217
|
+
validate: confirmValidator,
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="auth-password-set">
|
|
223
|
+
<PasswordInput
|
|
224
|
+
{...composePasswordFieldProps<InputPasswordProps>(
|
|
225
|
+
passwordField,
|
|
226
|
+
passwordHelperNode,
|
|
227
|
+
)}
|
|
228
|
+
register={passwordRegister}
|
|
229
|
+
success={null}
|
|
230
|
+
error={null}
|
|
231
|
+
/>
|
|
232
|
+
<PasswordInput
|
|
233
|
+
{...composePasswordFieldProps<InputPasswordProps>(
|
|
234
|
+
confirmPasswordField,
|
|
235
|
+
confirmHelperProps,
|
|
236
|
+
)}
|
|
237
|
+
register={confirmPasswordRegister}
|
|
238
|
+
success={null}
|
|
239
|
+
error={null}
|
|
240
|
+
disabled={
|
|
241
|
+
confirmPasswordField.props?.disabled ??
|
|
242
|
+
confirmPasswordField.attr?.disabled ??
|
|
243
|
+
!canConfirmPassword
|
|
244
|
+
}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default AuthPasswordSetField;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@use "@uniai-fe/uds-foundation/css";
|
|
2
|
+
|
|
3
|
+
.auth-password-set {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--spacing-gap-6, 12px);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.auth-password-set-helpers {
|
|
10
|
+
list-style: none;
|
|
11
|
+
padding: 0;
|
|
12
|
+
margin: var(--spacing-gap-3, 6px) 0 0;
|
|
13
|
+
display: flex;
|
|
14
|
+
gap: var(--spacing-gap-3, 6px);
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.auth-password-set-helper {
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 4px;
|
|
22
|
+
font-size: var(--font-label-small-size);
|
|
23
|
+
line-height: var(--font-label-small-line-height);
|
|
24
|
+
color: var(--color-label-assistive);
|
|
25
|
+
|
|
26
|
+
&[data-state="error"] {
|
|
27
|
+
color: var(--color-error);
|
|
28
|
+
|
|
29
|
+
svg path {
|
|
30
|
+
fill: var(--color-error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&[data-state="complete"] {
|
|
35
|
+
color: var(--color-success);
|
|
36
|
+
|
|
37
|
+
svg path {
|
|
38
|
+
fill: var(--color-success);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.auth-password-set-helper-icon {
|
|
44
|
+
width: 16px;
|
|
45
|
+
height: 16px;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
InputFieldProps,
|
|
4
|
+
InputPasswordProps,
|
|
5
|
+
InputProps,
|
|
6
|
+
} from "@uniai-fe/uds-primitives";
|
|
7
|
+
import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Password 필드 공통 prop; primitives InputFieldProps를 그대로 확장한다.
|
|
11
|
+
* @typedef
|
|
12
|
+
*/
|
|
13
|
+
export type AuthPasswordFieldProps<TProps extends InputProps = InputProps> =
|
|
14
|
+
InputFieldProps<TProps>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Password 규칙 메타데이터
|
|
18
|
+
* @typedef
|
|
19
|
+
*/
|
|
20
|
+
export interface AuthPasswordRule {
|
|
21
|
+
/**
|
|
22
|
+
* 규칙 식별자
|
|
23
|
+
* @type {string}
|
|
24
|
+
*/
|
|
25
|
+
id: string;
|
|
26
|
+
/**
|
|
27
|
+
* 규칙 라벨 또는 설명
|
|
28
|
+
* @type {ReactNode}
|
|
29
|
+
*/
|
|
30
|
+
label: ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* 충족 여부(직접 전달)
|
|
33
|
+
* @type {boolean | undefined}
|
|
34
|
+
*/
|
|
35
|
+
fulfilled?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 규칙을 계산하는 predicate
|
|
38
|
+
* @type {(value: string) => boolean | undefined}
|
|
39
|
+
*/
|
|
40
|
+
predicate?: (value: string) => boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Password helper 상태
|
|
45
|
+
* @typedef
|
|
46
|
+
*/
|
|
47
|
+
export interface AuthPasswordHelperState {
|
|
48
|
+
/**
|
|
49
|
+
* helper 텍스트
|
|
50
|
+
* @type {ReactNode | undefined}
|
|
51
|
+
*/
|
|
52
|
+
text?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* helper state (complete/error 등)
|
|
55
|
+
* @type {AuthPasswordHelperStatus | undefined}
|
|
56
|
+
*/
|
|
57
|
+
state?: AuthPasswordHelperStatus;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Helper 출력 아이템; list 렌더링에 사용
|
|
62
|
+
* @typedef
|
|
63
|
+
*/
|
|
64
|
+
export interface AuthPasswordHelperItem {
|
|
65
|
+
/**
|
|
66
|
+
* helper 아이템 식별자
|
|
67
|
+
* @type {string}
|
|
68
|
+
*/
|
|
69
|
+
id: string;
|
|
70
|
+
/**
|
|
71
|
+
* helper에 노출할 텍스트/노드
|
|
72
|
+
* @type {ReactNode}
|
|
73
|
+
*/
|
|
74
|
+
text: ReactNode;
|
|
75
|
+
/**
|
|
76
|
+
* helper state (success/error 등)
|
|
77
|
+
* @type {AuthPasswordHelperStatus | undefined}
|
|
78
|
+
*/
|
|
79
|
+
state?: AuthPasswordHelperStatus;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Password helper에서 사용하는 상태
|
|
84
|
+
*/
|
|
85
|
+
export type AuthPasswordHelperStatus = "error" | "complete";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* PasswordSetField 컴포넌트 props
|
|
89
|
+
* @typedef
|
|
90
|
+
*/
|
|
91
|
+
export interface AuthPasswordSetMessages {
|
|
92
|
+
/**
|
|
93
|
+
* 비밀번호 미입력 시 메시지
|
|
94
|
+
* @type {string | undefined}
|
|
95
|
+
*/
|
|
96
|
+
missing?: string;
|
|
97
|
+
/**
|
|
98
|
+
* 비밀번호 불일치 메시지
|
|
99
|
+
* @type {string | undefined}
|
|
100
|
+
*/
|
|
101
|
+
mismatch?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface AuthPasswordSetFieldProps<
|
|
105
|
+
TValues extends FieldValues = Record<string, string>,
|
|
106
|
+
> {
|
|
107
|
+
/**
|
|
108
|
+
* react-hook-form form 객체
|
|
109
|
+
* @type {UseFormReturn<TValues> | undefined}
|
|
110
|
+
*/
|
|
111
|
+
form?: UseFormReturn<TValues>;
|
|
112
|
+
/**
|
|
113
|
+
* 비밀번호 입력 필드 설정
|
|
114
|
+
* @type {AuthPasswordFieldProps<InputPasswordProps>}
|
|
115
|
+
*/
|
|
116
|
+
passwordField: AuthPasswordFieldProps<InputPasswordProps>;
|
|
117
|
+
/**
|
|
118
|
+
* 비밀번호 재확인 필드 설정
|
|
119
|
+
* @type {AuthPasswordFieldProps<InputPasswordProps>}
|
|
120
|
+
*/
|
|
121
|
+
confirmPasswordField: AuthPasswordFieldProps<InputPasswordProps>;
|
|
122
|
+
/**
|
|
123
|
+
* 비밀번호 필드명(미지정 시 attr.name 또는 password)
|
|
124
|
+
* @type {FieldPath<TValues> | undefined}
|
|
125
|
+
*/
|
|
126
|
+
passwordFieldName?: FieldPath<TValues>;
|
|
127
|
+
/**
|
|
128
|
+
* 재확인 필드명(미지정 시 attr.name 또는 confirmPassword)
|
|
129
|
+
* @type {FieldPath<TValues> | undefined}
|
|
130
|
+
*/
|
|
131
|
+
confirmPasswordFieldName?: FieldPath<TValues>;
|
|
132
|
+
/**
|
|
133
|
+
* 비밀번호 규칙 목록
|
|
134
|
+
* @type {AuthPasswordRule[] | undefined}
|
|
135
|
+
*/
|
|
136
|
+
passwordRules?: AuthPasswordRule[];
|
|
137
|
+
/**
|
|
138
|
+
* 검증 메시지 override
|
|
139
|
+
* @type {AuthPasswordSetMessages | undefined}
|
|
140
|
+
*/
|
|
141
|
+
messages?: AuthPasswordSetMessages;
|
|
142
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { InputProps } from "@uniai-fe/uds-primitives";
|
|
2
|
+
import type { AuthPasswordFieldProps, AuthPasswordHelperState } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Password 전용 필드 구성; attr/props를 합쳐 helper/label/state를 통일한다.
|
|
6
|
+
* @util
|
|
7
|
+
* @param {AuthPasswordFieldProps<TProps>} config 입력 필드 설정값
|
|
8
|
+
* @param {AuthPasswordHelperState} [helper] 훅에서 계산한 helper 상태
|
|
9
|
+
* @returns {TProps} Input 컴포넌트에 그대로 전달할 props
|
|
10
|
+
*/
|
|
11
|
+
export const composePasswordFieldProps = <TProps extends InputProps>(
|
|
12
|
+
config: AuthPasswordFieldProps<TProps>,
|
|
13
|
+
helper?: AuthPasswordHelperState,
|
|
14
|
+
): TProps => {
|
|
15
|
+
const baseProps = {
|
|
16
|
+
...(config.attr ?? {}),
|
|
17
|
+
...(config.props ?? ({} as TProps)),
|
|
18
|
+
} as TProps;
|
|
19
|
+
const mergedLabelProps = config.labelClassName
|
|
20
|
+
? {
|
|
21
|
+
...baseProps.labelProps,
|
|
22
|
+
className: config.labelClassName,
|
|
23
|
+
}
|
|
24
|
+
: baseProps.labelProps;
|
|
25
|
+
const mergedHelperProps = config.helperClassName
|
|
26
|
+
? {
|
|
27
|
+
...baseProps.helperProps,
|
|
28
|
+
className: config.helperClassName,
|
|
29
|
+
}
|
|
30
|
+
: baseProps.helperProps;
|
|
31
|
+
|
|
32
|
+
const helperDrivenState = helper?.state === "error" ? "error" : undefined;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...baseProps,
|
|
36
|
+
label: config.label ?? baseProps.label,
|
|
37
|
+
helper: helper?.text ?? config.helper ?? baseProps.helper,
|
|
38
|
+
state: helperDrivenState ?? baseProps.state,
|
|
39
|
+
block: config.block ?? baseProps.block ?? true,
|
|
40
|
+
className: config.className ?? baseProps.className,
|
|
41
|
+
labelProps: mergedLabelProps,
|
|
42
|
+
helperProps: mergedHelperProps,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FindAccountInfoScreen,
|
|
3
|
+
FindAccountCodeScreen,
|
|
4
|
+
FindAccountIdComplete,
|
|
5
|
+
useFindAccountForm,
|
|
6
|
+
} from "./find-id";
|
|
7
|
+
import {
|
|
8
|
+
FindAccountPasswordForm,
|
|
9
|
+
FindAccountPasswordComplete,
|
|
10
|
+
} from "./find-password";
|
|
11
|
+
|
|
12
|
+
export type * from "./find-id/types";
|
|
13
|
+
export { useFindAccountForm };
|
|
14
|
+
export {
|
|
15
|
+
FindAccountInfoScreen,
|
|
16
|
+
FindAccountCodeScreen,
|
|
17
|
+
FindAccountIdComplete,
|
|
18
|
+
FindAccountPasswordForm,
|
|
19
|
+
FindAccountPasswordComplete,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const FindAccount = {
|
|
23
|
+
Info: FindAccountInfoScreen,
|
|
24
|
+
Code: FindAccountCodeScreen,
|
|
25
|
+
IdComplete: FindAccountIdComplete,
|
|
26
|
+
PasswordForm: FindAccountPasswordForm,
|
|
27
|
+
PasswordComplete: FindAccountPasswordComplete,
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useFindAccountForm } from "../../common/find/hooks/useFindAccountForm";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "../common/find/styles/find-account.scss";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "./index.scss";
|
|
2
|
+
import FindIdStepIdentify from "./markup/StepIdentify";
|
|
3
|
+
import FindIdStepVerifyCode from "./markup/StepVerifyCode";
|
|
4
|
+
import FindIdStepComplete from "./markup/StepComplete";
|
|
5
|
+
|
|
6
|
+
export type * from "./types";
|
|
7
|
+
export * from "./hooks";
|
|
8
|
+
|
|
9
|
+
export const FindAccountInfoScreen = FindIdStepIdentify;
|
|
10
|
+
export const FindAccountCodeScreen = FindIdStepVerifyCode;
|
|
11
|
+
export const FindAccountIdComplete = FindIdStepComplete;
|
|
12
|
+
|
|
13
|
+
export { FindIdStepIdentify, FindIdStepVerifyCode, FindIdStepComplete };
|
|
14
|
+
|
|
15
|
+
export const FindId = {
|
|
16
|
+
StepIdentify: FindIdStepIdentify,
|
|
17
|
+
StepVerifyCode: FindIdStepVerifyCode,
|
|
18
|
+
StepComplete: FindIdStepComplete,
|
|
19
|
+
// Backwards compatibility
|
|
20
|
+
Info: FindAccountInfoScreen,
|
|
21
|
+
Code: FindAccountCodeScreen,
|
|
22
|
+
IdComplete: FindAccountIdComplete,
|
|
23
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import type { FindAccountIdCompleteProps } from "../types";
|
|
3
|
+
import AuthCompleteTemplate from "../../common/complete/Template";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TITLE = "아이디 찾기 완료";
|
|
6
|
+
const DEFAULT_ID_LABEL = "아이디";
|
|
7
|
+
const DEFAULT_REGISTERED_LABEL = "가입일";
|
|
8
|
+
const DEFAULT_DESCRIPTION = "입력하신 정보로 계정을 찾았어요.";
|
|
9
|
+
const DEFAULT_CTA_LABEL = "로그인하기";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 아이디 찾기 완료 화면
|
|
13
|
+
* @component
|
|
14
|
+
*/
|
|
15
|
+
export default function FindIdStepComplete({
|
|
16
|
+
className,
|
|
17
|
+
title,
|
|
18
|
+
summary,
|
|
19
|
+
description,
|
|
20
|
+
cta,
|
|
21
|
+
}: FindAccountIdCompleteProps) {
|
|
22
|
+
return (
|
|
23
|
+
<AuthCompleteTemplate
|
|
24
|
+
className={clsx("auth-find-account-container", className)}
|
|
25
|
+
title={title ?? DEFAULT_TITLE}
|
|
26
|
+
description={description ?? DEFAULT_DESCRIPTION}
|
|
27
|
+
cta={{
|
|
28
|
+
label: cta?.label ?? DEFAULT_CTA_LABEL,
|
|
29
|
+
buttonProps: cta?.buttonProps,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<section className="auth-find-account-id-summary">
|
|
33
|
+
<div className="auth-find-account-id-summary-card">
|
|
34
|
+
<dl className="auth-find-account-id-summary-row">
|
|
35
|
+
<dt className="auth-find-account-id-summary-text">
|
|
36
|
+
<span>{summary.userIdLabel ?? DEFAULT_ID_LABEL}</span>
|
|
37
|
+
</dt>
|
|
38
|
+
<dd className="auth-find-account-id-summary-text">
|
|
39
|
+
<span>{summary.userId}</span>
|
|
40
|
+
</dd>
|
|
41
|
+
</dl>
|
|
42
|
+
{summary.registeredAt ? (
|
|
43
|
+
<dl className="auth-find-account-id-summary-row">
|
|
44
|
+
<dt className="auth-find-account-id-summary-text">
|
|
45
|
+
<span>
|
|
46
|
+
{summary.registeredAtLabel ?? DEFAULT_REGISTERED_LABEL}
|
|
47
|
+
</span>
|
|
48
|
+
</dt>
|
|
49
|
+
<dd className="auth-find-account-id-summary-text">
|
|
50
|
+
<span>{summary.registeredAt}</span>
|
|
51
|
+
</dd>
|
|
52
|
+
</dl>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
</AuthCompleteTemplate>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import FindAccountInfoStep from "../../common/find/markup/InfoStep";
|
|
3
|
+
import type { FindAccountInfoStepProps } from "../../common/find/types";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_NAVIGATION = {
|
|
6
|
+
title: "아이디 찾기",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DEFAULT_HEADLINE = {
|
|
10
|
+
progress: { total: 2, current: 1 },
|
|
11
|
+
title: (
|
|
12
|
+
<>
|
|
13
|
+
가입한 이름과 메일 주소를
|
|
14
|
+
<br />
|
|
15
|
+
입력해 주세요
|
|
16
|
+
</>
|
|
17
|
+
),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CTA_LABEL = "인증코드 요청";
|
|
21
|
+
|
|
22
|
+
export type FindIdStepIdentifyProps = FindAccountInfoStepProps;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find ID Step1 — 이름/메일 입력
|
|
26
|
+
* @component
|
|
27
|
+
*/
|
|
28
|
+
export default function FindIdStepIdentify({
|
|
29
|
+
navigation,
|
|
30
|
+
headline,
|
|
31
|
+
cta,
|
|
32
|
+
...rest
|
|
33
|
+
}: FindIdStepIdentifyProps) {
|
|
34
|
+
const resolvedNavigation = navigation ?? DEFAULT_NAVIGATION;
|
|
35
|
+
const resolvedHeadline = headline ?? DEFAULT_HEADLINE;
|
|
36
|
+
const resolvedCTA = cta ?? { label: DEFAULT_CTA_LABEL as ReactNode };
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<FindAccountInfoStep
|
|
40
|
+
{...rest}
|
|
41
|
+
navigation={resolvedNavigation}
|
|
42
|
+
headline={resolvedHeadline}
|
|
43
|
+
cta={resolvedCTA}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import FindAccountCodeStep from "../../common/find/markup/CodeStep";
|
|
2
|
+
import type { FindAccountCodeStepProps } from "../../common/find/types";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_NAVIGATION = {
|
|
5
|
+
title: "아이디 찾기",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const DEFAULT_HEADLINE = {
|
|
9
|
+
progress: { total: 2, current: 2 },
|
|
10
|
+
title: (
|
|
11
|
+
<>
|
|
12
|
+
메일 주소로
|
|
13
|
+
<br />
|
|
14
|
+
인증을 진행해 주세요
|
|
15
|
+
</>
|
|
16
|
+
),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEFAULT_EMAIL_LABEL = "메일";
|
|
20
|
+
|
|
21
|
+
export type FindIdStepVerifyCodeProps = FindAccountCodeStepProps;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find ID Step2 — 인증코드 입력
|
|
25
|
+
* @component
|
|
26
|
+
*/
|
|
27
|
+
export default function FindIdStepVerifyCode({
|
|
28
|
+
navigation,
|
|
29
|
+
headline,
|
|
30
|
+
emailDisplay,
|
|
31
|
+
...rest
|
|
32
|
+
}: FindIdStepVerifyCodeProps) {
|
|
33
|
+
const resolvedNavigation = navigation ?? DEFAULT_NAVIGATION;
|
|
34
|
+
const resolvedHeadline = headline ?? DEFAULT_HEADLINE;
|
|
35
|
+
const resolvedEmailDisplay = {
|
|
36
|
+
label: DEFAULT_EMAIL_LABEL,
|
|
37
|
+
...emailDisplay,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<FindAccountCodeStep
|
|
42
|
+
{...rest}
|
|
43
|
+
navigation={resolvedNavigation}
|
|
44
|
+
headline={resolvedHeadline}
|
|
45
|
+
emailDisplay={resolvedEmailDisplay}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|