diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 7fb6e29e..0a4b7ec6 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,36 +1,41 @@ "use client"; -import React, { useState } from "react"; import { SignUpForm } from "@/features/auth/ui/SignUpForm/SignUpForm"; -import { Modal } from "@/shared/ui/Modal/Modal"; +import Link from "next/link"; +import { useModalStore } from "@/shared/model/modal.store"; import { useRouter } from "next/navigation"; export default function SignUpPage() { - const [showModal, setShowModal] = useState(false); + const { openModal, closeModal } = useModalStore(); const router = useRouter(); - const handleSuccess = () => { - setShowModal(true); // 회원가입 성공 시 모달 띄우기 - }; - - const handleCloseModal = () => { - setShowModal(false); - router.push("/login"); // 모달 닫고 로그인 페이지로 이동 - }; - return (

회원가입

- - {showModal && ( - - )} + { + openModal("normal", { + message: "회원가입이 완료되었습니다.", + buttonText: "로그인 하러 가기", + onClick: () => { + closeModal(); + router.push("/login"); + }, + }); + }} + onError={(msg) => { + openModal("normal", { + message: msg, + buttonText: "확인", + onClick: closeModal, + }); + }} + /> + + + 이미 계정이 있으신가요? 로그인 +
); } diff --git a/src/features/auth/ui/LoginForm/LoginForm.tsx b/src/features/auth/ui/LoginForm/LoginForm.tsx index 51e503b2..e9e71320 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.tsx @@ -3,43 +3,37 @@ import React, { useState } from "react"; import { Input } from "@/entities/user/ui/Input/Input"; import Button from "@/shared/ui/Button/Button"; -import { useAuthStore } from "../../model/auth.store"; +import { LoadingDots } from "@/shared/ui/Loading/LoadingDots"; import { apiFetch } from "@/shared/api/fetcher"; - -type FormSize = "sm" | "md" | "lg"; +import { useAuthStore } from "../../model/auth.store"; interface LoginFormProps { - size?: FormSize; onSuccess?: () => void; onError?: (msg: string) => void; } -export const LoginForm = ({ - size = "lg", - onSuccess, - onError, - ...props -}: LoginFormProps) => { +export const LoginForm = ({ onSuccess, onError }: LoginFormProps) => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(null); const [passwordError, setPasswordError] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const { setAccessToken } = useAuthStore(); const handleSubmit = async (e: React.FormEvent) => { - setIsLoading(true); e.preventDefault(); + setIsLoading(true); try { - const data = await apiFetch<{ accessToken: string }>("/api/auth/login", { + const res = await apiFetch<{ accessToken: string }>("/api/auth/login", { method: "POST", body: JSON.stringify({ email, password }), noAuth: true, }); - setAccessToken(data.accessToken); + setAccessToken(res.accessToken); onSuccess?.(); } catch (error) { onError?.("로그인에 실패하였습니다.\n이메일과 비밀번호를 확인해주세요."); @@ -100,7 +94,13 @@ export const LoginForm = ({ !email || emailError !== null || !password || passwordError !== null } > - {isLoading ? "로그인 중 ..." : "로그인"} + {isLoading ? ( + + 로그인 중 + + ) : ( + "로그인" + )} ); diff --git a/src/features/auth/ui/SignUpForm/SignUpForm.tsx b/src/features/auth/ui/SignUpForm/SignUpForm.tsx index 3190de6b..4f9a5930 100644 --- a/src/features/auth/ui/SignUpForm/SignUpForm.tsx +++ b/src/features/auth/ui/SignUpForm/SignUpForm.tsx @@ -6,6 +6,7 @@ import { DropDown } from "@/shared/ui/DropDown/DropDown"; import Button from "@/shared/ui/Button/Button"; import { apiFetch } from "@/shared/api/fetcher"; import cn from "@/shared/lib/cn"; +import { LoadingDots } from "@/shared/ui/Loading/LoadingDots"; interface SignUpFormProps { onSuccess?: () => void; @@ -13,18 +14,15 @@ interface SignUpFormProps { } export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { - //input text 상태 const [email, setEmail] = useState(""); const [nickname, setNickname] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); - // 생년월일 상태 const [year, setYear] = useState(""); const [month, setMonth] = useState(""); const [day, setDay] = useState(""); - // input error 상태 const [emailError, setEmailError] = useState(null); const [nicknameError, setNicknameError] = useState(null); const [passwordError, setPasswordError] = useState(null); @@ -34,13 +32,11 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { const [isLoading, setIsLoading] = useState(false); - //이메일 유효성 검사 const validateEmail = (value: string) => { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(value); }; - //input value change 핸들러 const handleEmailChange = (e: React.ChangeEvent) => { const value = e.target.value; setEmail(value); @@ -87,7 +83,6 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { } }; - // password, confirmPassword 동기화 체크 useEffect(() => { if (password && confirmPassword && password !== confirmPassword) { setConfirmPasswordError("비밀번호가 일치하지 않습니다."); @@ -96,9 +91,8 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { } }, [password, confirmPassword]); - //연, 월, 일 드롭다운 옵션 생성 const yearOptions = Array.from({ length: 2025 - 1900 + 1 }, (_, i) => { - const y = 1900 + i; + const y = 2025 - i; return { label: `${y}년`, value: y }; }); @@ -119,40 +113,31 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { }); }; - // 회원가입 폼 제출 핸들러 const handleSubmit = async (e: React.FormEvent) => { - setIsLoading(true); e.preventDefault(); - console.log("회원가입 요청 폼 제출"); - try { - const body = { - email, - nickname, - birthDate: `${year}-${String(month).padStart(2, "0")}-${String( - day, - ).padStart(2, "0")}`, - password, - }; + setIsLoading(true); - console.log(body); - const res = await apiFetch("/api/users/", { + try { + await apiFetch("/api/users", { method: "POST", - body: JSON.stringify(body), + body: JSON.stringify({ + email, + nickname, + birthDate: `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`, + password, + }), noAuth: true, }); - console.log("회원가입 성공:", res); onSuccess?.(); } catch (error: unknown) { - console.error("회원가입 실패:", error + "\n"); if (error instanceof Error) { - // 서버 에러 메시지 처리 if (error.message.includes("EMAIL_DUPLICATE")) { setEmailError("이미 사용 중인 이메일입니다."); } else if (error.message.includes("NICKNAME_DUPLICATE")) { setNicknameError("이미 사용 중인 닉네임입니다."); } else { - onError?.("회원가입에 실패했습니다. 잠시 후 다시 시도해주세요."); + onError?.("회원가입에 실패했습니다.\n잠시 후 다시 시도해주세요."); } } } finally { @@ -199,7 +184,6 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { onChange={handleConfirmPasswordChange} /> - {/* 생년월일 DropDown */}
@@ -214,9 +198,11 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { placeholder="연도" className={dropDownWidth} /> + { setMonth(val as number); setDay(""); @@ -224,9 +210,11 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { placeholder="월" className={dropDownWidth} /> + setDay(val as number)} placeholder="일" className={dropDownWidth} @@ -252,7 +240,13 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => { !day } > - {isLoading ? "가입 중 ..." : "가입하기"} + {isLoading ? ( + + 가입 중 + + ) : ( + "가입하기" + )} ); diff --git a/src/shared/styles/globals.css b/src/shared/styles/globals.css index ab8f6770..316128a3 100644 --- a/src/shared/styles/globals.css +++ b/src/shared/styles/globals.css @@ -35,3 +35,19 @@ display: none; /* Chrome, Safari */ } } + +@keyframes dotScale { + 0%, + 100% { + transform: scale(1); + opacity: 0.5; + } + 50% { + transform: scale(1.6); + opacity: 1; + } +} + +.dot-animate { + animation: dotScale 0.6s infinite ease-in-out; +} diff --git a/src/shared/ui/DropDown/DropDown.tsx b/src/shared/ui/DropDown/DropDown.tsx index 407d4312..d492b7dc 100644 --- a/src/shared/ui/DropDown/DropDown.tsx +++ b/src/shared/ui/DropDown/DropDown.tsx @@ -14,6 +14,7 @@ interface DropDownProps { onChange: (value: string | number) => void; placeholder?: string; className?: string; + disabled?: boolean; } export const DropDown = ({ @@ -22,6 +23,7 @@ export const DropDown = ({ onChange, placeholder = "선택하세요", className, + disabled = false, }: DropDownProps) => { const [open, setOpen] = useState(false); const ref = useRef(null); @@ -38,13 +40,22 @@ export const DropDown = ({ }; }, []); + const toggleOpen = () => { + if (disabled) return; + if (options.length === 0) return; + setOpen((prev) => !prev); + }; + return (
setOpen((prev) => !prev)} + onClick={toggleOpen} className={cn( - "bg-black-800 flex cursor-pointer items-center justify-between rounded-lg border px-4 transition-colors", - open ? "border-blue" : "border-gray-400", + "bg-black-800 flex items-center justify-between rounded-lg border px-4 transition-colors", + disabled + ? "cursor-not-allowed border-gray-600 opacity-50" + : "cursor-pointer", + open && !disabled ? "border-blue" : "border-gray-400", "h-[55px] w-[335px] text-[14px]", "md:h-[60px] md:w-[360px] md:text-[14px]", "xl:h-[70px] xl:w-[400px] xl:text-[16px]", @@ -56,20 +67,23 @@ export const DropDown = ({ ? options.find((opt) => opt.value === value)?.label : placeholder} - {open ? ( - arrow up - ) : ( - arrow down + + {!disabled && ( + <> + {open ? ( + arrow up + ) : ( + arrow down + )} + )}
@@ -83,10 +97,10 @@ export const DropDown = ({ "w-[335px] gap-[5px] p-[10px]", "md:w-[360px] md:gap-[5px] md:p-[10px]", "xl:w-[400px] xl:gap-[5px] xl:p-[10px]", - className, - open + open && !disabled ? "pointer-events-auto scale-y-100 opacity-100" : "pointer-events-none scale-y-0 opacity-0", + className, )} > {options.map((opt) => ( @@ -102,7 +116,6 @@ export const DropDown = ({ "w-[380px] gap-[5px] p-[10px]", "md:w-[360px] md:gap-[5px] md:p-[10px]", "xl:w-[400px] xl:gap-[5px] xl:p-[10px]", - className, )} > {opt.label} diff --git a/src/shared/ui/Loading/LoadingDots.tsx b/src/shared/ui/Loading/LoadingDots.tsx new file mode 100644 index 00000000..7d059e9b --- /dev/null +++ b/src/shared/ui/Loading/LoadingDots.tsx @@ -0,0 +1,17 @@ +"use client"; + +export const LoadingDots = () => { + return ( + + + • + + + • + + + • + + + ); +}; diff --git a/src/widgets/main/ui/Client/LoginPage.client.tsx b/src/widgets/main/ui/Client/LoginPage.client.tsx index ec7ee67a..706ec3de 100644 --- a/src/widgets/main/ui/Client/LoginPage.client.tsx +++ b/src/widgets/main/ui/Client/LoginPage.client.tsx @@ -1,17 +1,19 @@ "use client"; -import React, { useEffect, useState } from "react"; +import { useEffect } from "react"; import { LoginForm } from "@/features/auth/ui/LoginForm/LoginForm"; -import { Modal } from "@/shared/ui/Modal/Modal"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuthStore } from "@/features/auth/model/auth.store"; import { useChatStore } from "@/features/chat/model/chat.store"; +import { useModalStore } from "@/shared/model/modal.store"; +import Link from "next/link"; + export default function LoginPageClient() { const router = useRouter(); const searchParams = useSearchParams(); const expired = searchParams.get("expired"); - const [errorMessage, setErrorMessage] = useState(null); + const { openModal, closeModal } = useModalStore(); const { setIsLogined, logout } = useAuthStore(); const { unmount } = useChatStore(); @@ -19,32 +21,36 @@ export default function LoginPageClient() { if (expired === "true") { unmount(); logout(); - setErrorMessage("세션이 만료되었습니다.\n다시 로그인해주세요."); + + openModal("normal", { + message: "세션이 만료되었습니다.\n다시 로그인해주세요.", + buttonText: "확인", + onClick: () => closeModal(), + }); } }, [expired]); return (

로그인

+ { setIsLogined(true); - router.push("/"); //메인 페이지 이동 + router.push("/"); }} onError={(msg) => { - setErrorMessage(msg); + openModal("normal", { + message: msg, + buttonText: "확인", + onClick: () => closeModal(), + }); }} /> - {!!errorMessage && ( - setErrorMessage(null)} - className="" - /> - )} + + 아직 회원이 아니신가요? 회원가입 +
); }