files add
This commit is contained in:
parent
93e27a239a
commit
2e28c5a9df
31
package.json
31
package.json
@ -1,29 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "smarketing-frontend",
|
"name": "ai-marketing-frontend",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
"type-check": "vue-tsc --build",
|
|
||||||
"format": "prettier --write src/"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "^22.0.1",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
"@types/node": "^22.14.0",
|
"vite": "^5.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"vite-plugin-vuetify": "^2.0.0",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"sass": "^1.69.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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
323
src/App.vue
323
src/App.vue
@ -1,47 +1,308 @@
|
|||||||
<script setup lang="ts">
|
//* src/App.vue
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
|
||||||
import TheWelcome from './components/TheWelcome.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<v-app>
|
||||||
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
|
<!-- 로딩 오버레이 -->
|
||||||
|
<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>
|
</div>
|
||||||
</header>
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<main>
|
<!-- 상단 앱바 -->
|
||||||
<TheWelcome />
|
<v-app-bar
|
||||||
</main>
|
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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<script setup>
|
||||||
header {
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
line-height: 1.5;
|
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 {
|
const getNotificationIcon = (type) => {
|
||||||
display: block;
|
const icons = {
|
||||||
margin: 0 auto 2rem;
|
'success': 'mdi-check-circle',
|
||||||
|
'error': 'mdi-alert-circle',
|
||||||
|
'warning': 'mdi-alert',
|
||||||
|
'info': 'mdi-information'
|
||||||
|
}
|
||||||
|
return icons[type] || 'mdi-bell'
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
// 라이프사이클
|
||||||
header {
|
onMounted(async () => {
|
||||||
display: flex;
|
// 앱 초기화
|
||||||
place-items: center;
|
if (authStore.token) {
|
||||||
padding-right: calc(var(--section-gap) / 2);
|
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 {
|
.v-navigation-drawer {
|
||||||
margin: 0 2rem 0 0;
|
max-width: 280px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
header .wrapper {
|
/* 커스텀 스타일 */
|
||||||
display: flex;
|
.fade-transition {
|
||||||
place-items: flex-start;
|
transition: opacity 0.3s ease;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
}
|
|
||||||
|
.slide-transition {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</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