From 5b6313f16f508882a0ea67716b7dbaa1c6967f04 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 30 Jun 2025 08:28:13 +0000 Subject: (대표님) 20250630 16시 - 유저 도메인별 라우터 분리와 보안성검토 대응 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/users/access-control/domain-stats-cards.tsx | 232 ++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 lib/users/access-control/domain-stats-cards.tsx (limited to 'lib/users/access-control/domain-stats-cards.tsx') diff --git a/lib/users/access-control/domain-stats-cards.tsx b/lib/users/access-control/domain-stats-cards.tsx new file mode 100644 index 00000000..e6320e12 --- /dev/null +++ b/lib/users/access-control/domain-stats-cards.tsx @@ -0,0 +1,232 @@ +// components/domain-stats-cards.tsx +"use client" + +import * as React from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Users, + Clock, + Shield, + ShoppingCart, + TrendingUp, + Settings, + Building, + AlertCircle +} from "lucide-react" +import { toast } from "sonner" +import { getUserDomainStats } from "../service" + +interface DomainStatsCardsProps { + onDomainFilter: (domain: string | null) => void + currentFilter?: string | null +} + +// 도메인별 설정 +const domainConfig = { + pending: { + label: "승인 대기", + description: "신규 사용자", + icon: Clock, + color: "bg-yellow-500", + textColor: "text-yellow-700", + bgColor: "bg-yellow-50", + borderColor: "border-yellow-200" + }, + evcp: { + label: "전체 시스템", + description: "관리자급", + icon: Shield, + color: "bg-blue-500", + textColor: "text-blue-700", + bgColor: "bg-blue-50", + borderColor: "border-blue-200" + }, + procurement: { + label: "구매관리팀", + description: "구매/계약 관리", + icon: ShoppingCart, + color: "bg-green-500", + textColor: "text-green-700", + bgColor: "bg-green-50", + borderColor: "border-green-200" + }, + sales: { + label: "기술영업팀", + description: "영업/프로젝트", + icon: TrendingUp, + color: "bg-purple-500", + textColor: "text-purple-700", + bgColor: "bg-purple-50", + borderColor: "border-purple-200" + }, + engineering: { + label: "설계관리팀", + description: "설계/기술평가", + icon: Settings, + color: "bg-orange-500", + textColor: "text-orange-700", + bgColor: "bg-orange-50", + borderColor: "border-orange-200" + }, + // partners: { + // label: "협력업체", + // description: "외부 업체", + // icon: Building, + // color: "bg-indigo-500", + // textColor: "text-indigo-700", + // bgColor: "bg-indigo-50", + // borderColor: "border-indigo-200" + // } +} + +interface DomainStats { + domain: string + count: number +} + +export function DomainStatsCards({ onDomainFilter, currentFilter }: DomainStatsCardsProps) { + const [stats, setStats] = React.useState([]) + const [isLoading, setIsLoading] = React.useState(true) + const [totalUsers, setTotalUsers] = React.useState(0) + + // 통계 데이터 로드 + React.useEffect(() => { + const loadStats = async () => { + setIsLoading(true) + try { + const result = await getUserDomainStats() + if (result.success) { + setStats(result.data) + setTotalUsers(result.data.reduce((sum, item) => sum + item.count, 0)) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("통계 로드 오류:", error) + toast.error("통계를 불러오는 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + loadStats() + }, []) + + // 도메인별 카드 클릭 핸들러 + const handleDomainClick = (domain: string) => { + if (currentFilter === domain) { + // 이미 선택된 도메인이면 필터 해제 + onDomainFilter(null) + } else { + // 새로운 도메인 필터 적용 + onDomainFilter(domain) + } + } + + // pending 사용자 수 가져오기 + const pendingCount = stats.find(s => s.domain === "pending")?.count || 0 + + if (isLoading) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + +
+
+
+ ))} +
+ ) + } + + return ( +
+ {/* 요약 정보 */} +
+
+

도메인별 사용자 현황

+

+ 총 {totalUsers}명의 사용자가 등록되어 있습니다. +

+
+ + {pendingCount > 0 && ( + + )} +
+ + {/* 도메인별 통계 카드 */} +
+ {Object.entries(domainConfig).map(([domain, config]) => { + const domainStat = stats.find(s => s.domain === domain) + const count = domainStat?.count || 0 + const isActive = currentFilter === domain + const IconComponent = config.icon + + return ( + handleDomainClick(domain)} + > + +
+
+ +
+
+
+

+ {config.label} +

+ + {count} + +
+

+ {config.description} +

+
+
+
+
+ ) + })} +
+ + {/* 필터 상태 표시 */} + {currentFilter && ( +
+ 필터 적용됨: + + {domainConfig[currentFilter as keyof typeof domainConfig]?.label || currentFilter} + + +
+ )} +
+ ) +} \ No newline at end of file -- cgit v1.2.3