diff --git a/frontend/src/apis/schedule.ts b/frontend/src/apis/schedule.ts index 0fd280979..d8e7eb98e 100644 --- a/frontend/src/apis/schedule.ts +++ b/frontend/src/apis/schedule.ts @@ -5,12 +5,15 @@ export const fetchSchedules = ( teamPlaceId: number, year: number, month: number, + day?: number, ) => { + const query = day + ? `year=${year}&month=${month}&day=${day}` + : `year=${year}&month=${month}`; + return http.get<{ schedules: Schedule[]; - }>( - `/api/team-place/${teamPlaceId}/calendar/schedules?year=${year}&month=${month}`, - ); + }>(`/api/team-place/${teamPlaceId}/calendar/schedules?${query}`); }; export const fetchScheduleById = (teamPlaceId: number, scheduleId: number) => { diff --git a/frontend/src/components/Calendar/Calendar.tsx b/frontend/src/components/Calendar/Calendar.tsx index 083d2c78d..66ebf6f9f 100644 --- a/frontend/src/components/Calendar/Calendar.tsx +++ b/frontend/src/components/Calendar/Calendar.tsx @@ -14,9 +14,10 @@ import { useModal } from '~/hooks/useModal'; import { generateScheduleBars } from '~/utils/generateScheduleBars'; import { DAYS_OF_WEEK, MODAL_OPEN_TYPE } from '~/constants/calendar'; import { ArrowLeftIcon, ArrowRightIcon, PlusIcon } from '~/assets/svg'; -import type { ModalOpenType } from '~/types/schedule'; import { arrayOf } from '~/utils/arrayOf'; import ScheduleMoreCell from '~/components/ScheduleMoreCell/ScheduleMoreCell'; +import type { Position, ModalOpenType } from '~/types/schedule'; +import DailyScheduleModal from '~/components/DailyScheduleModal/DailyScheduleModal'; const Calendar = () => { const { @@ -35,10 +36,11 @@ const Calendar = () => { const [modalType, setModalType] = useState( MODAL_OPEN_TYPE.ADD, ); - - if (schedules === undefined) { - return null; - } + const [dailyModalDate, setDailyModalDate] = useState(new Date()); + const [dailyModalPosition, setDailyModalPosition] = useState({ + row: 0, + column: 0, + }); const scheduleBars = generateScheduleBars(year, month, schedules); const handleModalOpen = (modalOpenType: ModalOpenType) => { @@ -46,6 +48,20 @@ const Calendar = () => { openModal(); }; + const handleDailyScheduleModalOpen = ( + day: Date, + row: number, + col: number, + ) => { + setModalType(() => MODAL_OPEN_TYPE.DAILY); + setDailyModalDate(() => day); + setDailyModalPosition({ + row, + column: col, + }); + openModal(); + }; + return ( <> @@ -119,13 +135,21 @@ const Calendar = () => { })} - {week.map((day) => { + {week.map((day, colIndex) => { return ( handleModalOpen(MODAL_OPEN_TYPE.ADD)} + onDayClick={(e) => { + e.stopPropagation(); + handleDailyScheduleModalOpen( + day, + rowIndex, + colIndex, + ); + }} /> ); })} @@ -155,6 +179,14 @@ const Calendar = () => { )} /> )} + {isModalOpen && modalType === MODAL_OPEN_TYPE.DAILY && ( + setModalType(() => MODAL_OPEN_TYPE.VIEW)} + /> + )} ); }; diff --git a/frontend/src/components/Calendar/DateCell/DateCell.styled.ts b/frontend/src/components/Calendar/DateCell/DateCell.styled.ts index ab04b7399..b9621364b 100644 --- a/frontend/src/components/Calendar/DateCell/DateCell.styled.ts +++ b/frontend/src/components/Calendar/DateCell/DateCell.styled.ts @@ -8,6 +8,9 @@ interface WrapperProps { } export const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; ${({ size }) => { if (size === 'sm') return css` @@ -18,7 +21,7 @@ export const Wrapper = styled.div` if (size === 'md') return css``; if (size === 'lg') return css` - padding: 8px 8px 0 0; + padding: 2px 2px 0 0; text-align: right; `; }}; @@ -31,3 +34,17 @@ export const Wrapper = styled.div` cursor: pointer; `; + +export const TeamColorBadge = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 20px; + height: 20px; + border-radius: 50%; + + &:hover { + background-color: ${({ theme }) => theme.color.GRAY300}; + } +`; diff --git a/frontend/src/components/Calendar/DateCell/DateCell.tsx b/frontend/src/components/Calendar/DateCell/DateCell.tsx index aa7fabd97..fdda70c77 100644 --- a/frontend/src/components/Calendar/DateCell/DateCell.tsx +++ b/frontend/src/components/Calendar/DateCell/DateCell.tsx @@ -3,16 +3,18 @@ import { parseDate } from '~/utils/parseDate'; import Text from '~/components/common/Text/Text'; import * as S from './DateCell.styled'; import type { DateCellSize } from '~/types/size'; +import type { MouseEventHandler } from 'react'; interface DateCellProps { rawDate: Date; currentMonth: number; onClick?: () => void; + onDayClick?: MouseEventHandler; size?: DateCellSize; } const DateCell = (props: DateCellProps) => { - const { rawDate, currentMonth, size = 'lg', onClick } = props; + const { rawDate, currentMonth, size = 'lg', onClick, onDayClick } = props; const { date, day } = parseDate(rawDate); const isSunday = day === 0; @@ -26,14 +28,16 @@ const DateCell = (props: DateCellProps) => { size={size} onClick={onClick} > - - {date} - + + + {date} + + ); }; diff --git a/frontend/src/components/DailyScheduleModal/DailyScheduleModal.stories.tsx b/frontend/src/components/DailyScheduleModal/DailyScheduleModal.stories.tsx new file mode 100644 index 000000000..706fc6664 --- /dev/null +++ b/frontend/src/components/DailyScheduleModal/DailyScheduleModal.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import DailyScheduleModal from '~/components/DailyScheduleModal/DailyScheduleModal'; +import Button from '~/components/common/Button/Button'; +import { useModal } from '~/hooks/useModal'; +import type { SchedulePosition } from '~/types/schedule'; + +const meta = { + title: 'DailyScheduleModal', + component: DailyScheduleModal, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const { openModal } = useModal(); + + return ( + <> + + console.log('hi')} + rawDate={new Date()} + position={{ row: 0, column: 0 }} + onScheduleModalOpen={({ scheduleId, row, column, level }) => + alert(`${scheduleId}, ${row}, ${column}, ${level}`) + } + /> + + ); + }, + args: { + onScheduleModalOpen: ({ + scheduleId, + row, + column, + level, + }: SchedulePosition & { + scheduleId: number; + }) => { + alert(`${scheduleId}, ${row}, ${column}, ${level}`); + }, + onSetModalType: () => alert('hi'), + position: { row: 0, column: 0 }, + rawDate: new Date(), + }, +}; diff --git a/frontend/src/components/DailyScheduleModal/DailyScheduleModal.styled.ts b/frontend/src/components/DailyScheduleModal/DailyScheduleModal.styled.ts new file mode 100644 index 000000000..e13881cc5 --- /dev/null +++ b/frontend/src/components/DailyScheduleModal/DailyScheduleModal.styled.ts @@ -0,0 +1,85 @@ +import { css, styled } from 'styled-components'; +import type { DailyScheduleModalProps } from '~/components/DailyScheduleModal/DailyScheduleModal'; + +export const Backdrop = styled.div` + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +`; + +export const Container = styled.div` + display: flex; + position: absolute; + flex-direction: column; + z-index: ${({ theme }) => theme.zIndex.MODAL}; + + width: 300px; + height: 338px; + padding: 10px 20px 20px 20px; + + border-radius: 10px; + background-color: ${({ theme }) => theme.color.WHITE}; + + box-shadow: + 0 0 1px #1b1d1f33, + 0 15px 25px #1b1d1f33, + 0 5px 10px #1b1d1f1f; +`; + +export const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 38px; + padding-bottom: 4px; + margin-bottom: 24px; + + border-bottom: ${({ theme }) => `1px solid ${theme.color.GRAY300}`}; +`; + +export const ScheduleWrapper = styled.div` + display: flex; + position: relative; + flex-wrap: wrap; + overflow: auto; + + width: 100%; + max-height: 80%; + height: auto; + + gap: 10px; +`; + +export const ScheduleBox = styled.div>` + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 40px; + + background-color: ${({ color }) => color}; + border-radius: 4px; + + filter: brightness(1.5); + + cursor: pointer; +`; + +export const closeButton = css` + width: 22px; + height: 38px; + padding: 8px 0; +`; + +export const teamName = css` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + max-width: 200px; +`; diff --git a/frontend/src/components/DailyScheduleModal/DailyScheduleModal.tsx b/frontend/src/components/DailyScheduleModal/DailyScheduleModal.tsx new file mode 100644 index 000000000..42634b23f --- /dev/null +++ b/frontend/src/components/DailyScheduleModal/DailyScheduleModal.tsx @@ -0,0 +1,104 @@ +import { useModal } from '~/hooks/useModal'; +import * as S from './DailyScheduleModal.styled'; +import Modal from '~/components/common/Modal/Modal'; +import { CloseIcon } from '~/assets/svg'; +import Button from '~/components/common/Button/Button'; +import Text from '~/components/common/Text/Text'; +import { parseDate } from '~/utils/parseDate'; +import { useFetchDailySchedules } from '~/hooks/queries/useFetchDailySchedules'; +import type { Position, SchedulePosition } from '~/types/schedule'; +import type { CSSProperties } from 'react'; + +export interface DailyScheduleModalProps { + position: Position; + rawDate: Date; + onScheduleModalOpen: ({ + scheduleId, + row, + column, + level, + }: SchedulePosition & { + scheduleId: number; + }) => void; + onSetModalType: () => void; + color?: string; +} + +const DailyScheduleModal = (props: DailyScheduleModalProps) => { + const { + color = '#516FFF', + rawDate, + position, + onScheduleModalOpen, + onSetModalType, + } = props; + const { row, column } = position; + + const { closeModal } = useModal(); + const { year, month, date } = parseDate(rawDate); + + const schedules = useFetchDailySchedules(1, year, month, date); + + const modalLocation: CSSProperties = { + top: row < 3 ? `${(row + 2) * 118}px` : 'none', + bottom: row >= 3 ? `${(7 - row) * 120}px` : 'none', + left: column < 3 ? `${(column * 100) / 7}%` : 'none', + right: column >= 3 ? `${((6 - column) * 100) / 7}%` : 'none', + }; + + return ( + + + + + + {month}월 {date}일 + + + + + {schedules.length !== 0 ? ( + schedules.map((schedule, index) => { + const { id, title } = schedule; + + return ( + { + onScheduleModalOpen({ + scheduleId: id, + row, + column, + level: 4, + }); + onSetModalType(); + }} + > + + {title} + + + ); + }) + ) : ( + + 등록된 일정이 없습니다. + + )} + + + + ); +}; + +export default DailyScheduleModal; diff --git a/frontend/src/components/ScheduleAddModal/ScheduleAddModal.stories.tsx b/frontend/src/components/ScheduleAddModal/ScheduleAddModal.stories.tsx index 4064abbcd..3e8147b1d 100644 --- a/frontend/src/components/ScheduleAddModal/ScheduleAddModal.stories.tsx +++ b/frontend/src/components/ScheduleAddModal/ScheduleAddModal.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; -import Button from '../common/Button/Button'; import { useModal } from '~/hooks/useModal'; import ScheduleAddModal from './ScheduleAddModal'; +import Button from '~/components/common/Button/Button'; /** * `ScheduleAddModal` 컴포넌트는 일정 등록을 위한 폼을 포함하고 있는 모달 컴포넌트입니다. diff --git a/frontend/src/constants/calendar.ts b/frontend/src/constants/calendar.ts index 1cf0538cc..1ba3242f6 100644 --- a/frontend/src/constants/calendar.ts +++ b/frontend/src/constants/calendar.ts @@ -11,4 +11,5 @@ export const MODAL_OPEN_TYPE = { ADD: 'add', VIEW: 'view', EDIT: 'edit', + DAILY: 'daily', } as const; diff --git a/frontend/src/hooks/queries/useFetchDailySchedules.ts b/frontend/src/hooks/queries/useFetchDailySchedules.ts new file mode 100644 index 000000000..461534a06 --- /dev/null +++ b/frontend/src/hooks/queries/useFetchDailySchedules.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchSchedules } from '~/apis/schedule'; + +export const useFetchDailySchedules = ( + teamPlaceId: number, + year: number, + month: number, + day: number, +) => { + const { data } = useQuery(['dailySchedules', year, month, day], () => + fetchSchedules(teamPlaceId, year, month, day), + ); + + if (data === undefined) return []; + + const { schedules } = data; + + return schedules; +}; diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts index a648a5dfb..376dfd745 100644 --- a/frontend/src/styles/theme.ts +++ b/frontend/src/styles/theme.ts @@ -6,6 +6,8 @@ const color = { RED: '#FF5B5B', PURPLE: '#6E61ff', + NAVY: '#303650', + GRAY100: '#f2f4f6', GRAY200: '#e5e8eb', GRAY300: '#d1d6db',