Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions src/app/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-10 mb-20 flex flex-col items-center justify-center px-4">
<h1 className="mb-8 text-2xl font-bold text-white">회원가입</h1>
<SignUpForm onSuccess={handleSuccess} />

{showModal && (
<Modal
message="회원가입이 완료되었습니다."
buttonText="로그인 하러 가기"
onClick={handleCloseModal}
className=""
/>
)}
<SignUpForm
onSuccess={() => {
openModal("normal", {
message: "회원가입이 완료되었습니다.",
buttonText: "로그인 하러 가기",
onClick: () => {
closeModal();
router.push("/login");
},
});
}}
onError={(msg) => {
openModal("normal", {
message: msg,
buttonText: "확인",
onClick: closeModal,
});
}}
/>

<Link href="/login" className="mt-6 text-sm text-white/60">
이미 계정이 있으신가요? <span className="underline">로그인</span>
</Link>
</div>
);
}
30 changes: 15 additions & 15 deletions src/features/auth/ui/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [passwordError, setPasswordError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(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이메일과 비밀번호를 확인해주세요.");
Expand Down Expand Up @@ -100,7 +94,13 @@ export const LoginForm = ({
!email || emailError !== null || !password || passwordError !== null
}
>
{isLoading ? "로그인 중 ..." : "로그인"}
{isLoading ? (
<span className="flex items-center gap-1">
로그인 중 <LoadingDots />
</span>
) : (
"로그인"
)}
</Button>
</form>
);
Expand Down
52 changes: 23 additions & 29 deletions src/features/auth/ui/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,23 @@ 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;
onError?: (msg: string) => void;
}

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<number | "">("");
const [month, setMonth] = useState<number | "">("");
const [day, setDay] = useState<number | "">("");

// input error 상태
const [emailError, setEmailError] = useState<string | null>(null);
const [nicknameError, setNicknameError] = useState<string | null>(null);
const [passwordError, setPasswordError] = useState<string | null>(null);
Expand All @@ -34,13 +32,11 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => {

const [isLoading, setIsLoading] = useState<boolean>(false);

//이메일 유효성 검사
const validateEmail = (value: string) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value);
};

//input value change 핸들러
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
Expand Down Expand Up @@ -87,7 +83,6 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => {
}
};

// password, confirmPassword 동기화 체크
useEffect(() => {
if (password && confirmPassword && password !== confirmPassword) {
setConfirmPasswordError("비밀번호가 일치하지 않습니다.");
Expand All @@ -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 };
});

Expand All @@ -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 {
Expand Down Expand Up @@ -199,7 +184,6 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => {
onChange={handleConfirmPasswordChange}
/>

{/* 생년월일 DropDown */}
<div className="flex flex-col gap-2">
<label className="text-[16px] font-medium text-white">생년월일</label>
<div className="flex gap-4">
Expand All @@ -214,19 +198,23 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => {
placeholder="연도"
className={dropDownWidth}
/>

<DropDown
options={monthOptions()}
value={month}
disabled={!year}
onChange={(val) => {
setMonth(val as number);
setDay("");
}}
placeholder="월"
className={dropDownWidth}
/>

<DropDown
options={dayOptions()}
value={day}
disabled={!year || !month}
onChange={(val) => setDay(val as number)}
placeholder="일"
className={dropDownWidth}
Expand All @@ -252,7 +240,13 @@ export const SignUpForm = ({ onSuccess, onError }: SignUpFormProps) => {
!day
}
>
{isLoading ? "가입 중 ..." : "가입하기"}
{isLoading ? (
<span className="flex items-center gap-2">
가입 중 <LoadingDots />
</span>
) : (
"가입하기"
)}
</Button>
</form>
);
Expand Down
16 changes: 16 additions & 0 deletions src/shared/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading