release
This commit is contained in:
parent
2e28c5a9df
commit
c052e3300f
156
src/services/api.js
Normal file
156
src/services/api.js
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
205
src/services/auth.js
Normal file
205
src/services/auth.js
Normal file
@ -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<Object>} 로그인 결과
|
||||
*/
|
||||
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<Object>} 로그아웃 결과
|
||||
*/
|
||||
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<Object>} 토큰 갱신 결과
|
||||
*/
|
||||
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<Object>} 회원가입 결과
|
||||
*/
|
||||
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<Object>} 중복 확인 결과
|
||||
*/
|
||||
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<Object>} 유효성 검증 결과
|
||||
*/
|
||||
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
|
||||
248
src/services/content.js
Normal file
248
src/services/content.js
Normal file
@ -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<Object>} 생성된 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<Object>} 저장 결과
|
||||
*/
|
||||
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<Object>} 생성된 포스터
|
||||
*/
|
||||
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<Object>} 저장 결과
|
||||
*/
|
||||
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<Object>} 콘텐츠 목록
|
||||
*/
|
||||
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<Object>} 진행 중인 콘텐츠 목록
|
||||
*/
|
||||
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<Object>} 콘텐츠 상세 정보
|
||||
*/
|
||||
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<Object>} 수정 결과
|
||||
*/
|
||||
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<Object>} 삭제 결과
|
||||
*/
|
||||
async deleteContent(contentId) {
|
||||
try {
|
||||
await contentApi.delete(`/${contentId}`)
|
||||
|
||||
return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.')
|
||||
} catch (error) {
|
||||
return handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 다중 콘텐츠 삭제
|
||||
* @param {number[]} contentIds - 삭제할 콘텐츠 ID 배열
|
||||
* @returns {Promise<Object>} 삭제 결과
|
||||
*/
|
||||
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<Object>} 재생성된 콘텐츠
|
||||
*/
|
||||
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<Object>} 상태 변경 결과
|
||||
*/
|
||||
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
|
||||
154
src/services/recommend.js
Normal file
154
src/services/recommend.js
Normal file
@ -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<Object>} 생성된 마케팅 팁
|
||||
*/
|
||||
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<Object>} 날씨 기반 메뉴 추천
|
||||
*/
|
||||
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<Object>} 매출 예측 정보
|
||||
*/
|
||||
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<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
|
||||
* @returns {Promise<Object>} 통합 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<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 - 피드백 정보
|
||||
* @returns {Promise<Object>} 피드백 제공 결과
|
||||
*/
|
||||
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
|
||||
188
src/services/store.js
Normal file
188
src/services/store.js
Normal file
@ -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<Object>} 매장 등록 결과
|
||||
*/
|
||||
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<Object>} 매장 정보
|
||||
*/
|
||||
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<Object>} 매장 수정 결과
|
||||
*/
|
||||
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<Object>} 매출 정보
|
||||
*/
|
||||
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<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
|
||||
Loading…
x
Reference in New Issue
Block a user