@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.
Files changed (122) hide show
  1. package/README.md +11 -0
  2. package/dist/styles.css +916 -1074
  3. package/package.json +3 -3
  4. package/src/auth/common/complete/Template.tsx +47 -0
  5. package/src/auth/common/complete/img/circle-check-complete.svg +4 -0
  6. package/src/auth/common/complete/index.scss +38 -0
  7. package/src/auth/common/complete/types.ts +15 -0
  8. package/src/auth/common/container/header/StageHeader.tsx +61 -0
  9. package/src/auth/common/container/header/index.tsx +5 -0
  10. package/src/auth/common/container/header/stage-header.scss +50 -0
  11. package/src/{components/auth → auth/common}/container/index.tsx +2 -0
  12. package/src/auth/common/find/hooks/useFindAccountForm.ts +79 -0
  13. package/src/auth/common/find/markup/CodeStep.tsx +166 -0
  14. package/src/auth/common/find/markup/Header.tsx +46 -0
  15. package/src/auth/common/find/markup/InfoStep.tsx +109 -0
  16. package/src/auth/common/find/styles/email.scss +55 -0
  17. package/src/auth/common/find/styles/find-account.scss +4 -0
  18. package/src/auth/common/find/styles/layout.scss +19 -0
  19. package/src/auth/common/find/styles/password.scss +39 -0
  20. package/src/auth/common/find/styles/result.scss +78 -0
  21. package/src/auth/common/find/types/forms.ts +30 -0
  22. package/src/auth/common/find/types/index.ts +121 -0
  23. package/src/auth/common/find/utils/composeFieldProps.ts +45 -0
  24. package/src/auth/common/password/constants.ts +19 -0
  25. package/src/auth/common/password/hooks/useCheckPassword.ts +133 -0
  26. package/src/auth/common/password/img/check-password.svg +3 -0
  27. package/src/auth/common/password/markup/PasswordSetField.tsx +250 -0
  28. package/src/auth/common/password/styles/password-set-field.scss +49 -0
  29. package/src/auth/common/password/types.ts +142 -0
  30. package/src/auth/common/password/utils/composePasswordFieldProps.ts +44 -0
  31. package/src/auth/find-account.ts +28 -0
  32. package/src/auth/find-id/hooks/index.ts +1 -0
  33. package/src/auth/find-id/index.scss +1 -0
  34. package/src/auth/find-id/index.ts +23 -0
  35. package/src/auth/find-id/markup/StepComplete.tsx +58 -0
  36. package/src/auth/find-id/markup/StepIdentify.tsx +46 -0
  37. package/src/auth/find-id/markup/StepVerifyCode.tsx +48 -0
  38. package/src/auth/find-id/types/index.ts +66 -0
  39. package/src/auth/find-password/index.scss +1 -0
  40. package/src/auth/find-password/index.ts +30 -0
  41. package/src/auth/find-password/markup/StepComplete.tsx +30 -0
  42. package/src/auth/find-password/markup/StepIdentify.tsx +45 -0
  43. package/src/auth/find-password/markup/StepResetPassword.tsx +150 -0
  44. package/src/auth/find-password/markup/StepVerifyCode.tsx +48 -0
  45. package/src/auth/index.tsx +41 -0
  46. package/src/{components/auth → auth}/login/markup/Container.tsx +1 -1
  47. package/src/{components/auth → auth}/login/types/props.ts +1 -1
  48. package/src/{components/auth → auth}/signup/hooks/useSignupAccountForm.ts +26 -2
  49. package/src/{components/auth → auth}/signup/hooks/useSignupUserInfoForm.ts +10 -3
  50. package/src/auth/signup/img/check-agree.svg +3 -0
  51. package/src/auth/signup/img/chevron-open-detail.svg +3 -0
  52. package/src/{components/auth → auth}/signup/index.ts +3 -0
  53. package/src/auth/signup/markup/AccountForm.tsx +113 -0
  54. package/src/auth/signup/markup/Complete.tsx +59 -0
  55. package/src/auth/signup/markup/Template.tsx +110 -0
  56. package/src/{components/auth → auth}/signup/markup/UserInfoForm.tsx +23 -13
  57. package/src/auth/signup/markup/VerificationForm.tsx +285 -0
  58. package/src/{components/auth → auth}/signup/markup/index.ts +1 -0
  59. package/src/auth/signup/styles/signup.scss +187 -0
  60. package/src/{components/auth → auth}/signup/types/hooks.ts +1 -0
  61. package/src/{components/auth → auth}/signup/types/props.ts +49 -9
  62. package/src/auth/signup/utils/getSignupFieldDefaultValue.ts +40 -0
  63. package/src/index.scss +5 -4
  64. package/src/index.tsx +3 -3
  65. package/src/page-frame/mobile/header/PageFrameMobileHeader.tsx +52 -0
  66. package/src/page-frame/mobile/header/index.ts +4 -0
  67. package/src/page-frame/mobile/header/page-frame-mobile-header.scss +48 -0
  68. package/src/page-frame/mobile/img/chevron-backward.svg +3 -0
  69. package/src/components/auth/index.tsx +0 -20
  70. package/src/components/auth/signup/markup/AccountForm.tsx +0 -124
  71. package/src/components/auth/signup/markup/Complete.tsx +0 -61
  72. package/src/components/auth/signup/markup/VerificationForm.tsx +0 -155
  73. package/src/components/auth/signup/styles/signup.scss +0 -135
  74. /package/src/{components/auth → auth/common}/container/AuthContainer.tsx +0 -0
  75. /package/src/{components/auth → auth/common}/container/index.scss +0 -0
  76. /package/src/{components/auth → auth/common}/container/types.ts +0 -0
  77. /package/src/{components/auth → auth}/login/data/valid-options.ts +0 -0
  78. /package/src/{components/auth → auth}/login/hooks/index.ts +0 -0
  79. /package/src/{components/auth → auth}/login/hooks/useAuthLoginForm.ts +0 -0
  80. /package/src/{components/auth → auth}/login/index.scss +0 -0
  81. /package/src/{components/auth → auth}/login/index.tsx +0 -0
  82. /package/src/{components/auth → auth}/login/markup/FormField.tsx +0 -0
  83. /package/src/{components/auth → auth}/login/markup/LinkButtons.tsx +0 -0
  84. /package/src/{components/auth → auth}/login/styles/login.scss +0 -0
  85. /package/src/{components/auth → auth}/login/types/form.ts +0 -0
  86. /package/src/{components/auth → auth}/login/types/hooks.ts +0 -0
  87. /package/src/{components/auth → auth}/login/types.ts +0 -0
  88. /package/src/{components/auth → auth}/signup/hooks/index.ts +0 -0
  89. /package/src/{components/auth → auth}/signup/hooks/useSignupVerificationForm.ts +0 -0
  90. /package/src/{components/auth → auth}/signup/types/index.ts +0 -0
  91. /package/src/{components/auth → auth}/signup/utils/composeFieldProps.ts +0 -0
  92. /package/src/{components/modal → modal}/core/components/Container.tsx +0 -0
  93. /package/src/{components/modal → modal}/core/components/FooterButtons.tsx +0 -0
  94. /package/src/{components/modal → modal}/core/components/Provider.tsx +0 -0
  95. /package/src/{components/modal → modal}/core/components/Root.tsx +0 -0
  96. /package/src/{components/modal → modal}/core/hooks/useModal.ts +0 -0
  97. /package/src/{components/modal → modal}/core/jotai/atoms.ts +0 -0
  98. /package/src/{components/modal → modal}/index.scss +0 -0
  99. /package/src/{components/modal → modal}/index.tsx +0 -0
  100. /package/src/{components/modal → modal}/styles/animations.scss +0 -0
  101. /package/src/{components/modal → modal}/styles/base.scss +0 -0
  102. /package/src/{components/modal → modal}/styles/container.scss +0 -0
  103. /package/src/{components/modal → modal}/styles/dimmer.scss +0 -0
  104. /package/src/{components/modal → modal}/templates/Alert.tsx +0 -0
  105. /package/src/{components/modal → modal}/templates/Dialog.tsx +0 -0
  106. /package/src/{components/modal → modal}/types/footer.ts +0 -0
  107. /package/src/{components/modal → modal}/types/index.ts +0 -0
  108. /package/src/{components/modal → modal}/types/options.ts +0 -0
  109. /package/src/{components/modal → modal}/types/state.ts +0 -0
  110. /package/src/{components/modal → modal}/types/templates.ts +0 -0
  111. /package/src/{components/page-frame → page-frame}/container/PageFrameContainer.tsx +0 -0
  112. /package/src/{components/page-frame → page-frame}/container/index.scss +0 -0
  113. /package/src/{components/page-frame → page-frame}/container/index.tsx +0 -0
  114. /package/src/{components/page-frame → page-frame}/container/types.ts +0 -0
  115. /package/src/{components/page-frame → page-frame}/index.tsx +0 -0
  116. /package/src/{components/page-frame → page-frame}/mobile/PageFrameMobile.tsx +0 -0
  117. /package/src/{components/page-frame → page-frame}/mobile/index.scss +0 -0
  118. /package/src/{components/page-frame → page-frame}/mobile/index.tsx +0 -0
  119. /package/src/{components/page-frame → page-frame}/mobile/types.ts +0 -0
  120. /package/src/{components/page-frame → page-frame}/navigation/PageFrameNavigation.tsx +0 -0
  121. /package/src/{components/page-frame → page-frame}/navigation/index.scss +0 -0
  122. /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.11",
3
+ "version": "0.0.13",
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.1",
15
+ "packageManager": "pnpm@10.26.2",
16
16
  "engines": {
17
17
  "node": ">=24",
18
18
  "pnpm": ">=10"
@@ -74,7 +74,7 @@
74
74
  "next": "^15.5.9",
75
75
  "prettier": "^3.7.4",
76
76
  "react-hook-form": "^7.69.0",
77
- "jotai": "^2.16.0",
77
+ "jotai": "^2.16.1",
78
78
  "sass": "^1.97.1",
79
79
  "typescript": "~5.9.3"
80
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,5 @@
1
+ export { AuthStageHeader } from "./StageHeader";
2
+ export type {
3
+ StageHeaderProps,
4
+ StageHeaderIndicatorProps,
5
+ } from "./StageHeader";
@@ -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
+ }
@@ -1,4 +1,6 @@
1
1
  import "./index.scss";
2
+ import "./header/stage-header.scss";
2
3
 
4
+ export { AuthStageHeader } from "./header";
3
5
  export { AuthContainer } from "./AuthContainer";
4
6
  export type { AuthContainerProps } from "./types";
@@ -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;