Merge pull request #9 from won-ktds/front
login store menu function edit
This commit is contained in:
commit
ceea7139a4
@ -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>
|
||||||
@ -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) => {
|
||||||
|
|||||||
@ -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 || '',
|
||||||
|
|||||||
@ -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
Loading…
x
Reference in New Issue
Block a user