diff --git a/frontend/src/assets/marker.png b/frontend/src/assets/marker.png new file mode 100644 index 00000000..0b60010e Binary files /dev/null and b/frontend/src/assets/marker.png differ diff --git a/frontend/src/component/markerdrawer/MarkerDrawer.tsx b/frontend/src/component/markerdrawer/MarkerDrawer.tsx new file mode 100644 index 00000000..026e0d14 --- /dev/null +++ b/frontend/src/component/markerdrawer/MarkerDrawer.tsx @@ -0,0 +1,65 @@ +import React, { useRef, useState } from 'react'; +import { usePanning } from '@/hooks/usePanning'; +import { useZoom } from '@/hooks/useZoom'; +import { useMarker } from '@/hooks/useMarker'; + +interface IPoint { + x: number; + y: number; +} + +const NAVER_STEP_SCALES = [ + 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 = 7; + +export const MarkerDrawer = () => { + const canvasRef = useRef(null); + const [point, setPoint] = useState({ x: 0, y: 0 }); + + const { mark, scaleRef, viewPosRef } = useMarker({ + canvasRef, + point, + lineWidth: LINE_WIDTH, + strokeStyle: STROKE_STYLE, + }); + const { handleMouseMove, handleMouseDown, handleMouseUp } = usePanning({ + viewPosRef, + draw: mark, + }); + const { handleWheel } = useZoom({ + scaleRef, + viewPosRef, + draw: mark, + 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; + setPoint({ x, y }); + }; + + return ( + + ); +}; diff --git a/frontend/src/hooks/useMarker.ts b/frontend/src/hooks/useMarker.ts new file mode 100644 index 00000000..b99f6b4f --- /dev/null +++ b/frontend/src/hooks/useMarker.ts @@ -0,0 +1,112 @@ +import { useRef, useEffect, useState } from 'react'; +import markerImage from '@/assets/marker.png'; // 이미지 import + +interface IPoint { + x: number; + y: number; +} + +interface IUseDrawingProps { + canvasRef: React.RefObject; + point: IPoint; + lineWidth: number; + strokeStyle: string; +} + +const INITIAL_POSITION = { x: 0, y: 0 }; +const STEP_SCALES = [100, 50, 30, 20, 10, 5, 3, 1, 0.5, 0.3, 0.1, 0.05, 0.03, 0.02, 0.01, 0.005]; + +export const useMarker = (props: IUseDrawingProps) => { + const scaleRef = useRef(STEP_SCALES[7]); + const viewPosRef = useRef(INITIAL_POSITION); + const [iconImage, setIconImage] = useState(null); + const [markerPoint, setMarkerPoint] = useState(props.point); + const iconSize = 20; + + const getCanvasContext = (): CanvasRenderingContext2D | null => + props.canvasRef.current?.getContext('2d') || null; + + const setTransform = (context: CanvasRenderingContext2D) => { + context.setTransform( + scaleRef.current, + 0, + 0, + scaleRef.current, + viewPosRef.current.x, + viewPosRef.current.y, + ); + }; + + const loadIconImage = () => { + const img = new Image(); + img.src = markerImage; + img.onload = () => setIconImage(img); + }; + + const mark = () => { + if (!iconImage || markerPoint === null) return; + const context = getCanvasContext(); + if (!context) return; + + const canvas = props.canvasRef.current; + if (!canvas) return; + + context.clearRect(0, 0, canvas.width, canvas.height); + setTransform(context); + + context.drawImage( + iconImage, + props.point.x - iconSize / 2, + props.point.y - iconSize, + iconSize, + iconSize, + ); + }; + const deleteMarker = (point: IPoint) => { + if (point.x === 0 && point.y === 0) return; + if (markerPoint) { + const iconLeft = markerPoint.x - iconSize / 2; + const iconRight = markerPoint.x + iconSize / 2; + const iconTop = markerPoint.y - iconSize; + const iconBottom = markerPoint.y; + + const isInsideIcon = + point.x >= iconLeft && point.x <= iconRight && point.y >= iconTop && point.y <= iconBottom; + + if (isInsideIcon) { + setMarkerPoint(null); + return; + } + } + setMarkerPoint(point); + }; + + useEffect(() => { + setMarkerPoint(props.point); + loadIconImage(); + }, []); + + const clearCanvas = () => { + const context = getCanvasContext(); + const canvas = props.canvasRef.current; + if (canvas && context) { + context.clearRect(0, 0, canvas.width, canvas.height); + } + }; + + useEffect(() => { + deleteMarker(props.point); + }, [props.point]); + + useEffect(() => { + if (!iconImage) return; + + if (markerPoint === null) { + clearCanvas(); + } else { + mark(); + } + }, [markerPoint, iconImage]); + + return { mark, scaleRef, viewPosRef }; +};