diff --git a/frontend/.stylelintrc b/frontend/.stylelintrc index c6c1810e..28c0981f 100644 --- a/frontend/.stylelintrc +++ b/frontend/.stylelintrc @@ -59,6 +59,7 @@ "padding-bottom", "padding-left", "border", + "border-color", "border-radius" ] }, diff --git a/frontend/src/apis/pairRoom.ts b/frontend/src/apis/pairRoom.ts index 2a917c66..b48b6ce9 100644 --- a/frontend/src/apis/pairRoom.ts +++ b/frontend/src/apis/pairRoom.ts @@ -60,3 +60,10 @@ export const updatePairRole = async ({ accessCode }: UpdatePairRoleRequest) => { errorMessage: '', }); }; + +export const deletePairRoom = async (accessCode: string) => { + await fetcher.delete({ + url: `${API_URL}/pair-room/${accessCode}`, + errorMessage: ERROR_MESSAGES.DELETE_PAIR_ROOM, + }); +}; diff --git a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts index d00b9d6a..5ed11bf7 100644 --- a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts +++ b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts @@ -1,3 +1,6 @@ +import { Link } from 'react-router-dom'; + +import { FaTrashAlt } from 'react-icons/fa'; import styled, { keyframes, css } from 'styled-components'; import type { PairRoomStatus } from '@/apis/pairRoom'; @@ -11,7 +14,93 @@ const flow = keyframes` } `; -export const Layout = styled.button<{ $status: PairRoomStatus }>` +const commonTextStyles = css` + font-size: ${({ theme }) => theme.fontSize.base}; + + transition: color 0.7s ease; +`; + +const inProgressText = css` + background: linear-gradient( + 90deg, + ${({ theme }) => theme.color.black[60]}, + ${({ theme }) => theme.color.black[70]}, + ${({ theme }) => theme.color.black[60]} + ); + + animation: ${flow} 4s linear infinite; + background-size: 200% 100%; + background-clip: text; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 3rem; + + width: 100%; +`; + +export const LinkWrapper = styled(Link)` + width: 100%; +`; + +export const StatusText = styled.p<{ $status: PairRoomStatus }>` + width: 15%; + + ${commonTextStyles} + color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? 'transparent' : theme.color.black[70])}; + letter-spacing: 0.15rem; + text-align: left; + + ${({ $status }) => $status === 'IN_PROGRESS' && inProgressText} + + &:hover { + color: white; + } +`; + +export const RoleTextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + + width: 50%; +`; + +export const RoleText = styled.p<{ $status: PairRoomStatus }>` + display: flex; + align-items: center; + gap: 1rem; + + font-size: ${({ theme }) => theme.fontSize.md}; + + transition: color 0.7s ease; + + span { + color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? theme.color.secondary[600] : theme.color.black[70])}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + + transition: color 0.7s ease; + } +`; + +export const ConnectText = styled.div` + display: flex; + justify-content: right; + align-items: center; + gap: 0.4rem; + + width: 10%; + + color: ${({ theme }) => theme.color.black[10]}; + + transition: color 0.7s ease; +`; + +export const PairRoomButton = styled.button<{ $status: PairRoomStatus }>` display: flex; justify-content: space-between; align-items: center; @@ -25,6 +114,8 @@ export const Layout = styled.button<{ $status: PairRoomStatus }>` font-size: ${({ theme }) => theme.fontSize.base}; + cursor: pointer; + &::before { content: ''; @@ -37,73 +128,65 @@ export const Layout = styled.button<{ $status: PairRoomStatus }>` height: 100%; border-radius: 1rem; - background: ${({ $status, theme }) => + background-color: ${({ $status, theme }) => + $status === 'IN_PROGRESS' ? theme.color.secondary[100] : theme.color.black[30]}; + background-image: ${({ $status, theme }) => $status === 'IN_PROGRESS' ? `linear-gradient( - 120deg, + 90deg, ${theme.color.secondary[100]} 0 75%, ${theme.color.secondary[600]} 75% 100% )` : `linear-gradient( - 120deg, + 90deg, ${theme.color.black[30]} 0 75%, ${theme.color.black[60]} 75% 100% )`}; + background-size: 400% 100%; + + background-position: 72.5% 0; opacity: 0.7; - transition: opacity 0.2s ease-in-out; + transition: + background-position 0.5s ease, + opacity 0.2s ease; } &:hover::before { + background-position: 105.8% 0; opacity: 1; } -`; -export const RoleTextContainer = styled.div` - display: flex; - flex-direction: column; - gap: 1rem; -`; - -export const RoleText = styled.p<{ $status: PairRoomStatus }>` - display: flex; - align-items: center; - gap: 1rem; - - font-size: ${({ theme }) => theme.fontSize.md}; - - span { - color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? theme.color.secondary[600] : theme.color.black[70])}; - font-size: ${({ theme }) => theme.fontSize.lg}; - font-weight: ${({ theme }) => theme.fontWeight.medium}; + &:hover ${StatusText} { + color: white; + ${({ $status }) => + $status === 'IN_PROGRESS' && + css` + animation: ${flow} 4s linear infinite; + `} } -`; + &:hover ${RoleText} { + color: ${({ theme }) => theme.color.black[20]}; -const inProgressText = css` - background: linear-gradient( - 90deg, - ${({ theme }) => theme.color.black[60]}, - ${({ theme }) => theme.color.black[70]}, - ${({ theme }) => theme.color.black[60]} - ); + span { + color: ${({ theme }) => theme.color.black[10]}; + } + } - animation: ${flow} 4s linear infinite; - background-size: 200% 100%; - background-clip: text; + &:hover ${ConnectText} { + color: ${({ theme }) => theme.color.black[70]}; + } `; -export const StatusText = styled.p<{ $status: PairRoomStatus }>` - color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? 'transparent' : theme.color.black[70])}; - font-size: ${({ theme }) => theme.fontSize.base}; - letter-spacing: 0.15rem; +export const DeleteButton = styled(FaTrashAlt)` + color: ${({ theme }) => theme.color.black[60]}; + font-size: 1.6rem; - ${({ $status }) => $status === 'IN_PROGRESS' && inProgressText} -`; + transition: color 0.3s ease; -export const ConnectText = styled.div` - display: flex; - align-items: center; - gap: 0.4rem; + cursor: pointer; - color: ${({ theme }) => theme.color.black[10]}; + &:hover { + color: ${({ theme }) => theme.color.danger[600]}; + } `; diff --git a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx index 2f1a7c2e..ac0c530b 100644 --- a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx +++ b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx @@ -1,8 +1,13 @@ -import { Link } from 'react-router-dom'; - import { IoIosArrowForward } from 'react-icons/io'; -import type { PairRoomStatus } from '@/apis/pairRoom'; +import Spinner from '@/components/common/Spinner/Spinner'; +import PairRoomDeleteModal from '@/components/MyPage/PairRoomDeleteModal/PairRoomDeleteModal'; + +import { type PairRoomStatus } from '@/apis/pairRoom'; + +import useModal from '@/hooks/common/useModal'; + +import useDeletePairRoom from '@/queries/MyPage/useDeleteRoom'; import * as S from './PairRoomButton.styles'; @@ -14,26 +19,53 @@ interface PairRoomButtonProps { } const PairRoomButton = ({ driver, navigator, status, accessCode }: PairRoomButtonProps) => { + const { openModal, closeModal, isModalOpen } = useModal(); + const { mutate, isPending } = useDeletePairRoom(); + const handleOpenDeleteModal = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + openModal(); + }; + + const handleDeletePairRoom = async () => { + mutate(accessCode); + closeModal(); + }; + return ( - - - - - 드라이버 - {driver} - - - 내비게이터 - {navigator} - - - {status === 'IN_PROGRESS' ? '진행 중' : '진행 완료'} - - 접속 - - - - + + {isPending ? ( + + ) : ( + <> + + + + + 드라이버 + {driver} + + + 내비게이터 + {navigator} + + + {status === 'IN_PROGRESS' ? '진행 중' : '진행 완료'} + + 입장 + + + + + + + + )} + ); }; diff --git a/frontend/src/components/MyPage/PairRoomDeleteModal/PairRoomDeleteModal.tsx b/frontend/src/components/MyPage/PairRoomDeleteModal/PairRoomDeleteModal.tsx new file mode 100644 index 00000000..f8095d5f --- /dev/null +++ b/frontend/src/components/MyPage/PairRoomDeleteModal/PairRoomDeleteModal.tsx @@ -0,0 +1,33 @@ +import { Modal } from '@/components/common/Modal'; + +import * as S from './PariRoomDeleteModal.styles'; + +interface PairRoomDeleteModalProps { + isOpen: boolean; + closeModal: () => void; + handleDeletePairRoom: () => void; +} + +const PairRoomDeleteModal = ({ isOpen, closeModal, handleDeletePairRoom }: PairRoomDeleteModalProps) => { + return ( + + + + + 투두 리스트, 레퍼런스 링크 등
+ 모든 데이터가 삭제됩니다. +
+
+ + + 취소 + + + 삭제하기 + + +
+ ); +}; + +export default PairRoomDeleteModal; diff --git a/frontend/src/components/MyPage/PairRoomDeleteModal/PariRoomDeleteModal.styles.ts b/frontend/src/components/MyPage/PairRoomDeleteModal/PariRoomDeleteModal.styles.ts new file mode 100644 index 00000000..4f6b0726 --- /dev/null +++ b/frontend/src/components/MyPage/PairRoomDeleteModal/PariRoomDeleteModal.styles.ts @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +import Button from '@/components/common/Button/Button'; + +export const Description = styled.p` + text-align: center; +`; + +export const DangerText = styled.span` + color: ${({ theme }) => theme.color.danger[600]}; + font-size: ${({ theme }) => theme.fontSize.base}; + line-height: 1.5; +`; + +export const FilledButton = styled(Button)` + background-color: ${({ theme }) => theme.color.danger[600]}; + font-size: ${({ theme }) => theme.fontSize.md}; + border-color: ${({ theme }) => theme.color.danger[600]}; + + &:hover { + background-color: ${({ theme }) => theme.color.danger[700]}; + border-color: ${({ theme }) => theme.color.danger[700]}; + } + + &:active { + background-color: ${({ theme }) => theme.color.danger[800]}; + border-color: ${({ theme }) => theme.color.danger[800]}; + } +`; + +export const OutlinedButton = styled(Button)` + color: ${({ theme }) => theme.color.danger[600]}; + font-size: ${({ theme }) => theme.fontSize.md}; + border-color: ${({ theme }) => theme.color.danger[600]}; + + &:hover { + color: ${({ theme }) => theme.color.danger[700]}; + border-color: ${({ theme }) => theme.color.danger[700]}; + } + + &:active { + color: ${({ theme }) => theme.color.danger[800]}; + border-color: ${({ theme }) => theme.color.danger[800]}; + } +`; diff --git a/frontend/src/components/PairRoom/GuideModal/GuideModal.styles.ts b/frontend/src/components/PairRoom/GuideModal/GuideModal.styles.ts new file mode 100644 index 00000000..7e2569a6 --- /dev/null +++ b/frontend/src/components/PairRoom/GuideModal/GuideModal.styles.ts @@ -0,0 +1,87 @@ +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + width: 9rem; + height: 3.2rem; + border-color: ${({ theme }) => theme.color.danger[400]}; + border-radius: 0.8rem; + + background-color: ${({ theme }) => theme.color.danger[400]}; + font-size: ${({ theme }) => theme.fontSize.md}; + + &:hover { + border-color: ${({ theme }) => theme.color.danger[500]}; + + background-color: ${({ theme }) => theme.color.danger[500]}; + } + + &:active { + border-color: ${({ theme }) => theme.color.danger[500]}; + + background-color: ${({ theme }) => theme.color.danger[500]}; + } +`; + +export const startButtonStyles = css` + width: 18rem; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; + + padding: 1rem 0; +`; + +export const Title = styled.h2` + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const List = styled.ul` + display: flex; + flex-direction: column; + gap: 2rem; +`; + +export const Item = styled.li` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const Question = styled.p` + display: flex; + align-items: center; + gap: 0.6rem; + + font-size: ${({ theme }) => theme.fontSize.base}; +`; + +export const Description = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + + height: 5.2rem; + padding: 1rem 1rem 1rem 1.6rem; + border-radius: 1rem; + + background-color: ${({ theme }) => theme.color.danger[100]}; + font-size: ${({ theme }) => theme.fontSize.md}; + font-weight: ${({ theme }) => theme.fontWeight.light}; +`; + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; + + p { + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.sm}; + } +`; diff --git a/frontend/src/components/PairRoom/GuideModal/GuideModal.tsx b/frontend/src/components/PairRoom/GuideModal/GuideModal.tsx new file mode 100644 index 00000000..868f3352 --- /dev/null +++ b/frontend/src/components/PairRoom/GuideModal/GuideModal.tsx @@ -0,0 +1,94 @@ +import { useRef } from 'react'; + +import { FaCheck } from 'react-icons/fa6'; + +import { AlarmSound } from '@/assets'; + +import Button from '@/components/common/Button/Button'; +import { Modal } from '@/components/common/Modal'; + +import useToastStore from '@/stores/toastStore'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import * as S from './GuideModal.styles'; + +interface GuideModalProps { + isOpen: boolean; + close: () => void; + accessCode: string; +} + +const GuideModal = ({ isOpen, close, accessCode }: GuideModalProps) => { + const alarmAudio = useRef(new Audio(AlarmSound)); + + const { addToast } = useToastStore(); + + const [, onCopy] = useCopyClipBoard(); + + const checkPermission = () => { + if (Notification.permission !== 'granted') { + addToast({ status: 'ERROR', message: '알림 권한이 허용되지 않았습니다. 설정에서 권한을 허용해 주세요.' }); + return; + } + + addToast({ status: 'SUCCESS', message: '알림 권한이 허용된 상태입니다.' }); + }; + + return ( + + + + 페어 프로그래밍을 시작하기 전에... + + + + + 브라우저 알림을 허용하셨나요? + + + 브라우저 알림을 허용하지 않으면 타이머 종료 시 올바르게 알림을 제공할 수 없어요. + + + + + + + 사용 중인 기기의 소리가 켜져 있나요? + + + 사용 중인 기기의 소리가 꺼져 있다면 타이머 종료 시 알람 소리를 들으실 수 없어요. + + + + + + + 페어룸 코드를 복사하셨나요? + + + 페어에게 페어룸 코드를 전달하여 페어룸에 들어올 수 있도록 해주세요. + + + + + + +

모두 확인하셨나요?

+ +
+
+
+
+ ); +}; + +export default GuideModal; diff --git a/frontend/src/components/common/Header/Header.styles.ts b/frontend/src/components/common/Header/Header.styles.ts index d0779f16..15125e22 100644 --- a/frontend/src/components/common/Header/Header.styles.ts +++ b/frontend/src/components/common/Header/Header.styles.ts @@ -5,14 +5,19 @@ export const Layout = styled.div` justify-content: space-between; align-items: center; + position: fixed; + z-index: 99; + + width: 100%; height: 7rem; padding: 0 5rem; - border-bottom: 0.1rem solid ${({ theme }) => theme.color.black[30]}; - + background-color: ${({ theme }) => theme.color.black[10]}; color: ${({ theme }) => theme.color.black[80]}; font-size: ${({ theme }) => theme.fontSize.base}; + border-bottom: 0.1rem solid ${({ theme }) => theme.color.black[30]}; + a { display: flex; justify-content: center; @@ -47,7 +52,7 @@ export const Logo = styled.img` export const LinkContainer = styled.div` display: flex; - justify-content: space-between; + justify-content: space-evenly; align-items: center; gap: 1.4rem; `; diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx index c35b4f30..b724af00 100644 --- a/frontend/src/components/common/Header/Header.tsx +++ b/frontend/src/components/common/Header/Header.tsx @@ -26,7 +26,7 @@ const Header = () => { - 코딩해듀오 시작하기 + diff --git a/frontend/src/components/common/Tooltip/Tooltip.styles.ts b/frontend/src/components/common/Tooltip/Tooltip.styles.ts index b6615940..201567a2 100644 --- a/frontend/src/components/common/Tooltip/Tooltip.styles.ts +++ b/frontend/src/components/common/Tooltip/Tooltip.styles.ts @@ -52,8 +52,9 @@ const directionStyle = (direction: Direction, color: string) => { top: 100%; left: 50%; - transform: translateX(-50%); border-color: ${color} transparent transparent transparent; + + transform: translateX(-50%); } `; case 'bottom': @@ -68,8 +69,9 @@ const directionStyle = (direction: Direction, color: string) => { bottom: 100%; left: 50%; - transform: translateX(-50%); border-color: transparent transparent ${color} transparent; + + transform: translateX(-50%); } `; case 'left': @@ -84,8 +86,9 @@ const directionStyle = (direction: Direction, color: string) => { top: 50%; left: 100%; - transform: translateY(-50%); border-color: transparent transparent transparent ${color}; + + transform: translateY(-50%); } `; case 'right': @@ -100,8 +103,9 @@ const directionStyle = (direction: Direction, color: string) => { top: 50%; right: 100%; - transform: translateY(-50%); border-color: transparent ${color} transparent transparent; + + transform: translateY(-50%); } `; } diff --git a/frontend/src/constants/message.ts b/frontend/src/constants/message.ts index 044bd95a..705e2540 100644 --- a/frontend/src/constants/message.ts +++ b/frontend/src/constants/message.ts @@ -3,6 +3,7 @@ export const ERROR_MESSAGES = { ADD_REFERENCE_LINKS: '레퍼런스 링크를 저장하지 못했습니다. 다시 시도해 주세요.', DELETE_REFERENCE_LINKS: '레퍼런스 링크 삭제에 실패했습니다. 다시 시도해 주세요.', GET_PAIR_ROOM: '페어룸 정보를 불러오지 못했습니다. 다시 시도해 주세요.', + DELETE_PAIR_ROOM: '페어룸 삭제에 실패했습니다. 다시 시도해 주세요.', ADD_PAIR_NAMES: '페어룸 생성에 실패했습니다. 다시 시도해 주세요.', GET_TODOS: '투두 리스트를 불러오지 못했습니다. 다시 시도해 주세요.', ADD_TODO: '투두 아이템을 저장하지 못했습니다. 다시 시도해 주세요.', diff --git a/frontend/src/hooks/common/useHashScroll.ts b/frontend/src/hooks/common/useHashScroll.ts index d7c3d6ea..1a58c441 100644 --- a/frontend/src/hooks/common/useHashScroll.ts +++ b/frontend/src/hooks/common/useHashScroll.ts @@ -52,7 +52,15 @@ const useHashScroll = () => { if (location.hash) { const element = document.getElementById(currentHash); if (element) { - element.scrollIntoView({ behavior: 'smooth' }); + const headerHeight = 70; + const marginTop = 50; + const elementPosition = element.getBoundingClientRect().top + window.scrollY; + const offsetPosition = elementPosition - headerHeight - marginTop; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth', + }); } } }, [location, currentHash]); diff --git a/frontend/src/pages/Layout.styles.ts b/frontend/src/pages/Layout.styles.ts index fdbdd727..3d7cc4b2 100644 --- a/frontend/src/pages/Layout.styles.ts +++ b/frontend/src/pages/Layout.styles.ts @@ -3,6 +3,11 @@ import styled from 'styled-components'; export const Layout = styled.div` display: flex; flex-direction: column; + overflow: hidden; min-width: fit-content; `; + +export const Main = styled.main` + margin-top: 7rem; +`; diff --git a/frontend/src/pages/Layout.tsx b/frontend/src/pages/Layout.tsx index 7515fd33..fb7b153a 100644 --- a/frontend/src/pages/Layout.tsx +++ b/frontend/src/pages/Layout.tsx @@ -9,9 +9,9 @@ const Layout = () => { return (
-
+ -
+ ); diff --git a/frontend/src/pages/PairRoom/PairRoom.tsx b/frontend/src/pages/PairRoom/PairRoom.tsx index 07a376c0..b85aa21b 100644 --- a/frontend/src/pages/PairRoom/PairRoom.tsx +++ b/frontend/src/pages/PairRoom/PairRoom.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import Loading from '@/pages/Loading/Loading'; +import GuideModal from '@/components/PairRoom/GuideModal/GuideModal'; import PairListCard from '@/components/PairRoom/PairListCard/PairListCard'; import PairRoleCard from '@/components/PairRoom/PairRoleCard/PairRoleCard'; import ReferenceCard from '@/components/PairRoom/ReferenceCard/ReferenceCard'; @@ -11,6 +12,8 @@ import TodoListCard from '@/components/PairRoom/TodoListCard/TodoListCard'; import { getPairRoomExists } from '@/apis/pairRoom'; +import useModal from '@/hooks/common/useModal'; + import useGetPairRoom from '@/queries/PairRoom/useGetPairRoom'; import useUpdatePairRoom from '@/queries/PairRoom/useUpdatePairRoom'; @@ -51,6 +54,8 @@ const PairRoom = () => { const [isCardOpen, setIsCardOpen] = useState(false); + const { isModalOpen, closeModal } = useModal(true); + if (isFetching) { return ; } @@ -71,6 +76,7 @@ const PairRoom = () => { setIsCardOpen(false)} /> setIsCardOpen(true)} /> + ); }; diff --git a/frontend/src/queries/MyPage/useDeleteRoom.ts b/frontend/src/queries/MyPage/useDeleteRoom.ts new file mode 100644 index 00000000..c2ae09d4 --- /dev/null +++ b/frontend/src/queries/MyPage/useDeleteRoom.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import useToastStore from '@/stores/toastStore'; + +import { deletePairRoom } from '@/apis/pairRoom'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const useDeletePairRoom = () => { + const queryClient = useQueryClient(); + const { addToast } = useToastStore(); + + const { mutate, isPending } = useMutation({ + mutationFn: deletePairRoom, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_MY_PAIR_ROOMS] }); + addToast({ status: 'SUCCESS', message: '페어룸이 삭제되었습니다.' }); + }, + + onError: () => { + addToast({ status: 'ERROR', message: '페어룸 삭제에 실패했습니다.' }); + }, + }); + + return { mutate, isPending }; +}; + +export default useDeletePairRoom;