diff --git a/features/auth/components/login-form.tsx b/features/auth/components/login-form.tsx new file mode 100644 index 0000000..9c23db7 --- /dev/null +++ b/features/auth/components/login-form.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { + login, + signInWithGoogle, + signInWithKakao, +} from "@/features/auth/actions"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Separator } from "@/components/ui/separator"; +import { InlineSpinner } from "@/components/ui/loading-spinner"; + +/** + * [로그인 폼 클라이언트 컴포넌트] + * + * 이메일 기억하기 기능을 제공하는 로그인 폼입니다. + * - localStorage를 사용하여 이메일 저장/불러오기 + * - 체크박스 선택 시 이메일 자동 저장 + * - 서버 액션(login)과 연동 + */ +export default function LoginForm() { + // ========== 상태 관리 ========== + // 초기 상태를 함수로 지연 초기화하여 localStorage 읽기 + const [email, setEmail] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem("auto-trade-saved-email") || ""; + } + return ""; + }); + + const [rememberMe, setRememberMe] = useState(() => { + if (typeof window !== "undefined") { + return !!localStorage.getItem("auto-trade-saved-email"); + } + return false; + }); + + const [isLoading, setIsLoading] = useState(false); + + // ========== 폼 제출 핸들러 ========== + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + const formData = new FormData(e.currentTarget); + + // localStorage 처리 (동기) + if (rememberMe) { + localStorage.setItem("auto-trade-saved-email", email); + } else { + localStorage.removeItem("auto-trade-saved-email"); + } + + // 서버 액션 호출 (리다이렉트 발생) + try { + await login(formData); + } catch (error) { + console.error("Login error:", error); + setIsLoading(false); + } + }; + + return ( +
+ {/* ========== 로그인 폼 ========== */} +
+ {/* ========== 이메일 입력 필드 ========== */} +
+ + setEmail(e.target.value)} + className="h-11 transition-all duration-200" + /> +
+ + {/* ========== 비밀번호 입력 필드 ========== */} +
+ + +
+ + {/* ========== 이메일 기억하기 & 비밀번호 찾기 ========== */} +
+
+ setRememberMe(checked === true)} + /> + +
+ {/* 비밀번호 찾기 링크 */} + + 비밀번호 찾기 + +
+ + {/* ========== 로그인 버튼 ========== */} + + + {/* ========== 회원가입 링크 ========== */} +

+ 계정이 없으신가요?{" "} + + 회원가입 하기 + +

+
+ + {/* ========== 소셜 로그인 구분선 ========== */} +
+ + + 또는 소셜 로그인 + +
+ + {/* ========== 소셜 로그인 버튼들 ========== */} +
+ {/* ========== Google 로그인 버튼 ========== */} +
+ +
+ + {/* ========== Kakao 로그인 버튼 ========== */} +
+ +
+
+
+ ); +} diff --git a/features/auth/components/reset-password-form.tsx b/features/auth/components/reset-password-form.tsx new file mode 100644 index 0000000..d0eceab --- /dev/null +++ b/features/auth/components/reset-password-form.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { updatePassword } from "@/features/auth/actions"; +import { + resetPasswordSchema, + type ResetPasswordFormData, +} from "@/features/auth/schemas/auth-schema"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { InlineSpinner } from "@/components/ui/loading-spinner"; +import { useState } from "react"; + +/** + * [비밀번호 재설정 폼 클라이언트 컴포넌트 - React Hook Form 버전] + * + * React Hook Form과 Zod를 사용한 비밀번호 재설정 폼입니다. + * - 타입 안전한 폼 검증 + * - 비밀번호/비밀번호 확인 일치 검증 + * - 로딩 상태 표시 + * + * @see app/reset-password/page.tsx - 이 컴포넌트를 사용하는 페이지 + */ +export default function ResetPasswordForm() { + // ========== 로딩 상태 ========== + const [isLoading, setIsLoading] = useState(false); + const [serverError, setServerError] = useState(""); + + // ========== React Hook Form 설정 ========== + const { + register, + handleSubmit, + formState: { errors }, + watch, + } = useForm({ + resolver: zodResolver(resetPasswordSchema), + mode: "onBlur", + }); + + // 비밀번호 실시간 감시 + const password = watch("password"); + const confirmPassword = watch("confirmPassword"); + + // ========== 폼 제출 핸들러 ========== + const onSubmit = async (data: ResetPasswordFormData) => { + setServerError(""); + setIsLoading(true); + + try { + const formData = new FormData(); + formData.append("password", data.password); + + await updatePassword(formData); + } catch (error) { + console.error("Password reset error:", error); + setServerError("비밀번호 변경 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* ========== 서버 에러 메시지 표시 ========== */} + {serverError && ( +
+ {serverError} +
+ )} + + {/* ========== 새 비밀번호 입력 ========== */} +
+ + +

+ 최소 8자 이상, 대문자, 소문자, 숫자, 특수문자 포함 +

+ {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ + {/* ========== 비밀번호 확인 필드 ========== */} +
+ + + {/* 비밀번호 불일치 시 실시간 피드백 */} + {confirmPassword && + password !== confirmPassword && + !errors.confirmPassword && ( +

+ 비밀번호가 일치하지 않습니다 +

+ )} + {/* 비밀번호 일치 시 확인 메시지 */} + {confirmPassword && + password === confirmPassword && + !errors.confirmPassword && ( +

+ 비밀번호가 일치합니다 ✓ +

+ )} + {/* Zod 검증 에러 */} + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+ + {/* ========== 비밀번호 변경 버튼 ========== */} + +
+ ); +} diff --git a/features/auth/components/signup-form.tsx b/features/auth/components/signup-form.tsx new file mode 100644 index 0000000..48ac351 --- /dev/null +++ b/features/auth/components/signup-form.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { signup } from "@/features/auth/actions"; +import { + signupSchema, + type SignupFormData, +} from "@/features/auth/schemas/auth-schema"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { InlineSpinner } from "@/components/ui/loading-spinner"; +import { useState } from "react"; + +/** + * [회원가입 폼 클라이언트 컴포넌트 - React Hook Form 버전] + * + * React Hook Form과 Zod를 사용한 회원가입 폼입니다. + * - 타입 안전한 폼 검증 + * - 자동 에러 메시지 관리 + * - 비밀번호/비밀번호 확인 일치 검증 + * - 로딩 상태 표시 + * + * @see app/signup/page.tsx - 이 컴포넌트를 사용하는 페이지 + */ +export default function SignupForm() { + // ========== 로딩 상태 ========== + const [isLoading, setIsLoading] = useState(false); + const [serverError, setServerError] = useState(""); + + // ========== React Hook Form 설정 ========== + const { + register, + handleSubmit, + formState: { errors }, + watch, + } = useForm({ + resolver: zodResolver(signupSchema), + mode: "onBlur", // 포커스 아웃 시 검증 + }); + + // 비밀번호 실시간 감시 (일치 여부 표시용) + const password = watch("password"); + const confirmPassword = watch("confirmPassword"); + + // ========== 폼 제출 핸들러 ========== + const onSubmit = async (data: SignupFormData) => { + setServerError(""); + setIsLoading(true); + + try { + const formData = new FormData(); + formData.append("email", data.email); + formData.append("password", data.password); + + await signup(formData); + } catch (error) { + console.error("Signup error:", error); + setServerError("회원가입 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* ========== 서버 에러 메시지 표시 ========== */} + {serverError && ( +
+ {serverError} +
+ )} + + {/* ========== 이메일 입력 필드 ========== */} +
+ + + {errors.email && ( +

+ {errors.email.message} +

+ )} +
+ + {/* ========== 비밀번호 입력 필드 ========== */} +
+ + +

+ 최소 8자 이상, 대문자, 소문자, 숫자, 특수문자 포함 +

+ {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ + {/* ========== 비밀번호 확인 필드 ========== */} +
+ + + {/* 비밀번호 불일치 시 실시간 피드백 */} + {confirmPassword && + password !== confirmPassword && + !errors.confirmPassword && ( +

+ 비밀번호가 일치하지 않습니다 +

+ )} + {/* 비밀번호 일치 시 확인 메시지 */} + {confirmPassword && + password === confirmPassword && + !errors.confirmPassword && ( +

+ 비밀번호가 일치합니다 ✓ +

+ )} + {/* Zod 검증 에러 */} + {errors.confirmPassword && ( +

+ {errors.confirmPassword.message} +

+ )} +
+ + {/* ========== 회원가입 버튼 ========== */} + +
+ ); +}