쿠폰 관리 시스템
10,000원 주문 금액에 대해 1,000원 할인 쿠폰을 발급하는 시스템을 생각합니다.
주문 결제를 할 때 장바구니에서 사용할 수 있는 쿠폰의 목록을 보입니다.
주문 총금액 10,000원당 1,000원 할인 쿠폰을 발행하는 것이니
쿠폰의 목록에는 1,000원 할인 쿠폰 여러장이 보입니다.
이들 쿠폰 중 한장 또는 여러장을 선택하여 할인된 금액으로 결제할 수 있습니다.
결제를 하면 할인을 뺀 최종 결제 금액과 발급한 쿠폰의 수를 비교하여
새로 쿠폰을 발급해야 한다면 1,000원 할인 쿠폰을 발급합니다.
마이페이지에서 내 쿠폰을 조회할 수 있습니다.
내 쿠폰에는 쿠폰이 발급된 날짜와 사용날짜 그리고 사용 가능 여부를 확인할 수 있습니다.
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
useCoupons
- Vue에서 사용할 수 있는 커스텀 컴포저블 함수로 Firestore를 이용해 쿠폰 관련 비즈니스 로직을 처리하는 역할을 합니다.
// src/composables/useCoupons.js
import { ref } from 'vue'
import { db } from '@/firebase'
import {
collection,
query,
where,
getDocs,
addDoc,
updateDoc,
doc,
getDoc,
} from 'firebase/firestore'
export function useCoupons() {
const loading = ref(false)
const error = ref(null)
// ✅ 누적 주문 금액 기준 쿠폰 발급
const issueCouponsByTotalSpent = async (userId, companyId, companyName) => {
loading.value = true
error.value = null
try {
// 1. 해당 사용자의 누적 결제 금액 조회
const ordersColRef = collection(db, 'companies', companyId, 'orders')
const ordersSnap = await getDocs(query(ordersColRef, where('userId', '==', userId)))
const totalSpent = ordersSnap.docs.reduce((sum, doc) => {
return sum + (doc.data().finalAmount || 0)
}, 0)
// 2. 1만원당 1000원 쿠폰 1장 지급
const expectedCouponCount = Math.floor(totalSpent / 10000)
// 3. 이미 발급된 동일 조건 쿠폰 수 확인
const couponsColRef = collection(db, 'companies', companyId, 'coupons')
const issuedSnap = await getDocs(
query(
couponsColRef,
where('userId', '==', userId),
where('type', '==', 'fixed_discount'),
where('value', '==', 1000)
)
)
const alreadyIssued = issuedSnap.size
const toIssue = expectedCouponCount - alreadyIssued
// 4. 새로 발급해야 할 쿠폰 등록
for (let i = 0; i < toIssue; i++) {
await addDoc(couponsColRef, {
userId,
companyId, // companyId 추가
companyName, // companyName 추가
type: 'fixed_discount',
value: 1000,
issuedAt: new Date(),
used: false,
})
}
} catch (err) {
error.value = err
console.error('쿠폰 발급 오류:', err)
} finally {
loading.value = false
}
}
// 모든 회사에 있는 해당 사용자의 쿠폰을 가져옴
const fetchAllCoupons = async (userId) => {
try {
// 1. 모든 회사(company) 문서를 가져옴
const companiesSnap = await getDocs(collection(db, 'companies'))
// 2. 각 회사의 coupons 서브컬렉션에서 해당 유저의 쿠폰을 검색
const couponPromises = companiesSnap.docs.map(async (companyDoc) => {
const companyId = companyDoc.id
const couponsColRef = collection(db, 'companies', companyId, 'coupons')
const q = query(couponsColRef, where('userId', '==', userId))
const snap = await getDocs(q)
return snap.docs.map(doc => ({
id: doc.id,
companyId,
...doc.data(),
}))
})
const results = await Promise.all(couponPromises)
// 3. 2차원 배열을 평탄화
return results.flat()
} catch (err) {
console.error('모든 쿠폰 조회 오류:', err)
return []
}
}
// ✅ 사용 가능한 쿠폰 조회
const fetchAvailableCoupons = async (userId, companyId) => {
try {
const couponsColRef = collection(db, 'companies', companyId, 'coupons')
const q = query(
couponsColRef,
where('userId', '==', userId),
where('used', '==', false)
)
const snap = await getDocs(q)
return snap.docs.map(doc => ({ id: doc.id, ...doc.data() }))
} catch (err) {
error.value = err
console.error('쿠폰 조회 오류:', err)
return []
}
}
// ✅ 단일 쿠폰 사용
const useCoupon = async (couponId, companyId) => {
try {
const couponRef = doc(db, 'companies', companyId, 'coupons', couponId)
const couponSnap = await getDoc(couponRef)
if (!couponSnap.exists()) throw new Error('쿠폰이 존재하지 않습니다.')
const couponData = couponSnap.data()
if (couponData.used) throw new Error('이미 사용된 쿠폰입니다.')
await updateDoc(couponRef, {
used: true,
usedAt: new Date(),
})
} catch (err) {
error.value = err
console.error('쿠폰 사용 오류:', err)
throw err
}
}
// ✅ 복수 쿠폰 사용
const useMultipleCoupons = async (couponIds = [], companyId) => {
if (!Array.isArray(couponIds) || couponIds.length === 0) return
try {
const updatePromises = couponIds.map(async id => {
const couponRef = doc(db, 'companies', companyId, 'coupons', id)
const couponSnap = await getDoc(couponRef)
if (!couponSnap.exists()) throw new Error(`쿠폰(${id})이 존재하지 않습니다.`)
if (couponSnap.data().used) throw new Error(`쿠폰(${id})은 이미 사용됨.`)
return updateDoc(couponRef, {
used: true,
usedAt: new Date(),
})
})
await Promise.all(updatePromises)
} catch (err) {
error.value = err
console.error('여러 쿠폰 사용 오류:', err)
throw err
}
}
return {
loading,
error,
issueCouponsByTotalSpent,
fetchAllCoupons,
fetchAvailableCoupons,
useCoupon,
useMultipleCoupons,
}
}
🌱 기본 구조
- loading: 비동기 작업 중 로딩 상태 관리
- error: 에러 발생 시 저장
- db: Firebase Firestore 인스턴스
1. ✅ issueCouponsByTotalSpent(userId, companyId, companyName)
기능:
사용자의 누적 결제 금액에 따라 쿠폰 자동 발급 (1만 원당 1,000원 쿠폰)
로직:
- 주문 내역에서 finalAmount 누적 계산
- 1만 원당 한 장씩 쿠폰 예상 발급 개수 계산
- 동일 조건으로 이미 발급된 쿠폰 개수 파악
- 추가로 발급해야 할 만큼 쿠폰 생성 (중복 방지)
2. 🎁 fetchAllCoupons(userId)
기능:
모든 회사의 쿠폰 서브컬렉션에서 해당 사용자의 쿠폰 전부 조회
로직:
- 모든 회사 문서를 가져온 뒤
- 각각의 회사에서 유저 쿠폰들을 조회
- 최종적으로 flat()으로 쿠폰 배열 정리
3. 🔎 fetchAvailableCoupons(userId, companyId)
기능:
특정 회사에서 사용 가능한(used: false) 쿠폰 목록 조회
4. ✂️ useCoupon(couponId, companyId)
기능:
단일 쿠폰을 사용 처리
검증 로직 포함:
- 쿠폰이 존재하는지 확인
- 이미 사용됐는지 확인
- 사용 여부 업데이트 (used: true, usedAt 저장)
5. 🧾 useMultipleCoupons(couponIds, companyId)
기능:
복수 개의 쿠폰을 동시에 사용 처리
로직:
- 쿠폰 하나하나 확인 후 사용 여부 업데이트
- 병렬로 처리 (Promise.all)
🔁 반환값
return {
loading,
error,
issueCouponsByTotalSpent,
fetchAllCoupons,
fetchAvailableCoupons,
useCoupon,
useMultipleCoupons
}
각 기능은 컴포넌트에서 import { useCoupons } from '@/composables/useCoupons' 후 호출해서 사용할 수 있어요.
쿠폰 사용 - CartPage.vue
- 장바구니에서 주문하기 결제를 할 때 쿠폰을 사용합니다.
<!-- src/views/CartPage.vue -->
<template>
<v-container>
<v-card>
<v-card-title class="text-h6">
장바구니
<span v-if="isAnonymous" class="ml-1"> - 비회원 주문</span>
</v-card-title>
<v-divider />
<v-card-text v-if="cartItems.length">
<v-row>
<v-col v-for="(item, index) in cartItems" :key="index" cols="12">
<v-card class="pa-2 d-flex flex-column align-center" elevation="1" rounded="lg">
<div class="font-weight-bold text-subtitle-1 mb-1">[{{ item.categoryName }}] {{ item.name }}</div>
<div class="text-body-2 text-grey-darken-1 mb-1">옵션: {{ item.option?.name || '없음' }}</div>
<div class="text-body-2 mb-2">토핑: {{ item.toppings?.length ? item.toppings.map(t => t.name).join(', ') : '없음' }}</div>
<div class="d-flex align-center mb-2" style="gap: 10px;">
<v-btn icon size="x-small" @click="updateQuantity(index, -1)">
<v-icon>mdi-minus</v-icon>
</v-btn>
<span>{{ item.quantity }}</span>
<v-btn icon size="x-small" @click="updateQuantity(index, 1)">
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
<div class="text-body-1 text-primary font-weight-bold mb-3">
{{ calcItemPrice(item).toLocaleString() }}원
</div>
<v-btn size="small" color="error" @click="removeItem(index)">삭제</v-btn>
</v-card>
</v-col>
</v-row>
<v-divider class="my-4" />
<!-- 쿠폰 선택 영역 (회원만) -->
<v-row dense no-gutters class="mt-1 mb-1">
<v-col
cols="12"
md="6"
class="py-0 my-0"
v-for="coupon in availableCoupons"
:key="coupon.id"
>
<v-checkbox
v-model="selectedCouponIds"
:value="coupon.id"
:disabled="loading"
density="compact"
style="height: 32px;"
>
<template #label>
<span>
<strong class="text-primary">
{{ coupon.value.toLocaleString() }}원 할인 쿠폰
</strong>
<span class="text-grey-darken-1">
- 발급일: {{ formatDate(coupon.issuedAt) }}
</span>
</span>
</template>
</v-checkbox>
</v-col>
</v-row>
<div class="text-right font-weight-bold text-h6 mt-2">
총 합계: {{ totalAmount.toLocaleString() }}원<br />
할인 합계: {{ discountAmount.toLocaleString() }}원<br />
최종 결제 금액: {{ totalAfterDiscount.toLocaleString() }}원
</div>
</v-card-text>
<v-card-text v-else>
<v-alert type="info">장바구니에 담긴 항목이 없습니다.</v-alert>
</v-card-text>
<!-- 비회원 입력 영역 -->
<v-card-text v-if="isAnonymous && cartItems.length">
<v-text-field
v-model="guestName"
label="이름"
prepend-inner-icon="mdi-account"
required
class="mb-2"
/>
<v-text-field
v-model="guestPhone"
label="전화번호"
prepend-inner-icon="mdi-phone"
required
/>
</v-card-text>
<v-row class="mt-2 mb-4" dense justify="center" style="gap: 10px;">
<v-col cols="auto">
<v-btn color="success" :loading="loading" :disabled="loading" style="min-width: 120px;" @click="proceedToOrder">
주문하기
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn color="primary" style="min-width: 120px;" @click="goToMenu">메뉴 보기</v-btn>
</v-col>
</v-row>
</v-card>
</v-container>
<!-- 스크롤 상단 버튼 -->
<v-btn
v-show="showScrollTop"
icon
color="primary"
class="scroll-top-btn"
@click="scrollToTop"
>
<v-icon>mdi-arrow-up</v-icon>
</v-btn>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'
import { useOrder } from '@/composables/useOrder'
import { useCoupons } from '@/composables/useCoupons'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const cartItems = ref([])
const guestName = ref('')
const guestPhone = ref('')
const isGuest = ref(false)
const isAnonymous = computed(() => authStore.user?.isAnonymous === true)
const isMember = computed(() => !!authStore.user && !authStore.user.isAnonymous)
const { loading, createOrder } = useOrder()
const { fetchAvailableCoupons, useMultipleCoupons } = useCoupons()
const availableCoupons = ref([])
const selectedCouponIds = ref([])
const totalAmount = computed(() =>
cartItems.value.reduce((total, item) => total + calcItemPrice(item), 0)
)
const discountAmount = computed(() => {
// 선택 쿠폰들의 할인액 총합 (value가 1000원 고정으로 가정)
// 실제 value 필드를 더 정확히 계산할 수도 있음
return availableCoupons.value
.filter(c => selectedCouponIds.value.includes(c.id))
.reduce((sum, c) => sum + (c.value || 0), 0)
})
const totalAfterDiscount = computed(() => totalAmount.value - discountAmount.value)
const formatDate = (timestamp) => {
if (!timestamp) return ''
// Firestore Timestamp 대응
if (timestamp.toDate) return timestamp.toDate().toLocaleDateString()
if (timestamp.seconds) return new Date(timestamp.seconds * 1000).toLocaleDateString()
return new Date(timestamp).toLocaleDateString()
}
const proceedToOrder = async () => {
if (!cartItems.value.length) {
alert('장바구니가 비어있습니다.')
return
}
const companyId = route.query.companyId
if (!companyId) {
alert('회사 ID가 없습니다.')
return
}
if (isAnonymous.value && (!guestName.value || !guestPhone.value)) {
alert('비회원 주문 시 이름과 전화번호를 입력해주세요.')
return
}
if (totalAfterDiscount.value < 0) {
alert('할인액이 총 금액보다 클 수 없습니다.')
return
}
if (isAnonymous.value) isGuest.value = true
const orderData = {
userId: authStore.user?.uid || 'guest',
userName: authStore.profile?.name || guestName.value || 'guest',
userPhone: guestPhone.value || null,
isGuest: isGuest.value,
companyName: route.query.companyName || null,
items: cartItems.value.map(item => ({
categoryId: item.categoryId,
categoryName: item.categoryName,
menuId: item.menuId,
name: item.name,
price: item.price,
quantity: item.quantity,
toppings: item.toppings || [],
option: item.option || null,
imageUrl: item.imageUrl || null,
})),
totalAmount: totalAmount.value,
discountAmount: discountAmount.value,
finalAmount: totalAfterDiscount.value,
createdAt: new Date(),
couponIdsUsed: selectedCouponIds.value,
}
try {
const orderId = await createOrder(companyId, orderData)
if (isMember.value && selectedCouponIds.value.length > 0) {
// 쿠폰 여러장 병렬 사용 처리
await useMultipleCoupons(selectedCouponIds.value, companyId)
}
alert(`주문이 접수되었습니다! 주문 ID: ${orderId}`)
localStorage.removeItem('cart')
cartItems.value = []
selectedCouponIds.value = []
router.push('/')
} catch (e) {
console.error(e)
alert('주문 처리 중 오류가 발생했습니다.')
}
}
const goToMenu = () => {
router.push({
path: '/order',
query: {
companyId: route.query.companyId,
companyName: route.query.companyName,
username: authStore.profile?.name || 'guest',
returnToCart: true
}
})
}
const updateQuantity = (index, delta) => {
const item = cartItems.value[index]
item.quantity = Math.max(1, item.quantity + delta)
localStorage.setItem('cart', JSON.stringify(cartItems.value))
}
const removeItem = (index) => {
cartItems.value.splice(index, 1)
localStorage.setItem('cart', JSON.stringify(cartItems.value))
}
const calcItemPrice = (item) => {
const base = item.price || 0
const toppingSum = item.toppings?.reduce((sum, t) => sum + Number(t.price), 0) || 0
return (base + toppingSum) * (item.quantity || 1)
}
const showScrollTop = ref(false)
onMounted(async () => {
const saved = localStorage.getItem('cart')
cartItems.value = saved ? JSON.parse(saved).map(item => ({
...item,
quantity: item.quantity || 1
})) : []
if (isMember.value) {
try {
availableCoupons.value = await fetchAvailableCoupons(authStore.user.uid, route.query.companyId)
} catch (err) {
console.error('쿠폰 불러오기 실패:', err)
}
}
window.addEventListener('scroll', scrollHandler)
})
const scrollHandler = () => {
showScrollTop.value = window.scrollY > 200
}
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
onUnmounted(() => window.removeEventListener('scroll', scrollHandler))
</script>
<style scoped>
.scroll-top-btn {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 1000;
}
</style>
CartPage.vue 설명
✅ 페이지 역할
CartPage.vue 컴포넌트는 사용자가 장바구니에 담은 항목을 확인하고,
회원/비회원 여부에 따라 쿠폰을 적용하고 주문을 진행하는 기능을 제공합니다.
📦 장바구니 영역 ()
- 항목별로 카테고리 / 메뉴명 / 옵션 / 토핑 / 수량 변경 UI / 항목 금액 표시
- 수량 버튼(mdi-minus, mdi-plus)으로 갯수 조정, 로컬 스토리지 동기화
- 삭제 버튼을 통해 항목 제거 가능
- 상품 가격 계산: base price + toppings * quantity
🎟️ 쿠폰 선택 UI
- 회원만 사용 가능: v-if="isMember" 조건에 따라 쿠폰 UI 노출
- v-checkbox 컴포넌트로 선택, 각 쿠폰의 금액 강조
- 체크한 쿠폰들의 value 값을 합산해 할인 금액 계산
👤 비회원 정보 입력 (v-if="isAnonymous && cartItems.length")
- 이름과 전화번호를 필수로 입력해야 주문 가능
- guestName, guestPhone에 입력 바인딩됨
💰 결제 금액 요약
- 총 합계: 각 항목의 가격을 합산
- 할인 합계: 선택된 쿠폰의 금액 총합
- 최종 결제 금액: 총 합계에서 할인액 차감
📤 주문 처리 (proceedToOrder)
- 필수값 검사: 장바구니 비어 있음 / 비회원 정보 입력 누락 / 할인 초과 여부 등
- 주문 정보(orderData) 생성 후 Firebase의 useOrder().createOrder()로 전송
- 주문 완료 시 쿠폰 사용 처리 (useMultipleCoupons)
- 주문 성공 시 localStorage 초기화 후 홈으로 이동
🔃 쿠폰 불러오기 (onMounted)
- 로그인된 회원이라면 fetchAvailableCoupons()을 통해 회사별 쿠폰을 가져옴
- 비회원은 이 과정 생략
📜 기타 편의 기능
- 스크롤이 아래로 내려가면 상단으로 이동 버튼 (mdi-arrow-up) 노출
- 메뉴로 돌아가기 버튼 (goToMenu())
✨ Vuetify UI 커스터마이징 포인트
- density="compact"와 height: 32px을 사용해 쿠폰 목록 UI를 컴팩트하게 구성
- 할인 금액 강조를 위해 사용하여 시각적 포인트 부여
내 쿠폰 조회
마이페이지 - MyPage.vue
- src/views/MyPage.vue
<!-- src/views/MyPage.vue -->
<template>
<v-container class="py-6">
<v-row justify="center" align="center" dense>
<v-col cols="12" md="6" lg="4">
<v-card class="pa-4" elevation="2" @click="goToOrders" hover>
<v-card-title class="text-h6">📦 주문 내역</v-card-title>
<v-card-text class="text-body-2 text-grey-darken-1">
지금까지 주문한 내역을 확인할 수 있어요.
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6" lg="4">
<v-card class="pa-4" elevation="2" @click="goToReservations" hover>
<v-card-title class="text-h6">📅 예약 내역</v-card-title>
<v-card-text class="text-body-2 text-grey-darken-1">
예약한 시간과 상태를 확인할 수 있어요.
</v-card-text>
</v-card>
</v-col>
<!-- ✅ 내 쿠폰 보기 -->
<v-col cols="12" md="6" lg="4">
<v-card class="pa-4" elevation="2" @click="goToCoupons" hover>
<v-card-title class="text-h6">🎟️ 내 쿠폰</v-card-title>
<v-card-text class="text-body-2 text-grey-darken-1">
사용 가능한 쿠폰을 확인할 수 있어요.
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goToOrders = () => {
router.push('/my-orders')
}
const goToReservations = () => {
router.push('/my-reservations')
}
const goToCoupons = () => {
router.push('/my-coupons') // 해당 경로에 쿠폰 목록 페이지가 있어야 합니다.
}
</script>
내 쿠폰 - MyCoupons.vue
- src/views/MyCoupons.vue
<!-- src/views/MyCoupons.vue -->
<template>
<v-container class="py-6">
<v-card>
<v-card-title class="text-h6">🎟️ 보유 쿠폰</v-card-title>
<v-divider />
<v-card-text>
<v-alert type="info" v-if="loading">쿠폰을 불러오는 중입니다...</v-alert>
<v-alert type="info" v-else-if="coupons.length === 0">발급된 쿠폰이 없습니다.</v-alert>
<!-- ✅ 회사명 기준으로 그룹화 렌더링 -->
<div v-else>
<div v-for="(group, companyName) in groupedCoupons" :key="companyName" class="mb-4">
<h4 class="text-subtitle-1 font-weight-bold mb-2">{{ companyName }}</h4>
<v-list density="compact">
<v-list-item v-for="coupon in group" :key="coupon.id">
<v-list-item-title class="font-weight-bold">
<v-icon color="primary" size="small">mdi-ticket-percent</v-icon>
{{ coupon.value.toLocaleString() }}원 할인 쿠폰
</v-list-item-title>
<v-list-item-subtitle>
발급일: {{ formatDate(coupon.issuedAt) }}
<template v-if="coupon.used && coupon.usedAt">
・ 사용일: {{ formatDate(coupon.usedAt) }}
</template>
</v-list-item-subtitle>
<v-list-item-subtitle>
상태:
<span :class="coupon.used ? 'text-grey' : 'text-success'">
<strong>{{ coupon.used ? '사용 완료' : '사용 가능' }}</strong>
</span>
</v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
</div>
</v-card-text>
</v-card>
</v-container>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useAuthStore } from '@/stores/authStore'
import { useCoupons } from '@/composables/useCoupons'
const authStore = useAuthStore()
const coupons = ref([])
const loading = ref(true)
const { fetchAllCoupons } = useCoupons()
const formatDate = (timestamp) => {
if (!timestamp) return ''
if (timestamp.toDate) return timestamp.toDate().toLocaleDateString()
if (timestamp.seconds) return new Date(timestamp.seconds * 1000).toLocaleDateString()
return new Date(timestamp).toLocaleDateString()
}
// ✅ 회사명 기준으로 그룹화된 쿠폰 목록 계산
const groupedCoupons = computed(() => {
const groups = {}
for (const coupon of coupons.value) {
const name = coupon.companyName || '알 수 없는 상점'
if (!groups[name]) groups[name] = []
groups[name].push(coupon)
}
return groups
})
onMounted(async () => {
try {
if (authStore.user?.uid) {
coupons.value = await fetchAllCoupons(authStore.user.uid)
}
} catch (err) {
console.error('쿠폰 불러오기 실패:', err)
} finally {
loading.value = false
}
})
</script>
📌 주요 기능
- 사용자 인증 정보를 통해 해당 사용자의 모든 회사 쿠폰 불러오기
- 쿠폰 데이터를 회사명 기준으로 그룹화
- 발급일, 사용일, 사용 상태 등을 시각적으로 구분해 리스트 형태로 출력
- 쿠폰 데이터 로딩 시 v-alert로 상태 안내
🔍 핵심 구조
coupons: 모든 쿠폰 목록 (ref)
- Firestore로부터 불러온 결과를 담는 배열
- useCoupons()에서 제공하는 fetchAllCoupons(userId) 호출로 세팅
groupedCoupons: 회사명 기준 그룹화 (computed)
const groupedCoupons = computed(() => {
const groups = {}
for (const coupon of coupons.value) {
const name = coupon.companyName || '알 수 없는 상점'
if (!groups[name]) groups[name] = []
groups[name].push(coupon)
}
return groups
})
- 각 회사 이름을 키로 묶어서 그룹별로 쿠폰을 렌더링하기 위한 데이터 구조를 만듦
<v-list> 렌더링
- 회사명 헤더 () 아래에 해당 회사의 쿠폰들을 v-list-item으로 나열
- 쿠폰 금액, 발급일, 사용일, 상태를 표시
상태 표현
<span :class="coupon.used ? 'text-grey' : 'text-success'">
<strong>{{ coupon.used ? '사용 완료' : '사용 가능' }}</strong>
</span>
- 사용 여부에 따라 색상과 텍스트를 다르게 처리 (회색 or 녹색)
⚙️ onMounted 훅
onMounted(async () => {
if (authStore.user?.uid) {
coupons.value = await fetchAllCoupons(authStore.user.uid)
}
})
- 페이지가 마운트될 때, 인증된 사용자 ID로 쿠폰 목록을 불러와서 coupons에 저장
'예약 포털 (Vue3 + Firebase) - 서비스 오픈까지' 카테고리의 다른 글
41. Vue3 + Firebase 프로젝트 '우리 동네' - QR 코드 온라인 주문 (0) | 2025.06.30 |
---|---|
40. Vue3 + Firebase 프로젝트 '우리 동네' - 웹앱에서 GPS 사용 (2) | 2025.06.30 |
38. Vue3 + Firebase 프로젝트 '우리 동네' - PWA 설정 추가 (0) | 2025.06.23 |
37. 예약 포털 (Vue3 + Firebase) - 네이버 로그인 구현 (0) | 2025.06.19 |
36. 예약 포털 (Vue3 + Firebase) - 카카오 로그인 구현 (0) | 2025.06.19 |