Repository: codesquad-members-2025/fe-bye2money
Pull request #22: [Milo]-통계 뷰_도넛 차트 구현 완료(애니메이션 구현 완료)
Author: login: y-minion
Created: 2025-05-04T03:45:52Z | Merged: 2025-05-07T05:48:24Z

<h2><strong>🎯 1. 도넛 차트(DonutChart) 컴포넌트 구현</strong></h2>
<ul>
<li><strong>목표:</strong> 월별 카테고리별 지출 비율을 시각적으로 표현하는 SVG 기반 도넛 차트를 제작.</li>
<li><strong>진행 내용:</strong>
<ul>
<li>DonutChart/index.jsx에서 SVG 원형 기반의 circle 엘리먼트들을 순차적으로 렌더링하여 카테고리별 비율을 시각화.</li>
<li>각 원의 stroke는 strokeDasharray, strokeDashoffset, rotate(-90deg) 등을 통해 원형 퍼센트를 표현.</li>
<li>useTheme + chipColorMap 조합으로 카테고리별 색상을 theme.color.token.chip에서 매핑해 적용.</li>
</ul>
</li>
<li><strong>해결한 문제:</strong>
<ul>
<li><strong>문제:</strong> 전체 퍼센트 누계가 100%를 넘어 원이 잘못 그려지거나, strokeDasharray의 gap이 음수가 되는 경우 발생.</li>
<li><strong>해결:</strong> 누적 퍼센트 cumulativePercentage를 기반으로 dashOffset을 계산하여 정확한 위치 지정.</li>
</ul>
</li>
</ul>
<hr>
<h2><strong>🧱 2. 통계 뷰 데이터 파싱 및 구성</strong></h2>
<ul>
<li><strong>파일 추가:</strong>
<ul>
<li>transactionParser.js: 월별 수입·지출 데이터를 가공하여 도넛 차트용 데이터로 변환하는 유틸리티 모듈 제작.</li>
<li>CategoryStatisticsList/Index.jsx: 통계 화면에서 리스트 형태로 각 카테고리 정보 출력하는 단위 컴포넌트 작성.</li>
</ul>
</li>
<li><strong>포인트:</strong>
<ul>
<li>기존 거래 데이터를 기반으로 카테고리별 금액 합산 → 전체 합계 기준 비율(%) 계산 → 도넛 차트와 리스트 양쪽에서 재활용.</li>
<li>사용된 구조는 아래와 같음:</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">[
  { classification: '식비', amount: 50000, percentage: 25 },
  { classification: '교통', amount: 150000, percentage: 75 },
  ...
]
</code></pre>
<hr>
<h2>3. <strong>🎞️ 도넛 그래프 애니메이션 구현 상세 과정</strong></h2>
<h3><strong>✅ 목적</strong></h3>
<ul>
<li>월이 바뀔 때 도넛 차트가 부드럽게 다시 그려지는 <strong>재생형 애니메이션</strong>을 구현.</li>
</ul>
<h3><strong>🧩 핵심 전략 요약</strong></h3>
<blockquote>
<p>단순 재렌더링이나 컴포넌트 마운트에만 의존하지 않고,</p>
<p><strong>useRef로 transition 제어 상태를 기억하고 조작</strong></p>
</blockquote>
<blockquote>
<p>useState를 통한 offset 업데이트를 타이밍 조절로 제어</p>
</blockquote>
<hr>
<h3><strong>🛠️ 상세 흐름</strong></h3>
<p><strong>1. MaskCircle 컴포넌트 구조 요약</strong></p>
<pre><code class="language-jsx">const [offset, setOffset] = useState(0);
const transFormRef = useRef(true); // 애니메이션 전환 여부 기억

useEffect(() =&gt; {
  transFormRef.current = false;  // 초기화 시 transition 끔
  setOffset(0);                  // offset 초기화 → 도넛 차트가 안 보임

  const timer = setTimeout(() =&gt; {
    transFormRef.current = true;         // transition 켬
    setOffset(-circumference);          // 실제 stroke-dashoffset 설정 (애니메이션 실행)
  }, 100);
  return () =&gt; clearTimeout(timer);
}, [data]); // 데이터가 바뀌는 타이밍에 작동
</code></pre>
<hr>
<h3><strong>🔍 이 방식의 의도</strong></h3>

단계 | 동작 | 의도
-- | -- | --
transFormRef.current = false | 트랜지션 없이 offset 0 설정 | → 즉시 원을 초기 상태로 리셋
setTimeout(() => { transFormRef.current = true }) | 트랜지션 on | → 부드러운 애니메이션 준비
setOffset(-circumference) | strokeDashoffset 적용 | → 애니메이션처럼 보이게 만듦
useEffect 의존성: data | 월별 데이터가 바뀔 때만 실행됨 | → 월 전환 시 애니메이션 재실행 트리거


<hr>
<h3><strong>💡 왜 이 방식이 필요한가?</strong></h3>
<p>이 방식은 <strong>transition 타이밍 자체를 직접 조절</strong>함으로써 예측 가능한 애니메이션 재실행을 만들어냈음.</p>
<hr>
<h3><strong>🧠 정리 요약</strong></h3>
<ul>
<li><strong>기존 offset → 초기화</strong></li>
<li><strong>트랜지션 off → on 전환 제어</strong></li>
<li><strong>strokeDashoffset 변경 타이밍 조절</strong></li>
<li>결과적으로 <strong>“다시 그려지는 듯한” 애니메이션 효과</strong>를 안정적으로 구현함</li>
</ul>
<hr>
<h2><strong>⚙️ 4. 애니메이션 처리 및 개선</strong></h2>
<ul>
<li><strong>문제:</strong> 월을 바꿀 때 도넛 차트가 즉시 재렌더링되어 애니메이션 없이 바뀌는 문제 발생.</li>
<li><strong>개선 내역:</strong>
<ul>
<li>chartMaskHandler.jsx 추가.
<ul>
<li>MaskCircle 컴포넌트를 도입하여 내부의 원형을 덮고 애니메이션 타이밍을 제어.</li>
<li>월 전환 시 key 값을 변화시켜 React가 차트를 다시 마운트하도록 유도.</li>
</ul>
</li>
<li><strong>애니메이션 흐름:</strong> stroke-dasharray와 transition을 함께 사용하여 자연스러운 원 그리기 애니메이션을 구현.</li>
<li>📚 관련위키
<ul>
<li><a href="https://www.notion.so/1e875c9287fa800abc2ad16bd1c81736?pvs=21"><strong>🧠 도넛 그래프 애니메이션 재실행 문제 디버깅</strong></a></li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2><strong>🧠 느낀 점 및 후속 계획</strong></h2>
<ul>
<li>도넛 그래프 구현 자체보다 <strong>비율 계산, 퍼센트 누적 처리, 재렌더링 제어</strong>가 까다로운 부분이었음.</li>
<li>추후에는 소비 추이 그래프를 선그래프로 애니메이션을 사용해  구현 할 예정임.</li>
</ul>
<!-- notionvc: 5af096af-7462-4632-bb65-37c9f1ef2970 -->## **🎯 1. 도넛 차트(DonutChart) 컴포넌트 구현**

- **목표:** 월별 카테고리별 지출 비율을 시각적으로 표현하는 SVG 기반 도넛 차트를 제작.
- **진행 내용:**
    - DonutChart/index.jsx에서 SVG 원형 기반의 circle 엘리먼트들을 순차적으로 렌더링하여 카테고리별 비율을 시각화.
    - 각 원의 stroke는 strokeDasharray, strokeDashoffset, rotate(-90deg) 등을 통해 원형 퍼센트를 표현.
    - useTheme + chipColorMap 조합으로 카테고리별 색상을 theme.color.token.chip에서 매핑해 적용.
- **해결한 문제:**
    - **문제:** 전체 퍼센트 누계가 100%를 넘어 원이 잘못 그려지거나, strokeDasharray의 gap이 음수가 되는 경우 발생.
    - **해결:** 누적 퍼센트 cumulativePercentage를 기반으로 dashOffset을 계산하여 정확한 위치 지정.

---

## **🧱 2. 통계 뷰 데이터 파싱 및 구성**

- **파일 추가:**
    - transactionParser.js: 월별 수입·지출 데이터를 가공하여 도넛 차트용 데이터로 변환하는 유틸리티 모듈 제작.
    - CategoryStatisticsList/Index.jsx: 통계 화면에서 리스트 형태로 각 카테고리 정보 출력하는 단위 컴포넌트 작성.
- **포인트:**
    - 기존 거래 데이터를 기반으로 카테고리별 금액 합산 → 전체 합계 기준 비율(%) 계산 → 도넛 차트와 리스트 양쪽에서 재활용.
    - 사용된 구조는 아래와 같음:

```jsx
[
  { classification: '식비', amount: 50000, percentage: 25 },
  { classification: '교통', amount: 150000, percentage: 75 },
  ...
]
```

---

## 3. **🎞️ 도넛 그래프 애니메이션 구현 상세 과정**

### **✅ 목적**

- 월이 바뀔 때 도넛 차트가 부드럽게 다시 그려지는 **재생형 애니메이션**을 구현.

### **🧩 핵심 전략 요약**

> 단순 재렌더링이나 컴포넌트 마운트에만 의존하지 않고,
> 
> 
> **useRef로 transition 제어 상태를 기억하고 조작**
> 

> useState를 통한 offset 업데이트를 타이밍 조절로 제어
> 

---

### **🛠️ 상세 흐름**

**1. MaskCircle 컴포넌트 구조 요약**

```jsx
const [offset, setOffset] = useState(0);
const transFormRef = useRef(true); // 애니메이션 전환 여부 기억

useEffect(() => {
  transFormRef.current = false;  // 초기화 시 transition 끔
  setOffset(0);                  // offset 초기화 → 도넛 차트가 안 보임

  const timer = setTimeout(() => {
    transFormRef.current = true;         // transition 켬
    setOffset(-circumference);          // 실제 stroke-dashoffset 설정 (애니메이션 실행)
  }, 100);
  return () => clearTimeout(timer);
}, [data]); // 데이터가 바뀌는 타이밍에 작동
```

---

### **🔍 이 방식의 의도**

| **단계** | **동작** | **의도** |
| --- | --- | --- |
| transFormRef.current = false | 트랜지션 없이 offset 0 설정 | → 즉시 원을 초기 상태로 리셋 |
| setTimeout(() => { transFormRef.current = true }) | 트랜지션 on | → 부드러운 애니메이션 준비 |
| setOffset(-circumference) | strokeDashoffset 적용 | → 애니메이션처럼 보이게 만듦 |
| useEffect 의존성: data | 월별 데이터가 바뀔 때만 실행됨 | → 월 전환 시 애니메이션 재실행 트리거 |

---

### **💡 왜 이 방식이 필요한가?**

이 방식은 **transition 타이밍 자체를 직접 조절**함으로써 예측 가능한 애니메이션 재실행을 만들어냈음.

---

### **🧠 정리 요약**

- **기존 offset → 초기화**
- **트랜지션 off → on 전환 제어**
- **strokeDashoffset 변경 타이밍 조절**
- 결과적으로 **“다시 그려지는 듯한” 애니메이션 효과**를 안정적으로 구현함

---

## **⚙️ 4. 애니메이션 처리 및 개선**

- **문제:** 월을 바꿀 때 도넛 차트가 즉시 재렌더링되어 애니메이션 없이 바뀌는 문제 발생.
- **개선 내역:**
    - chartMaskHandler.jsx 추가.
        - MaskCircle 컴포넌트를 도입하여 내부의 원형을 덮고 애니메이션 타이밍을 제어.
        - 월 전환 시 key 값을 변화시켜 React가 차트를 다시 마운트하도록 유도.
    - **애니메이션 흐름:** stroke-dasharray와 transition을 함께 사용하여 자연스러운 원 그리기 애니메이션을 구현.
    - 📚 관련위키
        - [**[🧠 도넛 그래프 애니메이션 재실행 문제 디버깅](https://www.notion.so/1e875c9287fa800abc2ad16bd1c81736?pvs=21)**]

---

## **🧠 느낀 점 및 후속 계획**

- 도넛 그래프 구현 자체보다 **비율 계산, 퍼센트 누적 처리, 재렌더링 제어**가 까다로운 부분이었음.
- 추후에는 소비 추이 그래프를 선그래프로 애니메이션을 사용해  구현 할 예정임.


Diff:
diff --git a/BE/db.json b/BE/db.json
index 33671ac..a602ef9 100644
--- a/BE/db.json
+++ b/BE/db.json
@@ -72,18 +72,6 @@
       "year": 2025,
       "day": 27
     },
-    {
-      "id": "546c0b60-b3d4-42f6-8dfc-a6a2806a43ed",
-      "month": 4,
-      "regDate": "2025.04.02",
-      "currentType": "expense",
-      "amount": 160182,
-      "description": "식비",
-      "method": "이체",
-      "classification": "식비",
-      "year": 2025,
-      "day": 2
-    },
     {
       "id": "02ad489a-a187-4370-8267-281372f2f26a",
       "month": 4,
@@ -120,18 +108,6 @@
       "year": 2025,
       "day": 22
     },
-    {
-      "id": "470d5e89-0e7d-419d-a556-a69a4a67675b",
-      "month": 4,
-      "regDate": "2025.04.13",
-      "currentType": "expense",
-      "amount": 86902,
-      "description": "기타수입",
-      "method": "이체",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 13
-    },
     {
       "id": "7d3460f1-3f0b-4cd5-bb5f-216d0bbebd21",
       "month": 4,
@@ -156,42 +132,6 @@
       "year": 2025,
       "day": 12
     },
-    {
-      "id": "ce84f3be-e4d2-4d51-b4ce-9d48388b007d",
-      "month": 4,
-      "regDate": "2025.04.11",
-      "currentType": "earning",
-      "amount": 304374,
-      "description": "기타수입",
-      "method": "카드",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 11
-    },
-    {
-      "id": "0969b8eb-92fd-46eb-b8b0-112dbc0d3b8c",
-      "month": 4,
-      "regDate": "2025.04.11",
-      "currentType": "earning",
-      "amount": 433960,
-      "description": "월급",
-      "method": "카드",
-      "classification": "월급",
-      "year": 2025,
-      "day": 11
-    },
-    {
-      "id": "e808556a-6407-4876-8310-62fb3247801b",
-      "month": 4,
-      "regDate": "2025.04.15",
-      "currentType": "earning",
-      "amount": 106220,
-      "description": "기타수입",
-      "method": "현금",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 15
-    },
     {
       "id": "a46bf38e-49f2-4767-953c-41c6b20b6623",
       "month": 4,
@@ -204,18 +144,6 @@
       "year": 2025,
       "day": 12
     },
-    {
-      "id": "1d12a990-2f09-440c-a4e8-c0645795a997",
-      "month": 4,
-      "regDate": "2025.04.05",
-      "currentType": "expense",
-      "amount": 202259,
-      "description": "문화/여가",
-      "method": "이체",
-      "classification": "문화/여가",
-      "year": 2025,
-      "day": 5
-    },
     {
       "id": "a39e71df-bd3e-4c4c-879d-eda77ce371dc",
       "month": 4,
@@ -228,50 +156,14 @@
       "year": 2025,
       "day": 21
     },
-    {
-      "id": "7fb29401-c450-4cf1-bda9-9dd55204ef9a",
-      "month": 4,
-      "regDate": "2025.04.04",
-      "currentType": "earning",
-      "amount": 70362,
-      "description": "기타수입",
-      "method": "카드",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 4
-    },
-    {
-      "id": "12ad04da-8c1b-489c-a82f-78e517820e63",
-      "month": 4,
-      "regDate": "2025.04.03",
-      "currentType": "expense",
-      "amount": 87646,
-      "description": "쇼핑/뷰티",
-      "method": "카드",
-      "classification": "쇼핑/뷰티",
-      "year": 2025,
-      "day": 3
-    },
-    {
-      "id": "8849f9b3-dfc3-43fa-a7fa-2412f7e555f3",
-      "month": 4,
-      "regDate": "2025.04.07",
-      "currentType": "expense",
-      "amount": 387525,
-      "description": "교통",
-      "method": "현금",
-      "classification": "교통",
-      "year": 2025,
-      "day": 7
-    },
     {
       "id": "aad648bc-741d-4544-a9c7-07e13c8e557b",
       "month": 4,
       "regDate": "2025.04.04",
       "currentType": "expense",
       "amount": 112244,
-      "description": "쇼핑/뷰티",
-      "method": "이체",
+      "description": "쇼핑/뷰티2222",
+      "method": "카카오페이",
       "classification": "쇼핑/뷰티",
       "year": 2025,
       "day": 4
@@ -313,28 +205,16 @@
       "day": 26
     },
     {
-      "id": "af001959-a196-47d2-a624-730aaebbc59b",
+      "id": "a7e666df-7c05-4467-984e-c95dd2923fa7",
       "month": 4,
-      "regDate": "2025.04.08",
-      "currentType": "expense",
-      "amount": 113185,
+      "regDate": "2025.04.05",
+      "currentType": "earning",
+      "amount": 161561,
       "description": "기타수입",
-      "method": "이체",
+      "method": "카드",
       "classification": "기타수입",
       "year": 2025,
-      "day": 8
-    },
-    {
-      "id": "f11d0d4b-d040-46d4-81b3-16d771f8a2ef",
-      "month": 4,
-      "regDate": "2025.04.02",
-      "currentType": "expense",
-      "amount": 277932,
-      "description": "문화/여가",
-      "method": "이체",
-      "classification": "문화/여가",
-      "year": 2025,
-      "day": 2
+      "day": 5
     },
     {
       "id": "ae01bd0a-5c3e-400c-8a72-6d89bed48c4a",
@@ -576,18 +456,6 @@
       "year": 2025,
       "day": 10
     },
-    {
-      "id": "b8ba28dd-ea8d-4521-9929-8ffaa9107b96",
-      "month": 4,
-      "regDate": "2025.04.29",
-      "currentType": "expense",
-      "amount": 438402,
-      "description": "기타수입",
-      "method": "현금",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 29
-    },
     {
       "id": "f539b9db-565e-488d-a2ae-0092d1c18cfa",
       "month": 4,
@@ -600,30 +468,6 @@
       "year": 2025,
       "day": 8
     },
-    {
-      "id": "95d7a65b-da42-48f5-b630-4a8b647d55dd",
-      "month": 4,
-      "regDate": "2025.04.03",
-      "currentType": "earning",
-      "amount": 202387,
-      "description": "용돈",
-      "method": "카드",
-      "classification": "용돈",
-      "year": 2025,
-      "day": 3
-    },
-    {
-      "id": "1720a5e9-8766-49b8-99fd-eb5f807a8a19",
-      "month": 4,
-      "regDate": "2025.04.05",
-      "currentType": "expense",
-      "amount": 476306,
-      "description": "기타수입",
-      "method": "현금",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 5
-    },
     {
       "id": "c4f89c7e-f288-4ffc-b637-560e2536e2ee",
       "month": 4,
@@ -648,18 +492,6 @@
       "year": 2025,
       "day": 10
     },
-    {
-      "id": "fe7d3b63-1b64-49fa-9434-35dcb36dbb55",
-      "month": 4,
-      "regDate": "2025.04.04",
-      "currentType": "earning",
-      "amount": 110844,
-      "description": "용돈",
-      "method": "카드",
-      "classification": "용돈",
-      "year": 2025,
-      "day": 4
-    },
     {
       "id": "71038716-7260-422a-85e1-a233624a6486",
       "month": 4,
@@ -684,18 +516,6 @@
       "year": 2025,
       "day": 26
     },
-    {
-      "id": "572714b6-97f6-45f4-a68d-1e37ba045cf2",
-      "month": 4,
-      "regDate": "2025.04.06",
-      "currentType": "earning",
-      "amount": 284900,
-      "description": "용돈",
-      "method": "카드",
-      "classification": "용돈",
-      "year": 2025,
-      "day": 6
-    },
     {
       "id": "480b0dd3-1c35-4c69-a9dd-a6b15533b755",
       "month": 4,
@@ -720,18 +540,6 @@
       "year": 2025,
       "day": 5
     },
-    {
-      "id": "0e8a27c3-b6c8-4921-ad96-1fde4b1dbd16",
-      "month": 4,
-      "regDate": "2025.04.27",
-      "currentType": "expense",
-      "amount": 216405,
-      "description": "기타수입",
-      "method": "이체",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 27
-    },
     {
       "id": "49898620-ee01-4965-b467-fdbf642223ad",
       "month": 4,
@@ -840,18 +648,6 @@
       "year": 2025,
       "day": 24
     },
-    {
-      "id": "77794b3b-82c0-415f-8d94-fb43320551e1",
-      "month": 4,
-      "regDate": "2025.04.06",
-      "currentType": "earning",
-      "amount": 114868,
-      "description": "용돈",
-      "method": "현금",
-      "classification": "용돈",
-      "year": 2025,
-      "day": 6
-    },
     {
       "id": "f944c6d1-b7d3-4d8b-ad03-eb6ffa97141c",
       "month": 4,
@@ -900,18 +696,6 @@
       "year": 2025,
       "day": 12
     },
-    {
-      "id": "9fd76d06-602c-4dbc-acf5-117ee2a4010c",
-      "month": 4,
-      "regDate": "2025.04.11",
-      "currentType": "expense",
-      "amount": 375135,
-      "description": "기타수입",
-      "method": "이체",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 11
-    },
     {
       "id": "c4ce55ec-8964-487e-9cc2-56aacd73de2c",
       "month": 4,
@@ -984,18 +768,6 @@
       "year": 2025,
       "day": 26
     },
-    {
-      "id": "c995db7d-b24b-40ec-a72a-4b8fec7c77dc",
-      "month": 4,
-      "regDate": "2025.04.19",
-      "currentType": "expense",
-      "amount": 315248,
-      "description": "기타수입",
-      "method": "카드",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 19
-    },
     {
       "id": "df6d660a-55d2-44d0-860c-99f5eabcfc82",
       "month": 4,
@@ -1158,36 +930,12 @@
       "regDate": "2025.03.03",
       "currentType": "expense",
       "amount": 22000,
-      "description": "히히히히 맛있다!",
+      "description": "식비",
       "method": "현금",
       "classification": "식비",
       "year": 2025,
       "day": 3
     },
-    {
-      "id": "d4f2e8b2-9d9c-4a7e-bb5c-1e2e3f4a5b02",
-      "month": 3,
-      "regDate": "2025.03.07",
-      "currentType": "earning",
-      "amount": 150000,
-      "description": "용돈",
-      "method": "이체",
-      "classification": "용돈",
-      "year": 2025,
-      "day": 7
-    },
-    {
-      "id": "e3b7c6d5-8c6a-4b6d-9b3e-5e4f7a4c3b03",
-      "month": 3,
-      "regDate": "2025.03.15",
-      "currentType": "expense",
-      "amount": 43000,
-      "description": "교통(수정)",
-      "method": "카드",
-      "classification": "교통",
-      "year": 2025,
-      "day": 15
-    },
     {
       "id": "a8d9e1f3-6b3d-4a4a-9d1e-8b3f7e6a4c04",
       "month": 3,
@@ -1200,113 +948,149 @@
       "year": 2025,
       "day": 21
     },
-    {
-      "id": "b2a3d4e5-7c6f-4b5d-8e2a-0a1b2c3d4e05",
-      "month": 5,
-      "regDate": "2025.05.03",
-      "currentType": "expense",
-      "amount": 17000,
-      "description": "똥응가히ㄴ히 냠냠\n사실 먹기 싫어!!! 아닌가..?",
-      "method": "토스페이",
-      "classification": "기타수입",
-      "year": 2025,
-      "day": 3
-    },
     {
       "id": "c5d6e7f8-1a2b-4c3d-9e5f-0b1c2d3e4f06",
       "month": 5,
       "regDate": "2025.05.10",
-      "currentType": "expense",
+      "currentType": "earning",
       "amount": 120000,
-      "description": "방구 히히",
-      "method": "카카오페이",
-      "classification": "교통",
+      "description": "커피ㅇㅇㄹㅇ",
+      "method": "금",
+      "classification": "월급",
       "year": 2025,
       "day": 10
     },
     {
-      "id": "d7e8f9a1-2b3c-4d5e-8f6a-1b2c3d4e5f07",
-      "month": 5,
-      "regDate": "2025.05.15",
-      "currentType": "expense",
-      "amount": 65000,
-      "description": "쇼핑/뷰티",
-      "method": "카드",
-      "classification": "쇼핑/뷰티",
+      "id": "b5d656a0-b299-435d-9eaf-75a6aaa75c3e",
       "year": 2025,
-      "day": 15
+      "month": 4,
+      "day": 28,
+      "regDate": "2025.04.28",
+      "currentType": "expense",
+      "amount": 1230,
+      "description": "ㅎㅇ",
+      "method": "토스페이",
+      "classification": "문화/여가"
     },
     {
-      "id": "e9f1a2b3-4c5d-6e7f-8a9b-2c3d4e5f6a08",
+      "id": "26bd32e2-cfa5-4b82-a7c6-64befedeeccc",
+      "year": 2025,
       "month": 5,
-      "regDate": "2025.05.22",
-      "currentType": "earning",
-      "amount": 320000,
-      "description": "월급",
-      "method": "현금",
-      "classification": "월급",
+      "day": 9,
+      "regDate": "2025.05.09",
+      "currentType": "expense",
+      "amount": 100000,
+      "description": "아뱌뱌 우땨땨~!히히똥똥똥",
+      "method": "금",
+      "classification": "월급"
+    },
+    {
+      "id": "53a82a1d-1b98-40f2-bc5b-1b4c78a6c89d",
       "year": 2025,
-      "day": 22
+      "month": 4,
+      "day": 28,
+      "regDate": "2025.04.28",
+      "currentType": "earning",
+      "amount": 220,
+      "description": "ㅈㅈ",
+      "method": "ㅈㅈㅈ",
+      "classification": "월급"
     },
     {
-      "id": "fa1b2c3d-4e5f-6a7b-8c9d-3e4f5a6b7c09",
-      "month": 5,
-      "regDate": "2025.05.27",
+      "id": "cd42f0da-2bb1-42e0-b6e4-d22b7e03c040",
+      "year": 1414,
+      "month": 12,
+      "day": 21,
+      "regDate": "1414.12.21",
       "currentType": "expense",
-      "amount": 90000,
-      "description": "문화/여가",
-      "method": "이체",
-      "classification": "문화/여가",
+      "amount": 1.2412412412412414e+42,
+      "description": "42144444444444444444444444444444",
+      "method": "토스페이",
+      "classification": "식비"
+    },
+    {
+      "id": "83e60045-54b6-4cd8-98ec-b43eda88b603",
       "year": 2025,
-      "day": 27
+      "month": 4,
+      "day": 29,
+      "regDate": "2025.04.29",
+      "currentType": "earning",
+      "amount": 100000,
+      "description": "똥이 참 멋지네요",
+      "method": "똥",
+      "classification": "미분류"
     },
     {
-      "id": "b5d656a0-b299-435d-9eaf-75a6aaa75c3e",
+      "id": "f369c040-c709-405d-a455-61557d0d7f9d",
+      "year": 2025,
+      "month": 5,
+      "day": 9,
+      "regDate": "2025.05.09",
+      "currentType": "earning",
+      "amount": 100000,
+      "description": "똥이 참 멋지네요",
+      "method": "똥",
+      "classification": "기타 수입"
+    },
+    {
+      "id": "db1cf593-6c91-4049-baba-f2983133750b",
       "year": 2025,
       "month": 4,
-      "day": 28,
-      "regDate": "2025.04.28",
+      "day": 29,
+      "regDate": "2025.04.29",
       "currentType": "expense",
       "amount": 1230,
-      "description": "ㅎㅇ",
-      "method": "토스페이",
-      "classification": "문화/여가"
+      "description": "123",
+      "method": "똥",
+      "classification": "미분류"
     },
     {
-      "id": "a7bb599a-9db3-4195-951f-c930d5df43d1",
+      "id": "6d258434-cede-4403-a794-92b19ca196df",
       "year": 2025,
       "month": 6,
-      "day": 1,
-      "regDate": "2025.06.01",
+      "day": 11,
+      "regDate": "2025.06.11",
       "currentType": "expense",
       "amount": 10000,
-      "description": "프로틴",
-      "method": "감자10개",
+      "description": "트랄라레로트랄랄레",
+      "method": "토스페이",
+      "classification": "생활"
+    },
+    {
+      "id": "e903770f-4956-48b3-853b-457d2394b547",
+      "year": 2025,
+      "month": 6,
+      "day": 12,
+      "regDate": "2025.06.12",
+      "currentType": "expense",
+      "amount": 1230,
+      "description": "3",
+      "method": "토스페이",
       "classification": "쇼핑/뷰티"
     },
     {
-      "id": "62b29d4b-017e-4fdd-86e8-d4ec09490c7b",
-      "year": 2024,
-      "month": 2,
-      "day": 1,
-      "regDate": "2024.02.01",
-      "currentType": "earning",
-      "amount": 1110,
-      "description": "아뱌뱌우땨땨",
-      "method": "히히히히",
-      "classification": "용돈"
+      "id": "4e3c5897-d2bf-4e64-a408-a3c31d87aead",
+      "year": 2025,
+      "month": 6,
+      "day": 20,
+      "regDate": "2025.06.20",
+      "currentType": "expense",
+      "amount": 1230,
+      "description": "리리리라리라",
+      "method": "토스페이",
+      "classification": "의료/건강"
     },
     {
-      "id": "2d84e0cd-a32c-4a91-bec8-80c288725816",
-      "year": 2024,
-      "month": 12,
-      "day": 1,
-      "regDate": "2024.12.01",
+      "id": "173b3314-1f79-4609-9bc8-f69c2cb4fa76",
+      "year": 2025,
+      "month": 5,
+      "day": 11,
+      "regDate": "2025.05.11",
       "currentType": "expense",
-      "amount": 12303,
-      "description": "히히히",
-      "method": "카카오페이",
-      "classification": "교통"
+      "amount": 11101,
+      "description": "gg",
+      "method": "토스페이",
+      "classification": "의료/건강"
     }
   ]
 }
\ No newline at end of file
diff --git a/src/assets/react.svg b/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file
diff --git a/src/base-ui/listItem/CategoryTag/index.jsx b/src/base-ui/listItem/CategoryTag/index.jsx
index 7388329..101b2c4 100644
--- a/src/base-ui/listItem/CategoryTag/index.jsx
+++ b/src/base-ui/listItem/CategoryTag/index.jsx
@@ -1,17 +1,5 @@
 import styled from "styled-components";
-
-const chipColorMap = {
-  생활: 90,
-  "쇼핑/뷰티": 30,
-  "의료/건강": 50,
-  식비: 60,
-  교통: 70,
-  "문화/여가": 100,
-  미분류: 110,
-  월급: 20,
-  기타수입: 10,
-  용돈: 40,
-};
+import chipColorMap from "../../util/chipColorMap";
 
 export default function CategoryTag({ classification }) {
   return <Tag $classification={classification}>{classification}</Tag>;
diff --git a/src/base-ui/stats/DonutChart/chartMaskHandler.jsx b/src/base-ui/stats/DonutChart/chartMaskHandler.jsx
new file mode 100644
index 0000000..7c3d67b
--- /dev/null
+++ b/src/base-ui/stats/DonutChart/chartMaskHandler.jsx
@@ -0,0 +1,47 @@
+import { useState, useEffect, useRef } from "react";
+import { useTheme } from "styled-components";
+import styled from "styled-components";
+
+const StyledCircle = styled.circle`
+  transition: ${({ $transFormRef }) =>
+    $transFormRef ? "stroke-dashoffset 2s ease-out" : "none"};
+  transform: rotate(-90deg);
+  transform-origin: center;
+`;
+
+export function MaskCircle({
+  cx,
+  cy,
+  radius,
+  strokeWidth,
+  circumference,
+  data,
+}) {
+  const [offset, setOffset] = useState(0);
+  const theme = useTheme();
+  const transFormRef = useRef(true);
+
+  useEffect(() => {
+    transFormRef.current = false;
+    setOffset(0);
+    const timer = setTimeout(() => {
+      transFormRef.current = true;
+      setOffset(-circumference);
+    }, 100);
+    return () => clearTimeout(timer);
+  }, [data]);
+
+  return (
+    <StyledCircle
+      cx={cx}
+      cy={cy}
+      r={radius}
+      fill="transparent"
+      stroke={theme.color.token.surface.default}
+      strokeWidth={strokeWidth}
+      strokeDasharray={circumference}
+      strokeDashoffset={offset}
+      $transFormRef={transFormRef.current}
+    />
+  );
+}
diff --git a/src/base-ui/stats/DonutChart/index.jsx b/src/base-ui/stats/DonutChart/index.jsx
new file mode 100644
index 0000000..234ca5a
--- /dev/null
+++ b/src/base-ui/stats/DonutChart/index.jsx
@@ -0,0 +1,55 @@
+import { useTheme } from "styled-components";
+import chipColorMap from "../../util/chipColorMap";
+import { MaskCircle } from "./chartMaskHandler";
+
+export default function DonutChart({
+  data,
+  mainPageState,
+  svgSize = 254,
+  strokeWidth = svgSize * 0.2,
+}) {
+  const theme = useTheme();
+  const circleRadius = (svgSize - strokeWidth) / 2;
+  const circumference = 2 * Math.PI * circleRadius;
+  let cumulativePercentage = 0;
+  return (
+    <svg width={svgSize} height={svgSize}>
+      {data.map(({ classification, percentage }, idx) => {
+        const filled = (percentage / 100) * circumference;
+        const gap = circumference - filled;
+        const dashArray = `${filled}, ${gap}`;
+        const dashOffset = circumference * (1 - cumulativePercentage / 100);
+        if (gap < 0)
+          console.warn("gap is negative!", { classification, percentage });
+        const rotation = -90;
+        const strokeColor =
+          theme.color.token.chip[chipColorMap[classification]];
+        const circle = (
+          <circle
+            key={idx}
+            r={circleRadius}
+            cx={svgSize / 2}
+            cy={svgSize / 2}
+            fill="transparent"
+            stroke={strokeColor}
+            strokeWidth={strokeWidth}
+            strokeDasharray={dashArray}
+            strokeDashoffset={dashOffset}
+            transform={`rotate(${rotation}, ${svgSize / 2}, ${svgSize / 2})`}
+          />
+        );
+
+        cumulativePercentage += percentage;
+        return circle;
+      })}
+      <MaskCircle
+        data={data}
+        cx={svgSize / 2}
+        cy={svgSize / 2}
+        radius={circleRadius}
+        strokeWidth={strokeWidth}
+        circumference={circumference}
+      />
+    </svg>
+  );
+}
diff --git a/src/base-ui/stats/MonthlyAmountHeader/index.jsx b/src/base-ui/stats/MonthlyAmountHeader/index.jsx
new file mode 100644
index 0000000..d1bbc8f
--- /dev/null
+++ b/src/base-ui/stats/MonthlyAmountHeader/index.jsx
@@ -0,0 +1,22 @@
+import styled from "styled-components";
+
+const Container = styled.div`
+  font-family: ${({ theme }) => theme.font.serif};
+  font-size: ${({ theme }) => theme.font.size.sm};
+  font-weight: ${({ theme }) => theme.font.weight.middle};
+  line-height: ${({ theme }) => theme.font.lineHeight.sm};
+  color: ${({ theme }) => theme.color.token.text.default};
+  display: flex;
+  gap: 120px;
+`;
+
+export default function MonthlyAmountHeader({ amount }) {
+  const tag = "이번 달 지출 금액";
+
+  return (
+    <Container>
+      <span>{tag}</span>
+      <span>{amount}</span>
+    </Container>
+  );
+}
diff --git a/src/base-ui/stats/PolylineGraph/Axis.jsx b/src/base-ui/stats/PolylineGraph/Axis.jsx
new file mode 100644
index 0000000..965cf7f
--- /dev/null
+++ b/src/base-ui/stats/PolylineGraph/Axis.jsx
@@ -0,0 +1,27 @@
+export default function Axis({ width, height, padding }) {
+  const { left, right, top, bottom } = padding;
+
+  return (
+    <>
+      {/* X축 */}
+      <line
+        x1={left}
+        y1={height - bottom}
+        x2={width - right}
+        y2={height - bottom}
+        stroke="#ccc"
+        strokeWidth="1"
+      />
+
+      {/* Y축 */}
+      <line
+        x1={left}
+        y1={height - bottom}
+        x2={left}
+        y2={top}
+        stroke="#ccc"
+        strokeWidth="1"
+      />
+    </>
+  );
+}
diff --git a/src/base-ui/stats/PolylineGraph/GuideLine.jsx b/src/base-ui/stats/PolylineGraph/GuideLine.jsx
new file mode 100644
index 0000000..7680f55
--- /dev/null
+++ b/src/base-ui/stats/PolylineGraph/GuideLine.jsx
@@ -0,0 +1,25 @@
+function GuideLineX({ width, height, padding, count }) {
+  const { left, right, top, bottom } = padding;
+
+  const x1 = left;
+  const x2 = width - right;
+
+  for (let i = 0; i < count; i++) {
+    const y = (height - top - bottom) * (i / count) + top;
+    return <line key={i} x1={x1} y1={y} x2={x2} y2={y} />;
+  }
+}
+
+function GuideLineY({ width, height, padding, count }) {
+  const { left, right, top, bottom } = padding;
+
+  const y1 = top;
+  const y2 = height - bottom;
+
+  for (let i = 0; i < count; i++) {
+    const x = (width - right - left) * ((i + 1) / count) + left;
+    return <line key={i} x1={x} y1={y1} x2={x} y2={y2} />;
+  }
+}
+
+export { GuideLineX, GuideLineY };
diff --git a/src/base-ui/stats/PolylineGraph/Label.jsx b/src/base-ui/stats/PolylineGraph/Label.jsx
new file mode 100644
index 0000000..09985ba
--- /dev/null
+++ b/src/base-ui/stats/PolylineGraph/Label.jsx
@@ -0,0 +1,20 @@
+import getCoordinate from "./getCoordinate";
+import { formatAmount } from "../../../utils/amountChanger";
+
+export default function Label({ data, width, height, padding }) {
+  const max = Math.max(...data.map((d) => d.amount));
+
+  return data.map((d) => {
+    const x = getCoordinate.x(d.month, width, padding);
+    const y = getCoordinate.y(d.amount, max, height, padding);
+
+    return (
+      <g key={d.month}>
+        <circle cx={x} cy={y} r="4" fill="#C04646" />
+        <text x={x} y={y - 10} fontSize="12" textAnchor="middle">
+          {formatAmount(d.amount)}
+        </text>
+      </g>
+    );
+  });
+}
diff --git a/src/base-ui/stats/PolylineGraph/Polyline.jsx b/src/base-ui/stats/PolylineGraph/Polyline.jsx
new file mode 100644
index 0000000..f761e6a
--- /dev/null
+++ b/src/base-ui/stats/PolylineGraph/Polyline.jsx
@@ -0,0 +1,3 @@
+export default function Polyline({ points }) {
+  return <polyline points={points} />;
+}
diff --git a/src/base-ui/stats/PolylineGraph/getCoordinate.js b/src/base-ui/stats/PolylineGraph/getCoordinate.js
new file mode 100644
index 0000000..e883612
--- /dev/null
+++ b/src/base-ui/stats/PolylineGraph/getCoordinate.js
@@ -0,0 +1,26 @@
+const getCoordinate = {
+  points: function (data, width, height, padding) {
+    const max = Math.max(...data.map((d) => d.amount));
+    return data.map(
+      (d) =>
+        `${this.x(d.month, width, padding)},${this.y(
+          d.amount,
+          max,
+          height,
+          padding
+        )}`
+    );
+  },
+  x: function (month, width, padding) {
+    const graphWidth = width - padding.left - padding.right;
+    const gap = graphWidth / 11; // 11칸 (1~12는 11칸)
+    return padding.left + gap * (month - 1);
+  },
+  y: function (amount, max, height, padding) {
+    const graphHeight = height - padding.top - padding.bottom;
+    const ratio = amount / max;
+    return padding.top + (1 - ratio) * graphHeight;
+  },
+};
+
+export default getCoordinate;
diff --git a/src/base-ui/stats/PolylineGraph/index.jsx b/src/base-ui/stats/PolylineGraph/index.jsx
new file mode 100644
index 0000000..07000dd
--- /dev/null
+++ b/src/base-ui/stats/PolylineGraph/index.jsx
@@ -0,0 +1,30 @@
+import Axis from "./Axis";
+import Polyline from "./Polyline";
+import getCoordinate from "./getCoordinate";
+import Label from "./Label";
+import { GuideLineX, GuideLineY } from "./GuideLine";
+
+/*
+const data = [
+  { month: '2024-01', amount: 120000 },
+  { month: '2024-02', amount: 150000 },
+  { month: '2024-03', amount: 100000 },
+  ...
+];
+*/
+export default function PolygonGraph({ data }) {
+  const width = 750;
+  const height = 300;
+  const padding = { left: 0, right: 0, top: 0, bottom: 0 };
+  const points = getCoordinate.points(data, width, height, padding);
+
+  return (
+    <svg width={width} height={height}>
+      <Axis width={width} height={height} padding={padding} />
+      <GuideLineX width={width} height={height} padding={padding} count={10} />
+      <GuideLineY width={width} height={height} padding={padding} count={10} />
+      <Polyline points={points.join(" ")} />
+      <Label data={data} width={width} height={height} padding={padding} />
+    </svg>
+  );
+}
diff --git a/src/base-ui/stats/StatsTableItem/index.jsx b/src/base-ui/stats/StatsTableItem/index.jsx
new file mode 100644
index 0000000..03fdf87
--- /dev/null
+++ b/src/base-ui/stats/StatsTableItem/index.jsx
@@ -0,0 +1,50 @@
+import styled from "styled-components";
+import CategoryTag from "../../listItem/CategoryTag";
+import { formatAmount } from "../../../utils/amountChanger";
+import { useSearchParams } from "react-router-dom";
+
+const Container = styled.div`
+  display: flex;
+  width: 100%;
+  padding-right: 16px;
+  gap: 16px;
+  align-items: center;
+`;
+const Body = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: ${({ theme }) => theme.font.size.sm};
+  font-weight: ${({ theme }) => theme.font.weight.light};
+  line-height: 24px;
+  font-family: ${({ theme }) => theme.font.sans};
+  color: ${({ theme }) => theme.color.token.text.default};
+  text-align: center;
+`;
+
+export default function StatsTableItem({ classification, percentage, amount }) {
+  const percentageUnit = "%";
+  const amountUnit = "원";
+  const [searchParams, setSearchParams] = useSearchParams();
+
+  function paramsHandler(category) {
+    const currentParams = Object.fromEntries(searchParams.entries());
+    setSearchParams({ ...currentParams, category });
+  }
+  return (
+    <Container onClick={() => paramsHandler(classification)}>
+      <CategoryTag classification={classification} />
+      <Body>
+        <span>
+          {percentage}
+          {percentageUnit}
+        </span>
+        <span>
+          {formatAmount(amount)}
+          {amountUnit}
+        </span>
+      </Body>
+    </Container>
+  );
+}
diff --git a/src/base-ui/util/chipColorMap.js b/src/base-ui/util/chipColorMap.js
new file mode 100644
index 0000000..8c874da
--- /dev/null
+++ b/src/base-ui/util/chipColorMap.js
@@ -0,0 +1,14 @@
+const chipColorMap = {
+  생활: 90,
+  "쇼핑/뷰티": 30,
+  "의료/건강": 50,
+  식비: 60,
+  교통: 70,
+  "문화/여가": 100,
+  미분류: 110,
+  월급: 20,
+  기타수입: 10,
+  용돈: 40,
+};
+
+export default chipColorMap;
diff --git a/src/styles/theme.js b/src/styles/theme.js
index 1f4c172..b39a1df 100644
--- a/src/styles/theme.js
+++ b/src/styles/theme.js
@@ -17,6 +17,7 @@ export const theme = {
     },
     weight: {
       semibold: 600,
+      middle: 400,
       light: 300,
     },
   },
diff --git a/src/units/CategoryStatisticsList/Index.jsx b/src/units/CategoryStatisticsList/Index.jsx
new file mode 100644
index 0000000..51c3e94
--- /dev/null
+++ b/src/units/CategoryStatisticsList/Index.jsx
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+import StatsTableItem from "../../base-ui/stats/StatsTableItem";
+import MonthlyAmountHeader from "../../base-ui/stats/MonthlyAmountHeader";
+
+const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  align-items: center;
+  padding: 24px;
+`;
+
+const TableContainer = styled.div`
+  display: flex;
+  width: 280px;
+  flex-direction: column;
+  border-top: 1px solid ${({ theme }) => theme.color.token.border.default};
+  border-bottom: 1px solid ${({ theme }) => theme.color.token.border.default};
+`;
+export default function CategoryStatisticsList({ expense, expenseStatus }) {
+  return (
+    <Container>
+      <MonthlyAmountHeader amount={expense} />
+      <TableContainer>
+        {expenseStatus.map((status, idx) => {
+          return <StatsTableItem key={idx} {...status} />;
+        })}
+      </TableContainer>
+    </Container>
+  );
+}
diff --git a/src/units/PolylineGraphUnit/index.jsx b/src/units/PolylineGraphUnit/index.jsx
new file mode 100644
index 0000000..56d5ac1
--- /dev/null
+++ b/src/units/PolylineGraphUnit/index.jsx
@@ -0,0 +1,76 @@
+// 이 유닛 컴포넌트에서 현재 페이지의 월이 몇월인지, 현재 카테고리는 어떻게 되는지 쿼리 파람을 알아낸다.
+// 쿼리파람으로 찾은 것들을 바탕으로 제이슨 서버에 관련 데이터를 요청한다.
+//  카테고리는 해당하는 카테고리 명의 데이터를 찾는다.
+// 월은 현재 페이지의 월-6 범위만큼 데이터를 요청한다.
+
+import PolygonGraph from "../../base-ui/stats/PolylineGraph";
+import styled from "styled-components";
+import { useSearchParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import Domain from "../../utils/serverDomain";
+
+const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 40px;
+`;
+
+function getLastSixMonthsObjects(year, month) {
+  const result = [];
+  const baseDate = new Date(year, month - 1);
+
+  for (let i = 5; i >= 0; i--) {
+    const date = new Date(baseDate.getFullYear(), baseDate.getMonth() - i);
+    result.push({
+      year: date.getFullYear(),
+      month: date.getMonth() + 1,
+    });
+  }
+
+  return result;
+}
+
+export default function PolylineGraphUnit() {
+  const [polyLineData, setPolyLineData] = useState(null);
+  const [searchParams] = useSearchParams();
+  const currentMonth = searchParams.get("month");
+  const currentYear = searchParams.get("year");
+  const currentCategory = searchParams.get("category");
+  useEffect(() => {
+    async function getData() {
+      const lastSixMonths = getLastSixMonthsObjects(currentYear, currentMonth);
+      const currentType = "expense";
+      const query = lastSixMonths
+        .map((date) => `&year=${date.year}&month=${date.month}`)
+        .join("");
+      try {
+        const res = await fetch(`${Domain.main}`);
+        const raw = await res.json();
+        const filtered = raw.filter(
+          (item) =>
+            item.currentType === currentType &&
+            item.classification === currentCategory &&
+            lastSixMonths.some(
+              ({ year, month }) => item.year === year && item.month === month
+            )
+        );
+        const parsed = lastSixMonths.map(({ year, month }) => {
+          const amount = filtered
+            .filter((item) => item.year === year && item.month === month)
+            .reduce((acc, cur) => acc + Number(cur.amount), 0);
+          return { month, amount };
+        });
+        setPolyLineData(parsed);
+      } catch (err) {
+        console.error(err);
+      }
+    }
+    getData();
+  }, [currentMonth, currentCategory]);
+
+  return (
+    <Container>
+      <PolygonGraph data={polyLineData || []} />
+    </Container>
+  );
+}
diff --git a/src/views/StatisticsView/index.jsx b/src/views/StatisticsView/index.jsx
index ad0d24c..fcb6030 100644
--- a/src/views/StatisticsView/index.jsx
+++ b/src/views/StatisticsView/index.jsx
@@ -1,3 +1,45 @@
+import styled from "styled-components";
+import { useOutletContext, useSearchParams } from "react-router-dom";
+import { useEffect, useReducer } from "react";
+import DonutChart from "../../base-ui/stats/DonutChart";
+import CategoryStatisticsList from "../../units/CategoryStatisticsList/Index";
+import transactionParser from "./transactionParser";
+import moneyReducer from "../../utils/moneyReducer";
+import { parseAmount } from "../../utils/amountChanger";
+import PolylineGraphUnit from "../../units/PolylineGraphUnit";
+
+const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 40px;
+`;
+
+const Wrapper = styled.div`
+  display: flex;
+  gap: 32px;
+  padding: 22px 32px;
+`;
+
+const ChartContainer = styled.div`
+  padding: 110px 84px;
+`;
 export default function StatisticsView() {
-  return <h1>그래프 뷰</h1>;
+  const [searchParams, setSearchParams] = useSearchParams();
+  const { mainPageState } = useOutletContext();
+  const { expense } = moneyReducer(mainPageState);
+  const parsedData = transactionParser(mainPageState, parseAmount(expense));
+
+  const targetCategory = searchParams.get("category");
+
+  return (
+    <Container>
+      <Wrapper>
+        <ChartContainer>
+          <DonutChart data={parsedData} />
+        </ChartContainer>
+        <CategoryStatisticsList expense={expense} expenseStatus={parsedData} />
+      </Wrapper>
+      {targetCategory && <PolylineGraphUnit />}
+    </Container>
+  );
 }
diff --git a/src/views/StatisticsView/transactionParser.js b/src/views/StatisticsView/transactionParser.js
new file mode 100644
index 0000000..e62b82b
--- /dev/null
+++ b/src/views/StatisticsView/transactionParser.js
@@ -0,0 +1,22 @@
+export default function transactionParser(transactions, expense) {
+  const parsedData = transactions.reduce((acc, transaction) => {
+    if (transaction.currentType === "earning") return acc;
+    if (!acc[transaction.classification]) {
+      acc[transaction.classification] = {
+        classification: transaction.classification,
+        percentage: 0,
+        amount: 0,
+      };
+    }
+    acc[transaction.classification].amount += transaction.amount;
+
+    return acc;
+  }, {});
+
+  return Object.values(parsedData).map((category) => {
+    category.percentage = Number(
+      ((category.amount / expense) * 100).toFixed(1)
+    );
+    return category;
+  });
+}


Reviews:
user: login: crongro
body: 차트 구현 멋지군요.
한번 동작하는거 보고 싶군요. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T05:47:53Z

Review comments:
diff_hunk: @@ -0,0 +1,55 @@
+import { useTheme } from "styled-components";
+import chipColorMap from "../../util/chipColorMap";
+import { MaskCircle } from "./chartMaskHandler";
+
+export default function DonutChart({
+  data,
+  mainPageState,
+  svgSize = 254,
+  strokeWidth = svgSize * 0.2,
+}) {
+  const theme = useTheme();
+  const circleRadius = (svgSize - strokeWidth) / 2;
+  const circumference = 2 * Math.PI * circleRadius;
+  let cumulativePercentage = 0;
+  return (
+    <svg width={svgSize} height={svgSize}>
+      {data.map(({ classification, percentage }, idx) => {
+        const filled = (percentage / 100) * circumference;
+        const gap = circumference - filled;
+        const dashArray = `${filled}, ${gap}`;
+        const dashOffset = circumference * (1 - cumulativePercentage / 100);
+        if (gap < 0)
+          console.warn("gap is negative!", { classification, percentage });
+        const rotation = -90;
path: src/base-ui/stats/DonutChart/index.jsx
user: login: crongro
body: 매직넘버들만 지워도 더 좋을듯.
created_at: 2025-05-07T05:43:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+import Axis from "./Axis";
+import Polyline from "./Polyline";
+import getCoordinate from "./getCoordinate";
+import Label from "./Label";
+import { GuideLineX, GuideLineY } from "./GuideLine";
+
+/*
+const data = [
+  { month: '2024-01', amount: 120000 },
+  { month: '2024-02', amount: 150000 },
+  { month: '2024-03', amount: 100000 },
+  ...
+];
+*/
+export default function PolygonGraph({ data }) {
+  const width = 750;
+  const height = 300;
+  const padding = { left: 0, right: 0, top: 0, bottom: 0 };
+  const points = getCoordinate.points(data, width, height, padding);
+
+  return (
+    <svg width={width} height={height}>
+      <Axis width={width} height={height} padding={padding} />
+      <GuideLineX width={width} height={height} padding={padding} count={10} />
+      <GuideLineY width={width} height={height} padding={padding} count={10} />
path: src/base-ui/stats/PolylineGraph/index.jsx
user: login: crongro
body: 연관된 props 라면 하나의 객체로 묶어서 전달하는것도 보기 좋겠어요.
created_at: 2025-05-07T05:46:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,76 @@
+// 이 유닛 컴포넌트에서 현재 페이지의 월이 몇월인지, 현재 카테고리는 어떻게 되는지 쿼리 파람을 알아낸다.
+// 쿼리파람으로 찾은 것들을 바탕으로 제이슨 서버에 관련 데이터를 요청한다.
+//  카테고리는 해당하는 카테고리 명의 데이터를 찾는다.
+// 월은 현재 페이지의 월-6 범위만큼 데이터를 요청한다.
+
+import PolygonGraph from "../../base-ui/stats/PolylineGraph";
+import styled from "styled-components";
+import { useSearchParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import Domain from "../../utils/serverDomain";
+
+const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 40px;
+`;
+
+function getLastSixMonthsObjects(year, month) {
+  const result = [];
+  const baseDate = new Date(year, month - 1);
+
+  for (let i = 5; i >= 0; i--) {
+    const date = new Date(baseDate.getFullYear(), baseDate.getMonth() - i);
+    result.push({
+      year: date.getFullYear(),
+      month: date.getMonth() + 1,
+    });
+  }
+
+  return result;
+}
+
+export default function PolylineGraphUnit() {
+  const [polyLineData, setPolyLineData] = useState(null);
+  const [searchParams] = useSearchParams();
+  const currentMonth = searchParams.get("month");
+  const currentYear = searchParams.get("year");
+  const currentCategory = searchParams.get("category");
+  useEffect(() => {
+    async function getData() {
+      const lastSixMonths = getLastSixMonthsObjects(currentYear, currentMonth);
+      const currentType = "expense";
+      const query = lastSixMonths
+        .map((date) => `&year=${date.year}&month=${date.month}`)
+        .join("");
path: src/units/PolylineGraphUnit/index.jsx
user: login: crongro
body: 이런코드 reduce만 사용해서 바꿔보세요.
created_at: 2025-05-07T05:47:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,76 @@
+// 이 유닛 컴포넌트에서 현재 페이지의 월이 몇월인지, 현재 카테고리는 어떻게 되는지 쿼리 파람을 알아낸다.
+// 쿼리파람으로 찾은 것들을 바탕으로 제이슨 서버에 관련 데이터를 요청한다.
+//  카테고리는 해당하는 카테고리 명의 데이터를 찾는다.
+// 월은 현재 페이지의 월-6 범위만큼 데이터를 요청한다.
+
+import PolygonGraph from "../../base-ui/stats/PolylineGraph";
+import styled from "styled-components";
+import { useSearchParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import Domain from "../../utils/serverDomain";
+
+const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 40px;
+`;
+
+function getLastSixMonthsObjects(year, month) {
+  const result = [];
+  const baseDate = new Date(year, month - 1);
+
+  for (let i = 5; i >= 0; i--) {
+    const date = new Date(baseDate.getFullYear(), baseDate.getMonth() - i);
+    result.push({
+      year: date.getFullYear(),
+      month: date.getMonth() + 1,
+    });
+  }
+
+  return result;
+}
+
+export default function PolylineGraphUnit() {
+  const [polyLineData, setPolyLineData] = useState(null);
+  const [searchParams] = useSearchParams();
+  const currentMonth = searchParams.get("month");
+  const currentYear = searchParams.get("year");
+  const currentCategory = searchParams.get("category");
+  useEffect(() => {
+    async function getData() {
+      const lastSixMonths = getLastSixMonthsObjects(currentYear, currentMonth);
+      const currentType = "expense";
+      const query = lastSixMonths
+        .map((date) => `&year=${date.year}&month=${date.month}`)
+        .join("");
+      try {
+        const res = await fetch(`${Domain.main}`);
+        const raw = await res.json();
+        const filtered = raw.filter(
+          (item) =>
+            item.currentType === currentType &&
+            item.classification === currentCategory &&
+            lastSixMonths.some(
+              ({ year, month }) => item.year === year && item.month === month
+            )
+        );
+        const parsed = lastSixMonths.map(({ year, month }) => {
+          const amount = filtered
+            .filter((item) => item.year === year && item.month === month)
+            .reduce((acc, cur) => acc + Number(cur.amount), 0);
+          return { month, amount };
+        });
+        setPolyLineData(parsed);
+      } catch (err) {
+        console.error(err);
path: src/units/PolylineGraphUnit/index.jsx
user: login: crongro
body: 다음엔 조금더 섬세한 오류처리를 해보세요.
지금은 그저 개발자용 콘솔출력이 다네요. 사용자용 오류처리가 필요하다면? 
created_at: 2025-05-07T05:47:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: crongro
created_at: 2025-05-07T05:38:18Z
body: > const transFormRef = useRef(true); // 애니메이션 전환 여부 기억

usestate로 쓸 수 있으면 그것도 좋은 방법이에요.
author_association: CONTRIBUTOR
user: login: crongro
created_at: 2025-05-07T05:39:16Z
body: 
>   { classification: '식비', amount: 50000, percentage: 25 },
>   { classification: '교통', amount: 150000, percentage: 75 },


데이터 구조를 잘 만들어 두는것이 중요한데. 잘했네요.
author_association: CONTRIBUTOR