diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fca0612..8d2809d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,8 +17,10 @@ jobs: - name: .env setting run: | echo "REACT_APP_KAKAO_API_KEY=${{ secrets.REACT_APP_KAKAO_API_KEY }}" >> .env + echo "REACT_APP_SERVER_PATH=${{ secrets.REACT_APP_SERVER_PATH }}" >> .env env: REACT_APP_KAKAO_API_KEY: ${{ secrets.REACT_APP_KAKAO_API_KEY }} + REACT_APP_SERVER_PATH: ${{ secrets.REACT_APP_SERVER_PATH }} - name: Deploy uses: cloudtype-github-actions/deploy@v1 with: diff --git a/src/App.tsx b/src/App.tsx index 3bdedaa..31f53ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,23 @@ +import { setCurrentDate } from '@/store/reducers'; +import { TAppDispatch } from '@/store/state'; import { LoadingTemplate } from '@components/templates/LoadingTemplate'; import { appRouter } from '@routers/appRouter'; import { GlobalStyle, theme } from '@style/global-style'; +import dayjs from 'dayjs'; +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; import { RouterProvider } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; export const App = () => { + // 첫 접속시 날짜 등록 + const dispatch = useDispatch(); + + useEffect(() => { + // 첫 접속시 당일 날짜 등록 + dispatch(setCurrentDate(dayjs().format())); + }, []); + return ( diff --git a/src/api/types/planRecord.ts b/src/api/types/planRecord.ts index f946706..182eb48 100644 --- a/src/api/types/planRecord.ts +++ b/src/api/types/planRecord.ts @@ -32,3 +32,10 @@ export type TPlan = { // 플랜달성실패 데이터 export type TPreviouslyFailedPlan = Omit; + +// 전체 타입 +export type TPlanData = { + weedRecord: TPlanRecord[] | null; + planData: TPlan[] | null; + previouslyFailedPlan: TPreviouslyFailedPlan[] | null; +}; diff --git a/src/assets/icons/IconBook.tsx b/src/assets/icons/IconBook.tsx new file mode 100644 index 0000000..007b632 --- /dev/null +++ b/src/assets/icons/IconBook.tsx @@ -0,0 +1,21 @@ +import { SVGProps } from 'react'; + +type TIconProps = { + fillColor?: string; + strokeColor?: string; +} & SVGProps; + +export const IconBook = ({ fillColor = '#E3CCF2', strokeColor = 'white' }: TIconProps) => { + return ( + + + + + ); +}; diff --git a/src/assets/icons/IconCalendar.tsx b/src/assets/icons/IconCalendar.tsx index 7c45dfe..86f1dae 100644 --- a/src/assets/icons/IconCalendar.tsx +++ b/src/assets/icons/IconCalendar.tsx @@ -5,15 +5,9 @@ type TIconProps = { strokeColor?: string; } & SVGProps; -export const IconCalendar = ({ width = 45, height = 36, fillColor = '#ffffff' }: TIconProps) => { +export const IconCalendar = ({ fillColor = '#ffffff' }: TIconProps) => { return ( - + ; + +export const IconDayBirdMini = ({ + fillColor = 'transparent', + strokeColor = '#ABABAB', + ...props +}: TIconProps) => { + return ( + + + + ); +}; diff --git a/src/assets/icons/IconReact.tsx b/src/assets/icons/IconReact.tsx index ce60838..9c85958 100644 --- a/src/assets/icons/IconReact.tsx +++ b/src/assets/icons/IconReact.tsx @@ -1,10 +1,12 @@ import { IconBaseProps } from '@react-icons/all-files'; -import { FaPlus } from '@react-icons/all-files/fa/FaPlus'; import { HiDotsHorizontal } from '@react-icons/all-files/hi/HiDotsHorizontal'; +import { IoIosArrowBack } from '@react-icons/all-files/io/IoIosArrowBack'; +import { IoIosArrowForward } from '@react-icons/all-files/io/IoIosArrowForward'; const Icons = { dots: HiDotsHorizontal, - plus: FaPlus + arrow_left: IoIosArrowBack, + arrow_right: IoIosArrowForward }; type TProps = { diff --git a/src/assets/icons/index.tsx b/src/assets/icons/index.tsx index fc56081..8735f38 100644 --- a/src/assets/icons/index.tsx +++ b/src/assets/icons/index.tsx @@ -1,5 +1,7 @@ +export * from './IconBook'; export * from './IconCalendar'; export * from './IconDayBird'; +export * from './IconDayBirdMini'; export * from './IconFailed'; export * from './IconHome'; export * from './IconMyPage'; diff --git a/src/components/common/Calendar/Calendar.tsx b/src/components/common/Calendar/Calendar.tsx index bd9dbc8..8a2e3ed 100644 --- a/src/components/common/Calendar/Calendar.tsx +++ b/src/components/common/Calendar/Calendar.tsx @@ -1,25 +1,61 @@ -import { useState } from 'react'; -import ReactCalendar from 'react-calendar'; -import 'react-calendar/dist/Calendar.css'; +import { TPlanRecord } from '@api/types'; +import { DayBird } from '@components/common/DayBird'; +import { cls, getClassByStatus } from '@utils/classname'; +import { convertMap } from '@utils/function'; +import dayjs from 'dayjs'; +import { useCallback, useMemo } from 'react'; +import ReactCalendar, { TileContentFunc, TileDisabledFunc } from 'react-calendar'; +import '../../../styles/calendar.css'; -type ValuePiece = Date | null; +type TProps = { + currentDate: Date; + record: TPlanRecord[]; + changeCurrentDate: (date: string) => void; +}; + +export const Calendar = ({ record, currentDate, changeCurrentDate }: TProps) => { + const planDateRecord = useMemo(() => convertMap(record, 'createdAt'), [record]); + const onChange = (value: Date) => { + changeCurrentDate(dayjs(value).format()); + }; -type Value = ValuePiece | [ValuePiece, ValuePiece]; + const tileContent: TileContentFunc = useCallback( + ({ date }) => { + const formatDate = dayjs(date).format('YYYY-MM-DD'); -export const Calendar = () => { - const [value, setValue] = useState(new Date()); + const data = planDateRecord.get(formatDate); + const className = getClassByStatus(date, data?.status ?? null, currentDate); - const onChange = (value: Value, event: React.MouseEvent) => { - console.log(value); - }; + return ( + +

{dayjs(date).format('DD')}

+ + ); + }, + [planDateRecord, currentDate] + ); + + const tileDisabled: TileDisabledFunc = useCallback( + ({ date }) => getClassByStatus(date, null, currentDate) === 'after-today', + [currentDate] + ); return ( -
-
-
- -
-
-
+ dayjs(date).format('D')} + goToRangeStartOnSelect={false} + maxDate={new Date(dayjs().add(1, 'year').format('YYYY-MM-DD'))} + showNavigation={false} + tileContent={tileContent} + tileDisabled={tileDisabled} + activeStartDate={currentDate} + /> ); }; diff --git a/src/components/common/Calendar/CustomCalendar.tsx b/src/components/common/Calendar/WeekCalendar.tsx similarity index 51% rename from src/components/common/Calendar/CustomCalendar.tsx rename to src/components/common/Calendar/WeekCalendar.tsx index 4b836cc..53bc64b 100644 --- a/src/components/common/Calendar/CustomCalendar.tsx +++ b/src/components/common/Calendar/WeekCalendar.tsx @@ -1,53 +1,36 @@ import { TPlanRecord } from '@api/types'; import { DayBird } from '@components/common/DayBird'; -import { cls } from '@utils/classname'; +import { DAY_OF_WEEK } from '@constants/plan'; +import { cls, getClassByStatus } from '@utils/classname'; import dayjs from 'dayjs'; -import { useCallback } from 'react'; import styled from 'styled-components'; type TProps = { record: TPlanRecord[]; + currentDate: Date; }; -const dayOfWeek = ['일', '월', '화', '수', '목', '금', '토']; - -export const CustomCalendar = ({ record }: TProps) => { - const colorName = useCallback((record: TPlanRecord) => { - const today = new Date(); - const recordDate = new Date(record.createdAt); - - if (recordDate.getMonth() > today.getMonth()) { - return 'after-today'; - } - - if (recordDate.getDate() === today.getDate()) { - return 'today'; - } - - if (record.status === null) { - return 'before'; - } - - return record.status; - }, []); - +export const WeekCalendar = ({ record, currentDate }: TProps) => { return ( - {dayOfWeek.map((day) => ( + {DAY_OF_WEEK.map((day) => (

{day}

))}
- {record.map((weekRecord) => ( - - - {dayjs(weekRecord.createdAt).format('DD')} - - - ))} + {record.map((weekRecord) => { + const date = new Date(weekRecord.createdAt); + const className = getClassByStatus(date, weekRecord.status, currentDate); + + return ( + + {dayjs(weekRecord.createdAt).format('DD')} + + ); + })}
); diff --git a/src/components/common/Calendar/index.tsx b/src/components/common/Calendar/index.tsx index 98a9a2b..b551aaa 100644 --- a/src/components/common/Calendar/index.tsx +++ b/src/components/common/Calendar/index.tsx @@ -1,2 +1,2 @@ export { Calendar } from './Calendar'; -export { CustomCalendar } from './CustomCalendar'; +export { WeekCalendar } from './WeekCalendar'; diff --git a/src/components/common/Loadable/Loadable.tsx b/src/components/common/Loadable/Loadable.tsx index 13ea47b..c465854 100644 --- a/src/components/common/Loadable/Loadable.tsx +++ b/src/components/common/Loadable/Loadable.tsx @@ -1,12 +1,13 @@ +import { LoadingTemplate } from '@components/templates/LoadingTemplate'; import { LazyExoticComponent, Suspense } from 'react'; export const Loadable = - ( - Component: LazyExoticComponent<() => JSX.Element>, - fallback: JSX.Element =
로딩중...
- ) => - () => ( - - - - ); + ( + Component: LazyExoticComponent<() => JSX.Element>, + fallback: JSX.Element = + ) => + () => ( + + + + ); diff --git a/src/components/common/Loading/Loading.tsx b/src/components/common/Loading/Loading.tsx new file mode 100644 index 0000000..e41e3be --- /dev/null +++ b/src/components/common/Loading/Loading.tsx @@ -0,0 +1,3 @@ +export const Loading = () => { + return
로딩중이에요.
; +}; diff --git a/src/components/common/Loading/index.tsx b/src/components/common/Loading/index.tsx new file mode 100644 index 0000000..5ee0d48 --- /dev/null +++ b/src/components/common/Loading/index.tsx @@ -0,0 +1 @@ +export { Loading } from './Loading'; diff --git a/src/components/common/Navigation/Navigation.tsx b/src/components/common/Navigation/Navigation.tsx index 3a535b1..b260c37 100644 --- a/src/components/common/Navigation/Navigation.tsx +++ b/src/components/common/Navigation/Navigation.tsx @@ -8,26 +8,38 @@ export const Navigation = () => { return ( - - - - - - - 검색 - - - - 마이페이지 - + + + + + + + + 검색 + + + + 마이페이지 + + ); }; const NavWrap = styled.nav` flex: 0 0 70px; + width: 100%; + background-color: white; + + display: flex; + justify-content: center; + align-items: flex-start; +`; + +const NavInner = styled.div` width: 100%; max-width: 368px; + height: 100%; border-top: 1px solid ${({ theme }) => theme.colors.basic}; padding-top: 10px; diff --git a/src/components/common/ProgressBar/ProgressBar.tsx b/src/components/common/ProgressBar/ProgressBar.tsx index 184cb94..5da4795 100644 --- a/src/components/common/ProgressBar/ProgressBar.tsx +++ b/src/components/common/ProgressBar/ProgressBar.tsx @@ -35,13 +35,13 @@ const StyledProgress = styled.progress` } &::-moz-progress-bar { - background-color: #cbd2fc; + background-color: ${({ theme }) => theme.colors.subYellow}; border-radius: 20px; border: none; } &::-webkit-progress-value { - background-color: #cbd2fc; + background-color: ${({ theme }) => theme.colors.subYellow}; border-radius: 20px; border: none; } diff --git a/src/components/common/ProtectedLogin/ProtectedLogin.tsx b/src/components/common/ProtectedLogin/ProtectedLogin.tsx new file mode 100644 index 0000000..f6d01b5 --- /dev/null +++ b/src/components/common/ProtectedLogin/ProtectedLogin.tsx @@ -0,0 +1,44 @@ +import { Fragment, ReactNode, useEffect, useMemo, useState } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; + +type TProps = { + loginNode?: ReactNode; + appNode?: ReactNode; +}; + +export const ProtectedLogin = ({ loginNode, appNode }: TProps) => { + const [isLoading, setLoading] = useState(true); + const [loginToken, setLoginToken] = useState(null); + + const location = useLocation(); + const isLoginPath = useMemo(() => /login/g.test(location.pathname), [location.pathname]); + + useEffect(() => { + // api를 이용해 로그인 토큰을 불러온다 + setLoginToken('토큰이 존재한다고 가정'); + // setLoginToken(null); + setLoading(false); + }, []); + + if (isLoading) { + return <>; + } + + // 토큰 정보가 없고 로그인 화면인 경우 + if (loginToken === null && isLoginPath) { + return {loginNode}; + } + + // 토큰 정보가 없고 홈 화면인 경우 + if (loginToken === null && !isLoginPath) { + return ; + } + + // 토큰 정보가 있고 로그인 화면인 경우 + if (loginToken && isLoginPath) { + return ; + } + + // 토큰 정보가 있고 홈 화면인 경우 + return {appNode}; +}; diff --git a/src/components/common/ProtectedLogin/index.tsx b/src/components/common/ProtectedLogin/index.tsx new file mode 100644 index 0000000..6064f1c --- /dev/null +++ b/src/components/common/ProtectedLogin/index.tsx @@ -0,0 +1 @@ +export { ProtectedLogin } from './ProtectedLogin'; diff --git a/src/components/common/Spacing/Spacing.tsx b/src/components/common/Spacing/Spacing.tsx index 39830dc..b46c657 100644 --- a/src/components/common/Spacing/Spacing.tsx +++ b/src/components/common/Spacing/Spacing.tsx @@ -12,8 +12,8 @@ export const Spacing = (props: TStyleProps) => { const SpaceWrap = styled.div.attrs(({ style }) => ({ style: { ...style } }))` - width: ${({ width }) => `${width}px`}; - min-width: ${({ width }) => `${width}px`}; - height: ${({ height }) => `${height}px`}; - min-height: ${({ height }) => `${height}px`}; + width: ${({ width }) => (width ? `${width}px` : undefined)}; + min-width: ${({ width }) => (width ? `${width}px` : undefined)}; + height: ${({ height }) => (height ? `${height}px` : undefined)}; + min-height: ${({ height }) => (height ? `${height}px` : undefined)}; `; diff --git a/src/components/templates/AppTemplate/AppTemplate.tsx b/src/components/templates/AppTemplate/AppTemplate.tsx index 20550a0..f0b432d 100644 --- a/src/components/templates/AppTemplate/AppTemplate.tsx +++ b/src/components/templates/AppTemplate/AppTemplate.tsx @@ -15,13 +15,12 @@ export const AppTemplate = () => { const AppWrap = styled.div` width: 100%; - height: 100%; display: flex; flex-direction: column; align-items: center; `; const MainWrap = styled.main` - flex: 1; width: 100%; + height: calc(100vh - 70px); `; diff --git a/src/components/templates/HomeTemplate/HomeTemplate.tsx b/src/components/templates/HomeTemplate/HomeTemplate.tsx index b5b5035..207f63c 100644 --- a/src/components/templates/HomeTemplate/HomeTemplate.tsx +++ b/src/components/templates/HomeTemplate/HomeTemplate.tsx @@ -1,14 +1,18 @@ +import { setCurrentDate } from '@/store/reducers'; +import { TAppDispatch, TRootState } from '@/store/state'; +import { IconReact } from '@assets/icons'; import { CalendarBird } from '@components/common/CalendarBird'; import { Spacing } from '@components/common/Spacing'; import { colors } from '@style/global-style'; import { lastDayMonth } from '@utils/calendar'; import dayjs from 'dayjs'; -import { useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; -import { Body, CalendarWrap, Head, TodayText, Wrap } from './Styled'; +import { Body, CalendarWrap, FlexBox, Head, TodayText, Wrap } from './Styled'; export const HomeTemplate = () => { - const today = useMemo(() => new Date(), []); + const { currentDate } = useSelector((state: TRootState) => state.planStore); + const dispatch = useDispatch(); const navigate = useNavigate(); const location = useLocation(); @@ -21,16 +25,44 @@ export const HomeTemplate = () => { } }; + const handleClickArrowLeft = () => { + dispatch(setCurrentDate(dayjs(currentDate).subtract(1, 'month').format())); + }; + + const handleClickArrowRight = () => { + dispatch(setCurrentDate(dayjs(currentDate).add(1, 'month').format())); + }; + return ( - {dayjs(today).format('YYYY년 MM월')} + + + {dayjs(currentDate).format('YYYY년 MM월')} + + - {lastDayMonth(today)[today.getMonth() + 1]} + {lastDayMonth(currentDate)[currentDate.getMonth() + 1]} diff --git a/src/components/templates/HomeTemplate/Month/MonthTemplate.tsx b/src/components/templates/HomeTemplate/Month/MonthTemplate.tsx index c46de41..3f74c57 100644 --- a/src/components/templates/HomeTemplate/Month/MonthTemplate.tsx +++ b/src/components/templates/HomeTemplate/Month/MonthTemplate.tsx @@ -1,13 +1,76 @@ +import { setCurrentDate } from '@/store/reducers'; +import { TAppDispatch, TRootState } from '@/store/state'; +import { EAchievementStatus } from '@api/types'; +import { IconBook, IconDayBirdMini, IconSuccess } from '@assets/icons'; import { Calendar } from '@components/common/Calendar'; import { Spacing } from '@components/common/Spacing'; -import { Fragment } from 'react'; +import { dummy } from '@mocks/index'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { DefinitionList, FlexBox, GuideLabel, GuideText, Section, SubTitle, Wrap } from './Styled'; + +const data = { + before: '이 날짜에 등록된 플랜이 없어요.', + [EAchievementStatus.failed]: '플랜을 달성하지 못했어요.', + [EAchievementStatus.unstable]: '한 개 이상의 플랜을 달성했어요!', + [EAchievementStatus.success]: '모든 플랜을 달성했어요!' +}; export const MonthTemplate = () => { + const { currentDate } = useSelector((state: TRootState) => state.planStore); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const changeCurrentDate = useCallback( + (date: string) => { + dispatch(setCurrentDate(date)); + navigate('/'); + }, + [dispatch, navigate] + ); + return ( - - - - 캘린더 디자인 수정 예정 - + +
+ + + 날짜를 터치하면 해당 플랜으로 이동합니다. + + {Object.entries(data).map(([key, value]) => ( + +
+ +
+ {value} +
+ ))} +
+
+ {currentDate.getMonth() + 1}월의 플랜 달성 기록 + + + + + + 100% 달성한 플랜 + + 00개 + + + + + + + 완독에 성공한 책 + + 00권 + +
+
); }; diff --git a/src/components/templates/HomeTemplate/Month/Styled.tsx b/src/components/templates/HomeTemplate/Month/Styled.tsx new file mode 100644 index 0000000..3f4e84d --- /dev/null +++ b/src/components/templates/HomeTemplate/Month/Styled.tsx @@ -0,0 +1,81 @@ +import styled from 'styled-components'; + +export const Wrap = styled.div` + width: 100%; + height: 100%; + padding: 20px 30px 0; + + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; +`; + +export const Section = styled.section` + width: 100%; + + display: flex; + flex-direction: column; + + &.last { + min-height: 150px; + } +`; + +export const GuideLabel = styled.span` + display: inline-block; + width: 100%; + height: 32px; + line-height: 32px; + border-radius: 8px; + text-align: center; + background-color: ${({ theme }) => theme.colors.lightGray}; + + font-size: 14px; + font-weight: 400; + color: white; +`; + +export const DefinitionList = styled.dl` + width: 100%; + display: flex; + align-items: center; + gap: 5px; + + margin-bottom: 2px; + + dt { + display: flex; + align-items: center; + } +`; + +export const GuideText = styled.dd` + font-size: 12px; + font-weight: 400; + color: ${({ theme }) => theme.colors.darkGray}; +`; + +export const SubTitle = styled.h1` + font-size: 24; + font-weight: 700; + color: #000000; +`; + +export const FlexBox = styled.div<{ $justify?: 'space-between' }>` + display: flex; + justify-content: ${({ $justify }) => $justify}; + align-items: center; + + span { + font-size: 16px; + font-weight: 500; + color: #747474; + } + + strong { + font-size: 24; + font-weight: 700; + color: #000000; + } +`; diff --git a/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx b/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx index e385631..6ecec1d 100644 --- a/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx +++ b/src/components/templates/HomeTemplate/Plan/Dots/Dots.tsx @@ -35,7 +35,7 @@ export const Dots = ({ planId }: TProps) => { text: '* 삭제된 플랜은 마이페이지에서 2주동안 보관됩니다.', action: (result) => { if (result.isConfirmed) { - Alert.success({ text: '삭제되었습니다!' }); + Alert.success({ title: '삭제되었습니다!' }); } setOpen(null); } diff --git a/src/components/templates/HomeTemplate/Styled.tsx b/src/components/templates/HomeTemplate/Styled.tsx index e2518ba..ea48310 100644 --- a/src/components/templates/HomeTemplate/Styled.tsx +++ b/src/components/templates/HomeTemplate/Styled.tsx @@ -24,6 +24,7 @@ export const Body = styled.section` position: relative; flex: 1; width: 100%; + height: calc(100vh - 160px); border-radius: 50px 50px 0 0%; background-color: ${({ theme }) => theme.colors.white}; @@ -36,6 +37,7 @@ export const TodayText = styled.strong` font-size: 24px; font-weight: 700; color: ${({ theme }) => theme.colors.white}; + user-select: none; `; export const CalendarWrap = styled.div` @@ -52,3 +54,16 @@ export const CalendarWrap = styled.div` transform: scale(1.1); } `; + +export const FlexBox = styled.div<{ $view: boolean }>` + width: 100%; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + gap: 7px; + + .arrow { + display: ${({ $view }) => ($view ? 'block' : 'none')}; + } +`; diff --git a/src/components/templates/HomeTemplate/Week/Styled.tsx b/src/components/templates/HomeTemplate/Week/Styled.tsx index d270775..c32730e 100644 --- a/src/components/templates/HomeTemplate/Week/Styled.tsx +++ b/src/components/templates/HomeTemplate/Week/Styled.tsx @@ -3,8 +3,6 @@ import styled from 'styled-components'; export const Wrap = styled.div` width: 100%; height: 100%; - /* head(90px) + nav(70px) = 160px */ - max-height: calc(100vh - 160px); display: flex; flex-direction: column; align-items: center; @@ -41,6 +39,14 @@ export const PlanListBox = styled.ul` li { flex: 0 0 95px; width: 100%; + + &.last { + flex: 0 0 24px; + width: 100%; + + display: flex; + justify-content: center; + } } `; @@ -75,3 +81,14 @@ export const AddPlanWrap = styled.div` transform: scale(1.1); } `; + +export const PageProgress = styled.span` + display: inline-block; + line-height: 24px; + padding: 0 8px; + font-size: 12px; + font-weight: 400; + color: white; + background-color: ${({ theme }) => theme.colors.lightGray}; + border-radius: 15px; +`; diff --git a/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx b/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx index 38df3e4..84df370 100644 --- a/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx +++ b/src/components/templates/HomeTemplate/Week/WeekTemplate.tsx @@ -1,89 +1,24 @@ -import { EAchievementStatus, ERecordStatus, TPlanRecord } from '@api/types'; +import { TRootState } from '@/store/state'; import { IconPlus } from '@assets/icons'; import { IconEggNoPlan, IconEggOnePlan, IconEggThreePlan, IconEggTwoPlan } from '@assets/images'; -import { CustomCalendar } from '@components/common/Calendar'; +import { WeekCalendar } from '@components/common/Calendar'; import { Spacing } from '@components/common/Spacing'; import { Plan } from '@components/templates/HomeTemplate/Plan'; -import { AddPlanWrap, EmptyPlan, PlanListBox, PlanVisualBox, Wrap } from './Styled'; - -const calendarDummy: TPlanRecord[] = [ - { - createdAt: '2023-12-10', - status: EAchievementStatus.success - }, - { - createdAt: '2023-12-11', - status: EAchievementStatus.failed - }, - { - createdAt: '2023-12-12', - status: EAchievementStatus.unstable - }, - { - createdAt: '2023-12-13', - status: null - }, - { - createdAt: '2023-12-14', - status: null - }, - { - createdAt: '2023-12-15', - status: null - }, - { - createdAt: '2023-12-16', - status: null - } -]; - -const planDummy = [ - { - planId: 1, - title: '나의 라임 오렌지 나무', - author: 'author', - coverImage: 'url', - totalPage: 100, - currentPage: 80, - target: 30, - endDate: '2023-12-21', - planStatus: ERecordStatus.inProgress, - recordStatus: ERecordStatus.inProgress - }, - { - planId: 2, - title: '다빈치코드', - author: 'author', - coverImage: 'url', - totalPage: 100, - currentPage: 30, - target: 10, - endDate: '2023-12-15', - planStatus: ERecordStatus.inProgress, - recordStatus: ERecordStatus.inProgress - }, - { - planId: 3, - title: '천사와 악마', - author: 'author', - coverImage: 'url', - totalPage: 100, - currentPage: 1, - target: 30, - endDate: '2023-12-30', - planStatus: ERecordStatus.inProgress, - recordStatus: ERecordStatus.inProgress - } -]; +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'; export const WeekTemplate = () => { + const { currentDate } = useSelector((state: TRootState) => state.planStore); + const handleClickAdd = () => { // 모달 띄움 }; return ( - + { @@ -92,23 +27,28 @@ export const WeekTemplate = () => { 1: , 2: , 3: - }[planDummy?.length ?? 0] + }[dummy.plan?.length ?? 0] } - {!!planDummy?.length ? ( + {!!dummy.plan?.length ? ( - {planDummy.map((plan) => ( + {dummy.plan.map((plan) => (
  • ))} +
  • + + {dummy.plan?.length}/{MAX_CREATION_COUNT} + +
  • ) : ( 아직 읽고 있는 책이 없어요. )} {/* 총 등록개수가 3개가 되면 버튼 숨김 */} - {(planDummy?.length ?? 0) < 3 && ( + {(dummy.plan?.length ?? 0) < 3 && ( diff --git a/src/components/templates/LoadingTemplate/LoadingTemplate.tsx b/src/components/templates/LoadingTemplate/LoadingTemplate.tsx index 9c2a6ae..c553cf8 100644 --- a/src/components/templates/LoadingTemplate/LoadingTemplate.tsx +++ b/src/components/templates/LoadingTemplate/LoadingTemplate.tsx @@ -1,17 +1,24 @@ +import { Loading } from '@components/common/Loading'; import styled from 'styled-components'; export const LoadingTemplate = () => { - return 로딩중...; + return ( + + + + ); }; const Wrap = styled.div` - position: fixed; + position: absolute; top: 0; left: 0; width: 100%; height: 100%; + z-index: 100; display: flex; align-items: center; justify-content: center; + background-color: #f5f5f520; `; diff --git a/src/constants/plan.ts b/src/constants/plan.ts new file mode 100644 index 0000000..6ca5154 --- /dev/null +++ b/src/constants/plan.ts @@ -0,0 +1,2 @@ +export const MAX_CREATION_COUNT = 3; +export const DAY_OF_WEEK = ['일', '월', '화', '수', '목', '금', '토']; diff --git a/src/mocks/index.ts b/src/mocks/index.ts new file mode 100644 index 0000000..9c269e9 --- /dev/null +++ b/src/mocks/index.ts @@ -0,0 +1 @@ +export * as dummy from './planRecord'; diff --git a/src/mocks/planRecord.ts b/src/mocks/planRecord.ts new file mode 100644 index 0000000..958211f --- /dev/null +++ b/src/mocks/planRecord.ts @@ -0,0 +1,96 @@ +import { EAchievementStatus, ERecordStatus, TPlanRecord } from '@api/types'; +import { lastDayMonth } from '@utils/calendar'; + +// 주간 캘린더 +export const weekCalendar: TPlanRecord[] = [ + { + createdAt: '2023-12-10', + status: EAchievementStatus.success + }, + { + createdAt: '2023-12-11', + status: EAchievementStatus.failed + }, + { + createdAt: '2023-12-12', + status: EAchievementStatus.unstable + }, + { + createdAt: '2023-12-13', + status: null + }, + { + createdAt: '2023-12-14', + status: null + }, + { + createdAt: '2023-12-15', + status: null + }, + { + createdAt: '2023-12-16', + status: null + } +]; + +export const monthCalendar = (currDate: Date): TPlanRecord[] => { + let calendar: TPlanRecord[] = []; + const lastDay = lastDayMonth(currDate)[currDate.getMonth() + 1]; + + const status: string[] = ['before']; + for (let key in EAchievementStatus) { + status.push(key); + } + + const yearMonth = `${currDate.getFullYear()}-${currDate.getMonth() + 1}`; + for (let i = 1; i <= lastDay; i++) { + const random = Math.floor(Math.random() * status.length + 1); + const data = status[random] ?? null; + + calendar.push({ + createdAt: `${yearMonth}-${i}`, + status: data as EAchievementStatus | null + }); + } + + return calendar; +}; + +export const plan = [ + { + planId: 1, + title: '나의 라임 오렌지 나무', + author: 'author', + coverImage: 'url', + totalPage: 100, + currentPage: 80, + target: 30, + endDate: '2023-12-21', + planStatus: ERecordStatus.inProgress, + recordStatus: ERecordStatus.inProgress + }, + { + planId: 2, + title: '다빈치코드', + author: 'author', + coverImage: 'url', + totalPage: 100, + currentPage: 30, + target: 10, + endDate: '2023-12-15', + planStatus: ERecordStatus.inProgress, + recordStatus: ERecordStatus.inProgress + }, + { + planId: 3, + title: '천사와 악마', + author: 'author', + coverImage: 'url', + totalPage: 100, + currentPage: 1, + target: 30, + endDate: '2023-12-30', + planStatus: ERecordStatus.inProgress, + recordStatus: ERecordStatus.inProgress + } +]; diff --git a/src/routers/appRouter.tsx b/src/routers/appRouter.tsx index 7d4de30..44b2e23 100644 --- a/src/routers/appRouter.tsx +++ b/src/routers/appRouter.tsx @@ -1,4 +1,5 @@ import { Loadable } from '@components/common/Loadable'; +import { ProtectedLogin } from '@components/common/ProtectedLogin'; import { AppTemplate } from '@components/templates/AppTemplate'; import { lazy } from 'react'; import { createBrowserRouter } from 'react-router-dom'; @@ -20,7 +21,7 @@ const AppNotFound = Loadable( export const appRouter = createBrowserRouter([ { path: '/', - element: , + element: } />, errorElement: , children: [ { @@ -40,7 +41,6 @@ export const appRouter = createBrowserRouter([ } ] }, - { path: 'search', element:
    검색
    , @@ -52,5 +52,10 @@ export const appRouter = createBrowserRouter([ errorElement: } ] + }, + { + path: '/login', + element: 로그인 화면 연결} />, + errorElement: } ]); diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index a0ba152..cd33ed0 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -1 +1,2 @@ +export * from './userPlan'; export * from './userSlice'; diff --git a/src/store/reducers/userPlan.ts b/src/store/reducers/userPlan.ts new file mode 100644 index 0000000..9ccf0b2 --- /dev/null +++ b/src/store/reducers/userPlan.ts @@ -0,0 +1,36 @@ +import { TPlan, TPlanRecord, TPreviouslyFailedPlan } from '@api/types'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; + +type TPlanData = { + weekRecord: TPlanRecord[] | null; + planData: TPlan[] | null; + previouslyFailedPlan: TPreviouslyFailedPlan[] | null; + currentDate: Date; +}; + +const initialState: TPlanData = { + weekRecord: null, + planData: null, + previouslyFailedPlan: null, + currentDate: new Date() +}; + +const planSlice = createSlice({ + name: 'userPlan', + initialState, + reducers: { + setPlan: (state, action: PayloadAction) => { + const { weekRecord, planData, previouslyFailedPlan } = action.payload; + state.weekRecord = weekRecord; + state.planData = planData; + state.previouslyFailedPlan = previouslyFailedPlan; + }, + setCurrentDate: (state, action: PayloadAction) => { + state.currentDate = new Date(action.payload); + } + } +}); + +export const { setPlan, setCurrentDate } = planSlice.actions; + +export const planStore = planSlice.reducer; diff --git a/src/store/state/store.ts b/src/store/state/store.ts index c22562c..db7dcc0 100644 --- a/src/store/state/store.ts +++ b/src/store/state/store.ts @@ -1,10 +1,11 @@ -import { userStore } from '@/store/reducers'; +import { planStore, userStore } from '@/store/reducers'; import { configureStore } from '@reduxjs/toolkit'; export const store = configureStore({ - reducer: { - userStore: userStore - } + reducer: { + userStore: userStore, + planStore: planStore + } }); export type TRootState = ReturnType; diff --git a/src/styles/calendar.css b/src/styles/calendar.css index ee7b49d..e6b036b 100644 --- a/src/styles/calendar.css +++ b/src/styles/calendar.css @@ -1,56 +1,52 @@ -html, -body { - height: 100%; +/* 캘린더 root */ +.react-calendar { + width: 100%; + max-width: 330px; + background: #ffff; + line-height: 1.125em; + border-radius: 10px; } -body { - margin: 0; - font-family: - Segoe UI, - Tahoma, - sans-serif; +/* 달력 날짜 전체 wrap */ +.react-calendar__month-view__days { + display: flex; + flex-wrap: wrap; + gap: 8px 0px; } -.Calendar input, -.Calendar button { - font: inherit; +/* 달력 요일 wrap */ +.react-calendar__month-view__weekdays { + font-size: 14px; + color: #ababab; } -.Calendar header { - background-color: #323639; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); - padding: 20px; - color: white; -} +/* 달력 요일 개별 */ +.react-calendar__month-view__weekdays__weekday { + flex: 1 !important; + min-width: 30px; + min-height: 30px; -.Calendar header h1 { - font-size: inherit; - margin: 0; + display: flex; + justify-content: center; + align-items: center; } -.Calendar__container { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: flex-start; - margin: 10px 0; - padding: 10px; +abbr { + text-decoration: none; } -.Calendar__container > * > * { - margin: 10px; +/* 달력 날짜 개별 */ +.react-calendar__month-view__days__day abbr { + display: none; } -.Calendar__container__content { - display: flex; - max-width: 100%; - flex-basis: 420px; - flex-direction: column; - flex-grow: 100; - align-items: stretch; - padding-top: 1em; +.react-calendar__month-view__days__day { + background-color: transparent; + border: none; + padding: 0; } -.Calendar__container__content .react-calendar { - margin: 0 auto; +/* 달력 날짜 개별 custom child */ +.react-calendar__month-view__days__day div { + flex: 0 0 30px !important; } diff --git a/src/styles/global-style.ts b/src/styles/global-style.ts index 3632787..4a9f4ee 100644 --- a/src/styles/global-style.ts +++ b/src/styles/global-style.ts @@ -6,6 +6,7 @@ export const GlobalStyle = createGlobalStyle` } #root { + position: relative; max-width: 390px; height: 100%; margin: 0 auto; diff --git a/src/styles/sweet-alert.css b/src/styles/sweet-alert.css index 0584757..5c1a14b 100644 --- a/src/styles/sweet-alert.css +++ b/src/styles/sweet-alert.css @@ -1,20 +1,26 @@ div.swal2-popup { - padding: 10px 0px 20px; - width: 320px; - min-height: 170px; - background-color: #ededed; + padding: 30px 10px 12px; + width: 340px; + min-height: 190px; + background-color: #ffffff; } div.swal2-html-container#swal2-html-container { font-size: 13px; font-weight: 400; - color: #333333; + color: #ababab; text-align: center; + margin: 0; +} + +div.swal2-icon { + margin: 0 auto; } h2.swal2-title { - font-size: 18px; + font-size: 16px; font-weight: 700; + color: #000000; } div.swal2-validation-message { @@ -25,24 +31,28 @@ div.swal2-validation-message { word-break: break-word; } +div.swal2-actions { + width: 100%; +} + button.swal2-styled { width: 130px; height: 40px; - border-radius: 5px; + border-radius: 10px; } button.swal2-styled.swal2-confirm { - background-color: #3498db; + background-color: #b780db; } button.swal2-styled.swal2-confirm:focus { - box-shadow: 0 0 0 3px #3498db40; + box-shadow: 0 0 0 3px #b780db40; } button.swal2-styled.swal2-cancel { - background-color: #bebebe; + background-color: #cfcfcf; } button.swal2-styled.swal2-cancel:focus { - box-shadow: 0 0 0 3px #bebebe40; + box-shadow: 0 0 0 3px #cfcfcf40; } diff --git a/src/utils/Alert/Alert.ts b/src/utils/Alert/Alert.ts index 671a9f0..9653be5 100644 --- a/src/utils/Alert/Alert.ts +++ b/src/utils/Alert/Alert.ts @@ -10,7 +10,6 @@ const template = ({ action, failed, ...args }: TSwalTemplate) => { const confirm: TSwal = (args) => { template({ - icon: 'question', showCancelButton: true, confirmButtonText: '확인', cancelButtonText: '취소', diff --git a/src/utils/classname/function.ts b/src/utils/classname/function.ts index 21e9afd..220c4ab 100644 --- a/src/utils/classname/function.ts +++ b/src/utils/classname/function.ts @@ -1,3 +1,5 @@ +import { EAchievementStatus } from '@api/types'; + // className 조합 함수 type TClassArgs = (string | Record | undefined)[]; export const cls = (...classNames: TClassArgs) => { @@ -22,3 +24,26 @@ export const cls = (...classNames: TClassArgs) => { return names.join(' '); }; + +// 캘린더 색상 적용을 위한 classname 산출 +export const getClassByStatus = ( + date: Date, + status: EAchievementStatus | null, + today = new Date() +) => { + const recordDate = date; + + if (recordDate.getMonth() !== today.getMonth()) { + return 'after-today'; + } + + if (recordDate.getDate() === today.getDate()) { + return 'today'; + } + + if (status === null) { + return 'before'; + } + + return status; +}; diff --git a/src/utils/function.ts b/src/utils/function.ts index 1b4d759..d358343 100644 --- a/src/utils/function.ts +++ b/src/utils/function.ts @@ -23,3 +23,14 @@ export const go = (obj: Record) => { return queryString; }; + +// list 데이터 map 변환 함수 +export const convertMap = (list: T[], key: keyof T) => { + const map = new Map(); + + for (const data of list) { + map.set(data[key], data); + } + + return map; +}; diff --git a/tsconfig.paths.json b/tsconfig.paths.json index ebf54e0..875c0a1 100644 --- a/tsconfig.paths.json +++ b/tsconfig.paths.json @@ -1,17 +1,18 @@ { - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@api/*": ["src/api/*"], - "@assets/*": ["src/assets/*"], - "@components/*": ["src/components/*"], - "@constants/*": ["src/constants/*"], - "@hooks/*": ["src/hooks/*"], - "@pages/*": ["src/pages/*"], - "@routers/*": ["src/routers/*"], - "@style/*": ["src/styles/*"], - "@utils/*": ["src/utils/*"], - "@/*": ["src/*"] - } - } + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@api/*": ["src/api/*"], + "@assets/*": ["src/assets/*"], + "@components/*": ["src/components/*"], + "@constants/*": ["src/constants/*"], + "@hooks/*": ["src/hooks/*"], + "@mocks/*": ["src/mocks/*"], + "@pages/*": ["src/pages/*"], + "@routers/*": ["src/routers/*"], + "@style/*": ["src/styles/*"], + "@utils/*": ["src/utils/*"], + "@/*": ["src/*"] + } + } }