Repository: 9git9git/9git-frontend
Pull request #28: ✨[SCRUM-116]-FE-분석-페이지
Author: login: Yoonsoyoung02
Created: 2025-04-13T03:01:56Z | Merged: 2025-04-15T01:27:25Z

## Explain this Pull Request 🙏
이 이슈는 분석페이지 제작을 위해 생성되었습니다.

- Jira URL: https://9git-9git.atlassian.net/browse/SCRUM-116?atlOrigin=eyJpIjoiNTRlYTc1ZGM5MGY1NGZmNjgxMTVjNGFhNjJjN2MyNDgiLCJwIjoiaiJ9

## What has fixed? 🛠

📝 분석 페이지 제작 특이사항

1. 차트 구성 및 시각화 개선
- 탭을 통한 월별/일자별 데이터 분리
월별 평균 데이터와 일자별 데이터를 함께 렌더링할 경우 오류가 발생하여,
탭 형태로 월별/일자별 차트를 구분하여 구현했습니다.

- Y축 라벨 대신 최고/최저값 강조
차트 Y축에는 숫자 라벨을 제거하고,
카테고리별 최고/최저점에만 강조 표시(점 및 값)를 추가했습니다.

- 일자별 X축 라벨 최소화 및 Tooltip 처리
일자별 데이터의 수가 월마다 다를 수 있어 X축 레이블 간격이 불규칙해질 수 있는 문제를 방지하고자,
X축 라벨은 제거하고 마우스 hover 또는 클릭 시 일자와 데이터를 Tooltip으로 표시되도록 하여 해당 일의 데이터를 확인할 수 있도록 하였습니다.

2. 초기 상태 대응
분석 데이터가 없을 경우를 고려하여,
초기용 안내 텍스트 및 빈 상태 화면을 별도로 세팅하였습니다.

3. Mock 데이터 구성
추후 서버 연동을 고려하여,
화면 내에서 사용되는 텍스트, 안내 문구 등은 모두 mock 파일에서 관리되도록 설정했습니다.

4. info 아이콘을 눌렀을 때 팝업이 뜨도록 설정하였습니다.

## Whether needed review 🖍
1. 차트를 구현하는 과정에서 기존 기획팀으로부터 요청받은 사항과 달라진 점이 있어 리뷰 부탁드립니다!
2. api연동 과정에 이 코드에 문제점을 없을지도 검토 부탁드립니다.
3. 텍스트의 줄바꿈이 매끄럽지 못한 부분이 있는데 개선할 수 있는 방안이 궁금합니다.

<!-- 마크다운의 빈 체크박스 문법은 [ ] / 채워진 체크박스 문법은 [x] 입니다. -->

- [X] 🙋🏻 리뷰가 필요합니다!
- [ ] 🙅🏻 리뷰가 필요하지 않습니다!

(이유 : )

## Document🗒️ (Optional)

## Screen shot
![image](https://github.com/user-attachments/assets/c29f1bef-74f4-49db-a898-fe825e486984)
![image](https://github.com/user-attachments/assets/59033320-cd1e-4d2a-95d3-284fe8f297d1)
![image](https://github.com/user-attachments/assets/8c425d63-03de-41e6-aebf-ae94e3b318bd)


*초기 세팅
![image](https://github.com/user-attachments/assets/d4b38f0b-4cf5-40ca-84c0-7bbe23dd7b84)
![image](https://github.com/user-attachments/assets/55fecf37-27db-4268-840a-a427a7ba2d48)
![image](https://github.com/user-attachments/assets/cbae7baf-0a3c-4994-885f-ca5885287eb3)


## Test Checklist ✅ (Optional)

<!-- 마크다운의 빈 체크박스 문법은 [ ] / 채워진 체크박스 문법은 [x] 입니다. -->



Diff:
diff --git a/app/(service)/analysis/page.tsx b/app/(service)/analysis/page.tsx
index 5143e0e..5f3dec6 100644
--- a/app/(service)/analysis/page.tsx
+++ b/app/(service)/analysis/page.tsx
@@ -1,4 +1,4 @@
-import { Analysis } from "@/components/service/analysis/Analysis";
+import { Analysis } from '@/components/service/analysis/Analysis';
 
 export default function AnalysisPage() {
   return <Analysis />;
diff --git a/app/(service)/layout.tsx b/app/(service)/layout.tsx
index 428cf34..4045750 100644
--- a/app/(service)/layout.tsx
+++ b/app/(service)/layout.tsx
@@ -1,8 +1,9 @@
-import { BottomTabs } from "@/components/shared/BottomTabs/BottomTabs";
-import { Modal } from "@/components/shared/Modal/Modal";
-export default function Layout({ children }: { children: React.ReactNode }) {
+import { BottomTabs } from '@/components/shared/BottomTabs/BottomTabs';
+import { Modal } from '@/components/shared/Modal/Modal';
+
+export default function ServiceLayout({ children }: { children: React.ReactNode }) {
   return (
-    <div className="flex flex-col h-screen">
+    <div className="flex flex-col h-screen" style={{ fontFamily: 'Gugit, sans-serif' }}>
       <main className="flex-1 overflow-auto scrollbar-hide">{children}</main>
       <BottomTabs />
       <Modal />
diff --git a/app/globals.css b/app/globals.css
index 7d03e8d..3632fbd 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -54,6 +54,14 @@
   --color-chat-gray: var(--chat-gray);
 }
 
+@font-face {
+  font-family: 'Gugit';
+  src: url('/font/font.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+  font-display: swap;
+}
+
 :root {
   --radius: 0.625rem;
   --background: oklch(1 0 0);
@@ -98,6 +106,7 @@
   --primary-light: oklch(0.96 0.03 83.72); /* #FFF0DC */
   --main-gray: oklch(0.392 0.022 264.86); /* #4B5563 */
   --chat-gray: oklch(0.961 0.015 262.33); /* #F3F4F6 */
+  --font-gugit: 'Gugit', sans-serif;
 }
 
 .dark {
@@ -143,6 +152,7 @@
   --primary-light: oklch(0.96 0.03 83.72); /* #FFF0DC */
   --main-gray: oklch(0.392 0.022 264.86); /* #4B5563 */
   --chat-gray: oklch(0.961 0.015 262.33); /* #F3F4F6 */
+  --font-gugit: 'Gugit', sans-serif;
 }
 
 @layer base {
diff --git a/app/layout.tsx b/app/layout.tsx
index 5050c6f..cefe325 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -15,7 +15,7 @@ export default function RootLayout({
 }>) {
   return (
     <html lang="ko">
-      <body>
+      <body className="overflow-hidden">
         <Toaster position="top-center" />
         {children}
         <MSWProvider>{children}</MSWProvider>
diff --git a/components/common/Card.tsx b/components/common/Card.tsx
index 6e498b8..2388ed2 100644
--- a/components/common/Card.tsx
+++ b/components/common/Card.tsx
@@ -4,25 +4,47 @@ import { Separator } from '@/components/ui/separator';
 type Props = {
   title: React.ReactNode;
   isMore?: boolean;
-  children?: React.ReactNode;
   onMoreClick?: () => void;
+  children?: React.ReactNode;
+  rightAction?: React.ReactNode;
+  bgColor?: string;
+  shadowColor?: string;
+  className?: string;
 };
 
-export default function Card({ title, isMore = false, onMoreClick, children }: Props) {
+export default function Card({
+  title,
+  isMore = false,
+  onMoreClick,
+  children,
+  rightAction,
+  bgColor = 'var(--beige-light)',
+  shadowColor = 'rgba(250, 181, 127, 0.4)',
+  className = '',
+}: Props) {
   return (
-    <section className="bg-beige-light rounded-xl p-4 w-[90%] flex flex-col gap-y-4">
-      <div className="text-secondary flex justify-between items-center font-semibold">
-        <span>{title}</span>
-        {isMore && (
-          <button
-            onClick={onMoreClick}
-            className="text-sm text-secondary font-semibold hover:text-primary"
-          >
-            + 더보기
-          </button>
-        )}
+    <section
+      className={`rounded-xl p-4 w-[90%] flex flex-col gap-y-4 ${className}`}
+      style={{
+        backgroundColor: bgColor,
+        boxShadow: `2px 2px 4px ${shadowColor}`,
+      }}
+    >
+      <div className="flex flex-col gap-y-1">
+        <div className="flex justify-between items-center text-secondary text-xl ">
+          <span>{title}</span>
+
+          {rightAction ? (
+            rightAction
+          ) : isMore ? (
+            <button onClick={onMoreClick} className="text-lg text-secondary hover:text-primary">
+              + 더보기
+            </button>
+          ) : null}
+        </div>
+        <Separator className="bg-beige-deco" />
       </div>
-      <Separator className="bg-beige-deco" />
+
       <div className="gap-y-4">{children}</div>
     </section>
   );
diff --git a/components/common/PieChartBox.tsx b/components/common/PieChartBox.tsx
new file mode 100644
index 0000000..6309e12
--- /dev/null
+++ b/components/common/PieChartBox.tsx
@@ -0,0 +1,37 @@
+// components/chart/PieChartBox.tsx
+'use client';
+
+import { PieChart, Pie, Cell } from 'recharts';
+
+type Props = {
+  value: number;
+};
+
+export default function PieChartBox({ value }: Props) {
+  return (
+    <div className="relative w-[150px] h-[75px]">
+      <PieChart width={150} height={75}>
+        <Pie
+          data={[
+            { name: '전체 달성도', value: value },
+            { name: '남은 부분', value: 100 - value },
+          ]}
+          cx="50%"
+          cy="100%"
+          innerRadius={40}
+          outerRadius={60}
+          startAngle={180}
+          endAngle={0}
+          paddingAngle={3}
+          dataKey="value"
+        >
+          <Cell fill="var(--primary)" />
+          <Cell fill="var(--beige-light)" />
+        </Pie>
+      </PieChart>
+      <div className="absolute bottom-0 left-0 right-0 flex items-center justify-center text-xl font-bold text-secondary">
+        {value}%
+      </div>
+    </div>
+  );
+}
diff --git a/components/service/analysis/Analysis.tsx b/components/service/analysis/Analysis.tsx
index 1fce514..d0e53ac 100644
--- a/components/service/analysis/Analysis.tsx
+++ b/components/service/analysis/Analysis.tsx
@@ -1,12 +1,14 @@
-import { Logo } from "@/components/common/Logo";
-import { Header } from "@/components/shared/Header";
+import { Logo } from '@/components/common/Logo';
+import { Header } from '@/components/shared/Header';
+import AnalysisPage from './AnalysisPage';
 
 export const Analysis = () => {
   return (
-    <>
+    <div>
       <Header>
         <Logo width={45} height={45} />
       </Header>
-    </>
+      <AnalysisPage />
+    </div>
   );
 };
diff --git a/components/service/analysis/AnalysisPage.tsx b/components/service/analysis/AnalysisPage.tsx
new file mode 100644
index 0000000..5aff100
--- /dev/null
+++ b/components/service/analysis/AnalysisPage.tsx
@@ -0,0 +1,525 @@
+'use client';
+
+import { useEffect, useState, useMemo } from 'react';
+import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, DotProps } from 'recharts';
+import { Info } from 'lucide-react';
+import dynamic from 'next/dynamic';
+
+import Card from '@/components/common/Card';
+import { ActionButton } from '@/components/common/ActionButton';
+import { useModalStore } from '@/stores/modal';
+import {
+  rawDailyData,
+  insights,
+  aiChallenges,
+  chartData as mockChartData,
+  initialStateTexts as mockInitialStateTexts,
+} from '@/mocks/analysisData';
+import { groupByMonthAndAverage, getCategoryExtremes } from '@/utils/chartUtils';
+import { AnalysisPopup } from './AnalysisPopup';
+
+const PieChartBox = dynamic(() => import('@/components/common/PieChartBox'), {
+  ssr: false,
+});
+
+const COLORS: Record<CategoryType, string> = {
+  영어: 'var(--primary)',
+  운동: 'var(--category-exercise)',
+  코딩: 'var(--category-coding)',
+} as const;
+type CategoryType = '영어' | '운동' | '코딩';
+
+type TabType = 'daily' | 'monthly';
+
+// 초기 상태 텍스트 타입 정의
+type InitialStateTexts = {
+  totalAchievement: string;
+  strength: {
+    title: string;
+    emoji: string;
+    text: string;
+  };
+  improvement: {
+    title: string;
+    emoji: string;
+    text: string;
+  };
+  chart: string;
+  aiChallenges: string;
+};
+
+export default function AnalysisPage() {
+  const [selectedCategories, setSelectedCategories] = useState<CategoryType[]>([
+    '영어',
+    '운동',
+    '코딩',
+  ]);
+  const [chartData, setChartData] = useState<any[]>([]);
+  const [activeTab, setActiveTab] = useState<TabType>('monthly');
+  const [totalAchievementRate, setTotalAchievementRate] = useState<number>(0);
+  const [hasData, setHasData] = useState<boolean>(false);
+  const { openModal } = useModalStore();
+
+  // 초기 상태 텍스트 상태 추가
+  const [initialStateTexts, setInitialStateTexts] =
+    useState<InitialStateTexts>(mockInitialStateTexts);
+
+  // BE 데이터 fetch
+  useEffect(() => {
+    // TODO: BE API 호출
+    const fetchData = async () => {
+      try {
+        // 실제 API 호출 시 아래 주석 해제
+        // const response = await fetch('/api/analysis');
+        // const data = await response.json();
+        // setTotalAchievementRate(data.totalAchievementRate);
+        // setChartData(data.chartData);
+        // setHasData(true);
+
+        // 초기 상태 텍스트도 함께 가져오기
+        // setInitialStateTexts(data.initialStateTexts);
+
+        // 임시로 mock 데이터 사용
+        const monthlyData = groupByMonthAndAverage(rawDailyData);
+        setChartData(monthlyData);
+
+        // 초기 상태를 보여주기 위해 hasData를 false로 설정
+        setHasData(false);
+      } catch (error) {
+        console.error('Failed to fetch analysis data:', error);
+        setHasData(false);
+      }
+    };
+    fetchData();
+  }, []);
+
+  const isExtremePoint = (category: CategoryType, value: number | undefined) => {
+    if (value === undefined) return false;
+    const currentData = activeTab === 'daily' ? filteredDailyData : filteredMonthlyData;
+    const extremes = getCategoryExtremes(currentData, category);
+    return value === extremes.min || value === extremes.max;
+  };
+
+  const getCategoryColor = (category: CategoryType) => {
+    return COLORS[category];
+  };
+
+  const renderDot = (category: CategoryType) => (props: any) => {
+    const { cx, cy, payload, index } = props;
+    const value = payload[category];
+    if (!isExtremePoint(category, value)) {
+      return <circle key={`${category}-${index}-empty`} cx={cx} cy={cy} r={0} />;
+    }
+    return (
+      <g key={`${category}-${index}`}>
+        <circle cx={cx} cy={cy} r={4} fill={COLORS[category]} stroke="white" strokeWidth={1} />
+        <text x={cx} y={cy - 10} textAnchor="middle" fill={COLORS[category]} fontSize="12">
+          {value}%
+        </text>
+      </g>
+    );
+  };
+
+  const getAllExtremes = (): number[] => {
+    const values: number[] = [];
+    selectedCategories.forEach((category) => {
+      const extremes = getCategoryExtremes(rawDailyData, category);
+      values.push(extremes.min, extremes.max);
+    });
+    return Array.from(new Set(values));
+  };
+
+  // 1월부터 6월까지의 데이터만 필터링
+  const filteredDailyData = rawDailyData.filter((item) => {
+    const date = new Date(item.date);
+    const month = date.getMonth() + 1;
+    return month >= 1 && month <= 6;
+  });
+
+  // 월별 데이터 필터링
+  const filteredMonthlyData = chartData.filter((item) => {
+    const month = parseInt(item.month);
+    return month >= 1 && month <= 6;
+  });
+
+  // 월별로 데이터 그룹화하여 월 표시를 위한 데이터 준비
+  const monthlyLabels = useMemo(() => {
+    const labels: { date: string; month: number }[] = [];
+    const months = new Set<number>();
+
+    filteredDailyData.forEach((item) => {
+      const date = new Date(item.date);
+      const month = date.getMonth() + 1;
+      if (!months.has(month)) {
+        months.add(month);
+        labels.push({ date: item.date, month });
+      }
+    });
+
+    return labels.sort((a, b) => a.month - b.month);
+  }, [filteredDailyData]);
+
+  // 월 표시를 위한 커스텀 틱 컴포넌트
+  const CustomTick = (props: any) => {
+    const { x, y, payload } = props;
+    const date = new Date(payload.value);
+    const isFirstDayOfMonth = date.getDate() === 1;
+
+    if (!isFirstDayOfMonth) {
+      return null;
+    }
+
+    return (
+      <g transform={`translate(${x},${y})`}>
+        <text x={0} y={0} dy={16} textAnchor="middle" fill="var(--main-gray)" fontSize={13}>
+          {date.getMonth() + 1}월
+        </text>
+      </g>
+    );
+  };
+
+  // 월 표시를 위한 커스텀 X축 컴포넌트
+  const CustomXAxis = (props: any) => {
+    const { x, y, width, height, stroke, tickFormatter } = props;
+
+    // 월별 레이블 생성
+    const monthTicks = monthlyLabels.map((label, index) => {
+      // 해당 월의 첫 날 데이터 찾기
+      const firstDayData = filteredDailyData.find((item) => {
+        const date = new Date(item.date);
+        return date.getMonth() + 1 === label.month && date.getDate() === 1;
+      });
+
+      if (!firstDayData) return null;
+
+      // 해당 데이터의 X 좌표 계산 (대략적인 위치)
+      const dataIndex = filteredDailyData.findIndex((item) => item.date === firstDayData.date);
+      const position = (dataIndex / (filteredDailyData.length - 1)) * width;
+
+      return (
+        <g key={`month-${label.month}`} transform={`translate(${position},${y + height})`}>
+          <text x={0} y={0} dy={16} textAnchor="middle" fill="var(--main-gray)" fontSize={13}>
+            {label.month}월
+          </text>
+        </g>
+      );
+    });
+
+    return (
+      <g>
+        <line x1={x} y1={y + height} x2={x + width} y2={y + height} stroke={stroke} />
+        {monthTicks}
+      </g>
+    );
+  };
+
+  return (
+    <div className="min-h-screen bg-[var(--beige-light)] text-[var(--main-gray)] flex justify-center px-4 py-10">
+      <div className="w-full max-w-[800px] flex flex-col gap-12 items-center">
+        <h2 className="text-2xl text-secondary w-full text-left pl-4">AI 종합평가</h2>
+
+        <Card title={<div className="text-secondary">전체 달성도</div>} bgColor="var(--beige-base)">
+          <div className="flex flex-col items-center justify-center">
+            <PieChartBox value={totalAchievementRate} />
+
+            {/* 초기 상태 텍스트 박스 */}
+            {!hasData && (
+              <div className="w-full flex justify-center mt-4">
+                <div className="bg-[var(--beige-light)] rounded-xl p-4  w-[95%] min-h-[80px] flex items-center justify-center mx-auto">
+                  <p className="text-base whitespace-pre-line text-secondary text-center">
+                    {initialStateTexts.totalAchievement}
+                  </p>
+                </div>
+              </div>
+            )}
+
+            {hasData && (
+              <div className="bg-[var(--beige-base)] rounded-xl px-6 py-3 mt-4">
+                <p className="text-lg text-center whitespace-pre-line text-secondary">
+                  {initialStateTexts.totalAchievement}
+                </p>
+              </div>
+            )}
+          </div>
+        </Card>
+
+        <div className="flex gap-x-2 w-full px-2 justify-center">
+          {(hasData
+            ? insights
+            : [
+                {
+                  title: initialStateTexts.strength.title,
+                  emoji: initialStateTexts.strength.emoji,
+                  text: initialStateTexts.strength.text,
+                },
+                {
+                  title: initialStateTexts.improvement.title,
+                  emoji: initialStateTexts.improvement.emoji,
+                  text: initialStateTexts.improvement.text,
+                },
+              ]
+          ).map((item, idx) => (
+            <div key={idx} className="flex-1 flex justify-center">
+              <Card
+                title={<div className="text-secondary">{item.title}</div>}
+                bgColor="var(--beige-base)"
+                className="w-[95%]"
+              >
+                <div className="flex flex-col items-center text-center gap-4 w-full py-4">
+                  <div className="text-5xl">{item.emoji}</div>
+                  <p className="text-base whitespace-pre-line leading-relaxed text-secondary">
+                    {item.text}
+                  </p>
+                </div>
+              </Card>
+            </div>
+          ))}
+        </div>
+
+        <Card
+          title={<div className="text-secondary">목표별 달성현황</div>}
+          bgColor="var(--beige-base)"
+        >
+          <div className="flex flex-col items-center justify-center w-full">
+            {/* 초기 상태 텍스트 박스 */}
+            {!hasData && (
+              <div className="w-full flex justify-center mb-4">
+                <div className="bg-[var(--beige-light)] rounded-xl p-4 w-[95%] min-h-[80px] flex items-center justify-center mx-auto">
+                  <p className="text-base whitespace-pre-line text-secondary text-center">
+                    {initialStateTexts.chart}
+                  </p>
+                </div>
+              </div>
+            )}
+
+            {/* 탭 버튼 */}
+            <div className="flex justify-center w-full mb-4">
+              <div className="inline-flex rounded-full border border-[var(--beige-deco)] bg-[var(--beige-base)] p-0.5">
+                <button
+                  className={`px-4 py-0 text-base rounded-full transition-all duration-150 ${
+                    activeTab === 'monthly'
+                      ? 'bg-[var(--primary)] text-white shadow-sm'
+                      : 'text-secondary'
+                  }`}
+                  onClick={() => setActiveTab('monthly')}
+                >
+                  월평균
+                </button>
+                <button
+                  className={`px-4 py-0 text-base rounded-full transition-all duration-150 ${
+                    activeTab === 'daily'
+                      ? 'bg-[var(--primary)] text-white shadow-sm'
+                      : 'text-secondary'
+                  }`}
+                  onClick={() => setActiveTab('daily')}
+                >
+                  일자별
+                </button>
+              </div>
+            </div>
+
+            <div className="h-64 w-full max-w-[300px]">
+              <ResponsiveContainer width="100%" height="100%">
+                <AreaChart
+                  data={activeTab === 'daily' ? filteredDailyData : filteredMonthlyData}
+                  margin={{ top: 10, right: 20, bottom: 0, left: 0 }}
+                >
+                  <defs>
+                    {(['영어', '운동', '코딩'] as CategoryType[]).map((category) => (
+                      <linearGradient
+                        key={category}
+                        id={`color${category}`}
+                        x1="0"
+                        y1="0"
+                        x2="0"
+                        y2="1"
+                      >
+                        <stop offset="5%" stopColor={COLORS[category]} stopOpacity={0.6} />
+                        <stop offset="95%" stopColor={COLORS[category]} stopOpacity={0} />
+                      </linearGradient>
+                    ))}
+                  </defs>
+
+                  <XAxis
+                    dataKey={activeTab === 'daily' ? 'date' : 'month'}
+                    interval={0}
+                    axisLine={{ stroke: 'var(--primary)' }}
+                    tickLine={false}
+                    tick={
+                      activeTab === 'daily'
+                        ? { fontSize: 13, fill: 'var(--main-gray)' }
+                        : { fontSize: 13, fill: 'var(--main-gray)' }
+                    }
+                    tickFormatter={activeTab === 'monthly' ? (value) => `${value}` : (value) => ''}
+                    minTickGap={activeTab === 'daily' ? 5 : 0}
+                  />
+
+                  {activeTab === 'daily' && (
+                    <CustomXAxis x={0} y={0} width={300} height={200} stroke="var(--primary)" />
+                  )}
+
+                  <YAxis
+                    domain={[0, 100]}
+                    tickFormatter={() => ''}
+                    tickLine={false}
+                    axisLine={{ stroke: 'var(--primary)' }}
+                    width={30}
+                  />
+
+                  <Tooltip
+                    content={({ active, payload, label }) => {
+                      if (active && payload && payload.length) {
+                        if (activeTab === 'daily') {
+                          const date = new Date(label);
+                          return (
+                            <div className="bg-white p-2 rounded-lg shadow-md border border-gray-200">
+                              <p className="text-sm font-medium text-[var(--main-gray)]">
+                                {date.getFullYear()}년 {date.getMonth() + 1}월 {date.getDate()}일
+                              </p>
+                              {payload.map((entry: any, index: number) => (
+                                <p
+                                  key={index}
+                                  className="text-sm"
+                                  style={{ color: COLORS[entry.dataKey as CategoryType] }}
+                                >
+                                  {entry.dataKey}: {entry.value}%
+                                </p>
+                              ))}
+                            </div>
+                          );
+                        } else {
+                          return (
+                            <div className="bg-white p-2 rounded-lg shadow-md border border-gray-200">
+                              <p className="text-sm font-medium text-[var(--main-gray)]">{label}</p>
+                              {payload.map((entry: any, index: number) => (
+                                <p
+                                  key={index}
+                                  className="text-sm"
+                                  style={{ color: COLORS[entry.dataKey as CategoryType] }}
+                                >
+                                  {entry.dataKey}: {entry.value}%
+                                </p>
+                              ))}
+                            </div>
+                          );
+                        }
+                      }
+                      return null;
+                    }}
+                    cursor={{ stroke: 'var(--primary)', strokeWidth: 1, strokeDasharray: '3 3' }}
+                  />
+
+                  {selectedCategories.map((category) => (
+                    <Area
+                      key={category}
+                      type="monotone"
+                      dataKey={category}
+                      stroke={COLORS[category]}
+                      fill={`url(#color${category})`}
+                      strokeWidth={2}
+                      dot={renderDot(category)}
+                      activeDot={false}
+                      connectNulls
+                    />
+                  ))}
+                </AreaChart>
+              </ResponsiveContainer>
+            </div>
+
+            <div className="flex justify-center gap-2.5 mt-4 w-full">
+              {(['영어', '운동', '코딩'] as CategoryType[]).map((category) => {
+                const isSelected = selectedCategories.includes(category);
+                const color = getCategoryColor(category);
+                return (
+                  <button
+                    key={category}
+                    onClick={() =>
+                      setSelectedCategories((prev) =>
+                        isSelected ? prev.filter((c) => c !== category) : [...prev, category]
+                      )
+                    }
+                    className={`px-3.5 py-0 rounded-full border text-base transition-all ${
+                      isSelected
+                        ? 'text-white border-transparent'
+                        : 'text-secondary border-primary border-2 bg-white'
+                    }`}
+                    style={
+                      isSelected
+                        ? {
+                            backgroundColor: color,
+                            borderColor: color,
+                          }
+                        : {}
+                    }
+                  >
+                    {category}
+                  </button>
+                );
+              })}
+            </div>
+          </div>
+        </Card>
+
+        <Card
+          title={
+            <div className="-mx-4 px-4 flex justify-between items-center">
+              <div className="text-secondary text-xl">AI 추천 도전과제</div>
+              <div
+                className="cursor-pointer"
+                onClick={() =>
+                  openModal({
+                    title: '안내',
+                    component: <AnalysisPopup />,
+                  })
+                }
+              >
+                <Info className="w-4 h-4 text-white fill-[var(--primary)]" />
+              </div>
+            </div>
+          }
+          bgColor="var(--beige-base)"
+        >
+          <div className="flex flex-col gap-4">
+            {hasData ? (
+              aiChallenges.map((item, idx) => (
+                <div key={idx} className="bg-[var(--beige-light)] rounded-xl p-4 w-[95%] mx-auto">
+                  <div className="flex justify-between items-center mb-2">
+                    <h3 className="text-base text-[var(--primary)]">• {item.title}</h3>
+                    <span className="text-base bg-[var(--primary-light)] text-[var(--primary)] px-2 py-0.5 rounded-full">
+                      추천
+                    </span>
+                  </div>
+
+                  <ul
+                    className="text-sm whitespace-pre-line list-none text-secondary 
+                  mb-2 space-y-1"
+                  >
+                    <li className="flex gap-1 items-start">
+                      <span className="text-[var(--primary)] mt-0.5">💡</span>
+                      <span>{item.desc[0]}</span>
+                    </li>
+                    <li className="flex gap-1 items-start">
+                      <span className="text-[var(--primary)] mt-0.5">👍</span>
+                      <span>{item.desc[1]}</span>
+                    </li>
+                  </ul>
+
+                  <div className="flex items-center justify-start text-xs text-[var(--secondary)] mt-1 gap-4">
+                    <span className="flex items-center gap-1">📅 {item.period}</span>
+                    <span className="flex items-center gap-1">⭐ {item.level}</span>
+                  </div>
+                </div>
+              ))
+            ) : (
+              <div className="bg-[var(--beige-light)] rounded-xl p-4 min-h-[80px] flex items-center justify-center w-[95%] mx-auto">
+                <p className="text-base whitespace-pre-line text-secondary text-center">
+                  {initialStateTexts.aiChallenges}
+                </p>
+              </div>
+            )}
+          </div>
+        </Card>
+      </div>
+    </div>
+  );
+}
diff --git a/components/service/analysis/AnalysisPopup.tsx b/components/service/analysis/AnalysisPopup.tsx
new file mode 100644
index 0000000..2e047b4
--- /dev/null
+++ b/components/service/analysis/AnalysisPopup.tsx
@@ -0,0 +1,23 @@
+import { useModalStore } from '@/stores/modal';
+import { PawPrint } from 'lucide-react';
+
+export function AnalysisPopup() {
+  const { closeModal } = useModalStore();
+  return (
+    <div className="bg-[#FFF6EC] rounded-xl p-4 text-xl text-secondary">
+      <p className="whitespace-pre-line text-center">
+        다른 목표를 원하시나요?{`\n`}아래 챗봇에게 최종 목표를 알려주시면,{`\n`}더 구체적인 목표를
+        추천해드릴게요!
+      </p>
+      <div className="flex justify-center mt-3">
+        <button
+          className="bg-primary-light border-primary border-2 text-secondary text-xl px-4 py-1 rounded-full shadow cursor-pointer flex items-center gap-1"
+          onClick={closeModal}
+        >
+          확인
+          <PawPrint className="w-4.5 h-4.5 fill-secondary" />
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/mocks/analysisData.ts b/mocks/analysisData.ts
new file mode 100644
index 0000000..7bc2636
--- /dev/null
+++ b/mocks/analysisData.ts
@@ -0,0 +1,82 @@
+// src/mock/analysisData.ts
+
+export const rawDailyData = [
+  { date: '2025-01-01', 영어: 45, 운동: 60, 코딩: 70 },
+  { date: '2025-01-15', 영어: 55, 운동: 65, 코딩: 75 },
+  { date: '2025-02-01', 영어: 30, 운동: 40, 코딩: 50 },
+  { date: '2025-02-15', 영어: 35, 운동: 45, 코딩: 55 },
+  { date: '2025-03-01', 영어: 52, 운동: 58, 코딩: 68 },
+  { date: '2025-03-05', 영어: 50, 운동: 60, 코딩: 70 },
+  { date: '2025-03-20', 영어: 60, 운동: 65, 코딩: 75 },
+  { date: '2025-04-01', 영어: 63, 운동: 88, 코딩: 43 },
+  { date: '2025-04-10', 영어: 65, 운동: 90, 코딩: 40 },
+  { date: '2025-04-25', 영어: 60, 운동: 85, 코딩: 45 },
+  { date: '2025-05-01', 영어: 71, 운동: 56, 코딩: 61 },
+  { date: '2025-05-02', 영어: 70, 운동: 55, 코딩: 60 },
+  { date: '2025-05-18', 영어: 75, 운동: 60, 코딩: 65 },
+  { date: '2025-06-01', 영어: 89, 운동: 83, 코딩: 71 },
+  { date: '2025-06-05', 영어: 90, 운동: 85, 코딩: 70 },
+  { date: '2025-06-25', 영어: 85, 운동: 80, 코딩: 75 },
+];
+
+// 월별 평균으로 가공한 chartData
+export const chartData = [
+  { month: '1월', 영어: 50, 운동: 62.5, 코딩: 72.5 },
+  { month: '2월', 영어: 32.5, 운동: 42.5, 코딩: 52.5 },
+  { month: '3월', 영어: 55, 운동: 62.5, 코딩: 72.5 },
+  { month: '4월', 영어: 62.5, 운동: 87.5, 코딩: 42.5 },
+  { month: '5월', 영어: 72.5, 운동: 57.5, 코딩: 62.5 },
+  { month: '6월', 영어: 87.5, 운동: 82.5, 코딩: 72.5 },
+];
+
+export const insights = [
+  {
+    title: '강점',
+    emoji: '🏆',
+    text: '영어 목표에서\n지금까지 80% 이상의 달성도를 보이고 있어요',
+  },
+  {
+    title: '개선점',
+    emoji: '🤔',
+    text: '파이썬 프로젝트 todo 달성일은\n30% 밖에 되지 않아요..! \n목표를 낮춰보는 건 어떨까요?',
+  },
+];
+
+export const aiChallenges = [
+  {
+    title: '토익 850+ 달성',
+    desc: [
+      '14일 연속 하루에 10개씩 토익 단어를 외우고 있어요. 영어의 기본은 단어!',
+      '다음엔 토익 850점에 도전해보는 건 어떨까요?\n(25년 4월 27일에 시험이 있어요)',
+    ],
+    period: '3개월',
+    level: '중급',
+  },
+  {
+    title: '파이썬 웹 크롤링 프로젝트',
+    desc: [
+      '3월 23일 메모 기록을 보니 데이터 분석 취직에 관심이 많아 보여요.',
+      '포트폴리오용으로 공공데이터를 활용해 기획안을 작성해보는 프로젝트를 진행해보는 건 어떨까요?',
+    ],
+    period: '2개월',
+    level: '초급',
+  },
+];
+
+// 초기 상태 텍스트 추가
+export const initialStateTexts = {
+  totalAchievement: `✨ 반가워요! ${'\n'}오늘은 저장된 데이터가 없어서${'\n'}내일부터 달성률을 분석해드릴게요.`,
+  strength: {
+    title: '강점',
+    emoji: '🏆',
+    text: '아직 강점을 파악할 수 없어요.\n하루 이상 기록이 쌓이면\n맞춤형 피드백을 드릴게요!',
+  },
+  improvement: {
+    title: '개선점',
+    emoji: '🤔',
+    text: '아직 개선점을 파악할 수 없어요.\n하루 이상 기록이 쌓이면\n맞춤형 피드백을 드릴게요!',
+  },
+  chart: '📊 아직 활동 기록이 없어요!\n꾸준히 기록하면 내일부터\n예쁜 그래프가 완성돼요.',
+  aiChallenges:
+    '💡 AI 추천 도전과제도 2일차부터 보여드릴 수 있어요.\n목표별 도전 과제로 구체적으로 제공해드릴게요!',
+};
diff --git a/package.json b/package.json
index e6c80c5..7b9356c 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "react-dom": "^19.0.0",
     "react-hook-form": "^7.55.0",
     "sonner": "^2.0.3",
+    "recharts": "^2.15.2",
     "tailwind-merge": "^3.1.0",
     "tw-animate-css": "^1.2.5",
     "zod": "^3.24.2",
@@ -60,4 +61,4 @@
       "public"
     ]
   }
-}
\ No newline at end of file
+}
diff --git a/public/font/font.ttf b/public/font/font.ttf
new file mode 100644
index 0000000..ef15a6f
Binary files /dev/null and b/public/font/font.ttf differ
diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js
index 34057e8..a022907 100644
--- a/public/mockServiceWorker.js
+++ b/public/mockServiceWorker.js
@@ -8,7 +8,7 @@
  * - Please do NOT serve this file on production.
  */
 
-const PACKAGE_VERSION = '2.7.3'
+const PACKAGE_VERSION = '2.7.4'
 const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
 const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
 const activeClientIds = new Set()
diff --git a/stores/modal.ts b/stores/modal.ts
index deb6591..7fcda99 100644
--- a/stores/modal.ts
+++ b/stores/modal.ts
@@ -1,8 +1,8 @@
-import { ReactNode } from "react";
-import { create } from "zustand";
+import { ReactNode } from 'react';
+import { create } from 'zustand';
 
 type ModalConfig = {
-  title: string | ReactNode;
+  title?: string | ReactNode;
   component?: ReactNode;
 };
 
diff --git a/types/chart.ts b/types/chart.ts
new file mode 100644
index 0000000..c531ce3
--- /dev/null
+++ b/types/chart.ts
@@ -0,0 +1,13 @@
+export type RawProgressItem = {
+  date: string;
+  영어?: number;
+  운동?: number;
+  코딩?: number;
+};
+
+export type MonthlyAverageItem = {
+  month: string;
+  영어: number;
+  운동: number;
+  코딩: number;
+};
diff --git a/utils/chartUtils.ts b/utils/chartUtils.ts
new file mode 100644
index 0000000..949db93
--- /dev/null
+++ b/utils/chartUtils.ts
@@ -0,0 +1,75 @@
+// utils/chartUtils.ts
+
+import { RawProgressItem } from '@/types/chart';
+
+export type MonthlyAverageItem = {
+  month: string; // '1월', '2월' 등
+  영어: number;
+  운동: number;
+  코딩: number;
+};
+
+// 현재 월 기준으로 1월부터 현재 달까지의 데이터를 추림
+function isWithinCurrentYearToNow(dateStr: string) {
+  const date = new Date(dateStr);
+  const now = new Date();
+  const currentYear = now.getFullYear();
+  const currentMonth = now.getMonth();
+  return date.getFullYear() === currentYear && date.getMonth() <= currentMonth;
+}
+
+export const groupByMonthAndAverage = (rawData: RawProgressItem[]): MonthlyAverageItem[] => {
+  const monthlyMap: Record<string, RawProgressItem[]> = {};
+
+  rawData.forEach((item) => {
+    if (!isWithinCurrentYearToNow(item.date)) return;
+    const date = new Date(item.date);
+    const month = `${date.getMonth() + 1}월`;
+
+    if (!monthlyMap[month]) monthlyMap[month] = [];
+    monthlyMap[month].push(item);
+  });
+
+  const categories: (keyof Omit<RawProgressItem, 'date'>)[] = ['영어', '운동', '코딩'];
+
+  // 2025년의 모든 월을 포함
+  const fullMonths = Array.from({ length: 12 }, (_, i) => `${i + 1}월`);
+
+  return fullMonths.map((month) => {
+    const items = monthlyMap[month] || [];
+    const averaged: MonthlyAverageItem = {
+      month,
+      영어: 0,
+      운동: 0,
+      코딩: 0,
+    };
+
+    categories.forEach((category) => {
+      const values = items
+        .map((item) => item[category])
+        .filter((v): v is number => typeof v === 'number');
+
+      averaged[category] =
+        values.length > 0
+          ? Math.round(values.reduce((sum, cur) => sum + cur, 0) / values.length)
+          : 0;
+    });
+
+    return averaged;
+  });
+};
+
+export const getCategoryExtremes = (data: any[], category: string) => {
+  const values = data
+    .map((item) => item[category])
+    .filter((value): value is number => value !== undefined && value !== null);
+
+  if (values.length === 0) {
+    return { min: 0, max: 0 };
+  }
+
+  return {
+    min: Math.min(...values),
+    max: Math.max(...values),
+  };
+};
diff --git a/yarn.lock b/yarn.lock
index be47c2c..12e20a9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7,6 +7,13 @@
   resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
   integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
 
+"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
+  version "7.27.0"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
+  integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
+  dependencies:
+    regenerator-runtime "^0.14.0"
+
 "@bundled-es-modules/cookie@^2.0.1":
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz#b41376af6a06b3e32a15241d927b840a9b4de507"
@@ -52,9 +59,9 @@
     tslib "^2.4.0"
 
 "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz#bfe67b3d334a8579a35e48fe240dc0638d1bcd91"
-  integrity sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz#b0fc7e06d0c94f801537fd4237edc2706d3b8e4c"
+  integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==
   dependencies:
     eslint-visitor-keys "^3.4.3"
 
@@ -914,6 +921,57 @@
   resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
   integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
 
+"@types/d3-array@^3.0.3":
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
+  integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
+
+"@types/d3-color@*":
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+  integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-ease@^3.0.0":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+  integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-interpolate@^3.0.1":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+  integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+  dependencies:
+    "@types/d3-color" "*"
+
+"@types/d3-path@*":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a"
+  integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==
+
+"@types/d3-scale@^4.0.2":
+  version "4.0.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb"
+  integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==
+  dependencies:
+    "@types/d3-time" "*"
+
+"@types/d3-shape@^3.1.0":
+  version "3.1.7"
+  resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555"
+  integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==
+  dependencies:
+    "@types/d3-path" "*"
+
+"@types/d3-time@*", "@types/d3-time@^3.0.0":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f"
+  integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
+
+"@types/d3-timer@^3.0.0":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+  integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
 "@types/estree@^1.0.6":
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
@@ -942,9 +1000,9 @@
   integrity sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==
 
 "@types/react@^19":
-  version "19.1.1"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.1.tgz#f7f2bb4a0a8d22b9ee4b8f6e27f0c78d76eb7f9b"
-  integrity sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==
+  version "19.1.0"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.0.tgz#73c43ad9bc43496ca8184332b111e2aef63fc9da"
+  integrity sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==
   dependencies:
     csstype "^3.0.2"
 
@@ -1039,87 +1097,82 @@
     "@typescript-eslint/types" "8.29.1"
     eslint-visitor-keys "^4.2.0"
 
-"@unrs/resolver-binding-darwin-arm64@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz#0c64ebe422a3d05ada91d8ba84e037383742c955"
-  integrity sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ==
-
-"@unrs/resolver-binding-darwin-x64@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.5.0.tgz#57210874eca22ec3a07039c97c028fb19c0c6d57"
-  integrity sha512-qpUrXgH4e/0xu1LOhPEdfgSY3vIXOxDQv370NEL8npN8h40HcQDA+Pl2r4HBW6tTXezWIjxUFcP7tj529RZtDw==
-
-"@unrs/resolver-binding-freebsd-x64@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.5.0.tgz#4519371d0ad8e557a86623d8497e3abcdcb5ae43"
-  integrity sha512-3tX8r8vgjvZzaJZB4jvxUaaFCDCb3aWDCpZN3EjhGnnwhztslI05KSG5NY/jNjlcZ5QWZ7dEZZ/rNBFsmTaSPw==
-
-"@unrs/resolver-binding-linux-arm-gnueabihf@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.5.0.tgz#4fc05aec9e65a6478003a0b9034a06ac0da886ab"
-  integrity sha512-FH+ixzBKaUU9fWOj3TYO+Yn/eO6kYvMLV9eNJlJlkU7OgrxkCmiMS6wUbyT0KA3FOZGxnEQ2z3/BHgYm2jqeLA==
-
-"@unrs/resolver-binding-linux-arm-musleabihf@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.5.0.tgz#c24b35dd5818fcd25569425b1dc1a98a883e248b"
-  integrity sha512-pxCgXMgwB/4PfqFQg73lMhmWwcC0j5L+dNXhZoz/0ek0iS/oAWl65fxZeT/OnU7fVs52MgdP2q02EipqJJXHSg==
-
-"@unrs/resolver-binding-linux-arm64-gnu@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.5.0.tgz#07dc8478a0a356d343790208dc557d6d053689af"
-  integrity sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g==
-
-"@unrs/resolver-binding-linux-arm64-musl@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.5.0.tgz#169e531731f7e462dffa410034a1d06a7a921aa8"
-  integrity sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw==
-
-"@unrs/resolver-binding-linux-ppc64-gnu@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.5.0.tgz#f6ad2ff47d74c8158b28a18536a71a8ecf84a17f"
-  integrity sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA==
-
-"@unrs/resolver-binding-linux-riscv64-gnu@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.5.0.tgz#2f3986cb44f285f90d27e87cee8b4059de3ffbdd"
-  integrity sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA==
-
-"@unrs/resolver-binding-linux-s390x-gnu@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.5.0.tgz#813ea07833012bc34ecc59f023e422b421138761"
-  integrity sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw==
-
-"@unrs/resolver-binding-linux-x64-gnu@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.5.0.tgz#18b0d7553268fa490db92be578ac4b0fd8cae049"
-  integrity sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA==
-
-"@unrs/resolver-binding-linux-x64-musl@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.5.0.tgz#04541e98d16e358c695393251e365bc3d802dfa4"
-  integrity sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ==
-
-"@unrs/resolver-binding-wasm32-wasi@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.5.0.tgz#7a2ae7467c4c52d53c20ad7fc2bace1b23de8168"
-  integrity sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg==
+"@unrs/resolver-binding-darwin-arm64@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.4.1.tgz#56a6638f202eff593dab780cc7b45ea020489dc6"
+  integrity sha512-8Tv+Bsd0BjGwfEedIyor4inw8atppRxM5BdUnIt+3mAm/QXUm7Dw74CHnXpfZKXkp07EXJGiA8hStqCINAWhdw==
+
+"@unrs/resolver-binding-darwin-x64@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.4.1.tgz#9b935596b19fe249a4823af84e1949605fe084b8"
+  integrity sha512-X8c3PhWziEMKAzZz+YAYWfwawi5AEgzy/hmfizAB4C70gMHLKmInJcp1270yYAOs7z07YVFI220pp50z24Jk3A==
+
+"@unrs/resolver-binding-freebsd-x64@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.4.1.tgz#72bc9ae0299aad59bb07c53776f1f470f4457528"
+  integrity sha512-UUr/nREy1UdtxXQnmLaaTXFGOcGxPwNIzeJdb3KXai3TKtC1UgNOB9s8KOA4TaxOUBR/qVgL5BvBwmUjD5yuVA==
+
+"@unrs/resolver-binding-linux-arm-gnueabihf@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.4.1.tgz#82921377ff7e20b28ca26f636b2231fe449d4a53"
+  integrity sha512-e3pII53dEeS8inkX6A1ad2UXE0nuoWCqik4kOxaDnls0uJUq0ntdj5d9IYd+bv5TDwf9DSge/xPOvCmRYH+Tsw==
+
+"@unrs/resolver-binding-linux-arm-musleabihf@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.4.1.tgz#9b315738ecd677f9caa93717891016636a2ee820"
+  integrity sha512-e/AKKd9gR+HNmVyDEPI/PIz2t0DrA3cyonHNhHVjrkxe8pMCiYiqhtn1+h+yIpHUtUlM6Y1FNIdivFa+r7wrEQ==
+
+"@unrs/resolver-binding-linux-arm64-gnu@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.4.1.tgz#98e1bba4f7c7761fdea4ed5effc0a5ad6954cc79"
+  integrity sha512-vtIu34luF1jRktlHtiwm2mjuE8oJCsFiFr8hT5+tFQdqFKjPhbJXn83LswKsOhy0GxAEevpXDI4xxEwkjuXIPA==
+
+"@unrs/resolver-binding-linux-arm64-musl@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.4.1.tgz#62fb410adcf82b6513891639ef74f7e0339bb681"
+  integrity sha512-H3PaOuGyhFXiyJd+09uPhGl4gocmhyi1BRzvsP8Lv5AQO3p3/ZY7WjV4t2NkBksm9tMjf3YbOVHyPWi2eWsNYw==
+
+"@unrs/resolver-binding-linux-ppc64-gnu@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.4.1.tgz#0a0d7bc9eb5ff70678f266a8ee80a464ebbec0e3"
+  integrity sha512-4+GmJcaaFntCi1S01YByqp8wLMjV/FyQyHVGm0vedIhL1Vfx7uHkz/sZmKsidRwokBGuxi92GFmSzqT2O8KcNA==
+
+"@unrs/resolver-binding-linux-s390x-gnu@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.4.1.tgz#02c9dd02ac1c6192253749cc7fca35b9d7a249bd"
+  integrity sha512-6RDQVCmtFYTlhy89D5ixTqo9bTQqFhvNN0Ey1wJs5r+01Dq15gPHRXv2jF2bQATtMrOfYwv+R2ZR9ew1N1N3YQ==
+
+"@unrs/resolver-binding-linux-x64-gnu@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.4.1.tgz#83fa32f5da9a5f3143185b2d0bc2ad95e4a7a102"
+  integrity sha512-XpU9uzIkD86+19NjCXxlVPISMUrVXsXo5htxtuG+uJ59p5JauSRZsIxQxzzfKzkxEjdvANPM/lS1HFoX6A6QeA==
+
+"@unrs/resolver-binding-linux-x64-musl@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.4.1.tgz#32405d987943b4fd621f783f33fad700e3ac2d0f"
+  integrity sha512-3CDjG/spbTKCSHl66QP2ekHSD+H34i7utuDIM5gzoNBcZ1gTO0Op09Wx5cikXnhORRf9+HyDWzm37vU1PLSM1A==
+
+"@unrs/resolver-binding-wasm32-wasi@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.4.1.tgz#21d18351367e3bfe035636df05153d7f08823568"
+  integrity sha512-50tYhvbCTnuzMn7vmP8IV2UKF7ITo1oihygEYq9wW2DUb/Y+QMqBHJUSCABRngATjZ4shOK6f2+s0gQX6ElENQ==
   dependencies:
     "@napi-rs/wasm-runtime" "^0.2.8"
 
-"@unrs/resolver-binding-win32-arm64-msvc@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.5.0.tgz#11deb282b8ce73fab26f1d04df0fa4d6363752c2"
-  integrity sha512-FTZBxLL4SO1mgIM86KykzJmPeTPisBDHQV6xtfDXbTMrentuZ6SdQKJUV5BWaoUK3p8kIULlrCcucqdCnk8Npg==
+"@unrs/resolver-binding-win32-arm64-msvc@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.4.1.tgz#563563b9ea7761dd7d20603792b518c4924d6f50"
+  integrity sha512-KyJiIne/AqV4IW0wyQO34wSMuJwy3VxVQOfIXIPyQ/Up6y/zi2P/WwXb78gHsLiGRUqCA9LOoCX+6dQZde0g1g==
 
-"@unrs/resolver-binding-win32-ia32-msvc@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.5.0.tgz#2a5d414912379425bd395ea15901a5dd5febc7c1"
-  integrity sha512-i5bB7vJ1waUsFciU/FKLd4Zw0VnAkvhiJ4//jYQXyDUuiLKodmtQZVTcOPU7pp97RrNgCFtXfC1gnvj/DHPJTw==
+"@unrs/resolver-binding-win32-ia32-msvc@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.4.1.tgz#c734d9389fe57912b5b3b05aa000d9cb6771ceee"
+  integrity sha512-y2NUD7pygrBolN2NoXUrwVqBpKPhF8DiSNE5oB5/iFO49r2DpoYqdj5HPb3F42fPBH5qNqj6Zg63+xCEzAD2hw==
 
-"@unrs/resolver-binding-win32-x64-msvc@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.5.0.tgz#5768c6bba4a27833a48a8a77e50eb01b520d0962"
-  integrity sha512-wAvXp4k7jhioi4SebXW/yfzzYwsUCr9kIX4gCsUFKpCTUf8Mi7vScJXI3S+kupSUf0LbVHudR8qBbe2wFMSNUw==
+"@unrs/resolver-binding-win32-x64-msvc@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.4.1.tgz#e32c666092bc419fd7c3e28fea7c74292aaa94a3"
+  integrity sha512-hVXaObGI2lGFmrtT77KSbPQ3I+zk9IU500wobjk0+oX59vg/0VqAzABNtt3YSQYgXTC2a/LYxekLfND/wlt0yQ==
 
 acorn-jsx@^5.3.2:
   version "5.3.2"
@@ -1402,7 +1455,7 @@ cliui@^8.0.1:
     strip-ansi "^6.0.1"
     wrap-ansi "^7.0.0"
 
-clsx@^2.1.1:
+clsx@^2.0.0, clsx@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
   integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -1459,6 +1512,77 @@ csstype@^3.0.2:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
 
+"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+  integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+  dependencies:
+    internmap "1 - 2"
+
+"d3-color@1 - 3":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+  integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-ease@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+  integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+"d3-format@1 - 3":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+  integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+  integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+  dependencies:
+    d3-color "1 - 3"
+
+d3-path@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+  integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-scale@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+  integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+  dependencies:
+    d3-array "2.10.0 - 3"
+    d3-format "1 - 3"
+    d3-interpolate "1.2.0 - 3"
+    d3-time "2.1.1 - 3"
+    d3-time-format "2 - 4"
+
+d3-shape@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+  integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+  dependencies:
+    d3-path "^3.1.0"
+
+"d3-time-format@2 - 4":
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+  integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+  dependencies:
+    d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+  integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+  dependencies:
+    d3-array "2 - 3"
+
+d3-timer@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+  integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
 damerau-levenshtein@^1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -1510,6 +1634,11 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0:
   dependencies:
     ms "^2.1.3"
 
+decimal.js-light@^2.4.1:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
+  integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
 deep-is@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@@ -1550,6 +1679,14 @@ doctrine@^2.1.0:
   dependencies:
     esutils "^2.0.2"
 
+dom-helpers@^5.0.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
+  integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
+  dependencies:
+    "@babel/runtime" "^7.8.7"
+    csstype "^3.0.2"
+
 dunder-proto@^1.0.0, dunder-proto@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
@@ -1921,11 +2058,21 @@ esutils@^2.0.2:
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
+eventemitter3@^4.0.1:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
+fast-equals@^5.0.1:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.2.2.tgz#885d7bfb079fac0ce0e8450374bce29e9b742484"
+  integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==
+
 fast-glob@3.3.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
@@ -2211,6 +2358,11 @@ internal-slot@^1.1.0:
     hasown "^2.0.2"
     side-channel "^1.1.0"
 
+"internmap@1 - 2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+  integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
 is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280"
@@ -2588,6 +2740,11 @@ 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==
 
+lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
 loose-envify@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -2643,9 +2800,9 @@ ms@^2.1.1, ms@^2.1.3:
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
 msw@^2.7.3:
-  version "2.7.3"
-  resolved "https://registry.yarnpkg.com/msw/-/msw-2.7.3.tgz#5ad569fae7c7cdb8be2eeba3d041c185600b25a0"
-  integrity sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/msw/-/msw-2.7.4.tgz#2c218502a46045cf07b95b31c9d128fef1b2a376"
+  integrity sha512-A2kuMopOjAjNEYkn0AnB1uj+x7oBjLIunFk7Ud4icEnVWFf6iBekn8oXW4zIwcpfEdWP9sLqyVaHVzneWoGEww==
   dependencies:
     "@bundled-es-modules/cookie" "^2.0.1"
     "@bundled-es-modules/statuses" "^1.0.1"
@@ -2885,7 +3042,7 @@ prelude-ls@^1.2.1:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
 
-prop-types@^15.8.1:
+prop-types@^15.6.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==
@@ -2938,6 +3095,11 @@ react-is@^16.13.1:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
+react-is@^18.3.1:
+  version "18.3.1"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
+  integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
+
 react-remove-scroll-bar@^2.3.7:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
@@ -2957,6 +3119,15 @@ react-remove-scroll@^2.6.3:
     use-callback-ref "^1.3.3"
     use-sidecar "^1.1.3"
 
+react-smooth@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4"
+  integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==
+  dependencies:
+    fast-equals "^5.0.1"
+    prop-types "^15.8.1"
+    react-transition-group "^4.4.5"
+
 react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
@@ -2965,11 +3136,42 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
     get-nonce "^1.0.0"
     tslib "^2.0.0"
 
+react-transition-group@^4.4.5:
+  version "4.4.5"
+  resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
+  integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    dom-helpers "^5.0.1"
+    loose-envify "^1.4.0"
+    prop-types "^15.6.2"
+
 react@^19.0.0:
   version "19.1.0"
   resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
   integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
 
+recharts-scale@^0.4.4:
+  version "0.4.5"
+  resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
+  integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
+  dependencies:
+    decimal.js-light "^2.4.1"
+
+recharts@^2.15.2:
+  version "2.15.2"
+  resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.2.tgz#12adcda964b4054fba517512eb94fdff1463b0c1"
+  integrity sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==
+  dependencies:
+    clsx "^2.0.0"
+    eventemitter3 "^4.0.1"
+    lodash "^4.17.21"
+    react-is "^18.3.1"
+    react-smooth "^4.0.4"
+    recharts-scale "^0.4.4"
+    tiny-invariant "^1.3.1"
+    victory-vendor "^36.6.8"
+
 reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"
@@ -2984,6 +3186,11 @@ reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
     get-proto "^1.0.1"
     which-builtin-type "^1.2.1"
 
+regenerator-runtime@^0.14.0:
+  version "0.14.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+  integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
+
 regexp.prototype.flags@^1.5.3:
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19"
@@ -3371,6 +3578,11 @@ tapable@^2.2.0:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
   integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
 
+tiny-invariant@^1.3.1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
+  integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
+
 tinyglobby@^0.2.12:
   version "0.2.12"
   resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5"
@@ -3518,26 +3730,25 @@ universalify@^0.2.0:
   integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
 
 unrs-resolver@^1.3.2:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.5.0.tgz#d0a608f08321d8e90ba8eb10a3240e7995997275"
-  integrity sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA==
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.4.1.tgz#da01a5ca745a431b109af6798a775d9c4ba4ae0d"
+  integrity sha512-MhPB3wBI5BR8TGieTb08XuYlE8oFVEXdSAgat3psdlRyejl8ojQ8iqPcjh094qCZ1r+TnkxzP6BeCd/umfHckQ==
   optionalDependencies:
-    "@unrs/resolver-binding-darwin-arm64" "1.5.0"
-    "@unrs/resolver-binding-darwin-x64" "1.5.0"
-    "@unrs/resolver-binding-freebsd-x64" "1.5.0"
-    "@unrs/resolver-binding-linux-arm-gnueabihf" "1.5.0"
-    "@unrs/resolver-binding-linux-arm-musleabihf" "1.5.0"
-    "@unrs/resolver-binding-linux-arm64-gnu" "1.5.0"
-    "@unrs/resolver-binding-linux-arm64-musl" "1.5.0"
-    "@unrs/resolver-binding-linux-ppc64-gnu" "1.5.0"
-    "@unrs/resolver-binding-linux-riscv64-gnu" "1.5.0"
-    "@unrs/resolver-binding-linux-s390x-gnu" "1.5.0"
-    "@unrs/resolver-binding-linux-x64-gnu" "1.5.0"
-    "@unrs/resolver-binding-linux-x64-musl" "1.5.0"
-    "@unrs/resolver-binding-wasm32-wasi" "1.5.0"
-    "@unrs/resolver-binding-win32-arm64-msvc" "1.5.0"
-    "@unrs/resolver-binding-win32-ia32-msvc" "1.5.0"
-    "@unrs/resolver-binding-win32-x64-msvc" "1.5.0"
+    "@unrs/resolver-binding-darwin-arm64" "1.4.1"
+    "@unrs/resolver-binding-darwin-x64" "1.4.1"
+    "@unrs/resolver-binding-freebsd-x64" "1.4.1"
+    "@unrs/resolver-binding-linux-arm-gnueabihf" "1.4.1"
+    "@unrs/resolver-binding-linux-arm-musleabihf" "1.4.1"
+    "@unrs/resolver-binding-linux-arm64-gnu" "1.4.1"
+    "@unrs/resolver-binding-linux-arm64-musl" "1.4.1"
+    "@unrs/resolver-binding-linux-ppc64-gnu" "1.4.1"
+    "@unrs/resolver-binding-linux-s390x-gnu" "1.4.1"
+    "@unrs/resolver-binding-linux-x64-gnu" "1.4.1"
+    "@unrs/resolver-binding-linux-x64-musl" "1.4.1"
+    "@unrs/resolver-binding-wasm32-wasi" "1.4.1"
+    "@unrs/resolver-binding-win32-arm64-msvc" "1.4.1"
+    "@unrs/resolver-binding-win32-ia32-msvc" "1.4.1"
+    "@unrs/resolver-binding-win32-x64-msvc" "1.4.1"
 
 uri-js@^4.2.2:
   version "4.4.1"
@@ -3569,6 +3780,26 @@ use-sidecar@^1.1.3:
     detect-node-es "^1.1.0"
     tslib "^2.0.0"
 
+victory-vendor@^36.6.8:
+  version "36.9.2"
+  resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"
+  integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==
+  dependencies:
+    "@types/d3-array" "^3.0.3"
+    "@types/d3-ease" "^3.0.0"
+    "@types/d3-interpolate" "^3.0.1"
+    "@types/d3-scale" "^4.0.2"
+    "@types/d3-shape" "^3.1.0"
+    "@types/d3-time" "^3.0.0"
+    "@types/d3-timer" "^3.0.0"
+    d3-array "^3.1.6"
+    d3-ease "^3.0.1"
+    d3-interpolate "^3.0.1"
+    d3-scale "^4.0.2"
+    d3-shape "^3.1.0"
+    d3-time "^3.0.0"
+    d3-timer "^3.0.1"
+
 which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @Yoonsoyoung02, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces a new analysis page for the front-end, including chart visualizations, data handling, and UI components. It focuses on presenting user data in a clear and interactive manner, with considerations for initial states (no data available) and mock data integration for future server connectivity. The changes include improvements to chart configurations, data presentation, and overall page structure.

### Highlights
* **Analysis Page Creation**: Implements the basic structure and components for the analysis page, including chart rendering and data presentation logic.
* **Chart Configuration and Visualization**: Enhances chart display by separating monthly and daily data via tabs, emphasizing max/min values instead of Y-axis labels, and minimizing X-axis labels with tooltip support for detailed data.
* **Initial State Handling**: Adds handling for scenarios where analysis data is not yet available, providing placeholder text and empty state screens.
* **Mock Data Setup**: Configures mock data for text and UI elements to facilitate future integration with a backend API.
* **Component Reusability**: Modifies the Card component to accept React nodes for the title, allowing for more complex title structures, and adds styling options.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **app/(service)/analysis/page.tsx**
  * Imports and renders the new AnalysisPage component.
* **components/common/ActionButton.tsx**
  * Makes the `children` prop optional.
* **components/common/Card.tsx**
  * Updates the Card component to accept a React node as the title.
  * Adds bgColor, shadowColor, and className props for styling customization.
  * Adjusts the component's structure to include a separator and improve layout.
* **components/common/PieChartBox.tsx**
  * Creates a PieChartBox component for displaying data in a pie chart format, using recharts library.
  * Includes styling and dynamic value rendering.
* **components/service/analysis/Analysis.tsx**
  * Integrates the AnalysisPage component within the Analysis component, structuring the header and main content area.
* **components/service/analysis/AnalysisPage.tsx**
  * Implements the main analysis page with chart components, data fetching (mocked), and UI elements.
  * Includes tabbed views for monthly and daily data, chart configurations, and handling for initial states.
  * Adds AI-recommended challenges and a popup component for additional information.
* **components/service/analysis/AnalysisPopup.tsx**
  * Creates a popup component for displaying additional information or guidance related to the analysis page.
* **components/service/home/Home.tsx**
  * Removes the card components from the home page.
* **mocks/analysisData.ts**
  * Adds mock data for chart data, insights, AI challenges, and initial state texts to simulate API responses and data structures.
* **package.json**
  * Adds `recharts` as a dependency.
  * Updates the version of the package.json
* **stores/modal.ts**
  * Makes the `title` prop optional.
* **types/chart.ts**
  * Adds types for chart data.
* **utils/chartUtils.ts**
  * Adds utility functions for chart data manipulation, including grouping by month and calculating averages.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A chart's tale,
> Data points in the breeze,
> Insights bloom forth.


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-13T03:02:08Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request introduces a new analysis page with charts and AI-driven insights. The implementation appears well-structured, with good use of components and data mocking. However, there are several areas that could be improved, particularly regarding data handling, UI clarity, and code maintainability. I have provided some feedback below.

### Summary of Findings
* **Missing API Integration**: The code currently relies on mock data. Integrating with the actual backend API is crucial for the analysis page to display real-time and accurate information. The `TODO: BE API 호출` comment should be addressed.
* **Hardcoded Year in `isWithinCurrentYearToNow`**: The `isWithinCurrentYearToNow` function in `utils/chartUtils.ts` has a hardcoded year (2025). This should be dynamic to ensure the chart works correctly in future years.
* **Inconsistent use of `hasData`**: The `hasData` state variable is set to `false` after fetching mock data, which seems counterintuitive. This might lead to incorrect initial state rendering. Review the logic to ensure it aligns with the intended behavior.

### Merge Readiness
The pull request is not ready for merging. The reliance on mock data and the hardcoded year in `isWithinCurrentYearToNow` are significant issues that need to be addressed. The inconsistent use of `hasData` should also be reviewed. I am unable to approve this pull request, and recommend that it not be merged until these issues are resolved.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-13T03:02:19Z
user: login: trivialcoding6
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T07:29:19Z
user: login: Yoonsoyoung02
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T08:02:06Z
user: login: Yoonsoyoung02
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T08:02:32Z
user: login: trivialcoding6
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-14T23:07:22Z
user: login: Kristyn-Yoon
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-15T00:42:13Z

Review comments:
diff_hunk: @@ -0,0 +1,522 @@
+'use client';
+
+import { useEffect, useState, useMemo } from 'react';
+import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, DotProps } from 'recharts';
+import { Info } from 'lucide-react';
+import dynamic from 'next/dynamic';
+
+import Card from '@/components/common/Card';
+import { ActionButton } from '@/components/common/ActionButton';
+import { useModalStore } from '@/stores/modal';
+import {
+  rawDailyData,
+  insights,
+  aiChallenges,
+  chartData as mockChartData,
+  initialStateTexts as mockInitialStateTexts,
+} from '@/mocks/analysisData';
+import { groupByMonthAndAverage, getCategoryExtremes } from '@/utils/chartUtils';
+import { AnalysisPopup } from './AnalysisPopup';
+
+const PieChartBox = dynamic(() => import('@/components/common/PieChartBox'), {
+  ssr: false,
+});
+
+const COLORS: Record<CategoryType, string> = {
+  영어: 'var(--primary)',
+  운동: 'var(--category-exercise)',
+  코딩: 'var(--category-coding)',
+} as const;
+type CategoryType = '영어' | '운동' | '코딩';
+
+type TabType = 'daily' | 'monthly';
+
+// 초기 상태 텍스트 타입 정의
+type InitialStateTexts = {
+  totalAchievement: string;
+  strength: {
+    title: string;
+    emoji: string;
+    text: string;
+  };
+  improvement: {
+    title: string;
+    emoji: string;
+    text: string;
+  };
+  chart: string;
+  aiChallenges: string;
+};
+
+export default function AnalysisPage() {
+  const [selectedCategories, setSelectedCategories] = useState<CategoryType[]>([
+    '영어',
+    '운동',
+    '코딩',
+  ]);
+  const [chartData, setChartData] = useState<any[]>([]);
+  const [activeTab, setActiveTab] = useState<TabType>('monthly');
+  const [totalAchievementRate, setTotalAchievementRate] = useState<number>(0);
+  const [hasData, setHasData] = useState<boolean>(false);
+  const { openModal } = useModalStore();
+
+  // 초기 상태 텍스트 상태 추가
+  const [initialStateTexts, setInitialStateTexts] =
+    useState<InitialStateTexts>(mockInitialStateTexts);
+
+  // BE 데이터 fetch
+  useEffect(() => {
+    // TODO: BE API 호출
path: components/service/analysis/AnalysisPage.tsx
user: login: gemini-code-assist[bot]
body: ![critical](https://img.shields.io/badge/priority-critical-red)

This `TODO` comment indicates that the API integration is not yet complete. This is a critical issue that needs to be addressed before merging. Please integrate with the actual backend API to fetch real data.
created_at: 2025-04-13T03:02:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,522 @@
+'use client';
+
+import { useEffect, useState, useMemo } from 'react';
+import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, DotProps } from 'recharts';
+import { Info } from 'lucide-react';
+import dynamic from 'next/dynamic';
+
+import Card from '@/components/common/Card';
+import { ActionButton } from '@/components/common/ActionButton';
+import { useModalStore } from '@/stores/modal';
+import {
+  rawDailyData,
+  insights,
+  aiChallenges,
+  chartData as mockChartData,
+  initialStateTexts as mockInitialStateTexts,
+} from '@/mocks/analysisData';
+import { groupByMonthAndAverage, getCategoryExtremes } from '@/utils/chartUtils';
+import { AnalysisPopup } from './AnalysisPopup';
+
+const PieChartBox = dynamic(() => import('@/components/common/PieChartBox'), {
+  ssr: false,
+});
+
+const COLORS: Record<CategoryType, string> = {
+  영어: 'var(--primary)',
+  운동: 'var(--category-exercise)',
+  코딩: 'var(--category-coding)',
+} as const;
+type CategoryType = '영어' | '운동' | '코딩';
+
+type TabType = 'daily' | 'monthly';
+
+// 초기 상태 텍스트 타입 정의
+type InitialStateTexts = {
+  totalAchievement: string;
+  strength: {
+    title: string;
+    emoji: string;
+    text: string;
+  };
+  improvement: {
+    title: string;
+    emoji: string;
+    text: string;
+  };
+  chart: string;
+  aiChallenges: string;
+};
+
+export default function AnalysisPage() {
+  const [selectedCategories, setSelectedCategories] = useState<CategoryType[]>([
+    '영어',
+    '운동',
+    '코딩',
+  ]);
+  const [chartData, setChartData] = useState<any[]>([]);
+  const [activeTab, setActiveTab] = useState<TabType>('monthly');
+  const [totalAchievementRate, setTotalAchievementRate] = useState<number>(0);
+  const [hasData, setHasData] = useState<boolean>(false);
+  const { openModal } = useModalStore();
+
+  // 초기 상태 텍스트 상태 추가
+  const [initialStateTexts, setInitialStateTexts] =
+    useState<InitialStateTexts>(mockInitialStateTexts);
+
+  // BE 데이터 fetch
+  useEffect(() => {
+    // TODO: BE API 호출
+    const fetchData = async () => {
+      try {
+        // 실제 API 호출 시 아래 주석 해제
+        // const response = await fetch('/api/analysis');
+        // const data = await response.json();
+        // setTotalAchievementRate(data.totalAchievementRate);
+        // setChartData(data.chartData);
+        // setHasData(true);
+
+        // 초기 상태 텍스트도 함께 가져오기
+        // setInitialStateTexts(data.initialStateTexts);
+
+        // 임시로 mock 데이터 사용
+        const monthlyData = groupByMonthAndAverage(rawDailyData);
+        setChartData(monthlyData);
+
+        // 초기 상태를 보여주기 위해 hasData를 false로 설정
+        setHasData(false);
path: components/service/analysis/AnalysisPage.tsx
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

Why is `setHasData(false)` being called here? This seems counterintuitive, as you've just fetched the data (or mock data). Setting it to `false` will cause the initial state to always be displayed, even when data is available. Please review this logic.
created_at: 2025-04-13T03:02:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,72 @@
+// utils/chartUtils.ts
+
+import { RawProgressItem } from '@/types/chart';
+
+export type MonthlyAverageItem = {
+  month: string; // '1월', '2월' 등
+  영어: number;
+  운동: number;
+  코딩: number;
+};
+
+// 현재 월 기준으로 1월부터 현재 달까지의 데이터를 추림
+function isWithinCurrentYearToNow(dateStr: string) {
+  const date = new Date(dateStr);
+  return date.getFullYear() === 2025 && date.getMonth() <= 11; // 2025년의 모든 월 데이터 포함
path: utils/chartUtils.ts
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The year `2025` is hardcoded in this function. This will cause the chart to stop working correctly in future years. Consider making this dynamic by using `new Date().getFullYear()` or passing the year as a parameter.

```suggestion
  return date.getFullYear() === new Date().getFullYear() && date.getMonth() <= 11;
```
created_at: 2025-04-13T03:02:19Z
author_association: NONE
diff_hunk: @@ -5,7 +5,7 @@ import { useState, ReactNode } from 'react';
 type Props = {
   onClick: () => void;
   icon?: ReactNode;
-  children: ReactNode;
+  children?: ReactNode;
path: components/common/ActionButton.tsx
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider whether `children` should be optional. If the component always requires children, it's better to enforce it in the type definition.

```typescript
  children: ReactNode;
```
created_at: 2025-04-13T03:02:19Z
author_association: NONE
diff_hunk: @@ -1,30 +1,43 @@
 'use client';
 import { Separator } from '@/components/ui/separator';
 
-interface CardProps {
-  title: string;
-  height?: string;
+type Props = {
+  title: React.ReactNode;
   isMore?: boolean;
-  children?: React.ReactNode; //chilren 생성
-}
+  children: React.ReactNode;
+  bgColor?: string;
+  shadowColor?: string;
+  className?: string;
+};
 
 export default function Card({
   title,
-  height, //높이 수정
   isMore = false,
   children,
-}: CardProps) {
+  bgColor = '#FDE8CE',
path: components/common/Card.tsx
user: login: trivialcoding6
body: @Yoonsoyoung02 
컬러값 유틸리티 클래스로 수정해주세용!
created_at: 2025-04-14T01:42:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+// components/chart/PieChartBox.tsx
+'use client';
+
+import { PieChart, Pie, Cell } from 'recharts';
+
+type Props = {
+  value: number;
+};
+
+export default function PieChartBox({ value }: Props) {
+  return (
+    <div className="relative w-[150px] h-[75px]">
+      <PieChart width={150} height={75}>
+        <Pie
+          data={[
+            { name: '전체 달성도', value: value },
+            { name: '남은 부분', value: 100 - value },
+          ]}
+          cx="50%"
+          cy="100%"
+          innerRadius={40}
+          outerRadius={60}
+          startAngle={180}
+          endAngle={0}
+          paddingAngle={3}
+          dataKey="value"
+        >
+          <Cell fill="#FDA63A" />
path: components/common/PieChartBox.tsx
user: login: trivialcoding6
body: @Yoonsoyoung02 
혹시 여기 색상 코드는 tailwind가 아니라서 적용이 안되는걸까요!?
created_at: 2025-04-14T01:44:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,72 @@
+// utils/chartUtils.ts
+
+import { RawProgressItem } from '@/types/chart';
+
+export type MonthlyAverageItem = {
+  month: string; // '1월', '2월' 등
+  영어: number;
+  운동: number;
+  코딩: number;
+};
+
+// 현재 월 기준으로 1월부터 현재 달까지의 데이터를 추림
+function isWithinCurrentYearToNow(dateStr: string) {
+  const date = new Date(dateStr);
+  return date.getFullYear() === 2025 && date.getMonth() <= 11; // 2025년의 모든 월 데이터 포함
path: utils/chartUtils.ts
user: login: trivialcoding6
body: @Yoonsoyoung02 
여기 2025로 하드코딩해도 되는건지 한번 확인 부탁드립니닷
created_at: 2025-04-14T07:28:02Z
author_association: COLLABORATOR
diff_hunk: @@ -1,30 +1,43 @@
 'use client';
 import { Separator } from '@/components/ui/separator';
 
-interface CardProps {
-  title: string;
-  height?: string;
+type Props = {
+  title: React.ReactNode;
   isMore?: boolean;
-  children?: React.ReactNode; //chilren 생성
-}
+  children: React.ReactNode;
+  bgColor?: string;
+  shadowColor?: string;
+  className?: string;
+};
 
 export default function Card({
   title,
-  height, //높이 수정
   isMore = false,
   children,
-}: CardProps) {
+  bgColor = '#FDE8CE',
path: components/common/Card.tsx
user: login: Yoonsoyoung02
body: 수정 완료했습니다!
created_at: 2025-04-14T08:02:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,37 @@
+// components/chart/PieChartBox.tsx
+'use client';
+
+import { PieChart, Pie, Cell } from 'recharts';
+
+type Props = {
+  value: number;
+};
+
+export default function PieChartBox({ value }: Props) {
+  return (
+    <div className="relative w-[150px] h-[75px]">
+      <PieChart width={150} height={75}>
+        <Pie
+          data={[
+            { name: '전체 달성도', value: value },
+            { name: '남은 부분', value: 100 - value },
+          ]}
+          cx="50%"
+          cy="100%"
+          innerRadius={40}
+          outerRadius={60}
+          startAngle={180}
+          endAngle={0}
+          paddingAngle={3}
+          dataKey="value"
+        >
+          <Cell fill="#FDA63A" />
path: components/common/PieChartBox.tsx
user: login: Yoonsoyoung02
body: 네 기존 코드대로 두어야 할 것 같습니당..!
created_at: 2025-04-14T08:02:31Z
author_association: COLLABORATOR

PR comments:
user: login: trivialcoding6
created_at: 2025-04-14T01:37:21Z
body: @Yoonsoyoung02 
소영님 변경사항 부분은 아닌데용! 그 body에 스크롤이 생기면서 아래처럼 이상하게 동작하고 있습니닷! ( 아마 header와 bottomtab에 fixed가 아니라 sticky를 줘서 그런거 같아요)
그래서 루트레이아웃 body에 아래 처럼 스크롤 안하게만 한번 추가해주시면 좋을 거 같습니다!
![예시](https://github.com/user-attachments/assets/a8773685-75f7-46ae-9687-95ba13396edb)

app/layout.tsx
```tsx
import type { Metadata } from 'next';
import './globals.css';
import { Toaster } from 'sonner';
import { MSWProvider } from '@/components/providers/MSWProvider';

export const metadata: Metadata = {
  title: '구깃',
  description: '필요한 순간만 접어놓다, 내 하루의 구깃',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko">
      // 여기
      <body className="overflow-hidden">
        <Toaster position="top-center" />
        {children}
        <MSWProvider>{children}</MSWProvider>
      </body>
    </html>
  );
}

```
author_association: COLLABORATOR
user: login: Yoonsoyoung02
created_at: 2025-04-14T06:28:00Z
body: @trivialcoding6 
body 부분 변경 완료했습니다 ! 감사합니다 ㅎㅎ
author_association: COLLABORATOR
user: login: Yoonsoyoung02
created_at: 2025-04-14T08:02:58Z
body: 당시 년도를 기준으로 가져오도록 수정했습니다~!
author_association: COLLABORATOR
user: login: trivialcoding6
created_at: 2025-04-14T23:07:31Z
body: LGTM
author_association: COLLABORATOR