'use client';
import { useState, useCallback, useMemo } from 'react';
import { Search } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
} from 'recharts';
import { cn } from '@/lib/utils';
import { formatNumber as formatCurrency } from '@/lib/utils/amount';
import type {
DateFilterConfig,
PeriodSelectConfig,
SummaryCardData,
BarChartConfig,
PieChartConfig,
HorizontalBarChartConfig,
TableConfig,
ComparisonSectionConfig,
ReferenceTableConfig,
CalculationCardsConfig,
QuarterlyTableConfig,
ReviewCardsConfig,
} from '../types';
// ============================================
// 공통 유틸리티
// ============================================
// 필터 섹션
// ============================================
export const DateFilterSection = ({ config }: { config: DateFilterConfig }) => {
const today = new Date();
const [startDate, setStartDate] = useState(() => {
const d = new Date(today.getFullYear(), today.getMonth(), 1);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
});
const [endDate, setEndDate] = useState(() => {
const d = new Date(today.getFullYear(), today.getMonth() + 1, 0);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
});
const [searchText, setSearchText] = useState('');
return (
setSearchText(e.target.value)}
placeholder="검색"
className="h-8 pl-7 pr-3 text-xs w-[140px]"
/>
) : undefined
}
/>
);
};
export const PeriodSelectSection = ({ config }: { config: PeriodSelectConfig }) => {
const [selected, setSelected] = useState(config.defaultValue || config.options[0]?.value || '');
return (
신고기간
);
};
// ============================================
// 카드 섹션
// ============================================
export const SummaryCard = ({ data }: { data: SummaryCardData }) => {
const displayValue = typeof data.value === 'number'
? formatCurrency(data.value) + (data.unit || '원')
: data.value;
return (
{data.label}
{data.isComparison && !data.isPositive && typeof data.value === 'string' && !data.value.startsWith('-') ? '-' : ''}
{displayValue}
);
};
export const ReviewCardsSection = ({ config }: { config: ReviewCardsConfig }) => {
return (
{config.title}
{config.cards.map((card, index) => (
{card.label}
{formatCurrency(card.amount)}원
{card.subLabel}
))}
);
};
export const CalculationCardsSection = ({ config }: { config: CalculationCardsConfig }) => {
const isResultCard = (_index: number, operator?: string) => {
return operator === '=';
};
return (
{config.title}
{config.subtitle && (
{config.subtitle}
)}
{config.cards.map((card, index) => (
{index > 0 && card.operator && (
{card.operator}
)}
{card.label}
{formatCurrency(card.value)}{card.unit || '원'}
))}
);
};
// ============================================
// 차트 섹션
// ============================================
export const BarChartSection = ({ config }: { config: BarChartConfig }) => {
return (
{config.title}
value >= 10000 ? `${value / 10000}만` : value}
width={35}
/>
[formatCurrency(value as number) + '원', '']}
contentStyle={{ fontSize: 12 }}
/>
);
};
export const PieChartSection = ({ config }: { config: PieChartConfig }) => {
return (
{config.title}
>}
cx={50}
cy={50}
innerRadius={28}
outerRadius={45}
paddingAngle={2}
dataKey="value"
>
{config.data.map((entry, index) => (
|
))}
{config.data.map((item, index) => (
{item.name}
{item.percentage}%
{formatCurrency(item.value)}원
))}
);
};
export const HorizontalBarChartSection = ({ config }: { config: HorizontalBarChartConfig }) => {
const maxValue = Math.max(...config.data.map(d => d.value));
return (
{config.title}
{config.data.map((item, index) => (
{item.name}
{formatCurrency(item.value)}원
))}
);
};
// ============================================
// 비교 섹션
// ============================================
export const ComparisonSection = ({ config }: { config: ComparisonSectionConfig }) => {
const formatValue = (value: string | number, unit?: string): string => {
if (typeof value === 'number') {
return formatCurrency(value) + (unit || '원');
}
return value;
};
const borderColorClass = {
orange: 'border-orange-400',
blue: 'border-blue-400',
};
const titleBgClass = {
orange: 'bg-orange-50',
blue: 'bg-blue-50',
};
return (
{/* 왼쪽 박스 */}
{config.leftBox.title}
{config.leftBox.items.map((item, index) => (
{item.label}
{formatValue(item.value, item.unit)}
))}
{/* VS 영역 */}
VS
{config.vsLabel}
{typeof config.vsValue === 'number'
? formatCurrency(config.vsValue) + '원'
: config.vsValue}
{config.vsSubLabel && (
{config.vsSubLabel}
)}
{config.vsBreakdown && config.vsBreakdown.length > 0 && (
{config.vsBreakdown.map((item, index) => (
{item.label}
{typeof item.value === 'number'
? formatCurrency(item.value) + (item.unit || '원')
: item.value}
))}
)}
{/* 오른쪽 박스 */}
{config.rightBox.title}
{config.rightBox.items.map((item, index) => (
{item.label}
{formatValue(item.value, item.unit)}
))}
);
};
// ============================================
// 테이블 섹션
// ============================================
export const QuarterlyTableSection = ({ config }: { config: QuarterlyTableConfig }) => {
const formatValue = (value: number | string | undefined): string => {
if (value === undefined) return '-';
if (typeof value === 'number') return formatCurrency(value);
return value;
};
return (
{config.title}
| 구분 |
1사분기 |
2사분기 |
3사분기 |
4사분기 |
합계 |
{config.rows.map((row, rowIndex) => (
| {row.label} |
{formatValue(row.q1)} |
{formatValue(row.q2)} |
{formatValue(row.q3)} |
{formatValue(row.q4)} |
{formatValue(row.total)} |
))}
);
};
export const ReferenceTableSection = ({ config }: { config: ReferenceTableConfig }) => {
const getAlignClass = (align?: string): string => {
switch (align) {
case 'center': return 'text-center';
case 'right': return 'text-right';
default: return 'text-left';
}
};
return (
{config.title}
{config.columns.map((column) => (
|
{column.label}
|
))}
{config.data.map((row, rowIndex) => (
{config.columns.map((column) => (
|
{String(row[column.key] ?? '-')}
|
))}
))}
);
};
export const TableSection = ({ config }: { config: TableConfig }) => {
const [filters, setFilters] = useState>(() => {
const initial: Record = {};
config.filters?.forEach((filter) => {
initial[filter.key] = filter.defaultValue;
});
return initial;
});
const handleFilterChange = useCallback((key: string, value: string) => {
setFilters((prev) => ({ ...prev, [key]: value }));
}, []);
const filteredData = useMemo(() => {
if (!config.data || !Array.isArray(config.data)) {
return [];
}
let result = [...config.data];
config.filters?.forEach((filter) => {
if (filter.key === 'sortOrder') return;
const filterValue = filters[filter.key];
if (filterValue && filterValue !== 'all') {
result = result.filter((row) => row[filter.key] === filterValue);
}
});
if (filters['sortOrder']) {
const sortOrder = filters['sortOrder'];
result.sort((a, b) => {
if (sortOrder === 'amountDesc') {
return (b['amount'] as number) - (a['amount'] as number);
}
if (sortOrder === 'amountAsc') {
return (a['amount'] as number) - (b['amount'] as number);
}
const dateA = new Date(a['date'] as string).getTime();
const dateB = new Date(b['date'] as string).getTime();
return sortOrder === 'latest' ? dateB - dateA : dateA - dateB;
});
}
return result;
}, [config.data, config.filters, filters]);
const formatCellValue = (value: unknown, format?: string): string => {
if (value === null || value === undefined) return '-';
switch (format) {
case 'currency':
case 'number':
return typeof value === 'number' ? formatCurrency(value) : String(value);
default:
return String(value);
}
};
const getAlignClass = (align?: string): string => {
switch (align) {
case 'center': return 'text-center';
case 'right': return 'text-right';
default: return 'text-left';
}
};
return (
{config.title}
총 {filteredData.length}건
{config.filters && config.filters.length > 0 && (
{config.filters.map((filter) => (
))}
)}
{config.columns.map((column) => (
|
{column.label}
|
))}
{filteredData.map((row, rowIndex) => (
{config.columns.map((column) => {
const cellValue = column.key === 'no'
? rowIndex + 1
: formatCellValue(row[column.key], column.format);
const isHighlighted = column.highlightValue && String(row[column.key]) === column.highlightValue;
const highlightColorClass = column.highlightColor ? {
red: 'text-red-500',
orange: 'text-orange-500',
blue: 'text-blue-500',
green: 'text-green-500',
}[column.highlightColor] : '';
return (
|
{cellValue}
|
);
})}
))}
{config.showTotal && (
{config.columns.map((column, colIndex) => (
|
{column.key === config.totalColumnKey
? (typeof config.totalValue === 'number'
? formatCurrency(config.totalValue)
: config.totalValue)
: (colIndex === 0 ? config.totalLabel || '합계' : '')}
|
))}
)}
{config.footerSummary && config.footerSummary.length > 0 && (
{config.footerSummary.map((item, index) => (
{item.label}
{typeof item.value === 'number'
? formatCurrency(item.value)
: item.value}
))}
)}
);
};