diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355df..f9e1cb00 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -40,3 +40,10 @@ .read-the-docs { color: #888; } + +.disable-drag { + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none +} diff --git a/frontend/src/api/client.api.ts b/frontend/src/api/client.api.ts index 78bdadb1..211489c8 100644 --- a/frontend/src/api/client.api.ts +++ b/frontend/src/api/client.api.ts @@ -1,4 +1,4 @@ -import { AppConfig } from '@/constants'; +import { AppConfig } from '@/lib/constants/commonConstants.ts'; import { loadLocalData } from '@/utils/common/manageLocalData.ts'; import axios from 'axios'; diff --git a/frontend/src/component/authmodal/AuthModal.tsx b/frontend/src/component/authmodal/AuthModal.tsx index b7f4b96e..e99edf4b 100644 --- a/frontend/src/component/authmodal/AuthModal.tsx +++ b/frontend/src/component/authmodal/AuthModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Modal } from '@/component/common/modal/Modal'; import { doLogin, doRegister } from '@/api/auth.api.ts'; import { saveLocalData } from '@/utils/common/manageLocalData.ts'; -import { AppConfig } from '@/constants.ts'; +import { AppConfig } from '@/lib/constants/commonConstants.ts'; export interface IAuthModalProps { /** 모달이 열려 있는지 여부를 나타냅니다. */ diff --git a/frontend/src/component/canvas/Canvas.tsx b/frontend/src/component/canvas/Canvas.tsx index c8f13db4..6b4e7a37 100644 --- a/frontend/src/component/canvas/Canvas.tsx +++ b/frontend/src/component/canvas/Canvas.tsx @@ -1,170 +1,245 @@ -import classNames from 'classnames'; -import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; -import { useDrawing } from '@/hooks/useDrawing.ts'; -import { usePanning } from '@/hooks/usePanning.ts'; -import { useZoom } from '@/hooks/useZoom.ts'; -import { MdArrowCircleLeft, MdArrowCircleRight } from 'react-icons/md'; -import { useUndoRedo } from '@/hooks/useUndoRedo.ts'; -import { ButtonState } from '@/component/common/enums.ts'; -import { useFloatingButton } from '@/hooks/useFloatingButton.ts'; -import { FloatingButton } from '@/component/common/floatingbutton/FloatingButton.tsx'; +// /* eslint-disable */ +// import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; +// import classNames from 'classnames'; +// import { ButtonState } from '@/component/common/enums'; +// import { useFloatingButton } from '@/hooks/useFloatingButton'; +// import { FloatingButton } from '@/component/common/floatingbutton/FloatingButton'; +// import { MdArrowCircleLeft, MdArrowCircleRight } from 'react-icons/md'; +// +// interface ICanvasProps { +// className?: string; +// onPointConverted?: (latLng: { lat: number; lng: number }) => { x: number; y: number } | null; +// onPointReverted?: (point: { x: number; y: number }) => { lat: number; lng: number } | null; +// } +// +// interface IPoint { +// x: number; +// y: number; +// latLng?: { lat: number; lng: number }; +// } +// +// interface ICanvasRefMethods { +// getCanvasElement: () => HTMLCanvasElement | null; +// setScale: (scale: number) => void; +// setPosition: (x: number, y: number) => void; +// clear: () => void; +// redraw: () => void; +// } +// +// // 선의 스타일 상수 +// const LINE_WIDTH = 2; +// const STROKE_STYLE = 'black'; +// const START_MARKER_COLOR = '#4CAF50'; +// const END_MARKER_COLOR = '#F44336'; +// const MARKER_RADIUS = 6; +// +// export const Canvas = forwardRef((props, ref) => { +// const canvasRef = useRef(null); +// const [points, setPoints] = useState([]); +// const [undoStack, setUndoStack] = useState([]); +// const [redoStack, setRedoStack] = useState([]); +// const [startPoint, setStartPoint] = useState(null); +// const [endPoint, setEndPoint] = useState(null); +// const { isMenuOpen, toolType, toggleMenu, handleMenuClick } = useFloatingButton(); +// +// // Transform state +// const scaleRef = useRef(1); +// const offsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); +// +// useEffect(() => { +// const canvas = canvasRef.current; +// if (!canvas) return; +// +// canvas.width = canvas.offsetWidth; +// canvas.height = canvas.offsetHeight; +// +// redraw(); +// }, []); +// +// const clear = () => { +// const canvas = canvasRef.current; +// const context = canvas?.getContext('2d'); +// if (!canvas || !context) return; +// +// context.clearRect(0, 0, canvas.width, canvas.height); +// }; +// +// const drawMarker = (context: CanvasRenderingContext2D, point: IPoint, color: string) => { +// const scaledRadius = MARKER_RADIUS / scaleRef.current; +// +// context.beginPath(); +// context.arc( +// (point.x - offsetRef.current.x) / scaleRef.current, +// (point.y - offsetRef.current.y) / scaleRef.current, +// scaledRadius, +// 0, +// 2 * Math.PI, +// ); +// context.fillStyle = color; +// context.fill(); +// context.strokeStyle = 'white'; +// context.lineWidth = 2 / scaleRef.current; +// context.stroke(); +// }; +// +// const redraw = () => { +// const canvas = canvasRef.current; +// const context = canvas?.getContext('2d'); +// if (!canvas || !context) return; +// +// clear(); +// +// // Set line style +// context.lineWidth = LINE_WIDTH / scaleRef.current; +// context.strokeStyle = STROKE_STYLE; +// context.lineCap = 'round'; +// context.lineJoin = 'round'; +// +// // Draw lines +// if (points.length > 0) { +// context.beginPath(); +// points.forEach((point, index) => { +// const x = (point.x - offsetRef.current.x) / scaleRef.current; +// const y = (point.y - offsetRef.current.y) / scaleRef.current; +// +// if (index === 0) { +// context.moveTo(x, y); +// } else { +// context.lineTo(x, y); +// } +// }); +// context.stroke(); +// } +// +// // Draw markers +// if (startPoint) { +// drawMarker(context, startPoint, START_MARKER_COLOR); +// } +// if (endPoint) { +// drawMarker(context, endPoint, END_MARKER_COLOR); +// } +// }; +// +// const addPoint = (point: IPoint) => { +// setPoints(prev => { +// const newPoints = [...prev, point]; +// setUndoStack(stack => [...stack, prev]); +// setRedoStack([]); +// return newPoints; +// }); +// }; +// +// const undo = () => { +// if (undoStack.length === 0) return; +// +// const previousPoints = undoStack[undoStack.length - 1]; +// setPoints(previousPoints); +// setUndoStack(stack => stack.slice(0, -1)); +// setRedoStack(stack => [...stack, points]); +// redraw(); +// }; +// +// const redo = () => { +// if (redoStack.length === 0) return; +// +// const nextPoints = redoStack[redoStack.length - 1]; +// setPoints(nextPoints); +// setRedoStack(stack => stack.slice(0, -1)); +// setUndoStack(stack => [...stack, points]); +// redraw(); +// }; +// +// const handleCanvasClick = (e: React.MouseEvent) => { +// const canvas = canvasRef.current; +// if (!canvas) return; +// +// const rect = canvas.getBoundingClientRect(); +// const x = (e.clientX - rect.left) * scaleRef.current + offsetRef.current.x; +// const y = (e.clientY - rect.top) * scaleRef.current + offsetRef.current.y; +// +// // Convert to lat/lng if converter is provided +// let latLng; +// if (props.onPointReverted) { +// latLng = props.onPointReverted({ x, y }); +// } +// +// // @ts-ignore +// const point: IPoint = { x, y, latLng }; +// +// switch (toolType) { +// case ButtonState.LINE_DRAWING: +// addPoint(point); +// break; +// case ButtonState.START_MARKER: +// setStartPoint(point); +// break; +// case ButtonState.DESTINATION_MARKER: +// setEndPoint(point); +// break; +// default: +// addPoint(point); +// break; +// } +// +// redraw(); +// }; +// +// useImperativeHandle(ref, () => ({ +// getCanvasElement: () => canvasRef.current, +// setScale: (scale: number) => { +// scaleRef.current = scale; +// redraw(); +// }, +// setPosition: (x: number, y: number) => { +// offsetRef.current = { x, y }; +// redraw(); +// }, +// clear, +// redraw, +// })); +// +// return ( +//
+//
+// +// +//
+// +// +// +//
+// +//
+//
+// ); +// }); -interface ICanvasProps { - className?: string; - // onClick?: () => void; - // onMouseDown?: () => void; - // onMouseUp?: () => void; - // onMouseMove?: () => void; -} - -interface IPoint { - x: number; - y: number; -} - -// 네이버 지도 기준 확대/축소 비율 단계 -const NAVER_STEP_SCALES = [ - 100, 100, 100, 100, 100, 100, 50, 30, 20, 10, 5, 3, 1, 0.5, 0.3, 0.1, 0.05, 0.03, 0.02, 0.01, - 0.005, -]; - -// 선의 굵기 상수 -const LINE_WIDTH = 2; -// 선의 색 상수 -const STROKE_STYLE = 'black'; -// 지도의 처음 확대/축소 비율 단계 index -const INITIAL_ZOOM_INDEX = 12; - -export interface ICanvasRefMethods { - getCanvasElement: () => HTMLCanvasElement | null; - onMouseClickHandler: (event: React.MouseEvent) => void; - onMouseDownHandler: (event: React.MouseEvent) => void; - onMouseMoveHandler: (event: React.MouseEvent) => void; - onMouseUpHandler: (event: React.MouseEvent) => void; -} - -export const Canvas = forwardRef((props, ref) => { - const canvasRef = useRef(null!); - const { points, addPoint, undo, redo, undoStack, redoStack } = useUndoRedo([]); - const [startPoint, setStartPoint] = useState(null); - const [endPoint, setEndPoint] = useState(null); - const { isMenuOpen, toolType, toggleMenu, handleMenuClick } = useFloatingButton(); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const context = canvas.getContext('2d'); - if (!context) return; - - canvas.width = canvas.offsetWidth; - canvas.height = canvas.offsetHeight; - }, []); - - const { draw, scaleRef, viewPosRef } = useDrawing({ - canvasRef, - points, - startPoint, - endPoint, - lineWidth: LINE_WIDTH, - strokeStyle: STROKE_STYLE, - initialScale: NAVER_STEP_SCALES[INITIAL_ZOOM_INDEX], - }); - - const { handleMouseMove, handleMouseDown, handleMouseUp } = usePanning({ viewPosRef, draw }); - const { handleWheel } = useZoom({ - scaleRef, - viewPosRef, - draw, - stepScales: NAVER_STEP_SCALES, - initialZoomIndex: INITIAL_ZOOM_INDEX, - }); - - const handleCanvasClick = (e: React.MouseEvent) => { - const rect = canvasRef.current?.getBoundingClientRect(); - if (!rect) return; - - const x = (e.clientX - rect.left - viewPosRef.current.x) / scaleRef.current; - const y = (e.clientY - rect.top - viewPosRef.current.y) / scaleRef.current; - - switch (toolType) { - case ButtonState.LINE_DRAWING: - addPoint({ x, y }); - break; - case ButtonState.START_MARKER: - setStartPoint({ x, y }); - break; - case ButtonState.DESTINATION_MARKER: - setEndPoint({ x, y }); - break; - default: - addPoint({ x, y }); - break; - } - - draw(); - }; - - useImperativeHandle(ref, () => ({ - getCanvasElement: () => canvasRef.current ?? null, - onMouseClickHandler: event => { - handleCanvasClick(event); - }, - onMouseDownHandler: event => { - handleMouseDown(event); - }, - onMouseUpHandler: () => { - handleMouseUp(); - }, - onMouseMoveHandler: event => { - handleMouseMove(event); - }, - })); - - return ( -
-
- - -
- -
- -
-
- ); -}); diff --git a/frontend/src/component/canvas/CanvasWithMap.tsx b/frontend/src/component/canvas/CanvasWithMap.tsx index 50dcebae..0f12a458 100644 --- a/frontend/src/component/canvas/CanvasWithMap.tsx +++ b/frontend/src/component/canvas/CanvasWithMap.tsx @@ -1,65 +1,114 @@ -import { useRef, useEffect, useState } from 'react'; -import classNames from 'classnames'; -import { Canvas, ICanvasRefMethods } from '@/component/canvas/Canvas.tsx'; -import { Map } from '@/component/maps/Map.tsx'; -import { IMapObject, IMapRefMethods } from '@/component/maps/Map.types.ts'; -import { useEventHandlers } from '@/component/canvas/useEventHandlers.tsx'; - -interface ICanvasWithMapProps { - className?: string; - lat: number; - lng: number; - zoom: number; - mapType: string; - allowCanvas?: boolean; -} - -export const CanvasWithMap = (props: ICanvasWithMapProps) => { - const mapRefMethods = useRef(null); - const canvasRefMethods = useRef(null); - const mapElement = useRef(null); - const canvasElement = useRef(null); - - const [mapObject, setMapObject] = useState(null); - - useEffect(() => { - if (canvasRefMethods.current?.getCanvasElement) - canvasElement.current = canvasRefMethods.current.getCanvasElement(); - }, []); - - useEffect(() => { - mapElement.current = mapRefMethods.current?.getMapContainer() ?? null; - }, [mapObject]); - - const { handleClick, handleMouseDown, handleMouseMove, handleMouseUp } = useEventHandlers( - canvasElement.current, - canvasRefMethods.current, - mapElement.current, - mapRefMethods.current, - mapObject, - ); - - const initMap = (mapObj: IMapObject | null) => { - setMapObject(mapObj); - }; - - return ( -
- {props.allowCanvas && } - -
- ); -}; +// import { Canvas, ICanvasRefMethods } from '@/component/canvas/Canvas.tsx'; +// import { Map, IMapRefMethods } from '@/component/maps/Map.tsx'; +// import classNames from 'classnames'; +// import { ICanvasVertex } from '@/utils/screen/canvasUtils.ts'; +// import { INaverMapVertexPosition } from '@/component/maps/naverMapUtils.ts'; +// import { useRef, useEffect, useState } from 'react'; +// +// interface ICanvasWithMapProps { +// className?: string; +// lat: number; +// lng: number; +// zoom: number; +// mapType: string; +// allowCanvas?: boolean; +// } +// +// interface IMouseEventState { +// isMouseDown: boolean; +// mouseDownPosition: { x: number; y: number }; +// // mouseMovePosition: { x: number; y: number }; +// mouseDeltaPosition: { x: number; y: number }; +// } +// +// export interface ILocationObject { +// canvas: ICanvasVertex; +// map: INaverMapVertexPosition; +// } +// +// const MouseEventStateInitialValue = { +// isMouseDown: false, +// mouseDownPosition: { x: 0, y: 0 }, +// // mouseMovePosition: { x: 0, y: 0 }, +// mouseDeltaPosition: { x: 0, y: 0 }, +// }; +// +// export const CanvasWithMap = (props: ICanvasWithMapProps) => { +// const mapRefMethods = useRef(null); +// const mapElement = useRef(null); +// const canvasRefMethods = useRef(null); +// const canvasElement = useRef(null); +// const mouseEventState = useRef({ ...MouseEventStateInitialValue }); +// const [mapObject, setMapObject] = useState(null); +// +// useEffect(() => { +// if (canvasRefMethods.current?.getCanvasElement) +// canvasElement.current = canvasRefMethods.current.getCanvasElement(); +// }, []); +// +// useEffect(() => { +// mapElement.current = mapRefMethods.current?.getMapContainer() ?? null; +// }, [mapObject]); +// +// const initMap = (mapObj: naver.maps.Map | null) => { +// setMapObject(mapObj); +// }; +// +// const handleClick = (event: React.MouseEvent) => { +// mapRefMethods.current?.onMouseClickHandler(event); +// canvasRefMethods.current?.onMouseClickHandler(event); +// }; +// +// const handleMouseDown = (event: React.MouseEvent) => { +// if (!mapElement.current || !canvasElement.current) return; +// mouseEventState.current.isMouseDown = true; +// mouseEventState.current.mouseDownPosition = { x: event.clientX, y: event.clientY }; +// canvasRefMethods.current?.onMouseDownHandler(event); +// }; +// +// const handleMouseMove = (event: React.MouseEvent) => { +// if (!mapElement.current || !canvasElement.current || !mouseEventState.current.isMouseDown) +// return; +// +// // TODO: 쓰로틀링 걸기 +// mouseEventState.current.mouseDeltaPosition = { +// x: -(event.clientX - mouseEventState.current.mouseDownPosition.x), +// y: -(event.clientY - mouseEventState.current.mouseDownPosition.y), +// }; +// +// mapObject?.panBy( +// new naver.maps.Point( +// mouseEventState.current.mouseDeltaPosition.x, +// mouseEventState.current.mouseDeltaPosition.y, +// ), +// ); +// +// canvasRefMethods.current?.onMouseMoveHandler(event); +// }; +// +// const handleMouseUp = (event: React.MouseEvent) => { +// if (!mapElement.current || !canvasElement.current) return; +// mouseEventState.current = { ...MouseEventStateInitialValue }; +// canvasRefMethods.current?.onMouseUpHandler(event); +// }; +// +// return ( +//
+// {props.allowCanvas && } +// +//
+// ); +// }; \ No newline at end of file diff --git a/frontend/src/component/canvas/useEventHandlers.tsx b/frontend/src/component/canvas/useEventHandlers.tsx index 23191544..1e7a3d7e 100644 --- a/frontend/src/component/canvas/useEventHandlers.tsx +++ b/frontend/src/component/canvas/useEventHandlers.tsx @@ -1,86 +1,86 @@ -import { useRef } from 'react'; -import { ICanvasRefMethods } from '@/component/canvas/Canvas.tsx'; -import { IMapObject, IMapRefMethods } from '@/component/maps/Map.types.ts'; - -// TODO: 리팩토룅 시 null을 처리하기 -interface IUseEventHandlers { - ( - canvasElement: HTMLCanvasElement | null, - canvasRefMethods: ICanvasRefMethods | null, - mapElement: HTMLElement | null, - mapRefMethods: IMapRefMethods | null, - mapObject: IMapObject | null, // 비동기 로딩 시 null로 처리가 될 수 있어서 예외처리 필요 - ): { - handleClick: (event: React.MouseEvent) => void; - handleMouseDown: (event: React.MouseEvent) => void; - handleMouseMove: (event: React.MouseEvent) => void; - handleMouseUp: (event: React.MouseEvent) => void; - }; -} - -interface IMouseEventState { - isMouseDown: boolean; - mouseDownPosition: { x: number; y: number }; - // mouseMovePosition: { x: number; y: number }; - mouseDeltaPosition: { x: number; y: number }; -} - -const MouseEventStateInitialValue = { - isMouseDown: false, - mouseDownPosition: { x: 0, y: 0 }, - // mouseMovePosition: { x: 0, y: 0 }, - mouseDeltaPosition: { x: 0, y: 0 }, -}; - -export const useEventHandlers: IUseEventHandlers = ( - canvasElement, - canvasRefMethods, - mapElement, - mapRefMethods, - mapObject, -) => { - // if (!canvasElement || !canvasElement || !mapElement || !mapRefMethods || !mapObject) - // throw new Error('🚀 useEventHandler error : null 값이 포함되어 있습니다.'); - - const mouseEventState = useRef({ ...MouseEventStateInitialValue }); - - const handleClick = (event: React.MouseEvent) => { - mapRefMethods?.onMouseClickHandler(event); - canvasRefMethods?.onMouseClickHandler(event); - }; - - const handleMouseDown = (event: React.MouseEvent) => { - if (!mapElement || !canvasElement) return; - mouseEventState.current.isMouseDown = true; - mouseEventState.current.mouseDownPosition = { x: event.clientX, y: event.clientY }; - canvasRefMethods?.onMouseDownHandler(event); - }; - - const handleMouseMove = (event: React.MouseEvent) => { - if (!mapElement || !canvasElement || !mouseEventState.current.isMouseDown) return; - - // TODO: 쓰로틀링 걸기 - mouseEventState.current.mouseDeltaPosition = { - x: -(event.clientX - mouseEventState.current.mouseDownPosition.x), - y: -(event.clientY - mouseEventState.current.mouseDownPosition.y), - }; - - // TODO: 범용 지도에 따른 Refactoring 필요, 우선은 네이버 지도에 한해서만 수행 - mapObject?.panBy( - new naver.maps.Point( - mouseEventState.current.mouseDeltaPosition.x, - mouseEventState.current.mouseDeltaPosition.y, - ), - ); - - canvasRefMethods?.onMouseMoveHandler(event); - }; - - const handleMouseUp = (event: React.MouseEvent) => { - if (!mapElement || !canvasElement) return; - mouseEventState.current = { ...MouseEventStateInitialValue }; - canvasRefMethods?.onMouseUpHandler(event); - }; - - return { handleClick, handleMouseDown, handleMouseMove, handleMouseUp }; -}; +// import { useRef } from 'react'; +// import { ICanvasRefMethods } from '@/component/canvas/Canvas.tsx'; +// import { IMapObject, IMapRefMethods } from '@/component/maps/Map.types.ts'; +// +// // TODO: 리팩토룅 시 null을 처리하기 +// interface IUseEventHandlers { +// ( +// canvasElement: HTMLCanvasElement | null, +// canvasRefMethods: ICanvasRefMethods | null, +// mapElement: HTMLElement | null, +// mapRefMethods: IMapRefMethods | null, +// mapObject: IMapObject | null, // 비동기 로딩 시 null로 처리가 될 수 있어서 예외처리 필요 +// ): { +// handleClick: (event: React.MouseEvent) => void; +// handleMouseDown: (event: React.MouseEvent) => void; +// handleMouseMove: (event: React.MouseEvent) => void; +// handleMouseUp: (event: React.MouseEvent) => void; +// }; +// } +// +// interface IMouseEventState { +// isMouseDown: boolean; +// mouseDownPosition: { x: number; y: number }; +// // mouseMovePosition: { x: number; y: number }; +// mouseDeltaPosition: { x: number; y: number }; +// } +// +// const MouseEventStateInitialValue = { +// isMouseDown: false, +// mouseDownPosition: { x: 0, y: 0 }, +// // mouseMovePosition: { x: 0, y: 0 }, +// mouseDeltaPosition: { x: 0, y: 0 }, +// }; +// +// export const useEventHandlers: IUseEventHandlers = ( +// canvasElement, +// canvasRefMethods, +// mapElement, +// mapRefMethods, +// mapObject, +// ) => { +// // if (!canvasElement || !canvasElement || !mapElement || !mapRefMethods || !mapObject) +// // throw new Error('🚀 useEventHandler error : null 값이 포함되어 있습니다.'); +// +// const mouseEventState = useRef({ ...MouseEventStateInitialValue }); +// +// const handleClick = (event: React.MouseEvent) => { +// mapRefMethods?.onMouseClickHandler(event); +// canvasRefMethods?.onMouseClickHandler(event); +// }; +// +// const handleMouseDown = (event: React.MouseEvent) => { +// if (!mapElement || !canvasElement) return; +// mouseEventState.current.isMouseDown = true; +// mouseEventState.current.mouseDownPosition = { x: event.clientX, y: event.clientY }; +// canvasRefMethods?.onMouseDownHandler(event); +// }; +// +// const handleMouseMove = (event: React.MouseEvent) => { +// if (!mapElement || !canvasElement || !mouseEventState.current.isMouseDown) return; +// +// // TODO: 쓰로틀링 걸기 +// mouseEventState.current.mouseDeltaPosition = { +// x: -(event.clientX - mouseEventState.current.mouseDownPosition.x), +// y: -(event.clientY - mouseEventState.current.mouseDownPosition.y), +// }; +// +// // TODO: 범용 지도에 따른 Refactoring 필요, 우선은 네이버 지도에 한해서만 수행 +// mapObject?.panBy( +// new naver.maps.Point( +// mouseEventState.current.mouseDeltaPosition.x, +// mouseEventState.current.mouseDeltaPosition.y, +// ), +// ); +// +// canvasRefMethods?.onMouseMoveHandler(event); +// }; +// +// const handleMouseUp = (event: React.MouseEvent) => { +// if (!mapElement || !canvasElement) return; +// mouseEventState.current = { ...MouseEventStateInitialValue }; +// canvasRefMethods?.onMouseUpHandler(event); +// }; +// +// return { handleClick, handleMouseDown, handleMouseMove, handleMouseUp }; +// }; diff --git a/frontend/src/component/content/Content.tsx b/frontend/src/component/content/Content.tsx index 615d58d7..3ac6cf9a 100644 --- a/frontend/src/component/content/Content.tsx +++ b/frontend/src/component/content/Content.tsx @@ -1,4 +1,5 @@ import { MdGroup, MdMoreVert } from 'react-icons/md'; +import { useNavigate } from 'react-router-dom'; interface IContentProps { title: string; @@ -29,9 +30,12 @@ interface IContentProps { */ export const Content = (props: IContentProps) => { + const navigate = useNavigate(); return ( - { + navigate(props.link); + }} className="relative flex w-full flex-row items-center justify-between px-4 py-5" >
@@ -53,6 +57,6 @@ export const Content = (props: IContentProps) => { {/* {isMenuOpen && (드롭다운 메뉴)} */}
-
+ ); }; diff --git a/frontend/src/component/maps/Canvas.tsx b/frontend/src/component/maps/Canvas.tsx new file mode 100644 index 00000000..0d07fd8c --- /dev/null +++ b/frontend/src/component/maps/Canvas.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import { MapCanvas } from '@/component/maps/MapCanvas.tsx'; +import { DEFAULT_CENTER } from '@/lib/constants/mapConstants.ts'; + +export const FullScreenMap = () => { + const [windowSize, setWindowSize] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + + useEffect(() => { + const handleResize = () => { + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + window.addEventListener('resize', handleResize); + + window.addEventListener('orientationchange', () => { + setTimeout(handleResize, 100); + }); + + return () => { + window.removeEventListener('resize', handleResize); + window.removeEventListener('orientationchange', handleResize); + }; + }, []); + + useEffect(() => { + const preventDefault = (e: TouchEvent) => { + e.preventDefault(); + }; + + document.body.style.overflow = 'hidden'; + document.addEventListener('touchmove', preventDefault, { passive: false }); + + return () => { + document.body.style.overflow = 'auto'; + document.removeEventListener('touchmove', preventDefault); + }; + }, []); + + return ( +
+ +
+ ); +}; diff --git a/frontend/src/component/maps/Map.tsx b/frontend/src/component/maps/Map.tsx index b042d038..016ba5e8 100644 --- a/frontend/src/component/maps/Map.tsx +++ b/frontend/src/component/maps/Map.tsx @@ -1,71 +1,80 @@ -import { ReactNode, useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'; -import { NaverMap } from '@/component/maps/NaverMap.tsx'; -import { IMapObject, IMapOptions, IMapRefMethods } from '@/component/maps/Map.types.ts'; -import classNames from 'classnames'; - -const validateKindOfMap = (type: string) => ['naver'].includes(type); - -interface IMapProps extends IMapOptions { - className?: string; - type: string; - initMap: (mapObject: IMapObject) => void; -} - -export const Map = forwardRef((props, ref) => { - if (!validateKindOfMap(props.type)) - throw new Error('🚀 지도 로딩 오류 : 알 수 없는 지도 타입이 인자로 들어 왔습니다.'); - - const mapRefMethods = useRef(null); - const mapContainer = useRef(null); - - const [mapObject, setMapObject] = useState(); - const [MapComponent, setMapComponent] = useState(); - - const onMapInit = (mapObj: IMapObject) => { - setMapObject(mapObj); - }; - - useEffect(() => { - if (props.type === 'naver') { - const mapComponent = ( - - ); - setMapComponent(mapComponent); - } - }, []); - - useEffect(() => { - mapContainer.current = mapRefMethods.current?.getMapContainer() ?? null; - if (mapObject) props.initMap(mapObject); - }, [mapObject]); - - useImperativeHandle(ref, () => ({ - getMapObject: () => { - if (mapObject) return mapObject; - throw new Error('🚀 지도 로딩 오류 : 지도 객체가 존재하지 않습니다.'); - }, - getMapContainer: () => mapContainer.current, - onMouseClickHandler: () => {}, - })); - - return ( -
- {MapComponent} -
- ); -}); +// import { NaverMap } from '@/component/maps/NaverMap.tsx'; +// import { ReactNode, useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'; +// import classNames from 'classnames'; +// +// type IMapObject = naver.maps.Map | null; +// +// export interface IMapOptions { +// lat: number; +// lng: number; +// zoom?: number; +// } +// +// interface IMapProps extends IMapOptions { +// className?: string; +// type: string; +// initMap: (mapObject: IMapObject) => void; +// } +// +// // 부모 컴포넌트가 접근할 수 있는 메서드들을 정의한 인터페이스 +// export interface IMapRefMethods { +// getMapObject: () => naver.maps.Map | null; +// getMapContainer: () => HTMLElement | null; +// onMouseClickHandler: (event?: React.MouseEvent) => void; +// } +// +// const validateKindOfMap = (type: string) => ['naver'].includes(type); +// +// export const Map = forwardRef((props, ref) => { +// if (!validateKindOfMap(props.type)) throw new Error('Invalid map type'); +// +// const mapRef = useRef(null); +// const mapContainer = useRef(null); +// const [mapObject, setMapObject] = useState(null); +// const [MapComponent, setMapComponent] = useState(); +// +// const onMapInit = (mapObj: IMapObject) => { +// setMapObject(mapObj); +// }; +// +// useEffect(() => { +// if (props.type === 'naver') { +// const mapComponent = ( +// +// ); +// setMapComponent(mapComponent); +// } +// }, [props.lat, props.lng, props.zoom, props.type]); +// +// useEffect(() => { +// mapContainer.current = mapRef.current?.getMapContainer() ?? null; +// props.initMap(mapObject); +// }, [mapObject]); +// +// useImperativeHandle(ref, () => ({ +// getMapObject: () => mapObject, +// getMapContainer: () => mapContainer.current, +// onMouseClickHandler: () => {}, +// })); +// +// return ( +//
+// {MapComponent} +//
+// ); +// }); \ No newline at end of file diff --git a/frontend/src/component/maps/MapCanvas.tsx b/frontend/src/component/maps/MapCanvas.tsx new file mode 100644 index 00000000..0d84bbe2 --- /dev/null +++ b/frontend/src/component/maps/MapCanvas.tsx @@ -0,0 +1,466 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { ButtonState } from '@/component/common/enums'; +import classNames from 'classnames'; +import { MdArrowCircleLeft, MdArrowCircleRight } from 'react-icons/md'; +import { FloatingButton } from '@/component/common/floatingbutton/FloatingButton.tsx'; +import { useFloatingButton } from '@/hooks/useFloatingButton.ts'; +import { + END_MARKER_COLOR, + LINE_WIDTH, + START_MARKER_COLOR, + STROKE_STYLE, +} from '@/lib/constants/canvasConstants.ts'; +import { ICanvasPoint, IMapCanvasProps, IPoint } from '@/lib/types/canvasInterface.ts'; + +export const MapCanvas = ({ width, height, initialCenter, initialZoom }: IMapCanvasProps) => { + const mapRef = useRef(null); + const canvasRef = useRef(null); + const [projection, setProjection] = useState(null); + + const [map, setMap] = useState(null); + + const [startMarker, setStartMarker] = useState(null); + const [endMarker, setEndMarker] = useState(null); + const [pathPoints, setPathPoints] = useState([]); + + const [isDragging, setIsDragging] = useState(false); + const [dragStartPos, setDragStartPos] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + const [dragStartTime, setDragStartTime] = useState(null); + + const [isTouching, setIsTouching] = useState(false); + const [isTouchZooming, setIsTouchZooming] = useState(false); + const [touchStartDistance, setTouchStartDistance] = useState(null); + const [touchCenter, setTouchCenter] = useState<{ x: number; y: number } | null>(null); + + const [undoStack, setUndoStack] = useState([]); + const [redoStack, setRedoStack] = useState([]); + + const { isMenuOpen, toolType, toggleMenu, handleMenuClick } = useFloatingButton(); + + useEffect(() => { + if (!mapRef.current) return; + + const mapInstance = new naver.maps.Map(mapRef.current, { + center: new naver.maps.LatLng(initialCenter.lat, initialCenter.lng), + zoom: initialZoom, + minZoom: 7, + maxBounds: new naver.maps.LatLngBounds( + new naver.maps.LatLng(33.0, 124.5), + new naver.maps.LatLng(38.9, 131.9), + ), + }); + + setMap(mapInstance); + setProjection(mapInstance.getProjection()); + + // TODO: 필요 없을 것으로 예상, 혹시나해서 남겨둔 것이니 필요 없다 판단되면 제거 필요 + // naver.maps.Event.addListener(mapInstance, 'zoom_changed', () => { + // setProjection(mapInstance.getProjection()); + // updateCanvasSize(); + // redrawCanvas(); + // }); + // + // naver.maps.Event.addListener(mapInstance, 'center_changed', () => { + // setProjection(mapInstance.getProjection()); + // redrawCanvas(); + // }); + + // eslint-disable-next-line consistent-return + return () => { + mapInstance.destroy(); + }; + }, []); + + const latLngToCanvasPoint = (latLng: IPoint): ICanvasPoint | null => { + if (!map || !projection || !canvasRef.current) return null; + const coord = projection.fromCoordToOffset(new naver.maps.LatLng(latLng.lat, latLng.lng)); + const mapSize = map.getSize(); + const mapCenter = map.getCenter(); + const centerPoint = projection.fromCoordToOffset(mapCenter); + return { + x: coord.x - (centerPoint.x - mapSize.width / 2), + y: coord.y - (centerPoint.y - mapSize.height / 2), + }; + }; + + const canvasPointToLatLng = (point: ICanvasPoint): IPoint | null => { + if (!map || !projection || !canvasRef.current) return null; + const mapSize = map.getSize(); + const mapCenter = map.getCenter(); + const centerPoint = projection.fromCoordToOffset(mapCenter); + const coordPoint = new naver.maps.Point( + point.x + (centerPoint.x - mapSize.width / 2), + point.y + (centerPoint.y - mapSize.height / 2), + ); + const latLng = projection.fromOffsetToCoord(coordPoint); + return { + lat: latLng.y, + lng: latLng.x, + }; + }; + + const updateCanvasSize = () => { + if (!map || !canvasRef.current) return; + const mapSize = map.getSize(); + const canvas = canvasRef.current; + canvas.width = mapSize.width; + canvas.height = mapSize.height; + canvas.style.width = `${mapSize.width}px`; + canvas.style.height = `${mapSize.height}px`; + }; + + const redrawCanvas = () => { + if (!canvasRef.current || !map) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.lineWidth = LINE_WIDTH / map.getZoom(); + ctx.strokeStyle = STROKE_STYLE; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + if (startMarker) { + const startPoint = latLngToCanvasPoint(startMarker); + if (startPoint) { + ctx.beginPath(); + ctx.arc(startPoint.x, startPoint.y, 10, 0, 2 * Math.PI); + ctx.fillStyle = START_MARKER_COLOR; + ctx.fill(); + } + } + if (endMarker) { + const endPoint = latLngToCanvasPoint(endMarker); + if (endPoint) { + ctx.beginPath(); + ctx.arc(endPoint.x, endPoint.y, 10, 0, 2 * Math.PI); + ctx.fillStyle = END_MARKER_COLOR; + ctx.fill(); + } + } + if (pathPoints.length > 0) { + ctx.beginPath(); + const firstPoint = latLngToCanvasPoint(pathPoints[0]); + + if (firstPoint) { + ctx.moveTo(firstPoint.x, firstPoint.y); + for (let i = 1; i < pathPoints.length; i++) { + const point = latLngToCanvasPoint(pathPoints[i]); + if (point) { + ctx.lineTo(point.x, point.y); + } + } + ctx.stroke(); + } + } + }; + + const addPoint = (point: IPoint) => { + setUndoStack(stack => [...stack, [...pathPoints, point]]); + setRedoStack([]); + setPathPoints(prev => { + return [...prev, point]; + }); + }; + + const handleCanvasClick = (e: React.MouseEvent) => { + if (!map || !canvasRef.current) return; + const canvas = canvasRef.current; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const clickedPoint = canvasPointToLatLng({ x, y }); + + if (!clickedPoint) return; + switch (toolType) { + case ButtonState.START_MARKER: + setStartMarker(clickedPoint); + break; + case ButtonState.DESTINATION_MARKER: + setEndMarker(clickedPoint); + break; + case ButtonState.LINE_DRAWING: + addPoint(clickedPoint); + break; + default: + break; + } + redrawCanvas(); + }; + + // TODO: 줌인 줌아웃 버튼으로도 접근 가능하도록 추가 + // const handleZoomChange = (zoomChange: number) => { + // if (!map) return; + // const currentZoom = map.getZoom(); + // map.setZoom(currentZoom + zoomChange); + // redrawCanvas(); + // }; + + const handleWheel = (e: React.WheelEvent) => { + if (!map) return; + + const zoomChange = e.deltaY < 0 ? 1 : -1; + + const currentZoom = map.getZoom(); + map.setZoom(currentZoom + zoomChange); + + redrawCanvas(); + }; + + // TODO: 줌인 줌아웃 버튼으로도 접근 가능하도록 추가 + // const handleMapPan = (direction: 'up' | 'down' | 'left' | 'right') => { + // if (!map) return; + // const moveAmount = 100; + // let point: naver.maps.Point; + // + // switch (direction) { + // case 'up': + // point = new naver.maps.Point(0, -moveAmount); + // break; + // case 'down': + // point = new naver.maps.Point(0, moveAmount); + // break; + // case 'left': + // point = new naver.maps.Point(-moveAmount, 0); + // break; + // case 'right': + // point = new naver.maps.Point(moveAmount, 0); + // break; + // default: + // return; + // } + // + // map.panBy(point); + // redrawCanvas(); + // }; + + /** + * @description 마우스 클릭을 시작했을 때 이벤트 (onMouseDown) + */ + const handleMouseDown = (e: React.MouseEvent) => { + if (!map || !canvasRef.current) return; + + setDragStartTime(Date.now()); + const rect = canvasRef.current.getBoundingClientRect(); + setDragStartPos({ x: e.clientX - rect.left, y: e.clientY - rect.top }); + }; + /** + * @description 마우스가 움직일 때 이벤트 (onMouseMove) + */ + const handleMouseMove = () => { + if (!dragStartTime) return; + + // TODO: 클릭 후 0.3초 이상이 경과했으면 dragging 시작, 이동 관련 로직 개선 필요 + const timeElapsed = Date.now() - dragStartTime; + if (timeElapsed > 300 && !isDragging) { + setIsDragging(true); + } + + if (isDragging) { + redrawCanvas(); + } + }; + /** + * @description 마우스를 손에서 떼서 클릭이 끝났을 때 이벤트 (onMouseUp) + */ + const handleMouseUp = () => { + setIsDragging(false); + setDragStartTime(null); + }; + + /** + * @description 터치 시작될 때 이벤트 (onTouchStart) + */ + const handleTouchStart = (e: React.TouchEvent) => { + if (e.touches.length === 2) { + setIsTouchZooming(true); + + const distance = Math.sqrt( + (e.touches[0].clientX - e.touches[1].clientX) ** 2 + + (e.touches[0].clientY - e.touches[1].clientY) ** 2, + ); + + setTouchStartDistance(distance); + + const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2; + const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2; + setTouchCenter({ x: centerX, y: centerY }); + } else if (e.touches.length === 1) { + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + + setDragStartPos({ + x: e.touches[0].clientX - rect.left, + y: e.touches[0].clientY - rect.top, + }); + setIsTouching(true); + } + }; + + /** + * @description 터치한 채로 화면을 움직일 (끌어당길) 때 이벤트 (onTouchMove) + */ + const handleTouchMove = (e: React.TouchEvent) => { + if (isTouchZooming && e.touches.length === 2 && touchStartDistance) { + const newDistance = Math.sqrt( + (e.touches[0].clientX - e.touches[1].clientX) ** 2 + + (e.touches[0].clientY - e.touches[1].clientY) ** 2, + ); + + const zoomChange = (newDistance - touchStartDistance) / 30; // TODO: 스케일링 비율 조정 + const currentZoom = map?.getZoom() ?? 10; + + map?.setOptions({ zoomOrigin: touchCenter }); + map?.setZoom(currentZoom + zoomChange); + + setTouchStartDistance(newDistance); + } else if (isTouching && e.touches.length === 1) { + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + + const newX = e.touches[0].clientX - rect.left; + const newY = e.touches[0].clientY - rect.top; + + const deltaX = dragStartPos.x - newX; + const deltaY = dragStartPos.y - newY; + + map?.panBy(new naver.maps.Point(deltaX, deltaY)); + setDragStartPos({ x: newX, y: newY }); + } + redrawCanvas(); + }; + + /** + * @description 화면에서 터치를 종료할 때 (손을 뗐을 때) 이벤트 (onTouchEnd) + */ + const handleTouchEnd = (e: React.TouchEvent) => { + if (e.touches.length === 0) { + setIsTouchZooming(false); + setTouchStartDistance(null); + setTouchCenter(null); + setIsTouching(false); + } + }; + + const undo = () => { + if (undoStack.length === 0) return; + const previousPoints = undoStack[undoStack.length - 2]; + setPathPoints(previousPoints); + setUndoStack(stack => stack.slice(0, -1)); + setRedoStack(stack => [...stack, pathPoints]); + redrawCanvas(); + }; + + const redo = () => { + if (redoStack.length === 0) return; + const nextPoints = redoStack[redoStack.length - 1]; + setPathPoints(nextPoints); + setRedoStack(stack => stack.slice(0, -1)); + setUndoStack(stack => [...stack, pathPoints]); + redrawCanvas(); + }; + + useEffect(() => { + if (isDragging) { + if (canvasRef.current) { + canvasRef.current.style.pointerEvents = 'none'; + } + redrawCanvas(); + } else if (canvasRef.current) { + canvasRef.current.style.pointerEvents = 'auto'; + } + }, [isDragging]); + + useEffect(() => { + if (!canvasRef.current || !map) return; + updateCanvasSize(); + }, [map]); + + useEffect(() => { + redrawCanvas(); + }, [startMarker, endMarker, pathPoints, map, undoStack, redoStack]); + + return ( +
+
+
+ + +
+ +
+ +
+ + {/* TODO: 줌인 줌아웃 버튼으로도 접근 가능하도록 추가 */} + {/*
*/} + {/*
*/} + {/* {isTouchZooming ? 'true' : 'false'} {touchStartDistance} */} + {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
*/} +
+ ); +}; diff --git a/frontend/src/component/maps/NaverMap.tsx b/frontend/src/component/maps/NaverMap.tsx index 0f9049d0..2718bdfa 100644 --- a/frontend/src/component/maps/NaverMap.tsx +++ b/frontend/src/component/maps/NaverMap.tsx @@ -1,47 +1,40 @@ -import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; -import { setNaverMapSync } from '@/component/maps/naverMapUtils.ts'; -import { IMapOptions, IMapRefMethods } from '@/component/maps/Map.types.ts'; - -interface INaverMapProps extends IMapOptions { - onMapInit: (map: naver.maps.Map) => void; // 콜백 프로퍼티 추가 -} - -export const NaverMap = forwardRef((props, ref) => { - const mapObject = useRef(null); - const mapContainer = useRef(null); - - const [mapOptions, setMapOptions] = useState({ - lat: props.lat, - lng: props.lng, - zoom: props.zoom, - }); - - useEffect(() => { - setMapOptions({ - lat: props.lat, - lng: props.lng, - zoom: props.zoom, - }); - }, [props.lat, props.lng, props.zoom]); - - useEffect(() => { - if (mapContainer.current && mapOptions) { - mapObject.current = setNaverMapSync(mapContainer.current, mapOptions); - if (mapObject.current) props.onMapInit(mapObject.current); // 콜백 호출 - } - }, [mapOptions]); - - useImperativeHandle(ref, () => ({ - getMapObject: () => { - if (mapObject) return mapObject.current; - throw new Error('🚀 지도 로딩 오류 : 지도 객체가 존재하지 않습니다.'); - }, - getMapContainer: () => { - if (mapContainer) return mapContainer.current; - throw new Error('🚀 지도 로딩 오류 : 지도 컨테이너가 존재하지 않습니다.'); - }, - onMouseClickHandler: () => {}, - })); - - return
; -}); +// import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; +// import { setNaverMapSync } from '@/component/maps/naverMapUtils.ts'; +// import { IMapOptions, IMapRefMethods } from '@/component/maps/Map.tsx'; +// +// interface INaverMapProps extends IMapOptions { +// onMapInit: (map: naver.maps.Map) => void; // 콜백 프로퍼티 추가 +// } +// +// export const NaverMap = forwardRef((props, ref) => { +// const naverMapObject = useRef(null); +// const naverMapContainer = useRef(null); +// const [mapOptions, setMapOptions] = useState({ +// lat: props.lat, +// lng: props.lng, +// zoom: props.zoom, +// }); +// +// useEffect(() => { +// setMapOptions({ +// lat: props.lat, +// lng: props.lng, +// zoom: props.zoom, +// }); +// }, [props.lat, props.lng, props.zoom]); +// +// useEffect(() => { +// if (naverMapContainer.current && mapOptions !== null) { +// naverMapObject.current = setNaverMapSync(naverMapContainer.current, mapOptions); +// if (naverMapObject.current !== null) props.onMapInit(naverMapObject.current); // 콜백 호출 +// } +// }, [mapOptions]); +// +// useImperativeHandle(ref, () => ({ +// getMapObject: () => naverMapObject.current, +// getMapContainer: () => naverMapContainer.current, +// onMouseClickHandler: () => {}, +// })); +// +// return
; +// }); diff --git a/frontend/src/component/maps/NaverMapSample.tsx b/frontend/src/component/maps/NaverMapSample.tsx index 25f75780..eff3a603 100644 --- a/frontend/src/component/maps/NaverMapSample.tsx +++ b/frontend/src/component/maps/NaverMapSample.tsx @@ -1,103 +1,141 @@ -import { useEffect, useRef } from 'react'; -import { setNaverMapSync } from '@/component/maps/naverMapUtils'; - -export interface INaverMapOptions { - lat: number; - lng: number; - zoom?: number; -} -interface INaverMapProps extends INaverMapOptions { - otherLocations?: Array<{ location: { lat: number; lng: number }; token: string }>; -} - -export const NaverMap = (props: INaverMapProps) => { - const mapRef = useRef(null); - const mapInstanceRef = useRef(null); - const markersRef = useRef>(new Map()); - const userMarkerRef = useRef(null); - - // TODO: 사용자 순서 별로 색상 정하기 (util 함수로 빼기) - const getMarkerColor = (token: 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 mapOptions: INaverMapOptions = { - lat: props.lat, - lng: props.lng, - zoom: props.zoom, - }; - - useEffect(() => { - if (mapRef.current) { - const map = setNaverMapSync(mapRef.current, mapOptions); - mapInstanceRef.current = map; - } - }, []); - - useEffect(() => { - if (mapInstanceRef.current && props.otherLocations) { - const map = mapInstanceRef.current; - - // 업데이트된 마커 관리 - const existingMarkers = markersRef.current; - - props.otherLocations.forEach(({ location, token }) => { - const markerColor = getMarkerColor(token); - if (existingMarkers.has(token)) { - // 기존 마커 위치 업데이트 - const marker = existingMarkers.get(token); - marker?.setPosition(new naver.maps.LatLng(location.lat, location.lng)); - } else { - const newMarker = new naver.maps.Marker({ - position: new naver.maps.LatLng(location.lat, location.lng), - map, - icon: { - content: `
`, - anchor: new naver.maps.Point(10, 10), - }, - }); - - existingMarkers.set(token, newMarker); - } - }); - - // 삭제된 마커 제거 - existingMarkers.forEach((marker, token) => { - if (!props.otherLocations?.some(loc => loc.token === token)) { - marker.setMap(null); - existingMarkers.delete(token); - } - }); - } - }, [props.otherLocations]); - - // 현재 위치 마커 추가 및 업데이트 - useEffect(() => { - if (mapInstanceRef.current) { - const map = mapInstanceRef.current; - // 현재 위치 마커 생성 - userMarkerRef.current = new naver.maps.Marker({ - position: new naver.maps.LatLng(props.lat, props.lng), - map, - icon: { - content: `
`, - anchor: new naver.maps.Point(12.5, 12.5), - }, - }); - } - }, [props.lat, props.lng]); - - return
; -}; +// /* eslint-disable */ +// import React, { useEffect, useRef, useState } from 'react'; +// import { Canvas } from '@/component/canvas/Canvas'; +// +// interface MapPoint { +// lat: number; +// lng: number; +// } +// +// interface MapProps { +// initialCenter?: { lat: number; lng: number }; +// initialZoom?: number; +// } +// +// // 대한민국 영역 제한 +// const KOREA_BOUNDS = { +// sw: { lat: 33.0, lng: 125.0 }, // 서남단 +// ne: { lat: 38.0, lng: 132.0 }, // 동북단 +// }; +// +// const MIN_ZOOM = 7; // 대한민국 전체가 보이는 최소 줌 레벨 +// const MAX_ZOOM = 19; // 네이버 지도 최대 줌 레벨 +// +// export const MapWithCanvas: React.FC = ({ +// initialCenter = { lat: 36.5, lng: 127.5 }, // 대한민국 중심점 +// initialZoom = 7, +// }) => { +// const mapRef = useRef(null); +// const canvasRef = useRef(null); +// const [map, setMap] = useState(null); +// +// // 지도 초기화 +// useEffect(() => { +// const initializeMap = () => { +// if (!window.naver) return; +// +// const mapOptions = { +// center: new window.naver.maps.LatLng(initialCenter.lat, initialCenter.lng), +// zoom: initialZoom, +// minZoom: MIN_ZOOM, +// maxZoom: MAX_ZOOM, +// mapTypeControl: false, +// scaleControl: false, +// logoControl: false, +// mapDataControl: false, +// zoomControl: true, +// zoomControlOptions: { +// position: naver.maps.Position.RIGHT_CENTER, +// }, +// }; +// +// const mapInstance = new window.naver.maps.Map('map', mapOptions); +// +// // 영역 제한 설정 +// const bounds = new window.naver.maps.LatLngBounds( +// new window.naver.maps.LatLng(KOREA_BOUNDS.sw.lat, KOREA_BOUNDS.sw.lng), +// new window.naver.maps.LatLng(KOREA_BOUNDS.ne.lat, KOREA_BOUNDS.ne.lng), +// ); +// +// mapInstance.setOptions('maxBounds', bounds); +// mapRef.current = mapInstance; +// setMap(mapInstance); +// +// // 지도 이벤트 리스너 +// naver.maps.Event.addListener(mapInstance, 'zoom_changed', () => { +// updateCanvasPosition(); +// }); +// +// naver.maps.Event.addListener(mapInstance, 'center_changed', () => { +// updateCanvasPosition(); +// }); +// }; +// +// initializeMap(); +// +// return () => { +// if (mapRef.current) { +// mapRef.current.destroy(); +// } +// }; +// }, []); +// +// // 캔버스 위치 업데이트 +// const updateCanvasPosition = () => { +// if (!mapRef.current || !canvasRef.current) return; +// +// const mapBounds = mapRef.current.getBounds(); +// const projection = mapRef.current.getProjection(); +// const zoom = mapRef.current.getZoom(); +// +// // 캔버스의 위치와 크기를 지도에 맞춤 +// const canvasElement = canvasRef.current.getCanvasElement(); +// if (!canvasElement) return; +// +// // 캔버스 스케일 조정 +// const scale = Math.pow(2, zoom - MIN_ZOOM); +// canvasRef.current.setScale(scale); +// +// // 캔버스 위치 조정 +// const position = projection.fromCoordToOffset(mapRef.current.getCenter()); +// canvasRef.current.setPosition(position.x, position.y); +// }; +// +// // 지도상의 위경도를 캔버스 좌표로 변환 +// const convertLatLngToPoint = (latLng: MapPoint) => { +// if (!mapRef.current) return null; +// +// const projection = mapRef.current.getProjection(); +// const position = projection.fromCoordToOffset(new naver.maps.LatLng(latLng.lat, latLng.lng)); +// +// return { +// x: position.x, +// y: position.y, +// }; +// }; +// +// // 캔버스 좌표를 위경도로 변환 +// const convertPointToLatLng = (point: { x: number; y: number }) => { +// if (!mapRef.current) return null; +// +// const projection = mapRef.current.getProjection(); +// const latLng = projection.fromOffsetToCoord(new naver.maps.Point(point.x, point.y)); +// +// return { +// lat: latLng.y, +// lng: latLng.x, +// }; +// }; +// +// return ( +//
+//
+// +//
+// ); +// }; diff --git a/frontend/src/component/maps/naverMapUtils.ts b/frontend/src/component/maps/naverMapUtils.ts index 8de7b4e1..22664b78 100644 --- a/frontend/src/component/maps/naverMapUtils.ts +++ b/frontend/src/component/maps/naverMapUtils.ts @@ -1,38 +1,38 @@ -import { IMapOptions } from '@/component/maps/Map.types.ts'; - -export const setNaverMapOption = (mapOptions: IMapOptions): IMapOptions => { - return { - ...mapOptions, - lat: mapOptions.lat ? mapOptions.lat : 37.42829747263545, - lng: mapOptions.lng ? mapOptions.lng : 126.76620435615891, - zoom: mapOptions.zoom ? mapOptions.zoom : 20, - }; -}; - -export const setNaverMap = ( - htmlElement: HTMLElement, - mapOptions: IMapOptions, -): Promise => { - const { lat, lng, ...restProps } = setNaverMapOption(mapOptions); - - return new Promise(resolve => { - const map = new naver.maps.Map(htmlElement, { - center: new naver.maps.LatLng(lat, lng), - ...restProps, - }); - - resolve(map); - }); -}; - -export const setNaverMapSync = ( - htmlElement: HTMLElement, - mapOptions: IMapOptions, -): naver.maps.Map => { - const { lat, lng, ...restProps } = setNaverMapOption(mapOptions); - - return new naver.maps.Map(htmlElement, { - center: new naver.maps.LatLng(lat, lng), - ...restProps, - }); -}; +// import { IMapOptions } from '@/component/maps/Map.types.ts'; +// +// export const setNaverMapOption = (mapOptions: IMapOptions): IMapOptions => { +// return { +// ...mapOptions, +// lat: mapOptions.lat ? mapOptions.lat : 37.42829747263545, +// lng: mapOptions.lng ? mapOptions.lng : 126.76620435615891, +// zoom: mapOptions.zoom ? mapOptions.zoom : 20, +// }; +// }; +// +// export const setNaverMap = ( +// htmlElement: HTMLElement, +// mapOptions: IMapOptions, +// ): Promise => { +// const { lat, lng, ...restProps } = setNaverMapOption(mapOptions); +// +// return new Promise(resolve => { +// const map = new naver.maps.Map(htmlElement, { +// center: new naver.maps.LatLng(lat, lng), +// ...restProps, +// }); +// +// resolve(map); +// }); +// }; +// +// export const setNaverMapSync = ( +// htmlElement: HTMLElement, +// mapOptions: IMapOptions, +// ): naver.maps.Map => { +// const { lat, lng, ...restProps } = setNaverMapOption(mapOptions); +// +// return new naver.maps.Map(htmlElement, { +// center: new naver.maps.LatLng(lat, lng), +// ...restProps, +// }); +// }; diff --git a/frontend/src/index.css b/frontend/src/index.css index b9628a97..a867ad16 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -63,3 +63,9 @@ table { border-collapse: collapse; border-spacing: 0; } +span, a, img { + -webkit-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none +} diff --git a/frontend/src/lib/constants/canvasConstants.ts b/frontend/src/lib/constants/canvasConstants.ts new file mode 100644 index 00000000..a08a86db --- /dev/null +++ b/frontend/src/lib/constants/canvasConstants.ts @@ -0,0 +1,4 @@ +export const LINE_WIDTH = 30; +export const STROKE_STYLE = 'black'; +export const START_MARKER_COLOR = '#4CAF50'; +export const END_MARKER_COLOR = '#F44336'; diff --git a/frontend/src/constants.ts b/frontend/src/lib/constants/commonConstants.ts similarity index 100% rename from frontend/src/constants.ts rename to frontend/src/lib/constants/commonConstants.ts diff --git a/frontend/src/lib/constants/mapConstants.ts b/frontend/src/lib/constants/mapConstants.ts new file mode 100644 index 00000000..ff0c3825 --- /dev/null +++ b/frontend/src/lib/constants/mapConstants.ts @@ -0,0 +1,11 @@ +// 대한민국 경계 좌표 +export const KOREA_BOUNDS = { + sw: { lat: 33.0, lng: 124.5 }, // 남서쪽 끝 + ne: { lat: 38.9, lng: 131.9 }, // 북동쪽 끝 +}; + +// 초기 중심점 (대한민국 중앙 근처) +export const DEFAULT_CENTER = { + lat: (KOREA_BOUNDS.sw.lat + KOREA_BOUNDS.ne.lat) / 2, + lng: (KOREA_BOUNDS.sw.lng + KOREA_BOUNDS.ne.lng) / 2, +}; diff --git a/frontend/src/lib/types/canvasInterface.ts b/frontend/src/lib/types/canvasInterface.ts new file mode 100644 index 00000000..812cd04a --- /dev/null +++ b/frontend/src/lib/types/canvasInterface.ts @@ -0,0 +1,16 @@ +export interface IMapCanvasProps { + width: number; + height: number; + initialCenter: { lat: number; lng: number }; + initialZoom: number; +} + +export interface IPoint { + lat: number; + lng: number; +} + +export interface ICanvasPoint { + x: number; + y: number; +} diff --git a/frontend/src/pages/GuestView.tsx b/frontend/src/pages/GuestView.tsx index 8ef7517c..fc0efeb1 100644 --- a/frontend/src/pages/GuestView.tsx +++ b/frontend/src/pages/GuestView.tsx @@ -1,6 +1,5 @@ import { HeaderContext } from '@/component/layout/header/LayoutHeaderProvider'; import { useContext, useEffect } from 'react'; -import { CanvasWithMap } from '@/component/canvas/CanvasWithMap.tsx'; export const GuestView = () => { const headerContext = useContext(HeaderContext); @@ -12,5 +11,5 @@ export const GuestView = () => { }, []); // TODO: geoCoding API를 이용해서 현재 위치나 시작위치를 기반으로 자동 좌표 설정 구현 (현재: 하드코딩) - return ; + return
hello
; }; diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx index 7ecea493..2c9f39ab 100644 --- a/frontend/src/pages/Main.tsx +++ b/frontend/src/pages/Main.tsx @@ -1,82 +1,71 @@ -import { Fragment, useContext, useEffect, useState } from 'react'; -import { getUserLocation } from '@/hooks/getUserLocation'; -import { BottomSheet } from '@/component/bottomsheet/BottomSheet'; -import { Content } from '@/component/content/Content'; +import { useContext, useEffect } from 'react'; import { MdFormatListBulleted } from 'react-icons/md'; import { FooterContext } from '@/component/layout/footer/LayoutFooterProvider'; import { useNavigate } from 'react-router-dom'; -import { NaverMap } from '@/component/maps/NaverMapSample.tsx'; import { buttonActiveType } from '@/component/layout/enumTypes'; -import { loadLocalData, saveLocalData } from '@/utils/common/manageLocalData.ts'; -import { AppConfig } from '@/constants.ts'; -import { v4 as uuidv4 } from 'uuid'; +import { FullScreenMap } from '@/component/maps/Canvas.tsx'; -const contentData = [ - { - id: '1', - title: '아들네 집으로', - time: '0시간 34분', - person: 2, - link: '/test1', - }, - { - id: '2', - title: '손자네 집으로', - time: '2시간 32분', - person: 0, - link: '/test2', - }, - { - id: '3', - title: '마을회관으로', - time: '0시간 12분', - person: 1, - link: '/test3', - }, -]; +// const contentData = [ +// { +// id: '1', +// title: '아들네 집으로', +// time: '0시간 34분', +// person: 2, +// link: '/channel/123/guest/456', +// }, +// { +// id: '2', +// title: '손자네 집으로', +// time: '2시간 32분', +// person: 0, +// link: '/channel/123/guest/456', +// }, +// { +// id: '3', +// title: '마을회관으로', +// time: '0시간 12분', +// person: 1, +// link: '/channel/123/guest/456', +// }, +// ]; export const Main = () => { const { setFooterTitle, setFooterTransparency, setFooterOnClick, setFooterActive } = useContext(FooterContext); - const { lat, lng, error } = getUserLocation(); const navigate = useNavigate(); - const [otherLocations, setOtherLocations] = useState([]); - const MIN_HEIGHT = 0.5; - const MAX_HEIGHT = 0.8; - // eslint-disable-next-line consistent-return - useEffect(() => { - if (lat && lng) { - if (!loadLocalData(AppConfig.KEYS.BROWSER_TOKEN)) { - const token = uuidv4(); - saveLocalData(AppConfig.KEYS.BROWSER_TOKEN, token); - } - const token = loadLocalData(AppConfig.KEYS.BROWSER_TOKEN); - const ws = new WebSocket(`${AppConfig.SOCKET_SERVER}/?token=${token}`); - - // 초기 위치 전송 - ws.onopen = () => { - ws.send(JSON.stringify({ type: 'location', location: { lat, lng } })); - }; - - ws.onmessage = event => { - const data = JSON.parse(event.data); - - if (data.type === 'init') { - // 기존 클라이언트들의 위치 초기화 - setOtherLocations(data.clients); - } else if (data.type === 'location' && data.token !== token) { - // 새로 들어온 위치 업데이트 - setOtherLocations(prev => - prev.some(loc => loc.token === data.token) - ? prev.map(loc => (loc.token === data.token ? data : loc)) - : [...prev, data], - ); - } - }; - return () => ws.close(); - } - }, [lat, lng]); + // useEffect(() => { + // if (lat && lng) { + // if (!loadLocalData(AppConfig.KEYS.BROWSER_TOKEN)) { + // const token = uuidv4(); + // saveLocalData(AppConfig.KEYS.BROWSER_TOKEN, token); + // } + // const token = loadLocalData(AppConfig.KEYS.BROWSER_TOKEN); + // const ws = new WebSocket(`${AppConfig.SOCKET_SERVER}/?token=${token}`); + // + // // 초기 위치 전송 + // ws.onopen = () => { + // ws.send(JSON.stringify({ type: 'location', location: { lat, lng } })); + // }; + // + // ws.onmessage = event => { + // const data = JSON.parse(event.data); + // + // if (data.type === 'init') { + // // 기존 클라이언트들의 위치 초기화 + // setOtherLocations(data.clients); + // } else if (data.type === 'location' && data.token !== token) { + // // 새로 들어온 위치 업데이트 + // setOtherLocations(prev => + // prev.some(loc => loc.token === data.token) + // ? prev.map(loc => (loc.token === data.token ? data : loc)) + // : [...prev, data], + // ); + // } + // }; + // return () => ws.close(); + // } + // }, [lat, lng]); const goToAddChannel = () => { navigate('/add-channel'); @@ -94,37 +83,23 @@ export const Main = () => { + -
- {/* eslint-disable-next-line no-nested-ternary */} - {lat && lng ? ( - otherLocations ? ( - - ) : ( -
- Loading map data... -
- ) - ) : ( -
- {error ? `Error: ${error}` : 'Loading'} -
- )} -
+ {/*
*/} - - {contentData.map(item => ( - - -
-
- ))} -
+ {/* */} + {/* {contentData.map(item => ( */} + {/* */} + {/* */} + {/*
*/} + {/*
*/} + {/* ))} */} + {/*
*/}
); };