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

20. 동네 포털 (Vue 3 + Vuetify + Firebase) - 음료 Ice/Hot 옵션 관리

그랜파 개발자 2025. 6. 9. 12:17

아이스/핫 옵션 관리

대부분의 음료는 아이스(Ice), 핫(Hot)을 선택할 수 있습니다.

그러나 일부 음료의 경우 아이스만, 또는 핫만 가능할 수 있습니다.

그러므로 음료를 주문할 때 아이스 또는 핫을 선택할 수 있도록 메뉴를 구성해야 합니다.

 

아이스, 핫 선택 또는 아이스만, 또는 핫만 등록할 수 있습니다.

등록된 옵션은 리스에 나타나고 

등록된 아이스/핫 옵션의 순서는 드래그로 변경할 수 있습니다.

옵션 옆에 있는 삭제 아이콘을 눌러 삭제할 수 있습니다.

 

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는 Vue.js에서 드래그 앤 드롭(Drag & Drop) 기능을 쉽게 구현할 수 있도록 해 줍니다.

v-data-table은 Vuetify에서 제공하는 강력한 표 형식의 데이터 표시 컴포넌트입니다.

 

 

src/views/IceHotManagement.vue - Ice/Hot 옵션 관리

<!-- src/views/IceHotManagement.vue -->
<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="addOption">
        <v-row class="align-center">
          <v-col cols="12" md="6">
            <v-text-field v-model="newOptionName" label="옵션 이름 (예: Ice, Hot)" />
          </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="options"
          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 draggable from 'vuedraggable'
import { useIceHotManager } from '@/composables/useIceHotManager'

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

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

const {
  options,
  newOptionName,
  fetchOptions,
  addOption,
  deleteOption,
  saveOrder,
} = useIceHotManager(companyId)

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

onMounted(fetchOptions)

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

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

 

src/composables/useIceHotManager.js - Ice/Hot 옵션 DB 연동

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

export function useIceHotManager(companyId) {
  const options = ref([])
  const newOptionName = ref('')

  // 옵션 목록 불러오기
  const fetchOptions = async () => {
    const snap = await getDocs(collection(db, 'companies', companyId, 'icehotOptions'))
    options.value = snap.docs
      .map(doc => ({ id: doc.id, ...doc.data() }))
      .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
  }

  // 옵션 추가
  const addOption = async () => {
    if (!newOptionName.value.trim()) return
    const maxSort = options.value.reduce((max, o) => Math.max(max, o.sortOrder ?? 0), 0)
    await addDoc(collection(db, 'companies', companyId, 'icehotOptions'), {
      name: newOptionName.value.trim(),
      sortOrder: maxSort + 1,
    })
    newOptionName.value = ''
    await fetchOptions()
  }

  // 옵션 삭제
  const deleteOption = async (id) => {
    await deleteDoc(doc(db, 'companies', companyId, 'icehotOptions', id))
    await fetchOptions()
  }

  // 순서 저장
  const saveOrder = async () => {
    const batchUpdates = options.value.map((opt, index) =>
      updateDoc(doc(db, 'companies', companyId, 'icehotOptions', opt.id), {
        sortOrder: index,
      })
    )
    await Promise.all(batchUpdates)
    await fetchOptions()
  }

  return {
    options,
    newOptionName,
    fetchOptions,
    addOption,
    deleteOption,
    saveOrder,
  }
}