From 9b604eb23f4d577098421416a1380619e91c0261 Mon Sep 17 00:00:00 2001 From: effozen Date: Tue, 3 Dec 2024 13:56:59 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[FE][Feat]=20#370=20:=20=EB=A7=88=EC=BB=A4?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/endmarker.svg | 2 +- frontend/src/assets/footprint.svg | 2 +- frontend/src/assets/mylocation.svg | 2 +- frontend/src/assets/startmarker.svg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/assets/endmarker.svg b/frontend/src/assets/endmarker.svg index beab2b0a..6b4822cd 100644 --- a/frontend/src/assets/endmarker.svg +++ b/frontend/src/assets/endmarker.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/assets/footprint.svg b/frontend/src/assets/footprint.svg index 6ccb1958..6f631378 100644 --- a/frontend/src/assets/footprint.svg +++ b/frontend/src/assets/footprint.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/assets/mylocation.svg b/frontend/src/assets/mylocation.svg index b2b0ad7d..bbee0974 100644 --- a/frontend/src/assets/mylocation.svg +++ b/frontend/src/assets/mylocation.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/assets/startmarker.svg b/frontend/src/assets/startmarker.svg index 31934eb8..d32b5120 100644 --- a/frontend/src/assets/startmarker.svg +++ b/frontend/src/assets/startmarker.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 36321f2d91a1742b7ba88745078e9021852e7f0e Mon Sep 17 00:00:00 2001 From: effozen Date: Tue, 3 Dec 2024 17:27:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[FE][Feat]=20#372=20:=20=EB=93=9C=EB=9E=8D?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EA=B0=9C=EB=B3=84=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/common/dropdown/Dropdown.tsx | 13 +- .../common/dropdown/DropdownContext.tsx | 25 ---- .../common/dropdown/DropdownMenu.tsx | 18 ++- .../common/dropdown/DropdownTrigger.tsx | 13 +- frontend/src/component/content/Content.tsx | 3 +- frontend/src/context/DropdownContext.tsx | 24 ++++ .../src/context/DropdownInstanceContext.tsx | 3 + frontend/src/pages/Main.tsx | 130 +++++++++--------- 8 files changed, 125 insertions(+), 104 deletions(-) delete mode 100644 frontend/src/component/common/dropdown/DropdownContext.tsx create mode 100644 frontend/src/context/DropdownContext.tsx create mode 100644 frontend/src/context/DropdownInstanceContext.tsx diff --git a/frontend/src/component/common/dropdown/Dropdown.tsx b/frontend/src/component/common/dropdown/Dropdown.tsx index 0d8e1acf..d9c194c3 100644 --- a/frontend/src/component/common/dropdown/Dropdown.tsx +++ b/frontend/src/component/common/dropdown/Dropdown.tsx @@ -1,8 +1,9 @@ -import { ReactNode } from 'react'; +import { ReactNode, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { DropdownTrigger } from '@/component/common/dropdown/DropdownTrigger.tsx'; import { DropdownItem } from '@/component/common/dropdown/DropdownItem.tsx'; import { DropdownMenu } from '@/component/common/dropdown/DropdownMenu.tsx'; -import { ToggleProvider } from '@/component/common/dropdown/DropdownContext.tsx'; +import { DropdownInstanceContext } from '@/context/DropdownInstanceContext'; interface IDropdownProps { /** 드롭다운 컴포넌트 내부에 들어갈 컨텐츠 */ @@ -34,10 +35,12 @@ interface IDropdownProps { */ export const Dropdown = (props: IDropdownProps) => { + const id = useMemo(() => uuidv4(), []); // 각 Dropdown 인스턴스에 고유 ID 생성 + return ( - + + + ); }; diff --git a/frontend/src/component/common/dropdown/DropdownContext.tsx b/frontend/src/component/common/dropdown/DropdownContext.tsx deleted file mode 100644 index 61b865ac..00000000 --- a/frontend/src/component/common/dropdown/DropdownContext.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { createContext, ReactNode, useMemo, useState } from 'react'; - -export interface IToggleContext { - isOpen: boolean; - toggle: () => void; -} - -interface IToggleProviderProps { - children: ReactNode; -} - -export const ToggleContext = createContext({ - isOpen: false, - toggle: () => {}, -}); - -export const ToggleProvider = (props: IToggleProviderProps) => { - const [isOpen, setIsOpen] = useState(false); - const toggle = () => setIsOpen(prevIsOpen => !prevIsOpen); - const toggleContextValue = useMemo(() => ({ isOpen, toggle }), [isOpen]); - - return ( - {props.children} - ); -}; diff --git a/frontend/src/component/common/dropdown/DropdownMenu.tsx b/frontend/src/component/common/dropdown/DropdownMenu.tsx index c49eab34..189cd792 100644 --- a/frontend/src/component/common/dropdown/DropdownMenu.tsx +++ b/frontend/src/component/common/dropdown/DropdownMenu.tsx @@ -1,6 +1,7 @@ import { ReactNode, useContext, useRef, useEffect } from 'react'; import classNames from 'classnames'; -import { ToggleContext } from '@/component/common/dropdown/DropdownContext'; +import { ToggleContext } from '@/context/DropdownContext.tsx'; +import { DropdownInstanceContext } from '@/context/DropdownInstanceContext'; interface IDropdownMenuProps { /** 드롭다운 메뉴가 열려있는지 여부 */ @@ -27,7 +28,9 @@ interface IDropdownMenuProps { */ export const DropdownMenu = (props: IDropdownMenuProps) => { - const { isOpen, toggle } = useContext(ToggleContext); + const { openDropdownId, setOpenDropdownId } = useContext(ToggleContext); + const dropdownId = useContext(DropdownInstanceContext); + const isOpen = openDropdownId === dropdownId; const ref = useRef(null); const handleOutSideClick = (event: MouseEvent) => { @@ -43,23 +46,26 @@ export const DropdownMenu = (props: IDropdownMenuProps) => { !ref.current.contains(target) && target.dataset.component !== 'DropdownTrigger' ) { - toggle(); + setOpenDropdownId(null); // 외부 클릭 시 드롭다운 닫기 } }; useEffect(() => { - document.addEventListener('click', handleOutSideClick); + if (isOpen) { + document.addEventListener('click', handleOutSideClick); + } else { + document.removeEventListener('click', handleOutSideClick); + } return () => { document.removeEventListener('click', handleOutSideClick); }; - }, []); + }, [isOpen]); return ( isOpen && (
    { - const { toggle } = useContext(ToggleContext); + const { openDropdownId, setOpenDropdownId } = useContext(ToggleContext); + const dropdownId = useContext(DropdownInstanceContext); + const isOpen = openDropdownId === dropdownId; const handleOnClick = () => { - toggle(); + if (isOpen) { + setOpenDropdownId(null); // 이미 열려 있으면 닫기 + } else { + setOpenDropdownId(dropdownId); // 이 드롭다운 열기 + } }; return ( diff --git a/frontend/src/component/content/Content.tsx b/frontend/src/component/content/Content.tsx index b2426028..9eb9d5f3 100644 --- a/frontend/src/component/content/Content.tsx +++ b/frontend/src/component/content/Content.tsx @@ -82,7 +82,6 @@ export const Content = (props: IContentProps) => { className="relative flex w-full flex-row items-center justify-between px-4 py-5" onClick={goToHostViewPage} > - {/*
    */}
    {props.title} @@ -102,7 +101,7 @@ export const Content = (props: IContentProps) => {
    { - e.stopPropagation(); + e.stopPropagation(); // 부모의 onClick 이벤트 방지 }} > diff --git a/frontend/src/context/DropdownContext.tsx b/frontend/src/context/DropdownContext.tsx new file mode 100644 index 00000000..7d131dd2 --- /dev/null +++ b/frontend/src/context/DropdownContext.tsx @@ -0,0 +1,24 @@ +// DropdownContext.tsx +import { createContext, ReactNode, useState, useMemo } from 'react'; + +export interface IToggleContext { + openDropdownId: string | null; + setOpenDropdownId: (id: string | null) => void; +} + +interface IToggleProviderProps { + children: ReactNode; +} + +export const ToggleContext = createContext({ + openDropdownId: null, + setOpenDropdownId: () => {}, +}); + +export const ToggleProvider = ({ children }: IToggleProviderProps) => { + const [openDropdownId, setOpenDropdownId] = useState(null); + + const value = useMemo(() => ({ openDropdownId, setOpenDropdownId }), [openDropdownId]); + + return {children}; +}; diff --git a/frontend/src/context/DropdownInstanceContext.tsx b/frontend/src/context/DropdownInstanceContext.tsx new file mode 100644 index 00000000..c27b732f --- /dev/null +++ b/frontend/src/context/DropdownInstanceContext.tsx @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const DropdownInstanceContext = createContext(null); diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx index 245dce0f..bebbc120 100644 --- a/frontend/src/pages/Main.tsx +++ b/frontend/src/pages/Main.tsx @@ -1,3 +1,4 @@ +// Main.tsx import { Fragment, useContext, useEffect, useState } from 'react'; import { MdLogout } from 'react-icons/md'; import { FooterContext } from '@/component/layout/footer/LayoutFooterProvider'; @@ -14,6 +15,7 @@ import { getUserLocation } from '@/hooks/getUserLocation.ts'; import { MapCanvasForView } from '@/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx'; import { LoadingSpinner } from '@/component/common/loadingSpinner/LoadingSpinner.tsx'; import { UserContext } from '@/context/UserContext'; +import { ToggleProvider } from '@/context/DropdownContext.tsx'; export const Main = () => { const { @@ -116,73 +118,75 @@ export const Main = () => { const isUserLoggedIn = loadLocalData(AppConfig.KEYS.LOGIN_TOKEN) !== null; return ( -
    -
    - {isUserLoggedIn && ( - - )} -
    + +
    +
    + {isUserLoggedIn && ( + + )} +
    -
    - {/* eslint-disable-next-line no-nested-ternary */} - {lat && lng ? ( - otherLocations ? ( - +
    + {/* eslint-disable-next-line no-nested-ternary */} + {lat && lng ? ( + otherLocations ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( -
    - - {error ? `Error: ${error}` : 'Loading map data...'} -
    - )} -
    +
    + + {error ? `Error: ${error}` : 'Loading map data...'} +
    + )} +
    - {isUserLoggedIn ? ( - - {channels.map(item => ( - - -
    -
    - ))} -
    - - ) : ( - -
    -
    -

    로그인을 진행하여

    -

    더 많은 기능을

    -

    사용해보세요

    + {isUserLoggedIn ? ( + + {channels.map(item => ( + + +
    +
    + ))} +
    + + ) : ( + +
    +
    +

    로그인을 진행하여

    +

    더 많은 기능을

    +

    사용해보세요

    +
    -
    -
    - )} + + )} - {/* 로그인 모달 */} - -
    + {/* 로그인 모달 */} + +
    + ); }; From 51d1c205383b7d89b7271ff99c1a7db8e557e23a Mon Sep 17 00:00:00 2001 From: effozen Date: Tue, 3 Dec 2024 17:43:19 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[FE][Feat]=20#374=20:=20HostView=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/HostView.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/HostView.tsx b/frontend/src/pages/HostView.tsx index c2bb1b54..2440bc98 100644 --- a/frontend/src/pages/HostView.tsx +++ b/frontend/src/pages/HostView.tsx @@ -147,9 +147,14 @@ export const HostView = () => { if (clickedId === '') { setMapProps([]); - channelInfo.guests?.map(guest => - setMapProps(prev => [...prev, guest as IGuestDataInMapProps]), - ); + const tmpMapProps: IGuestDataInMapProps[] = []; + // TODO : 차후 로직 개선하기 + channelInfo.guests?.map(guest => tmpMapProps.push(guest as IGuestDataInMapProps)); + const orderedMapProps: IGuestDataInMapProps[] = []; + markerDefaultColor.forEach(color => { + orderedMapProps.push(...tmpMapProps.filter(guest => guest.markerStyle.color === color)); + }); + setMapProps([...orderedMapProps]); } else { setMapProps(channelInfo.guests?.filter(guest => guest.id === clickedId)); } From 97d41448df4bbf7b05c1f7de1c2af893b717c2b8 Mon Sep 17 00:00:00 2001 From: effozen Date: Tue, 3 Dec 2024 17:55:33 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[FE][Feat]=20#374=20:=20HostView=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20Dropdown=20=EB=8F=99=EC=9E=91=20=EC=95=88=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/component/layout/Layout.tsx | 25 +++++++++++++----------- frontend/src/pages/HostView.tsx | 25 ++++++++++++++++-------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/frontend/src/component/layout/Layout.tsx b/frontend/src/component/layout/Layout.tsx index 7da62970..fc549503 100644 --- a/frontend/src/component/layout/Layout.tsx +++ b/frontend/src/component/layout/Layout.tsx @@ -2,21 +2,24 @@ import { Outlet } from 'react-router-dom'; import { LayoutHeader } from '@/component/layout/header/LayoutHeader.tsx'; import { HeaderDropdownProvider } from '@/component/header/HeaderDropdownProvider.tsx'; import { LayoutFooterProvider } from '@/component/layout/footer/LayoutFooterProvider'; +import { ToggleProvider } from '@/context/DropdownContext.tsx'; import { LayoutFooter } from './footer/LayoutFooter'; export const Layout = () => { return ( - -
    - {/* LayoutHeader는 HeaderContext를 사용하므로 LayoutHeaderProvider로 감쌈 */} - + + +
    + {/* LayoutHeader는 HeaderContext를 사용하므로 LayoutHeaderProvider로 감쌈 */} + - {/* LayoutFooterProvider로 Outlet을 감싸서 FooterContext도 제공 */} - - - - -
    -
    + {/* LayoutFooterProvider로 Outlet을 감싸서 FooterContext도 제공 */} + + + + +
    +
    + ); }; diff --git a/frontend/src/pages/HostView.tsx b/frontend/src/pages/HostView.tsx index 2440bc98..69689761 100644 --- a/frontend/src/pages/HostView.tsx +++ b/frontend/src/pages/HostView.tsx @@ -118,6 +118,20 @@ export const HostView = () => { .then(res => { if (!res.data) throw new Error('🚀 Fetch Error: responsed undefined'); const transfromedData = transformTypeFromResToInfo(res.data); + + const orderedGuest: IGuest[] = []; + + markerDefaultColor.forEach(color => { + const guest = transfromedData.guests.find( + guestData => guestData.markerStyle.color === color, + ); + if (guest) { + orderedGuest.push(guest); + } + }); + + transfromedData.guests = orderedGuest; + setChannelInfo(transfromedData); }) .catch((err: any) => { @@ -147,14 +161,9 @@ export const HostView = () => { if (clickedId === '') { setMapProps([]); - const tmpMapProps: IGuestDataInMapProps[] = []; - // TODO : 차후 로직 개선하기 - channelInfo.guests?.map(guest => tmpMapProps.push(guest as IGuestDataInMapProps)); - const orderedMapProps: IGuestDataInMapProps[] = []; - markerDefaultColor.forEach(color => { - orderedMapProps.push(...tmpMapProps.filter(guest => guest.markerStyle.color === color)); - }); - setMapProps([...orderedMapProps]); + channelInfo.guests?.map(guest => + setMapProps(prev => [...prev, guest as IGuestDataInMapProps]), + ); } else { setMapProps(channelInfo.guests?.filter(guest => guest.id === clickedId)); }