diff --git a/umc-master/src/apis/authApi.ts b/umc-master/src/apis/authApi.ts index 55d9c5c..0052887 100644 --- a/umc-master/src/apis/authApi.ts +++ b/umc-master/src/apis/authApi.ts @@ -1,38 +1,18 @@ -// import axiosInstance from '@apis/axios-instance'; +import axiosInstance from '@apis/axios-instance'; -// interface UserSignup { -// email: string; -// password: string; -// nickname: string; -// hashtags: string[]; -// } +interface UserSignup { + email: string; + password: string; + nickname: string; + hashtags: string[]; +} -// export const postSignup = async ({ email, password, nickname, hashtags } : UserSignup) => { -// const { data } = await axiosInstance.post(`/signup`, { -// email, -// password, -// nickname, -// hashtags, -// }); -// return data; -// }; - -import axios from 'axios'; - -export const postSignup = async () => { - try { - const response = await axios.post('https://api.hmaster.shop/api/v1/signup', { - email: 'ekos555@naver.com', - password: 'asfa1234!@', - nickname: 'rael', - hashtags: ['봄', '패션', '청소', '요리', '재활용', '주택'] - }, { - headers: { - 'Content-Type': 'application/json' - } - }); - console.log('회원가입 성공:', response.data); - } catch (error) { - console.error('회원가입 오류:', error); - } +export const postSignup = async ({ email, password, nickname, hashtags }: UserSignup) => { + const { data } = await axiosInstance.post(`/signup`, { + email, + password, + nickname, + hashtags, + }); + return data; }; diff --git a/umc-master/src/pages/auth/SignUpPage.tsx b/umc-master/src/pages/auth/SignUpPage.tsx index 48f542f..0036ff2 100644 --- a/umc-master/src/pages/auth/SignUpPage.tsx +++ b/umc-master/src/pages/auth/SignUpPage.tsx @@ -43,15 +43,17 @@ const handleNicknameChange = (nickname: string) => { }; const handleHashtagChange = (hashtags: string[]) => { - setHashtag(hashtags); + const hashtagStrings = hashtags.map(hashtag => hashtag.toString()); + setHashtag(hashtagStrings); }; const handleSignUpComplete = async () => { try { const userSignupData = { email, password, nickname, hashtags }; console.log("회원가입 확인:", userSignupData) - await postSignup(); - navigate("/main"); + await postSignup(userSignupData); + alert("회원가입 성공!"); + navigate("/login"); } catch (error) { console.error("회원가입 오류:", error); alert("회원가입에 실패했습니다. 다시 시도해주세요."); diff --git a/umc-master/src/pages/auth/Signup_components/AgreementForm.tsx b/umc-master/src/pages/auth/Signup_components/AgreementForm.tsx index f0eff45..96c3573 100644 --- a/umc-master/src/pages/auth/Signup_components/AgreementForm.tsx +++ b/umc-master/src/pages/auth/Signup_components/AgreementForm.tsx @@ -3,7 +3,7 @@ import Typography from "@components/common/typography"; import styled, { useTheme } from "styled-components"; import ImgAdd from "@assets/add.svg"; import ImgRemove from "@assets/remove.svg"; -import { useEffect, useState } from "react"; +import { useState } from "react"; interface AgreementItemProps { isRequired: boolean; // 필수 여부 @@ -67,10 +67,14 @@ const AgreementForm: React.FC = ({ onCheckRequired }) => { }); // 전체 동의 체크박스 - const handleAllAgreeChange = (checked: boolean) => { - setIsAllAgreed(checked); + const handleAllAgreeChange = () => { + const newCheckedState = !isAllAgreed; + setIsAllAgreed(newCheckedState); const updatedItems = { - terms: checked, privacy: checked, thirdinfo: checked, marketing: checked, + terms: newCheckedState, + privacy: newCheckedState, + thirdinfo: newCheckedState, + marketing: newCheckedState, }; setCheckedItems(updatedItems); onCheckRequired(updatedItems.terms && updatedItems.privacy); @@ -79,22 +83,13 @@ const AgreementForm: React.FC = ({ onCheckRequired }) => { const handleCheckboxChange = (key: string, checked: boolean) => { setCheckedItems((prevState) => { const updatedItems = { ...prevState, [key]: checked }; + const allChecked = Object.values(updatedItems).every(Boolean); + setIsAllAgreed(allChecked); onCheckRequired(updatedItems.terms && updatedItems.privacy); return updatedItems; }); }; - useEffect(() => { - if (isAllAgreed) { - setCheckedItems({ - terms: true, privacy: true, thirdinfo: true, marketing: true, - }); - onCheckRequired(true); - } else { - onCheckRequired(checkedItems.terms && checkedItems.privacy); - } - }, [isAllAgreed, checkedItems, onCheckRequired]); - return ( (false); const [verified, setVerified] = useState(false); const [timer, setTimer] = useState(180); - const [retryEnabled, setRetryEnabled] = useState(false); + const [isEmailDuplicate, setIsEmailDuplicate] = useState(false); const fullEmail = localPart && domain ? `${localPart}@${domain}` : ""; @@ -49,9 +49,7 @@ const EmailForm: React.FC<{ timerInterval = setInterval(() => { setTimer((prev) => prev - 1); }, 1000); - } else if (timer === 0) { - setRetryEnabled(true); - } + } return () => { if (timerInterval) { clearInterval(timerInterval); @@ -103,6 +101,8 @@ const EmailForm: React.FC<{ onEmailChange(updatedFullEmail); console.log("도메인 변경:", updatedFullEmail); + + setIsEmailDuplicate(false); // 중복 확인 버튼을 다시 활성화 }; const handleEmailChange = (e: React.ChangeEvent) => { @@ -113,8 +113,26 @@ const EmailForm: React.FC<{ onEmailChange(updatedFullEmail); console.log("이메일 입력:", updatedFullEmail); + + setIsEmailDuplicate(false); // 중복 확인 버튼을 다시 활성화 }; + // 이메일 중복 체크 + const checkEmailDuplicate = async () => { + if (!fullEmail) { + alert("이메일을 입력해주세요."); + return; + } + try { + const response = await axiosInstance.post("/check-email", { email: fullEmail }); + if (response.data.isSuccess) { + setIsEmailDuplicate(true); + alert("사용 가능한 이메일입니다."); + } + } catch (error) { + alert("이메일이 이미 존재합니다."); + } + }; const handleRetry = () => { setLocalPart(""); @@ -123,7 +141,6 @@ const EmailForm: React.FC<{ setEmailSent(false); setVerified(false); setTimer(180); - setRetryEnabled(false); }; return ( @@ -147,7 +164,7 @@ const EmailForm: React.FC<{ {emails.map((email) => ( ))} - @@ -175,7 +200,7 @@ const EmailForm: React.FC<{ variant="titleXxxSmall" style={{color: theme.colors.red[500]}} >남은 시간: {Math.floor(timer / 60)}분 {timer % 60}초 - diff --git a/umc-master/src/pages/auth/Signup_components/PasswordForm.tsx b/umc-master/src/pages/auth/Signup_components/PasswordForm.tsx index 0de5716..01ebcf3 100644 --- a/umc-master/src/pages/auth/Signup_components/PasswordForm.tsx +++ b/umc-master/src/pages/auth/Signup_components/PasswordForm.tsx @@ -1,6 +1,8 @@ /* eslint-disable react/prop-types */ import Typography from "@components/common/typography"; import Input from "@components/Input/Input"; +import useInput from "@hooks/useInput"; +import { validatePasswordFormat } from "@utils/validation"; import { useEffect, useState } from "react"; import { styled, useTheme } from "styled-components"; @@ -13,11 +15,40 @@ const PasswordForm: React.FC<{ const theme = useTheme(); - const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordErrorMessage, setPasswordErrorMessage] = useState(''); + const [confirmPasswordErrorMessage, setConfirmPasswordErrorMessage] = useState(''); + const [isPasswordValid, setIsPasswordValid] = useState(false); + + // 비밀번호 상태 검증 및 에러메세지 + const { + input: password, + errorMessage: passwordInputErrorMessage, + changeHandler: passwordChangeHandler, + } = useInput({ + initialValue: "", + validate: async (value) => { + const error = validatePasswordFormat(value); + setIsPasswordValid(!error); // 형식이 맞으면 true, 아니면 false + return error; + } + }); + + // 비밀번호 확인 오류 메시지 실시간 변동 + useEffect(() => { + if (confirmPassword && password !== confirmPassword) { + setConfirmPasswordErrorMessage("비밀번호가 일치하지 않습니다."); + } else { + setConfirmPasswordErrorMessage(""); + } + }, [confirmPassword, password]); + + useEffect(() => { + setPasswordErrorMessage(passwordInputErrorMessage || ""); + }, [passwordInputErrorMessage]); useEffect(() => { - if (password && confirmPassword && password === confirmPassword) { + if (isPasswordValid && confirmPassword && password === confirmPassword) { onCheckRequired(true); } else { onCheckRequired(false); @@ -26,8 +57,8 @@ const PasswordForm: React.FC<{ const handlePasswordChange = (e: React.ChangeEvent) => { const newPassword = e.target.value; - setPassword(newPassword); onPasswordChange(newPassword); // 상위 컴포넌트에 전달 + passwordChangeHandler(e); console.log("비밀번호: ", newPassword); }; @@ -38,14 +69,14 @@ const PasswordForm: React.FC<{ style={{color: theme.colors.primary[700]}} >비밀번호 입력 (필수) * void; }> = ({ onCheckRequired, onNicknameChange }) => { + const theme = useTheme(); + + const [profileImageUrl, setProfileImageUrlLocal] = useState(gray_character); const [selectedCity, setSelectedCity] = useState("default"); const [districts, setDistricts] = useState([]); - const [nickname, setNickname] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); // 닉네임 입력값을 업데이트하는 함수 const handleNicknameChange = (e: React.ChangeEvent) => { const newNickname = e.target.value; setNickname(newNickname); - onNicknameChange(newNickname); // 상위 컴포넌트로 업데이트 - console.log("닉네임: ", newNickname); - // 닉네임이 0글자 이상일 때만 "다음" 버튼을 활성화 - onCheckRequired(newNickname.length > 0); + onNicknameChange(newNickname); + const nicknameRegex = /^[a-zA-Z0-9ㄱ-ㅎ가-힣._]+$/; + + if (newNickname.length === 0) { + setErrorMessage("닉네임을 입력해주세요."); + onCheckRequired(false); + } else if (newNickname.length > 11) { + setErrorMessage("닉네임을 최대 10자까지 입력 가능합니다."); + onCheckRequired(false); + } else if (!nicknameRegex.test(newNickname)) { + setErrorMessage("닉네임은 한글, 영문, 숫자, '.', '_' 만 사용할 수 있습니다."); + onCheckRequired(false); + } else { + setErrorMessage(""); + onCheckRequired(true); + } }; // 도시 선택시 구 목록을 업데이트하는 함수 @@ -90,17 +106,34 @@ const PrivacyForm: React.FC<{ break; } }; - - const theme = useTheme(); + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + const imageUrl = reader.result as string; + setProfileImageUrlLocal(imageUrl); // 로컬 상태 업데이트 + // api 연결 + }; + reader.readAsDataURL(file); + } + }; + return ( - 프로필 사진 (선택) - - + + document.getElementById('fileInput')?.click()} + /> + @@ -109,6 +142,7 @@ const PrivacyForm: React.FC<{ style={{color: theme.colors.primary[700]}} >닉네임 (필수) * = ({ sectionCount }) => { {step} @@ -51,7 +52,7 @@ const Sequence = styled.div` position: relative; display: flex; align-items: center; - gap: 160px; + gap: 130px; ` const Step = styled.div` @@ -85,7 +86,7 @@ const Num = styled.div<{ isActive: boolean }>` const Line = styled.div` position: absolute; top: 30px; - left: 5px; + left: 15px; width: 99%; height: 1px; border: 1px dotted ${({ theme }) => theme.colors.text.lightGray};