매출 분석
일(日), 주(週), 월(月), 년(年) 단위로 매출 트렌드와 성장률 정보를 제공하는 것은
비즈니스 인사이트 도출과 전략 수립에 매우 유용합니다.
✅ 1. 빠른 문제 감지와 대응 (일간/주간 매출)
- 일간 트렌드는 전날 대비 급격한 매출 변동을 감지할 수 있어, 이벤트 실패, 시스템 장애, 고객 이탈 등을 빠르게 파악하고 대응할 수 있습니다
- 주간 매출 변화는 일시적 현상인지, 지속적 추세인지 판단하게 해주어, 단기 마케팅 전략 조정에 도움을 줍니다.
✅ 2. 시즌성 및 반복 패턴 파악 (월간/연간 매출)
- 월간 분석은 월초/월말 구매 패턴, 특정 시즌(예: 명절, 세일 시즌) 효과를 분석해, 계획적인 프로모션이나 재고 조정이 가능합니다.
- 연간 비교는 전년도 대비 성장 여부를 확인하고, 장기적인 성장률과 방향성을 평가할 수 있게 해줍니다.
https://github.com/inetsos/downtown
GitHub - inetsos/downtown: 동네 포털 - Vue 3 + Firebase
동네 포털 - Vue 3 + Firebase. Contribute to inetsos/downtown development by creating an account on GitHub.
github.com
1. 매출을 vue-chartjs 차트로 시각화
기준일을 입력하면 기준일에 대해 매출 분석을 vue-chartjs로 시각화합니다.
useSalesSummary() composable은 매출 분석을 위한 강력한 도구입니다.
Firestore 데이터를 기반으로 오늘, 이번주, 이번달, 올해와 그 이전 기간의 데이터를 가져와 매출 트렌드와 성장율을 계산 계산하여 관리자 대시보드에 보여줍니다
- 일 매출: 최근 7일간의 일별 매출 트렌드 및 성장율
- 주 매출: 최근 6주 간의 주간 매출 트렌드 및 성장율
- 월 매출: 최근 6개월 간의 월별 매출 트렌드 및 성장율
- 년 매출: 최근 4년 간의 연도별 매출 트렌드 및 성장율
2. 매출 분석 차트 컴포넌트
- SalesLineChart.vue
<template>
<Line :data="chartData" :options="chartOptions" />
</template>
<script setup>
import { computed } from 'vue'
import { Line } from 'vue-chartjs'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
CategoryScale,
LinearScale,
PointElement,
Filler
} from 'chart.js'
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
CategoryScale,
LinearScale,
PointElement,
Filler
)
const props = defineProps({
labels: { type: Array, required: true }, // 날짜 혹은 기간 라벨
sales: { type: Array, required: true }, // 매출 데이터
labelText: { type: String, default: '매출' } // 차트 상단 라벨
})
const chartData = computed(() => ({
labels: props.labels,
datasets: [
{
label: props.labelText,
data: props.sales,
borderColor: '#42a5f5',
backgroundColor: 'rgba(66,165,245,0.2)',
tension: 0.3,
fill: true,
pointRadius: 5,
pointHoverRadius: 7
}
]
}))
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
title: { display: false }
},
scales: {
y: {
ticks: {
callback: value => `${value.toLocaleString()}원`
}
}
}
}
</script>
SalesLineChart 컴포넌트 설명
이 SalesLineChart.vue 컴포넌트는 Vue 3와 Chart.js, 그리고 vue-chartjs 라이브러리를 활용해서 매출 데이터를 선형 차트(Line Chart) 로 시각화하는 역할을 합니다. 간단히 구조를 나눠 설명드릴게요:
🧩 구성 요소
1. <template>
<Line :data="chartData" :options="chartOptions" />
- Line 컴포넌트는 Chart.js의 Line 차트를 나타냅니다.
- :data와 :options는 각각 차트에 표시할 데이터와 설정을 연결합니다.
2. <script setup>
- vue-chartjs에서 제공하는 Line 차트와 함께 필요한 Chart.js 모듈들을 register()를 통해 등록합니다.
- defineProps()로 컴포넌트에 세 가지 props를 받습니다:
- labels: x축에 표시될 날짜 혹은 기간 (예: ['1월', '2월'])
- sales: y축에 표시될 매출 값 배열 (예: [10000, 12000])
- labelText: 차트 상단의 라벨 텍스트 (기본값은 '매출')
3. chartData
const chartData = computed(() => ({
labels: props.labels,
datasets: [
{
label: props.labelText,
data: props.sales,
borderColor: '#42a5f5',
backgroundColor: 'rgba(66,165,245,0.2)',
tension: 0.3, // 곡선의 부드러움
fill: true,
pointRadius: 5,
pointHoverRadius: 7
}
]
}))
- 실제 선형 그래프에 표시될 데이터입니다.
- 곡선을 부드럽게 만들고 배경 색을 채우는 등의 설정이 포함되어 있습니다.
4. chartOptions
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
title: { display: false }
},
scales: {
y: {
ticks: {
callback: value => `${value.toLocaleString()}원`
}
}
}
}
- 차트의 전체적인 옵션을 설정합니다:
- 반응형 및 비율 유지 설정
- 범례 위치 설정
- y축의 숫자에 ‘원’ 단위를 붙이는 커스텀 포맷 설정
3. 매출 분석 View
- 기준일을 입력하면 일, 주, 월, 년의 매출 트렌드 차트와 성장률을 화면에 출력합니다.
- 매출 트렌드 차트
<SalesLineChart :labels="dayLabels" :sales="daySales" label-text="일 매출" />
<SalesLineChart :labels="weekLabels" :sales="weekSales" label-text="주 매출"/>
<SalesLineChart :labels="monthLabels" :sales="monthSales" label-text="월 매출"/>
<SalesLineChart :labels="yearLabels" :sales="yearSales" label-text="년 매출" />
- src/views/SalesAnalysis.vue
<!-- src/views/SalesAnalysis.vue -->
<template>
<v-container>
<!-- 운영 대시보드로 돌아가기 버튼 -->
<div class="d-flex justify-end mt-4 mb-4 mr-2">
<v-btn
text
color="primary"
class="text-subtitle-2"
@click="goToDashboard"
elevation="0"
>
<v-icon left>mdi-arrow-left</v-icon>
대시보드
</v-btn>
</div>
<v-card-title class="headline font-weight-bold">매출 분석</v-card-title>
<v-card-text>
<!-- 날짜 & 조회 -->
<v-row class="align-center mb-4">
<v-col cols="12" md="3" class="py-1">
<v-text-field
v-model="baseDate"
type="date"
label="기준 날짜"
dense
hide-details
/>
</v-col>
<v-col cols="12" md="3" class="py-1">
<v-btn
color="primary"
@click="loadSummaryByDate"
class="mt-1 mt-md-0 btn-responsive"
>
조회
</v-btn>
</v-col>
</v-row>
<!-- 일 매출 -->
<v-row v-if="salesSummary.day.count >= 0" class="mb-6">
<v-col cols="12" md="6">
<v-card outlined>
<v-card-title class="headline">일 매출</v-card-title>
<v-card-text>
<p>총 매출: <strong>{{ salesSummary.day.total.toLocaleString() }}원</strong></p>
<p>주문 수: <strong>{{ salesSummary.day.count }}건</strong></p>
<p>평균 주문 금액: <strong>{{ Math.round(salesSummary.day.avg).toLocaleString() }}원</strong></p>
<p>성장률 (전일 대비):
<strong v-if="salesSummary.day.growth !== null" :class="{'text-success': salesSummary.day.growth >= 0, 'text-error': salesSummary.day.growth < 0}">
{{ salesSummary.day.growth.toFixed(2) }}%
</strong>
<span v-else>N/A</span>
</p>
<div class="mt-4" style="width: 90%">
<SalesLineChart :labels="dayLabels" :sales="daySales" label-text="일 매출" />
</div>
</v-card-text>
</v-card>
</v-col>
<!-- 주 매출 -->
<v-col cols="12" md="6" v-if="salesSummary.week.count >= 0">
<v-card outlined>
<v-card-title class="headline">주 매출</v-card-title>
<v-card-text>
<p>총 매출: <strong>{{ salesSummary.week.total.toLocaleString() }}원</strong></p>
<p>주문 수: <strong>{{ salesSummary.week.count }}건</strong></p>
<p>평균 주문 금액: <strong>{{ Math.round(salesSummary.week.avg).toLocaleString() }}원</strong></p>
<p>성장률 (전주 대비):
<strong v-if="salesSummary.week.growth !== null" :class="{'text-success': salesSummary.week.growth >= 0, 'text-error': salesSummary.week.growth < 0}">
{{ salesSummary.week.growth.toFixed(2) }}%
</strong>
<span v-else>N/A</span>
</p>
<div class="mt-4" style="width: 90%">
<SalesLineChart :labels="weekLabels" :sales="weekSales" label-text="주 매출"/>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- 월 매출 -->
<v-col cols="12" md="6" v-if="salesSummary.month.count >= 0">
<v-card outlined>
<v-card-title class="headline">월 매출</v-card-title>
<v-card-text>
<p>총 매출: <strong>{{ salesSummary.month.total.toLocaleString() }}원</strong></p>
<p>주문 수: <strong>{{ salesSummary.month.count.toLocaleString() }}건</strong></p>
<p>평균 주문 금액: <strong>{{ Math.round(salesSummary.month.avg).toLocaleString() }}원</strong></p>
<p>성장률 (전월 대비):
<strong v-if="salesSummary.month.growth !== null" :class="{'text-success': salesSummary.month.growth >= 0, 'text-error': salesSummary.month.growth < 0}">
{{ salesSummary.month.growth.toFixed(2) }}%
</strong>
<span v-else>N/A</span>
</p>
<div class="mt-4" style="width: 90%">
<SalesLineChart :labels="monthLabels" :sales="monthSales" label-text="월 매출"/>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- 연 매출 -->
<v-col cols="12" md="6" v-if="salesSummary.year.count >= 0">
<v-card outlined>
<v-card-title class="headline">연 매출</v-card-title>
<v-card-text>
<p>총 매출: <strong>{{ salesSummary.year.total.toLocaleString() }}원</strong></p>
<p>주문 수: <strong>{{ salesSummary.year.count.toLocaleString() }}건</strong></p>
<p>평균 주문 금액: <strong>{{ Math.round(salesSummary.year.avg).toLocaleString() }}원</strong></p>
<p>성장률 (전년 대비):
<strong v-if="salesSummary.year.growth !== null" :class="{'text-success': salesSummary.year.growth >= 0, 'text-error': salesSummary.year.growth < 0}">
{{ salesSummary.year.growth.toFixed(2) }}%
</strong>
<span v-else>N/A</span>
</p>
<div class="mt-4" style="width: 90%">
<SalesLineChart :labels="yearLabels" :sales="yearSales" label-text="년 매출" />
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-container>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useSalesSummary } from '@/composables/useSalesSummary'
import SalesLineChart from '@/components/SalesLineChart.vue'
const router = useRouter()
const route = useRoute()
const companyId = route.query.companyId
const companyName = route.query.companyName
const {
salesSummary,
loadSummary,
dayLabels, daySales,
weekLabels, weekSales,
monthLabels, monthSales,
yearLabels, yearSales
} = useSalesSummary(companyId)
const baseDate = ref('')
//const baseDate = ref(new Date().toISOString().split('T')[0])
// 기본 날짜 없이도 초기 데이터를 로드할 수 있게 하려면 초기값 세팅 가능
baseDate.value = new Date().toISOString().split('T')[0] // 오늘 날짜
const loadSummaryByDate = () => {
if (!baseDate.value) {
alert('기준 날짜를 선택해주세요.')
return
}
loadSummary(baseDate.value)
}
watch(baseDate, (newDate) => {
if (newDate) {
loadSummary(newDate)
}
})
const goToDashboard = () => {
router.push({
name: 'OperationsDashboard',
query: { companyId, companyName }
})
}
</script>
<style scoped>
.text-success {
color: #4caf50;
}
.text-error {
color: #f44336;
}
.btn-responsive {
width: 100%; /* 모바일용 - 꽉 차게 */
height: 40px;
font-size: 16px;
}
@media (min-width: 960px) {
.btn-responsive {
width: auto; /* 데스크탑에서는 내용 크기만큼 */
height: 30px !important;
font-size: 14px !important;
}
}
</style>
'예약 포털 (Vue3 + Firebase) - 서비스 오픈까지' 카테고리의 다른 글
34. 예약 포털 (Vue3 + Firebase) - 상품별 매출 리포트 (1) | 2025.06.19 |
---|---|
33. 예약 포털 (Vue3 + Firebase) - 카페 시간대별 매출 분석 (1) | 2025.06.18 |
31. 예약 포털 (Vue3 + Firebase) - chart.js, vue-chartjs (0) | 2025.06.17 |
30. 예약 포털 (Vue3 + Firebase) - 회원의 카페 온라인 주문 (0) | 2025.06.16 |
29. 예약 포털 (Vue3 + Firebase) - 익명 로그인, 비회원 주문 (2) | 2025.06.15 |