@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
@@ -0,0 +1,59 @@
1
+ import clsx from "clsx";
2
+ import { Button } from "@uniai-fe/uds-primitives";
3
+ import AuthCompleteTemplate from "../../common/complete/Template";
4
+ import type { AuthSignupCompleteProps } from "../types";
5
+
6
+ /**
7
+ * 회원가입 Step4; 가입 완료/승인 대기 안내
8
+ * @component
9
+ * @param {AuthSignupCompleteProps} props complete props
10
+ * @param {React.ReactNode} props.title 완료 메시지
11
+ * @param {React.ReactNode} [props.description] 부가 설명
12
+ * @param {React.ReactNode} [props.illustration] 일러스트 영역
13
+ * @param {AuthSignupCompleteAction} props.primaryAction 주요 CTA
14
+ * @param {AuthSignupCompleteAction} [props.secondaryAction] 보조 CTA
15
+ */
16
+ export function AuthSignupComplete({
17
+ title,
18
+ description,
19
+ illustration,
20
+ primaryAction,
21
+ secondaryAction,
22
+ }: AuthSignupCompleteProps) {
23
+ const secondaryButton = secondaryAction ? (
24
+ <div className="auth-signup-complete-actions">
25
+ <Button.Default
26
+ type="button"
27
+ scale="solid-xlarge"
28
+ priority="secondary"
29
+ block
30
+ disabled={secondaryAction.disabled}
31
+ onClick={secondaryAction.onClick}
32
+ >
33
+ {secondaryAction.label}
34
+ </Button.Default>
35
+ </div>
36
+ ) : null;
37
+
38
+ return (
39
+ <AuthCompleteTemplate
40
+ className={clsx("auth-signup-complete")}
41
+ title={title}
42
+ description={description}
43
+ cta={{
44
+ label: primaryAction.label,
45
+ buttonProps: {
46
+ onClick: primaryAction.onClick,
47
+ disabled: primaryAction.disabled,
48
+ },
49
+ }}
50
+ >
51
+ {illustration ? (
52
+ <div className="auth-signup-complete-illustration">{illustration}</div>
53
+ ) : null}
54
+ {secondaryButton}
55
+ </AuthCompleteTemplate>
56
+ );
57
+ }
58
+
59
+ export default AuthSignupComplete;
@@ -0,0 +1,110 @@
1
+ import clsx from "clsx";
2
+ import type { ReactNode } from "react";
3
+ import { AuthContainer } from "../../common/container";
4
+ import type {
5
+ AuthSignupStepIndicatorItem,
6
+ AuthSignupTemplateProps,
7
+ } from "../types";
8
+ import type { AuthSignupStepId } from "../types";
9
+ import { AuthSignupUserInfoForm } from "./UserInfoForm";
10
+ import { AuthSignupVerificationForm } from "./VerificationForm";
11
+ import { AuthSignupAccountForm } from "./AccountForm";
12
+ import { AuthSignupComplete } from "./Complete";
13
+ import { AuthStageHeader } from "../../common/container/header";
14
+
15
+ const DEFAULT_STEPS: AuthSignupStepIndicatorItem[] = [
16
+ { id: "userInfo", label: "기본 정보" },
17
+ { id: "verifyAgreement", label: "약관 · 인증" },
18
+ { id: "generateAccount", label: "계정 생성" },
19
+ { id: "complete", label: "완료" },
20
+ ];
21
+
22
+ const STEP_INDEX: Record<AuthSignupStepId, number> = {
23
+ userInfo: 0,
24
+ verifyAgreement: 1,
25
+ generateAccount: 2,
26
+ complete: 3,
27
+ };
28
+
29
+ /**
30
+ * 회원가입 템플릿; Step1~4 화면을 AuthContainer 위에서 전환한다.
31
+ * @component
32
+ * @param {AuthSignupTemplateProps} props 템플릿 props
33
+ * @param {AuthSignupHeaderProps} props.header 헤더 구성(타이틀/뒤로가기 등)
34
+ * @param {AuthSignupStepId} props.step 현재 step
35
+ * @param {AuthSignupStepIndicatorProps} [props.stepIndicator] step indicator override
36
+ * @param {React.ReactNode} [props.footer] footer slot
37
+ */
38
+ export function AuthSignupTemplate({
39
+ className,
40
+ header,
41
+ step,
42
+ stepIndicator,
43
+ footer,
44
+ userInfo,
45
+ verification,
46
+ account,
47
+ complete,
48
+ }: AuthSignupTemplateProps) {
49
+ const steps = stepIndicator?.items ?? DEFAULT_STEPS;
50
+ const currentStep = stepIndicator?.current ?? step;
51
+ const defaultStepIndex = Math.max(
52
+ 0,
53
+ steps.findIndex(item => item.id === currentStep),
54
+ );
55
+ const fallbackTotal = steps.length || 1;
56
+ const providedTotal = stepIndicator?.total ?? fallbackTotal;
57
+ const resolvedTotal = Math.max(1, providedTotal);
58
+ const providedIndex = stepIndicator?.currentIndex ?? defaultStepIndex + 1;
59
+ const resolvedIndex = Math.min(Math.max(1, providedIndex), resolvedTotal);
60
+
61
+ const indicator =
62
+ resolvedTotal > 0
63
+ ? {
64
+ total: resolvedTotal,
65
+ current: resolvedIndex,
66
+ }
67
+ : undefined;
68
+
69
+ const headerNode = (
70
+ <AuthStageHeader
71
+ className="auth-signup-header"
72
+ navigationTitle={header.navigationTitle}
73
+ headline={header.headline}
74
+ description={header.description}
75
+ backIcon={header.backIcon}
76
+ onBack={header.onBack}
77
+ indicator={indicator}
78
+ />
79
+ );
80
+
81
+ let content: ReactNode = null;
82
+ switch (currentStep) {
83
+ case "userInfo":
84
+ content = <AuthSignupUserInfoForm {...userInfo} />;
85
+ break;
86
+ case "verifyAgreement":
87
+ content = <AuthSignupVerificationForm {...verification} />;
88
+ break;
89
+ case "generateAccount":
90
+ content = <AuthSignupAccountForm {...account} />;
91
+ break;
92
+ case "complete":
93
+ content = <AuthSignupComplete {...complete} />;
94
+ break;
95
+ default:
96
+ content = null;
97
+ }
98
+
99
+ return (
100
+ <AuthContainer
101
+ className={clsx("auth-signup-template", className)}
102
+ header={headerNode}
103
+ footer={footer}
104
+ >
105
+ {content}
106
+ </AuthContainer>
107
+ );
108
+ }
109
+
110
+ export default AuthSignupTemplate;
@@ -11,8 +11,9 @@ import type {
11
11
  AuthSignupUserInfoProps,
12
12
  AuthSignupUserInfoValues,
13
13
  } from "../types";
14
- import { useSignupUserInfoForm } from "../hooks";
14
+ import { useSignupUserInfoForm } from "../hooks/useSignupUserInfoForm";
15
15
  import { composeSignupFieldProps } from "../utils/composeFieldProps";
16
+ import { getSignupFieldDefaultValue } from "../utils/getSignupFieldDefaultValue";
16
17
 
17
18
  const DEFAULT_SUBMIT_LABEL = "다음";
18
19
 
@@ -27,6 +28,7 @@ const DEFAULT_SUBMIT_LABEL = "다음";
27
28
  */
28
29
  export function AuthSignupUserInfoForm({
29
30
  fields,
31
+ visibleFields,
30
32
  formAttr,
31
33
  submitLabel,
32
34
  onSubmit,
@@ -38,7 +40,7 @@ export function AuthSignupUserInfoForm({
38
40
  (acc, fieldKey) => {
39
41
  const config = fields[fieldKey];
40
42
  const fieldName = config.attr?.name ?? String(fieldKey);
41
- acc[fieldName] = "";
43
+ acc[fieldName] = getSignupFieldDefaultValue(config);
42
44
  return acc;
43
45
  },
44
46
  {} as AuthSignupUserInfoValues,
@@ -51,6 +53,9 @@ export function AuthSignupUserInfoForm({
51
53
  defaultValues,
52
54
  });
53
55
 
56
+ const renderedFields =
57
+ visibleFields ?? (Object.keys(fields) as Array<keyof typeof fields>);
58
+
54
59
  const {
55
60
  register,
56
61
  helpers,
@@ -60,6 +65,7 @@ export function AuthSignupUserInfoForm({
60
65
  fields,
61
66
  form,
62
67
  onSubmit,
68
+ activeFields: renderedFields,
63
69
  });
64
70
 
65
71
  return (
@@ -69,17 +75,21 @@ export function AuthSignupUserInfoForm({
69
75
  onSubmit={handleSubmit}
70
76
  >
71
77
  <div className="auth-signup-fields">
72
- <Input
73
- {...composeSignupFieldProps<InputProps>(fields.name, helpers.name)}
74
- register={register.name}
75
- />
76
- <PhoneInput
77
- {...composeSignupFieldProps<PhoneInputProps>(
78
- fields.phone,
79
- helpers.phone,
80
- )}
81
- register={register.phone}
82
- />
78
+ {renderedFields.includes("name") ? (
79
+ <Input
80
+ {...composeSignupFieldProps<InputProps>(fields.name, helpers.name)}
81
+ register={register.name}
82
+ />
83
+ ) : null}
84
+ {renderedFields.includes("phone") ? (
85
+ <PhoneInput
86
+ {...composeSignupFieldProps<PhoneInputProps>(
87
+ fields.phone,
88
+ helpers.phone,
89
+ )}
90
+ register={register.phone}
91
+ />
92
+ ) : null}
83
93
  </div>
84
94
  <Button.Default
85
95
  type="submit"
@@ -0,0 +1,285 @@
1
+ import { useMemo, useState } from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import {
4
+ Button,
5
+ CheckboxField,
6
+ DrawerBody,
7
+ DrawerContent,
8
+ DrawerFooter,
9
+ DrawerHeader,
10
+ DrawerOverlay,
11
+ DrawerPortal,
12
+ DrawerRoot,
13
+ DrawerTitle,
14
+ EmailInput,
15
+ type EmailInputProps,
16
+ } from "@uniai-fe/uds-primitives";
17
+ import type { FormEvent } from "react";
18
+ import type {
19
+ AuthSignupAgreementOption,
20
+ AuthSignupVerificationProps,
21
+ AuthSignupVerificationValues,
22
+ } from "../types";
23
+ import { useSignupVerificationForm } from "../hooks";
24
+ import { composeSignupFieldProps } from "../utils/composeFieldProps";
25
+ import { getSignupFieldDefaultValue } from "../utils/getSignupFieldDefaultValue";
26
+ import CheckAgreeIcon from "../img/check-agree.svg";
27
+ import ChevronOpenDetailIcon from "../img/chevron-open-detail.svg";
28
+
29
+ const DEFAULT_SUBMIT_LABEL = "다음";
30
+ const REQUEST_CODE_LABEL = "인증코드 요청";
31
+ const RESEND_CODE_LABEL = "인증번호 재요청";
32
+
33
+ /**
34
+ * 회원가입 Step2; 약관 동의 + 이메일 인증
35
+ * @component
36
+ * @param {AuthSignupVerificationProps} props verification props
37
+ * @param {AuthSignupAgreementOption[]} props.agreements 약관 목록
38
+ * @param {Record<string, boolean>} props.agreementState 약관 체크 상태
39
+ * @param {(agreementId: string) => void} props.onToggleAgreement 단일 약관 토글
40
+ * @param {(options?: AuthSignupAgreementToggleAllOptions) => void} props.onToggleAll 전체 동의(필수 only) 토글
41
+ * @param {(agreementId: string) => void} [props.onOpenAgreementDetail] 약관 상세 보기
42
+ * @param {React.FormHTMLAttributes<HTMLFormElement>} [props.formAttr] form attr
43
+ * @param {import("react").ReactNode} [props.submitLabel] CTA 라벨
44
+ */
45
+ export function AuthSignupVerificationForm({
46
+ fields,
47
+ agreements,
48
+ agreementState,
49
+ onToggleAgreement,
50
+ onToggleAll,
51
+ onOpenAgreementDetail,
52
+ formAttr,
53
+ submitLabel,
54
+ onSubmit,
55
+ }: AuthSignupVerificationProps) {
56
+ const [openedAgreementId, setOpenedAgreementId] = useState<string | null>(
57
+ null,
58
+ );
59
+
60
+ const handleOpenAgreementDetail = (agreementId: string) => {
61
+ if (onOpenAgreementDetail) {
62
+ onOpenAgreementDetail(agreementId);
63
+ }
64
+ setOpenedAgreementId(agreementId);
65
+ };
66
+
67
+ const handleCloseDrawer = () => {
68
+ setOpenedAgreementId(null);
69
+ };
70
+
71
+ const openedAgreement = useMemo(() => {
72
+ if (!openedAgreementId) {
73
+ return null;
74
+ }
75
+ return agreements.find(option => option.id === openedAgreementId) ?? null;
76
+ }, [agreements, openedAgreementId]);
77
+
78
+ // 이메일 필드 name attr 기반으로 RHF defaultValues를 설정한다.
79
+ const defaultValues = useMemo(
80
+ () =>
81
+ (Object.keys(fields) as Array<keyof typeof fields>).reduce(
82
+ (acc, fieldKey) => {
83
+ const config = fields[fieldKey];
84
+ const fieldName = config.attr?.name ?? String(fieldKey);
85
+ acc[fieldName] = getSignupFieldDefaultValue(config);
86
+ return acc;
87
+ },
88
+ {} as AuthSignupVerificationValues,
89
+ ),
90
+ [fields],
91
+ );
92
+
93
+ const form = useForm<AuthSignupVerificationValues>({
94
+ mode: "onChange",
95
+ defaultValues,
96
+ });
97
+
98
+ const {
99
+ register,
100
+ helpers,
101
+ disabled,
102
+ onSubmit: handleSubmit,
103
+ } = useSignupVerificationForm({
104
+ fields,
105
+ form,
106
+ onSubmit,
107
+ });
108
+
109
+ const requiredAgreements = useMemo(
110
+ () => agreements.filter(option => option.required),
111
+ [agreements],
112
+ );
113
+
114
+ const allRequiredChecked = requiredAgreements.length
115
+ ? requiredAgreements.every(option => agreementState[option.id])
116
+ : true;
117
+
118
+ const emailFieldProps = composeSignupFieldProps<EmailInputProps>(
119
+ fields.email,
120
+ helpers.email,
121
+ );
122
+
123
+ const emailRequestHandler = emailFieldProps.onRequestCode;
124
+ const codeRequested = Boolean(emailFieldProps.codeVisible);
125
+ const inlineRequestButtonVisible =
126
+ codeRequested && Boolean(emailRequestHandler);
127
+ const normalizedEmailFieldProps: EmailInputProps = inlineRequestButtonVisible
128
+ ? {
129
+ ...emailFieldProps,
130
+ requestButtonLabel:
131
+ emailFieldProps.requestButtonLabel ?? RESEND_CODE_LABEL,
132
+ }
133
+ : {
134
+ ...emailFieldProps,
135
+ onRequestCode: undefined,
136
+ requestButtonLabel: undefined,
137
+ requestButtonDisabled: undefined,
138
+ };
139
+
140
+ const shouldRequestCode =
141
+ allRequiredChecked && !codeRequested && Boolean(emailRequestHandler);
142
+ const ctaLabel = shouldRequestCode
143
+ ? REQUEST_CODE_LABEL
144
+ : (submitLabel ?? DEFAULT_SUBMIT_LABEL);
145
+ const ctaDisabled = disabled || !allRequiredChecked;
146
+
147
+ const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
148
+ formAttr?.onSubmit?.(event);
149
+ if (event.defaultPrevented) {
150
+ return;
151
+ }
152
+
153
+ if (shouldRequestCode && emailRequestHandler) {
154
+ event.preventDefault();
155
+ event.stopPropagation();
156
+ emailRequestHandler();
157
+ return;
158
+ }
159
+
160
+ handleSubmit(event);
161
+ };
162
+
163
+ const renderAgreementLabel = (option: AuthSignupAgreementOption) => (
164
+ <span className="auth-signup-agreement-label">
165
+ <span
166
+ className="auth-signup-agreement-badge"
167
+ data-required={option.required ? "true" : "false"}
168
+ >
169
+ [{option.required ? "필수" : "선택"}]
170
+ </span>
171
+ <span className="auth-signup-agreement-title">{option.label}</span>
172
+ </span>
173
+ );
174
+
175
+ return (
176
+ <form
177
+ className="auth-signup-form auth-signup-form--verification"
178
+ {...formAttr}
179
+ onSubmit={handleFormSubmit}
180
+ >
181
+ <div className="auth-signup-fields">
182
+ <EmailInput {...normalizedEmailFieldProps} register={register.email} />
183
+ </div>
184
+ {agreements.length ? (
185
+ <section className="auth-signup-agreements" aria-label="약관 동의">
186
+ <div className="auth-signup-agreement-all">
187
+ <CheckboxField
188
+ size="large"
189
+ label={<span>필수 약관에 모두 동의하기</span>}
190
+ checked={allRequiredChecked}
191
+ onCheckedChange={() => onToggleAll({ requiredOnly: true })}
192
+ />
193
+ </div>
194
+ <div className="auth-signup-agreements-list">
195
+ {agreements.map(option => {
196
+ const checked = Boolean(agreementState[option.id]);
197
+
198
+ return (
199
+ <div
200
+ className="auth-signup-agreement-row"
201
+ key={option.id}
202
+ data-required={option.required ? "true" : undefined}
203
+ data-checked={checked ? "true" : undefined}
204
+ >
205
+ {/* 약관 토글은 Figma 아이콘 사양대로 커스텀 버튼으로 구성한다. */}
206
+ <button
207
+ type="button"
208
+ className="auth-signup-agreement-toggle"
209
+ aria-pressed={checked}
210
+ data-checked={checked ? "true" : undefined}
211
+ onClick={() => onToggleAgreement(option.id)}
212
+ >
213
+ <span
214
+ className="auth-signup-agreement-icon"
215
+ aria-hidden="true"
216
+ >
217
+ <CheckAgreeIcon />
218
+ </span>
219
+ {renderAgreementLabel(option)}
220
+ </button>
221
+ {onOpenAgreementDetail ? (
222
+ <button
223
+ type="button"
224
+ className="auth-signup-agreement-detail"
225
+ aria-label={`${option.label} 약관 상세 보기`}
226
+ onClick={() => handleOpenAgreementDetail(option.id)}
227
+ >
228
+ <span aria-hidden="true">
229
+ <ChevronOpenDetailIcon />
230
+ </span>
231
+ </button>
232
+ ) : null}
233
+ </div>
234
+ );
235
+ })}
236
+ </div>
237
+ </section>
238
+ ) : null}
239
+ <Button.Default
240
+ type="submit"
241
+ scale="solid-xlarge"
242
+ priority="primary"
243
+ block
244
+ disabled={ctaDisabled}
245
+ >
246
+ {ctaLabel}
247
+ </Button.Default>
248
+ <DrawerRoot
249
+ open={Boolean(openedAgreement)}
250
+ onOpenChange={isOpen => {
251
+ if (!isOpen) {
252
+ handleCloseDrawer();
253
+ }
254
+ }}
255
+ >
256
+ <DrawerPortal>
257
+ <DrawerOverlay />
258
+ <DrawerContent>
259
+ <DrawerHeader>
260
+ <DrawerTitle>{openedAgreement?.label ?? "약관 상세"}</DrawerTitle>
261
+ </DrawerHeader>
262
+ {openedAgreement?.description ? (
263
+ <DrawerBody className="auth-signup-agreement-drawer-body">
264
+ {openedAgreement.description}
265
+ </DrawerBody>
266
+ ) : null}
267
+ <DrawerFooter>
268
+ <Button.Default
269
+ type="button"
270
+ scale="solid-medium"
271
+ priority="primary"
272
+ block
273
+ onClick={handleCloseDrawer}
274
+ >
275
+ 닫기
276
+ </Button.Default>
277
+ </DrawerFooter>
278
+ </DrawerContent>
279
+ </DrawerPortal>
280
+ </DrawerRoot>
281
+ </form>
282
+ );
283
+ }
284
+
285
+ export default AuthSignupVerificationForm;
@@ -2,3 +2,4 @@ export { AuthSignupUserInfoForm } from "./UserInfoForm";
2
2
  export { AuthSignupVerificationForm } from "./VerificationForm";
3
3
  export { AuthSignupAccountForm } from "./AccountForm";
4
4
  export { AuthSignupComplete } from "./Complete";
5
+ export { AuthSignupTemplate } from "./Template";