From 5b6313f16f508882a0ea67716b7dbaa1c6967f04 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<DomainStats[]>([])
+  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 (
+      <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
+        {Array.from({ length: 6 }).map((_, i) => (
+          <Card key={i} className="animate-pulse">
+            <CardContent className="p-4">
+              <div className="h-16 bg-gray-200 rounded"></div>
+            </CardContent>
+          </Card>
+        ))}
+      </div>
+    )
+  }
+
+  return (
+    <div className="space-y-4">
+      {/* 요약 정보 */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h3 className="text-lg font-semibold">도메인별 사용자 현황</h3>
+          <p className="text-sm text-muted-foreground">
+            총 {totalUsers}명의 사용자가 등록되어 있습니다.
+          </p>
+        </div>
+        
+        {pendingCount > 0 && (
+          <Button 
+            variant="outline" 
+            size="sm"
+            onClick={() => handleDomainClick("pending")}
+            className={`gap-2 ${currentFilter === "pending" ? "bg-yellow-50 border-yellow-300" : ""}`}
+          >
+            <AlertCircle className="h-4 w-4 text-yellow-600" />
+            승인 대기 {pendingCount}명
+          </Button>
+        )}
+      </div>
+
+      {/* 도메인별 통계 카드 */}
+      <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
+        {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 (
+            <Card 
+              key={domain}
+              className={`cursor-pointer transition-all hover:shadow-md ${
+                isActive 
+                  ? `${config.bgColor} ${config.borderColor} border-2` 
+                  : "hover:bg-gray-50"
+              }`}
+              onClick={() => handleDomainClick(domain)}
+            >
+              <CardContent className="p-4">
+                <div className="flex items-center space-x-3">
+                  <div className={`p-2 rounded-full ${config.color}`}>
+                    <IconComponent className="h-4 w-4 text-white" />
+                  </div>
+                  <div className="flex-1 min-w-0">
+                    <div className="flex items-center justify-between">
+                      <p className={`text-sm font-medium ${isActive ? config.textColor : "text-gray-900"}`}>
+                        {config.label}
+                      </p>
+                      <Badge 
+                        variant={isActive ? "default" : "secondary"}
+                        className="ml-2"
+                      >
+                        {count}
+                      </Badge>
+                    </div>
+                    <p className="text-xs text-muted-foreground truncate">
+                      {config.description}
+                    </p>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          )
+        })}
+      </div>
+
+      {/* 필터 상태 표시 */}
+      {currentFilter && (
+        <div className="flex items-center gap-2">
+          <span className="text-sm text-muted-foreground">필터 적용됨:</span>
+          <Badge variant="outline" className="gap-1">
+            {domainConfig[currentFilter as keyof typeof domainConfig]?.label || currentFilter}
+            <button 
+              onClick={() => onDomainFilter(null)}
+              className="ml-1 hover:bg-gray-200 rounded-full p-0.5"
+            >
+              ×
+            </button>
+          </Badge>
+        </div>
+      )}
+    </div>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

