2025-06-11 14:29:34 +09:00

308 lines
7.6 KiB
Vue

//* src/App.vue
<template>
<v-app>
<!-- 로딩 오버레이 -->
<v-overlay
v-if="loading"
class="align-center justify-center"
persistent
>
<v-progress-circular
color="primary"
indeterminate
size="64"
/>
</v-overlay>
<!-- 메인 네비게이션 -->
<v-navigation-drawer
v-if="isAuthenticated && !isLoginPage"
v-model="drawer"
app
temporary
width="280"
>
<v-list>
<v-list-item
prepend-avatar="/images/logo.png"
:title="userStore.user?.nickname || '사용자'"
:subtitle="userStore.user?.businessName || '매장명'"
/>
</v-list>
<v-divider />
<v-list nav density="compact">
<v-list-item
v-for="item in menuItems"
:key="item.route"
:to="item.route"
:prepend-icon="item.icon"
:title="item.title"
exact
/>
</v-list>
<template v-slot:append>
<div class="pa-4">
<v-btn
block
color="primary"
variant="outlined"
prepend-icon="mdi-logout"
@click="logout"
>
로그아웃
</v-btn>
</div>
</template>
</v-navigation-drawer>
<!-- 상단 앱바 -->
<v-app-bar
v-if="isAuthenticated && !isLoginPage"
app
elevation="1"
color="primary"
>
<v-app-bar-nav-icon
@click="drawer = !drawer"
color="white"
/>
<v-toolbar-title class="text-white font-weight-bold">
{{ currentPageTitle }}
</v-toolbar-title>
<v-spacer />
<!-- 알림 버튼 -->
<v-btn
icon
color="white"
@click="showNotifications = true"
>
<v-badge
v-if="notificationCount > 0"
:content="notificationCount"
color="error"
>
<v-icon>mdi-bell</v-icon>
</v-badge>
<v-icon v-else>mdi-bell</v-icon>
</v-btn>
</v-app-bar>
<!-- 메인 콘텐츠 -->
<v-main>
<router-view />
</v-main>
<!-- 하단 네비게이션 (모바일) -->
<v-bottom-navigation
v-if="isAuthenticated && !isLoginPage && $vuetify.display.mobile"
v-model="bottomNav"
app
grow
height="70"
>
<v-btn
v-for="item in bottomMenuItems"
:key="item.route"
:to="item.route"
:value="item.route"
stacked
>
<v-icon>{{ item.icon }}</v-icon>
<span class="text-caption">{{ item.title }}</span>
</v-btn>
</v-bottom-navigation>
<!-- 알림 다이얼로그 -->
<v-dialog
v-model="showNotifications"
max-width="400"
scrollable
>
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
알림
<v-btn
icon
size="small"
@click="showNotifications = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider />
<v-card-text style="max-height: 400px;">
<v-list v-if="notifications.length > 0">
<v-list-item
v-for="notification in notifications"
:key="notification.id"
:subtitle="notification.message"
:title="notification.title"
>
<template v-slot:prepend>
<v-icon :color="notification.type">
{{ getNotificationIcon(notification.type) }}
</v-icon>
</template>
</v-list-item>
</v-list>
<div v-else class="text-center pa-4">
<v-icon size="48" color="grey-lighten-2">mdi-bell-off</v-icon>
<p class="text-grey mt-2">새로운 알림이 없습니다</p>
</div>
</v-card-text>
</v-card>
</v-dialog>
<!-- 글로벌 스낵바 -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
location="top"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
닫기
</v-btn>
</template>
</v-snackbar>
</v-app>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/store/auth'
import { useAppStore } from '@/store/app'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const appStore = useAppStore()
// 반응형 데이터
const drawer = ref(false)
const bottomNav = ref('')
const showNotifications = ref(false)
const loading = ref(false)
// 컴퓨티드 속성
const isAuthenticated = computed(() => authStore.isAuthenticated)
const isLoginPage = computed(() => route.name === 'Login')
const userStore = computed(() => authStore)
const notificationCount = computed(() => appStore.notificationCount)
const notifications = computed(() => appStore.notifications)
const snackbar = computed(() => appStore.snackbar)
// 현재 페이지 제목
const currentPageTitle = computed(() => {
const titles = {
'Dashboard': '대시보드',
'StoreManagement': '매장 관리',
'MenuManagement': '메뉴 관리',
'ContentCreation': '콘텐츠 생성',
'ContentManagement': '콘텐츠 관리',
'AIRecommendation': 'AI 추천',
'SalesAnalysis': '매출 분석'
}
return titles[route.name] || 'AI 마케팅'
})
// 메뉴 아이템
const menuItems = [
{ title: '대시보드', icon: 'mdi-view-dashboard', route: '/dashboard' },
{ title: '매장 관리', icon: 'mdi-store', route: '/store' },
{ title: '메뉴 관리', icon: 'mdi-food', route: '/menu' },
{ title: '콘텐츠 생성', icon: 'mdi-plus-circle', route: '/content/create' },
{ title: '콘텐츠 관리', icon: 'mdi-folder-multiple', route: '/content' },
{ title: 'AI 추천', icon: 'mdi-robot', route: '/ai-recommend' },
{ title: '매출 분석', icon: 'mdi-chart-line', route: '/sales' }
]
const bottomMenuItems = [
{ title: '홈', icon: 'mdi-home', route: '/dashboard' },
{ title: '매장', icon: 'mdi-store', route: '/store' },
{ title: '생성', icon: 'mdi-plus-circle', route: '/content/create' },
{ title: '분석', icon: 'mdi-chart-line', route: '/sales' }
]
// 메서드
const logout = async () => {
try {
loading.value = true
await authStore.logout()
router.push('/login')
} catch (error) {
appStore.showSnackbar('로그아웃 중 오류가 발생했습니다', 'error')
} finally {
loading.value = false
}
}
const getNotificationIcon = (type) => {
const icons = {
'success': 'mdi-check-circle',
'error': 'mdi-alert-circle',
'warning': 'mdi-alert',
'info': 'mdi-information'
}
return icons[type] || 'mdi-bell'
}
// 라이프사이클
onMounted(async () => {
// 앱 초기화
if (authStore.token) {
try {
await authStore.refreshUserInfo()
} catch (error) {
console.error('사용자 정보 갱신 실패:', error)
}
}
})
// 라우트 변경 감지
watch(route, (to) => {
bottomNav.value = to.path
})
</script>
<style>
/* 글로벌 스타일 */
.v-application {
font-family: 'Noto Sans KR', sans-serif !important;
}
/* 모바일 최적화 */
@media (max-width: 600px) {
.v-toolbar-title {
font-size: 1.1rem !important;
}
.v-navigation-drawer {
max-width: 280px;
}
}
/* 커스텀 스타일 */
.fade-transition {
transition: opacity 0.3s ease;
}
.slide-transition {
transition: transform 0.3s ease;
}
</style>