diff --git a/src/App.tsx b/src/App.tsx index 18daf2e..f86d6ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,13 @@ + import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; +import Transactions from "./pages/Transactions"; +import Analytics from "./pages/Analytics"; +import Settings from "./pages/Settings"; import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); @@ -16,7 +20,9 @@ const App = () => ( } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + } /> + } /> } /> diff --git a/src/components/AddTransactionButton.tsx b/src/components/AddTransactionButton.tsx new file mode 100644 index 0000000..d981cef --- /dev/null +++ b/src/components/AddTransactionButton.tsx @@ -0,0 +1,45 @@ + +import React, { useState } from 'react'; +import { PlusIcon, MinusIcon, X } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const AddTransactionButton = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ {isOpen && ( +
+ + +
+ )} + + +
+ ); +}; + +export default AddTransactionButton; diff --git a/src/components/BudgetCard.tsx b/src/components/BudgetCard.tsx new file mode 100644 index 0000000..5c33e92 --- /dev/null +++ b/src/components/BudgetCard.tsx @@ -0,0 +1,60 @@ + +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface BudgetCardProps { + title: string; + current: number; + total: number; + color?: string; +} + +const BudgetCard: React.FC = ({ + title, + current, + total, + color = 'neuro-accent' +}) => { + const percentage = Math.min(Math.round((current / total) * 100), 100); + + const formattedCurrent = new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + maximumFractionDigits: 0 + }).format(current); + + const formattedTotal = new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + maximumFractionDigits: 0 + }).format(total); + + return ( +
+

{title}

+ +
+

{formattedCurrent}

+

/ {formattedTotal}

+
+ +
+
+
+ +
+ = 90 ? "text-neuro-expense" : "text-gray-500" + )}> + {percentage}% + +
+
+ ); +}; + +export default BudgetCard; diff --git a/src/components/ExpenseChart.tsx b/src/components/ExpenseChart.tsx new file mode 100644 index 0000000..ce0731d --- /dev/null +++ b/src/components/ExpenseChart.tsx @@ -0,0 +1,43 @@ + +import React from 'react'; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from 'recharts'; + +interface ExpenseData { + name: string; + value: number; + color: string; +} + +interface ExpenseChartProps { + data: ExpenseData[]; +} + +const ExpenseChart: React.FC = ({ data }) => { + return ( +
+

카테고리별 지출

+ + + + `${name} ${(percent * 100).toFixed(0)}%`} + > + {data.map((entry, index) => ( + + ))} + + + +
+ ); +}; + +export default ExpenseChart; diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx new file mode 100644 index 0000000..ec9e516 --- /dev/null +++ b/src/components/NavBar.tsx @@ -0,0 +1,49 @@ + +import React from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { Home, BarChart2, Calendar, Settings } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const NavBar = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const navItems = [ + { icon: Home, label: '홈', path: '/' }, + { icon: Calendar, label: '거래', path: '/transactions' }, + { icon: BarChart2, label: '분석', path: '/analytics' }, + { icon: Settings, label: '설정', path: '/settings' }, + ]; + + return ( +
+
+ {navItems.map((item) => { + const isActive = location.pathname === item.path; + return ( + + ); + })} +
+
+ ); +}; + +export default NavBar; diff --git a/src/components/TransactionCard.tsx b/src/components/TransactionCard.tsx new file mode 100644 index 0000000..5baa89d --- /dev/null +++ b/src/components/TransactionCard.tsx @@ -0,0 +1,72 @@ + +import React from 'react'; +import { ArrowDownIcon, ArrowUpIcon, ShoppingBag, Coffee, Home, Car, Gift } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export type Transaction = { + id: string; + title: string; + amount: number; + date: string; + category: string; + type: 'expense' | 'income'; +}; + +const categoryIcons: Record = { + shopping: , + food: , + housing: , + transportation: , + entertainment: , + // Add more categories as needed +}; + +interface TransactionCardProps { + transaction: Transaction; +} + +const TransactionCard: React.FC = ({ transaction }) => { + const { title, amount, date, category, type } = transaction; + + const formattedAmount = new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + maximumFractionDigits: 0 + }).format(amount); + + return ( +
+
+
+
+ {categoryIcons[category] || } +
+ +
+

{title}

+

{date}

+
+
+ +
+ {type === 'expense' ? ( + + ) : ( + + )} + + {formattedAmount} + +
+
+
+ ); +}; + +export default TransactionCard; diff --git a/src/index.css b/src/index.css index 33fdf9d..c4c5d75 100644 --- a/src/index.css +++ b/src/index.css @@ -1,7 +1,10 @@ + @tailwind base; @tailwind components; @tailwind utilities; +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + @layer base { :root { --background: 0 0% 100%; @@ -32,7 +35,7 @@ --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; - --radius: 0.5rem; + --radius: 1rem; --sidebar-background: 0 0% 98%; @@ -96,6 +99,53 @@ } body { - @apply bg-background text-foreground; + @apply bg-neuro-background text-foreground font-inter antialiased; } -} \ No newline at end of file + + html, body, #root { + @apply h-full overflow-x-hidden; + } +} + +@layer components { + .neuro-flat { + @apply bg-neuro-background shadow-neuro-flat rounded-xl; + } + + .neuro-pressed { + @apply bg-neuro-background shadow-neuro-pressed rounded-xl; + } + + .neuro-convex { + @apply bg-neuro-background shadow-neuro-convex rounded-xl; + } + + .neuro-text { + @apply font-medium tracking-wide; + } + + .page-transition-enter { + @apply animate-fade-in; + } + + .glass-effect { + @apply bg-white/10 backdrop-blur-lg border border-white/20 rounded-xl; + } + + .neuro-button { + @apply neuro-flat px-4 py-3 text-neuro-accent font-medium transition-all duration-200 + hover:shadow-neuro-convex hover:text-neuro-accent-light active:shadow-neuro-pressed; + } + + .neuro-card { + @apply neuro-flat p-6 transition-all duration-300 hover:shadow-neuro-convex; + } + + .neuro-input { + @apply neuro-pressed px-4 py-3 w-full focus:outline-none focus:ring-2 focus:ring-neuro-accent/30; + } +} + +.font-inter { + font-family: 'Inter', sans-serif; +} diff --git a/src/pages/Analytics.tsx b/src/pages/Analytics.tsx new file mode 100644 index 0000000..0243f28 --- /dev/null +++ b/src/pages/Analytics.tsx @@ -0,0 +1,180 @@ + +import React, { useState } from 'react'; +import NavBar from '@/components/NavBar'; +import ExpenseChart from '@/components/ExpenseChart'; +import AddTransactionButton from '@/components/AddTransactionButton'; +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +const Analytics = () => { + const [selectedPeriod, setSelectedPeriod] = useState('이번 달'); + + // Sample data for the expense categories + const expenseData = [ + { name: '식비', value: 350000, color: '#9b87f5' }, + { name: '주거', value: 650000, color: '#6e59a5' }, + { name: '교통', value: 125000, color: '#81c784' }, + { name: '취미', value: 200000, color: '#64b5f6' }, + { name: '기타', value: 175000, color: '#e57373' }, + ]; + + // Sample data for the monthly comparison + const monthlyData = [ + { name: '3월', income: 2400000, expense: 1800000 }, + { name: '4월', income: 2300000, expense: 1700000 }, + { name: '5월', income: 2700000, expense: 1900000 }, + { name: '6월', income: 2200000, expense: 1500000 }, + { name: '7월', income: 2500000, expense: 1650000 }, + { name: '8월', income: 2550000, expense: 1740000 }, + ]; + + const totalIncome = 2550000; + const totalExpense = 1740000; + const savings = totalIncome - totalExpense; + const savingsPercentage = Math.round((savings / totalIncome) * 100); + + return ( +
+
+ {/* Header */} +
+

지출 분석

+ + {/* Period Selector */} +
+ + +
+ {selectedPeriod} +
+ + +
+ + {/* Summary Cards */} +
+
+

수입

+

+ {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + notation: 'compact', + maximumFractionDigits: 1 + }).format(totalIncome)} +

+
+
+

지출

+

+ {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + notation: 'compact', + maximumFractionDigits: 1 + }).format(totalExpense)} +

+
+
+

저축

+

+ {savingsPercentage}% +

+
+
+
+ + {/* Category Pie Chart */} + + + {/* Monthly Comparison */} +
+

월별 추이

+
+ + + + + new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + notation: 'compact', + maximumFractionDigits: 1 + }).format(value) + } + /> + + new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + maximumFractionDigits: 0 + }).format(value) + } + /> + + + + + +
+
+ + {/* Top Spending Categories */} +

주요 지출 카테고리

+
+
+ {expenseData.slice(0, 3).map((category, index) => ( +
+
+
+ {category.name} +
+
+

+ {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW', + maximumFractionDigits: 0 + }).format(category.value)} +

+

+ {Math.round((category.value / totalExpense) * 100)}% +

+
+
+ ))} +
+
+
+ + + +
+ ); +}; + +export default Analytics; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 52ea22c..ca0ad7a 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,12 +1,111 @@ -// Update this page (the content is just a fallback if you fail to update the page) + +import React from 'react'; +import NavBar from '@/components/NavBar'; +import BudgetCard from '@/components/BudgetCard'; +import TransactionCard, { Transaction } from '@/components/TransactionCard'; +import AddTransactionButton from '@/components/AddTransactionButton'; +import { Wallet, TrendingUp, Bell } from 'lucide-react'; const Index = () => { + // Sample data - in a real app, this would come from a data source + const transactions: Transaction[] = [ + { + id: '1', + title: '식료품 구매', + amount: 25000, + date: '오늘, 12:30 PM', + category: 'shopping', + type: 'expense' + }, + { + id: '2', + title: '주유소', + amount: 50000, + date: '어제, 3:45 PM', + category: 'transportation', + type: 'expense' + }, + { + id: '3', + title: '월급', + amount: 2500000, + date: '2일전, 9:00 AM', + category: 'income', + type: 'income' + }, + ]; + return ( -
-
-

Welcome to Your Blank App

-

Start building your amazing project here!

+
+
+ {/* Header */} +
+
+
+

반갑습니다

+

오늘의 재정 현황

+
+ +
+
+ + {/* Balance Card */} +
+
+
+ +

총 잔액

+
+ +
+ +

+ ₩2,580,000 +

+

+ 지난 달보다 12% 증가 +

+
+ + {/* Budget Progress */} +

예산 현황

+
+ + + +
+ + {/* Recent Transactions */} +

최근 거래

+
+ {transactions.map(transaction => ( + + ))} +
+ +
+ +
+ + +
); }; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 0000000..89349d5 --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,113 @@ + +import React from 'react'; +import NavBar from '@/components/NavBar'; +import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const SettingsOption = ({ + icon: Icon, + label, + description, + onClick, + color = "text-gray-700" +}: { + icon: React.ElementType; + label: string; + description?: string; + onClick?: () => void; + color?: string; +}) => { + return ( +
+
+
+ +
+
+

{label}

+ {description &&

{description}

} +
+ +
+
+ ); +}; + +const Settings = () => { + return ( +
+
+ {/* Header */} +
+

설정

+ + {/* User Profile */} +
+
+ +
+
+

홍길동

+

honggildong@example.com

+
+
+
+ + {/* Settings Options */} +
+

계정

+ + + +
+ +
+

앱 설정

+ + +
+ +
+ +
+ +
+

앱 버전 1.0.0

+
+
+ + +
+ ); +}; + +export default Settings; diff --git a/src/pages/Transactions.tsx b/src/pages/Transactions.tsx new file mode 100644 index 0000000..64019cd --- /dev/null +++ b/src/pages/Transactions.tsx @@ -0,0 +1,146 @@ + +import React, { useState } from 'react'; +import NavBar from '@/components/NavBar'; +import TransactionCard, { Transaction } from '@/components/TransactionCard'; +import AddTransactionButton from '@/components/AddTransactionButton'; +import { Calendar, Search, ChevronLeft, ChevronRight } from 'lucide-react'; + +const Transactions = () => { + const [selectedMonth, setSelectedMonth] = useState('8월'); + + // Sample data - in a real app, this would come from a data source + const transactions: Transaction[] = [ + { + id: '1', + title: '식료품 구매', + amount: 25000, + date: '8월 25일, 12:30 PM', + category: 'shopping', + type: 'expense' + }, + { + id: '2', + title: '주유소', + amount: 50000, + date: '8월 24일, 3:45 PM', + category: 'transportation', + type: 'expense' + }, + { + id: '3', + title: '월급', + amount: 2500000, + date: '8월 20일, 9:00 AM', + category: 'income', + type: 'income' + }, + { + id: '4', + title: '아마존 프라임', + amount: 9900, + date: '8월 18일, 6:00 AM', + category: 'entertainment', + type: 'expense' + }, + { + id: '5', + title: '집세', + amount: 650000, + date: '8월 15일, 10:00 AM', + category: 'housing', + type: 'expense' + }, + { + id: '6', + title: '카페', + amount: 5500, + date: '8월 12일, 2:15 PM', + category: 'food', + type: 'expense' + }, + ]; + + // Group transactions by date + const groupedTransactions: Record = {}; + + transactions.forEach(transaction => { + const datePart = transaction.date.split(',')[0]; + if (!groupedTransactions[datePart]) { + groupedTransactions[datePart] = []; + } + groupedTransactions[datePart].push(transaction); + }); + + return ( +
+
+ {/* Header */} +
+

거래 내역

+ + {/* Search */} +
+ + +
+ + {/* Month Selector */} +
+ + +
+ + {selectedMonth} +
+ + +
+ + {/* Summary */} +
+
+

총 수입

+

₩2,500,000

+
+
+

총 지출

+

₩740,400

+
+
+
+ + {/* Transactions By Date */} +
+ {Object.entries(groupedTransactions).map(([date, transactions]) => ( +
+
+
+

{date}

+
+
+ +
+ {transactions.map(transaction => ( + + ))} +
+
+ ))} +
+
+ + + +
+ ); +}; + +export default Transactions; diff --git a/tailwind.config.ts b/tailwind.config.ts index 8706086..3496d69 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,3 +1,4 @@ + import type { Config } from "tailwindcss"; export default { @@ -61,6 +62,17 @@ export default { 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', border: 'hsl(var(--sidebar-border))', ring: 'hsl(var(--sidebar-ring))' + }, + neuro: { + background: '#f0f0f3', + light: '#ffffff', + dark: '#d1d9e6', + shadow: 'rgba(209, 217, 230, 0.5)', + highlight: 'rgba(255, 255, 255, 0.5)', + accent: '#6e59a5', + 'accent-light': '#9b87f5', + expense: '#e57373', + income: '#81c784' } }, borderRadius: { @@ -84,11 +96,46 @@ export default { to: { height: '0' } + }, + 'float': { + '0%, 100%': { transform: 'translateY(0)' }, + '50%': { transform: 'translateY(-5px)' } + }, + 'pulse-subtle': { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '0.8' } + }, + 'slide-up': { + '0%': { transform: 'translateY(10px)', opacity: '0' }, + '100%': { transform: 'translateY(0)', opacity: '1' } + }, + 'slide-down': { + '0%': { transform: 'translateY(-10px)', opacity: '0' }, + '100%': { transform: 'translateY(0)', opacity: '1' } + }, + 'fade-in': { + '0%': { opacity: '0' }, + '100%': { opacity: '1' } + }, + 'scale-in': { + '0%': { transform: 'scale(0.95)', opacity: '0' }, + '100%': { transform: 'scale(1)', opacity: '1' } } }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out' + 'accordion-up': 'accordion-up 0.2s ease-out', + 'float': 'float 3s ease-in-out infinite', + 'pulse-subtle': 'pulse-subtle 3s ease-in-out infinite', + 'slide-up': 'slide-up 0.3s ease-out', + 'slide-down': 'slide-down 0.3s ease-out', + 'fade-in': 'fade-in 0.4s ease-out', + 'scale-in': 'scale-in 0.3s ease-out' + }, + boxShadow: { + 'neuro-flat': '5px 5px 10px rgba(209, 217, 230, 0.5), -5px -5px 10px rgba(255, 255, 255, 0.5)', + 'neuro-pressed': 'inset 5px 5px 10px rgba(209, 217, 230, 0.5), inset -5px -5px 10px rgba(255, 255, 255, 0.5)', + 'neuro-convex': '5px 5px 10px rgba(209, 217, 230, 0.5), -5px -5px 10px rgba(255, 255, 255, 0.5), inset 1px 1px 2px rgba(255, 255, 255, 0.25), inset -1px -1px 2px rgba(209, 217, 230, 0.25)' } } },