'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}

{config.rows.map((row, rowIndex) => ( ))}
구분 1사분기 2사분기 3사분기 4사분기 합계
{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) => ( ))} {config.data.map((row, rowIndex) => ( {config.columns.map((column) => ( ))} ))}
{column.label}
{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) => ( ))} {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 ( ); })} ))} {config.showTotal && ( {config.columns.map((column, colIndex) => ( ))} )}
{column.label}
{cellValue}
{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}
))}
)}
); };