diff --git a/src/assets/result_dog.svg b/src/assets/result_dog.svg new file mode 100644 index 0000000..bd460df --- /dev/null +++ b/src/assets/result_dog.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Button/ActionButton.ts b/src/components/Button/ActionButton.ts index 5501ba6..9f2eadd 100644 --- a/src/components/Button/ActionButton.ts +++ b/src/components/Button/ActionButton.ts @@ -1,7 +1,7 @@ import styled, { BrandColors, FontWeight, GrayscaleColors, Typography } from 'styled-components' type BgColorType = - | Extract + | Extract | Extract type ActionButtonProps = { @@ -19,6 +19,7 @@ type ActionButtonStyles = { const ACTION_BUTTON_FONT_COLORS: Record = { default: 'gc_4', lighten_2: 'font_1', + lighten_3: 'font_1', font_1: 'gc_4', gc_4: 'font_1', gc_1: 'font_4', diff --git a/src/components/LazyComponents.tsx b/src/components/LazyComponents.tsx index be7ed86..f8bb3ae 100644 --- a/src/components/LazyComponents.tsx +++ b/src/components/LazyComponents.tsx @@ -8,4 +8,5 @@ export const WalkPage = lazy(() => import('~pages/WalkPage')) export const SocialPage = lazy(() => import('~pages/SocialPage')) export const RegisterPage = lazy(() => import('~pages/RegisterPage/Register')) export const RegisterDogPage = lazy(() => import('~pages/RegisterPage/Dog')) +export const WalkCompletePage = lazy(() => import('~pages/WalkCompletePage')) export const ProfilePage = lazy(() => import('~pages/ProfilePage')) diff --git a/src/pages/WalkCompletePage/index.tsx b/src/pages/WalkCompletePage/index.tsx new file mode 100644 index 0000000..4427939 --- /dev/null +++ b/src/pages/WalkCompletePage/index.tsx @@ -0,0 +1,45 @@ +import * as S from './styles' + +const walkData = { + date: '2024.12.14', + time: '1:10:00', + distance: '2.4km', + calories: '200kcal', + mapImage: 'https://imagedelivery.net/CJyrB-EkqcsF2D6ApJzEBg/6d853db2-fb51-465c-eaa8-e9e38be01f00/public', +} + +export default function WalkCompletePage() { + return ( + + {walkData.date} + + + 견주닉넴과 밤톨이가 +
+ 30분동안 산책했어요. +
+ + + + + + + {walkData.time} + 산책 시간 + + + {walkData.distance} + 산책 거리 + + + {walkData.calories} + 소모한 칼로리 + + + + + 산책 경로 + +
+ ) +} diff --git a/src/pages/WalkCompletePage/styles.ts b/src/pages/WalkCompletePage/styles.ts new file mode 100644 index 0000000..a3d3274 --- /dev/null +++ b/src/pages/WalkCompletePage/styles.ts @@ -0,0 +1,76 @@ +import { styled } from 'styled-components' +import WalkCompleteDog from '~assets/result_dog.svg?react' +import { Box } from '~components/Box' +import { Typo13, Typo17, Typo20 } from '~components/Typo' +import { FOOTER_HEIGHT } from '~constants/layout' + +export const WalkCompletePage = styled.div` + background-color: ${({ theme }) => theme.colors.brand.lighten_3}; + width: 100%; + height: calc(100dvh - ${FOOTER_HEIGHT}px); + padding: 24px; + display: flex; + flex-direction: column; + gap: 20px; +` + +export const Date = styled(Typo17)` + font-weight: 700; + color: #333; +` + +export const Title = styled(Typo20)` + font-weight: 800; + + span { + color: #8b4513; + } +` + +export const WalkStats = styled(Box)` + display: flex; + justify-content: space-between; + padding: 22px 33px; + /* border-radius: 12px; */ +` + +export const StatItem = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +` + +export const StatValue = styled(Typo20)` + font-weight: 800; + color: ${({ theme }) => theme.colors.grayscale.font_1}; +` + +export const StatLabel = styled(Typo13)` + font-weight: 500; + color: ${({ theme }) => theme.colors.grayscale.font_1}; +` + +export const MapSection = styled.div` + width: 100%; + height: 240px; + border-radius: 12px; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +` + +export const DogImageArea = styled.div` + display: flex; + justify-content: right; +` + +export const DogImage = styled(WalkCompleteDog)` + width: 142px; + height: 151px; + display: flex; +` diff --git a/src/pages/WalkPage/components/MapComponent/index.tsx b/src/pages/WalkPage/components/MapComponent/index.tsx index 32aa557..3ca2ed8 100644 --- a/src/pages/WalkPage/components/MapComponent/index.tsx +++ b/src/pages/WalkPage/components/MapComponent/index.tsx @@ -16,6 +16,7 @@ import XYZ from 'ol/source/XYZ' import ReactDOMServer from 'react-dom/server' import * as S from '~pages/WalkPage/styles' import { MIN_ACCURACY, MIN_DISTANCE, MIN_TIME_INTERVAL } from '~types/map' +import { useNavigate } from 'react-router-dom' const ORS_API_URL = '/ors/v2/directions/foot-walking/geojson' @@ -30,7 +31,12 @@ export const getMarkerIconString = () => { return svgString } -export default function MapComponent() { +// 모달 상태를 props로 받도록 수정 +interface MapComponentProps { + isModalOpen?: boolean +} + +export default function MapComponent({ isModalOpen = false }: MapComponentProps) { // 지도 관련 ref const mapRef = useRef(null) const currentLocationMarkerRef = useRef | null>(null) @@ -63,6 +69,8 @@ export default function MapComponent() { const [autoRotate, setAutoRotate] = useState(false) const lastHeadingRef = useRef(0) + const navigate = useNavigate() + useEffect(() => { return () => { if (currentLocationMarkerRef.current) { @@ -591,6 +599,8 @@ export default function MapComponent() { duration: 500, }) } + + navigate('/walk-complete') } } @@ -700,6 +710,7 @@ export default function MapComponent() { $bgColor={isWalking ? 'font_1' : 'default'} $fontWeight='700' $isWalking={isWalking} + disabled={isModalOpen} // 모달이 열려있을 때 버튼 비활성화 > {isWalking ? '산책 끝' : '산책 시작'} @@ -716,6 +727,7 @@ export default function MapComponent() { $bgColor={isWalking ? 'font_1' : 'default'} $fontWeight='700' $isWalking={isWalking} + disabled={isModalOpen} // 모달이 열려있을 때 버튼 비활성화 > {isWalking ? '산책 끝' : '산책 시작'} diff --git a/src/pages/WalkPage/components/WalkerListModal/index.tsx b/src/pages/WalkPage/components/WalkerListModal/index.tsx new file mode 100644 index 0000000..b792afa --- /dev/null +++ b/src/pages/WalkPage/components/WalkerListModal/index.tsx @@ -0,0 +1,54 @@ +import * as S from './styles' + +interface WalkerListModalProps { + isOpen: boolean + onClose: () => void + isClosing: boolean +} + +export default function WalkerListModal({ isOpen, onClose, isClosing }: WalkerListModalProps) { + if (!isOpen) return null + + const handleBackgroundClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose() + } + } + + return ( + + + 강번따 리스트 + + + {Array(10) + .fill(0) + .map((_, i) => ( + + + + + 밤돌이 + + 포메라니안 + + 4살 + + + + + 산책 횟수

 4회

+
+
+
+ + 강번따 + +
+ ))} +
+
+
+
+ ) +} diff --git a/src/pages/WalkPage/components/WalkerListModal/styles.ts b/src/pages/WalkPage/components/WalkerListModal/styles.ts new file mode 100644 index 0000000..d60438f --- /dev/null +++ b/src/pages/WalkPage/components/WalkerListModal/styles.ts @@ -0,0 +1,160 @@ +import { styled } from 'styled-components' +import { ActionButton } from '~components/Button/ActionButton' +import { Separator } from '~components/Separator' + +export const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: flex-end; + z-index: 1000; + + animation: fadeIn 0.3s ease-out; + + &.closing { + animation: fadeOut 0.3s ease-out; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } + } +` + +export const WalkerListContainer = styled.div` + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: white; + border-radius: 40px 40px 0 0; + padding: 20px; + z-index: 101; + height: 80dvh; + display: flex; + flex-direction: column; + animation: slideUp 0.6s ease-out; + + &.closing { + animation: slideDown 0.3s ease-out; + } + + @keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } + } + + @keyframes slideDown { + from { + transform: translateY(0); + } + to { + transform: translateY(100%); + } + } +` + +export const ModalTitle = styled.h2` + font-size: 18px; + font-weight: bold; + margin-bottom: 16px; + text-align: center; +` + +export const WalkerListSection = styled.div` + flex: 1; + overflow-y: auto; +` + +export const WalkerList = styled.div` + display: flex; + flex-direction: column; +` + +export const WalkerItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 0; + border-bottom: 1px solid #f0f0f0; +` + +export const ProfileArea = styled.div` + display: flex; + align-items: center; +` + +export const ProfileCircle = styled.div` + width: 64px; + height: 64px; + border-radius: 50%; + background-color: #ffe4d6; + margin-right: 12px; +` + +export const InfoArea = styled.div` + flex: 1; + color: ${({ theme }) => theme.colors.grayscale.font_2}; + font-size: 14px; +` + +export const Name = styled.div` + font-size: 17px; + font-weight: bold; + margin-bottom: 4px; + color: #111111; +` + +export const Details = styled.div` + font-size: 14px; + color: #666; + margin-bottom: 2px; + display: flex; + align-items: center; +` + +export const Detail = styled.p` + font-size: 14px; + color: #666; + margin-bottom: 2px; +` + +export const WalkCount = styled.div` + font-size: 14px; + color: ${({ theme }) => theme.colors.grayscale.font_1}; + font-weight: 700; + display: flex; + + p { + color: ${({ theme }) => theme.colors.brand.default}; + } +` + +export const WalkBtn = styled(ActionButton)` + width: fit-content; +` + +export const WalkListSeparator = styled(Separator)` + margin: 0 4px; +` diff --git a/src/pages/WalkPage/index.tsx b/src/pages/WalkPage/index.tsx index 47a9de4..105d017 100644 --- a/src/pages/WalkPage/index.tsx +++ b/src/pages/WalkPage/index.tsx @@ -3,9 +3,23 @@ import MapComponent from './components/MapComponent' import * as S from './styles' import { Helmet } from 'react-helmet-async' import { useNavigate } from 'react-router-dom' +import { useState } from 'react' +import WalkerListModal from '~pages/WalkPage/components/WalkerListModal' export default function WalkPage() { const navigate = useNavigate() + const [_modalType, _setModalType] = useState<'request' | 'accept' | 'complete' | 'progress' | 'friend' | null>(null) + const [isModalOpen, _setIsModalOpen] = useState(false) + const [isWalkerListOpen, setIsWalkerListOpen] = useState(false) + const [isClosing, setIsClosing] = useState(false) + + const handleWalkerListClose = () => { + setIsClosing(true) + setTimeout(() => { + setIsClosing(false) + setIsWalkerListOpen(false) + }, 300) + } return ( @@ -19,12 +33,13 @@ export default function WalkPage() { 강남구 논현동 - - - + + setIsWalkerListOpen(true)} /> + - + + ) } diff --git a/src/pages/WalkPage/styles.ts b/src/pages/WalkPage/styles.ts index 12d188d..e76634f 100644 --- a/src/pages/WalkPage/styles.ts +++ b/src/pages/WalkPage/styles.ts @@ -53,7 +53,7 @@ export const LocationText = styled.div` font-style: bold; ` -export const ProfileImgWrapper = styled.div` +export const WalkerListButtonWrapper = styled.div` width: 45px; height: 45px; border-radius: 50%; @@ -61,7 +61,7 @@ export const ProfileImgWrapper = styled.div` background-color: #f0f0f0; ` -export const ProfileImg = styled(DogProfile)` +export const WalkerListIcon = styled(DogProfile)` width: 100%; height: 100%; ` diff --git a/src/router.tsx b/src/router.tsx index 9627e10..e70c803 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -30,6 +30,10 @@ export const router = createBrowserRouter([ path: '/walk', element: , }, + { + path: '/walk-complete', + element: , + }, { path: '/mypage', element: ,