diff --git a/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx b/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx
index 77d1bfbb..fd665a52 100644
--- a/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx
+++ b/frontend/src/component/canvasWithMap/canvasWithMapForView/MapCanvasForView.tsx
@@ -7,6 +7,7 @@ import { ZoomSlider } from '@/component/zoomslider/ZoomSlider';
export const MapCanvasForView = ({
lat,
lng,
+ alpha,
otherLocations,
guests,
width,
@@ -70,6 +71,7 @@ export const MapCanvasForView = ({
guests,
lat,
lng,
+ alpha,
});
const {
@@ -101,7 +103,7 @@ export const MapCanvasForView = ({
useEffect(() => {
redrawCanvas();
- }, [guests, otherLocations, lat, lng, map]);
+ }, [guests, otherLocations, lat, lng, alpha, map]);
return (
{
const [location, setLocation] = useState
({
lat: null,
lng: null,
+ alpha: null,
error: null,
});
useEffect(() => {
- if (navigator.geolocation) {
- const watchId = navigator.geolocation.watchPosition(
- position => {
- setLocation({
- lat: position.coords.latitude,
- lng: position.coords.longitude,
- error: null,
- });
- },
- error => {
- setLocation({
- lat: 37.3595704,
- lng: 127.105399,
- error: error.message,
- });
- },
- { enableHighAccuracy: true, maximumAge: 5000, timeout: 10000 },
- );
+ let watchId: number;
+
+ const handlePosition = (position: GeolocationPosition) => {
+ setLocation(prev => ({
+ ...prev,
+ lat: position.coords.latitude,
+ lng: position.coords.longitude,
+ error: null,
+ }));
+ };
- return () => navigator.geolocation.clearWatch(watchId);
+ const handleError = (error: GeolocationPositionError) => {
+ setLocation({
+ lat: 37.3595704,
+ lng: 127.105399,
+ alpha: 0,
+ error: error.message,
+ });
+ };
+
+ if (navigator.geolocation) {
+ watchId = navigator.geolocation.watchPosition(handlePosition, handleError, {
+ enableHighAccuracy: true,
+ maximumAge: 5000,
+ timeout: 10000,
+ });
+ } else {
+ setLocation({
+ lat: 37.3595704,
+ lng: 127.105399,
+ alpha: 0,
+ error: '현재 위치를 불러오지 못했습니다',
+ });
}
- setLocation({
- lat: 37.3595704,
- lng: 127.105399,
- error: '현재 위치를 불러오지 못했습니다',
- });
+
+ const handleOrientation = (event: DeviceOrientationEvent) => {
+ if (event.alpha !== null) {
+ setLocation(prev => ({ ...prev, alpha: event.alpha }));
+ }
+ };
+
+ window.addEventListener('deviceorientation', handleOrientation);
+
+ return () => {
+ if (watchId) navigator.geolocation.clearWatch(watchId);
+ window.removeEventListener('deviceorientation', handleOrientation);
+ };
}, []);
return location;
diff --git a/frontend/src/hooks/useRedraw.ts b/frontend/src/hooks/useRedraw.ts
index 908bb6b5..18f95e13 100644
--- a/frontend/src/hooks/useRedraw.ts
+++ b/frontend/src/hooks/useRedraw.ts
@@ -3,23 +3,32 @@ import { LINE_WIDTH, STROKE_STYLE } from '@/lib/constants/canvasConstants.ts';
import startmarker from '@/assets/startmarker.svg';
import endmarker from '@/assets/endmarker.svg';
+import mylocation from '@/assets/mylocation.svg';
import character1 from '@/assets/character1.png';
import character2 from '@/assets/character2.png';
+import { IMarkerStyle } from '@/lib/types/canvasInterface.ts';
interface ILatLng {
lat: number;
lng: number;
}
+interface ILatLngAlpha {
+ lat: number;
+ lng: number;
+ alpha: number;
+}
+
interface IOtherLocation {
- location: ILatLng;
- token: string;
+ location: ILatLngAlpha;
+ color: string;
}
interface IGuest {
startPoint: ILatLng;
endPoint: ILatLng;
paths: ILatLng[];
+ markerStyle: IMarkerStyle;
}
interface IUseRedrawCanvasProps {
@@ -35,6 +44,7 @@ interface IUseRedrawCanvasProps {
guests?: IGuest[] | null;
lat?: number;
lng?: number;
+ alpha?: number | null;
}
export const useRedrawCanvas = ({
@@ -48,9 +58,11 @@ export const useRedrawCanvas = ({
guests = [],
lat,
lng,
+ alpha = 0,
}: IUseRedrawCanvasProps) => {
const startImageRef = useRef(null);
const endImageRef = useRef(null);
+ const mylocationRef = useRef(null);
const character1Ref = useRef(null);
const character2Ref = useRef(null);
@@ -61,6 +73,9 @@ export const useRedrawCanvas = ({
endImageRef.current = new Image();
endImageRef.current.src = endmarker;
+ mylocationRef.current = new Image();
+ mylocationRef.current.src = mylocation;
+
character1Ref.current = new Image();
character1Ref.current.src = character1;
@@ -72,11 +87,72 @@ export const useRedrawCanvas = ({
ctx: CanvasRenderingContext2D,
point: { x: number; y: number } | null,
image: HTMLImageElement | null,
+ zoom: number,
+ rotate: number,
) => {
if (point && image) {
- const markerSize = 32;
- ctx.drawImage(image, point.x - markerSize / 2, point.y - markerSize, markerSize, markerSize);
+ const markerSize = zoom * 5;
+ ctx.save();
+ ctx.translate(point.x, point.y);
+ ctx.rotate(rotate);
+ ctx.drawImage(image, -markerSize / 2, -markerSize / 2, markerSize, markerSize);
+ ctx.restore();
+ }
+ };
+
+ // eslint-disable-next-line no-shadow
+ const hexToRgba = (hex: string, alpha: number) => {
+ // eslint-disable-next-line no-param-reassign
+ hex = hex.replace(/^#/, '');
+
+ if (hex.length === 3) {
+ // eslint-disable-next-line no-param-reassign
+ hex = hex
+ .split('')
+ .map(char => char + char)
+ .join('');
}
+
+ const bigint = parseInt(hex, 16);
+ const r = (bigint >> 16) & 255;
+ const g = (bigint >> 8) & 255;
+ const b = bigint & 255;
+
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+ };
+
+ const drawNeonCircleAndDirection = (
+ ctx: CanvasRenderingContext2D,
+ point: { x: number; y: number } | null,
+ zoom: number,
+ color: string,
+ ) => {
+ if (!point) return;
+
+ const radius = zoom * 3;
+ const gradient = ctx.createRadialGradient(
+ point.x,
+ point.y + zoom,
+ 0,
+ point.x,
+ point.y + zoom,
+ radius,
+ );
+
+ const alphaStart = 0.75;
+ const alphaEnd = 0;
+
+ gradient.addColorStop(0, hexToRgba(color || '#3498db', alphaStart));
+ gradient.addColorStop(1, hexToRgba(color || '#3498db', alphaEnd));
+
+ ctx.beginPath();
+ ctx.arc(point.x, point.y + zoom + 1, radius, 0, 2 * Math.PI);
+ ctx.fillStyle = gradient;
+ ctx.fill();
+
+ ctx.save();
+
+ ctx.restore();
};
const drawPath = (ctx: CanvasRenderingContext2D, points: ILatLng[]) => {
@@ -96,20 +172,6 @@ export const useRedrawCanvas = ({
}
};
- // const getMarkerColor = (token: string): string => {
- // // 문자열 해싱을 통해 고유 숫자 생성
- // let hash = 0;
- // for (let i = 0; i < token.length; i++) {
- // hash = token.charCodeAt(i) + ((hash << 5) - hash);
- // }
- // // 해시 값을 기반으로 RGB 값 생성
- // const r = (hash >> 16) & 0xff;
- // const g = (hash >> 8) & 0xff;
- // const b = hash & 0xff;
- // // RGB를 HEX 코드로 변환
- // return `rgb(${r}, ${g}, ${b})`;
- // };
-
const redrawCanvas = () => {
if (!canvasRef.current || !map) return;
@@ -123,14 +185,15 @@ export const useRedrawCanvas = ({
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
+ const zoom = map.getZoom();
if (startMarker) {
const startPoint = latLngToCanvasPoint(startMarker);
- drawMarker(ctx, startPoint, startImageRef.current);
+ drawMarker(ctx, startPoint, startImageRef.current, zoom, 0);
}
if (endMarker) {
const endPoint = latLngToCanvasPoint(endMarker);
- drawMarker(ctx, endPoint, endImageRef.current);
+ drawMarker(ctx, endPoint, endImageRef.current, zoom, 0);
}
if (pathPoints) {
@@ -139,24 +202,38 @@ export const useRedrawCanvas = ({
if (lat && lng) {
const currentLocation = latLngToCanvasPoint({ lat, lng });
- drawMarker(ctx, currentLocation, character1Ref.current);
+ if (alpha) {
+ drawMarker(ctx, currentLocation, character1Ref.current, zoom, (alpha * Math.PI) / 180);
+ } else {
+ drawMarker(ctx, currentLocation, character1Ref.current, zoom, 0);
+ }
}
if (otherLocations) {
- otherLocations.forEach(({ location }) => {
- // const markerColor = getMarkerColor(token);
- const locationPoint = latLngToCanvasPoint(location);
- drawMarker(ctx, locationPoint, character2Ref.current);
+ otherLocations.forEach(({ location, color }) => {
+ const locationPoint = latLngToCanvasPoint({
+ lat: location.lat ? location.lat : 0,
+ lng: location.lng ? location.lng : 0,
+ });
+
+ drawNeonCircleAndDirection(ctx, locationPoint, zoom, color);
+ drawMarker(
+ ctx,
+ locationPoint,
+ character2Ref.current,
+ zoom,
+ (location.alpha * Math.PI) / 180,
+ );
});
}
if (guests) {
guests.forEach(({ startPoint, endPoint, paths }) => {
const startLocation = latLngToCanvasPoint(startPoint);
- drawMarker(ctx, startLocation, startImageRef.current);
+ drawMarker(ctx, startLocation, startImageRef.current, zoom, 0);
const endLocation = latLngToCanvasPoint(endPoint);
- drawMarker(ctx, endLocation, endImageRef.current);
+ drawMarker(ctx, endLocation, endImageRef.current, zoom, 0);
drawPath(ctx, paths);
});
diff --git a/frontend/src/hooks/useSocket.ts b/frontend/src/hooks/useSocket.ts
new file mode 100644
index 00000000..8857d345
--- /dev/null
+++ b/frontend/src/hooks/useSocket.ts
@@ -0,0 +1,33 @@
+import { useState, useEffect } from 'react';
+
+export const useSocket = (url: string) => {
+ const [ws, setWs] = useState(null);
+
+ useEffect(() => {
+ const socket = new WebSocket(url);
+ setWs(socket);
+
+ socket.onopen = () => {
+ console.log('Socket connected');
+ };
+
+ socket.onmessage = event => {
+ const data = JSON.parse(event.data);
+ console.log(data);
+ };
+
+ socket.onclose = () => {
+ console.log('Socket closed');
+ };
+
+ socket.onerror = err => {
+ console.error('Socket error:', err);
+ };
+
+ return () => {
+ socket.close();
+ };
+ }, [url]);
+
+ return ws;
+};
diff --git a/frontend/src/lib/types/canvasInterface.ts b/frontend/src/lib/types/canvasInterface.ts
index 15445017..f71be6d8 100644
--- a/frontend/src/lib/types/canvasInterface.ts
+++ b/frontend/src/lib/types/canvasInterface.ts
@@ -10,6 +10,12 @@ export interface IPoint {
lng: number;
}
+export interface IPointWithAlpha {
+ lat: number;
+ lng: number;
+ alpha: number;
+}
+
export interface ICanvasPoint {
x: number;
y: number;
@@ -21,8 +27,9 @@ export interface ICanvasScreenProps {
}
export interface IOtherLiveLocations {
- location: IPoint;
+ location: IPointWithAlpha;
token: string;
+ color: string;
}
export interface IMarkerStyle {
@@ -41,6 +48,7 @@ export interface IGuestDataInMapProps {
export interface IMapCanvasViewProps {
lat: number;
lng: number;
+ alpha?: number | null;
otherLocations?: IOtherLiveLocations[] | null;
guests?: IGuestDataInMapProps[] | null;
width: string;
diff --git a/frontend/src/pages/GuestView.tsx b/frontend/src/pages/GuestView.tsx
index 1d47b191..03638a4c 100644
--- a/frontend/src/pages/GuestView.tsx
+++ b/frontend/src/pages/GuestView.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { IGuest } from '@/types/channel.types.ts';
import { getGuestInfo } from '@/api/channel.api.ts';
import { useLocation } from 'react-router-dom';
@@ -8,9 +8,14 @@ import { guestEntity } from '@/api/dto/channel.dto.ts';
import { GusetMarker } from '@/component/IconGuide/GuestMarker.tsx';
import { LoadingSpinner } from '@/component/common/loadingSpinner/LoadingSpinner.tsx';
import { getUserLocation } from '@/hooks/getUserLocation.ts';
+import { loadLocalData, saveLocalData } from '@/utils/common/manageLocalData.ts';
+import { AppConfig } from '@/lib/constants/commonConstants.ts';
+import { v4 as uuidv4 } from 'uuid';
export const GuestView = () => {
- const { lat, lng, error } = getUserLocation();
+ const { lat, lng, alpha, error } = getUserLocation();
+ const location = useLocation();
+
const [guestInfo, setGuestInfo] = useState({
id: '',
name: '',
@@ -20,7 +25,35 @@ export const GuestView = () => {
paths: [],
});
- const location = useLocation();
+ const wsRef = useRef(null);
+
+ useEffect(() => {
+ // 소켓 연결 초기화
+ const token = loadLocalData(AppConfig.KEYS.BROWSER_TOKEN) || uuidv4();
+ saveLocalData(AppConfig.KEYS.BROWSER_TOKEN, token);
+
+ const ws = new WebSocket(
+ `${AppConfig.SOCKET_SERVER}/?token=${token}&channelId=${location.pathname.split('/')[2]}&role=guest&guestId=${location.pathname.split('/')[4]}`,
+ );
+
+ ws.onopen = () => {
+ console.log('WebSocket connection established');
+ };
+
+ wsRef.current = ws;
+ }, [location]);
+
+ useEffect(() => {
+ // 위치 정보가 변경될 때마다 전송
+ if (lat && lng && wsRef.current?.readyState === WebSocket.OPEN) {
+ wsRef.current.send(
+ JSON.stringify({
+ type: 'location',
+ location: { lat, lng, alpha },
+ }),
+ );
+ }
+ }, [lat, lng, alpha]);
const transformTypeGuestEntityToIGuest = (props: guestEntity | undefined): IGuest => {
return {
@@ -64,7 +97,14 @@ export const GuestView = () => {
{/* eslint-disable-next-line no-nested-ternary */}
{lat && lng ? (
guestInfo ? (
-
+
) : (
)
diff --git a/frontend/src/pages/HostView.tsx b/frontend/src/pages/HostView.tsx
index 9c5f5460..481e32b3 100644
--- a/frontend/src/pages/HostView.tsx
+++ b/frontend/src/pages/HostView.tsx
@@ -4,22 +4,83 @@ 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 { IGuestDataInMapProps, IPoint, IPointWithAlpha } from '@/lib/types/canvasInterface.ts';
import { getChannelResEntity, guestEntity } from '@/api/dto/channel.dto.ts';
import { HostMarker } from '@/component/IconGuide/HostMarker.tsx';
import { LoadingSpinner } from '@/component/common/loadingSpinner/LoadingSpinner.tsx';
import { getUserLocation } from '@/hooks/getUserLocation.ts';
-
+import { loadLocalData, saveLocalData } from '@/utils/common/manageLocalData.ts';
+import { AppConfig } from '@/lib/constants/commonConstants.ts';
+import { v4 as uuidv4 } from 'uuid';
+import { useSocket } from '@/hooks/useSocket.ts';
+
+interface IOtherLocationsInHostView {
+ guestId: string;
+ location: IPointWithAlpha;
+ token: string;
+ color: string;
+}
export const HostView = () => {
- const { lat, lng, error } = getUserLocation();
+ const { lat, lng, alpha, error } = getUserLocation();
+ const location = useLocation();
+
const [channelInfo, setChannelInfo] = useState();
const [guestsData, setGuestsData] = useState([]);
const [mapProps, setMapProps] = useState([]);
const [clickedId, setClickedId] = useState('');
+ const [otherLocations, setOtherLocations] = useState([]);
const headerDropdownContext = useContext(HeaderDropdownContext);
+ const markerDefaultColor = ['#B4D033', '#22A751', '#2722A7', '#8F22A7', '#A73D22'];
- const location = useLocation();
+ if (!loadLocalData(AppConfig.KEYS.BROWSER_TOKEN)) {
+ const token = uuidv4();
+ saveLocalData(AppConfig.KEYS.BROWSER_TOKEN, token);
+ }
+ const token = loadLocalData(AppConfig.KEYS.BROWSER_TOKEN);
+ const url = `${AppConfig.SOCKET_SERVER}/?token=${token}&channelId=${location.pathname.split('/')[2]}&role=host`;
+
+ const ws = useSocket(url);
+
+ useEffect(() => {
+ if (ws) {
+ ws.onmessage = event => {
+ const data = JSON.parse(event.data);
+ console.log(data);
+ if (data.type === 'init') {
+ // 기존 클라이언트들의 위치 초기화
+ const updatedLocations = data.clients.map((client: any, index: number) => {
+ const matchingGuest = channelInfo?.guests?.find(guest => guest.id === client.guestId);
+ return {
+ ...client,
+ color: matchingGuest?.markerStyle.color ?? markerDefaultColor[index],
+ };
+ });
+ setOtherLocations(updatedLocations);
+ } else if (data.type === 'location') {
+ // 새로 들어온 위치 업데이트
+ const matchingGuest = guestsData?.find(guest => guest.id === data.guestId);
+ const updatedLocation = {
+ guestId: data.guestId,
+ location: data.location,
+ token: data.token,
+ color: matchingGuest?.markerStyle.color ?? '#ffffff',
+ };
+
+ setOtherLocations(prev => {
+ const index = prev.findIndex(el => el.guestId === data.guestId);
+
+ if (index !== -1) {
+ const updatedLocations = [...prev];
+ updatedLocations[index] = updatedLocation;
+ return updatedLocations;
+ }
+ return [...prev, updatedLocation];
+ });
+ }
+ };
+ }
+ }, [ws, guestsData]);
const transformTypeGuestEntityToIGuest = (props: guestEntity): IGuest => {
return {
@@ -74,8 +135,6 @@ export const HostView = () => {
}, []);
useEffect(() => {
- const markerDefaultColor = ['#B4D033', '#22A751', '#2722A7', '#8F22A7', '#A73D22'];
-
if (channelInfo?.guests) {
const data: IGuestData[] = channelInfo.guests.map((guest, index) => ({
name: guest.name,
@@ -107,8 +166,21 @@ export const HostView = () => {
{/* eslint-disable-next-line no-nested-ternary */}
{lat && lng ? (
+ // eslint-disable-next-line no-nested-ternary
mapProps ? (
-
+ otherLocations ? (
+
+ ) : (
+
+ )
) : (
)
diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx
index 5ccbb926..0562fa9d 100644
--- a/frontend/src/pages/Main.tsx
+++ b/frontend/src/pages/Main.tsx
@@ -23,7 +23,7 @@ export const Main = () => {
setFooterActive,
resetFooterContext,
} = useContext(FooterContext);
- const { lat, lng, error } = getUserLocation();
+ const { lat, lng, alpha, error } = getUserLocation();
const [otherLocations, setOtherLocations] = useState([]);
const MIN_HEIGHT = 0.15;
const MAX_HEIGHT = 0.9;
@@ -65,7 +65,7 @@ export const Main = () => {
const ws = new WebSocket(`${AppConfig.SOCKET_SERVER}/?token=${token}`);
// 초기 위치 전송
ws.onopen = () => {
- ws.send(JSON.stringify({ type: 'location', location: { lat, lng } }));
+ ws.send(JSON.stringify({ type: 'location', location: { lat, lng, alpha } }));
};
ws.onmessage = event => {
const data = JSON.parse(event.data);
@@ -84,7 +84,8 @@ export const Main = () => {
return () => ws.close();
}
return undefined;
- }, [lat, lng]);
+ }, [lat, lng, alpha]);
+
const goToAddChannel = () => {
resetFooterContext();
resetUsers();
@@ -138,6 +139,7 @@ export const Main = () => {
height="100%"
lat={lat}
lng={lng}
+ alpha={alpha}
otherLocations={otherLocations}
/>
) : (