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
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+ 로그아웃
+
+
+
+
+
+
+
+
+
+
+ {{ currentPageTitle }}
+
+
+
+
+
+
+
+ mdi-bell
+
+ mdi-bell
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.icon }}
+ {{ item.title }}
+
+
+
+
+
+
+
+ 알림
+
+ mdi-close
+
+
+
+
+
+
+
+
+
+
+ {{ getNotificationIcon(notification.type) }}
+
+
+
+
+
+
+
mdi-bell-off
+
새로운 알림이 없습니다
+
+
+
+
+
+
+
+ {{ snackbar.message }}
+
+
+ 닫기
+
+
+
+
-
+
+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