diff --git a/src/components/layout/MainLayout.vue b/src/components/layout/MainLayout.vue index 9f21ad8..e815c92 100644 --- a/src/components/layout/MainLayout.vue +++ b/src/components/layout/MainLayout.vue @@ -1,658 +1,301 @@ -//* src/components/layout/MainLayout.vue - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ snackbar.message }} - - - - {{ snackbar.actionText }} - - - - - - - - - + + + + + + + + + + + + {{ storeInfoStatus.message }} + + + + + + + {{ storeStore.storeInfo.storeName }} + + + {{ storeStore.storeInfo.businessType }} + + + + + + + + + + + + + + + + {{ item.title }} + + + + {{ item.description }} + + + + + + + + + + + 매장 등록 후 이용 가능 + + + + + + + + + {{ item.title }} + + + + {{ item.description }} + + + + + + + + + + + + + + + mdi-logout + 로그아웃 + + + + + + + + + + + AI 마케팅 + + + + + + + + mdi-account + {{ authStore.user.nickname }}님 + + + + + + + + + + + \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 68b8980..918fc33 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,8 +1,5 @@ -//* src/router/index.js -/** - * Vue Router 설정 - * 라우팅 및 네비게이션 가드 설정 - */ +// src/router/index.js - 완전히 수정된 버전 + import { createRouter, createWebHistory } from 'vue-router' // 뷰 컴포넌트 lazy loading @@ -15,7 +12,7 @@ const ContentManagementView = () => import('@/views/ContentManagementView.vue') const routes = [ { path: '/', - redirect: '/login', // 항상 로그인 페이지로 먼저 리다이렉트 + redirect: '/login', }, { path: '/login', @@ -32,6 +29,7 @@ const routes = [ component: DashboardView, meta: { requiresAuth: true, + requiresStore: true, // ✅ 매장 정보 필수 title: '대시보드', }, }, @@ -41,6 +39,7 @@ const routes = [ component: StoreManagementView, meta: { requiresAuth: true, + requiresStore: false, // ✅ 매장 정보 없어도 접근 가능 title: '매장 관리', }, }, @@ -50,6 +49,7 @@ const routes = [ component: ContentCreationView, meta: { requiresAuth: true, + requiresStore: true, // ✅ 매장 정보 필수 title: '콘텐츠 생성', }, }, @@ -59,12 +59,13 @@ const routes = [ component: ContentManagementView, meta: { requiresAuth: true, + requiresStore: true, // ✅ 매장 정보 필수 title: '콘텐츠 관리', }, }, { path: '/:pathMatch(.*)*', - redirect: '/login', // 404시 로그인으로 이동 + redirect: '/login', }, ] @@ -73,12 +74,12 @@ const router = createRouter({ routes, }) -// 네비게이션 가드 - 수정된 버전 +// ✅ 개선된 네비게이션 가드 router.beforeEach(async (to, from, next) => { console.log('=== 라우터 가드 실행 ===') console.log('이동 경로:', `${from.path} → ${to.path}`) - // Pinia 스토어를 동적으로 가져오기 (순환 참조 방지) + // Pinia 스토어를 동적으로 가져오기 const { useAuthStore } = await import('@/store/auth') const authStore = useAuthStore() @@ -89,19 +90,79 @@ router.beforeEach(async (to, from, next) => { console.log('토큰 존재:', !!authStore.token) console.log('사용자 정보:', authStore.user?.nickname) - // 인증이 필요한 페이지인지 확인 + // 1단계: 인증 체크 const requiresAuth = to.meta.requiresAuth !== false if (requiresAuth && !authStore.isAuthenticated) { - console.log('인증 필요 - 로그인 페이지로 이동') + console.log('🚫 인증 필요 - 로그인 페이지로 이동') next('/login') - } else if (to.path === '/login' && authStore.isAuthenticated) { - console.log('이미 로그인됨 - 대시보드로 이동') - next('/dashboard') - } else { - console.log('이동 허용:', to.path) - next() + return } + + 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) => { @@ -109,4 +170,4 @@ router.afterEach((to) => { document.title = to.meta.title ? `${to.meta.title} - AI 마케팅` : 'AI 마케팅' }) -export default router +export default router \ No newline at end of file diff --git a/src/services/store.js b/src/services/store.js index 94abee9..429c544 100644 --- a/src/services/store.js +++ b/src/services/store.js @@ -69,59 +69,78 @@ class StoreService { * @returns {Promise} 매장 정보 */ async getStore() { - try { - console.log('=== 매장 정보 조회 API 호출 ===') - - // URL 슬래시 문제 해결: 빈 문자열로 호출하여 '/api/store'가 되도록 함 - const response = await storeApi.get('') - - console.log('매장 정보 조회 API 응답:', response.data) - - // 백엔드 응답 구조 수정: 디버깅 결과에 맞게 처리 - if (response.data && response.data.status === 200 && response.data.data) { - console.log('✅ 매장 정보 조회 성공:', response.data.data) - return { - success: true, - message: response.data.message || '매장 정보를 조회했습니다.', - 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 || '매장 정보를 찾을 수 없습니다.') + try { + console.log('=== 매장 정보 조회 API 호출 ===') + + const response = await storeApi.get('') + + console.log('매장 정보 조회 API 응답:', response.data) + + // 성공 응답 처리 + if (response.data && response.data.status === 200 && response.data.data) { + console.log('✅ 매장 정보 조회 성공:', response.data.data) + return { + success: true, + message: response.data.message || '매장 정보를 조회했습니다.', + data: response.data.data } - } catch (error) { - console.error('매장 정보 조회 실패:', error) - - // 404 오류 처리 (매장이 없음) - if (error.response?.status === 404) { - return { - success: false, - message: '등록된 매장이 없습니다', - data: null - } + } else if (response.data && response.data.status === 404) { + // 매장이 없는 경우 + console.log('📝 등록된 매장이 없음 (정상)') + return { + success: false, + message: '등록된 매장이 없습니다', + data: null } - - // 500 오류 처리 (서버 내부 오류) - if (error.response?.status === 500) { - console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) - return { - success: false, - message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.', - data: null - } + } else { + console.log('예상치 못한 응답 구조:', response.data) + return { + success: false, + message: '등록된 매장이 없습니다', + data: null } - - return handleApiError(error) + } + } catch (error) { + console.log('매장 정보 조회 중 오류:', error.message) + + // 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: 매장 수정) @@ -140,10 +159,8 @@ class StoreService { businessType: storeData.businessType, address: storeData.address, phoneNumber: storeData.phoneNumber, - // ✅ 수정: businessHours 필드 처리 - businessHours: storeData.businessHours || `${storeData.openTime || '09:00'}-${storeData.closeTime || '21:00'}`, - // ✅ 수정: closedDays 필드 처리 - closedDays: storeData.closedDays || storeData.holidays || '', + businessHours: storeData.businessHours, // 그대로 전달 + closedDays: storeData.closedDays, // 그대로 전달 seatCount: parseInt(storeData.seatCount) || 0, instaAccounts: storeData.instaAccounts || '', blogAccounts: storeData.blogAccounts || '', diff --git a/src/store/index.js b/src/store/index.js index 98da823..b5cb2a1 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -12,7 +12,25 @@ export const useStoreStore = defineStore('store', { getters: { hasStoreInfo: (state) => !!state.storeInfo, 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: { @@ -20,62 +38,107 @@ export const useStoreStore = defineStore('store', { * 매장 정보 조회 */ async fetchStoreInfo() { - console.log('=== Store 스토어: 매장 정보 조회 시작 ===') - this.loading = true - this.error = null + console.log('=== Store 스토어: 매장 정보 조회 시작 ===') + this.loading = true + 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 { - // 스토어 서비스 임포트 - 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) + const result = await this.fetchStoreInfo() if (result.success && result.data) { - // 매장 정보가 있는 경우 - console.log('✅ 매장 정보 설정:', result.data) - this.storeInfo = result.data - return { success: true, data: result.data } + return { + path: '/dashboard', + message: `${result.data.storeName}에 오신 것을 환영합니다!`, + type: 'success' + } } else { - // 매장이 없거나 조회 실패한 경우 - console.log('⚠️ 매장 정보 없음 또는 조회 실패') - this.storeInfo = null - - if (result.message === '등록된 매장이 없습니다') { - return { success: false, message: '등록된 매장이 없습니다' } - } else { - return { success: false, message: result.message || '매장 정보 조회에 실패했습니다' } + return { + path: '/store', + message: '매장 정보를 등록하고 AI 마케팅을 시작해보세요!', + type: 'info' } } } catch (error) { - console.error('=== Store 스토어: 매장 정보 조회 실패 ===') - console.error('Error:', error) - - this.error = error.message - this.storeInfo = null - - // HTTP 상태 코드별 처리 - if (error.response?.status === 404) { - return { success: false, message: '등록된 매장이 없습니다' } + return { + path: '/store', + message: '매장 정보를 확인할 수 없습니다. 매장 정보를 등록해주세요', + type: 'warning' } - - 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 } }, diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index ef9a0ee..8fabc1b 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -1,62 +1,96 @@ -//* src/views/LoginView.vue - 수정된 회원가입 기능 +//* src/views/LoginView.vue - 모던하고 세련된 디자인으로 개선 + + + + + + + + - - - - AI 마케팅 - 소상공인을 위한 스마트 마케팅 솔루션 + + + + + + + + AI + 마케팅 + + + mdi-rocket-launch + 소상공인을 위한 스마트 마케팅 솔루션 + - - + + + 로그인 + 계정에 로그인하여 시작하세요 + + + - 로그인 - - + + 아이디 + + - + + 비밀번호 + + - + 비밀번호 찾기 @@ -68,201 +102,186 @@ v-if="loginError" type="error" variant="tonal" - class="mb-4" + class="error-alert" closable @click:close="loginError = ''" > + mdi-alert-circle-outline {{ loginError }} mdi-login 로그인 - - 계정이 없으신가요? - + + 아직 계정이 없으신가요? + 회원가입 - - - - - - mdi-information - 데모 계정 정보 - - - 아이디: test - - - 비밀번호: test1234! - - - 데모 계정 자동 입력 - - - - - - - 비밀번호 찾기 - - 등록하신 이메일 주소를 입력해주세요. + + + mdi-key-variant + 비밀번호 찾기 + + + 등록하신 이메일 주소를 입력해주세요. - + - 취소 - 전송 + + 취소 + + + 전송 + - - - 회원가입 - + + + mdi-account-plus-outline + 회원가입 + + mdi-close - - AI 마케팅 서비스에 오신 것을 환영합니다! + + + mdi-hand-wave + AI 마케팅 서비스에 오신 것을 환영합니다! + - - + + + 아이디 + + - - + + + 이메일 + + - + + 비밀번호 + + - + + 비밀번호 확인 + + - - + + 이름 + + - - - - - - - - 디버깅 정보: - 폼 유효성: {{ isSignupFormValid }} - 아이디 확인: {{ userIdChecked }} - 이메일 확인: {{ emailChecked }} - 가입 가능: {{ canSignup }} - 로딩 상태: {{ signupLoading }} - + mdi-alert-circle-outline {{ signupError }} @@ -270,21 +289,25 @@ v-if="signupSuccess" type="success" variant="tonal" - class="mb-4" + class="signup-alert" > + mdi-check-circle-outline {{ signupSuccess }} - + - 취소 + + 취소 + + mdi-account-plus 가입하기 @@ -325,20 +348,20 @@ const checkingEmail = ref(false) const userIdChecked = ref(false) const emailChecked = ref(false) -// 로그인 자격 증명 +// 로그인 자격 증명 - 빈 값으로 초기화 const credentials = ref({ - username: 'test', - password: 'test1234!', + username: '', + password: '', }) -// 회원가입 데이터 +// 회원가입 데이터 - 백엔드 검증 규칙에 맞는 기본값 const signupData = ref({ - userId: '', - password: '', + userId: '', // 4자 이상 + password: '', // 특수문자 포함 passwordConfirm: '', name: '', email: '', - businessNumber: '', + businessNumber: '', // 빈 문자열 또는 10자리 숫자 }) const fieldErrors = ref({ @@ -349,13 +372,9 @@ const fieldErrors = ref({ // 개발 모드 여부 const isDev = ref(import.meta.env.DEV) -// 가입 가능 여부 computed (중복체크 조건 임시 제거) +// 가입 가능 여부 computed const canSignup = computed(() => { - return isSignupFormValid.value && - !signupLoading.value - // 임시로 중복체크 조건 제거 - // userIdChecked.value && - // emailChecked.value && + return isSignupFormValid.value && !signupLoading.value }) // 로그인 유효성 검사 규칙 @@ -369,21 +388,21 @@ const passwordRules = [ (v) => (v && v.length >= 6) || '비밀번호는 6자 이상이어야 합니다', ] -// 회원가입 유효성 검사 규칙 +// 회원가입 유효성 검사 규칙 - 백엔드와 일치하도록 수정 const signupUserIdRules = [ (v) => !!v || '아이디를 입력해주세요', - (v) => (v && v.length >= 3) || '아이디는 3자 이상이어야 합니다', + (v) => (v && v.length >= 4) || '아이디는 4자 이상이어야 합니다', // 백엔드: 4자 이상 (v) => (v && v.length <= 20) || '아이디는 20자 이하여야 합니다', - (v) => /^[a-zA-Z0-9_]+$/.test(v) || '아이디는 영문, 숫자, _만 사용 가능합니다', + (v) => /^[a-zA-Z0-9]+$/.test(v) || '아이디는 영문과 숫자만 사용 가능합니다', // 백엔드: _는 불허 ] const signupPasswordRules = [ (v) => !!v || '비밀번호를 입력해주세요', (v) => (v && v.length >= 8) || '비밀번호는 8자 이상이어야 합니다', (v) => (v && v.length <= 20) || '비밀번호는 20자 이하여야 합니다', - // 임시로 완화된 규칙 (영문과 숫자만 체크) - (v) => /^(?=.*[a-zA-Z])(?=.*\d)[A-Za-z\d@$!%*?&]/.test(v) || - '영문과 숫자를 포함해야 합니다 (특수문자 선택사항)', + // 백엔드: 영문, 숫자, 특수문자(@$!%*?&) 모두 필수 + (v) => /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/.test(v) || + '영문, 숫자, 특수문자(@$!%*?&)를 모두 포함해야 합니다', ] const passwordConfirmRules = [ @@ -394,26 +413,21 @@ const passwordConfirmRules = [ const nameRules = [ (v) => !!v || '이름을 입력해주세요', (v) => (v && v.length >= 2) || '이름은 2자 이상이어야 합니다', - (v) => (v && v.length <= 10) || '이름은 10자 이하여야 합니다', + (v) => (v && v.length <= 50) || '이름은 50자 이하여야 합니다', // 백엔드: 50자 이하 ] const emailRules = [ (v) => !!v || '이메일을 입력해주세요', (v) => /.+@.+\..+/.test(v) || '올바른 이메일 형식이 아닙니다', + (v) => (v && v.length <= 100) || '이메일은 100자 이하여야 합니다', // 백엔드: 100자 이하 ] const businessNumberRules = [ - (v) => !v || v.length === 10 || '사업자 번호는 10자리여야 합니다', - (v) => !v || /^\d{10}$/.test(v) || '사업자 번호는 숫자만 입력 가능합니다', + // 백엔드: 입력시 정확히 10자리 숫자여야 함 (빈 값은 허용) + (v) => !v || (v.length === 10 && /^\d{10}$/.test(v)) || '사업자번호는 10자리 숫자여야 합니다', ] // 로그인 관련 메서드 -const fillDemoCredentials = () => { - credentials.value.username = 'test' - credentials.value.password = 'test1234!' - loginError.value = '' -} - const handleLogin = async () => { if (!isFormValid.value) return @@ -449,81 +463,10 @@ const handleForgotPassword = () => { forgotEmail.value = '' } -// 회원가입 관련 메서드 -const checkUserIdDuplicate = async () => { - console.log('아이디 중복 확인 시작:', signupData.value.userId) - - if (!signupData.value.userId || signupData.value.userId.length < 3) { - signupError.value = '아이디를 3자 이상 입력해주세요' - return - } - - checkingUserId.value = true - signupError.value = '' - - try { - const response = await memberApi.get(`/check-duplicate/user-id?userId=${encodeURIComponent(signupData.value.userId)}`) - - console.log('아이디 중복 확인 응답:', response.data) - - if (response.data.success) { - const isDuplicate = response.data.data.isDuplicate - if (isDuplicate) { - signupError.value = '이미 사용 중인 아이디입니다' - userIdChecked.value = false - } else { - userIdChecked.value = true - appStore.showSnackbar('사용 가능한 아이디입니다', 'success') - } - } - } catch (error) { - console.error('아이디 중복 확인 실패:', error) - signupError.value = '아이디 중복 확인에 실패했습니다' - userIdChecked.value = false - } finally { - checkingUserId.value = false - } -} - -const checkEmailDuplicate = async () => { - console.log('이메일 중복 확인 시작:', signupData.value.email) - - if (!signupData.value.email || !/.+@.+\..+/.test(signupData.value.email)) { - signupError.value = '올바른 이메일을 입력해주세요' - return - } - - checkingEmail.value = true - signupError.value = '' - - try { - const response = await memberApi.get(`/check-duplicate/email?email=${encodeURIComponent(signupData.value.email)}`) - - console.log('이메일 중복 확인 응답:', response.data) - - if (response.data.success) { - const isDuplicate = response.data.data.isDuplicate - if (isDuplicate) { - signupError.value = '이미 사용 중인 이메일입니다' - emailChecked.value = false - } else { - emailChecked.value = true - appStore.showSnackbar('사용 가능한 이메일입니다', 'success') - } - } - } catch (error) { - console.error('이메일 중복 확인 실패:', error) - signupError.value = '이메일 중복 확인에 실패했습니다' - emailChecked.value = false - } finally { - checkingEmail.value = false - } -} - +// 회원가입 관련 메서드 - 백엔드에 맞게 수정 const handleSignup = async () => { console.log('회원가입 시도 시작') console.log('가입 데이터:', signupData.value) - // console.log('중복 확인 상태 - 아이디:', userIdChecked.value, '이메일:', emailChecked.value) // 임시 주석 if (!canSignup.value) { signupError.value = '모든 필드를 올바르게 입력해주세요' @@ -539,8 +482,12 @@ const handleSignup = async () => { userId: signupData.value.userId, password: signupData.value.password, name: signupData.value.name, - email: signupData.value.email, - businessNumber: signupData.value.businessNumber || null, + email: signupData.value.email + } + + // 사업자번호가 입력되었고 유효한 경우에만 추가 + if (signupData.value.businessNumber && signupData.value.businessNumber.length === 10) { + requestData.businessNumber = signupData.value.businessNumber } console.log('회원가입 요청 데이터:', requestData) @@ -549,104 +496,524 @@ const handleSignup = async () => { console.log('회원가입 응답:', response.data) - // 백엔드 응답 구조에 맞게 수정 if (response.data.status === 200 || response.data.message?.includes('완료')) { - signupSuccess.value = '회원가입이 완료되었습니다! 로그인해주세요' + signupSuccess.value = '회원가입이 완료되었습니다!' - // 즉시 성공 메시지 표시 - appStore.showSnackbar('회원가입이 완료되었습니다', 'success') - - // 1초 후 다이얼로그 닫기 (더 빠르게) + // 성공 후 로그인 탭으로 전환하고 폼 초기화 setTimeout(() => { closeSignupDialog() - }, 1000) + appStore.showSnackbar('회원가입이 완료되었습니다. 로그인해주세요.', 'success') + }, 1500) } else { - signupError.value = response.data.message || '회원가입에 실패했습니다' + throw new Error(response.data.message || '회원가입에 실패했습니다.') } } catch (error) { console.error('회원가입 실패:', error) - if (error.response?.data?.message) { - signupError.value = error.response.data.message + if (error.response && error.response.status === 400) { + // 백엔드 validation 에러 처리 + const errorData = error.response.data + + if (errorData.data && typeof errorData.data === 'object') { + // 각 필드별 에러 메시지들을 하나의 문자열로 결합 + const errorMessages = [] + for (const [field, message] of Object.entries(errorData.data)) { + errorMessages.push(`${field}: ${message}`) + } + signupError.value = errorMessages.join('\n') || '입력값을 확인해주세요.' + } else { + signupError.value = errorData.message || '입력값 검증에 실패했습니다.' + } } else { - signupError.value = '회원가입에 실패했습니다. 다시 시도해주세요' + signupError.value = error.response?.data?.message || '회원가입에 실패했습니다.' } } finally { signupLoading.value = false } } +// 폼 초기화 함수 const closeSignupDialog = () => { showSignup.value = false + + // 폼 초기화 - 백엔드 검증에 맞는 기본값 signupData.value = { - userId: '', - password: '', - passwordConfirm: '', - name: '', - email: '', + userId: 'user01', + password: 'test1234!', + passwordConfirm: 'test1234!', + name: 'test', + email: 'test@test.com', businessNumber: '', } + signupError.value = '' signupSuccess.value = '' userIdChecked.value = false emailChecked.value = false + + // 폼 validation 초기화 + if (signupForm.value) { + signupForm.value.resetValidation() + } } -// 컴포넌트 마운트 시 실행 onMounted(() => { - if (authStore.isAuthenticated) { - router.push('/dashboard') - } + // 컴포넌트 마운트 시 필요한 초기화 작업 }) \ No newline at end of file diff --git a/src/views/StoreManagementView.vue b/src/views/StoreManagementView.vue index 053c85b..b86ad5e 100644 --- a/src/views/StoreManagementView.vue +++ b/src/views/StoreManagementView.vue @@ -1,3 +1,5 @@ +storemanagement vue백업 + @@ -162,7 +164,7 @@ mdi-calendar-off 휴무일 - {{ formatClosedDays(storeInfo.holidays) }} + {{ formatClosedDays(storeInfo.closedDays) }} @@ -243,119 +245,132 @@ - - + + + + + + + + + + + + + mdi-image-off + + + + + + + + - - - - - - - - - - - - - mdi-image-off - - - - - - - - 품절 - - - 추천 - - - - - - - - - - - - - - {{ menu.menuName || menu.name }} - - - {{ menu.available ? '판매중' : '품절' }} - - - - - {{ menu.description || '설명이 없습니다' }} - - - - - {{ menu.price ? menu.price.toLocaleString() : '0' }}원 - - - {{ menu.category }} - - - - - - + 품절 + + + 추천 + + + + + + + + + + {{ menu.menuName || menu.name }} + + + {{ menu.available ? '판매중' : '품절' }} + + + + + + {{ menu.description || '설명이 없습니다' }} + + + + + + {{ menu.price ? menu.price.toLocaleString() : '0' }}원 + + + {{ menu.category }} + + + + + + + 수정 + + + 삭제 + + + + + + + + @@ -934,6 +949,20 @@ const shouldShowImagePreview = computed(() => { return null }) +// ✅ 추가: Store 스토어의 메뉴 상태를 감시하여 자동 동기화 +watch(() => storeStore.menus, (newMenus) => { + console.log('Store 스토어 메뉴 상태 변경 감지:', newMenus) + menus.value = newMenus || [] +}, { immediate: true, deep: true }) + +// ✅ 추가: 탭 변경 시 메뉴 조회 +watch(currentTab, async (newTab) => { + if (newTab === 'menu' && storeStore.hasStoreInfo && menus.value.length === 0) { + console.log('메뉴 탭으로 전환, 메뉴 데이터 조회 시작') + await loadMenus() + } +}) + // ===== 유틸리티 함수들 ===== /** @@ -1013,13 +1042,28 @@ const checkMenuImages = () => { } const formatClosedDays = (closedDays) => { - if (!closedDays) return '미설정' + console.log('=== formatClosedDays 호출 ===') + console.log('입력값:', closedDays, '타입:', typeof closedDays) - if (typeof closedDays === 'string') { - return closedDays + if (!closedDays) { + console.log('closedDays가 없음 -> 미설정') + return '미설정' } - if (Array.isArray(closedDays)) { + // ✅ 문자열인 경우 (백엔드에서 받은 "thursday,saturday" 형태) + if (typeof closedDays === 'string') { + console.log('문자열 형태의 closedDays:', closedDays) + + // 빈 문자열인 경우 + if (closedDays.trim() === '') { + console.log('빈 문자열 -> 연중무휴') + return '연중무휴' + } + + // ✅ 쉼표로 구분된 문자열을 배열로 변환 후 한글로 변환 + const dayArray = closedDays.split(',').map(day => day.trim()).filter(day => day) + console.log('분리된 배열:', dayArray) + const dayNames = { 'monday': '월요일', 'tuesday': '화요일', @@ -1030,9 +1074,42 @@ const formatClosedDays = (closedDays) => { 'sunday': '일요일' } - return closedDays.map(day => dayNames[day] || day).join(', ') || '연중무휴' + const koreanDays = dayArray.map(day => dayNames[day] || day).filter(day => day) + console.log('한글 변환된 배열:', koreanDays) + + if (koreanDays.length === 0) { + return '연중무휴' + } + + const result = koreanDays.join(', ') + console.log('최종 결과:', result) + return result } + // ✅ 배열인 경우 (기존 로직 유지) + if (Array.isArray(closedDays)) { + console.log('배열 형태의 closedDays:', closedDays) + + const dayNames = { + 'monday': '월요일', + 'tuesday': '화요일', + 'wednesday': '수요일', + 'thursday': '목요일', + 'friday': '금요일', + 'saturday': '토요일', + 'sunday': '일요일' + } + + const koreanDays = closedDays.map(day => dayNames[day] || day).filter(day => day) + + if (koreanDays.length === 0) { + return '연중무휴' + } + + return koreanDays.join(', ') + } + + console.log('예상치 못한 형태 -> 미설정') return '미설정' } @@ -1290,8 +1367,15 @@ const editBasicInfo = () => { console.log('매장 정보 수정 시작') editMode.value = true - // 기존 매장 정보로 폼 데이터 설정 const store = storeInfo.value + + // ✅ closedDays 문자열을 holidays 배열로 변환 + let holidaysArray = [] + if (store.closedDays && typeof store.closedDays === 'string') { + holidaysArray = store.closedDays.split(',').map(day => day.trim()).filter(day => day) + console.log('closedDays 문자열을 배열로 변환:', store.closedDays, '->', holidaysArray) + } + formData.value = { storeName: store.storeName || '', businessType: store.businessType || '', @@ -1302,10 +1386,12 @@ const editBasicInfo = () => { blogUrl: store.blogAccounts || '', openTime: store.openTime || '09:00', closeTime: store.closeTime || '21:00', - holidays: store.holidays || [], + // ✅ 수정: closedDays 문자열을 holidays 배열로 변환 + holidays: holidaysArray, description: store.description || '' } + console.log('수정용 폼 데이터 설정:', formData.value) showCreateDialog.value = true } @@ -1436,16 +1522,19 @@ const saveStore = async () => { try { console.log('매장 정보 저장 시작') + const businessHours = `${formData.value.openTime || '09:00'}-${formData.value.closeTime || '21:00'}` + const closedDays = Array.isArray(formData.value.holidays) + ? formData.value.holidays.join(',') + : (formData.value.holidays || '') + // 백엔드 형식에 맞는 데이터 구조로 변환 const storeData = { storeName: formData.value.storeName, businessType: formData.value.businessType, address: formData.value.address, phoneNumber: formData.value.phoneNumber || '', - openTime: formData.value.openTime || '09:00', - closeTime: formData.value.closeTime || '21:00', - holidays: Array.isArray(formData.value.holidays) ? - formData.value.holidays.join(',') : '', + businessHours: businessHours, // ✅ 추가 + closedDays: closedDays, // ✅ 추가 seatCount: parseInt(formData.value.seatCount) || 0, instaAccounts: formData.value.instagramUrl || '', blogAccounts: formData.value.blogUrl || '', @@ -1638,8 +1727,9 @@ const loadMenus = async () => { const result = await storeStore.fetchMenus() if (result.success) { - menus.value = result.data - console.log('✅ 메뉴 데이터 로드 완료:', result.data) + // ✅ Store에서 조회한 메뉴를 로컬 상태에 동기화 + menus.value = storeStore.menus || [] + console.log('✅ 메뉴 데이터 로드 완료:', menus.value) } else { console.log('메뉴 데이터 없음 또는 로드 실패:', result.message) menus.value = [] @@ -1650,6 +1740,7 @@ const loadMenus = async () => { } } + // 개발 환경에서 전역으로 노출 if (process.env.NODE_ENV === 'development') { window.checkMenuImages = checkMenuImages @@ -1663,32 +1754,38 @@ onMounted(async () => { console.log('=== StoreManagementView 마운트됨 ===') // 플레이스홀더 이미지 확인 - await checkPlaceholderImage() + checkPlaceholderImage() try { + // 매장 정보 조회 시도 const result = await storeStore.fetchStoreInfo() - console.log('매장 정보 조회 결과:', result) - if (result.success) { - console.log('✅ 매장 정보 로드 완료:', result.data) - await loadMenus() + // ✅ 수정: 매장 정보 조회 성공 여부와 관계없이 에러 메시지 표시하지 않음 + if (result && result.success && result.data) { + console.log('✅ 매장 정보 조회 성공') + // ✅ 수정: 매장이 있을 때만 메뉴 조회 - storeStore.fetchMenus() 직접 호출 + console.log('메뉴 목록 조회 시작') + await storeStore.fetchMenus() - // 개발 환경에서 이미지 상태 확인 - if (process.env.NODE_ENV === 'development') { + // ✅ 추가: Store에서 조회한 메뉴를 로컬 상태에 동기화 + menus.value = storeStore.menus || [] + + // 개발 모드에서만 메뉴 이미지 확인 + if (import.meta.env.DEV && currentTab.value === 'menu') { setTimeout(checkMenuImages, 2000) } } else { - if (result.message === '등록된 매장이 없습니다') { - console.log('⚠️ 등록된 매장이 없음 - 등록 화면 표시') - } else { - console.warn('❌ 매장 정보 조회 실패:', result.message) - showSnackbar(result.message || '매장 정보를 불러오는데 실패했습니다', 'error') - } + // 매장 정보가 없는 경우 - 조용히 처리 + console.log('📝 매장 정보 없음 - 등록 화면 표시') + // 에러 메시지를 표시하지 않고 등록 화면만 보여줌 } } catch (error) { - console.error('매장 정보 조회 중 예외 발생:', error) - showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error') + // 예외 발생 시에도 조용히 처리 + console.log('매장 정보 조회 중 오류:', error.message) + + // 500 에러나 네트워크 에러가 발생해도 사용자에게는 등록 화면을 보여줌 + // 사용자 경험을 위해 에러 메시지 대신 등록 안내만 표시 } }) @@ -1798,6 +1895,9 @@ const getStoreColor = (businessType) => { \ No newline at end of file
소상공인을 위한 스마트 마케팅 솔루션
+ mdi-rocket-launch + 소상공인을 위한 스마트 마케팅 솔루션 +
계정에 로그인하여 시작하세요
등록하신 이메일 주소를 입력해주세요.
AI 마케팅 서비스에 오신 것을 환영합니다!
+ mdi-hand-wave + AI 마케팅 서비스에 오신 것을 환영합니다! +
- {{ menu.description || '설명이 없습니다' }} -
+ {{ menu.description || '설명이 없습니다' }} +