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 2c3b3a27..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 { BrowserRouter, Route, Routes } from 'react-router-dom'; const Router = () => { return ( @@ -18,8 +19,6 @@ const Router = () => { } /> } /> } /> - {/* } /> - } /> */} } /> (undefined); - const [selectedSlots, setSelectedSlots] = useState({}); - 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 })}; + return ( + + {children({ date: dateKey, timeSlots })} + + ); })} - {emptyDates && emptyDates.map((value) => )} -
-
+ {emptyDates && emptyDates.map((value) => )} + +
{bottomItem} -
+ ); } @@ -54,26 +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 Column = styled.div` +const ColumnWrapper = styled.div` display: flex; flex-direction: column; border-right: 1px solid ${({ theme }) => theme.colors.grey7}; `; -const EmptyColumn = styled.div` +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 d6eb8a77..00000000 --- a/src/components/timetableComponents/context.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createContext, useContext } from 'react'; - -export interface SlotInfoType { - date: string; - startSlot: string; - endSlot: string; - priority: 0 | 1 | 2 | 3; -} -export interface SelectedSlotType { - [key: number]: SlotInfoType; -} - -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/types.ts b/src/components/timetableComponents/types.ts index a3242267..bf73e819 100644 --- a/src/components/timetableComponents/types.ts +++ b/src/components/timetableComponents/types.ts @@ -15,3 +15,8 @@ export interface DateType { 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 e89ccf60..d3f6d7bb 100644 --- a/src/components/timetableComponents/utils.ts +++ b/src/components/timetableComponents/utils.ts @@ -1,4 +1,4 @@ -import { SlotType } from 'pages/selectSchedule/SelectSchedule'; +import { SlotType } from './types'; /** * * @desc 문자열로 된 time('HH:MM')에 minutes을 더하는 함수 @@ -16,15 +16,12 @@ export const addMinutes = (time: string, minutes: number) => { * @desc 시작 시간(startTime)과 종료 시간(endTime) 사이에서 30분 간격으로 시간 슬롯을 생성하여 반환하는 함수 */ -export const getAvailableTimes = (times: SlotType) => { - 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/createMeeting/CreateMeeting.tsx b/src/pages/createMeeting/CreateMeeting.tsx index bcde5d43..b99eeccd 100644 --- a/src/pages/createMeeting/CreateMeeting.tsx +++ b/src/pages/createMeeting/CreateMeeting.tsx @@ -6,6 +6,7 @@ import ReturnBodyComponent from 'pages/createMeeting/components/ReturnBodyCompon import ReturnTitleComponent from 'pages/createMeeting/components/ReturnTitleComponent'; import { funnelStep } from './data/meetingInfoData'; import styled from 'styled-components/macro'; +import { useGetTimetable } from 'utils/apis/useGetTimetable'; const initialMeetingInfo: MeetingInfo = { title: '', @@ -23,6 +24,7 @@ function CreateMeeting() { const [step, setStep] = useState(0); const [meetingInfo, setMeetingInfo] = useState(initialMeetingInfo); const currentStep = funnelStep[step]; + return ( <> diff --git a/src/pages/createMeeting/components/useFunnel/SetAdditionalInfo.tsx b/src/pages/createMeeting/components/useFunnel/SetAdditionalInfo.tsx index fc86d9e1..79179077 100644 --- a/src/pages/createMeeting/components/useFunnel/SetAdditionalInfo.tsx +++ b/src/pages/createMeeting/components/useFunnel/SetAdditionalInfo.tsx @@ -1,13 +1,13 @@ -import React from 'react'; +import { FunnelProps, MeetingInfo } from 'pages/createMeeting/types/useFunnelInterface'; -import { isAxiosError } from 'axios'; import Button from 'components/atomComponents/Button'; +import React from 'react'; import Text from 'components/atomComponents/Text'; import TextAreaInput from 'components/atomComponents/TextAreaInput'; -import { MeetingInfo, FunnelProps } from 'pages/createMeeting/types/useFunnelInterface'; -import { useNavigate } from 'react-router-dom'; +import { createMeetingApi } from 'utils/apis/legacy/createMeetingApi'; +import { isAxiosError } from 'axios'; import styled from 'styled-components/macro'; -import { createMeetingApi } from 'utils/apis/createMeetingApi'; +import { useNavigate } from 'react-router-dom'; function SetAdditionalInfo({ meetingInfo, setMeetingInfo, setStep }: FunnelProps) { const navigate = useNavigate(); diff --git a/src/pages/legacy/overallSchedule/OverallSchedule.tsx b/src/pages/legacy/overallSchedule/OverallSchedule.tsx new file mode 100644 index 00000000..e19b2805 --- /dev/null +++ b/src/pages/legacy/overallSchedule/OverallSchedule.tsx @@ -0,0 +1,165 @@ +import React, { useEffect, useState } from 'react'; +import { availableDatesAtom, preferTimesAtom, timeSlotUserNameAtom } from 'atoms/atom'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import LoadingPage from 'pages/errorLoading/LoadingPage'; +import { OverallScheduleData } from 'src/types/overallScheduleType'; +import Text from 'components/atomComponents/Text'; +import TimeTable from './components/TimeTable'; +import { availableScheduleOptionApi } from 'utils/apis/legacy/availbleScheduleOptionApi'; +import { getFormattedAvailableDateTimes } from './utils/getFormattedAvailableDateTimes'; +import { overallScheduleApi } from 'utils/apis/legacy/overallScheduleApi'; +import { styled } from 'styled-components'; +import { theme } from 'styles/theme'; +import { useParams } from 'react-router-dom'; + +const 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 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 3e7a99d0..484f8e8e 100644 --- a/src/pages/legacy/selectSchedule/SelectModal.tsx +++ b/src/pages/legacy/selectSchedule/SelectModal.tsx @@ -1,28 +1,27 @@ -import { hostAvailableApi, userAvailableApi } from 'utils/apis/createHostAvailableSchedule'; -import { scheduleAtom, userNameAtom } from 'atoms/atom'; -import { transformHostScheduleType, transformUserScheduleType } from './utils/changeApiReq'; -import { useNavigate, useParams } from 'react-router'; -import { useRecoilState, useRecoilValue } from 'recoil'; - -import { ExitIc } from 'components/Icon/icon'; -import { ScheduleStates } from './types/Schedule'; -import Text from 'components/atomComponents/Text'; +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 { useRecoilValue } from 'recoil'; import styled from 'styled-components/macro'; import { theme } from 'styles/theme'; +import { hostAvailableApi, userAvailableApi } from 'utils/apis/legacy/createHostAvailableSchedule'; interface ModalProps { 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 { @@ -162,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 e0435132..6e8dadc1 100644 --- a/src/pages/selectSchedule/SelectSchedule.tsx +++ b/src/pages/selectSchedule/SelectSchedule.tsx @@ -2,88 +2,50 @@ import { useState } from 'react'; import Header from 'components/moleculesComponents/Header'; import TitleComponents from 'components/moleculesComponents/TitleComponents'; -import { DateType } from 'components/timetableComponents/types'; import { getAvailableTimes } from 'components/timetableComponents/utils'; +import { useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { useGetTimetable } from 'utils/apis/useGetTimetable'; import Description from './components/Description'; import SelectScheduleTable from './components/SelectScheduleTable'; -import { ScheduleStepContext } from './context'; +import { ScheduleStepContext } from './contexts/useScheduleStepContext'; import { ScheduleStepType } from './types'; - -/***** api 연결 후 지울 것*****/ - -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', -}; - -const timeSlots = getAvailableTimes(preferTimes); -const duration = 'HALF'; -const place = 'OFFLINE'; -const placeDetail = undefined; -/***** api 연결 후 지울 것*****/ +import { TITLES } from './utils'; function SelectSchedule() { const [scheduleStep, setScheduleStep] = useState('selectTimeSlot'); + const { meetingId } = useParams(); + const { data, isLoading } = useGetTimetable(meetingId); - interface TitlesType { - [key: string]: { - main: string; - sub?: string; - }; - } - const titles: TitlesType = { - selectTimeSlot: { - main: '가능한 시간대를 등록해주세요', - sub: '시작시간과 종료시간을 터치하여 블럭을 생성해주세요', - }, - selectPriority: { - main: '우선순위를 입력해주세요', - }, - }; + // 시간대 선택 단계가 없어질 것을 고려하여 상수값을 설정해놓음 + const PREFER_TIMES = { startTime: '06:00', endTime: '24:00' }; return (
- {scheduleStep === 'selectTimeSlot' && ( - - )} - - + {!isLoading && + data && ( + <> + {scheduleStep === 'selectTimeSlot' && ( + + )} + + + + )} ); diff --git a/src/pages/selectSchedule/components/Description.tsx b/src/pages/selectSchedule/components/Description.tsx index e87e25fe..34e88e25 100644 --- a/src/pages/selectSchedule/components/Description.tsx +++ b/src/pages/selectSchedule/components/Description.tsx @@ -1,18 +1,18 @@ -import { formatDuration, formatPlace } from '../utils'; +import { DURATION, PLACE } from '../utils'; import Text from 'components/atomComponents/Text'; import styled from 'styled-components'; import { theme } from 'styles/theme'; interface DescriptionProps { - duration: string; - place: string; + duration: keyof typeof DURATION; + place: keyof typeof PLACE; placeDetail?: string; } function Description({ duration: durationOg, place: placeOg, placeDetail }: DescriptionProps) { - const duration = formatDuration(durationOg); - const place = formatPlace(placeOg); + const duration = DURATION[durationOg]; + const place = PLACE[placeOg]; return ( diff --git a/src/pages/selectSchedule/components/SelectScheduleTable.tsx b/src/pages/selectSchedule/components/SelectScheduleTable.tsx index 65492546..4134e8bc 100644 --- a/src/pages/selectSchedule/components/SelectScheduleTable.tsx +++ b/src/pages/selectSchedule/components/SelectScheduleTable.tsx @@ -1,26 +1,32 @@ +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 PrioritySlots from './selectPriority/PrioritySlots'; -import SelectionSlots from './selectTimeSlot/SelectionSlots'; +import SelectionColumn from './selectTimeSlot/SelectionColumn'; import TimeSlotCta from './selectTimeSlot/TimeSlotCta'; -import { useScheduleStepContext } from '../context'; +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 stepSlots: StepSlotsType = { + const stepColumns: StepSlotsType = { selectTimeSlot: ({ date, timeSlots }: ColumnStructure) => ( - + ), selectPriority: ({ date, timeSlots }: ColumnStructure) => ( - + ), }; - const stepSlot = stepSlots[scheduleStep]; + const stepColumn = stepColumns[scheduleStep]; const bottomItems: StepbottomItemsType = { selectTimeSlot: , @@ -34,9 +40,18 @@ function SelectScheduleTable({ timeSlots, availableDates }: TimetableStructure) const bottomItem = bottomItems[scheduleStep]; return ( - - {stepSlot} - + + + {stepColumn} + + ); } diff --git a/src/pages/selectSchedule/components/selectPriority/PrioritySlots.tsx b/src/pages/selectSchedule/components/selectPriority/PriorityColumn.tsx similarity index 81% rename from src/pages/selectSchedule/components/selectPriority/PrioritySlots.tsx rename to src/pages/selectSchedule/components/selectPriority/PriorityColumn.tsx index 23187136..7c30d2b4 100644 --- a/src/pages/selectSchedule/components/selectPriority/PrioritySlots.tsx +++ b/src/pages/selectSchedule/components/selectPriority/PriorityColumn.tsx @@ -1,16 +1,17 @@ -import { ColumnStructure } from 'components/timetableComponents/types'; -import Slot from '../../../../components/timetableComponents/parts/Slot'; 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 { useTimetableContext } from '../../../../components/timetableComponents/context'; -function PrioritySlots({ date, timeSlots }: ColumnStructure) { - const { selectedSlots } = useTimetableContext(); +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 getPrioritySlotStyle = (selectedEntryId?: number, priority?: number) => { + const getPriorityColumntyle = (selectedEntryId?: number, priority?: number) => { const isSelectedSlot = selectedEntryId !== undefined; const slotColor = priority === 1 @@ -49,7 +50,7 @@ function PrioritySlots({ date, timeSlots }: ColumnStructure) { {isFirstSlot && priority !== 0 ? priority : ''} @@ -61,4 +62,4 @@ function PrioritySlots({ date, timeSlots }: ColumnStructure) { ); } -export default PrioritySlots; +export default PriorityColumn; diff --git a/src/pages/selectSchedule/components/selectPriority/PriorityDropdown.tsx b/src/pages/selectSchedule/components/selectPriority/PriorityDropdown.tsx index 007318f2..2631b4e2 100644 --- a/src/pages/selectSchedule/components/selectPriority/PriorityDropdown.tsx +++ b/src/pages/selectSchedule/components/selectPriority/PriorityDropdown.tsx @@ -2,12 +2,12 @@ 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, - SlotInfoType, - useTimetableContext, -} from 'components/timetableComponents/context'; -import { addMinutes } from 'components/timetableComponents/utils'; + SelectSlotType, + useSelectContext, +} from 'pages/selectSchedule/contexts/useSelectContext'; import styled from 'styled-components/macro'; import { theme } from 'styles/theme'; @@ -17,7 +17,7 @@ import { theme } from 'styles/theme'; */ function PriorityDropdown() { - const { selectedSlots, setSelectedSlots } = useTimetableContext(); + const { selectedSlots, setSelectedSlots } = useSelectContext(); const [timeSelect, setTimeSelect] = useState([false, false, false]); const formatDate = (date: string) => { @@ -56,7 +56,7 @@ function PriorityDropdown() { } }; - const handlePriority = (i: number, item: SlotInfoType, itemKey: string) => { + const handlePriority = (i: number, item: SelectSlotType, itemKey: string) => { let temp: 0 | 1 | 2 | 3 = 0; switch (i) { case 0: @@ -72,8 +72,9 @@ function PriorityDropdown() { temp = 0; break; } + setSelectedSlots((prev: SelectedSlotType) => { - const updatedSelectedSlots = Object.entries(prev).map(([key, value]) => { + const updatedSelectedSlots = Object.entries(prev).map(([_, value]) => { if (value.priority === temp) { return { ...value, priority: 0 }; } diff --git a/src/pages/selectSchedule/components/selectTimeSlot/SelectionSlots.tsx b/src/pages/selectSchedule/components/selectTimeSlot/SelectionColumn.tsx similarity index 85% rename from src/pages/selectSchedule/components/selectTimeSlot/SelectionSlots.tsx rename to src/pages/selectSchedule/components/selectTimeSlot/SelectionColumn.tsx index b49bb142..17f97f40 100644 --- a/src/pages/selectSchedule/components/selectTimeSlot/SelectionSlots.tsx +++ b/src/pages/selectSchedule/components/selectTimeSlot/SelectionColumn.tsx @@ -1,11 +1,12 @@ import { ColumnStructure } from 'components/timetableComponents/types'; -import Slot from '../../../../components/timetableComponents/parts/Slot'; +import { useSelectContext } from 'pages/selectSchedule/contexts/useSelectContext'; import { theme } from 'styles/theme'; + import useSlotSeletion from './hooks/useSlotSelection'; -import { useTimetableContext } from '../../../../components/timetableComponents/context'; +import Slot from '../../../../components/timetableComponents/parts/Slot'; -function SelectionSlots({ date, timeSlots }: ColumnStructure) { - const { selectedSlots } = useTimetableContext(); +function SelectionColumn({ date, timeSlots }: ColumnStructure) { + const { selectedSlots } = useSelectContext(); const selectedSlotsPerDate = Object.entries(selectedSlots).filter( ([, slot]) => slot.date === date, ); @@ -47,4 +48,4 @@ function SelectionSlots({ date, timeSlots }: ColumnStructure) { ); } -export default SelectionSlots; +export default SelectionColumn; diff --git a/src/pages/selectSchedule/components/selectTimeSlot/TimeSlotCta.tsx b/src/pages/selectSchedule/components/selectTimeSlot/TimeSlotCta.tsx index 6656907c..d021e1aa 100644 --- a/src/pages/selectSchedule/components/selectTimeSlot/TimeSlotCta.tsx +++ b/src/pages/selectSchedule/components/selectTimeSlot/TimeSlotCta.tsx @@ -1,12 +1,12 @@ import Button from 'components/atomComponents/Button'; import Text from 'components/atomComponents/Text'; -import { useTimetableContext } from 'components/timetableComponents/context'; -import { useScheduleStepContext } from 'pages/selectSchedule/context'; +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 } = useTimetableContext(); + const { selectedSlots } = useSelectContext(); const isValidSelection = Object.keys(selectedSlots).length !== 0; return ( diff --git a/src/pages/selectSchedule/components/selectTimeSlot/hooks/useSlotSelection.ts b/src/pages/selectSchedule/components/selectTimeSlot/hooks/useSlotSelection.ts index b4810c9e..abfadf2a 100644 --- a/src/pages/selectSchedule/components/selectTimeSlot/hooks/useSlotSelection.ts +++ b/src/pages/selectSchedule/components/selectTimeSlot/hooks/useSlotSelection.ts @@ -1,7 +1,7 @@ -import { useTimetableContext } from '../../../../../components/timetableComponents/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,7 +10,7 @@ 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), @@ -20,7 +20,9 @@ const useSlotSeletion = () => { 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/context.ts b/src/pages/selectSchedule/contexts/useScheduleStepContext.ts similarity index 69% rename from src/pages/selectSchedule/context.ts rename to src/pages/selectSchedule/contexts/useScheduleStepContext.ts index a211ee8d..9488eaa4 100644 --- a/src/pages/selectSchedule/context.ts +++ b/src/pages/selectSchedule/contexts/useScheduleStepContext.ts @@ -1,10 +1,10 @@ -import { createContext, useContext } from 'react'; +import { Dispatch, SetStateAction, createContext, useContext } from 'react'; -import { ScheduleStepType } from './types'; +import { ScheduleStepType } from '../types'; interface ScheduleStepContextType { scheduleStep: ScheduleStepType; - setScheduleStep: (scheduleStep: ScheduleStepType) => void; + setScheduleStep: Dispatch>; } export const ScheduleStepContext = createContext({ 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 index de210574..54ceb704 100644 --- a/src/pages/selectSchedule/types.ts +++ b/src/pages/selectSchedule/types.ts @@ -5,3 +5,10 @@ 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 index 2ecdbfda..f05805a3 100644 --- a/src/pages/selectSchedule/utils.ts +++ b/src/pages/selectSchedule/utils.ts @@ -1,42 +1,32 @@ -import { SelectedSlotType } from 'components/timetableComponents/context'; +import { addMinutes } from 'components/timetableComponents/utils'; -/** - * - * @desc 영어로 표현된 회의 진행 시간을 한글로 변환하는 함수 - */ -export const formatDuration = (duration: string): string => { - switch (duration) { - case 'HALF': - return '30분'; - case 'HOUR': - return '1시간'; - case 'HOUR_HALF': - return '1시간 30분'; - case 'TWO_HOUR': - return '2시간'; - case 'TWO_HOUR_HALF': - return '2시간 30분'; - case 'THREE_HOUR': - return '3시간'; - default: - return 'UNDEFINED'; - } -}; +import { SelectedSlotType } from './contexts/useSelectContext'; +import { TitlesType } from './types'; -/** - * - * @desc 영어로 표현된 회의 장소를 한글로 변환하는 함수 - */ -export const formatPlace = (place: string) => { - switch (place) { - case 'ONLINE': - return '온라인'; - case 'OFFLINE': - return '오프라인'; - case 'UNDEFINED': - return undefined; - } -}; +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; /** * @@ -56,3 +46,50 @@ export const resetPriorities = (selectedSlots: SelectedSlotType): SelectedSlotTy 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"