Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE][Feature] #280 : HostView, GuestView 구현 및 업데이트 #288

Merged
merged 8 commits into from
Nov 27, 2024
Merged
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions frontend/public/assets/icons/Assistant Navigation Icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/assets/icons/Person Pin Icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/assets/icons/flag2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/assets/icons/location_on.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions frontend/src/api/channel.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createChannelResEntity,
getChannelResEntity,
getUserChannelsResEntity,
guestEntity,
} from '@/api/dto/channel.dto.ts';
import { getApiClient } from '@/api/client.api.ts';

Expand Down Expand Up @@ -80,3 +81,30 @@ export const getChannelInfo = (userId: string): Promise<ResponseDto<getChannelRe
};
return new Promise(promiseFn);
};

export const getGuestInfo = (
channelId: string,
userId: string,
): Promise<ResponseDto<guestEntity>> => {
const promiseFn = (
fnResolve: (value: ResponseDto<guestEntity>) => void,
fnReject: (reason?: any) => void,
) => {
const apiClient = getApiClient();
apiClient
.get(`/channel/${channelId}/guest/${userId}`)
.then(res => {
if (res.status !== 200) {
console.error(res);
fnReject(`msg.${res}`);
} else {
fnResolve(new ResponseDto<guestEntity>(res.data));
}
})
.catch(err => {
console.error(err);
fnReject('msg.RESULT_FAILED');
});
};
return new Promise(promiseFn);
};
33 changes: 33 additions & 0 deletions frontend/src/component/IconGuide/GuestMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MdAssistantNavigation, MdFlag, MdLocationOn } from 'react-icons/md';
import { IconContext } from 'react-icons';
import { ReactNode, useMemo } from 'react';

interface IMarkerData {
name: string;
icon: ReactNode;
}

export const GusetMarker = () => {
const markerData: IMarkerData[] = [
{ name: '내 위치', icon: <MdAssistantNavigation color="blue" /> },
{ name: '도착지', icon: <MdFlag color="purple" /> },
{ name: '출발지', icon: <MdLocationOn color="red" /> },
happyhyep marked this conversation as resolved.
Show resolved Hide resolved
];

const iconContextValue = useMemo(() => ({ color: 'purple', className: 'size-5' }), []);

return (
<div className="z-4000 absolute bottom-8 right-5 w-fit text-base">
<ul className="flex flex-col gap-1">
<IconContext.Provider value={iconContextValue}>
{markerData.map(data => (
<li className="flex items-center justify-between gap-2">
{data.icon}
<span>{data.name}</span>
</li>
))}
</IconContext.Provider>
</ul>
</div>
);
};
21 changes: 21 additions & 0 deletions frontend/src/component/IconGuide/HostMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MdAssistantNavigation } from 'react-icons/md';
import { IGuestData } from '@/types/channel.types.ts';

interface IUserMarkerProps {
userData: IGuestData[];
}

export const HostMarker = (props: IUserMarkerProps) => {
return (
<div className="z-4000 absolute bottom-8 right-5 w-fit text-base">
<ul className="flex flex-col gap-2">
{props.userData.map(data => (
<li key={data.name} className="flex items-center justify-between gap-2">
<MdAssistantNavigation color={data.markerStyle.color} className="size-5" />
{data.name}
</li>
))}
</ul>
</div>
);
};
35 changes: 11 additions & 24 deletions frontend/src/component/common/dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>;
path?: string;
className?: string;
}

/**
* 드롭다운 메뉴의 아이템 컴포넌트입니다.
*
* @param {ReactNode} children - 드롭다운 아이템 내용
* @param {React.MouseEventHandler<HTMLButtonElement>} onClick - 버튼 클릭 시 실행할 함수
* @return {JSX.Element} 드롭다운 아이템 컴포넌트
*
* @remarks
* - 드롭다운 아이템 컴포넌트는 드롭다운 메뉴 내부에서 사용되어야 합니다.
* - 드롭다운 아이템 컴포넌트는 드롭다운 메뉴의 아이템 역할을 수행합니다.
* - 드롭다운 아이템 컴포넌트는 버튼 역할을 수행합니다.
* - 드롭다운 아이템 컴포넌트는 클릭 시 onClick 함수를 실행합니다.
*
* @example
* ```tsx
* <Dropdown.Item onClick={handleOnClick}>아이템</Dropdown.Item>
* ```
*/

export const DropdownItem = (props: IDropdownItemProps) => {
const location = useLocation();
const navigate = useNavigate();

const handleClick = () => {
navigate(`${location.pathname}/${props.path}`);
};

return (
<li className={classNames('list-none px-3 py-1.5 text-base', props.className)}>
<button
type="button"
className="flex w-full items-center justify-between whitespace-nowrap bg-transparent"
onClick={props.onClick}
className="flex w-full items-center justify-between gap-2 whitespace-nowrap bg-transparent"
onClick={handleClick}
>
{props.children}
</button>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/component/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,7 +17,7 @@ export interface IHeaderOption {
rightButton?: string;
title?: ReactNode;
subtitle?: string;
items?: string[];
items?: IGuestData[];
}

export const Header = (props: IHeaderProps) => {
Expand Down
23 changes: 7 additions & 16 deletions frontend/src/component/header/HeaderDropdown.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DropdownItem key={e}>
{e}
<MdLocationOn className={classNames(`h-5 w-5 fill-current ${textMarkerUser[i]}`)} />
<DropdownItem key={guestData.id} path={`../guest/${guestData.id}`}>
<span>{guestData.name} 위치</span>
<MdLocationOn className="h-5 w-5 fill-current" color={guestData.markerStyle.color} />
</DropdownItem>
);
});

if (Items.length > 1) {
Items.push(
<DropdownItem key="showall" className="text-gray-400">
<DropdownItem key="showall" path="host">
모두 보기
</DropdownItem>,
);
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/component/header/HeaderMainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ReactNode } from 'react';
import { Header } from '@/component/header/Header.tsx';
import { IGuestData } from '@/types/channel.types.ts';

interface IHeaderMainLayoutProps {
leftButton?: string;
rightButton?: string;
title?: ReactNode;
items?: string[];
items?: IGuestData[];
}

export const HeaderMainLayout = (props: IHeaderMainLayoutProps) => {
Expand All @@ -15,7 +16,7 @@ export const HeaderMainLayout = (props: IHeaderMainLayoutProps) => {
return <Header.BackButton />;
case 'dropdown':
if (props.items) return <Header.Dropdown items={props.items} />;
return <Header.Dropdown items={['사용자 1']} />;
return <Header.Dropdown items={[]} />;
default:
return null;
}
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/component/layout/header/LayoutHeaderProvider.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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 });
}, []);

Expand Down
81 changes: 73 additions & 8 deletions frontend/src/pages/GuestView.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,80 @@
import { HeaderContext } from '@/component/layout/header/LayoutHeaderProvider';
import { useContext, useEffect } from 'react';
import { ReactNode, useEffect, useState } from 'react';
import { IGuest } from '@/types/channel.types.ts';
import { getGuestInfo } from '@/api/channel.api.ts';
import { useLocation } from 'react-router-dom';
import { MapCanvasForView } from '@/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx';
import { IPoint } from '@/lib/types/canvasInterface.ts';
import { guestEntity } from '@/api/dto/channel.dto.ts';
import { GusetMarker } from '@/component/IconGuide/GuestMarker.tsx';

export const GuestView = () => {
const headerContext = useContext(HeaderContext);
const [guestInfo, setGuestInfo] = useState<IGuest>({
id: '',
name: '',
markerStyle: { color: '' },
startPoint: { lat: 0, lng: 0 },
endPoint: { lat: 0, lng: 0 },
paths: [],
});
const [component, setComponent] = useState<ReactNode>();

const location = useLocation();

const transformTypeGuestEntityToIGuest = (props: guestEntity): IGuest => {
return {
id: props.id ?? '',
name: props.name ?? '',
// 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 fetchGuestInfo = (channelId: string, userId: string) => {
getGuestInfo(channelId, userId)
.then(res => {
if (!res.data) throw new Error('🚀 Fetch Error: responsed undefined');
const transfromedData = transformTypeGuestEntityToIGuest(res.data);
setGuestInfo(transfromedData);
})
.catch((error: any) => {
console.error(error);
});
};

useEffect(() => {
headerContext.setRightButton('dropdown');
headerContext.setLeftButton('back');
headerContext.setItems(['사용자 1', '테스트 2', '길동이']);
fetchGuestInfo(location.pathname.split('/')[2], location.pathname.split('/')[4]);
}, []);

// TODO: geoCoding API를 이용해서 현재 위치나 시작위치를 기반으로 자동 좌표 설정 구현 (현재: 하드코딩)
return <div>hello</div>;
useEffect(() => {
console.log(guestInfo);
if (guestInfo) {
setComponent(
<MapCanvasForView
lat={37.3595704}
lng={127.105399}
width="100%"
height="100%"
guests={[guestInfo]}
/>,
);
}
}, [guestInfo]);

return (
<article className="absolute h-full w-screen flex-grow overflow-hidden">
<GusetMarker />
{component}
</article>
);
};
Loading
Loading