From 56546a7777da4b166054d5b1a741422e6132119a Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Jun 2025 14:15:45 +0900 Subject: [PATCH 1/4] temp release --- src/services/api.js | 2 +- src/store/index.js | 386 ++---- src/store/store.js | 404 +++--- src/views/StoreManagementView.vue | 2045 +++-------------------------- 4 files changed, 539 insertions(+), 2298 deletions(-) diff --git a/src/services/api.js b/src/services/api.js index 223b0f1..de8bf89 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -8,7 +8,7 @@ const getApiUrls = () => { GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', AUTH_URL: 'http://localhost:8081/api/auth', MEMBER_URL: 'http://localhost:8081/api/member', - STORE_URL: config.STORE_URL || 'http://20.1.2.3/api/store', + STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', CONTENT_URL: config.CONTENT_URL || 'http://20.1.2.3/api/content', MENU_URL: config.MENU_URL || 'http://20.1.2.3/api/menu', SALES_URL: config.SALES_URL || 'http://20.1.2.3/api/sales', diff --git a/src/store/index.js b/src/store/index.js index ae9412c..a74e198 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,285 +1,153 @@ -//* src/store/index.js -/** - * Pinia 스토어 설정 - * 전역 상태 관리 - */ +//* src/store/index.js - Store 스토어 수정 (매장 조회 부분) import { defineStore } from 'pinia' -import authService from '@/services/auth' -import storeService from '@/services/store' - -// 인증 스토어 -export const useAuthStore = defineStore('auth', { - state: () => ({ - user: null, - token: localStorage.getItem('token'), - refreshToken: localStorage.getItem('refreshToken'), - isAuthenticated: false - }), - - getters: { - getUserInfo: (state) => state.user, - isLoggedIn: (state) => state.isAuthenticated && !!state.token - }, - - actions: { - async login(credentials) { - try { - const response = await authService.login(credentials) - this.setAuth(response.data) - return response - } catch (error) { - this.clearAuth() - throw error - } - }, - - async register(userData) { - try { - const response = await authService.register(userData) - return response - } catch (error) { - throw error - } - }, - - async logout() { - try { - if (this.token) { - await authService.logout() - } - } catch (error) { - console.error('로그아웃 오류:', error) - } finally { - this.clearAuth() - } - }, - - async refreshUserInfo() { - try { - const response = await authService.getUserInfo() - this.user = response.data - this.isAuthenticated = true - return response - } catch (error) { - this.clearAuth() - throw error - } - }, - - setAuth(authData) { - this.user = authData.user - this.token = authData.accessToken - this.refreshToken = authData.refreshToken - this.isAuthenticated = true - - localStorage.setItem('token', authData.accessToken) - localStorage.setItem('refreshToken', authData.refreshToken) - }, - - clearAuth() { - this.user = null - this.token = null - this.refreshToken = null - this.isAuthenticated = false - - localStorage.removeItem('token') - localStorage.removeItem('refreshToken') - } - } -}) - -// 앱 전역 스토어 -export const useAppStore = defineStore('app', { - state: () => ({ - loading: false, - snackbar: { - show: false, - message: '', - color: 'success', - timeout: 3000 - }, - notifications: [], - notificationCount: 0 - }), - - actions: { - setLoading(status) { - this.loading = status - }, - - showSnackbar(message, color = 'success', timeout = 3000) { - this.snackbar = { - show: true, - message, - color, - timeout - } - }, - - hideSnackbar() { - this.snackbar.show = false - }, - - addNotification(notification) { - this.notifications.unshift({ - id: Date.now(), - timestamp: new Date(), - ...notification - }) - this.notificationCount = this.notifications.length - }, - - clearNotifications() { - this.notifications = [] - this.notificationCount = 0 - } - } -}) - -// 매장 스토어 +// 매장 스토어에 추가할 fetchStoreInfo 메서드 export const useStoreStore = defineStore('store', { state: () => ({ storeInfo: null, - loading: false - }), - - getters: { - hasStoreInfo: (state) => !!state.storeInfo - }, - - actions: { - setStoreInfo(storeInfo) { - this.storeInfo = storeInfo - }, - - async fetchStoreInfo() { - try { - this.loading = true - const response = await storeService.getStore() // getStoreInfo가 아닌 getStore - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async registerStore(storeData) { - try { - this.loading = true - const response = await storeService.registerStore(storeData) - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async updateStore(storeId, storeData) { - try { - this.loading = true - const response = await storeService.updateStore(storeId, storeData) - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async createStoreInfo(storeData) { - try { - this.loading = true - const response = await storeService.createStoreInfo(storeData) - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - } - } -}) - -// 메뉴 스토어 -export const useMenuStore = defineStore('menu', { - state: () => ({ - menus: [], loading: false, - totalCount: 0 + error: null }), getters: { - getMenuById: (state) => (id) => { - return state.menus.find(menu => menu.id === id) - }, - - getMenusByCategory: (state) => (category) => { - return state.menus.filter(menu => menu.category === category) - } + hasStoreInfo: (state) => !!state.storeInfo, + isLoading: (state) => state.loading }, actions: { - async fetchMenus() { + /** + * 매장 정보 조회 + */ + async fetchStoreInfo() { + console.log('=== Store 스토어: 매장 정보 조회 시작 ===') + this.loading = true + this.error = null + try { - this.loading = true - const response = await storeService.getMenus() - this.menus = response.data - this.totalCount = response.data.length - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async createMenu(menuData) { - try { - this.loading = true - const response = await storeService.createMenu(menuData) - this.menus.push(response.data) - this.totalCount++ - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async updateMenu(menuId, menuData) { - try { - this.loading = true - const response = await storeService.updateMenu(menuId, menuData) - const index = this.menus.findIndex(menu => menu.id === menuId) - if (index !== -1) { - this.menus[index] = response.data + // 스토어 서비스 임포트 + 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 + + if (result.message === '등록된 매장이 없습니다') { + return { success: false, message: '등록된 매장이 없습니다' } + } else { + return { success: false, message: result.message || '매장 정보 조회에 실패했습니다' } + } } - return response } catch (error) { - throw 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: '등록된 매장이 없습니다' } + } + + 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 } }, - - async deleteMenu(menuId) { + + /** + * 매장 등록 + */ + async registerStore(storeData) { + console.log('매장 등록 시작:', storeData) + this.loading = true + this.error = null + try { - this.loading = true - await storeService.deleteMenu(menuId) - this.menus = this.menus.filter(menu => menu.id !== menuId) - this.totalCount-- + const { storeService } = await import('@/services/store') + + const result = await storeService.registerStore(storeData) + + console.log('매장 등록 결과:', result) + + if (result.success) { + // 등록 성공 후 매장 정보 다시 조회 + await this.fetchStoreInfo() + return result + } else { + this.error = result.message + return result + } } catch (error) { - throw error + console.error('매장 등록 실패:', error) + this.error = error.message + return { success: false, message: error.message || '매장 등록에 실패했습니다' } } finally { this.loading = false } + }, + + /** + * 매장 정보 수정 + */ + async updateStore(storeId, storeData) { + console.log('매장 정보 수정 시작:', { storeId, storeData }) + this.loading = true + this.error = null + + try { + const { storeService } = await import('@/services/store') + + const result = await storeService.updateStore(storeId, storeData) + + console.log('매장 수정 결과:', result) + + if (result.success) { + // 수정 성공 후 매장 정보 다시 조회 + await this.fetchStoreInfo() + return result + } else { + this.error = result.message + return result + } + } catch (error) { + console.error('매장 수정 실패:', error) + this.error = error.message + return { success: false, message: error.message || '매장 수정에 실패했습니다' } + } finally { + this.loading = false + } + }, + + /** + * 매장 정보 초기화 + */ + clearStoreInfo() { + this.storeInfo = null + this.error = null + this.loading = false } } }) \ No newline at end of file diff --git a/src/store/store.js b/src/store/store.js index 138485c..45097a3 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,202 +1,266 @@ -//* src/store/store.js 수정 - 기존 구조 유지하고 API 연동만 추가 -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import storeService from '@/services/store' +//* src/services/store.js - 백엔드 API 연동 수정 +import { storeApi, handleApiError, formatSuccessResponse } from './api.js' -export const useStoreStore = defineStore('store', () => { - // 기존 상태들 유지 - const storeInfo = ref(null) - const menus = ref([]) - const salesData = ref(null) - const isLoading = ref(false) - - // 기존 computed 속성들 유지 - const hasStoreInfo = computed(() => !!storeInfo.value) - const menuCount = computed(() => menus.value?.length || 0) - - // fetchStoreInfo를 실제 API 호출로 수정 - const fetchStoreInfo = async () => { - if (import.meta.env.DEV) { - console.log('개발 모드: 매장 정보 API 호출 건너뛰기') - return { success: true } - } - - isLoading.value = true - +/** + * 매장 관련 API 서비스 + * 백엔드 Store Controller와 연동 (포트 8082) + */ +class StoreService { + /** + * 매장 등록 (STR-015: 매장 등록) + * @param {Object} storeData - 매장 정보 + * @returns {Promise} 매장 등록 결과 + */ + async registerStore(storeData) { try { - const result = await storeService.getStore() + console.log('매장 등록 API 호출 - 요청 데이터:', storeData) - if (result.success) { - storeInfo.value = result.data - return { success: true } - } else { - console.warn('매장 정보 조회 실패:', result.message) - return { success: false, error: result.message } - } - } catch (error) { - console.warn('매장 정보 조회 실패:', error) - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // saveStoreInfo를 실제 API 호출로 수정 - const saveStoreInfo = async (storeData) => { - isLoading.value = true - - try { - let result - if (storeInfo.value) { - // 기존 매장 정보 수정 - result = await storeService.updateStore(storeData) - } else { - // 새 매장 등록 - result = await storeService.registerStore(storeData) + // 백엔드 StoreCreateRequest에 맞는 형태로 변환 + const requestData = { + storeName: storeData.storeName, + businessType: storeData.businessType, + address: storeData.address, + phoneNumber: storeData.phoneNumber, + businessHours: storeData.businessHours || `${storeData.openTime}-${storeData.closeTime}`, + closedDays: Array.isArray(storeData.holidays) ? storeData.holidays.join(',') : storeData.closedDays, + seatCount: parseInt(storeData.seatCount) || 0, + snsAccounts: { + instagram: storeData.instagramUrl || '', + blog: storeData.blogUrl || '' + }, + description: storeData.description || '' } - if (result.success) { - storeInfo.value = result.data - return { success: true, message: '매장 정보가 저장되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // fetchMenus를 실제 API 호출로 수정 - const fetchMenus = async () => { - if (!storeInfo.value?.storeId) { - console.warn('매장 ID가 없어 메뉴를 조회할 수 없습니다.') - return { success: false, error: '매장 정보가 필요합니다.' } - } - - isLoading.value = true - - try { - const result = await storeService.getMenus(storeInfo.value.storeId) + console.log('백엔드 전송 데이터:', requestData) - if (result.success) { - menus.value = result.data - return { success: true } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // 메뉴 관련 메서드들 API 연동 추가 - const saveMenu = async (menuData) => { - isLoading.value = true - - try { - const result = await storeService.registerMenu(menuData) + const response = await storeApi.post('/register', requestData) - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 등록되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - const updateMenu = async (menuId, menuData) => { - isLoading.value = true - - try { - const result = await storeService.updateMenu(menuId, menuData) + console.log('매장 등록 API 응답:', response.data) - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 수정되었습니다.' } + // 백엔드 응답 구조에 맞게 처리 + if (response.data.status === 200 || response.data.message?.includes('성공')) { + return formatSuccessResponse(response.data.data, response.data.message || '매장이 등록되었습니다.') } else { - return { success: false, error: result.message } + throw new Error(response.data.message || '매장 등록에 실패했습니다.') } } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + console.error('매장 등록 실패:', error) + return handleApiError(error) } } - const deleteMenu = async (menuId) => { - isLoading.value = true - + /** + * 매장 정보 조회 (STR-005: 매장 정보 관리) + * @returns {Promise} 매장 정보 + */ + async getStore() { try { - const result = await storeService.deleteMenu(menuId) + console.log('매장 정보 조회 API 호출') - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 삭제되었습니다.' } + const response = await storeApi.get('/') + + console.log('매장 정보 조회 API 응답:', response.data) + + // 백엔드 응답 구조에 맞게 처리 + if (response.data.status === 200 && response.data.data) { + return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.') + } else if (response.data.data === null) { + // 매장이 없는 경우 + return { + success: false, + message: '등록된 매장이 없습니다', + data: null + } } else { - return { success: false, error: result.message } + throw new Error(response.data.message || '매장 정보를 찾을 수 없습니다.') } } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + console.error('매장 정보 조회 실패:', error) + + // 404 오류 처리 (매장이 없음) + if (error.response?.status === 404) { + return { + success: false, + message: '등록된 매장이 없습니다', + data: null + } + } + + // 500 오류 처리 (서버 내부 오류) + if (error.response?.status === 500) { + console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) + return { + success: false, + message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.', + data: null + } + } + + return handleApiError(error) } } - // 매출 정보 조회 추가 - const fetchSalesData = async () => { - if (!storeInfo.value?.storeId) { - return { success: false, error: '매장 정보가 필요합니다.' } - } - - isLoading.value = true - + /** + * 매장 정보 수정 (STR-010: 매장 수정) + * @param {number} storeId - 매장 ID (현재는 사용하지 않음 - JWT에서 사용자 확인) + * @param {Object} storeData - 수정할 매장 정보 + * @returns {Promise} 매장 수정 결과 + */ + async updateStore(storeId, storeData) { try { - const result = await storeService.getSales(storeInfo.value.storeId) + console.log('매장 정보 수정 API 호출 - 요청 데이터:', storeData) - if (result.success) { - salesData.value = result.data - return { success: true } + // 백엔드 StoreUpdateRequest에 맞는 형태로 변환 + const requestData = { + storeName: storeData.storeName, + businessType: storeData.businessType, + address: storeData.address, + phoneNumber: storeData.phoneNumber, + businessHours: storeData.businessHours || `${storeData.openTime}-${storeData.closeTime}`, + closedDays: Array.isArray(storeData.holidays) ? storeData.holidays.join(',') : storeData.closedDays, + seatCount: parseInt(storeData.seatCount) || 0, + snsAccounts: { + instagram: storeData.instagramUrl || '', + blog: storeData.blogUrl || '' + }, + description: storeData.description || '' + } + + console.log('백엔드 전송 데이터:', requestData) + + // PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음) + const response = await storeApi.put('/', requestData) + + console.log('매장 정보 수정 API 응답:', response.data) + + if (response.data.status === 200 || response.data.message?.includes('성공')) { + return formatSuccessResponse(response.data.data, response.data.message || '매장 정보가 수정되었습니다.') } else { - return { success: false, error: result.message } + throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.') } } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + console.error('매장 정보 수정 실패:', error) + return handleApiError(error) } } - return { - // 상태 - storeInfo, - menus, - salesData, - isLoading, - - // 컴퓨티드 - hasStoreInfo, - menuCount, - - // 메서드 - fetchStoreInfo, - saveStoreInfo, - fetchMenus, - saveMenu, - updateMenu, - deleteMenu, - fetchSalesData + /** + * 매출 정보 조회 (STR-020: 대시보드) + * @param {string} period - 조회 기간 (today, week, month, year) + * @returns {Promise} 매출 정보 + */ + async getSales(period = 'today') { + try { + // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) + const mockSalesData = { + todaySales: 150000, + yesterdaySales: 120000, + changeRate: 25.0, + monthlyTarget: 3000000, + achievementRate: 45.2 + } + + return formatSuccessResponse(mockSalesData, '매출 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } } -}) + /** + * 메뉴 등록 (STR-030: 메뉴 등록) + * @param {Object} menuData - 메뉴 정보 + * @returns {Promise} 메뉴 등록 결과 + */ + async registerMenu(menuData) { + try { + // 현재는 목업 처리 (추후 실제 API 연동 시 수정) + console.log('메뉴 등록 - 목업 처리:', menuData) + + const mockMenuResponse = { + id: Date.now(), + ...menuData, + createdAt: new Date().toISOString() + } + + return formatSuccessResponse(mockMenuResponse, '메뉴가 등록되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 메뉴 목록 조회 (STR-025: 메뉴 조회) + * @returns {Promise} 메뉴 목록 + */ + async getMenus() { + try { + // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) + const mockMenus = [ + { + id: 1, + name: '김치찌개', + category: '찌개', + price: 8000, + description: '푸짐한 김치찌개', + imageUrl: '/images/menu-placeholder.png', + isPopular: true + }, + { + id: 2, + name: '제육볶음', + category: '볶음', + price: 12000, + description: '매콤한 제육볶음', + imageUrl: '/images/menu-placeholder.png', + isRecommended: true + } + ] + + return formatSuccessResponse(mockMenus, '메뉴 목록을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 메뉴 수정 (STR-035: 메뉴 수정) + * @param {number} menuId - 메뉴 ID + * @param {Object} menuData - 수정할 메뉴 정보 + * @returns {Promise} 메뉴 수정 결과 + */ + async updateMenu(menuId, menuData) { + try { + // 현재는 목업 처리 (추후 실제 API 연동 시 수정) + console.log('메뉴 수정 - 목업 처리:', { menuId, menuData }) + + const mockMenuResponse = { + id: menuId, + ...menuData, + updatedAt: new Date().toISOString() + } + + return formatSuccessResponse(mockMenuResponse, '메뉴가 수정되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 메뉴 삭제 (STR-040: 메뉴 삭제) + * @param {number} menuId - 삭제할 메뉴 ID + * @returns {Promise} 메뉴 삭제 결과 + */ + async deleteMenu(menuId) { + try { + // 현재는 목업 처리 (추후 실제 API 연동 시 수정) + console.log('메뉴 삭제 - 목업 처리:', menuId) + + return formatSuccessResponse(null, '메뉴가 삭제되었습니다.') + } catch (error) { + return handleApiError(error) + } + } +} + +export const storeService = new StoreService() +export default storeService \ No newline at end of file diff --git a/src/views/StoreManagementView.vue b/src/views/StoreManagementView.vue index 7494f3f..5adaafe 100644 --- a/src/views/StoreManagementView.vue +++ b/src/views/StoreManagementView.vue @@ -1,4 +1,5 @@ -//* src/views/StoreManagementView.vue +//* src/views/StoreManagementView.vue - 데모 기능 제거 및 백엔드 연동 수정 + @@ -941,58 +247,24 @@ import { ref, computed, onMounted } from 'vue' import { useStoreStore } from '@/store/index' -/** - * AI 마케팅 서비스 - 매장 관리 페이지 - * 매장 정보 관리 및 메뉴 관리 기능 제공 - * 유저스토리: STR-005, STR-010, STR-015, STR-020, STR-025, STR-030, STR-035, STR-040 - */ - -// 스토어 const storeStore = useStoreStore() -// 탭 관리 +// 기존 상태들... const currentTab = ref('basic') - -// 매장 정보 관련 상태 const showCreateDialog = ref(false) const editMode = ref(false) const formValid = ref(false) const saving = ref(false) const imageInput = ref(null) +const storeForm = ref(null) -// 메뉴 관리 관련 상태 -const showMenuDialog = ref(false) -const editMenuMode = ref(false) -const menuFormValid = ref(false) -const savingMenu = ref(false) -const deletingMenu = ref(false) -const showDeleteMenuDialog = ref(false) -const deleteMenuTarget = ref(null) -const menuImageInput = ref(null) -const menuSearch = ref('') -const menuCategoryFilter = ref('전체') -const menuStatusFilter = ref('전체') - -// SNS 연동 관련 상태 -const snsCheckLoading = ref({ - instagram: false, - blog: false -}) -const showSnsResultDialog = ref(false) -const snsConnectionResult = ref({ - success: false, - platform: '', - message: '' -}) - -// 스낵바 상태 const snackbar = ref({ show: false, message: '', color: 'success' }) -// 폼 데이터 +// 기존 폼 데이터들... const formData = ref({ storeName: '', businessType: '', @@ -1011,892 +283,113 @@ const formData = ref({ imageUrl: '' }) -// 메뉴 폼 데이터 -const menuFormData = ref({ - menuName: '', - price: 0, - category: '', - description: '', - available: true, - recommended: false, - imageUrl: '' -}) - -// 메뉴 목록 (데모 데이터) -const menus = ref([ - { - id: 1, - menuName: '김치찌개', - category: '찌개류', - price: 8000, - description: '돼지고기와 신김치로 끓인 얼큰한 김치찌개', - available: true, - recommended: true, - imageUrl: '/images/kimchi-jjigae.jpg' - }, - { - id: 2, - menuName: '된장찌개', - category: '찌개류', - price: 7000, - description: '집된장으로 끓인 구수한 된장찌개', - available: true, - recommended: false, - imageUrl: '/images/doenjang-jjigae.jpg' - }, - { - id: 3, - menuName: '제육볶음', - category: '볶음류', - price: 12000, - description: '매콤달콤한 양념에 볶은 제육볶음', - available: false, - recommended: true, - imageUrl: '/images/jeyuk-bokkeum.jpg' - } -]) - // 컴퓨티드 속성 const storeInfo = computed(() => storeStore.storeInfo || {}) -const availableMenusCount = computed(() => - menus.value.filter(menu => menu.available).length -) - -const recommendedMenusCount = computed(() => - menus.value.filter(menu => menu.recommended).length -) - -const averagePrice = computed(() => { - if (menus.value.length === 0) return '0원' - const total = menus.value.reduce((sum, menu) => sum + menu.price, 0) - const average = Math.round(total / menus.value.length) - return formatCurrency(average) -}) - -const filteredMenus = computed(() => { - let filtered = menus.value - - // 검색 필터 - if (menuSearch.value) { - filtered = filtered.filter(menu => - menu.menuName.toLowerCase().includes(menuSearch.value.toLowerCase()) || - menu.description.toLowerCase().includes(menuSearch.value.toLowerCase()) - ) - } - - // 카테고리 필터 - if (menuCategoryFilter.value !== '전체') { - filtered = filtered.filter(menu => menu.category === menuCategoryFilter.value) - } - - // 상태 필터 - if (menuStatusFilter.value !== '전체') { - const isAvailable = menuStatusFilter.value === '판매중' - filtered = filtered.filter(menu => menu.available === isAvailable) - } - - return filtered -}) - -const menuCategories = computed(() => { - const categories = [...new Set(menus.value.map(menu => menu.category))] - return categories.sort() -}) - -// 선택 옵션 -const businessTypes = [ - '한식', '중식', '일식', '양식', '분식', '치킨', '피자', '버거', - '카페', '디저트', '술집', '기타' -] - -const daysOfWeek = [ - { title: '월요일', value: 'monday' }, - { title: '화요일', value: 'tuesday' }, - { title: '수요일', value: 'wednesday' }, - { title: '목요일', value: 'thursday' }, - { title: '금요일', value: 'friday' }, - { title: '토요일', value: 'saturday' }, - { title: '일요일', value: 'sunday' } -] - -// 유효성 검사 규칙 -const businessNumberRules = [ - v => !!v || '사업자등록번호를 입력해주세요', - v => /^\d{3}-\d{2}-\d{5}$/.test(v) || '올바른 사업자등록번호 형식이 아닙니다 (예: 123-45-67890)' -] - -const phoneRules = [ - v => !!v || '연락처를 입력해주세요', - v => /^[0-9-+\s()]+$/.test(v) || '올바른 연락처 형식이 아닙니다' -] - -const priceRules = [ - v => !!v || '가격을 입력해주세요', - v => v > 0 || '가격은 0보다 커야 합니다' -] - -// 유틸리티 함수 -const formatCurrency = (amount) => { - return new Intl.NumberFormat('ko-KR', { - style: 'currency', - currency: 'KRW' - }).format(amount) -} - -const formatHolidays = (holidays) => { - if (!holidays || holidays.length === 0) return '없음' - const dayNames = { - monday: '월요일', - tuesday: '화요일', - wednesday: '수요일', - thursday: '목요일', - friday: '금요일', - saturday: '토요일', - sunday: '일요일' - } - return holidays.map(day => dayNames[day]).join(', ') -} - -const getCategoryColor = (category) => { - const colors = { - '면류': 'orange', - '튀김류': 'amber', - '음료': 'blue', - '안주': 'green', - '디저트': 'pink', - '찌개류': 'red', - '볶음류': 'purple' - } - return colors[category] || 'grey' -} - -// 메서드 - -/** - * SNS 계정 연동 확인 - * @param {string} platform - SNS 플랫폼 (instagram, blog) - */ -const checkSnsConnection = async (platform) => { - console.log(`${platform} 연동 확인 시작`) +// 유틸리티 함수들 +const formatClosedDays = (closedDays) => { + if (!closedDays) return '미설정' - snsCheckLoading.value[platform] = true + if (typeof closedDays === 'string') { + return closedDays + } - try { - // 개발 모드에서는 빠른 시뮬레이션 - const delay = import.meta.env.DEV ? 1000 : 2000 - await new Promise(resolve => setTimeout(resolve, delay)) - - // 랜덤하게 성공/실패 결정 (실제로는 API 응답에 따라 결정) - const isSuccess = Math.random() > 0.3 - - snsConnectionResult.value.success = isSuccess - snsConnectionResult.value.platform = platform - - if (isSuccess) { - snsConnectionResult.value.message = platform === 'instagram' - ? '인스타그램 계정 연동이 확인되었습니다!' - : '네이버 블로그 연동이 확인되었습니다!' - } else { - snsConnectionResult.value.message = platform === 'instagram' - ? '인스타그램 계정을 찾을 수 없거나 연동할 수 없습니다. URL을 확인해주세요.' - : '네이버 블로그를 찾을 수 없거나 연동할 수 없습니다. URL을 확인해주세요.' + if (Array.isArray(closedDays)) { + const dayNames = { + 'monday': '월요일', + 'tuesday': '화요일', + 'wednesday': '수요일', + 'thursday': '목요일', + 'friday': '금요일', + 'saturday': '토요일', + 'sunday': '일요일' } - showSnsResultDialog.value = true - - } catch (error) { - console.error('SNS 연동 확인 중 오류:', error) - snsConnectionResult.value.success = false - snsConnectionResult.value.message = '연동 확인 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' - showSnsResultDialog.value = true - } finally { - snsCheckLoading.value[platform] = false - } -} - -/** - * 데모 매장 데이터 로드 (개발용) - */ -const loadDemoStoreData = () => { - console.log('데모 매장 데이터 로드') - - // 데모 매장 정보 설정 - const demoStoreInfo = { - id: 1, - storeName: '김씨네 분식점', - businessType: '분식', - ownerName: '김점주', - businessNumber: '123-45-67890', - address: '서울특별시 강남구 테헤란로 123', - phoneNumber: '02-1234-5678', - seatCount: 20, - instagramUrl: '@kimfood_bunsik', - blogUrl: 'blog.naver.com/kimfood123', - openTime: '09:00', - closeTime: '21:00', - holidays: ['sunday'], - deliveryAvailable: true, - takeoutAvailable: true, - imageUrl: '/images/store-demo.jpg' + return closedDays.map(day => dayNames[day] || day).join(', ') || '연중무휴' } - // 스토어에 데모 데이터 직접 설정 - storeStore.storeInfo = demoStoreInfo - - showSnackbar('데모 매장 정보가 로드되었습니다', 'success') + return '미설정' } -/** - * 기본 정보 수정 모드로 전환 - */ -const editBasicInfo = () => { - editMode.value = true +const getSnsAccount = (platform) => { + const snsAccounts = storeInfo.value.snsAccounts - // 현재 매장 정보를 폼 데이터에 복사 - Object.assign(formData.value, { - storeName: storeInfo.value.storeName || '', - businessType: storeInfo.value.businessType || '', - ownerName: storeInfo.value.ownerName || '', - businessNumber: storeInfo.value.businessNumber || '', - address: storeInfo.value.address || '', - phoneNumber: storeInfo.value.phoneNumber || '', - seatCount: storeInfo.value.seatCount || 0, - instagramUrl: storeInfo.value.instagramUrl || '', - blogUrl: storeInfo.value.blogUrl || '', - openTime: storeInfo.value.openTime || '09:00', - closeTime: storeInfo.value.closeTime || '21:00', - holidays: storeInfo.value.holidays || [], - deliveryAvailable: storeInfo.value.deliveryAvailable || false, - takeoutAvailable: storeInfo.value.takeoutAvailable || true, - imageUrl: storeInfo.value.imageUrl || '' - }) + if (!snsAccounts) return null - showCreateDialog.value = true -} - -/** - * 이미지 선택 - */ -const selectImage = () => { - imageInput.value?.click() -} - -/** - * 이미지 업로드 처리 - */ -const handleImageUpload = (event) => { - const file = event.target.files[0] - if (file) { - // 실제로는 서버에 업로드하고 URL을 받아옴 - const reader = new FileReader() - reader.onload = (e) => { - formData.value.imageUrl = e.target.result + if (typeof snsAccounts === 'string') { + try { + const parsed = JSON.parse(snsAccounts) + return parsed[platform] + } catch { + return null } - reader.readAsDataURL(file) } -} - -/** - * 매장 정보 저장 - */ -const saveStoreInfo = async () => { - if (!storeForm.value?.validate()) return - - console.log('매장 정보 저장:', storeFormData.value) - try { - const result = await storeStore.saveStoreInfo(storeFormData.value) - - if (result.success) { - appStore.showSnackbar(result.message || '매장 정보가 저장되었습니다', 'success') - showStoreForm.value = false - } else { - appStore.showSnackbar(result.error || '저장에 실패했습니다', 'error') - } - } catch (error) { - console.error('매장 정보 저장 실패:', error) - appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') + if (typeof snsAccounts === 'object') { + return snsAccounts[platform] } -} - -/** - * 다이얼로그 닫기 - */ -const closeDialog = () => { - showCreateDialog.value = false - editMode.value = false - // 폼 데이터 초기화 - Object.assign(formData.value, { - storeName: '', - businessType: '', - ownerName: '', - businessNumber: '', - address: '', - phoneNumber: '', - seatCount: 0, - instagramUrl: '', - blogUrl: '', - openTime: '09:00', - closeTime: '21:00', - holidays: [], - deliveryAvailable: false, - takeoutAvailable: true, - imageUrl: '' - }) + return null } -// 메뉴 관리 메서드 - -/** - * 메뉴 등록 다이얼로그 열기 - */ -const openCreateMenuDialog = () => { - editMenuMode.value = false - resetMenuForm() - showMenuDialog.value = true -} - -/** - * 메뉴 수정 - */ -const editMenu = (menu) => { - editMenuMode.value = true - Object.assign(menuFormData.value, menu) - showMenuDialog.value = true -} - -/** - * 메뉴 삭제 확인 - */ -const confirmDeleteMenu = (menu) => { - deleteMenuTarget.value = menu - showDeleteMenuDialog.value = true -} - -/** - * 메뉴 이미지 선택 - */ -const selectMenuImage = () => { - menuImageInput.value?.click() -} - -/** - * 메뉴 이미지 업로드 처리 - */ -const handleMenuImageUpload = (event) => { - const file = event.target.files[0] - if (file) { - const reader = new FileReader() - reader.onload = (e) => { - menuFormData.value.imageUrl = e.target.result - } - reader.readAsDataURL(file) - } -} - -/** - * 메뉴 저장 - */ -const saveMenu = async () => { - if (!menuForm.value?.validate()) return - - console.log('메뉴 저장:', menuFormData.value) - - try { - let result - if (isMenuEdit.value && editingMenuId.value) { - result = await storeStore.updateMenu(editingMenuId.value, menuFormData.value) - } else { - result = await storeStore.saveMenu(menuFormData.value) - } - - if (result.success) { - appStore.showSnackbar(result.message || '메뉴가 저장되었습니다', 'success') - showMenuForm.value = false - resetMenuForm() - } else { - appStore.showSnackbar(result.error || '저장에 실패했습니다', 'error') - } - } catch (error) { - console.error('메뉴 저장 실패:', error) - appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') - } -} - -/** - * 메뉴 삭제 - */ -const deleteMenu = async (menuId) => { - try { - const result = await storeStore.deleteMenu(menuId) - - if (result.success) { - appStore.showSnackbar(result.message || '메뉴가 삭제되었습니다', 'success') - } else { - appStore.showSnackbar(result.error || '삭제에 실패했습니다', 'error') - } - } catch (error) { - console.error('메뉴 삭제 실패:', error) - appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') - } -} - -/** - * 메뉴 다이얼로그 닫기 - */ -const closeMenuDialog = () => { - showMenuDialog.value = false - editMenuMode.value = false - resetMenuForm() -} - -/** - * 메뉴 폼 초기화 - */ -const resetMenuForm = () => { - Object.assign(menuFormData.value, { - menuName: '', - price: 0, - category: '', - description: '', - available: true, - recommended: false, - imageUrl: '' - }) -} - -/** - * 필터 초기화 - */ -const clearFilters = () => { - menuSearch.value = '' - menuCategoryFilter.value = '전체' - menuStatusFilter.value = '전체' -} - -/** - * 스낵바 표시 - */ const showSnackbar = (message, color = 'success') => { snackbar.value.message = message snackbar.value.color = color snackbar.value.show = true } -// 메뉴 상세 다이얼로그 관련 상태 (기존 상태들에 추가) -const showMenuDetailDialog = ref(false) -const selectedMenuDetail = ref(null) - -// 메뉴 관리 메서드에 추가할 함수들 - /** - * 메뉴 상세보기 - */ -const viewMenuDetail = (menu) => { - selectedMenuDetail.value = { ...menu } - showMenuDetailDialog.value = true -} - -/** - * 상세화면에서 수정 모드로 전환 - */ -const editFromDetail = () => { - showMenuDetailDialog.value = false - editMenuMode.value = true - Object.assign(menuFormData.value, selectedMenuDetail.value) - showMenuDialog.value = true -} - -/** - * 컴포넌트 마운트 시 실행 + * 컴포넌트 마운트 시 실행 (데모 기능 제거, 백엔드 연동만) */ onMounted(async () => { - console.log('StoreManagementView 마운트됨') + console.log('=== StoreManagementView 마운트됨 ===') try { - // 매장 정보 로드 - if (!storeStore.hasStoreInfo) { - await storeStore.fetchStoreInfo() + // 매장 정보 조회 + const result = await storeStore.fetchStoreInfo() + + console.log('매장 정보 조회 결과:', result) + + if (result.success) { + console.log('✅ 매장 정보 로드 완료:', result.data) + showSnackbar('매장 정보를 불러왔습니다', 'success') + } else { + if (result.message === '등록된 매장이 없습니다') { + console.log('⚠️ 등록된 매장이 없음 - 등록 화면 표시') + // 매장이 없는 경우는 정상적인 상황이므로 에러 메시지 표시하지 않음 + } else { + console.warn('❌ 매장 정보 조회 실패:', result.message) + showSnackbar(result.message || '매장 정보를 불러오는데 실패했습니다', 'error') + } } - - // 메뉴 목록 로드 - await storeStore.fetchMenus() - } catch (error) { - console.warn('매장 관리 데이터 로드 실패 (개발 중이므로 무시):', error) + console.error('매장 정보 조회 중 예외 발생:', error) + showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error') } }) + +// 기존 메서드들 유지 (데모 데이터 로드 함수만 제거) +// loadDemoStoreData 함수 제거 + +// 나머지 함수들은 기존과 동일하게 유지... \ No newline at end of file From edaf5da6ed1fa764755e7d09328a8704456e6563 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Jun 2025 14:39:16 +0900 Subject: [PATCH 2/4] Revert "temp release" This reverts commit 56546a7777da4b166054d5b1a741422e6132119a. --- src/services/api.js | 2 +- src/store/index.js | 372 ++++-- src/store/store.js | 378 +++--- src/views/StoreManagementView.vue | 2029 ++++++++++++++++++++++++++--- 4 files changed, 2270 insertions(+), 511 deletions(-) diff --git a/src/services/api.js b/src/services/api.js index de8bf89..223b0f1 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -8,7 +8,7 @@ const getApiUrls = () => { GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', AUTH_URL: 'http://localhost:8081/api/auth', MEMBER_URL: 'http://localhost:8081/api/member', - STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', + STORE_URL: config.STORE_URL || 'http://20.1.2.3/api/store', CONTENT_URL: config.CONTENT_URL || 'http://20.1.2.3/api/content', MENU_URL: config.MENU_URL || 'http://20.1.2.3/api/menu', SALES_URL: config.SALES_URL || 'http://20.1.2.3/api/sales', diff --git a/src/store/index.js b/src/store/index.js index a74e198..ae9412c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,153 +1,285 @@ -//* src/store/index.js - Store 스토어 수정 (매장 조회 부분) +//* src/store/index.js +/** + * Pinia 스토어 설정 + * 전역 상태 관리 + */ import { defineStore } from 'pinia' -// 매장 스토어에 추가할 fetchStoreInfo 메서드 -export const useStoreStore = defineStore('store', { +import authService from '@/services/auth' +import storeService from '@/services/store' + +// 인증 스토어 +export const useAuthStore = defineStore('auth', { state: () => ({ - storeInfo: null, - loading: false, - error: null + user: null, + token: localStorage.getItem('token'), + refreshToken: localStorage.getItem('refreshToken'), + isAuthenticated: false }), getters: { - hasStoreInfo: (state) => !!state.storeInfo, - isLoading: (state) => state.loading + getUserInfo: (state) => state.user, + isLoggedIn: (state) => state.isAuthenticated && !!state.token }, actions: { - /** - * 매장 정보 조회 - */ + async login(credentials) { + try { + const response = await authService.login(credentials) + this.setAuth(response.data) + return response + } catch (error) { + this.clearAuth() + throw error + } + }, + + async register(userData) { + try { + const response = await authService.register(userData) + return response + } catch (error) { + throw error + } + }, + + async logout() { + try { + if (this.token) { + await authService.logout() + } + } catch (error) { + console.error('로그아웃 오류:', error) + } finally { + this.clearAuth() + } + }, + + async refreshUserInfo() { + try { + const response = await authService.getUserInfo() + this.user = response.data + this.isAuthenticated = true + return response + } catch (error) { + this.clearAuth() + throw error + } + }, + + setAuth(authData) { + this.user = authData.user + this.token = authData.accessToken + this.refreshToken = authData.refreshToken + this.isAuthenticated = true + + localStorage.setItem('token', authData.accessToken) + localStorage.setItem('refreshToken', authData.refreshToken) + }, + + clearAuth() { + this.user = null + this.token = null + this.refreshToken = null + this.isAuthenticated = false + + localStorage.removeItem('token') + localStorage.removeItem('refreshToken') + } + } +}) + +// 앱 전역 스토어 +export const useAppStore = defineStore('app', { + state: () => ({ + loading: false, + snackbar: { + show: false, + message: '', + color: 'success', + timeout: 3000 + }, + notifications: [], + notificationCount: 0 + }), + + actions: { + setLoading(status) { + this.loading = status + }, + + showSnackbar(message, color = 'success', timeout = 3000) { + this.snackbar = { + show: true, + message, + color, + timeout + } + }, + + hideSnackbar() { + this.snackbar.show = false + }, + + addNotification(notification) { + this.notifications.unshift({ + id: Date.now(), + timestamp: new Date(), + ...notification + }) + this.notificationCount = this.notifications.length + }, + + clearNotifications() { + this.notifications = [] + this.notificationCount = 0 + } + } +}) + +// 매장 스토어 +export const useStoreStore = defineStore('store', { + state: () => ({ + storeInfo: null, + loading: false + }), + + getters: { + hasStoreInfo: (state) => !!state.storeInfo + }, + + actions: { + setStoreInfo(storeInfo) { + this.storeInfo = storeInfo + }, + async fetchStoreInfo() { - 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 - - if (result.message === '등록된 매장이 없습니다') { - return { success: false, message: '등록된 매장이 없습니다' } - } else { - return { success: false, message: result.message || '매장 정보 조회에 실패했습니다' } - } - } + this.loading = true + const response = await storeService.getStore() // getStoreInfo가 아닌 getStore + this.storeInfo = response.data + return response } 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: '등록된 매장이 없습니다' } - } - - if (error.response?.status >= 500) { - return { success: false, message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' } - } - - if (error.response?.status === 401) { - return { success: false, message: '로그인이 필요합니다' } - } - - return { success: false, message: error.message || '매장 정보 조회에 실패했습니다' } + throw error } finally { this.loading = false } }, - - /** - * 매장 등록 - */ + async registerStore(storeData) { - console.log('매장 등록 시작:', storeData) - this.loading = true - this.error = null - try { - const { storeService } = await import('@/services/store') - - const result = await storeService.registerStore(storeData) - - console.log('매장 등록 결과:', result) - - if (result.success) { - // 등록 성공 후 매장 정보 다시 조회 - await this.fetchStoreInfo() - return result - } else { - this.error = result.message - return result - } + this.loading = true + const response = await storeService.registerStore(storeData) + this.storeInfo = response.data + return response } catch (error) { - console.error('매장 등록 실패:', error) - this.error = error.message - return { success: false, message: error.message || '매장 등록에 실패했습니다' } + throw error } finally { this.loading = false } }, - - /** - * 매장 정보 수정 - */ + async updateStore(storeId, storeData) { - console.log('매장 정보 수정 시작:', { storeId, storeData }) - this.loading = true - this.error = null - try { - const { storeService } = await import('@/services/store') - - const result = await storeService.updateStore(storeId, storeData) - - console.log('매장 수정 결과:', result) - - if (result.success) { - // 수정 성공 후 매장 정보 다시 조회 - await this.fetchStoreInfo() - return result - } else { - this.error = result.message - return result - } + this.loading = true + const response = await storeService.updateStore(storeId, storeData) + this.storeInfo = response.data + return response } catch (error) { - console.error('매장 수정 실패:', error) - this.error = error.message - return { success: false, message: error.message || '매장 수정에 실패했습니다' } + throw error } finally { this.loading = false } }, + + async createStoreInfo(storeData) { + try { + this.loading = true + const response = await storeService.createStoreInfo(storeData) + this.storeInfo = response.data + return response + } catch (error) { + throw error + } finally { + this.loading = false + } + } + } +}) - /** - * 매장 정보 초기화 - */ - clearStoreInfo() { - this.storeInfo = null - this.error = null - this.loading = false +// 메뉴 스토어 +export const useMenuStore = defineStore('menu', { + state: () => ({ + menus: [], + loading: false, + totalCount: 0 + }), + + getters: { + getMenuById: (state) => (id) => { + return state.menus.find(menu => menu.id === id) + }, + + getMenusByCategory: (state) => (category) => { + return state.menus.filter(menu => menu.category === category) + } + }, + + actions: { + async fetchMenus() { + try { + this.loading = true + const response = await storeService.getMenus() + this.menus = response.data + this.totalCount = response.data.length + return response + } catch (error) { + throw error + } finally { + this.loading = false + } + }, + + async createMenu(menuData) { + try { + this.loading = true + const response = await storeService.createMenu(menuData) + this.menus.push(response.data) + this.totalCount++ + return response + } catch (error) { + throw error + } finally { + this.loading = false + } + }, + + async updateMenu(menuId, menuData) { + try { + this.loading = true + const response = await storeService.updateMenu(menuId, menuData) + const index = this.menus.findIndex(menu => menu.id === menuId) + if (index !== -1) { + this.menus[index] = response.data + } + return response + } catch (error) { + throw error + } finally { + this.loading = false + } + }, + + async deleteMenu(menuId) { + try { + this.loading = true + await storeService.deleteMenu(menuId) + this.menus = this.menus.filter(menu => menu.id !== menuId) + this.totalCount-- + } catch (error) { + throw error + } finally { + this.loading = false + } } } }) \ No newline at end of file diff --git a/src/store/store.js b/src/store/store.js index 45097a3..138485c 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,266 +1,202 @@ -//* src/services/store.js - 백엔드 API 연동 수정 -import { storeApi, handleApiError, formatSuccessResponse } from './api.js' +//* src/store/store.js 수정 - 기존 구조 유지하고 API 연동만 추가 +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import storeService from '@/services/store' -/** - * 매장 관련 API 서비스 - * 백엔드 Store Controller와 연동 (포트 8082) - */ -class StoreService { - /** - * 매장 등록 (STR-015: 매장 등록) - * @param {Object} storeData - 매장 정보 - * @returns {Promise} 매장 등록 결과 - */ - async registerStore(storeData) { +export const useStoreStore = defineStore('store', () => { + // 기존 상태들 유지 + const storeInfo = ref(null) + const menus = ref([]) + const salesData = ref(null) + const isLoading = ref(false) + + // 기존 computed 속성들 유지 + const hasStoreInfo = computed(() => !!storeInfo.value) + const menuCount = computed(() => menus.value?.length || 0) + + // fetchStoreInfo를 실제 API 호출로 수정 + const fetchStoreInfo = async () => { + if (import.meta.env.DEV) { + console.log('개발 모드: 매장 정보 API 호출 건너뛰기') + return { success: true } + } + + isLoading.value = true + try { - console.log('매장 등록 API 호출 - 요청 데이터:', storeData) + const result = await storeService.getStore() - // 백엔드 StoreCreateRequest에 맞는 형태로 변환 - const requestData = { - storeName: storeData.storeName, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours || `${storeData.openTime}-${storeData.closeTime}`, - closedDays: Array.isArray(storeData.holidays) ? storeData.holidays.join(',') : storeData.closedDays, - seatCount: parseInt(storeData.seatCount) || 0, - snsAccounts: { - instagram: storeData.instagramUrl || '', - blog: storeData.blogUrl || '' - }, - description: storeData.description || '' - } - - console.log('백엔드 전송 데이터:', requestData) - - const response = await storeApi.post('/register', requestData) - - console.log('매장 등록 API 응답:', response.data) - - // 백엔드 응답 구조에 맞게 처리 - if (response.data.status === 200 || response.data.message?.includes('성공')) { - return formatSuccessResponse(response.data.data, response.data.message || '매장이 등록되었습니다.') + if (result.success) { + storeInfo.value = result.data + return { success: true } } else { - throw new Error(response.data.message || '매장 등록에 실패했습니다.') + console.warn('매장 정보 조회 실패:', result.message) + return { success: false, error: result.message } } } catch (error) { - console.error('매장 등록 실패:', error) - return handleApiError(error) + console.warn('매장 정보 조회 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } - /** - * 매장 정보 조회 (STR-005: 매장 정보 관리) - * @returns {Promise} 매장 정보 - */ - async getStore() { + // saveStoreInfo를 실제 API 호출로 수정 + const saveStoreInfo = async (storeData) => { + isLoading.value = true + try { - console.log('매장 정보 조회 API 호출') - - const response = await storeApi.get('/') - - console.log('매장 정보 조회 API 응답:', response.data) - - // 백엔드 응답 구조에 맞게 처리 - if (response.data.status === 200 && response.data.data) { - return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.') - } else if (response.data.data === null) { - // 매장이 없는 경우 - return { - success: false, - message: '등록된 매장이 없습니다', - data: null - } + let result + if (storeInfo.value) { + // 기존 매장 정보 수정 + result = await storeService.updateStore(storeData) } else { - throw new Error(response.data.message || '매장 정보를 찾을 수 없습니다.') - } - } catch (error) { - console.error('매장 정보 조회 실패:', error) - - // 404 오류 처리 (매장이 없음) - if (error.response?.status === 404) { - return { - success: false, - message: '등록된 매장이 없습니다', - data: null - } + // 새 매장 등록 + result = await storeService.registerStore(storeData) } - // 500 오류 처리 (서버 내부 오류) - if (error.response?.status === 500) { - console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) - return { - success: false, - message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.', - data: null - } - } - - return handleApiError(error) - } - } - - /** - * 매장 정보 수정 (STR-010: 매장 수정) - * @param {number} storeId - 매장 ID (현재는 사용하지 않음 - JWT에서 사용자 확인) - * @param {Object} storeData - 수정할 매장 정보 - * @returns {Promise} 매장 수정 결과 - */ - async updateStore(storeId, storeData) { - try { - console.log('매장 정보 수정 API 호출 - 요청 데이터:', storeData) - - // 백엔드 StoreUpdateRequest에 맞는 형태로 변환 - const requestData = { - storeName: storeData.storeName, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours || `${storeData.openTime}-${storeData.closeTime}`, - closedDays: Array.isArray(storeData.holidays) ? storeData.holidays.join(',') : storeData.closedDays, - seatCount: parseInt(storeData.seatCount) || 0, - snsAccounts: { - instagram: storeData.instagramUrl || '', - blog: storeData.blogUrl || '' - }, - description: storeData.description || '' - } - - console.log('백엔드 전송 데이터:', requestData) - - // PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음) - const response = await storeApi.put('/', requestData) - - console.log('매장 정보 수정 API 응답:', response.data) - - if (response.data.status === 200 || response.data.message?.includes('성공')) { - return formatSuccessResponse(response.data.data, response.data.message || '매장 정보가 수정되었습니다.') + if (result.success) { + storeInfo.value = result.data + return { success: true, message: '매장 정보가 저장되었습니다.' } } else { - throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.') + return { success: false, error: result.message } } } catch (error) { - console.error('매장 정보 수정 실패:', error) - return handleApiError(error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } - /** - * 매출 정보 조회 (STR-020: 대시보드) - * @param {string} period - 조회 기간 (today, week, month, year) - * @returns {Promise} 매출 정보 - */ - async getSales(period = 'today') { + // fetchMenus를 실제 API 호출로 수정 + const fetchMenus = async () => { + if (!storeInfo.value?.storeId) { + console.warn('매장 ID가 없어 메뉴를 조회할 수 없습니다.') + return { success: false, error: '매장 정보가 필요합니다.' } + } + + isLoading.value = true + try { - // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) - const mockSalesData = { - todaySales: 150000, - yesterdaySales: 120000, - changeRate: 25.0, - monthlyTarget: 3000000, - achievementRate: 45.2 + const result = await storeService.getMenus(storeInfo.value.storeId) + + if (result.success) { + menus.value = result.data + return { success: true } + } else { + return { success: false, error: result.message } } - - return formatSuccessResponse(mockSalesData, '매출 정보를 조회했습니다.') } catch (error) { - return handleApiError(error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } - /** - * 메뉴 등록 (STR-030: 메뉴 등록) - * @param {Object} menuData - 메뉴 정보 - * @returns {Promise} 메뉴 등록 결과 - */ - async registerMenu(menuData) { + // 메뉴 관련 메서드들 API 연동 추가 + const saveMenu = async (menuData) => { + isLoading.value = true + try { - // 현재는 목업 처리 (추후 실제 API 연동 시 수정) - console.log('메뉴 등록 - 목업 처리:', menuData) + const result = await storeService.registerMenu(menuData) - const mockMenuResponse = { - id: Date.now(), - ...menuData, - createdAt: new Date().toISOString() + if (result.success) { + // 메뉴 목록 새로고침 + await fetchMenus() + return { success: true, message: '메뉴가 등록되었습니다.' } + } else { + return { success: false, error: result.message } } - - return formatSuccessResponse(mockMenuResponse, '메뉴가 등록되었습니다.') } catch (error) { - return handleApiError(error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } - /** - * 메뉴 목록 조회 (STR-025: 메뉴 조회) - * @returns {Promise} 메뉴 목록 - */ - async getMenus() { + const updateMenu = async (menuId, menuData) => { + isLoading.value = true + try { - // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) - const mockMenus = [ - { - id: 1, - name: '김치찌개', - category: '찌개', - price: 8000, - description: '푸짐한 김치찌개', - imageUrl: '/images/menu-placeholder.png', - isPopular: true - }, - { - id: 2, - name: '제육볶음', - category: '볶음', - price: 12000, - description: '매콤한 제육볶음', - imageUrl: '/images/menu-placeholder.png', - isRecommended: true - } - ] + const result = await storeService.updateMenu(menuId, menuData) - return formatSuccessResponse(mockMenus, '메뉴 목록을 조회했습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 메뉴 수정 (STR-035: 메뉴 수정) - * @param {number} menuId - 메뉴 ID - * @param {Object} menuData - 수정할 메뉴 정보 - * @returns {Promise} 메뉴 수정 결과 - */ - async updateMenu(menuId, menuData) { - try { - // 현재는 목업 처리 (추후 실제 API 연동 시 수정) - console.log('메뉴 수정 - 목업 처리:', { menuId, menuData }) - - const mockMenuResponse = { - id: menuId, - ...menuData, - updatedAt: new Date().toISOString() + if (result.success) { + // 메뉴 목록 새로고침 + await fetchMenus() + return { success: true, message: '메뉴가 수정되었습니다.' } + } else { + return { success: false, error: result.message } } - - return formatSuccessResponse(mockMenuResponse, '메뉴가 수정되었습니다.') } catch (error) { - return handleApiError(error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } - /** - * 메뉴 삭제 (STR-040: 메뉴 삭제) - * @param {number} menuId - 삭제할 메뉴 ID - * @returns {Promise} 메뉴 삭제 결과 - */ - async deleteMenu(menuId) { + const deleteMenu = async (menuId) => { + isLoading.value = true + try { - // 현재는 목업 처리 (추후 실제 API 연동 시 수정) - console.log('메뉴 삭제 - 목업 처리:', menuId) + const result = await storeService.deleteMenu(menuId) - return formatSuccessResponse(null, '메뉴가 삭제되었습니다.') + if (result.success) { + // 메뉴 목록 새로고침 + await fetchMenus() + return { success: true, message: '메뉴가 삭제되었습니다.' } + } else { + return { success: false, error: result.message } + } } catch (error) { - return handleApiError(error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } -} -export const storeService = new StoreService() -export default storeService \ No newline at end of file + // 매출 정보 조회 추가 + const fetchSalesData = async () => { + if (!storeInfo.value?.storeId) { + return { success: false, error: '매장 정보가 필요합니다.' } + } + + isLoading.value = true + + try { + const result = await storeService.getSales(storeInfo.value.storeId) + + if (result.success) { + salesData.value = result.data + return { success: true } + } else { + return { success: false, error: result.message } + } + } catch (error) { + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + } + } + + return { + // 상태 + storeInfo, + menus, + salesData, + isLoading, + + // 컴퓨티드 + hasStoreInfo, + menuCount, + + // 메서드 + fetchStoreInfo, + saveStoreInfo, + fetchMenus, + saveMenu, + updateMenu, + deleteMenu, + fetchSalesData + } +}) + diff --git a/src/views/StoreManagementView.vue b/src/views/StoreManagementView.vue index 5adaafe..7494f3f 100644 --- a/src/views/StoreManagementView.vue +++ b/src/views/StoreManagementView.vue @@ -1,5 +1,4 @@ -//* src/views/StoreManagementView.vue - 데모 기능 제거 및 백엔드 연동 수정 - +//* src/views/StoreManagementView.vue @@ -247,24 +941,58 @@ import { ref, computed, onMounted } from 'vue' import { useStoreStore } from '@/store/index' +/** + * AI 마케팅 서비스 - 매장 관리 페이지 + * 매장 정보 관리 및 메뉴 관리 기능 제공 + * 유저스토리: STR-005, STR-010, STR-015, STR-020, STR-025, STR-030, STR-035, STR-040 + */ + +// 스토어 const storeStore = useStoreStore() -// 기존 상태들... +// 탭 관리 const currentTab = ref('basic') + +// 매장 정보 관련 상태 const showCreateDialog = ref(false) const editMode = ref(false) const formValid = ref(false) const saving = ref(false) const imageInput = ref(null) -const storeForm = ref(null) +// 메뉴 관리 관련 상태 +const showMenuDialog = ref(false) +const editMenuMode = ref(false) +const menuFormValid = ref(false) +const savingMenu = ref(false) +const deletingMenu = ref(false) +const showDeleteMenuDialog = ref(false) +const deleteMenuTarget = ref(null) +const menuImageInput = ref(null) +const menuSearch = ref('') +const menuCategoryFilter = ref('전체') +const menuStatusFilter = ref('전체') + +// SNS 연동 관련 상태 +const snsCheckLoading = ref({ + instagram: false, + blog: false +}) +const showSnsResultDialog = ref(false) +const snsConnectionResult = ref({ + success: false, + platform: '', + message: '' +}) + +// 스낵바 상태 const snackbar = ref({ show: false, message: '', color: 'success' }) -// 기존 폼 데이터들... +// 폼 데이터 const formData = ref({ storeName: '', businessType: '', @@ -283,113 +1011,892 @@ const formData = ref({ imageUrl: '' }) +// 메뉴 폼 데이터 +const menuFormData = ref({ + menuName: '', + price: 0, + category: '', + description: '', + available: true, + recommended: false, + imageUrl: '' +}) + +// 메뉴 목록 (데모 데이터) +const menus = ref([ + { + id: 1, + menuName: '김치찌개', + category: '찌개류', + price: 8000, + description: '돼지고기와 신김치로 끓인 얼큰한 김치찌개', + available: true, + recommended: true, + imageUrl: '/images/kimchi-jjigae.jpg' + }, + { + id: 2, + menuName: '된장찌개', + category: '찌개류', + price: 7000, + description: '집된장으로 끓인 구수한 된장찌개', + available: true, + recommended: false, + imageUrl: '/images/doenjang-jjigae.jpg' + }, + { + id: 3, + menuName: '제육볶음', + category: '볶음류', + price: 12000, + description: '매콤달콤한 양념에 볶은 제육볶음', + available: false, + recommended: true, + imageUrl: '/images/jeyuk-bokkeum.jpg' + } +]) + // 컴퓨티드 속성 const storeInfo = computed(() => storeStore.storeInfo || {}) -// 유틸리티 함수들 -const formatClosedDays = (closedDays) => { - if (!closedDays) return '미설정' - - if (typeof closedDays === 'string') { - return closedDays +const availableMenusCount = computed(() => + menus.value.filter(menu => menu.available).length +) + +const recommendedMenusCount = computed(() => + menus.value.filter(menu => menu.recommended).length +) + +const averagePrice = computed(() => { + if (menus.value.length === 0) return '0원' + const total = menus.value.reduce((sum, menu) => sum + menu.price, 0) + const average = Math.round(total / menus.value.length) + return formatCurrency(average) +}) + +const filteredMenus = computed(() => { + let filtered = menus.value + + // 검색 필터 + if (menuSearch.value) { + filtered = filtered.filter(menu => + menu.menuName.toLowerCase().includes(menuSearch.value.toLowerCase()) || + menu.description.toLowerCase().includes(menuSearch.value.toLowerCase()) + ) } + + // 카테고리 필터 + if (menuCategoryFilter.value !== '전체') { + filtered = filtered.filter(menu => menu.category === menuCategoryFilter.value) + } + + // 상태 필터 + if (menuStatusFilter.value !== '전체') { + const isAvailable = menuStatusFilter.value === '판매중' + filtered = filtered.filter(menu => menu.available === isAvailable) + } + + return filtered +}) + +const menuCategories = computed(() => { + const categories = [...new Set(menus.value.map(menu => menu.category))] + return categories.sort() +}) + +// 선택 옵션 +const businessTypes = [ + '한식', '중식', '일식', '양식', '분식', '치킨', '피자', '버거', + '카페', '디저트', '술집', '기타' +] + +const daysOfWeek = [ + { title: '월요일', value: 'monday' }, + { title: '화요일', value: 'tuesday' }, + { title: '수요일', value: 'wednesday' }, + { title: '목요일', value: 'thursday' }, + { title: '금요일', value: 'friday' }, + { title: '토요일', value: 'saturday' }, + { title: '일요일', value: 'sunday' } +] + +// 유효성 검사 규칙 +const businessNumberRules = [ + v => !!v || '사업자등록번호를 입력해주세요', + v => /^\d{3}-\d{2}-\d{5}$/.test(v) || '올바른 사업자등록번호 형식이 아닙니다 (예: 123-45-67890)' +] + +const phoneRules = [ + v => !!v || '연락처를 입력해주세요', + v => /^[0-9-+\s()]+$/.test(v) || '올바른 연락처 형식이 아닙니다' +] + +const priceRules = [ + v => !!v || '가격을 입력해주세요', + v => v > 0 || '가격은 0보다 커야 합니다' +] + +// 유틸리티 함수 +const formatCurrency = (amount) => { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format(amount) +} + +const formatHolidays = (holidays) => { + if (!holidays || holidays.length === 0) return '없음' + const dayNames = { + monday: '월요일', + tuesday: '화요일', + wednesday: '수요일', + thursday: '목요일', + friday: '금요일', + saturday: '토요일', + sunday: '일요일' + } + return holidays.map(day => dayNames[day]).join(', ') +} + +const getCategoryColor = (category) => { + const colors = { + '면류': 'orange', + '튀김류': 'amber', + '음료': 'blue', + '안주': 'green', + '디저트': 'pink', + '찌개류': 'red', + '볶음류': 'purple' + } + return colors[category] || 'grey' +} + +// 메서드 + +/** + * SNS 계정 연동 확인 + * @param {string} platform - SNS 플랫폼 (instagram, blog) + */ +const checkSnsConnection = async (platform) => { + console.log(`${platform} 연동 확인 시작`) - if (Array.isArray(closedDays)) { - const dayNames = { - 'monday': '월요일', - 'tuesday': '화요일', - 'wednesday': '수요일', - 'thursday': '목요일', - 'friday': '금요일', - 'saturday': '토요일', - 'sunday': '일요일' + snsCheckLoading.value[platform] = true + + try { + // 개발 모드에서는 빠른 시뮬레이션 + const delay = import.meta.env.DEV ? 1000 : 2000 + await new Promise(resolve => setTimeout(resolve, delay)) + + // 랜덤하게 성공/실패 결정 (실제로는 API 응답에 따라 결정) + const isSuccess = Math.random() > 0.3 + + snsConnectionResult.value.success = isSuccess + snsConnectionResult.value.platform = platform + + if (isSuccess) { + snsConnectionResult.value.message = platform === 'instagram' + ? '인스타그램 계정 연동이 확인되었습니다!' + : '네이버 블로그 연동이 확인되었습니다!' + } else { + snsConnectionResult.value.message = platform === 'instagram' + ? '인스타그램 계정을 찾을 수 없거나 연동할 수 없습니다. URL을 확인해주세요.' + : '네이버 블로그를 찾을 수 없거나 연동할 수 없습니다. URL을 확인해주세요.' } - return closedDays.map(day => dayNames[day] || day).join(', ') || '연중무휴' + showSnsResultDialog.value = true + + } catch (error) { + console.error('SNS 연동 확인 중 오류:', error) + snsConnectionResult.value.success = false + snsConnectionResult.value.message = '연동 확인 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' + showSnsResultDialog.value = true + } finally { + snsCheckLoading.value[platform] = false } - - return '미설정' } -const getSnsAccount = (platform) => { - const snsAccounts = storeInfo.value.snsAccounts +/** + * 데모 매장 데이터 로드 (개발용) + */ +const loadDemoStoreData = () => { + console.log('데모 매장 데이터 로드') - if (!snsAccounts) return null + // 데모 매장 정보 설정 + const demoStoreInfo = { + id: 1, + storeName: '김씨네 분식점', + businessType: '분식', + ownerName: '김점주', + businessNumber: '123-45-67890', + address: '서울특별시 강남구 테헤란로 123', + phoneNumber: '02-1234-5678', + seatCount: 20, + instagramUrl: '@kimfood_bunsik', + blogUrl: 'blog.naver.com/kimfood123', + openTime: '09:00', + closeTime: '21:00', + holidays: ['sunday'], + deliveryAvailable: true, + takeoutAvailable: true, + imageUrl: '/images/store-demo.jpg' + } - if (typeof snsAccounts === 'string') { - try { - const parsed = JSON.parse(snsAccounts) - return parsed[platform] - } catch { - return null + // 스토어에 데모 데이터 직접 설정 + storeStore.storeInfo = demoStoreInfo + + showSnackbar('데모 매장 정보가 로드되었습니다', 'success') +} + +/** + * 기본 정보 수정 모드로 전환 + */ +const editBasicInfo = () => { + editMode.value = true + + // 현재 매장 정보를 폼 데이터에 복사 + Object.assign(formData.value, { + storeName: storeInfo.value.storeName || '', + businessType: storeInfo.value.businessType || '', + ownerName: storeInfo.value.ownerName || '', + businessNumber: storeInfo.value.businessNumber || '', + address: storeInfo.value.address || '', + phoneNumber: storeInfo.value.phoneNumber || '', + seatCount: storeInfo.value.seatCount || 0, + instagramUrl: storeInfo.value.instagramUrl || '', + blogUrl: storeInfo.value.blogUrl || '', + openTime: storeInfo.value.openTime || '09:00', + closeTime: storeInfo.value.closeTime || '21:00', + holidays: storeInfo.value.holidays || [], + deliveryAvailable: storeInfo.value.deliveryAvailable || false, + takeoutAvailable: storeInfo.value.takeoutAvailable || true, + imageUrl: storeInfo.value.imageUrl || '' + }) + + showCreateDialog.value = true +} + +/** + * 이미지 선택 + */ +const selectImage = () => { + imageInput.value?.click() +} + +/** + * 이미지 업로드 처리 + */ +const handleImageUpload = (event) => { + const file = event.target.files[0] + if (file) { + // 실제로는 서버에 업로드하고 URL을 받아옴 + const reader = new FileReader() + reader.onload = (e) => { + formData.value.imageUrl = e.target.result } + reader.readAsDataURL(file) } - - if (typeof snsAccounts === 'object') { - return snsAccounts[platform] - } - - return null } +/** + * 매장 정보 저장 + */ +const saveStoreInfo = async () => { + if (!storeForm.value?.validate()) return + + console.log('매장 정보 저장:', storeFormData.value) + + try { + const result = await storeStore.saveStoreInfo(storeFormData.value) + + if (result.success) { + appStore.showSnackbar(result.message || '매장 정보가 저장되었습니다', 'success') + showStoreForm.value = false + } else { + appStore.showSnackbar(result.error || '저장에 실패했습니다', 'error') + } + } catch (error) { + console.error('매장 정보 저장 실패:', error) + appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') + } +} + +/** + * 다이얼로그 닫기 + */ +const closeDialog = () => { + showCreateDialog.value = false + editMode.value = false + + // 폼 데이터 초기화 + Object.assign(formData.value, { + storeName: '', + businessType: '', + ownerName: '', + businessNumber: '', + address: '', + phoneNumber: '', + seatCount: 0, + instagramUrl: '', + blogUrl: '', + openTime: '09:00', + closeTime: '21:00', + holidays: [], + deliveryAvailable: false, + takeoutAvailable: true, + imageUrl: '' + }) +} + +// 메뉴 관리 메서드 + +/** + * 메뉴 등록 다이얼로그 열기 + */ +const openCreateMenuDialog = () => { + editMenuMode.value = false + resetMenuForm() + showMenuDialog.value = true +} + +/** + * 메뉴 수정 + */ +const editMenu = (menu) => { + editMenuMode.value = true + Object.assign(menuFormData.value, menu) + showMenuDialog.value = true +} + +/** + * 메뉴 삭제 확인 + */ +const confirmDeleteMenu = (menu) => { + deleteMenuTarget.value = menu + showDeleteMenuDialog.value = true +} + +/** + * 메뉴 이미지 선택 + */ +const selectMenuImage = () => { + menuImageInput.value?.click() +} + +/** + * 메뉴 이미지 업로드 처리 + */ +const handleMenuImageUpload = (event) => { + const file = event.target.files[0] + if (file) { + const reader = new FileReader() + reader.onload = (e) => { + menuFormData.value.imageUrl = e.target.result + } + reader.readAsDataURL(file) + } +} + +/** + * 메뉴 저장 + */ +const saveMenu = async () => { + if (!menuForm.value?.validate()) return + + console.log('메뉴 저장:', menuFormData.value) + + try { + let result + if (isMenuEdit.value && editingMenuId.value) { + result = await storeStore.updateMenu(editingMenuId.value, menuFormData.value) + } else { + result = await storeStore.saveMenu(menuFormData.value) + } + + if (result.success) { + appStore.showSnackbar(result.message || '메뉴가 저장되었습니다', 'success') + showMenuForm.value = false + resetMenuForm() + } else { + appStore.showSnackbar(result.error || '저장에 실패했습니다', 'error') + } + } catch (error) { + console.error('메뉴 저장 실패:', error) + appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') + } +} + +/** + * 메뉴 삭제 + */ +const deleteMenu = async (menuId) => { + try { + const result = await storeStore.deleteMenu(menuId) + + if (result.success) { + appStore.showSnackbar(result.message || '메뉴가 삭제되었습니다', 'success') + } else { + appStore.showSnackbar(result.error || '삭제에 실패했습니다', 'error') + } + } catch (error) { + console.error('메뉴 삭제 실패:', error) + appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') + } +} + +/** + * 메뉴 다이얼로그 닫기 + */ +const closeMenuDialog = () => { + showMenuDialog.value = false + editMenuMode.value = false + resetMenuForm() +} + +/** + * 메뉴 폼 초기화 + */ +const resetMenuForm = () => { + Object.assign(menuFormData.value, { + menuName: '', + price: 0, + category: '', + description: '', + available: true, + recommended: false, + imageUrl: '' + }) +} + +/** + * 필터 초기화 + */ +const clearFilters = () => { + menuSearch.value = '' + menuCategoryFilter.value = '전체' + menuStatusFilter.value = '전체' +} + +/** + * 스낵바 표시 + */ const showSnackbar = (message, color = 'success') => { snackbar.value.message = message snackbar.value.color = color snackbar.value.show = true } +// 메뉴 상세 다이얼로그 관련 상태 (기존 상태들에 추가) +const showMenuDetailDialog = ref(false) +const selectedMenuDetail = ref(null) + +// 메뉴 관리 메서드에 추가할 함수들 + /** - * 컴포넌트 마운트 시 실행 (데모 기능 제거, 백엔드 연동만) + * 메뉴 상세보기 + */ +const viewMenuDetail = (menu) => { + selectedMenuDetail.value = { ...menu } + showMenuDetailDialog.value = true +} + +/** + * 상세화면에서 수정 모드로 전환 + */ +const editFromDetail = () => { + showMenuDetailDialog.value = false + editMenuMode.value = true + Object.assign(menuFormData.value, selectedMenuDetail.value) + showMenuDialog.value = true +} + +/** + * 컴포넌트 마운트 시 실행 */ onMounted(async () => { - console.log('=== StoreManagementView 마운트됨 ===') + console.log('StoreManagementView 마운트됨') try { - // 매장 정보 조회 - const result = await storeStore.fetchStoreInfo() - - console.log('매장 정보 조회 결과:', result) - - if (result.success) { - console.log('✅ 매장 정보 로드 완료:', result.data) - showSnackbar('매장 정보를 불러왔습니다', 'success') - } else { - if (result.message === '등록된 매장이 없습니다') { - console.log('⚠️ 등록된 매장이 없음 - 등록 화면 표시') - // 매장이 없는 경우는 정상적인 상황이므로 에러 메시지 표시하지 않음 - } else { - console.warn('❌ 매장 정보 조회 실패:', result.message) - showSnackbar(result.message || '매장 정보를 불러오는데 실패했습니다', 'error') - } + // 매장 정보 로드 + if (!storeStore.hasStoreInfo) { + await storeStore.fetchStoreInfo() } + + // 메뉴 목록 로드 + await storeStore.fetchMenus() + } catch (error) { - console.error('매장 정보 조회 중 예외 발생:', error) - showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error') + console.warn('매장 관리 데이터 로드 실패 (개발 중이므로 무시):', error) } }) - -// 기존 메서드들 유지 (데모 데이터 로드 함수만 제거) -// loadDemoStoreData 함수 제거 - -// 나머지 함수들은 기존과 동일하게 유지... \ No newline at end of file From 76a69a645394f8820e169f6e42567ae2793e9938 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Jun 2025 15:24:11 +0900 Subject: [PATCH 3/4] store backend connect --- src/services/api.js | 6 +- src/services/store.js | 280 ++-- src/store/index.js | 436 +++--- src/store/store.js | 397 +++--- src/views/StoreManagementView.vue | 2087 ++++++----------------------- 5 files changed, 967 insertions(+), 2239 deletions(-) diff --git a/src/services/api.js b/src/services/api.js index 223b0f1..4b33392 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -8,10 +8,10 @@ const getApiUrls = () => { GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', AUTH_URL: 'http://localhost:8081/api/auth', MEMBER_URL: 'http://localhost:8081/api/member', - STORE_URL: config.STORE_URL || 'http://20.1.2.3/api/store', + STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', CONTENT_URL: config.CONTENT_URL || 'http://20.1.2.3/api/content', - MENU_URL: config.MENU_URL || 'http://20.1.2.3/api/menu', - SALES_URL: config.SALES_URL || 'http://20.1.2.3/api/sales', + MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu', + SALES_URL: config.SALES_URL || 'http://localhost:8082/api/sales', RECOMMEND_URL: config.RECOMMEND_URL || 'http://20.1.2.3/api/recommendation', } } diff --git a/src/services/store.js b/src/services/store.js index b347229..12cb28f 100644 --- a/src/services/store.js +++ b/src/services/store.js @@ -1,9 +1,9 @@ -//* src/services/store.js - 기존 파일 수정 (API 설계서 기준) -import { storeApi, menuApi, salesApi, handleApiError, formatSuccessResponse } from './api.js' +//* src/services/store.js - 매장 서비스 완전 수정 +import { storeApi, handleApiError, formatSuccessResponse } from './api.js' /** * 매장 관련 API 서비스 - * API 설계서 기준으로 수정됨 + * 백엔드 Store Controller와 연동 (포트 8082) */ class StoreService { /** @@ -13,171 +13,225 @@ class StoreService { */ async registerStore(storeData) { try { - const response = await storeApi.post('/register', { + console.log('=== 매장 등록 API 호출 ===') + console.log('요청 데이터:', storeData) + + // 백엔드 StoreCreateRequest에 맞는 형태로 변환 + const requestData = { storeName: storeData.storeName, businessType: storeData.businessType, address: storeData.address, phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours || storeData.operatingHours, + businessHours: storeData.businessHours, closedDays: storeData.closedDays, - seatCount: storeData.seatCount, - snsAccounts: storeData.snsAccounts || `인스타그램: ${storeData.instaAccount || ''}, 네이버블로그: ${storeData.naverBlogAccount || ''}`, + seatCount: parseInt(storeData.seatCount) || 0, + instaAccounts: storeData.instaAccounts || '', + blogAccounts: storeData.blogAccounts || '', description: storeData.description || '' - }) - - return formatSuccessResponse(response.data.data, '매장이 등록되었습니다.') + } + + console.log('=== 각 필드 상세 검증 ===') + console.log('storeName:', requestData.storeName, '(타입:', typeof requestData.storeName, ')') + console.log('businessType:', requestData.businessType, '(타입:', typeof requestData.businessType, ')') + console.log('address:', requestData.address, '(타입:', typeof requestData.address, ')') + console.log('seatCount:', requestData.seatCount, '(타입:', typeof requestData.seatCount, ')') + + console.log('백엔드 전송 데이터:', requestData) + + const response = await storeApi.post('/register', requestData) + + console.log('매장 등록 API 응답:', response.data) + + // 백엔드 응답 구조에 맞게 처리 + if (response.data && (response.data.status === 200 || response.data.success !== false)) { + return { + success: true, + message: response.data.message || '매장이 등록되었습니다.', + data: response.data.data + } + } else { + throw new Error(response.data.message || '매장 등록에 실패했습니다.') + } } catch (error) { + console.error('매장 등록 실패:', error) + + if (error.response) { + console.error('응답 상태:', error.response.status) + console.error('응답 데이터:', error.response.data) + } + return handleApiError(error) } } /** - * 매장 정보 조회 (STR-005: 매장 조회) + * 매장 정보 조회 (STR-005: 매장 정보 관리) * @returns {Promise} 매장 정보 */ async getStore() { try { - const response = await storeApi.get('/') - - return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.') + 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 || '매장 정보를 찾을 수 없습니다.') + } } catch (error) { + console.error('매장 정보 조회 실패:', error) + + // 404 오류 처리 (매장이 없음) + if (error.response?.status === 404) { + return { + success: false, + message: '등록된 매장이 없습니다', + data: null + } + } + + // 500 오류 처리 (서버 내부 오류) + if (error.response?.status === 500) { + console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) + return { + success: false, + message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.', + data: null + } + } + return handleApiError(error) } } /** * 매장 정보 수정 (STR-010: 매장 수정) + * @param {number} storeId - 매장 ID (현재는 사용하지 않음 - JWT에서 사용자 확인) * @param {Object} storeData - 수정할 매장 정보 * @returns {Promise} 매장 수정 결과 */ - async updateStore(storeData) { + async updateStore(storeId, storeData) { try { - const response = await storeApi.put('/', { + console.log('=== 매장 정보 수정 API 호출 ===') + console.log('요청 데이터:', storeData) + + // 백엔드 StoreUpdateRequest에 맞는 형태로 변환 + const requestData = { storeName: storeData.storeName, businessType: storeData.businessType, address: storeData.address, phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours || storeData.operatingHours, + businessHours: storeData.businessHours, closedDays: storeData.closedDays, - seatCount: storeData.seatCount, - snsAccounts: storeData.snsAccounts || `인스타그램: ${storeData.instaAccount || ''}, 네이버블로그: ${storeData.naverBlogAccount || ''}`, + seatCount: parseInt(storeData.seatCount) || 0, + instaAccounts: storeData.instaAccounts || '', + blogAccounts: storeData.blogAccounts || '', description: storeData.description || '' - }) - - return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.') + } + + console.log('백엔드 전송 데이터:', requestData) + + // PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음) + const response = await storeApi.put('/', requestData) + + console.log('매장 정보 수정 API 응답:', response.data) + + if (response.data && (response.data.status === 200 || response.data.success !== false)) { + return { + success: true, + message: response.data.message || '매장 정보가 수정되었습니다.', + data: response.data.data + } + } else { + throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.') + } } catch (error) { + console.error('매장 정보 수정 실패:', error) return handleApiError(error) } } /** - * 매출 정보 조회 (SAL-005: 매출 조회) - * @param {number} storeId - 매장 ID + * 매출 정보 조회 (STR-020: 대시보드) + * @param {string} period - 조회 기간 (today, week, month, year) * @returns {Promise} 매출 정보 */ - async getSales(storeId) { + async getSales(period = 'today') { try { - const response = await salesApi.get(`/${storeId}`) - - return formatSuccessResponse(response.data.data, '매출 정보를 조회했습니다.') + // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) + const mockSalesData = { + todaySales: 150000, + yesterdaySales: 120000, + changeRate: 25.0, + monthlyTarget: 3000000, + achievementRate: 45.2 + } + + return formatSuccessResponse(mockSalesData, '매출 정보를 조회했습니다.') } catch (error) { return handleApiError(error) } } /** - * 메뉴 등록 (MNU-010: 메뉴 등록) - * @param {Object} menuData - 메뉴 정보 - * @returns {Promise} 메뉴 등록 결과 - */ - async registerMenu(menuData) { - try { - const response = await menuApi.post('/register', { - menuName: menuData.menuName, - menuCategory: menuData.menuCategory || menuData.category, - menuImage: menuData.menuImage || menuData.image, - price: menuData.price, - description: menuData.description, - isPopular: menuData.isPopular || false, - isRecommended: menuData.isRecommended || false, - }) - - return formatSuccessResponse(response.data.data, '메뉴가 등록되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 메뉴 목록 조회 (MNU-005: 메뉴 조회) - * @param {number} storeId - 매장 ID + * 메뉴 목록 조회 (개발 예정) * @returns {Promise} 메뉴 목록 */ - async getMenus(storeId) { + async getMenus() { try { - const response = await menuApi.get(`/${storeId}`) - - return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 메뉴 수정 (MNU-015: 메뉴 수정) - * @param {number} menuId - 메뉴 ID - * @param {Object} menuData - 수정할 메뉴 정보 - * @returns {Promise} 메뉴 수정 결과 - */ - async updateMenu(menuId, menuData) { - try { - const response = await menuApi.put(`/${menuId}`, { - menuName: menuData.menuName, - menuCategory: menuData.menuCategory || menuData.category, - menuImage: menuData.menuImage || menuData.image, - price: menuData.price, - description: menuData.description, - isPopular: menuData.isPopular || false, - isRecommended: menuData.isRecommended || false, - }) - - return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 메뉴 삭제 (MNU-020: 메뉴 삭제) - * @param {number} menuId - 메뉴 ID - * @returns {Promise} 메뉴 삭제 결과 - */ - async deleteMenu(menuId) { - try { - await menuApi.delete(`/${menuId}`) - - return formatSuccessResponse(null, '메뉴가 삭제되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 다중 메뉴 삭제 - * @param {number[]} menuIds - 삭제할 메뉴 ID 배열 - * @returns {Promise} 삭제 결과 - */ - async deleteMenus(menuIds) { - try { - const deletePromises = menuIds.map((menuId) => this.deleteMenu(menuId)) - await Promise.all(deletePromises) - - return formatSuccessResponse(null, `${menuIds.length}개의 메뉴가 삭제되었습니다.`) + // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) + const mockMenus = [ + { + id: 1, + name: '아메리카노', + price: 4000, + category: '커피', + description: '진한 풍미의 아메리카노', + imageUrl: '/images/americano.jpg', + isAvailable: true + }, + { + id: 2, + name: '카페라떼', + price: 4500, + category: '커피', + description: '부드러운 우유가 들어간 라떼', + imageUrl: '/images/latte.jpg', + isAvailable: true + } + ] + + return formatSuccessResponse(mockMenus, '메뉴 목록을 조회했습니다.') } catch (error) { return handleApiError(error) } } } +// 싱글톤 인스턴스 생성 및 export export const storeService = new StoreService() -export default storeService \ No newline at end of file +export default storeService + +// 디버깅을 위한 전역 노출 (개발 환경에서만) +if (process.env.NODE_ENV === 'development') { + window.storeService = storeService +} \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index ae9412c..266c5b2 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,285 +1,205 @@ -//* src/store/index.js -/** - * Pinia 스토어 설정 - * 전역 상태 관리 - */ +//* src/store/index.js - Store 스토어 수정 (fetchMenus 메서드 추가) import { defineStore } from 'pinia' -import authService from '@/services/auth' -import storeService from '@/services/store' -// 인증 스토어 -export const useAuthStore = defineStore('auth', { - state: () => ({ - user: null, - token: localStorage.getItem('token'), - refreshToken: localStorage.getItem('refreshToken'), - isAuthenticated: false - }), - - getters: { - getUserInfo: (state) => state.user, - isLoggedIn: (state) => state.isAuthenticated && !!state.token - }, - - actions: { - async login(credentials) { - try { - const response = await authService.login(credentials) - this.setAuth(response.data) - return response - } catch (error) { - this.clearAuth() - throw error - } - }, - - async register(userData) { - try { - const response = await authService.register(userData) - return response - } catch (error) { - throw error - } - }, - - async logout() { - try { - if (this.token) { - await authService.logout() - } - } catch (error) { - console.error('로그아웃 오류:', error) - } finally { - this.clearAuth() - } - }, - - async refreshUserInfo() { - try { - const response = await authService.getUserInfo() - this.user = response.data - this.isAuthenticated = true - return response - } catch (error) { - this.clearAuth() - throw error - } - }, - - setAuth(authData) { - this.user = authData.user - this.token = authData.accessToken - this.refreshToken = authData.refreshToken - this.isAuthenticated = true - - localStorage.setItem('token', authData.accessToken) - localStorage.setItem('refreshToken', authData.refreshToken) - }, - - clearAuth() { - this.user = null - this.token = null - this.refreshToken = null - this.isAuthenticated = false - - localStorage.removeItem('token') - localStorage.removeItem('refreshToken') - } - } -}) - -// 앱 전역 스토어 -export const useAppStore = defineStore('app', { - state: () => ({ - loading: false, - snackbar: { - show: false, - message: '', - color: 'success', - timeout: 3000 - }, - notifications: [], - notificationCount: 0 - }), - - actions: { - setLoading(status) { - this.loading = status - }, - - showSnackbar(message, color = 'success', timeout = 3000) { - this.snackbar = { - show: true, - message, - color, - timeout - } - }, - - hideSnackbar() { - this.snackbar.show = false - }, - - addNotification(notification) { - this.notifications.unshift({ - id: Date.now(), - timestamp: new Date(), - ...notification - }) - this.notificationCount = this.notifications.length - }, - - clearNotifications() { - this.notifications = [] - this.notificationCount = 0 - } - } -}) - -// 매장 스토어 export const useStoreStore = defineStore('store', { state: () => ({ storeInfo: null, - loading: false - }), - - getters: { - hasStoreInfo: (state) => !!state.storeInfo - }, - - actions: { - setStoreInfo(storeInfo) { - this.storeInfo = storeInfo - }, - - async fetchStoreInfo() { - try { - this.loading = true - const response = await storeService.getStore() // getStoreInfo가 아닌 getStore - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async registerStore(storeData) { - try { - this.loading = true - const response = await storeService.registerStore(storeData) - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async updateStore(storeId, storeData) { - try { - this.loading = true - const response = await storeService.updateStore(storeId, storeData) - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async createStoreInfo(storeData) { - try { - this.loading = true - const response = await storeService.createStoreInfo(storeData) - this.storeInfo = response.data - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - } - } -}) - -// 메뉴 스토어 -export const useMenuStore = defineStore('menu', { - state: () => ({ - menus: [], + menus: [], // 메뉴 목록 추가 loading: false, - totalCount: 0 + error: null }), getters: { - getMenuById: (state) => (id) => { - return state.menus.find(menu => menu.id === id) - }, - - getMenusByCategory: (state) => (category) => { - return state.menus.filter(menu => menu.category === category) - } + hasStoreInfo: (state) => !!state.storeInfo, + isLoading: (state) => state.loading, + hasMenus: (state) => state.menus && state.menus.length > 0 }, actions: { - async fetchMenus() { + /** + * 매장 정보 조회 + */ + async fetchStoreInfo() { + console.log('=== Store 스토어: 매장 정보 조회 시작 ===') + this.loading = true + this.error = null + try { - this.loading = true - const response = await storeService.getMenus() - this.menus = response.data - this.totalCount = response.data.length - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async createMenu(menuData) { - try { - this.loading = true - const response = await storeService.createMenu(menuData) - this.menus.push(response.data) - this.totalCount++ - return response - } catch (error) { - throw error - } finally { - this.loading = false - } - }, - - async updateMenu(menuId, menuData) { - try { - this.loading = true - const response = await storeService.updateMenu(menuId, menuData) - const index = this.menus.findIndex(menu => menu.id === menuId) - if (index !== -1) { - this.menus[index] = response.data + // 스토어 서비스 임포트 + 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 + + if (result.message === '등록된 매장이 없습니다') { + return { success: false, message: '등록된 매장이 없습니다' } + } else { + return { success: false, message: result.message || '매장 정보 조회에 실패했습니다' } + } } - return response } catch (error) { - throw 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: '등록된 매장이 없습니다' } + } + + 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 } }, - - async deleteMenu(menuId) { + + /** + * 메뉴 목록 조회 - 추가된 메서드 + */ + async fetchMenus() { + console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===') + try { - this.loading = true - await storeService.deleteMenu(menuId) - this.menus = this.menus.filter(menu => menu.id !== menuId) - this.totalCount-- + // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) + const mockMenus = [ + { + id: 1, + name: '아메리카노', + price: 4000, + category: '커피', + description: '진한 풍미의 아메리카노', + imageUrl: '/images/americano.jpg', + isAvailable: true + }, + { + id: 2, + name: '카페라떼', + price: 4500, + category: '커피', + description: '부드러운 우유가 들어간 라떼', + imageUrl: '/images/latte.jpg', + isAvailable: true + }, + { + id: 3, + name: '치즈케이크', + price: 6000, + category: '디저트', + description: '진한 크림치즈로 만든 케이크', + imageUrl: '/images/cheesecake.jpg', + isAvailable: false + } + ] + + this.menus = mockMenus + console.log('✅ 메뉴 목록 설정 완료:', mockMenus) + + return { success: true, data: mockMenus } } catch (error) { - throw error + console.error('메뉴 목록 조회 실패:', error) + this.menus = [] + return { success: false, message: '메뉴 목록을 불러오는데 실패했습니다' } + } + }, + + /** + * 매장 등록 + */ + async registerStore(storeData) { + console.log('매장 등록 시작:', storeData) + this.loading = true + this.error = null + + try { + const { storeService } = await import('@/services/store') + + const result = await storeService.registerStore(storeData) + + console.log('매장 등록 결과:', result) + + if (result.success) { + // 등록 성공 후 매장 정보 다시 조회 + await this.fetchStoreInfo() + return result + } else { + this.error = result.message + return result + } + } catch (error) { + console.error('매장 등록 실패:', error) + this.error = error.message + return { success: false, message: error.message || '매장 등록에 실패했습니다' } } finally { this.loading = false } + }, + + /** + * 매장 정보 수정 + */ + async updateStore(storeId, storeData) { + console.log('매장 정보 수정 시작:', { storeId, storeData }) + this.loading = true + this.error = null + + try { + const { storeService } = await import('@/services/store') + + const result = await storeService.updateStore(storeId, storeData) + + console.log('매장 수정 결과:', result) + + if (result.success) { + // 수정 성공 후 매장 정보 다시 조회 + await this.fetchStoreInfo() + return result + } else { + this.error = result.message + return result + } + } catch (error) { + console.error('매장 수정 실패:', error) + this.error = error.message + return { success: false, message: error.message || '매장 수정에 실패했습니다' } + } finally { + this.loading = false + } + }, + + /** + * 매장 정보 초기화 + */ + clearStoreInfo() { + this.storeInfo = null + this.menus = [] + this.error = null + this.loading = false } } }) \ No newline at end of file diff --git a/src/store/store.js b/src/store/store.js index 138485c..f20178e 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,202 +1,233 @@ -//* src/store/store.js 수정 - 기존 구조 유지하고 API 연동만 추가 -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import storeService from '@/services/store' +//* src/services/store.js - 매장 서비스 완전 수정 +import { storeApi, handleApiError, formatSuccessResponse } from './api.js' -export const useStoreStore = defineStore('store', () => { - // 기존 상태들 유지 - const storeInfo = ref(null) - const menus = ref([]) - const salesData = ref(null) - const isLoading = ref(false) - - // 기존 computed 속성들 유지 - const hasStoreInfo = computed(() => !!storeInfo.value) - const menuCount = computed(() => menus.value?.length || 0) - - // fetchStoreInfo를 실제 API 호출로 수정 - const fetchStoreInfo = async () => { - if (import.meta.env.DEV) { - console.log('개발 모드: 매장 정보 API 호출 건너뛰기') - return { success: true } - } - - isLoading.value = true - +/** + * 매장 관련 API 서비스 + * 백엔드 Store Controller와 연동 (포트 8082) + */ +class StoreService { + /** + * 매장 등록 (STR-015: 매장 등록) + * @param {Object} storeData - 매장 정보 + * @returns {Promise} 매장 등록 결과 + */ + async registerStore(storeData) { try { - const result = await storeService.getStore() + console.log('=== 매장 등록 API 호출 ===') + console.log('요청 데이터:', storeData) - if (result.success) { - storeInfo.value = result.data - return { success: true } - } else { - console.warn('매장 정보 조회 실패:', result.message) - return { success: false, error: result.message } - } - } catch (error) { - console.warn('매장 정보 조회 실패:', error) - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // saveStoreInfo를 실제 API 호출로 수정 - const saveStoreInfo = async (storeData) => { - isLoading.value = true - - try { - let result - if (storeInfo.value) { - // 기존 매장 정보 수정 - result = await storeService.updateStore(storeData) - } else { - // 새 매장 등록 - result = await storeService.registerStore(storeData) + // 백엔드 StoreCreateRequest에 맞는 형태로 변환 + const requestData = { + storeName: storeData.storeName, + businessType: storeData.businessType, + address: storeData.address, + phoneNumber: storeData.phoneNumber, + businessHours: storeData.businessHours, + closedDays: storeData.closedDays, + seatCount: parseInt(storeData.seatCount) || 0, + instaAccounts: storeData.instaAccounts || '', + blogAccounts: storeData.blogAccounts || '', + description: storeData.description || '' } - if (result.success) { - storeInfo.value = result.data - return { success: true, message: '매장 정보가 저장되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // fetchMenus를 실제 API 호출로 수정 - const fetchMenus = async () => { - if (!storeInfo.value?.storeId) { - console.warn('매장 ID가 없어 메뉴를 조회할 수 없습니다.') - return { success: false, error: '매장 정보가 필요합니다.' } - } - - isLoading.value = true - - try { - const result = await storeService.getMenus(storeInfo.value.storeId) + console.log('=== 각 필드 상세 검증 ===') + console.log('storeName:', requestData.storeName, '(타입:', typeof requestData.storeName, ')') + console.log('businessType:', requestData.businessType, '(타입:', typeof requestData.businessType, ')') + console.log('address:', requestData.address, '(타입:', typeof requestData.address, ')') + console.log('seatCount:', requestData.seatCount, '(타입:', typeof requestData.seatCount, ')') - if (result.success) { - menus.value = result.data - return { success: true } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // 메뉴 관련 메서드들 API 연동 추가 - const saveMenu = async (menuData) => { - isLoading.value = true - - try { - const result = await storeService.registerMenu(menuData) + console.log('백엔드 전송 데이터:', requestData) - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 등록되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - const updateMenu = async (menuId, menuData) => { - isLoading.value = true - - try { - const result = await storeService.updateMenu(menuId, menuData) + const response = await storeApi.post('/register', requestData) - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 수정되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - const deleteMenu = async (menuId) => { - isLoading.value = true - - try { - const result = await storeService.deleteMenu(menuId) + console.log('매장 등록 API 응답:', response.data) - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 삭제되었습니다.' } + // 백엔드 응답 구조에 맞게 처리 + if (response.data && (response.data.status === 200 || response.data.success !== false)) { + return { + success: true, + message: response.data.message || '매장이 등록되었습니다.', + data: response.data.data + } } else { - return { success: false, error: result.message } + throw new Error(response.data.message || '매장 등록에 실패했습니다.') } } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // 매출 정보 조회 추가 - const fetchSalesData = async () => { - if (!storeInfo.value?.storeId) { - return { success: false, error: '매장 정보가 필요합니다.' } - } - - isLoading.value = true - - try { - const result = await storeService.getSales(storeInfo.value.storeId) + console.error('매장 등록 실패:', error) - if (result.success) { - salesData.value = result.data - return { success: true } - } else { - return { success: false, error: result.message } + if (error.response) { + console.error('응답 상태:', error.response.status) + console.error('응답 데이터:', error.response.data) } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + + return handleApiError(error) } } - return { - // 상태 - storeInfo, - menus, - salesData, - isLoading, - - // 컴퓨티드 - hasStoreInfo, - menuCount, - - // 메서드 - fetchStoreInfo, - saveStoreInfo, - fetchMenus, - saveMenu, - updateMenu, - deleteMenu, - fetchSalesData + /** + * 매장 정보 조회 (STR-005: 매장 정보 관리) + * @returns {Promise} 매장 정보 + */ + async getStore() { + try { + console.log('=== 매장 정보 조회 API 호출 ===') + + const response = await storeApi.get('/') + + console.log('매장 정보 조회 API 응답:', response.data) + + // 백엔드 응답 구조에 맞게 처리 + if (response.data && response.data.data) { + return { + success: true, + message: '매장 정보를 조회했습니다.', + data: response.data.data + } + } else if (response.data && response.data.data === null) { + // 매장이 없는 경우 + return { + success: false, + message: '등록된 매장이 없습니다', + data: null + } + } else { + throw new Error(response.data.message || '매장 정보를 찾을 수 없습니다.') + } + } catch (error) { + console.error('매장 정보 조회 실패:', error) + + // 404 오류 처리 (매장이 없음) + if (error.response?.status === 404) { + return { + success: false, + message: '등록된 매장이 없습니다', + data: null + } + } + + // 500 오류 처리 (서버 내부 오류) + if (error.response?.status === 500) { + console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) + return { + success: false, + message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.', + data: null + } + } + + return handleApiError(error) + } } -}) + /** + * 매장 정보 수정 (STR-010: 매장 수정) + * @param {number} storeId - 매장 ID (현재는 사용하지 않음 - JWT에서 사용자 확인) + * @param {Object} storeData - 수정할 매장 정보 + * @returns {Promise} 매장 수정 결과 + */ + async updateStore(storeId, storeData) { + try { + console.log('=== 매장 정보 수정 API 호출 ===') + console.log('요청 데이터:', storeData) + + // 백엔드 StoreUpdateRequest에 맞는 형태로 변환 + const requestData = { + storeName: storeData.storeName, + businessType: storeData.businessType, + address: storeData.address, + phoneNumber: storeData.phoneNumber, + businessHours: storeData.businessHours, + closedDays: storeData.closedDays, + seatCount: parseInt(storeData.seatCount) || 0, + instaAccounts: storeData.instaAccounts || '', + blogAccounts: storeData.blogAccounts || '', + description: storeData.description || '' + } + + console.log('백엔드 전송 데이터:', requestData) + + // PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음) + const response = await storeApi.put('/', requestData) + + console.log('매장 정보 수정 API 응답:', response.data) + + if (response.data && (response.data.status === 200 || response.data.success !== false)) { + return { + success: true, + message: response.data.message || '매장 정보가 수정되었습니다.', + data: response.data.data + } + } else { + throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.') + } + } catch (error) { + console.error('매장 정보 수정 실패:', error) + return handleApiError(error) + } + } + + /** + * 매출 정보 조회 (STR-020: 대시보드) + * @param {string} period - 조회 기간 (today, week, month, year) + * @returns {Promise} 매출 정보 + */ + async getSales(period = 'today') { + try { + // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) + const mockSalesData = { + todaySales: 150000, + yesterdaySales: 120000, + changeRate: 25.0, + monthlyTarget: 3000000, + achievementRate: 45.2 + } + + return formatSuccessResponse(mockSalesData, '매출 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 메뉴 목록 조회 (개발 예정) + * @returns {Promise} 메뉴 목록 + */ + async getMenus() { + try { + // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) + const mockMenus = [ + { + id: 1, + name: '아메리카노', + price: 4000, + category: '커피', + description: '진한 풍미의 아메리카노', + imageUrl: '/images/americano.jpg', + isAvailable: true + }, + { + id: 2, + name: '카페라떼', + price: 4500, + category: '커피', + description: '부드러운 우유가 들어간 라떼', + imageUrl: '/images/latte.jpg', + isAvailable: true + } + ] + + return formatSuccessResponse(mockMenus, '메뉴 목록을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } +} + +// 싱글톤 인스턴스 생성 및 export +export const storeService = new StoreService() +export default storeService + +// 디버깅을 위한 전역 노출 (개발 환경에서만) +if (process.env.NODE_ENV === 'development') { + window.storeService = storeService +} \ No newline at end of file diff --git a/src/views/StoreManagementView.vue b/src/views/StoreManagementView.vue index 7494f3f..adb6713 100644 --- a/src/views/StoreManagementView.vue +++ b/src/views/StoreManagementView.vue @@ -1,4 +1,5 @@ -//* src/views/StoreManagementView.vue +//* src/views/StoreManagementView.vue - 완전한 매장 등록 화면 + @@ -941,51 +534,16 @@ import { ref, computed, onMounted } from 'vue' import { useStoreStore } from '@/store/index' -/** - * AI 마케팅 서비스 - 매장 관리 페이지 - * 매장 정보 관리 및 메뉴 관리 기능 제공 - * 유저스토리: STR-005, STR-010, STR-015, STR-020, STR-025, STR-030, STR-035, STR-040 - */ - -// 스토어 const storeStore = useStoreStore() -// 탭 관리 +// 반응형 상태 const currentTab = ref('basic') - -// 매장 정보 관련 상태 const showCreateDialog = ref(false) const editMode = ref(false) const formValid = ref(false) const saving = ref(false) -const imageInput = ref(null) +const storeFormRef = ref(null) // 폼 참조 -// 메뉴 관리 관련 상태 -const showMenuDialog = ref(false) -const editMenuMode = ref(false) -const menuFormValid = ref(false) -const savingMenu = ref(false) -const deletingMenu = ref(false) -const showDeleteMenuDialog = ref(false) -const deleteMenuTarget = ref(null) -const menuImageInput = ref(null) -const menuSearch = ref('') -const menuCategoryFilter = ref('전체') -const menuStatusFilter = ref('전체') - -// SNS 연동 관련 상태 -const snsCheckLoading = ref({ - instagram: false, - blog: false -}) -const showSnsResultDialog = ref(false) -const snsConnectionResult = ref({ - success: false, - platform: '', - message: '' -}) - -// 스낵바 상태 const snackbar = ref({ show: false, message: '', @@ -996,8 +554,6 @@ const snackbar = ref({ const formData = ref({ storeName: '', businessType: '', - ownerName: '', - businessNumber: '', address: '', phoneNumber: '', seatCount: 0, @@ -1006,111 +562,15 @@ const formData = ref({ openTime: '09:00', closeTime: '21:00', holidays: [], - deliveryAvailable: false, - takeoutAvailable: true, - imageUrl: '' + description: '' }) -// 메뉴 폼 데이터 -const menuFormData = ref({ - menuName: '', - price: 0, - category: '', - description: '', - available: true, - recommended: false, - imageUrl: '' -}) - -// 메뉴 목록 (데모 데이터) -const menus = ref([ - { - id: 1, - menuName: '김치찌개', - category: '찌개류', - price: 8000, - description: '돼지고기와 신김치로 끓인 얼큰한 김치찌개', - available: true, - recommended: true, - imageUrl: '/images/kimchi-jjigae.jpg' - }, - { - id: 2, - menuName: '된장찌개', - category: '찌개류', - price: 7000, - description: '집된장으로 끓인 구수한 된장찌개', - available: true, - recommended: false, - imageUrl: '/images/doenjang-jjigae.jpg' - }, - { - id: 3, - menuName: '제육볶음', - category: '볶음류', - price: 12000, - description: '매콤달콤한 양념에 볶은 제육볶음', - available: false, - recommended: true, - imageUrl: '/images/jeyuk-bokkeum.jpg' - } -]) - -// 컴퓨티드 속성 -const storeInfo = computed(() => storeStore.storeInfo || {}) - -const availableMenusCount = computed(() => - menus.value.filter(menu => menu.available).length -) - -const recommendedMenusCount = computed(() => - menus.value.filter(menu => menu.recommended).length -) - -const averagePrice = computed(() => { - if (menus.value.length === 0) return '0원' - const total = menus.value.reduce((sum, menu) => sum + menu.price, 0) - const average = Math.round(total / menus.value.length) - return formatCurrency(average) -}) - -const filteredMenus = computed(() => { - let filtered = menus.value - - // 검색 필터 - if (menuSearch.value) { - filtered = filtered.filter(menu => - menu.menuName.toLowerCase().includes(menuSearch.value.toLowerCase()) || - menu.description.toLowerCase().includes(menuSearch.value.toLowerCase()) - ) - } - - // 카테고리 필터 - if (menuCategoryFilter.value !== '전체') { - filtered = filtered.filter(menu => menu.category === menuCategoryFilter.value) - } - - // 상태 필터 - if (menuStatusFilter.value !== '전체') { - const isAvailable = menuStatusFilter.value === '판매중' - filtered = filtered.filter(menu => menu.available === isAvailable) - } - - return filtered -}) - -const menuCategories = computed(() => { - const categories = [...new Set(menus.value.map(menu => menu.category))] - return categories.sort() -}) - -// 선택 옵션 +// 선택 옵션들 const businessTypes = [ - '한식', '중식', '일식', '양식', '분식', '치킨', '피자', '버거', - '카페', '디저트', '술집', '기타' + '카페', '레스토랑', '베이커리', '디저트', '주점', '패스트푸드', '기타' ] -const daysOfWeek = [ +const weekDays = [ { title: '월요일', value: 'monday' }, { title: '화요일', value: 'tuesday' }, { title: '수요일', value: 'wednesday' }, @@ -1120,219 +580,87 @@ const daysOfWeek = [ { title: '일요일', value: 'sunday' } ] -// 유효성 검사 규칙 -const businessNumberRules = [ - v => !!v || '사업자등록번호를 입력해주세요', - v => /^\d{3}-\d{2}-\d{5}$/.test(v) || '올바른 사업자등록번호 형식이 아닙니다 (예: 123-45-67890)' -] +// 컴퓨티드 속성 +const storeInfo = computed(() => storeStore.storeInfo || {}) -const phoneRules = [ - v => !!v || '연락처를 입력해주세요', - v => /^[0-9-+\s()]+$/.test(v) || '올바른 연락처 형식이 아닙니다' -] - -const priceRules = [ - v => !!v || '가격을 입력해주세요', - v => v > 0 || '가격은 0보다 커야 합니다' -] - -// 유틸리티 함수 -const formatCurrency = (amount) => { - return new Intl.NumberFormat('ko-KR', { - style: 'currency', - currency: 'KRW' - }).format(amount) -} - -const formatHolidays = (holidays) => { - if (!holidays || holidays.length === 0) return '없음' - const dayNames = { - monday: '월요일', - tuesday: '화요일', - wednesday: '수요일', - thursday: '목요일', - friday: '금요일', - saturday: '토요일', - sunday: '일요일' - } - return holidays.map(day => dayNames[day]).join(', ') -} - -const getCategoryColor = (category) => { - const colors = { - '면류': 'orange', - '튀김류': 'amber', - '음료': 'blue', - '안주': 'green', - '디저트': 'pink', - '찌개류': 'red', - '볶음류': 'purple' - } - return colors[category] || 'grey' -} - -// 메서드 - -/** - * SNS 계정 연동 확인 - * @param {string} platform - SNS 플랫폼 (instagram, blog) - */ -const checkSnsConnection = async (platform) => { - console.log(`${platform} 연동 확인 시작`) +// 유틸리티 함수들 +const formatClosedDays = (closedDays) => { + if (!closedDays) return '미설정' - snsCheckLoading.value[platform] = true + if (typeof closedDays === 'string') { + return closedDays + } - try { - // 개발 모드에서는 빠른 시뮬레이션 - const delay = import.meta.env.DEV ? 1000 : 2000 - await new Promise(resolve => setTimeout(resolve, delay)) - - // 랜덤하게 성공/실패 결정 (실제로는 API 응답에 따라 결정) - const isSuccess = Math.random() > 0.3 - - snsConnectionResult.value.success = isSuccess - snsConnectionResult.value.platform = platform - - if (isSuccess) { - snsConnectionResult.value.message = platform === 'instagram' - ? '인스타그램 계정 연동이 확인되었습니다!' - : '네이버 블로그 연동이 확인되었습니다!' - } else { - snsConnectionResult.value.message = platform === 'instagram' - ? '인스타그램 계정을 찾을 수 없거나 연동할 수 없습니다. URL을 확인해주세요.' - : '네이버 블로그를 찾을 수 없거나 연동할 수 없습니다. URL을 확인해주세요.' + if (Array.isArray(closedDays)) { + const dayNames = { + 'monday': '월요일', + 'tuesday': '화요일', + 'wednesday': '수요일', + 'thursday': '목요일', + 'friday': '금요일', + 'saturday': '토요일', + 'sunday': '일요일' } - showSnsResultDialog.value = true - - } catch (error) { - console.error('SNS 연동 확인 중 오류:', error) - snsConnectionResult.value.success = false - snsConnectionResult.value.message = '연동 확인 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' - showSnsResultDialog.value = true - } finally { - snsCheckLoading.value[platform] = false - } -} - -/** - * 데모 매장 데이터 로드 (개발용) - */ -const loadDemoStoreData = () => { - console.log('데모 매장 데이터 로드') - - // 데모 매장 정보 설정 - const demoStoreInfo = { - id: 1, - storeName: '김씨네 분식점', - businessType: '분식', - ownerName: '김점주', - businessNumber: '123-45-67890', - address: '서울특별시 강남구 테헤란로 123', - phoneNumber: '02-1234-5678', - seatCount: 20, - instagramUrl: '@kimfood_bunsik', - blogUrl: 'blog.naver.com/kimfood123', - openTime: '09:00', - closeTime: '21:00', - holidays: ['sunday'], - deliveryAvailable: true, - takeoutAvailable: true, - imageUrl: '/images/store-demo.jpg' + return closedDays.map(day => dayNames[day] || day).join(', ') || '연중무휴' } - // 스토어에 데모 데이터 직접 설정 - storeStore.storeInfo = demoStoreInfo - - showSnackbar('데모 매장 정보가 로드되었습니다', 'success') + return '미설정' +} + +const showSnackbar = (message, color = 'success') => { + snackbar.value.message = message + snackbar.value.color = color + snackbar.value.show = true +} + +// 메서드들 +const openCreateDialog = () => { + console.log('새 매장 등록 다이얼로그 열기') + editMode.value = false + resetForm() + showCreateDialog.value = true } -/** - * 기본 정보 수정 모드로 전환 - */ const editBasicInfo = () => { + console.log('매장 정보 수정 시작') editMode.value = true - // 현재 매장 정보를 폼 데이터에 복사 - Object.assign(formData.value, { - storeName: storeInfo.value.storeName || '', - businessType: storeInfo.value.businessType || '', - ownerName: storeInfo.value.ownerName || '', - businessNumber: storeInfo.value.businessNumber || '', - address: storeInfo.value.address || '', - phoneNumber: storeInfo.value.phoneNumber || '', - seatCount: storeInfo.value.seatCount || 0, - instagramUrl: storeInfo.value.instagramUrl || '', - blogUrl: storeInfo.value.blogUrl || '', - openTime: storeInfo.value.openTime || '09:00', - closeTime: storeInfo.value.closeTime || '21:00', - holidays: storeInfo.value.holidays || [], - deliveryAvailable: storeInfo.value.deliveryAvailable || false, - takeoutAvailable: storeInfo.value.takeoutAvailable || true, - imageUrl: storeInfo.value.imageUrl || '' - }) + // 기존 매장 정보로 폼 데이터 설정 + const store = storeInfo.value + formData.value = { + storeName: store.storeName || '', + businessType: store.businessType || '', + address: store.address || '', + phoneNumber: store.phoneNumber || '', + seatCount: store.seatCount || 0, + instagramUrl: store.instaAccounts || '', + blogUrl: store.blogAccounts || '', + openTime: store.openTime || '09:00', + closeTime: store.closeTime || '21:00', + holidays: store.holidays || [], + description: store.description || '' + } showCreateDialog.value = true } -/** - * 이미지 선택 - */ -const selectImage = () => { - imageInput.value?.click() +const addMenu = () => { + console.log('메뉴 추가 (개발 예정)') + showSnackbar('메뉴 관리 기능은 곧 업데이트 예정입니다', 'info') } -/** - * 이미지 업로드 처리 - */ -const handleImageUpload = (event) => { - const file = event.target.files[0] - if (file) { - // 실제로는 서버에 업로드하고 URL을 받아옴 - const reader = new FileReader() - reader.onload = (e) => { - formData.value.imageUrl = e.target.result - } - reader.readAsDataURL(file) - } -} - -/** - * 매장 정보 저장 - */ -const saveStoreInfo = async () => { - if (!storeForm.value?.validate()) return - - console.log('매장 정보 저장:', storeFormData.value) - - try { - const result = await storeStore.saveStoreInfo(storeFormData.value) - - if (result.success) { - appStore.showSnackbar(result.message || '매장 정보가 저장되었습니다', 'success') - showStoreForm.value = false - } else { - appStore.showSnackbar(result.error || '저장에 실패했습니다', 'error') - } - } catch (error) { - console.error('매장 정보 저장 실패:', error) - appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') - } -} - -/** - * 다이얼로그 닫기 - */ -const closeDialog = () => { +const cancelForm = () => { + console.log('폼 취소') showCreateDialog.value = false editMode.value = false - - // 폼 데이터 초기화 - Object.assign(formData.value, { + resetForm() +} + +const resetForm = () => { + formData.value = { storeName: '', businessType: '', - ownerName: '', - businessNumber: '', address: '', phoneNumber: '', seatCount: 0, @@ -1341,562 +669,141 @@ const closeDialog = () => { openTime: '09:00', closeTime: '21:00', holidays: [], - deliveryAvailable: false, - takeoutAvailable: true, - imageUrl: '' - }) -} - -// 메뉴 관리 메서드 - -/** - * 메뉴 등록 다이얼로그 열기 - */ -const openCreateMenuDialog = () => { - editMenuMode.value = false - resetMenuForm() - showMenuDialog.value = true -} - -/** - * 메뉴 수정 - */ -const editMenu = (menu) => { - editMenuMode.value = true - Object.assign(menuFormData.value, menu) - showMenuDialog.value = true -} - -/** - * 메뉴 삭제 확인 - */ -const confirmDeleteMenu = (menu) => { - deleteMenuTarget.value = menu - showDeleteMenuDialog.value = true -} - -/** - * 메뉴 이미지 선택 - */ -const selectMenuImage = () => { - menuImageInput.value?.click() -} - -/** - * 메뉴 이미지 업로드 처리 - */ -const handleMenuImageUpload = (event) => { - const file = event.target.files[0] - if (file) { - const reader = new FileReader() - reader.onload = (e) => { - menuFormData.value.imageUrl = e.target.result - } - reader.readAsDataURL(file) + description: '' + } + + // 폼 validation 초기화 + if (storeFormRef.value) { + storeFormRef.value.resetValidation() } } -/** - * 메뉴 저장 - */ -const saveMenu = async () => { - if (!menuForm.value?.validate()) return - - console.log('메뉴 저장:', menuFormData.value) +// 매장 정보 저장 함수 - 완전히 새로 작성 +const saveStoreInfo = async () => { + console.log('=== 매장 정보 저장 시작 ===') + console.log('편집 모드:', editMode.value) + console.log('폼 데이터:', formData.value) + + // 폼 유효성 검사 + if (!storeFormRef.value) { + console.error('폼 참조를 찾을 수 없습니다') + showSnackbar('폼 오류가 발생했습니다', 'error') + return + } + + const { valid } = await storeFormRef.value.validate() + if (!valid) { + console.log('폼 유효성 검사 실패') + showSnackbar('필수 정보를 모두 입력해주세요', 'error') + return + } + + saving.value = true try { + // 백엔드에 보낼 데이터 형식으로 변환 + const storeData = { + storeName: formData.value.storeName, + businessType: formData.value.businessType, + address: formData.value.address, + phoneNumber: formData.value.phoneNumber, + businessHours: `${formData.value.openTime}-${formData.value.closeTime}`, + closedDays: Array.isArray(formData.value.holidays) ? formData.value.holidays.join(',') : '', + seatCount: parseInt(formData.value.seatCount) || 0, + instaAccounts: formData.value.instagramUrl || '', + blogAccounts: formData.value.blogUrl || '', + description: formData.value.description || '' + } + + console.log('백엔드로 전송할 데이터:', storeData) + let result - if (isMenuEdit.value && editingMenuId.value) { - result = await storeStore.updateMenu(editingMenuId.value, menuFormData.value) + if (editMode.value) { + // 매장 정보 수정 + result = await storeStore.updateStore(storeInfo.value.storeId, storeData) } else { - result = await storeStore.saveMenu(menuFormData.value) + // 새 매장 등록 + result = await storeStore.registerStore(storeData) } - if (result.success) { - appStore.showSnackbar(result.message || '메뉴가 저장되었습니다', 'success') - showMenuForm.value = false - resetMenuForm() - } else { - appStore.showSnackbar(result.error || '저장에 실패했습니다', 'error') - } - } catch (error) { - console.error('메뉴 저장 실패:', error) - appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') - } -} - -/** - * 메뉴 삭제 - */ -const deleteMenu = async (menuId) => { - try { - const result = await storeStore.deleteMenu(menuId) + console.log('저장 결과:', result) if (result.success) { - appStore.showSnackbar(result.message || '메뉴가 삭제되었습니다', 'success') + showSnackbar( + editMode.value ? '매장 정보가 수정되었습니다' : '매장이 등록되었습니다', + 'success' + ) + showCreateDialog.value = false + editMode.value = false + resetForm() + + // 매장 정보 다시 조회 + await storeStore.fetchStoreInfo() } else { - appStore.showSnackbar(result.error || '삭제에 실패했습니다', 'error') + showSnackbar(result.message || '저장에 실패했습니다', 'error') } } catch (error) { - console.error('메뉴 삭제 실패:', error) - appStore.showSnackbar('네트워크 오류가 발생했습니다', 'error') + console.error('매장 정보 저장 중 오류:', error) + showSnackbar('저장 중 오류가 발생했습니다', 'error') + } finally { + saving.value = false } } -/** - * 메뉴 다이얼로그 닫기 - */ -const closeMenuDialog = () => { - showMenuDialog.value = false - editMenuMode.value = false - resetMenuForm() -} - -/** - * 메뉴 폼 초기화 - */ -const resetMenuForm = () => { - Object.assign(menuFormData.value, { - menuName: '', - price: 0, - category: '', - description: '', - available: true, - recommended: false, - imageUrl: '' - }) -} - -/** - * 필터 초기화 - */ -const clearFilters = () => { - menuSearch.value = '' - menuCategoryFilter.value = '전체' - menuStatusFilter.value = '전체' -} - -/** - * 스낵바 표시 - */ -const showSnackbar = (message, color = 'success') => { - snackbar.value.message = message - snackbar.value.color = color - snackbar.value.show = true -} - -// 메뉴 상세 다이얼로그 관련 상태 (기존 상태들에 추가) -const showMenuDetailDialog = ref(false) -const selectedMenuDetail = ref(null) - -// 메뉴 관리 메서드에 추가할 함수들 - -/** - * 메뉴 상세보기 - */ -const viewMenuDetail = (menu) => { - selectedMenuDetail.value = { ...menu } - showMenuDetailDialog.value = true -} - -/** - * 상세화면에서 수정 모드로 전환 - */ -const editFromDetail = () => { - showMenuDetailDialog.value = false - editMenuMode.value = true - Object.assign(menuFormData.value, selectedMenuDetail.value) - showMenuDialog.value = true -} - /** * 컴포넌트 마운트 시 실행 */ onMounted(async () => { - console.log('StoreManagementView 마운트됨') + console.log('=== StoreManagementView 마운트됨 ===') try { - // 매장 정보 로드 - if (!storeStore.hasStoreInfo) { - await storeStore.fetchStoreInfo() + // 매장 정보 조회 + const result = await storeStore.fetchStoreInfo() + + console.log('매장 정보 조회 결과:', result) + + if (result.success) { + console.log('✅ 매장 정보 로드 완료:', result.data) + + // 메뉴 관리 탭에서 사용할 메뉴 정보도 로드 + try { + await storeStore.fetchMenus() + console.log('메뉴 정보 로드 완료') + } catch (menuError) { + console.log('메뉴 정보 로드 실패 (개발 중이므로 무시):', menuError) + } + } else { + if (result.message === '등록된 매장이 없습니다') { + console.log('⚠️ 등록된 매장이 없음 - 등록 화면 표시') + // 매장이 없는 경우는 정상적인 상황이므로 에러 메시지 표시하지 않음 + } else { + console.warn('❌ 매장 정보 조회 실패:', result.message) + showSnackbar(result.message || '매장 정보를 불러오는데 실패했습니다', 'error') + } } - - // 메뉴 목록 로드 - await storeStore.fetchMenus() - } catch (error) { - console.warn('매장 관리 데이터 로드 실패 (개발 중이므로 무시):', error) + console.error('매장 정보 조회 중 예외 발생:', error) + showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error') } }) \ No newline at end of file From 87871709f2bbcdab5df004a3954d6ba0af3cadce Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Jun 2025 16:33:19 +0900 Subject: [PATCH 4/4] store menu source edit --- src/services/menu.js | 210 ++++++++++ src/store/index.js | 76 ++-- src/store/store.js | 345 +++++++--------- src/views/StoreManagementView.vue | 635 +++++++++++++++++++++++++++++- 4 files changed, 1013 insertions(+), 253 deletions(-) create mode 100644 src/services/menu.js diff --git a/src/services/menu.js b/src/services/menu.js new file mode 100644 index 0000000..d911e78 --- /dev/null +++ b/src/services/menu.js @@ -0,0 +1,210 @@ +// src/services/menu.js - 메뉴 관련 API 서비스 +import { menuApi, handleApiError, formatSuccessResponse } from './api.js' + +/** + * 메뉴 관련 API 서비스 + * 백엔드 Menu Controller와 연동 (포트 8082) + */ +class MenuService { + /** + * 메뉴 목록 조회 + * @param {number} storeId - 매장 ID + * @returns {Promise} 메뉴 목록 + */ + async getMenus(storeId) { + try { + console.log('=== 메뉴 목록 조회 API 호출 ===') + console.log('매장 ID:', storeId) + + if (!storeId) { + throw new Error('매장 ID가 필요합니다') + } + + // GET /api/menu?storeId={storeId} + const response = await menuApi.get('', { + params: { storeId } + }) + + console.log('메뉴 목록 조회 API 응답:', response.data) + + if (response.data && response.data.status === 200) { + return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.') + } else { + throw new Error(response.data.message || '메뉴 목록을 찾을 수 없습니다.') + } + } catch (error) { + console.error('메뉴 목록 조회 실패:', error) + + // 404 오류 또는 네트워크 오류 시 빈 배열 반환 (개발 중) + if (error.response?.status === 404 || + error.code === 'ECONNREFUSED' || + error.message.includes('Network Error')) { + console.warn('백엔드 미구현 또는 네트워크 오류 - 빈 메뉴 목록 반환') + return formatSuccessResponse([], '메뉴 목록이 비어있습니다.') + } + + return handleApiError(error) + } + } + + /** + * 메뉴 상세 조회 + * @param {number} menuId - 메뉴 ID + * @returns {Promise} 메뉴 상세 정보 + */ + async getMenuDetail(menuId) { + try { + console.log('=== 메뉴 상세 조회 API 호출 ===') + console.log('메뉴 ID:', menuId) + + if (!menuId || menuId === 'undefined') { + throw new Error('올바른 메뉴 ID가 필요합니다') + } + + const numericMenuId = parseInt(menuId) + if (isNaN(numericMenuId)) { + throw new Error('메뉴 ID는 숫자여야 합니다') + } + + // GET /api/menu/{menuId} + const response = await menuApi.get(`/${numericMenuId}`) + + console.log('메뉴 상세 조회 API 응답:', response.data) + + if (response.data && response.data.status === 200) { + return formatSuccessResponse(response.data.data, '메뉴 상세 정보를 조회했습니다.') + } else { + throw new Error(response.data.message || '메뉴를 찾을 수 없습니다.') + } + } catch (error) { + console.error('메뉴 상세 조회 실패:', error) + return handleApiError(error) + } + } + + /** + * 메뉴 등록 + * @param {Object} menuData - 메뉴 정보 + * @returns {Promise} 등록 결과 + */ + async createMenu(menuData) { + try { + console.log('=== 메뉴 등록 API 호출 ===') + console.log('요청 데이터:', menuData) + + const requestData = { + storeId: menuData.storeId, + menuName: menuData.menuName, + category: menuData.category, + price: parseInt(menuData.price) || 0, + description: menuData.description || '' + } + + console.log('백엔드 전송 데이터:', requestData) + + // POST /api/menu/register + const response = await menuApi.post('/register', requestData) + + console.log('메뉴 등록 API 응답:', response.data) + + if (response.data && response.data.status === 200) { + return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 등록되었습니다.') + } else { + throw new Error(response.data.message || '메뉴 등록에 실패했습니다.') + } + } catch (error) { + console.error('메뉴 등록 실패:', error) + return handleApiError(error) + } + } + + /** + * 메뉴 수정 + * @param {number} menuId - 메뉴 ID + * @param {Object} menuData - 수정할 메뉴 정보 + * @returns {Promise} 수정 결과 + */ + async updateMenu(menuId, menuData) { + try { + console.log('=== 메뉴 수정 API 호출 ===') + console.log('메뉴 ID:', menuId) + console.log('수정 데이터:', menuData) + + if (!menuId || menuId === 'undefined') { + throw new Error('올바른 메뉴 ID가 필요합니다') + } + + const numericMenuId = parseInt(menuId) + if (isNaN(numericMenuId)) { + throw new Error('메뉴 ID는 숫자여야 합니다') + } + + const requestData = { + menuName: menuData.menuName, + category: menuData.category, + price: parseInt(menuData.price) || 0, + description: menuData.description || '' + } + + console.log('백엔드 전송 데이터:', requestData) + + // PUT /api/menu/{menuId} + const response = await menuApi.put(`/${numericMenuId}`, requestData) + + console.log('메뉴 수정 API 응답:', response.data) + + if (response.data && response.data.status === 200) { + return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 수정되었습니다.') + } else { + throw new Error(response.data.message || '메뉴 수정에 실패했습니다.') + } + } catch (error) { + console.error('메뉴 수정 실패:', error) + return handleApiError(error) + } + } + + /** + * 메뉴 삭제 + * @param {number} menuId - 메뉴 ID + * @returns {Promise} 삭제 결과 + */ + async deleteMenu(menuId) { + try { + console.log('=== 메뉴 삭제 API 호출 ===') + console.log('메뉴 ID:', menuId) + + if (!menuId || menuId === 'undefined') { + throw new Error('올바른 메뉴 ID가 필요합니다') + } + + const numericMenuId = parseInt(menuId) + if (isNaN(numericMenuId)) { + throw new Error('메뉴 ID는 숫자여야 합니다') + } + + // DELETE /api/menu/{menuId} + const response = await menuApi.delete(`/${numericMenuId}`) + + console.log('메뉴 삭제 API 응답:', response.data) + + if (response.data && response.data.status === 200) { + return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 삭제되었습니다.') + } else { + throw new Error(response.data.message || '메뉴 삭제에 실패했습니다.') + } + } catch (error) { + console.error('메뉴 삭제 실패:', error) + return handleApiError(error) + } + } +} + +// 싱글톤 인스턴스 생성 및 export +export const menuService = new MenuService() +export default menuService + +// 디버깅을 위한 전역 노출 (개발 환경에서만) +if (process.env.NODE_ENV === 'development') { + window.menuService = menuService +} \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index 266c5b2..da8b7ff 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -80,51 +80,53 @@ export const useStoreStore = defineStore('store', { }, /** - * 메뉴 목록 조회 - 추가된 메서드 + * 메뉴 목록 조회 - 실제 API 연동 (매장 ID 필요) */ async fetchMenus() { console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===') try { - // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) - const mockMenus = [ - { - id: 1, - name: '아메리카노', - price: 4000, - category: '커피', - description: '진한 풍미의 아메리카노', - imageUrl: '/images/americano.jpg', - isAvailable: true - }, - { - id: 2, - name: '카페라떼', - price: 4500, - category: '커피', - description: '부드러운 우유가 들어간 라떼', - imageUrl: '/images/latte.jpg', - isAvailable: true - }, - { - id: 3, - name: '치즈케이크', - price: 6000, - category: '디저트', - description: '진한 크림치즈로 만든 케이크', - imageUrl: '/images/cheesecake.jpg', - isAvailable: false + // 매장 정보에서 storeId 가져오기 + const storeId = this.storeInfo?.storeId + if (!storeId) { + console.warn('매장 ID가 없습니다. 매장 정보를 먼저 조회해주세요.') + return { success: false, message: '매장 정보가 필요합니다', data: [] } + } + + // 메뉴 서비스 임포트 + const { menuService } = await import('@/services/menu') + + console.log('메뉴 목록 API 호출, 매장 ID:', storeId) + const result = await menuService.getMenus(storeId) + + 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.menus = result.data + return { success: true, data: result.data } + } else { + // 메뉴가 없거나 조회 실패한 경우 + console.log('⚠️ 메뉴 목록 없음 또는 조회 실패') + this.menus = [] + + if (result.message === '등록된 메뉴가 없습니다') { + return { success: false, message: '등록된 메뉴가 없습니다', data: [] } + } else { + return { success: false, message: result.message || '메뉴 목록 조회에 실패했습니다', data: [] } } - ] - - this.menus = mockMenus - console.log('✅ 메뉴 목록 설정 완료:', mockMenus) - - return { success: true, data: mockMenus } + } } catch (error) { - console.error('메뉴 목록 조회 실패:', error) + console.error('=== Store 스토어: 메뉴 목록 조회 실패 ===') + console.error('Error:', error) + this.menus = [] - return { success: false, message: '메뉴 목록을 불러오는데 실패했습니다' } + return { success: false, message: error.message || '메뉴 목록을 불러오는데 실패했습니다', data: [] } } }, diff --git a/src/store/store.js b/src/store/store.js index f20178e..d0739de 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,233 +1,164 @@ -//* src/services/store.js - 매장 서비스 완전 수정 -import { storeApi, handleApiError, formatSuccessResponse } from './api.js' +// src/store/store.js - StoreService 클래스의 getMenus 메서드 올바른 문법으로 수정 /** - * 매장 관련 API 서비스 - * 백엔드 Store Controller와 연동 (포트 8082) + * 메뉴 목록 조회 (수정된 버전 - storeId 파라미터 추가) + * @param {number} storeId - 매장 ID (옵션, 없으면 목업 데이터 반환) + * @returns {Promise} 메뉴 목록 */ -class StoreService { - /** - * 매장 등록 (STR-015: 매장 등록) - * @param {Object} storeData - 매장 정보 - * @returns {Promise} 매장 등록 결과 - */ - async registerStore(storeData) { - try { - console.log('=== 매장 등록 API 호출 ===') - console.log('요청 데이터:', storeData) - - // 백엔드 StoreCreateRequest에 맞는 형태로 변환 - const requestData = { - storeName: storeData.storeName, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours, - closedDays: storeData.closedDays, - seatCount: parseInt(storeData.seatCount) || 0, - instaAccounts: storeData.instaAccounts || '', - blogAccounts: storeData.blogAccounts || '', - description: storeData.description || '' - } - - console.log('=== 각 필드 상세 검증 ===') - console.log('storeName:', requestData.storeName, '(타입:', typeof requestData.storeName, ')') - console.log('businessType:', requestData.businessType, '(타입:', typeof requestData.businessType, ')') - console.log('address:', requestData.address, '(타입:', typeof requestData.address, ')') - console.log('seatCount:', requestData.seatCount, '(타입:', typeof requestData.seatCount, ')') - - console.log('백엔드 전송 데이터:', requestData) - - const response = await storeApi.post('/register', requestData) - - console.log('매장 등록 API 응답:', response.data) - - // 백엔드 응답 구조에 맞게 처리 - if (response.data && (response.data.status === 200 || response.data.success !== false)) { - return { - success: true, - message: response.data.message || '매장이 등록되었습니다.', - data: response.data.data - } - } else { - throw new Error(response.data.message || '매장 등록에 실패했습니다.') - } - } catch (error) { - console.error('매장 등록 실패:', error) - - if (error.response) { - console.error('응답 상태:', error.response.status) - console.error('응답 데이터:', error.response.data) - } - - return handleApiError(error) - } - } - - /** - * 매장 정보 조회 (STR-005: 매장 정보 관리) - * @returns {Promise} 매장 정보 - */ - async getStore() { - try { - console.log('=== 매장 정보 조회 API 호출 ===') - - const response = await storeApi.get('/') - - console.log('매장 정보 조회 API 응답:', response.data) - - // 백엔드 응답 구조에 맞게 처리 - if (response.data && response.data.data) { - return { - success: true, - message: '매장 정보를 조회했습니다.', - data: response.data.data - } - } else if (response.data && response.data.data === null) { - // 매장이 없는 경우 - return { - success: false, - message: '등록된 매장이 없습니다', - data: null - } - } else { - throw new Error(response.data.message || '매장 정보를 찾을 수 없습니다.') - } - } catch (error) { - console.error('매장 정보 조회 실패:', error) - - // 404 오류 처리 (매장이 없음) - if (error.response?.status === 404) { - return { - success: false, - message: '등록된 매장이 없습니다', - data: null - } - } - - // 500 오류 처리 (서버 내부 오류) - if (error.response?.status === 500) { - console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) - return { - success: false, - message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.', - data: null - } - } - - return handleApiError(error) - } - } - - /** - * 매장 정보 수정 (STR-010: 매장 수정) - * @param {number} storeId - 매장 ID (현재는 사용하지 않음 - JWT에서 사용자 확인) - * @param {Object} storeData - 수정할 매장 정보 - * @returns {Promise} 매장 수정 결과 - */ - async updateStore(storeId, storeData) { - try { - console.log('=== 매장 정보 수정 API 호출 ===') - console.log('요청 데이터:', storeData) - - // 백엔드 StoreUpdateRequest에 맞는 형태로 변환 - const requestData = { - storeName: storeData.storeName, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours, - closedDays: storeData.closedDays, - seatCount: parseInt(storeData.seatCount) || 0, - instaAccounts: storeData.instaAccounts || '', - blogAccounts: storeData.blogAccounts || '', - description: storeData.description || '' - } - - console.log('백엔드 전송 데이터:', requestData) - - // PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음) - const response = await storeApi.put('/', requestData) - - console.log('매장 정보 수정 API 응답:', response.data) - - if (response.data && (response.data.status === 200 || response.data.success !== false)) { - return { - success: true, - message: response.data.message || '매장 정보가 수정되었습니다.', - data: response.data.data - } - } else { - throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.') - } - } catch (error) { - console.error('매장 정보 수정 실패:', error) - return handleApiError(error) - } - } - - /** - * 매출 정보 조회 (STR-020: 대시보드) - * @param {string} period - 조회 기간 (today, week, month, year) - * @returns {Promise} 매출 정보 - */ - async getSales(period = 'today') { - try { - // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) - const mockSalesData = { - todaySales: 150000, - yesterdaySales: 120000, - changeRate: 25.0, - monthlyTarget: 3000000, - achievementRate: 45.2 - } - - return formatSuccessResponse(mockSalesData, '매출 정보를 조회했습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 메뉴 목록 조회 (개발 예정) - * @returns {Promise} 메뉴 목록 - */ - async getMenus() { - try { - // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) +async getMenus(storeId) { + try { + console.log('=== 메뉴 목록 조회 API 호출 ===') + console.log('매장 ID:', storeId) + + // storeId가 없으면 목업 데이터 반환 (개발 중) + if (!storeId) { + console.warn('매장 ID가 없어서 목업 데이터 반환') const mockMenus = [ { - id: 1, + menuId: 1, // id 대신 menuId 사용 + id: 1, // 호환성을 위해 name: '아메리카노', + menuName: '아메리카노', // 백엔드 형식 price: 4000, category: '커피', description: '진한 풍미의 아메리카노', imageUrl: '/images/americano.jpg', - isAvailable: true + isAvailable: true, + available: true // 백엔드 형식 }, { + menuId: 2, id: 2, name: '카페라떼', + menuName: '카페라떼', price: 4500, category: '커피', description: '부드러운 우유가 들어간 라떼', imageUrl: '/images/latte.jpg', - isAvailable: true + isAvailable: true, + available: true } ] - return formatSuccessResponse(mockMenus, '메뉴 목록을 조회했습니다.') - } catch (error) { - return handleApiError(error) + return formatSuccessResponse(mockMenus, '목업 메뉴 목록을 조회했습니다.') } + + // 실제 백엔드 API 호출 + try { + // 메뉴 API import + const { menuApi } = await import('./api.js') + + // GET /api/menu?storeId={storeId} + const response = await menuApi.get('', { + params: { storeId } + }) + + console.log('메뉴 목록 조회 API 응답:', response.data) + + if (response.data && response.data.status === 200) { + // 백엔드에서 받은 메뉴 데이터를 프론트엔드 형식으로 변환 + const menus = response.data.data.map(menu => ({ + menuId: menu.menuId, + id: menu.menuId, // 호환성을 위해 + storeId: menu.storeId, + menuName: menu.menuName, + name: menu.menuName, // 호환성을 위해 + category: menu.category, + price: menu.price, + description: menu.description, + available: menu.available !== undefined ? menu.available : true, + isAvailable: menu.available !== undefined ? menu.available : true, // 호환성 + imageUrl: menu.imageUrl || '/images/menu-placeholder.png', + createdAt: menu.createdAt, + updatedAt: menu.updatedAt + })) + + return formatSuccessResponse(menus, '메뉴 목록을 조회했습니다.') + } else { + throw new Error(response.data.message || '메뉴 목록을 찾을 수 없습니다.') + } + } catch (apiError) { + console.error('백엔드 API 호출 실패:', apiError) + + // 백엔드 미구현이나 네트워크 오류 시 목업 데이터 반환 + if (apiError.response?.status === 404 || + apiError.code === 'ECONNREFUSED' || + apiError.message.includes('Network Error')) { + console.warn('백엔드 미구현 - 목업 데이터 반환') + + const mockMenus = [ + { + menuId: 1, + id: 1, + storeId: storeId, + name: '아메리카노', + menuName: '아메리카노', + price: 4000, + category: '커피', + description: '진한 풍미의 아메리카노', + imageUrl: '/images/americano.jpg', + isAvailable: true, + available: true + }, + { + menuId: 2, + id: 2, + storeId: storeId, + name: '카페라떼', + menuName: '카페라떼', + price: 4500, + category: '커피', + description: '부드러운 우유가 들어간 라떼', + imageUrl: '/images/latte.jpg', + isAvailable: true, + available: true + } + ] + + return formatSuccessResponse(mockMenus, '목업 메뉴 목록을 조회했습니다. (백엔드 미구현)') + } + + throw apiError + } + } catch (error) { + console.error('메뉴 목록 조회 실패:', error) + return handleApiError(error) } } -// 싱글톤 인스턴스 생성 및 export -export const storeService = new StoreService() -export default storeService +// 만약 fetchMenus 메서드가 따로 필요하다면 다음과 같이 추가: +/** + * 메뉴 목록 조회 (fetchMenus 별칭) + * @param {number} storeId - 매장 ID + * @returns {Promise} 메뉴 목록 + */ +async fetchMenus(storeId) { + return await this.getMenus(storeId) +} -// 디버깅을 위한 전역 노출 (개발 환경에서만) -if (process.env.NODE_ENV === 'development') { - window.storeService = storeService -} \ No newline at end of file +// StoreService 클래스 전체 구조 예시: +class StoreService { + // ... 기존 메서드들 (registerStore, getStore, updateStore 등) ... + + /** + * 메뉴 목록 조회 (위의 getMenus 메서드) + */ + async getMenus(storeId) { + // 위의 구현 내용 + } + + /** + * 메뉴 목록 조회 별칭 + */ + async fetchMenus(storeId) { + return await this.getMenus(storeId) + } +} + +// 올바른 JavaScript 클래스 메서드 문법: +// ❌ 잘못된 문법: +// async function getMenus(storeId) { ... } +// function async getMenus(storeId) { ... } + +// ✅ 올바른 문법: +// async getMenus(storeId) { ... } \ No newline at end of file diff --git a/src/views/StoreManagementView.vue b/src/views/StoreManagementView.vue index adb6713..ee2ca70 100644 --- a/src/views/StoreManagementView.vue +++ b/src/views/StoreManagementView.vue @@ -163,7 +163,7 @@ - + @@ -518,6 +518,267 @@ + + + + +
+
+ mdi-food + 메뉴 상세 정보 +
+
+ + + {{ selectedMenu.available ? '판매중' : '품절' }} + + + + + mdi-star + 추천 + +
+
+
+ + + + + + + + + +
+ +
+
+

{{ selectedMenu.menuName }}

+ + {{ formatCurrency(selectedMenu.price) }} + +
+ + + {{ selectedMenu.category }} + + +

+ {{ selectedMenu.description || '메뉴 설명이 없습니다.' }} +

+
+ + + + +
+

+ mdi-information + 상세 정보 +

+ + + +
+ mdi-tag +
+
카테고리
+
{{ selectedMenu.category }}
+
+
+
+ + +
+ mdi-currency-krw +
+
가격
+
{{ formatCurrency(selectedMenu.price) }}
+
+
+
+
+
+
+
+ + + + + + + 닫기 + + + 수정 + + + 삭제 + + +
+
+ + + + + +
+ mdi-food-plus + {{ menuEditMode ? '메뉴 수정' : '메뉴 등록' }} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 취소 + + + {{ menuEditMode ? '수정' : '등록' }} + + +
+
+ import { ref, computed, onMounted } from 'vue' -import { useStoreStore } from '@/store/index' +import { useStoreStore } from '@/store/index' // 올바른 경로로 수정 const storeStore = useStoreStore() @@ -544,6 +805,28 @@ const formValid = ref(false) const saving = ref(false) const storeFormRef = ref(null) // 폼 참조 +// 메뉴 관리 관련 상태 +const menus = ref([]) +const menuSearch = ref('') +const menuCategoryFilter = ref('전체') +const menuCategories = ref(['커피', '음료', '디저트', '베이커리', '샐러드', '샌드위치']) +const showMenuDialog = ref(false) +const menuEditMode = ref(false) +const menuFormRef = ref(null) +const menuFormData = ref({ + menuName: '', + category: '', + price: 0, + description: '', + available: true, + recommended: false, + imageUrl: '' +}) + +// 메뉴 상세 관련 상태 +const showMenuDetailDialog = ref(false) +const selectedMenu = ref(null) + const snackbar = ref({ show: false, message: '', @@ -583,6 +866,27 @@ const weekDays = [ // 컴퓨티드 속성 const storeInfo = computed(() => storeStore.storeInfo || {}) +// 메뉴 관련 컴퓨티드 속성 +const filteredMenus = computed(() => { + let filtered = menus.value + + // 검색 필터 + if (menuSearch.value) { + const search = menuSearch.value.toLowerCase() + filtered = filtered.filter(menu => + menu.menuName.toLowerCase().includes(search) || + menu.description.toLowerCase().includes(search) + ) + } + + // 카테고리 필터 + if (menuCategoryFilter.value && menuCategoryFilter.value !== '전체') { + filtered = filtered.filter(menu => menu.category === menuCategoryFilter.value) + } + + return filtered +}) + // 유틸리티 함수들 const formatClosedDays = (closedDays) => { if (!closedDays) return '미설정' @@ -650,6 +954,161 @@ const addMenu = () => { showSnackbar('메뉴 관리 기능은 곧 업데이트 예정입니다', 'info') } +// 메뉴 관련 메서드들 +const openCreateMenuDialog = () => { + console.log('메뉴 등록 다이얼로그 열기') + menuEditMode.value = false + resetMenuForm() + showMenuDialog.value = true +} + +const editMenu = (menu) => { + console.log('메뉴 수정:', menu) + menuEditMode.value = true + + // 백엔드에서 받은 메뉴 데이터 구조에 맞게 설정 + menuFormData.value = { + id: menu.id || menu.menuId, // 메뉴 ID 추가 + menuName: menu.menuName || '', + category: menu.category || '', + price: menu.price || 0, + description: menu.description || '', + available: menu.available !== undefined ? menu.available : true, + recommended: menu.recommended !== undefined ? menu.recommended : false, + imageUrl: menu.imageUrl || menu.image || '' + } + + showMenuDialog.value = true +} + +const viewMenuDetail = async (menu) => { + console.log('메뉴 상세 보기:', menu) + + try { + // 메뉴 서비스 임포트 + const { menuService } = await import('@/services/menu') + + // 메뉴 상세 정보 조회 + const result = await menuService.getMenuDetail(menu.id) + + if (result.success) { + selectedMenu.value = result.data + showMenuDetailDialog.value = true + console.log('✅ 메뉴 상세 정보 로드:', result.data) + } else { + // API 실패 시 현재 메뉴 정보 사용 + selectedMenu.value = menu + showMenuDetailDialog.value = true + console.log('⚠️ 상세 정보 로드 실패, 기본 정보 사용:', menu) + } + } catch (error) { + // 오류 발생 시 현재 메뉴 정보 사용 + console.error('메뉴 상세 정보 로드 실패:', error) + selectedMenu.value = menu + showMenuDetailDialog.value = true + } +} + +const closeMenuDetail = () => { + showMenuDetailDialog.value = false + selectedMenu.value = null +} + +const confirmDeleteMenu = (menu) => { + console.log('메뉴 삭제 확인:', menu) + if (confirm(`'${menu.menuName}' 메뉴를 삭제하시겠습니까?`)) { + deleteMenu(menu.id) + } +} + +const clearFilters = () => { + menuSearch.value = '' + menuCategoryFilter.value = '전체' +} + +const cancelMenuForm = () => { + console.log('메뉴 폼 취소') + showMenuDialog.value = false + menuEditMode.value = false + resetMenuForm() +} + +const resetMenuForm = () => { + menuFormData.value = { + id: null, // 메뉴 ID 초기화 추가 + menuName: '', + category: '', + price: 0, + description: '', + available: true, + recommended: false, + imageUrl: '' + } + + // 폼 validation 초기화 + if (menuFormRef.value) { + menuFormRef.value.resetValidation() + } +} + +const getCategoryColor = (category) => { + const colors = { + '커피': 'brown', + '음료': 'blue', + '디저트': 'pink', + '베이커리': 'orange', + '샐러드': 'green', + '샌드위치': 'purple' + } + return colors[category] || 'grey' +} + +const formatCurrency = (amount) => { + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format(amount) +} + +const formatDateTime = (dateTimeString) => { + if (!dateTimeString) return '-' + + try { + const date = new Date(dateTimeString) + return new Intl.DateTimeFormat('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }).format(date) + } catch (error) { + return dateTimeString + } +} + +// 메뉴 상세에서 수정 버튼 클릭 +const editMenuFromDetail = () => { + if (selectedMenu.value) { + // 상세 다이얼로그 닫기 + closeMenuDetail() + + // 수정 다이얼로그 열기 + editMenu(selectedMenu.value) + } +} + +// 메뉴 상세에서 삭제 버튼 클릭 +const deleteMenuFromDetail = () => { + if (selectedMenu.value) { + // 상세 다이얼로그 닫기 + closeMenuDetail() + + // 삭제 확인 + confirmDeleteMenu(selectedMenu.value) + } +} + const cancelForm = () => { console.log('폼 취소') showCreateDialog.value = false @@ -765,13 +1224,9 @@ onMounted(async () => { if (result.success) { console.log('✅ 매장 정보 로드 완료:', result.data) - // 메뉴 관리 탭에서 사용할 메뉴 정보도 로드 - try { - await storeStore.fetchMenus() - console.log('메뉴 정보 로드 완료') - } catch (menuError) { - console.log('메뉴 정보 로드 실패 (개발 중이므로 무시):', menuError) - } + // 매장 정보가 있는 경우 메뉴 정보도 로드 + await loadMenus() + } else { if (result.message === '등록된 매장이 없습니다') { console.log('⚠️ 등록된 매장이 없음 - 등록 화면 표시') @@ -786,6 +1241,120 @@ onMounted(async () => { showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error') } }) + +// 메뉴 데이터 로드 함수 - 실제 API 연동 +const loadMenus = async () => { + try { + console.log('메뉴 데이터 로드 시작') + const result = await storeStore.fetchMenus() + + if (result.success) { + menus.value = result.data + console.log('✅ 메뉴 데이터 로드 완료:', result.data) + } else { + console.log('메뉴 데이터 없음 또는 로드 실패:', result.message) + menus.value = [] // 빈 배열로 설정 (빈 상태 UI 표시) + } + } catch (error) { + console.error('메뉴 데이터 로드 실패:', error) + menus.value = [] // 빈 배열로 초기화 + } +} + +// 실제 메뉴 등록/수정 함수 +const saveMenu = async () => { + if (!menuFormRef.value) { + showSnackbar('폼 오류가 발생했습니다', 'error') + return + } + + const { valid } = await menuFormRef.value.validate() + if (!valid) { + showSnackbar('필수 정보를 모두 입력해주세요', 'error') + return + } + + saving.value = true + + try { + // 메뉴 서비스 임포트 + const { menuService } = await import('@/services/menu') + + let result + + if (menuEditMode.value) { + // 메뉴 수정 - PUT /api/menu/{menuId} + const menuId = menuFormData.value.id + if (!menuId) { + showSnackbar('메뉴 ID가 없습니다', 'error') + return + } + + console.log('메뉴 수정 API 호출, 메뉴 ID:', menuId) + result = await menuService.updateMenu(menuId, menuFormData.value) + } else { + // 새 메뉴 등록 - POST /api/menu/register + const storeId = storeInfo.value.storeId + if (!storeId) { + showSnackbar('매장 정보를 찾을 수 없습니다', 'error') + return + } + + // 메뉴 데이터 준비 (매장 ID 포함) + const menuData = { + ...menuFormData.value, + storeId: storeId + } + + console.log('메뉴 등록 API 호출, 매장 ID:', storeId) + result = await menuService.registerMenu(menuData) + } + + console.log('메뉴 저장 결과:', result) + + if (result.success) { + showSnackbar( + menuEditMode.value ? '메뉴가 수정되었습니다' : '메뉴가 등록되었습니다', + 'success' + ) + showMenuDialog.value = false + menuEditMode.value = false + resetMenuForm() + + // 메뉴 목록 새로고침 + await loadMenus() + } else { + showSnackbar(result.message || '저장에 실패했습니다', 'error') + } + } catch (error) { + console.error('메뉴 저장 중 오류:', error) + showSnackbar('저장 중 오류가 발생했습니다', 'error') + } finally { + saving.value = false + } +} + +// 실제 메뉴 삭제 함수 +const deleteMenu = async (menuId) => { + try { + // 메뉴 서비스 임포트 + const { menuService } = await import('@/services/menu') + + console.log('메뉴 삭제:', menuId) + const result = await menuService.deleteMenu(menuId) + + if (result.success) { + showSnackbar('메뉴가 삭제되었습니다', 'success') + // 메뉴 목록 새로고침 + await loadMenus() + } else { + showSnackbar(result.message || '메뉴 삭제에 실패했습니다', 'error') + } + } catch (error) { + console.error('메뉴 삭제 실패:', error) + showSnackbar('메뉴 삭제 중 오류가 발생했습니다', 'error') + } +} \ No newline at end of file