회원의 카페 온라인 주문
비회원 온라인 주문을 위하여 상점에 ‘비회원 주문’ 버튼을 추가하여
이것을 누르면 비회원 주문으로 처리를 하였습니다.
회원의 주문은 상점의 ‘온라인 주문’ 버튼을 누르면 온라인 주문을 할 수 있습니다.
회원이 로그인 하지 않은 상태에서 ‘온라인 주문’을 누르면
로그인 페이지로 이동하여 로그인을 합니다.
로그인에 성공을 하면 주문 페이지로 이동합니다.
온라인 주문에서 메뉴를 선택한 후
장바구니로 이동하여 주문을 할 수 있습니다.
회원 주문의 경우 이름과 전화번호 입력은 없습니다.
온라인 주문 페이지에는 ‘주문 내역’ 링크가 있습니다.
이것을 누르면 회원의 이 카페에 대한 주문 내역을 확인할 수 있습니다.
1. 온라인 주문 - 홈
- 상점에 온라인 주문 버튼이 있습니다.
로그인 이동 - 홈
- 회원이 로그인 하지 않은 상태라면 로그인으로 이동합니다.
const handleOrder = (company) => {
if (!isOpenNow(company)) return;
// 회원이 로그인 하지 않은 상태에서 온라인 주문을 클릭한 경우
if (!isLoggedIn.value) {
// 로그인 페이지로 이동하면서 리다이렉트 경로를 쿼리로 전달
router.push({
path: '/login',
query: {
redirect: '/order',
companyId: company.id,
companyName: company.name
}
});
return;
}
goToOrder(company.id, company.name);
};
2. 로그인 컴포넌트
- 로그인 후 리다이렉트가 있으면 리다이렉트 처리
const login = async () => {
// 로그인하지 않은 상태에서 온라인 주문을 클릭한 경우
// 로그인을 하도록 함
try {
await authStore.login(email.value, password.value)
// 로그인 후 리다이렉트 처리
const redirect = route.query.redirect
const companyId = route.query.companyId
const companyName = route.query.companyName
if (redirect === '/order' && companyId && companyName) {
router.push({
path: '/order',
query: { companyId, companyName }
})
} else {
router.push('/')
}
} catch (error) {
console.error('로그인 실패:', error)
alert('로그인에 실패했습니다.')
}
}
3. 장바구니
- 회원은 이름, 전화번호 입력없이 주문합니다.
4. 주문 내역 보기
- 온라인 주문 페이지 상단에 '주문 내역'을 누르면 이 카페에 대한 회원의 주문 내역을 볼 수 있습니다.
- 주문 내역 보기
내 주문 내역 보기
- 마이페이지의 주문 내역 보기는 모든 카페에 대한 주문 내역을 보여 줍니다.
- 카페 온라인 페이지에서 주문 내역은 해당 카페의 주문 내역을 보여 줍니다.
watch(
companyId,
async (id) => {
if (!userId) return
if (id) {
await fetchOrders(id)
} else {
await fetchAllOrders()
}
},
{ immediate: true }
)
- src/views/MyOrderPage.vue
<!-- src/views/MyOrderPage.vue -->
<template>
<v-container fluid>
<v-card flat>
<v-card-title class="text-h6 font-weight-bold d-flex align-center">
<v-icon left color="primary" class="mr-2">mdi-store</v-icon>
{{ companyName ? `${companyName} 주문 내역` : '내 주문 내역' }}
</v-card-title>
<v-divider />
<v-alert
v-if="!groupedOrders || Object.keys(groupedOrders).length === 0"
type="info"
class="ma-4"
>
주문 내역이 없습니다.
</v-alert>
<div v-for="(orders, companyId) in groupedOrders" :key="companyId" class="mb-8">
<v-card class="mb-4" elevation="2" outlined>
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon left color="primary">mdi-store</v-icon>
{{ getCompanyName(orders[0]) }}
</v-card-title>
</v-card>
<v-row dense>
<v-col
v-for="order in orders"
:key="order.id"
cols="12" sm="6" md="4"
>
<v-card class="order-card" elevation="4" outlined>
<v-card-text>
<div color="primary" text-color="white">
주문 번호: {{ order.orderNumber }}
</div>
<div color="primary" text-color="white">
주문 ID: {{ order.id }}
</div>
<!-- <v-chip :color="getStatusColor(order.status)" dark>{{ order.status }}</v-chip> -->
<div class="order-header mb-3">
상태:
<strong :style="{ color: getStatusColor(order.status) }">
{{ order.status }}
</strong >
</div>
<div class="order-header d-flex justify-space-between align-center mb-3">
<div class="order-date grey--text text--darken-1">
주문일: {{ formatDate(order.createdAt) }}
</div>
</div>
<div class="order-items">
<div
v-for="(item, index) in order.items"
:key="index"
class="order-item mb-3"
>
<div class="d-flex justify-space-between align-center font-weight-medium">
<div>{{ item.name }} × {{ item.quantity }}</div>
<div class="order-item-price">{{ (item.price * item.quantity).toLocaleString() }}원</div>
</div>
<div v-if="item.toppings?.length" class="order-subinfo">
토핑: {{ item.toppings.map(t => t.name).join(', ') }}
</div>
<div v-if="item.option" class="order-subinfo">
옵션: {{ item.option.name }}
</div>
</div>
</div>
<v-divider class="my-3" />
<div class="order-footer d-flex justify-end font-weight-bold primary--text text--darken-2">
총 합계: {{ order.items.reduce((sum, item) => sum + item.price * item.quantity, 0).toLocaleString() }}원
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</v-card>
</v-container>
</template>
<script setup>
import { onMounted, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useMyOrders } from '@/composables/useMyOrders'
import { useAuthStore } from '@/stores/authStore'
import { format } from 'date-fns'
const authStore = useAuthStore()
const route = useRoute()
const userId = authStore.user?.uid
const companyId = computed(() => route.query.companyId || null)
const companyName = computed(() => route.query.companyName || null)
const { orders, fetchOrders, fetchAllOrders } = useMyOrders(userId)
watch(
companyId,
async (id) => {
if (!userId) return
if (id) {
await fetchOrders(id)
} else {
await fetchAllOrders()
}
},
{ immediate: true }
)
const groupedOrders = computed(() => {
return orders.value.reduce((acc, order) => {
const cid = order.companyId ?? 'unknown'
if (!acc[cid]) acc[cid] = []
acc[cid].push(order)
return acc
}, {})
})
const getCompanyName = (order) => {
return order.companyName || `업체 (${order.companyId ?? '알 수 없음'})`
}
const getStatusColor = (status) => {
switch (status) {
case '완료': return 'green'
case '대기': return 'orange'
case '취소': return 'red'
default: return 'grey'
}
}
const formatDate = (ts) => {
try {
const date = ts?.toDate?.() || new Date(ts)
return format(date, 'yyyy-MM-dd HH:mm')
} catch {
return '-'
}
}
</script>
<style scoped>
.order-card {
cursor: default;
border-radius: 8px;
transition: box-shadow 0.3s ease;
min-height: 320px; /* 원하는 높이로 조정 */
display: flex;
flex-direction: column;
justify-content: space-between; /* 헤더-내용-푸터 간 균등 배분 */
}
.order-card:hover {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
.order-header {
font-size: 0.875rem;
}
.order-items {
font-size: 0.95rem;
flex-grow: 1; /* 상세 내용이 카드 높이 안에서 늘어나도록 */
margin-top: 8px;
overflow-y: auto; /* 내용이 너무 많을 때 스크롤 가능 */
}
.order-item {
padding-left: 8px;
}
.order-item-price {
font-weight: 600;
color: #1e88e5; /* Vuetify primary blue */
}
.order-subinfo {
font-size: 0.8rem;
color: #6b7280;
padding-left: 12px;
margin-top: 2px;
}
.order-footer {
font-size: 1rem;
margin-top: 12px;
text-align: right;
font-weight: 700;
color: #1e88e5;
}
</style>
5. 내 주문 보기 composable
- src/composables/useMyOrders.js
// src/composables/useMyOrders.js
import { ref } from 'vue'
import { collection, getDocs, query, where, orderBy } from 'firebase/firestore'
import { db } from '@/firebase'
export function useMyOrders(userId) {
const orders = ref([])
// 전체 업체의 주문 가져오기
const fetchAllOrders = async () => {
const companiesSnap = await getDocs(collection(db, 'companies'))
const allOrders = []
for (const companyDoc of companiesSnap.docs) {
const companyId = companyDoc.id
const ordersRef = collection(db, 'companies', companyId, 'orders')
const q = query(
ordersRef,
where('userId', '==', userId),
orderBy('createdAt', 'desc')
)
const ordersSnap = await getDocs(q)
ordersSnap.forEach(doc => {
allOrders.push({
id: doc.id,
...doc.data(),
companyId,
companyName: companyDoc.data().name || '', // 이름 추가 (옵션)
})
})
}
orders.value = allOrders.sort((a, b) => b.createdAt?.toMillis() - a.createdAt?.toMillis())
}
// 특정 업체의 주문만 가져오기
const fetchOrders = async (companyId) => {
const ordersRef = collection(db, 'companies', companyId, 'orders')
const q = query(
ordersRef,
where('userId', '==', userId),
orderBy('createdAt', 'desc')
)
const ordersSnap = await getDocs(q)
const filtered = []
ordersSnap.forEach(doc => {
filtered.push({
id: doc.id,
...doc.data(),
companyId,
})
})
orders.value = filtered
}
return { orders, fetchAllOrders, fetchOrders }
}
'예약 포털 (Vue3 + Firebase) - 서비스 오픈까지' 카테고리의 다른 글
32. 예약 포털 (Vue3 + Firebase) - 매출을 vue-chartjs 차트로 시각화 (0) | 2025.06.18 |
---|---|
31. 예약 포털 (Vue3 + Firebase) - chart.js, vue-chartjs (0) | 2025.06.17 |
29. 예약 포털 (Vue3 + Firebase) - 익명 로그인, 비회원 주문 (2) | 2025.06.15 |
28. 예약 포털 (Vue3 + Firebase) 마이페이지 - 주문 내역, 예약 내역 (1) | 2025.06.14 |
27. 예약 포털 (Vue3 + Vuetify + Firebase) - 카페의 온라인 주문 관리 (0) | 2025.06.13 |