diff --git a/client/package.json b/client/package.json
index 93fa6a9a..ed5d9722 100644
--- a/client/package.json
+++ b/client/package.json
@@ -35,13 +35,13 @@
"isomorphic-dompurify": "^2.2.0",
"lowlight": "^3.1.0",
"next": "^14.0.4",
- "next-redux-wrapper": "^8.1.0",
"openai": "^4.8.0",
"postcss-nesting": "^12.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-redux": "^8.0.5",
+ "redux-persist": "^6.0.0",
"socketjs-client": "^1.0.2",
"tiptap-extension-resize-image": "^1.0.2",
"typescript": "5.0.2",
diff --git a/client/public/images/check.png b/client/public/images/check.png
new file mode 100644
index 00000000..a70c0a5b
Binary files /dev/null and b/client/public/images/check.png differ
diff --git a/client/src/app/StoreProvider.tsx b/client/src/app/StoreProvider.tsx
index c858b8cd..a2e6ff19 100644
--- a/client/src/app/StoreProvider.tsx
+++ b/client/src/app/StoreProvider.tsx
@@ -2,7 +2,8 @@
import { useRef } from 'react';
import { Provider } from 'react-redux';
-import { makeStore, AppStore, store } from '../lib/redux/store';
+import { makeStore, AppStore, store, persistor } from '../lib/redux/store';
+import { PersistGate } from 'redux-persist/integration/react';
export default function StoreProvider({
children,
@@ -15,5 +16,11 @@ export default function StoreProvider({
storeRef.current = makeStore();
}
- return {children};
+ return (
+
+
+ {children}
+
+
+ );
}
diff --git a/client/src/app/login/components/GithubLoginButton/index.tsx b/client/src/app/login/components/GithubLoginButton/index.tsx
new file mode 100644
index 00000000..6b1b2633
--- /dev/null
+++ b/client/src/app/login/components/GithubLoginButton/index.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import { Icon } from '@iconify/react';
+
+//hooks
+import useModal from '@/hooks/modal/useModal';
+
+//constant
+import { errorMessage, ModalType } from '@/constants/constant';
+
+export default function GithubLoginButton() {
+ const { openModal } = useModal();
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/app/login/components/GoogleLoginButton/index.tsx b/client/src/app/login/components/GoogleLoginButton/index.tsx
new file mode 100644
index 00000000..568438d5
--- /dev/null
+++ b/client/src/app/login/components/GoogleLoginButton/index.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import { Icon } from '@iconify/react';
+
+//hooks
+import useModal from '@/hooks/modal/useModal';
+
+//constant
+import { errorMessage, ModalType } from '@/constants/constant';
+
+export default function GoogleLoginButton() {
+ const { openModal } = useModal();
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/app/login/components/LoginForm/index.tsx b/client/src/app/login/components/LoginForm/index.tsx
new file mode 100644
index 00000000..24ea0c5a
--- /dev/null
+++ b/client/src/app/login/components/LoginForm/index.tsx
@@ -0,0 +1,188 @@
+'use client';
+
+import { useState, useCallback } from 'react';
+import { useRouter } from 'next/navigation';
+
+//redux
+import { useDispatch, useSelector } from 'react-redux';
+import { logIn } from '@/lib/redux/features/auth/authSlice';
+
+//components
+import { Tooltip } from '@/components';
+
+//hooks
+import useModal from '@/hooks/modal/useModal';
+
+//icons
+import { EyeOnIcon, EyeOffIcon } from '@/assets/Icon';
+
+//formik
+import { Formik, Form, Field, ErrorMessage } from 'formik';
+import * as Yup from 'yup';
+
+//services
+import { userManager } from '@/service/user';
+
+//constant
+import {
+ initialLoginValue,
+ errorMessage,
+ errorCodeToMessage,
+ ModalType,
+} from '@/constants/constant';
+
+//types
+import { IReduxState } from '@/types/redux/IReduxState';
+
+const ValidationSchema = Yup.object().shape({
+ userId: Yup.string().required(errorMessage.blankID),
+ password: Yup.string().required(errorMessage.blankPassword),
+});
+
+interface ILogin {
+ userId: string;
+ password: string;
+}
+
+export default function LoginForm() {
+ const [viewPassword, setViewPassword] = useState(false);
+
+ const dispatch = useDispatch();
+
+ const router = useRouter();
+
+ const modalState = useSelector((state: IReduxState) => state.modal);
+ const { openModal } = useModal();
+
+ const handleSubmit = useCallback(
+ async (sendData: ILogin, setSubmitting: (value: boolean) => void) => {
+ if (modalState.type === ModalType.CLOSE) {
+ setSubmitting(true);
+ try {
+ const userData = await userManager.loginUser({ data: sendData });
+ dispatch(logIn(userData));
+ router.replace(window.sessionStorage.getItem('previousURL') || '/');
+ } catch (error) {
+ if (error instanceof Error) {
+ type Code = 'USER_NOT_EXIST' | 'INVALID_PASSWORD';
+ openModal({
+ type: ModalType.ERROR,
+ message:
+ errorCodeToMessage[error.message as Code] || errorMessage.error,
+ });
+ } else {
+ openModal({
+ type: ModalType.ERROR,
+ message: errorMessage.error,
+ });
+ }
+ } finally {
+ setSubmitting(false);
+ }
+ }
+ },
+ [modalState]
+ );
+ return (
+ handleSubmit(data, setSubmitting)}
+ >
+ {({ errors, touched, isSubmitting }) => (
+
+ )}
+
+ );
+}
diff --git a/client/src/app/login/layout.tsx b/client/src/app/login/layout.tsx
deleted file mode 100644
index ca088b9d..00000000
--- a/client/src/app/login/layout.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import { Layout } from '@/components';
-
-export default function LayoutComponent({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return {children};
-}
diff --git a/client/src/app/login/page.tsx b/client/src/app/login/page.tsx
index fcc14c75..44d3199a 100644
--- a/client/src/app/login/page.tsx
+++ b/client/src/app/login/page.tsx
@@ -1,183 +1,37 @@
-'use client';
-
-import { useCallback } from 'react';
import Link from 'next/link';
-import { useRouter } from 'next/navigation';
-import { useDispatch, useSelector } from 'react-redux';
-import { logIn } from '@/lib/redux/features/auth/authSlice';
-
-// packages
-import { Icon } from '@iconify/react';
-import { Formik, Form, Field, ErrorMessage } from 'formik';
-import * as Yup from 'yup';
// components
-import { Divider } from '@/components';
-
-// service
-import { userManager } from '@/service/user';
-
-// hooks
-import useModal from '@/hooks/modal/useModal';
-
-// constant
-import {
- initialLoginValue,
- errorMessage,
- errorCodeToMessage,
- ModalType,
-} from '@/constants/constant';
-import { IReduxState } from '@/types/redux/IReduxState';
-
-const ValidationSchema = Yup.object().shape({
- userId: Yup.string().required(errorMessage.blankID),
- password: Yup.string().required(errorMessage.blankPassword),
-});
-
-interface ILogin {
- userId: string;
- password: string;
-}
+import Logo from '@/components/Navigator/Logo';
+import LoginForm from './components/LoginForm';
+import GoogleLoginButton from './components/GoogleLoginButton';
+import GithubLoginButton from './components/GithubLoginButton';
export default function Login() {
- const router = useRouter();
- const dispatch = useDispatch();
-
- // Modal
- const modalState = useSelector((state: IReduxState) => state.modal);
- const { openModal } = useModal();
-
- const handleSubmit = useCallback(
- async (sendData: ILogin, setSubmitting: (value: boolean) => void) => {
- if (modalState.type === ModalType.CLOSE) {
- setSubmitting(true);
- try {
- const userData = await userManager.loginUser({ data: sendData });
- dispatch(logIn(userData));
- router.push('/');
- } catch (error) {
- if (error instanceof Error) {
- type Code = 'USER_NOT_EXIST' | 'INVALID_PASSWORD';
- openModal({
- type: ModalType.ERROR,
- message:
- errorCodeToMessage[error.message as Code] || errorMessage.error,
- });
- } else {
- openModal({
- type: ModalType.ERROR,
- message: errorMessage.error,
- });
- }
- } finally {
- setSubmitting(false);
- }
- }
- },
- [modalState]
- );
return (
-
-
로그인
-
- handleSubmit(data, setSubmitting)
- }
- >
- {({ errors, touched, isSubmitting }) => (
-
- )}
-
+
+
+
+
+
+
+ 로그인
+
+
+
+
+
+
+
+ {"아직 '리덕' 회원이 아니신가요?"}
+
+
+ 회원가입 하기
+
+
+
);
}
diff --git a/client/src/assets/Icon/eye_off.tsx b/client/src/assets/Icon/eye_off.tsx
new file mode 100644
index 00000000..15cdf691
--- /dev/null
+++ b/client/src/assets/Icon/eye_off.tsx
@@ -0,0 +1,45 @@
+import { SVGProps } from 'react';
+
+export default function EyeOffIcon(props: SVGProps
) {
+ return (
+
+ );
+}
diff --git a/client/src/assets/Icon/eye_on.tsx b/client/src/assets/Icon/eye_on.tsx
new file mode 100644
index 00000000..5bb59370
--- /dev/null
+++ b/client/src/assets/Icon/eye_on.tsx
@@ -0,0 +1,29 @@
+import { SVGProps } from 'react';
+
+export default function EyeOnIcon(props: SVGProps) {
+ return (
+
+ );
+}
diff --git a/client/src/assets/Icon/index.ts b/client/src/assets/Icon/index.ts
index 7425cd5d..01084dec 100644
--- a/client/src/assets/Icon/index.ts
+++ b/client/src/assets/Icon/index.ts
@@ -17,3 +17,5 @@ export { default as LinkIcon } from './link';
export { default as ImageIcon } from './image';
export { default as ListIcon } from './list';
export { default as NumberListIcon } from './number-list';
+export { default as EyeOnIcon } from './eye_on';
+export { default as EyeOffIcon } from './eye_off';
diff --git a/client/src/components/Dropdown/Content/index.tsx b/client/src/components/Dropdown/Content/index.tsx
new file mode 100644
index 00000000..20498272
--- /dev/null
+++ b/client/src/components/Dropdown/Content/index.tsx
@@ -0,0 +1,14 @@
+import { HTMLAttributes } from 'react';
+
+const DropdownContent = (props: HTMLAttributes) => {
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default DropdownContent;
diff --git a/client/src/components/Dropdown/Wrapper/index.tsx b/client/src/components/Dropdown/Wrapper/index.tsx
new file mode 100644
index 00000000..4919a15f
--- /dev/null
+++ b/client/src/components/Dropdown/Wrapper/index.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import { PropsWithChildren } from 'react';
+
+// hooks
+import useClickAwayRef from '@/hooks/common/useClickAwayRef';
+
+interface IProps extends PropsWithChildren {
+ setDropdownOpen: (value: boolean) => void;
+ position?: string;
+}
+
+const DropdownWrapper = ({ setDropdownOpen, children, position }: IProps) => {
+ const ref = useClickAwayRef((e) => {
+ if (!ref.current || !ref.current.parentNode) {
+ return;
+ }
+ if (!ref.current.parentNode.contains(e.target as HTMLElement)) {
+ setDropdownOpen(false);
+ }
+ });
+
+ const DROPDOWN_POSITION = position || 'right-0 -bottom-24';
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default DropdownWrapper;
diff --git a/client/src/components/Dropdown/index.tsx b/client/src/components/Dropdown/index.tsx
new file mode 100644
index 00000000..3fa017ab
--- /dev/null
+++ b/client/src/components/Dropdown/index.tsx
@@ -0,0 +1,9 @@
+import DropdownWrapper from './Wrapper';
+import DropdownContent from './Content';
+
+const Dropdown = {
+ Wrapper: DropdownWrapper,
+ Content: DropdownContent,
+};
+
+export default Dropdown;
diff --git a/client/src/components/Navigator/Logo.tsx b/client/src/components/Navigator/Logo.tsx
new file mode 100644
index 00000000..9d6ccde4
--- /dev/null
+++ b/client/src/components/Navigator/Logo.tsx
@@ -0,0 +1,18 @@
+import Link from 'next/link';
+import Image from 'next/image';
+
+export default function Logo() {
+ return (
+
+
+
+ reDuck
+
+
+ );
+}
diff --git a/client/src/components/Navigator/NavigatorBottom.tsx b/client/src/components/Navigator/NavigatorBottom.tsx
new file mode 100644
index 00000000..6caf3ed5
--- /dev/null
+++ b/client/src/components/Navigator/NavigatorBottom.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+//components
+import Button from '../base/Button';
+import PostModeNavigation from './PostModeNavigation';
+
+//hooks
+import useClientLoginCheck from '@/hooks/common/useClientLoginCheck';
+
+//icons
+import { ArrowDownIcon } from '@/assets/Icon';
+
+//types
+import { PostViewType } from '@/types';
+
+interface IProps {
+ viewMode?: PostViewType;
+ initialUserLogin: boolean;
+}
+
+export default function NavigatorBottom({
+ viewMode,
+ initialUserLogin,
+}: IProps) {
+ const userLogin = useClientLoginCheck(initialUserLogin);
+
+ return (
+
+
+
+ {userLogin && (
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/Navigator/NavigatorTop.tsx b/client/src/components/Navigator/NavigatorTop.tsx
new file mode 100644
index 00000000..bd7dc5bb
--- /dev/null
+++ b/client/src/components/Navigator/NavigatorTop.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+
+//components
+import Logo from './Logo';
+import SearchButton from './SearchButton';
+import AlarmButton from './AlarmButton';
+import ChatButton from './ChatButton';
+import ProfileButton from './ProfileButton';
+import Button from '../base/Button';
+
+//hooks
+import useClientLoginCheck from '@/hooks/common/useClientLoginCheck';
+
+interface IProps {
+ initialUserLogin: boolean;
+}
+
+export default function NavigatorTop({ initialUserLogin }: IProps) {
+ const userLogin = useClientLoginCheck(initialUserLogin);
+
+ const router = useRouter();
+
+ const handleLinkToLogin = () => {
+ window.sessionStorage.setItem('previousURL', window.location.href);
+ router.push('login');
+ };
+
+ return (
+
+
+
+
+ -
+
+
+ {userLogin && (
+ <>
+ -
+
+
+ -
+
+
+ >
+ )}
+
+ {userLogin ? (
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/Navigator/ProfileButton.tsx b/client/src/components/Navigator/ProfileButton.tsx
index d0a4298f..eccbf0a9 100644
--- a/client/src/components/Navigator/ProfileButton.tsx
+++ b/client/src/components/Navigator/ProfileButton.tsx
@@ -1,9 +1,81 @@
+'use client';
+
+import { useState } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+
+// redux
+import { useSelector, useDispatch } from 'react-redux';
+import { selectUser } from '@/lib/redux/features/auth/authSlice';
+import { logOut } from '@/lib/redux/features/auth/authSlice';
+
+// components
+import { Avatar, Dropdown } from '../';
+
+// hooks
+import useModal from '@/hooks/modal/useModal';
+
+// icons
import { ProfileIcon } from '@/assets/Icon';
+// services
+import { BASE_URL } from '@/service/base/api';
+import { userManager } from '@/service/user';
+
+// types
+import { ModalType, errorMessage } from '@/constants/constant';
+
export default function ProfileButton() {
+ const user = useSelector(selectUser);
+ const dispatch = useDispatch();
+
+ const router = useRouter();
+
+ const { openModal } = useModal();
+
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+
+ const handleLogout = async () => {
+ try {
+ await userManager.logoutUser();
+ dispatch(logOut());
+ router.replace('/');
+ } catch {
+ openModal({
+ type: ModalType.ERROR,
+ message: errorMessage.tryAgain,
+ });
+ }
+ };
+
return (
-