@uniai-fe/uds-templates 0.4.33 → 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 +93 -19
- 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/dist/styles.css
CHANGED
|
@@ -1229,6 +1229,62 @@
|
|
|
1229
1229
|
gap: var(--spacing-padding-5);
|
|
1230
1230
|
}
|
|
1231
1231
|
|
|
1232
|
+
.auth-signup-type-options {
|
|
1233
|
+
display: flex;
|
|
1234
|
+
flex-direction: column;
|
|
1235
|
+
gap: var(--spacing-padding-6);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.auth-signup-type-option-group {
|
|
1239
|
+
display: flex;
|
|
1240
|
+
flex-direction: column;
|
|
1241
|
+
gap: var(--spacing-padding-3);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.auth-signup-type-question {
|
|
1245
|
+
margin: 0;
|
|
1246
|
+
color: var(--color-label-standard);
|
|
1247
|
+
font-size: var(--font-heading-xxsmall-size);
|
|
1248
|
+
line-height: 1.4;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
.auth-signup-type-option {
|
|
1252
|
+
display: flex;
|
|
1253
|
+
justify-content: flex-start;
|
|
1254
|
+
text-align: left;
|
|
1255
|
+
--button-default-padding-inline-large: 16px;
|
|
1256
|
+
--button-default-tertiary-outline-foreground: var(--color-label-alternative);
|
|
1257
|
+
}
|
|
1258
|
+
.auth-signup-type-option .button-left {
|
|
1259
|
+
display: flex;
|
|
1260
|
+
width: 24px;
|
|
1261
|
+
height: 24px;
|
|
1262
|
+
align-items: center;
|
|
1263
|
+
justify-content: center;
|
|
1264
|
+
flex-shrink: 0;
|
|
1265
|
+
}
|
|
1266
|
+
.auth-signup-type-option .button-left svg {
|
|
1267
|
+
display: block;
|
|
1268
|
+
width: 24px;
|
|
1269
|
+
height: 24px;
|
|
1270
|
+
}
|
|
1271
|
+
.auth-signup-type-option .button-label {
|
|
1272
|
+
flex: 1;
|
|
1273
|
+
text-align: left;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.auth-signup-type-option-label {
|
|
1277
|
+
font-size: var(--font-body-large-size);
|
|
1278
|
+
line-height: 1.5;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.auth-signup-type-description {
|
|
1282
|
+
margin: 0;
|
|
1283
|
+
color: var(--color-label-assistive);
|
|
1284
|
+
font-size: var(--font-body-xxsmall-size);
|
|
1285
|
+
line-height: 1.5;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1232
1288
|
.auth-signup-verification {
|
|
1233
1289
|
display: flex;
|
|
1234
1290
|
flex-direction: column;
|
|
@@ -1238,34 +1294,47 @@
|
|
|
1238
1294
|
.auth-signup-agreements {
|
|
1239
1295
|
display: flex;
|
|
1240
1296
|
flex-direction: column;
|
|
1241
|
-
gap:
|
|
1297
|
+
gap: 12px;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
.auth-signup-agreement-all-field {
|
|
1301
|
+
gap: 0;
|
|
1242
1302
|
}
|
|
1243
1303
|
|
|
1244
1304
|
.auth-signup-agreement-all {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
align-items: center;
|
|
1305
|
+
width: 100%;
|
|
1306
|
+
min-height: 56px;
|
|
1248
1307
|
background: var(--color-bg-alternative-cool-gray, #f2f2f3);
|
|
1249
|
-
border-radius:
|
|
1250
|
-
|
|
1251
|
-
|
|
1308
|
+
border-radius: 12px;
|
|
1309
|
+
padding: 8px 12px;
|
|
1310
|
+
gap: 6px;
|
|
1311
|
+
align-items: center;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
.auth-signup-agreement-all-checkbox {
|
|
1315
|
+
flex-shrink: 0;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.auth-signup-agreement-all-label {
|
|
1319
|
+
color: var(--color-label-standard);
|
|
1320
|
+
font-size: var(--font-body-large-size);
|
|
1321
|
+
font-weight: 500;
|
|
1322
|
+
line-height: 1.5;
|
|
1252
1323
|
}
|
|
1253
1324
|
|
|
1254
1325
|
.auth-signup-agreements-list {
|
|
1255
1326
|
width: 100%;
|
|
1256
|
-
background: var(--color-common-100);
|
|
1257
|
-
border-radius: var(--theme-radius-medium-3, 8px);
|
|
1258
|
-
padding: var(--spacing-padding-3);
|
|
1259
1327
|
display: flex;
|
|
1260
1328
|
flex-direction: column;
|
|
1261
|
-
gap:
|
|
1329
|
+
gap: 12px;
|
|
1330
|
+
padding: 4px 12px;
|
|
1262
1331
|
}
|
|
1263
1332
|
|
|
1264
1333
|
.auth-signup-agreement-row {
|
|
1265
1334
|
display: flex;
|
|
1266
1335
|
align-items: center;
|
|
1267
1336
|
justify-content: space-between;
|
|
1268
|
-
gap:
|
|
1337
|
+
gap: 8px;
|
|
1269
1338
|
}
|
|
1270
1339
|
|
|
1271
1340
|
.auth-signup-agreement-toggle {
|
|
@@ -1275,12 +1344,13 @@
|
|
|
1275
1344
|
margin: 0;
|
|
1276
1345
|
display: flex;
|
|
1277
1346
|
align-items: center;
|
|
1278
|
-
gap:
|
|
1347
|
+
gap: 8px;
|
|
1279
1348
|
cursor: pointer;
|
|
1280
1349
|
flex: 1;
|
|
1281
1350
|
text-align: left;
|
|
1282
1351
|
flex-wrap: nowrap;
|
|
1283
1352
|
font-size: 14px;
|
|
1353
|
+
min-width: 0;
|
|
1284
1354
|
}
|
|
1285
1355
|
|
|
1286
1356
|
.auth-signup-agreement-icon {
|
|
@@ -1300,18 +1370,19 @@
|
|
|
1300
1370
|
|
|
1301
1371
|
.auth-signup-agreement-toggle[data-checked=true] .auth-signup-agreement-icon {
|
|
1302
1372
|
color: var(--color-primary-default);
|
|
1303
|
-
border-color: var(--color-primary-default);
|
|
1304
1373
|
}
|
|
1305
1374
|
|
|
1306
1375
|
.auth-signup-agreement-label {
|
|
1307
|
-
display:
|
|
1308
|
-
gap:
|
|
1376
|
+
display: flex;
|
|
1377
|
+
gap: 0;
|
|
1309
1378
|
align-items: baseline;
|
|
1310
1379
|
flex-wrap: nowrap;
|
|
1380
|
+
min-width: 0;
|
|
1311
1381
|
}
|
|
1312
1382
|
|
|
1313
1383
|
.auth-signup-agreement-badge {
|
|
1314
1384
|
font-size: 14px;
|
|
1385
|
+
font-weight: 400;
|
|
1315
1386
|
color: var(--color-primary-default);
|
|
1316
1387
|
}
|
|
1317
1388
|
.auth-signup-agreement-badge[data-required=false] {
|
|
@@ -1322,7 +1393,9 @@
|
|
|
1322
1393
|
font-size: 14px;
|
|
1323
1394
|
color: var(--color-label-standard);
|
|
1324
1395
|
font-weight: 400;
|
|
1325
|
-
line-height: 1.
|
|
1396
|
+
line-height: 1.5;
|
|
1397
|
+
overflow: hidden;
|
|
1398
|
+
text-overflow: ellipsis;
|
|
1326
1399
|
}
|
|
1327
1400
|
|
|
1328
1401
|
.auth-signup-agreement-description {
|
|
@@ -1343,11 +1416,12 @@
|
|
|
1343
1416
|
justify-content: center;
|
|
1344
1417
|
width: 20px;
|
|
1345
1418
|
height: 20px;
|
|
1419
|
+
flex-shrink: 0;
|
|
1346
1420
|
}
|
|
1347
1421
|
.auth-signup-agreement-detail svg {
|
|
1348
1422
|
display: block;
|
|
1349
|
-
width:
|
|
1350
|
-
height:
|
|
1423
|
+
width: 16px;
|
|
1424
|
+
height: 16px;
|
|
1351
1425
|
}
|
|
1352
1426
|
|
|
1353
1427
|
.auth-signup-agreement-detail:disabled {
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type {
|
|
5
|
+
UseSignupAgreementFormOptions,
|
|
6
|
+
UseSignupAgreementFormReturn,
|
|
7
|
+
} from "../types";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_THIRD_PARTY_AGREEMENT_ID = "thirdParty";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 회원가입 Step4 훅; 약관 동의 상태 계산
|
|
13
|
+
* @hook
|
|
14
|
+
* @param {UseSignupAgreementFormOptions} options 훅 옵션
|
|
15
|
+
* @desc
|
|
16
|
+
* - 필수 약관 id 집합과 CTA 활성화 여부를 계산한다.
|
|
17
|
+
*/
|
|
18
|
+
export function useSignupAgreementForm({
|
|
19
|
+
agreements,
|
|
20
|
+
agreementState,
|
|
21
|
+
isThirdPartyRequired,
|
|
22
|
+
thirdPartyAgreementId = DEFAULT_THIRD_PARTY_AGREEMENT_ID,
|
|
23
|
+
}: UseSignupAgreementFormOptions): UseSignupAgreementFormReturn {
|
|
24
|
+
const requiredAgreementIds = useMemo(() => {
|
|
25
|
+
return agreements
|
|
26
|
+
.filter(option => {
|
|
27
|
+
if (option.required) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return isThirdPartyRequired && option.id === thirdPartyAgreementId;
|
|
32
|
+
})
|
|
33
|
+
.map(option => option.id);
|
|
34
|
+
}, [agreements, isThirdPartyRequired, thirdPartyAgreementId]);
|
|
35
|
+
|
|
36
|
+
const allRequiredChecked =
|
|
37
|
+
requiredAgreementIds.length > 0
|
|
38
|
+
? requiredAgreementIds.every(id => Boolean(agreementState[id]))
|
|
39
|
+
: true;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
requiredAgreementIds,
|
|
43
|
+
allRequiredChecked,
|
|
44
|
+
disabled: !allRequiredChecked,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default useSignupAgreementForm;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useWatch } from "react-hook-form";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import type {
|
|
7
|
+
AuthSignupFarmCodeFields,
|
|
8
|
+
AuthSignupFarmCodeValues,
|
|
9
|
+
AuthSignupFieldProps,
|
|
10
|
+
AuthSignupFormValues,
|
|
11
|
+
UseSignupFarmCodeFormOptions,
|
|
12
|
+
UseSignupFarmCodeFormReturn,
|
|
13
|
+
} from "../types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 회원가입 Step3 훅; 농장 식별번호 입력
|
|
17
|
+
* @hook
|
|
18
|
+
* @template TFields
|
|
19
|
+
* @param {UseSignupFarmCodeFormOptions<TFields>} options 훅 옵션
|
|
20
|
+
* @desc
|
|
21
|
+
* - 1) form init → 2) register merge → 3) helper/state → 4) submit 순서
|
|
22
|
+
*/
|
|
23
|
+
export function useSignupFarmCodeForm<
|
|
24
|
+
TFields extends Record<string, AuthSignupFieldProps> =
|
|
25
|
+
AuthSignupFarmCodeFields,
|
|
26
|
+
>({
|
|
27
|
+
fields,
|
|
28
|
+
form,
|
|
29
|
+
onSubmit,
|
|
30
|
+
}: UseSignupFarmCodeFormOptions<TFields>): UseSignupFarmCodeFormReturn<TFields> {
|
|
31
|
+
const values = useWatch({ control: form.control }) as
|
|
32
|
+
| AuthSignupFormValues
|
|
33
|
+
| undefined;
|
|
34
|
+
|
|
35
|
+
const register = useMemo(() => {
|
|
36
|
+
return (Object.keys(fields) as Array<keyof TFields>).reduce(
|
|
37
|
+
(acc, fieldKey) => {
|
|
38
|
+
const config = fields[fieldKey] as AuthSignupFieldProps;
|
|
39
|
+
const fieldName = config.attr?.name ?? String(fieldKey);
|
|
40
|
+
acc[fieldKey] = form.register(fieldName);
|
|
41
|
+
return acc;
|
|
42
|
+
},
|
|
43
|
+
{} as UseSignupFarmCodeFormReturn<TFields>["register"],
|
|
44
|
+
);
|
|
45
|
+
}, [fields, form]);
|
|
46
|
+
|
|
47
|
+
const helpers = useMemo(() => {
|
|
48
|
+
return (Object.keys(fields) as Array<keyof TFields>).reduce(
|
|
49
|
+
(acc, fieldKey) => {
|
|
50
|
+
const config = fields[fieldKey] as AuthSignupFieldProps;
|
|
51
|
+
const fieldName = config.attr?.name ?? String(fieldKey);
|
|
52
|
+
const state = form.getFieldState(fieldName);
|
|
53
|
+
acc[fieldKey] = {
|
|
54
|
+
text:
|
|
55
|
+
(state.error?.message as ReactNode | undefined) ?? config.helper,
|
|
56
|
+
state: state.invalid ? "error" : undefined,
|
|
57
|
+
};
|
|
58
|
+
return acc;
|
|
59
|
+
},
|
|
60
|
+
{} as UseSignupFarmCodeFormReturn<TFields>["helpers"],
|
|
61
|
+
);
|
|
62
|
+
}, [fields, form]);
|
|
63
|
+
|
|
64
|
+
const farmCodeFieldName =
|
|
65
|
+
fields.farmCode.attr?.name ??
|
|
66
|
+
("farmCode" as keyof AuthSignupFarmCodeValues);
|
|
67
|
+
const farmCodeValue =
|
|
68
|
+
values?.[farmCodeFieldName as keyof AuthSignupFormValues] ?? "";
|
|
69
|
+
const normalizedFarmCode =
|
|
70
|
+
typeof farmCodeValue === "string"
|
|
71
|
+
? farmCodeValue.replace(/\D/g, "")
|
|
72
|
+
: String(farmCodeValue ?? "").replace(/\D/g, "");
|
|
73
|
+
|
|
74
|
+
const disabled =
|
|
75
|
+
form.formState.isSubmitting || !/^\d{6}$/.test(normalizedFarmCode);
|
|
76
|
+
|
|
77
|
+
const onSubmitHandler = form.handleSubmit(onSubmit);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
register,
|
|
81
|
+
helpers,
|
|
82
|
+
disabled,
|
|
83
|
+
onSubmit: onSubmitHandler,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default useSignupFarmCodeForm;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect } from "react";
|
|
4
|
+
import { useWatch } from "react-hook-form";
|
|
5
|
+
import type {
|
|
6
|
+
AuthSignupFormValues,
|
|
7
|
+
AuthSignupTypeSelectValues,
|
|
8
|
+
AuthSignupTypeValue,
|
|
9
|
+
UseSignupTypeSelectFormOptions,
|
|
10
|
+
UseSignupTypeSelectFormReturn,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 회원가입 Step1 훅; 가입 유형 선택
|
|
15
|
+
* @hook
|
|
16
|
+
* @param {UseSignupTypeSelectFormOptions} options 훅 옵션
|
|
17
|
+
* @desc
|
|
18
|
+
* - 1) form init → 2) register merge → 3) state/helper → 4) submit 순서
|
|
19
|
+
*/
|
|
20
|
+
export function useSignupTypeSelectForm({
|
|
21
|
+
form,
|
|
22
|
+
onSubmit,
|
|
23
|
+
}: UseSignupTypeSelectFormOptions): UseSignupTypeSelectFormReturn {
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// 가입 유형은 커스텀 버튼으로 변경되므로 RHF에 명시적으로 등록한다.
|
|
26
|
+
form.register("signupType");
|
|
27
|
+
}, [form]);
|
|
28
|
+
|
|
29
|
+
const selectedType = useWatch({
|
|
30
|
+
control: form.control,
|
|
31
|
+
name: "signupType",
|
|
32
|
+
}) as AuthSignupTypeValue | undefined;
|
|
33
|
+
|
|
34
|
+
const handleValueChange = useCallback(
|
|
35
|
+
(value: AuthSignupTypeValue) => {
|
|
36
|
+
form.setValue("signupType", value, {
|
|
37
|
+
shouldDirty: true,
|
|
38
|
+
shouldTouch: true,
|
|
39
|
+
shouldValidate: true,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
[form],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const disabled = form.formState.isSubmitting || !selectedType;
|
|
46
|
+
const onSubmitHandler = form.handleSubmit(onSubmit);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
selectedType,
|
|
50
|
+
onValueChange: handleValueChange,
|
|
51
|
+
disabled,
|
|
52
|
+
onSubmit: onSubmitHandler,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default useSignupTypeSelectForm;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="24"
|
|
3
|
+
height="24"
|
|
4
|
+
viewBox="0 0 24 24"
|
|
5
|
+
fill="none"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
>
|
|
8
|
+
<path
|
|
9
|
+
d="M4 10.8995L9.65685 16.5563L19.5563 6.65683"
|
|
10
|
+
stroke="currentColor"
|
|
11
|
+
stroke-width="1.6"
|
|
12
|
+
stroke-linecap="round"
|
|
13
|
+
stroke-linejoin="round"
|
|
14
|
+
/>
|
|
15
|
+
</svg>
|
package/src/auth/signup/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AuthSignupTypeSelectForm,
|
|
2
3
|
AuthSignupUserInfoForm,
|
|
4
|
+
AuthSignupFarmCodeForm,
|
|
5
|
+
AuthSignupAgreementForm,
|
|
3
6
|
AuthSignupVerificationForm,
|
|
4
7
|
AuthSignupAccountForm,
|
|
5
8
|
AuthSignupComplete,
|
|
@@ -12,8 +15,11 @@ import "./styles/signup.scss";
|
|
|
12
15
|
export type * from "./types";
|
|
13
16
|
export * from "./hooks";
|
|
14
17
|
export {
|
|
18
|
+
AuthSignupTypeSelectForm,
|
|
15
19
|
AuthSignupProvider,
|
|
16
20
|
AuthSignupUserInfoForm,
|
|
21
|
+
AuthSignupFarmCodeForm,
|
|
22
|
+
AuthSignupAgreementForm,
|
|
17
23
|
AuthSignupVerificationForm,
|
|
18
24
|
AuthSignupAccountForm,
|
|
19
25
|
AuthSignupComplete,
|
|
@@ -23,6 +29,10 @@ export {
|
|
|
23
29
|
export const AuthSignup = {
|
|
24
30
|
Provider: AuthSignupProvider,
|
|
25
31
|
Template: AuthSignupTemplate,
|
|
32
|
+
StepTypeSelect: AuthSignupTypeSelectForm,
|
|
33
|
+
StepIdentity: AuthSignupUserInfoForm,
|
|
34
|
+
StepFarmCode: AuthSignupFarmCodeForm,
|
|
35
|
+
StepAgreement: AuthSignupAgreementForm,
|
|
26
36
|
StepUserInfo: AuthSignupUserInfoForm,
|
|
27
37
|
StepVerification: AuthSignupVerificationForm,
|
|
28
38
|
StepAccount: AuthSignupAccountForm,
|
|
@@ -86,6 +86,7 @@ export function AuthSignupAccountForm({
|
|
|
86
86
|
<div className="auth-signup-fields">
|
|
87
87
|
<Form.Field.Template
|
|
88
88
|
width={fields.accountId.template?.width ?? "full"}
|
|
89
|
+
state={helpers.accountId?.state === "error" ? "error" : undefined}
|
|
89
90
|
className={clsx(
|
|
90
91
|
"auth-signup-field",
|
|
91
92
|
"auth-signup-field-account-id",
|
|
@@ -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;
|