diff --git a/package.json b/package.json index 4af63ef..74e51d3 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,15 @@ "dependencies": { "@emotion/react": "^11.11.1", "@types/textarea-caret": "^3.0.3", + "axios": "^1.6.2", "framer-motion": "^10.16.4", "hangul-js": "^0.2.6", "immer": "^10.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", - "textarea-caret": "^3.1.0" + "textarea-caret": "^3.1.0", + "zustand": "^4.4.6" }, "devDependencies": { "@commitlint/cli": "^17.6.7", diff --git a/src/App.tsx b/src/App.tsx index cddf5ba..b8eb7c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,9 @@ import Layout from './layout'; function App() { return ( - Loading...

}> - {useRoutes(routes)} -
+ + Loading...

}>{useRoutes(routes)}
+
); } diff --git a/src/apis/instance.ts b/src/apis/instance.ts new file mode 100644 index 0000000..05af672 --- /dev/null +++ b/src/apis/instance.ts @@ -0,0 +1,3 @@ +import axios from 'axios'; + +export default axios.create({}); diff --git a/src/auth/hooks.ts b/src/auth/hooks.ts new file mode 100644 index 0000000..9efa0cc --- /dev/null +++ b/src/auth/hooks.ts @@ -0,0 +1,46 @@ +import { useContext, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { AuthContext } from '.'; +import { UserSetPropType, useUserState } from './stores'; + +export const useAuth = () => { + const { removeUser, setUser } = useUserState(); + const { token, setToken } = useContext(AuthContext); + + const setLogin = (user: UserSetPropType & { token: string }) => { + setUser(user); + setToken(user.token); + }; + + const setLogout = () => { + removeUser(); + setToken(undefined); + }; + + const updateToken = (token: string) => setToken(token); + + return { token, setLogin, setLogout, updateToken }; +}; + +export const useCurrentUser = ({ + redirectTo = '', + redirectIfFound = false, +} = {}) => { + const { user } = useUserState(); + const navigate = useNavigate(); + + useEffect(() => { + if (!redirectTo || !user) return; + + if ( + // If redirectTo is set, redirect if the user was not found. + (redirectTo && !redirectIfFound && !user?.isLoggedIn) || + // If redirectIfFound is also set, redirect if the user was found + (redirectIfFound && user?.isLoggedIn) + ) { + navigate(redirectTo); + } + }, [user, redirectIfFound, redirectTo]); + + return { user }; +}; diff --git a/src/auth/index.tsx b/src/auth/index.tsx new file mode 100644 index 0000000..de66821 --- /dev/null +++ b/src/auth/index.tsx @@ -0,0 +1,62 @@ +import { ReactNode, createContext, useEffect, useMemo, useState } from 'react'; +import instance from '../apis/instance'; +import { useUserState } from './stores'; + +export const AuthContext = createContext<{ + token: string | undefined; + setToken: (token: string | undefined) => void; +}>({ + token: undefined, + setToken: () => { + throw new Error('setToken function must be overridden'); + }, +}); + +export interface AuthProviderProps { + children: ReactNode; +} + +const mockAuthUser = (token: string) => ({ + name: 'hi', + email: '', + profileImage: '', +}); + +function AuthProvider({ children, ...props }: AuthProviderProps) { + const [token, setToken_] = useState( + localStorage.getItem('token') || undefined, + ); + + const { removeUser, setUser } = useUserState(); + + const setToken = (token: string | undefined) => { + setToken_(token); + }; + + useEffect(() => { + if (token) { + instance.defaults.headers.common['Authorization'] = `Bearer ${token}`; + localStorage.setItem('token', token); + } else { + delete instance.defaults.headers.common['Authorization']; + localStorage.removeItem('token'); + } + }, [token]); + + useEffect(() => { + if (!token) { + removeUser(); + return; + } + const userData = mockAuthUser(token); + if (userData) setUser(userData); + }, []); + + const contextValue = useMemo(() => ({ token, setToken }), [token]); + + return ( + {children} + ); +} + +export default AuthProvider; diff --git a/src/auth/stores.ts b/src/auth/stores.ts new file mode 100644 index 0000000..f33992a --- /dev/null +++ b/src/auth/stores.ts @@ -0,0 +1,38 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +export interface UserType { + name: string | null; + email: string | null; + profileImage: string | null; + isLoggedIn: boolean; +} + +export type UserSetPropType = Omit; + +interface UserState { + user: UserType; + setUser: (user: UserSetPropType) => void; + removeUser: () => void; +} + +export const useUserState = create()( + devtools((set) => ({ + user: { + isLoggedIn: false, + name: null, + email: null, + profileImage: null, + }, + setUser: (user) => set(() => ({ user: { ...user, isLoggedIn: true } })), + removeUser: () => + set(() => ({ + user: { + isLoggedIn: false, + name: null, + email: null, + profileImage: null, + }, + })), + })), +); diff --git a/src/components/TopNav/TopNav.tsx b/src/components/TopNav/TopNav.tsx index 66d799d..305a443 100644 --- a/src/components/TopNav/TopNav.tsx +++ b/src/components/TopNav/TopNav.tsx @@ -1,3 +1,4 @@ +import { UserType } from '@/auth/stores'; import DesignSystem from '@/utils/designSystem'; import globalStyles from '@/utils/styles'; import { Group, Stack, Stroke, Typography } from '@base'; @@ -35,13 +36,9 @@ export type NavBarMenuItemType = { label: string; path: string; }; -export interface UserType { - img?: string; - name: string; - email: string; -} + interface TopNavProps { - user: UserType | null; + user: UserType; navBarMenu: NavBarMenuItemType[]; onLoginClick?: () => void; onLogoutClick: () => void; @@ -66,7 +63,7 @@ function TopNav({ {navBarMenu} - {user ? ( + {user.isLoggedIn ? ( diff --git a/src/components/TopNav/TopNavUser.tsx b/src/components/TopNav/TopNavUser.tsx index d9a57ec..9d78316 100644 --- a/src/components/TopNav/TopNavUser.tsx +++ b/src/components/TopNav/TopNavUser.tsx @@ -1,12 +1,12 @@ -import DefaultProfile from '@/assets/default-profile.svg'; import IconDropdown from '@/assets/icon-dropdown.svg'; import IconSettings from '@/assets/icon-settings.svg'; +import { UserType } from '@/auth/stores'; import DesignSystem from '@/utils/designSystem'; import globalStyles from '@/utils/styles'; import { Group, Popover, Stack, Typography } from '@base'; +import UserProfileImg from '@copmonents/UserProfileImg'; import { css } from '@emotion/react'; import { ReactNode } from 'react'; -import { UserType } from './TopNav'; const styles = { trigger: globalStyles.button, @@ -32,7 +32,7 @@ function TopNavUser({ user, children }: TopNavUserProps) {
- +
@@ -42,7 +42,7 @@ function TopNavUser({ user, children }: TopNavUserProps) { - + {user.name} diff --git a/src/components/TopNav/index.stories.tsx b/src/components/TopNav/index.stories.tsx index a330344..8c7a4bb 100644 --- a/src/components/TopNav/index.stories.tsx +++ b/src/components/TopNav/index.stories.tsx @@ -10,6 +10,8 @@ const meta: Meta = { user: { email: 'test@gmail.com', name: '테스트 유저', + isLoggedIn: true, + profileImage: '', }, navBarMenu: [ { label: 'MY RECIPE', path: '/mypage' }, diff --git a/src/components/UserProfileImg.tsx b/src/components/UserProfileImg.tsx new file mode 100644 index 0000000..b8954c4 --- /dev/null +++ b/src/components/UserProfileImg.tsx @@ -0,0 +1,17 @@ +import DefaultProfile from '@/assets/default-profile.svg'; +import { useCurrentUser } from '@/auth/hooks'; +import { ImgHTMLAttributes } from 'react'; + +export interface UserProfileProps extends ImgHTMLAttributes { + width?: number; +} + +function UserProfileImg({ width, ...props }: UserProfileProps) { + const { user } = useCurrentUser(); + + return ( + + ); +} + +export default UserProfileImg; diff --git a/src/layout/index.tsx b/src/layout/index.tsx index 99736ac..0481210 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -1,6 +1,7 @@ -import { useUserData } from '@/utils/hooks'; +import { useAuth } from '@/auth/hooks'; +import { useUserState } from '@/auth/stores'; import globalStyles from '@/utils/styles'; -import TopNav, { UserType } from '@copmonents/TopNav/TopNav'; +import TopNav from '@copmonents/TopNav/TopNav'; import { css } from '@emotion/react'; import { ReactNode } from 'react'; import { useLocation } from 'react-router-dom'; @@ -24,7 +25,8 @@ export interface LayoutProps { const EXCEPT_PATH = ['/mypage/initial', '/post']; function Layout({ children, ...props }: LayoutProps) { - const [user, setUser] = useUserData(); + const { user } = useUserState(); + const { setLogin, setLogout } = useAuth(); const locaton = useLocation(); const isExceptPath = EXCEPT_PATH.includes(locaton.pathname); @@ -39,8 +41,15 @@ function Layout({ children, ...props }: LayoutProps) { { label: 'INVENTORY', path: '/inventory' }, { label: 'SEARCH', path: '/search' }, ]} - onLoginClick={() => setUser({ email: 'test@t.com', name: 'testUser' })} - onLogoutClick={() => setUser(null)} + onLoginClick={() => + setLogin({ + name: 'hi', + email: '', + profileImage: '', + token: 'temp token', + }) + } + onLogoutClick={setLogout} />
{children} diff --git a/src/main.tsx b/src/main.tsx index bfe2bef..a884e2f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,12 +2,15 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter as Router } from 'react-router-dom'; import App from './App'; +import AuthProvider from './auth'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + , ); diff --git a/src/pages/mypage/index.tsx b/src/pages/mypage/index.tsx index 6bb9a57..96fc6c0 100644 --- a/src/pages/mypage/index.tsx +++ b/src/pages/mypage/index.tsx @@ -1,6 +1,5 @@ import EmptyCheckbox from '@/assets/checkbox-empty.svg'; import FillCheckbox from '@/assets/checkbox-fill.svg'; -import DefaultProfile from '@/assets/default-profile.svg'; import EditIcon from '@/assets/icon-edit-box.svg'; import BackgroundImg from '@/assets/newmypage-background.png'; import DesignSystem from '@/utils/designSystem'; @@ -9,6 +8,7 @@ import { Group, Stack, Typography } from '@base'; import Button from '@copmonents/Button'; import Modal from '@copmonents/Modal'; import ToggleButton from '@copmonents/Toggle/ToggleButton'; +import UserProfileImg from '@copmonents/UserProfileImg'; import { css } from '@emotion/react'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -180,7 +180,7 @@ function MyPage({ ...props }) { - + 해피밀 diff --git a/src/pages/post/index.tsx b/src/pages/post/index.tsx index 92aa792..39a34fb 100644 --- a/src/pages/post/index.tsx +++ b/src/pages/post/index.tsx @@ -1,9 +1,10 @@ -import DefaultProfile from '@/assets/default-profile.svg'; import { ReactComponent as IconDropdown } from '@/assets/icon-dropdown.svg'; +import { useCurrentUser } from '@/auth/hooks'; import DesignSystem from '@/utils/designSystem'; import globalStyles from '@/utils/styles'; import { Group, Stack, Typography } from '@base'; import ToggleButton from '@copmonents/Toggle/ToggleButton'; +import UserProfileImg from '@copmonents/UserProfileImg'; import { css } from '@emotion/react'; import Editor from './components/Editor'; @@ -38,6 +39,7 @@ const styles = { export interface PostPageProps {} function PostPage({ ...props }: PostPageProps) { + useCurrentUser({ redirectTo: '/' }); return ( <> @@ -55,7 +57,7 @@ function PostPage({ ...props }: PostPageProps) {
- + 해피밀 data} /> diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 8c894e9..777487e 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -128,7 +128,7 @@ export function useClickOutside( return ref; } - +/* export function useUserData() { const localUserData = localStorage.getItem('user-data'); @@ -147,7 +147,7 @@ export function useUserData() { }, [userDataState]); return [userDataState, setUserData] as const; -} +}*/ export function useComposing() { const [isComposing, setIsComposing] = useState(false); diff --git a/yarn.lock b/yarn.lock index e8acc6f..cf34862 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4255,6 +4255,15 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" + integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" @@ -5770,6 +5779,11 @@ flow-parser@0.*: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.216.1.tgz#eeba9b0b689deeccc34a6b7d2b1f97b8f943afc0" integrity sha512-wstw46/C/8bRv/8RySCl15lK376j8DHxm41xFjD9eVL+jSS1UmVpbdLdA0LzGuS2v5uGgQiBLEj6mgSJQwW+MA== +follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -5802,6 +5816,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -7840,7 +7863,7 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -proxy-from-env@^1.0.0: +proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -9138,6 +9161,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -9468,3 +9496,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.4.6: + version "4.4.6" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.6.tgz#03c78e3e2686c47095c93714c0c600b72a6512bd" + integrity sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg== + dependencies: + use-sync-external-store "1.2.0"