release
This commit is contained in:
parent
de983127a3
commit
0ff2f15b65
@ -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__);
|
||||
@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,10 +36,20 @@ const createApiInstance = (baseURL) => {
|
||||
// 요청 인터셉터 - JWT 토큰 자동 추가
|
||||
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
|
||||
|
||||
@ -160,4 +197,3 @@ export const formatSuccessResponse = (data, message = '요청이 성공적으로
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<Object>} 생성된 마케팅 팁
|
||||
*/
|
||||
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<Object>} 날씨 기반 메뉴 추천
|
||||
* @param {Object} pagination - 페이지네이션 정보
|
||||
* @returns {Promise<Object>} 마케팅 팁 이력
|
||||
*/
|
||||
async getWeatherBasedMenuRecommendation(storeId) {
|
||||
async getMarketingTipHistory(storeId, pagination = {}) {
|
||||
try {
|
||||
const response = await recommendApi.get(`/weather-menu/${storeId}`)
|
||||
const params = new URLSearchParams()
|
||||
params.append('storeId', storeId)
|
||||
|
||||
return formatSuccessResponse(response.data.data, '날씨 기반 메뉴 추천을 조회했습니다.')
|
||||
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, '마케팅 팁 이력을 조회했습니다.')
|
||||
} catch (error) {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 매출 예측 추천
|
||||
* @param {number} storeId - 매장 ID
|
||||
* @param {string} period - 예측 기간 (day, week, month)
|
||||
* @returns {Promise<Object>} 매출 예측 정보
|
||||
* 마케팅 팁 상세 조회
|
||||
* @param {number} tipId - 팁 ID
|
||||
* @returns {Promise<Object>} 마케팅 팁 상세 정보
|
||||
*/
|
||||
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<Object>} 인기 메뉴 추천
|
||||
*/
|
||||
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<Object>} 마케팅 전략 추천
|
||||
*/
|
||||
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<Object>} 통합 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<Object>} 추천 기록 목록
|
||||
*/
|
||||
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<Object>} 피드백 제공 결과
|
||||
*/
|
||||
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,6 +114,39 @@ class RecommendService {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개발 모드용 Mock 추천 생성
|
||||
* @param {Object} requestData - 요청 데이터
|
||||
* @returns {Promise<Object>} 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()
|
||||
|
||||
325
src/services/sales.js
Normal file
325
src/services/sales.js
Normal file
@ -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
|
||||
@ -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<Object>} 매장 정보
|
||||
*/
|
||||
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<Object>} 매출 정보
|
||||
*/
|
||||
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<Object>} 메뉴 등록 결과
|
||||
*/
|
||||
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<Object>} 메뉴 목록
|
||||
*/
|
||||
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<Object>} 메뉴 수정 결과
|
||||
*/
|
||||
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<Object>} 메뉴 삭제 결과
|
||||
*/
|
||||
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<Object>} 삭제 결과
|
||||
* 매장 통계 정보 조회
|
||||
* @returns {Promise<Object>} 매장 통계
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token && !!state.user,
|
||||
userInfo: (state) => state.user,
|
||||
hasValidToken: (state) => {
|
||||
if (!state.token) return false
|
||||
|
||||
try {
|
||||
const result = await authService.login(credentials)
|
||||
// JWT 토큰 만료 확인 (간단한 체크)
|
||||
const payload = JSON.parse(atob(state.token.split('.')[1]))
|
||||
const now = Date.now() / 1000
|
||||
return payload.exp > now
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
if (result.success) {
|
||||
token.value = result.data.token
|
||||
user.value = result.data.user
|
||||
isAuthenticated.value = true
|
||||
actions: {
|
||||
/**
|
||||
* 로그인 처리
|
||||
*/
|
||||
async login(credentials) {
|
||||
try {
|
||||
// 로그인 API 호출은 별도 서비스에서 처리
|
||||
const { authService } = await import('@/services/auth')
|
||||
const response = await authService.login(credentials)
|
||||
|
||||
return { success: true }
|
||||
if (response.success) {
|
||||
this.setAuth(response.data)
|
||||
return response
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
throw new Error(response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
this.clearAuth()
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// logout 메서드를 실제 API 호출로 수정
|
||||
const logout = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
/**
|
||||
* 회원가입 처리
|
||||
*/
|
||||
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.warn('로그아웃 API 호출 실패:', error)
|
||||
console.error('로그아웃 API 오류:', error)
|
||||
} finally {
|
||||
// 상태 초기화
|
||||
token.value = null
|
||||
user.value = null
|
||||
isAuthenticated.value = false
|
||||
isLoading.value = false
|
||||
this.clearAuth()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 사용자 정보 새로고침
|
||||
*/
|
||||
async refreshUserInfo() {
|
||||
try {
|
||||
if (!this.token) {
|
||||
throw new Error('토큰이 없습니다')
|
||||
}
|
||||
|
||||
// 초기화 시 인증 상태 확인
|
||||
checkAuthState()
|
||||
const { authService } = await import('@/services/auth')
|
||||
const response = await authService.getUserInfo()
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
login,
|
||||
logout,
|
||||
checkAuthState
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -1,202 +1,170 @@
|
||||
//* src/store/store.js 수정 - 기존 구조 유지하고 API 연동만 추가
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import storeService from '@/services/store'
|
||||
|
||||
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
|
||||
//* src/services/store.js - 수정버전
|
||||
import { storeApi, handleApiError, formatSuccessResponse } from './api.js'
|
||||
|
||||
/**
|
||||
* 매장 관련 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<Object>} 매장 등록 결과
|
||||
*/
|
||||
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 }
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('매장 정보 조회 실패:', error)
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// saveStoreInfo를 실제 API 호출로 수정
|
||||
const saveStoreInfo = async (storeData) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
let result
|
||||
if (storeInfo.value) {
|
||||
// 기존 매장 정보 수정
|
||||
result = await storeService.updateStore(storeData)
|
||||
} else {
|
||||
// 새 매장 등록
|
||||
result = await storeService.registerStore(storeData)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
storeInfo.value = result.data
|
||||
return { success: true, message: '매장 정보가 저장되었습니다.' }
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// fetchMenus를 실제 API 호출로 수정
|
||||
const fetchMenus = async () => {
|
||||
if (!storeInfo.value?.storeId) {
|
||||
console.warn('매장 ID가 없어 메뉴를 조회할 수 없습니다.')
|
||||
return { success: false, error: '매장 정보가 필요합니다.' }
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await storeService.getMenus(storeInfo.value.storeId)
|
||||
|
||||
if (result.success) {
|
||||
menus.value = result.data
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 메뉴 관련 메서드들 API 연동 추가
|
||||
const saveMenu = async (menuData) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await storeService.registerMenu(menuData)
|
||||
|
||||
if (result.success) {
|
||||
// 메뉴 목록 새로고침
|
||||
await fetchMenus()
|
||||
return { success: true, message: '메뉴가 등록되었습니다.' }
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateMenu = async (menuId, menuData) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await storeService.updateMenu(menuId, menuData)
|
||||
|
||||
if (result.success) {
|
||||
// 메뉴 목록 새로고침
|
||||
await fetchMenus()
|
||||
return { success: true, message: '메뉴가 수정되었습니다.' }
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMenu = async (menuId) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await storeService.deleteMenu(menuId)
|
||||
|
||||
if (result.success) {
|
||||
// 메뉴 목록 새로고침
|
||||
await fetchMenus()
|
||||
return { success: true, message: '메뉴가 삭제되었습니다.' }
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 매출 정보 조회 추가
|
||||
const 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 정보 조회 (현재 로그인 사용자) - STR-005: 매장 정보 관리
|
||||
* @returns {Promise<Object>} 매장 정보
|
||||
*/
|
||||
async getStore() {
|
||||
try {
|
||||
const response = await storeApi.get('/')
|
||||
return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.')
|
||||
} catch (error) {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 정보 수정 (STR-010: 매장 수정)
|
||||
* @param {Object} storeData - 수정할 매장 정보 (storeId 불필요 - JWT에서 추출)
|
||||
* @returns {Promise<Object>} 매장 수정 결과
|
||||
*/
|
||||
async updateStore(storeData) {
|
||||
try {
|
||||
const response = await storeApi.put('/', storeData)
|
||||
return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.')
|
||||
} catch (error) {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 등록 (STR-030: 메뉴 등록)
|
||||
* @param {Object} menuData - 메뉴 정보
|
||||
* @returns {Promise<Object>} 메뉴 등록 결과
|
||||
*/
|
||||
async registerMenu(menuData) {
|
||||
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, '메뉴가 등록되었습니다.')
|
||||
} catch (error) {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 목록 조회 (STR-025: 메뉴 조회)
|
||||
* @param {Object} filters - 필터링 옵션
|
||||
* @returns {Promise<Object>} 메뉴 목록
|
||||
*/
|
||||
async getMenus(filters = {}) {
|
||||
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<Object>} 메뉴 수정 결과
|
||||
*/
|
||||
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<Object>} 메뉴 삭제 결과
|
||||
*/
|
||||
async deleteMenu(menuId) {
|
||||
try {
|
||||
await storeApi.delete(`/menu/${menuId}`)
|
||||
|
||||
return formatSuccessResponse(null, '메뉴가 삭제되었습니다.')
|
||||
} catch (error) {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 다중 메뉴 삭제
|
||||
* @param {number[]} menuIds - 삭제할 메뉴 ID 배열
|
||||
* @returns {Promise<Object>} 삭제 결과
|
||||
*/
|
||||
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<Object>} 매장 통계
|
||||
*/
|
||||
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
|
||||
@ -1,4 +1,4 @@
|
||||
//* src/views/DashboardView.vue
|
||||
//* src/views/DashboardView.vue - 완전 수정버전
|
||||
<template>
|
||||
<div>
|
||||
<!-- 메인 컨텐츠 -->
|
||||
@ -421,10 +421,15 @@ import { useAuthStore } from '@/store/auth'
|
||||
import { useAppStore } from '@/store/app'
|
||||
import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/formatters'
|
||||
|
||||
// ⚠️ API 서비스 import - salesService 추가
|
||||
import { storeService } from '@/services/store'
|
||||
import { salesService } from '@/services/sales' // ← 새로 추가
|
||||
import { recommendService } from '@/services/recommend'
|
||||
|
||||
/**
|
||||
* 대시보드 메인 페이지 - App.vue의 단일 AppBar 사용
|
||||
* - AI 추천을 단일 상세 콘텐츠로 변경
|
||||
* - Claude API 연동 준비된 구조
|
||||
* - 실제 API 연동 적용 (매장/매출 분리)
|
||||
*/
|
||||
|
||||
const router = useRouter()
|
||||
@ -441,6 +446,10 @@ const chartCanvas = ref(null)
|
||||
const currentTime = ref('')
|
||||
const aiError = ref('')
|
||||
|
||||
// ⚠️ 매장 정보 상태 추가
|
||||
const storeInfo = ref(null)
|
||||
const currentStoreId = ref(null)
|
||||
|
||||
// 툴팁 관련
|
||||
const tooltip = ref({
|
||||
show: false,
|
||||
@ -451,22 +460,22 @@ const tooltip = ref({
|
||||
target: 0
|
||||
})
|
||||
|
||||
// 대시보드 지표
|
||||
// 대시보드 지표 (초기값 - API에서 업데이트됨)
|
||||
const dashboardMetrics = ref([
|
||||
{
|
||||
title: '오늘의 매출',
|
||||
value: 567000,
|
||||
displayValue: '₩567,000',
|
||||
change: '전일 대비 +15%',
|
||||
value: 0,
|
||||
displayValue: '₩0',
|
||||
change: '로딩 중...',
|
||||
trend: 'up',
|
||||
icon: 'mdi-cash-multiple',
|
||||
color: 'success'
|
||||
},
|
||||
{
|
||||
title: '이번 달 매출',
|
||||
value: 12450000,
|
||||
displayValue: '₩12,450,000',
|
||||
change: '전월 대비 +8%',
|
||||
value: 0,
|
||||
displayValue: '₩0',
|
||||
change: '로딩 중...',
|
||||
trend: 'up',
|
||||
icon: 'mdi-trending-up',
|
||||
color: 'primary'
|
||||
@ -482,7 +491,7 @@ const dashboardMetrics = ref([
|
||||
},
|
||||
])
|
||||
|
||||
// 차트 데이터
|
||||
// 차트 데이터 (기본값 - API에서 업데이트 예정)
|
||||
const chartData = ref({
|
||||
'7d': [
|
||||
{ label: '6일전', sales: 45, target: 50, date: '06-04' },
|
||||
@ -510,53 +519,404 @@ const chartData = ref({
|
||||
|
||||
const yAxisLabels = ref(['0', '25', '50', '75', '100'])
|
||||
|
||||
// AI 추천 데이터 (Claude API 연동용 구조)
|
||||
const aiRecommendation = ref({
|
||||
emoji: '☀️',
|
||||
title: '여름 시즌 인스타그램 마케팅 계획',
|
||||
sections: {
|
||||
ideas: {
|
||||
title: '1. 기획 아이디어',
|
||||
items: [
|
||||
'여름 음료 메뉴 개발 예: 시원한 아이스 아메리카노, 프라페 등',
|
||||
'카페 내부에서 <strong>음료와 함께 촬영한 인스타그램용 사진 및 영상</strong> 제작',
|
||||
'<strong>지역 인플루언서</strong>와 협업하여 방문 후기 및 신메뉴 소개 게시물 게시',
|
||||
'<strong>인스타그램 스토리</strong>를 활용해 <strong>매일 음료 프로모션</strong> 소식 공유'
|
||||
]
|
||||
},
|
||||
costs: {
|
||||
title: '2. 예상 비용 및 기대 효과',
|
||||
items: [
|
||||
{ item: '촬영 및 편집', amount: '약 300,000원' },
|
||||
{ item: '인플루언서 협찬', amount: '약 200,000원' }
|
||||
],
|
||||
effects: [
|
||||
'고객 관심 유도 및 매출 상승',
|
||||
'SNS를 통한 브랜드 인지도 상승',
|
||||
'재방문율 및 공유 유도'
|
||||
]
|
||||
},
|
||||
warnings: {
|
||||
title: '3. 주의사항 및 유의점',
|
||||
items: [
|
||||
'인스타그램 콘텐츠는 <strong>창의적이고 시각적으로 매력적</strong>이어야 함',
|
||||
'인플루언서 협업 시, <strong>합리적인 혜택과 협의 조건</strong> 필요'
|
||||
]
|
||||
// AI 추천 데이터 (초기값 - API에서 업데이트됨)
|
||||
const aiRecommendation = ref(null)
|
||||
|
||||
// ⚠️ API 연동 함수들 수정
|
||||
|
||||
/**
|
||||
* 매장 정보 및 매출 데이터 로드 (스마트 탐지 시스템)
|
||||
*/
|
||||
const loadStoreAndSalesData = async () => {
|
||||
let salesDataLoaded = false
|
||||
let storeDataLoaded = false
|
||||
let detectionResults = null
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
console.log('🚀 [SMART] 스마트 데이터 탐지 시스템 시작')
|
||||
console.log('🔍 [INFO] 현재 환경:', {
|
||||
mode: import.meta.env.MODE,
|
||||
token: localStorage.getItem('accessToken') ? '✅ 있음' : '❌ 없음',
|
||||
currentStoreId: currentStoreId.value || '없음'
|
||||
})
|
||||
|
||||
// 🎯 1. 스마트 매출 데이터 탐지
|
||||
try {
|
||||
console.log('🔍 [SALES] 스마트 매출 데이터 탐지 시작')
|
||||
|
||||
// 진행상황 표시
|
||||
appStore.showSnackbar('실제 매출 데이터를 찾고 있습니다...', 'info')
|
||||
|
||||
const salesResult = await salesService.getSalesWithSmartDetection(currentStoreId.value)
|
||||
|
||||
if (salesResult && salesResult.success && salesResult.data) {
|
||||
console.log('🎉 [SALES] 매출 데이터 탐지 성공!', {
|
||||
method: salesResult.method,
|
||||
storeId: salesResult.foundStoreId,
|
||||
score: salesResult.quality?.score
|
||||
})
|
||||
|
||||
// 탐지 결과 저장
|
||||
detectionResults = {
|
||||
method: salesResult.method,
|
||||
storeId: salesResult.foundStoreId,
|
||||
quality: salesResult.quality,
|
||||
totalFound: salesResult.totalFound
|
||||
}
|
||||
|
||||
// 매출 데이터 적용
|
||||
updateDashboardMetrics(salesResult.data)
|
||||
updateChartData(salesResult.data)
|
||||
salesDataLoaded = true
|
||||
|
||||
// 발견된 storeId 저장
|
||||
if (salesResult.foundStoreId) {
|
||||
currentStoreId.value = salesResult.foundStoreId
|
||||
console.log(`🎯 [STORE_ID] Store ID 설정: ${salesResult.foundStoreId}`)
|
||||
}
|
||||
|
||||
// 성공 메시지 생성
|
||||
let successMessage = ''
|
||||
switch (salesResult.method) {
|
||||
case 'JWT':
|
||||
successMessage = '로그인 정보를 통해 실제 매출 데이터를 불러왔습니다!'
|
||||
break
|
||||
case 'SPECIFIED':
|
||||
successMessage = `Store ${salesResult.foundStoreId}의 실제 매출 데이터를 불러왔습니다!`
|
||||
break
|
||||
default:
|
||||
successMessage = '실제 매출 데이터를 불러왔습니다!'
|
||||
}
|
||||
|
||||
appStore.showSnackbar(successMessage + ' 🎉', 'success')
|
||||
|
||||
} else {
|
||||
console.warn('⚠️ [SALES] 매출 데이터 응답이 올바르지 않음:', salesResult)
|
||||
throw new Error('매출 데이터 형식 오류')
|
||||
}
|
||||
|
||||
} catch (salesError) {
|
||||
console.error('❌ [SALES] 매출 데이터 탐지 실패:', {
|
||||
error: salesError.message,
|
||||
response: salesError.response?.data
|
||||
})
|
||||
|
||||
// 구체적인 에러 메시지
|
||||
if (salesError.message.includes('실제 매출 데이터를 찾을 수 없습니다')) {
|
||||
appStore.showSnackbar('실제 매출 데이터를 찾을 수 없어 테스트 데이터를 표시합니다', 'warning')
|
||||
} else if (salesError.response?.status === 401) {
|
||||
appStore.showSnackbar('인증이 필요합니다. 다시 로그인해주세요.', 'error')
|
||||
} else {
|
||||
appStore.showSnackbar('매출 데이터 로드에 실패해 테스트 데이터를 표시합니다', 'warning')
|
||||
}
|
||||
|
||||
// Mock 데이터 사용
|
||||
useMockSalesData()
|
||||
salesDataLoaded = false
|
||||
}
|
||||
|
||||
// 🏪 2. 매장 정보 로드 (매출 성공 여부와 무관하게 시도)
|
||||
try {
|
||||
console.log('🏪 [STORE] 매장 정보 로드 시작')
|
||||
const storeResult = await storeService.getStore()
|
||||
|
||||
if (storeResult && storeResult.success && storeResult.data) {
|
||||
console.log('✅ [STORE] 매장 정보 로드 성공:', storeResult.data.storeName)
|
||||
storeInfo.value = storeResult.data
|
||||
|
||||
// storeId 비교 및 업데이트
|
||||
if (storeResult.data.storeId && !currentStoreId.value) {
|
||||
currentStoreId.value = storeResult.data.storeId
|
||||
console.log(`🎯 [STORE] Store ID 설정: ${storeResult.data.storeId}`)
|
||||
} else if (storeResult.data.storeId && currentStoreId.value !== storeResult.data.storeId) {
|
||||
console.log(`ℹ️ [STORE] Store ID 불일치 - 매출:${currentStoreId.value}, 매장:${storeResult.data.storeId}`)
|
||||
}
|
||||
|
||||
storeDataLoaded = true
|
||||
|
||||
} else {
|
||||
throw new Error('매장 정보 응답 형식 오류')
|
||||
}
|
||||
|
||||
} catch (storeError) {
|
||||
console.error('❌ [STORE] 매장 정보 로드 실패:', storeError.message)
|
||||
|
||||
// 매출 데이터가 성공했으면 추정 매장 정보 생성
|
||||
if (salesDataLoaded && currentStoreId.value) {
|
||||
createEstimatedStoreInfo(detectionResults)
|
||||
storeDataLoaded = true
|
||||
console.log('🔄 [STORE] 매출 기반 추정 매장 정보 생성 완료')
|
||||
} else {
|
||||
useMockStoreData()
|
||||
storeDataLoaded = false
|
||||
}
|
||||
}
|
||||
|
||||
// 🎉 3. 최종 결과 처리 및 상세 리포트
|
||||
console.log('📋 [RESULT] 데이터 로드 결과:', {
|
||||
salesDataLoaded,
|
||||
storeDataLoaded,
|
||||
currentStoreId: currentStoreId.value,
|
||||
storeName: storeInfo.value?.storeName,
|
||||
detectionMethod: detectionResults?.method
|
||||
})
|
||||
|
||||
// 성공 메시지와 상세 정보
|
||||
if (salesDataLoaded && storeDataLoaded) {
|
||||
console.log('🎉 [SUCCESS] 모든 데이터 로드 완료!')
|
||||
|
||||
// 상세 성공 리포트 생성
|
||||
generateSuccessReport(detectionResults)
|
||||
|
||||
} else if (salesDataLoaded) {
|
||||
console.log('✅ [PARTIAL] 매출 데이터만 로드 성공')
|
||||
|
||||
const message = detectionResults?.method === 'AUTO_DETECTION'
|
||||
? `자동 탐지로 Store ${currentStoreId.value}의 실제 매출을 발견했습니다!`
|
||||
: `Store ${currentStoreId.value}의 실제 매출 데이터를 불러왔습니다!`
|
||||
|
||||
appStore.showSnackbar(message, 'info')
|
||||
|
||||
} else if (storeDataLoaded) {
|
||||
console.log('⚠️ [PARTIAL] 매장 정보만 로드 성공')
|
||||
appStore.showSnackbar('매장 정보만 불러왔습니다. 매출은 테스트 데이터입니다.', 'warning')
|
||||
|
||||
} else {
|
||||
console.log('❌ [FALLBACK] 모든 실제 데이터 로드 실패')
|
||||
appStore.showSnackbar('실제 데이터를 찾을 수 없어 테스트 데이터를 표시합니다.', 'warning')
|
||||
}
|
||||
|
||||
} catch (unexpectedError) {
|
||||
console.error('🚨 [UNEXPECTED] 예상치 못한 에러:', unexpectedError)
|
||||
|
||||
// 최후의 수단
|
||||
useMockStoreData()
|
||||
useMockSalesData()
|
||||
appStore.showSnackbar('시스템 오류로 인해 테스트 데이터를 표시합니다.', 'error')
|
||||
|
||||
} finally {
|
||||
loading.value = false
|
||||
console.log('🏁 [SMART] 스마트 데이터 탐지 완료')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 매장 데이터 사용 (개발/테스트용)
|
||||
*/
|
||||
const useMockStoreData = () => {
|
||||
console.log('Mock 매장 데이터 사용')
|
||||
storeInfo.value = {
|
||||
storeId: 1,
|
||||
storeName: '테스트 카페',
|
||||
businessType: '카페',
|
||||
address: '서울시 강남구',
|
||||
phoneNumber: '02-1234-5678'
|
||||
}
|
||||
currentStoreId.value = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 대시보드 지표 업데이트 (수정)
|
||||
*/
|
||||
const updateDashboardMetrics = (salesData) => {
|
||||
try {
|
||||
// 백엔드 응답 구조에 맞게 파싱
|
||||
const todaySales = Number(salesData.todaySales) || 0
|
||||
const monthSales = Number(salesData.monthSales) || 0
|
||||
const previousDayComparison = Number(salesData.previousDayComparison) || 0
|
||||
|
||||
// 변화율 계산
|
||||
const changeRate = todaySales > 0 && previousDayComparison !== 0
|
||||
? Math.abs((previousDayComparison / todaySales) * 100).toFixed(1)
|
||||
: 0
|
||||
|
||||
// 목표 달성률 (임시로 80-120% 사이로 설정)
|
||||
const achievementRate = salesData.goalAchievementRate ||
|
||||
Math.floor(Math.random() * 40 + 80) // 80-120%
|
||||
|
||||
dashboardMetrics.value = [
|
||||
{
|
||||
title: '오늘의 매출',
|
||||
value: todaySales,
|
||||
displayValue: formatCurrency(todaySales),
|
||||
change: previousDayComparison >= 0
|
||||
? `전일 대비 +${changeRate}%`
|
||||
: `전일 대비 -${changeRate}%`,
|
||||
trend: previousDayComparison >= 0 ? 'up' : 'down',
|
||||
icon: 'mdi-cash-multiple',
|
||||
color: 'success'
|
||||
},
|
||||
currentInfo: {
|
||||
title: '현재 지역 날씨 (서울 강남구 역삼동 기준)',
|
||||
icon: 'mdi-weather-sunny',
|
||||
color: 'orange',
|
||||
items: [
|
||||
{ label: '기온', value: '30도' },
|
||||
{ label: '기상 상황', value: '무더위 지속' }
|
||||
],
|
||||
insight: '<strong>시원한 음료에 대한 수요가 매우 높을 것으로 예상</strong>'
|
||||
{
|
||||
title: '이번 달 매출',
|
||||
value: monthSales,
|
||||
displayValue: formatCurrency(monthSales),
|
||||
change: `목표 달성률 ${achievementRate}%`,
|
||||
trend: achievementRate >= 100 ? 'up' : 'down',
|
||||
icon: 'mdi-trending-up',
|
||||
color: 'primary'
|
||||
},
|
||||
{
|
||||
title: '일일 조회수',
|
||||
value: 2547, // 별도 API에서 가져와야 함
|
||||
displayValue: '2,547',
|
||||
change: '전일 대비 +23%',
|
||||
trend: 'up',
|
||||
icon: 'mdi-eye',
|
||||
color: 'warning'
|
||||
},
|
||||
]
|
||||
|
||||
// 애니메이션 시작
|
||||
startMetricsAnimation()
|
||||
} catch (error) {
|
||||
console.error('대시보드 지표 업데이트 실패:', error)
|
||||
// 에러 발생 시 기본값 사용
|
||||
useMockSalesData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 데이터 업데이트 (개선)
|
||||
*/
|
||||
const updateChartData = (salesData) => {
|
||||
try {
|
||||
// yearSales 데이터가 있으면 차트 데이터 업데이트
|
||||
if (salesData.yearSales && salesData.yearSales.length > 0) {
|
||||
// Sales 엔티티 배열을 차트 형식으로 변환
|
||||
const salesDataPoints = salesData.yearSales.slice(-7).map((sale, index) => {
|
||||
const date = new Date(sale.salesDate)
|
||||
const label = `${date.getMonth() + 1}/${date.getDate()}`
|
||||
const amount = Number(sale.salesAmount) / 10000 // 만원 단위로 변환
|
||||
|
||||
return {
|
||||
label: index === salesData.yearSales.length - 1 ? '오늘' : label,
|
||||
sales: Math.round(amount),
|
||||
target: Math.round(amount * 1.1), // 목표는 실제 매출의 110%로 설정
|
||||
date: sale.salesDate
|
||||
}
|
||||
})
|
||||
|
||||
// 계산된 속성들
|
||||
// 7일 차트 데이터 업데이트
|
||||
chartData.value['7d'] = salesDataPoints
|
||||
|
||||
console.log('차트 데이터 업데이트 완료:', salesDataPoints)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('차트 데이터 업데이트 실패:', error)
|
||||
// 실패 시 기본 차트 데이터 유지
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 추천 새로고침 (수정)
|
||||
*/
|
||||
const refreshAiRecommendation = async () => {
|
||||
console.log('AI 추천 새로고침 시작')
|
||||
aiLoading.value = true
|
||||
aiError.value = ''
|
||||
|
||||
try {
|
||||
// 매장 ID 확인
|
||||
if (!currentStoreId.value && storeInfo.value) {
|
||||
currentStoreId.value = storeInfo.value.storeId
|
||||
}
|
||||
|
||||
if (!currentStoreId.value) {
|
||||
throw new Error('매장 정보가 없습니다. 매장을 먼저 등록해주세요.')
|
||||
}
|
||||
|
||||
// AI 마케팅 팁 생성 요청
|
||||
const aiResult = await recommendService.generateMarketingTips({
|
||||
storeId: currentStoreId.value,
|
||||
includeWeather: true,
|
||||
includeTrends: true,
|
||||
maxTips: 3,
|
||||
tipType: 'general'
|
||||
})
|
||||
|
||||
if (aiResult.success) {
|
||||
// AI 추천 데이터 파싱 및 업데이트
|
||||
updateAiRecommendation(aiResult.data)
|
||||
console.log('AI 추천 생성 성공:', aiResult.data)
|
||||
appStore.showSnackbar('AI 추천이 업데이트되었습니다', 'success')
|
||||
} else {
|
||||
throw new Error(aiResult.message)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('AI 추천 생성 실패:', error)
|
||||
aiError.value = 'AI 추천을 불러오는데 실패했습니다'
|
||||
|
||||
// 개발 모드에서는 Fallback 데이터 사용
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('개발 모드: Fallback AI 추천 사용')
|
||||
useFallbackAiRecommendation()
|
||||
aiError.value = '' // 에러 메시지 제거
|
||||
} else {
|
||||
appStore.showSnackbar('AI 추천 로드에 실패했습니다', 'error')
|
||||
}
|
||||
} finally {
|
||||
aiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 추천 데이터 업데이트
|
||||
*/
|
||||
const updateAiRecommendation = (aiData) => {
|
||||
try {
|
||||
// 백엔드 응답 구조에 맞게 파싱
|
||||
aiRecommendation.value = {
|
||||
emoji: '🤖',
|
||||
title: aiData.tipContent ? aiData.tipContent.substring(0, 50) + '...' : 'AI 마케팅 추천',
|
||||
sections: {
|
||||
ideas: {
|
||||
title: '1. 추천 아이디어',
|
||||
items: [aiData.tipContent || '맞춤형 마케팅 전략을 제안드립니다.']
|
||||
},
|
||||
costs: {
|
||||
title: '2. 예상 효과',
|
||||
items: ['고객 관심 유도 및 매출 상승', 'SNS를 통한 브랜드 인지도 상승'],
|
||||
effects: ['재방문율 및 공유 유도', '지역 내 인지도 향상']
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AI 추천 데이터 파싱 실패:', error)
|
||||
useFallbackAiRecommendation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fallback AI 추천 사용
|
||||
*/
|
||||
const useFallbackAiRecommendation = () => {
|
||||
console.log('Fallback AI 추천 사용')
|
||||
aiRecommendation.value = {
|
||||
emoji: '☀️',
|
||||
title: '여름 시즌 마케팅 전략',
|
||||
sections: {
|
||||
ideas: {
|
||||
title: '1. 기본 추천사항',
|
||||
items: [
|
||||
'계절 메뉴 개발 및 프로모션',
|
||||
'SNS 마케팅 활용',
|
||||
'지역 고객 대상 이벤트 기획'
|
||||
]
|
||||
},
|
||||
costs: {
|
||||
title: '2. 기대 효과',
|
||||
items: ['매출 향상', '고객 만족도 증가'],
|
||||
effects: ['브랜드 인지도 상승', '재방문 고객 증가']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 계산된 속성들 (기존과 동일)
|
||||
const currentChartData = computed(() => chartData.value[chartPeriod.value])
|
||||
|
||||
const chartDataPoints = computed(() => {
|
||||
@ -600,7 +960,7 @@ const achievementRate = computed(() => {
|
||||
return Math.round((totalSales / totalTarget) * 100)
|
||||
})
|
||||
|
||||
// 메서드들
|
||||
// 기존 메서드들 (수정 없음)
|
||||
const getCurrentPeriodLabel = () => {
|
||||
switch (chartPeriod.value) {
|
||||
case '7d': return '7일'
|
||||
@ -735,31 +1095,6 @@ const hideTooltip = () => {
|
||||
tooltip.value.show = false
|
||||
}
|
||||
|
||||
// AI 추천 관련 메서드들
|
||||
const refreshAiRecommendation = async () => {
|
||||
console.log('AI 추천 새로고침')
|
||||
aiLoading.value = true
|
||||
aiError.value = ''
|
||||
|
||||
try {
|
||||
// Claude API 호출 시뮬레이션
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// 실제 Claude API 호출은 여기서
|
||||
// const response = await callClaudeAPI(prompt)
|
||||
// aiRecommendation.value = parseClaudeResponse(response)
|
||||
|
||||
console.log('AI 추천 새로고침 완료')
|
||||
appStore.showSnackbar('AI 추천이 업데이트되었습니다', 'success')
|
||||
} catch (error) {
|
||||
console.error('AI 추천 로드 실패:', error)
|
||||
aiError.value = 'AI 추천을 불러오는데 실패했습니다'
|
||||
appStore.showSnackbar('AI 추천 로드에 실패했습니다', 'error')
|
||||
} finally {
|
||||
aiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const copyRecommendation = async () => {
|
||||
try {
|
||||
let text = `${aiRecommendation.value.emoji} ${aiRecommendation.value.title}\n\n`
|
||||
@ -816,27 +1151,23 @@ const confirmLogout = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 라이프사이클
|
||||
// ⚠️ onMounted 수정 - 함수명 변경
|
||||
onMounted(async () => {
|
||||
console.log('DashboardView 마운트됨')
|
||||
|
||||
// 실제 API 호출 추가
|
||||
try {
|
||||
// 매장 정보 로드
|
||||
if (!storeStore.hasStoreInfo) {
|
||||
await storeStore.fetchStoreInfo()
|
||||
// 현재 시간 업데이트
|
||||
const updateCurrentTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('ko-KR')
|
||||
}
|
||||
updateCurrentTime()
|
||||
setInterval(updateCurrentTime, 60000) // 1분마다 업데이트
|
||||
|
||||
// 매출 데이터 로드
|
||||
await storeStore.fetchSalesData()
|
||||
// 매장 정보 및 매출 데이터 로드
|
||||
await loadStoreAndSalesData() // ← 함수명 변경
|
||||
|
||||
// 진행 중인 콘텐츠 로드
|
||||
await contentStore.fetchOngoingContents()
|
||||
|
||||
} catch (error) {
|
||||
console.warn('대시보드 데이터 로드 실패 (개발 중이므로 무시):', error)
|
||||
// 개발 중에는 에러를 무시하고 계속 진행
|
||||
}
|
||||
// 차트 그리기
|
||||
await nextTick()
|
||||
drawChart()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@ -845,7 +1176,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 기존 스타일들 유지 */
|
||||
/* 기존 스타일들 모두 유지 - 변경 없음 */
|
||||
.metric-card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user