+
-
- {isLocal && "Me"} {nickname}
+
+ {isVideoOn && nickname}
{renderMicIcon()}
@@ -75,22 +108,13 @@ const VideoContainer = ({
)}
- {
-
- {renderReaction(reaction)}
-
- }
+
+
);
};
diff --git a/frontend/src/components/session/VideoProfileOverlay.tsx b/frontend/src/components/session/VideoProfileOverlay.tsx
new file mode 100644
index 00000000..8e40e900
--- /dev/null
+++ b/frontend/src/components/session/VideoProfileOverlay.tsx
@@ -0,0 +1,41 @@
+interface VideoProfileOverlayProps {
+ isVideoOn: boolean;
+ videoLoading: boolean;
+ nickname: string;
+ profileImage?: string;
+}
+
+const VideoProfileOverlay = ({
+ isVideoOn,
+ videoLoading,
+ nickname,
+ profileImage,
+}: VideoProfileOverlayProps) => {
+ return (
+ !isVideoOn &&
+ !videoLoading && (
+
+ {profileImage ? (
+
+
+
+ ) : (
+
{nickname}
+ )}
+
+ )
+ );
+};
+export default VideoProfileOverlay;
diff --git a/frontend/src/components/session/VideoReactionBox.tsx b/frontend/src/components/session/VideoReactionBox.tsx
new file mode 100644
index 00000000..074494b7
--- /dev/null
+++ b/frontend/src/components/session/VideoReactionBox.tsx
@@ -0,0 +1,28 @@
+interface VideoReactionBoxProps {
+ reaction: string;
+ renderReaction: (reactionType: string) => string;
+}
+
+const VideoReactionBox = ({
+ reaction,
+ renderReaction,
+}: VideoReactionBoxProps) => {
+ return (
+
+ {renderReaction(reaction)}
+
+ );
+};
+
+export default VideoReactionBox;
diff --git a/frontend/src/components/sessions/SessionCard.tsx b/frontend/src/components/sessions/SessionCard.tsx
deleted file mode 100644
index f6049387..00000000
--- a/frontend/src/components/sessions/SessionCard.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { FaUserGroup } from "react-icons/fa6";
-import { IoArrowForwardSharp } from "react-icons/io5";
-interface Props {
- category?: string;
- title: string;
- host: string;
- inProgress: boolean;
- participant: number;
- maxParticipant: number;
- questionListId: number;
- onEnter: () => void;
-}
-
-const SessionCard = ({
- category = "None",
- title,
- host,
- participant,
- maxParticipant,
- inProgress,
- questionListId,
- onEnter,
-}: Props) => {
- return (
-
-
-
- {category}
-
-
{title}
-
- 질문지인데 누르면 질문 리스트를 볼 수 있음 {questionListId}
-
-
-
- {host} •
-
- {" "}
- 참여자
- {participant}/{maxParticipant}명
-
-
- {!inProgress && (
-
- 참여하기 {" "}
-
-
- )}
-
-
-
- );
-};
-
-export default SessionCard;
diff --git a/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx b/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx
index 078fda6c..8a1930da 100644
--- a/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx
+++ b/frontend/src/components/sessions/create/SessionForm/CategorySection/index.tsx
@@ -1,6 +1,6 @@
import SelectTitle from "@/components/common/SelectTitle";
import useSessionFormStore from "@/stores/useSessionFormStore";
-import CategorySelector from "@/components/common/CategorySelector";
+import Select from "@/components/common/Select";
const options = [
{
@@ -19,11 +19,11 @@ const CategorySection = () => {
return (
-
);
diff --git a/frontend/src/hooks/__test__/useSession.test.ts b/frontend/src/hooks/__test__/useSession.test.ts
index c140208b..dccca483 100644
--- a/frontend/src/hooks/__test__/useSession.test.ts
+++ b/frontend/src/hooks/__test__/useSession.test.ts
@@ -1,8 +1,5 @@
import { renderHook } from "@testing-library/react";
-import { useSession } from "@hooks/session/useSession";
import useSocketStore from "@stores/useSocketStore";
-import useMediaDevices from "@hooks/session/useMediaDevices";
-import usePeerConnection from "@hooks/session/usePeerConnection";
import { useNavigate } from "react-router-dom";
import { act } from "react";
import {
@@ -18,6 +15,9 @@ import {
SESSION_LISTEN_EVENT,
} from "@/constants/WebSocket/SessionEvent";
import { SIGNAL_LISTEN_EVENT } from "@/constants/WebSocket/SignalingEvent";
+import useMediaDevices from "@/pages/SessionPage/hooks/useMediaDevices";
+import usePeerConnection from "@/pages/SessionPage/hooks/usePeerConnection";
+import { useSession } from "@/pages/SessionPage/hooks/useSession";
const REACTION_DURATION = 3000;
diff --git a/frontend/src/hooks/api/useGetQuestionContent.ts b/frontend/src/hooks/api/useGetQuestionContent.ts
index 6a3a6cf2..6d479497 100644
--- a/frontend/src/hooks/api/useGetQuestionContent.ts
+++ b/frontend/src/hooks/api/useGetQuestionContent.ts
@@ -27,5 +27,5 @@ export const useGetQuestionContent = (questionListId: number) => {
data,
isLoading,
error,
- }
+ };
};
diff --git a/frontend/src/hooks/api/useGetQuestionList.ts b/frontend/src/hooks/api/useGetQuestionList.ts
index 4968950d..62fb4e55 100644
--- a/frontend/src/hooks/api/useGetQuestionList.ts
+++ b/frontend/src/hooks/api/useGetQuestionList.ts
@@ -1,22 +1,30 @@
-import { getQuestionList } from "@/api/question-list/getQuestionList";
+import {
+ getQuestionList,
+ getQuestionListWithCategory,
+} from "@/api/question-list/getQuestionList";
import { useQuery } from "@tanstack/react-query";
interface UseGetQuestionListProps {
- page: number;
- limit: number;
+ page?: number;
+ limit?: number;
+ category: string;
}
-export const useCreateQuestionList = ({
+export const useQuestionList = ({
page,
limit,
+ category = "전체",
}: UseGetQuestionListProps) => {
const { data, isLoading, error } = useQuery({
- queryKey: ["questions", page, limit],
- queryFn: () => getQuestionList({ page, limit }),
+ queryKey: ["questions", page, limit, category],
+ queryFn: () =>
+ category !== "전체"
+ ? getQuestionListWithCategory({ categoryName: category, page, limit })
+ : getQuestionList({ page, limit }),
});
return {
- questions: data,
+ data,
isLoading,
error,
};
diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts
index 0ee4fc3d..6262695b 100644
--- a/frontend/src/hooks/useAuth.ts
+++ b/frontend/src/hooks/useAuth.ts
@@ -49,8 +49,48 @@ const useAuth = () => {
};
const guestLogIn = () => {
- setNickname("게스트 사용자");
+ const randomNickname = createGuestRandomNickname();
+ setNickname(randomNickname);
guestLogin();
+ return randomNickname;
+ };
+
+ const firstNames = [
+ "신나는",
+ "즐거운",
+ "행복한",
+ "멋있는",
+ "귀여운",
+ "활기찬",
+ "열정적인",
+ "용감한",
+ "영리한",
+ "현명한",
+ "따뜻한",
+ "차가운",
+ ];
+
+ const secondNames = [
+ "판다",
+ "고양이",
+ "강아지",
+ "토끼",
+ "사자",
+ "기린",
+ "코끼리",
+ "하마",
+ "펭귄",
+ "여우",
+ "눈사람",
+ ];
+
+ const createGuestRandomNickname = () => {
+ // 각 배열에서 랜덤한 인덱스 선택
+ const randomFirstIndex = Math.floor(Math.random() * firstNames.length);
+ const randomSecondIndex = Math.floor(Math.random() * secondNames.length);
+
+ // 선택된 단어들을 조합
+ return `${firstNames[randomFirstIndex]} ${secondNames[randomSecondIndex]}${Math.floor(Math.random() * 1000)}`;
};
return {
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 31fb23af..1b52e920 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -21,9 +21,37 @@
}
}
+
+
:root {
--bg-color-default: #fafafa;
--text-default: #101010;
+ /* Gray Colors */
+ --color-gray-white: #FFFFFF;
+ --color-gray-50: #FAFAFA;
+ --color-gray-100: #EDEDED;
+ --color-gray-200: #E1E1E1;
+ --color-gray-300: #D9D9D9;
+ --color-gray-400: #6C6C6C;
+ --color-gray-500: #5E5E5E;
+ --color-gray-600: #3F3F3F;
+ --color-gray-black: #171717;
+
+ /* Green Colors */
+ --color-green-50: #F1FBF7;
+ --color-green-100: #01BF6F;
+ --color-green-200: #01AC64;
+ --color-green-300: #019959;
+ --color-green-400: #018F53;
+ --color-green-500: #017343;
+ --color-green-600: #005632;
+ --color-green-700: #004327;
+
+ /* Point Colors */
+ --color-point-1: #F04040;
+ --color-point-2: #DFDDD5;
+ --color-point-3: #2572E6;
+
line-height: 1.5;
font-weight: 400;
@@ -40,8 +68,28 @@
.dark {
--bg-color-default: #141414;
--text-default: #ffffff;
+ --color-gray-white: #3E3E3E;
+ --color-green-100: #3AF2A4;
+ --color-green-200: #3AF2A4;
+ --color-green-300: #3AF2A4;
+ --color-green-400: #3AF2A4;
+ --color-green-500: #3AF2A4;
+}
+
+input {
+ background-color: white;
+}
+
+.dark input {
+ color: white;
+ background-color: #3E3E3E;
}
+.dark input::placeholder {
+ color: #B0B0B0;
+}
+
+
.aspect-4-3 {
aspect-ratio: 4 / 3;
}
diff --git a/frontend/src/pages/CreateQuestionPage.tsx b/frontend/src/pages/CreateQuestionPage.tsx
index 3a0cf147..55dc7ec2 100644
--- a/frontend/src/pages/CreateQuestionPage.tsx
+++ b/frontend/src/pages/CreateQuestionPage.tsx
@@ -1,37 +1,26 @@
import QuestionForm from "@/components/questions/create/QuestionForm";
import { IoArrowBackSharp } from "react-icons/io5";
-import { useNavigate } from "react-router-dom";
-import useAuth from "@hooks/useAuth.ts";
-import { useEffect } from "react";
-import useToast from "@hooks/useToast.ts";
+import { Link } from "react-router-dom";
+import SidebarPageLayout from "@components/layout/SidebarPageLayout.tsx";
const CreateQuestionPage = () => {
- const navigate = useNavigate();
- const { isLoggedIn } = useAuth();
- const toast = useToast();
-
- useEffect(() => {
- if (!isLoggedIn) {
- toast.error("로그인이 필요한 서비스입니다.");
- navigate("/questions");
- }
- }, [isLoggedIn]);
-
return (
-
-
navigate("/questions")}
- >
-
- 면접 리스트로 돌아가기
-
-
새로운 면접 질문 리스트 만들기
-
- 면접 스터디를 위한 새로운 질문지를 생성합니다.
-
-
-
+
+
+
+
+
면접 리스트로 돌아가기
+
+
새로운 면접 질문 리스트 만들기
+
+ 면접 스터디를 위한 새로운 질문지를 생성합니다.
+
+
+
+
);
};
diff --git a/frontend/src/pages/Login/LoginPage.tsx b/frontend/src/pages/Login/LoginPage.tsx
index 8ea8e263..6d4878d7 100644
--- a/frontend/src/pages/Login/LoginPage.tsx
+++ b/frontend/src/pages/Login/LoginPage.tsx
@@ -1,16 +1,14 @@
import useAuth from "@hooks/useAuth.ts";
import { useNavigate } from "react-router-dom";
-import useToast from "@hooks/useToast.ts";
import DrawingSnowman from "@components/common/Animate/DrawingSnowman.tsx";
-import Divider from "@components/common/Divider.tsx";
-import OAuthContainer from "@components/login/OAuthContainer.tsx";
-import DefaultAuthFormContainer from "@components/login/DefaultAuthFormContainer.tsx";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
+import LoginTitle from "@/pages/Login/view/LoginTitle.tsx";
+import LoginForm from "@/pages/Login/view/LoginForm.tsx";
const LoginPage = () => {
- const { isLoggedIn, guestLogIn } = useAuth();
+ const { isLoggedIn } = useAuth();
const navigate = useNavigate();
- const toast = useToast();
+ const [isSignUp, setIsSignUp] = useState(false);
useEffect(() => {
if (isLoggedIn) {
@@ -18,31 +16,6 @@ const LoginPage = () => {
}
}, [isLoggedIn]);
- const handleOAuthLogin = (provider: "github" | "guest") => {
- if (provider === "github") {
- // 깃허브 로그인
- window.location.assign(
- `https://github.com/login/oauth/authorize?client_id=${import.meta.env.VITE_OAUTH_GITHUB_ID}&redirect_uri=${import.meta.env.VITE_OAUTH_GITHUB_CALLBACK}`
- );
- } else if (provider === "guest") {
- // 게스트 로그인
- guestLogIn();
- toast.success("게스트로 로그인되었습니다.");
- navigate("/");
- }
- };
-
- const handleDefaultLogin = (e: React.MouseEvent
) => {
- try {
- e.preventDefault();
- toast.error(
- "일반 로그인은 현재 지원되지 않습니다. Github나 게스트 로그인을 이용해주세요."
- );
- } catch (err) {
- console.error("로그인 도중 에러", err);
- }
- };
-
return (
@@ -51,19 +24,9 @@ const LoginPage = () => {
-
diff --git a/frontend/src/pages/Login/hooks/useValidate.ts b/frontend/src/pages/Login/hooks/useValidate.ts
new file mode 100644
index 00000000..6882c1e7
--- /dev/null
+++ b/frontend/src/pages/Login/hooks/useValidate.ts
@@ -0,0 +1,177 @@
+import useAuth from "@hooks/useAuth.ts";
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import useToast from "@hooks/useToast.ts";
+import axios, { isAxiosError } from "axios";
+
+interface UseValidateProps {
+ setIsSignUp: (isSignUp: boolean) => void;
+}
+
+const useValidate = ({ setIsSignUp }: UseValidateProps) => {
+ const auth = useAuth();
+ const navigate = useNavigate();
+ const toast = useToast();
+
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [passwordCheck, setPasswordCheck] = useState("");
+ const [nickname, setNickname] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const handleLogin = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.post("/api/auth/login", {
+ userId: username,
+ password: password,
+ });
+
+ const { success = false } = response.data;
+
+ if (success) {
+ toast.success("로그인에 성공했습니다.");
+ auth.logIn();
+ auth.setNickname(nickname);
+ navigate("/");
+ }
+ } catch (err) {
+ if (isAxiosError(err)) {
+ const { response } = err;
+ if (response?.status === 401) {
+ toast.error("아이디 또는 비밀번호가 일치하지 않습니다.");
+ } else {
+ toast.error("로그인에 실패했습니다. 잠시 후 다시 시도해주세요.");
+ }
+ } else console.error("로그인 도중 에러", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validate = () => {
+ const errors: string[] = [];
+
+ // 아이디 검증
+ if (!username) {
+ errors.push("아이디는 필수입니다.");
+ } else {
+ if (username.length < 4) {
+ errors.push("아이디는 최소 4글자 이상이어야 합니다.");
+ }
+ if (username.length > 20) {
+ errors.push("아이디는 20글자 이하여야 합니다.");
+ }
+ if (!/^[a-zA-Z0-9_]+$/.test(username)) {
+ errors.push(
+ "아이디는 영문자, 숫자, 언더스코어(_)만 사용할 수 있습니다."
+ );
+ }
+ }
+
+ // 비밀번호 검증
+ if (!password) {
+ errors.push("비밀번호는 필수입니다.");
+ } else {
+ if (password.length < 7) {
+ errors.push("비밀번호는 최소 7글자 이상이어야 합니다.");
+ }
+ if (password.length > 20) {
+ errors.push("비밀번호는 20글자 이하여야 합니다.");
+ }
+ if (!/[a-z]/.test(password)) {
+ errors.push("비밀번호는 최소 1개의 소문자를 포함해야 합니다.");
+ }
+ if (!/[0-9]/.test(password)) {
+ errors.push("비밀번호는 최소 1개의 숫자를 포함해야 합니다.");
+ }
+ if (password.includes(username)) {
+ errors.push("비밀번호에 아이디을 포함할 수 없습니다.");
+ }
+ }
+
+ // 비밀번호 확인 검증
+ if (!passwordCheck) {
+ errors.push("비밀번호 확인은 필수입니다.");
+ } else {
+ if (password !== passwordCheck) {
+ errors.push("비밀번호가 일치하지 않습니다.");
+ }
+ }
+
+ // 닉네임 검증
+ if (!nickname) {
+ errors.push("닉네임은 필수입니다.");
+ } else {
+ if (nickname.length < 2) {
+ errors.push("닉네임은 최소 2글자 이상이어야 합니다.");
+ }
+ if (nickname.length > 10) {
+ errors.push("닉네임은 10글자 이하여야 합니다.");
+ }
+ }
+
+ return {
+ isValid: errors.length === 0,
+ errors,
+ };
+ };
+
+ const handleSignUp = async () => {
+ try {
+ setLoading(true);
+ const { isValid, errors } = validate();
+
+ if (errors.length > 0) {
+ errors.forEach((error) => {
+ toast.error(error);
+ });
+ return;
+ }
+
+ if (isValid) {
+ const response = await axios.post("/api/user/signup", {
+ id: username,
+ password: password,
+ nickname: nickname,
+ });
+
+ if (response.data.status) {
+ toast.success("회원가입에 성공했습니다. 로그인해주세요.");
+ setIsSignUp(false);
+ } else {
+ const { code } = response.data;
+ switch (code) {
+ case "DUPLICATE_NICKNAME":
+ toast.error("이미 존재하는 닉네임입니다.");
+ break;
+ case "DUPLICATE_ID":
+ toast.error("이미 존재하는 아이디입니다.");
+ break;
+ default:
+ toast.error(
+ "회원가입에 실패했습니다. 잠시 후 다시 시도해주세요."
+ );
+ break;
+ }
+ }
+ }
+ } catch (err) {
+ console.error("회원가입 도중 에러", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return {
+ setUsername,
+ setPassword,
+ setPasswordCheck,
+ setNickname,
+ loading,
+ handleLogin,
+ handleSignUp,
+ };
+};
+
+export default useValidate;
diff --git a/frontend/src/pages/Login/view/DefaultAuthFormContainer.tsx b/frontend/src/pages/Login/view/DefaultAuthFormContainer.tsx
new file mode 100644
index 00000000..b1ae4fbc
--- /dev/null
+++ b/frontend/src/pages/Login/view/DefaultAuthFormContainer.tsx
@@ -0,0 +1,112 @@
+import useToast from "@hooks/useToast.ts";
+import LoadingIndicator from "@components/common/LoadingIndicator.tsx";
+import useValidate from "@/pages/Login/hooks/useValidate.ts";
+
+interface DefaultAuthFormContainerProps {
+ isSignUp: boolean;
+ setIsSignUp: (isSignUp: boolean) => void;
+}
+
+const DefaultAuthFormContainer = ({
+ isSignUp,
+ setIsSignUp,
+}: DefaultAuthFormContainerProps) => {
+ const toast = useToast();
+ const {
+ handleSignUp,
+ handleLogin,
+ loading,
+ setNickname,
+ setUsername,
+ setPassword,
+ setPasswordCheck,
+ } = useValidate({ setIsSignUp });
+
+ const handleDefaultLogin = async (e: React.MouseEvent
) => {
+ try {
+ e.preventDefault();
+ if (isSignUp) await handleSignUp();
+ else await handleLogin();
+ } catch (err) {
+ console.error("로그인 도중 에러", err);
+ }
+ };
+
+ return (
+ <>
+
+ setUsername(e.target.value)}
+ />
+
+
+
+ setPassword(e.target.value)}
+ />
+
+
+ {isSignUp && (
+ <>
+
+ setPasswordCheck(e.target.value)}
+ />
+
+
+ setNickname(e.target.value)}
+ />
+
+ >
+ )}
+
+ handleDefaultLogin(e)}
+ className="w-full bg-green-200 dark:bg-emerald-600 text-white py-3 rounded-md hover:bg-green-100 transition-colors font-medium text-lg shadow-16"
+ >
+
+ {!loading && (!isSignUp ? "로그인" : "회원가입")}
+
+
+
+ setIsSignUp(!isSignUp)}
+ type="button"
+ className="inline-flex items-center justify-center hover:text-green-300 transition-colors"
+ >
+ {isSignUp ? "로그인" : "회원가입"}
+
+ toast.error("비밀번호 찾기는 아직 지원하지 않습니다.")}
+ type="button"
+ className="hover:text-green-300 transition-colors"
+ >
+ 비밀번호 찾기
+
+
+ >
+ );
+};
+
+export default DefaultAuthFormContainer;
diff --git a/frontend/src/pages/Login/view/LoginForm.tsx b/frontend/src/pages/Login/view/LoginForm.tsx
new file mode 100644
index 00000000..202e92b1
--- /dev/null
+++ b/frontend/src/pages/Login/view/LoginForm.tsx
@@ -0,0 +1,21 @@
+import DefaultAuthFormContainer from "@/pages/Login/view/DefaultAuthFormContainer.tsx";
+import Divider from "@components/common/Divider.tsx";
+import OAuthContainer from "@/pages/Login/view/OAuthContainer.tsx";
+
+interface LoginFormProps {
+ signUp: boolean;
+ isSignUp: (value: ((prevState: boolean) => boolean) | boolean) => void;
+}
+const LoginForm = ({ signUp, isSignUp }: LoginFormProps) => {
+ return (
+
+ );
+};
+
+export default LoginForm;
diff --git a/frontend/src/pages/Login/view/LoginTitle.tsx b/frontend/src/pages/Login/view/LoginTitle.tsx
new file mode 100644
index 00000000..dc240be0
--- /dev/null
+++ b/frontend/src/pages/Login/view/LoginTitle.tsx
@@ -0,0 +1,17 @@
+interface LoginTitleProps {
+ isSignUp: boolean;
+}
+
+const LoginTitle = ({ isSignUp }: LoginTitleProps) => {
+ return (
+ !isSignUp && (
+
+ Preview
+
+ )
+ );
+};
+
+export default LoginTitle;
diff --git a/frontend/src/components/login/OAuthContainer.tsx b/frontend/src/pages/Login/view/OAuthContainer.tsx
similarity index 50%
rename from frontend/src/components/login/OAuthContainer.tsx
rename to frontend/src/pages/Login/view/OAuthContainer.tsx
index bada12ea..638ec196 100644
--- a/frontend/src/components/login/OAuthContainer.tsx
+++ b/frontend/src/pages/Login/view/OAuthContainer.tsx
@@ -1,10 +1,29 @@
import { FaGithub, FaRegUserCircle } from "react-icons/fa";
+import useToast from "@hooks/useToast.ts";
+import { useNavigate } from "react-router-dom";
+import useAuth from "@hooks/useAuth.ts";
-interface OAuthContainerProps {
- handleOAuthLogin: (provider: "github" | "guest") => void;
-}
+const OAuthContainer = () => {
+ const toast = useToast();
+ const navigate = useNavigate();
+ const { guestLogIn } = useAuth();
+
+ const handleOAuthLogin = (provider: "github" | "guest") => {
+ if (provider === "github") {
+ // 깃허브 로그인
+ window.location.assign(
+ `https://github.com/login/oauth/authorize?client_id=${import.meta.env.VITE_OAUTH_GITHUB_ID}&redirect_uri=${import.meta.env.VITE_OAUTH_GITHUB_CALLBACK}`
+ );
+ } else if (provider === "guest") {
+ // 게스트 로그인
+ const nickname = guestLogIn();
+ toast.success(
+ "게스트로 로그인되었습니다. 환영합니다. " + nickname + "님!"
+ );
+ navigate("/");
+ }
+ };
-const OAuthContainer = ({ handleOAuthLogin }: OAuthContainerProps) => {
return (
<>
{
- const { isLoggedIn, nickname } = useAuth();
+ const { isLoggedIn } = useAuth();
const toast = useToast();
const navigate = useNavigate();
@@ -16,7 +16,7 @@ const MyPage = () => {
}
}, [isLoggedIn, navigate, toast]);
- return ;
+ return ;
};
export default MyPage;
diff --git a/frontend/src/pages/MyPage/view/Profile.tsx b/frontend/src/pages/MyPage/view/Profile.tsx
index d955e91a..9d4b5788 100644
--- a/frontend/src/pages/MyPage/view/Profile.tsx
+++ b/frontend/src/pages/MyPage/view/Profile.tsx
@@ -1,5 +1,7 @@
import { MdEdit } from "react-icons/md";
import ProfileIcon from "@components/mypage/ProfileIcon";
+import { useUserStore } from "@/stores/useUserStore";
+import { useEffect } from "react";
interface UseModalReturn {
dialogRef: React.RefObject;
@@ -8,16 +10,17 @@ interface UseModalReturn {
closeModal: () => void;
}
-const Profile = ({
- nickname,
- modal,
-}: {
- nickname: string;
- modal: UseModalReturn;
-}) => {
+const Profile = ({ modal }: { modal: UseModalReturn }) => {
+ const user = useUserStore((state) => state.user);
+ const { getMyInfo } = useUserStore();
+
+ useEffect(() => {
+ getMyInfo();
+ }, []);
+
return (
-
+
회원 정보
@@ -25,7 +28,7 @@ const Profile = ({
-
{nickname}
+
{user?.nickname}
);
diff --git a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx
index 7ad8b5c2..b63b24ca 100644
--- a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx
+++ b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx
@@ -1,7 +1,10 @@
import TitleInput from "@components/common/TitleInput";
import { IoMdClose } from "react-icons/io";
import ButtonSection from "@components/mypage/ButtonSection";
-import useAuth from "@hooks/useAuth";
+import { useEffect, useState } from "react";
+import useToast from "@/hooks/useToast";
+import { useUserStore } from "@/stores/useUserStore";
+import PasswordInput from "../../../components/mypage/PasswordInput";
interface UseModalReturn {
dialogRef: React.RefObject;
@@ -10,24 +13,140 @@ interface UseModalReturn {
closeModal: () => void;
}
+interface EditForm {
+ avatarUrl: string;
+ nickname: string;
+ password: {
+ original: string;
+ newPassword: string;
+ };
+}
+
const ProfileEditModal = ({
- modal: { dialogRef, isOpen, closeModal },
+ modal: { dialogRef, closeModal },
}: {
modal: UseModalReturn;
}) => {
- const { nickname } = useAuth();
+ const toast = useToast();
+ const [showOriginalPassword, setShowOriginalPassword] = useState(false);
+ const [showNewPassword, setShowNewPassword] = useState(false);
+ const [originalPassword, setOriginalPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [nickname, setNickname] = useState("");
+ const user = useUserStore((state) => state.user);
+ const { editMyInfo } = useUserStore();
+
+ const [formData, setFormData] = useState({
+ avatarUrl: user?.avatarUrl || "",
+ nickname: user?.nickname || "",
+ password: {
+ original: "",
+ newPassword: "",
+ },
+ });
+
+ useEffect(() => {
+ const handleDialogClose = () => {
+ setShowOriginalPassword(false);
+ setShowNewPassword(false);
+ setOriginalPassword("");
+ setNewPassword("");
+ setNickname(user ? user.nickname : "");
+ };
+
+ const dialogElement = dialogRef.current;
+ dialogElement?.addEventListener("close", handleDialogClose);
+ setNickname(user ? user.nickname : "");
+
+ return () => {
+ dialogElement?.removeEventListener("close", handleDialogClose);
+ };
+ }, [dialogRef, user]);
+
+ const resetModal = () => {
+ if (dialogRef.current) {
+ dialogRef.current.close();
+ }
+ closeModal();
+ };
const handleMouseDown = (e: React.MouseEvent) => {
if (e.target === dialogRef.current) {
- closeModal();
+ resetModal();
}
};
- const closeHandler = () => {
- closeModal();
+ const handleClose = () => {
+ resetModal();
+ };
+
+ const handleChangeNickname = (e: React.ChangeEvent) => {
+ setFormData((prev) => ({
+ ...prev,
+ nickname: e.target.value,
+ }));
+
+ setNickname(e.target.value);
};
- if (!isOpen) return null;
+ const handlePasswordChange = (
+ field: "original" | "newPassword",
+ value: string
+ ) => {
+ setFormData((prev) => ({
+ ...prev,
+ password: {
+ ...prev.password!,
+ [field]: value,
+ },
+ }));
+ };
+
+ const handleSubmit = async () => {
+ if (
+ user?.loginType === "native" &&
+ formData.password.newPassword &&
+ formData.password.original
+ ) {
+ if (
+ formData.nickname.trim().length < 2 ||
+ formData.password.newPassword.trim().length < 7 ||
+ formData.password.original.trim().length < 7
+ ) {
+ toast.error("올바른 값을 입력해주세요.");
+ return;
+ } else if (formData.password.newPassword === formData.password.original) {
+ toast.error("기존 비밀번호와 같은 값을 입력했습니다.");
+ return;
+ } else if (
+ !/[a-z]/.test(formData.password.newPassword) ||
+ !/[0-9]/.test(formData.password.newPassword)
+ ) {
+ toast.error("비밀번호에 최소 하나의 숫자와 소문자를 넣어야합니다.");
+ return;
+ }
+ } else if (
+ user?.loginType === "native" &&
+ ((formData.password.newPassword && !formData.password.original) ||
+ (!formData.password.newPassword && formData.password.original))
+ ) {
+ toast.error("비밀번호 변경을 위해 둘 다 입력해주세요.");
+ return;
+ } else {
+ if (formData.nickname === user?.nickname) {
+ toast.error("변경사항이 없습니다.");
+ return;
+ }
+ }
+
+ try {
+ await editMyInfo(formData);
+ toast.success("회원 정보가 변경되었습니다.");
+ resetModal();
+ } catch (error) {
+ toast.error("회원 정보 변경에 실패하였습니다.");
+ }
+ };
return (
회원 정보 수정
-
+
@@ -50,23 +169,43 @@ const ProfileEditModal = ({
{}}
+ onChange={handleChangeNickname}
minLength={2}
/>
비밀번호 변경
-
+ {user?.loginType === "native" ? (
+ <>
+
) => {
+ handlePasswordChange("original", e.target.value);
+ setOriginalPassword(e.target.value);
+ }}
+ />
+ ) => {
+ handlePasswordChange("newPassword", e.target.value);
+ setNewPassword(e.target.value);
+ }}
+ />
+ >
+ ) : (
+
+ 일반 로그인 외에는 비밀번호를 변경할 수 없습니다.
+
+ )}
);
};
diff --git a/frontend/src/pages/MyPage/view/QuestionSection.tsx b/frontend/src/pages/MyPage/view/QuestionSection.tsx
index 0b3e8938..9080f4f5 100644
--- a/frontend/src/pages/MyPage/view/QuestionSection.tsx
+++ b/frontend/src/pages/MyPage/view/QuestionSection.tsx
@@ -19,12 +19,12 @@ const QuestionSection = () => {
data: myData,
isLoading: isMyListLoading,
error: myListError,
- } = useGetMyQuestionList({ limit: 8 });
+ } = useGetMyQuestionList({ page: myListPage, limit: 8 });
const {
data: scrapData,
isLoading: isScrapListLoading,
error: scrapListError,
- } = useGetScrapQuestionList({ limit: 8 });
+ } = useGetScrapQuestionList({ page: savedListPage, limit: 8 });
const isLoading = tab === "myList" ? isMyListLoading : isScrapListLoading;
const error = tab === "myList" ? myListError : scrapListError;
diff --git a/frontend/src/pages/MyPage/view/index.tsx b/frontend/src/pages/MyPage/view/index.tsx
index 6cf950e5..8950ab9f 100644
--- a/frontend/src/pages/MyPage/view/index.tsx
+++ b/frontend/src/pages/MyPage/view/index.tsx
@@ -5,19 +5,15 @@ import Profile from "@/pages/MyPage/view/Profile";
import QuestionSection from "@/pages/MyPage/view/QuestionSection";
import useModal from "@/hooks/useModal";
-interface MyPageViewProps {
- nickname: string;
-}
-
-const MyPageView = ({ nickname }: MyPageViewProps) => {
+const MyPageView = () => {
const modal = useModal();
return (
-