summaryrefslogtreecommitdiff
path: root/lib/risk-management/table/risks-dashboard.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/risk-management/table/risks-dashboard.tsx')
-rw-r--r--lib/risk-management/table/risks-dashboard.tsx244
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;