feat: Add CI/CD pipeline and code quality improvements
- Add GitHub Actions workflow for automated CI/CD - Configure Node.js 18.x and 20.x matrix testing - Add TypeScript type checking step - Add ESLint code quality checks with enhanced rules - Add Prettier formatting verification - Add production build validation - Upload build artifacts for deployment - Set up automated testing on push/PR - Replace console.log with environment-aware logger - Add pre-commit hooks for code quality - Exclude archive folder from linting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,80 +1,87 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import NavBar from '@/components/NavBar';
|
||||
import ExpenseChart from '@/components/ExpenseChart';
|
||||
import AddTransactionButton from '@/components/AddTransactionButton';
|
||||
import { useBudget } from '@/contexts/budget/BudgetContext';
|
||||
import { MONTHS_KR } from '@/hooks/useTransactions';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { getCategoryColor } from '@/utils/categoryColorUtils';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import NavBar from "@/components/NavBar";
|
||||
import ExpenseChart from "@/components/ExpenseChart";
|
||||
import AddTransactionButton from "@/components/AddTransactionButton";
|
||||
import { useBudget } from "@/contexts/budget/BudgetContext";
|
||||
import { MONTHS_KR } from "@/hooks/useTransactions";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { getCategoryColor } from "@/utils/categoryColorUtils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { MonthlyData } from "@/types";
|
||||
|
||||
// 새로 분리한 컴포넌트들 불러오기
|
||||
import PeriodSelector from '@/components/analytics/PeriodSelector';
|
||||
import SummaryCards from '@/components/analytics/SummaryCards';
|
||||
import MonthlyComparisonChart from '@/components/analytics/MonthlyComparisonChart';
|
||||
import CategorySpendingList from '@/components/analytics/CategorySpendingList';
|
||||
import PaymentMethodChart from '@/components/analytics/PaymentMethodChart';
|
||||
import PeriodSelector from "@/components/analytics/PeriodSelector";
|
||||
import SummaryCards from "@/components/analytics/SummaryCards";
|
||||
import MonthlyComparisonChart from "@/components/analytics/MonthlyComparisonChart";
|
||||
import CategorySpendingList from "@/components/analytics/CategorySpendingList";
|
||||
import PaymentMethodChart from "@/components/analytics/PaymentMethodChart";
|
||||
|
||||
const Analytics = () => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('이번 달');
|
||||
const [selectedPeriod, setSelectedPeriod] = useState("이번 달");
|
||||
const {
|
||||
budgetData,
|
||||
getCategorySpending,
|
||||
getPaymentMethodStats,
|
||||
// 새로 추가된 메서드
|
||||
transactions
|
||||
transactions,
|
||||
} = useBudget();
|
||||
const isMobile = useIsMobile();
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
const [monthlyData, setMonthlyData] = useState<any[]>([]);
|
||||
const [monthlyData, setMonthlyData] = useState<MonthlyData[]>([]);
|
||||
|
||||
// 페이지 가시성 변경시 데이터 새로고침
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log('분석 페이지 보임 - 데이터 새로고침');
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
if (document.visibilityState === "visible") {
|
||||
logger.info("분석 페이지 보임 - 데이터 새로고침");
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
|
||||
// 이벤트 발생시켜 데이터 새로고침
|
||||
try {
|
||||
window.dispatchEvent(new Event('storage'));
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
window.dispatchEvent(new Event("storage"));
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
window.dispatchEvent(new Event("budgetDataUpdated"));
|
||||
window.dispatchEvent(new Event("categoryBudgetsUpdated"));
|
||||
} catch (e) {
|
||||
console.error('이벤트 발생 오류:', e);
|
||||
logger.error("이벤트 발생 오류:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleFocus = () => {
|
||||
console.log('분석 페이지 포커스 - 데이터 새로고침');
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
logger.info("분석 페이지 포커스 - 데이터 새로고침");
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
|
||||
// 이벤트 발생시켜 데이터 새로고침
|
||||
try {
|
||||
window.dispatchEvent(new Event('storage'));
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
window.dispatchEvent(new Event("storage"));
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
window.dispatchEvent(new Event("budgetDataUpdated"));
|
||||
window.dispatchEvent(new Event("categoryBudgetsUpdated"));
|
||||
} catch (e) {
|
||||
console.error('이벤트 발생 오류:', e);
|
||||
logger.error("이벤트 발생 오류:", e);
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.addEventListener('focus', handleFocus);
|
||||
window.addEventListener('transactionUpdated', () => setRefreshTrigger(prev => prev + 1));
|
||||
window.addEventListener('budgetDataUpdated', () => setRefreshTrigger(prev => prev + 1));
|
||||
window.addEventListener('categoryBudgetsUpdated', () => setRefreshTrigger(prev => prev + 1));
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
window.addEventListener("focus", handleFocus);
|
||||
window.addEventListener("transactionUpdated", () =>
|
||||
setRefreshTrigger((prev) => prev + 1)
|
||||
);
|
||||
window.addEventListener("budgetDataUpdated", () =>
|
||||
setRefreshTrigger((prev) => prev + 1)
|
||||
);
|
||||
window.addEventListener("categoryBudgetsUpdated", () =>
|
||||
setRefreshTrigger((prev) => prev + 1)
|
||||
);
|
||||
|
||||
// 컴포넌트 마운트 시 초기 데이터 로드 이벤트 트리거
|
||||
handleFocus();
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
window.removeEventListener('transactionUpdated', () => {});
|
||||
window.removeEventListener('budgetDataUpdated', () => {});
|
||||
window.removeEventListener('categoryBudgetsUpdated', () => {});
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
window.removeEventListener("transactionUpdated", () => {});
|
||||
window.removeEventListener("budgetDataUpdated", () => {});
|
||||
window.removeEventListener("categoryBudgetsUpdated", () => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -82,25 +89,26 @@ const Analytics = () => {
|
||||
const totalBudget = budgetData?.monthly?.targetAmount || 0;
|
||||
const totalExpense = budgetData?.monthly?.spentAmount || 0;
|
||||
const savings = Math.max(0, totalBudget - totalExpense);
|
||||
const savingsPercentage = totalBudget > 0 ? Math.round(savings / totalBudget * 100) : 0;
|
||||
const savingsPercentage =
|
||||
totalBudget > 0 ? Math.round((savings / totalBudget) * 100) : 0;
|
||||
|
||||
// 카테고리별 지출 차트 데이터 생성 - 색상 유틸리티 사용
|
||||
const categorySpending = getCategorySpending();
|
||||
const expenseData = categorySpending.map(category => ({
|
||||
const expenseData = categorySpending.map((category) => ({
|
||||
name: category.title,
|
||||
value: category.current,
|
||||
color: getCategoryColor(category.title) // 일관된 색상 적용
|
||||
color: getCategoryColor(category.title), // 일관된 색상 적용
|
||||
}));
|
||||
|
||||
// 결제 방법 데이터 가져오기
|
||||
const paymentMethodData = getPaymentMethodStats();
|
||||
const hasPaymentData = paymentMethodData.some(method => method.amount > 0);
|
||||
const hasPaymentData = paymentMethodData.some((method) => method.amount > 0);
|
||||
|
||||
// 월별 데이터 생성 - 샘플 데이터 제거하고 현재 달만 실제 데이터 사용
|
||||
useEffect(() => {
|
||||
console.log('Analytics 페이지: 월별 데이터 생성', {
|
||||
logger.info("Analytics 페이지: 월별 데이터 생성", {
|
||||
totalBudget,
|
||||
totalExpense
|
||||
totalExpense,
|
||||
});
|
||||
|
||||
// 현재 월 가져오기
|
||||
@@ -108,56 +116,70 @@ const Analytics = () => {
|
||||
const currentMonth = today.getMonth();
|
||||
|
||||
// 현재 달만 실제 데이터 사용하는 배열 생성
|
||||
const monthlyDataArray = [{
|
||||
name: MONTHS_KR[currentMonth].split(' ')[0],
|
||||
// '8월' 형식으로 변환
|
||||
budget: totalBudget,
|
||||
expense: totalExpense
|
||||
}];
|
||||
const monthlyDataArray = [
|
||||
{
|
||||
name: MONTHS_KR[currentMonth].split(" ")[0],
|
||||
// '8월' 형식으로 변환
|
||||
budget: totalBudget,
|
||||
expense: totalExpense,
|
||||
},
|
||||
];
|
||||
setMonthlyData(monthlyDataArray);
|
||||
console.log('Analytics 페이지: 월별 데이터 생성 완료', monthlyDataArray);
|
||||
logger.info("Analytics 페이지: 월별 데이터 생성 완료", monthlyDataArray);
|
||||
}, [totalBudget, totalExpense, refreshTrigger]);
|
||||
|
||||
// 이전/다음 기간 이동 처리
|
||||
const handlePrevPeriod = () => {
|
||||
console.log('이전 기간으로 이동');
|
||||
logger.info("이전 기간으로 이동");
|
||||
};
|
||||
const handleNextPeriod = () => {
|
||||
console.log('다음 기간으로 이동');
|
||||
logger.info("다음 기간으로 이동");
|
||||
};
|
||||
|
||||
return <div className="min-h-screen bg-neuro-background pb-24">
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neuro-background pb-24">
|
||||
<div className="max-w-md mx-auto px-6">
|
||||
{/* Header */}
|
||||
<header className="py-4 w-full">
|
||||
<h1 className="font-bold neuro-text mb-3 text-xl">지출 분석</h1>
|
||||
|
||||
|
||||
{/* Period Selector */}
|
||||
<PeriodSelector selectedPeriod={selectedPeriod} onPrevPeriod={handlePrevPeriod} onNextPeriod={handleNextPeriod} />
|
||||
|
||||
<PeriodSelector
|
||||
selectedPeriod={selectedPeriod}
|
||||
onPrevPeriod={handlePrevPeriod}
|
||||
onNextPeriod={handleNextPeriod}
|
||||
/>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<SummaryCards totalBudget={totalBudget} totalExpense={totalExpense} savingsPercentage={savingsPercentage} />
|
||||
<SummaryCards
|
||||
totalBudget={totalBudget}
|
||||
totalExpense={totalExpense}
|
||||
savingsPercentage={savingsPercentage}
|
||||
/>
|
||||
</header>
|
||||
|
||||
|
||||
{/* Monthly Comparison Chart */}
|
||||
<div className="mb-8 w-full">
|
||||
<h2 className="text-lg font-semibold mb-3">월별 그래프</h2>
|
||||
<MonthlyComparisonChart monthlyData={monthlyData} isEmpty={totalBudget === 0 && totalExpense === 0} />
|
||||
<MonthlyComparisonChart
|
||||
monthlyData={monthlyData}
|
||||
isEmpty={totalBudget === 0 && totalExpense === 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 카테고리 비율과 지출을 하나의 카드로 합침 */}
|
||||
<h2 className="text-lg font-semibold mb-3">카테고리 비율</h2>
|
||||
<div className="neuro-card w-full mb-8">
|
||||
<div className="w-full">
|
||||
{expenseData.some(item => item.value > 0) ? (
|
||||
{expenseData.some((item) => item.value > 0) ? (
|
||||
<>
|
||||
<div className="h-72 flex items-center justify-center">
|
||||
<ExpenseChart data={expenseData} />
|
||||
</div>
|
||||
{/* 원그래프 아래에 카테고리 지출 목록 추가 */}
|
||||
<CategorySpendingList
|
||||
categories={categorySpending}
|
||||
totalExpense={totalExpense}
|
||||
<CategorySpendingList
|
||||
categories={categorySpending}
|
||||
totalExpense={totalExpense}
|
||||
showCard={false} // 카드 감싸지 않도록 설정
|
||||
/>
|
||||
</>
|
||||
@@ -168,18 +190,22 @@ const Analytics = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 결제 방법 차트 추가 */}
|
||||
<h2 className="text-lg font-semibold mb-3">결제 방법 비율</h2>
|
||||
<PaymentMethodChart data={paymentMethodData} isEmpty={!hasPaymentData} />
|
||||
|
||||
<PaymentMethodChart
|
||||
data={paymentMethodData}
|
||||
isEmpty={!hasPaymentData}
|
||||
/>
|
||||
|
||||
{/* 결제 방법 차트 아래 80px 여유 공간 추가 */}
|
||||
<div className="h-20"></div>
|
||||
</div>
|
||||
|
||||
<AddTransactionButton />
|
||||
<NavBar />
|
||||
</div>;
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics;
|
||||
|
||||
Reference in New Issue
Block a user