Procházet zdrojové kódy

+ 대시보드 차트 수정

송용우 před 4 měsíci
rodič
revize
66e70b521e
1 změnil soubory, kde provedl 250 přidání a 136 odebrání
  1. 250 136
      pages/view/vendor/dashboard/index.vue

+ 250 - 136
pages/view/vendor/dashboard/index.vue

@@ -58,7 +58,7 @@
             <h3>총 매출</h3>
             <i class="icon sales"></i>
           </div>
-          <div class="kpi--value">{{ formatCurrency(metrics.totalSales) }}</div>
+          <div class="kpi--value">{{ formatCurrency(animatedMetrics.totalSales) }}</div>
           <div class="kpi--change" :class="{ 'positive': metrics.salesChange >= 0, 'negative': metrics.salesChange < 0 }">
             {{ metrics.salesChange >= 0 ? '+' : '' }}{{ metrics.salesChange }}% 전월 대비
           </div>
@@ -68,7 +68,7 @@
             <h3>총 주문</h3>
             <i class="icon orders"></i>
           </div>
-          <div class="kpi--value">{{ metrics.totalOrders }}개</div>
+          <div class="kpi--value">{{ Math.round(animatedMetrics.totalOrders) }}개</div>
           <div class="kpi--change" :class="{ 'positive': metrics.ordersChange >= 0, 'negative': metrics.ordersChange < 0 }">
             {{ metrics.ordersChange >= 0 ? '+' : '' }}{{ metrics.ordersChange }}% 전월 대비
           </div>
@@ -78,7 +78,7 @@
             <h3>정산 완료율</h3>
             <i class="icon settlement"></i>
           </div>
-          <div class="kpi--value">{{ metrics.settlementRate }}%</div>
+          <div class="kpi--value">{{ animatedMetrics.settlementRate.toFixed(1) }}%</div>
           <div class="kpi--change" :class="{ 'positive': metrics.settlementChange >= 0, 'negative': metrics.settlementChange < 0 }">
             {{ metrics.settlementChange >= 0 ? '+' : '' }}{{ metrics.settlementChange }}%p 전월 대비
           </div>
@@ -88,7 +88,7 @@
             <h3>평균 주문액</h3>
             <i class="icon avg"></i>
           </div>
-          <div class="kpi--value">{{ formatCurrency(metrics.avgOrderValue) }}</div>
+          <div class="kpi--value">{{ formatCurrency(animatedMetrics.avgOrderValue) }}</div>
           <div class="kpi--change" :class="{ 'positive': metrics.avgOrderChange >= 0, 'negative': metrics.avgOrderChange < 0 }">
             {{ metrics.avgOrderChange >= 0 ? '+' : '' }}{{ metrics.avgOrderChange }}% 전월 대비
           </div>
@@ -207,6 +207,10 @@
     Tooltip,
     Legend,
     ArcElement,
+    LineController,
+    BarController,
+    DoughnutController,
+    PieController,
   } from 'chart.js';
   
   ChartJS.register(
@@ -218,7 +222,11 @@
     Title,
     Tooltip,
     Legend,
-    ArcElement
+    ArcElement,
+    LineController,
+    BarController,
+    DoughnutController,
+    PieController
   );
 
   /************************************************************************
@@ -257,6 +265,12 @@
   const settlementChartKey = ref(0);
   const categoryChartKey = ref(0);
 
+  // 차트 인스턴스 저장
+  let salesChartInstance = null;
+  let ordersChartInstance = null;
+  let settlementChartInstance = null;
+  let categoryChartInstance = null;
+
   // 메트릭 데이터 (초기값은 0으로 설정)
   const metrics = ref({
     totalSales: 0,
@@ -269,6 +283,14 @@
     avgOrderChange: 0
   });
 
+  // 애니메이션용 메트릭 데이터
+  const animatedMetrics = ref({
+    totalSales: 0,
+    totalOrders: 0,
+    settlementRate: 0,
+    avgOrderValue: 0
+  });
+
   // 테이블 관련
   const tableHeaders = ref([
     { title: '주문번호', key: 'orderNo', sortable: true },
@@ -296,6 +318,39 @@
     }).format(amount);
   };
 
+  // 카운트업 애니메이션 함수
+  const animateCountUp = (target, property, duration = 2000) => {
+    const startValue = animatedMetrics.value[property];
+    const endValue = target;
+    const startTime = Date.now();
+    
+    const updateCount = () => {
+      const now = Date.now();
+      const elapsed = now - startTime;
+      const progress = Math.min(elapsed / duration, 1);
+      
+      // easeOutQuart 이징 함수 사용
+      const easedProgress = 1 - Math.pow(1 - progress, 4);
+      
+      const currentValue = startValue + (endValue - startValue) * easedProgress;
+      animatedMetrics.value[property] = currentValue;
+      
+      if (progress < 1) {
+        requestAnimationFrame(updateCount);
+      }
+    };
+    
+    requestAnimationFrame(updateCount);
+  };
+
+  // 모든 메트릭 애니메이션 시작
+  const startMetricsAnimation = () => {
+    animateCountUp(metrics.value.totalSales, 'totalSales', 1500);      // 금액: 1.5초로 단축
+    animateCountUp(metrics.value.totalOrders, 'totalOrders', 2000);
+    animateCountUp(metrics.value.settlementRate, 'settlementRate', 2200);
+    animateCountUp(metrics.value.avgOrderValue, 'avgOrderValue', 1300); // 평균 주문액: 1.3초로 단축
+  };
+
   // 날짜 범위 변경
   const onDateRangeChange = (range) => {
     selectedPeriod.value = 'custom';
@@ -366,150 +421,205 @@
 
   // 차트 생성
   const createSalesChart = () => {
-    if (!salesChart.value) return;
-    
-    const ctx = salesChart.value.getContext('2d');
-    // 실제 데이터가 있으면 사용, 없으면 빈 차트
-    const salesData = metrics.value.totalSales > 0 ? 
-      [0, 0, 0, 0, 0, metrics.value.totalSales] : 
-      [0, 0, 0, 0, 0, 0];
+    if (!salesChart.value) {
+      console.log('salesChart ref가 없습니다');
+      return;
+    }
     
-    new ChartJS(ctx, {
-      type: salesChartType.value,
-      data: {
-        labels: ['1월', '2월', '3월', '4월', '5월', '6월'],
-        datasets: [{
-          label: '매출',
-          data: salesData,
-          borderColor: '#3f51b5',
-          backgroundColor: salesChartType.value === 'bar' ? '#3f51b5' : 'rgba(63, 81, 181, 0.1)',
-          borderWidth: 2,
-          fill: salesChartType.value === 'line'
-        }]
-      },
-      options: {
-        responsive: true,
-        maintainAspectRatio: false,
-        plugins: {
-          legend: {
-            display: false
-          }
+    try {
+      // 기존 차트 인스턴스 삭제
+      if (salesChartInstance) {
+        salesChartInstance.destroy();
+        salesChartInstance = null;
+      }
+      
+      // Canvas 컨텍스트 직접 가져오기 (재생성 없이)
+      const ctx = salesChart.value.getContext('2d');
+      
+      // 실제 데이터가 있으면 사용, 없으면 빈 차트
+      const salesData = metrics.value.totalSales > 0 ? 
+        [0, 0, 0, 0, 0, metrics.value.totalSales] : 
+        [0, 0, 0, 0, 0, 0];
+      
+      salesChartInstance = new ChartJS(ctx, {
+        type: salesChartType.value,
+        data: {
+          labels: ['1월', '2월', '3월', '4월', '5월', '6월'],
+          datasets: [{
+            label: '매출',
+            data: salesData,
+            borderColor: '#3f51b5',
+            backgroundColor: salesChartType.value === 'bar' ? '#3f51b5' : 'rgba(63, 81, 181, 0.1)',
+            borderWidth: 2,
+            fill: salesChartType.value === 'line'
+          }]
         },
-        scales: {
-          y: {
-            beginAtZero: true,
-            ticks: {
-              callback: function(value) {
-                return formatCurrency(value);
+        options: {
+          responsive: true,
+          maintainAspectRatio: false,
+          plugins: {
+            legend: {
+              display: false
+            }
+          },
+          scales: {
+            y: {
+              beginAtZero: true,
+              ticks: {
+                callback: function(value) {
+                  return formatCurrency(value);
+                }
               }
             }
           }
         }
-      }
-    });
+      });
+    } catch (error) {
+      console.error('Sales 차트 생성 오류:', error);
+    }
   };
 
   const createOrdersChart = () => {
-    if (!ordersChart.value) return;
-    
-    const ctx = ordersChart.value.getContext('2d');
-    // 실제 주문 데이터 계산
-    const totalOrders = metrics.value.totalOrders || 1;
-    const completedRate = metrics.value.settlementRate || 0;
-    const deliveredRate = Math.max(0, completedRate - 10); // 임시 계산
-    const shippingRate = Math.max(0, 100 - completedRate - deliveredRate - 10);
-    const pendingRate = Math.max(0, 100 - completedRate - deliveredRate - shippingRate);
+    if (!ordersChart.value) {
+      console.log('ordersChart ref가 없습니다');
+      return;
+    }
     
-    new ChartJS(ctx, {
-      type: 'doughnut',
-      data: {
-        labels: ['신규 주문', '배송중', '배송완료', '정산완료'],
-        datasets: [{
-          data: [pendingRate, shippingRate, deliveredRate, completedRate],
-          backgroundColor: ['#4caf50', '#ff9800', '#2196f3', '#9c27b0'],
-          borderWidth: 0
-        }]
-      },
-      options: {
-        responsive: true,
-        maintainAspectRatio: false,
-        plugins: {
-          legend: {
-            position: 'bottom'
+    try {
+      // 기존 차트 인스턴스 삭제
+      if (ordersChartInstance) {
+        ordersChartInstance.destroy();
+        ordersChartInstance = null;
+      }
+      
+      // Canvas 컨텍스트 직접 가져오기 (재생성 없이)
+      const ctx = ordersChart.value.getContext('2d');
+      // 실제 주문 데이터 계산
+      const totalOrders = metrics.value.totalOrders || 1;
+      const completedRate = metrics.value.settlementRate || 0;
+      const deliveredRate = Math.max(0, completedRate - 10); // 임시 계산
+      const shippingRate = Math.max(0, 100 - completedRate - deliveredRate - 10);
+      const pendingRate = Math.max(0, 100 - completedRate - deliveredRate - shippingRate);
+      
+      ordersChartInstance = new ChartJS(ctx, {
+        type: 'doughnut',
+        data: {
+          labels: ['신규 주문', '배송중', '배송완료', '정산완료'],
+          datasets: [{
+            data: [pendingRate, shippingRate, deliveredRate, completedRate],
+            backgroundColor: ['#4caf50', '#ff9800', '#2196f3', '#9c27b0'],
+            borderWidth: 0
+          }]
+        },
+        options: {
+          responsive: true,
+          maintainAspectRatio: false,
+          plugins: {
+            legend: {
+              position: 'bottom'
+            }
           }
         }
-      }
-    });
+      });
+    } catch (error) {
+      console.error('Orders 차트 생성 오류:', error);
+    }
   };
 
   const createSettlementChart = () => {
-    if (!settlementChart.value) return;
-    
-    const ctx = settlementChart.value.getContext('2d');
-    const settlementRate = metrics.value.settlementRate || 0;
-    const pendingRate = 100 - settlementRate;
+    if (!settlementChart.value) {
+      console.log('settlementChart ref가 없습니다');
+      return;
+    }
     
-    new ChartJS(ctx, {
-      type: 'bar',
-      data: {
-        labels: ['1주', '2주', '3주', '4주'],
-        datasets: [{
-          label: '정산 완료',
-          data: [settlementRate, settlementRate, settlementRate, settlementRate],
-          backgroundColor: '#4caf50'
-        }, {
-          label: '정산 대기',
-          data: [pendingRate, pendingRate, pendingRate, pendingRate],
-          backgroundColor: '#ff9800'
-        }]
-      },
-      options: {
-        responsive: true,
-        maintainAspectRatio: false,
-        scales: {
-          x: {
-            stacked: true
-          },
-          y: {
-            stacked: true,
-            beginAtZero: true,
-            max: 100,
-            ticks: {
-              callback: function(value) {
-                return value + '%';
+    try {
+      // 기존 차트 인스턴스 삭제
+      if (settlementChartInstance) {
+        settlementChartInstance.destroy();
+        settlementChartInstance = null;
+      }
+      
+      // Canvas 컨텍스트 직접 가져오기 (재생성 없이)
+      const ctx = settlementChart.value.getContext('2d');
+      const settlementRate = metrics.value.settlementRate || 0;
+      const pendingRate = 100 - settlementRate;
+      
+      settlementChartInstance = new ChartJS(ctx, {
+        type: 'bar',
+        data: {
+          labels: ['1주', '2주', '3주', '4주'],
+          datasets: [{
+            label: '정산 완료',
+            data: [settlementRate, settlementRate, settlementRate, settlementRate],
+            backgroundColor: '#4caf50'
+          }, {
+            label: '정산 대기',
+            data: [pendingRate, pendingRate, pendingRate, pendingRate],
+            backgroundColor: '#ff9800'
+          }]
+        },
+        options: {
+          responsive: true,
+          maintainAspectRatio: false,
+          scales: {
+            x: {
+              stacked: true
+            },
+            y: {
+              stacked: true,
+              beginAtZero: true,
+              max: 100,
+              ticks: {
+                callback: function(value) {
+                  return value + '%';
+                }
               }
             }
           }
         }
-      }
-    });
+      });
+    } catch (error) {
+      console.error('Settlement 차트 생성 오류:', error);
+    }
   };
 
   const createCategoryChart = () => {
-    if (!categoryChart.value) return;
+    if (!categoryChart.value) {
+      console.log('categoryChart ref가 없습니다');
+      return;
+    }
     
-    const ctx = categoryChart.value.getContext('2d');
-    // 실제 데이터가 없으므로 현재는 균등 분배로 표시
-    const totalSales = metrics.value.totalSales || 1;
-    new ChartJS(ctx, {
-      type: 'pie',
-      data: {
-        labels: ['전체 상품'],
-        datasets: [{
-          data: [100],
-          backgroundColor: ['#3f51b5']
-        }]
-      },
-      options: {
-        responsive: true,
-        maintainAspectRatio: false,
-        plugins: {
-          legend: {
-            position: 'right'
+    try {
+      // 기존 차트 인스턴스 삭제
+      if (categoryChartInstance) {
+        categoryChartInstance.destroy();
+        categoryChartInstance = null;
+      }
+      
+      // Canvas 컨텍스트 직접 가져오기 (재생성 없이)
+      const ctx = categoryChart.value.getContext('2d');
+      categoryChartInstance = new ChartJS(ctx, {
+        type: 'pie',
+        data: {
+          labels: ['전체 상품'],
+          datasets: [{
+            data: [100],
+            backgroundColor: ['#3f51b5']
+          }]
+        },
+        options: {
+          responsive: true,
+          maintainAspectRatio: false,
+          plugins: {
+            legend: {
+              position: 'right'
+            }
           }
         }
-      }
-    });
+      });
+    } catch (error) {
+      console.error('Category 차트 생성 오류:', error);
+    }
   };
 
   // 정산 데이터 가져오기
@@ -628,12 +738,23 @@
         fetchRecentOrders()
       ]);
       
-      // 데이터 로드 후 차트 업데이트
+      // 데이터 로드 후 애니메이션과 차트 생성
       nextTick(() => {
-        createSalesChart();
-        createOrdersChart();
-        createSettlementChart();
-        createCategoryChart();
+        startMetricsAnimation();
+        
+        // 차트 키를 업데이트하여 재렌더링 유도
+        salesChartKey.value++;
+        ordersChartKey.value++;
+        settlementChartKey.value++;
+        categoryChartKey.value++;
+        
+        // 차트 생성을 지연시켜 DOM이 완전히 렌더링된 후 실행
+        setTimeout(() => {
+          createSalesChart();
+          createOrdersChart();
+          createSettlementChart();
+          createCategoryChart();
+        }, 200);
       });
     } catch (error) {
       $toast.error('대시보드 데이터를 불러오는데 실패했습니다.');
@@ -658,15 +779,8 @@
     // 기본 1개월 기간 설정
     setQuickPeriod('month');
     
-    // 대시보드 데이터 로드
+    // 대시보드 데이터 로드 (차트는 데이터 로드 후에 생성됨)
     fetchDashboardData();
-    
-    nextTick(() => {
-      createSalesChart();
-      createOrdersChart();
-      createSettlementChart();
-      createCategoryChart();
-    });
   });
 </script>