기존 코드
import { useState, useEffect } from "react";
import {
Div,
Fieldset,
Legend,
Select,
P,
} from "../Login.page.component/Login.style";
import Modal from "../Modal/Modal";
import { useModal } from "../../hooks/useModal";
interface CalculateAgeProps {
isAdult: (isValid: boolean | null) => void;
onBirthdateChange: (year: string, month: string, day: string) => void;
}
const CalculateAge = ({ isAdult, onBirthdateChange }: CalculateAgeProps) => {
const { isModalOpen, modalTitle, modalContent, openModal, closeModal } =
useModal();
const [year, setYear] = useState("");
const [month, setMonth] = useState("");
const [day, setDay] = useState("");
const [isValid, setIsValid] = useState<boolean | null>(null);
const years = Array.from(
{ length: new Date().getFullYear() - 1899 },
(_, i) => new Date().getFullYear() - i
);
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const days = Array.from({ length: 31 }, (_, i) => i + 1);
const yearChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setYear(event.target.value);
setIsValid(null); // 값이 변경될 때만 유효성 검사를 다시 수행하게 설정
};
const monthChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setMonth(event.target.value);
setIsValid(null); // 값이 변경될 때만 유효성 검사를 다시 수행하게 설정
};
const dayChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setDay(event.target.value);
setIsValid(null); // 값이 변경될 때만 유효성 검사를 다시 수행하게 설정
};
useEffect(() => {
const validateDate = () => {
if (!year || !month || !day) {
setIsValid(null);
isAdult(null);
return;
}
const birthDate = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10)
);
const today = new Date();
const age = today.getFullYear() - birthDate.getFullYear();
const isUnder19 =
age < 19 ||
(age === 19 &&
(today.getMonth() < birthDate.getMonth() ||
(today.getMonth() === birthDate.getMonth() &&
today.getDate() < birthDate.getDate())));
if (isUnder19) {
openModal(
"잠깐!",
<Div className="modal-box">
<P>19세 미만 회원의 경우,</P>
<P>이용에 제한이 있을 수 있습니다.</P>
</Div>
);
setIsValid(false);
isAdult(false);
} else {
setIsValid(true);
isAdult(true);
}
};
if (isValid === null) {
validateDate(); // 날짜가 변경된 경우에만 유효성 검사 수행
}
onBirthdateChange(year, month, day);
}, [year, month, day]);
return (
<Fieldset>
<Modal
isOpen={isModalOpen}
onClose={closeModal}
title={modalTitle}
content={modalContent}
/>
<Legend>생년월일</Legend>
<Div
className={`item-box ${isValid === true ? "valid" : ""} ${
isValid === false ? "invalid" : ""
}`}
>
<Select name="year" value={year} onChange={yearChange} required>
<option value="" disabled hidden>
년
</option>
{years.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</Select>
<Select name="month" value={month} onChange={monthChange} required>
<option value="" disabled hidden>
월
</option>
{months.map((month) => (
<option key={month} value={month}>
{month}
</option>
))}
</Select>
<Select name="day" value={day} onChange={dayChange} required>
<option value="" disabled hidden>
일
</option>
{days.map((day) => (
<option key={day} value={day}>
{day}
</option>
))}
</Select>
</Div>
</Fieldset>
);
};
export default CalculateAge;
//위 코드의 부모 컴포넌트
import { useState, useEffect, useContext } from "react";
import {
Div,
Label,
Input,
Form,
H3,
H4,
Button,
Links,
} from "../Login.page.component/Login.style";
import { BaseButton } from "../../../public/assets/design-assets/BaseButton/BaseButton";
import { auth, db } from "../../firebase/firebaseConfig";
import { setDoc, doc } from "firebase/firestore";
import { createUserWithEmailAndPassword } from "firebase/auth";
import { useRouter, usePathname } from "next/navigation";
import { useModal } from "../../hooks/useModal";
import Modal from "../Modal/Modal";
import useLoading from "../../hooks/useLoading";
import { nicknameRule, fullnameRule } from "../../stores/NameRule";
import CalculateAge from "./CalculateAge";
import GenderSelect from "./GenderSelect";
import PhoneNumber from "./PhoneNumber";
import { AuthContext } from "../../context/AuthContext";
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
// import WishList from "../components/Wishlist/WishList";
export default function RegisterComp() {
const router = useRouter()
const path = usePathname()
const { isModalOpen, modalTitle, modalContent, openModal, closeModal } =
useModal();
const { isLoading, loadingProgress } = useLoading();
const [registerEmail, setRegisterEmail] = useState<string>("");
const [registerPw, setRegisterPw] = useState<string>("");
const [registerPwConfirm, setRegisterPwConfirm] = useState<string>("");
const [registerNickname, setRegisterNickname] = useState<string>("");
const [registerFullname, setRegisterFullname] = useState<string>("");
const [isValidAge, setIsValidAge] = useState<boolean>(false);
const [birthYear, setBirthYear] = useState<string>("");
const [birthMonth, setBirthMonth] = useState<string>("");
const [birthDay, setBirthDay] = useState<string>("");
const [gender, setGender] = useState<string>("");
const [countryCode, setCountryCode] = useState<string>("");
const [registerPhonenumber, setRegisterPhonenumber] = useState<string>("");
const [step, setStep] = useState<number>(1);
const { currentlyLoggedIn } = useContext(AuthContext);
useEffect(() => {
loadingProgress();
}, [loadingProgress]);
useEffect(() => {
if (currentlyLoggedIn) {
router.push("/mypage"); // 이미 로그인된 상태라면 프로필 페이지로
}
}, [currentlyLoggedIn, router, path]);
// 이메일 유효성 검사
const isValidEmail = (email: string) => {
const regex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
return regex.test(email);
};
// 폼 상태 초기화
const resetForm = () => {
setRegisterEmail("");
setRegisterPw("");
setRegisterPwConfirm("");
setRegisterNickname("");
setRegisterFullname("");
setBirthYear("");
setBirthMonth("");
setBirthDay("");
setGender("");
setCountryCode("");
setRegisterPhonenumber("");
setIsValidAge(false);
setStep(1);
};
const handleSignUp = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// 비밀번호 확인
if (registerPw !== registerPwConfirm) {
openModal("잠깐!", "비밀번호가 일치하지 않습니다.");
return;
}
// 이메일 유효성 검사
if (!isValidEmail(registerEmail)) {
openModal("잠깐!", "유효하지 않은 이메일 주소입니다.");
return;
}
const validateForm = () => {
return (
validateStepOne() &&
registerFullname.length > 0 &&
registerPhonenumber.length > 0 &&
registerNickname.length > 0 &&
countryCode.length > 0
);
};
if (!validateForm()) {
openModal("잠깐!", "필수 입력사항을 확인해주세요.");
return;
}
try {
// 사용자 등록
const userCredential = await createUserWithEmailAndPassword(
auth,
registerEmail,
registerPw
);
const user = userCredential.user; // 이 부분 떄문에 회원가입 후 자동 로그인된다.
try {
// Firestore에 사용자 정보 저장
const userData = {
email: registerEmail,
fullname: registerFullname,
nickname: registerNickname,
phonenumber: registerPhonenumber,
countryCode: countryCode,
isValidAge: isValidAge,
birthYear: birthYear,
birthMonth: birthMonth,
birthDay: birthDay,
gender: gender,
wishList: [], // 초기 위시리스트
};
console.log("Firestore에 저장할 데이터:", userData);
await setDoc(doc(db, "users", user.uid), userData);
console.log("Firestore에 사용자 정보 저장 성공!");
await auth.signOut(); // 가입 후 자동 로그아웃 처리
resetForm(); // 폼 초기화
openModal("가입 성공!", "로그인 화면으로 이동합니다.");
const handleModalClose = () => {
router.push("/login")
// closeModal();
};
return (
<Modal
isOpen={isModalOpen}
onClose={handleModalClose}
title={modalTitle}
content={modalContent}
/>
);
} catch (firestoreError) {
console.error("Firestore에 사용자 정보 저장 실패:", firestoreError);
// Firestore 저장 실패 시 사용자 계정 삭제
await user.delete();
openModal(undefined, "회원가입에 실패했습니다. 다시 시도해주세요.");
}
} catch (authError) {
const error = authError as { code: string };
console.error("Error signing up:", error);
// Firebase 인증 에러 처리
switch (error.code) {
case "auth/email-already-in-use":
openModal(undefined, "이미 사용 중인 이메일입니다.");
break;
case "auth/invalid-email":
openModal(undefined, "유효하지 않은 이메일 주소입니다.");
break;
case "auth/operation-not-allowed":
openModal(undefined, "회원가입이 현재 허용되지 않습니다.");
break;
default:
openModal(undefined, "회원가입 중 알 수 없는 오류가 발생했습니다.");
}
}
};
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
const target = event.target;
target.classList.remove("active");
if (!target.value) return;
const label = target.nextElementSibling as HTMLLabelElement | null;
if (label) {
if (
!target.checkValidity() ||
(target.name === "passwordConfirm" &&
registerPw !== "registerPwConfirm") ||
(target.name === "nickname" && !nicknameRule.test(target.value)) ||
(target.name === "fullname" && !fullnameRule.test(target.value))
) {
label.classList.add("invalid");
} else {
label.classList.remove("invalid");
}
}
};
const handleNicknameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const capitalizedNickName = value.charAt(0).toUpperCase() + value.slice(1);
setRegisterNickname(capitalizedNickName);
};
const handleFullnameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const capitalizedName = value.charAt(0).toUpperCase() + value.slice(1);
setRegisterFullname(capitalizedName);
};
const handleAgeValidation = (isValid: boolean | null) => {
if (!birthYear || !birthMonth || !birthDay) {
return;
}
setIsValidAge(!isValid);
};
const validateStepOne = () => {
if (!registerEmail) {
openModal(undefined, "이메일을 입력해주세요.");
return false;
} else if (!isValidEmail(registerEmail)) {
openModal(undefined, "유효하지 않은 이메일 주소입니다.");
return false;
} else if (registerPw !== registerPwConfirm) {
openModal(undefined, "비밀번호가 일치하지 않습니다.");
return false;
} else if (registerPw.length < 6) {
openModal(undefined, "비밀번호는 6자리 이상이어야 합니다.");
return false;
} else {
return true;
}
};
const handleNextStep = () => {
if (validateStepOne()) {
setStep(2);
}
};
const handleBirthdateChange = (year: string, month: string, day: string) => {
setBirthYear(year);
setBirthMonth(month);
setBirthDay(day);
};
const handleGenderChange = (onGenderChange: string) => {
setGender(onGenderChange);
};
const handlePhoneNumberChange = (code: string, phoneNumber: string) => {
setCountryCode(code);
setRegisterPhonenumber(phoneNumber);
};
const handlePrevStep = () => {
setStep(1);
};
return (
<Div className="page">
{isLoading && <LoadingSpinner />}
<Modal
isOpen={isModalOpen}
onClose={closeModal}
title={modalTitle}
content={modalContent}
/>
<Div className="container">
<Form onSubmit={handleSignUp}>
{step === 1 && (
<Div className="form-wrapper">
<H3>회원가입</H3>
<>
<Label htmlFor="email">
이메일
<Input
name="email"
type="email"
id="email"
value={registerEmail}
onChange={(event) => setRegisterEmail(event.target.value)}
onBlur={handleBlur}
required
autoComplete="username"
placeholder="[email protected]"
/>
</Label>
<Label htmlFor="password">
비밀번호
<Input
name="password"
type="password"
id="password"
value={registerPw}
minLength={6}
onChange={(event) => setRegisterPw(event.target.value)}
onBlur={handleBlur}
required
autoComplete="current-password"
placeholder="6자리 이상 입력하세요"
/>
</Label>
<Label htmlFor="passwordConfirm">
비밀번호 확인
<Input
name="passwordConfirm"
type="password"
id="passwordConfirm"
value={registerPwConfirm}
minLength={6}
onChange={(event) =>
setRegisterPwConfirm(event.target.value)
}
onBlur={handleBlur}
required
autoComplete="current-password"
placeholder="6자리 이상 입력하세요"
/>
</Label>
<BaseButton
type="button"
text="다음"
onClick={handleNextStep}
/>
<Div className="register-box">
<H4>이미 회원이신가요?</H4>
<Links
href={"/login"}
>
로그인하기
</Links>
</Div>
</>
</Div>
)}
{step === 2 && (
<Div className="register-step02">
<Div className="form-wrapper step02-left">
<Div className="title-container">
<H3>회원가입</H3>
<Button onClick={handlePrevStep} className="back">
이전 단계로
</Button>
</Div>
<Label htmlFor="fullname">
이름
<Input
name="fullname"
type="text"
id="fullname"
value={registerFullname}
onChange={handleFullnameChange}
onBlur={handleBlur}
required
placeholder="자신의 이름"
/>
</Label>
<Label htmlFor="nickname">
닉네임
<Input
name="nickname"
type="text"
id="nickname"
value={registerNickname}
minLength={2}
onChange={handleNicknameChange}
onBlur={handleBlur}
required
placeholder="한글 또는 영문, 숫자 조합"
/>
</Label>
<CalculateAge
isAdult={handleAgeValidation}
onBirthdateChange={handleBirthdateChange}
/>
</Div>
<Div className="form-wrapper step02-right">
<PhoneNumber onPhoneNumberChange={handlePhoneNumberChange} />
<GenderSelect onGenderChange={handleGenderChange} />
<BaseButton type="submit" text="가입하기" />
<Div className="register-box">
<H4>이미 회원이신가요?</H4>
<Links
href={"/login"}
>
로그인하기
</Links>
</Div>
</Div>
</Div>
)}
</Form>
</Div>
</Div>
);
}
에러 내용
./src/components/Register.page.component/CalculateAge.tsx
89:6 Warning: React Hook useEffect has missing dependencies: 'isAdult', 'isValid', 'onBirthdateChange', and 'openModal'. Either include them or remove the dependency array. If 'onBirthdateChange' changes too often, find the parent component that defines it and wrap that definition in useCallback. react-hooks/exhaustive-deps
수정 후
import { useState, useEffect, useCallback } from "react";
import {
Div,
Fieldset,
Legend,
Select,
P,
} from "../Login.page.component/Login.style";
import Modal from "../Modal/Modal";
import { useModal } from "../../hooks/useModal";
interface CalculateAgeProps {
isAdult: (isValid: boolean | null) => void;
onBirthdateChange: (year: string, month: string, day: string) => void;
}
const CalculateAge = ({ isAdult, onBirthdateChange }: CalculateAgeProps) => {
const { isModalOpen, modalTitle, modalContent, openModal, closeModal } = useModal();
const [year, setYear] = useState("");
const [month, setMonth] = useState("");
const [day, setDay] = useState("");
const [isValid, setIsValid] = useState<boolean | null>(null);
const years = Array.from({ length: new Date().getFullYear() - 1899 }, (_, i) => new Date().getFullYear() - i);
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const days = Array.from({ length: 31 }, (_, i) => i + 1);
const yearChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setYear(event.target.value);
setIsValid(null);
};
const monthChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setMonth(event.target.value);
setIsValid(null);
};
const dayChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setDay(event.target.value);
setIsValid(null);
};
// ✅ 부모 함수들을 `useCallback`으로 감싸기
const handleBirthdateChange = useCallback(
(year: string, month: string, day: string) => {
onBirthdateChange(year, month, day);
},
[onBirthdateChange]
);
const handleAdultCheck = useCallback(
(isValid: boolean | null) => {
isAdult(isValid);
},
[isAdult]
);
const handleOpenModal = useCallback(
(title: string, content: JSX.Element) => {
openModal(title, content);
},
[openModal]
);
// ✅ `useEffect` 내부에서 직접 부모 함수를 호출하지 않고, `useCallback`으로 감싼 함수를 사용!
useEffect(() => {
if (!year || !month || !day) {
setIsValid(null);
handleAdultCheck(null);
return;
}
const validateDate = () => {
const birthDate = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10)
);
const today = new Date();
const age = today.getFullYear() - birthDate.getFullYear();
const isUnder19 =
age < 19 ||
(age === 19 &&
(today.getMonth() < birthDate.getMonth() ||
(today.getMonth() === birthDate.getMonth() &&
today.getDate() < birthDate.getDate())));
if (isUnder19) {
handleOpenModal(
"잠깐!",
<Div className="modal-box">
<P>19세 미만 회원의 경우,</P>
<P>이용에 제한이 있을 수 있습니다.</P>
</Div>
);
setIsValid(false);
handleAdultCheck(false);
} else {
setIsValid(true);
handleAdultCheck(true);
}
};
validateDate();
handleBirthdateChange(year, month, day);
}, [year, month, day, handleBirthdateChange, handleAdultCheck, handleOpenModal]);
return (
<Fieldset>
<Modal isOpen={isModalOpen} onClose={closeModal} title={modalTitle} content={modalContent} />
<Legend>생년월일</Legend>
<Div className={`item-box ${isValid === true ? "valid" : ""} ${isValid === false ? "invalid" : ""}`}>
<Select name="year" value={year} onChange={yearChange} required>
<option value="" disabled hidden>년</option>
{years.map((year) => (
<option key={year} value={year}>{year}</option>
))}
</Select>
<Select name="month" value={month} onChange={monthChange} required>
<option value="" disabled hidden>월</option>
{months.map((month) => (
<option key={month} value={month}>{month}</option>
))}
</Select>
<Select name="day" value={day} onChange={dayChange} required>
<option value="" disabled hidden>일</option>
{days.map((day) => (
<option key={day} value={day}>{day}</option>
))}
</Select>
</Div>
</Fieldset>
);
};
export default CalculateAge;