Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(grep -E \"\\\\.\\(ts|tsx\\)$\")"
]
}
}
6 changes: 3 additions & 3 deletions src/components/auth/intro/IntroAIAnalytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@ export default function IntroAIAnalytics({ isActive }: { isActive: boolean }) {
{bars.map((bar, index) => (
<div
key={index}
className={`relative w-12 sm:w-16 rounded-t-2xl transition-all duration-500 ${
isActive ? "animate-graph-up" : "h-0"
className={`relative w-12 sm:w-16 rounded-t-2xl ${
isActive ? "animate-graph-up" : "origin-bottom scale-y-0"
} ${
bar.isBlue
? "bg-linear-to-b from-logo-1 to-logo-2"
: "bg-chart-inactive"
}`}
style={
{
"--target-height": bar.height,
height: bar.height,
animationDelay: bar.delay,
} as React.CSSProperties
}
Expand Down
15 changes: 10 additions & 5 deletions src/components/common/badge/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { HTMLAttributes, ReactNode } from "react";
import { twMerge } from "tailwind-merge";

type TBadgeVariant = "syncing" | "success" | "inactive" | "running" | "stopped";
export type TBadgeVariant =
| "syncing"
| "success"
| "inactive"
| "running"
| "stopped";

type TBadgeSize = "sm" | "md";

Expand All @@ -26,10 +31,10 @@ export default function Badge({

const variantClasses: Record<TBadgeVariant, string> = {
syncing:
"bg-status-yellow/30 text-status-yellow border border-status-yellow",
success: "bg-status-green/30 text-status-green border border-status-green",
inactive: "bg-status-red/30 text-status-red border border-status-red/40",
running: "bg-status-blue/30 text-status-blue border border-status-blue/40",
"bg-status-yellow/15 text-status-yellow border border-status-yellow",
success: "bg-status-green/15 text-status-green border border-status-green",
inactive: "bg-status-red/15 text-status-red border border-status-red/40",
running: "bg-status-blue/15 text-status-blue border border-status-blue/40",
stopped: "bg-brand-300 text-text-sub border border-text-placeholder/40",
};

Expand Down
6 changes: 5 additions & 1 deletion src/components/common/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export default function Card({
{hasHeader && (
<div className="flex flex-wrap items-start justify-between gap-2 mb-4">
<div className="flex flex-col gap-0.5">
{title && <h3 className="font-body1 text-text-main">{title}</h3>}
{title && (
<h3 className="font-heading4 font-semibold! text-text-main">
{title}
</h3>
)}
{description && (
<div className="font-caption text-text-sub">{description}</div>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/chart/ChartLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export interface IChartLegendProps {
export default function ChartLegend({ items, className }: IChartLegendProps) {
return (
<div className={twMerge("flex items-center gap-4", className)}>
{items.map((item, index) => (
<div key={index} className="flex items-center gap-1.5">
{items.map((item) => (
<div key={item.label} className="flex items-center gap-1.5">
<div
className={twMerge("w-1.5 h-1.5 rounded-full", item.colorClass)}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/dropdownmenu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function DropdownMenu({
"group flex w-full items-center gap-3 rounded-component-md px-5 py-4 text-left font-body2 transition-fast",
it.active
? "bg-brand-300 text-status-blue"
: "text-text-disabled hover:bg-brand-300 hover:text-status-blue",
: "text-text-auth-sub hover:bg-brand-300 hover:text-status-blue",
)}
>
{it.icon ? (
Expand Down
46 changes: 0 additions & 46 deletions src/components/common/toast/Toast.tsx

This file was deleted.

107 changes: 79 additions & 28 deletions src/components/dashboard/charts/BudgetGaugeChart.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useEffect, useState } from "react";
import { twMerge } from "tailwind-merge";

import Badge, { type TBadgeVariant } from "@/components/common/badge/Badge";

interface IBudgetGaugeChartProps {
totalBudget: number;
spent: number;
warningThreshold: number;
dangerThreshold: number;
}

const statusBadgeClasses: Record<string, string> = {
안정: "bg-status-green/[0.08] text-status-green",
주의: "bg-status-yellow/[0.08] text-status-yellow",
위험: "bg-status-red/[0.08] text-status-red",
const statusBadgeVariant: Record<string, TBadgeVariant> = {
안정: "success",
주의: "syncing",
위험: "inactive",
};

const statusPointClasses: Record<string, string> = {
Expand All @@ -25,8 +28,21 @@ export default function BudgetGaugeChart({
warningThreshold,
dangerThreshold,
}: IBudgetGaugeChartProps) {
const percentage = Math.round((spent / totalBudget) * 100);
const remaining = totalBudget - spent;
const percentage =
totalBudget > 0 ? Math.round((spent / totalBudget) * 100) : 0;
const isOverBudget = spent > totalBudget;
const remaining = isOverBudget ? spent - totalBudget : totalBudget - spent;
Comment thread
Seojegyeong marked this conversation as resolved.
Resolved

const now = new Date();
const periodElapsedDays = now.getDate();
const periodTotalDays = new Date(
now.getFullYear(),
now.getMonth() + 1,
0,
).getDate();
const periodElapsedRate = Math.round(
(periodElapsedDays / periodTotalDays) * 100,
);

const getStatus = () => {
if (percentage >= dangerThreshold) return "위험";
Expand All @@ -36,6 +52,12 @@ export default function BudgetGaugeChart({

const status = getStatus();

const [mounted, setMounted] = useState(false);
useEffect(() => {
const raf = requestAnimationFrame(() => setMounted(true));
return () => cancelAnimationFrame(raf);
}, []);

return (
<div className="flex flex-col w-full h-full justify-between font-pretendard">
<div className="flex flex-col gap-4 mb-6">
Expand All @@ -49,28 +71,41 @@ export default function BudgetGaugeChart({
</span>
</div>

<span
className={twMerge(
"inline-flex items-center px-2.5 py-1 rounded-full font-caption font-bold tracking-tight",
statusBadgeClasses[status],
)}
>
<Badge variant={statusBadgeVariant[status]} size="sm">
{status}
</span>
</Badge>
</div>

<div className="relative h-3 w-full bg-bg-disabled/50 rounded-full overflow-hidden">
<div className="relative py-1.5">
<div
className={twMerge(
"absolute top-0 left-0 h-full rounded-full transition-all duration-1000 ease-out",
status === "안정"
? "bg-status-green"
: status === "주의"
? "bg-status-yellow"
: "bg-status-red",
)}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
role="progressbar"
aria-valuenow={Math.min(Math.max(percentage, 0), 100)}
aria-valuemin={0}
aria-valuemax={100}
aria-label="예산 소진율"
aria-valuetext={
percentage > 100
? `예산 소진율 ${percentage}%, 예산을 초과했습니다`
: `예산 소진율 ${Math.max(percentage, 0)}%`
}
className="relative h-3 w-full bg-bg-disabled/40 rounded-full overflow-hidden shadow-[inset_0_1px_3px_rgba(0,0,0,0.08)]"
Comment thread
Seojegyeong marked this conversation as resolved.
Resolved
>
<div
className={twMerge(
"absolute top-0 left-0 h-full w-full rounded-full transition-transform duration-1000 ease-out origin-left",
status === "안정"
? "bg-linear-to-r from-status-green/50 to-status-green"
: status === "주의"
? "bg-linear-to-r from-status-yellow/50 to-status-yellow"
: "bg-linear-to-r from-status-red/50 to-status-red",
)}
style={{
transform: `scaleX(${mounted ? Math.min(percentage, 100) / 100 : 0})`,
}}
>
<div className="absolute inset-0 rounded-full bg-linear-to-b from-white/30 to-transparent" />
</div>
</div>
</div>
</div>

Expand All @@ -89,12 +124,28 @@ export default function BudgetGaugeChart({
₩{spent.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-body2 text-text-sub">기간 진행률</span>
<span className="font-body2 text-text-main tracking-tight">
{periodElapsedDays}/{periodTotalDays}일{" "}
<span className="text-text-sub font-medium">
({periodElapsedRate}%)
</span>
</span>
</div>
</div>

<div className="bg-bg-surface/40 rounded-component-lg p-6 flex flex-col gap-4 border border-white/40 mt-auto">
<div className="flex flex-col gap-1">
<span className="font-caption text-text-sub">이번 달 잔액</span>
<span className="font-heading2 text-text-main font-extrabold tracking-tight leading-none pt-1">
<span className="font-body2 text-text-sub">
{isOverBudget ? "초과 지출" : "이번 달 잔액"}
</span>
<span
className={twMerge(
"font-heading2 font-extrabold tracking-tight leading-none pt-1",
isOverBudget ? "text-status-red" : "text-text-main",
)}
>
₩{remaining.toLocaleString()}
</span>
</div>
Expand All @@ -104,12 +155,12 @@ export default function BudgetGaugeChart({
<div className="flex items-start gap-1.5">
<div
className={twMerge(
"w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 animate-pulse",
"w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 motion-safe:animate-pulse",
statusPointClasses[status],
)}
/>
<p className="font-caption text-text-sub leading-relaxed">
현재{" "}
기간의 {periodElapsedRate}%가 경과했으며, 예산은{" "}
<span
className={twMerge(
"font-bold",
Expand Down
Loading