login store menu function edit

This commit is contained in:
unknown 2025-06-20 12:27:55 +09:00
parent 59e8ecfc24
commit 9e7eb7da2b
6 changed files with 1831 additions and 1562 deletions

View File

@ -1,658 +1,301 @@
//* src/components/layout/MainLayout.vue <!-- src/components/layout/MainLayout.vue 또는 네비게이션 컴포넌트 수정 -->
<template>
<v-app>
<!-- 메인 네비게이션 (데스크톱용 사이드바) -->
<Sidebar
v-if="!isMobile"
v-model="drawer"
:rail="rail"
:permanent="!isMobile"
:temporary="isMobile"
:menu-groups="menuGroups"
:user-name="userStore.user?.name || '사용자'"
:business-name="userStore.user?.businessName || ''"
:user-avatar="userStore.user?.avatar || ''"
@menu-click="handleMenuClick"
@action-click="handleSidebarAction"
@update:rail="rail = $event"
/>
<!-- 모바일용 임시 사이드바 -->
<Sidebar
v-if="isMobile"
v-model="drawer"
:temporary="true"
:menu-groups="menuGroups"
:user-name="userStore.user?.name || '사용자'"
:business-name="userStore.user?.businessName || ''"
:user-avatar="userStore.user?.avatar || ''"
@menu-click="handleMenuClick"
@action-click="handleSidebarAction"
/>
<!-- 상단 앱바 -->
<AppHeader
v-if="showHeader"
:title="currentPageTitle"
:show-back-button="showBackButton"
:show-menu-button="isMobile"
:show-user-menu="!isMobile"
:actions="headerActions"
:user-name="userStore.user?.name || '사용자'"
:user-email="userStore.user?.email || ''"
:user-avatar="userStore.user?.avatar || ''"
@toggle-drawer="drawer = !drawer"
@back-click="handleBackClick"
@action-click="handleHeaderAction"
@user-menu-click="handleUserMenuClick"
/>
<!-- 메인 컨텐츠 -->
<v-main :class="mainClass">
<v-container
:fluid="fluidContainer"
:class="containerClass"
>
<!-- 로딩 오버레이 -->
<LoadingSpinner
v-if="loading"
overlay
:message="loadingMessage"
:sub-message="loadingSubMessage"
/>
<!-- 에러 알림 -->
<ErrorAlert
v-if="error"
v-model="showError"
:type="error.type || 'error'"
:title="error.title"
:message="error.message"
:details="error.details"
:show-retry-button="error.retryable"
@retry="handleErrorRetry"
@close="clearError"
/>
<!-- 페이지 컨텐츠 -->
<router-view v-slot="{ Component, route }">
<transition :name="pageTransition" mode="out-in">
<component
:is="Component"
:key="route.path"
@loading="setLoading"
@error="setError"
/>
</transition>
</router-view>
</v-container>
</v-main>
<!-- 하단 네비게이션 (모바일용) -->
<BottomNavigation
v-if="isMobile && showBottomNav"
:navigation-items="bottomNavItems"
:hide-on-routes="bottomNavHideRoutes"
@nav-click="handleBottomNavClick"
/>
<!-- 전역 스낵바 -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
:location="snackbar.location"
:multi-line="snackbar.multiLine"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
v-if="snackbar.action"
variant="text"
@click="handleSnackbarAction"
>
{{ snackbar.actionText }}
</v-btn>
<v-btn
icon="mdi-close"
variant="text"
@click="snackbar.show = false"
/>
</template>
</v-snackbar>
<!-- 확인 다이얼로그 -->
<ConfirmDialog
v-model="confirmDialog.show"
:title="confirmDialog.title"
:message="confirmDialog.message"
:icon="confirmDialog.icon"
:confirm-text="confirmDialog.confirmText"
:cancel-text="confirmDialog.cancelText"
:confirm-loading="confirmDialog.loading"
@confirm="handleConfirmDialog"
@cancel="confirmDialog.show = false"
/>
</v-app>
</template>
<script setup> <script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue' import { computed, ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useDisplay } from 'vuetify' import { useAuthStore } from '@/store/auth'
import { useStoreStore } from '@/store/index'
import { useAppStore } from '@/store/app'
//
import AppHeader from '../common/AppHeader.vue'
import LoadingSpinner from '../common/LoadingSpinner.vue'
import ErrorAlert from '../common/ErrorAlert.vue'
import ConfirmDialog from '../common/ConfirmDialog.vue'
import Sidebar from './Sidebar.vue'
import BottomNavigation from './BottomNavigation.vue'
// (Pinia )
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
/**
* AI 마케팅 서비스 - 메인 레이아웃 컴포넌트
* 전체 애플리케이션의 레이아웃을 관리하는 최상위 컴포넌트
*/
// Props
const props = defineProps({
//
showHeader: {
type: Boolean,
default: true
},
showBottomNav: {
type: Boolean,
default: true
},
fluidContainer: {
type: Boolean,
default: false
},
pageTransition: {
type: String,
default: 'fade'
},
//
containerClass: {
type: [String, Array, Object],
default: 'pa-4'
}
})
//
const userStore = useUserStore()
const appStore = useAppStore()
//
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const authStore = useAuthStore()
const storeStore = useStoreStore()
const appStore = useAppStore()
// Vuetify Display
const { mobile } = useDisplay()
// Reactive data
const drawer = ref(false) const drawer = ref(false)
const rail = ref(false)
const loading = ref(false)
const loadingMessage = ref('')
const loadingSubMessage = ref('')
const error = ref(null)
const showError = ref(false)
// //
const snackbar = ref({ const navigationItems = computed(() => {
show: false, const baseItems = [
message: '', {
color: 'info', title: '매장 관리',
timeout: 4000, icon: 'mdi-store',
location: 'bottom', to: '/store',
multiLine: false, color: 'primary',
action: null, available: true, //
actionText: '확인' description: '매장 정보를 등록하고 관리하세요'
}) }
]
// //
const confirmDialog = ref({ if (storeStore.hasStoreInfo) {
show: false, baseItems.unshift( //
title: '',
message: '',
icon: '',
confirmText: '확인',
cancelText: '취소',
loading: false,
callback: null
})
// Computed
const isMobile = computed(() => mobile.value)
const mainClass = computed(() => ({
'main-content': true,
'main-content--rail': rail.value && !isMobile.value,
'main-content--mobile': isMobile.value
}))
const showBackButton = computed(() => {
//
const mainRoutes = ['/dashboard', '/store', '/content', '/menu']
return !mainRoutes.includes(route.path)
})
const currentPageTitle = computed(() => {
return route.meta?.title || 'AI 마케팅'
})
const headerActions = computed(() => {
//
const actions = []
if (route.path.startsWith('/content')) {
actions.push({
id: 'add-content',
icon: 'mdi-plus',
text: '생성',
color: 'white'
})
}
//
actions.push({
id: 'notifications',
icon: 'mdi-bell',
badge: true,
badgeCount: appStore.notificationCount || 0,
color: 'white'
})
return actions
})
//
const menuGroups = computed(() => [
{
title: '대시보드',
items: [
{ {
id: 'dashboard', title: '대시보드',
title: '홈', icon: 'mdi-view-dashboard',
icon: 'mdi-home', to: '/dashboard',
route: '/dashboard' color: 'success',
available: true,
description: '매출 현황과 주요 지표를 확인하세요'
} }
] )
},
{ baseItems.push(
title: '매장 관리',
items: [
{ {
id: 'store',
title: '매장 정보',
icon: 'mdi-store',
route: '/store'
},
{
id: 'menu',
title: '메뉴 관리',
icon: 'mdi-food',
route: '/menu'
}
]
},
{
title: '마케팅',
items: [
{
id: 'content-create',
title: '콘텐츠 생성', title: '콘텐츠 생성',
icon: 'mdi-plus-circle', icon: 'mdi-plus-circle',
route: '/content/create' to: '/content/create',
color: 'orange',
available: true,
description: 'AI로 마케팅 콘텐츠를 생성하세요'
}, },
{ {
id: 'content-list', title: '콘텐츠 관리',
title: '콘텐츠 목록', icon: 'mdi-folder-multiple',
icon: 'mdi-file-document-multiple', to: '/content',
route: '/content' color: 'purple',
}, available: true,
{ description: '생성된 콘텐츠를 관리하고 발행하세요'
id: 'ai-recommend',
title: 'AI 추천',
icon: 'mdi-robot',
route: '/recommend'
} }
] )
},
{
title: '분석',
items: [
{
id: 'sales',
title: '매출 분석',
icon: 'mdi-chart-line',
route: '/sales'
}
]
} }
])
// return baseItems
const bottomNavItems = computed(() => [ })
{
id: 'dashboard', // ( )
label: '홈', const unavailableItems = computed(() => {
icon: 'mdi-home-outline', if (storeStore.hasStoreInfo) {
activeIcon: 'mdi-home', return [] //
route: '/dashboard'
},
{
id: 'store',
label: '매장',
icon: 'mdi-store-outline',
activeIcon: 'mdi-store',
route: '/store'
},
{
id: 'content-create',
label: '생성',
icon: 'mdi-plus-circle-outline',
activeIcon: 'mdi-plus-circle',
route: '/content/create'
},
{
id: 'content-list',
label: '목록',
icon: 'mdi-file-document-outline',
activeIcon: 'mdi-file-document',
route: '/content'
},
{
id: 'more',
label: '더보기',
icon: 'mdi-menu',
route: '/more'
} }
])
const bottomNavHideRoutes = ['/login', '/register'] return [
{
// Methods title: '대시보드',
const setLoading = (isLoading, message = '', subMessage = '') => { icon: 'mdi-view-dashboard',
loading.value = isLoading color: 'grey',
loadingMessage.value = message disabled: true,
loadingSubMessage.value = subMessage description: '매장 정보 등록 후 이용 가능합니다'
} },
{
const setError = (errorData) => { title: '콘텐츠 생성',
error.value = errorData icon: 'mdi-plus-circle',
showError.value = true color: 'grey',
} disabled: true,
description: '매장 정보 등록 후 이용 가능합니다'
const clearError = () => { },
error.value = null {
showError.value = false title: '콘텐츠 관리',
} icon: 'mdi-folder-multiple',
color: 'grey',
const handleErrorRetry = () => { disabled: true,
if (error.value?.retryCallback) { description: '매장 정보 등록 후 이용 가능합니다'
error.value.retryCallback()
}
clearError()
}
const showSnackbar = (message, options = {}) => {
snackbar.value = {
show: true,
message,
color: options.color || 'info',
timeout: options.timeout || 4000,
location: options.location || 'bottom',
multiLine: options.multiLine || false,
action: options.action || null,
actionText: options.actionText || '확인'
}
}
const handleSnackbarAction = () => {
if (snackbar.value.action) {
snackbar.value.action()
}
snackbar.value.show = false
}
const showConfirm = (title, message, callback, options = {}) => {
confirmDialog.value = {
show: true,
title,
message,
icon: options.icon || 'mdi-help-circle',
confirmText: options.confirmText || '확인',
cancelText: options.cancelText || '취소',
loading: false,
callback
}
}
const handleConfirmDialog = () => {
if (confirmDialog.value.callback) {
confirmDialog.value.loading = true
try {
const result = confirmDialog.value.callback()
// Promise
if (result && typeof result.then === 'function') {
result
.then(() => {
confirmDialog.value.show = false
confirmDialog.value.loading = false
})
.catch((error) => {
confirmDialog.value.loading = false
setError({
title: '오류 발생',
message: error.message || '작업 중 오류가 발생했습니다.',
type: 'error'
})
})
} else {
confirmDialog.value.show = false
confirmDialog.value.loading = false
}
} catch (error) {
confirmDialog.value.loading = false
setError({
title: '오류 발생',
message: error.message || '작업 중 오류가 발생했습니다.',
type: 'error'
})
} }
} else { ]
confirmDialog.value.show = false
}
}
const handleBackClick = () => {
router.go(-1)
}
const handleMenuClick = (item) => {
console.log('Menu clicked:', item)
// drawer
if (isMobile.value) {
drawer.value = false
}
}
const handleSidebarAction = (action) => {
switch (action.id) {
case 'settings':
router.push('/settings')
break
case 'logout':
showConfirm(
'로그아웃',
'정말 로그아웃하시겠습니까?',
() => {
return userStore.logout().then(() => {
router.push('/login')
showSnackbar('로그아웃되었습니다.', { color: 'success' })
})
},
{ icon: 'mdi-logout' }
)
break
}
}
const handleHeaderAction = (action) => {
switch (action.id) {
case 'add-content':
router.push('/content/create')
break
case 'notifications':
router.push('/notifications')
break
}
}
const handleUserMenuClick = (menuItem) => {
switch (menuItem.id) {
case 'profile':
router.push('/profile')
break
case 'settings':
router.push('/settings')
break
case 'logout':
handleSidebarAction({ id: 'logout' })
break
}
}
const handleBottomNavClick = (item) => {
console.log('Bottom nav clicked:', item)
}
//
const handleOrientationChange = () => {
// drawer
if (isMobile.value) {
drawer.value = false
}
}
// expose
defineExpose({
setLoading,
setError,
clearError,
showSnackbar,
showConfirm
}) })
// //
onMounted(() => { const handleDisabledMenuClick = (item) => {
// appStore.showSnackbar('매장 정보를 먼저 등록해주세요', 'warning')
window.addEventListener('orientationchange', handleOrientationChange) router.push('/store')
}
// //
if (!userStore.user) { const storeInfoStatus = computed(() => {
userStore.fetchUser().catch(error => { if (storeStore.loading) {
setError({ return {
title: '사용자 정보 로드 실패', status: 'loading',
message: '사용자 정보를 불러올 수 없습니다.', message: '매장 정보를 확인하는 중...',
details: error.message, color: 'orange'
retryable: true, }
retryCallback: () => userStore.fetchUser() }
})
}) if (storeStore.hasStoreInfo) {
return {
status: 'complete',
message: '매장 정보 등록 완료',
color: 'success'
}
}
return {
status: 'required',
message: '매장 정보 등록 필요',
color: 'warning'
} }
}) })
onUnmounted(() => { //
window.removeEventListener('orientationchange', handleOrientationChange) onMounted(async () => {
}) if (authStore.isAuthenticated && !storeStore.hasStoreInfo) {
await storeStore.fetchStoreInfo()
//
watch(() => route.path, (newPath) => {
//
clearError()
// drawer
if (isMobile.value) {
drawer.value = false
}
})
//
watch(() => userStore.isAuthenticated, (isAuth) => {
if (!isAuth && route.meta?.requiresAuth !== false) {
router.push('/login')
} }
}) })
</script> </script>
<template>
<v-app>
<!-- 네비게이션 드로어 -->
<v-navigation-drawer
v-model="drawer"
app
:rail="!drawer"
rail-width="72"
width="300"
>
<!-- 매장 정보 상태 카드 -->
<v-card
v-if="authStore.isAuthenticated"
class="ma-3 mb-4"
:color="storeInfoStatus.color"
variant="tonal"
elevation="2"
>
<v-card-text class="py-3">
<div class="d-flex align-center">
<v-icon
:icon="storeInfoStatus.status === 'loading' ? 'mdi-loading mdi-spin' :
storeInfoStatus.status === 'complete' ? 'mdi-check-circle' : 'mdi-alert'"
class="mr-2"
/>
<span class="text-body-2 font-weight-medium">
{{ storeInfoStatus.message }}
</span>
</div>
<!-- 매장명 표시 (매장 정보가 있을 ) -->
<div v-if="storeStore.hasStoreInfo && storeStore.storeInfo" class="mt-2">
<div class="text-h6 font-weight-bold">
{{ storeStore.storeInfo.storeName }}
</div>
<div class="text-caption text-medium-emphasis">
{{ storeStore.storeInfo.businessType }}
</div>
</div>
</v-card-text>
</v-card>
<v-divider class="mx-3 mb-2" />
<!-- 사용 가능한 메뉴들 -->
<v-list density="compact" nav>
<v-list-item
v-for="item in navigationItems"
:key="item.title"
:to="item.to"
:color="item.color"
class="mb-1 mx-2"
rounded="lg"
>
<template v-slot:prepend>
<v-icon :icon="item.icon" />
</template>
<v-list-item-title class="font-weight-medium">
{{ item.title }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption">
{{ item.description }}
</v-list-item-subtitle>
</v-list-item>
</v-list>
<!-- 사용할 없는 메뉴들 (매장 정보가 없을 때만 표시) -->
<template v-if="unavailableItems.length > 0">
<v-divider class="mx-3 my-2" />
<v-list density="compact">
<v-list-subheader class="text-caption text-medium-emphasis px-4">
매장 등록 이용 가능
</v-list-subheader>
<v-list-item
v-for="item in unavailableItems"
:key="item.title"
:disabled="item.disabled"
class="mb-1 mx-2"
rounded="lg"
@click="handleDisabledMenuClick(item)"
>
<template v-slot:prepend>
<v-icon :icon="item.icon" :color="item.color" />
</template>
<v-list-item-title class="font-weight-medium text-medium-emphasis">
{{ item.title }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption">
{{ item.description }}
</v-list-item-subtitle>
<template v-slot:append>
<v-icon icon="mdi-lock" size="small" color="grey" />
</template>
</v-list-item>
</v-list>
</template>
<!-- 하단 로그아웃 버튼 -->
<template v-slot:append>
<v-divider class="mx-3 mb-2" />
<div class="pa-3">
<v-btn
block
color="red"
variant="outlined"
@click="authStore.logout"
>
<v-icon start>mdi-logout</v-icon>
로그아웃
</v-btn>
</div>
</template>
</v-navigation-drawer>
<!-- 앱바 -->
<v-app-bar app color="primary" dark elevation="1">
<v-app-bar-nav-icon @click="drawer = !drawer" />
<v-app-bar-title class="font-weight-bold">
AI 마케팅
</v-app-bar-title>
<v-spacer />
<!-- 사용자 정보 -->
<div v-if="authStore.user" class="d-flex align-center">
<v-chip color="white" variant="outlined" class="mr-2">
<v-icon start>mdi-account</v-icon>
{{ authStore.user.nickname }}
</v-chip>
</div>
</v-app-bar>
<!-- 메인 컨텐츠 -->
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<style scoped> <style scoped>
.main-content { .v-navigation-drawer {
transition: all 0.3s ease; border-right: 1px solid rgba(0, 0, 0, 0.12);
} }
.main-content--rail { .v-list-item--active {
/* Rail 모드일 때 왼쪽 여백 조정 */ font-weight: 600;
margin-left: 72px;
} }
.main-content--mobile { .mdi-spin {
/* 모바일에서는 하단 네비게이션 공간 확보 */ animation: spin 1s linear infinite;
padding-bottom: 80px;
} }
/* 페이지 전환 애니메이션 */ @keyframes spin {
.fade-enter-active, from { transform: rotate(0deg); }
.fade-leave-active { to { transform: rotate(360deg); }
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
/* 반응형 디자인 */
@media (max-width: 960px) {
.main-content--rail {
margin-left: 0;
}
}
/* 안전 영역 지원 */
@supports (padding-top: env(safe-area-inset-top)) {
.v-app-bar {
padding-top: env(safe-area-inset-top);
}
}
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.main-content--mobile {
padding-bottom: calc(80px + env(safe-area-inset-bottom));
}
} }
</style>

View File

@ -1,8 +1,5 @@
//* src/router/index.js // src/router/index.js - 완전히 수정된 버전
/**
* Vue Router 설정
* 라우팅 네비게이션 가드 설정
*/
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
// 뷰 컴포넌트 lazy loading // 뷰 컴포넌트 lazy loading
@ -15,7 +12,7 @@ const ContentManagementView = () => import('@/views/ContentManagementView.vue')
const routes = [ const routes = [
{ {
path: '/', path: '/',
redirect: '/login', // 항상 로그인 페이지로 먼저 리다이렉트 redirect: '/login',
}, },
{ {
path: '/login', path: '/login',
@ -32,6 +29,7 @@ const routes = [
component: DashboardView, component: DashboardView,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
requiresStore: true, // ✅ 매장 정보 필수
title: '대시보드', title: '대시보드',
}, },
}, },
@ -41,6 +39,7 @@ const routes = [
component: StoreManagementView, component: StoreManagementView,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
requiresStore: false, // ✅ 매장 정보 없어도 접근 가능
title: '매장 관리', title: '매장 관리',
}, },
}, },
@ -50,6 +49,7 @@ const routes = [
component: ContentCreationView, component: ContentCreationView,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
requiresStore: true, // ✅ 매장 정보 필수
title: '콘텐츠 생성', title: '콘텐츠 생성',
}, },
}, },
@ -59,12 +59,13 @@ const routes = [
component: ContentManagementView, component: ContentManagementView,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
requiresStore: true, // ✅ 매장 정보 필수
title: '콘텐츠 관리', title: '콘텐츠 관리',
}, },
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
redirect: '/login', // 404시 로그인으로 이동 redirect: '/login',
}, },
] ]
@ -73,12 +74,12 @@ const router = createRouter({
routes, routes,
}) })
// 네비게이션 가드 - 수정된 버전 // ✅ 개선된 네비게이션 가드
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
console.log('=== 라우터 가드 실행 ===') console.log('=== 라우터 가드 실행 ===')
console.log('이동 경로:', `${from.path}${to.path}`) console.log('이동 경로:', `${from.path}${to.path}`)
// Pinia 스토어를 동적으로 가져오기 (순환 참조 방지) // Pinia 스토어를 동적으로 가져오기
const { useAuthStore } = await import('@/store/auth') const { useAuthStore } = await import('@/store/auth')
const authStore = useAuthStore() const authStore = useAuthStore()
@ -89,19 +90,79 @@ router.beforeEach(async (to, from, next) => {
console.log('토큰 존재:', !!authStore.token) console.log('토큰 존재:', !!authStore.token)
console.log('사용자 정보:', authStore.user?.nickname) console.log('사용자 정보:', authStore.user?.nickname)
// 인증이 필요한 페이지인지 확인 // 1단계: 인증 체크
const requiresAuth = to.meta.requiresAuth !== false const requiresAuth = to.meta.requiresAuth !== false
if (requiresAuth && !authStore.isAuthenticated) { if (requiresAuth && !authStore.isAuthenticated) {
console.log('인증 필요 - 로그인 페이지로 이동') console.log('🚫 인증 필요 - 로그인 페이지로 이동')
next('/login') next('/login')
} else if (to.path === '/login' && authStore.isAuthenticated) { return
console.log('이미 로그인됨 - 대시보드로 이동')
next('/dashboard')
} else {
console.log('이동 허용:', to.path)
next()
} }
if (to.path === '/login' && authStore.isAuthenticated) {
console.log('✅ 이미 로그인됨 - 매장 정보 체크 후 리다이렉트')
// 로그인 상태에서 /login 접근 시 매장 정보에 따라 리다이렉트
try {
const { useStoreStore } = await import('@/store/index')
const storeStore = useStoreStore()
const result = await storeStore.fetchStoreInfo()
if (result.success && result.data) {
console.log('🏪 매장 정보 있음 - 대시보드로 이동')
next('/dashboard')
} else {
console.log('📝 매장 정보 없음 - 매장 관리로 이동')
next('/store')
}
} catch (error) {
console.log('❌ 매장 정보 조회 실패 - 매장 관리로 이동')
next('/store')
}
return
}
// 2단계: 매장 정보 체크 (인증된 사용자만)
const requiresStore = to.meta.requiresStore === true
if (authStore.isAuthenticated && requiresStore) {
console.log('🏪 매장 정보 체크 필요한 페이지:', to.name)
try {
const { useStoreStore } = await import('@/store/index')
const storeStore = useStoreStore()
// 매장 정보 조회
const result = await storeStore.fetchStoreInfo()
if (!result.success || !result.data) {
console.log('🚫 매장 정보 없음 - 매장 관리 페이지로 리다이렉트')
// 사용자에게 알림 (스낵바)
const { useAppStore } = await import('@/store/app')
const appStore = useAppStore()
appStore.showSnackbar('매장 정보를 먼저 등록해주세요', 'warning')
next('/store')
return
} else {
console.log('✅ 매장 정보 확인됨 - 페이지 접근 허용')
}
} catch (error) {
console.log('❌ 매장 정보 조회 실패 - 매장 관리 페이지로 리다이렉트')
// 에러 시에도 매장 관리로
const { useAppStore } = await import('@/store/app')
const appStore = useAppStore()
appStore.showSnackbar('매장 정보를 확인할 수 없습니다. 매장 정보를 등록해주세요', 'error')
next('/store')
return
}
}
console.log('✅ 이동 허용:', to.path)
next()
}) })
router.afterEach((to) => { router.afterEach((to) => {

View File

@ -69,59 +69,78 @@ class StoreService {
* @returns {Promise<Object>} 매장 정보 * @returns {Promise<Object>} 매장 정보
*/ */
async getStore() { async getStore() {
try { try {
console.log('=== 매장 정보 조회 API 호출 ===') console.log('=== 매장 정보 조회 API 호출 ===')
// URL 슬래시 문제 해결: 빈 문자열로 호출하여 '/api/store'가 되도록 함 const response = await storeApi.get('')
const response = await storeApi.get('')
console.log('매장 정보 조회 API 응답:', response.data) console.log('매장 정보 조회 API 응답:', response.data)
// 백엔드 응답 구조 수정: 디버깅 결과에 맞게 처리 // 성공 응답 처리
if (response.data && response.data.status === 200 && response.data.data) { if (response.data && response.data.status === 200 && response.data.data) {
console.log('✅ 매장 정보 조회 성공:', response.data.data) console.log('✅ 매장 정보 조회 성공:', response.data.data)
return { return {
success: true, success: true,
message: response.data.message || '매장 정보를 조회했습니다.', message: response.data.message || '매장 정보를 조회했습니다.',
data: response.data.data data: response.data.data
}
} else if (response.data && response.data.status === 404) {
// 매장이 없는 경우
console.log('⚠️ 등록된 매장이 없음')
return {
success: false,
message: '등록된 매장이 없습니다',
data: null
}
} else {
console.warn('예상치 못한 응답 구조:', response.data)
throw new Error(response.data.message || '매장 정보를 찾을 수 없습니다.')
} }
} catch (error) { } else if (response.data && response.data.status === 404) {
console.error('매장 정보 조회 실패:', error) // 매장이 없는 경우
console.log('📝 등록된 매장이 없음 (정상)')
// 404 오류 처리 (매장이 없음) return {
if (error.response?.status === 404) { success: false,
return { message: '등록된 매장이 없습니다',
success: false, data: null
message: '등록된 매장이 없습니다',
data: null
}
} }
} else {
// 500 오류 처리 (서버 내부 오류) console.log('예상치 못한 응답 구조:', response.data)
if (error.response?.status === 500) { return {
console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) success: false,
return { message: '등록된 매장이 없습니다',
success: false, data: null
message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.',
data: null
}
} }
}
} catch (error) {
console.log('매장 정보 조회 중 오류:', error.message)
return handleApiError(error) // 404 오류 - 매장이 없음 (정상)
if (error.response?.status === 404) {
console.log('📝 404: 등록된 매장이 없음 (정상)')
return {
success: false,
message: '등록된 매장이 없습니다',
data: null
}
}
// 500 오류 - 서버 에러지만 매장이 없어서 발생할 수 있음
if (error.response?.status === 500) {
console.log('📝 500: 서버 에러 - 매장 없음으로 간주')
return {
success: false,
message: '등록된 매장이 없습니다',
data: null
}
}
// 401 오류 - 인증 문제
if (error.response?.status === 401) {
return {
success: false,
message: '로그인이 필요합니다',
data: null
}
}
// 기타 모든 에러도 매장 없음으로 간주
console.log('📝 기타 에러 - 매장 없음으로 간주')
return {
success: false,
message: '등록된 매장이 없습니다',
data: null
} }
} }
}
/** /**
* 매장 정보 수정 (STR-010: 매장 수정) * 매장 정보 수정 (STR-010: 매장 수정)
@ -140,10 +159,8 @@ class StoreService {
businessType: storeData.businessType, businessType: storeData.businessType,
address: storeData.address, address: storeData.address,
phoneNumber: storeData.phoneNumber, phoneNumber: storeData.phoneNumber,
// ✅ 수정: businessHours 필드 처리 businessHours: storeData.businessHours, // 그대로 전달
businessHours: storeData.businessHours || `${storeData.openTime || '09:00'}-${storeData.closeTime || '21:00'}`, closedDays: storeData.closedDays, // 그대로 전달
// ✅ 수정: closedDays 필드 처리
closedDays: storeData.closedDays || storeData.holidays || '',
seatCount: parseInt(storeData.seatCount) || 0, seatCount: parseInt(storeData.seatCount) || 0,
instaAccounts: storeData.instaAccounts || '', instaAccounts: storeData.instaAccounts || '',
blogAccounts: storeData.blogAccounts || '', blogAccounts: storeData.blogAccounts || '',

View File

@ -12,7 +12,25 @@ export const useStoreStore = defineStore('store', {
getters: { getters: {
hasStoreInfo: (state) => !!state.storeInfo, hasStoreInfo: (state) => !!state.storeInfo,
isLoading: (state) => state.loading, isLoading: (state) => state.loading,
hasMenus: (state) => state.menus && state.menus.length > 0 hasMenus: (state) => state.menus && state.menus.length > 0,
storeInfoSummary: (state) => {
if (!state.storeInfo) {
return {
hasStore: false,
message: '매장 정보를 등록해주세요',
action: '등록하기'
}
}
return {
hasStore: true,
storeName: state.storeInfo.storeName,
businessType: state.storeInfo.businessType,
message: `${state.storeInfo.storeName} 운영 중`,
action: '관리하기'
}
}
}, },
actions: { actions: {
@ -20,62 +38,107 @@ export const useStoreStore = defineStore('store', {
* 매장 정보 조회 * 매장 정보 조회
*/ */
async fetchStoreInfo() { async fetchStoreInfo() {
console.log('=== Store 스토어: 매장 정보 조회 시작 ===') console.log('=== Store 스토어: 매장 정보 조회 시작 ===')
this.loading = true this.loading = true
this.error = null this.error = null
try {
// 스토어 서비스 임포트
const { storeService } = await import('@/services/store')
console.log('매장 정보 API 호출')
const result = await storeService.getStore()
console.log('=== Store 스토어: API 응답 분석 ===')
console.log('Result:', result)
console.log('Result.success:', result.success)
console.log('Result.data:', result.data)
console.log('Result.message:', result.message)
if (result.success && result.data) {
// 매장 정보가 있는 경우
console.log('✅ 매장 정보 설정:', result.data)
this.storeInfo = result.data
return { success: true, data: result.data }
} else {
// 매장이 없거나 조회 실패한 경우
console.log('📝 매장 정보 없음 - 신규 사용자')
this.storeInfo = null
// 매장이 없는 것은 정상 상황이므로 success: false이지만 에러가 아님
return {
success: false,
message: '등록된 매장이 없습니다',
isNewUser: true // 신규 사용자 플래그 추가
}
}
} catch (error) {
console.log('=== Store 스토어: 매장 정보 조회 중 오류 ===')
console.log('Error:', error.message)
this.error = null // 에러 상태를 설정하지 않음
this.storeInfo = null
// HTTP 상태 코드별 처리 - 모두 신규 사용자로 간주
if (error.response?.status === 404) {
return {
success: false,
message: '등록된 매장이 없습니다',
isNewUser: true
}
}
if (error.response?.status >= 500) {
// 서버 에러도 신규 사용자로 간주 (매장이 없어서 발생할 수 있음)
console.log('서버 에러 발생, 신규 사용자로 간주')
return {
success: false,
message: '등록된 매장이 없습니다',
isNewUser: true
}
}
if (error.response?.status === 401) {
return {
success: false,
message: '로그인이 필요합니다',
needLogin: true
}
}
// 기타 모든 에러도 신규 사용자로 간주
return {
success: false,
message: '등록된 매장이 없습니다',
isNewUser: true
}
} finally {
this.loading = false
}
},
async getLoginRedirectPath() {
try { try {
// 스토어 서비스 임포트 const result = await this.fetchStoreInfo()
const { storeService } = await import('@/services/store')
console.log('매장 정보 API 호출')
const result = await storeService.getStore()
console.log('=== Store 스토어: API 응답 분석 ===')
console.log('Result:', result)
console.log('Result.success:', result.success)
console.log('Result.data:', result.data)
console.log('Result.message:', result.message)
if (result.success && result.data) { if (result.success && result.data) {
// 매장 정보가 있는 경우 return {
console.log('✅ 매장 정보 설정:', result.data) path: '/dashboard',
this.storeInfo = result.data message: `${result.data.storeName}에 오신 것을 환영합니다!`,
return { success: true, data: result.data } type: 'success'
}
} else { } else {
// 매장이 없거나 조회 실패한 경우 return {
console.log('⚠️ 매장 정보 없음 또는 조회 실패') path: '/store',
this.storeInfo = null message: '매장 정보를 등록하고 AI 마케팅을 시작해보세요!',
type: 'info'
if (result.message === '등록된 매장이 없습니다') {
return { success: false, message: '등록된 매장이 없습니다' }
} else {
return { success: false, message: result.message || '매장 정보 조회에 실패했습니다' }
} }
} }
} catch (error) { } catch (error) {
console.error('=== Store 스토어: 매장 정보 조회 실패 ===') return {
console.error('Error:', error) path: '/store',
message: '매장 정보를 확인할 수 없습니다. 매장 정보를 등록해주세요',
this.error = error.message type: 'warning'
this.storeInfo = null
// HTTP 상태 코드별 처리
if (error.response?.status === 404) {
return { success: false, message: '등록된 매장이 없습니다' }
} }
if (error.response?.status >= 500) {
return { success: false, message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }
}
if (error.response?.status === 401) {
return { success: false, message: '로그인이 필요합니다' }
}
return { success: false, message: error.message || '매장 정보 조회에 실패했습니다' }
} finally {
this.loading = false
} }
}, },

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff