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

6. 동네 (예약) 포털 (Vue 3 + Firebase) - 회원의 업체 관리

그랜파 개발자 2025. 6. 1. 16:03

Firebase Hosting으로 실제 웹 서비스 오픈까지 진행합니다.

서비스 오픈까지 개발을 진행하면서 계속 수정, 변경될 것입니다.

회원의 업체 관리

회원은 여러 업체를 등록할 수 있습니다.

회원이 등록한 업체는 등록한 회원이 관리자가 됩니다.

 

회원은 여러 업체를 등록할 수 있습니다.

회원이 등록한 업체는 등록한 회원이 관리자가 됩니다.

등록한 업체의 정보는 수정, 삭제할 수 있습니다.

companies collection

업체는 firestore의 companies 컬렉션에 저장을 합니다.

companies  collection 구조는 다음과 같습니다.

{
  name: '업체명',
  description: '소개글',
  category: '배달음식', 
  ownerUid: 'userUid',
  createdAt: Timestamp,
}

 

 

 

 

 

 

src/router/index.js

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Register from '../views/Register.vue'
import Profile from '../views/Profile.vue'
import Login from '../views/Login.vue'

const routes = [
  { path: '/', name: 'home', component: Home },
  { path: '/register', name: 'register', component: Register },
  { path: '/profile', name: 'profile', component: Profile },
  { path: '/login', component: Login },
  {
    path: '/register-company',
    name: 'RegisterCompany',
    component: () => import('@/views/RegisterCompany.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/my-companies',
    name: 'MyCompanies',
    component: () => import('@/views/MyCompanies.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/edit-company/:id',
    name: 'EditCompany',
    component: () => import('@/views/EditCompany.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/company/:id',
    component: () => import('@/views/CompanyDetail.vue')
  }

]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

 

src/stores/companyStore.js

🔹 업체 등록 : addCompany
🔹 회원별 등록 업체 조회 : fetchMyCompanies
🔹 업체 수정 : updateCompany
🔹 업체 목록 : fetchAllCompanies 
🔹 업체 삭제 : deleteCompany 

// src/stores/companyStore.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { db } from '@/firebase'
import {
  collection,
  doc,
  addDoc,
  updateDoc,
  deleteDoc,
  query,
  where,
  getDocs,
  serverTimestamp
} from 'firebase/firestore' 
import { useAuthStore } from './authStore'

export const useCompanyStore = defineStore('company', () => {
  const companies = ref([])

  // 🔹 업체 등록
  const addCompany = async (name, description, category) => {
    const authStore = useAuthStore()
    const user = authStore.user
    if (!user?.uid) throw new Error('로그인이 필요합니다.')

    const company = {
      ownerId: user.uid,
      name,
      description,
      category,
      createdAt: serverTimestamp(),
    }

    await addDoc(collection(db, 'companies'), company)
  }

  // 🔹 회원별 등록 업체 조회
  const fetchMyCompanies = async () => {
    const authStore = useAuthStore()
    const user = authStore.user
    if (!user?.uid) return

    const q = query(
      collection(db, 'companies'),
      where('ownerId', '==', user.uid)
    )

    const snapshot = await getDocs(q)
    companies.value = snapshot.docs.map(doc => ({
      id: doc.id,
      ...doc.data()
    })) 
  }
 
  // 🔹 업체 수정
  const updateCompany = async (id, updatedData) => {
    const ref = doc(db, 'companies', id)
    await updateDoc(ref, {
        ...updatedData,
        updatedAt: serverTimestamp()
    })
  }

  const fetchAllCompanies = async () => {
    const snapshot = await getDocs(collection(db, 'companies'))
    companies.value = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
    }))
  }

  const deleteCompany = async (id) => {
    await deleteDoc(doc(db, 'companies', id))

    if (Array.isArray(companies.value)) {
        companies.value = companies.value.filter(c => c.id !== id)
    } else {
        companies.value = []
    }
  }

  return {
    companies,
    addCompany,
    fetchMyCompanies,
    updateCompany,
    fetchAllCompanies,
    deleteCompany
  }
})

 

src/views/RegisterCompany.vue - 업체 등록

<!-- src/views/RegisterCompany.vue-->
<template>
  <v-container>
    <v-card class="pa-4" max-width="500" mx-auto>
      <v-card-title>업체 등록</v-card-title>

      <v-text-field v-model="name" label="업체명" required />

      <!-- 업종 태그 선택 -->
      <div class="my-3">
        <div class="mb-1">업종 선택</div>
        <v-chip-group v-model="category" column mandatory>
          <v-chip
            v-for="item in categories"
            :key="item"
            :value="item"
            class="ma-1"
            color="primary"
            variant="outlined"
            filter
          >
            {{ item }}
          </v-chip>
        </v-chip-group>
      </div>

      <v-textarea v-model="description" label="소개글" />

      <v-btn color="primary" @click="submit">등록</v-btn>
    </v-card>
  </v-container>
</template>

<script setup>
import { ref } from 'vue'
import { useCompanyStore } from '@/stores/companyStore'
import { useRouter } from 'vue-router'

const name = ref('')
const description = ref('')
const category = ref('')
const categories = ['배달음식', '카페', '소매업', '서비스업', '교육', '병원', '기타']

const companyStore = useCompanyStore()
const router = useRouter()

const submit = async () => {
  if (!name.value || !category.value) {
    alert('업체명과 업종을 입력해주세요.')
    return
  }
  try {
    await companyStore.addCompany(name.value, description.value, category.value)
    alert('업체가 등록되었습니다.')
    router.push('/my-companies')
  } catch (e) {
    alert('등록에 실패했습니다.')
  }
}
</script>

 

src/views/EditCompany.vue - 업체 등록, 삭제

<!-- src/views/EditCompany.vue-->
<template>
  <v-container>
    <v-card class="pa-4" max-width="500" mx-auto>
      <v-card-title>업체 수정</v-card-title>

      <v-text-field v-model="name" label="업체명" required />

      <div class="my-3">
        <div class="mb-1">업종 선택</div>
        <v-chip-group v-model="category" column mandatory>
          <v-chip
            v-for="item in categories"
            :key="item"
            :value="item"
            class="ma-1"
            color="primary"
            variant="outlined"
            filter
          >
            {{ item }}
          </v-chip>
        </v-chip-group>
      </div>

      <v-textarea v-model="description" label="소개글" />

      <v-card-actions class="justify-space-between mt-4">
        <v-btn color="red" @click="confirmDialog = true">삭제</v-btn>
        <div>
            <v-btn color="primary" @click="submit">수정</v-btn>
            <v-btn color="grey" @click="cancel">취소</v-btn>
        </div>
      </v-card-actions>

      <!-- 삭제 확인 다이얼로그 -->
      <v-dialog v-model="confirmDialog" max-width="400">
        <v-card>
            <v-card-title class="text-h6">정말 삭제하시겠습니까?</v-card-title>
            <v-card-actions class="justify-end">
            <v-btn color="grey" text @click="confirmDialog = false">취소</v-btn>
            <v-btn color="red" text @click="deleteCompany">삭제</v-btn>
            </v-card-actions>
        </v-card>
      </v-dialog>

    </v-card>
  </v-container>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useCompanyStore } from '@/stores/companyStore'
import { useRoute, useRouter } from 'vue-router'

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

const id = route.params.id
const name = ref('')
const description = ref('')
const category = ref('')
const categories = ['배달음식', '카페', '소매업', '서비스업', '교육', '병원', '기타']

const confirmDialog = ref(false)

onMounted(() => {
  const company = companyStore.companies.find(c => c.id === id)
  if (company) {
    name.value = company.name
    description.value = company.description
    category.value = company.category
  }
})

const cancel = () => {
  router.push('/my-companies')
}

const submit = async () => {
  if (!name.value || !category.value) {
    alert('업체명과 업종을 입력해주세요.')
    return
  }

  try {
    await companyStore.updateCompany(id, {
      name: name.value,
      description: description.value,
      category: category.value
    })
    alert('수정 완료되었습니다.')
    router.push('/my-companies')
  } catch (e) {
    alert('수정 실패')
  }
}

const deleteCompany = async () => {
  try {
    await companyStore.deleteCompany(id)
    alert('삭제 하였습니다.')
    router.push('/my-companies')
  } catch (e) {
    alert('삭제 실패: ' + (e?.message || '알 수 없는 오류'))
    console.error(e)
  }
}



</script>

 

src/views/MyCompanies.vue - 회원별 등록 업체

<!-- src/views/MyCompanies.vue -->
<template>
  <v-container>
    <v-card class="pa-4" max-width="700" mx-auto>
      <v-card-title>내 업체 목록</v-card-title>

      <v-list>
        <v-list-item
          v-for="company in companyStore.companies"
          :key="company.id"
          class="border mb-2"
          @click="goToEdit(company.id)"
          clickable
        >
          <v-list-item-title class="font-weight-bold">
            {{ company.name }}
          </v-list-item-title>
          <v-list-item-subtitle>
            업종: {{ company.category }}
          </v-list-item-subtitle>
          <v-list-item-subtitle>
            소개: {{ company.description || '없음' }}
          </v-list-item-subtitle>
        </v-list-item>        
      </v-list>
    </v-card>
  </v-container>
</template>

<script setup>
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useCompanyStore } from '@/stores/companyStore'

const companyStore = useCompanyStore()
const router = useRouter()

const goToEdit = (id) => {
  router.push(`/edit-company/${id}`)
}


onMounted(() => {
  companyStore.fetchMyCompanies()
})
</script>

.multiline-subtitle {
  white-space: pre-line;
}