diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 61e4af5b..b3474b60 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -36,6 +36,12 @@ module.exports = { 'import/extensions': 'off', 'jsx-a11y/no-noninteractive-element-interactions': 'warn', 'vitest/valid-title': 'off', + 'jsx-a11y/label-has-associated-control': [ + 2, + { + labelAttributes: ['htmlFor'], + }, + ], }, ignorePatterns: [ 'dist', diff --git a/package.json b/package.json index dcddc0f3..62bb1afe 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "react-hook-form": "^7.51.4", "react-icons": "^5.2.1", "react-router-dom": "^6.23.1", + "tailwind-scrollbar-hide": "^1.1.7", "zustand": "^4.5.2" }, "devDependencies": { diff --git a/src/assets/auth_logo.svg b/src/assets/auth_logo.svg new file mode 100644 index 00000000..f4dfe7bb --- /dev/null +++ b/src/assets/auth_logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/social_google_icon.svg b/src/assets/social_google_icon.svg new file mode 100644 index 00000000..0b87f9a7 --- /dev/null +++ b/src/assets/social_google_icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/social_kakao_icon.svg b/src/assets/social_kakao_icon.svg new file mode 100644 index 00000000..249c5857 --- /dev/null +++ b/src/assets/social_kakao_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/common/ValidationInput.tsx b/src/components/common/ValidationInput.tsx new file mode 100644 index 00000000..5e7e0229 --- /dev/null +++ b/src/components/common/ValidationInput.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import { UseFormRegisterReturn } from 'react-hook-form'; +import { RiEyeFill, RiEyeOffFill } from 'react-icons/ri'; + +/** + * ValidationInput 컴포넌트 params, 모든 params는 optional + * + * @params {string} [label] - 입력 필드의 label 텍스트 + * @params {boolean} [required] - 입력 필드 필수 여부 + * @params {string} [errors] - 유효성 검증 후 오류 발생 시 표시할 오류 메시지 + * @params {UseFormRegisterReturn} [register] - react-hook-form의 resister 함수. + * register('password', {required: ...})부분을 그대로 파라미터에 넣으면 됩니다. + * @params {string} [type] - 입력 필드의 유형(ex: text, password). 기본값은 'text' + * @params {string} [placeholder] - 입력 필드의 placeholder 텍스트 + * @params {boolean} [isButtonInput] - 버튼이 포함된 입력 필드인지 여부. 기본값은 'false' + * @params {React.ReactNode} [buttonLabel] - 버튼에 표시할 텍스트 또는 아이콘 + * @params {() => void} [onButtonClick] - 버튼 클릭 시 호출할 함수 + * + * 예시) + * value === watch('password') || '비밀번호가 일치하지 않습니다.', + })} + type="password" + /> + */ + +type ValidationInputProps = { + label?: string; + required?: boolean; + errors?: string; + register?: UseFormRegisterReturn; + type?: string; + placeholder?: string; + isButtonInput?: boolean; + buttonLabel?: React.ReactNode; + onButtonClick?: () => void; +}; + +export default function ValidationInput({ + label, + required = true, + errors, + register, + type = 'text', + placeholder, + isButtonInput = false, + buttonLabel, + onButtonClick, +}: ValidationInputProps) { + const [showPassword, setShowPassword] = useState(false); + + const handleTogglePassword = () => { + setShowPassword((prev) => !prev); + }; + + return ( +
+
+ {label && ( + + )} +
+
+
+ + {type === 'password' && ( +
+ {showPassword ? ( + + ) : ( + + )} +
+ )} + {isButtonInput && ( + + )} +
+
+ {errors &&

{errors}

} +
+ ); +} diff --git a/src/constants/formValidationRules.ts b/src/constants/formValidationRules.ts index 1f3ae126..d8c117f6 100644 --- a/src/constants/formValidationRules.ts +++ b/src/constants/formValidationRules.ts @@ -1,5 +1,6 @@ import Validator from '@utils/Validator'; import { deepFreeze } from '@utils/deepFreeze'; +import { EMAIL_REGEX, PASSWORD_REGEX, PHONE_REGEX } from './regex'; export const STATUS_VALIDATION_RULES = deepFreeze({ STATUS_NAME: (nameList: string[]) => ({ @@ -23,4 +24,44 @@ export const STATUS_VALIDATION_RULES = deepFreeze({ duplicatedName: (value: string) => !Validator.isDuplicatedName(colorList, value) || '이미 사용중인 색상입니다.', }, }), + EMAIL: () => ({ + required: '이메일 인증을 진행해 주세요.', + pattern: { + value: EMAIL_REGEX, + message: '이메일 형식에 맞지 않습니다.', + }, + }), + CERTIFICATION: () => ({ required: '인증번호를 입력해 주세요.' }), + PHONE: () => ({ + required: '휴대폰 번호 인증을 진행해 주세요.', + pattern: { + value: PHONE_REGEX, + message: '휴대폰 번호를 정확히 입력해 주세요.', + }, + }), + NICKNAME: () => ({ + required: '닉네임을 입력해 주세요.', + maxLength: { + value: 20, + message: '닉네임은 최대 20자까지 입력 가능합니다.', + }, + }), + PASSWORD: () => ({ + required: '비밀번호를 입력해 주세요.', + minLength: { + value: 8, + message: '비밀번호는 최소 8자 이상이어야 합니다.', + }, + maxLength: { + value: 16, + message: '비밀번호는 최대 16자 이하여야 합니다.', + }, + pattern: { + value: PASSWORD_REGEX, + message: '비밀번호는 영문자, 숫자, 기호를 포함해야 합니다.', + }, + }), + PASSWORD_CONFIRM: () => ({ + required: '비밀번호를 한 번 더 입력해 주세요.', + }), }); diff --git a/src/constants/regex.ts b/src/constants/regex.ts new file mode 100644 index 00000000..6863a928 --- /dev/null +++ b/src/constants/regex.ts @@ -0,0 +1,4 @@ +export const EMAIL_REGEX = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}(?:\.[a-z]{2,})?$/i; +export const PASSWORD_REGEX = + /^(?=.*[A-Za-z])(?=.*\d)(?=.*[~`!@#$%^&*()_\-+={[}\]|\\:;"'<,>.?/])[A-Za-z\d~`!@#$%^&*()_\-+={[}\]|\\:;"'<,>.?/]{8,16}$/; +export const PHONE_REGEX = /^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/; diff --git a/src/globals.css b/src/globals.css index 657b5d61..acc9a284 100644 --- a/src/globals.css +++ b/src/globals.css @@ -35,6 +35,7 @@ --color-main: #237700; --color-sub: #e1f4d9; --color-close: #be0000; + --color-error: #ff0000; --color-contents-box: #fdfdfd; --color-disable: #c2c2c2; --color-selected: #c2c2c2; @@ -57,7 +58,6 @@ --text-color-default: #2c2c2c; --text-color-emphasis: #5e5e5e; --text-color-category: #b1b1b1; - --text-color-error: #be0000; --text-color-blue: #0909e7; /* border color */ diff --git a/src/layouts/page/AuthLayout.tsx b/src/layouts/page/AuthLayout.tsx new file mode 100644 index 00000000..db28d263 --- /dev/null +++ b/src/layouts/page/AuthLayout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from 'react-router-dom'; +import AuthGULogo from '@/assets/auth_logo.svg'; + +export default function AuthLayout() { + return ( +
+ Auth GU Logo +
+ +
+
+ ); +} diff --git a/src/pages/user/SignInPage.tsx b/src/pages/user/SignInPage.tsx index 2ac27c76..d210a0af 100644 --- a/src/pages/user/SignInPage.tsx +++ b/src/pages/user/SignInPage.tsx @@ -1,3 +1,110 @@ +import { useNavigate } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import Kakao from '@assets/social_kakao_icon.svg'; +import Google from '@assets/social_google_icon.svg'; +import { EMAIL_REGEX, PASSWORD_REGEX } from '@/constants/regex'; +import { UserSignIn } from '@/types/UserType'; + export default function SignInPage() { - return
SignInPage
; + const nav = useNavigate(); + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + mode: 'onChange', + defaultValues: { + email: '', + password: '', + }, + }); + + const onSubmit = (data: UserSignIn) => { + console.log(data); + }; + + return ( +
+
+ Welcome to our site! +
Grow Up your Life with us. +
+
+ +
+ {errors.email &&

{errors.email.message}

} + + + {errors.password &&

{errors.password.message}

} + +
+ +
+ +
+

nav('/search/id')} onKeyDown={() => nav('/search/id')}> + 아이디 찾기 +

+

|

+

nav('/search/password')} + onKeyDown={() => nav('/search/password')} + > + 비밀번호 찾기 +

+
+ +
+

회원이 아니신가요?

+ +
+ +
+ + +
+
+ ); } diff --git a/src/pages/user/SignUpPage.tsx b/src/pages/user/SignUpPage.tsx index be64afeb..93dc3979 100644 --- a/src/pages/user/SignUpPage.tsx +++ b/src/pages/user/SignUpPage.tsx @@ -1,3 +1,259 @@ +import { Link } from 'react-router-dom'; +import { GoPlusCircle } from 'react-icons/go'; +import { FaRegTrashCan, FaPlus, FaMinus } from 'react-icons/fa6'; +import { ChangeEvent, useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { UserSignUp } from '@/types/UserType'; +import ValidationInput from '@/components/common/ValidationInput'; +import { STATUS_VALIDATION_RULES } from '@/constants/formValidationRules'; + export default function SignUpPage() { - return
SignUpPage
; + const [imagePreview, setImagePreview] = useState(''); + const [isFocused, setIsFocused] = useState(false); + const [link, setLink] = useState(''); + const [linksList, setLinksList] = useState([]); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + watch, + setValue, + } = useForm({ + mode: 'onChange', + defaultValues: { + image: [], // 추후 이미지 전송 폼 분리 예정 + email: '', + emailVerificationCode: '', + phone: '', + phoneVerificationCode: '', + nickname: '', + password: '', + checkPassword: '', + bio: '', + links: [''], + }, + }); + + // 이미지 미리보기 관련 코드 + const profileImage = watch('image'); + useEffect(() => { + if (profileImage && profileImage.length > 0) { + const file = profileImage[0]; + setImagePreview(URL.createObjectURL(file)); + } + }, [profileImage]); + + const handleRemoveImg = () => { + setValue('image', []); + setImagePreview(''); + }; + + // 웹사이트 링크 관련 코드 + const handleFocus = () => { + setIsFocused(true); + }; + + const handleBlur = () => { + setIsFocused(false); + }; + + const handleLinkChange = (e: ChangeEvent) => { + setLink(e.target.value); + }; + + const handleAddLink = (newLink: string) => { + if (newLink.trim() === '' || linksList.length === 3) { + // alert같은 걸로 유저에게 알려줘야 할 듯...? + return; + } + + setLinksList([...linksList, newLink.trim()]); + setValue('links', [...linksList, newLink.trim()]); + setLink(''); + }; + + const handleRemoveLink = (idx: number) => { + const filteredData = linksList.filter((_, index) => index !== idx); + setLinksList(filteredData); + setValue('links', filteredData); + }; + + // form 전송 함수 + const onSubmit = (data: UserSignUp) => { + const { emailVerificationCode, phoneVerificationCode, checkPassword, ...filteredData } = data; + console.log(filteredData); + }; + + return ( +
+ {/* 프로필 이미지 */} +
+
+ {imagePreview ? ( + <> + profileImage +
+

+ +

+
+ + ) : ( + + )} +
+
+ + {/* 이메일(아이디) */} + + + {/* 이메일 인증 */} + {/* 인증번호 발송이 완료되면 해당 폼으로 대체 */} + + + {/* 휴대폰 번호 */} + + + {/* 휴대폰 번호 인증 */} + + + {/* 닉네임, 중복 확인 */} + + + {/* 비밀번호 */} + + + {/* 비밀번호 확인 */} + value === watch('password') || '비밀번호가 일치하지 않습니다.', + })} + type="password" + /> + + {/* 자기소개 */} +
+ +