Refactor RegisterForm component

Splits the RegisterForm component into smaller, more manageable components to improve code readability and maintainability.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-15 16:18:10 +00:00
parent 7024c6423f
commit 5d1ff46c3e
4 changed files with 283 additions and 193 deletions

View File

@@ -0,0 +1,55 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { Mail, InfoIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
interface EmailConfirmationProps {
email: string;
onBackToForm: () => void;
}
const EmailConfirmation: React.FC<EmailConfirmationProps> = ({ email, onBackToForm }) => {
const navigate = useNavigate();
return (
<div className="neuro-flat p-8 mb-6">
<div className="text-center space-y-6">
<Mail className="w-16 h-16 mx-auto text-neuro-income" />
<h2 className="text-2xl font-bold"> </h2>
<p className="text-gray-600">
<strong>{email}</strong> .
.
</p>
<Alert className="bg-blue-50 border-blue-200 my-6">
<InfoIcon className="h-5 w-5 text-blue-600" />
<AlertTitle className="text-blue-700"> ?</AlertTitle>
<AlertDescription className="text-blue-600">
. .
</AlertDescription>
</Alert>
<div className="space-y-4 pt-4">
<Button
onClick={() => navigate("/login")}
variant="outline"
className="w-full"
>
</Button>
<Button
onClick={onBackToForm}
variant="ghost"
className="w-full"
>
</Button>
</div>
</div>
</div>
);
};
export default EmailConfirmation;

View File

@@ -1,34 +1,18 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrowRight, Mail, KeyRound, User, Eye, EyeOff } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { useToast } from "@/hooks/useToast.wrapper"; import { useToast } from "@/hooks/useToast.wrapper";
import { verifyServerConnection } from "@/contexts/auth/auth.utils"; import { verifyServerConnection } from "@/contexts/auth/auth.utils";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { ServerStatus, SignUpResponse } from "./types";
import { InfoIcon } from "lucide-react"; import EmailConfirmation from "./EmailConfirmation";
import RegisterFormFields from "./RegisterFormFields";
interface RegisterFormProps { interface RegisterFormProps {
signUp: (email: string, password: string, username: string) => Promise<{ signUp: (email: string, password: string, username: string) => Promise<SignUpResponse>;
error: any; serverStatus: ServerStatus;
user: any; setServerStatus: React.Dispatch<React.SetStateAction<ServerStatus>>;
redirectToSettings?: boolean;
emailConfirmationRequired?: boolean;
}>;
serverStatus: {
checked: boolean;
connected: boolean;
message: string;
};
setServerStatus: React.Dispatch<
React.SetStateAction<{
checked: boolean;
connected: boolean;
message: string;
}>
>;
setRegisterError: React.Dispatch<React.SetStateAction<string | null>>; setRegisterError: React.Dispatch<React.SetStateAction<string | null>>;
} }
@@ -49,10 +33,50 @@ const RegisterForm: React.FC<RegisterFormProps> = ({
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const handleRegister = async (e: React.FormEvent) => { const validateForm = (): boolean => {
e.preventDefault(); if (!username || !email || !password || !confirmPassword) {
setRegisterError(null); toast({
title: "입력 오류",
description: "모든 필드를 입력해주세요.",
variant: "destructive",
});
return false;
}
if (password !== confirmPassword) {
toast({
title: "비밀번호 불일치",
description: "비밀번호와 비밀번호 확인이 일치하지 않습니다.",
variant: "destructive",
});
return false;
}
// 비밀번호 강도 검사
if (password.length < 8) {
toast({
title: "비밀번호 강도 부족",
description: "비밀번호는 최소 8자 이상이어야 합니다.",
variant: "destructive",
});
return false;
}
// 이메일 형식 검사
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
toast({
title: "이메일 형식 오류",
description: "유효한 이메일 주소를 입력해주세요.",
variant: "destructive",
});
return false;
}
return true;
};
const checkServerConnectivity = async (): Promise<boolean> => {
// 서버 연결 상태 재확인 // 서버 연결 상태 재확인
if (!serverStatus.connected) { if (!serverStatus.connected) {
try { try {
@@ -69,7 +93,7 @@ const RegisterForm: React.FC<RegisterFormProps> = ({
description: "서버에 연결할 수 없습니다. 네트워크 또는 서버 상태를 확인하세요.", description: "서버에 연결할 수 없습니다. 네트워크 또는 서버 상태를 확인하세요.",
variant: "destructive" variant: "destructive"
}); });
return; return false;
} }
} catch (error: any) { } catch (error: any) {
toast({ toast({
@@ -77,48 +101,22 @@ const RegisterForm: React.FC<RegisterFormProps> = ({
description: error.message || "서버 연결 확인 중 오류가 발생했습니다.", description: error.message || "서버 연결 확인 중 오류가 발생했습니다.",
variant: "destructive" variant: "destructive"
}); });
return; return false;
} }
} }
return true;
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setRegisterError(null);
if (!username || !email || !password || !confirmPassword) { // 서버 연결 확인
toast({ const isServerConnected = await checkServerConnectivity();
title: "입력 오류", if (!isServerConnected) return;
description: "모든 필드를 입력해주세요.",
variant: "destructive",
});
return;
}
if (password !== confirmPassword) { // 폼 유효성 검사
toast({ if (!validateForm()) return;
title: "비밀번호 불일치",
description: "비밀번호와 비밀번호 확인이 일치하지 않습니다.",
variant: "destructive",
});
return;
}
// 비밀번호 강도 검사
if (password.length < 8) {
toast({
title: "비밀번호 강도 부족",
description: "비밀번호는 최소 8자 이상이어야 합니다.",
variant: "destructive",
});
return;
}
// 이메일 형식 검사
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
toast({
title: "이메일 형식 오류",
description: "유효한 이메일 주소를 입력해주세요.",
variant: "destructive",
});
return;
}
setIsLoading(true); setIsLoading(true);
@@ -203,140 +201,36 @@ const RegisterForm: React.FC<RegisterFormProps> = ({
// 이메일 인증 안내 화면 (인증 메일이 발송된 경우) // 이메일 인증 안내 화면 (인증 메일이 발송된 경우)
if (emailConfirmationSent) { if (emailConfirmationSent) {
return ( return <EmailConfirmation
<div className="neuro-flat p-8 mb-6"> email={email}
<div className="text-center space-y-6"> onBackToForm={() => setEmailConfirmationSent(false)}
<Mail className="w-16 h-16 mx-auto text-neuro-income" /> />;
<h2 className="text-2xl font-bold"> </h2>
<p className="text-gray-600">
<strong>{email}</strong> .
.
</p>
<Alert className="bg-blue-50 border-blue-200 my-6">
<InfoIcon className="h-5 w-5 text-blue-600" />
<AlertTitle className="text-blue-700"> ?</AlertTitle>
<AlertDescription className="text-blue-600">
. .
</AlertDescription>
</Alert>
<div className="space-y-4 pt-4">
<Button
onClick={() => navigate("/login")}
variant="outline"
className="w-full"
>
</Button>
<Button
onClick={() => setEmailConfirmationSent(false)}
variant="ghost"
className="w-full"
>
</Button>
</div>
</div>
</div>
);
} }
// 일반 회원가입 양식 // 일반 회원가입 양식
return ( return (
<div className="neuro-flat p-8 mb-6"> <div className="neuro-flat p-8 mb-6">
<form onSubmit={handleRegister}> <form onSubmit={handleRegister}>
<div className="space-y-6"> <RegisterFormFields
<div className="space-y-2"> username={username}
<Label htmlFor="username" className="text-base"></Label> setUsername={setUsername}
<div className="relative"> email={email}
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> setEmail={setEmail}
<Input password={password}
id="username" setPassword={setPassword}
type="text" confirmPassword={confirmPassword}
placeholder="홍길동" setConfirmPassword={setConfirmPassword}
value={username} showPassword={showPassword}
onChange={(e) => setUsername(e.target.value)} setShowPassword={setShowPassword}
className="pl-10 neuro-pressed" />
/>
</div> <Button
</div> type="submit"
className="w-full bg-neuro-income hover:bg-neuro-income/80 text-white py-6 h-auto mt-6"
<div className="space-y-2"> disabled={isLoading || (!serverStatus.connected && serverStatus.checked)}
<Label htmlFor="email" className="text-base"></Label> >
<div className="relative"> {isLoading ? "가입 중..." : "회원가입"} {!isLoading && <ArrowRight className="ml-2 h-5 w-5" />}
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" /> </Button>
<Input
id="email"
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="pl-10 neuro-pressed"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-base"></Label>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 neuro-pressed"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
{password && password.length > 0 && password.length < 8 && (
<p className="text-xs text-red-500 mt-1"> 8 .</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-base"> </Label>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
<Input
id="confirmPassword"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="pl-10 neuro-pressed"
/>
</div>
{confirmPassword && password !== confirmPassword && (
<p className="text-xs text-red-500 mt-1"> .</p>
)}
</div>
<Alert className="bg-amber-50 border-amber-200">
<InfoIcon className="h-5 w-5 text-amber-600" />
<AlertTitle className="text-amber-700"> </AlertTitle>
<AlertDescription className="text-amber-600">
.
.
</AlertDescription>
</Alert>
<Button
type="submit"
className="w-full bg-neuro-income hover:bg-neuro-income/80 text-white py-6 h-auto"
disabled={isLoading || (!serverStatus.connected && serverStatus.checked)}
>
{isLoading ? "가입 중..." : "회원가입"} {!isLoading && <ArrowRight className="ml-2 h-5 w-5" />}
</Button>
</div>
</form> </form>
</div> </div>
); );

View File

@@ -0,0 +1,120 @@
import React, { useState } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { User, Mail, KeyRound, Eye, EyeOff, InfoIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
interface RegisterFormFieldsProps {
username: string;
setUsername: (value: string) => void;
email: string;
setEmail: (value: string) => void;
password: string;
setPassword: (value: string) => void;
confirmPassword: string;
setConfirmPassword: (value: string) => void;
showPassword: boolean;
setShowPassword: (value: boolean) => void;
}
const RegisterFormFields: React.FC<RegisterFormFieldsProps> = ({
username,
setUsername,
email,
setEmail,
password,
setPassword,
confirmPassword,
setConfirmPassword,
showPassword,
setShowPassword
}) => {
return (
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor="username" className="text-base"></Label>
<div className="relative">
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
<Input
id="username"
type="text"
placeholder="홍길동"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10 neuro-pressed"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-base"></Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
<Input
id="email"
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="pl-10 neuro-pressed"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-base"></Label>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 neuro-pressed"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
{password && password.length > 0 && password.length < 8 && (
<p className="text-xs text-red-500 mt-1"> 8 .</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-base"> </Label>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5" />
<Input
id="confirmPassword"
type={showPassword ? "text" : "password"}
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="pl-10 neuro-pressed"
/>
</div>
{confirmPassword && password !== confirmPassword && (
<p className="text-xs text-red-500 mt-1"> .</p>
)}
</div>
<Alert className="bg-amber-50 border-amber-200">
<InfoIcon className="h-5 w-5 text-amber-600" />
<AlertTitle className="text-amber-700"> </AlertTitle>
<AlertDescription className="text-amber-600">
.
.
</AlertDescription>
</Alert>
</div>
);
};
export default RegisterFormFields;

View File

@@ -0,0 +1,21 @@
// 서버 연결 상태 타입
export interface ServerStatus {
checked: boolean;
connected: boolean;
message: string;
}
// 회원가입 응답 타입
export interface SignUpResponse {
error: any;
user: any;
redirectToSettings?: boolean;
emailConfirmationRequired?: boolean;
}
// 회원가입 폼 공통 props
export interface RegisterFormCommonProps {
serverStatus: ServerStatus;
setRegisterError: React.Dispatch<React.SetStateAction<string | null>>;
}