ERP-node/frontend/hooks/useSignup.ts

244 lines
7.4 KiB
TypeScript

import { useState, useCallback, useEffect } from "react";
import { useRouter } from "next/navigation";
import { SignupFormData } from "@/types/auth";
import { signupUser } from "@/lib/api/auth";
import { useToast } from "@/hooks/use-toast";
/**
* 유효성 검사 함수들
*/
const validators = {
// 연락처: 010-1234-5678 형식
phoneNumber: (value: string): string | null => {
const phoneRegex = /^01[0-9]-\d{3,4}-\d{4}$/;
if (!value) return "연락처를 입력해주세요";
if (!phoneRegex.test(value)) return "올바른 연락처 형식이 아닙니다 (예: 010-1234-5678)";
return null;
},
// 면허번호: 12-34-567890-12 형식
licenseNumber: (value: string): string | null => {
const licenseRegex = /^\d{2}-\d{2}-\d{6}-\d{2}$/;
if (!value) return "면허번호를 입력해주세요";
if (!licenseRegex.test(value)) return "올바른 면허번호 형식이 아닙니다 (예: 12-34-567890-12)";
return null;
},
// 차량번호: 12가1234 형식
vehicleNumber: (value: string): string | null => {
const vehicleRegex = /^\d{2,3}[가-힣]\d{4}$/;
if (!value) return "차량번호를 입력해주세요";
if (!vehicleRegex.test(value)) return "올바른 차량번호 형식이 아닙니다 (예: 12가1234)";
return null;
},
// 차량 타입: 필수 입력
vehicleType: (value: string): string | null => {
if (!value) return "차량 타입을 입력해주세요";
if (value.length < 2) return "차량 타입은 2자 이상이어야 합니다";
return null;
},
// 아이디: 4자 이상
userId: (value: string): string | null => {
if (!value) return "아이디를 입력해주세요";
if (value.length < 4) return "아이디는 4자 이상이어야 합니다";
return null;
},
// 비밀번호: 6자 이상
password: (value: string): string | null => {
if (!value) return "비밀번호를 입력해주세요";
if (value.length < 6) return "비밀번호는 6자 이상이어야 합니다";
return null;
},
// 비밀번호 확인: password와 일치해야 함
passwordConfirm: (value: string, password?: string): string | null => {
if (!value) return "비밀번호 확인을 입력해주세요";
if (password && value !== password) return "비밀번호가 일치하지 않습니다";
return null;
},
// 이름: 2자 이상
userName: (value: string): string | null => {
if (!value) return "이름을 입력해주세요";
if (value.length < 2) return "이름은 2자 이상이어야 합니다";
return null;
},
};
/**
* 회원가입 커스텀 훅
*/
export function useSignup() {
const router = useRouter();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState<SignupFormData>({
userId: "",
password: "",
passwordConfirm: "",
userName: "",
phoneNumber: "",
licenseNumber: "",
vehicleNumber: "",
vehicleType: "",
});
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
const [touchedFields, setTouchedFields] = useState<Record<string, boolean>>({});
const [isFormValid, setIsFormValid] = useState(false);
// 유효성 검사 실행
const validateField = useCallback((name: string, value: string, password?: string) => {
const validator = validators[name as keyof typeof validators];
if (validator) {
// passwordConfirm 검증 시 password도 함께 전달
if (name === "passwordConfirm") {
return validator(value, password);
}
return validator(value);
}
return null;
}, []);
// 전체 폼 유효성 검사
const validateForm = useCallback(() => {
const errors: Record<string, string> = {};
let hasErrors = false;
Object.keys(formData).forEach((key) => {
const error = validateField(key, formData[key as keyof SignupFormData], formData.password);
if (error) {
errors[key] = error;
hasErrors = true;
}
});
setValidationErrors(errors);
setIsFormValid(!hasErrors && Object.values(formData).every((value) => value !== ""));
}, [formData, validateField]);
// 입력 변경 핸들러
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFormData = { ...formData, [name]: value };
setFormData(newFormData);
// touched된 필드만 실시간 유효성 검사
if (touchedFields[name]) {
const fieldError = validateField(name, value, newFormData.password);
setValidationErrors((prev) => ({
...prev,
[name]: fieldError || "",
}));
}
// password가 변경되면 passwordConfirm도 다시 검증
if (name === "password" && touchedFields.passwordConfirm) {
const confirmError = validateField("passwordConfirm", newFormData.passwordConfirm, value);
setValidationErrors((prev) => ({
...prev,
passwordConfirm: confirmError || "",
}));
}
setError("");
},
[validateField, touchedFields, formData],
);
// 포커스 아웃 핸들러 (Blur)
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
const { name, value } = e.target;
// 필드를 touched로 표시
setTouchedFields((prev) => ({ ...prev, [name]: true }));
// 유효성 검사 실행
const fieldError = validateField(name, value, formData.password);
setValidationErrors((prev) => ({
...prev,
[name]: fieldError || "",
}));
},
[validateField, formData.password],
);
// 회원가입 제출
const handleSignup = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
setError("");
// 모든 필드를 touched로 표시 (제출 시 모든 에러 표시)
const allTouched = Object.keys(formData).reduce((acc, key) => ({ ...acc, [key]: true }), {});
setTouchedFields(allTouched);
// 최종 유효성 검사
validateForm();
if (!isFormValid) {
setError("입력 정보를 다시 확인해주세요");
return;
}
setIsLoading(true);
try {
const response = await signupUser(formData);
if (response.success) {
toast({
title: "회원가입 성공",
description: "로그인 페이지로 이동합니다",
});
// 1초 후 로그인 페이지로 이동
setTimeout(() => {
router.push("/login");
}, 1000);
} else {
setError(response.message || "회원가입에 실패했습니다");
}
} catch (err: any) {
console.error("회원가입 오류:", err);
setError(err.message || "회원가입 중 오류가 발생했습니다");
} finally {
setIsLoading(false);
}
},
[formData, isFormValid, router, toast, validateForm],
);
// 비밀번호 표시 토글
const togglePasswordVisibility = useCallback(() => {
setShowPassword((prev) => !prev);
}, []);
// 폼 데이터 변경 시 유효성 검사 실행
useEffect(() => {
validateForm();
}, [formData, validateForm]);
return {
formData,
isLoading,
error,
showPassword,
validationErrors,
touchedFields,
isFormValid,
handleInputChange,
handleBlur,
handleSignup,
togglePasswordVisibility,
};
}