diff --git a/public/assets/result/character-bg-gradient.svg b/public/assets/result/character-bg-gradient.svg index 66aff43d..362a7084 100644 --- a/public/assets/result/character-bg-gradient.svg +++ b/public/assets/result/character-bg-gradient.svg @@ -1,28 +1,28 @@ - - - + + + - - + + - - + + - + - + - + - + - + - + diff --git a/src/apis/getQueryKey.ts b/src/apis/getQueryKey.ts index 2e6e5a78..332c7bbe 100644 --- a/src/apis/getQueryKey.ts +++ b/src/apis/getQueryKey.ts @@ -40,6 +40,9 @@ type QueryList = { feed: { memberId: number; }; + missionSummaryList: { + date: string; + }; }; /** diff --git a/src/apis/result.ts b/src/apis/result.ts new file mode 100644 index 00000000..7e0477e9 --- /dev/null +++ b/src/apis/result.ts @@ -0,0 +1,36 @@ +import getQueryKey from '@/apis/getQueryKey'; +import apiInstance from '@/apis/instance.api'; +import { type MissionCategory, type MissionStatus } from '@/apis/schema/mission'; +import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; + +interface MissionSummaryType { + missionId: number; + name: string; + category: MissionCategory; + visibility: string; + missionStatus: MissionStatus; +} + +interface MissionSummaryListResponse { + missionAllCount: number; + missionCompleteCount: number; + missionNoneCount: number; + missionSummaryItems: MissionSummaryType[]; +} + +const RESULT_API = { + getMissionSummaryList: async (date: string): Promise => { + const { data } = await apiInstance.get(`/missions/summary-list?date=${date}`); + return data; + }, +}; + +export default RESULT_API; + +export const useGetMissionSummaryList = (date: string, option?: UseQueryOptions) => { + return useQuery({ + queryKey: getQueryKey('missionSummaryList', { date }), + queryFn: () => RESULT_API.getMissionSummaryList(date), + ...option, + }); +}; diff --git a/src/app/home/FollowList.tsx b/src/app/home/FollowList.tsx index a96623a8..f91b54a2 100644 --- a/src/app/home/FollowList.tsx +++ b/src/app/home/FollowList.tsx @@ -56,11 +56,9 @@ export default FollowList; const containerCss = flex({ overflowY: 'auto', - padding: '16px 0', - paddingBottom: '20px', + padding: '16px 16px 20px', gap: '12px', alignItems: 'stretch', - margin: '0 16px', _scrollbar: { display: 'none', }, diff --git a/src/app/home/ProfileItem.tsx b/src/app/home/ProfileItem.tsx index d08523e2..e126e8bb 100644 --- a/src/app/home/ProfileItem.tsx +++ b/src/app/home/ProfileItem.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { type FollowDataState } from '@/app/page'; import Thumbnail from '@/components/Thumbnail/Thumbnail'; +import { oneLineTextCss } from '@/constants/style/text'; import { css, cx } from '@styled-system/css'; interface Props { @@ -29,6 +30,7 @@ function ProfileItem(props: Props) { - {isLoading ? ( - // TODO : 스켈레톤 추가 -
- ) : ( - <> - - - - - - - - - - )} - - ); -} - -export default OverallStatus; - -const bannerSectionCss = grid({ - gridTemplateColumns: '1fr 1fr', - padding: '20px 16px', - gap: '10px', - maxWidth: '376px', - margin: '0 auto', -}); - -const imageSectionCss = css({ - margin: '43px auto 12px', - position: 'relative', - height: '210px', -}); diff --git a/src/app/result/OverallStatus/BannerSection.tsx b/src/app/result/OverallStatus/BannerSection.tsx new file mode 100644 index 00000000..8ca325e8 --- /dev/null +++ b/src/app/result/OverallStatus/BannerSection.tsx @@ -0,0 +1,46 @@ +import Banner from '@/components/Banner/Banner'; +import { CardBannerSkeleton } from '@/components/Banner/CardBanner'; +import MotionDiv from '@/components/Motion/MotionDiv'; +import { grid } from '@/styled-system/patterns'; + +interface Props { + totalTime: string; + totalMissionAttainRate: string; +} + +function BannerSection(props: Props) { + return ( + + + + + ); +} + +export default BannerSection; + +export function BannerSectionSkeleton() { + return ( +
+ + +
+ ); +} + +const bannerSectionCss = grid({ + gridTemplateColumns: '1fr 1fr', + padding: '20px 16px', + gap: '10px', + margin: '0 auto', +}); diff --git a/src/app/result/OverallStatus/Calendar.tsx b/src/app/result/OverallStatus/Calendar.tsx new file mode 100644 index 00000000..11aa6abe --- /dev/null +++ b/src/app/result/OverallStatus/Calendar.tsx @@ -0,0 +1,147 @@ +import CalendarItem from '@/app/result/OverallStatus/CalendarItem'; +import Icon from '@/components/Icon'; +import { WEEK_DAYS } from '@/components/MissionDetail/MissionCalender/MissionCalendar.constants'; +import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog'; +import useCalendar from '@/hooks/useCalendar'; +import { eventLogger } from '@/utils'; +import { css, cx } from '@styled-system/css'; +import dayjs, { type Dayjs } from 'dayjs'; + +interface Props { + selectDate: Dayjs; + setSelectDate: (date: Dayjs) => void; +} + +function MissionCalendar({ selectDate, setSelectDate }: Props) { + const currentData = dayjs(); + + const { date, monthCalendarData, onPrevMonth, onNextMonth, isCurrentMonth } = useCalendar({ + currentData, + isQueryParams: true, + }); + + const currentYear = date.year(); + const currentMonth = date.month() + 1; + + const handlePrevMonth = () => { + eventLogger.logEvent(EVENT_LOG_CATEGORY.RESULT, EVENT_LOG_NAME.RESULT.CLICK_CALENDER_ARROW, { + direction: 'prev', + }); + onPrevMonth(); + }; + + const handleNextMonth = () => { + eventLogger.logEvent(EVENT_LOG_CATEGORY.RESULT, EVENT_LOG_NAME.RESULT.CLICK_CALENDER_ARROW, { + direction: 'next', + }); + onNextMonth(); + }; + + return ( +
+
+
+ +
+ + {currentYear}년 {currentMonth}월 + + {/* TODO : 나중에 넣어도 되려나 */} + {/* */} +
+ +
+ + + + {WEEK_DAYS.map((day) => ( + + ))} + + + + {monthCalendarData.map((week, i) => ( + + {week.map((day, index) => { + if (!day) return + ))} + +
{day}
; + + // const isToday = dayjs().isSame(`${day.year}-${day.month}-${day.date}`, 'day'); + const isSelected = selectDate.isSame(`${day.year}-${day.month}-${day.date}`, 'day'); + const thisDay = `${day.year}-${day.month}-${day.date}`; + + return ( + { + setSelectDate(dayjs(thisDay)); + }} + > + {day.date} + + ); + })} +
+
+
+
+ ); +} + +export default MissionCalendar; + +const dateLabeWrapperCss = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '12px', + height: '36px', +}); + +const buttonCss = css({ + padding: '8px', +}); + +const tableCss = css({ + width: '100%', + textAlign: 'center', + borderSpacing: '0 8px', +}); + +const dateLabelTextCss = css({ + textStyle: 'subtitle2', + color: 'text.secondary', + display: 'flex', + alignItems: 'center', + gap: '4px', +}); + +const missionCalendarTdCss = css({ + padding: ' 12px 0', +}); + +const calendarHeaderCss = css({ + width: '100%', + fontSize: '12px', + fontWeight: '400', + lineHeight: '18px', + color: 'text.secondary', + justifyContent: 'space-between', + height: '40px', +}); + +const calendarBodyCss = css({ + textStyle: 'body6', + color: 'text.tertiary', +}); diff --git a/src/app/result/OverallStatus/CalendarItem.tsx b/src/app/result/OverallStatus/CalendarItem.tsx new file mode 100644 index 00000000..d0e3d54f --- /dev/null +++ b/src/app/result/OverallStatus/CalendarItem.tsx @@ -0,0 +1,72 @@ +import { type PropsWithChildren } from 'react'; +import { css, cx } from '@/styled-system/css'; + +interface Props { + isSelected?: boolean; + onClick: () => void; +} + +function CalendarItem(props: PropsWithChildren) { + return ( + + + {props.children} + + + ); +} + +export default CalendarItem; + +const itemCss = css({ + position: 'relative', + textStyle: 'subtitle3', + width: '40px', + height: '40px', + cursor: 'pointer', + + '&::before': { + content: '""', + display: 'block', + position: 'absolute', + top: '50%', + left: '50%', + width: '40px', + height: '40px', + borderRadius: '50%', + border: '1px solid', + borderColor: 'purple.purple700 !', + transform: 'translate(-50%, -50%)', + pointerEvents: 'none', + background: + 'linear-gradient(0deg, rgba(0, 0, 0, 0.87), rgba(0, 0, 0, 0.87)), linear-gradient(0deg, #FFFFFF, #FFFFFF)', + zIndex: 0, + + opacity: 0, + transition: 'opacity 0.3s', + }, +}); + +const selectedCss = css({ + '&::before': { + opacity: 1, + }, +}); + +const textCss = css({ + position: 'relative', + zIndex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '100%', + transition: 'color 0.3s', +}); diff --git a/src/app/result/OverallStatus/ImageAreaSection.tsx b/src/app/result/OverallStatus/ImageAreaSection.tsx new file mode 100644 index 00000000..123a567d --- /dev/null +++ b/src/app/result/OverallStatus/ImageAreaSection.tsx @@ -0,0 +1,117 @@ +'use client'; + +import Image from 'next/image'; +import Character from '@/app/level/guide/Character'; +import Icon from '@/components/Icon'; +import MotionDiv from '@/components/Motion/MotionDiv'; +import ProgressBar from '@/components/ProgressBar'; +import { gradientTextCss } from '@/constants/style/gradient'; +import { css, cx } from '@/styled-system/css'; +import { flex } from '@/styled-system/patterns'; +import { getLevel, getPercent } from '@/utils/result'; + +interface Props { + symbolStack?: number; +} + +function ImageAreaSection({ symbolStack }: Props) { + // TODO: skeleton 추가 + if (symbolStack === undefined) { + return
; + } + + const currentLevel = getLevel(symbolStack); + const { max, min } = currentLevel; + const percent = getPercent({ max, min, symbolStack }); + + return ( +
+
+
+ {symbolStack} + +
+

{currentLevel.label}

+ +
+ + +
+ gradient-bg +
+ +
+
+ ); +} + +export default ImageAreaSection; + +const levelWrapperCss = flex({ + gap: '8px', + alignItems: 'center', + height: '43px', +}); + +const levelLabelCss = css({ + fontSize: '36px', + lineHeight: '43px', + fontWeight: '100', +}); + +const sectionCss = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + height: '219px', + paddingLeft: '32px', + paddingRight: '24px', +}); + +const levelTextCss = css({ + margin: '12px 0 10px', + color: '#fff', + height: '20px', + textStyle: 'body4', +}); + +const levelStatusCss = css({ + width: '93px', + '& > div': { + width: '93px', + }, +}); + +const imageSectionCss = css({ + position: 'relative', + width: '200px', + height: '150px', +}); + +const characterBgCss = css({ + width: '255px', + height: '260px', + position: 'absolute', + top: '-41px', + left: '50%', + transform: 'translateX(-50%)', +}); + +export function ImageSectionSkeleton() { + return ( +
+
+
+

+

+
+
+ ); +} + +const skeletonCss = css({ + width: '100%', + animation: 'skeleton', + borderRadius: '12px', + backgroundColor: 'bg.surface4', +}); diff --git a/src/app/result/OverallStatus/MissionList.tsx b/src/app/result/OverallStatus/MissionList.tsx new file mode 100644 index 00000000..cb44b09e --- /dev/null +++ b/src/app/result/OverallStatus/MissionList.tsx @@ -0,0 +1,100 @@ +import { useGetMissionSummaryList } from '@/apis/result'; +import MissionBadge from '@/app/home/MissionBadge'; +import { TwoLineListItem } from '@/components/ListItem'; +import { MISSION_CATEGORY_LABEL } from '@/constants/mission'; +import { css } from '@/styled-system/css'; +import { motion } from 'framer-motion'; + +interface Props { + selectDate: string; +} + +function MissionList(props: Props) { + const { data: selectSummaryListData, isLoading } = useGetMissionSummaryList(props.selectDate); + + const missionList = selectSummaryListData?.missionSummaryItems ?? []; + + return ( +
+
+
+ + 전체 + + + {selectSummaryListData?.missionAllCount ?? ' '} + +
+
+ 성공 + {selectSummaryListData?.missionCompleteCount ?? ' '} +
+
+ 미완료 + {selectSummaryListData?.missionNoneCount ?? ' '} +
+
+ {isLoading ? ( +
+ ) : ( + + {missionList.length === 0 &&
등록된 미션 내역이 없습니다.
} + {missionList.map((item) => ( + } + name={item.name} + subName={MISSION_CATEGORY_LABEL[item.category].label} + imageUrl={MISSION_CATEGORY_LABEL[item.category].imgUrl} + isBackground={false} + /> + ))} +
+ )} +
+ ); +} + +export default MissionList; + +const sectionCss = css({ + backgroundColor: 'bg.surface1', + borderRadius: '20px', + padding: '20px', + marginTop: '10px', + height: '302px', + display: 'flex', + flexDirection: 'column', + gap: '12px', +}); + +const emptyTextCss = css({ + textStyle: 'subtitle3', + color: 'text.quaternary', + textAlign: 'center', + marginTop: '88px', +}); + +const infoWrapperCss = css({ + display: 'flex', + gap: '8px', + + '& div': { + display: 'flex', + gap: '4px', + }, + '& span': { + textStyle: 'body4', + color: 'text.secondary', + minWidth: '10px', + }, + '& b': { + color: 'purple.purple700', + minWidth: '10px', + }, +}); + +const listCss = css({ + flex: 1, + overflowY: 'auto', +}); diff --git a/src/app/result/OverallStatus/MissionSection.tsx b/src/app/result/OverallStatus/MissionSection.tsx new file mode 100644 index 00000000..23107984 --- /dev/null +++ b/src/app/result/OverallStatus/MissionSection.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import { css } from '@/styled-system/css'; +import dayjs from 'dayjs'; + +import MissionCalendar from './Calendar'; +import MissionList from './MissionList'; + +function MissionSection() { + const [selectDate, setSelectDate] = useState(dayjs()); + + const formatSelectDate = selectDate.format('YYYY-MM-DD'); + + return ( +
+ + + +
+
+ ); +} + +export default MissionSection; + +const containerCss = css({ + margin: '6px 12px 10px', +}); + +const blankCss = css({ + height: '156px', +}); diff --git a/src/app/result/OverallStatus/index.tsx b/src/app/result/OverallStatus/index.tsx new file mode 100644 index 00000000..10460272 --- /dev/null +++ b/src/app/result/OverallStatus/index.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useGetMissionSummary } from '@/apis/mission'; +import BannerSection, { BannerSectionSkeleton } from '@/app/result/OverallStatus/BannerSection'; +import ImageAreaSection, { ImageSectionSkeleton } from '@/app/result/OverallStatus/ImageAreaSection'; +import MissionSection from '@/app/result/OverallStatus/MissionSection'; + +function OverallStatus() { + const { data, isLoading } = useGetMissionSummary(); + + const totalTime = `${data?.totalMissionHour ?? 0}h ${data?.totalMissionMinute ?? 0}m`; + const totalMissionAttainRate = `${data?.totalMissionAttainRate ?? 0}%`; + + if (isLoading) { + return ( + <> + + + + ); + } + + return ( + <> + + v + + + ); +} + +export default OverallStatus; diff --git a/src/app/result/layout.tsx b/src/app/result/layout.tsx new file mode 100644 index 00000000..2c19f01a --- /dev/null +++ b/src/app/result/layout.tsx @@ -0,0 +1,15 @@ +import { type PropsWithChildren } from 'react'; +import { css } from '@/styled-system/css'; + +function Layout({ children }: PropsWithChildren) { + return
{children}
; +} + +export default Layout; + +const mainCss = css({ + maxWidth: 'maxWidth', + margin: '0 auto', + minHeight: '100vh', + overflowX: 'hidden', +}); diff --git a/src/app/result/page.tsx b/src/app/result/page.tsx index ef6430cc..ad34d35a 100644 --- a/src/app/result/page.tsx +++ b/src/app/result/page.tsx @@ -1,16 +1,17 @@ 'use client'; +import Link from 'next/link'; import { useGetFinishedMissions } from '@/apis/mission'; import FinishedMissionList from '@/app/result/FinishedMissionList'; import OverallStatus from '@/app/result/OverallStatus'; import { ResultTabId } from '@/app/result/result.constants'; import AppBarBottom from '@/components/AppBarBottom/AppBarBottom'; -import LinkButton from '@/components/Button/LinkButton'; import Tab from '@/components/Tab/Tab'; import { useTab } from '@/components/Tab/Tab.hooks'; import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog'; import { ROUTER } from '@/constants/router'; import useSearchParamsTypedValue from '@/hooks/useSearchParamsTypedValue'; +import { css } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; import { eventLogger } from '@/utils'; @@ -20,8 +21,32 @@ const handleLevelGuideClick = () => { function ResultPage() { const finishedMissionQueryData = useGetFinishedMissions(); - const finishedMissionCount = finishedMissionQueryData.data?.length ?? ''; + const finishedMissionCount = finishedMissionQueryData.data?.length ?? 0; + const tabProps = useResultTab(finishedMissionCount); + + return ( + <> +
+
+ +
+ + 레벨 안내 + +
+ {tabProps.activeTab === ResultTabId.OVERALL_STATUS && } + {tabProps.activeTab === ResultTabId.FINISHED_MISSION && ( + + )} + + + ); +} + +export default ResultPage; + +const useResultTab = (finishedMissionCount: number) => { const { searchParams } = useSearchParamsTypedValue('tab'); const initTabId = searchParams ?? ResultTabId.OVERALL_STATUS; @@ -40,25 +65,32 @@ function ResultPage() { const tabProps = useTab(tabList, initTabId); - return ( -
-
- - - 레벨 안내 - -
- {tabProps.activeTab === 'overall-status' && } - {tabProps.activeTab === 'finished-mission' && } - -
- ); -} - -export default ResultPage; + return tabProps; +}; const topWrapperCss = flex({ zIndex: 1, position: 'relative', - padding: '16px 16px 4px 16px', + justifyContent: 'space-between', +}); + +const tabWrapperCss = css({ + padding: '28px 16px 4px 16px', +}); + +const levelGuideLinkCss = css({ + color: 'gray.gray800', + border: '1px solid', + borderColor: 'gray.gray500', + borderRadius: '20px', + fontSize: '13px', + fontWeight: '300', + width: 'fit-content', + display: 'block', + padding: '0 12px', + minWidth: '75px', + lineHeight: '28px', + height: '30px', + marginTop: '23px', + marginRight: '16px', }); diff --git a/src/components/Banner/CardBanner.tsx b/src/components/Banner/CardBanner.tsx index 4bf2041a..e137f510 100644 --- a/src/components/Banner/CardBanner.tsx +++ b/src/components/Banner/CardBanner.tsx @@ -1,6 +1,6 @@ import Image from 'next/image'; import { type CardBannerType } from '@/components/Banner/Banner.types'; -import { css } from '@/styled-system/css'; +import { css, cx } from '@/styled-system/css'; function CardBanner(props: CardBannerType) { return ( @@ -18,6 +18,20 @@ function CardBanner(props: CardBannerType) { export default CardBanner; +export function CardBannerSkeleton() { + return ( +
+ ); +} + const containerCss = css({ width: '100%', height: '100%', @@ -29,6 +43,7 @@ const containerCss = css({ display: 'flex', flexDirection: 'column', alignItems: 'center', + border: 'none', }); const outerContainerCss = css({ @@ -38,8 +53,7 @@ const outerContainerCss = css({ padding: '0px !', // NOTE: padding 0 필수, backgroundOrigin: 'border-box', backgroundClip: 'content-box, border-box', - backgroundImage: - 'linear-gradient(token(colors.bg.surface3), token(colors.bg.surface3)), token(colors.gradients.stroke)', + backgroundImage: 'linear-gradient(#18181D, #18181D), linear-gradient(0deg, #474A5D00 0%, #474A5D 100%)', }); const descriptionCss = css({ diff --git a/src/components/Graph/GraphBase.tsx b/src/components/Graph/GraphBase.tsx index 72390116..eb275ec2 100644 --- a/src/components/Graph/GraphBase.tsx +++ b/src/components/Graph/GraphBase.tsx @@ -1,7 +1,10 @@ import { type PropsWithChildren } from 'react'; +import Icon from '@/components/Icon'; +import ProgressBar from '@/components/ProgressBar'; import { gradientTextCss } from '@/constants/style/gradient'; import { css, cx } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; +import { getPercent } from '@/utils/result'; function GraphRoot({ children }: PropsWithChildren) { return
{children}
; @@ -17,103 +20,54 @@ const containerCss = flex({ function SymbolText(props: { symbolStack: number }) { return (
-
- -
+ {props.symbolStack}
); } -const symbolContainerCss = css({ - width: '16px', - height: '16px', - position: 'relative', - - '& svg': { - position: 'absolute', - top: '-8.5px', - left: '-12.5px', - }, -}); - const levelWrapperCss = flex({ gap: '8px', alignItems: 'center', justifyContent: 'center', + height: '43px', }); const levelLabelCss = css({ fontSize: '36px', + lineHeight: '43px', fontWeight: '100', }); -const MIN_PERCENT = 3; +interface ProgressBarBaseProps { + isLabel?: boolean; +} -type LevelProgressBarProps = - | { symbolStack: number; min: number; max: number; isFull?: false } - | { isFull: true; min?: number }; +interface ProgressingProgressBarProps extends ProgressBarBaseProps { + symbolStack: number; + min: number; + max: number; + isFull?: false; +} + +interface FullProgressBarProps extends ProgressBarBaseProps { + isFull: true; + min?: number; +} + +type LevelProgressBarProps = ProgressingProgressBarProps | FullProgressBarProps; function LevelProgressBar(props: LevelProgressBarProps) { if (props?.isFull) { - return ; + return ; } const { symbolStack, max, min } = props; - const percent = (100 / (max - min)) * (symbolStack - min); - - return ; -} + const percent = getPercent({ symbolStack, max, min }); -function ProgressBar(props: { percent: number; labels?: string[] }) { - return ( -
-
-
-
- {props.labels && ( -
- {props.labels.map((label) => ( - {label} - ))} -
- )} -
- ); + return ; } -const progressBarContainerCss = css({ - width: '214px', -}); - -const progressContainerCss = css({ - borderRadius: '10px', - backgroundColor: '#3B3E4F', - width: '100%', - position: 'relative', - height: '4px', -}); - -const progressInnerContainerCss = css({ - position: 'absolute', - borderRadius: '10px', - height: '100%', - transition: 'width .7s ease-in-out', - background: 'gradients.primary', -}); - -const labelContainerCss = flex({ - textStyle: 'body4', - color: 'text.quaternary', - marginTop: '5px', - width: '100%', -}); - function Description({ children }: PropsWithChildren) { return
{children}
; } @@ -129,57 +83,3 @@ export default Object.assign(GraphRoot, { ProgressBar: LevelProgressBar, Description, }); -``; -// TODO: svg(?)로 변경 -function TenMMSymbol() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/components/Icon/NormalCalenderIcon.tsx b/src/components/Icon/NormalCalenderIcon.tsx index a3a8aaa2..e18bd6a7 100644 --- a/src/components/Icon/NormalCalenderIcon.tsx +++ b/src/components/Icon/NormalCalenderIcon.tsx @@ -10,14 +10,14 @@ function NormalCalender(props: IconComponentProps) { diff --git a/src/components/Icon/SymbolFillIcon.tsx b/src/components/Icon/SymbolFillIcon.tsx new file mode 100644 index 00000000..04273705 --- /dev/null +++ b/src/components/Icon/SymbolFillIcon.tsx @@ -0,0 +1,70 @@ +import { css } from '@/styled-system/css'; + +function SymbolFillIcon() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +export default SymbolFillIcon; + +const symbolContainerCss = css({ + width: '16px', + height: '16px', + position: 'relative', + + '& svg': { + position: 'absolute', + top: '-8.5px', + left: '-12.5px', + }, +}); diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 436fe29a..9a3365c0 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -23,6 +23,7 @@ import PlusIcon from '@/components/Icon/PlusIcon'; import RefreshIcon from '@/components/Icon/RefreshIcon'; import ShareIcon from '@/components/Icon/ShareIcon'; import SpinnerIcon from '@/components/Icon/SpinnerIcon'; +import SymbolFillIcon from '@/components/Icon/SymbolFillIcon'; import TENMMSymbolCircleIcon, { TENMMSymbolCircleLocked } from '@/components/Icon/TENMMSymbolCircleIcon'; import TENMMSymbolIcon from '@/components/Icon/TENMMSymbolIcon'; import TermsIcon from '@/components/Icon/TermsIcon'; @@ -83,6 +84,7 @@ export const IconComponentMap = { 'normal-setting': NormalSetting, 'normal-terms': NormalTerms, 'normal-link': NormalLink, + '10mm-symbol-fill': SymbolFillIcon, notification: NotificationIcon, } as const; diff --git a/src/components/ListItem/OneLineListItem.tsx b/src/components/ListItem/OneLineListItem.tsx index 30c392f9..994da4a1 100644 --- a/src/components/ListItem/OneLineListItem.tsx +++ b/src/components/ListItem/OneLineListItem.tsx @@ -1,8 +1,8 @@ import { type ReactNode } from 'react'; import Image from 'next/image'; -import { oneLineTextCss } from '@/components/ListItem/ListItem.styles'; import Thumbnail from '@/components/Thumbnail/Thumbnail'; import { type ThumbnailProps } from '@/components/Thumbnail/Thumbnail.types'; +import { oneLineTextCss } from '@/constants/style/text'; import { css, cx } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; diff --git a/src/components/ListItem/ProfileListItem.tsx b/src/components/ListItem/ProfileListItem.tsx index 3a1c056a..b3b27098 100644 --- a/src/components/ListItem/ProfileListItem.tsx +++ b/src/components/ListItem/ProfileListItem.tsx @@ -1,6 +1,6 @@ import { type ReactNode } from 'react'; -import { oneLineTextCss } from '@/components/ListItem/ListItem.styles'; import Thumbnail from '@/components/Thumbnail/Thumbnail'; +import { oneLineTextCss } from '@/constants/style/text'; import { css, cx } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; diff --git a/src/components/ListItem/TwoLineListItem.tsx b/src/components/ListItem/TwoLineListItem.tsx index 545df3c9..42fed1bd 100644 --- a/src/components/ListItem/TwoLineListItem.tsx +++ b/src/components/ListItem/TwoLineListItem.tsx @@ -1,6 +1,6 @@ import { type ReactNode } from 'react'; import Image from 'next/image'; -import { oneLineTextCss } from '@/components/ListItem/ListItem.styles'; +import { oneLineTextCss } from '@/constants/style/text'; import { css, cx } from '@/styled-system/css'; import { flex } from '@/styled-system/patterns'; @@ -58,5 +58,6 @@ const bgExistContainerCss = css({ const bgNoExistContainerCss = css({ background: 'transparent', - padding: '8px 0', + height: '56px !', + // padding: '8px 0', }); diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx new file mode 100644 index 00000000..8c321113 --- /dev/null +++ b/src/components/ProgressBar.tsx @@ -0,0 +1,58 @@ +import { css, cx } from '@/styled-system/css'; +import { flex } from '@/styled-system/patterns'; + +function ProgressBar(props: { percent: number; labels?: string[]; progressColor?: string }) { + const progressBarColor = props.progressColor || 'gradients.primary'; + + return ( +
+
+
+
+ {props.labels && ( +
+ {props.labels.map((label) => ( + {label} + ))} +
+ )} +
+ ); +} + +export default ProgressBar; + +// TODO: width 100%로 변경 +const progressBarContainerCss = css({ + width: '214px', +}); + +const progressContainerCss = css({ + borderRadius: '10px', + backgroundColor: '#3B3E4F', + width: '100%', + position: 'relative', + height: '4px', +}); + +const progressInnerContainerCss = css({ + position: 'absolute', + borderRadius: '10px', + height: '100%', + transition: 'width .7s ease-in-out', +}); + +const labelContainerCss = flex({ + textStyle: 'body4', + color: 'text.quaternary', + marginTop: '5px', + width: '100%', +}); diff --git a/src/constants/eventLog.ts b/src/constants/eventLog.ts index b9cfc5cf..e179e6d1 100644 --- a/src/constants/eventLog.ts +++ b/src/constants/eventLog.ts @@ -61,6 +61,8 @@ export const EVENT_LOG_NAME = { }, RESULT: { CLICK_MISSION: 'click/levelGuide', + CLICK_CALENDER_ARROW: 'click/calendarArrow', + CLICK_CALENDER: 'click/calendar', }, LEVEL: { CLICK_LEVEL: 'click/level', diff --git a/src/constants/router.ts b/src/constants/router.ts index ef4f4630..15dd2430 100644 --- a/src/constants/router.ts +++ b/src/constants/router.ts @@ -1,3 +1,5 @@ +import { type ResultTabId } from '@/app/result/result.constants'; + export const ROUTER = { HOME: '/', MISSION: { @@ -37,7 +39,7 @@ export const ROUTER = { SETTING: '/mypage/setting_and_private', }, RESULT: { - HOME: (query?: 'overall-status' | 'finished-mission') => `/result${query ? `?tab=${query}` : ''}`, + HOME: (query?: ResultTabId) => `/result${query ? `?tab=${query}` : ''}`, FINISHED_MISSION: (id: number) => `/result/finished/${id}`, }, LEVEL: { diff --git a/src/components/ListItem/ListItem.styles.ts b/src/constants/style/text.ts similarity index 100% rename from src/components/ListItem/ListItem.styles.ts rename to src/constants/style/text.ts diff --git a/src/msw/handlers/result.ts b/src/msw/handlers/result.ts new file mode 100644 index 00000000..15008fcc --- /dev/null +++ b/src/msw/handlers/result.ts @@ -0,0 +1,37 @@ +import { http, HttpResponse } from 'msw'; + +const BASE_URL = process.env.NEXT_PUBLIC_SEVER_API; + +const MissionSummaryListData = { + missionAllCount: 0, + missionCompleteCount: 0, + missionNoneCount: 0, + missionList: [ + { + missionId: 1, + name: 'UX방법론 1챕터씩 공부하기!', + category: 'STUDY', + visibility: 'ALL', + missionStatus: 'COMPLETED', + }, + { + missionId: 2, + name: '스쿼트하고 닭다리 되기!', + category: 'STUDY', + visibility: 'ALL', + missionStatus: 'COMPLETED', + }, + ], +}; + +// 미션 전체 현황 - 리스트 +const getMissionSummaryList = http.get(BASE_URL + '/missions/summary-list', () => { + return HttpResponse.json({ + data: MissionSummaryListData, + }); +}); + +// TODO: 왜 안되지? +const resultHandlers = [getMissionSummaryList]; + +export default resultHandlers; diff --git a/src/utils/result.ts b/src/utils/result.ts index 68f3748f..4c6801a0 100644 --- a/src/utils/result.ts +++ b/src/utils/result.ts @@ -17,3 +17,12 @@ export const calcProgress = (symbolStack: number) => { const currentLevel = getLevel(symbolStack); return ((symbolStack - currentLevel.min) / (currentLevel.max - currentLevel.min)) * 100; }; + +export const getPercent = (props: { symbolStack: number; max: number; min: number }): number => { + const MIN_PERCENT = 3; + + const { symbolStack, max, min } = props; + const percent = (100 / (max - min)) * (symbolStack - min); + + return Math.max(percent, MIN_PERCENT); +};