From c0eb17e2bda5f3f427bc50973324d9d0273d69dd Mon Sep 17 00:00:00 2001 From: turtle601 Date: Fri, 28 Jul 2023 21:11:49 +0900 Subject: [PATCH 01/20] =?UTF-8?q?chore:=20react-router-dom=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=85=8B=ED=8C=85=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 3 ++- frontend/yarn.lock | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 913d3ac0f..9e6784510 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "main": "index.tsx", "license": "MIT", "scripts": { - "start": "webpack serve --open --mode development", + "start": "webpack serve --open --mode development --port 3000", "start:prod": "webpack serve --open --mode production", "build": "webpack --mode production", "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", @@ -32,6 +32,7 @@ "msw": "^1.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.14.2", "storybook": "^7.0.25", "styled-components": "^6.0.2", "ts-loader": "^9.4.4", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 728acf6a8..3f5ec7114 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2146,6 +2146,11 @@ schema-utils "^3.0.0" source-map "^0.7.3" +"@remix-run/router@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8" + integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -9672,6 +9677,21 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-router-dom@^6.14.2: + version "6.14.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.2.tgz#88f520118b91aa60233bd08dbd3fdcaea3a68488" + integrity sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg== + dependencies: + "@remix-run/router" "1.7.2" + react-router "6.14.2" + +react-router@6.14.2: + version "6.14.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.2.tgz#1f60994d8c369de7b8ba7a78d8f7ec23df76b300" + integrity sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ== + dependencies: + "@remix-run/router" "1.7.2" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" From c25997518f0a8445444d46e45bcca07c3f37b6fa Mon Sep 17 00:00:00 2001 From: turtle601 Date: Fri, 28 Jul 2023 21:12:20 +0900 Subject: [PATCH 02/20] =?UTF-8?q?chore:=20naver,=20kakao=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=85=8B=ED=8C=85=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/icons/oauth/kakao.svg | 2 ++ frontend/src/assets/icons/oauth/naver.svg | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 frontend/src/assets/icons/oauth/kakao.svg create mode 100644 frontend/src/assets/icons/oauth/naver.svg diff --git a/frontend/src/assets/icons/oauth/kakao.svg b/frontend/src/assets/icons/oauth/kakao.svg new file mode 100644 index 000000000..1915a9b39 --- /dev/null +++ b/frontend/src/assets/icons/oauth/kakao.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/frontend/src/assets/icons/oauth/naver.svg b/frontend/src/assets/icons/oauth/naver.svg new file mode 100644 index 000000000..a9c147926 --- /dev/null +++ b/frontend/src/assets/icons/oauth/naver.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file From cc8fc400ae0475ebf4522451150e15d07139ec4c Mon Sep 17 00:00:00 2001 From: turtle601 Date: Fri, 28 Jul 2023 21:13:15 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20api=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EC=84=A0=EC=96=B8=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/api.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 frontend/src/constants/api.ts diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts new file mode 100644 index 000000000..bcbc5592a --- /dev/null +++ b/frontend/src/constants/api.ts @@ -0,0 +1,13 @@ +export const BASE_URL = `${window.location.protocol}//${process.env.BASE_URL}/api`; + +export const OAUTH_LINK = { + google: `${BASE_URL}/oauth/google`, + kakao: `${BASE_URL}/oauth/kakao`, + naver: `${BASE_URL}/oauth/naver`, +}; + +export const OAUTH_BUTTON_MESSAGE = { + google: '구글로 로그인하기', + kakao: '카카오로 로그인하기', + naver: '네이버로 로그인하기', +}; From c4190d90e53e487ac47946a84e546048e3695a08 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Fri, 28 Jul 2023 21:14:19 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=EB=B2=84,=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EA=B5=AC=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoginButton/LoginButton.stories.tsx | 33 ++++++++++ .../@common/LoginButton/LoginButton.tsx | 65 +++++++++++++++++++ .../components/@common/LoginButton/index.tsx | 3 + 3 files changed, 101 insertions(+) create mode 100644 frontend/src/components/@common/LoginButton/LoginButton.stories.tsx create mode 100644 frontend/src/components/@common/LoginButton/LoginButton.tsx create mode 100644 frontend/src/components/@common/LoginButton/index.tsx diff --git a/frontend/src/components/@common/LoginButton/LoginButton.stories.tsx b/frontend/src/components/@common/LoginButton/LoginButton.stories.tsx new file mode 100644 index 000000000..2b7cef39e --- /dev/null +++ b/frontend/src/components/@common/LoginButton/LoginButton.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import LoginButton from './LoginButton'; + +const meta: Meta = { + title: 'Oauth/LoginButton', + component: LoginButton, + decorators: [ + Story => ( + + + } /> + + + ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { type: 'google' }, +}; + +export const KaKao: Story = { + args: { type: 'kakao' }, +}; + +export const Naver: Story = { + args: { type: 'naver' }, +}; diff --git a/frontend/src/components/@common/LoginButton/LoginButton.tsx b/frontend/src/components/@common/LoginButton/LoginButton.tsx new file mode 100644 index 000000000..daf526e91 --- /dev/null +++ b/frontend/src/components/@common/LoginButton/LoginButton.tsx @@ -0,0 +1,65 @@ +import { Link } from 'react-router-dom'; +import styled, { css } from 'styled-components'; +import React from 'react'; +import { OAUTH_BUTTON_MESSAGE, OAUTH_LINK } from '~/constants/api'; + +import KaKao from '~/assets/icons/oauth/kakao.svg'; +import Naver from '~/assets/icons/oauth/naver.svg'; + +interface LoginButtonProps { + type: 'google' | 'kakao' | 'naver'; +} + +const LoginIcon: Record = { + naver: , + kakao: , +}; + +function LoginButton({ type }: LoginButtonProps) { + return ( + +
{LoginIcon[type]}
+ {OAUTH_BUTTON_MESSAGE[type]} +
+ ); +} + +export default LoginButton; + +const StyledLoginButtonWrapper = styled(Link)` + display: flex; + + width: 100%; + height: fit-content; + + padding: 2.3rem 1.3rem; + + border-radius: 12px; + + font-size: 1.4rem; + font-weight: 600; + text-decoration: none; + + ${({ type }) => + type === 'naver' && + css` + background: #03c759; + + color: #fff; + `} + + ${({ type }) => + type === 'kakao' && + css` + background: #fee500; + `} + + cursor: pointer; + transition: box-shadow 0.2s cubic-bezier(0.2, 0, 0, 1), transform 0.1s cubic-bezier(0.2, 0, 0, 1); +`; + +const StyledLoginButtonText = styled.span` + margin: 0 auto; + + color: inherit; +`; diff --git a/frontend/src/components/@common/LoginButton/index.tsx b/frontend/src/components/@common/LoginButton/index.tsx new file mode 100644 index 000000000..9ee6be126 --- /dev/null +++ b/frontend/src/components/@common/LoginButton/index.tsx @@ -0,0 +1,3 @@ +import LoginButton from '~/components/@common/LoginButton/LoginButton'; + +export default LoginButton; From 97445a3397fd2022cec4dde196ab725753376a18 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Fri, 28 Jul 2023 21:15:28 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20Oauth=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 15 ++++++-- frontend/src/pages/MainPage.tsx | 6 +++- frontend/src/pages/OauthRedirectPage.tsx | 44 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/OauthRedirectPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b45495c4f..2d0311bdd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,18 @@ -import MainPage from './pages/MainPage'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import OauthRedirectPage from '~/pages/OauthRedirectPage'; +import MainPage from '~/pages/MainPage'; function App() { - return ; + return ( + + + } /> + } /> + } /> + } /> + + + ); } export default App; diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index ef251fcce..68306dc66 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -17,6 +17,7 @@ import type { Celeb } from '~/@types/celeb.types'; import type { RestaurantData } from '~/@types/api.types'; import type { Coordinate, CoordinateBoundary } from '~/@types/map.types'; import type { Restaurant, RestaurantCategory, RestaurantModalInfo } from '~/@types/restaurant.types'; +import LoginButton from '~/components/@common/LoginButton/LoginButton'; function MainPage() { const [currentRestaurant, setCurrentRestaurant] = useState(null); @@ -32,7 +33,7 @@ function MainPage() { async (queryObject: { boundary: CoordinateBoundary; celebId: number; category: RestaurantCategory }) => { const queryString = getQueryString(queryObject); const response = await handleFetch({ queryString }); - + setData(response.content); }, [boundary, celebId, restaurantCategory], @@ -79,6 +80,9 @@ function MainPage() { return ( <>
+ + + diff --git a/frontend/src/pages/OauthRedirectPage.tsx b/frontend/src/pages/OauthRedirectPage.tsx new file mode 100644 index 000000000..75a2532e4 --- /dev/null +++ b/frontend/src/pages/OauthRedirectPage.tsx @@ -0,0 +1,44 @@ +import React, { useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { BASE_URL } from '~/constants/api'; + +interface OauthRedirectProps { + type: 'google' | 'kakao' | 'naver'; +} + +function OauthRedirectPage({ type }: OauthRedirectProps) { + const location = useLocation(); + const navigate = useNavigate(); + + const handleOAuth = async (code: string) => { + try { + // 카카오로부터 받아온 code를 서버에 전달하여 카카오로 회원가입 & 로그인한다 + const response = await fetch(`${BASE_URL}/oauth/login/${type}?code=${code}`); + await response.json(); // 응답 데이터 + + alert('로그인 성공'); + + navigate('/success'); + } catch (error) { + navigate('/fail'); // 실패 페이지 + } + }; + + useEffect(() => { + const searchParams = new URLSearchParams(location.search); + const code = searchParams.get('code'); // 카카오는 Redirect 시키면서 code를 쿼리 스트링으로 준다. + + if (code) { + alert(`CODE = ${code}`); + handleOAuth(code); + } + }, [location]); + + return ( +
+
Processing...
+
+ ); +} + +export default OauthRedirectPage; From 3e34ca977e794c0c1f029872cbe2570b239cee84 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sat, 29 Jul 2023 11:05:10 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20ui=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/icons/oauth/google.svg | 1 + .../@common/LoginButton/LoginButton.stories.tsx | 2 +- .../src/components/@common/LoginButton/LoginButton.tsx | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 frontend/src/assets/icons/oauth/google.svg diff --git a/frontend/src/assets/icons/oauth/google.svg b/frontend/src/assets/icons/oauth/google.svg new file mode 100644 index 000000000..138004410 --- /dev/null +++ b/frontend/src/assets/icons/oauth/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/@common/LoginButton/LoginButton.stories.tsx b/frontend/src/components/@common/LoginButton/LoginButton.stories.tsx index 2b7cef39e..52cd8c2e0 100644 --- a/frontend/src/components/@common/LoginButton/LoginButton.stories.tsx +++ b/frontend/src/components/@common/LoginButton/LoginButton.stories.tsx @@ -20,7 +20,7 @@ export default meta; type Story = StoryObj; -export const Default: Story = { +export const Google: Story = { args: { type: 'google' }, }; diff --git a/frontend/src/components/@common/LoginButton/LoginButton.tsx b/frontend/src/components/@common/LoginButton/LoginButton.tsx index daf526e91..51ad3f07a 100644 --- a/frontend/src/components/@common/LoginButton/LoginButton.tsx +++ b/frontend/src/components/@common/LoginButton/LoginButton.tsx @@ -5,6 +5,7 @@ import { OAUTH_BUTTON_MESSAGE, OAUTH_LINK } from '~/constants/api'; import KaKao from '~/assets/icons/oauth/kakao.svg'; import Naver from '~/assets/icons/oauth/naver.svg'; +import Google from '~/assets/icons/oauth/google.svg'; interface LoginButtonProps { type: 'google' | 'kakao' | 'naver'; @@ -13,6 +14,7 @@ interface LoginButtonProps { const LoginIcon: Record = { naver: , kakao: , + google: , }; function LoginButton({ type }: LoginButtonProps) { @@ -54,6 +56,12 @@ const StyledLoginButtonWrapper = styled(Link)` background: #fee500; `} + ${({ type }) => + type === 'google' && + css` + border: 1px solid var(--gray-3); + `} + cursor: pointer; transition: box-shadow 0.2s cubic-bezier(0.2, 0, 0, 1), transform 0.1s cubic-bezier(0.2, 0, 0, 1); `; From 38b01c366b6ae3f055816324be59b4ad316c59db Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sat, 29 Jul 2023 11:50:58 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20ui=20=EA=B5=AC=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/icons/etc/menu.svg | 3 ++ frontend/src/assets/icons/etc/user.svg | 1 + .../@common/InfoButton/InfoButton.stories.tsx | 15 ++++++++ .../@common/InfoButton/InfoButton.tsx | 37 +++++++++++++++++++ .../components/@common/InfoButton/index.tsx | 3 ++ 5 files changed, 59 insertions(+) create mode 100644 frontend/src/assets/icons/etc/menu.svg create mode 100644 frontend/src/assets/icons/etc/user.svg create mode 100644 frontend/src/components/@common/InfoButton/InfoButton.stories.tsx create mode 100644 frontend/src/components/@common/InfoButton/InfoButton.tsx create mode 100644 frontend/src/components/@common/InfoButton/index.tsx diff --git a/frontend/src/assets/icons/etc/menu.svg b/frontend/src/assets/icons/etc/menu.svg new file mode 100644 index 000000000..f2063d991 --- /dev/null +++ b/frontend/src/assets/icons/etc/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/etc/user.svg b/frontend/src/assets/icons/etc/user.svg new file mode 100644 index 000000000..23456087a --- /dev/null +++ b/frontend/src/assets/icons/etc/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/@common/InfoButton/InfoButton.stories.tsx b/frontend/src/components/@common/InfoButton/InfoButton.stories.tsx new file mode 100644 index 000000000..4be75506c --- /dev/null +++ b/frontend/src/components/@common/InfoButton/InfoButton.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import InfoButton from './InfoButton'; + +const meta: Meta = { + title: 'InfoButton', + component: InfoButton, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/@common/InfoButton/InfoButton.tsx b/frontend/src/components/@common/InfoButton/InfoButton.tsx new file mode 100644 index 000000000..adda52f06 --- /dev/null +++ b/frontend/src/components/@common/InfoButton/InfoButton.tsx @@ -0,0 +1,37 @@ +import styled from 'styled-components'; + +import Menu from '~/assets/icons/etc/menu.svg'; +import User from '~/assets/icons/etc/user.svg'; + +function InfoButton() { + return ( + + + + + ); +} + +export default InfoButton; + +const StyledInfoButton = styled.button` + display: flex; + justify-content: space-between; + align-items: center; + + width: 77px; + + padding: 0.5rem 0.5rem 0.5rem 1.2rem; + + border: 1px solid #ddd; + border-radius: 21px; + background: transparent; + + cursor: pointer; + + &:hover { + box-shadow: 0 1px 2px rgb(0 0 0 / 15%); + + transition: box-shadow 0.2s ease-in-out; + } +`; diff --git a/frontend/src/components/@common/InfoButton/index.tsx b/frontend/src/components/@common/InfoButton/index.tsx new file mode 100644 index 000000000..dac566eef --- /dev/null +++ b/frontend/src/components/@common/InfoButton/index.tsx @@ -0,0 +1,3 @@ +import InfoButton from '~/components/@common/InfoButton/InfoButton'; + +export default InfoButton; From c3874922031e5ebd4bac9579fd2ed158ac277275 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sat, 29 Jul 2023 20:06:47 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20InfoDropDown=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@common/InfoButton/InfoButton.tsx | 18 ++- .../InfoDropDown/InfoDropDown.stories.tsx | 36 ++++++ .../components/InfoDropDown/InfoDropDown.tsx | 117 ++++++++++++++++++ .../src/components/InfoDropDown/index.tsx | 0 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/InfoDropDown/InfoDropDown.stories.tsx create mode 100644 frontend/src/components/InfoDropDown/InfoDropDown.tsx create mode 100644 frontend/src/components/InfoDropDown/index.tsx diff --git a/frontend/src/components/@common/InfoButton/InfoButton.tsx b/frontend/src/components/@common/InfoButton/InfoButton.tsx index adda52f06..69bdad9be 100644 --- a/frontend/src/components/@common/InfoButton/InfoButton.tsx +++ b/frontend/src/components/@common/InfoButton/InfoButton.tsx @@ -1,11 +1,15 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import Menu from '~/assets/icons/etc/menu.svg'; import User from '~/assets/icons/etc/user.svg'; -function InfoButton() { +interface InfoButtonProps { + isShow?: boolean; +} + +function InfoButton({ isShow = false }: InfoButtonProps) { return ( - + @@ -14,7 +18,7 @@ function InfoButton() { export default InfoButton; -const StyledInfoButton = styled.button` +const StyledInfoButton = styled.button` display: flex; justify-content: space-between; align-items: center; @@ -29,6 +33,12 @@ const StyledInfoButton = styled.button` cursor: pointer; + ${({ isShow }) => + isShow && + css` + box-shadow: 0 1px 2px rgb(0 0 0 / 15%); + `} + &:hover { box-shadow: 0 1px 2px rgb(0 0 0 / 15%); diff --git a/frontend/src/components/InfoDropDown/InfoDropDown.stories.tsx b/frontend/src/components/InfoDropDown/InfoDropDown.stories.tsx new file mode 100644 index 000000000..1c62ce2f5 --- /dev/null +++ b/frontend/src/components/InfoDropDown/InfoDropDown.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import InfoDropDown from './InfoDropDown'; + +const meta: Meta = { + title: 'Selector/InfoDropDown', + component: InfoDropDown, +}; + +export default meta; + +const options = [ + { + id: 1, + value: '로그인', + }, + { + id: 2, + value: '회원가입', + }, + { + id: 3, + value: '기타', + }, + { + id: 4, + value: '등 등', + }, +]; + +type Story = StoryObj; + +export const Default: Story = { + args: { + options, + }, +}; diff --git a/frontend/src/components/InfoDropDown/InfoDropDown.tsx b/frontend/src/components/InfoDropDown/InfoDropDown.tsx new file mode 100644 index 000000000..0d232acc1 --- /dev/null +++ b/frontend/src/components/InfoDropDown/InfoDropDown.tsx @@ -0,0 +1,117 @@ +import styled from 'styled-components'; +import { MouseEvent, useCallback, useState } from 'react'; +import InfoButton from '~/components/@common/InfoButton'; + +interface Option { + id: number; + value: string; +} + +interface DropDownProps { + options: Option[]; + isOpen?: boolean; + externalOnClick?: (e?: MouseEvent) => void; +} + +function InfoDropDown({ options, externalOnClick, isOpen = false }: DropDownProps) { + const [isShow, setIsShow] = useState(isOpen); + + const onSelection = () => (event?: MouseEvent) => { + if (externalOnClick) externalOnClick(event); + }; + + const onToggleDropDown = useCallback(() => { + setIsShow(!isShow); + }, [isShow]); + + return ( + + + + + + {isShow && ( + + + {options.map(({ id, value }) => ( + + {value} + + ))} + + + )} + + ); +} + +export default InfoDropDown; + +const StyledInfoButtonWrapper = styled.button` + border: none; + background: transparent; + + cursor: pointer; + outline: none; +`; + +const StyledInfoDropDown = styled.div` + position: relative; +`; + +const StyledDropDownWrapper = styled.ul` + display: flex; + flex-direction: column; + align-content: center; + + position: absolute; + top: calc(100% + 16px); + left: 18px; + + width: 216px; + height: 176px; + + padding: 1.8rem 0; + + border-radius: 10px; + background: white; + + font-size: 1.4rem; + + box-shadow: var(--shadow); +`; + +const StyledSelectContainer = styled.div` + width: 100%; + height: 150px; + + background: transparent; + + overflow-y: auto; +`; + +const StyledDropDownOption = styled.li` + display: flex; + justify-content: space-between; + align-items: center; + + height: 44px; + + margin: 0 1.8rem; + + cursor: pointer; + + & + & { + border-bottom: 1px solid var(--gray-1); + } + + &:first-child { + border-bottom: 1px solid var(--gray-1); + } + + & > div { + display: flex; + align-items: center; + gap: 0.4rem; + } +`; diff --git a/frontend/src/components/InfoDropDown/index.tsx b/frontend/src/components/InfoDropDown/index.tsx new file mode 100644 index 000000000..e69de29bb From 59187a6f087ce3745eef474e1a2daf43e05e6335 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sat, 29 Jul 2023 21:52:55 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20Modal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/index.html | 1 + .../src/components/@common/Modal/Modal.tsx | 11 ++ .../@common/Modal/ModalContent.stories.tsx | 20 +++ .../components/@common/Modal/ModalContent.tsx | 115 ++++++++++++++++++ .../src/components/@common/Modal/index.tsx | 4 + 5 files changed, 151 insertions(+) create mode 100644 frontend/src/components/@common/Modal/Modal.tsx create mode 100644 frontend/src/components/@common/Modal/ModalContent.stories.tsx create mode 100644 frontend/src/components/@common/Modal/ModalContent.tsx create mode 100644 frontend/src/components/@common/Modal/index.tsx diff --git a/frontend/public/index.html b/frontend/public/index.html index b3789f56e..615b5e221 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -11,5 +11,6 @@

경고!

현재 사용 중인 브라우저는 스크립트를 지원하지 않거나, 해당 기능이 활성화 되어 있지 않습니다.
+ diff --git a/frontend/src/components/@common/Modal/Modal.tsx b/frontend/src/components/@common/Modal/Modal.tsx new file mode 100644 index 000000000..b5eb84500 --- /dev/null +++ b/frontend/src/components/@common/Modal/Modal.tsx @@ -0,0 +1,11 @@ +import { createPortal } from 'react-dom'; + +interface ModalProps { + children: React.ReactNode; +} + +function Modal({ children }: ModalProps) { + return createPortal(children, document.querySelector('#modal')); +} + +export default Modal; diff --git a/frontend/src/components/@common/Modal/ModalContent.stories.tsx b/frontend/src/components/@common/Modal/ModalContent.stories.tsx new file mode 100644 index 000000000..4340b066b --- /dev/null +++ b/frontend/src/components/@common/Modal/ModalContent.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ModalContent from './ModalContent'; + +const meta: Meta = { + title: 'ModalContent', + component: ModalContent, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isShow: true, + title: '로그인 또는 회원가입', + handleModalShow: () => {}, + children: '모달 내용', + }, +}; diff --git a/frontend/src/components/@common/Modal/ModalContent.tsx b/frontend/src/components/@common/Modal/ModalContent.tsx new file mode 100644 index 000000000..2e8be3f2d --- /dev/null +++ b/frontend/src/components/@common/Modal/ModalContent.tsx @@ -0,0 +1,115 @@ +import styled, { css } from 'styled-components'; +import Exit from '~/assets/icons/exit.svg'; + +interface ModalContentProps { + isShow?: boolean; + title: string; + handleModalShow: (isShow: boolean) => void; + children: React.ReactNode; +} + +function ModalContent({ isShow = false, title, handleModalShow, children }: ModalContentProps) { + return ( + + { + handleModalShow(false); + }} + > + + + { + handleModalShow(false); + }} + /> + {title} + + {children} + + + + ); +} + +export default ModalContent; + +const StyledModalContentWrapper = styled.div<{ isShow: boolean }>` + display: flex; + justify-content: center; + align-items: center; + + position: fixed; + top: 0; + left: 0; + z-index: 999; + + width: 100%; + height: 100%; + + opacity: 0; + visibility: hidden; + + ${({ isShow }) => + isShow && + css` + visibility: visible; + + opacity: 1; + transition: opacity ease 0.25s; + `} +`; + +const StyledModalOverlay = styled.div` + position: absolute; + top: 0; + left: 0; + z-index: 1; + + width: 100%; + height: 100%; + + background: rgb(0 0 0 / 50%); +`; + +const StyledModalContent = styled.div<{ isShow: boolean }>` + display: flex; + flex-direction: column; + + position: relative; + z-index: 10; + + width: 33%; + min-width: 500px; + max-width: 600px; + min-height: 100px; + + padding: 2rem; + + border-radius: 5px; + background: #fff; + + transition: transform ease 0.3s 0.1s; + transform: translateY(80px); + + overflow-y: auto; + + ${({ isShow }) => + isShow && + css` + transform: translateY(0); + `} +`; + +const StyledModalHeader = styled.h5` + display: flex; + align-items: center; +`; + +const StyledModalTitleText = styled.span` + margin: 0 auto; +`; + +const StyledModalBody = styled.div` + margin-top: 2.4rem; +`; diff --git a/frontend/src/components/@common/Modal/index.tsx b/frontend/src/components/@common/Modal/index.tsx new file mode 100644 index 000000000..4caa2d0b2 --- /dev/null +++ b/frontend/src/components/@common/Modal/index.tsx @@ -0,0 +1,4 @@ +import Modal from '~/components/@common/Modal/Modal'; +import ModalContent from '~/components/@common/Modal/ModalContent'; + +export { Modal, ModalContent }; From eee7e13651fec7c935ba33fa1b917ec19da75cf5 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sun, 30 Jul 2023 01:54:38 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20LoginModalContnet=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explained: 로그인 모달창에 쓰이는 content를 컴포넌트로 구현 --- .../LoginModalContent.stories.tsx | 25 +++++++++++++++++++ .../LoginModalContent/LoginModalContent.tsx | 20 +++++++++++++++ .../components/LoginModalContent/index.tsx | 3 +++ 3 files changed, 48 insertions(+) create mode 100644 frontend/src/components/LoginModalContent/LoginModalContent.stories.tsx create mode 100644 frontend/src/components/LoginModalContent/LoginModalContent.tsx create mode 100644 frontend/src/components/LoginModalContent/index.tsx diff --git a/frontend/src/components/LoginModalContent/LoginModalContent.stories.tsx b/frontend/src/components/LoginModalContent/LoginModalContent.stories.tsx new file mode 100644 index 000000000..48f398ca7 --- /dev/null +++ b/frontend/src/components/LoginModalContent/LoginModalContent.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import LoginModalContent from './LoginModalContent'; + +const meta: Meta = { + title: 'Modal/LoginModalContent', + component: LoginModalContent, + decorators: [ + Story => ( + + + } /> + + + ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/LoginModalContent/LoginModalContent.tsx b/frontend/src/components/LoginModalContent/LoginModalContent.tsx new file mode 100644 index 000000000..a7c7eef30 --- /dev/null +++ b/frontend/src/components/LoginModalContent/LoginModalContent.tsx @@ -0,0 +1,20 @@ +import styled from 'styled-components'; +import LoginButton from '~/components/@common/LoginButton'; + +function LoginModalContent() { + return ( + + + + + + ); +} + +export default LoginModalContent; + +const StyledLoginModalContent = styled.div` + a + a { + margin-top: 1.6rem; + } +`; diff --git a/frontend/src/components/LoginModalContent/index.tsx b/frontend/src/components/LoginModalContent/index.tsx new file mode 100644 index 000000000..c632d277c --- /dev/null +++ b/frontend/src/components/LoginModalContent/index.tsx @@ -0,0 +1,3 @@ +import LoginModalContent from '~/components/LoginModalContent/LoginModalContent'; + +export default LoginModalContent; From 941cc8295e032f466b24eb34d04d6f8176922b28 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sun, 30 Jul 2023 01:56:21 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20Header=EC=97=90=20InfoButton=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/@common/Header/Header.tsx | 38 +++++++++++++++++-- .../components/InfoDropDown/InfoDropDown.tsx | 21 +++++++--- .../src/components/InfoDropDown/index.tsx | 3 ++ frontend/src/pages/MainPage.tsx | 4 -- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/@common/Header/Header.tsx b/frontend/src/components/@common/Header/Header.tsx index 4d43da815..37b6ccf52 100644 --- a/frontend/src/components/@common/Header/Header.tsx +++ b/frontend/src/components/@common/Header/Header.tsx @@ -1,11 +1,40 @@ +import React, { useState } from 'react'; import { styled } from 'styled-components'; import Logo from '~/assets/logo.png'; +import { Modal, ModalContent } from '~/components/@common/Modal'; +import InfoDropDown from '~/components/InfoDropDown'; +import LoginModalContent from '~/components/LoginModalContent'; + +const options = [ + { id: 1, value: '로그인' }, + { id: 2, value: '회원가입' }, +]; function Header() { + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalShow = (isShow: boolean) => { + setIsModalOpen(isShow); + }; + + const handleInfoDropDown = (event: React.MouseEvent) => { + const currentOption = event.currentTarget.dataset.name; + + if (currentOption === '로그인') handleModalShow(true); + }; + return ( - - - + <> + + + + + + + + + + ); } @@ -13,11 +42,12 @@ export default Header; const StyledHeader = styled.header` display: flex; + justify-content: space-between; align-items: center; position: sticky; top: 0; - z-index: 10; + z-index: 20; width: 100%; height: 80px; diff --git a/frontend/src/components/InfoDropDown/InfoDropDown.tsx b/frontend/src/components/InfoDropDown/InfoDropDown.tsx index 0d232acc1..6638bb09e 100644 --- a/frontend/src/components/InfoDropDown/InfoDropDown.tsx +++ b/frontend/src/components/InfoDropDown/InfoDropDown.tsx @@ -10,7 +10,7 @@ interface Option { interface DropDownProps { options: Option[]; isOpen?: boolean; - externalOnClick?: (e?: MouseEvent) => void; + externalOnClick?: (e?: React.MouseEvent) => void; } function InfoDropDown({ options, externalOnClick, isOpen = false }: DropDownProps) { @@ -24,9 +24,13 @@ function InfoDropDown({ options, externalOnClick, isOpen = false }: DropDownProp setIsShow(!isShow); }, [isShow]); + const onCloseDropDown = useCallback(() => { + setIsShow(false); + }, []); + return ( - + @@ -34,7 +38,7 @@ function InfoDropDown({ options, externalOnClick, isOpen = false }: DropDownProp {options.map(({ id, value }) => ( - + {value} ))} @@ -56,7 +60,12 @@ const StyledInfoButtonWrapper = styled.button` `; const StyledInfoDropDown = styled.div` - position: relative; + display: relative; + + z-index: 100000000; + + width: 77px; + height: 42px; `; const StyledDropDownWrapper = styled.ul` @@ -66,7 +75,7 @@ const StyledDropDownWrapper = styled.ul` position: absolute; top: calc(100% + 16px); - left: 18px; + right: 0; width: 216px; height: 176px; @@ -78,7 +87,7 @@ const StyledDropDownWrapper = styled.ul` font-size: 1.4rem; - box-shadow: var(--shadow); + transition: box-shadow 0.2s var(--shadow); `; const StyledSelectContainer = styled.div` diff --git a/frontend/src/components/InfoDropDown/index.tsx b/frontend/src/components/InfoDropDown/index.tsx index e69de29bb..e4ff6498b 100644 --- a/frontend/src/components/InfoDropDown/index.tsx +++ b/frontend/src/components/InfoDropDown/index.tsx @@ -0,0 +1,3 @@ +import InfoDropDown from '~/components/InfoDropDown/InfoDropDown'; + +export default InfoDropDown; diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index 68306dc66..556c0bf8c 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -17,7 +17,6 @@ import type { Celeb } from '~/@types/celeb.types'; import type { RestaurantData } from '~/@types/api.types'; import type { Coordinate, CoordinateBoundary } from '~/@types/map.types'; import type { Restaurant, RestaurantCategory, RestaurantModalInfo } from '~/@types/restaurant.types'; -import LoginButton from '~/components/@common/LoginButton/LoginButton'; function MainPage() { const [currentRestaurant, setCurrentRestaurant] = useState(null); @@ -80,9 +79,6 @@ function MainPage() { return ( <>
- - - From 46ebf24f133ce9e1fd6994e2eff6ca77236f0df3 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sun, 30 Jul 2023 01:57:14 +0900 Subject: [PATCH 12/20] =?UTF-8?q?refactor:=20Modal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B0=80=EC=9A=B4=EB=8D=B0=EC=97=90=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=EC=9D=B4=20=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@common/Modal/ModalContent.stories.tsx | 2 +- .../components/@common/Modal/ModalContent.tsx | 25 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/@common/Modal/ModalContent.stories.tsx b/frontend/src/components/@common/Modal/ModalContent.stories.tsx index 4340b066b..0ff888e93 100644 --- a/frontend/src/components/@common/Modal/ModalContent.stories.tsx +++ b/frontend/src/components/@common/Modal/ModalContent.stories.tsx @@ -10,7 +10,7 @@ export default meta; type Story = StoryObj; -export const Default: Story = { +export const LoginModal: Story = { args: { isShow: true, title: '로그인 또는 회원가입', diff --git a/frontend/src/components/@common/Modal/ModalContent.tsx b/frontend/src/components/@common/Modal/ModalContent.tsx index 2e8be3f2d..a09728eef 100644 --- a/frontend/src/components/@common/Modal/ModalContent.tsx +++ b/frontend/src/components/@common/Modal/ModalContent.tsx @@ -15,19 +15,18 @@ function ModalContent({ isShow = false, title, handleModalShow, children }: Moda onClick={() => { handleModalShow(false); }} - > - - - { - handleModalShow(false); - }} - /> - {title} - - {children} - - + /> + + + { + handleModalShow(false); + }} + /> + {title} + + {children} + ); } From 929cc2e0856ec9129fbc32766c19f58dbc068d3c Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sun, 30 Jul 2023 07:33:55 +0900 Subject: [PATCH 13/20] =?UTF-8?q?refactor:=20useBooleanstate=20=ED=9B=85?= =?UTF-8?q?=20=ED=99=9C=EC=9A=A9=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.vscode/settings.json | 3 ++- .../src/components/@common/Header/Header.tsx | 13 +++++-------- .../@common/Modal/ModalContent.stories.tsx | 2 +- .../components/@common/Modal/ModalContent.tsx | 16 ++++------------ .../src/components/InfoDropDown/InfoDropDown.tsx | 13 +++---------- 5 files changed, 15 insertions(+), 32 deletions(-) diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index ab8fc7934..1ec82437f 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -10,5 +10,6 @@ "stylelint.enable": true, "stylelint.config": null, - "stylelint.validate": ["css", "scss", "typescript", "typescriptreact"] + "stylelint.validate": ["css", "scss", "typescript", "typescriptreact"], + "cSpell.words": ["naver"] } diff --git a/frontend/src/components/@common/Header/Header.tsx b/frontend/src/components/@common/Header/Header.tsx index 37b6ccf52..f8e6541e6 100644 --- a/frontend/src/components/@common/Header/Header.tsx +++ b/frontend/src/components/@common/Header/Header.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; +import React from 'react'; import { styled } from 'styled-components'; import Logo from '~/assets/logo.png'; import { Modal, ModalContent } from '~/components/@common/Modal'; import InfoDropDown from '~/components/InfoDropDown'; import LoginModalContent from '~/components/LoginModalContent'; +import useBooleanState from '~/hooks/useBooleanState'; const options = [ { id: 1, value: '로그인' }, @@ -11,16 +12,12 @@ const options = [ ]; function Header() { - const [isModalOpen, setIsModalOpen] = useState(false); - - const handleModalShow = (isShow: boolean) => { - setIsModalOpen(isShow); - }; + const { value: isModalOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false); const handleInfoDropDown = (event: React.MouseEvent) => { const currentOption = event.currentTarget.dataset.name; - if (currentOption === '로그인') handleModalShow(true); + if (currentOption === '로그인') openModal(); }; return ( @@ -30,7 +27,7 @@ function Header() { - + diff --git a/frontend/src/components/@common/Modal/ModalContent.stories.tsx b/frontend/src/components/@common/Modal/ModalContent.stories.tsx index 0ff888e93..fc7ce1800 100644 --- a/frontend/src/components/@common/Modal/ModalContent.stories.tsx +++ b/frontend/src/components/@common/Modal/ModalContent.stories.tsx @@ -14,7 +14,7 @@ export const LoginModal: Story = { args: { isShow: true, title: '로그인 또는 회원가입', - handleModalShow: () => {}, + closeModal: () => {}, children: '모달 내용', }, }; diff --git a/frontend/src/components/@common/Modal/ModalContent.tsx b/frontend/src/components/@common/Modal/ModalContent.tsx index a09728eef..fbab17a16 100644 --- a/frontend/src/components/@common/Modal/ModalContent.tsx +++ b/frontend/src/components/@common/Modal/ModalContent.tsx @@ -4,25 +4,17 @@ import Exit from '~/assets/icons/exit.svg'; interface ModalContentProps { isShow?: boolean; title: string; - handleModalShow: (isShow: boolean) => void; + closeModal: () => void; children: React.ReactNode; } -function ModalContent({ isShow = false, title, handleModalShow, children }: ModalContentProps) { +function ModalContent({ isShow = false, title, closeModal, children }: ModalContentProps) { return ( - { - handleModalShow(false); - }} - /> + - { - handleModalShow(false); - }} - /> + {title} {children} diff --git a/frontend/src/components/InfoDropDown/InfoDropDown.tsx b/frontend/src/components/InfoDropDown/InfoDropDown.tsx index 6638bb09e..134816914 100644 --- a/frontend/src/components/InfoDropDown/InfoDropDown.tsx +++ b/frontend/src/components/InfoDropDown/InfoDropDown.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components'; -import { MouseEvent, useCallback, useState } from 'react'; +import { MouseEvent } from 'react'; import InfoButton from '~/components/@common/InfoButton'; +import useBooleanState from '~/hooks/useBooleanState'; interface Option { id: number; @@ -14,20 +15,12 @@ interface DropDownProps { } function InfoDropDown({ options, externalOnClick, isOpen = false }: DropDownProps) { - const [isShow, setIsShow] = useState(isOpen); + const { value: isShow, toggle: onToggleDropDown, setFalse: onCloseDropDown } = useBooleanState(isOpen); const onSelection = () => (event?: MouseEvent) => { if (externalOnClick) externalOnClick(event); }; - const onToggleDropDown = useCallback(() => { - setIsShow(!isShow); - }, [isShow]); - - const onCloseDropDown = useCallback(() => { - setIsShow(false); - }, []); - return ( From 5f60d9ef94ba3d970262718331835ec1a4e141e6 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sun, 30 Jul 2023 07:33:55 +0900 Subject: [PATCH 14/20] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/@common/Header/Header.tsx | 13 +++++-------- .../@common/Modal/ModalContent.stories.tsx | 2 +- .../components/@common/Modal/ModalContent.tsx | 16 ++++------------ .../src/components/InfoDropDown/InfoDropDown.tsx | 13 +++---------- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/@common/Header/Header.tsx b/frontend/src/components/@common/Header/Header.tsx index 37b6ccf52..f8e6541e6 100644 --- a/frontend/src/components/@common/Header/Header.tsx +++ b/frontend/src/components/@common/Header/Header.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; +import React from 'react'; import { styled } from 'styled-components'; import Logo from '~/assets/logo.png'; import { Modal, ModalContent } from '~/components/@common/Modal'; import InfoDropDown from '~/components/InfoDropDown'; import LoginModalContent from '~/components/LoginModalContent'; +import useBooleanState from '~/hooks/useBooleanState'; const options = [ { id: 1, value: '로그인' }, @@ -11,16 +12,12 @@ const options = [ ]; function Header() { - const [isModalOpen, setIsModalOpen] = useState(false); - - const handleModalShow = (isShow: boolean) => { - setIsModalOpen(isShow); - }; + const { value: isModalOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false); const handleInfoDropDown = (event: React.MouseEvent) => { const currentOption = event.currentTarget.dataset.name; - if (currentOption === '로그인') handleModalShow(true); + if (currentOption === '로그인') openModal(); }; return ( @@ -30,7 +27,7 @@ function Header() { - + diff --git a/frontend/src/components/@common/Modal/ModalContent.stories.tsx b/frontend/src/components/@common/Modal/ModalContent.stories.tsx index 0ff888e93..fc7ce1800 100644 --- a/frontend/src/components/@common/Modal/ModalContent.stories.tsx +++ b/frontend/src/components/@common/Modal/ModalContent.stories.tsx @@ -14,7 +14,7 @@ export const LoginModal: Story = { args: { isShow: true, title: '로그인 또는 회원가입', - handleModalShow: () => {}, + closeModal: () => {}, children: '모달 내용', }, }; diff --git a/frontend/src/components/@common/Modal/ModalContent.tsx b/frontend/src/components/@common/Modal/ModalContent.tsx index a09728eef..fbab17a16 100644 --- a/frontend/src/components/@common/Modal/ModalContent.tsx +++ b/frontend/src/components/@common/Modal/ModalContent.tsx @@ -4,25 +4,17 @@ import Exit from '~/assets/icons/exit.svg'; interface ModalContentProps { isShow?: boolean; title: string; - handleModalShow: (isShow: boolean) => void; + closeModal: () => void; children: React.ReactNode; } -function ModalContent({ isShow = false, title, handleModalShow, children }: ModalContentProps) { +function ModalContent({ isShow = false, title, closeModal, children }: ModalContentProps) { return ( - { - handleModalShow(false); - }} - /> + - { - handleModalShow(false); - }} - /> + {title} {children} diff --git a/frontend/src/components/InfoDropDown/InfoDropDown.tsx b/frontend/src/components/InfoDropDown/InfoDropDown.tsx index 6638bb09e..134816914 100644 --- a/frontend/src/components/InfoDropDown/InfoDropDown.tsx +++ b/frontend/src/components/InfoDropDown/InfoDropDown.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components'; -import { MouseEvent, useCallback, useState } from 'react'; +import { MouseEvent } from 'react'; import InfoButton from '~/components/@common/InfoButton'; +import useBooleanState from '~/hooks/useBooleanState'; interface Option { id: number; @@ -14,20 +15,12 @@ interface DropDownProps { } function InfoDropDown({ options, externalOnClick, isOpen = false }: DropDownProps) { - const [isShow, setIsShow] = useState(isOpen); + const { value: isShow, toggle: onToggleDropDown, setFalse: onCloseDropDown } = useBooleanState(isOpen); const onSelection = () => (event?: MouseEvent) => { if (externalOnClick) externalOnClick(event); }; - const onToggleDropDown = useCallback(() => { - setIsShow(!isShow); - }, [isShow]); - - const onCloseDropDown = useCallback(() => { - setIsShow(false); - }, []); - return ( From 921339175e122798581c7a52d29a1b6633912c8d Mon Sep 17 00:00:00 2001 From: turtle601 Date: Sun, 30 Jul 2023 07:41:39 +0900 Subject: [PATCH 15/20] =?UTF-8?q?chore:=20.vscode=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.vscode/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index ab8fc7934..f1af082a8 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -9,6 +9,5 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "stylelint.enable": true, - "stylelint.config": null, - "stylelint.validate": ["css", "scss", "typescript", "typescriptreact"] + "stylelint.config": null } From f1a6b2d43aa419837167d592bd3c501c0de57837 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Mon, 31 Jul 2023 13:12:00 +0900 Subject: [PATCH 16/20] =?UTF-8?q?refactor:=20Oauth=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/@types/oauth.types.ts | 1 + frontend/src/components/@common/LoginButton/LoginButton.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 frontend/src/@types/oauth.types.ts diff --git a/frontend/src/@types/oauth.types.ts b/frontend/src/@types/oauth.types.ts new file mode 100644 index 000000000..3dbe5eb87 --- /dev/null +++ b/frontend/src/@types/oauth.types.ts @@ -0,0 +1 @@ +export type Oauth = 'google' | 'kakao' | 'naver'; diff --git a/frontend/src/components/@common/LoginButton/LoginButton.tsx b/frontend/src/components/@common/LoginButton/LoginButton.tsx index 51ad3f07a..4fd1b0e19 100644 --- a/frontend/src/components/@common/LoginButton/LoginButton.tsx +++ b/frontend/src/components/@common/LoginButton/LoginButton.tsx @@ -6,9 +6,10 @@ import { OAUTH_BUTTON_MESSAGE, OAUTH_LINK } from '~/constants/api'; import KaKao from '~/assets/icons/oauth/kakao.svg'; import Naver from '~/assets/icons/oauth/naver.svg'; import Google from '~/assets/icons/oauth/google.svg'; +import { Oauth } from '~/@types/oauth.types'; interface LoginButtonProps { - type: 'google' | 'kakao' | 'naver'; + type: Oauth; } const LoginIcon: Record = { From b704ab92a0557f94f1fcb4335ef30a40ff9ed6c1 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Mon, 31 Jul 2023 13:17:39 +0900 Subject: [PATCH 17/20] =?UTF-8?q?refactor:=20DropDown=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20box-shadow=20=EC=A0=81=EC=9A=A9=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/InfoDropDown/InfoDropDown.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/InfoDropDown/InfoDropDown.tsx b/frontend/src/components/InfoDropDown/InfoDropDown.tsx index 134816914..d757dcc98 100644 --- a/frontend/src/components/InfoDropDown/InfoDropDown.tsx +++ b/frontend/src/components/InfoDropDown/InfoDropDown.tsx @@ -67,8 +67,8 @@ const StyledDropDownWrapper = styled.ul` align-content: center; position: absolute; - top: calc(100% + 16px); - right: 0; + top: calc(100% - 8px); + right: 18px; width: 216px; height: 176px; @@ -80,7 +80,7 @@ const StyledDropDownWrapper = styled.ul` font-size: 1.4rem; - transition: box-shadow 0.2s var(--shadow); + box-shadow: var(--shadow); `; const StyledSelectContainer = styled.div` From 1b7a93dde0c8fb05d5908687a64c9be75ac61086 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Mon, 31 Jul 2023 14:28:22 +0900 Subject: [PATCH 18/20] =?UTF-8?q?refactor:=20box=20shadow=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=82=AC=EC=9A=A9=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/@common/InfoButton/InfoButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/@common/InfoButton/InfoButton.tsx b/frontend/src/components/@common/InfoButton/InfoButton.tsx index 69bdad9be..10f5f0ad2 100644 --- a/frontend/src/components/@common/InfoButton/InfoButton.tsx +++ b/frontend/src/components/@common/InfoButton/InfoButton.tsx @@ -36,11 +36,11 @@ const StyledInfoButton = styled.button` ${({ isShow }) => isShow && css` - box-shadow: 0 1px 2px rgb(0 0 0 / 15%); + box-shadow: var(--shadow); `} &:hover { - box-shadow: 0 1px 2px rgb(0 0 0 / 15%); + box-shadow: var(--shadow); transition: box-shadow 0.2s ease-in-out; } From 286583d742db153de950e11abc0437603ba21fb4 Mon Sep 17 00:00:00 2001 From: turtle601 Date: Mon, 31 Jul 2023 16:21:19 +0900 Subject: [PATCH 19/20] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit b81a58763000e0c3de734588ac1d501ac13f3dcf Author: Minjae Kim Date: Mon Jul 31 16:13:27 2023 +0900 feat: 음식점 카드에 필요한 컴포넌트 추가 (#205) * feat: 마커 호버시 마커를 맨 앞으로 가져오기 (#192) * style: 마커 호버시 마커 강조 (#192) * feat: 마커 클릭시 레스토랑 카드 띄우기 (#192) * refactor: 불필요한 코드 제거 (#192) - 레스토랑 카드 클릭시 맵 모달 이벤트 제거 - 마커 클릭시 맵 모달 이벤트 제거 * feat: 마커클릭 시 마커 위치에 따라 카드모달 위치 조정 (#192) * style: 레스토랑 오버레이 스타일 수정 (#192) * refactor: restaurantCard 컴포넌트를 용도에 따라 스타일 다르게 설정 (#192) * feat: 마커 클릭시 강조 효과 주기 (#192) * Squashed commit of the following: commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) commit 21128038e8dbd0953497950833f65dc918ffc40b Author: 황준승 <78203399+turtle601@users.noreply.github.com> Date: Thu Jul 27 16:45:06 2023 +0900 feat: 음식점 리스트 중복 필터링 기능 구현 (#186) * refactor: 지도 boundary 타입 추가 및 음식점 카테고리 타입 일부 수정 (#180) * feat: 셀럽 및 음식점 카테고리 별 필터링 기능 추가 (#180) * refactor: getQueryString 로직 분리 및 적용 (#180) * feat: CelebDropDown 및 CategoryNavbar에 전체 버튼 추가 (#184) * fix: 필터링 클릭 시 렌더링이 한 박자 늦게되는 오류 해결 (#184) * refactor: Map 컴포넌트에서 사용하지 않는 props 속성 제거 (#184) * refactor: celeb 전체를 나타내는 상태값을 -1로 변경 (#184) * feat: CelebDropDown blur 기능 추가 (#184) * fix: 불필요한 useEffect dependency 제거 (#184) * feat: Restaurant_Category에 전체 옵션 추가 (#184) * fix: CelebId 초기값 수정 (#184) * fix: API 명세서 수정에 따른 데이터 타입 변경 (#184) * refactor: NavButton props 프로퍼티 수정으로 인한 코드 수정 (#184) * refactor: css 선언방식을 삼항연산자를 && 로 변경 (#184) * fix: NavButton 불필요한 hover 기능 제거 (#184) Changed: hover 이벤트 && 연산자를 삼항연산자 사용으로 변경 * refactor: NavButton 컴포넌트를 NavItem 컴포넌트로 네이밍 수정 (#184) --------- Co-authored-by: d0dam Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> * feat: 다른 마커 클릭시 기존 마커 모달 닫기 기능 구현 (#192) * feat: 음식점 카드에 사용하는 컴포넌트 스켈레톤 구현 (#199) * refactor: 음식점 카드 리스트 컴포넌트 분리, 로딩 상태 추가 (#199) * refactor: 이미지에 대해 loading lazy 속성 추가 (#199) * refactor: 로딩 애니메이션을 자연스럽게 수정 (#199) * refactor: Map에 data 로딩 상태 추가 (#199) * feat: 전 음식점 갯수만큼 음식점 스켈레톤을 표시하도록 수정 (#199) * refactor: Map이 로딩 상태일 때 스타일 추가 (#199) * feat: ImageCarousel 컴포넌트 구현 (#201) * refactor: ImageCarousel 컴포넌트의 props 타입을 변경 (#201) * feat: WaterMarkImage 컴포넌트 구현 (#201) * refactor: 음식점 카드에 이미지 대신 케러셀 연결 (#201) * feat: 음식점 사진에 좋아요 아이콘 추가 (#201) * feat: ProfileImageList 구현 및 음식점 카드에 반영 (#201) * refactor: story에 size props 추가 (#201) * refactor: useBooleanState를 활용 가능한 상태를 해당 훅으로 변경 (#201) * Squashed commit of the following: commit cfb505f45c57e126c1266d596f03bc2b814759f9 Author: d0dam Date: Mon Jul 31 15:16:41 2023 +0900 fix: 충돌 해결 간 생긴 에러 및 lint 에러 수정 (#201) commit fbc4ff10e1ef002e75b22151d5cbec105df410bd Merge: 7dc4a3e 60f8707 Author: Minjae Kim Date: Mon Jul 31 15:13:46 2023 +0900 Merge branch 'develop-frontend' into 199-feat-레스토랑-카드에-loading-상태-추가-및-skeleton-반영 commit 7dc4a3e450c1fc54d292c9948d9871510a460f56 Author: d0dam Date: Mon Jul 31 15:10:42 2023 +0900 Squashed commit of the following: commit 60f870732951b14a824138828074370862fe40a8 Author: Jeremy <102432453+shackstack@users.noreply.github.com> Date: Mon Jul 31 14:41:46 2023 +0900 feat: 레스토랑 카드 및 마커 클릭 이벤트 변경 (#198) * feat: 마커 호버시 마커를 맨 앞으로 가져오기 (#192) * style: 마커 호버시 마커 강조 (#192) * feat: 마커 클릭시 레스토랑 카드 띄우기 (#192) * refactor: 불필요한 코드 제거 (#192) - 레스토랑 카드 클릭시 맵 모달 이벤트 제거 - 마커 클릭시 맵 모달 이벤트 제거 * feat: 마커클릭 시 마커 위치에 따라 카드모달 위치 조정 (#192) * style: 레스토랑 오버레이 스타일 수정 (#192) * refactor: restaurantCard 컴포넌트를 용도에 따라 스타일 다르게 설정 (#192) * feat: 마커 클릭시 강조 효과 주기 (#192) * Squashed commit of the following: commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) commit 21128038e8dbd0953497950833f65dc918ffc40b Author: 황준승 <78203399+turtle601@users.noreply.github.com> Date: Thu Jul 27 16:45:06 2023 +0900 feat: 음식점 리스트 중복 필터링 기능 구현 (#186) * refactor: 지도 boundary 타입 추가 및 음식점 카테고리 타입 일부 수정 (#180) * feat: 셀럽 및 음식점 카테고리 별 필터링 기능 추가 (#180) * refactor: getQueryString 로직 분리 및 적용 (#180) * feat: CelebDropDown 및 CategoryNavbar에 전체 버튼 추가 (#184) * fix: 필터링 클릭 시 렌더링이 한 박자 늦게되는 오류 해결 (#184) * refactor: Map 컴포넌트에서 사용하지 않는 props 속성 제거 (#184) * refactor: celeb 전체를 나타내는 상태값을 -1로 변경 (#184) * feat: CelebDropDown blur 기능 추가 (#184) * fix: 불필요한 useEffect dependency 제거 (#184) * feat: Restaurant_Category에 전체 옵션 추가 (#184) * fix: CelebId 초기값 수정 (#184) * fix: API 명세서 수정에 따른 데이터 타입 변경 (#184) * refactor: NavButton props 프로퍼티 수정으로 인한 코드 수정 (#184) * refactor: css 선언방식을 삼항연산자를 && 로 변경 (#184) * fix: NavButton 불필요한 hover 기능 제거 (#184) Changed: hover 이벤트 && 연산자를 삼항연산자 사용으로 변경 * refactor: NavButton 컴포넌트를 NavItem 컴포넌트로 네이밍 수정 (#184) --------- Co-authored-by: d0dam Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> * feat: 다른 마커 클릭시 기존 마커 모달 닫기 기능 구현 (#192) * style: 파일명 오류 수정 (#192) * refactor: baseURL 환경변수 설정 및 type import 분리 * feat: RestaurantCard 컴포넌트 props 수정 (#192) onClick을 optional로 수정 * refactor: getQuadrant 리팩터링 (#192) * style: 상태 네이밍 수정 (#192) mainPosition -> currentCenter * feat: 음식점 카드 호버시 해당 음식점 마커 강조 (#192) * design: 강조시 애니메이션 효과 추가 및 음식점 리스트 스타일 수정 (#192) * refactor: 음식점 카드 호버시 마커 강조 로직 변경 (#192) * refactor: 프로필 이미지 컴포넌트 Props 타입 수정 (#192) size: number => string * fix: setHoverId가 없을 때 default value 설정 (#192) commit 60f870732951b14a824138828074370862fe40a8 Author: Jeremy <102432453+shackstack@users.noreply.github.com> Date: Mon Jul 31 14:41:46 2023 +0900 feat: 레스토랑 카드 및 마커 클릭 이벤트 변경 (#198) * feat: 마커 호버시 마커를 맨 앞으로 가져오기 (#192) * style: 마커 호버시 마커 강조 (#192) * feat: 마커 클릭시 레스토랑 카드 띄우기 (#192) * refactor: 불필요한 코드 제거 (#192) - 레스토랑 카드 클릭시 맵 모달 이벤트 제거 - 마커 클릭시 맵 모달 이벤트 제거 * feat: 마커클릭 시 마커 위치에 따라 카드모달 위치 조정 (#192) * style: 레스토랑 오버레이 스타일 수정 (#192) * refactor: restaurantCard 컴포넌트를 용도에 따라 스타일 다르게 설정 (#192) * feat: 마커 클릭시 강조 효과 주기 (#192) * Squashed commit of the following: commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) commit 21128038e8dbd0953497950833f65dc918ffc40b Author: 황준승 <78203399+turtle601@users.noreply.github.com> Date: Thu Jul 27 16:45:06 2023 +0900 feat: 음식점 리스트 중복 필터링 기능 구현 (#186) * refactor: 지도 boundary 타입 추가 및 음식점 카테고리 타입 일부 수정 (#180) * feat: 셀럽 및 음식점 카테고리 별 필터링 기능 추가 (#180) * refactor: getQueryString 로직 분리 및 적용 (#180) * feat: CelebDropDown 및 CategoryNavbar에 전체 버튼 추가 (#184) * fix: 필터링 클릭 시 렌더링이 한 박자 늦게되는 오류 해결 (#184) * refactor: Map 컴포넌트에서 사용하지 않는 props 속성 제거 (#184) * refactor: celeb 전체를 나타내는 상태값을 -1로 변경 (#184) * feat: CelebDropDown blur 기능 추가 (#184) * fix: 불필요한 useEffect dependency 제거 (#184) * feat: Restaurant_Category에 전체 옵션 추가 (#184) * fix: CelebId 초기값 수정 (#184) * fix: API 명세서 수정에 따른 데이터 타입 변경 (#184) * refactor: NavButton props 프로퍼티 수정으로 인한 코드 수정 (#184) * refactor: css 선언방식을 삼항연산자를 && 로 변경 (#184) * fix: NavButton 불필요한 hover 기능 제거 (#184) Changed: hover 이벤트 && 연산자를 삼항연산자 사용으로 변경 * refactor: NavButton 컴포넌트를 NavItem 컴포넌트로 네이밍 수정 (#184) --------- Co-authored-by: d0dam Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> * feat: 다른 마커 클릭시 기존 마커 모달 닫기 기능 구현 (#192) * style: 파일명 오류 수정 (#192) * refactor: baseURL 환경변수 설정 및 type import 분리 * feat: RestaurantCard 컴포넌트 props 수정 (#192) onClick을 optional로 수정 * refactor: getQuadrant 리팩터링 (#192) * style: 상태 네이밍 수정 (#192) mainPosition -> currentCenter * feat: 음식점 카드 호버시 해당 음식점 마커 강조 (#192) * design: 강조시 애니메이션 효과 추가 및 음식점 리스트 스타일 수정 (#192) * refactor: 음식점 카드 호버시 마커 강조 로직 변경 (#192) * refactor: 프로필 이미지 컴포넌트 Props 타입 수정 (#192) size: number => string * fix: setHoverId가 없을 때 default value 설정 (#192) * refactor: 경로 수정 (#201) * fix: lint 에러 수정 (#201) --------- Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> commit 2977855a9662aa0a822f2d003e6fe5577b1e1618 Author: Minjae Kim Date: Mon Jul 31 16:05:17 2023 +0900 feat: 레스토랑 카드에 loading 상태 추가 및 skeleton 반영 (#202) * feat: 마커 호버시 마커를 맨 앞으로 가져오기 (#192) * style: 마커 호버시 마커 강조 (#192) * feat: 마커 클릭시 레스토랑 카드 띄우기 (#192) * refactor: 불필요한 코드 제거 (#192) - 레스토랑 카드 클릭시 맵 모달 이벤트 제거 - 마커 클릭시 맵 모달 이벤트 제거 * feat: 마커클릭 시 마커 위치에 따라 카드모달 위치 조정 (#192) * style: 레스토랑 오버레이 스타일 수정 (#192) * refactor: restaurantCard 컴포넌트를 용도에 따라 스타일 다르게 설정 (#192) * feat: 마커 클릭시 강조 효과 주기 (#192) * Squashed commit of the following: commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) commit 21128038e8dbd0953497950833f65dc918ffc40b Author: 황준승 <78203399+turtle601@users.noreply.github.com> Date: Thu Jul 27 16:45:06 2023 +0900 feat: 음식점 리스트 중복 필터링 기능 구현 (#186) * refactor: 지도 boundary 타입 추가 및 음식점 카테고리 타입 일부 수정 (#180) * feat: 셀럽 및 음식점 카테고리 별 필터링 기능 추가 (#180) * refactor: getQueryString 로직 분리 및 적용 (#180) * feat: CelebDropDown 및 CategoryNavbar에 전체 버튼 추가 (#184) * fix: 필터링 클릭 시 렌더링이 한 박자 늦게되는 오류 해결 (#184) * refactor: Map 컴포넌트에서 사용하지 않는 props 속성 제거 (#184) * refactor: celeb 전체를 나타내는 상태값을 -1로 변경 (#184) * feat: CelebDropDown blur 기능 추가 (#184) * fix: 불필요한 useEffect dependency 제거 (#184) * feat: Restaurant_Category에 전체 옵션 추가 (#184) * fix: CelebId 초기값 수정 (#184) * fix: API 명세서 수정에 따른 데이터 타입 변경 (#184) * refactor: NavButton props 프로퍼티 수정으로 인한 코드 수정 (#184) * refactor: css 선언방식을 삼항연산자를 && 로 변경 (#184) * fix: NavButton 불필요한 hover 기능 제거 (#184) Changed: hover 이벤트 && 연산자를 삼항연산자 사용으로 변경 * refactor: NavButton 컴포넌트를 NavItem 컴포넌트로 네이밍 수정 (#184) --------- Co-authored-by: d0dam Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> * feat: 다른 마커 클릭시 기존 마커 모달 닫기 기능 구현 (#192) * feat: 음식점 카드에 사용하는 컴포넌트 스켈레톤 구현 (#199) * refactor: 음식점 카드 리스트 컴포넌트 분리, 로딩 상태 추가 (#199) * refactor: 이미지에 대해 loading lazy 속성 추가 (#199) * refactor: 로딩 애니메이션을 자연스럽게 수정 (#199) * refactor: Map에 data 로딩 상태 추가 (#199) * feat: 전 음식점 갯수만큼 음식점 스켈레톤을 표시하도록 수정 (#199) * refactor: Map이 로딩 상태일 때 스타일 추가 (#199) * Squashed commit of the following: commit 60f870732951b14a824138828074370862fe40a8 Author: Jeremy <102432453+shackstack@users.noreply.github.com> Date: Mon Jul 31 14:41:46 2023 +0900 feat: 레스토랑 카드 및 마커 클릭 이벤트 변경 (#198) * feat: 마커 호버시 마커를 맨 앞으로 가져오기 (#192) * style: 마커 호버시 마커 강조 (#192) * feat: 마커 클릭시 레스토랑 카드 띄우기 (#192) * refactor: 불필요한 코드 제거 (#192) - 레스토랑 카드 클릭시 맵 모달 이벤트 제거 - 마커 클릭시 맵 모달 이벤트 제거 * feat: 마커클릭 시 마커 위치에 따라 카드모달 위치 조정 (#192) * style: 레스토랑 오버레이 스타일 수정 (#192) * refactor: restaurantCard 컴포넌트를 용도에 따라 스타일 다르게 설정 (#192) * feat: 마커 클릭시 강조 효과 주기 (#192) * Squashed commit of the following: commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) commit 21128038e8dbd0953497950833f65dc918ffc40b Author: 황준승 <78203399+turtle601@users.noreply.github.com> Date: Thu Jul 27 16:45:06 2023 +0900 feat: 음식점 리스트 중복 필터링 기능 구현 (#186) * refactor: 지도 boundary 타입 추가 및 음식점 카테고리 타입 일부 수정 (#180) * feat: 셀럽 및 음식점 카테고리 별 필터링 기능 추가 (#180) * refactor: getQueryString 로직 분리 및 적용 (#180) * feat: CelebDropDown 및 CategoryNavbar에 전체 버튼 추가 (#184) * fix: 필터링 클릭 시 렌더링이 한 박자 늦게되는 오류 해결 (#184) * refactor: Map 컴포넌트에서 사용하지 않는 props 속성 제거 (#184) * refactor: celeb 전체를 나타내는 상태값을 -1로 변경 (#184) * feat: CelebDropDown blur 기능 추가 (#184) * fix: 불필요한 useEffect dependency 제거 (#184) * feat: Restaurant_Category에 전체 옵션 추가 (#184) * fix: CelebId 초기값 수정 (#184) * fix: API 명세서 수정에 따른 데이터 타입 변경 (#184) * refactor: NavButton props 프로퍼티 수정으로 인한 코드 수정 (#184) * refactor: css 선언방식을 삼항연산자를 && 로 변경 (#184) * fix: NavButton 불필요한 hover 기능 제거 (#184) Changed: hover 이벤트 && 연산자를 삼항연산자 사용으로 변경 * refactor: NavButton 컴포넌트를 NavItem 컴포넌트로 네이밍 수정 (#184) --------- Co-authored-by: d0dam Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> * feat: 다른 마커 클릭시 기존 마커 모달 닫기 기능 구현 (#192) * style: 파일명 오류 수정 (#192) * refactor: baseURL 환경변수 설정 및 type import 분리 * feat: RestaurantCard 컴포넌트 props 수정 (#192) onClick을 optional로 수정 * refactor: getQuadrant 리팩터링 (#192) * style: 상태 네이밍 수정 (#192) mainPosition -> currentCenter * feat: 음식점 카드 호버시 해당 음식점 마커 강조 (#192) * design: 강조시 애니메이션 효과 추가 및 음식점 리스트 스타일 수정 (#192) * refactor: 음식점 카드 호버시 마커 강조 로직 변경 (#192) * refactor: 프로필 이미지 컴포넌트 Props 타입 수정 (#192) size: number => string * fix: setHoverId가 없을 때 default value 설정 (#192) * fix: 충돌 해결 간 생긴 에러 및 lint 에러 수정 (#201) --------- Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> commit 60f870732951b14a824138828074370862fe40a8 Author: Jeremy <102432453+shackstack@users.noreply.github.com> Date: Mon Jul 31 14:41:46 2023 +0900 feat: 레스토랑 카드 및 마커 클릭 이벤트 변경 (#198) * feat: 마커 호버시 마커를 맨 앞으로 가져오기 (#192) * style: 마커 호버시 마커 강조 (#192) * feat: 마커 클릭시 레스토랑 카드 띄우기 (#192) * refactor: 불필요한 코드 제거 (#192) - 레스토랑 카드 클릭시 맵 모달 이벤트 제거 - 마커 클릭시 맵 모달 이벤트 제거 * feat: 마커클릭 시 마커 위치에 따라 카드모달 위치 조정 (#192) * style: 레스토랑 오버레이 스타일 수정 (#192) * refactor: restaurantCard 컴포넌트를 용도에 따라 스타일 다르게 설정 (#192) * feat: 마커 클릭시 강조 효과 주기 (#192) * Squashed commit of the following: commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) commit 21128038e8dbd0953497950833f65dc918ffc40b Author: 황준승 <78203399+turtle601@users.noreply.github.com> Date: Thu Jul 27 16:45:06 2023 +0900 feat: 음식점 리스트 중복 필터링 기능 구현 (#186) * refactor: 지도 boundary 타입 추가 및 음식점 카테고리 타입 일부 수정 (#180) * feat: 셀럽 및 음식점 카테고리 별 필터링 기능 추가 (#180) * refactor: getQueryString 로직 분리 및 적용 (#180) * feat: CelebDropDown 및 CategoryNavbar에 전체 버튼 추가 (#184) * fix: 필터링 클릭 시 렌더링이 한 박자 늦게되는 오류 해결 (#184) * refactor: Map 컴포넌트에서 사용하지 않는 props 속성 제거 (#184) * refactor: celeb 전체를 나타내는 상태값을 -1로 변경 (#184) * feat: CelebDropDown blur 기능 추가 (#184) * fix: 불필요한 useEffect dependency 제거 (#184) * feat: Restaurant_Category에 전체 옵션 추가 (#184) * fix: CelebId 초기값 수정 (#184) * fix: API 명세서 수정에 따른 데이터 타입 변경 (#184) * refactor: NavButton props 프로퍼티 수정으로 인한 코드 수정 (#184) * refactor: css 선언방식을 삼항연산자를 && 로 변경 (#184) * fix: NavButton 불필요한 hover 기능 제거 (#184) Changed: hover 이벤트 && 연산자를 삼항연산자 사용으로 변경 * refactor: NavButton 컴포넌트를 NavItem 컴포넌트로 네이밍 수정 (#184) --------- Co-authored-by: d0dam Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> * feat: 다른 마커 클릭시 기존 마커 모달 닫기 기능 구현 (#192) * style: 파일명 오류 수정 (#192) * refactor: baseURL 환경변수 설정 및 type import 분리 * feat: RestaurantCard 컴포넌트 props 수정 (#192) onClick을 optional로 수정 * refactor: getQuadrant 리팩터링 (#192) * style: 상태 네이밍 수정 (#192) mainPosition -> currentCenter * feat: 음식점 카드 호버시 해당 음식점 마커 강조 (#192) * design: 강조시 애니메이션 효과 추가 및 음식점 리스트 스타일 수정 (#192) * refactor: 음식점 카드 호버시 마커 강조 로직 변경 (#192) * refactor: 프로필 이미지 컴포넌트 Props 타입 수정 (#192) size: number => string * fix: setHoverId가 없을 때 default value 설정 (#192) commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) --- frontend/src/@types/api.types.ts | 9 + frontend/src/@types/image.type.ts | 5 + frontend/src/App.tsx | 2 + frontend/src/assets/all.png | Bin 0 -> 14819 bytes frontend/src/assets/icons/love.svg | 3 + .../assets/icons/restaurantCategory/all.svg | 10 ++ .../ImageCarousel/ImageCarousel.stories.tsx | 28 ++++ .../@common/ImageCarousel/ImageCarousel.tsx | 155 ++++++++++++++++++ .../@common/ImageCarousel/index.tsx | 3 + .../@common/LoadingDots/LoadingDots.tsx | 8 +- frontend/src/components/@common/Map/Map.tsx | 59 +++++-- .../@common/Map/Overlay/Overlay.tsx | 1 + .../components/@common/Map/OverlayMarker.tsx | 91 ++++++++-- .../ProfileImage/ProfileImage.stories.tsx | 2 +- .../@common/ProfileImage/ProfileImage.tsx | 11 +- .../ProfileImage/ProfileImageSkeleton.tsx | 21 +++ .../ProfileImageList.stories.tsx | 43 +++++ .../ProfileImageList/ProfileImageList.tsx | 49 ++++++ .../@common/ProfileImageList/index.tsx | 3 + .../WaterMarkImage/WaterMarkImage.stories.tsx | 18 ++ .../@common/WaterMarkImage/WaterMarkImage.tsx | 51 ++++++ .../@common/WaterMarkImage/index.tsx | 3 + .../CategoryNavbar/CategoryNavbar.tsx | 4 +- .../components/CelebBanner/CelebBanner.tsx | 2 +- .../CelebDropDown/CelebDropDown.tsx | 2 +- .../MapModalContent/MapModalContent.tsx | 6 +- .../RestaurantCard/RestaurantCard.stories.tsx | 2 +- .../RestaurantCard/RestaurantCard.tsx | 63 ++++--- .../RestaurantCard/RestaurantCardSkeleton.tsx | 92 +++++++++++ .../RestaurantCardList/RestaurantCardList.tsx | 56 +++++++ .../RestaurantCardListSkeleton.tsx | 46 ++++++ .../components/RestaurantCardList/index.tsx | 3 + .../components/VideoPreview/VideoPreview.tsx | 2 +- frontend/src/constants/celebs.ts | 5 +- frontend/src/constants/restaurantCategory.tsx | 3 +- frontend/src/hooks/useOnClickOuside.ts | 19 +++ frontend/src/hooks/useOnClickOutside.ts | 19 +++ frontend/src/pages/MainPage.tsx | 78 ++------- frontend/src/styles/base.ts | 3 +- frontend/src/styles/common.ts | 2 - frontend/src/utils/getQuadrant.ts | 18 ++ 41 files changed, 859 insertions(+), 141 deletions(-) create mode 100644 frontend/src/@types/image.type.ts create mode 100644 frontend/src/assets/all.png create mode 100644 frontend/src/assets/icons/love.svg create mode 100644 frontend/src/assets/icons/restaurantCategory/all.svg create mode 100644 frontend/src/components/@common/ImageCarousel/ImageCarousel.stories.tsx create mode 100644 frontend/src/components/@common/ImageCarousel/ImageCarousel.tsx create mode 100644 frontend/src/components/@common/ImageCarousel/index.tsx create mode 100644 frontend/src/components/@common/ProfileImage/ProfileImageSkeleton.tsx create mode 100644 frontend/src/components/@common/ProfileImageList/ProfileImageList.stories.tsx create mode 100644 frontend/src/components/@common/ProfileImageList/ProfileImageList.tsx create mode 100644 frontend/src/components/@common/ProfileImageList/index.tsx create mode 100644 frontend/src/components/@common/WaterMarkImage/WaterMarkImage.stories.tsx create mode 100644 frontend/src/components/@common/WaterMarkImage/WaterMarkImage.tsx create mode 100644 frontend/src/components/@common/WaterMarkImage/index.tsx create mode 100644 frontend/src/components/RestaurantCard/RestaurantCardSkeleton.tsx create mode 100644 frontend/src/components/RestaurantCardList/RestaurantCardList.tsx create mode 100644 frontend/src/components/RestaurantCardList/RestaurantCardListSkeleton.tsx create mode 100644 frontend/src/components/RestaurantCardList/index.tsx create mode 100644 frontend/src/hooks/useOnClickOuside.ts create mode 100644 frontend/src/hooks/useOnClickOutside.ts create mode 100644 frontend/src/utils/getQuadrant.ts diff --git a/frontend/src/@types/api.types.ts b/frontend/src/@types/api.types.ts index 92975dd42..e773b993a 100644 --- a/frontend/src/@types/api.types.ts +++ b/frontend/src/@types/api.types.ts @@ -1,3 +1,12 @@ +export interface RestaurantListData { + content: RestaurantData[]; + currentElementsCount: number; + currentPage: number; + pageSize: number; + totalElementsCount: number; + totalPage: number; +} + export interface RestaurantData { id: number; name: string; diff --git a/frontend/src/@types/image.type.ts b/frontend/src/@types/image.type.ts new file mode 100644 index 000000000..aadbc4cb2 --- /dev/null +++ b/frontend/src/@types/image.type.ts @@ -0,0 +1,5 @@ +import type { RestaurantData } from './api.types'; + +type RestaurantImages = RestaurantData['images']; + +export type RestaurantImage = RestaurantImages[number]; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d0311bdd..f5eb41aa3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,8 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import OauthRedirectPage from '~/pages/OauthRedirectPage'; import MainPage from '~/pages/MainPage'; +export const { BASE_URL } = process.env; + function App() { return ( diff --git a/frontend/src/assets/all.png b/frontend/src/assets/all.png new file mode 100644 index 0000000000000000000000000000000000000000..b608aef60b48c1ef6923976544f05de02306b328 GIT binary patch literal 14819 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 B`WgTL literal 0 HcmV?d00001 diff --git a/frontend/src/assets/icons/love.svg b/frontend/src/assets/icons/love.svg new file mode 100644 index 000000000..75a0db3be --- /dev/null +++ b/frontend/src/assets/icons/love.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/restaurantCategory/all.svg b/frontend/src/assets/icons/restaurantCategory/all.svg new file mode 100644 index 000000000..f066ec43f --- /dev/null +++ b/frontend/src/assets/icons/restaurantCategory/all.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/@common/ImageCarousel/ImageCarousel.stories.tsx b/frontend/src/components/@common/ImageCarousel/ImageCarousel.stories.tsx new file mode 100644 index 000000000..0c905dd92 --- /dev/null +++ b/frontend/src/components/@common/ImageCarousel/ImageCarousel.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ImageCarousel from './ImageCarousel'; + +const meta: Meta = { + title: 'ImageCarousel', + component: ImageCarousel, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + images: [ + { id: 1, name: 'https://picsum.photos/315/300', author: '@d0dam', sns: 'youtube' }, + { id: 2, name: 'https://picsum.photos/315/300', author: '@d0dam', sns: 'youtube' }, + { id: 3, name: 'https://picsum.photos/315/300', author: '@d0dam', sns: 'youtube' }, + { id: 4, name: 'https://picsum.photos/315/300', author: '@d0dam', sns: 'youtube' }, + ], + }, +}; + +export const OneImage: Story = { + args: { + images: [{ id: 1, name: 'https://picsum.photos/315/300', author: '@d0dam', sns: 'youtube' }], + }, +}; diff --git a/frontend/src/components/@common/ImageCarousel/ImageCarousel.tsx b/frontend/src/components/@common/ImageCarousel/ImageCarousel.tsx new file mode 100644 index 000000000..18354d279 --- /dev/null +++ b/frontend/src/components/@common/ImageCarousel/ImageCarousel.tsx @@ -0,0 +1,155 @@ +import { useState } from 'react'; +import styled, { css } from 'styled-components'; +import { RestaurantImage } from '~/@types/image.type'; +import LeftBracket from '~/assets/icons/left-bracket.svg'; +import RightBracket from '~/assets/icons/right-bracket.svg'; +import { BORDER_RADIUS } from '~/styles/common'; +import WaterMarkImage from '../WaterMarkImage'; + +interface ImageCarouselProps { + images: RestaurantImage[]; + type: 'list' | 'map'; +} + +function ImageCarousel({ images, type }: ImageCarouselProps) { + const [currentIndex, setCurrentIndex] = useState(0); + + const goToPrevious = () => { + setCurrentIndex(prevIndex => prevIndex - 1); + }; + + const goToNext = () => { + setCurrentIndex(prevIndex => prevIndex + 1); + }; + + return ( + + + {images.map(({ id, name, author }) => ( + + ))} + + {currentIndex !== 0 && ( + + + + )} + {currentIndex !== images.length - 1 && ( + + + + )} + {images.length > 1 && ( + + {Array.from({ length: images.length }, () => ( + + ))} + + )} + + ); +} + +export default ImageCarousel; + +const StyledCarouselContainer = styled.div<{ type: 'list' | 'map' }>` + position: relative; + + width: 100%; + overflow: hidden; + + border-radius: ${({ type }) => + type === 'list' ? `${BORDER_RADIUS.md}` : `${BORDER_RADIUS.md} ${BORDER_RADIUS.md} 0 0`}; + + button { + visibility: hidden; + + display: flex; + justify-content: center; + align-items: center; + + position: absolute; + top: 50%; + + width: 32px; + height: 32px; + + border: none; + border-radius: 50%; + background-color: var(--white); + + cursor: pointer; + opacity: 0; + transition: transform 0.15s ease-in-out, opacity 0.2s ease-in-out; + transform: translateY(-50%); + box-shadow: var(--shadow); + outline: none; + + &:hover { + transform: translateY(-50%) scale(1.04); + } + } + + &:hover { + button { + visibility: visible; + + opacity: 0.85; + + &:hover { + opacity: 1; + } + } + } +`; + +const StyledLeftButton = styled.button` + left: 12px; +`; + +const StyledRightButton = styled.button` + right: 12px; +`; + +const StyledCarouselSlide = styled.div<{ currentIndex: number }>` + display: flex; + + width: 100%; + + transition: transform 0.3s ease-in-out; + transform: ${({ currentIndex }) => `translateX(-${currentIndex * 100}%)`}; + flex-wrap: nowrap; + + aspect-ratio: 1.05 / 1; +`; + +const StyledDots = styled.div<{ currentIndex: number }>` + display: flex; + justify-content: center; + align-items: center; + gap: 0 0.5rem; + + position: absolute; + bottom: 12px; + + width: 100%; + + ${({ currentIndex }) => css` + & > span:nth-child(${currentIndex + 1}) { + opacity: 1; + transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; + transform: scale(1.1); + } + `} +`; + +const StyledDot = styled.span` + width: 6px; + height: 6px; + + border-radius: 50%; + background-color: var(--white); + + opacity: 0.2; + transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; +`; diff --git a/frontend/src/components/@common/ImageCarousel/index.tsx b/frontend/src/components/@common/ImageCarousel/index.tsx new file mode 100644 index 000000000..d557e0a41 --- /dev/null +++ b/frontend/src/components/@common/ImageCarousel/index.tsx @@ -0,0 +1,3 @@ +import ImageCarousel from './ImageCarousel'; + +export default ImageCarousel; diff --git a/frontend/src/components/@common/LoadingDots/LoadingDots.tsx b/frontend/src/components/@common/LoadingDots/LoadingDots.tsx index 24f0d5b9a..387c68396 100644 --- a/frontend/src/components/@common/LoadingDots/LoadingDots.tsx +++ b/frontend/src/components/@common/LoadingDots/LoadingDots.tsx @@ -14,7 +14,7 @@ export default LoadingDots; const StyledLoadingDots = styled.div` display: flex; - gap: 0 0.6rem; + gap: 0 1.4rem; & > div:nth-child(2) { animation-delay: 0.14s; @@ -30,13 +30,13 @@ const pulseAnimation = keyframes` transform: scale(0); } 90%, 100% { - transform: scale(1); + transform: scale(10); } `; const StyledLoadingDot = styled.div` - width: 12px; - height: 12px; + width: 1.2px; + height: 1.2px; border-radius: 50%; background-color: var(--black); diff --git a/frontend/src/components/@common/Map/Map.tsx b/frontend/src/components/@common/Map/Map.tsx index ba1537440..82137b652 100644 --- a/frontend/src/components/@common/Map/Map.tsx +++ b/frontend/src/components/@common/Map/Map.tsx @@ -1,9 +1,6 @@ import { useState } from 'react'; import { Wrapper, Status } from '@googlemaps/react-wrapper'; import { styled } from 'styled-components'; -import OverlayMarker from './OverlayMarker'; -import type { Coordinate, CoordinateBoundary } from '~/@types/map.types'; -import type { Celeb } from '~/@types/celeb.types'; import MapContent from './MapContent'; import OverlayMyLocation from './OverlayMyLocation'; import LoadingDots from '../LoadingDots'; @@ -13,27 +10,50 @@ import LeftBracket from '~/assets/icons/left-bracket.svg'; import RightBracket from '~/assets/icons/right-bracket.svg'; import Minus from '~/assets/icons/minus.svg'; import Plus from '~/assets/icons/plus.svg'; +import getQuadrant from '~/utils/getQuadrant'; +import OverlayMarker from './OverlayMarker'; + +import type { Coordinate, CoordinateBoundary } from '~/@types/map.types'; +import type { RestaurantData } from '~/@types/api.types'; interface MapProps { - clickMarker: ({ lat, lng }: Coordinate) => void; - markers: { position: Coordinate; celebs: Celeb[] }[]; + data: RestaurantData[]; + hoveredId: number | null; setBoundary: React.Dispatch>; toggleMapExpand: () => void; + loadingData: boolean; } const render = (status: Status) => { if (status === Status.FAILURE) return
지도를 불러올 수 없습니다. 페이지를 새로고침 하거나 네트워크 연결을 다시 한 번 확인해주세요.
; - return ; + return ( + + + + ); }; -function Map({ clickMarker, markers, setBoundary, toggleMapExpand }: MapProps) { +const StyledMapLoadingContainer = styled.section` + display: flex; + justify-content: center; + align-items: center; + + height: 100%; + + background-color: var(--gray-2); +`; + +const JamsilCampus = { lat: 37.515271, lng: 127.1029949 }; + +function Map({ data, setBoundary, toggleMapExpand, loadingData, hoveredId }: MapProps) { const [center, setCenter] = useState({ lat: 37.5057482, lng: 127.050727 }); const [clicks, setClicks] = useState([]); const [zoom, setZoom] = useState(16); const [myPosition, setMyPosition] = useState(null); const [isMapExpanded, setIsMapExpanded] = useState(false); const [loading, setLoading] = useState(false); + const [currentCenter, setCurrentCenter] = useState(JamsilCampus); const onClick = (e: google.maps.MapMouseEvent) => { setClicks([...clicks, e.latLng!]); @@ -41,6 +61,7 @@ function Map({ clickMarker, markers, setBoundary, toggleMapExpand }: MapProps) { const onIdle = (m: google.maps.Map) => { setZoom(m.getZoom()!); + setCurrentCenter({ lat: m.getCenter().lat(), lng: m.getCenter().lng() }); const lowLatitude = String(m.getBounds().getSouthWest().lat()); const highLatitude = String(m.getBounds().getNorthEast().lat()); @@ -60,11 +81,6 @@ function Map({ clickMarker, markers, setBoundary, toggleMapExpand }: MapProps) { }); }; - const clickOverlayMarker = (position: Coordinate) => { - clickMarker(position); - setCenter(position); - }; - const clickZoom = (number: number): React.MouseEventHandler => () => { @@ -85,11 +101,19 @@ function Map({ clickMarker, markers, setBoundary, toggleMapExpand }: MapProps) { zoom={zoom} center={center} > - {markers.map(({ position, celebs }) => ( - - ))} + {data?.map(({ celebs, ...restaurant }) => { + const { lat, lng } = restaurant; + return ( + + ); + })} {myPosition && } - {loading && ( + {(loadingData || loading) && ( @@ -123,8 +147,7 @@ const LoadingUI = styled.div` right: calc(50% - 41px); width: 82px; - - padding: 1.6rem 2.4rem; + height: 40px; `; const StyledMyPositionButtonUI = styled.button` diff --git a/frontend/src/components/@common/Map/Overlay/Overlay.tsx b/frontend/src/components/@common/Map/Overlay/Overlay.tsx index bfd0581ce..5b2bbd3ac 100644 --- a/frontend/src/components/@common/Map/Overlay/Overlay.tsx +++ b/frontend/src/components/@common/Map/Overlay/Overlay.tsx @@ -14,6 +14,7 @@ function Overlay({ position, pane = 'floatPane', map, zIndex, children }: Overla const container = useMemo(() => { const div = document.createElement('div'); div.style.position = 'absolute'; + return div; }, []); diff --git a/frontend/src/components/@common/Map/OverlayMarker.tsx b/frontend/src/components/@common/Map/OverlayMarker.tsx index a3fa57362..414ffdd7c 100644 --- a/frontend/src/components/@common/Map/OverlayMarker.tsx +++ b/frontend/src/components/@common/Map/OverlayMarker.tsx @@ -1,37 +1,102 @@ -import styled from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; +import { useRef, useState } from 'react'; import ProfileImage from '../ProfileImage'; import Overlay from './Overlay/Overlay'; +import RestaurantCard from '~/components/RestaurantCard'; +import useOnClickOutside from '~/hooks/useOnClickOutside'; + +import type { Quadrant } from '~/utils/getQuadrant'; +import type { Restaurant } from '~/@types/restaurant.types'; import type { Celeb } from '~/@types/celeb.types'; -import type { Coordinate } from '~/@types/map.types'; interface OverlayMarkerProps { celeb: Celeb; - position: Coordinate; - onClick: ({ lat, lng }: Coordinate) => void; map?: google.maps.Map; + restaurant: Restaurant; + quadrant: Quadrant; + isRestaurantHovered: boolean; } -function OverlayMarker({ celeb, position, map, onClick }: OverlayMarkerProps) { +function OverlayMarker({ celeb, restaurant, map, quadrant, isRestaurantHovered }: OverlayMarkerProps) { + const { lat, lng } = restaurant; + const [isClicked, setIsClicked] = useState(false); + const ref = useRef(); + useOnClickOutside(ref, () => setIsClicked(false)); + + const clickMarker = () => setIsClicked(true); + return ( map && ( - - onClick(position)}> - + + + + {isClicked && ( + + + + )} ) ); } -const StyledMarker = styled.button` - border: none; - background-color: transparent; +const scaleUp = keyframes` + 0% { + transform: scale(1); + } + 100% { + transform: scale(1.5); + } +`; + +const StyledMarker = styled.div<{ isClicked: boolean; isRestaurantHovered: boolean }>` + display: flex; + justify-content: center; + align-items: center; + + width: 36px; + height: 36px; - transition: all 0.2s ease-in-out; + border: ${({ isClicked, isRestaurantHovered }) => + isClicked || isRestaurantHovered ? '3px solid var(--orange-2)' : '3px solid transparent'}; + border-radius: 50%; + + transition: transform 0.2s ease-in-out; + transform: ${({ isClicked }) => (isClicked ? 'scale(1.5)' : 'scale(1)')}; &:hover { - transform: scale(1.1); + transform: scale(1.5); } + + ${({ isRestaurantHovered }) => + isRestaurantHovered && + css` + animation: ${scaleUp} 0.2s ease-in-out forwards; + `} +`; + +const fadeInAnimation = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const StyledModal = styled.div<{ quadrant: Quadrant }>` + position: absolute; + top: ${({ quadrant }) => (quadrant === 1 || quadrant === 2 ? '40px' : '-280px')}; + right: ${({ quadrant }) => (quadrant === 1 || quadrant === 4 ? '45px' : '-210px')}; + + width: 200px; + + border-radius: 12px; + background-color: #fff; + + animation: ${fadeInAnimation} 100ms ease-in; + box-shadow: 0 4px 6px rgb(0 0 0 / 20%); `; export default OverlayMarker; diff --git a/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx b/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx index e5dbbe3a9..58f63f8a1 100644 --- a/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx +++ b/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx @@ -14,6 +14,6 @@ export const Default: Story = { args: { name: '누군가', imageUrl: 'https://avatars.githubusercontent.com/u/51052049?v=4', - size: 64, + size: '64px', }, }; diff --git a/frontend/src/components/@common/ProfileImage/ProfileImage.tsx b/frontend/src/components/@common/ProfileImage/ProfileImage.tsx index 92d95ef41..e2efe8345 100644 --- a/frontend/src/components/@common/ProfileImage/ProfileImage.tsx +++ b/frontend/src/components/@common/ProfileImage/ProfileImage.tsx @@ -4,7 +4,7 @@ interface ProfileImageProps extends React.HTMLAttributes { name: string; imageUrl: string; border?: boolean; - size: number; + size?: string; } function ProfileImage({ name = '셀럽', imageUrl, size, border = false, ...props }: ProfileImageProps) { @@ -13,11 +13,10 @@ function ProfileImage({ name = '셀럽', imageUrl, size, border = false, ...prop export default ProfileImage; -const StyledProfile = styled.img<{ size: number; border: boolean }>` - width: ${({ size }) => `${size}px`}; - height: ${({ size }) => `${size}px`}; +const StyledProfile = styled.img<{ size: string; border: boolean }>` + width: ${({ size }) => size || 'auto'}; + height: ${({ size }) => size || 'auto'}; - border: ${({ border }) => (border ? `2px solid var(--primary-1)` : `none`)}; border-radius: 50%; - background: var(--red-5); + background: none; `; diff --git a/frontend/src/components/@common/ProfileImage/ProfileImageSkeleton.tsx b/frontend/src/components/@common/ProfileImage/ProfileImageSkeleton.tsx new file mode 100644 index 000000000..9ca92eab1 --- /dev/null +++ b/frontend/src/components/@common/ProfileImage/ProfileImageSkeleton.tsx @@ -0,0 +1,21 @@ +import { styled } from 'styled-components'; +import { paintSkeleton } from '~/styles/common'; + +interface ProfileImageSkeletonProps { + size: number; +} + +function ProfileImageSkeleton({ size }: ProfileImageSkeletonProps) { + return ; +} + +export default ProfileImageSkeleton; + +const StyledProfileImageSkeleton = styled.div<{ size: number }>` + ${paintSkeleton} + width: ${({ size }) => (size ? `${size}px` : '100%')}; + height: ${({ size }) => (size ? `${size}px` : 'auto')}; + + border-radius: 50%; + background: none; +`; diff --git a/frontend/src/components/@common/ProfileImageList/ProfileImageList.stories.tsx b/frontend/src/components/@common/ProfileImageList/ProfileImageList.stories.tsx new file mode 100644 index 000000000..2d4992598 --- /dev/null +++ b/frontend/src/components/@common/ProfileImageList/ProfileImageList.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import ProfileImageList from './ProfileImageList'; + +const meta: Meta = { + title: 'ProfileImageList', + component: ProfileImageList, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + celebs: [ + { + name: '누군가', + profileImageUrl: 'https://avatars.githubusercontent.com/u/51052049?v=4', + id: 1, + youtubeChannelName: '@d0dam', + }, + { + name: '누군가', + profileImageUrl: 'https://avatars.githubusercontent.com/u/51052049?v=4', + id: 2, + youtubeChannelName: '@d0dam', + }, + { + name: '누군가', + profileImageUrl: 'https://avatars.githubusercontent.com/u/51052049?v=4', + id: 3, + youtubeChannelName: '@d0dam', + }, + { + name: '누군가', + profileImageUrl: 'https://avatars.githubusercontent.com/u/51052049?v=4', + id: 4, + youtubeChannelName: '@d0dam', + }, + ], + size: '42px', + }, +}; diff --git a/frontend/src/components/@common/ProfileImageList/ProfileImageList.tsx b/frontend/src/components/@common/ProfileImageList/ProfileImageList.tsx new file mode 100644 index 000000000..1ee2c0ec1 --- /dev/null +++ b/frontend/src/components/@common/ProfileImageList/ProfileImageList.tsx @@ -0,0 +1,49 @@ +import { styled } from 'styled-components'; +import ProfileImage from '../ProfileImage/ProfileImage'; +import useBooleanState from '~/hooks/useBooleanState'; + +import type { Celeb } from '~/@types/celeb.types'; + +interface ProfileImageListProps { + celebs: Celeb[]; + size: string; +} + +function ProfileImageList({ celebs, size }: ProfileImageListProps) { + const { value: hover, setTrue, setFalse } = useBooleanState(false); + + return ( + + {celebs.map((celeb, index) => ( + + + + ))} + + ); +} + +export default ProfileImageList; + +const StyledProfileImageList = styled.div<{ size: string }>` + position: relative; + + width: ${({ size }) => `${size}`}; + height: ${({ size }) => `${size}`}; +`; + +const StyledProfileImageWrapper = styled.div<{ index: number; hover: boolean }>` + position: absolute; + z-index: ${({ index }) => 100 - index}; + + transition: 0.4s ease-in-out; + + ${({ hover, index }) => + hover + ? ` + transform: translateX(${index * -110}%); + ` + : ` + transform: translateX(${index * -20}%); + `}; +`; diff --git a/frontend/src/components/@common/ProfileImageList/index.tsx b/frontend/src/components/@common/ProfileImageList/index.tsx new file mode 100644 index 000000000..11aaa2980 --- /dev/null +++ b/frontend/src/components/@common/ProfileImageList/index.tsx @@ -0,0 +1,3 @@ +import ProfileImageList from './ProfileImageList'; + +export default ProfileImageList; diff --git a/frontend/src/components/@common/WaterMarkImage/WaterMarkImage.stories.tsx b/frontend/src/components/@common/WaterMarkImage/WaterMarkImage.stories.tsx new file mode 100644 index 000000000..3026f3220 --- /dev/null +++ b/frontend/src/components/@common/WaterMarkImage/WaterMarkImage.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import WaterMarkImage from './WaterMarkImage'; + +const meta: Meta = { + title: 'WaterMarkImage', + component: WaterMarkImage, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + waterMark: '@d0dam', + imageUrl: 'https://picsum.photos/315/300', + }, +}; diff --git a/frontend/src/components/@common/WaterMarkImage/WaterMarkImage.tsx b/frontend/src/components/@common/WaterMarkImage/WaterMarkImage.tsx new file mode 100644 index 000000000..2a2fa9b89 --- /dev/null +++ b/frontend/src/components/@common/WaterMarkImage/WaterMarkImage.tsx @@ -0,0 +1,51 @@ +import styled from 'styled-components'; +import { BORDER_RADIUS, FONT_SIZE, paintSkeleton } from '~/styles/common'; + +interface WaterMarkImageProps { + waterMark: string; + imageUrl: string; +} + +function WaterMarkImage({ waterMark, imageUrl }: WaterMarkImageProps) { + return ( + + + {waterMark} + + ); +} + +export default WaterMarkImage; + +const StyledWaterMarkImage = styled.div` + position: relative; + + width: 100%; + aspect-ratio: 1.05 / 1; + + height: auto; +`; + +const StyledImage = styled.img` + ${paintSkeleton} + display: block; + + aspect-ratio: 1.05 / 1; + object-fit: cover; + + width: 100%; +`; + +const StyledWaterMark = styled.div` + position: absolute; + top: 12px; + left: 12px; + + padding: 0.4rem 0.8rem; + + border-radius: ${BORDER_RADIUS.xs}; + background-color: var(--white); + + color: var(--black); + font-size: ${FONT_SIZE.sm}; +`; diff --git a/frontend/src/components/@common/WaterMarkImage/index.tsx b/frontend/src/components/@common/WaterMarkImage/index.tsx new file mode 100644 index 000000000..b45cee9d3 --- /dev/null +++ b/frontend/src/components/@common/WaterMarkImage/index.tsx @@ -0,0 +1,3 @@ +import WaterMarkImage from './WaterMarkImage'; + +export default WaterMarkImage; diff --git a/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx b/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx index f326bcb71..53ecd90ed 100644 --- a/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx +++ b/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import styled from 'styled-components'; -import { RestaurantCategory } from '~/@types/restaurant.types'; import NavItem from '~/components/@common/NavButton/NavButton'; - import isEqual from '~/utils/compare'; +import type { RestaurantCategory } from '~/@types/restaurant.types'; + interface Category { label: RestaurantCategory; icon: React.ReactNode; diff --git a/frontend/src/components/CelebBanner/CelebBanner.tsx b/frontend/src/components/CelebBanner/CelebBanner.tsx index 077b14997..9a78383cf 100644 --- a/frontend/src/components/CelebBanner/CelebBanner.tsx +++ b/frontend/src/components/CelebBanner/CelebBanner.tsx @@ -27,7 +27,7 @@ function CelebBanner({ return ( - + {name} {youtubeChannelName} 구독자 {subscriberCount / 10_000}만명 ∙ 음식점 {restaurantCount}개 diff --git a/frontend/src/components/CelebDropDown/CelebDropDown.tsx b/frontend/src/components/CelebDropDown/CelebDropDown.tsx index a560a35d6..54201f29e 100644 --- a/frontend/src/components/CelebDropDown/CelebDropDown.tsx +++ b/frontend/src/components/CelebDropDown/CelebDropDown.tsx @@ -40,7 +40,7 @@ function CelebDropDown({ celebs, externalOnClick, isOpen = false }: DropDownProp {celebs.map(({ id, name, profileImageUrl }) => (
- + {name}
{isEqual(selected, name) && } diff --git a/frontend/src/components/MapModalContent/MapModalContent.tsx b/frontend/src/components/MapModalContent/MapModalContent.tsx index b85084d36..9350e71f9 100644 --- a/frontend/src/components/MapModalContent/MapModalContent.tsx +++ b/frontend/src/components/MapModalContent/MapModalContent.tsx @@ -2,6 +2,7 @@ import { styled } from 'styled-components'; import { RestaurantModalInfo } from '~/@types/restaurant.types'; import { BORDER_RADIUS, FONT_SIZE } from '~/styles/common'; import TextButton from '../@common/Button'; +import { BASE_URL } from '~/App'; interface MapModalContentProps { content: RestaurantModalInfo; @@ -17,10 +18,7 @@ function MapModalContent({ content }: MapModalContentProps) {
{roadAddress}
{phoneNumber}
- + >; } -function RestaurantCard({ restaurant, celebs, size, onClick }: RestaurantCardProps) { - const { images, name, roadAddress, category } = restaurant; +function RestaurantCard({ + restaurant, + celebs, + size, + type = 'list', + onClick = () => {}, + setHoveredId = () => {}, +}: RestaurantCardProps) { + const { images, name, roadAddress, category, phoneNumber } = restaurant; + + const onMouseEnter = () => { + setHoveredId(restaurant.id); + }; + + const onMouseLeave = () => { + setHoveredId(null); + }; return ( - - + + + + +
{category} {name} {roadAddress} - 02-1234-5678 + {phoneNumber} - + {celebs && }
@@ -51,18 +74,18 @@ const StyledContainer = styled.div` cursor: pointer; `; -const StyledImage = styled.img` - width: 100%; - aspect-ratio: 1.05 / 1; - - border-radius: ${BORDER_RADIUS.md}; +const StyledImageViewer = styled.div` + position: relative; - object-fit: cover; + & > svg { + position: absolute; + top: 12px; + right: 12px; + } `; const StyledInfo = styled.div` display: flex; - flex: 1; flex-direction: column; gap: 0.4rem; diff --git a/frontend/src/components/RestaurantCard/RestaurantCardSkeleton.tsx b/frontend/src/components/RestaurantCard/RestaurantCardSkeleton.tsx new file mode 100644 index 000000000..92ff4b945 --- /dev/null +++ b/frontend/src/components/RestaurantCard/RestaurantCardSkeleton.tsx @@ -0,0 +1,92 @@ +import { styled } from 'styled-components'; +import ProfileImageSkeleton from '../@common/ProfileImage/ProfileImageSkeleton'; +import { BORDER_RADIUS, paintSkeleton } from '~/styles/common'; + +function RestaurantCardSkeleton() { + return ( + + +
+ + + + + + + + + +
+
+ ); +} + +export default RestaurantCardSkeleton; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: start; + gap: 0.8rem; + + width: 100%; + height: 100%; + + & > section { + display: flex; + justify-content: space-between; + } + + cursor: pointer; +`; + +const StyledImage = styled.div` + ${paintSkeleton} + width: 100%; + aspect-ratio: 1.05 / 1; + + object-fit: cover; + + border-radius: ${BORDER_RADIUS.md}; +`; + +const StyledInfo = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: 0.4rem; + + position: relative; + + width: 100%; + + padding: 0.4rem; +`; + +const StyledName = styled.h5` + ${paintSkeleton} + width: 100%; + height: 20px; + + border-radius: ${BORDER_RADIUS.xs}; +`; + +const StyledAddress = styled.span` + ${paintSkeleton} + width: 50%; + height: 12px; + + border-radius: ${BORDER_RADIUS.xs}; +`; + +const StyledCategory = styled.span` + ${paintSkeleton} + width: 40%; + height: 12px; + + border-radius: ${BORDER_RADIUS.xs}; +`; + +const StyledProfileImageSection = styled.div` + align-self: flex-end; +`; diff --git a/frontend/src/components/RestaurantCardList/RestaurantCardList.tsx b/frontend/src/components/RestaurantCardList/RestaurantCardList.tsx new file mode 100644 index 000000000..3aa388b47 --- /dev/null +++ b/frontend/src/components/RestaurantCardList/RestaurantCardList.tsx @@ -0,0 +1,56 @@ +import { styled } from 'styled-components'; +import { useEffect, useState } from 'react'; +import RestaurantCard from '../RestaurantCard'; +import { FONT_SIZE } from '~/styles/common'; +import RestaurantCardListSkeleton from './RestaurantCardListSkeleton'; + +import type { RestaurantData, RestaurantListData } from '~/@types/api.types'; + +interface RestaurantCardListProps { + restaurantDataList: RestaurantListData | null; + loading: boolean; + setHoveredId: React.Dispatch>; +} + +function RestaurantCardList({ restaurantDataList, loading, setHoveredId }: RestaurantCardListProps) { + const [prevCardNumber, setPrevCardNumber] = useState(18); + + useEffect(() => { + if (restaurantDataList) setPrevCardNumber(restaurantDataList.currentElementsCount); + }, [restaurantDataList?.currentElementsCount]); + + if (!restaurantDataList || loading) return ; + + return ( +
+ 음식점 수 {restaurantDataList.totalElementsCount} 개 + + {restaurantDataList.content?.map(({ celebs, ...restaurant }: RestaurantData) => ( + + ))} + +
+ ); +} + +export default RestaurantCardList; + +const StyledCardListHeader = styled.p` + margin: 3.2rem 2.4rem; + + font-size: ${FONT_SIZE.md}; +`; + +const StyledRestaurantCardList = styled.div` + display: grid; + gap: 4rem 2.4rem; + + height: 100%; + + margin: 0 2.4rem; + grid-template-columns: 1fr 1fr 1fr; + + @media screen and (width <= 1240px) { + grid-template-columns: 1fr 1fr; + } +`; diff --git a/frontend/src/components/RestaurantCardList/RestaurantCardListSkeleton.tsx b/frontend/src/components/RestaurantCardList/RestaurantCardListSkeleton.tsx new file mode 100644 index 000000000..93e07ed21 --- /dev/null +++ b/frontend/src/components/RestaurantCardList/RestaurantCardListSkeleton.tsx @@ -0,0 +1,46 @@ +import { styled } from 'styled-components'; +import RestaurantCardSkeleton from '../RestaurantCard/RestaurantCardSkeleton'; +import { BORDER_RADIUS, paintSkeleton } from '~/styles/common'; + +interface RestaurantCardListSkeletonProps { + cardNumber: number; +} + +function RestaurantCardListSkeleton({ cardNumber }: RestaurantCardListSkeletonProps) { + return ( +
+ + + {Array.from({ length: cardNumber }, () => ( + + ))} + +
+ ); +} + +export default RestaurantCardListSkeleton; + +const StyledCardListHeader = styled.p` + ${paintSkeleton} + width: 35%; + height: 16px; + + margin: 3.2rem 2.4rem; + + border-radius: ${BORDER_RADIUS.xs}; +`; + +const StyledRestaurantCardList = styled.div` + display: grid; + gap: 4rem 2.4rem; + + height: 100%; + + margin: 0 2.4rem; + grid-template-columns: 1fr 1fr 1fr; + + @media screen and (width <= 1240px) { + grid-template-columns: 1fr 1fr; + } +`; diff --git a/frontend/src/components/RestaurantCardList/index.tsx b/frontend/src/components/RestaurantCardList/index.tsx new file mode 100644 index 000000000..0f31b4b56 --- /dev/null +++ b/frontend/src/components/RestaurantCardList/index.tsx @@ -0,0 +1,3 @@ +import RestaurantCardList from './RestaurantCardList'; + +export default RestaurantCardList; diff --git a/frontend/src/components/VideoPreview/VideoPreview.tsx b/frontend/src/components/VideoPreview/VideoPreview.tsx index 78b42ca14..4b3c261ad 100644 --- a/frontend/src/components/VideoPreview/VideoPreview.tsx +++ b/frontend/src/components/VideoPreview/VideoPreview.tsx @@ -42,7 +42,7 @@ function VideoPreview({ )} - + {title}
{celebName}
diff --git a/frontend/src/constants/celebs.ts b/frontend/src/constants/celebs.ts index 10da6bf1e..65aba2e6a 100644 --- a/frontend/src/constants/celebs.ts +++ b/frontend/src/constants/celebs.ts @@ -1,3 +1,5 @@ +import All from '~/assets/all.png'; + export const CELEBS = [ { id: 1, @@ -48,8 +50,7 @@ export const CELEBS_OPTIONS = [ id: -1, name: '전체', youtubeChannelName: '@all', - profileImageUrl: - 'https://yt3.googleusercontent.com/ytc/AOPolaQnS9nKQFCtkBihMKt1Jhm-nzkUFY3Z6RQpwnbf=s176-c-k-c0x00ffffff-no-rj', + profileImageUrl: All, }, ...CELEBS, ]; diff --git a/frontend/src/constants/restaurantCategory.tsx b/frontend/src/constants/restaurantCategory.tsx index 504ee16a9..9868f508c 100644 --- a/frontend/src/constants/restaurantCategory.tsx +++ b/frontend/src/constants/restaurantCategory.tsx @@ -8,6 +8,7 @@ import Pub from '~/assets/icons/restaurantCategory/pub.svg'; import Sashimi from '~/assets/icons/restaurantCategory/sashimi.svg'; import Sushi from '~/assets/icons/restaurantCategory/sushi.svg'; import Wine from '~/assets/icons/restaurantCategory/wine.svg'; +import All from '~/assets/icons/restaurantCategory/all.svg'; import type { RestaurantCategory } from '~/@types/restaurant.types'; interface Category { @@ -18,7 +19,7 @@ interface Category { const RESTAURANT_CATEGORY: Category[] = [ { label: '전체', - icon: , + icon: , }, { label: '일식당', diff --git a/frontend/src/hooks/useOnClickOuside.ts b/frontend/src/hooks/useOnClickOuside.ts new file mode 100644 index 000000000..f95470cc2 --- /dev/null +++ b/frontend/src/hooks/useOnClickOuside.ts @@ -0,0 +1,19 @@ +import { useEffect, RefObject } from 'react'; + +export default function useOnClickOutside( + ref: RefObject, + handler: (event?: Event | MouseEvent) => void, +) { + useEffect(() => { + function onClickHandler(event: Event | MouseEvent) { + if (!ref?.current || ref?.current.contains(event?.target as Node)) { + return; + } + handler(event); + } + window.addEventListener('click', onClickHandler); + return () => { + window.removeEventListener('click', onClickHandler); + }; + }, [ref, handler]); +} diff --git a/frontend/src/hooks/useOnClickOutside.ts b/frontend/src/hooks/useOnClickOutside.ts new file mode 100644 index 000000000..f95470cc2 --- /dev/null +++ b/frontend/src/hooks/useOnClickOutside.ts @@ -0,0 +1,19 @@ +import { useEffect, RefObject } from 'react'; + +export default function useOnClickOutside( + ref: RefObject, + handler: (event?: Event | MouseEvent) => void, +) { + useEffect(() => { + function onClickHandler(event: Event | MouseEvent) { + if (!ref?.current || ref?.current.contains(event?.target as Node)) { + return; + } + handler(event); + } + window.addEventListener('click', onClickHandler); + return () => { + window.removeEventListener('click', onClickHandler); + }; + }, [ref, handler]); +} diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index 556c0bf8c..8d3a0823e 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -5,55 +5,39 @@ import Header from '~/components/@common/Header'; import Map from '~/components/@common/Map'; import CategoryNavbar from '~/components/CategoryNavbar'; import CelebDropDown from '~/components/CelebDropDown/CelebDropDown'; -import MapModal from '~/components/MapModal/MapModal'; -import RestaurantCard from '~/components/RestaurantCard'; import RESTAURANT_CATEGORY from '~/constants/restaurantCategory'; import { CELEBS_OPTIONS } from '~/constants/celebs'; import useFetch from '~/hooks/useFetch'; -import useMapModal from '~/hooks/useMapModal'; import getQueryString from '~/utils/getQueryString'; -import { FONT_SIZE } from '~/styles/common'; +import RestaurantCardList from '~/components/RestaurantCardList'; + import type { Celeb } from '~/@types/celeb.types'; -import type { RestaurantData } from '~/@types/api.types'; -import type { Coordinate, CoordinateBoundary } from '~/@types/map.types'; -import type { Restaurant, RestaurantCategory, RestaurantModalInfo } from '~/@types/restaurant.types'; +import type { RestaurantListData } from '~/@types/api.types'; +import type { CoordinateBoundary } from '~/@types/map.types'; +import type { RestaurantCategory } from '~/@types/restaurant.types'; function MainPage() { - const [currentRestaurant, setCurrentRestaurant] = useState(null); - const { modalOpen, isVisible, closeModal, openModal } = useMapModal(true); const [isMapExpanded, setIsMapExpanded] = useState(false); - const [data, setData] = useState([]); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); const [boundary, setBoundary] = useState(); const [celebId, setCelebId] = useState(-1); const [restaurantCategory, setRestaurantCategory] = useState('전체'); + const [hoveredId, setHoveredId] = useState(null); const { handleFetch } = useFetch('restaurants'); const fetchRestaurants = useCallback( async (queryObject: { boundary: CoordinateBoundary; celebId: number; category: RestaurantCategory }) => { + setLoading(true); const queryString = getQueryString(queryObject); const response = await handleFetch({ queryString }); - setData(response.content); + setData(response); + setLoading(false); }, [boundary, celebId, restaurantCategory], ); - const clickCard = (restaurant: Restaurant) => { - const { lat, lng, ...restaurantModalInfo } = restaurant; - - openModal(); - setCurrentRestaurant(restaurantModalInfo); - }; - - const clickMarker = ({ lat, lng }: Coordinate) => { - const filteredRestaurant = data.find(restaurantData => lat === restaurantData.lat && lng === restaurantData.lng); - - const { id, name, category, roadAddress, phoneNumber, naverMapUrl, images }: RestaurantModalInfo = - filteredRestaurant; - - setCurrentRestaurant({ id, name, category, roadAddress, phoneNumber, naverMapUrl, images }); - }; - const clickRestaurantCategory = (e: React.MouseEvent) => { const currentCategory = e.currentTarget.dataset.label as RestaurantCategory; @@ -86,28 +70,16 @@ function MainPage() {
- 음식점 수 {data.length} 개 - - {data?.map(({ celebs, ...restaurant }: RestaurantData) => ( - clickCard(restaurant)} /> - ))} - + ({ position: { lat, lng }, celebs }))} + data={data?.content} toggleMapExpand={toggleMapExpand} + hoveredId={hoveredId} + loadingData={loading} /> - {currentRestaurant && ( - - )}