release
This commit is contained in:
parent
ea04212298
commit
df016023be
@ -1,13 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite App</title>
|
<title>Vite App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
2935
package-lock.json
generated
2935
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,7 +19,11 @@ const SalesAnalysisView = () => import('@/views/SalesAnalysisView.vue')
|
|||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/dashboard'
|
redirect: (to) => {
|
||||||
|
// 인증 상태에 따른 동적 리다이렉트
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return authStore.isAuthenticated ? '/dashboard' : '/login'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@ -27,8 +31,8 @@ const routes = [
|
|||||||
component: LoginView,
|
component: LoginView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: false,
|
requiresAuth: false,
|
||||||
title: '로그인'
|
title: '로그인',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
@ -36,8 +40,8 @@ const routes = [
|
|||||||
component: DashboardView,
|
component: DashboardView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: '대시보드'
|
title: '대시보드',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/store',
|
path: '/store',
|
||||||
@ -45,8 +49,8 @@ const routes = [
|
|||||||
component: StoreManagementView,
|
component: StoreManagementView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: '매장 관리'
|
title: '매장 관리',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/menu',
|
path: '/menu',
|
||||||
@ -54,8 +58,8 @@ const routes = [
|
|||||||
component: MenuManagementView,
|
component: MenuManagementView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: '메뉴 관리'
|
title: '메뉴 관리',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/content/create',
|
path: '/content/create',
|
||||||
@ -63,8 +67,8 @@ const routes = [
|
|||||||
component: ContentCreationView,
|
component: ContentCreationView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: '콘텐츠 생성'
|
title: '콘텐츠 생성',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/content',
|
path: '/content',
|
||||||
@ -72,8 +76,8 @@ const routes = [
|
|||||||
component: ContentManagementView,
|
component: ContentManagementView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: '콘텐츠 관리'
|
title: '콘텐츠 관리',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/ai-recommend',
|
path: '/ai-recommend',
|
||||||
@ -81,8 +85,8 @@ const routes = [
|
|||||||
component: AIRecommendationView,
|
component: AIRecommendationView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: 'AI 추천'
|
title: 'AI 추천',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/sales',
|
path: '/sales',
|
||||||
@ -90,50 +94,55 @@ const routes = [
|
|||||||
component: SalesAnalysisView,
|
component: SalesAnalysisView,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
title: '매출 분석'
|
title: '매출 분석',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
redirect: '/dashboard'
|
redirect: '/login', // 404시 로그인으로 이동
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 네비게이션 가드
|
// 네비게이션 가드
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
console.log('=== 라우터 가드 실행 ===')
|
||||||
|
console.log('이동 경로:', `${from.path} → ${to.path}`)
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
console.log('현재 인증 상태:', authStore.isAuthenticated)
|
||||||
|
console.log('토큰 존재:', !!authStore.token)
|
||||||
|
console.log('사용자 정보:', authStore.user)
|
||||||
|
|
||||||
// 인증이 필요한 페이지인지 확인
|
// 인증이 필요한 페이지인지 확인
|
||||||
if (to.meta.requiresAuth) {
|
if (to.meta.requiresAuth) {
|
||||||
|
console.log('인증이 필요한 페이지')
|
||||||
|
|
||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
// 토큰이 있다면 사용자 정보 재검증
|
console.log('인증되지 않음, 로그인으로 이동')
|
||||||
if (authStore.token) {
|
next('/login')
|
||||||
try {
|
|
||||||
await authStore.refreshUserInfo()
|
|
||||||
next()
|
|
||||||
} catch (error) {
|
|
||||||
authStore.clearAuth()
|
|
||||||
next('/login')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next('/login')
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
console.log('인증됨, 페이지 이동 허용')
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.log('인증이 필요하지 않은 페이지')
|
||||||
|
|
||||||
// 로그인 페이지에 이미 인증된 사용자가 접근하는 경우
|
// 로그인 페이지에 이미 인증된 사용자가 접근하는 경우
|
||||||
if (to.name === 'Login' && authStore.isAuthenticated) {
|
if (to.name === 'Login' && authStore.isAuthenticated) {
|
||||||
|
console.log('이미 로그인됨, 대시보드로 리다이렉트')
|
||||||
next('/dashboard')
|
next('/dashboard')
|
||||||
} else {
|
} else {
|
||||||
|
console.log('페이지 이동 허용')
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('=== 라우터 가드 종료 ===')
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
154
src/store/app.js
Normal file
154
src/store/app.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
//* src/store/app.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 앱 전역 상태 관리 스토어
|
||||||
|
* UI 상태, 로딩, 에러, 알림 등 관리
|
||||||
|
*/
|
||||||
|
export const useAppStore = defineStore('app', () => {
|
||||||
|
// State
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const loadingMessage = ref('')
|
||||||
|
const error = ref(null)
|
||||||
|
const drawer = ref(false)
|
||||||
|
const rail = ref(false)
|
||||||
|
|
||||||
|
// 스낵바 상태
|
||||||
|
const snackbar = ref({
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
color: 'info',
|
||||||
|
timeout: 4000,
|
||||||
|
location: 'bottom',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 확인 다이얼로그 상태
|
||||||
|
const confirmDialog = ref({
|
||||||
|
show: false,
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
icon: '',
|
||||||
|
confirmText: '확인',
|
||||||
|
cancelText: '취소',
|
||||||
|
loading: false,
|
||||||
|
callback: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 앱 설정
|
||||||
|
const settings = ref({
|
||||||
|
darkMode: false,
|
||||||
|
language: 'ko',
|
||||||
|
notifications: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const isDrawerOpen = computed(() => drawer.value)
|
||||||
|
const isRailMode = computed(() => rail.value)
|
||||||
|
const currentError = computed(() => error.value)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const setLoading = (loading, message = '') => {
|
||||||
|
isLoading.value = loading
|
||||||
|
loadingMessage.value = message
|
||||||
|
}
|
||||||
|
|
||||||
|
const setError = (errorMessage) => {
|
||||||
|
error.value = errorMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearError = () => {
|
||||||
|
error.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDrawer = () => {
|
||||||
|
drawer.value = !drawer.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDrawer = (value) => {
|
||||||
|
drawer.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRail = () => {
|
||||||
|
rail.value = !rail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setRail = (value) => {
|
||||||
|
rail.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSnackbar = (message, color = 'info', timeout = 4000) => {
|
||||||
|
snackbar.value = {
|
||||||
|
show: true,
|
||||||
|
message,
|
||||||
|
color,
|
||||||
|
timeout,
|
||||||
|
location: 'bottom',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideSnackbar = () => {
|
||||||
|
snackbar.value.show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const showConfirmDialog = (options) => {
|
||||||
|
confirmDialog.value = {
|
||||||
|
show: true,
|
||||||
|
title: options.title || '확인',
|
||||||
|
message: options.message || '',
|
||||||
|
icon: options.icon || '',
|
||||||
|
confirmText: options.confirmText || '확인',
|
||||||
|
cancelText: options.cancelText || '취소',
|
||||||
|
loading: false,
|
||||||
|
callback: options.callback || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideConfirmDialog = () => {
|
||||||
|
confirmDialog.value.show = false
|
||||||
|
confirmDialog.value.callback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSettings = (newSettings) => {
|
||||||
|
settings.value = { ...settings.value, ...newSettings }
|
||||||
|
// 로컬 스토리지에 저장
|
||||||
|
localStorage.setItem('app_settings', JSON.stringify(settings.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSettings = () => {
|
||||||
|
const savedSettings = localStorage.getItem('app_settings')
|
||||||
|
if (savedSettings) {
|
||||||
|
settings.value = { ...settings.value, ...JSON.parse(savedSettings) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
isLoading,
|
||||||
|
loadingMessage,
|
||||||
|
error,
|
||||||
|
drawer,
|
||||||
|
rail,
|
||||||
|
snackbar,
|
||||||
|
confirmDialog,
|
||||||
|
settings,
|
||||||
|
// Getters
|
||||||
|
isDrawerOpen,
|
||||||
|
isRailMode,
|
||||||
|
currentError,
|
||||||
|
// Actions
|
||||||
|
setLoading,
|
||||||
|
setError,
|
||||||
|
clearError,
|
||||||
|
toggleDrawer,
|
||||||
|
setDrawer,
|
||||||
|
toggleRail,
|
||||||
|
setRail,
|
||||||
|
showSnackbar,
|
||||||
|
hideSnackbar,
|
||||||
|
showConfirmDialog,
|
||||||
|
hideConfirmDialog,
|
||||||
|
updateSettings,
|
||||||
|
loadSettings,
|
||||||
|
}
|
||||||
|
})
|
||||||
117
src/store/auth.js
Normal file
117
src/store/auth.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
//* src/store/auth.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 관리 스토어
|
||||||
|
* 로그인/로그아웃 상태 관리
|
||||||
|
*/
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
// State
|
||||||
|
const user = ref(null)
|
||||||
|
const token = ref(localStorage.getItem('auth_token') || null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||||
|
const userInfo = computed(() => user.value)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const login = async (credentials) => {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
console.log('Auth Store - 로그인 시도:', credentials)
|
||||||
|
|
||||||
|
// 임시 로그인 로직 (실제 API 연동 전)
|
||||||
|
if (credentials.userId === 'user01' && credentials.password === 'passw0rd') {
|
||||||
|
const userData = {
|
||||||
|
id: 1,
|
||||||
|
userId: credentials.userId,
|
||||||
|
name: '테스트 사용자',
|
||||||
|
email: 'test@example.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = 'temp_jwt_token_' + Date.now()
|
||||||
|
|
||||||
|
// 상태 업데이트
|
||||||
|
token.value = tokenData
|
||||||
|
user.value = userData
|
||||||
|
|
||||||
|
// 로컬 스토리지에 저장
|
||||||
|
localStorage.setItem('auth_token', tokenData)
|
||||||
|
localStorage.setItem('user_info', JSON.stringify(userData))
|
||||||
|
|
||||||
|
console.log('Auth Store - 로그인 성공:', { token: tokenData, user: userData })
|
||||||
|
console.log('Auth Store - isAuthenticated:', isAuthenticated.value)
|
||||||
|
|
||||||
|
return { success: true, user: userData, token: tokenData }
|
||||||
|
} else {
|
||||||
|
return { success: false, message: '아이디 또는 비밀번호가 틀렸습니다' }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth Store - 로그인 에러:', error)
|
||||||
|
return { success: false, message: '로그인 중 오류가 발생했습니다' }
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
console.log('Auth Store - 로그아웃')
|
||||||
|
token.value = null
|
||||||
|
user.value = null
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
localStorage.removeItem('user_info')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAuth = () => {
|
||||||
|
console.log('Auth Store - 인증 정보 클리어')
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkAuth = () => {
|
||||||
|
console.log('Auth Store - 인증 상태 체크')
|
||||||
|
const storedToken = localStorage.getItem('auth_token')
|
||||||
|
const storedUser = localStorage.getItem('user_info')
|
||||||
|
|
||||||
|
if (storedToken && storedUser) {
|
||||||
|
try {
|
||||||
|
token.value = storedToken
|
||||||
|
user.value = JSON.parse(storedUser)
|
||||||
|
console.log('Auth Store - 저장된 인증 정보 복원:', { token: storedToken, user: user.value })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth Store - 인증 정보 복원 실패:', error)
|
||||||
|
clearAuth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshUserInfo = async () => {
|
||||||
|
console.log('Auth Store - 사용자 정보 갱신')
|
||||||
|
// 임시로 현재 사용자 정보 반환
|
||||||
|
if (token.value) {
|
||||||
|
return Promise.resolve(user.value)
|
||||||
|
} else {
|
||||||
|
throw new Error('토큰이 없습니다')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기 인증 상태 체크
|
||||||
|
checkAuth()
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
isLoading,
|
||||||
|
// Getters
|
||||||
|
isAuthenticated,
|
||||||
|
userInfo,
|
||||||
|
// Actions
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
clearAuth,
|
||||||
|
checkAuth,
|
||||||
|
refreshUserInfo,
|
||||||
|
}
|
||||||
|
})
|
||||||
300
src/store/content.js
Normal file
300
src/store/content.js
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
//* src/store/content.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 관리 스토어
|
||||||
|
* 마케팅 콘텐츠 CRUD 및 상태 관리
|
||||||
|
*/
|
||||||
|
export const useContentStore = defineStore('content', () => {
|
||||||
|
// State
|
||||||
|
const contents = ref([])
|
||||||
|
const currentContent = ref(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isGenerating = ref(false)
|
||||||
|
const filters = ref({
|
||||||
|
type: 'all',
|
||||||
|
platform: 'all',
|
||||||
|
status: 'all',
|
||||||
|
dateRange: 'all',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 임시 데이터 (실제로는 API에서 가져옴)
|
||||||
|
const sampleContents = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '떡볶이 신메뉴 출시 이벤트',
|
||||||
|
type: 'sns',
|
||||||
|
platform: 'instagram',
|
||||||
|
content: '🌶️ 떡볶이 신메뉴 출시! 치즈가 듬뿍 들어간 치즈떡볶이가 새로 나왔어요!',
|
||||||
|
hashtags: ['떡볶이', '신메뉴', '치즈떡볶이', '맛집'],
|
||||||
|
images: ['/images/menu-placeholder.png'],
|
||||||
|
status: 'published',
|
||||||
|
views: 1240,
|
||||||
|
likes: 85,
|
||||||
|
comments: 12,
|
||||||
|
createdAt: new Date('2024-01-15T10:30:00'),
|
||||||
|
publishedAt: new Date('2024-01-15T14:00:00'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '매장 소개 포스터',
|
||||||
|
type: 'poster',
|
||||||
|
platform: 'blog',
|
||||||
|
content: '우리 매장을 소개하는 포스터입니다.',
|
||||||
|
hashtags: ['매장소개', '분식집', '맛집'],
|
||||||
|
images: ['/images/store-placeholder.png'],
|
||||||
|
status: 'draft',
|
||||||
|
views: 0,
|
||||||
|
likes: 0,
|
||||||
|
comments: 0,
|
||||||
|
createdAt: new Date('2024-01-14T16:20:00'),
|
||||||
|
publishedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '할인 이벤트 안내',
|
||||||
|
type: 'sns',
|
||||||
|
platform: 'instagram',
|
||||||
|
content: '🎉 특별 할인 이벤트! 오늘 하루만 모든 메뉴 20% 할인!',
|
||||||
|
hashtags: ['할인', '이벤트', '특가', '분식'],
|
||||||
|
images: ['/images/ai-character.png'],
|
||||||
|
status: 'scheduled',
|
||||||
|
views: 0,
|
||||||
|
likes: 0,
|
||||||
|
comments: 0,
|
||||||
|
createdAt: new Date('2024-01-13T09:15:00'),
|
||||||
|
scheduledAt: new Date('2024-01-16T12:00:00'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const contentCount = computed(() => contents.value.length)
|
||||||
|
|
||||||
|
const filteredContents = computed(() => {
|
||||||
|
let filtered = contents.value
|
||||||
|
|
||||||
|
if (filters.value.type !== 'all') {
|
||||||
|
filtered = filtered.filter((content) => content.type === filters.value.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.platform !== 'all') {
|
||||||
|
filtered = filtered.filter((content) => content.platform === filters.value.platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.status !== 'all') {
|
||||||
|
filtered = filtered.filter((content) => content.status === filters.value.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||||
|
})
|
||||||
|
|
||||||
|
const publishedContents = computed(() =>
|
||||||
|
contents.value.filter((content) => content.status === 'published')
|
||||||
|
)
|
||||||
|
|
||||||
|
const draftContents = computed(() =>
|
||||||
|
contents.value.filter((content) => content.status === 'draft')
|
||||||
|
)
|
||||||
|
|
||||||
|
const scheduledContents = computed(() =>
|
||||||
|
contents.value.filter((content) => content.status === 'scheduled')
|
||||||
|
)
|
||||||
|
|
||||||
|
const recentContents = computed(() => {
|
||||||
|
return contents.value.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).slice(0, 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalViews = computed(() =>
|
||||||
|
contents.value.reduce((sum, content) => sum + (content.views || 0), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalLikes = computed(() =>
|
||||||
|
contents.value.reduce((sum, content) => sum + (content.likes || 0), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const loadContents = async () => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
// API 호출 시뮬레이션
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// 임시로 샘플 데이터 사용
|
||||||
|
contents.value = [...sampleContents]
|
||||||
|
|
||||||
|
console.log('콘텐츠 목록 로드 완료:', contents.value.length)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('콘텐츠 로드 실패:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContentById = (id) => {
|
||||||
|
return contents.value.find((content) => content.id === parseInt(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addContent = (content) => {
|
||||||
|
const newContent = {
|
||||||
|
...content,
|
||||||
|
id: Date.now(), // 임시 ID
|
||||||
|
createdAt: new Date(),
|
||||||
|
views: 0,
|
||||||
|
likes: 0,
|
||||||
|
comments: 0,
|
||||||
|
}
|
||||||
|
contents.value.unshift(newContent)
|
||||||
|
return newContent
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateContent = (contentId, updatedData) => {
|
||||||
|
const index = contents.value.findIndex((content) => content.id === contentId)
|
||||||
|
if (index !== -1) {
|
||||||
|
contents.value[index] = {
|
||||||
|
...contents.value[index],
|
||||||
|
...updatedData,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}
|
||||||
|
return contents.value[index]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteContent = (contentId) => {
|
||||||
|
const index = contents.value.findIndex((content) => content.id === contentId)
|
||||||
|
if (index !== -1) {
|
||||||
|
const deletedContent = contents.value[index]
|
||||||
|
contents.value.splice(index, 1)
|
||||||
|
return deletedContent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishContent = async (contentId) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
// API 호출 시뮬레이션
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
const content = updateContent(contentId, {
|
||||||
|
status: 'published',
|
||||||
|
publishedAt: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('콘텐츠 발행 완료:', content?.title)
|
||||||
|
return content
|
||||||
|
} catch (error) {
|
||||||
|
console.error('콘텐츠 발행 실패:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleContent = async (contentId, scheduledTime) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
// API 호출 시뮬레이션
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
const content = updateContent(contentId, {
|
||||||
|
status: 'scheduled',
|
||||||
|
scheduledAt: new Date(scheduledTime),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('콘텐츠 예약 완료:', content?.title)
|
||||||
|
return content
|
||||||
|
} catch (error) {
|
||||||
|
console.error('콘텐츠 예약 실패:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateContent = async (options) => {
|
||||||
|
try {
|
||||||
|
isGenerating.value = true
|
||||||
|
|
||||||
|
// AI 콘텐츠 생성 시뮬레이션
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
|
const generatedContent = {
|
||||||
|
title: `AI 생성 콘텐츠 - ${options.type}`,
|
||||||
|
type: options.type,
|
||||||
|
platform: options.platform,
|
||||||
|
content: `AI가 생성한 ${options.type} 콘텐츠입니다. ${options.description || ''}`,
|
||||||
|
hashtags: options.hashtags || [],
|
||||||
|
images: options.images || [],
|
||||||
|
status: 'draft',
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent = addContent(generatedContent)
|
||||||
|
console.log('AI 콘텐츠 생성 완료:', newContent.title)
|
||||||
|
|
||||||
|
return newContent
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI 콘텐츠 생성 실패:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
isGenerating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFilters = (newFilters) => {
|
||||||
|
filters.value = { ...filters.value, ...newFilters }
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
filters.value = {
|
||||||
|
type: 'all',
|
||||||
|
platform: 'all',
|
||||||
|
status: 'all',
|
||||||
|
dateRange: 'all',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCurrentContent = (content) => {
|
||||||
|
currentContent.value = content
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCurrentContent = () => {
|
||||||
|
currentContent.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
contents,
|
||||||
|
currentContent,
|
||||||
|
isLoading,
|
||||||
|
isGenerating,
|
||||||
|
filters,
|
||||||
|
// Getters
|
||||||
|
contentCount,
|
||||||
|
filteredContents,
|
||||||
|
publishedContents,
|
||||||
|
draftContents,
|
||||||
|
scheduledContents,
|
||||||
|
recentContents,
|
||||||
|
totalViews,
|
||||||
|
totalLikes,
|
||||||
|
// Actions
|
||||||
|
loadContents,
|
||||||
|
getContentById,
|
||||||
|
addContent,
|
||||||
|
updateContent,
|
||||||
|
deleteContent,
|
||||||
|
publishContent,
|
||||||
|
scheduleContent,
|
||||||
|
generateContent,
|
||||||
|
setFilters,
|
||||||
|
resetFilters,
|
||||||
|
setCurrentContent,
|
||||||
|
clearCurrentContent,
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,214 +1,213 @@
|
|||||||
|
//* src/utils/constants.js
|
||||||
/**
|
/**
|
||||||
* AI 마케팅 서비스 상수 정의
|
* 애플리케이션 전역 상수 정의
|
||||||
*
|
|
||||||
* @description 애플리케이션 전반에서 사용되는 상수들을 정의합니다.
|
|
||||||
* @author AI Marketing Team
|
|
||||||
* @version 1.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// API 엔드포인트 상수
|
// 콘텐츠 타입
|
||||||
export const API_ENDPOINTS = {
|
export const CONTENT_TYPES = {
|
||||||
AUTH: '/auth',
|
SNS: 'sns',
|
||||||
MEMBER: '/member',
|
POSTER: 'poster',
|
||||||
STORE: '/store',
|
VIDEO: 'video',
|
||||||
MENU: '/menu',
|
BLOG: 'blog',
|
||||||
CONTENT: '/content',
|
|
||||||
RECOMMEND: '/recommend',
|
|
||||||
ANALYSIS: '/analysis'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 인증 관련 상수
|
export const CONTENT_TYPE_LABELS = {
|
||||||
export const AUTH_CONSTANTS = {
|
[CONTENT_TYPES.SNS]: 'SNS 게시물',
|
||||||
ACCESS_TOKEN_KEY: 'accessToken',
|
[CONTENT_TYPES.POSTER]: '홍보 포스터',
|
||||||
REFRESH_TOKEN_KEY: 'refreshToken',
|
[CONTENT_TYPES.VIDEO]: '비디오',
|
||||||
USER_INFO_KEY: 'userInfo',
|
[CONTENT_TYPES.BLOG]: '블로그 포스트',
|
||||||
TOKEN_EXPIRY_BUFFER: 5 * 60 * 1000, // 5분 (밀리초)
|
|
||||||
MAX_LOGIN_ATTEMPTS: 5,
|
|
||||||
LOCKOUT_DURATION: 15 * 60 * 1000 // 15분 (밀리초)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 콘텐츠 타입 상수
|
// 플랫폼
|
||||||
export const CONTENT_TYPES = [
|
export const PLATFORMS = {
|
||||||
{ text: 'SNS 포스트', value: 'SNS_POST', icon: 'mdi-instagram' },
|
INSTAGRAM: 'instagram',
|
||||||
{ text: '포스터', value: 'POSTER', icon: 'mdi-image' },
|
NAVER_BLOG: 'naver_blog',
|
||||||
{ text: '블로그 글', value: 'BLOG', icon: 'mdi-post' },
|
FACEBOOK: 'facebook',
|
||||||
{ text: '광고 문구', value: 'AD_COPY', icon: 'mdi-bullhorn' }
|
TWITTER: 'twitter',
|
||||||
]
|
YOUTUBE: 'youtube',
|
||||||
|
KAKAO: 'kakao',
|
||||||
// SNS 플랫폼 상수
|
|
||||||
export const PLATFORMS = [
|
|
||||||
{ text: '인스타그램', value: 'INSTAGRAM', icon: 'mdi-instagram', color: '#E4405F' },
|
|
||||||
{ text: '페이스북', value: 'FACEBOOK', icon: 'mdi-facebook', color: '#1877F2' },
|
|
||||||
{ text: '네이버 블로그', value: 'NAVER_BLOG', icon: 'mdi-post', color: '#03C75A' },
|
|
||||||
{ text: '카카오톡', value: 'KAKAO_TALK', icon: 'mdi-chat', color: '#FEE500' },
|
|
||||||
{ text: '트위터', value: 'TWITTER', icon: 'mdi-twitter', color: '#1DA1F2' },
|
|
||||||
{ text: '유튜브', value: 'YOUTUBE', icon: 'mdi-youtube', color: '#FF0000' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 콘텐츠 상태 상수
|
|
||||||
export const CONTENT_STATUS = [
|
|
||||||
{ text: '초안', value: 'DRAFT', color: 'grey', icon: 'mdi-file-document-outline' },
|
|
||||||
{ text: '검토중', value: 'REVIEW', color: 'orange', icon: 'mdi-eye' },
|
|
||||||
{ text: '승인됨', value: 'APPROVED', color: 'green', icon: 'mdi-check-circle' },
|
|
||||||
{ text: '게시됨', value: 'PUBLISHED', color: 'blue', icon: 'mdi-publish' },
|
|
||||||
{ text: '보관됨', value: 'ARCHIVED', color: 'grey-darken-2', icon: 'mdi-archive' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 톤앤매너 옵션
|
|
||||||
export const TONE_OPTIONS = [
|
|
||||||
{ text: '친근함', value: '친근함', description: '고객과 가까운 느낌의 편안한 톤' },
|
|
||||||
{ text: '전문적', value: '전문적', description: '신뢰할 수 있는 전문가 느낌' },
|
|
||||||
{ text: '유머러스', value: '유머러스', description: '재미있고 유쾌한 분위기' },
|
|
||||||
{ text: '고급스러움', value: '고급스러움', description: '품격 있고 세련된 느낌' },
|
|
||||||
{ text: '트렌디', value: '트렌디', description: '최신 트렌드를 반영한 스타일' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 감정 강도 옵션
|
|
||||||
export const EMOTION_INTENSITY = [
|
|
||||||
{ text: '차분함', value: '차분함', description: '차분하고 안정적인 톤' },
|
|
||||||
{ text: '보통', value: '보통', description: '적당한 감정 표현' },
|
|
||||||
{ text: '활발함', value: '활발함', description: '에너지 넘치는 표현' },
|
|
||||||
{ text: '열정적', value: '열정적', description: '강렬하고 역동적인 표현' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 프로모션 옵션
|
|
||||||
export const PROMOTION_OPTIONS = [
|
|
||||||
{ text: '없음', value: '없음' },
|
|
||||||
{ text: '할인 이벤트', value: '할인 이벤트' },
|
|
||||||
{ text: '신메뉴 출시', value: '신메뉴 출시' },
|
|
||||||
{ text: '시즌 특가', value: '시즌 특가' },
|
|
||||||
{ text: '회원 혜택', value: '회원 혜택' },
|
|
||||||
{ text: '기념일 이벤트', value: '기념일 이벤트' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 업종 카테고리
|
|
||||||
export const BUSINESS_CATEGORIES = [
|
|
||||||
{ text: '음식점', value: 'RESTAURANT', icon: 'mdi-food' },
|
|
||||||
{ text: '카페', value: 'CAFE', icon: 'mdi-coffee' },
|
|
||||||
{ text: '베이커리', value: 'BAKERY', icon: 'mdi-cake' },
|
|
||||||
{ text: '치킨/피자', value: 'CHICKEN_PIZZA', icon: 'mdi-pizza' },
|
|
||||||
{ text: '분식', value: 'SNACK_BAR', icon: 'mdi-food-hot-dog' },
|
|
||||||
{ text: '술집', value: 'BAR', icon: 'mdi-glass-mug' },
|
|
||||||
{ text: '기타', value: 'OTHER', icon: 'mdi-store' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 메뉴 카테고리
|
|
||||||
export const MENU_CATEGORIES = [
|
|
||||||
{ text: '주메뉴', value: 'MAIN', color: 'primary' },
|
|
||||||
{ text: '사이드', value: 'SIDE', color: 'secondary' },
|
|
||||||
{ text: '음료', value: 'BEVERAGE', color: 'info' },
|
|
||||||
{ text: '디저트', value: 'DESSERT', color: 'warning' },
|
|
||||||
{ text: '세트메뉴', value: 'SET', color: 'success' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 매출 분석 기간 옵션
|
|
||||||
export const ANALYSIS_PERIODS = [
|
|
||||||
{ text: '오늘', value: 'today' },
|
|
||||||
{ text: '어제', value: 'yesterday' },
|
|
||||||
{ text: '이번 주', value: 'this_week' },
|
|
||||||
{ text: '지난 주', value: 'last_week' },
|
|
||||||
{ text: '이번 달', value: 'this_month' },
|
|
||||||
{ text: '지난 달', value: 'last_month' },
|
|
||||||
{ text: '지난 3개월', value: 'last_3_months' },
|
|
||||||
{ text: '직접 선택', value: 'custom' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 차트 타입 옵션
|
|
||||||
export const CHART_TYPES = [
|
|
||||||
{ text: '일별', value: 'daily', icon: 'mdi-calendar-today' },
|
|
||||||
{ text: '주별', value: 'weekly', icon: 'mdi-calendar-week' },
|
|
||||||
{ text: '월별', value: 'monthly', icon: 'mdi-calendar-month' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// AI 추천 타입
|
|
||||||
export const AI_RECOMMENDATION_TYPES = [
|
|
||||||
{ text: '마케팅 팁', value: 'MARKETING_TIP', icon: 'mdi-lightbulb' },
|
|
||||||
{ text: '메뉴 제안', value: 'MENU_SUGGESTION', icon: 'mdi-food' },
|
|
||||||
{ text: '프로모션 아이디어', value: 'PROMOTION_IDEA', icon: 'mdi-sale' },
|
|
||||||
{ text: '콘텐츠 아이디어', value: 'CONTENT_IDEA', icon: 'mdi-lightbulb-variant' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 날씨 조건 상수
|
|
||||||
export const WEATHER_CONDITIONS = [
|
|
||||||
{ text: '맑음', value: 'SUNNY', icon: 'mdi-weather-sunny' },
|
|
||||||
{ text: '흐림', value: 'CLOUDY', icon: 'mdi-weather-cloudy' },
|
|
||||||
{ text: '비', value: 'RAINY', icon: 'mdi-weather-rainy' },
|
|
||||||
{ text: '눈', value: 'SNOWY', icon: 'mdi-weather-snowy' },
|
|
||||||
{ text: '바람', value: 'WINDY', icon: 'mdi-weather-windy' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 시간대 분류
|
|
||||||
export const TIME_SLOTS = [
|
|
||||||
{ text: '아침 (06:00-11:00)', value: 'MORNING', start: 6, end: 11 },
|
|
||||||
{ text: '점심 (11:00-14:00)', value: 'LUNCH', start: 11, end: 14 },
|
|
||||||
{ text: '오후 (14:00-17:00)', value: 'AFTERNOON', start: 14, end: 17 },
|
|
||||||
{ text: '저녁 (17:00-21:00)', value: 'DINNER', start: 17, end: 21 },
|
|
||||||
{ text: '밤 (21:00-24:00)', value: 'NIGHT', start: 21, end: 24 },
|
|
||||||
{ text: '심야 (00:00-06:00)', value: 'LATE_NIGHT', start: 0, end: 6 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 페이지네이션 상수
|
|
||||||
export const PAGINATION = {
|
|
||||||
DEFAULT_PAGE_SIZE: 10,
|
|
||||||
PAGE_SIZE_OPTIONS: [5, 10, 20, 50],
|
|
||||||
MAX_VISIBLE_PAGES: 5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파일 업로드 상수
|
export const PLATFORM_LABELS = {
|
||||||
export const FILE_UPLOAD = {
|
[PLATFORMS.INSTAGRAM]: '인스타그램',
|
||||||
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
|
[PLATFORMS.NAVER_BLOG]: '네이버 블로그',
|
||||||
ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
[PLATFORMS.FACEBOOK]: '페이스북',
|
||||||
ALLOWED_VIDEO_TYPES: ['video/mp4', 'video/avi', 'video/mov'],
|
[PLATFORMS.TWITTER]: '트위터',
|
||||||
MAX_IMAGES_COUNT: 10,
|
[PLATFORMS.YOUTUBE]: '유튜브',
|
||||||
MAX_VIDEOS_COUNT: 3
|
[PLATFORMS.KAKAO]: '카카오',
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 관련 상수
|
export const PLATFORM_COLORS = {
|
||||||
export const UI_CONSTANTS = {
|
[PLATFORMS.INSTAGRAM]: 'purple',
|
||||||
MOBILE_BREAKPOINT: 768,
|
[PLATFORMS.NAVER_BLOG]: 'green',
|
||||||
TABLET_BREAKPOINT: 1024,
|
[PLATFORMS.FACEBOOK]: 'blue',
|
||||||
DESKTOP_BREAKPOINT: 1200,
|
[PLATFORMS.TWITTER]: 'light-blue',
|
||||||
HEADER_HEIGHT: 64,
|
[PLATFORMS.YOUTUBE]: 'red',
|
||||||
FOOTER_HEIGHT: 80,
|
[PLATFORMS.KAKAO]: 'yellow',
|
||||||
SIDEBAR_WIDTH: 280
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 알림 타입
|
// 콘텐츠 상태
|
||||||
export const NOTIFICATION_TYPES = {
|
export const CONTENT_STATUS = {
|
||||||
|
DRAFT: 'draft',
|
||||||
|
PUBLISHED: 'published',
|
||||||
|
SCHEDULED: 'scheduled',
|
||||||
|
ARCHIVED: 'archived',
|
||||||
|
FAILED: 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CONTENT_STATUS_LABELS = {
|
||||||
|
[CONTENT_STATUS.DRAFT]: '임시저장',
|
||||||
|
[CONTENT_STATUS.PUBLISHED]: '발행됨',
|
||||||
|
[CONTENT_STATUS.SCHEDULED]: '예약됨',
|
||||||
|
[CONTENT_STATUS.ARCHIVED]: '보관됨',
|
||||||
|
[CONTENT_STATUS.FAILED]: '실패',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CONTENT_STATUS_COLORS = {
|
||||||
|
[CONTENT_STATUS.DRAFT]: 'orange',
|
||||||
|
[CONTENT_STATUS.PUBLISHED]: 'success',
|
||||||
|
[CONTENT_STATUS.SCHEDULED]: 'info',
|
||||||
|
[CONTENT_STATUS.ARCHIVED]: 'grey',
|
||||||
|
[CONTENT_STATUS.FAILED]: 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 매장 업종
|
||||||
|
export const BUSINESS_TYPES = {
|
||||||
|
RESTAURANT: 'restaurant',
|
||||||
|
CAFE: 'cafe',
|
||||||
|
SNACK_BAR: 'snack_bar',
|
||||||
|
FAST_FOOD: 'fast_food',
|
||||||
|
BAKERY: 'bakery',
|
||||||
|
DESSERT: 'dessert',
|
||||||
|
CONVENIENCE: 'convenience',
|
||||||
|
OTHER: 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BUSINESS_TYPE_LABELS = {
|
||||||
|
[BUSINESS_TYPES.RESTAURANT]: '일반음식점',
|
||||||
|
[BUSINESS_TYPES.CAFE]: '카페',
|
||||||
|
[BUSINESS_TYPES.SNACK_BAR]: '분식점',
|
||||||
|
[BUSINESS_TYPES.FAST_FOOD]: '패스트푸드',
|
||||||
|
[BUSINESS_TYPES.BAKERY]: '제과점',
|
||||||
|
[BUSINESS_TYPES.DESSERT]: '디저트카페',
|
||||||
|
[BUSINESS_TYPES.CONVENIENCE]: '편의점',
|
||||||
|
[BUSINESS_TYPES.OTHER]: '기타',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 톤앤매너
|
||||||
|
export const TONE_AND_MANNER = {
|
||||||
|
FRIENDLY: 'friendly',
|
||||||
|
PROFESSIONAL: 'professional',
|
||||||
|
HUMOROUS: 'humorous',
|
||||||
|
ELEGANT: 'elegant',
|
||||||
|
CASUAL: 'casual',
|
||||||
|
TRENDY: 'trendy',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TONE_AND_MANNER_LABELS = {
|
||||||
|
[TONE_AND_MANNER.FRIENDLY]: '친근함',
|
||||||
|
[TONE_AND_MANNER.PROFESSIONAL]: '전문적',
|
||||||
|
[TONE_AND_MANNER.HUMOROUS]: '유머러스',
|
||||||
|
[TONE_AND_MANNER.ELEGANT]: '고급스러운',
|
||||||
|
[TONE_AND_MANNER.CASUAL]: '캐주얼',
|
||||||
|
[TONE_AND_MANNER.TRENDY]: '트렌디',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 감정 강도
|
||||||
|
export const EMOTION_INTENSITY = {
|
||||||
|
CALM: 'calm',
|
||||||
|
NORMAL: 'normal',
|
||||||
|
ENTHUSIASTIC: 'enthusiastic',
|
||||||
|
EXCITING: 'exciting',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EMOTION_INTENSITY_LABELS = {
|
||||||
|
[EMOTION_INTENSITY.CALM]: '차분함',
|
||||||
|
[EMOTION_INTENSITY.NORMAL]: '보통',
|
||||||
|
[EMOTION_INTENSITY.ENTHUSIASTIC]: '열정적',
|
||||||
|
[EMOTION_INTENSITY.EXCITING]: '과장된',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로모션 타입
|
||||||
|
export const PROMOTION_TYPES = {
|
||||||
|
DISCOUNT: 'discount',
|
||||||
|
EVENT: 'event',
|
||||||
|
NEW_MENU: 'new_menu',
|
||||||
|
NONE: 'none',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PROMOTION_TYPE_LABELS = {
|
||||||
|
[PROMOTION_TYPES.DISCOUNT]: '할인 정보',
|
||||||
|
[PROMOTION_TYPES.EVENT]: '이벤트 정보',
|
||||||
|
[PROMOTION_TYPES.NEW_MENU]: '신메뉴 알림',
|
||||||
|
[PROMOTION_TYPES.NONE]: '없음',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 스타일
|
||||||
|
export const PHOTO_STYLES = {
|
||||||
|
MODERN: 'modern',
|
||||||
|
CLASSIC: 'classic',
|
||||||
|
EMOTIONAL: 'emotional',
|
||||||
|
MINIMALIST: 'minimalist',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PHOTO_STYLE_LABELS = {
|
||||||
|
[PHOTO_STYLES.MODERN]: '모던',
|
||||||
|
[PHOTO_STYLES.CLASSIC]: '클래식',
|
||||||
|
[PHOTO_STYLES.EMOTIONAL]: '감성적',
|
||||||
|
[PHOTO_STYLES.MINIMALIST]: '미니멀',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 업로드 제한
|
||||||
|
export const FILE_LIMITS = {
|
||||||
|
MAX_SIZE: 10 * 1024 * 1024, // 10MB
|
||||||
|
ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||||||
|
ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 응답 상태
|
||||||
|
export const API_STATUS = {
|
||||||
SUCCESS: 'success',
|
SUCCESS: 'success',
|
||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
WARNING: 'warning',
|
LOADING: 'loading',
|
||||||
INFO: 'info'
|
IDLE: 'idle',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로딩 상태 메시지
|
// 페이지네이션
|
||||||
export const LOADING_MESSAGES = {
|
export const PAGINATION = {
|
||||||
GENERATING_CONTENT: 'AI가 콘텐츠를 생성하고 있습니다...',
|
DEFAULT_PAGE_SIZE: 20,
|
||||||
UPLOADING_FILE: '파일을 업로드하고 있습니다...',
|
PAGE_SIZE_OPTIONS: [10, 20, 50, 100],
|
||||||
SAVING_DATA: '데이터를 저장하고 있습니다...',
|
|
||||||
LOADING_DATA: '데이터를 불러오고 있습니다...',
|
|
||||||
ANALYZING_DATA: '데이터를 분석하고 있습니다...'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 에러 메시지
|
// 로컬 스토리지 키
|
||||||
export const ERROR_MESSAGES = {
|
export const STORAGE_KEYS = {
|
||||||
NETWORK_ERROR: '네트워크 연결을 확인해주세요.',
|
AUTH_TOKEN: 'auth_token',
|
||||||
SERVER_ERROR: '서버에서 오류가 발생했습니다.',
|
USER_INFO: 'user_info',
|
||||||
UNAUTHORIZED: '로그인이 필요합니다.',
|
APP_SETTINGS: 'app_settings',
|
||||||
FORBIDDEN: '접근 권한이 없습니다.',
|
CONTENT_FILTERS: 'content_filters',
|
||||||
NOT_FOUND: '요청한 데이터를 찾을 수 없습니다.',
|
|
||||||
VALIDATION_ERROR: '입력 정보를 확인해주세요.',
|
|
||||||
FILE_SIZE_ERROR: '파일 크기가 너무 큽니다.',
|
|
||||||
FILE_TYPE_ERROR: '지원하지 않는 파일 형식입니다.'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 성공 메시지
|
// 시간 관련 상수
|
||||||
export const SUCCESS_MESSAGES = {
|
export const TIME_FORMATS = {
|
||||||
SAVE_SUCCESS: '성공적으로 저장되었습니다.',
|
DATE: 'YYYY-MM-DD',
|
||||||
UPDATE_SUCCESS: '성공적으로 수정되었습니다.',
|
DATETIME: 'YYYY-MM-DD HH:mm:ss',
|
||||||
DELETE_SUCCESS: '성공적으로 삭제되었습니다.',
|
TIME: 'HH:mm',
|
||||||
UPLOAD_SUCCESS: '파일이 성공적으로 업로드되었습니다.',
|
}
|
||||||
CONTENT_GENERATED: 'AI 콘텐츠가 성공적으로 생성되었습니다.',
|
|
||||||
LOGIN_SUCCESS: '로그인되었습니다.',
|
export const DATE_RANGES = {
|
||||||
LOGOUT_SUCCESS: '로그아웃되었습니다.'
|
TODAY: 'today',
|
||||||
|
WEEK: 'week',
|
||||||
|
MONTH: 'month',
|
||||||
|
QUARTER: 'quarter',
|
||||||
|
YEAR: 'year',
|
||||||
|
ALL: 'all',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATE_RANGE_LABELS = {
|
||||||
|
[DATE_RANGES.TODAY]: '오늘',
|
||||||
|
[DATE_RANGES.WEEK]: '최근 1주일',
|
||||||
|
[DATE_RANGES.MONTH]: '최근 1개월',
|
||||||
|
[DATE_RANGES.QUARTER]: '최근 3개월',
|
||||||
|
[DATE_RANGES.YEAR]: '최근 1년',
|
||||||
|
[DATE_RANGES.ALL]: '전체',
|
||||||
}
|
}
|
||||||
@ -1,345 +1,281 @@
|
|||||||
|
//* src/utils/formatters.js
|
||||||
/**
|
/**
|
||||||
* 데이터 포맷팅 유틸리티
|
* 포맷팅 유틸리티 함수들
|
||||||
*
|
|
||||||
* @description 다양한 데이터 타입을 사용자 친화적인 형태로 포맷팅하는 함수들을 제공합니다.
|
|
||||||
* @author AI Marketing Team
|
|
||||||
* @version 1.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 숫자를 통화 형식으로 포맷팅
|
* 통화 포맷 (한국 원화)
|
||||||
* @param {number} amount - 포맷팅할 금액
|
|
||||||
* @param {string} currency - 통화 단위 (기본값: 'KRW')
|
|
||||||
* @param {boolean} showSymbol - 통화 기호 표시 여부 (기본값: true)
|
|
||||||
* @returns {string} 포맷팅된 통화 문자열
|
|
||||||
*/
|
*/
|
||||||
export const formatCurrency = (amount, currency = 'KRW', showSymbol = true) => {
|
export const formatCurrency = (amount) => {
|
||||||
if (amount === null || amount === undefined || isNaN(amount)) {
|
if (!amount && amount !== 0) return '₩0'
|
||||||
return '0원'
|
return new Intl.NumberFormat('ko-KR', {
|
||||||
}
|
|
||||||
|
|
||||||
const formatter = new Intl.NumberFormat('ko-KR', {
|
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: currency,
|
currency: 'KRW',
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
maximumFractionDigits: 0
|
maximumFractionDigits: 0,
|
||||||
})
|
}).format(amount)
|
||||||
|
|
||||||
if (showSymbol) {
|
|
||||||
return formatter.format(amount)
|
|
||||||
} else {
|
|
||||||
return amount.toLocaleString('ko-KR')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 큰 숫자를 축약된 형태로 포맷팅 (예: 1000 -> 1K)
|
* 숫자 포맷 (천 단위 콤마)
|
||||||
* @param {number} num - 포맷팅할 숫자
|
|
||||||
* @param {number} digits - 소수점 자릿수 (기본값: 1)
|
|
||||||
* @returns {string} 축약된 숫자 문자열
|
|
||||||
*/
|
*/
|
||||||
export const formatAbbreviatedNumber = (num, digits = 1) => {
|
export const formatNumber = (number) => {
|
||||||
if (num === null || num === undefined || isNaN(num)) {
|
if (!number && number !== 0) return '0'
|
||||||
return '0'
|
return new Intl.NumberFormat('ko-KR').format(number)
|
||||||
}
|
|
||||||
|
|
||||||
const units = [
|
|
||||||
{ value: 1e9, symbol: 'B' },
|
|
||||||
{ value: 1e8, symbol: '억' },
|
|
||||||
{ value: 1e6, symbol: 'M' },
|
|
||||||
{ value: 1e4, symbol: '만' },
|
|
||||||
{ value: 1e3, symbol: 'K' }
|
|
||||||
]
|
|
||||||
|
|
||||||
for (let unit of units) {
|
|
||||||
if (Math.abs(num) >= unit.value) {
|
|
||||||
return (num / unit.value).toFixed(digits) + unit.symbol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return num.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 퍼센트를 포맷팅
|
* 퍼센트 포맷
|
||||||
* @param {number} value - 퍼센트 값
|
|
||||||
* @param {number} decimals - 소수점 자릿수 (기본값: 1)
|
|
||||||
* @param {boolean} showSign - 부호 표시 여부 (기본값: false)
|
|
||||||
* @returns {string} 포맷팅된 퍼센트 문자열
|
|
||||||
*/
|
*/
|
||||||
export const formatPercentage = (value, decimals = 1, showSign = false) => {
|
export const formatPercent = (value, decimals = 1) => {
|
||||||
if (value === null || value === undefined || isNaN(value)) {
|
if (!value && value !== 0) return '0%'
|
||||||
return '0%'
|
return `${parseFloat(value).toFixed(decimals)}%`
|
||||||
}
|
|
||||||
|
|
||||||
const sign = showSign && value > 0 ? '+' : ''
|
|
||||||
return `${sign}${value.toFixed(decimals)}%`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 날짜를 한국어 형식으로 포맷팅
|
* 상대적 시간 포맷 (예: "30분 전", "2시간 전")
|
||||||
* @param {Date|string} date - 포맷팅할 날짜
|
|
||||||
* @param {string} format - 포맷 형식 ('full', 'date', 'time', 'datetime', 'relative')
|
|
||||||
* @returns {string} 포맷팅된 날짜 문자열
|
|
||||||
*/
|
*/
|
||||||
export const formatDate = (date, format = 'date') => {
|
export const formatRelativeTime = (date) => {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
|
|
||||||
const dateObj = typeof date === 'string' ? new Date(date) : date
|
|
||||||
|
|
||||||
if (isNaN(dateObj.getTime())) {
|
|
||||||
return '잘못된 날짜'
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
full: {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
weekday: 'long'
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit'
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
},
|
|
||||||
datetime: {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format === 'relative') {
|
|
||||||
return formatRelativeTime(dateObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatOptions = options[format] || options.date
|
|
||||||
return new Intl.DateTimeFormat('ko-KR', formatOptions).format(dateObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 상대적 시간 포맷팅 (예: 2시간 전, 3일 후)
|
|
||||||
* @param {Date|string} date - 기준 날짜
|
|
||||||
* @param {Date} baseDate - 비교 기준 날짜 (기본값: 현재 시간)
|
|
||||||
* @returns {string} 상대적 시간 문자열
|
|
||||||
*/
|
|
||||||
export const formatRelativeTime = (date, baseDate = new Date()) => {
|
|
||||||
if (!date) return ''
|
|
||||||
|
|
||||||
const dateObj = typeof date === 'string' ? new Date(date) : date
|
|
||||||
const baseDateObj = typeof baseDate === 'string' ? new Date(baseDate) : baseDate
|
|
||||||
|
|
||||||
if (isNaN(dateObj.getTime()) || isNaN(baseDateObj.getTime())) {
|
|
||||||
return '잘못된 날짜'
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffInSeconds = Math.floor((baseDateObj - dateObj) / 1000)
|
|
||||||
const absDiff = Math.abs(diffInSeconds)
|
|
||||||
|
|
||||||
const units = [
|
|
||||||
{ name: '년', seconds: 31536000 },
|
|
||||||
{ name: '개월', seconds: 2592000 },
|
|
||||||
{ name: '일', seconds: 86400 },
|
|
||||||
{ name: '시간', seconds: 3600 },
|
|
||||||
{ name: '분', seconds: 60 }
|
|
||||||
]
|
|
||||||
|
|
||||||
for (let unit of units) {
|
|
||||||
const interval = Math.floor(absDiff / unit.seconds)
|
|
||||||
if (interval >= 1) {
|
|
||||||
return diffInSeconds > 0
|
|
||||||
? `${interval}${unit.name} 전`
|
|
||||||
: `${interval}${unit.name} 후`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diffInSeconds > 0 ? '방금 전' : '곧'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 날짜와 시간을 사용자 친화적 형식으로 포맷팅
|
|
||||||
* @param {Date|string} dateTime - 포맷팅할 날짜시간
|
|
||||||
* @returns {string} 포맷팅된 날짜시간 문자열
|
|
||||||
*/
|
|
||||||
export const formatDateTime = (dateTime) => {
|
|
||||||
if (!dateTime) return ''
|
|
||||||
|
|
||||||
const dateObj = typeof dateTime === 'string' ? new Date(dateTime) : dateTime
|
|
||||||
|
|
||||||
if (isNaN(dateObj.getTime())) {
|
|
||||||
return '잘못된 날짜'
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const diffInDays = Math.floor((now - dateObj) / (1000 * 60 * 60 * 24))
|
const targetDate = new Date(date)
|
||||||
|
const diffInMs = now - targetDate
|
||||||
|
const diffInMinutes = Math.floor(diffInMs / (1000 * 60))
|
||||||
|
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60))
|
||||||
|
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
if (diffInDays === 0) {
|
if (diffInMinutes < 1) return '방금 전'
|
||||||
// 오늘인 경우 시간만 표시
|
if (diffInMinutes < 60) return `${diffInMinutes}분 전`
|
||||||
return formatDate(dateObj, 'time')
|
if (diffInHours < 24) return `${diffInHours}시간 전`
|
||||||
} else if (diffInDays === 1) {
|
if (diffInDays < 7) return `${diffInDays}일 전`
|
||||||
// 어제인 경우
|
|
||||||
return `어제 ${formatDate(dateObj, 'time')}`
|
return targetDate.toLocaleDateString('ko-KR')
|
||||||
} else if (diffInDays < 7) {
|
|
||||||
// 일주일 이내인 경우
|
|
||||||
const weekday = dateObj.toLocaleDateString('ko-KR', { weekday: 'short' })
|
|
||||||
return `${weekday} ${formatDate(dateObj, 'time')}`
|
|
||||||
} else {
|
|
||||||
// 일주일 이상인 경우 전체 날짜 표시
|
|
||||||
return formatDate(dateObj, 'datetime')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 파일 크기를 사람이 읽기 쉬운 형태로 포맷팅
|
* 날짜 포맷 (YYYY-MM-DD 형식)
|
||||||
* @param {number} bytes - 파일 크기 (바이트)
|
|
||||||
* @param {number} decimals - 소수점 자릿수 (기본값: 2)
|
|
||||||
* @returns {string} 포맷팅된 파일 크기 문자열
|
|
||||||
*/
|
*/
|
||||||
export const formatFileSize = (bytes, decimals = 2) => {
|
export const formatDate = (date, options = {}) => {
|
||||||
if (bytes === 0) return '0 B'
|
if (!date) return ''
|
||||||
if (!bytes) return ''
|
|
||||||
|
const targetDate = new Date(date)
|
||||||
|
const defaultOptions = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetDate.toLocaleDateString('ko-KR', { ...defaultOptions, ...options })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜시간 포맷 (YYYY-MM-DD HH:mm 형식)
|
||||||
|
*/
|
||||||
|
export const formatDateTime = (date, options = {}) => {
|
||||||
|
if (!date) return ''
|
||||||
|
|
||||||
|
const targetDate = new Date(date)
|
||||||
|
const defaultOptions = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetDate.toLocaleString('ko-KR', { ...defaultOptions, ...options })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시간 포맷 (HH:mm 형식)
|
||||||
|
*/
|
||||||
|
export const formatTime = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
|
||||||
|
const targetDate = new Date(date)
|
||||||
|
return targetDate.toLocaleTimeString('ko-KR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 크기 포맷
|
||||||
|
*/
|
||||||
|
export const formatFileSize = (bytes) => {
|
||||||
|
if (!bytes) return '0 B'
|
||||||
|
|
||||||
const k = 1024
|
const k = 1024
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 전화번호를 포맷팅
|
* 전화번호 포맷
|
||||||
* @param {string} phoneNumber - 전화번호 문자열
|
|
||||||
* @returns {string} 포맷팅된 전화번호
|
|
||||||
*/
|
*/
|
||||||
export const formatPhoneNumber = (phoneNumber) => {
|
export const formatPhoneNumber = (phone) => {
|
||||||
if (!phoneNumber) return ''
|
if (!phone) return ''
|
||||||
|
|
||||||
// 숫자만 추출
|
const cleaned = phone.replace(/\D/g, '')
|
||||||
const numbers = phoneNumber.replace(/\D/g, '')
|
const match = cleaned.match(/^(\d{3})(\d{4})(\d{4})$/)
|
||||||
|
|
||||||
// 휴대폰 번호 (010-xxxx-xxxx)
|
if (match) {
|
||||||
if (numbers.length === 11 && numbers.startsWith('010')) {
|
return `${match[1]}-${match[2]}-${match[3]}`
|
||||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일반 전화번호 (02-xxx-xxxx 또는 03x-xxx-xxxx)
|
return phone
|
||||||
if (numbers.length === 10) {
|
|
||||||
if (numbers.startsWith('02')) {
|
|
||||||
return `${numbers.slice(0, 2)}-${numbers.slice(2, 6)}-${numbers.slice(6)}`
|
|
||||||
} else {
|
|
||||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 6)}-${numbers.slice(6)}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numbers.length === 11 && !numbers.startsWith('010')) {
|
|
||||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return phoneNumber
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사업자등록번호를 포맷팅
|
* 사업자등록번호 포맷
|
||||||
* @param {string} businessNumber - 사업자등록번호
|
|
||||||
* @returns {string} 포맷팅된 사업자등록번호
|
|
||||||
*/
|
*/
|
||||||
export const formatBusinessNumber = (businessNumber) => {
|
export const formatBusinessNumber = (number) => {
|
||||||
if (!businessNumber) return ''
|
if (!number) return ''
|
||||||
|
|
||||||
const numbers = businessNumber.replace(/\D/g, '')
|
const cleaned = number.replace(/\D/g, '')
|
||||||
|
const match = cleaned.match(/^(\d{3})(\d{2})(\d{5})$/)
|
||||||
|
|
||||||
if (numbers.length === 10) {
|
if (match) {
|
||||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 5)}-${numbers.slice(5)}`
|
return `${match[1]}-${match[2]}-${match[3]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return businessNumber
|
return number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 텍스트를 지정된 길이로 자르고 말줄임표 추가
|
* 해시태그 포맷 (#포함하여 문자열로 변환)
|
||||||
* @param {string} text - 자를 텍스트
|
|
||||||
* @param {number} maxLength - 최대 길이
|
|
||||||
* @param {string} suffix - 말줄임표 (기본값: '...')
|
|
||||||
* @returns {string} 잘린 텍스트
|
|
||||||
*/
|
*/
|
||||||
export const truncateText = (text, maxLength, suffix = '...') => {
|
export const formatHashtags = (hashtags) => {
|
||||||
if (!text) return ''
|
if (!hashtags || !Array.isArray(hashtags)) return ''
|
||||||
|
|
||||||
if (text.length <= maxLength) {
|
return hashtags.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`)).join(' ')
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.slice(0, maxLength - suffix.length) + suffix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 해시태그 배열을 문자열로 포맷팅
|
* 콘텐츠 미리보기 포맷 (길이 제한)
|
||||||
* @param {Array} hashtags - 해시태그 배열
|
|
||||||
* @param {string} separator - 구분자 (기본값: ' ')
|
|
||||||
* @returns {string} 포맷팅된 해시태그 문자열
|
|
||||||
*/
|
*/
|
||||||
export const formatHashtags = (hashtags, separator = ' ') => {
|
export const formatContentPreview = (content, maxLength = 100) => {
|
||||||
if (!Array.isArray(hashtags) || hashtags.length === 0) {
|
if (!content) return ''
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return hashtags
|
if (content.length <= maxLength) return content
|
||||||
.filter(tag => tag && tag.trim())
|
|
||||||
.map(tag => tag.startsWith('#') ? tag : `#${tag}`)
|
return content.substring(0, maxLength) + '...'
|
||||||
.join(separator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 주소를 간단한 형태로 포맷팅
|
* 소셜미디어 URL 포맷
|
||||||
* @param {string} fullAddress - 전체 주소
|
|
||||||
* @param {boolean} showDetail - 상세 주소 표시 여부 (기본값: false)
|
|
||||||
* @returns {string} 포맷팅된 주소
|
|
||||||
*/
|
*/
|
||||||
export const formatAddress = (fullAddress, showDetail = false) => {
|
export const formatSocialUrl = (username, platform) => {
|
||||||
if (!fullAddress) return ''
|
if (!username) return ''
|
||||||
|
|
||||||
const parts = fullAddress.split(' ')
|
const urls = {
|
||||||
|
instagram: `https://instagram.com/${username}`,
|
||||||
if (showDetail) {
|
facebook: `https://facebook.com/${username}`,
|
||||||
return fullAddress
|
twitter: `https://twitter.com/${username}`,
|
||||||
|
youtube: `https://youtube.com/@${username}`,
|
||||||
|
naver_blog: `https://blog.naver.com/${username}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 시/도, 구/군만 표시
|
return urls[platform] || username
|
||||||
if (parts.length >= 2) {
|
|
||||||
return `${parts[0]} ${parts[1]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullAddress
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배열을 문자열로 포맷팅
|
* 성과 지표 포맷 (K, M 단위)
|
||||||
* @param {Array} array - 포맷팅할 배열
|
|
||||||
* @param {string} separator - 구분자 (기본값: ', ')
|
|
||||||
* @param {number} maxItems - 최대 표시 항목 수
|
|
||||||
* @returns {string} 포맷팅된 문자열
|
|
||||||
*/
|
*/
|
||||||
export const formatArrayToString = (array, separator = ', ', maxItems = null) => {
|
export const formatMetric = (number) => {
|
||||||
if (!Array.isArray(array) || array.length === 0) {
|
if (!number && number !== 0) return '0'
|
||||||
return ''
|
|
||||||
|
if (number >= 1000000) {
|
||||||
|
return `${(number / 1000000).toFixed(1)}M`
|
||||||
|
}
|
||||||
|
if (number >= 1000) {
|
||||||
|
return `${(number / 1000).toFixed(1)}K`
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = maxItems ? array.slice(0, maxItems) : array
|
return formatNumber(number)
|
||||||
const result = items.join(separator)
|
}
|
||||||
|
|
||||||
if (maxItems && array.length > maxItems) {
|
/**
|
||||||
const remaining = array.length - maxItems
|
* 진행률 포맷
|
||||||
return `${result} 외 ${remaining}개`
|
*/
|
||||||
}
|
export const formatProgress = (current, total) => {
|
||||||
|
if (!total || total === 0) return '0%'
|
||||||
return result
|
|
||||||
|
const percentage = (current / total) * 100
|
||||||
|
return `${Math.round(percentage)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이메일 마스킹 포맷
|
||||||
|
*/
|
||||||
|
export const formatEmailMask = (email) => {
|
||||||
|
if (!email) return ''
|
||||||
|
|
||||||
|
const [username, domain] = email.split('@')
|
||||||
|
if (!domain) return email
|
||||||
|
|
||||||
|
const maskedUsername =
|
||||||
|
username.length > 2 ? username.substring(0, 2) + '*'.repeat(username.length - 2) : username
|
||||||
|
|
||||||
|
return `${maskedUsername}@${domain}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주소 간단 포맷
|
||||||
|
*/
|
||||||
|
export const formatAddressShort = (address) => {
|
||||||
|
if (!address) return ''
|
||||||
|
|
||||||
|
// 상세주소 제거하고 주요 정보만 표시
|
||||||
|
const parts = address.split(' ')
|
||||||
|
if (parts.length > 3) {
|
||||||
|
return parts.slice(0, 3).join(' ') + '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 평점 포맷 (별점)
|
||||||
|
*/
|
||||||
|
export const formatRating = (rating, maxRating = 5) => {
|
||||||
|
if (!rating && rating !== 0) return '☆'.repeat(maxRating)
|
||||||
|
|
||||||
|
const fullStars = Math.floor(rating)
|
||||||
|
const hasHalfStar = rating % 1 >= 0.5
|
||||||
|
const emptyStars = maxRating - fullStars - (hasHalfStar ? 1 : 0)
|
||||||
|
|
||||||
|
return '★'.repeat(fullStars) + (hasHalfStar ? '☆' : '') + '☆'.repeat(emptyStars)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 운영시간 포맷
|
||||||
|
*/
|
||||||
|
export const formatBusinessHours = (openTime, closeTime) => {
|
||||||
|
if (!openTime || !closeTime) return '운영시간 미정'
|
||||||
|
|
||||||
|
return `${formatTime(openTime)} - ${formatTime(closeTime)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 나이 계산 및 포맷
|
||||||
|
*/
|
||||||
|
export const formatAge = (birthDate) => {
|
||||||
|
if (!birthDate) return ''
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const birth = new Date(birthDate)
|
||||||
|
let age = today.getFullYear() - birth.getFullYear()
|
||||||
|
|
||||||
|
const monthDiff = today.getMonth() - birth.getMonth()
|
||||||
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
||||||
|
age--
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${age}세`
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,4 @@
|
|||||||
|
//* src/views/DashboardView.vue
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
<!-- 환영 메시지 -->
|
<!-- 환영 메시지 -->
|
||||||
@ -8,17 +9,11 @@
|
|||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<h2 class="text-h5 text-white font-weight-bold mb-2">
|
<h2 class="text-h5 text-white font-weight-bold mb-2">
|
||||||
안녕하세요, {{ authStore.user?.nickname }}님! 👋
|
안녕하세요, {{ authStore.user?.name || '사용자' }}님! 👋
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-white opacity-90 mb-0">
|
<p class="text-white opacity-90 mb-0">오늘도 성공적인 마케팅을 위해 함께해요</p>
|
||||||
오늘도 성공적인 마케팅을 위해 함께해요
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<v-img
|
<v-img src="/images/ai-character.png" max-width="80" class="ml-4" />
|
||||||
src="/images/ai-character.png"
|
|
||||||
max-width="80"
|
|
||||||
class="ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@ -27,30 +22,32 @@
|
|||||||
|
|
||||||
<!-- 주요 지표 카드 -->
|
<!-- 주요 지표 카드 -->
|
||||||
<v-row class="mb-4">
|
<v-row class="mb-4">
|
||||||
<v-col
|
<v-col v-for="metric in dashboardMetrics" :key="metric.title" cols="6" md="3">
|
||||||
v-for="metric in dashboardMetrics"
|
|
||||||
:key="metric.title"
|
|
||||||
cols="6"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<v-card elevation="2" class="h-100">
|
<v-card elevation="2" class="h-100">
|
||||||
<v-card-text class="pa-4">
|
<v-card-text class="pa-4">
|
||||||
<div class="d-flex align-center justify-space-between mb-2">
|
<div class="d-flex align-center justify-space-between mb-2">
|
||||||
<v-icon :color="metric.color" size="24">{{ metric.icon }}</v-icon>
|
<v-icon :color="metric.color" size="24">{{ metric.icon }}</v-icon>
|
||||||
<v-chip
|
<v-chip
|
||||||
:color="metric.trend === 'up' ? 'success' : metric.trend === 'down' ? 'error' : 'warning'"
|
:color="
|
||||||
|
metric.trend === 'up' ? 'success' : metric.trend === 'down' ? 'error' : 'warning'
|
||||||
|
"
|
||||||
size="small"
|
size="small"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
>
|
>
|
||||||
<v-icon size="16" class="mr-1">
|
<v-icon size="16" class="mr-1">
|
||||||
{{ metric.trend === 'up' ? 'mdi-trending-up' :
|
{{
|
||||||
metric.trend === 'down' ? 'mdi-trending-down' : 'mdi-minus' }}
|
metric.trend === 'up'
|
||||||
|
? 'mdi-trending-up'
|
||||||
|
: metric.trend === 'down'
|
||||||
|
? 'mdi-trending-down'
|
||||||
|
: 'mdi-minus'
|
||||||
|
}}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
{{ metric.change }}
|
{{ metric.change }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-caption text-grey mb-1">{{ metric.title }}</p>
|
<h3 class="text-h6 font-weight-bold mb-1">{{ metric.value }}</h3>
|
||||||
<h3 class="text-h6 font-weight-bold">{{ metric.value }}</h3>
|
<p class="text-body-2 text-grey mb-0">{{ metric.title }}</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@ -60,30 +57,26 @@
|
|||||||
<v-row class="mb-4">
|
<v-row class="mb-4">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card elevation="2">
|
<v-card elevation="2">
|
||||||
<v-card-title class="pa-4">
|
<v-card-title class="pb-2">빠른 액션</v-card-title>
|
||||||
<v-icon class="mr-2" color="primary">mdi-flash</v-icon>
|
<v-card-text>
|
||||||
빠른 액션
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text class="pa-4">
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col v-for="action in quickActions" :key="action.title" cols="6" md="3">
|
||||||
v-for="action in quickActions"
|
<v-card
|
||||||
:key="action.title"
|
|
||||||
cols="6"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
block
|
|
||||||
size="large"
|
|
||||||
:color="action.color"
|
:color="action.color"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
class="pa-4 flex-column"
|
class="text-center"
|
||||||
style="height: 80px;"
|
|
||||||
@click="action.action"
|
@click="action.action"
|
||||||
|
style="cursor: pointer"
|
||||||
>
|
>
|
||||||
<v-icon size="28" class="mb-1">{{ action.icon }}</v-icon>
|
<v-card-text class="pa-4">
|
||||||
<span class="text-caption">{{ action.title }}</span>
|
<v-icon :color="action.color" size="32" class="mb-2">
|
||||||
</v-btn>
|
{{ action.icon }}
|
||||||
|
</v-icon>
|
||||||
|
<div class="text-body-2 font-weight-medium">
|
||||||
|
{{ action.title }}
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@ -91,150 +84,67 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- AI 추천 & 매출 차트 -->
|
<!-- AI 추천 -->
|
||||||
<v-row class="mb-4">
|
<v-row class="mb-4">
|
||||||
<!-- AI 마케팅 추천 -->
|
<v-col cols="12" md="8">
|
||||||
<v-col cols="12" md="6">
|
<v-card elevation="2">
|
||||||
<v-card elevation="2" class="h-100">
|
<v-card-title class="d-flex align-center">
|
||||||
<v-card-title class="pa-4">
|
<v-icon color="primary" class="mr-2">mdi-robot</v-icon>
|
||||||
<v-icon class="mr-2" color="purple">mdi-robot</v-icon>
|
|
||||||
AI 마케팅 추천
|
AI 마케팅 추천
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn
|
<v-btn icon size="small" @click="refreshAIRecommendations" :loading="aiLoading">
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
@click="refreshAIRecommendations"
|
|
||||||
:loading="aiLoading"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-refresh</v-icon>
|
<v-icon>mdi-refresh</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text class="pa-4">
|
<v-card-text>
|
||||||
<div v-if="aiRecommendations.length > 0">
|
<div v-if="aiRecommendations.length > 0">
|
||||||
<div
|
<v-alert
|
||||||
v-for="(recommendation, index) in aiRecommendations"
|
v-for="recommendation in aiRecommendations"
|
||||||
:key="index"
|
:key="recommendation.title"
|
||||||
class="mb-4 last:mb-0"
|
:type="recommendation.type"
|
||||||
|
:icon="recommendation.icon"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<v-alert
|
<v-alert-title>{{ recommendation.title }}</v-alert-title>
|
||||||
:type="recommendation.type"
|
{{ recommendation.content }}
|
||||||
variant="tonal"
|
</v-alert>
|
||||||
class="mb-2"
|
|
||||||
>
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-icon class="mr-2">{{ recommendation.icon }}</v-icon>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<h4 class="text-subtitle-2 font-weight-bold mb-1">
|
|
||||||
{{ recommendation.title }}
|
|
||||||
</h4>
|
|
||||||
<p class="text-body-2 mb-0">{{ recommendation.content }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center pa-4">
|
<div v-else class="text-center pa-4">
|
||||||
<v-icon size="48" color="grey-lighten-2">mdi-robot-outline</v-icon>
|
<v-icon size="48" color="grey-lighten-2">mdi-robot</v-icon>
|
||||||
<p class="text-grey mt-2">AI 추천을 가져오는 중...</p>
|
<p class="text-grey mt-2">AI 추천을 불러오는 중...</p>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<!-- 매출 차트 -->
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-card elevation="2" class="h-100">
|
|
||||||
<v-card-title class="pa-4">
|
|
||||||
<v-icon class="mr-2" color="success">mdi-chart-line</v-icon>
|
|
||||||
매출 현황
|
|
||||||
<v-spacer />
|
|
||||||
<v-select
|
|
||||||
v-model="chartPeriod"
|
|
||||||
:items="chartPeriods"
|
|
||||||
item-title="text"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
style="max-width: 120px;"
|
|
||||||
@update:model-value="updateChart"
|
|
||||||
/>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text class="pa-4">
|
|
||||||
<div class="chart-container" style="height: 200px;">
|
|
||||||
<canvas ref="salesChart" />
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 최근 활동 & 콘텐츠 성과 -->
|
|
||||||
<v-row>
|
|
||||||
<!-- 최근 활동 -->
|
<!-- 최근 활동 -->
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="4">
|
||||||
<v-card elevation="2" class="h-100">
|
<v-card elevation="2">
|
||||||
<v-card-title class="pa-4">
|
<v-card-title>최근 활동</v-card-title>
|
||||||
<v-icon class="mr-2" color="info">mdi-history</v-icon>
|
<v-card-text>
|
||||||
최근 활동
|
<div v-if="recentActivities.length > 0">
|
||||||
</v-card-title>
|
<div
|
||||||
<v-card-text class="pa-4">
|
|
||||||
<v-list density="compact">
|
|
||||||
<v-list-item
|
|
||||||
v-for="activity in recentActivities"
|
v-for="activity in recentActivities"
|
||||||
:key="activity.id"
|
:key="activity.id"
|
||||||
class="px-0"
|
class="d-flex align-center mb-3"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<v-avatar :color="activity.color" size="32" class="mr-3">
|
||||||
<v-avatar :color="activity.color" size="32">
|
<v-icon color="white" size="16">{{ activity.icon }}</v-icon>
|
||||||
<v-icon color="white" size="16">{{ activity.icon }}</v-icon>
|
</v-avatar>
|
||||||
</v-avatar>
|
<div class="flex-grow-1">
|
||||||
</template>
|
<div class="text-body-2 font-weight-medium">
|
||||||
<v-list-item-title class="text-body-2">
|
{{ activity.title }}
|
||||||
{{ activity.title }}
|
</div>
|
||||||
</v-list-item-title>
|
<div class="text-caption text-grey">
|
||||||
<v-list-item-subtitle class="text-caption">
|
{{ formatRelativeTime(activity.timestamp) }}
|
||||||
{{ formatRelativeTime(activity.timestamp) }}
|
</div>
|
||||||
</v-list-item-subtitle>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- 콘텐츠 성과 -->
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-card elevation="2" class="h-100">
|
|
||||||
<v-card-title class="pa-4">
|
|
||||||
<v-icon class="mr-2" color="orange">mdi-chart-donut</v-icon>
|
|
||||||
콘텐츠 성과
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text class="pa-4">
|
|
||||||
<div v-if="contentPerformance.length > 0">
|
|
||||||
<div
|
|
||||||
v-for="content in contentPerformance"
|
|
||||||
:key="content.id"
|
|
||||||
class="d-flex align-center justify-space-between py-2"
|
|
||||||
>
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-chip
|
|
||||||
:color="content.platform === 'instagram' ? 'purple' : 'blue'"
|
|
||||||
size="small"
|
|
||||||
class="mr-2"
|
|
||||||
>
|
|
||||||
{{ content.platform }}
|
|
||||||
</v-chip>
|
|
||||||
<span class="text-body-2">{{ content.title }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="text-caption text-grey">조회수</div>
|
|
||||||
<div class="font-weight-bold">{{ formatNumber(content.views) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center pa-4">
|
<div v-else class="text-center pa-4">
|
||||||
<v-icon size="48" color="grey-lighten-2">mdi-chart-donut</v-icon>
|
<v-icon size="48" color="grey-lighten-2">mdi-history</v-icon>
|
||||||
<p class="text-grey mt-2">콘텐츠 성과 데이터가 없습니다</p>
|
<p class="text-grey mt-2">최근 활동이 없습니다</p>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@ -243,30 +153,29 @@
|
|||||||
|
|
||||||
<!-- 로딩 오버레이 -->
|
<!-- 로딩 오버레이 -->
|
||||||
<v-overlay v-if="loading" class="align-center justify-center">
|
<v-overlay v-if="loading" class="align-center justify-center">
|
||||||
<v-progress-circular
|
<v-progress-circular color="primary" indeterminate size="64" />
|
||||||
color="primary"
|
|
||||||
indeterminate
|
|
||||||
size="64"
|
|
||||||
/>
|
|
||||||
</v-overlay>
|
</v-overlay>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore, useStoreStore } from '@/store/index'
|
import { useAuthStore } from '@/store/auth' // 수정된 import
|
||||||
|
import { useAppStore } from '@/store/app' // 추가된 import
|
||||||
import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/formatters'
|
import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/formatters'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 대시보드 메인 페이지
|
||||||
|
*/
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const storeStore = useStoreStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
// 반응형 데이터
|
// 반응형 데이터
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const aiLoading = ref(false)
|
const aiLoading = ref(false)
|
||||||
const chartPeriod = ref('week')
|
|
||||||
const salesChart = ref(null)
|
|
||||||
|
|
||||||
// 대시보드 지표
|
// 대시보드 지표
|
||||||
const dashboardMetrics = ref([
|
const dashboardMetrics = ref([
|
||||||
@ -276,7 +185,7 @@ const dashboardMetrics = ref([
|
|||||||
change: '+12%',
|
change: '+12%',
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
icon: 'mdi-cash',
|
icon: 'mdi-cash',
|
||||||
color: 'success'
|
color: 'success',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '월 매출',
|
title: '월 매출',
|
||||||
@ -284,7 +193,7 @@ const dashboardMetrics = ref([
|
|||||||
change: '+8%',
|
change: '+8%',
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
icon: 'mdi-chart-line',
|
icon: 'mdi-chart-line',
|
||||||
color: 'primary'
|
color: 'primary',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '콘텐츠 수',
|
title: '콘텐츠 수',
|
||||||
@ -292,7 +201,7 @@ const dashboardMetrics = ref([
|
|||||||
change: '+3',
|
change: '+3',
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
icon: 'mdi-file-document',
|
icon: 'mdi-file-document',
|
||||||
color: 'info'
|
color: 'info',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '조회수',
|
title: '조회수',
|
||||||
@ -300,8 +209,8 @@ const dashboardMetrics = ref([
|
|||||||
change: '+25%',
|
change: '+25%',
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
icon: 'mdi-eye',
|
icon: 'mdi-eye',
|
||||||
color: 'warning'
|
color: 'warning',
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// 빠른 액션
|
// 빠른 액션
|
||||||
@ -310,26 +219,26 @@ const quickActions = ref([
|
|||||||
title: 'SNS 콘텐츠',
|
title: 'SNS 콘텐츠',
|
||||||
icon: 'mdi-plus-circle',
|
icon: 'mdi-plus-circle',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
action: () => router.push('/content/create?type=sns')
|
action: () => router.push('/content/create?type=sns'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '포스터 생성',
|
title: '포스터 생성',
|
||||||
icon: 'mdi-image-plus',
|
icon: 'mdi-image-plus',
|
||||||
color: 'secondary',
|
color: 'secondary',
|
||||||
action: () => router.push('/content/create?type=poster')
|
action: () => router.push('/content/create?type=poster'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '메뉴 등록',
|
title: '메뉴 등록',
|
||||||
icon: 'mdi-food-apple',
|
icon: 'mdi-food-apple',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
action: () => router.push('/menu')
|
action: () => router.push('/menu'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '매출 분석',
|
title: '매출 분석',
|
||||||
icon: 'mdi-chart-bar',
|
icon: 'mdi-chart-bar',
|
||||||
color: 'info',
|
color: 'info',
|
||||||
action: () => router.push('/sales')
|
action: () => router.push('/sales'),
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// AI 추천
|
// AI 추천
|
||||||
@ -338,27 +247,20 @@ const aiRecommendations = ref([
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
icon: 'mdi-weather-rainy',
|
icon: 'mdi-weather-rainy',
|
||||||
title: '날씨 기반 추천',
|
title: '날씨 기반 추천',
|
||||||
content: '오늘은 비가 와서 따뜻한 음식이 인기 있을 것 같아요. 국물 요리를 추천해보세요!'
|
content: '오늘은 비가 와서 따뜻한 음식이 인기 있을 것 같아요. 국물 요리를 추천해보세요!',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'success',
|
type: 'success',
|
||||||
icon: 'mdi-trending-up',
|
icon: 'mdi-trending-up',
|
||||||
title: '트렌드 알림',
|
title: '트렌드 알림',
|
||||||
content: '최근 #떡볶이챌린지가 인기입니다. 관련 콘텐츠를 만들어보세요.'
|
content: '최근 #떡볶이챌린지가 인기입니다. 관련 콘텐츠를 만들어보세요.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
icon: 'mdi-clock-outline',
|
icon: 'mdi-clock-outline',
|
||||||
title: '시간대 팁',
|
title: '시간대 팁',
|
||||||
content: '점심시간(12-14시)에 주문이 집중됩니다. 미리 준비하세요.'
|
content: '점심시간(12-14시)에 주문이 집중됩니다. 미리 준비하세요.',
|
||||||
}
|
},
|
||||||
])
|
|
||||||
|
|
||||||
// 차트 기간 옵션
|
|
||||||
const chartPeriods = ref([
|
|
||||||
{ text: '일주일', value: 'week' },
|
|
||||||
{ text: '한달', value: 'month' },
|
|
||||||
{ text: '3개월', value: 'quarter' }
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// 최근 활동
|
// 최근 활동
|
||||||
@ -368,44 +270,22 @@ const recentActivities = ref([
|
|||||||
title: 'SNS 콘텐츠 "떡볶이 신메뉴 출시" 발행',
|
title: 'SNS 콘텐츠 "떡볶이 신메뉴 출시" 발행',
|
||||||
timestamp: new Date(Date.now() - 1000 * 60 * 30),
|
timestamp: new Date(Date.now() - 1000 * 60 * 30),
|
||||||
icon: 'mdi-instagram',
|
icon: 'mdi-instagram',
|
||||||
color: 'purple'
|
color: 'purple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: '메뉴 "치즈떡볶이" 등록',
|
title: '메뉴 "치즈떡볶이" 등록',
|
||||||
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
||||||
icon: 'mdi-food',
|
icon: 'mdi-food',
|
||||||
color: 'orange'
|
color: 'orange',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: '매장 정보 업데이트',
|
title: '매장 정보 업데이트',
|
||||||
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5),
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5),
|
||||||
icon: 'mdi-store',
|
icon: 'mdi-store',
|
||||||
color: 'blue'
|
color: 'blue',
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 콘텐츠 성과
|
|
||||||
const contentPerformance = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: '떡볶이 신메뉴 홍보',
|
|
||||||
platform: 'instagram',
|
|
||||||
views: 1240
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '매장 소개 포스터',
|
|
||||||
platform: 'blog',
|
|
||||||
views: 850
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: '할인 이벤트 안내',
|
|
||||||
platform: 'instagram',
|
|
||||||
views: 2100
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// 메서드
|
// 메서드
|
||||||
@ -413,44 +293,31 @@ const refreshAIRecommendations = async () => {
|
|||||||
try {
|
try {
|
||||||
aiLoading.value = true
|
aiLoading.value = true
|
||||||
// API 호출 시뮬레이션
|
// API 호출 시뮬레이션
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
// AI 추천 데이터 갱신 로직
|
appStore.showSnackbar('AI 추천이 갱신되었습니다', 'success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI 추천 갱신 실패:', error)
|
console.error('AI 추천 갱신 실패:', error)
|
||||||
|
appStore.showSnackbar('AI 추천 갱신에 실패했습니다', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
aiLoading.value = false
|
aiLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateChart = () => {
|
|
||||||
// 차트 업데이트 로직
|
|
||||||
console.log('차트 업데이트:', chartPeriod.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initChart = () => {
|
|
||||||
// Chart.js를 사용한 차트 초기화 로직
|
|
||||||
// 실제 구현에서는 Chart.js 라이브러리 사용
|
|
||||||
console.log('차트 초기화')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 라이프사이클
|
// 라이프사이클
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
console.log('DashboardView 마운트됨')
|
||||||
|
console.log('사용자 정보:', authStore.user)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// 대시보드 데이터 로드
|
// 대시보드 데이터 로드 시뮬레이션
|
||||||
await Promise.all([
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
// 매장 정보 로드 (필요시)
|
|
||||||
// storeStore.fetchStoreInfo(),
|
|
||||||
// AI 추천 로드
|
|
||||||
// refreshAIRecommendations()
|
|
||||||
])
|
|
||||||
|
|
||||||
// 차트 초기화
|
console.log('대시보드 데이터 로드 완료')
|
||||||
await nextTick()
|
|
||||||
initChart()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('대시보드 로드 실패:', error)
|
console.error('대시보드 로드 실패:', error)
|
||||||
|
appStore.showSnackbar('대시보드를 불러오는데 실패했습니다', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -459,17 +326,13 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bg-gradient-primary {
|
.bg-gradient-primary {
|
||||||
background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%);
|
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-100 {
|
.h-100 {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.text-h5 {
|
.text-h5 {
|
||||||
font-size: 1.3rem !important;
|
font-size: 1.3rem !important;
|
||||||
|
|||||||
@ -1,36 +1,22 @@
|
|||||||
|
//* src/views/LoginView.vue
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid class="fill-height pa-0">
|
<v-container fluid class="fill-height pa-0">
|
||||||
<v-row no-gutters class="fill-height">
|
<v-row no-gutters class="fill-height">
|
||||||
<v-col cols="12" class="d-flex flex-column">
|
<v-col cols="12" class="d-flex flex-column">
|
||||||
<!-- 로고 및 헤더 -->
|
<!-- 로고 및 헤더 -->
|
||||||
<div class="text-center pa-8 bg-primary">
|
<div class="text-center pa-8 bg-primary">
|
||||||
<v-img
|
<v-img src="/images/logo.png" alt="AI 마케팅 로고" max-width="80" class="mx-auto mb-4" />
|
||||||
src="/images/logo.png"
|
<h1 class="text-h4 text-white font-weight-bold mb-2">AI 마케팅</h1>
|
||||||
alt="AI 마케팅 로고"
|
<p class="text-white opacity-90">소상공인을 위한 똑똑한 마케팅 솔루션</p>
|
||||||
max-width="80"
|
|
||||||
class="mx-auto mb-4"
|
|
||||||
/>
|
|
||||||
<h1 class="text-h4 text-white font-weight-bold mb-2">
|
|
||||||
AI 마케팅
|
|
||||||
</h1>
|
|
||||||
<p class="text-white opacity-90">
|
|
||||||
소상공인을 위한 똑똑한 마케팅 솔루션
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 로그인 폼 -->
|
<!-- 로그인 폼 -->
|
||||||
<div class="flex-grow-1 pa-6">
|
<div class="flex-grow-1 pa-6">
|
||||||
<v-card class="mx-auto" max-width="400" elevation="8">
|
<v-card class="mx-auto" max-width="400" elevation="8">
|
||||||
<v-card-title class="text-h5 text-center pa-6">
|
<v-card-title class="text-h5 text-center pa-6"> 로그인 </v-card-title>
|
||||||
로그인
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<v-card-text class="pa-6">
|
<v-card-text class="pa-6">
|
||||||
<v-form
|
<v-form ref="loginForm" v-model="formValid" @submit.prevent="handleLogin">
|
||||||
ref="loginForm"
|
|
||||||
v-model="formValid"
|
|
||||||
@submit.prevent="login"
|
|
||||||
>
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="loginData.userId"
|
v-model="loginData.userId"
|
||||||
label="아이디"
|
label="아이디"
|
||||||
@ -51,7 +37,7 @@
|
|||||||
:rules="passwordRules"
|
:rules="passwordRules"
|
||||||
required
|
required
|
||||||
@click:append-inner="showPassword = !showPassword"
|
@click:append-inner="showPassword = !showPassword"
|
||||||
@keyup.enter="login"
|
@keyup.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
@ -66,12 +52,19 @@
|
|||||||
로그인
|
로그인
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- 테스트용 기본값 설정 버튼 -->
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
color="secondary"
|
||||||
|
@click="setTestCredentials"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
테스트 계정으로 입력
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<v-btn
|
<v-btn variant="text" color="primary" @click="showRegisterDialog = true">
|
||||||
variant="text"
|
|
||||||
color="primary"
|
|
||||||
@click="showRegisterDialog = true"
|
|
||||||
>
|
|
||||||
회원가입
|
회원가입
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -93,10 +86,7 @@
|
|||||||
<v-card-title class="text-h6">
|
<v-card-title class="text-h6">
|
||||||
회원가입
|
회원가입
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn
|
<v-btn icon @click="showRegisterDialog = false">
|
||||||
icon
|
|
||||||
@click="showRegisterDialog = false"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-close</v-icon>
|
<v-icon>mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
@ -119,7 +109,7 @@
|
|||||||
label="비밀번호"
|
label="비밀번호"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="password"
|
type="password"
|
||||||
:rules="passwordRules"
|
:rules="registerPasswordRules"
|
||||||
required
|
required
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
@ -167,13 +157,7 @@
|
|||||||
|
|
||||||
<v-card-actions class="pa-6">
|
<v-card-actions class="pa-6">
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn
|
<v-btn color="grey" variant="text" @click="showRegisterDialog = false"> 취소 </v-btn>
|
||||||
color="grey"
|
|
||||||
variant="text"
|
|
||||||
@click="showRegisterDialog = false"
|
|
||||||
>
|
|
||||||
취소
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="registerLoading"
|
:loading="registerLoading"
|
||||||
@ -185,26 +169,23 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<!-- 스낵바 -->
|
|
||||||
<v-snackbar
|
|
||||||
v-model="snackbar.show"
|
|
||||||
:color="snackbar.color"
|
|
||||||
:timeout="3000"
|
|
||||||
location="top"
|
|
||||||
>
|
|
||||||
{{ snackbar.message }}
|
|
||||||
</v-snackbar>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/store/index'
|
import { useAuthStore } from '@/store/auth' // 수정된 import 경로
|
||||||
|
import { useAppStore } from '@/store/app' // App Store 추가
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 페이지
|
||||||
|
* 사용자 인증 및 회원가입 기능 제공
|
||||||
|
*/
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const appStore = useAppStore() // App Store 사용
|
||||||
|
|
||||||
// 반응형 데이터
|
// 반응형 데이터
|
||||||
const formValid = ref(false)
|
const formValid = ref(false)
|
||||||
@ -215,8 +196,8 @@ const showPassword = ref(false)
|
|||||||
const showRegisterDialog = ref(false)
|
const showRegisterDialog = ref(false)
|
||||||
|
|
||||||
const loginData = reactive({
|
const loginData = reactive({
|
||||||
userId: '',
|
userId: 'user01', // 테스트용 기본값
|
||||||
password: ''
|
password: 'passw0rd', // 테스트용 기본값
|
||||||
})
|
})
|
||||||
|
|
||||||
const registerData = reactive({
|
const registerData = reactive({
|
||||||
@ -225,69 +206,87 @@ const registerData = reactive({
|
|||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
businessNumber: '',
|
businessNumber: '',
|
||||||
email: ''
|
email: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const snackbar = reactive({
|
// 유효성 검사 규칙 (테스트를 위해 완화)
|
||||||
show: false,
|
const userIdRules = [(v) => !!v || '아이디를 입력해주세요']
|
||||||
message: '',
|
|
||||||
color: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 유효성 검사 규칙
|
const passwordRules = [(v) => !!v || '비밀번호를 입력해주세요']
|
||||||
const userIdRules = [
|
|
||||||
v => !!v || '아이디를 입력해주세요',
|
|
||||||
v => v.length >= 4 || '아이디는 4자 이상이어야 합니다'
|
|
||||||
]
|
|
||||||
|
|
||||||
const passwordRules = [
|
// 회원가입용 더 엄격한 규칙
|
||||||
v => !!v || '비밀번호를 입력해주세요',
|
const registerPasswordRules = [
|
||||||
v => v.length >= 8 || '비밀번호는 8자 이상이어야 합니다',
|
(v) => !!v || '비밀번호를 입력해주세요',
|
||||||
v => /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(v) || '영문, 숫자, 특수문자를 포함해야 합니다'
|
(v) => v.length >= 8 || '비밀번호는 8자 이상이어야 합니다',
|
||||||
|
(v) =>
|
||||||
|
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(v) ||
|
||||||
|
'영문, 숫자, 특수문자를 포함해야 합니다',
|
||||||
]
|
]
|
||||||
|
|
||||||
const confirmPasswordRules = [
|
const confirmPasswordRules = [
|
||||||
v => !!v || '비밀번호 확인을 입력해주세요',
|
(v) => !!v || '비밀번호 확인을 입력해주세요',
|
||||||
v => v === registerData.password || '비밀번호가 일치하지 않습니다'
|
(v) => v === registerData.password || '비밀번호가 일치하지 않습니다',
|
||||||
]
|
]
|
||||||
|
|
||||||
const nicknameRules = [
|
const nicknameRules = [
|
||||||
v => !!v || '닉네임을 입력해주세요',
|
(v) => !!v || '닉네임을 입력해주세요',
|
||||||
v => v.length >= 2 || '닉네임은 2자 이상이어야 합니다'
|
(v) => v.length >= 2 || '닉네임은 2자 이상이어야 합니다',
|
||||||
]
|
]
|
||||||
|
|
||||||
const businessNumberRules = [
|
const businessNumberRules = [(v) => !!v || '사업자등록번호를 입력해주세요']
|
||||||
v => !!v || '사업자등록번호를 입력해주세요',
|
|
||||||
v => /^\d{3}-\d{2}-\d{5}$/.test(v) || '올바른 사업자등록번호 형식이 아닙니다 (예: 123-45-67890)'
|
|
||||||
]
|
|
||||||
|
|
||||||
const emailRules = [
|
const emailRules = [
|
||||||
v => !!v || '이메일을 입력해주세요',
|
(v) => !!v || '이메일을 입력해주세요',
|
||||||
v => /.+@.+\..+/.test(v) || '올바른 이메일 형식이 아닙니다'
|
(v) => /.+@.+\..+/.test(v) || '올바른 이메일 형식이 아닙니다',
|
||||||
]
|
]
|
||||||
|
|
||||||
// 메서드
|
// 메서드
|
||||||
const login = async () => {
|
const handleLogin = async () => {
|
||||||
if (!formValid.value) return
|
console.log('=== 로그인 처리 시작 ===')
|
||||||
|
console.log('입력 데이터:', loginData)
|
||||||
|
|
||||||
|
if (!formValid.value) {
|
||||||
|
console.log('폼 유효성 검사 실패')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
// Auth Store의 login 메서드 호출 (수정된 방식)
|
||||||
await authStore.login(loginData)
|
const result = await authStore.login({
|
||||||
|
userId: loginData.userId,
|
||||||
|
password: loginData.password,
|
||||||
|
})
|
||||||
|
|
||||||
snackbar.show = true
|
console.log('로그인 결과:', result)
|
||||||
snackbar.message = '로그인되었습니다'
|
|
||||||
snackbar.color = 'success'
|
|
||||||
|
|
||||||
setTimeout(() => {
|
if (result.success) {
|
||||||
router.push('/dashboard')
|
appStore.showSnackbar('로그인 되었습니다', 'success')
|
||||||
}, 1000)
|
|
||||||
|
console.log('로그인 성공, 인증 상태 확인:', authStore.isAuthenticated)
|
||||||
|
|
||||||
|
// 대시보드로 이동
|
||||||
|
console.log('대시보드로 이동 시도')
|
||||||
|
await router.push('/dashboard')
|
||||||
|
console.log('라우터 이동 완료')
|
||||||
|
} else {
|
||||||
|
appStore.showSnackbar(result.message || '로그인에 실패했습니다', 'error')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
snackbar.show = true
|
console.error('로그인 에러:', error)
|
||||||
snackbar.message = error.response?.data?.message || '로그인에 실패했습니다'
|
appStore.showSnackbar('로그인 중 오류가 발생했습니다', 'error')
|
||||||
snackbar.color = 'error'
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('=== 로그인 처리 종료 ===')
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTestCredentials = () => {
|
||||||
|
loginData.userId = 'user01'
|
||||||
|
loginData.password = 'passw0rd'
|
||||||
|
appStore.showSnackbar('테스트 계정 정보가 입력되었습니다', 'info')
|
||||||
}
|
}
|
||||||
|
|
||||||
const register = async () => {
|
const register = async () => {
|
||||||
@ -295,11 +294,12 @@ const register = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
registerLoading.value = true
|
registerLoading.value = true
|
||||||
await authStore.register(registerData)
|
|
||||||
|
|
||||||
snackbar.show = true
|
// 임시 회원가입 로직 (실제 API 연동 전)
|
||||||
snackbar.message = '회원가입이 완료되었습니다'
|
console.log('회원가입 데이터:', registerData)
|
||||||
snackbar.color = 'success'
|
|
||||||
|
// 시뮬레이션: 성공
|
||||||
|
appStore.showSnackbar('회원가입이 완료되었습니다', 'success')
|
||||||
|
|
||||||
showRegisterDialog.value = false
|
showRegisterDialog.value = false
|
||||||
|
|
||||||
@ -310,16 +310,27 @@ const register = async () => {
|
|||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
businessNumber: '',
|
businessNumber: '',
|
||||||
email: ''
|
email: '',
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
snackbar.show = true
|
console.error('회원가입 에러:', error)
|
||||||
snackbar.message = error.response?.data?.message || '회원가입에 실패했습니다'
|
appStore.showSnackbar('회원가입에 실패했습니다', 'error')
|
||||||
snackbar.color = 'error'
|
|
||||||
} finally {
|
} finally {
|
||||||
registerLoading.value = false
|
registerLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 실행
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('LoginView 마운트됨')
|
||||||
|
console.log('초기 인증 상태:', authStore.isAuthenticated)
|
||||||
|
|
||||||
|
// 이미 로그인된 경우 대시보드로 리다이렉트
|
||||||
|
if (authStore.isAuthenticated) {
|
||||||
|
console.log('이미 로그인됨, 대시보드로 이동')
|
||||||
|
router.push('/dashboard')
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -328,7 +339,7 @@ const register = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%);
|
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user