Skip to content

Commit

Permalink
feat: 방 상세페이지에 ControlButton 만들기
Browse files Browse the repository at this point in the history
  • Loading branch information
chlwlstlf committed Nov 15, 2024
1 parent 1a3eec1 commit e07f41c
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 89 deletions.
3 changes: 2 additions & 1 deletion frontend/src/@types/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type IconKind =
| "search"
| "add"
| "delete"
| "edit";
| "edit"
| "control";

export default IconKind;
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ const meta = {
description: "ContentSection 제목",
control: "text",
},
button: {
description: "ContentSection 버튼 (옵션)",
control: "object",
},
},
} satisfies Meta<typeof ContentSection>;

Expand All @@ -37,16 +33,3 @@ export const Default: Story = {
</ContentSection>
),
};

export const WithButton: Story = {
args: {
title: "버튼이 있는 ContentSection",
button: {
label: "클릭하세요",
onClick: () => alert("버튼이 클릭되었습니다!"),
},
},
render: (args) => (
<ContentSection {...args}>이 ContentSection에는 버튼이 포함되어 있습니다.</ContentSection>
),
};
15 changes: 3 additions & 12 deletions frontend/src/components/common/contentSection/ContentSection.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.ContentSectionContainer>
<S.ContentSectionHeader>
Expand All @@ -22,11 +17,7 @@ const ContentSection = ({ title, subtitle, children, button }: ContentSectionPro
<S.ContentSectionSubtitle>{subtitle}</S.ContentSectionSubtitle>
</S.TitleContainer>

{button && (
<Button onClick={button.onClick} size="small">
{button.label}
</Button>
)}
{controlSection && controlSection}
</S.ContentSectionHeader>
{children}
</S.ContentSectionContainer>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/common/icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MdInfoOutline,
MdInsertLink,
MdMenu,
MdMoreHoriz,
MdNotificationsNone,
MdOutlineArrowDropDown,
MdOutlineArrowDropUp,
Expand Down Expand Up @@ -67,6 +68,7 @@ const ICON: { [key in IconKind]: IconType } = {
add: MdAdd,
delete: MdDelete,
edit: MdEdit,
control: MdMoreHoriz,
};

interface IconProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -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};
}
`;
115 changes: 115 additions & 0 deletions frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx
Original file line number Diff line number Diff line change
@@ -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<string, DropdownItem[]> = {
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 (
<>
<ConfirmModal
isOpen={isModalOpen}
onClose={handleCloseModal}
onConfirm={handleConfirm}
onCancel={handleCloseModal}
>
{roomInfo.participationStatus === "MANAGER"
? MESSAGES.GUIDANCE.DELETE_ROOM
: MESSAGES.GUIDANCE.EXIT_ROOM}
</ConfirmModal>

<S.ControlButtonContainer ref={dropdownRef}>
<S.IconWrapper $isOpen={isDropdownOpen}>
<Icon kind="control" size="3rem" color="grey" onClick={handleControlButtonClick} />
</S.IconWrapper>

{isDropdownOpen && (
<S.DropdownMenu>
<FocusTrap onEscapeFocusTrap={() => handleToggleDropdown()}>
<S.DropdownItemWrapper>
{dropdownItems.map((item: DropdownItem) => (
<S.DropdownItem
key={item.name}
onClick={() => handleDropdownItemClick(item.action)}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") handleDropdownItemClick(item.action);
}}
>
{item.name}
</S.DropdownItem>
))}
</S.DropdownItemWrapper>
</FocusTrap>
</S.DropdownMenu>
)}
</S.ControlButtonContainer>
</>
);
};

export default ControlButton;
71 changes: 12 additions & 59 deletions frontend/src/pages/roomDetail/RoomDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,24 @@
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";
import ParticipantList from "@/components/roomDetailPage/participantList/ParticipantList";
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 (
<S.Layout>
<ContentSection title="미션 정보" {...buttonProps}>
<ContentSection title="미션 정보">
<RoomInfoCard roomInfo={roomInfo} />
</ContentSection>

Expand All @@ -70,7 +33,7 @@ const RoomDetailPage = () => {
if (roomInfo.roomStatus === "FAIL") {
return (
<S.Layout>
<ContentSection title="미션 정보" {...buttonProps}>
<ContentSection title="미션 정보">
<RoomInfoCard roomInfo={roomInfo} />
</ContentSection>

Expand All @@ -84,25 +47,15 @@ const RoomDetailPage = () => {

return (
<S.Layout>
<ConfirmModal
isOpen={isModalOpen}
onClose={handleCloseModal}
onConfirm={handleConfirm}
onCancel={handleCloseModal}
<ContentSection
title="미션 정보"
controlSection={
roomInfo.roomStatus === "OPEN" &&
["PARTICIPATED", "MANAGER"].includes(roomInfo.participationStatus) ? (
<ControlButton roomInfo={roomInfo} participationStatus={roomInfo.participationStatus} />
) : undefined
}
>
{roomInfo.participationStatus === "MANAGER"
? MESSAGES.GUIDANCE.DELETE_ROOM
: MESSAGES.GUIDANCE.EXIT_ROOM}
</ConfirmModal>

<ContentSection title="미션 정보" {...buttonProps}>
{roomInfo.roomStatus === "OPEN" && roomInfo.participationStatus === "MANAGER" && (
<S.EditButtonWrapper>
<S.EditButton onClick={() => navigate(`/rooms/edit/${roomId}`)}>
<Icon kind="edit" /> <span>수정</span>
</S.EditButton>
</S.EditButtonWrapper>
)}
<RoomInfoCard roomInfo={roomInfo} />
</ContentSection>

Expand Down

0 comments on commit e07f41c

Please sign in to comment.