diff options
Diffstat (limited to 'lib/dashboard')
| -rw-r--r-- | lib/dashboard/dashboard-client.tsx | 115 | ||||
| -rw-r--r-- | lib/dashboard/dashboard-overview-chart.tsx | 325 | ||||
| -rw-r--r-- | lib/dashboard/dashboard-stats-card.tsx | 88 | ||||
| -rw-r--r-- | lib/dashboard/dashboard-summary-cards.tsx | 64 | ||||
| -rw-r--r-- | lib/dashboard/partners-service.ts | 447 | ||||
| -rw-r--r-- | lib/dashboard/service.ts | 454 |
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 |
