미용실 예약 커스터마이징
예약 관리 시스템을 고도화합니다.
미용실에 대해 커스터마이징을 진행해 봅시다.
미용실 예약을 할 때 서비스를 선택할 수 있도록 합니다.
이를 위하여 서비스를 등록, 수정, 삭제할 수 있는 관리 기능을 구현합니다.
‘상점 보기’에서 서비스업 상점의 경우 ‘서비스(메뉴) 관리’ 링크가 추가되었습니다.
서비스 관리 페이지에서 등록된 서비스 리스트를 볼 수 있고
서비스를 등록할 수도 있고,
등록된 서비스를 선택하면 서비스를 수정할 수도 있습니다.
서비스 리스트 기능은 서비스 등록에도 사용되므로
상점의 관리자가 아닌 경우 ‘서비스 등록’ 버튼이 보이지 않습니다.
이를 위하여 현재 로그인한 회원이 상점의 관리자인지 확인할 수 있어야 합니다.
상점의 관리자는 company.ownerId가 현재 로그인한 user.uid와 같으면 상점의 관리자입니다.
서비서업인 경우 상점 상세 보기에서도 ‘서비스 보기’로 등록된 서비스를 볼 수 있습니다.
관리자는 예약 관리에서 예약 시간과 함께 고객의 서비스도 확인할 수 있습니다.
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
categories
const categories = [
"커트",
"염색",
"펌",
"클리닉",
"스타일링",
"특수 서비스"
]
service
{
"category": "펌",
"name": "디지털펌",
"description": "자연스러운 웨이브, 손질이 쉬움",
"price": 100000
}
firestore services collection
'companies/${companyId}/services'
1. 관리자 여부 확인
- 관리 페이지에 접근하기 위해서는 관리자임을 확인해야 합니다.
관리자 여부를 확인하는 함수 추가
- src/stores/companyStore.js
로그인한 회원의 user.uid와 companies 컬렉션의 상점 문서의 ownerId가 같으면 관리자입니다.
checkAdmin()
const isAdmin = snapshot.exists() && snapshot.data().ownerId === user.uid
// src/stores/companyStore.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { db } from '@/firebase'
import {
collection,
doc,
addDoc,
getDoc,
updateDoc,
deleteDoc,
query,
where,
getDocs,
serverTimestamp
} from 'firebase/firestore'
import { useAuthStore } from './authStore'
export const useCompanyStore = defineStore('company', () => {
const companies = ref([])
const company = ref(null)
const adminMap = ref({}) // 🔸 companyId별 관리자 여부 캐시
// 🔹 업체 등록
const addCompany = async (company) => {
const authStore = useAuthStore()
const user = authStore.user
if (!user?.uid) throw new Error('로그인이 필요합니다.')
company.ownerId = user.uid
company.createdAt = serverTimestamp()
await addDoc(collection(db, 'companies'), company)
}
// 🔹 companyId로 등록 업체 조회
const fetchCompany = async (companyId) => {
try {
const docRef = doc(db, 'companies', companyId)
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
company.value = docSnap.data()
} else {
console.error('업체 정보를 찾을 수 없습니다.')
}
} catch (err) {
console.error('업체 정보 조회 실패:', err)
}
}
// 🔹 회원별 등록 업체 조회
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 = []
}
}
// 🔹 관리자 여부 확인
const checkAdmin = async (companyId) => {
const authStore = useAuthStore()
const user = authStore.user
if (!user?.uid || !companyId) return false
if (adminMap.value[companyId] !== undefined) {
return adminMap.value[companyId]
}
const companyRef = doc(db, 'companies', companyId)
const snapshot = await getDoc(companyRef)
const isAdmin = snapshot.exists() && snapshot.data().ownerId === user.uid
adminMap.value[companyId] = isAdmin
return isAdmin
}
return {
companies,
company,
addCompany,
fetchCompany,
fetchMyCompanies,
updateCompany,
fetchAllCompanies,
deleteCompany,
checkAdmin
}
})
2. 미용실 서비스 관리
- 관리자만 서비스를 등록할 수 있습니다.
1. 상점에 서비스 관리 링크 추가
- src/views/MyCompanies.vue
<!-- src/views/MyCompanies.vue-->
<template>
<v-container>
<v-card class="pa-4 mx-auto" max-width="1200">
<v-card-title class="pa-0 mb-4">내 업체 목록</v-card-title>
<v-row dense>
<v-col
v-for="company in companyStore.companies"
:key="company.id"
cols="12"
sm="6"
>
<v-card class="pa-3 h-100" outlined>
<v-card-title class="font-weight-bold pa-0">
{{ company.name }}
</v-card-title>
<v-card-subtitle class="pa-0 pt-1">
<strong>업종:</strong> {{ company.category }}
</v-card-subtitle>
<v-card-subtitle class="pa-0 pt-1">
<strong>영업시간:</strong>
{{ company?.openTime || '--' }} ~ {{ company?.closeTime || '--' }}
</v-card-subtitle>
<v-card-subtitle class="pa-0 pt-1">
<strong>주소:</strong> {{ company.address || '--' }}
</v-card-subtitle>
<v-card-subtitle class="pa-0 pt-1">
<strong>상세주소:</strong> {{ company.detailAddress || '--' }}
</v-card-subtitle>
<v-card-text class="pa-0 pt-2 multiline-subtitle">
<strong>소개:</strong> {{ company.description || '없음' }}
</v-card-text>
<v-card-actions class="pa-0 pt-3">
<v-btn
size="small"
color="primary"
@click="goToEdit(company.id)"
>
수정
</v-btn>
<v-spacer />
<v-btn
v-if="company.category === '서비스업'"
size="small"
color="secondary"
class="mt-1"
@click="goToRegisterService(company.id, company.name)"
>
서비스(메뉴) 관리
</v-btn>
<v-btn
size="small"
color="secondary"
class="mt-1"
@click="goToReservationManagement(company.id, company.name)"
>
예약 관리
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</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}`)
}
const goToRegisterService = (id, name) => {
router.push({
name: 'ServiceList',
params: { companyId: id },
query: { companyName: name }
})
}
const goToReservationManagement = (companyId, companyName) => {
router.push({
path: `/company-reservations/${companyId}`,
query: {
companyName: companyName,
},
})
}
onMounted(() => {
companyStore.fetchMyCompanies()
})
</script>
<style scoped>
.multiline-subtitle {
white-space: pre-line;
}
</style>
2. 미용실 서비스 리스트
- src/views/ServiceList.vue
<!-- src/views/ServiceList.vue -->
<template>
<v-container>
<v-icon
@click="goBack"
style="cursor: pointer;"
aria-label="뒤로가기"
>
mdi-arrow-left
</v-icon>
<v-card class="pa-4 max-w-800 mx-auto">
<v-card-title>
{{ companyName }} - 서비스 목록
<v-spacer />
<v-btn
v-if="isAdmin"
color="primary"
class="mt-2"
@click="goToAddService"
>
+ 서비스 등록
</v-btn>
</v-card-title>
<v-data-table
:headers="headers"
:items="services"
:loading="loadingServices"
class="elevation-1"
@click:row="(event, item) => goToEditService(item)"
item-key="id"
>
<template #item.price="{ item }">
{{ formatPrice(item.price) }}
</template>
<template #no-data>
등록된 서비스가 없습니다.
</template>
<template #loading>
로딩 중...
</template>
</v-data-table>
</v-card>
</v-container>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { useServiceManagement } from '@/composables/useServiceManagement'
import { useCompanyStore } from '@/stores/companyStore'
import { ref, onMounted } from 'vue'
const companyStore = useCompanyStore()
const isAdmin = ref(false)
onMounted(async () => {
isAdmin.value = await companyStore.checkAdmin(companyId)
})
const route = useRoute()
const router = useRouter()
const companyId = route.params.companyId
const companyName = route.query.companyName || '회사'
// Vuetify 3 에 맞게 헤더는 title, key로 설정
const headers = [
{ title: '분류', key: 'category' },
{ title: '서비스명', key: 'name' },
{ title: '설명', key: 'description' },
{ title: '가격', key: 'price' },
]
const { services, loadingServices } = useServiceManagement(companyId)
function formatPrice(price) {
if (price == null || price === '') return '가격 미정'
const num = Number(price)
if (isNaN(num)) return '가격 미정'
return num.toLocaleString() + ' 원'
}
function goToAddService() {
router.push({
name: 'ServiceManagement',
params: { companyId },
query: { companyName },
})
}
function goToEditService({item}) {
router.push({
name: 'ServiceManagement',
params: { companyId, serviceId: item.id },
query: { companyName },
})
}
function goBack() {
router.back()
}
</script>
<style scoped>
/* 클릭 가능한 행에 커서 표시 */
.v-data-table tbody tr {
cursor: pointer;
}
</style>
3. 미용실 서비스 등록, 수정
- 서비스 목록에서 서비스를 선택하면 수정 모드입니다.
서비스 등록, 수정, 삭제
- src/views/ServiceManagement.vue
<!-- src/views/ServiceManagement.vue -->
<template>
<v-container>
<v-card class="pa-4 max-w-600 mx-auto">
<v-card-title class="d-flex align-center justify-space-between">
<div class="text-h6">
{{ isEditMode ? '서비스 수정' : companyName + ' - 서비스 등록' }}
</div>
<v-btn text @click="goToList">서비스 목록</v-btn>
</v-card-title>
<v-form ref="formRef" @submit.prevent="onSubmit">
<!-- v-chip-group으로 분류 선택 -->
<div class="mb-4">
<label class="mb-2" style="font-weight: 500;">분류</label>
<v-chip-group
v-model="form.category"
mandatory
active-class="primary--text"
column
:max="1"
>
<v-chip
v-for="item in categories"
:key="item"
:value="item"
outlined
class="ma-1"
>
{{ item }}
</v-chip>
</v-chip-group>
</div>
<v-text-field
v-model="form.name"
label="서비스명"
required
/>
<v-textarea
v-model="form.description"
label="설명"
/>
<v-text-field
v-model="form.price"
label="가격"
type="number"
/>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
type="submit"
:loading="loading"
>
{{ isEditMode ? '수정 완료' : '등록' }}
</v-btn>
<v-btn
v-if="isEditMode"
color="error"
@click="onDelete"
:loading="loading"
>
삭제
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-container>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { useServiceManagement } from '@/composables/useServiceManagement'
const route = useRoute()
const router = useRouter()
const companyId = route.params.companyId
const serviceId = route.params.serviceId || null
const companyName = route.query.companyName || ''
const {
form,
formRef,
categories,
submit,
remove,
loading,
isEditMode,
} = useServiceManagement(companyId, serviceId)
const onSubmit = async () => {
const success = await submit()
if (success) {
alert(isEditMode.value ? '서비스가 수정되었습니다.' : '서비스가 등록되었습니다.')
goToList()
} else {
alert('오류가 발생했습니다.')
}
}
const onDelete = async () => {
if (!confirm('정말 삭제하시겠습니까?')) return
const success = await remove()
if (success) {
alert('서비스가 삭제되었습니다.')
goToList()
} else {
alert('삭제 중 오류가 발생했습니다.')
}
}
function goToList() {
router.push({
name: 'ServiceList', // 서비스 목록 페이지 라우트 이름 (확인 필요)
params: { companyId },
query: { companyName },
})
}
</script>
3. 미용실 서비스 보기 - 회원
1. 상점 리스트 - 홈
2. 상점 상세 보기
- 서비스 보기 링크가 있습니다.
- src/views/CompanyDetail.vue
<!-- src/views/CompanyDetail.vue -->
<template>
<v-container fluid>
<div class="d-flex justify-center">
<v-card class="pa-4" width="900">
<v-card-title>업체 상세 정보</v-card-title>
<v-card-text>
<div class="mb-3">
<strong>업체명:</strong>
{{ company?.name || '없음' }}
</div>
<div class="mb-3">
<strong>업종:</strong>
{{ company?.category || '없음' }}
</div>
<div class="mb-3">
<strong>소개글:</strong><br />
<div class="multiline-text">
{{ company?.description || '없음' }}
</div>
</div>
<!-- 영업시간 및 상태 표시 -->
<div class="mb-3">
<strong>영업시간:</strong>
{{ company?.openTime || '--' }} ~ {{ company?.closeTime || '--' }}
</div>
<div class="mb-3" :class="isOpen ? 'text-success' : 'text-error'">
<strong>영업 상태:</strong>
{{ isOpen ? '영업중' : '영업 종료' }}
</div>
<v-card-subtitle class="pa-0 pt-1">
<strong>주소:</strong> {{ company.address || '--' }}
<v-btn
size="small"
class="ml-2"
variant="text"
color="blue"
@click.stop="goToMap(company)"
>
지도 보기
</v-btn>
</v-card-subtitle>
<v-card-subtitle class="pa-0 pt-1">
<strong>상세주소:</strong> {{ company.detailAddress || '--' }}
</v-card-subtitle>
</v-card-text>
<v-card-actions class="justify-end">
<!-- 🔹 서비스 목록 버튼 추가 -->
<v-btn
v-if="company.category === '서비스업'"
color="secondary"
@click="goToServiceList(company.id, company.name)">
서비스 보기
</v-btn>
<v-btn
v-if="authStore.user"
color="primary"
@click="goToReservation(company.id)"
:disabled="!isOpen"
>
예약하기
</v-btn>
<v-btn color="grey" @click="goBack">홈</v-btn>
</v-card-actions>
</v-card>
</div>
</v-container>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCompanyStore } from '@/stores/companyStore'
import { useAuthStore } from '@/stores/authStore'
const route = useRoute()
const router = useRouter()
const companyStore = useCompanyStore()
const authStore = useAuthStore()
const id = route.params.id
const company = computed(() =>
companyStore.companies.find((c) => c.id === id)
)
onMounted(() => {
if (!company.value) {
companyStore.fetchMyCompanies()
}
})
// 현재 시간이 영업시간 내인지 계산하는 함수
const isOpen = computed(() => {
if (!company.value?.openTime || !company.value?.closeTime) return false
const now = new Date()
// 현재 시간을 "HH:mm" 형태로 맞추기
const nowStr = now.toTimeString().slice(0, 5)
// "HH:mm" -> 분으로 변환 (예: 13:30 => 13*60 + 30)
const toMinutes = (timeStr) => {
const [h, m] = timeStr.split(':').map(Number)
return h * 60 + m
}
const openMinutes = toMinutes(company.value.openTime)
const closeMinutes = toMinutes(company.value.closeTime)
const nowMinutes = toMinutes(nowStr)
// 영업시간이 자정 넘는 경우(예: 22:00~02:00) 처리
if (closeMinutes < openMinutes) {
return nowMinutes >= openMinutes || nowMinutes < closeMinutes
} else {
return nowMinutes >= openMinutes && nowMinutes < closeMinutes
}
})
const goToReservation = (companyId) => {
router.push({
path: '/reservation',
query: {
companyId,
username: authStore.profile.name
}
})
}
const goToMap = (company) => {
router.push({
path: '/map',
query: {
address: company.address,
name: company.name,
},
})
}
const goToServiceList = (id, name) => {
router.push({
name: 'ServiceList',
params: { companyId: id },
query: { companyName: name }
})
}
const goBack = () => {
router.push('/')
}
</script>
<style scoped>
.multiline-text {
white-space: pre-line;
}
.text-success {
color: green;
}
.text-error {
color: red;
}
</style>
3. 미용실 서비스 리스트
4. 미용실 예약 하기
- 예약할 때 서비스를 선택해야 합니다.
- src/views/Reservation.vue
<!-- src/views/Reservation.vue -->
<template>
<v-container max-width="800" class="mx-auto">
<!-- ... 기존 코드 ... -->
<v-form @submit.prevent="submitReservation" ref="formRef">
<v-card class="pa-4" elevation="2">
<v-card-title class="text-h6 font-weight-bold">
{{ companyName }} ({{ companyCategory }}) - 예약하기
</v-card-title>
<!-- 영업시간 및 상태 표시 -->
<span class="mb-3 ml-4 mr-4">
<strong>영업시간:</strong>
{{ openTime || '--' }} ~ {{ closeTime || '--' }}
</span>
<strong>영업 상태:</strong>
<span class="mb-3 ml-2" :class="isOpen ? 'text-success' : 'text-error'">
{{ isOpen ? '영업중' : '영업 종료' }}
</span>
<v-card-subtitle class="text-end">{{ username }} </v-card-subtitle>
<v-card-text>
<!-- 서비스 상품 선택 추가 -->
<v-select
v-model="form.serviceId"
:items="services"
item-title="name"
item-value="id"
label="서비스 선택"
:rules="[rules.required]"
required
dense
class="mb-4"
clearable
></v-select>
<!-- 날짜 선택 -->
<v-text-field
label="예약 날짜"
v-model="form.date"
type="date"
:rules="[rules.required, rules.notPast]"
required
dense
class="mb-4"
/>
<!-- 시간 선택 -->
<div class="mb-2 font-weight-medium">시간 선택</div>
<v-input
v-model="form.timeSlots"
:rules="[v => v.length > 0 || '최소 1개 이상의 시간을 선택하세요.']"
hide-details="auto"
>
<v-row class="mb-4">
<v-col v-for="slot in allTimeSlots" :key="slot" cols="3" class="pa-0">
<v-checkbox
v-model="form.timeSlots"
:label="slot"
:value="slot"
color="primary"
hide-details
density="compact"
class="ma-0"
/>
</v-col>
</v-row>
</v-input>
<!-- 메모 -->
<v-textarea
label="메모"
v-model="form.memo"
auto-grow
clearable
rows="2"
class="mb-4 mt-4"
/>
<!-- 버튼 -->
<v-btn type="submit" color="primary" block>예약하기</v-btn>
</v-card-text>
</v-card>
</v-form>
<!-- 예약 성공 모달 -->
<!-- 예약 성공 모달 -->
<v-dialog
v-model="dialog"
max-width="400"
@click:outside="closeDialog"
@keydown.esc="closeDialog"
>
<v-card v-if="reservationResult">
<v-card-title class="headline">예약 완료</v-card-title>
<v-card-text>
<v-table>
<tbody>
<tr>
<th class="text-left"></th>
<td>{{ reservationResult.companyName }}</td>
</tr>
<tr>
<th class="text-left">예약 번호</th>
<td>{{ reservationResult.reservationNumber }}</td>
</tr>
<tr>
<th class="text-left">예약 날짜</th>
<td>{{ reservationResult.date }}</td>
</tr>
<tr>
<th class="text-left">시간</th>
<td>{{ reservationResult.timeSlots?.join(', ') || '' }}</td>
</tr>
<tr>
<th class="text-left">서비스</th>
<td>{{ reservationResult.serviceName || '없음' }}</td>
</tr>
<tr>
<th class="text-left">메모</th>
<td>{{ reservationResult.memo || '없음' }}</td>
</tr>
<tr>
<th class="text-left">상태</th>
<td>{{ reservationResult.status }}</td>
</tr>
<tr>
<th class="text-left">예약 시간</th>
<td>{{ formatDateTime(reservationResult.createdAt) }}</td>
</tr>
<tr>
<th class="text-left">예약 ID</th>
<td>{{ reservationResult.id }}</td>
</tr>
</tbody>
</v-table>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" text @click="closeDialog">확인</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCompanyStore } from '@/stores/companyStore'
import { useReservationStore } from '@/stores/reservationStore'
import { useServiceManagement } from '@/composables/useServiceManagement'
const router = useRouter()
const route = useRoute()
const companyId = route.query.companyId ?? ''
const username = route.query.username ?? ''
const selectedService = computed(() =>
services.value?.find(service => service.id === form.value.serviceId)
)
const companyStore = useCompanyStore()
const reservationStore = useReservationStore()
// 서비스를 가져온다.
const { services, loadingServices } = useServiceManagement(companyId)
const formRef = ref()
const today = new Date().toISOString().split('T')[0]
const form = ref({
companyId: '',
companyName: '',
serviceId: null,
serviceName: null,
date: today,
timeSlots: [],
memo: '',
})
const allTimeSlots = [
'09:00~09:30', '09:30~10:00',
'10:00~10:30', '10:30~11:00',
'11:00~11:30', '11:30~12:00',
'12:00~12:30', '12:30~13:00',
'13:00~13:30', '13:30~14:00',
'14:00~14:30', '14:30~15:00',
'15:00~15:30', '15:30~16:00',
'16:00~16:30', '16:30~17:00',
'17:00~17:30', '17:30~18:00',
]
const rules = {
required: v => (Array.isArray(v) ? v.length > 0 : !!v) || '필수 입력 항목입니다',
notPast: v => {
if (!v) return true
const selectedDate = new Date(v)
const today = new Date()
today.setHours(0, 0, 0, 0) // 오늘 00:00으로 초기화
return selectedDate >= today || '오늘 이전 날짜는 선택할 수 없습니다'
}
}
const dialog = ref(false)
const reservationResult = ref(null)
const company = computed(() => companyStore.company)
const companyName = computed(() => company.value?.name || '')
const companyCategory = computed(() => company.value?.category || '')
const openTime = computed(() => company.value?.openTime || '00:00')
const closeTime = computed(() => company.value?.closeTime || '00:00')
const submitReservation = async () => {
const { valid } = await formRef.value.validate()
if (!valid) return
try {
form.value.companyId = companyId
form.value.companyName = companyName.value
form.value.serviceName = selectedService.value?.name;
const res = await reservationStore.submitReservation(form.value)
reservationResult.value = res
dialog.value = true
} catch (err) {
alert('예약 실패: ' + (err.response?.data?.message || err.message))
}
}
const closeDialog = () => {
dialog.value = false
reservationResult.value = null
router.push('/my-reservations')
}
const formatDateTime = (value) => {
const date = new Date(value)
return date.toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
const isOpen = computed(() => {
if (!company.value?.openTime || !company.value?.closeTime) return false
const now = new Date()
const nowStr = now.toTimeString().slice(0, 5)
const toMinutes = (timeStr) => {
const [h, m] = timeStr.split(':').map(Number)
return h * 60 + m
}
const openMinutes = toMinutes(company.value.openTime)
const closeMinutes = toMinutes(company.value.closeTime)
const nowMinutes = toMinutes(nowStr)
if (closeMinutes < openMinutes) {
return nowMinutes >= openMinutes || nowMinutes < closeMinutes
} else {
return nowMinutes >= openMinutes && nowMinutes < closeMinutes
}
})
onMounted(async () => {
if (companyId) {
await companyStore.fetchCompany(companyId)
}
})
</script>
4. 라우터
- 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')
},
{
path: '/reservation',
name: 'Reservation',
component: () => import('@/views/Reservation.vue')
},
{
path: '/my-reservations',
name: 'MyReservations',
component: () => import('@/views/MyReservations.vue'),
meta: { requiresAuth: true } // 로그인 필요하면
},
{
path: '/company-reservations/:companyId',
name: 'CompanyReservations',
component: () => import('@/views/CompanyReservations.vue')
},
{
path: '/map',
name: 'MapView',
component: () => import('@/views/MapView.vue'),
},
{
path: '/companies/:companyId/services',
name: 'ServiceList',
component: () => import('@/views/ServiceList.vue'),
props: route => ({ companyName: route.query.companyName })
},
{
path: '/companies/:companyId/services/:serviceId?',
name: 'ServiceManagement',
component: () => import('@/views/ServiceManagement.vue'),
props: route => ({ companyName: route.query.companyName })
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
'예약 포털 (Vue3 + Firebase) - 서비스 오픈까지' 카테고리의 다른 글
18. 동네 포털 (Vue 3 + Vuetify + Firebase) - 메뉴 카테고리 관리 (0) | 2025.06.08 |
---|---|
17. 동네 포털 - v-expansion-panels, v-data-table, vuedraggable (2) | 2025.06.07 |
15. 동네 (예약) 포털 (Vue 3 + Firebase) - 네이버 지도 상점 위치 보기 개선 (6) | 2025.06.05 |
14. 동네 (예약) 포털 (Vue 3 + Firebase) - 네이버 지도에 업체 위치 보기 (2) | 2025.06.04 |
13. 동네 (예약) 포털 (Vue 3 + Firebase) - 네이버 지도 sdk 연동 가이드 (0) | 2025.06.04 |