From 64427ef9c460269161979a6a228dead29df3dac3 Mon Sep 17 00:00:00 2001 From: SeoJHeasdw Date: Wed, 18 Jun 2025 15:57:24 +0900 Subject: [PATCH] release --- src/components/poster/PosterForm.vue | 407 +++++++++++++++++++++ src/components/poster/PosterPreview.vue | 318 ++++++++++++++++ src/services/content.js | 267 +++++++++----- src/store/poster.js | 281 +++++++++++++++ src/views/ContentCreationView.vue | 461 +++++++++++++++++------- src/views/PosterCreationView.vue | 235 ++++++++++++ 6 files changed, 1751 insertions(+), 218 deletions(-) create mode 100644 src/components/poster/PosterForm.vue create mode 100644 src/components/poster/PosterPreview.vue create mode 100644 src/store/poster.js create mode 100644 src/views/PosterCreationView.vue 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/content.js b/src/services/content.js index d700972..e45bf01 100644 --- a/src/services/content.js +++ b/src/services/content.js @@ -1,4 +1,4 @@ -//* src/services/content.js - 두 파일 완전 통합 버전 +//* src/services/content.js - 완전한 파일 (모든 수정사항 포함) import axios from 'axios' // runtime-env.js에서 API URL 가져오기 (대체 방식 포함) @@ -106,11 +106,6 @@ class ContentService { /** * 콘텐츠 목록 조회 (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 getContents(filters = {}) { @@ -182,7 +177,7 @@ class ContentService { } /** - * SNS 콘텐츠 생성 (CON-019: AI 콘텐츠 생성) + * SNS 콘텐츠 생성 (CON-019: AI 콘텐츠 생성) - 수정된 버전 * @param {Object} contentData - 콘텐츠 생성 데이터 * @returns {Promise} 생성된 콘텐츠 */ @@ -190,97 +185,171 @@ class ContentService { 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, '개') + // ✅ contentData 기본 검증 + if (!contentData || typeof contentData !== 'object') { + throw new Error('콘텐츠 데이터가 전달되지 않았습니다.') } - // ✅ 실제 전달받은 데이터만 사용 (백엔드 API 스펙에 맞춤) - const requestData = {} + // ✅ images 속성 보장 (방어 코드) + if (!contentData.hasOwnProperty('images')) { + console.warn('⚠️ [API] images 속성이 없음, 빈 배열로 설정') + contentData.images = [] + } - if (contentData.storeId !== undefined) requestData.storeId = contentData.storeId + 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.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 - // 이미지는 처리된 것으로 설정 + // ✅ 이미지 처리 (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, '개') - // 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}개 이미지` + // ✅ 최종 검증 + console.log('📝 [API] 최종 SNS 요청 데이터:', { + title: requestData.title, + platform: requestData.platform, + category: requestData.category, + contentType: requestData.contentType, + storeId: requestData.storeId, + imageCount: requestData.images.length }) - // 기본 유효성 검사 - if (!requestData.platform) { - throw new Error('플랫폼은 필수입니다.') - } + // ✅ 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 (!requestData.title) { - throw new Error('제목은 필수입니다.') + 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 // 30초 + 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 포스터 생성) - 이미지 처리 강화 및 상세 검증 + * 포스터 생성 (CON-020: AI 포스터 생성) - 수정된 버전 * @param {Object} posterData - 포스터 생성 데이터 * @returns {Promise} 생성된 포스터 */ @@ -532,15 +601,6 @@ class ContentService { 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, @@ -565,6 +625,37 @@ class ContentService { } } + /** + * 통합 콘텐츠 생성 (타입에 따라 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 - 저장할 콘텐츠 데이터 @@ -639,21 +730,6 @@ class ContentService { } } - /** - * 통합 콘텐츠 생성 (타입에 따라 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 - 조회 기간 @@ -662,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) @@ -677,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) @@ -708,7 +782,6 @@ class ContentService { if (updateData.images) requestData.images = updateData.images const response = await contentApi.put(`/${contentId}`, requestData) - return formatSuccessResponse(response.data.data, '콘텐츠가 수정되었습니다.') } catch (error) { return handleApiError(error) @@ -723,7 +796,6 @@ class ContentService { async deleteContent(contentId) { try { await contentApi.delete(`/${contentId}`) - return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.') } catch (error) { return handleApiError(error) @@ -866,6 +938,19 @@ class ContentService { 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) + } + } } // 서비스 인스턴스 생성 및 내보내기 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/views/ContentCreationView.vue b/src/views/ContentCreationView.vue index 370b9d8..b5338f7 100644 --- a/src/views/ContentCreationView.vue +++ b/src/views/ContentCreationView.vue @@ -1,4 +1,3 @@ -//* src/views/ContentCreationView.vue