diff --git a/public/runtime-env.js b/public/runtime-env.js index 0cd6c8e..e8eba95 100644 --- a/public/runtime-env.js +++ b/public/runtime-env.js @@ -1,16 +1,19 @@ -//* public/runtime-env.js - 디버깅 포함 버전 +//* public/runtime-env.js - 수정버전 console.log('=== RUNTIME-ENV.JS 로드됨 ==='); window.__runtime_config__ = { - // 로컬 개발 환경 설정 + // 기존 설정들... 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', // ← 이 줄 추가 CONTENT_URL: 'http://localhost:8083/api/content', RECOMMEND_URL: 'http://localhost:8084/api/recommendation', - // Gateway 주석 처리 (로컬에서는 사용 안함) + // 프로덕션 환경 (주석 처리) // 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', // 기능 플래그 FEATURES: { @@ -31,5 +34,7 @@ window.__runtime_config__ = { console.log('=== 설정된 API URLs ==='); console.log('AUTH_URL:', window.__runtime_config__.AUTH_URL); -console.log('MEMBER_URL:', window.__runtime_config__.MEMBER_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 diff --git a/src/services/api.js b/src/services/api.js index 223b0f1..2a1cbd9 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,18 +1,24 @@ -//* src/services/api.js +//* src/services/api.js - 수정버전 import axios from 'axios' // 런타임 환경 설정에서 API URL 가져오기 const getApiUrls = () => { const config = window.__runtime_config__ || {} return { + // 환경변수에서 가져오도록 수정 + 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', - 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', - 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', - RECOMMEND_URL: config.RECOMMEND_URL || 'http://20.1.2.3/api/recommendation', } } @@ -28,12 +34,22 @@ const createApiInstance = (baseURL) => { }) // 요청 인터셉터 - JWT 토큰 자동 추가 -instance.interceptors.request.use( + instance.interceptors.request.use( (config) => { - const token = localStorage.getItem('accessToken') + // accessToken 또는 token 둘 다 확인 + const token = localStorage.getItem('accessToken') || localStorage.getItem('token') 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)}...`) + } + } + return config }, (error) => { @@ -44,11 +60,20 @@ instance.interceptors.request.use( // 응답 인터셉터 - 토큰 갱신 및 에러 처리 instance.interceptors.response.use( (response) => { + // 성공 응답 로깅 (개발 모드에서만) + if (import.meta.env.DEV) { + console.log(`API 응답: ${response.status} ${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) + } + // 401 에러이고 토큰 갱신을 시도하지 않은 경우 if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true @@ -62,6 +87,7 @@ instance.interceptors.request.use( const { accessToken, refreshToken: newRefreshToken } = refreshResponse.data.data localStorage.setItem('accessToken', accessToken) + localStorage.setItem('token', accessToken) // 호환성을 위해 둘 다 저장 localStorage.setItem('refreshToken', newRefreshToken) // 원래 요청에 새 토큰으로 재시도 @@ -71,6 +97,7 @@ instance.interceptors.request.use( } catch (refreshError) { // 토큰 갱신 실패 시 로그아웃 처리 localStorage.removeItem('accessToken') + localStorage.removeItem('token') localStorage.removeItem('refreshToken') localStorage.removeItem('userInfo') window.location.href = '/login' @@ -86,6 +113,15 @@ instance.interceptors.request.use( // API 인스턴스들 생성 const apiUrls = getApiUrls() + +// 디버깅용 로그 (개발 모드에서만) +if (import.meta.env.DEV) { + console.log('=== API URLs 설정 ===') + Object.entries(apiUrls).forEach(([key, url]) => { + console.log(`${key}: ${url}`) + }) +} + export const memberApi = createApiInstance(apiUrls.MEMBER_URL) export const authApi = createApiInstance(apiUrls.AUTH_URL) export const storeApi = createApiInstance(apiUrls.STORE_URL) @@ -93,11 +129,12 @@ 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 @@ -159,5 +196,4 @@ export const formatSuccessResponse = (data, message = '요청이 성공적으로 message, data } -} - +} \ No newline at end of file diff --git a/src/services/recommend.js b/src/services/recommend.js index 9099163..e6f2d62 100644 --- a/src/services/recommend.js +++ b/src/services/recommend.js @@ -1,4 +1,4 @@ -//* src/services/recommend.js +//* src/services/recommend.js - 수정버전 import { recommendApi, handleApiError, formatSuccessResponse } from './api.js' /** @@ -8,135 +8,101 @@ import { recommendApi, handleApiError, formatSuccessResponse } from './api.js' class RecommendService { /** * AI 마케팅 팁 생성 (REC-005: AI 마케팅 방법 추천) + * ⚠️ 수정: 백엔드 API 스펙에 맞게 요청 구조 변경 * @param {Object} requestData - 마케팅 팁 요청 정보 * @returns {Promise} 생성된 마케팅 팁 */ async generateMarketingTips(requestData = {}) { try { - const response = await recommendApi.post('/marketing-tips', { + // 백엔드 MarketingTipRequest DTO에 맞는 구조로 변경 + const requestBody = { storeId: requestData.storeId, - includeWeather: requestData.includeWeather !== false, // 기본값 true - includeTrends: requestData.includeTrends !== false, // 기본값 true - maxTips: requestData.maxTips || 3, - tipType: requestData.tipType || 'general', // general, menu, marketing, operation - }) + // 필요시 추가 필드들 + additionalRequirement: requestData.additionalRequirement || '', + } + + 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) } } /** - * 날씨 기반 메뉴 추천 + * 마케팅 팁 이력 조회 * @param {number} storeId - 매장 ID - * @returns {Promise} 날씨 기반 메뉴 추천 + * @param {Object} pagination - 페이지네이션 정보 + * @returns {Promise} 마케팅 팁 이력 */ - async getWeatherBasedMenuRecommendation(storeId) { + async getMarketingTipHistory(storeId, pagination = {}) { try { - const response = await recommendApi.get(`/weather-menu/${storeId}`) + 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) - return formatSuccessResponse(response.data.data, '날씨 기반 메뉴 추천을 조회했습니다.') + const response = await recommendApi.get(`/marketing-tips?${params.toString()}`) + + return formatSuccessResponse(response.data.data, '마케팅 팁 이력을 조회했습니다.') } catch (error) { return handleApiError(error) } } /** - * 매출 예측 추천 - * @param {number} storeId - 매장 ID - * @param {string} period - 예측 기간 (day, week, month) - * @returns {Promise} 매출 예측 정보 + * 마케팅 팁 상세 조회 + * @param {number} tipId - 팁 ID + * @returns {Promise} 마케팅 팁 상세 정보 */ - async getSalesPrediction(storeId, period = 'day') { + async getMarketingTip(tipId) { try { - const response = await recommendApi.get(`/sales-prediction/${storeId}?period=${period}`) + const response = await recommendApi.get(`/marketing-tips/${tipId}`) - return formatSuccessResponse(response.data.data, '매출 예측 정보를 조회했습니다.') + return formatSuccessResponse(response.data.data, '마케팅 팁 상세 정보를 조회했습니다.') } catch (error) { return handleApiError(error) } } /** - * 인기 메뉴 추천 - * @param {number} storeId - 매장 ID - * @param {string} period - 분석 기간 - * @returns {Promise} 인기 메뉴 추천 - */ - async getPopularMenuRecommendation(storeId, period = 'month') { - try { - const response = await recommendApi.get(`/popular-menu/${storeId}?period=${period}`) - - return formatSuccessResponse(response.data.data, '인기 메뉴 추천을 조회했습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 마케팅 전략 추천 - * @param {number} storeId - 매장 ID - * @param {string} strategyType - 전략 유형 (sales_boost, customer_retention, cost_reduction) - * @returns {Promise} 마케팅 전략 추천 - */ - async getMarketingStrategy(storeId, strategyType = 'sales_boost') { - try { - const response = await recommendApi.get(`/marketing-strategy/${storeId}?type=${strategyType}`) - - return formatSuccessResponse(response.data.data, '마케팅 전략 추천을 조회했습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 통합 AI 추천 (매출 예측 + 메뉴 추천 + 마케팅 전략) + * 종합 AI 추천 (대시보드용) * @param {number} storeId - 매장 ID * @returns {Promise} 통합 AI 추천 정보 */ async getComprehensiveRecommendation(storeId) { try { - const response = await recommendApi.get(`/comprehensive-recommendation/${storeId}`) + // 여러 추천 API를 병렬로 호출 + const [marketingTips, tipHistory] = await Promise.allSettled([ + this.generateMarketingTips({ storeId }), + this.getMarketingTipHistory(storeId, { size: 5 }) + ]) - return formatSuccessResponse(response.data.data, '통합 AI 추천을 조회했습니다.') + const result = { + marketingTips: marketingTips.status === 'fulfilled' ? marketingTips.value : null, + recentHistory: tipHistory.status === 'fulfilled' ? tipHistory.value : null, + } + + return formatSuccessResponse(result, '통합 AI 추천을 조회했습니다.') } catch (error) { return handleApiError(error) } } /** - * 추천 기록 조회 - * @param {Object} filters - 필터링 옵션 - * @returns {Promise} 추천 기록 목록 - */ - async getRecommendationHistory(filters = {}) { - try { - const queryParams = new URLSearchParams() - - if (filters.type) queryParams.append('type', filters.type) - if (filters.startDate) queryParams.append('startDate', filters.startDate) - if (filters.endDate) queryParams.append('endDate', filters.endDate) - if (filters.page) queryParams.append('page', filters.page) - if (filters.size) queryParams.append('size', filters.size || 20) - - const response = await recommendApi.get(`/history?${queryParams.toString()}`) - - return formatSuccessResponse(response.data.data, '추천 기록을 조회했습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 추천 피드백 제공 - * @param {number} recommendationId - 추천 ID + * 추천 피드백 제공 (향후 구현) + * @param {number} tipId - 추천 ID * @param {Object} feedback - 피드백 정보 * @returns {Promise} 피드백 제공 결과 */ - async provideFeedback(recommendationId, feedback) { + async provideFeedback(tipId, feedback) { try { - const response = await recommendApi.post(`/feedback/${recommendationId}`, { + const response = await recommendApi.post(`/marketing-tips/${tipId}/feedback`, { rating: feedback.rating, // 1-5 점수 useful: feedback.useful, // true/false comment: feedback.comment || '', @@ -148,7 +114,40 @@ class RecommendService { return handleApiError(error) } } + + /** + * 개발 모드용 Mock 추천 생성 + * @param {Object} requestData - 요청 데이터 + * @returns {Promise} Mock 추천 데이터 + */ + async generateMockRecommendation(requestData = {}) { + // 개발 모드에서만 사용 + if (!import.meta.env.DEV) { + return this.generateMarketingTips(requestData) + } + + 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 마케팅 팁이 생성되었습니다.') + } } export const recommendService = new RecommendService() -export default recommendService +export default recommendService \ No newline at end of file diff --git a/src/services/sales.js b/src/services/sales.js new file mode 100644 index 0000000..7df1b13 --- /dev/null +++ b/src/services/sales.js @@ -0,0 +1,325 @@ +//* src/services/sales.js - 스마트 데이터 탐지 버전 +import { salesApi, handleApiError, formatSuccessResponse } from './api.js' + +/** + * 매출 관련 API 서비스 - 스마트 데이터 탐지 버전 + */ +class SalesService { + /** + * 현재 사용자의 매출 정보 조회 (JWT 기반) + */ + async getMySales() { + try { + const response = await salesApi.get('/my') + return formatSuccessResponse(response.data.data, '내 매출 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 매장 매출 정보 조회 + */ + async getSales(storeId) { + try { + const response = await salesApi.get(`/${storeId}`) + return formatSuccessResponse(response.data.data, '매출 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 실제 데이터가 있는 Store 자동 탐지 🔍 + */ + async findStoreWithData(maxStoreId = 50) { + console.log(`🔍 [DETECTOR] 실제 데이터 탐지 시작 (1~${maxStoreId}번까지)`) + + const foundStores = [] + const errors = [] + + // 1~maxStoreId까지 모든 Store ID 시도 + for (let storeId = 1; storeId <= maxStoreId; storeId++) { + 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) + } + } 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.log('📊 [SUMMARY] 데이터 탐지 완료:', { + totalScanned: maxStoreId, + foundStores: foundStores.length, + errors: errors.length, + errorTypes: this.summarizeErrors(errors) + }) + + 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 + } + } + } + + /** + * 데이터 품질 검사 📋 + */ + checkDataQuality(data) { + const quality = { + hasRealData: false, + score: 0, + reasons: [], + issues: [] + } + + // 1. 기본 데이터 존재 여부 + if (data.todaySales && Number(data.todaySales) > 0) { + quality.score += 30 + quality.reasons.push('오늘 매출 데이터 존재') + } else { + quality.issues.push('오늘 매출 없음') + } + + if (data.monthSales && Number(data.monthSales) > 0) { + quality.score += 30 + quality.reasons.push('월간 매출 데이터 존재') + } else { + quality.issues.push('월간 매출 없음') + } + + // 2. 연간 데이터 품질 + if (data.yearSales && Array.isArray(data.yearSales) && data.yearSales.length > 0) { + quality.score += 25 + 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}건`) + } + } 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' + + 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' + } + } + + /** + * 에러 요약 📊 + */ + 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 기반 조회 시도 + 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 + } + } + } + } 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] 자동 탐지 성공!') + 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}에서 실제 데이터 발견` + } + } else { + console.error('❌ [AUTO] 자동 탐지 실패 - 실제 데이터를 찾지 못했습니다') + throw new Error('실제 매출 데이터를 찾을 수 없습니다') + } + } + + /** + * 특정 Store ID 테스트 + */ + async testSpecificStore(storeId) { + try { + console.log(`🧪 [TEST] Store ${storeId} 테스트`) + const result = await this.getSales(storeId) + + if (result.success && result.data) { + const quality = this.checkDataQuality(result.data) + console.log(`📊 [TEST] Store ${storeId} 결과:`, { + hasData: quality.hasRealData, + grade: quality.grade, + score: quality.score, + reasons: quality.reasons + }) + return { ...result, quality } + } else { + console.warn(`⚠️ [TEST] Store ${storeId} 실패:`, result.message) + return null + } + } catch (error) { + console.error(`❌ [TEST] Store ${storeId} 에러:`, error) + return null + } + } +} + +export const salesService = new SalesService() +export default salesService \ No newline at end of file diff --git a/src/services/store.js b/src/services/store.js index b347229..57916ba 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 - 매출 API 수정버전 +import { storeApi, salesApi, handleApiError, formatSuccessResponse } from './api.js' /** * 매장 관련 API 서비스 - * API 설계서 기준으로 수정됨 + * 유저스토리: STR-005, STR-010, STR-015, STR-020, STR-025, STR-030, STR-035, STR-040 */ class StoreService { /** @@ -15,14 +15,17 @@ class StoreService { try { const response = await storeApi.post('/register', { storeName: storeData.storeName, + storeImage: storeData.storeImage, businessType: storeData.businessType, address: storeData.address, phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours || storeData.operatingHours, + businessNumber: storeData.businessNumber, + instaAccounts: storeData.instaAccounts, + blogAccounts: storeData.blogAccounts, + businessHours: storeData.businessHours, closedDays: storeData.closedDays, seatCount: storeData.seatCount, - snsAccounts: storeData.snsAccounts || `인스타그램: ${storeData.instaAccount || ''}, 네이버블로그: ${storeData.naverBlogAccount || ''}`, - description: storeData.description || '' + description: storeData.description, }) return formatSuccessResponse(response.data.data, '매장이 등록되었습니다.') @@ -32,7 +35,7 @@ class StoreService { } /** - * 매장 정보 조회 (STR-005: 매장 조회) + * 매장 정보 조회 (STR-005: 매장 정보 관리) * @returns {Promise} 매장 정보 */ async getStore() { @@ -52,17 +55,7 @@ class StoreService { */ async updateStore(storeData) { try { - const response = await storeApi.put('/', { - storeName: storeData.storeName, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours || storeData.operatingHours, - closedDays: storeData.closedDays, - seatCount: storeData.seatCount, - snsAccounts: storeData.snsAccounts || `인스타그램: ${storeData.instaAccount || ''}, 네이버블로그: ${storeData.naverBlogAccount || ''}`, - description: storeData.description || '' - }) + const response = await storeApi.put('/', storeData) return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.') } catch (error) { @@ -71,35 +64,48 @@ class StoreService { } /** - * 매출 정보 조회 (SAL-005: 매출 조회) + * 매출 정보 조회 (STR-020: 대시보드) + * ⚠️ 수정: salesApi 사용하고 storeId 매개변수 추가 * @param {number} storeId - 매장 ID * @returns {Promise} 매출 정보 */ async getSales(storeId) { try { + // storeId가 없으면 먼저 매장 정보를 조회해서 storeId를 가져옴 + if (!storeId) { + const storeResponse = await this.getStore() + if (storeResponse.success && storeResponse.data.storeId) { + storeId = storeResponse.data.storeId + } else { + throw new Error('매장 정보를 찾을 수 없습니다.') + } + } + + // Sales API 호출 (Store 서비스의 /api/sales/{storeId} 엔드포인트) const response = await salesApi.get(`/${storeId}`) return formatSuccessResponse(response.data.data, '매출 정보를 조회했습니다.') } catch (error) { + console.error('매출 정보 조회 실패:', error) return handleApiError(error) } } /** - * 메뉴 등록 (MNU-010: 메뉴 등록) + * 메뉴 등록 (STR-030: 메뉴 등록) + * ⚠️ 수정: 올바른 API 경로 사용 * @param {Object} menuData - 메뉴 정보 * @returns {Promise} 메뉴 등록 결과 */ async registerMenu(menuData) { try { - const response = await menuApi.post('/register', { + // Store 서비스의 Menu API 사용 + const response = await storeApi.post('/menu/register', { + storeId: menuData.storeId, menuName: menuData.menuName, - menuCategory: menuData.menuCategory || menuData.category, - menuImage: menuData.menuImage || menuData.image, + category: menuData.category, price: menuData.price, description: menuData.description, - isPopular: menuData.isPopular || false, - isRecommended: menuData.isRecommended || false, }) return formatSuccessResponse(response.data.data, '메뉴가 등록되었습니다.') @@ -109,13 +115,13 @@ class StoreService { } /** - * 메뉴 목록 조회 (MNU-005: 메뉴 조회) + * 메뉴 목록 조회 (STR-025: 메뉴 조회) * @param {number} storeId - 매장 ID * @returns {Promise} 메뉴 목록 */ async getMenus(storeId) { try { - const response = await menuApi.get(`/${storeId}`) + const response = await storeApi.get(`/menu?storeId=${storeId}`) return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.') } catch (error) { @@ -124,22 +130,14 @@ class StoreService { } /** - * 메뉴 수정 (MNU-015: 메뉴 수정) + * 메뉴 수정 (STR-035: 메뉴 수정) * @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, - }) + const response = await storeApi.put(`/menu/${menuId}`, menuData) return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.') } catch (error) { @@ -148,13 +146,13 @@ class StoreService { } /** - * 메뉴 삭제 (MNU-020: 메뉴 삭제) + * 메뉴 삭제 (STR-040: 메뉴 삭제) * @param {number} menuId - 메뉴 ID * @returns {Promise} 메뉴 삭제 결과 */ async deleteMenu(menuId) { try { - await menuApi.delete(`/${menuId}`) + await storeApi.delete(`/menu/${menuId}`) return formatSuccessResponse(null, '메뉴가 삭제되었습니다.') } catch (error) { @@ -163,16 +161,14 @@ class StoreService { } /** - * 다중 메뉴 삭제 - * @param {number[]} menuIds - 삭제할 메뉴 ID 배열 - * @returns {Promise} 삭제 결과 + * 매장 통계 정보 조회 + * @returns {Promise} 매장 통계 */ - async deleteMenus(menuIds) { + async getStoreStatistics() { try { - const deletePromises = menuIds.map((menuId) => this.deleteMenu(menuId)) - await Promise.all(deletePromises) + const response = await storeApi.get('/statistics') - return formatSuccessResponse(null, `${menuIds.length}개의 메뉴가 삭제되었습니다.`) + return formatSuccessResponse(response.data.data, '매장 통계를 조회했습니다.') } catch (error) { return handleApiError(error) } diff --git a/src/store/auth.js b/src/store/auth.js index af2baef..11189f6 100644 --- a/src/store/auth.js +++ b/src/store/auth.js @@ -1,82 +1,217 @@ -//* src/store/auth.js 수정 - 기존 구조 유지하고 API 연동만 추가 +//* src/store/auth.js - 토큰 관리 수정버전 import { defineStore } from 'pinia' -import { ref } from 'vue' -import authService from '@/services/auth' -export const useAuthStore = defineStore('auth', () => { - // 기존 상태들 유지 - const user = ref(null) - const token = ref(null) - const isAuthenticated = ref(false) - const isLoading = ref(false) +export const useAuthStore = defineStore('auth', { + state: () => ({ + user: null, + token: null, + refreshToken: null, + isAuthenticated: false, + }), - // 기존 checkAuthState 메서드 유지 - const checkAuthState = () => { - const storedToken = localStorage.getItem('accessToken') - const storedUser = localStorage.getItem('userInfo') - - if (storedToken && storedUser) { - token.value = storedToken - user.value = JSON.parse(storedUser) - isAuthenticated.value = true - } else { - token.value = null - user.value = null - isAuthenticated.value = false - } - } - - // login 메서드를 실제 API 호출로 수정 - const login = async (credentials) => { - isLoading.value = true - - try { - const result = await authService.login(credentials) + getters: { + isLoggedIn: (state) => !!state.token && !!state.user, + userInfo: (state) => state.user, + hasValidToken: (state) => { + if (!state.token) return false - if (result.success) { - token.value = result.data.token - user.value = result.data.user - isAuthenticated.value = true - - return { success: true } - } else { - return { success: false, error: result.message } + try { + // JWT 토큰 만료 확인 (간단한 체크) + const payload = JSON.parse(atob(state.token.split('.')[1])) + const now = Date.now() / 1000 + return payload.exp > now + } catch { + return false + } + } + }, + + actions: { + /** + * 로그인 처리 + */ + async login(credentials) { + try { + // 로그인 API 호출은 별도 서비스에서 처리 + const { authService } = await import('@/services/auth') + const response = await authService.login(credentials) + + if (response.success) { + this.setAuth(response.data) + return response + } else { + throw new Error(response.message) + } + } catch (error) { + this.clearAuth() + throw error + } + }, + + /** + * 회원가입 처리 + */ + async register(userData) { + try { + const { authService } = await import('@/services/auth') + const response = await authService.register(userData) + return response + } catch (error) { + throw error + } + }, + + /** + * 로그아웃 처리 + */ + async logout() { + try { + if (this.token) { + const { authService } = await import('@/services/auth') + await authService.logout() + } + } catch (error) { + console.error('로그아웃 API 오류:', error) + } finally { + this.clearAuth() + } + }, + + /** + * 사용자 정보 새로고침 + */ + async refreshUserInfo() { + try { + if (!this.token) { + throw new Error('토큰이 없습니다') + } + + const { authService } = await import('@/services/auth') + const response = await authService.getUserInfo() + + if (response.success) { + this.user = response.data + this.isAuthenticated = true + return response + } else { + throw new Error(response.message) + } + } catch (error) { + this.clearAuth() + throw error + } + }, + + /** + * 인증 정보 설정 + * ⚠️ 수정: 토큰을 여러 형태로 저장하여 호환성 확보 + */ + setAuth(authData) { + console.log('인증 정보 설정:', authData) + + this.user = authData.user || authData.userInfo + this.token = authData.accessToken || authData.token + this.refreshToken = authData.refreshToken + this.isAuthenticated = true + + // localStorage에 여러 형태로 저장 (호환성) + if (this.token) { + localStorage.setItem('accessToken', this.token) + localStorage.setItem('token', this.token) + } + if (this.refreshToken) { + localStorage.setItem('refreshToken', this.refreshToken) + } + if (this.user) { + localStorage.setItem('userInfo', JSON.stringify(this.user)) + } + + console.log('토큰 저장 완료:', { + token: this.token?.substring(0, 20) + '...', + hasRefreshToken: !!this.refreshToken, + hasUser: !!this.user + }) + }, + + /** + * 인증 정보 초기화 + */ + clearAuth() { + console.log('인증 정보 초기화') + + this.user = null + this.token = null + this.refreshToken = null + this.isAuthenticated = false + + // localStorage에서 모든 토큰 제거 + localStorage.removeItem('accessToken') + localStorage.removeItem('token') + localStorage.removeItem('refreshToken') + localStorage.removeItem('userInfo') + }, + + /** + * 앱 시작 시 인증 상태 복원 + */ + checkAuthState() { + console.log('인증 상태 확인 중...') + + try { + // localStorage에서 토큰 복원 + const accessToken = localStorage.getItem('accessToken') || localStorage.getItem('token') + const refreshToken = localStorage.getItem('refreshToken') + const userInfo = localStorage.getItem('userInfo') + + if (accessToken && userInfo) { + this.token = accessToken + this.refreshToken = refreshToken + this.user = JSON.parse(userInfo) + + // 토큰 유효성 간단 확인 + if (this.hasValidToken) { + this.isAuthenticated = true + console.log('인증 상태 복원 성공') + } else { + console.log('토큰 만료됨 - 재로그인 필요') + this.clearAuth() + } + } else { + console.log('저장된 인증 정보 없음') + this.clearAuth() + } + } catch (error) { + console.error('인증 상태 복원 실패:', error) + this.clearAuth() + } + }, + + /** + * 토큰 갱신 + */ + async refreshAccessToken() { + try { + if (!this.refreshToken) { + throw new Error('리프레시 토큰이 없습니다') + } + + const { authService } = await import('@/services/auth') + const response = await authService.refreshToken(this.refreshToken) + + if (response.success) { + this.setAuth({ + ...response.data, + user: this.user // 기존 사용자 정보 유지 + }) + return response + } else { + throw new Error(response.message) + } + } catch (error) { + console.error('토큰 갱신 실패:', error) + this.clearAuth() + throw error } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false } } - - // logout 메서드를 실제 API 호출로 수정 - const logout = async () => { - isLoading.value = true - - try { - await authService.logout() - } catch (error) { - console.warn('로그아웃 API 호출 실패:', error) - } finally { - // 상태 초기화 - token.value = null - user.value = null - isAuthenticated.value = false - isLoading.value = false - } - } - - // 초기화 시 인증 상태 확인 - checkAuthState() - - return { - user, - token, - isAuthenticated, - isLoading, - login, - logout, - checkAuthState - } -}) - +}) \ No newline at end of file diff --git a/src/store/store.js b/src/store/store.js index 138485c..c88850e 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,202 +1,170 @@ -//* 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 서비스 + * 유저스토리: 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 result = await storeService.getStore() - - if (result.success) { - storeInfo.value = result.data - return { success: true } - } else { - console.warn('매장 정보 조회 실패:', result.message) - return { success: false, error: result.message } - } + 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) { - console.warn('매장 정보 조회 실패:', error) - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + return handleApiError(error) } } - // saveStoreInfo를 실제 API 호출로 수정 - const saveStoreInfo = async (storeData) => { - isLoading.value = true - + /** + * 매장 정보 조회 (현재 로그인 사용자) - STR-005: 매장 정보 관리 + * @returns {Promise} 매장 정보 + */ + async getStore() { try { - let result - if (storeInfo.value) { - // 기존 매장 정보 수정 - result = await storeService.updateStore(storeData) - } else { - // 새 매장 등록 - result = await storeService.registerStore(storeData) - } - - if (result.success) { - storeInfo.value = result.data - return { success: true, message: '매장 정보가 저장되었습니다.' } - } else { - return { success: false, error: result.message } - } + const response = await storeApi.get('/') + return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.') } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + return handleApiError(error) } } - // fetchMenus를 실제 API 호출로 수정 - const fetchMenus = async () => { - if (!storeInfo.value?.storeId) { - console.warn('매장 ID가 없어 메뉴를 조회할 수 없습니다.') - return { success: false, error: '매장 정보가 필요합니다.' } - } - - isLoading.value = true - + /** + * 매장 정보 수정 (STR-010: 매장 수정) + * @param {Object} storeData - 수정할 매장 정보 (storeId 불필요 - JWT에서 추출) + * @returns {Promise} 매장 수정 결과 + */ + async updateStore(storeData) { try { - 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 } - } + const response = await storeApi.put('/', storeData) + return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.') } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + return handleApiError(error) } } - // 메뉴 관련 메서드들 API 연동 추가 - const saveMenu = async (menuData) => { - isLoading.value = true - + /** + * 메뉴 등록 (STR-030: 메뉴 등록) + * @param {Object} menuData - 메뉴 정보 + * @returns {Promise} 메뉴 등록 결과 + */ + async registerMenu(menuData) { try { - const result = await storeService.registerMenu(menuData) - - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 등록되었습니다.' } - } else { - return { success: false, error: result.message } - } + 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, '메뉴가 등록되었습니다.') } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + return handleApiError(error) } } - const updateMenu = async (menuId, menuData) => { - isLoading.value = true - + /** + * 메뉴 목록 조회 (STR-025: 메뉴 조회) + * @param {Object} filters - 필터링 옵션 + * @returns {Promise} 메뉴 목록 + */ + async getMenus(filters = {}) { try { - const result = await storeService.updateMenu(menuId, menuData) - - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 수정되었습니다.' } - } else { - return { success: false, error: result.message } - } + 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 { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + return handleApiError(error) } } - const deleteMenu = async (menuId) => { - isLoading.value = true - + /** + * 메뉴 수정 (STR-035: 메뉴 수정) + * @param {number} menuId - 메뉴 ID + * @param {Object} menuData - 수정할 메뉴 정보 + * @returns {Promise} 메뉴 수정 결과 + */ + async updateMenu(menuId, menuData) { try { - const result = await storeService.deleteMenu(menuId) - - if (result.success) { - // 메뉴 목록 새로고침 - await fetchMenus() - return { success: true, message: '메뉴가 삭제되었습니다.' } - } else { - return { success: false, error: result.message } - } + const response = await storeApi.put(`/menu/${menuId}`, menuData) + + return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.') } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + return handleApiError(error) } } - // 매출 정보 조회 추가 - const fetchSalesData = async () => { - if (!storeInfo.value?.storeId) { - return { success: false, error: '매장 정보가 필요합니다.' } - } - - isLoading.value = true - + /** + * 메뉴 삭제 (STR-040: 메뉴 삭제) + * @param {number} menuId - 메뉴 ID + * @returns {Promise} 메뉴 삭제 결과 + */ + async deleteMenu(menuId) { 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 } - } + await storeApi.delete(`/menu/${menuId}`) + + return formatSuccessResponse(null, '메뉴가 삭제되었습니다.') } 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 - } -}) + /** + * 다중 메뉴 삭제 + * @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, '매장 통계를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } +} + +export const storeService = new StoreService() +export default storeService \ No newline at end of file diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 14ab02b..d7ee4c4 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -1,4 +1,4 @@ -//* src/views/DashboardView.vue +//* src/views/DashboardView.vue - 완전 수정버전