|
|
@@ -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>
|
|
|
|