From 2e28c5a9dfbbcff442132ecdbddcd1228185f3de Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 11 Jun 2025 14:29:34 +0900 Subject: [PATCH] files add --- package.json | 33 ++--- src/App.vue | 331 +++++++++++++++++++++++++++++++++++++++----- src/main.js | 65 +++++++++ src/router/index.js | 139 +++++++++++++++++++ src/store/index.js | 268 +++++++++++++++++++++++++++++++++++ vite.config.js | 26 ++++ 6 files changed, 809 insertions(+), 53 deletions(-) create mode 100644 src/main.js create mode 100644 src/router/index.js create mode 100644 src/store/index.js create mode 100644 vite.config.js diff --git a/package.json b/package.json index 19ff793..4df90f0 100644 --- a/package.json +++ b/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" } -} +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index d05208d..9d1139b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,47 +1,308 @@ - - +//* src/App.vue - + +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 +}) + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..0bbbe1b --- /dev/null +++ b/src/main.js @@ -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') \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..9a26767 --- /dev/null +++ b/src/router/index.js @@ -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 \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..2a84d77 --- /dev/null +++ b/src/store/index.js @@ -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 + } + } + } +}) \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c48ade5 --- /dev/null +++ b/vite.config.js @@ -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 + } +}) \ No newline at end of file