diff --git a/.github/workflows/workflow-pr.yml b/.github/workflows/workflow-pr.yml index 0f73331..bdb7386 100644 --- a/.github/workflows/workflow-pr.yml +++ b/.github/workflows/workflow-pr.yml @@ -26,5 +26,13 @@ jobs: - name: Install Dependencies run: yarn install --frozen-lockfile + # 환경 변수 추가 + - name: Set Environment Variables + run: | + echo "NEXT_PUBLIC_KAKAO_API_KEY=${{ secrets.NEXT_PUBLIC_KAKAO_API_KEY }}" >> .env + echo "NEXT_PUBLIC_KAKAO_REDIRECT_URI=${{ secrets.NEXT_PUBLIC_KAKAO_REDIRECT_URI }}" >> .env + echo "NEXT_PUBLIC_GOOGLE_API_KEY=${{ secrets.NEXT_PUBLIC_GOOGLE_API_KEY }}" >> .env + echo "NEXT_PUBLIC_GOOGLE_REDIRECT_URI=${{ secrets.NEXT_PUBLIC_GOOGLE_REDIRECT_URI }}" >> .env + - name: Build run: yarn build diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 40b845e..599122f 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -29,6 +29,14 @@ jobs: - name: Install Dependencies run: yarn install --frozen-lockfile + # 환경 변수 추가 + - name: Set Environment Variables + run: | + echo "NEXT_PUBLIC_KAKAO_API_KEY=${{ secrets.NEXT_PUBLIC_KAKAO_API_KEY }}" >> .env + echo "NEXT_PUBLIC_KAKAO_REDIRECT_URI=${{ secrets.NEXT_PUBLIC_KAKAO_REDIRECT_URI }}" >> .env + echo "NEXT_PUBLIC_GOOGLE_API_KEY=${{ secrets.NEXT_PUBLIC_GOOGLE_API_KEY }}" >> .env + echo "NEXT_PUBLIC_GOOGLE_REDIRECT_URI=${{ secrets.NEXT_PUBLIC_GOOGLE_REDIRECT_URI }}" >> .env + - name: Build run: yarn build diff --git a/.gitignore b/.gitignore index fd3dbb5..00bba9b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env*.local +.env # vercel .vercel diff --git a/package.json b/package.json index cb988cd..319d0ef 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@commitlint/config-conventional": "^18.4.3", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", + "@tanstack/react-query": "^5.18.1", "@typescript-eslint/parser": "^6.19.0", "axios": "^1.6.5", "dayjs": "^1.11.10", diff --git a/src/api/login/getToken.ts b/src/api/login/getToken.ts new file mode 100644 index 0000000..9a752f5 --- /dev/null +++ b/src/api/login/getToken.ts @@ -0,0 +1,31 @@ +import axiosInstance from '../axiosInstance'; + +interface LoginResponseType { + data: { accessToken: string; refreshToken: string; email: string }; +} + +const fetchKaKao = async (code: string): Promise => + await axiosInstance.get(`/users/kakao-login?code=${code}`); + +export const getKaKaoToken: (code: string) => Promise = async (code) => { + fetchKaKao(code) + .then((response: LoginResponseType) => { + localStorage.setItem('token', response.data.accessToken); + }) + .catch((error) => { + console.error(error); + }); +}; + +const fetchGoogle = async (code: string): Promise => + await axiosInstance.get(`/users/google-login?code=${code}`); + +export const getGoogleToken: (code: string) => Promise = async (code) => { + fetchGoogle(code) + .then((response: LoginResponseType) => { + localStorage.setItem('token', response.data.accessToken); + }) + .catch((error) => { + console.error(error); + }); +}; diff --git a/src/assets/images/img_login_google.svg b/src/assets/images/img_login_google.svg new file mode 100644 index 0000000..373a445 --- /dev/null +++ b/src/assets/images/img_login_google.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/img_login_kakao.svg b/src/assets/images/img_login_kakao.svg new file mode 100644 index 0000000..507950a --- /dev/null +++ b/src/assets/images/img_login_kakao.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/img_login_monsters.svg b/src/assets/images/img_login_monsters.svg new file mode 100644 index 0000000..9af49a3 --- /dev/null +++ b/src/assets/images/img_login_monsters.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/atoms/ExclamationAlertSpan.tsx b/src/components/atoms/ExclamationAlertSpan.tsx new file mode 100644 index 0000000..4260a5b --- /dev/null +++ b/src/components/atoms/ExclamationAlertSpan.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { ExclamationIcon } from '@/assets/icons'; + +interface AlertMessageProps { + message: string; + className?: string; +} + +const ExclamationAlertSpan: React.FC = ({ + message, + className, +}) => { + return ( + + + {message} + + ); +}; + +export default ExclamationAlertSpan; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index c3e68bf..cedd6a3 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -15,3 +15,4 @@ export { default as SortButton } from './SortButton'; export { default as RadioButtonField } from './RadioButtonField'; export { default as Input } from './Input'; export { default as MiniButton } from './MiniButton'; +export { default as ExclamationAlertSpan } from './ExclamationAlertSpan'; diff --git a/src/components/templates/withLogin.tsx b/src/components/templates/withLogin.tsx new file mode 100644 index 0000000..1ad69e5 --- /dev/null +++ b/src/components/templates/withLogin.tsx @@ -0,0 +1,28 @@ +import React, { useEffect } from 'react'; +import { useRouter } from 'next/router'; + +const withLogin = (InnerComponent: React.FC) => { + return () => { + const router = useRouter(); + const token = localStorage.getItem('token'); + + const redirectToLogin: () => Promise = async () => { + if (!token) { + alert('로그인이 필요합니다.'); + try { + await router.push('/login'); + } catch (error) { + console.error('로그인 체크 에러', error); + } + } + }; + + useEffect(() => { + void redirectToLogin(); + }, [token]); + + return token ? : null; + }; +}; + +export default withLogin; diff --git a/src/hooks/queries/fridge/index.ts b/src/hooks/queries/fridge/index.ts new file mode 100644 index 0000000..8c602e7 --- /dev/null +++ b/src/hooks/queries/fridge/index.ts @@ -0,0 +1 @@ +export { default as useGetIngredientList } from './useGetIngredientList'; diff --git a/src/hooks/queries/fridge/useGetIngredientList.ts b/src/hooks/queries/fridge/useGetIngredientList.ts new file mode 100644 index 0000000..d3e126e --- /dev/null +++ b/src/hooks/queries/fridge/useGetIngredientList.ts @@ -0,0 +1,9 @@ +import type { IngredientType } from '@/types/fridge'; +import { queryKeys } from '../queryKeys'; +import { useBaseQuery } from '../useBaseQuery'; + +const useGetIngredientList = () => { + return useBaseQuery(queryKeys.INGREDIENT(), '/ingrs'); +}; + +export default useGetIngredientList; diff --git a/src/hooks/queries/queryKeys.ts b/src/hooks/queries/queryKeys.ts new file mode 100644 index 0000000..d4896fc --- /dev/null +++ b/src/hooks/queries/queryKeys.ts @@ -0,0 +1,5 @@ +export const queryKeys = { + INGREDIENT: (id?: number) => ['ingredient', id] as const, +} as const; + +export type QueryKeys = (typeof queryKeys)[keyof typeof queryKeys]; diff --git a/src/hooks/queries/useBaseQuery.ts b/src/hooks/queries/useBaseQuery.ts new file mode 100644 index 0000000..c3cf378 --- /dev/null +++ b/src/hooks/queries/useBaseQuery.ts @@ -0,0 +1,19 @@ +import axiosInstance from '@/api/axiosInstance'; +import { useQuery } from '@tanstack/react-query'; + +export const fetchData = async (url: string) => { + const response = await axiosInstance.get<{ data: T }>(url); + return response.data; +}; + +export const useBaseQuery = (queryKey: any, url: string) => { + return useQuery({ + queryKey, + queryFn: async () => + await fetchData(url) + .then((res) => res.data) + .catch((error) => { + console.error(error); + }), + }); +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 02f0d7c..47fa34c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -3,6 +3,7 @@ import Layout from '@/components/templates/Layout'; import '@/styles/globals.css'; import type { AppProps } from 'next/app'; import { RecoilRoot } from 'recoil'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import 'dayjs/locale/ko'; import dayjs from 'dayjs'; dayjs.locale('ko'); @@ -13,15 +14,26 @@ const theme = extendTheme({ }, }); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + }, + }, +}); + export default function App({ Component, pageProps }: AppProps): JSX.Element { return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/src/pages/fridge/index.tsx b/src/pages/fridge/index.tsx index 8b8f527..73d827c 100644 --- a/src/pages/fridge/index.tsx +++ b/src/pages/fridge/index.tsx @@ -7,12 +7,16 @@ import { } from '@/components/organisms'; import { type NextPage } from 'next'; import { useState } from 'react'; +import { useGetIngredientList } from '@/hooks/queries/fridge'; const FridgePage: NextPage = () => { const [isOpenIngredientAddModal, setIsOpenIngredientAddModal] = useState(false); const [isOpenFridgeListModal, setIsOpenFridgeListModal] = useState(false); + const data = useGetIngredientList(); + console.log('받아올 데이터', data); + const toggleIsOpenIngredientAddModal: () => void = () => { setIsOpenIngredientAddModal((prev) => !prev); }; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..946d9b7 --- /dev/null +++ b/src/pages/login/index.tsx @@ -0,0 +1,61 @@ +import MonstersImg from '@/assets/images/img_login_monsters.svg'; +import KaKaoImg from '@/assets/images/img_login_kakao.svg'; +import GoogleImg from '@/assets/images/img_login_google.svg'; +import { type NextPage } from 'next'; +import { useEffect } from 'react'; +import { getKaKaoToken } from '@/api/login/getToken'; + +const LoginPage: NextPage = () => { + const kakaoURL = `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI}&response_type=code`; + const googleURL = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI}&response_type=code&scope=email&access_type=offline`; + + const handleKaKaoClick: () => void = () => { + window.location.href = `${kakaoURL}&type=kakao`; + }; + + const handleGoogleClick: () => void = () => { + window.location.href = `${googleURL}&type=google`; + }; + + useEffect(() => { + const fetchData = async (): Promise => { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + // const type = urlParams.get('type'); + // 구글 추가시 타입 redirect_uri 변경 + + if (code) { + await getKaKaoToken(code); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchData(); + }, []); + + return ( +
+
+ +
+ 냉장고 관리, 얼른 시작해봐요 +
+
로고
+
+
+
+
+
SNS 계정으로 로그인
+
+
+
+ + +
+
+
+ ); +}; +export default LoginPage; diff --git a/src/pages/mypage/profile/index.tsx b/src/pages/mypage/profile/index.tsx index 5f84358..423f798 100644 --- a/src/pages/mypage/profile/index.tsx +++ b/src/pages/mypage/profile/index.tsx @@ -1,9 +1,8 @@ import { type NextPage } from 'next'; import Image from 'next/image'; import ProfileImg from '@/assets/profile.png'; -import { Button } from '@/components/atoms'; +import { Button, ExclamationAlertSpan } from '@/components/atoms'; import React, { useCallback, useState } from 'react'; -import { ExclamationIcon } from '@/assets/icons'; import Header from '@/components/organisms/Header'; import { debounceFunction } from '@/utils/debounceUtil'; @@ -70,10 +69,9 @@ const FriendsListPage: NextPage = () => { 사용가능한 닉네임입니다. ) : ( -
- - 중복되는 닉네임이에요. 다시 작성해주세요. -
+ ))}
diff --git a/src/types/fridge/index.d.ts b/src/types/fridge/index.d.ts new file mode 100644 index 0000000..dce6cc7 --- /dev/null +++ b/src/types/fridge/index.d.ts @@ -0,0 +1,4 @@ +export interface IngredientType { + name: string; + category: string; +} diff --git a/yarn.lock b/yarn.lock index 53be205..bd2ca0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3966,6 +3966,18 @@ resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a" integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw== +"@tanstack/query-core@5.18.1": + version "5.18.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.18.1.tgz#b653ee354b7f4712d53565ccc5c6d8fb83ec866c" + integrity sha512-fYhrG7bHgSNbnkIJF2R4VUXb4lF7EBiQjKkDc5wOlB7usdQOIN4LxxHpDxyE3qjqIst1WBGvDtL48T0sHJGKCw== + +"@tanstack/react-query@^5.18.1": + version "5.18.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.18.1.tgz#fd4e7b87260e82c5277355ad64f0e431a9302e02" + integrity sha512-PdI07BbsahZ+04PxSuDQsQvBWe008eWFk/YYWzt8fvzt2sALUM0TpAJa/DFpqa7+SSo7j1EQR6Jx6znXNHyaXw== + dependencies: + "@tanstack/query-core" "5.18.1" + "@testing-library/dom@^9.3.1": version "9.3.4" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce"