diff --git a/.env.example b/.env.example deleted file mode 100644 index f6a41f1..0000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -NEXT_PUBLIC_KAKAO_CLIENT_ID= -NEXT_PUBLIC_CLIENT_REDIRECT_URI= -NEXT_PUBLIC_LOCAL_BASE_URL= -NEXT_PUBLIC_KAKAO_MAP_KEY= diff --git a/app/(router)/analysis/amount/page.tsx b/app/(router)/analysis/amount/page.tsx index 1bc78eb..7819db4 100644 --- a/app/(router)/analysis/amount/page.tsx +++ b/app/(router)/analysis/amount/page.tsx @@ -1,5 +1,9 @@ +'use client'; + +import AmountAnalysisWrapper from '@/_components/analysis/amount/AmountAnalysisWrapper'; + const AmountAnalysis = () => { - return

금액별

; + return ; }; export default AmountAnalysis; diff --git a/app/(router)/analysis/period/filter/page.tsx b/app/(router)/analysis/period/filter/page.tsx new file mode 100644 index 0000000..729919f --- /dev/null +++ b/app/(router)/analysis/period/filter/page.tsx @@ -0,0 +1,9 @@ +'use client'; + +import PeriodicDateFilterWrapper from '@/_components/analysis/period/filter/PeriodicDateFilterWrapper'; + +const PeriodAnalysis = () => { + return ; +}; + +export default PeriodAnalysis; diff --git a/app/(router)/analysis/period/pick/page.tsx b/app/(router)/analysis/period/pick/page.tsx deleted file mode 100644 index 31db556..0000000 --- a/app/(router)/analysis/period/pick/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import PeriodicDatePicker from '@/_components/analysis/period/pick/PeriodicDatePicker'; - -const PeriodAnalysis = () => { - return ; -}; - -export default PeriodAnalysis; diff --git a/app/(router)/analysis/rounds/page.tsx b/app/(router)/analysis/rounds/page.tsx index 7a0a3d3..327db5c 100644 --- a/app/(router)/analysis/rounds/page.tsx +++ b/app/(router)/analysis/rounds/page.tsx @@ -1,5 +1,9 @@ +'use client'; + +import RoundsAnalysisWrapper from '@/_components/analysis/rounds/RoundsAnalysisWrapper'; + const RoundsAnalysis = () => { - return

회차별

; + return ; }; export default RoundsAnalysis; diff --git a/app/_components/analysis/AnalysisMainWrapper.tsx b/app/_components/analysis/AnalysisMainWrapper.tsx index 36f0edc..29c4a54 100644 --- a/app/_components/analysis/AnalysisMainWrapper.tsx +++ b/app/_components/analysis/AnalysisMainWrapper.tsx @@ -1,36 +1,79 @@ 'use client'; import palette from '@/_styles/palette'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import ResultAmountBannerList from './ResultAmountBannerList'; import AnalyticsDashboardMenu from './AnalyticsDashboardMenu'; import Image from 'next/image'; import PolygonIcon from '@assets/svg/polygon.svg'; +import ArrowDropDownIcon from '@assets/svg/arrowDropDown.svg'; +import AuthProvider from '../providers/AuthProvider'; +import useLatestNumber from '@/_hooks/useLatestNumber'; +import { parse } from 'date-fns'; +import BottomSheet from '../common/BottomSheet'; +import RoundSelector from './RoundSelector'; +import { LatestNumberResponseType } from '@/_types/analysis'; -type AnalysisMainWrapperProps = {}; +const AnalysisMainWrapper: React.FC = () => { + const { latestNumbers, isLoading } = useLatestNumber(); + const [isOpenScrapBottomSheet, setIsOpenScrapBottomSheet] = useState(false); + const [selectNumbers, setSelectNumbers] = useState(); -const AnalysisMainWrapper: React.FC = () => { - // const test = [3, 5, 7, 9, 1, 13, 4]; //? res값 정제 + useEffect(() => { + if (!latestNumbers) { + return console.log('no number'); + } + setSelectNumbers(latestNumbers); + }, [latestNumbers]); - return ( - - - 7월 2주차 1078회 -
- 당첨번호를 확인하세요. -
+ const parsedDate = parse(`${selectNumbers?.drwt_date}`, 'yyyy-MM-dd', new Date()); + const monthNumber = parsedDate.getMonth() + 1; + const weekNumber = Math.ceil(parsedDate.getDate() / 7); + + if (isLoading) return

loading

; //로딩 UI 필요 + //latestNumbers 없을 때 행동 - - 3 5 7 9 1 13 4{' '} - 34 - - + return ( + + + + + {monthNumber}월 {weekNumber}주차{' '} + setIsOpenScrapBottomSheet(true)}> + {selectNumbers?.drwt_no}회 + + + 당첨번호를 확인하세요. + + + {selectNumbers && + new Array(6) + .fill('') + .map((_, i) => ( + {selectNumbers[`drwt_no${i + 1}`]} + ))} + {selectNumbers?.bnus_no} + + + 분석페이지 + - 분석페이지 - - - + +
+ setIsOpenScrapBottomSheet(true)} + onClose={() => setIsOpenScrapBottomSheet(false)} + title="회차 선택" + > + + + ); }; @@ -42,14 +85,28 @@ const AnalysisMainWrapperBlock = styled.div` `; const AnalysisMainTitle = styled.p` + display: flex; + flex-direction: column; + align-items: center; font-size: 20px; font-weight: bold; line-height: 26px; - text-align: center; padding: 15px 0; margin-bottom: 16px; `; +const Text = styled.p` + display: flex; + gap: 5px; +`; + +const SelectorButton = styled.button` + display: flex; + font-size: 20px; + font-weight: bold; + color: ${palette.orange_20}; +`; + const PolygonIconSVG = styled(PolygonIcon)` position: absolute; bottom: -10px; @@ -64,12 +121,13 @@ const WeekWinningNumberBox = styled.div` border-radius: 400px; margin-bottom: 21px; position: relative; + display: flex; + gap: 10px; `; const WeekWinningNumber = styled.span` color: ${palette.grey_20}; font-size: 20px; - letter-spacing: 6px; font-weight: bold; `; const WeekWinningBonusNumber = styled.span` diff --git a/app/_components/analysis/AnalyticsDashboardMenu.tsx b/app/_components/analysis/AnalyticsDashboardMenu.tsx index cbe5858..085b3dc 100644 --- a/app/_components/analysis/AnalyticsDashboardMenu.tsx +++ b/app/_components/analysis/AnalyticsDashboardMenu.tsx @@ -21,7 +21,7 @@ const AnalyticsDashboardMenu: React.FC = () => { - + 당첨금액별 금액이 높은 순으로 조회하기 diff --git a/app/_components/analysis/ResultAmountBannerList.tsx b/app/_components/analysis/ResultAmountBannerList.tsx index faebaa7..af6a599 100644 --- a/app/_components/analysis/ResultAmountBannerList.tsx +++ b/app/_components/analysis/ResultAmountBannerList.tsx @@ -2,54 +2,31 @@ import React from 'react'; import styled from 'styled-components'; import palette from '@/_styles/palette'; import { Swiper, SwiperSlide } from 'swiper/react'; +import useLatestNumber from '@/_hooks/useLatestNumber'; +import { LatestNumberResponseType } from '@/_types/analysis'; -type ResultAmountBannerListProps = {}; - -const ResultAmountBannerList: React.FC = () => { - const testList = [ - { - count: 1, - amount: 2539849237, - }, - { - count: 1, - amount: 2539849237, - }, - { - count: 1, - amount: 2539849237, - }, - { - count: 1, - amount: 2539849237, - }, - { - count: 1, - amount: 2539849237, - }, - { - count: 1, - amount: 2539849237, - }, - ]; +type ResultAmountBannerListProps = { + roundNumbers: LatestNumberResponseType; +}; +const ResultAmountBannerList: React.FC = ({ roundNumbers }) => { return ( - {testList.map((test, i) => ( - + {roundNumbers && ( + -

{i + 1}등

-

{test.count}명

+

1등

+

{roundNumbers.first_win_count}명

- {test.amount.toLocaleString()}원 + {roundNumbers.first_win_amount.toLocaleString()}원
- ))} + )}
); }; @@ -71,9 +48,10 @@ const ResultAmountBannerListBlock = styled(Swiper)` `; const ResultAmountBannerItem = styled.li` + width: 108px; display: flex; flex-direction: column; - padding: 20px 14px; + padding: 20px 15px; background-color: ${palette.white}; border-radius: 10px; `; diff --git a/app/_components/analysis/RoundSelector.tsx b/app/_components/analysis/RoundSelector.tsx new file mode 100644 index 0000000..b103fc9 --- /dev/null +++ b/app/_components/analysis/RoundSelector.tsx @@ -0,0 +1,56 @@ +import instance from '@/_apis/core'; +import { LatestNumberResponseType } from '@/_types/analysis'; +import React from 'react'; +import styled from 'styled-components'; + +type RoundSelectorProps = { + lastestRound: number; + setIsOpenScrapBottomSheet: React.Dispatch>; + setSelectNumbers: React.Dispatch>; +}; + +const RoundSelector: React.FC = ({ + lastestRound, + setIsOpenScrapBottomSheet, + setSelectNumbers, +}) => { + const selectOptions = Array.from({ length: lastestRound }, (_, index) => lastestRound - index); + + const onSelect = async (drwtNo: number) => { + try { + const data = await instance.get(`/api/number`, { + params: { + drwtNo, + }, + }); + + setSelectNumbers(data); + } catch (err) { + console.log('err', err); + } + setIsOpenScrapBottomSheet(false); + }; + + return ( + + {selectOptions.map(round => ( + onSelect(round)}> + {round} + + ))} + + ); +}; + +const RoundSelectorBlock = styled.div` + display: flex; + flex-direction: column; + gap: 26px; +`; + +const Item = styled.button` + text-align: center; + font-size: 20px; +`; + +export default RoundSelector; diff --git a/app/_components/analysis/amount/AmountAnalysisWrapper.tsx b/app/_components/analysis/amount/AmountAnalysisWrapper.tsx new file mode 100644 index 0000000..f937409 --- /dev/null +++ b/app/_components/analysis/amount/AmountAnalysisWrapper.tsx @@ -0,0 +1,45 @@ +import NavTabs from '@/_components/common/NavTabs'; +import React from 'react'; +import styled from 'styled-components'; +import PrizeAmountList from './PrizeAmountList'; +import { useSearchParams } from 'next/navigation'; +import PrizeRankAnalysisWrapper from './PrizeRankAnalysisWrapper'; +import { SortOption } from '@/_types/analysis'; + +type AmountAnalysisWrapperProps = {}; + +const AmountAnalysisWrapper: React.FC = () => { + const searchParams = useSearchParams(); + + const tabOptions = [ + { + label: '당첨금액 높은 순', + queryParams: 'type', + value: 'desc', + }, + { + label: '당첨금액 낮은 순', + queryParams: 'type', + value: 'asc', + }, + ]; + + return ( + + + + + + + ); +}; + +const AmountAnalysisWrapperBlock = styled.div``; + +const Line = styled.div` + width: 100%; + background-color: #eff3f8; + height: 10px; +`; + +export default AmountAnalysisWrapper; diff --git a/app/_components/analysis/amount/PrizeAmountList.tsx b/app/_components/analysis/amount/PrizeAmountList.tsx new file mode 100644 index 0000000..806383a --- /dev/null +++ b/app/_components/analysis/amount/PrizeAmountList.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import styled from 'styled-components'; +import PrizeAmountListItem from './PrizeAmountListItem'; +import useRankDetail from '@/_hooks/useRankDetail'; +import { useSearchParams } from 'next/navigation'; +import { SortOption } from '@/_types/analysis'; +import { Box, CircularProgress } from '@mui/material'; +import palette from '@/_styles/palette'; + +type PrizeAmountListProps = {}; + +const PrizeAmountList: React.FC = () => { + const searchParams = useSearchParams(); + const { rankDetailData, isLoading } = useRankDetail({ + size: 5, + sortOption: (searchParams.get('type') || 'desc') as SortOption, + }); + + return ( + + + + 당첨금이 가장 높은 순서대로 + <br /> + 확인할 수 있어요. + + + + {isLoading ? ( + + + + ) : ( + rankDetailData.map((rankDetail, i) => ( + + )) + )} + + + + ); +}; + +const PrizeAmountListBlock = styled.div``; + +const PrizeAmountTopBox = styled.div` + padding: 31px 20px 27px; +`; + +const Title = styled.p` + font-size: 18px; + font-weight: 500; + text-align: center; + line-height: 24px; + padding-bottom: 17px; +`; + +const PrizeAmountListBox = styled.ul` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export default PrizeAmountList; diff --git a/app/_components/analysis/amount/PrizeAmountListItem.tsx b/app/_components/analysis/amount/PrizeAmountListItem.tsx new file mode 100644 index 0000000..af50570 --- /dev/null +++ b/app/_components/analysis/amount/PrizeAmountListItem.tsx @@ -0,0 +1,76 @@ +import palette from '@/_styles/palette'; +import { RankDeatilResponseType } from '@/_types/analysis'; +import React from 'react'; +import styled from 'styled-components'; + +type PrizeAmountListItemProps = { + isTop?: boolean; + index: number; + rankDetail: RankDeatilResponseType; +}; + +const PrizeAmountListItem: React.FC = ({ + isTop = false, + index, + rankDetail, +}) => { + return ( + + + {index} + {rankDetail.drwt_no}회 + + + + {rankDetail.first_win_amount.toLocaleString()}원 + + + {rankDetail.first_win_amount_tax.toLocaleString()}원 + + + + ); +}; + +const PrizeAmountListItemBlock = styled.li<{ isTop: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 10px; + background: ${({ isTop }) => + isTop ? 'linear-gradient(134deg, #1666ef 32.36%, #6ca7ff 98.43%)' : palette.blue_ligth}; + padding: 19px 20px 15px; +`; + +const OrderBox = styled.div` + display: flex; + gap: 16px; +`; + +const OrderNum = styled.p<{ isTop: boolean }>` + font-weight: 700; + color: ${({ isTop }) => isTop && palette.white}; +`; + +const RoundNum = styled.p<{ isTop: boolean }>` + color: ${({ isTop }) => isTop && palette.white}; +`; + +const AmountBox = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-end; +`; + +const BeforeTaxAmount = styled.p<{ isTop: boolean }>` + font-size: 14px; + color: ${({ isTop }) => isTop && palette.white}; +`; + +const AfterTaxAmount = styled.p<{ isTop: boolean }>` + font-size: 12px; + color: ${({ isTop }) => (isTop ? '#A0C3FF' : '#979da6')}; +`; + +export default PrizeAmountListItem; diff --git a/app/_components/analysis/amount/PrizeRankAnalysisWrapper.tsx b/app/_components/analysis/amount/PrizeRankAnalysisWrapper.tsx new file mode 100644 index 0000000..becae56 --- /dev/null +++ b/app/_components/analysis/amount/PrizeRankAnalysisWrapper.tsx @@ -0,0 +1,89 @@ +import Selector from '@/_components/common/Selector'; +import useLatestNumber from '@/_hooks/useLatestNumber'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import BarChartWrapper from '../chart/BarChartWrapper'; +import useRankNumber from '@/_hooks/useRankNumber'; +import { useSearchParams } from 'next/navigation'; +import { SortOption } from '@/_types/analysis'; + +type PrizeRankAnalysisWrapperProps = {}; + +const PrizeRankAnalysisWrapper: React.FC = () => { + const searchParams = useSearchParams(); + const { latestRoundsNumber } = useLatestNumber(); + + const [selectedStartRank, setSelectedStartRank] = useState({ + label: '1등', + value: 1, + }); + const [selectedEndRank, setSelectedEndRank] = useState({ + label: '10등', + value: 10, + }); + + const selectOptions = Array.from({ length: latestRoundsNumber }, (_, index) => ({ + label: `${index + 1}등`, + value: index + 1, + })); + + const { rankNumbersData, isLoading } = useRankNumber({ + startRank: selectedStartRank.value, + size: selectedEndRank.value - selectedStartRank.value + 1, + rankSortOption: searchParams.get('type') as SortOption, + }); + + return ( + + + { + if (Number(event.target.value) > selectedEndRank.value) { + setSelectedEndRank({ + label: `${event.target.value}등`, + value: Number(event.target.value), + }); + } + setSelectedStartRank({ + label: `${event.target.value}등`, + value: Number(event.target.value), + }); + }} + options={selectOptions} + /> +

-

+ { + if (Number(event.target.value) < selectedStartRank.value) { + setSelectedStartRank({ + label: `${event.target.value}등`, + value: Number(event.target.value), + }); + } + setSelectedEndRank({ + label: `${event.target.value}등`, + value: Number(event.target.value), + }); + }} + options={selectOptions} + /> +
+ +
+ ); +}; + +const PrizeRankAnalysisWrapperBlock = styled.div` + padding: 27px 20px 50px; +`; + +const RoundsAnalysisSelectorBlock = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 35px; +`; + +export default PrizeRankAnalysisWrapper; diff --git a/app/_components/analysis/chart/BarChartWrapper.tsx b/app/_components/analysis/chart/BarChartWrapper.tsx index 9900db8..45f8b27 100644 --- a/app/_components/analysis/chart/BarChartWrapper.tsx +++ b/app/_components/analysis/chart/BarChartWrapper.tsx @@ -1,51 +1,187 @@ -import React from 'react'; -import { Bar } from 'react-chartjs-2'; -import styled from 'styled-components'; -import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; +import React, { useEffect, useMemo, useState } from 'react'; +import styled, { keyframes } from 'styled-components'; +import ArrowIcon from '@assets/svg/arrow.svg'; import palette from '@/_styles/palette'; +import { Box, CircularProgress } from '@mui/material'; +import { useSearchParams } from 'next/navigation'; -const BarChartWrapperBlock = styled.div``; - -type BarChartWrapperProps = {}; - -const BarChartWrapper: React.FC = () => { - ChartJS.register(ArcElement, Tooltip); - - const data = { - scales: { - x: { - beginAtZero: true, - grid: { - display: false, // x축의 그리드 라인 제거 - }, - }, - y: { - beginAtZero: true, - }, - }, - indexAxis: 'y', +type BarChartWrapperProps = { + hasSwitch?: boolean; + numbers: { no: number; count: number }[]; + isLoading: boolean; +}; + +const START_NUM = 1; +const END_NUM = 10; +const MAX_NUM = 45; + +const BarChartWrapper: React.FC = ({ + hasSwitch = false, + numbers, + isLoading, +}) => { + const [start, setStart] = useState(START_NUM); + const [end, setEnd] = useState(END_NUM); + + const handleNumbers = (arrow: 'next' | 'prev') => { + if (arrow === 'next') { + setStart(end + 1); + setEnd(end + END_NUM); + return; + } + + setStart(start - END_NUM); + setEnd(start - 1); }; + const filterByNumbers = useMemo(() => { + if (numbers.length && hasSwitch) { + const sliceNums = numbers.slice(start - 1, end); + if (sliceNums.length !== END_NUM) { + setEnd(numbers[numbers.length - 1]?.no); + } + return sliceNums; + } + return numbers; + }, [numbers, hasSwitch, start, end]); + + useEffect(() => { + setStart(START_NUM); + setEnd(END_NUM); + }, [numbers]); + return ( - - {/* */} + + {isLoading ? ( + + + + ) : ( + <> + {hasSwitch && ( + + handleNumbers('prev')}> + + + + {start} ~ {end} + + handleNumbers('next')} + > + + + + )} + + {filterByNumbers.map(num => ( + + {num.no} + + + ))} + + + )} ); }; +const BarChartWrapperBlock = styled.div` + margin: 18px 0; +`; + +const NumberBar = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 19px; +`; + +const Text = styled.p` + font-weight: bold; + margin: 0 8px; +`; + +const NextButton = styled.button` + display: flex; + align-items: center; + border: none; + background-color: transparent; + + &:disabled { + cursor: default; + path { + fill: ${palette.grey_60}; + } + } +`; + +const PrevButton = styled(NextButton)` + transform: rotate(-180deg); + + &:disabled { + cursor: default; + path { + fill: ${palette.grey_60}; + } + } +`; + +const BarList = styled.ul` + display: flex; + flex-direction: column; + gap: 27px; +`; + +const BarItem = styled.li` + display: flex; +`; + +const Num = styled.span` + color: #979da6; + font-size: 14px; + font-weight: 600; + min-width: 19px; +`; + +const barAni = (count: number) => keyframes` + from { + width: 0; + } + to { + width: ${count}%; + } +`; + +const StatisticalBar = styled.div<{ $count: number }>` + width: 100%; + height: 12px; + background-color: #eff3f8; + position: relative; + border-radius: 6px; + overflow: hidden; + margin-left: 15px; + + &::after { + content: ''; + position: absolute; + top: 0; + height: 100%; + width: 0; + border-radius: 6px; + background-color: ${palette.blue_15}; + animation: ${props => barAni(props.$count)} 0.8s forwards; + } +`; + export default BarChartWrapper; diff --git a/app/_components/analysis/chart/DoughnutChartWrapper.tsx b/app/_components/analysis/chart/DoughnutChartWrapper.tsx index 9016866..ed25052 100644 --- a/app/_components/analysis/chart/DoughnutChartWrapper.tsx +++ b/app/_components/analysis/chart/DoughnutChartWrapper.tsx @@ -2,21 +2,25 @@ import palette from '@styles/palette'; import React from 'react'; import styled from 'styled-components'; import { Doughnut } from 'react-chartjs-2'; -import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; import { Chart, registerables } from 'chart.js'; +import { Box, CircularProgress } from '@mui/material'; + Chart.register(...registerables); -type DoughnutChartWrapperProps = {}; -const DoughnutChartWrapper: React.FC = () => { - // ChartJS.register(ArcElement, Tooltip, Legend); - // Chart.register(CategoryScale); +type DoughnutChartWrapperProps = { + numbers: { no: number; count: number }[]; + isLoading: boolean; +}; + +const DoughnutChartWrapper: React.FC = ({ numbers, isLoading }) => { + const mostNumbersList = numbers.slice(0, 6); const data = { - labels: [10, 20, 30, 40, 50, 60], + labels: mostNumbersList.map(num => num.no), datasets: [ { label: 'My First Dataset', - data: [300, 50, 100, 100, 100, 100], + data: mostNumbersList.map(num => num.count), backgroundColor: [ palette.blue_15, palette.green_30, @@ -32,32 +36,71 @@ const DoughnutChartWrapper: React.FC = () => { }; return ( - + 가장 많이 출현한
6개의 번호를 확인해보세요.
- - - + + + ) : ( + + { + let title = ``; + return title; + }, + label: function (context) { + let label = `${context.raw}회 출현`; + return label; + }, }, }, }, - }, - }} - /> - + }} + /> + + )}
); }; diff --git a/app/_components/analysis/period/PeriodicAnalysisDateBar.tsx b/app/_components/analysis/period/PeriodicAnalysisDateBar.tsx index b7f12e8..f682b53 100644 --- a/app/_components/analysis/period/PeriodicAnalysisDateBar.tsx +++ b/app/_components/analysis/period/PeriodicAnalysisDateBar.tsx @@ -1,38 +1,85 @@ import palette from '@/_styles/palette'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import format from 'date-fns/format'; -import { endOfMonth, set } from 'date-fns'; +import { addMonths, subMonths, parse, addYears } from 'date-fns'; import Link from 'next/link'; -import { useSearchParams } from 'next/navigation'; +import { usePathname, useSearchParams } from 'next/navigation'; import ArrowIcon from '@assets/svg/arrow.svg'; +import RefreshIcon from '@assets/svg/refresh.svg'; import DateFilterIcon from '@assets/svg/dateFilter.svg'; +import { useRouter } from 'next/navigation'; +import { subYears } from 'date-fns/esm'; -type PeriodicAnalysisDateBarProps = { - type: 'month' | 'year'; -}; +type PeriodicAnalysisDateBarProps = {}; -const PeriodicAnalysisDateBar: React.FC = ({ type }) => { +const PeriodicAnalysisDateBar: React.FC = () => { + const router = useRouter(); const searchParams = useSearchParams(); - const year = 2023; //test - const month = 8; //test - const dateInSpecificMonth = set(new Date(), { year, month: month - 1 }); + const pathname = usePathname(); + const newParams = new URLSearchParams(searchParams.toString()); + + const categoryMode = searchParams.get('category'); + const startDt = searchParams.get('startDt'); + const endDt = searchParams.get('endDt'); + const isMonthMode = categoryMode === 'month' ? 'yyyyMM' : 'yyyy'; + const targetFormatDate = format(new Date(), isMonthMode); + const [currenEndDt, setCurrenEndDt] = useState(endDt); + + useEffect(() => { + setCurrenEndDt(endDt || targetFormatDate); + }, [categoryMode, targetFormatDate, endDt]); + + const handleDateNavigation = ({ type }: { type: 'prev' | 'next' }) => { + const targetDate = parse(currenEndDt || targetFormatDate, isMonthMode, new Date()); + + const isPrev = categoryMode === 'month' ? subMonths(targetDate, 1) : subYears(targetDate, 1); + const isNext = categoryMode === 'month' ? addMonths(targetDate, 1) : addYears(targetDate, 1); + + const controlDt = type === 'prev' ? isPrev : isNext; + setCurrenEndDt(format(controlDt, isMonthMode)); + newParams.set('endDt', format(controlDt, isMonthMode)); + router.push(`${pathname}?${newParams.toString()}`); + }; return ( + { + setCurrenEndDt(targetFormatDate); + router.push(`${pathname}?&category=${categoryMode || 'month'}&endDt=${targetFormatDate}`); + }} + isVisible={!!startDt} + > + + - - - -

- {format(dateInSpecificMonth, 'yyyy.MM.01')} ~{' '} - {format(endOfMonth(dateInSpecificMonth), 'yyyy.MM.dd')} -

- - - + {!startDt && ( + handleDateNavigation({ type: 'prev' })}> + + + )} + {startDt ? ( + + {startDt} ~ {currenEndDt} + + ) : ( + {currenEndDt} + )} + {!startDt && ( + handleDateNavigation({ type: 'next' })} + disabled={currenEndDt === targetFormatDate} + > + + + )}
- +
@@ -41,32 +88,43 @@ const PeriodicAnalysisDateBar: React.FC = ({ type const PeriodicAnalysisDateBarBlock = styled.div` display: flex; - justify-content: center; - padding: 14px 0; + padding: 12px 20px; background-color: ${palette.grey_70}; - position: relative; + justify-content: space-between; + font-size: 14px; `; const NextButton = styled.button` display: flex; align-items: center; - border: none; - background-color: transparent; + + &:disabled { + svg { + path { + fill: ${palette.grey_50}; + } + } + } `; const PrevButton = styled(NextButton)` transform: rotate(-180deg); `; +const CurrentDate = styled.p` + font-weight: bold; + margin: 0 8px; +`; + const AnalysisDateBarBox = styled.div` display: flex; align-items: center; - font-size: 14px; `; -const AnalysisDatePickerBox = styled(Link)` - position: absolute; - right: 22px; +const AnalysisDatePickerBox = styled(Link)``; + +const AnalysisDateRefreshBox = styled.button<{ isVisible: boolean }>` + visibility: ${({ isVisible }) => !isVisible && 'hidden'}; `; export default PeriodicAnalysisDateBar; diff --git a/app/_components/analysis/period/PeriodicAnalysisHub.tsx b/app/_components/analysis/period/PeriodicAnalysisHub.tsx index 92e01c3..ac8b911 100644 --- a/app/_components/analysis/period/PeriodicAnalysisHub.tsx +++ b/app/_components/analysis/period/PeriodicAnalysisHub.tsx @@ -2,32 +2,77 @@ import React from 'react'; import styled from 'styled-components'; -import PeriodicAnalysisTabs from './PeriodicAnalysisTabs'; -import PeriodicAnalysisDateBar from './PeriodicAnalysisDateBar'; -import { useSearchParams } from 'next/navigation'; import DoughnutChartWrapper from '../chart/DoughnutChartWrapper'; import BarChartWrapper from '../chart/BarChartWrapper'; +import PeriodicAnalysisDateBar from './PeriodicAnalysisDateBar'; +import { useSearchParams } from 'next/navigation'; +import usePeriodNumber from '@/_hooks/usePeriodNumber'; +import NavTabs from '@/_components/common/NavTabs'; +import { SortType } from '@/_types/analysis'; +import ToggleSwitch from '@/_components/common/ToggleSwitch'; type PeriodicAnalysisHubProps = {}; const PeriodicAnalysisHub: React.FC = () => { const searchParams = useSearchParams(); + const { periodNumbers: periodNumbersByDesc, isLoading } = usePeriodNumber({ + sortOption: 'desc', + sortType: 'COUNT', + }); + const { periodNumbers: periodNumbersByAsc, isLoading: isLoadingByAsc } = usePeriodNumber({ + sortOption: searchParams.get('sortType') === 'COUNT' ? 'desc' : 'asc', + sortType: searchParams.get('sortType') as SortType, + }); return ( - - + - - + + + ); }; -const PeriodicAnalysisHubBlock = styled.div``; +const PeriodicAnalysisHubBlock = styled.div` + .doughnut-chart { + margin-bottom: 42px; + } + .bar-chart { + margin-top: 30px; + } +`; const ChartWrapper = styled.div` padding: 24px 20px; diff --git a/app/_components/analysis/period/PeriodicAnalysisTabs.tsx b/app/_components/analysis/period/PeriodicAnalysisTabs.tsx deleted file mode 100644 index 6bdeb85..0000000 --- a/app/_components/analysis/period/PeriodicAnalysisTabs.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import palette from '@/_styles/palette'; -import Link from 'next/link'; -import React from 'react'; -import styled from 'styled-components'; -import { useSearchParams } from 'next/navigation'; - -type PeriodicAnalysisTabsProps = {}; - -const PeriodicAnalysisTabs: React.FC = () => { - const searchParams = useSearchParams(); - - return ( - - - 월별 - - - 연도별 - - - ); -}; - -const PeriodicAnalysisTabsBlock = styled.div` - display: flex; -`; - -const PeriodicAnalysisTabLink = styled(Link)<{ isFocused: boolean }>` - padding: 13px 0; - color: ${({ isFocused }) => (isFocused ? palette.black : palette.grey_40)}; - font-weight: ${({ isFocused }) => (isFocused ? 'bold' : 'normal')}; - font-size: 14px; - background-color: ${palette.white}; - text-decoration: none; - flex: 1; - text-align: center; - //animation 추후 추가 - border-bottom: 2px solid ${({ isFocused }) => (isFocused ? palette.black : palette.grey_60)}; -`; - -export default PeriodicAnalysisTabs; diff --git a/app/_components/analysis/period/filter/MonthPickerFilter.tsx b/app/_components/analysis/period/filter/MonthPickerFilter.tsx new file mode 100644 index 0000000..786719e --- /dev/null +++ b/app/_components/analysis/period/filter/MonthPickerFilter.tsx @@ -0,0 +1,192 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import palette from '@/_styles/palette'; +import { ko } from 'date-fns/locale'; +import { format, parse } from 'date-fns'; +import ArrowIcon from '@assets/svg/arrow.svg'; +import { Button } from '@/_components/common'; +import { useRouter, useSearchParams } from 'next/navigation'; + +type MonthPickerFilterTestProps = {}; + +const MonthPickerFilterTest: React.FC = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const startDt = searchParams.get('startDt') || format(new Date(), 'yyyyMM'); + const endDt = searchParams.get('endDt') || format(new Date(), 'yyyyMM'); + + const [selectedStartDate, setSelectedStartDate] = useState( + parse(startDt, 'yyyyMM', new Date()), + ); + const [selectedEndDate, setSelectedEndDate] = useState(parse(endDt, 'yyyyMM', new Date())); + + const rangeMonthPicker = [ + { + title: '시작일', + value: selectedStartDate, + }, + { + title: '종료일', + value: selectedEndDate, + }, + ]; + + return ( + + {rangeMonthPicker.map((monthPicker, i) => ( + + {monthPicker.title} + { + if (!date) return; + monthPicker.title === '종료일' + ? setSelectedEndDate(date) + : setSelectedStartDate(date); + }} + selectsStart={!(monthPicker.title === '종료일')} + selectsEnd={monthPicker.title === '종료일'} + startDate={selectedStartDate} + endDate={selectedEndDate} + dateFormat="yyyyMM" + showMonthYearPicker + inline + minDate={monthPicker.title === '종료일' ? selectedStartDate : null} + maxDate={new Date()} + showFourColumnMonthYearPicker + renderCustomHeader={({ + date, + decreaseYear, + increaseYear, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => ( + + + + + {format(new Date(date), 'yyyy')} + + + + + )} + /> + + ))} + + + ); +}; + +const MonthPickerFilterTestBlock = styled.div` + display: flex; + flex-direction: column; + gap: 64px; + padding: 32px 20px; + + .react-datepicker { + border: none; + width: 100%; + + .react-datepicker__header { + background-color: transparent; + border: none; + padding: 0; + } + + .react-datepicker__month-container { + width: inherit; + + .react-datepicker__monthPicker { + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; + + .react-datepicker__month-wrapper { + display: flex; + gap: 8px; + width: max-content; + } + + //? default month + .react-datepicker__month-text { + padding: 14px 7px; + border-radius: 8px; + background-color: ${palette.grey_70}; + color: ${palette.grey_20}; + margin: 0; + } + + //? selected range month + .react-datepicker__month-text--in-range { + background-color: #c9dbfa; //? test color + color: ${palette.white}; + } + + //? selected month + .react-datepicker__month-text--selected { + background-color: ${palette.blue_30}; + color: ${palette.white}; + } + + //? disabled month + .react-datepicker__month-text--disabled { + background-color: transparent; + color: ${palette.grey_50}; + } + } + } + } +`; + +const MonthHeader = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + margin-bottom: 16px; +`; + +const ArrowNextButton = styled.button` + display: flex; + align-items: center; +`; + +const ArrowPrevButton = styled(ArrowNextButton)` + transform: rotate(-180deg); +`; + +const YearText = styled.span` + font-size: 18px; + font-weight: bold; +`; + +const MonthBox = styled.div` + display: flex; + flex-direction: column; +`; + +const Title = styled.p` + font-size: 20px; + font-weight: bold; + margin-bottom: 8px; +`; + +export default MonthPickerFilterTest; diff --git a/app/_components/analysis/period/filter/PeriodicDateFilterWrapper.tsx b/app/_components/analysis/period/filter/PeriodicDateFilterWrapper.tsx new file mode 100644 index 0000000..8b141b8 --- /dev/null +++ b/app/_components/analysis/period/filter/PeriodicDateFilterWrapper.tsx @@ -0,0 +1,26 @@ +'use client'; + +import React from 'react'; +import styled from 'styled-components'; +import MonthPickerFilter from './MonthPickerFilter'; +import { useSearchParams } from 'next/navigation'; +import YearPickerFilter from './YearPickerFilter'; + +type PeriodicDateFilterProps = {}; + +const PeriodicDateFilter: React.FC = () => { + const searchParams = useSearchParams(); + const categoryMode = searchParams.get('category'); + + return ( + + {categoryMode === 'month' ? : } + + ); +}; + +const PeriodicDateFilterBlock = styled.div` + min-height: calc(100vh - 10.8rem); +`; + +export default PeriodicDateFilter; diff --git a/app/_components/analysis/period/filter/YearPickerFilter.tsx b/app/_components/analysis/period/filter/YearPickerFilter.tsx new file mode 100644 index 0000000..f4be241 --- /dev/null +++ b/app/_components/analysis/period/filter/YearPickerFilter.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import palette from '@/_styles/palette'; +import { ko } from 'date-fns/locale'; +import { format } from 'date-fns'; +import ArrowIcon from '@assets/svg/arrow.svg'; +import { Button } from '@/_components/common'; +import { useRouter, useSearchParams } from 'next/navigation'; + +type YearPickerFilterProps = {}; + +const YearPickerFilter: React.FC = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const startDt = searchParams.get('startDt'); + const endDt = searchParams.get('endDt'); + + const [selectedStartDate, setSelectedStartDate] = useState(new Date(`${startDt}`)); + const [selectedEndDate, setSelectedEndDate] = useState(new Date(`${endDt}`)); + + const rangeYearPicker = [ + { + title: '시작일', + value: selectedStartDate, + }, + { + title: '종료일', + value: selectedEndDate, + }, + ]; + + //! 타이틀수정 + return ( + + {rangeYearPicker.map((yearPicker, i) => ( + + {yearPicker.title} + { + if (!date) return; + yearPicker.title === '종료일' ? setSelectedEndDate(date) : setSelectedStartDate(date); + }} + selectsStart={!(yearPicker.title === '종료일')} + selectsEnd={yearPicker.title === '종료일'} + startDate={selectedStartDate} + endDate={selectedEndDate} + dateFormat="yyyy" + showYearPicker + inline + minDate={yearPicker.title === '종료일' ? selectedStartDate : null} + maxDate={new Date()} + showFourColumnMonthYearPicker + renderCustomHeader={({ + date, + decreaseYear, + increaseYear, + prevYearButtonDisabled, + nextYearButtonDisabled, + customHeaderCount, + }) => ( + + + + + + {yearPicker.title === '종료일' + ? format(selectedEndDate, 'yyyy') + : format(selectedStartDate, 'yyyy')} + + + + + + )} + /> + + ))} + + + ); +}; + +const YearPickerFilterBlock = styled.div` + display: flex; + flex-direction: column; + gap: 64px; + padding: 32px 20px; + + .react-datepicker { + border: none; + width: 100%; + + .react-datepicker__header { + background-color: transparent; + border: none; + padding: 0; + } + + .react-datepicker__year--container { + width: inherit; + + .react-datepicker__year { + margin: 0; + } + + .react-datepicker__year-wrapper { + display: flex; + gap: 7px; + width: max-content; + flex-wrap: wrap; + max-width: 100%; + } + + //? default year + .react-datepicker__year-text { + padding: 14px 7px; + border-radius: 8px; + background-color: ${palette.grey_70}; + color: ${palette.grey_20}; + margin: 0; + } + + //? selected range year + .react-datepicker__year-text--in-range { + background-color: #c9dbfa; //? test color + color: ${palette.white}; + } + + //? selected year + .react-datepicker__year-text--selected { + background-color: ${palette.blue_30}; + color: ${palette.white}; + } + + //? disabled year + .react-datepicker__year-text--disabled { + background-color: transparent; + color: ${palette.grey_50}; + } + } + } +`; + +const MonthHeader = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + margin-bottom: 16px; +`; + +const ArrowNextButton = styled.button` + display: flex; + align-items: center; +`; + +const ArrowPrevButton = styled(ArrowNextButton)` + transform: rotate(-180deg); +`; + +const YearText = styled.span` + font-size: 18px; + font-weight: bold; +`; + +const MonthBox = styled.div` + display: flex; + flex-direction: column; +`; + +const Title = styled.p` + font-size: 20px; + font-weight: bold; + margin-bottom: 8px; +`; + +export default YearPickerFilter; diff --git a/app/_components/analysis/period/pick/PeriodicDatePicker.tsx b/app/_components/analysis/period/pick/PeriodicDatePicker.tsx deleted file mode 100644 index 5fba683..0000000 --- a/app/_components/analysis/period/pick/PeriodicDatePicker.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import React from 'react'; -import styled from 'styled-components'; - -type PeriodicDatePickerProps = {}; - -const PeriodicDatePicker: React.FC = () => { - return ( - -

날짜 선택

-
- ); -}; - -const PeriodicDatePickerBlock = styled.div``; - -export default PeriodicDatePicker; diff --git a/app/_components/analysis/rounds/RoundsAnalysisWrapper.tsx b/app/_components/analysis/rounds/RoundsAnalysisWrapper.tsx new file mode 100644 index 0000000..94dac37 --- /dev/null +++ b/app/_components/analysis/rounds/RoundsAnalysisWrapper.tsx @@ -0,0 +1,105 @@ +'use client'; + +import React, { useState } from 'react'; +import styled from 'styled-components'; +import Selector from '@/_components/common/Selector'; +import useLatestNumber from '@/_hooks/useLatestNumber'; +import useRoundsNumber from '@/_hooks/useRoundsNumber'; +import DoughnutChartWrapper from '../chart/DoughnutChartWrapper'; +import BarChartWrapper from '../chart/BarChartWrapper'; + +type RoundsAnalysisWrapperProps = {}; + +const RoundsAnalysisWrapper: React.FC = () => { + const { latestRoundsNumber } = useLatestNumber(); + + //? 1072 최신회차 논의 필요 + const [selectedStartRound, setSelectedStartRound] = useState<{ label: string; value: number }>({ + label: `${latestRoundsNumber || 1072}회`, + value: latestRoundsNumber || 1072, + }); + const [selectedEndRound, setSelectedEndRound] = useState({ + label: `${latestRoundsNumber || 1072}회`, + value: latestRoundsNumber || 1072, + }); + + const selectOptions = Array.from({ length: latestRoundsNumber }, (_, index) => ({ + label: `${latestRoundsNumber - index}회`, + value: latestRoundsNumber - index, + })); + + const { roundNumbersData, isLoading } = useRoundsNumber({ + startNo: selectedStartRound.value, + endNo: selectedEndRound.value, + sortType: 'COUNT', + sortOption: 'desc', + }); + + const { roundNumbersData: roundNumbersDataByAsc, isLoading: isLoadingByAsc } = useRoundsNumber({ + startNo: selectedStartRound.value, + endNo: selectedEndRound.value, + sortOption: 'asc', + }); + + return ( + + + { + if (Number(event.target.value) > selectedEndRound.value) { + setSelectedEndRound({ + label: `${event.target.value}회`, + value: Number(event.target.value), + }); + } + setSelectedStartRound({ + label: `${event.target.value}회`, + value: Number(event.target.value), + }); + }} + options={selectOptions} + /> +

-

+ { + if (Number(event.target.value) < selectedStartRound.value) { + setSelectedStartRound({ + label: `${event.target.value}회`, + value: Number(event.target.value), + }); + } + setSelectedEndRound({ + label: `${event.target.value}회`, + value: Number(event.target.value), + }); + }} + options={selectOptions} + /> +
+ + + +
+ ); +}; + +const RoundsAnalysisWrapperBlock = styled.div` + display: flex; + flex-direction: column; + padding: 6px 20px; +`; + +const RoundsAnalysisSelectorBlock = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 18px; +`; + +export default RoundsAnalysisWrapper; diff --git a/app/_components/common/BottomSheet.tsx b/app/_components/common/BottomSheet.tsx index 5db8b65..1392530 100644 --- a/app/_components/common/BottomSheet.tsx +++ b/app/_components/common/BottomSheet.tsx @@ -4,6 +4,7 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import palette from '@styles/palette'; import type { PropsWithChildren } from 'react'; import { styled } from 'styled-components'; +import CloseIconSVG from '@assets/svg/close.svg'; const CUSTOM_STYLE = { margin: '0 auto', @@ -32,6 +33,7 @@ interface BottomSheetProps { isOpen: boolean; onOpen: () => void; onClose: () => void; + title?: string; } const BottomSheet = ({ @@ -39,6 +41,7 @@ const BottomSheet = ({ isOpen, onOpen, onClose, + title, }: PropsWithChildren) => { const isMobile = useMediaQuery('(max-width:576px)'); @@ -51,6 +54,15 @@ const BottomSheet = ({ sx={isMobile ? MOBILE_CUSTOM_STYLE : DESKTOP_CUSTOM_STYLE} > + {title && ( +
+ {title} + + + + +
+ )} {children} ); @@ -69,6 +81,30 @@ const HandleBar = styled(Box)` background-color: ${palette.grey_60}; `; +const Header = styled.div` + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin-bottom: 32px; +`; + +const Title = styled.p` + font-size: 18px; + font-weight: 600; +`; + +const CloseButton = styled.button` + position: absolute; + right: 20px; +`; + +const CloseIcon = styled(CloseIconSVG)` + path { + fill: ${palette.black}; + } +`; + const BottomSheetBody = styled.div` overflow-x: hidden; overflow-y: auto; diff --git a/app/_components/common/Button.tsx b/app/_components/common/Button.tsx index 0e838ab..fe981bc 100644 --- a/app/_components/common/Button.tsx +++ b/app/_components/common/Button.tsx @@ -3,6 +3,7 @@ import type { ComponentPropsWithoutRef, CSSProperties, PropsWithChildren } from import { styled } from 'styled-components'; const SIZE_STYLE = { + small: '4.5rem', medium: '9.6rem', full: '100%', } as const; diff --git a/app/_components/common/NavTabs.tsx b/app/_components/common/NavTabs.tsx new file mode 100644 index 0000000..d59b100 --- /dev/null +++ b/app/_components/common/NavTabs.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import palette from '@/_styles/palette'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +type NavTabsProps = { + tabOptions: { label: string; queryParams: string; value: string }[]; +}; + +const NavTabs: React.FC = ({ tabOptions }) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + const [value, setValue] = useState(searchParams.get(tabOptions[0].queryParams)); + + const handleChange = (event: React.SyntheticEvent, newValue: string) => { + setValue(newValue); + }; + + return ( + + {tabOptions.map((tab, i) => ( + { + router.push(`${pathname}?${tab.queryParams}=${tab.value}`); + }} + sx={{ + '&.Mui-selected': { + fontWeight: 700, + }, + }} + /> + ))} + + ); +}; + +export default NavTabs; diff --git a/app/_components/common/Selector.tsx b/app/_components/common/Selector.tsx new file mode 100644 index 0000000..dc09d28 --- /dev/null +++ b/app/_components/common/Selector.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import styled from 'styled-components'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import palette from '@/_styles/palette'; + +type SelectorProps = { + selectOption: { label: string; value: number }; + onChange: (e: SelectChangeEvent) => void; + options: { label: string; value: number }[]; +}; + +const Selector: React.FC = ({ selectOption, onChange, options }) => { + return ( + + {options.map((option, i) => ( + + {option.label} + + ))} + + ); +}; + +const SelectorBlock = styled(Select)` + &::-webkit-scrollbar { + width: '120px'; // 스크롤바의 너비를 조절할 수 있습니다. + } + + &::-webkit-scrollbar-thumb { + background-color: '#888'; // 스크롤바 색상을 설정할 수 있습니다. + border-radius: '6px'; // 스크롤바의 모서리를 둥글게 만들 수 있습니다. + } +`; + +export default Selector; diff --git a/app/_hooks/useLatestNumber.tsx b/app/_hooks/useLatestNumber.tsx new file mode 100644 index 0000000..5002c26 --- /dev/null +++ b/app/_hooks/useLatestNumber.tsx @@ -0,0 +1,31 @@ +import instance from '@/_apis/core'; +import { LatestNumberResponseType } from '@/_types/analysis'; +import { useQuery } from '@tanstack/react-query'; +import { AxiosResponse } from 'axios'; + +const useLatestNumber = () => { + const fetcher = async () => { + try { + const data = await instance.get( + `/api/number/latest`, + ); + return data; + } catch (err) { + console.log('err', err); + } + }; + + const { data, error, isFetching, isLoading } = useQuery(['latestNumberData'], fetcher, { + retry: 0, + }); + + return { + latestNumbers: data, + latestRoundsNumber: data?.drwt_no!!, + isLoading, + error, + isFetching, + }; +}; + +export default useLatestNumber; diff --git a/app/_hooks/usePeriodNumber.tsx b/app/_hooks/usePeriodNumber.tsx new file mode 100644 index 0000000..b2968e6 --- /dev/null +++ b/app/_hooks/usePeriodNumber.tsx @@ -0,0 +1,56 @@ +import instance from '@/_apis/core'; +import { NumberResponseType, SortOption, SortType, PeriodParamType } from '@/_types/analysis'; +import { useQuery } from '@tanstack/react-query'; +import { AxiosResponse } from 'axios'; +import { format } from 'date-fns'; +import { useSearchParams } from 'next/navigation'; + +const usePeriodNumber = ({ + sortOption, + sortType, +}: Pick) => { + const searchParams = useSearchParams(); + const monthFormatDate = format(new Date(), 'yyyyMM'); + const yearFormatDate = format(new Date(), 'yyyy'); + const category = (searchParams.get('category') || 'month') as 'year' | 'month'; + const startDt = searchParams.get('startDt'); + const endDt = searchParams.get('endDt'); + + const params = { + month: { + startDt: startDt || endDt || monthFormatDate, + endDt: endDt || monthFormatDate, + }, + year: { + startDt: `${startDt || endDt || yearFormatDate}01`, + endDt: `${endDt || yearFormatDate}12`, + }, + }; + + const fetcher = async () => { + try { + const data = await instance.get(`/api/statics/period`, { + params: { + ...params[category], + sortOption: sortOption || (searchParams.get('sortOption') as SortOption) || 'asc', + sortType: sortType || (searchParams.get('sortType') as SortType) || 'NO', + }, + }); + return data; + } catch (err) { + console.log('err', err); + } + }; + + const { data, error, isFetching, isLoading } = useQuery( + ['PeriodNumberData', { category, sortOption, startDt, endDt, sortType }], + fetcher, + { + retry: 0, + }, + ); + + return { periodNumbers: data || [], isLoading, error, isFetching }; +}; + +export default usePeriodNumber; diff --git a/app/_hooks/useRankDetail.tsx b/app/_hooks/useRankDetail.tsx new file mode 100644 index 0000000..391b038 --- /dev/null +++ b/app/_hooks/useRankDetail.tsx @@ -0,0 +1,32 @@ +import instance from '@/_apis/core'; +import { RankDeatilResponseType, RankDetailParamsType } from '@/_types/analysis'; +import { useQuery } from '@tanstack/react-query'; +import { AxiosResponse } from 'axios'; + +const useRankDetail = ({ size, sortOption = 'desc' }: RankDetailParamsType) => { + const params = { + size, + sortOption, + }; + + const fetcher = async () => { + try { + const data = await instance.get( + `/api/statics/rank/detail`, + { + params, + }, + ); + return data; + } catch (err) { + console.log('err', err); + return null; + } + }; + + const { data, isLoading } = useQuery([`rankDetailData_${sortOption}`, sortOption], fetcher); + + return { rankDetailData: data || [], isLoading }; +}; + +export default useRankDetail; diff --git a/app/_hooks/useRankNumber.tsx b/app/_hooks/useRankNumber.tsx new file mode 100644 index 0000000..341226b --- /dev/null +++ b/app/_hooks/useRankNumber.tsx @@ -0,0 +1,38 @@ +import instance from '@/_apis/core'; +import { NumberResponseType, RankNumbersParamsType } from '@/_types/analysis'; +import { useQuery } from '@tanstack/react-query'; +import { AxiosResponse } from 'axios'; + +const useRankNumber = ({ + startRank, + size, + rankSortOption = 'desc', + sortOption = 'asc', + sortType = 'NO', +}: RankNumbersParamsType) => { + const params = { + startRank, + size, + rankSortOption, + sortOption, + sortType, + }; + + const fetcher = async () => { + try { + const data = await instance.get(`/api/statics/rank`, { + params, + }); + return data; + } catch (err) { + console.log('err', err); + return null; + } + }; + + const { data, isLoading } = useQuery([`rankNumbersData_${sortOption}`, params], fetcher); + + return { rankNumbersData: data || [], isLoading }; +}; + +export default useRankNumber; diff --git a/app/_hooks/useRoundsNumber.tsx b/app/_hooks/useRoundsNumber.tsx new file mode 100644 index 0000000..17ab819 --- /dev/null +++ b/app/_hooks/useRoundsNumber.tsx @@ -0,0 +1,36 @@ +import instance from '@/_apis/core'; +import { NumberResponseType, RoundNumbersParamsType } from '@/_types/analysis'; +import { useQuery } from '@tanstack/react-query'; +import { AxiosResponse } from 'axios'; + +const useRoundsNumber = ({ + startNo, + endNo, + sortOption = 'asc', + sortType = 'NO', +}: RoundNumbersParamsType) => { + const params = { + startNo, + endNo, + sortOption, + sortType, + }; + + const fetcher = async () => { + try { + const data = await instance.get(`/api/statics/number`, { + params, + }); + return data; + } catch (err) { + console.log('err', err); + return null; + } + }; + + const { data, isLoading } = useQuery([`roundNumbersData_${sortOption}`, params], fetcher); + + return { roundNumbersData: data || [], isLoading }; +}; + +export default useRoundsNumber; diff --git a/app/_types/analysis.ts b/app/_types/analysis.ts new file mode 100644 index 0000000..e3e0bfc --- /dev/null +++ b/app/_types/analysis.ts @@ -0,0 +1,61 @@ +export interface LatestNumberResponseType { + drwt_no: number; + drwt_date: string; + drwt_no1: number; + drwt_no2: number; + drwt_no3: number; + drwt_no4: number; + drwt_no5: number; + drwt_no6: number; + bnus_no: number; + tot_sell_amount: number; + first_win_amount: number; + first_win_count: number; + first_tot_amount: number; +} +export interface NumberResponseType { + no: number; + count: number; +} + +export interface RankDeatilResponseType { + drwt_no: number; + first_win_amount: number; + first_win_amount_tax: number; +} + +export type SortOption = 'asc' | 'desc'; //? 정렬옵션 + +export type SortType = 'NO' | 'COUNT'; //? 정렬구분 + +export interface PeriodParamType { + startDt: string; + endDt: string; + sortOption: SortOption; + sortType?: SortType; +} + +export interface PeriodNumbersParamsType { + month: PeriodParamType; + year: PeriodParamType; +} + +export interface RankNumbersParamsType { + startRank: number; //? 시작 등수 + size: number; //? 조회 개수 + rankSortOption?: SortOption; //? 랭크 정렬옵션 + sortOption?: SortOption; + sortType?: SortType; +} + +export interface RankDetailParamsType { + size: number; + sortOption?: SortOption; +} + +export interface RoundNumbersParamsType { + startNo: number; + endNo: number; + sortType?: SortType; + sortOption?: SortOption; +} diff --git a/package.json b/package.json index 302952d..83b89e4 100644 --- a/package.json +++ b/package.json @@ -22,17 +22,20 @@ "next": "13.4.13", "react": "18.2.0", "react-chartjs-2": "^5.2.0", + "react-datepicker": "^4.16.0", "react-dom": "18.2.0", "react-kakao-maps-sdk": "^1.1.11", "react-query": "^3.39.3", "styled-components": "^6.0.7", "swiper": "^10.1.0", + "typescript": "5.1.6", "zustand": "^4.4.1" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^4.32.5", "@types/node": "^20.4.9", "@types/react": "^18.2.20", + "@types/react-datepicker": "^4.15.0", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/parser": "^6.3.0", diff --git a/public/assets/images/analysisMain.png b/public/assets/images/analysisMain.png index 134a4ee..77678a0 100644 Binary files a/public/assets/images/analysisMain.png and b/public/assets/images/analysisMain.png differ diff --git a/public/assets/svg/Icon.svg b/public/assets/svg/Icon.svg new file mode 100644 index 0000000..93b73a5 --- /dev/null +++ b/public/assets/svg/Icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/svg/arrowDropDown.svg b/public/assets/svg/arrowDropDown.svg new file mode 100644 index 0000000..765d9a6 --- /dev/null +++ b/public/assets/svg/arrowDropDown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/svg/calendarFilter.svg b/public/assets/svg/calendarFilter.svg new file mode 100644 index 0000000..85a6a37 --- /dev/null +++ b/public/assets/svg/calendarFilter.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/svg/dash.svg b/public/assets/svg/dash.svg new file mode 100644 index 0000000..7ad448c --- /dev/null +++ b/public/assets/svg/dash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/svg/refresh.svg b/public/assets/svg/refresh.svg new file mode 100644 index 0000000..8306097 --- /dev/null +++ b/public/assets/svg/refresh.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/yarn.lock b/yarn.lock index 3dd2ebe..74a58bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1444,7 +1444,7 @@ picocolors "^1.0.0" tslib "^2.6.0" -"@popperjs/core@^2.11.8": +"@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== @@ -1641,6 +1641,16 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== +"@types/react-datepicker@^4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-4.15.0.tgz#24a9c03e79ab4b232b346edd006cfb6060b0fb43" + integrity sha512-kr10s8ex4+MmCJmzdhA9kfmoMQBmsW5uDYDlH+8f/PgStrp7rRaz23Y/cvTiMgvESVq8ujDh4SOo6jlSwEw13g== + dependencies: + "@popperjs/core" "^2.9.2" + "@types/react" "*" + date-fns "^2.0.1" + react-popper "^2.2.5" + "@types/react-dom@^18.2.7": version "18.2.7" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" @@ -2125,6 +2135,11 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" +classnames@^2.2.6: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + client-only@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" @@ -2307,7 +2322,7 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -date-fns@^2.30.0: +date-fns@^2.0.1, date-fns@^2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== @@ -3479,7 +3494,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3919,7 +3934,7 @@ prettier@^3.0.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.1.tgz#65271fc9320ce4913c57747a70ce635b30beaa40" integrity sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ== -prop-types@^15.6.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -3948,6 +3963,18 @@ react-chartjs-2@^5.2.0: resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== +react-datepicker@^4.16.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f" + integrity sha512-hNQ0PAg/LQoVbDUO/RWAdm/RYmPhN3cz7LuQ3hqbs24OSp69QCiKOJRrQ4jk1gv1jNR5oYu8SjjgfDh8q6Q1yw== + dependencies: + "@popperjs/core" "^2.11.8" + classnames "^2.2.6" + date-fns "^2.30.0" + prop-types "^15.7.2" + react-onclickoutside "^6.12.2" + react-popper "^2.3.0" + react-dom@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -3956,6 +3983,11 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-fast-compare@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -3973,6 +4005,19 @@ react-kakao-maps-sdk@^1.1.11: dependencies: kakao.maps.d.ts "^0.1.38" +react-onclickoutside@^6.12.2: + version "6.13.0" + resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" + integrity sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A== + +react-popper@^2.2.5, react-popper@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba" + integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + react-query@^3.39.3: version "3.39.3" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" @@ -4488,7 +4533,7 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^5.1.6: +typescript@5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== @@ -4559,6 +4604,13 @@ use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +warning@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"