diff --git a/package-lock.json b/package-lock.json index 1470208..4720d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "react-naver-maps": "^0.1.3", "react-router-dom": "^7.1.1", "react-table": "^7.8.0", + "react-zoom-pan-pinch": "^3.7.0", "styled-components": "^6.1.14", "zustand": "^5.0.3" }, @@ -4324,6 +4325,20 @@ "react-dom": "*" } }, + "node_modules/react-zoom-pan-pinch": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", + "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==", + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", diff --git a/package.json b/package.json index 30b45af..eaf81a5 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-naver-maps": "^0.1.3", "react-router-dom": "^7.1.1", "react-table": "^7.8.0", + "react-zoom-pan-pinch": "^3.7.0", "styled-components": "^6.1.14", "zustand": "^5.0.3" }, diff --git a/src/components/ImgViewerModal.tsx b/src/components/ImgViewerModal.tsx index 9fd459b..83b9153 100644 --- a/src/components/ImgViewerModal.tsx +++ b/src/components/ImgViewerModal.tsx @@ -1,7 +1,9 @@ -import { useImgViewerStore } from '../stores/imgViewerStore'; import styled from 'styled-components'; import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded'; import ArrowBackIosRoundedIcon from '@mui/icons-material/ArrowBackIosRounded'; +import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; +import { useState, useEffect } from 'react'; +import { useImgViewerStore } from '../stores/imgViewerStore'; const ImgViewerModal = () => { const { @@ -18,6 +20,32 @@ const ImgViewerModal = () => { images, } = useImgViewerStore(); + // 핀치줌 상태 관리 + const [isZooming, setIsZooming] = useState(false); + + // 디버깅을 위한 WebView 메시지 전송 + useEffect(() => { + const sendDebugMessage = (message: any) => { + if (typeof window !== 'undefined' && (window as any).ReactNativeWebView) { + (window as any).ReactNativeWebView.postMessage(JSON.stringify(message)); + } + }; + + // 컴포넌트 마운트 시 디버그 메시지 + sendDebugMessage({ + type: 'modal_opened', + imageType: currentImageType, + timestamp: Date.now(), + }); + + return () => { + sendDebugMessage({ + type: 'modal_closed', + timestamp: Date.now(), + }); + }; + }, [currentImageType]); + if (!isOpen || !currentImageType || !images) return null; const { current, total } = getImagePosition(); @@ -25,7 +53,7 @@ const ImgViewerModal = () => { const nextEnabled = canGoNext(); // 이미지 타입별 한국어 이름 - const getImageTypeName = (type: string) => { + const getImageTypeName = (type: string): string => { switch (type) { case 'position': return '위치도'; @@ -40,14 +68,14 @@ const ImgViewerModal = () => { } }; - const handlePrev = () => { + const handlePrev = (): void => { const prevType = getPrevImageType(); if (prevType && images[prevType]?.url) { openImage(images[prevType].url, prevType); } }; - const handleNext = () => { + const handleNext = (): void => { const nextType = getNextImageType(); if (nextType && images[nextType]?.url) { openImage(images[nextType].url, nextType); @@ -55,18 +83,38 @@ const ImgViewerModal = () => { }; // 키보드 이벤트 처리 - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'ArrowLeft' && prevEnabled) { + const handleKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === 'ArrowLeft' && prevEnabled && !isZooming) { handlePrev(); - } else if (e.key === 'ArrowRight' && nextEnabled) { + } else if (e.key === 'ArrowRight' && nextEnabled && !isZooming) { handleNext(); } else if (e.key === 'Escape') { onClose(); } }; + // 배경 클릭 시 모달 닫기 (핀치줌 중이 아닐 때만) + const handleOverlayClick = (e: React.MouseEvent): void => { + if (e.target === e.currentTarget && !isZooming) { + onClose(); + } + }; + + // 디버깅 메시지 전송 함수 + const sendDebugMessage = (message: any): void => { + if (typeof window !== 'undefined' && (window as any).ReactNativeWebView) { + (window as any).ReactNativeWebView.postMessage(JSON.stringify(message)); + } + }; + return ( - + e.stopPropagation()} + onTouchMove={(e) => e.stopPropagation()} + > e.stopPropagation()}> @@ -76,34 +124,116 @@ const ImgViewerModal = () => { - {/* 이미지 */} - { - console.error('이미지 로드 실패:', currentImageUrl); + { + setIsZooming(true); + sendDebugMessage({ + type: 'pinch_start', + timestamp: Date.now(), + }); + }} + onPinchingStop={() => { + setTimeout(() => { + setIsZooming(false); + sendDebugMessage({ + type: 'pinch_stop', + timestamp: Date.now(), + }); + }, 100); + }} + onZoomStart={() => { + setIsZooming(true); + sendDebugMessage({ + type: 'zoom_start', + timestamp: Date.now(), + }); + }} + onZoomStop={() => { + setTimeout(() => { + setIsZooming(false); + sendDebugMessage({ + type: 'zoom_stop', + timestamp: Date.now(), + }); + }, 100); + }} + onPanningStart={() => { + sendDebugMessage({ + type: 'pan_start', + timestamp: Date.now(), + }); + }} + onPanningStop={() => { + sendDebugMessage({ + type: 'pan_stop', + timestamp: Date.now(), + }); }} - /> - - {/* 왼쪽 화살표 - MUI 아이콘 사용 */} - - - - - {/* 오른쪽 화살표 - MUI 아이콘 사용 */} - - - + + { + console.error('이미지 로드 실패:', currentImageUrl); + }} + draggable={false} + /> + + + + {/* 왼쪽 화살표 - 핀치줌 중이 아닐 때만 표시 */} + {!isZooming && ( + + + + )} + + {/* 오른쪽 화살표 - 핀치줌 중이 아닐 때만 표시 */} + {!isZooming && ( + + + + )} @@ -123,6 +253,9 @@ const ModalOverlay = styled.div` display: flex; flex-direction: column; outline: none; + + /* 터치 이벤트 처리 강화 */ + touch-action: none; `; const ModalContainer = styled.div` @@ -182,13 +315,19 @@ const ImageContainer = styled.div` display: flex; align-items: center; justify-content: center; - overflow: hidden; + /* overflow 제거 - 핀치줌을 위해 필요 */ `; const MainImage = styled.img` width: 100%; height: 100%; object-fit: contain; + user-select: none; + /* 드래그 방지 */ + -webkit-user-drag: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; `; const ArrowButton = styled.button<{ diff --git a/src/pages/MapPage/components/InfoTable.tsx b/src/pages/MapPage/components/InfoTable.tsx index badff77..fa8527d 100644 --- a/src/pages/MapPage/components/InfoTable.tsx +++ b/src/pages/MapPage/components/InfoTable.tsx @@ -239,6 +239,7 @@ const TitleWrapper = styled.div` gap: 10px; padding-bottom: 15px; flex-grow: 1; + overflow: hidden; `; const Title = styled.div` @@ -253,8 +254,7 @@ const UpperAddressValue = styled.div` font-size: 14px; color: #7e7e7e; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + overflow-x: scroll; `; const ContentSection = styled.div` diff --git a/src/pages/MapPage/components/comment/CommentContainer.tsx b/src/pages/MapPage/components/comment/CommentContainer.tsx index 1cbca43..488e70e 100644 --- a/src/pages/MapPage/components/comment/CommentContainer.tsx +++ b/src/pages/MapPage/components/comment/CommentContainer.tsx @@ -4,11 +4,12 @@ import { CommentContainerProps } from '../../interface'; import { slopeCommentAPI } from '../../../../apis/slopeMap'; import CommentDeleteModal from './CommentDeleteModal'; import CommentUpdateModal from './CommentUpdateModal'; -import { useCommentStore } from '../../../../stores/commentStore'; +import { useState } from 'react'; const CommentContainer = ({ comment, fetchComment }: CommentContainerProps) => { - const { isMoreOpen, setIsMore, setIsDelete, setIsModi } = useCommentStore(); - + const [isMoreOpen, setIsMoreOpen] = useState(false); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [isModiOpen, setIsModiOpen] = useState(false); //수정 삭제에 접근 가능한지 const accessible = (): boolean => { const userId = localStorage.getItem('_id') || ''; @@ -30,7 +31,7 @@ const CommentContainer = ({ comment, fetchComment }: CommentContainerProps) => { try { await slopeCommentAPI.updateComment(formData); await fetchComment(); - setIsModi(false); + setIsModiOpen(false); } catch (error) { console.log('코멘트 수정 오류', error); } @@ -40,14 +41,18 @@ const CommentContainer = ({ comment, fetchComment }: CommentContainerProps) => { <> {/* 삭제모달 */} { handleDelete(); - setIsMore(false); - setIsDelete(false); + setIsMoreOpen(false); + setIsDeleteOpen(false); }} /> {/*수정모달 */} { - {new Date(comment.createdAt).toLocaleDateString()} + {new Date(comment.createdAt).toLocaleString('ko-KR')} { - setIsMore(true); + setIsMoreOpen(true); }} > { <> { - setIsMore(false); + setIsMoreOpen(false); }} > { - setIsModi(true); - setIsMore(false); + setIsModiOpen(true); + setIsMoreOpen(false); }} > 수정하기 { - setIsDelete(true); - setIsMore(false); + setIsDeleteOpen(true); + setIsMoreOpen(false); }} > 삭제하기 diff --git a/src/pages/MapPage/components/comment/CommentCreateModal.tsx b/src/pages/MapPage/components/comment/CommentCreateModal.tsx index 765ea08..6639a88 100644 --- a/src/pages/MapPage/components/comment/CommentCreateModal.tsx +++ b/src/pages/MapPage/components/comment/CommentCreateModal.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { useNotificationStore } from '../../../../hooks/notificationStore'; import { CommentAddModalProps } from '../../interface'; -import { useCommentStore } from '../../../../stores/commentStore'; interface ImageFile { file: File; @@ -19,10 +18,13 @@ interface MobileImageAsset { dataUrl?: string; } -const CommentAddModal = ({ onSubmit }: CommentAddModalProps) => { - const { isMoreOpen, setIsMore } = useCommentStore(); +const CommentAddModal = ({ + isAddOpen, + setIsAddOpen, + onSubmit, +}: CommentAddModalProps) => { const onClose = () => { - setIsMore(false); + setIsAddOpen(false); }; const [comment, setComment] = useState(''); const [images, setImages] = useState([]); @@ -34,11 +36,11 @@ const CommentAddModal = ({ onSubmit }: CommentAddModalProps) => { typeof window !== 'undefined' && window.ReactNativeWebView != null; //모달이 열릴 때 초기상태로 복원 useEffect(() => { - if (isMoreOpen) { + if (isAddOpen) { setComment(''); setImages([]); } - }, [isMoreOpen]); + }, [isAddOpen]); //사진 권한 요청 useEffect(() => { @@ -239,7 +241,7 @@ const CommentAddModal = ({ onSubmit }: CommentAddModalProps) => { }; return ( - +

결함사진 등록

diff --git a/src/pages/MapPage/components/comment/CommentDeleteModal.tsx b/src/pages/MapPage/components/comment/CommentDeleteModal.tsx index bf3c0e8..fb7e843 100644 --- a/src/pages/MapPage/components/comment/CommentDeleteModal.tsx +++ b/src/pages/MapPage/components/comment/CommentDeleteModal.tsx @@ -1,11 +1,13 @@ import styled from 'styled-components'; import { DeleteModalProps } from '../../interface'; -import { useCommentStore } from '../../../../stores/commentStore'; -const CommentDeleteModal = ({ onSubmit }: DeleteModalProps) => { - const { isDeleteOpen, setIsDelete } = useCommentStore(); +const CommentDeleteModal = ({ + isDeleteOpen, + setIsDeleteOpen, + onSubmit, +}: DeleteModalProps) => { const onClose = () => { - setIsDelete(false); + setIsDeleteOpen(false); }; return ( diff --git a/src/pages/MapPage/components/comment/CommentList.tsx b/src/pages/MapPage/components/comment/CommentList.tsx index 1846729..05c9d97 100644 --- a/src/pages/MapPage/components/comment/CommentList.tsx +++ b/src/pages/MapPage/components/comment/CommentList.tsx @@ -5,11 +5,10 @@ import CommentAddModal from './CommentCreateModal'; import { slopeCommentAPI } from '../../../../apis/slopeMap'; import { CommentData, CommentListProps } from '../../interface'; import NoInfo from '../NoInfo'; -import { useCommentStore } from '../../../../stores/commentStore'; const CommentList = ({ slopeId }: CommentListProps) => { const [commentData, setCommentData] = useState([]); - const { setIsMore } = useCommentStore(); + const [isAddOpen, setIsAddOpen] = useState(false); const handleCreate = async (content: string, images: File[]) => { try { const formData = new FormData(); @@ -26,7 +25,7 @@ const CommentList = ({ slopeId }: CommentListProps) => { // 생성 후 코멘트 목록 다시 조회 const newData = await slopeCommentAPI.getComment(slopeId); setCommentData(newData); - setIsMore(false); + setIsAddOpen(false); } catch (error) { console.log('코멘트 생성 실패', error); } @@ -43,15 +42,17 @@ const CommentList = ({ slopeId }: CommentListProps) => { useEffect(() => { fetchComment(); }, [slopeId]); - + console.log(commentData); return ( handleCreate(content, images)} /> { - setIsMore(true); + setIsAddOpen(true); }} > 결함사진 등록 diff --git a/src/pages/MapPage/components/comment/CommentUpdateModal.tsx b/src/pages/MapPage/components/comment/CommentUpdateModal.tsx index f9f0bfd..aa0cecd 100644 --- a/src/pages/MapPage/components/comment/CommentUpdateModal.tsx +++ b/src/pages/MapPage/components/comment/CommentUpdateModal.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { useNotificationStore } from '../../../../hooks/notificationStore'; -import { useCommentStore } from '../../../../stores/commentStore'; import { CommentUpdateModalProps } from '../../interface'; interface ImageFile { @@ -21,14 +20,15 @@ interface MobileImageAsset { dataUrl?: string; } const CommentUpdateModal = ({ + isModiOpen, + setIsModiOpen, onSubmit, defaultComment, defaultImages, commentId, }: CommentUpdateModalProps) => { - const { isModiOpen, setIsModi } = useCommentStore(); const onClose = () => { - setIsModi(false); + setIsModiOpen(false); }; const [comment, setComment] = useState(defaultComment); const [images, setImages] = useState(() => diff --git a/src/pages/MapPage/interface.ts b/src/pages/MapPage/interface.ts index 07746d9..e02c3d4 100644 --- a/src/pages/MapPage/interface.ts +++ b/src/pages/MapPage/interface.ts @@ -47,15 +47,21 @@ export interface SearchResultProps { }; } export interface CommentAddModalProps { + isAddOpen: boolean; + setIsAddOpen: (value: boolean) => void; onSubmit: (comment: string, images: File[]) => void; } export interface DeleteModalProps { + isDeleteOpen: boolean; + setIsDeleteOpen: (value: boolean) => void; onSubmit: () => void; } export interface CommentListProps { slopeId: string; } export interface CommentUpdateModalProps { + isModiOpen: boolean; + setIsModiOpen: (value: boolean) => void; onSubmit: (formData: FormData) => void; defaultComment: string; defaultImages: string[]; diff --git a/src/stores/commentStore.ts b/src/stores/commentStore.ts deleted file mode 100644 index f7e3780..0000000 --- a/src/stores/commentStore.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { create } from 'zustand'; - -interface commentStore { - //초기상태 - isMoreOpen: boolean; - isDeleteOpen: boolean; - isModiOpen: boolean; - //액션 - setIsMore: (value: boolean) => void; - setIsDelete: (value: boolean) => void; - setIsModi: (value: boolean) => void; -} - -export const useCommentStore = create((set) => ({ - //초기상태 - isMoreOpen: false, - isDeleteOpen: false, - isModiOpen: false, - //액션 - setIsMore: (value) => set({ isMoreOpen: value }), - setIsDelete: (value) => set({ isDeleteOpen: value }), - setIsModi: (value) => set({ isModiOpen: value }), - - //비즈니스 로직 -}));