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

18. 동네 포털 (Vue 3 + Vuetify + Firebase) - 메뉴 카테고리 관리

그랜파 개발자 2025. 6. 8. 17:18

메뉴 카테고리 관리

 테이크아웃 커피점의 메뉴 관리를 생각해 봅니다.

 

음료의 종류가 생각보다 많습니다.

음료 외에 토핑, 사이드 메뉴까지 있습니다.

 

우선 메뉴 관리를 위하여 

카테고리를 등록하여 

카테고리 별로 메뉴를 볼 수 있도록 하겠습니다.

 

카테고리를 등록하면 

등록한 카테고리는 리스트에 나타납니다.

 

카테고리 리스트의 카테고리 옆 삭제 아이콘을 누르면 카테고리가 삭제 됩니다.

 

카테고리의 순서는 메뉴를 나타낼 때 카테고리 순서로 나타내기 위해 필요합니다.

카테고리 리스트에서 카테고리를 드래그하면 순서를 변경할 수 있습니다.

 

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

 

vuedraggable

vuedraggable는 Vue.js에서 드래그 앤 드롭(Drag & Drop) 기능을 쉽게 구현할 수 있게 해주는 인기 라이브러리입니다. 이 라이브러리는 SortableJS를 기반으로 만들어졌으며, 리스트 재정렬이나 카드 이동 같은 UI 기능을 쉽게 구현할 수 있게 도와줍니다.

 

v-data-table

v-data-table은 Vuetify에서 제공하는 강력한 표 형식의 데이터 표시 컴포넌트입니다. 데이터 목록을 표로 보여주고, 정렬, 페이징, 필터링, 선택, 사용자 정의 셀 렌더링 등 다양한 기능을 제공합니다.

 

메뉴 관리 페이지에  ‘카테고리 관리’ 

 

Vue.js에서 드래그 앤 드롭(Drag & Drop)

드래그 아이콘을 누르고 드래그 하면 순서 변경할 수 있습니다.

 

삭제 아이콘을 누르면 등록된 카테고리가 삭제 됩니다.

 

src/views/CategoryManagement - 메뉴 카테고리 관리

<!-- src/views/CategoryManagement -->
<template>
  <v-container>
    <v-card class="pa-4 mx-auto" style="max-width: 600px;">
      <v-card-title class="d-flex justify-space-between align-center">
        <span>{{ companyName }} - 카테고리 관리 (드래그로 순서 변경)</span>
        <v-spacer />
        <v-btn variant="text" class="mt-4" color="primary" @click="goToMenu">
          메뉴로 가기
        </v-btn>        
      </v-card-title>

      <v-form @submit.prevent="addCategory">
        <v-row class="align-center">
          <v-col cols="12" md="6">
            <v-text-field v-model="newCategoryName" label="카테고리 이름" />
          </v-col>
          <v-col cols="12" md="6" class="d-flex justify-end">
            <v-btn color="primary" type="submit" class="mt-2 mt-md-0">
              등록
            </v-btn>
          </v-col>
        </v-row>
      </v-form>

      <v-divider class="my-4" />

      <!-- v-table with draggable rows -->
      <v-table>
        <thead>
          <tr>
            <th style="width: 40px;"></th>
            <th>카테고리명</th>
            <th class="text-end" style="width: 80px;">순서</th>
            <th style="width: 40px;"></th>
          </tr>
        </thead>

        <draggable
          tag="tbody"
          v-model="categories"
          item-key="id"
          handle=".drag-handle"
          @end="saveOrder"
        >
          <template #item="{ element }">
            <tr>
              <td>
                <v-icon class="drag-handle" color="grey darken-1" size="20" style="cursor: grab;">
                  mdi-drag
                </v-icon>
              </td>
              <td>{{ element.name }}</td>
              <td class="text-end">{{ element.sortOrder }}</td>
              <td>
                <v-icon
                  color="error"
                  class="cursor-pointer"
                  @click="confirmDelete(element.id)"
                >
                  mdi-delete
                </v-icon>
              </td>
            </tr>
          </template>
        </draggable>
      </v-table>
    </v-card>
  </v-container>
</template>

<script setup>
import { onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCategoryManager } from '@/composables/useCategoryManager'
import draggable from 'vuedraggable'

const router = useRouter()
const route = useRoute()

const companyId = route.params.companyId
const companyName = route.query.companyName || ''

const {
  categories,
  newCategoryName,
  fetchCategories,
  addCategory,
  deleteCategory,
  saveOrder,
} = useCategoryManager(companyId)

const goToMenu = () => {
  router.push({ name: 'MenuList', params: { companyId }, query: { companyName } })
}

onMounted(fetchCategories)

function confirmDelete(id) {
  if (window.confirm('정말 삭제하시겠습니까?')) {
    deleteCategory(id)
  }
}
</script>

<style scoped>
.drag-handle {
  cursor: grab;
}
</style>

 

src/composables/useCategoryManager.js - 카테고리 관리 DB 연동

// src/composables/useCategoryManager.js
import { ref } from 'vue'
import { collection, getDocs, addDoc, deleteDoc, doc, updateDoc } from 'firebase/firestore'
import { db } from '@/firebase'

export function useCategoryManager(companyId) {
  const categories = ref([])
  const newCategoryName = ref('')

  const menusByCategory = ref({})
  const categoryNames = ref([])

  const fetchCategories = async () => {
    const snap = await getDocs(collection(db, 'companies', companyId, 'categories'))
    categories.value = snap.docs
      .map(doc => ({ id: doc.id, ...doc.data() }))
      .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
  }

  const addCategory = async () => {
    if (!newCategoryName.value.trim()) return
    const maxSort = categories.value.reduce((max, c) => Math.max(max, c.sortOrder ?? 0), 0)
    await addDoc(collection(db, 'companies', companyId, 'categories'), {
      name: newCategoryName.value.trim(),
      sortOrder: maxSort + 1,
    })
    newCategoryName.value = ''
    await fetchCategories()
  }

  const deleteCategory = async (id) => {
    await deleteDoc(doc(db, 'companies', companyId, 'categories', id))
    await fetchCategories()
  }

  const saveOrder = async () => {
    const batchUpdates = categories.value.map((cat, index) =>
      updateDoc(doc(db, 'companies', companyId, 'categories', cat.id), {
        sortOrder: index,
      })
    )
    await Promise.all(batchUpdates)
    await fetchCategories()
  }

  const groupMenusByCategory = (allMenus) => {
    const grouped = {}
    allMenus.forEach(menu => {
      const category = menu.category || '기타'
      if (!grouped[category]) grouped[category] = []
      grouped[category].push(menu)
    })
    menusByCategory.value = grouped

    categoryNames.value = Object.keys(grouped).sort((a, b) => {
      const aOrder = categories.value.find(c => c.name === a)?.sortOrder ?? 9999
      const bOrder = categories.value.find(c => c.name === b)?.sortOrder ?? 9999
      return aOrder - bOrder
    })

    //console.log('categoryNames.value:', categoryNames.value)
  }

  return {
    categories,
    newCategoryName,
    categoryNames,
    menusByCategory,
    fetchCategories,
    addCategory,
    deleteCategory,
    saveOrder,
    groupMenusByCategory
  }
}