예약 포털 (Vue3 + Firebase) - 서비스 오픈까지

30. 예약 포털 (Vue3 + Firebase) - 회원의 카페 온라인 주문

그랜파 개발자 2025. 6. 16. 11:38

회원의 카페 온라인 주문

비회원 온라인 주문을 위하여 상점에 ‘비회원 주문’ 버튼을 추가하여

이것을 누르면 비회원 주문으로 처리를 하였습니다.

 

회원의 주문은 상점의 ‘온라인 주문’ 버튼을 누르면 온라인 주문을 할 수 있습니다.

 

회원이 로그인 하지 않은 상태에서 ‘온라인 주문’을 누르면

로그인 페이지로 이동하여 로그인을 합니다.

로그인에 성공을 하면 주문 페이지로 이동합니다.

 

온라인 주문에서 메뉴를 선택한 후 

장바구니로 이동하여 주문을 할 수 있습니다.

회원 주문의 경우 이름과 전화번호 입력은 없습니다.

 

온라인 주문 페이지에는 ‘주문 내역’ 링크가 있습니다.

이것을 누르면 회원의 이 카페에 대한 주문 내역을 확인할 수 있습니다.

 

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