diff --git a/frontend/package.json b/frontend/package.json index 8bc7b9b8..d5fe6de4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,6 +57,7 @@ "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", "vite": "^5.4.10", + "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^5.1.1" }, "eslintConfig": { diff --git a/frontend/public/assets/icons/Assistant Navigation Icon.svg b/frontend/public/assets/icons/Assistant Navigation Icon.svg new file mode 100644 index 00000000..f84efc86 --- /dev/null +++ b/frontend/public/assets/icons/Assistant Navigation Icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/assets/icons/Person Pin Icon.svg b/frontend/public/assets/icons/Person Pin Icon.svg new file mode 100644 index 00000000..94948f20 --- /dev/null +++ b/frontend/public/assets/icons/Person Pin Icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/assets/icons/flag.svg b/frontend/public/assets/icons/flag.svg new file mode 100644 index 00000000..2fbb36c2 --- /dev/null +++ b/frontend/public/assets/icons/flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/assets/icons/location_on.svg b/frontend/public/assets/icons/location_on.svg new file mode 100644 index 00000000..a6ce005d --- /dev/null +++ b/frontend/public/assets/icons/location_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/api/channel.api.ts b/frontend/src/api/channel.api.ts index de9d6fe4..1907acdc 100644 --- a/frontend/src/api/channel.api.ts +++ b/frontend/src/api/channel.api.ts @@ -2,6 +2,7 @@ import { ResponseDto } from '@/api/dto/response.dto.ts'; import { createChannelReqEntity, createChannelResEntity, + getChannelResEntity, getUserChannelsResEntity, } from '@/api/dto/channel.dto.ts'; import { getApiClient } from '@/api/client.api.ts'; @@ -56,9 +57,9 @@ export const getUserChannels = (userId: string): Promise> => { +export const getChannelInfo = (userId: string): Promise> => { const promiseFn = ( - fnResolve: (value: ResponseDto) => void, + fnResolve: (value: ResponseDto) => void, fnReject: (reason?: any) => void, ) => { const apiClient = getApiClient(); @@ -69,7 +70,7 @@ export const getChannelInfo = (userId: string): Promise(res.data)); + fnResolve(new ResponseDto(res.data)); } }) .catch(err => { diff --git a/frontend/src/api/dto/channel.dto.ts b/frontend/src/api/dto/channel.dto.ts index 58150eeb..d07fb3a5 100644 --- a/frontend/src/api/dto/channel.dto.ts +++ b/frontend/src/api/dto/channel.dto.ts @@ -9,6 +9,8 @@ export class guestMarkerStyleEntity { } export class guestEntity { + id: string | undefined; + name: string | undefined; start_location: locationEntity | undefined; @@ -57,3 +59,13 @@ export class channelListEntity { export class getUserChannelsResEntity { channels: channelListEntity[] | undefined; } + +export class getChannelResEntity { + id: string | undefined; + + name: string | undefined; + + host_id: string | undefined; + + guests: guestEntity[] | undefined; +} diff --git a/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx b/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx index 45596aeb..f65c1d9e 100644 --- a/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx +++ b/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx @@ -36,6 +36,7 @@ export const MapCanvasForView = ({ endImageRef.current = new Image(); endImageRef.current.src = endmarker; + console.log(guests); }, []); useEffect(() => { diff --git a/frontend/src/component/common/dropdown/DropdownItem.tsx b/frontend/src/component/common/dropdown/DropdownItem.tsx index 46897929..92378d15 100644 --- a/frontend/src/component/common/dropdown/DropdownItem.tsx +++ b/frontend/src/component/common/dropdown/DropdownItem.tsx @@ -1,40 +1,27 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; +import { useLocation, useNavigate } from 'react-router-dom'; interface IDropdownItemProps { - /** 드롭다운 아이템 내용 */ children: ReactNode; - /** 버튼 클릭 시 실행할 함수 */ - onClick?: React.MouseEventHandler; + path?: string; className?: string; } -/** - * 드롭다운 메뉴의 아이템 컴포넌트입니다. - * - * @param {ReactNode} children - 드롭다운 아이템 내용 - * @param {React.MouseEventHandler} onClick - 버튼 클릭 시 실행할 함수 - * @return {JSX.Element} 드롭다운 아이템 컴포넌트 - * - * @remarks - * - 드롭다운 아이템 컴포넌트는 드롭다운 메뉴 내부에서 사용되어야 합니다. - * - 드롭다운 아이템 컴포넌트는 드롭다운 메뉴의 아이템 역할을 수행합니다. - * - 드롭다운 아이템 컴포넌트는 버튼 역할을 수행합니다. - * - 드롭다운 아이템 컴포넌트는 클릭 시 onClick 함수를 실행합니다. - * - * @example - * ```tsx - * 아이템 - * ``` - */ - export const DropdownItem = (props: IDropdownItemProps) => { + const location = useLocation(); + const navigate = useNavigate(); + + const handleClick = () => { + navigate(`${location.pathname}/${props.path}`); + }; + return (
  • diff --git a/frontend/src/component/header/Header.tsx b/frontend/src/component/header/Header.tsx index 61d68d5c..136b8691 100644 --- a/frontend/src/component/header/Header.tsx +++ b/frontend/src/component/header/Header.tsx @@ -5,6 +5,7 @@ import { HeaderTitle } from '@/component/header/HeaderTitle.tsx'; import { HeaderMainLayout } from '@/component/header/HeaderMainLayout.tsx'; import { HeaderSubtitle } from '@/component/header/HeaderSubtitle.tsx'; import classNames from 'classnames'; +import { IGuestData } from '@/types/channel.types.ts'; interface IHeaderProps { children: ReactNode; @@ -16,7 +17,7 @@ export interface IHeaderOption { rightButton?: string; title?: ReactNode; subtitle?: string; - items?: string[]; + items?: string[] | IGuestData[]; } export const Header = (props: IHeaderProps) => { diff --git a/frontend/src/component/header/HeaderDropdown.tsx b/frontend/src/component/header/HeaderDropdown.tsx index 60f20c97..a467143e 100644 --- a/frontend/src/component/header/HeaderDropdown.tsx +++ b/frontend/src/component/header/HeaderDropdown.tsx @@ -1,35 +1,26 @@ import { Dropdown } from '@/component/common/dropdown/Dropdown.tsx'; import { MdMenu, MdLocationOn } from 'react-icons/md'; import { DropdownItem } from '@/component/common/dropdown/DropdownItem.tsx'; -import classNames from 'classnames'; +import { IGuestData } from '@/types/channel.types.ts'; interface IDropdownContainerProps { - items: string[]; + items: IGuestData[]; } export const HeaderDropdown = (props: IDropdownContainerProps) => { - // TODO: 하드코딩된 자료 말고 마커 색상 가져오기 - const textMarkerUser = [ - 'text-marker-user1', - 'text-marker-user2', - 'text-marker-user3', - 'text-marker-user4', - 'text-marker-user5', - ]; - const DropdownItems = () => { - const Items = props.items.map((e, i) => { + const Items = props.items.map(guestData => { return ( - - {e} - + + {guestData.name} 위치 + ); }); if (Items.length > 1) { Items.push( - + 모두 보기 , ); diff --git a/frontend/src/component/layout/header/LayoutHeaderProvider.tsx b/frontend/src/component/layout/header/LayoutHeaderProvider.tsx index 36547207..cd096314 100644 --- a/frontend/src/component/layout/header/LayoutHeaderProvider.tsx +++ b/frontend/src/component/layout/header/LayoutHeaderProvider.tsx @@ -1,5 +1,6 @@ import { ReactNode, createContext, useReducer, useMemo, useCallback } from 'react'; import { IHeaderOption } from '@/component/header/Header.tsx'; +import { IGuestData } from '@/types/channel.types.ts'; interface ILayoutHeaderProviderProps { children: ReactNode; @@ -11,7 +12,7 @@ interface IHeaderOptionContext { setSubTitle: (subtitle: string) => void; setLeftButton: (leftButton: string) => void; setRightButton: (rightButton: string) => void; - setItems: (items: string[]) => void; + setItems: (items: IGuestData[]) => void; resetHeaderContext: () => void; } @@ -38,7 +39,7 @@ type Action = | { type: 'SET_SUBTITLE'; payload: string } | { type: 'SET_LEFT_BUTTON'; payload: string } | { type: 'SET_RIGHT_BUTTON'; payload: string } - | { type: 'SET_ITEMS'; payload: string[] }; + | { type: 'SET_ITEMS'; payload: IGuestData[] }; const headerReducer = (state: IHeaderOption, action: Action): IHeaderOption => { switch (action.type) { @@ -76,7 +77,7 @@ export const LayoutHeaderProvider = (props: ILayoutHeaderProviderProps) => { dispatch({ type: 'SET_RIGHT_BUTTON', payload: rightButton }); }, []); - const setItems = useCallback((items: string[]) => { + const setItems = useCallback((items: IGuestData[]) => { dispatch({ type: 'SET_ITEMS', payload: items }); }, []); diff --git a/frontend/src/component/userMarker/UserMarker.tsx b/frontend/src/component/userMarker/UserMarker.tsx new file mode 100644 index 00000000..2aeb209b --- /dev/null +++ b/frontend/src/component/userMarker/UserMarker.tsx @@ -0,0 +1,21 @@ +import { MdLocationOn } from 'react-icons/md'; +import { IGuestData } from '@/types/channel.types.ts'; + +interface IUserMarkerProps { + userData: IGuestData[]; +} + +export const UserMarker = (props: IUserMarkerProps) => { + return ( +
    +
      + {props.userData.map(data => ( +
    • + + {data.name} +
    • + ))} +
    +
    + ); +}; diff --git a/frontend/src/lib/types/canvasInterface.ts b/frontend/src/lib/types/canvasInterface.ts index 924592a5..15445017 100644 --- a/frontend/src/lib/types/canvasInterface.ts +++ b/frontend/src/lib/types/canvasInterface.ts @@ -25,12 +25,17 @@ export interface IOtherLiveLocations { token: string; } +export interface IMarkerStyle { + color: string; +} + export interface IGuestDataInMapProps { - guestName: string; - guestUUID: string; + id: string; + name: string; startPoint: IPoint; endPoint: IPoint; paths: IPoint[]; + markerStyle: IMarkerStyle; } export interface IMapCanvasViewProps { diff --git a/frontend/src/pages/HostView.tsx b/frontend/src/pages/HostView.tsx index ffe2e09d..c1c2d36e 100644 --- a/frontend/src/pages/HostView.tsx +++ b/frontend/src/pages/HostView.tsx @@ -1,48 +1,109 @@ -// import { HeaderContext } from '@/component/layout/header/LayoutHeaderProvider'; -// import { useContext, useEffect, useState } from 'react'; -// import { IUserChannelInfo } from '@/types/channel.types.ts'; -// import { getChannelInfo } from '@/api/channel.api.ts'; -// import { useLocation } from 'react-router-dom'; -// import { CanvasWithMap } from '@/component/canvas/CanvasWithMap.tsx'; - -// export const HostView = () => { -// const [userChannels, setUserChannels] = useState(); -// const [userNames, setUserNames] = useState(['사용자 1']); - -// const headerContext = useContext(HeaderContext); - -// const location = useLocation(); - -// const fetchChannelInfo = (id: string) => { -// getChannelInfo(id) -// .then(res => { -// if (res.data) setUserChannels(res.data); -// }) -// .catch((error: any) => { -// console.error(error); -// }); -// }; - -// useEffect(() => { -// headerContext.setRightButton('dropdown'); -// headerContext.setLeftButton('back'); -// headerContext.setItems(['1']); - -// fetchChannelInfo(location.pathname.split('/')[2]); -// }, []); - -// useEffect(() => { -// if (userChannels?.guests) { -// const names = userChannels.guests.filter(Boolean).map(guest => guest.name!); -// setUserNames(names); -// } -// }, [userChannels]); - -// useEffect(() => { -// headerContext.setItems(userNames); -// }, [userNames]); - -// // TODO: geoCoding API를 이용해서 현재 위치나 시작위치를 기반으로 자동 좌표 설정 구현 (현재: 하드코딩) -// return ; -// }; -export const HostView = () => <>Hello; +import { HeaderContext } from '@/component/layout/header/LayoutHeaderProvider'; +import { ReactNode, useContext, useEffect, useState } from 'react'; +import { IGuest, IChannelInfo, IGuestData } from '@/types/channel.types.ts'; +import { getChannelInfo } from '@/api/channel.api.ts'; +import { useLocation } from 'react-router-dom'; +import { MapCanvasForView } from '@/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx'; +import { IGuestDataInMapProps, IPoint } from '@/lib/types/canvasInterface.ts'; +import { getChannelResEntity, guestEntity } from '@/api/dto/channel.dto.ts'; +import { UserMarker } from '@/component/userMarker/UserMarker.tsx'; + +export const HostView = () => { + const [channelInfo, setChannelInfo] = useState(); + const [guestData, setGuestData] = useState([]); + const [mapProps, setMapProps] = useState([]); + const [component, setComponent] = useState(); + + const headerContext = useContext(HeaderContext); + + const location = useLocation(); + + const transformTypeGuestEntityToIGuest = (props: guestEntity): IGuest => { + return { + id: props.id ?? '', + name: props.name ?? '', + startPoint: { + lat: props.start_location?.lat ?? 0, + lng: props.start_location?.lng ?? 0, + }, + endPoint: { + lat: props.end_location?.lat ?? 0, + lng: props.end_location?.lng ?? 0, + }, + paths: (props.path as IPoint[]) ?? [], + markerStyle: { + color: props.marker_style?.color ?? '', + }, + }; + }; + + const transformTypeFromResToInfo = (props: getChannelResEntity): IChannelInfo => { + const guests = props.guests?.map(guest => transformTypeGuestEntityToIGuest(guest)) ?? []; + + return { + name: props.name ?? '', + hostId: props.host_id ?? '', + channelId: props.id ?? '', + guests, + }; + }; + + const fetchChannelInfo = (userId: string) => { + getChannelInfo(userId) + .then(res => { + if (!res.data) throw new Error('🚀 Fetch Error: responsed undefined'); + const transfromedData = transformTypeFromResToInfo(res.data); + setChannelInfo(transfromedData); + }) + .catch((error: any) => { + console.error(error); + }); + }; + + useEffect(() => { + headerContext.setRightButton('dropdown'); + headerContext.setLeftButton('back'); + headerContext.setItems([{ name: '사용자 1', id: '1', markerStyle: { color: '#000' } }]); + + fetchChannelInfo(location.pathname.split('/')[2]); + }, []); + + useEffect(() => { + if (channelInfo?.guests) { + const data: IGuestData[] = channelInfo.guests.filter(Boolean).map(guest => ({ + name: guest.name, + markerStyle: guest.markerStyle, + id: guest.id, + })); + setGuestData(data); + channelInfo.guests?.map(guest => + setMapProps(prev => [...prev, guest as IGuestDataInMapProps]), + ); + } + }, [channelInfo]); + + useEffect(() => { + headerContext.setItems(guestData); + }, [guestData]); + + useEffect(() => { + if (mapProps.length > 1) { + setComponent( + , + ); + } + }, [mapProps]); + + return ( +
    + + {component} +
    + ); +}; diff --git a/frontend/src/types/channel.types.ts b/frontend/src/types/channel.types.ts index c96961e0..c3e63a31 100644 --- a/frontend/src/types/channel.types.ts +++ b/frontend/src/types/channel.types.ts @@ -1,22 +1,30 @@ -export interface ILocation { - lat: number | undefined; - lng: number | undefined; +export interface IPoint { + lat: number; + lng: number; } export interface IGuestMarkerStyle { - color: string | undefined; + color: string; } export interface IGuest { - name: string | undefined; - start_location: ILocation | undefined; - end_location: ILocation | undefined; - path: ILocation[] | undefined; - marker_style: IGuestMarkerStyle | undefined; + id: string; + name: string; + startPoint: IPoint; + endPoint: IPoint; + paths: IPoint[]; + markerStyle: IGuestMarkerStyle; } -export interface IUserChannelInfo { - host_id: string | undefined; - name: string | undefined; - guests: IGuest[] | undefined; +export interface IGuestData { + id: string; + name: string; + markerStyle: IGuestMarkerStyle; +} + +export interface IChannelInfo { + channelId: string; + hostId: string; + name: string; + guests: IGuest[]; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b804b6a9..662a3794 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,6 +311,9 @@ importers: vite: specifier: ^5.4.10 version: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + vite-plugin-svgr: + specifier: ^4.3.0 + version: 4.3.0(rollup@4.26.0)(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) vite-tsconfig-paths: specifier: ^5.1.1 version: 5.1.2(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) @@ -7633,6 +7636,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-plugin-svgr@4.3.0: + resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==} + peerDependencies: + vite: '>=2.6.0' + vite-tsconfig-paths@5.1.2: resolution: {integrity: sha512-gEIbKfJzSEv0yR3XS2QEocKetONoWkbROj6hGx0FHM18qKUojhvcokQsxQx5nMkelZq2n37zbSGCJn+FSODSjA==} peerDependencies: @@ -17078,6 +17086,17 @@ snapshots: - supports-color - terser + vite-plugin-svgr@4.3.0(rollup@4.26.0)(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)): + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.26.0) + '@svgr/core': 8.1.0(typescript@5.6.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.6.3)) + vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + vite-tsconfig-paths@5.1.2(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)): dependencies: debug: 4.3.7