This commit is contained in:
SeoJHeasdw 2025-06-16 16:33:37 +09:00
parent de983127a3
commit 0ff2f15b65
8 changed files with 1280 additions and 485 deletions

View File

@ -1,16 +1,19 @@
//* public/runtime-env.js - 디버깅 포함 버전 //* public/runtime-env.js - 수정버전
console.log('=== RUNTIME-ENV.JS 로드됨 ==='); console.log('=== RUNTIME-ENV.JS 로드됨 ===');
window.__runtime_config__ = { window.__runtime_config__ = {
// 로컬 개발 환경 설정 // 기존 설정들...
AUTH_URL: 'http://localhost:8081/api/auth', AUTH_URL: 'http://localhost:8081/api/auth',
MEMBER_URL: 'http://localhost:8081/api/member', MEMBER_URL: 'http://localhost:8081/api/member',
STORE_URL: 'http://localhost:8082/api/store', STORE_URL: 'http://localhost:8082/api/store',
SALES_URL: 'http://localhost:8082/api/sales', // ← 이 줄 추가
CONTENT_URL: 'http://localhost:8083/api/content', CONTENT_URL: 'http://localhost:8083/api/content',
RECOMMEND_URL: 'http://localhost:8084/api/recommendation', RECOMMEND_URL: 'http://localhost:8084/api/recommendation',
// Gateway 주석 처리 (로컬에서는 사용 안함) // 프로덕션 환경 (주석 처리)
// GATEWAY_URL: 'http://20.1.2.3', // 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: { FEATURES: {
@ -31,5 +34,7 @@ window.__runtime_config__ = {
console.log('=== 설정된 API URLs ==='); console.log('=== 설정된 API URLs ===');
console.log('AUTH_URL:', window.__runtime_config__.AUTH_URL); 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__); console.log('전체 설정:', window.__runtime_config__);

View File

@ -1,18 +1,24 @@
//* src/services/api.js //* src/services/api.js - 수정버전
import axios from 'axios' import axios from 'axios'
// 런타임 환경 설정에서 API URL 가져오기 // 런타임 환경 설정에서 API URL 가져오기
const getApiUrls = () => { const getApiUrls = () => {
const config = window.__runtime_config__ || {} const config = window.__runtime_config__ || {}
return { 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', 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 토큰 자동 추가 // 요청 인터셉터 - JWT 토큰 자동 추가
instance.interceptors.request.use( instance.interceptors.request.use(
(config) => { (config) => {
const token = localStorage.getItem('accessToken') // accessToken 또는 token 둘 다 확인
const token = localStorage.getItem('accessToken') || localStorage.getItem('token')
if (token) { if (token) {
config.headers.Authorization = `Bearer ${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 return config
}, },
(error) => { (error) => {
@ -44,11 +60,20 @@ instance.interceptors.request.use(
// 응답 인터셉터 - 토큰 갱신 및 에러 처리 // 응답 인터셉터 - 토큰 갱신 및 에러 처리
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => { (response) => {
// 성공 응답 로깅 (개발 모드에서만)
if (import.meta.env.DEV) {
console.log(`API 응답: ${response.status} ${response.config.url}`)
}
return response return response
}, },
async (error) => { async (error) => {
const originalRequest = error.config const originalRequest = error.config
// 개발 모드에서 에러 로깅
if (import.meta.env.DEV) {
console.error(`API 에러: ${error.response?.status} ${error.config?.url}`, error.response?.data)
}
// 401 에러이고 토큰 갱신을 시도하지 않은 경우 // 401 에러이고 토큰 갱신을 시도하지 않은 경우
if (error.response?.status === 401 && !originalRequest._retry) { if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true originalRequest._retry = true
@ -62,6 +87,7 @@ instance.interceptors.request.use(
const { accessToken, refreshToken: newRefreshToken } = refreshResponse.data.data const { accessToken, refreshToken: newRefreshToken } = refreshResponse.data.data
localStorage.setItem('accessToken', accessToken) localStorage.setItem('accessToken', accessToken)
localStorage.setItem('token', accessToken) // 호환성을 위해 둘 다 저장
localStorage.setItem('refreshToken', newRefreshToken) localStorage.setItem('refreshToken', newRefreshToken)
// 원래 요청에 새 토큰으로 재시도 // 원래 요청에 새 토큰으로 재시도
@ -71,6 +97,7 @@ instance.interceptors.request.use(
} catch (refreshError) { } catch (refreshError) {
// 토큰 갱신 실패 시 로그아웃 처리 // 토큰 갱신 실패 시 로그아웃 처리
localStorage.removeItem('accessToken') localStorage.removeItem('accessToken')
localStorage.removeItem('token')
localStorage.removeItem('refreshToken') localStorage.removeItem('refreshToken')
localStorage.removeItem('userInfo') localStorage.removeItem('userInfo')
window.location.href = '/login' window.location.href = '/login'
@ -86,6 +113,15 @@ instance.interceptors.request.use(
// API 인스턴스들 생성 // API 인스턴스들 생성
const apiUrls = getApiUrls() 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 memberApi = createApiInstance(apiUrls.MEMBER_URL)
export const authApi = createApiInstance(apiUrls.AUTH_URL) export const authApi = createApiInstance(apiUrls.AUTH_URL)
export const storeApi = createApiInstance(apiUrls.STORE_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 menuApi = createApiInstance(apiUrls.MENU_URL)
export const salesApi = createApiInstance(apiUrls.SALES_URL) export const salesApi = createApiInstance(apiUrls.SALES_URL)
export const recommendApi = createApiInstance(apiUrls.RECOMMEND_URL) export const recommendApi = createApiInstance(apiUrls.RECOMMEND_URL)
export const imagesApi = createApiInstance(apiUrls.IMAGES_URL)
// 기본 API 인스턴스 (Gateway URL 사용) // 기본 API 인스턴스 (Gateway URL 사용)
export const api = createApiInstance(apiUrls.GATEWAY_URL) export const api = createApiInstance(apiUrls.GATEWAY_URL)
// 공통 에러 핸들러 // 공통 에러 핸들러 (기존과 동일)
export const handleApiError = (error) => { export const handleApiError = (error) => {
const response = error.response const response = error.response
@ -160,4 +197,3 @@ export const formatSuccessResponse = (data, message = '요청이 성공적으로
data data
} }
} }

View File

@ -1,4 +1,4 @@
//* src/services/recommend.js //* src/services/recommend.js - 수정버전
import { recommendApi, handleApiError, formatSuccessResponse } from './api.js' import { recommendApi, handleApiError, formatSuccessResponse } from './api.js'
/** /**
@ -8,135 +8,101 @@ import { recommendApi, handleApiError, formatSuccessResponse } from './api.js'
class RecommendService { class RecommendService {
/** /**
* AI 마케팅 생성 (REC-005: AI 마케팅 방법 추천) * AI 마케팅 생성 (REC-005: AI 마케팅 방법 추천)
* 수정: 백엔드 API 스펙에 맞게 요청 구조 변경
* @param {Object} requestData - 마케팅 요청 정보 * @param {Object} requestData - 마케팅 요청 정보
* @returns {Promise<Object>} 생성된 마케팅 * @returns {Promise<Object>} 생성된 마케팅
*/ */
async generateMarketingTips(requestData = {}) { async generateMarketingTips(requestData = {}) {
try { try {
const response = await recommendApi.post('/marketing-tips', { // 백엔드 MarketingTipRequest DTO에 맞는 구조로 변경
const requestBody = {
storeId: requestData.storeId, storeId: requestData.storeId,
includeWeather: requestData.includeWeather !== false, // 기본값 true // 필요시 추가 필드들
includeTrends: requestData.includeTrends !== false, // 기본값 true additionalRequirement: requestData.additionalRequirement || '',
maxTips: requestData.maxTips || 3, }
tipType: requestData.tipType || 'general', // general, menu, marketing, operation
}) console.log('AI 마케팅 팁 생성 요청:', requestBody)
const response = await recommendApi.post('/marketing-tips', requestBody)
return formatSuccessResponse(response.data.data, 'AI 마케팅 팁이 생성되었습니다.') return formatSuccessResponse(response.data.data, 'AI 마케팅 팁이 생성되었습니다.')
} catch (error) { } catch (error) {
console.error('AI 마케팅 팁 생성 실패:', error)
return handleApiError(error) return handleApiError(error)
} }
} }
/** /**
* 날씨 기반 메뉴 추천 * 마케팅 이력 조회
* @param {number} storeId - 매장 ID * @param {number} storeId - 매장 ID
* @returns {Promise<Object>} 날씨 기반 메뉴 추천 * @param {Object} pagination - 페이지네이션 정보
* @returns {Promise<Object>} 마케팅 이력
*/ */
async getWeatherBasedMenuRecommendation(storeId) { async getMarketingTipHistory(storeId, pagination = {}) {
try { 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) { } catch (error) {
return handleApiError(error) return handleApiError(error)
} }
} }
/** /**
* 매출 예측 추천 * 마케팅 상세 조회
* @param {number} storeId - 매장 ID * @param {number} tipId - ID
* @param {string} period - 예측 기간 (day, week, month) * @returns {Promise<Object>} 마케팅 상세 정보
* @returns {Promise<Object>} 매출 예측 정보
*/ */
async getSalesPrediction(storeId, period = 'day') { async getMarketingTip(tipId) {
try { 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) { } catch (error) {
return handleApiError(error) return handleApiError(error)
} }
} }
/** /**
* 인기 메뉴 추천 * 종합 AI 추천 (대시보드용)
* @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 추천 (매출 예측 + 메뉴 추천 + 마케팅 전략)
* @param {number} storeId - 매장 ID * @param {number} storeId - 매장 ID
* @returns {Promise<Object>} 통합 AI 추천 정보 * @returns {Promise<Object>} 통합 AI 추천 정보
*/ */
async getComprehensiveRecommendation(storeId) { async getComprehensiveRecommendation(storeId) {
try { 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) { } catch (error) {
return handleApiError(error) return handleApiError(error)
} }
} }
/** /**
* 추천 기록 조회 * 추천 피드백 제공 (향후 구현)
* @param {Object} filters - 필터링 옵션 * @param {number} tipId - 추천 ID
* @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 {Object} feedback - 피드백 정보 * @param {Object} feedback - 피드백 정보
* @returns {Promise<Object>} 피드백 제공 결과 * @returns {Promise<Object>} 피드백 제공 결과
*/ */
async provideFeedback(recommendationId, feedback) { async provideFeedback(tipId, feedback) {
try { try {
const response = await recommendApi.post(`/feedback/${recommendationId}`, { const response = await recommendApi.post(`/marketing-tips/${tipId}/feedback`, {
rating: feedback.rating, // 1-5 점수 rating: feedback.rating, // 1-5 점수
useful: feedback.useful, // true/false useful: feedback.useful, // true/false
comment: feedback.comment || '', comment: feedback.comment || '',
@ -148,6 +114,39 @@ class RecommendService {
return handleApiError(error) 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() export const recommendService = new RecommendService()

325
src/services/sales.js Normal file
View 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

View File

@ -1,9 +1,9 @@
//* src/services/store.js - 기존 파일 수정 (API 설계서 기준) //* src/services/store.js - 매출 API 수정버전
import { storeApi, menuApi, salesApi, handleApiError, formatSuccessResponse } from './api.js' import { storeApi, salesApi, handleApiError, formatSuccessResponse } from './api.js'
/** /**
* 매장 관련 API 서비스 * 매장 관련 API 서비스
* API 설계서 기준으로 수정됨 * 유저스토리: STR-005, STR-010, STR-015, STR-020, STR-025, STR-030, STR-035, STR-040
*/ */
class StoreService { class StoreService {
/** /**
@ -15,14 +15,17 @@ class StoreService {
try { try {
const response = await storeApi.post('/register', { const response = await storeApi.post('/register', {
storeName: storeData.storeName, storeName: storeData.storeName,
storeImage: storeData.storeImage,
businessType: storeData.businessType, businessType: storeData.businessType,
address: storeData.address, address: storeData.address,
phoneNumber: storeData.phoneNumber, phoneNumber: storeData.phoneNumber,
businessHours: storeData.businessHours || storeData.operatingHours, businessNumber: storeData.businessNumber,
instaAccounts: storeData.instaAccounts,
blogAccounts: storeData.blogAccounts,
businessHours: storeData.businessHours,
closedDays: storeData.closedDays, closedDays: storeData.closedDays,
seatCount: storeData.seatCount, seatCount: storeData.seatCount,
snsAccounts: storeData.snsAccounts || `인스타그램: ${storeData.instaAccount || ''}, 네이버블로그: ${storeData.naverBlogAccount || ''}`, description: storeData.description,
description: storeData.description || ''
}) })
return formatSuccessResponse(response.data.data, '매장이 등록되었습니다.') return formatSuccessResponse(response.data.data, '매장이 등록되었습니다.')
@ -32,7 +35,7 @@ class StoreService {
} }
/** /**
* 매장 정보 조회 (STR-005: 매장 조회) * 매장 정보 조회 (STR-005: 매장 정보 관리)
* @returns {Promise<Object>} 매장 정보 * @returns {Promise<Object>} 매장 정보
*/ */
async getStore() { async getStore() {
@ -52,17 +55,7 @@ class StoreService {
*/ */
async updateStore(storeData) { async updateStore(storeData) {
try { try {
const response = await storeApi.put('/', { const response = await storeApi.put('/', storeData)
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 || ''
})
return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.') return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.')
} catch (error) { } catch (error) {
@ -71,35 +64,48 @@ class StoreService {
} }
/** /**
* 매출 정보 조회 (SAL-005: 매출 조회) * 매출 정보 조회 (STR-020: 대시보드)
* 수정: salesApi 사용하고 storeId 매개변수 추가
* @param {number} storeId - 매장 ID * @param {number} storeId - 매장 ID
* @returns {Promise<Object>} 매출 정보 * @returns {Promise<Object>} 매출 정보
*/ */
async getSales(storeId) { async getSales(storeId) {
try { 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}`) const response = await salesApi.get(`/${storeId}`)
return formatSuccessResponse(response.data.data, '매출 정보를 조회했습니다.') return formatSuccessResponse(response.data.data, '매출 정보를 조회했습니다.')
} catch (error) { } catch (error) {
console.error('매출 정보 조회 실패:', error)
return handleApiError(error) return handleApiError(error)
} }
} }
/** /**
* 메뉴 등록 (MNU-010: 메뉴 등록) * 메뉴 등록 (STR-030: 메뉴 등록)
* 수정: 올바른 API 경로 사용
* @param {Object} menuData - 메뉴 정보 * @param {Object} menuData - 메뉴 정보
* @returns {Promise<Object>} 메뉴 등록 결과 * @returns {Promise<Object>} 메뉴 등록 결과
*/ */
async registerMenu(menuData) { async registerMenu(menuData) {
try { try {
const response = await menuApi.post('/register', { // Store 서비스의 Menu API 사용
const response = await storeApi.post('/menu/register', {
storeId: menuData.storeId,
menuName: menuData.menuName, menuName: menuData.menuName,
menuCategory: menuData.menuCategory || menuData.category, category: menuData.category,
menuImage: menuData.menuImage || menuData.image,
price: menuData.price, price: menuData.price,
description: menuData.description, description: menuData.description,
isPopular: menuData.isPopular || false,
isRecommended: menuData.isRecommended || false,
}) })
return formatSuccessResponse(response.data.data, '메뉴가 등록되었습니다.') return formatSuccessResponse(response.data.data, '메뉴가 등록되었습니다.')
@ -109,13 +115,13 @@ class StoreService {
} }
/** /**
* 메뉴 목록 조회 (MNU-005: 메뉴 조회) * 메뉴 목록 조회 (STR-025: 메뉴 조회)
* @param {number} storeId - 매장 ID * @param {number} storeId - 매장 ID
* @returns {Promise<Object>} 메뉴 목록 * @returns {Promise<Object>} 메뉴 목록
*/ */
async getMenus(storeId) { async getMenus(storeId) {
try { try {
const response = await menuApi.get(`/${storeId}`) const response = await storeApi.get(`/menu?storeId=${storeId}`)
return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.') return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.')
} catch (error) { } catch (error) {
@ -124,22 +130,14 @@ class StoreService {
} }
/** /**
* 메뉴 수정 (MNU-015: 메뉴 수정) * 메뉴 수정 (STR-035: 메뉴 수정)
* @param {number} menuId - 메뉴 ID * @param {number} menuId - 메뉴 ID
* @param {Object} menuData - 수정할 메뉴 정보 * @param {Object} menuData - 수정할 메뉴 정보
* @returns {Promise<Object>} 메뉴 수정 결과 * @returns {Promise<Object>} 메뉴 수정 결과
*/ */
async updateMenu(menuId, menuData) { async updateMenu(menuId, menuData) {
try { try {
const response = await menuApi.put(`/${menuId}`, { const response = await storeApi.put(`/menu/${menuId}`, menuData)
menuName: menuData.menuName,
menuCategory: menuData.menuCategory || menuData.category,
menuImage: menuData.menuImage || menuData.image,
price: menuData.price,
description: menuData.description,
isPopular: menuData.isPopular || false,
isRecommended: menuData.isRecommended || false,
})
return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.') return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.')
} catch (error) { } catch (error) {
@ -148,13 +146,13 @@ class StoreService {
} }
/** /**
* 메뉴 삭제 (MNU-020: 메뉴 삭제) * 메뉴 삭제 (STR-040: 메뉴 삭제)
* @param {number} menuId - 메뉴 ID * @param {number} menuId - 메뉴 ID
* @returns {Promise<Object>} 메뉴 삭제 결과 * @returns {Promise<Object>} 메뉴 삭제 결과
*/ */
async deleteMenu(menuId) { async deleteMenu(menuId) {
try { try {
await menuApi.delete(`/${menuId}`) await storeApi.delete(`/menu/${menuId}`)
return formatSuccessResponse(null, '메뉴가 삭제되었습니다.') return formatSuccessResponse(null, '메뉴가 삭제되었습니다.')
} catch (error) { } catch (error) {
@ -163,16 +161,14 @@ class StoreService {
} }
/** /**
* 다중 메뉴 삭제 * 매장 통계 정보 조회
* @param {number[]} menuIds - 삭제할 메뉴 ID 배열 * @returns {Promise<Object>} 매장 통계
* @returns {Promise<Object>} 삭제 결과
*/ */
async deleteMenus(menuIds) { async getStoreStatistics() {
try { try {
const deletePromises = menuIds.map((menuId) => this.deleteMenu(menuId)) const response = await storeApi.get('/statistics')
await Promise.all(deletePromises)
return formatSuccessResponse(null, `${menuIds.length}개의 메뉴가 삭제되었습니다.`) return formatSuccessResponse(response.data.data, '매장 통계를 조회했습니다.')
} catch (error) { } catch (error) {
return handleApiError(error) return handleApiError(error)
} }

View File

@ -1,82 +1,217 @@
//* src/store/auth.js 수정 - 기존 구조 유지하고 API 연동만 추가 //* src/store/auth.js - 토큰 관리 수정버전
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue'
import authService from '@/services/auth'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', {
// 기존 상태들 유지 state: () => ({
const user = ref(null) user: null,
const token = ref(null) token: null,
const isAuthenticated = ref(false) refreshToken: null,
const isLoading = ref(false) isAuthenticated: false,
}),
// 기존 checkAuthState 메서드 유지 getters: {
const checkAuthState = () => { isLoggedIn: (state) => !!state.token && !!state.user,
const storedToken = localStorage.getItem('accessToken') userInfo: (state) => state.user,
const storedUser = localStorage.getItem('userInfo') hasValidToken: (state) => {
if (!state.token) return false
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 { 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) { actions: {
token.value = result.data.token /**
user.value = result.data.user * 로그인 처리
isAuthenticated.value = true */
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 { } else {
return { success: false, error: result.message } throw new Error(response.message)
} }
} catch (error) { } catch (error) {
return { success: false, error: '네트워크 오류가 발생했습니다.' } this.clearAuth()
} finally { throw error
isLoading.value = false
}
} }
},
// logout 메서드를 실제 API 호출로 수정 /**
const logout = async () => { * 회원가입 처리
isLoading.value = true */
async register(userData) {
try { 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() await authService.logout()
}
} catch (error) { } catch (error) {
console.warn('로그아웃 API 호출 실패:', error) console.error('로그아웃 API 오류:', error)
} finally { } finally {
// 상태 초기화 this.clearAuth()
token.value = null
user.value = null
isAuthenticated.value = false
isLoading.value = false
} }
},
/**
* 사용자 정보 새로고침
*/
async refreshUserInfo() {
try {
if (!this.token) {
throw new Error('토큰이 없습니다')
} }
// 초기화 시 인증 상태 확인 const { authService } = await import('@/services/auth')
checkAuthState() const response = await authService.getUserInfo()
return { if (response.success) {
user, this.user = response.data
token, this.isAuthenticated = true
isAuthenticated, return response
isLoading, } else {
login, throw new Error(response.message)
logout, }
checkAuthState } 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
}
}
} }
}) })

View File

@ -1,202 +1,170 @@
//* src/store/store.js 수정 - 기존 구조 유지하고 API 연동만 추가 //* src/services/store.js - 수정버전
import { defineStore } from 'pinia' import { storeApi, handleApiError, formatSuccessResponse } from './api.js'
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
/**
* 매장 관련 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 { try {
const result = await storeService.getStore() 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,
})
if (result.success) { return formatSuccessResponse(response.data.data, '매장이 등록되었습니다.')
storeInfo.value = result.data
return { success: true }
} else {
console.warn('매장 정보 조회 실패:', result.message)
return { success: false, error: result.message }
}
} catch (error) { } catch (error) {
console.warn('매장 정보 조회 실패:', error) return handleApiError(error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
isLoading.value = false
} }
} }
// saveStoreInfo를 실제 API 호출로 수정 /**
const saveStoreInfo = async (storeData) => { * 매장 정보 조회 (현재 로그인 사용자) - STR-005: 매장 정보 관리
isLoading.value = true * @returns {Promise<Object>} 매장 정보
*/
async getStore() {
try { try {
let result const response = await storeApi.get('/')
if (storeInfo.value) { return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.')
// 기존 매장 정보 수정
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) { } catch (error) {
return { success: false, error: '네트워크 오류가 발생했습니다.' } return handleApiError(error)
} finally {
isLoading.value = false
} }
} }
// fetchMenus를 실제 API 호출로 수정 /**
const fetchMenus = async () => { * 매장 정보 수정 (STR-010: 매장 수정)
if (!storeInfo.value?.storeId) { * @param {Object} storeData - 수정할 매장 정보 (storeId 불필요 - JWT에서 추출)
console.warn('매장 ID가 없어 메뉴를 조회할 수 없습니다.') * @returns {Promise<Object>} 매장 수정 결과
return { success: false, error: '매장 정보가 필요합니다.' } */
} async updateStore(storeData) {
isLoading.value = true
try { try {
const result = await storeService.getMenus(storeInfo.value.storeId) const response = await storeApi.put('/', storeData)
return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.')
if (result.success) {
menus.value = result.data
return { success: true }
} else {
return { success: false, error: result.message }
}
} catch (error) { } catch (error) {
return { success: false, error: '네트워크 오류가 발생했습니다.' } return handleApiError(error)
} finally {
isLoading.value = false
} }
} }
// 메뉴 관련 메서드들 API 연동 추가 /**
const saveMenu = async (menuData) => { * 메뉴 등록 (STR-030: 메뉴 등록)
isLoading.value = true * @param {Object} menuData - 메뉴 정보
* @returns {Promise<Object>} 메뉴 등록 결과
*/
async registerMenu(menuData) {
try { try {
const result = await storeService.registerMenu(menuData) 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,
})
if (result.success) { return formatSuccessResponse(response.data.data, '메뉴가 등록되었습니다.')
// 메뉴 목록 새로고침
await fetchMenus()
return { success: true, message: '메뉴가 등록되었습니다.' }
} else {
return { success: false, error: result.message }
}
} catch (error) { } catch (error) {
return { success: false, error: '네트워크 오류가 발생했습니다.' } return handleApiError(error)
} finally {
isLoading.value = false
} }
} }
const updateMenu = async (menuId, menuData) => { /**
isLoading.value = true * 메뉴 목록 조회 (STR-025: 메뉴 조회)
* @param {Object} filters - 필터링 옵션
* @returns {Promise<Object>} 메뉴 목록
*/
async getMenus(filters = {}) {
try { try {
const result = await storeService.updateMenu(menuId, menuData) const queryParams = new URLSearchParams()
if (result.success) { if (filters.category) queryParams.append('category', filters.category)
// 메뉴 목록 새로고침 if (filters.sortBy) queryParams.append('sortBy', filters.sortBy)
await fetchMenus() if (filters.isPopular !== undefined) queryParams.append('isPopular', filters.isPopular)
return { success: true, message: '메뉴가 수정되었습니다.' }
} else { const response = await storeApi.get(`/menu?${queryParams.toString()}`)
return { success: false, error: result.message }
} return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.')
} catch (error) { } catch (error) {
return { success: false, error: '네트워크 오류가 발생했습니다.' } return handleApiError(error)
} finally {
isLoading.value = false
} }
} }
const deleteMenu = async (menuId) => { /**
isLoading.value = true * 메뉴 수정 (STR-035: 메뉴 수정)
* @param {number} menuId - 메뉴 ID
* @param {Object} menuData - 수정할 메뉴 정보
* @returns {Promise<Object>} 메뉴 수정 결과
*/
async updateMenu(menuId, menuData) {
try { try {
const result = await storeService.deleteMenu(menuId) const response = await storeApi.put(`/menu/${menuId}`, menuData)
if (result.success) { return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.')
// 메뉴 목록 새로고침
await fetchMenus()
return { success: true, message: '메뉴가 삭제되었습니다.' }
} else {
return { success: false, error: result.message }
}
} catch (error) { } catch (error) {
return { success: false, error: '네트워크 오류가 발생했습니다.' } return handleApiError(error)
} finally {
isLoading.value = false
} }
} }
// 매출 정보 조회 추가 /**
const fetchSalesData = async () => { * 메뉴 삭제 (STR-040: 메뉴 삭제)
if (!storeInfo.value?.storeId) { * @param {number} menuId - 메뉴 ID
return { success: false, error: '매장 정보가 필요합니다.' } * @returns {Promise<Object>} 메뉴 삭제 결과
} */
async deleteMenu(menuId) {
isLoading.value = true
try { try {
const result = await storeService.getSales(storeInfo.value.storeId) await storeApi.delete(`/menu/${menuId}`)
if (result.success) { return formatSuccessResponse(null, '메뉴가 삭제되었습니다.')
salesData.value = result.data
return { success: true }
} else {
return { success: false, error: result.message }
}
} catch (error) { } catch (error) {
return { success: false, error: '네트워크 오류가 발생했습니다.' } return handleApiError(error)
} finally {
isLoading.value = false
} }
} }
return { /**
// 상태 * 다중 메뉴 삭제
storeInfo, * @param {number[]} menuIds - 삭제할 메뉴 ID 배열
menus, * @returns {Promise<Object>} 삭제 결과
salesData, */
isLoading, async deleteMenus(menuIds) {
try {
const deletePromises = menuIds.map((menuId) => this.deleteMenu(menuId))
await Promise.all(deletePromises)
// 컴퓨티드 return formatSuccessResponse(null, `${menuIds.length}개의 메뉴가 삭제되었습니다.`)
hasStoreInfo, } catch (error) {
menuCount, return handleApiError(error)
}
// 메서드
fetchStoreInfo,
saveStoreInfo,
fetchMenus,
saveMenu,
updateMenu,
deleteMenu,
fetchSalesData
} }
})
/**
* 매장 통계 정보 조회
* @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

View File

@ -1,4 +1,4 @@
//* src/views/DashboardView.vue //* src/views/DashboardView.vue -
<template> <template>
<div> <div>
<!-- 메인 컨텐츠 --> <!-- 메인 컨텐츠 -->
@ -421,10 +421,15 @@ import { useAuthStore } from '@/store/auth'
import { useAppStore } from '@/store/app' import { useAppStore } from '@/store/app'
import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/formatters' 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 사용 * 대시보드 메인 페이지 - App.vue의 단일 AppBar 사용
* - AI 추천을 단일 상세 콘텐츠로 변경 * - AI 추천을 단일 상세 콘텐츠로 변경
* - Claude API 연동 준비된 구조 * - 실제 API 연동 적용 (매장/매출 분리)
*/ */
const router = useRouter() const router = useRouter()
@ -441,6 +446,10 @@ const chartCanvas = ref(null)
const currentTime = ref('') const currentTime = ref('')
const aiError = ref('') const aiError = ref('')
//
const storeInfo = ref(null)
const currentStoreId = ref(null)
// //
const tooltip = ref({ const tooltip = ref({
show: false, show: false,
@ -451,22 +460,22 @@ const tooltip = ref({
target: 0 target: 0
}) })
// // ( - API )
const dashboardMetrics = ref([ const dashboardMetrics = ref([
{ {
title: '오늘의 매출', title: '오늘의 매출',
value: 567000, value: 0,
displayValue: '₩567,000', displayValue: '₩0',
change: '전일 대비 +15%', change: '로딩 중...',
trend: 'up', trend: 'up',
icon: 'mdi-cash-multiple', icon: 'mdi-cash-multiple',
color: 'success' color: 'success'
}, },
{ {
title: '이번 달 매출', title: '이번 달 매출',
value: 12450000, value: 0,
displayValue: '₩12,450,000', displayValue: '₩0',
change: '전월 대비 +8%', change: '로딩 중...',
trend: 'up', trend: 'up',
icon: 'mdi-trending-up', icon: 'mdi-trending-up',
color: 'primary' color: 'primary'
@ -482,7 +491,7 @@ const dashboardMetrics = ref([
}, },
]) ])
// // ( - API )
const chartData = ref({ const chartData = ref({
'7d': [ '7d': [
{ label: '6일전', sales: 45, target: 50, date: '06-04' }, { label: '6일전', sales: 45, target: 50, date: '06-04' },
@ -510,53 +519,404 @@ const chartData = ref({
const yAxisLabels = ref(['0', '25', '50', '75', '100']) const yAxisLabels = ref(['0', '25', '50', '75', '100'])
// AI (Claude API ) // AI ( - API )
const aiRecommendation = ref({ const aiRecommendation = ref(null)
emoji: '☀️',
title: '여름 시즌 인스타그램 마케팅 계획', // 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'
},
{
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: { sections: {
ideas: { ideas: {
title: '1. 기획 아이디어', 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: [ items: [
'여름 음료 메뉴 개발 예: 시원한 아이스 아메리카노, 프라페 등', '계절 메뉴 개발 및 프로모션',
'카페 내부에서 <strong>음료와 함께 촬영한 인스타그램용 사진 및 영상</strong> 제작', 'SNS 마케팅 활용',
'<strong>지역 인플루언서</strong>와 협업하여 방문 후기 및 신메뉴 소개 게시물 게시', '지역 고객 대상 이벤트 기획'
'<strong>인스타그램 스토리</strong>를 활용해 <strong>매일 음료 프로모션</strong> 소식 공유'
] ]
}, },
costs: { costs: {
title: '2. 예상 비용 및 기대 효과', title: '2. 기대 효과',
items: [ items: ['매출 향상', '고객 만족도 증가'],
{ item: '촬영 및 편집', amount: '약 300,000원' }, effects: ['브랜드 인지도 상승', '재방문 고객 증가']
{ item: '인플루언서 협찬', amount: '약 200,000원' }
],
effects: [
'고객 관심 유도 및 매출 상승',
'SNS를 통한 브랜드 인지도 상승',
'재방문율 및 공유 유도'
]
},
warnings: {
title: '3. 주의사항 및 유의점',
items: [
'인스타그램 콘텐츠는 <strong>창의적이고 시각적으로 매력적</strong>이어야 함',
'인플루언서 협업 시, <strong>합리적인 혜택과 협의 조건</strong> 필요'
]
} }
},
currentInfo: {
title: '현재 지역 날씨 (서울 강남구 역삼동 기준)',
icon: 'mdi-weather-sunny',
color: 'orange',
items: [
{ label: '기온', value: '30도' },
{ label: '기상 상황', value: '무더위 지속' }
],
insight: '<strong>시원한 음료에 대한 수요가 매우 높을 것으로 예상</strong>'
} }
}) }
}
// // ( )
const currentChartData = computed(() => chartData.value[chartPeriod.value]) const currentChartData = computed(() => chartData.value[chartPeriod.value])
const chartDataPoints = computed(() => { const chartDataPoints = computed(() => {
@ -600,7 +960,7 @@ const achievementRate = computed(() => {
return Math.round((totalSales / totalTarget) * 100) return Math.round((totalSales / totalTarget) * 100)
}) })
// // ( )
const getCurrentPeriodLabel = () => { const getCurrentPeriodLabel = () => {
switch (chartPeriod.value) { switch (chartPeriod.value) {
case '7d': return '7일' case '7d': return '7일'
@ -735,31 +1095,6 @@ const hideTooltip = () => {
tooltip.value.show = false 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 () => { const copyRecommendation = async () => {
try { try {
let text = `${aiRecommendation.value.emoji} ${aiRecommendation.value.title}\n\n` let text = `${aiRecommendation.value.emoji} ${aiRecommendation.value.title}\n\n`
@ -816,27 +1151,23 @@ const confirmLogout = () => {
} }
} }
// // onMounted -
onMounted(async () => { onMounted(async () => {
console.log('DashboardView 마운트됨') console.log('DashboardView 마운트됨')
// API //
try { const updateCurrentTime = () => {
// currentTime.value = new Date().toLocaleString('ko-KR')
if (!storeStore.hasStoreInfo) {
await storeStore.fetchStoreInfo()
} }
updateCurrentTime()
setInterval(updateCurrentTime, 60000) // 1
// //
await storeStore.fetchSalesData() await loadStoreAndSalesData() //
// //
await contentStore.fetchOngoingContents() await nextTick()
drawChart()
} catch (error) {
console.warn('대시보드 데이터 로드 실패 (개발 중이므로 무시):', error)
//
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -845,7 +1176,7 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
/* 기존 스타일들 유지 */ /* 기존 스타일들 모두 유지 - 변경 없음 */
.metric-card { .metric-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;