views files add
This commit is contained in:
parent
c052e3300f
commit
43066ca623
478
src/views/DashboardView.vue
Normal file
478
src/views/DashboardView.vue
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid class="pa-4">
|
||||||
|
<!-- 환영 메시지 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card class="bg-gradient-primary" elevation="4">
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h2 class="text-h5 text-white font-weight-bold mb-2">
|
||||||
|
안녕하세요, {{ authStore.user?.nickname }}님! 👋
|
||||||
|
</h2>
|
||||||
|
<p class="text-white opacity-90 mb-0">
|
||||||
|
오늘도 성공적인 마케팅을 위해 함께해요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<v-img
|
||||||
|
src="/images/ai-character.png"
|
||||||
|
max-width="80"
|
||||||
|
class="ml-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 주요 지표 카드 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col
|
||||||
|
v-for="metric in dashboardMetrics"
|
||||||
|
:key="metric.title"
|
||||||
|
cols="6"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<v-icon :color="metric.color" size="24">{{ metric.icon }}</v-icon>
|
||||||
|
<v-chip
|
||||||
|
:color="metric.trend === 'up' ? 'success' : metric.trend === 'down' ? 'error' : 'warning'"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
<v-icon size="16" class="mr-1">
|
||||||
|
{{ metric.trend === 'up' ? 'mdi-trending-up' :
|
||||||
|
metric.trend === 'down' ? 'mdi-trending-down' : 'mdi-minus' }}
|
||||||
|
</v-icon>
|
||||||
|
{{ metric.change }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<p class="text-caption text-grey mb-1">{{ metric.title }}</p>
|
||||||
|
<h3 class="text-h6 font-weight-bold">{{ metric.value }}</h3>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 빠른 액션 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-flash</v-icon>
|
||||||
|
빠른 액션
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="action in quickActions"
|
||||||
|
:key="action.title"
|
||||||
|
cols="6"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
:color="action.color"
|
||||||
|
variant="tonal"
|
||||||
|
class="pa-4 flex-column"
|
||||||
|
style="height: 80px;"
|
||||||
|
@click="action.action"
|
||||||
|
>
|
||||||
|
<v-icon size="28" class="mb-1">{{ action.icon }}</v-icon>
|
||||||
|
<span class="text-caption">{{ action.title }}</span>
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- AI 추천 & 매출 차트 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<!-- AI 마케팅 추천 -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
<v-icon class="mr-2" color="purple">mdi-robot</v-icon>
|
||||||
|
AI 마케팅 추천
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
@click="refreshAIRecommendations"
|
||||||
|
:loading="aiLoading"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-refresh</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div v-if="aiRecommendations.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(recommendation, index) in aiRecommendations"
|
||||||
|
:key="index"
|
||||||
|
class="mb-4 last:mb-0"
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
:type="recommendation.type"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2">{{ recommendation.icon }}</v-icon>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h4 class="text-subtitle-2 font-weight-bold mb-1">
|
||||||
|
{{ recommendation.title }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-body-2 mb-0">{{ recommendation.content }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center pa-4">
|
||||||
|
<v-icon size="48" color="grey-lighten-2">mdi-robot-outline</v-icon>
|
||||||
|
<p class="text-grey mt-2">AI 추천을 가져오는 중...</p>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- 매출 차트 -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
<v-icon class="mr-2" color="success">mdi-chart-line</v-icon>
|
||||||
|
매출 현황
|
||||||
|
<v-spacer />
|
||||||
|
<v-select
|
||||||
|
v-model="chartPeriod"
|
||||||
|
:items="chartPeriods"
|
||||||
|
item-title="text"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
style="max-width: 120px;"
|
||||||
|
@update:model-value="updateChart"
|
||||||
|
/>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="chart-container" style="height: 200px;">
|
||||||
|
<canvas ref="salesChart" />
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 최근 활동 & 콘텐츠 성과 -->
|
||||||
|
<v-row>
|
||||||
|
<!-- 최근 활동 -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
<v-icon class="mr-2" color="info">mdi-history</v-icon>
|
||||||
|
최근 활동
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="activity in recentActivities"
|
||||||
|
:key="activity.id"
|
||||||
|
class="px-0"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-avatar :color="activity.color" size="32">
|
||||||
|
<v-icon color="white" size="16">{{ activity.icon }}</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title class="text-body-2">
|
||||||
|
{{ activity.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="text-caption">
|
||||||
|
{{ formatRelativeTime(activity.timestamp) }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- 콘텐츠 성과 -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
<v-icon class="mr-2" color="orange">mdi-chart-donut</v-icon>
|
||||||
|
콘텐츠 성과
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div v-if="contentPerformance.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="content in contentPerformance"
|
||||||
|
:key="content.id"
|
||||||
|
class="d-flex align-center justify-space-between py-2"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-chip
|
||||||
|
:color="content.platform === 'instagram' ? 'purple' : 'blue'"
|
||||||
|
size="small"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
{{ content.platform }}
|
||||||
|
</v-chip>
|
||||||
|
<span class="text-body-2">{{ content.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-caption text-grey">조회수</div>
|
||||||
|
<div class="font-weight-bold">{{ formatNumber(content.views) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center pa-4">
|
||||||
|
<v-icon size="48" color="grey-lighten-2">mdi-chart-donut</v-icon>
|
||||||
|
<p class="text-grey mt-2">콘텐츠 성과 데이터가 없습니다</p>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 로딩 오버레이 -->
|
||||||
|
<v-overlay v-if="loading" class="align-center justify-center">
|
||||||
|
<v-progress-circular
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
/>
|
||||||
|
</v-overlay>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore, useStoreStore } from '@/store/index'
|
||||||
|
import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/formatters'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const storeStore = useStoreStore()
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const loading = ref(false)
|
||||||
|
const aiLoading = ref(false)
|
||||||
|
const chartPeriod = ref('week')
|
||||||
|
const salesChart = ref(null)
|
||||||
|
|
||||||
|
// 대시보드 지표
|
||||||
|
const dashboardMetrics = ref([
|
||||||
|
{
|
||||||
|
title: '오늘 매출',
|
||||||
|
value: '₩567,000',
|
||||||
|
change: '+12%',
|
||||||
|
trend: 'up',
|
||||||
|
icon: 'mdi-cash',
|
||||||
|
color: 'success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '월 매출',
|
||||||
|
value: '₩12,340,000',
|
||||||
|
change: '+8%',
|
||||||
|
trend: 'up',
|
||||||
|
icon: 'mdi-chart-line',
|
||||||
|
color: 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '콘텐츠 수',
|
||||||
|
value: '24',
|
||||||
|
change: '+3',
|
||||||
|
trend: 'up',
|
||||||
|
icon: 'mdi-file-document',
|
||||||
|
color: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '조회수',
|
||||||
|
value: '15.2K',
|
||||||
|
change: '+25%',
|
||||||
|
trend: 'up',
|
||||||
|
icon: 'mdi-eye',
|
||||||
|
color: 'warning'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 빠른 액션
|
||||||
|
const quickActions = ref([
|
||||||
|
{
|
||||||
|
title: 'SNS 콘텐츠',
|
||||||
|
icon: 'mdi-plus-circle',
|
||||||
|
color: 'primary',
|
||||||
|
action: () => router.push('/content/create?type=sns')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '포스터 생성',
|
||||||
|
icon: 'mdi-image-plus',
|
||||||
|
color: 'secondary',
|
||||||
|
action: () => router.push('/content/create?type=poster')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '메뉴 등록',
|
||||||
|
icon: 'mdi-food-apple',
|
||||||
|
color: 'success',
|
||||||
|
action: () => router.push('/menu')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '매출 분석',
|
||||||
|
icon: 'mdi-chart-bar',
|
||||||
|
color: 'info',
|
||||||
|
action: () => router.push('/sales')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// AI 추천
|
||||||
|
const aiRecommendations = ref([
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
icon: 'mdi-weather-rainy',
|
||||||
|
title: '날씨 기반 추천',
|
||||||
|
content: '오늘은 비가 와서 따뜻한 음식이 인기 있을 것 같아요. 국물 요리를 추천해보세요!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
icon: 'mdi-trending-up',
|
||||||
|
title: '트렌드 알림',
|
||||||
|
content: '최근 #떡볶이챌린지가 인기입니다. 관련 콘텐츠를 만들어보세요.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
icon: 'mdi-clock-outline',
|
||||||
|
title: '시간대 팁',
|
||||||
|
content: '점심시간(12-14시)에 주문이 집중됩니다. 미리 준비하세요.'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 차트 기간 옵션
|
||||||
|
const chartPeriods = ref([
|
||||||
|
{ text: '일주일', value: 'week' },
|
||||||
|
{ text: '한달', value: 'month' },
|
||||||
|
{ text: '3개월', value: 'quarter' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 최근 활동
|
||||||
|
const recentActivities = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'SNS 콘텐츠 "떡볶이 신메뉴 출시" 발행',
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 30),
|
||||||
|
icon: 'mdi-instagram',
|
||||||
|
color: 'purple'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '메뉴 "치즈떡볶이" 등록',
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
||||||
|
icon: 'mdi-food',
|
||||||
|
color: 'orange'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '매장 정보 업데이트',
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5),
|
||||||
|
icon: 'mdi-store',
|
||||||
|
color: 'blue'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 콘텐츠 성과
|
||||||
|
const contentPerformance = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '떡볶이 신메뉴 홍보',
|
||||||
|
platform: 'instagram',
|
||||||
|
views: 1240
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '매장 소개 포스터',
|
||||||
|
platform: 'blog',
|
||||||
|
views: 850
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '할인 이벤트 안내',
|
||||||
|
platform: 'instagram',
|
||||||
|
views: 2100
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 메서드
|
||||||
|
const refreshAIRecommendations = async () => {
|
||||||
|
try {
|
||||||
|
aiLoading.value = true
|
||||||
|
// API 호출 시뮬레이션
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
// AI 추천 데이터 갱신 로직
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI 추천 갱신 실패:', error)
|
||||||
|
} finally {
|
||||||
|
aiLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateChart = () => {
|
||||||
|
// 차트 업데이트 로직
|
||||||
|
console.log('차트 업데이트:', chartPeriod.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
// Chart.js를 사용한 차트 초기화 로직
|
||||||
|
// 실제 구현에서는 Chart.js 라이브러리 사용
|
||||||
|
console.log('차트 초기화')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이프사이클
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 대시보드 데이터 로드
|
||||||
|
await Promise.all([
|
||||||
|
// 매장 정보 로드 (필요시)
|
||||||
|
// storeStore.fetchStoreInfo(),
|
||||||
|
// AI 추천 로드
|
||||||
|
// refreshAIRecommendations()
|
||||||
|
])
|
||||||
|
|
||||||
|
// 차트 초기화
|
||||||
|
await nextTick()
|
||||||
|
initChart()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('대시보드 로드 실패:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bg-gradient-primary {
|
||||||
|
background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-100 {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.text-h5 {
|
||||||
|
font-size: 1.3rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
343
src/views/LoginView.vue
Normal file
343
src/views/LoginView.vue
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid class="fill-height pa-0">
|
||||||
|
<v-row no-gutters class="fill-height">
|
||||||
|
<v-col cols="12" class="d-flex flex-column">
|
||||||
|
<!-- 로고 및 헤더 -->
|
||||||
|
<div class="text-center pa-8 bg-primary">
|
||||||
|
<v-img
|
||||||
|
src="/images/logo.png"
|
||||||
|
alt="AI 마케팅 로고"
|
||||||
|
max-width="80"
|
||||||
|
class="mx-auto mb-4"
|
||||||
|
/>
|
||||||
|
<h1 class="text-h4 text-white font-weight-bold mb-2">
|
||||||
|
AI 마케팅
|
||||||
|
</h1>
|
||||||
|
<p class="text-white opacity-90">
|
||||||
|
소상공인을 위한 똑똑한 마케팅 솔루션
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 로그인 폼 -->
|
||||||
|
<div class="flex-grow-1 pa-6">
|
||||||
|
<v-card class="mx-auto" max-width="400" elevation="8">
|
||||||
|
<v-card-title class="text-h5 text-center pa-6">
|
||||||
|
로그인
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-form
|
||||||
|
ref="loginForm"
|
||||||
|
v-model="formValid"
|
||||||
|
@submit.prevent="login"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="loginData.userId"
|
||||||
|
label="아이디"
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="userIdRules"
|
||||||
|
required
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="loginData.password"
|
||||||
|
label="비밀번호"
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
|
variant="outlined"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||||
|
:rules="passwordRules"
|
||||||
|
required
|
||||||
|
@click:append-inner="showPassword = !showPassword"
|
||||||
|
@keyup.enter="login"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!formValid"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
로그인
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
@click="showRegisterDialog = true"
|
||||||
|
>
|
||||||
|
회원가입
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 푸터 -->
|
||||||
|
<div class="text-center pa-4 text-caption text-grey">
|
||||||
|
© 2024 AI Marketing Service. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 회원가입 다이얼로그 -->
|
||||||
|
<v-dialog v-model="showRegisterDialog" max-width="500" scrollable>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h6">
|
||||||
|
회원가입
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="showRegisterDialog = false"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-form ref="registerForm" v-model="registerFormValid">
|
||||||
|
<v-text-field
|
||||||
|
v-model="registerData.userId"
|
||||||
|
label="아이디"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="userIdRules"
|
||||||
|
required
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="registerData.password"
|
||||||
|
label="비밀번호"
|
||||||
|
variant="outlined"
|
||||||
|
type="password"
|
||||||
|
:rules="passwordRules"
|
||||||
|
required
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="registerData.confirmPassword"
|
||||||
|
label="비밀번호 확인"
|
||||||
|
variant="outlined"
|
||||||
|
type="password"
|
||||||
|
:rules="confirmPasswordRules"
|
||||||
|
required
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="registerData.nickname"
|
||||||
|
label="닉네임"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="nicknameRules"
|
||||||
|
required
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="registerData.businessNumber"
|
||||||
|
label="사업자등록번호"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="businessNumberRules"
|
||||||
|
required
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="registerData.email"
|
||||||
|
label="이메일"
|
||||||
|
variant="outlined"
|
||||||
|
type="email"
|
||||||
|
:rules="emailRules"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="pa-6">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey"
|
||||||
|
variant="text"
|
||||||
|
@click="showRegisterDialog = false"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="registerLoading"
|
||||||
|
:disabled="!registerFormValid"
|
||||||
|
@click="register"
|
||||||
|
>
|
||||||
|
가입하기
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 스낵바 -->
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.show"
|
||||||
|
:color="snackbar.color"
|
||||||
|
:timeout="3000"
|
||||||
|
location="top"
|
||||||
|
>
|
||||||
|
{{ snackbar.message }}
|
||||||
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/store/index'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const formValid = ref(false)
|
||||||
|
const registerFormValid = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const registerLoading = ref(false)
|
||||||
|
const showPassword = ref(false)
|
||||||
|
const showRegisterDialog = ref(false)
|
||||||
|
|
||||||
|
const loginData = reactive({
|
||||||
|
userId: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const registerData = reactive({
|
||||||
|
userId: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
nickname: '',
|
||||||
|
businessNumber: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const snackbar = reactive({
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
color: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 유효성 검사 규칙
|
||||||
|
const userIdRules = [
|
||||||
|
v => !!v || '아이디를 입력해주세요',
|
||||||
|
v => v.length >= 4 || '아이디는 4자 이상이어야 합니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
const passwordRules = [
|
||||||
|
v => !!v || '비밀번호를 입력해주세요',
|
||||||
|
v => v.length >= 8 || '비밀번호는 8자 이상이어야 합니다',
|
||||||
|
v => /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(v) || '영문, 숫자, 특수문자를 포함해야 합니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
const confirmPasswordRules = [
|
||||||
|
v => !!v || '비밀번호 확인을 입력해주세요',
|
||||||
|
v => v === registerData.password || '비밀번호가 일치하지 않습니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
const nicknameRules = [
|
||||||
|
v => !!v || '닉네임을 입력해주세요',
|
||||||
|
v => v.length >= 2 || '닉네임은 2자 이상이어야 합니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
const businessNumberRules = [
|
||||||
|
v => !!v || '사업자등록번호를 입력해주세요',
|
||||||
|
v => /^\d{3}-\d{2}-\d{5}$/.test(v) || '올바른 사업자등록번호 형식이 아닙니다 (예: 123-45-67890)'
|
||||||
|
]
|
||||||
|
|
||||||
|
const emailRules = [
|
||||||
|
v => !!v || '이메일을 입력해주세요',
|
||||||
|
v => /.+@.+\..+/.test(v) || '올바른 이메일 형식이 아닙니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 메서드
|
||||||
|
const login = async () => {
|
||||||
|
if (!formValid.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await authStore.login(loginData)
|
||||||
|
|
||||||
|
snackbar.show = true
|
||||||
|
snackbar.message = '로그인되었습니다'
|
||||||
|
snackbar.color = 'success'
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/dashboard')
|
||||||
|
}, 1000)
|
||||||
|
} catch (error) {
|
||||||
|
snackbar.show = true
|
||||||
|
snackbar.message = error.response?.data?.message || '로그인에 실패했습니다'
|
||||||
|
snackbar.color = 'error'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async () => {
|
||||||
|
if (!registerFormValid.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
registerLoading.value = true
|
||||||
|
await authStore.register(registerData)
|
||||||
|
|
||||||
|
snackbar.show = true
|
||||||
|
snackbar.message = '회원가입이 완료되었습니다'
|
||||||
|
snackbar.color = 'success'
|
||||||
|
|
||||||
|
showRegisterDialog.value = false
|
||||||
|
|
||||||
|
// 폼 초기화
|
||||||
|
Object.assign(registerData, {
|
||||||
|
userId: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
nickname: '',
|
||||||
|
businessNumber: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
snackbar.show = true
|
||||||
|
snackbar.message = error.response?.data?.message || '회원가입에 실패했습니다'
|
||||||
|
snackbar.color = 'error'
|
||||||
|
} finally {
|
||||||
|
registerLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fill-height {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.pa-8 {
|
||||||
|
padding: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-h4 {
|
||||||
|
font-size: 1.8rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
657
src/views/MenuManagementView.vue
Normal file
657
src/views/MenuManagementView.vue
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid class="pa-4">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex align-center justify-space-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-h4 font-weight-bold mb-2">메뉴 관리</h1>
|
||||||
|
<p class="text-grey">매장의 메뉴를 등록하고 관리할 수 있습니다</p>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openCreateDialog"
|
||||||
|
>
|
||||||
|
메뉴 추가
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 검색 및 필터 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
label="메뉴 검색"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
variant="outlined"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedCategory"
|
||||||
|
label="카테고리"
|
||||||
|
variant="outlined"
|
||||||
|
:items="categories"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="sortBy"
|
||||||
|
label="정렬"
|
||||||
|
variant="outlined"
|
||||||
|
:items="sortOptions"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 메뉴가 없는 경우 -->
|
||||||
|
<div v-if="filteredMenus.length === 0 && !menuStore.loading">
|
||||||
|
<v-row justify="center">
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card class="text-center pa-8" elevation="4">
|
||||||
|
<v-img
|
||||||
|
src="/images/menu-placeholder.png"
|
||||||
|
max-width="200"
|
||||||
|
class="mx-auto mb-4"
|
||||||
|
/>
|
||||||
|
<h2 class="text-h5 font-weight-bold mb-3">첫 메뉴를 등록해보세요!</h2>
|
||||||
|
<p class="text-grey mb-4">
|
||||||
|
메뉴를 등록하면 AI가 더 정확한 마케팅 콘텐츠를 생성할 수 있습니다
|
||||||
|
</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
prepend-icon="mdi-food-apple"
|
||||||
|
@click="openCreateDialog"
|
||||||
|
>
|
||||||
|
메뉴 등록하기
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 메뉴 목록 -->
|
||||||
|
<div v-else>
|
||||||
|
<!-- 메뉴 통계 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card elevation="2" class="text-center pa-4">
|
||||||
|
<h3 class="text-h4 font-weight-bold text-primary">{{ menuStore.totalCount }}</h3>
|
||||||
|
<p class="text-caption text-grey">전체 메뉴</p>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card elevation="2" class="text-center pa-4">
|
||||||
|
<h3 class="text-h4 font-weight-bold text-success">{{ availableMenuCount }}</h3>
|
||||||
|
<p class="text-caption text-grey">판매중</p>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card elevation="2" class="text-center pa-4">
|
||||||
|
<h3 class="text-h4 font-weight-bold text-info">{{ categoriesCount }}</h3>
|
||||||
|
<p class="text-caption text-grey">카테고리</p>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="3">
|
||||||
|
<v-card elevation="2" class="text-center pa-4">
|
||||||
|
<h3 class="text-h4 font-weight-bold text-warning">{{ formatCurrency(averagePrice) }}</h3>
|
||||||
|
<p class="text-caption text-grey">평균 가격</p>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 메뉴 그리드 -->
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="menu in filteredMenus"
|
||||||
|
:key="menu.id"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
lg="3"
|
||||||
|
>
|
||||||
|
<v-card elevation="2" class="h-100">
|
||||||
|
<!-- 메뉴 이미지 -->
|
||||||
|
<v-img
|
||||||
|
:src="menu.imageUrl || '/images/menu-placeholder.png'"
|
||||||
|
:alt="menu.menuName"
|
||||||
|
height="200"
|
||||||
|
cover
|
||||||
|
>
|
||||||
|
<div class="d-flex pa-2">
|
||||||
|
<v-spacer />
|
||||||
|
<v-chip
|
||||||
|
:color="menu.available ? 'success' : 'error'"
|
||||||
|
size="small"
|
||||||
|
variant="elevated"
|
||||||
|
>
|
||||||
|
{{ menu.available ? '판매중' : '품절' }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-img>
|
||||||
|
|
||||||
|
<!-- 메뉴 정보 -->
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
|
<v-chip
|
||||||
|
:color="getCategoryColor(menu.category)"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ menu.category }}
|
||||||
|
</v-chip>
|
||||||
|
<div class="d-flex">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editMenu(menu)"
|
||||||
|
>
|
||||||
|
<v-icon size="20">mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="confirmDelete(menu)"
|
||||||
|
>
|
||||||
|
<v-icon size="20">mdi-delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-h6 font-weight-bold mb-2">{{ menu.menuName }}</h3>
|
||||||
|
<p class="text-body-2 text-grey mb-3" style="min-height: 40px;">
|
||||||
|
{{ menu.description || '메뉴 설명이 없습니다' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="d-flex align-center justify-space-between">
|
||||||
|
<span class="text-h6 font-weight-bold text-primary">
|
||||||
|
{{ formatCurrency(menu.price) }}
|
||||||
|
</span>
|
||||||
|
<v-rating
|
||||||
|
:model-value="menu.rating || 0"
|
||||||
|
readonly
|
||||||
|
size="small"
|
||||||
|
color="warning"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 메뉴 등록/수정 다이얼로그 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showMenuDialog"
|
||||||
|
max-width="600"
|
||||||
|
persistent
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
<span class="text-h6">{{ editMode ? '메뉴 수정' : '메뉴 등록' }}</span>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="closeMenuDialog"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text class="pa-6" style="max-height: 500px;">
|
||||||
|
<v-form ref="menuForm" v-model="menuFormValid">
|
||||||
|
<!-- 메뉴 이미지 -->
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<v-img
|
||||||
|
:src="menuFormData.imageUrl || '/images/menu-placeholder.png'"
|
||||||
|
:alt="menuFormData.menuName"
|
||||||
|
max-width="200"
|
||||||
|
max-height="150"
|
||||||
|
class="mx-auto mb-3 rounded"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-camera"
|
||||||
|
@click="selectMenuImage"
|
||||||
|
>
|
||||||
|
이미지 선택
|
||||||
|
</v-btn>
|
||||||
|
<input
|
||||||
|
ref="menuImageInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none;"
|
||||||
|
@change="handleMenuImageUpload"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="8">
|
||||||
|
<v-text-field
|
||||||
|
v-model="menuFormData.menuName"
|
||||||
|
label="메뉴명 *"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[v => !!v || '메뉴명을 입력해주세요']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="menuFormData.price"
|
||||||
|
label="가격 *"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
prefix="₩"
|
||||||
|
:rules="priceRules"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-combobox
|
||||||
|
v-model="menuFormData.category"
|
||||||
|
label="카테고리 *"
|
||||||
|
variant="outlined"
|
||||||
|
:items="categories"
|
||||||
|
:rules="[v => !!v || '카테고리를 선택해주세요']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-switch
|
||||||
|
v-model="menuFormData.available"
|
||||||
|
label="판매 가능"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="menuFormData.description"
|
||||||
|
label="메뉴 설명"
|
||||||
|
variant="outlined"
|
||||||
|
rows="3"
|
||||||
|
counter="200"
|
||||||
|
:rules="[v => !v || v.length <= 200 || '200자 이내로 입력해주세요']"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="menuFormData.ingredients"
|
||||||
|
label="주요 재료"
|
||||||
|
variant="outlined"
|
||||||
|
hint="쉼표로 구분하여 입력"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select
|
||||||
|
v-model="menuFormData.spicyLevel"
|
||||||
|
label="매운맛 정도"
|
||||||
|
variant="outlined"
|
||||||
|
:items="spicyLevels"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="menuFormData.calories"
|
||||||
|
label="칼로리"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
suffix="kcal"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-switch
|
||||||
|
v-model="menuFormData.recommended"
|
||||||
|
label="추천 메뉴"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey"
|
||||||
|
variant="text"
|
||||||
|
@click="closeMenuDialog"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="!menuFormValid"
|
||||||
|
@click="saveMenu"
|
||||||
|
>
|
||||||
|
{{ editMode ? '수정하기' : '등록하기' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 삭제 확인 다이얼로그 -->
|
||||||
|
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h6">메뉴 삭제</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p>정말로 <strong>{{ deleteTarget?.menuName }}</strong> 메뉴를 삭제하시겠습니까?</p>
|
||||||
|
<v-alert type="warning" variant="tonal" class="mt-3">
|
||||||
|
삭제된 메뉴는 복구할 수 없습니다.
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey"
|
||||||
|
variant="text"
|
||||||
|
@click="showDeleteDialog = false"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:loading="deleting"
|
||||||
|
@click="deleteMenu"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 로딩 오버레이 -->
|
||||||
|
<v-overlay v-if="menuStore.loading" class="align-center justify-center">
|
||||||
|
<v-progress-circular
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
/>
|
||||||
|
</v-overlay>
|
||||||
|
|
||||||
|
<!-- 스낵바 -->
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.show"
|
||||||
|
:color="snackbar.color"
|
||||||
|
:timeout="3000"
|
||||||
|
location="top"
|
||||||
|
>
|
||||||
|
{{ snackbar.message }}
|
||||||
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useMenuStore, useAppStore } from '@/store/index'
|
||||||
|
import { formatCurrency } from '@/utils/formatters'
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedCategory = ref('')
|
||||||
|
const sortBy = ref('name')
|
||||||
|
const showMenuDialog = ref(false)
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
const editMode = ref(false)
|
||||||
|
const menuFormValid = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const deleting = ref(false)
|
||||||
|
const deleteTarget = ref(null)
|
||||||
|
const menuImageInput = ref(null)
|
||||||
|
|
||||||
|
const snackbar = reactive({
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
color: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 메뉴 폼 데이터
|
||||||
|
const menuFormData = reactive({
|
||||||
|
menuName: '',
|
||||||
|
price: 0,
|
||||||
|
category: '',
|
||||||
|
description: '',
|
||||||
|
ingredients: '',
|
||||||
|
spicyLevel: '보통',
|
||||||
|
calories: 0,
|
||||||
|
available: true,
|
||||||
|
recommended: false,
|
||||||
|
imageUrl: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 옵션 데이터
|
||||||
|
const categories = computed(() => {
|
||||||
|
const menuCategories = menuStore.menus.map(menu => menu.category)
|
||||||
|
const defaultCategories = ['면류', '튀김류', '음료', '안주', '디저트']
|
||||||
|
return [...new Set([...defaultCategories, ...menuCategories])]
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortOptions = [
|
||||||
|
{ title: '이름순', value: 'name' },
|
||||||
|
{ title: '가격 낮은순', value: 'price_asc' },
|
||||||
|
{ title: '가격 높은순', value: 'price_desc' },
|
||||||
|
{ title: '추천순', value: 'recommended' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const spicyLevels = ['안매움', '조금매움', '보통', '매움', '아주매움']
|
||||||
|
|
||||||
|
// 유효성 검사 규칙
|
||||||
|
const priceRules = [
|
||||||
|
v => !!v || '가격을 입력해주세요',
|
||||||
|
v => v > 0 || '0원보다 큰 가격을 입력해주세요'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 컴퓨티드 속성
|
||||||
|
const filteredMenus = computed(() => {
|
||||||
|
let filtered = menuStore.menus
|
||||||
|
|
||||||
|
// 검색 필터
|
||||||
|
if (searchQuery.value) {
|
||||||
|
filtered = filtered.filter(menu =>
|
||||||
|
menu.menuName.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 카테고리 필터
|
||||||
|
if (selectedCategory.value) {
|
||||||
|
filtered = filtered.filter(menu => menu.category === selectedCategory.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정렬
|
||||||
|
switch (sortBy.value) {
|
||||||
|
case 'name':
|
||||||
|
filtered.sort((a, b) => a.menuName.localeCompare(b.menuName))
|
||||||
|
break
|
||||||
|
case 'price_asc':
|
||||||
|
filtered.sort((a, b) => a.price - b.price)
|
||||||
|
break
|
||||||
|
case 'price_desc':
|
||||||
|
filtered.sort((a, b) => b.price - a.price)
|
||||||
|
break
|
||||||
|
case 'recommended':
|
||||||
|
filtered.sort((a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
const availableMenuCount = computed(() =>
|
||||||
|
menuStore.menus.filter(menu => menu.available).length
|
||||||
|
)
|
||||||
|
|
||||||
|
const categoriesCount = computed(() =>
|
||||||
|
new Set(menuStore.menus.map(menu => menu.category)).size
|
||||||
|
)
|
||||||
|
|
||||||
|
const averagePrice = computed(() => {
|
||||||
|
if (menuStore.menus.length === 0) return 0
|
||||||
|
const total = menuStore.menus.reduce((sum, menu) => sum + menu.price, 0)
|
||||||
|
return Math.round(total / menuStore.menus.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 메서드
|
||||||
|
const getCategoryColor = (category) => {
|
||||||
|
const colors = {
|
||||||
|
'면류': 'orange',
|
||||||
|
'튀김류': 'red',
|
||||||
|
'음료': 'blue',
|
||||||
|
'안주': 'purple',
|
||||||
|
'디저트': 'pink'
|
||||||
|
}
|
||||||
|
return colors[category] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
editMode.value = false
|
||||||
|
resetMenuForm()
|
||||||
|
showMenuDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const editMenu = (menu) => {
|
||||||
|
editMode.value = true
|
||||||
|
Object.assign(menuFormData, menu)
|
||||||
|
showMenuDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenuDialog = () => {
|
||||||
|
showMenuDialog.value = false
|
||||||
|
resetMenuForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetMenuForm = () => {
|
||||||
|
Object.assign(menuFormData, {
|
||||||
|
menuName: '',
|
||||||
|
price: 0,
|
||||||
|
category: '',
|
||||||
|
description: '',
|
||||||
|
ingredients: '',
|
||||||
|
spicyLevel: '보통',
|
||||||
|
calories: 0,
|
||||||
|
available: true,
|
||||||
|
recommended: false,
|
||||||
|
imageUrl: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectMenuImage = () => {
|
||||||
|
menuImageInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMenuImageUpload = (event) => {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
menuFormData.imageUrl = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveMenu = async () => {
|
||||||
|
if (!menuFormValid.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
if (editMode.value) {
|
||||||
|
await menuStore.updateMenu(menuFormData.id, menuFormData)
|
||||||
|
snackbar.message = '메뉴가 수정되었습니다'
|
||||||
|
} else {
|
||||||
|
await menuStore.createMenu(menuFormData)
|
||||||
|
snackbar.message = '메뉴가 등록되었습니다'
|
||||||
|
}
|
||||||
|
|
||||||
|
snackbar.color = 'success'
|
||||||
|
snackbar.show = true
|
||||||
|
closeMenuDialog()
|
||||||
|
} catch (error) {
|
||||||
|
snackbar.message = error.response?.data?.message || '저장 중 오류가 발생했습니다'
|
||||||
|
snackbar.color = 'error'
|
||||||
|
snackbar.show = true
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = (menu) => {
|
||||||
|
deleteTarget.value = menu
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMenu = async () => {
|
||||||
|
if (!deleteTarget.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
deleting.value = true
|
||||||
|
await menuStore.deleteMenu(deleteTarget.value.id)
|
||||||
|
|
||||||
|
snackbar.message = '메뉴가 삭제되었습니다'
|
||||||
|
snackbar.color = 'success'
|
||||||
|
snackbar.show = true
|
||||||
|
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
deleteTarget.value = null
|
||||||
|
} catch (error) {
|
||||||
|
snackbar.message = error.response?.data?.message || '삭제 중 오류가 발생했습니다'
|
||||||
|
snackbar.color = 'error'
|
||||||
|
snackbar.show = true
|
||||||
|
} finally {
|
||||||
|
deleting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이프사이클
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await menuStore.fetchMenus()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('메뉴 목록 로드 실패:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.h-100 {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.text-h4 {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
696
src/views/StoreManagementView.vue
Normal file
696
src/views/StoreManagementView.vue
Normal file
@ -0,0 +1,696 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid class="pa-4">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex align-center justify-space-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-h4 font-weight-bold mb-2">매장 관리</h1>
|
||||||
|
<p class="text-grey">매장 정보를 관리하고 설정을 변경할 수 있습니다</p>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
v-if="!storeStore.hasStoreInfo"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showCreateDialog = true"
|
||||||
|
>
|
||||||
|
매장 등록
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 매장 정보가 없는 경우 -->
|
||||||
|
<div v-if="!storeStore.hasStoreInfo && !storeStore.loading">
|
||||||
|
<v-row justify="center">
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card class="text-center pa-8" elevation="4">
|
||||||
|
<v-img
|
||||||
|
src="/images/store-placeholder.png"
|
||||||
|
max-width="200"
|
||||||
|
class="mx-auto mb-4"
|
||||||
|
/>
|
||||||
|
<h2 class="text-h5 font-weight-bold mb-3">첫 매장을 등록해볼까요?</h2>
|
||||||
|
<p class="text-grey mb-4">
|
||||||
|
매장 정보를 등록하면 AI 마케팅 서비스를 이용할 수 있습니다
|
||||||
|
</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
prepend-icon="mdi-store-plus"
|
||||||
|
@click="showCreateDialog = true"
|
||||||
|
>
|
||||||
|
매장 정보 등록하기
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 매장 정보가 있는 경우 -->
|
||||||
|
<div v-else-if="storeStore.hasStoreInfo">
|
||||||
|
<!-- 탭 메뉴 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-tabs v-model="currentTab" color="primary">
|
||||||
|
<v-tab value="basic">기본 정보</v-tab>
|
||||||
|
<v-tab value="operation">운영 설정</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-tabs-window v-model="currentTab">
|
||||||
|
<!-- 기본 정보 탭 -->
|
||||||
|
<v-tabs-window-item value="basic">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4 d-flex align-center justify-space-between">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-store</v-icon>
|
||||||
|
매장 기본 정보
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-pencil"
|
||||||
|
@click="editBasicInfo"
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<div class="text-center">
|
||||||
|
<v-avatar size="120" class="mb-4">
|
||||||
|
<v-img
|
||||||
|
:src="storeInfo.imageUrl || '/images/store-placeholder.png'"
|
||||||
|
:alt="storeInfo.storeName"
|
||||||
|
/>
|
||||||
|
</v-avatar>
|
||||||
|
<h3 class="text-h6 font-weight-bold">{{ storeInfo.storeName }}</h3>
|
||||||
|
<p class="text-caption text-grey">{{ storeInfo.businessType }}</p>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-subtitle-2 text-grey mb-1">사업자명</h4>
|
||||||
|
<p class="text-body-1">{{ storeInfo.ownerName }}</p>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-subtitle-2 text-grey mb-1">사업자등록번호</h4>
|
||||||
|
<p class="text-body-1">{{ storeInfo.businessNumber }}</p>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-subtitle-2 text-grey mb-1">주소</h4>
|
||||||
|
<p class="text-body-1">{{ storeInfo.address }}</p>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-subtitle-2 text-grey mb-1">연락처</h4>
|
||||||
|
<p class="text-body-1">{{ storeInfo.phoneNumber }}</p>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-subtitle-2 text-grey mb-1">좌석 수</h4>
|
||||||
|
<p class="text-body-1">{{ storeInfo.seatCount }}석</p>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- SNS 계정 정보 -->
|
||||||
|
<v-divider class="my-6" />
|
||||||
|
|
||||||
|
<h4 class="text-h6 font-weight-bold mb-4">SNS 계정 정보</h4>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon color="purple" class="mr-3">mdi-instagram</v-icon>
|
||||||
|
<div>
|
||||||
|
<p class="text-body-2 text-grey mb-1">Instagram</p>
|
||||||
|
<p class="text-body-1">{{ storeInfo.instagramUrl || '등록되지 않음' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon color="green" class="mr-3">mdi-blogger</v-icon>
|
||||||
|
<div>
|
||||||
|
<p class="text-body-2 text-grey mb-1">네이버 블로그</p>
|
||||||
|
<p class="text-body-1">{{ storeInfo.blogUrl || '등록되지 않음' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<!-- 운영 설정 탭 -->
|
||||||
|
<v-tabs-window-item value="operation">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4 d-flex align-center justify-space-between">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-clock</v-icon>
|
||||||
|
운영 설정
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-pencil"
|
||||||
|
@click="editOperationInfo"
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<!-- 영업시간 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="text-h6 font-weight-bold mb-4">영업시간</h4>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="day in daysOfWeek"
|
||||||
|
:key="day.value"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center justify-space-between pa-3 bg-grey-lighten-5 rounded">
|
||||||
|
<span class="font-weight-medium">{{ day.text }}</span>
|
||||||
|
<span class="text-body-2">
|
||||||
|
{{ getOperatingHours(day.value) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 휴무일 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="text-h6 font-weight-bold mb-4">휴무일</h4>
|
||||||
|
<v-chip-group>
|
||||||
|
<v-chip
|
||||||
|
v-for="holiday in storeInfo.holidays"
|
||||||
|
:key="holiday"
|
||||||
|
color="error"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ holiday }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip v-if="!storeInfo.holidays?.length" color="success" variant="tonal">
|
||||||
|
연중무휴
|
||||||
|
</v-chip>
|
||||||
|
</v-chip-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기타 설정 -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-h6 font-weight-bold mb-4">기타 설정</h4>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="text-subtitle-2 text-grey mb-1">배달 서비스</h5>
|
||||||
|
<v-chip
|
||||||
|
:color="storeInfo.deliveryAvailable ? 'success' : 'error'"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ storeInfo.deliveryAvailable ? '이용 가능' : '이용 불가' }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="text-subtitle-2 text-grey mb-1">포장 서비스</h5>
|
||||||
|
<v-chip
|
||||||
|
:color="storeInfo.takeoutAvailable ? 'success' : 'error'"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ storeInfo.takeoutAvailable ? '이용 가능' : '이용 불가' }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
</v-tabs-window>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 매장 등록/수정 다이얼로그 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showCreateDialog"
|
||||||
|
max-width="800"
|
||||||
|
persistent
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
<span class="text-h6">{{ editMode ? '매장 정보 수정' : '매장 정보 등록' }}</span>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="closeDialog"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text class="pa-6" style="max-height: 500px;">
|
||||||
|
<v-form ref="storeForm" v-model="formValid">
|
||||||
|
<v-row>
|
||||||
|
<!-- 매장 이미지 -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<h4 class="text-subtitle-1 font-weight-bold mb-3">매장 이미지</h4>
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<v-avatar size="120" class="mb-3">
|
||||||
|
<v-img
|
||||||
|
:src="formData.imageUrl || '/images/store-placeholder.png'"
|
||||||
|
alt="매장 이미지"
|
||||||
|
/>
|
||||||
|
</v-avatar>
|
||||||
|
<br>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-camera"
|
||||||
|
@click="selectImage"
|
||||||
|
>
|
||||||
|
이미지 선택
|
||||||
|
</v-btn>
|
||||||
|
<input
|
||||||
|
ref="imageInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none;"
|
||||||
|
@change="handleImageUpload"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- 기본 정보 -->
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.storeName"
|
||||||
|
label="매장명 *"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[v => !!v || '매장명을 입력해주세요']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.businessType"
|
||||||
|
label="업종 *"
|
||||||
|
variant="outlined"
|
||||||
|
:items="businessTypes"
|
||||||
|
:rules="[v => !!v || '업종을 선택해주세요']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.ownerName"
|
||||||
|
label="사업자명 *"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[v => !!v || '사업자명을 입력해주세요']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.businessNumber"
|
||||||
|
label="사업자등록번호 *"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="businessNumberRules"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.address"
|
||||||
|
label="주소 *"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[v => !!v || '주소를 입력해주세요']"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.phoneNumber"
|
||||||
|
label="연락처 *"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="phoneRules"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="formData.seatCount"
|
||||||
|
label="좌석 수"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- SNS 정보 -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<h4 class="text-subtitle-1 font-weight-bold mb-3">SNS 계정 정보</h4>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.instagramUrl"
|
||||||
|
label="Instagram URL"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-instagram"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.blogUrl"
|
||||||
|
label="네이버 블로그 URL"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-blogger"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- 운영 설정 -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<h4 class="text-subtitle-1 font-weight-bold mb-3">운영 설정</h4>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.openTime"
|
||||||
|
label="오픈 시간"
|
||||||
|
variant="outlined"
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.closeTime"
|
||||||
|
label="마감 시간"
|
||||||
|
variant="outlined"
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.holidays"
|
||||||
|
label="휴무일"
|
||||||
|
variant="outlined"
|
||||||
|
:items="daysOfWeek"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
closable-chips
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-switch
|
||||||
|
v-model="formData.deliveryAvailable"
|
||||||
|
label="배달 서비스 이용 가능"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-switch
|
||||||
|
v-model="formData.takeoutAvailable"
|
||||||
|
label="포장 서비스 이용 가능"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey"
|
||||||
|
variant="text"
|
||||||
|
@click="closeDialog"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="!formValid"
|
||||||
|
@click="saveStore"
|
||||||
|
>
|
||||||
|
{{ editMode ? '수정하기' : '등록하기' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 로딩 오버레이 -->
|
||||||
|
<v-overlay v-if="storeStore.loading" class="align-center justify-center">
|
||||||
|
<v-progress-circular
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
/>
|
||||||
|
</v-overlay>
|
||||||
|
|
||||||
|
<!-- 스낵바 -->
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.show"
|
||||||
|
:color="snackbar.color"
|
||||||
|
:timeout="3000"
|
||||||
|
location="top"
|
||||||
|
>
|
||||||
|
{{ snackbar.message }}
|
||||||
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useStoreStore, useAppStore } from '@/store/index'
|
||||||
|
|
||||||
|
const storeStore = useStoreStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const currentTab = ref('basic')
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const editMode = ref(false)
|
||||||
|
const formValid = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const imageInput = ref(null)
|
||||||
|
|
||||||
|
const snackbar = reactive({
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
color: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 폼 데이터
|
||||||
|
const formData = reactive({
|
||||||
|
storeName: '',
|
||||||
|
businessType: '',
|
||||||
|
ownerName: '',
|
||||||
|
businessNumber: '',
|
||||||
|
address: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
seatCount: 0,
|
||||||
|
instagramUrl: '',
|
||||||
|
blogUrl: '',
|
||||||
|
openTime: '09:00',
|
||||||
|
closeTime: '21:00',
|
||||||
|
holidays: [],
|
||||||
|
deliveryAvailable: false,
|
||||||
|
takeoutAvailable: true,
|
||||||
|
imageUrl: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 컴퓨티드 속성
|
||||||
|
const storeInfo = computed(() => storeStore.storeInfo || {})
|
||||||
|
|
||||||
|
// 옵션 데이터
|
||||||
|
const businessTypes = [
|
||||||
|
'한식',
|
||||||
|
'중식',
|
||||||
|
'일식',
|
||||||
|
'양식',
|
||||||
|
'분식',
|
||||||
|
'치킨',
|
||||||
|
'피자',
|
||||||
|
'카페',
|
||||||
|
'베이커리',
|
||||||
|
'기타'
|
||||||
|
]
|
||||||
|
|
||||||
|
const daysOfWeek = [
|
||||||
|
{ text: '월요일', value: 'monday' },
|
||||||
|
{ text: '화요일', value: 'tuesday' },
|
||||||
|
{ text: '수요일', value: 'wednesday' },
|
||||||
|
{ text: '목요일', value: 'thursday' },
|
||||||
|
{ text: '금요일', value: 'friday' },
|
||||||
|
{ text: '토요일', value: 'saturday' },
|
||||||
|
{ text: '일요일', value: 'sunday' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 유효성 검사 규칙
|
||||||
|
const businessNumberRules = [
|
||||||
|
v => !!v || '사업자등록번호를 입력해주세요',
|
||||||
|
v => /^\d{3}-\d{2}-\d{5}$/.test(v) || '올바른 사업자등록번호 형식이 아닙니다 (예: 123-45-67890)'
|
||||||
|
]
|
||||||
|
|
||||||
|
const phoneRules = [
|
||||||
|
v => !!v || '연락처를 입력해주세요',
|
||||||
|
v => /^[\d-+().\s]+$/.test(v) || '올바른 전화번호 형식이 아닙니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 메서드
|
||||||
|
const editBasicInfo = () => {
|
||||||
|
editMode.value = true
|
||||||
|
Object.assign(formData, storeInfo.value)
|
||||||
|
showCreateDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const editOperationInfo = () => {
|
||||||
|
editMode.value = true
|
||||||
|
Object.assign(formData, storeInfo.value)
|
||||||
|
currentTab.value = 'operation'
|
||||||
|
showCreateDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showCreateDialog.value = false
|
||||||
|
editMode.value = false
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.assign(formData, {
|
||||||
|
storeName: '',
|
||||||
|
businessType: '',
|
||||||
|
ownerName: '',
|
||||||
|
businessNumber: '',
|
||||||
|
address: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
seatCount: 0,
|
||||||
|
instagramUrl: '',
|
||||||
|
blogUrl: '',
|
||||||
|
openTime: '09:00',
|
||||||
|
closeTime: '21:00',
|
||||||
|
holidays: [],
|
||||||
|
deliveryAvailable: false,
|
||||||
|
takeoutAvailable: true,
|
||||||
|
imageUrl: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectImage = () => {
|
||||||
|
imageInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageUpload = (event) => {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
formData.imageUrl = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveStore = async () => {
|
||||||
|
if (!formValid.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
if (editMode.value) {
|
||||||
|
await storeStore.updateStoreInfo(formData)
|
||||||
|
snackbar.message = '매장 정보가 수정되었습니다'
|
||||||
|
} else {
|
||||||
|
await storeStore.createStoreInfo(formData)
|
||||||
|
snackbar.message = '매장 정보가 등록되었습니다'
|
||||||
|
}
|
||||||
|
|
||||||
|
snackbar.color = 'success'
|
||||||
|
snackbar.show = true
|
||||||
|
closeDialog()
|
||||||
|
} catch (error) {
|
||||||
|
snackbar.message = error.response?.data?.message || '저장 중 오류가 발생했습니다'
|
||||||
|
snackbar.color = 'error'
|
||||||
|
snackbar.show = true
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOperatingHours = (day) => {
|
||||||
|
if (storeInfo.value.holidays?.includes(day)) {
|
||||||
|
return '휴무'
|
||||||
|
}
|
||||||
|
return `${storeInfo.value.openTime || '09:00'} - ${storeInfo.value.closeTime || '21:00'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이프사이클
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await storeStore.fetchStoreInfo()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('매장 정보 로드 실패:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bg-grey-lighten-5 {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.text-h4 {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user