release
This commit is contained in:
parent
a9005e6dc4
commit
a5aaff660c
211
src/App.vue
211
src/App.vue
@ -2,16 +2,8 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<!-- 로딩 오버레이 -->
|
||||
<v-overlay
|
||||
v-if="loading"
|
||||
class="align-center justify-center"
|
||||
persistent
|
||||
>
|
||||
<v-progress-circular
|
||||
color="primary"
|
||||
indeterminate
|
||||
size="64"
|
||||
/>
|
||||
<v-overlay v-if="loading" class="align-center justify-center" persistent>
|
||||
<v-progress-circular color="primary" indeterminate size="64" />
|
||||
</v-overlay>
|
||||
|
||||
<!-- 메인 네비게이션 -->
|
||||
@ -24,7 +16,7 @@
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
prepend-avatar="/images/logo.png"
|
||||
prepend-avatar="/images/logo192.png"
|
||||
:title="userStore.user?.nickname || '사용자'"
|
||||
:subtitle="userStore.user?.businessName || '매장명'"
|
||||
/>
|
||||
@ -45,13 +37,7 @@
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="pa-4">
|
||||
<v-btn
|
||||
block
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-logout"
|
||||
@click="logout"
|
||||
>
|
||||
<v-btn block color="primary" variant="outlined" prepend-icon="mdi-logout" @click="logout">
|
||||
로그아웃
|
||||
</v-btn>
|
||||
</div>
|
||||
@ -59,16 +45,8 @@
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- 상단 앱바 -->
|
||||
<v-app-bar
|
||||
v-if="isAuthenticated && !isLoginPage"
|
||||
app
|
||||
elevation="1"
|
||||
color="primary"
|
||||
>
|
||||
<v-app-bar-nav-icon
|
||||
@click="drawer = !drawer"
|
||||
color="white"
|
||||
/>
|
||||
<v-app-bar v-if="isAuthenticated && !isLoginPage" app elevation="1" color="primary">
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer" color="white" />
|
||||
|
||||
<v-toolbar-title class="text-white font-weight-bold">
|
||||
{{ currentPageTitle }}
|
||||
@ -77,84 +55,31 @@
|
||||
<v-spacer />
|
||||
|
||||
<!-- 알림 버튼 -->
|
||||
<v-btn
|
||||
icon
|
||||
color="white"
|
||||
@click="showNotifications = true"
|
||||
>
|
||||
<v-badge
|
||||
v-if="notificationCount > 0"
|
||||
:content="notificationCount"
|
||||
color="error"
|
||||
>
|
||||
<v-btn icon color="white" @click="showNotifications = true">
|
||||
<v-badge v-if="notificationCount > 0" :content="notificationCount" color="error">
|
||||
<v-icon>mdi-bell</v-icon>
|
||||
</v-badge>
|
||||
<v-icon v-else>mdi-bell</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<!-- 메인 컨텐츠 -->
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
|
||||
<!-- 하단 네비게이션 (모바일) -->
|
||||
<v-bottom-navigation
|
||||
v-if="isAuthenticated && !isLoginPage && $vuetify.display.mobile"
|
||||
v-model="bottomNav"
|
||||
app
|
||||
grow
|
||||
height="70"
|
||||
>
|
||||
<v-btn
|
||||
v-for="item in bottomMenuItems"
|
||||
:key="item.route"
|
||||
:to="item.route"
|
||||
:value="item.route"
|
||||
stacked
|
||||
>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
<span class="text-caption">{{ item.title }}</span>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
|
||||
<!-- 알림 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showNotifications"
|
||||
max-width="400"
|
||||
scrollable
|
||||
>
|
||||
<v-dialog v-model="showNotifications" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
알림
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
@click="showNotifications = false"
|
||||
>
|
||||
<v-card-title>
|
||||
<span class="text-h6">알림</span>
|
||||
<v-spacer />
|
||||
<v-btn icon variant="text" @click="showNotifications = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text style="max-height: 400px;">
|
||||
<v-list v-if="notifications.length > 0">
|
||||
<v-list-item
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:subtitle="notification.message"
|
||||
:title="notification.title"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="notification.type">
|
||||
{{ getNotificationIcon(notification.type) }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div v-else class="text-center pa-4">
|
||||
<v-card-text>
|
||||
<div v-if="notifications.length === 0" class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2">mdi-bell-off</v-icon>
|
||||
<p class="text-grey mt-2">새로운 알림이 없습니다</p>
|
||||
</div>
|
||||
@ -171,19 +96,14 @@
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="snackbar.show = false"
|
||||
>
|
||||
닫기
|
||||
</v-btn>
|
||||
<v-btn variant="text" @click="snackbar.show = false"> 닫기 </v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useAppStore } from '@/store/app'
|
||||
@ -195,7 +115,6 @@ const appStore = useAppStore()
|
||||
|
||||
// 반응형 데이터
|
||||
const drawer = ref(false)
|
||||
const bottomNav = ref('')
|
||||
const showNotifications = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
@ -203,106 +122,50 @@ const loading = ref(false)
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const isLoginPage = computed(() => route.name === 'Login')
|
||||
const userStore = computed(() => authStore)
|
||||
const notificationCount = computed(() => appStore.notificationCount)
|
||||
const notifications = computed(() => appStore.notifications)
|
||||
const notificationCount = computed(() => appStore.notificationCount || 0)
|
||||
const notifications = computed(() => appStore.notifications || [])
|
||||
const snackbar = computed(() => appStore.snackbar)
|
||||
|
||||
// 현재 페이지 제목
|
||||
const currentPageTitle = computed(() => {
|
||||
const titles = {
|
||||
'Dashboard': '대시보드',
|
||||
'StoreManagement': '매장 관리',
|
||||
'MenuManagement': '메뉴 관리',
|
||||
'ContentCreation': '콘텐츠 생성',
|
||||
'ContentManagement': '콘텐츠 관리',
|
||||
'AIRecommendation': 'AI 추천',
|
||||
'SalesAnalysis': '매출 분석'
|
||||
Dashboard: '대시보드',
|
||||
StoreManagement: '매장 관리',
|
||||
ContentCreation: '콘텐츠 생성',
|
||||
ContentManagement: '콘텐츠 관리',
|
||||
}
|
||||
return titles[route.name] || 'AI 마케팅'
|
||||
})
|
||||
|
||||
// 메뉴 아이템
|
||||
// 메뉴 아이템 (수정된 메뉴 구조)
|
||||
const menuItems = [
|
||||
{ title: '대시보드', icon: 'mdi-view-dashboard', route: '/dashboard' },
|
||||
{ title: '매장 관리', icon: 'mdi-store', route: '/store' },
|
||||
{ title: '메뉴 관리', icon: 'mdi-food', route: '/menu' },
|
||||
{ title: '콘텐츠 생성', icon: 'mdi-plus-circle', route: '/content/create' },
|
||||
{ title: '콘텐츠 생성', icon: 'mdi-creation', route: '/content/create' },
|
||||
{ title: '콘텐츠 관리', icon: 'mdi-folder-multiple', route: '/content' },
|
||||
{ title: 'AI 추천', icon: 'mdi-robot', route: '/ai-recommend' },
|
||||
{ title: '매출 분석', icon: 'mdi-chart-line', route: '/sales' }
|
||||
]
|
||||
|
||||
const bottomMenuItems = [
|
||||
{ title: '홈', icon: 'mdi-home', route: '/dashboard' },
|
||||
{ title: '매장', icon: 'mdi-store', route: '/store' },
|
||||
{ title: '생성', icon: 'mdi-plus-circle', route: '/content/create' },
|
||||
{ title: '분석', icon: 'mdi-chart-line', route: '/sales' }
|
||||
]
|
||||
|
||||
// 메서드
|
||||
// 로그아웃
|
||||
const logout = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
appStore.showSnackbar('로그아웃 중 오류가 발생했습니다', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
console.error('로그아웃 실패:', error)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 초기화
|
||||
onMounted(() => {
|
||||
console.log('App 컴포넌트 마운트됨')
|
||||
console.log('현재 라우트:', route.path)
|
||||
console.log('인증 상태:', authStore.isAuthenticated)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 글로벌 스타일 */
|
||||
.v-application {
|
||||
font-family: 'Noto Sans KR', sans-serif !important;
|
||||
}
|
||||
|
||||
/* 모바일 최적화 */
|
||||
@media (max-width: 600px) {
|
||||
.v-toolbar-title {
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
.v-navigation-drawer {
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 커스텀 스타일 */
|
||||
.fade-transition {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-transition {
|
||||
transition: transform 0.3s ease;
|
||||
<style scoped>
|
||||
.v-app-bar-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
54
src/main.js
54
src/main.js
@ -40,20 +40,6 @@ const vuetify = createVuetify({
|
||||
'surface-variant': '#F5F5F5',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
primary: '#2196F3',
|
||||
secondary: '#616161',
|
||||
accent: '#82B1FF',
|
||||
error: '#FF5252',
|
||||
info: '#2196F3',
|
||||
success: '#4CAF50',
|
||||
warning: '#FFC107',
|
||||
background: '#121212',
|
||||
surface: '#1E1E1E',
|
||||
'surface-variant': '#424242',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
@ -72,27 +58,6 @@ const vuetify = createVuetify({
|
||||
xl: 1920,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
VCard: {
|
||||
elevation: 2,
|
||||
rounded: 'lg',
|
||||
},
|
||||
VBtn: {
|
||||
rounded: 'lg',
|
||||
},
|
||||
VTextField: {
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
},
|
||||
VSelect: {
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
},
|
||||
VTextarea: {
|
||||
variant: 'outlined',
|
||||
density: 'comfortable',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Pinia 스토어 생성
|
||||
@ -107,12 +72,6 @@ app.config.globalProperties.$config = window.__runtime_config__ || {}
|
||||
// 에러 핸들링
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('Vue 앱 에러:', err, info)
|
||||
|
||||
// 프로덕션 환경에서 에러 리포팅
|
||||
if (import.meta.env.PROD) {
|
||||
// 에러 리포팅 서비스에 전송
|
||||
// reportError(err, instance, info)
|
||||
}
|
||||
}
|
||||
|
||||
// 플러그인 등록
|
||||
@ -123,20 +82,7 @@ app.use(vuetify)
|
||||
// 개발 모드 설정
|
||||
if (import.meta.env.DEV) {
|
||||
app.config.performance = true
|
||||
window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = window.__VUE_DEVTOOLS_GLOBAL_HOOK__ || {}
|
||||
}
|
||||
|
||||
// 앱 마운트
|
||||
app.mount('#app')
|
||||
|
||||
// 서비스 워커 등록 (프로덕션에서만)
|
||||
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', async () => {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/sw.js')
|
||||
console.log('서비스 워커 등록 성공:', registration)
|
||||
} catch (error) {
|
||||
console.log('서비스 워커 등록 실패:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,26 +4,18 @@
|
||||
* 라우팅 및 네비게이션 가드 설정
|
||||
*/
|
||||
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: (to) => {
|
||||
// 인증 상태에 따른 동적 리다이렉트
|
||||
const authStore = useAuthStore()
|
||||
return authStore.isAuthenticated ? '/dashboard' : '/login'
|
||||
},
|
||||
redirect: '/login', // 항상 로그인 페이지로 먼저 리다이렉트
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
@ -52,15 +44,6 @@ const routes = [
|
||||
title: '매장 관리',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/menu',
|
||||
name: 'MenuManagement',
|
||||
component: MenuManagementView,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '메뉴 관리',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/content/create',
|
||||
name: 'ContentCreation',
|
||||
@ -79,24 +62,6 @@ const routes = [
|
||||
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: '/login', // 404시 로그인으로 이동
|
||||
@ -108,41 +73,40 @@ const router = createRouter({
|
||||
routes,
|
||||
})
|
||||
|
||||
// 네비게이션 가드
|
||||
// 네비게이션 가드 - 수정된 버전
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
console.log('=== 라우터 가드 실행 ===')
|
||||
console.log('이동 경로:', `${from.path} → ${to.path}`)
|
||||
|
||||
// Pinia 스토어를 동적으로 가져오기 (순환 참조 방지)
|
||||
const { useAuthStore } = await import('@/store/auth')
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 인증 상태 확인
|
||||
authStore.checkAuthState()
|
||||
|
||||
console.log('현재 인증 상태:', authStore.isAuthenticated)
|
||||
console.log('토큰 존재:', !!authStore.token)
|
||||
console.log('사용자 정보:', authStore.user)
|
||||
console.log('사용자 정보:', authStore.user?.nickname)
|
||||
|
||||
// 인증이 필요한 페이지인지 확인
|
||||
if (to.meta.requiresAuth) {
|
||||
console.log('인증이 필요한 페이지')
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
|
||||
if (!authStore.isAuthenticated) {
|
||||
console.log('인증되지 않음, 로그인으로 이동')
|
||||
if (requiresAuth && !authStore.isAuthenticated) {
|
||||
console.log('인증 필요 - 로그인 페이지로 이동')
|
||||
next('/login')
|
||||
} else {
|
||||
console.log('인증됨, 페이지 이동 허용')
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
console.log('인증이 필요하지 않은 페이지')
|
||||
|
||||
// 로그인 페이지에 이미 인증된 사용자가 접근하는 경우
|
||||
if (to.name === 'Login' && authStore.isAuthenticated) {
|
||||
console.log('이미 로그인됨, 대시보드로 리다이렉트')
|
||||
} else if (to.path === '/login' && authStore.isAuthenticated) {
|
||||
console.log('이미 로그인됨 - 대시보드로 이동')
|
||||
next('/dashboard')
|
||||
} else {
|
||||
console.log('페이지 이동 허용')
|
||||
console.log('이동 허용:', to.path)
|
||||
next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('=== 라우터 가드 종료 ===')
|
||||
router.afterEach((to) => {
|
||||
// 페이지 타이틀 설정
|
||||
document.title = to.meta.title ? `${to.meta.title} - AI 마케팅` : 'AI 마케팅'
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@ -2,116 +2,154 @@
|
||||
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 token = ref(null)
|
||||
const refreshToken = ref(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const userInfo = computed(() => user.value)
|
||||
// 컴퓨티드 - 더 엄격한 인증 체크
|
||||
const isAuthenticated = computed(() => {
|
||||
return !!(token.value && user.value)
|
||||
})
|
||||
|
||||
// Actions
|
||||
// 액션
|
||||
const login = async (credentials) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
console.log('Auth Store - 로그인 시도:', credentials)
|
||||
console.log('=== AUTH STORE 로그인 시작 ===')
|
||||
console.log('받은 자격증명:', credentials)
|
||||
|
||||
// 임시 로그인 로직 (실제 API 연동 전)
|
||||
if (credentials.userId === 'user01' && credentials.password === 'passw0rd') {
|
||||
const userData = {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// 실제 API 호출 시뮬레이션
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 정확한 자격증명 확인 (대소문자 구분, 공백 제거)
|
||||
const username = credentials.username?.trim()
|
||||
const password = credentials.password?.trim()
|
||||
|
||||
console.log('정제된 자격증명:', { username, password })
|
||||
console.log('예상 자격증명:', { username: 'user01', password: 'passw0rd' })
|
||||
console.log('username 일치:', username === 'user01')
|
||||
console.log('password 일치:', password === 'passw0rd')
|
||||
|
||||
if (username === 'user01' && password === 'passw0rd') {
|
||||
const mockToken = 'mock_jwt_token_' + Date.now()
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
userId: credentials.userId,
|
||||
name: '테스트 사용자',
|
||||
email: 'test@example.com',
|
||||
username: username,
|
||||
nickname: '김사장',
|
||||
businessName: '김사장님의 분식점',
|
||||
email: 'kim@example.com',
|
||||
avatar: '/images/avatar.png',
|
||||
}
|
||||
|
||||
const tokenData = 'temp_jwt_token_' + Date.now()
|
||||
|
||||
// 상태 업데이트
|
||||
token.value = tokenData
|
||||
user.value = userData
|
||||
// 토큰과 사용자 정보 저장
|
||||
token.value = mockToken
|
||||
user.value = mockUser
|
||||
|
||||
// 로컬 스토리지에 저장
|
||||
localStorage.setItem('auth_token', tokenData)
|
||||
localStorage.setItem('user_info', JSON.stringify(userData))
|
||||
localStorage.setItem('auth_token', mockToken)
|
||||
localStorage.setItem('user_info', JSON.stringify(mockUser))
|
||||
|
||||
console.log('Auth Store - 로그인 성공:', { token: tokenData, user: userData })
|
||||
console.log('Auth Store - isAuthenticated:', isAuthenticated.value)
|
||||
|
||||
return { success: true, user: userData, token: tokenData }
|
||||
console.log('로그인 성공! 사용자:', mockUser.nickname)
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, message: '아이디 또는 비밀번호가 틀렸습니다' }
|
||||
console.log('로그인 실패 - 자격증명 불일치')
|
||||
throw new Error('아이디 또는 비밀번호가 올바르지 않습니다.')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth Store - 로그인 에러:', error)
|
||||
return { success: false, message: '로그인 중 오류가 발생했습니다' }
|
||||
console.error('로그인 실패:', error)
|
||||
return { success: false, error: error.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) {
|
||||
const logout = async () => {
|
||||
try {
|
||||
token.value = storedToken
|
||||
user.value = JSON.parse(storedUser)
|
||||
console.log('Auth Store - 저장된 인증 정보 복원:', { token: storedToken, user: user.value })
|
||||
// 로컬 상태 초기화
|
||||
user.value = null
|
||||
token.value = null
|
||||
refreshToken.value = null
|
||||
|
||||
// 로컬 스토리지 정리
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user_info')
|
||||
|
||||
console.log('로그아웃 완료')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Auth Store - 인증 정보 복원 실패:', error)
|
||||
clearAuth()
|
||||
console.error('로그아웃 실패:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
const checkAuthState = () => {
|
||||
try {
|
||||
const savedToken = localStorage.getItem('auth_token')
|
||||
const savedUser = localStorage.getItem('user_info')
|
||||
|
||||
if (savedToken && savedUser) {
|
||||
// JSON 파싱이 가능한지 확인
|
||||
const parsedUser = JSON.parse(savedUser)
|
||||
if (parsedUser && parsedUser.id) {
|
||||
token.value = savedToken
|
||||
user.value = parsedUser
|
||||
console.log('저장된 인증 정보 복원됨:', parsedUser.nickname)
|
||||
} else {
|
||||
// 잘못된 사용자 정보인 경우 정리
|
||||
clearAuthData()
|
||||
}
|
||||
} else {
|
||||
console.log('저장된 인증 정보가 없음')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('인증 상태 확인 실패:', error)
|
||||
clearAuthData()
|
||||
}
|
||||
}
|
||||
|
||||
const clearAuthData = () => {
|
||||
user.value = null
|
||||
token.value = null
|
||||
refreshToken.value = null
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user_info')
|
||||
console.log('인증 데이터 초기화됨')
|
||||
}
|
||||
|
||||
const refreshUserInfo = async () => {
|
||||
console.log('Auth Store - 사용자 정보 갱신')
|
||||
// 임시로 현재 사용자 정보 반환
|
||||
if (token.value) {
|
||||
return Promise.resolve(user.value)
|
||||
} else {
|
||||
throw new Error('토큰이 없습니다')
|
||||
}
|
||||
}
|
||||
if (!token.value) return
|
||||
|
||||
// 초기 인증 상태 체크
|
||||
checkAuth()
|
||||
try {
|
||||
// 실제 API 호출로 사용자 정보 갱신
|
||||
// const response = await api.get('/auth/me')
|
||||
// user.value = response.data
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 갱신 실패:', error)
|
||||
// 토큰이 유효하지 않은 경우 로그아웃
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
// 상태
|
||||
user,
|
||||
token,
|
||||
isLoading,
|
||||
// Getters
|
||||
|
||||
// 컴퓨티드
|
||||
isAuthenticated,
|
||||
userInfo,
|
||||
// Actions
|
||||
|
||||
// 액션
|
||||
login,
|
||||
logout,
|
||||
clearAuth,
|
||||
checkAuth,
|
||||
refreshUserInfo,
|
||||
checkAuthState,
|
||||
clearAuthData,
|
||||
}
|
||||
})
|
||||
|
||||
95
src/styles/variables.scss
Normal file
95
src/styles/variables.scss
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Sass 변수 정의
|
||||
* 전역에서 사용할 색상, 크기, 간격 등의 변수들
|
||||
*/
|
||||
|
||||
// 색상 변수
|
||||
$primary-color: #1976d2;
|
||||
$secondary-color: #424242;
|
||||
$accent-color: #82b1ff;
|
||||
$error-color: #ff5252;
|
||||
$info-color: #2196f3;
|
||||
$success-color: #4caf50;
|
||||
$warning-color: #ffc107;
|
||||
|
||||
// 배경색
|
||||
$background-color: #ffffff;
|
||||
$surface-color: #ffffff;
|
||||
$surface-variant-color: #f5f5f5;
|
||||
|
||||
// 텍스트 색상
|
||||
$text-primary: #212121;
|
||||
$text-secondary: #757575;
|
||||
$text-disabled: #bdbdbd;
|
||||
|
||||
// 테두리 및 구분선
|
||||
$border-color: #e0e0e0;
|
||||
$divider-color: #f5f5f5;
|
||||
|
||||
// 그림자
|
||||
$shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
$shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
$shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 테두리 반경
|
||||
$border-radius-sm: 4px;
|
||||
$border-radius-md: 8px;
|
||||
$border-radius-lg: 12px;
|
||||
$border-radius-xl: 16px;
|
||||
|
||||
// 간격
|
||||
$space-1: 4px;
|
||||
$space-2: 8px;
|
||||
$space-3: 12px;
|
||||
$space-4: 16px;
|
||||
$space-5: 20px;
|
||||
$space-6: 24px;
|
||||
$space-8: 32px;
|
||||
$space-10: 40px;
|
||||
$space-12: 48px;
|
||||
$space-16: 64px;
|
||||
|
||||
// 폰트 크기
|
||||
$font-size-xs: 0.75rem; // 12px
|
||||
$font-size-sm: 0.875rem; // 14px
|
||||
$font-size-base: 1rem; // 16px
|
||||
$font-size-lg: 1.125rem; // 18px
|
||||
$font-size-xl: 1.25rem; // 20px
|
||||
$font-size-2xl: 1.5rem; // 24px
|
||||
$font-size-3xl: 1.875rem; // 30px
|
||||
$font-size-4xl: 2.25rem; // 36px
|
||||
|
||||
// 폰트 두께
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
// 전환 효과
|
||||
$transition-fast: 0.15s ease;
|
||||
$transition-normal: 0.3s ease;
|
||||
$transition-slow: 0.5s ease;
|
||||
|
||||
// 브레이크포인트
|
||||
$breakpoint-xs: 0;
|
||||
$breakpoint-sm: 600px;
|
||||
$breakpoint-md: 960px;
|
||||
$breakpoint-lg: 1280px;
|
||||
$breakpoint-xl: 1920px;
|
||||
|
||||
// Z-인덱스
|
||||
$z-index-dropdown: 1000;
|
||||
$z-index-modal: 1050;
|
||||
$z-index-tooltip: 1100;
|
||||
$z-index-toast: 1200;
|
||||
|
||||
// 다크 테마 색상
|
||||
$dark-background-color: #121212;
|
||||
$dark-surface-color: #1e1e1e;
|
||||
$dark-surface-variant-color: #424242;
|
||||
$dark-text-primary: #ffffff;
|
||||
$dark-text-secondary: #aaaaaa;
|
||||
$dark-text-disabled: #666666;
|
||||
$dark-border-color: #333333;
|
||||
$dark-divider-color: #2a2a2a;
|
||||
@ -1,171 +1,179 @@
|
||||
//* src/views/LoginView.vue
|
||||
<template>
|
||||
<v-container fluid class="fill-height pa-0">
|
||||
<v-row no-gutters class="fill-height">
|
||||
<v-col cols="12" class="d-flex flex-column">
|
||||
<!-- 로고 및 헤더 -->
|
||||
<div class="text-center pa-8 bg-primary">
|
||||
<v-img src="/images/logo.png" alt="AI 마케팅 로고" 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>
|
||||
<v-container fluid class="login-container">
|
||||
<v-row justify="center" align="center" class="fill-height">
|
||||
<v-col cols="12" sm="8" md="6" lg="4" xl="3">
|
||||
<v-card class="login-card" elevation="8">
|
||||
<!-- 로고 섹션 -->
|
||||
<v-card-text class="text-center pa-8">
|
||||
<div class="logo-section mb-6">
|
||||
<v-img
|
||||
src="/images/logo192.png"
|
||||
alt="AI 마케팅 로고"
|
||||
width="80"
|
||||
height="80"
|
||||
class="mx-auto mb-4"
|
||||
/>
|
||||
<h1 class="text-h4 font-weight-bold primary--text mb-2">AI 마케팅</h1>
|
||||
<p class="text-subtitle-1 text-grey-darken-1">소상공인을 위한 스마트 마케팅 솔루션</p>
|
||||
</div>
|
||||
|
||||
<!-- 로그인 폼 -->
|
||||
<div class="flex-grow-1 pa-6">
|
||||
<v-card class="mx-auto" max-width="400" elevation="8">
|
||||
<v-card-title class="text-h5 text-center pa-6"> 로그인 </v-card-title>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-form ref="loginForm" v-model="formValid" @submit.prevent="handleLogin">
|
||||
<v-form ref="loginForm" v-model="isFormValid" @submit.prevent="handleLogin">
|
||||
<v-text-field
|
||||
v-model="loginData.userId"
|
||||
v-model="credentials.username"
|
||||
label="아이디"
|
||||
prepend-inner-icon="mdi-account"
|
||||
variant="outlined"
|
||||
:rules="userIdRules"
|
||||
required
|
||||
:rules="usernameRules"
|
||||
:error-messages="fieldErrors.username"
|
||||
class="mb-4"
|
||||
autocomplete="username"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="loginData.password"
|
||||
v-model="credentials.password"
|
||||
label="비밀번호"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
variant="outlined"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
variant="outlined"
|
||||
:rules="passwordRules"
|
||||
required
|
||||
:error-messages="fieldErrors.password"
|
||||
class="mb-4"
|
||||
autocomplete="current-password"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
|
||||
<!-- 로그인 옵션 -->
|
||||
<div class="d-flex justify-space-between align-center mb-6">
|
||||
<v-checkbox
|
||||
v-model="rememberMe"
|
||||
label="로그인 상태 유지"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="primary"
|
||||
@click="showForgotPassword = true"
|
||||
>
|
||||
비밀번호 찾기
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 에러 메시지 -->
|
||||
<v-alert
|
||||
v-if="loginError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
closable
|
||||
@click:close="loginError = ''"
|
||||
>
|
||||
{{ loginError }}
|
||||
</v-alert>
|
||||
|
||||
<!-- 디버그 정보 (개발 중에만 표시) -->
|
||||
<v-card v-if="showDebugInfo" variant="outlined" class="mb-4 pa-3">
|
||||
<div class="text-caption">디버그 정보:</div>
|
||||
<div class="text-caption">아이디: "{{ credentials.username }}"</div>
|
||||
<div class="text-caption">비밀번호: "{{ credentials.password }}"</div>
|
||||
<div class="text-caption">폼 유효성: {{ isFormValid }}</div>
|
||||
<div class="text-caption">로딩 상태: {{ authStore.isLoading }}</div>
|
||||
</v-card>
|
||||
|
||||
<!-- 로그인 버튼 -->
|
||||
<v-btn
|
||||
type="submit"
|
||||
block
|
||||
size="large"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!formValid"
|
||||
size="large"
|
||||
block
|
||||
:loading="authStore.isLoading"
|
||||
:disabled="!isFormValid || authStore.isLoading"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-icon start>mdi-login</v-icon>
|
||||
로그인
|
||||
</v-btn>
|
||||
|
||||
<!-- 테스트용 기본값 설정 버튼 -->
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
block
|
||||
color="secondary"
|
||||
@click="setTestCredentials"
|
||||
class="mb-4"
|
||||
>
|
||||
테스트 계정으로 입력
|
||||
</v-btn>
|
||||
|
||||
<!-- 회원가입 링크 -->
|
||||
<div class="text-center">
|
||||
<v-btn variant="text" color="primary" @click="showRegisterDialog = true">
|
||||
<span class="text-body-2 text-grey-darken-1"> 계정이 없으신가요? </span>
|
||||
<v-btn variant="text" color="primary" size="small" @click="showSignup = true">
|
||||
회원가입
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- 푸터 -->
|
||||
<div class="text-center pa-4 text-caption text-grey">
|
||||
© 2024 AI Marketing Service. All rights reserved.
|
||||
<!-- 데모 정보 카드 -->
|
||||
<v-card class="mt-4" variant="outlined">
|
||||
<v-card-text class="pa-4">
|
||||
<div class="text-center">
|
||||
<v-icon color="info" class="mb-2">mdi-information</v-icon>
|
||||
<h3 class="text-subtitle-2 font-weight-bold mb-2">데모 계정 정보</h3>
|
||||
<div class="demo-info">
|
||||
<div class="text-body-2 mb-1">
|
||||
<span class="font-weight-medium">아이디:</span> user01
|
||||
</div>
|
||||
<div class="text-body-2 mb-2">
|
||||
<span class="font-weight-medium">비밀번호:</span> passw0rd
|
||||
</div>
|
||||
<v-btn size="small" color="info" variant="outlined" @click="fillDemoCredentials">
|
||||
데모 계정 자동 입력
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 회원가입 다이얼로그 -->
|
||||
<v-dialog v-model="showRegisterDialog" max-width="500" scrollable>
|
||||
<!-- 비밀번호 찾기 다이얼로그 -->
|
||||
<v-dialog v-model="showForgotPassword" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">
|
||||
회원가입
|
||||
<v-spacer />
|
||||
<v-btn icon @click="showRegisterDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-form ref="registerForm" v-model="registerFormValid">
|
||||
<v-card-title>비밀번호 찾기</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="mb-4">등록하신 이메일 주소를 입력해주세요.</p>
|
||||
<v-text-field
|
||||
v-model="registerData.userId"
|
||||
label="아이디"
|
||||
variant="outlined"
|
||||
:rules="userIdRules"
|
||||
required
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="registerData.password"
|
||||
label="비밀번호"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
:rules="registerPasswordRules"
|
||||
required
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="registerData.confirmPassword"
|
||||
label="비밀번호 확인"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
:rules="confirmPasswordRules"
|
||||
required
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="registerData.nickname"
|
||||
label="닉네임"
|
||||
variant="outlined"
|
||||
:rules="nicknameRules"
|
||||
required
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="registerData.businessNumber"
|
||||
label="사업자등록번호"
|
||||
variant="outlined"
|
||||
:rules="businessNumberRules"
|
||||
required
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="registerData.email"
|
||||
v-model="forgotEmail"
|
||||
label="이메일"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
:rules="emailRules"
|
||||
required
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-email"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showForgotPassword = false">취소</v-btn>
|
||||
<v-btn color="primary" @click="handleForgotPassword">전송</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 회원가입 다이얼로그 -->
|
||||
<v-dialog v-model="showSignup" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title>회원가입</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="mb-4">AI 마케팅 서비스에 오신 것을 환영합니다!</p>
|
||||
<v-form>
|
||||
<v-text-field label="아이디" variant="outlined" class="mb-2" />
|
||||
<v-text-field label="비밀번호" type="password" variant="outlined" class="mb-2" />
|
||||
<v-text-field label="비밀번호 확인" type="password" variant="outlined" class="mb-2" />
|
||||
<v-text-field label="이메일" type="email" variant="outlined" class="mb-2" />
|
||||
<v-text-field label="매장명" variant="outlined" class="mb-2" />
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-6">
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="showRegisterDialog = false"> 취소 </v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="registerLoading"
|
||||
:disabled="!registerFormValid"
|
||||
@click="register"
|
||||
>
|
||||
가입하기
|
||||
</v-btn>
|
||||
<v-btn variant="text" @click="showSignup = false">취소</v-btn>
|
||||
<v-btn color="primary" @click="handleSignup">가입하기</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@ -173,182 +181,168 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/store/auth' // 수정된 import 경로
|
||||
import { useAppStore } from '@/store/app' // App Store 추가
|
||||
|
||||
/**
|
||||
* 로그인 페이지
|
||||
* 사용자 인증 및 회원가입 기능 제공
|
||||
*/
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useAppStore } from '@/store/app'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const appStore = useAppStore() // App Store 사용
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 반응형 데이터
|
||||
const formValid = ref(false)
|
||||
const registerFormValid = ref(false)
|
||||
const loading = ref(false)
|
||||
const registerLoading = ref(false)
|
||||
const loginForm = ref(null)
|
||||
const isFormValid = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const showRegisterDialog = ref(false)
|
||||
const rememberMe = ref(false)
|
||||
const loginError = ref('')
|
||||
const showForgotPassword = ref(false)
|
||||
const showSignup = ref(false)
|
||||
const forgotEmail = ref('')
|
||||
const showDebugInfo = ref(true) // 개발 중에는 true로 설정
|
||||
|
||||
const loginData = reactive({
|
||||
userId: 'user01', // 테스트용 기본값
|
||||
password: 'passw0rd', // 테스트용 기본값
|
||||
// 로그인 자격 증명 - 기본값 설정
|
||||
const credentials = ref({
|
||||
username: 'user01', // 기본값 설정
|
||||
password: 'passw0rd', // 기본값 설정
|
||||
})
|
||||
|
||||
const registerData = reactive({
|
||||
userId: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
nickname: '',
|
||||
businessNumber: '',
|
||||
email: '',
|
||||
const fieldErrors = ref({
|
||||
username: [],
|
||||
password: [],
|
||||
})
|
||||
|
||||
// 유효성 검사 규칙 (테스트를 위해 완화)
|
||||
const userIdRules = [(v) => !!v || '아이디를 입력해주세요']
|
||||
// 유효성 검사 규칙
|
||||
const usernameRules = [
|
||||
(v) => !!v || '아이디를 입력해주세요',
|
||||
(v) => (v && v.length >= 3) || '아이디는 3자 이상이어야 합니다',
|
||||
]
|
||||
|
||||
const passwordRules = [(v) => !!v || '비밀번호를 입력해주세요']
|
||||
|
||||
// 회원가입용 더 엄격한 규칙
|
||||
const registerPasswordRules = [
|
||||
const passwordRules = [
|
||||
(v) => !!v || '비밀번호를 입력해주세요',
|
||||
(v) => v.length >= 8 || '비밀번호는 8자 이상이어야 합니다',
|
||||
(v) =>
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(v) ||
|
||||
'영문, 숫자, 특수문자를 포함해야 합니다',
|
||||
]
|
||||
|
||||
const confirmPasswordRules = [
|
||||
(v) => !!v || '비밀번호 확인을 입력해주세요',
|
||||
(v) => v === registerData.password || '비밀번호가 일치하지 않습니다',
|
||||
]
|
||||
|
||||
const nicknameRules = [
|
||||
(v) => !!v || '닉네임을 입력해주세요',
|
||||
(v) => v.length >= 2 || '닉네임은 2자 이상이어야 합니다',
|
||||
]
|
||||
|
||||
const businessNumberRules = [(v) => !!v || '사업자등록번호를 입력해주세요']
|
||||
|
||||
const emailRules = [
|
||||
(v) => !!v || '이메일을 입력해주세요',
|
||||
(v) => /.+@.+\..+/.test(v) || '올바른 이메일 형식이 아닙니다',
|
||||
(v) => (v && v.length >= 6) || '비밀번호는 6자 이상이어야 합니다',
|
||||
]
|
||||
|
||||
// 메서드
|
||||
const handleLogin = async () => {
|
||||
console.log('=== 로그인 처리 시작 ===')
|
||||
console.log('입력 데이터:', loginData)
|
||||
const fillDemoCredentials = () => {
|
||||
credentials.value.username = 'user01'
|
||||
credentials.value.password = 'passw0rd'
|
||||
loginError.value = ''
|
||||
console.log('데모 계정 정보 자동 입력 완료')
|
||||
}
|
||||
|
||||
if (!formValid.value) {
|
||||
const handleLogin = async () => {
|
||||
console.log('=== 로그인 시도 시작 ===')
|
||||
console.log('폼 유효성:', isFormValid.value)
|
||||
console.log('입력된 자격증명:', {
|
||||
username: credentials.value.username,
|
||||
password: credentials.value.password,
|
||||
usernameLength: credentials.value.username?.length,
|
||||
passwordLength: credentials.value.password?.length,
|
||||
})
|
||||
|
||||
// 폼 유효성 검사
|
||||
if (!isFormValid.value) {
|
||||
console.log('폼 유효성 검사 실패')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
// 빈 값 체크
|
||||
if (!credentials.value.username || !credentials.value.password) {
|
||||
loginError.value = '아이디와 비밀번호를 모두 입력해주세요'
|
||||
return
|
||||
}
|
||||
|
||||
// 에러 초기화
|
||||
loginError.value = ''
|
||||
fieldErrors.value = { username: [], password: [] }
|
||||
|
||||
try {
|
||||
// Auth Store의 login 메서드 호출 (수정된 방식)
|
||||
console.log('auth store 로그인 호출 중...')
|
||||
const result = await authStore.login({
|
||||
userId: loginData.userId,
|
||||
password: loginData.password,
|
||||
username: credentials.value.username.trim(), // 공백 제거
|
||||
password: credentials.value.password.trim(), // 공백 제거
|
||||
})
|
||||
|
||||
console.log('로그인 결과:', result)
|
||||
|
||||
if (result.success) {
|
||||
appStore.showSnackbar('로그인 되었습니다', 'success')
|
||||
|
||||
console.log('로그인 성공, 인증 상태 확인:', authStore.isAuthenticated)
|
||||
|
||||
// 대시보드로 이동
|
||||
console.log('대시보드로 이동 시도')
|
||||
await router.push('/dashboard')
|
||||
console.log('라우터 이동 완료')
|
||||
console.log('로그인 성공!')
|
||||
appStore.showSnackbar('로그인되었습니다', 'success')
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
appStore.showSnackbar(result.message || '로그인에 실패했습니다', 'error')
|
||||
console.log('로그인 실패:', result.error)
|
||||
loginError.value = result.error || '로그인에 실패했습니다'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('로그인 에러:', error)
|
||||
appStore.showSnackbar('로그인 중 오류가 발생했습니다', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
loginError.value = '서버 오류가 발생했습니다'
|
||||
}
|
||||
|
||||
console.log('=== 로그인 처리 종료 ===')
|
||||
}
|
||||
|
||||
const setTestCredentials = () => {
|
||||
loginData.userId = 'user01'
|
||||
loginData.password = 'passw0rd'
|
||||
appStore.showSnackbar('테스트 계정 정보가 입력되었습니다', 'info')
|
||||
const handleForgotPassword = () => {
|
||||
appStore.showSnackbar('비밀번호 재설정 이메일을 발송했습니다', 'info')
|
||||
showForgotPassword.value = false
|
||||
forgotEmail.value = ''
|
||||
}
|
||||
|
||||
const register = async () => {
|
||||
if (!registerFormValid.value) return
|
||||
|
||||
try {
|
||||
registerLoading.value = true
|
||||
|
||||
// 임시 회원가입 로직 (실제 API 연동 전)
|
||||
console.log('회원가입 데이터:', registerData)
|
||||
|
||||
// 시뮬레이션: 성공
|
||||
appStore.showSnackbar('회원가입이 완료되었습니다', 'success')
|
||||
|
||||
showRegisterDialog.value = false
|
||||
|
||||
// 폼 초기화
|
||||
Object.assign(registerData, {
|
||||
userId: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
nickname: '',
|
||||
businessNumber: '',
|
||||
email: '',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('회원가입 에러:', error)
|
||||
appStore.showSnackbar('회원가입에 실패했습니다', 'error')
|
||||
} finally {
|
||||
registerLoading.value = false
|
||||
}
|
||||
const handleSignup = () => {
|
||||
appStore.showSnackbar('회원가입 기능은 곧 제공될 예정입니다', 'info')
|
||||
showSignup.value = false
|
||||
}
|
||||
|
||||
// 컴포넌트 마운트 시 실행
|
||||
onMounted(() => {
|
||||
console.log('LoginView 마운트됨')
|
||||
console.log('초기 인증 상태:', authStore.isAuthenticated)
|
||||
console.log('초기 자격증명:', credentials.value)
|
||||
|
||||
// 이미 로그인된 경우 대시보드로 리다이렉트
|
||||
// 이미 로그인된 상태라면 대시보드로 리다이렉트
|
||||
if (authStore.isAuthenticated) {
|
||||
console.log('이미 로그인됨, 대시보드로 이동')
|
||||
console.log('이미 로그인된 상태 - 대시보드로 이동')
|
||||
router.push('/dashboard')
|
||||
}
|
||||
|
||||
// 디버그용 - 3초 후 디버그 정보 숨기기
|
||||
setTimeout(() => {
|
||||
showDebugInfo.value = false
|
||||
}, 10000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fill-height {
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||||
.login-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pa-8 {
|
||||
padding: 2rem !important;
|
||||
.login-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.text-h4 {
|
||||
font-size: 1.8rem !important;
|
||||
.logo-section {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
//* vite.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
@ -28,11 +27,4 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@import "@/styles/variables.scss";`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user