From e07f41c72fa15d39bba35433e9b308fbcfae205a Mon Sep 17 00:00:00 2001 From: jinsil Date: Fri, 15 Nov 2024 21:45:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=A9=20=EC=83=81=EC=84=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=20ControlButton=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/@types/icon.ts | 3 +- .../contentSection/ContentSection.stories.tsx | 17 --- .../common/contentSection/ContentSection.tsx | 15 +-- frontend/src/components/common/icon/Icon.tsx | 2 + .../controlButton/ControlButton.style.ts | 82 +++++++++++++ .../controlButton/ControlButton.tsx | 115 ++++++++++++++++++ .../src/pages/roomDetail/RoomDetailPage.tsx | 71 ++--------- 7 files changed, 216 insertions(+), 89 deletions(-) create mode 100644 frontend/src/components/roomDetailPage/controlButton/ControlButton.style.ts create mode 100644 frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx diff --git a/frontend/src/@types/icon.ts b/frontend/src/@types/icon.ts index cafef24dd..cb7cb4055 100644 --- a/frontend/src/@types/icon.ts +++ b/frontend/src/@types/icon.ts @@ -28,6 +28,7 @@ type IconKind = | "search" | "add" | "delete" - | "edit"; + | "edit" + | "control"; export default IconKind; diff --git a/frontend/src/components/common/contentSection/ContentSection.stories.tsx b/frontend/src/components/common/contentSection/ContentSection.stories.tsx index 5b0e72163..3f367bdc6 100644 --- a/frontend/src/components/common/contentSection/ContentSection.stories.tsx +++ b/frontend/src/components/common/contentSection/ContentSection.stories.tsx @@ -16,10 +16,6 @@ const meta = { description: "ContentSection 제목", control: "text", }, - button: { - description: "ContentSection 버튼 (옵션)", - control: "object", - }, }, } satisfies Meta; @@ -37,16 +33,3 @@ export const Default: Story = { ), }; - -export const WithButton: Story = { - args: { - title: "버튼이 있는 ContentSection", - button: { - label: "클릭하세요", - onClick: () => alert("버튼이 클릭되었습니다!"), - }, - }, - render: (args) => ( - 이 ContentSection에는 버튼이 포함되어 있습니다. - ), -}; diff --git a/frontend/src/components/common/contentSection/ContentSection.tsx b/frontend/src/components/common/contentSection/ContentSection.tsx index 3e6e686d9..54c9153e3 100644 --- a/frontend/src/components/common/contentSection/ContentSection.tsx +++ b/frontend/src/components/common/contentSection/ContentSection.tsx @@ -1,19 +1,14 @@ import { ReactNode } from "react"; -import Button, { ButtonProps } from "@/components/common/button/Button"; import * as S from "@/components/common/contentSection/ContentSection.style"; -interface ContentSectionButton extends ButtonProps { - label: string; -} - interface ContentSectionProps { title: string; subtitle?: string; + controlSection?: ReactNode; children?: ReactNode; - button?: ContentSectionButton | undefined; } -const ContentSection = ({ title, subtitle, children, button }: ContentSectionProps) => { +const ContentSection = ({ title, subtitle, children, controlSection }: ContentSectionProps) => { return ( @@ -22,11 +17,7 @@ const ContentSection = ({ title, subtitle, children, button }: ContentSectionPro {subtitle} - {button && ( - - )} + {controlSection && controlSection} {children} diff --git a/frontend/src/components/common/icon/Icon.tsx b/frontend/src/components/common/icon/Icon.tsx index be24e6a9a..cd4f10a20 100644 --- a/frontend/src/components/common/icon/Icon.tsx +++ b/frontend/src/components/common/icon/Icon.tsx @@ -23,6 +23,7 @@ import { MdInfoOutline, MdInsertLink, MdMenu, + MdMoreHoriz, MdNotificationsNone, MdOutlineArrowDropDown, MdOutlineArrowDropUp, @@ -67,6 +68,7 @@ const ICON: { [key in IconKind]: IconType } = { add: MdAdd, delete: MdDelete, edit: MdEdit, + control: MdMoreHoriz, }; interface IconProps { diff --git a/frontend/src/components/roomDetailPage/controlButton/ControlButton.style.ts b/frontend/src/components/roomDetailPage/controlButton/ControlButton.style.ts new file mode 100644 index 000000000..6e32bd19b --- /dev/null +++ b/frontend/src/components/roomDetailPage/controlButton/ControlButton.style.ts @@ -0,0 +1,82 @@ +import styled, { keyframes } from "styled-components"; + +const dropdown = keyframes` + 0% { + transform: translateY(-10%); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +`; + +export const ControlButtonContainer = styled.div` + position: relative; +`; + +export const IconWrapper = styled.div<{ $isOpen: boolean }>` + cursor: pointer; + + display: flex; + align-items: center; + justify-content: center; + + width: 40px; + height: 40px; + + background-color: ${({ theme, $isOpen }) => $isOpen && theme.COLOR.grey0}; + border-radius: 50%; + + &:hover { + background-color: ${({ theme }) => theme.COLOR.grey0}; + } +`; + +export const DropdownMenu = styled.div` + position: absolute; + z-index: 1; + right: 0; + + display: flex; + flex-direction: column; + + min-width: 180px; + padding: 1rem; + + background-color: white; + border: 1px solid ${({ theme }) => theme.COLOR.grey2}; + border-radius: 5px; + + animation: ${dropdown} 0.2s ease; +`; + +export const DropdownItemWrapper = styled.ul` + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 0.5rem; + + :last-child { + gap: 0; + border-top: 1px solid ${({ theme }) => theme.COLOR.grey1}; + } +`; + +export const DropdownItem = styled.li` + cursor: pointer; + + display: flex; + gap: 0.3rem; + align-items: center; + + padding: 0.5rem; + + font: ${({ theme }) => theme.TEXT.small}; + color: ${({ theme }) => theme.COLOR.grey4}; + + &:hover { + font: ${({ theme }) => theme.TEXT.small_bold}; + background-color: ${({ theme }) => theme.COLOR.grey0}; + } +`; diff --git a/frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx b/frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx new file mode 100644 index 000000000..5d46bc626 --- /dev/null +++ b/frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import useDropdown from "@/hooks/common/useDropdown"; +import useModal from "@/hooks/common/useModal"; +import useMutateRoom from "@/hooks/mutations/useMutateRoom"; +import FocusTrap from "@/components/common/focusTrap/FocusTrap"; +import Icon from "@/components/common/icon/Icon"; +import ConfirmModal from "@/components/common/modal/confirmModal/ConfirmModal"; +import * as S from "@/components/roomDetailPage/controlButton/ControlButton.style"; +import { ParticipationStatus, RoomInfo } from "@/@types/roomInfo"; +import MESSAGES from "@/constants/message"; + +export type DropdownItem = { + name: string; + action: string; +}; + +export const dropdownItemsConfig: Record = { + MANAGER: [ + { name: "수정하기", action: "editRoom" }, + { name: "삭제하기", action: "deleteRoom" }, + ], + PARTICIPATED: [{ name: "방 나가기", action: "exitRoom" }], +}; + +interface ControlButtonProps { + roomInfo: RoomInfo; + participationStatus: ParticipationStatus; +} + +const ControlButton = ({ roomInfo, participationStatus }: ControlButtonProps) => { + const navigate = useNavigate(); + const { isModalOpen, handleOpenModal, handleCloseModal } = useModal(); + const { isDropdownOpen, handleToggleDropdown, dropdownRef } = useDropdown(); + const { deleteParticipateInMutation, deleteParticipatedRoomMutation } = useMutateRoom(); + + const dropdownItems = dropdownItemsConfig[participationStatus] || []; + + const handleControlButtonClick = (event: React.MouseEvent) => { + event.preventDefault(); + handleToggleDropdown(); + }; + + const handleDropdownItemClick = (action: string) => { + switch (action) { + case "editRoom": + navigate(`/rooms/edit/${roomInfo.id}`); + break; + case "deleteRoom": + handleOpenModal(); + break; + case "exitRoom": + handleOpenModal(); + break; + default: + break; + } + }; + + const handleConfirm = () => { + if (roomInfo.participationStatus === "MANAGER") { + deleteParticipatedRoomMutation.mutate(roomInfo.id, { + onSuccess: () => navigate("/"), + }); + return; + } + deleteParticipateInMutation.mutate(roomInfo.id, { + onSuccess: () => navigate("/"), + }); + }; + + return ( + <> + + {roomInfo.participationStatus === "MANAGER" + ? MESSAGES.GUIDANCE.DELETE_ROOM + : MESSAGES.GUIDANCE.EXIT_ROOM} + + + + + + + + {isDropdownOpen && ( + + handleToggleDropdown()}> + + {dropdownItems.map((item: DropdownItem) => ( + handleDropdownItemClick(item.action)} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") handleDropdownItemClick(item.action); + }} + > + {item.name} + + ))} + + + + )} + + + ); +}; + +export default ControlButton; diff --git a/frontend/src/pages/roomDetail/RoomDetailPage.tsx b/frontend/src/pages/roomDetail/RoomDetailPage.tsx index b86e4a9e1..1e6647648 100644 --- a/frontend/src/pages/roomDetail/RoomDetailPage.tsx +++ b/frontend/src/pages/roomDetail/RoomDetailPage.tsx @@ -1,10 +1,7 @@ -import { useNavigate, useParams } from "react-router-dom"; -import useModal from "@/hooks/common/useModal"; -import useMutateRoom from "@/hooks/mutations/useMutateRoom"; +import { useParams } from "react-router-dom"; import { useFetchDetailRoomInfo } from "@/hooks/queries/useFetchRooms"; import ContentSection from "@/components/common/contentSection/ContentSection"; -import Icon from "@/components/common/icon/Icon"; -import ConfirmModal from "@/components/common/modal/confirmModal/ConfirmModal"; +import ControlButton from "@/components/roomDetailPage/controlButton/ControlButton"; import FeedbackProcessInfo from "@/components/roomDetailPage/feedbackProcessInfo/FeedbackProcessInfo"; import MyReviewee from "@/components/roomDetailPage/myReviewee/MyReviewee"; import MyReviewer from "@/components/roomDetailPage/myReviewer/MyReviewer"; @@ -12,50 +9,16 @@ import ParticipantList from "@/components/roomDetailPage/participantList/Partici import RoomInfoCard from "@/components/roomDetailPage/roomInfoCard/RoomInfoCard"; import * as S from "@/pages/roomDetail/RoomDetailPage.style"; import { defaultCharacter } from "@/assets"; -import MESSAGES from "@/constants/message"; const RoomDetailPage = () => { - const navigate = useNavigate(); const params = useParams(); const roomId = params.id ? Number(params.id) : 0; - const { isModalOpen, handleOpenModal, handleCloseModal } = useModal(); const { data: roomInfo } = useFetchDetailRoomInfo(roomId); - const { deleteParticipateInMutation, deleteParticipatedRoomMutation } = useMutateRoom(); - - const handleCancelParticipateInClick = () => { - deleteParticipateInMutation.mutate(roomInfo.id, { - onSuccess: () => navigate("/"), - }); - }; - - const handleDeleteRoomClick = () => { - deleteParticipatedRoomMutation.mutate(roomInfo.id, { - onSuccess: () => navigate("/"), - }); - }; - - const handleConfirm = () => { - if (roomInfo.participationStatus === "MANAGER") { - handleDeleteRoomClick(); - return; - } - handleCancelParticipateInClick(); - }; - - const buttonProps = - roomInfo.roomStatus === "OPEN" && roomInfo.participationStatus !== "NOT_PARTICIPATED" - ? { - button: { - label: roomInfo.participationStatus === "MANAGER" ? "방 삭제하기" : "방 나가기", - onClick: handleOpenModal, - }, - } - : {}; if (roomInfo.participationStatus === "NOT_PARTICIPATED") { return ( - + @@ -70,7 +33,7 @@ const RoomDetailPage = () => { if (roomInfo.roomStatus === "FAIL") { return ( - + @@ -84,25 +47,15 @@ const RoomDetailPage = () => { return ( - + ) : undefined + } > - {roomInfo.participationStatus === "MANAGER" - ? MESSAGES.GUIDANCE.DELETE_ROOM - : MESSAGES.GUIDANCE.EXIT_ROOM} - - - - {roomInfo.roomStatus === "OPEN" && roomInfo.participationStatus === "MANAGER" && ( - - navigate(`/rooms/edit/${roomId}`)}> - 수정 - - - )}