From df7cc790b9c6e992107889b1d2be5fe69ca64ee7 Mon Sep 17 00:00:00 2001 From: effozen Date: Sat, 23 Nov 2024 08:22:22 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[FE][Refactor]=20#224=20:=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코드 가독성 개선 - 에러 케이스 추가 - 타입 분리 --- frontend/src/component/maps/Map.tsx | 41 +++++++---------- frontend/src/component/maps/Map.types.ts | 42 +++++++++++++++++ frontend/src/component/maps/NaverMap.tsx | 25 +++++++---- frontend/src/component/maps/mapUtils.ts | 32 +++++++++++++ frontend/src/component/maps/naverMapUtils.ts | 47 +------------------- 5 files changed, 107 insertions(+), 80 deletions(-) create mode 100644 frontend/src/component/maps/Map.types.ts create mode 100644 frontend/src/component/maps/mapUtils.ts diff --git a/frontend/src/component/maps/Map.tsx b/frontend/src/component/maps/Map.tsx index 6fee30b8..b042d038 100644 --- a/frontend/src/component/maps/Map.tsx +++ b/frontend/src/component/maps/Map.tsx @@ -1,14 +1,9 @@ -import { NaverMap } from '@/component/maps/NaverMap.tsx'; 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'; -type IMapObject = naver.maps.Map | null; - -export interface IMapOptions { - lat: number; - lng: number; - zoom?: number; -} +const validateKindOfMap = (type: string) => ['naver'].includes(type); interface IMapProps extends IMapOptions { className?: string; @@ -16,21 +11,14 @@ interface IMapProps extends IMapOptions { 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'); + if (!validateKindOfMap(props.type)) + throw new Error('🚀 지도 로딩 오류 : 알 수 없는 지도 타입이 인자로 들어 왔습니다.'); - const mapRef = useRef(null); + const mapRefMethods = useRef(null); const mapContainer = useRef(null); - const [mapObject, setMapObject] = useState(null); + + const [mapObject, setMapObject] = useState(); const [MapComponent, setMapComponent] = useState(); const onMapInit = (mapObj: IMapObject) => { @@ -44,21 +32,24 @@ export const Map = forwardRef((props, ref) => { lat={props.lat} lng={props.lng} zoom={props.zoom} - ref={mapRef} + ref={mapRefMethods} onMapInit={onMapInit} /> ); setMapComponent(mapComponent); } - }, [props.lat, props.lng, props.zoom, props.type]); + }, []); useEffect(() => { - mapContainer.current = mapRef.current?.getMapContainer() ?? null; - props.initMap(mapObject); + mapContainer.current = mapRefMethods.current?.getMapContainer() ?? null; + if (mapObject) props.initMap(mapObject); }, [mapObject]); useImperativeHandle(ref, () => ({ - getMapObject: () => mapObject, + getMapObject: () => { + if (mapObject) return mapObject; + throw new Error('🚀 지도 로딩 오류 : 지도 객체가 존재하지 않습니다.'); + }, getMapContainer: () => mapContainer.current, onMouseClickHandler: () => {}, })); diff --git a/frontend/src/component/maps/Map.types.ts b/frontend/src/component/maps/Map.types.ts new file mode 100644 index 00000000..83089b24 --- /dev/null +++ b/frontend/src/component/maps/Map.types.ts @@ -0,0 +1,42 @@ +export type IMapObject = naver.maps.Map; + +export type IMapLatLngBound = naver.maps.LatLngBounds; + +export interface IMapOptions { + lat: number; + lng: number; + zoom?: number; +} + +/** + * Forward Ref 를 통해서, 부모에서 자식 컴포넌트의 Ref에 접근할 때 쓰이는 인터페이스. + * 다음과 같은 목적으로 쓰인다. + * 1. 지도 컨테이너 요소와, 지도 객체를 가져온다. + * 2. 지도 객체의 이벤트 위임을 위해 자신을 컨트롤 할 수 있는 Handler를 부모에게 전달하는 역할을 한다. + * 이렇게 전달받은 핸들러로 부모 컴포넌트에서 자식에 있는 지도 객체를 컨트롤 할 수 있다. + * */ +export interface IMapRefMethods { + getMapObject: () => naver.maps.Map | null; + getMapContainer: () => HTMLElement | null; + onMouseClickHandler: (event?: React.MouseEvent) => void; +} + +// lat: 위도(y), lng: 경도(x) INaverMapVertexPosition +export interface IMapVertexCoordinate { + ne: { + lng: number; + lat: number; + }; + nw: { + lng: number; + lat: number; + }; + se: { + lng: number; + lat: number; + }; + sw: { + lng: number; + lat: number; + }; +} diff --git a/frontend/src/component/maps/NaverMap.tsx b/frontend/src/component/maps/NaverMap.tsx index 72cdfa03..0f9049d0 100644 --- a/frontend/src/component/maps/NaverMap.tsx +++ b/frontend/src/component/maps/NaverMap.tsx @@ -1,14 +1,15 @@ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { setNaverMapSync } from '@/component/maps/naverMapUtils.ts'; -import { IMapOptions, IMapRefMethods } from '@/component/maps/Map.tsx'; +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 naverMapObject = useRef(null); - const naverMapContainer = useRef(null); + const mapObject = useRef(null); + const mapContainer = useRef(null); + const [mapOptions, setMapOptions] = useState({ lat: props.lat, lng: props.lng, @@ -24,17 +25,23 @@ export const NaverMap = forwardRef((props, ref) }, [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); // 콜백 호출 + if (mapContainer.current && mapOptions) { + mapObject.current = setNaverMapSync(mapContainer.current, mapOptions); + if (mapObject.current) props.onMapInit(mapObject.current); // 콜백 호출 } }, [mapOptions]); useImperativeHandle(ref, () => ({ - getMapObject: () => naverMapObject.current, - getMapContainer: () => naverMapContainer.current, + getMapObject: () => { + if (mapObject) return mapObject.current; + throw new Error('🚀 지도 로딩 오류 : 지도 객체가 존재하지 않습니다.'); + }, + getMapContainer: () => { + if (mapContainer) return mapContainer.current; + throw new Error('🚀 지도 로딩 오류 : 지도 컨테이너가 존재하지 않습니다.'); + }, onMouseClickHandler: () => {}, })); - return
; + return
; }); diff --git a/frontend/src/component/maps/mapUtils.ts b/frontend/src/component/maps/mapUtils.ts new file mode 100644 index 00000000..821341ac --- /dev/null +++ b/frontend/src/component/maps/mapUtils.ts @@ -0,0 +1,32 @@ +import { IMapVertexCoordinate, IMapObject, IMapLatLngBound } from '@/component/maps/Map.types.ts'; + +export const getMapVertexCoordinate = (map: IMapObject): IMapVertexCoordinate => { + let bounds: IMapLatLngBound; + + if (map instanceof naver.maps.Map) { + bounds = map.getBounds() as naver.maps.LatLngBounds; + } else { + throw new Error('🚀 꼭지점 좌표 가져오기 오류 : 지도 객체가 없습니다.'); + } + + const sw = bounds.getSW(); + const ne = bounds.getNE(); + return { + se: { + lng: ne.lng(), + lat: sw.lat(), + }, + sw: { + lng: sw.lng(), + lat: sw.lat(), + }, + ne: { + lng: ne.lng(), + lat: ne.lat(), + }, + nw: { + lng: sw.lng(), + lat: ne.lat(), + }, + }; +}; diff --git a/frontend/src/component/maps/naverMapUtils.ts b/frontend/src/component/maps/naverMapUtils.ts index 4a6788e9..8de7b4e1 100644 --- a/frontend/src/component/maps/naverMapUtils.ts +++ b/frontend/src/component/maps/naverMapUtils.ts @@ -1,48 +1,4 @@ -import { IMapOptions } from '@/component/maps/Map.tsx'; - -// lat: 위도(y), lng: 경도(x) -export interface INaverMapVertexPosition { - ne: { - lng: number; - lat: number; - }; - nw: { - lng: number; - lat: number; - }; - se: { - lng: number; - lat: number; - }; - sw: { - lng: number; - lat: number; - }; -} - -export const getNaverMapVertexPosition = (map: naver.maps.Map): INaverMapVertexPosition => { - const bounds = map.getBounds() as naver.maps.LatLngBounds; - const sw = bounds.getSW(); - const ne = bounds.getNE(); - return { - se: { - lng: ne.lng(), - lat: sw.lat(), - }, - sw: { - lng: sw.lng(), - lat: sw.lat(), - }, - ne: { - lng: ne.lng(), - lat: ne.lat(), - }, - nw: { - lng: sw.lng(), - lat: ne.lat(), - }, - }; -}; +import { IMapOptions } from '@/component/maps/Map.types.ts'; export const setNaverMapOption = (mapOptions: IMapOptions): IMapOptions => { return { @@ -53,7 +9,6 @@ export const setNaverMapOption = (mapOptions: IMapOptions): IMapOptions => { }; }; -// utils에 있는 일반 함수로 사용 export const setNaverMap = ( htmlElement: HTMLElement, mapOptions: IMapOptions, From 6a6a6ca428a9d467b36aa51ac793a2692987d88e Mon Sep 17 00:00:00 2001 From: effozen Date: Sat, 23 Nov 2024 16:53:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[FE][Refactor]=20#224=20:=20CanvasWithMap?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 함수를 분리했다. --- .github/workflows/ci.yml | 2 +- frontend/src/component/canvas/Canvas.tsx | 2 +- .../src/component/canvas/CanvasWithMap.tsx | 83 ++++-------------- .../src/component/canvas/useEventHandlers.tsx | 86 +++++++++++++++++++ 4 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 frontend/src/component/canvas/useEventHandlers.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59b20bf4..32698c10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Run Build run: pnpm build # pnpm을 사용하여 빌드 실행 - continue-on-error: false # 빌드 실패 시 워크플로우 실패로 처리 +# continue-on-error: false # 빌드 실패 시 워크플로우 실패로 처리 - name: Run Tests run: pnpm test # pnpm을 사용하여 테스트 실행 diff --git a/frontend/src/component/canvas/Canvas.tsx b/frontend/src/component/canvas/Canvas.tsx index f1406b3c..c8f13db4 100644 --- a/frontend/src/component/canvas/Canvas.tsx +++ b/frontend/src/component/canvas/Canvas.tsx @@ -44,7 +44,7 @@ export interface ICanvasRefMethods { } export const Canvas = forwardRef((props, ref) => { - const canvasRef = useRef(null); + const canvasRef = useRef(null!); const { points, addPoint, undo, redo, undoStack, redoStack } = useUndoRedo([]); const [startPoint, setStartPoint] = useState(null); const [endPoint, setEndPoint] = useState(null); diff --git a/frontend/src/component/canvas/CanvasWithMap.tsx b/frontend/src/component/canvas/CanvasWithMap.tsx index a4718071..50dcebae 100644 --- a/frontend/src/component/canvas/CanvasWithMap.tsx +++ b/frontend/src/component/canvas/CanvasWithMap.tsx @@ -1,9 +1,9 @@ -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'; +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; @@ -14,32 +14,13 @@ interface ICanvasWithMapProps { 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 mapElement = useRef(null); const canvasElement = useRef(null); - const mouseEventState = useRef({ ...MouseEventStateInitialValue }); - const [mapObject, setMapObject] = useState(null); + + const [mapObject, setMapObject] = useState(null); useEffect(() => { if (canvasRefMethods.current?.getCanvasElement) @@ -50,46 +31,16 @@ export const CanvasWithMap = (props: ICanvasWithMapProps) => { 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 { handleClick, handleMouseDown, handleMouseMove, handleMouseUp } = useEventHandlers( + canvasElement.current, + canvasRefMethods.current, + mapElement.current, + mapRefMethods.current, + mapObject, + ); - const handleMouseUp = (event: React.MouseEvent) => { - if (!mapElement.current || !canvasElement.current) return; - mouseEventState.current = { ...MouseEventStateInitialValue }; - canvasRefMethods.current?.onMouseUpHandler(event); + const initMap = (mapObj: IMapObject | null) => { + setMapObject(mapObj); }; return ( diff --git a/frontend/src/component/canvas/useEventHandlers.tsx b/frontend/src/component/canvas/useEventHandlers.tsx new file mode 100644 index 00000000..23191544 --- /dev/null +++ b/frontend/src/component/canvas/useEventHandlers.tsx @@ -0,0 +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 }; +};