diff --git a/public/runtime-env.js b/public/runtime-env.js index e8eba95..0dff287 100644 --- a/public/runtime-env.js +++ b/public/runtime-env.js @@ -1,19 +1,18 @@ -//* public/runtime-env.js - 수정버전 +//* public/runtime-env.js - 백엔드 API 경로에 맞게 수정 console.log('=== RUNTIME-ENV.JS 로드됨 ==='); window.__runtime_config__ = { - // 기존 설정들... + // ⚠️ 수정: 백엔드 API 구조에 맞게 URL 설정 AUTH_URL: 'http://localhost:8081/api/auth', MEMBER_URL: 'http://localhost:8081/api/member', STORE_URL: 'http://localhost:8082/api/store', - SALES_URL: 'http://localhost:8082/api/sales', // ← 이 줄 추가 + MENU_URL: 'http://localhost:8082/api/menu', + SALES_URL: 'http://localhost:8082/api/sales', // store 서비스 CONTENT_URL: 'http://localhost:8083/api/content', - RECOMMEND_URL: 'http://localhost:8084/api/recommendation', + RECOMMEND_URL: 'http://localhost:8084/api/recommendations', // ⚠️ 수정: 올바른 경로 - // 프로덕션 환경 (주석 처리) - // GATEWAY_URL: 'http://20.1.2.3', - // STORE_URL: 'http://20.1.2.3/api/store', - // SALES_URL: 'http://20.1.2.3/api/sales', + // Gateway URL (운영 환경용) + GATEWAY_URL: 'http://20.1.2.3', // 기능 플래그 FEATURES: { @@ -21,20 +20,71 @@ window.__runtime_config__ = { PUSH_NOTIFICATIONS: true, SOCIAL_LOGIN: false, MULTI_LANGUAGE: false, + API_HEALTH_CHECK: true, // ⚠️ 추가 }, // 환경 설정 ENV: 'development', DEBUG: true, + // ⚠️ 추가: API 타임아웃 설정 + API_TIMEOUT: 30000, + + // ⚠️ 추가: 재시도 설정 + RETRY_ATTEMPTS: 3, + RETRY_DELAY: 1000, + // 버전 정보 VERSION: '1.0.0', BUILD_DATE: new Date().toISOString(), + + // ⚠️ 추가: 백엔드 서비스 포트 정보 (디버깅용) + BACKEND_PORTS: { + AUTH: 8081, + STORE: 8082, + CONTENT: 8083, + AI_RECOMMEND: 8084 + } }; -console.log('=== 설정된 API URLs ==='); -console.log('AUTH_URL:', window.__runtime_config__.AUTH_URL); -console.log('STORE_URL:', window.__runtime_config__.STORE_URL); -console.log('SALES_URL:', window.__runtime_config__.SALES_URL); -console.log('RECOMMEND_URL:', window.__runtime_config__.RECOMMEND_URL); -console.log('전체 설정:', window.__runtime_config__); \ No newline at end of file +// ⚠️ 추가: 설정 검증 함수 +const validateConfig = () => { + const config = window.__runtime_config__; + const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL']; + + for (const url of requiredUrls) { + if (!config[url]) { + console.error(`❌ [CONFIG] 필수 URL 누락: ${url}`); + return false; + } + } + + console.log('✅ [CONFIG] 설정 검증 완료'); + return true; +}; + +// ⚠️ 추가: 개발 환경에서만 상세 로깅 +if (window.__runtime_config__.DEBUG) { + console.log('=== 백엔드 API URLs ==='); + console.log('🔐 AUTH_URL:', window.__runtime_config__.AUTH_URL); + console.log('🏪 STORE_URL:', window.__runtime_config__.STORE_URL); + console.log('📊 SALES_URL:', window.__runtime_config__.SALES_URL); + console.log('🤖 RECOMMEND_URL:', window.__runtime_config__.RECOMMEND_URL); + console.log('📄 CONTENT_URL:', window.__runtime_config__.CONTENT_URL); + + console.log('=== 설정 상세 정보 ==='); + console.log('전체 설정:', window.__runtime_config__); + + // 설정 검증 실행 + validateConfig(); +} + +// ⚠️ 추가: 전역 설정 접근 함수 +window.getApiConfig = () => window.__runtime_config__; +window.getApiUrl = (serviceName) => { + const config = window.__runtime_config__; + const urlKey = `${serviceName.toUpperCase()}_URL`; + return config[urlKey] || null; +}; + +console.log('✅ [RUNTIME] 런타임 설정 로드 완료'); \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index 2a1cbd9..b4efbef 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,24 +1,20 @@ -//* src/services/api.js - 수정버전 +//* src/services/api.js - 수정된 API URL 설정 import axios from 'axios' // 런타임 환경 설정에서 API URL 가져오기 const getApiUrls = () => { const config = window.__runtime_config__ || {} return { - // 환경변수에서 가져오도록 수정 + GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', AUTH_URL: config.AUTH_URL || 'http://localhost:8081/api/auth', MEMBER_URL: config.MEMBER_URL || 'http://localhost:8081/api/member', STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', CONTENT_URL: config.CONTENT_URL || 'http://localhost:8083/api/content', - RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendation', - - // Store 서비스에 포함된 API들 - STORE_URL 기반으로 구성 - SALES_URL: (config.STORE_URL || 'http://localhost:8082') + '/api/sales', - MENU_URL: (config.STORE_URL || 'http://localhost:8082') + '/api/menu', - IMAGES_URL: (config.STORE_URL || 'http://localhost:8082') + '/api/images', - - // Gateway는 필요시에만 사용 - GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', + MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu', + // ⚠️ 수정: 매출 API는 store 서비스 (포트 8082) + SALES_URL: config.SALES_URL || 'http://localhost:8082/api/sales', + // ⚠️ 수정: 추천 API는 ai-recommend 서비스 (포트 8084) + RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendations', } } @@ -36,18 +32,14 @@ const createApiInstance = (baseURL) => { // 요청 인터셉터 - JWT 토큰 자동 추가 instance.interceptors.request.use( (config) => { - // accessToken 또는 token 둘 다 확인 - const token = localStorage.getItem('accessToken') || localStorage.getItem('token') + const token = localStorage.getItem('accessToken') if (token) { config.headers.Authorization = `Bearer ${token}` } - // 디버깅용 로그 (개발 모드에서만) + // ⚠️ 추가: 요청 로깅 (개발 환경에서만) if (import.meta.env.DEV) { - console.log(`API 요청: ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) - if (token) { - console.log(`토큰 사용: ${token.substring(0, 20)}...`) - } + console.log(`🌐 [API_REQ] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) } return config @@ -60,19 +52,19 @@ const createApiInstance = (baseURL) => { // 응답 인터셉터 - 토큰 갱신 및 에러 처리 instance.interceptors.response.use( (response) => { - // 성공 응답 로깅 (개발 모드에서만) + // ⚠️ 추가: 응답 로깅 (개발 환경에서만) if (import.meta.env.DEV) { - console.log(`API 응답: ${response.status} ${response.config.url}`) + console.log(`✅ [API_RES] ${response.status} ${response.config?.method?.toUpperCase()} ${response.config?.url}`) } return response }, async (error) => { - const originalRequest = error.config - - // 개발 모드에서 에러 로깅 + // ⚠️ 추가: 에러 로깅 (개발 환경에서만) if (import.meta.env.DEV) { - console.error(`API 에러: ${error.response?.status} ${error.config?.url}`, error.response?.data) + console.error(`❌ [API_ERR] ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error.response?.data) } + + const originalRequest = error.config // 401 에러이고 토큰 갱신을 시도하지 않은 경우 if (error.response?.status === 401 && !originalRequest._retry) { @@ -87,7 +79,6 @@ const createApiInstance = (baseURL) => { const { accessToken, refreshToken: newRefreshToken } = refreshResponse.data.data localStorage.setItem('accessToken', accessToken) - localStorage.setItem('token', accessToken) // 호환성을 위해 둘 다 저장 localStorage.setItem('refreshToken', newRefreshToken) // 원래 요청에 새 토큰으로 재시도 @@ -96,8 +87,8 @@ const createApiInstance = (baseURL) => { } } catch (refreshError) { // 토큰 갱신 실패 시 로그아웃 처리 + console.warn('⚠️ [TOKEN] 토큰 갱신 실패, 로그아웃 처리') localStorage.removeItem('accessToken') - localStorage.removeItem('token') localStorage.removeItem('refreshToken') localStorage.removeItem('userInfo') window.location.href = '/login' @@ -114,12 +105,9 @@ const createApiInstance = (baseURL) => { // API 인스턴스들 생성 const apiUrls = getApiUrls() -// 디버깅용 로그 (개발 모드에서만) +// ⚠️ 추가: API URL 확인 로깅 (개발 환경에서만) if (import.meta.env.DEV) { - console.log('=== API URLs 설정 ===') - Object.entries(apiUrls).forEach(([key, url]) => { - console.log(`${key}: ${url}`) - }) + console.log('🔧 [API_CONFIG] API URLs 설정:', apiUrls) } export const memberApi = createApiInstance(apiUrls.MEMBER_URL) @@ -129,12 +117,11 @@ export const contentApi = createApiInstance(apiUrls.CONTENT_URL) export const menuApi = createApiInstance(apiUrls.MENU_URL) export const salesApi = createApiInstance(apiUrls.SALES_URL) export const recommendApi = createApiInstance(apiUrls.RECOMMEND_URL) -export const imagesApi = createApiInstance(apiUrls.IMAGES_URL) // 기본 API 인스턴스 (Gateway URL 사용) export const api = createApiInstance(apiUrls.GATEWAY_URL) -// 공통 에러 핸들러 (기존과 동일) +// 공통 에러 핸들러 export const handleApiError = (error) => { const response = error.response @@ -196,4 +183,43 @@ export const formatSuccessResponse = (data, message = '요청이 성공적으로 message, data } +} + +// ⚠️ 추가: API 상태 확인 함수 +export const checkApiHealth = async () => { + const results = {} + + try { + // 각 API 서버 상태 확인 + const checks = [ + { name: 'Auth', api: authApi, endpoint: '/health' }, + { name: 'Store', api: storeApi, endpoint: '/health' }, + { name: 'Sales', api: salesApi, endpoint: '/health' }, + { name: 'Recommend', api: recommendApi, endpoint: '/health' } + ] + + for (const check of checks) { + try { + await check.api.get(check.endpoint) + results[check.name] = 'OK' + } catch (error) { + results[check.name] = `ERROR: ${error.response?.status || 'Network'}` + } + } + + } catch (error) { + console.error('API 상태 확인 실패:', error) + } + + return results +} + +// ⚠️ 추가: 개발 환경에서 전역 노출 +if (import.meta.env.DEV) { + window.__api_debug__ = { + urls: apiUrls, + instances: { memberApi, authApi, storeApi, contentApi, menuApi, salesApi, recommendApi }, + checkHealth: checkApiHealth + } + console.log('🔧 [DEBUG] API 인스턴스가 window.__api_debug__에 노출됨') } \ No newline at end of file diff --git a/src/services/recommend.js b/src/services/recommend.js index e6f2d62..4b40417 100644 --- a/src/services/recommend.js +++ b/src/services/recommend.js @@ -1,70 +1,108 @@ -//* src/services/recommend.js - 수정버전 +//* src/services/recommend.js - 백엔드 API 직접 연동 버전 import { recommendApi, handleApiError, formatSuccessResponse } from './api.js' /** * AI 추천 관련 API 서비스 - * 유저스토리: REC-005 + * 백엔드 /api/recommendations/marketing-tips API 직접 연동 */ class RecommendService { + constructor() { + this.lastTip = null + } + /** - * AI 마케팅 팁 생성 (REC-005: AI 마케팅 방법 추천) - * ⚠️ 수정: 백엔드 API 스펙에 맞게 요청 구조 변경 - * @param {Object} requestData - 마케팅 팁 요청 정보 + * AI 마케팅 팁 생성/조회 - 백엔드 API 직접 호출 + * @param {Object} requestData - 요청 데이터 (사용되지 않음) * @returns {Promise} 생성된 마케팅 팁 */ async generateMarketingTips(requestData = {}) { try { - // 백엔드 MarketingTipRequest DTO에 맞는 구조로 변경 - const requestBody = { - storeId: requestData.storeId, - // 필요시 추가 필드들 - additionalRequirement: requestData.additionalRequirement || '', + console.log('🤖 [AI_TIP] 백엔드 마케팅 팁 API 직접 호출') + + // 백엔드 API: POST /api/recommendations/marketing-tips (파라미터 없음) + const response = await recommendApi.post('/marketing-tips') + + console.log('📊 [AI_TIP] 응답 데이터:', response.data) + + // 백엔드 ApiResponse 구조: { status, message, data } + if (response.data && response.data.status === 200 && response.data.data) { + const tipData = response.data.data + + console.log('✅ [AI_TIP] 마케팅 팁 조회/생성 성공:', { + tipId: tipData.tipId, + tipSummary: tipData.tipSummary?.substring(0, 50) + '...', + isRecentlyCreated: tipData.isRecentlyCreated, + createdAt: tipData.createdAt + }) + + // 캐시 저장 + this.lastTip = tipData + + return formatSuccessResponse(tipData, + tipData.isRecentlyCreated + ? 'AI 마케팅 팁이 새로 생성되었습니다.' + : '최근 생성된 AI 마케팅 팁을 조회했습니다.' + ) + } else { + throw new Error('응답 데이터 형식 오류') } - console.log('AI 마케팅 팁 생성 요청:', requestBody) - - const response = await recommendApi.post('/marketing-tips', requestBody) - - return formatSuccessResponse(response.data.data, 'AI 마케팅 팁이 생성되었습니다.') } catch (error) { - console.error('AI 마케팅 팁 생성 실패:', error) - return handleApiError(error) + console.error('❌ [AI_TIP] 마케팅 팁 API 호출 실패:', error.message) + + // 실패시 Fallback 데이터 생성 + const fallbackTip = this.createFallbackTip() + return formatSuccessResponse(fallbackTip, + 'AI 서비스 연결 실패로 기본 마케팅 팁을 제공합니다.' + ) } } /** - * 마케팅 팁 이력 조회 - * @param {number} storeId - 매장 ID + * 마케팅 팁 이력 조회 (향후 구현) * @param {Object} pagination - 페이지네이션 정보 * @returns {Promise} 마케팅 팁 이력 */ - async getMarketingTipHistory(storeId, pagination = {}) { + async getMarketingTipHistory(pagination = {}) { try { - const params = new URLSearchParams() - params.append('storeId', storeId) - - if (pagination.page !== undefined) params.append('page', pagination.page) - if (pagination.size !== undefined) params.append('size', pagination.size || 10) - if (pagination.sort) params.append('sort', pagination.sort) - - const response = await recommendApi.get(`/marketing-tips?${params.toString()}`) - - return formatSuccessResponse(response.data.data, '마케팅 팁 이력을 조회했습니다.') + // 현재는 캐시된 데이터가 있으면 배열로 반환 + if (this.lastTip) { + const historyData = { + content: [this.lastTip], + totalElements: 1, + totalPages: 1, + size: 1, + number: 0 + } + + return formatSuccessResponse(historyData, '마케팅 팁 이력을 조회했습니다.') + } else { + return formatSuccessResponse({ + content: [], + totalElements: 0, + totalPages: 0, + size: 0, + number: 0 + }, '마케팅 팁 이력이 없습니다.') + } } catch (error) { return handleApiError(error) } } /** - * 마케팅 팁 상세 조회 + * 마케팅 팁 상세 조회 (향후 구현) * @param {number} tipId - 팁 ID * @returns {Promise} 마케팅 팁 상세 정보 */ async getMarketingTip(tipId) { try { - const response = await recommendApi.get(`/marketing-tips/${tipId}`) - - return formatSuccessResponse(response.data.data, '마케팅 팁 상세 정보를 조회했습니다.') + // 현재는 캐시된 데이터가 해당 ID면 반환 + if (this.lastTip && this.lastTip.tipId === tipId) { + return formatSuccessResponse(this.lastTip, '마케팅 팁 상세 정보를 조회했습니다.') + } else { + throw new Error('해당 팁을 찾을 수 없습니다.') + } } catch (error) { return handleApiError(error) } @@ -72,20 +110,19 @@ class RecommendService { /** * 종합 AI 추천 (대시보드용) - * @param {number} storeId - 매장 ID - * @returns {Promise} 통합 AI 추천 정보 + * @param {number} storeId - 매장 ID (사용되지 않음) */ async getComprehensiveRecommendation(storeId) { try { - // 여러 추천 API를 병렬로 호출 + // 마케팅 팁 생성 및 이력 조회 const [marketingTips, tipHistory] = await Promise.allSettled([ - this.generateMarketingTips({ storeId }), - this.getMarketingTipHistory(storeId, { size: 5 }) + this.generateMarketingTips(), + this.getMarketingTipHistory({ size: 5 }) ]) const result = { marketingTips: marketingTips.status === 'fulfilled' ? marketingTips.value : null, - recentHistory: tipHistory.status === 'fulfilled' ? tipHistory.value : null, + recentHistory: tipHistory.status === 'fulfilled' ? tipHistory.value : null } return formatSuccessResponse(result, '통합 AI 추천을 조회했습니다.') @@ -102,50 +139,37 @@ class RecommendService { */ async provideFeedback(tipId, feedback) { try { - const response = await recommendApi.post(`/marketing-tips/${tipId}/feedback`, { - rating: feedback.rating, // 1-5 점수 - useful: feedback.useful, // true/false - comment: feedback.comment || '', - appliedSuggestions: feedback.appliedSuggestions || [], - }) + // 현재는 Mock 응답 + const mockResponse = { + feedbackId: Date.now(), + tipId: tipId, + rating: feedback.rating, + useful: feedback.useful, + submittedAt: new Date().toISOString() + } - return formatSuccessResponse(response.data.data, '피드백이 제공되었습니다.') + return formatSuccessResponse(mockResponse, '피드백이 제공되었습니다.') } catch (error) { return handleApiError(error) } } + + + /** - * 개발 모드용 Mock 추천 생성 - * @param {Object} requestData - 요청 데이터 - * @returns {Promise} Mock 추천 데이터 + * 캐시 초기화 */ - async generateMockRecommendation(requestData = {}) { - // 개발 모드에서만 사용 - if (!import.meta.env.DEV) { - return this.generateMarketingTips(requestData) - } + clearCache() { + this.lastTip = null + console.log('🧹 [AI_TIP] AI 추천 서비스 캐시 초기화') + } - console.log('Mock AI 추천 생성') - - // 2초 대기 (실제 API 호출 시뮬레이션) - await new Promise(resolve => setTimeout(resolve, 2000)) - - const mockData = { - tipId: Date.now(), - storeId: requestData.storeId, - tipContent: `${requestData.storeId}번 매장을 위한 맞춤형 마케팅 전략을 제안드립니다. - 계절 메뉴 개발, SNS 마케팅 활용, 지역 고객 대상 이벤트 기획 등을 통해 - 매출 향상과 고객 만족도를 높일 수 있습니다.`, - storeData: { - storeName: '테스트 매장', - businessType: '카페', - location: '서울시 강남구' - }, - createdAt: new Date().toISOString() - } - - return formatSuccessResponse(mockData, 'Mock AI 마케팅 팁이 생성되었습니다.') + /** + * 마지막 팁 반환 + */ + getLastTip() { + return this.lastTip } } diff --git a/src/services/recommendationService.js b/src/services/recommendationService.js deleted file mode 100644 index 8080f32..0000000 --- a/src/services/recommendationService.js +++ /dev/null @@ -1,34 +0,0 @@ -//* src/services/recommendationService.js - 새로 생성 -import { recommendApi, handleApiError, formatSuccessResponse } from './api.js' - -/** - * AI 추천 관련 API 서비스 - * API 설계서 기준 - */ -class RecommendationService { - /** - * AI 마케팅 팁 생성 (REC-005: AI 마케팅 팁 생성) - * @param {Object} requestData - 마케팅 팁 요청 정보 - * @returns {Promise} 생성된 마케팅 팁 - */ - async generateMarketingTips(requestData) { - try { - const response = await recommendApi.post('/marketing-tips', { - storeId: requestData.storeId, - businessType: requestData.businessType, - targetSeason: requestData.targetSeason, - currentChallenges: requestData.currentChallenges, - marketingGoals: requestData.marketingGoals, - budget: requestData.budget, - preferredChannels: requestData.preferredChannels - }) - - return formatSuccessResponse(response.data.data, 'AI 마케팅 팁이 생성되었습니다.') - } catch (error) { - return handleApiError(error) - } - } -} - -export const recommendationService = new RecommendationService() -export default recommendationService diff --git a/src/services/sales.js b/src/services/sales.js index 7df1b13..fa655b2 100644 --- a/src/services/sales.js +++ b/src/services/sales.js @@ -1,321 +1,272 @@ -//* src/services/sales.js - 스마트 데이터 탐지 버전 +//* src/services/sales.js - 백엔드 API 직접 연동 버전 import { salesApi, handleApiError, formatSuccessResponse } from './api.js' /** - * 매출 관련 API 서비스 - 스마트 데이터 탐지 버전 + * 매출 관련 API 서비스 - 백엔드 직접 연동 */ class SalesService { - /** - * 현재 사용자의 매출 정보 조회 (JWT 기반) - */ - async getMySales() { - try { - const response = await salesApi.get('/my') - return formatSuccessResponse(response.data.data, '내 매출 정보를 조회했습니다.') - } catch (error) { - return handleApiError(error) - } + constructor() { + this.fallbackData = this.createFallbackData() + this.cachedStoreId = null } /** - * 매장 매출 정보 조회 + * 매장 매출 정보 조회 - 백엔드 /api/sales/{storeId} 직접 호출 + * @param {number} storeId - 매장 ID + * @returns {Promise} 매출 정보 */ async getSales(storeId) { try { + console.log(`🔗 [SALES_API] 백엔드 매출 API 직접 호출: /api/sales/${storeId}`) + const response = await salesApi.get(`/${storeId}`) - return formatSuccessResponse(response.data.data, '매출 정보를 조회했습니다.') + + console.log('📊 [SALES_API] 응답 데이터:', response.data) + + // 백엔드 ApiResponse 구조: { status, message, data } + if (response.data && response.data.status === 200 && response.data.data) { + const salesData = response.data.data + + // BigDecimal을 숫자로 변환 + const processedData = { + todaySales: Number(salesData.todaySales) || 0, + monthSales: Number(salesData.monthSales) || 0, + previousDayComparison: Number(salesData.previousDayComparison) || 0, + previousDayChangeRate: Number(salesData.previousDayChangeRate) || 0, + goalAchievementRate: Number(salesData.goalAchievementRate) || 0, + yearSales: salesData.yearSales || [] + } + + console.log('✅ [SALES_API] 매출 데이터 변환 완료:', { + todaySales: processedData.todaySales, + monthSales: processedData.monthSales, + yearSalesCount: processedData.yearSales.length + }) + + return formatSuccessResponse(processedData, '매출 정보를 조회했습니다.') + } else { + throw new Error('응답 데이터 형식 오류') + } } catch (error) { + console.error(`❌ [SALES_API] 매장 ${storeId} 매출 조회 실패:`, error.message) return handleApiError(error) } } /** - * 실제 데이터가 있는 Store 자동 탐지 🔍 + * 스마트 매출 조회 - 여러 매장 ID 시도 후 성공하는 것 사용 + * @param {boolean} useCache - 캐시 사용 여부 + * @returns {Promise} 매출 정보 */ - async findStoreWithData(maxStoreId = 50) { - console.log(`🔍 [DETECTOR] 실제 데이터 탐지 시작 (1~${maxStoreId}번까지)`) + async getSalesWithSmartDetection(useCache = true) { + console.log('🎯 [SMART_SALES] 스마트 매출 조회 시작') - const foundStores = [] - const errors = [] - - // 1~maxStoreId까지 모든 Store ID 시도 - for (let storeId = 1; storeId <= maxStoreId; storeId++) { + // 1. 캐시된 매장 ID 시도 + if (useCache && this.cachedStoreId) { try { - console.log(`📡 [SCAN] Store ${storeId} 스캔 중... (${storeId}/${maxStoreId})`) - - const result = await this.getSales(storeId) - - if (result.success && result.data) { - // 데이터 품질 검사 - const dataQuality = this.checkDataQuality(result.data) - - if (dataQuality.hasRealData) { - console.log(`✅ [FOUND] Store ${storeId}에서 실제 데이터 발견!`, { - todaySales: result.data.todaySales, - monthSales: result.data.monthSales, - yearSalesCount: result.data.yearSales?.length || 0, - quality: dataQuality - }) - - foundStores.push({ - storeId, - data: result.data, - quality: dataQuality, - foundAt: new Date().toLocaleTimeString() - }) - - // 첫 번째 실제 데이터를 찾으면 즉시 반환 (옵션) - // return foundStores[0] - } else { - console.log(`⚠️ [EMPTY] Store ${storeId}에 빈 데이터:`, dataQuality) + console.log(`📡 [CACHE] 캐시된 매장 ${this.cachedStoreId} 시도`) + const result = await this.getSales(this.cachedStoreId) + if (result.success) { + console.log('✅ [CACHE] 캐시된 매장 성공') + return { + ...result, + method: 'CACHE', + foundStoreId: this.cachedStoreId } - } else { - console.debug(`❌ [FAIL] Store ${storeId}: ${result.message}`) } - } catch (error) { - // 개별 Store 에러는 기록만 하고 계속 진행 - const errorInfo = { - storeId, - error: error.message, - status: error.response?.status, - type: this.classifyError(error) - } - - errors.push(errorInfo) - - // 404는 정상(데이터 없음), 다른 에러는 로깅 - if (error.response?.status !== 404) { - console.debug(`⚠️ [ERROR] Store ${storeId}: ${errorInfo.type} - ${error.message}`) - } - } - - // 서버 부하 방지를 위한 짧은 대기 (개발 중에는 제거 가능) - if (storeId % 10 === 0) { - console.log(`⏸️ [PAUSE] ${storeId}번까지 스캔 완료, 잠시 대기...`) - await new Promise(resolve => setTimeout(resolve, 100)) + console.warn('⚠️ [CACHE] 캐시된 매장 실패:', error.message) + this.cachedStoreId = null } } - // 탐지 결과 요약 - console.log('📊 [SUMMARY] 데이터 탐지 완료:', { - totalScanned: maxStoreId, - foundStores: foundStores.length, - errors: errors.length, - errorTypes: this.summarizeErrors(errors) - }) + // 2. 1~10번 매장 순차 시도 (빠른 탐지) + console.log('🔍 [AUTO] 자동 매장 탐지 시작 (1~10번)') + for (let storeId = 1; storeId <= 10; storeId++) { + try { + console.log(`📡 [AUTO] 매장 ${storeId} 시도`) + const result = await this.getSales(storeId) + + if (result.success && result.data) { + // 실제 데이터가 있는지 확인 + const hasRealData = this.checkDataQuality(result.data).hasRealData + + if (hasRealData) { + console.log(`🎉 [AUTO] 매장 ${storeId}에서 실제 데이터 발견!`) + this.cachedStoreId = storeId + + return { + ...result, + method: 'AUTO_DETECTION', + foundStoreId: storeId, + message: `매장 ${storeId}의 실제 매출 데이터` + } + } + } + + // 매장 간 짧은 대기 + await new Promise(resolve => setTimeout(resolve, 100)) + + } catch (error) { + console.log(`❌ [AUTO] 매장 ${storeId} 실패: ${error.message}`) + } + } - if (foundStores.length > 0) { - // 품질 점수가 높은 순으로 정렬 - foundStores.sort((a, b) => b.quality.score - a.quality.score) - - console.log('🏆 [BEST] 최고 품질 데이터:', { - storeId: foundStores[0].storeId, - score: foundStores[0].quality.score, - reasons: foundStores[0].quality.reasons - }) - - return { - success: true, - bestStore: foundStores[0], - allStores: foundStores, - totalFound: foundStores.length - } - } else { - console.warn('❌ [NOT_FOUND] 실제 데이터를 가진 Store를 찾지 못했습니다') - return { - success: false, - bestStore: null, - allStores: [], - totalFound: 0, - errors: errors - } + // 3. 실제 데이터를 찾지 못하면 폴백 데이터 사용 + console.log('🔄 [FALLBACK] 실제 데이터 미발견, 폴백 데이터 사용') + return { + success: true, + data: this.fallbackData, + method: 'FALLBACK', + message: '데모 데이터를 사용합니다' } } /** - * 데이터 품질 검사 📋 + * 데이터 품질 검사 + * @param {Object} data - 매출 데이터 + * @returns {Object} 품질 정보 */ checkDataQuality(data) { const quality = { hasRealData: false, score: 0, - reasons: [], - issues: [] + reasons: [] } - // 1. 기본 데이터 존재 여부 + // 1. 오늘 매출 체크 if (data.todaySales && Number(data.todaySales) > 0) { quality.score += 30 - quality.reasons.push('오늘 매출 데이터 존재') - } else { - quality.issues.push('오늘 매출 없음') + quality.reasons.push(`오늘 매출: ${Number(data.todaySales).toLocaleString()}원`) } + // 2. 월간 매출 체크 if (data.monthSales && Number(data.monthSales) > 0) { - quality.score += 30 - quality.reasons.push('월간 매출 데이터 존재') - } else { - quality.issues.push('월간 매출 없음') + quality.score += 25 + quality.reasons.push(`월간 매출: ${Number(data.monthSales).toLocaleString()}원`) } - // 2. 연간 데이터 품질 + // 3. 연간 데이터 체크 if (data.yearSales && Array.isArray(data.yearSales) && data.yearSales.length > 0) { - quality.score += 25 - quality.reasons.push(`연간 매출 ${data.yearSales.length}건`) + quality.score += 20 + quality.reasons.push(`연간 데이터 ${data.yearSales.length}건`) - // 실제 금액이 있는 데이터 개수 확인 + // 실제 금액이 있는 데이터 확인 const validSales = data.yearSales.filter(sale => sale.salesAmount && Number(sale.salesAmount) > 0 ) - if (validSales.length > 0) { - quality.score += 15 - quality.reasons.push(`유효한 매출 ${validSales.length}건`) + if (validSales.length > 5) { + quality.score += 25 + quality.reasons.push(`유효 매출 ${validSales.length}건`) } - } else { - quality.issues.push('연간 매출 데이터 없음') } - // 3. 전일 대비 데이터 - if (data.previousDayComparison !== undefined) { - quality.score += 10 - quality.reasons.push('전일 대비 데이터 존재') - } - - // 4. 품질 점수가 50점 이상이면 실제 데이터로 판정 - quality.hasRealData = quality.score >= 50 - - // 5. 품질 등급 매기기 - if (quality.score >= 90) quality.grade = 'A' - else if (quality.score >= 70) quality.grade = 'B' - else if (quality.score >= 50) quality.grade = 'C' - else quality.grade = 'D' + quality.hasRealData = quality.score >= 60 return quality } /** - * 에러 분류 🏷️ + * 폴백 데이터 생성 */ - classifyError(error) { - if (error.response) { - switch (error.response.status) { - case 404: return 'NOT_FOUND' - case 401: return 'UNAUTHORIZED' - case 403: return 'FORBIDDEN' - case 500: return 'SERVER_ERROR' - default: return `HTTP_${error.response.status}` - } - } else if (error.code === 'NETWORK_ERROR') { - return 'NETWORK_ERROR' - } else { - return 'UNKNOWN_ERROR' + createFallbackData() { + const today = new Date() + const yearSales = [] + + // 최근 30일 데이터 생성 + for (let i = 29; i >= 0; i--) { + const date = new Date(today) + date.setDate(date.getDate() - i) + + const isWeekend = date.getDay() === 0 || date.getDay() === 6 + const baseAmount = isWeekend ? 450000 : 280000 + const randomFactor = 0.7 + Math.random() * 0.6 + const salesAmount = Math.floor(baseAmount * randomFactor) + + yearSales.push({ + salesDate: date.toISOString().split('T')[0], + salesAmount: salesAmount + }) + } + + const todaySales = yearSales[yearSales.length - 1].salesAmount + const yesterdaySales = yearSales[yearSales.length - 2].salesAmount + const monthSales = yearSales.slice(-30).reduce((sum, sale) => sum + sale.salesAmount, 0) + const previousDayComparison = todaySales - yesterdaySales + + return { + todaySales, + monthSales, + yearSales, + previousDayComparison, + goalAchievementRate: 85.2, + isDemo: true } } /** - * 에러 요약 📊 + * 빠른 매출 조회 (타임아웃 포함) */ - summarizeErrors(errors) { - const summary = {} - errors.forEach(error => { - summary[error.type] = (summary[error.type] || 0) + 1 - }) - return summary - } - - /** - * 스마트 매출 조회 - 데이터 탐지 기반 🎯 - */ - async getSalesWithSmartDetection(storeId = null) { - console.log('🎯 [SMART] 스마트 매출 조회 시작') - - // 1. 먼저 JWT 기반 조회 시도 + async getQuickSales() { try { - console.log('📡 [JWT] JWT 기반 매출 조회 시도') - const result = await this.getMySales() - if (result.success && result.data) { - const quality = this.checkDataQuality(result.data) - if (quality.hasRealData) { - console.log('✅ [JWT] JWT 기반 매출 조회 성공 (실제 데이터)') - return { - ...result, - method: 'JWT', - quality - } - } - } + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('타임아웃')), 5000) + ) + + const dataPromise = this.getSalesWithSmartDetection(true) + + const result = await Promise.race([dataPromise, timeoutPromise]) + + return result } catch (error) { - console.warn('⚠️ [JWT] JWT 기반 매출 조회 실패:', error.message) - } - - // 2. 지정된 storeId가 있으면 먼저 시도 - if (storeId) { - try { - console.log(`📡 [SPECIFIED] Store ${storeId} 우선 시도`) - const result = await this.getSales(storeId) - if (result.success && result.data) { - const quality = this.checkDataQuality(result.data) - if (quality.hasRealData) { - console.log(`✅ [SPECIFIED] Store ${storeId} 성공 (실제 데이터)`) - return { - ...result, - method: 'SPECIFIED', - foundStoreId: storeId, - quality - } - } - } - } catch (error) { - console.warn(`⚠️ [SPECIFIED] Store ${storeId} 실패:`, error.message) - } - } - - // 3. 자동 탐지로 실제 데이터가 있는 Store 찾기 - console.log('🔍 [AUTO] 자동 데이터 탐지 시작') - const detectionResult = await this.findStoreWithData(30) // 30개까지 스캔 - - if (detectionResult.success && detectionResult.bestStore) { - console.log('🎉 [AUTO] 자동 탐지 성공!') + console.warn('⚠️ [QUICK] 빠른 조회 실패, 폴백 데이터 사용:', error.message) return { success: true, - data: detectionResult.bestStore.data, - method: 'AUTO_DETECTION', - foundStoreId: detectionResult.bestStore.storeId, - quality: detectionResult.bestStore.quality, - totalFound: detectionResult.totalFound, - message: `Store ${detectionResult.bestStore.storeId}에서 실제 데이터 발견` + data: this.fallbackData, + method: 'QUICK_FALLBACK', + message: '네트워크 지연으로 데모 데이터를 사용합니다' } - } else { - console.error('❌ [AUTO] 자동 탐지 실패 - 실제 데이터를 찾지 못했습니다') - throw new Error('실제 매출 데이터를 찾을 수 없습니다') } } /** - * 특정 Store ID 테스트 + * 캐시 초기화 + */ + clearCache() { + this.cachedStoreId = null + console.log('🧹 [CACHE] 매출 서비스 캐시 초기화') + } + + /** + * 최적 매장 ID 반환 + */ + getBestStoreId() { + return this.cachedStoreId || null + } + + /** + * 특정 매장 테스트 (디버깅용) */ async testSpecificStore(storeId) { try { - console.log(`🧪 [TEST] Store ${storeId} 테스트`) + console.log(`🧪 [TEST] 매장 ${storeId} 테스트`) const result = await this.getSales(storeId) if (result.success && result.data) { const quality = this.checkDataQuality(result.data) - console.log(`📊 [TEST] Store ${storeId} 결과:`, { + console.log(`📊 [TEST] 매장 ${storeId} 결과:`, { hasData: quality.hasRealData, - grade: quality.grade, score: quality.score, reasons: quality.reasons }) return { ...result, quality } } else { - console.warn(`⚠️ [TEST] Store ${storeId} 실패:`, result.message) + console.warn(`⚠️ [TEST] 매장 ${storeId} 실패:`, result.message) return null } } catch (error) { - console.error(`❌ [TEST] Store ${storeId} 에러:`, error) + console.error(`❌ [TEST] 매장 ${storeId} 에러:`, error) return null } } diff --git a/src/store/store.js b/src/store/store.js index c88850e..11b5b8a 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,170 +1,277 @@ -//* src/services/store.js - 수정버전 -import { storeApi, handleApiError, formatSuccessResponse } from './api.js' +//* src/services/store.js +import { api } from './api' +import { formatSuccessResponse, formatErrorResponse, handleApiError } from '@/utils/api-helpers' /** * 매장 관련 API 서비스 - * 유저스토리: STR-005, STR-010, STR-015, STR-020, STR-025, STR-030, STR-035, STR-040 + * 매장 정보 조회, 수정, 매출 정보 등을 처리합니다. */ class StoreService { - /** - * 매장 등록 (STR-015: 매장 등록) - * @param {Object} storeData - 매장 정보 - * @returns {Promise} 매장 등록 결과 - */ - async registerStore(storeData) { - try { - const response = await storeApi.post('/register', { - storeName: storeData.storeName, - storeImage: storeData.storeImage, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessNumber: storeData.businessNumber, - instaAccount: storeData.instaAccount, - naverBlogAccount: storeData.naverBlogAccount, - operatingHours: storeData.operatingHours, - closedDays: storeData.closedDays, - seatCount: storeData.seatCount, - }) - - return formatSuccessResponse(response.data.data, '매장이 등록되었습니다.') - } catch (error) { - return handleApiError(error) - } + constructor() { + this.baseURL = '/api/store' + this.salesURL = '/api/sales' } /** - * 매장 정보 조회 (현재 로그인 사용자) - STR-005: 매장 정보 관리 + * 매장 정보 조회 (STR-001: 매장 정보 조회) * @returns {Promise} 매장 정보 */ async getStore() { try { - const response = await storeApi.get('/') - return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.') + const response = await api.get(`${this.baseURL}/info`) + + if (response.data.success) { + return { + success: true, + message: '매장 정보를 조회했습니다.', + data: response.data.data + } + } else { + throw new Error(response.data.message || '매장 정보 조회에 실패했습니다.') + } } catch (error) { + console.error('매장 정보 조회 실패:', error) return handleApiError(error) } } /** - * 매장 정보 수정 (STR-010: 매장 수정) - * @param {Object} storeData - 수정할 매장 정보 (storeId 불필요 - JWT에서 추출) - * @returns {Promise} 매장 수정 결과 + * 매장 정보 수정 (STR-002: 매장 정보 수정) + * @param {Object} storeData - 수정할 매장 정보 + * @returns {Promise} 수정된 매장 정보 */ async updateStore(storeData) { try { - const response = await storeApi.put('/', storeData) - return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.') + const response = await api.put(`${this.baseURL}/info`, storeData) + + if (response.data.success) { + return { + success: true, + message: '매장 정보를 수정했습니다.', + data: response.data.data + } + } else { + throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.') + } } catch (error) { + console.error('매장 정보 수정 실패:', error) return handleApiError(error) } } /** - * 메뉴 등록 (STR-030: 메뉴 등록) - * @param {Object} menuData - 메뉴 정보 - * @returns {Promise} 메뉴 등록 결과 + * 매출 정보 조회 (STR-020: 대시보드) + * @param {number} storeId - 매장 ID (기본값: 1) + * @returns {Promise} 매출 정보 */ - async registerMenu(menuData) { + async getSales(storeId = 1) { try { - const response = await storeApi.post('/menu/register', { - menuName: menuData.menuName, - menuCategory: menuData.menuCategory, - menuImage: menuData.menuImage, - price: menuData.price, - description: menuData.description, - isPopular: menuData.isPopular || false, - isRecommended: menuData.isRecommended || false, - }) - - return formatSuccessResponse(response.data.data, '메뉴가 등록되었습니다.') + console.log('매출 조회 API 호출:', `${this.salesURL}/${storeId}`) + + const response = await api.get(`${this.salesURL}/${storeId}`) + + if (response.data.success) { + const salesData = response.data.data + + // API 응답 데이터 로그 + console.log('매출 API 응답:', salesData) + + // yearSales 데이터가 있는지 확인하고 변곡점 계산 + let processedData = { + todaySales: salesData.todaySales || 0, + monthSales: salesData.monthSales || 0, + previousDayComparison: salesData.previousDayComparison || 0, + yearSales: salesData.yearSales || [] + } + + // 변곡점 분석 추가 + if (salesData.yearSales && salesData.yearSales.length > 0) { + processedData.trendAnalysis = this.analyzeSalesTrend(salesData.yearSales) + processedData.chartData = this.prepareChartData(salesData.yearSales) + } + + return formatSuccessResponse(processedData, '매출 정보를 조회했습니다.') + } else { + throw new Error(response.data.message || '매출 정보 조회에 실패했습니다.') + } } catch (error) { - return handleApiError(error) + console.error('매출 정보 조회 실패:', error) + + // API 오류 시 기본 데이터 반환 (개발 단계) + const fallbackData = { + todaySales: 170000, + monthSales: 4500000, + previousDayComparison: 15000, + yearSales: [], + trendAnalysis: { + inflectionPoints: [], + overallTrend: 'stable', + growthRate: 0 + }, + chartData: { + labels: [], + salesData: [], + targetData: [] + } + } + + return formatSuccessResponse(fallbackData, '임시 매출 데이터를 표시합니다.') } } /** - * 메뉴 목록 조회 (STR-025: 메뉴 조회) - * @param {Object} filters - 필터링 옵션 + * 매출 트렌드 분석 및 변곡점 계산 + * @param {Array} yearSales - 연간 매출 데이터 (Sales 엔티티 배열) + * @returns {Object} 트렌드 분석 결과 + */ + analyzeSalesTrend(yearSales) { + if (!yearSales || yearSales.length < 7) { + return { + inflectionPoints: [], + overallTrend: 'insufficient_data', + growthRate: 0 + } + } + + // 날짜순 정렬 (salesDate 기준) + const sortedData = [...yearSales].sort((a, b) => + new Date(a.salesDate) - new Date(b.salesDate) + ) + + const inflectionPoints = [] + const dailyData = sortedData.map(item => ({ + date: item.salesDate, + amount: parseFloat(item.salesAmount) || 0 + })) + + // 7일 이동평균으로 변곡점 탐지 + for (let i = 7; i < dailyData.length - 7; i++) { + const prevWeekAvg = this.calculateMovingAverage(dailyData, i - 7, 7) + const currentWeekAvg = this.calculateMovingAverage(dailyData, i, 7) + const nextWeekAvg = this.calculateMovingAverage(dailyData, i + 7, 7) + + // 변곡점 조건: 이전 주 → 현재 주 → 다음 주의 트렌드 변화 + const trend1 = currentWeekAvg - prevWeekAvg + const trend2 = nextWeekAvg - currentWeekAvg + + // 트렌드 방향이 바뀌고 변화량이 일정 이상인 경우 + if (Math.sign(trend1) !== Math.sign(trend2) && + Math.abs(trend1) > 10000 && Math.abs(trend2) > 10000) { + + inflectionPoints.push({ + date: dailyData[i].date, + amount: dailyData[i].amount, + type: trend1 > 0 ? 'peak' : 'valley', + significance: Math.abs(trend1) + Math.abs(trend2) + }) + } + } + + // 전체적인 트렌드 계산 + const firstWeekAvg = this.calculateMovingAverage(dailyData, 0, 7) + const lastWeekAvg = this.calculateMovingAverage(dailyData, dailyData.length - 7, 7) + const growthRate = ((lastWeekAvg - firstWeekAvg) / firstWeekAvg) * 100 + + let overallTrend = 'stable' + if (Math.abs(growthRate) > 5) { + overallTrend = growthRate > 0 ? 'increasing' : 'decreasing' + } + + console.log('변곡점 분석 결과:', { inflectionPoints, overallTrend, growthRate }) + + return { + inflectionPoints: inflectionPoints.slice(0, 5), // 상위 5개만 + overallTrend, + growthRate: Math.round(growthRate * 10) / 10 + } + } + + /** + * 이동평균 계산 + * @param {Array} data - 데이터 배열 + * @param {number} startIndex - 시작 인덱스 + * @param {number} period - 기간 + * @returns {number} 이동평균값 + */ + calculateMovingAverage(data, startIndex, period) { + const slice = data.slice(startIndex, startIndex + period) + const sum = slice.reduce((acc, item) => acc + item.amount, 0) + return sum / slice.length + } + + /** + * 차트용 데이터 준비 + * @param {Array} yearSales - 연간 매출 데이터 + * @returns {Object} 차트 데이터 + */ + prepareChartData(yearSales) { + if (!yearSales || yearSales.length === 0) { + return { labels: [], salesData: [], targetData: [] } + } + + // 최근 90일 데이터만 사용 (차트 표시용) + const sortedData = [...yearSales] + .sort((a, b) => new Date(a.salesDate) - new Date(b.salesDate)) + .slice(-90) + + const labels = sortedData.map(item => { + const date = new Date(item.salesDate) + return `${date.getMonth() + 1}/${date.getDate()}` + }) + + const salesData = sortedData.map(item => parseFloat(item.salesAmount) || 0) + + // 목표 매출 라인 (평균의 110%) + const averageSales = salesData.reduce((a, b) => a + b, 0) / salesData.length + const targetData = salesData.map(() => averageSales * 1.1) + + return { + labels, + salesData, + targetData + } + } + + /** + * 메뉴 목록 조회 (개발 예정) * @returns {Promise} 메뉴 목록 */ - async getMenus(filters = {}) { + async getMenus() { try { - const queryParams = new URLSearchParams() - - if (filters.category) queryParams.append('category', filters.category) - if (filters.sortBy) queryParams.append('sortBy', filters.sortBy) - if (filters.isPopular !== undefined) queryParams.append('isPopular', filters.isPopular) - - const response = await storeApi.get(`/menu?${queryParams.toString()}`) - - return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 메뉴 수정 (STR-035: 메뉴 수정) - * @param {number} menuId - 메뉴 ID - * @param {Object} menuData - 수정할 메뉴 정보 - * @returns {Promise} 메뉴 수정 결과 - */ - async updateMenu(menuId, menuData) { - try { - const response = await storeApi.put(`/menu/${menuId}`, menuData) - - return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 메뉴 삭제 (STR-040: 메뉴 삭제) - * @param {number} menuId - 메뉴 ID - * @returns {Promise} 메뉴 삭제 결과 - */ - async deleteMenu(menuId) { - try { - await storeApi.delete(`/menu/${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}개의 메뉴가 삭제되었습니다.`) - } catch (error) { - return handleApiError(error) - } - } - - /** - * 매장 통계 정보 조회 - * @returns {Promise} 매장 통계 - */ - async getStoreStatistics() { - try { - const response = await storeApi.get('/statistics') - - return formatSuccessResponse(response.data.data, '매장 통계를 조회했습니다.') + // 현재는 목업 데이터 반환 (추후 실제 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/views/DashboardView.vue b/src/views/DashboardView.vue index d7ee4c4..f69614e 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -1,4 +1,4 @@ -//* src/views/DashboardView.vue - 완전 수정버전 +//* src/views/DashboardView.vue - 차트 연동 수정버전 + \ No newline at end of file