1. 카페 시간대별 매출 분석의 장점
시간대별로 매출과 주문량을 함께 시각화하면 유용한 인사이트를 얻을 수 있습니다.
⏰ 고객 행동 패턴 파악
- 어떤 시간대에 주문이 몰리는지 확인할 수 있어요.
- 예: 오전 11시~12시에 주문이 집중된다면, 점심시간 프로모션이 효과적이었다는 걸 뜻할 수 있죠.
💰 매출 대비 효율 분석
- 주문량이 많지만 매출이 낮다면 단가가 낮은 메뉴가 인기가 많다는 신호일 수 있어요.
- 반대로 주문량은 적은데 매출이 높은 시간대는 고가 제품이 주로 팔리는 구간일 수도 있죠.
🧑🍳 운영 전략 최적화
- 주문과 매출이 많은 시간대엔 인력을 집중 배치하고,
- 한산한 시간엔 직원 교육, 준비 작업 등을 배치하면 효율적인 운영이 가능해요.
🎯 시간대별 마케팅 타깃팅
- 매출이 낮은 시간대엔 할인, 적립 이벤트를 시도해보고,
- 높은 시간대엔 추가 판매(업셀링) 전략을 적용해볼 수 있어요.
만약 두 데이터(매출 & 주문량)를 이중 Y축 차트로 동시에 보여주어
흐름을 한눈에 비교할 수 있어서 직관적입니다.
기간을 입력하면 Firebase Firestore에 저장된 주문 데이터를 기준으로, 입력 기간 내 시간대별 매출(totalAmount)과 주문 수(order count)를 트렌드 차트로 화면에 출력합니다.
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
2. 카페 시간대별 매출 분석
시간대별 매출 분석 뷰
- src/views/HourlySalesAnalysis.vue
<!-- src/views/HourlySalesAnalysis.vue-->
<template>
<v-card>
<!-- 운영 대시보드로 돌아가기 버튼 -->
<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="mb-4" align="center" dense>
<v-col cols="12" sm="5">
<v-menu
v-model="menu1"
:close-on-content-click="false"
transition="scale-transition"
offset-y
>
<template #activator="{ props }">
<v-text-field
v-model="startDateFormatted"
label="시작일"
readonly
v-bind="props"
prepend-inner-icon="mdi-calendar"
class="cursor-pointer"
/>
</template>
<v-date-picker
v-model="startDate"
@update:model-value="menu1 = false"
:max="endDateFormatted"
/>
</v-menu>
</v-col>
<v-col cols="12" sm="5">
<v-menu
v-model="menu2"
:close-on-content-click="false"
transition="scale-transition"
offset-y
>
<template #activator="{ props }">
<v-text-field
v-model="endDateFormatted"
label="종료일"
readonly
v-bind="props"
prepend-inner-icon="mdi-calendar"
class="cursor-pointer"
/>
</template>
<v-date-picker
v-model="endDate"
@update:model-value="menu2 = false"
:min="startDateFormatted"
:max="todayFormatted"
/>
</v-menu>
</v-col>
<v-col cols="12" sm="2" class="d-flex align-center justify-start">
<v-btn
color="primary"
class="ma-0 mb-4"
@click="loadData"
:loading="loading"
elevation="2"
style="min-width: 80px; padding-left: 16px; padding-right: 16px;"
>
조회
</v-btn>
</v-col>
</v-row>
<!-- 차트 영역 -->
<v-row>
<v-col>
<div style="height: 400px; width: 90%">
<LineChart
:labels="labels"
:totalAmounts="totalAmounts"
:orderCounts="orderCounts"
/>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { format } from 'date-fns'
import { useRoute, useRouter } from 'vue-router'
import LineChart from '@/components/HourlySalesChart.vue'
import { useSalesSummary } from '@/composables/useSalesSummary'
const router = useRouter()
const route = useRoute()
const companyId = ref(route.query.companyId || '')
const companyName = ref(route.query.companyName || '')
watch(() => route.query.companyId, (newId) => {
companyId.value = newId || ''
})
const goToDashboard = () => {
router.push({
name: 'OperationsDashboard',
query: { companyId: companyId.value, companyName: companyName.value },
})
}
const {
hourlyLabels,
hourlyTotalAmounts,
hourlyOrderCounts,
loadHourlySales,
} = useSalesSummary(companyId.value)
const getZeroTime = (date) => {
const d = new Date(date)
d.setHours(0, 0, 0, 0)
return d
}
const today = new Date()
const startDate = ref(getZeroTime(today))
const endDate = ref(today)
const menu1 = ref(false)
const menu2 = ref(false)
const loading = ref(false)
const startDateFormatted = computed({
get: () => format(startDate.value, 'yyyy-MM-dd'),
set: (val) => {
if (val) {
const d = new Date(val)
startDate.value = getZeroTime(d)
}
},
})
const endDateFormatted = computed({
get: () => format(endDate.value, 'yyyy-MM-dd'),
set: (val) => {
if (val) {
endDate.value = new Date(val)
}
},
})
const todayFormatted = format(today, 'yyyy-MM-dd')
const labels = ref([])
const totalAmounts = ref([])
const orderCounts = ref([])
async function loadData() {
if (!companyId.value) {
alert('회사 ID가 없습니다.')
return
}
if (startDate.value > endDate.value) {
alert('시작일이 종료일보다 클 수 없습니다.')
return
}
loading.value = true
try {
await loadHourlySales(companyId.value, startDate.value, endDate.value)
labels.value = hourlyLabels.value
totalAmounts.value = hourlyTotalAmounts.value
orderCounts.value = hourlyOrderCounts.value
} finally {
loading.value = false
}
}
watch(companyId, (newVal) => {
if (newVal) loadData()
})
if (companyId.value) {
loadData()
}
</script>
3. 시간대별 매출 분석 차트
1. useSalesSummary composable
- 시간대별 매출 : loadHourlySales
// 시간대별 매출
const hourlyLabels = ref([])
const hourlyTotalAmounts = ref([])
const hourlyOrderCounts = ref([])
const loadHourlySales = async (companyId, startDate, endDate) => {
const ordersRef = collection(db, "companies", companyId, "orders")
const q = query(
ordersRef,
where("createdAt", ">=", Timestamp.fromDate(startDate)),
where("createdAt", "<=", Timestamp.fromDate(endDate))
)
const snapshot = await getDocs(q)
const hourlyData = new Array(24).fill(0)
const hourlyCounts = new Array(24).fill(0)
snapshot.forEach(doc => {
const order = doc.data()
const hour = order.createdAt.toDate().getHours()
hourlyData[hour] += order.totalAmount || 0
hourlyCounts[hour] += 1
})
hourlyLabels.value = Array.from({ length: 24 }, (_, i) => `${i}시`)
hourlyTotalAmounts.value = hourlyData
hourlyOrderCounts.value = hourlyCounts
}
Firebase Firestore에 저장된 주문 데이터를 기준으로,
입력한 기간 내 시간대별 매출(totalAmount)과 주문 수(order count)를 분석하는 함수
🧾 데이터 구조
const hourlyLabels = ref([])
const hourlyTotalAmounts = ref([])
const hourlyOrderCounts = ref([])
- hourlyLabels: X축 라벨 (예: "13시", "14시" 등 24시간)
- hourlyTotalAmounts: 각 시간대의 총 매출
- hourlyOrderCounts: 각 시간대의 주문 건수
⚙️ loadHourlySales 함수
const loadHourlySales = async (companyId, startDate, endDate) => { ... }
- companyId, startDate, endDate를 받아 해당 회사의 특정 기간 동안의 주문 데이터를 불러옵니다.
📦 Firestore 쿼리
const q = query(
ordersRef,
where("createdAt", ">=", Timestamp.fromDate(startDate)),
where("createdAt", "<=", Timestamp.fromDate(endDate))
)
- Firestore의 "companies/{companyId}/orders" 경로에서 createdAt 필드 기준으로 기간 내 데이터를 조회합니다.
⏲️ 시간별 집계
const hourlyData = new Array(24).fill(0)
const hourlyCounts = new Array(24).fill(0)
- 0시부터 23시까지, 24시간을 기준으로 초기화한 배열입니다.
- 각 주문에 대해 createdAt에서 시간(hour) 만 추출해서 해당 인덱스 위치에:
- totalAmount 값을 더하고
- 주문 수를 1씩 증가시킵니다.
📊 결과 저장
hourlyLabels.value = Array.from({ length: 24 }, (_, i) => `${i}시`)
hourlyTotalAmounts.value = hourlyData
hourlyOrderCounts.value = hourlyCounts
- 이 값을 Vue에서 반응형으로 사용할 수 있도록 ref()에 담은 값을 업데이트합니다.
이 함수로 만든 hourlyTotalAmounts와 hourlyOrderCounts 데이터를 기반으로 Line 차트로 시각화합니다.
2. 시간대별 매출 분석 차트
- src/components/HourlySalesChart.vue
<!-- src/components/HourlySalesChart.vue -->
<template>
<Line :data="chartData" :options="chartOptions" />
</template>
<script setup>
import { computed } from "vue";
import { Line } from "vue-chartjs";
// chart.js 플러그인 및 구성 요소 수동 등록
import {
Chart as ChartJS,
LineElement,
PointElement,
LinearScale,
Title,
Tooltip,
Legend,
CategoryScale,
Filler
} from 'chart.js';
ChartJS.register(
LineElement,
PointElement,
LinearScale,
Title,
Tooltip,
Legend,
CategoryScale,
Filler
);
const props = defineProps({
labels: Array,
totalAmounts: Array,
orderCounts: Array,
});
const chartData = computed(() => ({
labels: Array.isArray(props.labels) ? props.labels : [],
datasets: [
{
label: "매출액 (원)",
data: Array.isArray(props.totalAmounts) ? props.totalAmounts : [],
borderColor: "#42a5f5",
backgroundColor: "rgba(66,165,245,0.2)",
yAxisID: "y",
tension: 0.3,
fill: true,
pointRadius: 4,
pointHoverRadius: 6,
},
{
label: "주문 건수",
data: Array.isArray(props.orderCounts) ? props.orderCounts : [],
borderColor: "#f44336",
backgroundColor: "rgba(244,67,54,0.2)",
yAxisID: "y1",
tension: 0.3,
fill: true,
pointRadius: 4,
pointHoverRadius: 6,
},
],
}));
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false,
},
stacked: false,
plugins: {
legend: { position: "top" },
title: { display: false },
},
scales: {
y: {
type: "linear",
display: true,
position: "left",
ticks: {
callback: (value) => `${value.toLocaleString()}원`,
},
},
y1: {
type: "linear",
display: true,
position: "right",
grid: { drawOnChartArea: false },
ticks: {
callback: (value) => `${value}건`,
},
},
},
};
</script>
3. 시간대별 매출 분석 차트 설명
Vue 3와 Chart.js, 그리고 vue-chartjs를 활용하여
매출액과 주문 건수를 동시에 시각화하는 이중 Y축 선형 차트(Line Chart)를 구현하였습니다.
🧩 구성 개요
- labels: X축 라벨 (예: 0시 ~ 23시)
- totalAmounts: Y축 왼쪽에 표시될 매출 데이터 (y축)
- orderCounts: Y축 오른쪽에 표시될 주문 건수 데이터 (y1축)
📐 <template>
<Line :data="chartData" :options="chartOptions" />
- Line 컴포넌트는 vue-chartjs에서 제공하는 차트로, 여기에 chartData와 chartOptions를 전달하여 차트를 렌더링합니다.
🧠 <script setup> 주요 구성
1. Chart.js 구성요소 등록
ChartJS.register(...)
- 사용하려는 차트 요소와 플러그인을 직접 등록해야 합니다 (Chart.js 3 이상에서는 필수).
2. Props 정의
const props = defineProps({
labels: Array,
totalAmounts: Array,
orderCounts: Array,
});
- 부모 컴포넌트로부터 X축 라벨, 매출액, 주문 건수 데이터를 받아옵니다.
3. 차트 데이터 (chartData)
const chartData = computed(() => ({
labels: props.labels,
datasets: [
{ label: "매출액", data: props.totalAmounts, yAxisID: "y", ... },
{ label: "주문 건수", data: props.orderCounts, yAxisID: "y1", ... },
]
}))
- 두 개의 데이터셋을 구성하여 각각 다른 Y축에 매핑합니다.
- yAxisID: "y" → 왼쪽 Y축 (매출)
- yAxisID: "y1" → 오른쪽 Y축 (건수)
- 선 색상과 곡선, 포인트 스타일도 커스터마이징 되어 있어요.
4. 차트 옵션 (chartOptions)
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: "index", intersect: false },
stacked: false,
plugins: { ... },
scales: {
y: {
position: "left",
ticks: { callback: (value) => `${value.toLocaleString()}원` },
},
y1: {
position: "right",
ticks: { callback: (value) => `${value}건` },
grid: { drawOnChartArea: false },
}
}
};
- 반응형 차트로 설정하고, 두 개의 독립적인 Y축(y, y1)을 설정합니다.
- Y축마다 포맷 콜백을 주어 원, 건 단위를 명확히 구분합니다.
- drawOnChartArea: false → 오른쪽 Y축은 왼쪽과 겹치지 않게 별도 격자선을 그리지 않도록 설정
🪄 요약
이 컴포넌트는 매출액과 주문량의 시간대별 흐름을 한 눈에 비교할 수 있어, 운영 인사이트를 얻을 수 있습니다.
예: “주문은 많았지만 매출은 낮았던 시간대” 또는 그 반대 상황을 쉽게 파악할 수 있습니다.
'예약 포털 (Vue3 + Firebase) - 서비스 오픈까지' 카테고리의 다른 글
35. 예약 포털 (Vue3 + Firebase) - 소셜 로그인 구현에 필요한 것들 (1) | 2025.06.19 |
---|---|
34. 예약 포털 (Vue3 + Firebase) - 상품별 매출 리포트 (1) | 2025.06.19 |
32. 예약 포털 (Vue3 + Firebase) - 매출을 vue-chartjs 차트로 시각화 (0) | 2025.06.18 |
31. 예약 포털 (Vue3 + Firebase) - chart.js, vue-chartjs (0) | 2025.06.17 |
30. 예약 포털 (Vue3 + Firebase) - 회원의 카페 온라인 주문 (0) | 2025.06.16 |