diff --git a/.env.sample b/.env.sample index bc497a6..44f4a94 100644 --- a/.env.sample +++ b/.env.sample @@ -3,4 +3,6 @@ API_PORT=3000 API_VERSION=v1 API_URL=http://$API_HOSTNAME:$API_PORT/api/$API_VERSION -NEXT_PUBLIC_API_URL=$API_URL \ No newline at end of file +NEXT_PUBLIC_API_URL=$API_URL + +NEXT_PUBLIC_GOOGLE_CLIENT_ID= diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 4b0d572..5fcc0a6 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -32,13 +32,14 @@ jobs: echo "API_VERSION=$API_VERSION" >> .env echo "API_URL=$API_URL" >> .env echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL" >> .env + echo "NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID" >> .env env: API_HOSTNAME: ${{ secrets.API_HOSTNAME }} API_PORT: ${{ secrets.API_PORT }} API_VERSION: ${{ secrets.API_VERSION }} API_URL: ${{ secrets.API_URL }} NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} - + NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_GOOGLE_CLIENT_ID }} - name: Publish to Chromatic uses: chromaui/action@v1 diff --git a/.storybook/preview.js b/.storybook/preview.js index 09abc66..1978246 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { initialize, mswDecorator } from 'msw-storybook-addon'; import { RouterContext } from 'next/dist/shared/lib/router-context'; import * as NextImage from 'next/image'; +import { RecoilRoot } from 'recoil'; import '@src/styles/reset.css'; @@ -16,9 +17,11 @@ initAxiosConfig(); export const decorators = [ mswDecorator, (Story) => ( - - - + + + + + ), ]; diff --git a/__mocks__/apis/handlers/auth.ts b/__mocks__/apis/handlers/auth.ts new file mode 100644 index 0000000..b1d778a --- /dev/null +++ b/__mocks__/apis/handlers/auth.ts @@ -0,0 +1,30 @@ +import { ResponseComposition, RestContext, RestRequest, rest } from 'msw'; + +import { PostSigninResponse } from '@src/apis/auth'; +import { BASE_URL } from '@src/configs/axios'; + +const NEW_MEMBER = false; +const MEMBER = true; + +const AUTH_DATA = { + isMember: MEMBER, + jwtTokens: { + accessToken: 'access-token', + refreshToken: 'refresh-token', + }, +}; + +const signin = (req: RestRequest, res: ResponseComposition, ctx: RestContext) => { + return res( + ctx.status(201), + ctx.json({ + code: 'SUCCESS', + message: '성공', + data: AUTH_DATA, + }), + ); +}; + +export const authHandler = [ + rest.post(`${BASE_URL}/auth/signin`, signin), // +]; diff --git a/__mocks__/apis/handlers/index.ts b/__mocks__/apis/handlers/index.ts index 4ac5f38..99f7f2a 100644 --- a/__mocks__/apis/handlers/index.ts +++ b/__mocks__/apis/handlers/index.ts @@ -1,7 +1,9 @@ +import { authHandler } from './auth'; import { commentHandler } from './comment'; import { topicDetailHandler } from './topic'; export const handlers = [ - ...topicDetailHandler, // + ...authHandler, // + ...topicDetailHandler, ...commentHandler, ]; diff --git a/src/apis/auth.ts b/src/apis/auth.ts new file mode 100644 index 0000000..2c0ce2f --- /dev/null +++ b/src/apis/auth.ts @@ -0,0 +1,25 @@ +import axios from 'axios'; + +import { BaseResponse } from '.'; + +export interface Auth { + isMember: boolean; + jwtTokens: { + accessToken: string; + refreshToken: string; + }; +} + +/** + * 로그인 + */ +export type PostSigninResponse = BaseResponse; +export const signin = async (authCode: string) => { + const res = await axios.post(`/auth/signin`, null, { + headers: { + 'auth-code': authCode, + }, + }); + + return res.data.data; +}; diff --git a/src/apis/index.ts b/src/apis/index.ts index 8c7bbb5..8c834ac 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,5 +1,6 @@ export * from './topic'; export * from './comment'; +export * from './auth'; export interface BaseResponse { message: string; diff --git a/src/assets/icon/google_logo.svg b/src/assets/icon/google_logo.svg new file mode 100644 index 0000000..bd004e0 --- /dev/null +++ b/src/assets/icon/google_logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icon/index.tsx b/src/assets/icon/index.tsx index f3c46c1..840ccce 100644 --- a/src/assets/icon/index.tsx +++ b/src/assets/icon/index.tsx @@ -12,6 +12,7 @@ export { ReactComponent as Paint } from './emoji_paint.svg'; export { ReactComponent as Paper } from './emoji_paper.svg'; export { ReactComponent as Smile } from './emoji_smile.svg'; export { ReactComponent as ThumbsUp } from './emoji_thumbs_up.svg'; +export { ReactComponent as GoogleLogo } from './google_logo.svg'; export { ReactComponent as Image } from './image.svg'; export { ReactComponent as Logo } from './logo.svg'; export { ReactComponent as More } from './more.svg'; diff --git a/src/components/common/Header/Header.stories.tsx b/src/components/common/Header/Header.stories.tsx index bcc91b2..716ef49 100644 --- a/src/components/common/Header/Header.stories.tsx +++ b/src/components/common/Header/Header.stories.tsx @@ -1,4 +1,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { RecoilRoot } from 'recoil'; + +import $userSession from '@src/recoil/userSession'; import Header from '.'; @@ -12,3 +15,22 @@ const Template: ComponentStory = (args) =>
; export const 기본 = Template.bind({}); 기본.args = {}; + +export const 로그인 = Template.bind({}); +로그인.decorators = [ + (Story) => ( + { + set($userSession, { + isMember: true, + jwtTokens: { + accessToken: 'access-token', + refreshToken: 'refresh-token', + }, + }); + }} + > + + + ), +]; diff --git a/src/components/common/Header/Header.style.tsx b/src/components/common/Header/Header.style.tsx index e14ec59..8bc1d7a 100644 --- a/src/components/common/Header/Header.style.tsx +++ b/src/components/common/Header/Header.style.tsx @@ -11,12 +11,12 @@ export const HeaderWrapper = styled.header` z-index: 1; height: 5rem; - color: ${theme.color.Primary1}; background-color: ${theme.color.G1}; border-bottom: 1px solid ${theme.color.Primary1}; `; export const HeaderContents = styled.div` + position: relative; margin: 0 auto; max-width: 1200px; display: flex; @@ -43,4 +43,17 @@ export const Menu = styled.li` font-weight: 700; font-size: 15px; line-height: 150%; + + text-decoration: none; + color: ${theme.color.Primary1}; +`; + +export const UserInfoWrapper = styled.div` + cursor: pointer; +`; + +export const UserMenuWrapper = styled.div` + position: absolute; + top: 70px; + right: 0; `; diff --git a/src/components/common/Header/Header.tsx b/src/components/common/Header/Header.tsx index 2bc8248..21d5c99 100644 --- a/src/components/common/Header/Header.tsx +++ b/src/components/common/Header/Header.tsx @@ -1,11 +1,27 @@ import Link from 'next/link'; -import { FC } from 'react'; +import { FC, useState } from 'react'; +import { useRecoilValue, useResetRecoilState } from 'recoil'; import Icon from '@src/components/common/Icon'; +import UserInfo from '@src/components/common/UserInfo'; +import $userSession from '@src/recoil/userSession'; import * as S from './Header.style'; +import UserMenu from './UserMenu'; const Header: FC = () => { + const userSession = useRecoilValue($userSession); + const resetUser = useResetRecoilState($userSession); + const [viewUserMenu, setViewUserMenu] = useState(false); + + const isLogin = !!userSession; + + const handleLogout = () => { + resetUser(); + setViewUserMenu(false); + }; + + // TODO-GYU: backend 와 논의 후 user 정보를 어떻게 받아올지 처리 return ( @@ -14,10 +30,23 @@ const Header: FC = () => { - + - 로그인 + {isLogin ? ( + setViewUserMenu((prev) => !prev)}> + + + ) : ( + + 로그인 + + )} + {viewUserMenu && ( + + + + )} ); diff --git a/src/components/common/Header/UserMenu/UserMenu.stories.tsx b/src/components/common/Header/UserMenu/UserMenu.stories.tsx new file mode 100644 index 0000000..fe1396a --- /dev/null +++ b/src/components/common/Header/UserMenu/UserMenu.stories.tsx @@ -0,0 +1,14 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; + +import UserMenu from './UserMenu'; + +export default { + title: 'common/UserMenu', + component: UserMenu, + args: {}, +} as ComponentMeta; + +const Template: ComponentStory = ({ ...args }) => ; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/src/components/common/Header/UserMenu/UserMenu.styles.tsx b/src/components/common/Header/UserMenu/UserMenu.styles.tsx new file mode 100644 index 0000000..c30bdf6 --- /dev/null +++ b/src/components/common/Header/UserMenu/UserMenu.styles.tsx @@ -0,0 +1,45 @@ +import styled from '@emotion/styled'; + +import theme from '@src/styles/theme'; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + width: 227px; + height: 182px; + box-shadow: 0px 4px 30px 5px rgba(0, 0, 0, 0.15); + border-radius: 8px; +`; + +export const UserItem = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 16px; + height: 76px; + + border-radius: 8px 8px 0 0; + background: ${theme.color.G4}; + &:hover { + background: ${theme.color.G5}; + cursor: pointer; + } +`; + +export const MenuItem = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 16px; + height: 53px; + + background: ${theme.color.G4}; + &:hover { + background: ${theme.color.G5}; + cursor: pointer; + } + &:last-child { + border-radius: 0 0 8px 8px; + } +`; diff --git a/src/components/common/Header/UserMenu/UserMenu.tsx b/src/components/common/Header/UserMenu/UserMenu.tsx new file mode 100644 index 0000000..81c06b5 --- /dev/null +++ b/src/components/common/Header/UserMenu/UserMenu.tsx @@ -0,0 +1,26 @@ +import React, { FC } from 'react'; + +import Icon from '@src/components/common/Icon'; +import UserInfo from '@src/components/common/UserInfo'; + +import * as S from './UserMenu.styles'; + +interface Props { + onLogout: () => void; +} +const UserMenu: FC = (props) => { + const { onLogout } = props; + + return ( + + + + + + 문의하기 + 로그아웃 + + ); +}; + +export default UserMenu; diff --git a/src/components/common/Header/UserMenu/index.tsx b/src/components/common/Header/UserMenu/index.tsx new file mode 100644 index 0000000..63aab39 --- /dev/null +++ b/src/components/common/Header/UserMenu/index.tsx @@ -0,0 +1 @@ +export { default } from './UserMenu'; diff --git a/src/components/common/SingleContentLayout/SingleContentLayout.stories.tsx b/src/components/common/SingleContentLayout/SingleContentLayout.stories.tsx new file mode 100644 index 0000000..f8f2c4c --- /dev/null +++ b/src/components/common/SingleContentLayout/SingleContentLayout.stories.tsx @@ -0,0 +1,16 @@ +import { ComponentStory } from '@storybook/react'; +import React from 'react'; + +import SingleContentLayout from '.'; + +export default { + component: SingleContentLayout, + title: 'common/SingleContentLayout', +}; + +const Template: ComponentStory = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + children: '컨텐츠', +}; diff --git a/src/components/common/SingleContentLayout/SingleContentLayout.styles.tsx b/src/components/common/SingleContentLayout/SingleContentLayout.styles.tsx new file mode 100644 index 0000000..9e4cb3e --- /dev/null +++ b/src/components/common/SingleContentLayout/SingleContentLayout.styles.tsx @@ -0,0 +1,7 @@ +import styled from '@emotion/styled'; + +export const Wrapper = styled.div` + display: flex; + justify-content: center; + margin-top: 206px; +`; diff --git a/src/components/common/SingleContentLayout/SingleContentLayout.tsx b/src/components/common/SingleContentLayout/SingleContentLayout.tsx new file mode 100644 index 0000000..020c75c --- /dev/null +++ b/src/components/common/SingleContentLayout/SingleContentLayout.tsx @@ -0,0 +1,18 @@ +import React, { FC, PropsWithChildren } from 'react'; + +import Header from '@src/components/common/Header'; + +import * as S from './SingleContentLayout.styles'; + +const ContentLayout: FC = (props) => { + const { children } = props; + + return ( + <> +
+ {children} + + ); +}; + +export default ContentLayout; diff --git a/src/components/common/SingleContentLayout/index.tsx b/src/components/common/SingleContentLayout/index.tsx new file mode 100644 index 0000000..fb3e7f2 --- /dev/null +++ b/src/components/common/SingleContentLayout/index.tsx @@ -0,0 +1 @@ +export { default } from './SingleContentLayout'; diff --git a/src/components/common/UserInfo/UserInfo.stories.tsx b/src/components/common/UserInfo/UserInfo.stories.tsx new file mode 100644 index 0000000..027bcc4 --- /dev/null +++ b/src/components/common/UserInfo/UserInfo.stories.tsx @@ -0,0 +1,21 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; + +import UserInfo from './UserInfo'; + +export default { + title: 'common/UserInfo', + component: UserInfo, + args: {}, +} as ComponentMeta; + +const Template: ComponentStory = ({ ...args }) => ; + +export const simple = Template.bind({}); +simple.args = { + type: 'simple', +}; + +export const full = Template.bind({}); +full.args = { + type: 'full', +}; diff --git a/src/components/common/UserInfo/UserInfo.styles.tsx b/src/components/common/UserInfo/UserInfo.styles.tsx new file mode 100644 index 0000000..66f9e68 --- /dev/null +++ b/src/components/common/UserInfo/UserInfo.styles.tsx @@ -0,0 +1,32 @@ +import styled from '@emotion/styled'; +import Image from 'next/image'; + +import theme from '@src/styles/theme'; + +export const Wrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + padding: 0px; + gap: 8px; +`; + +export const Profile = styled(Image)` + background: transparent; + background-color: ${theme.color.G7}; + border-radius: 50%; +`; + +export const SummaryWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 2px; +`; + +export const UserNickName = styled.p` + font-size: ${theme.textSize.B2}; +`; + +export const UserInfo = styled.p` + font-size: ${theme.textSize.B3}; +`; diff --git a/src/components/common/UserInfo/UserInfo.tsx b/src/components/common/UserInfo/UserInfo.tsx new file mode 100644 index 0000000..bdf4e2e --- /dev/null +++ b/src/components/common/UserInfo/UserInfo.tsx @@ -0,0 +1,38 @@ +import React, { FC } from 'react'; + +import { MEMBER } from '@mocks/data/member'; + +import { Member } from '@src/apis'; +import DefaultImage from '@src/assets/user-default.png'; + +import * as S from './UserInfo.styles'; + +// TODO-GYU: 로그인 관련해서 user api 방식에 따라 달라질 예정 +// 우선 Member 의 MockData 로 처리 +interface Props { + type?: 'simple' | 'full'; + member?: Member; +} + +const UserInfo: FC = (props) => { + const { type = 'full', member = MEMBER } = props; + + const size = type === 'full' ? 44 : 28; + + const { name, jobCategory, workingYears, profileImage } = member; + return ( + + + + {name} + {type === 'full' && ( + + {jobCategory}·{workingYears}년차 + + )} + + + ); +}; + +export default UserInfo; diff --git a/src/components/common/UserInfo/index.tsx b/src/components/common/UserInfo/index.tsx new file mode 100644 index 0000000..fcf3235 --- /dev/null +++ b/src/components/common/UserInfo/index.tsx @@ -0,0 +1 @@ +export { default } from './UserInfo'; diff --git a/src/components/login/Login/Login.stories.tsx b/src/components/login/Login/Login.stories.tsx new file mode 100644 index 0000000..02f4810 --- /dev/null +++ b/src/components/login/Login/Login.stories.tsx @@ -0,0 +1,14 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; + +import Login from './Login'; + +export default { + title: 'login/Login', + component: Login, + args: {}, +} as ComponentMeta; + +const Template: ComponentStory = ({ ...args }) => ; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/src/components/login/Login/Login.styles.tsx b/src/components/login/Login/Login.styles.tsx new file mode 100644 index 0000000..19878ef --- /dev/null +++ b/src/components/login/Login/Login.styles.tsx @@ -0,0 +1,58 @@ +import styled from '@emotion/styled'; + +import theme from '@src/styles/theme'; + +export const Wrapper = styled.main` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 519px; + width: 562px; + background-color: ${theme.color.G2}; + padding: 24px; + border-radius: 8px; +`; + +export const Title = styled.p` + margin-bottom: 20px; + color: ${theme.color.Primary1}; + font-family: ${theme.fontFamily.english}; + font-size: 28px; + text-align: center; +`; + +export const Summary = styled.p` + color: ${theme.color.G8}; + font-size: ${theme.textSize.B1}; + line-height: ${theme.lineHeight.B}; +`; + +export const GoogleLogin = styled.a` + display: flex; + justify-content: center; + align-items: center; + height: 50px; + width: 260px; + margin-top: 50px; + margin-bottom: 16px; + padding: 2px 6px; + border-radius: 4px; + background: ${theme.color.G8}; + gap: 10px; + font-size: ${theme.textSize.B2}; + line-height: ${theme.lineHeight.B}; + color: ${theme.color.G1}; + text-decoration: none; + &:hover { + cursor: pointer; + color: ${theme.color.G1}; + } +`; + +export const Terms = styled.p` + font-size: ${theme.textSize.B4}; + line-height: ${theme.lineHeight.B}; + text-align: center; + color: ${theme.color.G7}; +`; diff --git a/src/components/login/Login/Login.tsx b/src/components/login/Login/Login.tsx new file mode 100644 index 0000000..42aa4fe --- /dev/null +++ b/src/components/login/Login/Login.tsx @@ -0,0 +1,38 @@ +import React, { FC } from 'react'; + +import Icon from '@src/components/common/Icon'; + +import * as S from './Login.styles'; + +const Login: FC = () => { + return ( + + Hello There! + 반갑습니다 유저님! + Thumbs UP에 간편하게 로그인하고 토픽에 참여해보세요 + + + Google 계정으로 로그인 + + 로그인은 서비스 이용약관 및 개인정보 처리방침에 동의함을 의미하며, + 서비스 이용을 위해 이메일과 이름, 프로필 이미지를 수집합니다. + + ); +}; + +const getGoogleUrl = () => { + const rootUrl = 'https://accounts.google.com/o/oauth2/v2/auth'; + + const options = { + redirect_uri: 'http://localhost:3000/auth', + scope: 'email', + response_type: 'code', + client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID as string, + }; + + const qs = new URLSearchParams(options); + + return `${rootUrl}?${qs.toString()}`; +}; + +export default Login; diff --git a/src/components/login/Login/index.tsx b/src/components/login/Login/index.tsx new file mode 100644 index 0000000..2a741cd --- /dev/null +++ b/src/components/login/Login/index.tsx @@ -0,0 +1 @@ +export { default } from './Login'; diff --git a/src/configs/recoil.ts b/src/configs/recoil.ts new file mode 100644 index 0000000..f546827 --- /dev/null +++ b/src/configs/recoil.ts @@ -0,0 +1,3 @@ +import { RecoilEnv } from 'recoil'; + +RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED = false; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index cc8f9c8..2357d17 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import type { AppProps } from 'next/app'; +import { useEffect, useState } from 'react'; import { RecoilRoot } from 'recoil'; import '@src/styles/reset.css'; @@ -8,6 +9,7 @@ import '@src/styles/reset.css'; import '@src/styles/common.css'; import { initAxiosConfig } from '@src/configs/axios'; +import '@src/configs/recoil'; import queryClient from '../configs/queryClient'; @@ -27,6 +29,15 @@ if (process.env.NODE_ENV === 'development') { } function MyApp({ Component, pageProps }: AppProps) { + const [domLoaded, setDomLoaded] = useState(false); + useEffect(() => { + setDomLoaded(true); + }, []); + + if (!domLoaded || typeof window === 'undefined') { + return null; + } + return ( diff --git a/src/pages/auth.ts b/src/pages/auth.ts new file mode 100644 index 0000000..0dc39ef --- /dev/null +++ b/src/pages/auth.ts @@ -0,0 +1,40 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +import { signin } from '@src/apis'; +import $userSession from '@src/recoil/userSession'; + +const Auth = () => { + const router = useRouter(); + const setUserSession = useSetRecoilState($userSession); + + const code = router.query.code as string; + + useEffect(() => { + if (!code) return; + + const googleOauth = async () => { + const result = await signin(code); + + // 에러 인 경우 + if (!result) return; + + if (result.isMember === true) { + // 이미 가입된 유저로 홈으로 이동 + // 로그인 성공 + // user 상태 관리! + setUserSession(result); + + router.push('/'); + } else { + // onboarding 으로 이동 + // router.push('/onboardin'); + } + }; + + googleOauth(); + }, [code, router, setUserSession]); +}; + +export default Auth; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..63268d3 --- /dev/null +++ b/src/pages/login/index.tsx @@ -0,0 +1,14 @@ +import type { NextPage } from 'next'; + +import SingleContentLayout from '@src/components/common/SingleContentLayout'; +import Login from '@src/components/login/Login'; + +const LoginPage: NextPage = () => { + return ( + + + + ); +}; + +export default LoginPage; diff --git a/src/pages/login/login.stories.tsx b/src/pages/login/login.stories.tsx new file mode 100644 index 0000000..29859b1 --- /dev/null +++ b/src/pages/login/login.stories.tsx @@ -0,0 +1,13 @@ +import { ComponentStory } from '@storybook/react'; +import React from 'react'; + +import Login from './'; + +export default { + component: Login, + title: 'pages/Login', +}; + +const Template: ComponentStory = (args) => ; + +export const Default = Template.bind({}); diff --git a/src/recoil/effects/localstorageEffect.ts b/src/recoil/effects/localstorageEffect.ts new file mode 100644 index 0000000..2c8379c --- /dev/null +++ b/src/recoil/effects/localstorageEffect.ts @@ -0,0 +1,21 @@ +import { AtomEffect } from 'recoil'; + +const localStorageEffect: (key: string) => AtomEffect = + (key: string) => + ({ setSelf, onSet }) => { + if (typeof window === 'undefined') { + return; + } + + const savedValue = localStorage.getItem(key); + + if (savedValue) { + setSelf(JSON.parse(savedValue)); + } + + onSet((newValue, _, isReset) => { + isReset ? localStorage.removeItem(key) : localStorage.setItem(key, JSON.stringify(newValue)); + }); + }; + +export default localStorageEffect; diff --git a/src/recoil/userSession.ts b/src/recoil/userSession.ts new file mode 100644 index 0000000..d6c00bb --- /dev/null +++ b/src/recoil/userSession.ts @@ -0,0 +1,15 @@ +import { atom } from 'recoil'; + +// TODO-GYU: user 값이 무엇인지에 따라 타입 변경해야함! (API 나오면 확인하기) +// 임시로 Auth로 처리 +import { Auth } from '@src/apis/auth'; + +import localStorageEffect from './effects/localstorageEffect'; + +const $userSession = atom({ + key: 'user-sesseion', + default: undefined, + effects: [localStorageEffect('user')], +}); + +export default $userSession; diff --git a/src/styles/reset.css b/src/styles/reset.css index cb8e3b6..922bbb2 100644 --- a/src/styles/reset.css +++ b/src/styles/reset.css @@ -117,3 +117,6 @@ button { background-color: transparent; font-family: 'Pretendard-Regular', 'Noto Sans CJK KR', syne; } +a { + text-decoration: none; +}