diff --git a/src/services/content.js b/src/services/content.js index 99ee797..d700972 100644 --- a/src/services/content.js +++ b/src/services/content.js @@ -1,172 +1,670 @@ -//* src/services/content.js -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 서비스 - * 백엔드 SnsContentCreateRequest DTO에 맞게 수정 + * 콘텐츠 서비스 클래스 - 완전 통합 버전 + * 백엔드 API 설계서와 일치하도록 구현 */ class ContentService { /** - * SNS 게시물 생성 - * @param {Object} contentData - SNS 콘텐츠 생성 정보 - * @returns {Promise} 생성된 SNS 콘텐츠 + * 콘텐츠 목록 조회 (CON-021: 콘텐츠 조회) + * @param {Object} filters - 필터 조건 + * @param {string} filters.platform - 플랫폼 (instagram, blog, poster) + * @param {number} filters.storeId - 매장 ID + * @param {string} filters.contentType - 콘텐츠 타입 + * @param {string} filters.period - 조회 기간 + * @param {string} filters.sortBy - 정렬 기준 + * @returns {Promise} 콘텐츠 목록 */ - async generateSnsContent(contentData) { + async getContents(filters = {}) { try { - console.log('🚀 SNS 콘텐츠 생성 요청:', contentData) + console.log('🔄 콘텐츠 목록 조회 요청:', filters) - // 백엔드 SnsContentCreateRequest DTO에 맞는 데이터 구조 - const requestData = { - // === 기본 정보 === - storeId: contentData.storeId || 1, - storeName: contentData.storeName || '테스트 매장', - storeType: contentData.storeType || '음식점', - platform: this.mapPlatform(contentData.platform), - title: contentData.title, - - // === 콘텐츠 생성 조건 === - category: contentData.category || this.mapTargetToCategory(contentData.targetType), - requirement: contentData.requirements || contentData.content || '', - target: contentData.targetType || '일반 고객', - contentType: 'SNS 게시물', - - // === 이벤트 정보 === - eventName: contentData.eventName || null, - startDate: contentData.startDate ? this.formatDate(contentData.startDate) : null, - endDate: contentData.endDate ? this.formatDate(contentData.endDate) : null, - - // === 미디어 정보 === - images: contentData.images || [], - photoStyle: this.mapPhotoStyle(contentData.aiOptions?.photoStyle), - - // === 추가 옵션 === - includeHashtags: true, - includeEmojis: true, - includeCallToAction: true, - includeLocationInfo: false + // 쿼리 파라미터 구성 - 빈 값 제거 + 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() - console.log('📤 백엔드 DTO 맞춤 데이터:', requestData) + // 백엔드 응답 구조에 따른 데이터 추출 + let contentData = [] - const response = await contentApi.post('/sns/generate', requestData) + 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('📥 API 응답:', response.data) + console.log('✅ 콘텐츠 조회 성공:', contentData.length, '개') - // 응답 데이터 구조에 맞게 처리 - const responseData = response.data.data || response.data - - return formatSuccessResponse({ - content: responseData.content || responseData, - hashtags: responseData.hashtags || [], - ...responseData - }, 'SNS 게시물이 생성되었습니다.') + return formatSuccessResponse(contentData, '콘텐츠 목록을 조회했습니다.') } catch (error) { - console.error('❌ SNS 콘텐츠 생성 실패:', error) + console.error('❌ 콘텐츠 조회 실패:', error) return handleApiError(error) } } /** - * 플랫폼 매핑 (프론트엔드 -> 백엔드) + * SNS 콘텐츠 생성 (CON-019: AI 콘텐츠 생성) + * @param {Object} contentData - 콘텐츠 생성 데이터 + * @returns {Promise} 생성된 콘텐츠 */ - mapPlatform(platform) { - const mapping = { - 'instagram': 'INSTAGRAM', - 'naver_blog': 'NAVER_BLOG', - 'facebook': 'FACEBOOK', - 'kakao_story': 'KAKAO_STORY' + async generateSnsContent(contentData) { + try { + console.log('🤖 SNS 콘텐츠 생성 요청:', contentData) + + // ✅ 이미지 처리 (SNS는 선택사항) + let processedImages = [] + if (contentData.images && Array.isArray(contentData.images) && contentData.images.length > 0) { + console.log('📁 [API] SNS 이미지 처리:', contentData.images.length, '개') + + processedImages = contentData.images.filter(img => { + const isValid = img && typeof img === 'string' && img.length > 0 + console.log('📁 [API] SNS 이미지 유효성:', { isValid, type: typeof img, length: img?.length }) + return isValid + }) + + console.log('📁 [API] SNS 유효 이미지:', processedImages.length, '개') + } + + // ✅ 실제 전달받은 데이터만 사용 (백엔드 API 스펙에 맞춤) + const requestData = {} + + if (contentData.storeId !== undefined) requestData.storeId = contentData.storeId + if (contentData.storeName) requestData.storeName = contentData.storeName + if (contentData.storeType) requestData.storeType = contentData.storeType + if (contentData.platform) requestData.platform = contentData.platform + if (contentData.title) requestData.title = contentData.title + if (contentData.category) requestData.category = contentData.category + if (contentData.requirement || contentData.requirements) { + requestData.requirement = contentData.requirement || contentData.requirements + } + if (contentData.target || contentData.targetAudience) { + requestData.target = contentData.target || contentData.targetAudience + } + if (contentData.contentType) requestData.contentType = contentData.contentType + if (contentData.eventName) requestData.eventName = contentData.eventName + if (contentData.startDate) requestData.startDate = contentData.startDate + if (contentData.endDate) requestData.endDate = contentData.endDate + if (contentData.photoStyle) requestData.photoStyle = contentData.photoStyle + if (contentData.targetAge) requestData.targetAge = contentData.targetAge + if (contentData.toneAndManner) requestData.toneAndManner = contentData.toneAndManner + if (contentData.emotionalIntensity || contentData.emotionIntensity) { + requestData.emotionalIntensity = contentData.emotionalIntensity || contentData.emotionIntensity + } + if (contentData.promotionalType || contentData.promotionType) { + requestData.promotionalType = contentData.promotionalType || contentData.promotionType + } + if (contentData.eventDate) requestData.eventDate = contentData.eventDate + if (contentData.hashtagStyle) requestData.hashtagStyle = contentData.hashtagStyle + if (contentData.hashtagCount) requestData.hashtagCount = contentData.hashtagCount + if (contentData.contentLength) requestData.contentLength = contentData.contentLength + + // 이미지는 처리된 것으로 설정 + requestData.images = processedImages + + // Boolean 필드들 (기본값 처리) + if (contentData.includeHashtags !== undefined) requestData.includeHashtags = contentData.includeHashtags + if (contentData.includeEmojis !== undefined) requestData.includeEmojis = contentData.includeEmojis + if (contentData.includeEmoji !== undefined) requestData.includeEmoji = contentData.includeEmoji + if (contentData.includeCallToAction !== undefined) requestData.includeCallToAction = contentData.includeCallToAction + if (contentData.includeLocation !== undefined) requestData.includeLocation = contentData.includeLocation + if (contentData.forInstagramStory !== undefined) requestData.forInstagramStory = contentData.forInstagramStory + if (contentData.forNaverBlogPost !== undefined) requestData.forNaverBlogPost = contentData.forNaverBlogPost + + if (contentData.alternativeTitleCount !== undefined) requestData.alternativeTitleCount = contentData.alternativeTitleCount + if (contentData.alternativeHashtagSetCount !== undefined) requestData.alternativeHashtagSetCount = contentData.alternativeHashtagSetCount + if (contentData.preferredAiModel) requestData.preferredAiModel = contentData.preferredAiModel + + console.log('📝 [API] SNS 요청 데이터:', { + ...requestData, + images: `${requestData.images.length}개 이미지` + }) + + // 기본 유효성 검사 + if (!requestData.platform) { + throw new Error('플랫폼은 필수입니다.') + } + + if (!requestData.title) { + throw new Error('제목은 필수입니다.') + } + + const response = await contentApi.post('/sns/generate', requestData, { + timeout: 30000 // 30초 + }) + + console.log('✅ [API] SNS 콘텐츠 생성 응답:', response.data) + return formatSuccessResponse(response.data, 'SNS 게시물이 생성되었습니다.') + } catch (error) { + console.error('❌ [API] SNS 콘텐츠 생성 실패:', error) + return handleApiError(error) } - return mapping[platform] || 'INSTAGRAM' } /** - * 타겟 타입을 카테고리로 매핑 + * 포스터 생성 (CON-020: AI 포스터 생성) - 이미지 처리 강화 및 상세 검증 + * @param {Object} posterData - 포스터 생성 데이터 + * @returns {Promise} 생성된 포스터 */ - mapTargetToCategory(targetType) { - const mapping = { - 'new_menu': '메뉴소개', - 'discount': '이벤트', - 'store': '인테리어', - 'event': '이벤트' + 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 = '서버에서 포스터 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.' + } + + // 유효성 검사 오류가 있다면 추출 + if (error.response.data && error.response.data.errors) { + console.error('❌ [API] 유효성 검사 오류:', error.response.data.errors) + const validationMessages = Object.values(error.response.data.errors).flat() + if (validationMessages.length > 0) { + backendMessage = validationMessages.join(', ') + } + } + + 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' + } + } } - return mapping[targetType] || '메뉴소개' } /** - * 날짜 형식 변환 (YYYY-MM-DD -> LocalDate) - */ - formatDate(dateString) { - if (!dateString) return null - // YYYY-MM-DD 형식이 LocalDate와 호환됨 - return dateString - } - - /** - * 사진 스타일 매핑 - */ - mapPhotoStyle(style) { - const mapping = { - 'bright': '밝고 화사한', - 'calm': '차분하고 세련된', - 'vintage': '빈티지한', - 'modern': '모던한', - 'natural': '자연스러운' - } - return mapping[style] || '밝고 화사한' - } - - /** - * SNS 게시물 저장 - * @param {Object} saveData - 저장할 SNS 콘텐츠 정보 + * SNS 콘텐츠 저장 (CON-010: SNS 게시물 저장) + * @param {Object} saveData - 저장할 콘텐츠 데이터 * @returns {Promise} 저장 결과 */ async saveSnsContent(saveData) { try { - console.log('💾 SNS 콘텐츠 저장 요청:', saveData) + const requestData = {} - // 백엔드 SnsContentSaveRequest DTO에 맞는 구조로 변환 - const requestData = { - title: saveData.title, - content: saveData.content, - hashtags: saveData.hashtags || [], - platform: this.mapPlatform(saveData.platform), - category: saveData.category || '메뉴소개', - // 백엔드 DTO에서 지원하는 필드들만 포함 - eventName: saveData.eventName, - eventDate: saveData.eventDate, - status: saveData.status || 'DRAFT' + 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 } - - console.log('📤 저장 요청 데이터:', requestData) - + 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) { - console.error('❌ SNS 콘텐츠 저장 실패:', error) return handleApiError(error) } } /** - * 콘텐츠 목록 조회 - * @param {Object} filters - 필터 조건 - * @returns {Promise} 콘텐츠 목록 + * 포스터 저장 (CON-015: 포스터 저장) + * @param {Object} saveData - 저장할 포스터 데이터 + * @returns {Promise} 저장 결과 */ - async getContentList(filters = {}) { + async savePoster(saveData) { try { - const params = new URLSearchParams() + const requestData = {} - if (filters.contentType) params.append('contentType', filters.contentType) - if (filters.platform) params.append('platform', filters.platform) - if (filters.period) params.append('period', filters.period) - if (filters.sortBy) params.append('sortBy', filters.sortBy) + 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.get(`/list?${params.toString()}`) - - return formatSuccessResponse(response.data.data, '콘텐츠 목록을 조회했습니다.') + const response = await contentApi.post('/poster/save', requestData) + return formatSuccessResponse(response.data.data, '포스터가 저장되었습니다.') + } catch (error) { + return handleApiError(error) + } + } + + /** + * 통합 콘텐츠 생성 (타입에 따라 SNS 또는 포스터 생성) + * @param {Object} contentData - 콘텐츠 생성 데이터 + * @returns {Promise} 생성 결과 + */ + async generateContent(contentData) { + console.log('🎯 [API] 통합 콘텐츠 생성:', contentData) + + if (contentData.contentType === 'poster' || contentData.type === 'poster') { + return await this.generatePoster(contentData) + } else { + return await this.generateSnsContent(contentData) + } + } + + /** + * 진행 중인 콘텐츠 조회 + * @param {string} period - 조회 기간 + * @returns {Promise} 진행 중인 콘텐츠 목록 + */ + async getOngoingContents(period = 'month') { + try { + const response = await contentApi.get(`/ongoing?period=${period}`) + + return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.') } catch (error) { - console.error('❌ 콘텐츠 목록 조회 실패:', error) return handleApiError(error) } } @@ -179,29 +677,200 @@ class ContentService { async getContentDetail(contentId) { try { const response = await contentApi.get(`/${contentId}`) - + return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.') } catch (error) { - console.error('❌ 콘텐츠 상세 조회 실패:', error) return handleApiError(error) } } /** - * 콘텐츠 삭제 + * 콘텐츠 수정 (CON-024: 콘텐츠 수정) + * @param {number} contentId - 콘텐츠 ID + * @param {Object} updateData - 수정할 콘텐츠 정보 + * @returns {Promise} 수정 결과 + */ + async updateContent(contentId, updateData) { + try { + 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) + } + } + + /** + * 콘텐츠 삭제 (CON-025: 콘텐츠 삭제) * @param {number} contentId - 콘텐츠 ID * @returns {Promise} 삭제 결과 */ async deleteContent(contentId) { try { - const response = await contentApi.delete(`/${contentId}`) - + await contentApi.delete(`/${contentId}`) + return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.') } catch (error) { - console.error('❌ 콘텐츠 삭제 실패:', 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) } } } -export default new ContentService() \ No newline at end of file +// 서비스 인스턴스 생성 및 내보내기 +const contentService = new ContentService() + +// API 인스턴스와 유틸리티 함수도 함께 내보내기 +export { contentApi, handleApiError, formatSuccessResponse } +export default contentService \ No newline at end of file diff --git a/src/store/content.js b/src/store/content.js index 176e852..0694d74 100644 --- a/src/store/content.js +++ b/src/store/content.js @@ -1,56 +1,73 @@ -//* src/store/content.js +//* src/store/content.js - 두 파일 완전 통합 버전 (Part 1) import { defineStore } from 'pinia' -import ContentService from '@/services/content.js' -import { PLATFORM_SPECS, PLATFORM_LABELS } from '@/utils/constants' +import { ref, computed, readonly } from 'vue' +import contentService from '@/services/content' +import { useAuthStore } from '@/store/auth' -/** - * 콘텐츠 관련 상태 관리 - */ -export const useContentStore = defineStore('content', { - state: () => ({ - // 콘텐츠 목록 - contentList: [], - totalCount: 0, +// 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 속성들 ===== + const contentCount = computed(() => contentList.value.length) + const ongoingContentCount = computed(() => ongoingContents.value.length) + + /** + * 필터링된 콘텐츠 목록 + */ + const filteredContents = computed(() => { + let filtered = [...contentList.value] - // 선택된 콘텐츠 - selectedContent: null, - - // 로딩 상태 - loading: false, - generating: false, - - // 필터 상태 - filters: { - contentType: '', - platform: '', - period: '', - sortBy: 'createdAt_desc' - }, - - // 페이지네이션 - pagination: { - page: 1, - itemsPerPage: 10 + if (filters.value.contentType) { + filtered = filtered.filter(content => content.type === filters.value.contentType) } - }), - - getters: { - /** - * 필터링된 콘텐츠 목록 - */ - filteredContents: (state) => { - let filtered = [...state.contentList] - - if (state.filters.contentType) { - filtered = filtered.filter(content => content.type === state.filters.contentType) - } - - if (state.filters.platform) { - filtered = filtered.filter(content => content.platform === state.filters.platform) - } - - // 정렬 - const [field, order] = state.filters.sortBy.split('_') + + 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] @@ -66,242 +83,996 @@ export const useContentStore = defineStore('content', { return aValue > bValue ? 1 : -1 } }) - - return filtered - }, - - /** - * 페이지네이션된 콘텐츠 목록 - */ - paginatedContents: (state) => { - const start = (state.pagination.page - 1) * state.pagination.itemsPerPage - const end = start + state.pagination.itemsPerPage - return state.filteredContents.slice(start, end) - }, - - /** - * 총 페이지 수 - */ - totalPages: (state) => { - return Math.ceil(state.filteredContents.length / state.pagination.itemsPerPage) + } else if (sortBy === 'latest') { + filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) } - }, + + return filtered + }) - actions: { - /** - * 콘텐츠 생성 (AI 기반) - */ - async generateContent(contentData) { - this.generating = true + /** + * 페이지네이션된 콘텐츠 목록 + */ + 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) - try { - console.log('🎯 콘텐츠 생성 시작:', contentData) - - // 백엔드 DTO에 맞는 데이터 구조로 변환 - const requestData = { - storeId: 1, // 현재는 하드코딩, 추후 로그인한 사용자의 매장 ID 사용 - storeName: '테스트 매장', // 추후 실제 매장 정보 사용 - storeType: '음식점', // 추후 실제 매장 업종 사용 - platform: contentData.platform, - title: contentData.title, - category: contentData.category || this.mapTargetToCategory(contentData.targetType), - requirement: contentData.requirements || '', - target: contentData.targetType || '일반 고객', - contentType: 'SNS 게시물', - eventName: contentData.eventName, - startDate: contentData.startDate, - endDate: contentData.endDate, - images: contentData.images || [], - photoStyle: '밝고 화사한', - includeHashtags: true, - includeEmojis: true, - includeCallToAction: true, - includeLocationInfo: false - } - - const result = await ContentService.generateSnsContent(requestData) - - if (result.success) { - console.log('✅ 콘텐츠 생성 성공:', result.data) - return result.data - } else { - throw new Error(result.message || '콘텐츠 생성에 실패했습니다.') - } - } catch (error) { - console.error('❌ 콘텐츠 생성 실패:', error) - throw error - } finally { - this.generating = false - } - }, - - /** - * 타겟 타입을 카테고리로 매핑 - */ - mapTargetToCategory(targetType) { - const mapping = { - 'new_menu': '메뉴소개', - 'discount': '이벤트', - 'store': '인테리어', - 'event': '이벤트' - } - return mapping[targetType] || '메뉴소개' - }, - - /** - * 플랫폼별 특성 조회 - */ - getPlatformSpec(platform) { - return PLATFORM_SPECS[platform] || null - }, - - /** - * 플랫폼 유효성 검사 - */ - validatePlatform(platform) { - return Object.keys(PLATFORM_SPECS).includes(platform) - }, - - /** - * 콘텐츠 저장 - */ - async saveContent(contentData) { - this.loading = true + // 매장 정보 API 호출 + const storeApiUrl = (window.__runtime_config__ && window.__runtime_config__.STORE_URL) + ? window.__runtime_config__.STORE_URL + : 'http://localhost:8082/api/store' - try { - // 백엔드 DTO에 맞는 형식으로 데이터 정제 - const saveData = { - title: contentData.title, - content: contentData.content, - hashtags: contentData.hashtags || [], - platform: contentData.platform, // 이미 백엔드 형식 (INSTAGRAM, NAVER_BLOG 등) - category: contentData.category || '메뉴소개', - eventName: contentData.eventName, - eventDate: contentData.eventDate, - status: contentData.status || 'DRAFT' - } - - const result = await ContentService.saveSnsContent(saveData) - - if (result.success) { - // 목록 새로고침 - await this.fetchContentList() - return result.data - } else { - throw new Error(result.message || '콘텐츠 저장에 실패했습니다.') - } - } catch (error) { - console.error('❌ 콘텐츠 저장 실패:', error) - throw error - } finally { - this.loading = false - } - }, - - /** - * 콘텐츠 목록 조회 - */ - async fetchContentList() { - this.loading = true + console.log('매장 API URL:', storeApiUrl) - try { - const result = await ContentService.getContentList(this.filters) - - if (result.success) { - this.contentList = result.data || [] - this.totalCount = this.contentList.length - } else { - throw new Error(result.message || '콘텐츠 목록 조회에 실패했습니다.') + 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' } - } catch (error) { - console.error('❌ 콘텐츠 목록 조회 실패:', error) - this.contentList = [] - this.totalCount = 0 - } finally { - this.loading = false - } - }, - - /** - * 콘텐츠 상세 조회 - */ - async fetchContentDetail(contentId) { - this.loading = true + }) - try { - const result = await ContentService.getContentDetail(contentId) - - if (result.success) { - this.selectedContent = result.data - return result.data - } else { - throw new Error(result.message || '콘텐츠 상세 조회에 실패했습니다.') - } - } catch (error) { - console.error('❌ 콘텐츠 상세 조회 실패:', error) - throw error - } finally { - this.loading = false - } - }, - - /** - * 콘텐츠 삭제 - */ - async deleteContent(contentId) { - this.loading = true - - try { - const result = await ContentService.deleteContent(contentId) - - if (result.success) { - // 목록에서 제거 - this.contentList = this.contentList.filter(content => content.id !== contentId) - this.totalCount = this.contentList.length - return true - } else { - throw new Error(result.message || '콘텐츠 삭제에 실패했습니다.') - } - } catch (error) { - console.error('❌ 콘텐츠 삭제 실패:', error) - throw error - } finally { - this.loading = false - } - }, - - /** - * 필터 설정 - */ - setFilters(newFilters) { - this.filters = { ...this.filters, ...newFilters } - this.pagination.page = 1 // 필터 변경 시 첫 페이지로 - }, - - /** - * 페이지네이션 설정 - */ - setPagination(newPagination) { - this.pagination = { ...this.pagination, ...newPagination } - }, - - /** - * 상태 초기화 - */ - reset() { - this.contentList = [] - this.selectedContent = null - this.totalCount = 0 - this.filters = { - contentType: '', - platform: '', - period: '', - sortBy: 'createdAt_desc' - } - this.pagination = { - page: 1, - itemsPerPage: 10 + 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 === '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 } + } else { + return { success: false, error: result.message } + } + } catch (error) { + console.error('❌ 템플릿 콘텐츠 생성 실패:', error) + return { success: false, error: '네트워크 오류가 발생했습니다.' } + } finally { + generating.value = false + } + } + + // ===== 반환할 store 객체 ===== + return { + // 상태 + 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, + + // 콘텐츠 목록 조회 + loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함) + fetchContentList, // 기존 호환성 유지 + + // AI 콘텐츠 생성 + generateContent, // 통합 메서드 (타입에 따라 라우팅) + generateSnsContent, // SNS 전용 + generatePoster, // 포스터 전용 + + // 콘텐츠 저장 + saveContent, // 통합 메서드 (타입에 따라 라우팅) + saveSnsContent, // SNS 전용 + savePoster, // 포스터 전용 + + // 기본 CRUD + fetchOngoingContents, + fetchContentDetail, + updateContent, + deleteContent, + + // 유틸리티 + mapTargetToCategory, + getPlatformSpec, + validatePlatform, + setFilters, + setPagination, + reset, + + // 고급 기능 + searchContents, + getContentStats, + duplicateContent, + updateContentStatus, + toggleContentFavorite, + getContentTemplates, + generateFromTemplate + } }) \ No newline at end of file diff --git a/src/views/ContentManagementView.vue b/src/views/ContentManagementView.vue index c927e13..b6af897 100644 --- a/src/views/ContentManagementView.vue +++ b/src/views/ContentManagementView.vue @@ -46,18 +46,6 @@ @input="applyFilters" class="flex-grow-1" /> - - - @@ -435,17 +423,10 @@ const errorMessage = ref('') // 옵션 데이터 const contentTypeOptions = [ - { title: '📊 전체', value: 'all', color: 'primary', emoji: '📊' }, - { title: '📷 Instagram', value: 'instagram', color: 'pink', emoji: '📷' }, - { title: '📝 네이버 블로그', value: 'blog', color: 'green', emoji: '📝' }, - { title: '🎨 포스터', value: 'poster', color: 'orange', emoji: '🎨' } -] - -const sortOptions = [ - { title: '최신순', value: 'latest' }, - { title: '오래된순', value: 'oldest' }, - { title: '제목순', value: 'title' }, - { title: '조회수순', value: 'views' } + { title: '전체', value: 'all', color: 'primary', emoji: '📊' }, + { title: 'Instagram', value: 'instagram', color: 'pink', emoji: '📷' }, + { title: '네이버 블로그', value: 'blog', color: 'green', emoji: '📝' }, + { title: '포스터', value: 'poster', color: 'orange', emoji: '🎨' } ] const titleRules = [ diff --git a/src/views/StoreManagementView.vue b/src/views/StoreManagementView.vue index 07c27af..4a1250c 100644 --- a/src/views/StoreManagementView.vue +++ b/src/views/StoreManagementView.vue @@ -1,789 +1,404 @@ +