diff --git a/eslint.config.mjs b/eslint.config.mjs index 373d4d4..3787778 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,7 +10,7 @@ import tsConfig from './eslint/typescript.mjs'; export default [ { - ignores: ['src/vite-env.d.ts'], + ignores: ['src/vite-env.d.ts', 'public/firebase-messaging-sw.js'], }, { files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'], diff --git a/package.json b/package.json index 1a84e71..2c046f5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "chart.js": "^4.5.0", "chartjs-plugin-datalabels": "^2.2.0", "clsx": "^2.1.1", + "firebase": "^12.0.0", "lodash": "^4.17.21", "lodash.throttle": "^4.1.1", "path": "^0.12.7", @@ -30,7 +31,9 @@ "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", "react-hook-form": "^7.60.0", + "react-intersection-observer": "^9.16.0", "react-router-dom": "^7.6.0", + "react-spinners": "^0.17.0", "sonner": "^2.0.3", "tailwindcss": "^4.1.7", "wordcloud": "^1.2.3", diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 0000000..ff4fef2 --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,42 @@ +/// +/* eslint-env serviceworker */ +/* global firebase importScripts clients */ +importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-app-compat.js'); +importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-compat.js'); + +firebase.initializeApp({ + apiKey: 'AIzaSyAjZqK2lhCOeX_P2Sf-_2IGEFlORchcO5w', + authDomain: 'withtime-ff471.firebaseapp.com', + projectId: 'withtime-ff471', + storageBucket: 'withtime-ff471.firebasestorage.app', + messagingSenderId: '47995224236', + appId: '1:47995224236:web:85371605ce4a6659529f09', + measurementId: 'G-5E8Q23LL4H', +}); + +const messaging = firebase.messaging(); + +self.addEventListener('push', function (event) { + try { + const payload = event.data.json(); + const title = payload.notification.title; + + const options = { + body: payload.notification.body, + icon: payload.notification.icon, + data: payload.notification.click_action, + }; + + event.waitUntil(self.registration.showNotification(title, options)); + } catch (error) { + console.error('Push event error:', error); + } +}); + +self.addEventListener('notificationclick', function (event) { + console.log(event.notification); + + event.notification.close(); + + event.waitUntil(clients.openWindow(event.notification.data).catch((error) => console.error('Failed to open window:', error))); +}); diff --git a/src/App.css b/src/App.css index e69de29..79e8923 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,10 @@ +@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard-dynamic-subset.css'); + +html, body, button, input, select, textarea { + font-family: 'Pretendard', + -apple-system, BlinkMacSystemFont, system-ui, + 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + 'Noto Sans KR', 'Apple SD Gothic Neo', 'Malgun Gothic', + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + sans-serif; +} \ No newline at end of file diff --git a/src/api/alarm/alarm.ts b/src/api/alarm/alarm.ts new file mode 100644 index 0000000..49b7ec3 --- /dev/null +++ b/src/api/alarm/alarm.ts @@ -0,0 +1,18 @@ +import type { TRequestGetAlarm, TRequestPostDeviceToken, TResponseGetAlarm, TResponsePostDeviceToken } from '@/types/alarm/alarm'; + +import { axiosInstance } from '@/api/axiosInstance'; + +export const getAlarm = async ({ size = 5, cursor }: TRequestGetAlarm): Promise => { + const { data } = await axiosInstance.get('/api/v1/alarms', { + params: { + size, + cursor, + }, + }); + return data; +}; + +export const postDeviceToken = async ({ deviceToken }: TRequestPostDeviceToken): Promise => { + const { data } = await axiosInstance.post('/api/v1/alarms/device-tokens', { deviceToken }); + return data; +}; diff --git a/src/api/auth/auth.ts b/src/api/auth/auth.ts index 48f6d9a..40176e7 100644 --- a/src/api/auth/auth.ts +++ b/src/api/auth/auth.ts @@ -13,7 +13,7 @@ import type { TSocialLoginValues, } from '@/types/auth/auth'; -import { axiosInstance } from '../axiosInstance'; +import { axiosInstance } from '@/api/axiosInstance'; export const defaultSignup = async ({ email, password, username, gender, phoneNumber, birth, socialId }: TSignupValues): Promise => { const { data } = await axiosInstance.post('/api/v1/auth/sign-up', { email, password, socialId, username, gender, phoneNumber, birth }); diff --git a/src/api/course/course.ts b/src/api/course/course.ts index fa176f3..c1ec404 100644 --- a/src/api/course/course.ts +++ b/src/api/course/course.ts @@ -1,6 +1,6 @@ import type { TSearchRegionResponse, TSearchRegionValues } from '@/types/dateCourse/dateCourse'; -import { axiosInstance } from '../axiosInstance'; +import { axiosInstance } from '@/api/axiosInstance'; export const searchRegion = async ({ keyword }: TSearchRegionValues): Promise => { const { data } = await axiosInstance.get('/api/v1/regions/search', { diff --git a/src/api/home/dateCourse.ts b/src/api/home/dateCourse.ts new file mode 100644 index 0000000..b1c3a18 --- /dev/null +++ b/src/api/home/dateCourse.ts @@ -0,0 +1,8 @@ +import type { TDateCourseSavedCountResponse } from '@/types/home/dateCourse'; + +import { axiosInstance } from '@/api/axiosInstance'; + +export const getDateCourseSavedCount = async (): Promise => { + const { data } = await axiosInstance.get('/api/v1/logs/datecourses/saved-count'); + return data; +}; diff --git a/src/api/home/dateTimes.ts b/src/api/home/dateTimes.ts new file mode 100644 index 0000000..b03573f --- /dev/null +++ b/src/api/home/dateTimes.ts @@ -0,0 +1,14 @@ +import type { TGetDateTimeStates, TMonthlyDatePlaceResponse } from '../../types/home/datePlace'; + +import { axiosInstance } from '@/api/axiosInstance'; + +// 월별 데이트 장소 수 조회 API +export const getMonthlyDatePlaceStates = async (): Promise => { + const { data } = await axiosInstance.get('/api/v1/logs/dateplaces/monthly'); + return data; +}; + +export const getDateTimeStates = async (): Promise => { + const { data } = await axiosInstance.get('/api/v1/logs/datecourses/average'); + return data; +}; diff --git a/src/api/home/keyword.ts b/src/api/home/keyword.ts new file mode 100644 index 0000000..852d218 --- /dev/null +++ b/src/api/home/keyword.ts @@ -0,0 +1,9 @@ +import type { TWeeklyKeywordResponse } from '@/types/home/keyword'; + +import { axiosInstance } from '@/api/axiosInstance'; + +// 이번 주 인기 키워드 조회 API +export const getWeeklyKeywords = async (): Promise => { + const { data } = await axiosInstance.get('/api/v1/logs/keyword/weekly'); + return data; +}; diff --git a/src/api/home/level.ts b/src/api/home/level.ts new file mode 100644 index 0000000..eb649ac --- /dev/null +++ b/src/api/home/level.ts @@ -0,0 +1,9 @@ +import type { TUserGradeResponse } from '@/types/home/level'; + +import { axiosInstance } from '@/api/axiosInstance'; + +// 사용자 등급 조회 API +export const getUserGrade = async (): Promise => { + const { data } = await axiosInstance.get('/api/v1/members/grade'); + return data; +}; diff --git a/src/api/home/region.ts b/src/api/home/region.ts new file mode 100644 index 0000000..42148ed --- /dev/null +++ b/src/api/home/region.ts @@ -0,0 +1,13 @@ +import type { TGetUserRegionResponse, TPatchUserRegionRequest, TPatchUserRegionResponse } from '@/types/home/region'; + +import { axiosInstance } from '@/api/axiosInstance'; + +export const patchUserRegion = async ({ regionId }: TPatchUserRegionRequest): Promise => { + const { data } = await axiosInstance.patch('/api/v1/regions/users', { regionId }); + return data; +}; + +export const getUserRegion = async (): Promise => { + const { data } = await axiosInstance.get('/api/v1/regions/users/current'); + return data; +}; diff --git a/src/api/home/weather.ts b/src/api/home/weather.ts new file mode 100644 index 0000000..cbca1ff --- /dev/null +++ b/src/api/home/weather.ts @@ -0,0 +1,22 @@ +import type { + TGetPrecipitationRequest, + TGetPrecipitationResponse, + TGetWeeklyWeatherRecommendationRequest, + TGetWeeklyWeatherRecommendationResponse, +} from '@/types/home/weather'; + +import { axiosInstance } from '@/api/axiosInstance'; + +// 주간 날씨 추천 조회 API +export const getWeeklyWeatherRecommendation = async ({ + regionId, + startDate, +}: TGetWeeklyWeatherRecommendationRequest): Promise => { + const { data } = await axiosInstance.get(`/api/v1/weather/${regionId}/weekly`, { params: { startDate } }); + return data; +}; + +export const getPrecipitation = async ({ regionId, startDate }: TGetPrecipitationRequest): Promise => { + const { data } = await axiosInstance.get(`/api/v1/weather/${regionId}/precipitation`, { params: { startDate } }); + return data; +}; diff --git a/src/api/notice/notice.ts b/src/api/notice/notice.ts index 2249296..b2ce1c0 100644 --- a/src/api/notice/notice.ts +++ b/src/api/notice/notice.ts @@ -1,19 +1,11 @@ -import type { TFetchNoticeDetailResponse, TFetchNoticesResponse } from '@/types/notice/notice'; +import type { TFetchNoticeDetailResponse, TFetchNoticesResponse, TRequestGetNoticeRequest } from '@/types/notice/notice'; -import { axiosInstance } from '../axiosInstance'; +import { axiosInstance } from '@/api/axiosInstance'; // 공지사항 전체 조회 API -export const fetchNotices = async ({ - category, - page, - size, -}: { - category: 'SERVICE' | 'SYSTEM'; - page: number; - size: number; -}): Promise => { +export const fetchNotices = async ({ noticeCategory = 'SERVICE', page, size }: TRequestGetNoticeRequest): Promise => { const { data } = await axiosInstance.get('/api/v1/notices', { - params: { noticeCategory: category, page, size }, + params: { noticeCategory: noticeCategory, page, size }, }); return data; }; diff --git a/src/assets/icons/weather/rain.svg b/src/assets/icons/weather/rain.svg index 7d28a95..966e7de 100644 --- a/src/assets/icons/weather/rain.svg +++ b/src/assets/icons/weather/rain.svg @@ -1,12 +1,11 @@ - + - diff --git a/src/components/alarmModal/alarm.tsx b/src/components/alarmModal/alarm.tsx new file mode 100644 index 0000000..c6f0a9e --- /dev/null +++ b/src/components/alarmModal/alarm.tsx @@ -0,0 +1,14 @@ +import type { TAlarm } from '@/types/alarm/alarm'; + +import ChevronForward from '@/assets/icons/default_arrows/chevron_forward.svg?react'; + +function Alarm({ title }: TAlarm) { + return ( +
+
{title}
+ +
+ ); +} + +export default Alarm; diff --git a/src/components/common/EditableInputBox.tsx b/src/components/common/EditableInputBox.tsx index 56a6fcb..1c65e89 100644 --- a/src/components/common/EditableInputBox.tsx +++ b/src/components/common/EditableInputBox.tsx @@ -65,7 +65,7 @@ export default function EditableInputBox({ `; return ( -
+
{label &&

{label}

}
diff --git a/src/components/common/ToggleSwitch.tsx b/src/components/common/ToggleSwitch.tsx index 8108a49..83e7015 100644 --- a/src/components/common/ToggleSwitch.tsx +++ b/src/components/common/ToggleSwitch.tsx @@ -1,7 +1,5 @@ -//setting - ON, OFF Toggle Switch import { useEffect, useState } from 'react'; -// Props 정의 interface IToggleSwitchProps { value?: boolean; onChange?: (val: boolean) => void; @@ -11,17 +9,14 @@ interface IToggleSwitchProps { } export default function ToggleSwitch({ value, onChange, onLabel = 'ON', offLabel = 'OFF', className = '' }: IToggleSwitchProps) { - // 내부 상태 관리 (초깃값은 props.value, 없으면 false) const [isOn, setIsOn] = useState(value ?? false); - // 외부에서 value가 바뀌면 내부 상태도 동기화 useEffect(() => { if (value !== undefined) { setIsOn(value); } }, [value]); - // 버튼 클릭 시 상태 토글 const handleToggle = () => { const next = !isOn; setIsOn(next); @@ -31,32 +26,22 @@ export default function ToggleSwitch({ value, onChange, onLabel = 'ON', offLabel return ( ); diff --git a/src/components/common/graySvgButton.tsx b/src/components/common/graySvgButton.tsx index 9c9d2d7..d074938 100644 --- a/src/components/common/graySvgButton.tsx +++ b/src/components/common/graySvgButton.tsx @@ -1,10 +1,10 @@ -import type { ReactElement } from 'react'; +import React from 'react'; import ArrowLeftCircle from '@/assets/icons/Arrow_left_circle.svg?react'; import ErrorCircle from '@/assets/icons/Error-circle_Fill.svg?react'; type TGraySVGButton = { - child?: ReactElement; + child?: React.ReactNode; type?: 'cancle' | 'backward'; onClick: () => void; size?: 'big' | 'default'; diff --git a/src/components/common/modalProvider.tsx b/src/components/common/modalProvider.tsx index 579d80a..821efa9 100644 --- a/src/components/common/modalProvider.tsx +++ b/src/components/common/modalProvider.tsx @@ -1,9 +1,12 @@ import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import DateCourseSearchFilterModal from '../modal/dateCourseSearchFilterModal'; -import ErrorModal from '../modal/errorModal'; -import SettingsModal from '../modal/SettingModal'; +import AlarmModal from '@/components/modal/alarmModal'; +import DateCourseSearchFilterModal from '@/components/modal/dateCourseSearchFilterModal'; +import ErrorModal from '@/components/modal/errorModal'; +import SettingsModal from '@/components/modal/SettingModal'; + +import RegionModal from '../modal/regionModal'; import useModalStore from '@/store/useModalStore'; @@ -13,12 +16,16 @@ export const MODAL_TYPES = { ErrorModal: 'ErrorModal', DateCourseSearchFilterModal: 'DateCourseSearchFilterModal', SettingsModal: 'SettingsModal', //설정 모달 추가 + AlarmModal: 'AlarmModal', + RegionModal: 'RegionModal', }; export const MODAL_COMPONENTS = { [MODAL_TYPES.ErrorModal]: ErrorModal, [MODAL_TYPES.DateCourseSearchFilterModal]: DateCourseSearchFilterModal, [MODAL_TYPES.SettingsModal]: SettingsModal, + [MODAL_TYPES.AlarmModal]: AlarmModal, + [MODAL_TYPES.RegionModal]: RegionModal, }; export default function ModalProvider() { diff --git a/src/components/home/banner.tsx b/src/components/home/banner.tsx index 3230142..6e94f3b 100644 --- a/src/components/home/banner.tsx +++ b/src/components/home/banner.tsx @@ -5,28 +5,35 @@ import Button from '../common/Button'; import ChevronBack from '@/assets/icons/default_arrows/chevron_back.svg?react'; import ChevronForward from '@/assets/icons/default_arrows/chevron_forward.svg?react'; -import scroll from '@/images/scroll.png'; +import bicycle from '@/images/banner/bicycle.png'; +import bukchon from '@/images/banner/bukchon.png'; +import itaewon from '@/images/banner/itaewon.png'; +import sungsu from '@/images/banner/sungsu.png'; const slides = [ { title: '서울 성수동 : 옛것과 새로운 것이 교차하는 하루', description: '1960년대부터 조성된 오래된 공장 건물과 최근 벽돌 건물들의 분위기', tags: ['#활발한 활동', '#레트로 감성', '#서울 핫플'], + img: sungsu, }, { title: '한강 자전거 데이트 : 바람 따라 달리는 낭만', description: '도심 속 자연을 만끽하며 힐링 타임', tags: ['#운동 데이트', '#자연과 함께', '#저녁노을'], + img: bicycle, }, { title: '이태원 세계 음식 투어 : 입 안 가득 여행', description: '세계 각국의 맛을 한 자리에서 즐기기', tags: ['#미식가 커플', '#이국적인 분위기', '#도심 속 여행'], + img: itaewon, }, { title: '북촌 한옥마을 산책 : 전통의 미를 따라 걷기', description: '골목골목 숨어있는 사진 명소', tags: ['#한옥', '#조용한 산책', '#전통과 현대'], + img: bukchon, }, ]; @@ -34,16 +41,14 @@ function Banner() { const navigate = useNavigate(); const [currentIndex, setCurrentIndex] = useState(0); - // ⏱️ 자동 슬라이드 타이머 useEffect(() => { const interval = setInterval(() => { setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length); }, 3000); // 2초 - return () => clearInterval(interval); // 언마운트 시 정리 + return () => clearInterval(interval); }, []); - // ⬅️➡️ 버튼 클릭 핸들러 const goToPrev = () => { setCurrentIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length); }; @@ -52,12 +57,12 @@ function Banner() { setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length); }; - const { title, description, tags } = slides[currentIndex]; + const { title, description, tags, img } = slides[currentIndex]; return (
- 배너 - + 배너 +
{/* 내용 */}
오늘의 데이트 추천
diff --git a/src/components/home/dateCourseStore.tsx b/src/components/home/dateCourseStore.tsx index 819f18d..e969977 100644 --- a/src/components/home/dateCourseStore.tsx +++ b/src/components/home/dateCourseStore.tsx @@ -1,8 +1,16 @@ +import { Navigate } from 'react-router-dom'; + +import { useDateCourseSavedCount } from '@/hooks/home/useDateCourseStates'; + import MainCard from './mainCard'; import ArchiveBlank from '@/assets/icons/Archive_Blank.svg?react'; function DateCourseStore() { + const { data, isLoading, error } = useDateCourseSavedCount(); + if (error) { + return ; + } return (
@@ -11,11 +19,16 @@ function DateCourseStore() {
내 데이트 코스를
-
2,345명
+ {isLoading ? ( +
로딩...
+ ) : ( +
{data?.result.count}명
+ )}
이 저장했어요.
); } + export default DateCourseStore; diff --git a/src/components/home/dateLocation.tsx b/src/components/home/dateLocation.tsx index d402dce..e8487fa 100644 --- a/src/components/home/dateLocation.tsx +++ b/src/components/home/dateLocation.tsx @@ -1,31 +1,45 @@ +import { useMemo } from 'react'; +import { Navigate } from 'react-router-dom'; +import ClipLoader from 'react-spinners/ClipLoader'; + +import { useMonthlyPlaceStates } from '@/hooks/home/useDatePlaceStates'; + import MainCard from '@/components/home/mainCard'; function DateLocation() { + const { data, isLoading, error } = useMonthlyPlaceStates(); + const maxCount = useMemo(() => { + return data?.result?.datePlaceLogList?.reduce((max, cur) => Math.max(max, cur.count), 0) ?? 0; + }, [data]); + if (error) { + return ; + } + if (isLoading) { + return ( + + + + ); + } return (
WithTime에 등록된 데이트 장소 수
-
- 230 -
-
2022
-
-
- 430 -
-
2023
-
-
- 830 -
-
2024
-
-
- 1,230 -
-
2025
-
+ {(data?.result?.datePlaceLogList ?? []).map((graph, idx) => { + const height = maxCount ? Math.max((graph.count / maxCount) * 200, 4) : 4; + return ( +
+ {graph.count} +
+
{graph.month}월
+
+ ); + })}
diff --git a/src/components/home/dateRecommend.tsx b/src/components/home/dateRecommend.tsx index 281d32b..fdb13e5 100644 --- a/src/components/home/dateRecommend.tsx +++ b/src/components/home/dateRecommend.tsx @@ -1,139 +1,163 @@ -import { useState } from 'react'; +// src/components/home/dateRecommend.tsx +import { type ComponentType, type SVGProps, useEffect, useMemo, useState } from 'react'; import { Line } from 'react-chartjs-2'; +import ClipLoader from 'react-spinners/ClipLoader'; import { CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, Title, Tooltip } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; +import { chartOptions } from '@/constants/chartOptions'; + +import { getNextSevenDay } from '@/utils/getNextSevenDay'; +import { getTodayString } from '@/utils/getTodayString'; +import { normalizeEmojiKey } from '@/utils/normalizeEmojiKey'; +import { getWeatherSentence } from '@/utils/weatherMessage'; + +import { useGetUserRegion } from '@/hooks/home/useUserRegion'; +import { useRainyInfo, useWeatherForecast } from '@/hooks/home/useWeather'; + import Button from '@/components/common/Button'; import MainCard from '@/components/home/mainCard'; +import { MODAL_TYPES } from '../common/modalProvider'; + +import Cloud from '@/assets/icons/weather/cloud.svg?react'; +import Rain from '@/assets/icons/weather/rain.svg?react'; +import Snow from '@/assets/icons/weather/snow.svg?react'; import Sun from '@/assets/icons/weather/sun.svg?react'; +import Shower from '@/assets/icons/weather/sunShower.svg?react'; +import useModalStore from '@/store/useModalStore'; ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ChartDataLabels); +const EMOJI_KEY_ICON_MAP: Record>> = { + sunny: Sun, + cloudy: Cloud, + rainy: Rain, + shower: Shower, + snowy: Snow, +}; + function DateRecommend() { - const [date, setDate] = useState(0); - const rainData = { - labels: ['7월 9일', '7월 10일', '7월 11일', '7월 12일', '7월 13일', '7월 14일', '7월 15일'], - datasets: [ - { - label: '강수확률', - data: [10, 30, 10, 45, 70, 45, 90], - borderColor: '#3FA495', - borderWidth: 2, - pointRadius: 4, - pointBackgroundColor: '#ffffff', - fill: false, - tension: 0, - datalabels: { - align: 'end' as const, - anchor: 'end' as const, - offset: 2, - color: '#c3c3c3', - font: { - weight: 'bold' as const, - size: 12, - }, - formatter: (value: number) => `${value}%`, - }, - }, - ], - }; - - const chartOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false, - }, - tooltip: { - enabled: false, - }, - }, - scales: { - x: { - grid: { - display: false, - }, - ticks: { - color: '#616161', - font: { - size: 12, - weight: 500, + const [dateIdx, setDateIdx] = useState(0); + const { data } = useGetUserRegion(); + // 기준 날짜/지역 + const startDate = getTodayString(); + const regionId = data?.result.regionId; + + const { data: forecastData, isLoading: forecastLoading } = useWeatherForecast({ startDate, regionId: regionId ?? 0 }); + const { data: rainyData, isLoading: rainyLoading } = useRainyInfo({ startDate, regionId: regionId ?? 0 }); + + const safeStartDate = rainyData?.result?.startDate ?? startDate; + const dateList = useMemo(() => getNextSevenDay(safeStartDate), [safeStartDate]); + + // 차트 데이터: 로딩/초기엔 빈 배열 + const rainData = useMemo(() => { + const values = rainyData?.result?.dailyPrecipitations?.map((f) => f.precipitationProbability) ?? []; + return { + labels: dateList, + datasets: [ + { + label: '강수확률', + data: values, + borderColor: '#3FA495', + borderWidth: 2, + pointRadius: 4, + pointBackgroundColor: '#ffffff', + fill: false, + tension: 0, + datalabels: { + align: 'end' as const, + anchor: 'end' as const, + offset: 2, + color: '#c3c3c3', + font: { weight: 'bold' as const, size: 12 }, + formatter: (value: number) => `${value}%`, }, }, - drawBorder: false, - drawOnChartArea: false, - border: { - display: false, - }, - }, - y: { - min: 0, - max: 100, - ticks: { - stepSize: 10, - }, - display: false, - grid: { - display: false, - }, - }, - }, - }; + ], + }; + }, [dateList, rainyData]); + const { openModal } = useModalStore(); + const currentRec = forecastData?.result?.dailyRecommendations?.[dateIdx]; + const rawKey = currentRec?.emoji; + const iconKey = normalizeEmojiKey(rawKey); + const Icon = EMOJI_KEY_ICON_MAP[iconKey] ?? Sun; + + useEffect(() => { + if (dateIdx >= dateList.length) setDateIdx(0); + }, [dateIdx, dateList.length]); + + if (rainyLoading || forecastLoading) { + return ( + + + + ); + } return (
{/* 상단 텍스트 */}
-
이번 주 강남구 데이트 추천
- +
이번 주 {forecastData?.result?.region?.regionName ?? '지역'} 데이트 추천
+
{/* 날짜 버튼 */}
- {rainData.labels.map((data, idx) => ( + {dateList.map((d, idx) => ( ))}
{/* 날씨 설명 */} -
-
-
- - 맑고 무더운 날 +
+
+
+ + + {forecastLoading + ? '날씨 로딩 중...' + : currentRec + ? getWeatherSentence({ + weather: currentRec.weatherType!, + temp: currentRec.tempCategory!, + }) + : '날씨 정보 없음'} +
+
- #실내추천 - - #카페데이트 - - - #시원한하루 - + {(currentRec?.keywords ?? []).map((tag: string, index: number) => ( + + {tag} + + ))}
-
- 맑은 하늘무더운 날씨가 기승을 - 부려요. -
- 우산 없이도 걱정 없어요. -
- 시원한 음료와 함께 실내 데이트가 좋아요. +
+ {currentRec?.message ?? (forecastLoading ? '로드 중...' : '메시지 없음')}
+ + {/* 차트 */}
이번주 강수확률 (%)
diff --git a/src/components/home/dateTimes.tsx b/src/components/home/dateTimes.tsx index 4c92da3..1788307 100644 --- a/src/components/home/dateTimes.tsx +++ b/src/components/home/dateTimes.tsx @@ -1,10 +1,24 @@ +import ClipLoader from 'react-spinners/ClipLoader'; + +import { useDateTimeStates } from '@/hooks/home/useDateTimes'; + import MainCard from './mainCard'; function DateTimes() { + const { data: states, isLoading, error } = useDateTimeStates(); + + const displayStates = states?.result; + if (isLoading) { + return ( +
+ +
+ ); + } return (
- {/* 첫 번째 카드 */} + {/* 첫 번째 카드 - WithTime 이용자 평균 데이트 횟수 */}
최근 1개월
@@ -12,21 +26,27 @@ function DateTimes() {
평균 데이트 횟수
-
4.6회
+ +
+ {displayStates?.averageDateCount != null ? `${displayStates.averageDateCount}회` : '—'} +
+
- {/* 두 번째 카드 */} + + {/* 두 번째 카드 - 나의 데이트 횟수 */}
최근 1개월
-
- WithTime 이용자 -
- 평균 데이트 횟수 -
-
2회
+
나의 데이트 횟수
+ +
{displayStates?.myDateCount != null ? `${displayStates.myDateCount}회` : '—'}
+ + {/* 에러 상태 표시 */} + {error &&
데이터를 불러올 수 없습니다
} ); } + export default DateTimes; diff --git a/src/components/home/info.tsx b/src/components/home/info.tsx index 1ab8083..09a0df9 100644 --- a/src/components/home/info.tsx +++ b/src/components/home/info.tsx @@ -1,11 +1,27 @@ -import { useNavigate } from 'react-router-dom'; +import { Navigate, useNavigate } from 'react-router-dom'; +import ClipLoader from 'react-spinners/ClipLoader'; + +import { useGetNotices } from '@/hooks/notices/useGetNotices'; import MainCard from './mainCard'; import AddCircleBlank from '@/assets/icons/add-circle_Blank.svg?react'; -export default function MainInfo() { +function MainInfo() { const navigate = useNavigate(); + const { data, error, isLoading } = useGetNotices({ size: 3, page: 0, noticeCategory: 'SERVICE' }); + + if (error) { + return ; + } + if (isLoading) { + return ( + + + + ); + } + const notices = data?.pages.flatMap((page) => page.result.noticeList) ?? []; return (
@@ -20,11 +36,30 @@ export default function MainInfo() {
    -
  • 여름 맞이 피서 데이트 코스 추가 업데이트
  • -
  • 슬기로운 데이트를 하고싶은 커플을 위한 이벤트
  • -
  • 위티 사칭 웹사이트 및 보이스피싱 주의 안내
  • + {notices.length === 0 ? ( +
  • 공지사항이 없습니다.
  • + ) : ( + notices.map((notice) => ( +
  • + +
  • + )) + )}
); } + +export default MainInfo; diff --git a/src/components/home/level.tsx b/src/components/home/level.tsx index 69bb720..0ea8d0a 100644 --- a/src/components/home/level.tsx +++ b/src/components/home/level.tsx @@ -1,29 +1,44 @@ +import { useEffect, useState } from 'react'; + +import type { IGradeInfo } from '@/types/home/level'; + import MainCard from '@/components/home/mainCard'; -import ramji from '@/images/animals/ramgi.png'; +import mainCharacter from '@/images/mainCharacter.png'; + +function Level({ grade, nextRequiredPoint }: IGradeInfo) { + const [percentage, setPercentage] = useState(0); + + useEffect(() => { + setPercentage(100 - nextRequiredPoint); + }, [nextRequiredPoint]); -function Level() { return (
캐릭터
-
Flirt
+
{grade}
+
다음 데이트 레벨 성장까지
- 20점 - 의 데이트 활동이 필요합니다 + {`${nextRequiredPoint}점`} + 의 데이트 활동이 필요합니다
+ {/* 진행바 */}
-
+
@@ -31,4 +46,5 @@ function Level() { ); } + export default Level; diff --git a/src/components/home/wordCloud.tsx b/src/components/home/wordCloud.tsx index aad36d3..dabf9dc 100644 --- a/src/components/home/wordCloud.tsx +++ b/src/components/home/wordCloud.tsx @@ -1,34 +1,26 @@ -import { useEffect, useRef } from 'react'; -import type { DebouncedFunc } from 'lodash'; +import { useEffect, useRef, useState } from 'react'; +import { type DebouncedFunc } from 'lodash'; import throttle from 'lodash.throttle'; import type { ListEntry } from 'wordcloud'; import WordCloud from 'wordcloud'; -const keywords = [ - { text: '드라이브', value: 19 }, - { text: '포토존', value: 18 }, - { text: '카페', value: 20 }, - { text: '감성', value: 17 }, - { text: '맛집', value: 19.5 }, - { text: '피크닉', value: 16 }, - { text: '영화관', value: 17.5 }, - { text: '산책', value: 15 }, - { text: '실내', value: 15.5 }, - { text: '레저', value: 14 }, - { text: '홍대', value: 19.2 }, - { text: '전시', value: 14.8 }, - { text: '성수', value: 18.8 }, - { text: '가로수길', value: 15.2 }, - { text: '행궁동', value: 15.3 }, - { text: '레트로', value: 15.6 }, - { text: '쇼핑', value: 15.5 }, -]; +import { useWeeklyKeywords } from '@/hooks/home/useKeywordStates'; -export default function WordCloudCanvas() { +function WordCloudCanvas() { + const { data } = useWeeklyKeywords(); + const [list, setList] = useState([]); const containerRef = useRef(null); const canvasRef = useRef(null); const throttledDrawRef = useRef void> | null>(null); + useEffect(() => { + if (data?.result?.placeCategoryLogList) { + setList(data.result.placeCategoryLogList.map((k) => [String(k.placeCategoryLabel), Number(k.count)]) as ListEntry[]); + } else { + setList([]); + } + }, [data]); + const drawCloud = (width: number, height: number) => { const canvas = canvasRef.current; if (!canvas) return; @@ -36,25 +28,29 @@ export default function WordCloudCanvas() { const dpr = window.devicePixelRatio || 1; canvas.width = width * dpr; canvas.height = height * dpr; - canvas.style.width = `${width}px`; + canvas.style.width = `${width - 10}px`; canvas.style.height = `${height}px`; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - const list: ListEntry[] = keywords.map((k) => [k.text, k.value]); - WordCloud(canvas, { list, gridSize: Math.round(8 * (width / 400)), - weightFactor: (size) => size * (width / 350), + weightFactor: (size) => { + const factor = width / 1500; + const minFactor = 0.3; + const maxFactor = 1.5; + return size * Math.max(minFactor, Math.min(maxFactor, factor)); + }, fontFamily: 'Pretendard, sans-serif', color: (_word, weight) => { - if (Number(weight) > 18) return '#186a6d'; - if (Number(weight) > 17) return '#3fa495'; - if (Number(weight) > 16) return '#7fe4c1'; - if (Number(weight) > 15) return '#b5f7d3'; + if (Number(weight) > 95) return '#186a6d'; + if (Number(weight) > 85) return '#389486'; + if (Number(weight) > 75) return '#6fc9a9'; + if (Number(weight) > 65) return '#99d4b4'; + if (Number(weight) > 55) return '#b5f7d3'; return '#c3c3c3'; }, rotateRatio: 0.4, @@ -79,13 +75,17 @@ export default function WordCloudCanvas() { const { width, height } = container.getBoundingClientRect(); drawCloud(width, height); // 최초 한 번 직접 호출 } - }, []); + return () => { + WordCloud.stop(); + }; + }, [list]); // ResizeObserver 적용 useEffect(() => { if (!containerRef.current || !throttledDrawRef.current) return; const resizeObserver = new ResizeObserver((entries) => { + WordCloud.stop(); for (const entry of entries) { const { width, height } = entry.contentRect; throttledDrawRef.current?.(width, height); @@ -98,14 +98,15 @@ export default function WordCloudCanvas() { resizeObserver.disconnect(); throttledDrawRef.current?.cancel?.(); }; - }, []); + }, [list]); return (
이번주 인기 데이트 키워드 현황
-
- +
+
); } +export default WordCloudCanvas; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ccb6d17..20011d7 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -2,21 +2,22 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; import MobileMenu from './MobileMenu'; -import SettingsModal from '../modal/SettingModal'; +import { MODAL_TYPES } from '../common/modalProvider'; import BurgerIcon from '@/assets/icons/Burger_fill.svg?react'; import ClearIcon from '@/assets/icons/Clear.svg?react'; import NotificationsIcon from '@/assets/icons/notifications_Blank.svg?react'; import SettingsIcon from '@/assets/icons/settings_Blank.svg?react'; import NavbarLogo from '@/assets/withTimeLogo/navbarLogo.svg?react'; +import useModalStore from '@/store/useModalStore'; interface IHeaderProps { mode?: 'full' | 'minimal'; // full: nav + border | minimal: 로고만 } export default function Header({ mode = 'full' }: IHeaderProps) { - const [isSettingsOpen, setIsSettingsOpen] = useState(false); //설정 모달 const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); // 모바일 메뉴 + const { openModal } = useModalStore(); const showNav = mode === 'full'; const showBorder = mode === 'full'; @@ -56,10 +57,10 @@ export default function Header({ mode = 'full' }: IHeaderProps) { {/* 아이콘 버튼 */}
- + +
@@ -83,10 +84,7 @@ export default function Header({ mode = 'full' }: IHeaderProps) {
{/* 모바일 메뉴 */} - {isMobileMenuOpen && setIsMobileMenuOpen(false)} onOpenSettings={() => setIsSettingsOpen(true)} />} - - {/* 설정 모달 */} - {isSettingsOpen && setIsSettingsOpen(false)} />} + {isMobileMenuOpen && setIsMobileMenuOpen(false)} onOpenSettings={() => openModal(MODAL_TYPES.SettingsModal)} />} ); } diff --git a/src/components/layout/MobileMenu.tsx b/src/components/layout/MobileMenu.tsx index 6fe19e3..d8cee30 100644 --- a/src/components/layout/MobileMenu.tsx +++ b/src/components/layout/MobileMenu.tsx @@ -1,8 +1,11 @@ import { Link } from 'react-router-dom'; +import { MODAL_TYPES } from '../common/modalProvider'; + import ClearIcon from '@/assets/icons/Clear.svg?react'; import NotificationsIcon from '@/assets/icons/notifications_Blank.svg?react'; import SettingsIcon from '@/assets/icons/settings_Blank.svg?react'; +import useModalStore from '@/store/useModalStore'; interface IMobileMenuProps { onClose: () => void; @@ -10,9 +13,10 @@ interface IMobileMenuProps { } export default function MobileMenu({ onClose, onOpenSettings }: IMobileMenuProps) { + const { openModal } = useModalStore(); return ( <> - {/* 검정 반투명 배경 오버레이 */} + {/* 배경 오버레이 */}
{/* 사이드 메뉴 */} @@ -46,9 +50,14 @@ export default function MobileMenu({ onClose, onOpenSettings }: IMobileMenuProps {/* 알림, 설정 */}
- + + {index < regionList.result.regions.length - 1 &&
} + + ))} + +
+ )} +
+ + ); +} + +export default RegionModal; diff --git a/src/components/settingTab/AlarmSetting.tsx b/src/components/settingTab/AlarmSetting.tsx index e59d7b4..e6ccc61 100644 --- a/src/components/settingTab/AlarmSetting.tsx +++ b/src/components/settingTab/AlarmSetting.tsx @@ -36,12 +36,13 @@ export default function AlarmSetting() { ]; return ( -
+
{alarmItems.map(({ label, key }) => ( -
- {/* 알람 이름 */} -

{label}

- {/* 각 토글 스위치 */} +
+ {/* 텍스트 */} +

{label}

+ + {/* 토글 */} handleToggle(key)} onLabel="ON" offLabel="OFF" />
))} diff --git a/src/components/settingTab/InfoSetting.tsx b/src/components/settingTab/InfoSetting.tsx index 31dd49e..aa29684 100644 --- a/src/components/settingTab/InfoSetting.tsx +++ b/src/components/settingTab/InfoSetting.tsx @@ -19,7 +19,7 @@ export default function InfoSetting() { }, []); return ( -
+
{/* 닉네임 - 수정가능 */} +
{/* 등급 */}

현재 등급은 Premium 입니다. @@ -38,7 +38,7 @@ export default function MembershipSetting() {

다음 결제 예정일 : 2025.06.14

{/* 자동 결제 토글 */} -
+

자동 결제

diff --git a/src/constants/chartOptions.ts b/src/constants/chartOptions.ts new file mode 100644 index 0000000..106e699 --- /dev/null +++ b/src/constants/chartOptions.ts @@ -0,0 +1,24 @@ +import type { ChartOptions } from 'chart.js'; + +export const chartOptions: ChartOptions<'line'> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { enabled: false }, + }, + scales: { + x: { + grid: { display: false, drawOnChartArea: false }, + ticks: { color: '#616161', font: { size: 12, weight: 500 } }, + border: { display: false }, + }, + y: { + min: 0, + max: 100, + ticks: { stepSize: 10 }, + display: false, + grid: { display: false }, + }, + }, +}; diff --git a/src/constants/weather.ts b/src/constants/weather.ts new file mode 100644 index 0000000..19c945b --- /dev/null +++ b/src/constants/weather.ts @@ -0,0 +1,31 @@ +export enum WeatherType { + CLEAR = 'CLEAR', // 맑고 + CLOUDY = 'CLOUDY', // 흐리고 + RAINY = 'RAINY', // 비오고 + SNOWY = 'SNOWY', // 눈오고 + RAIN_SNOW = 'RAIN_SNOW', // 비/눈오는 + SHOWER = 'SHOWER', // 소나기 +} + +export const WeatherTypeLabel: Record = { + [WeatherType.CLEAR]: '맑고', + [WeatherType.CLOUDY]: '흐리고', + [WeatherType.RAINY]: '비오고', + [WeatherType.SNOWY]: '눈오는', + [WeatherType.RAIN_SNOW]: '비/눈오는', + [WeatherType.SHOWER]: '소나기오는', +}; + +export enum TempCategory { + CHILLY = 'CHILLY', // 쌀쌀한 날씨 + COOL = 'COOL', // 선선한 날씨 + MILD = 'MILD', // 무난한 날씨 + HOT = 'HOT', // 무더운 날씨 +} + +export const TempCategoryLabel: Record = { + [TempCategory.CHILLY]: '쌀쌀한 날씨', + [TempCategory.COOL]: '선선한 날씨', + [TempCategory.MILD]: '무난한 날씨', + [TempCategory.HOT]: '무더운 날씨', +}; diff --git a/src/firebase/firebase.ts b/src/firebase/firebase.ts new file mode 100644 index 0000000..2762064 --- /dev/null +++ b/src/firebase/firebase.ts @@ -0,0 +1,41 @@ +// src/firebase/firebase.ts +import { initializeApp } from 'firebase/app'; +import { getMessaging, getToken } from 'firebase/messaging'; + +const firebaseConfig = { + apiKey: import.meta.env.VITE_FIREBASE_API_KEY, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_PROJECT_ID, + storageBucket: import.meta.env.VITE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_MESSAGE_SENDER_ID, + appId: import.meta.env.VITE_APP_ID, + measurementId: import.meta.env.VITE_MEASUREMENT_ID, +}; + +const app = initializeApp(firebaseConfig); +const messaging = getMessaging(app); + +export const generateToken = async () => { + try { + const token = await getToken(messaging, { + vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY, + }); + if (!token) { + console.warn('FCM 토큰 생성에 실패했습니다. 알림 권한을 확인해주세요.'); + } + return token; + } catch (error) { + console.error('FCM 토큰 생성 중 오류 발생:', error); + return null; + } +}; + +export const registerServiceWorker = async () => { + try { + if ('serviceWorker' in navigator) { + await navigator.serviceWorker.register('/firebase-messaging-sw.js'); + } + } catch (err) { + console.error('Service Worker registration failed:', err); + } +}; diff --git a/src/hooks/alarm/useDeviceToken.ts b/src/hooks/alarm/useDeviceToken.ts new file mode 100644 index 0000000..73523c4 --- /dev/null +++ b/src/hooks/alarm/useDeviceToken.ts @@ -0,0 +1,39 @@ +// src/hooks/alarm/useDeviceToken.ts +import { useEffect } from 'react'; +import { isSupported } from 'firebase/messaging'; + +import { postDeviceToken } from '@/api/alarm/alarm'; // 서버에 FCM 토큰 전송하는 API 함수 +import { generateToken, registerServiceWorker } from '@/firebase/firebase'; + +export const useDeviceToken = () => { + useEffect(() => { + const setupFCM = async () => { + if (!(await isSupported())) { + console.warn('FCM은 현재 브라우저에서 지원되지 않습니다.'); + return; + } + + await registerServiceWorker(); + const token = await generateToken(); + + if (token) { + try { + await postDeviceToken({ deviceToken: token }); // 서버에 전송 + } catch (err) { + console.error('디바이스 토큰 서버 전송 실패:', err); + } + } + }; + + const handleClick = () => { + setupFCM(); + window.removeEventListener('click', handleClick); + }; + + window.addEventListener('click', handleClick); + + return () => { + window.removeEventListener('click', handleClick); + }; + }, []); +}; diff --git a/src/hooks/alarm/useGetAlarm.ts b/src/hooks/alarm/useGetAlarm.ts new file mode 100644 index 0000000..1444051 --- /dev/null +++ b/src/hooks/alarm/useGetAlarm.ts @@ -0,0 +1,15 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import type { TRequestGetAlarm } from '@/types/alarm/alarm'; + +import { getAlarm } from '@/api/alarm/alarm'; +import { alarmKeys } from '@/queryKey/queryKey'; + +export const useGetAlarm = ({ cursor, size }: TRequestGetAlarm) => { + return useInfiniteQuery({ + queryKey: alarmKeys.getAlarm(size ?? 5, cursor).queryKey, + queryFn: ({ pageParam = cursor }) => getAlarm({ cursor: pageParam, size: size ?? 5 }), + initialPageParam: cursor, + getNextPageParam: (lastPage) => lastPage.result.cursor ?? undefined, + }); +}; diff --git a/src/hooks/customQuery.ts b/src/hooks/customQuery.ts index c5b6ace..2ab4371 100644 --- a/src/hooks/customQuery.ts +++ b/src/hooks/customQuery.ts @@ -13,7 +13,7 @@ export function useCoreQuery( queryKey: keyName, queryFn: query, ...options, - staleTime: 1000 * 60 * 5, + staleTime: options?.staleTime ?? 1000 * 60 * 5, }); } diff --git a/src/hooks/home/useDateCourseStates.ts b/src/hooks/home/useDateCourseStates.ts new file mode 100644 index 0000000..39e50c2 --- /dev/null +++ b/src/hooks/home/useDateCourseStates.ts @@ -0,0 +1,12 @@ +import { useCoreQuery } from '@/hooks/customQuery'; + +import { getDateCourseSavedCount } from '@/api/home/dateCourse'; +import { HomeKeys } from '@/queryKey/queryKey'; + +// 데이트 코스 저장 횟수 훅 +export const useDateCourseSavedCount = () => { + return useCoreQuery(HomeKeys.dateCourseSave().queryKey, getDateCourseSavedCount, { + gcTime: 15 * 60 * 1000, // 15분 + retry: 3, + }); +}; diff --git a/src/hooks/home/useDatePlaceStates.ts b/src/hooks/home/useDatePlaceStates.ts new file mode 100644 index 0000000..35c60df --- /dev/null +++ b/src/hooks/home/useDatePlaceStates.ts @@ -0,0 +1,11 @@ +import { useCoreQuery } from '../customQuery'; + +import { getMonthlyDatePlaceStates } from '@/api/home/dateTimes'; +import { HomeKeys } from '@/queryKey/queryKey'; + +export const useMonthlyPlaceStates = () => { + return useCoreQuery(HomeKeys.monthlyPlaceStates().queryKey, () => getMonthlyDatePlaceStates(), { + gcTime: 15 * 60 * 1000, + retry: 3, + }); +}; diff --git a/src/hooks/home/useDateTimes.ts b/src/hooks/home/useDateTimes.ts new file mode 100644 index 0000000..225e46a --- /dev/null +++ b/src/hooks/home/useDateTimes.ts @@ -0,0 +1,8 @@ +import { useCoreQuery } from '@/hooks/customQuery'; + +import { getDateTimeStates } from '@/api/home/dateTimes'; +import { HomeKeys } from '@/queryKey/queryKey'; + +export const useDateTimeStates = () => { + return useCoreQuery(HomeKeys.dateTimes().queryKey, () => getDateTimeStates(), { staleTime: 5 * 60 * 1000, gcTime: 15 * 60 * 1000, retry: 3 }); +}; diff --git a/src/hooks/home/useKeywordStates.ts b/src/hooks/home/useKeywordStates.ts new file mode 100644 index 0000000..3cc4d4d --- /dev/null +++ b/src/hooks/home/useKeywordStates.ts @@ -0,0 +1,12 @@ +import { useCoreQuery } from '@/hooks/customQuery'; + +import { getWeeklyKeywords } from '@/api/home/keyword'; +import { HomeKeys } from '@/queryKey/queryKey'; + +// 이번 주 인기 키워드 훅 +export const useWeeklyKeywords = () => { + return useCoreQuery(HomeKeys.keywords().queryKey, getWeeklyKeywords, { + gcTime: 30 * 60 * 1000, + retry: 3, + }); +}; diff --git a/src/hooks/home/useUserGrade.ts b/src/hooks/home/useUserGrade.ts new file mode 100644 index 0000000..bc79b16 --- /dev/null +++ b/src/hooks/home/useUserGrade.ts @@ -0,0 +1,12 @@ +import { useCoreQuery } from '@/hooks/customQuery'; + +import { getUserGrade } from '@/api/home/level'; +import { HomeKeys } from '@/queryKey/queryKey'; + +// 사용자 등급 정보 훅 +export const useUserGrade = () => { + return useCoreQuery(HomeKeys.getUserGrade().queryKey, () => getUserGrade(), { + gcTime: 15 * 60 * 1000, // 15분 + retry: 3, + }); +}; diff --git a/src/hooks/home/useUserRegion.ts b/src/hooks/home/useUserRegion.ts new file mode 100644 index 0000000..bcb7ac9 --- /dev/null +++ b/src/hooks/home/useUserRegion.ts @@ -0,0 +1,12 @@ +import { useCoreMutation, useCoreQuery } from '../customQuery'; + +import { getUserRegion, patchUserRegion } from '@/api/home/region'; +import { HomeKeys } from '@/queryKey/queryKey'; + +export function useUserRegion() { + return useCoreMutation(patchUserRegion); +} + +export function useGetUserRegion() { + return useCoreQuery(HomeKeys.userRegion().queryKey, getUserRegion); +} diff --git a/src/hooks/home/useWeather.ts b/src/hooks/home/useWeather.ts new file mode 100644 index 0000000..cb9cc3c --- /dev/null +++ b/src/hooks/home/useWeather.ts @@ -0,0 +1,19 @@ +import { getPrecipitation, getWeeklyWeatherRecommendation } from '../../api/home/weather'; +import { useCoreQuery } from '../customQuery'; + +import { HomeKeys } from '@/queryKey/queryKey'; + +// 주간 날씨 추천 훅 +export const useWeatherForecast = ({ startDate, regionId }: { startDate: string; regionId: number }) => { + return useCoreQuery(HomeKeys.weather(startDate, regionId).queryKey, () => getWeeklyWeatherRecommendation({ startDate, regionId: regionId! }), { + staleTime: 1000 * 60 * 30, + enabled: !!startDate && !!regionId, + }); +}; + +export const useRainyInfo = ({ startDate, regionId }: { startDate: string; regionId: number }) => { + return useCoreQuery(HomeKeys.rainyInfo(startDate, regionId).queryKey, () => getPrecipitation({ startDate, regionId: regionId! }), { + staleTime: 1000 * 60 * 30, + enabled: !!startDate && !!regionId, + }); +}; diff --git a/src/hooks/notices/useGetNotices.ts b/src/hooks/notices/useGetNotices.ts new file mode 100644 index 0000000..61c8451 --- /dev/null +++ b/src/hooks/notices/useGetNotices.ts @@ -0,0 +1,15 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import type { TRequestGetNoticeRequest } from '@/types/notice/notice'; + +import { fetchNotices } from '@/api/notice/notice'; +import { NoticeKeys } from '@/queryKey/queryKey'; + +export const useGetNotices = ({ size, page, noticeCategory }: TRequestGetNoticeRequest) => { + return useInfiniteQuery({ + queryKey: NoticeKeys.getAllNotices(page, size ?? 5, noticeCategory).queryKey, + queryFn: ({ pageParam = page }) => fetchNotices({ page: pageParam, size: size ?? 5, noticeCategory }), + initialPageParam: page, + getNextPageParam: (lastPage) => (lastPage.result.hasNextPage ? lastPage.result.currentPage + 1 : undefined), + }); +}; diff --git a/src/images/banner/bicycle.png b/src/images/banner/bicycle.png new file mode 100644 index 0000000..6f0cbfa Binary files /dev/null and b/src/images/banner/bicycle.png differ diff --git a/src/images/banner/bukchon.png b/src/images/banner/bukchon.png new file mode 100644 index 0000000..4a92819 Binary files /dev/null and b/src/images/banner/bukchon.png differ diff --git a/src/images/banner/itaewon.png b/src/images/banner/itaewon.png new file mode 100644 index 0000000..767038f Binary files /dev/null and b/src/images/banner/itaewon.png differ diff --git a/src/images/banner/sungsu.png b/src/images/banner/sungsu.png new file mode 100644 index 0000000..3e15354 Binary files /dev/null and b/src/images/banner/sungsu.png differ diff --git a/src/images/mainCharacter.png b/src/images/mainCharacter.png new file mode 100644 index 0000000..d2221d2 Binary files /dev/null and b/src/images/mainCharacter.png differ diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index 964de68..90838f0 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -1,3 +1,9 @@ +import { Navigate } from 'react-router-dom'; +import ClipLoader from 'react-spinners/ClipLoader'; + +import { useDeviceToken } from '@/hooks/alarm/useDeviceToken'; +import { useUserGrade } from '@/hooks/home/useUserGrade'; + import Banner from '@/components/home/banner'; import DateCourseStore from '@/components/home/dateCourseStore'; import DateLocation from '@/components/home/dateLocation'; @@ -8,18 +14,29 @@ import Level from '@/components/home/level'; import WordCloudCard from '@/components/home/wordCloud'; function Home() { + useDeviceToken(); + + const { data: gradeData, isLoading, error } = useUserGrade(); + if (error) return ; + if (isLoading) { + return ( +
+ +
+ ); + } return (
- Madeleine + {gradeData?.result.username} 님의 WithTime
- + {gradeData?.result && }
diff --git a/src/pages/notice/Notice.tsx b/src/pages/notice/Notice.tsx index 884070a..1fe1f4c 100644 --- a/src/pages/notice/Notice.tsx +++ b/src/pages/notice/Notice.tsx @@ -32,7 +32,7 @@ export default function Notice() { try { // 공지사항 목록 요청 const response = await fetchNotices({ - category: categoryKey, + noticeCategory: categoryKey, page: currentPage - 1, size: itemsPerPage, }); diff --git a/src/queryKey/queryKey.ts b/src/queryKey/queryKey.ts index 1e3fb30..0b60752 100644 --- a/src/queryKey/queryKey.ts +++ b/src/queryKey/queryKey.ts @@ -3,3 +3,24 @@ import { createQueryKeys } from '@lukemorales/query-key-factory'; export const regionKeys = createQueryKeys('region', { search: (keyword: string) => [keyword], }); + +export const alarmKeys = createQueryKeys('alarm', { + getAlarm: (size: number, cursor?: number) => [size, cursor], +}); + +export const HomeKeys = createQueryKeys('home', { + all: () => ['home'], + getUserGrade: () => ['home', 'user', 'grade'], + dateCourseSave: () => ['home', 'date-courses', 'saved-count'], + weather: (startDate, regionId) => ['home', 'weather', 'forecast', startDate, regionId], + rainyInfo: (startDate, regionId) => ['home', 'rainy', 'forecast', startDate, regionId], + keywords: () => ['home', 'keywords'], + dateTimes: () => ['home', 'dateTimes'], + monthlyPlaceStates: () => ['home', 'monthlyPlaceStates'], + userRegion: () => ['home', 'user', 'region'], +}); + +export const NoticeKeys = createQueryKeys('notice', { + all: () => ['notice'], + getAllNotices: (page: number, size: number, noticeCategory: 'SERVICE' | 'SYSTEM') => ['notice', page, size, noticeCategory], +}); diff --git a/src/types/alarm/alarm.ts b/src/types/alarm/alarm.ts new file mode 100644 index 0000000..6f2b43d --- /dev/null +++ b/src/types/alarm/alarm.ts @@ -0,0 +1,22 @@ +import type { TCommonResponse } from '../common/common'; + +export type TRequestGetAlarm = { + size?: number; + cursor?: number; +}; +export type TResponseGetAlarm = TCommonResponse<{ + alarmList: TAlarm[]; + size: number; + hasNext: boolean; + cursor: number | null; +}>; +export type TRequestPostDeviceToken = { + deviceToken: string; +}; +export type TResponsePostDeviceToken = TCommonResponse<{}>; +export type TAlarm = { + id: number; + title: string; + alarmType: string; + isRead: boolean; +}; diff --git a/src/types/home/dateCourse.ts b/src/types/home/dateCourse.ts new file mode 100644 index 0000000..3a07367 --- /dev/null +++ b/src/types/home/dateCourse.ts @@ -0,0 +1,6 @@ +import type { TCommonResponse } from '../common/common'; + +// 사용자 등급 응답 타입 (실제 API 응답 구조) +export type TDateCourseSavedCountResponse = TCommonResponse<{ + count: number; +}>; diff --git a/src/types/home/datePlace.ts b/src/types/home/datePlace.ts new file mode 100644 index 0000000..bc267c3 --- /dev/null +++ b/src/types/home/datePlace.ts @@ -0,0 +1,21 @@ +import type { TCommonResponse } from '../common/common'; + +// 월별 데이트 장소 수 응답 타입 +export type TMonthlyDatePlaceResponse = TCommonResponse<{ datePlaceLogList: IMonthlyDatePlaceLog[] }>; + +export type TGetDateTimeStates = TCommonResponse<{ + averageDateCount: number; + myDateCount: number; +}>; +// 월별 데이트 장소 로그 타입 +export interface IMonthlyDatePlaceLog { + year: number; + month: number; + count: number; +} + +// 연도별 통계로 변환된 타입 +export interface IYearlyPlaceStates { + year: number; + placeCount: number; +} diff --git a/src/types/home/keyword.ts b/src/types/home/keyword.ts new file mode 100644 index 0000000..97c673e --- /dev/null +++ b/src/types/home/keyword.ts @@ -0,0 +1,12 @@ +import type { TCommonResponse } from '../common/common'; + +// 이번 주 인기 키워드 응답 타입 +export type TWeeklyKeywordResponse = TCommonResponse<{ + placeCategoryLogList: IPlaceCategoryLog[]; +}>; + +// 장소 카테고리 로그 타입 +export interface IPlaceCategoryLog { + placeCategoryLabel: string; + count: number; +} diff --git a/src/types/home/level.ts b/src/types/home/level.ts new file mode 100644 index 0000000..95d7175 --- /dev/null +++ b/src/types/home/level.ts @@ -0,0 +1,13 @@ +import type { TCommonResponse } from '../common/common'; + +// 사용자 등급 응답 타입 (실제 API 응답 구조) +export type TUserGradeResponse = TCommonResponse; + +// 등급 정보 타입 +export interface IGradeInfo { + username: string; + grade: string; + level: string; + description: string; + nextRequiredPoint: number; +} diff --git a/src/types/home/region.ts b/src/types/home/region.ts new file mode 100644 index 0000000..01613d6 --- /dev/null +++ b/src/types/home/region.ts @@ -0,0 +1,29 @@ +import type { TCommonResponse } from '../common/common'; + +export type TPatchUserRegionRequest = { + regionId: number; +}; + +export type TPatchUserRegionResponse = TCommonResponse<{ + regionId: number; + name: string; + regionCode: TRegionCode; + message: string; +}>; + +type TRegionCode = { + regionCodeId: number; + landRegCode: string; + tempRegCode: string; + name: string; +}; + +export type TGetUserRegionResponse = TCommonResponse<{ + regionId: number; + name: string; + latitude: number; + longitude: number; + gridX: number; + gridY: number; + regionCode: TRegionCode; +}>; diff --git a/src/types/home/weather.ts b/src/types/home/weather.ts new file mode 100644 index 0000000..cf32d36 --- /dev/null +++ b/src/types/home/weather.ts @@ -0,0 +1,50 @@ +import type { TempCategory, WeatherType } from '@/constants/weather'; + +import type { TCommonResponse } from '../common/common'; + +export type TGetWeeklyWeatherRecommendationRequest = { + startDate: string; + regionId: number; +}; +type TDailyRecommendations = { + forecastDate: string; + weatherType: WeatherType; + tempCategory: TempCategory; + precipCategory: string; + message: string; + emoji: string; + keywords: string[]; +}; + +type TWeatherRegion = { + regionId: number; + regionName: string; + landRegCode: string; + tempRegCode: string; +}; +export type TGetWeeklyWeatherRecommendationResponse = TCommonResponse<{ + region: TWeatherRegion; + startDate: string; + endDate: string; + dailyRecommendations: TDailyRecommendations[]; + totalDays: number; + message: string; +}>; + +export type TGetPrecipitationRequest = { + startDate: string; + regionId: number; +}; +export type TGetPrecipitationResponse = TCommonResponse<{ + region: TWeatherRegion; + startDate: string; + endDate: string; + dailyPrecipitations: TDailyPrecipitations[]; + totalDays: number; + message: string; +}>; + +type TDailyPrecipitations = { + forecastDate: string; + precipitationProbability: number; +}; diff --git a/src/types/notice/notice.ts b/src/types/notice/notice.ts index a9d41e9..c52ef05 100644 --- a/src/types/notice/notice.ts +++ b/src/types/notice/notice.ts @@ -8,6 +8,12 @@ export type TNoticeItem = { createdAt: string; }; +export type TRequestGetNoticeRequest = { + size?: number; + noticeCategory: 'SERVICE' | 'SYSTEM'; + page: number; +}; + export type TFetchNoticesResponse = TCommonResponse<{ noticeList: TNoticeItem[]; totalPages: number; diff --git a/src/utils/getNextSevenDay.ts b/src/utils/getNextSevenDay.ts new file mode 100644 index 0000000..0566f52 --- /dev/null +++ b/src/utils/getNextSevenDay.ts @@ -0,0 +1,14 @@ +export function getNextSevenDay(startDate: string): string[] { + const result: string[] = []; + const start = new Date(startDate); + + for (let i = 0; i < 7; i++) { + const date = new Date(start); + date.setDate(start.getDate() + i); + const label = date.toLocaleDateString('ko-KR', { month: 'long', day: 'numeric', timeZone: 'Asia/Seoul' }); + + result.push(label); + } + + return result; +} diff --git a/src/utils/getTodayString.ts b/src/utils/getTodayString.ts new file mode 100644 index 0000000..bf9fad0 --- /dev/null +++ b/src/utils/getTodayString.ts @@ -0,0 +1,7 @@ +export function getTodayString(date: Date = new Date()): string { + const today = date; + const yyyy = today.getFullYear(); + const mm = String(today.getMonth() + 1).padStart(2, '0'); + const dd = String(today.getDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; +} diff --git a/src/utils/normalizeEmojiKey.ts b/src/utils/normalizeEmojiKey.ts new file mode 100644 index 0000000..ce58631 --- /dev/null +++ b/src/utils/normalizeEmojiKey.ts @@ -0,0 +1,9 @@ +export function normalizeEmojiKey(k?: string): string { + if (!k) return ''; + return k + .normalize('NFKD') + .replace(/[\uFE0E\uFE0F\u200D]/g, '') + .replace(/\p{M}/gu, '') + .trim() + .toLowerCase(); +} diff --git a/src/utils/weatherMessage.ts b/src/utils/weatherMessage.ts new file mode 100644 index 0000000..6704e4c --- /dev/null +++ b/src/utils/weatherMessage.ts @@ -0,0 +1,8 @@ +import type { TempCategory, WeatherType } from '@/constants/weather'; +import { TempCategoryLabel, WeatherTypeLabel } from '@/constants/weather'; + +export function getWeatherSentence({ weather, temp }: { weather: WeatherType; temp: TempCategory }): string { + const weatherLabel = WeatherTypeLabel[weather]; + const tempLabel = TempCategoryLabel[temp]; + return [weatherLabel, tempLabel].filter(Boolean).join(' '); +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 87da4c3..20f7021 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -26,5 +26,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src", "src/**/*", "**/*.ts", "**/*.tsx", "tailwind.config.ts"] + "include": ["src", "src/**/*", "**/*.ts", "**/*.tsx", "tailwind.config.ts", "public/firebase-messaging-sw.js"] } diff --git a/tsconfig.json b/tsconfig.json index f6df6c7..0d11e84 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,10 @@ { "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } } diff --git a/yarn.lock b/yarn.lock index f20e38c..809543e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -519,6 +519,415 @@ "@eslint/core" "^0.15.1" levn "^0.4.1" +"@firebase/ai@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@firebase/ai/-/ai-2.0.0.tgz#c0324b48c2451b28d621c96517e056ab67576dc4" + integrity sha512-N/aSHjqOpU+KkYU3piMkbcuxzvqsOvxflLUXBAkYAPAz8wjE2Ye3BQDgKHEYuhMmEWqj6LFgEBUN8wwc6dfMTw== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/analytics-compat@0.2.24": + version "0.2.24" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.24.tgz#806c34ddd5c4869006eead08bfde575972d73ce2" + integrity sha512-jE+kJnPG86XSqGQGhXXYt1tpTbCTED8OQJ/PQ90SEw14CuxRxx/H+lFbWA1rlFtFSsTCptAJtgyRBwr/f00vsw== + dependencies: + "@firebase/analytics" "0.10.18" + "@firebase/analytics-types" "0.8.3" + "@firebase/component" "0.7.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.3.tgz#d08cd39a6209693ca2039ba7a81570dfa6c1518f" + integrity sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg== + +"@firebase/analytics@0.10.18": + version "0.10.18" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.18.tgz#930d43504a02fe0128a8d82f8c5361911b0dbd04" + integrity sha512-iN7IgLvM06iFk8BeFoWqvVpRFW3Z70f+Qe2PfCJ7vPIgLPjHXDE774DhCT5Y2/ZU/ZbXPDPD60x/XPWEoZLNdg== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/installations" "0.6.19" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz#94ac0cf9f66cab1d81a7b14e0c151dcc2684bc95" + integrity sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g== + dependencies: + "@firebase/app-check" "0.11.0" + "@firebase/app-check-types" "0.5.3" + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz#ed9c4a4f48d1395ef378f007476db3940aa5351a" + integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A== + +"@firebase/app-check-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.3.tgz#38ba954acf4bffe451581a32fffa20337f11d8e5" + integrity sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng== + +"@firebase/app-check@0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.11.0.tgz#a7e1d1e3f5ae36eabed1455db937114fe869ce8f" + integrity sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/app-compat@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.5.0.tgz#7463e9fb8d84706787773d3660accedff4479d05" + integrity sha512-nUnNpOeRj0KZzVzHsyuyrmZKKHfykZ8mn40FtG28DeSTWeM5b/2P242Va4bmQpJsy5y32vfv50+jvdckrpzy7Q== + dependencies: + "@firebase/app" "0.14.0" + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/app-types@0.9.3": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.3.tgz#8408219eae9b1fb74f86c24e7150a148460414ad" + integrity sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw== + +"@firebase/app@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.14.0.tgz#7132e96df95e85783922fd09750f08ab2ebfd699" + integrity sha512-APIAeKvRNFWKJLjIL8wLDjh7u8g6ZjaeVmItyqSjCdEkJj14UuVlus74D8ofsOMWh45HEwxwkd96GYbi+CImEg== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.6.0.tgz#1464ea6049b2ad0aae83b4fdcd5e5e5aba6b1c50" + integrity sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw== + dependencies: + "@firebase/auth" "1.11.0" + "@firebase/auth-types" "0.13.0" + "@firebase/component" "0.7.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/auth-interop-types@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz#176a08686b0685596ff03d7879b7e4115af53de0" + integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA== + +"@firebase/auth-types@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.13.0.tgz#ae6e0015e3bd4bfe18edd0942b48a0a118a098d9" + integrity sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg== + +"@firebase/auth@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.11.0.tgz#81a4f77b16d97c502e493b2a14a97443e243a2a0" + integrity sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/component@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.7.0.tgz#3736644fdb6d3572dceae7fdc1c35a8bd3819adc" + integrity sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg== + dependencies: + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/data-connect@0.3.11": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@firebase/data-connect/-/data-connect-0.3.11.tgz#60a7a9649e4aedd005546032466ef9abc0a544c1" + integrity sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw== + dependencies: + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/database-compat@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-2.1.0.tgz#c64488d741c6da2ed8dcf02f2e433089dae2f590" + integrity sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/database" "1.1.0" + "@firebase/database-types" "1.0.16" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/database-types@1.0.16": + version "1.0.16" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.16.tgz#262f54b8dbebbc46259757b3ba384224fb2ede48" + integrity sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw== + dependencies: + "@firebase/app-types" "0.9.3" + "@firebase/util" "1.13.0" + +"@firebase/database@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.1.0.tgz#bdf60f1605079a87ceb2b5e30d90846e0bde294b" + integrity sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.4.0.tgz#911cf956e489fa8335ed2f2ace14a74909bcd94d" + integrity sha512-4O7v4VFeSEwAZtLjsaj33YrMHMRjplOIYC2CiYsF6o/MboOhrhe01VrTt8iY9Y5EwjRHuRz4pS6jMBT8LfQYJA== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/firestore" "4.9.0" + "@firebase/firestore-types" "3.0.3" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.3.tgz#7d0c3dd8850c0193d8f5ee0cc8f11961407742c1" + integrity sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q== + +"@firebase/firestore@4.9.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.9.0.tgz#753d73c002b4c0ae639437b049ef0086791a0cf3" + integrity sha512-5zl0+/h1GvlCSLt06RMwqFsd7uqRtnNZt4sW99k2rKRd6k/ECObIWlEnvthm2cuOSnUmwZknFqtmd1qyYSLUuQ== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + "@firebase/webchannel-wrapper" "1.0.4" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + +"@firebase/functions-compat@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.4.0.tgz#aa63dea248053e9c06904605704662ea550e50ed" + integrity sha512-VPgtvoGFywWbQqtvgJnVWIDFSHV1WE6Hmyi5EGI+P+56EskiGkmnw6lEqc/MEUfGpPGdvmc4I9XMU81uj766/g== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/functions" "0.13.0" + "@firebase/functions-types" "0.6.3" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.3.tgz#f5faf770248b13f45d256f614230da6a11bfb654" + integrity sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg== + +"@firebase/functions@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.13.0.tgz#91685a59589b3a00f6c48faf383acd28a35800c2" + integrity sha512-2/LH5xIbD8aaLOWSFHAwwAybgSzHIM0dB5oVOL0zZnxFG1LctX2bc1NIAaPk1T+Zo9aVkLKUlB5fTXTkVUQprQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.0" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/installations-compat@0.2.19": + version "0.2.19" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.19.tgz#4bc57c8c57d241eeca95900ff3033d6ec3dbcc7c" + integrity sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/installations" "0.6.19" + "@firebase/installations-types" "0.5.3" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.3.tgz#cac8a14dd49f09174da9df8ae453f9b359c3ef2f" + integrity sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA== + +"@firebase/installations@0.6.19": + version "0.6.19" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.19.tgz#93c569321f6fb399f4f1a197efc0053ce6452c7c" + integrity sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/util" "1.13.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.5.0.tgz#a9e55b1c669a0983dc67127fa4a5964ce8ed5e1b" + integrity sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.23": + version "0.2.23" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz#2ca6b36ea238fae4dff53bf85442c4a2af516224" + integrity sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/messaging" "0.12.23" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz#e647c9cd1beecfe6a6e82018a6eec37555e4da3e" + integrity sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q== + +"@firebase/messaging@0.12.23": + version "0.12.23" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.23.tgz#71f932a521ac39d9f036175672e37897531010eb" + integrity sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/installations" "0.6.19" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.13.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.21": + version "0.2.21" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.21.tgz#60f04ecb5ff98b5d84a7a932f2e8294aae711c72" + integrity sha512-OQfYRsIQiEf9ez1SOMLb5TRevBHNIyA2x1GI1H10lZ432W96AK5r4LTM+SNApg84dxOuHt6RWSQWY7TPWffKXg== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/performance" "0.7.8" + "@firebase/performance-types" "0.2.3" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.3.tgz#5ce64e90fa20ab5561f8b62a305010cf9fab86fb" + integrity sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ== + +"@firebase/performance@0.7.8": + version "0.7.8" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.7.8.tgz#a3e5ff36070e0f26e59f84fcd8faf7a8ee77c677" + integrity sha512-k6xfNM/CdTl4RaV4gT/lH53NU+wP33JiN0pUeNBzGVNvfXZ3HbCkoISE3M/XaiOwHgded1l6XfLHa4zHgm0Wyg== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/installations" "0.6.19" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + web-vitals "^4.2.4" + +"@firebase/remote-config-compat@0.2.19": + version "0.2.19" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.19.tgz#10cfd804f65c5ca80a4d40994bc853ca6d1f7307" + integrity sha512-y7PZAb0l5+5oIgLJr88TNSelxuASGlXyAKj+3pUc4fDuRIdPNBoONMHaIUa9rlffBR5dErmaD2wUBJ7Z1a513Q== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/logger" "0.5.0" + "@firebase/remote-config" "0.6.6" + "@firebase/remote-config-types" "0.4.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz#91b9a836d5ca30ced68c1516163b281fbb544537" + integrity sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg== + +"@firebase/remote-config@0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.6.6.tgz#50eae3d2d71791d76fb6521971bb646d6628805e" + integrity sha512-Yelp5xd8hM4NO1G1SuWrIk4h5K42mNwC98eWZ9YLVu6Z0S6hFk1mxotAdCRmH2luH8FASlYgLLq6OQLZ4nbnCA== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/installations" "0.6.19" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/storage-compat@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.4.0.tgz#a09bd33c262123e7e3ed0cd590b4c6e2ce4a8902" + integrity sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/storage" "0.14.0" + "@firebase/storage-types" "0.8.3" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.3": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.3.tgz#2531ef593a3452fc12c59117195d6485c6632d3d" + integrity sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg== + +"@firebase/storage@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.14.0.tgz#01acb97d413ada7c91de860fb260623468baa25d" + integrity sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA== + dependencies: + "@firebase/component" "0.7.0" + "@firebase/util" "1.13.0" + tslib "^2.1.0" + +"@firebase/util@1.13.0": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.13.0.tgz#2e9e7569722a1e3fc86b1b4076d5cbfbfa7265d6" + integrity sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ== + dependencies: + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.4.tgz#9d5b4b6f23309260a12856cb574c5e64e6c133f7" + integrity sha512-6m8+P+dE/RPl4OPzjTxcTbQ0rGeRyeTvAi9KwIffBVCiAMKrfXfLZaqD1F+m8t4B5/Q5aHsMozOgirkH1F5oMQ== + +"@grpc/grpc-js@~1.9.0": + version "1.9.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.15" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" + integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + "@hookform/resolvers@^5.1.1": version "5.1.1" resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz" @@ -595,12 +1004,12 @@ "@kurkle/color@^0.3.0": version "0.3.4" - resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf" + resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz" integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== "@lukemorales/query-key-factory@^1.3.4": version "1.3.4" - resolved "https://registry.yarnpkg.com/@lukemorales/query-key-factory/-/query-key-factory-1.3.4.tgz#d14001dbd781b024df93ca73bd785db590924486" + resolved "https://registry.npmjs.org/@lukemorales/query-key-factory/-/query-key-factory-1.3.4.tgz" integrity sha512-A3frRDdkmaNNQi6mxIshsDk4chRXWoXa05US8fBo4kci/H+lVmujS6QrwQLLGIkNIRFGjMqp2uKjC4XsLdydRw== "@napi-rs/wasm-runtime@^0.2.11": @@ -638,6 +1047,59 @@ resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz" integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@rolldown/pluginutils@1.0.0-beta.11": version "1.0.0-beta.11" resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz" @@ -1105,7 +1567,7 @@ "@types/lodash@*", "@types/lodash@^4.17.20": version "4.17.20" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz" integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA== "@types/node@*": @@ -1115,6 +1577,13 @@ dependencies: undici-types "~7.8.0" +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "24.2.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.2.0.tgz#cde712f88c5190006d6b069232582ecd1f94a760" + integrity sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw== + dependencies: + undici-types "~7.10.0" + "@types/react-dom@^19.1.2": version "19.1.6" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz" @@ -1257,7 +1726,7 @@ acorn@^8.15.0: add@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235" + resolved "https://registry.npmjs.org/add/-/add-2.0.6.tgz" integrity sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q== ajv@^6.12.4: @@ -1561,14 +2030,14 @@ chalk@^5.3.0, chalk@^5.4.1: chart.js@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.5.0.tgz#11a1ef6c4befc514b1b0b613ebac226c4ad2740b" + resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz" integrity sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ== dependencies: "@kurkle/color" "^0.3.0" chartjs-plugin-datalabels@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz#369578e131d743c2e34b5fbe2d3f9335f6639b8f" + resolved "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz" integrity sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw== chownr@^3.0.0: @@ -2317,6 +2786,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + fdir@^6.4.4, fdir@^6.4.6: version "6.4.6" resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz" @@ -2353,6 +2829,40 @@ find-up@^7.0.0: path-exists "^5.0.0" unicorn-magic "^0.1.0" +firebase@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-12.0.0.tgz#436ee1b97b64265f3e7d3b6b0ce4419cc9fe5ae9" + integrity sha512-KV+OrMJpi2uXlqL2zaCcXb7YuQbY/gMIWT1hf8hKeTW1bSumWaHT5qfmn0WTpHwKQa3QEVOtZR2ta9EchcmYuw== + dependencies: + "@firebase/ai" "2.0.0" + "@firebase/analytics" "0.10.18" + "@firebase/analytics-compat" "0.2.24" + "@firebase/app" "0.14.0" + "@firebase/app-check" "0.11.0" + "@firebase/app-check-compat" "0.4.0" + "@firebase/app-compat" "0.5.0" + "@firebase/app-types" "0.9.3" + "@firebase/auth" "1.11.0" + "@firebase/auth-compat" "0.6.0" + "@firebase/data-connect" "0.3.11" + "@firebase/database" "1.1.0" + "@firebase/database-compat" "2.1.0" + "@firebase/firestore" "4.9.0" + "@firebase/firestore-compat" "0.4.0" + "@firebase/functions" "0.13.0" + "@firebase/functions-compat" "0.4.0" + "@firebase/installations" "0.6.19" + "@firebase/installations-compat" "0.2.19" + "@firebase/messaging" "0.12.23" + "@firebase/messaging-compat" "0.2.23" + "@firebase/performance" "0.7.8" + "@firebase/performance-compat" "0.2.21" + "@firebase/remote-config" "0.6.6" + "@firebase/remote-config-compat" "0.2.19" + "@firebase/storage" "0.14.0" + "@firebase/storage-compat" "0.4.0" + "@firebase/util" "1.13.0" + flat-cache@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" @@ -2380,7 +2890,7 @@ for-each@^0.3.3, for-each@^0.3.5: form-data@^4.0.0: version "4.0.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz" integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== dependencies: asynckit "^0.4.0" @@ -2580,11 +3090,21 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +http-parser-js@>=0.5.1: + version "0.5.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== + husky@^9.1.7: version "9.1.7" resolved "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz" integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== +idb@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + ignore@^5.2.0: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" @@ -3143,7 +3663,7 @@ lodash.upperfirst@^4.3.1: lodash@^4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-update@^6.1.0: @@ -3157,6 +3677,11 @@ log-update@^6.1.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" +long@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" @@ -3541,6 +4066,24 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@^7.2.5: + version "7.5.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.3.tgz#13f95a9e3c84669995ec3652db2ac2fb00b89363" + integrity sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" @@ -3558,7 +4101,7 @@ queue-microtask@^1.2.2: react-chartjs-2@^5.3.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz#2d3286339a742bc7f77b5829c33ebab215f714cc" + resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz" integrity sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw== react-dom@^19.1.0: @@ -3573,6 +4116,11 @@ react-hook-form@^7.60.0: resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz" integrity sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A== +react-intersection-observer@^9.16.0: + version "9.16.0" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz#7376d54edc47293300961010844d53b273ee0fb9" + integrity sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA== + react-is@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -3593,6 +4141,11 @@ react-router@7.6.3: cookie "^1.0.1" set-cookie-parser "^2.6.0" +react-spinners@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.17.0.tgz#d518913e2192afaae76c5b781929ea3bd7b1910e" + integrity sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ== + react@^19.1.0: version "19.1.0" resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" @@ -3727,6 +4280,11 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" +safe-buffer@>=5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-push-apply@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz" @@ -4105,9 +4663,9 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.3, tslib@^2.4.0, tslib@^2.8.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0: version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== type-check@^0.4.0, type-check@~0.4.0: @@ -4186,6 +4744,11 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" +undici-types@~7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" + integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== + undici-types@~7.8.0: version "7.8.0" resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz" @@ -4241,6 +4804,25 @@ vite@^7.0.2: optionalDependencies: fsevents "~2.3.3" +web-vitals@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7" + integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz" @@ -4354,7 +4936,7 @@ yargs-parser@^21.1.1: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.0.0: +yargs@^17.0.0, yargs@^17.7.2: version "17.7.2" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==