diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..d2d98e0 --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,156 @@ +//* src/services/api.js +import axios from 'axios' + +// 런타임 환경 설정에서 API URL 가져오기 +const getApiUrls = () => { + const config = window.__runtime_config__ || {} + return { + GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', + MEMBER_URL: config.MEMBER_URL || 'http://20.1.2.3/api/member', + AUTH_URL: config.AUTH_URL || 'http://20.1.2.3/api/auth', + STORE_URL: config.STORE_URL || 'http://20.1.2.3/api/store', + CONTENT_URL: config.CONTENT_URL || 'http://20.1.2.3/api/content', + RECOMMEND_URL: config.RECOMMEND_URL || 'http://20.1.2.3/api/recommendation', + } +} + +// Axios 인스턴스 생성 +const createApiInstance = (baseURL) => { + const instance = axios.create({ + baseURL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }) + + // 요청 인터셉터 - JWT 토큰 자동 추가 + instance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('accessToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + // 응답 인터셉터 - 토큰 갱신 및 에러 처리 + instance.interceptors.response.use( + (response) => { + return response + }, + async (error) => { + const originalRequest = error.config + + // 401 에러이고 토큰 갱신을 시도하지 않은 경우 + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + + try { + const refreshToken = localStorage.getItem('refreshToken') + if (refreshToken) { + const refreshResponse = await axios.post(`${getApiUrls().AUTH_URL}/refresh`, { + refreshToken, + }) + + const { accessToken } = refreshResponse.data.data + localStorage.setItem('accessToken', accessToken) + + // 원래 요청에 새 토큰으로 재시도 + originalRequest.headers.Authorization = `Bearer ${accessToken}` + return instance(originalRequest) + } + } catch (refreshError) { + // 토큰 갱신 실패 시 로그아웃 처리 + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + window.location.href = '/login' + } + } + + return Promise.reject(error) + }, + ) + + return instance +} + +// API 인스턴스들 생성 +const apiUrls = getApiUrls() +export const memberApi = createApiInstance(apiUrls.MEMBER_URL) +export const authApi = createApiInstance(apiUrls.AUTH_URL) +export const storeApi = createApiInstance(apiUrls.STORE_URL) +export const contentApi = createApiInstance(apiUrls.CONTENT_URL) +export const recommendApi = createApiInstance(apiUrls.RECOMMEND_URL) + +// 기본 API 인스턴스 (Gateway URL 사용) +export const api = createApiInstance(apiUrls.GATEWAY_URL) + +// 공통 에러 핸들러 +export const handleApiError = (error) => { + const response = error.response + + if (!response) { + return { + success: false, + message: '네트워크 연결을 확인해주세요.', + code: 'NETWORK_ERROR', + } + } + + const status = response.status + const data = response.data + + switch (status) { + case 400: + return { + success: false, + message: data?.message || '잘못된 요청입니다.', + code: 'BAD_REQUEST', + } + case 401: + return { + success: false, + message: '인증이 필요합니다.', + code: 'UNAUTHORIZED', + } + case 403: + return { + success: false, + message: '접근 권한이 없습니다.', + code: 'FORBIDDEN', + } + case 404: + return { + success: false, + message: '요청하신 정보를 찾을 수 없습니다.', + code: 'NOT_FOUND', + } + case 500: + return { + success: false, + message: '서버 오류가 발생했습니다.', + code: 'SERVER_ERROR', + } + default: + return { + success: false, + message: data?.message || '알 수 없는 오류가 발생했습니다.', + code: 'UNKNOWN_ERROR', + } + } +} + +// 성공 응답 포맷터 +export const formatSuccessResponse = (data, message = '성공적으로 처리되었습니다.') => { + return { + success: true, + message, + data, + } +} diff --git a/src/services/auth.js b/src/services/auth.js new file mode 100644 index 0000000..da9a337 --- /dev/null +++ b/src/services/auth.js @@ -0,0 +1,205 @@ +//* src/services/auth.js +import { memberApi, authApi, handleApiError, formatSuccessResponse } from './api.js' + +/** + * 인증 관련 API 서비스 + * 유저스토리: USR-005, USR-010, USR-020, USR-030, USR-035, USR-040 + */ +class AuthService { + /** + * 로그인 (USR-005: 정상 로그인) + * @param {Object} credentials - 로그인 정보 + * @param {string} credentials.userId - 사용자 ID + * @param {string} credentials.password - 비밀번호 + * @returns {Promise} 로그인 결과 + */ + async login(credentials) { + try { + const response = await authApi.post('/login', { + userId: credentials.userId, + password: credentials.password, + }) + + const { accessToken, refreshToken, expiresIn } = response.data.data + + // 토큰 저장 + localStorage.setItem('accessToken', accessToken) + localStorage.setItem('refreshToken', refreshToken) + localStorage.setItem('tokenExpiresIn', expiresIn.toString()) + localStorage.setItem('userId', credentials.userId) + + return formatSuccessResponse(response.data.data, '로그인되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 로그아웃 (USR-020: 로그아웃) + * @returns {Promise} 로그아웃 결과 + */ + async logout() { + try { + const refreshToken = localStorage.getItem('refreshToken') + + if (refreshToken) { + await authApi.post('/logout', { refreshToken }) + } + + // 로컬 스토리지 정리 + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + localStorage.removeItem('tokenExpiresIn') + localStorage.removeItem('userId') + + return formatSuccessResponse(null, '로그아웃되었습니다.') + } catch (error) { + // 로그아웃은 실패해도 로컬 데이터는 정리 + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + localStorage.removeItem('tokenExpiresIn') + localStorage.removeItem('userId') + + return handleApiError(error) + } + } + + /** + * 토큰 갱신 + * @returns {Promise} 토큰 갱신 결과 + */ + async refreshToken() { + try { + const refreshToken = localStorage.getItem('refreshToken') + + if (!refreshToken) { + throw new Error('리프레시 토큰이 없습니다.') + } + + const response = await authApi.post('/refresh', { refreshToken }) + + const { accessToken, refreshToken: newRefreshToken, expiresIn } = response.data.data + + localStorage.setItem('accessToken', accessToken) + localStorage.setItem('refreshToken', newRefreshToken) + localStorage.setItem('tokenExpiresIn', expiresIn.toString()) + + return formatSuccessResponse(response.data.data, '토큰이 갱신되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 회원가입 (USR-030: 회원등록) + * @param {Object} memberData - 회원가입 정보 + * @param {string} memberData.userId - 사용자 ID + * @param {string} memberData.password - 비밀번호 + * @param {string} memberData.name - 이름 + * @param {string} memberData.businessNumber - 사업자 번호 + * @param {string} memberData.email - 이메일 + * @returns {Promise} 회원가입 결과 + */ + async register(memberData) { + try { + const response = await memberApi.post('/register', { + userId: memberData.userId, + password: memberData.password, + name: memberData.name, + businessNumber: memberData.businessNumber, + email: memberData.email, + }) + + return formatSuccessResponse(response.data.data, '회원가입이 완료되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * ID 중복 확인 (USR-035: 중복ID검사) + * @param {string} userId - 확인할 사용자 ID + * @returns {Promise} 중복 확인 결과 + */ + async checkDuplicate(userId) { + try { + const response = await memberApi.get(`/check-duplicate?userId=${userId}`) + + const { isDuplicate } = response.data.data + + if (isDuplicate) { + return { + success: false, + message: '이미 사용 중인 ID입니다.', + data: { isDuplicate: true }, + } + } + + return formatSuccessResponse({ isDuplicate: false }, '사용 가능한 ID입니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 비밀번호 유효성 검증 (USR-040: 암호유효성 검사) + * @param {string} password - 검증할 비밀번호 + * @returns {Promise} 유효성 검증 결과 + */ + async validatePassword(password) { + try { + const response = await memberApi.post('/validate-password', { password }) + + const { isValid, errors } = response.data.data + + if (!isValid) { + return { + success: false, + message: '비밀번호가 규칙에 맞지 않습니다.', + data: { isValid: false, errors }, + } + } + + return formatSuccessResponse({ isValid: true, errors: [] }, '사용 가능한 비밀번호입니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 현재 로그인 상태 확인 + * @returns {boolean} 로그인 여부 + */ + isAuthenticated() { + const token = localStorage.getItem('accessToken') + const expiresIn = localStorage.getItem('tokenExpiresIn') + + if (!token || !expiresIn) { + return false + } + + // 토큰 만료 시간 확인 (여유를 두고 5분 전에 만료로 처리) + const now = Date.now() + const expiryTime = parseInt(expiresIn) - 5 * 60 * 1000 + + return now < expiryTime + } + + /** + * 현재 사용자 정보 가져오기 + * @returns {Object|null} 사용자 정보 + */ + getCurrentUser() { + const userId = localStorage.getItem('userId') + const token = localStorage.getItem('accessToken') + + if (!userId || !token || !this.isAuthenticated()) { + return null + } + + return { userId } + } +} + +export const authService = new AuthService() +export default authService diff --git a/src/services/content.js b/src/services/content.js new file mode 100644 index 0000000..fe92956 --- /dev/null +++ b/src/services/content.js @@ -0,0 +1,248 @@ +//* src/services/content.js +import { contentApi, handleApiError, formatSuccessResponse } from './api.js' + +/** + * 마케팅 콘텐츠 관련 API 서비스 + * 유저스토리: CON-005, CON-015, CON-020, CON-025, CON-030 + */ +class ContentService { + /** + * SNS 게시물 생성 (CON-005: SNS 게시물 생성) + * @param {Object} contentData - SNS 콘텐츠 생성 정보 + * @returns {Promise} 생성된 SNS 콘텐츠 + */ + async generateSnsContent(contentData) { + try { + const response = await contentApi.post('/sns/generate', { + targetType: contentData.targetType, // 메뉴, 매장, 이벤트 + platform: contentData.platform, // 인스타그램, 네이버블로그 + images: contentData.images, + eventName: contentData.eventName, + startDate: contentData.startDate, + endDate: contentData.endDate, + toneAndManner: contentData.toneAndManner, // 친근함, 전문적, 유머러스, 고급스러운 + promotionType: contentData.promotionType, // 할인정보, 이벤트정보, 신메뉴알림, 없음 + emotionIntensity: contentData.emotionIntensity, // 차분함, 보통, 열정적, 과장된 + requirements: contentData.requirements || '', + }) + + return formatSuccessResponse(response.data.data, 'SNS 게시물이 생성되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * SNS 게시물 저장 + * @param {Object} saveData - 저장할 SNS 콘텐츠 정보 + * @returns {Promise} 저장 결과 + */ + async saveSnsContent(saveData) { + try { + const response = await contentApi.post('/sns/save', { + title: saveData.title, + content: saveData.content, + hashtags: saveData.hashtags, + images: saveData.images, + platform: saveData.platform, + status: saveData.status || 'DRAFT', + publishSchedule: saveData.publishSchedule, + }) + + return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 홍보 포스터 생성 (CON-015: 홍보 포스터 생성) + * @param {Object} posterData - 포스터 생성 정보 + * @returns {Promise} 생성된 포스터 + */ + async generatePoster(posterData) { + try { + const response = await contentApi.post('/poster/generate', { + targetType: posterData.targetType, + images: posterData.images, + eventName: posterData.eventName, + startDate: posterData.startDate, + endDate: posterData.endDate, + photoStyle: posterData.photoStyle, // 모던, 클래식, 감성적 + promotionType: posterData.promotionType, + emotionIntensity: posterData.emotionIntensity, + sizes: posterData.sizes || ['1:1', '9:16', '16:9'], // SNS용, 스토리용, 블로그용 + }) + + return formatSuccessResponse(response.data.data, '홍보 포스터가 생성되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 홍보 포스터 저장 + * @param {Object} saveData - 저장할 포스터 정보 + * @returns {Promise} 저장 결과 + */ + async savePoster(saveData) { + try { + const response = await contentApi.post('/poster/save', { + title: saveData.title, + images: saveData.images, + posterSizes: saveData.posterSizes, + targetType: saveData.targetType, + eventName: saveData.eventName, + status: saveData.status || 'DRAFT', + }) + + return formatSuccessResponse(response.data.data, '홍보 포스터가 저장되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 목록 조회 (CON-020: 마케팅 콘텐츠 이력) + * @param {Object} filters - 필터링 옵션 + * @returns {Promise} 콘텐츠 목록 + */ + async getContents(filters = {}) { + try { + const queryParams = new URLSearchParams() + + if (filters.contentType) queryParams.append('contentType', filters.contentType) + if (filters.platform) queryParams.append('platform', filters.platform) + if (filters.period) queryParams.append('period', filters.period) + if (filters.sortBy) queryParams.append('sortBy', filters.sortBy || 'latest') + if (filters.page) queryParams.append('page', filters.page) + if (filters.size) queryParams.append('size', filters.size || 20) + if (filters.search) queryParams.append('search', filters.search) + + const response = await contentApi.get(`/?${queryParams.toString()}`) + + return formatSuccessResponse(response.data.data, '콘텐츠 목록을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 진행 중인 콘텐츠 조회 + * @param {string} period - 조회 기간 + * @returns {Promise} 진행 중인 콘텐츠 목록 + */ + async getOngoingContents(period = 'month') { + try { + const response = await contentApi.get(`/ongoing?period=${period}`) + + return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 상세 조회 + * @param {number} contentId - 콘텐츠 ID + * @returns {Promise} 콘텐츠 상세 정보 + */ + async getContentDetail(contentId) { + try { + const response = await contentApi.get(`/${contentId}`) + + return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 수정 + * @param {number} contentId - 콘텐츠 ID + * @param {Object} updateData - 수정할 콘텐츠 정보 + * @returns {Promise} 수정 결과 + */ + async updateContent(contentId, updateData) { + try { + const response = await contentApi.put(`/${contentId}`, { + title: updateData.title, + content: updateData.content, + hashtags: updateData.hashtags, + startDate: updateData.startDate, + endDate: updateData.endDate, + status: updateData.status, + }) + + return formatSuccessResponse(response.data.data, '콘텐츠가 수정되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 삭제 (CON-025: 콘텐츠 삭제) + * @param {number} contentId - 콘텐츠 ID + * @returns {Promise} 삭제 결과 + */ + async deleteContent(contentId) { + try { + await contentApi.delete(`/${contentId}`) + + return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 다중 콘텐츠 삭제 + * @param {number[]} contentIds - 삭제할 콘텐츠 ID 배열 + * @returns {Promise} 삭제 결과 + */ + async deleteContents(contentIds) { + try { + const deletePromises = contentIds.map((contentId) => this.deleteContent(contentId)) + await Promise.all(deletePromises) + + return formatSuccessResponse(null, `${contentIds.length}개의 콘텐츠가 삭제되었습니다.`) + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 재생성 + * @param {number} contentId - 원본 콘텐츠 ID + * @param {Object} regenerateOptions - 재생성 옵션 + * @returns {Promise} 재생성된 콘텐츠 + */ + async regenerateContent(contentId, regenerateOptions = {}) { + try { + const response = await contentApi.post(`/${contentId}/regenerate`, regenerateOptions) + + return formatSuccessResponse(response.data.data, '콘텐츠가 재생성되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 발행 상태 변경 + * @param {number} contentId - 콘텐츠 ID + * @param {string} status - 변경할 상태 (DRAFT, PUBLISHED, ARCHIVED) + * @returns {Promise} 상태 변경 결과 + */ + async updateContentStatus(contentId, status) { + try { + const response = await contentApi.patch(`/${contentId}/status`, { status }) + + return formatSuccessResponse(response.data.data, '콘텐츠 상태가 변경되었습니다.') + } catch (error) { + return handleApiError(error) + } + } +} + +export const contentService = new ContentService() +export default contentService diff --git a/src/services/recommend.js b/src/services/recommend.js new file mode 100644 index 0000000..9099163 --- /dev/null +++ b/src/services/recommend.js @@ -0,0 +1,154 @@ +//* src/services/recommend.js +import { recommendApi, handleApiError, formatSuccessResponse } from './api.js' + +/** + * AI 추천 관련 API 서비스 + * 유저스토리: REC-005 + */ +class RecommendService { + /** + * AI 마케팅 팁 생성 (REC-005: AI 마케팅 방법 추천) + * @param {Object} requestData - 마케팅 팁 요청 정보 + * @returns {Promise} 생성된 마케팅 팁 + */ + async generateMarketingTips(requestData = {}) { + try { + const response = await recommendApi.post('/marketing-tips', { + 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 + }) + + return formatSuccessResponse(response.data.data, 'AI 마케팅 팁이 생성되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 날씨 기반 메뉴 추천 + * @param {number} storeId - 매장 ID + * @returns {Promise} 날씨 기반 메뉴 추천 + */ + async getWeatherBasedMenuRecommendation(storeId) { + try { + const response = await recommendApi.get(`/weather-menu/${storeId}`) + + return formatSuccessResponse(response.data.data, '날씨 기반 메뉴 추천을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 매출 예측 추천 + * @param {number} storeId - 매장 ID + * @param {string} period - 예측 기간 (day, week, month) + * @returns {Promise} 매출 예측 정보 + */ + async getSalesPrediction(storeId, period = 'day') { + try { + const response = await recommendApi.get(`/sales-prediction/${storeId}?period=${period}`) + + return formatSuccessResponse(response.data.data, '매출 예측 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 인기 메뉴 추천 + * @param {number} storeId - 매장 ID + * @param {string} period - 분석 기간 + * @returns {Promise} 인기 메뉴 추천 + */ + async getPopularMenuRecommendation(storeId, period = 'month') { + try { + const response = await recommendApi.get(`/popular-menu/${storeId}?period=${period}`) + + return formatSuccessResponse(response.data.data, '인기 메뉴 추천을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 마케팅 전략 추천 + * @param {number} storeId - 매장 ID + * @param {string} strategyType - 전략 유형 (sales_boost, customer_retention, cost_reduction) + * @returns {Promise} 마케팅 전략 추천 + */ + async getMarketingStrategy(storeId, strategyType = 'sales_boost') { + try { + const response = await recommendApi.get(`/marketing-strategy/${storeId}?type=${strategyType}`) + + return formatSuccessResponse(response.data.data, '마케팅 전략 추천을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 통합 AI 추천 (매출 예측 + 메뉴 추천 + 마케팅 전략) + * @param {number} storeId - 매장 ID + * @returns {Promise} 통합 AI 추천 정보 + */ + async getComprehensiveRecommendation(storeId) { + try { + const response = await recommendApi.get(`/comprehensive-recommendation/${storeId}`) + + return formatSuccessResponse(response.data.data, '통합 AI 추천을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 추천 기록 조회 + * @param {Object} filters - 필터링 옵션 + * @returns {Promise} 추천 기록 목록 + */ + async getRecommendationHistory(filters = {}) { + try { + const queryParams = new URLSearchParams() + + if (filters.type) queryParams.append('type', filters.type) + if (filters.startDate) queryParams.append('startDate', filters.startDate) + if (filters.endDate) queryParams.append('endDate', filters.endDate) + if (filters.page) queryParams.append('page', filters.page) + if (filters.size) queryParams.append('size', filters.size || 20) + + const response = await recommendApi.get(`/history?${queryParams.toString()}`) + + return formatSuccessResponse(response.data.data, '추천 기록을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 추천 피드백 제공 + * @param {number} recommendationId - 추천 ID + * @param {Object} feedback - 피드백 정보 + * @returns {Promise} 피드백 제공 결과 + */ + async provideFeedback(recommendationId, feedback) { + try { + const response = await recommendApi.post(`/feedback/${recommendationId}`, { + rating: feedback.rating, // 1-5 점수 + useful: feedback.useful, // true/false + comment: feedback.comment || '', + appliedSuggestions: feedback.appliedSuggestions || [], + }) + + return formatSuccessResponse(response.data.data, '피드백이 제공되었습니다.') + } catch (error) { + return handleApiError(error) + } + } +} + +export const recommendService = new RecommendService() +export default recommendService diff --git a/src/services/store.js b/src/services/store.js new file mode 100644 index 0000000..36dcab5 --- /dev/null +++ b/src/services/store.js @@ -0,0 +1,188 @@ +//* 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} 매장 등록 결과 + */ + async registerStore(storeData) { + try { + const response = await storeApi.post('/register', { + storeName: storeData.storeName, + storeImage: storeData.storeImage, + businessType: storeData.businessType, + address: storeData.address, + phoneNumber: storeData.phoneNumber, + businessNumber: storeData.businessNumber, + instaAccount: storeData.instaAccount, + naverBlogAccount: storeData.naverBlogAccount, + operatingHours: storeData.operatingHours, + closedDays: storeData.closedDays, + seatCount: storeData.seatCount, + }) + + return formatSuccessResponse(response.data.data, '매장이 등록되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 매장 정보 조회 (STR-005: 매장 정보 관리) + * @returns {Promise} 매장 정보 + */ + async getStore() { + try { + const response = await storeApi.get('/') + + return formatSuccessResponse(response.data.data, '매장 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 매장 정보 수정 (STR-010: 매장 수정) + * @param {number} storeId - 매장 ID + * @param {Object} storeData - 수정할 매장 정보 + * @returns {Promise} 매장 수정 결과 + */ + async updateStore(storeId, storeData) { + try { + const response = await storeApi.put(`/${storeId}`, storeData) + + return formatSuccessResponse(response.data.data, '매장 정보가 수정되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 매출 정보 조회 (STR-020: 대시보드) + * @param {string} period - 조회 기간 (today, week, month, year) + * @returns {Promise} 매출 정보 + */ + async getSales(period = 'today') { + try { + const response = await storeApi.get(`/sales?period=${period}`) + + return formatSuccessResponse(response.data.data, '매출 정보를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 메뉴 등록 (STR-030: 메뉴 등록) + * @param {Object} menuData - 메뉴 정보 + * @returns {Promise} 메뉴 등록 결과 + */ + 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} 메뉴 목록 + */ + 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} 메뉴 수정 결과 + */ + async updateMenu(menuId, menuData) { + try { + const response = await storeApi.put(`/menu/${menuId}`, menuData) + + return formatSuccessResponse(response.data.data, '메뉴가 수정되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 메뉴 삭제 (STR-040: 메뉴 삭제) + * @param {number} menuId - 메뉴 ID + * @returns {Promise} 메뉴 삭제 결과 + */ + async deleteMenu(menuId) { + try { + await storeApi.delete(`/menu/${menuId}`) + + return formatSuccessResponse(null, '메뉴가 삭제되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 다중 메뉴 삭제 + * @param {number[]} menuIds - 삭제할 메뉴 ID 배열 + * @returns {Promise} 삭제 결과 + */ + async deleteMenus(menuIds) { + try { + const deletePromises = menuIds.map((menuId) => this.deleteMenu(menuId)) + await Promise.all(deletePromises) + + return formatSuccessResponse(null, `${menuIds.length}개의 메뉴가 삭제되었습니다.`) + } catch (error) { + return handleApiError(error) + } + } + + /** + * 매장 통계 정보 조회 + * @returns {Promise} 매장 통계 + */ + async getStoreStatistics() { + try { + const response = await storeApi.get('/statistics') + + return formatSuccessResponse(response.data.data, '매장 통계를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } +} + +export const storeService = new StoreService() +export default storeService