diff --git a/.eslintrc.json b/.eslintrc.json index 10f001f8..2180e0d1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,7 +28,8 @@ "no-duplicate-imports": "error", //중복 Import 안돼 "no-console": ["warn", { "allow": ["warn", "error", "info"] }], //콘솔은 확인 뒤 지우기 - "no-unused-vars": "error", //사용하지 않은 변수면 없애기 + "no-unused-vars": "off", //사용하지 않은 변수면 없애기 + "@typescript-eslint/no-unused-vars": ["error"], //사용하지 않은 변수면 없애기 "no-multiple-empty-lines": "error", //공백 금지 "no-undef": "error", //정의 안 한 변수 사용 x "indent": "off", // 프리티어 충돌로 인한 OFF diff --git a/package.json b/package.json index 1a45c503..5826cc24 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "lint:css": "stylelint './src/**/*.{ts,tsx}'" }, "dependencies": { + "@tanstack/react-query": "^5.45.1", + "@tanstack/react-query-devtools": "^5.45.1", "@types/axios": "^0.14.0", "@types/react-copy-to-clipboard": "^5.0.4", "axios": "^1.4.0", diff --git a/src/App.css b/src/App.css index a19e07a5..a37449c4 100644 --- a/src/App.css +++ b/src/App.css @@ -1,58 +1,57 @@ #app { - height: 100%; + height: 100%; } html, body { - position: relative; - height: 100%; + position: relative; + height: 100%; } body { - margin: 0; - background: #eee; - padding: 0; - color: #000; - font-family: Helvetica Neue, Helvetica, Arial, sans-serif; - font-size: 14px; + margin: 0; + padding: 0; + color: #000; + font-size: 14px; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; + background: #eee; } .swiper { - z-index: 0; - width: 100%; - height: 100%; + z-index: 0; + width: 100%; + height: 100%; } .swiper-slide { - - /* Center slide text vertically */ - display: flex; - align-items: center; - justify-content: center; - background: #141414; - text-align: center; - font-size: 18px; + /* Center slide text vertically */ + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + text-align: center; + background: #141414; } .swiper-slide img { - display: block; - width: 100%; - height: 100%; - object-fit: cover; + display: block; + width: 100%; + height: 100%; + object-fit: cover; } .swiper-button-prev { - display: none; + display: none; } .swiper-button-next { - display: none; + display: none; } .swiper-pagination-bullet { - background-color: #A4A4A4; + background-color: #a4a4a4 !important; } .swiper-pagination-bullet-active { - background-color: #3253FF; -} \ No newline at end of file + background-color: #3253ff !important; +} diff --git a/src/App.tsx b/src/App.tsx index a5a64135..75914087 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,18 @@ -import { useEffect } from 'react'; +/**카카오톡 인앱브라우저 종료후 크롬 및 사파리로 오픈하는 utils file */ +import './utils/changeBrowser'; +import 'react-toastify/dist/ReactToastify.css'; +import './App.css'; -import { ThemeProvider } from 'styled-components'; -import styled from 'styled-components/macro'; -import ToastContainerBox from 'utils/toast/ToastContainer'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import Router from './Router'; import GlobalStyle from './styles/globalStyles'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import Router from './Router'; +import { ThemeProvider } from 'styled-components'; +import ToastContainerBox from 'utils/toast/ToastContainer'; +import styled from 'styled-components/macro'; import { theme } from './styles/theme'; - -import './App.css'; -/**카카오톡 인앱브라우저 종료후 크롬 및 사파리로 오픈하는 utils file */ -import './utils/changeBrowser'; -import 'react-toastify/dist/ReactToastify.css'; +import { useEffect } from 'react'; const MobileWrapper = styled.div` display: flex; @@ -47,14 +48,19 @@ function App() { window.removeEventListener('resize', setScreenSize); }; }, []); + + const queryClient = new QueryClient(); return ( <> - - - - - + + + + + + + + ); diff --git a/src/Router.tsx b/src/Router.tsx index 8536d068..93b95642 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,15 +1,16 @@ +import { BrowserRouter, Route, Routes } from 'react-router-dom'; + import ChooseBestTime from 'pages/bestMeetTime/ChooseBestTime'; import CreateMeeting from 'pages/createMeeting/CreateMeeting'; import CueCard from 'pages/cueCard/CueCard'; import ErrorPage404 from 'pages/errorLoading/ErrorPage404'; import LoadingPage from 'pages/errorLoading/LoadingPage'; -import SelectSchedulePriority from 'pages/legacy/selectSchedule/SelectPriorityPage'; -import SelectPage from 'pages/legacy/selectSchedule/SelectSchedulePage'; import LoginEntrance from 'pages/loginEntrance/LoginEntrance'; import OnBoarding from 'pages/onBoarding/OnBoarding'; +import SelectPage from 'pages/legacy/selectSchedule/SelectSchedulePage'; +import SelectSchedule from 'pages/selectSchedule/SelectSchedule'; +import SelectSchedulePriority from 'pages/legacy/selectSchedule/SelectPriorityPage'; import SteppingLayout from 'pages/steppingStone/SteppingLayout'; -import Test from 'pages/Test'; -import { BrowserRouter, Route, Routes } from 'react-router-dom'; const Router = () => { return ( @@ -18,8 +19,7 @@ const Router = () => { } /> } /> } /> - } /> - } /> + } /> } @@ -36,8 +36,6 @@ const Router = () => { } /> } /> } /> - } /> - } /> ); diff --git a/src/components/atomComponents/Button.tsx b/src/components/atomComponents/Button.tsx index 8428104a..34f99cd2 100644 --- a/src/components/atomComponents/Button.tsx +++ b/src/components/atomComponents/Button.tsx @@ -1,7 +1,7 @@ -import React from 'react'; - import { css, styled } from 'styled-components'; +import React from 'react'; + interface ButtonProps { children: React.ReactNode; typeState: string; @@ -73,6 +73,7 @@ const buttonCSS = { ${buttonDefaultCSS.basicCss}; background: ${({ theme }) => theme.colors.grey7}; color: ${({ theme }) => theme.colors.grey5}; + pointer-events: none; `, halfsecondaryDisabled: css` ${buttonDefaultCSS.basicCss}; diff --git a/src/components/moleculesComponents/Header.tsx b/src/components/moleculesComponents/Header.tsx index 10528811..0ac32bf3 100644 --- a/src/components/moleculesComponents/Header.tsx +++ b/src/components/moleculesComponents/Header.tsx @@ -1,30 +1,28 @@ -import { BackIc, ExitIc, HambergerIc, LinkIc, MainLogoIc } from 'components/Icon/icon'; import { Dispatch, SetStateAction, useState } from 'react'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import Navigation from './Navigation'; import Text from 'components/atomComponents/Text'; -import { notify } from 'utils/toast/copyLink'; +import { BackIc, ExitIc, HambergerIc, LinkIc, MainLogoIc } from 'components/Icon/icon'; +import { useScheduleStepContext } from 'pages/selectSchedule/contexts/useScheduleStepContext'; +import { ScheduleStepType } from 'pages/selectSchedule/types'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { useParams } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import styled from 'styled-components/macro'; import { theme } from 'styles/theme'; -import { useNavigate } from 'react-router-dom'; -import { useParams } from 'react-router'; +import { notify } from 'utils/toast/copyLink'; + +import Navigation from './Navigation'; + interface HeaderProps { position: string; - setStep?: Dispatch>; + setFunnelStep?: Dispatch>; + setSelectScheduleStep?: Dispatch>; } -function Header({ position, setStep }: HeaderProps) { +function Header({ position, setFunnelStep }: HeaderProps) { + const { scheduleStep, setScheduleStep } = useScheduleStepContext(); const navigationOptions = [ - // { - // title: '공지사항', - // url: '', - // }, - // { - // title: 'ASAP family', - // url: '', - // }, { title: '약속 생성하기', url: '/meet/create', @@ -38,8 +36,8 @@ function Header({ position, setStep }: HeaderProps) { const navigate = useNavigate(); const [isNaviOpen, setIsNaviOpen] = useState(false); const backToFunnel = () => { - if (setStep !== undefined) { - setStep((prev) => { + if (setFunnelStep !== undefined) { + setFunnelStep((prev) => { if (prev === 0) { navigate('/'); return prev; @@ -48,6 +46,16 @@ function Header({ position, setStep }: HeaderProps) { }); } }; + const backToSelectSchedule = () => { + if (setScheduleStep !== undefined) { + if (scheduleStep === 'selectTimeSlot') { + window.history.back(); + return; + } else if (scheduleStep === 'selectPriority') { + setScheduleStep('selectTimeSlot'); + } + } + }; const { meetingId } = useParams(); return ( @@ -78,7 +86,7 @@ function Header({ position, setStep }: HeaderProps) { ) : position === 'schedule' ? ( - window.history.back()}> + diff --git a/src/components/moleculesComponents/TitleComponents.tsx b/src/components/moleculesComponents/TitleComponents.tsx index 4a532931..66ff3363 100644 --- a/src/components/moleculesComponents/TitleComponents.tsx +++ b/src/components/moleculesComponents/TitleComponents.tsx @@ -4,30 +4,35 @@ import { theme } from 'styles/theme'; interface TextProps { main: string; - sub: string; + sub?: string; + padding?: string; } -function TitleComponents({ main, sub }: TextProps) { +const defaultPadding = `4.4rem 0 4.2rem 0`; +function TitleComponents({ main, sub, padding = defaultPadding }: TextProps) { return ( - + {main} - - {sub} - + {sub && ( + + {sub} + + )} ); } export default TitleComponents; -const TitleWrapper = styled.div` +const TitleWrapper = styled.div<{ $padding: string }>` display: flex; position: relative; flex-direction: column; gap: 1.2rem; - padding: 4.4rem 0 4.2rem 0; + padding: ${({ $padding }) => $padding}; + width: 100%; `; diff --git a/src/components/timetableComponents/Timetable.tsx b/src/components/timetableComponents/Timetable.tsx index bde346a3..aa2b904e 100644 --- a/src/components/timetableComponents/Timetable.tsx +++ b/src/components/timetableComponents/Timetable.tsx @@ -1,40 +1,42 @@ import { ReactNode } from 'react'; -import { DateType } from 'pages/selectSchedule/SelectSchedule'; import styled from 'styled-components'; -import Column from './parts/Column'; import DateTitle from './parts/ColumnTitle'; import SlotTitle from './parts/SlotTitle'; - -interface RenderProps { - date: string; - timeSlots: string[]; -} +import { ColumnStructure, DateType } from './types'; interface TimetableProps { timeSlots: string[]; availableDates: DateType[]; - children: (props: RenderProps) => ReactNode; + children: (props: ColumnStructure) => ReactNode; + bottomItem?: ReactNode; } -function Timetable({ timeSlots, availableDates, children }: TimetableProps) { +function Timetable({ timeSlots, availableDates, children, bottomItem }: TimetableProps) { const emptyDates = Array.from({ length: 7 - availableDates.length }, (_, i) => `empty${i + 1}`); return ( - - - - - - {availableDates.map((date) => { - const dateKey = Object.values(date).join('/'); - return {children({ date: dateKey, timeSlots })}; - })} - {emptyDates && emptyDates.map((value) => )} -
-
-
+ <> + + + + + + {availableDates.map((date) => { + const dateKey = Object.values(date).join('/'); + return ( + + {children({ date: dateKey, timeSlots })} + + ); + })} + {emptyDates && emptyDates.map((value) => )} + + + + {bottomItem} + ); } @@ -45,19 +47,26 @@ const TimetableWrapper = styled.div` gap: 0.75rem; `; -const TableWrapper = styled.div` +const TableWithDateWrapper = styled.div` display: flex; flex-direction: column; gap: 0.8rem; `; -const Table = styled.div` +const TableWrapper = styled.div` display: flex; border-bottom: 1px solid ${({ theme }) => theme.colors.grey7}; border-left: 1px solid ${({ theme }) => theme.colors.grey7}; `; -const EmptyColumn = styled.div` +const ColumnWrapper = styled.div` + display: flex; + flex-direction: column; + + border-right: 1px solid ${({ theme }) => theme.colors.grey7}; +`; + +const EmptyColumnWrapper = styled.div` display: flex; flex-direction: column; border-top: 1px solid ${({ theme }) => theme.colors.grey7}; diff --git a/src/components/timetableComponents/context.ts b/src/components/timetableComponents/context.ts deleted file mode 100644 index 94d14cf6..00000000 --- a/src/components/timetableComponents/context.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createContext, useContext } from 'react'; - -export type SelectedSlotType = { - [key: number]: { - date: string; - startSlot: string; - endSlot: string; - }; -}; - -type TimetableContextType = { - startSlot?: string; - setStartSlot: (startSlot?: string) => void; - selectedSlots: SelectedSlotType; - setSelectedSlots: (selectedSlots: SelectedSlotsType) => void; -}; - -export const TimetableContext = createContext({ - startSlot: undefined, - setStartSlot: () => undefined, - selectedSlots: {}, - setSelectedSlots: () => undefined, -}); - -export function useTimetableContext() { - const context = useContext(TimetableContext); - if (context == null) { - throw new Error('TimetableContext Error'); - } - return context; -} diff --git a/src/components/timetableComponents/parts/Column.tsx b/src/components/timetableComponents/parts/Column.tsx deleted file mode 100644 index 894d8da6..00000000 --- a/src/components/timetableComponents/parts/Column.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode } from 'react'; - -import styled from 'styled-components'; - -interface ColumnProps { - children: ReactNode; -} - -function Column({ children }: ColumnProps) { - return {children}; -} - -export default Column; - -const ColumnWrapper = styled.div` - display: flex; - flex-direction: column; - - border-right: 1px solid ${({ theme }) => theme.colors.grey7}; -`; diff --git a/src/components/timetableComponents/parts/ColumnTitle.tsx b/src/components/timetableComponents/parts/ColumnTitle.tsx index c6bf61dd..74dd699a 100644 --- a/src/components/timetableComponents/parts/ColumnTitle.tsx +++ b/src/components/timetableComponents/parts/ColumnTitle.tsx @@ -1,8 +1,9 @@ -import { DateType } from '../Timetable'; import Text from 'components/atomComponents/Text'; import styled from 'styled-components'; import { theme } from 'styles/theme'; +import { DateType } from '../types'; + interface ColumnTitleProps { availableDates: DateType[]; } diff --git a/src/components/timetableComponents/parts/Slot.tsx b/src/components/timetableComponents/parts/Slot.tsx index fd2dea06..355fd68a 100644 --- a/src/components/timetableComponents/parts/Slot.tsx +++ b/src/components/timetableComponents/parts/Slot.tsx @@ -1,45 +1,36 @@ +import { ReactNode } from 'react'; import styled from 'styled-components'; -import useSlotSeletion from '../hooks/useSlotSelection'; - interface SlotProps { - slot: string; - selectedEntryId?: number; + slotId: string; + slotStyle?: string; + onClick?: () => void; + children?: ReactNode; } -function Slot({ slot, selectedEntryId }: SlotProps) { - const { startSlot, onClickSlot } = useSlotSeletion(); - - const borderStyle = slot.endsWith(':30') ? 'dashed' : 'solid'; - const styledSlotProps = { - $borderStyle: borderStyle, - $isSelected: selectedEntryId !== undefined, - onClick: () => onClickSlot(slot, selectedEntryId), - }; +function Slot({ slotId, slotStyle, onClick, children }: SlotProps) { + const borderStyle = slotId.endsWith(':30') ? 'dashed' : 'solid'; - if (slot === startSlot) { - return ; - } else { - return ; - } + return ( + + {children} + + ); } export default Slot; -const DefaultSlot = styled.div<{ $borderStyle: string; $isSelected: boolean }>` - border-top: 1px ${({ $borderStyle }) => $borderStyle} ${({ theme }) => theme.colors.grey7}; - background-color: ${({ $isSelected, theme }) => - $isSelected ? theme.colors.main1 : 'transparent'}; - cursor: pointer; - width: 4.4rem; - height: 2.2rem; -`; +const DefaultSlot = styled.div<{ + $borderStyle: string; + $slotStyle?: string; +}>` + border-top: 1px solid ${({ theme }) => theme.colors.grey7}; + border-top-style: ${({ $borderStyle }) => $borderStyle}; + ${({ $slotStyle }) => $slotStyle}; -const SelectingSlot = styled.div<{ $borderStyle: string; $isSelected: boolean }>` - border: 1px dashed ${({ theme }) => theme.colors.main5}; - background-color: ${({ $isSelected, theme }) => - $isSelected ? theme.colors.main1 : 'transparent'}; - cursor: pointer; width: 4.4rem; height: 2.2rem; + + display: flex; + justify-content: center; `; diff --git a/src/components/timetableComponents/types.ts b/src/components/timetableComponents/types.ts new file mode 100644 index 00000000..bf73e819 --- /dev/null +++ b/src/components/timetableComponents/types.ts @@ -0,0 +1,22 @@ +interface BaseStructure { + timeSlots: string[]; +} + +export interface TimetableStructure extends BaseStructure { + availableDates: DateType[]; +} + +export interface ColumnStructure extends BaseStructure { + date: string; +} + +export interface DateType { + month: string | undefined; + day: string | undefined; + dayOfWeek: string | undefined; +} + +export interface SlotType { + startTime: string; + endTime: string; +} diff --git a/src/components/timetableComponents/utils.ts b/src/components/timetableComponents/utils.ts index 76cab6a5..d3f6d7bb 100644 --- a/src/components/timetableComponents/utils.ts +++ b/src/components/timetableComponents/utils.ts @@ -1,27 +1,27 @@ -import { SlotType } from './Timetable'; - +import { SlotType } from './types'; /** * - * @description 시작 시간(startTime)과 종료 시간(endTime) 사이에서 30분 간격으로 시간 슬롯을 생성하여 반환하는 함수입니다. + * @desc 문자열로 된 time('HH:MM')에 minutes을 더하는 함수 */ +export const addMinutes = (time: string, minutes: number) => { + const [hour, minute] = time.split(':').map(Number); + const totalMinutes = hour * 60 + minute + minutes; + const newHour = String(Math.floor(totalMinutes / 60)).padStart(2, '0'); + const newMinute = String(totalMinutes % 60).padStart(2, '0'); + return `${newHour}:${newMinute}`; +}; -export function getAvailableTimes(times: SlotType) { - function addMinutes(time: string, minutes: number) { - const [hour, minute] = time.split(':').map(Number); - const totalMinutes = hour * 60 + minute + minutes; - const newHour = String(Math.floor(totalMinutes / 60)).padStart(2, '0'); - const newMinute = String(totalMinutes % 60).padStart(2, '0'); - return `${newHour}:${newMinute}`; - } +/** + * + * @desc 시작 시간(startTime)과 종료 시간(endTime) 사이에서 30분 간격으로 시간 슬롯을 생성하여 반환하는 함수 + */ - function getTimeSlots(startTime: string, endTime: string): string[] { - const slots = []; - let curTime = startTime; - while (curTime < endTime) { - slots.push(curTime); - curTime = addMinutes(curTime, 30); - } - return slots; +export const getAvailableTimes = ({ startTime, endTime }: SlotType) => { + const slots = []; + let curTime = startTime; + while (curTime < endTime) { + slots.push(curTime); + curTime = addMinutes(curTime, 30); } - return getTimeSlots(times.startTime, times.endTime); -} + return slots; +}; diff --git a/src/pages/LoginEntrance/components/MemberComponent.tsx b/src/pages/LoginEntrance/components/MemberComponent.tsx index b1a30430..a87ea7ac 100644 --- a/src/pages/LoginEntrance/components/MemberComponent.tsx +++ b/src/pages/LoginEntrance/components/MemberComponent.tsx @@ -1,16 +1,16 @@ import React, { Dispatch, SetStateAction } from 'react'; -import { userNameAtom } from 'atoms/atom'; import Button from 'components/atomComponents/Button'; +import Header from 'components/moleculesComponents/Header'; import Text from 'components/atomComponents/Text'; import TextInput from 'components/atomComponents/TextInput'; -import Header from 'components/moleculesComponents/Header'; import TitleComponent from 'components/moleculesComponents/TitleComponents'; -import { useParams } from 'react-router'; -import { useNavigate } from 'react-router-dom'; -import { useRecoilState } from 'recoil'; import styled from 'styled-components/macro'; import { theme } from 'styles/theme'; +import { useNavigate } from 'react-router-dom'; +import { useParams } from 'react-router'; +import { useRecoilState } from 'recoil'; +import { userNameAtom } from 'atoms/atom'; interface HostInfoProps { name: string; @@ -38,7 +38,7 @@ function MemberComponent({ hostInfo, setHostInfo }: HostProps) { const loginMember = () => { setUserName(hostInfo.name); - navigate(`/member/schedule/${meetingId}`); + navigate(`/member/select/${meetingId}`); }; return ( diff --git a/src/pages/LoginEntrance/components/NoAvailableTimeModal.tsx b/src/pages/LoginEntrance/components/NoAvailableTimeModal.tsx index 3b56bf2d..0b82b270 100644 --- a/src/pages/LoginEntrance/components/NoAvailableTimeModal.tsx +++ b/src/pages/LoginEntrance/components/NoAvailableTimeModal.tsx @@ -10,7 +10,7 @@ interface ModalProps { setIsModalOpen: Dispatch>; } -function NoAvailableTimeModal ({ setIsModalOpen }: ModalProps) { +function NoAvailableTimeModal({ setIsModalOpen }: ModalProps) { const { meetingId } = useParams(); return ( @@ -29,7 +29,7 @@ function NoAvailableTimeModal ({ setIsModalOpen }: ModalProps) { 방장 페이지에 접속할 수 있어요! - + 가능 시간 입력하러 가기 diff --git a/src/pages/OverallSchedule/OverallSchedule.tsx b/src/pages/OverallSchedule/OverallSchedule.tsx index f29f495f..84a2d4ef 100644 --- a/src/pages/OverallSchedule/OverallSchedule.tsx +++ b/src/pages/OverallSchedule/OverallSchedule.tsx @@ -1,167 +1,47 @@ -import React, { useEffect, useState } from 'react'; - -import { availableDatesAtom, preferTimesAtom, timeSlotUserNameAtom } from 'atoms/atom'; -import Text from 'components/atomComponents/Text'; -import LoadingPage from 'pages/errorLoading/LoadingPage'; +import { getAvailableTimes } from 'components/timetableComponents/utils'; import { useParams } from 'react-router-dom'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { OverallScheduleData } from 'src/types/overallScheduleType'; -import { styled } from 'styled-components'; -import { theme } from 'styles/theme'; -import { availableScheduleOptionApi } from 'utils/apis/availbleScheduleOptionApi'; -import { overallScheduleApi } from 'utils/apis/overallScheduleApi'; - -import TimeTable from './components/TimeTable'; -import { getFormattedAvailableDateTimes } from './utils/getFormattedAvailableDateTimes'; +import styled from 'styled-components'; +import { useGetOverallSchedule } from 'utils/apis/useGetOverallSchedule'; +import { useGetTimetable } from 'utils/apis/useGetTimetable'; +import OverallScheduleTable from './components/OverallScheduleTable'; +import Title from './components/Title'; -const OverallSchedule = () => { +function OverallSchedule() { const { meetingId } = useParams(); - const [overallScheduleData, setOverallScheduleData] = useState(); - - const [availableDates, setAvailableDates] = useRecoilState(availableDatesAtom); - - const [preferTimes, setPreferTimes] = useRecoilState(preferTimesAtom); - - const timeSlotUserNames = useRecoilValue(timeSlotUserNameAtom); - - const [memberCount, setMemberCount] = useState(0); - const [totalUserNames, setTotalUserNames] = useState(); - - const getAvailableScheduleOption = async () => { - try { - const { data } = await availableScheduleOptionApi(meetingId); - setAvailableDates(data.data.availableDates); - setPreferTimes(data.data.preferTimes); - } catch (err) { - console.log(err); - } - }; - - - const getOverallSchedule = async () => { - try { - const result = await overallScheduleApi(meetingId); - const { data } = result.data; - const uniqueData = [...new Set(data.totalUserNames)]; - setOverallScheduleData(data); - setMemberCount(data.memberCount); - setTotalUserNames(uniqueData); - } catch (err) { - console.log(err); - } - }; - - useEffect(() => { - getAvailableScheduleOption(); - getOverallSchedule(); - }, []); + const { data: dataTimetable, isLoading: isLoadingTimetable } = useGetTimetable(meetingId); + const { data: dataOverallSchedule, isLoading: isLoadingOverallSchedule } = useGetOverallSchedule( + meetingId, + ); - const formattedAvailableDateTimes = - overallScheduleData && getFormattedAvailableDateTimes(overallScheduleData); + // 시간대 선택 단계가 없어질 것을 고려하여 상수값을 설정해놓음 + const PREFER_TIMES = { startTime: '06:00', endTime: '24:00' }; return ( - {overallScheduleData ? ( - <> - - - 현재까지  - - - {memberCount.toString()}명 - - - 이 입력했어요 - - - - {totalUserNames && - totalUserNames.map((name, idx) => ( - - {name} - {idx !== totalUserNames.length - 1 ? ',' : ''}  - - ))} - - + {!isLoadingTimetable && + !isLoadingOverallSchedule && + dataTimetable && + dataOverallSchedule && ( + - - {!timeSlotUserNames ? ( - - - 블럭을 선택하면 해당 시간대에 참여가능한 - - - 인원을 확인할 수 있어요 - - - ) : ( - timeSlotUserNames.map((name, idx) => ( - - {name} - {idx !== timeSlotUserNames.length - 1 ? ',' : ''}  - - )) - )} - - - ) : ( - - - - )} + )} ); -}; +} export default OverallSchedule; -const UserNameWrapper = styled.aside` +const OverallScheduleWrapper = styled.div` display: flex; - position: fixed; - bottom: 4.4rem; - flex-wrap: wrap; + flex-direction: column; + align-items: center; justify-content: center; - border: 1px solid ${({ theme }) => theme.colors.grey5}; - border-radius: 0.8rem; - background: ${({ theme }) => theme.colors.grey9}; - width: 33.5rem; - min-height: 8.3rem; - text-align: center; - color: ${({ theme }) => theme.colors.white}; -`; - -const OverallScheduleWrapper = styled.main` - margin-bottom: 16.1rem; + margin-bottom: 16.4rem; `; -const TextOneLine = styled.div` - display: flex; - flex-wrap: wrap; - margin-top: 3.7rem; - width: 100%; -`; -const TotalUserNames = styled.div` - display: flex; - margin-top: 1.2rem; - margin-bottom: 2.4rem; -`; - -const LoadingWrapper = styled.div` - position: relative; - top: 25rem; - width: 100%; -`; - -const TextTwoLine = styled.div` - display:flex; - flex-direction:column; - align-items: center; - justify-content:center; -` \ No newline at end of file diff --git a/src/pages/OverallSchedule/components/OverallScheduleColumn.tsx b/src/pages/OverallSchedule/components/OverallScheduleColumn.tsx new file mode 100644 index 00000000..96853a6d --- /dev/null +++ b/src/pages/OverallSchedule/components/OverallScheduleColumn.tsx @@ -0,0 +1,45 @@ +import Slot from 'components/timetableComponents/parts/Slot'; +import { ColumnStructure } from 'components/timetableComponents/types'; +import { theme } from 'styles/theme'; +import { TimeSlot } from 'utils/apis/useGetOverallSchedule'; + +import { useSlotClick } from '../hooks/useSlotClick'; + +interface OverallScheduleColumnProps extends ColumnStructure { + availableSlotInfo: TimeSlot[]; +} + +function OverallScheduleColumn({ date, timeSlots, availableSlotInfo }: OverallScheduleColumnProps) { + + const { clickedSlot, onClickSlot } = useSlotClick(); + + const getTimeSlotStyle = (colorLevel: number, slotId:string) => { + const COLOR :{ [key : number]: string } = { + 0: 'transparent', + 1: theme.colors.level1, + 2: theme.colors.level2, + 3: theme.colors.level3, + 4: theme.colors.level4, + 5: theme.colors.level5, + }; + + const isClickedSlot = clickedSlot === slotId; + return ` + background-color: ${isClickedSlot && colorLevel!==0 ? theme.colors.sub1 : COLOR[colorLevel]}; + cursor: ${colorLevel !== 0 ? 'pointer' : 'default'}; + ` + } + + return ( + <> + {timeSlots.map((timeSlot) => { + const { colorLevel = 0, userNames = [] } = availableSlotInfo.find((info) => info.time === timeSlot) ?? {}; + const slotId = `${date}/${timeSlot}`; + + return onClickSlot(slotId, userNames)}/>; + })} + + ); +} + +export default OverallScheduleColumn; diff --git a/src/pages/OverallSchedule/components/OverallScheduleTable.tsx b/src/pages/OverallSchedule/components/OverallScheduleTable.tsx new file mode 100644 index 00000000..1553de8d --- /dev/null +++ b/src/pages/OverallSchedule/components/OverallScheduleTable.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; + +import Timetable from 'components/timetableComponents/Timetable'; +import { ColumnStructure, TimetableStructure } from 'components/timetableComponents/types'; +import { + AvailableDateTime, + TimeSlot, + getOverallScheduleResponse, +} from 'utils/apis/useGetOverallSchedule'; + +import OverallScheduleColumn from './OverallScheduleColumn'; +import UserNames from './UserNames'; +import { ClickContext } from '../contexts/useClickContext'; + +interface OverallScheduleTableProps extends TimetableStructure { + dataOverallSchedule: getOverallScheduleResponse['data']; +} + +function OverallScheduleTable({ + timeSlots, + availableDates, + dataOverallSchedule, +}: OverallScheduleTableProps) { + const [clickedSlot, setClickedSlot] = useState(undefined); + const [clickedUserNames, setClickedUserNames] = useState([]); + + const getAvailableTimesPerDate = ( + availableDates: AvailableDateTime[], + date: string, + ): TimeSlot[] => { + const [month, day, dayOfWeek] = date.split('/'); + + const matchedDate = availableDates.find( + (date) => date.month === month && date.day === day && date.dayOfWeek === dayOfWeek, + ); + + return matchedDate ? matchedDate.timeSlots : []; + }; + + return ( + + }> + {({ date, timeSlots }: ColumnStructure) => ( + + )} + + + ); +} + +export default OverallScheduleTable; diff --git a/src/pages/OverallSchedule/components/Title.tsx b/src/pages/OverallSchedule/components/Title.tsx new file mode 100644 index 00000000..5fea4c8a --- /dev/null +++ b/src/pages/OverallSchedule/components/Title.tsx @@ -0,0 +1,49 @@ +import Text from 'components/atomComponents/Text'; +import styled from 'styled-components'; +import { theme } from 'styles/theme'; + +interface TitleProps { + memberCount?: number; + totalUserNames?: string[]; +} + +function Title({ memberCount, totalUserNames }: TitleProps) { + return ( + <> + + + 현재까지  + + + {memberCount !== undefined ? memberCount.toString() : ''}명 + + + 이 입력했어요 + + + + {totalUserNames && ( + + {totalUserNames.join(',')} + + )} + + + ); +} + +export default Title; + +const TextOneLine = styled.div` + display: flex; + flex-wrap: wrap; + margin-top: 3.7rem; + width: 100%; +`; + +const TotalUserNames = styled.div` + display: flex; + margin-top: 1.2rem; + margin-bottom: 2.4rem; + width: 100%; +`; diff --git a/src/pages/OverallSchedule/components/UserNames.tsx b/src/pages/OverallSchedule/components/UserNames.tsx new file mode 100644 index 00000000..48c04a8c --- /dev/null +++ b/src/pages/OverallSchedule/components/UserNames.tsx @@ -0,0 +1,52 @@ +import Text from 'components/atomComponents/Text'; +import styled from 'styled-components'; +import { theme } from 'styles/theme'; + +import { useClickContext } from '../contexts/useClickContext'; + +function UserNames() { + const { clickedUserNames } = useClickContext(); + + return ( + + {clickedUserNames.length === 0 ? ( + + + 블럭을 선택하면 해당 시간대에 참여가능한 + + + 인원을 확인할 수 있어요 + + + ) : ( + + {clickedUserNames.join(', ')} + + )} + + ); +} + +export default UserNames; + +const UserNamesWrapper = styled.aside` + display: flex; + position: fixed; + bottom: 4.4rem; + flex-wrap: wrap; + justify-content: center; + border: 1px solid ${({ theme }) => theme.colors.grey5}; + border-radius: 0.8rem; + background: ${({ theme }) => theme.colors.grey9}; + width: 33.5rem; + min-height: 8.3rem; + text-align: center; + color: ${({ theme }) => theme.colors.white}; +`; + +const Texts = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; diff --git a/src/pages/OverallSchedule/contexts/useClickContext.ts b/src/pages/OverallSchedule/contexts/useClickContext.ts new file mode 100644 index 00000000..4723cb90 --- /dev/null +++ b/src/pages/OverallSchedule/contexts/useClickContext.ts @@ -0,0 +1,23 @@ +import { Dispatch, SetStateAction, createContext, useContext } from 'react'; + +interface ClickContextType { + clickedSlot: string | undefined; + setClickedSlot: Dispatch>; + clickedUserNames: string[]; + setClickedUserNames: Dispatch>; +} + +export const ClickContext = createContext({ + clickedSlot: undefined, + setClickedSlot: () => undefined, + clickedUserNames: [], + setClickedUserNames: () => [], +}); + +export function useClickContext() { + const context = useContext(ClickContext); + if (context == null) { + throw new Error('ClickContext Error'); + } + return context; +} diff --git a/src/pages/OverallSchedule/hooks/useSlotClick.ts b/src/pages/OverallSchedule/hooks/useSlotClick.ts new file mode 100644 index 00000000..f49c5cd7 --- /dev/null +++ b/src/pages/OverallSchedule/hooks/useSlotClick.ts @@ -0,0 +1,12 @@ +import { useClickContext } from '../contexts/useClickContext'; + +export const useSlotClick = () => { + const { clickedSlot, setClickedSlot, setClickedUserNames } = useClickContext(); + + const onClickSlot = (targetSlot: string, targetUserNames: string[]) => { + setClickedSlot(targetSlot); + setClickedUserNames(targetUserNames); + }; + + return { clickedSlot, onClickSlot }; +}; diff --git a/src/pages/SteppingStone/components/SteppingBtnSection.tsx b/src/pages/SteppingStone/components/SteppingBtnSection.tsx index b44ede21..2107e84e 100644 --- a/src/pages/SteppingStone/components/SteppingBtnSection.tsx +++ b/src/pages/SteppingStone/components/SteppingBtnSection.tsx @@ -46,7 +46,7 @@ function SteppingBtnSection({ steppingType }: SteppingProps) { - + diff --git a/src/pages/Test.tsx b/src/pages/Test.tsx deleted file mode 100644 index 828db086..00000000 --- a/src/pages/Test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import styled from 'styled-components'; - -import SelectSchedule from './selectSchedule/SelectSchedule'; - -function Test() { - return ( - - - - ); -} - -export default Test; - -const StyledTest = styled.div` - padding: 5rem; -`; diff --git a/src/pages/createMeeting/CreateMeeting.tsx b/src/pages/createMeeting/CreateMeeting.tsx index 5759f390..b99eeccd 100644 --- a/src/pages/createMeeting/CreateMeeting.tsx +++ b/src/pages/createMeeting/CreateMeeting.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; import Header from 'components/moleculesComponents/Header'; +import { MeetingInfo } from './types/useFunnelInterface'; import ReturnBodyComponent from 'pages/createMeeting/components/ReturnBodyComponent'; import ReturnTitleComponent from 'pages/createMeeting/components/ReturnTitleComponent'; -import styled from 'styled-components/macro'; - import { funnelStep } from './data/meetingInfoData'; -import { MeetingInfo } from './types/useFunnelInterface'; +import styled from 'styled-components/macro'; +import { useGetTimetable } from 'utils/apis/useGetTimetable'; const initialMeetingInfo: MeetingInfo = { title: '', @@ -24,10 +24,11 @@ function CreateMeeting() { const [step, setStep] = useState(0); const [meetingInfo, setMeetingInfo] = useState(initialMeetingInfo); const currentStep = funnelStep[step]; + return ( <> -
+
{ + const { meetingId } = useParams(); + const [overallScheduleData, setOverallScheduleData] = useState(); + + const [availableDates, setAvailableDates] = useRecoilState(availableDatesAtom); + + const [preferTimes, setPreferTimes] = useRecoilState(preferTimesAtom); + + const timeSlotUserNames = useRecoilValue(timeSlotUserNameAtom); + + const [memberCount, setMemberCount] = useState(0); + const [totalUserNames, setTotalUserNames] = useState(); + + const getAvailableScheduleOption = async () => { + try { + const { data } = await availableScheduleOptionApi(meetingId); + setAvailableDates(data.data.availableDates); + setPreferTimes(data.data.preferTimes); + } catch (err) { + console.log(err); + } + }; + + + const getOverallSchedule = async () => { + try { + const result = await overallScheduleApi(meetingId); + const { data } = result.data; + const uniqueData = [...new Set(data.totalUserNames)]; + setOverallScheduleData(data); + setMemberCount(data.memberCount); + setTotalUserNames(uniqueData); + } catch (err) { + console.log(err); + } + }; + + useEffect(() => { + getAvailableScheduleOption(); + getOverallSchedule(); + }, []); + + const formattedAvailableDateTimes = + overallScheduleData && getFormattedAvailableDateTimes(overallScheduleData); + + return ( + + {overallScheduleData ? ( + <> + + + 현재까지  + + + {memberCount.toString()}명 + + + 이 입력했어요 + + + + {totalUserNames && + totalUserNames.map((name, idx) => ( + + {name} + {idx !== totalUserNames.length - 1 ? ',' : ''}  + + ))} + + + + {!timeSlotUserNames ? ( + + + 블럭을 선택하면 해당 시간대에 참여가능한 + + + 인원을 확인할 수 있어요 + + + ) : ( + timeSlotUserNames.map((name, idx) => ( + + {name} + {idx !== timeSlotUserNames.length - 1 ? ',' : ''}  + + )) + )} + + + ) : ( + + + + )} + + ); +}; + +export default OverallSchedule; + +const UserNameWrapper = styled.aside` + display: flex; + position: fixed; + bottom: 4.4rem; + flex-wrap: wrap; + justify-content: center; + border: 1px solid ${({ theme }) => theme.colors.grey5}; + border-radius: 0.8rem; + background: ${({ theme }) => theme.colors.grey9}; + width: 33.5rem; + min-height: 8.3rem; + text-align: center; + color: ${({ theme }) => theme.colors.white}; +`; + +const OverallScheduleWrapper = styled.main` + margin-bottom: 16.1rem; +`; + +const TextOneLine = styled.div` + display: flex; + flex-wrap: wrap; + margin-top: 3.7rem; + width: 100%; +`; + +const TotalUserNames = styled.div` + display: flex; + margin-top: 1.2rem; + margin-bottom: 2.4rem; +`; + +const LoadingWrapper = styled.div` + position: relative; + top: 25rem; + width: 100%; +`; + +const TextTwoLine = styled.div` + display:flex; + flex-direction:column; + align-items: center; + justify-content:center; +` \ No newline at end of file diff --git a/src/pages/OverallSchedule/components/Column.tsx b/src/pages/legacy/overallSchedule/components/Column.tsx similarity index 100% rename from src/pages/OverallSchedule/components/Column.tsx rename to src/pages/legacy/overallSchedule/components/Column.tsx diff --git a/src/pages/OverallSchedule/components/Row.tsx b/src/pages/legacy/overallSchedule/components/Row.tsx similarity index 100% rename from src/pages/OverallSchedule/components/Row.tsx rename to src/pages/legacy/overallSchedule/components/Row.tsx diff --git a/src/pages/OverallSchedule/components/TimeTable.tsx b/src/pages/legacy/overallSchedule/components/TimeTable.tsx similarity index 100% rename from src/pages/OverallSchedule/components/TimeTable.tsx rename to src/pages/legacy/overallSchedule/components/TimeTable.tsx diff --git a/src/pages/OverallSchedule/utils/getFormattedAvailableDateTimes.ts b/src/pages/legacy/overallSchedule/utils/getFormattedAvailableDateTimes.ts similarity index 100% rename from src/pages/OverallSchedule/utils/getFormattedAvailableDateTimes.ts rename to src/pages/legacy/overallSchedule/utils/getFormattedAvailableDateTimes.ts diff --git a/src/pages/OverallSchedule/utils/setUserNames.ts b/src/pages/legacy/overallSchedule/utils/setUserNames.ts similarity index 100% rename from src/pages/OverallSchedule/utils/setUserNames.ts rename to src/pages/legacy/overallSchedule/utils/setUserNames.ts diff --git a/src/pages/legacy/selectSchedule/SelectModal.tsx b/src/pages/legacy/selectSchedule/SelectModal.tsx index b8320c87..484f8e8e 100644 --- a/src/pages/legacy/selectSchedule/SelectModal.tsx +++ b/src/pages/legacy/selectSchedule/SelectModal.tsx @@ -1,30 +1,27 @@ -import React, { Dispatch, SetStateAction, useEffect } from 'react'; - -import { scheduleAtom, userNameAtom } from 'atoms/atom'; +import { userNameAtom } from 'atoms/atom'; import { isAxiosError } from 'axios'; import Text from 'components/atomComponents/Text'; import { ExitIc } from 'components/Icon/icon'; +import { useSelectContext } from 'pages/selectSchedule/contexts/useSelectContext'; +import { formatHostScheduleScheme, formatMemberScheduleScheme } from 'pages/selectSchedule/utils'; import { useNavigate, useParams } from 'react-router'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import styled from 'styled-components/macro'; import { theme } from 'styles/theme'; -import { hostAvailableApi, userAvailableApi } from 'utils/apis/createHostAvailableSchedule'; - -import { ScheduleStates } from './types/Schedule'; -import { transformHostScheduleType, transformUserScheduleType } from './utils/changeApiReq'; +import { hostAvailableApi, userAvailableApi } from 'utils/apis/legacy/createHostAvailableSchedule'; interface ModalProps { - setShowModal: Dispatch>; + setShowModal: (isModalOpen: boolean) => void; } function SelectModal({ setShowModal }: ModalProps) { - const [scheduleList, setScheduleList] = useRecoilState(scheduleAtom); + const { selectedSlots } = useSelectContext(); const userName = useRecoilValue(userNameAtom); const navigate = useNavigate(); const { auth, meetingId } = useParams(); - const updateScheduleType = transformHostScheduleType(scheduleList); - const updateMemberScheduleType = transformUserScheduleType(scheduleList, userName); + const updateScheduleType = formatHostScheduleScheme(selectedSlots); + const updateMemberScheduleType = formatMemberScheduleScheme(selectedSlots, userName); const postHostAvailableApi = async () => { try { @@ -117,7 +114,7 @@ export default SelectModal; const ReturnModalWrpper = styled.div` display: flex; - position: absolute; + position: fixed; top: 0; left: 0; flex-direction: column; @@ -164,12 +161,15 @@ const MentContainer = styled.div` `; const ModalMent = styled.span` - color: ${({ theme }) => theme.colors.white}; - ${({ theme }) => theme.fonts.body2}; + margin-top: 2.4rem; + margin-bottom: 0.8rem; + width: 14.4rem; + text-align: center; - margin-bottom: 0.8rem; - margin-top: 2.4rem; + + color: ${({ theme }) => theme.colors.white}; + ${({ theme }) => theme.fonts.body2}; `; const ModalHighlight = styled.span` diff --git a/src/pages/legacy/selectSchedule/SelectPriorityPage.tsx b/src/pages/legacy/selectSchedule/SelectPriorityPage.tsx index 8a4f0358..fbf5e6ed 100644 --- a/src/pages/legacy/selectSchedule/SelectPriorityPage.tsx +++ b/src/pages/legacy/selectSchedule/SelectPriorityPage.tsx @@ -1,19 +1,18 @@ import React, { useEffect, useState } from 'react'; - import { availableDatesAtom, preferTimesAtom, scheduleAtom } from 'atoms/atom'; -import axios from 'axios'; +import { useNavigate, useParams } from 'react-router-dom'; + import Button from 'components/atomComponents/Button'; -import Text from 'components/atomComponents/Text'; +import Header from 'components/moleculesComponents/Header'; import PriorityDropdown from 'components/legacy/scheduleComponents/components/PriorityDropdown'; +import SelectModal from './SelectModal'; +import Text from 'components/atomComponents/Text'; import TimeTable from 'components/legacy/scheduleComponents/components/TimeTable'; -import Header from 'components/moleculesComponents/Header'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilState } from 'recoil'; +import { availableScheduleOptionApi } from 'utils/apis/legacy/availbleScheduleOptionApi'; +import axios from 'axios'; import styled from 'styled-components'; import { theme } from 'styles/theme'; -import { availableScheduleOptionApi } from 'utils/apis/availbleScheduleOptionApi'; - -import SelectModal from './SelectModal'; +import { useRecoilState } from 'recoil'; const SelectSchedulePriority = () => { const [availableDates, setAvailableDates] = useRecoilState(availableDatesAtom); diff --git a/src/pages/legacy/selectSchedule/SelectSchedulePage.tsx b/src/pages/legacy/selectSchedule/SelectSchedulePage.tsx index 5084ac55..93648246 100644 --- a/src/pages/legacy/selectSchedule/SelectSchedulePage.tsx +++ b/src/pages/legacy/selectSchedule/SelectSchedulePage.tsx @@ -1,21 +1,20 @@ import React, { useEffect, useRef, useState } from 'react'; - import { availableDatesAtom, preferTimesAtom, scheduleAtom } from 'atoms/atom'; -import axios from 'axios'; +import { useNavigate, useParams } from 'react-router-dom'; + import Button from 'components/atomComponents/Button'; -import Text from 'components/atomComponents/Text'; -import { PlusIc } from 'components/Icon/icon'; -import TimeTable from 'components/legacy/scheduleComponents/components/TimeTable'; import Header from 'components/moleculesComponents/Header'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilState } from 'recoil'; import { MeetingDetail } from 'src/types/availbleScheduleType'; +import { PlusIc } from 'components/Icon/icon'; +import { ScheduleStates } from './types/Schedule'; +import SelectSchedule from './components/SelectSchedule'; +import Text from 'components/atomComponents/Text'; +import TimeTable from 'components/legacy/scheduleComponents/components/TimeTable'; +import { availableScheduleOptionApi } from 'utils/apis/legacy/availbleScheduleOptionApi'; +import axios from 'axios'; import styled from 'styled-components/macro'; import { theme } from 'styles/theme'; -import { availableScheduleOptionApi } from 'utils/apis/availbleScheduleOptionApi'; - -import SelectSchedule from './components/SelectSchedule'; -import { ScheduleStates } from './types/Schedule'; +import { useRecoilState } from 'recoil'; function SelectSchedulePage() { // 가능시간 선택지 - 날짜 diff --git a/src/pages/legacy/selectSchedule/utils/changeApiReq.ts b/src/pages/legacy/selectSchedule/utils/changeApiReq.ts deleted file mode 100644 index 2aac57f0..00000000 --- a/src/pages/legacy/selectSchedule/utils/changeApiReq.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - HostAvailableSchduleRequestType, - UserAvailableScheduleRequestType, -} from 'src/types/createAvailableSchduleType'; - -import { ScheduleStates } from '../types/Schedule'; - -export const transformHostScheduleType = ( - scheduleList: ScheduleStates[], -): (HostAvailableSchduleRequestType | null)[] => { - return scheduleList.map((item) => { - const matchedResult = item.date.match(/(\d+)월 (\d+)일 \((\S+)\)/); - if (!matchedResult) { - return null; - } - const [, month, day, dateOfWeek] = matchedResult; - - return { - id: item.id.toString(), - month: month.padStart(2, '0'), - day: day.padStart(2, '0'), - dayOfWeek: dateOfWeek, - startTime: item.startTime, - endTime: item.endTime, - priority: item.priority, - }; - }); -}; - -export const transformUserScheduleType = ( - scheduleList: ScheduleStates[], - meetInfo: string, -): UserAvailableScheduleRequestType => { - const availableTimes = scheduleList.map((item) => { - const matchedResult = item.date.match(/(\d+)월 (\d+)일 \((\S+)\)/); - if (!matchedResult) { - return { - id: '', - month: '', - day: '', - dayOfWeek: '', - startTime: '', - endTime: '', - priority: 0, - }; - } - // const [, month, day, dateOfWeek]: string[] | null = item.date.match( - // /(\d+)월 (\d+)일 \((\S+)\)/, - // ); - const [, month, day, dateOfWeek] = matchedResult; - return { - id: item.id.toString(), - month: month.padStart(2, '0'), - day: day.padStart(2, '0'), - dayOfWeek: dateOfWeek, - startTime: item.startTime, - endTime: item.endTime, - priority: item.priority, - }; - }); - - const final: UserAvailableScheduleRequestType = { - name: meetInfo, - availableTimes, - }; - return final; -}; diff --git a/src/pages/onBoarding/OnBoarding.tsx b/src/pages/onBoarding/OnBoarding.tsx index aafc912a..3457f444 100644 --- a/src/pages/onBoarding/OnBoarding.tsx +++ b/src/pages/onBoarding/OnBoarding.tsx @@ -2,19 +2,19 @@ import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; +import { Autoplay, Navigation, Pagination } from 'swiper/modules'; +import { Swiper, SwiperSlide } from 'swiper/react'; + +import Button from 'components/atomComponents/Button'; import CardPng from 'assets/images/card.png'; +import Explain from './components/Explain'; +import Header from 'components/moleculesComponents/Header'; import InsertPng from 'assets/images/insert.png'; +import { Link } from 'react-router-dom'; import MakePng from 'assets/images/make.png'; import PointPng from 'assets/images/point.png'; -import Button from 'components/atomComponents/Button'; import Text from 'components/atomComponents/Text'; -import Header from 'components/moleculesComponents/Header'; -import { Link } from 'react-router-dom'; import styled from 'styled-components/macro'; -import { Autoplay, Navigation, Pagination } from 'swiper/modules'; -import { Swiper, SwiperSlide } from 'swiper/react'; - -import Explain from './components/Explain'; const slides = [ { @@ -60,7 +60,7 @@ function OnBoarding() { pagination={{ clickable: true, }} - navigation={true} + navigation={false} modules={[Autoplay, Pagination, Navigation]} className="mySwiper" > diff --git a/src/pages/selectSchedule/SelectSchedule.tsx b/src/pages/selectSchedule/SelectSchedule.tsx index 4712222f..6e8dadc1 100644 --- a/src/pages/selectSchedule/SelectSchedule.tsx +++ b/src/pages/selectSchedule/SelectSchedule.tsx @@ -1,70 +1,62 @@ import { useState } from 'react'; -import { SelectedSlotType, TimetableContext } from 'components/timetableComponents/context'; +import Header from 'components/moleculesComponents/Header'; +import TitleComponents from 'components/moleculesComponents/TitleComponents'; import { getAvailableTimes } from 'components/timetableComponents/utils'; +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; +import { useGetTimetable } from 'utils/apis/useGetTimetable'; -import SelectionSlots from './components/SelectionSlots'; -import Timetable from '../../components/timetableComponents/Timetable'; +import Description from './components/Description'; +import SelectScheduleTable from './components/SelectScheduleTable'; +import { ScheduleStepContext } from './contexts/useScheduleStepContext'; +import { ScheduleStepType } from './types'; +import { TITLES } from './utils'; -// api 연결 후 지울 것 -export type DateType = { - month: string | undefined; - day: string | undefined; - dayOfWeek: string | undefined; -}; - -const availableDates: DateType[] = [ - { - month: '6', - day: '20', - dayOfWeek: '목', - }, - { - month: '6', - day: '21', - dayOfWeek: '금', - }, - { - month: '6', - day: '22', - dayOfWeek: '토', - }, - { - month: '6', - day: '23', - dayOfWeek: '일', - }, -]; - -export type SlotType = { - startTime: string; - endTime: string; -}; - -const preferTimes: SlotType = { - startTime: '06:00', - endTime: '24:00', -}; +function SelectSchedule() { + const [scheduleStep, setScheduleStep] = useState('selectTimeSlot'); + const { meetingId } = useParams(); + const { data, isLoading } = useGetTimetable(meetingId); -const timeSlots = getAvailableTimes(preferTimes); + // 시간대 선택 단계가 없어질 것을 고려하여 상수값을 설정해놓음 + const PREFER_TIMES = { startTime: '06:00', endTime: '24:00' }; -function SelectSchedule() { - const [startSlot, setStartSlot] = useState(undefined); - const [selectedSlots, setSelectedSlots] = useState({}); return ( - - - {({ date, timeSlots }) => } - - + + +
+ {!isLoading && + data && ( + <> + {scheduleStep === 'selectTimeSlot' && ( + + )} + + + + )} + + ); } export default SelectSchedule; + +const SelectScheduleWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-bottom: 16.4rem; +`; diff --git a/src/pages/selectSchedule/components/Description.tsx b/src/pages/selectSchedule/components/Description.tsx new file mode 100644 index 00000000..34e88e25 --- /dev/null +++ b/src/pages/selectSchedule/components/Description.tsx @@ -0,0 +1,84 @@ +import { DURATION, PLACE } from '../utils'; + +import Text from 'components/atomComponents/Text'; +import styled from 'styled-components'; +import { theme } from 'styles/theme'; + +interface DescriptionProps { + duration: keyof typeof DURATION; + place: keyof typeof PLACE; + placeDetail?: string; +} + +function Description({ duration: durationOg, place: placeOg, placeDetail }: DescriptionProps) { + const duration = DURATION[durationOg]; + const place = PLACE[placeOg]; + return ( + + + {place ? ( + <> + + + 회의는  + + + {duration}  + + + {'동안'} + + + + + {place} + + {placeDetail && ( + + {`(${placeDetail})`} + + )} + + {'으로 진행될 예정이에요!'} + + + + ) : ( + + + 회의는  + + + {duration}  + + + {'동안 진행될 예정이에요!'} + + + )} + + + ); +} + +export default Description; + +const DescriptionWrapper = styled.div` + display: flex; + position: relative; + margin: 2rem 0; +`; + +const Texts = styled.div` + display: flex; + flex-direction: column; + border-radius: 0.8rem; + background-color: ${theme.colors.grey9}; + padding: 1.5rem 2.4rem; + width: 33.5rem; +`; +const OneLine = styled.div` + display: flex; + flex-wrap: wrap; + width: 100%; +`; diff --git a/src/pages/selectSchedule/components/SelectScheduleTable.tsx b/src/pages/selectSchedule/components/SelectScheduleTable.tsx new file mode 100644 index 00000000..4134e8bc --- /dev/null +++ b/src/pages/selectSchedule/components/SelectScheduleTable.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; + +import Timetable from 'components/timetableComponents/Timetable'; +import { ColumnStructure, TimetableStructure } from 'components/timetableComponents/types'; + +import PriorityColumn from './selectPriority/PriorityColumn'; +import PriorityCta from './selectPriority/PriorityCta'; +import PriorityDropdown from './selectPriority/PriorityDropdown'; +import SelectionColumn from './selectTimeSlot/SelectionColumn'; +import TimeSlotCta from './selectTimeSlot/TimeSlotCta'; +import { useScheduleStepContext } from '../contexts/useScheduleStepContext'; +import { SelectContext, SelectedSlotType } from '../contexts/useSelectContext'; +import { StepSlotsType, StepbottomItemsType } from '../types'; + +function SelectScheduleTable({ timeSlots, availableDates }: TimetableStructure) { + const [startSlot, setStartSlot] = useState(undefined); + const [selectedSlots, setSelectedSlots] = useState({}); + + const { scheduleStep } = useScheduleStepContext(); + + const stepColumns: StepSlotsType = { + selectTimeSlot: ({ date, timeSlots }: ColumnStructure) => ( + + ), + selectPriority: ({ date, timeSlots }: ColumnStructure) => ( + + ), + }; + const stepColumn = stepColumns[scheduleStep]; + + const bottomItems: StepbottomItemsType = { + selectTimeSlot: , + selectPriority: ( + <> + + + + ), + }; + const bottomItem = bottomItems[scheduleStep]; + + return ( + + + {stepColumn} + + + ); +} + +export default SelectScheduleTable; diff --git a/src/pages/selectSchedule/components/SelectionSlots.tsx b/src/pages/selectSchedule/components/SelectionSlots.tsx deleted file mode 100644 index 3e23acaa..00000000 --- a/src/pages/selectSchedule/components/SelectionSlots.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTimetableContext } from '../../../components/timetableComponents/context'; -import Slot from '../../../components/timetableComponents/parts/Slot'; - -interface SelectionSlotsProps { - date: string; - timeSlots: string[]; -} - -function SelectionSlots({ date, timeSlots }: SelectionSlotsProps) { - const { selectedSlots } = useTimetableContext(); - const selectedSlotsPerDate = Object.entries(selectedSlots).filter( - ([, slot]) => slot.date === date, - ); - - return ( - <> - {timeSlots.map((timeSlot) => { - const belongingEntry = selectedSlotsPerDate.find( - ([, { startSlot, endSlot }]) => timeSlot >= startSlot && timeSlot <= endSlot, - ); - - const selectedEntryId = belongingEntry ? parseInt(belongingEntry[0]) : undefined; - - return ( - - ); - })} - - ); -} - -export default SelectionSlots; diff --git a/src/pages/selectSchedule/components/selectPriority/PriorityColumn.tsx b/src/pages/selectSchedule/components/selectPriority/PriorityColumn.tsx new file mode 100644 index 00000000..7c30d2b4 --- /dev/null +++ b/src/pages/selectSchedule/components/selectPriority/PriorityColumn.tsx @@ -0,0 +1,65 @@ +import Text from 'components/atomComponents/Text'; +import { ColumnStructure } from 'components/timetableComponents/types'; +import { useSelectContext } from 'pages/selectSchedule/contexts/useSelectContext'; +import { theme } from 'styles/theme'; + +import Slot from '../../../../components/timetableComponents/parts/Slot'; + +function PriorityColumn({ date, timeSlots }: ColumnStructure) { + const { selectedSlots } = useSelectContext(); + const selectedSlotsPerDate = Object.entries(selectedSlots).filter( + ([, slot]) => slot.date === date, + ); + + const getPriorityColumntyle = (selectedEntryId?: number, priority?: number) => { + const isSelectedSlot = selectedEntryId !== undefined; + const slotColor = + priority === 1 + ? theme.colors.main1 + : priority === 2 + ? theme.colors.main2 + : priority === 3 + ? theme.colors.main3 + : theme.colors.grey6; + + return ` + ${isSelectedSlot ? `background-color:${slotColor}` : `background-color: transparent`} + `; + }; + + return ( + <> + {timeSlots.map((timeSlot) => { + const belongingEntry = selectedSlotsPerDate.find( + ([, { startSlot, endSlot }]) => timeSlot >= startSlot && timeSlot <= endSlot, + ); + + let isFirstSlot = false; + if (belongingEntry !== undefined) { + if (selectedSlots[parseInt(belongingEntry[0])].startSlot === timeSlot) { + isFirstSlot = true; + } + } + + const selectedEntryId = belongingEntry ? parseInt(belongingEntry[0]) : undefined; + const slotId = `${date}/${timeSlot}`; + const priority = + selectedEntryId !== undefined ? selectedSlots[selectedEntryId].priority : 0; + + return ( + + + {isFirstSlot && priority !== 0 ? priority : ''} + + + ); + })} + + ); +} + +export default PriorityColumn; diff --git a/src/pages/selectSchedule/components/selectPriority/PriorityCta.tsx b/src/pages/selectSchedule/components/selectPriority/PriorityCta.tsx new file mode 100644 index 00000000..61709cdd --- /dev/null +++ b/src/pages/selectSchedule/components/selectPriority/PriorityCta.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react'; + +import Button from 'components/atomComponents/Button'; +import Text from 'components/atomComponents/Text'; +import SelectModal from 'pages/legacy/selectSchedule/SelectModal'; +import styled from 'styled-components'; + +function PriorityCta() { + const [isModalOpen, setIsModalOpen] = useState(false); + return ( + <> + + + + + {isModalOpen && } + + ); +} + +export default PriorityCta; + +const BtnDim = styled.div` + display: flex; + position: fixed; + bottom: 0; + gap: 1rem; + align-items: end; + justify-content: center; + z-index: 2; + + margin-top: 3rem; + background: ${({ theme }) => theme.colors.dim_gradient}; + padding-bottom: 2.9rem; + + width: 100%; + height: 16.4rem; + + pointer-events: none; +`; diff --git a/src/pages/selectSchedule/components/selectPriority/PriorityDropdown.tsx b/src/pages/selectSchedule/components/selectPriority/PriorityDropdown.tsx new file mode 100644 index 00000000..2631b4e2 --- /dev/null +++ b/src/pages/selectSchedule/components/selectPriority/PriorityDropdown.tsx @@ -0,0 +1,270 @@ +import { useState } from 'react'; + +import Text from 'components/atomComponents/Text'; +import { Circle1Ic, Circle2Ic, Circle3Ic, DropDownIc, DropUpIc } from 'components/Icon/icon'; +import { addMinutes } from 'components/timetableComponents/utils'; +import { + SelectedSlotType, + SelectSlotType, + useSelectContext, +} from 'pages/selectSchedule/contexts/useSelectContext'; +import styled from 'styled-components/macro'; +import { theme } from 'styles/theme'; + +/** + * + * @desc 기존의 우선순위 Dropdown 컴포넌트를 그대로 가져와서, 따로 리팩토링 없이 새로운 시간표 컴포넌트에 맞게 적용되도록 수정한 컴포넌트입니다. + */ + +function PriorityDropdown() { + const { selectedSlots, setSelectedSlots } = useSelectContext(); + const [timeSelect, setTimeSelect] = useState([false, false, false]); + + const formatDate = (date: string) => { + const [month, day, dayOfWeek] = date.split('/'); + return `${month}/${day}(${dayOfWeek})`; + }; + + let defaultInput1 = ''; + let defaultInput2 = ''; + let defaultInput3 = ''; + for (const key in selectedSlots) { + const item = selectedSlots[key]; + const date = formatDate(item.date); + const endSlot = addMinutes(item.endSlot, 30); + if (item.priority === 1) { + defaultInput1 = `${date} ${item.startSlot}~${endSlot}`; + } else if (item.priority === 2) { + defaultInput2 = `${date} ${item.startSlot}~${endSlot}`; + } else if (item.priority === 3) { + defaultInput3 = `${date} ${item.startSlot}~${endSlot}`; + } + } + const [input_, setInput] = useState([defaultInput1, defaultInput2, defaultInput3]); + const handleDropdown = (i: number) => { + if (!timeSelect[i]) { + setTimeSelect((prevState) => { + const updatedTimeSelect = prevState.map((value, index) => index === i); + return updatedTimeSelect; + }); + } else { + setTimeSelect((prevState) => { + const updatedTimeSelect = [...prevState]; + updatedTimeSelect[i] = !updatedTimeSelect[i]; + return updatedTimeSelect; + }); + } + }; + + const handlePriority = (i: number, item: SelectSlotType, itemKey: string) => { + let temp: 0 | 1 | 2 | 3 = 0; + switch (i) { + case 0: + temp = 1; + break; + case 1: + temp = 2; + break; + case 2: + temp = 3; + break; + default: + temp = 0; + break; + } + + setSelectedSlots((prev: SelectedSlotType) => { + const updatedSelectedSlots = Object.entries(prev).map(([_, value]) => { + if (value.priority === temp) { + return { ...value, priority: 0 }; + } + return value; + }); + return updatedSelectedSlots; + }); + + setSelectedSlots((prev: SelectedSlotType) => { + const updatedSelectedSlots = Object.entries(prev).map(([key, value]) => { + if (key === itemKey) { + return { ...value, priority: temp }; + } + return value; + }); + return updatedSelectedSlots; + }); + + setInput((prev) => { + const updatedInput = [...prev]; + const endSlot = addMinutes(item.endSlot, 30); + const date = formatDate(item.date); + if (i === 0) { + updatedInput[i] = `${date} ${item.startSlot}~${endSlot}`; + } else if (i === 1) { + updatedInput[i] = `${date} ${item.startSlot}~${endSlot}`; + } else if (i === 2) { + updatedInput[i] = `${date} ${item.startSlot}~${endSlot}`; + } else { + updatedInput[i] = 'error'; + } + return updatedInput; + }); + handleDropdown(i); + }; + + return ( + + {Object.entries(selectedSlots).map(([key, _], i) => { + return i < 3 ? ( + + + + + {`${i + 1}`}순위 + + + {i === 0 ? ( + + ) : i === 1 ? ( + + ) : i === 2 ? ( + + ) : ( +
+ )} + + + handleDropdown(i)} + value={input_[i]} + /> + + {timeSelect[i] ? ( + + {' '} + + ) : ( + + + + )} + + {timeSelect[i] && ( + + {Object.entries(selectedSlots).map( + ([key, value]) => + !value.priority && ( + handlePriority(i, value, key)}> + + {formatDate(value.date)} {value.startSlot}~{addMinutes( + value.endSlot, + 30, + )} + + + ), + )} + + )} + + + ) : ( +
+ ); + })} + + ); +} +const PriorityDropdownWrapper = styled.div` + display: flex; + + flex-direction: column; + gap: 1.2rem; + justify-content: start; + + margin-top: 3rem; + margin-bottom: 7.5rem; + width: 100%; + height: 18rem; +`; + +const PriorityDropdownSection = styled.div` + display: flex; + gap: 1.3rem; + justify-content: space-between; + width: 100%; + height: 5.2rem; +`; +const CircleWrapper = styled.div` + position: relative; + width: 4.8rem; + height: 4.8rem; +`; + +const TextWrapper = styled.div` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; +const Circle1Icon = styled(Circle1Ic)``; +const Circle2Icon = styled(Circle2Ic)``; +const Circle3Icon = styled(Circle3Ic)``; + +const InputWrapper = styled.div` + position: relative; +`; +const TimeInput = styled.input<{ $drop: boolean }>` + appearance: none; + outline: none; + border: none; + border-radius: 0.8rem; + border-bottom-left-radius: ${(props) => (props.$drop ? '0rem' : '0.8rem')}; + border-bottom-right-radius: ${(props) => (props.$drop ? '0rem' : '0.8rem')}; + + background-color: ${({ theme }) => theme.colors.grey7}; + ${({ theme }) => theme.fonts.button1}; + cursor: pointer; + padding-left: 2rem; + width: 27.4rem; + height: 5.2rem; + color: ${({ theme }) => theme.colors.white}; +`; +const DropDownIconWrapper = styled.div` + position: absolute; + top: 36%; + right: 1rem; + cursor: pointer; +`; +const DropdownWrapper = styled.div` + position: absolute; + top: 5.2rem; + z-index: 2; + border-radius: 0rem 0rem 0.8rem 0.8rem; + background-color: ${({ theme }) => theme.colors.grey6}; + width: 27.4rem; + height: fit-content; + max-height: 15.6rem; + + overflow-x: hidden; + overflow-y: auto; +`; + +const DropDownItem = styled.div` + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; + border: 1px solid ${({ theme }) => theme.colors.grey7}; + background-color: ${({ theme }) => theme.colors.grey6}; + + cursor: pointer; + + width: 27.4rem; + height: 5.2rem; +`; + +export default PriorityDropdown; diff --git a/src/pages/selectSchedule/components/selectTimeSlot/SelectionColumn.tsx b/src/pages/selectSchedule/components/selectTimeSlot/SelectionColumn.tsx new file mode 100644 index 00000000..17f97f40 --- /dev/null +++ b/src/pages/selectSchedule/components/selectTimeSlot/SelectionColumn.tsx @@ -0,0 +1,51 @@ +import { ColumnStructure } from 'components/timetableComponents/types'; +import { useSelectContext } from 'pages/selectSchedule/contexts/useSelectContext'; +import { theme } from 'styles/theme'; + +import useSlotSeletion from './hooks/useSlotSelection'; +import Slot from '../../../../components/timetableComponents/parts/Slot'; + +function SelectionColumn({ date, timeSlots }: ColumnStructure) { + const { selectedSlots } = useSelectContext(); + const selectedSlotsPerDate = Object.entries(selectedSlots).filter( + ([, slot]) => slot.date === date, + ); + + const { startSlot, onClickSlot } = useSlotSeletion(); + + const getTimeSlotStyle = (slotId: string, selectedEntryId?: number) => { + const isStartSlot = slotId === startSlot; + const isSelectedSlot = selectedEntryId !== undefined; + + return ` + cursor:pointer; + ${isStartSlot && `border: 1px dashed ${theme.colors.main5}`}; + ${ + isSelectedSlot ? `background-color: ${theme.colors.main1}` : `background-color: transparent` + }; + `; + }; + + return ( + <> + {timeSlots.map((timeSlot) => { + const belongingEntry = selectedSlotsPerDate.find( + ([, { startSlot, endSlot }]) => timeSlot >= startSlot && timeSlot <= endSlot, + ); + const selectedEntryId = belongingEntry ? parseInt(belongingEntry[0]) : undefined; + const slotId = `${date}/${timeSlot}`; + + return ( + onClickSlot(slotId, selectedEntryId)} + /> + ); + })} + + ); +} + +export default SelectionColumn; diff --git a/src/pages/selectSchedule/components/selectTimeSlot/TimeSlotCta.tsx b/src/pages/selectSchedule/components/selectTimeSlot/TimeSlotCta.tsx new file mode 100644 index 00000000..d021e1aa --- /dev/null +++ b/src/pages/selectSchedule/components/selectTimeSlot/TimeSlotCta.tsx @@ -0,0 +1,42 @@ +import Button from 'components/atomComponents/Button'; +import Text from 'components/atomComponents/Text'; +import { useScheduleStepContext } from 'pages/selectSchedule/contexts/useScheduleStepContext'; +import { useSelectContext } from 'pages/selectSchedule/contexts/useSelectContext'; +import styled from 'styled-components'; + +function TimeSlotCta() { + const { setScheduleStep } = useScheduleStepContext(); + const { selectedSlots } = useSelectContext(); + const isValidSelection = Object.keys(selectedSlots).length !== 0; + return ( + + + + ); +} + +export default TimeSlotCta; + +const BtnDim = styled.div` + display: flex; + position: fixed; + bottom: 0; + align-items: end; + justify-content: center; + + margin-top: 3rem; + background: ${({ theme }) => theme.colors.dim_gradient}; + padding-bottom: 2.9rem; + + width: 100%; + height: 16.4rem; + + pointer-events: none; +`; diff --git a/src/components/timetableComponents/hooks/useSlotSelection.ts b/src/pages/selectSchedule/components/selectTimeSlot/hooks/useSlotSelection.ts similarity index 80% rename from src/components/timetableComponents/hooks/useSlotSelection.ts rename to src/pages/selectSchedule/components/selectTimeSlot/hooks/useSlotSelection.ts index 6b171a69..abfadf2a 100644 --- a/src/components/timetableComponents/hooks/useSlotSelection.ts +++ b/src/pages/selectSchedule/components/selectTimeSlot/hooks/useSlotSelection.ts @@ -1,7 +1,7 @@ -import { useTimetableContext } from '../context' +import { useSelectContext } from 'pages/selectSchedule/contexts/useSelectContext'; const useSlotSeletion = () => { - const {startSlot, setStartSlot, selectedSlots, setSelectedSlots} = useTimetableContext(); + const {startSlot, setStartSlot, selectedSlots, setSelectedSlots} = useSelectContext(); const handleSelectSlot = (targetSlot: string) => { setStartSlot(targetSlot); @@ -10,16 +10,19 @@ const useSlotSeletion = () => { const handleCompleteSlot = (targetSlot: string) => { const dateOfStartSlot = startSlot?.substring(0, startSlot.lastIndexOf('/')); const dateOfTargetSlot = targetSlot.substring(0, targetSlot.lastIndexOf('/')) - if (dateOfStartSlot === dateOfTargetSlot){ + if (startSlot && dateOfStartSlot === dateOfTargetSlot){ const newSelectedSlot = { date:dateOfStartSlot, startSlot:startSlot?.substring(startSlot.lastIndexOf('/')+1), - endSlot:targetSlot.substring(targetSlot.lastIndexOf('/')+1) + endSlot:targetSlot.substring(targetSlot.lastIndexOf('/')+1), + priority:0, } const keys = Object.keys(selectedSlots).map(Number) const newKey = keys.length ? Math.max(...keys) + 1 : 0; - setSelectedSlots({...selectedSlots, [newKey]:newSelectedSlot}) + const newSelectedSlots = {...selectedSlots}; + newSelectedSlots[newKey] = newSelectedSlot; + setSelectedSlots(newSelectedSlots) } setStartSlot(undefined); } diff --git a/src/pages/selectSchedule/contexts/useScheduleStepContext.ts b/src/pages/selectSchedule/contexts/useScheduleStepContext.ts new file mode 100644 index 00000000..9488eaa4 --- /dev/null +++ b/src/pages/selectSchedule/contexts/useScheduleStepContext.ts @@ -0,0 +1,21 @@ +import { Dispatch, SetStateAction, createContext, useContext } from 'react'; + +import { ScheduleStepType } from '../types'; + +interface ScheduleStepContextType { + scheduleStep: ScheduleStepType; + setScheduleStep: Dispatch>; +} + +export const ScheduleStepContext = createContext({ + scheduleStep: 'selectTimeSlot', + setScheduleStep: () => undefined, +}); + +export function useScheduleStepContext() { + const context = useContext(ScheduleStepContext); + if (context == null) { + throw new Error('ScheduleStepContext Error'); + } + return context; +} diff --git a/src/pages/selectSchedule/contexts/useSelectContext.ts b/src/pages/selectSchedule/contexts/useSelectContext.ts new file mode 100644 index 00000000..2ab073d0 --- /dev/null +++ b/src/pages/selectSchedule/contexts/useSelectContext.ts @@ -0,0 +1,34 @@ +import { Dispatch, SetStateAction, createContext, useContext } from 'react'; + +export interface SelectSlotType { + date: string; + startSlot: string; + endSlot: string; + priority: number; +} + +export interface SelectedSlotType { + [key: number]: SelectSlotType; +} + +interface SelectContextType { + startSlot: string | undefined; + setStartSlot: Dispatch>; + selectedSlots: SelectedSlotType; + setSelectedSlots: Dispatch>; +} + +export const SelectContext = createContext({ + startSlot: undefined, + setStartSlot: () => undefined, + selectedSlots: {}, + setSelectedSlots: () => undefined, +}); + +export function useSelectContext() { + const context = useContext(SelectContext); + if (context == null) { + throw new Error('SelectContext Error'); + } + return context; +} diff --git a/src/pages/selectSchedule/types.ts b/src/pages/selectSchedule/types.ts new file mode 100644 index 00000000..54ceb704 --- /dev/null +++ b/src/pages/selectSchedule/types.ts @@ -0,0 +1,14 @@ +import { ReactNode } from 'react'; + +import { ColumnStructure } from 'components/timetableComponents/types'; + +export type ScheduleStepType = 'selectTimeSlot' | 'selectPriority'; +export type StepSlotsType = { [key in ScheduleStepType]: (props: ColumnStructure) => ReactNode }; +export type StepbottomItemsType = { [key in ScheduleStepType]: ReactNode }; + +export interface TitlesType { + [key: string]: { + main: string; + sub?: string; + }; +} diff --git a/src/pages/selectSchedule/utils.ts b/src/pages/selectSchedule/utils.ts new file mode 100644 index 00000000..f05805a3 --- /dev/null +++ b/src/pages/selectSchedule/utils.ts @@ -0,0 +1,95 @@ +import { addMinutes } from 'components/timetableComponents/utils'; + +import { SelectedSlotType } from './contexts/useSelectContext'; +import { TitlesType } from './types'; + +export const DURATION = { + HALF: '30분', + HOUR: '1시간', + HOUR_HALF: '1시간 30분', + TWO_HOUR: '2시간', + TWO_HOUR_HALF: '2시간 30분', + THREE_HOUR: '3시간', +} as const; + +export const PLACE = { + ONLINE: '온라인', + OFFLINE: '오프라인', + UNDEFINED: undefined, +} as const; + +export const TITLES: TitlesType = { + selectTimeSlot: { + main: '가능한 시간대를 등록해주세요', + sub: '시작시간과 종료시간을 터치하여 블럭을 생성해주세요', + }, + selectPriority: { + main: '우선순위를 입력해주세요', + }, +} as const; + +/** + * + * @desc 선택된 슬롯들의 우선순위를 0으로 초기화하는 함수 + */ +export const resetPriorities = (selectedSlots: SelectedSlotType): SelectedSlotType => { + const updatedSlots: SelectedSlotType = {}; + + for (const key in selectedSlots) { + if (typeof selectedSlots[key] === 'object') { + updatedSlots[key] = { + ...selectedSlots[key], + priority: 0, + }; + } + } + + return updatedSlots; +}; + +/** + * + * @desc 방장 시간표 입력 POST를 위한 형식을 맞추는 함수 + */ +export const formatHostScheduleScheme = (selectedSlots: SelectedSlotType) => { + const availableTimes = Object.keys(selectedSlots).map((key) => { + const slot = selectedSlots[parseInt(key)]; + const [month, day, dayOfWeek] = slot.date.split('/'); + + return { + id: key, + month: month.padStart(2, '0'), + day: day.padStart(2, '0'), + dayOfWeek: dayOfWeek, + startTime: slot.startSlot, + endTime: addMinutes(slot.endSlot, 30), + priority: slot.priority, + }; + }); + return availableTimes; +}; + +/** + * + * @desc 멤버 시간표 입력 POST를 위한 형식을 맞추는 함수 + */ +export const formatMemberScheduleScheme = (selectedSlots: SelectedSlotType, userName: string) => { + const availableTimes = Object.keys(selectedSlots).map((key) => { + const slot = selectedSlots[parseInt(key)]; + const [month, day, dayOfWeek] = slot.date.split('/'); + + return { + id: key, + month: month.padStart(2, '0'), + day: day.padStart(2, '0'), + dayOfWeek: dayOfWeek, + startTime: slot.startSlot, + endTime: addMinutes(slot.endSlot, 30), + priority: slot.priority, + }; + }); + return { + name: userName, + availableTimes: availableTimes, + }; +}; diff --git a/src/utils/apis/bestMeetTimeApi.ts b/src/utils/apis/bestMeetTimeApi.ts deleted file mode 100644 index a6ea58c7..00000000 --- a/src/utils/apis/bestMeetTimeApi.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { BestMeetTimeResponse, BestMeetTimeRequest } from 'src/types/bestMeetTimeType'; - -// import { client } from './axios'; - -// export const BestMeetTimeApi = (BestMeetTimeRequest: BestMeetTimeRequest) => { -// return client.post(`/meeting/${meetingId}/confirm`, BestMeetTimeRequest, { -// headers: { -// Authorization: `[Bearer] ${token}`, -// }, -// }); -// }; diff --git a/src/utils/apis/availbleScheduleOptionApi.ts b/src/utils/apis/legacy/availbleScheduleOptionApi.ts similarity index 89% rename from src/utils/apis/availbleScheduleOptionApi.ts rename to src/utils/apis/legacy/availbleScheduleOptionApi.ts index 2f424339..b10400da 100644 --- a/src/utils/apis/availbleScheduleOptionApi.ts +++ b/src/utils/apis/legacy/availbleScheduleOptionApi.ts @@ -1,6 +1,5 @@ import { AvailableScheduleOptionResponse } from 'src/types/availbleScheduleType'; - -import { client } from './axios'; +import { client } from '../axios'; /** 가능 시간 입력 선택지 조회 api */ export const availableScheduleOptionApi = (meetingId?: string) => { diff --git a/src/utils/apis/createHostAvailableSchedule.ts b/src/utils/apis/legacy/createHostAvailableSchedule.ts similarity index 88% rename from src/utils/apis/createHostAvailableSchedule.ts rename to src/utils/apis/legacy/createHostAvailableSchedule.ts index c5915f09..0cc98699 100644 --- a/src/utils/apis/createHostAvailableSchedule.ts +++ b/src/utils/apis/legacy/createHostAvailableSchedule.ts @@ -1,12 +1,10 @@ -import { AxiosResponse } from 'axios'; import { HostAvailableSchduleRequestType, HostAvailableScheduleResponseType, - UserAvailableScheduleResponseType, UserAvailableScheduleRequestType, + UserAvailableScheduleResponseType, } from 'src/types/createAvailableSchduleType'; - -import { authClient, client } from './axios'; +import { authClient, client } from '../axios'; export const hostAvailableApi = ( meetingId: string, diff --git a/src/utils/apis/createMeetingApi.ts b/src/utils/apis/legacy/createMeetingApi.ts similarity index 88% rename from src/utils/apis/createMeetingApi.ts rename to src/utils/apis/legacy/createMeetingApi.ts index ce62c47d..54f43623 100644 --- a/src/utils/apis/createMeetingApi.ts +++ b/src/utils/apis/legacy/createMeetingApi.ts @@ -1,6 +1,6 @@ import { CreateMeetingRequest, CreateMeetingResponse } from 'src/types/createMeetingType'; -import { client } from './axios'; +import { client } from '../axios'; export const createMeetingApi = (CreateMeetingRequest: CreateMeetingRequest) => { return client.post(`/meeting`, CreateMeetingRequest); diff --git a/src/utils/apis/cueCardAPI.ts b/src/utils/apis/legacy/cueCardAPI.ts similarity index 75% rename from src/utils/apis/cueCardAPI.ts rename to src/utils/apis/legacy/cueCardAPI.ts index f5f1d85f..fd7517ab 100644 --- a/src/utils/apis/cueCardAPI.ts +++ b/src/utils/apis/legacy/cueCardAPI.ts @@ -1,4 +1,4 @@ -import { client } from './axios'; +import { client } from '../axios'; export const cueCardApi = (meetingId: string) => { return client.get(`/meeting/${meetingId}/card`); diff --git a/src/utils/apis/overallScheduleApi.ts b/src/utils/apis/legacy/overallScheduleApi.ts similarity index 92% rename from src/utils/apis/overallScheduleApi.ts rename to src/utils/apis/legacy/overallScheduleApi.ts index 9cff94bd..50b40c6d 100644 --- a/src/utils/apis/overallScheduleApi.ts +++ b/src/utils/apis/legacy/overallScheduleApi.ts @@ -1,8 +1,8 @@ +import { authClient, client } from '../axios'; + import { AvailableScheduleOptionResponse } from 'src/types/availbleScheduleType'; import { OverallScheduleResponse } from 'src/types/overallScheduleType'; -import { authClient, client } from './axios'; - /** 가능 시간 입력 선택지 조회 api */ export const availbleScheduleOptionApi = (meetingId?: string) => { return client.get(`/meeting/${meetingId}/schedule`); diff --git a/src/utils/apis/useGetOverallSchedule.ts b/src/utils/apis/useGetOverallSchedule.ts new file mode 100644 index 00000000..1e501114 --- /dev/null +++ b/src/utils/apis/useGetOverallSchedule.ts @@ -0,0 +1,54 @@ +import { useQuery } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { useNavigate } from 'react-router-dom'; + +import { authClient } from './axios'; + +interface Date { + month: string; + day: string; + dayOfWeek: string; +} + +export interface TimeSlot { + time: string; + userNames: string[]; + colorLevel: number; +} + +export interface AvailableDateTime extends Date { + timeSlots: TimeSlot[]; +} + +export interface getOverallScheduleResponse { + data: { + memberCount: number; + totalUserNames: string[]; + availableDateTimes: AvailableDateTime[]; + }; +} + +const getOverallSchedule = async (meetingId: string) => { + try { + const res = await authClient.get(`/meeting/${meetingId}/timetable`); + return res.data.data; + } catch (err) { + if (isAxiosError(err) && err.response) { + throw new Error(err.response.data.message); + } + } +}; + +export const useGetOverallSchedule = (meetingId?: string) => { + const navigate = useNavigate(); + if (meetingId === undefined) { + navigate('/error'); + throw new Error('잘못된 회의 아이디입니다.'); + } + const { data, isLoading } = useQuery({ + queryKey: ['getOverallSchedule', meetingId], + queryFn: () => getOverallSchedule(meetingId), + }); + + return { data, isLoading }; +}; diff --git a/src/utils/apis/useGetTimetable.ts b/src/utils/apis/useGetTimetable.ts new file mode 100644 index 00000000..eb6ae73e --- /dev/null +++ b/src/utils/apis/useGetTimetable.ts @@ -0,0 +1,51 @@ +import { useQuery } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import { DURATION, PLACE } from 'pages/selectSchedule/utils'; +import { useNavigate } from 'react-router-dom'; + +import { client } from './axios'; + +interface Date { + month: string; + day: string; + dayOfWeek: string; +} +interface TimeSlot { + startTime: string; + endTime: string; +} + +interface getTimetableResponse { + data: { + duration: keyof typeof DURATION; + place: keyof typeof PLACE; + placeDetail: string; + availableDates: Date[]; + preferTimes: TimeSlot[]; + }; +} + +const getTimetable = async (meetingId: string) => { + try { + const res = await client.get(`/meeting/${meetingId}/schedule`); + return res.data.data; + } catch (err) { + if (isAxiosError(err) && err.response) { + throw new Error(err.response.data.message); + } + } +}; + +export const useGetTimetable = (meetingId?: string) => { + const navigate = useNavigate(); + if (meetingId === undefined) { + navigate('/error'); + throw new Error('잘못된 회의 아이디입니다.'); + } + const { data, isLoading } = useQuery({ + queryKey: ['getTimetable', meetingId], + queryFn: () => getTimetable(meetingId), + }); + + return { data, isLoading }; +}; diff --git a/yarn.lock b/yarn.lock index 07569bdb..35168db7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1536,6 +1536,30 @@ "@svgr/hast-util-to-babel-ast" "^7.0.0" svg-parser "^2.0.4" +"@tanstack/query-core@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.45.0.tgz#47a662d311c2588867341238960ec21dc7f0714e" + integrity sha512-RVfIZQmFUTdjhSAAblvueimfngYyfN6HlwaJUPK71PKd7yi43Vs1S/rdimmZedPWX/WGppcq/U1HOj7O7FwYxw== + +"@tanstack/query-devtools@5.37.1": + version "5.37.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.37.1.tgz#8dcfa1488b4f2e353be7eede6691b0ad9197183b" + integrity sha512-XcG4IIHIv0YQKrexTqo2zogQWR1Sz672tX2KsfE9kzB+9zhx44vRKH5si4WDILE1PIWQpStFs/NnrDQrBAUQpg== + +"@tanstack/react-query-devtools@^5.45.1": + version "5.45.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.45.1.tgz#bea7ba0ffd509f0930237c2df7feba9209f76aa6" + integrity sha512-4mrbk1g5jqlqh0pifZNsKzy7FtgeqgwzMICL4d6IJGayrrcrKq9K4N/OzRNbgRWrTn6YTY63qcAcKo+NJU2QMw== + dependencies: + "@tanstack/query-devtools" "5.37.1" + +"@tanstack/react-query@^5.45.1": + version "5.45.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.45.1.tgz#a0ac6bb89b4a2c2b0251f6647a0a370d86f05347" + integrity sha512-mYYfJujKg2kxmkRRjA6nn4YKG3ITsKuH22f1kteJ5IuVQqgKUgbaSQfYwVP0gBS05mhwxO03HVpD0t7BMN7WOA== + dependencies: + "@tanstack/query-core" "5.45.0" + "@types/axios@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"