diff --git a/frontend/src/@types/icon.ts b/frontend/src/@types/icon.ts index 61e76b7b7..cb7cb4055 100644 --- a/frontend/src/@types/icon.ts +++ b/frontend/src/@types/icon.ts @@ -2,7 +2,7 @@ type IconKind = | "person" | "link" | "calendar" - | "plus" + | "more" | "star" | "people" | "pencil" @@ -25,6 +25,10 @@ type IconKind = | "close" | "githubLogo" | "notificationBell" - | "search"; + | "search" + | "add" + | "delete" + | "edit" + | "control"; export default IconKind; diff --git a/frontend/src/@types/roomInfo.ts b/frontend/src/@types/roomInfo.ts index 429fbbe46..4f591b46a 100644 --- a/frontend/src/@types/roomInfo.ts +++ b/frontend/src/@types/roomInfo.ts @@ -20,7 +20,7 @@ interface BaseRoomInfo { keywords: string[]; limitedParticipants: number; classification: Classification; - isPrivate: boolean; + isPublic: boolean; } export interface CreateRoomInfo extends BaseRoomInfo { recruitmentDeadline: Date; diff --git a/frontend/src/components/common/calendar/Calendar.style.ts b/frontend/src/components/common/calendar/Calendar.style.ts index 9a51dfd08..0d0dbcc70 100644 --- a/frontend/src/components/common/calendar/Calendar.style.ts +++ b/frontend/src/components/common/calendar/Calendar.style.ts @@ -1,8 +1,9 @@ import styled from "styled-components"; +import { Z_INDEX } from "@/styles/zIndex"; export const CalendarContainer = styled.section` position: absolute; - z-index: 999; + z-index: ${Z_INDEX.dropdown}; width: 400px; padding: 2rem; diff --git a/frontend/src/components/common/calendar/Calendar.tsx b/frontend/src/components/common/calendar/Calendar.tsx index 5edec46e6..9cf9a4dab 100644 --- a/frontend/src/components/common/calendar/Calendar.tsx +++ b/frontend/src/components/common/calendar/Calendar.tsx @@ -36,11 +36,10 @@ const Calendar = ({ selectedDate, handleSelectedDate, options }: CalendarProps) const checkIsAvailableClick = (day: number) => { if (!options?.isPastDateDisabled) return true; - const targetDate = options.disabledBeforeDate || new Date(); + const targetDate = new Date(options.disabledBeforeDate || new Date()); targetDate.setHours(0, 0, 0, 0); const checkingDate = new Date(currentViewYear, currentViewMonth - 1, day); - return targetDate <= checkingDate; }; diff --git a/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx b/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx index ecadb43c7..7a22c9fe7 100644 --- a/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx +++ b/frontend/src/components/common/calendarDropdown/CalendarDropdown.stories.tsx @@ -20,6 +20,11 @@ const meta = { description: "달력 날짜 선택 함수", action: "selected", }, + error: { + control: { + type: "boolean", + }, + }, }, } satisfies Meta; @@ -30,6 +35,7 @@ export const Default: Story = { args: { selectedDate: new Date(), handleSelectedDate: () => {}, + error: false, }, render: (args) => { const [selectedDate, setSelectedDate] = useState(args.selectedDate); @@ -38,7 +44,13 @@ export const Default: Story = { setSelectedDate(newSelectedDate); }; - return ; + return ( + + ); }, }; @@ -49,6 +61,7 @@ export const 이전날짜_선택_불가_캘린더: Story = { options: { isPastDateDisabled: true, }, + error: false, }, render: (args) => { const [selectedDate, setSelectedDate] = useState(args.selectedDate); @@ -62,6 +75,7 @@ export const 이전날짜_선택_불가_캘린더: Story = { selectedDate={selectedDate} handleSelectedDate={handleSelectedDate} options={args.options} + error={false} /> ); }, @@ -74,6 +88,7 @@ export const 캘린더_드롭다운_에러: Story = { options: { isPastDateDisabled: true, }, + error: false, }, render: (args) => { const [selectedDate, setSelectedDate] = useState(args.selectedDate); @@ -87,6 +102,7 @@ export const 캘린더_드롭다운_에러: Story = { selectedDate={selectedDate} handleSelectedDate={handleSelectedDate} options={args.options} + error={false} /> ); }, diff --git a/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts b/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts index 18c99ccb7..b91649333 100644 --- a/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts +++ b/frontend/src/components/common/calendarDropdown/CalendarDropdown.style.ts @@ -5,7 +5,7 @@ export const CalendarDropdownContainer = styled.section` width: 130px; `; -export const CalendarDropdownToggle = styled.input` +export const CalendarDropdownToggle = styled.input<{ $error: boolean }>` cursor: pointer; width: 100%; @@ -15,7 +15,7 @@ export const CalendarDropdownToggle = styled.input` text-align: center; letter-spacing: 0.2rem; - border: 1px solid ${({ theme }) => theme.COLOR.grey1}; + border: 1px solid ${({ theme, $error }) => ($error ? theme.COLOR.error : theme.COLOR.grey1)}; border-radius: 6px; outline-color: ${({ theme }) => theme.COLOR.black}; `; diff --git a/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx b/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx index 2eb1ba8a5..de339888a 100644 --- a/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx +++ b/frontend/src/components/common/calendarDropdown/CalendarDropdown.tsx @@ -4,12 +4,14 @@ import useDropdown from "@/hooks/common/useDropdown"; import Calendar, { CalendarProps } from "@/components/common/calendar/Calendar"; import { formatDate } from "@/utils/dateFormatter"; -type CalendarDropdownProps = CalendarProps & InputHTMLAttributes; +type CalendarDropdownProps = CalendarProps & + InputHTMLAttributes & { error: boolean }; const CalendarDropdown = ({ selectedDate, handleSelectedDate, options, + error, ...rest }: CalendarDropdownProps) => { const { isDropdownOpen, handleToggleDropdown, dropdownRef } = useDropdown(); @@ -27,6 +29,7 @@ const CalendarDropdown = ({ value={formatDate(selectedDate)} onClick={handleToggleDropdown} placeholder="날짜를 선택하세요" + $error={error} readOnly {...rest} /> 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.style.ts b/frontend/src/components/common/contentSection/ContentSection.style.ts index d682e79fc..b1b8a9919 100644 --- a/frontend/src/components/common/contentSection/ContentSection.style.ts +++ b/frontend/src/components/common/contentSection/ContentSection.style.ts @@ -1,6 +1,7 @@ import styled from "styled-components"; export const ContentSectionContainer = styled.div` + position: relative; display: flex; flex-direction: column; gap: 1rem; 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/dropdown/Dropdown.style.ts b/frontend/src/components/common/dropdown/Dropdown.style.ts index acc822827..6f06df410 100644 --- a/frontend/src/components/common/dropdown/Dropdown.style.ts +++ b/frontend/src/components/common/dropdown/Dropdown.style.ts @@ -1,4 +1,5 @@ import styled, { keyframes } from "styled-components"; +import { Z_INDEX } from "@/styles/zIndex"; const dropdown = keyframes` 0% { @@ -37,7 +38,7 @@ export const DropdownToggle = styled.button<{ $error: boolean }>` export const DropdownMenu = styled.div` position: absolute; - z-index: 1; + z-index: ${Z_INDEX.dropdown}; right: 0; display: flex; diff --git a/frontend/src/components/common/header/ProfileDropdown.style.ts b/frontend/src/components/common/header/ProfileDropdown.style.ts index e3a019c93..baa9a4647 100644 --- a/frontend/src/components/common/header/ProfileDropdown.style.ts +++ b/frontend/src/components/common/header/ProfileDropdown.style.ts @@ -1,4 +1,5 @@ import styled, { keyframes } from "styled-components"; +import { Z_INDEX } from "@/styles/zIndex"; const dropdown = keyframes` 0% { @@ -17,7 +18,7 @@ export const ProfileContainer = styled.div` export const DropdownMenu = styled.div` position: absolute; - z-index: 1; + z-index: ${Z_INDEX.dropdown}; right: 0; display: flex; diff --git a/frontend/src/components/common/icon/Icon.tsx b/frontend/src/components/common/icon/Icon.tsx index 2c0a9cc3c..cd4f10a20 100644 --- a/frontend/src/components/common/icon/Icon.tsx +++ b/frontend/src/components/common/icon/Icon.tsx @@ -9,6 +9,7 @@ import { import { IoLogoGithub } from "react-icons/io"; import { IconType } from "react-icons/lib"; import { + MdAdd, MdArrowBackIos, MdArrowDropDown, MdArrowForwardIos, @@ -16,10 +17,13 @@ import { MdCalendarMonth, MdCheck, MdClear, + MdDelete, + MdEdit, MdExpandMore, MdInfoOutline, MdInsertLink, MdMenu, + MdMoreHoriz, MdNotificationsNone, MdOutlineArrowDropDown, MdOutlineArrowDropUp, @@ -37,7 +41,7 @@ const ICON: { [key in IconKind]: IconType } = { person: MdPerson, link: MdInsertLink, calendar: MdCalendarMonth, - plus: MdExpandMore, + more: MdExpandMore, info: MdInfoOutline, star: MdOutlineStar, people: MdOutlinePeopleAlt, @@ -61,6 +65,10 @@ const ICON: { [key in IconKind]: IconType } = { githubLogo: IoLogoGithub, notificationBell: MdNotificationsNone, search: MdOutlineSearch, + add: MdAdd, + delete: MdDelete, + edit: MdEdit, + control: MdMoreHoriz, }; interface IconProps { diff --git a/frontend/src/components/common/input/Input.style.ts b/frontend/src/components/common/input/Input.style.ts index ef63326c5..b09bfe494 100644 --- a/frontend/src/components/common/input/Input.style.ts +++ b/frontend/src/components/common/input/Input.style.ts @@ -18,7 +18,7 @@ export const StyledInput = styled.input<{ $error: boolean }>` export const CharCount = styled.div` position: absolute; - right: 10px; + right: 0; bottom: -20px; font: ${({ theme }) => theme.TEXT.xSmall}; diff --git a/frontend/src/components/common/input/Input.tsx b/frontend/src/components/common/input/Input.tsx index c022bce15..589cbb39e 100644 --- a/frontend/src/components/common/input/Input.tsx +++ b/frontend/src/components/common/input/Input.tsx @@ -29,7 +29,7 @@ export const Input = ({ {...rest} /> {showCharCount && ( - + {value.toString().length} {rest.maxLength ? ` / ${rest.maxLength}자` : ""} diff --git a/frontend/src/components/common/keyword/Keyword.style.ts b/frontend/src/components/common/keyword/Keyword.style.ts new file mode 100644 index 000000000..5dcb3b052 --- /dev/null +++ b/frontend/src/components/common/keyword/Keyword.style.ts @@ -0,0 +1,53 @@ +import styled from "styled-components"; + +export const KeywordContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const KeywordLabelContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 1rem; +`; + +export const KeywordButton = styled.button` + position: relative; + + display: flex; + align-items: center; + justify-content: center; + + width: fit-content; + height: fit-content; + padding: 0.4rem 0.8rem; + + font: ${({ theme }) => theme.TEXT.small}; + color: ${({ theme }) => theme.COLOR.grey4}; + white-space: nowrap; + + background-color: ${({ theme }) => theme.COLOR.grey0}; + border: 1px solid ${({ theme }) => theme.COLOR.grey0}; + border-radius: 15px; + + svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + color: ${({ theme }) => theme.COLOR.white}; + + opacity: 0; + } + + &:hover { + opacity: 0.5; + background-color: ${({ theme }) => theme.COLOR.grey3}; + } + + &:hover svg { + opacity: 1; + } +`; diff --git a/frontend/src/components/common/keyword/Keyword.tsx b/frontend/src/components/common/keyword/Keyword.tsx new file mode 100644 index 000000000..309c14ea5 --- /dev/null +++ b/frontend/src/components/common/keyword/Keyword.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { useState } from "react"; +import Icon from "@/components/common/icon/Icon"; +import { Input } from "@/components/common/input/Input"; +import * as S from "@/components/common/keyword/Keyword.style"; + +interface KeywordProps { + currentKeywords: string[]; + onKeywordsChange: (keywords: string[]) => void; + error: boolean; +} + +const Keyword = ({ currentKeywords, onKeywordsChange, error }: KeywordProps) => { + const [keyword, setKeyword] = useState(""); + + const removeKeyword = (index: number) => { + const updatedKeywords = currentKeywords.filter((_, i) => i !== index); + onKeywordsChange(updatedKeywords); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setKeyword(e.target.value); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + const trimmedKeyword = keyword.trim(); + + if (trimmedKeyword === "") return; + if (currentKeywords.includes(trimmedKeyword)) { + setKeyword(""); + return; + } + onKeywordsChange([...currentKeywords, trimmedKeyword]); + setKeyword(""); + } + }; + + return ( + + {currentKeywords.length !== 0 && ( + + {currentKeywords.map((keyword, index) => ( + removeKeyword(index)} + title="키워드를 클릭하면 삭제돼요" + > + {keyword} + + + ))} + + )} + + + ); +}; + +export default Keyword; diff --git a/frontend/src/components/common/label/Label.style.ts b/frontend/src/components/common/label/Label.style.ts index f15e6f90c..56ef5b7b1 100644 --- a/frontend/src/components/common/label/Label.style.ts +++ b/frontend/src/components/common/label/Label.style.ts @@ -60,12 +60,7 @@ export const LabelWrapper = styled.div` `; case "MANAGER": return css` - display: flex; gap: 0.4rem; - - font: ${({ theme }) => theme.TEXT.small}; - color: ${theme.COLOR.black}; - background-color: ${theme.COLOR.grey0}; border: 1px solid ${theme.COLOR.grey0}; `; diff --git a/frontend/src/components/common/loading/Loading.style.ts b/frontend/src/components/common/loading/Loading.style.ts index 997845e32..729c416f9 100644 --- a/frontend/src/components/common/loading/Loading.style.ts +++ b/frontend/src/components/common/loading/Loading.style.ts @@ -1,4 +1,5 @@ import styled, { keyframes } from "styled-components"; +import { Z_INDEX } from "@/styles/zIndex"; const moveUpDown = keyframes` 0% { @@ -19,6 +20,7 @@ export const LoadingContainer = styled.div` svg { position: absolute; + z-index: ${Z_INDEX.loading}; top: 50%; left: 50%; transform: translate(-50%, -50%); @@ -31,6 +33,7 @@ export const LoadingContainer = styled.div` export const BackDrop = styled.div` position: fixed; + z-index: ${Z_INDEX.loadingBackdrop}; top: 0; left: 0; diff --git a/frontend/src/components/common/modal/Modal.style.ts b/frontend/src/components/common/modal/Modal.style.ts index 283b16457..4b8e3d246 100644 --- a/frontend/src/components/common/modal/Modal.style.ts +++ b/frontend/src/components/common/modal/Modal.style.ts @@ -1,5 +1,6 @@ import styled, { css, keyframes } from "styled-components"; import media from "@/styles/media"; +import { Z_INDEX } from "@/styles/zIndex"; const fadeIn = keyframes` 0% { @@ -47,6 +48,7 @@ const fadeOutMobile = keyframes` export const BackDrop = styled.div` position: fixed; + z-index: ${Z_INDEX.modalBackdrop}; top: 0; left: 0; @@ -59,8 +61,12 @@ export const BackDrop = styled.div` export const ModalContent = styled.div<{ $isVisible: boolean; $isClosing: boolean }>` position: relative; - overflow: hidden auto; - padding: 2rem; + z-index: ${Z_INDEX.modal}; + + overflow: hidden; + + padding: 3rem 2rem 3rem 3rem; + background-color: ${({ theme }) => theme.COLOR.white}; ${({ $isVisible, $isClosing }) => css` @@ -119,12 +125,18 @@ export const ModalContent = styled.div<{ $isVisible: boolean; $isClosing: boolea `} `; +export const ModalBody = styled.div` + overflow: hidden auto; + width: 100%; + height: 100%; + padding-right: 1rem; +`; + export const CloseButton = styled.button` position: absolute; top: 1rem; right: 1rem; - font: ${({ theme }) => theme.TEXT.large_bold}; color: ${({ theme }) => theme.COLOR.grey2}; background: transparent; diff --git a/frontend/src/components/common/modal/Modal.tsx b/frontend/src/components/common/modal/Modal.tsx index 93887e9d1..0e6286d4e 100644 --- a/frontend/src/components/common/modal/Modal.tsx +++ b/frontend/src/components/common/modal/Modal.tsx @@ -1,3 +1,4 @@ +import Icon from "../icon/Icon"; import { CSSProperties, MouseEvent, ReactNode, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import FocusTrap from "@/components/common/focusTrap/FocusTrap"; @@ -89,15 +90,15 @@ const Modal = ({ isOpen, onClose, hasCloseButton = true, style, children }: Moda handleModalClose(); }} > -
+ <>
{hasCloseButton && ( - × + )} - {children} -
+ {children} + , diff --git a/frontend/src/components/common/plusButton/PlusButton.tsx b/frontend/src/components/common/plusButton/PlusButton.tsx index d5475ac97..ff472ba1c 100644 --- a/frontend/src/components/common/plusButton/PlusButton.tsx +++ b/frontend/src/components/common/plusButton/PlusButton.tsx @@ -9,7 +9,7 @@ const PlusButton = ({ onClick }: PlusButtonProps) => { return ( 더보기 - + ); }; diff --git a/frontend/src/components/common/textarea/Textarea.style.ts b/frontend/src/components/common/textarea/Textarea.style.ts index aeedfc9d0..119d31ad2 100644 --- a/frontend/src/components/common/textarea/Textarea.style.ts +++ b/frontend/src/components/common/textarea/Textarea.style.ts @@ -1,11 +1,7 @@ import styled from "styled-components"; export const TextareaWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 0.8rem; - align-items: flex-end; - + position: relative; width: 100%; `; @@ -40,6 +36,10 @@ export const StyledTextarea = styled.textarea<{ $error: boolean }>` `; export const CharCount = styled.div` + position: absolute; + right: 0; + bottom: -20px; + font: ${({ theme }) => theme.TEXT.xSmall}; color: ${({ theme }) => theme.COLOR.grey2}; `; diff --git a/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx b/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx index f80737ac8..86c21d4f4 100644 --- a/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx +++ b/frontend/src/components/common/timeDropdown/TimeDropdown.stories.tsx @@ -20,6 +20,11 @@ const meta = { description: "시간 변경 시 호출되는 콜백", action: "changed", }, + error: { + control: { + type: "boolean", + }, + }, }, } satisfies Meta; @@ -32,6 +37,7 @@ export const Default: Story = { args: { selectedTime: new Date(), onTimeChange: () => {}, + error: false, }, render: (args) => { const [time, setTime] = useState(args.selectedTime); @@ -42,6 +48,7 @@ export const Default: Story = { setTime(newTime); args.onTimeChange(newTime); }} + error={false} /> ); }, diff --git a/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts b/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts index 63210c468..3d04a9ed8 100644 --- a/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts +++ b/frontend/src/components/common/timeDropdown/TimeDropdown.style.ts @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { Z_INDEX } from "@/styles/zIndex"; //TimeDropdown export const TimeDropdownContainer = styled.section` @@ -6,7 +7,7 @@ export const TimeDropdownContainer = styled.section` width: 100px; `; -export const TimeDropdownToggle = styled.input` +export const TimeDropdownToggle = styled.input<{ $error: boolean }>` cursor: pointer; width: 100%; @@ -16,7 +17,7 @@ export const TimeDropdownToggle = styled.input` text-align: center; letter-spacing: 0.2rem; - border: 1px solid ${({ theme }) => theme.COLOR.grey1}; + border: 1px solid ${({ theme, $error }) => ($error ? theme.COLOR.error : theme.COLOR.grey1)}; border-radius: 6px; outline-color: ${({ theme }) => theme.COLOR.black}; `; @@ -24,7 +25,7 @@ export const TimeDropdownToggle = styled.input` //TimePicker export const TimePickerWrapper = styled.div` position: absolute; - z-index: 999; + z-index: ${Z_INDEX.dropdown}; display: flex; gap: 1rem; diff --git a/frontend/src/components/common/timeDropdown/TimeDropdown.tsx b/frontend/src/components/common/timeDropdown/TimeDropdown.tsx index 88eb8f439..493ad606b 100644 --- a/frontend/src/components/common/timeDropdown/TimeDropdown.tsx +++ b/frontend/src/components/common/timeDropdown/TimeDropdown.tsx @@ -6,6 +6,7 @@ import { formatTime } from "@/utils/dateFormatter"; interface TimeDropdownProps extends InputHTMLAttributes { selectedTime: Date; onTimeChange: (time: Date) => void; + error: boolean; } interface TimeDropdownChangeProps { @@ -71,7 +72,7 @@ const TimePicker = ({ ); }; -export const TimeDropdown = ({ selectedTime, onTimeChange, ...rest }: TimeDropdownProps) => { +export const TimeDropdown = ({ selectedTime, onTimeChange, error, ...rest }: TimeDropdownProps) => { const { isDropdownOpen, handleToggleDropdown, dropdownRef } = useDropdown(); const handleTimeChange = ({ newTime, canCloseDropdown }: TimeDropdownChangeProps) => { @@ -87,6 +88,7 @@ export const TimeDropdown = ({ selectedTime, onTimeChange, ...rest }: TimeDropdo value={formatTime(selectedTime)} onClick={handleToggleDropdown} placeholder="시간을 선택하세요" + $error={error} readOnly {...rest} /> diff --git a/frontend/src/components/dateTimePicker/DateTimePicker.tsx b/frontend/src/components/dateTimePicker/DateTimePicker.tsx index 690e054ba..d060a3c2c 100644 --- a/frontend/src/components/dateTimePicker/DateTimePicker.tsx +++ b/frontend/src/components/dateTimePicker/DateTimePicker.tsx @@ -8,9 +8,15 @@ interface DateTimePickerProps { disabledBeforeDate?: Date; isPastDateDisabled: boolean; }; + error: boolean; } -const DateTimePicker = ({ selectedDateTime, options, onDateTimeChange }: DateTimePickerProps) => { +const DateTimePicker = ({ + selectedDateTime, + onDateTimeChange, + options, + error, +}: DateTimePickerProps) => { const handleDateChange = (newDate: Date) => { const updatedDateTime = new Date(newDate); updatedDateTime.setHours(selectedDateTime.getHours()); @@ -26,14 +32,15 @@ const DateTimePicker = ({ selectedDateTime, options, onDateTimeChange }: DateTim }; return ( - <> +
- - + +
); }; diff --git a/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts b/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts index a6c2251ad..019b205f9 100644 --- a/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts +++ b/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts @@ -33,7 +33,6 @@ export const Overlay = styled.div` pointer-events: auto; position: absolute; - z-index: 10; top: 60px; left: 0; @@ -141,7 +140,6 @@ export const FeedbackKeyword = styled.div` `; export const FeedbackDetailContainer = styled.div` - overflow: hidden; display: flex; flex-direction: column; gap: 1.6rem; diff --git a/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx b/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx index d2990a6ef..2a4d8dced 100644 --- a/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx +++ b/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx @@ -61,20 +61,6 @@ const FeedbackCard = ({ 미션의 상세 피드백 내용입니다. - {!feedbackCardData.isWrited && ( - - -

상대방 피드백을 작성해야 볼 수 있습니다.

- -
-
- )} - @@ -142,6 +128,20 @@ const FeedbackCard = ({ )} + + {!feedbackCardData.isWrited && ( + + +

상대방 피드백을 작성해야 볼 수 있습니다.

+ +
+
+ )}
); 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..df16527f8 --- /dev/null +++ b/frontend/src/components/roomDetailPage/controlButton/ControlButton.style.ts @@ -0,0 +1,72 @@ +import styled from "styled-components"; +import { Z_INDEX } from "@/styles/zIndex"; + +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: ${Z_INDEX.dropdown}; + 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; + box-shadow: ${({ theme }) => theme.BOX_SHADOW.regular}; +`; + +export const DropdownItemWrapper = styled.ul` + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 0.5rem; +`; + +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}; + } +`; + +export const Divider = styled.div` + width: 100%; + height: 1px; + background-color: ${({ theme }) => theme.COLOR.grey2}; +`; diff --git a/frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx b/frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx new file mode 100644 index 000000000..486c2cf14 --- /dev/null +++ b/frontend/src/components/roomDetailPage/controlButton/ControlButton.tsx @@ -0,0 +1,112 @@ +import React, { useState } 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: "EDIT_ROOM" }, + { name: "삭제하기", action: "DELETE_ROOM" }, + ], + PARTICIPATED: [{ name: "방 나가기", action: "EXIT_ROOM" }], +}; + +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 [selectedAction, setSelectedAction] = useState(""); + + const dropdownItems = dropdownItemsConfig[participationStatus] || []; + + const handleControlButtonClick = (event: React.MouseEvent) => { + event.preventDefault(); + handleToggleDropdown(); + }; + + const handleDropdownItemClick = (action: string) => { + setSelectedAction(action); + if (action === "EDIT_ROOM") { + navigate(`/rooms/edit/${roomInfo.id}`); + } else { + handleOpenModal(); + } + }; + + const handleConfirm = () => { + if (selectedAction === "DELETE_ROOM") { + deleteParticipatedRoomMutation.mutate(roomInfo.id, { + onSuccess: () => navigate("/"), + }); + } + if (selectedAction === "EXIT_ROOM") { + deleteParticipateInMutation.mutate(roomInfo.id, { + onSuccess: () => navigate("/"), + }); + } + handleCloseModal(); + }; + + return ( + <> + + {selectedAction && MESSAGES.GUIDANCE[selectedAction]} + + + + + + + + {isDropdownOpen && ( + + handleToggleDropdown()}> + + {dropdownItems.map((item: DropdownItem, index) => ( + <> + handleDropdownItemClick(item.action)} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") handleDropdownItemClick(item.action); + }} + > + {item.name} + + {index < dropdownItems.length - 1 && } + + ))} + + + + )} + + + ); +}; + +export default ControlButton; diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts index e40c565c2..fffd97edf 100644 --- a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.style.ts @@ -121,9 +121,10 @@ export const NoKeywordText = styled.span` `; export const RoomContentSmall = styled.div` - font: ${({ theme }) => theme.TEXT.small_bold}; + font: ${({ theme }) => theme.TEXT.semiSmall}; line-height: normal; - color: ${({ theme }) => theme.COLOR.black}; + color: ${({ theme }) => theme.COLOR.grey4}; + white-space: pre-line; `; export const ContentLineBreak = styled.div` diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.test.tsx b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.test.tsx index 2a88cb1ce..c2238dbb8 100644 --- a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.test.tsx +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.test.tsx @@ -25,7 +25,7 @@ const mockBaseRoomInfo: RoomInfo = { recruitmentDeadline: "2024-10-05T10:30:00+09:00", reviewDeadline: "2024-10-08T10:30:00+09:00", message: "테스트 메세지", - isPrivate: false, + isPublic: false, }; describe("RoomInfoCard 컴포넌트 테스트", () => { diff --git a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx index 7c907e10c..05a6489a1 100644 --- a/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx +++ b/frontend/src/components/roomDetailPage/roomInfoCard/RoomInfoCard.tsx @@ -42,7 +42,7 @@ const RoomInfoCard = ({ roomInfo }: { roomInfo: RoomInfo }) => { -