logistream 공차중계용 회원가입 구현
This commit is contained in:
parent
a2e58c3848
commit
22b6404a5b
|
|
@ -384,4 +384,71 @@ export class AuthController {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/signup
|
||||
* 회원가입 API
|
||||
*/
|
||||
static async signup(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { userId, password, userName, phoneNumber, licenseNumber, vehicleNumber } = req.body;
|
||||
|
||||
logger.info(`=== 회원가입 API 호출 ===`);
|
||||
logger.info(`userId: ${userId}`);
|
||||
|
||||
// 입력값 검증
|
||||
if (!userId || !password || !userName || !phoneNumber || !licenseNumber || !vehicleNumber) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "모든 필수 항목을 입력해주세요.",
|
||||
error: {
|
||||
code: "INVALID_INPUT",
|
||||
details: "필수 입력값이 누락되었습니다.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 회원가입 처리
|
||||
const signupResult = await AuthService.signupUser({
|
||||
userId,
|
||||
password,
|
||||
userName,
|
||||
phoneNumber,
|
||||
licenseNumber,
|
||||
vehicleNumber,
|
||||
});
|
||||
|
||||
if (signupResult.success) {
|
||||
logger.info(`회원가입 성공: ${userId}`);
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "회원가입이 완료되었습니다.",
|
||||
data: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logger.warn(`회원가입 실패: ${userId} - ${signupResult.message}`);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: signupResult.message || "회원가입에 실패했습니다.",
|
||||
error: {
|
||||
code: "SIGNUP_FAILED",
|
||||
details: signupResult.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("회원가입 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "회원가입 처리 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "SIGNUP_ERROR",
|
||||
details: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,4 +41,10 @@ router.post("/logout", AuthController.logout);
|
|||
*/
|
||||
router.post("/refresh", AuthController.refreshToken);
|
||||
|
||||
/**
|
||||
* POST /api/auth/signup
|
||||
* 회원가입 API
|
||||
*/
|
||||
router.post("/signup", AuthController.signup);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -342,4 +342,77 @@ export class AuthService {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 처리
|
||||
*/
|
||||
static async signupUser(data: {
|
||||
userId: string;
|
||||
password: string;
|
||||
userName: string;
|
||||
phoneNumber: string;
|
||||
licenseNumber: string;
|
||||
vehicleNumber: string;
|
||||
}): Promise<{ success: boolean; message?: string }> {
|
||||
try {
|
||||
const { userId, password, userName, phoneNumber, licenseNumber, vehicleNumber } = data;
|
||||
|
||||
// 1. 중복 사용자 확인
|
||||
const existingUser = await query<any>(
|
||||
`SELECT user_id FROM user_info WHERE user_id = $1`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (existingUser.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "이미 존재하는 아이디입니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 비밀번호 암호화
|
||||
const bcrypt = require("bcryptjs");
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 3. 사용자 정보 저장
|
||||
await query(
|
||||
`INSERT INTO user_info (
|
||||
user_id,
|
||||
user_password,
|
||||
user_name,
|
||||
cell_phone,
|
||||
license_number,
|
||||
vehicle_number,
|
||||
company_code,
|
||||
user_type,
|
||||
status,
|
||||
regdate
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())`,
|
||||
[
|
||||
userId,
|
||||
hashedPassword,
|
||||
userName,
|
||||
phoneNumber,
|
||||
licenseNumber,
|
||||
vehicleNumber,
|
||||
"*", // 기본 회사 코드
|
||||
null, // user_type: null
|
||||
"active", // status: active
|
||||
]
|
||||
);
|
||||
|
||||
logger.info(`회원가입 성공: ${userId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "회원가입이 완료되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.error("회원가입 오류:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || "회원가입 중 오류가 발생했습니다.",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
"use client";
|
||||
|
||||
import { useSignup } from "@/hooks/useSignup";
|
||||
import { LoginHeader } from "@/components/auth/LoginHeader";
|
||||
import { SignupForm } from "@/components/auth/SignupForm";
|
||||
import { LoginFooter } from "@/components/auth/LoginFooter";
|
||||
|
||||
/**
|
||||
* 회원가입 페이지 컴포넌트
|
||||
*/
|
||||
export default function SignupPage() {
|
||||
const {
|
||||
formData,
|
||||
isLoading,
|
||||
error,
|
||||
showPassword,
|
||||
validationErrors,
|
||||
touchedFields,
|
||||
isFormValid,
|
||||
handleInputChange,
|
||||
handleBlur,
|
||||
handleSignup,
|
||||
togglePasswordVisibility,
|
||||
} = useSignup();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<LoginHeader />
|
||||
|
||||
<SignupForm
|
||||
formData={formData}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
showPassword={showPassword}
|
||||
validationErrors={validationErrors}
|
||||
touchedFields={touchedFields}
|
||||
isFormValid={isFormValid}
|
||||
onInputChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
onSubmit={handleSignup}
|
||||
onTogglePassword={togglePasswordVisibility}
|
||||
/>
|
||||
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||
import { Eye, EyeOff, Loader2 } from "lucide-react";
|
||||
import { LoginFormData } from "@/types/auth";
|
||||
import { ErrorMessage } from "./ErrorMessage";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface LoginFormProps {
|
||||
formData: LoginFormData;
|
||||
|
|
@ -28,6 +29,8 @@ export function LoginForm({
|
|||
onSubmit,
|
||||
onTogglePassword,
|
||||
}: LoginFormProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-xl">
|
||||
<CardHeader className="space-y-1">
|
||||
|
|
@ -97,6 +100,17 @@ export function LoginForm({
|
|||
"로그인"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* 회원가입 버튼 */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-11 w-full font-medium"
|
||||
onClick={() => router.push("/signup")}
|
||||
disabled={isLoading}
|
||||
>
|
||||
회원가입
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Eye, EyeOff, Loader2, ArrowLeft } from "lucide-react";
|
||||
import { SignupFormData } from "@/types/auth";
|
||||
import { ErrorMessage } from "./ErrorMessage";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface SignupFormProps {
|
||||
formData: SignupFormData;
|
||||
isLoading: boolean;
|
||||
error: string;
|
||||
showPassword: boolean;
|
||||
validationErrors: Record<string, string>;
|
||||
touchedFields: Record<string, boolean>;
|
||||
isFormValid: boolean;
|
||||
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onBlur: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onTogglePassword: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 폼 컴포넌트
|
||||
*/
|
||||
export function SignupForm({
|
||||
formData,
|
||||
isLoading,
|
||||
error,
|
||||
showPassword,
|
||||
validationErrors,
|
||||
touchedFields,
|
||||
isFormValid,
|
||||
onInputChange,
|
||||
onBlur,
|
||||
onSubmit,
|
||||
onTogglePassword,
|
||||
}: SignupFormProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-xl">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-center text-2xl">회원가입</CardTitle>
|
||||
<CardDescription className="text-center">새로운 계정을 만들어보세요</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ErrorMessage message={error} />
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{/* 아이디 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="userId">아이디 *</Label>
|
||||
<Input
|
||||
id="userId"
|
||||
name="userId"
|
||||
type="text"
|
||||
placeholder="아이디를 입력하세요"
|
||||
value={formData.userId}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
className="h-11"
|
||||
required
|
||||
/>
|
||||
{touchedFields?.userId && validationErrors.userId && (
|
||||
<p className="text-xs text-destructive">{validationErrors.userId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">비밀번호 *</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="비밀번호를 입력하세요"
|
||||
value={formData.password}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
className="h-11 pr-10"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onTogglePassword}
|
||||
className="absolute top-1/2 right-3 -translate-y-1/2 transform cursor-pointer text-slate-400 transition-colors hover:text-slate-600"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
{touchedFields?.password && validationErrors.password && (
|
||||
<p className="text-xs text-destructive">{validationErrors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 비밀번호 확인 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="passwordConfirm">비밀번호 확인 *</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="비밀번호를 다시 입력하세요"
|
||||
value={formData.passwordConfirm}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
className="h-11 pr-10"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onTogglePassword}
|
||||
className="absolute top-1/2 right-3 -translate-y-1/2 transform cursor-pointer text-slate-400 transition-colors hover:text-slate-600"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
{touchedFields?.passwordConfirm && validationErrors.passwordConfirm && (
|
||||
<p className="text-xs text-destructive">{validationErrors.passwordConfirm}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 이름 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="userName">이름 *</Label>
|
||||
<Input
|
||||
id="userName"
|
||||
name="userName"
|
||||
type="text"
|
||||
placeholder="이름을 입력하세요"
|
||||
value={formData.userName}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
className="h-11"
|
||||
required
|
||||
/>
|
||||
{touchedFields?.userName && validationErrors.userName && (
|
||||
<p className="text-xs text-destructive">{validationErrors.userName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 연락처 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phoneNumber">연락처 *</Label>
|
||||
<Input
|
||||
id="phoneNumber"
|
||||
name="phoneNumber"
|
||||
type="text"
|
||||
placeholder="010-1234-5678"
|
||||
value={formData.phoneNumber}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
className="h-11"
|
||||
required
|
||||
/>
|
||||
{touchedFields?.phoneNumber && validationErrors.phoneNumber && (
|
||||
<p className="text-xs text-destructive">{validationErrors.phoneNumber}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 면허번호 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="licenseNumber">면허번호 *</Label>
|
||||
<Input
|
||||
id="licenseNumber"
|
||||
name="licenseNumber"
|
||||
type="text"
|
||||
placeholder="12-34-567890-12"
|
||||
value={formData.licenseNumber}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
className="h-11"
|
||||
required
|
||||
/>
|
||||
{touchedFields?.licenseNumber && validationErrors.licenseNumber && (
|
||||
<p className="text-xs text-destructive">{validationErrors.licenseNumber}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 차량번호 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vehicleNumber">차량번호 *</Label>
|
||||
<Input
|
||||
id="vehicleNumber"
|
||||
name="vehicleNumber"
|
||||
type="text"
|
||||
placeholder="12가1234"
|
||||
value={formData.vehicleNumber}
|
||||
onChange={onInputChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
className="h-11"
|
||||
required
|
||||
/>
|
||||
{touchedFields?.vehicleNumber && validationErrors.vehicleNumber && (
|
||||
<p className="text-xs text-destructive">{validationErrors.vehicleNumber}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 회원가입 버튼 */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-11 w-full bg-slate-900 font-medium text-white hover:bg-slate-800"
|
||||
disabled={isLoading || !isFormValid}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
가입 중...
|
||||
</>
|
||||
) : (
|
||||
"회원가입"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* 로그인으로 돌아가기 버튼 */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-11 w-full font-medium"
|
||||
onClick={() => router.push("/login")}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
로그인으로 돌아가기
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
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;
|
||||
},
|
||||
|
||||
// 아이디: 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: "",
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { apiClient } from "./client";
|
||||
import { SignupFormData, SignupResponse } from "@/types/auth";
|
||||
|
||||
/**
|
||||
* 회원가입 API
|
||||
*/
|
||||
export async function signupUser(data: SignupFormData): Promise<SignupResponse> {
|
||||
try {
|
||||
const response = await apiClient.post<SignupResponse>("/auth/signup", data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response?.data) {
|
||||
return error.response.data;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || "회원가입 중 오류가 발생했습니다",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -7,6 +7,16 @@ export interface LoginFormData {
|
|||
password: string;
|
||||
}
|
||||
|
||||
export interface SignupFormData {
|
||||
userId: string;
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
userName: string;
|
||||
phoneNumber: string;
|
||||
licenseNumber: string;
|
||||
vehicleNumber: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
|
|
@ -18,6 +28,14 @@ export interface LoginResponse {
|
|||
errorCode?: string;
|
||||
}
|
||||
|
||||
export interface SignupResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: {
|
||||
userId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthStatus {
|
||||
isLoggedIn: boolean;
|
||||
isAdmin?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in New Issue