From 1f2bbba0ee043edd2ae70425c376bf2cac0023ee Mon Sep 17 00:00:00 2001 From: ruehan Date: Thu, 5 Dec 2024 17:35:58 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EA=B0=95=EC=95=84=EC=A7=80=20=ED=83=80=EC=9E=85=EC=97=90=20?= =?UTF-8?q?=EC=82=B0=EC=B1=85=20=ED=9A=9F=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/api.ts b/src/types/api.ts index d7d75ea5..2975629f 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -48,6 +48,8 @@ export type OtherDog = { otherDogAge: number /** 다른 강아지 성별 @example "MALE" */ otherDogGender: Gender + /** 다른 강아지 산책 횟수 @example 4 */ + otherDogWalkCount?: number /** 다른 강아지 주인의 유저 ID @example 2 */ memberId: number } From de8fe0fb2a7245b4b0a9f094c846c41402abd2f9 Mon Sep 17 00:00:00 2001 From: ruehan Date: Thu, 5 Dec 2024 17:46:20 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EA=B0=95=EB=B2=88?= =?UTF-8?q?=EB=94=B0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/MapComponent/index.tsx | 41 ++++++++++++++++--- .../components/WalkerListModal/index.tsx | 34 ++++++++------- .../components/WalkerListModal/styles.ts | 8 ++++ src/pages/WalkPage/index.tsx | 13 ++++-- 4 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/pages/WalkPage/components/MapComponent/index.tsx b/src/pages/WalkPage/components/MapComponent/index.tsx index 63980def..266a7878 100644 --- a/src/pages/WalkPage/components/MapComponent/index.tsx +++ b/src/pages/WalkPage/components/MapComponent/index.tsx @@ -32,16 +32,29 @@ export const getMarkerIconString = () => { return svgString } -type MapComponentProps = { - isModalOpen?: boolean -} - // DeviceOrientationEvent 타입 확장 interface ExtendedDeviceOrientationEvent extends DeviceOrientationEvent { webkitCompassHeading?: number } -export default function MapComponent({ isModalOpen = false }: MapComponentProps) { +export type NearbyWalker = { + dogId: number + dogProfileImg: string + dogName: string + breed: string + dogWalkCount: number + dogAge: number + dogGender: 'MALE' | 'FEMALE' + memberId: number + memberEmail: string +} + +type MapComponentProps = { + isModalOpen?: boolean + setNearbyWalkers: React.Dispatch> +} + +export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: MapComponentProps) { const mapRef = useRef(null) const currentLocationMarkerRef = useRef | null>(null) const watchPositionIdRef = useRef(null) @@ -77,7 +90,23 @@ export default function MapComponent({ isModalOpen = false }: MapComponentProps) useEffect(() => { if (isConnected) { const handleMessage = (message: { body: string }) => { - console.log('수신된 메시지:', message.body) + try { + const response = JSON.parse(message.body) + if (response.code === 1000 && response.data) { + const newWalker = response.data as NearbyWalker + setNearbyWalkers((prev: NearbyWalker[]) => { + const exists = prev.some(walker => walker.dogId === newWalker.dogId) + if (!exists) { + return [...prev, newWalker] + } + return prev + }) + } else { + setNearbyWalkers([]) + } + } catch (error) { + console.error('메시지 파싱 실패:', error) + } } subscribe(`/sub/walk/${memberEmail}`, handleMessage) diff --git a/src/pages/WalkPage/components/WalkerListModal/index.tsx b/src/pages/WalkPage/components/WalkerListModal/index.tsx index b792afa3..1ed0b250 100644 --- a/src/pages/WalkPage/components/WalkerListModal/index.tsx +++ b/src/pages/WalkPage/components/WalkerListModal/index.tsx @@ -1,12 +1,14 @@ +import { NearbyWalker } from '~pages/WalkPage/components/MapComponent' import * as S from './styles' -interface WalkerListModalProps { +type WalkerListModalProps = { isOpen: boolean onClose: () => void isClosing: boolean + walkers: NearbyWalker[] } -export default function WalkerListModal({ isOpen, onClose, isClosing }: WalkerListModalProps) { +export default function WalkerListModal({ isOpen, onClose, isClosing, walkers }: WalkerListModalProps) { if (!isOpen) return null const handleBackgroundClick = (e: React.MouseEvent) => { @@ -20,24 +22,25 @@ export default function WalkerListModal({ isOpen, onClose, isClosing }: WalkerLi 강번따 리스트 - - {Array(10) - .fill(0) - .map((_, i) => ( - + {walkers.length > 0 ? ( + + {walkers.map(walker => ( + - + + {walker.dogName} + - 밤돌이 + {walker.dogName} - 포메라니안 + {walker.breed} - 4살 + {walker.dogAge}살 - + {walker.dogGender === 'MALE' ? '남' : '여'} - 산책 횟수

 4회

+ 산책 횟수

 {walker.dogWalkCount}회

@@ -46,7 +49,10 @@ export default function WalkerListModal({ isOpen, onClose, isClosing }: WalkerLi
))} -
+
+ ) : ( + 주변에 산책 가능한 유저가 없습니다. + )}
diff --git a/src/pages/WalkPage/components/WalkerListModal/styles.ts b/src/pages/WalkPage/components/WalkerListModal/styles.ts index d60438f5..a933955c 100644 --- a/src/pages/WalkPage/components/WalkerListModal/styles.ts +++ b/src/pages/WalkPage/components/WalkerListModal/styles.ts @@ -158,3 +158,11 @@ export const WalkBtn = styled(ActionButton)` export const WalkListSeparator = styled(Separator)` margin: 0 4px; ` + +export const NoWalkersMessage = styled.div` + padding: 20px; + text-align: center; + color: ${({ theme }) => theme.colors.grayscale.font_3}; + font-size: ${({ theme }) => theme.typography._17}; + font-weight: 500; +` diff --git a/src/pages/WalkPage/index.tsx b/src/pages/WalkPage/index.tsx index 6abadde9..00fec83c 100644 --- a/src/pages/WalkPage/index.tsx +++ b/src/pages/WalkPage/index.tsx @@ -1,5 +1,5 @@ import { IoChevronBack } from 'react-icons/io5' -import MapComponent from './components/MapComponent' +import MapComponent, { NearbyWalker } from './components/MapComponent' import * as S from './styles' import { Helmet } from 'react-helmet-async' import { useNavigate } from 'react-router-dom' @@ -14,6 +14,8 @@ export default function WalkPage() { const [isWalkerListOpen, setIsWalkerListOpen] = useState(false) const [isClosing, setIsClosing] = useState(false) + const [nearbyWalkers, setNearbyWalkers] = useState([]) + const handleWalkerListClose = () => { setIsClosing(true) setTimeout(() => { @@ -39,8 +41,13 @@ export default function WalkPage() { - - + + ) } From 125ce3f1f015fb81254206cf6676150e68f3ad91 Mon Sep 17 00:00:00 2001 From: ruehan Date: Thu, 5 Dec 2024 22:30:08 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/WebSocketContext.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/WebSocketContext.tsx b/src/WebSocketContext.tsx index c637ab81..cf3834bc 100644 --- a/src/WebSocketContext.tsx +++ b/src/WebSocketContext.tsx @@ -73,7 +73,12 @@ export const WebSocketProvider = ({ children }: { children: React.ReactNode }) = ) } -export const useWebSocket = () => { +export const useWebSocket = (): { + client: Client | null + isConnected: boolean + subscribe: (destination: string, callback: (message: any) => void) => void + publish: (destination: string, body: any) => void +} => { const context = useContext(WebSocketContext) if (!context) { throw new Error('useWebSocket must be used within a WebSocketProvider') From 257ad7f18ccd7d5ec803adafa0450f9e614fd7c7 Mon Sep 17 00:00:00 2001 From: ruehan Date: Thu, 5 Dec 2024 23:46:07 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EA=B0=95=EB=B2=88?= =?UTF-8?q?=EB=94=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8D=94=EB=AF=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WalkPage/components/WalkModal/index.tsx | 22 ++++- .../components/WalkerListModal/index.tsx | 10 ++- .../components/WalkerListModal/styles.ts | 7 ++ src/pages/WalkPage/index.tsx | 90 ++++++++++++++++++- src/types/modal.ts | 4 +- 5 files changed, 125 insertions(+), 8 deletions(-) diff --git a/src/pages/WalkPage/components/WalkModal/index.tsx b/src/pages/WalkPage/components/WalkModal/index.tsx index 252114ff..973a7db1 100644 --- a/src/pages/WalkPage/components/WalkModal/index.tsx +++ b/src/pages/WalkPage/components/WalkModal/index.tsx @@ -2,6 +2,7 @@ import { SetStateAction, useState, useEffect } from 'react' import * as S from './styles' import Select from '~components/Select' import { WalkModalProps, RequestUserInfo, OtherUserInfo } from '~types/modal' +import { useWebSocket } from '~/WebSocketContext' const reportOptions = [ { value: 'dog', label: '강아지가 사나워요.' }, @@ -9,6 +10,23 @@ const reportOptions = [ ] const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalProps) => { + const [message, setMessage] = useState('') + const { publish, isConnected } = useWebSocket() + + const handleConfirm = () => { + if (type === 'request') { + const proposalData = { + otherMemberId: userInfo.email, + message, + } + + publish('/pub/api/v1/proposal', proposalData) + onClose() + } else { + onConfirm() + } + } + useEffect(() => { document.body.style.overflow = 'hidden' @@ -85,7 +103,7 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr {type !== 'progress' && type !== 'report' && type !== 'reportComplete' && ( - +

{userInfo.name}

{type === 'request' || type === 'accept' || type === 'walkRequest' ? ( @@ -141,7 +159,7 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr {modalContent.cancelText} )} - + {modalContent?.confirmText} diff --git a/src/pages/WalkPage/components/WalkerListModal/index.tsx b/src/pages/WalkPage/components/WalkerListModal/index.tsx index 1ed0b250..031fed59 100644 --- a/src/pages/WalkPage/components/WalkerListModal/index.tsx +++ b/src/pages/WalkPage/components/WalkerListModal/index.tsx @@ -6,9 +6,10 @@ type WalkerListModalProps = { onClose: () => void isClosing: boolean walkers: NearbyWalker[] + onWalkRequest: (walker: NearbyWalker) => void } -export default function WalkerListModal({ isOpen, onClose, isClosing, walkers }: WalkerListModalProps) { +export default function WalkerListModal({ isOpen, onClose, isClosing, walkers, onWalkRequest }: WalkerListModalProps) { if (!isOpen) return null const handleBackgroundClick = (e: React.MouseEvent) => { @@ -44,7 +45,12 @@ export default function WalkerListModal({ isOpen, onClose, isClosing, walkers }: - + onWalkRequest(walker)} + > 강번따 diff --git a/src/pages/WalkPage/components/WalkerListModal/styles.ts b/src/pages/WalkPage/components/WalkerListModal/styles.ts index a933955c..1a2407dc 100644 --- a/src/pages/WalkPage/components/WalkerListModal/styles.ts +++ b/src/pages/WalkPage/components/WalkerListModal/styles.ts @@ -111,6 +111,13 @@ export const ProfileCircle = styled.div` border-radius: 50%; background-color: #ffe4d6; margin-right: 12px; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } ` export const InfoArea = styled.div` diff --git a/src/pages/WalkPage/index.tsx b/src/pages/WalkPage/index.tsx index 00fec83c..82b30abc 100644 --- a/src/pages/WalkPage/index.tsx +++ b/src/pages/WalkPage/index.tsx @@ -5,17 +5,82 @@ import { Helmet } from 'react-helmet-async' import { useNavigate } from 'react-router-dom' import { useState } from 'react' import WalkerListModal from '~pages/WalkPage/components/WalkerListModal' +import WalkModal from '~pages/WalkPage/components/WalkModal' + +const dummyWalkers: NearbyWalker[] = [ + { + dogId: 1, + dogName: '멍멍이', + dogProfileImg: 'https://placedog.net/200/200?id=1', + breed: '골든리트리버', + dogAge: 3, + dogGender: 'MALE', + dogWalkCount: 15, + memberId: 1, + memberEmail: 'test@test.com', + }, + { + dogId: 2, + dogName: '초코', + dogProfileImg: 'https://placedog.net/200/200?id=2', + breed: '푸들', + dogAge: 2, + dogGender: 'FEMALE', + dogWalkCount: 8, + memberId: 2, + memberEmail: 'test2@test.com', + }, + { + dogId: 3, + dogName: '바둑이', + dogProfileImg: 'https://placedog.net/200/200?id=3', + breed: '말티즈', + dogAge: 4, + dogGender: 'MALE', + dogWalkCount: 20, + memberId: 3, + memberEmail: 'test3@test.com', + }, + { + dogId: 4, + dogName: '뽀삐', + dogProfileImg: 'https://placedog.net/200/200?id=4', + breed: '포메라니안', + dogAge: 1, + dogGender: 'FEMALE', + dogWalkCount: 5, + memberId: 4, + memberEmail: 'test4@test.com', + }, + { + dogId: 5, + dogName: '보리', + dogProfileImg: 'https://placedog.net/200/200?id=5', + breed: '포메라니안', + dogAge: 1, + dogGender: 'FEMALE', + dogWalkCount: 5, + memberId: 5, + memberEmail: 'test5@test.com', + }, +] export default function WalkPage() { const navigate = useNavigate() - // const [_modalType, _setModalType] = useState<'request' | 'accept' | 'complete' | 'progress' | 'friend' | null>(null) - // const [isModalOpen, _setIsModalOpen] = useState(false) const [isModalOpen] = useState(false) const [isWalkerListOpen, setIsWalkerListOpen] = useState(false) const [isClosing, setIsClosing] = useState(false) const [nearbyWalkers, setNearbyWalkers] = useState([]) + const [isWalkModalOpen, setIsWalkModalOpen] = useState(false) + const [selectedWalker, setSelectedWalker] = useState(null) + + const handleWalkRequest = (walker: NearbyWalker) => { + setSelectedWalker(walker) + setIsWalkModalOpen(true) + } + const handleWalkerListClose = () => { setIsClosing(true) setTimeout(() => { @@ -46,8 +111,27 @@ export default function WalkPage() { isOpen={isWalkerListOpen} onClose={handleWalkerListClose} isClosing={isClosing} - walkers={nearbyWalkers} + walkers={dummyWalkers} + onWalkRequest={handleWalkRequest} /> + + {isWalkModalOpen && selectedWalker && ( + setIsWalkModalOpen(false)} + onConfirm={() => { + // 산책 요청 처리 로직 + setIsWalkModalOpen(false) + }} + /> + )} ) } diff --git a/src/types/modal.ts b/src/types/modal.ts index 62460a03..4e5f799f 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -7,8 +7,10 @@ export type BaseModalProps = { export type RequestUserInfo = { name: string breed: string - age: string + age: number gender: string + profileImg?: string + email?: string } export type OtherUserInfo = { From b0bb38535b00dad61c8636c01ff04c0da4626e8e Mon Sep 17 00:00:00 2001 From: ruehan Date: Fri, 6 Dec 2024 00:42:28 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20=20Feat:=20=EA=B0=95=EB=B2=88?= =?UTF-8?q?=EB=94=B0=20=EC=9A=94=EC=B2=AD=20=EB=B0=8F=20=EC=A0=9C=EC=95=88?= =?UTF-8?q?=20=EC=88=98=EB=9D=BD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/MapComponent/index.tsx | 75 ++++++++++++++++--- .../WalkPage/components/WalkModal/index.tsx | 70 +++++++++++------ .../WalkPage/components/WalkModal/styles.ts | 14 +++- src/types/modal.ts | 1 + 4 files changed, 128 insertions(+), 32 deletions(-) diff --git a/src/pages/WalkPage/components/MapComponent/index.tsx b/src/pages/WalkPage/components/MapComponent/index.tsx index 266a7878..e1d5581f 100644 --- a/src/pages/WalkPage/components/MapComponent/index.tsx +++ b/src/pages/WalkPage/components/MapComponent/index.tsx @@ -18,6 +18,7 @@ import * as S from './styles' import { MIN_ACCURACY, MIN_DISTANCE, MIN_TIME_INTERVAL } from '~types/map' import { useNavigate } from 'react-router-dom' import { useWebSocket } from '~/WebSocketContext' +import WalkModal from '~pages/WalkPage/components/WalkModal' const ORS_API_URL = '/ors/v2/directions/foot-walking/geojson' @@ -32,7 +33,6 @@ export const getMarkerIconString = () => { return svgString } -// DeviceOrientationEvent 타입 확장 interface ExtendedDeviceOrientationEvent extends DeviceOrientationEvent { webkitCompassHeading?: number } @@ -49,6 +49,21 @@ export type NearbyWalker = { memberEmail: string } +export type ProposalResponse = { + code: number + message: string + data: { + dogId: number + dogName: string + dogBreed: string + dogProfileImg: string + comment: string + dogGender: string + dogAge: number + email: string + } +} + type MapComponentProps = { isModalOpen?: boolean setNearbyWalkers: React.Dispatch> @@ -79,6 +94,18 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: const [autoRotate, setAutoRotate] = useState(false) const lastHeadingRef = useRef(0) + const [showProposalModal, setShowProposalModal] = useState(true) + const [proposalInfo, setProposalInfo] = useState({ + dogId: 1, + dogName: '몽이', + dogBreed: '골든 리트리버', + dogProfileImg: 'https://placedog.net/200/200?id=1', + comment: '같이 산책 해요 :)', + dogGender: 'MALE', + dogAge: 5, + email: 'example@example.com', + }) + const [currentAccuracy, setCurrentAccuracy] = useState(null) const navigate = useNavigate() @@ -93,14 +120,20 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: try { const response = JSON.parse(message.body) if (response.code === 1000 && response.data) { - const newWalker = response.data as NearbyWalker - setNearbyWalkers((prev: NearbyWalker[]) => { - const exists = prev.some(walker => walker.dogId === newWalker.dogId) - if (!exists) { - return [...prev, newWalker] - } - return prev - }) + if (response.type === 'proposal') { + const proposalData = response.data as ProposalResponse['data'] + setProposalInfo(proposalData) + setShowProposalModal(true) + } else { + const newWalker = response.data as NearbyWalker + setNearbyWalkers((prev: NearbyWalker[]) => { + const exists = prev.some(walker => walker.dogId === newWalker.dogId) + if (!exists) { + return [...prev, newWalker] + } + return prev + }) + } } else { setNearbyWalkers([]) } @@ -957,6 +990,30 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: )} + + {showProposalModal && proposalInfo && ( + setShowProposalModal(false)} + onConfirm={() => { + // TODO: 수락 로직 구현 + setShowProposalModal(false) + }} + onCancel={() => { + // TODO: 거절 로직 구현 + setShowProposalModal(false) + }} + /> + )} ) } diff --git a/src/pages/WalkPage/components/WalkModal/index.tsx b/src/pages/WalkPage/components/WalkModal/index.tsx index 973a7db1..bb2a04c7 100644 --- a/src/pages/WalkPage/components/WalkModal/index.tsx +++ b/src/pages/WalkPage/components/WalkModal/index.tsx @@ -13,20 +13,43 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr const [message, setMessage] = useState('') const { publish, isConnected } = useWebSocket() + const convertGenderToKorean = (gender: string) => { + return gender === 'MALE' ? '남' : '여' + } + const handleConfirm = () => { if (type === 'request') { const proposalData = { otherMemberId: userInfo.email, message, } - publish('/pub/api/v1/proposal', proposalData) onClose() + } else if (type === 'accept') { + const decisionData = { + otherEmail: (userInfo as RequestUserInfo).email, + decision: 'ACCEPT', + } + publish('/pub/api/v1/decision', decisionData) + onClose() } else { onConfirm() } } + const handleCancel = () => { + if (type === 'accept') { + const decisionData = { + otherEmail: (userInfo as RequestUserInfo).email, + decision: 'DENY', + } + publish('/pub/api/v1/decision', decisionData) + onClose() + } else { + onCancel?.() + } + } + useEffect(() => { document.body.style.overflow = 'hidden' @@ -102,26 +125,29 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr {type !== 'accept' && type === 'friend' &&
2024.12.14
} {type !== 'progress' && type !== 'report' && type !== 'reportComplete' && ( - - - -

{userInfo.name}

- {type === 'request' || type === 'accept' || type === 'walkRequest' ? ( - <> -

- {(userInfo as RequestUserInfo).breed} {' '} - {(userInfo as RequestUserInfo).age} {' '} - {(userInfo as RequestUserInfo).gender} -

- - ) : ( - <> - {(userInfo as OtherUserInfo).location &&

{(userInfo as OtherUserInfo).location}

} - {(userInfo as OtherUserInfo).time &&

{(userInfo as OtherUserInfo).time}

} - - )} -
-
+ <> + + + +

{userInfo.name}

+ {type === 'request' || type === 'accept' || type === 'walkRequest' ? ( + <> +

+ {(userInfo as RequestUserInfo).breed} {' '} + {(userInfo as RequestUserInfo).age} {' '} + {convertGenderToKorean((userInfo as RequestUserInfo).gender)} +

+ + ) : ( + <> + {(userInfo as OtherUserInfo).location &&

{(userInfo as OtherUserInfo).location}

} + {(userInfo as OtherUserInfo).time &&

{(userInfo as OtherUserInfo).time}

} + + )} +
+
+ {type === 'accept' && {(userInfo as RequestUserInfo).comment}} + )} {type === 'report' && ( @@ -155,7 +181,7 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr {modalContent?.cancelText && ( - + {modalContent.cancelText} )} diff --git a/src/pages/WalkPage/components/WalkModal/styles.ts b/src/pages/WalkPage/components/WalkModal/styles.ts index 0a719198..b9e8ab8a 100644 --- a/src/pages/WalkPage/components/WalkModal/styles.ts +++ b/src/pages/WalkPage/components/WalkModal/styles.ts @@ -144,7 +144,7 @@ export const UserInfo = styled.div` }} ` -export const Avatar = styled.div` +export const Avatar = styled.img` width: 48px; height: 48px; border-radius: 50%; @@ -225,6 +225,18 @@ export const Info = styled.div` }} ` +export const ProposalMessage = styled.div` + background-color: ${({ theme }) => theme.colors.grayscale.gc_3}; + border-radius: 16px; + padding: 20px; + font-size: 14px; + font-weight: 500; + color: ${({ theme }) => theme.colors.grayscale.font_3}; + width: 100%; + text-align: left; + line-height: 1.5; +` + export const Message = styled.p` margin: 0; ${({ type }) => { diff --git a/src/types/modal.ts b/src/types/modal.ts index 4e5f799f..12e90474 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -11,6 +11,7 @@ export type RequestUserInfo = { gender: string profileImg?: string email?: string + comment?: string } export type OtherUserInfo = { From da435fb89b5d1385bb81dc6472f14abaa2e3fc09 Mon Sep 17 00:00:00 2001 From: ruehan Date: Fri, 6 Dec 2024 14:32:59 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EA=B0=95=EB=B2=88?= =?UTF-8?q?=EB=94=B0=20=EC=9D=91=EB=8B=B5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/MapComponent/index.tsx | 52 ++++++++++++++++++- .../WalkPage/components/WalkModal/index.tsx | 13 +---- src/types/modal.ts | 3 +- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/pages/WalkPage/components/MapComponent/index.tsx b/src/pages/WalkPage/components/MapComponent/index.tsx index e1d5581f..8991a4c4 100644 --- a/src/pages/WalkPage/components/MapComponent/index.tsx +++ b/src/pages/WalkPage/components/MapComponent/index.tsx @@ -61,6 +61,18 @@ export type ProposalResponse = { dogGender: string dogAge: number email: string + type: 'PROPOSAL' + } +} + +export type DecisionResponse = { + code: number + message: string + data: { + decision: 'ACCEPT' | 'DENY' + otherMemberName: string + otherMemberProfileImg: string + type: 'DECISION' } } @@ -95,6 +107,7 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: const lastHeadingRef = useRef(0) const [showProposalModal, setShowProposalModal] = useState(true) + const [showDecisionModal, setShowDecisionModal] = useState(true) const [proposalInfo, setProposalInfo] = useState({ dogId: 1, dogName: '몽이', @@ -104,6 +117,14 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: dogGender: 'MALE', dogAge: 5, email: 'example@example.com', + type: 'PROPOSAL', + }) + + const [decisionInfo, setDecisionInfo] = useState({ + decision: 'ACCEPT', + otherMemberName: '홍길동', + otherMemberProfileImg: 'https://placedog.net/200/200?id=2', + type: 'DECISION', }) const [currentAccuracy, setCurrentAccuracy] = useState(null) @@ -120,10 +141,20 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: try { const response = JSON.parse(message.body) if (response.code === 1000 && response.data) { - if (response.type === 'proposal') { + if (response.data.type === 'PROPOSAL') { const proposalData = response.data as ProposalResponse['data'] setProposalInfo(proposalData) setShowProposalModal(true) + } else if (response.data.type === 'DECISION') { + const decisionData = response.data as DecisionResponse['data'] + + if (decisionData.decision === 'ACCEPT') { + } else { + setDecisionInfo(decisionData) + setShowDecisionModal(true) + } + // 수락 시 같이 산책 모드 + // 거절 시 거절 모달 띄워 줌 } else { const newWalker = response.data as NearbyWalker setNearbyWalkers((prev: NearbyWalker[]) => { @@ -999,7 +1030,7 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: breed: proposalInfo.dogBreed, profileImg: proposalInfo.dogProfileImg, age: proposalInfo.dogAge, - gender: proposalInfo.dogGender, + gender: proposalInfo.dogGender === 'MALE' ? '남' : '여', email: proposalInfo.email, comment: proposalInfo.comment, }} @@ -1014,6 +1045,23 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: }} /> )} + {showDecisionModal && decisionInfo && ( + setShowDecisionModal(false)} + onConfirm={() => { + // TODO: 다시 시도 구현 + setShowDecisionModal(false) + }} + onCancel={() => { + setShowDecisionModal(false) + }} + /> + )} ) } diff --git a/src/pages/WalkPage/components/WalkModal/index.tsx b/src/pages/WalkPage/components/WalkModal/index.tsx index bb2a04c7..8702986b 100644 --- a/src/pages/WalkPage/components/WalkModal/index.tsx +++ b/src/pages/WalkPage/components/WalkModal/index.tsx @@ -13,10 +13,6 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr const [message, setMessage] = useState('') const { publish, isConnected } = useWebSocket() - const convertGenderToKorean = (gender: string) => { - return gender === 'MALE' ? '남' : '여' - } - const handleConfirm = () => { if (type === 'request') { const proposalData = { @@ -130,19 +126,14 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr

{userInfo.name}

- {type === 'request' || type === 'accept' || type === 'walkRequest' ? ( + {(type === 'request' || type === 'accept' || type === 'walkRequest') ?? ( <>

{(userInfo as RequestUserInfo).breed} {' '} {(userInfo as RequestUserInfo).age} {' '} - {convertGenderToKorean((userInfo as RequestUserInfo).gender)} + {(userInfo as RequestUserInfo).gender}

- ) : ( - <> - {(userInfo as OtherUserInfo).location &&

{(userInfo as OtherUserInfo).location}

} - {(userInfo as OtherUserInfo).time &&

{(userInfo as OtherUserInfo).time}

} - )}
diff --git a/src/types/modal.ts b/src/types/modal.ts index 12e90474..cef7613f 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -16,8 +16,7 @@ export type RequestUserInfo = { export type OtherUserInfo = { name: string - location?: string - time?: string + profileImg: string } export type ModalContentType = { From c2efa844113b87adefb8307f72e31ef749af2a23 Mon Sep 17 00:00:00 2001 From: ruehan Date: Mon, 9 Dec 2024 01:56:10 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EA=B0=95=EB=B2=88?= =?UTF-8?q?=EB=94=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EC=82=B0=EC=B1=85=20=EC=99=84=EB=A3=8C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/walk/fetchWalkComplete.ts | 80 ++++ src/assets/map_pin_1.svg | 37 ++ src/assets/map_pin_2.svg | 39 ++ src/pages/WalkCompletePage/index.tsx | 25 +- .../components/MapComponent/index.tsx | 358 ++++++++++++------ .../components/MapComponent/styles.ts | 8 +- .../WalkPage/components/WalkModal/index.tsx | 22 +- src/pages/WalkPage/index.tsx | 61 +-- src/types/modal.ts | 2 +- 9 files changed, 448 insertions(+), 184 deletions(-) create mode 100644 src/apis/walk/fetchWalkComplete.ts create mode 100644 src/assets/map_pin_1.svg create mode 100644 src/assets/map_pin_2.svg diff --git a/src/apis/walk/fetchWalkComplete.ts b/src/apis/walk/fetchWalkComplete.ts new file mode 100644 index 00000000..b8b47397 --- /dev/null +++ b/src/apis/walk/fetchWalkComplete.ts @@ -0,0 +1,80 @@ +import { AxiosError } from 'axios' +import { APIResponse, ErrorResponse } from '~types/api' +import { axiosInstance } from '~apis/axiosInstance' +import { useQuery } from '@tanstack/react-query' + +export const WALK_COMPLETE_QUERY_KEY = 'walkComplete' as const + +export type FetchWalkCompleteRequest = { + walkImgFile: File + totalDistanceMeter: number + totalWalkTimeSecond: number +} + +export type TimeDuration = { + hours: number + minutes: number + seconds: number +} + +export type WalkWithDogInfo = { + otherDogId: number + otherDogProfileImg: string + otherDogName: string + otherDogBreed: string + otherDogAge: number + otherDogGender: 'MALE' | 'FEMALE' + memberId: number +} + +export type FetchWalkCompleteResponse = { + date: string + memberName: string + dogName: string + totalDistanceMeter: number + timeDuration: TimeDuration + totalCalorie: number + walkImg: string + walkWithDogInfo: WalkWithDogInfo +} + +export const fetchWalkComplete = async (formData: FormData): Promise> => { + try { + const { data } = await axiosInstance.post>(`/walk/complete`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + return data + } catch (error) { + if (error instanceof AxiosError) { + const { response } = error as AxiosError + + if (response) { + const { code, message } = response.data + switch (code) { + case 400: + throw new Error(message || '잘못된 요청입니다.') + case 401: + throw new Error(message || '인증에 실패했습니다.') + case 500: + throw new Error(message || '서버 오류가 발생했습니다.') + default: + throw new Error(message || '알 수 없는 오류가 발생했습니다.') + } + } + throw new Error('네트워크 연결을 확인해주세요') + } + + console.error('예상치 못한 에러:', error) + throw new Error('다시 시도해주세요') + } +} + +export const useWalkComplete = (formData: FormData) => { + return useQuery({ + queryKey: [WALK_COMPLETE_QUERY_KEY, formData], + queryFn: () => fetchWalkComplete(formData), + enabled: false, + }) +} diff --git a/src/assets/map_pin_1.svg b/src/assets/map_pin_1.svg new file mode 100644 index 00000000..47291d34 --- /dev/null +++ b/src/assets/map_pin_1.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/map_pin_2.svg b/src/assets/map_pin_2.svg new file mode 100644 index 00000000..63437afd --- /dev/null +++ b/src/assets/map_pin_2.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/WalkCompletePage/index.tsx b/src/pages/WalkCompletePage/index.tsx index dd4ea443..58edf93c 100644 --- a/src/pages/WalkCompletePage/index.tsx +++ b/src/pages/WalkCompletePage/index.tsx @@ -2,8 +2,10 @@ import { useLocation } from 'react-router-dom' import * as S from './styles' interface WalkCompleteData { + dogName: string + memberName: string date: string - time: string + timeDuration: { hours: number; minutes: number; seconds: number } distance: string calories: string mapImage: string @@ -11,7 +13,10 @@ interface WalkCompleteData { export default function WalkCompletePage() { const location = useLocation() + console.log(location.state) const walkData: WalkCompleteData = { + dogName: location.state?.dogName || '', + memberName: location.state?.memberName || '', date: new Date() .toLocaleDateString('ko-KR', { year: 'numeric', @@ -19,27 +24,27 @@ export default function WalkCompletePage() { day: '2-digit', }) .replace(/\. /g, '.'), - time: location.state?.time || '00:00:00', - distance: location.state?.distance || '0m', - calories: '200kcal', - mapImage: location.state?.mapImage || '', + timeDuration: location.state?.timeDuration, + distance: `${location.state?.totalDistanceMeter}m` || '0m', + calories: `${location.state?.totalCalorie}kcal` || '0kcal', + mapImage: location.state?.walkImg || '', } console.log(walkData) - const getMinutesFromTime = (time: string) => { - const [hours, minutes] = time.split(':').map(Number) + const getMinutesFromTime = (time: { hours: number; minutes: number; seconds?: number }) => { + const { hours, minutes } = time return hours * 60 + minutes } - const walkTimeInMinutes = getMinutesFromTime(walkData.time) + const walkTimeInMinutes = getMinutesFromTime(walkData.timeDuration) return ( {walkData.date} - 견주닉넴과 밤톨이가 + {walkData.memberName}와(과) {walkData.dogName}가
{walkTimeInMinutes}분동안 산책했어요.
@@ -49,7 +54,7 @@ export default function WalkCompletePage() { - {walkData.time} + {`${walkData.timeDuration.hours} : ${walkData.timeDuration.minutes} : ${walkData.timeDuration.seconds}`} 산책 시간 diff --git a/src/pages/WalkPage/components/MapComponent/index.tsx b/src/pages/WalkPage/components/MapComponent/index.tsx index 8991a4c4..e31f1457 100644 --- a/src/pages/WalkPage/components/MapComponent/index.tsx +++ b/src/pages/WalkPage/components/MapComponent/index.tsx @@ -18,6 +18,8 @@ import * as S from './styles' import { MIN_ACCURACY, MIN_DISTANCE, MIN_TIME_INTERVAL } from '~types/map' import { useNavigate } from 'react-router-dom' import { useWebSocket } from '~/WebSocketContext' +import { useMutation } from '@tanstack/react-query' +import { fetchWalkComplete } from '~apis/walk/fetchWalkComplete' import WalkModal from '~pages/WalkPage/components/WalkModal' const ORS_API_URL = '/ors/v2/directions/foot-walking/geojson' @@ -33,6 +35,11 @@ export const getMarkerIconString = () => { return svgString } +export const getMarkerIconString2 = () => { + const svgString = ReactDOMServer.renderToString() + return svgString +} + interface ExtendedDeviceOrientationEvent extends DeviceOrientationEvent { webkitCompassHeading?: number } @@ -106,8 +113,8 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: const [autoRotate, setAutoRotate] = useState(false) const lastHeadingRef = useRef(0) - const [showProposalModal, setShowProposalModal] = useState(true) - const [showDecisionModal, setShowDecisionModal] = useState(true) + const [showProposalModal, setShowProposalModal] = useState(false) + const [showDecisionModal, setShowDecisionModal] = useState(false) const [proposalInfo, setProposalInfo] = useState({ dogId: 1, dogName: '몽이', @@ -129,51 +136,79 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: const [currentAccuracy, setCurrentAccuracy] = useState(null) + const [isWalkingTogether, setIsWalkingTogether] = useState(false) + const [, setWalkPartnerEmail] = useState(null) + const navigate = useNavigate() const { subscribe, publish, isConnected } = useWebSocket() - const memberEmail = 'mkh6793@naver.com' + const memberEmail = localStorage.getItem('email') + + // const { data } = useQuery({ + // queryKey: ['walkPage'], + // queryFn: fetchMypage, + // }) + + const walkCompleteMutation = useMutation({ + mutationFn: (walkData: FormData) => fetchWalkComplete(walkData), + }) + + const partnerMarkerRef = useRef(null) useEffect(() => { + console.log('isConnected', isConnected) if (isConnected) { const handleMessage = (message: { body: string }) => { try { const response = JSON.parse(message.body) - if (response.code === 1000 && response.data) { - if (response.data.type === 'PROPOSAL') { - const proposalData = response.data as ProposalResponse['data'] - setProposalInfo(proposalData) - setShowProposalModal(true) - } else if (response.data.type === 'DECISION') { - const decisionData = response.data as DecisionResponse['data'] - - if (decisionData.decision === 'ACCEPT') { - } else { - setDecisionInfo(decisionData) - setShowDecisionModal(true) - } - // 수락 시 같이 산책 모드 - // 거절 시 거절 모달 띄워 줌 + console.log('response', response) + const type = Array.isArray(response.data) ? response.data[0].type : response.data.type + + if (type === 'WALK_ALONE') { + const nearbyWalkersData = response.data.map((walker: NearbyWalker) => ({ + dogId: walker.dogId, + dogProfileImg: walker.dogProfileImg, + dogName: walker.dogName, + breed: walker.breed, + dogWalkCount: walker.dogWalkCount, + dogAge: walker.dogAge, + dogGender: walker.dogGender, + memberId: walker.memberId, + memberEmail: walker.memberEmail, + })) + + setNearbyWalkers(nearbyWalkersData) + } else if (type === 'PROPOSAL') { + const proposalData = response.data as ProposalResponse['data'] + setProposalInfo(proposalData) + setShowProposalModal(true) + setWalkPartnerEmail(proposalData.email) + } else if (type === 'DECISION') { + const decisionData = response.data as DecisionResponse['data'] + + if (decisionData.decision === 'DENY') { + setDecisionInfo(decisionData) + setShowDecisionModal(true) } else { - const newWalker = response.data as NearbyWalker - setNearbyWalkers((prev: NearbyWalker[]) => { - const exists = prev.some(walker => walker.dogId === newWalker.dogId) - if (!exists) { - return [...prev, newWalker] - } - return prev - }) + setIsWalkingTogether(true) } - } else { - setNearbyWalkers([]) + } else if (type === 'WALK_WITH') { + setIsWalkingTogether(true) + setWalkPartnerEmail(response.data.email) + + updatePartnerLocation(response.data.latitude, response.data.longitude) } } catch (error) { console.error('메시지 파싱 실패:', error) } } + const handleError = (message: { body: string }) => { + console.error('WebSocket 오류:', message.body) + } subscribe(`/sub/walk/${memberEmail}`, handleMessage) + subscribe('/user/queue/errors', handleError) } }, [isConnected, subscribe, memberEmail]) @@ -539,8 +574,7 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: const filterPosition = (position: GeolocationPosition): boolean => { const isAccurate = position.coords.accuracy <= MIN_ACCURACY - const accuracy = position.coords.accuracy - setCurrentAccuracy(accuracy) + setCurrentAccuracy(position.coords.accuracy) return isAccurate } @@ -716,86 +750,117 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: } } - const handleWalkToggle = async () => { - if (!isWalking) { - handleCompassPermission() + const base64ToFile = (base64String: string): File => { + // base64 문자열에서 데이터 URI 스키마 제거 + const base64Content = base64String.split(',')[1] + // base64를 바이너리 데이터로 변환 + const binaryData = atob(base64Content) - setIsWalking(true) - setAutoRotate(true) - setWalkDistance(0) - setEstimatedDistance(0) - accumulatedPositionsRef.current = [] + // 바이너리 데이터를 Uint8Array로 변환 + const bytes = new Uint8Array(binaryData.length) + for (let i = 0; i < binaryData.length; i++) { + bytes[i] = binaryData.charCodeAt(i) + } - routeSourceRef.current.clear() - vectorSourceRef.current.clear() + // Blob 생성 + const blob = new Blob([bytes], { type: 'image/png' }) - rotateMap(lastHeadingRef.current) - watchPositionIdRef.current = navigator.geolocation.watchPosition( - (position: GeolocationPosition) => { - if (!filterPosition(position)) return + // File 객체 생성 + const fileName = `walk-map-${new Date().getTime()}.png` + return new File([blob], fileName, { type: 'image/png' }) + } - const newPosition = { - lat: position.coords.latitude, - lng: position.coords.longitude, - } + const startWatchingPosition = () => { + return navigator.geolocation.watchPosition( + (position: GeolocationPosition) => { + if (!filterPosition(position)) return + + const newPosition = { + lat: position.coords.latitude, + lng: position.coords.longitude, + } - const coordinates = fromLonLat([newPosition.lng, newPosition.lat]) + const coordinates = fromLonLat([newPosition.lng, newPosition.lat]) - if (currentLocationMarkerRef.current) { - const point = currentLocationMarkerRef.current.getGeometry() as Point - point.setCoordinates(coordinates) - vectorSourceRef.current.changed() - } else { - currentLocationMarkerRef.current = new Feature({ - geometry: new Point(coordinates), - }) - const markerStyle = new Style({ - image: new Icon({ - anchor: [0.5, 1], - anchorXUnits: 'fraction', - anchorYUnits: 'fraction', - src: `data:image/svg+xml;utf8,${encodeURIComponent(getMarkerIconString())}`, - scale: 1, - }), - }) - currentLocationMarkerRef.current.setStyle(markerStyle) - vectorSourceRef.current.addFeature(currentLocationMarkerRef.current) + if (currentLocationMarkerRef.current) { + const point = currentLocationMarkerRef.current.getGeometry() as Point + point.setCoordinates(coordinates) + vectorSourceRef.current.changed() + } else { + currentLocationMarkerRef.current = new Feature({ + geometry: new Point(coordinates), + }) + const markerStyle = new Style({ + image: new Icon({ + anchor: [0.5, 1], + anchorXUnits: 'fraction', + anchorYUnits: 'fraction', + src: `data:image/svg+xml;utf8,${encodeURIComponent(getMarkerIconString())}`, + scale: 1, + }), + }) + currentLocationMarkerRef.current.setStyle(markerStyle) + vectorSourceRef.current.addFeature(currentLocationMarkerRef.current) + } + + if (mapRef.current) { + mapRef.current.getView().animate({ + center: coordinates, + duration: 500, + }) + } + + if (shouldCallApi(newPosition)) { + accumulatedPositionsRef.current.push(newPosition) + addWalkLocationMarker(coordinates) + + const walkData = { + latitude: newPosition.lat.toFixed(6), + longitude: newPosition.lng.toFixed(6), } - if (mapRef.current) { - mapRef.current.getView().animate({ - center: coordinates, - duration: 500, - }) + const endpoint = isWalkingTogether ? '/pub/api/v1/walk-with' : '/pub/api/v1/walk-alone' + publish(endpoint, walkData) + + if (accumulatedPositionsRef.current.length >= 2) { + const lastTwoPositions = accumulatedPositionsRef.current.slice(-2) + calculateWalkingDistance(lastTwoPositions) } - if (shouldCallApi(newPosition)) { - accumulatedPositionsRef.current.push(newPosition) - addWalkLocationMarker(coordinates) + lastApiCallTimeRef.current = Date.now() + } - try { - publish('/pub/api/v1/walk-alone', { - latitude: newPosition.lat, - longitude: newPosition.lng, - }) - console.log('웹소켓 발행 성공') - } catch (error) { - console.error('웹소켓 발행 실패:', error) - } + updateEstimatedDistance() + }, + handleLocationError, + getGeoOptions() + ) + } - if (accumulatedPositionsRef.current.length >= 2) { - const lastTwoPositions = accumulatedPositionsRef.current.slice(-2) - calculateWalkingDistance(lastTwoPositions) - } + useEffect(() => { + if (isWalking) { + if (watchPositionIdRef.current !== null) { + navigator.geolocation.clearWatch(watchPositionIdRef.current) + } + watchPositionIdRef.current = startWatchingPosition() + } + }, [isWalkingTogether, isWalking]) - lastApiCallTimeRef.current = Date.now() - } + const handleWalkToggle = async () => { + if (!isWalking) { + handleCompassPermission() - updateEstimatedDistance() - }, - handleLocationError, - getGeoOptions() - ) + setIsWalking(true) + setAutoRotate(true) + setWalkDistance(0) + setEstimatedDistance(0) + accumulatedPositionsRef.current = [] + + routeSourceRef.current.clear() + vectorSourceRef.current.clear() + + rotateMap(lastHeadingRef.current) + watchPositionIdRef.current = startWatchingPosition() walkIntervalRef.current = window.setInterval(() => { setWalkTime(prev => prev + 1) @@ -816,15 +881,35 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: const mapImage = await captureMap() - const walkCompleteData = { - time: formatTime(walkTime), - distance: formatDistance(walkDistance || estimatedDistance), - mapImage: mapImage, - } + try { + const mapImageFile = base64ToFile(mapImage) + + // FormData 객체 생성 + const formData = new FormData() + formData.append('walkImgFile', mapImageFile) + formData.append( + 'request', + new Blob( + [ + JSON.stringify({ + totalDistanceMeter: walkDistance || estimatedDistance, + totalWalkTimeSecond: walkTime, + }), + ], + { type: 'application/json' } + ) + ) - navigate('/walk-complete', { - state: walkCompleteData, - }) + const response = await walkCompleteMutation.mutateAsync(formData) + + console.log(response) + + navigate('/walk-complete', { + state: response.data, + }) + } catch (error) { + console.error('산책 완료 데이터 전송 실패:', error) + } } } @@ -872,8 +957,8 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: const drawRoute = (coordinates: number[][], append: boolean = false) => { const routeStyle = new Style({ stroke: new Stroke({ - color: '#FF6B6B', - width: 5, + color: '#ECB99A', + width: 10, lineCap: 'round', lineJoin: 'round', }), @@ -934,6 +1019,65 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: } }, [isWalking]) + // 파트너 위치 마커 스타일 정의 + const getPartnerMarkerStyle = new Style({ + image: new Icon({ + anchor: [0.5, 1], + anchorXUnits: 'fraction', + anchorYUnits: 'fraction', + src: `data:image/svg+xml;utf8,${encodeURIComponent(getMarkerIconString2())}`, + + scale: 0.75, + }), + }) + + // 파트너 위치 업데이트 함수 + const updatePartnerLocation = (latitude: number, longitude: number) => { + const coordinates = fromLonLat([longitude, latitude]) + + if (partnerMarkerRef.current) { + // 기존 마커 위치 업데이트 + const point = partnerMarkerRef.current.getGeometry() as Point + const currentCoords = point.getCoordinates() + + // 부드러운 애니메이션으로 위치 이동 + const duration = 2000 + const start = Date.now() + const animate = () => { + const elapsed = Date.now() - start + const progress = Math.min(elapsed / duration, 1) + const easeProgress = easeOut(progress) + + const x = currentCoords[0] + (coordinates[0] - currentCoords[0]) * easeProgress + const y = currentCoords[1] + (coordinates[1] - currentCoords[1]) * easeProgress + + point.setCoordinates([x, y]) + + if (progress < 1) { + requestAnimationFrame(animate) + } + } + + animate() + } else { + // 새로운 마커 생성 + partnerMarkerRef.current = new Feature({ + geometry: new Point(coordinates), + }) + partnerMarkerRef.current.setStyle(getPartnerMarkerStyle) + vectorSourceRef.current.addFeature(partnerMarkerRef.current) + } + } + + // 컴포넌트 정리 시 파트너 마커 제거 + useEffect(() => { + return () => { + if (partnerMarkerRef.current) { + vectorSourceRef.current.removeFeature(partnerMarkerRef.current) + } + } + }, []) + return ( @@ -1031,7 +1175,7 @@ export default function MapComponent({ isModalOpen = false, setNearbyWalkers }: profileImg: proposalInfo.dogProfileImg, age: proposalInfo.dogAge, gender: proposalInfo.dogGender === 'MALE' ? '남' : '여', - email: proposalInfo.email, + memberEmail: proposalInfo.email, comment: proposalInfo.comment, }} onClose={() => setShowProposalModal(false)} diff --git a/src/pages/WalkPage/components/MapComponent/styles.ts b/src/pages/WalkPage/components/MapComponent/styles.ts index 4ce1eec6..46e96497 100644 --- a/src/pages/WalkPage/components/MapComponent/styles.ts +++ b/src/pages/WalkPage/components/MapComponent/styles.ts @@ -1,7 +1,8 @@ import { styled } from 'styled-components' import { FOOTER_HEIGHT } from '~constants/layout' import DogProfile from '~assets/walk_dog.svg?react' -import MapPin from '~assets/walk_pin.svg?react' +import MapPin from '~assets/map_pin_1.svg?react' +import MapPin2 from '~assets/map_pin_2.svg?react' import { ActionButton } from '~components/Button/ActionButton' import Center from '~assets/center.svg?react' import { Box } from '~components/Box' @@ -71,6 +72,11 @@ export const MapPinIcon = styled(MapPin)` height: 100%; ` +export const MapPinIcon2 = styled(MapPin2)` + width: 100%; + height: 100%; +` + export const WalkControlContainer = styled.div` position: absolute; bottom: 20px; diff --git a/src/pages/WalkPage/components/WalkModal/index.tsx b/src/pages/WalkPage/components/WalkModal/index.tsx index 8702986b..eb187682 100644 --- a/src/pages/WalkPage/components/WalkModal/index.tsx +++ b/src/pages/WalkPage/components/WalkModal/index.tsx @@ -13,19 +13,22 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr const [message, setMessage] = useState('') const { publish, isConnected } = useWebSocket() + console.log(userInfo) const handleConfirm = () => { if (type === 'request') { const proposalData = { - otherMemberId: userInfo.email, - message, + otherMemberEmail: (userInfo as RequestUserInfo).memberEmail, + comment: message, } + console.log(proposalData) publish('/pub/api/v1/proposal', proposalData) onClose() } else if (type === 'accept') { const decisionData = { - otherEmail: (userInfo as RequestUserInfo).email, + otherEmail: (userInfo as RequestUserInfo).memberEmail, decision: 'ACCEPT', } + console.log(decisionData) publish('/pub/api/v1/decision', decisionData) onClose() } else { @@ -36,7 +39,7 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr const handleCancel = () => { if (type === 'accept') { const decisionData = { - otherEmail: (userInfo as RequestUserInfo).email, + otherEmail: (userInfo as RequestUserInfo).memberEmail, decision: 'DENY', } publish('/pub/api/v1/decision', decisionData) @@ -126,7 +129,7 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr

{userInfo.name}

- {(type === 'request' || type === 'accept' || type === 'walkRequest') ?? ( + {(type === 'request' || type === 'accept' || type === 'walkRequest') && ( <>

{(userInfo as RequestUserInfo).breed} {' '} @@ -156,7 +159,14 @@ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalPr )} {type === 'request' ? ( - + setMessage(e.target.value)} + /> ) : type === 'progress' ? ( {modalContent?.message} diff --git a/src/pages/WalkPage/index.tsx b/src/pages/WalkPage/index.tsx index 82b30abc..b2278f86 100644 --- a/src/pages/WalkPage/index.tsx +++ b/src/pages/WalkPage/index.tsx @@ -7,64 +7,6 @@ import { useState } from 'react' import WalkerListModal from '~pages/WalkPage/components/WalkerListModal' import WalkModal from '~pages/WalkPage/components/WalkModal' -const dummyWalkers: NearbyWalker[] = [ - { - dogId: 1, - dogName: '멍멍이', - dogProfileImg: 'https://placedog.net/200/200?id=1', - breed: '골든리트리버', - dogAge: 3, - dogGender: 'MALE', - dogWalkCount: 15, - memberId: 1, - memberEmail: 'test@test.com', - }, - { - dogId: 2, - dogName: '초코', - dogProfileImg: 'https://placedog.net/200/200?id=2', - breed: '푸들', - dogAge: 2, - dogGender: 'FEMALE', - dogWalkCount: 8, - memberId: 2, - memberEmail: 'test2@test.com', - }, - { - dogId: 3, - dogName: '바둑이', - dogProfileImg: 'https://placedog.net/200/200?id=3', - breed: '말티즈', - dogAge: 4, - dogGender: 'MALE', - dogWalkCount: 20, - memberId: 3, - memberEmail: 'test3@test.com', - }, - { - dogId: 4, - dogName: '뽀삐', - dogProfileImg: 'https://placedog.net/200/200?id=4', - breed: '포메라니안', - dogAge: 1, - dogGender: 'FEMALE', - dogWalkCount: 5, - memberId: 4, - memberEmail: 'test4@test.com', - }, - { - dogId: 5, - dogName: '보리', - dogProfileImg: 'https://placedog.net/200/200?id=5', - breed: '포메라니안', - dogAge: 1, - dogGender: 'FEMALE', - dogWalkCount: 5, - memberId: 5, - memberEmail: 'test5@test.com', - }, -] - export default function WalkPage() { const navigate = useNavigate() const [isModalOpen] = useState(false) @@ -111,7 +53,7 @@ export default function WalkPage() { isOpen={isWalkerListOpen} onClose={handleWalkerListClose} isClosing={isClosing} - walkers={dummyWalkers} + walkers={nearbyWalkers} onWalkRequest={handleWalkRequest} /> @@ -124,6 +66,7 @@ export default function WalkPage() { age: selectedWalker.dogAge, gender: selectedWalker.dogGender === 'MALE' ? '남' : '여', profileImg: selectedWalker.dogProfileImg, + memberEmail: selectedWalker.memberEmail, }} onClose={() => setIsWalkModalOpen(false)} onConfirm={() => { diff --git a/src/types/modal.ts b/src/types/modal.ts index cef7613f..a2691742 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -10,7 +10,7 @@ export type RequestUserInfo = { age: number gender: string profileImg?: string - email?: string + memberEmail?: string comment?: string } From 5efadbc238cb9b411d90672c9e17b120c22b6cff Mon Sep 17 00:00:00 2001 From: ruehan Date: Mon, 9 Dec 2024 01:58:10 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/WalkPage/components/WalkModal/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/WalkPage/components/WalkModal/index.tsx b/src/pages/WalkPage/components/WalkModal/index.tsx index eb187682..aee79813 100644 --- a/src/pages/WalkPage/components/WalkModal/index.tsx +++ b/src/pages/WalkPage/components/WalkModal/index.tsx @@ -1,7 +1,7 @@ import { SetStateAction, useState, useEffect } from 'react' import * as S from './styles' import Select from '~components/Select' -import { WalkModalProps, RequestUserInfo, OtherUserInfo } from '~types/modal' +import { WalkModalProps, RequestUserInfo } from '~types/modal' import { useWebSocket } from '~/WebSocketContext' const reportOptions = [ @@ -11,9 +11,8 @@ const reportOptions = [ const WalkModal = ({ type, userInfo, onClose, onConfirm, onCancel }: WalkModalProps) => { const [message, setMessage] = useState('') - const { publish, isConnected } = useWebSocket() + const { publish } = useWebSocket() - console.log(userInfo) const handleConfirm = () => { if (type === 'request') { const proposalData = {