diff --git a/index.html b/index.html index a2cb802..63ea31b 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + Hearus diff --git a/src/apis/auth.ts b/src/apis/auth.ts index a9e9ef9..a7d16b8 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -1,4 +1,4 @@ -import { API_URL } from '.'; +import { API_URL, IApiResponse } from '.'; interface IEmailSignUpParams { userEmail: string; @@ -11,23 +11,15 @@ interface IEmailLoginParams { userPassword: string; } -interface IGoogleLoginParams { +interface ISocialLoginParams { + social: string; state: string; code: string; } -interface ILoginResponse { - status: string; - msg: string; - object: ITokens; -} +interface ILoginResponse extends IApiResponse {} -interface IEmailSignupResponse { - status: string; - msg: string; - object: null; - success: boolean; -} +interface IEmailSignupResponse extends IApiResponse {} interface ITokens { grantType: string; @@ -35,10 +27,14 @@ interface ITokens { refreshToken: string; } -export const googleLogin = async ({ state, code }: IGoogleLoginParams) => { +export const OAuthLogin = async ({ + social, + state, + code, +}: ISocialLoginParams) => { try { const res = await fetch( - `${API_URL}/login/oauth2/code/google?state=${state}&code=${code}`, + `${API_URL}/login/oauth2/code/${social}?state=${state}&code=${code}`, { credentials: 'include', }, @@ -66,9 +62,6 @@ export const emailLogin = async ({ userIsOAuth: false, }), }); - if (!res.ok) { - throw new Error('Login failed'); - } const data: ILoginResponse = await res.json(); return data.object; } catch (error) { @@ -95,10 +88,6 @@ export const emailSignUp = async ({ }), }); const data: IEmailSignupResponse = await res.json(); - - if (!res.ok) { - throw new Error('SignUp failed'); - } return data; } catch (error) { throw error; diff --git a/src/apis/index.ts b/src/apis/index.ts index b40063a..5e5a06e 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,6 +1,17 @@ +/* 경로 */ export const API_URL: string = import.meta.env.VITE_API_URL; +export const SOCKETURL = import.meta.env.VITE_SOCKETIO_HOST; + +/* API 공통 */ export const getToken = () => { const token = localStorage.getItem('token'); return token; }; + +export interface IApiResponse { + status: string; + msg: string; + object: T; + success: boolean; +} diff --git a/src/apis/record.ts b/src/apis/record.ts deleted file mode 100644 index 21f870e..0000000 --- a/src/apis/record.ts +++ /dev/null @@ -1,3 +0,0 @@ -const SOCKETURL = import.meta.env.VITE_SOCKETIO_HOST; - -export default SOCKETURL; diff --git a/src/apis/schedule.ts b/src/apis/schedule.ts index 5d13fa0..d2d7648 100644 --- a/src/apis/schedule.ts +++ b/src/apis/schedule.ts @@ -1,18 +1,14 @@ -import { API_URL } from '.'; +import { API_URL, IApiResponse, getToken } from '.'; import { IScheduleElement } from '../constants/schedule'; -import { IScheduleElementDTO } from '../utils/schedule'; -import { getToken } from './'; +import { IScheduleElementDTO } from '../constants/schedule'; -interface IGetScheduleResponse { - status: string; - msg: string; - object: { - id: number; - scheduleElements: IScheduleElement[]; - name: string; - userId: string | null; - }; - success: boolean; +interface IGetScheduleResponse extends IApiResponse {} + +interface IGetScheduleObject { + id: number; + scheduleElements: IScheduleElement[]; + name: string; + userId: string | null; } export const getSchedule = async ( @@ -29,18 +25,18 @@ export const getSchedule = async ( }, ); const data: IGetScheduleResponse = await res.json(); + if (data.msg === 'Schedule not found with name') { + await createNewScheduleName(name); + return getSchedule(name); + } return data.object['scheduleElements']; } catch (error) { throw error; } }; -interface IGetLectureByScheduleElementResponse { - status: string; - msg: string; - object: ILectureItem[]; - success: boolean; -} +interface IGetLectureByScheduleElementResponse + extends IApiResponse {} interface ILectureItem { id: string; @@ -70,12 +66,7 @@ export const getLectureByScheduleElement = async (id: number) => { } }; -interface IPOSTScheduleElementResponse { - status: string; - msg: string; - object: null; - success: boolean; -} +interface IPOSTScheduleElementResponse extends IApiResponse {} export const addScheduleElement = async ( inputData: IScheduleElementDTO, @@ -99,7 +90,7 @@ export const addScheduleElement = async ( const data: IPOSTScheduleElementResponse = await res.json(); return data.success; } catch (error) { - throw new Error('Failed to delete schedule element'); + throw error; } }; @@ -127,6 +118,28 @@ export const deleteScheduleElement = async ( const data: IPOSTScheduleElementResponse = await res.json(); return data.success; } catch (error) { - throw new Error('Failed to delete schedule element'); + throw error; + } +}; + +const createNewScheduleName = async (name: string) => { + const token = getToken(); + try { + const res = await fetch(`${API_URL}/api/v1/schedule/addSchedule`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + name, + }), + }); + const data = await res.json(); + if (!data.success) { + throw new Error(data.msg); + } + } catch (error) { + throw error; } }; diff --git a/src/apis/script.ts b/src/apis/script.ts index 830e310..0ffb7a3 100644 --- a/src/apis/script.ts +++ b/src/apis/script.ts @@ -1,18 +1,8 @@ -import { API_URL, getToken } from '.'; +import { API_URL, getToken, IApiResponse } from '.'; -interface IGetAllScriptResponse { - status: string; - msg: string; - object: IScriptInList[]; - success: boolean; -} +interface IGetAllScriptResponse extends IApiResponse {} -interface IGetScriptDetailResponse { - status: string; - msg: string; - object: IScriptDetail; - success: boolean; -} +interface IGetScriptDetailResponse extends IApiResponse {} export interface IScriptInList { id: string; diff --git a/src/apis/test.ts b/src/apis/test.ts index 2c48efe..31fb3aa 100644 --- a/src/apis/test.ts +++ b/src/apis/test.ts @@ -1,11 +1,6 @@ -import { API_URL, getToken } from '.'; +import { API_URL, getToken, IApiResponse } from '.'; -interface IGenerateProblemResponse { - status: string; - msg: string; - object: IQuestion[]; - success: boolean; -} +interface IGenerateProblemResponse extends IApiResponse {} export interface IQuestion { type: string; @@ -33,7 +28,7 @@ export const generateProblem = async (inputData: IProblemInput) => { body: JSON.stringify(inputData), }); const data: IGenerateProblemResponse = await res.json(); - return data.object; + return data; } catch (error) { throw error; } diff --git a/src/apis/user.ts b/src/apis/user.ts index 7a099ce..9ef6ecf 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,11 +1,6 @@ -import { API_URL, getToken } from '.'; +import { API_URL, getToken, IApiResponse } from '.'; -interface IGetUserInfoResponse { - status: string; - msg: string; - object: IUserInfo; - success: boolean; -} +interface IGetUserInfoResponse extends IApiResponse {} export interface IUserInfo { userId: string; diff --git a/src/components/molecules/RecordTagDropDown/RecordTagDropDown.tsx b/src/components/molecules/RecordTagDropDown/RecordTagDropDown.tsx index 201ae04..925574f 100644 --- a/src/components/molecules/RecordTagDropDown/RecordTagDropDown.tsx +++ b/src/components/molecules/RecordTagDropDown/RecordTagDropDown.tsx @@ -1,6 +1,8 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import useRecordModalStore from '../../../store/useRecordModalStore'; +import useRecordModalStore, { + ITagItem, +} from '../../../store/useRecordModalStore'; import { IScheduleElement } from '../../../constants/schedule'; import { getSchedule } from '../../../apis/schedule'; import styles from './RecordTagDropDown.module.scss'; @@ -21,18 +23,33 @@ const RecordTagDropDown = () => { const TAGS = useMemo(() => { if (!data) return []; - return Array.from(new Set(data.map((item) => item.name))); + const tagObject: { [key: string]: number } = {}; + + data.forEach((item) => { + if (!tagObject.hasOwnProperty(item.name)) { + tagObject[item.name] = item.scheduleId; + } + }); + + return Object.entries(tagObject).map(([name, id]) => ({ + name, + scheduleId: id, + })); }, [data]); const handleTagBtnClick = () => { setIsTagBtnClicked((prev) => !prev); }; - const handleLiClick = (name: string) => { - updateRecordData({ tag: name }); + const handleLiClick = (item: ITagItem) => { + updateRecordData({ tag: item.name, scheduleId: item.scheduleId }); setIsTagBtnClicked(false); }; + useEffect(() => { + console.log(recordData); + }, [recordData]); + return (
{isTagBtnClicked && (
    - {TAGS.map((name) => ( + {TAGS.map((item) => (
  • handleLiClick(name)} + onClick={() => handleLiClick(item)} role="option" > - {name} + {item.name}
  • ))}
diff --git a/src/components/molecules/ScriptToolTip/ScriptToolTip.tsx b/src/components/molecules/ScriptToolTip/ScriptToolTip.tsx index a4e4624..d2330e4 100644 --- a/src/components/molecules/ScriptToolTip/ScriptToolTip.tsx +++ b/src/components/molecules/ScriptToolTip/ScriptToolTip.tsx @@ -29,7 +29,9 @@ const ScriptToolTip = ({ id, scheduleName }: IProps) => { const deleteMutation = useMutation({ mutationFn: (id: number) => deleteScheduleElement(id, userInfo.userName), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['schedule', name] }); + queryClient.invalidateQueries({ + queryKey: ['schedule', userInfo.userName], + }); }, onError: () => { alert('시간표 삭제를 실패했습니다.'); diff --git a/src/components/organisms/AddScheduleForm/AddScheduleForm.tsx b/src/components/organisms/AddScheduleForm/AddScheduleForm.tsx index 4aafc3a..29c9953 100644 --- a/src/components/organisms/AddScheduleForm/AddScheduleForm.tsx +++ b/src/components/organisms/AddScheduleForm/AddScheduleForm.tsx @@ -1,8 +1,12 @@ import React, { useState, useEffect, useRef } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useUserInfoStore } from '../../../store/userUserInfoStore'; import Warning from '../../../assets/images/warning.svg?react'; import { COLORS, ColorKey, + IScheduleElement, + IScheduleElementDTO, LectureInfo, daysOfWeek, initialLectureInfo, @@ -10,13 +14,11 @@ import { import { getIsAddScheduleFormValid, getIsTimeValid, - IScheduleElementDTO, + hasNewElementConflict, transformToScheduleElementDTO, } from '../../../utils/schedule'; -import styles from './AddScheduleForm.module.scss'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; import { addScheduleElement } from '../../../apis/schedule'; -import { useUserInfoStore } from '../../../store/userUserInfoStore'; +import styles from './AddScheduleForm.module.scss'; interface IProps { onClose: () => void; @@ -98,6 +100,7 @@ const AddScheduleForm = ({ onClose }: IProps) => { mutationFn: (data: IScheduleElementDTO) => addScheduleElement(data, userInfo.userName), onSuccess: () => { + alert('강의 추가에 성공했습니다.'); onClose(); queryClient.invalidateQueries({ queryKey: ['schedule', userInfo.userName], @@ -110,8 +113,25 @@ const AddScheduleForm = ({ onClose }: IProps) => { const handleSubmit = () => { if (isFormValid) { + const existingSchedule = queryClient.getQueryData([ + 'schedule', + userInfo.userName, + ]); const formattedData = transformToScheduleElementDTO(lectureInfo); - postMutation.mutate(formattedData); + if (existingSchedule) { + if (hasNewElementConflict(formattedData, existingSchedule)) { + alert( + '새로운 강의 시간이 기존 스케줄과 겹칩니다. 다른 시간을 선택해주세요.', + ); + return; + } else { + postMutation.mutate(formattedData); + } + } else { + alert( + '스케줄 데이터를 불러올 수 없습니다. 페이지를 새로고침한 후 다시 시도해주세요.', + ); + } } }; diff --git a/src/components/organisms/TestOptionSelector/TestOptionSelector.module.scss b/src/components/organisms/TestOptionSelector/TestOptionSelector.module.scss index e311525..49e1648 100644 --- a/src/components/organisms/TestOptionSelector/TestOptionSelector.module.scss +++ b/src/components/organisms/TestOptionSelector/TestOptionSelector.module.scss @@ -74,7 +74,7 @@ label:before { background-color: $brand-point; border: 1px solid $brand-point; border-radius: 3px; - background-image: url('../../assets/images/check.svg'); + background-image: url('../../../assets/images/check.svg'); background-repeat: no-repeat; background-position: 50%; } diff --git a/src/components/organisms/headers/TestHeader/TestHeader.tsx b/src/components/organisms/headers/TestHeader/TestHeader.tsx index 5e3fcd4..955e670 100644 --- a/src/components/organisms/headers/TestHeader/TestHeader.tsx +++ b/src/components/organisms/headers/TestHeader/TestHeader.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import TestModal from '../../../templates/modals/TestModal/TestModal'; import useTestModalStore from '../../../../store/useTestModalStore'; @@ -16,26 +16,26 @@ const TestHeader = ({ handleSubmit, showResults }: IProps) => { const [seconds, setSeconds] = useState(0); const timerIntervalRef = useRef(null); - const { testName } = useTestSettingsStore(); - const { isModalOpen, openModal } = useTestModalStore(); + const { testName, timeLimit } = useTestSettingsStore(); + const { isModalOpen, openModal, clearTestData } = useTestModalStore(); - const startTimer = () => { + const startTimer = useCallback(() => { if (timerIntervalRef.current !== null) return; timerIntervalRef.current = setInterval(() => { setSeconds((prev) => prev + 1); }, 1000); - }; + }, []); - const stopTimer = () => { + const stopTimer = useCallback(() => { if (timerIntervalRef.current === null) return; setSeconds(0); clearInterval(timerIntervalRef.current); timerIntervalRef.current = null; - }; + }, []); - const handleClickQuitBtn = () => { + const handleClickQuitBtn = useCallback(() => { if (!showResults) openModal(); - }; + }, []); useEffect(() => { startTimer(); @@ -46,6 +46,16 @@ const TestHeader = ({ handleSubmit, showResults }: IProps) => { if (showResults) stopTimer(); }, [showResults]); + useEffect(() => { + if (timeLimit != null) { + if (seconds >= timeLimit * 60) { + stopTimer(); + clearTestData(); + handleSubmit(); + } + } + }, [timeLimit, seconds]); + return (
diff --git a/src/constants/landing.ts b/src/constants/landing.ts index 0582535..4bec55b 100644 --- a/src/constants/landing.ts +++ b/src/constants/landing.ts @@ -1,6 +1,12 @@ export const SCROLLING_TEXTS = [ + '배움을 향한 열정이 상처받지 않도록', + '모두의 들을 권리를 위하여', + '정확하지 못한 정보에 기대지 않도록', '신체의 불편함이 배움의 불편함이 되지 않도록', '배움을 향한 열정이 상처받지 않도록', '모두의 들을 권리를 위하여', + + '정확하지 못한 정보에 기대지 않도록', + '신체의 불편함이 배움의 불편함이 되지 않도록', ]; diff --git a/src/constants/schedule.ts b/src/constants/schedule.ts index 72fb4d5..79cfe44 100644 --- a/src/constants/schedule.ts +++ b/src/constants/schedule.ts @@ -13,6 +13,24 @@ export interface IScheduleElement { endTime: string; } +export interface IAddScheduleElement { + name: string; + location: string; + dayOfWeek: DayOfWeek; + color: ColorKey; + startTime: string; + endTime: string; +} + +export interface IScheduleElementDTO { + name: string; + location: string; + dayOfWeek: DayOfWeek; + startTime: string; + endTime: string; + color: ColorKey; +} + export const TIMELIST = Array.from(Array(13), (_, i) => i + 9); export const COLORS = { @@ -49,7 +67,7 @@ export const initialLectureInfo: LectureInfo = { endMinute: '00', }; -export const daysObject = { +export const daysKorEnMap = { 월: 'MON', 화: 'TUE', 수: 'WED', @@ -57,4 +75,14 @@ export const daysObject = { 금: 'FRI', 토: 'SAT', 일: 'SUN', +} as const; + +export const daysEnNumMap: Record = { + SUN: 0, + MON: 1, + TUE: 2, + WED: 3, + THU: 4, + FRI: 5, + SAT: 6, }; diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts deleted file mode 100644 index 6681a13..0000000 --- a/src/hooks/useModal.ts +++ /dev/null @@ -1,30 +0,0 @@ -// type OpenModalType = { -// title: string; -// content: JSX.Element | string; -// callback?: () => any; -// }; - -// export const useModal = () => { -// const [modalDataState, setModalDataState] = useRecoilState(modalState); - -// const closeModal = useCallback( -// () => -// setModalDataState((prev) => { -// return { ...prev, isOpen: false }; -// }), -// [setModalDataState] -// ); - -// const openModal = useCallback( -// ({ title, content, callback }: OpenModalType) => -// setModalDataState({ -// isOpen: true, -// title: title, -// content: content, -// callBack: callback -// }), -// [setModalDataState] -// ); - -// return { modalDataState, closeModal, openModal }; -// }; diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts index d13d02a..0fde16f 100644 --- a/src/hooks/useSocket.ts +++ b/src/hooks/useSocket.ts @@ -1,9 +1,11 @@ import { useEffect, useRef } from 'react'; import { io, Socket } from 'socket.io-client'; -import SOCKETURL from '../apis/record'; +import { SOCKETURL } from '../apis/index'; +import useRecordModalStore from '../store/useRecordModalStore'; export const useSocket = (onTransitionResult: (result: string) => void) => { const socketRef = useRef(null); + const { recordData } = useRecordModalStore(); useEffect(() => { socketRef.current = io(SOCKETURL, { @@ -20,7 +22,7 @@ export const useSocket = (onTransitionResult: (result: string) => void) => { socketRef.current.on('connect', () => { console.log('socket connected'); - const lectureId = '668cceb8ebef2b4462de0fb5'; + const lectureId = recordData.scheduleId; socketRef.current?.emit('lectureId', lectureId); }); diff --git a/src/pages/Auth/AuthForm/AuthForm.tsx b/src/pages/Auth/AuthForm/AuthForm.tsx index 078d857..6d6299d 100644 --- a/src/pages/Auth/AuthForm/AuthForm.tsx +++ b/src/pages/Auth/AuthForm/AuthForm.tsx @@ -44,7 +44,6 @@ const AuthForm = ({ e.preventDefault(); setIsShowPasswordConfirmClick((prev) => !prev); }; - const loginMutation = useMutation({ mutationFn: emailLogin, onSuccess: (data) => { @@ -93,8 +92,8 @@ const AuthForm = ({ } }; - const handleGoogleClick = () => { - window.location.href = `${API_URL}/oauth2/authorization/google`; + const handleOAuthClick = (e: string) => { + window.location.href = `${API_URL}/oauth2/authorization/${e}`; }; return ( @@ -124,9 +123,9 @@ const AuthForm = ({ value={password} onChange={(e) => setPassword(e.target.value)} /> - +
{title === '로그인' && (
@@ -154,12 +153,12 @@ const AuthForm = ({ onChange={(e) => setPasswordConfirm(e.target.value)} /> - +