diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-14 00:26:53 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-14 00:26:53 +0000 |
| commit | dd20ba9785cdbd3d61f6b014d003d3bd9646ad13 (patch) | |
| tree | 4e99d62311a6c115dbc894084714a29c34bca11a /lib/risk-management/table/risks-dashboard.tsx | |
| parent | 33be47506f0aa62b969d82521580a29e95080268 (diff) | |
(고건) 리스크 관리 페이지 추가
Diffstat (limited to 'lib/risk-management/table/risks-dashboard.tsx')
| -rw-r--r-- | lib/risk-management/table/risks-dashboard.tsx | 244 |
1 files changed, 244 insertions, 0 deletions
diff --git a/lib/risk-management/table/risks-dashboard.tsx b/lib/risk-management/table/risks-dashboard.tsx new file mode 100644 index 00000000..1f26d48a --- /dev/null +++ b/lib/risk-management/table/risks-dashboard.tsx @@ -0,0 +1,244 @@ +'use client'; + +/* IMPORT */ +import { Bar, BarChart, Cell, LabelList, XAxis, YAxis } from 'recharts'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'; +import { getRisksViewCount } from '../service'; +import { LoaderCircle } from 'lucide-react'; +import { type DateRange } from 'react-day-picker'; +import { useEffect, useState, useMemo, useCallback } from 'react'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; +import { ValueType } from 'recharts/types/component/DefaultTooltipContent'; + +// ---------------------------------------------------------------------------------------------------- + +/* TYPES */ +interface RisksDashboardProps { + targetValues: string[]; + defaultDateRange: DateRange; +} + +interface CountData { + [key: string]: number; +} + +interface ChartData { + name: string; + count: number; + color: string; +} + +// ---------------------------------------------------------------------------------------------------- + +/* RISKS DASHBOARD COMPONENT */ +function RisksDashboard(props: RisksDashboardProps) { + const { targetValues, defaultDateRange } = props; + const router = useRouter(); + const searchParams = useSearchParams(); + const params = useParams(); + const [counts, setCounts] = useState<CountData>({}); + const [isLoading, setIsLoading] = useState(true); + const [dateQuery, setDateQuery] = useState({ + from: defaultDateRange.from?.toISOString() ?? '', + to: defaultDateRange.to?.toISOString() ?? '', + search: '', + }); + const language = params?.lng as string; + + const chartData: ChartData[] = useMemo(() => { + const chartItems = ['단기연체', '노무비체불', '세금체납', '채무불이행', '행정처분', '당좌거래정지', '기업회생', '휴/폐업']; + const colors = [ + '#22c55e', + '#6dd85d', + '#b4e85b', + '#f0f06d', + '#fce46d', + '#fcb36d', + '#f98b6d', + '#ef4444', + ]; + + return chartItems.map((item, index) => ({ + name: item, + count: counts[item] ?? 0, + color: colors[index], + })); + }, [counts]); + const chartConfig: ChartConfig = { + count: { + label: '건수', + }, + }; + + const fetchAllCounts = useCallback(async () => { + if (!dateQuery.from || !dateQuery.to) { + return; + } + + setIsLoading(true); + try { + const countPromises = targetValues.map(async (targetValue) => { + const filters = [ + { + id: 'eventType', + value: targetValue, + type: 'text', + operator: 'iLike', + rowId: '', + } + ]; + + const searchParams = { + filters, + joinOperator: 'and', + from: dateQuery.from, + to: dateQuery.to, + search: dateQuery.search, + flags: [], + page: 1, + perPage: 10, + sort: [{ id: 'createdAt', desc: true }], + }; + + const { count } = await getRisksViewCount(searchParams as any); + return { targetValue, count }; + }); + + const results = await Promise.all(countPromises); + const newCounts: CountData = {}; + results.forEach(({ targetValue, count }) => { + newCounts[targetValue] = count; + }); + + setCounts(newCounts); + } catch (error) { + console.error('리스크 데이터 개수 조회에 실패했어요:', error); + const resetCounts: CountData = {}; + targetValues.forEach(value => { + resetCounts[value] = 0; + }); + setCounts(resetCounts); + } finally { + setIsLoading(false); + } + }, [dateQuery, targetValues]); + + useEffect(() => { + const urlParams = new URLSearchParams(searchParams?.toString()); + const from = urlParams.get('from') ?? defaultDateRange.from?.toISOString() ?? ''; + const to = urlParams.get('to') ?? defaultDateRange.to?.toISOString() ?? ''; + const search = urlParams.get('search') ?? ''; + + setDateQuery((prev) => { + if (prev.from === from && prev.to === to && prev.search === search) { + return prev; + } + return { from, to, search }; + }); + }, [searchParams, defaultDateRange]); + + useEffect(() => { + fetchAllCounts(); + }, [fetchAllCounts]); + + const handleButtonClick = useCallback((targetValue: string) => { + const newFilters = [ + { + id: 'eventType', + value: targetValue, + type: 'text', + operator: 'iLike', + rowId: '', + } + ]; + + const newUrlParams = new URLSearchParams(searchParams?.toString()); + newUrlParams.set('filters', JSON.stringify(newFilters)); + + const baseUrl = `/${language}/evcp/risk-management`; + const fullUrl = `${baseUrl}?${newUrlParams.toString()}`; + const decodedUrl = decodeURIComponent(fullUrl); + + router.push(decodedUrl); + router.refresh(); + }, [searchParams, language, router]); + + return ( + <div className="flex items-center justify-center gap-16"> + <div className="w-3/5 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-16 gap-y-4 p-8"> + {targetValues.map((targetValue) => ( + <div key={targetValue} className="w-full"> + <div className="w-32 flex flex-col items-center justify-center gap-2"> + <div className="font-bold">{targetValue}</div> + <Button + className="w-full h-12 text-lg font-bold" + onClick={() => handleButtonClick(targetValue)} + disabled={isLoading} + > + {isLoading ? ( + <LoaderCircle width={16} className="animate-spin" /> + ) : ( + <> + {counts[targetValue] ?? 0}건 + </> + )} + </Button> + </div> + </div> + ))} + </div> + <Card className="w-1/3"> + <CardHeader> + <CardTitle className="text-lg font-semibold">주요 리스크 현황</CardTitle> + </CardHeader> + <CardContent> + {chartData.filter(item => item.count > 0).length === 0 ? ( + <div className="flex items-center justify-center h-[300px] text-gray-500"> + 주요 리스크가 존재하지 않아요. + </div> + ) : ( + <ChartContainer config={chartConfig} className="h-[300px]"> + <BarChart + data={chartData.filter(item => item.count > 0)} + layout="vertical" + margin={{ left: 30, right: 30 }} + > + <XAxis type="number" dataKey="count" hide /> + <YAxis + dataKey="name" + type="category" + tickLine={false} + tickMargin={10} + axisLine={false} + /> + <ChartTooltip + content={<ChartTooltipContent className="flex justify-center" />} + formatter={(value) => [`${value}건`]} + /> + <Bar dataKey="count" radius={[8, 8, 8, 8]}> + <LabelList + position="right" + offset={12} + className="fill-foreground" + fontSize={12} + formatter={(value: ValueType) => [`${value}건`]} + /> + {chartData.map((entry, index) => ( + <Cell key={`cell-${index}`} fill={entry.color} /> + ))} + </Bar> + </BarChart> + </ChartContainer> + )} + </CardContent> + </Card> + </div> + ); +} + +// ---------------------------------------------------------------------------------------------------- + +/* EXPORT */ +export default RisksDashboard; |
