Merge remote changes with local improvements

This commit is contained in:
hansoo
2025-03-18 00:42:02 +09:00
10 changed files with 290 additions and 281 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import BudgetCard from '@/components/BudgetCard';
import { categoryIcons } from '@/constants/categoryIcons';
import { formatCurrency } from '@/utils/formatters';
interface BudgetCategoriesSectionProps {
categories: {
@@ -10,23 +11,75 @@ interface BudgetCategoriesSectionProps {
}[];
}
const BudgetCategoriesSection: React.FC<BudgetCategoriesSectionProps> = ({ categories }) => {
const BudgetCategoriesSection: React.FC<BudgetCategoriesSectionProps> = ({
categories
}) => {
return <>
<h2 className="text-lg font-semibold mb-3 mt-8"> </h2>
<div className="neuro-card mb-8">
{categories.map((category, index) => {
// 예산 초과 여부 확인
const isOverBudget = category.current > category.total && category.total > 0;
// 실제 백분율 계산 (초과해도 실제 퍼센트로 표시)
const actualPercentage = category.total > 0
? Math.round((category.current / category.total) * 100)
: 0;
// 프로그레스 바용 퍼센트 - 제한 없이 실제 퍼센트 표시
const displayPercentage = actualPercentage;
// 예산이 얼마 남지 않은 경우 (10% 미만)
const isLowBudget = category.total > 0 && actualPercentage >= 90 && actualPercentage < 100;
// 프로그레스 바 색상 결정
const progressBarColor = isOverBudget
? 'bg-red-500'
: isLowBudget
? 'bg-yellow-400'
: 'bg-neuro-income';
// 남은 예산 또는 초과 예산
const budgetStatusText = isOverBudget
? '예산 초과: '
: '남은 예산: ';
const budgetAmount = isOverBudget
? Math.abs(category.total - category.current)
: Math.max(0, category.total - category.current);
return (
<>
<h2 className="text-lg font-semibold mb-3 mt-8"> </h2>
<div className="grid gap-4 mb-8 desktop-card">
{categories.map((category, index) => (
<BudgetCard
key={index}
title={category.title}
current={category.current}
total={category.total}
color="neuro-income"
/>
))}
<div key={index} className={`${index !== 0 ? 'mt-4 pt-4 border-t border-gray-100' : ''}`}>
<div className="flex items-center gap-2 mb-1">
<div className="text-neuro-income">
{categoryIcons[category.title]}
</div>
<h3 className="text-sm font-medium text-gray-600">{category.title}</h3>
</div>
<div className="flex items-center justify-between mb-2">
<p className="text-lg font-semibold">{formatCurrency(category.current)}</p>
<p className="text-sm text-gray-500">/ {formatCurrency(category.total)}</p>
</div>
<div className="relative h-3 neuro-pressed overflow-hidden">
<div
className={`absolute top-0 left-0 h-full transition-all duration-700 ease-out ${progressBarColor}`}
style={{ width: `${Math.min(displayPercentage, 100)}%` }}
/>
</div>
<div className="mt-2 flex justify-between items-center">
<span className={`text-xs font-medium ${isOverBudget ? 'text-red-500' : 'text-neuro-income'}`}>
{budgetStatusText}{formatCurrency(budgetAmount)}
</span>
<span className="text-xs font-medium text-gray-500">
{displayPercentage}%
</span>
</div>
</div>
</>
);
})}
</div>
</>;
};
export default BudgetCategoriesSection;

View File

@@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { Check, ChevronDown, ChevronUp, Wallet } from 'lucide-react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
interface BudgetGoalProps {
@@ -13,11 +13,13 @@ interface BudgetGoalProps {
monthly: number;
};
onSave: (type: 'daily' | 'weekly' | 'monthly', amount: number) => void;
highlight?: boolean;
}
const BudgetInputCard: React.FC<BudgetGoalProps> = ({
initialBudgets,
onSave
onSave,
highlight = false
}) => {
const [selectedTab, setSelectedTab] = useState<'daily' | 'weekly' | 'monthly'>('daily');
const [budgetInputs, setBudgetInputs] = useState({
@@ -25,7 +27,7 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
weekly: initialBudgets.weekly > 0 ? initialBudgets.weekly.toString() : '',
monthly: initialBudgets.monthly > 0 ? initialBudgets.monthly.toString() : ''
});
const [isOpen, setIsOpen] = useState(false);
const [isOpen, setIsOpen] = useState(highlight);
// Format with commas for display
const formatWithCommas = (amount: string) => {
@@ -87,10 +89,13 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
<Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className="neuro-card"
className={`neuro-card ${highlight ? 'border-2 border-neuro-income shadow-lg' : ''}`}
>
<CollapsibleTrigger className="flex items-center justify-between w-full p-4">
<span className="text-sm font-medium"> </span>
<span className={`text-sm font-medium flex items-center ${highlight ? 'text-neuro-income' : ''}`}>
{highlight && <Wallet size={18} className="mr-2 animate-pulse" />}
</span>
{isOpen ? (
<ChevronUp size={18} className="text-gray-500" />
) : (
@@ -114,7 +119,7 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
placeholder="목표 금액 입력"
className="neuro-pressed"
/>
<Button onClick={handleSave} size="icon" className="neuro-flat text-slate-50 bg-slate-400 hover:bg-slate-300">
<Button onClick={handleSave} size="icon" className={`neuro-flat ${highlight ? 'bg-neuro-income hover:bg-neuro-income/90' : 'bg-slate-400 hover:bg-slate-300'} text-white`}>
<Check size={18} />
</Button>
</div>
@@ -131,7 +136,7 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
placeholder="목표 금액 입력"
className="neuro-pressed"
/>
<Button onClick={handleSave} size="icon" className="neuro-flat bg-slate-400 hover:bg-slate-300">
<Button onClick={handleSave} size="icon" className={`neuro-flat ${highlight ? 'bg-neuro-income hover:bg-neuro-income/90' : 'bg-slate-400 hover:bg-slate-300'} text-white`}>
<Check size={18} />
</Button>
</div>
@@ -148,7 +153,7 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
placeholder="목표 금액 입력"
className="neuro-pressed"
/>
<Button onClick={handleSave} size="icon" className="neuro-flat bg-slate-400 hover:bg-slate-300">
<Button onClick={handleSave} size="icon" className={`neuro-flat ${highlight ? 'bg-neuro-income hover:bg-neuro-income/90' : 'bg-slate-400 hover:bg-slate-300'} text-white`}>
<Check size={18} />
</Button>
</div>

View File

@@ -1,137 +1,151 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { CirclePlus, Save, Check } from 'lucide-react';
import BudgetInputCard from './BudgetInputCard';
import { Button } from '@/components/ui/button';
import { Check, ChevronDown, ChevronUp, Calculator } from 'lucide-react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import BudgetProgress from './BudgetProgress';
import CategoryBudgetInputs from './CategoryBudgetInputs';
import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 함수 사용
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
interface BudgetData {
targetAmount: number;
spentAmount: number;
remainingAmount: number;
}
interface BudgetTabContentProps {
data: BudgetData;
formatCurrency: (amount: number) => string;
calculatePercentage: (spent: number, target: number) => number;
onSaveBudget: (amount: number, categoryBudgets?: Record<string, number>) => void;
}
const BudgetTabContent: React.FC<BudgetTabContentProps> = ({
data,
formatCurrency,
calculatePercentage,
onSaveBudget
}) => {
const percentage = calculatePercentage(data.spentAmount, data.targetAmount);
const [isOpen, setIsOpen] = useState(false);
const [budgetInput, setBudgetInput] = useState(data.targetAmount > 0 ? data.targetAmount.toString() : '');
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
const [showBudgetInput, setShowBudgetInput] = useState(false);
const spentAmount = data.spentAmount;
const targetAmount = data.targetAmount;
// 저장된 카테고리 예산을 불러옵니다
const savedCategoryBudgets = localStorage.getItem('categoryBudgets');
const defaultCategoryAmount = data.targetAmount > 0 ? Math.round(data.targetAmount / EXPENSE_CATEGORIES.length) : 0;
const initialCategoryBudgets = savedCategoryBudgets ? JSON.parse(savedCategoryBudgets) : EXPENSE_CATEGORIES.reduce((acc, category) => {
acc[category] = defaultCategoryAmount;
return acc;
}, {} as Record<string, number>);
// 실제 백분율 계산 (초과해도 실제 퍼센트로 표시)
const actualPercentage = targetAmount > 0 ? Math.round(spentAmount / targetAmount * 100) : 0;
const percentage = actualPercentage;
const isFirstBudget = targetAmount === 0;
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>(initialCategoryBudgets);
// 예산 초과 여부 계산
const isOverBudget = spentAmount > targetAmount;
// 예산이 얼마 남지 않은 경우 (10% 미만)
const isLowBudget = targetAmount > 0 && percentage >= 90 && percentage < 100;
const handleInputChange = (value: string) => {
// Remove all non-numeric characters
const numericValue = value.replace(/[^0-9]/g, '');
setBudgetInput(numericValue);
};
// 프로그레스 바 색상 결정
const progressBarColor = isOverBudget ? 'bg-red-500' : isLowBudget ? 'bg-yellow-400' : 'bg-neuro-income';
// 남은 예산 또는 초과 예산 텍스트 및 금액
const budgetStatusText = isOverBudget ? '예산 초과: ' : '남은 예산: ';
const budgetAmount = isOverBudget ? formatCurrency(Math.abs(targetAmount - spentAmount)) : formatCurrency(Math.max(0, targetAmount - spentAmount));
const handleCategoryInputChange = (value: string, category: string) => {
// Remove all non-numeric characters
const numericValue = value.replace(/[^0-9]/g, '');
const numValue = parseInt(value, 10) || 0;
setCategoryBudgets(prev => ({
...prev,
[category]: parseInt(numericValue) || 0
[category]: numValue
}));
};
const handleSave = () => {
// Calculate total from all categories
const totalAmount = Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0);
// 카테고리별 예산 합계 계산
const calculateTotalBudget = () => {
return Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0);
};
// 버튼 중복 클릭 방지를 위해 비활성화 처리 (setTimeout 사용)
const button = document.querySelector('button[type="button"]') as HTMLButtonElement;
if (button) {
button.disabled = true;
setTimeout(() => {
button.disabled = false;
}, 1000);
// 카테고리 예산 저장
const handleSaveCategoryBudgets = () => {
const totalBudget = calculateTotalBudget();
onSaveBudget(totalBudget, categoryBudgets);
setShowBudgetInput(false);
};
// 기존 카테고리 예산 불러오기
useEffect(() => {
if (showBudgetInput) {
// 로컬 스토리지에서 카테고리 예산 불러오기
try {
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
if (storedCategoryBudgets) {
setCategoryBudgets(JSON.parse(storedCategoryBudgets));
}
} catch (error) {
console.error('카테고리 예산 불러오기 오류:', error);
}
}
}, [showBudgetInput]);
// 카테고리 예산도 함께 전달합니다
onSaveBudget(totalAmount, categoryBudgets);
// 단일 토스트만 표시하고 즉시 패널 닫음
setIsOpen(false);
// 토스트는 이벤트 발생 후 처리되므로 여기서는 호출하지 않음
// 이 함수에서 직접 toast 호출하지 않고 budgetStorage에서 처리되도록 함
// 예산 버튼 클릭 핸들러 - 토글 기능 추가
const toggleBudgetInput = () => {
setShowBudgetInput(prev => !prev);
};
// Format with commas for display
const formatWithCommas = (amount: string) => {
return amount.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
return (
<div className="space-y-4">
<BudgetProgress
spentAmount={data.spentAmount}
targetAmount={data.targetAmount}
percentage={percentage}
formatCurrency={formatCurrency}
/>
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
<span className="text-gray-500 text-sm"> </span>
<span className="font-semibold text-neuro-income">{formatCurrency(data.remainingAmount)}</span>
// 예산 여부에 따른 텍스트 결정
const budgetButtonText = targetAmount > 0 ? "예산 수정하기" : "예산 입력하기";
return <div>
{targetAmount > 0 ? <>
<div className="flex justify-between items-center mb-3">
<div className="text-2xl font-bold">{formatCurrency(spentAmount)}</div>
<div className="text-sm text-gray-500">/ {formatCurrency(targetAmount)}</div>
</div>
<div className="pt-2 border-t border-gray-100">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="flex items-center justify-between w-full px-1 text-left py-[10px]">
<span className="text-sm text-gray-600 font-bold"> </span>
{isOpen ? <ChevronUp size={16} className="text-gray-500" /> : <ChevronDown size={16} className="text-gray-500" />}
</CollapsibleTrigger>
<CollapsibleContent className="pt-2 space-y-3">
<CategoryBudgetInputs
categoryBudgets={categoryBudgets}
handleCategoryInputChange={handleCategoryInputChange}
/>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<div className="flex items-center gap-1">
<Calculator size={16} className="text-gray-500" />
<span className="text-sm font-medium"> :</span>
<div className="w-full h-2 neuro-pressed overflow-hidden mb-3">
<div className={`h-full ${progressBarColor} transition-all duration-700 ease-out`} style={{
width: `${Math.min(percentage, 100)}%`
}} />
</div>
<span className="font-semibold">{formatCurrency(Object.values(categoryBudgets).reduce((sum, value) => sum + value, 0))}</span>
<div className="flex justify-between items-center">
<div className={`text-sm font-medium ${isOverBudget ? 'text-red-500' : 'text-gray-500'}`}>
{budgetStatusText}{budgetAmount}
</div>
<div className="text-sm font-medium text-gray-500">
{percentage}%
</div>
</div>
<div className="mt-6">
<button
onClick={toggleBudgetInput}
className="text-neuro-income hover:underline flex items-center text-lg font-bold group"
>
<CirclePlus size={26} className="mr-2 text-neuro-income animate-pulse transition-transform group-hover:scale-110" />
<span className="text-base font-semibold animate-pulse">{budgetButtonText}</span>
</button>
</div>
</> : <div className="py-4 text-center">
<div className="text-gray-400 mb-4"> </div>
<Button onClick={toggleBudgetInput} variant="default" className="bg-neuro-income hover:bg-neuro-income/90 animate-pulse shadow-lg">
<CirclePlus className="mr-2" size={24} />
<span className="animate-pulse">{budgetButtonText}</span>
</Button>
</div>}
{showBudgetInput && <div className="mt-4">
<div className="neuro-card p-4">
<div>
<h3 className="text-base font-medium mb-3"> </h3>
<p className="text-sm text-gray-500 mb-4"> , , .</p>
<CategoryBudgetInputs categoryBudgets={categoryBudgets} handleCategoryInputChange={handleCategoryInputChange} />
<div className="mt-4 border-t border-gray-300 pt-3">
<div className="flex justify-between items-center mb-4">
<h3 className="font-medium text-sm px-[34px]"> :</h3>
<p className="font-bold text-neuro-income text-base px-[10px]">{formatCurrency(calculateTotalBudget())}</p>
</div>
<div className="flex justify-end">
<Button onClick={handleSave} size="sm" className="neuro-flat text-slate-50 bg-neuro-income hover:bg-neuro-income/90">
<Check size={16} className="mr-1" />
<Button onClick={handleSaveCategoryBudgets} size="sm" className="bg-neuro-income hover:bg-neuro-income/90 text-white mx-[6px]">
<Check size={18} className="mr-1" />
</Button>
</div>
<p className="text-xs text-gray-500 text-center py-[6px]"> , , .</p>
</CollapsibleContent>
</Collapsible>
</div>
</div>
);
</div>
</div>}
</div>;
};
export default BudgetTabContent;

View File

@@ -1,9 +1,8 @@
import React, { useEffect, useRef } from 'react';
import React from 'react';
import { Input } from '@/components/ui/input';
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
import { EXPENSE_CATEGORIES, categoryIcons } from '@/constants/categoryIcons';
import { useIsMobile } from '@/hooks/use-mobile';
import { toast } from '@/components/ui/use-toast';
interface CategoryBudgetInputsProps {
categoryBudgets: Record<string, number>;
@@ -15,7 +14,6 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
handleCategoryInputChange
}) => {
const isMobile = useIsMobile();
const previousBudgetsRef = useRef<Record<string, number>>({});
// Format number with commas for display
const formatWithCommas = (value: number): string => {
@@ -36,50 +34,19 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
}, 300);
};
// 컴포넌트가 마운트될 때 categoryBudgets가 로컬 스토리지에서 다시 로드되도록 이벤트 리스너 설정
useEffect(() => {
const handleStorageChange = () => {
// 부모 컴포넌트에서 데이터가 업데이트되므로 별도 처리 필요 없음
console.log('카테고리 예산 데이터 변경 감지됨');
};
window.addEventListener('categoryBudgetsUpdated', handleStorageChange);
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('categoryBudgetsUpdated', handleStorageChange);
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// 값이 변경될 때마다 토스트 메시지 표시
useEffect(() => {
const hasChanges = Object.keys(categoryBudgets).some(
category => categoryBudgets[category] !== previousBudgetsRef.current[category]
);
const totalBudget = Object.values(categoryBudgets).reduce((sum, val) => sum + val, 0);
const previousTotal = Object.values(previousBudgetsRef.current).reduce((sum, val) => sum + val, 0);
// 이전 값과 다르고, 총 예산이 있는 경우 토스트 표시
if (hasChanges && totalBudget > 0 && totalBudget !== previousTotal) {
// 토스트 메시지는 storage에서 처리
}
// 현재 값을 이전 값으로 업데이트
previousBudgetsRef.current = { ...categoryBudgets };
}, [categoryBudgets]);
return (
<div className="space-y-2 w-full">
<div className="space-y-3 w-full">
{EXPENSE_CATEGORIES.map(category => (
<div key={category} className="flex items-center justify-between w-full">
<label className="text-sm text-gray-600">{category}</label>
<div key={category} className="flex items-center justify-between w-full p-2 rounded-lg">
<div className="flex items-center space-x-2">
<span className="text-neuro-income">{categoryIcons[category]}</span>
<label className="text-sm font-medium text-gray-700">{category}</label>
</div>
<Input
value={formatWithCommas(categoryBudgets[category] || 0)}
onChange={(e) => handleInput(e, category)}
placeholder="예산 입력"
className={`neuro-pressed transition-colors duration-300 ${isMobile ? 'w-[150px]' : 'max-w-[150px]'} text-xs`}
className={`transition-colors duration-300 ${isMobile ? 'w-[150px]' : 'max-w-[150px]'}`}
/>
</div>
))}

View File

@@ -4,6 +4,8 @@ import TransactionEditDialog from './TransactionEditDialog';
import { ChevronRight } from 'lucide-react';
import { useBudget } from '@/contexts/BudgetContext';
import { Link } from 'react-router-dom';
import { categoryIcons } from '@/constants/categoryIcons';
import TransactionIcon from './transaction/TransactionIcon';
interface RecentTransactionsSectionProps {
transactions: Transaction[];
onUpdateTransaction?: (transaction: Transaction) => void;
@@ -36,26 +38,6 @@ const RecentTransactionsSection: React.FC<RecentTransactionsSectionProps> = ({
const formatCurrency = (amount: number) => {
return amount.toLocaleString('ko-KR') + '원';
};
const getCategoryIcon = (category: string) => {
switch (category) {
case '식비':
return '🍽️';
case '교통비':
return '🚗';
case '생활비':
return '🏠';
case '쇼핑':
return '🛍️';
case '의료':
return '💊';
case '여가':
return '🎮';
case '교육':
return '📚';
default:
return '💰';
}
};
return <div className="mt-6 mb-[50px]">
<div className="flex justify-between items-center mb-3">
<h2 className="text-lg font-semibold"> </h2>
@@ -64,11 +46,11 @@ const RecentTransactionsSection: React.FC<RecentTransactionsSectionProps> = ({
</Link>
</div>
<div className="neuro-card divide-y divide-gray-100 w-full">
{transactions.length > 0 ? transactions.map(transaction => <div key={transaction.id} onClick={() => handleTransactionClick(transaction)} className="flex justify-between py-3 px-4 cursor-pointer hover:bg-gray-50">
{transactions.length > 0 ? transactions.map(transaction => <div key={transaction.id} onClick={() => handleTransactionClick(transaction)} className="flex justify-between py-3 cursor-pointer hover:bg-gray-50 px-[5px]">
<div className="flex items-center">
<span className="text-xl mr-3">{getCategoryIcon(transaction.category)}</span>
<div>
<h3 className="font-medium text-black">{transaction.title}</h3>
<TransactionIcon category={transaction.category} />
<div className="ml-3">
<h3 className="font-medium text-black text-left">{transaction.title}</h3>
<p className="text-xs text-gray-500">{transaction.date}</p>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts';
import { formatCurrency } from '@/utils/formatters';
interface MonthlyData {
@@ -47,6 +47,17 @@ const MonthlyComparisonChart: React.FC<MonthlyComparisonChartProps> = ({
monthlyData.length > 0 &&
monthlyData.some(item => (item.budget > 0 || item.expense > 0));
// 지출 색상 결정 함수 추가
const getExpenseColor = (budget: number, expense: number) => {
if (budget === 0) return "#81c784"; // 예산이 0이면 기본 색상
const ratio = expense / budget;
if (ratio > 1) return "#f44336"; // 빨간색 (예산 초과)
if (ratio >= 0.9) return "#ffeb3b"; // 노란색 (예산의 90% 이상)
return "#81c784"; // 기본 초록색
};
return (
<div className="neuro-card h-72 w-full">
{hasValidData ? (
@@ -61,10 +72,21 @@ const MonthlyComparisonChart: React.FC<MonthlyComparisonChartProps> = ({
}}>
<XAxis dataKey="name" />
<YAxis tickFormatter={formatYAxisTick} />
<Tooltip formatter={formatTooltip} />
<Tooltip
formatter={formatTooltip}
contentStyle={{ backgroundColor: 'white', border: 'none' }}
cursor={{ fill: 'transparent' }}
/>
<Legend />
<Bar dataKey="budget" name="예산" fill="#C8C8C9" radius={[4, 4, 0, 0]} />
<Bar dataKey="expense" name="지출" fill="#81c784" radius={[4, 4, 0, 0]} />
<Bar dataKey="expense" name="지출" radius={[4, 4, 0, 0]}>
{monthlyData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={getExpenseColor(entry.budget, entry.expense)}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : (

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Wallet, CreditCard, PiggyBank } from 'lucide-react';
import { Wallet, CreditCard, Coins } from 'lucide-react';
import { formatCurrency } from '@/utils/formatters';
import { useIsMobile } from '@/hooks/use-mobile';
@@ -17,6 +17,10 @@ const SummaryCards: React.FC<SummaryCardsProps> = ({
}) => {
const isMobile = useIsMobile();
// 남은 예산 계산
const remainingBudget = totalBudget - totalExpense;
const isOverBudget = remainingBudget < 0;
return (
<div className={`grid ${isMobile ? 'grid-cols-1' : 'grid-cols-3'} gap-3 mb-8 w-full desktop-card`}>
<div className="neuro-card w-full">
@@ -39,12 +43,18 @@ const SummaryCards: React.FC<SummaryCardsProps> = ({
</div>
<div className="neuro-card w-full">
<div className="flex items-center gap-2 mb-1 py-[5px]">
<PiggyBank size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p>
<Coins size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p>
</div>
<p className="text-sm font-bold text-neuro-income">
{savingsPercentage}%
{isOverBudget ? (
<p className="text-sm font-bold text-red-500">
: {formatCurrency(Math.abs(remainingBudget))}
</p>
) : (
<p className="text-sm font-bold text-neuro-income">
{formatCurrency(remainingBudget)}
</p>
)}
</div>
</div>
);

View File

@@ -13,8 +13,8 @@ const SyncExplanation: React.FC<SyncExplanationProps> = ({ enabled }) => {
return (
<Alert className="bg-amber-50 text-amber-800 border-amber-200">
<AlertCircle className="h-4 w-4" />
<AlertTitle> </AlertTitle>
<AlertDescription className="text-sm">
<AlertTitle className="text-black"> </AlertTitle>
<AlertDescription className="text-sm text-black">
. .
.
</AlertDescription>

View File

@@ -1,18 +1,15 @@
import React from 'react';
interface TransactionDetailsProps {
title: string;
date: string;
}
const TransactionDetails: React.FC<TransactionDetailsProps> = ({ title, date }) => {
return (
<div>
<h3 className="text-sm font-medium">{title || '제목 없음'}</h3>
<p className="text-xs text-gray-500">{date || '날짜 정보 없음'}</p>
</div>
);
const TransactionDetails: React.FC<TransactionDetailsProps> = ({
title,
date
}) => {
return <div>
<h3 className="text-sm font-medium text-left">{title || '제목 없음'}</h3>
<p className="text-xs text-gray-500 text-left">{date || '날짜 정보 없음'}</p>
</div>;
};
export default TransactionDetails;

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import NavBar from '@/components/NavBar';
@@ -8,7 +7,6 @@ import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from '
import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/auth';
import { useToast } from '@/hooks/useToast.wrapper';
const SettingsOption = ({
icon: Icon,
label,
@@ -24,40 +22,35 @@ const SettingsOption = ({
color?: string;
disabled?: boolean;
}) => {
return <div
className={cn(
"neuro-flat p-4 transition-all duration-300 hover:shadow-neuro-convex",
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
)}
onClick={disabled ? undefined : onClick}
>
return <div className={cn("neuro-flat p-4 transition-all duration-300 hover:shadow-neuro-convex", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer")} onClick={disabled ? undefined : onClick}>
<div className="flex items-center">
<div className={cn("neuro-pressed p-3 rounded-full mr-4", color)}>
<Icon size={20} />
</div>
<div className="flex-1">
<h3 className="font-medium">{label}</h3>
{description && <p className="text-xs text-gray-500">{description}</p>}
<h3 className="font-medium text-left">{label}</h3>
{description && <p className="text-xs text-gray-500 text-left">{description}</p>}
</div>
<ChevronRight size={18} className="text-gray-400" />
</div>
</div>;
};
const Settings = () => {
const navigate = useNavigate();
const { user, signOut } = useAuth();
const { toast } = useToast();
const {
user,
signOut
} = useAuth();
const {
toast
} = useToast();
const handleLogout = async () => {
await signOut();
navigate('/login');
};
const handleClick = (path: string) => {
navigate(path);
};
return <div className="min-h-screen bg-neuro-background pb-24">
<div className="max-w-md mx-auto px-6">
{/* Header */}
@@ -66,8 +59,7 @@ const Settings = () => {
{/* User Profile */}
<div className="neuro-flat p-6 mb-8">
{user ? (
<div className="flex items-center">
{user ? <div className="flex items-center">
<div className="neuro-flat p-3 rounded-full mr-4 text-neuro-income">
<User size={24} />
</div>
@@ -79,72 +71,40 @@ const Settings = () => {
{user.email}
</p>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center text-center py-3">
</div> : <div className="flex flex-col items-center justify-center text-center py-3">
<div className="neuro-flat p-3 rounded-full mb-3 text-neuro-income">
<User size={24} />
</div>
<h2 className="font-semibold text-lg"> </h2>
<p className="text-sm text-gray-500"> </p>
</div>
)}
</div>}
</div>
</header>
{/* Data Sync Settings */}
<div className="mb-8">
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2"> </h2>
<SyncSettings />
</div>
{/* Settings Options */}
<div className="space-y-4 mb-8">
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2"></h2>
<SettingsOption
icon={User}
label="프로필 관리"
description="프로필 및 비밀번호 설정"
onClick={() => user ? navigate('/profile') : navigate('/login')}
/>
<SettingsOption
icon={CreditCard}
label="결제 방법"
description="카드 및 은행 계좌 관리"
onClick={() => user ? navigate('/payment-methods') : navigate('/login')}
/>
<SettingsOption
icon={Bell}
label="알림 설정"
description="앱 알림 및 리마인더"
onClick={() => user ? navigate('/notifications') : navigate('/login')}
/>
<SettingsOption icon={User} label="프로필 관리" description="프로필 및 비밀번호 설정" onClick={() => user ? navigate('/profile') : navigate('/login')} />
<SettingsOption icon={CreditCard} label="결제 방법" description="카드 및 은행 계좌 관리" onClick={() => user ? navigate('/payment-methods') : navigate('/login')} />
<SettingsOption icon={Bell} label="알림 설정" description="앱 알림 및 리마인더" onClick={() => user ? navigate('/notifications') : navigate('/login')} />
</div>
<div className="space-y-4 mb-8">
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2"> </h2>
<SettingsOption
icon={Lock}
label="보안 및 개인정보"
description="보안 및 데이터 설정"
<SettingsOption icon={Lock} label="보안 및 개인정보" description="보안 및 데이터 설정"
// 로그인 상태와 관계없이 접근 가능하도록 변경
onClick={() => navigate('/security-privacy')}
/>
<SettingsOption
icon={HelpCircle}
label="도움말 및 지원"
description="FAQ 및 고객 지원"
onClick={() => navigate('/help-support')}
/>
onClick={() => navigate('/security-privacy')} />
<SettingsOption icon={HelpCircle} label="도움말 및 지원" description="FAQ 및 고객 지원" onClick={() => navigate('/help-support')} />
</div>
<div className="mt-8">
<SettingsOption
icon={LogOut}
label={user ? "로그아웃" : "로그인"}
color="text-neuro-expense"
onClick={user ? handleLogout : () => navigate('/login')}
/>
<SettingsOption icon={LogOut} label={user ? "로그아웃" : "로그인"} color="text-neuro-expense" onClick={user ? handleLogout : () => navigate('/login')} />
</div>
<div className="mt-10 border-t border-gray-200 pt-4">
@@ -157,5 +117,4 @@ const Settings = () => {
<NavBar />
</div>;
};
export default Settings;