files add
This commit is contained in:
parent
93e27a239a
commit
2e28c5a9df
31
package.json
31
package.json
@ -1,29 +1,26 @@
|
||||
{
|
||||
"name": "smarketing-frontend",
|
||||
"version": "0.0.0",
|
||||
"name": "ai-marketing-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"format": "prettier --write src/"
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"vuetify": "^3.5.0",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"axios": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-vuetify": "^2.0.0",
|
||||
"sass": "^1.69.0"
|
||||
}
|
||||
}
|
||||
323
src/App.vue
323
src/App.vue
@ -1,47 +1,308 @@
|
||||
<script setup lang="ts">
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import TheWelcome from './components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
//* src/App.vue
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
|
||||
<v-app>
|
||||
<!-- 로딩 오버레이 -->
|
||||
<v-overlay
|
||||
v-if="loading"
|
||||
class="align-center justify-center"
|
||||
persistent
|
||||
>
|
||||
<v-progress-circular
|
||||
color="primary"
|
||||
indeterminate
|
||||
size="64"
|
||||
/>
|
||||
</v-overlay>
|
||||
|
||||
<div class="wrapper">
|
||||
<HelloWorld msg="You did it!" />
|
||||
<!-- 메인 네비게이션 -->
|
||||
<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>
|
||||
</header>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
<!-- 상단 앱바 -->
|
||||
<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>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
<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
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
const getNotificationIcon = (type) => {
|
||||
const icons = {
|
||||
'success': 'mdi-check-circle',
|
||||
'error': 'mdi-alert-circle',
|
||||
'warning': 'mdi-alert',
|
||||
'info': 'mdi-information'
|
||||
}
|
||||
return icons[type] || 'mdi-bell'
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
// 라이프사이클
|
||||
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;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
.v-navigation-drawer {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 커스텀 스타일 */
|
||||
.fade-transition {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-transition {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
65
src/main.js
Normal file
65
src/main.js
Normal file
@ -0,0 +1,65 @@
|
||||
//* src/main.js
|
||||
/**
|
||||
* AI 마케팅 서비스 - 메인 앱 진입점
|
||||
* Vue 3 + Vuetify 3 기반 애플리케이션 초기화
|
||||
*/
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// Vuetify
|
||||
import 'vuetify/styles'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import { mdi } from 'vuetify/iconsets/mdi'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
|
||||
// Vuetify 테마 설정
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: 'light',
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
primary: '#1976D2',
|
||||
secondary: '#424242',
|
||||
accent: '#82B1FF',
|
||||
error: '#FF5252',
|
||||
info: '#2196F3',
|
||||
success: '#4CAF50',
|
||||
warning: '#FFC107',
|
||||
background: '#F5F5F5',
|
||||
surface: '#FFFFFF'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
icons: {
|
||||
defaultSet: 'mdi',
|
||||
sets: {
|
||||
mdi
|
||||
}
|
||||
},
|
||||
display: {
|
||||
mobileBreakpoint: 'sm',
|
||||
thresholds: {
|
||||
xs: 0,
|
||||
sm: 600,
|
||||
md: 960,
|
||||
lg: 1280,
|
||||
xl: 1920
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(vuetify)
|
||||
|
||||
app.mount('#app')
|
||||
139
src/router/index.js
Normal file
139
src/router/index.js
Normal file
@ -0,0 +1,139 @@
|
||||
//* src/router/index.js
|
||||
/**
|
||||
* Vue Router 설정
|
||||
* 라우팅 및 네비게이션 가드 설정
|
||||
*/
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
|
||||
// 뷰 컴포넌트 lazy loading
|
||||
const LoginView = () => import('@/views/LoginView.vue')
|
||||
const DashboardView = () => import('@/views/DashboardView.vue')
|
||||
const StoreManagementView = () => import('@/views/StoreManagementView.vue')
|
||||
const MenuManagementView = () => import('@/views/MenuManagementView.vue')
|
||||
const ContentCreationView = () => import('@/views/ContentCreationView.vue')
|
||||
const ContentManagementView = () => import('@/views/ContentManagementView.vue')
|
||||
const AIRecommendationView = () => import('@/views/AIRecommendationView.vue')
|
||||
const SalesAnalysisView = () => import('@/views/SalesAnalysisView.vue')
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: LoginView,
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
title: '로그인'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: DashboardView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '대시보드'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/store',
|
||||
name: 'StoreManagement',
|
||||
component: StoreManagementView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '매장 관리'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/menu',
|
||||
name: 'MenuManagement',
|
||||
component: MenuManagementView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '메뉴 관리'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/content/create',
|
||||
name: 'ContentCreation',
|
||||
component: ContentCreationView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '콘텐츠 생성'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/content',
|
||||
name: 'ContentManagement',
|
||||
component: ContentManagementView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '콘텐츠 관리'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ai-recommend',
|
||||
name: 'AIRecommendation',
|
||||
component: AIRecommendationView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: 'AI 추천'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/sales',
|
||||
name: 'SalesAnalysis',
|
||||
component: SalesAnalysisView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '매출 분석'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/dashboard'
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
// 네비게이션 가드
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 인증이 필요한 페이지인지 확인
|
||||
if (to.meta.requiresAuth) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
// 토큰이 있다면 사용자 정보 재검증
|
||||
if (authStore.token) {
|
||||
try {
|
||||
await authStore.refreshUserInfo()
|
||||
next()
|
||||
} catch (error) {
|
||||
authStore.clearAuth()
|
||||
next('/login')
|
||||
}
|
||||
} else {
|
||||
next('/login')
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
// 로그인 페이지에 이미 인증된 사용자가 접근하는 경우
|
||||
if (to.name === 'Login' && authStore.isAuthenticated) {
|
||||
next('/dashboard')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
268
src/store/index.js
Normal file
268
src/store/index.js
Normal file
@ -0,0 +1,268 @@
|
||||
//* src/store/index.js
|
||||
/**
|
||||
* Pinia 스토어 설정
|
||||
* 전역 상태 관리
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import authService from '@/services/auth'
|
||||
import storeService from '@/services/store'
|
||||
|
||||
// 인증 스토어
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: null,
|
||||
token: localStorage.getItem('token'),
|
||||
refreshToken: localStorage.getItem('refreshToken'),
|
||||
isAuthenticated: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getUserInfo: (state) => state.user,
|
||||
isLoggedIn: (state) => state.isAuthenticated && !!state.token
|
||||
},
|
||||
|
||||
actions: {
|
||||
async login(credentials) {
|
||||
try {
|
||||
const response = await authService.login(credentials)
|
||||
this.setAuth(response.data)
|
||||
return response
|
||||
} catch (error) {
|
||||
this.clearAuth()
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
async register(userData) {
|
||||
try {
|
||||
const response = await authService.register(userData)
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
if (this.token) {
|
||||
await authService.logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('로그아웃 오류:', error)
|
||||
} finally {
|
||||
this.clearAuth()
|
||||
}
|
||||
},
|
||||
|
||||
async refreshUserInfo() {
|
||||
try {
|
||||
const response = await authService.getUserInfo()
|
||||
this.user = response.data
|
||||
this.isAuthenticated = true
|
||||
return response
|
||||
} catch (error) {
|
||||
this.clearAuth()
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
setAuth(authData) {
|
||||
this.user = authData.user
|
||||
this.token = authData.accessToken
|
||||
this.refreshToken = authData.refreshToken
|
||||
this.isAuthenticated = true
|
||||
|
||||
localStorage.setItem('token', authData.accessToken)
|
||||
localStorage.setItem('refreshToken', authData.refreshToken)
|
||||
},
|
||||
|
||||
clearAuth() {
|
||||
this.user = null
|
||||
this.token = null
|
||||
this.refreshToken = null
|
||||
this.isAuthenticated = false
|
||||
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('refreshToken')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 앱 전역 스토어
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
snackbar: {
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success',
|
||||
timeout: 3000
|
||||
},
|
||||
notifications: [],
|
||||
notificationCount: 0
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setLoading(status) {
|
||||
this.loading = status
|
||||
},
|
||||
|
||||
showSnackbar(message, color = 'success', timeout = 3000) {
|
||||
this.snackbar = {
|
||||
show: true,
|
||||
message,
|
||||
color,
|
||||
timeout
|
||||
}
|
||||
},
|
||||
|
||||
hideSnackbar() {
|
||||
this.snackbar.show = false
|
||||
},
|
||||
|
||||
addNotification(notification) {
|
||||
this.notifications.unshift({
|
||||
id: Date.now(),
|
||||
timestamp: new Date(),
|
||||
...notification
|
||||
})
|
||||
this.notificationCount = this.notifications.length
|
||||
},
|
||||
|
||||
clearNotifications() {
|
||||
this.notifications = []
|
||||
this.notificationCount = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 매장 스토어
|
||||
export const useStoreStore = defineStore('store', {
|
||||
state: () => ({
|
||||
storeInfo: null,
|
||||
loading: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
hasStoreInfo: (state) => !!state.storeInfo
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchStoreInfo() {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await storeService.getStoreInfo()
|
||||
this.storeInfo = response.data
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async updateStoreInfo(storeData) {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await storeService.updateStoreInfo(storeData)
|
||||
this.storeInfo = response.data
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async createStoreInfo(storeData) {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await storeService.createStoreInfo(storeData)
|
||||
this.storeInfo = response.data
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 메뉴 스토어
|
||||
export const useMenuStore = defineStore('menu', {
|
||||
state: () => ({
|
||||
menus: [],
|
||||
loading: false,
|
||||
totalCount: 0
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getMenuById: (state) => (id) => {
|
||||
return state.menus.find(menu => menu.id === id)
|
||||
},
|
||||
|
||||
getMenusByCategory: (state) => (category) => {
|
||||
return state.menus.filter(menu => menu.category === category)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchMenus() {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await storeService.getMenus()
|
||||
this.menus = response.data
|
||||
this.totalCount = response.data.length
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async createMenu(menuData) {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await storeService.createMenu(menuData)
|
||||
this.menus.push(response.data)
|
||||
this.totalCount++
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async updateMenu(menuId, menuData) {
|
||||
try {
|
||||
this.loading = true
|
||||
const response = await storeService.updateMenu(menuId, menuData)
|
||||
const index = this.menus.findIndex(menu => menu.id === menuId)
|
||||
if (index !== -1) {
|
||||
this.menus[index] = response.data
|
||||
}
|
||||
return response
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async deleteMenu(menuId) {
|
||||
try {
|
||||
this.loading = true
|
||||
await storeService.deleteMenu(menuId)
|
||||
this.menus = this.menus.filter(menu => menu.id !== menuId)
|
||||
this.totalCount--
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
26
vite.config.js
Normal file
26
vite.config.js
Normal file
@ -0,0 +1,26 @@
|
||||
//* vite.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vuetify from 'vite-plugin-vuetify'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vuetify({
|
||||
autoImport: true,
|
||||
theme: {
|
||||
defaultTheme: 'light'
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user