Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[김도훈] Sprint 11 #331

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
Binary file added my-app/public/Img_home_01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/Img_home_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/Img_home_03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/Img_home_bottom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/Img_home_top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/google.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/ic_facebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/ic_instagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/ic_twitter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/ic_youtube.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/kakao.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/visibility.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/icon/visible.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added my-app/public/og_img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions my-app/src/app/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const url = "https://panda-market-api.vercel.app/auth";
export const signup = async (data: {
email: string;
nickname: string;
password: string;
passwordConfirmation?: string;
}) => {
const response = await fetch(`${url}/signUp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});

if (!response.ok) {
throw new Error("회원가입 실패");
}

return response.json();
};

export const login = async (data: { email: string; password: string }) => {
const response = await fetch(`${url}/signIn`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});

if (!response.ok) {
throw new Error("로그인 실패");
}

const responseData = await response.json();
return responseData;
};
319 changes: 319 additions & 0 deletions my-app/src/app/components/CommonForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
"use client";

import Image from "next/image";
import Link from "next/link";
import { AuthFormProps } from "../type/type";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { signup, login } from "../api/api";

export default function CommonForm({ type }: AuthFormProps) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중복되는 부분을 공통 컴포넌트화 하시려 한 것 같은데요, 이 영역은 오히려 분리해서 작성하는게 유지보수 측면에서 더 낫지 않을까 싶네요..🤔
하나의 컴포넌트에 로그인과 회원가입 모두 다루려고 하다보니 state 가 많아져 가독성이 떨어지고, handleSubmit 라는 하나의 함수 안에서 분기처리를 하게되면 나중에 더 복잡한 로직이 추가되었을 때 점점 더 읽기 어려운 코드가 될 것 같아요.
차라리 공통된 로직 부분을 custom hook 으로 분리해보시는건 어떨까요? email, password, showPassword 등의 로직은 훅으로 빼서 재사용하게 만들면 좋을 것 같습니다.

const isLogin = type === "login";
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState("");
const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState("");
const [repassword, setRePassword] = useState("");
const [repasswordError, setRePasswordError] = useState("");
const [nickname, setNickname] = useState("");
const [nicknameError, setNicknameError] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showRePassword, setShowRePassword] = useState(false);
const router = useRouter();

useEffect(() => {}, [email, nickname, password, repassword]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 줄은 왜 필요한건가요?? 실수로 넣으신거겠죵?


const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();

try {
if (type === "signup") {
const signupData = {
email,
nickname,
password,
passwordConfirmation: repassword,
};
await signup(signupData); // 회원가입 API 호출
router.push("/login");
alert("회원가입 성공!");
} else if (type === "login") {
const loginData = {
email,
password,
};
const data = await login(loginData); // 로그인 API 호출
if (data && data.accessToken) {
localStorage.setItem("token", data.accessToken); // 토큰 저장
router.push("/");
alert("로그인 성공!");
} else {
alert("로그인 실패: 토큰이 없습니다.");
}
}
} catch (error: any) {
const errorMessage =
error.response?.data?.message ||
error.message ||
"알 수 없는 오류가 발생했습니다.";
alert(errorMessage);
}
};
const validateEmail = (email: string) => {
if (!email) {
setEmailError("이메일을 입력해주세요");
} else if (!/\S+@\S+\.\S+/.test(email)) {
setEmailError("올바른 이메일 형식을 입력해주세요");
} else {
setEmailError("");
}
};

const validatePassword = (password: string) => {
if (!password) {
setPasswordError("비밀번호를 입력해주세요");
} else if (password.length < 8) {
setPasswordError("비밀번호는 8자리 이상이어야 합니다");
} else {
setPasswordError("");
}
};
const validateRePassword = (repassword: string, password: string) => {
if (!repassword) {
setRePasswordError("비밀번호를 다시 한 번 입력해주세요");
} else if (repassword !== password) {
setRePasswordError("비밀번호가 일치하지 않습니다.");
} else {
setRePasswordError("");
}
};

// 이메일 입력 변경 시 에러 메시지 제거
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
setEmailError(""); // 실시간으로 에러 메시지 제거
validateEmail(e.target.value); // 실시간 이메일 유효성 검사
};

// 비밀번호 입력 변경 시 에러 메시지 제거
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
setPasswordError(""); // 실시간으로 에러 메시지 제거
validatePassword(e.target.value); // 실시간 비밀번호 유효성 검사
};

// 비밀번호 입력 변경 시 에러 메시지 제거
const handleRePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRePassword(e.target.value);
setRePasswordError(""); // 실시간으로 에러 메시지 제거
validateRePassword(e.target.value, password); // 실시간 비밀번호 확인 유효성 검사
};

// 닉네임 입력 변경 시
const handleNicknameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNickname(e.target.value);
setNicknameError("");
};

const handlePasswordVisibilityToggle = () => {
setShowPassword((prevState) => !prevState); // 비밀번호 보이기/숨기기 상태 토글
};

const handleRePasswordVisibilityToggle = () => {
setShowRePassword((prevState) => !prevState); // 비밀번호 확인 보이기/숨기기 상태 토글
};
return (
<>
<div className={`mt-[80px] ${isLogin ? "md:mt-[140px]" : ""}`}>
<div className="flex flex-col items-center pl-[16px] pr-[16px]">
<div className="w-full">
<Link
className="w-full flex items-center justify-center gap-[11px]"
href={"/"}
>
<div className="w-full h-auto max-w-[51px] md:max-w-[103px]">
<Image
className="object-contain"
src="/head/logo_face.png"
alt="로고"
width={104}
height={105}
/>
</div>
<div className="w-full h-auto max-w-[133px] md:max-w-[259px]">
<Image
className="object-contain"
src="/head/logo_txt.png"
alt="로고"
width={259}
height={64}
/>
</div>
</Link>
</div>
<form onSubmit={handleSubmit} className="mt-[60px]">
<div className="w-[343px] md:w-[640px] flex flex-col gap-[16px] md:gap-[24px]">
<div className="flex flex-col">
<label className="mb-[8px]" htmlFor="pandaEmail">
이메일
</label>
<input
className="w-full h-[56px] bg-gray100 pl-[24px] rounded-[12px]"
type="email"
placeholder="이메일을 입력해주세요"
id="pandaEmail"
value={email}
onChange={handleEmailChange}
required
/>
<div className="text-red">{emailError}</div>
</div>
{!isLogin && (
<div className="flex flex-col">
<label className="mb-[8px]" htmlFor="pandaName">
닉네임
</label>
<input
className="w-full h-[56px] bg-gray100 pl-[24px] rounded-[12px]"
type="text"
placeholder="닉네임"
id="pandaName"
value={nickname}
onChange={handleNicknameChange}
required
/>
</div>
)}
<div className="flex flex-col">
<label className="mb-[8px]" htmlFor="pandaPassword">
비밀번호
</label>
<div className="relative">
<input
className="w-full h-[56px] bg-gray100 pl-[24px] rounded-[12px]"
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 입력해주세요"
id="pandaPassword"
value={password}
onChange={handlePasswordChange}
required
/>
<div className="absolute top-1/2 right-6 w-[22px] h-[19px] transform -translate-y-1/2 ">
<div
className="w-full h-auto max-w-[22px] absolute opacity-1 pointer-events-auto"
onClick={handlePasswordVisibilityToggle}
>
<Image
className="object-contain"
src={
showPassword
? "/icon/visible.png"
: "/icon/visibility.png"
}
alt="클릭시비밀번호보기"
width={22}
height={19}
/>
</div>
</div>
</div>
<div className="text-red">{passwordError}</div>
</div>
{!isLogin && (
<div className="flex flex-col">
<label className="mb-[8px]" htmlFor="repandaPassword">
비밀번호 확인
</label>
<div className="relative">
<input
className="w-full h-[56px] bg-gray100 pl-[24px] rounded-[12px]"
type={showRePassword ? "text" : "password"}
placeholder="비밀번호를 다시 한 번 입력해주세요"
id="repandaPassword"
value={repassword}
onChange={handleRePasswordChange}
required
/>
<div
className="absolute top-1/2 right-6 w-[22px] h-[19px] transform -translate-y-1/2"
onClick={handleRePasswordVisibilityToggle}
>
<Image
className="object-contain"
src={
showRePassword
? "/icon/visible.png"
: "/icon/visibility.png"
}
alt="클릭시비밀번호보기"
width={22}
height={19}
/>
</div>
</div>
<div className="text-red">{repasswordError}</div>
</div>
)}
<button
type="submit"
className="w-full h-[56px] text-center text-background text-[20px] text-gray100 bg-gray400 rounded-[40px]"
disabled={
!!emailError ||
!!passwordError ||
(!isLogin && (!!nicknameError || !!repasswordError))
}
>
{isLogin ? "로그인" : "회원가입"}
</button>

<div className="flex justify-between items-center w-full h-[74px] bg-mainbg rounded-[8px] pl-[24px] pr-[24px]">
<p className="w-full">간편 로그인하기</p>
<div className="w-full flex justify-end gap-[16px]">
<div className="w-[42px] h-[42px] rounded-full relative bg-background">
<Link
className="w-[22px] h-auto absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
href="https://www.google.com/"
>
<Image
className="object-contain"
src="/icon/google.png"
alt="구글로고"
width={22}
height={22}
/>
</Link>
</div>
<div className="w-[42px] h-[42px] rounded-full relative bg-background">
<Link
className="w-[22px] h-auto absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
href="https://www.kakaocorp.com/page/"
>
<Image
className="object-contain"
src="/icon/kakao.png"
alt="카카오톡로고"
width={26}
height={24}
/>
</Link>
</div>
</div>
</div>
<div className="flex justify-center gap-[4px]">
<span className="text-[14px]">
{isLogin ? "판다마켓이 처음이신가요?" : "이미 회원이신가요?"}
</span>
<Link
className="text-skyblue"
href={isLogin ? "/signup" : "/login"}
>
{isLogin ? "회원가입" : "로그인"}
</Link>
</div>
</div>
</form>
</div>
</div>
</>
);
}
Loading
Loading