diff --git a/public/images/menu-placeholder.png b/public/images/menu-placeholder.png new file mode 100644 index 0000000..f097a4b Binary files /dev/null and b/public/images/menu-placeholder.png differ diff --git a/src/components/poster/PosterForm.vue b/src/components/poster/PosterForm.vue new file mode 100644 index 0000000..abdcfcc --- /dev/null +++ b/src/components/poster/PosterForm.vue @@ -0,0 +1,407 @@ +//* src/components/poster/PosterForm.vue + + + \ No newline at end of file diff --git a/src/components/poster/PosterPreview.vue b/src/components/poster/PosterPreview.vue new file mode 100644 index 0000000..a615f54 --- /dev/null +++ b/src/components/poster/PosterPreview.vue @@ -0,0 +1,318 @@ +//* src/components/poster/PosterPreview.vue + + + + + \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index 496ae7c..6ac5977 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,4 +1,5 @@ -//* src/services/api.js - 수정된 API URL 설정 +//* src/services/api.js - 수정된 버전 (createImageApiInstance 함수 추가) + import axios from 'axios' // 런타임 환경 설정에서 API URL 가져오기 @@ -6,15 +7,14 @@ const getApiUrls = () => { const config = window.__runtime_config__ || {} return { GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', - AUTH_URL: config.AUTH_URL || 'http://smarketing.20.249.184.228.nip.io/api/auth', - MEMBER_URL: config.MEMBER_URL || 'http://smarketing.20.249.184.228.nip.io/api/member', - STORE_URL: config.STORE_URL || 'http://smarketing.20.249.184.228.nip.io/api/store', - CONTENT_URL: config.CONTENT_URL || 'http://smarketing.20.249.184.228.nip.io/api/content', - MENU_URL: config.MENU_URL || 'http://smarketing.20.249.184.228.nip.io/api/menu', - // ⚠️ 수정: 매출 API는 store 서비스 (포트 8082) - SALES_URL: config.SALES_URL || 'http://smarketing.20.249.184.228.nip.io/api/sales', - // ⚠️ 수정: 추천 API는 ai-recommend 서비스 (포트 8084) - RECOMMEND_URL: config.RECOMMEND_URL || 'http://smarketing.20.249.184.228.nip.io/api/recommendations' + 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', + MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu', + SALES_URL: config.SALES_URL || 'http://localhost:8082/api/sales', + RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendations', + IMAGE_URL: config.IMAGE_URL || 'http://localhost:8082/api/images' } } @@ -37,7 +37,6 @@ const createApiInstance = (baseURL) => { config.headers.Authorization = `Bearer ${token}` } - // ⚠️ 추가: 요청 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.log(`🌐 [API_REQ] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) } @@ -52,14 +51,12 @@ const createApiInstance = (baseURL) => { // 응답 인터셉터 - 토큰 갱신 및 에러 처리 instance.interceptors.response.use( (response) => { - // ⚠️ 추가: 응답 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.log(`✅ [API_RES] ${response.status} ${response.config?.method?.toUpperCase()} ${response.config?.url}`) } return response }, async (error) => { - // ⚠️ 추가: 에러 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.error(`❌ [API_ERR] ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error.response?.data) } @@ -102,10 +99,155 @@ const createApiInstance = (baseURL) => { return instance } +// ✅ 이미지 업로드 전용 API 인스턴스 생성 함수 추가 +const createImageApiInstance = (baseURL) => { + const instance = axios.create({ + baseURL, + timeout: 60000, // 이미지 업로드는 시간이 더 걸릴 수 있음 + headers: { + Accept: 'application/json', + }, + }) + + // 요청 인터셉터 - JWT 토큰 자동 추가 + instance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('accessToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + if (import.meta.env.DEV) { + console.log(`🌐 [IMG_REQ] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) + console.log('FormData 포함:', config.data instanceof FormData) + } + + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + // 응답 인터셉터 + instance.interceptors.response.use( + (response) => { + if (import.meta.env.DEV) { + console.log(`✅ [IMG_RES] ${response.status} ${response.config?.method?.toUpperCase()} ${response.config?.url}`) + } + return response + }, + async (error) => { + if (import.meta.env.DEV) { + console.error(`❌ [IMG_ERR] ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error.response?.data) + } + + // 토큰 갱신 로직은 기존과 동일 + const originalRequest = error.config + 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, refreshToken: newRefreshToken } = refreshResponse.data.data + localStorage.setItem('accessToken', accessToken) + localStorage.setItem('refreshToken', newRefreshToken) + originalRequest.headers.Authorization = `Bearer ${accessToken}` + return instance(originalRequest) + } + } catch (refreshError) { + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + localStorage.removeItem('userInfo') + window.location.href = '/login' + } + } + return Promise.reject(error) + }, + ) + + return instance +} + +// ✅ 메뉴 이미지 업로드 전용 API 인스턴스 생성 함수 추가 +const createMenuImageApiInstance = (baseURL) => { + const instance = axios.create({ + baseURL, + timeout: 60000, // 이미지 업로드는 시간이 더 걸릴 수 있음 + headers: { + Accept: 'application/json', + }, + }) + + // 요청 인터셉터 - JWT 토큰 자동 추가 + instance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('accessToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + if (import.meta.env.DEV) { + console.log(`🌐 [MENU_IMG_REQ] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) + console.log('FormData 포함:', config.data instanceof FormData) + } + + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + // 응답 인터셉터 + instance.interceptors.response.use( + (response) => { + if (import.meta.env.DEV) { + console.log(`✅ [MENU_IMG_RES] ${response.status} ${response.config?.method?.toUpperCase()} ${response.config?.url}`) + } + return response + }, + async (error) => { + if (import.meta.env.DEV) { + console.error(`❌ [MENU_IMG_ERR] ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error.response?.data) + } + + // 토큰 갱신 로직은 기존과 동일 + const originalRequest = error.config + 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, refreshToken: newRefreshToken } = refreshResponse.data.data + localStorage.setItem('accessToken', accessToken) + localStorage.setItem('refreshToken', newRefreshToken) + originalRequest.headers.Authorization = `Bearer ${accessToken}` + return instance(originalRequest) + } + } catch (refreshError) { + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + localStorage.removeItem('userInfo') + window.location.href = '/login' + } + } + return Promise.reject(error) + }, + ) + + return instance +} + // API 인스턴스들 생성 const apiUrls = getApiUrls() -// ⚠️ 추가: API URL 확인 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.log('🔧 [API_CONFIG] API URLs 설정:', apiUrls) } @@ -115,8 +257,11 @@ export const authApi = createApiInstance(apiUrls.AUTH_URL) export const storeApi = createApiInstance(apiUrls.STORE_URL) export const contentApi = createApiInstance(apiUrls.CONTENT_URL) export const menuApi = createApiInstance(apiUrls.MENU_URL) +export const menuImageApi = createMenuImageApiInstance(apiUrls.MENU_URL) // ✅ 추가 export const salesApi = createApiInstance(apiUrls.SALES_URL) export const recommendApi = createApiInstance(apiUrls.RECOMMEND_URL) +export const imageApi = createApiInstance(apiUrls.IMAGE_URL) +export const apiWithImage = imageApi // 별칭 (기존 코드 호환성) // 기본 API 인스턴스 (Gateway URL 사용) export const api = createApiInstance(apiUrls.GATEWAY_URL) @@ -185,7 +330,7 @@ export const formatSuccessResponse = (data, message = '요청이 성공적으로 } } -// ⚠️ 추가: API 상태 확인 함수 +// API 상태 확인 함수 export const checkApiHealth = async () => { const results = {} @@ -214,11 +359,14 @@ export const checkApiHealth = async () => { return results } -// ⚠️ 추가: 개발 환경에서 전역 노출 +// 개발 환경에서 전역 노출 if (import.meta.env.DEV) { window.__api_debug__ = { urls: apiUrls, - instances: { memberApi, authApi, storeApi, contentApi, menuApi, salesApi, recommendApi }, + instances: { + memberApi, authApi, storeApi, contentApi, menuApi, menuImageApi, + salesApi, recommendApi, imageApi + }, checkHealth: checkApiHealth } console.log('🔧 [DEBUG] API 인스턴스가 window.__api_debug__에 노출됨') diff --git a/src/services/content.js b/src/services/content.js index 1a3a44f..e45bf01 100644 --- a/src/services/content.js +++ b/src/services/content.js @@ -1,64 +1,693 @@ -//* src/services/content.js - 기존 파일 수정 (API 설계서 기준) -import { contentApi, handleApiError, formatSuccessResponse } from './api.js' +//* src/services/content.js - 완전한 파일 (모든 수정사항 포함) +import axios from 'axios' + +// runtime-env.js에서 API URL 가져오기 (대체 방식 포함) +const getApiUrl = (serviceName) => { + if (typeof window !== 'undefined' && window.__runtime_config__) { + const urlKey = `${serviceName.toUpperCase()}_URL` + const apiUrl = window.__runtime_config__[urlKey] + if (apiUrl) { + console.log(`✅ ${serviceName} API URL 로드됨:`, apiUrl) + return apiUrl + } + } + + // 대체 URL 반환 - /api/content 사용 (백엔드 경로에 맞춤) + const fallbackUrl = `http://localhost:8083/api/content` + console.log(`⚠️ ${serviceName} API URL 대체값 사용:`, fallbackUrl) + return fallbackUrl +} + +// Content API 인스턴스 생성 - baseURL 수정 (마지막 슬래시 제거) +const contentApi = axios.create({ + baseURL: getApiUrl('CONTENT').replace(/\/$/, ''), // 마지막 슬래시 제거 + timeout: 30000, + headers: { + 'Content-Type': 'application/json' + } +}) + +// 요청 인터셉터 - JWT 토큰 자동 추가 +contentApi.interceptors.request.use( + (config) => { + console.log('🔄 Content API 요청:', config.method?.toUpperCase(), config.url) + + const token = localStorage.getItem('accessToken') || localStorage.getItem('auth_token') || localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } else { + console.warn('⚠️ JWT 토큰이 없습니다!') + } + + console.log('요청 데이터:', config.data || config.params) + return config + }, + (error) => { + console.error('❌ Content API 요청 오류:', error) + return Promise.reject(error) + } +) + +// 응답 인터셉터 +contentApi.interceptors.response.use( + (response) => { + console.log('✅ Content API 응답:', response.status, response.data) + return response + }, + (error) => { + console.error('❌ Content API 응답 오류:', error.response?.status, error.response?.data) + + if (error.response?.status === 401) { + // 토큰 만료 처리 + localStorage.removeItem('auth_token') + window.location.href = '/login' + } + + return Promise.reject(error) + } +) + +// 공통 응답 처리 함수 +const formatSuccessResponse = (data, message = '요청이 성공했습니다.') => ({ + success: true, + data, + message +}) + +const handleApiError = (error) => { + console.error('API 오류 처리:', error) + + if (error.response) { + return { + success: false, + error: error.response.data?.message || `서버 오류 (${error.response.status})`, + status: error.response.status + } + } else if (error.request) { + return { + success: false, + error: '네트워크 연결을 확인해주세요.', + status: 0 + } + } else { + return { + success: false, + error: error.message || '알 수 없는 오류가 발생했습니다.', + status: 0 + } + } +} /** - * 마케팅 콘텐츠 관련 API 서비스 - * API 설계서 기준으로 수정됨 + * 콘텐츠 서비스 클래스 - 완전 통합 버전 + * 백엔드 API 설계서와 일치하도록 구현 */ class ContentService { /** - * SNS 게시물 생성 (CON-005: SNS 게시물 생성) - * @param {Object} contentData - SNS 콘텐츠 생성 정보 - * @returns {Promise} 생성된 SNS 콘텐츠 + * 콘텐츠 목록 조회 (CON-021: 콘텐츠 조회) + * @param {Object} filters - 필터 조건 + * @returns {Promise} 콘텐츠 목록 */ - async generateSnsContent(contentData) { + async getContents(filters = {}) { try { - const response = await contentApi.post('/sns/generate', { - storeId: contentData.storeId, - platform: contentData.platform, - title: contentData.title, - category: contentData.category, - requirement: contentData.requirement || contentData.requirements, - toneAndManner: contentData.toneAndManner, - emotionalIntensity: contentData.emotionalIntensity || contentData.emotionIntensity, - targetAudience: contentData.targetAudience, - promotionalType: contentData.promotionalType || contentData.promotionType, - eventName: contentData.eventName, - eventDate: contentData.eventDate, - hashtagStyle: contentData.hashtagStyle, - hashtagCount: contentData.hashtagCount || 10, - includeCallToAction: contentData.includeCallToAction || false, - includeEmoji: contentData.includeEmoji || true, - contentLength: contentData.contentLength || '보통' + console.log('🔄 콘텐츠 목록 조회 요청:', filters) + + // 쿼리 파라미터 구성 - 빈 값 제거 + const params = new URLSearchParams() + + // 필수 파라미터만 추가 (값이 있을 때만) + if (filters.storeId) { + params.append('storeId', filters.storeId.toString()) + } + if (filters.sortBy) { + params.append('sortBy', filters.sortBy) + } + + // 선택적 파라미터 (값이 있고 'all'이 아닐 때만) + if (filters.platform && filters.platform !== 'all') { + params.append('platform', filters.platform) + } + if (filters.contentType && filters.contentType !== 'all') { + params.append('contentType', filters.contentType) + } + if (filters.period && filters.period !== 'all') { + params.append('period', filters.period) + } + + const queryString = params.toString() + const fullUrl = `${getApiUrl('CONTENT').replace(/\/$/, '')}${queryString ? `?${queryString}` : ''}` + + console.log('📡 완전한 API URL:', fullUrl) + + // axios 대신 fetch 사용 (브라우저 콘솔 테스트와 동일하게) + const response = await fetch(fullUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('accessToken') || localStorage.getItem('auth_token') || localStorage.getItem('token')}`, + 'Content-Type': 'application/json' + } }) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const responseData = await response.json() - return formatSuccessResponse(response.data.data, 'SNS 게시물이 생성되었습니다.') + // 백엔드 응답 구조에 따른 데이터 추출 + let contentData = [] + + if (responseData.success) { + contentData = responseData.data || [] + } else if (Array.isArray(responseData)) { + contentData = responseData + } else if (responseData.contents) { + contentData = responseData.contents + } else if (responseData.data) { + contentData = responseData.data || [] + } + + console.log('✅ 콘텐츠 조회 성공:', contentData.length, '개') + + return formatSuccessResponse(contentData, '콘텐츠 목록을 조회했습니다.') } catch (error) { + console.error('❌ 콘텐츠 조회 실패:', error) return handleApiError(error) } } /** - * SNS 게시물 저장 (CON-010: SNS 게시물 저장) - * @param {Object} saveData - 저장할 SNS 콘텐츠 정보 + * SNS 콘텐츠 생성 (CON-019: AI 콘텐츠 생성) - 수정된 버전 + * @param {Object} contentData - 콘텐츠 생성 데이터 + * @returns {Promise} 생성된 콘텐츠 + */ + async generateSnsContent(contentData) { + try { + console.log('🤖 SNS 콘텐츠 생성 요청:', contentData) + + // ✅ contentData 기본 검증 + if (!contentData || typeof contentData !== 'object') { + throw new Error('콘텐츠 데이터가 전달되지 않았습니다.') + } + + // ✅ images 속성 보장 (방어 코드) + if (!contentData.hasOwnProperty('images')) { + console.warn('⚠️ [API] images 속성이 없음, 빈 배열로 설정') + contentData.images = [] + } + + if (!Array.isArray(contentData.images)) { + console.warn('⚠️ [API] images가 배열이 아님, 빈 배열로 변환:', typeof contentData.images) + contentData.images = [] + } + + // ✅ 필수 필드 검증 + const requiredFields = ['title', 'platform'] + const missingFields = requiredFields.filter(field => !contentData[field]) + + if (missingFields.length > 0) { + throw new Error(`필수 필드가 누락되었습니다: ${missingFields.join(', ')}`) + } + + // ✅ 플랫폼 형식 통일 + const normalizeplatform = (platform) => { + const platformMap = { + 'INSTAGRAM': 'instagram', + 'instagram': 'instagram', + 'NAVER_BLOG': 'naver_blog', + 'naver_blog': 'naver_blog', + 'FACEBOOK': 'facebook', + 'facebook': 'facebook', + 'KAKAO_STORY': 'kakao_story', + 'kakao_story': 'kakao_story' + } + return platformMap[platform] || platform.toLowerCase() + } + + // ✅ 카테고리 매핑 + const getCategoryFromTargetType = (targetType) => { + const categoryMap = { + 'new_menu': '메뉴소개', + 'menu': '메뉴소개', + 'discount': '이벤트', + 'event': '이벤트', + 'store': '매장홍보', + 'service': '서비스', + 'interior': '인테리어', + 'daily': '일상', + 'review': '고객후기' + } + return categoryMap[targetType] || '기타' + } + + // ✅ 요청 데이터 구성 + const requestData = { + // 필수 필드들 + title: contentData.title.trim(), + platform: normalizeplatform(contentData.platform), + contentType: contentData.contentType || 'sns', + category: contentData.category || getCategoryFromTargetType(contentData.targetType), + images: contentData.images || [] // 기본값 보장 + } + + // ✅ storeId 처리 + if (contentData.storeId !== undefined && contentData.storeId !== null) { + requestData.storeId = contentData.storeId + } else { + try { + const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}') + requestData.storeId = storeInfo.storeId || 1 + } catch { + requestData.storeId = 1 + } + } + + // ✅ 선택적 필드들 + if (contentData.storeName) requestData.storeName = contentData.storeName + if (contentData.storeType) requestData.storeType = contentData.storeType + if (contentData.requirement || contentData.requirements) { + requestData.requirement = contentData.requirement || contentData.requirements + } + if (contentData.target || contentData.targetAudience) { + requestData.target = contentData.target || contentData.targetAudience + } + if (contentData.eventName) requestData.eventName = contentData.eventName + if (contentData.startDate) requestData.startDate = contentData.startDate + if (contentData.endDate) requestData.endDate = contentData.endDate + if (contentData.targetAge) requestData.targetAge = contentData.targetAge + + // ✅ 이미지 처리 (contentData.images가 보장됨) + console.log('📁 [API] 이미지 처리 시작:', contentData.images.length, '개') + + const processedImages = contentData.images + .filter(img => img && typeof img === 'string' && img.length > 50) + .map(img => { + if (typeof img === 'string' && img.startsWith('data:image/')) { + return img // Base64 그대로 사용 + } else if (typeof img === 'string' && (img.startsWith('http') || img.startsWith('//'))) { + return img // URL 그대로 사용 + } else { + console.warn('📁 [API] 알 수 없는 이미지 형식:', img.substring(0, 50)) + return img + } + }) + + requestData.images = processedImages + console.log('📁 [API] 처리된 이미지:', processedImages.length, '개') + + // ✅ 최종 검증 + console.log('📝 [API] 최종 SNS 요청 데이터:', { + title: requestData.title, + platform: requestData.platform, + category: requestData.category, + contentType: requestData.contentType, + storeId: requestData.storeId, + imageCount: requestData.images.length + }) + + // ✅ Python AI 서비스 필수 필드 검증 + const pythonRequiredFields = ['title', 'category', 'contentType', 'platform', 'images'] + const pythonMissingFields = pythonRequiredFields.filter(field => { + if (field === 'images') { + return !Array.isArray(requestData[field]) + } + return !requestData[field] + }) + + if (pythonMissingFields.length > 0) { + console.error('❌ [API] Python AI 서비스 필수 필드 누락:', pythonMissingFields) + throw new Error(`AI 서비스 필수 필드가 누락되었습니다: ${pythonMissingFields.join(', ')}`) + } + + const response = await contentApi.post('/sns/generate', requestData, { + timeout: 30000 + }) + + console.log('✅ [API] SNS 콘텐츠 생성 응답:', response.data) + return formatSuccessResponse(response.data, 'SNS 게시물이 생성되었습니다.') + + } catch (error) { + console.error('❌ [API] SNS 콘텐츠 생성 실패:', error) + + if (error.response?.status === 400) { + const backendMessage = error.response.data?.message || '요청 데이터가 잘못되었습니다.' + return { + success: false, + message: backendMessage, + error: error.response.data + } + } else if (error.response?.status === 500) { + return { + success: false, + message: 'AI 서비스에서 콘텐츠 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', + error: error.response.data + } + } + + return handleApiError(error) + } + } + + /** + * 포스터 생성 (CON-020: AI 포스터 생성) - 수정된 버전 + * @param {Object} posterData - 포스터 생성 데이터 + * @returns {Promise} 생성된 포스터 + */ + async generatePoster(posterData) { + try { + console.log('🎯 [API] 포스터 생성 요청 받음:', posterData) + + // ✅ 1. 이미지 상세 분석 및 검증 + console.log('📁 [API] 이미지 상세 분석 시작...') + console.log('📁 [API] posterData.images 타입:', typeof posterData.images) + console.log('📁 [API] posterData.images 배열 여부:', Array.isArray(posterData.images)) + console.log('📁 [API] posterData.images 길이:', posterData.images?.length) + + let processedImages = [] + + if (posterData.images && Array.isArray(posterData.images) && posterData.images.length > 0) { + console.log('📁 [API] 원본 이미지 배열 처리 시작...') + + // 각 이미지를 개별적으로 검증 + posterData.images.forEach((img, index) => { + console.log(`📁 [API] 이미지 ${index + 1} 분석:`, { + type: typeof img, + isString: typeof img === 'string', + length: img?.length, + isNull: img === null, + isUndefined: img === undefined, + isEmpty: img === '', + isBase64: typeof img === 'string' && img.startsWith('data:image/'), + preview: typeof img === 'string' ? img.substring(0, 50) + '...' : 'Not string' + }) + }) + + // 유효한 이미지만 필터링 (더 엄격한 검증) + processedImages = posterData.images.filter((img, index) => { + const isValid = img && + typeof img === 'string' && + img.length > 100 && // 최소 길이 체크 (Base64는 보통 매우 길다) + (img.startsWith('data:image/') || img.startsWith('http')) + + console.log(`📁 [API] 이미지 ${index + 1} 유효성:`, { + isValid, + reason: !img ? 'null/undefined' : + typeof img !== 'string' ? 'not string' : + img.length <= 100 ? 'too short' : + !img.startsWith('data:image/') && !img.startsWith('http') ? 'invalid format' : + 'valid' + }) + + return isValid + }) + + console.log('📁 [API] 필터링 결과:', { + 원본개수: posterData.images.length, + 유효개수: processedImages.length, + 제거된개수: posterData.images.length - processedImages.length + }) + + if (processedImages.length === 0) { + console.error('❌ [API] 유효한 이미지가 없습니다!') + console.error('❌ [API] 원본 이미지 상태:', posterData.images.map((img, i) => ({ + index: i, + type: typeof img, + length: img?.length, + preview: typeof img === 'string' ? img.substring(0, 30) : 'not string' + }))) + + throw new Error('유효한 이미지가 없습니다. 이미지를 다시 선택해 주세요.') + } + } else { + console.warn('⚠️ [API] 이미지가 없거나 유효하지 않음!') + console.warn('⚠️ [API] posterData.images:', posterData.images) + processedImages = [] + } + + // ✅ 2. 필수 필드 검증 강화 + const validationErrors = [] + + if (!posterData.title || posterData.title.trim() === '') { + validationErrors.push('제목은 필수입니다.') + } + + if (!posterData.targetAudience) { + validationErrors.push('홍보 대상은 필수입니다.') + } + + if (processedImages.length === 0) { + validationErrors.push('포스터 생성을 위해서는 최소 1개의 유효한 이미지가 필요합니다.') + } + + if (validationErrors.length > 0) { + console.error('❌ [API] 유효성 검사 실패:', validationErrors) + throw new Error(validationErrors.join(' ')) + } + + // ✅ 3. 실제 전달받은 데이터만 사용 (백엔드 API 스펙에 맞춤) + const requestData = {} + + // 필수 필드들 (값이 있을 때만 추가) + if (posterData.storeId !== undefined && posterData.storeId !== null) { + requestData.storeId = posterData.storeId + } + + if (posterData.title) { + requestData.title = posterData.title.trim() + } + + if (posterData.targetAudience || posterData.targetType) { + requestData.targetAudience = posterData.targetAudience || posterData.targetType + } + + if (posterData.promotionStartDate) { + requestData.promotionStartDate = posterData.promotionStartDate + } + + if (posterData.promotionEndDate) { + requestData.promotionEndDate = posterData.promotionEndDate + } + + // 선택적 필드들 (값이 있을 때만 추가) + if (posterData.eventName) { + requestData.eventName = posterData.eventName + } + + if (posterData.imageStyle) { + requestData.imageStyle = posterData.imageStyle + } + + if (posterData.promotionType || posterData.targetType) { + requestData.promotionType = posterData.promotionType || posterData.targetType + } + + if (posterData.emotionIntensity) { + requestData.emotionIntensity = posterData.emotionIntensity + } + + // 이미지는 검증된 것만 포함 + requestData.images = processedImages + + if (posterData.category) { + requestData.category = posterData.category + } + + if (posterData.requirement || posterData.requirements) { + requestData.requirement = posterData.requirement || posterData.requirements + } + + if (posterData.toneAndManner) { + requestData.toneAndManner = posterData.toneAndManner + } + + if (posterData.startDate) { + requestData.startDate = posterData.startDate + } + + if (posterData.endDate) { + requestData.endDate = posterData.endDate + } + + if (posterData.photoStyle) { + requestData.photoStyle = posterData.photoStyle + } + + if (posterData.targetAge) { + requestData.targetAge = posterData.targetAge + } + + console.log('📝 [API] 최종 요청 데이터 구성 완료:') + console.log('📝 [API] 제목:', requestData.title) + console.log('📝 [API] 홍보대상:', requestData.targetAudience) + console.log('📝 [API] 이미지개수:', requestData.images.length) + console.log('📝 [API] 첫번째이미지크기:', requestData.images[0]?.length, 'chars') + console.log('📝 [API] 매장ID:', requestData.storeId) + console.log('📝 [API] 타겟연령:', requestData.targetAge) + + // ✅ 4. 최종 요청 데이터 검증 + if (!requestData.images || requestData.images.length === 0) { + throw new Error('처리된 이미지가 없습니다. 이미지 업로드를 다시 시도해 주세요.') + } + + // JSON 직렬화 테스트 + try { + const testJson = JSON.stringify(requestData) + console.log('📝 [API] JSON 직렬화 테스트 성공, 크기:', Math.round(testJson.length / 1024), 'KB') + } catch (jsonError) { + console.error('❌ [API] JSON 직렬화 실패:', jsonError) + throw new Error('요청 데이터 직렬화에 실패했습니다.') + } + + console.log('🚀 [API] 백엔드 API 호출 시작:', '/poster/generate') + + // ✅ 5. 실제 백엔드 API 호출 (타임아웃 증가) + const response = await contentApi.post('/poster/generate', requestData, { + timeout: 60000, // 60초로 증가 (포스터 생성은 시간이 걸림) + headers: { + 'Content-Type': 'application/json' + } + }) + + console.log('✅ [API] 포스터 생성 응답 수신:', { + status: response.status, + hasData: !!response.data, + dataType: typeof response.data + }) + console.log('✅ [API] 응답 데이터:', response.data) + + // ✅ 6. 백엔드 응답 구조에 맞춰 처리 + if (response.data && response.data.success !== false) { + return formatSuccessResponse(response.data, '홍보 포스터가 생성되었습니다.') + } else { + throw new Error(response.data?.message || '포스터 생성에 실패했습니다.') + } + + } catch (error) { + console.error('❌ [API] 포스터 생성 실패:', error) + + // ✅ 7. 백엔드 오류 상세 정보 추출 및 분석 + if (error.response) { + console.error('❌ [API] HTTP 응답 오류:') + console.error(' - Status:', error.response.status) + console.error(' - Status Text:', error.response.statusText) + console.error(' - Headers:', error.response.headers) + console.error(' - Data:', JSON.stringify(error.response.data, null, 2)) + + // 백엔드에서 반환하는 구체적인 오류 메시지 추출 + let backendMessage = '서버 오류가 발생했습니다.' + + if (error.response.data) { + if (typeof error.response.data === 'string') { + backendMessage = error.response.data + } else if (error.response.data.message) { + backendMessage = error.response.data.message + } else if (error.response.data.error) { + backendMessage = error.response.data.error + } else if (error.response.data.detail) { + backendMessage = error.response.data.detail + } + } + + console.error('❌ [API] 백엔드 오류 메시지:', backendMessage) + + // 특정 오류 코드별 처리 + if (error.response.status === 400) { + if (backendMessage.includes('이미지') || backendMessage.includes('image')) { + backendMessage = '이미지 처리 중 오류가 발생했습니다. 이미지를 다시 선택해 주세요.' + } + } else if (error.response.status === 413) { + backendMessage = '이미지 파일이 너무 큽니다. 더 작은 이미지를 선택해 주세요.' + } else if (error.response.status === 500) { + backendMessage = '서버에서 포스터 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.' + } + + return { + success: false, + message: backendMessage, + error: error.response.data, + statusCode: error.response.status + } + } else if (error.request) { + console.error('❌ [API] 네트워크 요청 오류:', error.request) + return { + success: false, + message: '서버에 연결할 수 없습니다. 네트워크 연결을 확인해 주세요.', + error: 'NETWORK_ERROR' + } + } else { + console.error('❌ [API] 일반 오류:', error.message) + return { + success: false, + message: error.message || '포스터 생성 중 예상치 못한 오류가 발생했습니다.', + error: 'UNKNOWN_ERROR' + } + } + } + } + + /** + * 통합 콘텐츠 생성 (타입에 따라 SNS 또는 포스터 생성) - 수정된 버전 + * @param {Object} contentData - 콘텐츠 생성 데이터 + * @returns {Promise} 생성 결과 + */ + async generateContent(contentData) { + console.log('🎯 [API] 통합 콘텐츠 생성:', contentData) + + // ✅ contentData 유효성 검사 추가 + if (!contentData || typeof contentData !== 'object') { + console.error('❌ [API] contentData가 유효하지 않음:', contentData) + return { + success: false, + message: '콘텐츠 데이터가 유효하지 않습니다.', + error: 'INVALID_CONTENT_DATA' + } + } + + // ✅ images 속성 보장 + if (!Array.isArray(contentData.images)) { + console.warn('⚠️ [API] images 속성이 배열이 아님, 빈 배열로 초기화:', contentData.images) + contentData.images = [] + } + + if (contentData.contentType === 'poster' || contentData.type === 'poster') { + return await this.generatePoster(contentData) + } else { + return await this.generateSnsContent(contentData) + } + } + + /** + * SNS 콘텐츠 저장 (CON-010: SNS 게시물 저장) + * @param {Object} saveData - 저장할 콘텐츠 데이터 * @returns {Promise} 저장 결과 */ async saveSnsContent(saveData) { try { - const response = await contentApi.post('/sns/save', { - title: saveData.title, - content: saveData.content, - hashtags: saveData.hashtags, - platform: saveData.platform, - category: saveData.category, - toneAndManner: saveData.toneAndManner, - targetAudience: saveData.targetAudience, - promotionalType: saveData.promotionalType, - eventName: saveData.eventName, - eventDate: saveData.eventDate, - status: saveData.status || 'DRAFT' - }) - + const requestData = {} + + if (saveData.contentId) requestData.contentId = saveData.contentId + if (saveData.storeId !== undefined) requestData.storeId = saveData.storeId + if (saveData.platform) requestData.platform = saveData.platform + if (saveData.title) requestData.title = saveData.title + if (saveData.content) requestData.content = saveData.content + if (saveData.hashtags) requestData.hashtags = saveData.hashtags + if (saveData.images) requestData.images = saveData.images + if (saveData.finalTitle) requestData.finalTitle = saveData.finalTitle + if (saveData.finalContent) requestData.finalContent = saveData.finalContent + if (saveData.status) requestData.status = saveData.status + if (saveData.category) requestData.category = saveData.category + if (saveData.requirement) requestData.requirement = saveData.requirement + if (saveData.toneAndManner) requestData.toneAndManner = saveData.toneAndManner + if (saveData.emotionIntensity || saveData.emotionalIntensity) { + requestData.emotionIntensity = saveData.emotionIntensity || saveData.emotionalIntensity + } + if (saveData.eventName) requestData.eventName = saveData.eventName + if (saveData.startDate) requestData.startDate = saveData.startDate + if (saveData.endDate) requestData.endDate = saveData.endDate + if (saveData.promotionalType) requestData.promotionalType = saveData.promotionalType + if (saveData.eventDate) requestData.eventDate = saveData.eventDate + + const response = await contentApi.post('/sns/save', requestData) return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.') } catch (error) { return handleApiError(error) @@ -66,76 +695,36 @@ class ContentService { } /** - * 홍보 포스터 생성 (CON-015: 홍보 포스터 생성) - * @param {Object} posterData - 포스터 생성 정보 - * @returns {Promise} 생성된 포스터 - */ - async generatePoster(posterData) { - try { - const response = await contentApi.post('/poster/generate', { - storeId: posterData.storeId, - title: posterData.title, - targetType: posterData.targetType, - eventName: posterData.eventName, - eventDate: posterData.eventDate, - discountInfo: posterData.discountInfo, - designStyle: posterData.designStyle, - colorScheme: posterData.colorScheme, - includeQrCode: posterData.includeQrCode || false, - includeContact: posterData.includeContact || true, - imageStyle: posterData.imageStyle || posterData.photoStyle, - layoutType: posterData.layoutType, - sizes: posterData.sizes || ['1:1', '9:16', '16:9'] - }) - - return formatSuccessResponse(response.data.data, '홍보 포스터가 생성되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 홍보 포스터 저장 (CON-016: 홍보 포스터 저장) - * @param {Object} saveData - 저장할 포스터 정보 + * 포스터 저장 (CON-015: 포스터 저장) + * @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, '콘텐츠 목록을 조회했습니다.') + const requestData = {} + + if (saveData.contentId) requestData.contentId = saveData.contentId + if (saveData.storeId !== undefined) requestData.storeId = saveData.storeId + if (saveData.title) requestData.title = saveData.title + if (saveData.content) requestData.content = saveData.content + if (saveData.images) requestData.images = saveData.images + if (saveData.status) requestData.status = saveData.status + if (saveData.category) requestData.category = saveData.category + if (saveData.requirement) requestData.requirement = saveData.requirement + if (saveData.toneAndManner) requestData.toneAndManner = saveData.toneAndManner + if (saveData.emotionIntensity) requestData.emotionIntensity = saveData.emotionIntensity + if (saveData.eventName) requestData.eventName = saveData.eventName + if (saveData.startDate) requestData.startDate = saveData.startDate + if (saveData.endDate) requestData.endDate = saveData.endDate + if (saveData.photoStyle) requestData.photoStyle = saveData.photoStyle + if (saveData.targetAudience) requestData.targetAudience = saveData.targetAudience + if (saveData.promotionType) requestData.promotionType = saveData.promotionType + if (saveData.imageStyle) requestData.imageStyle = saveData.imageStyle + if (saveData.promotionStartDate) requestData.promotionStartDate = saveData.promotionStartDate + if (saveData.promotionEndDate) requestData.promotionEndDate = saveData.promotionEndDate + + const response = await contentApi.post('/poster/save', requestData) + return formatSuccessResponse(response.data.data, '포스터가 저장되었습니다.') } catch (error) { return handleApiError(error) } @@ -149,7 +738,6 @@ class ContentService { async getOngoingContents(period = 'month') { try { const response = await contentApi.get(`/ongoing?period=${period}`) - return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.') } catch (error) { return handleApiError(error) @@ -164,7 +752,6 @@ class ContentService { async getContentDetail(contentId) { try { const response = await contentApi.get(`/${contentId}`) - return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.') } catch (error) { return handleApiError(error) @@ -172,22 +759,29 @@ class ContentService { } /** - * 콘텐츠 수정 + * 콘텐츠 수정 (CON-024: 콘텐츠 수정) * @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 - }) - + const requestData = {} + + if (updateData.title) requestData.title = updateData.title + if (updateData.content) requestData.content = updateData.content + if (updateData.hashtags) requestData.hashtags = updateData.hashtags + if (updateData.startDate) requestData.startDate = updateData.startDate + if (updateData.endDate) requestData.endDate = updateData.endDate + if (updateData.status) requestData.status = updateData.status + if (updateData.category) requestData.category = updateData.category + if (updateData.requirement) requestData.requirement = updateData.requirement + if (updateData.toneAndManner) requestData.toneAndManner = updateData.toneAndManner + if (updateData.emotionIntensity) requestData.emotionIntensity = updateData.emotionIntensity + if (updateData.eventName) requestData.eventName = updateData.eventName + if (updateData.images) requestData.images = updateData.images + + const response = await contentApi.put(`/${contentId}`, requestData) return formatSuccessResponse(response.data.data, '콘텐츠가 수정되었습니다.') } catch (error) { return handleApiError(error) @@ -202,13 +796,166 @@ class ContentService { async deleteContent(contentId) { try { await contentApi.delete(`/${contentId}`) - return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.') } catch (error) { return handleApiError(error) } } + + /** + * 타겟 타입을 카테고리로 매핑 + * @param {string} targetType - 타겟 타입 + * @returns {string} 매핑된 카테고리 + */ + mapTargetToCategory(targetType) { + const mapping = { + 'new_menu': '메뉴소개', + 'discount': '이벤트', + 'store': '인테리어', + 'event': '이벤트', + 'menu': '메뉴소개', + 'service': '서비스' + } + return mapping[targetType] || '이벤트' + } + + /** + * 콘텐츠 검색 (추가 기능) + * @param {string} query - 검색어 + * @param {Object} filters - 필터 조건 + * @returns {Promise} 검색 결과 + */ + async searchContents(query, filters = {}) { + try { + const queryParams = new URLSearchParams() + if (query) queryParams.append('search', query) + 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) + if (filters.page) queryParams.append('page', filters.page) + if (filters.size) queryParams.append('size', filters.size) + + const queryString = queryParams.toString() + const url = queryString ? `/search?${queryString}` : '/search' + + const response = await contentApi.get(url) + return formatSuccessResponse(response.data.data, '콘텐츠 검색을 완료했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 통계 조회 (추가 기능) + * @param {Object} filters - 필터 조건 + * @returns {Promise} 통계 데이터 + */ + async getContentStats(filters = {}) { + try { + const queryParams = new URLSearchParams() + if (filters.period) queryParams.append('period', filters.period) + if (filters.storeId) queryParams.append('storeId', filters.storeId) + + const queryString = queryParams.toString() + const url = queryString ? `/stats?${queryString}` : '/stats' + + const response = await contentApi.get(url) + return formatSuccessResponse(response.data.data, '콘텐츠 통계를 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 복제 (추가 기능) + * @param {number} contentId - 복제할 콘텐츠 ID + * @returns {Promise} 복제 결과 + */ + async duplicateContent(contentId) { + try { + const response = await contentApi.post(`/${contentId}/duplicate`) + return formatSuccessResponse(response.data.data, '콘텐츠가 복제되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 상태 변경 (추가 기능) + * @param {number} contentId - 콘텐츠 ID + * @param {string} status - 변경할 상태 + * @returns {Promise} 상태 변경 결과 + */ + async updateContentStatus(contentId, status) { + try { + const response = await contentApi.patch(`/${contentId}/status`, { status }) + return formatSuccessResponse(response.data.data, `콘텐츠 상태가 ${status}로 변경되었습니다.`) + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 즐겨찾기 토글 (추가 기능) + * @param {number} contentId - 콘텐츠 ID + * @returns {Promise} 즐겨찾기 토글 결과 + */ + async toggleContentFavorite(contentId) { + try { + const response = await contentApi.post(`/${contentId}/favorite`) + return formatSuccessResponse(response.data.data, '즐겨찾기가 변경되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 템플릿 목록 조회 (추가 기능) + * @param {string} type - 템플릿 타입 + * @returns {Promise} 템플릿 목록 + */ + async getContentTemplates(type = 'all') { + try { + const response = await contentApi.get(`/templates?type=${type}`) + return formatSuccessResponse(response.data.data, '콘텐츠 템플릿을 조회했습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 템플릿으로 콘텐츠 생성 (추가 기능) + * @param {number} templateId - 템플릿 ID + * @param {Object} customData - 커스터마이징 데이터 + * @returns {Promise} 생성 결과 + */ + async generateFromTemplate(templateId, customData = {}) { + try { + const response = await contentApi.post(`/templates/${templateId}/generate`, customData) + return formatSuccessResponse(response.data.data, '템플릿으로 콘텐츠가 생성되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 콘텐츠 저장 (통합) + * @param {Object} saveData - 저장할 콘텐츠 데이터 + * @returns {Promise} 저장 결과 + */ + async saveContent(saveData) { + if (saveData.contentType === 'poster' || saveData.type === 'poster') { + return await this.savePoster(saveData) + } else { + return await this.saveSnsContent(saveData) + } + } } -export const contentService = new ContentService() +// 서비스 인스턴스 생성 및 내보내기 +const contentService = new ContentService() + +// API 인스턴스와 유틸리티 함수도 함께 내보내기 +export { contentApi, handleApiError, formatSuccessResponse } export default contentService \ No newline at end of file diff --git a/src/services/store.js b/src/services/store.js index ef880b8..94abee9 100644 --- a/src/services/store.js +++ b/src/services/store.js @@ -140,8 +140,10 @@ class StoreService { businessType: storeData.businessType, address: storeData.address, phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours, - closedDays: storeData.closedDays, + // ✅ 수정: businessHours 필드 처리 + businessHours: storeData.businessHours || `${storeData.openTime || '09:00'}-${storeData.closeTime || '21:00'}`, + // ✅ 수정: closedDays 필드 처리 + closedDays: storeData.closedDays || storeData.holidays || '', seatCount: parseInt(storeData.seatCount) || 0, instaAccounts: storeData.instaAccounts || '', blogAccounts: storeData.blogAccounts || '', @@ -150,9 +152,9 @@ class StoreService { console.log('백엔드 전송 데이터:', requestData) - // PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음) - const response = await storeApi.put('/', requestData) - + // ✅ 핵심 수정: 슬래시 제거하고 빈 문자열 사용 + console.log('API 호출 경로: PUT /api/store (baseURL + 빈 문자열)') + const response = await storeApi.put('', requestData) console.log('매장 정보 수정 API 응답:', response.data) if (response.data && (response.data.status === 200 || response.data.success !== false)) { diff --git a/src/store/content.js b/src/store/content.js index a1430e9..0694d74 100644 --- a/src/store/content.js +++ b/src/store/content.js @@ -1,32 +1,1007 @@ -//* src/store/content.js 수정 - 기존 구조 유지하고 API 연동만 추가 +//* src/store/content.js - 두 파일 완전 통합 버전 (Part 1) import { defineStore } from 'pinia' -import { ref, computed } from 'vue' +import { ref, computed, readonly } from 'vue' import contentService from '@/services/content' +import { useAuthStore } from '@/store/auth' + +// constants가 없는 경우를 위한 기본값 +const PLATFORM_SPECS = { + INSTAGRAM: { name: '인스타그램', maxLength: 2200 }, + NAVER_BLOG: { name: '네이버 블로그', maxLength: 10000 }, + POSTER: { name: '포스터', maxLength: 500 } +} + +const PLATFORM_LABELS = { + INSTAGRAM: '인스타그램', + NAVER_BLOG: '네이버 블로그', + POSTER: '포스터' +} export const useContentStore = defineStore('content', () => { - // 기존 상태들 유지 + // ===== 상태 관리 ===== + // 기본 상태 const contentList = ref([]) + const contents = ref([]) // ContentManagementView에서 사용하는 속성 const ongoingContents = ref([]) const selectedContent = ref(null) const generatedContent = ref(null) + const totalCount = ref(0) + + // 로딩 상태 const isLoading = ref(false) + const loading = ref(false) + const generating = ref(false) + + // 필터 상태 + const filters = ref({ + contentType: '', + platform: '', + period: '', + sortBy: 'latest' + }) + + // 페이지네이션 + const pagination = ref({ + page: 1, + itemsPerPage: 10 + }) - // 기존 computed 속성들 유지 + // ===== Computed 속성들 ===== const contentCount = computed(() => contentList.value.length) const ongoingContentCount = computed(() => ongoingContents.value.length) + + /** + * 필터링된 콘텐츠 목록 + */ + const filteredContents = computed(() => { + let filtered = [...contentList.value] + + if (filters.value.contentType) { + filtered = filtered.filter(content => content.type === filters.value.contentType) + } + + if (filters.value.platform) { + filtered = filtered.filter(content => content.platform === filters.value.platform) + } + + // 정렬 + const sortBy = filters.value.sortBy || 'latest' + if (sortBy.includes('_')) { + const [field, order] = sortBy.split('_') + filtered.sort((a, b) => { + let aValue = a[field] + let bValue = b[field] + + if (field === 'createdAt' || field === 'updatedAt') { + aValue = new Date(aValue) + bValue = new Date(bValue) + } + + if (order === 'desc') { + return bValue > aValue ? 1 : -1 + } else { + return aValue > bValue ? 1 : -1 + } + }) + } else if (sortBy === 'latest') { + filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + } + + return filtered + }) - // generateContent를 실제 API 호출로 수정 + /** + * 페이지네이션된 콘텐츠 목록 + */ + const paginatedContents = computed(() => { + const start = (pagination.value.page - 1) * pagination.value.itemsPerPage + const end = start + pagination.value.itemsPerPage + return filteredContents.value.slice(start, end) + }) + + /** + * 총 페이지 수 + */ + const totalPages = computed(() => { + return Math.ceil(filteredContents.value.length / pagination.value.itemsPerPage) + }) + + // ===== 매장 정보 조회 함수 (공통 유틸리티) ===== + const getStoreId = async () => { + try { + const userInfo = useAuthStore().user + console.log('사용자 정보:', userInfo) + + // 매장 정보 API 호출 + const storeApiUrl = (window.__runtime_config__ && window.__runtime_config__.STORE_URL) + ? window.__runtime_config__.STORE_URL + : 'http://localhost:8082/api/store' + + console.log('매장 API URL:', storeApiUrl) + + const token = localStorage.getItem('accessToken') || localStorage.getItem('auth_token') || localStorage.getItem('token') + const storeResponse = await fetch(`${storeApiUrl}`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }) + + if (storeResponse.ok) { + const storeData = await storeResponse.json() + const storeId = storeData.data?.storeId + console.log('✅ 매장 정보 조회 성공, storeId:', storeId) + return storeId + } else { + throw new Error(`매장 정보 조회 실패: ${storeResponse.status}`) + } + } catch (error) { + console.error('❌ 매장 정보 조회 실패:', error) + throw new Error('매장 정보를 조회할 수 없습니다.') + } + } + + // ===== 콘텐츠 목록 조회 ===== + /** + * 콘텐츠 목록 로딩 (ContentManagementView에서 사용) + */ + const loadContents = async (requestFilters = {}) => { + console.log('=== 콘텐츠 목록 조회 시작 ===') + isLoading.value = true + loading.value = true + + try { + // 1단계: 매장 정보 조회하여 실제 storeId 가져오기 + const storeId = await getStoreId() + + if (!storeId) { + throw new Error('매장 ID를 찾을 수 없습니다.') + } + + console.log('조회된 storeId:', storeId) + + // 2단계: 조회된 storeId로 콘텐츠 목록 조회 + const apiFilters = { + platform: requestFilters.platform || filters.value.platform || null, + storeId: storeId, + sortBy: requestFilters.sortBy || filters.value.sortBy || 'latest' + } + + console.log('API 요청 필터:', apiFilters) + + const result = await contentService.getContents(apiFilters) + + console.log('🔍 contentService.getContents 결과:', result) + console.log('🔍 result.success:', result.success) + console.log('🔍 result.data:', result.data) + console.log('🔍 result.data 타입:', typeof result.data) + console.log('🔍 result.data 길이:', result.data?.length) + + if (result.success) { + const responseData = result.data || [] + contents.value = responseData + contentList.value = responseData + totalCount.value = responseData.length + console.log('✅ 콘텐츠 로딩 성공:', contents.value.length, '개') + return { success: true } + } else { + console.error('❌ 콘텐츠 로딩 실패:', result.error) + contents.value = [] + contentList.value = [] + totalCount.value = 0 + return { success: false, error: result.error } + } + } catch (error) { + console.error('❌ 콘텐츠 로딩 실패:', error) + contents.value = [] + contentList.value = [] + totalCount.value = 0 + return { success: false, error: error.message || '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + loading.value = false + } + } + + /** + * 콘텐츠 목록 조회 (기존 호환성 유지) + */ + const fetchContentList = async (requestFilters = {}) => { + console.log('📋 [STORE] fetchContentList 호출:', requestFilters) + return await loadContents(requestFilters) + } + + // ===== AI 콘텐츠 생성 ===== + /** + * SNS 콘텐츠 생성 (API 설계서 기준) - 이미지 디버깅 강화 + */ + const generateSnsContent = async (contentData) => { + generating.value = true + + try { + console.log('🎯 [STORE] SNS 콘텐츠 생성 요청:', contentData) + console.log('📁 [STORE] SNS 이미지 확인:', { + hasImages: !!contentData.images, + imageCount: contentData.images?.length || 0, + imageTypes: contentData.images?.map(img => typeof img) || [], + imageSizes: contentData.images?.map(img => img?.length || 'unknown') || [] + }) + + // 매장 ID 조회 (필요한 경우) + let storeId = contentData.storeId + if (!storeId) { + try { + storeId = await getStoreId() + } catch (error) { + console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error) + storeId = 1 + } + } + + // SnsContentCreateRequest 구조에 맞게 데이터 변환 + const requestData = { + storeId: storeId, + storeName: contentData.storeName || '', + storeType: contentData.storeType || '', + platform: contentData.platform || 'INSTAGRAM', + title: contentData.title || 'SNS 게시물', + category: contentData.category || '메뉴소개', + requirement: contentData.requirement || contentData.requirements || 'SNS 게시물을 생성해주세요', + target: contentData.target || contentData.targetAudience || '', + contentType: contentData.contentType || 'SNS 게시물', + eventName: contentData.eventName || '', + startDate: contentData.startDate, + endDate: contentData.endDate, + images: contentData.images || [], + photoStyle: contentData.photoStyle || '밝고 화사한', + targetAge: contentData.targetAge || '20대', + toneAndManner: contentData.toneAndManner || '친근함', + emotionalIntensity: contentData.emotionalIntensity || contentData.emotionIntensity || '보통', + promotionalType: contentData.promotionalType || contentData.promotionType || '', + eventDate: contentData.eventDate, + hashtagStyle: contentData.hashtagStyle || '', + hashtagCount: contentData.hashtagCount || 10, + contentLength: contentData.contentLength || '보통', + includeHashtags: contentData.includeHashtags !== false, + includeEmojis: contentData.includeEmojis !== false, + includeEmoji: contentData.includeEmoji !== false, + includeCallToAction: contentData.includeCallToAction !== false, + includeLocation: contentData.includeLocation || false, + forInstagramStory: contentData.forInstagramStory || false, + forNaverBlogPost: contentData.forNaverBlogPost || false, + alternativeTitleCount: contentData.alternativeTitleCount || 3, + alternativeHashtagSetCount: contentData.alternativeHashtagSetCount || 2, + preferredAiModel: contentData.preferredAiModel || '' + } + + console.log('📤 [STORE] SNS 변환된 요청 데이터:', { + ...requestData, + images: `${requestData.images.length}개 이미지` + }) + + const result = await contentService.generateSnsContent(requestData) + + if (result.success) { + console.log('✅ [STORE] SNS 콘텐츠 생성 성공:', result.data) + generatedContent.value = result.data + + return { + success: true, + content: result.data.content || '콘텐츠가 생성되었습니다.', + hashtags: result.data.hashtags || [], + data: result.data + } + } else { + console.error('❌ [STORE] SNS 콘텐츠 생성 실패:', result.message) + return { + success: false, + message: result.message || 'SNS 콘텐츠 생성에 실패했습니다.', + error: result.error + } + } + + } catch (error) { + console.error('❌ [STORE] SNS 콘텐츠 생성 예외:', error) + return { + success: false, + message: error.message || '네트워크 오류가 발생했습니다.', + error: error + } + } finally { + generating.value = false + } + } + //* src/store/content.js - 두 파일 완전 통합 버전 (Part 2) + /** + * 포스터 생성 (API 설계서 기준) - 이미지 디버깅 대폭 강화 + */ + const generatePoster = async (posterData) => { + generating.value = true + + try { + console.log('🎯 [STORE] 포스터 생성 요청 받음:', posterData) + console.log('📁 [STORE] 포스터 이미지 상세 분석:', { + hasImages: !!posterData.images, + imageCount: posterData.images?.length || 0, + imageArray: Array.isArray(posterData.images), + firstImageInfo: posterData.images?.[0] ? { + type: typeof posterData.images[0], + length: posterData.images[0]?.length || 'unknown', + isBase64: posterData.images[0]?.startsWith?.('data:image/') || false, + preview: posterData.images[0]?.substring(0, 50) + '...' + } : null + }) + + // 매장 ID 조회 (필요한 경우) + let storeId = posterData.storeId + if (storeId === undefined || storeId === null) { + try { + storeId = await getStoreId() + } catch (error) { + console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error) + storeId = 1 + } + } + + // ✅ 실제 전달받은 데이터만 사용 (기본값 완전 제거) + const requestData = {} + + // 조건부로 필드 추가 (값이 있을 때만) + if (storeId !== undefined) { + requestData.storeId = storeId + console.log('📝 [STORE] storeId 추가:', requestData.storeId) + } + + if (posterData.title) { + requestData.title = posterData.title + console.log('📝 [STORE] title 추가:', requestData.title) + } + + if (posterData.targetAudience) { + requestData.targetAudience = posterData.targetAudience + console.log('📝 [STORE] targetAudience 추가:', requestData.targetAudience) + } else if (posterData.targetType) { + requestData.targetAudience = posterData.targetType + console.log('📝 [STORE] targetAudience 추가 (from targetType):', requestData.targetAudience) + } + + if (posterData.promotionStartDate) { + requestData.promotionStartDate = posterData.promotionStartDate + console.log('📝 [STORE] promotionStartDate 추가:', requestData.promotionStartDate) + } + + if (posterData.promotionEndDate) { + requestData.promotionEndDate = posterData.promotionEndDate + console.log('📝 [STORE] promotionEndDate 추가:', requestData.promotionEndDate) + } + + // 선택적 필드들 + if (posterData.eventName) { + requestData.eventName = posterData.eventName + console.log('📝 [STORE] eventName 추가:', requestData.eventName) + } + + if (posterData.imageStyle) requestData.imageStyle = posterData.imageStyle + if (posterData.promotionType) requestData.promotionType = posterData.promotionType + if (posterData.emotionIntensity) requestData.emotionIntensity = posterData.emotionIntensity + if (posterData.category) requestData.category = posterData.category + if (posterData.requirement || posterData.requirements) { + requestData.requirement = posterData.requirement || posterData.requirements + } + if (posterData.toneAndManner) requestData.toneAndManner = posterData.toneAndManner + if (posterData.startDate) requestData.startDate = posterData.startDate + if (posterData.endDate) requestData.endDate = posterData.endDate + if (posterData.photoStyle) requestData.photoStyle = posterData.photoStyle + if (posterData.targetAge) { + requestData.targetAge = posterData.targetAge + console.log('📝 [STORE] targetAge 추가:', requestData.targetAge) + } + + // ✅ 이미지 처리 - 가장 중요한 부분 + console.log('📁 [STORE] 이미지 처리 시작...') + if (posterData.images && Array.isArray(posterData.images) && posterData.images.length > 0) { + console.log('📁 [STORE] 원본 이미지 배열:', posterData.images.length, '개') + + // 유효한 이미지만 필터링 + const validImages = posterData.images.filter(img => { + const isValid = img && typeof img === 'string' && img.length > 0 + console.log('📁 [STORE] 이미지 유효성 검사:', { + isValid, + type: typeof img, + length: img?.length, + isBase64: img?.startsWith?.('data:image/') + }) + return isValid + }) + + requestData.images = validImages + console.log('📁 [STORE] 필터링된 이미지:', validImages.length, '개') + console.log('📁 [STORE] 첫 번째 이미지 샘플:', validImages[0]?.substring(0, 100) + '...') + } else { + requestData.images = [] + console.warn('📁 [STORE] ⚠️ 이미지가 없거나 유효하지 않음!') + console.warn('📁 [STORE] posterData.images:', posterData.images) + } + + console.log('🔍 [STORE] 최종 요청 데이터 확인:') + console.log(' - 제목:', requestData.title) + console.log(' - 홍보 대상:', requestData.targetAudience) + console.log(' - 타겟 연령층:', requestData.targetAge) + console.log(' - 홍보 시작일:', requestData.promotionStartDate) + console.log(' - 홍보 종료일:', requestData.promotionEndDate) + console.log(' - 이미지 개수:', requestData.images.length) + + if (requestData.images.length === 0) { + console.error('❌ [STORE] 포스터에 이미지가 없습니다!') + return { + success: false, + message: '포스터 생성을 위해서는 최소 1개의 이미지가 필요합니다.' + } + } + + const result = await contentService.generatePoster(requestData) + + if (result.success) { + console.log('✅ [STORE] 포스터 생성 성공:', result.data) + generatedContent.value = result.data + + return { + success: true, + content: result.data.posterImage || result.data.content || '포스터가 생성되었습니다.', + posterImage: result.data.posterImage, + title: result.data.title, + data: result.data + } + } else { + console.error('❌ [STORE] 포스터 생성 실패:', result.message) + return { + success: false, + message: result.message || '포스터 생성에 실패했습니다.', + error: result.error + } + } + + } catch (error) { + console.error('❌ [STORE] 포스터 생성 예외:', error) + return { + success: false, + message: error.message || '네트워크 오류가 발생했습니다.', + error: error + } + } finally { + generating.value = false + } + } + + /** + * AI 콘텐츠 생성 통합 메서드 (기존 호환성 유지) + */ const generateContent = async (type, formData) => { + console.log('🎯 [STORE] 콘텐츠 생성 요청 (통합):', { type, formData }) + console.log('📁 [STORE] 통합 메서드 이미지 확인:', { + hasImages: !!formData.images, + imageCount: formData.images?.length || 0 + }) + isLoading.value = true try { let result - if (type === 'sns') { - result = await contentService.generateSnsContent(formData) - } else if (type === 'poster') { - result = await contentService.generatePoster(formData) + + // 타입에 따라 적절한 메서드 호출 + if (type === 'poster' || formData.contentType === 'poster' || formData.type === 'poster') { + console.log('🎯 [STORE] 포스터 생성으로 라우팅') + result = await generatePoster(formData) + } else if (type === 'sns' || type === 'snsContent') { + console.log('🎯 [STORE] SNS 생성으로 라우팅') + result = await generateSnsContent(formData) + } else { + console.log('🎯 [STORE] 기본 SNS 생성으로 라우팅') + result = await generateSnsContent(formData) } + if (result.success) { + return { success: true, data: result.data || result } + } else { + return { success: false, error: result.message || result.error } + } + } catch (error) { + console.error('❌ [STORE] 통합 콘텐츠 생성 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + } + } + + // ===== 콘텐츠 저장 ===== + /** + * SNS 콘텐츠 저장 + */ + const saveSnsContent = async (saveData) => { + loading.value = true + + try { + console.log('💾 [STORE] SNS 콘텐츠 저장 요청:', saveData) + + // 매장 ID 조회 (필요한 경우) + let storeId = saveData.storeId + if (!storeId) { + try { + storeId = await getStoreId() + } catch (error) { + console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error) + storeId = 1 + } + } + + // SnsContentSaveRequest 구조에 맞게 데이터 변환 + const requestData = { + contentId: saveData.contentId, + storeId: storeId, + platform: saveData.platform || 'INSTAGRAM', + title: saveData.title || '', + content: saveData.content || '', + hashtags: saveData.hashtags || [], + images: saveData.images || [], + finalTitle: saveData.finalTitle || saveData.title || '', + finalContent: saveData.finalContent || saveData.content || '', + status: saveData.status || 'DRAFT', + category: saveData.category || '메뉴소개', + requirement: saveData.requirement || '', + toneAndManner: saveData.toneAndManner || '친근함', + emotionIntensity: saveData.emotionIntensity || saveData.emotionalIntensity || '보통', + eventName: saveData.eventName || '', + startDate: saveData.startDate, + endDate: saveData.endDate, + promotionalType: saveData.promotionalType, + eventDate: saveData.eventDate + } + + const result = await contentService.saveSnsContent(requestData) + + if (result.success) { + console.log('✅ [STORE] SNS 콘텐츠 저장 성공') + + // 목록 새로고침 + await loadContents() + + return { success: true, message: 'SNS 콘텐츠가 저장되었습니다.' } + } else { + console.error('❌ [STORE] SNS 콘텐츠 저장 실패:', result.message) + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ [STORE] SNS 콘텐츠 저장 예외:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 포스터 저장 + */ + const savePoster = async (saveData) => { + loading.value = true + + try { + console.log('💾 [STORE] 포스터 저장 요청:', saveData) + + // 매장 ID 조회 (필요한 경우) + let storeId = saveData.storeId + if (!storeId) { + try { + storeId = await getStoreId() + } catch (error) { + console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error) + storeId = 1 + } + } + + // PosterContentSaveRequest 구조에 맞게 데이터 변환 + const requestData = { + contentId: saveData.contentId, + storeId: storeId, + title: saveData.title || '', + content: saveData.content || '', + images: saveData.images || [], + status: saveData.status || 'DRAFT', + category: saveData.category || '이벤트', + requirement: saveData.requirement || '', + toneAndManner: saveData.toneAndManner || '친근함', + emotionIntensity: saveData.emotionIntensity || '보통', + eventName: saveData.eventName || '', + startDate: saveData.startDate, + endDate: saveData.endDate, + photoStyle: saveData.photoStyle || '밝고 화사한', + targetAudience: saveData.targetAudience, + promotionType: saveData.promotionType, + imageStyle: saveData.imageStyle, + promotionStartDate: saveData.promotionStartDate, + promotionEndDate: saveData.promotionEndDate + } + + const result = await contentService.savePoster(requestData) + + if (result.success) { + console.log('✅ [STORE] 포스터 저장 성공') + + // 목록 새로고침 + await loadContents() + + return { success: true, message: '포스터가 저장되었습니다.' } + } else { + console.error('❌ [STORE] 포스터 저장 실패:', result.message) + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ [STORE] 포스터 저장 예외:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 콘텐츠 저장 통합 메서드 (기존 호환성 유지) + */ + const saveContent = async (type, contentData) => { + console.log('💾 [STORE] 콘텐츠 저장 요청 (통합):', { type, contentData }) + + isLoading.value = true + + try { + let result + + // 타입에 따라 적절한 메서드 호출 + if (type === 'poster' || contentData.contentType === 'poster' || contentData.type === 'poster') { + result = await savePoster(contentData) + } else if (type === 'sns' || type === 'snsContent') { + result = await saveSnsContent(contentData) + } else { + // 기본적으로 SNS 콘텐츠로 간주 + result = await saveSnsContent(contentData) + } + + if (result.success) { + return { success: true, message: result.message || '콘텐츠가 저장되었습니다.' } + } else { + return { success: false, error: result.error } + } + } catch (error) { + console.error('❌ [STORE] 통합 콘텐츠 저장 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + } + } + + // ===== 기타 콘텐츠 관리 메서드들 ===== + /** + * 진행 중인 콘텐츠 조회 + */ + const fetchOngoingContents = async (period = 'month') => { + isLoading.value = true + loading.value = true + + try { + const result = await contentService.getOngoingContents(period) + + if (result.success) { + ongoingContents.value = result.data || [] + return { success: true } + } else { + console.error('❌ 진행 중인 콘텐츠 조회 실패:', result.message) + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 진행 중인 콘텐츠 조회 예외:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + loading.value = false + } + } + + /** + * 콘텐츠 상세 조회 + */ + const fetchContentDetail = async (contentId) => { + loading.value = true + + try { + const result = await contentService.getContentDetail(contentId) + + if (result.success) { + selectedContent.value = result.data + return result.data + } else { + throw new Error(result.message || '콘텐츠 상세 조회에 실패했습니다.') + } + } catch (error) { + console.error('❌ 콘텐츠 상세 조회 실패:', error) + throw error + } finally { + loading.value = false + } + } + + /** + * 콘텐츠 수정 + */ + const updateContent = async (contentId, updateData) => { + isLoading.value = true + loading.value = true + + try { + const result = await contentService.updateContent(contentId, updateData) + + if (result.success) { + await loadContents() + return { success: true, message: '콘텐츠가 수정되었습니다.' } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 콘텐츠 수정 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + loading.value = false + } + } + + /** + * 콘텐츠 삭제 + */ + const deleteContent = async (contentId) => { + isLoading.value = true + loading.value = true + + try { + const result = await contentService.deleteContent(contentId) + + if (result.success) { + // 목록에서 제거 + contentList.value = contentList.value.filter(content => content.id !== contentId) + contents.value = contents.value.filter(content => content.id !== contentId) + totalCount.value = contentList.value.length + + await loadContents() // 최신 목록으로 새로고침 + return { success: true, message: '콘텐츠가 삭제되었습니다.' } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 콘텐츠 삭제 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + loading.value = false + } + } + + // ===== 유틸리티 메서드들 ===== + /** + * 타겟 타입을 카테고리로 매핑 + */ + const mapTargetToCategory = (targetType) => { + const mapping = { + 'new_menu': '메뉴소개', + 'discount': '이벤트', + 'store': '인테리어', + 'event': '이벤트', + 'menu': '메뉴소개', + 'service': '서비스' + } + return mapping[targetType] || '메뉴소개' + } + + /** + * 플랫폼별 특성 조회 + */ + const getPlatformSpec = (platform) => { + return PLATFORM_SPECS?.[platform] || null + } + + /** + * 플랫폼 유효성 검사 + */ + const validatePlatform = (platform) => { + return PLATFORM_SPECS ? Object.keys(PLATFORM_SPECS).includes(platform) : true + } + + /** + * 필터 설정 + */ + const setFilters = (newFilters) => { + filters.value = { ...filters.value, ...newFilters } + pagination.value.page = 1 // 필터 변경 시 첫 페이지로 + } + + /** + * 페이지네이션 설정 + */ + const setPagination = (newPagination) => { + pagination.value = { ...pagination.value, ...newPagination } + } + + /** + * 상태 초기화 + */ + const reset = () => { + contentList.value = [] + contents.value = [] + ongoingContents.value = [] + selectedContent.value = null + generatedContent.value = null + totalCount.value = 0 + + filters.value = { + contentType: '', + platform: '', + period: '', + sortBy: 'latest' + } + + pagination.value = { + page: 1, + itemsPerPage: 10 + } + + isLoading.value = false + loading.value = false + generating.value = false + } + + // ===== 고급 기능들 (추가) ===== + /** + * 콘텐츠 검색 + */ + const searchContents = async (query, searchFilters = {}) => { + loading.value = true + + try { + const result = await contentService.searchContents(query, searchFilters) + + if (result.success) { + contentList.value = result.data || [] + contents.value = result.data || [] + totalCount.value = result.data?.length || 0 + return { success: true } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 콘텐츠 검색 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 콘텐츠 통계 조회 + */ + const getContentStats = async (statsFilters = {}) => { + loading.value = true + + try { + const result = await contentService.getContentStats(statsFilters) + + if (result.success) { + return { success: true, data: result.data } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 콘텐츠 통계 조회 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 콘텐츠 복제 + */ + const duplicateContent = async (contentId) => { + loading.value = true + + try { + const result = await contentService.duplicateContent(contentId) + + if (result.success) { + await loadContents() // 목록 새로고침 + return { success: true, message: '콘텐츠가 복제되었습니다.' } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 콘텐츠 복제 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 콘텐츠 상태 변경 + */ + const updateContentStatus = async (contentId, status) => { + loading.value = true + + try { + const result = await contentService.updateContentStatus(contentId, status) + + if (result.success) { + await loadContents() // 목록 새로고침 + return { success: true, message: `콘텐츠 상태가 ${status}로 변경되었습니다.` } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 콘텐츠 상태 변경 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 콘텐츠 즐겨찾기 토글 + */ + const toggleContentFavorite = async (contentId) => { + loading.value = true + + try { + const result = await contentService.toggleContentFavorite(contentId) + + if (result.success) { + await loadContents() // 목록 새로고침 + return { success: true, message: '즐겨찾기가 변경되었습니다.' } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 즐겨찾기 토글 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 콘텐츠 템플릿 목록 조회 + */ + const getContentTemplates = async (type = 'all') => { + loading.value = true + + try { + const result = await contentService.getContentTemplates(type) + + if (result.success) { + return { success: true, data: result.data } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 템플릿 목록 조회 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + loading.value = false + } + } + + /** + * 템플릿으로 콘텐츠 생성 + */ + const generateFromTemplate = async (templateId, customData = {}) => { + generating.value = true + + try { + const result = await contentService.generateFromTemplate(templateId, customData) + if (result.success) { generatedContent.value = result.data return { success: true, data: result.data } @@ -34,135 +1009,70 @@ export const useContentStore = defineStore('content', () => { return { success: false, error: result.message } } } catch (error) { + console.error('❌ 템플릿 콘텐츠 생성 실패:', error) return { success: false, error: '네트워크 오류가 발생했습니다.' } } finally { - isLoading.value = false - } - } - - // saveContent를 실제 API 호출로 수정 - const saveContent = async (type, contentData) => { - isLoading.value = true - - try { - let result - if (type === 'sns') { - result = await contentService.saveSnsContent(contentData) - } else if (type === 'poster') { - result = await contentService.savePoster(contentData) - } - - if (result.success) { - // 콘텐츠 목록 새로고침 - await fetchContentList() - return { success: true, message: '콘텐츠가 저장되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // fetchContentList를 실제 API 호출로 수정 - const fetchContentList = async (filters = {}) => { - isLoading.value = true - - try { - const result = await contentService.getContents(filters) - - if (result.success) { - contentList.value = result.data - return { success: true } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // fetchOngoingContents를 실제 API 호출로 수정 - const fetchOngoingContents = async (period = 'month') => { - isLoading.value = true - - try { - const result = await contentService.getOngoingContents(period) - - if (result.success) { - ongoingContents.value = result.data - return { success: true } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // 콘텐츠 수정/삭제 메서드 추가 - const updateContent = async (contentId, updateData) => { - isLoading.value = true - - try { - const result = await contentService.updateContent(contentId, updateData) - - if (result.success) { - await fetchContentList() - return { success: true, message: '콘텐츠가 수정되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - const deleteContent = async (contentId) => { - isLoading.value = true - - try { - const result = await contentService.deleteContent(contentId) - - if (result.success) { - await fetchContentList() - return { success: true, message: '콘텐츠가 삭제되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false + generating.value = false } } + // ===== 반환할 store 객체 ===== return { // 상태 - contentList, - ongoingContents, - selectedContent, - generatedContent, - isLoading, + contentList: readonly(contentList), + contents: readonly(contents), // ContentManagementView에서 사용 + ongoingContents: readonly(ongoingContents), + selectedContent: readonly(selectedContent), + generatedContent: readonly(generatedContent), + totalCount: readonly(totalCount), + isLoading: readonly(isLoading), + loading: readonly(loading), + generating: readonly(generating), + filters: readonly(filters), + pagination: readonly(pagination), // 컴퓨티드 contentCount, ongoingContentCount, + filteredContents, + paginatedContents, + totalPages, - // 메서드 - generateContent, - saveContent, - fetchContentList, + // 콘텐츠 목록 조회 + loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함) + fetchContentList, // 기존 호환성 유지 + + // AI 콘텐츠 생성 + generateContent, // 통합 메서드 (타입에 따라 라우팅) + generateSnsContent, // SNS 전용 + generatePoster, // 포스터 전용 + + // 콘텐츠 저장 + saveContent, // 통합 메서드 (타입에 따라 라우팅) + saveSnsContent, // SNS 전용 + savePoster, // 포스터 전용 + + // 기본 CRUD fetchOngoingContents, + fetchContentDetail, updateContent, - deleteContent + deleteContent, + + // 유틸리티 + mapTargetToCategory, + getPlatformSpec, + validatePlatform, + setFilters, + setPagination, + reset, + + // 고급 기능 + searchContents, + getContentStats, + duplicateContent, + updateContentStatus, + toggleContentFavorite, + getContentTemplates, + generateFromTemplate } }) \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index 8ff224a..98da823 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -79,80 +79,88 @@ export const useStoreStore = defineStore('store', { } }, - /** - * 메뉴 목록 조회 - 실제 API 연동 (매장 ID 필요) - ✅ ID 필드 보장 - */ - async fetchMenus() { - console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===') + // src/store/index.js에서 fetchMenus 부분만 수정 + +/** + * 메뉴 목록 조회 - 실제 API 연동 (매장 ID 필요) - ✅ 이미지 필드 매핑 수정 + */ +async fetchMenus() { + console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===') + + try { + // 매장 정보에서 storeId 가져오기 + const storeId = this.storeInfo?.storeId + if (!storeId) { + console.warn('매장 ID가 없습니다. 매장 정보를 먼저 조회해주세요.') + return { success: false, message: '매장 정보가 필요합니다', data: [] } + } + + // 메뉴 서비스 임포트 + const { menuService } = await import('@/services/menu') + + console.log('메뉴 목록 API 호출, 매장 ID:', storeId) + const result = await menuService.getMenus(storeId) + + console.log('=== Store 스토어: 메뉴 API 응답 분석 ===') + console.log('Result:', result) + console.log('Result.success:', result.success) + console.log('Result.data:', result.data) + console.log('Result.message:', result.message) + + if (result.success && result.data) { + // ✅ 백엔드 MenuResponse의 필드명에 맞게 매핑 수정 + const menusWithId = (result.data || []).map(menu => { + // ID 필드가 확실히 있도록 보장 + const menuId = menu.menuId || menu.id + + if (!menuId) { + console.warn('⚠️ 메뉴 ID가 없는 항목 발견:', menu) + } + + console.log('메뉴 원본 데이터:', menu) // 디버깅용 + + return { + ...menu, + id: menuId, // ✅ id 필드 확실히 설정 + menuId: menuId, // ✅ menuId 필드도 설정 + // 기타 필드들 보장 + menuName: menu.menuName || menu.name || '이름 없음', + category: menu.category || '기타', + price: menu.price || 0, + description: menu.description || '', + available: menu.available !== undefined ? menu.available : true, + recommended: menu.recommended !== undefined ? menu.recommended : false, + // ✅ 이미지 필드 수정: 백엔드는 'image' 필드 사용 + imageUrl: menu.image || menu.imageUrl || '/images/menu-placeholder.png', + image: menu.image || menu.imageUrl, // 백엔드 호환성 + createdAt: menu.createdAt, + updatedAt: menu.updatedAt + } + }) - try { - // 매장 정보에서 storeId 가져오기 - const storeId = this.storeInfo?.storeId - if (!storeId) { - console.warn('매장 ID가 없습니다. 매장 정보를 먼저 조회해주세요.') - return { success: false, message: '매장 정보가 필요합니다', data: [] } - } - - // 메뉴 서비스 임포트 - const { menuService } = await import('@/services/menu') - - console.log('메뉴 목록 API 호출, 매장 ID:', storeId) - const result = await menuService.getMenus(storeId) - - console.log('=== Store 스토어: 메뉴 API 응답 분석 ===') - console.log('Result:', result) - console.log('Result.success:', result.success) - console.log('Result.data:', result.data) - console.log('Result.message:', result.message) - - if (result.success && result.data) { - // ✅ 메뉴 데이터 ID 필드 보장 처리 - const menusWithId = (result.data || []).map(menu => { - // ID 필드가 확실히 있도록 보장 - const menuId = menu.menuId || menu.id - - if (!menuId) { - console.warn('⚠️ 메뉴 ID가 없는 항목 발견:', menu) - } - - return { - ...menu, - id: menuId, // ✅ id 필드 확실히 설정 - menuId: menuId, // ✅ menuId 필드도 설정 - // 기타 필드들 보장 - menuName: menu.menuName || menu.name || '이름 없음', - category: menu.category || '기타', - price: menu.price || 0, - description: menu.description || '', - available: menu.available !== undefined ? menu.available : true, - recommended: menu.recommended !== undefined ? menu.recommended : false, - imageUrl: menu.imageUrl || '/images/menu-placeholder.png' - } - }) - - // 메뉴 목록이 있는 경우 - console.log('✅ 메뉴 목록 설정 (ID 보장됨):', menusWithId) - this.menus = menusWithId - return { success: true, data: menusWithId } - } else { - // 메뉴가 없거나 조회 실패한 경우 - console.log('⚠️ 메뉴 목록 없음 또는 조회 실패') - this.menus = [] - - if (result.message === '등록된 메뉴가 없습니다') { - return { success: false, message: '등록된 메뉴가 없습니다', data: [] } - } else { - return { success: false, message: result.message || '메뉴 목록 조회에 실패했습니다', data: [] } - } - } - } catch (error) { - console.error('=== Store 스토어: 메뉴 목록 조회 실패 ===') - console.error('Error:', error) - - this.menus = [] - return { success: false, message: error.message || '메뉴 목록을 불러오는데 실패했습니다', data: [] } + // 메뉴 목록이 있는 경우 + console.log('✅ 메뉴 목록 설정 (이미지 필드 매핑 완료):', menusWithId) + this.menus = menusWithId + return { success: true, data: menusWithId } + } else { + // 메뉴가 없거나 조회 실패한 경우 + console.log('⚠️ 메뉴 목록 없음 또는 조회 실패') + this.menus = [] + + if (result.message === '등록된 메뉴가 없습니다') { + return { success: false, message: '등록된 메뉴가 없습니다', data: [] } + } else { + return { success: false, message: result.message || '메뉴 목록 조회에 실패했습니다', data: [] } } - }, + } + } catch (error) { + console.error('=== Store 스토어: 메뉴 목록 조회 실패 ===') + console.error('Error:', error) + + this.menus = [] + return { success: false, message: error.message || '메뉴 목록을 불러오는데 실패했습니다', data: [] } + } + }, /** * 매장 등록 diff --git a/src/store/poster.js b/src/store/poster.js new file mode 100644 index 0000000..121b477 --- /dev/null +++ b/src/store/poster.js @@ -0,0 +1,281 @@ +//* src/store/poster.js - 이미지 처리 강화 및 디버깅 추가 +import { defineStore } from 'pinia' +import { contentService } from '@/services/content' +import { useAuthStore } from '@/store/auth' + +export const usePosterStore = defineStore('poster', { + state: () => ({ + posters: [], + currentPoster: null, + loading: false, + error: null + }), + + getters: { + getPosterById: (state) => (id) => { + return state.posters.find(poster => poster.id === id) + }, + + recentPosters: (state) => { + return state.posters + .slice() + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + .slice(0, 10) + } + }, + + actions: { + /** + * 포스터 생성 - 이미지 처리 강화 + */ + async generatePoster(posterData) { + this.loading = true + this.error = null + + try { + console.log('🎯 [POSTER_STORE] 포스터 생성 요청 받음:', posterData) + console.log('📁 [POSTER_STORE] 이미지 상태 확인:', { + hasImages: !!posterData.images, + isArray: Array.isArray(posterData.images), + imageCount: posterData.images?.length || 0, + imageDetails: posterData.images?.map((img, idx) => ({ + index: idx, + type: typeof img, + length: img?.length, + isBase64: typeof img === 'string' && img.startsWith('data:image/'), + preview: typeof img === 'string' ? img.substring(0, 30) + '...' : 'not string' + })) || [] + }) + + // ✅ 이미지 전처리 및 검증 + let processedImages = [] + if (posterData.images && Array.isArray(posterData.images) && posterData.images.length > 0) { + console.log('📁 [POSTER_STORE] 이미지 전처리 시작...') + + processedImages = posterData.images + .filter((img, index) => { + const isValid = img && + typeof img === 'string' && + img.length > 100 && + (img.startsWith('data:image/') || img.startsWith('http')) + + console.log(`📁 [POSTER_STORE] 이미지 ${index + 1} 검증:`, { + isValid, + type: typeof img, + length: img?.length, + format: img?.substring(0, 20) || 'unknown' + }) + + return isValid + }) + + console.log('📁 [POSTER_STORE] 전처리 결과:', { + 원본: posterData.images.length, + 유효: processedImages.length, + 제거됨: posterData.images.length - processedImages.length + }) + + if (processedImages.length === 0) { + throw new Error('유효한 이미지가 없습니다. 이미지를 다시 업로드해 주세요.') + } + } else { + console.warn('⚠️ [POSTER_STORE] 이미지가 없습니다!') + throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.') + } + + // ✅ API 요청에 맞는 형태로 데이터 변환 - 검증된 이미지 사용 + const requestData = { + storeId: posterData.storeId, + title: posterData.title, + targetAudience: posterData.targetAudience, + promotionStartDate: posterData.promotionStartDate, + promotionEndDate: posterData.promotionEndDate, + images: processedImages, // 검증된 이미지만 사용 + targetAge: posterData.targetAge + } + + // 선택적 필드들 + if (posterData.eventName) requestData.eventName = posterData.eventName + if (posterData.imageStyle) requestData.imageStyle = posterData.imageStyle + if (posterData.promotionType) requestData.promotionType = posterData.promotionType + if (posterData.emotionIntensity) requestData.emotionIntensity = posterData.emotionIntensity + if (posterData.category) requestData.category = posterData.category + if (posterData.requirement) requestData.requirement = posterData.requirement + if (posterData.toneAndManner) requestData.toneAndManner = posterData.toneAndManner + if (posterData.startDate) requestData.startDate = posterData.startDate + if (posterData.endDate) requestData.endDate = posterData.endDate + if (posterData.photoStyle) requestData.photoStyle = posterData.photoStyle + + console.log('📤 [POSTER_STORE] 최종 요청 데이터:', { + ...requestData, + images: `${requestData.images.length}개 이미지 (${Math.round(JSON.stringify(requestData.images).length / 1024)}KB)` + }) + + // ✅ 마지막 검증 + if (!requestData.title) { + throw new Error('제목은 필수입니다.') + } + if (!requestData.targetAudience) { + throw new Error('홍보 대상은 필수입니다.') + } + if (!requestData.images || requestData.images.length === 0) { + throw new Error('이미지는 필수입니다.') + } + + console.log('🚀 [POSTER_STORE] contentService.generatePoster 호출...') + const result = await contentService.generatePoster(requestData) + + if (result.success) { + console.log('✅ [POSTER_STORE] 포스터 생성 성공:', result.data) + this.currentPoster = result.data + return result + } else { + console.error('❌ [POSTER_STORE] 포스터 생성 실패:', result.message) + this.error = result.message + return result + } + + } catch (error) { + console.error('❌ [POSTER_STORE] 포스터 생성 예외:', error) + + // 상세한 오류 정보 로깅 + if (error.response) { + console.error('❌ [POSTER_STORE] HTTP 오류:', { + status: error.response.status, + statusText: error.response.statusText, + data: error.response.data + }) + } + + this.error = error.message || '포스터 생성 중 오류가 발생했습니다.' + return { + success: false, + message: this.error + } + } finally { + this.loading = false + } + }, + + /** + * 포스터 저장 + */ + async savePoster(saveData) { + this.loading = true + this.error = null + + try { + console.log('💾 [POSTER_STORE] 포스터 저장 요청:', saveData) + + const result = await contentService.savePoster(saveData) + + if (result.success) { + console.log('✅ [POSTER_STORE] 포스터 저장 성공') + // 저장된 포스터를 목록에 추가 + if (result.data) { + this.posters.unshift(result.data) + } + return result + } else { + console.error('❌ [POSTER_STORE] 포스터 저장 실패:', result.message) + this.error = result.message + return result + } + + } catch (error) { + console.error('❌ [POSTER_STORE] 포스터 저장 예외:', error) + this.error = error.message || '포스터 저장 중 오류가 발생했습니다.' + return { + success: false, + message: this.error + } + } finally { + this.loading = false + } + }, + + /** + * 포스터 목록 조회 + */ + async fetchPosters() { + this.loading = true + this.error = null + + try { + const result = await contentService.getContents({ + contentType: 'poster', + sortBy: 'latest' + }) + + if (result.success) { + this.posters = result.data || [] + return result + } else { + this.error = result.message + return result + } + + } catch (error) { + console.error('❌ [POSTER_STORE] 포스터 목록 조회 예외:', error) + this.error = error.message || '포스터 목록 조회 중 오류가 발생했습니다.' + return { + success: false, + message: this.error + } + } finally { + this.loading = false + } + }, + + /** + * 포스터 삭제 + */ + async deletePoster(posterId) { + this.loading = true + this.error = null + + try { + const result = await contentService.deleteContent(posterId) + + if (result.success) { + // 목록에서 삭제 + this.posters = this.posters.filter(poster => poster.id !== posterId) + + // 현재 포스터가 삭제된 포스터라면 초기화 + if (this.currentPoster?.id === posterId) { + this.currentPoster = null + } + + return result + } else { + this.error = result.message + return result + } + + } catch (error) { + console.error('❌ [POSTER_STORE] 포스터 삭제 예외:', error) + this.error = error.message || '포스터 삭제 중 오류가 발생했습니다.' + return { + success: false, + message: this.error + } + } finally { + this.loading = false + } + }, + + /** + * 에러 상태 초기화 + */ + clearError() { + this.error = null + }, + + /** + * 현재 포스터 설정 + */ + setCurrentPoster(poster) { + this.currentPoster = poster + } + } +}) \ No newline at end of file diff --git a/src/store/store.js b/src/store/store.js index a09960c..ef920ed 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,533 +1,243 @@ -//* src/services/store.js - 병합 충돌 해결된 매장 서비스 -import { storeApi, menuApi, handleApiError, formatSuccessResponse } from './api.js' +//* src/store/content.js 수정 - 매장 정보 조회 후 콘텐츠 목록 조회 +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import contentService from '@/services/content' +import { useAuthStore } from '@/store/auth' -/** - * 매장 관련 API 서비스 - * 백엔드 Store Controller와 연동 (포트 8082) - */ -class StoreService { - /** - * 매장 등록 (STR-015: 매장 등록) - * @param {Object} storeData - 매장 정보 - * @returns {Promise} 매장 등록 결과 - */ - async registerStore(storeData) { - try { - console.log('=== 매장 등록 API 호출 ===') - console.log('요청 데이터:', storeData) - - // 백엔드 StoreCreateRequest에 맞는 형태로 변환 - const requestData = { - storeName: storeData.storeName, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours, - closedDays: storeData.closedDays, - seatCount: parseInt(storeData.seatCount) || 0, - instaAccounts: storeData.instaAccounts || '', - blogAccounts: storeData.blogAccounts || '', - description: storeData.description || '' - } - - console.log('=== 각 필드 상세 검증 ===') - console.log('storeName:', requestData.storeName, '(타입:', typeof requestData.storeName, ')') - console.log('businessType:', requestData.businessType, '(타입:', typeof requestData.businessType, ')') - console.log('address:', requestData.address, '(타입:', typeof requestData.address, ')') - console.log('seatCount:', requestData.seatCount, '(타입:', typeof requestData.seatCount, ')') - - console.log('백엔드 전송 데이터:', requestData) - - const response = await storeApi.post('/register', requestData) - - console.log('매장 등록 API 응답:', response.data) - - // 백엔드 응답 구조에 맞게 처리 - if (response.data && (response.data.status === 200 || response.data.success !== false)) { - return { - success: true, - message: response.data.message || '매장이 등록되었습니다.', - data: response.data.data - } - } else { - throw new Error(response.data.message || '매장 등록에 실패했습니다.') - } - } catch (error) { - console.error('매장 등록 실패:', error) - - if (error.response) { - console.error('응답 상태:', error.response.status) - console.error('응답 데이터:', error.response.data) - } - - return handleApiError(error) - } - } +export const useContentStore = defineStore('content', () => { + // 상태 + const contentList = ref([]) + const contents = ref([]) // ContentManagementView에서 사용하는 속성 + const ongoingContents = ref([]) + const selectedContent = ref(null) + const generatedContent = ref(null) + const isLoading = ref(false) - /** - * 매장 정보 조회 (STR-005: 매장 정보 관리) - * @returns {Promise} 매장 정보 - */ - async getStore() { - try { - console.log('=== 매장 정보 조회 API 호출 ===') - - // URL 슬래시 문제 해결: 빈 문자열로 호출하여 '/api/store'가 되도록 함 - const response = await storeApi.get('') - - console.log('매장 정보 조회 API 응답:', response.data) - - // 백엔드 응답 구조 수정: 디버깅 결과에 맞게 처리 - if (response.data && response.data.status === 200 && response.data.data) { - console.log('✅ 매장 정보 조회 성공:', response.data.data) - return { - success: true, - message: response.data.message || '매장 정보를 조회했습니다.', - data: response.data.data - } - } else if (response.data && response.data.status === 404) { - // 매장이 없는 경우 - console.log('⚠️ 등록된 매장이 없음') - return { - success: false, - message: '등록된 매장이 없습니다', - data: null - } - } else { - console.warn('예상치 못한 응답 구조:', response.data) - throw new Error(response.data.message || '매장 정보를 찾을 수 없습니다.') - } - } catch (error) { - console.error('매장 정보 조회 실패:', error) - - // 404 오류 처리 (매장이 없음) - if (error.response?.status === 404) { - return { - success: false, - message: '등록된 매장이 없습니다', - data: null - } - } - - // 500 오류 처리 (서버 내부 오류) - if (error.response?.status === 500) { - console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data) - return { - success: false, - message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.', - data: null - } - } - - return handleApiError(error) - } - } + // computed 속성들 + const contentCount = computed(() => contentList.value.length) + const ongoingContentCount = computed(() => ongoingContents.value.length) - /** - * 매장 정보 수정 (STR-010: 매장 수정) - * @param {number} storeId - 매장 ID (현재는 사용하지 않음 - JWT에서 사용자 확인) - * @param {Object} storeData - 수정할 매장 정보 - * @returns {Promise} 매장 수정 결과 - */ - async updateStore(storeId, storeData) { + // 콘텐츠 목록 로딩 (ContentManagementView에서 사용) + const loadContents = async (filters = {}) => { + console.log('=== 콘텐츠 목록 조회 시작 ===') + isLoading.value = true + try { - console.log('=== 매장 정보 수정 API 호출 ===') - console.log('요청 데이터:', storeData) + // 1단계: 매장 정보 조회하여 실제 storeId 가져오기 + const userInfo = useAuthStore().user + let storeId = null - // 백엔드 StoreUpdateRequest에 맞는 형태로 변환 - const requestData = { - storeName: storeData.storeName, - businessType: storeData.businessType, - address: storeData.address, - phoneNumber: storeData.phoneNumber, - businessHours: storeData.businessHours, - closedDays: storeData.closedDays, - seatCount: parseInt(storeData.seatCount) || 0, - instaAccounts: storeData.instaAccounts || '', - blogAccounts: storeData.blogAccounts || '', - description: storeData.description || '' - } - - console.log('백엔드 전송 데이터:', requestData) - - // PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음) - const response = await storeApi.put('/', requestData) - - console.log('매장 정보 수정 API 응답:', response.data) - - if (response.data && (response.data.status === 200 || response.data.success !== false)) { - return { - success: true, - message: response.data.message || '매장 정보가 수정되었습니다.', - data: response.data.data - } - } else { - throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.') - } - } catch (error) { - console.error('매장 정보 수정 실패:', error) - return handleApiError(error) - } - } - - /** - * 매출 정보 조회 (STR-020: 대시보드) - * @param {string} period - 조회 기간 (today, week, month, year) - * @returns {Promise} 매출 정보 - */ - async getSales(period = 'today') { - try { - console.log('=== 매출 정보 조회 API 호출 ===') - console.log('조회 기간:', period) - - // 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) - const mockSalesData = { - todaySales: 150000, - yesterdaySales: 120000, - changeRate: 25.0, - monthlyTarget: 3000000, - achievementRate: 45.2, - yearSales: this.generateMockYearSales() - } - - // 매출 트렌드 분석 추가 - if (mockSalesData.yearSales && mockSalesData.yearSales.length > 0) { - mockSalesData.trendAnalysis = this.analyzeSalesTrend(mockSalesData.yearSales) - mockSalesData.chartData = this.prepareChartData(mockSalesData.yearSales) - } - - return formatSuccessResponse(mockSalesData, '매출 정보를 조회했습니다.') - } catch (error) { - console.error('매출 정보 조회 실패:', error) - return handleApiError(error) - } - } - - /** - * 메뉴 목록 조회 (최종 통합 버전 - 모든 충돌 해결) - * @param {number} storeId - 매장 ID (옵션, 없으면 목업 데이터 반환) - * @returns {Promise} 메뉴 목록 - */ - async getMenus(storeId) { - try { - console.log('=== 메뉴 목록 조회 API 호출 ===') - console.log('매장 ID:', storeId) - - // storeId가 없으면 목업 데이터 반환 (개발 중) - if (!storeId) { - console.warn('매장 ID가 없어서 목업 데이터 반환') - const mockMenus = [ - { - menuId: 1, // id 대신 menuId 사용 - id: 1, // 호환성을 위해 - name: '아메리카노', - menuName: '아메리카노', // 백엔드 형식 - price: 4000, - category: '커피', - description: '진한 풍미의 아메리카노', - imageUrl: '/images/americano.jpg', - isAvailable: true, - available: true // 백엔드 형식 - }, - { - menuId: 2, - id: 2, - name: '카페라떼', - menuName: '카페라떼', - price: 4500, - category: '커피', - description: '부드러운 우유가 들어간 라떼', - imageUrl: '/images/latte.jpg', - isAvailable: true, - available: true - } - ] - return formatSuccessResponse(mockMenus, '목업 메뉴 목록을 조회했습니다.') - } - - // 실제 백엔드 API 호출 시도 try { - // GET /api/menu?storeId={storeId} - const response = await menuApi.get('', { - params: { storeId } + // 매장 정보 API 호출 + const storeApiUrl = (window.__runtime_config__ && window.__runtime_config__.STORE_URL) + ? window.__runtime_config__.STORE_URL + : 'http://localhost:8082/api/store' + + console.log('매장 API URL:', storeApiUrl) + + const token = localStorage.getItem('accessToken') || localStorage.getItem('auth_token') || localStorage.getItem('token') + const storeResponse = await fetch(`${storeApiUrl}`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } }) - console.log('메뉴 목록 조회 API 응답:', response.data) - - if (response.data && response.data.status === 200) { - // 백엔드에서 받은 메뉴 데이터를 프론트엔드 형식으로 변환 - const menus = response.data.data.map(menu => ({ - menuId: menu.menuId, - id: menu.menuId, // 호환성을 위해 - storeId: menu.storeId, - menuName: menu.menuName, - name: menu.menuName, // 호환성을 위해 - category: menu.category, - price: menu.price, - description: menu.description, - available: menu.available !== undefined ? menu.available : true, - isAvailable: menu.available !== undefined ? menu.available : true, // 호환성 - imageUrl: menu.imageUrl || '/images/menu-placeholder.png', - createdAt: menu.createdAt, - updatedAt: menu.updatedAt - })) - - return formatSuccessResponse(menus, '메뉴 목록을 조회했습니다.') + if (storeResponse.ok) { + const storeData = await storeResponse.json() + storeId = storeData.data?.storeId + console.log('✅ 매장 정보 조회 성공, storeId:', storeId) } else { - throw new Error(response.data.message || '메뉴 목록을 찾을 수 없습니다.') + throw new Error(`매장 정보 조회 실패: ${storeResponse.status}`) } - } catch (apiError) { - console.error('백엔드 API 호출 실패:', apiError) - - // 백엔드 미구현이나 네트워크 오류 시 목업 데이터 반환 - if (apiError.response?.status === 404 || - apiError.code === 'ECONNREFUSED' || - apiError.message.includes('Network Error')) { - console.warn('백엔드 미구현 - 목업 데이터 반환') - - const mockMenus = [ - { - menuId: 1, - id: 1, - storeId: storeId, - name: '아메리카노', - menuName: '아메리카노', - price: 4000, - category: '커피', - description: '진한 풍미의 아메리카노', - imageUrl: '/images/americano.jpg', - isAvailable: true, - available: true - }, - { - menuId: 2, - id: 2, - storeId: storeId, - name: '카페라떼', - menuName: '카페라떼', - price: 4500, - category: '커피', - description: '부드러운 우유가 들어간 라떼', - imageUrl: '/images/latte.jpg', - isAvailable: true, - available: true - } - ] - - return formatSuccessResponse(mockMenus, '목업 메뉴 목록을 조회했습니다. (백엔드 미구현)') - } - - throw apiError + } catch (error) { + console.error('❌ 매장 정보 조회 실패:', error) + throw new Error('매장 정보를 조회할 수 없습니다.') + } + + if (!storeId) { + throw new Error('매장 ID를 찾을 수 없습니다.') + } + + console.log('사용자 정보:', userInfo) + console.log('조회된 storeId:', storeId) + + // 2단계: 조회된 storeId로 콘텐츠 목록 조회 + const apiFilters = { + platform: filters.platform || null, // 전체, INSTAGRAM, NAVER_BLOG, POSTER 등 + storeId: storeId, + sortBy: filters.sortBy || 'latest' + // contentType, period는 백엔드에서 사용하지 않으므로 제외 + } + + console.log('API 요청 필터:', apiFilters) + + const result = await contentService.getContents(apiFilters) + + console.log('🔍 contentService.getContents 결과:', result) + console.log('🔍 result.success:', result.success) + console.log('🔍 result.data:', result.data) + console.log('🔍 result.data 타입:', typeof result.data) + console.log('🔍 result.data 길이:', result.data?.length) + + + if (result.success) { + contents.value = result.data || [] + contentList.value = result.data || [] + console.log('✅ 콘텐츠 로딩 성공:', contents.value.length, '개') + return { success: true } + } else { + console.error('❌ 콘텐츠 로딩 실패:', result.error) + contents.value = [] + contentList.value = [] + return { success: false, error: result.error } } } catch (error) { - console.error('메뉴 목록 조회 실패:', error) - return handleApiError(error) + console.error('❌ 콘텐츠 로딩 실패:', error) + contents.value = [] + contentList.value = [] + return { success: false, error: error.message || '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } - /** - * 메뉴 목록 조회 별칭 (fetchMenus) - * @param {number} storeId - 매장 ID - * @returns {Promise} 메뉴 목록 - */ - async fetchMenus(storeId) { - return await this.getMenus(storeId) - } - - /** - * 목업 메뉴 데이터 생성 - * @param {number} storeId - 매장 ID (옵션) - * @returns {Array} 목업 메뉴 배열 - */ - getMockMenus(storeId = null) { - return [ - { - menuId: 1, - id: 1, - storeId: storeId, - name: '아메리카노', - menuName: '아메리카노', - price: 4000, - category: '커피', - description: '진한 풍미의 아메리카노', - imageUrl: '/images/americano.jpg', - isAvailable: true, - available: true - }, - { - menuId: 2, - id: 2, - storeId: storeId, - name: '카페라떼', - menuName: '카페라떼', - price: 4500, - category: '커피', - description: '부드러운 우유가 들어간 라떼', - imageUrl: '/images/latte.jpg', - isAvailable: true, - available: true - }, - { - menuId: 3, - id: 3, - storeId: storeId, - name: '에스프레소', - menuName: '에스프레소', - price: 3500, - category: '커피', - description: '진한 에스프레소 한 잔', - imageUrl: '/images/espresso.jpg', - isAvailable: true, - available: true - } - ] - } - - /** - * 목업 연간 매출 데이터 생성 - * @returns {Array} 목업 매출 데이터 - */ - generateMockYearSales() { - const salesData = [] - const today = new Date() + // AI 콘텐츠 생성 + const generateContent = async (type, formData) => { + isLoading.value = true - // 최근 90일 데이터 생성 - for (let i = 89; i >= 0; i--) { - const date = new Date(today) - date.setDate(date.getDate() - i) - - // 랜덤하지만 현실적인 매출 패턴 생성 - const baseAmount = 120000 + Math.random() * 80000 // 120,000 ~ 200,000 - const weekendBonus = date.getDay() === 0 || date.getDay() === 6 ? 1.3 : 1.0 - const monthlyTrend = 1 + (Math.sin(i / 30) * 0.2) // 월별 트렌드 - - salesData.push({ - salesDate: date.toISOString().split('T')[0], - salesAmount: Math.round(baseAmount * weekendBonus * monthlyTrend) - }) - } - - return salesData - } - - /** - * 매출 트렌드 분석 및 변곡점 계산 - * @param {Array} yearSales - 연간 매출 데이터 - * @returns {Object} 트렌드 분석 결과 - */ - analyzeSalesTrend(yearSales) { - if (!yearSales || yearSales.length < 7) { - return { - inflectionPoints: [], - overallTrend: 'insufficient_data', - growthRate: 0 + try { + let result + if (type === 'sns') { + result = await contentService.generateSnsContent(formData) + } else if (type === 'poster') { + result = await contentService.generatePoster(formData) } - } - - // 날짜순 정렬 - const sortedData = [...yearSales].sort((a, b) => - new Date(a.salesDate) - new Date(b.salesDate) - ) - - const inflectionPoints = [] - const dailyData = sortedData.map(item => ({ - date: item.salesDate, - amount: parseFloat(item.salesAmount) || 0 - })) - - // 7일 이동평균으로 변곡점 탐지 - for (let i = 7; i < dailyData.length - 7; i++) { - const prevWeekAvg = this.calculateMovingAverage(dailyData, i - 7, 7) - const currentWeekAvg = this.calculateMovingAverage(dailyData, i, 7) - const nextWeekAvg = this.calculateMovingAverage(dailyData, i + 7, 7) - const trend1 = currentWeekAvg - prevWeekAvg - const trend2 = nextWeekAvg - currentWeekAvg - - // 변곡점 조건: 트렌드 방향 변화 + 임계값 초과 - if (Math.sign(trend1) !== Math.sign(trend2) && - Math.abs(trend1) > 10000 && Math.abs(trend2) > 10000) { - - inflectionPoints.push({ - date: dailyData[i].date, - amount: dailyData[i].amount, - type: trend1 > 0 ? 'peak' : 'valley', - significance: Math.abs(trend1) + Math.abs(trend2) - }) + if (result.success) { + generatedContent.value = result.data + return { success: true, data: result.data } + } else { + return { success: false, error: result.message } } - } - - // 전체 트렌드 계산 - const firstWeekAvg = this.calculateMovingAverage(dailyData, 0, 7) - const lastWeekAvg = this.calculateMovingAverage(dailyData, dailyData.length - 7, 7) - const growthRate = ((lastWeekAvg - firstWeekAvg) / firstWeekAvg) * 100 - - let overallTrend = 'stable' - if (Math.abs(growthRate) > 5) { - overallTrend = growthRate > 0 ? 'increasing' : 'decreasing' - } - - console.log('변곡점 분석 결과:', { inflectionPoints, overallTrend, growthRate }) - - return { - inflectionPoints: inflectionPoints.slice(0, 5), // 상위 5개만 - overallTrend, - growthRate: Math.round(growthRate * 10) / 10 + } catch (error) { + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } - /** - * 이동평균 계산 - * @param {Array} data - 데이터 배열 - * @param {number} startIndex - 시작 인덱스 - * @param {number} period - 기간 - * @returns {number} 이동평균값 - */ - calculateMovingAverage(data, startIndex, period) { - const slice = data.slice(startIndex, startIndex + period) - const sum = slice.reduce((acc, item) => acc + item.amount, 0) - return sum / slice.length - } - - /** - * 차트용 데이터 준비 - * @param {Array} yearSales - 연간 매출 데이터 - * @returns {Object} 차트 데이터 - */ - prepareChartData(yearSales) { - if (!yearSales || yearSales.length === 0) { - return { labels: [], salesData: [], targetData: [] } - } - - // 최근 30일 데이터만 사용 (차트 표시용) - const sortedData = [...yearSales] - .sort((a, b) => new Date(a.salesDate) - new Date(b.salesDate)) - .slice(-30) - - const labels = sortedData.map(item => { - const date = new Date(item.salesDate) - return `${date.getMonth() + 1}/${date.getDate()}` - }) - - const salesData = sortedData.map(item => parseFloat(item.salesAmount) || 0) + // 콘텐츠 저장 + const saveContent = async (type, contentData) => { + isLoading.value = true - // 목표 매출 라인 (평균의 110%) - const averageSales = salesData.reduce((a, b) => a + b, 0) / salesData.length - const targetData = salesData.map(() => Math.round(averageSales * 1.1)) - - return { - labels, - salesData, - targetData + try { + let result + if (type === 'sns') { + result = await contentService.saveSnsContent(contentData) + } else if (type === 'poster') { + result = await contentService.savePoster(contentData) + } + + if (result.success) { + // 콘텐츠 목록 새로고침 + await loadContents() + return { success: true, message: '콘텐츠가 저장되었습니다.' } + } else { + return { success: false, error: result.message } + } + } catch (error) { + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false } } -} -// 싱글톤 인스턴스 생성 및 export -export const storeService = new StoreService() -export default storeService + // fetchContentList - 기존 호환성 유지 + const fetchContentList = async (filters = {}) => { + return await loadContents(filters) + } -// 디버깅을 위한 전역 노출 (개발 환경에서만) -if (process.env.NODE_ENV === 'development') { - window.storeService = storeService -} \ No newline at end of file + // 진행 중인 콘텐츠 조회 + const fetchOngoingContents = async (period = 'month') => { + isLoading.value = true + + try { + const result = await contentService.getOngoingContents(period) + + if (result.success) { + ongoingContents.value = result.data + return { success: true } + } else { + return { success: false, error: result.message } + } + } catch (error) { + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + } + } + + // 콘텐츠 수정 + const updateContent = async (contentId, updateData) => { + isLoading.value = true + + try { + const result = await contentService.updateContent(contentId, updateData) + + if (result.success) { + await loadContents() + return { success: true, message: '콘텐츠가 수정되었습니다.' } + } else { + return { success: false, error: result.message } + } + } catch (error) { + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + } + } + + // 콘텐츠 삭제 + const deleteContent = async (contentId) => { + isLoading.value = true + + try { + const result = await contentService.deleteContent(contentId) + + if (result.success) { + await loadContents() + return { success: true, message: '콘텐츠가 삭제되었습니다.' } + } else { + return { success: false, error: result.message } + } + } catch (error) { + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + isLoading.value = false + } + } + + return { + // 상태 + contentList, + contents, // ContentManagementView에서 사용 + ongoingContents, + selectedContent, + generatedContent, + isLoading, + + // 컴퓨티드 + contentCount, + ongoingContentCount, + + // 메서드 + loadContents, // 새로 추가된 메서드 + generateContent, + saveContent, + fetchContentList, // 기존 호환성 유지 + fetchOngoingContents, + updateContent, + deleteContent + } +}) \ No newline at end of file diff --git a/src/utils/constants.js b/src/utils/constants.js index 26b368f..eb7213a 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -7,15 +7,7 @@ export const CONTENT_TYPES = { SNS: 'sns', POSTER: 'poster', - VIDEO: 'video', - BLOG: 'blog', -} - -export const CONTENT_TYPE_LABELS = { - [CONTENT_TYPES.SNS]: 'SNS 게시물', - [CONTENT_TYPES.POSTER]: '홍보 포스터', - [CONTENT_TYPES.VIDEO]: '비디오', - [CONTENT_TYPES.BLOG]: '블로그 포스트', + BLOG: 'blog' } // 플랫폼 @@ -23,191 +15,201 @@ export const PLATFORMS = { INSTAGRAM: 'instagram', NAVER_BLOG: 'naver_blog', FACEBOOK: 'facebook', - TWITTER: 'twitter', - YOUTUBE: 'youtube', - KAKAO: 'kakao', + KAKAO_STORY: 'kakao_story' } +// 플랫폼 라벨 export const PLATFORM_LABELS = { [PLATFORMS.INSTAGRAM]: '인스타그램', [PLATFORMS.NAVER_BLOG]: '네이버 블로그', [PLATFORMS.FACEBOOK]: '페이스북', - [PLATFORMS.TWITTER]: '트위터', - [PLATFORMS.YOUTUBE]: '유튜브', - [PLATFORMS.KAKAO]: '카카오', + [PLATFORMS.KAKAO_STORY]: '카카오스토리' } +// 플랫폼 컬러 export const PLATFORM_COLORS = { - [PLATFORMS.INSTAGRAM]: 'purple', + [PLATFORMS.INSTAGRAM]: 'pink', [PLATFORMS.NAVER_BLOG]: 'green', [PLATFORMS.FACEBOOK]: 'blue', - [PLATFORMS.TWITTER]: 'light-blue', - [PLATFORMS.YOUTUBE]: 'red', - [PLATFORMS.KAKAO]: 'yellow', + [PLATFORMS.KAKAO_STORY]: 'amber' } -// 콘텐츠 상태 -export const CONTENT_STATUS = { - DRAFT: 'draft', - PUBLISHED: 'published', - SCHEDULED: 'scheduled', - ARCHIVED: 'archived', - FAILED: 'failed', +// 플랫폼 아이콘 +export const PLATFORM_ICONS = { + [PLATFORMS.INSTAGRAM]: 'mdi-instagram', + [PLATFORMS.NAVER_BLOG]: 'mdi-web', + [PLATFORMS.FACEBOOK]: 'mdi-facebook', + [PLATFORMS.KAKAO_STORY]: 'mdi-chat' } -export const CONTENT_STATUS_LABELS = { - [CONTENT_STATUS.DRAFT]: '임시저장', - [CONTENT_STATUS.PUBLISHED]: '발행됨', - [CONTENT_STATUS.SCHEDULED]: '예약됨', - [CONTENT_STATUS.ARCHIVED]: '보관됨', - [CONTENT_STATUS.FAILED]: '실패', -} - -export const CONTENT_STATUS_COLORS = { - [CONTENT_STATUS.DRAFT]: 'orange', - [CONTENT_STATUS.PUBLISHED]: 'success', - [CONTENT_STATUS.SCHEDULED]: 'info', - [CONTENT_STATUS.ARCHIVED]: 'grey', - [CONTENT_STATUS.FAILED]: 'error', -} - -// 매장 업종 -export const BUSINESS_TYPES = { - RESTAURANT: 'restaurant', - CAFE: 'cafe', - SNACK_BAR: 'snack_bar', - FAST_FOOD: 'fast_food', - BAKERY: 'bakery', - DESSERT: 'dessert', - CONVENIENCE: 'convenience', - OTHER: 'other', -} - -export const BUSINESS_TYPE_LABELS = { - [BUSINESS_TYPES.RESTAURANT]: '일반음식점', - [BUSINESS_TYPES.CAFE]: '카페', - [BUSINESS_TYPES.SNACK_BAR]: '분식점', - [BUSINESS_TYPES.FAST_FOOD]: '패스트푸드', - [BUSINESS_TYPES.BAKERY]: '제과점', - [BUSINESS_TYPES.DESSERT]: '디저트카페', - [BUSINESS_TYPES.CONVENIENCE]: '편의점', - [BUSINESS_TYPES.OTHER]: '기타', +// 플랫폼 사양 정의 (누락된 PLATFORM_SPECS 추가) +export const PLATFORM_SPECS = { + [PLATFORMS.INSTAGRAM]: { + name: '인스타그램', + icon: 'mdi-instagram', + color: 'pink', + maxLength: 2200, + hashtags: true, + imageRequired: true, + format: 'sns' + }, + [PLATFORMS.NAVER_BLOG]: { + name: '네이버 블로그', + icon: 'mdi-web', + color: 'green', + maxLength: 5000, + hashtags: false, + imageRequired: false, + format: 'blog' + }, + [PLATFORMS.FACEBOOK]: { + name: '페이스북', + icon: 'mdi-facebook', + color: 'blue', + maxLength: 63206, + hashtags: true, + imageRequired: false, + format: 'sns' + }, + [PLATFORMS.KAKAO_STORY]: { + name: '카카오스토리', + icon: 'mdi-chat', + color: 'amber', + maxLength: 1000, + hashtags: true, + imageRequired: false, + format: 'sns' + } } // 톤앤매너 export const TONE_AND_MANNER = { FRIENDLY: 'friendly', PROFESSIONAL: 'professional', - HUMOROUS: 'humorous', - ELEGANT: 'elegant', CASUAL: 'casual', - TRENDY: 'trendy', -} - -export const TONE_AND_MANNER_LABELS = { - [TONE_AND_MANNER.FRIENDLY]: '친근함', - [TONE_AND_MANNER.PROFESSIONAL]: '전문적', - [TONE_AND_MANNER.HUMOROUS]: '유머러스', - [TONE_AND_MANNER.ELEGANT]: '고급스러운', - [TONE_AND_MANNER.CASUAL]: '캐주얼', - [TONE_AND_MANNER.TRENDY]: '트렌디', + HUMOROUS: 'humorous' } // 감정 강도 export const EMOTION_INTENSITY = { - CALM: 'calm', - NORMAL: 'normal', - ENTHUSIASTIC: 'enthusiastic', - EXCITING: 'exciting', -} - -export const EMOTION_INTENSITY_LABELS = { - [EMOTION_INTENSITY.CALM]: '차분함', - [EMOTION_INTENSITY.NORMAL]: '보통', - [EMOTION_INTENSITY.ENTHUSIASTIC]: '열정적', - [EMOTION_INTENSITY.EXCITING]: '과장된', + LOW: 'low', + MEDIUM: 'medium', + HIGH: 'high' } // 프로모션 타입 export const PROMOTION_TYPES = { - DISCOUNT: 'discount', - EVENT: 'event', - NEW_MENU: 'new_menu', - NONE: 'none', + DISCOUNT: 'DISCOUNT', + EVENT: 'EVENT', + NEW_PRODUCT: 'NEW_PRODUCT', + REVIEW: 'REVIEW' } -export const PROMOTION_TYPE_LABELS = { - [PROMOTION_TYPES.DISCOUNT]: '할인 정보', - [PROMOTION_TYPES.EVENT]: '이벤트 정보', - [PROMOTION_TYPES.NEW_MENU]: '신메뉴 알림', - [PROMOTION_TYPES.NONE]: '없음', -} - -// 이미지 스타일 +// 사진 스타일 export const PHOTO_STYLES = { MODERN: 'modern', - CLASSIC: 'classic', - EMOTIONAL: 'emotional', + VINTAGE: 'vintage', MINIMALIST: 'minimalist', + COLORFUL: 'colorful', + BRIGHT: 'bright', + CALM: 'calm', + NATURAL: 'natural' } -export const PHOTO_STYLE_LABELS = { - [PHOTO_STYLES.MODERN]: '모던', - [PHOTO_STYLES.CLASSIC]: '클래식', - [PHOTO_STYLES.EMOTIONAL]: '감성적', - [PHOTO_STYLES.MINIMALIST]: '미니멀', +// 콘텐츠 상태 +export const CONTENT_STATUS = { + DRAFT: 'draft', + PUBLISHED: 'published', + ARCHIVED: 'archived' } -// 파일 업로드 제한 -export const FILE_LIMITS = { - MAX_SIZE: 10 * 1024 * 1024, // 10MB - ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], - ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], +// 타겟 대상 +export const TARGET_TYPES = { + NEW_MENU: 'new_menu', + DISCOUNT: 'discount', + STORE: 'store', + EVENT: 'event' +} + +// 타겟 대상 라벨 +export const TARGET_TYPE_LABELS = { + [TARGET_TYPES.NEW_MENU]: '신메뉴', + [TARGET_TYPES.DISCOUNT]: '할인 이벤트', + [TARGET_TYPES.STORE]: '매장 홍보', + [TARGET_TYPES.EVENT]: '일반 이벤트' +} + +// 백엔드 플랫폼 매핑 (프론트엔드 -> 백엔드) +export const BACKEND_PLATFORM_MAPPING = { + [PLATFORMS.INSTAGRAM]: 'INSTAGRAM', + [PLATFORMS.NAVER_BLOG]: 'NAVER_BLOG', + [PLATFORMS.FACEBOOK]: 'FACEBOOK', + [PLATFORMS.KAKAO_STORY]: 'KAKAO_STORY' +} + +// 백엔드에서 프론트엔드로 매핑 (백엔드 -> 프론트엔드) +export const FRONTEND_PLATFORM_MAPPING = { + 'INSTAGRAM': PLATFORMS.INSTAGRAM, + 'NAVER_BLOG': PLATFORMS.NAVER_BLOG, + 'FACEBOOK': PLATFORMS.FACEBOOK, + 'KAKAO_STORY': PLATFORMS.KAKAO_STORY } // API 응답 상태 export const API_STATUS = { SUCCESS: 'success', ERROR: 'error', - LOADING: 'loading', - IDLE: 'idle', + LOADING: 'loading' } -// 페이지네이션 -export const PAGINATION = { - DEFAULT_PAGE_SIZE: 20, - PAGE_SIZE_OPTIONS: [10, 20, 50, 100], +// 페이지 크기 +export const PAGE_SIZES = { + SMALL: 10, + MEDIUM: 20, + LARGE: 50 +} + +// 정렬 방향 +export const SORT_DIRECTION = { + ASC: 'asc', + DESC: 'desc' +} + +// 날짜 포맷 +export const DATE_FORMATS = { + DISPLAY: 'YYYY-MM-DD HH:mm', + API: 'YYYY-MM-DD', + FULL: 'YYYY-MM-DD HH:mm:ss' +} + +// 파일 업로드 제한 +export const FILE_LIMITS = { + MAX_SIZE: 10485760, // 10MB + ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + MAX_FILES: 5 +} + +// 콘텐츠 생성 제한 +export const CONTENT_LIMITS = { + TITLE_MAX_LENGTH: 100, + DESCRIPTION_MAX_LENGTH: 500, + REQUIREMENTS_MAX_LENGTH: 1000, + MAX_HASHTAGS: 30 +} + +// 알림 타입 +export const NOTIFICATION_TYPES = { + SUCCESS: 'success', + ERROR: 'error', + WARNING: 'warning', + INFO: 'info' } // 로컬 스토리지 키 export const STORAGE_KEYS = { - AUTH_TOKEN: 'auth_token', - USER_INFO: 'user_info', - APP_SETTINGS: 'app_settings', - CONTENT_FILTERS: 'content_filters', -} - -// 시간 관련 상수 -export const TIME_FORMATS = { - DATE: 'YYYY-MM-DD', - DATETIME: 'YYYY-MM-DD HH:mm:ss', - TIME: 'HH:mm', -} - -export const DATE_RANGES = { - TODAY: 'today', - WEEK: 'week', - MONTH: 'month', - QUARTER: 'quarter', - YEAR: 'year', - ALL: 'all', -} - -export const DATE_RANGE_LABELS = { - [DATE_RANGES.TODAY]: '오늘', - [DATE_RANGES.WEEK]: '최근 1주일', - [DATE_RANGES.MONTH]: '최근 1개월', - [DATE_RANGES.QUARTER]: '최근 3개월', - [DATE_RANGES.YEAR]: '최근 1년', - [DATE_RANGES.ALL]: '전체', -} + ACCESS_TOKEN: 'accessToken', + REFRESH_TOKEN: 'refreshToken', + USER_INFO: 'userInfo', + THEME: 'theme', + LANGUAGE: 'language' +} \ No newline at end of file diff --git a/src/views/ContentCreationView.vue b/src/views/ContentCreationView.vue index be215ed..b5338f7 100644 --- a/src/views/ContentCreationView.vue +++ b/src/views/ContentCreationView.vue @@ -1,4 +1,3 @@ -//* src/views/ContentCreationView.vue \ No newline at end of file diff --git a/src/views/ContentManagementView.vue b/src/views/ContentManagementView.vue index 26006c9..b6af897 100644 --- a/src/views/ContentManagementView.vue +++ b/src/views/ContentManagementView.vue @@ -1,351 +1,347 @@ //* src/views/ContentManagementView.vue + + + + \ No newline at end of file diff --git a/src/views/StoreManagementView.vue b/src/views/StoreManagementView.vue index ddea127..a24f8c2 100644 --- a/src/views/StoreManagementView.vue +++ b/src/views/StoreManagementView.vue @@ -1,7 +1,18 @@ -//* src/views/StoreManagementView.vue - 완전한 매장 관리 화면 (이미지 업로드 포함) - -