diff --git a/src/api/axios.ts b/src/api/axios.ts index 6daa2b3..dd20453 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -1,17 +1,33 @@ import axios from 'axios'; export const authFetch = axios.create({ - baseURL: `${process.env.REACT_APP_SERVER_PATH}` + baseURL: `${process.env.REACT_APP_SERVER_PATH}`, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } }); authFetch.interceptors.request.use( (config) => { - config.headers['Content-Type'] = 'application/json'; - config.headers['Accept'] = 'application/json'; config.headers.Authorization = `${localStorage.getItem('rb-access-token')}`; + config.headers.refreshtoken = `${localStorage.getItem('rb-refresh-token')}`; return config; }, (error) => { return Promise.reject(error); } ); + +authFetch.interceptors.response.use( + (response) => response, + async (error) => { + const status = error.response.status; + if (status === 412) { + const result = await authFetch.post('/api/user/token'); + localStorage.setItem('rb-access-token', result.headers?.authorization); + } + + return Promise.reject(error); + } +); diff --git a/src/api/plan.ts b/src/api/plan.ts new file mode 100644 index 0000000..42df8ec --- /dev/null +++ b/src/api/plan.ts @@ -0,0 +1,34 @@ +import { authFetch } from '@api/axios'; +import { TPlanData } from '@api/types'; +import { AxiosError } from 'axios'; + +type TResponse = { error: boolean; success: boolean; data: T }; + +// 플랜 조회하기 +export const getPlanList = async (date: string): Promise> => { + try { + const result = await authFetch.get('/api/plan', { params: { date } }); + return { error: false, success: true, data: result.data }; + } catch (e) { + if (e instanceof AxiosError) { + return { error: true, success: false, data: e.response?.data.message ?? '' }; + } + + return { error: true, success: false, data: '' }; + } +}; + +// 플랜 삭제하기 +export const deletePlan = async (planId: number, userId: number): Promise> => { + try { + const result = await authFetch.delete(`/api/plan/${planId}`, { data: { userId } }); + + return { error: false, success: true, data: result.data }; + } catch (e: any) { + if (e instanceof AxiosError) { + return { error: true, success: false, data: e.response?.data.message ?? '' }; + } + + return { error: true, success: false, data: '' }; + } +}; diff --git a/src/api/types/planRecord.ts b/src/api/types/planRecord.ts index 182eb48..6d625a5 100644 --- a/src/api/types/planRecord.ts +++ b/src/api/types/planRecord.ts @@ -12,8 +12,8 @@ export enum ERecordStatus { // 주간 플랜달성데이터 export type TPlanRecord = { - createdAt: string; - status: EAchievementStatus | null; + date: string; + achievementStatus: EAchievementStatus | null; }; // 플랜조회 데이터 @@ -35,7 +35,7 @@ export type TPreviouslyFailedPlan = Omit { +export const Images = memo(({ imgUrl = '', imgAlt, imgWidth, imgHeight, imgStyle }: TProps) => { const handleError = useCallback((e: SyntheticEvent) => { e.currentTarget.src = require('/src/assets/images/noImage.jpeg'); }, []); diff --git a/src/components/common/Calendar/Calendar.tsx b/src/components/common/Calendar/Calendar.tsx index 8a2e3ed..3965d90 100644 --- a/src/components/common/Calendar/Calendar.tsx +++ b/src/components/common/Calendar/Calendar.tsx @@ -1,7 +1,7 @@ import { TPlanRecord } from '@api/types'; import { DayBird } from '@components/common/DayBird'; import { cls, getClassByStatus } from '@utils/classname'; -import { convertMap } from '@utils/function'; +import { convertObject } from '@utils/function'; import dayjs from 'dayjs'; import { useCallback, useMemo } from 'react'; import ReactCalendar, { TileContentFunc, TileDisabledFunc } from 'react-calendar'; @@ -14,7 +14,7 @@ type TProps = { }; export const Calendar = ({ record, currentDate, changeCurrentDate }: TProps) => { - const planDateRecord = useMemo(() => convertMap(record, 'createdAt'), [record]); + const planDateRecord = useMemo(() => convertObject(record, 'date'), [record]); const onChange = (value: Date) => { changeCurrentDate(dayjs(value).format()); }; @@ -23,8 +23,8 @@ export const Calendar = ({ record, currentDate, changeCurrentDate }: TProps) => ({ date }) => { const formatDate = dayjs(date).format('YYYY-MM-DD'); - const data = planDateRecord.get(formatDate); - const className = getClassByStatus(date, data?.status ?? null, currentDate); + const data = planDateRecord[formatDate]; + const className = getClassByStatus(date, data?.achievementStatus ?? null, currentDate); return ( ; + currentDate: Date; }; export const WeekCalendar = ({ record, currentDate }: TProps) => { + const weeks = useMemo(() => makeWeekArr(currentDate), [currentDate]); + return ( @@ -21,14 +25,14 @@ export const WeekCalendar = ({ record, currentDate }: TProps) => { ))} - {record.map((weekRecord) => { - const date = new Date(weekRecord.createdAt); - const nowDate = new Date(currentDate); - const className = getClassByStatus(date, weekRecord.status, nowDate); + {weeks.map((date) => { + const achievementStatus = + record[dayjs(date).format('YYYY-MM-DD')]?.achievementStatus ?? null; + const className = getClassByStatus(date, achievementStatus, currentDate); return ( - - {dayjs(weekRecord.createdAt).format('DD')} + + {dayjs(date).format('DD')} ); })} diff --git a/src/components/templates/HomeTemplate/HomeTemplate.tsx b/src/components/templates/HomeTemplate/HomeTemplate.tsx index cd44522..f4eaa68 100644 --- a/src/components/templates/HomeTemplate/HomeTemplate.tsx +++ b/src/components/templates/HomeTemplate/HomeTemplate.tsx @@ -1,12 +1,14 @@ -import { setCurrentDate } from '@/store/reducers'; +import { setCurrentDate, setPlan } from '@/store/reducers'; import { TAppDispatch, TRootState } from '@/store/state'; +import { getPlanList } from '@api/plan'; import { IconReact } from '@assets/icons'; import { CalendarBird } from '@components/common/CalendarBird'; import { Spacing } from '@components/common/Spacing'; import { colors } from '@style/global-style'; +import { Alert } from '@utils/Alert'; import { lastDayMonth } from '@utils/calendar'; import dayjs from 'dayjs'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Body, CalendarWrap, FlexBox, Head, TodayText, Wrap } from './Styled'; @@ -35,6 +37,27 @@ export const HomeTemplate = () => { dispatch(setCurrentDate(dayjs(currentDate).add(1, 'month').format())); }; + const getList = async () => { + const result = await getPlanList(dayjs(currentDate).format('YYYY-MM-DD')); + + if (typeof result.data !== 'string') { + dispatch(setPlan({ ...result.data })); + } else if (result.error) { + if (result.data) { + Alert.error({ + title: result.data, + action: () => { + dispatch(setPlan({ weedRecord: [], planData: [], previouslyFailedPlan: [] })); + } + }); + } + } + }; + + useEffect(() => { + getList(); + }, []); + return ( diff --git a/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx b/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx index 6ecec1d..5658ebd 100644 --- a/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx +++ b/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx @@ -1,8 +1,11 @@ +import { TRootState } from '@/store/state'; +import { deletePlan } from '@api/plan'; import { IconReact } from '@assets/icons'; import { MiniModal } from '@components/templates/HomeTemplate/Plan/Modal'; import { colors } from '@style/global-style'; import { Alert } from '@utils/Alert'; import { MouseEvent, useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; import styled from 'styled-components'; type TProps = { @@ -11,6 +14,7 @@ type TProps = { export const Dots = ({ planId }: TProps) => { const [isOpen, setOpen] = useState(null); + const { userId } = useSelector((state: TRootState) => state.userStore); const handleClose = useCallback(() => { setOpen(null); @@ -29,18 +33,28 @@ export const Dots = ({ planId }: TProps) => { setOpen(null); }, []); - const handleClickRemove = useCallback(() => { - Alert.confirm({ - title: '이 플랜을 정말 삭제할까요?', - text: '* 삭제된 플랜은 마이페이지에서 2주동안 보관됩니다.', - action: (result) => { - if (result.isConfirmed) { - Alert.success({ title: '삭제되었습니다!' }); + const handleClickRemove = useCallback( + (planId: number, userId: number | null) => () => { + if (userId === null) return; + + Alert.confirm({ + title: '이 플랜을 정말 삭제할까요?', + text: '* 삭제된 플랜은 마이페이지에서 2주동안 보관됩니다.', + action: async (result) => { + if (result.isConfirmed) { + const result = await deletePlan(planId, userId); + if (result.success) { + Alert.success({ title: '삭제되었습니다!' }); + } else { + Alert.error({ title: `${result.data.replace('Bad Request :', '')}` }); + } + setOpen(null); + } } - setOpen(null); - } - }); - }, []); + }); + }, + [] + ); return ( @@ -53,7 +67,7 @@ export const Dots = ({ planId }: TProps) => { /> - + ); diff --git a/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx b/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx index 1dec6c0..993b7cc 100644 --- a/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx +++ b/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx @@ -1,69 +1,65 @@ -import {TRootState} from '@/store/state'; -import {IconPlus} from '@assets/icons'; -import {IconEggNoPlan, IconEggOnePlan, IconEggThreePlan, IconEggTwoPlan} from '@assets/images'; -import {WeekCalendar} from '@components/common/Calendar'; -import {PlanModalTemplate} from "@components/templates/PlanModalTemplate"; -import {Spacing} from '@components/common/Spacing'; -import {Plan} from '@components/templates/HomeTemplate/Plan'; -import {MAX_CREATION_COUNT} from '@constants/plan'; -import {dummy} from '@mocks/index'; -import {useSelector} from 'react-redux'; -import {AddPlanWrap, EmptyPlan, PageProgress, PlanListBox, PlanVisualBox, Wrap} from './Styled'; -import {useState} from "react"; +import { TRootState } from '@/store/state'; +import { IconPlus } from '@assets/icons'; +import { IconEggNoPlan, IconEggOnePlan, IconEggThreePlan, IconEggTwoPlan } from '@assets/images'; +import { WeekCalendar } from '@components/common/Calendar'; +import { Spacing } from '@components/common/Spacing'; +import { Plan } from '@components/templates/HomeTemplate/Plan'; +import { PlanModalTemplate } from '@components/templates/PlanModalTemplate'; +import { MAX_CREATION_COUNT } from '@constants/plan'; +import { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AddPlanWrap, EmptyPlan, PageProgress, PlanListBox, PlanVisualBox, Wrap } from './Styled'; export const WeekTemplate = () => { - const {currentDate} = useSelector((state: TRootState) => state.planStore); - const [isModalOpen, setIsModalOpen] = useState(false); + const { currentDate, planData, weedRecord } = useSelector((state: TRootState) => state.planStore); + const date = useMemo(() => new Date(currentDate), [currentDate]); + const [isModalOpen, setIsModalOpen] = useState(false); - const handleClickAdd = () => { - // 모달 띄움 - setIsModalOpen(true); - }; + const handleClickAdd = () => { + // 모달 띄움 + setIsModalOpen(true); + }; - return ( - - - - - { - { - 0: , - 1: , - 2: , - 3: - }[dummy.plan?.length ?? 0] - } - - - {!!dummy.plan?.length ? ( - - {dummy.plan.map((plan) => ( -
  • - -
  • - ))} -
  • - - {dummy.plan?.length}/{MAX_CREATION_COUNT} - -
  • -
    - ) : ( - 아직 읽고 있는 책이 없어요. - )} - {/* 총 등록개수가 3개가 되면 버튼 숨김 */} - {(dummy.plan?.length ?? 0) < 3 && ( - - - - )} - + return ( + + {} + + + { + { + 0: , + 1: , + 2: , + 3: + }[planData?.length ?? 0] + } + + + {!!planData?.length ? ( + + {planData.map((plan) => ( +
  • + +
  • + ))} +
  • + + {planData.length}/{MAX_CREATION_COUNT} + +
  • +
    + ) : ( + 아직 읽고 있는 책이 없어요. + )} + {/* 총 등록개수가 3개가 되면 버튼 숨김 */} + {(planData?.length ?? 0) < 3 && ( + + + + )} + - -
    - ); + +
    + ); }; diff --git a/src/components/templates/LoginTemplate/LoginBtn.tsx b/src/components/templates/LoginTemplate/LoginBtn.tsx index 57973d2..0c8f26d 100644 --- a/src/components/templates/LoginTemplate/LoginBtn.tsx +++ b/src/components/templates/LoginTemplate/LoginBtn.tsx @@ -1,4 +1,4 @@ -import { setAccessToken } from '@/store/reducers'; +import { setAccessToken, setUserId } from '@/store/reducers'; import { authFetch } from '@api/axios'; import { TLoginResType } from '@api/types'; import { BtnKakaoLogin } from '@assets/images/BtnKakaoLogin'; @@ -28,8 +28,11 @@ export const LoginBtn = (props: TProps) => { const res = await authFetch.post(`/api/user/login-guest`); if (res.status === 200) { const extractedToken = res.headers?.authorization; + const refreshToken = res.headers?.refreshtoken; localStorage.setItem('rb-access-token', extractedToken); + localStorage.setItem('rb-refresh-token', refreshToken); localStorage.setItem('rb-user-info', JSON.stringify(res.data)); + dispatch(setUserId(res.data.userId)); dispatch(setAccessToken(extractedToken)); navigate('/'); } diff --git a/src/mocks/planRecord.ts b/src/mocks/planRecord.ts index 46d7c22..6254570 100644 --- a/src/mocks/planRecord.ts +++ b/src/mocks/planRecord.ts @@ -4,32 +4,32 @@ import { lastDayMonth } from '@utils/calendar'; // 주간 캘린더 export const weekCalendar: TPlanRecord[] = [ { - createdAt: '2023-12-10', - status: EAchievementStatus.success + date: '2023-12-10', + achievementStatus: EAchievementStatus.success }, { - createdAt: '2023-12-11', - status: EAchievementStatus.failed + date: '2023-12-11', + achievementStatus: EAchievementStatus.failed }, { - createdAt: '2023-12-12', - status: EAchievementStatus.unstable + date: '2023-12-12', + achievementStatus: EAchievementStatus.unstable }, { - createdAt: '2023-12-13', - status: null + date: '2023-12-13', + achievementStatus: null }, { - createdAt: '2023-12-14', - status: null + date: '2023-12-14', + achievementStatus: null }, { - createdAt: '2023-12-15', - status: null + date: '2023-12-15', + achievementStatus: null }, { - createdAt: '2023-12-16', - status: null + date: '2023-12-16', + achievementStatus: null } ]; @@ -48,8 +48,8 @@ export const monthCalendar = (currDate: Date): TPlanRecord[] => { const data = status[random] ?? null; calendar.push({ - createdAt: `${yearMonth}-${i}`, - status: data as EAchievementStatus | null + date: `${yearMonth}-${i}`, + achievementStatus: data as EAchievementStatus | null }); } @@ -80,7 +80,7 @@ export const plan = [ endDate: '2023-12-15', planStatus: ERecordStatus.inProgress, recordStatus: ERecordStatus.inProgress - }, + } // { // planId: 3, // title: '천사와 악마', diff --git a/src/store/reducers/userPlan.ts b/src/store/reducers/userPlan.ts index 5f12dad..aa20b2c 100644 --- a/src/store/reducers/userPlan.ts +++ b/src/store/reducers/userPlan.ts @@ -1,18 +1,19 @@ -import { TPlan, TPlanRecord, TPreviouslyFailedPlan } from '@api/types'; +import { TPlan, TPlanData, TPlanRecord, TPreviouslyFailedPlan } from '@api/types'; import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { convertObject } from '@utils/function'; import dayjs from 'dayjs'; -type TPlanData = { - weekRecord: TPlanRecord[] | null; - planData: TPlan[] | null; - previouslyFailedPlan: TPreviouslyFailedPlan[] | null; +type TState = { + weedRecord: Record; + planData: TPlan[]; + previouslyFailedPlan: TPreviouslyFailedPlan[]; currentDate: string; }; -const initialState: TPlanData = { - weekRecord: null, - planData: null, - previouslyFailedPlan: null, +const initialState: TState = { + weedRecord: {}, + planData: [], + previouslyFailedPlan: [], currentDate: dayjs().format() }; @@ -21,8 +22,8 @@ const planSlice = createSlice({ initialState, reducers: { setPlan: (state, action: PayloadAction) => { - const { weekRecord, planData, previouslyFailedPlan } = action.payload; - state.weekRecord = weekRecord; + const { weedRecord, planData, previouslyFailedPlan } = action.payload; + state.weedRecord = convertObject(weedRecord, 'date'); state.planData = planData; state.previouslyFailedPlan = previouslyFailedPlan; }, diff --git a/src/store/reducers/userSlice.ts b/src/store/reducers/userSlice.ts index 6a143f9..d1072e3 100644 --- a/src/store/reducers/userSlice.ts +++ b/src/store/reducers/userSlice.ts @@ -1,31 +1,33 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; interface IUserState { - accessToken: string; - userName: string; + accessToken: string; + userName: string; + userId: number | null; } const initialState: IUserState = { - accessToken: '', - userName: '', + accessToken: '', + userName: '', + userId: null }; const userSlice = createSlice({ - name: 'userInfo', - initialState, - reducers: { - setAccessToken: (state, action) => { - state.accessToken = action.payload; - }, - setUserName: (state, action) => { - state.userName = action.payload; - }, - } + name: 'userInfo', + initialState, + reducers: { + setAccessToken: (state, action) => { + state.accessToken = action.payload; + }, + setUserName: (state, action) => { + state.userName = action.payload; + }, + setUserId: (state, action: PayloadAction) => { + state.userId = action.payload; + } + } }); -export const { - setAccessToken, - setUserName , -} = userSlice.actions; +export const { setAccessToken, setUserName, setUserId } = userSlice.actions; export const userStore = userSlice.reducer; diff --git a/src/utils/calendar/function.ts b/src/utils/calendar/function.ts index 2779d47..e7282b6 100644 --- a/src/utils/calendar/function.ts +++ b/src/utils/calendar/function.ts @@ -36,3 +36,14 @@ export const calculateDday = (targetDate: Date) => { return daysDiff ? daysDiff : 'DAY'; }; + +// 주간 날짜 불러오기 +export const makeWeekArr = (date: Date) => { + let day = date.getDay(); + let week = []; + for (let i = 0; i < 7; i++) { + let newDate = new Date(date.valueOf() + 86400000 * (i - day)); + week.push(newDate); + } + return week; +}; diff --git a/src/utils/function.ts b/src/utils/function.ts index d358343..0dc6cd9 100644 --- a/src/utils/function.ts +++ b/src/utils/function.ts @@ -24,13 +24,15 @@ export const go = (obj: Record) => { return queryString; }; -// list 데이터 map 변환 함수 -export const convertMap = (list: T[], key: keyof T) => { - const map = new Map(); +// list 데이터 object 변환 함수 +export const convertObject = (list: T[], key: keyof T): Record => { + const object: Record = {}; for (const data of list) { - map.set(data[key], data); + const mapKey: T[keyof T] = data[key]; + + if (typeof mapKey === 'string') object[mapKey] = data; } - return map; + return object; };