From edaf5da6ed1fa764755e7d09328a8704456e6563 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Jun 2025 14:39:16 +0900 Subject: [PATCH] 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