-
Notifications
You must be signed in to change notification settings - Fork 2
feat: SP1 회원가입 페이지 디자인 반영사항 적용 #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
3328d19
79322bb
7273a6e
e8bc3dd
8160f0b
28ec19e
bd1ef2e
6fb01b3
74e5914
cade871
96be92b
b10c2f3
ea2a96d
3ec2de8
76ac12c
2ebc285
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import Button from '@components/button/button/button'; | ||
| import Icon from '@components/icon/icon'; | ||
| import CheckboxRow from '@pages/sign-up/components/checkbox-row'; | ||
| import { useState } from 'react'; | ||
|
|
||
| interface AgreementStepProps { | ||
| next: () => void; | ||
| } | ||
|
|
||
| const AgreementStep = ({ next }: AgreementStepProps) => { | ||
| const [terms, setTerms] = useState(false); | ||
| const [privacy, setPrivacy] = useState(false); | ||
|
|
||
| const isAllChecked = terms && privacy; | ||
|
|
||
| const handleCheckAll = () => { | ||
| const next = !isAllChecked; | ||
| setTerms(next); | ||
| setPrivacy(next); | ||
| }; | ||
|
|
||
| const handleCheckTerms = () => { | ||
| const next = !terms; | ||
| setTerms(next); | ||
| }; | ||
|
|
||
| const handleCheckPrivacy = () => { | ||
| const next = !privacy; | ||
| setPrivacy(next); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="h-full flex-col justify-between pt-[4.8rem] pb-[2.4rem]"> | ||
| <div className="flex-col gap-[0.8rem] px-[1.6rem]"> | ||
| <h1 className="title_24_sb">서비스 이용약관</h1> | ||
| <p className="body_16_m text-gray-500"> | ||
| 서비스 가입을 위해 <br /> 아래 항목에 동의해주세요. | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="flex-col gap-[2.4rem]"> | ||
| <div className="flex-col gap-[0.8rem]"> | ||
| <CheckboxRow | ||
| label="약관 전체 동의" | ||
| onClick={handleCheckAll} | ||
| checked={isAllChecked} | ||
| divider | ||
| /> | ||
| <CheckboxRow | ||
| label="이용약관 동의 (필수)" | ||
| onClick={handleCheckTerms} | ||
| checked={terms} | ||
| svg={<Icon name="arrow-right-18" size={1.8} />} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클릭시 이동할 주소는 아직 전달 못받은거져??
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네! 여기도 페이지로 퍼블리싱 작업 들어갈 것 같아요 ㅠㅠ |
||
| /> | ||
|
|
||
| <CheckboxRow | ||
| label="개인정보 수집 및 이용동의 (필수)" | ||
| onClick={handleCheckPrivacy} | ||
| checked={privacy} | ||
| svg={<Icon name="arrow-right-18" size={1.8} />} | ||
| /> | ||
| </div> | ||
| <div className="px-[1.6rem]"> | ||
| <Button label="다음으로" disabled={!isAllChecked} onClick={next} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default AgreementStep; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import Icon from '@components/icon/icon'; | ||
| import { cn } from '@libs/cn'; | ||
| import type { ReactNode } from 'react'; | ||
|
|
||
| interface CheckboxProps { | ||
| label: ReactNode; | ||
| checked: boolean; | ||
| onClick: () => void; | ||
| svg?: ReactNode; | ||
| divider?: boolean; | ||
| className?: string; | ||
| } | ||
|
|
||
| const CheckboxRow = ({ label, checked, onClick, svg, divider, className }: CheckboxProps) => { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| className={cn( | ||
| 'flex w-full items-center justify-between gap-[0.8rem] p-[0.8rem] px-[1.6rem] text-left', | ||
| divider && 'border-gray-200 border-b', | ||
| className, | ||
| )} | ||
| > | ||
heesunee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <div className="flex items-center gap-[0.8rem]"> | ||
| <Icon | ||
| name="check-filled" | ||
| onClick={onClick} | ||
|
||
| className={checked ? 'text-main-800' : 'text-gray-300'} | ||
| /> | ||
| <span className="body_16_m">{label}</span> | ||
| </div> | ||
| {svg} | ||
| </button> | ||
| ); | ||
| }; | ||
|
|
||
| export default CheckboxRow; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,17 @@ import queryClient from '@libs/query-client'; | |
| import { | ||
| BIRTHYEAR_RULE_MESSAGE, | ||
| BIRTHYEAR_SUCCESS_MESSAGE, | ||
| INFORMATION_RULE_MESSAGE, | ||
| NICKNAME_RULE_MESSAGE, | ||
| NICKNAME_SUCCESS_MESSAGE, | ||
| NICKNAME_TITLE, | ||
| } from '@pages/sign-up/constants/NOTICE'; | ||
| import { BIRTH_PLACEHOLDER, NICKNAME_PLACEHOLDER } from '@pages/sign-up/constants/validation'; | ||
| import { | ||
| BIRTH_PLACEHOLDER, | ||
| INFORMATION_MAX_LENGTH, | ||
| INFORMATION_PLACEHOLDER, | ||
| NICKNAME_PLACEHOLDER, | ||
| } from '@pages/sign-up/constants/validation'; | ||
| import { type NicknameFormValues, NicknameSchema } from '@pages/sign-up/schema/validation-schema'; | ||
| import { ROUTES } from '@routes/routes-config'; | ||
| import { useMutation } from '@tanstack/react-query'; | ||
|
|
@@ -28,21 +34,25 @@ const SignupStep = () => { | |
| } = useForm<NicknameFormValues>({ | ||
| mode: 'onChange', | ||
| resolver: zodResolver(NicknameSchema), | ||
| defaultValues: { nickname: '', gender: undefined, birthYear: '' }, | ||
| defaultValues: { nickname: '', gender: undefined, birthYear: '', information: '' }, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 한 줄 소개 항목이 추가가 되어서 form 에도 등록해두었습니다. 서버 api는 아직 업데이트가 안돼서 api 수정은 안되어있어용 |
||
| }); | ||
|
|
||
| const navigate = useNavigate(); | ||
|
|
||
| const nicknameValue = watch('nickname'); | ||
| const birthYearValue = watch('birthYear'); | ||
| const genderValue = watch('gender'); | ||
| const informationValue = watch('information'); | ||
|
|
||
| const isNicknameValid = !errors.nickname && nicknameValue.length > 0; | ||
| const isBirthYearValid = !errors.birthYear && birthYearValue.length > 0; | ||
| const isInformationValid = !errors.information && informationValue.length > 0; | ||
|
|
||
| const nicknameMutation = useMutation(userMutations.NICKNAME()); | ||
| const userInfoMutation = useMutation(userMutations.USER_INFO()); | ||
|
|
||
| const informationLength = informationValue.length ?? 0; | ||
|
|
||
| const onSubmit = (data: NicknameFormValues) => { | ||
| nicknameMutation.mutate( | ||
| { nickname: data.nickname }, | ||
|
|
@@ -79,14 +89,20 @@ const SignupStep = () => { | |
| ...birthYearInputProps | ||
| } = register('birthYear'); | ||
|
|
||
| const { | ||
| onBlur: onInformationBlur, | ||
| ref: informationRef, | ||
| ...informationInputProps | ||
| } = register('information'); | ||
|
|
||
| const handleGenderClick = (gender: '여성' | '남성') => { | ||
| setValue('gender', gender, { shouldValidate: true, shouldDirty: true }); | ||
| }; | ||
|
|
||
| return ( | ||
| <form | ||
| onSubmit={handleSubmit(onSubmit)} | ||
| className="h-full w-full flex-col justify-between gap-[4rem]" | ||
| className="h-full w-full flex-col justify-between gap-[4rem] px-[1.6rem] pt-[4rem] pb-[1.6rem]" | ||
| > | ||
| <div className="w-full flex-col gap-[4rem]"> | ||
| <h1 className="title_24_sb whitespace-pre-line">{NICKNAME_TITLE}</h1> | ||
|
|
@@ -102,9 +118,24 @@ const SignupStep = () => { | |
| ref={nicknameRef} | ||
| {...nicknameInputProps} | ||
| /> | ||
| <Input | ||
| placeholder={INFORMATION_PLACEHOLDER} | ||
| className="h-[10.4rem]" | ||
| label="한 줄 소개" | ||
| defaultMessage={INFORMATION_RULE_MESSAGE} | ||
| multiline | ||
| maxLength={INFORMATION_MAX_LENGTH} | ||
| isError={!!errors.information} | ||
| isValid={isInformationValid} | ||
| onBlur={onInformationBlur} | ||
| ref={informationRef} | ||
| length={informationLength} | ||
| hasLength | ||
| {...informationInputProps} | ||
| /> | ||
| <Input | ||
| placeholder={BIRTH_PLACEHOLDER} | ||
| label="생년" | ||
| label="출생 연도" | ||
| defaultMessage={isBirthYearValid ? BIRTHYEAR_SUCCESS_MESSAGE : BIRTHYEAR_RULE_MESSAGE} | ||
| validationMessage={errors.birthYear?.message} | ||
| isError={!!errors.birthYear} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,23 @@ | ||
| import Icon from '@components/icon/icon'; | ||
| import { iconColorMap, inputClassMap } from '@components/input/styles/input-variants'; | ||
| import { cn } from '@libs/cn'; | ||
| import type React from 'react'; | ||
| import type { InputHTMLAttributes } from 'react'; | ||
| import { useState } from 'react'; | ||
| import { defineInputState } from '@/shared/utils/define-input-state'; | ||
|
|
||
| interface InputProps extends InputHTMLAttributes<HTMLInputElement> { | ||
| interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onBlur'> { | ||
| label?: string; | ||
| isError?: boolean; | ||
| isValid?: boolean; | ||
| hasLength?: boolean; | ||
| defaultMessage?: string; | ||
| validationMessage?: string; | ||
| ref?: React.Ref<HTMLInputElement>; | ||
| className?: string; | ||
| multiline?: boolean; | ||
| length?: number; | ||
| onBlur?: (e: React.FocusEvent<HTMLInputElement> | React.FocusEvent<HTMLTextAreaElement>) => void; | ||
| } | ||
|
|
||
| const Input = ({ | ||
|
|
@@ -21,8 +27,12 @@ const Input = ({ | |
| label, | ||
| validationMessage, | ||
| defaultMessage, | ||
| length, | ||
| onBlur, | ||
| ref, | ||
| className, | ||
| hasLength = false, | ||
| multiline = false, | ||
| ...props | ||
| }: InputProps) => { | ||
| const [isFocused, setIsFocused] = useState(false); | ||
|
|
@@ -41,31 +51,55 @@ const Input = ({ | |
| )} | ||
| <div | ||
| className={cn( | ||
| 'body_16_m h-[5.6rem] w-full flex-row-between rounded-[12px] bg-gray-100 p-[1.6rem]', | ||
| 'body_16_m h-[5.6rem] w-full flex-row-between rounded-[12px] bg-gray-100 ', | ||
| borderClass, | ||
| className, | ||
| )} | ||
| > | ||
| <input | ||
| id={id} | ||
| type="text" | ||
| className="flex-1 text-gray-black placeholder:text-gray-500" | ||
| ref={ref} | ||
| onFocus={() => setIsFocused(true)} | ||
| onBlur={(e) => { | ||
| setIsFocused(false); | ||
| onBlur?.(e); | ||
| }} | ||
| {...props} | ||
| /> | ||
| {multiline ? ( | ||
| <textarea | ||
| id={id} | ||
| ref={ref as React.Ref<HTMLTextAreaElement>} | ||
| className={cn( | ||
| 'w-full bg-transparent text-gray-black outline-none placeholder:text-gray-500', | ||
| 'resize-none whitespace-pre-wrap break-words', | ||
| 'h-[10.4rem] p-[1.6rem]', | ||
| )} | ||
| onFocus={() => setIsFocused(true)} | ||
| onBlur={(e) => { | ||
| setIsFocused(false); | ||
| onBlur?.(e); | ||
| }} | ||
| {...(props as React.TextareaHTMLAttributes<HTMLTextAreaElement>)} | ||
| /> | ||
| ) : ( | ||
|
Comment on lines
+61
to
+77
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여러 줄 입력받을 수 있게 input 컴포넌트를 확장시켰습니다. multiline props가 true면 input이 아니라 textarea로 동작해용 |
||
| <input | ||
| id={id} | ||
| type="text" | ||
| className="flex-1 p-[1.6rem] text-gray-black placeholder:text-gray-500" | ||
| ref={ref} | ||
| onFocus={() => setIsFocused(true)} | ||
| onBlur={(e) => { | ||
| setIsFocused(false); | ||
| onBlur?.(e); | ||
| }} | ||
| {...props} | ||
| /> | ||
| )} | ||
| </div> | ||
| {messageToShow && ( | ||
| <div className="flex-row gap-[0.8rem]"> | ||
| <Icon | ||
| name={inputState === 'valid' ? 'check-filled' : 'info-filled'} | ||
| size={2} | ||
| className={iconColorClass} | ||
| className={cn('text-gray-600', !multiline && iconColorClass)} | ||
| /> | ||
| <p className={`cap_14_m ${iconColorClass}`}>{messageToShow}</p> | ||
| <div className="flex w-full justify-between"> | ||
| <p className={cn('cap_14_m text-gray-600', !multiline && iconColorClass)}> | ||
| {messageToShow} | ||
| </p> | ||
| {hasLength && <p className="cap_14_m text-gray-600">{length}/50</p>} | ||
|
||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
각각 이용약관 동의, 개인정보 수집 동의 값을 저장하는 상태와, 전체 항목이 체크되었는지 검사를 위한 isAllChecked입니다. 둘다 true여야지 활성화 됩니다