summaryrefslogtreecommitdiff
path: root/lib/dashboard
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-02 00:45:49 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-02 00:45:49 +0000
commit2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 (patch)
treef406b5c86f563347c7fd088a85fd1a82284dc5ff /lib/dashboard
parent6a9ca20deddcdcbe8495cf5a73ec7ea5f53f9b55 (diff)
(대표님/최겸) 20250702 변경사항 업데이트
Diffstat (limited to 'lib/dashboard')
-rw-r--r--lib/dashboard/dashboard-client.tsx115
-rw-r--r--lib/dashboard/dashboard-overview-chart.tsx325
-rw-r--r--lib/dashboard/dashboard-stats-card.tsx88
-rw-r--r--lib/dashboard/dashboard-summary-cards.tsx64
-rw-r--r--lib/dashboard/partners-service.ts447
-rw-r--r--lib/dashboard/service.ts454
6 files changed, 1493 insertions, 0 deletions
diff --git a/lib/dashboard/dashboard-client.tsx b/lib/dashboard/dashboard-client.tsx
new file mode 100644
index 00000000..37dc1901
--- /dev/null
+++ b/lib/dashboard/dashboard-client.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useState } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { RefreshCw } from "lucide-react";
+import { DashboardStatsCard } from "./dashboard-stats-card";
+import { DashboardOverviewChart } from "./dashboard-overview-chart";
+import { DashboardSummaryCards } from "./dashboard-summary-cards";
+import { toast } from "sonner";
+import { DashboardData } from "./service";
+
+interface DashboardClientProps {
+ initialData: DashboardData;
+ onRefresh: () => Promise<DashboardData>;
+}
+
+export function DashboardClient({ initialData, onRefresh }: DashboardClientProps) {
+ const [data, setData] = useState<DashboardData>(initialData);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+
+
+ const handleRefresh = async () => {
+ try {
+ setIsRefreshing(true);
+ const newData = await onRefresh();
+ setData(newData);
+ toast.success("대시보드 데이터가 새로고침되었습니다.");
+ } catch (error) {
+ toast.error("데이터 새로고침에 실패했습니다.");
+ console.error("Dashboard refresh error:", error);
+ } finally {
+ setIsRefreshing(false);
+ }
+ };
+
+ const getDomainDisplayName = (domain: string) => {
+ const domainNames: Record<string, string> = {
+ 'procurement': '구매 관리',
+ 'sales': '영업 관리',
+ "partners": 'Partners',
+ 'engineering': '엔지니어링'
+ };
+ return domainNames[domain] || domain;
+ };
+
+ return (
+ <div className="space-y-6">
+ {/* 헤더 */}
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ {getDomainDisplayName(data.domain)} Dashboard
+ </h2>
+ <p className="text-muted-foreground">
+ {data.domain ==="partners"? "회사와 개인에게 할당된 일들을 보여줍니다.":"팀과 개인에게 할당된 일들을 보여줍니다."}
+ </p>
+ </div>
+ <Button
+ onClick={handleRefresh}
+ disabled={isRefreshing}
+ variant="outline"
+ size="sm"
+ >
+ <RefreshCw
+ className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`}
+ />
+ 새로고침
+ </Button>
+ </div>
+
+ {/* 요약 카드 */}
+ <DashboardSummaryCards summary={data.summary} />
+
+ {/* 차트 */}
+ <DashboardOverviewChart
+ data={data.teamStats}
+ title={getDomainDisplayName(data.domain)}
+ description="업무 타입별 현황을 확인하세요"
+ />
+
+ {/* 탭 */}
+ <Tabs defaultValue="team" className="space-y-4">
+ <TabsList className="grid w-full grid-cols-2 max-w-md">
+ <TabsTrigger value="team"> {data.domain ==="partners"? "회사 업무 현황":"팀 업무 현황"}</TabsTrigger>
+ <TabsTrigger value="personal">내 업무 현황</TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="team" className="space-y-4">
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+ {data.teamStats.map((stats) => (
+ <DashboardStatsCard
+ key={stats.tableName}
+ stats={stats}
+ showUserStats={false}
+ />
+ ))}
+ </div>
+ </TabsContent>
+
+ <TabsContent value="personal" className="space-y-4">
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
+ {data.userStats.map((stats) => (
+ <DashboardStatsCard
+ key={stats.tableName}
+ stats={stats}
+ showUserStats={true}
+ />
+ ))}
+ </div>
+ </TabsContent>
+ </Tabs>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/lib/dashboard/dashboard-overview-chart.tsx b/lib/dashboard/dashboard-overview-chart.tsx
new file mode 100644
index 00000000..ca5c0006
--- /dev/null
+++ b/lib/dashboard/dashboard-overview-chart.tsx
@@ -0,0 +1,325 @@
+"use client";
+
+import { TrendingUp, BarChart3, PieChart } from "lucide-react";
+import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Pie, PieChart as RechartsPieChart, Cell, ResponsiveContainer, LabelList } from "recharts";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { DashboardStats } from "@/lib/dashboard/service";
+
+interface DashboardOverviewChartProps {
+ data: DashboardStats[];
+ title: string;
+ description?: string;
+}
+
+// 차트 설정
+const chartConfig = {
+ pending: {
+ label: "대기",
+ color: "hsl(var(--chart-1))", // 회색 계열
+ },
+ inProgress: {
+ label: "진행중",
+ color: "hsl(var(--chart-2))", // 파란색 계열
+ },
+ completed: {
+ label: "완료",
+ color: "hsl(var(--chart-3))", // 초록색 계열
+ },
+} satisfies ChartConfig;
+
+// 파이 차트용 색상
+const PIE_COLORS = {
+ pending: "#6b7280",
+ inProgress: "#3b82f6",
+ completed: "#10b981"
+};
+
+export function DashboardOverviewChart({ data, title, description }: DashboardOverviewChartProps) {
+ // 바 차트용 데이터 변환
+ const barChartData = data.map(item => ({
+ name: item.displayName.length > 10 ?
+ item.displayName.substring(0, 10) + "..." :
+ item.displayName,
+ fullName: item.displayName,
+ pending: item.pending,
+ inProgress: item.inProgress,
+ completed: item.completed,
+ total: item.total
+ }));
+
+ // 파이 차트용 데이터 (전체 요약)
+ const totalPending = data.reduce((sum, item) => sum + item.pending, 0);
+ const totalInProgress = data.reduce((sum, item) => sum + item.inProgress, 0);
+ const totalCompleted = data.reduce((sum, item) => sum + item.completed, 0);
+ const totalTasks = totalPending + totalInProgress + totalCompleted;
+
+ const pieChartData = [
+ { name: "대기", value: totalPending, color: PIE_COLORS.pending },
+ { name: "진행중", value: totalInProgress, color: PIE_COLORS.inProgress },
+ { name: "완료", value: totalCompleted, color: PIE_COLORS.completed }
+ ].filter(item => item.value > 0);
+
+ // 완료율 계산
+ const completionRate = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0;
+ const isImproving = completionRate > 50; // 50% 이상이면 개선으로 간주
+
+ return (
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
+ {/* 바 차트 - 2/3 너비 */}
+ <Card className="md:col-span-2">
+ <CardHeader className="pb-4">
+ <CardTitle className="flex items-center gap-2 text-lg">
+ <BarChart3 className="h-5 w-5" />
+ {title} - 업무별 현황
+ </CardTitle>
+ {description && <CardDescription className="text-sm">{description}</CardDescription>}
+ </CardHeader>
+ <CardContent className="pb-2">
+ <ChartContainer
+ config={chartConfig}
+ className="max-h-[200px] w-full" // 고정 높이로 바 차트 크기 제한
+ >
+ <BarChart
+ accessibilityLayer
+ data={barChartData}
+ margin={{ top: 30, right: 15, left: 10, bottom: 5 }} // top margin 증가로 라벨 공간 확보
+ >
+ <CartesianGrid vertical={false} />
+ <XAxis
+ dataKey="name"
+ tickLine={false}
+ tickMargin={10}
+ axisLine={false}
+ fontSize={12}
+ interval={0}
+ // angle={-45}
+ textAnchor="end"
+ height={60}
+ />
+ <YAxis
+ tickLine={false}
+ axisLine={false}
+ fontSize={12}
+ />
+ <ChartTooltip
+ cursor={false}
+ content={<ChartTooltipContent
+ indicator="dashed"
+ labelFormatter={(label, payload) => {
+ const item = barChartData.find(d => d.name === label);
+ return item?.fullName || label;
+ }}
+ />}
+ />
+ <Bar
+ dataKey="pending"
+ fill="var(--color-pending)"
+ radius={[0, 0, 4, 4]}
+ stackId="status"
+ />
+ <Bar
+ dataKey="inProgress"
+ fill="var(--color-inProgress)"
+ radius={[0, 0, 0, 0]}
+ stackId="status"
+ />
+ <Bar
+ dataKey="completed"
+ fill="var(--color-completed)"
+ radius={[4, 4, 0, 0]}
+ stackId="status"
+ >
+ <LabelList
+ dataKey="total"
+ position="top"
+ offset={8}
+ className="fill-foreground"
+ fontSize={12}
+ formatter={(value: number) => value > 0 ? value : ''}
+ />
+ </Bar>
+ </BarChart>
+ </ChartContainer>
+ </CardContent>
+ <CardFooter className="flex-col items-start gap-1 text-sm pt-2">
+ <div className="flex gap-2 items-center leading-none font-medium">
+ {totalTasks > 0 && (
+ <>
+ <TrendingUp className={`h-4 w-4 ${isImproving ? 'text-green-600' : 'text-orange-600'}`} />
+ <span>완료율 {completionRate}% - {isImproving ? '순조롭게 진행중' : '진행 필요'}</span>
+ </>
+ )}
+ </div>
+ <div className="text-muted-foreground leading-none">
+ 총 {totalTasks}건의 업무 현황
+ </div>
+ </CardFooter>
+ </Card>
+
+ {/* 파이 차트 - 1/3 너비 */}
+ <Card className="md:col-span-1">
+ <CardHeader className="pb-4">
+ <CardTitle className="flex items-center gap-2 text-lg">
+ <PieChart className="h-5 w-5" />
+ 업무 분포
+ </CardTitle>
+ <CardDescription className="text-sm">상태별 비율</CardDescription>
+ </CardHeader>
+ <CardContent className="pb-2">
+ <ChartContainer
+ config={chartConfig}
+ className="max-h-[200px] w-full" // 고정 높이로 바 차트 크기 제한
+ >
+ <RechartsPieChart>
+ <ChartTooltip
+ cursor={false}
+ content={<ChartTooltipContent
+ hideLabel
+ formatter={(value, name) => [
+ `${value}건 (${Math.round((Number(value) / totalTasks) * 100)}%)`,
+ name
+ ]}
+ />}
+ />
+ <Pie
+ data={pieChartData}
+ cx="50%"
+ cy="50%"
+ labelLine={false}
+ label={({ name, percent }) =>
+ percent > 0.1 ? `${(percent * 100).toFixed(0)}%` : ''
+ }
+ outerRadius={70} // 작은 공간에 맞게 더 작게 조정
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {pieChartData.map((entry, index) => (
+ <Cell key={`cell-${index}`} fill={entry.color} />
+ ))}
+ </Pie>
+ </RechartsPieChart>
+ </ChartContainer>
+ </CardContent>
+ <CardFooter className="pt-2">
+ <div className="grid grid-cols-1 gap-1 w-full text-center">
+ <div className="text-xs space-y-1">
+ <div className="flex items-center justify-between">
+ <span className="text-muted-foreground">대기</span>
+ <span className="font-medium text-gray-600">{totalPending}</span>
+ </div>
+ <div className="flex items-center justify-between">
+ <span className="text-muted-foreground">진행</span>
+ <span className="font-medium text-blue-600">{totalInProgress}</span>
+ </div>
+ <div className="flex items-center justify-between">
+ <span className="text-muted-foreground">완료</span>
+ <span className="font-medium text-green-600">{totalCompleted}</span>
+ </div>
+ </div>
+ </div>
+ </CardFooter>
+ </Card>
+ </div>
+ );
+}
+
+// 더 컴팩트한 버전 (높이를 더 많이 줄이고 싶은 경우)
+export function CompactDashboardChart({ data, title, description }: DashboardOverviewChartProps) {
+ const totalPending = data.reduce((sum, item) => sum + item.pending, 0);
+ const totalInProgress = data.reduce((sum, item) => sum + item.inProgress, 0);
+ const totalCompleted = data.reduce((sum, item) => sum + item.completed, 0);
+ const totalTasks = totalPending + totalInProgress + totalCompleted;
+
+ const pieChartData = [
+ { name: "대기", value: totalPending, color: PIE_COLORS.pending },
+ { name: "진행중", value: totalInProgress, color: PIE_COLORS.inProgress },
+ { name: "완료", value: totalCompleted, color: PIE_COLORS.completed }
+ ].filter(item => item.value > 0);
+
+ const completionRate = totalTasks > 0 ? Math.round((totalCompleted / totalTasks) * 100) : 0;
+
+ return (
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
+ {/* 요약 통계 */}
+ <Card className="flex items-center p-4">
+ <div className="flex-1">
+ <h3 className="text-sm font-medium text-muted-foreground">완료율</h3>
+ <div className="text-2xl font-bold">{completionRate}%</div>
+ <p className="text-xs text-muted-foreground">총 {totalTasks}건</p>
+ </div>
+ <TrendingUp className={`h-8 w-8 ${completionRate > 50 ? 'text-green-600' : 'text-orange-600'}`} />
+ </Card>
+
+ {/* 컴팩트 파이 차트 */}
+ <Card className="col-span-2">
+ <CardHeader className="pb-3">
+ <CardTitle className="text-lg">{title}</CardTitle>
+ </CardHeader>
+ <CardContent className="pb-3">
+ <div className="flex items-center gap-6">
+ {/* 작은 파이 차트 */}
+ <div className="flex-shrink-0">
+ <ChartContainer
+ config={chartConfig}
+ className="h-[120px] w-[120px]"
+ >
+ <RechartsPieChart>
+ <ChartTooltip
+ cursor={false}
+ content={<ChartTooltipContent
+ hideLabel
+ formatter={(value, name) => [`${value}건`, name]}
+ />}
+ />
+ <Pie
+ data={pieChartData}
+ cx="50%"
+ cy="50%"
+ outerRadius={50}
+ innerRadius={20} // 도넛 차트로 만들어 공간 절약
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {pieChartData.map((entry, index) => (
+ <Cell key={`cell-${index}`} fill={entry.color} />
+ ))}
+ </Pie>
+ </RechartsPieChart>
+ </ChartContainer>
+ </div>
+
+ {/* 통계 목록 */}
+ <div className="flex-1 grid grid-cols-1 gap-3">
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-gray-500"></div>
+ <span className="text-sm">대기: {totalPending}건</span>
+ </div>
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-blue-500"></div>
+ <span className="text-sm">진행중: {totalInProgress}건</span>
+ </div>
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-green-500"></div>
+ <span className="text-sm">완료: {totalCompleted}건</span>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+
diff --git a/lib/dashboard/dashboard-stats-card.tsx b/lib/dashboard/dashboard-stats-card.tsx
new file mode 100644
index 00000000..4485e8e0
--- /dev/null
+++ b/lib/dashboard/dashboard-stats-card.tsx
@@ -0,0 +1,88 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Progress } from "@/components/ui/progress";
+import { DashboardStats, UserDashboardStats } from "./service";
+
+interface DashboardStatsCardProps {
+ stats: DashboardStats | UserDashboardStats;
+ showUserStats?: boolean;
+}
+
+export function DashboardStatsCard({ stats, showUserStats = false }: DashboardStatsCardProps) {
+ const userStats = showUserStats ? stats as UserDashboardStats : null;
+
+ const completionRate = stats.total > 0 ? Math.round((stats.completed / stats.total) * 100) : 0;
+ const myCompletionRate = userStats && userStats.myTotal > 0
+ ? Math.round((userStats.myCompleted / userStats.myTotal) * 100)
+ : 0;
+
+ return (
+ <Card className="h-full">
+ <CardHeader className="pb-3">
+ <CardTitle className="text-lg font-medium">{stats.displayName}</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {/* 팀 전체 통계 */}
+ <div className="space-y-3">
+ <div className="flex items-center justify-between">
+ <span className="text-sm font-medium text-muted-foreground">팀 전체</span>
+ <span className="text-sm font-bold">{stats.total}건</span>
+ </div>
+
+ <div className="flex gap-2">
+ <Badge variant="secondary" className="text-xs">
+ 대기 {stats.pending}
+ </Badge>
+ <Badge variant="default" className="text-xs bg-blue-500">
+ 진행 {stats.inProgress}
+ </Badge>
+ <Badge variant="default" className="text-xs bg-green-500">
+ 완료 {stats.completed}
+ </Badge>
+ </div>
+
+ <div className="space-y-1">
+ <div className="flex justify-between text-xs">
+ <span>완료율</span>
+ <span>{completionRate}%</span>
+ </div>
+ <Progress value={completionRate} className="h-2" />
+ </div>
+ </div>
+
+ {/* 개인 통계 */}
+ {showUserStats && userStats && (
+ <>
+ <hr className="border-muted" />
+ <div className="space-y-3">
+ <div className="flex items-center justify-between">
+ <span className="text-sm font-medium text-muted-foreground">내 업무</span>
+ <span className="text-sm font-bold">{userStats.myTotal}건</span>
+ </div>
+
+ <div className="flex gap-2">
+ <Badge variant="outline" className="text-xs">
+ 대기 {userStats.myPending}
+ </Badge>
+ <Badge variant="outline" className="text-xs border-blue-500 text-blue-600">
+ 진행 {userStats.myInProgress}
+ </Badge>
+ <Badge variant="outline" className="text-xs border-green-500 text-green-600">
+ 완료 {userStats.myCompleted}
+ </Badge>
+ </div>
+
+ <div className="space-y-1">
+ <div className="flex justify-between text-xs">
+ <span>완료율</span>
+ <span>{myCompletionRate}%</span>
+ </div>
+ <Progress value={myCompletionRate} className="h-2" />
+ </div>
+ </div>
+ </>
+ )}
+ </CardContent>
+ </Card>
+ );
+} \ No newline at end of file
diff --git a/lib/dashboard/dashboard-summary-cards.tsx b/lib/dashboard/dashboard-summary-cards.tsx
new file mode 100644
index 00000000..9d1d9ef2
--- /dev/null
+++ b/lib/dashboard/dashboard-summary-cards.tsx
@@ -0,0 +1,64 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { CheckCircle, Clock, PlayCircle, Users } from "lucide-react";
+import { DashboardData } from "./service";
+
+interface DashboardSummaryCardsProps {
+ summary: DashboardData['summary'];
+}
+
+export function DashboardSummaryCards({ summary }: DashboardSummaryCardsProps) {
+ const cards = [
+ {
+ title: "전체 업무",
+ value: summary.totalTasks,
+ icon: Users,
+ description: `내 업무 ${summary.myTasks}건`,
+ color: "text-blue-600"
+ },
+ {
+ title: "대기중",
+ value: summary.teamPending,
+ icon: Clock,
+ description: `내 대기 ${summary.myPending}건`,
+ color: "text-gray-600"
+ },
+ {
+ title: "진행중",
+ value: summary.teamInProgress,
+ icon: PlayCircle,
+ description: `내 진행 ${summary.myInProgress}건`,
+ color: "text-blue-600"
+ },
+ {
+ title: "완료",
+ value: summary.teamCompleted,
+ icon: CheckCircle,
+ description: `내 완료 ${summary.myCompleted}건`,
+ color: "text-green-600"
+ }
+ ];
+
+ return (
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ {cards.map((card, index) => {
+ const Icon = card.icon;
+ return (
+ <Card key={index}>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">
+ {card.title}
+ </CardTitle>
+ <Icon className={`h-4 w-4 ${card.color}`} />
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">{card.value}</div>
+ <p className="text-xs text-muted-foreground">
+ {card.description}
+ </p>
+ </CardContent>
+ </Card>
+ );
+ })}
+ </div>
+ );
+} \ No newline at end of file
diff --git a/lib/dashboard/partners-service.ts b/lib/dashboard/partners-service.ts
new file mode 100644
index 00000000..327a16a9
--- /dev/null
+++ b/lib/dashboard/partners-service.ts
@@ -0,0 +1,447 @@
+"use server";
+
+import db from "@/db/db";
+import { sql } from "drizzle-orm";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { getPartnerTablesByDomain } from "@/config/partners-dashboard-table";
+import { TableConfig } from "@/types/dashboard";
+
+export interface PartnersDashboardStats {
+ tableName: string;
+ displayName: string;
+ total: number;
+ pending: number;
+ inProgress: number;
+ completed: number;
+}
+
+export interface PartnersUserDashboardStats extends PartnersDashboardStats {
+ myTotal: number;
+ myPending: number;
+ myInProgress: number;
+ myCompleted: number;
+}
+
+export interface PartnersDashboardData {
+ domain: string;
+ companyId: string;
+ teamStats: PartnersDashboardStats[];
+ userStats: PartnersUserDashboardStats[];
+ summary: {
+ totalTasks: number;
+ myTasks: number;
+ teamPending: number;
+ teamInProgress: number;
+ teamCompleted: number;
+ myPending: number;
+ myInProgress: number;
+ myCompleted: number;
+ };
+}
+
+// Partners 팀 대시보드 데이터 조회 (회사 필터링 포함)
+export async function getPartnersTeamDashboardData(domain: string): Promise<PartnersDashboardStats[]> {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.companyId) {
+ throw new Error("회사 정보가 없습니다.");
+ }
+
+ const companyId = session.user.companyId;
+ const tables = getPartnerTablesByDomain(domain);
+
+ if (tables.length === 0) {
+ console.warn(`파트너 도메인 '${domain}'에 대한 테이블이 없습니다.`);
+ return [];
+ }
+
+ console.log(`👥 회사 ID: ${companyId}로 파트너 데이터 조회`);
+
+ // 병렬 처리로 성능 향상
+ const results = await Promise.allSettled(
+ tables.map(tableConfig => getPartnersTableStats(tableConfig, companyId))
+ );
+
+ // 성공한 결과만 반환
+ const successfulResults: PartnersDashboardStats[] = [];
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled') {
+ successfulResults.push(result.value);
+ } else {
+ console.error(`파트너 테이블 ${tables[index].tableName} 통계 조회 실패:`, result.reason);
+ }
+ });
+
+ console.log('📊 파트너 팀 대시보드 결과:', successfulResults);
+ return successfulResults;
+ } catch (error) {
+ console.error("파트너 팀 대시보드 데이터 조회 실패:", error);
+ throw new Error("파트너 팀 대시보드 데이터를 불러오는데 실패했습니다.");
+ }
+}
+
+// Partners 사용자 대시보드 데이터 조회
+export async function getPartnersUserDashboardData(domain: string): Promise<PartnersUserDashboardStats[]> {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id || !session?.user?.companyId) {
+ throw new Error("사용자 또는 회사 정보가 없습니다.");
+ }
+
+ const userId = session.user.id;
+ const companyId = session.user.companyId;
+ const tables = getPartnerTablesByDomain(domain);
+
+ if (tables.length === 0) {
+ console.warn(`파트너 도메인 '${domain}'에 대한 테이블이 없습니다.`);
+ return [];
+ }
+
+ console.log(`👤 사용자 ID: ${userId}, 회사 ID: ${companyId}`);
+
+ // 병렬 처리로 성능 향상
+ const results = await Promise.allSettled(
+ tables.map(async (tableConfig) => {
+ const [teamStats, userStats] = await Promise.all([
+ getPartnersTableStats(tableConfig, companyId),
+ getPartnersUserTableStats(tableConfig, companyId, userId)
+ ]);
+
+ return {
+ ...teamStats,
+ myTotal: userStats.total,
+ myPending: userStats.pending,
+ myInProgress: userStats.inProgress,
+ myCompleted: userStats.completed
+ } as PartnersUserDashboardStats;
+ })
+ );
+
+ // 성공한 결과만 반환
+ const successfulResults: PartnersUserDashboardStats[] = [];
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled') {
+ successfulResults.push(result.value);
+ } else {
+ console.error(`파트너 테이블 ${tables[index].tableName} 사용자 통계 조회 실패:`, result.reason);
+ }
+ });
+
+ return successfulResults;
+ } catch (error) {
+ console.error("파트너 사용자 대시보드 데이터 조회 실패:", error);
+ throw new Error("파트너 사용자 대시보드 데이터를 불러오는데 실패했습니다.");
+ }
+}
+
+// Partners 전체 대시보드 데이터 조회
+export async function getPartnersDashboardData(domain: string): Promise<PartnersDashboardData> {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id || !session?.user?.companyId) {
+ throw new Error("사용자 또는 회사 정보가 없습니다.");
+ }
+
+ const [teamStats, userStats] = await Promise.all([
+ getPartnersTeamDashboardData(domain),
+ getPartnersUserDashboardData(domain)
+ ]);
+
+ // 요약 통계 계산
+ const summary = {
+ totalTasks: teamStats.reduce((sum, stat) => sum + stat.total, 0),
+ myTasks: userStats.reduce((sum, stat) => sum + stat.myTotal, 0),
+ teamPending: teamStats.reduce((sum, stat) => sum + stat.pending, 0),
+ teamInProgress: teamStats.reduce((sum, stat) => sum + stat.inProgress, 0),
+ teamCompleted: teamStats.reduce((sum, stat) => sum + stat.completed, 0),
+ myPending: userStats.reduce((sum, stat) => sum + stat.myPending, 0),
+ myInProgress: userStats.reduce((sum, stat) => sum + stat.myInProgress, 0),
+ myCompleted: userStats.reduce((sum, stat) => sum + stat.myCompleted, 0)
+ };
+
+ return {
+ domain,
+ companyId: session.user.companyId,
+ teamStats,
+ userStats,
+ summary
+ };
+ } catch (error) {
+ console.error("파트너 대시보드 데이터 조회 실패:", error);
+ throw new Error("파트너 대시보드 데이터를 불러오는데 실패했습니다.");
+ }
+}
+
+// Partners 테이블별 전체 통계 조회 (회사 필터링 포함)
+async function getPartnersTableStats(config: TableConfig, companyId: string): Promise<PartnersDashboardStats> {
+ try {
+ console.log(`\n🔍 파트너 테이블 ${config.tableName} 통계 조회 (회사: ${companyId})`);
+
+ // 1단계: 회사별 총 개수 확인
+ const totalQuery = `
+ SELECT COUNT(*)::INTEGER as total
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}'
+ `;
+ console.log("Total SQL:", totalQuery);
+
+ const totalResult = await db.execute(sql.raw(totalQuery));
+ console.log("Total 결과:", totalResult.rows[0]);
+
+ // 2단계: 회사별 상태값 분포 확인
+ const statusQuery = `
+ SELECT "${config.statusField}" as status, COUNT(*) as count
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND "${config.statusField}" IS NOT NULL
+ GROUP BY "${config.statusField}"
+ ORDER BY count DESC
+ `;
+ console.log("Status SQL:", statusQuery);
+
+ const statusResult = await db.execute(sql.raw(statusQuery));
+ console.log("Status 결과:", statusResult.rows);
+
+ // 3단계: 상태별 개수 조회
+ const pendingValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'pending')
+ .map(([original]) => original);
+
+ const inProgressValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'in_progress')
+ .map(([original]) => original);
+
+ const completedValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'completed')
+ .map(([original]) => original);
+
+ console.log("파트너 상태 매핑:");
+ console.log("- pending:", pendingValues);
+ console.log("- inProgress:", inProgressValues);
+ console.log("- completed:", completedValues);
+
+ let pendingCount = 0;
+ let inProgressCount = 0;
+ let completedCount = 0;
+
+ // Pending 개수 (회사 필터 포함)
+ if (pendingValues.length > 0) {
+ const pendingValuesList = pendingValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const pendingQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND "${config.statusField}" IN (${pendingValuesList})
+ `;
+
+ const pendingResult = await db.execute(sql.raw(pendingQuery));
+ pendingCount = parseInt(pendingResult.rows[0]?.count || '0');
+ console.log("Pending 개수:", pendingCount);
+ }
+
+ // In Progress 개수 (회사 필터 포함)
+ if (inProgressValues.length > 0) {
+ const inProgressValuesList = inProgressValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const inProgressQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND "${config.statusField}" IN (${inProgressValuesList})
+ `;
+
+ const inProgressResult = await db.execute(sql.raw(inProgressQuery));
+ inProgressCount = parseInt(inProgressResult.rows[0]?.count || '0');
+ console.log("InProgress 개수:", inProgressCount);
+ }
+
+ // Completed 개수 (회사 필터 포함)
+ if (completedValues.length > 0) {
+ const completedValuesList = completedValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const completedQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND "${config.statusField}" IN (${completedValuesList})
+ `;
+
+ const completedResult = await db.execute(sql.raw(completedQuery));
+ completedCount = parseInt(completedResult.rows[0]?.count || '0');
+ console.log("Completed 개수:", completedCount);
+ }
+
+ const stats = {
+ tableName: config.tableName,
+ displayName: config.displayName,
+ total: parseInt(totalResult.rows[0]?.total || '0'),
+ pending: pendingCount,
+ inProgress: inProgressCount,
+ completed: completedCount
+ };
+
+ console.log(`✅ 파트너 ${config.tableName} 최종 통계:`, stats);
+ return stats;
+ } catch (error) {
+ console.error(`❌ 파트너 테이블 ${config.tableName} 통계 조회 중 오류:`, error);
+ return createEmptyPartnersStats(config);
+ }
+}
+
+// Partners 사용자별 테이블 통계 조회 (회사 + 사용자 필터링)
+async function getPartnersUserTableStats(config: TableConfig, companyId: string, userId: string): Promise<PartnersDashboardStats> {
+ try {
+ // 사용자 필드가 없는 경우 빈 통계 반환
+ if (!hasUserFields(config)) {
+ console.log(`⚠️ 파트너 테이블 ${config.tableName}에 사용자 필드가 없습니다.`);
+ return createEmptyPartnersStats(config);
+ }
+
+ console.log(`\n👤 파트너 사용자 ${userId}의 ${config.tableName} 통계 조회 (회사: ${companyId})`);
+
+ // 사용자 조건 생성 (회사 필터 포함)
+ const userConditions = [];
+ if (config.userFields.creator) {
+ userConditions.push(`"${config.userFields.creator}" = '${userId}'`);
+ }
+ if (config.userFields.updater) {
+ userConditions.push(`"${config.userFields.updater}" = '${userId}'`);
+ }
+ if (config.userFields.assignee) {
+ userConditions.push(`"${config.userFields.assignee}" = '${userId}'`);
+ }
+
+ if (userConditions.length === 0) {
+ return createEmptyPartnersStats(config);
+ }
+
+ const userConditionStr = userConditions.join(' OR ');
+
+ // 1. 사용자 + 회사 총 개수
+ const userTotalQuery = `
+ SELECT COUNT(*)::INTEGER as total
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND (${userConditionStr})
+ `;
+ console.log("User Total SQL:", userTotalQuery);
+
+ const userTotalResult = await db.execute(sql.raw(userTotalQuery));
+ console.log("User Total 결과:", userTotalResult.rows[0]);
+
+ // 2. 사용자 + 회사 상태별 개수
+ const pendingValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'pending')
+ .map(([original]) => original);
+
+ const inProgressValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'in_progress')
+ .map(([original]) => original);
+
+ const completedValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'completed')
+ .map(([original]) => original);
+
+ let userPendingCount = 0;
+ let userInProgressCount = 0;
+ let userCompletedCount = 0;
+
+ // User + Company Pending 개수
+ if (pendingValues.length > 0) {
+ const pendingValuesList = pendingValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const userPendingQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND (${userConditionStr}) AND "${config.statusField}" IN (${pendingValuesList})
+ `;
+
+ const userPendingResult = await db.execute(sql.raw(userPendingQuery));
+ userPendingCount = parseInt(userPendingResult.rows[0]?.count || '0');
+ console.log("User Pending 개수:", userPendingCount);
+ }
+
+ // User + Company In Progress 개수
+ if (inProgressValues.length > 0) {
+ const inProgressValuesList = inProgressValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const userInProgressQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND (${userConditionStr}) AND "${config.statusField}" IN (${inProgressValuesList})
+ `;
+
+ const userInProgressResult = await db.execute(sql.raw(userInProgressQuery));
+ userInProgressCount = parseInt(userInProgressResult.rows[0]?.count || '0');
+ console.log("User InProgress 개수:", userInProgressCount);
+ }
+
+ // User + Company Completed 개수
+ if (completedValues.length > 0) {
+ const completedValuesList = completedValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const userCompletedQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "vendor_id" = '${companyId}' AND (${userConditionStr}) AND "${config.statusField}" IN (${completedValuesList})
+ `;
+
+ const userCompletedResult = await db.execute(sql.raw(userCompletedQuery));
+ userCompletedCount = parseInt(userCompletedResult.rows[0]?.count || '0');
+ console.log("User Completed 개수:", userCompletedCount);
+ }
+
+ const stats = {
+ tableName: config.tableName,
+ displayName: config.displayName,
+ total: parseInt(userTotalResult.rows[0]?.total || '0'),
+ pending: userPendingCount,
+ inProgress: userInProgressCount,
+ completed: userCompletedCount
+ };
+
+ console.log(`✅ 파트너 사용자 ${config.tableName} 최종 통계:`, stats);
+ return stats;
+ } catch (error) {
+ console.error(`❌ 파트너 테이블 ${config.tableName} 사용자 통계 조회 중 오류:`, error);
+ return createEmptyPartnersStats(config);
+ }
+}
+
+// 유틸리티 함수들
+function createEmptyPartnersStats(config: TableConfig): PartnersDashboardStats {
+ return {
+ tableName: config.tableName,
+ displayName: config.displayName,
+ total: 0,
+ pending: 0,
+ inProgress: 0,
+ completed: 0
+ };
+}
+
+function hasUserFields(config: TableConfig): boolean {
+ return !!(config.userFields.creator || config.userFields.updater || config.userFields.assignee);
+}
+
+// 디버깅 함수: Partners 전용
+export async function simplePartnersTest(tableName: string, statusField: string, companyId: string) {
+ try {
+ console.log(`\n🧪 파트너 ${tableName} 간단한 테스트 (회사: ${companyId}):`);
+
+ // 1. 회사별 총 개수
+ const totalQuery = `SELECT COUNT(*) as total FROM "${tableName}" WHERE "vendor_id" = '${companyId}'`;
+ const totalResult = await db.execute(sql.raw(totalQuery));
+ console.log("회사별 총 개수:", totalResult.rows[0]);
+
+ // 2. 회사별 상태 분포
+ const statusQuery = `
+ SELECT "${statusField}" as status, COUNT(*) as count
+ FROM "${tableName}"
+ WHERE "vendor_id" = '${companyId}'
+ GROUP BY "${statusField}"
+ ORDER BY count DESC
+ `;
+ const statusResult = await db.execute(sql.raw(statusQuery));
+ console.log("회사별 상태 분포:", statusResult.rows);
+
+ return {
+ total: totalResult.rows[0],
+ statusDistribution: statusResult.rows
+ };
+ } catch (error) {
+ console.error("파트너 간단한 테스트 실패:", error);
+ return null;
+ }
+} \ No newline at end of file
diff --git a/lib/dashboard/service.ts b/lib/dashboard/service.ts
new file mode 100644
index 00000000..16b05d45
--- /dev/null
+++ b/lib/dashboard/service.ts
@@ -0,0 +1,454 @@
+"use server";
+
+import db from "@/db/db";
+import { sql, eq, or, and, count, sum, inArray } from "drizzle-orm";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { getTablesByDomain } from "@/config/dashboard-table";
+import { TableConfig } from "@/types/dashboard";
+
+export interface DashboardStats {
+ tableName: string;
+ displayName: string;
+ total: number;
+ pending: number;
+ inProgress: number;
+ completed: number;
+}
+
+export interface UserDashboardStats extends DashboardStats {
+ myTotal: number;
+ myPending: number;
+ myInProgress: number;
+ myCompleted: number;
+}
+
+export interface DashboardData {
+ domain: string;
+ teamStats: DashboardStats[];
+ userStats: UserDashboardStats[];
+ summary: {
+ totalTasks: number;
+ myTasks: number;
+ teamPending: number;
+ teamInProgress: number;
+ teamCompleted: number;
+ myPending: number;
+ myInProgress: number;
+ myCompleted: number;
+ };
+}
+
+// 팀 대시보드 데이터 조회
+export async function getTeamDashboardData(domain: string): Promise<DashboardStats[]> {
+ try {
+ const tables = getTablesByDomain(domain);
+
+ if (tables.length === 0) {
+ console.warn(`도메인 '${domain}'에 대한 테이블이 없습니다.`);
+ return [];
+ }
+
+ // 병렬 처리로 성능 향상
+ const results = await Promise.allSettled(
+ tables.map(tableConfig => getTableStats(tableConfig))
+ );
+
+ // 성공한 결과만 반환, 실패한 것들은 로그 출력
+ const successfulResults: DashboardStats[] = [];
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled') {
+ successfulResults.push(result.value);
+ } else {
+ console.error(`테이블 ${tables[index].tableName} 통계 조회 실패:`, result.reason);
+ }
+ });
+
+ console.log('📊 팀 대시보드 결과:', successfulResults);
+
+ return successfulResults;
+ } catch (error) {
+ console.error("팀 대시보드 데이터 조회 실패:", error);
+ throw new Error("팀 대시보드 데이터를 불러오는데 실패했습니다.");
+ }
+}
+
+// 사용자 대시보드 데이터 조회
+export async function getUserDashboardData(domain: string): Promise<UserDashboardStats[]> {
+ try {
+ // 현재 사용자 정보 가져오기
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ throw new Error("인증되지 않은 사용자입니다.");
+ }
+
+ const userId = session.user.id;
+ const tables = getTablesByDomain(domain);
+
+ if (tables.length === 0) {
+ console.warn(`도메인 '${domain}'에 대한 테이블이 없습니다.`);
+ return [];
+ }
+
+ console.log(`👤 사용자 ID: ${userId}`);
+
+ // 병렬 처리로 성능 향상
+ const results = await Promise.allSettled(
+ tables.map(async (tableConfig) => {
+ const [teamStats, userStats] = await Promise.all([
+ getTableStats(tableConfig),
+ getUserTableStats(tableConfig, userId)
+ ]);
+
+ return {
+ ...teamStats,
+ myTotal: userStats.total,
+ myPending: userStats.pending,
+ myInProgress: userStats.inProgress,
+ myCompleted: userStats.completed
+ } as UserDashboardStats;
+ })
+ );
+
+ // 성공한 결과만 반환
+ const successfulResults: UserDashboardStats[] = [];
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled') {
+ successfulResults.push(result.value);
+ } else {
+ console.error(`테이블 ${tables[index].tableName} 사용자 통계 조회 실패:`, result.reason);
+ }
+ });
+
+ return successfulResults;
+ } catch (error) {
+ console.error("사용자 대시보드 데이터 조회 실패:", error);
+ throw new Error("사용자 대시보드 데이터를 불러오는데 실패했습니다.");
+ }
+}
+
+// 전체 대시보드 데이터 조회 (팀 + 개인)
+export async function getDashboardData(domain: string): Promise<DashboardData> {
+ try {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.id) {
+ throw new Error("인증되지 않은 사용자입니다.");
+ }
+
+ // 병렬 처리로 성능 향상
+ const [teamStats, userStats] = await Promise.all([
+ getTeamDashboardData(domain),
+ getUserDashboardData(domain)
+ ]);
+
+ // 요약 통계 계산
+ const summary = {
+ totalTasks: teamStats.reduce((sum, stat) => sum + stat.total, 0),
+ myTasks: userStats.reduce((sum, stat) => sum + stat.myTotal, 0),
+ teamPending: teamStats.reduce((sum, stat) => sum + stat.pending, 0),
+ teamInProgress: teamStats.reduce((sum, stat) => sum + stat.inProgress, 0),
+ teamCompleted: teamStats.reduce((sum, stat) => sum + stat.completed, 0),
+ myPending: userStats.reduce((sum, stat) => sum + stat.myPending, 0),
+ myInProgress: userStats.reduce((sum, stat) => sum + stat.myInProgress, 0),
+ myCompleted: userStats.reduce((sum, stat) => sum + stat.myCompleted, 0)
+ };
+
+ return {
+ domain,
+ teamStats,
+ userStats,
+ summary
+ };
+ } catch (error) {
+ console.error("대시보드 데이터 조회 실패:", error);
+ throw new Error("대시보드 데이터를 불러오는데 실패했습니다.");
+ }
+}
+
+// 테이블별 전체 통계 조회 (완전히 수정된 버전)
+async function getTableStats(config: TableConfig): Promise<DashboardStats> {
+ try {
+ console.log(`\n🔍 테이블 ${config.tableName} 통계 조회 시작`);
+
+ // 1단계: 기본 총 개수 확인
+ console.log("1단계: 총 개수 조회");
+ const totalQuery = `SELECT COUNT(*)::INTEGER as total FROM "${config.tableName}"`;
+ console.log("Total SQL:", totalQuery);
+
+ const totalResult = await db.execute(sql.raw(totalQuery));
+ console.log("Total 결과:", totalResult.rows[0]);
+
+ // 2단계: 실제 상태값 확인
+ console.log("2단계: 상태값 분포 확인");
+ const statusQuery = `
+ SELECT "${config.statusField}" as status, COUNT(*) as count
+ FROM "${config.tableName}"
+ WHERE "${config.statusField}" IS NOT NULL
+ GROUP BY "${config.statusField}"
+ ORDER BY count DESC
+ `;
+ console.log("Status SQL:", statusQuery);
+
+ const statusResult = await db.execute(sql.raw(statusQuery));
+ console.log("Status 결과:", statusResult.rows);
+
+ // 3단계: 상태별 개수 조회 (개별 쿼리)
+ console.log("3단계: 상태별 개수 조회");
+
+ const pendingValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'pending')
+ .map(([original]) => original);
+
+ const inProgressValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'in_progress')
+ .map(([original]) => original);
+
+ const completedValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'completed')
+ .map(([original]) => original);
+
+ console.log("매핑된 상태값:");
+ console.log("- pending:", pendingValues);
+ console.log("- inProgress:", inProgressValues);
+ console.log("- completed:", completedValues);
+
+ // 개별 쿼리로 정확한 개수 조회
+ let pendingCount = 0;
+ let inProgressCount = 0;
+ let completedCount = 0;
+
+ // Pending 개수
+ if (pendingValues.length > 0) {
+ const pendingValuesList = pendingValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const pendingQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "${config.statusField}" IN (${pendingValuesList})
+ `;
+ console.log("Pending SQL:", pendingQuery);
+
+ const pendingResult = await db.execute(sql.raw(pendingQuery));
+ pendingCount = parseInt(pendingResult.rows[0]?.count || '0');
+ console.log("Pending 개수:", pendingCount);
+ }
+
+ // In Progress 개수
+ if (inProgressValues.length > 0) {
+ const inProgressValuesList = inProgressValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const inProgressQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "${config.statusField}" IN (${inProgressValuesList})
+ `;
+ console.log("InProgress SQL:", inProgressQuery);
+
+ const inProgressResult = await db.execute(sql.raw(inProgressQuery));
+ inProgressCount = parseInt(inProgressResult.rows[0]?.count || '0');
+ console.log("InProgress 개수:", inProgressCount);
+ }
+
+ // Completed 개수
+ if (completedValues.length > 0) {
+ const completedValuesList = completedValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const completedQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE "${config.statusField}" IN (${completedValuesList})
+ `;
+ console.log("Completed SQL:", completedQuery);
+
+ const completedResult = await db.execute(sql.raw(completedQuery));
+ completedCount = parseInt(completedResult.rows[0]?.count || '0');
+ console.log("Completed 개수:", completedCount);
+ }
+
+ const stats = {
+ tableName: config.tableName,
+ displayName: config.displayName,
+ total: parseInt(totalResult.rows[0]?.total || '0'),
+ pending: pendingCount,
+ inProgress: inProgressCount,
+ completed: completedCount
+ };
+
+ console.log(`✅ ${config.tableName} 최종 통계:`, stats);
+ return stats;
+ } catch (error) {
+ console.error(`❌ 테이블 ${config.tableName} 통계 조회 중 오류:`, error);
+ // 에러 발생 시 빈 통계 반환
+ return createEmptyStats(config);
+ }
+}
+
+// 사용자별 테이블 통계 조회 (수정된 버전)
+async function getUserTableStats(config: TableConfig, userId: string): Promise<DashboardStats> {
+ try {
+ // 사용자 필드가 없는 경우 빈 통계 반환
+ if (!hasUserFields(config)) {
+ console.log(`⚠️ 테이블 ${config.tableName}에 사용자 필드가 없습니다.`);
+ return createEmptyStats(config);
+ }
+
+ console.log(`\n👤 사용자 ${userId}의 ${config.tableName} 통계 조회`);
+
+ // 사용자 조건 생성
+ const userConditions = [];
+ if (config.userFields.creator) {
+ userConditions.push(`"${config.userFields.creator}" = '${userId}'`);
+ }
+ if (config.userFields.updater) {
+ userConditions.push(`"${config.userFields.updater}" = '${userId}'`);
+ }
+ if (config.userFields.assignee) {
+ userConditions.push(`"${config.userFields.assignee}" = '${userId}'`);
+ }
+
+ if (userConditions.length === 0) {
+ return createEmptyStats(config);
+ }
+
+ const userConditionStr = userConditions.join(' OR ');
+
+ // 1. 사용자 총 개수
+ const userTotalQuery = `
+ SELECT COUNT(*)::INTEGER as total
+ FROM "${config.tableName}"
+ WHERE ${userConditionStr}
+ `;
+ console.log("User Total SQL:", userTotalQuery);
+
+ const userTotalResult = await db.execute(sql.raw(userTotalQuery));
+ console.log("User Total 결과:", userTotalResult.rows[0]);
+
+ // 2. 사용자 상태별 개수
+ const pendingValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'pending')
+ .map(([original]) => original);
+
+ const inProgressValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'in_progress')
+ .map(([original]) => original);
+
+ const completedValues = Object.entries(config.statusMapping)
+ .filter(([_, mapped]) => mapped === 'completed')
+ .map(([original]) => original);
+
+ let userPendingCount = 0;
+ let userInProgressCount = 0;
+ let userCompletedCount = 0;
+
+ // User Pending 개수
+ if (pendingValues.length > 0) {
+ const pendingValuesList = pendingValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const userPendingQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE (${userConditionStr}) AND "${config.statusField}" IN (${pendingValuesList})
+ `;
+
+ const userPendingResult = await db.execute(sql.raw(userPendingQuery));
+ userPendingCount = parseInt(userPendingResult.rows[0]?.count || '0');
+ console.log("User Pending 개수:", userPendingCount);
+ }
+
+ // User In Progress 개수
+ if (inProgressValues.length > 0) {
+ const inProgressValuesList = inProgressValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const userInProgressQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE (${userConditionStr}) AND "${config.statusField}" IN (${inProgressValuesList})
+ `;
+
+ const userInProgressResult = await db.execute(sql.raw(userInProgressQuery));
+ userInProgressCount = parseInt(userInProgressResult.rows[0]?.count || '0');
+ console.log("User InProgress 개수:", userInProgressCount);
+ }
+
+ // User Completed 개수
+ if (completedValues.length > 0) {
+ const completedValuesList = completedValues.map(v => `'${v.replace(/'/g, "''")}'`).join(',');
+ const userCompletedQuery = `
+ SELECT COUNT(*)::INTEGER as count
+ FROM "${config.tableName}"
+ WHERE (${userConditionStr}) AND "${config.statusField}" IN (${completedValuesList})
+ `;
+
+ const userCompletedResult = await db.execute(sql.raw(userCompletedQuery));
+ userCompletedCount = parseInt(userCompletedResult.rows[0]?.count || '0');
+ console.log("User Completed 개수:", userCompletedCount);
+ }
+
+ const stats = {
+ tableName: config.tableName,
+ displayName: config.displayName,
+ total: parseInt(userTotalResult.rows[0]?.total || '0'),
+ pending: userPendingCount,
+ inProgress: userInProgressCount,
+ completed: userCompletedCount
+ };
+
+ console.log(`✅ 사용자 ${config.tableName} 최종 통계:`, stats);
+ return stats;
+ } catch (error) {
+ console.error(`❌ 테이블 ${config.tableName} 사용자 통계 조회 중 오류:`, error);
+ return createEmptyStats(config);
+ }
+}
+
+// 유틸리티 함수들
+function createEmptyStats(config: TableConfig): DashboardStats {
+ return {
+ tableName: config.tableName,
+ displayName: config.displayName,
+ total: 0,
+ pending: 0,
+ inProgress: 0,
+ completed: 0
+ };
+}
+
+function hasUserFields(config: TableConfig): boolean {
+ return !!(config.userFields.creator || config.userFields.updater || config.userFields.assignee);
+}
+
+// 디버깅 함수: 단순한 테스트
+export async function simpleTest(tableName: string, statusField: string) {
+ try {
+ console.log(`\n🧪 ${tableName} 간단한 테스트:`);
+
+ // 1. 총 개수
+ const totalQuery = `SELECT COUNT(*) as total FROM "${tableName}"`;
+ const totalResult = await db.execute(sql.raw(totalQuery));
+ console.log("총 개수:", totalResult.rows[0]);
+
+ // 2. 상태 분포
+ const statusQuery = `
+ SELECT "${statusField}" as status, COUNT(*) as count
+ FROM "${tableName}"
+ GROUP BY "${statusField}"
+ ORDER BY count DESC
+ `;
+ const statusResult = await db.execute(sql.raw(statusQuery));
+ console.log("상태 분포:", statusResult.rows);
+
+ // 3. 특정 상태 테스트
+ const draftQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${statusField}" = 'DRAFT'`;
+ const draftResult = await db.execute(sql.raw(draftQuery));
+ console.log("DRAFT 개수:", draftResult.rows[0]);
+
+ const docConfirmedQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${statusField}" = 'Doc. Confirmed'`;
+ const docConfirmedResult = await db.execute(sql.raw(docConfirmedQuery));
+ console.log("Doc. Confirmed 개수:", docConfirmedResult.rows[0]);
+
+ return {
+ total: totalResult.rows[0],
+ statusDistribution: statusResult.rows,
+ draft: draftResult.rows[0],
+ docConfirmed: docConfirmedResult.rows[0]
+ };
+ } catch (error) {
+ console.error("간단한 테스트 실패:", error);
+ return null;
+ }
+} \ No newline at end of file