@uniai-fe/uds-templates 0.0.10 → 0.0.12
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 +88 -1
- package/dist/styles.css +2051 -2266
- package/package.json +5 -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/index.tsx +1 -7
- package/src/{components/auth → auth}/login/markup/Container.tsx +1 -1
- package/src/{components/auth → auth}/login/markup/FormField.tsx +2 -2
- package/src/{components/auth → auth}/login/types/props.ts +13 -13
- package/src/auth/login/types.ts +2 -0
- package/src/auth/signup/hooks/index.ts +3 -0
- package/src/auth/signup/hooks/useSignupAccountForm.ts +101 -0
- package/src/auth/signup/hooks/useSignupUserInfoForm.ts +88 -0
- package/src/auth/signup/hooks/useSignupVerificationForm.ts +77 -0
- package/src/auth/signup/img/check-agree.svg +3 -0
- package/src/auth/signup/img/chevron-open-detail.svg +3 -0
- package/src/auth/signup/index.ts +27 -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/auth/signup/markup/UserInfoForm.tsx +107 -0
- package/src/auth/signup/markup/VerificationForm.tsx +285 -0
- package/src/auth/signup/markup/index.ts +5 -0
- package/src/auth/signup/styles/signup.scss +187 -0
- package/src/auth/signup/types/hooks.ts +86 -0
- package/src/auth/signup/types/index.ts +2 -0
- package/src/auth/signup/types/props.ts +145 -0
- package/src/auth/signup/utils/composeFieldProps.ts +50 -0
- package/src/auth/signup/utils/getSignupFieldDefaultValue.ts +40 -0
- package/src/index.scss +5 -3
- package/src/index.tsx +3 -2
- package/src/modal/core/components/Container.tsx +41 -0
- package/src/modal/core/components/FooterButtons.tsx +132 -0
- package/src/modal/core/components/Provider.tsx +28 -0
- package/src/modal/core/components/Root.tsx +93 -0
- package/src/modal/core/hooks/useModal.ts +136 -0
- package/src/modal/core/jotai/atoms.ts +10 -0
- package/src/modal/index.scss +4 -0
- package/src/modal/index.tsx +16 -0
- package/src/modal/styles/animations.scss +24 -0
- package/src/modal/styles/base.scss +45 -0
- package/src/modal/styles/container.scss +138 -0
- package/src/modal/styles/dimmer.scss +23 -0
- package/src/modal/templates/Alert.tsx +104 -0
- package/src/modal/templates/Dialog.tsx +112 -0
- package/src/modal/types/footer.ts +36 -0
- package/src/modal/types/index.ts +21 -0
- package/src/modal/types/options.ts +6 -0
- package/src/modal/types/state.ts +31 -0
- package/src/modal/types/templates.ts +32 -0
- 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/login/types.ts +0 -2
- /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/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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-templates",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
|
-
"packageManager": "pnpm@10.26.
|
|
15
|
+
"packageManager": "pnpm@10.26.2",
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=24",
|
|
18
18
|
"pnpm": ">=10"
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"react": ">= 19",
|
|
54
54
|
"react-dom": ">= 19",
|
|
55
55
|
"react-hook-form": ">= 7",
|
|
56
|
-
"next": "^15"
|
|
56
|
+
"next": "^15",
|
|
57
|
+
"jotai": ">= 2"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"clsx": "^2.1.1",
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
"next": "^15.5.9",
|
|
74
75
|
"prettier": "^3.7.4",
|
|
75
76
|
"react-hook-form": "^7.69.0",
|
|
77
|
+
"jotai": "^2.16.1",
|
|
76
78
|
"sass": "^1.97.1",
|
|
77
79
|
"typescript": "~5.9.3"
|
|
78
80
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import "./index.scss";
|
|
4
|
+
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import { Button } from "@uniai-fe/uds-primitives";
|
|
7
|
+
import { AuthContainer } from "../container";
|
|
8
|
+
import CircleCheckCompleteIcon from "./img/circle-check-complete.svg";
|
|
9
|
+
import type { AuthCompleteTemplateProps } from "./types";
|
|
10
|
+
|
|
11
|
+
export default function AuthCompleteTemplate({
|
|
12
|
+
className,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
children,
|
|
16
|
+
cta,
|
|
17
|
+
}: AuthCompleteTemplateProps) {
|
|
18
|
+
const confirmButtonProps = {
|
|
19
|
+
type: "button" as const,
|
|
20
|
+
block: true,
|
|
21
|
+
...cta?.buttonProps,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<AuthContainer
|
|
26
|
+
className={clsx("auth-complete-container", className)}
|
|
27
|
+
footer={
|
|
28
|
+
<Button.Default {...confirmButtonProps}>
|
|
29
|
+
{cta?.label ?? "확인"}
|
|
30
|
+
</Button.Default>
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
<div className="auth-complete-wrapper">
|
|
34
|
+
<figure className="auth-complete-icon" aria-hidden="true">
|
|
35
|
+
<CircleCheckCompleteIcon />
|
|
36
|
+
</figure>
|
|
37
|
+
<h2 className="auth-complete-title">{title}</h2>
|
|
38
|
+
{description ? (
|
|
39
|
+
<p className="auth-complete-description">{description}</p>
|
|
40
|
+
) : null}
|
|
41
|
+
{children ? (
|
|
42
|
+
<div className="auth-complete-contents">{children}</div>
|
|
43
|
+
) : null}
|
|
44
|
+
</div>
|
|
45
|
+
</AuthContainer>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="30" cy="30" r="25" fill="#1A6AFF"/>
|
|
3
|
+
<path d="M21 30.3137L27.364 36.6777L38.6777 25.364" stroke="white" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.auth-complete-container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.auth-complete-wrapper {
|
|
6
|
+
text-align: center;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.auth-complete-icon {
|
|
10
|
+
width: 60px;
|
|
11
|
+
height: 60px;
|
|
12
|
+
margin: 0 auto 12px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.auth-complete-icon svg {
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
display: block;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.auth-complete-title {
|
|
22
|
+
font-size: var(--font-heading-medium-size, 24px);
|
|
23
|
+
font-weight: var(--font-heading-medium-weight, 600);
|
|
24
|
+
color: var(--color-cool-gray-20);
|
|
25
|
+
line-height: var(--font-heading-medium-line-height, 1.4em);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.auth-complete-description {
|
|
29
|
+
margin-top: 4px;
|
|
30
|
+
font-size: var(--font-caption-large-size, 12px);
|
|
31
|
+
color: var(--color-label-standard);
|
|
32
|
+
line-height: var(--font-caption-large-line-height, 1.5em);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.auth-complete-contents {
|
|
36
|
+
width: 100%;
|
|
37
|
+
margin-top: var(--spacing-padding-4, 16px);
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ButtonProps } from "@uniai-fe/uds-primitives";
|
|
3
|
+
|
|
4
|
+
export type AuthCompleteCTA = {
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
buttonProps?: Omit<ButtonProps, "children">;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type AuthCompleteTemplateProps = {
|
|
10
|
+
className?: string;
|
|
11
|
+
title: ReactNode;
|
|
12
|
+
description?: ReactNode;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
cta?: AuthCompleteCTA;
|
|
15
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { PaginationCarousel } from "@uniai-fe/uds-primitives";
|
|
4
|
+
import { PageFrameMobileHeader } from "../../../../page-frame/mobile/header";
|
|
5
|
+
|
|
6
|
+
import "./stage-header.scss";
|
|
7
|
+
|
|
8
|
+
export interface StageHeaderIndicatorProps {
|
|
9
|
+
total: number;
|
|
10
|
+
current: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StageHeaderProps {
|
|
14
|
+
className?: string;
|
|
15
|
+
navigationTitle: ReactNode;
|
|
16
|
+
headline: ReactNode;
|
|
17
|
+
description?: ReactNode;
|
|
18
|
+
backIcon?: ReactNode;
|
|
19
|
+
onBack?: () => void;
|
|
20
|
+
indicator?: StageHeaderIndicatorProps;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Page Frame용 optional header 컴포넌트; 모바일 서비스 공통 헤더 패턴을 제공한다.
|
|
25
|
+
* @component
|
|
26
|
+
*/
|
|
27
|
+
export function AuthStageHeader({
|
|
28
|
+
className,
|
|
29
|
+
navigationTitle,
|
|
30
|
+
headline,
|
|
31
|
+
description,
|
|
32
|
+
backIcon,
|
|
33
|
+
onBack,
|
|
34
|
+
indicator,
|
|
35
|
+
}: StageHeaderProps) {
|
|
36
|
+
return (
|
|
37
|
+
<div className={clsx("auth-stage-header", className)}>
|
|
38
|
+
<PageFrameMobileHeader
|
|
39
|
+
title={navigationTitle}
|
|
40
|
+
backIcon={backIcon}
|
|
41
|
+
onBack={onBack}
|
|
42
|
+
/>
|
|
43
|
+
{indicator ? (
|
|
44
|
+
<div className="auth-stage-step">
|
|
45
|
+
<PaginationCarousel
|
|
46
|
+
className="auth-stage-step-pagination"
|
|
47
|
+
aria-label="진행 단계"
|
|
48
|
+
total={indicator.total}
|
|
49
|
+
current={indicator.current}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
) : null}
|
|
53
|
+
<div className="auth-stage-headline">
|
|
54
|
+
<p className="auth-stage-headline-text">{headline}</p>
|
|
55
|
+
{description ? (
|
|
56
|
+
<p className="auth-stage-headline-description">{description}</p>
|
|
57
|
+
) : null}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.auth-stage-header {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--spacing-padding-5, 20px);
|
|
5
|
+
padding: 0 var(--spacing-padding-1, 4px);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.auth-stage-step {
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: flex-start;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.auth-stage-step-pagination {
|
|
14
|
+
--pagination-carousel-gap: 6px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.auth-stage-headline {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: var(--spacing-padding-1, 4px);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.auth-stage-headline-text {
|
|
24
|
+
margin: 0;
|
|
25
|
+
font-size: 24px;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
line-height: 1.4;
|
|
28
|
+
letter-spacing: 0.2px;
|
|
29
|
+
color: var(--color-label-standard);
|
|
30
|
+
font-family:
|
|
31
|
+
"Pretendard JP Variable",
|
|
32
|
+
"Pretendard",
|
|
33
|
+
system-ui,
|
|
34
|
+
-apple-system,
|
|
35
|
+
BlinkMacSystemFont,
|
|
36
|
+
sans-serif;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.auth-stage-headline-description {
|
|
40
|
+
margin: 0;
|
|
41
|
+
font-size: 14px;
|
|
42
|
+
line-height: 1.4;
|
|
43
|
+
color: var(--color-label-assistive);
|
|
44
|
+
font-family:
|
|
45
|
+
"Pretendard",
|
|
46
|
+
system-ui,
|
|
47
|
+
-apple-system,
|
|
48
|
+
BlinkMacSystemFont,
|
|
49
|
+
sans-serif;
|
|
50
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useWatch, type Path } from "react-hook-form";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import type { InputFieldProps } from "@uniai-fe/uds-primitives";
|
|
5
|
+
import type {
|
|
6
|
+
UseFindAccountFormOptions,
|
|
7
|
+
UseFindAccountFormReturn,
|
|
8
|
+
} from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find Account 단계 공통 RHF 훅.
|
|
12
|
+
* @hook
|
|
13
|
+
*/
|
|
14
|
+
export function useFindAccountForm<
|
|
15
|
+
TFields extends Record<string, InputFieldProps>,
|
|
16
|
+
TValues extends Record<string, unknown>,
|
|
17
|
+
>({
|
|
18
|
+
fields,
|
|
19
|
+
form,
|
|
20
|
+
onSubmit,
|
|
21
|
+
isSubmittable,
|
|
22
|
+
}: UseFindAccountFormOptions<TFields, TValues>): UseFindAccountFormReturn<
|
|
23
|
+
TFields,
|
|
24
|
+
TValues
|
|
25
|
+
> {
|
|
26
|
+
const values = useWatch({
|
|
27
|
+
control: form.control,
|
|
28
|
+
}) as TValues | undefined;
|
|
29
|
+
|
|
30
|
+
const register = useMemo(() => {
|
|
31
|
+
return (Object.keys(fields) as Array<keyof TFields>).reduce(
|
|
32
|
+
(acc, fieldKey) => {
|
|
33
|
+
const config = fields[fieldKey];
|
|
34
|
+
const fieldName = (config.attr?.name ??
|
|
35
|
+
String(fieldKey)) as Path<TValues>;
|
|
36
|
+
acc[fieldKey] = form.register(fieldName);
|
|
37
|
+
return acc;
|
|
38
|
+
},
|
|
39
|
+
{} as UseFindAccountFormReturn<TFields, TValues>["register"],
|
|
40
|
+
);
|
|
41
|
+
}, [fields, form]);
|
|
42
|
+
|
|
43
|
+
const helpers = useMemo(() => {
|
|
44
|
+
return (Object.keys(fields) as Array<keyof TFields>).reduce(
|
|
45
|
+
(acc, fieldKey) => {
|
|
46
|
+
const config = fields[fieldKey];
|
|
47
|
+
const fieldName = (config.attr?.name ??
|
|
48
|
+
String(fieldKey)) as Path<TValues>;
|
|
49
|
+
const state = form.getFieldState(fieldName);
|
|
50
|
+
acc[fieldKey] = {
|
|
51
|
+
text: state.error?.message ?? config.helper,
|
|
52
|
+
state: state.invalid ? "error" : undefined,
|
|
53
|
+
};
|
|
54
|
+
return acc;
|
|
55
|
+
},
|
|
56
|
+
{} as UseFindAccountFormReturn<TFields, TValues>["helpers"],
|
|
57
|
+
);
|
|
58
|
+
}, [fields, form]);
|
|
59
|
+
|
|
60
|
+
const defaultFilled =
|
|
61
|
+
values &&
|
|
62
|
+
Object.values(values).every(value =>
|
|
63
|
+
typeof value === "string" ? value.trim().length > 0 : Boolean(value),
|
|
64
|
+
);
|
|
65
|
+
const resolvedFilled = isSubmittable
|
|
66
|
+
? isSubmittable(values)
|
|
67
|
+
: Boolean(defaultFilled);
|
|
68
|
+
const disabled = form.formState.isSubmitting || !resolvedFilled;
|
|
69
|
+
|
|
70
|
+
const handleSubmit: React.FormEventHandler<HTMLFormElement> =
|
|
71
|
+
form.handleSubmit(onSubmit);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
register,
|
|
75
|
+
helpers,
|
|
76
|
+
disabled,
|
|
77
|
+
onSubmit: handleSubmit,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import type { MouseEvent } from "react";
|
|
4
|
+
import { useForm, useWatch } from "react-hook-form";
|
|
5
|
+
import { AuthContainer } from "../../container";
|
|
6
|
+
import { Button, EmailInput } from "@uniai-fe/uds-primitives";
|
|
7
|
+
import type { ButtonProps } from "@uniai-fe/uds-primitives";
|
|
8
|
+
import type { FindAccountCodeStepProps, FindAccountCodeValues } from "../types";
|
|
9
|
+
import FindAccountHeader from "./Header";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_RESEND_LABEL = "인증코드 재요청";
|
|
12
|
+
const DEFAULT_EXTEND_LABEL = "시간연장";
|
|
13
|
+
const DEFAULT_CODE_LABEL = "인증코드 입력";
|
|
14
|
+
const DEFAULT_EMAIL_LABEL = "메일";
|
|
15
|
+
const DEFAULT_SUBMIT_LABEL = "완료";
|
|
16
|
+
const DEFAULT_CODE_LENGTH = 6;
|
|
17
|
+
type ButtonComponentEvent = Parameters<NonNullable<ButtonProps["onClick"]>>[0];
|
|
18
|
+
type UtilityClickEvent =
|
|
19
|
+
| MouseEvent<HTMLButtonElement>
|
|
20
|
+
| MouseEvent<HTMLAnchorElement>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 인증코드 입력 Step
|
|
24
|
+
* @component
|
|
25
|
+
*/
|
|
26
|
+
export function FindAccountCodeStep({
|
|
27
|
+
className,
|
|
28
|
+
header,
|
|
29
|
+
footer,
|
|
30
|
+
emailDisplay,
|
|
31
|
+
timer,
|
|
32
|
+
fieldOptions,
|
|
33
|
+
cta,
|
|
34
|
+
navigation,
|
|
35
|
+
headline,
|
|
36
|
+
}: FindAccountCodeStepProps) {
|
|
37
|
+
const { fields, formAttr, onSubmit, isSubmittable } = fieldOptions;
|
|
38
|
+
const codeFieldName = fields.code.attr?.name ?? "code";
|
|
39
|
+
const resolvedCodeLength = fields.code.length ?? DEFAULT_CODE_LENGTH;
|
|
40
|
+
|
|
41
|
+
const defaultValues = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
({
|
|
44
|
+
[codeFieldName]: "",
|
|
45
|
+
}) as FindAccountCodeValues,
|
|
46
|
+
[codeFieldName],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const form = useForm<FindAccountCodeValues>({
|
|
50
|
+
mode: "onChange",
|
|
51
|
+
defaultValues,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const values = useWatch({
|
|
55
|
+
control: form.control,
|
|
56
|
+
}) as FindAccountCodeValues | undefined;
|
|
57
|
+
|
|
58
|
+
const submitButtonProps = {
|
|
59
|
+
type: "submit" as const,
|
|
60
|
+
scale: "solid-xlarge" as const,
|
|
61
|
+
priority: "primary" as const,
|
|
62
|
+
block: true,
|
|
63
|
+
...cta?.buttonProps,
|
|
64
|
+
};
|
|
65
|
+
const resolvedEmailValue = String(emailDisplay.value ?? "");
|
|
66
|
+
const codeInputProps = {
|
|
67
|
+
...(fields.code.props ?? {}),
|
|
68
|
+
register: form.register(codeFieldName),
|
|
69
|
+
};
|
|
70
|
+
const countdownActionLabel = timer?.extend
|
|
71
|
+
? (timer.extend.label ?? DEFAULT_EXTEND_LABEL)
|
|
72
|
+
: undefined;
|
|
73
|
+
const countdownActionHandler = timer?.extend?.buttonProps?.onClick;
|
|
74
|
+
const countdownActionDisabled = timer?.extend?.buttonProps?.disabled;
|
|
75
|
+
const resendButtonLabel = emailDisplay.resend?.label ?? DEFAULT_RESEND_LABEL;
|
|
76
|
+
const resendButtonHandler = emailDisplay.resend?.buttonProps?.onClick;
|
|
77
|
+
const resendButtonDisabled = emailDisplay.resend?.buttonProps?.disabled;
|
|
78
|
+
const handleResendButtonClick = (event?: UtilityClickEvent) => {
|
|
79
|
+
if (!resendButtonHandler || !event) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
resendButtonHandler(event as ButtonComponentEvent);
|
|
83
|
+
};
|
|
84
|
+
const handleCountdownActionClick = (event?: UtilityClickEvent) => {
|
|
85
|
+
if (!countdownActionHandler || !event) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
countdownActionHandler(event as ButtonComponentEvent);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const resolvedHeader =
|
|
92
|
+
header ??
|
|
93
|
+
(navigation || headline ? (
|
|
94
|
+
<FindAccountHeader navigation={navigation} headline={headline} />
|
|
95
|
+
) : undefined);
|
|
96
|
+
|
|
97
|
+
const resolvedValues = values ?? form.getValues();
|
|
98
|
+
const codeValue =
|
|
99
|
+
resolvedValues && typeof resolvedValues[codeFieldName] === "string"
|
|
100
|
+
? resolvedValues[codeFieldName]
|
|
101
|
+
: "";
|
|
102
|
+
const normalizedCodeValue = codeValue.replace(/\D/g, "");
|
|
103
|
+
const defaultFilled = normalizedCodeValue.length === resolvedCodeLength;
|
|
104
|
+
const resolvedFilled = isSubmittable
|
|
105
|
+
? isSubmittable(resolvedValues)
|
|
106
|
+
: defaultFilled;
|
|
107
|
+
const disabled = form.formState.isSubmitting || !resolvedFilled;
|
|
108
|
+
|
|
109
|
+
const fieldState = form.getFieldState(codeFieldName);
|
|
110
|
+
const codeHelper = fieldState.error?.message ?? fields.code.helper;
|
|
111
|
+
const codeState = fieldState.invalid ? "error" : undefined;
|
|
112
|
+
|
|
113
|
+
const handleSubmit = form.handleSubmit(onSubmit);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<AuthContainer
|
|
117
|
+
className={clsx("auth-find-account-container", className)}
|
|
118
|
+
header={resolvedHeader}
|
|
119
|
+
footer={footer}
|
|
120
|
+
>
|
|
121
|
+
<form
|
|
122
|
+
className="auth-find-account-form auth-find-account-form--code"
|
|
123
|
+
{...formAttr}
|
|
124
|
+
onSubmit={handleSubmit}
|
|
125
|
+
>
|
|
126
|
+
<div className="auth-find-account-fields">
|
|
127
|
+
<EmailInput
|
|
128
|
+
priority="secondary"
|
|
129
|
+
label={emailDisplay.label ?? DEFAULT_EMAIL_LABEL}
|
|
130
|
+
value={resolvedEmailValue}
|
|
131
|
+
readOnly
|
|
132
|
+
helper={emailDisplay.helper}
|
|
133
|
+
onRequestCode={
|
|
134
|
+
resendButtonHandler ? handleResendButtonClick : undefined
|
|
135
|
+
}
|
|
136
|
+
requestButtonLabel={resendButtonLabel}
|
|
137
|
+
requestButtonDisabled={resendButtonDisabled}
|
|
138
|
+
codeVisible
|
|
139
|
+
codeLength={resolvedCodeLength}
|
|
140
|
+
codeLabel={fields.code.label ?? DEFAULT_CODE_LABEL}
|
|
141
|
+
codeHelper={codeHelper}
|
|
142
|
+
codeState={codeState}
|
|
143
|
+
countdownText={timer?.text}
|
|
144
|
+
countdownActionLabel={countdownActionLabel}
|
|
145
|
+
onCountdownAction={
|
|
146
|
+
countdownActionHandler ? handleCountdownActionClick : undefined
|
|
147
|
+
}
|
|
148
|
+
countdownActionDisabled={countdownActionDisabled}
|
|
149
|
+
codeInputProps={codeInputProps}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
{timer?.helper ? (
|
|
153
|
+
<p className="auth-find-account-timer-helper">{timer.helper}</p>
|
|
154
|
+
) : null}
|
|
155
|
+
<Button.Default
|
|
156
|
+
{...submitButtonProps}
|
|
157
|
+
disabled={cta?.buttonProps?.disabled ?? disabled}
|
|
158
|
+
>
|
|
159
|
+
{cta?.label ?? DEFAULT_SUBMIT_LABEL}
|
|
160
|
+
</Button.Default>
|
|
161
|
+
</form>
|
|
162
|
+
</AuthContainer>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default FindAccountCodeStep;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AuthStageHeader } from "../../container/header";
|
|
2
|
+
import type {
|
|
3
|
+
FindAccountHeadlineProps,
|
|
4
|
+
FindAccountNavigationProps,
|
|
5
|
+
} from "../types";
|
|
6
|
+
|
|
7
|
+
type FindAccountHeaderProps = {
|
|
8
|
+
navigation?: FindAccountNavigationProps;
|
|
9
|
+
headline?: FindAccountHeadlineProps;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find Account 공통 헤더
|
|
15
|
+
* @component
|
|
16
|
+
*/
|
|
17
|
+
export function FindAccountHeader({
|
|
18
|
+
navigation,
|
|
19
|
+
headline,
|
|
20
|
+
className,
|
|
21
|
+
}: FindAccountHeaderProps) {
|
|
22
|
+
if (!navigation && !headline) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<AuthStageHeader
|
|
28
|
+
className={className}
|
|
29
|
+
navigationTitle={navigation?.title ?? ""}
|
|
30
|
+
backIcon={navigation?.backIcon}
|
|
31
|
+
onBack={navigation?.onBack}
|
|
32
|
+
headline={headline?.title ?? null}
|
|
33
|
+
description={headline?.description}
|
|
34
|
+
indicator={
|
|
35
|
+
headline?.progress
|
|
36
|
+
? {
|
|
37
|
+
total: headline.progress.total,
|
|
38
|
+
current: headline.progress.current,
|
|
39
|
+
}
|
|
40
|
+
: undefined
|
|
41
|
+
}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default FindAccountHeader;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useForm } from "react-hook-form";
|
|
4
|
+
import { AuthContainer } from "../../container";
|
|
5
|
+
import { Button, Input, type InputProps } from "@uniai-fe/uds-primitives";
|
|
6
|
+
import type { FindAccountInfoStepProps, FindAccountInfoValues } from "../types";
|
|
7
|
+
import { useFindAccountForm } from "../hooks/useFindAccountForm";
|
|
8
|
+
import { composeFindAccountFieldProps } from "../utils/composeFieldProps";
|
|
9
|
+
import FindAccountHeader from "./Header";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CTA_LABEL = "인증코드 요청";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 이름/메일 입력 Step
|
|
15
|
+
* @component
|
|
16
|
+
*/
|
|
17
|
+
export function FindAccountInfoStep({
|
|
18
|
+
className,
|
|
19
|
+
header,
|
|
20
|
+
footer,
|
|
21
|
+
fieldOptions,
|
|
22
|
+
cta,
|
|
23
|
+
navigation,
|
|
24
|
+
headline,
|
|
25
|
+
}: FindAccountInfoStepProps) {
|
|
26
|
+
const { fields, formAttr, onSubmit, isSubmittable } = fieldOptions;
|
|
27
|
+
|
|
28
|
+
const defaultValues = useMemo(
|
|
29
|
+
() =>
|
|
30
|
+
(Object.keys(fields) as Array<keyof typeof fields>).reduce(
|
|
31
|
+
(acc, fieldKey) => {
|
|
32
|
+
const config = fields[fieldKey];
|
|
33
|
+
const fieldName = config.attr?.name ?? String(fieldKey);
|
|
34
|
+
acc[fieldName] = "";
|
|
35
|
+
return acc;
|
|
36
|
+
},
|
|
37
|
+
{} as FindAccountInfoValues,
|
|
38
|
+
),
|
|
39
|
+
[fields],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const form = useForm<FindAccountInfoValues>({
|
|
43
|
+
mode: "onChange",
|
|
44
|
+
defaultValues,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
register,
|
|
49
|
+
helpers,
|
|
50
|
+
disabled,
|
|
51
|
+
onSubmit: handleSubmit,
|
|
52
|
+
} = useFindAccountForm({
|
|
53
|
+
fields,
|
|
54
|
+
form,
|
|
55
|
+
onSubmit,
|
|
56
|
+
isSubmittable,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const buttonProps = {
|
|
60
|
+
type: "submit" as const,
|
|
61
|
+
scale: "solid-xlarge" as const,
|
|
62
|
+
priority: "primary" as const,
|
|
63
|
+
block: true,
|
|
64
|
+
...cta?.buttonProps,
|
|
65
|
+
disabled: cta?.buttonProps?.disabled ?? disabled,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const resolvedHeader =
|
|
69
|
+
header ??
|
|
70
|
+
(navigation || headline ? (
|
|
71
|
+
<FindAccountHeader navigation={navigation} headline={headline} />
|
|
72
|
+
) : undefined);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<AuthContainer
|
|
76
|
+
className={clsx("auth-find-account-container", className)}
|
|
77
|
+
header={resolvedHeader}
|
|
78
|
+
footer={footer}
|
|
79
|
+
>
|
|
80
|
+
<form
|
|
81
|
+
className="auth-find-account-form auth-find-account-form--info"
|
|
82
|
+
{...formAttr}
|
|
83
|
+
onSubmit={handleSubmit}
|
|
84
|
+
>
|
|
85
|
+
<div className="auth-find-account-fields">
|
|
86
|
+
<Input
|
|
87
|
+
{...composeFindAccountFieldProps<InputProps>(
|
|
88
|
+
fields.name,
|
|
89
|
+
helpers.name,
|
|
90
|
+
)}
|
|
91
|
+
register={register.name}
|
|
92
|
+
/>
|
|
93
|
+
<Input
|
|
94
|
+
{...composeFindAccountFieldProps<InputProps>(
|
|
95
|
+
fields.email,
|
|
96
|
+
helpers.email,
|
|
97
|
+
)}
|
|
98
|
+
register={register.email}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<Button.Default {...buttonProps}>
|
|
102
|
+
{cta?.label ?? DEFAULT_CTA_LABEL}
|
|
103
|
+
</Button.Default>
|
|
104
|
+
</form>
|
|
105
|
+
</AuthContainer>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default FindAccountInfoStep;
|