2025-11-13 16:06:39 +09:00
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-28 17:24:18 +09:00
|
|
|
// 차량 타입: 필수 입력
|
|
|
|
|
vehicleType: (value: string): string | null => {
|
|
|
|
|
if (!value) return "차량 타입을 입력해주세요";
|
|
|
|
|
if (value.length < 2) return "차량 타입은 2자 이상이어야 합니다";
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-13 16:06:39 +09:00
|
|
|
// 아이디: 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: "",
|
2025-11-28 17:24:18 +09:00
|
|
|
vehicleType: "",
|
2025-11-13 16:06:39 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|