|
| 1 | +import { useState, useEffect } from "react"; |
| 2 | +import { useNavigate } from "react-router-dom"; |
| 3 | +import axios from "axios"; |
| 4 | +import EnterIcon from "@/assets/svgs/enter.svg?react"; |
| 5 | +import UserIcon from "@/assets/svgs/user.svg?react"; |
| 6 | +import CloseIcon from "@/assets/svgs/close.svg?react"; |
| 7 | +import Button from "@/components/Button/Button"; |
| 8 | +import LogoOriginal from "@/assets/imgs/logoOriginal.png"; |
| 9 | +import SubLogoOriginal from "@/assets/imgs/subLogoOriginal.png"; |
| 10 | +import { useToast } from "@/components/Toast/useToast"; |
| 11 | + |
| 12 | +interface UserAuthSectionProps { |
| 13 | + isSignIn: boolean; |
| 14 | + setIsSignIn: React.Dispatch<React.SetStateAction<boolean>>; |
| 15 | +} |
| 16 | + |
| 17 | +const EMAIL_REGEXP = /^[a-zA-Z0-9._+]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/; |
| 18 | +const NICKNAME_REGEXP = /^[a-zA-Z0-9๊ฐ-ํฃ]{3,15}$/; |
| 19 | +const PASSWORD_REGEXP = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; |
| 20 | + |
| 21 | +const UserAuthSection = ({ isSignIn, setIsSignIn }: UserAuthSectionProps) => { |
| 22 | + const navigate = useNavigate(); |
| 23 | + const showToast = useToast(); |
| 24 | + const [email, setEmail] = useState<string>(""); |
| 25 | + const [username, setUsername] = useState<string>(""); |
| 26 | + const [password, setPassword] = useState<string>(""); |
| 27 | + const [passwordConfirm, setPasswordConfirm] = useState<string>(""); |
| 28 | + const [isEmailValid, setIsEmailValid] = useState<boolean | null>(null); |
| 29 | + const [isUsernameValid, setIsUsernameValid] = useState<boolean | null>(null); |
| 30 | + const [isPasswordValid, setIsPasswordValid] = useState<boolean | null>(null); |
| 31 | + const [isPasswordConfirm, setIsPasswordConfirm] = useState<boolean | null>(null); |
| 32 | + |
| 33 | + useEffect(() => { |
| 34 | + setEmail(""); |
| 35 | + setUsername(""); |
| 36 | + setPassword(""); |
| 37 | + setPasswordConfirm(""); |
| 38 | + setIsEmailValid(null); |
| 39 | + setIsUsernameValid(null); |
| 40 | + setIsPasswordValid(null); |
| 41 | + setIsPasswordConfirm(null); |
| 42 | + }, [isSignIn]); |
| 43 | + |
| 44 | + const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| 45 | + setEmail(e.target.value); |
| 46 | + setIsEmailValid(EMAIL_REGEXP.test(e.target.value)); |
| 47 | + }; |
| 48 | + |
| 49 | + const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| 50 | + setUsername(e.target.value); |
| 51 | + setIsUsernameValid(NICKNAME_REGEXP.test(e.target.value)); |
| 52 | + }; |
| 53 | + |
| 54 | + const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| 55 | + setPassword(e.target.value); |
| 56 | + setIsPasswordValid(PASSWORD_REGEXP.test(e.target.value)); |
| 57 | + setIsPasswordConfirm(passwordConfirm === e.target.value); |
| 58 | + }; |
| 59 | + |
| 60 | + const handlePasswordConfirm = (e: React.ChangeEvent<HTMLInputElement>) => { |
| 61 | + setPasswordConfirm(e.target.value); |
| 62 | + setIsPasswordConfirm(e.target.value !== "" && e.target.value === password); |
| 63 | + }; |
| 64 | + |
| 65 | + const handleLeftButtonClicked = () => { |
| 66 | + if (isSignIn) { |
| 67 | + setIsSignIn(false); |
| 68 | + } else { |
| 69 | + setIsSignIn(true); |
| 70 | + } |
| 71 | + }; |
| 72 | + |
| 73 | + const handleRightButtonClicked = () => { |
| 74 | + if (isSignIn) { |
| 75 | + if (isEmailValid && isPasswordValid) { |
| 76 | + axios |
| 77 | + .post(`${import.meta.env.VITE_API_SERVER_URL}/auth/signin`, { |
| 78 | + email, |
| 79 | + password |
| 80 | + }) |
| 81 | + .then((result) => { |
| 82 | + localStorage.setItem("token", result.data.token); |
| 83 | + showToast({ message: "๋ก๊ทธ์ธ์ ์ฑ๊ณตํ์ต๋๋ค.", type: "success" }); |
| 84 | + navigate("/"); |
| 85 | + }) |
| 86 | + .catch((error) => { |
| 87 | + if (error.response.status === 404) showToast({ message: "์ฌ์ฉ์๊ฐ ์กด์ฌํ์ง ์์์.", type: "alert" }); |
| 88 | + else showToast({ message: "์๋ชป๋ ์์ฒญ์ด์์.", type: "alert" }); |
| 89 | + }); |
| 90 | + } else showToast({ message: "์ฌ๋ฐ๋ฅธ ์ ๋ณด๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.", type: "alert" }); |
| 91 | + } else { |
| 92 | + if (isEmailValid && isUsernameValid && isPasswordValid && isPasswordConfirm) { |
| 93 | + axios |
| 94 | + .post(`${import.meta.env.VITE_API_SERVER_URL}/auth/signup`, { |
| 95 | + email, |
| 96 | + username, |
| 97 | + password |
| 98 | + }) |
| 99 | + .then(() => { |
| 100 | + showToast({ message: "ํ์๊ฐ์
์ ์ฑ๊ณตํ์ด์.", type: "success" }); |
| 101 | + setIsSignIn(true); |
| 102 | + }) |
| 103 | + .catch((error) => { |
| 104 | + if (error.response.status === 409) showToast({ message: "์ด๋ฏธ ๊ฐ์
ํ ์ ์ด ์์ด์.", type: "alert" }); |
| 105 | + else showToast({ message: "์๋ชป๋ ์์ฒญ์ด์์.", type: "alert" }); |
| 106 | + }); |
| 107 | + } else showToast({ message: "์ฌ๋ฐ๋ฅธ ์ ๋ณด๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.", type: "alert" }); |
| 108 | + } |
| 109 | + }; |
| 110 | + |
| 111 | + const getDescriptionColor = (isValid: boolean | null) => { |
| 112 | + if (isValid === null) return "text-grayscale-darkgray"; |
| 113 | + return isValid ? "text-boarlog-100" : "text-alert-100"; |
| 114 | + }; |
| 115 | + |
| 116 | + return ( |
| 117 | + <div className="flex flex-col relative py-20 sm:py-32 items-center justify-center w-full overflow-x-hidden overflow-y-visible"> |
| 118 | + <section className="flex relative w-11/12 sm:max-w-md p-8 bg-grayscale-white flex-col items-start gap-8 rounded-xl border-default shadow-xl"> |
| 119 | + <img src={LogoOriginal} className="absolute hidden -top-2 -left-52 rotate-[-20deg] z-[-1] w-64 sm:block" /> |
| 120 | + <img src={SubLogoOriginal} className="absolute hidden -bottom-16 -right-80 rotate-[20deg] z-[-1] sm:block" /> |
| 121 | + |
| 122 | + <div className="flex flex-col"> |
| 123 | + <h1 className="semibold-64">Boarlog</h1> |
| 124 | + <h2 className="semibold-20 text-grayscale-darkgray">๊ธฐ๋ก์ผ๋ก ๋จ๊ธฐ๋ ์ค์๊ฐ ๊ฐ์</h2> |
| 125 | + </div> |
| 126 | + <div className="flex flex-col gap-4 w-full"> |
| 127 | + <h3 className="semibold-32">{isSignIn ? "๋ก๊ทธ์ธ" : "ํ์๊ฐ์
"}</h3> |
| 128 | + <div className="flex flex-col gap-2 w-full"> |
| 129 | + <p className="semibold-18">์ด๋ฉ์ผ</p> |
| 130 | + {!isSignIn && ( |
| 131 | + <p className={`medium-12 ${getDescriptionColor(isEmailValid)}`}>์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ ์
๋ ฅํด์ฃผ์ธ์.</p> |
| 132 | + )} |
| 133 | + <input |
| 134 | + type="text" |
| 135 | + className="rounded-xl border-black w-full flex-grow medium-12 p-3 focus:outline-none focus:ring-1 focus:ring-boarlog-100 focus:border-transparent" |
| 136 | + placeholder="์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์" |
| 137 | + maxLength={50} |
| 138 | + value={email} |
| 139 | + onChange={handleEmailChange} |
| 140 | + /> |
| 141 | + </div> |
| 142 | + {!isSignIn && ( |
| 143 | + <div className="flex flex-col gap-2 w-full"> |
| 144 | + <p className="semibold-18">๋๋ค์</p> |
| 145 | + <p className={`medium-12 ${getDescriptionColor(isUsernameValid)}`}> |
| 146 | + 3์ ์ด์ 15์ ์ดํ์ ์๋ฌธ, ์ซ์, ํ๊ธ๋ง ์ฌ์ฉํ ์ ์์ต๋๋ค. |
| 147 | + </p> |
| 148 | + <input |
| 149 | + type="text" |
| 150 | + className="rounded-xl border-black w-full flex-grow medium-12 p-3 focus:outline-none focus:ring-1 focus:ring-boarlog-100 focus:border-transparent" |
| 151 | + placeholder="๋๋ค์์ ์
๋ ฅํด์ฃผ์ธ์" |
| 152 | + maxLength={15} |
| 153 | + value={username} |
| 154 | + onChange={handleUsernameChange} |
| 155 | + /> |
| 156 | + </div> |
| 157 | + )} |
| 158 | + <div className="flex flex-col gap-2 w-full"> |
| 159 | + <p className="semibold-18">๋น๋ฐ๋ฒํธ</p> |
| 160 | + {!isSignIn && ( |
| 161 | + <p className={`medium-12 ${getDescriptionColor(isPasswordValid)}`}> |
| 162 | + 8์ ์ด์, ๋์๋ฌธ์, ์ซ์, ํน์ ๋ฌธ์(@$!%*?&)๋ฅผ ์ต์ ํ๋์ฉ ํฌํจํด์ผ ํฉ๋๋ค. |
| 163 | + </p> |
| 164 | + )} |
| 165 | + <input |
| 166 | + type="password" |
| 167 | + className="rounded-xl border-black w-full flex-grow medium-12 p-3 focus:outline-none focus:ring-1 focus:ring-boarlog-100 focus:border-transparent" |
| 168 | + placeholder="๋น๋ฐ๋ฒํธ์ ์
๋ ฅํด์ฃผ์ธ์" |
| 169 | + maxLength={50} |
| 170 | + value={password} |
| 171 | + onChange={handlePasswordChange} |
| 172 | + /> |
| 173 | + </div> |
| 174 | + {!isSignIn && ( |
| 175 | + <div className="flex flex-col gap-2 w-full"> |
| 176 | + <p className="semibold-18">๋น๋ฐ๋ฒํธ ํ์ธ</p> |
| 177 | + <p className={`medium-12 ${getDescriptionColor(isPasswordConfirm)}`}> |
| 178 | + ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ๋๋ก ์
๋ ฅํด์ฃผ์ธ์. |
| 179 | + </p> |
| 180 | + <input |
| 181 | + type="password" |
| 182 | + className="rounded-xl border-black w-full flex-grow medium-12 p-3 focus:outline-none focus:ring-1 focus:ring-boarlog-100 focus:border-transparent" |
| 183 | + placeholder="๋น๋ฐ๋ฒํธ์ ๋ค์ ์
๋ ฅํด์ฃผ์ธ์" |
| 184 | + maxLength={50} |
| 185 | + value={passwordConfirm} |
| 186 | + onChange={handlePasswordConfirm} |
| 187 | + /> |
| 188 | + </div> |
| 189 | + )} |
| 190 | + </div> |
| 191 | + |
| 192 | + <div className="flex flex-col w-full gap-4 sm:flex-row"> |
| 193 | + <Button type="grow" buttonStyle="black" onClick={handleLeftButtonClicked}> |
| 194 | + {isSignIn ? ( |
| 195 | + <> |
| 196 | + <UserIcon className="fill-grayscale-white" /> |
| 197 | + ํ์๊ฐ์
|
| 198 | + </> |
| 199 | + ) : ( |
| 200 | + <> |
| 201 | + <CloseIcon className="fill-grayscale-white" /> |
| 202 | + ๋ค๋ก๊ฐ๊ธฐ |
| 203 | + </> |
| 204 | + )} |
| 205 | + </Button> |
| 206 | + <Button type="grow" buttonStyle="black" onClick={handleRightButtonClicked}> |
| 207 | + {isSignIn ? ( |
| 208 | + <> |
| 209 | + <EnterIcon className="fill-grayscale-white" /> |
| 210 | + ์์ํ๊ธฐ |
| 211 | + </> |
| 212 | + ) : ( |
| 213 | + <> |
| 214 | + <EnterIcon className="fill-grayscale-white" /> |
| 215 | + ๊ฐ์
ํ๊ธฐ |
| 216 | + </> |
| 217 | + )} |
| 218 | + </Button> |
| 219 | + </div> |
| 220 | + </section> |
| 221 | + </div> |
| 222 | + ); |
| 223 | +}; |
| 224 | + |
| 225 | +export default UserAuthSection; |
0 commit comments