@uniai-fe/uds-templates 0.4.32 → 0.5.0
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/dist/styles.css +108 -21
- package/package.json +1 -1
- package/src/auth/signup/hooks/index.ts +3 -0
- package/src/auth/signup/hooks/useSignupAgreementForm.ts +48 -0
- package/src/auth/signup/hooks/useSignupFarmCodeForm.ts +87 -0
- package/src/auth/signup/hooks/useSignupTypeSelectForm.ts +56 -0
- package/src/auth/signup/img/select-user-type-default.svg +15 -0
- package/src/auth/signup/img/select-user-type-selected.svg +3 -0
- package/src/auth/signup/index.ts +10 -0
- package/src/auth/signup/markup/AccountForm.tsx +1 -0
- package/src/auth/signup/markup/AgreementForm.tsx +229 -0
- package/src/auth/signup/markup/FarmCodeForm.tsx +122 -0
- package/src/auth/signup/markup/Template.tsx +58 -2
- package/src/auth/signup/markup/TypeSelectForm.tsx +102 -0
- package/src/auth/signup/markup/UserInfoForm.tsx +9 -0
- package/src/auth/signup/markup/VerificationForm.tsx +36 -26
- package/src/auth/signup/markup/index.ts +3 -0
- package/src/auth/signup/styles/signup.scss +95 -20
- package/src/auth/signup/types/hooks.ts +132 -0
- package/src/auth/signup/types/props.ts +210 -4
- package/src/modal/styles/container.scss +17 -2
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
import type { SubmitEvent } from "react";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
CheckboxField,
|
|
9
|
+
DrawerBody,
|
|
10
|
+
DrawerContent,
|
|
11
|
+
DrawerFooter,
|
|
12
|
+
DrawerHeader,
|
|
13
|
+
DrawerOverlay,
|
|
14
|
+
DrawerPortal,
|
|
15
|
+
DrawerRoot,
|
|
16
|
+
DrawerTitle,
|
|
17
|
+
} from "@uniai-fe/uds-primitives";
|
|
18
|
+
import type {
|
|
19
|
+
AuthSignupAgreementOption,
|
|
20
|
+
AuthSignupAgreementProps,
|
|
21
|
+
} from "../types";
|
|
22
|
+
import { useSignupAgreementForm } from "../hooks";
|
|
23
|
+
import CheckAgreeIcon from "../img/check-agree.svg";
|
|
24
|
+
import ChevronOpenDetailIcon from "../img/chevron-open-detail.svg";
|
|
25
|
+
|
|
26
|
+
const AGREEMENT_DETAIL_FALLBACK = (
|
|
27
|
+
<>
|
|
28
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
|
29
|
+
<p>Nulla facilisi. Integer porta, nisl at volutpat posuere, erat arcu.</p>
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 회원가입 Step4; 약관 동의 폼
|
|
35
|
+
* @component
|
|
36
|
+
* @param {AuthSignupAgreementProps} props agreement props
|
|
37
|
+
* @param {AuthSignupAgreementOption[]} props.agreements 약관 목록
|
|
38
|
+
* @param {Record<string, boolean>} props.agreementState 약관 체크 상태
|
|
39
|
+
* @param {boolean} [props.isThirdPartyRequired] 제3자 제공 동의 필수 여부
|
|
40
|
+
* @param {string} [props.thirdPartyAgreementId] 제3자 제공 동의 id
|
|
41
|
+
* @param {(agreementId: string) => void} props.onToggleAgreement 단일 약관 토글
|
|
42
|
+
* @param {(options?: AuthSignupAgreementToggleAllOptions) => void} props.onToggleAll 전체 동의 토글
|
|
43
|
+
* @param {(agreementId: string) => void} [props.onOpenAgreementDetail] 약관 상세 열기
|
|
44
|
+
* @param {React.FormHTMLAttributes<HTMLFormElement>} [props.formAttr] form attr
|
|
45
|
+
* @param {import("react").ReactNode} [props.submitLabel] CTA 라벨
|
|
46
|
+
*/
|
|
47
|
+
export function AuthSignupAgreementForm({
|
|
48
|
+
agreements,
|
|
49
|
+
agreementState,
|
|
50
|
+
isThirdPartyRequired,
|
|
51
|
+
thirdPartyAgreementId,
|
|
52
|
+
onToggleAgreement,
|
|
53
|
+
onToggleAll,
|
|
54
|
+
onOpenAgreementDetail,
|
|
55
|
+
formAttr,
|
|
56
|
+
submitLabel,
|
|
57
|
+
submitDisabled,
|
|
58
|
+
onSubmit,
|
|
59
|
+
}: AuthSignupAgreementProps) {
|
|
60
|
+
const [openedAgreementId, setOpenedAgreementId] = useState<string | null>(
|
|
61
|
+
null,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const { requiredAgreementIds, allRequiredChecked, disabled } =
|
|
65
|
+
useSignupAgreementForm({
|
|
66
|
+
agreements,
|
|
67
|
+
agreementState,
|
|
68
|
+
isThirdPartyRequired,
|
|
69
|
+
thirdPartyAgreementId,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const openedAgreement = useMemo(() => {
|
|
73
|
+
if (!openedAgreementId) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return agreements.find(option => option.id === openedAgreementId) ?? null;
|
|
78
|
+
}, [agreements, openedAgreementId]);
|
|
79
|
+
|
|
80
|
+
const handleCloseDrawer = () => {
|
|
81
|
+
setOpenedAgreementId(null);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleOpenAgreementDetail = (agreementId: string) => {
|
|
85
|
+
onOpenAgreementDetail?.(agreementId);
|
|
86
|
+
setOpenedAgreementId(agreementId);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// React 19 submit 이벤트 계약과 동일하게 SubmitEvent로 맞춘다.
|
|
90
|
+
const handleSubmit = (event: SubmitEvent<HTMLFormElement>) => {
|
|
91
|
+
formAttr?.onSubmit?.(event);
|
|
92
|
+
|
|
93
|
+
if (event.defaultPrevented) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
if (!disabled && !submitDisabled) {
|
|
99
|
+
onSubmit();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const renderAgreementLabel = (
|
|
104
|
+
option: AuthSignupAgreementOption,
|
|
105
|
+
required: boolean,
|
|
106
|
+
) => (
|
|
107
|
+
<span className="auth-signup-agreement-label">
|
|
108
|
+
<span
|
|
109
|
+
className="auth-signup-agreement-badge"
|
|
110
|
+
data-required={required ? "true" : "false"}
|
|
111
|
+
>
|
|
112
|
+
[{required ? "필수" : "선택"}]
|
|
113
|
+
</span>
|
|
114
|
+
<span className="auth-signup-agreement-title">{option.label}</span>
|
|
115
|
+
</span>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<form
|
|
120
|
+
className={clsx("auth-signup-form", "auth-signup-form-agreement")}
|
|
121
|
+
{...formAttr}
|
|
122
|
+
onSubmit={handleSubmit}
|
|
123
|
+
>
|
|
124
|
+
{agreements.length ? (
|
|
125
|
+
<section className="auth-signup-agreements" aria-label="약관 동의">
|
|
126
|
+
<CheckboxField
|
|
127
|
+
size="large"
|
|
128
|
+
checked={allRequiredChecked}
|
|
129
|
+
onCheckedChange={() => onToggleAll({ requiredOnly: true })}
|
|
130
|
+
fieldClassName="auth-signup-agreement-all-field"
|
|
131
|
+
labelWrapperClassName="auth-signup-agreement-all"
|
|
132
|
+
className="auth-signup-agreement-all-checkbox"
|
|
133
|
+
label={
|
|
134
|
+
<span className="auth-signup-agreement-all-label">
|
|
135
|
+
필수 약관에 모두 동의하기
|
|
136
|
+
</span>
|
|
137
|
+
}
|
|
138
|
+
/>
|
|
139
|
+
<div className="auth-signup-agreements-list">
|
|
140
|
+
{agreements.map(option => {
|
|
141
|
+
const checked = Boolean(agreementState[option.id]);
|
|
142
|
+
const required = requiredAgreementIds.includes(option.id);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
className="auth-signup-agreement-row"
|
|
147
|
+
key={option.id}
|
|
148
|
+
data-required={required ? "true" : undefined}
|
|
149
|
+
data-checked={checked ? "true" : undefined}
|
|
150
|
+
>
|
|
151
|
+
{/* 약관 토글은 Figma 아이콘 사양에 맞춰 카드 버튼으로 렌더링한다. */}
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
className="auth-signup-agreement-toggle"
|
|
155
|
+
aria-pressed={checked}
|
|
156
|
+
data-checked={checked ? "true" : undefined}
|
|
157
|
+
onClick={() => onToggleAgreement(option.id)}
|
|
158
|
+
>
|
|
159
|
+
<span
|
|
160
|
+
className="auth-signup-agreement-icon"
|
|
161
|
+
aria-hidden="true"
|
|
162
|
+
>
|
|
163
|
+
<CheckAgreeIcon />
|
|
164
|
+
</span>
|
|
165
|
+
{renderAgreementLabel(option, required)}
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className="auth-signup-agreement-detail"
|
|
170
|
+
aria-label={`${option.label} 약관 상세 보기`}
|
|
171
|
+
onClick={() => handleOpenAgreementDetail(option.id)}
|
|
172
|
+
>
|
|
173
|
+
<span aria-hidden="true">
|
|
174
|
+
<ChevronOpenDetailIcon />
|
|
175
|
+
</span>
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
</div>
|
|
181
|
+
</section>
|
|
182
|
+
) : null}
|
|
183
|
+
<Button.Default
|
|
184
|
+
type="submit"
|
|
185
|
+
fill="solid"
|
|
186
|
+
size="xlarge"
|
|
187
|
+
priority="primary"
|
|
188
|
+
block
|
|
189
|
+
disabled={disabled || Boolean(submitDisabled)}
|
|
190
|
+
>
|
|
191
|
+
{submitLabel ?? "다음"}
|
|
192
|
+
</Button.Default>
|
|
193
|
+
<DrawerRoot
|
|
194
|
+
open={Boolean(openedAgreement)}
|
|
195
|
+
onOpenChange={isOpen => {
|
|
196
|
+
if (!isOpen) {
|
|
197
|
+
handleCloseDrawer();
|
|
198
|
+
}
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<DrawerPortal>
|
|
202
|
+
<DrawerOverlay />
|
|
203
|
+
<DrawerContent>
|
|
204
|
+
<DrawerHeader>
|
|
205
|
+
<DrawerTitle>{openedAgreement?.label ?? "약관 상세"}</DrawerTitle>
|
|
206
|
+
</DrawerHeader>
|
|
207
|
+
<DrawerBody className="auth-signup-agreement-drawer-body">
|
|
208
|
+
{openedAgreement?.description ?? AGREEMENT_DETAIL_FALLBACK}
|
|
209
|
+
</DrawerBody>
|
|
210
|
+
<DrawerFooter>
|
|
211
|
+
<Button.Default
|
|
212
|
+
type="button"
|
|
213
|
+
fill="solid"
|
|
214
|
+
size="medium"
|
|
215
|
+
priority="primary"
|
|
216
|
+
block
|
|
217
|
+
onClick={handleCloseDrawer}
|
|
218
|
+
>
|
|
219
|
+
닫기
|
|
220
|
+
</Button.Default>
|
|
221
|
+
</DrawerFooter>
|
|
222
|
+
</DrawerContent>
|
|
223
|
+
</DrawerPortal>
|
|
224
|
+
</DrawerRoot>
|
|
225
|
+
</form>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default AuthSignupAgreementForm;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { useFormContext } from "react-hook-form";
|
|
6
|
+
import { Button, Form, Input } from "@uniai-fe/uds-primitives";
|
|
7
|
+
import type { AuthSignupFarmCodeProps, AuthSignupFormValues } from "../types";
|
|
8
|
+
import { useSignupFarmCodeForm } from "../hooks";
|
|
9
|
+
import { getSignupFieldDefaultValue } from "../utils/getSignupFieldDefaultValue";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 회원가입 Step3; 농장 식별번호 입력 폼
|
|
13
|
+
* @component
|
|
14
|
+
* @param {AuthSignupFarmCodeProps} props step props
|
|
15
|
+
* @param {AuthSignupFarmCodeFields} props.fields 필드 설정
|
|
16
|
+
* @param {React.FormHTMLAttributes<HTMLFormElement>} [props.formAttr] form attr
|
|
17
|
+
* @param {import("react").ReactNode} [props.submitLabel] CTA 라벨
|
|
18
|
+
* @param {AuthSignupFarmCodeProps["onSubmit"]} props.onSubmit 제출 핸들러
|
|
19
|
+
*/
|
|
20
|
+
export function AuthSignupFarmCodeForm({
|
|
21
|
+
fields,
|
|
22
|
+
formAttr,
|
|
23
|
+
submitLabel,
|
|
24
|
+
onSubmit,
|
|
25
|
+
}: AuthSignupFarmCodeProps) {
|
|
26
|
+
const form = useFormContext<AuthSignupFormValues>();
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const config = fields.farmCode;
|
|
30
|
+
const fieldName = config.attr?.name ?? "farmCode";
|
|
31
|
+
const currentValue = form.getValues(
|
|
32
|
+
fieldName as keyof AuthSignupFormValues,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
(typeof currentValue !== "string" || currentValue.length === 0) &&
|
|
37
|
+
config.attr?.defaultValue === undefined
|
|
38
|
+
) {
|
|
39
|
+
form.setValue(
|
|
40
|
+
fieldName as keyof AuthSignupFormValues,
|
|
41
|
+
getSignupFieldDefaultValue(config),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}, [fields, form]);
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
register,
|
|
48
|
+
helpers,
|
|
49
|
+
disabled,
|
|
50
|
+
onSubmit: handleSubmit,
|
|
51
|
+
} = useSignupFarmCodeForm({
|
|
52
|
+
fields,
|
|
53
|
+
form,
|
|
54
|
+
onSubmit,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<form
|
|
59
|
+
className={clsx("auth-signup-form", "auth-signup-form-farm-code")}
|
|
60
|
+
{...formAttr}
|
|
61
|
+
onSubmit={handleSubmit}
|
|
62
|
+
>
|
|
63
|
+
<div className="auth-signup-fields">
|
|
64
|
+
<Form.Field.Template
|
|
65
|
+
width={fields.farmCode.template?.width ?? "full"}
|
|
66
|
+
state={helpers.farmCode?.state === "error" ? "error" : undefined}
|
|
67
|
+
className={clsx(
|
|
68
|
+
"auth-signup-field",
|
|
69
|
+
"auth-signup-field-farm-code",
|
|
70
|
+
fields.farmCode.template?.className,
|
|
71
|
+
fields.farmCode.className,
|
|
72
|
+
)}
|
|
73
|
+
headerProps={{
|
|
74
|
+
...fields.farmCode.template?.headerProps,
|
|
75
|
+
label:
|
|
76
|
+
fields.farmCode.template?.headerProps?.label ??
|
|
77
|
+
fields.farmCode.label ??
|
|
78
|
+
"농장 식별번호",
|
|
79
|
+
}}
|
|
80
|
+
footer={
|
|
81
|
+
fields.farmCode.template?.footer ??
|
|
82
|
+
helpers.farmCode?.text ??
|
|
83
|
+
fields.farmCode.helper
|
|
84
|
+
}
|
|
85
|
+
footerProps={{
|
|
86
|
+
...fields.farmCode.template?.footerProps,
|
|
87
|
+
...(helpers.farmCode?.state
|
|
88
|
+
? { "data-state": helpers.farmCode.state }
|
|
89
|
+
: {}),
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<Input.Base
|
|
93
|
+
{...(fields.farmCode.props ?? {})}
|
|
94
|
+
placeholder={fields.farmCode.props?.placeholder ?? "6자리 숫자"}
|
|
95
|
+
register={register.farmCode}
|
|
96
|
+
state={helpers.farmCode?.state ?? fields.farmCode.props?.state}
|
|
97
|
+
block={
|
|
98
|
+
fields.farmCode.block ?? fields.farmCode.props?.block ?? true
|
|
99
|
+
}
|
|
100
|
+
inputMode={fields.farmCode.props?.inputMode ?? "numeric"}
|
|
101
|
+
maxLength={fields.farmCode.props?.maxLength ?? 6}
|
|
102
|
+
pattern={fields.farmCode.props?.pattern ?? "[0-9]*"}
|
|
103
|
+
priority={fields.farmCode.props?.priority ?? "secondary"}
|
|
104
|
+
size={fields.farmCode.props?.size ?? "large"}
|
|
105
|
+
/>
|
|
106
|
+
</Form.Field.Template>
|
|
107
|
+
</div>
|
|
108
|
+
<Button.Default
|
|
109
|
+
type="submit"
|
|
110
|
+
fill="solid"
|
|
111
|
+
size="xlarge"
|
|
112
|
+
priority="primary"
|
|
113
|
+
block
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
>
|
|
116
|
+
{submitLabel ?? "다음"}
|
|
117
|
+
</Button.Default>
|
|
118
|
+
</form>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default AuthSignupFarmCodeForm;
|
|
@@ -3,21 +3,49 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import { AuthContainer } from "../../common/container";
|
|
4
4
|
import type {
|
|
5
5
|
AuthSignupStepIndicatorItem,
|
|
6
|
+
AuthSignupTypeValue,
|
|
6
7
|
AuthSignupTemplateProps,
|
|
7
8
|
} from "../types";
|
|
9
|
+
import { AuthSignupTypeSelectForm } from "./TypeSelectForm";
|
|
8
10
|
import { AuthSignupUserInfoForm } from "./UserInfoForm";
|
|
11
|
+
import { AuthSignupFarmCodeForm } from "./FarmCodeForm";
|
|
12
|
+
import { AuthSignupAgreementForm } from "./AgreementForm";
|
|
9
13
|
import { AuthSignupVerificationForm } from "./VerificationForm";
|
|
10
14
|
import { AuthSignupAccountForm } from "./AccountForm";
|
|
11
15
|
import { AuthSignupComplete } from "./Complete";
|
|
12
16
|
import { AuthStageHeader } from "../../common/container/header";
|
|
13
17
|
|
|
14
|
-
const
|
|
18
|
+
const LEGACY_DEFAULT_STEPS: AuthSignupStepIndicatorItem[] = [
|
|
15
19
|
{ id: "userInfo", label: "기본 정보" },
|
|
16
20
|
{ id: "verifyAgreement", label: "약관 · 인증" },
|
|
17
21
|
{ id: "generateAccount", label: "계정 생성" },
|
|
18
22
|
{ id: "complete", label: "완료" },
|
|
19
23
|
];
|
|
20
24
|
|
|
25
|
+
const NEXT_DEFAULT_STEPS: AuthSignupStepIndicatorItem[] = [
|
|
26
|
+
{ id: "typeSelect", label: "가입 유형" },
|
|
27
|
+
{ id: "identity", label: "본인 확인" },
|
|
28
|
+
{ id: "farmCode", label: "농장 코드" },
|
|
29
|
+
{ id: "agreement", label: "약관 동의" },
|
|
30
|
+
{ id: "account", label: "계정 생성" },
|
|
31
|
+
{ id: "complete", label: "완료" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const resolveDefaultSteps = (
|
|
35
|
+
isNextFlow: boolean,
|
|
36
|
+
signupType?: AuthSignupTypeValue,
|
|
37
|
+
) => {
|
|
38
|
+
if (!isNextFlow) {
|
|
39
|
+
return LEGACY_DEFAULT_STEPS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (signupType === "regionManager") {
|
|
43
|
+
return NEXT_DEFAULT_STEPS.filter(item => item.id !== "farmCode");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return NEXT_DEFAULT_STEPS;
|
|
47
|
+
};
|
|
48
|
+
|
|
21
49
|
/**
|
|
22
50
|
* 회원가입 템플릿; Step1~4 화면을 AuthContainer 위에서 전환한다.
|
|
23
51
|
* @component
|
|
@@ -30,15 +58,26 @@ const DEFAULT_STEPS: AuthSignupStepIndicatorItem[] = [
|
|
|
30
58
|
export function AuthSignupTemplate({
|
|
31
59
|
className,
|
|
32
60
|
header,
|
|
61
|
+
signupType,
|
|
33
62
|
step,
|
|
34
63
|
stepIndicator,
|
|
35
64
|
footer,
|
|
65
|
+
typeSelect,
|
|
66
|
+
identity,
|
|
67
|
+
farmCode,
|
|
68
|
+
agreement,
|
|
36
69
|
userInfo,
|
|
37
70
|
verification,
|
|
38
71
|
account,
|
|
39
72
|
complete,
|
|
40
73
|
}: AuthSignupTemplateProps) {
|
|
41
|
-
const
|
|
74
|
+
const isNextFlow =
|
|
75
|
+
Boolean(typeSelect || identity || farmCode || agreement) ||
|
|
76
|
+
["typeSelect", "identity", "farmCode", "agreement", "account"].includes(
|
|
77
|
+
step,
|
|
78
|
+
);
|
|
79
|
+
const steps =
|
|
80
|
+
stepIndicator?.items ?? resolveDefaultSteps(isNextFlow, signupType);
|
|
42
81
|
const currentStep = stepIndicator?.current ?? step;
|
|
43
82
|
const defaultStepIndex = Math.max(
|
|
44
83
|
0,
|
|
@@ -72,12 +111,29 @@ export function AuthSignupTemplate({
|
|
|
72
111
|
|
|
73
112
|
let content: ReactNode = null;
|
|
74
113
|
switch (currentStep) {
|
|
114
|
+
case "typeSelect":
|
|
115
|
+
content = typeSelect ? (
|
|
116
|
+
<AuthSignupTypeSelectForm {...typeSelect} />
|
|
117
|
+
) : null;
|
|
118
|
+
break;
|
|
119
|
+
case "identity":
|
|
120
|
+
content = <AuthSignupUserInfoForm {...(identity ?? userInfo)} />;
|
|
121
|
+
break;
|
|
122
|
+
case "farmCode":
|
|
123
|
+
content = farmCode ? <AuthSignupFarmCodeForm {...farmCode} /> : null;
|
|
124
|
+
break;
|
|
125
|
+
case "agreement":
|
|
126
|
+
content = agreement ? <AuthSignupAgreementForm {...agreement} /> : null;
|
|
127
|
+
break;
|
|
75
128
|
case "userInfo":
|
|
76
129
|
content = <AuthSignupUserInfoForm {...userInfo} />;
|
|
77
130
|
break;
|
|
78
131
|
case "verifyAgreement":
|
|
79
132
|
content = <AuthSignupVerificationForm {...verification} />;
|
|
80
133
|
break;
|
|
134
|
+
case "account":
|
|
135
|
+
content = <AuthSignupAccountForm {...account} />;
|
|
136
|
+
break;
|
|
81
137
|
case "generateAccount":
|
|
82
138
|
content = <AuthSignupAccountForm {...account} />;
|
|
83
139
|
break;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { useFormContext } from "react-hook-form";
|
|
5
|
+
import { Button } from "@uniai-fe/uds-primitives";
|
|
6
|
+
import type { AuthSignupFormValues, AuthSignupTypeSelectProps } from "../types";
|
|
7
|
+
import { useSignupTypeSelectForm } from "../hooks";
|
|
8
|
+
import CheckIcon from "../img/select-user-type-default.svg";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 회원가입 Step1; 가입 유형 선택 폼
|
|
12
|
+
* @component
|
|
13
|
+
* @param {AuthSignupTypeSelectProps} props step props
|
|
14
|
+
* @param {AuthSignupTypeSelectOption[]} props.options 선택 옵션 목록
|
|
15
|
+
* @param {React.FormHTMLAttributes<HTMLFormElement>} [props.formAttr] form attr
|
|
16
|
+
* @param {import("react").ReactNode} [props.submitLabel] CTA 라벨
|
|
17
|
+
* @param {AuthSignupTypeSelectProps["onSubmit"]} props.onSubmit 제출 핸들러
|
|
18
|
+
*/
|
|
19
|
+
export function AuthSignupTypeSelectForm({
|
|
20
|
+
options,
|
|
21
|
+
formAttr,
|
|
22
|
+
submitLabel,
|
|
23
|
+
onSubmit,
|
|
24
|
+
}: AuthSignupTypeSelectProps) {
|
|
25
|
+
const form = useFormContext<AuthSignupFormValues>();
|
|
26
|
+
const {
|
|
27
|
+
selectedType,
|
|
28
|
+
onValueChange,
|
|
29
|
+
disabled,
|
|
30
|
+
onSubmit: handleSubmit,
|
|
31
|
+
} = useSignupTypeSelectForm({
|
|
32
|
+
form,
|
|
33
|
+
onSubmit,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<form
|
|
38
|
+
className={clsx("auth-signup-form", "auth-signup-form-type-select")}
|
|
39
|
+
{...formAttr}
|
|
40
|
+
onSubmit={handleSubmit}
|
|
41
|
+
>
|
|
42
|
+
<div className="auth-signup-type-options" role="radiogroup">
|
|
43
|
+
{options.map(option => {
|
|
44
|
+
const isSelected = selectedType === option.id;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="auth-signup-type-option-group" key={option.id}>
|
|
48
|
+
<p className="auth-signup-type-question">{option.question}</p>
|
|
49
|
+
<Button.Default
|
|
50
|
+
className="auth-signup-type-option"
|
|
51
|
+
role="radio"
|
|
52
|
+
aria-checked={isSelected}
|
|
53
|
+
data-selected={isSelected ? "true" : "false"}
|
|
54
|
+
data-user-action={isSelected ? "hover" : undefined}
|
|
55
|
+
disabled={option.disabled}
|
|
56
|
+
fill="outlined"
|
|
57
|
+
size="xlarge"
|
|
58
|
+
priority={isSelected ? "primary" : "tertiary"}
|
|
59
|
+
block
|
|
60
|
+
left={
|
|
61
|
+
// 단일 svg asset을 currentColor 기반으로 재사용해 선택 상태를 버튼 foreground와 동기화한다.
|
|
62
|
+
<CheckIcon
|
|
63
|
+
width={24}
|
|
64
|
+
height={24}
|
|
65
|
+
viewBox="0 0 24 24"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
/>
|
|
68
|
+
}
|
|
69
|
+
onClick={() => {
|
|
70
|
+
if (!option.disabled) {
|
|
71
|
+
onValueChange(option.id);
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<span className="auth-signup-type-option-label">
|
|
76
|
+
{option.label}
|
|
77
|
+
</span>
|
|
78
|
+
</Button.Default>
|
|
79
|
+
{option.description ? (
|
|
80
|
+
<p className="auth-signup-type-description">
|
|
81
|
+
{option.description}
|
|
82
|
+
</p>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</div>
|
|
88
|
+
<Button.Default
|
|
89
|
+
type="submit"
|
|
90
|
+
fill="solid"
|
|
91
|
+
size="xlarge"
|
|
92
|
+
priority="primary"
|
|
93
|
+
block
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
>
|
|
96
|
+
{submitLabel ?? "다음"}
|
|
97
|
+
</Button.Default>
|
|
98
|
+
</form>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default AuthSignupTypeSelectForm;
|
|
@@ -71,6 +71,11 @@ export function AuthSignupUserInfoForm({
|
|
|
71
71
|
{renderedFields.includes("name") ? (
|
|
72
72
|
<Form.Field.Template
|
|
73
73
|
width={fields.name.template?.width ?? "full"}
|
|
74
|
+
state={
|
|
75
|
+
(helpers.name?.state ?? fields.name.props?.state) === "error"
|
|
76
|
+
? "error"
|
|
77
|
+
: undefined
|
|
78
|
+
}
|
|
74
79
|
className={clsx(
|
|
75
80
|
"auth-signup-field",
|
|
76
81
|
"auth-signup-field-name",
|
|
@@ -109,6 +114,10 @@ export function AuthSignupUserInfoForm({
|
|
|
109
114
|
<AuthCodePhoneTemplate
|
|
110
115
|
templateProps={{
|
|
111
116
|
width: fields.phone.template?.width ?? "full",
|
|
117
|
+
state:
|
|
118
|
+
(helpers.phone?.state ?? fields.phone.props?.state) === "error"
|
|
119
|
+
? "error"
|
|
120
|
+
: undefined,
|
|
112
121
|
className: clsx(
|
|
113
122
|
"auth-signup-field",
|
|
114
123
|
"auth-signup-field-phone",
|
|
@@ -53,6 +53,13 @@ const resolveCodeValue = (inputProps?: AuthCodeInputProps): string => {
|
|
|
53
53
|
return "";
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
const AGREEMENT_DETAIL_FALLBACK = (
|
|
57
|
+
<>
|
|
58
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
|
|
59
|
+
<p>Nulla facilisi. Integer porta, nisl at volutpat posuere, erat arcu.</p>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
|
|
56
63
|
/**
|
|
57
64
|
* 회원가입 Step2; 약관 동의 + 이메일 인증
|
|
58
65
|
* @component
|
|
@@ -145,6 +152,7 @@ export function AuthSignupVerificationForm({
|
|
|
145
152
|
const emailFooterProps = fields.email.template?.footerProps ?? {};
|
|
146
153
|
const emailTemplateProps: AuthFormFieldTemplateProps = {
|
|
147
154
|
width: fields.email.template?.width ?? "full",
|
|
155
|
+
state: helpers.email?.state === "error" ? "error" : undefined,
|
|
148
156
|
className: clsx(
|
|
149
157
|
"auth-signup-field",
|
|
150
158
|
"auth-signup-field-email",
|
|
@@ -301,14 +309,20 @@ export function AuthSignupVerificationForm({
|
|
|
301
309
|
</div>
|
|
302
310
|
{agreements.length ? (
|
|
303
311
|
<section className="auth-signup-agreements" aria-label="약관 동의">
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
+
{/* all-agree는 checkbox primitive를 유지하고 step3 카드 스타일만 signup에서 덮어쓴다. */}
|
|
313
|
+
<CheckboxField
|
|
314
|
+
size="large"
|
|
315
|
+
checked={allRequiredChecked}
|
|
316
|
+
onCheckedChange={() => onToggleAll({ requiredOnly: true })}
|
|
317
|
+
fieldClassName="auth-signup-agreement-all-field"
|
|
318
|
+
labelWrapperClassName="auth-signup-agreement-all"
|
|
319
|
+
className="auth-signup-agreement-all-checkbox"
|
|
320
|
+
label={
|
|
321
|
+
<span className="auth-signup-agreement-all-label">
|
|
322
|
+
필수 약관에 모두 동의하기
|
|
323
|
+
</span>
|
|
324
|
+
}
|
|
325
|
+
/>
|
|
312
326
|
<div className="auth-signup-agreements-list">
|
|
313
327
|
{agreements.map(option => {
|
|
314
328
|
const checked = Boolean(agreementState[option.id]);
|
|
@@ -320,7 +334,7 @@ export function AuthSignupVerificationForm({
|
|
|
320
334
|
data-required={option.required ? "true" : undefined}
|
|
321
335
|
data-checked={checked ? "true" : undefined}
|
|
322
336
|
>
|
|
323
|
-
{/* 약관
|
|
337
|
+
{/* 하위 약관 row는 primitive checkbox가 아니라 커스텀 check icon을 유지한다. */}
|
|
324
338
|
<button
|
|
325
339
|
type="button"
|
|
326
340
|
className="auth-signup-agreement-toggle"
|
|
@@ -336,18 +350,16 @@ export function AuthSignupVerificationForm({
|
|
|
336
350
|
</span>
|
|
337
351
|
{renderAgreementLabel(option)}
|
|
338
352
|
</button>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
>
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
</button>
|
|
350
|
-
) : null}
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
className="auth-signup-agreement-detail"
|
|
356
|
+
aria-label={`${option.label} 약관 상세 보기`}
|
|
357
|
+
onClick={() => handleOpenAgreementDetail(option.id)}
|
|
358
|
+
>
|
|
359
|
+
<span aria-hidden="true">
|
|
360
|
+
<ChevronOpenDetailIcon />
|
|
361
|
+
</span>
|
|
362
|
+
</button>
|
|
351
363
|
</div>
|
|
352
364
|
);
|
|
353
365
|
})}
|
|
@@ -378,11 +390,9 @@ export function AuthSignupVerificationForm({
|
|
|
378
390
|
<DrawerHeader>
|
|
379
391
|
<DrawerTitle>{openedAgreement?.label ?? "약관 상세"}</DrawerTitle>
|
|
380
392
|
</DrawerHeader>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
</DrawerBody>
|
|
385
|
-
) : null}
|
|
393
|
+
<DrawerBody className="auth-signup-agreement-drawer-body">
|
|
394
|
+
{openedAgreement?.description ?? AGREEMENT_DETAIL_FALLBACK}
|
|
395
|
+
</DrawerBody>
|
|
386
396
|
<DrawerFooter>
|
|
387
397
|
<Button.Default
|
|
388
398
|
type="button"
|